From 2bb2f7bbbd316c9368cd4fc5ed1429a72ea79584 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 30 Aug 2018 16:12:42 +1000 Subject: [PATCH] componentizes umb-variant-content and umb-variant-content-editors, moves the logic dealing with the `editors` array to a component and out of the content controller, ensures that the editor array is updated correctly so that all directives are re-bound --- .../components/content/edit.controller.js | 146 +-------- .../content/umbvariantcontent.directive.js | 177 ++++------- .../umbvariantcontenteditors.directive.js | 280 ++++++++++++++++++ .../src/views/components/content/edit.html | 20 +- .../content/umb-variant-content-editors.html | 17 ++ .../content/umb-variant-content.html | 30 +- 6 files changed, 378 insertions(+), 292 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index a72fad8552..57491fc81e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -25,11 +25,6 @@ $scope.page.hideChangeVariant = infiniteMode ? true : false; $scope.allowOpen = true; - // add all editors to an editors array to support split view - $scope.editors = []; - $scope.initVariant = initVariant; - $scope.splitViewChanged = splitViewChanged; - function init(content) { if (infiniteMode) { @@ -55,17 +50,10 @@ // set first app to active // We need to track active $scope.content.apps[0].active = true; - - setActiveCulture(); - + resetVariantFlags(); } - /** This is called when the split view changes based on the umb-variant-content */ - function splitViewChanged() { - //send an event downwards - $scope.$broadcast("editors.content.splitViewChanged", { editors: $scope.editors }); - } /** * This will reset isDirty flags if save is true. @@ -108,128 +96,6 @@ return $scope.content.variants.length > 1; } - /** - * The content item(s) are loaded into an array and this will set the active content item based on the current culture (query string). - * If the content item is invariant, then only one item exists in the array. - */ - function setActiveCulture() { - // set the active variant - var activeVariant = null; - _.each($scope.content.variants, function (v) { - if (v.language && v.language.culture === $scope.culture) { - v.active = true; - activeVariant = v; - } - else { - v.active = false; - } - }); - if (!activeVariant) { - // set the first variant to active - $scope.content.variants[0].active = true; - activeVariant = $scope.content.variants[0]; - } - - initVariant(activeVariant); - - var variantCulture = activeVariant.language ? activeVariant.language.culture : "invariant"; - - //If there are no editors yet then create one with the current content. - //if there's already a main editor then update it with the current content. - if ($scope.editors.length === 0) { - var editor = { - content: activeVariant, - //used for "track-by" ng-repeat - culture: variantCulture - }; - $scope.editors.push(editor); - } - else { - - //check if the current editor is the same culture - var currentIndex = _.findIndex($scope.editors, function (e) { - return e.culture === variantCulture; - }); - - if (currentIndex < 0) { - //not the current culture which means we need to modify the array, - //if we just replace the content object at the zero index, the rg-repeat will not update - //which means directives do not refresh which cause problems - $scope.editors.splice(0, 1, { - content: activeVariant, - //used for "track-by" ng-repeat - culture: variantCulture - }); - } - else { - //replace the content for the same culture - $scope.editors[0].content = activeVariant; - } - - if ($scope.editors.length > 1) { - //now re-sync any other editor content (i.e. if split view is open) - for (var s = 1; s < $scope.editors.length; s++) { - //get the variant from the scope model - var variant = _.find($scope.content.variants, function (v) { - return v.language.culture === $scope.editors[s].content.language.culture; - }); - $scope.editors[s].content = initVariant(variant); - } - } - - } - } - - function initVariant(variant) { - //The model that is assigned to the editor contains the current content variant along - //with a copy of the contentApps. This is required because each editor renders it's own - //header and content apps section and the content apps contains the view for editing content itself - //and we need to assign a view model to the subView so that it is scoped to the current - //editor so that split views work. - - //copy the apps from the main model if not assigned yet to the variant - if (!variant.apps) { - variant.apps = angular.copy($scope.content.apps); - } - - //if this is a variant has a culture/language than we need to assign the language drop down info - if (variant.language) { - //if the variant list that defines the header drop down isn't assigned to the variant then assign it now - if (!variant.variants) { - variant.variants = _.map($scope.content.variants, - function (v) { - return _.pick(v, "active", "language", "state"); - }); - } - else { - //merge the scope variants on top of the header variants collection (handy when needing to refresh) - angular.extend(variant.variants, - _.map($scope.content.variants, - function (v) { - return _.pick(v, "active", "language", "state"); - })); - } - - //ensure the current culture is set as the active one - for (var i = 0; i < variant.variants.length; i++) { - if (variant.variants[i].language.culture === variant.language.culture) { - variant.variants[i].active = true; - } - else { - variant.variants[i].active = false; - } - } - } - - //then assign the variant to a view model to the content app - var contentApp = _.find(variant.apps, function (a) { - return a.alias === "content"; - }); - contentApp.viewModel = variant; - - return variant; - } - function bindEvents() { //bindEvents can be called more than once and we don't want to have multiple bound events for (var e in evts) { @@ -594,10 +460,6 @@ } }; - $scope.backToListView = function () { - $location.path($scope.page.listViewPath); - }; - $scope.restore = function (content) { $scope.page.buttonRestore = "busy"; @@ -703,12 +565,6 @@ } }; - $scope.$watch('culture', function (newVal, oldVal) { - if (newVal !== oldVal) { - setActiveCulture(); - } - }); - //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index cdc23ee985..5bc94ee97d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -2,133 +2,78 @@ 'use strict'; /** - * A directive to encapsulate each variant editor which includes the name header and all content apps for a given variant - * @param {any} $timeout - * @param {any} $location + * A component to encapsulate each variant editor which includes the name header and all content apps for a given variant */ - function variantContentDirective($timeout, $location) { + var umbVariantContent = { + templateUrl: 'views/components/content/umb-variant-content.html', + bindings: { + content: "<", + page: "<", + editor: "<", + editorCount: "<", + onCloseSplitView: "&", + onSelectVariant: "&", + onOpenSplitView: "&" + }, + controllerAs: 'vm', + controller: umbVariantContentController + }; + + function umbVariantContentController($scope, $element, $location) { - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/content/umb-variant-content.html', - link: function (scope) { + var unsubscribe = []; - /** - * Adds a new editor to the editors array to show content in a split view - * @param {any} selectedVariant - */ - scope.openInSplitView = function (selectedVariant) { + var vm = this; - var selectedCulture = selectedVariant.language.culture; + vm.$postLink = postLink; + vm.$onDestroy = onDestroy; - //only the content app can be selected since no other apps are shown, and because we copy all of these apps - //to the "editors" we need to update this across all editors - for (var e = 0; e < scope.editors.length; e++) { - var editor = scope.editors[e]; - for (var i = 0; i < editor.content.apps.length; i++) { - var app = editor.content.apps[i]; - if (app.alias === "content") { - app.active = true; - } - else { - app.active = false; - } - } + vm.selectVariant = selectVariant; + vm.openSplitView = openSplitView; + vm.backToListView = backToListView; + + /** Called when the component has linked all elements, this is when the form controller is available */ + function postLink() { + //set the content to dirty if the header changes + unsubscribe.push($scope.$watch("contentHeaderForm.$dirty", + function(newValue, oldValue) { + if (newValue === true) { + scope.editor.content.isDirty = true; } - - //Find the whole variant model based on the culture that was chosen - var variant = _.find(scope.content.variants, function (v) { - return v.language.culture === selectedCulture; - }); - - var editor = { - content: scope.initVariant({ variant: variant}) - }; - scope.editors.push(editor); - - //TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular - editor.collapsed = true; - editor.loading = true; - $timeout(function () { - editor.collapsed = false; - editor.loading = false; - scope.onSplitViewChanged(); - }, 100); - }; - - /** - * Changes the currently selected variant - * @param {any} variantDropDownItem - */ - scope.selectVariant = function (variantDropDownItem) { - - var editorIndex = _.findIndex(scope.editors, function (e) { - return e === scope.editor; - }); - - //if the editor index is zero, then update the query string to track the lang selection, otherwise if it's part - //of a 2nd split view editor then update the model directly. - if (editorIndex === 0) { - //if we've made it this far, then update the query string - $location.search("cculture", variantDropDownItem.language.culture); - } - else { - //set all variant drop down items as inactive for this editor and then set the selected on as active - for (var i = 0; i < scope.editor.content.variants.length; i++) { - scope.editor.content.variants[i].active = false; - } - variantDropDownItem.active = true; - - //get the variant content model and initialize the editor with that - var variant = _.find(scope.content.variants, function (v) { - return v.language.culture === variantDropDownItem.language.culture; - }); - scope.editor.content = scope.initVariant({ variant: variant }); - } - }; - - /** Closes the split view */ - scope.closeSplitView = function () { - //TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular - scope.editor.loading = true; - scope.editor.collapsed = true; - $timeout(function () { - var index = _.findIndex(scope.editors, function(e) { - return e === scope.editor; - }); - scope.editors.splice(index, 1); - scope.onSplitViewChanged(); - }, 400); - }; - - //set the content to dirty if the header changes - scope.$watch("contentHeaderForm.$dirty", - function (newValue, oldValue) { - if (newValue === true) { - scope.editor.content.isDirty = true; - } - }); - - }, - scope: { - //TODO: This should be turned into a proper component - - page: "=", - content: "=", - editor: "=", - editors: "=", - //TODO: I don't like having this callback defined and would like to make this directive a bit less - // coupled but right now don't have time - initVariant: "&", - onSplitViewChanged: "&" + })); + } + + function onDestroy() { + for (var i = 0; i < unsubscribe.length; i++) { + unsubscribe[i](); } + } + + function backToListView() { + $location.path(vm.page.listViewPath); }; - return directive; + /** + * Used to proxy a callback + * @param {any} variant + */ + function selectVariant(variant) { + if (vm.onSelectVariant) { + vm.onSelectVariant({ "variant": variant }); + } + } + /** + * Used to proxy a callback + * @param {any} variant + */ + function openSplitView(variant) { + if (vm.onOpenSplitView) { + vm.onOpenSplitView({ "variant": variant }); + } + } } - angular.module('umbraco.directives').directive('umbVariantContent', variantContentDirective); + angular.module('umbraco.directives').component('umbVariantContent', umbVariantContent); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js new file mode 100644 index 0000000000..5e2775de73 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -0,0 +1,280 @@ +(function () { + 'use strict'; + + /** + * A component for split view content editing + */ + var umbVariantContentEditors = { + templateUrl: 'views/components/content/umb-variant-content-editors.html', + bindings: { + page: "<", + content: "<", //TODO: Not sure if this should be = since we are changing the 'active' property of a variant + culture: "<" + }, + controllerAs: 'vm', + controller: umbVariantContentEditorsController + }; + + function umbVariantContentEditorsController($scope, $element, $location, $timeout) { + + var prevContentDateUpdated = null; + + var vm = this; + + vm.$onInit = onInit; + vm.$onChanges = onChanges; + vm.$doCheck = doCheck; + vm.$postLink = postLink; + + vm.openSplitView = openSplitView; + vm.closeSplitView = closeSplitView; + vm.selectVariant = selectVariant; + + //Used to track how many content views there are (for split view there will be 2, it could support more in theory) + vm.editors = []; + + /** Called when the component initializes */ + function onInit() { + prevContentDateUpdated = angular.copy(vm.content.updateDate); + setActiveCulture(); + } + + /** Called when the component has linked all elements, this is when the form controller is available */ + function postLink() { + + } + + /** + * Watch for model changes + * @param {any} changes + */ + function onChanges(changes) { + + if (changes.culture && !changes.culture.isFirstChange() && changes.culture.currentValue !== changes.culture.previousValue) { + setActiveCulture(); + } + } + + /** Allows us to deep watch whatever we want - executes on every digest cycle */ + function doCheck() { + if (!angular.equals(vm.content.updateDate, prevContentDateUpdated)) { + setActiveCulture(); + prevContentDateUpdated = angular.copy(vm.content.updateDate); + } + } + + /** This is called when the split view changes based on the umb-variant-content */ + function splitViewChanged() { + //send an event downwards + $scope.$broadcast("editors.content.splitViewChanged", { editors: vm.editors }); + } + + /** + * Set the active variant based on the current culture (query string) + */ + function setActiveCulture() { + // set the active variant + var activeVariant = null; + _.each(vm.content.variants, function (v) { + if (v.language && v.language.culture === vm.culture) { + v.active = true; + activeVariant = v; + } + else { + v.active = false; + } + }); + if (!activeVariant) { + // Set the first variant to active if we can't find it. + // If the content item is invariant, then only one item exists in the array. + vm.content.variants[0].active = true; + activeVariant = vm.content.variants[0]; + } + + insertVariantEditor(0, initVariant(activeVariant)); + + if (vm.editors.length > 1) { + //now re-sync any other editor content (i.e. if split view is open) + for (var s = 1; s < vm.editors.length; s++) { + //get the variant from the scope model + var variant = _.find(vm.content.variants, function (v) { + return v.language.culture === vm.editors[s].content.language.culture; + }); + vm.editors[s].content = initVariant(variant); + } + } + + } + + /** + * Updates the editors collection for a given index for the specified variant + * @param {any} index + * @param {any} variant + */ + function insertVariantEditor(index, variant) { + + var variantCulture = variant.language ? variant.language.culture : "invariant"; + + //check if the culture at the index is the same, if it's null an editor will be added + var currentCulture = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].culture; + + if (currentCulture !== variantCulture) { + //Not the current culture which means we need to modify the array. + //NOTE: It is not good enough to just replace the `content` object at a given index in the array + // since that would mean that directives are not re-initialized. + vm.editors.splice(index, 1, { + content: variant, + //used for "track-by" ng-repeat + culture: variantCulture + }); + } + else { + //replace the editor for the same culture + vm.editors[index].content = variant; + } + } + + function initVariant(variant) { + //The model that is assigned to the editor contains the current content variant along + //with a copy of the contentApps. This is required because each editor renders it's own + //header and content apps section and the content apps contains the view for editing content itself + //and we need to assign a view model to the subView so that it is scoped to the current + //editor so that split views work. + + //copy the apps from the main model if not assigned yet to the variant + if (!variant.apps) { + variant.apps = angular.copy(vm.content.apps); + } + + //if this is a variant has a culture/language than we need to assign the language drop down info + if (variant.language) { + //if the variant list that defines the header drop down isn't assigned to the variant then assign it now + if (!variant.variants) { + variant.variants = _.map(vm.content.variants, + function (v) { + return _.pick(v, "active", "language", "state"); + }); + } + else { + //merge the scope variants on top of the header variants collection (handy when needing to refresh) + angular.extend(variant.variants, + _.map(vm.content.variants, + function (v) { + return _.pick(v, "active", "language", "state"); + })); + } + + //ensure the current culture is set as the active one + for (var i = 0; i < variant.variants.length; i++) { + if (variant.variants[i].language.culture === variant.language.culture) { + variant.variants[i].active = true; + } + else { + variant.variants[i].active = false; + } + } + } + + //then assign the variant to a view model to the content app + var contentApp = _.find(variant.apps, function (a) { + return a.alias === "content"; + }); + contentApp.viewModel = variant; + + return variant; + } + /** + * Adds a new editor to the editors array to show content in a split view + * @param {any} selectedVariant + */ + function openSplitView(selectedVariant) { + + var selectedCulture = selectedVariant.language.culture; + + //only the content app can be selected since no other apps are shown, and because we copy all of these apps + //to the "editors" we need to update this across all editors + for (var e = 0; e < vm.editors.length; e++) { + var editor = vm.editors[e]; + for (var i = 0; i < editor.content.apps.length; i++) { + var app = editor.content.apps[i]; + if (app.alias === "content") { + app.active = true; + } + else { + app.active = false; + } + } + } + + //Find the whole variant model based on the culture that was chosen + var variant = _.find(vm.content.variants, function (v) { + return v.language.culture === selectedCulture; + }); + + insertVariantEditor(vm.editors.length, initVariant(variant)); + + //TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular + editor.collapsed = true; + editor.loading = true; + $timeout(function () { + editor.collapsed = false; + editor.loading = false; + splitViewChanged(); + }, 100); + } + + /** Closes the split view */ + function closeSplitView(editorIndex) { + //TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular + var editor = vm.editors[editorIndex]; + editor.loading = true; + editor.collapsed = true; + $timeout(function () { + vm.editors.splice(editorIndex, 1); + splitViewChanged(); + }, 400); + } + + /** + * Changes the currently selected variant + * @param {any} variant This is the model of the variant/language drop down item in the editor header + * @param {any} editorIndex The index of the editor being changed + */ + function selectVariant(variant, editorIndex) { + + var variantCulture = variant.language ? variant.language.culture : "invariant"; + + //if the editor index is zero, then update the query string to track the lang selection, otherwise if it's part + //of a 2nd split view editor then update the model directly. + if (editorIndex === 0) { + //If we've made it this far, then update the query string. + //The editor will respond to this query string changing. + $location.search("cculture", variant.language.culture); + } + else { + + //Update the 'active' variant for this editor + var editor = vm.editors[editorIndex]; + //set all variant drop down items as inactive for this editor and then set the selected one as active + for (var i = 0; i < editor.content.variants.length; i++) { + editor.content.variants[i].active = false; + } + variant.active = true; + + //get the variant content model and initialize the editor with that + var contentVariant = _.find(vm.content.variants, + function (v) { + return v.language.culture === variant.language.culture; + }); + editor.content = initVariant(contentVariant); + + //update the editors collection + insertVariantEditor(editorIndex, contentVariant); + + } + } + } + + angular.module('umbraco.directives').component('umbVariantContentEditors', umbVariantContentEditors); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html index fb131d9055..bd71eb6691 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html @@ -5,23 +5,11 @@
- -
-
- - - -
-
+ + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html new file mode 100644 index 0000000000..c4212adc58 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html @@ -0,0 +1,17 @@ +
+
+ + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html index f0c1132005..d6e03669c5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content.html @@ -1,31 +1,31 @@ 
- + -
+
- + navigation="vm.editor.content.apps" + variants="vm.editor.content.variants" + hide-change-variant="vm.page.hideChangeVariant" + on-back="vm.backToListView()" + show-back-button="vm.page.listViewPath !== null" + split-view-open="vm.editorCount > 1" + on-open-in-split-view="vm.openSplitView(variant)" + on-close-split-view="vm.onCloseSplitView()" + on-select-variant="vm.selectVariant(variant)">
-
+