highlights.cljs 42 KB

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