diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index 9bdef41225..21f0af6c8f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -7,94 +7,94 @@ var evts = []; var allowedNumberOfVisibleEditors = 3; - + scope.editors = []; - + function addEditor(editor) { - + editor.inFront = true; editor.moveRight = true; editor.level = 0; editor.styleIndex = 0; - + // push the new editor to the dom scope.editors.push(editor); - + $timeout(() => { editor.moveRight = false; }) - + editor.animating = true; setTimeout(revealEditorContent.bind(this, editor), 400); - + updateEditors(); } - + function removeEditor(editor) { - + editor.moveRight = true; - + editor.animating = true; setTimeout(removeEditorFromDOM.bind(this, editor), 400); - + updateEditors(-1); - + } - + function revealEditorContent(editor) { - + editor.animating = false; - + scope.$digest(); - + } - + function removeEditorFromDOM(editor) { - + // push the new editor to the dom var index = scope.editors.indexOf(editor); if (index !== -1) { scope.editors.splice(index, 1); } - + updateEditors(); - + scope.$digest(); - + } - + /** update layer positions. With ability to offset positions, needed for when an item is moving out, then we dont want it to influence positions */ function updateEditors(offset) { - + offset = offset || 0;// fallback value. - + var len = scope.editors.length; var calcLen = len + offset; var ceiling = Math.min(calcLen, allowedNumberOfVisibleEditors); - var origin = Math.max(calcLen-1, 0)-ceiling; + var origin = Math.max(calcLen - 1, 0) - ceiling; var i = 0; - while(i= ceiling; i++; } } - + evts.push(eventsService.on("appState.editors.open", function (name, args) { addEditor(args.editor); })); evts.push(eventsService.on("appState.editors.close", function (name, args) { // remove the closed editor - if(args && args.editor) { + if (args && args.editor) { removeEditor(args.editor); } // close all editors - if(args && !args.editor && args.editors.length === 0) { + if (args && !args.editor && args.editors.length === 0) { scope.editors = []; } })); @@ -119,6 +119,64 @@ } + // This directive allows for us to run a custom $compile for the view within the repeater which allows + // us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the + // infinite editing. The retain the $scope hiearchy a special $parentScope property is passed in to the model. + function EditorRepeaterDirective($http, $templateCache, $compile) { + function link(scope, el, attr, ctrl) { + + var editor = scope && scope.$parent ? scope.$parent.model : null; + if (!editor) { + return; + } + + var unsubscribe = []; + + //if a custom parent scope is defined then we need to manually compile the view + if (editor.$parentScope) { + var element = el.find(".scoped-view"); + $http.get(editor.view, { cache: $templateCache }) + .then(function (response) { + var templateScope = editor.$parentScope.$new(); + + unsubscribe.push(function () { + templateScope.$destroy(); + }); + + // NOTE: the 'model' name here directly affects the naming convention used in infinite editors, this why you access the model + // like $scope.model.If this is changed, everything breaks.This is because we are entirely reliant upon ng - include and inheriting $scopes. + // by default without a $parentScope used for infinite editing the 'model' propety will be set because the view creates the scopes in + // ng-repeat by ng-repeat="model in editors" + templateScope.model = editor; + + element.html(response.data); + element.show(); + $compile(element.contents())(templateScope); + }); + } + + scope.$on('$destroy', function () { + for (var i = 0; i < unsubscribe.length; i++) { + unsubscribe[i](); + } + }); + } + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + scope: { + editors: "=" + }, + template: "
", + link: link + }; + + return directive; + } + angular.module('umbraco.directives').directive('umbEditors', EditorsDirective); + angular.module('umbraco.directives').directive('umbEditorRepeater', EditorRepeaterDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js index 239259b3fa..2679ce92a0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js @@ -4,44 +4,54 @@ /** * @ngdoc component - * @name Umbraco.umbBlockListBlockContent + * @name umbraco.umbNestedProperty * @function * * @description - * The component for a style-inheriting block of the block list property editor. + * Used to have nested property editors within complex editors in order to generate jsonpath for them to be used in validation */ angular .module("umbraco") .component("umbNestedProperty", { transclude: true, - template: '
', + template: '
{{vm.getValidationPath()}}
', controller: NestedPropertyController, controllerAs: 'vm', bindings: { propertyTypeAlias: "@", - elementTypeIndex: "@" + elementTypeIndex: "<", + findInScopeChain: "<" // TODO: Use/enable this }, require: { - umbNestedProperty: "?^^umbNestedProperty" + umbNestedProperty: "?^^" } }); - function NestedPropertyController($scope) { + function NestedPropertyController($scope, angularHelper) { var vm = this; vm.$onInit = function () { - + if (!vm.propertyTypeAlias) { + throw "no propertyTypeAlias specified for umbNestedProperty"; + } }; + vm.$postLink = function () { + // if directive inheritance (DOM) doesn't find one, then check scope inheritance + if (!vm.umbNestedProperty/* && findInScopeChain*/) { + var found = angularHelper.traverseScopeChain($scope, s => s.vm && s.vm.constructor.name == "NestedPropertyController"); + if (found) { + vm.umbNestedProperty = found.vm; + } + } + } + // returns a jsonpath for where this property is located in a hierarchy // this will call into all hierarchical parents vm.getValidationPath = function () { - var path = vm.umbNestedProperty ? vm.umbNestedProperty.getValidationPath() : "$"; - if (vm.propertyTypeAlias && vm.elementTypeIndex) { - path += ".[nestedValidation].[" + vm.elementTypeIndex + "].[" + vm.propertyTypeAlias + "]"; - return path; - } - return null; + var path = vm.umbNestedProperty ? vm.umbNestedProperty.getValidationPath() : "$"; + path += ".[nestedValidation].[" + vm.elementTypeIndex + "].[" + vm.propertyTypeAlias + "]"; + return path; } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js index b135d9cdc4..b632471a69 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js @@ -67,7 +67,7 @@ scope: { ngModel: '=', tabAlias: '=', - itemIndex: '@' + itemIndex: '=' }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index f2ff711ac9..325c6255a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -9,6 +9,23 @@ function angularHelper($q) { return { + /** + * Will traverse up the $scope chain to all ancestors until the predicate matches for the current scope or until it's at the root. + * @param {any} scope + * @param {any} predicate + */ + traverseScopeChain: function (scope, predicate) { + var s = scope.$parent; + while (s) { + var result = predicate(s); + if (result === true) { + return s; + } + s = s.$parent; + } + return null; + }, + /** * Method used to re-run the $parsers for a given ngModel * @param {} scope diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js index 08ffb4a9ae..48522d22ff 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js @@ -4,21 +4,19 @@ angular.module("umbraco") function ($scope, localizationService, formHelper) { var vm = this; + // TODO: Why are we assigning content/setting separately when we already have vm.model? vm.content = $scope.model.content; - vm.settings = $scope.model.settings; - + vm.settings = $scope.model.settings; + vm.model = $scope.model; + vm.tabs = []; localizationService.localizeMany([ - $scope.model.liveEditing ? "prompt_discardChanges" : "general_close", - $scope.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges" + vm.model.liveEditing ? "prompt_discardChanges" : "general_close", + vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges" ]).then(function (data) { vm.closeLabel = data[0]; vm.submitLabel = data[1]; }); - vm.model = $scope.model; - - vm.tabs = []; - if (vm.content && vm.content.variants) { var apps = vm.content.apps; @@ -27,11 +25,12 @@ angular.module("umbraco") // replace view of content app. var contentApp = apps.find(entry => entry.alias === "umbContent"); - if(contentApp) { + if (contentApp) { + // TODO: This is strange, why does this render a view from somewhere else and this is the only place where that view is used? contentApp.view = "views/common/infiniteeditors/elementeditor/elementeditor.content.html"; - if($scope.model.hideContent) { + if(vm.model.hideContent) { apps.splice(apps.indexOf(contentApp), 1); - } else if ($scope.model.openSettings !== true) { + } else if (vm.model.openSettings !== true) { contentApp.active = true; } } @@ -49,10 +48,11 @@ angular.module("umbraco") "name": settingsName, "alias": "settings", "icon": "icon-settings", + // TODO: This is strange, why does this render a view from somewhere else and this is the only place where that view is used? "view": "views/common/infiniteeditors/elementeditor/elementeditor.settings.html" }; vm.tabs.push(settingsTab); - if ($scope.model.openSettings) { + if (vm.model.openSettings) { settingsTab.active = true; } } @@ -60,17 +60,17 @@ angular.module("umbraco") } vm.submitAndClose = function () { - if ($scope.model && $scope.model.submit) { + if (vm.model && vm.model.submit) { if (formHelper.submitForm({ scope: $scope })) { - $scope.model.submit($scope.model); + vm.model.submit(vm.model); } } } vm.close = function() { - if ($scope.model && $scope.model.close) { + if (vm.model && vm.model.close) { // TODO: If content has changed, we should notify user. - $scope.model.close($scope.model); + vm.model.close(vm.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html index 5308173c72..aa1a8c5afa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html @@ -9,19 +9,23 @@
- -
- - -
+ + + + +
+ + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js index 5056576ca3..c4456a584b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js @@ -1,6 +1,8 @@ (function () { 'use strict'; + // TODO: Add docs - this component is used to render a content item based on an Element Type as a nested editor + angular .module('umbraco.directives') .component('umbElementEditorContent', { @@ -8,7 +10,10 @@ controller: ElementEditorContentComponentController, controllerAs: 'vm', bindings: { - model: '=' + model: '=', + // As this component is used for creating nested editors based on an element type, we need to know the index of this nested + // editor so that validation works. For example, if this is used in the block editor, this is the index of the block being rendered. + itemIndex: '<' } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html index eb8c72c579..3e2aec72a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html @@ -1,3 +1,3 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html index df69e2e648..63222a5cab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html @@ -1 +1 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html index 2f1286b090..9bb5a6161d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html @@ -1,26 +1,29 @@
-
+ +
-
+ +
+
-
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js index 8f1cb00c6d..f07c4f1529 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js @@ -1,6 +1,8 @@ (function () { "use strict"; + // TODO: Does this belong in the property editors folder? + angular .module("umbraco") .component("umbBlockCard", { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js index fb47ffdaab..af4fcf438c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js @@ -19,7 +19,7 @@ bindings: { view: "@", block: "=", - api: "=", + api: "=", // Should this be a one way bind? index: "<" } } @@ -27,10 +27,22 @@ function BlockListBlockContentController($scope) { var model = this; - model.$onInit = function() { + model.$onInit = function () { + // Ugh, due to the way we work with angularjs and property editors not being components and needing to use ng-include, + // it means we need to expose things directly on the $scope so they can use them. + // It also means we need to watch for changes and upate the $scope values. + $scope.block = model.block; $scope.api = model.api; + $scope.index = model.index; }; + model.$onChanges = function (changes) { + if (changes.index) { + $scope.index = changes.index.currentValue; + } + + // TODO: Wouldn't we need to watch for any changes to model.block/api here too? + } } 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 3c255e023a..ac2e9eb376 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 @@ -8,31 +8,36 @@
-
- - - - + - + - -
-
- @@ -82,12 +85,11 @@
- + 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 0ae5527da9..33684e9c03 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 @@ -131,6 +131,7 @@ // Append the blockObjects to our layout. vm.layout.forEach(entry => { if (entry.$block === undefined || entry.$block === null) { + var block = getBlockObject(entry); // If this entry was not supported by our property-editor it would return 'null'. @@ -221,7 +222,12 @@ }); } - function editBlock(blockObject, openSettings) { + function editBlock(blockObject, openSettings, blockIndex) { + + // this must be set + if (blockIndex === undefined) { + throw "blockIndex was not specified on call to editBlock"; + } var wasNotActiveBefore = blockObject.active !== true; blockObject.active = true; @@ -239,12 +245,14 @@ } var hideContent = (openSettings === true && inlineEditing === true); - + var blockEditorModel = { + $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing hideContent: hideContent, openSettings: openSettings === true, liveEditing: liveEditing, title: blockObject.label, + index: blockIndex, view: "views/common/infiniteeditors/blockeditor/blockeditor.html", size: blockObject.config.editorSize || "medium", submit: function(blockEditorModel) { @@ -326,7 +334,7 @@ if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { editorService.close(); if (added && vm.layout.length > createIndex) { - editBlock(vm.layout[createIndex].$block); + editBlock(vm.layout[createIndex].$block, false, createIndex); } } }, @@ -471,18 +479,16 @@ }); } - function openSettingsForBlock(block) { - editBlock(block, true); + function openSettingsForBlock(block, blockIndex) { + editBlock(block, true, blockIndex); } - - vm.blockEditorApi = { editBlock: editBlock, requestCopyBlock: requestCopyBlock, requestDeleteBlock: requestDeleteBlock, deleteBlock: deleteBlock, - openSettingsForBlock: openSettingsForBlock + openSettingsForBlock: openSettingsForBlock } vm.sortableOptions = { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js index b778abc35b..4678b5741f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js @@ -19,7 +19,7 @@ stylesheet: "@", view: "@", block: "=", - api: "=", + api: "=", // Should this be a one way bind? index: "<" } } @@ -27,9 +27,15 @@ function BlockListScopedBlockContentController($compile, $element, $scope) { var model = this; - model.$onInit = function() { + model.$onInit = function () { + // Ugh, due to the way we work with angularjs and property editors not being components and needing to use ng-include, + // it means we need to expose things directly on the $scope so they can use them. + // It also means we need to watch for changes and upate the $scope values. + $scope.block = model.block; $scope.api = model.api; + $scope.index = model.index; + var shadowRoot = $element[0].attachShadow({mode:'open'}); shadowRoot.innerHTML = `