From ebfe4ec5699d6895e029bae2a340f9543d4f106c Mon Sep 17 00:00:00 2001 From: Jack Sleight Date: Thu, 13 Nov 2025 16:35:40 +0000 Subject: [PATCH 1/4] Ability to drag and drop between replicators --- .../css/components/fieldtypes/replicator.css | 18 ++++ resources/js/bootstrap/globals.js | 19 ++++ .../fieldtypes/replicator/Replicator.vue | 72 +++++++++++++-- .../components/fieldtypes/replicator/Set.vue | 3 +- .../js/components/sortable/SortableList.vue | 88 +++++++++++++++---- src/Fields/Field.php | 10 +++ src/Fieldtypes/Replicator.php | 38 +++++--- 7 files changed, 209 insertions(+), 39 deletions(-) diff --git a/resources/css/components/fieldtypes/replicator.css b/resources/css/components/fieldtypes/replicator.css index cbf0f8dc3bf..542ee844648 100644 --- a/resources/css/components/fieldtypes/replicator.css +++ b/resources/css/components/fieldtypes/replicator.css @@ -1,3 +1,21 @@ /* ========================================================================== REPLICATOR FIELDTYPE ========================================================================== */ + +@scope (.replicator-droppable) to (.replicator-fieldtype) { + :scope { + cursor: auto; + } + * { + pointer-events: auto; + } +} + +@scope (.replicator-not-droppable) to (.replicator-fieldtype) { + :scope { + cursor: not-allowed; + } + * { + pointer-events: none; + } +} \ No newline at end of file diff --git a/resources/js/bootstrap/globals.js b/resources/js/bootstrap/globals.js index f8e79def259..89f520bf2b3 100644 --- a/resources/js/bootstrap/globals.js +++ b/resources/js/bootstrap/globals.js @@ -157,3 +157,22 @@ export function str_slug(string) { export function snake_case(string) { return Statamic.$slug.separatedBy('_').create(string); } + +export function arrayAdd(items, item, index) { + return [ + ...items.slice(0, index), + item, + ...items.slice(index, items.length) + ] +} + +export function arrayRemove(items, index) { + return [ + ...items.slice(0, index), + ...items.slice(index + 1, items.length) + ] +} + +export function arrayMove(items, oldIndex, newIndex) { + return arrayAdd(arrayRemove(items, oldIndex), items[oldIndex], newIndex); +} \ No newline at end of file diff --git a/resources/js/components/fieldtypes/replicator/Replicator.vue b/resources/js/components/fieldtypes/replicator/Replicator.vue index 9626e8cf9a3..44e9f9eaf56 100644 --- a/resources/js/components/fieldtypes/replicator/Replicator.vue +++ b/resources/js/components/fieldtypes/replicator/Replicator.vue @@ -7,7 +7,11 @@
c.handle === handle) || {}; }, + setConfigHash(handle) { + return this.setConfigHashes[handle]; + }, + updated(index, set) { this.update([...this.value.slice(0, index), set, ...this.value.slice(index + 1)]); }, @@ -194,9 +214,31 @@ export default { this.update([...this.value.slice(0, index), ...this.value.slice(index + 1)]); }, - sorted(value) { - this.update(value); - }, + sorted({ operation, oldIndex, newIndex, oldList }) { + if (operation === 'move') { + // Move set within this replicator + this.update(arrayMove(this.value, oldIndex, newIndex)); + } else if (operation === 'add') { + // Add set to this replicator + const oldReplicator = oldList.owner; + const set = oldReplicator.value[oldIndex]; + const meta = oldReplicator.meta.existing[set._id]; + this.updateSetMeta(set._id, meta); + this.update(arrayAdd(this.value, set, newIndex)); + if (oldReplicator.collapsed.includes(set._id)) { + this.collapseSet(set._id); + } else { + this.expandSet(set._id); + } + // Remove set from old replicator + // Do this from the target replicator in order to avoid race conditions with nested + // replicators both trying to update their parent's value at the same time. + this.$nextTick(() => { + oldReplicator.removeSetMeta(set._id); + oldReplicator.update(arrayRemove(oldReplicator.value, oldIndex)); + }); + } + }, addSet(handle, index) { const set = { @@ -273,6 +315,20 @@ export default { return this.errorsById.hasOwnProperty(id) && this.errorsById[id].length > 0; }, + + sortableGroupValidator({ source }) { + this.canDropSet = this.canAddSet && Object.values(this.setConfigHashes).includes(source.dataset.configHash); + return this.canDropSet; + }, + + sortableGroupStart({ valid }) { + this.canDropSet = valid; + }, + + sortableGroupEnd() { + this.canDropSet = null; + }, + }, mounted() { diff --git a/resources/js/components/fieldtypes/replicator/Set.vue b/resources/js/components/fieldtypes/replicator/Set.vue index 240641ca93a..ca8a537897f 100644 --- a/resources/js/components/fieldtypes/replicator/Set.vue +++ b/resources/js/components/fieldtypes/replicator/Set.vue @@ -25,6 +25,7 @@ const replicatorSets = inject('replicatorSets'); const props = defineProps({ config: Object, + configHash: String, id: String, fieldPath: String, metaPath: String, @@ -120,7 +121,7 @@ function destroy() {