highlights.cljs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  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.handler.notification :as notification]
  7. [frontend.extensions.pdf.utils :as pdf-utils]
  8. [frontend.extensions.pdf.assets :as pdf-assets]
  9. [frontend.util :as front-utils]
  10. [frontend.state :as state]
  11. [frontend.config :as config]
  12. [frontend.components.svg :as svg]
  13. [medley.core :as medley]
  14. [frontend.fs :as fs]
  15. [clojure.string :as string]))
  16. (defn dd [& args]
  17. (apply js/console.debug args))
  18. (defn reset-current-pdf!
  19. []
  20. (state/set-state! :pdf/current nil))
  21. (rum/defc pdf-resizer
  22. [^js viewer]
  23. (let [el-ref (rum/use-ref nil)
  24. adjust-main-size!
  25. (front-utils/debounce
  26. 200 (fn [width]
  27. (let [root-el js/document.documentElement]
  28. (.setProperty (.-style root-el) "--ph-view-container-width" width)
  29. (pdf-utils/adjust-viewer-size! viewer))))]
  30. ;; draggable handler
  31. (rum/use-effect!
  32. (fn []
  33. (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
  34. (-> (js/interact el)
  35. (.draggable
  36. (bean/->js
  37. {:listeners
  38. {:move
  39. (fn [^js/MouseEvent e]
  40. (let [width js/document.documentElement.clientWidth
  41. offset (.-left (.-rect e))
  42. el-ratio (.toFixed (/ offset width) 6)
  43. target-el (js/document.getElementById "pdf-layout-container")]
  44. (when target-el
  45. (let [width (str (* el-ratio 100) "vw")]
  46. (.setProperty (.-style target-el) "width" width)
  47. (adjust-main-size! width)))))}}))
  48. (.styleCursor false)
  49. (.on "dragstart" #(.. js/document.documentElement -classList (add "is-resizing-buf")))
  50. (.on "dragend" #(.. js/document.documentElement -classList (remove "is-resizing-buf")))))
  51. #())
  52. [])
  53. [:span.extensions__pdf-resizer {:ref el-ref}]))
  54. (rum/defc pdf-highlights-ctx-menu
  55. [^js viewer {:keys [highlight vw-pos point]}
  56. {:keys [clear-ctx-tip! add-hl! upd-hl! del-hl!]}]
  57. (let [mounted (rum/use-ref false)]
  58. (rum/use-effect!
  59. (fn []
  60. (let [cb #(if-not (rum/deref mounted)
  61. (rum/set-ref! mounted true)
  62. (clear-ctx-tip!))]
  63. (js/document.addEventListener "click" cb)
  64. #(js/document.removeEventListener "click" cb)))
  65. [clear-ctx-tip!]))
  66. ;; TODO: precise position
  67. ;;(when-let [
  68. ;;page-bounding (and highlight (pdf-utils/get-page-bounding viewer (:page highlight)))
  69. ;;])
  70. (let [head-height 0 ;; 48 temp
  71. top (- (+ (:y point) (.. viewer -container -scrollTop)) head-height)
  72. left (:x point)
  73. id (:id highlight)
  74. content (:content highlight)]
  75. [:ul.extensions__pdf-hls-ctx-menu
  76. {:style {:top top :left left}
  77. :on-click (fn [^js/MouseEvent e]
  78. (when-let [action (.. e -target -dataset -action)]
  79. (case action
  80. "copy"
  81. (do
  82. (front-utils/copy-to-clipboard! (:text content))
  83. (pdf-utils/clear-all-selection))
  84. "del"
  85. (del-hl! highlight)
  86. ;; colors
  87. (let [properties {:color action}]
  88. (if-not id
  89. ;; add highlight
  90. (do
  91. (add-hl! (merge highlight
  92. {:id (pdf-utils/gen-uuid)
  93. :properties properties}))
  94. (pdf-utils/clear-all-selection))
  95. ;; update highlight
  96. (do
  97. (upd-hl! (assoc highlight :properties properties)))))))
  98. (clear-ctx-tip!))}
  99. [:li.item-colors
  100. (for [it ["yellow", "blue", "green", "red", "purple"]]
  101. [:a {:key it :data-color it :data-action it} it])]
  102. [:li.item {:data-action "copy"} "Copy text"]
  103. (and id [:li.item {:data-action "del"} "Delete"])
  104. ]))
  105. (rum/defc pdf-highlights-text-region
  106. [^js viewer vw-hl hl
  107. {:keys [show-ctx-tip!]}]
  108. (let [id (:id hl)
  109. {:keys [rects]} (:position vw-hl)
  110. {:keys [color]} (:properties hl)]
  111. [:div.extensions__pdf-hls-text-region
  112. {:on-click
  113. (fn [e]
  114. (let [x (.-clientX e)
  115. y (.-clientY e)]
  116. (show-ctx-tip! viewer hl {:x x :y y})))}
  117. (map-indexed
  118. (fn [idx rect]
  119. [:div.hls-text-region-item
  120. {:key idx
  121. :style rect
  122. :data-color color}])
  123. rects)]))
  124. (rum/defc pdf-highlights-region-container
  125. [^js viewer page-hls ops]
  126. [:div.hls-region-container
  127. (for [hl page-hls]
  128. (let [vw-hl (update-in hl [:position] #(pdf-utils/scaled-to-vw-pos viewer %))]
  129. (rum/with-key (pdf-highlights-text-region viewer vw-hl hl ops) (:id hl))
  130. ))])
  131. (rum/defc pdf-highlights
  132. [^js el ^js viewer initial-hls loaded-pages {:keys [set-dirty-hls!]}]
  133. (let [^js doc (.-ownerDocument el)
  134. ^js win (.-defaultView doc)
  135. *mounted (rum/use-ref false)
  136. [sel-state, set-sel-state!] (rum/use-state {:range nil :collapsed nil :point nil})
  137. [highlights, set-highlights!] (rum/use-state initial-hls)
  138. [tip-state, set-tip-state!] (rum/use-state {:highlight nil :vw-pos nil :point nil})
  139. clear-ctx-tip! #(set-tip-state! {})
  140. show-ctx-tip! (fn [^js viewer hl point]
  141. (let [vw-pos (pdf-utils/scaled-to-vw-pos viewer (:position hl))]
  142. (set-tip-state! {:highlight hl :vw-pos vw-pos :point point})))
  143. add-hl! (fn [hl] (when (:id hl)
  144. ;; fix js object
  145. (let [highlights (pdf-utils/fix-nested-js highlights)]
  146. (set-highlights! (conj highlights hl)))))
  147. upd-hl! (fn [hl]
  148. (let [highlights (pdf-utils/fix-nested-js highlights)]
  149. (when-let [[target-idx] (medley/find-first
  150. #(= (:id (second %)) (:id hl))
  151. (medley/indexed highlights))]
  152. (set-highlights! (assoc-in highlights [target-idx] hl)))))
  153. del-hl! (fn [hl] (when-let [id (:id hl)] (set-highlights! (into [] (remove #(= id (:id %)) highlights)))))]
  154. ;; consume dirtied
  155. (rum/use-effect!
  156. (fn []
  157. (if (rum/deref *mounted)
  158. (set-dirty-hls! highlights)
  159. (rum/set-ref! *mounted true)))
  160. [highlights])
  161. ;; selection events
  162. (rum/use-effect!
  163. (fn []
  164. (let [fn-selection-ok
  165. (fn [^js/MouseEvent e]
  166. (let [^js/Selection selection (js/document.getSelection)
  167. ^js/Range sel-range (.getRangeAt selection 0)]
  168. (cond
  169. (.-isCollapsed selection)
  170. (set-sel-state! {:collapsed true})
  171. (and sel-range (.contains el (.-commonAncestorContainer sel-range)))
  172. (set-sel-state! {:collapsed false :range sel-range :point {:x (.-clientX e) :y (.-clientY e)}}))))
  173. fn-selection
  174. (fn []
  175. (let [*dirty (volatile! false)
  176. fn-dirty #(vreset! *dirty true)]
  177. (js/document.addEventListener "selectionchange" fn-dirty)
  178. (js/document.addEventListener "mouseup"
  179. (fn [^js e]
  180. (and @*dirty (fn-selection-ok e))
  181. (js/document.removeEventListener "selectionchange" fn-dirty))
  182. #js {:once true})))
  183. fn-resize
  184. (partial pdf-utils/adjust-viewer-size! viewer)]
  185. ;;(doto (.-eventBus viewer))
  186. (doto el
  187. (.addEventListener "mousedown" fn-selection))
  188. (doto win
  189. (.addEventListener "resize" fn-resize))
  190. ;; destroy
  191. #(do
  192. ;;(doto (.-eventBus viewer))
  193. (doto el
  194. (.removeEventListener "mousedown" fn-selection))
  195. (doto win
  196. (.removeEventListener "resize" fn-resize)))))
  197. [viewer])
  198. ;; selection context menu
  199. (rum/use-effect!
  200. (fn []
  201. (when-let [^js sel-range (and (not (:collapsed sel-state)) (:range sel-state))]
  202. (when-let [page-info (pdf-utils/get-page-from-range sel-range)]
  203. (when-let [sel-rects (pdf-utils/get-range-rects<-page-cnt sel-range (:page-el page-info))]
  204. (let [page (int (:page-number page-info))
  205. ^js point (:point sel-state)
  206. ^js bounding (pdf-utils/get-bounding-rect sel-rects)
  207. vw-pos {:bounding bounding :rects sel-rects :page page}
  208. sc-pos (pdf-utils/vw-to-scaled-pos viewer vw-pos)]
  209. ;; TODO: debug
  210. ;;(dd "[VW x SC] ====>" vw-pos sc-pos)
  211. ;;(dd "[Range] ====> [" page-info "]" (.toString sel-range) point)
  212. ;;(dd "[Rects] ====>" sel-rects " [Bounding] ====>" bounding)
  213. (let [hl {:id nil
  214. :page page
  215. :position sc-pos
  216. :content {:text (.toString sel-range)}
  217. :properties {}}]
  218. ;; show context menu
  219. (set-tip-state! {:highlight hl
  220. :vw-pos vw-pos
  221. :point point})))))))
  222. [(:range sel-state)])
  223. ;; render hls
  224. (rum/use-effect!
  225. (fn []
  226. (dd "[rebuild highlights] " (count highlights))
  227. (when-let [grouped-hls (and (sequential? highlights) (group-by :page highlights))]
  228. (doseq [page loaded-pages]
  229. (when-let [^js/HTMLDivElement hls-layer (pdf-utils/resolve-hls-layer! viewer page)]
  230. (let [page-hls (get grouped-hls page)]
  231. (rum/mount
  232. ;; TODO: area & text hls
  233. (pdf-highlights-region-container viewer page-hls {:show-ctx-tip! show-ctx-tip!})
  234. hls-layer)))))
  235. ;; destroy
  236. #())
  237. [loaded-pages highlights])
  238. [:div.extensions__pdf-highlights-cnt
  239. ;; hl context tip menu
  240. (if (:highlight tip-state)
  241. (js/ReactDOM.createPortal
  242. (pdf-highlights-ctx-menu
  243. viewer tip-state
  244. {:clear-ctx-tip! clear-ctx-tip!
  245. :add-hl! add-hl!
  246. :del-hl! del-hl!
  247. :upd-hl! upd-hl!})
  248. (.querySelector el ".pp-holder")))
  249. ;; debug highlights anchor
  250. ;;(if (seq highlights)
  251. ;; [:ul.extensions__pdf-highlights
  252. ;; (for [hl highlights]
  253. ;; [:li
  254. ;; [:a
  255. ;; {:on-click #(pdf-utils/scroll-to-highlight viewer hl)}
  256. ;; (str "#" (:id hl) "# ")]
  257. ;; (:text (:content hl))])
  258. ;; ])
  259. ]))
  260. (rum/defc pdf-outline-item
  261. [^js viewer {:keys [title items href parent dest] :as node}]
  262. (let [has-child? (seq items)]
  263. [:div.extensions__pdf-outline-item
  264. {:class (if has-child? "has-children")}
  265. [:div.inner
  266. [:a
  267. {:href "javascript:;"
  268. :data-dest (js/JSON.stringify (bean/->js dest))
  269. :on-click (fn []
  270. (when-let [^js dest (and dest (bean/->js dest))]
  271. (.goToDestination (.-linkService viewer) dest)))}
  272. [:span title]]]
  273. ;; children
  274. (when has-child?
  275. [:div.children
  276. (map-indexed
  277. (fn [idx itm]
  278. (let [parent (str parent "-" idx)]
  279. (rum/with-key
  280. (pdf-outline-item viewer (merge itm {:parent parent})) parent))) items)])]))
  281. (rum/defc pdf-outline
  282. [^js viewer hide!]
  283. (when-let [^js pdf-doc (and viewer (.-pdfDocument viewer))]
  284. (let [*el-outline (rum/use-ref nil)
  285. [outline-data, set-outline-data!] (rum/use-state [])]
  286. (rum/use-effect!
  287. (fn []
  288. (p/catch
  289. (p/let [^js data (.getOutline pdf-doc)]
  290. (when-let [data (and data (.map data (fn [^js it]
  291. (set! (.-href it) (.. viewer -linkService (getDestinationHash (.-dest it))))
  292. it)))])
  293. (set-outline-data! (bean/->clj data)))
  294. (fn [e]
  295. (js/console.error "[Load outline Error]" e))))
  296. [pdf-doc])
  297. (rum/use-effect!
  298. (fn []
  299. (let [el-outline (rum/deref *el-outline)
  300. cb (fn [^js e]
  301. (and (= e.which 27) (hide!)))]
  302. (js/setTimeout #(.focus el-outline))
  303. (.addEventListener el-outline "keyup" cb)
  304. #(.removeEventListener el-outline "keyup" cb)))
  305. [])
  306. [:div.extensions__pdf-outline-wrap
  307. {:on-click (fn [^js/MouseEvent e]
  308. (let [target (.-target e)]
  309. (when-not (.contains (rum/deref *el-outline) target)
  310. (hide!))))}
  311. [:div.extensions__pdf-outline
  312. {:ref *el-outline
  313. :tab-index -1}
  314. (if (seq outline-data)
  315. [:section
  316. (map-indexed (fn [idx itm]
  317. (rum/with-key
  318. (pdf-outline-item viewer (merge itm {:parent idx}))
  319. idx))
  320. outline-data)]
  321. [:section.is-empty "No outlines"])]])))
  322. (rum/defc pdf-toolbar
  323. [^js viewer]
  324. (let [[outline-visible?, set-outline-visible!] (rum/use-state false)]
  325. [:div.extensions__pdf-toolbar
  326. [:div.inner
  327. [:div.r.flex
  328. ;; zoom
  329. [:a.button
  330. {:on-click (partial pdf-utils/zoom-out-viewer viewer)}
  331. (svg/zoom-out 18)]
  332. [:a.button
  333. {:on-click (partial pdf-utils/zoom-in-viewer viewer)}
  334. (svg/zoom-in 18)]
  335. [:a.button
  336. {:on-click #(set-outline-visible! (not outline-visible?))}
  337. (svg/view-list 16)]
  338. [:a.button
  339. {:on-click #(state/set-state! :pdf/current nil)}
  340. "close"]]]
  341. ;; contents outline
  342. (when outline-visible? (pdf-outline viewer #(set-outline-visible! false)))]))
  343. (rum/defc pdf-viewer
  344. [url initial-hls ^js pdf-document ops]
  345. (dd "==== render pdf-viewer ====")
  346. (let [*el-ref (rum/create-ref)
  347. [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
  348. [ano-state, set-ano-state!] (rum/use-state {:loaded-pages []})]
  349. ;; instant pdfjs viewer
  350. (rum/use-effect!
  351. (fn [] (let [^js event-bus (js/pdfjsViewer.EventBus.)
  352. ^js link-service (js/pdfjsViewer.PDFLinkService. #js {:eventBus event-bus :externalLinkTarget 2})
  353. ^js el (rum/deref *el-ref)
  354. ^js viewer (js/pdfjsViewer.PDFViewer.
  355. #js {:container el
  356. :eventBus event-bus
  357. :linkService link-service
  358. :enhanceTextSelection true
  359. :removePageBorders true})]
  360. (. link-service setDocument pdf-document)
  361. (. link-service setViewer viewer)
  362. ;; TODO: debug
  363. (set! (. js/window -lsPdfViewer) viewer)
  364. (p/then (. viewer setDocument pdf-document)
  365. #(set-state! {:viewer viewer :bus event-bus :link link-service :el el})))
  366. ;;TODO: destroy
  367. #(.destroy pdf-document))
  368. [])
  369. ;; interaction events
  370. (rum/use-effect!
  371. (fn []
  372. (when-let [^js viewer (:viewer state)]
  373. (let [^js el (rum/deref *el-ref)
  374. fn-textlayer-ready
  375. (fn [^js p]
  376. (set-ano-state! {:loaded-pages (conj (:loaded-pages ano-state) (int (.-pageNumber p)))}))
  377. fn-page-ready
  378. (fn []
  379. (set! (. viewer -currentScaleValue) "auto"))]
  380. (doto (.-eventBus viewer)
  381. (.on "pagesinit" fn-page-ready)
  382. (.on "textlayerrendered" fn-textlayer-ready))
  383. #(do
  384. (doto (.-eventBus viewer)
  385. (.off "pagesinit" fn-page-ready)
  386. (.off "textlayerrendered" fn-textlayer-ready))))))
  387. [(:viewer state)
  388. (:loaded-pages ano-state)])
  389. [:div.extensions__pdf-viewer-cnt
  390. [:div.extensions__pdf-viewer {:ref *el-ref}
  391. [:div.pdfViewer "viewer pdf"]
  392. [:div.pp-holder]]
  393. (if-let [^js viewer (:viewer state)]
  394. [(rum/with-key
  395. (pdf-highlights
  396. (:el state) viewer
  397. initial-hls (:loaded-pages ano-state)
  398. ops) "pdf-highlights")
  399. (rum/with-key (pdf-toolbar viewer) "pdf-toolbar")
  400. (rum/with-key (pdf-resizer viewer) "pdf-resizer")])]))
  401. (rum/defc pdf-loader
  402. [{:keys [url hls-file] :as pdf-current}]
  403. (let [*doc-ref (rum/use-ref nil)
  404. [state, set-state!] (rum/use-state {:error nil :pdf-document nil :status nil})
  405. [hls-state, set-hls-state!] (rum/use-state {:initial-hls nil :latest-hls nil})
  406. repo-cur (state/get-current-repo)
  407. repo-dir (config/get-repo-dir repo-cur)
  408. set-dirty-hls! (fn [latest-hls] ;; TODO: incremental
  409. (set-hls-state! {:initial-hls [] :latest-hls latest-hls}))]
  410. ;; load highlights
  411. (rum/use-effect!
  412. (fn []
  413. (p/catch
  414. (p/let [data (pdf-assets/load-hls-data$ pdf-current)
  415. highlights (:highlights data)]
  416. (set-hls-state! {:initial-hls highlights}))
  417. ;; error
  418. (fn [e]
  419. (js/console.error "[load hls error]" e)
  420. (set-hls-state! {:initial-hls []})))
  421. ;; cancel
  422. #())
  423. [hls-file])
  424. ;; cache highlights
  425. (rum/use-effect!
  426. (fn []
  427. (when-let [hls (:latest-hls hls-state)]
  428. (p/catch
  429. (pdf-assets/persist-hls-data$ pdf-current hls)
  430. ;; write hls file error
  431. (fn [e]
  432. (js/console.error "[write hls error]" e)))))
  433. [(:latest-hls hls-state)])
  434. ;; load document
  435. (rum/use-effect!
  436. (fn []
  437. (let [get-doc$ (fn [^js opts] (.-promise (js/pdfjsLib.getDocument opts)))
  438. own-doc (rum/deref *doc-ref)
  439. opts {:url url
  440. :ownerDocument js/document
  441. ;;:cMapUrl "./js/pdfjs/cmaps/"
  442. :cMapUrl "https://cdn.jsdelivr.net/npm/[email protected]/cmaps/"
  443. :cMapPacked true}]
  444. (p/finally
  445. (p/catch (p/then
  446. (do
  447. (set-state! {:status :loading})
  448. (get-doc$ (clj->js opts)))
  449. #(set-state! {:pdf-document %}))
  450. #(set-state! {:error %}))
  451. #(set-state! {:status :completed}))
  452. #()))
  453. [url])
  454. (rum/use-effect!
  455. (fn []
  456. (dd "[ERROR loader]" (:error state)))
  457. [(:error state)])
  458. [:div.extensions__pdf-loader {:ref *doc-ref}
  459. (let [status-doc (:status state)
  460. initial-hls (:initial-hls hls-state)]
  461. (if (or (= status-doc :loading)
  462. (nil? initial-hls))
  463. [:div.flex.justify-center.items-center.h-screen.text-gray-500.text-md
  464. "Downloading PDF file " url]
  465. [(rum/with-key (pdf-viewer
  466. url initial-hls
  467. (:pdf-document state)
  468. {:set-dirty-hls! set-dirty-hls!}) "pdf-viewer")]))]))
  469. (rum/defc pdf-container
  470. [pdf-current]
  471. (let [[prepared set-prepared!] (rum/use-state false)
  472. [ready set-ready!] (rum/use-state false)]
  473. ;; load assets
  474. (rum/use-effect!
  475. (fn []
  476. (p/then
  477. (pdf-utils/load-base-assets$)
  478. (fn [] (set-prepared! true))))
  479. [])
  480. ;; refresh loader
  481. (rum/use-effect!
  482. (fn []
  483. (js/setTimeout #(set-ready! true) 100)
  484. #(set-ready! false))
  485. [pdf-current])
  486. [:div#pdf-layout-container.extensions__pdf-container
  487. (if (and prepared pdf-current ready)
  488. (pdf-loader pdf-current))]))
  489. (rum/defc playground-effects
  490. [active]
  491. (rum/use-effect!
  492. (fn []
  493. (let [flg "is-pdf-active"
  494. ^js cls (.-classList js/document.body)]
  495. (and active (.add cls flg))
  496. #(.remove cls flg)))
  497. [active])
  498. nil)
  499. (rum/defcs playground < rum/reactive
  500. [state]
  501. (let [pdf-current (state/sub :pdf/current)]
  502. [:div.extensions__pdf-playground
  503. (playground-effects (not (nil? pdf-current)))
  504. (when pdf-current
  505. (js/ReactDOM.createPortal
  506. (pdf-container pdf-current)
  507. (js/document.querySelector "#app-single-container")))]))