diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index e411be2c83..e65a3d238c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -512,6 +512,7 @@ Opens an overlay to show a custom YSOD.
model: "=", view: "=", position: "@", + size: "=?", parentScope: "=?" }, link: link diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js index 366294630b..471714d30b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js @@ -3,7 +3,7 @@ function () { var link = function ($scope) { - + // Clone the model because some property editors // do weird things like updating and config values // so we want to ensure we start from a fresh every @@ -12,10 +12,10 @@ $scope.nodeContext = $scope.model; // Find the selected tab - var selectedTab = $scope.model.tabs[0]; + var selectedTab = $scope.model.variants[0].tabs[0]; if ($scope.tabAlias) { - angular.forEach($scope.model.tabs, function (tab) { + angular.forEach($scope.model.variants[0].tabs, function (tab) { if (tab.alias.toLowerCase() === $scope.tabAlias.toLowerCase()) { selectedTab = tab; return; @@ -31,9 +31,9 @@ // Tell inner controls we are submitting $scope.$broadcast("formSubmitting", { scope: $scope }); - + // Sync the values back - angular.forEach($scope.ngModel.tabs, function (tab) { + angular.forEach($scope.ngModel.variants[0].tabs, function (tab) { if (tab.alias.toLowerCase() === selectedTab.alias.toLowerCase()) { var localPropsMap = selectedTab.properties.reduce(function (map, obj) { @@ -94,4 +94,4 @@ // }, // link: link // } -//}); \ No newline at end of file +//}); diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/truncate.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/truncate.filter.js new file mode 100644 index 0000000000..5efe0f5fff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/filters/truncate.filter.js @@ -0,0 +1,31 @@ +/** + * @ngdoc filter + * @name umbraco.filters.filter:truncate + * @namespace truncateFilter + * + * param {any} wordwise if true, the string will be cut after last fully displayed word. + * param {any} max max length of the outputtet string + * param {any} tail option tail, defaults to: ' ...' + * + * @description + * Limits the length of a string, if a cut happens only the string will be appended with three dots to indicate that more is available. + */ +angular.module("umbraco.filters").filter('truncate', + function () { + return function (value, wordwise, max, tail) { + if (!value) return ''; + max = parseInt(max, 10); + if (!max) return value; + if (value.length <= max) return value; + + value = value.substr(0, max); + if (wordwise) { + var lastspace = value.lastIndexOf(' '); + if (lastspace != -1) { + value = value.substr(0, lastspace); + } + } + return value + (tail || (wordwise ? ' …' : '…')); + }; + } +); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js new file mode 100644 index 0000000000..6be9aba335 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -0,0 +1,203 @@ +/** + * @ngdoc service + * @name umbraco.services.clipboardService + * + * @requires notificationsService + * @requires eventsService + * + * @description + * Service to handle clipboard in general across the application. Responsible for handling the data both storing and retrive. + * The service has a set way for defining a data-set by a entryType and alias, which later will be used to retrive the posible entries for a paste scenario. + * + */ +function clipboardService(notificationsService, eventsService, localStorageService) { + + + var STORAGE_KEY = "umbClipboardService"; + + var retriveStorage = function() { + if (localStorageService.isSupported === false) { + return null; + } + var dataJSON; + var dataString = localStorageService.get(STORAGE_KEY); + if (dataString != null) { + dataJSON = JSON.parse(dataString); + } + + if(dataJSON == null) { + dataJSON = new Object(); + } + + if(dataJSON.entries === undefined) { + dataJSON.entries = []; + } + + return dataJSON; + } + + var saveStorage = function(storage) { + var storageString = JSON.stringify(storage); + + try { + var storageJSON = JSON.parse(storageString); + localStorageService.set(STORAGE_KEY, storageString); + + eventsService.emit("clipboardService.storageUpdate"); + + return true; + } catch(e) { + return false; + } + + return false; + } + + + var service = {}; + + /** + * @ngdoc method + * @name umbraco.services.clipboardService#copy + * @methodOf umbraco.services.clipboardService + * + * @param type {string} umbraco A string defining the type of data to storing, example: 'elementType', 'contentNode' + * @param alias {string} umbraco A string defining the alias of the data to store, example: 'product' + * @param data {object} umbraco A object containing the properties to be saved. + * + * @description + * Saves a single JS-object with a type and alias to the clipboard. + */ + service.copy = function(type, alias, data) { + + var storage = retriveStorage(); + + var shallowCloneData = Object.assign({}, data);// Notice only a shallow copy, since we dont need to deep copy. (that will happen when storing the data) + delete shallowCloneData.key; + delete shallowCloneData.$$hashKey; + + var key = data.key || data.$$hashKey || console.error("missing unique key for this content"); + + // remove previous copies of this entry: + storage.entries = storage.entries.filter( + (entry) => { + return entry.unique !== key; + } + ); + + var entry = {unique:key, type:type, alias:alias, data:shallowCloneData}; + storage.entries.push(entry); + + if (saveStorage(storage) === true) { + notificationsService.success("Clipboard", "Copied to clipboard."); + } else { + notificationsService.success("Clipboard", "Couldnt copy this data to clipboard."); + } + + }; + + + /** + * @ngdoc method + * @name umbraco.services.supportsCopy#supported + * @methodOf umbraco.services.clipboardService + * + * @description + * Determins wether the current browser is able to performe its actions. + */ + service.isSupported = function() { + return localStorageService.isSupported; + }; + + /** + * @ngdoc method + * @name umbraco.services.supportsCopy#hasEntriesOfType + * @methodOf umbraco.services.clipboardService + * + * @param type {string} umbraco A string defining the type of data test for. + * @param aliases {string} umbraco A array of strings providing the alias of the data you want to test for. + * + * @description + * Determines whether the current clipboard has entries that match a given type and one of the aliases. + */ + service.hasEntriesOfType = function(type, aliases) { + + if(service.retriveEntriesOfType(type, aliases).length > 0) { + return true; + } + + return false; + }; + + /** + * @ngdoc method + * @name umbraco.services.supportsCopy#retriveEntriesOfType + * @methodOf umbraco.services.clipboardService + * + * @param type {string} umbraco A string defining the type of data to recive. + * @param aliases {string} umbraco A array of strings providing the alias of the data you want to recive. + * + * @description + * Returns an array of entries matching the given type and one of the provided aliases. + */ + service.retriveEntriesOfType = function(type, aliases) { + + var storage = retriveStorage(); + + // Find entries that are fulfilling the criteria for this nodeType and nodeTypesAliases. + var filteretEntries = storage.entries.filter( + (entry) => { + return (entry.type === type && aliases.filter(alias => alias === entry.alias).length > 0); + } + ); + + return filteretEntries; + }; + + /** + * @ngdoc method + * @name umbraco.services.supportsCopy#retriveEntriesOfType + * @methodOf umbraco.services.clipboardService + * + * @param type {string} umbraco A string defining the type of data to recive. + * @param aliases {string} umbraco A array of strings providing the alias of the data you want to recive. + * + * @description + * Returns an array of data of entries matching the given type and one of the provided aliases. + */ + service.retriveDataOfType = function(type, aliases) { + return service.retriveEntriesOfType(type, aliases).map((x) => x.data); + }; + + /** + * @ngdoc method + * @name umbraco.services.supportsCopy#retriveEntriesOfType + * @methodOf umbraco.services.clipboardService + * + * @param type {string} umbraco A string defining the type of data to remove. + * @param aliases {string} umbraco A array of strings providing the alias of the data you want to remove. + * + * @description + * Removes entries matching the given type and one of the provided aliases. + */ + service.clearEntriesOfType = function(type, aliases) { + + var storage = retriveStorage(); + + // Find entries that are NOT fulfilling the criteria for this nodeType and nodeTypesAliases. + var filteretEntries = storage.entries.filter( + (entry) => { + return !(entry.type === type && aliases.filter(alias => alias === entry.alias).length > 0); + } + ); + + storage.entries = filteretEntries; + + saveStorage(storage); + }; + + + + return service; +} +angular.module("umbraco.services").factory("clipboardService", clipboardService); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js index 6de0b4170b..e853e07092 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js @@ -27,6 +27,11 @@ overlay.position = "center"; } + // set the default overlay size to small + if(!overlay.size) { + overlay.size = "small"; + } + // use a default empty view if nothing is set if(!overlay.view) { overlay.view = "views/common/overlays/default/default.html"; @@ -72,4 +77,4 @@ angular.module("umbraco.services").factory("overlayService", overlayService); -})(); \ No newline at end of file +})(); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 441f52c81b..1e48500bb0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -130,6 +130,7 @@ @import "components/tooltip/umb-tooltip.less"; @import "components/tooltip/umb-tooltip-list.less"; @import "components/overlays/umb-overlay-backdrop.less"; +@import "components/overlays/umb-itempicker.less"; @import "components/umb-grid.less"; @import "components/umb-empty-state.less"; @import "components/umb-property-editor.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less index 0f4f2a7a9d..8324698685 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/card.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -6,6 +6,7 @@ position: relative; padding: 5px 10px 5px 10px; background: white; + width: 100%; .title{padding: 12px; color: @gray-3; border-bottom: 1px solid @gray-8; font-weight: 400; font-size: 16px; text-transform: none; margin: 0 -10px 10px -10px;} @@ -84,63 +85,73 @@ padding: 0; margin: 0 auto; list-style: none; - + width: 100%; + display: flex; flex-flow: row wrap; justify-content: flex-start; } .umb-card-grid li { - padding: 5px; overflow: hidden; font-size: 12px; text-align: center; - width: 100px; box-sizing: border-box; position: relative; + width: 100px; } -.umb-card-grid li.-four-in-row { +.umb-card-grid.-four-in-row li { flex: 0 0 25%; max-width: 25%; } -.umb-card-grid li.-three-in-row { +.umb-card-grid.-three-in-row li { flex: 0 0 33.33%; max-width:33.33%; } .umb-card-grid .umb-card-grid-item { + position: relative; display: block; width: 100%; - height: 100%; + //height: 100%; + padding-top: 100%; border-radius: 3px; - padding-bottom: 5px; + transition: background-color 120ms; + + > span { + position: absolute; + top: 10px; + bottom: 10px; + left: 10px; + right: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background-color: transparent; + } } - .umb-card-grid .umb-card-grid-item:hover, - .umb-card-grid .umb-card-grid-item:focus, - .umb-card-grid .umb-card-grid-item:hover > *, - .umb-card-grid .umb-card-grid-item:focus > * { - background: @ui-option-hover; +.umb-card-grid .umb-card-grid-item:hover, +.umb-card-grid .umb-card-grid-item:focus { + background-color: @ui-option-hover; color: @ui-option-type-hover; - cursor: pointer; - outline: none; - border-radius: 3px; } .umb-card-grid a { - color: @ui-option-type; - text-decoration: none; - } + color: @ui-option-type; + text-decoration: none; +} .umb-card-grid i { - font-size: 30px; - line-height: 50px; - display: block; - color: @ui-option-type; - } + font-size: 30px; + line-height: 20px; + margin-bottom: 10px; + display: block; +} .umb-card-grid .umb-card-grid-item__loading { position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 9205dc9c5f..f5050fad85 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -5,51 +5,72 @@ z-index: @zindexUmbOverlay; animation: fadeIn 0.2s; box-shadow: 0 10px 50px rgba(0,0,0,0.1), 0 6px 20px rgba(0,0,0,0.16); + text-align: left; } .umb-overlay__form { display: flex; flex-wrap: nowrap; flex-direction: column; - height: 100%; } .umb-overlay .umb-overlay-header { - //background: @gray-10; border-bottom: 1px solid @purple-l3; - //background: @blueExtraDark; - //color:@u-white; - padding: 10px; margin-top: 0; flex-grow: 0; flex-shrink: 0; + + padding: 20px 30px 0; } -.umb-overlay .umb-overlay__title { + +.umb-overlay__section-header { + width: 100%; + margin-top:30px; + margin-bottom: 10px; + + h5 { + display: inline; + } + + button { + display: inline; + float: right; + background-color: transparent; + border:none; + &:hover { + color: @ui-option-type-hover; + } + } +} + +.umb-overlay__title { font-size: @fontSizeLarge; color: @black; font-weight: bold; margin: 7px 0; } -.umb-overlay .umb-overlay__subtitle { +.umb-overlay__subtitle { font-size: @fontSizeSmall; color: @gray-3; margin: 0; } -.umb-overlay .umb-overlay-container { +.umb-overlay-container { flex-grow: 1; flex-shrink: 1; flex-basis: auto; - overflow-y: auto; - overflow-x: hidden; position: relative; - height: auto; + + padding: 0px 30px; + margin-bottom: 10px; + max-height: calc(100vh - 170px); + overflow-y: auto; } -.umb-overlay .umb-overlay-drawer { +.umb-overlay-drawer { flex-grow: 0; flex-shrink: 0; flex-basis: 31px; @@ -60,16 +81,16 @@ border-top: 1px solid @purple-l3; } -.umb-overlay .umb-overlay-drawer.-auto-height { +.umb-overlay-drawer.-auto-height { flex-basis: auto; } -.umb-overlay .umb-overlay-drawer .umb-overlay-drawer__align-right { +.umb-overlay-drawer .umb-overlay-drawer__align-right { display: flex; justify-content: flex-end; } -.umb-overlay .umb-overlay-drawer .umb-overlay-drawer-content .dropdown-menu { +.umb-overlay-drawer .umb-overlay-drawer-content .dropdown-menu { right: 0; left: auto; } @@ -89,46 +110,44 @@ .umb-overlay.umb-overlay-center .umb-overlay-header { border: none; background: transparent; - padding: 20px 20px 0 20px; + padding: 30px 30px 0; } .umb-overlay.umb-overlay-center .umb-overlay__form { - max-height: 80vh; -} - -.umb-overlay.umb-overlay-center .umb-overlay-container { - padding: 20px; + } .umb-overlay.umb-overlay-center .umb-overlay-drawer { border: none; background: transparent; - padding: 0 20px 20px 20px; + padding: 0 30px 20px; } /* ---------- OVERLAY TARGET ---------- */ .umb-overlay.umb-overlay-target { width: 400px; - height: 400px; + max-height: 100vh; box-sizing: border-box; border-radius: @baseBorderRadius; + /* default: + &.umb-overlay--small { + width: 400px; + } + */ + &.umb-overlay--medium { + width: 480px; + } } .umb-overlay.umb-overlay-target .umb-overlay-header { border: none; background: transparent; - padding: 20px 20px 0 20px; - text-align: center; -} - -.umb-overlay.umb-overlay-target .umb-overlay-container { - padding: 20px; } .umb-overlay.umb-overlay-target .umb-overlay-drawer { border: none; background: transparent; - padding: 0 20px 20px 20px; + padding: 0 30px 20px; } /* ---------- OVERLAY RIGHT ---------- */ @@ -143,14 +162,9 @@ .umb-overlay.umb-overlay-right .umb-overlay-header { flex-basis: 100px; - padding: 20px; box-sizing: border-box; } -.umb-overlay.umb-overlay-right .umb-overlay-container { - padding: 20px; -} - // reset the top position to 0 because we are in a asbolute container and want to // overlay to go all the way to the top .umb-editors .umb-overlay.umb-overlay-right { @@ -175,14 +189,10 @@ .umb-overlay.umb-overlay-left .umb-overlay-header { flex-basis: 100px; - padding: 20px; + padding: 30px 30px 0; box-sizing: border-box; } -.umb-overlay.umb-overlay-left .umb-overlay-container { - padding: 20px; -} - @media (max-width: 767px) { .umb-overlay.umb-overlay-left { margin-left: 61px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less new file mode 100644 index 0000000000..3727c92251 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less @@ -0,0 +1,6 @@ +.umb-itempicker .form-search { + margin-top:10px; +} +.umb-card-grid { + margin-top: 10px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index b49be17a40..f1a6300481 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -87,23 +87,11 @@ .umb-nested-content__icons { opacity: 0; - transition: opacity .15s ease-in-out; + transition: opacity 120ms ease-in-out; position: absolute; - right: 0px; - top: 2px; - background-color: @white; + right: 8px; + top: 4px; padding: 5px; - - &:before { - content: ' '; - position: absolute; - display: block; - width: 30px; - left: -30px; - top: 0; - bottom: 0; - background: linear-gradient(90deg, rgba(255,255,255,0), white); - } } .umb-nested-content__item--active > .umb-nested-content__header-bar { @@ -130,41 +118,22 @@ -.umb-nested-content__icon, -.umb-nested-content__icon.umb-nested-content__icon--disabled:hover { +.umb-nested-content__icon { display: inline-block; - padding: 4px 6px; + padding: 4px; margin: 2px; cursor: pointer; - background: @white; - border: 1px solid @gray-7; - border-radius: 200px; - text-decoration: none !important; + color: @ui-option-type; } -.umb-nested-content__icon.umb-nested-content__icon--disabled:hover { - cursor: default; -} - -.umb-nested-content__icon:hover, -.umb-nested-content__icon--active -{ - color: @white; - background: @blueMid; - border-color: @blueMid; +.umb-nested-content__icon:hover { + color: @ui-option-type-hover; text-decoration: none; } -.umb-nested-content__icon .icon, -.umb-nested-content__icon.umb-nested-content__icon--disabled:hover .icon { +.umb-nested-content__icon .icon { display: block; font-size: 16px !important; - color: @gray-3; -} - -.umb-nested-content__icon:hover .icon, -.umb-nested-content__icon--active .icon { - color: @white; } .umb-nested-content__icon--disabled { @@ -223,16 +192,6 @@ display: none !important; } -.umb-nested-content__help-text { - display: inline-block; - padding: 10px 20px 10px 20px; - clear: both; - font-size: 14px; - color: @gray-3; - background: @gray-10; - border-radius: 15px; -} - .umb-nested-content__doctypepicker table input, .umb-nested-content__doctypepicker table select { width: 100%; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html index 43933f8051..fd859b9e2e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html @@ -33,24 +33,25 @@
-
{{key}}
-
    + @@ -59,18 +60,19 @@
    {{key}}
    -
      + @@ -84,17 +86,18 @@
      {{result.group}}
      -
        + @@ -104,14 +107,15 @@
        {{result.group}}
        -
          + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html index 80d4baff6e..18d0c40b6a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html @@ -23,14 +23,15 @@ umb-auto-focus no-dirty-check />
        - -
          + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html index 1e75a4ba06..66c64657a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html @@ -29,13 +29,14 @@ no-dirty-check />
        -
          + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html index 328344ea84..0fdf303c6b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -1,4 +1,4 @@ -
          +
          -
            +
            +
            Paste from clipboard
            + +
            + + + +
            +
            Create new
            +
            + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediatypepicker/mediatypepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediatypepicker/mediatypepicker.html index bd37800717..161f0b1cf2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediatypepicker/mediatypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediatypepicker/mediatypepicker.html @@ -1,13 +1,14 @@
            -
              + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html index aac2e2665b..f44a73ff2a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html @@ -1,4 +1,4 @@ -
              +
              diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 23e01e3e6f..7a67e07e0c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -12,8 +12,7 @@ ncAlias: "", ncTabAlias: "", nameTemplate: "" - } - ); + }); } $scope.remove = function (index) { @@ -58,7 +57,7 @@ ncResources.getContentTypes().then(function (docTypes) { $scope.model.docTypes = docTypes; - + // Populate document type tab dictionary docTypes.forEach(function (value) { $scope.docTypeTabs[value.alias] = value.tabs; @@ -74,7 +73,6 @@ return docType.alias === c.ncAlias; }); }); - } if (!$scope.model.value) { @@ -93,10 +91,17 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop "contentResource", "localizationService", "iconHelper", - - function ($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper) { + "clipboardService", + "eventsService", + + function ($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService) { var inited = false; + + var contentTypeAliases = []; + _.each($scope.model.config.contentTypes, function (contentType) { + contentTypeAliases.push(contentType.ncAlias); + }); _.each($scope.model.config.contentTypes, function (contentType) { contentType.nameExp = !!contentType.nameTemplate @@ -122,8 +127,9 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.hasContentTypes = $scope.model.config.contentTypes.length > 0; $scope.labels = {}; - localizationService.localizeMany(["grid_insertControl"]).then(function(data) { - $scope.labels.docTypePickerTitle = data[0]; + localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function(data) { + $scope.labels.grid_addElement = data[0]; + $scope.labels.content_createEmpty = data[1]; }); // helper to force the current form into the dirty state @@ -136,7 +142,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.addNode = function (alias) { var scaffold = $scope.getScaffold(alias); - var newNode = initNode(scaffold, null); + var newNode = createNode(scaffold, null); $scope.currentNode = newNode; $scope.setDirty(); @@ -148,14 +154,18 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop } $scope.overlayMenu = { - title: $scope.labels.docTypePickerTitle, show: false, style: {}, - filter: $scope.scaffolds.length > 15 ? true : false, + filter: $scope.scaffolds.length > 12 ? true : false, orderBy: "$index", view: "itempicker", event: $event, - submit: function(model) { + clickPasteItem: function(item) { + $scope.pasteFromClipboard(item.data); + $scope.overlayMenu.show = false; + $scope.overlayMenu = null; + }, + submit: function(model) { if(model && model.selectedItem) { $scope.addNode(model.selectedItem.alias); } @@ -181,13 +191,35 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop if ($scope.overlayMenu.availableItems.length === 0) { return; } - - if ($scope.overlayMenu.availableItems.length === 1) { + + $scope.overlayMenu.size = $scope.overlayMenu.availableItems.length > 6 ? "medium" : "small"; + + $scope.overlayMenu.pasteItems = []; + var availableNodesForPaste = clipboardService.retriveDataOfType("elementType", contentTypeAliases); + _.each(availableNodesForPaste, function (node) { + $scope.overlayMenu.pasteItems.push({ + alias: node.contentTypeAlias, + name: node.name, //contentTypeName + data: node, + icon: iconHelper.convertFromLegacyIcon(node.icon) + }); + }); + + $scope.overlayMenu.title = $scope.overlayMenu.pasteItems.length > 0 ? $scope.labels.grid_addElement : $scope.labels.content_createEmpty; + + $scope.overlayMenu.clickClearPaste = function($event) { + $event.stopPropagation(); + $event.preventDefault(); + clipboardService.clearEntriesOfType("elementType", contentTypeAliases); + $scope.overlayMenu.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. + }; + + if ($scope.overlayMenu.availableItems.length === 1 && $scope.overlayMenu.pasteItems.length === 0) { // only one scaffold type - no need to display the picker $scope.addNode($scope.scaffolds[0].contentTypeAlias); return; } - + $scope.overlayMenu.show = true; }; @@ -201,19 +233,20 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.deleteNode = function (idx) { if ($scope.nodes.length > $scope.model.config.minItems) { - if ($scope.model.config.confirmDeletes && $scope.model.config.confirmDeletes === 1) { - localizationService.localize("content_nestedContentDeleteItem").then(function (value) { - if (confirm(value)) { - $scope.nodes.splice(idx, 1); - $scope.setDirty(); - updateModel(); - } - }); - } else { - $scope.nodes.splice(idx, 1); - $scope.setDirty(); - updateModel(); - } + $scope.nodes.splice(idx, 1); + $scope.setDirty(); + updateModel(); + } + }; + $scope.requestDeleteNode = function (idx) { + if ($scope.model.config.confirmDeletes === true) { + localizationService.localize("content_nestedContentDeleteItem").then(function (value) { + if (confirm(value)) { + $scope.deleteNode(idx); + } + }); + } else { + $scope.deleteNode(idx); } }; @@ -247,20 +280,22 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop if ($scope.nodes[idx].name !== name) { $scope.nodes[idx].name = name; } - - + return name; }; - + $scope.getIcon = function (idx) { var scaffold = $scope.getScaffold($scope.model.value[idx].ncContentTypeAlias); return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : "icon-folder"; } - $scope.sortableOptions = { axis: "y", cursor: "move", - handle: ".umb-nested-content__icon--move", + handle:'.umb-nested-content__header-bar', + distance: 10, + opacity: 0.7, + tolerance: "pointer", + scroll: true, start: function (ev, ui) { updateModel(); // Yea, yea, we shouldn't modify the dom, sue me @@ -298,7 +333,40 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop return contentType.ncAlias === alias; }); } - + + $scope.showCopy = clipboardService.isSupported(); + + $scope.showPaste = false; + + $scope.clickCopy = function($event, node) { + + syncCurrentNode(); + + clipboardService.copy("elementType", node.contentTypeAlias, node); + $event.stopPropagation(); + } + + $scope.pasteFromClipboard = function(newNode) { + + if (newNode === undefined) { + return; + } + + // generate a new key. + newNode.key = String.CreateGuid(); + + $scope.nodes.push(newNode); + //updateModel();// done by setting current node... + + $scope.currentNode = newNode; + } + + function checkAbilityToPasteContent() { + $scope.showPaste = clipboardService.hasEntriesOfType("elementType", contentTypeAliases); + } + + eventsService.on("clipboardService.storageUpdate", checkAbilityToPasteContent); + var notSupported = [ "Umbraco.Tags", "Umbraco.UploadField", @@ -317,9 +385,9 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop var tab = _.find(tabs, function (tab) { return tab.id !== 0 && (tab.alias.toLowerCase() === contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias === ""); }); - scaffold.tabs = []; + scaffold.variants[0].tabs = []; if (tab) { - scaffold.tabs.push(tab); + scaffold.variants[0].tabs.push(tab); angular.forEach(tab.properties, function (property) { @@ -348,7 +416,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop if ($scope.model.config.contentTypes.length === scaffoldsLoaded) { // Because we're loading the scaffolds async one at a time, we need to // sort them explicitly according to the sort order defined by the data type. - var contentTypeAliases = []; + contentTypeAliases = []; _.each($scope.model.config.contentTypes, function (contentType) { contentTypeAliases.push(contentType.ncAlias); }); @@ -365,7 +433,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop // No such scaffold - the content type might have been deleted. We need to skip it. continue; } - initNode(scaffold, item); + createNode(scaffold, item); } } @@ -382,64 +450,78 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop } inited = true; + + checkAbilityToPasteContent(); } } - - var initNode = function (scaffold, item) { + + function createNode(scaffold, fromNcEntry) { var node = angular.copy(scaffold); - - node.key = item && item.key ? item.key : UUID.generate(); - node.ncContentTypeAlias = scaffold.contentTypeAlias; - - for (var t = 0; t < node.tabs.length; t++) { - var tab = node.tabs[t]; - for (var p = 0; p < tab.properties.length; p++) { - var prop = tab.properties[p]; - prop.propertyAlias = prop.alias; - prop.alias = $scope.model.alias + "___" + prop.alias; - // Force validation to occur server side as this is the - // only way we can have consistency between mandatory and - // regex validation messages. Not ideal, but it works. - prop.validation = { - mandatory: false, - pattern: "" - }; - if (item) { - if (item[prop.propertyAlias]) { - prop.value = item[prop.propertyAlias]; + + node.key = fromNcEntry && fromNcEntry.key ? fromNcEntry.key : String.CreateGuid(); + + for (var v = 0; v < node.variants.length; v++) { + var variant = node.variants[v]; + + 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]; + + prop.propertyAlias = prop.alias; + prop.alias = $scope.model.alias + "___" + prop.alias; + // Force validation to occur server side as this is the + // only way we can have consistency between mandatory and + // regex validation messages. Not ideal, but it works. + prop.validation = { + mandatory: false, + pattern: "" + }; + + if (fromNcEntry && fromNcEntry[prop.propertyAlias]) { + prop.value = fromNcEntry[prop.propertyAlias]; } } } } - + $scope.nodes.push(node); return node; } - - var updateModel = function () { + + function convertNodeIntoNCEntry(node) { + var obj = { + key: node.key, + name: node.name, + ncContentTypeAlias: node.contentTypeAlias + }; + for (var t = 0; t < node.variants[0].tabs.length; t++) { + var tab = node.variants[0].tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (typeof prop.value !== "function") { + obj[prop.propertyAlias] = prop.value; + } + } + } + return obj; + } + + function syncCurrentNode() { if ($scope.realCurrentNode) { $scope.$broadcast("ncSyncVal", { key: $scope.realCurrentNode.key }); } + } + + function updateModel() { + syncCurrentNode(); + if (inited) { var newValues = []; for (var i = 0; i < $scope.nodes.length; i++) { - var node = $scope.nodes[i]; - var newValue = { - key: node.key, - name: node.name, - ncContentTypeAlias: node.ncContentTypeAlias - }; - for (var t = 0; t < node.tabs.length; t++) { - var tab = node.tabs[t]; - for (var p = 0; p < tab.properties.length; p++) { - var prop = tab.properties[p]; - if (typeof prop.value !== "function") { - newValue[prop.propertyAlias] = prop.value; - } - } - } - newValues.push(newValue); + newValues.push(convertNodeIntoNCEntry($scope.nodes[i])); } $scope.model.value = newValues; } @@ -457,23 +539,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.$on("$destroy", function () { unsubscribe(); }); - - // TODO: Move this into a shared location? - var UUID = (function () { - var self = {}; - var lut = []; for (var i = 0; i < 256; i++) { lut[i] = (i < 16 ? "0" : "") + (i).toString(16); } - self.generate = function () { - var d0 = Math.random() * 0xffffffff | 0; - var d1 = Math.random() * 0xffffffff | 0; - var d2 = Math.random() * 0xffffffff | 0; - var d3 = Math.random() * 0xffffffff | 0; - return lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + "-" + - lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + "-" + lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + "-" + - lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + "-" + lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] + - lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] + lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff]; - } - return self; - })(); + } ]); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html index 1f6b986c64..a8a77e2d53 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -3,29 +3,26 @@ ng-class="{'umb-nested-content--narrow':!wideMode, 'umb-nested-content--wide':wideMode}"> -
              +
              -
              +
              -
              +
              @@ -49,6 +46,7 @@ diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 168a01d66c..79ac6bc641 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -284,7 +284,9 @@ Klar til at udgive? Klar til at gemme? Send til godkendelse - Vælg dato og klokkeslæt for at udgive og/eller afpublicere indholdet. + Vælg dato og klokkeslæt for at udgive og/eller afpublicere indholdet. + Opret ny + Indsæt fra udklipsmappen Opret en ny indholdsskabelon fra '%0%' diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 9678903975..d00f428b09 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -293,7 +293,9 @@ Ready to Publish? Ready to Save? Send for approval - Select the date and time to publish and/or unpublish the content item. + Select the date and time to publish and/or unpublish the content item. + Create new + Paste from clipboard Create a new Content Template from '%0%' diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index af6add9f64..133901732a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -297,7 +297,9 @@ Ready to Publish? Ready to Save? Send for approval - Select the date and time to publish and/or unpublish the content item. + Select the date and time to publish and/or unpublish the content item. + Create new + Paste from clipboard Create a new Content Template from '%0%' @@ -1408,7 +1410,7 @@ To manage your website, simply open the Umbraco back office and start adding con User %0% was deleted Invite user Invitation has been re-sent to %0% - Cannot publish the document since the required '%0%' is not published + Cannot publish the document since the required '%0%' is not published Validation failed for language '%0%' Document type was exported to file An error occurred while exporting the document type