Tabs.purs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. module PureTabs.Sidebar.Tabs (component, Query(..)) 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, (!!)) 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.Unit (Unit, unit)
  19. import Effect.Class (class MonadEffect)
  20. import Effect.Class.Console (log)
  21. import Halogen as H
  22. import Halogen.HTML as HH
  23. import Halogen.HTML.CSS as CSS
  24. import Halogen.HTML.Events as HE
  25. import Halogen.HTML.Properties as HP
  26. import PureTabs.Model (SidebarEvent(..), _tabs)
  27. import Web.Event.Event (Event)
  28. import Web.Event.Event as Event
  29. import Web.HTML.Event.DataTransfer as DT
  30. import Web.HTML.Event.DragEvent as DE
  31. import Web.UIEvent.MouseEvent as ME
  32. data Query a
  33. = InitialTabList (Array Tab) a
  34. | TabCreated Tab a
  35. | TabDeleted TabId a
  36. | TabActivated (Maybe TabId) TabId a
  37. | TabMoved TabId Int Int a
  38. | TabInfoChanged TabId ChangeInfo a
  39. data Action
  40. = UserClosedTab TabId Event
  41. | UserActivatedTab TabId Event
  42. | UserOpenedTab (Maybe TabId) Event
  43. -- drags
  44. | TabDragStart DE.DragEvent Tab Int
  45. | TabDragOver DE.DragEvent Int
  46. | TabDragEnd DE.DragEvent
  47. -- mouse event
  48. | TabMouseEnter ME.MouseEvent Int
  49. | TabMouseLeave ME.MouseEvent Int
  50. type DraggedTab
  51. = { tab :: Tab
  52. , originalIndex :: Int
  53. , overIndex :: Int
  54. }
  55. type State
  56. = { tabs :: Array Tab
  57. , selectedElem :: Maybe DraggedTab
  58. , tabHovered :: Maybe Int
  59. }
  60. component :: forall i m. MonadEffect m => H.Component HH.HTML Query i SidebarEvent m
  61. component =
  62. H.mkComponent
  63. { initialState
  64. , render: render
  65. , eval:
  66. H.mkEval
  67. $ H.defaultEval
  68. { handleQuery = handleQuery
  69. , handleAction = handleAction
  70. }
  71. }
  72. initialState :: forall i. i -> State
  73. initialState _ = { tabs: empty, selectedElem: Nothing, tabHovered: Nothing }
  74. _tab :: SProxy "tab"
  75. _tab = SProxy
  76. render :: forall m. State -> H.ComponentHTML Action () m
  77. render state =
  78. let
  79. tabsWithIndex = state.tabs
  80. tabs =
  81. fromMaybe tabsWithIndex
  82. $ state.selectedElem
  83. >>= ( \{ originalIndex, overIndex } -> moveElem originalIndex overIndex tabsWithIndex
  84. )
  85. in
  86. HH.div
  87. [ HP.id_ "tabs"
  88. , HE.onDoubleClick (\ev -> Just (UserOpenedTab Nothing (ME.toEvent ev)))
  89. ]
  90. (A.mapWithIndex renderTab tabs)
  91. where
  92. renderTab index (Tab t) =
  93. HH.div
  94. [ HP.id_ $ show t.id
  95. , HP.draggable true
  96. -- drag events
  97. , HE.onDragStart \evt -> Just $ TabDragStart evt (Tab t) index
  98. , HE.onDragEnd \evt -> Just $ TabDragEnd evt
  99. , HE.onDragOver \evt -> Just $ TabDragOver evt index
  100. -- fake hover
  101. , HE.onMouseEnter \evt -> Just $ TabMouseEnter evt index
  102. , HE.onMouseLeave \evt -> Just $ TabMouseLeave evt index
  103. -- click event
  104. , HE.onClick (\ev -> Just (UserActivatedTab t.id (ME.toEvent ev)))
  105. , HE.onDoubleClick (\ev -> Just (UserOpenedTab (Just t.id) (ME.toEvent ev)))
  106. -- TODO: on double click on a tab, open a tab right below
  107. -- clases
  108. , HP.classes $ H.ClassName
  109. <$> A.catMaybes
  110. [ Just "tab"
  111. , if t.active then Just "active" else Nothing
  112. , if isDiscarded t then Just "discarded" else Nothing
  113. , case state.tabHovered of
  114. Just idx
  115. | idx == index -> Just "hover"
  116. _ -> Nothing
  117. ]
  118. , HP.title t.title
  119. ]
  120. [ HH.div [ HP.class_ $ H.ClassName "tab-favicon", faviconStyle t.favIconUrl ] []
  121. , HH.div [ HP.class_ $ H.ClassName "tab-title" ]
  122. [ HH.text
  123. $ case t.status of
  124. Just "loading" -> "Loading ..."
  125. _ -> t.title
  126. ]
  127. , HH.div
  128. [ HP.class_ $ H.ClassName "close-button-parent"
  129. , HE.onClick (\ev -> Just (UserClosedTab t.id (ME.toEvent ev)))
  130. ]
  131. [ HH.div [ HP.class_ $ H.ClassName "close-button-outer" ]
  132. [ HH.div [ HP.class_ $ H.ClassName "close-button-inner" ] []
  133. ]
  134. ]
  135. ]
  136. faviconStyle favicon' =
  137. CSS.style
  138. $ do
  139. case favicon' of
  140. Nothing -> pure unit
  141. Just favicon -> CssBackground.backgroundImage $ CssBackground.url favicon
  142. isDiscarded :: forall r. { discarded :: Maybe Boolean | r } -> Boolean
  143. isDiscarded { discarded: Just true } = true
  144. isDiscarded _ = false
  145. handleAction :: forall m. MonadEffect m => Action -> H.HalogenM State Action () SidebarEvent m Unit
  146. handleAction = case _ of
  147. UserClosedTab tid ev -> do
  148. H.liftEffect
  149. $ do
  150. Event.preventDefault ev
  151. Event.stopPropagation ev
  152. log "sb: closed a tab"
  153. H.raise $ SbDeleteTab tid
  154. UserActivatedTab tid ev -> do
  155. H.liftEffect
  156. $ do
  157. Event.preventDefault ev
  158. Event.stopPropagation ev
  159. log "sb: activated a tab"
  160. H.raise $ SbActivateTab tid
  161. UserOpenedTab tid ev -> do
  162. H.liftEffect
  163. $ do
  164. Event.preventDefault ev
  165. Event.stopPropagation ev
  166. log "sb: created a tab"
  167. H.raise $ SbCreateTab tid
  168. -- Drag actions
  169. TabDragStart dragEvent tab index -> do
  170. let
  171. dataTransfer = DE.dataTransfer dragEvent
  172. H.liftEffect
  173. $ do
  174. DT.setData textPlain "" dataTransfer
  175. DT.setDropEffect DT.Move dataTransfer
  176. H.modify_ _ { selectedElem = Just { tab: tab, originalIndex: index, overIndex: index }, tabHovered = Nothing }
  177. H.liftEffect $ log $ "sb: drag start from " <> (show index)
  178. TabDragOver event index -> do
  179. -- prevent the ghost from flying back to its (wrong) place
  180. -- see https://stackoverflow.com/questions/42725321/prevent-html5-drag-ghost-image-flying-back
  181. H.liftEffect $ Event.preventDefault (DE.toEvent event)
  182. state <- H.get
  183. case state.selectedElem of
  184. Just selectedRec@{ originalIndex, overIndex }
  185. | overIndex /= index -> do
  186. H.modify_ (_ { selectedElem = Just $ selectedRec { overIndex = index } })
  187. _ -> pure unit
  188. TabDragEnd event -> do
  189. state <- H.get
  190. case state.selectedElem of
  191. Nothing -> pure unit
  192. Just { tab: (Tab t), originalIndex, overIndex } -> H.raise (SbMoveTab t.id overIndex)
  193. -- Mouse over action
  194. TabMouseEnter evt index -> do
  195. state <- H.get
  196. case state of
  197. { tabHovered: Nothing, selectedElem: Nothing } -> H.modify_ _ { tabHovered = Just index }
  198. _ -> pure unit
  199. TabMouseLeave evt index -> do
  200. state <- H.get
  201. case state.tabHovered of
  202. Nothing -> pure unit
  203. Just prevIdx -> H.modify_ _ { tabHovered = Nothing }
  204. handleQuery :: forall act o m a. Query a -> H.HalogenM State act () o m (Maybe a)
  205. handleQuery = case _ of
  206. InitialTabList tabs a -> H.modify_ (\s -> s { tabs = tabs }) *> pure (Just a)
  207. TabCreated (Tab t) a ->
  208. H.modify_
  209. (over _tabs $ \tabs -> fromMaybe tabs $ A.insertAt t.index (Tab t) tabs)
  210. *> pure (Just a)
  211. TabDeleted tid a ->
  212. H.modify_
  213. ( over _tabs
  214. $ applyAtTabId tid A.deleteAt
  215. )
  216. *> pure (Just a)
  217. TabActivated oldTid tid a ->
  218. H.modify_
  219. ( over _tabs
  220. $ maybe identity (\old -> applyAtTabId old $ setTabActiveAtIndex false) oldTid
  221. >>> applyAtTabId tid (setTabActiveAtIndex true)
  222. )
  223. *> pure (Just a)
  224. TabMoved tid prev next a -> do
  225. state <- H.get
  226. let
  227. tab' = state.tabs A.!! prev
  228. maybeFlipped tab' (pure unit) \tab -> do
  229. H.modify_
  230. ( over _tabs \tabs ->
  231. fromMaybe tabs $ (A.deleteAt prev >=> A.insertAt next tab) tabs
  232. )
  233. -- Wait for a move to disable the drag data, otherwise the tab will come
  234. -- back briefly to its original place before switching again.
  235. -- This also means that if the move fail, this will be in an inconsistant
  236. -- state.
  237. H.modify_ \s -> s { selectedElem = Nothing }
  238. pure (Just a)
  239. TabInfoChanged tid cinfo a ->
  240. H.modify_
  241. ( over _tabs
  242. $ \tabs ->
  243. fromMaybe tabs
  244. $ (findIndexTabId tid >=> \index -> A.modifyAt index (updateTabFromInfo cinfo) tabs) tabs
  245. )
  246. *> pure (Just a)
  247. setTabActive :: Boolean -> Tab -> Tab
  248. setTabActive act (Tab t) = Tab (t { active = act })
  249. setTabActiveAtIndex :: Boolean -> Int -> Array Tab -> Maybe (Array Tab)
  250. setTabActiveAtIndex act i = A.modifyAt i (setTabActive act)
  251. findTabByTabId :: TabId -> Array Tab -> Maybe Tab
  252. findTabByTabId tid = A.head <<< A.filter \(Tab t) -> t.id == tid
  253. findIndexTabId :: TabId -> Array Tab -> Maybe Int
  254. findIndexTabId tid = A.findIndex \(Tab t) -> t.id == tid
  255. applyAtTabId :: TabId -> (Int -> Array Tab -> Maybe (Array Tab)) -> Array Tab -> Array Tab
  256. applyAtTabId tid f a = fromMaybe a $ findIndexTabId tid a >>= (flip f) a
  257. maybeFlipped :: forall a b. Maybe a -> b -> (a -> b) -> b
  258. maybeFlipped ma b f = maybe b f ma
  259. updateTabFromInfo :: ChangeInfo -> Tab -> Tab
  260. updateTabFromInfo (ChangeInfo cinfo) (Tab t) =
  261. let
  262. updateField :: forall r a. { acc :: ChangeInfoRec -> Maybe a, update :: a -> r -> r } -> r -> r
  263. updateField { acc, update } tab = case acc cinfo of
  264. Nothing -> tab
  265. Just field -> update field tab
  266. applyChange =
  267. updateField { acc: _.title, update: (\val -> _ { title = val }) }
  268. >>> updateField { acc: _.status, update: (\val -> _ { status = Just val }) }
  269. >>> updateField { acc: _.discarded, update: (\val -> _ { discarded = Just val }) }
  270. >>> updateField { acc: _.url, update: (\val -> _ { url = Just val }) }
  271. >>> updateField { acc: _.pinned, update: (\val -> _ { pinned = val }) }
  272. >>> updateField { acc: _.hidden, update: (\val -> _ { hidden = val }) }
  273. >>> updateField { acc: _.favIconUrl, update: (\val -> _ { favIconUrl = Just val }) }
  274. in
  275. Tab (applyChange t)
  276. moveElem :: forall a. Int -> Int -> Array a -> Maybe (Array a)
  277. moveElem from to arr = do
  278. elem <- arr A.!! from
  279. (A.deleteAt from >=> A.insertAt to elem) arr