Merge pull request #9258 from umbraco/v8/feature/AB8699-Support-blocks-in-clipboard-service

Support blocks in backoffice clipboard
This commit is contained in:
Warren Buckley
2020-11-03 13:48:07 +00:00
committed by GitHub
4 changed files with 221 additions and 40 deletions

View File

@@ -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);
}]);

View File

@@ -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;
},
/**

View File

@@ -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);

View File

@@ -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 = {