diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 14669414a..59864afbb 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -36,6 +36,7 @@ [app.main.data.workspace.specialized-panel :as dwsp] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] + [app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.undo :as dwu] [app.main.features :as features] [app.main.features.pointer-map :as fpmap] @@ -534,34 +535,38 @@ (defn instantiate-component "Create a new shape in the current page, from the component with the given id in the given file library. Then selects the newly created instance." - [file-id component-id position] - (dm/assert! (uuid? file-id)) - (dm/assert! (uuid? component-id)) - (dm/assert! (gpt/point? position)) - (ptk/reify ::instantiate-component - ptk/WatchEvent - (watch [it state _] - (let [page (wsh/lookup-page state) - libraries (wsh/get-libraries state) + ([file-id component-id position] + (instantiate-component file-id component-id position nil)) + ([file-id component-id position {:keys [start-move? initial-point]}] + (dm/assert! (uuid? file-id)) + (dm/assert! (uuid? component-id)) + (dm/assert! (gpt/point? position)) + (ptk/reify ::instantiate-component + ptk/WatchEvent + (watch [it state _] + (let [page (wsh/lookup-page state) + libraries (wsh/get-libraries state) - objects (:objects page) - changes (-> (pcb/empty-changes it (:id page)) - (pcb/with-objects objects)) + objects (:objects page) + changes (-> (pcb/empty-changes it (:id page)) + (pcb/with-objects objects)) - [new-shape changes] - (dwlh/generate-instantiate-component changes - objects - file-id - component-id - position - page - libraries) - undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (ptk/data-event :layout/update [(:id new-shape)]) - (dws/select-shapes (d/ordered-set (:id new-shape))) - (dwu/commit-undo-transaction undo-id)))))) + [new-shape changes] + (dwlh/generate-instantiate-component changes + objects + file-id + component-id + position + page + libraries) + undo-id (js/Symbol)] + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (ptk/data-event :layout/update [(:id new-shape)]) + (dws/select-shapes (d/ordered-set (:id new-shape))) + (when start-move? + (dwtr/start-move initial-point #{(:id new-shape)})) + (dwu/commit-undo-transaction undo-id))))))) (defn detach-component "Remove all references to components in the shape with the given id, diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index dda92054b..f47b71b60 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -496,7 +496,7 @@ (when-let [node (dom/get-element-by-class "ghost-outline")] (dom/set-property! node "transform" (gmt/translate-matrix move-vector)))))) -(defn- start-move +(defn start-move ([from-position] (start-move from-position nil)) ([from-position ids] (ptk/reify ::start-move diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index d97fdad92..944d32ac7 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -50,13 +50,6 @@ (fn [] (st/emit! (dsc/pop-shortcuts key)))))) -(defn invisible-image - [] - (let [img (js/Image.) - imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="] - (set! (.-src img) imd) - img)) - (defn- set-timer [state ms func] (assoc state :timer (ts/schedule ms func))) @@ -128,7 +121,7 @@ (do (dom/stop-propagation event) (dnd/set-data! event data-type data) - (dnd/set-drag-image! event (invisible-image)) + (dnd/set-drag-image! event (dnd/invisible-image)) (dnd/set-allowed-effect! event "move") (when (fn? on-drag) (on-drag data))))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs index 3175edcf9..54d9e2d33 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/components.cljs @@ -472,13 +472,26 @@ (mf/use-fn (mf/deps file-id) (fn [component event] - ;; dnd api only allow to acces to the dataTransfer data on on-drop (https://html.spec.whatwg.org/dev/dnd.html#concept-dnd-p) - ;; We need to know if the dragged element is from the local library on on-drag-enter, so we need to keep the info elsewhere - (set-drag-data! {:local? local?}) - (dnd/set-data! event "penpot/component" {:file-id file-id - :component component}) - (dnd/set-allowed-effect! event "move"))) + (let [file-data + (d/nilv (dm/get-in @refs/workspace-libraries [file-id :data]) @refs/workspace-data) + + shape-main + (ctf/get-component-root file-data component)] + + ;; dnd api only allow to acces to the dataTransfer data on on-drop (https://html.spec.whatwg.org/dev/dnd.html#concept-dnd-p) + ;; We need to know if the dragged element is from the local library on on-drag-enter, so we need to keep the info elsewhere + (set-drag-data! {:file-id file-id + :component component + :shape shape-main + :local? local?}) + + (dnd/set-data! event "penpot/component" true) + + ;; Remove the ghost image for componentes because we're going to instantiate it on the viewport + (dnd/set-drag-image! event (dnd/invisible-image)) + + (dnd/set-allowed-effect! event "move")))) on-show-main (mf/use-fn @@ -569,4 +582,3 @@ {:option-name (tr "workspace.shape.menu.show-main") :id "assets-show-main-component" :option-handler on-show-main})]}]]])) - diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index ac499058f..8e4e04d4d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -84,7 +84,7 @@ (dom/focus! textarea)))) on-delete-annotation (mf/use-callback - (mf/deps shape) + (mf/deps (:id shape)) (fn [event] (dom/stop-propagation event) (st/emit! (modal/show @@ -98,7 +98,7 @@ (dw/update-component-annotation component-id nil)))}))))] (mf/use-effect - (mf/deps shape) + (mf/deps (:id shape)) (fn [] (initialize) (when (and (not creating?) (:id-for-create workspace-annotations)) ;; cleanup set-annotations-id-for-create if we aren't on the marked component diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 41cb81686..181e5f5da 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -174,9 +174,12 @@ on-click (actions/on-click hover selected edition drawing-path? drawing-tool space? selrect z?) on-context-menu (actions/on-context-menu hover hover-ids workspace-read-only?) on-double-click (actions/on-double-click hover hover-ids hover-top-frame-id drawing-path? base-objects edition drawing-tool z? workspace-read-only?) - on-drag-enter (actions/on-drag-enter) + + comp-inst-ref (mf/use-ref false) + on-drag-enter (actions/on-drag-enter comp-inst-ref) on-drag-over (actions/on-drag-over move-stream) - on-drop (actions/on-drop file) + on-drag-end (actions/on-drag-over comp-inst-ref) + on-drop (actions/on-drop file comp-inst-ref) on-pointer-down (actions/on-pointer-down @hover selected edition drawing-tool text-editing? node-editing? grid-editing? drawing-path? create-comment? space? panning z? workspace-read-only?) @@ -365,6 +368,7 @@ :on-double-click on-double-click :on-drag-enter on-drag-enter :on-drag-over on-drag-over + :on-drag-end on-drag-end :on-drop on-drop :on-pointer-down on-pointer-down :on-pointer-enter on-pointer-enter diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 30fa24840..ca982acd7 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -21,6 +21,7 @@ [app.main.data.workspace.specialized-panel :as-alias dwsp] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.workspace.sidebar.assets.components :as wsac] [app.main.ui.workspace.viewport.viewport-ref :as uwvv] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] @@ -28,7 +29,8 @@ [app.util.keyboard :as kbd] [app.util.mouse :as mse] [app.util.object :as obj] - [app.util.timers :as timers] + [app.util.rxops :refer [throttle-fn]] + [app.util.timers :as ts] [app.util.webapi :as wapi] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -216,7 +218,7 @@ (st/emit! (mse/->MouseEvent :double-click ctrl? shift? alt? meta?)) ;; Emit asynchronously so the double click to exit shapes won't break - (timers/schedule + (ts/schedule (fn [] (when (and (not drawing-path?) shape) (cond @@ -244,7 +246,7 @@ workspace-read-only?) (let [position (dom/get-client-position event)] ;; Delayed callback because we need to wait to the previous context menu to be closed - (timers/schedule + (ts/schedule #(st/emit! (if (some? @hover) (dw/show-shape-context-menu {:position position @@ -290,7 +292,7 @@ ;; We store this so in Firefox the middle button won't do a paste of the content (reset! disable-paste true) - (timers/schedule #(reset! disable-paste false))) + (ts/schedule #(reset! disable-paste false))) (st/emit! (dw/finish-panning) (dw/finish-zooming)))))) @@ -400,9 +402,28 @@ (st/emit! (dw/update-viewport-position {:x #(+ % (/ delta-x zoom)) :y #(+ % (/ delta-y zoom))})))))))))) -(defn on-drag-enter [] +(defn on-drag-enter + [comp-inst-ref] (mf/use-callback (fn [e] + (let [component-inst? (mf/ref-val comp-inst-ref)] + (when (and (dnd/has-type? e "penpot/component") + (dom/class? (dom/get-target e) "viewport-controls") + (not component-inst?)) + (let [point (gpt/point (.-clientX e) (.-clientY e)) + viewport-coord (uwvv/point->viewport point) + {:keys [component file-id shape]} @wsac/drag-data* + + ;; shape (get-in component [:objects (:id component)]) + final-x (- (:x viewport-coord) (/ (:width shape) 2)) + final-y (- (:y viewport-coord) (/ (:height shape) 2))] + + (mf/set-ref-val! comp-inst-ref true) + (st/emit! (dwl/instantiate-component + file-id + (:id component) + (gpt/point final-x final-y) + {:start-move? true :initial-point viewport-coord}))))) (when (or (dnd/has-type? e "penpot/shape") (dnd/has-type? e "penpot/component") (dnd/has-type? e "Files") @@ -410,8 +431,19 @@ (dnd/has-type? e "text/asset-id")) (dom/prevent-default e))))) +(defn on-drag-end + [comp-inst-ref] + (mf/use-callback + (fn [] + (mf/set-ref-val! comp-inst-ref false)))) + (defn on-drag-over [move-stream] - (let [on-pointer-move (on-pointer-move move-stream)] + (let [on-pointer-move (on-pointer-move move-stream) + + ;; Drag-over is not the same as pointer-move. Drag over is fired less frequently so we need + ;; to create a throttle so the events that cannot be processed at a certain path are + ;; discarded. + on-pointer-move (throttle-fn 50 (fn [e] (ts/raf #(on-pointer-move e))))] (mf/use-callback (fn [e] (when (or (dnd/has-type? e "penpot/shape") @@ -423,7 +455,7 @@ (dom/prevent-default e)))))) (defn on-drop - [file] + [file comp-inst-ref] (mf/use-fn (fn [event] (dom/prevent-default event) @@ -443,13 +475,13 @@ (assoc :y final-y))))) (dnd/has-type? event "penpot/component") - (let [{:keys [component file-id]} (dnd/get-data event "penpot/component") - shape (get-in component [:objects (:id component)]) - final-x (- (:x viewport-coord) (/ (:width shape) 2)) - final-y (- (:y viewport-coord) (/ (:height shape) 2))] - (st/emit! (dwl/instantiate-component file-id - (:id component) - (gpt/point final-x final-y)))) + (let [event (.-nativeEvent event) + ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + alt? (kbd/alt? event) + meta? (kbd/meta? event)] + (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)) + (mf/set-ref-val! comp-inst-ref false)) ;; Will trigger when the user drags an image from a browser ;; to the viewport (firefox and chrome do it a bit different @@ -517,4 +549,3 @@ (not @disable-paste) (not workspace-read-only?)) (st/emit! (dw/paste-from-event event @in-viewport?))))))) - diff --git a/frontend/src/app/util/dom/dnd.cljs b/frontend/src/app/util/dom/dnd.cljs index 5ea91a22a..0f29caab5 100644 --- a/frontend/src/app/util/dom/dnd.cljs +++ b/frontend/src/app/util/dom/dnd.cljs @@ -62,6 +62,13 @@ (.setData dt data-type data)) e))) +(defn invisible-image + [] + (let [img (js/Image.) + imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="] + (set! (.-src img) imd) + img)) + (defn set-drag-image! ([e image] (set-drag-image! e image 0 0)) @@ -108,11 +115,13 @@ ([e] (get-data e "penpot/data")) ([e data-type] - (let [dt (.-dataTransfer e)] - (if (or (str/starts-with? data-type "penpot") - (= data-type "application/json")) - (t/decode-str (.getData dt data-type)) - (.getData dt data-type))))) + (let [dt (.-dataTransfer e) + data (.getData dt data-type)] + (cond-> data + (and (some? data) (not= data "") + (or (str/starts-with? data-type "penpot") + (= data-type "application/json"))) + (t/decode-str))))) (defn get-files [e] diff --git a/frontend/src/app/util/rxops.cljs b/frontend/src/app/util/rxops.cljs index 0b08a23ae..05732f3d0 100644 --- a/frontend/src/app/util/rxops.cljs +++ b/frontend/src/app/util/rxops.cljs @@ -8,7 +8,7 @@ (:require [beicon.v2.core :as rx])) -(defn- throttle-fn +(defn throttle-fn [delay f] (let [state #js {:lastExecTime 0