Merge pull request #4090 from penpot/alotor-drag-component-instance

 Change drag component to instantiate on enter the viewport
This commit is contained in:
Alejandro 2024-02-02 09:42:48 +01:00 committed by GitHub
commit c70acb1570
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 121 additions and 67 deletions

View file

@ -36,6 +36,7 @@
[app.main.data.workspace.specialized-panel :as dwsp] [app.main.data.workspace.specialized-panel :as dwsp]
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.thumbnails :as dwt]
[app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
[app.main.features :as features] [app.main.features :as features]
[app.main.features.pointer-map :as fpmap] [app.main.features.pointer-map :as fpmap]
@ -534,34 +535,38 @@
(defn instantiate-component (defn instantiate-component
"Create a new shape in the current page, from the component with the given id "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." in the given file library. Then selects the newly created instance."
[file-id component-id position] ([file-id component-id position]
(dm/assert! (uuid? file-id)) (instantiate-component file-id component-id position nil))
(dm/assert! (uuid? component-id)) ([file-id component-id position {:keys [start-move? initial-point]}]
(dm/assert! (gpt/point? position)) (dm/assert! (uuid? file-id))
(ptk/reify ::instantiate-component (dm/assert! (uuid? component-id))
ptk/WatchEvent (dm/assert! (gpt/point? position))
(watch [it state _] (ptk/reify ::instantiate-component
(let [page (wsh/lookup-page state) ptk/WatchEvent
libraries (wsh/get-libraries state) (watch [it state _]
(let [page (wsh/lookup-page state)
libraries (wsh/get-libraries state)
objects (:objects page) objects (:objects page)
changes (-> (pcb/empty-changes it (:id page)) changes (-> (pcb/empty-changes it (:id page))
(pcb/with-objects objects)) (pcb/with-objects objects))
[new-shape changes] [new-shape changes]
(dwlh/generate-instantiate-component changes (dwlh/generate-instantiate-component changes
objects objects
file-id file-id
component-id component-id
position position
page page
libraries) libraries)
undo-id (js/Symbol)] undo-id (js/Symbol)]
(rx/of (dwu/start-undo-transaction undo-id) (rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes) (dch/commit-changes changes)
(ptk/data-event :layout/update [(:id new-shape)]) (ptk/data-event :layout/update [(:id new-shape)])
(dws/select-shapes (d/ordered-set (:id new-shape))) (dws/select-shapes (d/ordered-set (:id new-shape)))
(dwu/commit-undo-transaction undo-id)))))) (when start-move?
(dwtr/start-move initial-point #{(:id new-shape)}))
(dwu/commit-undo-transaction undo-id)))))))
(defn detach-component (defn detach-component
"Remove all references to components in the shape with the given id, "Remove all references to components in the shape with the given id,

View file

@ -496,7 +496,7 @@
(when-let [node (dom/get-element-by-class "ghost-outline")] (when-let [node (dom/get-element-by-class "ghost-outline")]
(dom/set-property! node "transform" (gmt/translate-matrix move-vector)))))) (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] (start-move from-position nil))
([from-position ids] ([from-position ids]
(ptk/reify ::start-move (ptk/reify ::start-move

View file

@ -50,13 +50,6 @@
(fn [] (fn []
(st/emit! (dsc/pop-shortcuts key)))))) (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 (defn- set-timer
[state ms func] [state ms func]
(assoc state :timer (ts/schedule ms func))) (assoc state :timer (ts/schedule ms func)))
@ -128,7 +121,7 @@
(do (do
(dom/stop-propagation event) (dom/stop-propagation event)
(dnd/set-data! event data-type data) (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") (dnd/set-allowed-effect! event "move")
(when (fn? on-drag) (when (fn? on-drag)
(on-drag data))))) (on-drag data)))))

View file

@ -472,13 +472,26 @@
(mf/use-fn (mf/use-fn
(mf/deps file-id) (mf/deps file-id)
(fn [component event] (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 (let [file-data
:component component}) (d/nilv (dm/get-in @refs/workspace-libraries [file-id :data]) @refs/workspace-data)
(dnd/set-allowed-effect! event "move")))
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 on-show-main
(mf/use-fn (mf/use-fn
@ -569,4 +582,3 @@
{:option-name (tr "workspace.shape.menu.show-main") {:option-name (tr "workspace.shape.menu.show-main")
:id "assets-show-main-component" :id "assets-show-main-component"
:option-handler on-show-main})]}]]])) :option-handler on-show-main})]}]]]))

View file

@ -84,7 +84,7 @@
(dom/focus! textarea)))) (dom/focus! textarea))))
on-delete-annotation on-delete-annotation
(mf/use-callback (mf/use-callback
(mf/deps shape) (mf/deps (:id shape))
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(st/emit! (modal/show (st/emit! (modal/show
@ -98,7 +98,7 @@
(dw/update-component-annotation component-id nil)))}))))] (dw/update-component-annotation component-id nil)))}))))]
(mf/use-effect (mf/use-effect
(mf/deps shape) (mf/deps (:id shape))
(fn [] (fn []
(initialize) (initialize)
(when (and (not creating?) (:id-for-create workspace-annotations)) ;; cleanup set-annotations-id-for-create if we aren't on the marked component (when (and (not creating?) (:id-for-create workspace-annotations)) ;; cleanup set-annotations-id-for-create if we aren't on the marked component

View file

@ -174,9 +174,12 @@
on-click (actions/on-click hover selected edition drawing-path? drawing-tool space? selrect z?) 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-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-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-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? 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?) drawing-path? create-comment? space? panning z? workspace-read-only?)
@ -365,6 +368,7 @@
:on-double-click on-double-click :on-double-click on-double-click
:on-drag-enter on-drag-enter :on-drag-enter on-drag-enter
:on-drag-over on-drag-over :on-drag-over on-drag-over
:on-drag-end on-drag-end
:on-drop on-drop :on-drop on-drop
:on-pointer-down on-pointer-down :on-pointer-down on-pointer-down
:on-pointer-enter on-pointer-enter :on-pointer-enter on-pointer-enter

View file

@ -21,6 +21,7 @@
[app.main.data.workspace.specialized-panel :as-alias dwsp] [app.main.data.workspace.specialized-panel :as-alias dwsp]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.workspace.sidebar.assets.components :as wsac]
[app.main.ui.workspace.viewport.viewport-ref :as uwvv] [app.main.ui.workspace.viewport.viewport-ref :as uwvv]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.dom.dnd :as dnd] [app.util.dom.dnd :as dnd]
@ -28,7 +29,8 @@
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.mouse :as mse] [app.util.mouse :as mse]
[app.util.object :as obj] [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] [app.util.webapi :as wapi]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -216,7 +218,7 @@
(st/emit! (mse/->MouseEvent :double-click ctrl? shift? alt? meta?)) (st/emit! (mse/->MouseEvent :double-click ctrl? shift? alt? meta?))
;; Emit asynchronously so the double click to exit shapes won't break ;; Emit asynchronously so the double click to exit shapes won't break
(timers/schedule (ts/schedule
(fn [] (fn []
(when (and (not drawing-path?) shape) (when (and (not drawing-path?) shape)
(cond (cond
@ -244,7 +246,7 @@
workspace-read-only?) workspace-read-only?)
(let [position (dom/get-client-position event)] (let [position (dom/get-client-position event)]
;; Delayed callback because we need to wait to the previous context menu to be closed ;; Delayed callback because we need to wait to the previous context menu to be closed
(timers/schedule (ts/schedule
#(st/emit! #(st/emit!
(if (some? @hover) (if (some? @hover)
(dw/show-shape-context-menu {:position position (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 ;; We store this so in Firefox the middle button won't do a paste of the content
(reset! disable-paste true) (reset! disable-paste true)
(timers/schedule #(reset! disable-paste false))) (ts/schedule #(reset! disable-paste false)))
(st/emit! (dw/finish-panning) (st/emit! (dw/finish-panning)
(dw/finish-zooming)))))) (dw/finish-zooming))))))
@ -400,9 +402,28 @@
(st/emit! (dw/update-viewport-position {:x #(+ % (/ delta-x zoom)) (st/emit! (dw/update-viewport-position {:x #(+ % (/ delta-x zoom))
:y #(+ % (/ delta-y zoom))})))))))))) :y #(+ % (/ delta-y zoom))}))))))))))
(defn on-drag-enter [] (defn on-drag-enter
[comp-inst-ref]
(mf/use-callback (mf/use-callback
(fn [e] (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") (when (or (dnd/has-type? e "penpot/shape")
(dnd/has-type? e "penpot/component") (dnd/has-type? e "penpot/component")
(dnd/has-type? e "Files") (dnd/has-type? e "Files")
@ -410,8 +431,19 @@
(dnd/has-type? e "text/asset-id")) (dnd/has-type? e "text/asset-id"))
(dom/prevent-default e))))) (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] (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 (mf/use-callback
(fn [e] (fn [e]
(when (or (dnd/has-type? e "penpot/shape") (when (or (dnd/has-type? e "penpot/shape")
@ -423,7 +455,7 @@
(dom/prevent-default e)))))) (dom/prevent-default e))))))
(defn on-drop (defn on-drop
[file] [file comp-inst-ref]
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
@ -443,13 +475,13 @@
(assoc :y final-y))))) (assoc :y final-y)))))
(dnd/has-type? event "penpot/component") (dnd/has-type? event "penpot/component")
(let [{:keys [component file-id]} (dnd/get-data event "penpot/component") (let [event (.-nativeEvent event)
shape (get-in component [:objects (:id component)]) ctrl? (kbd/ctrl? event)
final-x (- (:x viewport-coord) (/ (:width shape) 2)) shift? (kbd/shift? event)
final-y (- (:y viewport-coord) (/ (:height shape) 2))] alt? (kbd/alt? event)
(st/emit! (dwl/instantiate-component file-id meta? (kbd/meta? event)]
(:id component) (st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?))
(gpt/point final-x final-y)))) (mf/set-ref-val! comp-inst-ref false))
;; Will trigger when the user drags an image from a browser ;; Will trigger when the user drags an image from a browser
;; to the viewport (firefox and chrome do it a bit different ;; to the viewport (firefox and chrome do it a bit different
@ -517,4 +549,3 @@
(not @disable-paste) (not @disable-paste)
(not workspace-read-only?)) (not workspace-read-only?))
(st/emit! (dw/paste-from-event event @in-viewport?))))))) (st/emit! (dw/paste-from-event event @in-viewport?)))))))

View file

@ -62,6 +62,13 @@
(.setData dt data-type data)) (.setData dt data-type data))
e))) e)))
(defn invisible-image
[]
(let [img (js/Image.)
imd "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="]
(set! (.-src img) imd)
img))
(defn set-drag-image! (defn set-drag-image!
([e image] ([e image]
(set-drag-image! e image 0 0)) (set-drag-image! e image 0 0))
@ -108,11 +115,13 @@
([e] ([e]
(get-data e "penpot/data")) (get-data e "penpot/data"))
([e data-type] ([e data-type]
(let [dt (.-dataTransfer e)] (let [dt (.-dataTransfer e)
(if (or (str/starts-with? data-type "penpot") data (.getData dt data-type)]
(= data-type "application/json")) (cond-> data
(t/decode-str (.getData dt data-type)) (and (some? data) (not= data "")
(.getData dt data-type))))) (or (str/starts-with? data-type "penpot")
(= data-type "application/json")))
(t/decode-str)))))
(defn get-files (defn get-files
[e] [e]

View file

@ -8,7 +8,7 @@
(:require (:require
[beicon.v2.core :as rx])) [beicon.v2.core :as rx]))
(defn- throttle-fn (defn throttle-fn
[delay f] [delay f]
(let [state (let [state
#js {:lastExecTime 0 #js {:lastExecTime 0