Tabs.purs 15 KB

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