Tabs.purs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. module PureTabs.Sidebar.Tabs (component, Query(..), Output(..), Slot) where
  2. import Browser.Tabs (Tab(..), TabId, showTabId)
  3. import Browser.Tabs.OnUpdated (ChangeInfo(..), ChangeInfoRec)
  4. import CSS.Background as CssBackground
  5. import Control.Alt ((<$>))
  6. import Control.Alternative (empty, pure)
  7. import Control.Bind (bind, discard, (>=>), (>>=))
  8. import Control.Category (identity, (<<<), (>>>))
  9. import Data.Array (catMaybes, deleteAt, filter, findIndex, head, insertAt, length, mapWithIndex, modifyAt) as A
  10. import Data.Eq ((/=), (==))
  11. import Data.Foldable (for_)
  12. import Data.Function (flip, ($))
  13. import Data.Maybe (Maybe(..), fromMaybe, maybe)
  14. import Data.MediaType.Common (textPlain)
  15. import Data.Monoid ((<>))
  16. import Data.Show (show)
  17. import Data.String.CodeUnits (length)
  18. import Data.Symbol (SProxy(..))
  19. import Data.Time.Duration (Milliseconds(..))
  20. import Data.Unit (Unit, unit)
  21. import Effect.AVar (AVar)
  22. import Effect.Aff (Aff, Fiber, forkAff, delay, killFiber)
  23. import Effect.Aff.AVar (put, empty, take) as AVar
  24. import Effect.Aff.Class (class MonadAff)
  25. import Effect.Class (class MonadEffect)
  26. import Effect.Class.Console (log)
  27. import Effect.Exception (error)
  28. import Halogen as H
  29. import Halogen.HTML as HH
  30. import Halogen.HTML.CSS as CSS
  31. import Halogen.HTML.Events as HE
  32. import Halogen.HTML.Properties as HP
  33. import Prelude (negate, sub)
  34. import PureTabs.Browser.Dom.Element (scrollIntoView)
  35. import PureTabs.Model.SidebarEvent (SidebarEvent(..))
  36. import Sidebar.Utils (moveElem)
  37. import Web.Event.Event (Event)
  38. import Web.Event.Event as Event
  39. import Web.HTML.Event.DataTransfer as DT
  40. import Web.HTML.Event.DragEvent as DE
  41. import Web.HTML.HTMLElement (toElement) as DOM
  42. import Web.UIEvent.MouseEvent as ME
  43. type Slot a = H.Slot Query Output a
  44. data Query a
  45. = InitialTabList (Array Tab) a
  46. | TabCreated Tab a
  47. | TabDeleted TabId (Maybe Tab -> a)
  48. | TabActivated (Maybe TabId) TabId a
  49. | TabMoved TabId Int a
  50. | TabInfoChanged TabId ChangeInfo a
  51. | TabDetached TabId a
  52. | TabAttached Tab a
  53. data Output
  54. = TabsSidebarAction SidebarEvent
  55. -- Nothing if we already did the move
  56. -- Just TabId in case the dragged ended somewhere else
  57. | OutputTabDragEnd (Maybe TabId)
  58. data Action
  59. = UserClosedTab TabId Event
  60. | UserActivatedTab TabId Event
  61. | UserOpenedTab (Maybe TabId) Event
  62. -- drags
  63. | TabDragStart DE.DragEvent Tab Int
  64. | TabDragOver DE.DragEvent Int
  65. | TabDragEnd DE.DragEvent
  66. | TabDragLeave DE.DragEvent
  67. | TabDragLeaveRun DE.DragEvent
  68. -- mouse event
  69. | TabMouseEnter ME.MouseEvent Int
  70. | TabMouseLeave ME.MouseEvent Int
  71. -- special
  72. -- stop the propagation of the event
  73. | PreventPropagation Event
  74. type DraggedTab
  75. = { tab :: Tab
  76. , originalIndex :: Int
  77. , overIndex :: Maybe Int
  78. }
  79. -- Debouncer based on https://gist.github.com/natefaubion/3405f930b9008e52e5d995681a7d6f2b
  80. type Debouncer
  81. = { var :: AVar Unit
  82. , timer :: Fiber Unit
  83. }
  84. type State
  85. = { tabs :: Array Tab
  86. , selectedElem :: Maybe DraggedTab
  87. , tabHovered :: Maybe Int
  88. , leaveDebounce :: Maybe Debouncer
  89. }
  90. type TabProperties
  91. = { isActive :: Boolean
  92. , isDiscarded :: Boolean
  93. , isBeingDragged :: Boolean
  94. , isHovered :: Boolean
  95. }
  96. getTabProperties
  97. :: forall r.
  98. Tab
  99. -> Int
  100. -> { selectedElem :: Maybe DraggedTab, tabHovered :: Maybe Int | r }
  101. -> TabProperties
  102. getTabProperties (Tab t) index props =
  103. let
  104. isBeingDragged = fromMaybe false $ do
  105. dt <- props.selectedElem
  106. overIndex <- dt.overIndex
  107. Just $ overIndex == index
  108. in
  109. { isActive: t.active
  110. , isDiscarded: fromMaybe false t.discarded
  111. , isBeingDragged: isBeingDragged
  112. , isHovered: maybe false ((==) index) props.tabHovered
  113. }
  114. component :: forall i m. MonadEffect m => MonadAff m => H.Component HH.HTML Query i Output m
  115. component =
  116. H.mkComponent
  117. { initialState
  118. , render: render
  119. , eval:
  120. H.mkEval
  121. $ H.defaultEval
  122. { handleQuery = handleQuery
  123. , handleAction = handleAction
  124. }
  125. }
  126. initialState :: forall i. i -> State
  127. initialState _ =
  128. { tabs: empty
  129. , selectedElem: Nothing
  130. , tabHovered: Nothing
  131. , leaveDebounce: Nothing
  132. }
  133. debounceTimeout :: Milliseconds -> AVar Unit -> Aff (Fiber Unit)
  134. debounceTimeout ms var =
  135. forkAff do
  136. delay ms
  137. AVar.put unit var
  138. _tab :: SProxy "tab"
  139. _tab = SProxy
  140. tabContainerRef :: H.RefLabel
  141. tabContainerRef = H.RefLabel "tab-container"
  142. getTabRef :: TabId -> H.RefLabel
  143. getTabRef tid = H.RefLabel $ "tab-" <> show tid
  144. render :: forall m. State -> H.ComponentHTML Action () m
  145. render state =
  146. let
  147. tabsWithIndex = state.tabs
  148. tabs =
  149. fromMaybe tabsWithIndex
  150. $ state.selectedElem
  151. >>= ( \{ originalIndex, overIndex } -> case overIndex of
  152. Just overIndex' -> moveElem originalIndex overIndex' tabsWithIndex
  153. Nothing -> A.deleteAt originalIndex tabsWithIndex
  154. )
  155. currentOverIndex = fromMaybe (-1) $ state.selectedElem >>= _.overIndex
  156. in
  157. HH.div
  158. [ HP.class_ $ H.ClassName "tabs"
  159. , HE.onDoubleClick (\ev -> Just (UserOpenedTab Nothing (ME.toEvent ev)))
  160. , HE.onDragOver \evt -> Just $ TabDragOver evt (sub (A.length tabs) 1)
  161. , HE.onDragLeave \evt -> Just $ TabDragLeave evt
  162. ]
  163. [ HH.div
  164. [ HP.class_ $ H.ClassName "inner-tabs"
  165. , HP.ref tabContainerRef
  166. -- We prevent both propagation to avoid tabs blinking during drag and
  167. -- drop. In the case of dragOver, the handler from #tabs triggers
  168. -- when we drag over between two tabs (because of the margin), and
  169. -- the tab jumps brefiely to the end.
  170. -- The same happens for dragLeave, but with the tab disappearing
  171. -- brefiely.
  172. , HE.onDragOver \evt -> Just $ PreventPropagation $ DE.toEvent evt
  173. , HE.onDragLeave \evt -> Just $ TabDragLeave evt
  174. ]
  175. (A.mapWithIndex (\idx tab ->
  176. renderTab idx (getTabProperties tab idx state) tab
  177. ) tabs)
  178. ]
  179. where
  180. threeDotBounces = HH.div [ HP.class_ (H.ClassName "three-dot-bounce") ] [
  181. HH.div [HP.class_ (H.ClassName "three-dot-bounce-1")] [],
  182. HH.div [HP.class_ (H.ClassName "three-dot-bounce-2")] [],
  183. HH.div [HP.class_ (H.ClassName "three-dot-bounce-3")] []
  184. ]
  185. renderTab :: Int -> TabProperties -> Tab -> H.ComponentHTML Action () m
  186. renderTab index props (Tab t) =
  187. HH.div
  188. [ HP.id_ $ show t.id
  189. , HP.ref $ getTabRef t.id
  190. , HP.draggable true
  191. -- drag events
  192. , HE.onDragStart \evt -> Just $ TabDragStart evt (Tab t) index
  193. , HE.onDragEnd \evt -> Just $ TabDragEnd evt
  194. , HE.onDragOver \evt -> Just $ TabDragOver evt index
  195. -- fake hover to fix incorrect css hover effect during dragging
  196. , HE.onMouseEnter \evt -> Just $ TabMouseEnter evt index
  197. , HE.onMouseLeave \evt -> Just $ TabMouseLeave evt index
  198. -- click event
  199. , HE.onClick (\ev -> Just (UserActivatedTab t.id (ME.toEvent ev)))
  200. , HE.onDoubleClick (\ev -> Just (UserOpenedTab (Just t.id) (ME.toEvent ev)))
  201. -- classes
  202. , HP.classes $ H.ClassName
  203. <$> A.catMaybes
  204. [ Just "tab"
  205. , if props.isActive then Just "active" else Nothing
  206. , if props.isDiscarded then Just "discarded" else Nothing
  207. , if props.isBeingDragged then Just "being-dragged" else Nothing
  208. , if props.isHovered then Just "hover" else Nothing
  209. ]
  210. , HP.title t.title
  211. ] [
  212. case t.status of
  213. Just "loading" -> threeDotBounces
  214. _ -> HH.div [ HP.class_ $ H.ClassName "tab-favicon", faviconStyle t.favIconUrl ] []
  215. , HH.div [ HP.class_ $ H.ClassName "tab-title" ] [ HH.text (if length t.title /= 0 then t.title else maybe "" identity t.url) ]
  216. , HH.div
  217. [ HP.class_ $ H.ClassName "close-button-parent"
  218. , HE.onClick (\ev -> Just (UserClosedTab t.id (ME.toEvent ev)))
  219. ]
  220. [ HH.div [ HP.class_ $ H.ClassName "close-button-outer" ]
  221. [ HH.div [ HP.class_ $ H.ClassName "close-button-inner" ] []
  222. ]
  223. ]
  224. ]
  225. faviconStyle favicon' =
  226. CSS.style
  227. $ do
  228. case favicon' of
  229. Nothing -> pure unit
  230. Just favicon -> CssBackground.backgroundImage $ CssBackground.url favicon
  231. cancelLeaveDebounce :: forall m. MonadAff m => State -> H.HalogenM State Action () Output m Unit
  232. cancelLeaveDebounce state = case state.leaveDebounce of
  233. Just { var, timer } -> do
  234. H.liftAff $ killFiber (error "could not cancel timer") timer
  235. H.modify_ _ { leaveDebounce = Nothing }
  236. Nothing -> pure unit
  237. runDebounce :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit
  238. runDebounce actionToRun = do
  239. state <- H.get
  240. let
  241. debounceTime = Milliseconds 50.0
  242. case state.leaveDebounce of
  243. Nothing -> do
  244. var <- H.liftAff AVar.empty
  245. timer <- H.liftAff (debounceTimeout debounceTime var)
  246. _ <-
  247. H.fork do
  248. H.liftAff (AVar.take var)
  249. H.modify_ _ { leaveDebounce = Nothing }
  250. handleAction actionToRun
  251. let
  252. debouncer = { var, timer }
  253. H.modify_ _ { leaveDebounce = Just debouncer }
  254. Just { var, timer } -> do
  255. H.liftAff $ killFiber (error "could not cancel timer") timer
  256. nextTimer <- H.liftAff (debounceTimeout debounceTime var)
  257. let
  258. debouncer = { var, timer: nextTimer }
  259. H.modify_ _ { leaveDebounce = Just debouncer }
  260. handleAction :: forall m. MonadEffect m => MonadAff m => Action -> H.HalogenM State Action () Output m Unit
  261. handleAction = case _ of
  262. UserClosedTab tid ev -> do
  263. H.liftEffect
  264. $ do
  265. Event.preventDefault ev
  266. Event.stopPropagation ev
  267. log "sb: closed a tab"
  268. H.raise $ TabsSidebarAction $ SbDeleteTab tid
  269. UserActivatedTab tid ev -> do
  270. H.liftEffect
  271. $ do
  272. Event.preventDefault ev
  273. Event.stopPropagation ev
  274. log "sb: activated a tab"
  275. H.raise $ TabsSidebarAction $ SbActivateTab tid
  276. UserOpenedTab tid ev -> do
  277. H.liftEffect
  278. $ do
  279. Event.preventDefault ev
  280. Event.stopPropagation ev
  281. log "sb: created a tab"
  282. H.raise $ TabsSidebarAction $ SbCreateTab tid
  283. -- Drag actions
  284. TabDragStart dragEvent tab index -> do
  285. let
  286. dataTransfer = DE.dataTransfer dragEvent
  287. H.liftEffect
  288. $ do
  289. DT.setData textPlain (showTabId tab) dataTransfer
  290. DT.setDropEffect DT.Move dataTransfer
  291. H.modify_ _ { selectedElem = Just { tab: tab, originalIndex: index, overIndex: Just index }, tabHovered = Nothing }
  292. H.liftEffect $ log $ "sb: drag start from " <> (show index)
  293. TabDragOver event index -> do
  294. -- prevent the ghost from flying back to its (wrong) place
  295. -- see https://stackoverflow.com/questions/42725321/prevent-html5-drag-ghost-image-flying-back
  296. let
  297. evt = (DE.toEvent event)
  298. H.liftEffect $ Event.preventDefault evt
  299. -- because we're also triggering this event on over of the empty part of the
  300. -- tab list, we need to prevent it from triggering twice.
  301. H.liftEffect $ Event.stopPropagation evt
  302. state <- H.get
  303. cancelLeaveDebounce state
  304. case state.selectedElem of
  305. Just selectedRec@{ originalIndex, overIndex } -> case overIndex of
  306. -- we only do nothing if we're still over the same element
  307. Just overIndex'
  308. | overIndex' == index -> pure unit
  309. _ -> H.modify_ (_ { selectedElem = Just $ selectedRec { overIndex = Just index } })
  310. Nothing -> pure unit
  311. PreventPropagation event -> do
  312. H.liftEffect $ Event.stopImmediatePropagation event
  313. pure unit
  314. TabDragEnd event -> do
  315. state <- H.get
  316. cancelLeaveDebounce state
  317. case state.selectedElem of
  318. Nothing -> pure unit
  319. -- On success, we don't remove the dragged element here. It is instead done in the
  320. -- query handler for TabMoved. See comment there for the explanation.
  321. Just { tab: (Tab t), originalIndex, overIndex: (Just overIndex) } -> do
  322. H.raise $ TabsSidebarAction (SbMoveTab t.id overIndex)
  323. H.raise $ OutputTabDragEnd Nothing
  324. H.liftEffect $ log "sb: drag end (asking to do a move)"
  325. Just { tab: (Tab t), overIndex: Nothing } -> do
  326. H.modify_ _ { selectedElem = Nothing }
  327. H.raise $ OutputTabDragEnd $ Just t.id
  328. H.liftEffect $ log "sb: drag end (doing nothing)"
  329. TabDragLeave event -> runDebounce $ TabDragLeaveRun event
  330. TabDragLeaveRun event -> do
  331. state <- H.get
  332. case state.selectedElem of
  333. Just selectedRec@{ overIndex: (Just overIndex) } -> H.modify_ _ { selectedElem = Just $ selectedRec { overIndex = Nothing } }
  334. _ -> pure unit
  335. -- Mouse over action
  336. TabMouseEnter evt index -> do
  337. state <- H.get
  338. case state of
  339. { tabHovered: Nothing, selectedElem: Nothing } -> H.modify_ _ { tabHovered = Just index }
  340. _ -> pure unit
  341. TabMouseLeave evt index -> do
  342. state <- H.get
  343. case state.tabHovered of
  344. Nothing -> pure unit
  345. Just prevIdx -> H.modify_ _ { tabHovered = Nothing }
  346. handleQuery :: forall act o m a. MonadEffect m => Query a -> H.HalogenM State act () o m (Maybe a)
  347. handleQuery = case _ of
  348. InitialTabList tabs a -> do
  349. H.modify_ _ { tabs = tabs }
  350. pure (Just a)
  351. TabCreated (Tab t) a -> do
  352. H.modify_ \s ->
  353. s { tabs = fromMaybe s.tabs $ A.insertAt t.index (Tab t) s.tabs}
  354. pure (Just a)
  355. TabDeleted tid reply -> do
  356. { tabs } <- H.get
  357. let deletedTab = findTabByTabId tid tabs
  358. H.modify_ \s -> s { tabs = applyAtTabId tid A.deleteAt s.tabs}
  359. pure (Just (reply deletedTab))
  360. TabActivated prevTid tid a -> do
  361. let
  362. updateTabs = maybe identity (\old -> applyAtTabId old $ setTabActiveAtIndex false) prevTid
  363. >>> applyAtTabId tid (setTabActiveAtIndex true)
  364. H.modify_ \s -> s { tabs = updateTabs s.tabs }
  365. scrollToTab tid
  366. pure (Just a)
  367. TabMoved tid next a -> do
  368. H.modify_ \s ->
  369. let
  370. newTabs = do
  371. tabPosition <- A.findIndex (\(Tab t) -> t.id == tid) s.tabs
  372. moveElem tabPosition next s.tabs
  373. in
  374. -- Regarding `selectedElem = Nothing`:
  375. -- Wait for a move to disable the drag data, otherwise the tab will come
  376. -- back briefly to its original place before switching again.
  377. -- This also means that if the move fail, this will be in an inconsistant
  378. -- state.
  379. s { tabs = fromMaybe s.tabs newTabs, selectedElem = Nothing}
  380. pure (Just a)
  381. TabInfoChanged tid cinfo a -> do
  382. H.modify_ \s ->
  383. s { tabs =
  384. fromMaybe s.tabs $
  385. (findIndexTabId tid >=> \index -> A.modifyAt index (updateTabFromInfo cinfo) s.tabs) s.tabs
  386. }
  387. pure (Just a)
  388. TabDetached tid reply ->
  389. handleQuery $ TabDeleted tid \_ -> reply
  390. TabAttached tab a -> do
  391. H.liftEffect (log $ "sb: tab attached " <> (showTabId tab))
  392. handleQuery $ TabCreated tab a
  393. setTabActive :: Boolean -> Tab -> Tab
  394. setTabActive act (Tab t) = Tab (t { active = act })
  395. setTabActiveAtIndex :: Boolean -> Int -> Array Tab -> Maybe (Array Tab)
  396. setTabActiveAtIndex act i = A.modifyAt i (setTabActive act)
  397. findTabByTabId :: TabId -> Array Tab -> Maybe Tab
  398. findTabByTabId tid = A.head <<< A.filter \(Tab t) -> t.id == tid
  399. findIndexTabId :: TabId -> Array Tab -> Maybe Int
  400. findIndexTabId tid = A.findIndex \(Tab t) -> t.id == tid
  401. applyAtTabId :: TabId -> (Int -> Array Tab -> Maybe (Array Tab)) -> Array Tab -> Array Tab
  402. applyAtTabId tid f a = fromMaybe a $ findIndexTabId tid a >>= (flip f) a
  403. updateTabFromInfo :: ChangeInfo -> Tab -> Tab
  404. updateTabFromInfo (ChangeInfo cinfo) (Tab t) =
  405. let
  406. updateField :: forall r a. { acc :: ChangeInfoRec -> Maybe a, update :: a -> r -> r } -> r -> r
  407. updateField { acc, update } tab = case acc cinfo of
  408. Nothing -> tab
  409. Just field -> update field tab
  410. applyChange =
  411. updateField { acc: _.title, update: (\val -> _ { title = val }) }
  412. >>> updateField { acc: _.status, update: (\val -> _ { status = Just val }) }
  413. >>> updateField { acc: _.discarded, update: (\val -> _ { discarded = Just val }) }
  414. >>> updateField { acc: _.url, update: (\val -> _ { url = Just val }) }
  415. >>> updateField { acc: _.pinned, update: (\val -> _ { pinned = val }) }
  416. >>> updateField { acc: _.hidden, update: (\val -> _ { hidden = val }) }
  417. >>> updateField { acc: _.favIconUrl, update: (\val -> _ { favIconUrl = Just val }) }
  418. in
  419. Tab (applyChange t)
  420. scrollToTab
  421. :: forall state action input output monad
  422. . MonadEffect monad
  423. => TabId
  424. -> H.HalogenM state action input output monad Unit
  425. scrollToTab tid = do
  426. ref <- H.getHTMLElementRef $ getTabRef tid
  427. for_ ref \el -> H.liftEffect $ scrollIntoView $ DOM.toElement el