Merge branch 'dev-v7.7' into temp-content-blueprints

This commit is contained in:
Shannon
2017-06-05 10:23:10 +02:00
31 changed files with 2022 additions and 101 deletions

View File

@@ -0,0 +1,97 @@
angular.module("umbraco.directives").directive('nestedContentEditor', [
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
// time, we'll just sync the value back when we need to
$scope.model = angular.copy($scope.ngModel);
$scope.nodeContext = $scope.model;
// Find the selected tab
var selectedTab = $scope.model.tabs[0];
if ($scope.tabAlias) {
angular.forEach($scope.model.tabs, function (tab) {
if (tab.alias.toLowerCase() === $scope.tabAlias.toLowerCase()) {
selectedTab = tab;
return;
}
});
}
$scope.tab = selectedTab;
// Listen for sync request
var unsubscribe = $scope.$on("ncSyncVal", function (ev, args) {
if (args.key === $scope.model.key) {
// Tell inner controls we are submitting
$scope.$broadcast("formSubmitting", { scope: $scope });
// Sync the values back
angular.forEach($scope.ngModel.tabs, function (tab) {
if (tab.alias.toLowerCase() === selectedTab.alias.toLowerCase()) {
var localPropsMap = selectedTab.properties.reduce(function (map, obj) {
map[obj.alias] = obj;
return map;
}, {});
angular.forEach(tab.properties, function (prop) {
if (localPropsMap.hasOwnProperty(prop.alias)) {
prop.value = localPropsMap[prop.alias].value;
}
});
}
});
}
});
$scope.$on('$destroy', function () {
unsubscribe();
});
};
return {
restrict: "E",
replace: true,
templateUrl: Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/views/propertyeditors/nestedcontent/nestedcontent.editor.html",
scope: {
ngModel: '=',
tabAlias: '='
},
link: link
};
}
]);
//angular.module("umbraco.directives").directive('nestedContentSubmitWatcher', function () {
// var link = function (scope) {
// // call the load callback on scope to obtain the ID of this submit watcher
// var id = scope.loadCallback();
// scope.$on("formSubmitting", function (ev, args) {
// // on the "formSubmitting" event, call the submit callback on scope to notify the nestedContent controller to do it's magic
// if (id === scope.activeSubmitWatcher) {
// scope.submitCallback();
// }
// });
// }
// return {
// restrict: "E",
// replace: true,
// template: "",
// scope: {
// loadCallback: '=',
// submitCallback: '=',
// activeSubmitWatcher: '='
// },
// link: link
// }
//});

View File

@@ -0,0 +1,47 @@
// Filter to take a node id and grab it's name instead
// Usage: {{ pickerAlias | ncNodeName }}
// Cache for node names so we don't make a ton of requests
var ncNodeNameCache = {
id: "",
keys: {}
};
angular.module("umbraco.filters").filter("ncNodeName", function (editorState, entityResource) {
return function (input) {
// Check we have a value at all
if (input === "" || input.toString() === "0") {
return "";
}
var currentNode = editorState.getCurrent();
// Ensure a unique cache per editor instance
var key = "ncNodeName_" + currentNode.key;
if (ncNodeNameCache.id !== key) {
ncNodeNameCache.id = key;
ncNodeNameCache.keys = {};
}
// See if there is a value in the cache and use that
if (ncNodeNameCache.keys[input]) {
return ncNodeNameCache.keys[input];
}
// No value, so go fetch one
// We'll put a temp value in the cache though so we don't
// make a load of requests while we wait for a response
ncNodeNameCache.keys[input] = "Loading...";
entityResource.getById(input, "Document")
.then(function (ent) {
ncNodeNameCache.keys[input] = ent.name;
});
// Return the current value for now
return ncNodeNameCache.keys[input];
};
});

View File

@@ -0,0 +1,12 @@
angular.module('umbraco.resources').factory('Umbraco.PropertyEditors.NestedContent.Resources',
function ($q, $http, umbRequestHelper) {
return {
getContentTypes: function () {
var url = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/backoffice/UmbracoApi/NestedContent/GetContentTypes";
return umbRequestHelper.resourcePromise(
$http.get(url),
'Failed to retrieve content types'
);
},
};
});

View File

@@ -17,7 +17,7 @@ angular.module("umbraco.install").factory('installerService', function($rootScop
//add to umbraco installer facts here
var facts = ['Umbraco helped millions of people watch a man jump from the edge of space',
'Over 370 000 websites are currently powered by Umbraco',
'Over 420 000 websites are currently powered by Umbraco',
"At least 2 people have named their cat 'Umbraco'",
'On an average day, more than 1000 people download Umbraco',
'<a target="_blank" href="http://umbraco.tv">umbraco.tv</a> is the premier source of Umbraco video tutorials to get you started',
@@ -31,7 +31,7 @@ angular.module("umbraco.install").factory('installerService', function($rootScop
"At least 4 people have the Umbraco logo tattooed on them",
"'Umbraco' is the danish name for an allen key",
"Umbraco has been around since 2005, that's a looong time in IT",
"More than 400 people from all over the world meet each year in Denmark in June for our annual conference <a target='_blank' href='http://codegarden15.com'>CodeGarden</a>",
"More than 550 people from all over the world meet each year in Denmark in June for our annual conference <a target='_blank' href='http://codegarden15.com'>CodeGarden</a>",
"While you are installing Umbraco someone else on the other side of the planet is probably doing it too",
"You can extend Umbraco without modifying the source code using either JavaScript or C#",
"Umbraco was installed in more than 165 countries in 2015"

View File

@@ -1,23 +1,16 @@
<div ng-controller="Umbraco.Installer.PackagesController" id="starterKits">
<h1>Install a starter website</h1>
<p>
Installing a starter website helps you learn how Umbraco works, and gives you a solid
and simple foundation to build on top of.
</p>
<small ng-if="!packages || packages.length == 0">Loading...</small>
<ul class="thumbnails">
<li class="span3" ng-repeat="pck in packages">
<a href ng-click="setPackageAndContinue(pck.id)" class="thumbnail">
<small>Loading...</small>
<img ng-src="http://our.umbraco.org{{pck.thumbnail}}?width=170" alt="{{pck.name}}">
</a>
</li>
</ul>
<h1>Would you like to learn or demo Umbraco?</h1>
<img ng-src="http://our.umbraco.org{{packages[0].thumbnail}}?width=350" style="float: left; padding: 0 15px 50px 0" alt="{{pck.name}}">
<p>The Starter Kit is a great way to experience some of the ways you can use Umbraco. It's a complete website with textpages, landing pages, blog, product listings and more that's easy to get started with Umbraco.
</p>
<p>
It's also a great way to learn Umbraco as the Starter Kit comes with a set of Lessons that'll teach you how to implement and extend Umbraco using short 5-15 minute tasks.
</p>
<p>
<a href ng-click="setPackageAndContinue(packages[0].id)" class="btn btn-success">Yes, I'd like a Starter Kit</a>
&nbsp;
<a href ng-click="setPackageAndContinue('00000000-0000-0000-0000-000000000000')" class="btn btn-link btn-link-reverse">
No thanks, I do not want to install a starter website
No thanks
</a>
</div>

View File

@@ -120,6 +120,7 @@
@import "components/umb-querybuilder.less";
@import "components/umb-pagination.less";
@import "components/umb-mini-list-view.less";
@import "components/umb-nested-content.less";
@import "components/buttons/umb-button.less";
@import "components/buttons/umb-button-group.less";

View File

@@ -0,0 +1,192 @@
.nested-content
{
text-align: center;
}
.nested-content__item
{
position: relative;
text-align: left;
border-top: solid 1px transparent;
background: white;
}
.nested-content__item--active:not(.nested-content__item--single)
{
background: #f8f8f8;
}
.nested-content__item.ui-sortable-placeholder
{
background: #f8f8f8;
border: 1px dashed #d9d9d9;
visibility: visible !important;
height: 55px;
margin-top: -1px;
}
.nested-content__item--single > .nested-content__content
{
border: 0;
}
.nested-content__item--single > .nested-content__content > .umb-pane
{
margin: 0;
}
.nested-content__header-bar
{
padding: 15px 20px;
border-bottom: 1px dashed #e0e0e0;
text-align: right;
cursor: pointer;
background-color: white;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
}
.nested-content__heading
{
float: left;
line-height: 20px;
}
.nested-content__heading i
{
vertical-align: text-top;
color: #999; /* same icon color as the icons in the item type picker */
margin-right: 10px;
}
.nested-content__icons
{
margin: -6px 0;
opacity: 0;
transition: opacity .15s ease-in-out;
-moz-transition: opacity .15s ease-in-out;
-webkit-transition: opacity .15s ease-in-out;
}
.nested-content__header-bar:hover .nested-content__icons,
.nested-content__item--active > .nested-content__header-bar .nested-content__icons
{
opacity: 1;
}
.nested-content__icon,
.nested-content__icon.nested-content__icon--disabled:hover
{
display: inline-block;
padding: 4px 6px;
margin: 2px;
cursor: pointer;
background: #fff;
border: 1px solid #b6b6b6;
border-radius: 200px;
text-decoration: none !important;
}
.nested-content__icon:hover,
.nested-content__icon--active
{
color: white;
background: #2e8aea;
border-color: #2e8aea;
text-decoration: none;
}
.nested-content__icon .icon,
.nested-content__icon.nested-content__icon--disabled:hover .icon
{
display: block;
font-size: 16px !important;
color: #5f5f5f;
}
.nested-content__icon:hover .icon,
.nested-content__icon--active .icon
{
color: white;
}
.nested-content__icon--disabled
{
opacity: 0.3;
}
.nested-content__footer-bar
{
text-align: center;
padding-top: 20px;
}
.nested-content__content
{
border-bottom: 1px dashed #e0e0e0;
}
.nested-content__content .umb-control-group {
padding-bottom: 0;
}
.nested-content__item.ui-sortable-helper .nested-content__content
{
display: none !important;
}
.nested-content__help-text
{
display: inline-block;
padding: 10px 20px 10px 20px;
clear: both;
font-size: 14px;
color: #555;
background: #f8f8f8;
border-radius: 15px;
}
.nested-content__doctypepicker table input, .nested-content__doctypepicker table select {
width: 100%;
padding-right: 0;
}
.nested-content__doctypepicker table td.icon-navigation, .nested-content__doctypepicker i.nested-content__help-icon {
vertical-align: middle;
color: #CCC;
}
.nested-content__doctypepicker table td.icon-navigation:hover, .nested-content__doctypepicker i.nested-content__help-icon:hover {
color: #343434;
}
.nested-content__doctypepicker i.nested-content__help-icon {
margin-left: 10px;
}
.form-horizontal .nested-content--narrow .controls-row
{
margin-left: 40% !important;
}
.form-horizontal .nested-content--narrow .controls-row .umb-textstring,
.form-horizontal .nested-content--narrow .controls-row .umb-textarea
{
width: 95%;
}
.form-horizontal .nested-content--narrow .controls-row .umb-dropdown {
width: 99%;
}
.usky-grid.nested-content__node-type-picker .cell-tools-menu {
position: relative;
transform: translate(-50%, -25%);
}

View File

@@ -0,0 +1,417 @@
angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.DocTypePickerController", [
"$scope",
"Umbraco.PropertyEditors.NestedContent.Resources",
function ($scope, ncResources) {
$scope.add = function () {
$scope.model.value.push({
// As per PR #4, all stored content type aliases must be prefixed "nc" for easier recognition.
// For good measure we'll also prefix the tab alias "nc"
ncAlias: "",
ncTabAlias: "",
nameTemplate: ""
}
);
}
$scope.remove = function (index) {
$scope.model.value.splice(index, 1);
}
$scope.sortableOptions = {
axis: 'y',
cursor: "move",
handle: ".icon-navigation"
};
$scope.selectedDocTypeTabs = {};
ncResources.getContentTypes().then(function (docTypes) {
$scope.model.docTypes = docTypes;
// Populate document type tab dictionary
docTypes.forEach(function (value) {
$scope.selectedDocTypeTabs[value.alias] = value.tabs;
});
});
if (!$scope.model.value) {
$scope.model.value = [];
$scope.add();
}
}
]);
angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.PropertyEditorController", [
"$scope",
"$interpolate",
"$filter",
"$timeout",
"contentResource",
"localizationService",
"iconHelper",
"Umbraco.PropertyEditors.NestedContent.Resources",
function ($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, ncResources) {
//$scope.model.config.contentTypes;
//$scope.model.config.minItems;
//$scope.model.config.maxItems;
//console.log($scope);
var inited = false;
_.each($scope.model.config.contentTypes, function (contentType) {
contentType.nameExp = !!contentType.nameTemplate
? $interpolate(contentType.nameTemplate)
: undefined;
});
$scope.editIconTitle = '';
$scope.moveIconTitle = '';
$scope.deleteIconTitle = '';
// localize the edit icon title
localizationService.localize('general_edit').then(function (value) {
$scope.editIconTitle = value;
});
// localize the delete icon title
localizationService.localize('general_delete').then(function (value) {
$scope.deleteIconTitle = value;
});
// localize the move icon title
localizationService.localize('actions_move').then(function (value) {
$scope.moveIconTitle = value;
});
$scope.nodes = [];
$scope.currentNode = undefined;
$scope.realCurrentNode = undefined;
$scope.scaffolds = undefined;
$scope.sorting = false;
$scope.minItems = $scope.model.config.minItems || 0;
$scope.maxItems = $scope.model.config.maxItems || 0;
if ($scope.maxItems == 0)
$scope.maxItems = 1000;
$scope.singleMode = $scope.minItems == 1 && $scope.maxItems == 1;
$scope.showIcons = $scope.model.config.showIcons || true;
$scope.wideMode = $scope.model.config.hideLabel == "1";
$scope.overlayMenu = {
show: false,
style: {}
};
// helper to force the current form into the dirty state
$scope.setDirty = function () {
if ($scope.propertyForm) {
$scope.propertyForm.$setDirty();
}
};
$scope.addNode = function (alias) {
var scaffold = $scope.getScaffold(alias);
var newNode = initNode(scaffold, null);
$scope.currentNode = newNode;
$scope.setDirty();
$scope.closeNodeTypePicker();
};
$scope.openNodeTypePicker = function (event) {
if ($scope.nodes.length >= $scope.maxItems) {
return;
}
// this could be used for future limiting on node types
$scope.overlayMenu.scaffolds = [];
_.each($scope.scaffolds, function (scaffold) {
$scope.overlayMenu.scaffolds.push({
alias: scaffold.contentTypeAlias,
name: scaffold.contentTypeName,
icon: iconHelper.convertFromLegacyIcon(scaffold.icon)
});
});
if ($scope.overlayMenu.scaffolds.length == 0) {
return;
}
if ($scope.overlayMenu.scaffolds.length == 1) {
// only one scaffold type - no need to display the picker
$scope.addNode($scope.scaffolds[0].contentTypeAlias);
return;
}
$scope.overlayMenu.show = true;
};
$scope.closeNodeTypePicker = function () {
$scope.overlayMenu.show = false;
};
$scope.editNode = function (idx) {
if ($scope.currentNode && $scope.currentNode.key == $scope.nodes[idx].key) {
$scope.currentNode = undefined;
} else {
$scope.currentNode = $scope.nodes[idx];
}
};
$scope.deleteNode = function (idx) {
if ($scope.nodes.length > $scope.model.config.minItems) {
if ($scope.model.config.confirmDeletes && $scope.model.config.confirmDeletes == 1) {
if (confirm("Are you sure you want to delete this item?")) {
$scope.nodes.splice(idx, 1);
$scope.setDirty();
updateModel();
}
} else {
$scope.nodes.splice(idx, 1);
$scope.setDirty();
updateModel();
}
}
};
$scope.getName = function (idx) {
var name = "Item " + (idx + 1);
if ($scope.model.value[idx]) {
var contentType = $scope.getContentTypeConfig($scope.model.value[idx].ncContentTypeAlias);
if (contentType != null && contentType.nameExp) {
// Run the expression against the stored dictionary value, NOT the node object
var item = $scope.model.value[idx];
// Add a temporary index property
item['$index'] = (idx + 1);
var newName = contentType.nameExp(item);
if (newName && (newName = $.trim(newName))) {
name = newName;
}
// Delete the index property as we don't want to persist it
delete item['$index'];
}
}
// Update the nodes actual name value
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: ".nested-content__icon--move",
start: function (ev, ui) {
// Yea, yea, we shouldn't modify the dom, sue me
$("#nested-content--" + $scope.model.id + " .umb-rte textarea").each(function () {
tinymce.execCommand('mceRemoveEditor', false, $(this).attr('id'));
$(this).css("visibility", "hidden");
});
$scope.$apply(function () {
$scope.sorting = true;
});
},
update: function (ev, ui) {
$scope.setDirty();
},
stop: function (ev, ui) {
$("#nested-content--" + $scope.model.id + " .umb-rte textarea").each(function () {
tinymce.execCommand('mceAddEditor', true, $(this).attr('id'));
$(this).css("visibility", "visible");
});
$scope.$apply(function () {
$scope.sorting = false;
updateModel();
});
}
};
$scope.getScaffold = function (alias) {
return _.find($scope.scaffolds, function (scaffold) {
return scaffold.contentTypeAlias == alias;
});
}
$scope.getContentTypeConfig = function (alias) {
return _.find($scope.model.config.contentTypes, function (contentType) {
return contentType.ncAlias == alias;
});
}
// Initialize
var scaffoldsLoaded = 0;
$scope.scaffolds = [];
_.each($scope.model.config.contentTypes, function (contentType) {
contentResource.getScaffold(-20, contentType.ncAlias).then(function (scaffold) {
// remove all tabs except the specified tab
var tab = _.find(scaffold.tabs, function (tab) {
return tab.id != 0 && (tab.alias.toLowerCase() == contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias == "");
});
scaffold.tabs = [];
if (tab) {
scaffold.tabs.push(tab);
}
// Store the scaffold object
$scope.scaffolds.push(scaffold);
scaffoldsLoaded++;
initIfAllScaffoldsHaveLoaded();
}, function (error) {
scaffoldsLoaded++;
initIfAllScaffoldsHaveLoaded();
});
});
var initIfAllScaffoldsHaveLoaded = function () {
// Initialize when all scaffolds have loaded
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 = [];
_.each($scope.model.config.contentTypes, function (contentType) {
contentTypeAliases.push(contentType.ncAlias);
});
$scope.scaffolds = $filter('orderBy')($scope.scaffolds, function (s) {
return contentTypeAliases.indexOf(s.contentTypeAlias);
});
// Convert stored nodes
if ($scope.model.value) {
for (var i = 0; i < $scope.model.value.length; i++) {
var item = $scope.model.value[i];
var scaffold = $scope.getScaffold(item.ncContentTypeAlias);
if (scaffold == null) {
// No such scaffold - the content type might have been deleted. We need to skip it.
continue;
}
initNode(scaffold, item);
}
}
// Enforce min items
if ($scope.nodes.length < $scope.model.config.minItems) {
for (var i = $scope.nodes.length; i < $scope.model.config.minItems; i++) {
$scope.addNode($scope.scaffolds[0].contentTypeAlias);
}
}
// If there is only one item, set it as current node
if ($scope.singleMode || ($scope.nodes.length == 1 && $scope.maxItems == 1)) {
$scope.currentNode = $scope.nodes[0];
}
inited = true;
}
}
var initNode = function (scaffold, item) {
var node = angular.copy(scaffold);
node.key = guid();
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 consistancy 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];
}
}
}
}
$scope.nodes.push(node);
return node;
}
var updateModel = function () {
if ($scope.realCurrentNode) {
$scope.$broadcast("ncSyncVal", { key: $scope.realCurrentNode.key });
}
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);
}
$scope.model.value = newValues;
}
}
$scope.$watch("currentNode", function (newVal) {
updateModel();
$scope.realCurrentNode = newVal;
});
var unsubscribe = $scope.$on("formSubmitting", function (ev, args) {
updateModel();
});
$scope.$on('$destroy', function () {
unsubscribe();
});
var guid = function () {
function _p8(s) {
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
}
return _p8() + _p8(true) + _p8(true) + _p8();
};
}
]);

View File

@@ -0,0 +1,58 @@
<div id="{{model.alias}}" class="nested-content__doctypepicker" ng-controller="Umbraco.PropertyEditors.NestedContent.DocTypePickerController">
<div>
<table class="table table-striped">
<thead>
<tr>
<th/>
<th>
Document Type
</th>
<th>
Tab
</th>
<th>
Name Template
</th>
<th />
</tr>
</thead>
<tbody ui-sortable="sortableOptions" ng-model="model.value">
<tr ng-repeat="config in model.value">
<td class="icon icon-navigation">
</td>
<td>
<select id="{{model.alias}}_doctype_select"
ng-options="dt.alias as dt.name for dt in model.docTypes | orderBy: 'name'"
ng-model="config.ncAlias" required></select>
</td>
<td>
<select id="{{model.alias}}_tab_select"
ng-options="t for t in selectedDocTypeTabs[config.ncAlias]"
ng-model="config.ncTabAlias" required></select>
</td>
<td>
<input type="text" ng-model="config.nameTemplate" />
</td>
<td>
<a class="btn btn-danger" ng-click="remove($index)" ng-show="model.value.length > 1">Remove</a>
</td>
</tr>
</tbody>
</table>
<div>
<a class="btn" ng-click="add()">Add</a>
<i class="icon icon-help-alt medium nested-content__help-icon" ng-click="showHelpText = !showHelpText"></i>
</div>
</div>
<br/>
<div class="nested-content__help-text" ng-show="showHelpText">
<p>
<b>Tab:</b><br/>
Select the tab who's properties should be displayed. If left blank, the first tab on the doc type will be used.
</p>
<p>
<b>Name template:</b><br/>
Enter an angular expression to evaluate against each item for its name. Use <code ng-non-bindable>{{$index}}</code> to display the item index
</p>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div class="umb-pane">
<umb-property
property="property"
ng-repeat="property in tab.properties">
<umb-editor model="property"></umb-editor>
</umb-property>
</div>

View File

@@ -0,0 +1,62 @@
<div id="nested-content--{{model.id}}" class="nested-content"
ng-controller="Umbraco.PropertyEditors.NestedContent.PropertyEditorController"
ng-class="{'nested-content--narrow':!wideMode, 'nested-content--wide':wideMode}">
<ng-form>
<div class="nested-content__items" ng-hide="nodes.length == 0" ui-sortable="sortableOptions" ng-model="nodes">
<div class="nested-content__item" ng-repeat="node in nodes" ng-class="{ 'nested-content__item--active' : $parent.realCurrentNode.key == node.key, 'nested-content__item--single' : $parent.singleMode }">
<div class="nested-content__header-bar" ng-click="$parent.editNode($index)" ng-hide="$parent.singleMode">
<div class="nested-content__heading"><i ng-if="showIcons" class="icon" ng-class="$parent.getIcon($index)"></i><span ng-bind="$parent.getName($index)"></span></div>
<div class="nested-content__icons">
<a class="nested-content__icon nested-content__icon--edit" title="{{editIconTitle}}" ng-class="{ '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>
<a class="nested-content__icon nested-content__icon--move" title="{{moveIconTitle}}" ng-click="$event.stopPropagation();" ng-show="$parent.nodes.length > 1" prevent-default>
<i class="icon icon-navigation"></i>
</a>
<a class="nested-content__icon nested-content__icon--delete" title="{{deleteIconTitle}}" ng-class="{ 'nested-content__icon--disabled': $parent.nodes.length <= $parent.minItems }" ng-click="$parent.deleteNode($index); $event.stopPropagation();" prevent-default>
<i class="icon icon-trash"></i>
</a>
</div>
</div>
<div class="nested-content__content" ng-if="$parent.realCurrentNode.key == node.key && !$parent.sorting">
<nested-content-editor ng-model="node" tab-alias="ncTabAlias" />
</div>
</div>
</div>
<div class="nested-content__help-text" ng-show="nodes.length == 0">
<localize key="grid_addElement"></localize>
</div>
<div class="nested-content__footer-bar" ng-hide="nodes.length >= maxItems">
<a class="nested-content__icon" ng-click="openNodeTypePicker($event)" prevent-default>
<i class="icon icon-add"></i>
</a>
</div>
<div class="usky-grid nested-content__node-type-picker" ng-if="overlayMenu.show">
<div class="cell-tools-menu" ng-style="overlayMenu.style" style="margin: 0;" delayed-mouseleave="closeNodeTypePicker()" on-delayed-mouseleave="closeNodeTypePicker()">
<h5>
<localize key="grid_insertControl" />
</h5>
<ul class="elements">
<li ng-repeat="scaffold in overlayMenu.scaffolds">
<a ng-click="addNode(scaffold.alias)" href>
<i class="icon {{scaffold.icon}}"></i>
{{scaffold.name}}
</a>
</li>
</ul>
</div>
</div>
</ng-form>
</div>