Files
logseq/src/main/frontend/extensions/pdf/core.cljs
2023-11-01 21:11:34 +08:00

1071 lines
45 KiB
Clojure

(ns frontend.extensions.pdf.core
(:require [cljs-bean.core :as bean]
[clojure.string :as string]
[frontend.components.svg :as svg]
[frontend.components.block :as block]
[frontend.context.i18n :refer [t]]
[frontend.extensions.pdf.assets :as pdf-assets]
[frontend.extensions.pdf.utils :as pdf-utils]
[frontend.extensions.pdf.toolbar :refer [pdf-toolbar *area-dashed? *area-mode? *highlight-mode? *highlights-ctx*]]
[frontend.extensions.pdf.windows :as pdf-windows]
[frontend.handler.notification :as notification]
[frontend.config :as config]
[frontend.modules.shortcut.core :as shortcut]
[frontend.commands :as commands]
[frontend.rum :refer [use-atom]]
[frontend.state :as state]
[frontend.util :as util]
[medley.core :as medley]
[promesa.core :as p]
[rum.core :as rum]))
(declare pdf-container system-embed-playground)
(def *highlight-last-color (atom :yellow))
(defn open-external-win! [pdf-current]
(pdf-windows/open-pdf-in-new-window! system-embed-playground pdf-current))
(defn reset-current-pdf!
[]
(state/set-state! :pdf/current nil))
(rum/defcs pdf-highlight-finder
< rum/static rum/reactive
(rum/local false ::mounted?)
[state ^js viewer]
(let [*mounted? (::mounted? state)]
(when viewer
(when-let [ref-hl (state/sub :pdf/ref-highlight)]
;; delay handle: aim to fix page blink
(js/setTimeout
(fn []
(if (:id ref-hl)
(pdf-utils/scroll-to-highlight viewer ref-hl)
(set! (.-currentPageNumber viewer) (or (:page ref-hl) 1))))
(if @*mounted? 50 500))
(js/setTimeout
#(state/set-state! :pdf/ref-highlight nil) 1000)))
(reset! *mounted? true)))
(rum/defc pdf-page-finder < rum/static
[^js viewer]
(rum/use-effect!
(fn []
(when viewer
(when-let [_ (:pdf/current @state/state)]
(let [active-hl (:pdf/ref-highlight @state/state)]
(when-not active-hl
(.on (.-eventBus viewer) (name :restore-last-page)
(fn [last-page]
(when last-page
(set! (.-currentPageNumber viewer) (util/safe-parse-int last-page))))))))))
[viewer])
nil)
(rum/defc pdf-resizer
"Watches for changes in the pdf container's width and adjusts the viewer."
[^js viewer]
(let [el-ref (rum/use-ref nil)
adjust-main-size!
(util/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))))
group-id (.-$groupIdentity 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 (str "pdf-layout-container_" group-id))]
(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 ^:large-vars/data-var pdf-highlights-ctx-menu
"The contextual menu which appears over a text selection and allows e.g. creating a highlight."
[^js viewer
{:keys [highlight point ^js selection]}
{:keys [clear-ctx-menu! add-hl! upd-hl! del-hl!]}]
(rum/use-effect!
(fn []
(let [cb #(clear-ctx-menu!)
doc (pdf-windows/resolve-own-document viewer)]
(js/setTimeout #(.addEventListener doc "click" cb))
#(.removeEventListener doc "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)
^js cnt (.-container viewer)
head-height 0 ;; 48 temp
top (- (+ (:y point) (.-scrollTop cnt)) head-height)
left (+ (:x point) (.-scrollLeft cnt))
id (:id highlight)
new? (nil? id)
new-&-highlight-mode? (and @*highlight-mode? new?)
show-ctx-menu? (and (not new-&-highlight-mode?)
(or (not selection) (and selection (state/sub :pdf/auto-open-ctx-menu?))))
content (:content highlight)
area? (not (string/blank? (:image content)))
action-fn! (fn [action clear?]
(when-let [action (and action (name action))]
(let [highlight (if (fn? highlight) (highlight) highlight)
content (:content highlight)]
(case action
"ref"
(pdf-assets/copy-hl-ref! highlight viewer)
"copy"
(do
(util/copy-to-clipboard!
(or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection)))
:owner-window (pdf-windows/resolve-own-window viewer))
(pdf-utils/clear-all-selection))
"link"
(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))
"hook"
:dune
;; 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 viewer))
;; update highlight
(upd-hl! (assoc highlight :properties properties)))
(reset! *highlight-last-color (keyword action)))))
(and clear? (js/setTimeout #(clear-ctx-menu!) 68))))]
(rum/use-effect!
(fn []
(if new-&-highlight-mode?
;; wait for selection cleared ...
(js/setTimeout #(action-fn! @*highlight-last-color true) 300)
(let [^js el (rum/deref *el)
{:keys [x y]} (util/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)"))))
#())
[])
[:ul.extensions__pdf-hls-ctx-menu
{:ref *el
:style {:top top
:left left
:visibility (if show-ctx-menu? "visible" "hidden")}
:on-click (fn [^js/MouseEvent e]
(.stopPropagation e)
(when-let [action (.. e -target -dataset -action)]
(action-fn! action true)))}
[:li.item-colors
(for [it ["yellow", "red", "green", "blue", "purple"]]
[:a {:key it :data-color it :data-action it} it])]
(and id [:li.item {:data-action "ref"} (t :pdf/copy-ref)])
(and (not area?) [: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)])
(when (and config/lsp-enabled? (not area?))
(for [[_ {:keys [key label extras] :as _cmd} action pid]
(state/get-plugins-commands-with-type :highlight-context-menu-item)]
[:li.item {:key key
:data-action "hook"
:on-click #(let [highlight (if (fn? highlight) (highlight) highlight)]
(commands/exec-plugin-simple-command!
pid {:key key :content (:content highlight) :point point} action)
(when (true? (:clearSelection extras))
(pdf-utils/clear-all-selection)))}
label]))
]))
(rum/defc pdf-highlights-text-region
[^js viewer vw-hl hl {:keys [show-ctx-menu!]}]
(let [{:keys [id]} hl
{:keys [rects]} (:position vw-hl)
{:keys [color]} (:properties hl)
open-ctx-menu!
(fn [^js/MouseEvent e]
(.preventDefault e)
(let [x (.-clientX e)
y (.-clientY e)]
(show-ctx-menu! viewer hl {:x x :y y})))
dragstart-handle!
(fn [^js e]
(when-let [^js dt (and id (.-dataTransfer e))]
(reset! block/*dragging? true)
(pdf-assets/ensure-ref-block! (state/get-current-pdf) hl)
(.setData dt "text/plain" (str "((" id "))"))))]
[:div.extensions__pdf-hls-text-region
{:id (str "hl_" id)
:on-click open-ctx-menu!
:on-context-menu open-ctx-menu!}
(map-indexed
(fn [idx rect]
[:div.hls-text-region-item
{:key idx
:style rect
:draggable "true"
:on-drag-start dragstart-handle!
:data-color color}])
rects)]))
(rum/defc ^:large-vars/cleanup-todo pdf-highlight-area-region
[^js viewer vw-hl hl {:keys [show-ctx-menu!] :as ops}]
(let [{:keys [id]} hl
*el (rum/use-ref nil)
*dirty (rum/use-ref nil)
*ops-ref (rum/use-ref ops)
open-ctx-menu! (fn [^js/MouseEvent e]
(.preventDefault e)
(when-not (rum/deref *dirty)
(let [x (.-clientX e)
y (.-clientY e)]
(show-ctx-menu! viewer hl {:x x :y y}))))
dragstart-handle! (fn [^js e]
(when-let [^js dt (and id (.-dataTransfer e))]
(.setData dt "text/plain" (str "((" id "))"))))
update-hl! (fn [hl] (some-> (rum/deref *ops-ref) (:upd-hl!) (apply [hl])))]
(rum/use-effect!
(fn []
(rum/set-ref! *ops-ref ops))
[ops])
;; 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")
(update-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 [(js/interact.modifiers.restrict
(bean/->js {:restriction (.closest el ".page")}))]
: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
{:id id
:ref *el
:style vw-bounding
:data-color color
:draggable "true"
:on-drag-start dragstart-handle!
:on-click open-ctx-menu!
:on-context-menu open-ctx-menu!}]))))
(rum/defc pdf-highlights-region-container
"Displays the highlights over a pdf document."
[^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 ^:large-vars/cleanup-todo pdf-highlight-area-selection
[^js viewer {:keys [show-ctx-menu!]}]
(let [^js viewer-clt (.. viewer -viewer -classList)
^js cnt-el (.-container viewer)
*el (rum/use-ref nil)
*start-el (rum/use-ref nil)
*cnt-rect (rum/use-ref nil)
*page-el (rum/use-ref nil)
*page-rect (rum/use-ref nil)
*start-xy (rum/use-ref nil)
[start, set-start!] (rum/use-state nil)
[end, set-end!] (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 util/win32? (.-shiftKey e))
@*area-mode?)))))
reset-coords! #(do
(set-start! nil)
(set-end! nil)
(rum/set-ref! *start-xy nil)
(rum/set-ref! *start-el nil)
(rum/set-ref! *cnt-rect nil)
(rum/set-ref! *page-el nil)
(rum/set-ref! *page-rect nil))
calc-coords! (fn [page-x page-y]
(when cnt-el
(let [cnt-rect (rum/deref *cnt-rect)
cnt-rect (or cnt-rect (bean/->clj (.toJSON (.getBoundingClientRect cnt-el))))
page-rect (rum/deref *page-rect)
[start-x, start-y] (rum/deref *start-xy)
dx-left? (> start-x page-x)
dy-top? (> start-y page-y)
page-left (:left page-rect)
page-right (:right page-rect)
page-top (:top page-rect)
page-bottom (:bottom page-rect)
_ (rum/set-ref! *cnt-rect cnt-rect)]
{:x (-> page-x
(#(if dx-left?
(if (< % page-left) page-left %)
(if (> % page-right) page-right %)))
(+ (.-scrollLeft cnt-el)))
:y (-> page-y
(#(if dy-top?
(if (< % page-top) page-top %)
(if (> % page-bottom) page-bottom %)))
(+ (.-scrollTop cnt-el)))})))
calc-rect (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! (calc-coords! (.-pageX e) (.-pageY e))))
[])]
(rum/use-effect!
(fn []
(when-let [^js/HTMLElement root cnt-el]
(let [fn-start (fn [^js/MouseEvent e]
(if (should-start e)
(let [target (.-target e)
page-el (.closest target ".page")
[x y] [(.-pageX e) (.-pageY e)]]
(rum/set-ref! *start-el target)
(rum/set-ref! *start-xy [x y])
(rum/set-ref! *page-el page-el)
(rum/set-ref! *page-rect (some-> page-el (.getBoundingClientRect) (.toJSON) (bean/->clj)))
(set-start! (calc-coords! x y))
(disable-text-selection! true)
(.addEventListener root "mousemove" fn-move))
;; reset
(do (reset-coords!)
(disable-text-selection! false))))
fn-end (fn [^js/MouseEvent e]
(when-let [start-el (rum/deref *start-el)]
(let [end (calc-coords! (.-pageX e) (.-pageY e))
rect (calc-rect start end)]
(if (and (> (:width rect) 10)
(> (:height rect) 10))
(when-let [^js page-el (.closest start-el ".page")]
(let [page-number (int (.-pageNumber (.-dataset page-el)))
page-pos (merge rect {:top (- (:top rect) (.-offsetTop page-el))
:left (- (:left rect) (.-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 for area
(show-ctx-menu! viewer hl point {:reset-fn #(reset-coords!)}))
(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])
[:div.extensions__pdf-area-selection
{:ref *el}
(when (and start end)
[:div.shadow-rect {:style (calc-rect start end)}])]))
(rum/defc ^:large-vars/cleanup-todo 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 {:selection nil :range nil :collapsed nil :point nil})
[highlights, set-highlights!] (rum/use-state initial-hls)
[ctx-menu-state, set-ctx-menu-state!] (rum/use-state {:highlight nil :vw-pos nil :selection nil :point nil :reset-fn nil})
clear-ctx-menu! (rum/use-callback
#(let [reset-fn (:reset-fn ctx-menu-state)]
(set-ctx-menu-state! {})
(and (fn? reset-fn) (reset-fn)))
[ctx-menu-state])
show-ctx-menu! (fn [^js viewer hl point & ops]
(let [vw-pos (pdf-utils/scaled-to-vw-pos viewer (:position hl))]
(set-ctx-menu-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-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 (.getSelection doc)
^js/Range sel-range (.getRangeAt selection 0)]
(cond
(.-isCollapsed selection)
(set-sel-state! {:collapsed true})
(and sel-range (.contains el (.-commonAncestorContainer sel-range)))
;; NOTE: `Range.toString()` forgets newlines whereas `Selection.toString()`
;; preserves them, so we derive text contents from the selection. However
;; `Document.getSelection()` seems to return the same object across multiple
;; selection changes, so we use the range as the `use-effect!` dep. Thus,
;; we need to store both the selection and the range.
(set-sel-state! {:collapsed false :selection selection :range sel-range :point {:x (.-clientX e) :y (.-clientY e)}}))))
fn-selection
(fn []
(let [*dirty (volatile! false)
fn-dirty #(vreset! *dirty true)]
(.addEventListener doc "selectionchange" fn-dirty)
(.addEventListener doc "mouseup"
(fn [^js e]
(and @*dirty (fn-selection-ok e))
(.removeEventListener doc "selectionchange" fn-dirty))
#js {:once true})))
fn-resize
(partial pdf-utils/adjust-viewer-size! viewer)]
;;(doto (.-eventBus viewer))
(when el
(.addEventListener el "mousedown" fn-selection))
(when win
(.addEventListener win "resize" fn-resize))
;; destroy
#(do
;;(doto (.-eventBus viewer))
(when el
(.removeEventListener el "mousedown" fn-selection))
(when win
(.removeEventListener win "resize" fn-resize)))))
[viewer])
;; selection context menu
(rum/use-effect!
(fn []
(when-let [^js/Range sel-range (and (not (:collapsed sel-state)) (:range sel-state))]
(let [^js point (:point sel-state)
^js/Selection selection (:selection sel-state)
hl-fn #(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 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)]
{:id nil
:page page
:position sc-pos
:content {:text (pdf-utils/fix-selection-text-breakline (.toString selection))}
:properties {}})))]
;; show ctx menu
(js/setTimeout (fn []
(set-ctx-menu-state! {:highlight hl-fn
:selection selection
:point point})))) 0))
[(:range sel-state)])
;; render hls
(rum/use-effect!
(fn []
(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
(pdf-highlights-region-container
viewer page-hls {:show-ctx-menu! show-ctx-menu!
:upd-hl! upd-hl!})
hls-layer)))))
;; destroy
#())
[loaded-pages highlights])
[:div.extensions__pdf-highlights-cnt
;; hl context tip menu
(when-let [_hl (:highlight ctx-menu-state)]
(js/ReactDOM.createPortal
(pdf-highlights-ctx-menu viewer ctx-menu-state
{:clear-ctx-menu! clear-ctx-menu!
: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))])
;; ])
(pdf-page-finder viewer)
;; area selection container
(pdf-highlight-area-selection
viewer
{:clear-ctx-menu! clear-ctx-menu!
:show-ctx-menu! show-ctx-menu!
:add-hl! add-hl!
})]))
(rum/defc ^:large-vars/data-var pdf-viewer
[_url ^js pdf-document {:keys [identity filename initial-hls initial-page initial-error]} ops]
(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)
[area-dashed?, _set-area-dashed?] (use-atom *area-dashed?)]
;; 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
:findController (js/pdfjsViewer.PDFFindController.
#js {:linkService link-service :eventBus event-bus})
:textLayerMode 2
:annotationMode 2
:removePageBorders true})
in-system-win? (boolean (.closest el ".is-system-window"))]
(set! (.-$groupIdentity viewer) identity)
(set! (.-$inSystemWindow viewer) in-system-win?)
(. link-service setDocument pdf-document)
(. link-service setViewer viewer)
;; events
(doto event-bus
;; it must be initialized before set-up document
(.on "pagesinit"
(fn []
(set! (. viewer -currentScaleValue) "auto")
(set-page-ready! true)))
(.on (name :ls-update-extra-state)
#(when-let [extra (bean/->clj %)]
(apply (:set-hls-extra! ops) [extra]))))
(p/then (. viewer setDocument pdf-document)
#(set-state! {:viewer viewer :bus event-bus :link link-service :el el}))
;; TODO: set as active viewer
(set! (. js/window -lsActivePdfViewer) viewer)
;; set initial page
(js/setTimeout
#(set! (.-currentPageNumber viewer) initial-page) 16)
;; destroy
(fn []
(.destroy pdf-document)
(set! (. js/window -lsActivePdfViewer) nil)
(.cleanup viewer))))
[])
;; update window title
(rum/use-effect!
(fn []
(when-let [^js viewer (:viewer state)]
(when (pdf-windows/check-viewer-in-system-win? viewer)
(some-> (pdf-windows/resolve-own-document viewer)
(set! -title filename)))))
[(:viewer state)])
;; interaction events
(rum/use-effect!
(fn []
(when-let [^js viewer (:viewer state)]
(let [fn-textlayer-ready
(fn [^js p]
(set-ano-state! {:loaded-pages (conj (:loaded-pages ano-state) (int (.-pageNumber p)))}))]
(doto (.-eventBus viewer)
(.on "textlayerrendered" fn-textlayer-ready))
#(do
(doto (.-eventBus viewer)
(.off "textlayerrendered" fn-textlayer-ready))))))
[(:viewer state)
(:loaded-pages ano-state)])
(let [^js viewer (:viewer state)
in-system-window? (some-> viewer (.-$inSystemWindow))]
[:div.extensions__pdf-viewer-cnt
[:div.extensions__pdf-viewer
{:ref *el-ref :class (util/classnames [{:is-area-dashed area-dashed?}])}
[:div.pdfViewer "viewer pdf"]
[:div.pp-holder]
;; block hls refs
(pdf-highlight-finder viewer)
(when (and page-ready? viewer (not initial-error))
[(rum/with-key
(pdf-highlights
(:el state) viewer
initial-hls (:loaded-pages ano-state)
ops) "pdf-highlights")])]
(when (and page-ready? viewer)
[(when-not in-system-window?
(rum/with-key (pdf-resizer viewer) "pdf-resizer"))
(rum/with-key (pdf-toolbar viewer {:on-external-window! #(open-external-win! (state/get-current-pdf))}) "pdf-toolbar")])])))
(rum/defcs pdf-password-input <
(rum/local "" ::password)
[state confirm-fn]
(let [password (get state ::password)]
[:div.container
[:div.text-lg.mb-4 "Password required"]
[:div.sm:flex.sm:items-start
[:div.mt-3.text-center.sm:mt-0.sm:text-left
[:h3#modal-headline.leading-6.font-medium
"This document is password protected. Please enter a password:"]]]
[:input.form-input.block.w-full.sm:text-sm.sm:leading-5.my-2.mb-4
{:auto-focus true
:on-change (fn [e]
(reset! password (util/evalue e)))}]
[:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
[:span.flex.w-full.rounded-md.shadow-sm.sm:ml-3.sm:w-auto
[: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
{:type "button"
:on-click (fn []
(let [password @password]
(confirm-fn password)))}
"Submit"]]]]))
(rum/defc ^:large-vars/data-var pdf-loader
[{:keys [url hls-file identity filename] :as pdf-current}]
(let [*doc-ref (rum/use-ref nil)
[loader-state, set-loader-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 :extra nil :loaded false :error nil})
[doc-password, set-doc-password!] (rum/use-state nil) ;; use nil to handle empty string
[initial-page, set-initial-page!] (rum/use-state 1)
set-dirty-hls! (fn [latest-hls] ;; TODO: incremental
(set-hls-state! #(merge % {:initial-hls [] :latest-hls latest-hls})))
set-hls-extra! (fn [extra]
(set-hls-state! #(merge % {:extra extra})))]
;; current pdf effects
(rum/use-effect!
(fn []
(when pdf-current
(pdf-assets/ensure-ref-page! pdf-current)))
[pdf-current])
;; load highlights
(rum/use-effect!
(fn []
(p/catch
(p/let [data (pdf-assets/load-hls-data$ pdf-current)
{:keys [highlights extra]} data]
(set-initial-page! (or (when-let [page (:page extra)]
(util/safe-parse-int page)) 1))
(set-hls-state! {:initial-hls highlights :latest-hls highlights :extra extra :loaded true}))
;; error
(fn [^js e]
(js/console.error "[load hls error]" e)
(let [msg (str (util/format "Error: failed to load the highlights file: \"%s\". \n"
(:hls-file pdf-current))
e)]
(notification/show! msg :error)
(set-hls-state! {:loaded true :error e}))))
;; cancel
#())
[hls-file])
;; cache highlights
(let [persist-hls-data!
(rum/use-callback
(util/debounce
4000 (fn [latest-hls extra]
(pdf-assets/persist-hls-data$
pdf-current latest-hls extra))) [pdf-current])]
(rum/use-effect!
(fn []
(when (= :completed (:status loader-state))
(p/catch
(when-not (:error hls-state)
(p/do! (persist-hls-data! (:latest-hls hls-state) (:extra hls-state))))
;; write hls file error
(fn [e]
(js/console.error "[write hls error]" e)))))
[(:latest-hls hls-state) (:extra hls-state)]))
;; load document
(rum/use-effect!
(fn []
(let [^js loader-el (rum/deref *doc-ref)
get-doc$ (fn [^js opts] (.-promise (js/pdfjsLib.getDocument opts)))
opts {:url url
:password (or doc-password "")
:ownerDocument (.-ownerDocument loader-el)
:cMapUrl "./js/pdfjs/cmaps/"
;:cMapUrl "https://cdn.jsdelivr.net/npm/pdfjs-dist@3.9.179/cmaps/"
:cMapPacked true}]
(set-loader-state! {:status :loading})
(-> (get-doc$ (clj->js opts))
(p/then (fn [doc]
(set-loader-state! {:pdf-document doc :status :completed})))
(p/catch #(set-loader-state! {:error %})))
#()))
[url doc-password])
(rum/use-effect!
(fn []
(when-let [error (:error loader-state)]
(js/console.error "[PDF loader]" (:error loader-state))
(case (.-name error)
"MissingPDFException"
(do
(notification/show!
(str "Error: " (.-message error) "\n Is this the correct path?")
:error
false)
(state/set-state! :pdf/current nil))
"InvalidPDFException"
(do
(notification/show!
(str "Error: " (.-message error) "\n"
"Is this .pdf file corrupted?\n"
"Please confirm with external pdf viewer.")
:error
false)
(state/set-state! :pdf/current nil))
"PasswordException"
(do
(set-loader-state! {:error nil})
(state/set-modal! (fn [close-fn]
(let [on-password-fn
(fn [password]
(close-fn)
(set-doc-password! password))]
(pdf-password-input on-password-fn)))))
(do
(notification/show!
(str "Error: " (.-name error) "\n" (.-message error) "\n"
"Please confirm with pdf file resource.")
:error
false)
(state/set-state! :pdf/current nil)))))
[(:error loader-state)])
(rum/bind-context
[*highlights-ctx* hls-state]
[:div.extensions__pdf-loader {:ref *doc-ref}
(let [status-doc (:status loader-state)
initial-hls (:initial-hls hls-state)
initial-error (:error hls-state)]
(if (= status-doc :loading)
[:div.flex.justify-center.items-center.h-screen.text-gray-500.text-lg
svg/loading]
(when-let [pdf-document (and (:loaded hls-state) (:pdf-document loader-state))]
[(rum/with-key (pdf-viewer
url pdf-document
{:identity identity
:filename filename
:initial-hls initial-hls
:initial-page initial-page
:initial-error initial-error}
{:set-dirty-hls! set-dirty-hls!
:set-hls-extra! set-hls-extra!}) "pdf-viewer")])))])))
(rum/defc pdf-container-outer
< (shortcut/mixin :shortcut.handler/pdf false)
[child]
[:<> child])
(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.extensions__pdf-container
{:id (str "pdf-layout-container_" identity)}
(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 default-embed-playground
< rum/static rum/reactive
[state]
(let [pdf-current (state/sub :pdf/current)
system-win? (state/sub :pdf/system-win?)]
[:div.extensions__pdf-playground
(playground-effects (and (not system-win?)
(not (nil? pdf-current))))
(when (and (not system-win?) pdf-current)
(js/ReactDOM.createPortal
(pdf-container-outer
(pdf-container pdf-current))
(js/document.querySelector "#app-single-container")))]))
(rum/defcs system-embed-playground
< rum/reactive
[]
(let [pdf-current (state/sub :pdf/current)]
(pdf-container pdf-current)))