Tabs.purs 15 KB

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