Tabs.purs 9.3 KB

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