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 c3aaf1a5f5..955e026da5 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 @@ -28,13 +28,20 @@ * *
  * 
+ *     // We must get a scope that exists in all the lifetime of this data. Across variants and split-view.
+ *     var scopeOfExistence = $scope;
+ *     // Setup your component to require umbVariantContentEditors and use the method getScope to retrive a shared scope for multiple editors of this content.
+ *     if(vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) {
+ *         scopeOfExistence = vm.umbVariantContentEditors.getScope();
+ *     }
+ * 
  *     // Define variables for layout and modelObject as you will be using these through our your property-editor.
  *     var layout;
  *     var modelObject;
  *     
  *     // When we are ready we can instantiate the Model Object can load the dependencies of it.
  *     vm.$onInit = function() {
- *         modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope);
+ *         modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence);
  *         modelObject.load().then(onLoaded);
  *     }
  * 
@@ -171,13 +178,14 @@
 
     function blockEditorModelObjectFactory($interpolate, udiService, contentResource) {
 
-
         /**
          * Simple mapping from property model content entry to editing model,
          * needs to stay simple to avoid deep watching.
          */
         function mapToElementModel(elementModel, dataModel) {
 
+            if (!elementModel || !elementModel.variants || !elementModel.variants.length) { return; }
+
             var variant = elementModel.variants[0];
             
             for (var t = 0; t < variant.tabs.length; t++) {
@@ -190,6 +198,7 @@
                     }
                 }
             }
+            
         }
 
         /**
@@ -198,6 +207,8 @@
          */
         function mapToPropertyModel(elementModel, dataModel) {
             
+            if (!elementModel || !elementModel.variants || !elementModel.variants.length) { return; }
+
             var variant = elementModel.variants[0];
             
             for (var t = 0; t < variant.tabs.length; t++) {
@@ -210,6 +221,7 @@
                     }
                 }
             }
+            
         }
 
         /**
@@ -272,7 +284,7 @@
             // Start watching each property value.
             var variant = model.variants[0];
             var field = forSettings ? "settings" : "content";
-            var watcherCreator = forSettings ? createSettingsModelPropWatcher : createDataModelPropWatcher;
+            var watcherCreator = forSettings ? createSettingsModelPropWatcher : createContentModelPropWatcher;
             for (var t = 0; t < variant.tabs.length; t++) {
                 var tab = variant.tabs[t];
                 for (var p = 0; p < tab.properties.length; p++) {
@@ -283,6 +295,13 @@
                     // But we like to sync non-primative values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. 
                     // Non-primative values act as references to the same data and are therefor synced.
                     blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockObject, prop)));
+                    
+                    // We also like to watch our data model to be able to capture changes coming from other places.
+                    if (forSettings === true) {
+                        blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "layout.settings" + "." + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop)));
+                    } else {
+                        blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "data" + "." + prop.alias, createDataModelWatcher(blockObject, prop)));
+                    }
                 }
             }
             if (blockObject.watchers.length === 0) {
@@ -291,10 +310,31 @@
             }
         }
 
+        /**
+         * Used to create a prop watcher for the data in the property editor data model.
+         */
+        function createDataModelWatcher(blockObject, prop)  {
+            return function() {
+                // sync data:
+                prop.value = blockObject.data[prop.alias];
+
+                blockObject.updateLabel();
+            }
+        }
+        /**
+         * Used to create a prop watcher for the settings in the property editor data model.
+         */
+        function createLayoutSettingsModelWatcher(blockObject, prop)  {
+            return function() {
+                // sync data:
+                prop.value = blockObject.layout.settings[prop.alias];
+            }
+        }
+
         /**
          * Used to create a scoped watcher for a content property on a blockObject.
          */
-        function createDataModelPropWatcher(blockObject, prop)  {
+        function createContentModelPropWatcher(blockObject, prop)  {
             return function() {
                 // sync data:
                 blockObject.data[prop.alias] = prop.value;
@@ -482,8 +522,9 @@
              * @ngdoc method
              * @name getBlockObject
              * @methodOf umbraco.services.blockEditorModelObject
-             * @description Retrieve editor friendly model of a block.
-             * BlockObject is a class instance which setups live syncronization of content and settings models back to the data of your property editor model.
+             * @description Retrieve a Block Object for the given layout entry.
+             * The Block Object offers the nesecary data to display and edit a block.
+             * The Block Object setups live syncronization of content and settings models back to the data of your Property Editor model.
              * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen.
              * The ´BlockObject´ contains the following properties:
              * - key {string}: runtime generated key, usefull for tracking of this object
@@ -509,19 +550,32 @@
                 }
 
                 var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey);
+                var contentScaffold;
 
                 if (blockConfiguration === null) {
-                    console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeKey is not allowed for this PropertyEditor")
-                    // This is not an allowed block type, therefor we return null;
-                    return null;
+                    console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeKey is not allowed for this PropertyEditor");
+                } else {
+                    var contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
+                    if(contentScaffold === null) {
+                        console.error("The block entry of "+udi+" is not begin initialized cause its Element Type was not loaded.");
+                    }
                 }
 
-                var contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
-                if(contentScaffold === null) {
-                    return null;
+                if (blockConfiguration === null || contentScaffold === null) {
+
+                    blockConfiguration = {
+                        label: "Unsupported Block ("+udi+")",
+                        unsupported: true
+                    };
+                    contentScaffold = {};
+                    
                 }
 
                 var blockObject = {};
+                // Set an angularJS cloneNode method, to avoid this object begin cloned.
+                blockObject.cloneNode = function() {
+                    return null;// angularJS accept this as a cloned value as long as the 
+                }
                 blockObject.key = String.CreateGuid().replace(/-/g, "");
                 blockObject.config = Utilities.copy(blockConfiguration);
                 if (blockObject.config.label && blockObject.config.label !== "") {
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html
index e3afba8cab..64ae99eeb5 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html
@@ -4,9 +4,9 @@
 
     
-
+
-
+
-
+
- + - +
- - - - - + +
- Minimum %0% entries, needs %1% more. + Minimum %0% entries, needs %1% more.
-
+
- Maximum %0% entries, %1% too many. + Maximum %0% entries, %1% too many.
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 da62d10c87..d57b3a9012 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 @@ -22,7 +22,8 @@ }, require: { umbProperty: "?^umbProperty", - umbVariantContent: '?^^umbVariantContent' + umbVariantContent: '?^^umbVariantContent', + umbVariantContentEditors: '?^^umbVariantContentEditors' } }); @@ -49,10 +50,9 @@ vm.currentBlockInFocus = block; block.focus = true; } - vm.showCopy = clipboardService.isSupported(); + vm.supportCopy = clipboardService.isSupported(); - var layout = [];// The layout object specific to this Block Editor, will be a direct reference from Property Model. - vm.blocks = [];// Runtime list of block models, needs to be synced to property model on form submit. + vm.layout = [];// The layout object specific to this Block Editor, will be a direct reference from Property Model. vm.availableBlockTypes = [];// Available block entries of this property editor. var labels = {}; @@ -82,9 +82,14 @@ if(typeof vm.model.value !== 'object' || vm.model.value === null) {// testing if we have null or undefined value or if the value is set to another type than Object. vm.model.value = {}; } + + var scopeOfExistence = $scope; + if(vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) { + scopeOfExistence = vm.umbVariantContentEditors.getScope(); + } // Create Model Object, to manage our data for this Block Editor. - modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope); + modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence); modelObject.load().then(onLoaded); copyAllBlocksAction = { @@ -124,15 +129,20 @@ function onLoaded() { // Store a reference to the layout model, because we need to maintain this model. - layout = modelObject.getLayout([]); + vm.layout = modelObject.getLayout([]); - // maps layout entries to editor friendly models aka. blockObjects. - layout.forEach(entry => { - var block = getBlockObject(entry); - - // If this entry was not supported by our property-editor it would return 'null'. - if(block !== null) { - vm.blocks.push(block); + // Append the blockObjects to our layout. + vm.layout.forEach(entry => { + if (entry.$block === undefined || entry.$block === null) { + console.log("We are creating a BlockObject for", entry.udi); + var block = getBlockObject(entry); + + // If this entry was not supported by our property-editor it would return 'null'. + if(block !== null) { + entry.$block = block; + } else { + entry.$block = blockEditorService.UNSUPPORTED_BLOCKOBJECT; + } } }); @@ -145,16 +155,25 @@ } + function getDefaultViewForBlock(block) { + + if (block.config.unsupported === true) + return "views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html"; + + if (inlineEditing === true) + return "views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html"; + return "views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html"; + } function getBlockObject(entry) { var block = modelObject.getBlockObject(entry); if (block === null) return null; - // Lets apply fallback views, and make the view available directly on the blockObject. - block.view = (block.config.view ? "/" + block.config.view : (inlineEditing ? "views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html" : "views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html")); + block.view = (block.config.view ? "/" + block.config.view : getDefaultViewForBlock(block)); block.showSettings = block.config.settingsElementTypeKey != null; + block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it dosnt make sense to copy. return block; } @@ -176,11 +195,11 @@ // If we reach this line, we are good to add the layoutEntry and blockObject to our models. - // add layout entry at the decired location in layout. - layout.splice(index, 0, layoutEntry); + // Add the Block Object to our layout entry. + layoutEntry.$block = blockObject; - // apply block model at decired location in blocks. - vm.blocks.splice(index, 0, blockObject); + // add layout entry at the decired location in layout. + vm.layout.splice(index, 0, layoutEntry); // lets move focus to this new block. vm.setBlockFocus(blockObject); @@ -193,24 +212,19 @@ function deleteBlock(block) { - var index = vm.blocks.indexOf(block); - if(index !== -1) { - - var layoutIndex = layout.findIndex(entry => entry.udi === block.content.udi); - if(layoutIndex !== -1) { - layout.splice(index, 1); - } else { - throw new Error("Could not find layout entry of block with udi: "+block.content.udi) - } - - vm.blocks.splice(index, 1); - - modelObject.removeDataAndDestroyModel(block); + var layoutIndex = vm.layout.findIndex(entry => entry.udi === block.content.udi); + if(layoutIndex === -1) { + throw new Error("Could not find layout entry of block with udi: "+block.content.udi) } + vm.layout.splice(layoutIndex, 1); + modelObject.removeDataAndDestroyModel(block); + } function deleteAllBlocks() { - vm.blocks.forEach(deleteBlock); + vm.layout.forEach(entry => { + deleteBlock(entry.$block); + }); } function editBlock(blockObject, openSettings) { @@ -293,15 +307,15 @@ if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { editorService.close(); - if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.blocks.length > createIndex) { - editBlock(vm.blocks[createIndex]); + if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.layout.length > createIndex) { + editBlock(vm.layout[createIndex].$block); } } }, close: function() { // if opned by a inline creator button(index less than length), we want to move the focus away, to hide line-creator. - if (createIndex < vm.blocks.length) { - vm.setBlockFocus(vm.blocks[Math.max(createIndex-1, 0)]); + if (createIndex < vm.layout.length) { + vm.setBlockFocus(vm.layout[Math.max(createIndex-1, 0)].$block); } editorService.close(); @@ -357,7 +371,7 @@ var requestCopyAllBlocks = function() { // list aliases - var aliases = vm.blocks.map(block => block.content.contentTypeAlias); + var aliases = vm.layout.map(entry => entry.$block.content.contentTypeAlias); // remove dublicates aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); @@ -366,8 +380,9 @@ if(vm.umbVariantContent) { contentNodeName = vm.umbVariantContent.editor.content.name; } + // TODO: check if we are in an overlay and then lets get the Label of this block. - var elementTypesToCopy = vm.blocks.map(block => block.content); + var elementTypesToCopy = vm.layout.map(entry => entry.$block.content); localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) { clipboardService.copyArray("elementTypeArray", aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id); @@ -392,13 +407,13 @@ if (blockObject === null) { return false; } + + // set the BlockObject on our layout entry. + layoutEntry.$block = blockObject; // insert layout entry at the decired location in layout. - layout.splice(index, 0, layoutEntry); + vm.layout.splice(index, 0, layoutEntry); - // insert block model at the decired location in blocks. - vm.blocks.splice(index, 0, blockObject); - vm.currentBlockInFocus = blockObject; return true; @@ -452,11 +467,6 @@ openSettingsForBlock: openSettingsForBlock } - - - var runtimeSortVars = {}; - - vm.sorting = false; vm.sortableOptions = { axis: "y", cursor: "grabbing", @@ -466,47 +476,25 @@ distance: 5, tolerance: "pointer", scroll: true, - start: function (ev, ui) { - runtimeSortVars.moveFromIndex = ui.item.index(); - $scope.$evalAsync(function () { - vm.sorting = true; - }); - }, update: function (ev, ui) { setDirty(); }, - stop: function (ev, ui) { - - // Lets update the layout part of the property model to match the update. - var moveFromIndex = runtimeSortVars.moveFromIndex; - var moveToIndex = ui.item.index(); - - if (moveToIndex !== -1 && moveFromIndex !== moveToIndex) { - var movedEntry = layout[moveFromIndex]; - layout.splice(moveFromIndex, 1); - layout.splice(moveToIndex, 0, movedEntry); - } - - $scope.$evalAsync(function () { - vm.sorting = false; - }); - } }; function onAmountOfBlocksChanged() { // enable/disable property actions - copyAllBlocksAction.isDisabled = vm.blocks.length === 0; - deleteAllBlocksAction.isDisabled = vm.blocks.length === 0; + copyAllBlocksAction.isDisabled = vm.layout.length === 0; + deleteAllBlocksAction.isDisabled = vm.layout.length === 0; // validate limits: if (vm.propertyForm) { - var isMinRequirementGood = vm.validationLimit.min === null || vm.blocks.length >= vm.validationLimit.min; + var isMinRequirementGood = vm.validationLimit.min === null || vm.layout.length >= vm.validationLimit.min; vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood); - var isMaxRequirementGood = vm.validationLimit.max === null || vm.blocks.length <= vm.validationLimit.max; + var isMaxRequirementGood = vm.validationLimit.max === null || vm.layout.length <= vm.validationLimit.max; vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood); } @@ -515,7 +503,7 @@ - unsubscribe.push($scope.$watch(() => vm.blocks.length, onAmountOfBlocksChanged)); + unsubscribe.push($scope.$watch(() => vm.layout.length, onAmountOfBlocksChanged)); $scope.$on("$destroy", function () { for (const subscription of unsubscribe) {