highlights.cljs 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. (ns frontend.extensions.pdf.highlights
  2. (:require [rum.core :as rum]
  3. [promesa.core :as p]
  4. [cljs-bean.core :as bean]
  5. [medley.core :as medley]
  6. [frontend.context.i18n :as i18n]
  7. [frontend.handler.notification :as notification]
  8. [frontend.extensions.pdf.utils :as pdf-utils]
  9. [frontend.extensions.pdf.assets :as pdf-assets]
  10. [frontend.util :as front-utils]
  11. [frontend.state :as state]
  12. [frontend.config :as config]
  13. [frontend.storage :as storage]
  14. [frontend.components.svg :as svg]
  15. [frontend.rum :refer [use-atom]]
  16. [medley.core :as medley]
  17. [frontend.fs :as fs]
  18. [clojure.string :as string]
  19. [frontend.ui :as ui]))
  20. (defn dd [& args]
  21. (apply js/console.debug args))
  22. (def *area-mode? (atom false))
  23. (defn reset-current-pdf!
  24. []
  25. (state/set-state! :pdf/current nil))
  26. (rum/defcs pdf-highlight-finder
  27. < rum/static rum/reactive
  28. [state ^js viewer]
  29. (when viewer
  30. (if-let [ref-hl (state/sub :pdf/ref-highlight)]
  31. (do
  32. ;; delay handle: aim to fix page blink
  33. (js/setTimeout #(pdf-utils/scroll-to-highlight viewer ref-hl) 100)
  34. (js/setTimeout #(state/set-state! :pdf/ref-highlight nil) 1000)))))
  35. (rum/defc pdf-page-finder < rum/static
  36. [^js viewer]
  37. (when viewer
  38. (when-let [current (:pdf/current @state/state)]
  39. (let [active-hl (:pdf/ref-highlight @state/state)
  40. page-key (:filename current)
  41. last-page (and page-key
  42. (front-utils/safe-parse-int (storage/get (str "ls-pdf-last-page-" page-key))))]
  43. (when (and last-page (nil? active-hl))
  44. (set! (.-currentPageNumber viewer) last-page)))))
  45. nil)
  46. (rum/defc pdf-resizer
  47. [^js viewer]
  48. (let [el-ref (rum/use-ref nil)
  49. adjust-main-size!
  50. (front-utils/debounce
  51. 200 (fn [width]
  52. (let [root-el js/document.documentElement]
  53. (.setProperty (.-style root-el) "--ph-view-container-width" width)
  54. (pdf-utils/adjust-viewer-size! viewer))))]
  55. ;; draggable handler
  56. (rum/use-effect!
  57. (fn []
  58. (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
  59. (-> (js/interact el)
  60. (.draggable
  61. (bean/->js
  62. {:listeners
  63. {:move
  64. (fn [^js/MouseEvent e]
  65. (let [width js/document.documentElement.clientWidth
  66. offset (.-left (.-rect e))
  67. el-ratio (.toFixed (/ offset width) 6)
  68. target-el (js/document.getElementById "pdf-layout-container")]
  69. (when target-el
  70. (let [width (str (* el-ratio 100) "vw")]
  71. (.setProperty (.-style target-el) "width" width)
  72. (adjust-main-size! width)))))}}))
  73. (.styleCursor false)
  74. (.on "dragstart" #(.. js/document.documentElement -classList (add "is-resizing-buf")))
  75. (.on "dragend" #(.. js/document.documentElement -classList (remove "is-resizing-buf")))))
  76. #())
  77. [])
  78. [:span.extensions__pdf-resizer {:ref el-ref}]))
  79. (rum/defc pdf-highlights-ctx-menu
  80. [^js viewer
  81. {:keys [highlight vw-pos point]}
  82. {:keys [clear-ctx-tip! add-hl! upd-hl! del-hl!]}]
  83. (let [mounted (rum/use-ref false)]
  84. (rum/use-effect!
  85. (fn []
  86. (let [cb #(if-not (rum/deref mounted)
  87. (rum/set-ref! mounted true)
  88. (clear-ctx-tip!))]
  89. (js/document.addEventListener "click" cb)
  90. #(js/document.removeEventListener "click" cb)))
  91. [clear-ctx-tip!]))
  92. ;; TODO: precise position
  93. ;;(when-let [
  94. ;;page-bounding (and highlight (pdf-utils/get-page-bounding viewer (:page highlight)))
  95. ;;])
  96. (let [head-height 0 ;; 48 temp
  97. top (- (+ (:y point) (.. viewer -container -scrollTop)) head-height)
  98. left (:x point)
  99. id (:id highlight)
  100. content (:content highlight)]
  101. (rum/with-context
  102. [[t] i18n/*tongue-context*]
  103. [:ul.extensions__pdf-hls-ctx-menu
  104. {:style {:top top :left left}
  105. :on-click (fn [^js/MouseEvent e]
  106. (when-let [action (.. e -target -dataset -action)]
  107. (case action
  108. "ref"
  109. (pdf-assets/copy-hl-ref! highlight)
  110. "copy"
  111. (do
  112. (front-utils/copy-to-clipboard! (:text content))
  113. (pdf-utils/clear-all-selection))
  114. "del"
  115. (do
  116. (del-hl! highlight)
  117. (pdf-assets/del-ref-block! highlight)
  118. (pdf-assets/unlink-hl-area-image$ viewer (:pdf/current @state/state) highlight))
  119. ;; colors
  120. (let [properties {:color action}]
  121. (if-not id
  122. ;; add highlight
  123. (let [highlight (merge highlight
  124. {:id (pdf-utils/gen-uuid)
  125. :properties properties})]
  126. (add-hl! highlight)
  127. (pdf-utils/clear-all-selection)
  128. (pdf-assets/copy-hl-ref! highlight))
  129. ;; update highlight
  130. (do
  131. (upd-hl! (assoc highlight :properties properties)))))))
  132. (clear-ctx-tip!))}
  133. [:li.item-colors
  134. (for [it ["yellow", "blue", "green", "red", "purple"]]
  135. [:a {:key it :data-color it :data-action it} it])]
  136. (and id [:li.item {:data-action "ref"} (t :pdf/copy-ref)])
  137. (and (not (:image content)) [:li.item {:data-action "copy"} (t :pdf/copy-text)])
  138. (and id [:li.item {:data-action "del"} (t :delete)])
  139. ])))
  140. (rum/defc pdf-highlights-text-region
  141. [^js viewer vw-hl hl
  142. {:keys [show-ctx-tip!]}]
  143. (let [id (:id hl)
  144. {:keys [rects]} (:position vw-hl)
  145. {:keys [color]} (:properties hl)]
  146. [:div.extensions__pdf-hls-text-region
  147. {:on-click
  148. (fn [^js/MouseEvent e]
  149. (let [x (.-clientX e)
  150. y (.-clientY e)]
  151. (show-ctx-tip! viewer hl {:x x :y y})))}
  152. (map-indexed
  153. (fn [idx rect]
  154. [:div.hls-text-region-item
  155. {:key idx
  156. :style rect
  157. :data-color color}])
  158. rects)]))
  159. (rum/defc pdf-highlight-area-region
  160. [^js viewer vw-hl hl
  161. {:keys [show-ctx-tip! upd-hl!]}]
  162. (let [*el (rum/use-ref nil)
  163. *dirty (rum/use-ref nil)]
  164. ;; resizable
  165. (rum/use-effect!
  166. (fn []
  167. (let [^js el (rum/deref *el)
  168. ^js it (-> (js/interact el)
  169. (.resizable
  170. (bean/->js
  171. {:edges {:left true :right true :top true :bottom true}
  172. :listeners {:start (fn [^js/MouseEvent e]
  173. (rum/set-ref! *dirty true))
  174. :end (fn [^js/MouseEvent e]
  175. (let [vw-pos (:position vw-hl)
  176. ^js target (. e -target)
  177. ^js vw-rect (. e -rect)
  178. [dx, dy] (mapv #(let [val (.getAttribute target (str "data-" (name %)))]
  179. (if-not (nil? val) (js/parseFloat val) 0)) [:x :y])
  180. to-top (+ (get-in vw-pos [:bounding :top]) dy)
  181. to-left (+ (get-in vw-pos [:bounding :left]) dx)
  182. to-w (. vw-rect -width)
  183. to-h (. vw-rect -height)
  184. to-vw-pos (update vw-pos :bounding assoc
  185. :top to-top
  186. :left to-left
  187. :width to-w
  188. :height to-h)
  189. to-sc-pos (pdf-utils/vw-to-scaled-pos viewer to-vw-pos)]
  190. ;; TODO: exception
  191. (let [hl' (assoc hl :position to-sc-pos)
  192. hl' (assoc-in hl' [:content :image] (js/Date.now))]
  193. (p/then
  194. (pdf-assets/persist-hl-area-image$ viewer
  195. (:pdf/current @state/state)
  196. hl' hl (:bounding to-vw-pos))
  197. (fn [] (js/setTimeout
  198. #(do
  199. ;; reset dom effects
  200. (set! (.. target -style -transform) (str "translate(0, 0)"))
  201. (.removeAttribute target "data-x")
  202. (.removeAttribute target "data-y")
  203. (upd-hl! hl')) 200))))
  204. (js/setTimeout #(rum/set-ref! *dirty false))))
  205. :move (fn [^js/MouseEvent e]
  206. (let [^js/HTMLElement target (.-target e)
  207. x (.getAttribute target "data-x")
  208. y (.getAttribute target "data-y")
  209. bx (if-not (nil? x) (js/parseFloat x) 0)
  210. by (if-not (nil? y) (js/parseFloat y) 0)]
  211. ;; update element style
  212. (set! (.. target -style -width) (str (.. e -rect -width) "px"))
  213. (set! (.. target -style -height) (str (.. e -rect -height) "px"))
  214. ;; translate when resizing from top or left edges
  215. (let [ax (+ bx (.. e -deltaRect -left))
  216. ay (+ by (.. e -deltaRect -top))]
  217. (set! (.. target -style -transform) (str "translate(" ax "px, " ay "px)"))
  218. ;; cache pos
  219. (.setAttribute target "data-x" ax)
  220. (.setAttribute target "data-y" ay))
  221. ))}
  222. :modifiers [;; minimum
  223. (js/interact.modifiers.restrictSize
  224. (bean/->js {:min {:width 60 :height 25}}))]
  225. :inertia true})
  226. ))]
  227. ;; destroy
  228. #(.unset it)))
  229. [hl])
  230. (when-let [vw-bounding (get-in vw-hl [:position :bounding])]
  231. (let [{:keys [color]} (:properties hl)]
  232. [:div.extensions__pdf-hls-area-region
  233. {:ref *el
  234. :style vw-bounding
  235. :data-color color
  236. :on-click (fn [^js/MouseEvent e]
  237. (when-not (rum/deref *dirty)
  238. (let [x (.-clientX e)
  239. y (.-clientY e)]
  240. (show-ctx-tip! viewer hl {:x x :y y}))))}]))))
  241. (rum/defc pdf-highlights-region-container
  242. [^js viewer page-hls ops]
  243. [:div.hls-region-container
  244. (for [hl page-hls]
  245. (let [vw-hl (update-in hl [:position] #(pdf-utils/scaled-to-vw-pos viewer %))]
  246. (rum/with-key
  247. (if (get-in hl [:content :image])
  248. (pdf-highlight-area-region viewer vw-hl hl ops)
  249. (pdf-highlights-text-region viewer vw-hl hl ops))
  250. (:id hl))
  251. ))])
  252. (rum/defc pdf-highlight-area-selection
  253. [^js viewer {:keys [clear-ctx-tip! show-ctx-tip!] :as ops}]
  254. (let [^js viewer-clt (.. viewer -viewer -classList)
  255. *el (rum/use-ref nil)
  256. *cnt-el (rum/use-ref nil)
  257. *sta-el (rum/use-ref nil)
  258. *cnt-rect (rum/use-ref nil)
  259. [start-coord, set-start-coord!] (rum/use-state nil)
  260. [end-coord, set-end-coord!] (rum/use-state nil)
  261. [_ set-area-mode!] (use-atom *area-mode?)
  262. should-start (fn [^js e]
  263. (let [^js target (.-target e)]
  264. (when (and (not
  265. (.contains (.-classList target) "extensions__pdf-hls-area-region"))
  266. (.closest target ".page"))
  267. (and e (or (.-metaKey e)
  268. (.-altKey e)
  269. @*area-mode?)))))
  270. reset-coords #(do
  271. (set-start-coord! nil)
  272. (set-end-coord! nil)
  273. (rum/set-ref! *sta-el nil))
  274. calc-coords (fn [page-x page-y]
  275. (when-let [cnt-el (or (rum/deref *cnt-el)
  276. (when-let [cnt-el (.querySelector (.closest (rum/deref *el) ".extensions__pdf-viewer-cnt") ".extensions__pdf-viewer")]
  277. (rum/set-ref! *cnt-el cnt-el) cnt-el))]
  278. (let [cnt-rect (rum/deref *cnt-rect)
  279. cnt-rect (or cnt-rect (bean/->clj (.toJSON (.getBoundingClientRect cnt-el))))
  280. _ (rum/set-ref! *cnt-rect cnt-rect)]
  281. {:x (- page-x (:left cnt-rect) (.-scrollLeft cnt-el))
  282. :y (-> page-y
  283. (- (:top cnt-rect))
  284. (+ (.-scrollTop cnt-el)))})))
  285. calc-pos (fn [start end]
  286. {:left (min (:x start) (:x end))
  287. :top (min (:y start) (:y end))
  288. :width (js/Math.abs (- (:x end) (:x start)))
  289. :height (js/Math.abs (- (:y end) (:y start)))})
  290. disable-text-selection! #(js-invoke viewer-clt (if % "add" "remove") "disabled-text-selection")
  291. fn-move (rum/use-callback
  292. (fn [^js/MouseEvent e]
  293. (set-end-coord! (calc-coords (.-pageX e) (.-pageY e))))
  294. [])]
  295. (rum/use-effect!
  296. (fn []
  297. (when-let [^js/HTMLElement root (.closest (rum/deref *el) ".extensions__pdf-container")]
  298. (let [fn-start (fn [^js/MouseEvent e]
  299. (if (should-start e)
  300. (do
  301. (rum/set-ref! *sta-el (.-target e))
  302. (set-start-coord! (calc-coords (.-pageX e) (.-pageY e)))
  303. (disable-text-selection! true)
  304. (.addEventListener root "mousemove" fn-move))
  305. ;; reset
  306. (reset-coords)))
  307. fn-end (fn [^js/MouseEvent e]
  308. (when-let [start-el (rum/deref *sta-el)]
  309. (let [end (calc-coords (.-pageX e) (.-pageY e))
  310. pos (calc-pos start-coord end)]
  311. (if (and (> (:width pos) 10)
  312. (> (:height pos) 10))
  313. (when-let [^js page-el (.closest start-el ".page")]
  314. (let [page-number (int (.-pageNumber (.-dataset page-el)))
  315. page-pos (merge pos {:top (- (:top pos) (.-offsetTop page-el))
  316. :left (- (:left pos) (.-offsetLeft page-el))})
  317. vw-pos {:bounding page-pos :rects [] :page page-number}
  318. sc-pos (pdf-utils/vw-to-scaled-pos viewer vw-pos)
  319. point {:x (.-clientX e) :y (.-clientY e)}
  320. hl {:id nil
  321. :page page-number
  322. :position sc-pos
  323. :content {:text "[:span]" :image (js/Date.now)}
  324. :properties {}}]
  325. ;; ctx tips
  326. (show-ctx-tip! viewer hl point {:reset-fn #(reset-coords)})
  327. ;; export area highlight
  328. ;;(dd "[selection end] :start"
  329. ;; start-coord ":end" end ":pos" pos
  330. ;; ":page" page-number
  331. ;; ":offset" page-pos
  332. ;; ":vw-pos" vw-pos
  333. ;; ":sc-pos" sc-pos)
  334. )
  335. (set-area-mode! false))
  336. ;; reset
  337. (reset-coords)))
  338. (disable-text-selection! false)
  339. (.removeEventListener root "mousemove" fn-move)))]
  340. (doto root
  341. (.addEventListener "mousedown" fn-start)
  342. (.addEventListener "mouseup" fn-end #js {:once true}))
  343. ;; destroy
  344. #(doto root
  345. (.removeEventListener "mousedown" fn-start)
  346. (.removeEventListener "mouseup" fn-end)))))
  347. [start-coord])
  348. [:div.extensions__pdf-area-selection
  349. {:ref *el}
  350. (when (and start-coord end-coord)
  351. [:div.shadow-rect {:style (calc-pos start-coord end-coord)}])]))
  352. (rum/defc pdf-highlights
  353. [^js el ^js viewer initial-hls loaded-pages {:keys [set-dirty-hls!]}]
  354. (let [^js doc (.-ownerDocument el)
  355. ^js win (.-defaultView doc)
  356. *mounted (rum/use-ref false)
  357. [sel-state, set-sel-state!] (rum/use-state {:range nil :collapsed nil :point nil})
  358. [highlights, set-highlights!] (rum/use-state initial-hls)
  359. [tip-state, set-tip-state!] (rum/use-state {:highlight nil :vw-pos nil :point nil :reset-fn nil})
  360. clear-ctx-tip! (rum/use-callback
  361. #(let [reset-fn (:reset-fn tip-state)]
  362. (set-tip-state! {})
  363. (and (fn? reset-fn) (reset-fn)))
  364. [tip-state])
  365. show-ctx-tip! (fn [^js viewer hl point & ops]
  366. (let [vw-pos (pdf-utils/scaled-to-vw-pos viewer (:position hl))]
  367. (set-tip-state! (apply merge (list* {:highlight hl :vw-pos vw-pos :point point} ops)))))
  368. add-hl! (fn [hl] (when (:id hl)
  369. ;; fix js object
  370. (let [highlights (pdf-utils/fix-nested-js highlights)]
  371. (set-highlights! (conj highlights hl)))
  372. (when-let [vw-pos (and (pdf-assets/area-highlight? hl)
  373. (pdf-utils/scaled-to-vw-pos viewer (:position hl)))]
  374. ;; exceptions
  375. (pdf-assets/persist-hl-area-image$ viewer (:pdf/current @state/state)
  376. hl nil (:bounding vw-pos)))))
  377. upd-hl! (fn [hl]
  378. (let [highlights (pdf-utils/fix-nested-js highlights)]
  379. (when-let [[target-idx] (medley/find-first
  380. #(= (:id (second %)) (:id hl))
  381. (medley/indexed highlights))]
  382. (set-highlights! (assoc-in highlights [target-idx] hl))
  383. (pdf-assets/update-hl-area-block! hl))))
  384. del-hl! (fn [hl] (when-let [id (:id hl)] (set-highlights! (into [] (remove #(= id (:id %)) highlights)))))]
  385. ;; consume dirtied
  386. (rum/use-effect!
  387. (fn []
  388. (if (rum/deref *mounted)
  389. (set-dirty-hls! highlights)
  390. (rum/set-ref! *mounted true)))
  391. [highlights])
  392. ;; selection events
  393. (rum/use-effect!
  394. (fn []
  395. (let [fn-selection-ok
  396. (fn [^js/MouseEvent e]
  397. (let [^js/Selection selection (js/document.getSelection)
  398. ^js/Range sel-range (.getRangeAt selection 0)]
  399. (cond
  400. (.-isCollapsed selection)
  401. (set-sel-state! {:collapsed true})
  402. (and sel-range (.contains el (.-commonAncestorContainer sel-range)))
  403. (set-sel-state! {:collapsed false :range sel-range :point {:x (.-clientX e) :y (.-clientY e)}}))))
  404. fn-selection
  405. (fn []
  406. (let [*dirty (volatile! false)
  407. fn-dirty #(vreset! *dirty true)]
  408. (js/document.addEventListener "selectionchange" fn-dirty)
  409. (js/document.addEventListener "mouseup"
  410. (fn [^js e]
  411. (and @*dirty (fn-selection-ok e))
  412. (js/document.removeEventListener "selectionchange" fn-dirty))
  413. #js {:once true})))
  414. fn-resize
  415. (partial pdf-utils/adjust-viewer-size! viewer)]
  416. ;;(doto (.-eventBus viewer))
  417. (doto el
  418. (.addEventListener "mousedown" fn-selection))
  419. (doto win
  420. (.addEventListener "resize" fn-resize))
  421. ;; destroy
  422. #(do
  423. ;;(doto (.-eventBus viewer))
  424. (doto el
  425. (.removeEventListener "mousedown" fn-selection))
  426. (doto win
  427. (.removeEventListener "resize" fn-resize)))))
  428. [viewer])
  429. ;; selection context menu
  430. (rum/use-effect!
  431. (fn []
  432. (when-let [^js sel-range (and (not (:collapsed sel-state)) (:range sel-state))]
  433. (when-let [page-info (pdf-utils/get-page-from-range sel-range)]
  434. (when-let [sel-rects (pdf-utils/get-range-rects<-page-cnt sel-range (:page-el page-info))]
  435. (let [page (int (:page-number page-info))
  436. ^js point (:point sel-state)
  437. ^js bounding (pdf-utils/get-bounding-rect sel-rects)
  438. vw-pos {:bounding bounding :rects sel-rects :page page}
  439. sc-pos (pdf-utils/vw-to-scaled-pos viewer vw-pos)]
  440. ;; TODO: debug
  441. ;;(dd "[VW x SC] ====>" vw-pos sc-pos)
  442. ;;(dd "[Range] ====> [" page-info "]" (.toString sel-range) point)
  443. ;;(dd "[Rects] ====>" sel-rects " [Bounding] ====>" bounding)
  444. (let [hl {:id nil
  445. :page page
  446. :position sc-pos
  447. :content {:text (.toString sel-range)}
  448. :properties {}}]
  449. ;; show context menu
  450. (set-tip-state! {:highlight hl
  451. :vw-pos vw-pos
  452. :point point})))))))
  453. [(:range sel-state)])
  454. ;; render hls
  455. (rum/use-effect!
  456. (fn []
  457. ;;(dd "=== rebuild highlights ===" (count highlights))
  458. (when-let [grouped-hls (and (sequential? highlights) (group-by :page highlights))]
  459. (doseq [page loaded-pages]
  460. (when-let [^js/HTMLDivElement hls-layer (pdf-utils/resolve-hls-layer! viewer page)]
  461. (let [page-hls (get grouped-hls page)]
  462. (rum/mount
  463. ;; TODO: area & text hls
  464. (pdf-highlights-region-container
  465. viewer page-hls {:show-ctx-tip! show-ctx-tip!
  466. :upd-hl! upd-hl!})
  467. hls-layer)))))
  468. ;; destroy
  469. #())
  470. [loaded-pages highlights])
  471. [:div.extensions__pdf-highlights-cnt
  472. ;; hl context tip menu
  473. (if (:highlight tip-state)
  474. (js/ReactDOM.createPortal
  475. (pdf-highlights-ctx-menu
  476. viewer tip-state
  477. {:clear-ctx-tip! clear-ctx-tip!
  478. :add-hl! add-hl!
  479. :del-hl! del-hl!
  480. :upd-hl! upd-hl!})
  481. (.querySelector el ".pp-holder")))
  482. ;; debug highlights anchor
  483. ;;(if (seq highlights)
  484. ;; [:ul.extensions__pdf-highlights
  485. ;; (for [hl highlights]
  486. ;; [:li
  487. ;; [:a
  488. ;; {:on-click #(pdf-utils/scroll-to-highlight viewer hl)}
  489. ;; (str "#" (:id hl) "# ")]
  490. ;; (:text (:content hl))])
  491. ;; ])
  492. ;; refs
  493. (pdf-highlight-finder viewer)
  494. (pdf-page-finder viewer)
  495. ;; area selection container
  496. (pdf-highlight-area-selection
  497. viewer
  498. {:clear-ctx-tip! clear-ctx-tip!
  499. :show-ctx-tip! show-ctx-tip!
  500. :add-hl! add-hl!
  501. })]))
  502. (rum/defc pdf-settings
  503. [^js viewer theme {:keys [hide-settings! select-theme!]}]
  504. (let [*el-popup (rum/use-ref nil)]
  505. (rum/use-effect!
  506. (fn []
  507. (let [el-popup (rum/deref *el-popup)
  508. cb (fn [^js e]
  509. (and (= e.which 27) (hide-settings!)))]
  510. (js/setTimeout #(.focus el-popup))
  511. (.addEventListener el-popup "keyup" cb)
  512. #(.removeEventListener el-popup "keyup" cb)))
  513. [])
  514. [:div.extensions__pdf-settings.hls-popup-wrap.visible
  515. {:on-click (fn [^js/MouseEvent e]
  516. (let [target (.-target e)]
  517. (when-not (.contains (rum/deref *el-popup) target)
  518. (hide-settings!))))}
  519. [:div.extensions__pdf-settings-inner.hls-popup-box
  520. {:ref *el-popup
  521. :tab-index -1}
  522. [:div.extensions__pdf-settings-item.theme-picker
  523. (map (fn [it]
  524. [:button.flex.items-center.justify-center
  525. {:key it :class it :on-click #(do (select-theme! it) (hide-settings!))}
  526. (if (= theme it) (svg/check))])
  527. ["light", "warm", "dark"])
  528. ]]]))
  529. (rum/defc pdf-outline-item
  530. [^js viewer
  531. {:keys [title items href parent dest expanded] :as node}
  532. {:keys [upt-outline-node!] :as ops}]
  533. (let [has-child? (seq items)
  534. expanded? (boolean expanded)]
  535. [:div.extensions__pdf-outline-item
  536. {:class (front-utils/classnames [{:has-children has-child? :is-expand expanded?}])}
  537. [:div.inner
  538. [:a
  539. {:href "javascript:void(0);"
  540. :data-dest (js/JSON.stringify (bean/->js dest))
  541. :on-click (fn [^js/MouseEvent e]
  542. (let [target (.-target e)]
  543. (if (.closest target "i")
  544. (let [path (map #(if (re-find #"\d+" %) (int %) (keyword %))
  545. (string/split parent #"\-"))]
  546. (.preventDefault e)
  547. (upt-outline-node! path {:expanded (not expanded?)}))
  548. (when-let [^js dest (and dest (bean/->js dest))]
  549. (.goToDestination (.-linkService viewer) dest)))))}
  550. [:i.arrow svg/arrow-right-v2]
  551. [:span title]]]
  552. ;; children
  553. (when (and has-child? expanded?)
  554. [:div.children
  555. (map-indexed
  556. (fn [idx itm]
  557. (let [parent (str parent "-items-" idx)]
  558. (rum/with-key
  559. (pdf-outline-item
  560. viewer
  561. (merge itm {:parent parent})
  562. ops) parent))) items)])]))
  563. (rum/defc pdf-outline
  564. [^js viewer visible? set-visible!]
  565. (when-let [^js pdf-doc (and viewer (.-pdfDocument viewer))]
  566. (let [*el-outline (rum/use-ref nil)
  567. [outline-data, set-outline-data!] (rum/use-state [])
  568. upt-outline-node! (rum/use-callback
  569. (fn [path attrs]
  570. (set-outline-data! (update-in outline-data path merge attrs)))
  571. [outline-data])]
  572. (rum/use-effect!
  573. (fn []
  574. (p/catch
  575. (p/let [^js data (.getOutline pdf-doc)]
  576. (when-let [data (and data (.map data (fn [^js it]
  577. (set! (.-href it) (.. viewer -linkService (getDestinationHash (.-dest it))))
  578. (set! (.-expanded it) false)
  579. it)))])
  580. (set-outline-data! (bean/->clj data)))
  581. (fn [e]
  582. (js/console.error "[Load outline Error]" e))))
  583. [pdf-doc])
  584. (rum/use-effect!
  585. (fn []
  586. (let [el-outline (rum/deref *el-outline)
  587. cb (fn [^js e]
  588. (and (= e.which 27) (set-visible! false)))]
  589. (js/setTimeout #(.focus el-outline))
  590. (.addEventListener el-outline "keyup" cb)
  591. #(.removeEventListener el-outline "keyup" cb)))
  592. [])
  593. [:div.extensions__pdf-outline-wrap.hls-popup-wrap
  594. {:class (front-utils/classnames [{:visible visible?}])
  595. :on-click (fn [^js/MouseEvent e]
  596. (let [target (.-target e)]
  597. (when-not (.contains (rum/deref *el-outline) target)
  598. (set-visible! false))))}
  599. [:div.extensions__pdf-outline.hls-popup-box
  600. {:ref *el-outline
  601. :tab-index -1}
  602. (if (seq outline-data)
  603. [:section
  604. (map-indexed (fn [idx itm]
  605. (rum/with-key
  606. (pdf-outline-item
  607. viewer
  608. (merge itm {:parent idx})
  609. {:upt-outline-node! upt-outline-node!})
  610. idx))
  611. outline-data)]
  612. [:section.is-empty "No outlines"])]])))
  613. (rum/defc docinfo-display
  614. [info close-fn!]
  615. [:div#pdf-docinfo.extensions__pdf-doc-info
  616. [:div.inner-text
  617. (for [[k v] info
  618. :let [k (str (string/replace-first (pr-str k) #"^\:" "") "::")]]
  619. [:p {:key k} [:strong k] " " [:i (pr-str v)]])]
  620. [:div.flex.items-center.justify-center.pt-2.pb--2
  621. (ui/button "Copy all"
  622. :on-click
  623. (fn []
  624. (let [text (.-innerText (js/document.querySelector "#pdf-docinfo > .inner-text"))
  625. text (string/replace-all text #"[\n\t]+" "\n")]
  626. (front-utils/copy-to-clipboard! text)
  627. (notification/show! "Copied!" :success)
  628. (close-fn!))))]])
  629. (defn make-docinfo-in-modal
  630. [info]
  631. (fn [close-fn!]
  632. (docinfo-display info close-fn!)))
  633. (rum/defc pdf-toolbar
  634. [^js viewer]
  635. (let [[area-mode? set-area-mode!] (use-atom *area-mode?)
  636. [outline-visible?, set-outline-visible!] (rum/use-state false)
  637. [settings-visible?, set-settings-visible!] (rum/use-state false)
  638. [viewer-theme, set-viewer-theme!] (rum/use-state (or (storage/get "ls-pdf-viewer-theme") "light"))]
  639. ;; themes hooks
  640. (rum/use-effect!
  641. (fn []
  642. (when-let [^js el (js/document.getElementById "pdf-layout-container")]
  643. (set! (. (. el -dataset) -theme) viewer-theme)
  644. (storage/set "ls-pdf-viewer-theme" viewer-theme)
  645. #(js-delete (. el -dataset) "theme")))
  646. [viewer-theme])
  647. (rum/with-context
  648. [[t] i18n/*tongue-context*]
  649. [:div.extensions__pdf-toolbar
  650. [:div.inner
  651. [:div.r.flex
  652. ;; appearance
  653. [:a.button
  654. {:title "More settings"
  655. :on-click #(set-settings-visible! (not settings-visible?))}
  656. (svg/adjustments 18)]
  657. ;; selection
  658. [:a.button
  659. {:title (str "Area highlight (" (if front-utils/mac? "⌘" "alt") ")")
  660. :class (if area-mode? "is-active")
  661. :on-click #(set-area-mode! (not area-mode?))}
  662. (svg/icon-area 18)]
  663. ;; zoom
  664. [:a.button
  665. {:title "Zoom out"
  666. :on-click (partial pdf-utils/zoom-out-viewer viewer)}
  667. (svg/zoom-out 18)]
  668. [:a.button
  669. {:title "Zoom in"
  670. :on-click (partial pdf-utils/zoom-in-viewer viewer)}
  671. (svg/zoom-in 18)]
  672. [:a.button
  673. {:title "Outline"
  674. :on-click #(set-outline-visible! (not outline-visible?))}
  675. (svg/view-list 16)]
  676. ;; metadata
  677. [:a.button.is-info
  678. {:title "Document info"
  679. :on-click #(do
  680. (p/let [ret (pdf-utils/get-meta-data$ viewer)]
  681. (state/set-modal! (make-docinfo-in-modal ret))))}
  682. (svg/icon-info)]
  683. [:a.button
  684. {:on-click #(state/set-state! :pdf/current nil)}
  685. (t :close)]]]
  686. ;; contents outline
  687. (pdf-outline viewer outline-visible? set-outline-visible!)
  688. ;; settings
  689. (and settings-visible?
  690. (pdf-settings
  691. viewer
  692. viewer-theme
  693. {:hide-settings! #(set-settings-visible! false)
  694. :select-theme! #(set-viewer-theme! %)}))])))
  695. (rum/defc pdf-viewer
  696. [url initial-hls ^js pdf-document ops]
  697. ;;(dd "==== render pdf-viewer ====")
  698. (let [*el-ref (rum/create-ref)
  699. [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
  700. [ano-state, set-ano-state!] (rum/use-state {:loaded-pages []})
  701. [page-ready?, set-page-ready!] (rum/use-state false)]
  702. ;; instant pdfjs viewer
  703. (rum/use-effect!
  704. (fn [] (let [^js event-bus (js/pdfjsViewer.EventBus.)
  705. ^js link-service (js/pdfjsViewer.PDFLinkService. #js {:eventBus event-bus :externalLinkTarget 2})
  706. ^js el (rum/deref *el-ref)
  707. ^js viewer (js/pdfjsViewer.PDFViewer.
  708. #js {:container el
  709. :eventBus event-bus
  710. :linkService link-service
  711. :enhanceTextSelection true
  712. :removePageBorders true})]
  713. (. link-service setDocument pdf-document)
  714. (. link-service setViewer viewer)
  715. ;; TODO: debug
  716. (set! (. js/window -lsPdfViewer) viewer)
  717. (p/then (. viewer setDocument pdf-document)
  718. #(set-state! {:viewer viewer :bus event-bus :link link-service :el el}))
  719. ;;TODO: destroy
  720. (fn []
  721. (when-let [last-page (.-currentPageNumber viewer)]
  722. (storage/set (str "ls-pdf-last-page-" (front-utils/node-path.basename url)) last-page))
  723. (.destroy pdf-document))))
  724. [])
  725. ;; interaction events
  726. (rum/use-effect!
  727. (fn []
  728. (when-let [^js viewer (:viewer state)]
  729. (let [^js el (rum/deref *el-ref)
  730. fn-textlayer-ready
  731. (fn [^js p]
  732. (set-ano-state! {:loaded-pages (conj (:loaded-pages ano-state) (int (.-pageNumber p)))}))
  733. fn-page-ready
  734. (fn []
  735. (set! (. viewer -currentScaleValue) "auto")
  736. (set-page-ready! true))]
  737. (doto (.-eventBus viewer)
  738. (.on "pagesinit" fn-page-ready)
  739. (.on "textlayerrendered" fn-textlayer-ready))
  740. #(do
  741. (doto (.-eventBus viewer)
  742. (.off "pagesinit" fn-page-ready)
  743. (.off "textlayerrendered" fn-textlayer-ready))))))
  744. [(:viewer state)
  745. (:loaded-pages ano-state)])
  746. (let [^js viewer (:viewer state)]
  747. [:div.extensions__pdf-viewer-cnt
  748. [:div.extensions__pdf-viewer {:ref *el-ref}
  749. [:div.pdfViewer "viewer pdf"]
  750. [:div.pp-holder]
  751. (when (and page-ready? viewer)
  752. [(rum/with-key
  753. (pdf-highlights
  754. (:el state) viewer
  755. initial-hls (:loaded-pages ano-state)
  756. ops) "pdf-highlights")])]
  757. (when (and page-ready? viewer)
  758. [(rum/with-key (pdf-resizer viewer) "pdf-resizer")
  759. (rum/with-key (pdf-toolbar viewer) "pdf-toolbar")])])))
  760. (rum/defc pdf-loader
  761. [{:keys [url hls-file] :as pdf-current}]
  762. (let [*doc-ref (rum/use-ref nil)
  763. [state, set-state!] (rum/use-state {:error nil :pdf-document nil :status nil})
  764. [hls-state, set-hls-state!] (rum/use-state {:initial-hls nil :latest-hls nil})
  765. repo-cur (state/get-current-repo)
  766. repo-dir (config/get-repo-dir repo-cur)
  767. set-dirty-hls! (fn [latest-hls] ;; TODO: incremental
  768. (set-hls-state! {:initial-hls [] :latest-hls latest-hls}))]
  769. ;; load highlights
  770. (rum/use-effect!
  771. (fn []
  772. (p/catch
  773. (p/let [data (pdf-assets/load-hls-data$ pdf-current)
  774. highlights (:highlights data)]
  775. (set-hls-state! {:initial-hls highlights}))
  776. ;; error
  777. (fn [e]
  778. (js/console.error "[load hls error]" e)
  779. (set-hls-state! {:initial-hls []})))
  780. ;; cancel
  781. #())
  782. [hls-file])
  783. ;; cache highlights
  784. (rum/use-effect!
  785. (fn []
  786. (when-let [hls (:latest-hls hls-state)]
  787. (p/catch
  788. (pdf-assets/persist-hls-data$ pdf-current hls)
  789. ;; write hls file error
  790. (fn [e]
  791. (js/console.error "[write hls error]" e)))))
  792. [(:latest-hls hls-state)])
  793. ;; load document
  794. (rum/use-effect!
  795. (fn []
  796. (let [get-doc$ (fn [^js opts] (.-promise (js/pdfjsLib.getDocument opts)))
  797. own-doc (rum/deref *doc-ref)
  798. opts {:url url
  799. :ownerDocument js/document
  800. ;;:cMapUrl "./js/pdfjs/cmaps/"
  801. :cMapUrl "https://cdn.jsdelivr.net/npm/[email protected]/cmaps/"
  802. :cMapPacked true}]
  803. (p/finally
  804. (p/catch (p/then
  805. (do
  806. (set-state! {:status :loading})
  807. (get-doc$ (clj->js opts)))
  808. #(set-state! {:pdf-document %}))
  809. #(set-state! {:error %}))
  810. #(set-state! {:status :completed}))
  811. #()))
  812. [url])
  813. (rum/use-effect!
  814. (fn []
  815. (dd "[ERROR loader]" (:error state)))
  816. [(:error state)])
  817. [:div.extensions__pdf-loader {:ref *doc-ref}
  818. (let [status-doc (:status state)
  819. initial-hls (:initial-hls hls-state)]
  820. (if (or (= status-doc :loading)
  821. (nil? initial-hls))
  822. [:div.flex.justify-center.items-center.h-screen.text-gray-500.text-md
  823. "Downloading PDF file " url]
  824. [(rum/with-key (pdf-viewer
  825. url initial-hls
  826. (:pdf-document state)
  827. {:set-dirty-hls! set-dirty-hls!}) "pdf-viewer")]))]))
  828. (rum/defc pdf-container
  829. [{:keys [identity] :as pdf-current}]
  830. (let [[prepared set-prepared!] (rum/use-state false)
  831. [ready set-ready!] (rum/use-state false)]
  832. ;; load assets
  833. (rum/use-effect!
  834. (fn []
  835. (p/then
  836. (pdf-utils/load-base-assets$)
  837. (fn [] (set-prepared! true))))
  838. [])
  839. ;; refresh loader
  840. (rum/use-effect!
  841. (fn []
  842. (js/setTimeout #(set-ready! true) 100)
  843. #(set-ready! false))
  844. [identity])
  845. [:div#pdf-layout-container.extensions__pdf-container
  846. (if (and prepared identity ready)
  847. (pdf-loader pdf-current))]))
  848. (rum/defc playground-effects
  849. [active]
  850. (rum/use-effect!
  851. (fn []
  852. (let [flg "is-pdf-active"
  853. ^js cls (.-classList js/document.body)]
  854. (and active (.add cls flg))
  855. #(.remove cls flg)))
  856. [active])
  857. nil)
  858. (rum/defcs playground
  859. < rum/static
  860. rum/reactive
  861. [state]
  862. (let [pdf-current (state/sub :pdf/current)]
  863. [:div.extensions__pdf-playground
  864. (playground-effects (not (nil? pdf-current)))
  865. (when pdf-current
  866. (js/ReactDOM.createPortal
  867. (pdf-container pdf-current)
  868. (js/document.querySelector "#app-single-container")))]))