|
| 1 | +(ns nextjournal.clerk.render.window |
| 2 | + (:require [applied-science.js-interop :as j] |
| 3 | + [nextjournal.clerk.render.hooks :as hooks])) |
| 4 | + |
| 5 | +(defn resizer [{:keys [on-resize on-resize-start on-resize-end] :or {on-resize-start #() on-resize-end #()}}] |
| 6 | + (let [!direction (hooks/use-state nil) |
| 7 | + !mouse-down (hooks/use-state false) |
| 8 | + handle-mouse-down (fn [dir] |
| 9 | + (on-resize-start) |
| 10 | + (reset! !direction dir) |
| 11 | + (reset! !mouse-down true))] |
| 12 | + (hooks/use-effect (fn [] |
| 13 | + (let [handle-mouse-move (fn [e] |
| 14 | + (when-let [dir @!direction] |
| 15 | + (on-resize dir (.-movementX e) (.-movementY e))))] |
| 16 | + (when @!mouse-down |
| 17 | + (js/addEventListener "mousemove" handle-mouse-move)) |
| 18 | + #(js/removeEventListener "mousemove" handle-mouse-move))) |
| 19 | + [!mouse-down !direction on-resize]) |
| 20 | + (hooks/use-effect (fn [] |
| 21 | + (let [handle-mouse-up (fn [] |
| 22 | + (on-resize-end) |
| 23 | + (reset! !mouse-down false))] |
| 24 | + (js/addEventListener "mouseup" handle-mouse-up) |
| 25 | + #(js/removeEventListener "mouseup" handle-mouse-up)))) |
| 26 | + [:<> |
| 27 | + [:div.absolute.z-2.cursor-nwse-resize |
| 28 | + {:on-mouse-down #(handle-mouse-down :top-left) |
| 29 | + :class "w-[14px] h-[14px] -left-[7px] -top-[7px]"}] |
| 30 | + [:div.absolute.z-1.left-0.w-full.cursor-ns-resize |
| 31 | + {:on-mouse-down #(handle-mouse-down :top) |
| 32 | + :class "h-[4px] -top-[4px]"}] |
| 33 | + [:div.absolute.z-2.cursor-nesw-resize |
| 34 | + {:on-mouse-down #(handle-mouse-down :top-right) |
| 35 | + :class "w-[14px] h-[14px] -right-[7px] -top-[7px]"}] |
| 36 | + [:div.absolute.z-1.top-0.h-full.cursor-ew-resize |
| 37 | + {:on-mouse-down #(handle-mouse-down :right) |
| 38 | + :class "w-[4px] -right-[2px]"}] |
| 39 | + [:div.absolute.z-2.cursor-nwse-resize |
| 40 | + {:on-mouse-down #(handle-mouse-down :bottom-right) |
| 41 | + :class "w-[14px] h-[14px] -right-[7px] -bottom-[7px]"}] |
| 42 | + [:div.absolute.z-1.bottom-0.w-full.cursor-ns-resize |
| 43 | + {:on-mouse-down #(handle-mouse-down :bottom) |
| 44 | + :class "h-[4px] -left-[2px]"}] |
| 45 | + [:div.absolute.z-2.cursor-nesw-resize |
| 46 | + {:on-mouse-down #(handle-mouse-down :bottom-left) |
| 47 | + :class "w-[14px] h-[14px] -left-[7px] -bottom-[7px]"}] |
| 48 | + [:div.absolute.z-1.left-0.top-0.h-full.cursor-ew-resize |
| 49 | + {:on-mouse-down #(handle-mouse-down :left) |
| 50 | + :class "w-[4px]"}]])) |
| 51 | + |
| 52 | +(defn header [{:keys [on-drag on-drag-start on-drag-end] :or {on-drag-start #() on-drag-end #()}}] |
| 53 | + (let [!mouse-down (hooks/use-state false)] |
| 54 | + (hooks/use-effect (fn [] |
| 55 | + (let [handle-mouse-up (fn [] |
| 56 | + (on-drag-end) |
| 57 | + (reset! !mouse-down false))] |
| 58 | + (js/addEventListener "mouseup" handle-mouse-up) |
| 59 | + #(js/addEventListener "mouseup" handle-mouse-up)))) |
| 60 | + (hooks/use-effect (fn [] |
| 61 | + (let [handle-mouse-move #(on-drag {:x (.-clientX %) :y (.-clientY %) :dx (.-movementX %) :dy (.-movementY %)})] |
| 62 | + (when @!mouse-down |
| 63 | + (js/addEventListener "mousemove" handle-mouse-move)) |
| 64 | + #(js/removeEventListener "mousemove" handle-mouse-move))) |
| 65 | + [!mouse-down on-drag]) |
| 66 | + [:div.bg-slate-100.hover:bg-slate-200.dark:bg-slate-800.dark:hover:bg-slate-700.cursor-move.w-full.rounded-t-lg |
| 67 | + {:class "h-[14px]" |
| 68 | + :on-mouse-down (fn [event] |
| 69 | + (on-drag-start) |
| 70 | + (reset! !mouse-down {:start-x (.-screenX event) :start-y (.-screenY event)}))}])) |
| 71 | + |
| 72 | +(defn resize-top [panel {:keys [top height]} dy] |
| 73 | + (j/assoc-in! panel [:style :height] (str (- height dy) "px")) |
| 74 | + (j/assoc-in! panel [:style :top] (str (+ top dy) "px"))) |
| 75 | + |
| 76 | +(defn resize-right [panel {:keys [width]} dx] |
| 77 | + (j/assoc-in! panel [:style :width] (str (+ width dx) "px"))) |
| 78 | + |
| 79 | +(defn resize-bottom [panel {:keys [height]} dy] |
| 80 | + (j/assoc-in! panel [:style :height] (str (+ height dy) "px"))) |
| 81 | + |
| 82 | +(defn resize-left [panel {:keys [left width]} dx] |
| 83 | + (j/assoc-in! panel [:style :width] (str (- width dx) "px")) |
| 84 | + (j/assoc-in! panel [:style :left] (str (+ left dx) "px"))) |
| 85 | + |
| 86 | +(defn dock-at-top [panel] |
| 87 | + (-> panel |
| 88 | + (j/assoc-in! [:style :width] "calc(100vw - 10px)") |
| 89 | + (j/assoc-in! [:style :height] "33vh") |
| 90 | + (j/assoc-in! [:style :top] "5px") |
| 91 | + (j/assoc-in! [:style :left] "5px"))) |
| 92 | + |
| 93 | +(defn dock-at-right [panel] |
| 94 | + (-> panel |
| 95 | + (j/assoc-in! [:style :width] "33vw") |
| 96 | + (j/assoc-in! [:style :height] "calc(100vh - 10px)") |
| 97 | + (j/assoc-in! [:style :top] "5px") |
| 98 | + (j/assoc-in! [:style :left] "calc(100vw - 33vw - 5px)"))) |
| 99 | + |
| 100 | +(defn dock-at-bottom [panel] |
| 101 | + (-> panel |
| 102 | + (j/assoc-in! [:style :width] "calc(100vw - 10px)") |
| 103 | + (j/assoc-in! [:style :height] "33vh") |
| 104 | + (j/assoc-in! [:style :top] "calc(100vh - 33vh - 10px)") |
| 105 | + (j/assoc-in! [:style :left] "5px"))) |
| 106 | + |
| 107 | +(defn dock-at-left [panel] |
| 108 | + (-> panel |
| 109 | + (j/assoc-in! [:style :width] "33vw") |
| 110 | + (j/assoc-in! [:style :height] "calc(100vh - 10px)") |
| 111 | + (j/assoc-in! [:style :top] "5px") |
| 112 | + (j/assoc-in! [:style :left] "5px"))) |
| 113 | + |
| 114 | +(defn show [& content] |
| 115 | + (let [!panel-ref (hooks/use-ref nil) |
| 116 | + !dragging? (hooks/use-state nil) |
| 117 | + !dockable-at (hooks/use-state nil) |
| 118 | + !docking-ref (hooks/use-ref nil)] |
| 119 | + [:<> |
| 120 | + [:div.fixed.border-2.border-dashed.border-indigo-600.border-opacity-70.bg-indigo-600.bg-opacity-30.pointer-events-none.transition-all.rounded-lg |
| 121 | + {:class (str "z-[999] " (if-let [side @!dockable-at] |
| 122 | + (str "opacity-100 " (case side |
| 123 | + :top "left-[5px] top-[5px] right-[5px] h-[33vh]" |
| 124 | + :left "left-[5px] top-[5px] bottom-[5px] w-[33vw]" |
| 125 | + :bottom "left-[5px] bottom-[5px] right-[5px] h-[33vh]" |
| 126 | + :right "right-[5px] top-[5px] bottom-[5px] w-[33vw]")) |
| 127 | + "opacity-0 "))}] |
| 128 | + [:div.fixed.bg-white.dark:bg-slate-900.shadow-xl.text-slate-800.dark:text-slate-100.rounded-lg.flex.flex-col.hover:ring-2 |
| 129 | + {:class (str "z-[1000] " (if @!dragging? "ring-indigo-600 select-none ring-2 " "ring-slate-300 dark:ring-slate-700 ring-1 ")) |
| 130 | + :ref !panel-ref |
| 131 | + :style {:top 30 :right 30 :width 400 :height 400}} |
| 132 | + [resizer {:on-resize (fn [dir dx dy] |
| 133 | + (when-let [panel @!panel-ref] |
| 134 | + (let [rect (j/lookup (.getBoundingClientRect panel))] |
| 135 | + (case dir |
| 136 | + :top-left (do (resize-top panel rect dy) |
| 137 | + (resize-left panel rect dx)) |
| 138 | + :top (resize-top panel rect dy) |
| 139 | + :top-right (do (resize-top panel rect dy) |
| 140 | + (resize-right panel rect dx)) |
| 141 | + :right (resize-right panel rect dx) |
| 142 | + :bottom-right (do (resize-bottom panel rect dy) |
| 143 | + (resize-right panel rect dx)) |
| 144 | + :bottom (resize-bottom panel rect dy) |
| 145 | + :bottom-left (do (resize-bottom panel rect dy) |
| 146 | + (resize-left panel rect dx)) |
| 147 | + :left (resize-left panel rect dx))))) |
| 148 | + :on-resize-start #(reset! !dragging? true) |
| 149 | + :on-resize-end #(reset! !dragging? false)}] |
| 150 | + [header {:on-drag (fn [{:keys [x y dx dy]}] |
| 151 | + (when-let [panel @!panel-ref] |
| 152 | + (let [{:keys [left top width]} (j/lookup (.getBoundingClientRect panel)) |
| 153 | + x-edge-offset 20 |
| 154 | + y-edge-offset 10 |
| 155 | + vw js/innerWidth |
| 156 | + vh js/innerHeight] |
| 157 | + (reset! !dockable-at (cond |
| 158 | + (zero? x) :left |
| 159 | + (>= x (- vw 2)) :right |
| 160 | + (<= y 0) :top |
| 161 | + (>= y (- vh 2)) :bottom |
| 162 | + :else nil)) |
| 163 | + (reset! !docking-ref @!dockable-at) |
| 164 | + (j/assoc-in! panel [:style :left] (str (min (- vw x-edge-offset) (max (+ x-edge-offset (- width)) (+ left dx))) "px")) |
| 165 | + (j/assoc-in! panel [:style :top] (str (min (- vh y-edge-offset) (max y-edge-offset (+ top dy))) "px"))))) |
| 166 | + :on-drag-start #(reset! !dragging? true) |
| 167 | + :on-drag-end (fn [] |
| 168 | + (when-let [side @!docking-ref] |
| 169 | + (let [panel @!panel-ref] |
| 170 | + (case side |
| 171 | + :top (dock-at-top panel) |
| 172 | + :right (dock-at-right panel) |
| 173 | + :bottom (dock-at-bottom panel) |
| 174 | + :left (dock-at-left panel)))) |
| 175 | + (reset! !dockable-at nil) |
| 176 | + (reset! !docking-ref nil))}] |
| 177 | + (into [:div.p-3.flex-auto.overflow-auto] content)]])) |
0 commit comments