| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083 |
- (ns frontend.extensions.pdf.highlights
- (:require [cljs-bean.core :as bean]
- [clojure.string :as string]
- [frontend.components.svg :as svg]
- [frontend.config :as config]
- [frontend.context.i18n :as i18n]
- [frontend.extensions.pdf.assets :as pdf-assets]
- [frontend.extensions.pdf.utils :as pdf-utils]
- [frontend.handler.notification :as notification]
- [frontend.rum :refer [use-atom]]
- [frontend.state :as state]
- [frontend.storage :as storage]
- [frontend.ui :as ui]
- [frontend.util :as front-utils]
- [medley.core :as medley]
- [promesa.core :as p]
- [rum.core :as rum]))
- (defn dd [& args]
- (apply js/console.debug args))
- (def *area-mode? (atom false))
- (defn reset-current-pdf!
- []
- (state/set-state! :pdf/current nil))
- (rum/defcs pdf-highlight-finder
- < rum/static rum/reactive
- [state ^js viewer]
- (when viewer
- (when-let [ref-hl (state/sub :pdf/ref-highlight)]
- ;; delay handle: aim to fix page blink
- (js/setTimeout #(pdf-utils/scroll-to-highlight viewer ref-hl) 100)
- (js/setTimeout #(state/set-state! :pdf/ref-highlight nil) 1000))))
- (rum/defc pdf-page-finder < rum/static
- [^js viewer]
- (when viewer
- (when-let [current (:pdf/current @state/state)]
- (let [active-hl (:pdf/ref-highlight @state/state)
- page-key (:filename current)
- last-page (and page-key
- (front-utils/safe-parse-int (storage/get (str "ls-pdf-last-page-" page-key))))]
- (when (and last-page (nil? active-hl))
- (set! (.-currentPageNumber viewer) last-page)))))
- nil)
- (rum/defc pdf-resizer
- [^js viewer]
- (let [el-ref (rum/use-ref nil)
- adjust-main-size!
- (front-utils/debounce
- 200 (fn [width]
- (let [root-el js/document.documentElement]
- (.setProperty (.-style root-el) "--ph-view-container-width" width)
- (pdf-utils/adjust-viewer-size! viewer))))]
- ;; draggable handler
- (rum/use-effect!
- (fn []
- (when-let [el (and (fn? js/window.interact) (rum/deref el-ref))]
- (-> (js/interact el)
- (.draggable
- (bean/->js
- {:listeners
- {:move
- (fn [^js/MouseEvent e]
- (let [width js/document.documentElement.clientWidth
- offset (.-left (.-rect e))
- el-ratio (.toFixed (/ offset width) 6)
- target-el (js/document.getElementById "pdf-layout-container")]
- (when target-el
- (let [width (str (min (max (* el-ratio 100) 20) 80) "vw")]
- (.setProperty (.-style target-el) "width" width)
- (adjust-main-size! width)))))}}))
- (.styleCursor false)
- (.on "dragstart" #(.. js/document.documentElement -classList (add "is-resizing-buf")))
- (.on "dragend" #(.. js/document.documentElement -classList (remove "is-resizing-buf")))))
- #())
- [])
- [:span.extensions__pdf-resizer {:ref el-ref}]))
- (rum/defc pdf-highlights-ctx-menu
- [^js viewer
- {:keys [highlight vw-pos point]}
- {:keys [clear-ctx-tip! add-hl! upd-hl! del-hl!]}]
- (rum/use-effect!
- (fn []
- (let [cb #(clear-ctx-tip!)]
- (js/setTimeout #(js/document.addEventListener "click" cb))
- #(js/document.removeEventListener "click" cb)))
- [])
- ;; TODO: precise position
- ;;(when-let [
- ;;page-bounding (and highlight (pdf-utils/get-page-bounding viewer (:page highlight)))
- ;;])
- (let [*el (rum/use-ref nil)
- head-height 0 ;; 48 temp
- top (- (+ (:y point) (.. viewer -container -scrollTop)) head-height)
- left (:x point)
- id (:id highlight)
- content (:content highlight)]
- (rum/use-effect!
- (fn []
- (let [^js el (rum/deref *el)
- {:keys [x y]} (front-utils/calc-delta-rect-offset el (.closest el ".extensions__pdf-viewer"))]
- (set! (.. el -style -transform)
- (str "translate3d(" (if (neg? x) (- x 5) 0) "px," (if (neg? y) (- y 5) 0) "px" ",0)")))
- #())
- [])
- (rum/with-context
- [[t] i18n/*tongue-context*]
- [:ul.extensions__pdf-hls-ctx-menu
- {:ref *el
- :style {:top top :left left}
- :on-click (fn [^js/MouseEvent e]
- (when-let [action (.. e -target -dataset -action)]
- (case action
- "ref"
- (pdf-assets/copy-hl-ref! highlight)
- "copy"
- (do
- (front-utils/copy-to-clipboard! (:text content))
- (pdf-utils/clear-all-selection))
- "link"
- (do
- (pdf-assets/goto-block-ref! highlight))
- "del"
- (do
- (del-hl! highlight)
- (pdf-assets/del-ref-block! highlight)
- (pdf-assets/unlink-hl-area-image$ viewer (:pdf/current @state/state) highlight))
- ;; colors
- (let [properties {:color action}]
- (if-not id
- ;; add highlight
- (let [highlight (merge highlight
- {:id (pdf-utils/gen-uuid)
- :properties properties})]
- (add-hl! highlight)
- (pdf-utils/clear-all-selection)
- (pdf-assets/copy-hl-ref! highlight))
- ;; update highlight
- (do
- (upd-hl! (assoc highlight :properties properties)))))))
- (clear-ctx-tip!))}
- [:li.item-colors
- (for [it ["yellow", "blue", "green", "red", "purple"]]
- [:a {:key it :data-color it :data-action it} it])]
- (and id [:li.item {:data-action "ref"} (t :pdf/copy-ref)])
- (and (not (:image content)) [:li.item {:data-action "copy"} (t :pdf/copy-text)])
- (and id [:li.item {:data-action "link"} (t :pdf/linked-ref)])
- (and id [:li.item {:data-action "del"} (t :delete)])
- ])))
- (rum/defc pdf-highlights-text-region
- [^js viewer vw-hl hl
- {:keys [show-ctx-tip!]}]
- (let [id (:id hl)
- {:keys [rects]} (:position vw-hl)
- {:keys [color]} (:properties hl)
- open-tip! (fn [^js/MouseEvent e]
- (.preventDefault e)
- (let [x (.-clientX e)
- y (.-clientY e)]
- (show-ctx-tip! viewer hl {:x x :y y})))]
- [:div.extensions__pdf-hls-text-region
- {:on-click open-tip!
- :on-context-menu open-tip!}
- (map-indexed
- (fn [idx rect]
- [:div.hls-text-region-item
- {:key idx
- :style rect
- :data-color color}])
- rects)]))
- (rum/defc pdf-highlight-area-region
- [^js viewer vw-hl hl
- {:keys [show-ctx-tip! upd-hl!]}]
- (let [*el (rum/use-ref nil)
- *dirty (rum/use-ref nil)
- open-tip! (fn [^js/MouseEvent e]
- (.preventDefault e)
- (when-not (rum/deref *dirty)
- (let [x (.-clientX e)
- y (.-clientY e)]
- (show-ctx-tip! viewer hl {:x x :y y}))))]
- ;; resizable
- (rum/use-effect!
- (fn []
- (let [^js el (rum/deref *el)
- ^js it (-> (js/interact el)
- (.resizable
- (bean/->js
- {:edges {:left true :right true :top true :bottom true}
- :listeners {:start (fn [^js/MouseEvent e]
- (rum/set-ref! *dirty true))
- :end (fn [^js/MouseEvent e]
- (let [vw-pos (:position vw-hl)
- ^js target (. e -target)
- ^js vw-rect (. e -rect)
- [dx, dy] (mapv #(let [val (.getAttribute target (str "data-" (name %)))]
- (if-not (nil? val) (js/parseFloat val) 0)) [:x :y])
- to-top (+ (get-in vw-pos [:bounding :top]) dy)
- to-left (+ (get-in vw-pos [:bounding :left]) dx)
- to-w (. vw-rect -width)
- to-h (. vw-rect -height)
- to-vw-pos (update vw-pos :bounding assoc
- :top to-top
- :left to-left
- :width to-w
- :height to-h)
- to-sc-pos (pdf-utils/vw-to-scaled-pos viewer to-vw-pos)]
- ;; TODO: exception
- (let [hl' (assoc hl :position to-sc-pos)
- hl' (assoc-in hl' [:content :image] (js/Date.now))]
- (p/then
- (pdf-assets/persist-hl-area-image$ viewer
- (:pdf/current @state/state)
- hl' hl (:bounding to-vw-pos))
- (fn [] (js/setTimeout
- #(do
- ;; reset dom effects
- (set! (.. target -style -transform) (str "translate(0, 0)"))
- (.removeAttribute target "data-x")
- (.removeAttribute target "data-y")
- (upd-hl! hl')) 200))))
- (js/setTimeout #(rum/set-ref! *dirty false))))
- :move (fn [^js/MouseEvent e]
- (let [^js/HTMLElement target (.-target e)
- x (.getAttribute target "data-x")
- y (.getAttribute target "data-y")
- bx (if-not (nil? x) (js/parseFloat x) 0)
- by (if-not (nil? y) (js/parseFloat y) 0)]
- ;; update element style
- (set! (.. target -style -width) (str (.. e -rect -width) "px"))
- (set! (.. target -style -height) (str (.. e -rect -height) "px"))
- ;; translate when resizing from top or left edges
- (let [ax (+ bx (.. e -deltaRect -left))
- ay (+ by (.. e -deltaRect -top))]
- (set! (.. target -style -transform) (str "translate(" ax "px, " ay "px)"))
- ;; cache pos
- (.setAttribute target "data-x" ax)
- (.setAttribute target "data-y" ay))
- ))}
- :modifiers [;; minimum
- (js/interact.modifiers.restrictSize
- (bean/->js {:min {:width 60 :height 25}}))]
- :inertia true})
- ))]
- ;; destroy
- #(.unset it)))
- [hl])
- (when-let [vw-bounding (get-in vw-hl [:position :bounding])]
- (let [{:keys [color]} (:properties hl)]
- [:div.extensions__pdf-hls-area-region
- {:ref *el
- :style vw-bounding
- :data-color color
- :on-click open-tip!
- :on-context-menu open-tip!}]))))
- (rum/defc pdf-highlights-region-container
- [^js viewer page-hls ops]
- [:div.hls-region-container
- (for [hl page-hls]
- (let [vw-hl (update-in hl [:position] #(pdf-utils/scaled-to-vw-pos viewer %))]
- (rum/with-key
- (if (get-in hl [:content :image])
- (pdf-highlight-area-region viewer vw-hl hl ops)
- (pdf-highlights-text-region viewer vw-hl hl ops))
- (:id hl))
- ))])
- (rum/defc pdf-highlight-area-selection
- [^js viewer {:keys [clear-ctx-tip! show-ctx-tip!] :as ops}]
- (let [^js viewer-clt (.. viewer -viewer -classList)
- *el (rum/use-ref nil)
- *cnt-el (rum/use-ref nil)
- *sta-el (rum/use-ref nil)
- *cnt-rect (rum/use-ref nil)
- [start-coord, set-start-coord!] (rum/use-state nil)
- [end-coord, set-end-coord!] (rum/use-state nil)
- [_ set-area-mode!] (use-atom *area-mode?)
- should-start (fn [^js e]
- (let [^js target (.-target e)]
- (when (and (not
- (.contains (.-classList target) "extensions__pdf-hls-area-region"))
- (.closest target ".page"))
- (and e (or (.-metaKey e)
- (and front-utils/win32? (.-shiftKey e))
- @*area-mode?)))))
- reset-coords #(do
- (set-start-coord! nil)
- (set-end-coord! nil)
- (rum/set-ref! *sta-el nil))
- calc-coords (fn [page-x page-y]
- (when-let [cnt-el (or (rum/deref *cnt-el)
- (when-let [cnt-el (.querySelector (.closest (rum/deref *el) ".extensions__pdf-viewer-cnt") ".extensions__pdf-viewer")]
- (rum/set-ref! *cnt-el cnt-el) cnt-el))]
- (let [cnt-rect (rum/deref *cnt-rect)
- cnt-rect (or cnt-rect (bean/->clj (.toJSON (.getBoundingClientRect cnt-el))))
- _ (rum/set-ref! *cnt-rect cnt-rect)]
- {:x (- page-x (:left cnt-rect) (.-scrollLeft cnt-el))
- :y (-> page-y
- (- (:top cnt-rect))
- (+ (.-scrollTop cnt-el)))})))
- calc-pos (fn [start end]
- {:left (min (:x start) (:x end))
- :top (min (:y start) (:y end))
- :width (js/Math.abs (- (:x end) (:x start)))
- :height (js/Math.abs (- (:y end) (:y start)))})
- disable-text-selection! #(js-invoke viewer-clt (if % "add" "remove") "disabled-text-selection")
- fn-move (rum/use-callback
- (fn [^js/MouseEvent e]
- (set-end-coord! (calc-coords (.-pageX e) (.-pageY e))))
- [])]
- (rum/use-effect!
- (fn []
- (when-let [^js/HTMLElement root (.closest (rum/deref *el) ".extensions__pdf-container")]
- (let [fn-start (fn [^js/MouseEvent e]
- (if (should-start e)
- (do
- (rum/set-ref! *sta-el (.-target e))
- (set-start-coord! (calc-coords (.-pageX e) (.-pageY e)))
- (disable-text-selection! true)
- (.addEventListener root "mousemove" fn-move))
- ;; reset
- (reset-coords)))
- fn-end (fn [^js/MouseEvent e]
- (when-let [start-el (rum/deref *sta-el)]
- (let [end (calc-coords (.-pageX e) (.-pageY e))
- pos (calc-pos start-coord end)]
- (if (and (> (:width pos) 10)
- (> (:height pos) 10))
- (when-let [^js page-el (.closest start-el ".page")]
- (let [page-number (int (.-pageNumber (.-dataset page-el)))
- page-pos (merge pos {:top (- (:top pos) (.-offsetTop page-el))
- :left (- (:left pos) (.-offsetLeft page-el))})
- vw-pos {:bounding page-pos :rects [] :page page-number}
- sc-pos (pdf-utils/vw-to-scaled-pos viewer vw-pos)
- point {:x (.-clientX e) :y (.-clientY e)}
- hl {:id nil
- :page page-number
- :position sc-pos
- :content {:text "[:span]" :image (js/Date.now)}
- :properties {}}]
- ;; ctx tips
- (show-ctx-tip! viewer hl point {:reset-fn #(reset-coords)})
- ;; export area highlight
- ;;(dd "[selection end] :start"
- ;; start-coord ":end" end ":pos" pos
- ;; ":page" page-number
- ;; ":offset" page-pos
- ;; ":vw-pos" vw-pos
- ;; ":sc-pos" sc-pos)
- )
- (set-area-mode! false))
- ;; reset
- (reset-coords)))
- (disable-text-selection! false)
- (.removeEventListener root "mousemove" fn-move)))]
- (doto root
- (.addEventListener "mousedown" fn-start)
- (.addEventListener "mouseup" fn-end #js {:once true}))
- ;; destroy
- #(doto root
- (.removeEventListener "mousedown" fn-start)
- (.removeEventListener "mouseup" fn-end)))))
- [start-coord])
- [:div.extensions__pdf-area-selection
- {:ref *el}
- (when (and start-coord end-coord)
- [:div.shadow-rect {:style (calc-pos start-coord end-coord)}])]))
- (rum/defc pdf-highlights
- [^js el ^js viewer initial-hls loaded-pages {:keys [set-dirty-hls!]}]
- (let [^js doc (.-ownerDocument el)
- ^js win (.-defaultView doc)
- *mounted (rum/use-ref false)
- [sel-state, set-sel-state!] (rum/use-state {:range nil :collapsed nil :point nil})
- [highlights, set-highlights!] (rum/use-state initial-hls)
- [tip-state, set-tip-state!] (rum/use-state {:highlight nil :vw-pos nil :point nil :reset-fn nil})
- clear-ctx-tip! (rum/use-callback
- #(let [reset-fn (:reset-fn tip-state)]
- (set-tip-state! {})
- (and (fn? reset-fn) (reset-fn)))
- [tip-state])
- show-ctx-tip! (fn [^js viewer hl point & ops]
- (let [vw-pos (pdf-utils/scaled-to-vw-pos viewer (:position hl))]
- (set-tip-state! (apply merge (list* {:highlight hl :vw-pos vw-pos :point point} ops)))))
- add-hl! (fn [hl] (when (:id hl)
- ;; fix js object
- (let [highlights (pdf-utils/fix-nested-js highlights)]
- (set-highlights! (conj highlights hl)))
- (when-let [vw-pos (and (pdf-assets/area-highlight? hl)
- (pdf-utils/scaled-to-vw-pos viewer (:position hl)))]
- ;; exceptions
- (pdf-assets/persist-hl-area-image$ viewer (:pdf/current @state/state)
- hl nil (:bounding vw-pos)))))
- upd-hl! (fn [hl]
- (let [highlights (pdf-utils/fix-nested-js highlights)]
- (when-let [[target-idx] (medley/find-first
- #(= (:id (second %)) (:id hl))
- (medley/indexed highlights))]
- (set-highlights! (assoc-in highlights [target-idx] hl))
- (pdf-assets/update-hl-area-block! hl))))
- del-hl! (fn [hl] (when-let [id (:id hl)] (set-highlights! (into [] (remove #(= id (:id %)) highlights)))))]
- ;; consume dirtied
- (rum/use-effect!
- (fn []
- (if (rum/deref *mounted)
- (set-dirty-hls! highlights)
- (rum/set-ref! *mounted true)))
- [highlights])
- ;; selection events
- (rum/use-effect!
- (fn []
- (let [fn-selection-ok
- (fn [^js/MouseEvent e]
- (let [^js/Selection selection (js/document.getSelection)
- ^js/Range sel-range (.getRangeAt selection 0)]
- (cond
- (.-isCollapsed selection)
- (set-sel-state! {:collapsed true})
- (and sel-range (.contains el (.-commonAncestorContainer sel-range)))
- (set-sel-state! {:collapsed false :range sel-range :point {:x (.-clientX e) :y (.-clientY e)}}))))
- fn-selection
- (fn []
- (let [*dirty (volatile! false)
- fn-dirty #(vreset! *dirty true)]
- (js/document.addEventListener "selectionchange" fn-dirty)
- (js/document.addEventListener "mouseup"
- (fn [^js e]
- (and @*dirty (fn-selection-ok e))
- (js/document.removeEventListener "selectionchange" fn-dirty))
- #js {:once true})))
- fn-resize
- (partial pdf-utils/adjust-viewer-size! viewer)]
- ;;(doto (.-eventBus viewer))
- (doto el
- (.addEventListener "mousedown" fn-selection))
- (doto win
- (.addEventListener "resize" fn-resize))
- ;; destroy
- #(do
- ;;(doto (.-eventBus viewer))
- (doto el
- (.removeEventListener "mousedown" fn-selection))
- (doto win
- (.removeEventListener "resize" fn-resize)))))
- [viewer])
- ;; selection context menu
- (rum/use-effect!
- (fn []
- (when-let [^js sel-range (and (not (:collapsed sel-state)) (:range sel-state))]
- (when-let [page-info (pdf-utils/get-page-from-range sel-range)]
- (when-let [sel-rects (pdf-utils/get-range-rects<-page-cnt sel-range (:page-el page-info))]
- (let [page (int (:page-number page-info))
- ^js point (:point sel-state)
- ^js bounding (pdf-utils/get-bounding-rect sel-rects)
- vw-pos {:bounding bounding :rects sel-rects :page page}
- sc-pos (pdf-utils/vw-to-scaled-pos viewer vw-pos)]
- ;; TODO: debug
- ;;(dd "[VW x SC] ====>" vw-pos sc-pos)
- ;;(dd "[Range] ====> [" page-info "]" (.toString sel-range) point)
- ;;(dd "[Rects] ====>" sel-rects " [Bounding] ====>" bounding)
- (let [hl {:id nil
- :page page
- :position sc-pos
- :content {:text (.toString sel-range)}
- :properties {}}]
- ;; show context menu
- (set-tip-state! {:highlight hl
- :vw-pos vw-pos
- :point point})))))))
- [(:range sel-state)])
- ;; render hls
- (rum/use-effect!
- (fn []
- ;;(dd "=== rebuild highlights ===" (count highlights))
- (when-let [grouped-hls (and (sequential? highlights) (group-by :page highlights))]
- (doseq [page loaded-pages]
- (when-let [^js/HTMLDivElement hls-layer (pdf-utils/resolve-hls-layer! viewer page)]
- (let [page-hls (get grouped-hls page)]
- (rum/mount
- ;; TODO: area & text hls
- (pdf-highlights-region-container
- viewer page-hls {:show-ctx-tip! show-ctx-tip!
- :upd-hl! upd-hl!})
- hls-layer)))))
- ;; destroy
- #())
- [loaded-pages highlights])
- [:div.extensions__pdf-highlights-cnt
- ;; hl context tip menu
- (when (:highlight tip-state)
- (js/ReactDOM.createPortal
- (pdf-highlights-ctx-menu
- viewer tip-state
- {:clear-ctx-tip! clear-ctx-tip!
- :add-hl! add-hl!
- :del-hl! del-hl!
- :upd-hl! upd-hl!})
- (.querySelector el ".pp-holder")))
- ;; debug highlights anchor
- ;;(if (seq highlights)
- ;; [:ul.extensions__pdf-highlights
- ;; (for [hl highlights]
- ;; [:li
- ;; [:a
- ;; {:on-click #(pdf-utils/scroll-to-highlight viewer hl)}
- ;; (str "#" (:id hl) "# ")]
- ;; (:text (:content hl))])
- ;; ])
- ;; refs
- (pdf-highlight-finder viewer)
- (pdf-page-finder viewer)
- ;; area selection container
- (pdf-highlight-area-selection
- viewer
- {:clear-ctx-tip! clear-ctx-tip!
- :show-ctx-tip! show-ctx-tip!
- :add-hl! add-hl!
- })]))
- (rum/defc pdf-settings
- [^js viewer theme {:keys [hide-settings! select-theme!]}]
- (let [*el-popup (rum/use-ref nil)]
- (rum/use-effect!
- (fn []
- (let [el-popup (rum/deref *el-popup)
- cb (fn [^js e]
- (and (= e.which 27) (hide-settings!)))]
- (js/setTimeout #(.focus el-popup))
- (.addEventListener el-popup "keyup" cb)
- #(.removeEventListener el-popup "keyup" cb)))
- [])
- [:div.extensions__pdf-settings.hls-popup-wrap.visible
- {:on-click (fn [^js/MouseEvent e]
- (let [target (.-target e)]
- (when-not (.contains (rum/deref *el-popup) target)
- (hide-settings!))))}
- [:div.extensions__pdf-settings-inner.hls-popup-box
- {:ref *el-popup
- :tab-index -1}
- [:div.extensions__pdf-settings-item.theme-picker
- (map (fn [it]
- [:button.flex.items-center.justify-center
- {:key it :class it :on-click #(do (select-theme! it) (hide-settings!))}
- (when (= theme it) (svg/check))])
- ["light", "warm", "dark"])
- ]]]))
- (rum/defc pdf-outline-item
- [^js viewer
- {:keys [title items href parent dest expanded] :as node}
- {:keys [upt-outline-node!] :as ops}]
- (let [has-child? (seq items)
- expanded? (boolean expanded)]
- [:div.extensions__pdf-outline-item
- {:class (front-utils/classnames [{:has-children has-child? :is-expand expanded?}])}
- [:div.inner
- [:a
- {:href "javascript:void(0);"
- :data-dest (js/JSON.stringify (bean/->js dest))
- :on-click (fn [^js/MouseEvent e]
- (let [target (.-target e)]
- (if (.closest target "i")
- (let [path (map #(if (re-find #"\d+" %) (int %) (keyword %))
- (string/split parent #"\-"))]
- (.preventDefault e)
- (upt-outline-node! path {:expanded (not expanded?)}))
- (when-let [^js dest (and dest (bean/->js dest))]
- (.goToDestination (.-linkService viewer) dest)))))}
- [:i.arrow svg/arrow-right-v2]
- [:span title]]]
- ;; children
- (when (and has-child? expanded?)
- [:div.children
- (map-indexed
- (fn [idx itm]
- (let [parent (str parent "-items-" idx)]
- (rum/with-key
- (pdf-outline-item
- viewer
- (merge itm {:parent parent})
- ops) parent))) items)])]))
- (rum/defc pdf-outline
- [^js viewer visible? set-visible!]
- (when-let [^js pdf-doc (and viewer (.-pdfDocument viewer))]
- (let [*el-outline (rum/use-ref nil)
- [outline-data, set-outline-data!] (rum/use-state [])
- upt-outline-node! (rum/use-callback
- (fn [path attrs]
- (set-outline-data! (update-in outline-data path merge attrs)))
- [outline-data])]
- (rum/use-effect!
- (fn []
- (p/catch
- (p/let [^js data (.getOutline pdf-doc)]
- (when-let [data (and data (.map data (fn [^js it]
- (set! (.-href it) (.. viewer -linkService (getDestinationHash (.-dest it))))
- (set! (.-expanded it) false)
- it)))])
- (set-outline-data! (bean/->clj data)))
- (fn [e]
- (js/console.error "[Load outline Error]" e))))
- [pdf-doc])
- (rum/use-effect!
- (fn []
- (let [el-outline (rum/deref *el-outline)
- cb (fn [^js e]
- (and (= e.which 27) (set-visible! false)))]
- (js/setTimeout #(.focus el-outline))
- (.addEventListener el-outline "keyup" cb)
- #(.removeEventListener el-outline "keyup" cb)))
- [])
- [:div.extensions__pdf-outline-wrap.hls-popup-wrap
- {:class (front-utils/classnames [{:visible visible?}])
- :on-click (fn [^js/MouseEvent e]
- (let [target (.-target e)]
- (when-not (.contains (rum/deref *el-outline) target)
- (set-visible! false))))}
- [:div.extensions__pdf-outline.hls-popup-box
- {:ref *el-outline
- :tab-index -1}
- (if (seq outline-data)
- [:section
- (map-indexed (fn [idx itm]
- (rum/with-key
- (pdf-outline-item
- viewer
- (merge itm {:parent idx})
- {:upt-outline-node! upt-outline-node!})
- idx))
- outline-data)]
- [:section.is-empty "No outlines"])]])))
- (rum/defc docinfo-display
- [info close-fn!]
- [:div#pdf-docinfo.extensions__pdf-doc-info
- [:div.inner-text
- (for [[k v] info
- :let [k (str (string/replace-first (pr-str k) #"^\:" "") "::")]]
- [:p {:key k} [:strong k] " " [:i (pr-str v)]])]
- [:div.flex.items-center.justify-center.pt-2.pb--2
- (ui/button "Copy all"
- :on-click
- (fn []
- (let [text (.-innerText (js/document.querySelector "#pdf-docinfo > .inner-text"))
- text (string/replace-all text #"[\n\t]+" "\n")]
- (front-utils/copy-to-clipboard! text)
- (notification/show! "Copied!" :success)
- (close-fn!))))]])
- (defn make-docinfo-in-modal
- [info]
- (fn [close-fn!]
- (docinfo-display info close-fn!)))
- (rum/defc pdf-toolbar
- [^js viewer]
- (let [[area-mode? set-area-mode!] (use-atom *area-mode?)
- [outline-visible?, set-outline-visible!] (rum/use-state false)
- [settings-visible?, set-settings-visible!] (rum/use-state false)
- [viewer-theme, set-viewer-theme!] (rum/use-state (or (storage/get "ls-pdf-viewer-theme") "light"))]
- ;; themes hooks
- (rum/use-effect!
- (fn []
- (when-let [^js el (js/document.getElementById "pdf-layout-container")]
- (set! (. (. el -dataset) -theme) viewer-theme)
- (storage/set "ls-pdf-viewer-theme" viewer-theme)
- #(js-delete (. el -dataset) "theme")))
- [viewer-theme])
- (rum/with-context
- [[t] i18n/*tongue-context*]
- [:div.extensions__pdf-toolbar
- [:div.inner
- [:div.r.flex.buttons
- ;; appearance
- [:a.button
- {:title "More settings"
- :on-click #(set-settings-visible! (not settings-visible?))}
- (svg/adjustments 18)]
- ;; selection
- [:a.button
- {:title (str "Area highlight (" (if front-utils/mac? "⌘" "Shift") ")")
- :class (when area-mode? "is-active")
- :on-click #(set-area-mode! (not area-mode?))}
- (svg/icon-area 18)]
- ;; zoom
- [:a.button
- {:title "Zoom out"
- :on-click (partial pdf-utils/zoom-out-viewer viewer)}
- (svg/zoom-out 18)]
- [:a.button
- {:title "Zoom in"
- :on-click (partial pdf-utils/zoom-in-viewer viewer)}
- (svg/zoom-in 18)]
- [:a.button
- {:title "Outline"
- :on-click #(set-outline-visible! (not outline-visible?))}
- (svg/view-list 16)]
- ;; metadata
- [:a.button.is-info
- {:title "Document info"
- :on-click #(do
- (p/let [ret (pdf-utils/get-meta-data$ viewer)]
- (state/set-modal! (make-docinfo-in-modal ret))))}
- (svg/icon-info)]
- [:a.button
- {:on-click #(state/set-state! :pdf/current nil)}
- (t :close)]]]
- ;; contents outline
- (pdf-outline viewer outline-visible? set-outline-visible!)
- ;; settings
- (and settings-visible?
- (pdf-settings
- viewer
- viewer-theme
- {:hide-settings! #(set-settings-visible! false)
- :select-theme! #(set-viewer-theme! %)}))])))
- (rum/defc pdf-viewer
- [url initial-hls ^js pdf-document ops]
- ;;(dd "==== render pdf-viewer ====")
- (let [*el-ref (rum/create-ref)
- [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
- [ano-state, set-ano-state!] (rum/use-state {:loaded-pages []})
- [page-ready?, set-page-ready!] (rum/use-state false)]
- ;; instant pdfjs viewer
- (rum/use-effect!
- (fn [] (let [^js event-bus (js/pdfjsViewer.EventBus.)
- ^js link-service (js/pdfjsViewer.PDFLinkService. #js {:eventBus event-bus :externalLinkTarget 2})
- ^js el (rum/deref *el-ref)
- ^js viewer (js/pdfjsViewer.PDFViewer.
- #js {:container el
- :eventBus event-bus
- :linkService link-service
- :enhanceTextSelection true
- :removePageBorders true})]
- (. link-service setDocument pdf-document)
- (. link-service setViewer viewer)
- ;; TODO: debug
- (set! (. js/window -lsPdfViewer) viewer)
- (p/then (. viewer setDocument pdf-document)
- #(set-state! {:viewer viewer :bus event-bus :link link-service :el el}))
- ;;TODO: destroy
- (fn []
- (when-let [last-page (.-currentPageNumber viewer)]
- (storage/set (str "ls-pdf-last-page-" (front-utils/node-path.basename url)) last-page))
- (when pdf-document (.destroy pdf-document)))))
- [])
- ;; interaction events
- (rum/use-effect!
- (fn []
- (when-let [^js viewer (:viewer state)]
- (let [^js el (rum/deref *el-ref)
- fn-textlayer-ready
- (fn [^js p]
- (set-ano-state! {:loaded-pages (conj (:loaded-pages ano-state) (int (.-pageNumber p)))}))
- fn-page-ready
- (fn []
- (set! (. viewer -currentScaleValue) "auto")
- (set-page-ready! true))]
- (doto (.-eventBus viewer)
- (.on "pagesinit" fn-page-ready)
- (.on "textlayerrendered" fn-textlayer-ready))
- #(do
- (doto (.-eventBus viewer)
- (.off "pagesinit" fn-page-ready)
- (.off "textlayerrendered" fn-textlayer-ready))))))
- [(:viewer state)
- (:loaded-pages ano-state)])
- (let [^js viewer (:viewer state)]
- [:div.extensions__pdf-viewer-cnt
- [:div.extensions__pdf-viewer {:ref *el-ref}
- [:div.pdfViewer "viewer pdf"]
- [:div.pp-holder]
- (when (and page-ready? viewer)
- [(rum/with-key
- (pdf-highlights
- (:el state) viewer
- initial-hls (:loaded-pages ano-state)
- ops) "pdf-highlights")])]
- (when (and page-ready? viewer)
- [(rum/with-key (pdf-resizer viewer) "pdf-resizer")
- (rum/with-key (pdf-toolbar viewer) "pdf-toolbar")])])))
- (rum/defc pdf-loader
- [{:keys [url hls-file] :as pdf-current}]
- (let [*doc-ref (rum/use-ref nil)
- [state, set-state!] (rum/use-state {:error nil :pdf-document nil :status nil})
- [hls-state, set-hls-state!] (rum/use-state {:initial-hls nil :latest-hls nil})
- repo-cur (state/get-current-repo)
- repo-dir (config/get-repo-dir repo-cur)
- set-dirty-hls! (fn [latest-hls] ;; TODO: incremental
- (set-hls-state! {:initial-hls [] :latest-hls latest-hls}))]
- ;; load highlights
- (rum/use-effect!
- (fn []
- (p/catch
- (p/let [data (pdf-assets/load-hls-data$ pdf-current)
- highlights (:highlights data)]
- (set-hls-state! {:initial-hls highlights}))
- ;; error
- (fn [e]
- (js/console.error "[load hls error]" e)
- (set-hls-state! {:initial-hls []})))
- ;; cancel
- #())
- [hls-file])
- ;; cache highlights
- (rum/use-effect!
- (fn []
- (when-let [hls (:latest-hls hls-state)]
- (p/catch
- (pdf-assets/persist-hls-data$ pdf-current hls)
- ;; write hls file error
- (fn [e]
- (js/console.error "[write hls error]" e)))))
- [(:latest-hls hls-state)])
- ;; load document
- (rum/use-effect!
- (fn []
- (let [get-doc$ (fn [^js opts] (.-promise (js/pdfjsLib.getDocument opts)))
- own-doc (rum/deref *doc-ref)
- opts {:url url
- :ownerDocument js/document
- :cMapUrl "./js/pdfjs/cmaps/"
- ;;:cMapUrl "https://cdn.jsdelivr.net/npm/[email protected]/cmaps/"
- :cMapPacked true}]
- (p/finally
- (p/catch (p/then
- (do
- (set-state! {:status :loading})
- (get-doc$ (clj->js opts)))
- #(set-state! {:pdf-document %}))
- #(set-state! {:error %}))
- #(set-state! {:status :completed}))
- #()))
- [url])
- (rum/use-effect!
- (fn []
- (when-let [error (:error state)]
- (dd "[ERROR loader]" (:error state))
- (case (.-name error)
- "MissingPDFException"
- (do
- (notification/show!
- (str (.-message error) " Is this the correct path?")
- :error
- false)
- (state/set-state! :pdf/current nil)))))
- [(:error state)])
- [:div.extensions__pdf-loader {:ref *doc-ref}
- (let [status-doc (:status state)
- initial-hls (:initial-hls hls-state)]
- (if (or (= status-doc :loading)
- (nil? initial-hls))
- [:div.flex.justify-center.items-center.h-screen.text-gray-500.text-md
- "Downloading PDF file " url]
- [(rum/with-key (pdf-viewer
- url initial-hls
- (:pdf-document state)
- {:set-dirty-hls! set-dirty-hls!}) "pdf-viewer")]))]))
- (rum/defc pdf-container
- [{:keys [identity] :as pdf-current}]
- (let [[prepared set-prepared!] (rum/use-state false)
- [ready set-ready!] (rum/use-state false)]
- ;; load assets
- (rum/use-effect!
- (fn []
- (p/then
- (pdf-utils/load-base-assets$)
- (fn [] (set-prepared! true))))
- [])
- ;; refresh loader
- (rum/use-effect!
- (fn []
- (js/setTimeout #(set-ready! true) 100)
- #(set-ready! false))
- [identity])
- [:div#pdf-layout-container.extensions__pdf-container
- (when (and prepared identity ready)
- (pdf-loader pdf-current))]))
- (rum/defc playground-effects
- [active]
- (rum/use-effect!
- (fn []
- (let [flg "is-pdf-active"
- ^js cls (.-classList js/document.body)]
- (and active (.add cls flg))
- #(.remove cls flg)))
- [active])
- nil)
- (rum/defcs playground
- < rum/static
- rum/reactive
- [state]
- (let [pdf-current (state/sub :pdf/current)]
- [:div.extensions__pdf-playground
- (playground-effects (not (nil? pdf-current)))
- (when pdf-current
- (js/ReactDOM.createPortal
- (pdf-container pdf-current)
- (js/document.querySelector "#app-single-container")))]))
|