diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js index 8c23094bbf..b0ea8be9a3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js +++ b/src/Umbraco.Web.UI.Client/src/common/filters/nestedcontent.filter.js @@ -3,67 +3,78 @@ // Cache for node names so we don't make a ton of requests var ncNodeNameCache = { - id: "", - keys: {} + id: "", + keys: {} }; -angular.module("umbraco.filters").filter("ncNodeName", function (editorState, entityResource) { +angular.module("umbraco.filters").filter("ncNodeName", function (editorState, entityResource, udiParser) { - function formatLabel(firstNodeName, totalNodes) { - return totalNodes <= 1 - ? firstNodeName - // If there is more than one item selected, append the additional number of items selected to hint that - : firstNodeName + " (+" + (totalNodes - 1) + ")"; + function formatLabel(firstNodeName, totalNodes) { + return totalNodes <= 1 + ? firstNodeName + // If there is more than one item selected, append the additional number of items selected to hint that + : firstNodeName + " (+" + (totalNodes - 1) + ")"; + } + + nodeNameFilter.$stateful = true; + function nodeNameFilter(input) { + + // Check we have a value at all + if (typeof input === 'undefined' || input === "" || input.toString() === "0" || input === null) { + return ""; } - return function (input) { + var currentNode = editorState.getCurrent(); - // Check we have a value at all - if (input === "" || input.toString() === "0") { - return ""; - } + // Ensure a unique cache per editor instance + var key = "ncNodeName_" + currentNode.key; + if (ncNodeNameCache.id !== key) { + ncNodeNameCache.id = key; + ncNodeNameCache.keys = {}; + } - var currentNode = editorState.getCurrent(); + // MNTP values are comma separated IDs. We'll only fetch the first one for the NC header. + var ids = input.split(','); + var lookupId = ids[0]; + var serviceInvoked = false; - // Ensure a unique cache per editor instance - var key = "ncNodeName_" + currentNode.key; - if (ncNodeNameCache.id !== key) { - ncNodeNameCache.id = key; - ncNodeNameCache.keys = {}; - } + // See if there is a value in the cache and use that + if (ncNodeNameCache.keys[lookupId]) { + return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); + } - // MNTP values are comma separated IDs. We'll only fetch the first one for the NC header. - var ids = input.split(','); - var lookupId = ids[0]; + // No value, so go fetch one + // We'll put a temp value in the cache though so we don't + // make a load of requests while we wait for a response + ncNodeNameCache.keys[lookupId] = "Loading..."; - // See if there is a value in the cache and use that - if (ncNodeNameCache.keys[lookupId]) { - return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); - } + // If the service has already been invoked, don't do it again + if (serviceInvoked) { + return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); + } - // No value, so go fetch one - // We'll put a temp value in the cache though so we don't - // make a load of requests while we wait for a response - ncNodeNameCache.keys[lookupId] = "Loading..."; + serviceInvoked = true; - var type = lookupId.indexOf("umb://media/") === 0 - ? "Media" - : lookupId.indexOf("umb://member/") === 0 - ? "Member" - : "Document"; - entityResource.getById(lookupId, type) - .then( - function (ent) { - ncNodeNameCache.keys[lookupId] = ent.name; - } - ); + var udi = udiParser.parse(lookupId); - // Return the current value for now - return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); - }; + if (udi) { + entityResource.getById(udi.value, udi.entityType).then(function (ent) { + ncNodeNameCache.keys[lookupId] = ent.name; + }).catch(function () { + ncNodeNameCache.keys[lookupId] = "Error: Could not load"; + }); + } else { + ncNodeNameCache.keys[lookupId] = "Error: Not a UDI"; + } + + // Return the current value for now + return formatLabel(ncNodeNameCache.keys[lookupId], ids.length); + } + + return nodeNameFilter; }).filter("ncRichText", function () { - return function(input) { - return $("
").html(input).text(); - }; + return function (input) { + return $("
").html(input).text(); + }; }); 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 24432ca261..08c2f93001 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, notificationsService) { + function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService, notificationsService, $compile) { /** * Simple mapping from property model content entry to editing model, @@ -62,8 +62,8 @@ /** * Map property values from an ElementModel to another ElementModel. * Used to tricker watchers for synchronization. - * @param {Object} fromModel ElementModel to recive property values from. - * @param {Object} toModel ElementModel to recive property values from. + * @param {Object} fromModel ElementModel to receive property values from. + * @param {Object} toModel ElementModel to receive property values from. */ function mapElementValues(fromModel, toModel) { if (!fromModel || !fromModel.variants) { @@ -97,40 +97,6 @@ } } - /** - * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. - * @param {Object} blockObject BlockObject to receive data values from. - */ - function getBlockLabel(blockObject) { - if (blockObject.labelInterpolator !== undefined) { - // blockobject.content may be null if the block is no longer allowed, - // so try and fall back to the label in the config, - // if that too is null, there's not much we can do, so just default to empty string. - var contentTypeName; - if(blockObject.content != null){ - contentTypeName = blockObject.content.contentTypeName; - } - else if(blockObject.config != null && blockObject.config.label != null){ - contentTypeName = blockObject.config.label; - } - else { - contentTypeName = ""; - } - - var labelVars = Object.assign({ - "$contentTypeName": contentTypeName, - "$settings": blockObject.settingsData || {}, - "$layout": blockObject.layout || {}, - "$index": (blockObject.index || 0)+1 - }, blockObject.data); - var label = blockObject.labelInterpolator(labelVars); - if (label) { - return label; - } - } - return blockObject.content.contentTypeName; - } - /** * Used to add watchers on all properties in a content or settings model */ @@ -161,10 +127,6 @@ } } } - if (blockObject.__watchers.length === 0) { - // If no watcher where created, it means we have no properties to watch. This means that nothing will activate our generate the label, since its only triggered by watchers. - blockObject.updateLabel(); - } } /** @@ -176,8 +138,6 @@ // sync data: prop.value = blockObject.data[prop.alias]; - - blockObject.updateLabel(); } } } @@ -203,8 +163,6 @@ // sync data: blockObject.data[prop.alias] = prop.value; } - - blockObject.updateLabel(); } } @@ -322,11 +280,11 @@ * @param {object} propertyModelValue data object of the property editor, usually model.value. * @param {string} propertyEditorAlias alias of the property. * @param {object} blockConfigurations block configurations. - * @param {angular-scope} scopeOfExistance A local angularJS scope that exists as long as the data exists. + * @param {angular-scope} scopeOfExistence A local angularJS scope that exists as long as the data exists. * @param {angular-scope} propertyEditorScope A local angularJS scope that represents the property editors scope. * @returns {BlockEditorModelObject} A instance of BlockEditorModelObject. */ - function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope) { + function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistence, propertyEditorScope) { 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."); @@ -358,8 +316,8 @@ }); this.scaffolds = []; - - this.isolatedScope = scopeOfExistance.$new(true); + this.__scopeOfExistence = scopeOfExistence; + this.isolatedScope = scopeOfExistence.$new(true); this.isolatedScope.blockObjects = {}; this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); @@ -397,7 +355,7 @@ * @name getBlockConfiguration * @methodOf umbraco.services.blockEditorModelObject * @description Get block configuration object for a given contentElementTypeKey. - * @param {string} key contentElementTypeKey to recive the configuration model for. + * @param {string} key contentElementTypeKey to receive the configuration model for. * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentElementTypeKey isnt available in the current block configurations. */ getBlockConfiguration: function (key) { @@ -477,7 +435,7 @@ * @ngdoc method * @name getAvailableBlocksForBlockPicker * @methodOf umbraco.services.blockEditorModelObject - * @description Retrieve a list of available blocks, the list containing object with the confirugation model(blockConfigModel) and the element type model(elementTypeModel). + * @description Retrieve a list of available blocks, the list containing object with the configuration model(blockConfigModel) and the element type model(elementTypeModel). * The purpose of this data is to provide it for the Block Picker. * @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel. */ @@ -503,7 +461,7 @@ * @name getScaffoldFromKey * @methodOf umbraco.services.blockEditorModelObject * @description Get scaffold model for a given contentTypeKey. - * @param {string} key contentTypeKey to recive the scaffold model for. + * @param {string} key contentTypeKey to receive the scaffold model for. * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. */ getScaffoldFromKey: function (contentTypeKey) { @@ -515,7 +473,7 @@ * @name getScaffoldFromAlias * @methodOf umbraco.services.blockEditorModelObject * @description Get scaffold model for a given contentTypeAlias, used by clipboardService. - * @param {string} alias contentTypeAlias to recive the scaffold model for. + * @param {string} alias contentTypeAlias to receive the scaffold model for. * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. */ getScaffoldFromAlias: function (contentTypeAlias) { @@ -535,8 +493,7 @@ * - content {Object}: Content model, the content data in a ElementType model. * - settings {Object}: Settings model, the settings data in a ElementType model. * - config {Object}: A local deep copy of the block configuration model. - * - label {string}: The label for this block. - * - updateLabel {Method}: Method to trigger an update of the label for this block. + * - label {string}: The compiled label for this block. * - data {Object}: A reference to the content data object from your property editor model. * - settingsData {Object}: A reference to the settings data object from your property editor model. * - layout {Object}: A reference to the layout entry from your property editor model. @@ -581,18 +538,12 @@ blockObject.key = String.CreateGuid().replace(/-/g, ""); blockObject.config = Utilities.copy(blockConfiguration); if (blockObject.config.label && blockObject.config.label !== "") { - blockObject.labelInterpolator = $interpolate(blockObject.config.label); + /** + * @deprecated use blockObject.label instead + */ + blockObject.labelInterpolator = $interpolate(blockObject.config.label); } blockObject.__scope = this.isolatedScope; - blockObject.updateLabel = _.debounce( - function () { - // Check wether scope still exists, maybe object was destoyed in these seconds. - if (this.__scope) { - this.label = getBlockLabel(this); - this.__scope.$evalAsync(); - } - }.bind(blockObject) - , 10); // make basics from scaffold if(contentScaffold !== null) {// We might not have contentScaffold @@ -655,6 +606,7 @@ if (this.config.settingsElementTypeKey !== null) { mapElementValues(settings, this.settings); } + }; blockObject.sync = function () { @@ -667,7 +619,61 @@ }; // first time instant update of label. - blockObject.label = getBlockLabel(blockObject); + blockObject.label = blockObject.content.contentTypeName; + blockObject.index = 0; + + if (blockObject.config.label && blockObject.config.label !== "") { + var labelElement = $('
', { text: blockObject.config.label}); + + var observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + blockObject.label = mutation.target.textContent; + blockObject.__scope.$evalAsync(); + }); + }); + + observer.observe(labelElement[0], {characterData: true, subtree:true}); + + blockObject.__watchers.push(() => { + observer.disconnect(); + }) + + blockObject.__labelScope = this.__scopeOfExistence.$new(true); + blockObject.__renderLabel = function() { + + var labelVars = { + $contentTypeName: this.content.contentTypeName, + $settings: this.settingsData || {}, + $layout: this.layout || {}, + $index: this.index + 1, + ... this.data + }; + + this.__labelScope = Object.assign(this.__labelScope, labelVars); + + $compile(labelElement.contents())(this.__labelScope); + }.bind(blockObject) + } else { + blockObject.__renderLabel = function() {}; + } + + blockObject.updateLabel = _.debounce(blockObject.__renderLabel, 10); + + + // label rendering watchers: + blockObject.__watchers.push(blockObject.__scope.$watchCollection(function () { + return blockObject.data; + }, blockObject.__renderLabel)); + blockObject.__watchers.push(blockObject.__scope.$watchCollection(function () { + return blockObject.settingsData; + }, blockObject.__renderLabel)); + blockObject.__watchers.push(blockObject.__scope.$watchCollection(function () { + return blockObject.layout; + }, blockObject.__renderLabel)); + blockObject.__watchers.push(blockObject.__scope.$watch(function () { + return blockObject.index; + }, blockObject.__renderLabel)); + // Add blockObject to our isolated scope to enable watching its values: this.isolatedScope.blockObjects["_" + blockObject.key] = blockObject; @@ -679,9 +685,8 @@ this.__watchers.forEach(w => { w(); }); delete this.__watchers; - // help carbage collector: + // help garbage collector: delete this.config; - delete this.layout; delete this.data; delete this.settingsData; @@ -695,6 +700,11 @@ // destroyed. If we do that here it breaks the scope chain and validation. delete this.__scope; + if(this.__labelScope) { + this.__labelScope.$destroy(); + delete this.__labelScope; + } + // removes this method, making it impossible to destroy again. delete this.destroy; @@ -917,6 +927,7 @@ delete this.scaffolds; this.isolatedScope.$destroy(); delete this.isolatedScope; + delete this.__scopeOfExistence; delete this.destroy; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html index 65530f0595..335b477928 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html @@ -1,8 +1,11 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 35c478b297..c71773a04b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -28,7 +28,7 @@ } }); - function BlockListController($scope, $timeout, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper, eventsService, $attrs) { + function BlockListController($scope, $timeout, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper, eventsService, $attrs) { var unsubscribe = []; var modelObject; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js index 1027b82e51..0dc74d7edf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js @@ -1,85 +1,86 @@ (function () { - "use strict"; + "use strict"; - /** - * @ngdoc directive - * @name umbraco.directives.directive:umbBlockListBlock - * @description - * The component to render the view for a block. - * If a stylesheet is used then this uses a ShadowDom to make a scoped element. - * This way the backoffice styling does not collide with the block style. - */ + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbBlockListBlock + * @description + * The component to render the view for a block. + * If a stylesheet is used then this uses a ShadowDom to make a scoped element. + * This way the backoffice styling does not collide with the block style. + */ + + angular + .module("umbraco") + .component("umbBlockListBlock", { + controller: BlockListBlockController, + controllerAs: "model", + bindings: { + stylesheet: "@", + view: "@", + block: "=", + api: "<", + index: "<", + parentForm: "<" + }, + require: { + valFormManager: "^^valFormManager" + } + } + ); - angular - .module("umbraco") - .component("umbBlockListBlock", { - controller: BlockListBlockController, - controllerAs: "model", - bindings: { - stylesheet: "@", - view: "@", - block: "=", - api: "<", - index: "<", - parentForm: "<" - }, - require: { - valFormManager: "^^valFormManager" - } - } - ); + function BlockListBlockController($scope, $compile, $element) { + var model = this; - function BlockListBlockController($scope, $compile, $element) { - var model = this; + model.$onInit = function () { + // This is ugly and is only necessary because we are not using components and instead + // relying on ng-include. It is definitely possible to compile the contents + // of the view into the DOM using $templateCache and $http instead of using + // ng - include which means that the controllerAs flows directly to the view. + // This would mean that any custom components would need to be updated instead of relying on $scope. + // Guess we'll leave it for now but means all things need to be copied to the $scope and then all + // primitives need to be watched. - model.$onInit = function () { - // This is ugly and is only necessary because we are not using components and instead - // relying on ng-include. It is definitely possible to compile the contents - // of the view into the DOM using $templateCache and $http instead of using - // ng - include which means that the controllerAs flows directly to the view. - // This would mean that any custom components would need to be updated instead of relying on $scope. - // Guess we'll leave it for now but means all things need to be copied to the $scope and then all - // primitives need to be watched. + // let the Block know about its form + model.block.setParentForm(model.parentForm); - // let the Block know about its form - model.block.setParentForm(model.parentForm); + // let the Block know about the current index + model.block.index = model.index; - // let the Block know about the current index - model.block.index = model.index; + $scope.block = model.block; + $scope.api = model.api; + $scope.index = model.index; + $scope.parentForm = model.parentForm; + $scope.valFormManager = model.valFormManager; - $scope.block = model.block; - $scope.api = model.api; - $scope.index = model.index; - $scope.parentForm = model.parentForm; - $scope.valFormManager = model.valFormManager; - - if (model.stylesheet) { - var shadowRoot = $element[0].attachShadow({ mode: 'open' }); - shadowRoot.innerHTML = ` + if (model.stylesheet) { + var shadowRoot = $element[0].attachShadow({ mode: 'open' }); + shadowRoot.innerHTML = `
`; - $compile(shadowRoot)($scope); - } - else { - $element.append($compile('
')($scope)); - } - }; + $compile(shadowRoot)($scope); + } + else { + $element.append($compile('
')($scope)); + } + }; - // We need to watch for changes on primitive types and upate the $scope values. - model.$onChanges = function (changes) { - if (changes.index) { - var index = changes.index.currentValue; - $scope.index = index; + // We need to watch for changes on primitive types and update the $scope values. + model.$onChanges = function (changes) { + if (changes.index) { + var index = changes.index.currentValue; + $scope.index = index; - // let the Block know about the current index: - model.block.index = index; - model.block.updateLabel(); - } - }; - } + // let the Block know about the current index: + if ($scope.block) { + $scope.block.index = index; + } + } + }; + } })();