Tabs.purs 14 KB

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