Appending the block objects to layout, to share it across variants and in split-view.

This commit is contained in:
Niels Lyngsø
2020-06-16 14:38:49 +02:00
parent 87059a9f95
commit 53968202a0
3 changed files with 143 additions and 101 deletions

View File

@@ -28,13 +28,20 @@
*
* <pre>
*
* // 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 !== "") {

View File

@@ -4,9 +4,9 @@
<div class="umb-block-list__wrapper" ng-style="vm.listWrapperStyles">
<div ui-sortable="vm.sortableOptions" ng-model="vm.blocks" ng-if="vm.loading !== true">
<div ui-sortable="vm.sortableOptions" ng-model="vm.layout" ng-if="vm.loading !== true">
<div ng-repeat="block in vm.blocks track by block.key">
<div ng-repeat="layout in vm.layout track by layout.$block.key">
<button
type="button"
@@ -18,27 +18,27 @@
<div class="__plus" ng-style="{'left':inlineCreateButtonCtrl.plusPosX}">+</div>
</button>
<div class="umb-block-list__block" ng-class="{'--open':block.isOpen}">
<div class="umb-block-list__block" ng-class="{'--open':layout.$block.isOpen}">
<umb-block-list-scoped-block-content ng-if="block.config.stylesheet" class="umb-block-list__block--content blockelement__draggable-element" view="{{block.view}}" stylesheet="/{{::block.config.stylesheet}}" api="vm.blockEditorApi" block="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="!block.config.stylesheet" class="umb-block-list__block--content" view="{{block.view}}" api="vm.blockEditorApi" block="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>
<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(block);" ng-if="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);" 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>
</span>
</button>
<button type="button" class="btn-reset umb-outline action --copy" localize="title" title="actions_copy" ng-click="vm.blockEditorApi.requestCopyBlock(block);" ng-if="vm.showCopy">
<button type="button" class="btn-reset umb-outline action --copy" localize="title" title="actions_copy" ng-click="vm.blockEditorApi.requestCopyBlock(layout.$block);" ng-if="layout.$block.showCopy === true">
<i class="icon icon-documents" aria-hidden="true"></i>
<span class="sr-only">
<localize key="general_copy">Copy</localize>
</span>
</button>
<button type="button" class="btn-reset umb-outline action --delete" localize="title" title="actions_delete" ng-click="vm.blockEditorApi.requestDeleteBlock(block);">
<button type="button" class="btn-reset umb-outline action --delete" localize="title" title="actions_delete" ng-click="vm.blockEditorApi.requestDeleteBlock(layout.$block);">
<i class="icon icon-trash" aria-hidden="true"></i>
<span class="sr-only">
<localize key="general_delete">Delete</localize>
@@ -56,21 +56,21 @@
type="button"
class="btn-reset umb-block-list__create-button umb-outline"
ng-class="{ '--disabled': vm.availableBlockTypes.length === 0 }"
ng-click="vm.showCreateDialog(vm.blocks.length, $event)"
ng-click="vm.showCreateDialog(vm.layout.length, $event)"
>
<localize key="grid_addElement"></localize>
</button>
<input type="hidden" name="minCount" ng-model="vm.blocks" />
<input type="hidden" name="maxCount" ng-model="vm.blocks" />
<input type="hidden" name="minCount" ng-model="vm.layout" />
<input type="hidden" name="maxCount" ng-model="vm.layout" />
<div ng-messages="vm.propertyForm.minCount.$error" show-validation-on-submit>
<div class="help text-error" ng-message="minCount">
<localize key="validation_entriesShort" tokens="[vm.validationLimit.min, vm.validationLimit.min - vm.blocks.length]" watch-tokens="true">Minimum %0% entries, needs <strong>%1%</strong> more.</localize>
<localize key="validation_entriesShort" tokens="[vm.validationLimit.min, vm.validationLimit.min - vm.layout.length]" watch-tokens="true">Minimum %0% entries, needs <strong>%1%</strong> more.</localize>
</div>
</div>
<div ng-if="vm.propertyForm.maxCount.$error === true && vm.blocks.length > vm.validationLimit.max">
<div ng-if="vm.propertyForm.maxCount.$error === true && vm.layout.length > vm.validationLimit.max">
<div class="help text-error">
<localize key="validation_entriesExceed" tokens="[vm.validationLimit.max, vm.blocks.length - vm.validationLimit.max]" watch-tokens="true">Maximum %0% entries, <strong>%1%</strong> too many.</localize>
<localize key="validation_entriesExceed" tokens="[vm.validationLimit.max, vm.layout.length - vm.validationLimit.max]" watch-tokens="true">Maximum %0% entries, <strong>%1%</strong> too many.</localize>
</div>
</div>

View File

@@ -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) {