From 09be6b8ee4707c2b0d161bc8464b92c8bcdbcdc6 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Fri, 27 Mar 2020 15:41:48 -0700 Subject: [PATCH] Enable Block List Editor settings editing --- .../common/services/blockeditor.service.js | 86 +++++++++++++------ .../blockeditor/blockeditor.controller.js | 42 +++++++-- .../blockeditor/blockeditor.html | 24 ++++-- .../blocklist/blocklist.component.js | 31 ++++--- .../Umbraco/js/main.controller.js | 18 +++- .../Umbraco/js/navigation.controller.js | 61 +++++++++---- .../Views/Partials/BlockList/Default.cshtml | 3 +- .../BlockListPropertyValueConverter.cs | 8 ++ 8 files changed, 200 insertions(+), 73 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js index 8df95bb684..8703aeb385 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -56,7 +56,21 @@ * @param {Object} toModel ElementTypeModel to recive property values from. */ function mapElementTypeValues(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++) { @@ -81,6 +95,31 @@ } + /** + * 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 : 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. + blockModel.watchers.push(isolatedScope.$watch("blockModels._" + blockModel.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockModel, prop))); + } + } + } + /** * Used to create a scoped watcher for a content property on a blockModel. */ @@ -225,13 +264,13 @@ blockModel.config = angular.copy(blockConfiguration); blockModel.labelInterpolator = $interpolate(blockModel.config.label); - var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); - if(scaffold === null) { + var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); + if(contentScaffold === null) { return null; } // make basics from scaffold - blockModel.content = angular.copy(scaffold); + blockModel.content = angular.copy(contentScaffold); blockModel.content.udi = udi; mapToElementTypeModel(blockModel.content, contentModel); @@ -240,37 +279,32 @@ blockModel.layoutModel = layoutEntry; blockModel.watchers = []; - // TODO: implement settings - - // create ElementTypeModel of settings - // store ElementTypeModel in blockModel.settings - // setup watchers for mapping + if (blockConfiguration.settingsElementTypeAlias) { + var settingsScaffold = this.getScaffoldFor(blockConfiguration.settingsElementTypeAlias); + if (settingsScaffold === null) { + return null; + } + // make basics from scaffold + blockModel.settings = angular.copy(settingsScaffold); + layoutEntry.settings = layoutEntry.settings || { key: String.CreateGuid(), contentTypeAlias: blockConfiguration.settingsElementTypeAlias }; + if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); } + if (!layoutEntry.settings.contentTypeAlias) { layoutEntry.settings.contentTypeAlias = blockConfiguration.settingsElementTypeAlias; } + mapToElementTypeModel(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; - - // Start watching each property value. - var variant = blockModel.content.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]; - - // 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(this.isolatedScope.$watch("blockModels._"+blockModel.key+".content.variants[0].tabs["+t+"].properties["+p+"].value", createContentModelPropWatcher(blockModel, prop))); - } - } + addWatchers(blockModel, this.isolatedScope); + addWatchers(blockModel, this.isolatedScope, true); return blockModel; }, - - removeDataAndDestroyModel: function(blockModel) { + removeDataAndDestroyModel: function (blockModel) { this.destroyBlockModel(blockModel); this.removeDataByUdi(blockModel.content.udi); }, @@ -331,7 +365,7 @@ } if (blockConfiguration.settingsElementTypeAlias != null) { - entry.settings = {}; + entry.settings = { key: String.CreateGuid(), contentTypeAlias: blockConfiguration.settingsElementTypeAlias }; } return entry; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js index fea337f31f..7380c9b0c8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js @@ -2,17 +2,49 @@ angular.module("umbraco") .controller("Umbraco.Editors.BlockEditorController", function ($scope) { - var vm = this; + function showContent() { + if (vm.settingsTab) vm.settingsTab.active = false; + vm.contentTab.active = true; + } + + function showSettings() { + if (vm.settingsTab) vm.settingsTab.active = true; + vm.contentTab.active = false; + } + vm.content = $scope.model.content; + vm.settings = $scope.model.settings; + vm.tabs = []; + var settingsOnly = vm.content && vm.content.variants ? false : true; - // TODO: implement settings — do notice that settings is optional. - //vm.settings = $scope.model.settings; + if (!settingsOnly) { + vm.contentTab = { + "name": "Content", + "alias": "content", + "icon": "icon-document", + "action": showContent, + "active": true + }; - vm.title = $scope.model.title; + vm.tabs.push(vm.contentTab); + } - vm.saveAndClose = function() { + if (vm.settings && vm.settings.variants) { + vm.settingsTab = { + "name": "Settings", + "alias": "settings", + "icon": "icon-settings", + "action": showSettings, + "active": settingsOnly + }; + vm.tabs.push(vm.settingsTab); + } + + vm.title = (settingsOnly ? 'SETTINGS: ' : '') + $scope.model.title; + + vm.saveAndClose = function () { if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index b52480c2a7..51750756a8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html @@ -1,15 +1,16 @@
- + + hide-description="true" + on-select-navigation-item="vm.selectTab">
@@ -21,12 +22,19 @@ --> +
+
+
+ +
+
+
- - -
-
- +
+
+
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 209af0323e..6217721cf2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -182,24 +182,29 @@ vm.blocks.forEach(deleteBlock); } - function editBlock(blockModel) { + function editBlock(blockModel, hideContent) { + if (hideContent && !blockModel.config.settingsElementTypeAlias) { + return; + } // make a clone to avoid editing model directly. - var blockContentModelClone = angular.copy(blockModel.content); - - // TODO: implement settings - // Settings should be available as a tab in this overlay: - //var blockSettingsModelClone = angular.copy(blockModel.settings); + var blockContentModelClone = angular.copy(hideContent ? null : blockModel.content); + var blockSettingsModelClone = angular.copy(blockModel.settings); var blockEditorModel = { content: blockContentModelClone, - //settings: blockSettingsModelClone, + settings: blockSettingsModelClone, title: blockModel.label, view: "views/common/infiniteeditors/blockeditor/blockeditor.html", size: blockModel.config.overlaySize || "medium", submit: function(blockEditorModel) { // To ensure syncronization gets tricked we transfer - blockEditorService.mapElementTypeValues(blockEditorModel.content, blockModel.content) + if (!hideContent) { + blockEditorService.mapElementTypeValues(blockEditorModel.content, blockModel.content) + } + if (blockModel.config.settingsElementTypeAlias) { + blockEditorService.mapElementTypeValues(blockEditorModel.settings, blockModel.settings) + } editorService.close(); }, close: function() { @@ -244,10 +249,14 @@ vm.blockTypePicker.close(); }, submit: function (model) { + var added = false; if (model && model.selectedItem) { - addNewBlock(createIndex, model.selectedItem.alias); + added = addNewBlock(createIndex, model.selectedItem.alias); } vm.blockTypePicker.close(); + if (added && vm.blocks.length > createIndex) { + editBlock(vm.blocks[createIndex]); + } }, close: function () { vm.blockTypePicker.show = false; @@ -379,8 +388,8 @@ }); } - function openSettingsForBlock() { - alert("settings not implemented jet."); + function openSettingsForBlock(block) { + editBlock(block, true); } diff --git a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js index 883907d1dc..d026cdb305 100644 --- a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js +++ b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js @@ -32,13 +32,17 @@ function MainController($scope, $location, appState, treeService, notificationsS // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { if (evt.keyCode === 9) { - $scope.tabbingActive = true; - $scope.$digest(); - window.removeEventListener('keydown', handleFirstTab); - window.addEventListener('mousedown', disableTabbingActive); + enableTabbingActive(); } } + function enableTabbingActive() { + $scope.tabbingActive = true; + $scope.$digest(); + window.addEventListener('mousedown', disableTabbingActive); + window.removeEventListener("keydown", handleFirstTab); + } + function disableTabbingActive(evt) { $scope.tabbingActive = false; $scope.$digest(); @@ -48,6 +52,12 @@ function MainController($scope, $location, appState, treeService, notificationsS window.addEventListener("keydown", handleFirstTab); + $scope.$on("showFocusOutline", function() { + $scope.tabbingActive = true; + window.addEventListener('mousedown', disableTabbingActive); + window.removeEventListener("keydown", handleFirstTab); + }); + $scope.removeNotification = function (index) { notificationsService.remove(index); diff --git a/src/Umbraco.Web.UI/Umbraco/js/navigation.controller.js b/src/Umbraco.Web.UI/Umbraco/js/navigation.controller.js index 194c45afe6..281be2d331 100644 --- a/src/Umbraco.Web.UI/Umbraco/js/navigation.controller.js +++ b/src/Umbraco.Web.UI/Umbraco/js/navigation.controller.js @@ -257,6 +257,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar evts.push(eventsService.on("app.ready", function (evt, data) { $scope.authenticated = true; ensureInit(); + ensureMainCulture(); })); // event for infinite editors @@ -279,8 +280,22 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } })); - - + /** + * For multi language sites, this ensures that mculture is set to either the last selected language or the default one + */ + function ensureMainCulture() { + if ($location.search().mculture) { + return; + } + var language = lastLanguageOrDefault(); + if (!language) { + return; + } + // trigger a language selection in the next digest cycle + $timeout(function () { + $scope.selectLanguage(language); + }); + } /** * Based on the current state of the application, this configures the scope variables that control the main tree and language drop down @@ -385,28 +400,19 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar if ($scope.languages.length > 1) { //if there's already one set, check if it exists - var currCulture = null; + var language = null; var mainCulture = $location.search().mculture; if (mainCulture) { - currCulture = _.find($scope.languages, function (l) { + language = _.find($scope.languages, function (l) { return l.culture.toLowerCase() === mainCulture.toLowerCase(); }); } - if (!currCulture) { - // no culture in the request, let's look for one in the cookie that's set when changing language - var defaultCulture = $cookies.get("UMB_MCULTURE"); - if (!defaultCulture || !_.find($scope.languages, function (l) { - return l.culture.toLowerCase() === defaultCulture.toLowerCase(); - })) { - // no luck either, look for the default language - var defaultLang = _.find($scope.languages, function (l) { - return l.isDefault; - }); - if (defaultLang) { - defaultCulture = defaultLang.culture; - } + if (!language) { + language = lastLanguageOrDefault(); + + if (language) { + $location.search("mculture", language.culture); } - $location.search("mculture", defaultCulture ? defaultCulture : null); } } @@ -431,6 +437,25 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar }); }); } + + function lastLanguageOrDefault() { + if (!$scope.languages || $scope.languages.length <= 1) { + return null; + } + // see if we can find a culture in the cookie set when changing language + var lastCulture = $cookies.get("UMB_MCULTURE"); + var language = lastCulture ? _.find($scope.languages, function (l) { + return l.culture.toLowerCase() === lastCulture.toLowerCase(); + }) : null; + if (!language) { + // no luck, look for the default language + language = _.find($scope.languages, function (l) { + return l.isDefault; + }); + } + return language; + } + function nodeExpandedHandler(args) { //store the reference to the expanded node path if (args.node) { diff --git a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml index 56031628c3..a9f66d7419 100644 --- a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml @@ -1,4 +1,5 @@ @inherits UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; @using Umbraco.Core.Models.Blocks @{ if (Model?.Layout == null || !Model.Layout.Any()) { return; } @@ -8,6 +9,6 @@ { if (layout?.Udi == null) { continue; } var data = Model.GetData(layout.Udi); - @Html.Partial("BlockList/" + data.ContentType.Alias, data); + @Html.Partial("BlockList/" + data.ContentType.Alias, (data, layout.Settings)) }
diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 4e2a66489c..1e013e851c 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -59,6 +59,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.Blocks; + var contentTypeMap = contentTypes.ToDictionary(x => x.Alias); var elements = (contentTypes.Length == 1 ? (IList)_publishedModelFactory.CreateModelList(contentTypes[0].Alias) : new List()) @@ -106,6 +107,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (!elements.TryGetValue(guidUdi.Guid, out var data)) continue; + if (!contentTypeMap.TryGetValue(data.ContentType.Alias, out var blockConfig)) + continue; + + // this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again + if (element != null && string.IsNullOrWhiteSpace(blockConfig.SettingsElementTypeAlias)) + element = null; + var layoutRef = new BlockListLayoutReference(udi, element); layout.Add(layoutRef); }