Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.x] Ability to cut/copy/paste replicator sets #10359

Open
wants to merge 10 commits into
base: 5.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions resources/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import './components/portals/Portals';
import './components/stacks/Stacks';
import './components/ProgressBar';
import './components/DirtyState';
import './components/Clipboard';
import './components/Config';
import './components/Preference';
import './components/Permission';
Expand Down
93 changes: 93 additions & 0 deletions resources/js/components/Clipboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Vue from 'vue'
import Cookies from 'cookies-js';
import uid from 'uniqid';

const vm = new Vue({

data: {
id: null,
data: null,
},

created() {
let id = Cookies.get('statamic.clipboard');
if (!id) {
id = uid();
Cookies.set('statamic.clipboard', id);
}
this.id = id;
this.data = this.storageParse(this.storageRead());
window.addEventListener('storage', (event) => {
if (event.key === 'statamic.clipboard') {
this.data = this.storageParse(event.newValue);
}
});
},

methods: {

set(data) {
this.data = data;
this.storageWrite(this.data);
},

get() {
return this.data;
},

clear() {
this.data = null;
this.storageClear();
},

storageRead() {
return localStorage.getItem('statamic.clipboard');
},

storageWrite(data) {
localStorage.setItem('statamic.clipboard', JSON.stringify({
id: this.id,
payload: data,
}));
},

storageClear() {
localStorage.removeItem('statamic.clipboard');
},

storageParse(value) {
const parsed = JSON.parse(value);
if (!parsed) {
return null;
}
const { id, payload } = parsed;
if (id !== this.id) {
this.storageClear();
return null;
}
return payload;
},

}

});

class Clipboard {
set(data) {
vm.set(data);
}
get() {
return vm.data;
}
clear(data) {
vm.clear();
}
}

Object.defineProperties(Vue.prototype, {
$clipboard: {
get() {
return new Clipboard;
}
}
});
20 changes: 18 additions & 2 deletions resources/js/components/fieldtypes/replicator/AddSetButton.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>

<div class="replicator-set-picker">
<div class="replicator-set-picker flex items-center">
<set-picker :enabled="enabled" :sets="groups" @added="addSet">
<template #trigger>
<div class="replicator-set-picker-button-wrapper flex items-center ">
Expand All @@ -23,6 +23,17 @@
</div>
</template>
</set-picker>
<button
v-if="enabled && pasteEnabled"
v-tooltip="__('Paste Sets')"
class="btn-round flex items-center justify-center h-5 w-5 ml-1"
@click="pasteSets">
<svg-icon name="regular/paragraph-align-justified"
:class="{
'w-2 h-2 text-gray-800 group-hover:text-black': last,
'w-2 h-2 text-gray-700 group-hover:text-black transition duration-150': !last
}" />
</button>
</div>

</template>
Expand All @@ -41,6 +52,7 @@ export default {
groups: Array,
index: Number,
last: Boolean,
pasteEnabled: Boolean,
enabled: { type: Boolean, default: true },
label: String,
},
Expand All @@ -55,7 +67,11 @@ export default {
if (this.sets.length === 1) {
this.addSet(this.sets[0].handle);
}
}
},

pasteSets() {
this.$emit('pasted', this.index);
},

}

Expand Down
115 changes: 113 additions & 2 deletions resources/js/components/fieldtypes/replicator/Replicator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
@collapsed="collapseSet(set._id)"
@expanded="expandSet(set._id)"
@duplicated="duplicateSet(set._id)"
@cut="copySet(set._id, true)"
@copied="copySet(set._id)"
@updated="updated"
@meta-updated="updateSetMeta(set._id, $event)"
@removed="removed(set, index)"
Expand All @@ -63,7 +65,9 @@
:sets="setConfigs"
:index="index"
:enabled="canAddSet"
@added="addSet" />
:paste-enabled="canPasteSets"
@added="addSet"
@pasted="pasteSets" />
</template>
</replicator-set>
</div>
Expand All @@ -76,7 +80,9 @@
:sets="setConfigs"
:index="value.length"
:label="config.button_label"
@added="addSet" />
:paste-enabled="canPasteSets"
@added="addSet"
@pasted="pasteSets" />

</section>

Expand Down Expand Up @@ -128,12 +134,29 @@ export default {
return !this.config.max_sets || this.value.length < this.config.max_sets;
},

canPasteSets() {
if (!this.canAddSet) {
return false;
}
const data = this.$clipboard.get();
if (data?.type !== 'replicator') {
return false;
}
const itemConfigHashes = data.items.map(item => item.configHash);
const setConfigHashes = Object.values(this.setConfigHashes);
return itemConfigHashes.every(hash => setConfigHashes.includes(hash));
},

setConfigs() {
return reduce(this.groupConfigs, (sets, group) => {
return sets.concat(group.sets);
}, []);
},

setConfigHashes() {
return this.meta.setConfigHashes;
},

groupConfigs() {
return this.config.sets;
},
Expand Down Expand Up @@ -172,6 +195,14 @@ export default {
visibleWhenReadOnly: true,
run: this.collapseAll,
},
{
title: __('Cut All Sets'),
run: () => this.copySets(true),
},
{
title: __('Copy All Sets'),
run: () => this.copySets(),
},
{
title: __('Toggle Fullscreen Mode'),
icon: ({ vm }) => vm.fullScreenMode ? 'shrink-all' : 'expand-bold',
Expand All @@ -189,6 +220,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)]);
},
Expand Down Expand Up @@ -245,6 +280,81 @@ export default {
this.expandSet(set._id);
},

copySet(id, cut = false) {
const index = this.value.findIndex(v => v._id === id);
const value = this.value[index];
const meta = this.meta.existing[id];

this.$clipboard.set({
type: 'replicator',
items: [{
configHash: this.setConfigHash(value.type),
value: value,
meta: meta,
}],
});

if (cut) {
this.removed(value, index);
}
},

copySets(cut = false) {
this.$clipboard.set({
type: 'replicator',
items: this.value.map((value) => ({
configHash: this.setConfigHash(value.type),
value: value,
meta: this.meta.existing[value._id],
})),
});

if (cut) {
this.update([]);
this.updateMeta({ ...this.meta, existing: {} });
}
},

pasteSets(index) {
const data = this.$clipboard.get();
if (!data || data.type !== 'replicator') {
return;
}

const value = [];
const meta = {};
const previews = {};
data.items.forEach((item) => {
const set = { ...item.value, _id: uniqid() };
value.push(set);
meta[set._id] = item.meta;
previews[set._id] = {};
});

this.previews = {
...this.previews,
...previews,
};

this.updateMeta({
...this.meta,
existing: {
...this.meta.existing,
...meta,
},
});

this.update([
...this.value.slice(0, index),
...value,
...this.value.slice(index)
]);

value.forEach((set) => {
this.expandSet(set._id);
});
},

updateSetPreviews(id, previews) {
this.previews[id] = previews;
},
Expand Down Expand Up @@ -292,6 +402,7 @@ export default {

return Object.keys(this.storeState.errors ?? []).some(handle => handle.startsWith(prefix));
},

},

mounted() {
Expand Down
10 changes: 10 additions & 0 deletions resources/js/components/fieldtypes/replicator/Set.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
<div class="divider" />
<dropdown-item :text="__(collapsed ? __('Expand Set') : __('Collapse Set'))" @click="toggleCollapsedState" />
<dropdown-item :text="__('Duplicate Set')" @click="duplicate" v-if="canAddSet" />
<dropdown-item :text="__('Cut Set')" @click="cut" />
<dropdown-item :text="__('Copy Set')" @click="copy" />
<dropdown-item :text="__('Delete Set')" class="warning" @click="destroy" />
</dropdown-list>
</div>
Expand Down Expand Up @@ -263,6 +265,14 @@ export default {
this.$emit('duplicated');
},

cut() {
this.$emit('cut');
},

copy() {
this.$emit('copied');
},

fieldPath(field) {
return `${this.fieldPathPrefix}.${this.index}.${field.handle}`;
},
Expand Down
10 changes: 10 additions & 0 deletions src/Fields/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Field implements Arrayable
protected $handle;
protected $prefix;
protected $config;
protected $configHash;
protected $value;
protected $parent;
protected $parentField;
Expand Down Expand Up @@ -459,6 +460,15 @@ public function form(): ?Form
return $this->form;
}

public function configHash(): string
{
if (! isset($this->configHash)) {
$this->configHash = md5($this->handle.json_encode($this->config));
}

return $this->configHash;
}

public static function commonFieldOptions(): Fields
{
$reserved = [
Expand Down
Loading
Loading