Fixes contentpicker to update model.value correctly and sync it's renderModel and deal with the quirks of the ui-sortable stuff
This commit is contained in:
@@ -43,7 +43,7 @@
|
||||
"clipboard": "~2.0.0",
|
||||
"font-awesome": "~4.2",
|
||||
"animejs": "^2.2.0",
|
||||
"angular-ui-sortable": "0.14.3",
|
||||
"angular-ui-sortable": "0.14.4",
|
||||
"angular-messages": "^1.7.2"
|
||||
},
|
||||
"install": {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* @param {any} $location
|
||||
* @param {any} localizationService
|
||||
*/
|
||||
function contentPickerController($scope, entityResource, editorState, iconHelper, $routeParams, angularHelper, navigationService, $location, localizationService, editorService) {
|
||||
function contentPickerController($scope, entityResource, editorState, iconHelper, $routeParams, angularHelper, navigationService, $location, localizationService, editorService, $q) {
|
||||
|
||||
var unsubscribe;
|
||||
|
||||
@@ -30,23 +30,9 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
return str.replace(rgxtrim, '');
|
||||
}
|
||||
|
||||
function startWatch() {
|
||||
//We need to watch our renderModel so that we can update the underlying $scope.model.value properly, this is required
|
||||
// because the ui-sortable doesn't dispatch an event after the digest of the sort operation. Any of the events for UI sortable
|
||||
// occur after the DOM has updated but BEFORE the digest has occured so the model has NOT changed yet - it even states so in the docs.
|
||||
// In their source code there is no event so we need to just subscribe to our model changes here.
|
||||
//This also makes it easier to manage models, we update one and the rest will just work.
|
||||
$scope.$watch(function () {
|
||||
//return the joined Ids as a string to watch
|
||||
return _.map($scope.renderModel, function (i) {
|
||||
return $scope.model.config.idType === "udi" ? i.udi : i.id;
|
||||
}).join();
|
||||
}, function (newVal) {
|
||||
var currIds = _.map($scope.renderModel, function (i) {
|
||||
return $scope.model.config.idType === "udi" ? i.udi : i.id;
|
||||
});
|
||||
$scope.model.value = trim(currIds.join(), ",");
|
||||
|
||||
/** Performs validation based on the renderModel data */
|
||||
function validate() {
|
||||
if ($scope.contentPickerForm) {
|
||||
//Validate!
|
||||
if ($scope.model.config && $scope.model.config.minNumber && parseInt($scope.model.config.minNumber) > $scope.renderModel.length) {
|
||||
$scope.contentPickerForm.minCount.$setValidity("minCount", false);
|
||||
@@ -61,15 +47,32 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
else {
|
||||
$scope.contentPickerForm.maxCount.$setValidity("maxCount", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSortingState($scope.renderModel);
|
||||
function startWatch() {
|
||||
|
||||
//due to the way angular-sortable works, it needs to update a model, we don't want it to update renderModel since renderModel
|
||||
//is updated based on changes to model.value so if we bound angular-sortable to that and put a watch on it we'd end up in a
|
||||
//infinite loop. Instead we have a custom array model for angular-sortable and we'll watch that which we'll use to sync the model.value
|
||||
//which in turn will sync the renderModel.
|
||||
$scope.$watchCollection("sortableModel", function(newVal, oldVal) {
|
||||
$scope.model.value = newVal.join();
|
||||
});
|
||||
|
||||
//if the underlying model changes, update the view model, this ensures that the view is always consistent with the underlying
|
||||
//model if it changes (i.e. based on server updates, or if used in split view, etc...)
|
||||
$scope.$watch("model.value", function(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
syncRenderModel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.renderModel = [];
|
||||
|
||||
$scope.dialogEditor = editorState && editorState.current && editorState.current.isDialogEditor === true;
|
||||
$scope.sortableModel = [];
|
||||
|
||||
$scope.dialogEditor = editorState && editorState.current && editorState.current.isDialogEditor === true;
|
||||
|
||||
//the default pre-values
|
||||
var defaultConfig = {
|
||||
@@ -78,11 +81,11 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
showEditButton: false,
|
||||
showPathOnHover: false,
|
||||
maxNumber: 1,
|
||||
minNumber : 0,
|
||||
minNumber: 0,
|
||||
startNode: {
|
||||
query: "",
|
||||
type: "content",
|
||||
id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker
|
||||
id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,12 +110,12 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
$scope.model.config.showOpenButton = Object.toBoolean($scope.model.config.showOpenButton);
|
||||
$scope.model.config.showEditButton = Object.toBoolean($scope.model.config.showEditButton);
|
||||
$scope.model.config.showPathOnHover = Object.toBoolean($scope.model.config.showPathOnHover);
|
||||
|
||||
|
||||
var entityType = $scope.model.config.startNode.type === "member"
|
||||
? "Member"
|
||||
: $scope.model.config.startNode.type === "media"
|
||||
? "Media"
|
||||
: "Document";
|
||||
? "Media"
|
||||
: "Document";
|
||||
$scope.allowOpenButton = entityType === "Document";
|
||||
$scope.allowEditButton = entityType === "Document";
|
||||
$scope.allowRemoveButton = true;
|
||||
@@ -145,7 +148,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
//since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the
|
||||
// pre-value config on to the dialog options
|
||||
angular.extend(dialogOptions, $scope.model.config);
|
||||
|
||||
|
||||
//We need to manually handle the filter for members here since the tree displayed is different and only contains
|
||||
// searchable list views
|
||||
if (entityType === "Member") {
|
||||
@@ -153,7 +156,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
dialogOptions.filterCssClass = "not-allowed";
|
||||
var currFilter = dialogOptions.filter;
|
||||
//now change the filter to be a method
|
||||
dialogOptions.filter = function(i) {
|
||||
dialogOptions.filter = function (i) {
|
||||
//filter out the list view nodes
|
||||
if (i.metaData.isContainer) {
|
||||
return true;
|
||||
@@ -175,7 +178,8 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
if ($routeParams.section === "settings" && $routeParams.tree === "documentTypes") {
|
||||
//if the content-picker is being rendered inside the document-type editor, we don't need to process the startnode query
|
||||
dialogOptions.startNodeId = -1;
|
||||
} else if ($scope.model.config.startNode.query) {
|
||||
}
|
||||
else if ($scope.model.config.startNode.query) {
|
||||
//if we have a query for the startnode, we will use that.
|
||||
var rootId = $routeParams.id;
|
||||
entityResource.getByQuery($scope.model.config.startNode.query, rootId, "Document").then(function (ent) {
|
||||
@@ -209,8 +213,12 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
};
|
||||
|
||||
$scope.remove = function (index) {
|
||||
$scope.renderModel.splice(index, 1);
|
||||
angularHelper.getCurrentForm($scope).$setDirty();
|
||||
var currIds = $scope.model.value ? $scope.model.value.split(',') : [];
|
||||
if (currIds.length > 0) {
|
||||
currIds.splice(index, 1);
|
||||
angularHelper.getCurrentForm($scope).$setDirty();
|
||||
$scope.model.value = currIds.join();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showNode = function (index) {
|
||||
@@ -229,36 +237,35 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
}
|
||||
|
||||
$scope.add = function (item) {
|
||||
var currIds = _.map($scope.renderModel, function (i) {
|
||||
return $scope.model.config.idType === "udi" ? i.udi : i.id;
|
||||
});
|
||||
var currIds = $scope.model.value ? $scope.model.value.split(',') : [];
|
||||
|
||||
var itemId = $scope.model.config.idType === "udi" ? item.udi : item.id;
|
||||
|
||||
if (currIds.indexOf(itemId) < 0) {
|
||||
setEntityUrl(item);
|
||||
currIds.push(itemId);
|
||||
$scope.model.value = currIds.join();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.clear = function () {
|
||||
$scope.renderModel = [];
|
||||
$scope.model.value = null;
|
||||
};
|
||||
|
||||
$scope.openContentEditor = function(node) {
|
||||
$scope.openContentEditor = function (node) {
|
||||
var contentEditor = {
|
||||
id: node.id,
|
||||
submit: function(model) {
|
||||
submit: function (model) {
|
||||
// update the node
|
||||
node.name = model.contentNode.name;
|
||||
node.published = model.contentNode.hasPublishedVersion;
|
||||
if(entityType !== "Member") {
|
||||
entityResource.getUrl(model.contentNode.id, entityType).then(function(data){
|
||||
if (entityType !== "Member") {
|
||||
entityResource.getUrl(model.contentNode.id, entityType).then(function (data) {
|
||||
node.url = data;
|
||||
});
|
||||
}
|
||||
editorService.close();
|
||||
},
|
||||
close: function() {
|
||||
close: function () {
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
@@ -267,48 +274,95 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
|
||||
//when the scope is destroyed we need to unsubscribe
|
||||
$scope.$on('$destroy', function () {
|
||||
if(unsubscribe) {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
var modelIds = $scope.model.value ? $scope.model.value.split(',') : [];
|
||||
|
||||
//load current data if anything selected
|
||||
if (modelIds.length > 0) {
|
||||
entityResource.getByIds(modelIds, entityType).then(function(data) {
|
||||
/** Syncs the renderModel based on the actual model.value and returns a promise */
|
||||
function syncRenderModel() {
|
||||
|
||||
var valueIds = $scope.model.value ? $scope.model.value.split(',') : [];
|
||||
|
||||
//sync the sortable model
|
||||
$scope.sortableModel = valueIds;
|
||||
|
||||
//load current data if anything selected
|
||||
if (valueIds.length > 0) {
|
||||
|
||||
//need to determine which items we already have loaded
|
||||
var renderModelIds = _.map($scope.renderModel, function(d) {
|
||||
return $scope.model.config.idType === "udi" ? d.udi : d.id;
|
||||
});
|
||||
|
||||
//get the ids that no longer exist
|
||||
var toRemove = _.difference(renderModelIds, valueIds);
|
||||
|
||||
//remove the ones that no longer exist
|
||||
for (var j = 0; j < toRemove.length; j++) {
|
||||
var index = renderModelIds.indexOf(toRemove[j]);
|
||||
$scope.renderModel.splice(index, 1);
|
||||
}
|
||||
|
||||
//get the ids that we need to lookup entities for
|
||||
var missingIds = _.difference(valueIds, renderModelIds);
|
||||
|
||||
if (missingIds.length > 0) {
|
||||
return entityResource.getByIds(missingIds, entityType).then(function(data) {
|
||||
|
||||
_.each(valueIds,
|
||||
function(id, i) {
|
||||
var entity = _.find(data, function(d) {
|
||||
return $scope.model.config.idType === "udi" ? (d.udi == id) : (d.id == id);
|
||||
});
|
||||
|
||||
if (entity) {
|
||||
addSelectedItem(entity);
|
||||
}
|
||||
|
||||
_.each(modelIds,
|
||||
function(id, i) {
|
||||
var entity = _.find(data,
|
||||
function(d) {
|
||||
return $scope.model.config.idType === "udi" ? (d.udi == id) : (d.id == id);
|
||||
});
|
||||
|
||||
if (entity) {
|
||||
setEntityUrl(entity);
|
||||
}
|
||||
|
||||
validate();
|
||||
setSortingState($scope.renderModel);
|
||||
return $q.when(true);
|
||||
});
|
||||
}
|
||||
else {
|
||||
//if there's nothing missing, make sure it's sorted correctly
|
||||
|
||||
var current = $scope.renderModel;
|
||||
$scope.renderModel = [];
|
||||
for (var k = 0; k < valueIds.length; k++) {
|
||||
var id = valueIds[k];
|
||||
var found = _.find(current, function(d) {
|
||||
return $scope.model.config.idType === "udi" ? (d.udi == id) : (d.id == id);
|
||||
});
|
||||
if (found) {
|
||||
$scope.renderModel.push(found);
|
||||
}
|
||||
}
|
||||
|
||||
validate();
|
||||
setSortingState($scope.renderModel);
|
||||
return $q.when(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
$scope.renderModel = [];
|
||||
validate();
|
||||
setSortingState($scope.renderModel);
|
||||
return $q.when(true);
|
||||
}
|
||||
|
||||
//everything is loaded, start the watch on the model
|
||||
startWatch();
|
||||
subscribe();
|
||||
});
|
||||
}
|
||||
else {
|
||||
//everything is loaded, start the watch on the model
|
||||
startWatch();
|
||||
subscribe();
|
||||
}
|
||||
|
||||
function setEntityUrl(entity) {
|
||||
|
||||
// get url for content and media items
|
||||
if(entityType !== "Member") {
|
||||
entityResource.getUrl(entity.id, entityType).then(function(data){
|
||||
if (entityType !== "Member") {
|
||||
entityResource.getUrl(entity.id, entityType).then(function (data) {
|
||||
// update url
|
||||
angular.forEach($scope.renderModel, function(item){
|
||||
angular.forEach($scope.renderModel, function (item) {
|
||||
if (item.id === entity.id) {
|
||||
if (entity.trashed) {
|
||||
item.url = localizationService.dictionary.general_recycleBin;
|
||||
@@ -320,17 +374,12 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
});
|
||||
}
|
||||
|
||||
// add the selected item to the renderModel
|
||||
// if it needs to show a url the item will get
|
||||
// updated when the url comes back from server
|
||||
addSelectedItem(entity);
|
||||
|
||||
}
|
||||
|
||||
function addSelectedItem(item) {
|
||||
|
||||
// set icon
|
||||
if(item.icon) {
|
||||
if (item.icon) {
|
||||
item.icon = iconHelper.convertFromLegacyIcon(item.icon);
|
||||
}
|
||||
|
||||
@@ -349,7 +398,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
}
|
||||
}
|
||||
|
||||
$scope.renderModel.push({
|
||||
$scope.renderModel.push({
|
||||
"name": item.name,
|
||||
"id": item.id,
|
||||
"udi": item.udi,
|
||||
@@ -361,17 +410,28 @@ function contentPickerController($scope, entityResource, editorState, iconHelper
|
||||
// only content supports published/unpublished content so we set everything else to published so the UI looks correct
|
||||
});
|
||||
|
||||
setEntityUrl(item);
|
||||
}
|
||||
|
||||
function setSortingState(items) {
|
||||
// disable sorting if the list only consist of one item
|
||||
if(items.length > 1) {
|
||||
if (items.length > 1) {
|
||||
$scope.sortableOptions.disabled = false;
|
||||
} else {
|
||||
$scope.sortableOptions.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
syncRenderModel().then(function () {
|
||||
//everything is loaded, start the watch on the model
|
||||
startWatch();
|
||||
subscribe();
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
}
|
||||
|
||||
angular.module('umbraco').controller("Umbraco.PropertyEditors.ContentPickerController", contentPickerController);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
<ng-form name="contentPickerForm">
|
||||
|
||||
<div ui-sortable="sortableOptions" ng-model="renderModel">
|
||||
<umb-node-preview ng-repeat="node in renderModel"
|
||||
<div ui-sortable="sortableOptions" ng-model="sortableModel">
|
||||
<umb-node-preview ng-repeat="node in renderModel track by node.id"
|
||||
icon="node.icon"
|
||||
name="node.name"
|
||||
published="node.published"
|
||||
|
||||
Reference in New Issue
Block a user