mirror of
https://github.com/penpot/penpot.git
synced 2026-03-26 21:31:24 +01: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:
@@ -874,6 +874,42 @@
|
||||
(duplicate-cells :column index (inc index) ids-map)
|
||||
(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
|
||||
[attr span-attr track-num]
|
||||
(fn [[_ cell]]
|
||||
|
||||
@@ -787,3 +787,135 @@
|
||||
(dch/commit-changes changes)
|
||||
(ptk/data-event :layout/update {:ids [layout-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]}]
|
||||
(let [{:keys [grid cells]} mdata
|
||||
|
||||
grid-id (:id grid)
|
||||
single? (= (count cells) 1)
|
||||
|
||||
can-merge?
|
||||
@@ -786,17 +787,53 @@
|
||||
(mf/deps 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
|
||||
(mf/use-fn
|
||||
(mf/deps grid cells)
|
||||
(mf/deps grid-id cells)
|
||||
(fn []
|
||||
(st/emit! (dwsl/merge-cells (:id grid) (map :id cells)))))
|
||||
(st/emit! (dwsl/merge-cells grid-id (map :id cells)))))
|
||||
|
||||
do-create-board
|
||||
(mf/use-fn
|
||||
(mf/deps grid cells)
|
||||
(mf/deps grid-id cells)
|
||||
(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?)
|
||||
[:> 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")
|
||||
: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
|
||||
|
||||
@@ -5589,6 +5589,15 @@ msgstr "Create board"
|
||||
msgid "workspace.context-menu.grid-cells.merge"
|
||||
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
|
||||
msgid "workspace.context-menu.grid-track.column.add-after"
|
||||
msgstr "Add 1 column to the right"
|
||||
|
||||
Reference in New Issue
Block a user