diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js index 12e1144acc..122d430165 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -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); }]); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 77ed357c35..8a2b230a27 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -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; + }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 58ed07367e..83fd3d08c2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -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 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 = {