| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- module PureTabs.Sidebar.Tabs (component, Query(..), Output(..)) where
- import Browser.Tabs (Tab(..), TabId, showTabId)
- import Browser.Tabs.OnUpdated (ChangeInfo(..), ChangeInfoRec)
- import CSS.Background as CssBackground
- import Control.Alt ((<$>))
- import Control.Alternative (empty, pure)
- import Control.Bind (bind, discard, (>=>), (>>=))
- import Control.Category (identity, (<<<), (>>>))
- import Data.Array (catMaybes, deleteAt, filter, findIndex, head, insertAt, length, mapWithIndex, modifyAt) as A
- import Data.Eq ((/=), (==))
- import Data.Function (flip, ($))
- import Data.Maybe (Maybe(..), fromMaybe, maybe)
- import Data.MediaType.Common (textPlain)
- import Data.Monoid ((<>))
- import Data.Show (show)
- import Data.String.CodeUnits (length)
- import Data.Symbol (SProxy(..))
- import Data.Time.Duration (Milliseconds(..))
- import Data.Unit (Unit, unit)
- import Effect.AVar (AVar)
- import Effect.Aff (Aff, Fiber, forkAff, delay, killFiber)
- import Effect.Aff.AVar (put, empty, take) as AVar
- import Effect.Aff.Class (class MonadAff)
- import Effect.Class (class MonadEffect)
- import Effect.Class.Console (log)
- import Effect.Exception (error)
- import Halogen as H
- import Halogen.HTML as HH
- import Halogen.HTML.CSS as CSS
- import Halogen.HTML.Events as HE
- import Halogen.HTML.Properties as HP
- import Prelude (negate, sub)
- import PureTabs.Model.Events (SidebarEvent(..))
- import Sidebar.Utils (moveElem)
- import Web.Event.Event (Event)
- import Web.Event.Event as Event
- import Web.HTML.Event.DataTransfer as DT
- import Web.HTML.Event.DragEvent as DE
- import Web.UIEvent.MouseEvent as ME
- data Query a
- = InitialTabList (Array Tab) a
- | TabCreated Tab a
- | TabDeleted TabId a
- | TabActivated (Maybe TabId) TabId a
- | TabMoved TabId Int a
- | TabInfoChanged TabId ChangeInfo a
- | TabDetached TabId a
- | TabAttached Tab a
- data Output
- = TabsSidebarAction SidebarEvent
- data Action
- = UserClosedTab TabId Event
- | UserActivatedTab TabId Event
- | UserOpenedTab (Maybe TabId) Event
- -- drags
- | TabDragStart DE.DragEvent Tab Int
- | TabDragOver DE.DragEvent Int
- | TabDragEnd DE.DragEvent
- | TabDragLeave DE.DragEvent
- | TabDragLeaveRun DE.DragEvent
- -- mouse event
- | TabMouseEnter ME.MouseEvent Int
- | TabMouseLeave ME.MouseEvent Int
- -- special
- -- stop the propagation of the event
- | PreventPropagation Event
- type DraggedTab
- = { tab :: Tab
- , originalIndex :: Int
- , overIndex :: Maybe Int
- }
- -- Debouncer based on https://gist.github.com/natefaubion/3405f930b9008e52e5d995681a7d6f2b
- type Debouncer
- = { var :: AVar Unit
- , timer :: Fiber Unit
- }
- type State
- = { tabs :: Array Tab
- , selectedElem :: Maybe DraggedTab
- , tabHovered :: Maybe Int
- , leaveDebounce :: Maybe Debouncer
- }
- type TabProperties
- = { isActive :: Boolean
- , isDiscarded :: Boolean
- , isBeingDragged :: Boolean
- , isHovered :: Boolean
- }
- getTabProperties
- :: forall r.
- Tab
- -> Int
- -> { selectedElem :: Maybe DraggedTab, tabHovered :: Maybe Int | r }
- -> TabProperties
- getTabProperties (Tab t) index props =
- let
- isBeingDragged = fromMaybe false $ do
- dt <- props.selectedElem
- overIndex <- dt.overIndex
- Just $ overIndex == index
- in
- { isActive: t.active
- , isDiscarded: fromMaybe false t.discarded
- , isBeingDragged: isBeingDragged
- , isHovered: maybe false ((==) index) props.tabHovered
- }
- component :: forall i m. MonadEffect m => MonadAff m => H.Component HH.HTML Query i Output m
- component =
- H.mkComponent
- { initialState
- , render: render
- , eval:
- H.mkEval
- $ H.defaultEval
- { handleQuery = handleQuery
- , handleAction = handleAction
- }
- }
- initialState :: forall i. i -> State
- initialState _ =
- { tabs: empty
- , selectedElem: Nothing
- , tabHovered: Nothing
- , leaveDebounce: Nothing
- }
- debounceTimeout :: Milliseconds -> AVar Unit -> Aff (Fiber Unit)
- debounceTimeout ms var =
- forkAff do
- delay ms
- AVar.put unit var
- _tab :: SProxy "tab"
- _tab = SProxy
- render :: forall m. State -> H.ComponentHTML Action () m
- render state =
- let
- tabsWithIndex = state.tabs
- tabs =
- fromMaybe tabsWithIndex
- $ state.selectedElem
- >>= ( \{ originalIndex, overIndex } -> case overIndex of
- Just overIndex' -> moveElem originalIndex overIndex' tabsWithIndex
- Nothing -> A.deleteAt originalIndex tabsWithIndex
- )
- currentOverIndex = fromMaybe (-1) $ state.selectedElem >>= _.overIndex
- in
- HH.div
- [ HP.id_ "tabs"
- , HE.onDoubleClick (\ev -> Just (UserOpenedTab Nothing (ME.toEvent ev)))
- , HE.onDragOver \evt -> Just $ TabDragOver evt (sub (A.length tabs) 1)
- , HE.onDragLeave \evt -> Just $ TabDragLeave evt
- ]
- [ HH.div
- [ HP.id_ "inner-tabs"
- -- We prevent both propagation to avoid tabs blinking during drag and
- -- drop. In the case of dragOver, the handler from #tabs triggers
- -- when we drag over between two tabs (because of the margin), and
- -- the tab jumps brefiely to the end.
- -- The same happens for dragLeave, but with the tab disappearing
- -- brefiely.
- , HE.onDragOver \evt -> Just $ PreventPropagation $ DE.toEvent evt
- , HE.onDragLeave \evt -> Just $ TabDragLeave evt
- ]
- (A.mapWithIndex (\idx tab ->
- renderTab idx (getTabProperties tab idx state) tab
- ) tabs)
- ]
- where
- threeDotBounces = HH.div [ HP.class_ (H.ClassName "three-dot-bounce") ] [
- HH.div [HP.class_ (H.ClassName "three-dot-bounce-1")] [],
- HH.div [HP.class_ (H.ClassName "three-dot-bounce-2")] [],
- HH.div [HP.class_ (H.ClassName "three-dot-bounce-3")] []
- ]
- renderTab :: Int -> TabProperties -> Tab -> H.ComponentHTML Action () m
- renderTab index props (Tab t) =
- HH.div
- [ HP.id_ $ show t.id
- , HP.draggable true
- -- drag events
- , HE.onDragStart \evt -> Just $ TabDragStart evt (Tab t) index
- , HE.onDragEnd \evt -> Just $ TabDragEnd evt
- , HE.onDragOver \evt -> Just $ TabDragOver evt index
- -- fake hover to fix incorrect css hover effect during dragging
- , HE.onMouseEnter \evt -> Just $ TabMouseEnter evt index
- , HE.onMouseLeave \evt -> Just $ TabMouseLeave evt index
- -- click event
- , HE.onClick (\ev -> Just (UserActivatedTab t.id (ME.toEvent ev)))
- , HE.onDoubleClick (\ev -> Just (UserOpenedTab (Just t.id) (ME.toEvent ev)))
- -- classes
- , HP.classes $ H.ClassName
- <$> A.catMaybes
- [ Just "tab"
- , if props.isActive then Just "active" else Nothing
- , if props.isDiscarded then Just "discarded" else Nothing
- , if props.isBeingDragged then Just "being-dragged" else Nothing
- , if props.isHovered then Just "hover" else Nothing
- ]
- , HP.title t.title
- ] [
- case t.status of
- Just "loading" -> threeDotBounces
- _ -> HH.div [ HP.class_ $ H.ClassName "tab-favicon", faviconStyle t.favIconUrl ] []
- , HH.div [ HP.class_ $ H.ClassName "tab-title" ] [ HH.text (if length t.title /= 0 then t.title else maybe "" identity t.url) ]
- , HH.div
- [ HP.class_ $ H.ClassName "close-button-parent"
- , HE.onClick (\ev -> Just (UserClosedTab t.id (ME.toEvent ev)))
- ]
- [ HH.div [ HP.class_ $ H.ClassName "close-button-outer" ]
- [ HH.div [ HP.class_ $ H.ClassName "close-button-inner" ] []
- ]
- ]
- ]
- faviconStyle favicon' =
- CSS.style
- $ do
- case favicon' of
- Nothing -> pure unit
- Just favicon -> CssBackground.backgroundImage $ CssBackground.url favicon
- cancelLeaveDebounce :: forall m. MonadAff m => State -> H.HalogenM State Action () Output m Unit
- cancelLeaveDebounce state = case state.leaveDebounce of
- Just { var, timer } -> do
- H.liftAff $ killFiber (error "could not cancel timer") timer
- H.modify_ _ { leaveDebounce = Nothing }
- Nothing -> pure unit
- runDebounce :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit
- runDebounce actionToRun = do
- state <- H.get
- let
- debounceTime = Milliseconds 50.0
- case state.leaveDebounce of
- Nothing -> do
- var <- H.liftAff AVar.empty
- timer <- H.liftAff (debounceTimeout debounceTime var)
- _ <-
- H.fork do
- H.liftAff (AVar.take var)
- H.modify_ _ { leaveDebounce = Nothing }
- handleAction actionToRun
- let
- debouncer = { var, timer }
- H.modify_ _ { leaveDebounce = Just debouncer }
- Just { var, timer } -> do
- H.liftAff $ killFiber (error "could not cancel timer") timer
- nextTimer <- H.liftAff (debounceTimeout debounceTime var)
- let
- debouncer = { var, timer: nextTimer }
- H.modify_ _ { leaveDebounce = Just debouncer }
- handleAction :: forall m. MonadEffect m => MonadAff m => Action -> H.HalogenM State Action () Output m Unit
- handleAction = case _ of
- UserClosedTab tid ev -> do
- H.liftEffect
- $ do
- Event.preventDefault ev
- Event.stopPropagation ev
- log "sb: closed a tab"
- H.raise $ TabsSidebarAction $ SbDeleteTab tid
- UserActivatedTab tid ev -> do
- H.liftEffect
- $ do
- Event.preventDefault ev
- Event.stopPropagation ev
- log "sb: activated a tab"
- H.raise $ TabsSidebarAction $ SbActivateTab tid
- UserOpenedTab tid ev -> do
- H.liftEffect
- $ do
- Event.preventDefault ev
- Event.stopPropagation ev
- log "sb: created a tab"
- H.raise $ TabsSidebarAction $ SbCreateTab tid
- -- Drag actions
- TabDragStart dragEvent tab index -> do
- let
- dataTransfer = DE.dataTransfer dragEvent
- H.liftEffect
- $ do
- DT.setData textPlain "" dataTransfer
- DT.setDropEffect DT.Move dataTransfer
- H.modify_ _ { selectedElem = Just { tab: tab, originalIndex: index, overIndex: Just index }, tabHovered = Nothing }
- H.liftEffect $ log $ "sb: drag start from " <> (show index)
- TabDragOver event index -> do
- -- prevent the ghost from flying back to its (wrong) place
- -- see https://stackoverflow.com/questions/42725321/prevent-html5-drag-ghost-image-flying-back
- let
- evt = (DE.toEvent event)
- H.liftEffect $ Event.preventDefault evt
- -- because we're also triggering this event on over of the empty part of the
- -- tab list, we need to prevent it from triggering twice.
- H.liftEffect $ Event.stopPropagation evt
- state <- H.get
- cancelLeaveDebounce state
- case state.selectedElem of
- Just selectedRec@{ originalIndex, overIndex } -> case overIndex of
- -- we only do nothing if we're still over the same element
- Just overIndex'
- | overIndex' == index -> pure unit
- _ -> H.modify_ (_ { selectedElem = Just $ selectedRec { overIndex = Just index } })
- Nothing -> pure unit
- PreventPropagation event -> do
- H.liftEffect $ Event.stopImmediatePropagation event
- pure unit
- TabDragEnd event -> do
- state <- H.get
- cancelLeaveDebounce state
- case state.selectedElem of
- Nothing -> pure unit
- -- On success, we don't remove the dragged element here. It is instead done in the
- -- query handler for TabMoved. See comment there for the explanation.
- Just { tab: (Tab t), originalIndex, overIndex: (Just overIndex) } -> H.raise $ TabsSidebarAction (SbMoveTab t.id overIndex)
- Just { overIndex: Nothing } -> H.modify_ _ { selectedElem = Nothing }
- TabDragLeave event -> runDebounce $ TabDragLeaveRun event
- TabDragLeaveRun event -> do
- state <- H.get
- H.liftEffect $ log "actually running drag leave"
- case state.selectedElem of
- Just selectedRec@{ overIndex: (Just overIndex) } -> H.modify_ _ { selectedElem = Just $ selectedRec { overIndex = Nothing } }
- _ -> pure unit
- -- Mouse over action
- TabMouseEnter evt index -> do
- state <- H.get
- case state of
- { tabHovered: Nothing, selectedElem: Nothing } -> H.modify_ _ { tabHovered = Just index }
- _ -> pure unit
- TabMouseLeave evt index -> do
- state <- H.get
- case state.tabHovered of
- Nothing -> pure unit
- Just prevIdx -> H.modify_ _ { tabHovered = Nothing }
- handleQuery :: forall act o m a. MonadEffect m => Query a -> H.HalogenM State act () o m (Maybe a)
- handleQuery = case _ of
- InitialTabList tabs a -> do
- H.modify_ _ { tabs = tabs }
- pure (Just a)
- TabCreated (Tab t) a -> do
- H.modify_ \s ->
- s { tabs = fromMaybe s.tabs $ A.insertAt t.index (Tab t) s.tabs}
- pure (Just a)
- TabDeleted tid a -> do
- H.modify_ \s -> s { tabs = applyAtTabId tid A.deleteAt s.tabs}
- pure (Just a)
- TabActivated prevTid tid a -> do
- let
- updateTabs = maybe identity (\old -> applyAtTabId old $ setTabActiveAtIndex false) prevTid
- >>> applyAtTabId tid (setTabActiveAtIndex true)
- H.modify_ \s -> s { tabs = updateTabs s.tabs }
- pure (Just a)
- TabMoved tid next a -> do
- H.modify_ \s ->
- let
- newTabs = do
- tabPosition <- A.findIndex (\(Tab t) -> t.id == tid) s.tabs
- moveElem tabPosition next s.tabs
- in
- -- Regarding `selectedElem = Nothing`:
- -- Wait for a move to disable the drag data, otherwise the tab will come
- -- back briefly to its original place before switching again.
- -- This also means that if the move fail, this will be in an inconsistant
- -- state.
- s { tabs = fromMaybe s.tabs newTabs, selectedElem = Nothing}
- pure (Just a)
- TabInfoChanged tid cinfo a -> do
- H.modify_ \s ->
- s { tabs =
- fromMaybe s.tabs $
- (findIndexTabId tid >=> \index -> A.modifyAt index (updateTabFromInfo cinfo) s.tabs) s.tabs
- }
- pure (Just a)
- TabDetached tid a ->
- handleQuery $ TabDeleted tid a
- TabAttached tab a -> do
- H.liftEffect (log $ "sb: tab attached " <> (showTabId tab))
- handleQuery $ TabCreated tab a
- setTabActive :: Boolean -> Tab -> Tab
- setTabActive act (Tab t) = Tab (t { active = act })
- setTabActiveAtIndex :: Boolean -> Int -> Array Tab -> Maybe (Array Tab)
- setTabActiveAtIndex act i = A.modifyAt i (setTabActive act)
- findTabByTabId :: TabId -> Array Tab -> Maybe Tab
- findTabByTabId tid = A.head <<< A.filter \(Tab t) -> t.id == tid
- findIndexTabId :: TabId -> Array Tab -> Maybe Int
- findIndexTabId tid = A.findIndex \(Tab t) -> t.id == tid
- applyAtTabId :: TabId -> (Int -> Array Tab -> Maybe (Array Tab)) -> Array Tab -> Array Tab
- applyAtTabId tid f a = fromMaybe a $ findIndexTabId tid a >>= (flip f) a
- updateTabFromInfo :: ChangeInfo -> Tab -> Tab
- updateTabFromInfo (ChangeInfo cinfo) (Tab t) =
- let
- updateField :: forall r a. { acc :: ChangeInfoRec -> Maybe a, update :: a -> r -> r } -> r -> r
- updateField { acc, update } tab = case acc cinfo of
- Nothing -> tab
- Just field -> update field tab
- applyChange =
- updateField { acc: _.title, update: (\val -> _ { title = val }) }
- >>> updateField { acc: _.status, update: (\val -> _ { status = Just val }) }
- >>> updateField { acc: _.discarded, update: (\val -> _ { discarded = Just val }) }
- >>> updateField { acc: _.url, update: (\val -> _ { url = Just val }) }
- >>> updateField { acc: _.pinned, update: (\val -> _ { pinned = val }) }
- >>> updateField { acc: _.hidden, update: (\val -> _ { hidden = val }) }
- >>> updateField { acc: _.favIconUrl, update: (\val -> _ { favIconUrl = Just val }) }
- in
- Tab (applyChange t)
|