Tabs.purs 15 KB

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