Gets property json paths working in complex editors, next lets see if we can make it work for validation.

This commit is contained in:
Shannon
2020-06-26 00:13:21 +10:00
parent 020835390d
commit 79bf359fa0
19 changed files with 279 additions and 142 deletions
@@ -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<len) {
while (i < len) {
var iEditor = scope.editors[i];
iEditor.styleIndex = Math.min(i+1, allowedNumberOfVisibleEditors);
iEditor.level = Math.max(i-origin, -1);
iEditor.styleIndex = Math.min(i + 1, allowedNumberOfVisibleEditors);
iEditor.level = Math.max(i - origin, -1);
iEditor.inFront = iEditor.level >= 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: "<div ng-transclude></div>",
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('umbEditors', EditorsDirective);
angular.module('umbraco.directives').directive('umbEditorRepeater', EditorRepeaterDirective);
})();
@@ -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: '<div ng-transclude></div>',
template: '<div><pre>{{vm.getValidationPath()}}</pre><div ng-transclude></div></div>',
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;
}
}
@@ -67,7 +67,7 @@
scope: {
ngModel: '=',
tabAlias: '=',
itemIndex: '@'
itemIndex: '='
},
link: link
};
@@ -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
@@ -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);
}
}
@@ -9,19 +9,23 @@
</div>
<div class="umb-group-panel__content">
<umb-property
data-element="property-{{property.alias}}"
ng-repeat="property in group.properties track by property.alias"
property="property"
show-inherit="vm.model.variants.length > 1 && !property.culture && !activeVariant.language.isDefault"
inherits-from="defaultVariant.language.name">
<div ng-class="{'o-40 cursor-not-allowed': vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue}">
<umb-property-editor
model="property"
preview="vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue">
</umb-property-editor>
</div>
<umb-property data-element="property-{{property.alias}}"
ng-repeat="property in group.properties track by property.alias"
property="property"
show-inherit="vm.model.variants.length > 1 && !property.culture && !activeVariant.language.isDefault"
inherits-from="defaultVariant.language.name">
<umb-nested-property property-type-alias="{{property.alias}}"
element-type-index="vm.itemIndex">
<div ng-class="{'o-40 cursor-not-allowed': vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue}">
<umb-property-editor model="property"
preview="vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue">
</umb-property-editor>
</div>
</umb-nested-property>
</umb-property>
</div>
@@ -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: '<'
}
});
@@ -1,3 +1,3 @@
<div>
<umb-element-editor-content model="model.content"></umb-element-editor-content>
<umb-element-editor-content model="model.content" item-index="model.index"></umb-element-editor-content>
</div>
@@ -1 +1 @@
<umb-element-editor-content model="model.settings"></umb-element-editor-content>
<umb-element-editor-content model="model.settings" item-index="model.index"></umb-element-editor-content>
@@ -1,26 +1,29 @@
<div class="umb-editors">
<div class="umb-editor"
ng-repeat="model in editors"
ng-class="{'umb-editor--small': model.size === 'small',
'umb-editor--medium': model.size === 'medium',
'umb-editor--animating': model.animating,
'umb-editor--notInFront': model.inFront !== true,
'umb-editor--infiniteMode': model.infiniteMode,
'umb-editor--moveRight': model.moveRight,
'umb-editor--n0': model.styleIndex === 0,
'umb-editor--n1': model.styleIndex === 1,
'umb-editor--n2': model.styleIndex === 2,
'umb-editor--n3': model.styleIndex === 3,
'umb-editor--outOfRange': model.level === -1,
'umb-editor--level0': model.level === 0,
'umb-editor--level1': model.level === 1,
'umb-editor--level2': model.level === 2,
'umb-editor--level3': model.level === 3}"
>
<umb-editor-repeater class="umb-editor"
ng-repeat="model in editors"
ng-class="{'umb-editor--small': model.size === 'small',
'umb-editor--medium': model.size === 'medium',
'umb-editor--animating': model.animating,
'umb-editor--notInFront': model.inFront !== true,
'umb-editor--infiniteMode': model.infiniteMode,
'umb-editor--moveRight': model.moveRight,
'umb-editor--n0': model.styleIndex === 0,
'umb-editor--n1': model.styleIndex === 1,
'umb-editor--n2': model.styleIndex === 2,
'umb-editor--n3': model.styleIndex === 3,
'umb-editor--outOfRange': model.level === -1,
'umb-editor--level0': model.level === 0,
'umb-editor--level1': model.level === 1,
'umb-editor--level2': model.level === 2,
'umb-editor--level3': model.level === 3}">
<div ng-if="!model.view && !model.animating" ng-transclude></div>
<div ng-if="model.view && !model.animating" ng-include="model.view"></div>
<div ng-if="model.view && !model.animating && !model.$parentScope" ng-include="model.view"></div>
<div ng-show="model.$parentScope" class="scoped-view"></div>
<div class="umb-editor__overlay"></div>
</div>
</umb-editor-repeater>
</div>
@@ -1,6 +1,8 @@
(function () {
"use strict";
// TODO: Does this belong in the property editors folder?
angular
.module("umbraco")
.component("umbBlockCard", {
@@ -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?
}
}
@@ -8,31 +8,36 @@
<div ng-repeat="layout in vm.layout track by layout.$block.key">
<button
type="button"
class="btn-reset umb-block-list__block--create-button"
ng-click="vm.showCreateDialog($index, $event)"
ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as inlineCreateButtonCtrl"
ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)"
>
<button type="button"
class="btn-reset umb-block-list__block--create-button"
ng-click="vm.showCreateDialog($index, $event)"
ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as inlineCreateButtonCtrl"
ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)">
<div class="__plus" ng-style="{'left':inlineCreateButtonCtrl.plusPosX}">+</div>
</button>
<div class="umb-block-list__block" ng-class="{'--active':layout.$block.active}">
<umb-nested-property property-type-alias="{{property.propertyAlias}}"
element-type-index="{{$index}}">
<umb-block-list-scoped-block-content ng-if="layout.$block.config.stylesheet" class="umb-block-list__block--content blockelement__draggable-element" view="{{layout.$block.view}}" stylesheet="/{{::layout.$block.config.stylesheet}}" api="vm.blockEditorApi" block="layout.$block" index="$index">
<umb-block-list-scoped-block-content ng-if="layout.$block.config.stylesheet"
class="umb-block-list__block--content blockelement__draggable-element"
view="{{layout.$block.view}}"
stylesheet="/{{::layout.$block.config.stylesheet}}"
api="vm.blockEditorApi"
block="layout.$block"
index="$index">
</umb-block-list-scoped-block-content>
<umb-block-list-block-content ng-if="!layout.$block.config.stylesheet" class="umb-block-list__block--content" view="{{layout.$block.view}}" api="vm.blockEditorApi" block="layout.$block" index="$index">
<umb-block-list-block-content ng-if="!layout.$block.config.stylesheet"
class="umb-block-list__block--content"
view="{{layout.$block.view}}"
api="vm.blockEditorApi"
block="layout.$block"
index="$index">
</umb-block-list-block-content>
</umb-nested-property>
<div class="umb-block-list__block--actions">
<button type="button" class="btn-reset umb-outline action --settings" localize="title" title="actions_editSettings" ng-click="vm.blockEditorApi.openSettingsForBlock(layout.$block);" ng-if="layout.$block.showSettings === true">
<button type="button" class="btn-reset umb-outline action --settings" localize="title" title="actions_editSettings"
ng-click="vm.blockEditorApi.openSettingsForBlock(layout.$block, $index);"
ng-if="layout.$block.showSettings === true">
<i class="icon icon-settings" aria-hidden="true"></i>
<span class="sr-only">
<localize key="general_settings">Settings</localize>
@@ -56,14 +61,12 @@
</div>
</div>
<button
ng-if="vm.loading !== true"
id="{{vm.model.alias}}"
type="button"
class="btn-reset umb-block-list__create-button umb-outline"
ng-class="{ '--disabled': vm.availableBlockTypes.length === 0 }"
ng-click="vm.showCreateDialog(vm.layout.length, $event)"
>
<button ng-if="vm.loading !== true"
id="{{vm.model.alias}}"
type="button"
class="btn-reset umb-block-list__create-button umb-outline"
ng-class="{ '--disabled': vm.availableBlockTypes.length === 0 }"
ng-click="vm.showCreateDialog(vm.layout.length, $event)">
<localize key="grid_addElement"></localize>
</button>
@@ -82,12 +85,11 @@
</div>
<umb-overlay
ng-if="vm.blockTypePicker.show"
position="target"
size="vm.blockTypePicker.size"
view="vm.blockTypePicker.view"
model="vm.blockTypePicker">
<umb-overlay ng-if="vm.blockTypePicker.show"
position="target"
size="vm.blockTypePicker.size"
view="vm.blockTypePicker.view"
model="vm.blockTypePicker">
</umb-overlay>
@@ -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 = {
@@ -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 = `
<style>
@@ -39,6 +45,14 @@
`;
$compile(shadowRoot)($scope);
}
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?
}
}
@@ -5,6 +5,7 @@
<span>{{block.label}}</span>
</button>
<div class="blockelement-inlineblock-editor__inner" ng-class="{'--singleGroup':block.content.variants[0].tabs.length === 1}" ng-if="block.active === true">
<umb-element-editor-content model="block.content"></umb-element-editor-content>
<!-- NOTE: The 'index' is passed to this via it's scope which is set on the outer component: umbBlockListBlockContent -->
<umb-element-editor-content model="block.content" item-index="index"></umb-element-editor-content>
</div>
</div>
@@ -1,4 +1,7 @@
<button type="button" class="btn-reset umb-outline blockelement-labelblock-editor blockelement__draggable-element" ng-click="api.editBlock(block)" ng-focus="block.focus" ng-class="{'--active':block.active}">
<button type="button" class="btn-reset umb-outline blockelement-labelblock-editor blockelement__draggable-element"
ng-click="api.editBlock(block, false, index)"
ng-focus="block.focus"
ng-class="{'--active':block.active}">
<i class="icon {{block.content.icon}}"></i>
<span>{{block.label}}</span>
</button>
@@ -6,7 +6,7 @@
data-element="property-{{property.alias}}">
<umb-nested-property property-type-alias="{{property.propertyAlias}}"
element-type-index="{{itemIndex}}">
element-type-index="itemIndex">
<umb-property-editor model="property"></umb-property-editor>
</umb-nested-property>
@@ -28,7 +28,7 @@
</div>
<div class="umb-nested-content__content" ng-if="vm.currentNode.key === node.key && !vm.sorting">
<umb-nested-content-editor ng-model="node" item-index="{{$index}}" tab-alias="ncTabAlias" />
<umb-nested-content-editor ng-model="node" item-index="$index" tab-alias="ncTabAlias" />
</div>
</div>