allow complex layouts in NC templates

This commit is contained in:
2019-08-29 16:40:34 +02:00
parent 259c1de11b
commit 08aaa767c4
9 changed files with 569 additions and 180 deletions
@@ -1,4 +1,121 @@
angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.PropertyEditorController", [
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.canAdd = function ()
{
return !$scope.model.docTypes || !$scope.model.value || $scope.model.value.length < $scope.model.docTypes.length;
}
$scope.remove = function (index)
{
$scope.model.value.splice(index, 1);
}
$scope.sortableOptions = {
axis: "y",
cursor: "move",
handle: ".handle",
placeholder: 'sortable-placeholder',
forcePlaceholderSize: true,
helper: function (e, ui)
{
// When sorting table rows, the cells collapse. This helper fixes that: https://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/
ui.children().each(function ()
{
$(this).width($(this).width());
});
return ui;
},
start: function (e, ui)
{
var cellHeight = ui.item.height();
// Build a placeholder cell that spans all the cells in the row: https://stackoverflow.com/questions/25845310/jquery-ui-sortable-and-table-cell-size
var cellCount = 0;
$('td, th', ui.helper).each(function ()
{
// For each td or th try and get it's colspan attribute, and add that or 1 to the total
var colspan = 1;
var colspanAttr = $(this).attr('colspan');
if (colspanAttr > 1)
{
colspan = colspanAttr;
}
cellCount += colspan;
});
// Add the placeholder UI - note that this is the item's content, so td rather than tr - and set height of tr
ui.placeholder.html('<td colspan="' + cellCount + '"></td>').height(cellHeight);
}
};
$scope.docTypeTabs = {};
ncResources.getContentTypes().then(function (docTypes)
{
$scope.model.docTypes = docTypes;
// Count doctype name occurrences
var docTypeNameOccurrences = _.countBy(docTypes, 'name');
// Populate document type tab dictionary
// And append alias to name if multiple doctypes have the same name
docTypes.forEach(function (value)
{
$scope.docTypeTabs[value.alias] = value.tabs;
value.displayName = value.name;
if (docTypeNameOccurrences[value.name] > 1)
{
value.displayName += " (" + value.alias + ")";
}
});
});
$scope.selectableDocTypesFor = function (config)
{
// return all doctypes that are:
// 1. either already selected for this config, or
// 2. not selected in any other config
return _.filter($scope.model.docTypes, function (docType)
{
return docType.alias === config.ncAlias || !_.find($scope.model.value, function (c)
{
return docType.alias === c.ncAlias;
});
});
}
if (!$scope.model.value)
{
$scope.model.value = [];
$scope.add();
}
}
]);
angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.PropertyEditorController", [
"$scope",
"$interpolate",
@@ -1,15 +0,0 @@
{
"text": {
"html": "text"
},
"button": {
"text": "text",
"link": "url"
},
"images": {
"items": "media"
},
"xblocks": {
"items": "list"
}
}
@@ -1,17 +0,0 @@
angular.module('umbraco.services').config(['$httpProvider', function ($httpProvider)
{
$httpProvider.interceptors.push(['$q', '$injector', 'notificationsService', function ($q, $injector, notificationsService)
{
return {
'request': function (request)
{
if (request.url.indexOf("views/propertyeditors/nestedcontent/nestedcontent.html") === 0)
{
request.url = '/App_Plugins/brothers.uNesting/uNesting.html';
}
return request || $q.when(request);
}
};
}]);
}]);
@@ -1,7 +1,6 @@
{
"javascript": [
"~/App_Plugins/brothers.uNesting/interceptor.js",
"~/App_Plugins/brothers.uNesting/uNesting.controller.js"
"~/App_Plugins/brothers.uNesting/uNesting.js"
],
"css": [
"~/App_Plugins/brothers.uNesting/uNesting.css"
@@ -1,101 +0,0 @@
angular.module("umbraco").controller("brothers.uNesting.PropertyEditorController", function ($scope, $controller, $http)
{
angular.extend(this, $controller('Umbraco.PropertyEditors.NestedContent.PropertyEditorController', { $scope: $scope }));
var elementsConfig = {};
var init = function ()
{
$http.get("/App_Plugins/brothers.uNesting/elements.json").then(function (res)
{
elementsConfig = res.data;
_.each($scope.nodes, function (node, idx)
{
node.uNestingContent = getContent(node, $scope.model.value[idx], elementsConfig[node.contentTypeAlias]);
});
});
};
$scope.$watch("inited", function (newVal)
{
if (newVal)
{
init();
}
});
var getContent = function (node, item, config)
{
if (!item)
{
return '';
}
if (!config)
{
return node.documentType.description;
}
var lines = [];
_.each(config, function (type, alias)
{
var value = item[alias];
if (value)
{
if (type === 'text')
{
lines.push(stripHtml(value));
}
if (type === 'url')
{
lines.push(value[0].url);
}
}
});
if (lines.length < 1)
{
return node.documentType.description;
}
else
{
return lines.join('<br>');
}
};
var stripHtml = function (html)
{
if (!html)
{
return '';
}
var stripped = html.replace('<br>', ' ').replace('<br />', ' ').replace('<br/>', ' ').replace(/<[^>]+>/gm, '');
return stripped.length > 120 ? (stripped.substring(0, 120) + '...') : stripped;
};
//$scope.getView = function (node, idx)
//{
// 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];
// var newName = contentType.nameExp(item);
// if (newName && (newName = $.trim(newName)))
// {
// return newName;
// }
// }
// }
// return node.documentType.description || 'Enter data ...';
//};
});
@@ -18,14 +18,48 @@
margin-top: -1px;
}
.unesting-items
{
margin-top: -15px;
}
/*.unesting-item .unesting-items
{
background-color: #f9f7f7;
box-shadow: 0 1px 1px 0 rgba(0,0,0,.16);
}*/
/* header */
.unesting-item-header
{
border-bottom: 1px solid #e9e9eb;
border-bottom: 1px solid #f6f6f7;
cursor: pointer;
background-color: #fff;
user-select: none;
position: relative;
}
.unesting-item.is-active > .unesting-item-header:after,
.unesting-item.is-active > .unesting-item-header:before
{
position: absolute;
content: ' ';
display: inline-block;
left: 5px;
bottom: -19px;
border: 10px solid transparent;
border-top-color: white;
width: 0;
height: 0;
}
.unesting-item.is-active > .unesting-item-header:before
{
bottom: -21px;
border-top-color: #e9e9eb;
}
.unesting-item:last-child .unesting-item-header
{
border-bottom: none;
}
.unesting-item-header-inner
@@ -36,19 +70,42 @@
padding: 15px 5px;
color: #1b264f;
border-radius: 3px 3px 0 0;
border-bottom: none;
background: none;
}
.unesting-item-header-inner i
/*.unesting-item:nth-child(2n+1) .unesting-item-header-inner
{
background: #faf9f9;
}*/
.unesting-item-header-inner > i
{
position: absolute;
top: 50%;
margin-top: -11px;
font-size: 1.2rem;
margin-top: -13px;
left: 3px;
font-size: 1.4rem;
z-index: 1;
}
.unesting-item-header-content.--has-icon
{
padding-left: 40px;
margin-left: 40px;
padding-left: 20px;
border-left: 1px solid #f6f6f7;
}
.unesting-item-header-content-iconbg
{
display: none;
position: absolute;
left: -46px;
width: 46px;
top: -16px;
background: #faf9f9;
z-index: 0;
bottom: -15px;
}
.unesting-item-header:hover .unesting-item-header-inner .unesting-item-header-content
@@ -61,11 +118,14 @@
line-height: 20px;
color: #1b264f;
white-space: nowrap;
position: relative;
}
.unesting-item-header-content-name
{
max-height: 20px;
font-weight: 600;
font-size: 14px;
}
.unesting-item-header-content-text
@@ -73,6 +133,9 @@
color: #817f85;
font-size: 13px;
overflow: hidden;
margin-top: 4px;
display: block;
line-height: 18px;
}
/* icons */
@@ -83,9 +146,9 @@
transition: opacity .12s ease-in-out;
position: absolute;
right: 0;
top: 3px;
top: 50%;
padding: 5px;
margin-top: 8px;
margin-top: -22px;
display: flex;
grid-gap: 8px;
}
@@ -128,6 +191,7 @@
display: inline-block;
margin: 0 1px;
opacity: .6;
position: static;
}
.unesting-icon .icon
@@ -160,9 +224,89 @@
background: none;
}
.unesting-content > .umb-pane
{
margin: 30px 20px;
}
.unesting-item.is-active > .unesting-content
{
background: #faf9f9;
margin: 0 -20px;
border-left: none;
border-right: none;
border-radius: 0;
border-top: 1px solid #e9e9eb;
}
.unesting-content .unesting-item.is-active > .unesting-content
{
background: white;
margin: 0 -20px;
border-left: 1px solid #e9e9eb;
}
.unesting-item.is-active > .unesting-item-header
{
border-bottom: none;
}
/* media */
.unesting-media
{
display: flex;
flex-direction: row;
margin-top: 4px;
}
.unesting-media-item
{
border-radius: 3px;
margin-right: 6px;
display: flex;
align-items: center;
}
.unesting-media-item.has-title
{
margin-right: 12px;
}
.unesting-media-item-image
{
background: #f6f4f4;
border-radius: 3px;
border: 1px solid #f6f6f7;
}
.unesting-media-item-text
{
padding: 0 10px;
max-width: 100px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
}
.unesting-media-more
{
max-width: 100%;
height: auto;
vertical-align: middle;
width: 50px;
height: 50px;
background: #f6f4f4;
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 12px;
border-radius: 3px;
}
/* footer */
.unesting-footer
{
text-align: left;
@@ -174,11 +318,22 @@
}
/* data type overrides */
/* doctypepicker */
.unesting-doctype-textarea
{
width: 100%;
height: 32px;
max-height: 200px;
min-height: 32px;
padding-top: 6px;
resize: vertical;
}
/* data type overrides */
.unesting-item .umb-sortable-thumbnails li
{
background: none;
/*background: none;*/
}
@@ -0,0 +1,63 @@
<div id="{{model.alias}}" class="umb-nested-content__doctypepicker" ng-controller="brothers.uNesting.DocTypePickerController">
<div>
<table class="table table-striped">
<thead>
<tr>
<th />
<th>
<localize key="contentTypeEditor_elementType">Element Type</localize>
</th>
<th>
<localize key="general_group">Group</localize>
</th>
<th>
<localize key="template_template">Template</localize>
</th>
<th />
</tr>
</thead>
<tbody ui-sortable="sortableOptions" ng-model="model.value">
<tr ng-repeat="config in model.value">
<td>
<i class="icon icon-navigation handle"></i>
</td>
<td>
<select id="{{model.alias}}_doctype_select"
ng-options="dt.alias as dt.name for dt in selectableDocTypesFor(config) | orderBy: 'name'"
ng-model="config.ncAlias" required></select>
</td>
<td>
<select id="{{model.alias}}_tab_select"
ng-options="t for t in docTypeTabs[config.ncAlias]"
ng-model="config.ncTabAlias" required></select>
</td>
<td>
<textarea class="unesting-doctype-textarea" ng-model="config.nameTemplate" rows="1" cols="40"></textarea>
</td>
<td>
<button type="button" class="btn btn-danger" ng-click="remove($index)">
<localize key="general_delete">Delete</localize>
</button>
</td>
</tr>
</tbody>
</table>
<div>
<button type="button" class="btn" ng-click="add()" ng-disabled="!canAdd()">
<localize key="general_add">Add</localize>
</button>
<i class="icon icon-help-alt medium umb-nested-content__help-icon" ng-click="showHelpText = !showHelpText"></i>
</div>
</div>
<br />
<div class="umb-nested-content__help-text" ng-show="showHelpText">
<p>
<b><localize key="general_group">Group</localize>:</b><br />
Select the group whose properties should be displayed. If left blank, the first group on the element type will be used.
</p>
<p>
<b><localize key="template_template">Template</localize>:</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>
@@ -14,16 +14,17 @@
<div class="unesting-items" ng-hide="nodes.length === 0" ng-class="{ 'is-hidden': nodes.length === 0 }" ui-sortable="sortableOptions" ng-model="nodes">
<div class="unesting-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, 'is-hidden': $index === 1 }">
<div class="unesting-item" ng-repeat="node in nodes" ng-class="{ 'is-active' : $parent.realCurrentNode.key === node.key, 'umb-nested-content__item--single' : $parent.singleMode, 'is-hidden': $index === 1 }">
<div class="unesting-item-header" ng-click="$parent.editNode($index)" ng-hide="$parent.singleMode">
<div class="unesting-item-header-inner">
<div class="umb-nested-content__header-bar unesting-item-header-inner">
<i ng-if="showIcons" class="icon" ng-class="$parent.getIcon($index)"></i>
<div class="unesting-item-header-content" ng-class="{'--has-icon': showIcons}">
<span class="unesting-item-header-content-iconbg" ng-if="showIcons"></span>
<div class="unesting-item-header-content-name" ng-bind="node.contentTypeName"></div>
<div class="unesting-item-header-content-text" ng-if="node.uNestingContent" ng-bind-html="node.uNestingContent"></div>
</div>
<un-content class="unesting-item-header-content-text" item="model.value[$index]" node="node" config="getContentTypeConfig(node.documentType.alias)"></un-content>
<!--<div class="unesting-item-header-content-text" ng-if="!$parent.hasTemplate(node, $index)" ng-bind-html="$parent.getPlainContent(node, $index)"></div>-->
</div>
<div class="unesting-icons">
@@ -58,6 +59,8 @@
<i class="icon icon-trash-alt"></i>
</a>
</div>
</div>
<!--<div class="umb-nested-content__icons">
<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>
@@ -0,0 +1,185 @@
angular.module('umbraco.services').config(['$httpProvider', function ($httpProvider)
{
$httpProvider.interceptors.push(['$q', '$injector', 'notificationsService', function ($q, $injector, notificationsService)
{
return {
'request': function (request)
{
if (request.url.indexOf("views/propertyeditors/nestedcontent/nestedcontent.html") === 0)
{
request.url = '/App_Plugins/brothers.uNesting/uNesting.html';
}
else if (request.url.indexOf("views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html") === 0)
{
request.url = '/App_Plugins/brothers.uNesting/uNesting.doctypepicker.html';
}
return request || $q.when(request);
}
};
}]);
}]);
angular.module("umbraco").controller("brothers.uNesting.DocTypePickerController", function ($scope)
{
angular.extend(this, $controller('Umbraco.PropertyEditors.NestedContent.DocTypePickerController', { $scope: $scope }));
});
angular.module("umbraco").controller("brothers.uNesting.PropertyEditorController", function ($scope, $controller, $http, $compile)
{
angular.extend(this, $controller('Umbraco.PropertyEditors.NestedContent.PropertyEditorController', { $scope: $scope }));
});
angular.module('umbraco.directives').directive('unContent', function ($compile)
{
return {
restrict: 'E',
scope: {
item: '=',
node: '=',
config: '='
},
link: function (scope, element)
{
// get template
var template = scope.config && scope.config.nameTemplate ? scope.config.nameTemplate : null;
// compile & render template
function render()
{
// merge element values into local scope
angular.merge(scope, scope.item);
// nothing when no template is specified
if (!template)
{
element.html('');
//element.html(scope.node.documentType.description);
return;
}
// create html and compile
element.html(template);
$compile(element.contents())(scope);
}
//scope.$watch('item', function (value)
//{
// render();
//});
// first-time rendering
render();
}
};
});
angular.module('umbraco.directives').directive('unMedia', function ($http, $compile, umbRequestHelper)
{
return {
restrict: 'E',
scope: {
items: '=',
key: '@',
titleKey: '@',
limit: '@'
},
template: '' +
'<div class= "unesting-media" ng- class="{\'has-title\': !!item.title}">' +
'<div class="unesting-media-item" ng-repeat="item in media">' +
'<img src="{{item.src}}" title="{{item.title}}" class="unesting-media-item-image" />' +
'<span class="unesting-media-item-text" ng-if="item.title">{{item.title | unHtml }}</span>' +
'</div>' +
'<span class="unesting-media-more" ng-if="count > media.length">+{{count - media.length}}</span>' +
'</div>',
link: function (scope, element)
{
var resize = 'width=50&height=50&mode=crop&quality=60';
var limit = scope.limit || 5;
scope.media = [];
var titleCache = {};
// compile & render template
function render()
{
var ids = scope.items;
if (!scope.items)
{
return;
}
if (_.isArray(scope.items))
{
if (!scope.key)
{
console.error("You need the <un-media key='my_key' /> property for nested media");
return;
}
ids = _.map(scope.items, function (item)
{
var value = item[scope.key];
if (scope.titleKey)
{
titleCache[value] = item[scope.titleKey];
}
return value;
}).join(',');
}
ids = ids.split(',');
scope.count = ids.length;
var query = "ids=" + ids.slice(0, limit).join('&ids=');
var url = $http.get(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "GetByUdis", query).replace("/Media/", "/MediaExtended/"));
// TODO we have to implement this method in a controller
umbRequestHelper.resourcePromise(url, 'Failed to retrieve data for media ids').then(function (result)
{
_.each(result, function (src, id)
{
scope.media.push({
id: id,
src: src + "?" + resize,
title: titleCache[id]
});
});
});
}
render();
}
};
});
angular.module('umbraco').filter('unHtml', function ()
{
return function (html, maxLength)
{
if (!html)
{
return '';
}
if (!maxLength)
{
maxLength = 120;
}
var stripped = html.replace('<br>', ' ').replace('<br />', ' ').replace('<br/>', ' ').replace(/<[^>]+>/gm, '');
return stripped.length > maxLength ? (stripped.substring(0, maxLength) + '...') : stripped;
};
});