diff --git a/resources/css/components/fieldtypes/replicator.css b/resources/css/components/fieldtypes/replicator.css index 46c7d7dcae..03a532d078 100644 --- a/resources/css/components/fieldtypes/replicator.css +++ b/resources/css/components/fieldtypes/replicator.css @@ -4,6 +4,7 @@ .replicator-set-container { @apply outline-none; + min-height: 1rem; } .replicator-set { @@ -80,3 +81,21 @@ .replicator-fullscreen { @apply fixed bg-gray-200 inset-0 min-h-screen overflow-scroll rounded-none; } + +@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; + } +} diff --git a/resources/js/bootstrap/components.js b/resources/js/bootstrap/components.js index 1ab414772f..001f5b14a7 100644 --- a/resources/js/bootstrap/components.js +++ b/resources/js/bootstrap/components.js @@ -60,6 +60,7 @@ import ResourceDeleter from '../components/ResourceDeleter.vue'; import Stack from '../components/stacks/Stack.vue'; import StackTest from '../components/stacks/StackTest.vue'; import CodeBlock from '../components/CodeBlock.vue'; +import SortableList from '../components/sortable/SortableList.vue'; // Third Party Vue.component('v-select', vSelect) @@ -129,6 +130,7 @@ Vue.component('create-entry-button', CreateEntryButton); Vue.component('popover', Popover); Vue.component('portal', Portal); Vue.component('code-block', CodeBlock); +Vue.component('sortable-list', SortableList); // Recursive Vue.component('role-permission-tree', PermissionTree); diff --git a/resources/js/bootstrap/globals.js b/resources/js/bootstrap/globals.js index aca3362eb7..aba61aeefe 100644 --- a/resources/js/bootstrap/globals.js +++ b/resources/js/bootstrap/globals.js @@ -118,13 +118,18 @@ export function replicatorPreviewHtml(html) { return new PreviewHtml(html); } -export function closestVm(el, name) { - let parent = el; - while (parent) { - if (parent.__vue__) break; - parent = parent.parentElement; +export function closestVm(obj, name) { + let vm, parent; + if (obj instanceof HTMLElement) { + parent = obj; + while (parent) { + if (parent.__vue__) break; + parent = parent.parentElement; + } + vm = parent.__vue__; + } else { + vm = obj; } - let vm = parent.__vue__; while (vm !== vm.$root) { if (!name || name === vm.$options.name) return vm; vm = vm.$parent; @@ -138,3 +143,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 fe01c7dfa1..19669aae6d 100644 --- a/resources/js/components/fieldtypes/replicator/Replicator.vue +++ b/resources/js/components/fieldtypes/replicator/Replicator.vue @@ -6,7 +6,11 @@ once it has been portaled out. -->
-
+

@@ -43,11 +47,15 @@ @@ -59,9 +67,10 @@ :values="set" :meta="meta.existing[set._id]" :config="setConfig(set.type)" + :config-hash="setConfigHash(set.type)" :parent-name="name" - :sortable-item-class="sortableItemClass" - :sortable-handle-class="sortableHandleClass" + sortable-item-class="replicator-sortable-item" + sortable-handle-class="replicator-sortable-handle" :is-read-only="isReadOnly" :collapsed="collapsed.includes(set._id)" :field-path-prefix="fieldPathPrefix || handle" @@ -117,6 +126,7 @@ import AddSetButton from './AddSetButton.vue'; import ManagesSetMeta from './ManagesSetMeta'; import { SortableList } from '../../sortable/Sortable'; import reduce from 'underscore/modules/reduce'; +import { closestVm } from '../../../bootstrap/globals'; export default { @@ -136,6 +146,7 @@ export default { collapsed: clone(this.meta.collapsed), previews: this.meta.previews, fullScreenMode: false, + canDropSet: null, provide: { storeName: this.storeName, replicatorSets: this.config.sets @@ -157,16 +168,12 @@ export default { }, []); }, - groupConfigs() { - return this.config.sets; + setConfigHashes() { + return this.meta.setConfigHashes; }, - sortableItemClass() { - return `${this.name}-sortable-item`; - }, - - sortableHandleClass() { - return `${this.name}-sortable-handle`; + groupConfigs() { + return this.config.sets; }, storeState() { @@ -186,6 +193,10 @@ export default { return _.find(this.setConfigs, { handle }) || {}; }, + setConfigHash(handle) { + return this.setConfigHashes[handle]; + }, + updated(index, set) { this.update([...this.value.slice(0, index), set, ...this.value.slice(index + 1)]); }, @@ -196,8 +207,32 @@ 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 = closestVm(oldList, 'replicator-fieldtype'); + const set = oldReplicator.value[oldIndex]; + const meta = oldReplicator.meta.existing[set._id]; + const previews = oldReplicator.previews[set._id]; + this.updateSetPreviews(set._id, previews); + 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) { @@ -285,6 +320,20 @@ export default { return Object.keys(this.storeState.errors ?? []).some(handle => handle.startsWith(prefix)); }, + + 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 47ee202824..9d6d5c81f2 100644 --- a/resources/js/components/fieldtypes/replicator/Set.vue +++ b/resources/js/components/fieldtypes/replicator/Set.vue @@ -1,6 +1,6 @@