Files
Umbraco-CMS/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js
2018-06-06 22:36:22 +02:00

680 lines
24 KiB
JavaScript

(function() {
'use strict';
function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService, editorService) {
function link(scope, el, attr, ctrl) {
var validationTranslated = "";
var tabNoSortOrderTranslated = "";
scope.sortingMode = false;
scope.toolbar = [];
scope.sortableOptionsGroup = {};
scope.sortableOptionsProperty = {};
scope.sortingButtonKey = "general_reorder";
function activate() {
setSortingOptions();
// set placeholder property on each group
if (scope.model.groups.length !== 0) {
angular.forEach(scope.model.groups, function(group) {
addInitProperty(group);
});
}
// add init tab
addInitGroup(scope.model.groups);
activateFirstGroup(scope.model.groups);
// localize texts
localizationService.localize("validation_validation").then(function(value) {
validationTranslated = value;
});
localizationService.localize("contentTypeEditor_tabHasNoSortOrder").then(function(value) {
tabNoSortOrderTranslated = value;
});
}
function setSortingOptions() {
scope.sortableOptionsGroup = {
distance: 10,
tolerance: "pointer",
opacity: 0.7,
scroll: true,
cursor: "move",
placeholder: "umb-group-builder__group-sortable-placeholder",
zIndex: 6000,
handle: ".umb-group-builder__group-handle",
items: ".umb-group-builder__group-sortable",
start: function(e, ui) {
ui.placeholder.height(ui.item.height());
},
stop: function(e, ui) {
updateTabsSortOrder();
},
};
scope.sortableOptionsProperty = {
distance: 10,
tolerance: "pointer",
connectWith: ".umb-group-builder__properties",
opacity: 0.7,
scroll: true,
cursor: "move",
placeholder: "umb-group-builder__property_sortable-placeholder",
zIndex: 6000,
handle: ".umb-group-builder__property-handle",
items: ".umb-group-builder__property-sortable",
start: function(e, ui) {
ui.placeholder.height(ui.item.height());
},
stop: function(e, ui) {
updatePropertiesSortOrder();
}
};
}
function updateTabsSortOrder() {
var first = true;
var prevSortOrder = 0;
scope.model.groups.map(function(group){
var index = scope.model.groups.indexOf(group);
if(group.tabState !== "init") {
// set the first not inherited tab to sort order 0
if(!group.inherited && first) {
// set the first tab sort order to 0 if prev is 0
if( prevSortOrder === 0 ) {
group.sortOrder = 0;
// when the first tab is inherited and sort order is not 0
} else {
group.sortOrder = prevSortOrder + 1;
}
first = false;
} else if(!group.inherited && !first) {
// find next group
var nextGroup = scope.model.groups[index + 1];
// if a groups is dropped in the middle of to groups with
// same sort order. Give it the dropped group same sort order
if( prevSortOrder === nextGroup.sortOrder ) {
group.sortOrder = prevSortOrder;
} else {
group.sortOrder = prevSortOrder + 1;
}
}
// store this tabs sort order as reference for the next
prevSortOrder = group.sortOrder;
}
});
}
function filterAvailableCompositions(selectedContentType, selecting) {
//selecting = true if the user has check the item, false if the user has unchecked the item
var selectedContentTypeAliases = selecting ?
//the user has selected the item so add to the current list
_.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) :
//the user has unselected the item so remove from the current list
_.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) {
return i === selectedContentType.alias;
});
//get the currently assigned property type aliases - ensure we pass these to the server side filer
var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
return _.map(g.properties, function(p) {
return p.alias;
});
})), function (f) {
return f !== null && f !== undefined;
});
//use a different resource lookup depending on the content type type
var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) {
_.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) {
//reset first
current.allowed = true;
//see if this list item is found in the response (allowed) list
var found = _.find(filteredAvailableCompositeTypes, function (f) {
return current.contentType.alias === f.contentType.alias;
});
//allow if the item was found in the response (allowed) list -
// and ensure its set to allowed if it is currently checked,
// DO not allow if it's a locked content type.
current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 &&
(selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || ((found !== null && found !== undefined) ? found.allowed : false);
});
});
}
function updatePropertiesSortOrder() {
angular.forEach(scope.model.groups, function(group){
if( group.tabState !== "init" ) {
group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties);
}
});
}
function setupAvailableContentTypesModel(result) {
scope.compositionsDialogModel.availableCompositeContentTypes = result;
//iterate each one and set it up
_.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) {
//enable it if it's part of the selected model
if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) {
c.allowed = true;
}
//set the inherited flags
c.inherited = false;
if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) {
c.inherited = true;
}
// convert icons for composite content types
iconHelper.formatContentTypeIcons([c.contentType]);
});
}
/* ---------- DELETE PROMT ---------- */
scope.togglePrompt = function (object) {
object.deletePrompt = !object.deletePrompt;
};
scope.hidePrompt = function (object) {
object.deletePrompt = false;
};
/* ---------- TOOLBAR ---------- */
scope.toggleSortingMode = function(tool) {
if (scope.sortingMode === true) {
var sortOrderMissing = false;
for (var i = 0; i < scope.model.groups.length; i++) {
var group = scope.model.groups[i];
if (group.tabState !== "init" && group.sortOrder === undefined) {
sortOrderMissing = true;
group.showSortOrderMissing = true;
notificationsService.error(validationTranslated + ": " + group.name + " " + tabNoSortOrderTranslated);
}
}
if (!sortOrderMissing) {
scope.sortingMode = false;
scope.sortingButtonKey = "general_reorder";
}
} else {
scope.sortingMode = true;
scope.sortingButtonKey = "general_reorderDone";
}
};
scope.openCompositionsDialog = function() {
scope.compositionsDialogModel = {
title: "Compositions",
contentType: scope.model,
compositeContentTypes: scope.model.compositeContentTypes,
view: "views/common/overlays/contenttypeeditor/compositions/compositions.html",
confirmSubmit: {
title: "Warning",
description: "Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?",
checkboxLabel: "I know what I'm doing",
enable: true
},
submit: function(model, oldModel, confirmed) {
var compositionRemoved = false;
// check if any compositions has been removed
for(var i = 0; oldModel.compositeContentTypes.length > i; i++) {
var oldComposition = oldModel.compositeContentTypes[i];
if(_.contains(model.compositeContentTypes, oldComposition) === false) {
compositionRemoved = true;
}
}
// show overlay confirm box if compositions has been removed.
if(compositionRemoved && confirmed === false) {
scope.compositionsDialogModel.confirmSubmit.show = true;
// submit overlay if no compositions has been removed
// or the action has been confirmed
} else {
// make sure that all tabs has an init property
if (scope.model.groups.length !== 0) {
angular.forEach(scope.model.groups, function(group) {
addInitProperty(group);
});
}
// remove overlay
scope.compositionsDialogModel.show = false;
scope.compositionsDialogModel = null;
}
},
close: function(oldModel) {
// reset composition changes
scope.model.groups = oldModel.contentType.groups;
scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes;
// remove overlay
scope.compositionsDialogModel.show = false;
scope.compositionsDialogModel = null;
},
selectCompositeContentType: function (selectedContentType) {
//first check if this is a new selection - we need to store this value here before any further digests/async
// because after that the scope.model.compositeContentTypes will be populated with the selected value.
var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1;
if (newSelection) {
//merge composition with content type
//use a different resource lookup depending on the content type type
var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById;
resourceLookup(selectedContentType.id).then(function (composition) {
//based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and
// double check here.
var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition);
if (overlappingAliases.length > 0) {
//this will create an invalid composition, need to uncheck it
scope.compositionsDialogModel.compositeContentTypes.splice(
scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1);
//dissallow this until something else is unchecked
selectedContentType.allowed = false;
}
else {
contentTypeHelper.mergeCompositeContentType(scope.model, composition);
}
//based on the selection, we need to filter the available composite types list
filterAvailableCompositions(selectedContentType, newSelection).then(function () {
//TODO: Here we could probably re-enable selection if we previously showed a throbber or something
});
});
}
else {
// split composition from content type
contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType);
//based on the selection, we need to filter the available composite types list
filterAvailableCompositions(selectedContentType, newSelection).then(function () {
//TODO: Here we could probably re-enable selection if we previously showed a throbber or something
});
}
}
};
//select which resource methods to use, eg document Type or Media Type versions
var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
var whereUsedContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getWhereCompositionIsUsedInContentTypes : mediaTypeResource.getWhereCompositionIsUsedInContentTypes;
var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount;
//get the currently assigned property type aliases - ensure we pass these to the server side filer
var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
return _.map(g.properties, function(p) {
return p.alias;
});
})), function(f) {
return f !== null && f !== undefined;
});
$q.all([
//get available composite types
availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) {
setupAvailableContentTypesModel(result);
}),
//get where used document types
whereUsedContentTypeResource(scope.model.id).then(function (whereUsed) {
//pass to the dialog model the content type eg documentType or mediaType
scope.compositionsDialogModel.section = scope.contentType;
//pass the list of 'where used' document types
scope.compositionsDialogModel.whereCompositionUsed = whereUsed;
}),
//get content type count
countContentTypeResource().then(function(result) {
scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10);
})
]).then(function() {
//resolves when both other promises are done, now show it
scope.compositionsDialogModel.show = true;
});
};
/* ---------- GROUPS ---------- */
scope.addGroup = function(group) {
// set group sort order
var index = scope.model.groups.indexOf(group);
var prevGroup = scope.model.groups[index - 1];
if( index > 0) {
// set index to 1 higher than the previous groups sort order
group.sortOrder = prevGroup.sortOrder + 1;
} else {
// first group - sort order will be 0
group.sortOrder = 0;
}
// activate group
scope.activateGroup(group);
};
scope.activateGroup = function(selectedGroup) {
// set all other groups that are inactive to active
angular.forEach(scope.model.groups, function(group) {
// skip init tab
if (group.tabState !== "init") {
group.tabState = "inActive";
}
});
selectedGroup.tabState = "active";
};
scope.removeGroup = function(groupIndex) {
scope.model.groups.splice(groupIndex, 1);
addInitGroup(scope.model.groups);
};
scope.updateGroupTitle = function(group) {
if (group.properties.length === 0) {
addInitProperty(group);
}
};
scope.changeSortOrderValue = function(group) {
if (group.sortOrder !== undefined) {
group.showSortOrderMissing = false;
}
scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder');
};
function addInitGroup(groups) {
// check i init tab already exists
var addGroup = true;
angular.forEach(groups, function(group) {
if (group.tabState === "init") {
addGroup = false;
}
});
if (addGroup) {
groups.push({
properties: [],
parentTabContentTypes: [],
parentTabContentTypeNames: [],
name: "",
tabState: "init"
});
}
return groups;
}
function activateFirstGroup(groups) {
if (groups && groups.length > 0) {
var firstGroup = groups[0];
if(!firstGroup.tabState || firstGroup.tabState === "inActive") {
firstGroup.tabState = "active";
}
}
}
/* ---------- PROPERTIES ---------- */
scope.addProperty = function(property, group) {
// set property sort order
var index = group.properties.indexOf(property);
var prevProperty = group.properties[index - 1];
if( index > 0) {
// set index to 1 higher than the previous property sort order
property.sortOrder = prevProperty.sortOrder + 1;
} else {
// first property - sort order will be 0
property.sortOrder = 0;
}
// open property settings dialog
scope.editPropertyTypeSettings(property, group);
};
scope.editPropertyTypeSettings = function(property, group) {
if (!property.inherited) {
var oldPropertyModel = angular.copy(property);
var propertySettings = {
title: "Property settings",
property: property,
contentType: scope.contentType,
contentTypeName: scope.model.name,
contentTypeAllowCultureVariant: scope.model.allowCultureVariant,
view: "views/common/infiniteeditors/propertysettings/propertysettings.html",
size: "small",
submit: function(model) {
property.inherited = false;
property.dialogIsOpen = false;
// update existing data types
if(model.updateSameDataTypes) {
updateSameDataTypes(property);
}
// close the editor
editorService.close();
// push new init property to group
addInitProperty(group);
// set focus on init property
var numberOfProperties = group.properties.length;
group.properties[numberOfProperties - 1].focus = true;
// push new init tab to the scope
addInitGroup(scope.model.groups);
},
close: function() {
// reset all property changes
property.label = oldPropertyModel.label;
property.alias = oldPropertyModel.alias;
property.description = oldPropertyModel.description;
property.config = oldPropertyModel.config;
property.editor = oldPropertyModel.editor;
property.view = oldPropertyModel.view;
property.dataTypeId = oldPropertyModel.dataTypeId;
property.dataTypeIcon = oldPropertyModel.dataTypeIcon;
property.dataTypeName = oldPropertyModel.dataTypeName;
property.validation.mandatory = oldPropertyModel.validation.mandatory;
property.validation.pattern = oldPropertyModel.validation.pattern;
property.showOnMemberProfile = oldPropertyModel.showOnMemberProfile;
property.memberCanEdit = oldPropertyModel.memberCanEdit;
property.isSensitiveValue = oldPropertyModel.isSensitiveValue;
// because we set state to active, to show a preview, we have to check if has been filled out
// label is required so if it is not filled we know it is a placeholder
if(oldPropertyModel.editor === undefined || oldPropertyModel.editor === null || oldPropertyModel.editor === "") {
property.propertyState = "init";
} else {
property.propertyState = oldPropertyModel.propertyState;
}
// remove the editor
editorService.close();
}
};
// open property settings editor
editorService.open(propertySettings);
// set state to active to access the preview
property.propertyState = "active";
// set property states
property.dialogIsOpen = true;
}
};
scope.deleteProperty = function(tab, propertyIndex) {
// remove property
tab.properties.splice(propertyIndex, 1);
// if the last property in group is an placeholder - remove add new tab placeholder
if(tab.properties.length === 1 && tab.properties[0].propertyState === "init") {
angular.forEach(scope.model.groups, function(group, index, groups){
if(group.tabState === 'init') {
groups.splice(index, 1);
}
});
}
};
function addInitProperty(group) {
var addInitPropertyBool = true;
var initProperty = {
label: null,
alias: null,
propertyState: "init",
validation: {
mandatory: false,
pattern: null
}
};
// check if there already is an init property
angular.forEach(group.properties, function(property) {
if (property.propertyState === "init") {
addInitPropertyBool = false;
}
});
if (addInitPropertyBool) {
group.properties.push(initProperty);
}
return group;
}
function updateSameDataTypes(newProperty) {
// find each property
angular.forEach(scope.model.groups, function(group){
angular.forEach(group.properties, function(property){
if(property.dataTypeId === newProperty.dataTypeId) {
// update property data
property.config = newProperty.config;
property.editor = newProperty.editor;
property.view = newProperty.view;
property.dataTypeId = newProperty.dataTypeId;
property.dataTypeIcon = newProperty.dataTypeIcon;
property.dataTypeName = newProperty.dataTypeName;
}
});
});
}
var unbindModelWatcher = scope.$watch('model', function(newValue, oldValue) {
if (newValue !== undefined && newValue.groups !== undefined) {
activate();
}
});
// clean up
scope.$on('$destroy', function(){
unbindModelWatcher();
});
}
var directive = {
restrict: "E",
replace: true,
templateUrl: "views/components/umb-groups-builder.html",
scope: {
model: "=",
compositions: "=",
sorting: "=",
contentType: "@"
},
link: link
};
return directive;
}
angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective);
})();