Merge pull request #9258 from umbraco/v8/feature/AB8699-Support-blocks-in-clipboard-service
Support blocks in backoffice clipboard
This commit is contained in:
@@ -57,14 +57,32 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
function replaceElementTypeBlockListUDIsResolver(obj, propClearingMethod) {
|
||||
replaceRawBlockListUDIsResolver(obj.value, propClearingMethod);
|
||||
function removeBlockReferences(obj) {
|
||||
for (var k in obj) {
|
||||
if(k === "contentUdi") {
|
||||
delete obj[k];
|
||||
} else if(k === "settingsUdi") {
|
||||
delete obj[k];
|
||||
} else {
|
||||
// lets crawl through all properties of layout to make sure get captured all `contentUdi` and `settingsUdi` properties.
|
||||
var propType = typeof obj[k];
|
||||
if(propType != null && (propType === "object" || propType === "array")) {
|
||||
removeBlockReferences(obj[k])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clipboardService.registerPastePropertyResolver(replaceElementTypeBlockListUDIsResolver, clipboardService.TYPES.ELEMENT_TYPE);
|
||||
|
||||
function elementTypeBlockResolver(obj, propPasteResolverMethod) {
|
||||
// we could filter for specific Property Editor Aliases, but as the Block Editor structure can be used by many Property Editor we do not in this code know a good way to detect that this is a Block Editor and will therefor leave it to the value structure to determin this.
|
||||
rawBlockResolver(obj.value, propPasteResolverMethod);
|
||||
}
|
||||
|
||||
clipboardService.registerPastePropertyResolver(elementTypeBlockResolver, clipboardService.TYPES.ELEMENT_TYPE);
|
||||
|
||||
|
||||
function replaceRawBlockListUDIsResolver(value, propClearingMethod) {
|
||||
function rawBlockResolver(value, propPasteResolverMethod) {
|
||||
if (value != null && typeof value === "object") {
|
||||
|
||||
// we got an object, and it has these three props then we are most likely dealing with a Block Editor.
|
||||
@@ -72,19 +90,19 @@
|
||||
|
||||
replaceUdisOfObject(value.layout, value);
|
||||
|
||||
// replace UDIs for inner properties of this Block Editors content data.
|
||||
// run resolvers for inner properties of this Blocks content data.
|
||||
if(value.contentData.length > 0) {
|
||||
value.contentData.forEach((item) => {
|
||||
for (var k in item) {
|
||||
propClearingMethod(item[k], clipboardService.TYPES.RAW);
|
||||
propPasteResolverMethod(item[k], clipboardService.TYPES.RAW);
|
||||
}
|
||||
});
|
||||
}
|
||||
// replace UDIs for inner properties of this Block Editors settings data.
|
||||
// run resolvers for inner properties of this Blocks settings data.
|
||||
if(value.settingsData.length > 0) {
|
||||
value.settingsData.forEach((item) => {
|
||||
for (var k in item) {
|
||||
propClearingMethod(item[k], clipboardService.TYPES.RAW);
|
||||
propPasteResolverMethod(item[k], clipboardService.TYPES.RAW);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -93,7 +111,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
clipboardService.registerPastePropertyResolver(replaceRawBlockListUDIsResolver, clipboardService.TYPES.RAW);
|
||||
clipboardService.registerPastePropertyResolver(rawBlockResolver, clipboardService.TYPES.RAW);
|
||||
|
||||
|
||||
function provideNewUdisForBlockResolver(block, propPasteResolverMethod) {
|
||||
|
||||
if(block.layout) {
|
||||
// We do not support layout child blocks currently, these should be stripped out as we only will be copying a single entry.
|
||||
removeBlockReferences(block.layout);
|
||||
}
|
||||
|
||||
if(block.data) {
|
||||
// Make new UDI for content-element
|
||||
block.data.udi = block.layout.contentUdi = udiService.create("element");
|
||||
}
|
||||
|
||||
if(block.settingsData) {
|
||||
// Make new UDI for settings-element
|
||||
block.settingsData.udi = block.layout.settingsUdi = udiService.create("element");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
clipboardService.registerPastePropertyResolver(provideNewUdisForBlockResolver, clipboardService.TYPES.BLOCK);
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService) {
|
||||
function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService, notificationsService) {
|
||||
|
||||
/**
|
||||
* Simple mapping from property model content entry to editing model,
|
||||
@@ -773,6 +773,57 @@
|
||||
|
||||
return layoutEntry;
|
||||
|
||||
},
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name createFromBlockData
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Insert data from raw models
|
||||
* @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration.
|
||||
*/
|
||||
createFromBlockData: function (blockData) {
|
||||
|
||||
blockData = clipboardService.parseContentForPaste(blockData, clipboardService.TYPES.BLOCK);
|
||||
|
||||
// As the blockData is a cloned object we can use its layout part for our layout entry.
|
||||
var layoutEntry = blockData.layout;
|
||||
if (layoutEntry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var blockConfiguration;
|
||||
|
||||
if (blockData.data) {
|
||||
// Ensure that we support the alias:
|
||||
blockConfiguration = this.getBlockConfiguration(blockData.data.contentTypeKey);
|
||||
if(blockConfiguration === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.value.contentData.push(blockData.data);
|
||||
} else {
|
||||
// We do not have data, this cannot be succesful paste.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (blockData.settingsData) {
|
||||
// Ensure that we support the alias:
|
||||
if(blockConfiguration.settingsElementTypeKey) {
|
||||
// If we have settings for this Block Configuration, we need to check that they align, if we dont we do not want to fail.
|
||||
if(blockConfiguration.settingsElementTypeKey === blockData.settingsData.contentTypeKey) {
|
||||
this.value.settingsData.push(blockData.settingsData);
|
||||
} else {
|
||||
notificationsService.error("Clipboard", "Couldn't paste because settings-data is not compatible.");
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// We do not have settings currently, so lets get rid of the settings part and move on with the paste.
|
||||
delete layoutEntry.settingUdi;
|
||||
}
|
||||
}
|
||||
|
||||
return layoutEntry;
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,33 +10,67 @@
|
||||
* The service has a set way for defining a data-set by a entryType and alias, which later will be used to retrive the posible entries for a paste scenario.
|
||||
*
|
||||
*/
|
||||
function clipboardService(notificationsService, eventsService, localStorageService, iconHelper) {
|
||||
function clipboardService($window, notificationsService, eventsService, localStorageService, iconHelper) {
|
||||
|
||||
|
||||
const TYPES = {};
|
||||
TYPES.ELEMENT_TYPE = "elementType";
|
||||
TYPES.BLOCK = "block";
|
||||
TYPES.RAW = "raw";
|
||||
|
||||
var clearPropertyResolvers = {};
|
||||
var pastePropertyResolvers = {};
|
||||
var clipboardTypeResolvers = {};
|
||||
|
||||
clipboardTypeResolvers[TYPES.ELEMENT_TYPE] = function(data, propMethod) {
|
||||
for (var t = 0; t < data.variants[0].tabs.length; t++) {
|
||||
var tab = data.variants[0].tabs[t];
|
||||
clipboardTypeResolvers[TYPES.ELEMENT_TYPE] = function(element, propMethod) {
|
||||
for (var t = 0; t < element.variants[0].tabs.length; t++) {
|
||||
var tab = element.variants[0].tabs[t];
|
||||
for (var p = 0; p < tab.properties.length; p++) {
|
||||
var prop = tab.properties[p];
|
||||
propMethod(prop, TYPES.ELEMENT_TYPE);
|
||||
}
|
||||
}
|
||||
}
|
||||
clipboardTypeResolvers[TYPES.BLOCK] = function (block, propMethod) {
|
||||
|
||||
propMethod(block, TYPES.BLOCK);
|
||||
|
||||
if(block.data) {
|
||||
Object.keys(block.data).forEach( key => {
|
||||
if(key === 'udi' || key === 'contentTypeKey') {
|
||||
return;
|
||||
}
|
||||
propMethod(block.data[key], TYPES.RAW);
|
||||
});
|
||||
}
|
||||
|
||||
if(block.settingsData) {
|
||||
Object.keys(block.settingsData).forEach( key => {
|
||||
if(key === 'udi' || key === 'contentTypeKey') {
|
||||
return;
|
||||
}
|
||||
propMethod(block.settingsData[key], TYPES.RAW);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
// Concept for supporting Block that contains other Blocks.
|
||||
// Missing clarifications:
|
||||
// How do we ensure that the inner blocks of a block is supported in the new scenario. Not that likely but still relevant, so considerations should be made.
|
||||
if(block.references) {
|
||||
// A Block clipboard entry can contain other Block Clipboard Entries, here we will make sure to resolve those identical to the main entry.
|
||||
for (var r = 0; r < block.references.length; r++) {
|
||||
clipboardTypeResolvers[TYPES.BLOCK](block.references[r], propMethod);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
clipboardTypeResolvers[TYPES.RAW] = function(data, propMethod) {
|
||||
for (var p = 0; p < data.length; p++) {
|
||||
propMethod(data[p], TYPES.RAW);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var STORAGE_KEY = "umbClipboardService";
|
||||
|
||||
var retriveStorage = function() {
|
||||
@@ -64,7 +98,10 @@ function clipboardService(notificationsService, eventsService, localStorageServi
|
||||
var storageString = JSON.stringify(storage);
|
||||
|
||||
try {
|
||||
// Check that we can parse the JSON:
|
||||
var storageJSON = JSON.parse(storageString);
|
||||
|
||||
// Store the string:
|
||||
localStorageService.set(STORAGE_KEY, storageString);
|
||||
|
||||
eventsService.emit("clipboardService.storageUpdate");
|
||||
@@ -82,11 +119,11 @@ function clipboardService(notificationsService, eventsService, localStorageServi
|
||||
|
||||
type = type || "raw";
|
||||
var resolvers = clearPropertyResolvers[type];
|
||||
|
||||
for (var i=0; i<resolvers.length; i++) {
|
||||
resolvers[i](prop, resolvePropertyForStorage);
|
||||
if (resolvers) {
|
||||
for (var i=0; i<resolvers.length; i++) {
|
||||
resolvers[i](prop, resolvePropertyForStorage);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var prepareEntryForStorage = function(type, entryData, firstLevelClearupMethod) {
|
||||
@@ -123,9 +160,10 @@ function clipboardService(notificationsService, eventsService, localStorageServi
|
||||
|
||||
type = type || "raw";
|
||||
var resolvers = pastePropertyResolvers[type];
|
||||
|
||||
for (var i=0; i<resolvers.length; i++) {
|
||||
resolvers[i](prop, resolvePropertyForPaste);
|
||||
if (resolvers) {
|
||||
for (var i=0; i<resolvers.length; i++) {
|
||||
resolvers[i](prop, resolvePropertyForPaste);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +192,7 @@ function clipboardService(notificationsService, eventsService, localStorageServi
|
||||
var cloneData = Utilities.copy(pasteEntryData);
|
||||
|
||||
var typeResolver = clipboardTypeResolvers[type];
|
||||
if(typeResolver) {
|
||||
if (typeResolver) {
|
||||
typeResolver(cloneData, resolvePropertyForPaste);
|
||||
} else {
|
||||
console.warn("Umbraco.service.clipboardService has no type resolver for '" + type + "'.");
|
||||
@@ -427,10 +465,17 @@ function clipboardService(notificationsService, eventsService, localStorageServi
|
||||
};
|
||||
|
||||
|
||||
var emitClipboardStorageUpdate = _.debounce(function(e) {
|
||||
eventsService.emit("clipboardService.storageUpdate");
|
||||
}, 1000);
|
||||
|
||||
// Fires if LocalStorage was changed from another tab than this one.
|
||||
$window.addEventListener("storage", emitClipboardStorageUpdate);
|
||||
|
||||
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
|
||||
angular.module("umbraco.services").factory("clipboardService", clipboardService);
|
||||
|
||||
|
||||
@@ -430,12 +430,12 @@
|
||||
if (Array.isArray(item.pasteData)) {
|
||||
var indexIncrementor = 0;
|
||||
item.pasteData.forEach(function (entry) {
|
||||
if (requestPasteFromClipboard(createIndex + indexIncrementor, entry)) {
|
||||
if (requestPasteFromClipboard(createIndex + indexIncrementor, entry, item.type)) {
|
||||
indexIncrementor++;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
requestPasteFromClipboard(createIndex, item.pasteData);
|
||||
requestPasteFromClipboard(createIndex, item.pasteData, item.type);
|
||||
}
|
||||
if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) {
|
||||
blockPickerModel.close();
|
||||
@@ -470,6 +470,7 @@
|
||||
|
||||
blockPickerModel.clickClearClipboard = function ($event) {
|
||||
clipboardService.clearEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases);
|
||||
clipboardService.clearEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases);
|
||||
};
|
||||
|
||||
blockPickerModel.clipboardItems = [];
|
||||
@@ -485,10 +486,28 @@
|
||||
icon: entry.icon
|
||||
}
|
||||
}
|
||||
if(Array.isArray(pasteEntry.data) === false) {
|
||||
pasteEntry.blockConfigModel = modelObject.getScaffoldFromAlias(entry.alias);
|
||||
} else {
|
||||
pasteEntry.blockConfigModel = {};
|
||||
if(Array.isArray(entry.data) === false) {
|
||||
var scaffold = modelObject.getScaffoldFromAlias(entry.alias);
|
||||
if(scaffold) {
|
||||
pasteEntry.blockConfigModel = modelObject.getBlockConfiguration(scaffold.contentTypeKey);
|
||||
}
|
||||
}
|
||||
blockPickerModel.clipboardItems.push(pasteEntry);
|
||||
});
|
||||
|
||||
var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases);
|
||||
entriesForPaste.forEach(function (entry) {
|
||||
var pasteEntry = {
|
||||
type: clipboardService.TYPES.BLOCK,
|
||||
date: entry.date,
|
||||
pasteData: entry.data,
|
||||
elementTypeModel: {
|
||||
name: entry.label,
|
||||
icon: entry.icon
|
||||
}
|
||||
}
|
||||
if(Array.isArray(entry.data) === false) {
|
||||
pasteEntry.blockConfigModel = modelObject.getBlockConfiguration(entry.data.data.contentTypeKey);
|
||||
}
|
||||
blockPickerModel.clipboardItems.push(pasteEntry);
|
||||
});
|
||||
@@ -504,42 +523,67 @@
|
||||
|
||||
var requestCopyAllBlocks = function() {
|
||||
|
||||
var elementTypesToCopy = vm.layout.filter(entry => entry.$block.config.unsupported !== true).map(entry => entry.$block.content);
|
||||
var aliases = [];
|
||||
|
||||
// list aliases
|
||||
var aliases = elementTypesToCopy.map(content => content.contentTypeAlias);
|
||||
var elementTypesToCopy = vm.layout.filter(entry => entry.$block.config.unsupported !== true).map(
|
||||
(entry) => {
|
||||
|
||||
// remove dublicates
|
||||
aliases.push(entry.$block.content.contentTypeAlias);
|
||||
|
||||
// No need to clone the data as its begin handled by the clipboardService.
|
||||
return {"layout": entry.$block.layout, "data": entry.$block.data, "settingsData":entry.$block.settingsData}
|
||||
}
|
||||
);
|
||||
|
||||
// remove duplicate aliases
|
||||
aliases = aliases.filter((item, index) => aliases.indexOf(item) === index);
|
||||
|
||||
var contentNodeName = "";
|
||||
var contentNodeName = "?";
|
||||
var contentNodeIcon = null;
|
||||
if(vm.umbVariantContent) {
|
||||
contentNodeName = vm.umbVariantContent.editor.content.name;
|
||||
if(vm.umbVariantContentEditors) {
|
||||
contentNodeIcon = vm.umbVariantContentEditors.content.icon.split(" ")[0];
|
||||
} else if (vm.umbElementEditorContent) {
|
||||
contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0];
|
||||
}
|
||||
} else if (vm.umbElementEditorContent) {
|
||||
contentNodeName = vm.umbElementEditorContent.model.documentType.name
|
||||
contentNodeName = vm.umbElementEditorContent.model.documentType.name;
|
||||
contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0];
|
||||
}
|
||||
|
||||
localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) {
|
||||
clipboardService.copyArray(clipboardService.TYPES.ELEMENT_TYPE, aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id);
|
||||
clipboardService.copyArray(clipboardService.TYPES.BLOCK, aliases, elementTypesToCopy, localizedLabel, contentNodeIcon || "icon-thumbnail-list", vm.model.id);
|
||||
});
|
||||
}
|
||||
function copyBlock(block) {
|
||||
clipboardService.copy(clipboardService.TYPES.ELEMENT_TYPE, block.content.contentTypeAlias, block.content, block.label);
|
||||
clipboardService.copy(clipboardService.TYPES.BLOCK, block.content.contentTypeAlias, {"layout": block.layout, "data": block.data, "settingsData":block.settingsData}, block.label, block.content.icon, block.content.udi);
|
||||
}
|
||||
function requestPasteFromClipboard(index, pasteEntry) {
|
||||
function requestPasteFromClipboard(index, pasteEntry, pasteType) {
|
||||
|
||||
if (pasteEntry === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var layoutEntry = modelObject.createFromElementType(pasteEntry);
|
||||
var layoutEntry;
|
||||
if (pasteType === clipboardService.TYPES.ELEMENT_TYPE) {
|
||||
layoutEntry = modelObject.createFromElementType(pasteEntry);
|
||||
} else if (pasteType === clipboardService.TYPES.BLOCK) {
|
||||
layoutEntry = modelObject.createFromBlockData(pasteEntry);
|
||||
} else {
|
||||
// Not a supported paste type.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layoutEntry === null) {
|
||||
// Pasting did not go well.
|
||||
return false;
|
||||
}
|
||||
|
||||
// make block model
|
||||
var blockObject = getBlockObject(layoutEntry);
|
||||
if (blockObject === null) {
|
||||
// Initalization of the Block Object didnt go well, therefor we will fail the paste action.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -554,6 +598,7 @@
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
function requestDeleteBlock(block) {
|
||||
localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "contentTypeEditor_yesDelete"]).then(function (data) {
|
||||
const overlay = {
|
||||
|
||||
Reference in New Issue
Block a user