Merge remote-tracking branch 'origin/v8/feature/block-editor-list' into v8/feature/block-editor-list-validation
# Conflicts: # src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html
This commit is contained in:
@@ -31,6 +31,6 @@ exports.build = series(parallel(dependencies, js, less, views), testUnit);
|
||||
exports.dev = series(setDevelopmentMode, parallel(dependencies, js, less, views), watchTask);
|
||||
exports.watch = series(watchTask);
|
||||
//
|
||||
exports.runTests = series(setTestMode, parallel(js, testUnit));
|
||||
exports.runUnit = series(setTestMode, parallel(js, runUnitTestServer), watchTask);
|
||||
exports.runTests = series(setTestMode, series(js, testUnit));
|
||||
exports.runUnit = series(setTestMode, series(js, runUnitTestServer), watchTask);
|
||||
exports.testE2e = series(setTestMode, parallel(testE2e));
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
@scope
|
||||
|
||||
@description
|
||||
<b>Added in Umbraco 7.8</b>. The tour component is a global component and is already added to the umbraco markup.
|
||||
In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco.
|
||||
You can easily add you own tours to the Help-drawer or show and start tours from
|
||||
<b>Added in Umbraco 7.8</b>. The tour component is a global component and is already added to the umbraco markup.
|
||||
In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco.
|
||||
You can easily add you own tours to the Help-drawer or show and start tours from
|
||||
anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install <a href="https://our.umbraco.com/projects/starter-kits/the-starter-kit/">The Starter Kit</a> in Umbraco 7.8
|
||||
|
||||
<h1><b>Extending the help drawer with custom tours</b></h1>
|
||||
The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
|
||||
Place the file in <i>App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json</i> and it will automatically be
|
||||
The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file.
|
||||
Place the file in <i>App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json</i> and it will automatically be
|
||||
picked up by Umbraco and shown in the Help-drawer.
|
||||
|
||||
<h3><b>The tour object</b></h3>
|
||||
@@ -26,7 +26,7 @@ The tour object consist of two parts - The overall tour configuration and a list
|
||||
"groupOrder": 200 // Control the order of tour groups
|
||||
"allowDisable": // Adds a "Don't" show this tour again"-button to the intro step
|
||||
"culture" : // From v7.11+. Specifies the culture of the tour (eg. en-US), if set the tour will only be shown to users with this culture set on their profile. If omitted or left empty the tour will be visible to all users
|
||||
"requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.
|
||||
"requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.
|
||||
"steps": [] // tour steps - see next example
|
||||
}
|
||||
</pre>
|
||||
@@ -43,11 +43,12 @@ The tour object consist of two parts - The overall tour configuration and a list
|
||||
"backdropOpacity": 0.4 // the backdrop opacity
|
||||
"view": "" // add a custom view
|
||||
"customProperties" : {} // add any custom properties needed for the custom view
|
||||
"skipStepIfVisible": ".dashboard div [data-element='my-tour-button']" // if we can find this DOM element on the page then we will skip this step
|
||||
}
|
||||
</pre>
|
||||
|
||||
<h1><b>Adding tours to other parts of the Umbraco backoffice</b></h1>
|
||||
It is also possible to add a list of custom tours to other parts of the Umbraco backoffice,
|
||||
It is also possible to add a list of custom tours to other parts of the Umbraco backoffice,
|
||||
as an example on a Dashboard in a Custom section. You can then use the {@link umbraco.services.tourService tourService} to start and stop tours but you don't have to register them as part of the tour service.
|
||||
|
||||
<h1><b>Using the tour service</b></h1>
|
||||
@@ -86,7 +87,8 @@ as an example on a Dashboard in a Custom section. You can then use the {@link um
|
||||
"element": "[data-element='my-tour-button']",
|
||||
"title": "Click the button",
|
||||
"content": "Click the button",
|
||||
"event": "click"
|
||||
"event": "click",
|
||||
"skipStepIfVisible": "[data-element='my-other-tour-button']"
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -257,9 +259,27 @@ In the following example you see how to run some custom logic before a step goes
|
||||
|
||||
// make sure we don't go too far
|
||||
if (scope.model.currentStepIndex !== scope.model.steps.length) {
|
||||
|
||||
var upcomingStep = scope.model.steps[scope.model.currentStepIndex];
|
||||
|
||||
// If the currentStep JSON object has 'skipStepIfVisible'
|
||||
// It's a DOM selector - if we find it then we ship over this step
|
||||
if (upcomingStep.skipStepIfVisible) {
|
||||
let tryFindDomEl = document.querySelector(upcomingStep.skipStepIfVisible);
|
||||
if (tryFindDomEl) {
|
||||
// check if element is visible:
|
||||
if( tryFindDomEl.offsetWidth || tryFindDomEl.offsetHeight || tryFindDomEl.getClientRects().length ) {
|
||||
// if it was visible then we skip the step.
|
||||
nextStep();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startStep();
|
||||
// tour completed - final step
|
||||
} else {
|
||||
// tour completed - final step
|
||||
scope.loadingStep = true;
|
||||
|
||||
waitForPendingRerequests().then(function () {
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
vm.selectApp = selectApp;
|
||||
vm.selectAppAnchor = selectAppAnchor;
|
||||
|
||||
vm.getScope = getScope;// used by property editors to get a scope that is the root of split view, content apps etc.
|
||||
|
||||
//Used to track how many content views there are (for split view there will be 2, it could support more in theory)
|
||||
vm.editors = [];
|
||||
|
||||
@@ -243,6 +245,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
function getScope() {
|
||||
return $scope;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('umbraco.directives').component('umbVariantContentEditors', umbVariantContentEditors);
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Some property editors need to performe an action after all property editors have reacted to the formSubmitting.
|
||||
$scope.$broadcast("postFormSubmitting", { scope: $scope });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
@ngdoc service
|
||||
* @ngdoc service
|
||||
* @name umbraco.services.blockEditorService
|
||||
*
|
||||
* @description
|
||||
* <b>Added in Umbraco 8.7</b>. Service for dealing with Block Editors.
|
||||
*
|
||||
* Block Editor Service provides the basic features for a block editor.
|
||||
* The main feature is the ability to create a Model Object which takes care of your data for your Block Editor.
|
||||
*
|
||||
@@ -13,526 +15,42 @@
|
||||
*
|
||||
* <pre>
|
||||
* modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope);
|
||||
* modelObject.loadScaffolding().then(onLoaded);
|
||||
* modelObject.load().then(onLoaded);
|
||||
* </pre>
|
||||
*
|
||||
* ####Use the Model Object to retrive Content Models for the blocks you want to edit. Content Models contains all the data about the properties of that content and as well handles syncroniztion so your data is always up-to-date.
|
||||
*
|
||||
* <pre>
|
||||
* // Store a reference to the layout model, because we need to maintain this model.
|
||||
* var layout = modelObject.getLayout();
|
||||
*
|
||||
* // maps layout entries to editor friendly models aka. BlockModels.
|
||||
* layout.forEach(entry => {
|
||||
* var block = modelObject.getBlockModel(entry);
|
||||
* // If this entry was not supported by our property-editor it would return 'null'.
|
||||
* if(block !== null) {
|
||||
* // store this block in an array we use in our view.
|
||||
* vm.myViewModelOfBlocks.push(block);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* ####Use the Model Object to retrive Content Models for the blocks you want to edit. Content Models contains all the data about the properties of that content and as well handles syncroniztion so your data is always up-to-date.
|
||||
*
|
||||
* <pre>
|
||||
* function addNewBlock(index, contentTypeKey) {
|
||||
*
|
||||
* // Create layout entry. (not added to property model jet.)
|
||||
* var layoutEntry = modelObject.create(contentTypeKey);
|
||||
* if (layoutEntry === null) {
|
||||
* return false;
|
||||
* }
|
||||
*
|
||||
* // make block model
|
||||
* var blockModel = getBlockModel(layoutEntry);
|
||||
* if (blockModel === null) {
|
||||
* return false;
|
||||
* }
|
||||
*
|
||||
* // If we reach this line, we are good to add the layoutEntry and blockModel to layout model and view model.
|
||||
* // add layout entry at the decired location in layout, depending on your layout structure.
|
||||
* layout.splice(index, 0, layoutEntry);
|
||||
*
|
||||
* // apply block model at decired location in our model used for the view.
|
||||
* vm.myViewModelOfBlocks.splice(index, 0, blockModel);
|
||||
*
|
||||
* // at this stage we know everything went well.
|
||||
* return true;
|
||||
* }
|
||||
* </pre>
|
||||
* See {@link umbraco.services.blockEditorModelObject BlockEditorModelObject} for more samples.
|
||||
*
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
|
||||
function blockEditorService($interpolate, udiService, contentResource) {
|
||||
|
||||
function blockEditorService(blockEditorModelObject) {
|
||||
|
||||
/**
|
||||
* Simple mapping from property model content entry to editing model,
|
||||
* needs to stay simple to avoid deep watching.
|
||||
* @ngdocs function
|
||||
* @name createModelObject
|
||||
* @methodOf umbraco.services.blockEditorService
|
||||
*
|
||||
* @description
|
||||
* Create a new Block Editor Model Object.
|
||||
* See {@link umbraco.services.blockEditorModelObject blockEditorModelObject}
|
||||
*
|
||||
* @see umbraco.services.blockEditorModelObject
|
||||
* @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} propertyEditorScope A local angularJS scope that represents the property editors scope.
|
||||
* @return {blockEditorModelObject} A instance of the BlockEditorModelObject class.
|
||||
*/
|
||||
function mapToElementModel(elementModel, dataModel) {
|
||||
|
||||
var variant = elementModel.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 (dataModel[prop.alias]) {
|
||||
prop.value = dataModel[prop.alias];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple mapping from elementModel to property model content entry,
|
||||
* needs to stay simple to avoid deep watching.
|
||||
*/
|
||||
function mapToPropertyModel(elementModel, dataModel) {
|
||||
|
||||
var variant = elementModel.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 (prop.value) {
|
||||
dataModel[prop.alias] = prop.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapValueToPropertyModel(value, alias, dataModel) {
|
||||
dataModel[alias] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function mapElementValues(fromModel, toModel) {
|
||||
if (!fromModel || !fromModel.variants) {
|
||||
toModel.variants = null;
|
||||
return;
|
||||
}
|
||||
if (!fromModel.variants.length) {
|
||||
toModel.variants = [];
|
||||
return;
|
||||
}
|
||||
|
||||
var fromVariant = fromModel.variants[0];
|
||||
if (!fromVariant) {
|
||||
toModel.variants = [null];
|
||||
return;
|
||||
}
|
||||
|
||||
var toVariant = toModel.variants[0];
|
||||
|
||||
for (var t = 0; t < fromVariant.tabs.length; t++) {
|
||||
var fromTab = fromVariant.tabs[t];
|
||||
var toTab = toVariant.tabs[t];
|
||||
|
||||
for (var p = 0; p < fromTab.properties.length; p++) {
|
||||
var fromProp = fromTab.properties[p];
|
||||
var toProp = toTab.properties[p];
|
||||
toProp.value = fromProp.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getBlockLabel(blockModel) {
|
||||
if(blockModel.labelInterpolator !== undefined) {
|
||||
// We are just using the data model, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.)
|
||||
return blockModel.labelInterpolator(blockModel.data);
|
||||
}
|
||||
return blockModel.content.contentTypeName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to add watchers on all properties in a content or settings model
|
||||
*/
|
||||
function addWatchers(blockModel, isolatedScope, forSettings) {
|
||||
var model = forSettings ? blockModel.settings : blockModel.content;
|
||||
if (!model || !model.variants || !model.variants.length) { return; }
|
||||
|
||||
// Start watching each property value.
|
||||
var variant = model.variants[0];
|
||||
var field = forSettings ? "settings" : "content";
|
||||
var watcherCreator = forSettings ? createSettingsModelPropWatcher : createDataModelPropWatcher;
|
||||
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];
|
||||
|
||||
// Watch value of property since this is the only value we want to keep synced.
|
||||
// Do notice that it is not performing a deep watch, meaning that we are only watching primatives and changes directly to the object of property-value.
|
||||
// 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.
|
||||
blockModel.watchers.push(isolatedScope.$watch("blockModels._" + blockModel.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockModel, prop)));
|
||||
}
|
||||
}
|
||||
if (blockModel.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.
|
||||
blockModel.updateLabel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to create a scoped watcher for a content property on a blockModel.
|
||||
*/
|
||||
function createDataModelPropWatcher(blockModel, prop) {
|
||||
return function() {
|
||||
// sync data:
|
||||
blockModel.data[prop.alias] = prop.value;
|
||||
|
||||
blockModel.updateLabel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to create a scoped watcher for a settings property on a blockModel.
|
||||
*/
|
||||
function createSettingsModelPropWatcher(blockModel, prop) {
|
||||
return function() {
|
||||
// sync data:
|
||||
blockModel.layout.settings[prop.alias] = prop.value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property.
|
||||
*/
|
||||
var notSupportedProperties = [
|
||||
"Umbraco.Tags",
|
||||
"Umbraco.UploadField",
|
||||
"Umbraco.ImageCropper"
|
||||
];
|
||||
function replaceUnsupportedProperties(scaffold) {
|
||||
scaffold.variants.forEach((variant) => {
|
||||
variant.tabs.forEach((tab) => {
|
||||
tab.properties.forEach((property) => {
|
||||
if (notSupportedProperties.indexOf(property.editor) !== -1) {
|
||||
property.view = "notsupported";
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return scaffold;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc factory
|
||||
* @name umbraco.factory.BlockEditorModelObject
|
||||
* @description A model object used to handle Block Editor data.
|
||||
**/
|
||||
function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) {
|
||||
|
||||
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 = [];
|
||||
|
||||
this.watchers = [];
|
||||
|
||||
this.isolatedScope = propertyScope.$new(true);
|
||||
this.isolatedScope.blockModels = {};
|
||||
|
||||
this.isolatedScope.$on("$destroy", this.onDestroyed.bind(this));
|
||||
|
||||
};
|
||||
|
||||
BlockEditorModelObject.prototype = {
|
||||
|
||||
getBlockConfiguration: function(key) {
|
||||
return this.blockConfigurations.find(bc => bc.contentTypeKey === key);
|
||||
},
|
||||
|
||||
loadScaffolding: function() {
|
||||
var tasks = [];
|
||||
|
||||
var scaffoldKeys = [];
|
||||
|
||||
this.blockConfigurations.forEach(blockConfiguration => {
|
||||
scaffoldKeys.push(blockConfiguration.contentTypeKey);
|
||||
if (blockConfiguration.settingsElementTypeKey != null) {
|
||||
scaffoldKeys.push(blockConfiguration.settingsElementTypeKey);
|
||||
}
|
||||
});
|
||||
|
||||
// removing dublicates.
|
||||
scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index);
|
||||
|
||||
scaffoldKeys.forEach((contentTypeKey => {
|
||||
tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => {
|
||||
this.scaffolds.push(replaceUnsupportedProperties(scaffold));
|
||||
}));
|
||||
}));
|
||||
|
||||
return Promise.all(tasks);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings.
|
||||
* @return {Array} array of strings representing alias.
|
||||
*/
|
||||
getAvailableAliasesForBlockContent: function() {
|
||||
return this.blockConfigurations.map(blockConfiguration => getScaffoldFor(blockConfiguration.contentTypeKey).contentTypeKey);
|
||||
},
|
||||
|
||||
getAvailableBlocksForBlockPicker: function() {
|
||||
|
||||
var blocks = [];
|
||||
|
||||
this.blockConfigurations.forEach(blockConfiguration => {
|
||||
var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeKey);
|
||||
if(scaffold) {
|
||||
blocks.push({
|
||||
blockConfigModel: blockConfiguration,
|
||||
elementTypeModel: scaffold.documentType
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return blocks;
|
||||
},
|
||||
|
||||
getScaffoldFor: function(contentTypeKey) {
|
||||
return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve editor friendly model of a block.
|
||||
* @param {Object} layoutEntry the layout entry to build the block model from.
|
||||
* @return {Object} Scaffolded Block Content object.
|
||||
*/
|
||||
getBlockModel: function(layoutEntry) {
|
||||
|
||||
var udi = layoutEntry.udi;
|
||||
|
||||
var dataModel = this._getDataByUdi(udi);
|
||||
|
||||
if (dataModel === null) {
|
||||
console.error("Couldnt find content model of " + udi)
|
||||
return null;
|
||||
}
|
||||
|
||||
var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var blockModel = {};
|
||||
blockModel.key = String.CreateGuid().replace(/-/g, "");
|
||||
blockModel.config = Utilities.copy(blockConfiguration);
|
||||
if (blockModel.config.label && blockModel.config.label !== "") {
|
||||
blockModel.labelInterpolator = $interpolate(blockModel.config.label);
|
||||
}
|
||||
blockModel.__scope = this.isolatedScope;
|
||||
blockModel.updateLabel = _.debounce(function () {this.__scope.$evalAsync(function() {
|
||||
this.label = getBlockLabel(this);
|
||||
}.bind(this))}.bind(blockModel), 10);
|
||||
|
||||
var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeKey);
|
||||
if(contentScaffold === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// make basics from scaffold
|
||||
blockModel.content = Utilities.copy(contentScaffold);
|
||||
blockModel.content.udi = udi;
|
||||
|
||||
mapToElementModel(blockModel.content, dataModel);
|
||||
|
||||
blockModel.data = dataModel;
|
||||
blockModel.layout = layoutEntry;
|
||||
blockModel.watchers = [];
|
||||
|
||||
if (blockConfiguration.settingsElementTypeKey) {
|
||||
var settingsScaffold = this.getScaffoldFor(blockConfiguration.settingsElementTypeKey);
|
||||
if (settingsScaffold === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// make basics from scaffold
|
||||
blockModel.settings = Utilities.copy(settingsScaffold);
|
||||
layoutEntry.settings = layoutEntry.settings || {};
|
||||
if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); }
|
||||
if (!layoutEntry.settings.contentTypeKey) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; }
|
||||
mapToElementModel(blockModel.settings, layoutEntry.settings);
|
||||
} else {
|
||||
layoutEntry.settings = null;
|
||||
}
|
||||
|
||||
// Add blockModel to our isolated scope to enable watching its values:
|
||||
this.isolatedScope.blockModels["_"+blockModel.key] = blockModel;
|
||||
addWatchers(blockModel, this.isolatedScope);
|
||||
addWatchers(blockModel, this.isolatedScope, true);
|
||||
|
||||
return blockModel;
|
||||
|
||||
},
|
||||
|
||||
removeDataAndDestroyModel: function (blockModel) {
|
||||
this.destroyBlockModel(blockModel);
|
||||
this.removeDataByUdi(blockModel.content.udi);
|
||||
},
|
||||
|
||||
destroyBlockModel: function(blockModel) {
|
||||
|
||||
// remove property value watchers:
|
||||
blockModel.watchers.forEach(w => { w(); });
|
||||
|
||||
// remove model from isolatedScope.
|
||||
delete this.isolatedScope.blockModels[blockModel.key];
|
||||
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve block model of a layout entry
|
||||
* @return {Object} Scaffolded Block Content object.
|
||||
*/
|
||||
setDataFromBlockModel: function(blockModel) {
|
||||
|
||||
var udi = blockModel.content.key;
|
||||
|
||||
mapToPropertyModel(blockModel.content, blockModel.data);
|
||||
|
||||
// TODO: implement settings, sync settings to layout entry.
|
||||
// mapToPropertyModel(blockModel.settings, blockModel.layout.settings)
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the layout object for this specific property editor.
|
||||
* @return {Object} Layout object.
|
||||
*/
|
||||
getLayout: function() {
|
||||
if (!this.value.layout[this.propertyEditorAlias]) {
|
||||
this.value.layout[this.propertyEditorAlias] = [];
|
||||
}
|
||||
return this.value.layout[this.propertyEditorAlias];
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a empty layout entry
|
||||
* @param {Object} blockConfiguration
|
||||
* @return {Object} Layout entry object, to be inserted at a decired location in the layout object.
|
||||
*/
|
||||
create: function(contentTypeKey) {
|
||||
|
||||
var blockConfiguration = this.getBlockConfiguration(contentTypeKey);
|
||||
if(blockConfiguration === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var entry = {
|
||||
udi: this._createDataEntry(contentTypeKey)
|
||||
}
|
||||
|
||||
if (blockConfiguration.settingsElementTypeKey != null) {
|
||||
entry.settings = { key: String.CreateGuid(), contentTypeKey: blockConfiguration.settingsElementTypeKey };
|
||||
}
|
||||
|
||||
return entry;
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert data from ElementType Model
|
||||
* @return {Object} Layout entry object, to be inserted at a decired location in the layout object.
|
||||
*/
|
||||
createFromElementType: function(elementTypeDataModel) {
|
||||
|
||||
elementTypeDataModel = Utilities.copy(elementTypeDataModel);
|
||||
|
||||
var contentTypeKey = elementTypeDataModel.contentTypeKey;
|
||||
|
||||
var layoutEntry = this.create(contentTypeKey);
|
||||
if(layoutEntry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var dataModel = this._getDataByUdi(layoutEntry.udi);
|
||||
if(dataModel === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
mapToPropertyModel(elementTypeDataModel, dataModel);
|
||||
|
||||
return layoutEntry;
|
||||
|
||||
},
|
||||
|
||||
// private
|
||||
_createDataEntry: function(elementTypeKey) {
|
||||
var content = {
|
||||
contentTypeKey: elementTypeKey,
|
||||
udi: udiService.create("element")
|
||||
};
|
||||
this.value.data.push(content);
|
||||
return content.udi;
|
||||
},
|
||||
// private
|
||||
_getDataByUdi: function(udi) {
|
||||
return this.value.data.find(entry => entry.udi === udi) || null;
|
||||
},
|
||||
|
||||
removeDataByUdi: function(udi) {
|
||||
const index = this.value.data.findIndex(o => o.udi === udi);
|
||||
if (index !== -1) {
|
||||
this.value.data.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
onDestroyed: function() {
|
||||
|
||||
for (const key in this.isolatedScope.blockModels) {
|
||||
this.destroyBlockModel(this.isolatedScope.blockModels[key]);
|
||||
}
|
||||
|
||||
delete this.value;
|
||||
delete this.propertyEditorAlias;
|
||||
delete this.blockConfigurations;
|
||||
delete this.scaffolds;
|
||||
delete this.watchers;
|
||||
this.isolatedScope.$destroy();
|
||||
delete this.isolatedScope;
|
||||
}
|
||||
function createModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope) {
|
||||
return new blockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope);
|
||||
}
|
||||
|
||||
return {
|
||||
createModelObject: function(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) {
|
||||
return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope);
|
||||
},
|
||||
mapElementValues: mapElementValues,
|
||||
getBlockLabel: getBlockLabel
|
||||
createModelObject: createModelObject
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,833 @@
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name umbraco.services.blockEditorModelObject
|
||||
*
|
||||
* @description
|
||||
* <b>Added in Umbraco 8.7</b>. Model Object for dealing with data of Block Editors.
|
||||
*
|
||||
* Block Editor Model Object provides the basic features for editing data of a block editor.<br/>
|
||||
* Use the Block Editor Service to instantiate the Model Object.<br/>
|
||||
* See {@link umbraco.services.blockEditorService blockEditorService}
|
||||
*
|
||||
* ## <b>Basic knowledge for understanding how to work with Block Editor data.</b>
|
||||
* There is a few things we need to understand before we can use the Model Object(BlockEditorModelObject).
|
||||
* The data structure of a Block Editor contains two main properties 'layout' and 'data'.
|
||||
* - The 'layout' property is the origin of the data, this object defines the blocks of this property including the the order and layout of those.
|
||||
* - The 'data' property is the data of your blocks and is managed by the Model Object therefor it can be ignored for most use.
|
||||
*
|
||||
* To get a better understanding of what this means as a user of the Model Object, we need to look at some simple usages of the Model Object:
|
||||
*
|
||||
* ## <b>Maintain and work with the Layout of a Block Editor.</b>
|
||||
* The 'layout' of a Block Editor can be of any structure. Therefor the Model Object(BlockEditorModelObject) cannot maintain this data.
|
||||
* Since the origin of blocks is in the 'layout' the Model Object only can serve as a helper to maintain and create data.
|
||||
* Therefor the Property Editor code will be using the 'layout' as origin, using the Model Object help managing speicfic parts.<br/>
|
||||
* To give an unstanding of what that means please read the following documentation of how to create a block.
|
||||
*
|
||||
* ## <b>The basic setup for a Block Editor.</b>
|
||||
* ####Instantiate a Model Object and load dependencies. And provide the basic structure for the 'layout' property when reciving the reference to it:
|
||||
*
|
||||
* <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, scopeOfExistence);
|
||||
* modelObject.load().then(onLoaded);
|
||||
* }
|
||||
*
|
||||
* function onLoaded() {
|
||||
*
|
||||
* // Define the default layout, this is used when there is no data jet stored for this property.
|
||||
* var defaultLayout = [];
|
||||
*
|
||||
* // We store a reference to layout as we have to maintain this.
|
||||
* layout = modelObject.getLayout(defaultLayout);
|
||||
*
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* ## <b>Create a Block.</b>
|
||||
* Use the Model Object to create a Block and append the returned layout-entry to the 'layout'.
|
||||
*
|
||||
* #### In the following example we will create a new block and append it at the decidered location in the 'layout' object:
|
||||
*
|
||||
* <pre>
|
||||
* // continuing from previous example.
|
||||
*
|
||||
* // Creates a block and returns a layout entry. The layout entry is not part of layout jet as its not managed by the Model Object.
|
||||
* var layoutEntry = modelObject.create(contentTypeKey);
|
||||
* if (layoutEntry === null) {
|
||||
* // The creation was not successful, therefore exit and without appending anything to our 'layout' object.
|
||||
* return false;
|
||||
* }
|
||||
*
|
||||
* // If we reach this line, we are good to add the layoutEntry to layout model.
|
||||
* // In this example our layout is an array and we would like to append the new block as the last entry.
|
||||
* layout.push(layoutEntry);
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* ## <b>Working with Blocks</b>
|
||||
*
|
||||
* The layout-entries does not provide much value when it comes to displaying or editing Blocks.
|
||||
* Our Model Object provides the option to get a Block Object for a given Block. Retrived by parsing the layout-entry of the block we would like.
|
||||
* The Block Object provides data of interest, the most important of these properties are: Block configuration, A label and the Block content in the Element Type Data Model format, this Content-model is very usefull to make UI for editing the Content of a Block.
|
||||
*
|
||||
* #### This example uses the Model Object to retrive a Block Object for outputting its label in the console.<br/>
|
||||
*
|
||||
* <pre>
|
||||
* // We store blocks in the layout
|
||||
* var layout = modelObject.getLayout([]);
|
||||
*
|
||||
* if (layout.length > 0) {
|
||||
*
|
||||
* // Get first entry of from the layout, which is an array in this sample.
|
||||
* var firstLayoutEntry = layout[0];
|
||||
*
|
||||
* // Create a Block Object for that entry.
|
||||
* var block = modelObject.getBlockObject(firstLayoutEntry);
|
||||
*
|
||||
* // Check if the Block Object creation went well. (If a block isnt supported by the configuration of the Property Editor)
|
||||
* if(block !== null) {
|
||||
* console.log(block.label);
|
||||
* }
|
||||
*
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* #### This similar example uses the Block Object for settings a value on the first property in the Blocks Content.<br/>
|
||||
*
|
||||
* <pre>
|
||||
* // We store blocks in the layout
|
||||
* var layout = modelObject.getLayout([]);
|
||||
*
|
||||
* if (layout.length > 0) {
|
||||
*
|
||||
* // Get first entry of from the layout, which is an array in this sample.
|
||||
* var firstLayoutEntry = layout[0];
|
||||
*
|
||||
* // Create a Block Object for that entry.
|
||||
* var block = modelObject.getBlockObject(firstLayoutEntry);
|
||||
*
|
||||
* // Check if the Block Object creation went well. (If a block isnt supported by the configuration of the Property Editor)
|
||||
* if(block !== null) {
|
||||
* block.content.variants[0].tabs[0].properties[0].value = "Hello world";// This value will automaticly be synced to the Property Editors Data Model.
|
||||
* }
|
||||
*
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* See {@link umbraco.services.blockEditorModelObject#methods_getBlockObject getBlockObject} method for more information on the properties avaiable on a Block Object.
|
||||
*
|
||||
* ## <b>Remove a Block</b>
|
||||
*
|
||||
* Removing a Block and destroying the data of it is done by calling one method of the Model Object, but we have remember that we need to maintain the 'layout' object and this case is a great example of how thats done.
|
||||
* You will find that your code will very much be based on working with Block Objects and therefor removal of a Block is be done by refering a Block Object.
|
||||
*
|
||||
* #### This example shows how to remove the first Block of our imaginary Block Editor and removing the block from our layout.
|
||||
*
|
||||
* <pre>
|
||||
* var layout = modelObject.getLayout([]);
|
||||
* if (layout.length > 0) {
|
||||
*
|
||||
* // Get first entry of from the layout, which is an array in this sample.
|
||||
* var firstLayoutEntry = layout[0];
|
||||
*
|
||||
* // Create a Block Object for that entry.
|
||||
* var block = modelObject.getBlockObject(firstLayoutEntry);
|
||||
*
|
||||
* // Check if the Block Object creation went well. (If a block isnt supported by the configuration of the Property Editor)
|
||||
* if(block !== null) {
|
||||
* modelObject.removeDataAndDestroyModel(block);// Removing the data of our block and destroying the Block Object for performance reasons.
|
||||
*
|
||||
* // We need to maintain the 'layout' object, so therefor its up to our code to remove the block from the 'layout' object.
|
||||
* const index = array.indexOf(5);
|
||||
* if (index > -1) {
|
||||
* layout.splice(index, 1);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* }
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
* ## <b>Manage a Render Model for Displaying Blocks in the Property Editor</b>
|
||||
*
|
||||
* For Rendering a Block in our AngularJS view
|
||||
*
|
||||
* <pre>
|
||||
* // TODO to be done.
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
|
||||
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++) {
|
||||
var tab = variant.tabs[t];
|
||||
|
||||
for (var p = 0; p < tab.properties.length; p++) {
|
||||
var prop = tab.properties[p];
|
||||
if (dataModel[prop.alias]) {
|
||||
prop.value = dataModel[prop.alias];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple mapping from elementModel to property model content entry,
|
||||
* needs to stay simple to avoid deep watching.
|
||||
*/
|
||||
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++) {
|
||||
var tab = variant.tabs[t];
|
||||
|
||||
for (var p = 0; p < tab.properties.length; p++) {
|
||||
var prop = tab.properties[p];
|
||||
if (prop.value) {
|
||||
dataModel[prop.alias] = prop.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function mapElementValues(fromModel, toModel) {
|
||||
if (!fromModel || !fromModel.variants) {
|
||||
toModel.variants = null;
|
||||
return;
|
||||
}
|
||||
if (!fromModel.variants.length) {
|
||||
toModel.variants = [];
|
||||
return;
|
||||
}
|
||||
|
||||
var fromVariant = fromModel.variants[0];
|
||||
if (!fromVariant) {
|
||||
toModel.variants = [null];
|
||||
return;
|
||||
}
|
||||
|
||||
var toVariant = toModel.variants[0];
|
||||
|
||||
for (var t = 0; t < fromVariant.tabs.length; t++) {
|
||||
var fromTab = fromVariant.tabs[t];
|
||||
var toTab = toVariant.tabs[t];
|
||||
|
||||
for (var p = 0; p < fromTab.properties.length; p++) {
|
||||
var fromProp = fromTab.properties[p];
|
||||
var toProp = toTab.properties[p];
|
||||
toProp.value = fromProp.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName.
|
||||
* @param {Object} blockObject BlockObject to recive data values from.
|
||||
*/
|
||||
function getBlockLabel(blockObject) {
|
||||
if(blockObject.labelInterpolator !== undefined) {
|
||||
// We are just using the data model, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.)
|
||||
return blockObject.labelInterpolator(blockObject.data);
|
||||
}
|
||||
return blockObject.content.contentTypeName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to add watchers on all properties in a content or settings model
|
||||
*/
|
||||
function addWatchers(blockObject, isolatedScope, forSettings) {
|
||||
var model = forSettings ? blockObject.settings : blockObject.content;
|
||||
if (!model || !model.variants || !model.variants.length) { return; }
|
||||
|
||||
// Start watching each property value.
|
||||
var variant = model.variants[0];
|
||||
var field = forSettings ? "settings" : "content";
|
||||
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++) {
|
||||
var prop = tab.properties[p];
|
||||
|
||||
// Watch value of property since this is the only value we want to keep synced.
|
||||
// Do notice that it is not performing a deep watch, meaning that we are only watching primatives and changes directly to the object of property-value.
|
||||
// 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) {
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 createContentModelPropWatcher(blockObject, prop) {
|
||||
return function() {
|
||||
// sync data:
|
||||
blockObject.data[prop.alias] = prop.value;
|
||||
|
||||
blockObject.updateLabel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to create a scoped watcher for a settings property on a blockObject.
|
||||
*/
|
||||
function createSettingsModelPropWatcher(blockObject, prop) {
|
||||
return function() {
|
||||
// sync data:
|
||||
blockObject.layout.settings[prop.alias] = prop.value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property.
|
||||
*/
|
||||
var notSupportedProperties = [
|
||||
"Umbraco.Tags",
|
||||
"Umbraco.UploadField",
|
||||
"Umbraco.ImageCropper"
|
||||
];
|
||||
function replaceUnsupportedProperties(scaffold) {
|
||||
scaffold.variants.forEach((variant) => {
|
||||
variant.tabs.forEach((tab) => {
|
||||
tab.properties.forEach((property) => {
|
||||
if (notSupportedProperties.indexOf(property.editor) !== -1) {
|
||||
property.view = "notsupported";
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return scaffold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name constructor
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Constructor of the model object used to handle Block Editor data.
|
||||
* @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} propertyEditorScope A local angularJS scope that represents the property editors scope.
|
||||
* @returns {BlockEditorModelObject} A instance of BlockEditorModelObject.
|
||||
*/
|
||||
function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, 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.");
|
||||
}
|
||||
|
||||
this.__watchers = [];
|
||||
|
||||
// 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 = [];
|
||||
|
||||
this.isolatedScope = scopeOfExistance.$new(true);
|
||||
this.isolatedScope.blockObjects = {};
|
||||
|
||||
this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this)));
|
||||
|
||||
this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this)));
|
||||
|
||||
};
|
||||
|
||||
BlockEditorModelObject.prototype = {
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name getBlockConfiguration
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Get block configuration object for a given contentTypeKey.
|
||||
* @param {string} key contentTypeKey to recive the configuration model for.
|
||||
* @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentTypeKey isnt available in the current block configurations.
|
||||
*/
|
||||
getBlockConfiguration: function(key) {
|
||||
return this.blockConfigurations.find(bc => bc.contentTypeKey === key) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name load
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Load the scaffolding models for the given configuration, these are needed to provide usefull models for each block.
|
||||
* @param {Object} blockObject BlockObject to recive data values from.
|
||||
* @returns {Promise} A Promise object which resolves when all scaffold models are loaded.
|
||||
*/
|
||||
load: function() {
|
||||
var tasks = [];
|
||||
|
||||
var scaffoldKeys = [];
|
||||
|
||||
this.blockConfigurations.forEach(blockConfiguration => {
|
||||
scaffoldKeys.push(blockConfiguration.contentTypeKey);
|
||||
if (blockConfiguration.settingsElementTypeKey != null) {
|
||||
scaffoldKeys.push(blockConfiguration.settingsElementTypeKey);
|
||||
}
|
||||
});
|
||||
|
||||
// removing dublicates.
|
||||
scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index);
|
||||
|
||||
scaffoldKeys.forEach((contentTypeKey => {
|
||||
tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => {
|
||||
this.scaffolds.push(replaceUnsupportedProperties(scaffold));
|
||||
}));
|
||||
}));
|
||||
|
||||
return Promise.all(tasks);
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name getAvailableAliasesForBlockContent
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings.
|
||||
* @return {Array} array of strings representing alias.
|
||||
*/
|
||||
getAvailableAliasesForBlockContent: function() {
|
||||
return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeAlias);
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name getAvailableBlocksForBlockPicker
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Retrive a list of available blocks, the list containing object with the confirugation 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.
|
||||
*/
|
||||
getAvailableBlocksForBlockPicker: function() {
|
||||
|
||||
var blocks = [];
|
||||
|
||||
this.blockConfigurations.forEach(blockConfiguration => {
|
||||
var scaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
|
||||
if(scaffold) {
|
||||
blocks.push({
|
||||
blockConfigModel: blockConfiguration,
|
||||
elementTypeModel: scaffold.documentType
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return blocks;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name getScaffoldFromKey
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Get scaffold model for a given contentTypeKey.
|
||||
* @param {string} key contentTypeKey to recive 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) {
|
||||
return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey);
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @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.
|
||||
* @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context.
|
||||
*/
|
||||
getScaffoldFromAlias: function(contentTypeAlias) {
|
||||
return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias);
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name getBlockObject
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @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
|
||||
* - 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.
|
||||
* - 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 refernce to the layout entry from your property editor model.
|
||||
* @param {Object} layoutEntry the layout entry object to build the block model from.
|
||||
* @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasnt found for this block.
|
||||
*/
|
||||
getBlockObject: function(layoutEntry) {
|
||||
|
||||
var udi = layoutEntry.udi;
|
||||
|
||||
var dataModel = this._getDataByUdi(udi);
|
||||
|
||||
if (dataModel === null) {
|
||||
console.error("Couldnt find content model of " + udi)
|
||||
return null;
|
||||
}
|
||||
|
||||
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");
|
||||
} 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.");
|
||||
}
|
||||
}
|
||||
|
||||
if (blockConfiguration === null || contentScaffold === null) {
|
||||
|
||||
blockConfiguration = {
|
||||
label: "Unsupported Block",
|
||||
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 !== "") {
|
||||
blockObject.labelInterpolator = $interpolate(blockObject.config.label);
|
||||
}
|
||||
blockObject.__scope = this.isolatedScope;
|
||||
blockObject.updateLabel = _.debounce(function () {this.__scope.$evalAsync(function() {
|
||||
this.label = getBlockLabel(this);
|
||||
}.bind(this))}.bind(blockObject), 10);
|
||||
|
||||
// make basics from scaffold
|
||||
blockObject.content = Utilities.copy(contentScaffold);
|
||||
blockObject.content.udi = udi;
|
||||
|
||||
mapToElementModel(blockObject.content, dataModel);
|
||||
|
||||
blockObject.data = dataModel;
|
||||
blockObject.layout = layoutEntry;
|
||||
blockObject.__watchers = [];
|
||||
|
||||
if (blockConfiguration.settingsElementTypeKey) {
|
||||
var settingsScaffold = this.getScaffoldFromKey(blockConfiguration.settingsElementTypeKey);
|
||||
if (settingsScaffold !== null) {
|
||||
|
||||
layoutEntry.settings = layoutEntry.settings || {};
|
||||
|
||||
blockObject.settingsData = layoutEntry.settings;
|
||||
|
||||
// make basics from scaffold
|
||||
blockObject.settings = Utilities.copy(settingsScaffold);
|
||||
layoutEntry.settings = layoutEntry.settings || {};
|
||||
if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); }
|
||||
if (!layoutEntry.settings.contentTypeKey) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; }
|
||||
mapToElementModel(blockObject.settings, layoutEntry.settings);
|
||||
}
|
||||
}
|
||||
|
||||
blockObject.retriveValuesFrom = function(content, settings) {
|
||||
if (this.content !== null) {
|
||||
mapElementValues(content, this.content);
|
||||
}
|
||||
if (this.config.settingsElementTypeKey !== null) {
|
||||
mapElementValues(settings, this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
blockObject.sync = function() {
|
||||
if (this.content !== null) {
|
||||
mapToPropertyModel(this.content, this.data);
|
||||
}
|
||||
if (this.config.settingsElementTypeKey !== null) {
|
||||
mapToPropertyModel(this.settings, this.layout.settings);
|
||||
}
|
||||
}
|
||||
|
||||
// first time instant update of label.
|
||||
blockObject.label = getBlockLabel(blockObject);
|
||||
|
||||
// Add blockObject to our isolated scope to enable watching its values:
|
||||
this.isolatedScope.blockObjects["_" + blockObject.key] = blockObject;
|
||||
addWatchers(blockObject, this.isolatedScope);
|
||||
addWatchers(blockObject, this.isolatedScope, true);
|
||||
|
||||
blockObject.destroy = function() {
|
||||
// remove property value watchers:
|
||||
this.__watchers.forEach(w => { w(); });
|
||||
delete this.__watchers;
|
||||
|
||||
// help carbage collector:
|
||||
delete this.layout;
|
||||
delete this.data;
|
||||
|
||||
// remove model from isolatedScope.
|
||||
delete this.__scope.blockObjects["_" + this.key];
|
||||
delete this.__scope;
|
||||
|
||||
// removes this method, making it unposible to destroy again.
|
||||
delete this.destroy;
|
||||
}
|
||||
|
||||
return blockObject;
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name removeDataAndDestroyModel
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Removes the data and destroys the Block Model.
|
||||
* Notice this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure.
|
||||
* @param {Object} blockObject The BlockObject to be removed and destroyed.
|
||||
*/
|
||||
removeDataAndDestroyModel: function (blockObject) {
|
||||
this.destroyBlockObject(blockObject);
|
||||
this.removeDataByUdi(blockObject.content.udi);
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name destroyBlockObject
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Destroys the Block Model, but all data is kept.
|
||||
* @param {Object} blockObject The BlockObject to be destroyed.
|
||||
*/
|
||||
destroyBlockObject: function(blockObject) {
|
||||
blockObject.destroy();
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name getLayout
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Retrieve the layout object from this specific property editor model.
|
||||
* @param {object} defaultStructure if no data exist the layout of your poerty editor will be set to this object.
|
||||
* @return {Object} Layout object, structure depends on the model of your property editor.
|
||||
*/
|
||||
getLayout: function(defaultStructure) {
|
||||
if (!this.value.layout[this.propertyEditorAlias]) {
|
||||
this.value.layout[this.propertyEditorAlias] = defaultStructure;
|
||||
}
|
||||
return this.value.layout[this.propertyEditorAlias];
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name create
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Create a empty layout entry, notice the layout entry is not added to the property editors model layout object, since the layout sturcture depends on the property editor.
|
||||
* @param {string} contentTypeKey the contentTypeKey of the block you wish to create, if contentTypeKey is not avaiable in the block configuration then ´null´ will be returned.
|
||||
* @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentTypeKey is unavaiaible.
|
||||
*/
|
||||
create: function(contentTypeKey) {
|
||||
|
||||
var blockConfiguration = this.getBlockConfiguration(contentTypeKey);
|
||||
if(blockConfiguration === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var entry = {
|
||||
udi: this._createDataEntry(contentTypeKey)
|
||||
}
|
||||
|
||||
if (blockConfiguration.settingsElementTypeKey != null) {
|
||||
entry.settings = { key: String.CreateGuid(), contentTypeKey: blockConfiguration.settingsElementTypeKey };
|
||||
}
|
||||
|
||||
return entry;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name createFromElementType
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Insert data from ElementType Model
|
||||
* @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration.
|
||||
*/
|
||||
createFromElementType: function(elementTypeDataModel) {
|
||||
|
||||
elementTypeDataModel = Utilities.copy(elementTypeDataModel);
|
||||
|
||||
var contentTypeKey = elementTypeDataModel.contentTypeKey;
|
||||
|
||||
var layoutEntry = this.create(contentTypeKey);
|
||||
if(layoutEntry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var dataModel = this._getDataByUdi(layoutEntry.udi);
|
||||
if(dataModel === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
mapToPropertyModel(elementTypeDataModel, dataModel);
|
||||
|
||||
return layoutEntry;
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name sync
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Force immidiate update of the blockobject models to the property model.
|
||||
*/
|
||||
sync: function() {
|
||||
for (const key in this.isolatedScope.blockObjects) {
|
||||
this.isolatedScope.blockObjects[key].sync();
|
||||
}
|
||||
},
|
||||
|
||||
// private
|
||||
_createDataEntry: function(elementTypeKey) {
|
||||
var content = {
|
||||
contentTypeKey: elementTypeKey,
|
||||
udi: udiService.create("element")
|
||||
};
|
||||
this.value.data.push(content);
|
||||
return content.udi;
|
||||
},
|
||||
// private
|
||||
_getDataByUdi: function(udi) {
|
||||
return this.value.data.find(entry => entry.udi === udi) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name removeDataByUdi
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Removes the data of a given UDI.
|
||||
* Notice this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure.
|
||||
* @param {string} udi The UDI of the data to be removed.
|
||||
*/
|
||||
removeDataByUdi: function(udi) {
|
||||
const index = this.value.data.findIndex(o => o.udi === udi);
|
||||
if (index !== -1) {
|
||||
this.value.data.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name destroy
|
||||
* @methodOf umbraco.services.blockEditorModelObject
|
||||
* @description Notice you should not need to destroy the BlockEditorModelObject since it will automaticly be destroyed when the scope of existance gets destroyed.
|
||||
*/
|
||||
destroy: function() {
|
||||
|
||||
this.__watchers.forEach(w => { w(); });
|
||||
for (const key in this.isolatedScope.blockObjects) {
|
||||
this.destroyBlockObject(this.isolatedScope.blockObjects[key]);
|
||||
}
|
||||
|
||||
delete this.__watchers;
|
||||
delete this.value;
|
||||
delete this.propertyEditorAlias;
|
||||
delete this.blockConfigurations;
|
||||
delete this.scaffolds;
|
||||
this.isolatedScope.$destroy();
|
||||
delete this.isolatedScope;
|
||||
delete this.destroy;
|
||||
}
|
||||
}
|
||||
|
||||
return BlockEditorModelObject;
|
||||
}
|
||||
|
||||
angular.module('umbraco.services').service('blockEditorModelObject', blockEditorModelObjectFactory);
|
||||
|
||||
})();
|
||||
@@ -44,6 +44,9 @@ function formHelper(angularHelper, serverValidationManager, notificationsService
|
||||
//the first thing any form must do is broadcast the formSubmitting event
|
||||
args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action });
|
||||
|
||||
// Some property editors need to performe an action after all property editors have reacted to the formSubmitting.
|
||||
args.scope.$broadcast("postFormSubmitting", { scope: args.scope, action: args.action });
|
||||
|
||||
//then check if the form is valid
|
||||
if (!args.skipValidation) {
|
||||
if (currentForm.$invalid) {
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
@import "../views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less";
|
||||
@import "../views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less";
|
||||
@import "../views/propertyeditors/notsupported/notsupported.less";
|
||||
@import "../views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less";
|
||||
@import "../views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less";
|
||||
@import "../views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less";
|
||||
|
||||
|
||||
@@ -20,12 +20,18 @@
|
||||
justify-content: center;
|
||||
height: calc(~'@{editorHeaderHeight}'- ~'1px'); // need to offset the 1px border-bottom on .umb-editor-header - avoids overflowing top of the container
|
||||
color: @ui-active-type;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: @ui-active-type-hover !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
color: @gray-6;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
||||
@@ -48,6 +48,9 @@ label.umb-form-check--checkbox{
|
||||
&:checked ~ .umb-form-check__state .umb-form-check__check {
|
||||
border-color: @ui-option-type;
|
||||
}
|
||||
&[type='checkbox']:checked ~ .umb-form-check__state .umb-form-check__check {
|
||||
background-color: @ui-option-type;
|
||||
}
|
||||
&:checked:hover ~ .umb-form-check__state .umb-form-check__check {
|
||||
&::before {
|
||||
background: @ui-option-type-hover;
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
@ui-btn-positive-type: @white;
|
||||
|
||||
@ui-btn-negative: @red;
|
||||
@ui-btn-negative-type: @white;
|
||||
@ui-btn-negative-hover: @red;
|
||||
|
||||
@ui-icon: @blueNight;
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
//used for the media picker dialog
|
||||
angular.module("umbraco")
|
||||
.controller("Umbraco.Editors.BlockEditorController",
|
||||
function ($scope, localizationService) {
|
||||
function ($scope, localizationService, formHelper) {
|
||||
var vm = this;
|
||||
|
||||
vm.content = $scope.model.content;
|
||||
vm.settings = $scope.model.settings;
|
||||
|
||||
localizationService.localizeMany([
|
||||
$scope.model.liveEditing ? "prompt_discardChanges" : "general_close",
|
||||
$scope.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges"
|
||||
]).then(function (data) {
|
||||
vm.closeLabel = data[0];
|
||||
vm.submitLabel = data[1];
|
||||
});
|
||||
|
||||
vm.model = $scope.model;
|
||||
|
||||
vm.tabs = [];
|
||||
@@ -53,7 +61,9 @@ angular.module("umbraco")
|
||||
|
||||
vm.submitAndClose = function () {
|
||||
if ($scope.model && $scope.model.submit) {
|
||||
$scope.model.submit($scope.model);
|
||||
if (formHelper.submitForm({ scope: $scope })) {
|
||||
$scope.model.submit($scope.model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<umb-button
|
||||
action="vm.close()"
|
||||
button-style="link"
|
||||
label-key="general_close"
|
||||
label="{{vm.closeLabel}}"
|
||||
type="button">
|
||||
</umb-button>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
action="vm.submitAndClose()"
|
||||
button-style="primary"
|
||||
state="submitButtonState"
|
||||
label-key="buttons_submitChanges"
|
||||
label="{{vm.submitLabel}}"
|
||||
type="button">
|
||||
</umb-button>
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ angular.module("umbraco")
|
||||
"alias": "clipboard",
|
||||
"name": data[1],
|
||||
"icon": "icon-paste-in",
|
||||
"view": ""
|
||||
"view": "",
|
||||
"disabled": vm.model.clipboardItems.length === 0
|
||||
}];
|
||||
|
||||
vm.activeTab = vm.navigation[0];
|
||||
@@ -38,6 +39,7 @@ angular.module("umbraco")
|
||||
|
||||
vm.clickClearClipboard = function() {
|
||||
vm.onNavigationChanged(vm.navigation[0]);
|
||||
vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here.
|
||||
vm.model.clipboardItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually.
|
||||
vm.model.clickClearClipboard();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
hotkey="{{::vm.hotkey}}"
|
||||
hotkey-when-hidden="true"
|
||||
ng-class="{'is-active': vm.item.active, '-has-error': vm.item.hasError}"
|
||||
ng-disabled="vm.item.disabled"
|
||||
class="umb-sub-views-nav-item__action umb-outline umb-outline--thin">
|
||||
<i class="icon {{ vm.item.icon }}" aria-hidden="true"></i>
|
||||
<span class="umb-sub-views-nav-item-text">{{ vm.item.name }}</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
<div class="__showcase" ng-style="{'background-color':vm.blockConfigModel.backgroundColor, 'background-image':'url('+vm.blockConfigModel.thumbnail+')'}">
|
||||
<div class="__showcase" ng-style="{'background-color':vm.blockConfigModel.backgroundColor, 'background-image': vm.blockConfigModel.thumbnail ? 'url('+vm.blockConfigModel.thumbnail+')' : 'transparent'}">
|
||||
<i ng-if="vm.blockConfigModel.thumbnail == null && vm.elementTypeModel.icon" class="__icon {{ vm.elementTypeModel.icon }}" ng-style="{'color':vm.blockConfigModel.iconColor}" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="__info">
|
||||
|
||||
@@ -4,8 +4,6 @@ umb-block-card {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
background-color: white;
|
||||
border-radius: @doubleBorderRadius;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.2);
|
||||
@@ -57,7 +55,7 @@ umb-block-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 10/16*100%;
|
||||
background-color: @gray-11;
|
||||
background-color: @gray-12;
|
||||
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
|
||||
@@ -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,30 +18,33 @@
|
||||
<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="{'--active':layout.$block.active}">
|
||||
|
||||
<umb-nested-property property-type-alias="{{property.propertyAlias}}"
|
||||
<umb-nested-property property-type-alias="{{property.propertyAlias}}"
|
||||
element-type-index="{{$index}}">
|
||||
<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>
|
||||
<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>
|
||||
</umb-nested-property>
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
</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(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>
|
||||
@@ -55,24 +58,25 @@
|
||||
|
||||
<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.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>
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
},
|
||||
require: {
|
||||
umbProperty: "?^umbProperty",
|
||||
umbVariantContent: '?^^umbVariantContent'
|
||||
umbVariantContent: '?^^umbVariantContent',
|
||||
umbVariantContentEditors: '?^^umbVariantContentEditors'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,15 +37,22 @@
|
||||
var deleteAllBlocksAction;
|
||||
|
||||
var inlineEditing = false;
|
||||
var liveEditing = true;
|
||||
|
||||
var vm = this;
|
||||
|
||||
vm.loading = true;
|
||||
vm.moveFocusToBlock = null;
|
||||
vm.showCopy = clipboardService.isSupported();
|
||||
vm.currentBlockInFocus = null;
|
||||
vm.setBlockFocus = function(block) {
|
||||
if(vm.currentBlockInFocus !== null) {
|
||||
vm.currentBlockInFocus.focus = false;
|
||||
}
|
||||
vm.currentBlockInFocus = block;
|
||||
block.focus = true;
|
||||
}
|
||||
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 = {};
|
||||
@@ -57,6 +65,7 @@
|
||||
vm.$onInit = function() {
|
||||
|
||||
inlineEditing = vm.model.config.useInlineEditingAsDefault;
|
||||
liveEditing = vm.model.config.useLiveEditing;
|
||||
|
||||
vm.validationLimit = vm.model.config.validationLimit;
|
||||
|
||||
@@ -70,10 +79,15 @@
|
||||
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.loadScaffolding().then(onLoaded);
|
||||
modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope);
|
||||
modelObject.load().then(onLoaded);
|
||||
|
||||
copyAllBlocksAction = {
|
||||
labelKey: "clipboard_labelForCopyAllEntries",
|
||||
@@ -112,36 +126,48 @@
|
||||
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. BlockModels.
|
||||
layout.forEach(entry => {
|
||||
var block = getBlockModel(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) {
|
||||
var block = getBlockObject(entry);
|
||||
|
||||
// If this entry was not supported by our property-editor it would return 'null'.
|
||||
if(block !== null) {
|
||||
entry.$block = block;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
vm.availableContentTypesAliases = modelObject.getAvailableAliasForBlockContent();
|
||||
vm.availableContentTypesAliases = modelObject.getAvailableAliasesForBlockContent();
|
||||
vm.availableBlockTypes = modelObject.getAvailableBlocksForBlockPicker();
|
||||
|
||||
vm.loading = false;
|
||||
|
||||
|
||||
$scope.$evalAsync();
|
||||
|
||||
}
|
||||
|
||||
function getDefaultViewForBlock(block) {
|
||||
|
||||
if (block.config.unsupported === true)
|
||||
return "views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html";
|
||||
|
||||
function getBlockModel(entry) {
|
||||
var block = modelObject.getBlockModel(entry);
|
||||
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 blockModel.
|
||||
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;
|
||||
}
|
||||
@@ -156,21 +182,21 @@
|
||||
}
|
||||
|
||||
// make block model
|
||||
var blockModel = getBlockModel(layoutEntry);
|
||||
if (blockModel === null) {
|
||||
var blockObject = getBlockObject(layoutEntry);
|
||||
if (blockObject === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we reach this line, we are good to add the layoutEntry and blockModel to our models.
|
||||
// If we reach this line, we are good to add the layoutEntry and blockObject to our models.
|
||||
|
||||
// Add the Block Object to our layout entry.
|
||||
layoutEntry.$block = blockObject;
|
||||
|
||||
// add layout entry at the decired location in layout.
|
||||
layout.splice(index, 0, layoutEntry);
|
||||
|
||||
// apply block model at decired location in blocks.
|
||||
vm.blocks.splice(index, 0, blockModel);
|
||||
vm.layout.splice(index, 0, layoutEntry);
|
||||
|
||||
// lets move focus to this new block.
|
||||
vm.moveFocusToBlock = blockModel;
|
||||
vm.setBlockFocus(blockObject);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -180,61 +206,79 @@
|
||||
|
||||
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(blockModel, openSettings) {
|
||||
function editBlock(blockObject, openSettings) {
|
||||
|
||||
var wasNotActiveBefore = blockObject.active !== true;
|
||||
blockObject.active = true;
|
||||
|
||||
if (inlineEditing === true && openSettings !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// make a clone to avoid editing model directly.
|
||||
var blockContentClone = Utilities.copy(blockModel.content);
|
||||
var blockContentClone = Utilities.copy(blockObject.content);
|
||||
var blockSettingsClone = null;
|
||||
|
||||
if (blockModel.config.settingsElementTypeKey) {
|
||||
blockSettingsClone = Utilities.copy(blockModel.settings);
|
||||
if (blockObject.config.settingsElementTypeKey) {
|
||||
blockSettingsClone = Utilities.copy(blockObject.settings);
|
||||
}
|
||||
|
||||
var hideContent = (openSettings === true && inlineEditing === true);
|
||||
|
||||
var blockEditorModel = {
|
||||
content: blockContentClone,
|
||||
hideContent: hideContent,
|
||||
openSettings: openSettings === true,
|
||||
settings: blockSettingsClone,
|
||||
title: blockModel.label,
|
||||
liveEditing: liveEditing,
|
||||
title: blockObject.label,
|
||||
view: "views/common/infiniteeditors/blockeditor/blockeditor.html",
|
||||
size: blockModel.config.editorSize || "medium",
|
||||
size: blockObject.config.editorSize || "medium",
|
||||
submit: function(blockEditorModel) {
|
||||
// To ensure syncronization gets tricked we transfer each property.
|
||||
if (blockEditorModel.content !== null) {
|
||||
blockEditorService.mapElementValues(blockEditorModel.content, blockModel.content)
|
||||
}
|
||||
if (blockModel.config.settingsElementTypeKey !== null) {
|
||||
blockEditorService.mapElementValues(blockEditorModel.settings, blockModel.settings)
|
||||
|
||||
if (liveEditing === false) {
|
||||
// transfer values when submitting in none-liveediting mode.
|
||||
blockObject.retriveValuesFrom(blockEditorModel.content, blockEditorModel.settings);
|
||||
}
|
||||
|
||||
blockObject.active = false;
|
||||
editorService.close();
|
||||
},
|
||||
close: function() {
|
||||
|
||||
if (liveEditing === true) {
|
||||
// revert values when closing in liveediting mode.
|
||||
blockObject.retriveValuesFrom(blockContentClone, blockSettingsClone);
|
||||
}
|
||||
|
||||
if (wasNotActiveBefore === true) {
|
||||
blockObject.active = false;
|
||||
}
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
|
||||
if (liveEditing === true) {
|
||||
blockEditorModel.content = blockObject.content;
|
||||
blockEditorModel.settings = blockObject.settings;
|
||||
} else {
|
||||
blockEditorModel.content = blockContentClone;
|
||||
blockEditorModel.settings = blockSettingsClone;
|
||||
}
|
||||
|
||||
// open property settings editor
|
||||
editorService.open(blockEditorModel);
|
||||
}
|
||||
@@ -280,13 +324,18 @@
|
||||
}
|
||||
|
||||
if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) {
|
||||
blockPickerModel.close();
|
||||
if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.blocks.length > createIndex) {
|
||||
editBlock(vm.blocks[createIndex]);
|
||||
editorService.close();
|
||||
if (added && 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.layout.length) {
|
||||
vm.setBlockFocus(vm.layout[Math.max(createIndex-1, 0)].$block);
|
||||
}
|
||||
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
@@ -304,7 +353,7 @@
|
||||
{
|
||||
type: "elementType",
|
||||
pasteData: entry.data,
|
||||
blockConfigModel: modelObject.getScaffoldFor(entry.key),
|
||||
blockConfigModel: modelObject.getScaffoldFromAlias(entry.alias),
|
||||
elementTypeModel: {
|
||||
name: entry.label,
|
||||
icon: entry.icon
|
||||
@@ -338,9 +387,11 @@
|
||||
}
|
||||
|
||||
var requestCopyAllBlocks = function() {
|
||||
|
||||
var elementTypesToCopy = vm.layout.filter(entry => entry.$block.config.unsupported !== true).map(entry => entry.$block.content);
|
||||
|
||||
// list aliases
|
||||
var aliases = vm.blocks.map(block => block.content.contentTypeAlias);
|
||||
var aliases = elementTypesToCopy.map(content => content.contentTypeAlias);
|
||||
|
||||
// remove dublicates
|
||||
aliases = aliases.filter((item, index) => aliases.indexOf(item) === index);
|
||||
@@ -349,8 +400,7 @@
|
||||
if(vm.umbVariantContent) {
|
||||
contentNodeName = vm.umbVariantContent.editor.content.name;
|
||||
}
|
||||
|
||||
var elementTypesToCopy = vm.blocks.map(block => block.content);
|
||||
// TODO: check if we are in an overlay and then lets get the Label of this block.
|
||||
|
||||
localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) {
|
||||
clipboardService.copyArray("elementTypeArray", aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id);
|
||||
@@ -371,18 +421,18 @@
|
||||
}
|
||||
|
||||
// make block model
|
||||
var blockModel = getBlockModel(layoutEntry);
|
||||
if (blockModel === null) {
|
||||
var blockObject = getBlockObject(layoutEntry);
|
||||
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, blockModel);
|
||||
|
||||
vm.moveFocusToBlock = blockModel;
|
||||
vm.currentBlockInFocus = blockObject;
|
||||
|
||||
return true;
|
||||
|
||||
@@ -435,11 +485,6 @@
|
||||
openSettingsForBlock: openSettingsForBlock
|
||||
}
|
||||
|
||||
|
||||
|
||||
var runtimeSortVars = {};
|
||||
|
||||
vm.sorting = false;
|
||||
vm.sortableOptions = {
|
||||
axis: "y",
|
||||
cursor: "grabbing",
|
||||
@@ -449,30 +494,8 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -480,30 +503,22 @@
|
||||
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) {
|
||||
if (vm.validationLimit.min !== null && vm.blocks.length < vm.validationLimit.min) {
|
||||
vm.propertyForm.minCount.$setValidity("minCount", false);
|
||||
}
|
||||
else {
|
||||
vm.propertyForm.minCount.$setValidity("minCount", true);
|
||||
}
|
||||
if (vm.validationLimit.max !== null && vm.blocks.length > vm.validationLimit.max) {
|
||||
vm.propertyForm.maxCount.$setValidity("maxCount", false);
|
||||
}
|
||||
else {
|
||||
vm.propertyForm.maxCount.$setValidity("maxCount", true);
|
||||
}
|
||||
|
||||
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.layout.length <= vm.validationLimit.max;
|
||||
vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood);
|
||||
|
||||
}
|
||||
}
|
||||
unsubscribe.push($scope.$watch(() => vm.layout.length, onAmountOfBlocksChanged));
|
||||
|
||||
|
||||
|
||||
|
||||
unsubscribe.push($scope.$watch(() => vm.blocks.length, onAmountOfBlocksChanged));
|
||||
|
||||
$scope.$on("$destroy", function () {
|
||||
for (const subscription of unsubscribe) {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:focus-within,
|
||||
&.--open {
|
||||
&.--active {
|
||||
|
||||
> .umb-block-list__block--actions {
|
||||
opacity: 1;
|
||||
@@ -42,6 +42,7 @@
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
.action {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: @ui-action-discreet-type;
|
||||
font-size: 18px;
|
||||
@@ -137,6 +138,7 @@
|
||||
}
|
||||
}
|
||||
.umb-block-list__create-button {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function InlineBlockEditor() {
|
||||
function InlineBlockEditor($scope) {
|
||||
|
||||
const bc = this;
|
||||
|
||||
bc.openBlock = function(block) {
|
||||
block.isOpen = !block.isOpen;
|
||||
|
||||
// if we are closing:
|
||||
if (block.active === true) {
|
||||
// boardcast the formSubmitting event to trigger syncronization or none-live property-editors
|
||||
$scope.$broadcast("formSubmitting", { scope: $scope });
|
||||
// Some property editors need to performe an action after all property editors have reacted to the formSubmitting.
|
||||
$scope.$broadcast("postFormSubmitting", { scope: $scope });
|
||||
}
|
||||
|
||||
block.active = !block.active;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div class="blockelement-inlineblock-editor" ng-controller="Umbraco.PropertyEditors.BlockEditor.InlineBlockEditor as vm">
|
||||
<button type="button" class="btn-reset umb-outline blockelement__draggable-element" ng-click="vm.openBlock(block)">
|
||||
<button type="button" class="btn-reset umb-outline blockelement__draggable-element" ng-click="vm.openBlock(block)" ng-focus="block.focus">
|
||||
<span class="caret"></span>
|
||||
<i class="icon {{block.content.icon}}"></i>
|
||||
<span>{{block.label}}</span>
|
||||
</button>
|
||||
<div class="blockelement-inlineblock-editor__inner" ng-class="{'--singleGroup':block.content.variants[0].tabs.length === 1}" ng-if="block.isOpen === true">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
border-radius: @baseBorderRadius;
|
||||
transition: border-color 120ms;
|
||||
|
||||
.umb-block-list__block:not(.--open) &:hover {
|
||||
.umb-block-list__block:not(.--active) &:hover {
|
||||
border-color: @gray-8;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.umb-block-list__block.--open & {
|
||||
.umb-block-list__block.--active & {
|
||||
border-color: @gray-8;
|
||||
box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.05);
|
||||
> button {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<button type="button" class="btn-reset umb-outline blockelement-labelblock-editor blockelement__draggable-element" ng-click="api.editBlock(block)">
|
||||
<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}">
|
||||
<i class="icon {{block.content.icon}}"></i>
|
||||
<span>{{block.label}}</span>
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.blockelement-labelblock-editor {
|
||||
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
@@ -32,6 +33,12 @@
|
||||
|
||||
&:hover {
|
||||
color: @ui-action-discreet-type-hover;
|
||||
border-color: @gray-8;
|
||||
background-color: @ui-action-hover;
|
||||
}
|
||||
|
||||
&.--active {
|
||||
color: @ui-active-type;
|
||||
border-color: @ui-active;
|
||||
background-color: @ui-active;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="blockelement-unsupportedblock-editor blockelement__draggable-element" ng-focus="block.focus">
|
||||
<div class="__header">
|
||||
<i class="icon icon-alert"></i>
|
||||
<span>{{block.label}}</span>
|
||||
</div>
|
||||
<div class="__body">
|
||||
This Block is no longer supported in this context.<br/>
|
||||
You might want to remove this block, or contact your developer to take actions for making this block available again.<br/><br/>
|
||||
<a href="http://our.umbraco.com" target="_blank">Learn about this circumstance</a>
|
||||
<h5>Block data:</h5>
|
||||
<pre ng-bind="block.data | json : 4"></pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
.blockelement-unsupportedblock-editor {
|
||||
|
||||
position: relative;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 4px;
|
||||
margin-top: 4px;
|
||||
width: 100%;
|
||||
border: 1px solid @gray-9;
|
||||
border-radius: @baseBorderRadius;
|
||||
|
||||
> .__header {
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
padding-left: 20px;
|
||||
padding-bottom: 2px;
|
||||
min-height: 48px;
|
||||
border-bottom: 1px solid @gray-9;
|
||||
|
||||
background-color: @gray-11;
|
||||
color: @gray-6;
|
||||
|
||||
i {
|
||||
font-size: 22px;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
> .__body {
|
||||
|
||||
padding: 20px;
|
||||
|
||||
background-color: @gray-11;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: @ui-action-type;
|
||||
&:hover {
|
||||
color:@ui-action-type-hover;
|
||||
}
|
||||
}
|
||||
|
||||
pre {
|
||||
border: none;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -102,7 +102,7 @@
|
||||
createNewItem: {
|
||||
action: function() {
|
||||
overlayService.close();
|
||||
vm.createElementTypeAndAdd(vm.addBlockFromElementTypeKey);
|
||||
vm.createElementTypeAndCallback(vm.addBlockFromElementTypeKey);
|
||||
},
|
||||
icon: "icon-add",
|
||||
name: localized[1]
|
||||
@@ -124,7 +124,7 @@
|
||||
});
|
||||
};
|
||||
|
||||
vm.createElementTypeAndAdd = function(callback) {
|
||||
vm.createElementTypeAndCallback = function(callback) {
|
||||
const editor = {
|
||||
create: true,
|
||||
infiniteMode: true,
|
||||
@@ -146,9 +146,14 @@
|
||||
|
||||
var entry = {
|
||||
"contentTypeKey": key,
|
||||
"view": null,
|
||||
"settingsElementTypeKey": null,
|
||||
"labelTemplate": "",
|
||||
"settingsElementTypeKey": null
|
||||
"view": null,
|
||||
"stylesheet": null,
|
||||
"editorSize": "medium",
|
||||
"iconColor": null,
|
||||
"backgroundColor": null,
|
||||
"thumbnail": null
|
||||
};
|
||||
|
||||
$scope.model.value.push(entry);
|
||||
@@ -171,6 +176,7 @@
|
||||
view: "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html",
|
||||
size: "small",
|
||||
submit: function(overlayModel) {
|
||||
loadElementTypes()// lets load elementType again, to ensure we are up to date.
|
||||
TransferProperties(overlayModel.block, block);// transfer properties back to block object. (Doing this cause we dont know if block object is added to model jet, therefor we cant use index or replace the object.)
|
||||
overlayModel.close();
|
||||
},
|
||||
|
||||
@@ -44,6 +44,24 @@
|
||||
editorService.documentTypeEditor(editor);
|
||||
}
|
||||
|
||||
vm.createElementTypeAndCallback = function(callback) {
|
||||
const editor = {
|
||||
create: true,
|
||||
infiniteMode: true,
|
||||
isElement: true,
|
||||
submit: function (model) {
|
||||
loadElementTypes().then( function () {
|
||||
callback(model.documentTypeKey);
|
||||
});
|
||||
editorService.close();
|
||||
},
|
||||
close: function () {
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
editorService.documentTypeEditor(editor);
|
||||
}
|
||||
|
||||
vm.addSettingsForBlock = function ($event, block) {
|
||||
|
||||
localizationService.localizeMany(["blockEditor_headlineAddSettingsElementType", "blockEditor_labelcreateNewElementType"]).then(function(localized) {
|
||||
@@ -58,15 +76,15 @@
|
||||
createNewItem: {
|
||||
action: function() {
|
||||
overlayService.close();
|
||||
vm.createElementTypeAndAdd((alias) => {
|
||||
vm.applySettingsToBlock(block, alias);
|
||||
vm.createElementTypeAndCallback((key) => {
|
||||
vm.applySettingsToBlock(block, key);
|
||||
});
|
||||
},
|
||||
icon: "icon-add",
|
||||
name: localized[1]
|
||||
},
|
||||
submit: function (overlay) {
|
||||
vm.applySettingsToBlock(block, overlay.selectedItem.alias);
|
||||
vm.applySettingsToBlock(block, overlay.selectedItem.key);
|
||||
overlayService.close();
|
||||
},
|
||||
close: function () {
|
||||
@@ -85,7 +103,7 @@
|
||||
vm.requestRemoveSettingsForBlock = function(block) {
|
||||
localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) {
|
||||
|
||||
var settingsElementType = vm.getElementTypeByAlias(entry.settingsElementTypeKey);
|
||||
var settingsElementType = vm.getElementTypeByKey(block.settingsElementTypeKey);
|
||||
|
||||
overlayService.confirmRemove({
|
||||
title: data[0],
|
||||
@@ -94,14 +112,14 @@
|
||||
overlayService.close();
|
||||
},
|
||||
submit: function () {
|
||||
vm.removeSettingsForEntry(entry);
|
||||
vm.removeSettingsForBlock(block);
|
||||
overlayService.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
vm.removeSettingsForEntry = function(entry) {
|
||||
entry.settingsElementTypeKey = null;
|
||||
vm.removeSettingsForBlock = function(block) {
|
||||
block.settingsElementTypeKey = null;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="umb-group-panel">
|
||||
|
||||
<div class="umb-group-panel__header">
|
||||
<localize key="blockEditor_headlineBlockAppearance">Block appearance</localize>
|
||||
<localize key="blockEditor_headlineEditorAppearance">Editor appearance</localize>
|
||||
</div>
|
||||
|
||||
<div class="umb-group-panel__content">
|
||||
@@ -148,7 +148,7 @@
|
||||
<div class="umb-group-panel">
|
||||
|
||||
<div class="umb-group-panel__header">
|
||||
<localize key="blockEditor_headlineShowcase">Showcase</localize>
|
||||
<localize key="blockEditor_headlineCatalogueAppearance">Catalogue appearance</localize>
|
||||
</div>
|
||||
|
||||
<div class="umb-group-panel__content">
|
||||
|
||||
@@ -12,7 +12,7 @@ module.exports = function (config) {
|
||||
|
||||
// Jasmine plugins
|
||||
'node_modules/jasmine-promise-matchers/dist/jasmine-promise-matchers.js',
|
||||
|
||||
|
||||
//libraries
|
||||
'node_modules/jquery/dist/jquery.min.js',
|
||||
'node_modules/angular/angular.js',
|
||||
|
||||
@@ -49,28 +49,28 @@
|
||||
|
||||
it('fail if no model value', function () {
|
||||
function createWithNoModelValue() {
|
||||
blockEditorService.createModelObject(null, "Umbraco.TestBlockEditor", [], $scope);
|
||||
blockEditorService.createModelObject(null, "Umbraco.TestBlockEditor", [], $scope, $scope);
|
||||
}
|
||||
expect(createWithNoModelValue).toThrow();
|
||||
});
|
||||
|
||||
it('return a object, with methods', function () {
|
||||
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [], $scope);
|
||||
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [], $scope, $scope);
|
||||
|
||||
expect(modelObject).not.toBeUndefined();
|
||||
expect(modelObject.loadScaffolding).not.toBeUndefined();
|
||||
expect(modelObject.load).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('getBlockConfiguration provide the requested block configurtion', function () {
|
||||
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope);
|
||||
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeKey).label).toBe(blockConfigurationMock.label);
|
||||
});
|
||||
|
||||
it('loadScaffolding provides data for itemPicker', function (done) {
|
||||
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope);
|
||||
it('load provides data for itemPicker', function (done) {
|
||||
var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.loadScaffolding().then(() => {
|
||||
modelObject.load().then(() => {
|
||||
var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker();
|
||||
expect(itemPickerOptions.length).toBe(1);
|
||||
expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey);
|
||||
@@ -82,9 +82,9 @@
|
||||
it('getLayoutEntry has values', function (done) {
|
||||
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope);
|
||||
var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.loadScaffolding().then(() => {
|
||||
modelObject.load().then(() => {
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
@@ -98,20 +98,20 @@
|
||||
|
||||
});
|
||||
|
||||
it('getBlockModel has values', function (done) {
|
||||
it('getBlockObject has values', function (done) {
|
||||
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope);
|
||||
var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.loadScaffolding().then(() => {
|
||||
modelObject.load().then(() => {
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
var blockModel = modelObject.getBlockModel(layout[0]);
|
||||
var blockObject = modelObject.getBlockObject(layout[0]);
|
||||
|
||||
expect(blockModel).not.toBeUndefined();
|
||||
expect(blockModel.data.udi).toBe(propertyModelMock.data[0].udi);
|
||||
expect(blockModel.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty);
|
||||
expect(blockObject).not.toBeUndefined();
|
||||
expect(blockObject.data.udi).toBe(propertyModelMock.data[0].udi);
|
||||
expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty);
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -119,24 +119,24 @@
|
||||
});
|
||||
|
||||
|
||||
it('getBlockModel syncs primative values', function (done) {
|
||||
it('getBlockObject syncs primative values', function (done) {
|
||||
|
||||
var propertyModel = angular.copy(propertyModelMock);
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope);
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.loadScaffolding().then(() => {
|
||||
modelObject.load().then(() => {
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
var blockModel = modelObject.getBlockModel(layout[0]);
|
||||
var blockObject = modelObject.getBlockObject(layout[0]);
|
||||
|
||||
blockModel.content.variants[0].tabs[0].properties[0].value = "anotherTestValue";
|
||||
blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue";
|
||||
|
||||
$rootScope.$digest();// invoke angularJS Store.
|
||||
|
||||
expect(blockModel.data).toBe(propertyModel.data[0]);
|
||||
expect(blockModel.data.testproperty).toBe("anotherTestValue");
|
||||
expect(blockObject.data).toBe(propertyModel.data[0]);
|
||||
expect(blockObject.data.testproperty).toBe("anotherTestValue");
|
||||
expect(propertyModel.data[0].testproperty).toBe("anotherTestValue");
|
||||
|
||||
//
|
||||
@@ -147,7 +147,7 @@
|
||||
});
|
||||
|
||||
|
||||
it('getBlockModel syncs values of object', function (done) {
|
||||
it('getBlockObject syncs values of object', function (done) {
|
||||
|
||||
var propertyModel = angular.copy(propertyModelMock);
|
||||
|
||||
@@ -155,16 +155,16 @@
|
||||
propertyModel.data[0].testproperty = complexValue;
|
||||
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope);
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.loadScaffolding().then(() => {
|
||||
modelObject.load().then(() => {
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
var blockModel = modelObject.getBlockModel(layout[0]);
|
||||
var blockObject = modelObject.getBlockObject(layout[0]);
|
||||
|
||||
blockModel.content.variants[0].tabs[0].properties[0].value.list[0] = "AA";
|
||||
blockModel.content.variants[0].tabs[0].properties[0].value.list.push("D");
|
||||
blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA";
|
||||
blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D");
|
||||
|
||||
$rootScope.$digest();// invoke angularJS Store.
|
||||
|
||||
@@ -180,9 +180,9 @@
|
||||
|
||||
var propertyModel = angular.copy(propertyModelMock);
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope);
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.loadScaffolding().then(() => {
|
||||
modelObject.load().then(() => {
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
@@ -201,19 +201,19 @@
|
||||
|
||||
var propertyModel = angular.copy(propertyModelMock);
|
||||
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope);
|
||||
var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope);
|
||||
|
||||
modelObject.loadScaffolding().then(() => {
|
||||
modelObject.load().then(() => {
|
||||
|
||||
var layout = modelObject.getLayout();
|
||||
|
||||
var blockModel = modelObject.getBlockModel(layout[0]);
|
||||
var blockObject = modelObject.getBlockObject(layout[0]);
|
||||
|
||||
// remove from layout;
|
||||
layout.splice(0, 1);
|
||||
|
||||
// remove from data;
|
||||
modelObject.removeDataAndDestroyModel(blockModel);
|
||||
modelObject.removeDataAndDestroyModel(blockObject);
|
||||
|
||||
expect(propertyModel.data.length).toBe(0);
|
||||
expect(propertyModel.data[0]).toBeUndefined();
|
||||
|
||||
Reference in New Issue
Block a user