From 8ffae1ce31befedda4d13025e5f7964a12943f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Feb 2020 13:02:22 +0100 Subject: [PATCH] implement block list editor --- .../common/services/blockeditor.service.js | 227 ++++++++++++++++++ .../src/common/services/udi.service.js | 32 +++ .../inlineblock/inlineblock.editor.html | 2 +- .../labelblock/labelblock.editor.html | 2 +- .../textareablock.editor.controller.js | 2 +- .../elementContentEditor.component.js | 2 +- .../blocklist/blocklist.component.js | 74 +++--- .../blocklist.elementtypepicker.controller.js | 10 +- .../prevalue/blocklist.elementtypepicker.html | 6 +- .../PropertyEditors/BlockListConfiguration.cs | 6 +- 10 files changed, 321 insertions(+), 42 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/udi.service.js 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 new file mode 100644 index 0000000000..8846dc7d3a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -0,0 +1,227 @@ +(function () { + 'use strict'; + + + function blockEditorService($interpolate, udiService) { + + + function applyModelToScaffold(scaffold, contentModel) { + + scaffold.key = contentModel.key; + + var variant = scaffold.variants[0]; + + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (contentModel[prop.propertyAlias]) { + prop.value = contentModel[prop.propertyAlias]; + } + } + } + } + + + /** + * @ngdoc factory + * @name umbraco.factory.BlockEditorModelObject + * @description A model object used to handle Block Editor data. + **/ + function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations) { + + if (!propertyModelValue) { + throw new Error("propertyModelValue cannot be undefined, to ensure we keep the binding to the angular model we need minimum an empty object."); + } + + // ensure basic part of data-structure is in place: + this.value = propertyModelValue; + this.value.layout = this.value.layout || []; + this.value.data = this.value.data || []; + + this.propertyEditorAlias = propertyEditorAlias; + this.blockConfigurations = blockConfigurations; + + this.scaffolds = []; + + }; + + BlockEditorModelObject.prototype = { + + getBlockConfiguration: function(alias) { + return this.blockConfigurations.find(blockConfiguration => blockConfiguration.contentTypeAlias === alias); + }, + + loadScaffolds: function(contentResource) { + var tasks = []; + + var scaffoldAliases = []; + + this.blockConfigurations.forEach(blockConfiguration => { + scaffoldAliases.push(blockConfiguration.contentTypeAlias); + if (blockConfiguration.settingsElementTypeAlias != null) { + scaffoldAliases.push(elementType.settingsElementTypeAlias); + } + }); + + // remove dublicates. + scaffoldAliases = scaffoldAliases.filter((value, index, self) => self.indexOf(value) === index); + + scaffoldAliases.forEach((elementTypeAlias => { + tasks.push(contentResource.getScaffold(-20, elementTypeAlias).then(scaffold => { + console.log(scaffold); + this.scaffolds.push(scaffold); + })); + })); + + return Promise.all(tasks); + }, + + getAvailableBlocksForItemPicker: function() { + + var blocks = []; + + this.blockConfigurations.forEach(blockConfiguration => { + + var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); + + blocks.push({ + alias: scaffold.contentTypeAlias, + name: scaffold.contentTypeName, + icon: scaffold.icon + }); + }); + + return blocks; + }, + + getScaffoldFor: function(contentTypeAlias, data) { + return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias); + }, + + /** + * Retrieve editing model of a layout entry + * @return {Object} Scaffolded Block Content object. + */ + getEditingModel: function(layoutEntry) { + + var contentModel = this.getContentByUdi(layoutEntry.udi); + + var blockConfiguration = this.getBlockConfiguration(contentModel.contentTypeAlias); + + // TODO: make blockConfiguration the base for model, remeber to make a copy. + var model = { + label: "", + labelInterpolate: $interpolate(blockConfiguration.label), + editor: blockConfiguration.view, + overlaySize: "medium" + }; + + var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); + if(scaffold === null) { + return null; + } + + model.content = angular.copy(scaffold); + applyModelToScaffold(model.content, contentModel); + + // TODO: settings + + return model; + + }, + + /** + * Retrieve layout data + * @return layout object. + */ + getLayout: function() { + if (!this.value.layout[this.propertyEditorAlias]) { + this.value.layout[this.propertyEditorAlias] = []; + } + return this.value.layout[this.propertyEditorAlias]; + }, + + /** + * Create layout entry + * @param {object} blockConfiguration + * @return layout entry, to be added in the layout. + */ + createLayoutEntry: function(contentTypeAlias) { + + var blockConfiguration = this.getBlockConfiguration(contentTypeAlias); + + var entry = { + udi: this.createContent(contentTypeAlias) + } + + if (blockConfiguration.settingsElementTypeAlias != null) { + // TODO: Settings. + } + + return entry; + }, + + // You make entries in your layout your self. + + getContentByUdi: function(udi) { + return this.value.data.find(entry => entry.udi === udi); + }, + + createContent: function(elementTypeAlias) { + var content = { + contentTypeAlias: elementTypeAlias, + udi: udiService.create("element") + }; + this.value.data.push(content); + return content.udi; + }, + + removeContent: function(entry) { + const index = this.value.data.indexOf(entry) + if (index > -1) { + this.value.splice(index, 1); + } + }, + + removeContentByUdi: function(udi) { + const index = this.value.data.findIndex(o => o.udi === udi); + if (index > -1) { + this.value.splice(index, 1); + } + } + } + + return { + createModelObject: function(propertyModelValue, propertyEditorAlias, blockConfigurations) { + return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations); + }, + getBlockLabel: function(blockModelObject, labelIndex) { + + console.log("getBlockLabel", blockModelObject); + + // TODO: we should do something about this for performance. + + var vars = new Object(); + vars["$index"] = labelIndex; + + var variant = blockModelObject.content.variants[0]; + var tab = variant.tabs[0]; + // TODO: need to look up all tabs... + for(const property of tab.properties) { + vars[property.alias] = property.value; + } + + if(blockModelObject.labelInterpolate) { + return blockModelObject.labelInterpolate(vars); + } + + return blockModelObject.contentTypeName; + } + } + } + + angular.module('umbraco.services').service('blockEditorService', blockEditorService); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js new file mode 100644 index 0000000000..b7d0cd05e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js @@ -0,0 +1,32 @@ +(function () { + "use strict"; + + /** + * @ngdoc service + * @name umbraco.services.udiService + * @description A service for UDIs + **/ + function udiService() { + return { + + /** + * @ngdoc method + * @name umbraco.services.udiService#parse + * @methodOf umbraco.services.udiService + * @function + * + * @description + * Generates a Udi string. + * + * @param {string} entityType The entityType as a string. + * @returns {string} The generated UDI + */ + create: function(entityType) { + return "umb://" + entityType + "/" + String.CreateGuid(); + } + } + } + + angular.module("umbraco.services").factory("udiService", udiService); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html index ff555fc773..9561d28a9e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html @@ -1,7 +1,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html index bd8f9558fd..419a1fd8a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html @@ -1,4 +1,4 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js index caad1c83a7..868573fb72 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js @@ -5,7 +5,7 @@ angular.module("umbraco") var vm = this; - vm.firstProperty = $scope.block.content.tabs[0].properties[0]; + vm.firstProperty = $scope.block.content.variants[0].tabs[0].properties[0]; /* vm.onBlur = function() { if (vm.firstProperty.value === null || vm.firstProperty.value === "") { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js index 34cfed2ad9..637df93a72 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js @@ -4,7 +4,7 @@ angular .module('umbraco.directives') .component('umbElementContentEditor', { - templateUrl: 'views/common/infiniteeditors/elementeditor/elementeditor.component.html', + templateUrl: 'views/common/infiniteeditors/elementeditor/elementContentEditor.component.html', controller: ElementEditorComponentController, controllerAs: 'vm', bindings: { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index abfd738861..7aa03f9402 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -15,7 +15,7 @@ } }); - function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService) { + function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService, contentResource) { var unsubscribe = []; var vm = this; @@ -24,13 +24,15 @@ $scope.moveFocusToBlock = null; + console.log("model JSON:", JSON.stringify(model)); + console.log("model:", model); console.log("config:", model.config); vm.validationLimit = model.config.validationLimit; console.log("value:", model.value); - + /* vm.availableBlockTypes = [ { alias: "pageModule", @@ -856,7 +858,34 @@ ]; + */ + + model.value = model.value || {}; + + var modelObject = blockEditorService.createModelObject(model.value, model.editor, model.config.blocks); + + modelObject.loadScaffolds(contentResource).then(loaded); + + vm.layout = []; + vm.blocks = []; + vm.availableBlockTypes = []; + + function loaded() { + + console.log("Loading done!!!"); + console.log(modelObject); + + + vm.layout = modelObject.getLayout(); + vm.layout.forEach(entry => { + vm.blocks.push(modelObject.getEditingModel(entry)); + }); + + vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); + console.log(vm.availableBlockTypes); + + } function setDirty() { @@ -865,33 +894,23 @@ } }; - function addNewBlock(index, type) { + function addNewBlock(index, contentTypeAlias) { - var block = angular.copy(type.prototype_paste_data); + // Create layout entry. + var layoutEntry = modelObject.createLayoutEntry(contentTypeAlias); + // add layout entry a decired location in layout. + vm.layout.splice(index, 0, layoutEntry); - vm.blocks.splice(index, 0, block); - $scope.moveFocusToBlock = block; + // make editing object + var blockEditingObject = modelObject.getEditingModel(layoutEntry); + // apply editing model at decired location in editing model. + vm.blocks.splice(index, 0, blockEditingObject); + + $scope.moveFocusToBlock = blockEditingObject; } - function getBlockLabel(block) { - - // TODO: we should do something about this for performance. - - var props = new Object(); - - var tab = block.content.tabs[0]; - // TODO: need to look up all tabs... - for(const property of tab.properties) { - props[property.alias] = property.value; - } - - if(block.labelInterpolate) { - return block.labelInterpolate(props); - } - - return "block.label"; - } + vm.deleteBlock = function(block) { var index = vm.blocks.indexOf(block); @@ -942,7 +961,7 @@ availableItems: vm.availableBlockTypes, submit: function (model) { if (model && model.selectedItem) { - addNewBlock(createIndex, model.selectedItem); + addNewBlock(createIndex, model.selectedItem.alias); } vm.blockTypePicker.close(); }, @@ -1050,9 +1069,10 @@ // TODO: We need to investigate if we can do a specific watch on each block, so we dont re-render all blocks. unsubscribe.push($scope.$watch("vm.blocks", onBlocksUpdated, true)); - function onBlocksUpdated(newVal, oldVal){ + function onBlocksUpdated(newVal, oldVal) { + var labelIndex = 1; for(const block of vm.blocks) { - block.label = getBlockLabel(block); + block.label = blockEditorService.getBlockLabel(block, labelIndex++); } } unsubscribe.push($scope.$watch(() => vm.blocks.length, validateLimits)); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js index 970b42d2cb..77751579c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js @@ -47,7 +47,7 @@ vm.requestRemoveEntryByIndex = function (index) { localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) { - var contentElementType = vm.getElementTypeByAlias($scope.model.value[index].elementTypeAlias); + var contentElementType = vm.getElementTypeByAlias($scope.model.value[index].contentTypeAlias); overlayService.confirmDelete({ title: data[0], content: localizationService.tokenReplace(data[1], [contentElementType.name]), @@ -78,7 +78,7 @@ vm.getAvailableElementTypes = function () { return vm.elementTypes.filter(function (type) { return !$scope.model.value.find(function (entry) { - return type.alias === entry.elementTypeAlias; + return type.alias === entry.contentTypeAlias; }); }); }; @@ -91,9 +91,9 @@ vm.openAddDialog = function ($event, entry) { - //we have to add the alias to the objects (they are stored as elementTypeAlias) + //we have to add the alias to the objects (they are stored as contentTypeAlias) var selectedItems = _.each($scope.model.value, function (obj) { - obj.alias = obj.elementTypeAlias; + obj.alias = obj.contentTypeAlias; return obj; }); @@ -149,7 +149,7 @@ vm.addEntryFromElementTypeAlias = function(alias) { var entry = { - "elementTypeAlias": alias, + "contentTypeAlias": alias, "view": null, "labelTemplate": "", "settingsElementTypeAlias": null diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html index 9ca35778be..8184761a1a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html @@ -24,16 +24,16 @@
- {{ contentPreview = vm.getElementTypeByAlias(entry.elementTypeAlias); "" }} + {{ contentPreview = vm.getElementTypeByAlias(entry.contentTypeAlias); "" }}
-
- +
diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 6485bd061a..f069804348 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.PropertyEditors public class BlockConfiguration { // TODO: rename this to contentElementTypeAlias, I would like this to be specific, since we have the settings. - [JsonProperty("elementTypeAlias")] + [JsonProperty("contentTypeAlias")] public string Alias { get; set; } [JsonProperty("settingsElementTypeAlias")] @@ -38,8 +38,8 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("view")] public string View { get; set; } - [JsonProperty("labelTemplate")] - public string Template { get; set; } + [JsonProperty("label")] + public string Label { get; set; } } [ConfigurationField("useAccordionsAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view")]