implement block list editor

This commit is contained in:
Niels Lyngsø
2020-02-26 13:02:22 +01:00
parent 82676ae649
commit 8ffae1ce31
10 changed files with 321 additions and 42 deletions
@@ -0,0 +1,227 @@
(function () {
'use strict';
function blockEditorService($interpolate, udiService) {
function applyModelToScaffold(scaffold, contentModel) {
scaffold.key = contentModel.key;
var variant = scaffold.variants[0];
for (var t = 0; t < variant.tabs.length; t++) {
var tab = variant.tabs[t];
for (var p = 0; p < tab.properties.length; p++) {
var prop = tab.properties[p];
if (contentModel[prop.propertyAlias]) {
prop.value = contentModel[prop.propertyAlias];
}
}
}
}
/**
* @ngdoc factory
* @name umbraco.factory.BlockEditorModelObject
* @description A model object used to handle Block Editor data.
**/
function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations) {
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.");
}
// ensure basic part of data-structure is in place:
this.value = propertyModelValue;
this.value.layout = this.value.layout || [];
this.value.data = this.value.data || [];
this.propertyEditorAlias = propertyEditorAlias;
this.blockConfigurations = blockConfigurations;
this.scaffolds = [];
};
BlockEditorModelObject.prototype = {
getBlockConfiguration: function(alias) {
return this.blockConfigurations.find(blockConfiguration => blockConfiguration.contentTypeAlias === alias);
},
loadScaffolds: function(contentResource) {
var tasks = [];
var scaffoldAliases = [];
this.blockConfigurations.forEach(blockConfiguration => {
scaffoldAliases.push(blockConfiguration.contentTypeAlias);
if (blockConfiguration.settingsElementTypeAlias != null) {
scaffoldAliases.push(elementType.settingsElementTypeAlias);
}
});
// remove dublicates.
scaffoldAliases = scaffoldAliases.filter((value, index, self) => self.indexOf(value) === index);
scaffoldAliases.forEach((elementTypeAlias => {
tasks.push(contentResource.getScaffold(-20, elementTypeAlias).then(scaffold => {
console.log(scaffold);
this.scaffolds.push(scaffold);
}));
}));
return Promise.all(tasks);
},
getAvailableBlocksForItemPicker: function() {
var blocks = [];
this.blockConfigurations.forEach(blockConfiguration => {
var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias);
blocks.push({
alias: scaffold.contentTypeAlias,
name: scaffold.contentTypeName,
icon: scaffold.icon
});
});
return blocks;
},
getScaffoldFor: function(contentTypeAlias, data) {
return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias);
},
/**
* Retrieve editing model of a layout entry
* @return {Object} Scaffolded Block Content object.
*/
getEditingModel: function(layoutEntry) {
var contentModel = this.getContentByUdi(layoutEntry.udi);
var blockConfiguration = this.getBlockConfiguration(contentModel.contentTypeAlias);
// TODO: make blockConfiguration the base for model, remeber to make a copy.
var model = {
label: "",
labelInterpolate: $interpolate(blockConfiguration.label),
editor: blockConfiguration.view,
overlaySize: "medium"
};
var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias);
if(scaffold === null) {
return null;
}
model.content = angular.copy(scaffold);
applyModelToScaffold(model.content, contentModel);
// TODO: settings
return model;
},
/**
* Retrieve layout data
* @return layout object.
*/
getLayout: function() {
if (!this.value.layout[this.propertyEditorAlias]) {
this.value.layout[this.propertyEditorAlias] = [];
}
return this.value.layout[this.propertyEditorAlias];
},
/**
* Create layout entry
* @param {object} blockConfiguration
* @return layout entry, to be added in the layout.
*/
createLayoutEntry: function(contentTypeAlias) {
var blockConfiguration = this.getBlockConfiguration(contentTypeAlias);
var entry = {
udi: this.createContent(contentTypeAlias)
}
if (blockConfiguration.settingsElementTypeAlias != null) {
// TODO: Settings.
}
return entry;
},
// You make entries in your layout your self.
getContentByUdi: function(udi) {
return this.value.data.find(entry => entry.udi === udi);
},
createContent: function(elementTypeAlias) {
var content = {
contentTypeAlias: elementTypeAlias,
udi: udiService.create("element")
};
this.value.data.push(content);
return content.udi;
},
removeContent: function(entry) {
const index = this.value.data.indexOf(entry)
if (index > -1) {
this.value.splice(index, 1);
}
},
removeContentByUdi: function(udi) {
const index = this.value.data.findIndex(o => o.udi === udi);
if (index > -1) {
this.value.splice(index, 1);
}
}
}
return {
createModelObject: function(propertyModelValue, propertyEditorAlias, blockConfigurations) {
return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations);
},
getBlockLabel: function(blockModelObject, labelIndex) {
console.log("getBlockLabel", blockModelObject);
// TODO: we should do something about this for performance.
var vars = new Object();
vars["$index"] = labelIndex;
var variant = blockModelObject.content.variants[0];
var tab = variant.tabs[0];
// TODO: need to look up all tabs...
for(const property of tab.properties) {
vars[property.alias] = property.value;
}
if(blockModelObject.labelInterpolate) {
return blockModelObject.labelInterpolate(vars);
}
return blockModelObject.contentTypeName;
}
}
}
angular.module('umbraco.services').service('blockEditorService', blockEditorService);
})();
@@ -0,0 +1,32 @@
(function () {
"use strict";
/**
* @ngdoc service
* @name umbraco.services.udiService
* @description A service for UDIs
**/
function udiService() {
return {
/**
* @ngdoc method
* @name umbraco.services.udiService#parse
* @methodOf umbraco.services.udiService
* @function
*
* @description
* Generates a Udi string.
*
* @param {string} entityType The entityType as a string.
* @returns {string} The generated UDI
*/
create: function(entityType) {
return "umb://" + entityType + "/" + String.CreateGuid();
}
}
}
angular.module("umbraco.services").factory("udiService", udiService);
})();
@@ -1,7 +1,7 @@
<div class="blockelement-inlineblock-editor" ng-class="{'--open': bc.isOpen}" ng-controller="Umbraco.PropertyEditors.BlockEditor.InlineBlockEditor as bc">
<button type="button" class="btn-reset umb-outline blockelement__draggable-element" ng-click="bc.openBlock(block)" focus-when="{{moveFocusToBlock === block}}">
<span class="caret"></span>
<i class="icon {{block.elementType.icon}}"></i>
<i class="icon {{block.content.icon}}"></i>
<span>{{block.label}}</span>
</button>
<div class="blockelement-inlineblock-editor__inner" ng-if="bc.isOpen === true">
@@ -1,4 +1,4 @@
<button type="button" class="btn-reset umb-outline blockelement-labelblock-editor blockelement__draggable-element" ng-click="vm.editBlock(block)" focus-when="{{moveFocusToBlock === block}}">
<i class="icon {{block.elementType.icon}}"></i>
<i class="icon {{block.content.icon}}"></i>
<span>{{block.label}}</span>
</button>
@@ -5,7 +5,7 @@ angular.module("umbraco")
var vm = this;
vm.firstProperty = $scope.block.content.tabs[0].properties[0];
vm.firstProperty = $scope.block.content.variants[0].tabs[0].properties[0];
/*
vm.onBlur = function() {
if (vm.firstProperty.value === null || vm.firstProperty.value === "") {
@@ -4,7 +4,7 @@
angular
.module('umbraco.directives')
.component('umbElementContentEditor', {
templateUrl: 'views/common/infiniteeditors/elementeditor/elementeditor.component.html',
templateUrl: 'views/common/infiniteeditors/elementeditor/elementContentEditor.component.html',
controller: ElementEditorComponentController,
controllerAs: 'vm',
bindings: {
@@ -15,7 +15,7 @@
}
});
function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService) {
function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService, contentResource) {
var unsubscribe = [];
var vm = this;
@@ -24,13 +24,15 @@
$scope.moveFocusToBlock = null;
console.log("model JSON:", JSON.stringify(model));
console.log("model:", model);
console.log("config:", model.config);
vm.validationLimit = model.config.validationLimit;
console.log("value:", model.value);
/*
vm.availableBlockTypes = [
{
alias: "pageModule",
@@ -856,7 +858,34 @@
];
*/
model.value = model.value || {};
var modelObject = blockEditorService.createModelObject(model.value, model.editor, model.config.blocks);
modelObject.loadScaffolds(contentResource).then(loaded);
vm.layout = [];
vm.blocks = [];
vm.availableBlockTypes = [];
function loaded() {
console.log("Loading done!!!");
console.log(modelObject);
vm.layout = modelObject.getLayout();
vm.layout.forEach(entry => {
vm.blocks.push(modelObject.getEditingModel(entry));
});
vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker();
console.log(vm.availableBlockTypes);
}
function setDirty() {
@@ -865,33 +894,23 @@
}
};
function addNewBlock(index, type) {
function addNewBlock(index, contentTypeAlias) {
var block = angular.copy(type.prototype_paste_data);
// Create layout entry.
var layoutEntry = modelObject.createLayoutEntry(contentTypeAlias);
// add layout entry a decired location in layout.
vm.layout.splice(index, 0, layoutEntry);
vm.blocks.splice(index, 0, block);
$scope.moveFocusToBlock = block;
// make editing object
var blockEditingObject = modelObject.getEditingModel(layoutEntry);
// apply editing model at decired location in editing model.
vm.blocks.splice(index, 0, blockEditingObject);
$scope.moveFocusToBlock = blockEditingObject;
}
function getBlockLabel(block) {
// TODO: we should do something about this for performance.
var props = new Object();
var tab = block.content.tabs[0];
// TODO: need to look up all tabs...
for(const property of tab.properties) {
props[property.alias] = property.value;
}
if(block.labelInterpolate) {
return block.labelInterpolate(props);
}
return "block.label";
}
vm.deleteBlock = function(block) {
var index = vm.blocks.indexOf(block);
@@ -942,7 +961,7 @@
availableItems: vm.availableBlockTypes,
submit: function (model) {
if (model && model.selectedItem) {
addNewBlock(createIndex, model.selectedItem);
addNewBlock(createIndex, model.selectedItem.alias);
}
vm.blockTypePicker.close();
},
@@ -1050,9 +1069,10 @@
// TODO: We need to investigate if we can do a specific watch on each block, so we dont re-render all blocks.
unsubscribe.push($scope.$watch("vm.blocks", onBlocksUpdated, true));
function onBlocksUpdated(newVal, oldVal){
function onBlocksUpdated(newVal, oldVal) {
var labelIndex = 1;
for(const block of vm.blocks) {
block.label = getBlockLabel(block);
block.label = blockEditorService.getBlockLabel(block, labelIndex++);
}
}
unsubscribe.push($scope.$watch(() => vm.blocks.length, validateLimits));
@@ -47,7 +47,7 @@
vm.requestRemoveEntryByIndex = function (index) {
localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) {
var contentElementType = vm.getElementTypeByAlias($scope.model.value[index].elementTypeAlias);
var contentElementType = vm.getElementTypeByAlias($scope.model.value[index].contentTypeAlias);
overlayService.confirmDelete({
title: data[0],
content: localizationService.tokenReplace(data[1], [contentElementType.name]),
@@ -78,7 +78,7 @@
vm.getAvailableElementTypes = function () {
return vm.elementTypes.filter(function (type) {
return !$scope.model.value.find(function (entry) {
return type.alias === entry.elementTypeAlias;
return type.alias === entry.contentTypeAlias;
});
});
};
@@ -91,9 +91,9 @@
vm.openAddDialog = function ($event, entry) {
//we have to add the alias to the objects (they are stored as elementTypeAlias)
//we have to add the alias to the objects (they are stored as contentTypeAlias)
var selectedItems = _.each($scope.model.value, function (obj) {
obj.alias = obj.elementTypeAlias;
obj.alias = obj.contentTypeAlias;
return obj;
});
@@ -149,7 +149,7 @@
vm.addEntryFromElementTypeAlias = function(alias) {
var entry = {
"elementTypeAlias": alias,
"contentTypeAlias": alias,
"view": null,
"labelTemplate": "",
"settingsElementTypeAlias": null
@@ -24,16 +24,16 @@
<div class="umb-table-body" ui-sortable="vm.sortableOptions" ng-model="model.value">
<div class="umb-table-row block-entry" ng-repeat="entry in model.value">
<div class="umb-table-cell not-fixed umb-table__name">
{{ contentPreview = vm.getElementTypeByAlias(entry.elementTypeAlias); "" }}
{{ contentPreview = vm.getElementTypeByAlias(entry.contentTypeAlias); "" }}
<umb-node-preview icon="contentPreview.icon" name="contentPreview.name"></umb-node-preview>
<div class="cell-actions">
<button type="button" class="btn-reset cell-actions-btn --open umb-outline" ng-click="vm.openElementType(entry.elementTypeAlias)">
<button type="button" class="btn-reset cell-actions-btn --open umb-outline" ng-click="vm.openElementType(entry.contentTypeAlias)">
<i class="icon icon-edit"></i>
</button>
</div>
</div>
<div class="umb-table-cell">
<input type="text" ng-model="entry.labelTemplate" class="text-input"/>
<input type="text" ng-model="entry.label" class="text-input"/>
</div>
<div class="umb-table-cell">
<div class="settings-input --hasValue" ng-if="entry.view !== null" >
@@ -29,7 +29,7 @@ namespace Umbraco.Web.PropertyEditors
public class BlockConfiguration
{
// TODO: rename this to contentElementTypeAlias, I would like this to be specific, since we have the settings.
[JsonProperty("elementTypeAlias")]
[JsonProperty("contentTypeAlias")]
public string Alias { get; set; }
[JsonProperty("settingsElementTypeAlias")]
@@ -38,8 +38,8 @@ namespace Umbraco.Web.PropertyEditors
[JsonProperty("view")]
public string View { get; set; }
[JsonProperty("labelTemplate")]
public string Template { get; set; }
[JsonProperty("label")]
public string Label { get; set; }
}
[ConfigurationField("useAccordionsAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view")]