(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) 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 (and @*highlight-mode? new?) ;; 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 (and @*highlight-mode? new?) "hidden" "visible")} :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 (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})))] ;; 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 (rum/use-effect! (fn [] (when (= :completed (:status loader-state)) (p/catch (when-not (:error hls-state) (p/do! (p/delay 100) (pdf-assets/persist-hls-data$ pdf-current (: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 "./cmaps/" ;;:cMapUrl "https://cdn.jsdelivr.net/npm/pdfjs-dist@2.8.335/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 [{: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 (shortcut/mixin :shortcut.handler/pdf) [] (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 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)))