whiteboard.cljs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. (ns frontend.components.whiteboard
  2. (:require [cljs.math :as math]
  3. [frontend.components.page :as page]
  4. [frontend.components.reference :as reference]
  5. [frontend.context.i18n :refer [t]]
  6. [frontend.db.model :as model]
  7. [frontend.handler.route :as route-handler]
  8. [frontend.handler.user :as user-handler]
  9. [frontend.handler.whiteboard :as whiteboard-handler]
  10. [frontend.state :as state]
  11. [frontend.ui :as ui]
  12. [frontend.util :as util]
  13. [promesa.core :as p]
  14. [rum.core :as rum]
  15. [shadow.loader :as loader]))
  16. (defonce tldraw-loaded? (atom false))
  17. (rum/defc tldraw-app < rum/reactive
  18. {:init (fn [state]
  19. (p/let [_ (loader/load :tldraw)]
  20. (reset! tldraw-loaded? true))
  21. state)}
  22. [name shape-id]
  23. (let [loaded? (rum/react tldraw-loaded?)
  24. draw-component (when loaded?
  25. (resolve 'frontend.extensions.tldraw/tldraw-app))]
  26. (when draw-component
  27. (draw-component name shape-id))))
  28. ;; TODO: make it reactive to db changes
  29. (rum/defc tldraw-preview < rum/reactive
  30. {:init (fn [state]
  31. (p/let [_ (loader/load :tldraw)]
  32. (reset! tldraw-loaded? true))
  33. state)}
  34. [page-name]
  35. (let [loaded? (rum/react tldraw-loaded?)
  36. tldr (whiteboard-handler/page-name->tldr! page-name)
  37. generate-preview (when loaded?
  38. (resolve 'frontend.extensions.tldraw/generate-preview))]
  39. (when generate-preview
  40. (generate-preview tldr))))
  41. (rum/defc dropdown
  42. [label children show? outside-click-hander]
  43. (let [anchor-ref (rum/use-ref nil)
  44. content-ref (rum/use-ref nil)
  45. rect (util/use-component-size (when show? anchor-ref))
  46. _ (util/use-click-outside content-ref outside-click-hander)
  47. [d-open set-d-open] (rum/use-state false)
  48. _ (rum/use-effect! (fn [] (js/setTimeout #(set-d-open show?) 100))
  49. [show?])]
  50. [:div.dropdown-anchor {:ref anchor-ref}
  51. label
  52. (when (and rect show? (> (.-y rect) 0))
  53. (ui/portal
  54. [:div.fixed.shadow-lg.color-level.px-2.rounded-lg.transition
  55. {:ref content-ref
  56. :style {:opacity (if d-open 1 0)
  57. :width "240px"
  58. :min-height "40px"
  59. :left (+ (* 0.5 (.-width rect)) (.-x rect) -120)
  60. :top (+ (.-y rect) (.-height rect) 8)}} children]))]))
  61. (rum/defc page-refs-count < rum/static
  62. ([page-name classname]
  63. (page-refs-count page-name classname nil))
  64. ([page-name classname render-fn]
  65. (let [page-entity (model/get-page page-name)
  66. block-uuid (:block/uuid page-entity)
  67. ref (rum/use-ref nil)
  68. refs-count (count (:block/_refs page-entity))
  69. [open-flag set-open-flag] (rum/use-state 0)
  70. open? (not= open-flag 0)
  71. d-open-flag (rum/use-memo #(util/debounce 200 set-open-flag) [])]
  72. ;; TODO: move click outside to the utility?
  73. (rum/use-effect!
  74. (let [listener (fn [e]
  75. (when (and (.-current ref)
  76. (not (.contains (.-current ref) (.-target e))))
  77. (d-open-flag 0)))]
  78. (.addEventListener js/document.body "mousedown" listener true)
  79. #(.removeEventListener js/document.body "mousedown" listener))
  80. [ref])
  81. (when (> refs-count 0)
  82. (dropdown
  83. [:div.flex.items-center.gap-2.whiteboard-page-refs-count
  84. {:class (str classname (when open? " open"))
  85. :on-mouse-enter (fn [] (d-open-flag #(if (= % 0) 1 %)))
  86. :on-mouse-leave (fn [] (d-open-flag #(if (= % 2) % 0)))
  87. :on-click (fn [e]
  88. (util/stop e)
  89. (d-open-flag (fn [o] (if (not= o 2) 2 0))))}
  90. [:div.open-page-ref-link refs-count]
  91. (when render-fn (render-fn open?))]
  92. (reference/block-linked-references block-uuid)
  93. open?
  94. #(set-open-flag 0))))))
  95. (defn- get-page-display-name
  96. [page-name]
  97. (let [page-entity (model/get-page page-name)]
  98. (or
  99. (get-in page-entity [:block/properties :title] nil)
  100. (:block/original-name page-entity)
  101. page-name)))
  102. ;; This is not accurate yet
  103. (defn- get-page-human-update-time
  104. [page-name]
  105. (let [page-entity (model/get-page page-name)
  106. {:block/keys [updated-at created-at]} page-entity]
  107. (str (if (= created-at updated-at) "Created " "Edited ")
  108. (util/time-ago (js/Date. updated-at)))))
  109. (rum/defc dashboard-preview-card
  110. [page-name {:keys [checked on-checked-change show-checked?]}]
  111. [:div.dashboard-card.dashboard-preview-card.cursor-pointer.hover:shadow-lg
  112. {:data-checked checked
  113. :style {:filter (if (and show-checked? (not checked)) "opacity(0.5)" "none")}
  114. :on-click
  115. (fn [e]
  116. (util/stop e)
  117. (if show-checked?
  118. (on-checked-change (not checked))
  119. (route-handler/redirect-to-whiteboard! page-name)))}
  120. [:div.dashboard-card-title
  121. [:div.flex.w-full.items-center
  122. [:div.dashboard-card-title-name.font-bold
  123. (if (parse-uuid page-name)
  124. [:span.opacity-50 (t :untitled)]
  125. (get-page-display-name page-name))]
  126. [:div.flex-1]
  127. [:div.dashboard-card-checkbox
  128. {:tab-index -1
  129. :style {:visibility (when show-checked? "visible")}
  130. :on-click util/stop-propagation}
  131. (ui/checkbox {:checked checked
  132. :on-change (fn [] (on-checked-change (not checked)))})]]
  133. [:div.flex.w-full.opacity-50
  134. [:div (get-page-human-update-time page-name)]
  135. [:div.flex-1]
  136. (page-refs-count page-name nil)]]
  137. [:div.p-4.h-64.flex.justify-center
  138. (tldraw-preview page-name)]])
  139. (rum/defc dashboard-create-card
  140. []
  141. [:div.dashboard-card.dashboard-create-card.cursor-pointer#tl-create-whiteboard
  142. {:on-click
  143. (fn [e]
  144. (util/stop e)
  145. (whiteboard-handler/create-new-whiteboard!))}
  146. (ui/icon "plus")
  147. [:span.dashboard-create-card-caption.select-none
  148. "New whiteboard"]])
  149. (rum/defc whiteboard-dashboard
  150. []
  151. (if (user-handler/alpha-user?)
  152. (let [whiteboards (->> (model/get-all-whiteboards (state/get-current-repo))
  153. (sort-by :block/updated-at)
  154. reverse)
  155. whiteboard-names (map :block/name whiteboards)
  156. ref (rum/use-ref nil)
  157. rect (util/use-component-size ref)
  158. [container-width] (when rect [(.-width rect) (.-height rect)])
  159. cols (cond (< container-width 600) 1
  160. (< container-width 900) 2
  161. (< container-width 1200) 3
  162. :else 4)
  163. total-whiteboards (count whiteboards)
  164. empty-cards (- (max (* (math/ceil (/ (inc total-whiteboards) cols)) cols) (* 2 cols))
  165. (inc total-whiteboards))
  166. [checked-page-names set-checked-page-names] (rum/use-state #{})
  167. has-checked? (not-empty checked-page-names)]
  168. [:<>
  169. [:h1.select-none.flex.items-center.whiteboard-dashboard-title.title
  170. [:div "All whiteboards"
  171. [:span.opacity-50
  172. (str " · " total-whiteboards)]]
  173. [:div.flex-1]
  174. (when has-checked?
  175. [:button.ui__button.m-0.py-1.inline-flex.items-center.bg-red-800
  176. {:on-click
  177. (fn []
  178. (state/set-modal! (page/batch-delete-dialog
  179. (map (fn [name]
  180. (some (fn [w] (when (= (:block/name w) name) w)) whiteboards))
  181. checked-page-names)
  182. false route-handler/redirect-to-whiteboard-dashboard!)))}
  183. [:span.flex.gap-2.items-center
  184. [:span.opacity-50 (ui/icon "trash" {:style {:font-size 15}})]
  185. (t :delete)
  186. [:span.opacity-50
  187. (str " · " (count checked-page-names))]]])]
  188. [:div
  189. {:ref ref}
  190. [:div.gap-8.grid.grid-rows-auto
  191. {:style {:visibility (when (nil? container-width) "hidden")
  192. :grid-template-columns (str "repeat(" cols ", minmax(0, 1fr))")}}
  193. (dashboard-create-card)
  194. (for [whiteboard-name whiteboard-names]
  195. [:<> {:key whiteboard-name}
  196. (dashboard-preview-card whiteboard-name
  197. {:show-checked? has-checked?
  198. :checked (boolean (checked-page-names whiteboard-name))
  199. :on-checked-change (fn [checked]
  200. (set-checked-page-names (if checked
  201. (conj checked-page-names whiteboard-name)
  202. (disj checked-page-names whiteboard-name))))})])
  203. (for [n (range empty-cards)]
  204. [:div.dashboard-card.dashboard-bg-card {:key n}])]]])
  205. [:div "This feature is not public available yet."]))
  206. (rum/defc whiteboard-page
  207. [name block-id]
  208. [:div.absolute.w-full.h-full.whiteboard-page
  209. ;; makes sure the whiteboard will not cover the borders
  210. {:key name
  211. :style {:padding "0.5px" :z-index 0
  212. :transform "translateZ(0)"
  213. :text-rendering "geometricPrecision"
  214. :-webkit-font-smoothing "subpixel-antialiased"}}
  215. [:div.whiteboard-page-title-root
  216. [:span.whiteboard-page-title
  217. {:style {:color "var(--ls-primary-text-color)"
  218. :user-select "none"}}
  219. (page/page-title name
  220. [:span.tie.tie-whiteboard
  221. {:style {:font-size "0.9em"}}]
  222. (get-page-display-name name)
  223. nil
  224. false)]
  225. (page-refs-count name
  226. "text-md px-3 py-2 cursor-default whiteboard-page-refs-count"
  227. (fn [open?] [:<> "References" (ui/icon (if open? "references-hide" "references-show")
  228. {:extension? true})]))]
  229. (tldraw-app name block-id)])
  230. (rum/defc whiteboard-route
  231. [route-match]
  232. (when (user-handler/alpha-user?)
  233. (let [name (get-in route-match [:parameters :path :name])
  234. {:keys [block-id]} (get-in route-match [:parameters :query])]
  235. (whiteboard-page name block-id))))