diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 6119bd5ae3..1d7096cb29 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -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": { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 78ce1f29a5..5a8396776e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -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); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index 0a41119a48..2f1303077c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -5,8 +5,8 @@ -
- +