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 @@
-
+
@@ -91,6 +91,10 @@ export default {
type: Object,
required: true
},
+ configHash: {
+ type: String,
+ required: true
+ },
meta: {
type: Object,
required: true
diff --git a/resources/js/components/sortable/SortableList.vue b/resources/js/components/sortable/SortableList.vue
index ce1cc588dc..10cd3b4545 100644
--- a/resources/js/components/sortable/SortableList.vue
+++ b/resources/js/components/sortable/SortableList.vue
@@ -1,18 +1,8 @@