core.cljs 45 KB

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