Merge pull request #5353 from umbraco/v8/feature/ux-copy-paste-service-for-nested-content

V8: ClipboardService + implementation for nested content
This commit is contained in:
Warren Buckley
2019-05-09 15:42:30 +01:00
committed by GitHub
21 changed files with 603 additions and 275 deletions

View File

@@ -512,6 +512,7 @@ Opens an overlay to show a custom YSOD. </br>
model: "=",
view: "=",
position: "@",
size: "=?",
parentScope: "=?"
},
link: link

View File

@@ -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
// }
//});
//});

View File

@@ -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 ? ' …' : '…'));
};
}
);

View File

@@ -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;
}

View File

@@ -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);
})();
})();

View File

@@ -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";

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
.umb-itempicker .form-search {
margin-top:10px;
}
.umb-card-grid {
margin-top: 10px;
}

View File

@@ -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%;

View File

@@ -33,24 +33,25 @@
<umb-load-indicator ng-if="vm.loading"></umb-load-indicator>
<!-- TABS -->
<div ng-if="vm.showTabs">
<umb-tabs-nav
ng-if="vm.tabs"
tabs="vm.tabs"
<umb-tabs-nav
ng-if="vm.tabs"
tabs="vm.tabs"
on-tab-change="vm.onTabChange(tab)">
</umb-tabs-nav>
<umb-tab-content ng-repeat="tab in vm.tabs" tab="tab" ng-if="tab.active">
<div ng-if="tab.alias==='Default'">
<div ng-repeat="(key,value) in tab.typesAndEditors">
<h5>{{key}}</h5>
<ul class="umb-card-grid" ng-mouseleave="vm.hideDetailsOverlay()">
<ul class="umb-card-grid -four-in-row" ng-mouseleave="vm.hideDetailsOverlay()">
<li ng-repeat="systemDataType in value | orderBy:'name'"
data-element="editor-{{systemDataType.name}}"
ng-mouseover="vm.showDetailsOverlay(systemDataType)"
ng-click="vm.pickEditor(systemDataType)"
class="-four-in-row">
ng-click="vm.pickEditor(systemDataType)">
<a class="umb-card-grid-item" href="" title="{{ systemDataType.name }}">
<i class="{{ systemDataType.icon }}" ng-class="{'icon-autofill': systemDataType.icon == null}"></i>
{{ systemDataType.name }}
<span>
<i class="{{ systemDataType.icon }}" ng-class="{'icon-autofill': systemDataType.icon == null}"></i>
{{ systemDataType.name }}
</span>
</a>
</li>
</ul>
@@ -59,18 +60,19 @@
<div ng-if="tab.alias==='Reuse'">
<div ng-repeat="(key,value) in tab.userConfigured">
<h5>{{key}}</h5>
<ul class="umb-card-grid" ng-mouseleave="vm.hideDetailsOverlay()">
<ul class="umb-card-grid -four-in-row" ng-mouseleave="vm.hideDetailsOverlay()">
<li ng-repeat="dataType in value | orderBy:'name'"
data-element="editor-{{dataType.name}}"
ng-mouseover="vm.showDetailsOverlay(dataType)"
ng-click="vm.pickDataType(dataType)"
class="-four-in-row">
ng-click="vm.pickDataType(dataType)">
<div ng-if="dataType.loading" class="umb-card-grid-item__loading">
<div class="umb-button__progress"></div>
</div>
<a class="umb-card-grid-item" href="" title="{{ dataType.name }}">
<i class="{{ dataType.icon }}" ng-class="{'icon-autofill': dataType.icon == null}"></i>
{{ dataType.name }}
<span>
<i class="{{ dataType.icon }}" ng-class="{'icon-autofill': dataType.icon == null}"></i>
{{ dataType.name }}
</span>
</a>
</li>
</ul>
@@ -84,17 +86,18 @@
<div ng-repeat="result in vm.filterResult.userConfigured">
<div ng-if="result.dataTypes.length > 0">
<h5>{{result.group}}</h5>
<ul class="umb-card-grid" ng-mouseleave="vm.hideDetailsOverlay()">
<ul class="umb-card-grid -four-in-row" ng-mouseleave="vm.hideDetailsOverlay()">
<li ng-repeat="dataType in result.dataTypes | orderBy:'name'"
ng-mouseover="vm.showDetailsOverlay(dataType)"
ng-click="vm.pickDataType(dataType)"
class="-four-in-row">
ng-click="vm.pickDataType(dataType)">
<div ng-if="dataType.loading" class="umb-card-grid-item__loading">
<div class="umb-button__progress"></div>
</div>
<a class="umb-card-grid-item" href="" title="{{ dataType.name }}">
<i class="{{ dataType.icon }}" ng-class="{'icon-autofill': dataType.icon == null}"></i>
{{ dataType.name }}
<span>
<i class="{{ dataType.icon }}" ng-class="{'icon-autofill': dataType.icon == null}"></i>
{{ dataType.name }}
</span>
</a>
</li>
</ul>
@@ -104,14 +107,15 @@
<div ng-repeat="result in vm.filterResult.typesAndEditors">
<div ng-if="result.dataTypes.length > 0">
<h5>{{result.group}}</h5>
<ul class="umb-card-grid" ng-mouseleave="vm.hideDetailsOverlay()">
<ul class="umb-card-grid -four-in-row" ng-mouseleave="vm.hideDetailsOverlay()">
<li ng-repeat="systemDataType in result.dataTypes | orderBy:'name'"
ng-mouseover="vm.showDetailsOverlay(systemDataType)"
ng-click="vm.pickEditor(systemDataType)"
class="-four-in-row">
ng-click="vm.pickEditor(systemDataType)">
<a class="umb-card-grid-item" href="" title="{{ systemDataType.name }}">
<i class="{{ systemDataType.icon }}" ng-class="{'icon-autofill': systemDataType.icon == null}"></i>
{{ systemDataType.name }}
<span>
<i class="{{ systemDataType.icon }}" ng-class="{'icon-autofill': systemDataType.icon == null}"></i>
{{ systemDataType.name }}
</span>
</a>
</li>
</ul>

View File

@@ -23,14 +23,15 @@
umb-auto-focus
no-dirty-check />
</div>
<ul class="umb-card-grid">
<ul class="umb-card-grid -three-in-row">
<li ng-repeat="availableItem in model.availableItems | compareArrays:model.selectedItems:'alias' | orderBy:'name' | filter:searchTerm"
ng-click="vm.selectItem(availableItem)"
class="-three-in-row">
ng-click="vm.selectItem(availableItem)">
<a class="umb-card-grid-item" href="" title="{{ availableItem.name }}">
<i class="{{ availableItem.icon }}"></i>
{{ availableItem.name }}
<span>
<i class="{{ availableItem.icon }}"></i>
{{ availableItem.name }}
</span>
</a>
</li>
</ul>

View File

@@ -29,13 +29,14 @@
no-dirty-check />
</div>
<ul class="umb-card-grid">
<ul class="umb-card-grid -three-in-row">
<li ng-repeat="availableItem in macros | orderBy:'name' | filter:searchTerm"
ng-click="selectMacro(availableItem)"
class="-three-in-row">
ng-click="selectMacro(availableItem)">
<a class="umb-card-grid-item" href="" title="{{ availableItem.name }}">
<i class="icon-settings-alt"></i>
{{ availableItem.name }}
<span>
<i class="icon-settings-alt"></i>
{{ availableItem.name }}
</span>
</a>
</li>
</ul>

View File

@@ -1,4 +1,4 @@
<div ng-controller="Umbraco.Overlays.ItemPickerOverlay">
<div ng-controller="Umbraco.Overlays.ItemPickerOverlay" class="umb-itempicker">
<div class="form-search" ng-hide="model.filter === false" style="margin-bottom: 15px;">
<i class="icon-search"></i>
@@ -11,13 +11,37 @@
no-dirty-check />
</div>
<ul class="umb-card-grid">
<div class="umb-overlay__section-header" ng-if="(model.pasteItems | filter:searchTerm).length > 0">
<h5><localize key="content_createFromClipboard">Paste from clipboard</localize></h5>
<button ng-if="model.clickClearPaste" ng-click="model.clickClearPaste($event)" alt="Clear clipboard for entries accepted in this context.">
<i class="icon-trash"></i>
</button>
</div>
<ul class="umb-card-grid" ng-class="{'-three-in-row': model.availableItems.length < 7, '-four-in-row': model.availableItems.length >= 7}">
<li ng-repeat="pasteItem in model.pasteItems | filter:searchTerm"
ng-click="model.clickPasteItem(pasteItem)">
<a class="umb-card-grid-item" href="" title="{{ pasteItem.name }}">
<span>
<i class="{{ pasteItem.icon }}"></i>
{{ pasteItem.name | truncate:true:36 }}
</span>
</a>
</li>
</ul>
<div class="umb-overlay__section-header" ng-if="::(model.pasteItems | filter:searchTerm).length > 0">
<h5><localize key="content_createEmpty">Create new</localize></h5>
</div>
<ul class="umb-card-grid" ng-class="{'-three-in-row': model.availableItems.length < 7, '-four-in-row': model.availableItems.length >= 7}">
<li ng-repeat="availableItem in model.availableItems | compareArrays:model.selectedItems:'alias' | orderBy:model.orderBy | filter:searchTerm"
ng-click="selectItem(availableItem)"
class="-three-in-row">
ng-click="selectItem(availableItem)">
<a class="umb-card-grid-item" href="" title="{{ availableItem.name }}">
<i class="{{ availableItem.icon }}"></i>
{{ availableItem.name }}
<span>
<i class="{{ availableItem.icon }}"></i>
{{ availableItem.name }}
</span>
</a>
</li>
</ul>

View File

@@ -1,13 +1,14 @@
<div ng-controller="Umbraco.Overlays.MediaTypePickerController">
<ul class="umb-card-grid">
<ul class="umb-card-grid -three-in-row">
<li
ng-repeat="mediatype in model.acceptedMediatypes | orderBy:'name'"
ng-click="select(mediatype)"
class="-three-in-row">
ng-click="select(mediatype)">
<a class="umb-card-grid-item" href="" title="{{mediatype.name}}">
<i class="{{ mediatype.icon }}"></i>
{{ mediatype.name }}
<span>
<i class="{{ mediatype.icon }}"></i>
{{ mediatype.name }}
</span>
</a>
</li>
</ul>

View File

@@ -1,4 +1,4 @@
<div data-element="overlay" class="umb-overlay umb-overlay-{{position}}" on-outside-click="outSideClick()">
<div data-element="overlay" class="umb-overlay umb-overlay-{{position}} umb-overlay--{{size}}" on-outside-click="outSideClick()">
<ng-form class="umb-overlay__form" name="overlayForm" novalidate val-form-manager>
<div data-element="overlay-header" class="umb-overlay-header">

View File

@@ -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;
})();
}
]);

View File

@@ -3,29 +3,26 @@
ng-class="{'umb-nested-content--narrow':!wideMode, 'umb-nested-content--wide':wideMode}">
<ng-form>
<div class="umb-nested-content__items" ng-hide="nodes.length == 0" ui-sortable="sortableOptions" ng-model="nodes">
<div class="umb-nested-content__items" ng-hide="nodes.length === 0" ui-sortable="sortableOptions" ng-model="nodes">
<div class="umb-nested-content__item" ng-repeat="node in nodes" ng-class="{ 'umb-nested-content__item--active' : $parent.realCurrentNode.key == node.key, 'umb-nested-content__item--single' : $parent.singleMode }">
<div class="umb-nested-content__item" ng-repeat="node in nodes" ng-class="{ 'umb-nested-content__item--active' : $parent.realCurrentNode.key === node.key, 'umb-nested-content__item--single' : $parent.singleMode }">
<div class="umb-nested-content__header-bar" ng-click="$parent.editNode($index)" ng-hide="$parent.singleMode">
<div class="umb-nested-content__heading"><i ng-if="showIcons" class="icon" ng-class="$parent.getIcon($index)"></i><span class="umb-nested-content__item-name" ng-bind="$parent.getName($index)"></span></div>
<div class="umb-nested-content__icons">
<a class="umb-nested-content__icon umb-nested-content__icon--edit" localize="title" title="general_edit" ng-class="{ 'umb-nested-content__icon--active' : $parent.realCurrentNode.id == node.id }" ng-click="$parent.editNode($index); $event.stopPropagation();" ng-show="$parent.maxItems > 1" prevent-default>
<i class="icon icon-edit"></i>
<a class="umb-nested-content__icon umb-nested-content__icon--copy" title="{{copyIconTitle}}" ng-click="clickCopy($event, node);" ng-if="showCopy" prevent-default>
<i class="icon icon-documents"></i>
</a>
<a class="umb-nested-content__icon umb-nested-content__icon--move" localize="title" title="actions_move" ng-click="$event.stopPropagation();" ng-show="$parent.nodes.length > 1" prevent-default>
<i class="icon icon-navigation"></i>
</a>
<a class="umb-nested-content__icon umb-nested-content__icon--delete" localize="title" title="general_delete" ng-class="{ 'umb-nested-content__icon--disabled': $parent.nodes.length <= $parent.minItems }" ng-click="$parent.deleteNode($index); $event.stopPropagation();" prevent-default>
<a class="umb-nested-content__icon umb-nested-content__icon--delete" localize="title" title="general_delete" ng-class="{ 'umb-nested-content__icon--disabled': $parent.nodes.length <= $parent.minItems }" ng-click="$parent.requestDeleteNode($index); $event.stopPropagation();" prevent-default>
<i class="icon icon-trash"></i>
</a>
</div>
</div>
<div class="umb-nested-content__content" ng-if="$parent.realCurrentNode.key == node.key && !$parent.sorting">
<div class="umb-nested-content__content" ng-if="$parent.realCurrentNode.key === node.key && !$parent.sorting">
<umb-nested-content-editor ng-model="node" tab-alias="ncTabAlias" />
</div>
</div>
@@ -49,6 +46,7 @@
<umb-overlay
ng-if="overlayMenu.show"
position="target"
size="overlayMenu.size"
view="overlayMenu.view"
model="overlayMenu">
</umb-overlay>