mirror of
https://github.com/penpot/penpot.git
synced 2026-09-12 23:19:28 +00:00
✨ Add copy and paste for grid layout rows and columns via co… (#8498)
* ✨ Add copy and paste for grid layout rows and columns via context menu * 🔧 Use grid-id instead of grid in context menu deps --------- Co-authored-by: bittoby <bittoby@users.noreply.github.com>
This commit is contained in:
parent
2a09f30199
commit
b876417d5b
@ -874,6 +874,42 @@
|
|||||||
(duplicate-cells :column index (inc index) ids-map)
|
(duplicate-cells :column index (inc index) ids-map)
|
||||||
(assign-cells objects))))
|
(assign-cells objects))))
|
||||||
|
|
||||||
|
(defn duplicate-row-at
|
||||||
|
"Duplicate source row and insert the copy at target-index (0-indexed).
|
||||||
|
Like `duplicate-row` but inserts at an arbitrary position.
|
||||||
|
Note: after add-grid-row, if target <= source the source cells shift
|
||||||
|
by +1, so we must adjust the from-index for duplicate-cells."
|
||||||
|
[shape objects source-index target-index ids-map]
|
||||||
|
(let [value (dm/get-in shape [:layout-grid-rows source-index])
|
||||||
|
;; After inserting at target-index, cells at rows >= (inc target-index)
|
||||||
|
;; get shifted +1. If target <= source, the source row shifts.
|
||||||
|
adjusted-source (if (<= target-index source-index)
|
||||||
|
(inc source-index)
|
||||||
|
source-index)]
|
||||||
|
(-> shape
|
||||||
|
(remove-cell-areas-after :row source-index)
|
||||||
|
(add-grid-row value target-index)
|
||||||
|
(duplicate-cells :row adjusted-source target-index ids-map)
|
||||||
|
(assign-cells objects))))
|
||||||
|
|
||||||
|
(defn duplicate-column-at
|
||||||
|
"Duplicate source column and insert the copy at target-index (0-indexed).
|
||||||
|
Like `duplicate-column` but inserts at an arbitrary position.
|
||||||
|
Note: after add-grid-column, if target <= source the source cells shift
|
||||||
|
by +1, so we must adjust the from-index for duplicate-cells."
|
||||||
|
[shape objects source-index target-index ids-map]
|
||||||
|
(let [value (dm/get-in shape [:layout-grid-columns source-index])
|
||||||
|
;; After inserting at target-index, cells at columns >= (inc target-index)
|
||||||
|
;; get shifted +1. If target <= source, the source column shifts.
|
||||||
|
adjusted-source (if (<= target-index source-index)
|
||||||
|
(inc source-index)
|
||||||
|
source-index)]
|
||||||
|
(-> shape
|
||||||
|
(remove-cell-areas-after :column source-index)
|
||||||
|
(add-grid-column value target-index)
|
||||||
|
(duplicate-cells :column adjusted-source target-index ids-map)
|
||||||
|
(assign-cells objects))))
|
||||||
|
|
||||||
(defn make-remove-cell
|
(defn make-remove-cell
|
||||||
[attr span-attr track-num]
|
[attr span-attr track-num]
|
||||||
(fn [[_ cell]]
|
(fn [[_ cell]]
|
||||||
|
|||||||
@ -787,3 +787,135 @@
|
|||||||
(dch/commit-changes changes)
|
(dch/commit-changes changes)
|
||||||
(ptk/data-event :layout/update {:ids [layout-id]})
|
(ptk/data-event :layout/update {:ids [layout-id]})
|
||||||
(dwu/commit-undo-transaction undo-id))))))
|
(dwu/commit-undo-transaction undo-id))))))
|
||||||
|
|
||||||
|
(defn complete-rows?
|
||||||
|
"Check if the selected cells cover complete row(s) — all columns must be included."
|
||||||
|
[grid cells]
|
||||||
|
(let [{:keys [first-column last-column]} (ctl/cells-coordinates cells)
|
||||||
|
num-columns (count (:layout-grid-columns grid))]
|
||||||
|
(and (= first-column 1)
|
||||||
|
(= last-column num-columns))))
|
||||||
|
|
||||||
|
(defn complete-columns?
|
||||||
|
"Check if the selected cells cover complete column(s) — all rows must be included."
|
||||||
|
[grid cells]
|
||||||
|
(let [{:keys [first-row last-row]} (ctl/cells-coordinates cells)
|
||||||
|
num-rows (count (:layout-grid-rows grid))]
|
||||||
|
(and (= first-row 1)
|
||||||
|
(= last-row num-rows))))
|
||||||
|
|
||||||
|
(defn copy-grid-tracks
|
||||||
|
"Store the selected track indices for later paste. Works for both
|
||||||
|
complete rows and complete columns."
|
||||||
|
[grid-id type]
|
||||||
|
(assert (#{:row :column} type))
|
||||||
|
(ptk/reify ::copy-grid-tracks
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [objects (dsh/lookup-page-objects state)
|
||||||
|
grid (get objects grid-id)
|
||||||
|
selected (get-in state [:workspace-grid-edition grid-id :selected])
|
||||||
|
cells (->> selected (map #(get-in grid [:layout-grid-cells %])))
|
||||||
|
{:keys [first-row last-row first-column last-column]} (ctl/cells-coordinates cells)
|
||||||
|
;; Convert 1-indexed cell positions to 0-indexed track indices
|
||||||
|
track-indices (if (= type :row)
|
||||||
|
(vec (range (dec first-row) last-row))
|
||||||
|
(vec (range (dec first-column) last-column)))]
|
||||||
|
(assoc-in state [:workspace-grid-edition grid-id :copied-tracks]
|
||||||
|
{:track-indices track-indices
|
||||||
|
:type type
|
||||||
|
:grid-id grid-id})))))
|
||||||
|
|
||||||
|
(defn paste-grid-tracks
|
||||||
|
"Paste previously copied tracks at the end of the grid.
|
||||||
|
Each source track is duplicated and appended after the last
|
||||||
|
existing track. All operations are grouped in a single undo
|
||||||
|
transaction. Follows the same pattern as `duplicate-layout-track`."
|
||||||
|
[grid-id]
|
||||||
|
(ptk/reify ::paste-grid-tracks
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [it state _]
|
||||||
|
(let [file-id (:current-file-id state)
|
||||||
|
page (dsh/lookup-page state)
|
||||||
|
objects (:objects page)
|
||||||
|
libraries (dsh/lookup-libraries state)
|
||||||
|
library-data (dsh/lookup-file state file-id)
|
||||||
|
grid (get objects grid-id)
|
||||||
|
|
||||||
|
copied (get-in state [:workspace-grid-edition grid-id :copied-tracks])
|
||||||
|
track-indices (:track-indices copied)
|
||||||
|
type (:type copied)
|
||||||
|
undo-id (js/Symbol)]
|
||||||
|
|
||||||
|
(when (and (seq track-indices) (some? type))
|
||||||
|
(let [shapes-by-track-fn
|
||||||
|
(if (= type :row)
|
||||||
|
ctl/shapes-by-row
|
||||||
|
ctl/shapes-by-column)
|
||||||
|
|
||||||
|
;; Collect shapes from all source tracks
|
||||||
|
all-shapes
|
||||||
|
(->> track-indices
|
||||||
|
(mapcat #(shapes-by-track-fn grid % false))
|
||||||
|
(set))
|
||||||
|
|
||||||
|
;; Generate duplication changes for all shapes at once
|
||||||
|
changes
|
||||||
|
(-> (pcb/empty-changes it)
|
||||||
|
(cll/generate-duplicate-changes objects page all-shapes (gpt/point 0 0) libraries library-data file-id)
|
||||||
|
(cll/generate-duplicate-changes-update-indices objects all-shapes))
|
||||||
|
|
||||||
|
;; Build ids-map: old-shape-id -> new-shape-id
|
||||||
|
ids-map
|
||||||
|
(->> changes
|
||||||
|
:redo-changes
|
||||||
|
(filter #(= (:type %) :add-obj))
|
||||||
|
(filter #(all-shapes (:old-id %)))
|
||||||
|
(map #(vector (:old-id %) (get-in % [:obj :id])))
|
||||||
|
(into {}))
|
||||||
|
|
||||||
|
duplicate-at-fn
|
||||||
|
(if (= type :row)
|
||||||
|
ctl/duplicate-row-at
|
||||||
|
ctl/duplicate-column-at)
|
||||||
|
|
||||||
|
tracks-prop
|
||||||
|
(if (= type :row)
|
||||||
|
:layout-grid-rows
|
||||||
|
:layout-grid-columns)
|
||||||
|
|
||||||
|
;; Sort source indices ascending — we'll append each
|
||||||
|
;; copy at the end in order, preserving the original
|
||||||
|
;; track ordering in the appended block.
|
||||||
|
sorted-indices (vec (sort track-indices))
|
||||||
|
|
||||||
|
changes
|
||||||
|
(-> changes
|
||||||
|
(pcb/update-shapes
|
||||||
|
[grid-id]
|
||||||
|
(fn [shape objects]
|
||||||
|
;; Restore grid structure (duplication may have altered it)
|
||||||
|
(let [shape (merge shape (select-keys grid [:layout-grid-cells :layout-grid-columns :layout-grid-rows]))]
|
||||||
|
;; Append each source track at the end.
|
||||||
|
;; Process in ascending order so the copies
|
||||||
|
;; appear in the same order as the originals.
|
||||||
|
;; Each insertion adds one track, so both the
|
||||||
|
;; target index and the source index (if it
|
||||||
|
;; comes after the target) shift by 1.
|
||||||
|
(reduce
|
||||||
|
(fn [s [offset src-idx]]
|
||||||
|
(let [;; Source tracks don't shift because we
|
||||||
|
;; append after them (target > source).
|
||||||
|
actual-src src-idx
|
||||||
|
;; Append at the end (which grows by
|
||||||
|
;; one with each iteration).
|
||||||
|
target-idx (+ (count (get grid tracks-prop)) offset)]
|
||||||
|
(duplicate-at-fn s objects actual-src target-idx ids-map)))
|
||||||
|
shape
|
||||||
|
(map-indexed vector sorted-indices))))
|
||||||
|
{:with-objects? true}))]
|
||||||
|
|
||||||
|
(rx/of (dwu/start-undo-transaction undo-id)
|
||||||
|
(dch/commit-changes changes)
|
||||||
|
(ptk/data-event :layout/update {:ids [grid-id]})
|
||||||
|
(dwu/commit-undo-transaction undo-id))))))))
|
||||||
|
|||||||
@ -779,6 +779,7 @@
|
|||||||
[{:keys [mdata]}]
|
[{:keys [mdata]}]
|
||||||
(let [{:keys [grid cells]} mdata
|
(let [{:keys [grid cells]} mdata
|
||||||
|
|
||||||
|
grid-id (:id grid)
|
||||||
single? (= (count cells) 1)
|
single? (= (count cells) 1)
|
||||||
|
|
||||||
can-merge?
|
can-merge?
|
||||||
@ -786,17 +787,53 @@
|
|||||||
(mf/deps cells)
|
(mf/deps cells)
|
||||||
#(ctl/valid-area-cells? cells))
|
#(ctl/valid-area-cells? cells))
|
||||||
|
|
||||||
|
can-copy-rows?
|
||||||
|
(mf/use-memo
|
||||||
|
(mf/deps grid cells)
|
||||||
|
#(dwsl/complete-rows? grid cells))
|
||||||
|
|
||||||
|
can-copy-columns?
|
||||||
|
(mf/use-memo
|
||||||
|
(mf/deps grid cells)
|
||||||
|
#(dwsl/complete-columns? grid cells))
|
||||||
|
|
||||||
|
grid-edition-ref
|
||||||
|
(mf/use-memo
|
||||||
|
(mf/deps grid-id)
|
||||||
|
#(refs/workspace-grid-edition-id grid-id))
|
||||||
|
|
||||||
|
grid-edition (mf/deref grid-edition-ref)
|
||||||
|
has-copied-tracks? (some? (:copied-tracks grid-edition))
|
||||||
|
|
||||||
do-merge-cells
|
do-merge-cells
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps grid cells)
|
(mf/deps grid-id cells)
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit! (dwsl/merge-cells (:id grid) (map :id cells)))))
|
(st/emit! (dwsl/merge-cells grid-id (map :id cells)))))
|
||||||
|
|
||||||
do-create-board
|
do-create-board
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps grid cells)
|
(mf/deps grid-id cells)
|
||||||
(fn []
|
(fn []
|
||||||
(st/emit! (dwsl/create-cell-board (:id grid) (map :id cells)))))]
|
(st/emit! (dwsl/create-cell-board grid-id (map :id cells)))))
|
||||||
|
|
||||||
|
do-copy-rows
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps grid-id)
|
||||||
|
(fn []
|
||||||
|
(st/emit! (dwsl/copy-grid-tracks grid-id :row))))
|
||||||
|
|
||||||
|
do-copy-columns
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps grid-id)
|
||||||
|
(fn []
|
||||||
|
(st/emit! (dwsl/copy-grid-tracks grid-id :column))))
|
||||||
|
|
||||||
|
do-paste-tracks
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps grid-id)
|
||||||
|
(fn []
|
||||||
|
(st/emit! (dwsl/paste-grid-tracks grid-id))))]
|
||||||
[:*
|
[:*
|
||||||
(when (not single?)
|
(when (not single?)
|
||||||
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.merge")
|
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.merge")
|
||||||
@ -809,7 +846,19 @@
|
|||||||
|
|
||||||
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.create-board")
|
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.create-board")
|
||||||
:on-click do-create-board
|
:on-click do-create-board
|
||||||
:disabled (and (not single?) (not can-merge?))}]]))
|
:disabled (and (not single?) (not can-merge?))}]
|
||||||
|
|
||||||
|
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.copy-rows")
|
||||||
|
:on-click do-copy-rows
|
||||||
|
:disabled (not can-copy-rows?)}]
|
||||||
|
|
||||||
|
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.copy-columns")
|
||||||
|
:on-click do-copy-columns
|
||||||
|
:disabled (not can-copy-columns?)}]
|
||||||
|
|
||||||
|
[:> menu-entry* {:title (tr "workspace.context-menu.grid-cells.paste-tracks")
|
||||||
|
:on-click do-paste-tracks
|
||||||
|
:disabled (not has-copied-tracks?)}]]))
|
||||||
|
|
||||||
|
|
||||||
;; FIXME: optimize because it is rendered always
|
;; FIXME: optimize because it is rendered always
|
||||||
|
|||||||
@ -5589,6 +5589,15 @@ msgstr "Create board"
|
|||||||
msgid "workspace.context-menu.grid-cells.merge"
|
msgid "workspace.context-menu.grid-cells.merge"
|
||||||
msgstr "Merge cells"
|
msgstr "Merge cells"
|
||||||
|
|
||||||
|
msgid "workspace.context-menu.grid-cells.copy-rows"
|
||||||
|
msgstr "Copy rows"
|
||||||
|
|
||||||
|
msgid "workspace.context-menu.grid-cells.copy-columns"
|
||||||
|
msgstr "Copy columns"
|
||||||
|
|
||||||
|
msgid "workspace.context-menu.grid-cells.paste-tracks"
|
||||||
|
msgstr "Paste"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/context_menu.cljs:754
|
#: src/app/main/ui/workspace/context_menu.cljs:754
|
||||||
msgid "workspace.context-menu.grid-track.column.add-after"
|
msgid "workspace.context-menu.grid-track.column.add-after"
|
||||||
msgstr "Add 1 column to the right"
|
msgstr "Add 1 column to the right"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user