Tabs.purs 16 KB

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