11201 lines
348 KiB
JavaScript
11201 lines
348 KiB
JavaScript
/*! umbraco
|
|
* https://github.com/umbraco/umbraco-cms/
|
|
* Copyright (c) 2016 Umbraco HQ;
|
|
* Licensed
|
|
*/
|
|
|
|
(function() {
|
|
|
|
angular.module("umbraco.directives", ["umbraco.directives.editors", "umbraco.directives.html", "umbraco.directives.validation", "ui.sortable"]);
|
|
angular.module("umbraco.directives.editors", []);
|
|
angular.module("umbraco.directives.html", []);
|
|
angular.module("umbraco.directives.validation", []);
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:autoScale
|
|
* @element div
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
* @function
|
|
* @description
|
|
* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set
|
|
* So if you only want to scale the div to 70 pixels from the bottom you pass "70"
|
|
|
|
* @example
|
|
* <example module="umbraco.directives">
|
|
* <file name="index.html">
|
|
* <div auto-scale="70" class="input-block-level"></div>
|
|
* </file>
|
|
* </example>
|
|
**/
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('autoScale', function ($window) {
|
|
return function (scope, el, attrs) {
|
|
|
|
var totalOffset = 0;
|
|
var offsety = parseInt(attrs.autoScale, 10);
|
|
var window = angular.element($window);
|
|
if (offsety !== undefined){
|
|
totalOffset += offsety;
|
|
}
|
|
|
|
setTimeout(function () {
|
|
el.height(window.height() - (el.offset().top + totalOffset));
|
|
}, 500);
|
|
|
|
window.bind("resize", function () {
|
|
el.height(window.height() - (el.offset().top + totalOffset));
|
|
});
|
|
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:detectFold
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
* @description This is used for the editor buttons to ensure they are displayed correctly if the horizontal overflow of the editor
|
|
* exceeds the height of the window
|
|
**/
|
|
|
|
angular.module("umbraco.directives.html")
|
|
.directive('detectFold', function ($timeout, $log, windowResizeListener) {
|
|
return {
|
|
require: "^?umbTabs",
|
|
restrict: 'A',
|
|
link: function (scope, el, attrs, tabsCtrl) {
|
|
|
|
var firstRun = false;
|
|
var parent = $(".umb-panel-body");
|
|
var winHeight = $(window).height();
|
|
var calculate = function () {
|
|
if (el && el.is(":visible") && !el.hasClass("umb-bottom-bar")) {
|
|
|
|
//now that the element is visible, set the flag in a couple of seconds,
|
|
// this will ensure that loading time of a current tab get's completed and that
|
|
// we eventually stop watching to save on CPU time
|
|
$timeout(function() {
|
|
firstRun = true;
|
|
}, 4000);
|
|
|
|
//var parent = el.parent();
|
|
var hasOverflow = parent.innerHeight() < parent[0].scrollHeight;
|
|
//var belowFold = (el.offset().top + el.height()) > winHeight;
|
|
if (hasOverflow) {
|
|
el.addClass("umb-bottom-bar");
|
|
|
|
//I wish we didn't have to put this logic here but unfortunately we
|
|
// do. This needs to calculate the left offest to place the bottom bar
|
|
// depending on if the left column splitter has been moved by the user
|
|
// (based on the nav-resize directive)
|
|
var wrapper = $("#mainwrapper");
|
|
var contentPanel = $("#leftcolumn").next();
|
|
var contentPanelLeftPx = contentPanel.css("left");
|
|
|
|
el.css({ left: contentPanelLeftPx });
|
|
}
|
|
}
|
|
return firstRun;
|
|
};
|
|
|
|
var resizeCallback = function(size) {
|
|
winHeight = size.height;
|
|
el.removeClass("umb-bottom-bar");
|
|
calculate();
|
|
};
|
|
|
|
windowResizeListener.register(resizeCallback);
|
|
|
|
//Only execute the watcher if this tab is the active (first) tab on load, otherwise there's no reason to execute
|
|
// the watcher since it will be recalculated when the tab changes!
|
|
if (el.closest(".umb-tab-pane").index() === 0) {
|
|
//run a watcher to ensure that the calculation occurs until it's firstRun but ensure
|
|
// the calculations are throttled to save a bit of CPU
|
|
var listener = scope.$watch(_.throttle(calculate, 1000), function (newVal, oldVal) {
|
|
if (newVal !== oldVal) {
|
|
listener();
|
|
}
|
|
});
|
|
}
|
|
|
|
//listen for tab changes
|
|
if (tabsCtrl != null) {
|
|
tabsCtrl.onTabShown(function (args) {
|
|
calculate();
|
|
});
|
|
}
|
|
|
|
//ensure to unregister
|
|
scope.$on('$destroy', function() {
|
|
windowResizeListener.unregister(resizeCallback);
|
|
});
|
|
}
|
|
};
|
|
});
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbItemSorter
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
* @function
|
|
* @element ANY
|
|
* @restrict E
|
|
* @description A re-usable directive for sorting items
|
|
**/
|
|
|
|
function umbItemSorter(angularHelper) {
|
|
return {
|
|
scope: {
|
|
model: "="
|
|
},
|
|
restrict: "E", // restrict to an element
|
|
replace: true, // replace the html element with the template
|
|
templateUrl: 'views/directives/_obsolete/umb-item-sorter.html',
|
|
link: function(scope, element, attrs, ctrl) {
|
|
var defaultModel = {
|
|
okButton: "Ok",
|
|
successMsg: "Sorting successful",
|
|
complete: false
|
|
};
|
|
//assign user vals to default
|
|
angular.extend(defaultModel, scope.model);
|
|
//re-assign merged to user
|
|
scope.model = defaultModel;
|
|
|
|
scope.performSort = function() {
|
|
scope.$emit("umbItemSorter.sorting", {
|
|
sortedItems: scope.model.itemsToSort
|
|
});
|
|
};
|
|
|
|
scope.handleCancel = function () {
|
|
scope.$emit("umbItemSorter.cancel");
|
|
};
|
|
|
|
scope.handleOk = function() {
|
|
scope.$emit("umbItemSorter.ok");
|
|
};
|
|
|
|
//defines the options for the jquery sortable
|
|
scope.sortableOptions = {
|
|
axis: 'y',
|
|
cursor: "move",
|
|
placeholder: "ui-sortable-placeholder",
|
|
update: function (ev, ui) {
|
|
//highlight the item when the position is changed
|
|
$(ui.item).effect("highlight", { color: "#049cdb" }, 500);
|
|
},
|
|
stop: function (ev, ui) {
|
|
//the ui-sortable directive already ensures that our list is re-sorted, so now we just
|
|
// need to update the sortOrder to the index of each item
|
|
angularHelper.safeApply(scope, function () {
|
|
angular.forEach(scope.itemsToSort, function (val, index) {
|
|
val.sortOrder = index + 1;
|
|
});
|
|
|
|
});
|
|
}
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive("umbItemSorter", umbItemSorter);
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbContentName
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
* @restrict E
|
|
* @function
|
|
* @description
|
|
* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form.
|
|
**/
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('umbContentName', function ($timeout, localizationService) {
|
|
return {
|
|
require: "ngModel",
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/directives/_obsolete/umb-content-name.html',
|
|
|
|
scope: {
|
|
placeholder: '@placeholder',
|
|
model: '=ngModel',
|
|
ngDisabled: '='
|
|
},
|
|
link: function(scope, element, attrs, ngModel) {
|
|
|
|
var inputElement = element.find("input");
|
|
if(scope.placeholder && scope.placeholder[0] === "@"){
|
|
localizationService.localize(scope.placeholder.substring(1))
|
|
.then(function(value){
|
|
scope.placeholder = value;
|
|
});
|
|
}
|
|
|
|
var mX, mY, distance;
|
|
|
|
function calculateDistance(elem, mouseX, mouseY) {
|
|
|
|
var cx = Math.max(Math.min(mouseX, elem.offset().left + elem.width()), elem.offset().left);
|
|
var cy = Math.max(Math.min(mouseY, elem.offset().top + elem.height()), elem.offset().top);
|
|
return Math.sqrt((mouseX - cx) * (mouseX - cx) + (mouseY - cy) * (mouseY - cy));
|
|
}
|
|
|
|
var mouseMoveDebounce = _.throttle(function (e) {
|
|
mX = e.pageX;
|
|
mY = e.pageY;
|
|
// not focused and not over element
|
|
if (!inputElement.is(":focus") && !inputElement.hasClass("ng-invalid")) {
|
|
// on page
|
|
if (mX >= inputElement.offset().left) {
|
|
distance = calculateDistance(inputElement, mX, mY);
|
|
if (distance <= 155) {
|
|
|
|
distance = 1 - (100 / 150 * distance / 100);
|
|
inputElement.css("border", "1px solid rgba(175,175,175, " + distance + ")");
|
|
inputElement.css("background-color", "rgba(255,255,255, " + distance + ")");
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
}, 15);
|
|
|
|
$(document).bind("mousemove", mouseMoveDebounce);
|
|
|
|
$timeout(function(){
|
|
if(!scope.model){
|
|
scope.goEdit();
|
|
}
|
|
}, 100, false);
|
|
|
|
scope.goEdit = function(){
|
|
scope.editMode = true;
|
|
|
|
$timeout(function () {
|
|
inputElement.focus();
|
|
}, 100, false);
|
|
};
|
|
|
|
scope.exitEdit = function(){
|
|
if(scope.model && scope.model !== ""){
|
|
scope.editMode = false;
|
|
}
|
|
};
|
|
|
|
//unbind doc event!
|
|
scope.$on('$destroy', function () {
|
|
$(document).unbind("mousemove", mouseMoveDebounce);
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbHeader
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
* @restrict E
|
|
* @function
|
|
* @description
|
|
* The header on an editor that contains tabs using bootstrap tabs - THIS IS OBSOLETE, use umbTabHeader instead
|
|
**/
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('umbHeader', function ($parse, $timeout) {
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
transclude: 'true',
|
|
templateUrl: 'views/directives/_obsolete/umb-header.html',
|
|
//create a new isolated scope assigning a tabs property from the attribute 'tabs'
|
|
//which is bound to the parent scope property passed in
|
|
scope: {
|
|
tabs: "="
|
|
},
|
|
link: function (scope, iElement, iAttrs) {
|
|
|
|
scope.showTabs = iAttrs.tabs ? true : false;
|
|
scope.visibleTabs = [];
|
|
|
|
//since tabs are loaded async, we need to put a watch on them to determine
|
|
// when they are loaded, then we can close the watch
|
|
var tabWatch = scope.$watch("tabs", function (newValue, oldValue) {
|
|
|
|
angular.forEach(newValue, function(val, index){
|
|
var tab = {id: val.id, label: val.label};
|
|
scope.visibleTabs.push(tab);
|
|
});
|
|
|
|
//don't process if we cannot or have already done so
|
|
if (!newValue) {return;}
|
|
if (!newValue.length || newValue.length === 0){return;}
|
|
|
|
//we need to do a timeout here so that the current sync operation can complete
|
|
// and update the UI, then this will fire and the UI elements will be available.
|
|
$timeout(function () {
|
|
|
|
//use bootstrap tabs API to show the first one
|
|
iElement.find(".nav-tabs a:first").tab('show');
|
|
|
|
//enable the tab drop
|
|
iElement.find('.nav-pills, .nav-tabs').tabdrop();
|
|
|
|
//ensure to destroy tabdrop (unbinds window resize listeners)
|
|
scope.$on('$destroy', function () {
|
|
iElement.find('.nav-pills, .nav-tabs').tabdrop("destroy");
|
|
});
|
|
|
|
//stop watching now
|
|
tabWatch();
|
|
}, 200);
|
|
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbLogin
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
* @function
|
|
* @element ANY
|
|
* @restrict E
|
|
**/
|
|
|
|
function loginDirective() {
|
|
return {
|
|
restrict: "E", // restrict to an element
|
|
replace: true, // replace the html element with the template
|
|
templateUrl: 'views/directives/_obsolete/umb-login.html'
|
|
};
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive("umbLogin", loginDirective);
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbOptionsMenu
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
* @function
|
|
* @element ANY
|
|
* @restrict E
|
|
**/
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('umbOptionsMenu', function ($injector, treeService, navigationService, umbModelMapper, appState) {
|
|
return {
|
|
scope: {
|
|
currentSection: "@",
|
|
currentNode: "="
|
|
},
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/directives/_obsolete/umb-optionsmenu.html',
|
|
link: function (scope, element, attrs, ctrl) {
|
|
|
|
//adds a handler to the context menu item click, we need to handle this differently
|
|
//depending on what the menu item is supposed to do.
|
|
scope.executeMenuItem = function (action) {
|
|
navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection);
|
|
};
|
|
|
|
//callback method to go and get the options async
|
|
scope.getOptions = function () {
|
|
|
|
if (!scope.currentNode) {
|
|
return;
|
|
}
|
|
|
|
//when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu)
|
|
appState.setMenuState("currentNode", scope.currentNode);
|
|
|
|
if (!scope.actions) {
|
|
treeService.getMenu({ treeNode: scope.currentNode })
|
|
.then(function (data) {
|
|
scope.actions = data.menuItems;
|
|
});
|
|
}
|
|
};
|
|
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbPhotoFolder
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
* @restrict E
|
|
**/
|
|
|
|
angular.module("umbraco.directives.html")
|
|
.directive('umbPhotoFolder', function($compile, $log, $timeout, $filter, umbPhotoFolderHelper) {
|
|
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
require: '?ngModel',
|
|
terminate: true,
|
|
templateUrl: 'views/directives/_obsolete/umb-photo-folder.html',
|
|
link: function(scope, element, attrs, ngModel) {
|
|
|
|
var lastWatch = null;
|
|
|
|
ngModel.$render = function() {
|
|
if (ngModel.$modelValue) {
|
|
|
|
$timeout(function() {
|
|
var photos = ngModel.$modelValue;
|
|
|
|
scope.clickHandler = scope.$eval(element.attr('on-click'));
|
|
|
|
|
|
var imagesOnly = element.attr('images-only') === "true";
|
|
|
|
|
|
var margin = element.attr('border') ? parseInt(element.attr('border'), 10) : 5;
|
|
var startingIndex = element.attr('baseline') ? parseInt(element.attr('baseline'), 10) : 0;
|
|
var minWidth = element.attr('min-width') ? parseInt(element.attr('min-width'), 10) : 420;
|
|
var minHeight = element.attr('min-height') ? parseInt(element.attr('min-height'), 10) : 100;
|
|
var maxHeight = element.attr('max-height') ? parseInt(element.attr('max-height'), 10) : 300;
|
|
var idealImgPerRow = element.attr('ideal-items-per-row') ? parseInt(element.attr('ideal-items-per-row'), 10) : 5;
|
|
var fixedRowWidth = Math.max(element.width(), minWidth);
|
|
|
|
scope.containerStyle = { width: fixedRowWidth + "px" };
|
|
scope.rows = umbPhotoFolderHelper.buildGrid(photos, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly);
|
|
|
|
if (attrs.filterBy) {
|
|
|
|
//we track the watches that we create, we don't want to create multiple, so clear it
|
|
// if it already exists before creating another.
|
|
if (lastWatch) {
|
|
lastWatch();
|
|
}
|
|
|
|
//TODO: Need to debounce this so it doesn't filter too often!
|
|
lastWatch = scope.$watch(attrs.filterBy, function (newVal, oldVal) {
|
|
if (newVal && newVal !== oldVal) {
|
|
var p = $filter('filter')(photos, newVal, false);
|
|
scope.baseline = 0;
|
|
var m = umbPhotoFolderHelper.buildGrid(p, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly);
|
|
scope.rows = m;
|
|
}
|
|
});
|
|
}
|
|
|
|
}, 500); //end timeout
|
|
} //end if modelValue
|
|
|
|
}; //end $render
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbSort
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
*
|
|
* @element div
|
|
* @function
|
|
*
|
|
* @description
|
|
* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set
|
|
* So if you only want to scale the div to 70 pixels from the bottom you pass "70"
|
|
*
|
|
* @example
|
|
* <example module="umbraco.directives">
|
|
* <file name="index.html">
|
|
* <div umb-sort="70" class="input-block-level"></div>
|
|
* </file>
|
|
* </example>
|
|
**/
|
|
|
|
angular.module("umbraco.directives")
|
|
.value('umbSortContextInternal',{})
|
|
.directive('umbSort', function($log,umbSortContextInternal) {
|
|
return {
|
|
require: '?ngModel',
|
|
link: function(scope, element, attrs, ngModel) {
|
|
var adjustment;
|
|
|
|
var cfg = scope.$eval(element.attr('umb-sort')) || {};
|
|
|
|
scope.model = ngModel;
|
|
|
|
scope.opts = cfg;
|
|
scope.opts.containerSelector= cfg.containerSelector || ".umb-" + cfg.group + "-container",
|
|
scope.opts.nested= cfg.nested || true,
|
|
scope.opts.drop= cfg.drop || true,
|
|
scope.opts.drag= cfg.drag || true,
|
|
scope.opts.clone = cfg.clone || "<li/>";
|
|
scope.opts.mode = cfg.mode || "list";
|
|
|
|
scope.opts.itemSelectorFull = $.trim(scope.opts.itemPath + " " + scope.opts.itemSelector);
|
|
|
|
/*
|
|
scope.opts.isValidTarget = function(item, container) {
|
|
if(container.el.is(".umb-" + scope.opts.group + "-container")){
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
*/
|
|
|
|
element.addClass("umb-sort");
|
|
element.addClass("umb-" + cfg.group + "-container");
|
|
|
|
scope.opts.onDrag = function (item, position) {
|
|
if(scope.opts.mode === "list"){
|
|
item.css({
|
|
left: position.left - adjustment.left,
|
|
top: position.top - adjustment.top
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
scope.opts.onDrop = function (item, targetContainer, _super) {
|
|
|
|
if(scope.opts.mode === "list"){
|
|
//list mode
|
|
var clonedItem = $(scope.opts.clone).css({height: 0});
|
|
item.after(clonedItem);
|
|
clonedItem.animate({'height': item.height()});
|
|
|
|
item.animate(clonedItem.position(), function () {
|
|
clonedItem.detach();
|
|
_super(item);
|
|
});
|
|
}
|
|
|
|
var children = $(scope.opts.itemSelectorFull, targetContainer.el);
|
|
var targetIndex = children.index(item);
|
|
var targetScope = $(targetContainer.el[0]).scope();
|
|
|
|
|
|
if(targetScope === umbSortContextInternal.sourceScope){
|
|
if(umbSortContextInternal.sourceScope.opts.onSortHandler){
|
|
var _largs = {
|
|
oldIndex: umbSortContextInternal.sourceIndex,
|
|
newIndex: targetIndex,
|
|
scope: umbSortContextInternal.sourceScope
|
|
};
|
|
|
|
umbSortContextInternal.sourceScope.opts.onSortHandler.call(this, item, _largs);
|
|
}
|
|
}else{
|
|
|
|
|
|
if(targetScope.opts.onDropHandler){
|
|
var args = {
|
|
sourceScope: umbSortContextInternal.sourceScope,
|
|
sourceIndex: umbSortContextInternal.sourceIndex,
|
|
sourceContainer: umbSortContextInternal.sourceContainer,
|
|
|
|
targetScope: targetScope,
|
|
targetIndex: targetIndex,
|
|
targetContainer: targetContainer
|
|
};
|
|
|
|
targetScope.opts.onDropHandler.call(this, item, args);
|
|
}
|
|
|
|
if(umbSortContextInternal.sourceScope.opts.onReleaseHandler){
|
|
var _args = {
|
|
sourceScope: umbSortContextInternal.sourceScope,
|
|
sourceIndex: umbSortContextInternal.sourceIndex,
|
|
sourceContainer: umbSortContextInternal.sourceContainer,
|
|
|
|
targetScope: targetScope,
|
|
targetIndex: targetIndex,
|
|
targetContainer: targetContainer
|
|
};
|
|
|
|
umbSortContextInternal.sourceScope.opts.onReleaseHandler.call(this, item, _args);
|
|
}
|
|
}
|
|
};
|
|
|
|
scope.changeIndex = function(from, to){
|
|
scope.$apply(function(){
|
|
var i = ngModel.$modelValue.splice(from, 1)[0];
|
|
ngModel.$modelValue.splice(to, 0, i);
|
|
});
|
|
};
|
|
|
|
scope.move = function(args){
|
|
var from = args.sourceIndex;
|
|
var to = args.targetIndex;
|
|
|
|
if(args.sourceContainer === args.targetContainer){
|
|
scope.changeIndex(from, to);
|
|
}else{
|
|
scope.$apply(function(){
|
|
var i = args.sourceScope.model.$modelValue.splice(from, 1)[0];
|
|
args.targetScope.model.$modelvalue.splice(to,0, i);
|
|
});
|
|
}
|
|
};
|
|
|
|
scope.opts.onDragStart = function (item, container, _super) {
|
|
var children = $(scope.opts.itemSelectorFull, container.el);
|
|
var offset = item.offset();
|
|
|
|
umbSortContextInternal.sourceIndex = children.index(item);
|
|
umbSortContextInternal.sourceScope = $(container.el[0]).scope();
|
|
umbSortContextInternal.sourceContainer = container;
|
|
|
|
//current.item = ngModel.$modelValue.splice(current.index, 1)[0];
|
|
|
|
var pointer = container.rootGroup.pointer;
|
|
adjustment = {
|
|
left: pointer.left - offset.left,
|
|
top: pointer.top - offset.top
|
|
};
|
|
|
|
_super(item, container);
|
|
};
|
|
|
|
element.sortable( scope.opts );
|
|
}
|
|
};
|
|
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbTabView
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
*
|
|
* @restrict E
|
|
**/
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('umbTabView', function($timeout, $log){
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
transclude: 'true',
|
|
templateUrl: 'views/directives/_obsolete/umb-tab-view.html'
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbUploadDropzone
|
|
* @deprecated
|
|
* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
|
|
*
|
|
* @restrict E
|
|
**/
|
|
|
|
angular.module("umbraco.directives.html")
|
|
.directive('umbUploadDropzone', function(){
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/directives/_obsolete/umb-upload-dropzone.html'
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:navResize
|
|
* @restrict A
|
|
*
|
|
* @description
|
|
* Handles how the navigation responds to window resizing and controls how the draggable resize panel works
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('navResize', function (appState, eventsService, windowResizeListener) {
|
|
return {
|
|
restrict: 'A',
|
|
link: function (scope, element, attrs, ctrl) {
|
|
|
|
var minScreenSize = 1100;
|
|
var resizeEnabled = false;
|
|
|
|
function setTreeMode() {
|
|
appState.setGlobalState("showNavigation", appState.getGlobalState("isTablet") === false);
|
|
}
|
|
|
|
function enableResize() {
|
|
//only enable when the size is correct and it's not already enabled
|
|
if (!resizeEnabled && appState.getGlobalState("isTablet") === false) {
|
|
element.resizable(
|
|
{
|
|
containment: $("#mainwrapper"),
|
|
autoHide: true,
|
|
handles: "e",
|
|
alsoResize: ".navigation-inner-container",
|
|
resize: function(e, ui) {
|
|
var wrapper = $("#mainwrapper");
|
|
var contentPanel = $("#contentwrapper");
|
|
var umbNotification = $("#umb-notifications-wrapper");
|
|
var apps = $("#applications");
|
|
var bottomBar = contentPanel.find(".umb-bottom-bar");
|
|
var navOffeset = $("#navOffset");
|
|
|
|
var leftPanelWidth = ui.element.width() + apps.width();
|
|
|
|
contentPanel.css({ left: leftPanelWidth });
|
|
bottomBar.css({ left: leftPanelWidth });
|
|
umbNotification.css({ left: leftPanelWidth });
|
|
|
|
navOffeset.css({ "margin-left": ui.element.outerWidth() });
|
|
},
|
|
stop: function (e, ui) {
|
|
|
|
}
|
|
});
|
|
|
|
resizeEnabled = true;
|
|
}
|
|
}
|
|
|
|
function resetResize() {
|
|
if (resizeEnabled) {
|
|
//kill the resize
|
|
element.resizable("destroy");
|
|
element.css("width", "");
|
|
|
|
var navInnerContainer = element.find(".navigation-inner-container");
|
|
|
|
navInnerContainer.css("width", "");
|
|
$("#contentwrapper").css("left", "");
|
|
$("#umb-notifications-wrapper").css("left", "");
|
|
$("#navOffset").css("margin-left", "");
|
|
|
|
resizeEnabled = false;
|
|
}
|
|
}
|
|
|
|
var evts = [];
|
|
|
|
//Listen for global state changes
|
|
evts.push(eventsService.on("appState.globalState.changed", function (e, args) {
|
|
if (args.key === "showNavigation") {
|
|
if (args.value === false) {
|
|
resetResize();
|
|
}
|
|
else {
|
|
enableResize();
|
|
}
|
|
}
|
|
}));
|
|
|
|
var resizeCallback = function(size) {
|
|
//set the global app state
|
|
appState.setGlobalState("isTablet", (size.width <= minScreenSize));
|
|
setTreeMode();
|
|
};
|
|
|
|
windowResizeListener.register(resizeCallback);
|
|
|
|
//ensure to unregister from all events and kill jquery plugins
|
|
scope.$on('$destroy', function () {
|
|
windowResizeListener.unregister(resizeCallback);
|
|
for (var e in evts) {
|
|
eventsService.unsubscribe(evts[e]);
|
|
}
|
|
var navInnerContainer = element.find(".navigation-inner-container");
|
|
navInnerContainer.resizable("destroy");
|
|
});
|
|
|
|
//init
|
|
//set the global app state
|
|
appState.setGlobalState("isTablet", ($(window).width() <= minScreenSize));
|
|
setTreeMode();
|
|
}
|
|
};
|
|
});
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('sectionIcon', function ($compile, iconHelper) {
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
|
|
link: function (scope, element, attrs) {
|
|
|
|
var icon = attrs.icon;
|
|
|
|
if (iconHelper.isLegacyIcon(icon)) {
|
|
//its a known legacy icon, convert to a new one
|
|
element.html("<i class='" + iconHelper.convertFromLegacyIcon(icon) + "'></i>");
|
|
}
|
|
else if (iconHelper.isFileBasedIcon(icon)) {
|
|
var convert = iconHelper.convertFromLegacyImage(icon);
|
|
if(convert){
|
|
element.html("<i class='icon-section " + convert + "'></i>");
|
|
}else{
|
|
element.html("<img class='icon-section' src='images/tray/" + icon + "'>");
|
|
}
|
|
//it's a file, normally legacy so look in the icon tray images
|
|
}
|
|
else {
|
|
//it's normal
|
|
element.html("<i class='icon-section " + icon + "'></i>");
|
|
}
|
|
}
|
|
};
|
|
});
|
|
angular.module("umbraco.directives")
|
|
.directive('umbContextMenu', function (navigationService) {
|
|
return {
|
|
scope: {
|
|
menuDialogTitle: "@",
|
|
currentSection: "@",
|
|
currentNode: "=",
|
|
menuActions: "="
|
|
},
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/application/umb-contextmenu.html',
|
|
link: function (scope, element, attrs, ctrl) {
|
|
|
|
//adds a handler to the context menu item click, we need to handle this differently
|
|
//depending on what the menu item is supposed to do.
|
|
scope.executeMenuItem = function (action) {
|
|
navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection);
|
|
};
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbNavigation
|
|
* @restrict E
|
|
**/
|
|
function umbNavigationDirective() {
|
|
return {
|
|
restrict: "E", // restrict to an element
|
|
replace: true, // replace the html element with the template
|
|
templateUrl: 'views/components/application/umb-navigation.html'
|
|
};
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive("umbNavigation", umbNavigationDirective);
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbSections
|
|
* @restrict E
|
|
**/
|
|
function sectionsDirective($timeout, $window, navigationService, treeService, sectionResource, appState, eventsService, $location) {
|
|
return {
|
|
restrict: "E", // restrict to an element
|
|
replace: true, // replace the html element with the template
|
|
templateUrl: 'views/components/application/umb-sections.html',
|
|
link: function (scope, element, attr, ctrl) {
|
|
|
|
//setup scope vars
|
|
scope.maxSections = 7;
|
|
scope.overflowingSections = 0;
|
|
scope.sections = [];
|
|
scope.currentSection = appState.getSectionState("currentSection");
|
|
scope.showTray = false; //appState.getGlobalState("showTray");
|
|
scope.stickyNavigation = appState.getGlobalState("stickyNavigation");
|
|
scope.needTray = false;
|
|
scope.trayAnimation = function() {
|
|
if (scope.showTray) {
|
|
return 'slide';
|
|
}
|
|
else if (scope.showTray === false) {
|
|
return 'slide';
|
|
}
|
|
else {
|
|
return '';
|
|
}
|
|
};
|
|
|
|
function loadSections(){
|
|
sectionResource.getSections()
|
|
.then(function (result) {
|
|
scope.sections = result;
|
|
calculateHeight();
|
|
});
|
|
}
|
|
|
|
function calculateHeight(){
|
|
$timeout(function(){
|
|
//total height minus room for avatar and help icon
|
|
var height = $(window).height()-200;
|
|
scope.totalSections = scope.sections.length;
|
|
scope.maxSections = Math.floor(height / 70);
|
|
scope.needTray = false;
|
|
|
|
if(scope.totalSections > scope.maxSections){
|
|
scope.needTray = true;
|
|
scope.overflowingSections = scope.maxSections - scope.totalSections;
|
|
}
|
|
});
|
|
}
|
|
|
|
var evts = [];
|
|
|
|
//Listen for global state changes
|
|
evts.push(eventsService.on("appState.globalState.changed", function(e, args) {
|
|
if (args.key === "showTray") {
|
|
scope.showTray = args.value;
|
|
}
|
|
if (args.key === "stickyNavigation") {
|
|
scope.stickyNavigation = args.value;
|
|
}
|
|
}));
|
|
|
|
evts.push(eventsService.on("appState.sectionState.changed", function(e, args) {
|
|
if (args.key === "currentSection") {
|
|
scope.currentSection = args.value;
|
|
}
|
|
}));
|
|
|
|
evts.push(eventsService.on("app.reInitialize", function(e, args) {
|
|
//re-load the sections if we're re-initializing (i.e. package installed)
|
|
loadSections();
|
|
}));
|
|
|
|
//ensure to unregister from all events!
|
|
scope.$on('$destroy', function () {
|
|
for (var e in evts) {
|
|
eventsService.unsubscribe(evts[e]);
|
|
}
|
|
});
|
|
|
|
//on page resize
|
|
window.onresize = calculateHeight;
|
|
|
|
scope.avatarClick = function(){
|
|
|
|
if(scope.helpDialog) {
|
|
closeHelpDialog();
|
|
}
|
|
|
|
if(!scope.userDialog) {
|
|
scope.userDialog = {
|
|
view: "user",
|
|
show: true,
|
|
close: function(oldModel) {
|
|
closeUserDialog();
|
|
}
|
|
};
|
|
} else {
|
|
closeUserDialog();
|
|
}
|
|
|
|
};
|
|
|
|
function closeUserDialog() {
|
|
scope.userDialog.show = false;
|
|
scope.userDialog = null;
|
|
}
|
|
|
|
scope.helpClick = function(){
|
|
|
|
if(scope.userDialog) {
|
|
closeUserDialog();
|
|
}
|
|
|
|
if(!scope.helpDialog) {
|
|
scope.helpDialog = {
|
|
view: "help",
|
|
show: true,
|
|
close: function(oldModel) {
|
|
closeHelpDialog();
|
|
}
|
|
};
|
|
} else {
|
|
closeHelpDialog();
|
|
}
|
|
|
|
};
|
|
|
|
function closeHelpDialog() {
|
|
scope.helpDialog.show = false;
|
|
scope.helpDialog = null;
|
|
}
|
|
|
|
scope.sectionClick = function (event, section) {
|
|
|
|
if (event.ctrlKey ||
|
|
event.shiftKey ||
|
|
event.metaKey || // apple
|
|
(event.button && event.button === 1) // middle click, >IE9 + everyone else
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (scope.userDialog) {
|
|
closeUserDialog();
|
|
}
|
|
if (scope.helpDialog) {
|
|
closeHelpDialog();
|
|
}
|
|
|
|
navigationService.hideSearch();
|
|
navigationService.showTree(section.alias);
|
|
$location.path("/" + section.alias);
|
|
};
|
|
|
|
scope.sectionDblClick = function(section){
|
|
navigationService.reloadSection(section.alias);
|
|
};
|
|
|
|
scope.trayClick = function () {
|
|
// close dialogs
|
|
if (scope.userDialog) {
|
|
closeUserDialog();
|
|
}
|
|
if (scope.helpDialog) {
|
|
closeHelpDialog();
|
|
}
|
|
|
|
if (appState.getGlobalState("showTray") === true) {
|
|
navigationService.hideTray();
|
|
} else {
|
|
navigationService.showTray();
|
|
}
|
|
};
|
|
|
|
loadSections();
|
|
|
|
}
|
|
};
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive("umbSections", sectionsDirective);
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbButton
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to render an umbraco button. The directive can be used to generate all types of buttons, set type, style, translation, shortcut and much more.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-button
|
|
action="vm.clickButton()"
|
|
type="button"
|
|
button-style="success"
|
|
state="vm.buttonState"
|
|
shortcut="ctrl+c"
|
|
label="My button"
|
|
disabled="vm.buttonState === 'busy'">
|
|
</umb-button>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller(myService) {
|
|
|
|
var vm = this;
|
|
vm.buttonState = "init";
|
|
|
|
vm.clickButton = clickButton;
|
|
|
|
function clickButton() {
|
|
|
|
vm.buttonState = "busy";
|
|
|
|
myService.clickButton().then(function() {
|
|
vm.buttonState = "success";
|
|
}, function() {
|
|
vm.buttonState = "error";
|
|
});
|
|
|
|
}
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
|
|
})();
|
|
</pre>
|
|
|
|
@param {callback} action The button action which should be performed when the button is clicked.
|
|
@param {string=} href Url/Path to navigato to.
|
|
@param {string=} type Set the button type ("button" or "submit").
|
|
@param {string=} buttonStyle Set the style of the button. The directive uses the default bootstrap styles ("primary", "info", "success", "warning", "danger", "inverse", "link").
|
|
@param {string=} state Set a progress state on the button ("init", "busy", "success", "error").
|
|
@param {string=} shortcut Set a keyboard shortcut for the button ("ctrl+c").
|
|
@param {string=} label Set the button label.
|
|
@param {string=} labelKey Set a localization key to make a multi lingual button ("general_buttonText").
|
|
@param {string=} icon Set a button icon. Can only be used when buttonStyle is "link".
|
|
@param {boolean=} disabled Set to <code>true</code> to disable the button.
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function ButtonDirective($timeout) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.style = null;
|
|
|
|
function activate() {
|
|
|
|
if (!scope.state) {
|
|
scope.state = "init";
|
|
}
|
|
|
|
if (scope.buttonStyle) {
|
|
scope.style = "btn-" + scope.buttonStyle;
|
|
}
|
|
|
|
}
|
|
|
|
activate();
|
|
|
|
var unbindStateWatcher = scope.$watch('state', function(newValue, oldValue) {
|
|
|
|
if (newValue === 'success' || newValue === 'error') {
|
|
$timeout(function() {
|
|
scope.state = 'init';
|
|
}, 2000);
|
|
}
|
|
|
|
});
|
|
|
|
scope.$on('$destroy', function() {
|
|
unbindStateWatcher();
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/buttons/umb-button.html',
|
|
link: link,
|
|
scope: {
|
|
action: "&?",
|
|
href: "@?",
|
|
type: "@",
|
|
buttonStyle: "@?",
|
|
state: "=?",
|
|
shortcut: "@?",
|
|
shortcutWhenHidden: "@",
|
|
label: "@?",
|
|
labelKey: "@?",
|
|
icon: "@?",
|
|
disabled: "="
|
|
}
|
|
};
|
|
|
|
return directive;
|
|
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbButton', ButtonDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbButtonGroup
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to render a button with a dropdown of alternative actions.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-button-group
|
|
ng-if="vm.buttonGroup"
|
|
default-button="vm.buttonGroup.defaultButton"
|
|
sub-buttons="vm.buttonGroup.subButtons"
|
|
direction="down"
|
|
float="right">
|
|
</umb-button-group>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
|
|
vm.buttonGroup = {
|
|
defaultButton: {
|
|
labelKey: "general_defaultButton",
|
|
hotKey: "ctrl+d",
|
|
hotKeyWhenHidden: true,
|
|
handler: function() {
|
|
// do magic here
|
|
}
|
|
},
|
|
subButtons: [
|
|
{
|
|
labelKey: "general_subButton",
|
|
hotKey: "ctrl+b",
|
|
hotKeyWhenHidden: true,
|
|
handler: function() {
|
|
// do magic here
|
|
}
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
|
|
})();
|
|
</pre>
|
|
|
|
<h3>Button model description</h3>
|
|
<ul>
|
|
<li>
|
|
<strong>labekKey</strong>
|
|
<small>(string)</small> -
|
|
Set a localization key to make a multi lingual button ("general_buttonText").
|
|
</li>
|
|
<li>
|
|
<strong>hotKey</strong>
|
|
<small>(array)</small> -
|
|
Set a keyboard shortcut for the button ("ctrl+c").
|
|
</li>
|
|
<li>
|
|
<strong>hotKeyWhenHidden</strong>
|
|
<small>(boolean)</small> -
|
|
As a default the hotkeys only works on elements visible in the UI. Set to <code>true</code> to set a hotkey on the hidden sub buttons.
|
|
</li>
|
|
<li>
|
|
<strong>handler</strong>
|
|
<small>(callback)</small> -
|
|
Set a callback to handle button click events.
|
|
</li>
|
|
</ul>
|
|
|
|
@param {object} defaultButton The model of the default button.
|
|
@param {array} subButtons Array of sub buttons.
|
|
@param {string=} state Set a progress state on the button ("init", "busy", "success", "error").
|
|
@param {string=} direction Set the direction of the dropdown ("up", "down").
|
|
@param {string=} float Set the float of the dropdown. ("left", "right").
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function ButtonGroupDirective() {
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/buttons/umb-button-group.html',
|
|
scope: {
|
|
defaultButton: "=",
|
|
subButtons: "=",
|
|
state: "=?",
|
|
direction: "@?",
|
|
float: "@?"
|
|
}
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbButtonGroup', ButtonGroupDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorSubHeader
|
|
@restrict E
|
|
|
|
@description
|
|
Use this directive to construct a sub header in the main editor window.
|
|
The sub header is sticky and will follow along down the page when scrolling.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-container>
|
|
|
|
<umb-editor-sub-header>
|
|
// sub header content here
|
|
</umb-editor-sub-header>
|
|
|
|
</umb-editor-container>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorSubHeaderDirective() {
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/subheader/umb-editor-sub-header.html'
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorSubHeader', EditorSubHeaderDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorSubHeaderContentLeft
|
|
@restrict E
|
|
|
|
@description
|
|
Use this directive to left align content in a sub header in the main editor window.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-container>
|
|
|
|
<umb-editor-sub-header>
|
|
|
|
<umb-editor-sub-header-content-left>
|
|
// left content here
|
|
</umb-editor-sub-header-content-left>
|
|
|
|
<umb-editor-sub-header-content-right>
|
|
// right content here
|
|
</umb-editor-sub-header-content-right>
|
|
|
|
</umb-editor-sub-header>
|
|
|
|
</umb-editor-container>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorSubHeaderContentLeftDirective() {
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-left.html'
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorSubHeaderContentLeft', EditorSubHeaderContentLeftDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorSubHeaderContentRight
|
|
@restrict E
|
|
|
|
@description
|
|
Use this directive to rigt align content in a sub header in the main editor window.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-container>
|
|
|
|
<umb-editor-sub-header>
|
|
|
|
<umb-editor-sub-header-content-left>
|
|
// left content here
|
|
</umb-editor-sub-header-content-left>
|
|
|
|
<umb-editor-sub-header-content-right>
|
|
// right content here
|
|
</umb-editor-sub-header-content-right>
|
|
|
|
</umb-editor-sub-header>
|
|
|
|
</umb-editor-container>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorSubHeaderContentRightDirective() {
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-right.html'
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorSubHeaderContentRight', EditorSubHeaderContentRightDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorSubHeaderSection
|
|
@restrict E
|
|
|
|
@description
|
|
Use this directive to create sections, divided by borders, in a sub header in the main editor window.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-container>
|
|
|
|
<umb-editor-sub-header>
|
|
|
|
<umb-editor-sub-header-content-right>
|
|
|
|
<umb-editor-sub-header-section>
|
|
// section content here
|
|
</umb-editor-sub-header-section>
|
|
|
|
<umb-editor-sub-header-section>
|
|
// section content here
|
|
</umb-editor-sub-header-section>
|
|
|
|
<umb-editor-sub-header-section>
|
|
// section content here
|
|
</umb-editor-sub-header-section>
|
|
|
|
</umb-editor-sub-header-content-right>
|
|
|
|
</umb-editor-sub-header>
|
|
|
|
</umb-editor-container>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorSubHeaderSectionDirective() {
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-section.html'
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorSubHeaderSection', EditorSubHeaderSectionDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbBreadcrumbs
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to generate a list of breadcrumbs.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
<umb-breadcrumbs
|
|
ng-if="vm.ancestors && vm.ancestors.length > 0"
|
|
ancestors="vm.ancestors"
|
|
entity-type="content">
|
|
</umb-breadcrumbs>
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller(myService) {
|
|
|
|
var vm = this;
|
|
vm.ancestors = [];
|
|
|
|
myService.getAncestors().then(function(ancestors){
|
|
vm.ancestors = ancestors;
|
|
});
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
@param {array} ancestors Array of ancestors
|
|
@param {string} entityType The content entity type (member, media, content).
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function BreadcrumbsDirective() {
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-breadcrumbs.html',
|
|
scope: {
|
|
ancestors: "=",
|
|
entityType: "@"
|
|
}
|
|
};
|
|
|
|
return directive;
|
|
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorContainer
|
|
@restrict E
|
|
|
|
@description
|
|
Use this directive to construct a main content area inside the main editor window.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="Umbraco.Controller as vm">
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-header
|
|
// header configuration>
|
|
</umb-editor-header>
|
|
|
|
<umb-editor-container>
|
|
// main content here
|
|
</umb-editor-container>
|
|
|
|
<umb-editor-footer>
|
|
// footer content here
|
|
</umb-editor-footer>
|
|
|
|
</umb-editor-view>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorContainerDirective(overlayHelper) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.numberOfOverlays = 0;
|
|
|
|
scope.$watch(function(){
|
|
return overlayHelper.getNumberOfOverlays();
|
|
}, function (newValue) {
|
|
scope.numberOfOverlays = newValue;
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-editor-container.html',
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorContainer', EditorContainerDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorFooter
|
|
@restrict E
|
|
|
|
@description
|
|
Use this directive to construct a footer inside the main editor window.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-header
|
|
// header configuration>
|
|
</umb-editor-header>
|
|
|
|
<umb-editor-container>
|
|
// main content here
|
|
</umb-editor-container>
|
|
|
|
<umb-editor-footer>
|
|
// footer content here
|
|
</umb-editor-footer>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorFooterDirective() {
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-editor-footer.html'
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorFooter', EditorFooterDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorFooterContentLeft
|
|
@restrict E
|
|
|
|
@description
|
|
Use this directive to align content left inside the main editor footer.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-footer>
|
|
|
|
<umb-editor-footer-content-left>
|
|
// align content left
|
|
</umb-editor-footer-content-left>
|
|
|
|
<umb-editor-footer-content-right>
|
|
// align content right
|
|
</umb-editor-footer-content-right>
|
|
|
|
</umb-editor-footer>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorFooterContentLeftDirective() {
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-editor-footer-content-left.html'
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorFooterContentLeft', EditorFooterContentLeftDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorFooterContentRight
|
|
@restrict E
|
|
|
|
@description
|
|
Use this directive to align content right inside the main editor footer.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-footer>
|
|
|
|
<umb-editor-footer-content-left>
|
|
// align content left
|
|
</umb-editor-footer-content-left>
|
|
|
|
<umb-editor-footer-content-right>
|
|
// align content right
|
|
</umb-editor-footer-content-right>
|
|
|
|
</umb-editor-footer>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorFooterContentRightDirective() {
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-editor-footer-content-right.html'
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorFooterContentRight', EditorFooterContentRightDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorHeader
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to construct a header inside the main editor window.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-header
|
|
name="vm.content.name"
|
|
hide-alias="true"
|
|
hide-description="true"
|
|
hide-icon="true">
|
|
</umb-editor-header>
|
|
|
|
<umb-editor-container>
|
|
// main content here
|
|
</umb-editor-container>
|
|
|
|
<umb-editor-footer>
|
|
// footer content here
|
|
</umb-editor-footer>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Markup example - with tabs</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" val-form-manager novalidate>
|
|
|
|
<umb-editor-view umb-tabs>
|
|
|
|
<umb-editor-header
|
|
name="vm.content.name"
|
|
tabs="vm.content.tabs"
|
|
hide-alias="true"
|
|
hide-description="true"
|
|
hide-icon="true">
|
|
</umb-editor-header>
|
|
|
|
<umb-editor-container>
|
|
<umb-tabs-content class="form-horizontal" view="true">
|
|
<umb-tab id="tab{{tab.id}}" ng-repeat="tab in vm.content.tabs" rel="{{tab.id}}">
|
|
|
|
<div ng-show="tab.alias==='tab1'">
|
|
// tab 1 content
|
|
</div>
|
|
|
|
<div ng-show="tab.alias==='tab2'">
|
|
// tab 2 content
|
|
</div>
|
|
|
|
</umb-tab>
|
|
</umb-tabs-content>
|
|
</umb-editor-container>
|
|
|
|
<umb-editor-footer>
|
|
// footer content here
|
|
</umb-editor-footer>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example - with tabs</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
vm.content = {
|
|
name: "",
|
|
tabs: [
|
|
{
|
|
id: 1,
|
|
label: "Tab 1",
|
|
alias: "tab1",
|
|
active: true
|
|
},
|
|
{
|
|
id: 2,
|
|
label: "Tab 2",
|
|
alias: "tab2",
|
|
active: false
|
|
}
|
|
]
|
|
};
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("MySection.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
<h3>Markup example - with sub views</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" val-form-manager novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-header
|
|
name="vm.content.name"
|
|
navigation="vm.content.navigation"
|
|
hide-alias="true"
|
|
hide-description="true"
|
|
hide-icon="true">
|
|
</umb-editor-header>
|
|
|
|
<umb-editor-container>
|
|
|
|
<umb-editor-sub-views
|
|
sub-views="vm.content.navigation"
|
|
model="vm.content">
|
|
</umb-editor-sub-views>
|
|
|
|
</umb-editor-container>
|
|
|
|
<umb-editor-footer>
|
|
// footer content here
|
|
</umb-editor-footer>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example - with sub views</h3>
|
|
<pre>
|
|
(function () {
|
|
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
vm.content = {
|
|
name: "",
|
|
navigation: [
|
|
{
|
|
"name": "Section 1",
|
|
"icon": "icon-document-dashed-line",
|
|
"view": "/App_Plugins/path/to/html.html",
|
|
"active": true
|
|
},
|
|
{
|
|
"name": "Section 2",
|
|
"icon": "icon-list",
|
|
"view": "/App_Plugins/path/to/html.html",
|
|
}
|
|
]
|
|
};
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("MySection.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
|
|
</ul>
|
|
|
|
@param {string} name The content name.
|
|
@param {array=} tabs Array of tabs. See example above.
|
|
@param {array=} navigation Array of sub views. See example above.
|
|
@param {boolean=} nameLocked Set to <code>true</code> to lock the name.
|
|
@param {object=} menu Add a context menu to the editor.
|
|
@param {string=} icon Show and edit the content icon. Opens an overlay to change the icon.
|
|
@param {boolean=} hideIcon Set to <code>true</code> to hide icon.
|
|
@param {string=} alias show and edit the content alias.
|
|
@param {boolean=} hideAlias Set to <code>true</code> to hide alias.
|
|
@param {string=} description Add a description to the content.
|
|
@param {boolean=} hideDescription Set to <code>true</code> to hide description.
|
|
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorHeaderDirective(iconHelper) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.openIconPicker = function() {
|
|
scope.dialogModel = {
|
|
view: "iconpicker",
|
|
show: true,
|
|
submit: function (model) {
|
|
|
|
/* ensure an icon is selected, because on focus on close button
|
|
or an element in background no icon is submitted. So don't clear/update existing icon/preview.
|
|
*/
|
|
if (model.icon) {
|
|
|
|
if (model.color) {
|
|
scope.icon = model.icon + " " + model.color;
|
|
} else {
|
|
scope.icon = model.icon;
|
|
}
|
|
|
|
// set form to dirty
|
|
ctrl.$setDirty();
|
|
}
|
|
|
|
scope.dialogModel.show = false;
|
|
scope.dialogModel = null;
|
|
}
|
|
};
|
|
};
|
|
}
|
|
|
|
var directive = {
|
|
require: '^form',
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-editor-header.html',
|
|
scope: {
|
|
tabs: "=",
|
|
actions: "=",
|
|
name: "=",
|
|
nameLocked: "=",
|
|
menu: "=",
|
|
icon: "=",
|
|
hideIcon: "@",
|
|
alias: "=",
|
|
hideAlias: "@",
|
|
description: "=",
|
|
hideDescription: "@",
|
|
navigation: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorHeader', EditorHeaderDirective);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorMenuDirective($injector, treeService, navigationService, umbModelMapper, appState) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
//adds a handler to the context menu item click, we need to handle this differently
|
|
//depending on what the menu item is supposed to do.
|
|
scope.executeMenuItem = function (action) {
|
|
navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection);
|
|
};
|
|
|
|
//callback method to go and get the options async
|
|
scope.getOptions = function () {
|
|
|
|
if (!scope.currentNode) {
|
|
return;
|
|
}
|
|
|
|
//when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu)
|
|
appState.setMenuState("currentNode", scope.currentNode);
|
|
|
|
if (!scope.actions) {
|
|
treeService.getMenu({ treeNode: scope.currentNode })
|
|
.then(function (data) {
|
|
scope.actions = data.menuItems;
|
|
});
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-editor-menu.html',
|
|
link: link,
|
|
scope: {
|
|
currentNode: "=",
|
|
currentSection: "@"
|
|
}
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorMenu', EditorMenuDirective);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorNavigationDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.showNavigation = true;
|
|
|
|
scope.clickNavigationItem = function(selectedItem) {
|
|
setItemToActive(selectedItem);
|
|
runItemAction(selectedItem);
|
|
};
|
|
|
|
function runItemAction(selectedItem) {
|
|
if (selectedItem.action) {
|
|
selectedItem.action(selectedItem);
|
|
}
|
|
}
|
|
|
|
function setItemToActive(selectedItem) {
|
|
// set all other views to inactive
|
|
if (selectedItem.view) {
|
|
|
|
for (var index = 0; index < scope.navigation.length; index++) {
|
|
var item = scope.navigation[index];
|
|
item.active = false;
|
|
}
|
|
|
|
// set view to active
|
|
selectedItem.active = true;
|
|
|
|
}
|
|
}
|
|
|
|
function activate() {
|
|
|
|
// hide navigation if there is only 1 item
|
|
if (scope.navigation.length <= 1) {
|
|
scope.showNavigation = false;
|
|
}
|
|
|
|
}
|
|
|
|
activate();
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-editor-navigation.html',
|
|
scope: {
|
|
navigation: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives.html').directive('umbEditorNavigation', EditorNavigationDirective);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorSubViewsDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.activeView = {};
|
|
|
|
// set toolbar from selected navigation item
|
|
function setActiveView(items) {
|
|
|
|
for (var index = 0; index < items.length; index++) {
|
|
|
|
var item = items[index];
|
|
|
|
if (item.active && item.view) {
|
|
scope.activeView = item;
|
|
}
|
|
}
|
|
}
|
|
|
|
// watch for navigation changes
|
|
scope.$watch('subViews', function(newValue, oldValue) {
|
|
if (newValue) {
|
|
setActiveView(newValue);
|
|
}
|
|
}, true);
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-editor-sub-views.html',
|
|
scope: {
|
|
subViews: "=",
|
|
model: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEditorView
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to construct the main editor window.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="MySection.Controller as vm">
|
|
|
|
<form name="mySectionForm" novalidate>
|
|
|
|
<umb-editor-view>
|
|
|
|
<umb-editor-header
|
|
name="vm.content.name"
|
|
hide-alias="true"
|
|
hide-description="true"
|
|
hide-icon="true">
|
|
</umb-editor-header>
|
|
|
|
<umb-editor-container>
|
|
// main content here
|
|
</umb-editor-container>
|
|
|
|
<umb-editor-footer>
|
|
// footer content here
|
|
</umb-editor-footer>
|
|
|
|
</umb-editor-view>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</pre>
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("MySection.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
|
|
<h3>Use in combination with</h3>
|
|
<ul>
|
|
<li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
|
|
<li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EditorViewDirective() {
|
|
|
|
function link(scope, el, attr) {
|
|
|
|
if(attr.footer) {
|
|
scope.footer = attr.footer;
|
|
}
|
|
|
|
}
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/editor/umb-editor-view.html',
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEditorView', EditorViewDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
* @description Utillity directives for key and field events
|
|
**/
|
|
angular.module('umbraco.directives')
|
|
|
|
.directive('onKeyup', function () {
|
|
return {
|
|
link: function (scope, elm, attrs) {
|
|
var f = function () {
|
|
scope.$apply(attrs.onKeyup);
|
|
};
|
|
elm.on("keyup", f);
|
|
scope.$on("$destroy", function(){ elm.off("keyup", f);} );
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('onKeydown', function () {
|
|
return {
|
|
link: function (scope, elm, attrs) {
|
|
var f = function () {
|
|
scope.$apply(attrs.onKeydown);
|
|
};
|
|
elm.on("keydown", f);
|
|
scope.$on("$destroy", function(){ elm.off("keydown", f);} );
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('onBlur', function () {
|
|
return {
|
|
link: function (scope, elm, attrs) {
|
|
var f = function () {
|
|
scope.$apply(attrs.onBlur);
|
|
};
|
|
elm.on("blur", f);
|
|
scope.$on("$destroy", function(){ elm.off("blur", f);} );
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('onFocus', function () {
|
|
return {
|
|
link: function (scope, elm, attrs) {
|
|
var f = function () {
|
|
scope.$apply(attrs.onFocus);
|
|
};
|
|
elm.on("focus", f);
|
|
scope.$on("$destroy", function(){ elm.off("focus", f);} );
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('onDragEnter', function () {
|
|
return {
|
|
link: function (scope, elm, attrs) {
|
|
var f = function () {
|
|
scope.$apply(attrs.onDragEnter);
|
|
};
|
|
elm.on("dragenter", f);
|
|
scope.$on("$destroy", function(){ elm.off("dragenter", f);} );
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('onDragLeave', function () {
|
|
return function (scope, elm, attrs) {
|
|
var f = function (event) {
|
|
var rect = this.getBoundingClientRect();
|
|
var getXY = function getCursorPosition(event) {
|
|
var x, y;
|
|
|
|
if (typeof event.clientX === 'undefined') {
|
|
// try touch screen
|
|
x = event.pageX + document.documentElement.scrollLeft;
|
|
y = event.pageY + document.documentElement.scrollTop;
|
|
} else {
|
|
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
|
|
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
|
|
}
|
|
|
|
return { x: x, y : y };
|
|
};
|
|
|
|
var e = getXY(event.originalEvent);
|
|
|
|
// Check the mouseEvent coordinates are outside of the rectangle
|
|
if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) {
|
|
scope.$apply(attrs.onDragLeave);
|
|
}
|
|
};
|
|
|
|
elm.on("dragleave", f);
|
|
scope.$on("$destroy", function(){ elm.off("dragleave", f);} );
|
|
};
|
|
})
|
|
|
|
.directive('onDragOver', function () {
|
|
return {
|
|
link: function (scope, elm, attrs) {
|
|
var f = function () {
|
|
scope.$apply(attrs.onDragOver);
|
|
};
|
|
elm.on("dragover", f);
|
|
scope.$on("$destroy", function(){ elm.off("dragover", f);} );
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('onDragStart', function () {
|
|
return {
|
|
link: function (scope, elm, attrs) {
|
|
var f = function () {
|
|
scope.$apply(attrs.onDragStart);
|
|
};
|
|
elm.on("dragstart", f);
|
|
scope.$on("$destroy", function(){ elm.off("dragstart", f);} );
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('onDragEnd', function () {
|
|
return {
|
|
link: function (scope, elm, attrs) {
|
|
var f = function () {
|
|
scope.$apply(attrs.onDragEnd);
|
|
};
|
|
elm.on("dragend", f);
|
|
scope.$on("$destroy", function(){ elm.off("dragend", f);} );
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('onDrop', function () {
|
|
return {
|
|
link: function (scope, elm, attrs) {
|
|
var f = function () {
|
|
scope.$apply(attrs.onDrop);
|
|
};
|
|
elm.on("drop", f);
|
|
scope.$on("$destroy", function(){ elm.off("drop", f);} );
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('onOutsideClick', function ($timeout) {
|
|
return function (scope, element, attrs) {
|
|
|
|
var eventBindings = [];
|
|
|
|
function oneTimeClick(event) {
|
|
var el = event.target.nodeName;
|
|
|
|
//ignore link and button clicks
|
|
var els = ["INPUT","A","BUTTON"];
|
|
if(els.indexOf(el) >= 0){return;}
|
|
|
|
// ignore children of links and buttons
|
|
// ignore clicks on new overlay
|
|
var parents = $(event.target).parents("a,button,.umb-overlay");
|
|
if(parents.length > 0){
|
|
return;
|
|
}
|
|
|
|
// ignore clicks on dialog from old dialog service
|
|
var oldDialog = $(el).parents("#old-dialog-service");
|
|
if (oldDialog.length === 1) {
|
|
return;
|
|
}
|
|
|
|
// ignore clicks in tinyMCE dropdown(floatpanel)
|
|
var floatpanel = $(el).parents(".mce-floatpanel");
|
|
if (floatpanel.length === 1) {
|
|
return;
|
|
}
|
|
|
|
//ignore clicks inside this element
|
|
if( $(element).has( $(event.target) ).length > 0 ){
|
|
return;
|
|
}
|
|
|
|
scope.$apply(attrs.onOutsideClick);
|
|
}
|
|
|
|
|
|
$timeout(function(){
|
|
|
|
if ("bindClickOn" in attrs) {
|
|
|
|
eventBindings.push(scope.$watch(function() {
|
|
return attrs.bindClickOn;
|
|
}, function(newValue) {
|
|
if (newValue === "true") {
|
|
$(document).on("click", oneTimeClick);
|
|
} else {
|
|
$(document).off("click", oneTimeClick);
|
|
}
|
|
}));
|
|
|
|
} else {
|
|
$(document).on("click", oneTimeClick);
|
|
}
|
|
|
|
scope.$on("$destroy", function() {
|
|
$(document).off("click", oneTimeClick);
|
|
|
|
// unbind watchers
|
|
for (var e in eventBindings) {
|
|
eventBindings[e]();
|
|
}
|
|
|
|
});
|
|
}); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution.
|
|
|
|
};
|
|
})
|
|
|
|
.directive('onRightClick',function(){
|
|
|
|
document.oncontextmenu = function (e) {
|
|
if(e.target.hasAttribute('on-right-click')) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
return false;
|
|
}
|
|
};
|
|
|
|
return function(scope,el,attrs){
|
|
el.on('contextmenu',function(e){
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
scope.$apply(attrs.onRightClick);
|
|
return false;
|
|
});
|
|
};
|
|
})
|
|
|
|
.directive('onDelayedMouseleave', function ($timeout, $parse) {
|
|
return {
|
|
|
|
restrict: 'A',
|
|
|
|
link: function (scope, element, attrs, ctrl) {
|
|
var active = false;
|
|
var fn = $parse(attrs.onDelayedMouseleave);
|
|
|
|
var leave_f = function(event) {
|
|
var callback = function() {
|
|
fn(scope, {$event:event});
|
|
};
|
|
|
|
active = false;
|
|
$timeout(function(){
|
|
if(active === false){
|
|
scope.$apply(callback);
|
|
}
|
|
}, 650);
|
|
};
|
|
|
|
var enter_f = function(event, args){
|
|
active = true;
|
|
};
|
|
|
|
|
|
element.on("mouseleave", leave_f);
|
|
element.on("mouseenter", enter_f);
|
|
|
|
//unsub events
|
|
scope.$on("$destroy", function(){
|
|
element.off("mouseleave", leave_f);
|
|
element.off("mouseenter", enter_f);
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
/*
|
|
|
|
http://vitalets.github.io/checklist-model/
|
|
<label ng-repeat="role in roles">
|
|
<input type="checkbox" checklist-model="user.roles" checklist-value="role.id"> {{role.text}}
|
|
</label>
|
|
*/
|
|
angular.module('umbraco.directives')
|
|
.directive('checklistModel', ['$parse', '$compile', function($parse, $compile) {
|
|
// contains
|
|
function contains(arr, item) {
|
|
if (angular.isArray(arr)) {
|
|
for (var i = 0; i < arr.length; i++) {
|
|
if (angular.equals(arr[i], item)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// add
|
|
function add(arr, item) {
|
|
arr = angular.isArray(arr) ? arr : [];
|
|
for (var i = 0; i < arr.length; i++) {
|
|
if (angular.equals(arr[i], item)) {
|
|
return arr;
|
|
}
|
|
}
|
|
arr.push(item);
|
|
return arr;
|
|
}
|
|
|
|
// remove
|
|
function remove(arr, item) {
|
|
if (angular.isArray(arr)) {
|
|
for (var i = 0; i < arr.length; i++) {
|
|
if (angular.equals(arr[i], item)) {
|
|
arr.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// http://stackoverflow.com/a/19228302/1458162
|
|
function postLinkFn(scope, elem, attrs) {
|
|
// compile with `ng-model` pointing to `checked`
|
|
$compile(elem)(scope);
|
|
|
|
// getter / setter for original model
|
|
var getter = $parse(attrs.checklistModel);
|
|
var setter = getter.assign;
|
|
|
|
// value added to list
|
|
var value = $parse(attrs.checklistValue)(scope.$parent);
|
|
|
|
// watch UI checked change
|
|
scope.$watch('checked', function(newValue, oldValue) {
|
|
if (newValue === oldValue) {
|
|
return;
|
|
}
|
|
var current = getter(scope.$parent);
|
|
if (newValue === true) {
|
|
setter(scope.$parent, add(current, value));
|
|
} else {
|
|
setter(scope.$parent, remove(current, value));
|
|
}
|
|
});
|
|
|
|
// watch original model change
|
|
scope.$parent.$watch(attrs.checklistModel, function(newArr, oldArr) {
|
|
scope.checked = contains(newArr, value);
|
|
}, true);
|
|
}
|
|
|
|
return {
|
|
restrict: 'A',
|
|
priority: 1000,
|
|
terminal: true,
|
|
scope: true,
|
|
compile: function(tElement, tAttrs) {
|
|
if (tElement[0].tagName !== 'INPUT' || !tElement.attr('type', 'checkbox')) {
|
|
throw 'checklist-model should be applied to `input[type="checkbox"]`.';
|
|
}
|
|
|
|
if (!tAttrs.checklistValue) {
|
|
throw 'You should provide `checklist-value`.';
|
|
}
|
|
|
|
// exclude recursion
|
|
tElement.removeAttr('checklist-model');
|
|
|
|
// local scope var storing individual checkbox model
|
|
tElement.attr('ng-model', 'checked');
|
|
|
|
return postLinkFn;
|
|
}
|
|
};
|
|
}]);
|
|
angular.module("umbraco.directives")
|
|
.directive("contenteditable", function() {
|
|
|
|
return {
|
|
require: "ngModel",
|
|
link: function(scope, element, attrs, ngModel) {
|
|
|
|
function read() {
|
|
ngModel.$setViewValue(element.html());
|
|
}
|
|
|
|
ngModel.$render = function() {
|
|
element.html(ngModel.$viewValue || "");
|
|
};
|
|
|
|
|
|
element.bind("focus", function(){
|
|
|
|
var range = document.createRange();
|
|
range.selectNodeContents(element[0]);
|
|
|
|
var sel = window.getSelection();
|
|
sel.removeAllRanges();
|
|
sel.addRange(range);
|
|
|
|
});
|
|
|
|
element.bind("blur keyup change", function() {
|
|
scope.$apply(read);
|
|
});
|
|
}
|
|
|
|
};
|
|
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:fixNumber
|
|
* @restrict A
|
|
* @description Used in conjunction with type='number' input fields to ensure that the bound value is converted to a number when using ng-model
|
|
* because normally it thinks it's a string and also validation doesn't work correctly due to an angular bug.
|
|
**/
|
|
function fixNumber($parse) {
|
|
return {
|
|
restrict: "A",
|
|
require: "ngModel",
|
|
|
|
link: function (scope, elem, attrs, ctrl) {
|
|
|
|
//parse ngModel onload
|
|
var modelVal = scope.$eval(attrs.ngModel);
|
|
if (modelVal) {
|
|
var asNum = parseFloat(modelVal, 10);
|
|
if (!isNaN(asNum)) {
|
|
$parse(attrs.ngModel).assign(scope, asNum);
|
|
}
|
|
}
|
|
|
|
//always return an int to the model
|
|
ctrl.$parsers.push(function (value) {
|
|
if (value === 0) {
|
|
return 0;
|
|
}
|
|
return parseFloat(value || '', 10);
|
|
});
|
|
|
|
//always try to format the model value as an int
|
|
ctrl.$formatters.push(function (value) {
|
|
if (angular.isString(value)) {
|
|
return parseFloat(value, 10);
|
|
}
|
|
return value;
|
|
});
|
|
|
|
//This fixes this angular issue:
|
|
//https://github.com/angular/angular.js/issues/2144
|
|
// which doesn't actually validate the number input properly since the model only changes when a real number is entered
|
|
// but the input box still allows non-numbers to be entered which do not validate (only via html5)
|
|
if (typeof elem.prop('validity') === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
elem.bind('input', function (e) {
|
|
var validity = elem.prop('validity');
|
|
scope.$apply(function () {
|
|
ctrl.$setValidity('number', !validity.badInput);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives').directive("fixNumber", fixNumber);
|
|
angular.module("umbraco.directives").directive('focusWhen', function ($timeout) {
|
|
return {
|
|
restrict: 'A',
|
|
link: function (scope, elm, attrs, ctrl) {
|
|
attrs.$observe("focusWhen", function (newValue) {
|
|
if (newValue === "true") {
|
|
$timeout(function () {
|
|
elm.focus();
|
|
});
|
|
}
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:hexBgColor
|
|
* @restrict A
|
|
* @description Used to set a hex background color on an element, this will detect valid hex and when it is valid it will set the color, otherwise
|
|
* a color will not be set.
|
|
**/
|
|
function hexBgColor() {
|
|
return {
|
|
restrict: "A",
|
|
link: function (scope, element, attr, formCtrl) {
|
|
|
|
var origColor = null;
|
|
if (attr.hexBgOrig) {
|
|
//set the orig based on the attribute if there is one
|
|
origColor = attr.hexBgOrig;
|
|
}
|
|
|
|
attr.$observe("hexBgColor", function (newVal) {
|
|
if (newVal) {
|
|
if (!origColor) {
|
|
//get the orig color before changing it
|
|
origColor = element.css("border-color");
|
|
}
|
|
//validate it - test with and without the leading hash.
|
|
if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) {
|
|
element.css("background-color", "#" + newVal);
|
|
return;
|
|
}
|
|
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) {
|
|
element.css("background-color", newVal);
|
|
return;
|
|
}
|
|
}
|
|
element.css("background-color", origColor);
|
|
});
|
|
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives').directive("hexBgColor", hexBgColor);
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:hotkey
|
|
**/
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('hotkey', function($window, keyboardService, $log) {
|
|
|
|
return function(scope, el, attrs) {
|
|
|
|
var options = {};
|
|
var keyCombo = attrs.hotkey;
|
|
|
|
if (!keyCombo) {
|
|
//support data binding
|
|
keyCombo = scope.$eval(attrs["hotkey"]);
|
|
}
|
|
|
|
function activate() {
|
|
|
|
if (keyCombo) {
|
|
|
|
// disable shortcuts in input fields if keycombo is 1 character
|
|
if (keyCombo.length === 1) {
|
|
options = {
|
|
inputDisabled: true
|
|
};
|
|
}
|
|
|
|
keyboardService.bind(keyCombo, function() {
|
|
|
|
var element = $(el);
|
|
var activeElementType = document.activeElement.tagName;
|
|
var clickableElements = ["A", "BUTTON"];
|
|
|
|
if (element.is("a,div,button,input[type='button'],input[type='submit'],input[type='checkbox']") && !element.is(':disabled')) {
|
|
|
|
if (element.is(':visible') || attrs.hotkeyWhenHidden) {
|
|
|
|
if (attrs.hotkeyWhen && attrs.hotkeyWhen === "false") {
|
|
return;
|
|
}
|
|
|
|
// when keycombo is enter and a link or button has focus - click the link or button instead of using the hotkey
|
|
if (keyCombo === "enter" && clickableElements.indexOf(activeElementType) === 0) {
|
|
document.activeElement.click();
|
|
} else {
|
|
element.click();
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
element.focus();
|
|
}
|
|
|
|
}, options);
|
|
|
|
el.on('$destroy', function() {
|
|
keyboardService.unbind(keyCombo);
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
activate();
|
|
|
|
};
|
|
});
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:preventDefault
|
|
|
|
@description
|
|
Use this directive to prevent default action of an element. Effectively implementing <a href="https://api.jquery.com/event.preventdefault/">jQuery's preventdefault</a>
|
|
|
|
<h3>Markup example</h3>
|
|
|
|
<pre>
|
|
<a href="https://umbraco.com" prevent-default>Don't go to Umbraco.com</a>
|
|
</pre>
|
|
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('preventDefault', function() {
|
|
return function(scope, element, attrs) {
|
|
|
|
var enabled = true;
|
|
//check if there's a value for the attribute, if there is and it's false then we conditionally don't
|
|
//prevent default.
|
|
if (attrs.preventDefault) {
|
|
attrs.$observe("preventDefault", function (newVal) {
|
|
enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true;
|
|
});
|
|
}
|
|
|
|
$(element).click(function (event) {
|
|
if (event.metaKey || event.ctrlKey) {
|
|
return;
|
|
}
|
|
else {
|
|
if (enabled === true) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:preventEnterSubmit
|
|
* @description prevents a form from submitting when the enter key is pressed on an input field
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('preventEnterSubmit', function() {
|
|
return function(scope, element, attrs) {
|
|
|
|
var enabled = true;
|
|
//check if there's a value for the attribute, if there is and it's false then we conditionally don't
|
|
//prevent default.
|
|
if (attrs.preventEnterSubmit) {
|
|
attrs.$observe("preventEnterSubmit", function (newVal) {
|
|
enabled = (newVal === "false" || newVal === 0 || newVal === false) ? false : true;
|
|
});
|
|
}
|
|
|
|
$(element).keypress(function (event) {
|
|
if (event.which === 13) {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
};
|
|
});
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:resizeToContent
|
|
* @element div
|
|
* @function
|
|
*
|
|
* @description
|
|
* Resize iframe's automatically to fit to the content they contain
|
|
*
|
|
* @example
|
|
<example module="umbraco.directives">
|
|
<file name="index.html">
|
|
<iframe resize-to-content src="meh.html"></iframe>
|
|
</file>
|
|
</example>
|
|
*/
|
|
angular.module("umbraco.directives")
|
|
.directive('resizeToContent', function ($window, $timeout) {
|
|
return function (scope, el, attrs) {
|
|
var iframe = el[0];
|
|
var iframeWin = iframe.contentWindow || iframe.contentDocument.parentWindow;
|
|
if (iframeWin.document.body) {
|
|
|
|
$timeout(function(){
|
|
var height = iframeWin.document.documentElement.scrollHeight || iframeWin.document.body.scrollHeight;
|
|
el.height(height);
|
|
}, 3000);
|
|
}
|
|
};
|
|
});
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('selectOnFocus', function () {
|
|
return function (scope, el, attrs) {
|
|
$(el).bind("click", function () {
|
|
var editmode = $(el).data("editmode");
|
|
//If editmode is true a click is handled like a normal click
|
|
if (!editmode) {
|
|
//Initial click, select entire text
|
|
this.select();
|
|
//Set the edit mode so subsequent clicks work normally
|
|
$(el).data("editmode", true);
|
|
}
|
|
}).
|
|
bind("blur", function () {
|
|
//Reset on focus lost
|
|
$(el).data("editmode", false);
|
|
});
|
|
};
|
|
});
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('umbAutoFocus', function($timeout) {
|
|
|
|
return function(scope, element, attr){
|
|
var update = function() {
|
|
//if it uses its default naming
|
|
if(element.val() === "" || attr.focusOnFilled){
|
|
element.focus();
|
|
}
|
|
};
|
|
|
|
$timeout(function() {
|
|
update();
|
|
});
|
|
};
|
|
});
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('umbAutoResize', function($timeout) {
|
|
return {
|
|
require: ["^?umbTabs", "ngModel"],
|
|
link: function(scope, element, attr, controllersArr) {
|
|
|
|
var domEl = element[0];
|
|
var domElType = domEl.type;
|
|
var umbTabsController = controllersArr[0];
|
|
var ngModelController = controllersArr[1];
|
|
|
|
// IE elements
|
|
var isIEFlag = false;
|
|
var wrapper = angular.element('#umb-ie-resize-input-wrapper');
|
|
var mirror = angular.element('<span style="white-space:pre;"></span>');
|
|
|
|
function isIE() {
|
|
|
|
var ua = window.navigator.userAgent;
|
|
var msie = ua.indexOf("MSIE ");
|
|
|
|
if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./) || navigator.userAgent.match(/Edge\/\d+/)) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
function activate() {
|
|
|
|
// check if browser is Internet Explorere
|
|
isIEFlag = isIE();
|
|
|
|
// scrollWidth on element does not work in IE on inputs
|
|
// we have to do some dirty dom element copying.
|
|
if (isIEFlag === true && domElType === "text") {
|
|
setupInternetExplorerElements();
|
|
}
|
|
|
|
}
|
|
|
|
function setupInternetExplorerElements() {
|
|
|
|
if (!wrapper.length) {
|
|
wrapper = angular.element('<div id="umb-ie-resize-input-wrapper" style="position:fixed; top:-999px; left:0;"></div>');
|
|
angular.element('body').append(wrapper);
|
|
}
|
|
|
|
angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle',
|
|
'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent',
|
|
'boxSizing', 'borderRightWidth', 'borderLeftWidth', 'borderLeftStyle', 'borderRightStyle',
|
|
'paddingLeft', 'paddingRight', 'marginLeft', 'marginRight'
|
|
], function(value) {
|
|
mirror.css(value, element.css(value));
|
|
});
|
|
|
|
wrapper.append(mirror);
|
|
|
|
}
|
|
|
|
function resizeInternetExplorerInput() {
|
|
|
|
mirror.text(element.val() || attr.placeholder);
|
|
element.css('width', mirror.outerWidth() + 1);
|
|
|
|
}
|
|
|
|
function resizeInput() {
|
|
|
|
if (domEl.scrollWidth !== domEl.clientWidth) {
|
|
if (ngModelController.$modelValue) {
|
|
element.width(domEl.scrollWidth);
|
|
}
|
|
}
|
|
|
|
if(!ngModelController.$modelValue && attr.placeholder) {
|
|
attr.$set('size', attr.placeholder.length);
|
|
element.width('auto');
|
|
}
|
|
|
|
}
|
|
|
|
function resizeTextarea() {
|
|
|
|
if(domEl.scrollHeight !== domEl.clientHeight) {
|
|
|
|
element.height(domEl.scrollHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var update = function(force) {
|
|
|
|
|
|
if (force === true) {
|
|
|
|
if (domElType === "textarea") {
|
|
element.height(0);
|
|
} else if (domElType === "text") {
|
|
element.width(0);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
if (isIEFlag === true && domElType === "text") {
|
|
|
|
resizeInternetExplorerInput();
|
|
|
|
} else {
|
|
|
|
if (domElType === "textarea") {
|
|
|
|
resizeTextarea();
|
|
|
|
} else if (domElType === "text") {
|
|
|
|
resizeInput();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
activate();
|
|
|
|
//listen for tab changes
|
|
if (umbTabsController != null) {
|
|
umbTabsController.onTabShown(function(args) {
|
|
update();
|
|
});
|
|
}
|
|
|
|
// listen for ng-model changes
|
|
var unbindModelWatcher = scope.$watch(function() {
|
|
return ngModelController.$modelValue;
|
|
}, function(newValue) {
|
|
update(true);
|
|
});
|
|
|
|
scope.$on('$destroy', function() {
|
|
element.unbind('keyup keydown keypress change', update);
|
|
element.unbind('blur', update(true));
|
|
unbindModelWatcher();
|
|
|
|
// clean up IE dom element
|
|
if (isIEFlag === true && domElType === "text") {
|
|
mirror.remove();
|
|
}
|
|
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
/*
|
|
example usage: <textarea json-edit="myObject" rows="8" class="form-control"></textarea>
|
|
|
|
jsonEditing is a string which we edit in a textarea. we try parsing to JSON with each change. when it is valid, propagate model changes via ngModelCtrl
|
|
|
|
use isolate scope to prevent model propagation when invalid - will update manually. cannot replace with template, or will override ngModelCtrl, and not hide behind facade
|
|
|
|
will override element type to textarea and add own attribute ngModel tied to jsonEditing
|
|
*/
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('umbRawModel', function () {
|
|
return {
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
template: '<textarea ng-model="jsonEditing"></textarea>',
|
|
replace : true,
|
|
scope: {
|
|
model: '=umbRawModel',
|
|
validateOn:'='
|
|
},
|
|
link: function (scope, element, attrs, ngModelCtrl) {
|
|
|
|
function setEditing (value) {
|
|
scope.jsonEditing = angular.copy( jsonToString(value));
|
|
}
|
|
|
|
function updateModel (value) {
|
|
scope.model = stringToJson(value);
|
|
}
|
|
|
|
function setValid() {
|
|
ngModelCtrl.$setValidity('json', true);
|
|
}
|
|
|
|
function setInvalid () {
|
|
ngModelCtrl.$setValidity('json', false);
|
|
}
|
|
|
|
function stringToJson(text) {
|
|
try {
|
|
return angular.fromJson(text);
|
|
} catch (err) {
|
|
setInvalid();
|
|
return text;
|
|
}
|
|
}
|
|
|
|
function jsonToString(object) {
|
|
// better than JSON.stringify(), because it formats + filters $$hashKey etc.
|
|
// NOTE that this will remove all $-prefixed values
|
|
return angular.toJson(object, true);
|
|
}
|
|
|
|
function isValidJson(model) {
|
|
var flag = true;
|
|
try {
|
|
angular.fromJson(model);
|
|
} catch (err) {
|
|
flag = false;
|
|
}
|
|
return flag;
|
|
}
|
|
|
|
//init
|
|
setEditing(scope.model);
|
|
|
|
var onInputChange = function(newval,oldval){
|
|
if (newval !== oldval) {
|
|
if (isValidJson(newval)) {
|
|
setValid();
|
|
updateModel(newval);
|
|
} else {
|
|
setInvalid();
|
|
}
|
|
}
|
|
};
|
|
|
|
if(scope.validateOn){
|
|
element.on(scope.validateOn, function(){
|
|
scope.$apply(function(){
|
|
onInputChange(scope.jsonEditing);
|
|
});
|
|
});
|
|
}else{
|
|
//check for changes going out
|
|
scope.$watch('jsonEditing', onInputChange, true);
|
|
}
|
|
|
|
//check for changes coming in
|
|
scope.$watch('model', function (newval, oldval) {
|
|
if (newval !== oldval) {
|
|
setEditing(newval);
|
|
}
|
|
}, true);
|
|
|
|
}
|
|
};
|
|
});
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function SelectWhen($timeout) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
attr.$observe("umbSelectWhen", function(newValue) {
|
|
if (newValue === "true") {
|
|
$timeout(function() {
|
|
el.select();
|
|
});
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'A',
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbSelectWhen', SelectWhen);
|
|
|
|
})();
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('gridRte', function (tinyMceService, stylesheetResource, angularHelper, assetsService, $q, $timeout) {
|
|
return {
|
|
scope: {
|
|
uniqueId: '=',
|
|
value: '=',
|
|
onClick: '&',
|
|
onFocus: '&',
|
|
onBlur: '&',
|
|
configuration:"=",
|
|
onMediaPickerClick: "=",
|
|
onEmbedClick: "=",
|
|
onMacroPickerClick: "=",
|
|
onLinkPickerClick: "="
|
|
},
|
|
template: "<textarea ng-model=\"value\" rows=\"10\" class=\"mceNoEditor\" style=\"overflow:hidden\" id=\"{{uniqueId}}\"></textarea>",
|
|
replace: true,
|
|
link: function (scope, element, attrs) {
|
|
|
|
var initTiny = function () {
|
|
|
|
//we always fetch the default one, and then override parts with our own
|
|
tinyMceService.configuration().then(function (tinyMceConfig) {
|
|
|
|
|
|
|
|
//config value from general tinymce.config file
|
|
var validElements = tinyMceConfig.validElements;
|
|
var fallbackStyles = [{title: "Page header", block: "h2"}, {title: "Section header", block: "h3"}, {title: "Paragraph header", block: "h4"}, {title: "Normal", block: "p"}, {title: "Quote", block: "blockquote"}, {title: "Code", block: "code"}];
|
|
|
|
//These are absolutely required in order for the macros to render inline
|
|
//we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce
|
|
var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]";
|
|
|
|
var invalidElements = tinyMceConfig.inValidElements;
|
|
var plugins = _.map(tinyMceConfig.plugins, function (plugin) {
|
|
if (plugin.useOnFrontend) {
|
|
return plugin.name;
|
|
}
|
|
}).join(" ") + " autoresize";
|
|
|
|
//config value on the data type
|
|
var toolbar = ["code", "styleselect", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "link", "umbmediapicker", "umbembeddialog"].join(" | ");
|
|
var stylesheets = [];
|
|
|
|
var styleFormats = [];
|
|
var await = [];
|
|
|
|
//queue file loading
|
|
if (typeof (tinymce) === "undefined") {
|
|
await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", scope));
|
|
}
|
|
|
|
|
|
if(scope.configuration && scope.configuration.toolbar){
|
|
toolbar = scope.configuration.toolbar.join(' | ');
|
|
}
|
|
|
|
|
|
if(scope.configuration && scope.configuration.stylesheets){
|
|
angular.forEach(scope.configuration.stylesheets, function(stylesheet, key){
|
|
|
|
stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + stylesheet + ".css");
|
|
await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) {
|
|
angular.forEach(rules, function (rule) {
|
|
var r = {};
|
|
var split = "";
|
|
r.title = rule.name;
|
|
if (rule.selector[0] === ".") {
|
|
r.inline = "span";
|
|
r.classes = rule.selector.substring(1);
|
|
}else if (rule.selector[0] === "#") {
|
|
//Even though this will render in the style drop down, it will not actually be applied
|
|
// to the elements, don't think TinyMCE even supports this and it doesn't really make much sense
|
|
// since only one element can have one id.
|
|
r.inline = "span";
|
|
r.attributes = { id: rule.selector.substring(1) };
|
|
}else if (rule.selector[0] !== "." && rule.selector.indexOf(".") > -1) {
|
|
split = rule.selector.split(".");
|
|
r.block = split[0];
|
|
r.classes = rule.selector.substring(rule.selector.indexOf(".") + 1).replace(".", " ");
|
|
}else if (rule.selector[0] !== "#" && rule.selector.indexOf("#") > -1) {
|
|
split = rule.selector.split("#");
|
|
r.block = split[0];
|
|
r.classes = rule.selector.substring(rule.selector.indexOf("#") + 1);
|
|
}else {
|
|
r.block = rule.selector;
|
|
}
|
|
styleFormats.push(r);
|
|
});
|
|
}));
|
|
});
|
|
}else{
|
|
stylesheets.push("views/propertyeditors/grid/config/grid.default.rtestyles.css");
|
|
styleFormats = fallbackStyles;
|
|
}
|
|
|
|
//stores a reference to the editor
|
|
var tinyMceEditor = null;
|
|
$q.all(await).then(function () {
|
|
|
|
var uniqueId = scope.uniqueId;
|
|
|
|
//create a baseline Config to exten upon
|
|
var baseLineConfigObj = {
|
|
mode: "exact",
|
|
skin: "umbraco",
|
|
plugins: plugins,
|
|
valid_elements: validElements,
|
|
invalid_elements: invalidElements,
|
|
extended_valid_elements: extendedValidElements,
|
|
menubar: false,
|
|
statusbar: false,
|
|
relative_urls: false,
|
|
toolbar: toolbar,
|
|
content_css: stylesheets,
|
|
style_formats: styleFormats,
|
|
autoresize_bottom_margin: 0
|
|
};
|
|
|
|
|
|
if (tinyMceConfig.customConfig) {
|
|
|
|
//if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to
|
|
// convert it to json instead of having it as a string since this is what tinymce requires
|
|
for (var i in tinyMceConfig.customConfig) {
|
|
var val = tinyMceConfig.customConfig[i];
|
|
if (val) {
|
|
val = val.toString().trim();
|
|
if (val.detectIsJson()) {
|
|
try {
|
|
tinyMceConfig.customConfig[i] = JSON.parse(val);
|
|
//now we need to check if this custom config key is defined in our baseline, if it is we don't want to
|
|
//overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise
|
|
//if it's an object it will overwrite the baseline
|
|
if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) {
|
|
//concat it and below this concat'd array will overwrite the baseline in angular.extend
|
|
tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]);
|
|
}
|
|
}
|
|
catch (e) {
|
|
//cannot parse, we'll just leave it
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
angular.extend(baseLineConfigObj, tinyMceConfig.customConfig);
|
|
}
|
|
|
|
//set all the things that user configs should not be able to override
|
|
baseLineConfigObj.elements = uniqueId;
|
|
baseLineConfigObj.setup = function (editor) {
|
|
|
|
//set the reference
|
|
tinyMceEditor = editor;
|
|
|
|
|
|
//enable browser based spell checking
|
|
editor.on('init', function (e) {
|
|
|
|
editor.getBody().setAttribute('spellcheck', true);
|
|
|
|
//force overflow to hidden to prevent no needed scroll
|
|
editor.getBody().style.overflow = "hidden";
|
|
|
|
$timeout(function(){
|
|
if(scope.value === null){
|
|
editor.focus();
|
|
}
|
|
}, 400);
|
|
|
|
});
|
|
|
|
//when we leave the editor (maybe)
|
|
editor.on('blur', function (e) {
|
|
editor.save();
|
|
angularHelper.safeApply(scope, function () {
|
|
scope.value = editor.getContent();
|
|
|
|
var _toolbar = $(editor.editorContainer)
|
|
.find(".mce-toolbar");
|
|
|
|
if(scope.onBlur){
|
|
scope.onBlur();
|
|
}
|
|
|
|
});
|
|
});
|
|
|
|
// Focus on editor
|
|
editor.on('focus', function (e) {
|
|
angularHelper.safeApply(scope, function () {
|
|
|
|
if(scope.onFocus){
|
|
scope.onFocus();
|
|
}
|
|
|
|
});
|
|
});
|
|
|
|
// Click on editor
|
|
editor.on('click', function (e) {
|
|
angularHelper.safeApply(scope, function () {
|
|
|
|
if(scope.onClick){
|
|
scope.onClick();
|
|
}
|
|
|
|
});
|
|
});
|
|
|
|
//when buttons modify content
|
|
editor.on('ExecCommand', function (e) {
|
|
editor.save();
|
|
angularHelper.safeApply(scope, function () {
|
|
scope.value = editor.getContent();
|
|
});
|
|
});
|
|
|
|
// Update model on keypress
|
|
editor.on('KeyUp', function (e) {
|
|
editor.save();
|
|
angularHelper.safeApply(scope, function () {
|
|
scope.value = editor.getContent();
|
|
});
|
|
});
|
|
|
|
// Update model on change, i.e. copy/pasted text, plugins altering content
|
|
editor.on('SetContent', function (e) {
|
|
if (!e.initial) {
|
|
editor.save();
|
|
angularHelper.safeApply(scope, function () {
|
|
scope.value = editor.getContent();
|
|
});
|
|
}
|
|
});
|
|
|
|
editor.on('ObjectResized', function (e) {
|
|
var qs = "?width=" + e.width + "&height=" + e.height;
|
|
var srcAttr = $(e.target).attr("src");
|
|
var path = srcAttr.split("?")[0];
|
|
$(e.target).attr("data-mce-src", path + qs);
|
|
});
|
|
|
|
//Create the insert link plugin
|
|
tinyMceService.createLinkPicker(editor, scope, function(currentTarget, anchorElement){
|
|
if(scope.onLinkPickerClick) {
|
|
scope.onLinkPickerClick(editor, currentTarget, anchorElement);
|
|
}
|
|
});
|
|
|
|
//Create the insert media plugin
|
|
tinyMceService.createMediaPicker(editor, scope, function(currentTarget, userData){
|
|
if(scope.onMediaPickerClick) {
|
|
scope.onMediaPickerClick(editor, currentTarget, userData);
|
|
}
|
|
});
|
|
|
|
//Create the embedded plugin
|
|
tinyMceService.createInsertEmbeddedMedia(editor, scope, function(){
|
|
if(scope.onEmbedClick) {
|
|
scope.onEmbedClick(editor);
|
|
}
|
|
});
|
|
|
|
//Create the insert macro plugin
|
|
tinyMceService.createInsertMacro(editor, scope, function(dialogData){
|
|
if(scope.onMacroPickerClick) {
|
|
scope.onMacroPickerClick(editor, dialogData);
|
|
}
|
|
});
|
|
|
|
};
|
|
|
|
/** Loads in the editor */
|
|
function loadTinyMce() {
|
|
|
|
//we need to add a timeout here, to force a redraw so TinyMCE can find
|
|
//the elements needed
|
|
$timeout(function () {
|
|
tinymce.DOM.events.domLoaded = true;
|
|
tinymce.init(baseLineConfigObj);
|
|
}, 150, false);
|
|
}
|
|
|
|
loadTinyMce();
|
|
|
|
//here we declare a special method which will be called whenever the value has changed from the server
|
|
//this is instead of doing a watch on the model.value = faster
|
|
//scope.model.onValueChanged = function (newVal, oldVal) {
|
|
// //update the display val again if it has changed from the server;
|
|
// tinyMceEditor.setContent(newVal, { format: 'raw' });
|
|
// //we need to manually fire this event since it is only ever fired based on loading from the DOM, this
|
|
// // is required for our plugins listening to this event to execute
|
|
// tinyMceEditor.fire('LoadContent', null);
|
|
//};
|
|
|
|
//listen for formSubmitting event (the result is callback used to remove the event subscription)
|
|
var unsubscribe = scope.$on("formSubmitting", function () {
|
|
//TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer
|
|
// we do parse it out on the server side but would be nice to do that on the client side before as well.
|
|
scope.value = tinyMceEditor.getContent();
|
|
});
|
|
|
|
//when the element is disposed we need to unsubscribe!
|
|
// NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom
|
|
// element might still be there even after the modal has been hidden.
|
|
scope.$on('$destroy', function () {
|
|
unsubscribe();
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
initTiny();
|
|
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbControlGroup
|
|
* @restrict E
|
|
**/
|
|
angular.module("umbraco.directives.html")
|
|
.directive('umbControlGroup', function (localizationService) {
|
|
return {
|
|
scope: {
|
|
label: "@label",
|
|
description: "@",
|
|
hideLabel: "@",
|
|
alias: "@"
|
|
},
|
|
require: '?^form',
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/html/umb-control-group.html',
|
|
link: function (scope, element, attr, formCtrl) {
|
|
|
|
scope.formValid = function() {
|
|
if (formCtrl) {
|
|
return formCtrl.$valid;
|
|
}
|
|
//there is no form.
|
|
return true;
|
|
};
|
|
|
|
if (scope.label && scope.label[0] === "@") {
|
|
scope.labelstring = localizationService.localize(scope.label.substring(1));
|
|
}
|
|
else {
|
|
scope.labelstring = scope.label;
|
|
}
|
|
|
|
if (scope.description && scope.description[0] === "@") {
|
|
scope.descriptionstring = localizationService.localize(scope.description.substring(1));
|
|
}
|
|
else {
|
|
scope.descriptionstring = scope.description;
|
|
}
|
|
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbPane
|
|
* @restrict E
|
|
**/
|
|
angular.module("umbraco.directives.html")
|
|
.directive('umbPane', function () {
|
|
return {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/html/umb-pane.html'
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbPanel
|
|
* @restrict E
|
|
**/
|
|
angular.module("umbraco.directives.html")
|
|
.directive('umbPanel', function($timeout, $log){
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
transclude: 'true',
|
|
templateUrl: 'views/components/html/umb-panel.html'
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbImageCrop
|
|
* @restrict E
|
|
* @function
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('umbImageCrop',
|
|
function ($timeout, localizationService, cropperHelper, $log) {
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/imaging/umb-image-crop.html',
|
|
scope: {
|
|
src: '=',
|
|
width: '@',
|
|
height: '@',
|
|
crop: "=",
|
|
center: "=",
|
|
maxSize: '@'
|
|
},
|
|
|
|
link: function(scope, element, attrs) {
|
|
scope.width = 400;
|
|
scope.height = 320;
|
|
|
|
scope.dimensions = {
|
|
image: {},
|
|
cropper:{},
|
|
viewport:{},
|
|
margin: 20,
|
|
scale: {
|
|
min: 0.3,
|
|
max: 3,
|
|
current: 1
|
|
}
|
|
};
|
|
|
|
|
|
//live rendering of viewport and image styles
|
|
scope.style = function () {
|
|
return {
|
|
'height': (parseInt(scope.dimensions.viewport.height, 10)) + 'px',
|
|
'width': (parseInt(scope.dimensions.viewport.width, 10)) + 'px'
|
|
};
|
|
};
|
|
|
|
|
|
//elements
|
|
var $viewport = element.find(".viewport");
|
|
var $image = element.find("img");
|
|
var $overlay = element.find(".overlay");
|
|
var $container = element.find(".crop-container");
|
|
|
|
//default constraints for drag n drop
|
|
var constraints = {left: {max: scope.dimensions.margin, min: scope.dimensions.margin}, top: {max: scope.dimensions.margin, min: scope.dimensions.margin}, };
|
|
scope.constraints = constraints;
|
|
|
|
|
|
//set constaints for cropping drag and drop
|
|
var setConstraints = function(){
|
|
constraints.left.min = scope.dimensions.margin + scope.dimensions.cropper.width - scope.dimensions.image.width;
|
|
constraints.top.min = scope.dimensions.margin + scope.dimensions.cropper.height - scope.dimensions.image.height;
|
|
};
|
|
|
|
|
|
var setDimensions = function(originalImage){
|
|
originalImage.width("auto");
|
|
originalImage.height("auto");
|
|
|
|
var image = {};
|
|
image.originalWidth = originalImage.width();
|
|
image.originalHeight = originalImage.height();
|
|
|
|
image.width = image.originalWidth;
|
|
image.height = image.originalHeight;
|
|
image.left = originalImage[0].offsetLeft;
|
|
image.top = originalImage[0].offsetTop;
|
|
|
|
scope.dimensions.image = image;
|
|
|
|
//unscaled editor size
|
|
//var viewPortW = $viewport.width();
|
|
//var viewPortH = $viewport.height();
|
|
var _viewPortW = parseInt(scope.width, 10);
|
|
var _viewPortH = parseInt(scope.height, 10);
|
|
|
|
//if we set a constraint we will scale it down if needed
|
|
if(scope.maxSize){
|
|
var ratioCalculation = cropperHelper.scaleToMaxSize(
|
|
_viewPortW,
|
|
_viewPortH,
|
|
scope.maxSize);
|
|
|
|
//so if we have a max size, override the thumb sizes
|
|
_viewPortW = ratioCalculation.width;
|
|
_viewPortH = ratioCalculation.height;
|
|
}
|
|
|
|
scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin;
|
|
scope.dimensions.viewport.height = _viewPortH + 2 * scope.dimensions.margin;
|
|
scope.dimensions.cropper.width = _viewPortW; // scope.dimensions.viewport.width - 2 * scope.dimensions.margin;
|
|
scope.dimensions.cropper.height = _viewPortH; // scope.dimensions.viewport.height - 2 * scope.dimensions.margin;
|
|
};
|
|
|
|
|
|
//when loading an image without any crop info, we center and fit it
|
|
var resizeImageToEditor = function(){
|
|
//returns size fitting the cropper
|
|
var size = cropperHelper.calculateAspectRatioFit(
|
|
scope.dimensions.image.width,
|
|
scope.dimensions.image.height,
|
|
scope.dimensions.cropper.width,
|
|
scope.dimensions.cropper.height,
|
|
true);
|
|
|
|
//sets the image size and updates the scope
|
|
scope.dimensions.image.width = size.width;
|
|
scope.dimensions.image.height = size.height;
|
|
|
|
//calculate the best suited ratios
|
|
scope.dimensions.scale.min = size.ratio;
|
|
scope.dimensions.scale.max = 2;
|
|
scope.dimensions.scale.current = size.ratio;
|
|
|
|
//center the image
|
|
var position = cropperHelper.centerInsideViewPort(scope.dimensions.image, scope.dimensions.cropper);
|
|
scope.dimensions.top = position.top;
|
|
scope.dimensions.left = position.left;
|
|
|
|
setConstraints();
|
|
};
|
|
|
|
//resize to a given ratio
|
|
var resizeImageToScale = function(ratio){
|
|
//do stuff
|
|
var size = cropperHelper.calculateSizeToRatio(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, ratio);
|
|
scope.dimensions.image.width = size.width;
|
|
scope.dimensions.image.height = size.height;
|
|
|
|
setConstraints();
|
|
validatePosition(scope.dimensions.image.left, scope.dimensions.image.top);
|
|
};
|
|
|
|
//resize the image to a predefined crop coordinate
|
|
var resizeImageToCrop = function(){
|
|
scope.dimensions.image = cropperHelper.convertToStyle(
|
|
scope.crop,
|
|
{width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight},
|
|
scope.dimensions.cropper,
|
|
scope.dimensions.margin);
|
|
|
|
var ratioCalculation = cropperHelper.calculateAspectRatioFit(
|
|
scope.dimensions.image.originalWidth,
|
|
scope.dimensions.image.originalHeight,
|
|
scope.dimensions.cropper.width,
|
|
scope.dimensions.cropper.height,
|
|
true);
|
|
|
|
scope.dimensions.scale.current = scope.dimensions.image.ratio;
|
|
|
|
//min max based on original width/height
|
|
scope.dimensions.scale.min = ratioCalculation.ratio;
|
|
scope.dimensions.scale.max = 2;
|
|
};
|
|
|
|
|
|
|
|
var validatePosition = function(left, top){
|
|
if(left > constraints.left.max)
|
|
{
|
|
left = constraints.left.max;
|
|
}
|
|
|
|
if(left <= constraints.left.min){
|
|
left = constraints.left.min;
|
|
}
|
|
|
|
if(top > constraints.top.max)
|
|
{
|
|
top = constraints.top.max;
|
|
}
|
|
if(top <= constraints.top.min){
|
|
top = constraints.top.min;
|
|
}
|
|
|
|
if(scope.dimensions.image.left !== left){
|
|
scope.dimensions.image.left = left;
|
|
}
|
|
|
|
if(scope.dimensions.image.top !== top){
|
|
scope.dimensions.image.top = top;
|
|
}
|
|
};
|
|
|
|
|
|
//sets scope.crop to the recalculated % based crop
|
|
var calculateCropBox = function(){
|
|
scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin);
|
|
};
|
|
|
|
|
|
//Drag and drop positioning, using jquery ui draggable
|
|
var onStartDragPosition, top, left;
|
|
$overlay.draggable({
|
|
drag: function(event, ui) {
|
|
scope.$apply(function(){
|
|
validatePosition(ui.position.left, ui.position.top);
|
|
});
|
|
},
|
|
stop: function(event, ui){
|
|
scope.$apply(function(){
|
|
//make sure that every validates one more time...
|
|
validatePosition(ui.position.left, ui.position.top);
|
|
|
|
calculateCropBox();
|
|
scope.dimensions.image.rnd = Math.random();
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
|
|
var init = function(image){
|
|
scope.loaded = false;
|
|
|
|
//set dimensions on image, viewport, cropper etc
|
|
setDimensions(image);
|
|
|
|
//if we have a crop already position the image
|
|
if(scope.crop){
|
|
resizeImageToCrop();
|
|
}else{
|
|
resizeImageToEditor();
|
|
}
|
|
|
|
//sets constaints for the cropper
|
|
setConstraints();
|
|
scope.loaded = true;
|
|
};
|
|
|
|
|
|
/// WATCHERS ////
|
|
scope.$watchCollection('[width, height]', function(newValues, oldValues){
|
|
//we have to reinit the whole thing if
|
|
//one of the external params changes
|
|
if(newValues !== oldValues){
|
|
setDimensions($image);
|
|
setConstraints();
|
|
}
|
|
});
|
|
|
|
var throttledResizing = _.throttle(function(){
|
|
resizeImageToScale(scope.dimensions.scale.current);
|
|
calculateCropBox();
|
|
}, 100);
|
|
|
|
|
|
//happens when we change the scale
|
|
scope.$watch("dimensions.scale.current", function(){
|
|
if(scope.loaded){
|
|
throttledResizing();
|
|
}
|
|
});
|
|
|
|
//ie hack
|
|
if(window.navigator.userAgent.indexOf("MSIE ")){
|
|
var ranger = element.find("input");
|
|
ranger.bind("change",function(){
|
|
scope.$apply(function(){
|
|
scope.dimensions.scale.current = ranger.val();
|
|
});
|
|
});
|
|
}
|
|
|
|
//// INIT /////
|
|
$image.load(function(){
|
|
$timeout(function(){
|
|
init($image);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbImageGravity
|
|
* @restrict E
|
|
* @function
|
|
* @description
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('umbImageGravity', function ($timeout, localizationService, $log) {
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/imaging/umb-image-gravity.html',
|
|
scope: {
|
|
src: '=',
|
|
center: "=",
|
|
onImageLoaded: "="
|
|
},
|
|
link: function(scope, element, attrs) {
|
|
|
|
//Internal values for keeping track of the dot and the size of the editor
|
|
scope.dimensions = {
|
|
width: 0,
|
|
height: 0,
|
|
left: 0,
|
|
top: 0
|
|
};
|
|
|
|
scope.loaded = false;
|
|
|
|
//elements
|
|
var $viewport = element.find(".viewport");
|
|
var $image = element.find("img");
|
|
var $overlay = element.find(".overlay");
|
|
|
|
scope.style = function () {
|
|
if(scope.dimensions.width <= 0){
|
|
setDimensions();
|
|
}
|
|
|
|
return {
|
|
'top': scope.dimensions.top + 'px',
|
|
'left': scope.dimensions.left + 'px'
|
|
};
|
|
};
|
|
|
|
scope.setFocalPoint = function(event) {
|
|
|
|
scope.$emit("imageFocalPointStart");
|
|
|
|
var offsetX = event.offsetX - 10;
|
|
var offsetY = event.offsetY - 10;
|
|
|
|
calculateGravity(offsetX, offsetY);
|
|
|
|
lazyEndEvent();
|
|
|
|
};
|
|
|
|
var setDimensions = function(){
|
|
scope.dimensions.width = $image.width();
|
|
scope.dimensions.height = $image.height();
|
|
|
|
if(scope.center){
|
|
scope.dimensions.left = scope.center.left * scope.dimensions.width -10;
|
|
scope.dimensions.top = scope.center.top * scope.dimensions.height -10;
|
|
}else{
|
|
scope.center = { left: 0.5, top: 0.5 };
|
|
}
|
|
};
|
|
|
|
var calculateGravity = function(offsetX, offsetY){
|
|
scope.dimensions.left = offsetX;
|
|
scope.dimensions.top = offsetY;
|
|
|
|
scope.center.left = (scope.dimensions.left+10) / scope.dimensions.width;
|
|
scope.center.top = (scope.dimensions.top+10) / scope.dimensions.height;
|
|
};
|
|
|
|
var lazyEndEvent = _.debounce(function(){
|
|
scope.$apply(function(){
|
|
scope.$emit("imageFocalPointStop");
|
|
});
|
|
}, 2000);
|
|
|
|
|
|
//Drag and drop positioning, using jquery ui draggable
|
|
//TODO ensure that the point doesnt go outside the box
|
|
$overlay.draggable({
|
|
containment: "parent",
|
|
start: function(){
|
|
scope.$apply(function(){
|
|
scope.$emit("imageFocalPointStart");
|
|
});
|
|
},
|
|
stop: function() {
|
|
scope.$apply(function(){
|
|
var offsetX = $overlay[0].offsetLeft;
|
|
var offsetY = $overlay[0].offsetTop;
|
|
calculateGravity(offsetX, offsetY);
|
|
});
|
|
|
|
lazyEndEvent();
|
|
}
|
|
});
|
|
|
|
//// INIT /////
|
|
$image.load(function(){
|
|
$timeout(function(){
|
|
setDimensions();
|
|
scope.loaded = true;
|
|
scope.onImageLoaded();
|
|
});
|
|
});
|
|
|
|
$(window).on('resize.umbImageGravity', function(){
|
|
scope.$apply(function(){
|
|
$timeout(function(){
|
|
setDimensions();
|
|
});
|
|
// Make sure we can find the offset values for the overlay(dot) before calculating
|
|
// fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte)
|
|
if($overlay.is(':visible')) {
|
|
var offsetX = $overlay[0].offsetLeft;
|
|
var offsetY = $overlay[0].offsetTop;
|
|
calculateGravity(offsetX, offsetY);
|
|
}
|
|
});
|
|
});
|
|
|
|
scope.$on('$destroy', function() {
|
|
$(window).off('.umbImageGravity');
|
|
});
|
|
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbImageThumbnail
|
|
* @restrict E
|
|
* @function
|
|
* @description
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('umbImageThumbnail',
|
|
function ($timeout, localizationService, cropperHelper, $log) {
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/imaging/umb-image-thumbnail.html',
|
|
|
|
scope: {
|
|
src: '=',
|
|
width: '@',
|
|
height: '@',
|
|
center: "=",
|
|
crop: "=",
|
|
maxSize: '@'
|
|
},
|
|
|
|
link: function(scope, element, attrs) {
|
|
//// INIT /////
|
|
var $image = element.find("img");
|
|
scope.loaded = false;
|
|
|
|
$image.load(function(){
|
|
$timeout(function(){
|
|
$image.width("auto");
|
|
$image.height("auto");
|
|
|
|
scope.image = {};
|
|
scope.image.width = $image[0].width;
|
|
scope.image.height = $image[0].height;
|
|
|
|
//we force a lower thumbnail size to fit the max size
|
|
//we do not compare to the image dimensions, but the thumbs
|
|
if(scope.maxSize){
|
|
var ratioCalculation = cropperHelper.calculateAspectRatioFit(
|
|
scope.width,
|
|
scope.height,
|
|
scope.maxSize,
|
|
scope.maxSize,
|
|
false);
|
|
|
|
//so if we have a max size, override the thumb sizes
|
|
scope.width = ratioCalculation.width;
|
|
scope.height = ratioCalculation.height;
|
|
}
|
|
|
|
setPreviewStyle();
|
|
scope.loaded = true;
|
|
});
|
|
});
|
|
|
|
/// WATCHERS ////
|
|
scope.$watchCollection('[crop, center]', function(newValues, oldValues){
|
|
//we have to reinit the whole thing if
|
|
//one of the external params changes
|
|
setPreviewStyle();
|
|
});
|
|
|
|
scope.$watch("center", function(){
|
|
setPreviewStyle();
|
|
}, true);
|
|
|
|
function setPreviewStyle(){
|
|
if(scope.crop && scope.image){
|
|
scope.preview = cropperHelper.convertToStyle(
|
|
scope.crop,
|
|
scope.image,
|
|
{width: scope.width, height: scope.height},
|
|
0);
|
|
}else if(scope.image){
|
|
|
|
//returns size fitting the cropper
|
|
var p = cropperHelper.calculateAspectRatioFit(
|
|
scope.image.width,
|
|
scope.image.height,
|
|
scope.width,
|
|
scope.height,
|
|
true);
|
|
|
|
|
|
if(scope.center){
|
|
var xy = cropperHelper.alignToCoordinates(p, scope.center, {width: scope.width, height: scope.height});
|
|
p.top = xy.top;
|
|
p.left = xy.left;
|
|
}else{
|
|
|
|
}
|
|
|
|
p.position = "absolute";
|
|
scope.preview = p;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
angular.module("umbraco.directives")
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:localize
|
|
* @restrict EA
|
|
* @function
|
|
* @description Localize directive
|
|
**/
|
|
.directive('localize', function ($log, localizationService) {
|
|
return {
|
|
restrict: 'E',
|
|
scope:{
|
|
key: '@'
|
|
},
|
|
replace: true,
|
|
|
|
link: function (scope, element, attrs) {
|
|
var key = scope.key;
|
|
localizationService.localize(key).then(function(value){
|
|
element.html(value);
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('localize', function ($log, localizationService) {
|
|
return {
|
|
restrict: 'A',
|
|
link: function (scope, element, attrs) {
|
|
var keys = attrs.localize.split(',');
|
|
|
|
angular.forEach(keys, function(value, key){
|
|
var attr = element.attr(value);
|
|
|
|
if(attr){
|
|
if(attr[0] === '@'){
|
|
|
|
var t = localizationService.tokenize(attr.substring(1), scope);
|
|
localizationService.localize(t.key, t.tokens).then(function(val){
|
|
element.attr(value, val);
|
|
});
|
|
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbNotifications
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function NotificationDirective(notificationsService) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
//subscribes to notifications in the notification service
|
|
scope.notifications = notificationsService.current;
|
|
scope.$watch('notificationsService.current', function (newVal, oldVal, scope) {
|
|
if (newVal) {
|
|
scope.notifications = newVal;
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: "E",
|
|
replace: true,
|
|
templateUrl: 'views/components/notifications/umb-notifications.html',
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbNotifications', NotificationDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbOverlay
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<button type="button" ng-click="vm.openOverlay()"></button>
|
|
|
|
<umb-overlay
|
|
ng-if="vm.overlay.show"
|
|
model="vm.overlay"
|
|
view="vm.overlay.view"
|
|
position="right">
|
|
</umb-overlay>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
|
|
vm.openOverlay = openOverlay;
|
|
|
|
function openOverlay() {
|
|
|
|
vm.overlay = {
|
|
view: "mediapicker",
|
|
show: true,
|
|
submit: function(model) {
|
|
|
|
vm.overlay.show = false;
|
|
vm.overlay = null;
|
|
},
|
|
close: function(oldModel) {
|
|
vm.overlay.show = false;
|
|
vm.overlay = null;
|
|
}
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
<h1>General Options</h1>
|
|
Lorem ipsum dolor sit amet..
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Param</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tr>
|
|
<td>model.title</td>
|
|
<td>String</td>
|
|
<td>Set the title of the overlay.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.subTitle</td>
|
|
<td>String</td>
|
|
<td>Set the subtitle of the overlay.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.submitButtonLabel</td>
|
|
<td>String</td>
|
|
<td>Set an alternate submit button text</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.submitButtonLabelKey</td>
|
|
<td>String</td>
|
|
<td>Set an alternate submit button label key for localized texts</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.hideSubmitButton</td>
|
|
<td>Boolean</td>
|
|
<td>Hides the submit button</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.closeButtonLabel</td>
|
|
<td>String</td>
|
|
<td>Set an alternate close button text</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.closeButtonLabelKey</td>
|
|
<td>String</td>
|
|
<td>Set an alternate close button label key for localized texts</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.show</td>
|
|
<td>Boolean</td>
|
|
<td>Show/hide the overlay</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.submit</td>
|
|
<td>Function</td>
|
|
<td>Callback function when the overlay submits. Returns the overlay model object</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.close</td>
|
|
<td>Function</td>
|
|
<td>Callback function when the overlay closes. Returns a copy of the overlay model object before being modified</td>
|
|
</tr>
|
|
</table>
|
|
|
|
|
|
<h1>Content Picker</h1>
|
|
Opens a content picker.</br>
|
|
<strong>view: </strong>contentpicker
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Param</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tr>
|
|
<td>model.multiPicker</td>
|
|
<td>Boolean</td>
|
|
<td>Pick one or multiple items</td>
|
|
</tr>
|
|
</table>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Returns</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tr>
|
|
<td>model.selection</td>
|
|
<td>Array</td>
|
|
<td>Array of content objects</td>
|
|
</tr>
|
|
</table>
|
|
|
|
|
|
<h1>Icon Picker</h1>
|
|
Opens an icon picker.</br>
|
|
<strong>view: </strong>iconpicker
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Returns</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tr>
|
|
<td>model.icon</td>
|
|
<td>String</td>
|
|
<td>The icon class</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<h1>Item Picker</h1>
|
|
Opens an item picker.</br>
|
|
<strong>view: </strong>itempicker
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Param</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.availableItems</td>
|
|
<td>Array</td>
|
|
<td>Array of available items</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.selectedItems</td>
|
|
<td>Array</td>
|
|
<td>Array of selected items. When passed in the selected items will be filtered from the available items.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.filter</td>
|
|
<td>Boolean</td>
|
|
<td>Set to false to hide the filter</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Returns</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tr>
|
|
<td>model.selectedItem</td>
|
|
<td>Object</td>
|
|
<td>The selected item</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<h1>Macro Picker</h1>
|
|
Opens a media picker.</br>
|
|
<strong>view: </strong>macropicker
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Param</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.dialogData</td>
|
|
<td>Object</td>
|
|
<td>Object which contains array of allowedMacros. Set to <code>null</code> to allow all.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Returns</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.macroParams</td>
|
|
<td>Array</td>
|
|
<td>Array of macro params</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.selectedMacro</td>
|
|
<td>Object</td>
|
|
<td>The selected macro</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h1>Media Picker</h1>
|
|
Opens a media picker.</br>
|
|
<strong>view: </strong>mediapicker
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Param</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.multiPicker</td>
|
|
<td>Boolean</td>
|
|
<td>Pick one or multiple items</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.onlyImages</td>
|
|
<td>Boolean</td>
|
|
<td>Only display files that have an image file-extension</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.disableFolderSelect</td>
|
|
<td>Boolean</td>
|
|
<td>Disable folder selection</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Returns</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.selectedImages</td>
|
|
<td>Array</td>
|
|
<td>Array of selected images</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h1>Member Group Picker</h1>
|
|
Opens a member group picker.</br>
|
|
<strong>view: </strong>membergrouppicker
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Param</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.multiPicker</td>
|
|
<td>Boolean</td>
|
|
<td>Pick one or multiple items</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Returns</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.selectedMemberGroup</td>
|
|
<td>String</td>
|
|
<td>The selected member group</td>
|
|
</tr>
|
|
<tr>
|
|
<td>model.selectedMemberGroups (multiPicker)</td>
|
|
<td>Array</td>
|
|
<td>The selected member groups</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h1>Member Picker</h1>
|
|
Opens a member picker. </br>
|
|
<strong>view: </strong>memberpicker
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Param</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.multiPicker</td>
|
|
<td>Boolean</td>
|
|
<td>Pick one or multiple items</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Returns</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.selection</td>
|
|
<td>Array</td>
|
|
<td>Array of selected members/td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h1>YSOD</h1>
|
|
Opens an overlay to show a custom YSOD. </br>
|
|
<strong>view: </strong>ysod
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Param</th>
|
|
<th>Type</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>model.error</td>
|
|
<td>Object</td>
|
|
<td>Error object</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
@param {object} model Overlay options.
|
|
@param {string} view Path to view or one of the default view names.
|
|
@param {string} position The overlay position ("left", "right", "center": "target").
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function OverlayDirective($timeout, formHelper, overlayHelper, localizationService) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.directive = {
|
|
enableConfirmButton: false
|
|
};
|
|
|
|
var overlayNumber = 0;
|
|
var numberOfOverlays = 0;
|
|
var isRegistered = false;
|
|
|
|
var modelCopy = {};
|
|
|
|
function activate() {
|
|
|
|
setView();
|
|
|
|
setButtonText();
|
|
|
|
modelCopy = makeModelCopy(scope.model);
|
|
|
|
$timeout(function() {
|
|
|
|
if (scope.position === "target") {
|
|
setTargetPosition();
|
|
}
|
|
|
|
// this has to be done inside a timeout to ensure the destroy
|
|
// event on other overlays is run before registering a new one
|
|
registerOverlay();
|
|
|
|
setOverlayIndent();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
function setView() {
|
|
|
|
if (scope.view) {
|
|
|
|
if (scope.view.indexOf(".html") === -1) {
|
|
var viewAlias = scope.view.toLowerCase();
|
|
scope.view = "views/common/overlays/" + viewAlias + "/" + viewAlias + ".html";
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function setButtonText() {
|
|
if (!scope.model.closeButtonLabelKey && !scope.model.closeButtonLabel) {
|
|
scope.model.closeButtonLabel = localizationService.localize("general_close");
|
|
}
|
|
if (!scope.model.submitButtonLabelKey && !scope.model.submitButtonLabel) {
|
|
scope.model.submitButtonLabel = localizationService.localize("general_submit");
|
|
}
|
|
}
|
|
|
|
function registerOverlay() {
|
|
|
|
overlayNumber = overlayHelper.registerOverlay();
|
|
|
|
$(document).bind("keydown.overlay-" + overlayNumber, function(event) {
|
|
|
|
if (event.which === 27) {
|
|
|
|
numberOfOverlays = overlayHelper.getNumberOfOverlays();
|
|
|
|
if(numberOfOverlays === overlayNumber) {
|
|
scope.closeOverLay();
|
|
}
|
|
|
|
event.preventDefault();
|
|
}
|
|
|
|
if (event.which === 13) {
|
|
|
|
numberOfOverlays = overlayHelper.getNumberOfOverlays();
|
|
|
|
if(numberOfOverlays === overlayNumber) {
|
|
|
|
var activeElementType = document.activeElement.tagName;
|
|
var clickableElements = ["A", "BUTTON"];
|
|
var submitOnEnter = document.activeElement.hasAttribute("overlay-submit-on-enter");
|
|
|
|
if(clickableElements.indexOf(activeElementType) === 0) {
|
|
document.activeElement.click();
|
|
event.preventDefault();
|
|
} else if(activeElementType === "TEXTAREA" && !submitOnEnter) {
|
|
|
|
|
|
} else {
|
|
scope.$apply(function () {
|
|
scope.submitForm(scope.model);
|
|
});
|
|
event.preventDefault();
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
isRegistered = true;
|
|
|
|
}
|
|
|
|
function unregisterOverlay() {
|
|
|
|
if(isRegistered) {
|
|
|
|
overlayHelper.unregisterOverlay();
|
|
|
|
$(document).unbind("keydown.overlay-" + overlayNumber);
|
|
|
|
isRegistered = false;
|
|
}
|
|
|
|
}
|
|
|
|
function makeModelCopy(object) {
|
|
|
|
var newObject = {};
|
|
|
|
for (var key in object) {
|
|
if (key !== "event") {
|
|
newObject[key] = angular.copy(object[key]);
|
|
}
|
|
}
|
|
|
|
return newObject;
|
|
|
|
}
|
|
|
|
function setOverlayIndent() {
|
|
|
|
var overlayIndex = overlayNumber - 1;
|
|
var indentSize = overlayIndex * 20;
|
|
var overlayWidth = el.context.clientWidth;
|
|
|
|
el.css('width', overlayWidth - indentSize);
|
|
|
|
if(scope.position === "center" || scope.position === "target") {
|
|
var overlayTopPosition = el.context.offsetTop;
|
|
el.css('top', overlayTopPosition + indentSize);
|
|
}
|
|
|
|
}
|
|
|
|
function setTargetPosition() {
|
|
|
|
var container = $("#contentwrapper");
|
|
var containerLeft = container[0].offsetLeft;
|
|
var containerRight = containerLeft + container[0].offsetWidth;
|
|
var containerTop = container[0].offsetTop;
|
|
var containerBottom = containerTop + container[0].offsetHeight;
|
|
|
|
var mousePositionClickX = null;
|
|
var mousePositionClickY = null;
|
|
var elementHeight = null;
|
|
var elementWidth = null;
|
|
|
|
var position = {
|
|
right: "inherit",
|
|
left: "inherit",
|
|
top: "inherit",
|
|
bottom: "inherit"
|
|
};
|
|
|
|
// if mouse click position is know place element with mouse in center
|
|
if (scope.model.event && scope.model.event) {
|
|
|
|
// click position
|
|
mousePositionClickX = scope.model.event.pageX;
|
|
mousePositionClickY = scope.model.event.pageY;
|
|
|
|
// element size
|
|
elementHeight = el.context.clientHeight;
|
|
elementWidth = el.context.clientWidth;
|
|
|
|
// move element to this position
|
|
position.left = mousePositionClickX - (elementWidth / 2);
|
|
position.top = mousePositionClickY - (elementHeight / 2);
|
|
|
|
// check to see if element is outside screen
|
|
// outside right
|
|
if (position.left + elementWidth > containerRight) {
|
|
position.right = 10;
|
|
position.left = "inherit";
|
|
}
|
|
|
|
// outside bottom
|
|
if (position.top + elementHeight > containerBottom) {
|
|
position.bottom = 10;
|
|
position.top = "inherit";
|
|
}
|
|
|
|
// outside left
|
|
if (position.left < containerLeft) {
|
|
position.left = containerLeft + 10;
|
|
position.right = "inherit";
|
|
}
|
|
|
|
// outside top
|
|
if (position.top < containerTop) {
|
|
position.top = 10;
|
|
position.bottom = "inherit";
|
|
}
|
|
|
|
el.css(position);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
scope.submitForm = function(model) {
|
|
if(scope.model.submit) {
|
|
if (formHelper.submitForm({scope: scope})) {
|
|
formHelper.resetForm({ scope: scope });
|
|
|
|
if(scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) {
|
|
scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton);
|
|
} else {
|
|
unregisterOverlay();
|
|
scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton);
|
|
}
|
|
|
|
}
|
|
}
|
|
};
|
|
|
|
scope.cancelConfirmSubmit = function() {
|
|
scope.model.confirmSubmit.show = false;
|
|
};
|
|
|
|
scope.closeOverLay = function() {
|
|
|
|
unregisterOverlay();
|
|
|
|
if (scope.model.close) {
|
|
scope.model = modelCopy;
|
|
scope.model.close(scope.model);
|
|
} else {
|
|
scope.model.show = false;
|
|
scope.model = null;
|
|
}
|
|
|
|
};
|
|
|
|
// angular does not support ng-show on custom directives
|
|
// width isolated scopes. So we have to make our own.
|
|
if (attr.hasOwnProperty("ngShow")) {
|
|
scope.$watch("ngShow", function(value) {
|
|
if (value) {
|
|
el.show();
|
|
activate();
|
|
} else {
|
|
unregisterOverlay();
|
|
el.hide();
|
|
}
|
|
});
|
|
} else {
|
|
activate();
|
|
}
|
|
|
|
scope.$on('$destroy', function(){
|
|
unregisterOverlay();
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/overlays/umb-overlay.html',
|
|
scope: {
|
|
ngShow: "=",
|
|
model: "=",
|
|
view: "=",
|
|
position: "@"
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbOverlay', OverlayDirective);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function OverlayBackdropDirective(overlayHelper) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.numberOfOverlays = 0;
|
|
|
|
scope.$watch(function(){
|
|
return overlayHelper.getNumberOfOverlays();
|
|
}, function (newValue) {
|
|
scope.numberOfOverlays = newValue;
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/overlays/umb-overlay-backdrop.html',
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbOverlayBackdrop', OverlayBackdropDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbProperty
|
|
* @restrict E
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('umbProperty', function (umbPropEditorHelper) {
|
|
return {
|
|
scope: {
|
|
property: "="
|
|
},
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/property/umb-property.html',
|
|
link: function(scope) {
|
|
scope.propertyAlias = Umbraco.Sys.ServerVariables.isDebuggingEnabled === true ? scope.property.alias : null;
|
|
},
|
|
//Define a controller for this directive to expose APIs to other directives
|
|
controller: function ($scope, $timeout) {
|
|
|
|
var self = this;
|
|
|
|
//set the API properties/methods
|
|
|
|
self.property = $scope.property;
|
|
self.setPropertyError = function(errorMsg) {
|
|
$scope.property.propertyErrorMessage = errorMsg;
|
|
};
|
|
}
|
|
};
|
|
});
|
|
/**
|
|
* @ngdoc directive
|
|
* @function
|
|
* @name umbraco.directives.directive:umbPropertyEditor
|
|
* @requires formController
|
|
* @restrict E
|
|
**/
|
|
|
|
//share property editor directive function
|
|
var _umbPropertyEditor = function (umbPropEditorHelper) {
|
|
return {
|
|
scope: {
|
|
model: "=",
|
|
isPreValue: "@",
|
|
preview: "@"
|
|
},
|
|
|
|
require: "^form",
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/property/umb-property-editor.html',
|
|
link: function (scope, element, attrs, ctrl) {
|
|
|
|
//we need to copy the form controller val to our isolated scope so that
|
|
//it get's carried down to the child scopes of this!
|
|
//we'll also maintain the current form name.
|
|
scope[ctrl.$name] = ctrl;
|
|
|
|
if(!scope.model.alias){
|
|
scope.model.alias = Math.random().toString(36).slice(2);
|
|
}
|
|
|
|
scope.$watch("model.view", function(val){
|
|
scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue);
|
|
});
|
|
}
|
|
};
|
|
};
|
|
|
|
//Preffered is the umb-property-editor as its more explicit - but we keep umb-editor for backwards compat
|
|
angular.module("umbraco.directives").directive('umbPropertyEditor', _umbPropertyEditor);
|
|
angular.module("umbraco.directives").directive('umbEditor', _umbPropertyEditor);
|
|
|
|
angular.module("umbraco.directives.html")
|
|
.directive('umbPropertyGroup', function () {
|
|
return {
|
|
transclude: true,
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/property/umb-property-group.html'
|
|
};
|
|
});
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbTab
|
|
* @restrict E
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('umbTab', function ($parse, $timeout) {
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
transclude: 'true',
|
|
templateUrl: 'views/components/tabs/umb-tab.html'
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbTabs
|
|
* @restrict A
|
|
* @description Used to bind to bootstrap tab events so that sub directives can use this API to listen to tab changes
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('umbTabs', function () {
|
|
return {
|
|
restrict: 'A',
|
|
controller: function ($scope, $element, $attrs) {
|
|
|
|
var callbacks = [];
|
|
this.onTabShown = function(cb) {
|
|
callbacks.push(cb);
|
|
};
|
|
|
|
function tabShown(event) {
|
|
|
|
var curr = $(event.target); // active tab
|
|
var prev = $(event.relatedTarget); // previous tab
|
|
|
|
$scope.$apply();
|
|
|
|
for (var c in callbacks) {
|
|
callbacks[c].apply(this, [{current: curr, previous: prev}]);
|
|
}
|
|
}
|
|
|
|
//NOTE: it MUST be done this way - binding to an ancestor element that exists
|
|
// in the DOM to bind to the dynamic elements that will be created.
|
|
// It would be nicer to create this event handler as a directive for which child
|
|
// directives can attach to.
|
|
$element.on('shown', '.nav-tabs a', tabShown);
|
|
|
|
//ensure to unregister
|
|
$scope.$on('$destroy', function () {
|
|
$element.off('shown', '.nav-tabs a', tabShown);
|
|
|
|
for (var c in callbacks) {
|
|
delete callbacks[c];
|
|
}
|
|
callbacks = null;
|
|
});
|
|
}
|
|
};
|
|
});
|
|
(function() {
|
|
'use strict';
|
|
|
|
function UmbTabsContentDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.view = attr.view;
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: "E",
|
|
replace: true,
|
|
transclude: 'true',
|
|
templateUrl: "views/components/tabs/umb-tabs-content.html",
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbTabsContent', UmbTabsContentDirective);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function UmbTabsNavDirective($timeout) {
|
|
|
|
function link(scope, el, attr) {
|
|
|
|
function activate() {
|
|
|
|
$timeout(function () {
|
|
|
|
//use bootstrap tabs API to show the first one
|
|
el.find("a:first").tab('show');
|
|
|
|
//enable the tab drop
|
|
el.tabdrop();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
var unbindModelWatch = scope.$watch('model', function(newValue, oldValue){
|
|
|
|
activate();
|
|
|
|
});
|
|
|
|
|
|
scope.$on('$destroy', function () {
|
|
|
|
//ensure to destroy tabdrop (unbinds window resize listeners)
|
|
el.tabdrop("destroy");
|
|
|
|
unbindModelWatch();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: "E",
|
|
replace: true,
|
|
templateUrl: "views/components/tabs/umb-tabs-nav.html",
|
|
scope: {
|
|
model: "=",
|
|
tabdrop: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbTabsNav', UmbTabsNavDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbTree
|
|
* @restrict E
|
|
**/
|
|
function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificationsService, $timeout, userService) {
|
|
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
terminal: false,
|
|
|
|
scope: {
|
|
section: '@',
|
|
treealias: '@',
|
|
hideoptions: '@',
|
|
hideheader: '@',
|
|
cachekey: '@',
|
|
isdialog: '@',
|
|
//Custom query string arguments to pass in to the tree as a string, example: "startnodeid=123&something=value"
|
|
customtreeparams: '@',
|
|
eventhandler: '=',
|
|
enablecheckboxes: '@',
|
|
enablelistviewsearch: '@'
|
|
},
|
|
|
|
compile: function(element, attrs) {
|
|
//config
|
|
//var showheader = (attrs.showheader !== 'false');
|
|
var hideoptions = (attrs.hideoptions === 'true') ? "hide-options" : "";
|
|
var template = '<ul class="umb-tree ' + hideoptions + '"><li class="root">';
|
|
template += '<div ng-hide="hideheader" on-right-click="altSelect(tree.root, $event)">' +
|
|
'<h5>' +
|
|
'<a href="#/{{section}}" ng-click="select(tree.root, $event)" class="root-link"><i ng-if="enablecheckboxes == \'true\'" ng-class="selectEnabledNodeClass(tree.root)"></i> {{tree.name}}</a></h5>' +
|
|
'<a class="umb-options" ng-hide="tree.root.isContainer || !tree.root.menuUrl" ng-click="options(tree.root, $event)" ng-swipe-right="options(tree.root, $event)"><i></i><i></i><i></i></a>' +
|
|
'</div>';
|
|
template += '<ul>' +
|
|
'<umb-tree-item ng-repeat="child in tree.root.children" eventhandler="eventhandler" node="child" current-node="currentNode" tree="this" section="{{section}}" ng-animate="animation()"></umb-tree-item>' +
|
|
'</ul>' +
|
|
'</li>' +
|
|
'</ul>';
|
|
|
|
element.replaceWith(template);
|
|
|
|
return function(scope, elem, attr, controller) {
|
|
|
|
//flag to track the last loaded section when the tree 'un-loads'. We use this to determine if we should
|
|
// re-load the tree again. For example, if we hover over 'content' the content tree is shown. Then we hover
|
|
// outside of the tree and the tree 'un-loads'. When we re-hover over 'content', we don't want to re-load the
|
|
// entire tree again since we already still have it in memory. Of course if the section is different we will
|
|
// reload it. This saves a lot on processing if someone is navigating in and out of the same section many times
|
|
// since it saves on data retreival and DOM processing.
|
|
var lastSection = "";
|
|
|
|
//setup a default internal handler
|
|
if (!scope.eventhandler) {
|
|
scope.eventhandler = $({});
|
|
}
|
|
|
|
//flag to enable/disable delete animations
|
|
var deleteAnimations = false;
|
|
|
|
|
|
/** Helper function to emit tree events */
|
|
function emitEvent(eventName, args) {
|
|
if (scope.eventhandler) {
|
|
$(scope.eventhandler).trigger(eventName, args);
|
|
}
|
|
}
|
|
|
|
/** This will deleteAnimations to true after the current digest */
|
|
function enableDeleteAnimations() {
|
|
//do timeout so that it re-enables them after this digest
|
|
$timeout(function () {
|
|
//enable delete animations
|
|
deleteAnimations = true;
|
|
}, 0, false);
|
|
}
|
|
|
|
|
|
/*this is the only external interface a tree has */
|
|
function setupExternalEvents() {
|
|
if (scope.eventhandler) {
|
|
|
|
scope.eventhandler.clearCache = function(section) {
|
|
treeService.clearCache({ section: section });
|
|
};
|
|
|
|
scope.eventhandler.load = function(section) {
|
|
scope.section = section;
|
|
loadTree();
|
|
};
|
|
|
|
scope.eventhandler.reloadNode = function(node) {
|
|
|
|
if (!node) {
|
|
node = scope.currentNode;
|
|
}
|
|
|
|
if (node) {
|
|
scope.loadChildren(node, true);
|
|
}
|
|
};
|
|
|
|
/**
|
|
Used to do the tree syncing. If the args.tree is not specified we are assuming it has been
|
|
specified previously using the _setActiveTreeType
|
|
*/
|
|
scope.eventhandler.syncTree = function(args) {
|
|
if (!args) {
|
|
throw "args cannot be null";
|
|
}
|
|
if (!args.path) {
|
|
throw "args.path cannot be null";
|
|
}
|
|
|
|
var deferred = $q.defer();
|
|
|
|
//this is super complex but seems to be working in other places, here we're listening for our
|
|
// own events, once the tree is sycned we'll resolve our promise.
|
|
scope.eventhandler.one("treeSynced", function (e, syncArgs) {
|
|
deferred.resolve(syncArgs);
|
|
});
|
|
|
|
//this should normally be set unless it is being called from legacy
|
|
// code, so set the active tree type before proceeding.
|
|
if (args.tree) {
|
|
loadActiveTree(args.tree);
|
|
}
|
|
|
|
if (angular.isString(args.path)) {
|
|
args.path = args.path.replace('"', '').split(',');
|
|
}
|
|
|
|
//reset current node selection
|
|
//scope.currentNode = null;
|
|
|
|
//Filter the path for root node ids (we don't want to pass in -1 or 'init')
|
|
|
|
args.path = _.filter(args.path, function (item) { return (item !== "init" && item !== "-1"); });
|
|
|
|
//Once those are filtered we need to check if the current user has a special start node id,
|
|
// if they do, then we're going to trim the start of the array for anything found from that start node
|
|
// and previous so that the tree syncs properly. The tree syncs from the top down and if there are parts
|
|
// of the tree's path in there that don't actually exist in the dom/model then syncing will not work.
|
|
|
|
userService.getCurrentUser().then(function(userData) {
|
|
|
|
var startNodes = [userData.startContentId, userData.startMediaId];
|
|
_.each(startNodes, function (i) {
|
|
var found = _.find(args.path, function (p) {
|
|
return String(p) === String(i);
|
|
});
|
|
if (found) {
|
|
args.path = args.path.splice(_.indexOf(args.path, found));
|
|
}
|
|
});
|
|
|
|
|
|
loadPath(args.path, args.forceReload, args.activate);
|
|
|
|
});
|
|
|
|
|
|
|
|
return deferred.promise;
|
|
};
|
|
|
|
/**
|
|
Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to
|
|
have to set an active tree and then sync, the new API does this in one method by using syncTree.
|
|
loadChildren is optional but if it is set, it will set the current active tree and load the root
|
|
node's children - this is synonymous with the legacy refreshTree method - again should not be used
|
|
and should only be used for the legacy code to work.
|
|
*/
|
|
scope.eventhandler._setActiveTreeType = function(treeAlias, loadChildren) {
|
|
loadActiveTree(treeAlias, loadChildren);
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
//helper to load a specific path on the active tree as soon as its ready
|
|
function loadPath(path, forceReload, activate) {
|
|
|
|
if (scope.activeTree) {
|
|
syncTree(scope.activeTree, path, forceReload, activate);
|
|
}
|
|
else {
|
|
scope.eventhandler.one("activeTreeLoaded", function (e, args) {
|
|
syncTree(args.tree, path, forceReload, activate);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
//given a tree alias, this will search the current section tree for the specified tree alias and
|
|
//set that to the activeTree
|
|
//NOTE: loadChildren is ONLY used for legacy purposes, do not use this when syncing the tree as it will cause problems
|
|
// since there will be double request and event handling operations.
|
|
function loadActiveTree(treeAlias, loadChildren) {
|
|
if (!treeAlias) {
|
|
return;
|
|
}
|
|
|
|
scope.activeTree = undefined;
|
|
|
|
function doLoad(tree) {
|
|
var childrenAndSelf = [tree].concat(tree.children);
|
|
scope.activeTree = _.find(childrenAndSelf, function (node) {
|
|
if(node && node.metaData && node.metaData.treeAlias) {
|
|
return node.metaData.treeAlias.toUpperCase() === treeAlias.toUpperCase();
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (!scope.activeTree) {
|
|
throw "Could not find the tree " + treeAlias + ", activeTree has not been set";
|
|
}
|
|
|
|
//This is only used for the legacy tree method refreshTree!
|
|
if (loadChildren) {
|
|
scope.activeTree.expanded = true;
|
|
scope.loadChildren(scope.activeTree, false).then(function() {
|
|
emitEvent("activeTreeLoaded", { tree: scope.activeTree });
|
|
});
|
|
}
|
|
else {
|
|
emitEvent("activeTreeLoaded", { tree: scope.activeTree });
|
|
}
|
|
}
|
|
|
|
if (scope.tree) {
|
|
doLoad(scope.tree.root);
|
|
}
|
|
else {
|
|
scope.eventhandler.one("treeLoaded", function(e, args) {
|
|
doLoad(args.tree.root);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
/** Method to load in the tree data */
|
|
|
|
function loadTree() {
|
|
if (!scope.loading && scope.section) {
|
|
scope.loading = true;
|
|
|
|
//anytime we want to load the tree we need to disable the delete animations
|
|
deleteAnimations = false;
|
|
|
|
//default args
|
|
var args = { section: scope.section, tree: scope.treealias, cacheKey: scope.cachekey, isDialog: scope.isdialog ? scope.isdialog : false };
|
|
|
|
//add the extra query string params if specified
|
|
if (scope.customtreeparams) {
|
|
args["queryString"] = scope.customtreeparams;
|
|
}
|
|
|
|
treeService.getTree(args)
|
|
.then(function(data) {
|
|
//set the data once we have it
|
|
scope.tree = data;
|
|
|
|
enableDeleteAnimations();
|
|
|
|
scope.loading = false;
|
|
|
|
//set the root as the current active tree
|
|
scope.activeTree = scope.tree.root;
|
|
emitEvent("treeLoaded", { tree: scope.tree });
|
|
emitEvent("treeNodeExpanded", { tree: scope.tree, node: scope.tree.root, children: scope.tree.root.children });
|
|
|
|
}, function(reason) {
|
|
scope.loading = false;
|
|
notificationsService.error("Tree Error", reason);
|
|
});
|
|
}
|
|
}
|
|
|
|
/** syncs the tree, the treeNode can be ANY tree node in the tree that requires syncing */
|
|
function syncTree(treeNode, path, forceReload, activate) {
|
|
|
|
deleteAnimations = false;
|
|
|
|
treeService.syncTree({
|
|
node: treeNode,
|
|
path: path,
|
|
forceReload: forceReload
|
|
}).then(function (data) {
|
|
|
|
if (activate === undefined || activate === true) {
|
|
scope.currentNode = data;
|
|
}
|
|
|
|
emitEvent("treeSynced", { node: data, activate: activate });
|
|
|
|
enableDeleteAnimations();
|
|
});
|
|
|
|
}
|
|
|
|
scope.selectEnabledNodeClass = function (node) {
|
|
return node ?
|
|
node.selected ?
|
|
'icon umb-tree-icon sprTree icon-check blue temporary' :
|
|
'' :
|
|
'';
|
|
};
|
|
|
|
/** method to set the current animation for the node.
|
|
* This changes dynamically based on if we are changing sections or just loading normal tree data.
|
|
* When changing sections we don't want all of the tree-ndoes to do their 'leave' animations.
|
|
*/
|
|
scope.animation = function() {
|
|
if (deleteAnimations && scope.tree && scope.tree.root && scope.tree.root.expanded) {
|
|
return { leave: 'tree-node-delete-leave' };
|
|
}
|
|
else {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
/* helper to force reloading children of a tree node */
|
|
scope.loadChildren = function(node, forceReload) {
|
|
var deferred = $q.defer();
|
|
|
|
//emit treeNodeExpanding event, if a callback object is set on the tree
|
|
emitEvent("treeNodeExpanding", { tree: scope.tree, node: node });
|
|
|
|
//standardising
|
|
if (!node.children) {
|
|
node.children = [];
|
|
}
|
|
|
|
if (forceReload || (node.hasChildren && node.children.length === 0)) {
|
|
//get the children from the tree service
|
|
treeService.loadNodeChildren({ node: node, section: scope.section })
|
|
.then(function(data) {
|
|
//emit expanded event
|
|
emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: data });
|
|
|
|
enableDeleteAnimations();
|
|
|
|
deferred.resolve(data);
|
|
});
|
|
}
|
|
else {
|
|
emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: node.children });
|
|
node.expanded = true;
|
|
|
|
enableDeleteAnimations();
|
|
|
|
deferred.resolve(node.children);
|
|
}
|
|
|
|
return deferred.promise;
|
|
};
|
|
|
|
/**
|
|
Method called when the options button next to the root node is called.
|
|
The tree doesnt know about this, so it raises an event to tell the parent controller
|
|
about it.
|
|
*/
|
|
scope.options = function(n, ev) {
|
|
emitEvent("treeOptionsClick", { element: elem, node: n, event: ev });
|
|
};
|
|
|
|
/**
|
|
Method called when an item is clicked in the tree, this passes the
|
|
DOM element, the tree node object and the original click
|
|
and emits it as a treeNodeSelect element if there is a callback object
|
|
defined on the tree
|
|
*/
|
|
scope.select = function (n, ev) {
|
|
//on tree select we need to remove the current node -
|
|
// whoever handles this will need to make sure the correct node is selected
|
|
//reset current node selection
|
|
scope.currentNode = null;
|
|
|
|
emitEvent("treeNodeSelect", { element: elem, node: n, event: ev });
|
|
};
|
|
|
|
scope.altSelect = function(n, ev) {
|
|
emitEvent("treeNodeAltSelect", { element: elem, tree: scope.tree, node: n, event: ev });
|
|
};
|
|
|
|
//watch for section changes
|
|
scope.$watch("section", function(newVal, oldVal) {
|
|
|
|
if (!scope.tree) {
|
|
loadTree();
|
|
}
|
|
|
|
if (!newVal) {
|
|
//store the last section loaded
|
|
lastSection = oldVal;
|
|
}
|
|
else if (newVal !== oldVal && newVal !== lastSection) {
|
|
//only reload the tree data and Dom if the newval is different from the old one
|
|
// and if the last section loaded is different from the requested one.
|
|
loadTree();
|
|
|
|
//store the new section to be loaded as the last section
|
|
//clear any active trees to reset lookups
|
|
lastSection = newVal;
|
|
}
|
|
});
|
|
|
|
setupExternalEvents();
|
|
loadTree();
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
angular.module("umbraco.directives").directive('umbTree', umbTreeDirective);
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbTreeItem
|
|
* @element li
|
|
* @function
|
|
*
|
|
* @description
|
|
* Renders a list item, representing a single node in the tree.
|
|
* Includes element to toggle children, and a menu toggling button
|
|
*
|
|
* **note:** This directive is only used internally in the umbTree directive
|
|
*
|
|
* @example
|
|
<example module="umbraco">
|
|
<file name="index.html">
|
|
<umb-tree-item ng-repeat="child in tree.children" node="child" callback="callback" section="content"></umb-tree-item>
|
|
</file>
|
|
</example>
|
|
*/
|
|
angular.module("umbraco.directives")
|
|
.directive('umbTreeItem', function ($compile, $http, $templateCache, $interpolate, $log, $location, $rootScope, $window, treeService, $timeout, localizationService) {
|
|
return {
|
|
restrict: 'E',
|
|
replace: true,
|
|
|
|
scope: {
|
|
section: '@',
|
|
eventhandler: '=',
|
|
currentNode: '=',
|
|
node: '=',
|
|
tree: '='
|
|
},
|
|
|
|
//TODO: Remove more of the binding from this template and move the DOM manipulation to be manually done in the link function,
|
|
// this will greatly improve performance since there's potentially a lot of nodes being rendered = a LOT of watches!
|
|
|
|
template: '<li ng-class="{\'current\': (node == currentNode), \'has-children\': node.hasChildren}" on-right-click="altSelect(node, $event)">' +
|
|
'<div ng-class="getNodeCssClass(node)" ng-swipe-right="options(node, $event)" >' +
|
|
//NOTE: This ins element is used to display the search icon if the node is a container/listview and the tree is currently in dialog
|
|
//'<ins ng-if="tree.enablelistviewsearch && node.metaData.isContainer" class="umb-tree-node-search icon-search" ng-click="searchNode(node, $event)" alt="searchAltText"></ins>' +
|
|
'<ins ng-class="{\'icon-navigation-right\': !node.expanded, \'icon-navigation-down\': node.expanded}" ng-click="load(node)"> </ins>' +
|
|
'<i class="icon umb-tree-icon sprTree" ng-click="select(node, $event)"></i>' +
|
|
'<a href="#/{{node.routePath}}" ng-click="select(node, $event)"></a>' +
|
|
//NOTE: These are the 'option' elipses
|
|
'<a class="umb-options" ng-click="options(node, $event)"><i></i><i></i><i></i></a>' +
|
|
'<div ng-show="node.loading" class="l"><div></div></div>' +
|
|
'</div>' +
|
|
'</li>',
|
|
|
|
link: function (scope, element, attrs) {
|
|
|
|
localizationService.localize("general_search").then(function (value) {
|
|
scope.searchAltText = value;
|
|
});
|
|
|
|
//flag to enable/disable delete animations, default for an item is true
|
|
var deleteAnimations = true;
|
|
|
|
// Helper function to emit tree events
|
|
function emitEvent(eventName, args) {
|
|
|
|
if (scope.eventhandler) {
|
|
$(scope.eventhandler).trigger(eventName, args);
|
|
}
|
|
}
|
|
|
|
// updates the node's DOM/styles
|
|
function setupNodeDom(node, tree) {
|
|
|
|
//get the first div element
|
|
element.children(":first")
|
|
//set the padding
|
|
.css("padding-left", (node.level * 20) + "px");
|
|
|
|
//toggle visibility of last 'ins' depending on children
|
|
//visibility still ensure the space is "reserved", so both nodes with and without children are aligned.
|
|
if (!node.hasChildren) {
|
|
element.find("ins").last().css("visibility", "hidden");
|
|
}
|
|
else {
|
|
element.find("ins").last().css("visibility", "visible");
|
|
}
|
|
|
|
var icon = element.find("i:first");
|
|
icon.addClass(node.cssClass);
|
|
icon.attr("title", node.routePath);
|
|
|
|
element.find("a:first").text(node.name);
|
|
|
|
if (!node.menuUrl) {
|
|
element.find("a.umb-options").remove();
|
|
}
|
|
|
|
if (node.style) {
|
|
element.find("i:first").attr("style", node.style);
|
|
}
|
|
}
|
|
|
|
//This will deleteAnimations to true after the current digest
|
|
function enableDeleteAnimations() {
|
|
//do timeout so that it re-enables them after this digest
|
|
$timeout(function () {
|
|
//enable delete animations
|
|
deleteAnimations = true;
|
|
}, 0, false);
|
|
}
|
|
|
|
/** Returns the css classses assigned to the node (div element) */
|
|
scope.getNodeCssClass = function (node) {
|
|
if (!node) {
|
|
return '';
|
|
}
|
|
var css = [];
|
|
if (node.cssClasses) {
|
|
_.each(node.cssClasses, function(c) {
|
|
css.push(c);
|
|
});
|
|
}
|
|
if (node.selected) {
|
|
css.push("umb-tree-node-checked");
|
|
}
|
|
return css.join(" ");
|
|
};
|
|
|
|
//add a method to the node which we can use to call to update the node data if we need to ,
|
|
// this is done by sync tree, we don't want to add a $watch for each node as that would be crazy insane slow
|
|
// so we have to do this
|
|
scope.node.updateNodeData = function (newNode) {
|
|
_.extend(scope.node, newNode);
|
|
//now update the styles
|
|
setupNodeDom(scope.node, scope.tree);
|
|
};
|
|
|
|
/**
|
|
Method called when the options button next to a node is called
|
|
In the main tree this opens the menu, but internally the tree doesnt
|
|
know about this, so it simply raises an event to tell the parent controller
|
|
about it.
|
|
*/
|
|
scope.options = function (n, ev) {
|
|
emitEvent("treeOptionsClick", { element: element, tree: scope.tree, node: n, event: ev });
|
|
};
|
|
|
|
/**
|
|
Method called when an item is clicked in the tree, this passes the
|
|
DOM element, the tree node object and the original click
|
|
and emits it as a treeNodeSelect element if there is a callback object
|
|
defined on the tree
|
|
*/
|
|
scope.select = function (n, ev) {
|
|
if (ev.ctrlKey ||
|
|
ev.shiftKey ||
|
|
ev.metaKey || // apple
|
|
(ev.button && ev.button === 1) // middle click, >IE9 + everyone else
|
|
) {
|
|
return;
|
|
}
|
|
|
|
emitEvent("treeNodeSelect", { element: element, tree: scope.tree, node: n, event: ev });
|
|
ev.preventDefault();
|
|
};
|
|
|
|
/**
|
|
Method called when an item is right-clicked in the tree, this passes the
|
|
DOM element, the tree node object and the original click
|
|
and emits it as a treeNodeSelect element if there is a callback object
|
|
defined on the tree
|
|
*/
|
|
scope.altSelect = function (n, ev) {
|
|
emitEvent("treeNodeAltSelect", { element: element, tree: scope.tree, node: n, event: ev });
|
|
};
|
|
|
|
/** method to set the current animation for the node.
|
|
* This changes dynamically based on if we are changing sections or just loading normal tree data.
|
|
* When changing sections we don't want all of the tree-ndoes to do their 'leave' animations.
|
|
*/
|
|
scope.animation = function () {
|
|
if (scope.node.showHideAnimation) {
|
|
return scope.node.showHideAnimation;
|
|
}
|
|
if (deleteAnimations && scope.node.expanded) {
|
|
return { leave: 'tree-node-delete-leave' };
|
|
}
|
|
else {
|
|
return {};
|
|
}
|
|
};
|
|
|
|
/**
|
|
Method called when a node in the tree is expanded, when clicking the arrow
|
|
takes the arrow DOM element and node data as parameters
|
|
emits treeNodeCollapsing event if already expanded and treeNodeExpanding if collapsed
|
|
*/
|
|
scope.load = function (node) {
|
|
if (node.expanded) {
|
|
deleteAnimations = false;
|
|
emitEvent("treeNodeCollapsing", { tree: scope.tree, node: node, element: element });
|
|
node.expanded = false;
|
|
}
|
|
else {
|
|
scope.loadChildren(node, false);
|
|
}
|
|
};
|
|
|
|
/* helper to force reloading children of a tree node */
|
|
scope.loadChildren = function (node, forceReload) {
|
|
//emit treeNodeExpanding event, if a callback object is set on the tree
|
|
emitEvent("treeNodeExpanding", { tree: scope.tree, node: node });
|
|
|
|
if (node.hasChildren && (forceReload || !node.children || (angular.isArray(node.children) && node.children.length === 0))) {
|
|
//get the children from the tree service
|
|
treeService.loadNodeChildren({ node: node, section: scope.section })
|
|
.then(function (data) {
|
|
//emit expanded event
|
|
emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: data });
|
|
enableDeleteAnimations();
|
|
});
|
|
}
|
|
else {
|
|
emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: node.children });
|
|
node.expanded = true;
|
|
enableDeleteAnimations();
|
|
}
|
|
};
|
|
|
|
//if the current path contains the node id, we will auto-expand the tree item children
|
|
|
|
setupNodeDom(scope.node, scope.tree);
|
|
|
|
var template = '<ul ng-class="{collapsed: !node.expanded}"><umb-tree-item ng-repeat="child in node.children" eventhandler="eventhandler" tree="tree" current-node="currentNode" node="child" section="{{section}}" ng-animate="animation()"></umb-tree-item></ul>';
|
|
var newElement = angular.element(template);
|
|
$compile(newElement)(scope);
|
|
element.append(newElement);
|
|
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbTreeSearchBox
|
|
* @function
|
|
* @element ANY
|
|
* @restrict E
|
|
**/
|
|
function treeSearchBox(localizationService, searchService, $q) {
|
|
return {
|
|
scope: {
|
|
searchFromId: "@",
|
|
searchFromName: "@",
|
|
showSearch: "@",
|
|
section: "@",
|
|
hideSearchCallback: "=",
|
|
searchCallback: "="
|
|
},
|
|
restrict: "E", // restrict to an element
|
|
replace: true, // replace the html element with the template
|
|
templateUrl: 'views/components/tree/umb-tree-search-box.html',
|
|
link: function (scope, element, attrs, ctrl) {
|
|
|
|
scope.term = "";
|
|
scope.hideSearch = function() {
|
|
scope.term = "";
|
|
scope.hideSearchCallback();
|
|
};
|
|
|
|
localizationService.localize("general_typeToSearch").then(function (value) {
|
|
scope.searchPlaceholderText = value;
|
|
});
|
|
|
|
if (!scope.showSearch) {
|
|
scope.showSearch = "false";
|
|
}
|
|
|
|
//used to cancel any request in progress if another one needs to take it's place
|
|
var canceler = null;
|
|
|
|
function performSearch() {
|
|
if (scope.term) {
|
|
scope.results = [];
|
|
|
|
//a canceler exists, so perform the cancelation operation and reset
|
|
if (canceler) {
|
|
canceler.resolve();
|
|
canceler = $q.defer();
|
|
}
|
|
else {
|
|
canceler = $q.defer();
|
|
}
|
|
|
|
var searchArgs = {
|
|
term: scope.term,
|
|
canceler: canceler
|
|
};
|
|
|
|
//append a start node context if there is one
|
|
if (scope.searchFromId) {
|
|
searchArgs["searchFrom"] = scope.searchFromId;
|
|
}
|
|
|
|
searcher(searchArgs).then(function (data) {
|
|
scope.searchCallback(data);
|
|
//set back to null so it can be re-created
|
|
canceler = null;
|
|
});
|
|
}
|
|
}
|
|
|
|
scope.$watch("term", _.debounce(function(newVal, oldVal) {
|
|
scope.$apply(function() {
|
|
if (newVal !== null && newVal !== undefined && newVal !== oldVal) {
|
|
performSearch();
|
|
}
|
|
});
|
|
}, 200));
|
|
|
|
var searcher = searchService.searchContent;
|
|
//search
|
|
if (scope.section === "member") {
|
|
searcher = searchService.searchMembers;
|
|
}
|
|
else if (scope.section === "media") {
|
|
searcher = searchService.searchMedia;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives').directive("umbTreeSearchBox", treeSearchBox);
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbTreeSearchResults
|
|
* @function
|
|
* @element ANY
|
|
* @restrict E
|
|
**/
|
|
function treeSearchResults() {
|
|
return {
|
|
scope: {
|
|
results: "=",
|
|
selectResultCallback: "="
|
|
},
|
|
restrict: "E", // restrict to an element
|
|
replace: true, // replace the html element with the template
|
|
templateUrl: 'views/components/tree/umb-tree-search-results.html',
|
|
link: function (scope, element, attrs, ctrl) {
|
|
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives').directive("umbTreeSearchResults", treeSearchResults);
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbGenerateAlias
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to generate a camelCased umbraco alias.
|
|
When the aliasFrom value is changed the directive will get a formatted alias from the server and update the alias model. If "enableLock" is set to <code>true</code>
|
|
the directive will use {@link umbraco.directives.directive:umbLockedField umbLockedField} to lock and unlock the alias.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<input type="text" ng-model="vm.name" />
|
|
|
|
<umb-generate-alias
|
|
enable-lock="true"
|
|
alias-from="vm.name"
|
|
alias="vm.alias">
|
|
</umb-generate-alias>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
|
|
vm.name = "";
|
|
vm.alias = "";
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
@param {string} alias (<code>binding</code>): The model where the alias is bound.
|
|
@param {string} aliasFrom (<code>binding</code>): The model to generate the alias from.
|
|
@param {boolean=} enableLock (<code>binding</code>): Set to <code>true</code> to add a lock next to the alias from where it can be unlocked and changed.
|
|
**/
|
|
|
|
angular.module("umbraco.directives")
|
|
.directive('umbGenerateAlias', function ($timeout, entityResource) {
|
|
return {
|
|
restrict: 'E',
|
|
templateUrl: 'views/components/umb-generate-alias.html',
|
|
replace: true,
|
|
scope: {
|
|
alias: '=',
|
|
aliasFrom: '=',
|
|
enableLock: '=?',
|
|
serverValidationField: '@'
|
|
},
|
|
link: function (scope, element, attrs, ctrl) {
|
|
|
|
var eventBindings = [];
|
|
var bindWatcher = true;
|
|
var generateAliasTimeout = "";
|
|
var updateAlias = false;
|
|
|
|
scope.locked = true;
|
|
scope.placeholderText = "Enter alias...";
|
|
|
|
function generateAlias(value) {
|
|
|
|
if (generateAliasTimeout) {
|
|
$timeout.cancel(generateAliasTimeout);
|
|
}
|
|
|
|
if( value !== undefined && value !== "" && value !== null) {
|
|
|
|
scope.alias = "";
|
|
scope.placeholderText = "Generating Alias...";
|
|
|
|
generateAliasTimeout = $timeout(function () {
|
|
updateAlias = true;
|
|
entityResource.getSafeAlias(value, true).then(function (safeAlias) {
|
|
if (updateAlias) {
|
|
scope.alias = safeAlias.alias;
|
|
}
|
|
});
|
|
}, 500);
|
|
|
|
} else {
|
|
updateAlias = true;
|
|
scope.alias = "";
|
|
scope.placeholderText = "Enter alias...";
|
|
}
|
|
|
|
}
|
|
|
|
// if alias gets unlocked - stop watching alias
|
|
eventBindings.push(scope.$watch('locked', function(newValue, oldValue){
|
|
if(newValue === false) {
|
|
bindWatcher = false;
|
|
}
|
|
}));
|
|
|
|
// validate custom entered alias
|
|
eventBindings.push(scope.$watch('alias', function(newValue, oldValue){
|
|
|
|
if(scope.alias === "" && bindWatcher === true || scope.alias === null && bindWatcher === true) {
|
|
// add watcher
|
|
eventBindings.push(scope.$watch('aliasFrom', function(newValue, oldValue) {
|
|
if(bindWatcher) {
|
|
generateAlias(newValue);
|
|
}
|
|
}));
|
|
}
|
|
|
|
}));
|
|
|
|
// clean up
|
|
scope.$on('$destroy', function(){
|
|
// unbind watchers
|
|
for(var e in eventBindings) {
|
|
eventBindings[e]();
|
|
}
|
|
});
|
|
|
|
}
|
|
};
|
|
});
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbAvatar
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to render an avatar.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-avatar
|
|
size="xs"
|
|
img-src="{{vm.avatar[0].value}}"
|
|
img-srcset="{{vm.avatar[1].value}} 2x, {{vm.avatar[2].value}} 3x">
|
|
</umb-avatar>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
|
|
vm.avatar = [
|
|
{ value: "assets/logo.png" },
|
|
{ value: "assets/logo@2x.png" },
|
|
{ value: "assets/logo@3x.png" }
|
|
];
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
|
|
})();
|
|
</pre>
|
|
|
|
@param {string} size (<code>attribute</code>): The size of the avatar (xs, s, m, l, xl).
|
|
@param {string} img-src (<code>attribute</code>): The image source to the avatar.
|
|
@param {string} img-srcset (<code>atribute</code>): Reponsive support for the image source.
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function AvatarDirective() {
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-avatar.html',
|
|
scope: {
|
|
size: "@",
|
|
imgSrc: "@",
|
|
imgSrcset: "@"
|
|
}
|
|
};
|
|
|
|
return directive;
|
|
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbAvatar', AvatarDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbChildSelector
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to render a ui component for selecting child items to a parent node.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-child-selector
|
|
selected-children="vm.selectedChildren"
|
|
available-children="vm.availableChildren"
|
|
parent-name="vm.name"
|
|
parent-icon="vm.icon"
|
|
parent-id="vm.id"
|
|
on-add="vm.addChild"
|
|
on-remove="vm.removeChild">
|
|
</umb-child-selector>
|
|
|
|
<!-- use overlay to select children from -->
|
|
<umb-overlay
|
|
ng-if="vm.overlay.show"
|
|
model="vm.overlay"
|
|
position="target"
|
|
view="vm.overlay.view">
|
|
</umb-overlay>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
|
|
vm.id = 1;
|
|
vm.name = "My Parent element";
|
|
vm.icon = "icon-document";
|
|
vm.selectedChildren = [];
|
|
vm.availableChildren = [
|
|
{
|
|
id: 1,
|
|
alias: "item1",
|
|
name: "Item 1",
|
|
icon: "icon-document"
|
|
},
|
|
{
|
|
id: 2,
|
|
alias: "item2",
|
|
name: "Item 2",
|
|
icon: "icon-document"
|
|
}
|
|
];
|
|
|
|
vm.addChild = addChild;
|
|
vm.removeChild = removeChild;
|
|
|
|
function addChild($event) {
|
|
vm.overlay = {
|
|
view: "itempicker",
|
|
title: "Choose child",
|
|
availableItems: vm.availableChildren,
|
|
selectedItems: vm.selectedChildren,
|
|
event: $event,
|
|
show: true,
|
|
submit: function(model) {
|
|
|
|
// add selected child
|
|
vm.selectedChildren.push(model.selectedItem);
|
|
|
|
// close overlay
|
|
vm.overlay.show = false;
|
|
vm.overlay = null;
|
|
}
|
|
};
|
|
}
|
|
|
|
function removeChild($index) {
|
|
vm.selectedChildren.splice($index, 1);
|
|
}
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
|
|
})();
|
|
</pre>
|
|
|
|
@param {array} selectedChildren (<code>binding</code>): Array of selected children.
|
|
@param {array} availableChildren (<code>binding</code>: Array of items available for selection.
|
|
@param {string} parentName (<code>binding</code>): The parent name.
|
|
@param {string} parentIcon (<code>binding</code>): The parent icon.
|
|
@param {number} parentId (<code>binding</code>): The parent id.
|
|
@param {callback} onRemove (<code>binding</code>): Callback when the remove button is clicked on an item.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>child</code>: The selected item.</li>
|
|
<li><code>$index</code>: The selected item index.</li>
|
|
</ul>
|
|
@param {callback} onAdd (<code>binding</code>): Callback when the add button is clicked.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>$event</code>: The select event.</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function ChildSelectorDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
var eventBindings = [];
|
|
scope.dialogModel = {};
|
|
scope.showDialog = false;
|
|
|
|
scope.removeChild = function(selectedChild, $index) {
|
|
if(scope.onRemove) {
|
|
scope.onRemove(selectedChild, $index);
|
|
}
|
|
};
|
|
|
|
scope.addChild = function($event) {
|
|
if(scope.onAdd) {
|
|
scope.onAdd($event);
|
|
}
|
|
};
|
|
|
|
function syncParentName() {
|
|
|
|
// update name on available item
|
|
angular.forEach(scope.availableChildren, function(availableChild){
|
|
if(availableChild.id === scope.parentId) {
|
|
availableChild.name = scope.parentName;
|
|
}
|
|
});
|
|
|
|
// update name on selected child
|
|
angular.forEach(scope.selectedChildren, function(selectedChild){
|
|
if(selectedChild.id === scope.parentId) {
|
|
selectedChild.name = scope.parentName;
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
function syncParentIcon() {
|
|
|
|
// update icon on available item
|
|
angular.forEach(scope.availableChildren, function(availableChild){
|
|
if(availableChild.id === scope.parentId) {
|
|
availableChild.icon = scope.parentIcon;
|
|
}
|
|
});
|
|
|
|
// update icon on selected child
|
|
angular.forEach(scope.selectedChildren, function(selectedChild){
|
|
if(selectedChild.id === scope.parentId) {
|
|
selectedChild.icon = scope.parentIcon;
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
eventBindings.push(scope.$watch('parentName', function(newValue, oldValue){
|
|
|
|
if (newValue === oldValue) { return; }
|
|
if ( oldValue === undefined || newValue === undefined) { return; }
|
|
|
|
syncParentName();
|
|
|
|
}));
|
|
|
|
eventBindings.push(scope.$watch('parentIcon', function(newValue, oldValue){
|
|
|
|
if (newValue === oldValue) { return; }
|
|
if ( oldValue === undefined || newValue === undefined) { return; }
|
|
|
|
syncParentIcon();
|
|
}));
|
|
|
|
// clean up
|
|
scope.$on('$destroy', function(){
|
|
// unbind watchers
|
|
for(var e in eventBindings) {
|
|
eventBindings[e]();
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-child-selector.html',
|
|
scope: {
|
|
selectedChildren: '=',
|
|
availableChildren: "=",
|
|
parentName: "=",
|
|
parentIcon: "=",
|
|
parentId: "=",
|
|
onRemove: "=",
|
|
onAdd: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbChildSelector', ChildSelectorDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbConfirm
|
|
* @function
|
|
* @description
|
|
* A confirmation dialog
|
|
*
|
|
* @restrict E
|
|
*/
|
|
function confirmDirective() {
|
|
return {
|
|
restrict: "E", // restrict to an element
|
|
replace: true, // replace the html element with the template
|
|
templateUrl: 'views/components/umb-confirm.html',
|
|
scope: {
|
|
onConfirm: '=',
|
|
onCancel: '=',
|
|
caption: '@'
|
|
},
|
|
link: function (scope, element, attr, ctrl) {
|
|
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives').directive("umbConfirm", confirmDirective);
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbConfirmAction
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
<p>Use this directive to toggle a confirmation prompt for an action.
|
|
The prompt consists of a checkmark and a cross to confirm or cancel the action.
|
|
The prompt can be opened in four direction up, down, left or right.</p>
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<div class="my-action" style="position:relative;">
|
|
<i class="icon-trash" ng-click="vm.showPrompt()"></i>
|
|
<umb-confirm-action
|
|
ng-if="vm.promptIsVisible"
|
|
direction="left"
|
|
on-confirm="vm.confirmAction()"
|
|
on-cancel="vm.hidePrompt()">
|
|
</umb-confirm-action>
|
|
</div>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
vm.promptIsVisible = false;
|
|
|
|
vm.confirmAction = confirmAction;
|
|
vm.showPrompt = showPrompt;
|
|
vm.hidePrompt = hidePrompt;
|
|
|
|
function confirmAction() {
|
|
// confirm logic here
|
|
}
|
|
|
|
function showPrompt() {
|
|
vm.promptIsVisible = true;
|
|
}
|
|
|
|
function hidePrompt() {
|
|
vm.promptIsVisible = false;
|
|
}
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
@param {string} direction The direction the prompt opens ("up", "down", "left", "right").
|
|
@param {callback} onConfirm Callback when the checkmark is clicked.
|
|
@param {callback} onCancel Callback when the cross is clicked.
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function ConfirmAction() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.clickConfirm = function() {
|
|
if(scope.onConfirm) {
|
|
scope.onConfirm();
|
|
}
|
|
};
|
|
|
|
scope.clickCancel = function() {
|
|
if(scope.onCancel) {
|
|
scope.onCancel();
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-confirm-action.html',
|
|
scope: {
|
|
direction: "@",
|
|
onConfirm: "&",
|
|
onCancel: "&"
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbConfirmAction', ConfirmAction);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbContentGrid
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to generate a list of content items presented as a flexbox grid.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-content-grid
|
|
content="vm.contentItems"
|
|
content-properties="vm.includeProperties"
|
|
on-click="vm.selectItem"
|
|
on-click-name="vm.clickItem">
|
|
</umb-content-grid>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
vm.contentItems = [
|
|
{
|
|
"name": "Cape",
|
|
"published": true,
|
|
"icon": "icon-document",
|
|
"updateDate": "15-02-2016",
|
|
"owner": "Mr. Batman",
|
|
"selected": false
|
|
},
|
|
{
|
|
"name": "Utility Belt",
|
|
"published": true,
|
|
"icon": "icon-document",
|
|
"updateDate": "15-02-2016",
|
|
"owner": "Mr. Batman",
|
|
"selected": false
|
|
},
|
|
{
|
|
"name": "Cave",
|
|
"published": true,
|
|
"icon": "icon-document",
|
|
"updateDate": "15-02-2016",
|
|
"owner": "Mr. Batman",
|
|
"selected": false
|
|
}
|
|
];
|
|
vm.includeProperties = [
|
|
{
|
|
"alias": "updateDate",
|
|
"header": "Last edited"
|
|
},
|
|
{
|
|
"alias": "owner",
|
|
"header": "Created by"
|
|
}
|
|
];
|
|
|
|
vm.clickItem = clickItem;
|
|
vm.selectItem = selectItem;
|
|
|
|
|
|
function clickItem(item, $event, $index){
|
|
// do magic here
|
|
}
|
|
|
|
function selectItem(item, $event, $index) {
|
|
// set item.selected = true; to select the item
|
|
// do magic here
|
|
}
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
@param {array} content (<code>binding</code>): Array of content items.
|
|
@param {array=} contentProperties (<code>binding</code>): Array of content item properties to include in the item. If left empty the item will only show the item icon and name.
|
|
@param {callback=} onClick (<code>binding</code>): Callback method to handle click events on the content item.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>item</code>: The clicked item</li>
|
|
<li><code>$event</code>: The select event</li>
|
|
<li><code>$index</code>: The item index</li>
|
|
</ul>
|
|
@param {callback=} onClickName (<code>binding</code>): Callback method to handle click events on the checkmark icon.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>item</code>: The selected item</li>
|
|
<li><code>$event</code>: The select event</li>
|
|
<li><code>$index</code>: The item index</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function ContentGridDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.clickItem = function(item, $event, $index) {
|
|
if(scope.onClick) {
|
|
scope.onClick(item, $event, $index);
|
|
}
|
|
};
|
|
|
|
scope.clickItemName = function(item, $event, $index) {
|
|
if(scope.onClickName) {
|
|
scope.onClickName(item, $event, $index);
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-content-grid.html',
|
|
scope: {
|
|
content: '=',
|
|
contentProperties: "=",
|
|
onClick: "=",
|
|
onClickName: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbContentGrid', ContentGridDirective);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function UmbDisableFormValidation() {
|
|
|
|
var directive = {
|
|
restrict: 'A',
|
|
require: '?form',
|
|
link: function (scope, elm, attrs, ctrl) {
|
|
//override the $setValidity function of the form to disable validation
|
|
ctrl.$setValidity = function () { };
|
|
}
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbDisableFormValidation', UmbDisableFormValidation);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbEmptyState
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to show an empty state message.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-empty-state
|
|
ng-if="!vm.items"
|
|
position="center">
|
|
// Empty state content
|
|
</umb-empty-state>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
@param {string=} size Set the size of the text ("small", "large").
|
|
@param {string=} position Set the position of the text ("center").
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function EmptyStateDirective() {
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
transclude: true,
|
|
templateUrl: 'views/components/umb-empty-state.html',
|
|
scope: {
|
|
size: '@',
|
|
position: '@'
|
|
}
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbEmptyState', EmptyStateDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbFolderGrid
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to generate a list of folders presented as a flexbox grid.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
<umb-folder-grid
|
|
ng-if="vm.folders.length > 0"
|
|
folders="vm.folders"
|
|
on-click="vm.clickFolder"
|
|
on-select="vm.selectFolder">
|
|
</umb-folder-grid>
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller(myService) {
|
|
|
|
var vm = this;
|
|
vm.folders = [
|
|
{
|
|
"name": "Folder 1",
|
|
"icon": "icon-folder",
|
|
"selected": false
|
|
},
|
|
{
|
|
"name": "Folder 2",
|
|
"icon": "icon-folder",
|
|
"selected": false
|
|
}
|
|
|
|
];
|
|
|
|
vm.clickFolder = clickFolder;
|
|
vm.selectFolder = selectFolder;
|
|
|
|
myService.getFolders().then(function(folders){
|
|
vm.folders = folders;
|
|
});
|
|
|
|
function clickFolder(folder){
|
|
// Execute when clicking folder name/link
|
|
}
|
|
|
|
function selectFolder(folder, event, index) {
|
|
// Execute when clicking folder
|
|
// set folder.selected = true; to show checkmark icon
|
|
}
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
@param {array} folders (<code>binding</code>): Array of folders
|
|
@param {callback=} onClick (<code>binding</code>): Callback method to handle click events on the folder.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>folder</code>: The selected folder</li>
|
|
</ul>
|
|
@param {callback=} onSelect (<code>binding</code>): Callback method to handle click events on the checkmark icon.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>folder</code>: The selected folder</li>
|
|
<li><code>$event</code>: The select event</li>
|
|
<li><code>$index</code>: The folder index</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function FolderGridDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.clickFolder = function(folder, $event, $index) {
|
|
if(scope.onClick) {
|
|
scope.onClick(folder, $event, $index);
|
|
}
|
|
};
|
|
|
|
scope.clickFolderName = function(folder, $event, $index) {
|
|
if(scope.onClickName) {
|
|
scope.onClickName(folder, $event, $index);
|
|
$event.stopPropagation();
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-folder-grid.html',
|
|
scope: {
|
|
folders: '=',
|
|
onClick: "=",
|
|
onClickName: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbFolderGrid', FolderGridDirective);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function GridSelector() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
var eventBindings = [];
|
|
scope.dialogModel = {};
|
|
scope.showDialog = false;
|
|
scope.itemLabel = "";
|
|
|
|
// set default item name
|
|
if(!scope.itemName){
|
|
scope.itemLabel = "item";
|
|
} else {
|
|
scope.itemLabel = scope.itemName;
|
|
}
|
|
|
|
scope.removeItem = function(selectedItem) {
|
|
var selectedItemIndex = scope.selectedItems.indexOf(selectedItem);
|
|
scope.selectedItems.splice(selectedItemIndex, 1);
|
|
};
|
|
|
|
scope.removeDefaultItem = function() {
|
|
|
|
// it will be the last item so we can clear the array
|
|
scope.selectedItems = [];
|
|
|
|
// remove as default item
|
|
scope.defaultItem = null;
|
|
|
|
};
|
|
|
|
scope.openItemPicker = function($event){
|
|
scope.dialogModel = {
|
|
view: "itempicker",
|
|
title: "Choose " + scope.itemLabel,
|
|
availableItems: scope.availableItems,
|
|
selectedItems: scope.selectedItems,
|
|
event: $event,
|
|
show: true,
|
|
submit: function(model) {
|
|
scope.selectedItems.push(model.selectedItem);
|
|
|
|
// if no default item - set item as default
|
|
if(scope.defaultItem === null) {
|
|
scope.setAsDefaultItem(model.selectedItem);
|
|
}
|
|
|
|
scope.dialogModel.show = false;
|
|
scope.dialogModel = null;
|
|
}
|
|
};
|
|
};
|
|
|
|
scope.setAsDefaultItem = function(selectedItem) {
|
|
|
|
// clear default item
|
|
scope.defaultItem = {};
|
|
|
|
// set as default item
|
|
scope.defaultItem = selectedItem;
|
|
};
|
|
|
|
function updatePlaceholders() {
|
|
|
|
// update default item
|
|
if(scope.defaultItem !== null && scope.defaultItem.placeholder) {
|
|
|
|
scope.defaultItem.name = scope.name;
|
|
|
|
if(scope.alias !== null && scope.alias !== undefined) {
|
|
scope.defaultItem.alias = scope.alias;
|
|
}
|
|
|
|
}
|
|
|
|
// update selected items
|
|
angular.forEach(scope.selectedItems, function(selectedItem) {
|
|
if(selectedItem.placeholder) {
|
|
|
|
selectedItem.name = scope.name;
|
|
|
|
if(scope.alias !== null && scope.alias !== undefined) {
|
|
selectedItem.alias = scope.alias;
|
|
}
|
|
|
|
}
|
|
});
|
|
|
|
// update availableItems
|
|
angular.forEach(scope.availableItems, function(availableItem) {
|
|
if(availableItem.placeholder) {
|
|
|
|
availableItem.name = scope.name;
|
|
|
|
if(scope.alias !== null && scope.alias !== undefined) {
|
|
availableItem.alias = scope.alias;
|
|
}
|
|
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
function activate() {
|
|
|
|
// add watchers for updating placeholde name and alias
|
|
if(scope.updatePlaceholder) {
|
|
eventBindings.push(scope.$watch('name', function(newValue, oldValue){
|
|
updatePlaceholders();
|
|
}));
|
|
|
|
eventBindings.push(scope.$watch('alias', function(newValue, oldValue){
|
|
updatePlaceholders();
|
|
}));
|
|
}
|
|
|
|
}
|
|
|
|
activate();
|
|
|
|
// clean up
|
|
scope.$on('$destroy', function(){
|
|
|
|
// clear watchers
|
|
for(var e in eventBindings) {
|
|
eventBindings[e]();
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-grid-selector.html',
|
|
scope: {
|
|
name: "=",
|
|
alias: "=",
|
|
selectedItems: '=',
|
|
availableItems: "=",
|
|
defaultItem: "=",
|
|
itemName: "@",
|
|
updatePlaceholder: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbGridSelector', GridSelector);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
var validationTranslated = "";
|
|
var tabNoSortOrderTranslated = "";
|
|
|
|
scope.sortingMode = false;
|
|
scope.toolbar = [];
|
|
scope.sortableOptionsGroup = {};
|
|
scope.sortableOptionsProperty = {};
|
|
scope.sortingButtonKey = "general_reorder";
|
|
|
|
function activate() {
|
|
|
|
setSortingOptions();
|
|
|
|
// set placeholder property on each group
|
|
if (scope.model.groups.length !== 0) {
|
|
angular.forEach(scope.model.groups, function(group) {
|
|
addInitProperty(group);
|
|
});
|
|
}
|
|
|
|
// add init tab
|
|
addInitGroup(scope.model.groups);
|
|
|
|
activateFirstGroup(scope.model.groups);
|
|
|
|
// localize texts
|
|
localizationService.localize("validation_validation").then(function(value) {
|
|
validationTranslated = value;
|
|
});
|
|
|
|
localizationService.localize("contentTypeEditor_tabHasNoSortOrder").then(function(value) {
|
|
tabNoSortOrderTranslated = value;
|
|
});
|
|
}
|
|
|
|
function setSortingOptions() {
|
|
|
|
scope.sortableOptionsGroup = {
|
|
distance: 10,
|
|
tolerance: "pointer",
|
|
opacity: 0.7,
|
|
scroll: true,
|
|
cursor: "move",
|
|
placeholder: "umb-group-builder__group-sortable-placeholder",
|
|
zIndex: 6000,
|
|
handle: ".umb-group-builder__group-handle",
|
|
items: ".umb-group-builder__group-sortable",
|
|
start: function(e, ui) {
|
|
ui.placeholder.height(ui.item.height());
|
|
},
|
|
stop: function(e, ui) {
|
|
updateTabsSortOrder();
|
|
},
|
|
};
|
|
|
|
scope.sortableOptionsProperty = {
|
|
distance: 10,
|
|
tolerance: "pointer",
|
|
connectWith: ".umb-group-builder__properties",
|
|
opacity: 0.7,
|
|
scroll: true,
|
|
cursor: "move",
|
|
placeholder: "umb-group-builder__property_sortable-placeholder",
|
|
zIndex: 6000,
|
|
handle: ".umb-group-builder__property-handle",
|
|
items: ".umb-group-builder__property-sortable",
|
|
start: function(e, ui) {
|
|
ui.placeholder.height(ui.item.height());
|
|
},
|
|
stop: function(e, ui) {
|
|
updatePropertiesSortOrder();
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
function updateTabsSortOrder() {
|
|
|
|
var first = true;
|
|
var prevSortOrder = 0;
|
|
|
|
scope.model.groups.map(function(group){
|
|
|
|
var index = scope.model.groups.indexOf(group);
|
|
|
|
if(group.tabState !== "init") {
|
|
|
|
// set the first not inherited tab to sort order 0
|
|
if(!group.inherited && first) {
|
|
|
|
// set the first tab sort order to 0 if prev is 0
|
|
if( prevSortOrder === 0 ) {
|
|
group.sortOrder = 0;
|
|
// when the first tab is inherited and sort order is not 0
|
|
} else {
|
|
group.sortOrder = prevSortOrder + 1;
|
|
}
|
|
|
|
first = false;
|
|
|
|
} else if(!group.inherited && !first) {
|
|
|
|
// find next group
|
|
var nextGroup = scope.model.groups[index + 1];
|
|
|
|
// if a groups is dropped in the middle of to groups with
|
|
// same sort order. Give it the dropped group same sort order
|
|
if( prevSortOrder === nextGroup.sortOrder ) {
|
|
group.sortOrder = prevSortOrder;
|
|
} else {
|
|
group.sortOrder = prevSortOrder + 1;
|
|
}
|
|
|
|
}
|
|
|
|
// store this tabs sort order as reference for the next
|
|
prevSortOrder = group.sortOrder;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
function filterAvailableCompositions(selectedContentType, selecting) {
|
|
|
|
//selecting = true if the user has check the item, false if the user has unchecked the item
|
|
|
|
var selectedContentTypeAliases = selecting ?
|
|
//the user has selected the item so add to the current list
|
|
_.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) :
|
|
//the user has unselected the item so remove from the current list
|
|
_.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) {
|
|
return i === selectedContentType.alias;
|
|
});
|
|
|
|
//get the currently assigned property type aliases - ensure we pass these to the server side filer
|
|
var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
|
|
return _.map(g.properties, function(p) {
|
|
return p.alias;
|
|
});
|
|
})), function (f) {
|
|
return f !== null && f !== undefined;
|
|
});
|
|
|
|
//use a different resource lookup depending on the content type type
|
|
var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
|
|
|
|
return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) {
|
|
_.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) {
|
|
//reset first
|
|
current.allowed = true;
|
|
//see if this list item is found in the response (allowed) list
|
|
var found = _.find(filteredAvailableCompositeTypes, function (f) {
|
|
return current.contentType.alias === f.contentType.alias;
|
|
});
|
|
|
|
//allow if the item was found in the response (allowed) list -
|
|
// and ensure its set to allowed if it is currently checked,
|
|
// DO not allow if it's a locked content type.
|
|
current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 &&
|
|
(selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || ((found !== null && found !== undefined) ? found.allowed : false);
|
|
|
|
});
|
|
});
|
|
}
|
|
|
|
function updatePropertiesSortOrder() {
|
|
|
|
angular.forEach(scope.model.groups, function(group){
|
|
if( group.tabState !== "init" ) {
|
|
group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties);
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
function setupAvailableContentTypesModel(result) {
|
|
scope.compositionsDialogModel.availableCompositeContentTypes = result;
|
|
//iterate each one and set it up
|
|
_.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) {
|
|
//enable it if it's part of the selected model
|
|
if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) {
|
|
c.allowed = true;
|
|
}
|
|
|
|
//set the inherited flags
|
|
c.inherited = false;
|
|
if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) {
|
|
c.inherited = true;
|
|
}
|
|
// convert icons for composite content types
|
|
iconHelper.formatContentTypeIcons([c.contentType]);
|
|
});
|
|
}
|
|
|
|
/* ---------- DELETE PROMT ---------- */
|
|
|
|
scope.togglePrompt = function (object) {
|
|
object.deletePrompt = !object.deletePrompt;
|
|
};
|
|
|
|
scope.hidePrompt = function (object) {
|
|
object.deletePrompt = false;
|
|
};
|
|
|
|
/* ---------- TOOLBAR ---------- */
|
|
|
|
scope.toggleSortingMode = function(tool) {
|
|
|
|
if (scope.sortingMode === true) {
|
|
|
|
var sortOrderMissing = false;
|
|
|
|
for (var i = 0; i < scope.model.groups.length; i++) {
|
|
var group = scope.model.groups[i];
|
|
if (group.tabState !== "init" && group.sortOrder === undefined) {
|
|
sortOrderMissing = true;
|
|
group.showSortOrderMissing = true;
|
|
notificationsService.error(validationTranslated + ": " + group.name + " " + tabNoSortOrderTranslated);
|
|
}
|
|
}
|
|
|
|
if (!sortOrderMissing) {
|
|
scope.sortingMode = false;
|
|
scope.sortingButtonKey = "general_reorder";
|
|
}
|
|
|
|
} else {
|
|
|
|
scope.sortingMode = true;
|
|
scope.sortingButtonKey = "general_reorderDone";
|
|
|
|
}
|
|
|
|
};
|
|
|
|
scope.openCompositionsDialog = function() {
|
|
|
|
scope.compositionsDialogModel = {
|
|
title: "Compositions",
|
|
contentType: scope.model,
|
|
compositeContentTypes: scope.model.compositeContentTypes,
|
|
view: "views/common/overlays/contenttypeeditor/compositions/compositions.html",
|
|
confirmSubmit: {
|
|
title: "Warning",
|
|
description: "Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?",
|
|
checkboxLabel: "I know what I'm doing",
|
|
enable: true
|
|
},
|
|
submit: function(model, oldModel, confirmed) {
|
|
|
|
var compositionRemoved = false;
|
|
|
|
// check if any compositions has been removed
|
|
for(var i = 0; oldModel.compositeContentTypes.length > i; i++) {
|
|
|
|
var oldComposition = oldModel.compositeContentTypes[i];
|
|
|
|
if(_.contains(model.compositeContentTypes, oldComposition) === false) {
|
|
compositionRemoved = true;
|
|
}
|
|
|
|
}
|
|
|
|
// show overlay confirm box if compositions has been removed.
|
|
if(compositionRemoved && confirmed === false) {
|
|
|
|
scope.compositionsDialogModel.confirmSubmit.show = true;
|
|
|
|
// submit overlay if no compositions has been removed
|
|
// or the action has been confirmed
|
|
} else {
|
|
|
|
// make sure that all tabs has an init property
|
|
if (scope.model.groups.length !== 0) {
|
|
angular.forEach(scope.model.groups, function(group) {
|
|
addInitProperty(group);
|
|
});
|
|
}
|
|
|
|
// remove overlay
|
|
scope.compositionsDialogModel.show = false;
|
|
scope.compositionsDialogModel = null;
|
|
}
|
|
|
|
},
|
|
close: function(oldModel) {
|
|
|
|
// reset composition changes
|
|
scope.model.groups = oldModel.contentType.groups;
|
|
scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes;
|
|
|
|
// remove overlay
|
|
scope.compositionsDialogModel.show = false;
|
|
scope.compositionsDialogModel = null;
|
|
|
|
},
|
|
selectCompositeContentType: function (selectedContentType) {
|
|
|
|
//first check if this is a new selection - we need to store this value here before any further digests/async
|
|
// because after that the scope.model.compositeContentTypes will be populated with the selected value.
|
|
var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1;
|
|
|
|
if (newSelection) {
|
|
//merge composition with content type
|
|
|
|
//use a different resource lookup depending on the content type type
|
|
var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById;
|
|
|
|
resourceLookup(selectedContentType.id).then(function (composition) {
|
|
//based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and
|
|
// double check here.
|
|
var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition);
|
|
if (overlappingAliases.length > 0) {
|
|
//this will create an invalid composition, need to uncheck it
|
|
scope.compositionsDialogModel.compositeContentTypes.splice(
|
|
scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1);
|
|
//dissallow this until something else is unchecked
|
|
selectedContentType.allowed = false;
|
|
}
|
|
else {
|
|
contentTypeHelper.mergeCompositeContentType(scope.model, composition);
|
|
}
|
|
|
|
//based on the selection, we need to filter the available composite types list
|
|
filterAvailableCompositions(selectedContentType, newSelection).then(function () {
|
|
//TODO: Here we could probably re-enable selection if we previously showed a throbber or something
|
|
});
|
|
});
|
|
}
|
|
else {
|
|
// split composition from content type
|
|
contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType);
|
|
|
|
//based on the selection, we need to filter the available composite types list
|
|
filterAvailableCompositions(selectedContentType, newSelection).then(function () {
|
|
//TODO: Here we could probably re-enable selection if we previously showed a throbber or something
|
|
});
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
|
|
var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount;
|
|
|
|
//get the currently assigned property type aliases - ensure we pass these to the server side filer
|
|
var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
|
|
return _.map(g.properties, function(p) {
|
|
return p.alias;
|
|
});
|
|
})), function(f) {
|
|
return f !== null && f !== undefined;
|
|
});
|
|
$q.all([
|
|
//get available composite types
|
|
availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) {
|
|
setupAvailableContentTypesModel(result);
|
|
}),
|
|
//get content type count
|
|
countContentTypeResource().then(function(result) {
|
|
scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10);
|
|
})
|
|
]).then(function() {
|
|
//resolves when both other promises are done, now show it
|
|
scope.compositionsDialogModel.show = true;
|
|
});
|
|
|
|
};
|
|
|
|
|
|
/* ---------- GROUPS ---------- */
|
|
|
|
scope.addGroup = function(group) {
|
|
|
|
// set group sort order
|
|
var index = scope.model.groups.indexOf(group);
|
|
var prevGroup = scope.model.groups[index - 1];
|
|
|
|
if( index > 0) {
|
|
// set index to 1 higher than the previous groups sort order
|
|
group.sortOrder = prevGroup.sortOrder + 1;
|
|
|
|
} else {
|
|
// first group - sort order will be 0
|
|
group.sortOrder = 0;
|
|
}
|
|
|
|
// activate group
|
|
scope.activateGroup(group);
|
|
|
|
};
|
|
|
|
scope.activateGroup = function(selectedGroup) {
|
|
|
|
// set all other groups that are inactive to active
|
|
angular.forEach(scope.model.groups, function(group) {
|
|
// skip init tab
|
|
if (group.tabState !== "init") {
|
|
group.tabState = "inActive";
|
|
}
|
|
});
|
|
|
|
selectedGroup.tabState = "active";
|
|
|
|
};
|
|
|
|
scope.removeGroup = function(groupIndex) {
|
|
scope.model.groups.splice(groupIndex, 1);
|
|
addInitGroup(scope.model.groups);
|
|
};
|
|
|
|
scope.updateGroupTitle = function(group) {
|
|
if (group.properties.length === 0) {
|
|
addInitProperty(group);
|
|
}
|
|
};
|
|
|
|
scope.changeSortOrderValue = function(group) {
|
|
|
|
if (group.sortOrder !== undefined) {
|
|
group.showSortOrderMissing = false;
|
|
}
|
|
scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder');
|
|
};
|
|
|
|
function addInitGroup(groups) {
|
|
|
|
// check i init tab already exists
|
|
var addGroup = true;
|
|
|
|
angular.forEach(groups, function(group) {
|
|
if (group.tabState === "init") {
|
|
addGroup = false;
|
|
}
|
|
});
|
|
|
|
if (addGroup) {
|
|
groups.push({
|
|
properties: [],
|
|
parentTabContentTypes: [],
|
|
parentTabContentTypeNames: [],
|
|
name: "",
|
|
tabState: "init"
|
|
});
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
function activateFirstGroup(groups) {
|
|
if (groups && groups.length > 0) {
|
|
var firstGroup = groups[0];
|
|
if(!firstGroup.tabState || firstGroup.tabState === "inActive") {
|
|
firstGroup.tabState = "active";
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ---------- PROPERTIES ---------- */
|
|
|
|
scope.addProperty = function(property, group) {
|
|
|
|
// set property sort order
|
|
var index = group.properties.indexOf(property);
|
|
var prevProperty = group.properties[index - 1];
|
|
|
|
if( index > 0) {
|
|
// set index to 1 higher than the previous property sort order
|
|
property.sortOrder = prevProperty.sortOrder + 1;
|
|
|
|
} else {
|
|
// first property - sort order will be 0
|
|
property.sortOrder = 0;
|
|
}
|
|
|
|
// open property settings dialog
|
|
scope.editPropertyTypeSettings(property, group);
|
|
|
|
};
|
|
|
|
scope.editPropertyTypeSettings = function(property, group) {
|
|
|
|
if (!property.inherited && !property.locked) {
|
|
|
|
scope.propertySettingsDialogModel = {};
|
|
scope.propertySettingsDialogModel.title = "Property settings";
|
|
scope.propertySettingsDialogModel.property = property;
|
|
scope.propertySettingsDialogModel.contentType = scope.contentType;
|
|
scope.propertySettingsDialogModel.contentTypeName = scope.model.name;
|
|
scope.propertySettingsDialogModel.view = "views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html";
|
|
scope.propertySettingsDialogModel.show = true;
|
|
|
|
// set state to active to access the preview
|
|
property.propertyState = "active";
|
|
|
|
// set property states
|
|
property.dialogIsOpen = true;
|
|
|
|
scope.propertySettingsDialogModel.submit = function(model) {
|
|
|
|
property.inherited = false;
|
|
property.dialogIsOpen = false;
|
|
|
|
// update existing data types
|
|
if(model.updateSameDataTypes) {
|
|
updateSameDataTypes(property);
|
|
}
|
|
|
|
// remove dialog
|
|
scope.propertySettingsDialogModel.show = false;
|
|
scope.propertySettingsDialogModel = null;
|
|
|
|
// push new init property to group
|
|
addInitProperty(group);
|
|
|
|
// set focus on init property
|
|
var numberOfProperties = group.properties.length;
|
|
group.properties[numberOfProperties - 1].focus = true;
|
|
|
|
// push new init tab to the scope
|
|
addInitGroup(scope.model.groups);
|
|
|
|
};
|
|
|
|
scope.propertySettingsDialogModel.close = function(oldModel) {
|
|
|
|
// reset all property changes
|
|
property.label = oldModel.property.label;
|
|
property.alias = oldModel.property.alias;
|
|
property.description = oldModel.property.description;
|
|
property.config = oldModel.property.config;
|
|
property.editor = oldModel.property.editor;
|
|
property.view = oldModel.property.view;
|
|
property.dataTypeId = oldModel.property.dataTypeId;
|
|
property.dataTypeIcon = oldModel.property.dataTypeIcon;
|
|
property.dataTypeName = oldModel.property.dataTypeName;
|
|
property.validation.mandatory = oldModel.property.validation.mandatory;
|
|
property.validation.pattern = oldModel.property.validation.pattern;
|
|
property.showOnMemberProfile = oldModel.property.showOnMemberProfile;
|
|
property.memberCanEdit = oldModel.property.memberCanEdit;
|
|
|
|
// because we set state to active, to show a preview, we have to check if has been filled out
|
|
// label is required so if it is not filled we know it is a placeholder
|
|
if(oldModel.property.editor === undefined || oldModel.property.editor === null || oldModel.property.editor === "") {
|
|
property.propertyState = "init";
|
|
} else {
|
|
property.propertyState = oldModel.property.propertyState;
|
|
}
|
|
|
|
// remove dialog
|
|
scope.propertySettingsDialogModel.show = false;
|
|
scope.propertySettingsDialogModel = null;
|
|
|
|
};
|
|
|
|
}
|
|
};
|
|
|
|
scope.deleteProperty = function(tab, propertyIndex) {
|
|
|
|
// remove property
|
|
tab.properties.splice(propertyIndex, 1);
|
|
|
|
// if the last property in group is an placeholder - remove add new tab placeholder
|
|
if(tab.properties.length === 1 && tab.properties[0].propertyState === "init") {
|
|
|
|
angular.forEach(scope.model.groups, function(group, index, groups){
|
|
if(group.tabState === 'init') {
|
|
groups.splice(index, 1);
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
function addInitProperty(group) {
|
|
|
|
var addInitPropertyBool = true;
|
|
var initProperty = {
|
|
label: null,
|
|
alias: null,
|
|
propertyState: "init",
|
|
validation: {
|
|
mandatory: false,
|
|
pattern: null
|
|
}
|
|
};
|
|
|
|
// check if there already is an init property
|
|
angular.forEach(group.properties, function(property) {
|
|
if (property.propertyState === "init") {
|
|
addInitPropertyBool = false;
|
|
}
|
|
});
|
|
|
|
if (addInitPropertyBool) {
|
|
group.properties.push(initProperty);
|
|
}
|
|
|
|
return group;
|
|
}
|
|
|
|
function updateSameDataTypes(newProperty) {
|
|
|
|
// find each property
|
|
angular.forEach(scope.model.groups, function(group){
|
|
angular.forEach(group.properties, function(property){
|
|
|
|
if(property.dataTypeId === newProperty.dataTypeId) {
|
|
|
|
// update property data
|
|
property.config = newProperty.config;
|
|
property.editor = newProperty.editor;
|
|
property.view = newProperty.view;
|
|
property.dataTypeId = newProperty.dataTypeId;
|
|
property.dataTypeIcon = newProperty.dataTypeIcon;
|
|
property.dataTypeName = newProperty.dataTypeName;
|
|
|
|
}
|
|
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
var unbindModelWatcher = scope.$watch('model', function(newValue, oldValue) {
|
|
if (newValue !== undefined && newValue.groups !== undefined) {
|
|
activate();
|
|
}
|
|
});
|
|
|
|
// clean up
|
|
scope.$on('$destroy', function(){
|
|
unbindModelWatcher();
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: "E",
|
|
replace: true,
|
|
templateUrl: "views/components/umb-groups-builder.html",
|
|
scope: {
|
|
model: "=",
|
|
compositions: "=",
|
|
sorting: "=",
|
|
contentType: "@"
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbkeyboardShortcutsOverview
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
|
|
<p>Use this directive to show an overview of keyboard shortcuts in an editor.
|
|
The directive will render an overview trigger wich shows how the overview is opened.
|
|
When this combination is hit an overview is opened with shortcuts based on the model sent to the directive.</p>
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-keyboard-shortcuts-overview
|
|
model="vm.keyboardShortcutsOverview">
|
|
</umb-keyboard-shortcuts-overview>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
|
|
vm.keyboardShortcutsOverview = [
|
|
{
|
|
"name": "Sections",
|
|
"shortcuts": [
|
|
{
|
|
"description": "Navigate sections",
|
|
"keys": [
|
|
{"key": "1"},
|
|
{"key": "4"}
|
|
],
|
|
"keyRange": true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"name": "Design",
|
|
"shortcuts": [
|
|
{
|
|
"description": "Add tab",
|
|
"keys": [
|
|
{"key": "alt"},
|
|
{"key": "shift"},
|
|
{"key": "t"}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
<h3>Model description</h3>
|
|
<ul>
|
|
<li>
|
|
<strong>name</strong>
|
|
<small>(string)</small> -
|
|
Sets the shortcut section name.
|
|
</li>
|
|
<li>
|
|
<strong>shortcuts</strong>
|
|
<small>(array)</small> -
|
|
Array of available shortcuts in the section.
|
|
</li>
|
|
<ul>
|
|
<li>
|
|
<strong>description</strong>
|
|
<small>(string)</small> -
|
|
Short description of the shortcut.
|
|
</li>
|
|
<li>
|
|
<strong>keys</strong>
|
|
<small>(array)</small> -
|
|
Array of keys in the shortcut.
|
|
</li>
|
|
<ul>
|
|
<li>
|
|
<strong>key</strong>
|
|
<small>(string)</small> -
|
|
The invidual key in the shortcut.
|
|
</li>
|
|
</ul>
|
|
<li>
|
|
<strong>keyRange</strong>
|
|
<small>(boolean)</small> -
|
|
Set to <code>true</code> to show a key range. It combines the shortcut keys with "-" instead of "+".
|
|
</li>
|
|
</ul>
|
|
</ul>
|
|
|
|
@param {object} model keyboard shortcut model. See description and example above.
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function KeyboardShortcutsOverviewDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.shortcutOverlay = false;
|
|
|
|
scope.toggleShortcutsOverlay = function() {
|
|
scope.shortcutOverlay = !scope.shortcutOverlay;
|
|
};
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-keyboard-shortcuts-overview.html',
|
|
link: link,
|
|
scope: {
|
|
model: "="
|
|
}
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbKeyboardShortcutsOverview', KeyboardShortcutsOverviewDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbLaunchMiniEditor
|
|
* @restrict E
|
|
* @function
|
|
* @description
|
|
* Used on a button to launch a mini content editor editor dialog
|
|
**/
|
|
angular.module("umbraco.directives")
|
|
.directive('umbLaunchMiniEditor', function (dialogService, editorState, fileManager, contentEditingHelper) {
|
|
return {
|
|
restrict: 'A',
|
|
replace: false,
|
|
scope: {
|
|
node: '=umbLaunchMiniEditor',
|
|
},
|
|
link: function(scope, element, attrs) {
|
|
|
|
var launched = false;
|
|
|
|
element.click(function() {
|
|
|
|
if (launched === true) {
|
|
return;
|
|
}
|
|
|
|
launched = true;
|
|
|
|
//We need to store the current files selected in the file manager locally because the fileManager
|
|
// is a singleton and is shared globally. The mini dialog will also be referencing the fileManager
|
|
// and we don't want it to be sharing the same files as the main editor. So we'll store the current files locally here,
|
|
// clear them out and then launch the dialog. When the dialog closes, we'll reset the fileManager to it's previous state.
|
|
var currFiles = _.groupBy(fileManager.getFiles(), "alias");
|
|
fileManager.clearFiles();
|
|
|
|
//We need to store the original editorState entity because it will need to change when the mini editor is loaded so that
|
|
// any property editors that are working with editorState get given the correct entity, otherwise strange things will
|
|
// start happening.
|
|
var currEditorState = editorState.getCurrent();
|
|
|
|
dialogService.open({
|
|
template: "views/common/dialogs/content/edit.html",
|
|
id: scope.node.id,
|
|
closeOnSave: true,
|
|
tabFilter: ["Generic properties"],
|
|
callback: function (data) {
|
|
|
|
//set the node name back
|
|
scope.node.name = data.name;
|
|
|
|
//reset the fileManager to what it was
|
|
fileManager.clearFiles();
|
|
_.each(currFiles, function (val, key) {
|
|
fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; }));
|
|
});
|
|
|
|
//reset the editor state
|
|
editorState.set(currEditorState);
|
|
|
|
//Now we need to check if the content item that was edited was actually the same content item
|
|
// as the main content editor and if so, update all property data
|
|
if (data.id === currEditorState.id) {
|
|
var changed = contentEditingHelper.reBindChangedProperties(currEditorState, data);
|
|
}
|
|
|
|
launched = false;
|
|
},
|
|
closeCallback: function () {
|
|
//reset the fileManager to what it was
|
|
fileManager.clearFiles();
|
|
_.each(currFiles, function (val, key) {
|
|
fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; }));
|
|
});
|
|
|
|
//reset the editor state
|
|
editorState.set(currEditorState);
|
|
|
|
launched = false;
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
};
|
|
});
|
|
(function() {
|
|
'use strict';
|
|
|
|
function LayoutSelectorDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.layoutDropDownIsOpen = false;
|
|
scope.showLayoutSelector = true;
|
|
|
|
function activate() {
|
|
|
|
setVisibility();
|
|
|
|
setActiveLayout(scope.layouts);
|
|
|
|
}
|
|
|
|
function setVisibility() {
|
|
|
|
var numberOfAllowedLayouts = getNumberOfAllowedLayouts(scope.layouts);
|
|
|
|
if(numberOfAllowedLayouts === 1) {
|
|
scope.showLayoutSelector = false;
|
|
}
|
|
|
|
}
|
|
|
|
function getNumberOfAllowedLayouts(layouts) {
|
|
|
|
var allowedLayouts = 0;
|
|
|
|
for (var i = 0; layouts.length > i; i++) {
|
|
|
|
var layout = layouts[i];
|
|
|
|
if(layout.selected === true) {
|
|
allowedLayouts++;
|
|
}
|
|
|
|
}
|
|
|
|
return allowedLayouts;
|
|
}
|
|
|
|
function setActiveLayout(layouts) {
|
|
|
|
for (var i = 0; layouts.length > i; i++) {
|
|
var layout = layouts[i];
|
|
if(layout.path === scope.activeLayout.path) {
|
|
layout.active = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
scope.pickLayout = function(selectedLayout) {
|
|
if(scope.onLayoutSelect) {
|
|
scope.onLayoutSelect(selectedLayout);
|
|
scope.layoutDropDownIsOpen = false;
|
|
}
|
|
};
|
|
|
|
scope.toggleLayoutDropdown = function() {
|
|
scope.layoutDropDownIsOpen = !scope.layoutDropDownIsOpen;
|
|
};
|
|
|
|
scope.closeLayoutDropdown = function() {
|
|
scope.layoutDropDownIsOpen = false;
|
|
};
|
|
|
|
activate();
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-layout-selector.html',
|
|
scope: {
|
|
layouts: '=',
|
|
activeLayout: '=',
|
|
onLayoutSelect: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbLayoutSelector', LayoutSelectorDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbLightbox
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
<p>Use this directive to open a gallery in a lightbox overlay.</p>
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<div class="my-gallery">
|
|
<a href="" ng-repeat="image in images" ng-click="vm.openLightbox($index, images)">
|
|
<img ng-src="image.source" />
|
|
</a>
|
|
</div>
|
|
|
|
<umb-lightbox
|
|
ng-if="vm.lightbox.show"
|
|
items="vm.lightbox.items"
|
|
active-item-index="vm.lightbox.activeIndex"
|
|
on-close="vm.closeLightbox">
|
|
</umb-lightbox>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
|
|
vm.images = [
|
|
{
|
|
"source": "linkToImage"
|
|
},
|
|
{
|
|
"source": "linkToImage"
|
|
}
|
|
]
|
|
|
|
vm.openLightbox = openLightbox;
|
|
vm.closeLightbox = closeLightbox;
|
|
|
|
function openLightbox(itemIndex, items) {
|
|
vm.lightbox = {
|
|
show: true,
|
|
items: items,
|
|
activeIndex: itemIndex
|
|
};
|
|
}
|
|
|
|
function closeLightbox() {
|
|
vm.lightbox.show = false;
|
|
vm.lightbox = null;
|
|
}
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
@param {array} items Array of gallery items.
|
|
@param {callback} onClose Callback when the lightbox is closed.
|
|
@param {number} activeItemIndex Index of active item.
|
|
**/
|
|
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function LightboxDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
|
|
function activate() {
|
|
|
|
var eventBindings = [];
|
|
|
|
el.appendTo("body");
|
|
|
|
// clean up
|
|
scope.$on('$destroy', function() {
|
|
// unbind watchers
|
|
for (var e in eventBindings) {
|
|
eventBindings[e]();
|
|
}
|
|
});
|
|
}
|
|
|
|
scope.next = function() {
|
|
|
|
var nextItemIndex = scope.activeItemIndex + 1;
|
|
|
|
if( nextItemIndex < scope.items.length) {
|
|
scope.items[scope.activeItemIndex].active = false;
|
|
scope.items[nextItemIndex].active = true;
|
|
scope.activeItemIndex = nextItemIndex;
|
|
}
|
|
};
|
|
|
|
scope.prev = function() {
|
|
|
|
var prevItemIndex = scope.activeItemIndex - 1;
|
|
|
|
if( prevItemIndex >= 0) {
|
|
scope.items[scope.activeItemIndex].active = false;
|
|
scope.items[prevItemIndex].active = true;
|
|
scope.activeItemIndex = prevItemIndex;
|
|
}
|
|
|
|
};
|
|
|
|
scope.close = function() {
|
|
if(scope.onClose) {
|
|
scope.onClose();
|
|
}
|
|
};
|
|
|
|
activate();
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-lightbox.html',
|
|
scope: {
|
|
items: '=',
|
|
onClose: "=",
|
|
activeItemIndex: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbLightbox', LightboxDirective);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function ListViewLayoutDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.getContent = function(contentId) {
|
|
if(scope.onGetContent) {
|
|
scope.onGetContent(contentId);
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-list-view-layout.html',
|
|
scope: {
|
|
contentId: '=',
|
|
folders: '=',
|
|
items: '=',
|
|
selection: '=',
|
|
options: '=',
|
|
entityType: '@',
|
|
onGetContent: '='
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbListViewLayout', ListViewLayoutDirective);
|
|
|
|
})();
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper, listViewPrevalueHelper) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.dataType = {};
|
|
scope.editDataTypeSettings = false;
|
|
scope.customListViewCreated = false;
|
|
|
|
/* ---------- INIT ---------- */
|
|
|
|
function activate() {
|
|
|
|
if(scope.enableListView) {
|
|
|
|
dataTypeResource.getByName(scope.listViewName)
|
|
.then(function(dataType) {
|
|
|
|
scope.dataType = dataType;
|
|
|
|
listViewPrevalueHelper.setPrevalues(dataType.preValues);
|
|
scope.customListViewCreated = checkForCustomListView();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
scope.dataType = {};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
/* ----------- LIST VIEW SETTINGS --------- */
|
|
|
|
scope.toggleEditListViewDataTypeSettings = function() {
|
|
scope.editDataTypeSettings = !scope.editDataTypeSettings;
|
|
};
|
|
|
|
scope.saveListViewDataType = function() {
|
|
|
|
var preValues = dataTypeHelper.createPreValueProps(scope.dataType.preValues);
|
|
|
|
dataTypeResource.save(scope.dataType, preValues, false).then(function(dataType) {
|
|
|
|
// store data type
|
|
scope.dataType = dataType;
|
|
|
|
// hide settings panel
|
|
scope.editDataTypeSettings = false;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
/* ---------- CUSTOM LIST VIEW ---------- */
|
|
|
|
scope.createCustomListViewDataType = function() {
|
|
|
|
dataTypeResource.createCustomListView(scope.modelAlias).then(function(dataType) {
|
|
|
|
// store data type
|
|
scope.dataType = dataType;
|
|
|
|
// set list view name on scope
|
|
scope.listViewName = dataType.name;
|
|
|
|
// change state to custom list view
|
|
scope.customListViewCreated = true;
|
|
|
|
// show settings panel
|
|
scope.editDataTypeSettings = true;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
scope.removeCustomListDataType = function() {
|
|
|
|
scope.editDataTypeSettings = false;
|
|
|
|
// delete custom list view data type
|
|
dataTypeResource.deleteById(scope.dataType.id).then(function(dataType) {
|
|
|
|
// set list view name on scope
|
|
if(scope.contentType === "documentType") {
|
|
|
|
scope.listViewName = "List View - Content";
|
|
|
|
} else if(scope.contentType === "mediaType") {
|
|
|
|
scope.listViewName = "List View - Media";
|
|
|
|
}
|
|
|
|
// get default data type
|
|
dataTypeResource.getByName(scope.listViewName)
|
|
.then(function(dataType) {
|
|
|
|
// store data type
|
|
scope.dataType = dataType;
|
|
|
|
// change state to default list view
|
|
scope.customListViewCreated = false;
|
|
|
|
});
|
|
});
|
|
|
|
};
|
|
|
|
/* ----------- SCOPE WATCHERS ----------- */
|
|
var unbindEnableListViewWatcher = scope.$watch('enableListView', function(newValue, oldValue){
|
|
|
|
if(newValue !== undefined) {
|
|
activate();
|
|
}
|
|
|
|
});
|
|
|
|
// clean up
|
|
scope.$on('$destroy', function(){
|
|
unbindEnableListViewWatcher();
|
|
});
|
|
|
|
/* ----------- METHODS ---------- */
|
|
|
|
function checkForCustomListView() {
|
|
return scope.dataType.name === "List View - " + scope.modelAlias;
|
|
}
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-list-view-settings.html',
|
|
scope: {
|
|
enableListView: "=",
|
|
listViewName: "=",
|
|
modelAlias: "=",
|
|
contentType: "@"
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbListViewSettings', ListViewSettingsDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbLoadIndicator
|
|
@restrict E
|
|
|
|
@description
|
|
Use this directive to generate a loading indicator.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-load-indicator
|
|
ng-if="vm.loading">
|
|
</umb-load-indicator>
|
|
|
|
<div class="content" ng-if="!vm.loading">
|
|
<p>{{content}}</p>
|
|
</div>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller(myService) {
|
|
|
|
var vm = this;
|
|
|
|
vm.content = "";
|
|
vm.loading = true;
|
|
|
|
myService.getContent().then(function(content){
|
|
vm.content = content;
|
|
vm.loading = false;
|
|
});
|
|
|
|
}
|
|
½
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function UmbLoadIndicatorDirective() {
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-load-indicator.html'
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbLoadIndicator', UmbLoadIndicatorDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbLockedField
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to render a value with a lock next to it. When the lock is clicked the value gets unlocked and can be edited.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-locked-field
|
|
ng-model="vm.value"
|
|
placeholder-text="'Click to unlock...'">
|
|
</umb-locked-field>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
vm.value = "My locked text";
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
|
|
})();
|
|
</pre>
|
|
|
|
@param {string} ngModel (<code>binding</code>): The locked text.
|
|
@param {boolean=} locked (<code>binding</code>): <Code>true</code> by default. Set to <code>false</code> to unlock the text.
|
|
@param {string=} placeholderText (<code>binding</code>): If ngModel is empty this text will be shown.
|
|
@param {string=} regexValidation (<code>binding</code>): Set a regex expression for validation of the field.
|
|
@param {string=} serverValidationField (<code>attribute</code>): Set a server validation field.
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function LockedFieldDirective($timeout, localizationService) {
|
|
|
|
function link(scope, el, attr, ngModelCtrl) {
|
|
|
|
function activate() {
|
|
|
|
// if locked state is not defined as an attr set default state
|
|
if (scope.locked === undefined || scope.locked === null) {
|
|
scope.locked = true;
|
|
}
|
|
|
|
// if regex validation is not defined as an attr set default state
|
|
// if this is set to an empty string then regex validation can be ignored.
|
|
if (scope.regexValidation === undefined || scope.regexValidation === null) {
|
|
scope.regexValidation = "^[a-zA-Z]\\w.*$";
|
|
}
|
|
|
|
if (scope.serverValidationField === undefined || scope.serverValidationField === null) {
|
|
scope.serverValidationField = "";
|
|
}
|
|
|
|
// if locked state is not defined as an attr set default state
|
|
if (scope.placeholderText === undefined || scope.placeholderText === null) {
|
|
scope.placeholderText = "Enter value...";
|
|
}
|
|
|
|
}
|
|
|
|
scope.lock = function() {
|
|
scope.locked = true;
|
|
};
|
|
|
|
scope.unlock = function() {
|
|
scope.locked = false;
|
|
};
|
|
|
|
activate();
|
|
|
|
}
|
|
|
|
var directive = {
|
|
require: "ngModel",
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-locked-field.html',
|
|
scope: {
|
|
ngModel: "=",
|
|
locked: "=?",
|
|
placeholderText: "=?",
|
|
regexValidation: "=?",
|
|
serverValidationField: "@"
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbLockedField', LockedFieldDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbMediaGrid
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to generate a thumbnail grid of media items.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-media-grid
|
|
items="vm.mediaItems"
|
|
on-click="vm.clickItem"
|
|
on-click-name="vm.clickItemName">
|
|
</umb-media-grid>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
vm.mediaItems = [];
|
|
|
|
vm.clickItem = clickItem;
|
|
vm.clickItemName = clickItemName;
|
|
|
|
myService.getMediaItems().then(function (mediaItems) {
|
|
vm.mediaItems = mediaItems;
|
|
});
|
|
|
|
function clickItem(item, $event, $index){
|
|
// do magic here
|
|
}
|
|
|
|
function clickItemName(item, $event, $index) {
|
|
// set item.selected = true; to select the item
|
|
// do magic here
|
|
}
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
@param {array} items (<code>binding</code>): Array of media items.
|
|
@param {callback=} onDetailsHover (<code>binding</code>): Callback method when the details icon is hovered.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>item</code>: The hovered item</li>
|
|
<li><code>$event</code>: The hover event</li>
|
|
<li><code>hover</code>: Boolean to tell if the item is hovered or not</li>
|
|
</ul>
|
|
@param {callback=} onClick (<code>binding</code>): Callback method to handle click events on the media item.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>item</code>: The clicked item</li>
|
|
<li><code>$event</code>: The click event</li>
|
|
<li><code>$index</code>: The item index</li>
|
|
</ul>
|
|
@param {callback=} onClickName (<code>binding</code>): Callback method to handle click events on the media item name.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>item</code>: The clicked item</li>
|
|
<li><code>$event</code>: The click event</li>
|
|
<li><code>$index</code>: The item index</li>
|
|
</ul>
|
|
@param {string=} filterBy (<code>binding</code>): String to filter media items by
|
|
@param {string=} itemMaxWidth (<code>attribute</code>): Sets a max width on the media item thumbnails.
|
|
@param {string=} itemMaxHeight (<code>attribute</code>): Sets a max height on the media item thumbnails.
|
|
@param {string=} itemMinWidth (<code>attribute</code>): Sets a min width on the media item thumbnails.
|
|
@param {string=} itemMinHeight (<code>attribute</code>): Sets a min height on the media item thumbnails.
|
|
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function MediaGridDirective($filter, mediaHelper) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
var itemDefaultHeight = 200;
|
|
var itemDefaultWidth = 200;
|
|
var itemMaxWidth = 200;
|
|
var itemMaxHeight = 200;
|
|
var itemMinWidth = 125;
|
|
var itemMinHeight = 125;
|
|
|
|
function activate() {
|
|
|
|
if (scope.itemMaxWidth) {
|
|
itemMaxWidth = scope.itemMaxWidth;
|
|
}
|
|
|
|
if (scope.itemMaxHeight) {
|
|
itemMaxHeight = scope.itemMaxHeight;
|
|
}
|
|
|
|
if (scope.itemMinWidth) {
|
|
itemMinWidth = scope.itemMinWidth;
|
|
}
|
|
|
|
if (scope.itemMinWidth) {
|
|
itemMinHeight = scope.itemMinHeight;
|
|
}
|
|
|
|
for (var i = 0; scope.items.length > i; i++) {
|
|
var item = scope.items[i];
|
|
setItemData(item);
|
|
setOriginalSize(item, itemMaxHeight);
|
|
|
|
// remove non images when onlyImages is set to true
|
|
if(scope.onlyImages === "true" && !item.isFolder && !item.thumbnail){
|
|
scope.items.splice(i, 1);
|
|
i--;
|
|
}
|
|
|
|
}
|
|
|
|
if (scope.items.length > 0) {
|
|
setFlexValues(scope.items);
|
|
}
|
|
|
|
}
|
|
|
|
function setItemData(item) {
|
|
item.isFolder = !mediaHelper.hasFilePropertyType(item);
|
|
if (!item.isFolder) {
|
|
item.thumbnail = mediaHelper.resolveFile(item, true);
|
|
item.image = mediaHelper.resolveFile(item, false);
|
|
|
|
var fileProp = _.find(item.properties, function (v) {
|
|
return (v.alias === "umbracoFile");
|
|
});
|
|
|
|
if (fileProp && fileProp.value) {
|
|
item.file = fileProp.value;
|
|
}
|
|
|
|
var extensionProp = _.find(item.properties, function (v) {
|
|
return (v.alias === "umbracoExtension");
|
|
});
|
|
|
|
if (extensionProp && extensionProp.value) {
|
|
item.extension = extensionProp.value;
|
|
}
|
|
}
|
|
}
|
|
|
|
function setOriginalSize(item, maxHeight) {
|
|
|
|
//set to a square by default
|
|
item.width = itemDefaultWidth;
|
|
item.height = itemDefaultHeight;
|
|
item.aspectRatio = 1;
|
|
|
|
var widthProp = _.find(item.properties, function(v) {
|
|
return (v.alias === "umbracoWidth");
|
|
});
|
|
|
|
if (widthProp && widthProp.value) {
|
|
item.width = parseInt(widthProp.value, 10);
|
|
if (isNaN(item.width)) {
|
|
item.width = itemDefaultWidth;
|
|
}
|
|
}
|
|
|
|
var heightProp = _.find(item.properties, function(v) {
|
|
return (v.alias === "umbracoHeight");
|
|
});
|
|
|
|
if (heightProp && heightProp.value) {
|
|
item.height = parseInt(heightProp.value, 10);
|
|
if (isNaN(item.height)) {
|
|
item.height = itemDefaultWidth;
|
|
}
|
|
}
|
|
|
|
item.aspectRatio = item.width / item.height;
|
|
|
|
// set max width and height
|
|
// landscape
|
|
if (item.aspectRatio >= 1) {
|
|
if (item.width > itemMaxWidth) {
|
|
item.width = itemMaxWidth;
|
|
item.height = itemMaxWidth / item.aspectRatio;
|
|
}
|
|
// portrait
|
|
} else {
|
|
if (item.height > itemMaxHeight) {
|
|
item.height = itemMaxHeight;
|
|
item.width = itemMaxHeight * item.aspectRatio;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
function setFlexValues(mediaItems) {
|
|
|
|
var flexSortArray = mediaItems;
|
|
var smallestImageWidth = null;
|
|
var widestImageAspectRatio = null;
|
|
|
|
// sort array after image width with the widest image first
|
|
flexSortArray = $filter('orderBy')(flexSortArray, 'width', true);
|
|
|
|
// find widest image aspect ratio
|
|
widestImageAspectRatio = flexSortArray[0].aspectRatio;
|
|
|
|
// find smallest image width
|
|
smallestImageWidth = flexSortArray[flexSortArray.length - 1].width;
|
|
|
|
for (var i = 0; flexSortArray.length > i; i++) {
|
|
|
|
var mediaItem = flexSortArray[i];
|
|
var flex = 1 / (widestImageAspectRatio / mediaItem.aspectRatio);
|
|
|
|
if (flex === 0) {
|
|
flex = 1;
|
|
}
|
|
|
|
var imageMinFlexWidth = smallestImageWidth * flex;
|
|
|
|
var flexStyle = {
|
|
"flex": flex + " 1 " + imageMinFlexWidth + "px",
|
|
"max-width": mediaItem.width + "px",
|
|
"min-width": itemMinWidth + "px",
|
|
"min-height": itemMinHeight + "px"
|
|
};
|
|
|
|
mediaItem.flexStyle = flexStyle;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
scope.clickItem = function(item, $event, $index) {
|
|
if (scope.onClick) {
|
|
scope.onClick(item, $event, $index);
|
|
}
|
|
};
|
|
|
|
scope.clickItemName = function(item, $event, $index) {
|
|
if (scope.onClickName) {
|
|
scope.onClickName(item, $event, $index);
|
|
$event.stopPropagation();
|
|
}
|
|
};
|
|
|
|
scope.hoverItemDetails = function(item, $event, hover) {
|
|
if (scope.onDetailsHover) {
|
|
scope.onDetailsHover(item, $event, hover);
|
|
}
|
|
};
|
|
|
|
var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) {
|
|
if (angular.isArray(newValue)) {
|
|
activate();
|
|
}
|
|
});
|
|
|
|
scope.$on('$destroy', function() {
|
|
unbindItemsWatcher();
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-media-grid.html',
|
|
scope: {
|
|
items: '=',
|
|
onDetailsHover: "=",
|
|
onClick: '=',
|
|
onClickName: "=",
|
|
filterBy: "=",
|
|
itemMaxWidth: "@",
|
|
itemMaxHeight: "@",
|
|
itemMinWidth: "@",
|
|
itemMinHeight: "@",
|
|
onlyImages: "@"
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbMediaGrid', MediaGridDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbPagination
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to generate a pagination.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<umb-pagination
|
|
page-number="vm.pagination.pageNumber"
|
|
total-pages="vm.pagination.totalPages"
|
|
on-next="vm.nextPage"
|
|
on-prev="vm.prevPage"
|
|
on-go-to-page="vm.goToPage">
|
|
</umb-pagination>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
|
|
vm.pagination = {
|
|
pageNumber: 1,
|
|
totalPages: 10
|
|
}
|
|
|
|
vm.nextPage = nextPage;
|
|
vm.prevPage = prevPage;
|
|
vm.goToPage = goToPage;
|
|
|
|
function nextPage(pageNumber) {
|
|
// do magic here
|
|
console.log(pageNumber);
|
|
alert("nextpage");
|
|
}
|
|
|
|
function prevPage(pageNumber) {
|
|
// do magic here
|
|
console.log(pageNumber);
|
|
alert("prevpage");
|
|
}
|
|
|
|
function goToPage(pageNumber) {
|
|
// do magic here
|
|
console.log(pageNumber);
|
|
alert("go to");
|
|
}
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
})();
|
|
</pre>
|
|
|
|
@param {number} pageNumber (<code>binding</code>): Current page number.
|
|
@param {number} totalPages (<code>binding</code>): The total number of pages.
|
|
@param {callback} onNext (<code>binding</code>): Callback method to go to the next page.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>pageNumber</code>: The page number</li>
|
|
</ul>
|
|
@param {callback=} onPrev (<code>binding</code>): Callback method to go to the previous page.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>pageNumber</code>: The page number</li>
|
|
</ul>
|
|
@param {callback=} onGoToPage (<code>binding</code>): Callback method to go to a specific page.
|
|
<h3>The callback returns:</h3>
|
|
<ul>
|
|
<li><code>pageNumber</code>: The page number</li>
|
|
</ul>
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function PaginationDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
function activate() {
|
|
|
|
scope.pagination = [];
|
|
|
|
var i = 0;
|
|
|
|
if (scope.totalPages <= 10) {
|
|
for (i = 0; i < scope.totalPages; i++) {
|
|
scope.pagination.push({
|
|
val: (i + 1),
|
|
isActive: scope.pageNumber === (i + 1)
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
//if there is more than 10 pages, we need to do some fancy bits
|
|
|
|
//get the max index to start
|
|
var maxIndex = scope.totalPages - 10;
|
|
//set the start, but it can't be below zero
|
|
var start = Math.max(scope.pageNumber - 5, 0);
|
|
//ensure that it's not too far either
|
|
start = Math.min(maxIndex, start);
|
|
|
|
for (i = start; i < (10 + start) ; i++) {
|
|
scope.pagination.push({
|
|
val: (i + 1),
|
|
isActive: scope.pageNumber === (i + 1)
|
|
});
|
|
}
|
|
|
|
//now, if the start is greater than 0 then '1' will not be displayed, so do the elipses thing
|
|
if (start > 0) {
|
|
scope.pagination.unshift({ name: "First", val: 1, isActive: false }, {val: "...",isActive: false});
|
|
}
|
|
|
|
//same for the end
|
|
if (start < maxIndex) {
|
|
scope.pagination.push({ val: "...", isActive: false }, { name: "Last", val: scope.totalPages, isActive: false });
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
scope.next = function() {
|
|
if (scope.onNext && scope.pageNumber < scope.totalPages) {
|
|
scope.pageNumber++;
|
|
scope.onNext(scope.pageNumber);
|
|
}
|
|
};
|
|
|
|
scope.prev = function(pageNumber) {
|
|
if (scope.onPrev && scope.pageNumber > 1) {
|
|
scope.pageNumber--;
|
|
scope.onPrev(scope.pageNumber);
|
|
}
|
|
};
|
|
|
|
scope.goToPage = function(pageNumber) {
|
|
if(scope.onGoToPage) {
|
|
scope.pageNumber = pageNumber + 1;
|
|
scope.onGoToPage(scope.pageNumber);
|
|
}
|
|
};
|
|
|
|
var unbindPageNumberWatcher = scope.$watch('pageNumber', function(newValue, oldValue){
|
|
activate();
|
|
});
|
|
|
|
scope.$on('$destroy', function(){
|
|
unbindPageNumberWatcher();
|
|
});
|
|
|
|
activate();
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-pagination.html',
|
|
scope: {
|
|
pageNumber: "=",
|
|
totalPages: "=",
|
|
onNext: "=",
|
|
onPrev: "=",
|
|
onGoToPage: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbPagination', PaginationDirective);
|
|
|
|
})();
|
|
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbProgressBar
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to generate a progress bar.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<umb-progress-bar
|
|
percentage="60">
|
|
</umb-progress-bar>
|
|
</pre>
|
|
|
|
@param {number} percentage (<code>attribute</code>): The progress in percentage.
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function ProgressBarDirective() {
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-progress-bar.html',
|
|
scope: {
|
|
percentage: "@"
|
|
}
|
|
};
|
|
|
|
return directive;
|
|
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbProgressBar', ProgressBarDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbStickyBar
|
|
@restrict A
|
|
|
|
@description
|
|
Use this directive make an element sticky and follow the page when scrolling.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<div
|
|
class="my-sticky-bar"
|
|
umb-sticky-bar
|
|
scrollable-container=".container">
|
|
</div>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>CSS example</h3>
|
|
<pre>
|
|
.my-sticky-bar {
|
|
padding: 15px 0;
|
|
background: #000000;
|
|
position: relative;
|
|
top: 0;
|
|
}
|
|
|
|
.my-sticky-bar.-umb-sticky-bar {
|
|
top: 100px;
|
|
}
|
|
</pre>
|
|
|
|
@param {string} scrollableContainer Set the class (".element") or the id ("#element") of the scrollable container element.
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function StickyBarDirective($rootScope) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
var bar = $(el);
|
|
var scrollableContainer = null;
|
|
var clonedBar = null;
|
|
var cloneIsMade = false;
|
|
var barTop = bar.context.offsetTop;
|
|
|
|
function activate() {
|
|
|
|
if (attr.scrollableContainer) {
|
|
scrollableContainer = $(attr.scrollableContainer);
|
|
} else {
|
|
scrollableContainer = $(window);
|
|
}
|
|
|
|
scrollableContainer.on('scroll.umbStickyBar', determineVisibility).trigger("scroll");
|
|
$(window).on('resize.umbStickyBar', determineVisibility);
|
|
|
|
scope.$on('$destroy', function() {
|
|
scrollableContainer.off('.umbStickyBar');
|
|
$(window).off('.umbStickyBar');
|
|
});
|
|
|
|
}
|
|
|
|
function determineVisibility() {
|
|
|
|
var scrollTop = scrollableContainer.scrollTop();
|
|
|
|
if (scrollTop > barTop) {
|
|
|
|
if (!cloneIsMade) {
|
|
|
|
createClone();
|
|
|
|
clonedBar.css({
|
|
'visibility': 'visible'
|
|
});
|
|
|
|
} else {
|
|
|
|
calculateSize();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (cloneIsMade) {
|
|
|
|
//remove cloned element (switched places with original on creation)
|
|
bar.remove();
|
|
bar = clonedBar;
|
|
clonedBar = null;
|
|
|
|
bar.removeClass('-umb-sticky-bar');
|
|
bar.css({
|
|
position: 'relative',
|
|
'width': 'auto',
|
|
'height': 'auto',
|
|
'z-index': 'auto',
|
|
'visibility': 'visible'
|
|
});
|
|
|
|
cloneIsMade = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function calculateSize() {
|
|
clonedBar.css({
|
|
width: bar.outerWidth(),
|
|
height: bar.height()
|
|
});
|
|
}
|
|
|
|
function createClone() {
|
|
//switch place with cloned element, to keep binding intact
|
|
clonedBar = bar;
|
|
bar = clonedBar.clone();
|
|
clonedBar.after(bar);
|
|
clonedBar.addClass('-umb-sticky-bar');
|
|
clonedBar.css({
|
|
'position': 'fixed',
|
|
'z-index': 500,
|
|
'visibility': 'hidden'
|
|
});
|
|
|
|
cloneIsMade = true;
|
|
calculateSize();
|
|
|
|
}
|
|
|
|
activate();
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'A',
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbStickyBar', StickyBarDirective);
|
|
|
|
})();
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
function TableDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.clickItem = function (item, $event) {
|
|
if (scope.onClick) {
|
|
scope.onClick(item);
|
|
$event.stopPropagation();
|
|
}
|
|
};
|
|
|
|
scope.selectItem = function (item, $index, $event) {
|
|
if (scope.onSelect) {
|
|
scope.onSelect(item, $index, $event);
|
|
$event.stopPropagation();
|
|
}
|
|
};
|
|
|
|
scope.selectAll = function ($event) {
|
|
if (scope.onSelectAll) {
|
|
scope.onSelectAll($event);
|
|
}
|
|
};
|
|
|
|
scope.isSelectedAll = function () {
|
|
if (scope.onSelectedAll && scope.items && scope.items.length > 0) {
|
|
return scope.onSelectedAll();
|
|
}
|
|
};
|
|
|
|
scope.isSortDirection = function (col, direction) {
|
|
if (scope.onSortingDirection) {
|
|
return scope.onSortingDirection(col, direction);
|
|
}
|
|
};
|
|
|
|
scope.sort = function (field, allow, isSystem) {
|
|
if (scope.onSort) {
|
|
scope.onSort(field, allow, isSystem);
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-table.html',
|
|
scope: {
|
|
items: '=',
|
|
itemProperties: '=',
|
|
allowSelectAll: '=',
|
|
onSelect: '=',
|
|
onClick: '=',
|
|
onSelectAll: '=',
|
|
onSelectedAll: '=',
|
|
onSortingDirection: '=',
|
|
onSort: '='
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbTable', TableDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
@ngdoc directive
|
|
@name umbraco.directives.directive:umbTooltip
|
|
@restrict E
|
|
@scope
|
|
|
|
@description
|
|
Use this directive to render a tooltip.
|
|
|
|
<h3>Markup example</h3>
|
|
<pre>
|
|
<div ng-controller="My.Controller as vm">
|
|
|
|
<div
|
|
ng-mouseover="vm.mouseOver($event)"
|
|
ng-mouseleave="vm.mouseLeave()">
|
|
Hover me
|
|
</div>
|
|
|
|
<umb-tooltip
|
|
ng-if="vm.tooltip.show"
|
|
event="vm.tooltip.event">
|
|
// tooltip content here
|
|
</umb-tooltip>
|
|
|
|
</div>
|
|
</pre>
|
|
|
|
<h3>Controller example</h3>
|
|
<pre>
|
|
(function () {
|
|
"use strict";
|
|
|
|
function Controller() {
|
|
|
|
var vm = this;
|
|
vm.tooltip = {
|
|
show: false,
|
|
event: null
|
|
};
|
|
|
|
vm.mouseOver = mouseOver;
|
|
vm.mouseLeave = mouseLeave;
|
|
|
|
function mouseOver($event) {
|
|
vm.tooltip = {
|
|
show: true,
|
|
event: $event
|
|
};
|
|
}
|
|
|
|
function mouseLeave() {
|
|
vm.tooltip = {
|
|
show: false,
|
|
event: null
|
|
};
|
|
}
|
|
|
|
}
|
|
|
|
angular.module("umbraco").controller("My.Controller", Controller);
|
|
|
|
})();
|
|
</pre>
|
|
|
|
@param {string} event Set the $event from the target element to position the tooltip relative to the mouse cursor.
|
|
**/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function TooltipDirective($timeout) {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
scope.tooltipStyles = {};
|
|
scope.tooltipStyles.left = 0;
|
|
scope.tooltipStyles.top = 0;
|
|
|
|
function activate() {
|
|
|
|
$timeout(function() {
|
|
setTooltipPosition(scope.event);
|
|
});
|
|
|
|
}
|
|
|
|
function setTooltipPosition(event) {
|
|
|
|
var container = $("#contentwrapper");
|
|
var containerLeft = container[0].offsetLeft;
|
|
var containerRight = containerLeft + container[0].offsetWidth;
|
|
var containerTop = container[0].offsetTop;
|
|
var containerBottom = containerTop + container[0].offsetHeight;
|
|
|
|
var elementHeight = null;
|
|
var elementWidth = null;
|
|
|
|
var position = {
|
|
right: "inherit",
|
|
left: "inherit",
|
|
top: "inherit",
|
|
bottom: "inherit"
|
|
};
|
|
|
|
// element size
|
|
elementHeight = el.context.clientHeight;
|
|
elementWidth = el.context.clientWidth;
|
|
|
|
position.left = event.pageX - (elementWidth / 2);
|
|
position.top = event.pageY;
|
|
|
|
// check to see if element is outside screen
|
|
// outside right
|
|
if (position.left + elementWidth > containerRight) {
|
|
position.right = 10;
|
|
position.left = "inherit";
|
|
}
|
|
|
|
// outside bottom
|
|
if (position.top + elementHeight > containerBottom) {
|
|
position.bottom = 10;
|
|
position.top = "inherit";
|
|
}
|
|
|
|
// outside left
|
|
if (position.left < containerLeft) {
|
|
position.left = containerLeft + 10;
|
|
position.right = "inherit";
|
|
}
|
|
|
|
// outside top
|
|
if (position.top < containerTop) {
|
|
position.top = 10;
|
|
position.bottom = "inherit";
|
|
}
|
|
|
|
scope.tooltipStyles = position;
|
|
|
|
el.css(position);
|
|
|
|
}
|
|
|
|
activate();
|
|
|
|
}
|
|
|
|
var directive = {
|
|
restrict: 'E',
|
|
transclude: true,
|
|
replace: true,
|
|
templateUrl: 'views/components/umb-tooltip.html',
|
|
scope: {
|
|
event: "="
|
|
},
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbTooltip', TooltipDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbFileDropzone
|
|
* @restrict E
|
|
* @function
|
|
* @description
|
|
* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form.
|
|
**/
|
|
|
|
/*
|
|
TODO
|
|
.directive("umbFileDrop", function ($timeout, $upload, localizationService, umbRequestHelper){
|
|
|
|
return{
|
|
restrict: "A",
|
|
link: function(scope, element, attrs){
|
|
|
|
//load in the options model
|
|
|
|
|
|
}
|
|
}
|
|
})
|
|
*/
|
|
|
|
angular.module("umbraco.directives")
|
|
|
|
.directive('umbFileDropzone', function ($timeout, Upload, localizationService, umbRequestHelper) {
|
|
return {
|
|
|
|
restrict: 'E',
|
|
replace: true,
|
|
|
|
templateUrl: 'views/components/upload/umb-file-dropzone.html',
|
|
|
|
scope: {
|
|
parentId: '@',
|
|
contentTypeAlias: '@',
|
|
propertyAlias: '@',
|
|
accept: '@',
|
|
maxFileSize: '@',
|
|
|
|
compact: '@',
|
|
hideDropzone: '@',
|
|
|
|
filesQueued: '=',
|
|
handleFile: '=',
|
|
filesUploaded: '='
|
|
},
|
|
|
|
link: function(scope, element, attrs) {
|
|
|
|
scope.queue = [];
|
|
scope.done = [];
|
|
scope.rejected = [];
|
|
scope.currentFile = undefined;
|
|
|
|
function _filterFile(file) {
|
|
|
|
var ignoreFileNames = ['Thumbs.db'];
|
|
var ignoreFileTypes = ['directory'];
|
|
|
|
// ignore files with names from the list
|
|
// ignore files with types from the list
|
|
// ignore files which starts with "."
|
|
if(ignoreFileNames.indexOf(file.name) === -1 &&
|
|
ignoreFileTypes.indexOf(file.type) === -1 &&
|
|
file.name.indexOf(".") !== 0) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
}
|
|
|
|
function _filesQueued(files, event){
|
|
|
|
//Push into the queue
|
|
angular.forEach(files, function(file){
|
|
|
|
if(_filterFile(file) === true) {
|
|
|
|
if(file.$error) {
|
|
scope.rejected.push(file);
|
|
} else {
|
|
scope.queue.push(file);
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
//when queue is done, kick the uploader
|
|
if(!scope.working){
|
|
_processQueueItem();
|
|
}
|
|
}
|
|
|
|
|
|
function _processQueueItem(){
|
|
|
|
if(scope.queue.length > 0){
|
|
scope.currentFile = scope.queue.shift();
|
|
_upload(scope.currentFile);
|
|
}else if(scope.done.length > 0){
|
|
|
|
if(scope.filesUploaded){
|
|
//queue is empty, trigger the done action
|
|
scope.filesUploaded(scope.done);
|
|
}
|
|
|
|
//auto-clear the done queue after 3 secs
|
|
var currentLength = scope.done.length;
|
|
$timeout(function(){
|
|
scope.done.splice(0, currentLength);
|
|
}, 3000);
|
|
}
|
|
}
|
|
|
|
function _upload(file) {
|
|
|
|
scope.propertyAlias = scope.propertyAlias ? scope.propertyAlias : "umbracoFile";
|
|
scope.contentTypeAlias = scope.contentTypeAlias ? scope.contentTypeAlias : "Image";
|
|
|
|
Upload.upload({
|
|
url: umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostAddFile"),
|
|
fields: {
|
|
'currentFolder': scope.parentId,
|
|
'contentTypeAlias': scope.contentTypeAlias,
|
|
'propertyAlias': scope.propertyAlias,
|
|
'path': file.path
|
|
},
|
|
file: file
|
|
}).progress(function (evt) {
|
|
|
|
// calculate progress in percentage
|
|
var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10);
|
|
|
|
// set percentage property on file
|
|
file.uploadProgress = progressPercentage;
|
|
|
|
// set uploading status on file
|
|
file.uploadStatus = "uploading";
|
|
|
|
}).success(function (data, status, headers, config) {
|
|
|
|
if(data.notifications && data.notifications.length > 0) {
|
|
|
|
// set error status on file
|
|
file.uploadStatus = "error";
|
|
|
|
// Throw message back to user with the cause of the error
|
|
file.serverErrorMessage = data.notifications[0].message;
|
|
|
|
// Put the file in the rejected pool
|
|
scope.rejected.push(file);
|
|
|
|
} else {
|
|
|
|
// set done status on file
|
|
file.uploadStatus = "done";
|
|
|
|
// set date/time for when done - used for sorting
|
|
file.doneDate = new Date();
|
|
|
|
// Put the file in the done pool
|
|
scope.done.push(file);
|
|
|
|
}
|
|
|
|
scope.currentFile = undefined;
|
|
|
|
//after processing, test if everthing is done
|
|
_processQueueItem();
|
|
|
|
}).error( function (evt, status, headers, config) {
|
|
|
|
// set status done
|
|
file.uploadStatus = "error";
|
|
|
|
//if the service returns a detailed error
|
|
if (evt.InnerException) {
|
|
file.serverErrorMessage = evt.InnerException.ExceptionMessage;
|
|
|
|
//Check if its the common "too large file" exception
|
|
if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) {
|
|
file.serverErrorMessage = "File too large to upload";
|
|
}
|
|
|
|
} else if (evt.Message) {
|
|
file.serverErrorMessage = evt.Message;
|
|
}
|
|
|
|
// If file not found, server will return a 404 and display this message
|
|
if(status === 404 ) {
|
|
file.serverErrorMessage = "File not found";
|
|
}
|
|
|
|
//after processing, test if everthing is done
|
|
scope.rejected.push(file);
|
|
scope.currentFile = undefined;
|
|
|
|
_processQueueItem();
|
|
});
|
|
}
|
|
|
|
|
|
scope.handleFiles = function(files, event){
|
|
if(scope.filesQueued){
|
|
scope.filesQueued(files, event);
|
|
}
|
|
|
|
_filesQueued(files, event);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbFileUpload
|
|
* @function
|
|
* @restrict A
|
|
* @scope
|
|
* @description
|
|
* Listens for file input control changes and emits events when files are selected for use in other controllers.
|
|
**/
|
|
function umbFileUpload() {
|
|
return {
|
|
restrict: "A",
|
|
scope: true, //create a new scope
|
|
link: function (scope, el, attrs) {
|
|
el.bind('change', function (event) {
|
|
var files = event.target.files;
|
|
//emit event upward
|
|
scope.$emit("filesSelected", { files: files });
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive("umbFileUpload", umbFileUpload);
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:umbSingleFileUpload
|
|
* @function
|
|
* @restrict A
|
|
* @scope
|
|
* @description
|
|
* A single file upload field that will reset itself based on the object passed in for the rebuild parameter. This
|
|
* is required because the only way to reset an upload control is to replace it's html.
|
|
**/
|
|
function umbSingleFileUpload($compile) {
|
|
return {
|
|
restrict: "E",
|
|
scope: {
|
|
rebuild: "="
|
|
},
|
|
replace: true,
|
|
template: "<div><input type='file' umb-file-upload /></div>",
|
|
link: function (scope, el, attrs) {
|
|
|
|
scope.$watch("rebuild", function (newVal, oldVal) {
|
|
if (newVal && newVal !== oldVal) {
|
|
//recompile it!
|
|
el.html("<input type='file' umb-file-upload />");
|
|
$compile(el.contents())(scope);
|
|
}
|
|
});
|
|
|
|
}
|
|
};
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload);
|
|
/**
|
|
* Konami Code directive for AngularJS
|
|
* @version v0.0.1
|
|
* @license MIT License, http://www.opensource.org/licenses/MIT
|
|
*/
|
|
|
|
angular.module('umbraco.directives')
|
|
.directive('konamiCode', ['$document', function ($document) {
|
|
var konamiKeysDefault = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
|
|
|
|
return {
|
|
restrict: 'A',
|
|
link: function (scope, element, attr) {
|
|
|
|
if (!attr.konamiCode) {
|
|
throw ('Konami directive must receive an expression as value.');
|
|
}
|
|
|
|
// Let user define a custom code.
|
|
var konamiKeys = attr.konamiKeys || konamiKeysDefault;
|
|
var keyIndex = 0;
|
|
|
|
/**
|
|
* Fired when konami code is type.
|
|
*/
|
|
function activated() {
|
|
if ('konamiOnce' in attr) {
|
|
stopListening();
|
|
}
|
|
// Execute expression.
|
|
scope.$eval(attr.konamiCode);
|
|
}
|
|
|
|
/**
|
|
* Handle keydown events.
|
|
*/
|
|
function keydown(e) {
|
|
if (e.keyCode === konamiKeys[keyIndex++]) {
|
|
if (keyIndex === konamiKeys.length) {
|
|
keyIndex = 0;
|
|
activated();
|
|
}
|
|
} else {
|
|
keyIndex = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop to listen typing.
|
|
*/
|
|
function stopListening() {
|
|
$document.off('keydown', keydown);
|
|
}
|
|
|
|
// Start listening to key typing.
|
|
$document.on('keydown', keydown);
|
|
|
|
// Stop listening when scope is destroyed.
|
|
scope.$on('$destroy', stopListening);
|
|
}
|
|
};
|
|
}]);
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:noDirtyCheck
|
|
* @restrict A
|
|
* @description Can be attached to form inputs to prevent them from setting the form as dirty (http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs)
|
|
**/
|
|
function noDirtyCheck() {
|
|
return {
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
link: function (scope, elm, attrs, ctrl) {
|
|
elm.focus(function () {
|
|
ctrl.$pristine = false;
|
|
});
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives.validation').directive("noDirtyCheck", noDirtyCheck);
|
|
(function() {
|
|
'use strict';
|
|
|
|
function SetDirtyOnChange() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
var initValue = attr.umbSetDirtyOnChange;
|
|
|
|
attr.$observe("umbSetDirtyOnChange", function (newValue) {
|
|
if(newValue !== initValue) {
|
|
ctrl.$setDirty();
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
require: "^form",
|
|
restrict: 'A',
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('umbSetDirtyOnChange', SetDirtyOnChange);
|
|
|
|
})();
|
|
|
|
/**
|
|
* General-purpose validator for ngModel.
|
|
* angular.js comes with several built-in validation mechanism for input fields (ngRequired, ngPattern etc.) but using
|
|
* an arbitrary validation function requires creation of a custom formatters and / or parsers.
|
|
* The ui-validate directive makes it easy to use any function(s) defined in scope as a validator function(s).
|
|
* A validator function will trigger validation on both model and input changes.
|
|
*
|
|
* @example <input val-custom=" 'myValidatorFunction($value)' ">
|
|
* @example <input val-custom="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }">
|
|
* @example <input val-custom="{ foo : '$value > anotherModel' }" val-custom-watch=" 'anotherModel' ">
|
|
* @example <input val-custom="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }" val-custom-watch=" { foo : 'anotherModel' } ">
|
|
*
|
|
* @param val-custom {string|object literal} If strings is passed it should be a scope's function to be used as a validator.
|
|
* If an object literal is passed a key denotes a validation error key while a value should be a validator function.
|
|
* In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result.
|
|
*/
|
|
|
|
/*
|
|
This code comes from the angular UI project, we had to change the directive name and module
|
|
but other then that its unmodified
|
|
*/
|
|
angular.module('umbraco.directives.validation')
|
|
.directive('valCustom', function () {
|
|
|
|
return {
|
|
restrict: 'A',
|
|
require: 'ngModel',
|
|
link: function (scope, elm, attrs, ctrl) {
|
|
var validateFn, watch, validators = {},
|
|
validateExpr = scope.$eval(attrs.valCustom);
|
|
|
|
if (!validateExpr){ return;}
|
|
|
|
if (angular.isString(validateExpr)) {
|
|
validateExpr = { validator: validateExpr };
|
|
}
|
|
|
|
angular.forEach(validateExpr, function (exprssn, key) {
|
|
validateFn = function (valueToValidate) {
|
|
var expression = scope.$eval(exprssn, { '$value' : valueToValidate });
|
|
if (angular.isObject(expression) && angular.isFunction(expression.then)) {
|
|
// expression is a promise
|
|
expression.then(function(){
|
|
ctrl.$setValidity(key, true);
|
|
}, function(){
|
|
ctrl.$setValidity(key, false);
|
|
});
|
|
return valueToValidate;
|
|
} else if (expression) {
|
|
// expression is true
|
|
ctrl.$setValidity(key, true);
|
|
return valueToValidate;
|
|
} else {
|
|
// expression is false
|
|
ctrl.$setValidity(key, false);
|
|
return undefined;
|
|
}
|
|
};
|
|
validators[key] = validateFn;
|
|
|
|
ctrl.$parsers.push(validateFn);
|
|
});
|
|
|
|
function apply_watch(watch)
|
|
{
|
|
//string - update all validators on expression change
|
|
if (angular.isString(watch))
|
|
{
|
|
scope.$watch(watch, function(){
|
|
angular.forEach(validators, function(validatorFn){
|
|
validatorFn(ctrl.$modelValue);
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
//array - update all validators on change of any expression
|
|
if (angular.isArray(watch))
|
|
{
|
|
angular.forEach(watch, function(expression){
|
|
scope.$watch(expression, function()
|
|
{
|
|
angular.forEach(validators, function(validatorFn){
|
|
validatorFn(ctrl.$modelValue);
|
|
});
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
//object - update appropriate validator
|
|
if (angular.isObject(watch))
|
|
{
|
|
angular.forEach(watch, function(expression, validatorKey)
|
|
{
|
|
//value is string - look after one expression
|
|
if (angular.isString(expression))
|
|
{
|
|
scope.$watch(expression, function(){
|
|
validators[validatorKey](ctrl.$modelValue);
|
|
});
|
|
}
|
|
|
|
//value is array - look after all expressions in array
|
|
if (angular.isArray(expression))
|
|
{
|
|
angular.forEach(expression, function(intExpression)
|
|
{
|
|
scope.$watch(intExpression, function(){
|
|
validators[validatorKey](ctrl.$modelValue);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// Support for val-custom-watch
|
|
if (attrs.valCustomWatch){
|
|
apply_watch( scope.$eval(attrs.valCustomWatch) );
|
|
}
|
|
}
|
|
};
|
|
});
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valHighlight
|
|
* @restrict A
|
|
* @description Used on input fields when you want to signal that they are in error, this will highlight the item for 1 second
|
|
**/
|
|
function valHighlight($timeout) {
|
|
return {
|
|
restrict: "A",
|
|
link: function (scope, element, attrs, ctrl) {
|
|
|
|
attrs.$observe("valHighlight", function (newVal) {
|
|
if (newVal === "true") {
|
|
element.addClass("highlight-error");
|
|
$timeout(function () {
|
|
//set the bound scope property to false
|
|
scope[attrs.valHighlight] = false;
|
|
}, 1000);
|
|
}
|
|
else {
|
|
element.removeClass("highlight-error");
|
|
}
|
|
});
|
|
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives.validation').directive("valHighlight", valHighlight);
|
|
|
|
angular.module('umbraco.directives.validation')
|
|
.directive('valCompare',function () {
|
|
return {
|
|
require: "ngModel",
|
|
link: function (scope, elem, attrs, ctrl) {
|
|
|
|
//TODO: Pretty sure this should be done using a requires ^form in the directive declaration
|
|
var otherInput = elem.inheritedData("$formController")[attrs.valCompare];
|
|
|
|
ctrl.$parsers.push(function(value) {
|
|
if(value === otherInput.$viewValue) {
|
|
ctrl.$setValidity("valCompare", true);
|
|
return value;
|
|
}
|
|
ctrl.$setValidity("valCompare", false);
|
|
});
|
|
|
|
otherInput.$parsers.push(function(value) {
|
|
ctrl.$setValidity("valCompare", value === ctrl.$viewValue);
|
|
return value;
|
|
});
|
|
}
|
|
};
|
|
});
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valEmail
|
|
* @restrict A
|
|
* @description A custom directive to validate an email address string, this is required because angular's default validator is incorrect.
|
|
**/
|
|
function valEmail(valEmailExpression) {
|
|
|
|
return {
|
|
require: 'ngModel',
|
|
restrict: "A",
|
|
link: function (scope, elm, attrs, ctrl) {
|
|
|
|
var patternValidator = function (viewValue) {
|
|
//NOTE: we don't validate on empty values, use required validator for that
|
|
if (!viewValue || valEmailExpression.EMAIL_REGEXP.test(viewValue)) {
|
|
// it is valid
|
|
ctrl.$setValidity('valEmail', true);
|
|
//assign a message to the validator
|
|
ctrl.errorMsg = "";
|
|
return viewValue;
|
|
}
|
|
else {
|
|
// it is invalid, return undefined (no model update)
|
|
ctrl.$setValidity('valEmail', false);
|
|
//assign a message to the validator
|
|
ctrl.errorMsg = "Invalid email";
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
//if there is an attribute: type="email" then we need to remove those formatters and parsers
|
|
if (attrs.type === "email") {
|
|
//we need to remove the existing parsers = the default angular one which is created by
|
|
// type="email", but this has a regex issue, so we'll remove that and add our custom one
|
|
ctrl.$parsers.pop();
|
|
//we also need to remove the existing formatter - the default angular one will not render
|
|
// what it thinks is an invalid email address, so it will just be blank
|
|
ctrl.$formatters.pop();
|
|
}
|
|
|
|
ctrl.$parsers.push(patternValidator);
|
|
}
|
|
};
|
|
}
|
|
|
|
angular.module('umbraco.directives.validation')
|
|
.directive("valEmail", valEmail)
|
|
.factory('valEmailExpression', function () {
|
|
//NOTE: This is the fixed regex which is part of the newer angular
|
|
return {
|
|
EMAIL_REGEXP: /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i
|
|
};
|
|
});
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valFormManager
|
|
* @restrict A
|
|
* @require formController
|
|
* @description Used to broadcast an event to all elements inside this one to notify that form validation has
|
|
* changed. If we don't use this that means you have to put a watch for each directive on a form's validation
|
|
* changing which would result in much higher processing. We need to actually watch the whole $error collection of a form
|
|
* because just watching $valid or $invalid doesn't acurrately trigger form validation changing.
|
|
* This also sets the show-validation (or a custom) css class on the element when the form is invalid - this lets
|
|
* us css target elements to be displayed when the form is submitting/submitted.
|
|
* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will
|
|
* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly.
|
|
**/
|
|
function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) {
|
|
return {
|
|
require: "form",
|
|
restrict: "A",
|
|
controller: function($scope) {
|
|
//This exposes an API for direct use with this directive
|
|
|
|
var unsubscribe = [];
|
|
var self = this;
|
|
|
|
//This is basically the same as a directive subscribing to an event but maybe a little
|
|
// nicer since the other directive can use this directive's API instead of a magical event
|
|
this.onValidationStatusChanged = function (cb) {
|
|
unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) {
|
|
cb.apply(self, [evt, args]);
|
|
}));
|
|
};
|
|
|
|
//Ensure to remove the event handlers when this instance is destroyted
|
|
$scope.$on('$destroy', function () {
|
|
for (var u in unsubscribe) {
|
|
unsubscribe[u]();
|
|
}
|
|
});
|
|
},
|
|
link: function (scope, element, attr, formCtrl) {
|
|
|
|
scope.$watch(function () {
|
|
return formCtrl.$error;
|
|
}, function (e) {
|
|
scope.$broadcast("valStatusChanged", { form: formCtrl });
|
|
|
|
//find all invalid elements' .control-group's and apply the error class
|
|
var inError = element.find(".control-group .ng-invalid").closest(".control-group");
|
|
inError.addClass("error");
|
|
|
|
//find all control group's that have no error and ensure the class is removed
|
|
var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError);
|
|
noInError.removeClass("error");
|
|
|
|
}, true);
|
|
|
|
var className = attr.valShowValidation ? attr.valShowValidation : "show-validation";
|
|
var savingEventName = attr.savingEvent ? attr.savingEvent : "formSubmitting";
|
|
var savedEvent = attr.savedEvent ? attr.savingEvent : "formSubmitted";
|
|
|
|
//This tracks if the user is currently saving a new item, we use this to determine
|
|
// if we should display the warning dialog that they are leaving the page - if a new item
|
|
// is being saved we never want to display that dialog, this will also cause problems when there
|
|
// are server side validation issues.
|
|
var isSavingNewItem = false;
|
|
|
|
//we should show validation if there are any msgs in the server validation collection
|
|
if (serverValidationManager.items.length > 0) {
|
|
element.addClass(className);
|
|
}
|
|
|
|
var unsubscribe = [];
|
|
|
|
//listen for the forms saving event
|
|
unsubscribe.push(scope.$on(savingEventName, function(ev, args) {
|
|
element.addClass(className);
|
|
|
|
//set the flag so we can check to see if we should display the error.
|
|
isSavingNewItem = $routeParams.create;
|
|
}));
|
|
|
|
//listen for the forms saved event
|
|
unsubscribe.push(scope.$on(savedEvent, function(ev, args) {
|
|
//remove validation class
|
|
element.removeClass(className);
|
|
|
|
//clear form state as at this point we retrieve new data from the server
|
|
//and all validation will have cleared at this point
|
|
formCtrl.$setPristine();
|
|
}));
|
|
|
|
//This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but
|
|
// the form has pending changes
|
|
var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) {
|
|
if (!formCtrl.$dirty || isSavingNewItem) {
|
|
return;
|
|
}
|
|
|
|
var path = nextLocation.split("#")[1];
|
|
if (path) {
|
|
if (path.indexOf("%253") || path.indexOf("%252")) {
|
|
path = decodeURIComponent(path);
|
|
}
|
|
|
|
if (!notificationsService.hasView()) {
|
|
var msg = { view: "confirmroutechange", args: { path: path, listener: locationEvent } };
|
|
notificationsService.add(msg);
|
|
}
|
|
|
|
//prevent the route!
|
|
event.preventDefault();
|
|
|
|
//raise an event
|
|
eventsService.emit("valFormManager.pendingChanges", true);
|
|
}
|
|
|
|
});
|
|
unsubscribe.push(locationEvent);
|
|
|
|
//Ensure to remove the event handler when this instance is destroyted
|
|
scope.$on('$destroy', function() {
|
|
for (var u in unsubscribe) {
|
|
unsubscribe[u]();
|
|
}
|
|
});
|
|
|
|
$timeout(function(){
|
|
formCtrl.$setPristine();
|
|
}, 1000);
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives.validation').directive("valFormManager", valFormManager);
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valPropertyMsg
|
|
* @restrict A
|
|
* @element textarea
|
|
* @requires formController
|
|
* @description This directive is used to control the display of the property level validation message.
|
|
* We will listen for server side validation changes
|
|
* and when an error is detected for this property we'll show the error message.
|
|
* In order for this directive to work, the valStatusChanged directive must be placed on the containing form.
|
|
**/
|
|
function valPropertyMsg(serverValidationManager) {
|
|
|
|
return {
|
|
scope: {
|
|
property: "="
|
|
},
|
|
require: "^form", //require that this directive is contained within an ngForm
|
|
replace: true, //replace the element with the template
|
|
restrict: "E", //restrict to element
|
|
template: "<div ng-show=\"errorMsg != ''\" class='alert alert-error property-error' >{{errorMsg}}</div>",
|
|
|
|
/**
|
|
Our directive requries a reference to a form controller
|
|
which gets passed in to this parameter
|
|
*/
|
|
link: function (scope, element, attrs, formCtrl) {
|
|
|
|
var watcher = null;
|
|
|
|
// Gets the error message to display
|
|
function getErrorMsg() {
|
|
//this can be null if no property was assigned
|
|
if (scope.property) {
|
|
//first try to get the error msg from the server collection
|
|
var err = serverValidationManager.getPropertyError(scope.property.alias, "");
|
|
//if there's an error message use it
|
|
if (err && err.errorMsg) {
|
|
return err.errorMsg;
|
|
}
|
|
else {
|
|
return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : "Property has errors";
|
|
}
|
|
|
|
}
|
|
return "Property has errors";
|
|
}
|
|
|
|
// We need to subscribe to any changes to our model (based on user input)
|
|
// This is required because when we have a server error we actually invalidate
|
|
// the form which means it cannot be resubmitted.
|
|
// So once a field is changed that has a server error assigned to it
|
|
// we need to re-validate it for the server side validator so the user can resubmit
|
|
// the form. Of course normal client-side validators will continue to execute.
|
|
function startWatch() {
|
|
//if there's not already a watch
|
|
if (!watcher) {
|
|
watcher = scope.$watch("property.value", function (newValue, oldValue) {
|
|
|
|
if (!newValue || angular.equals(newValue, oldValue)) {
|
|
return;
|
|
}
|
|
|
|
var errCount = 0;
|
|
for (var e in formCtrl.$error) {
|
|
if (angular.isArray(formCtrl.$error[e])) {
|
|
errCount++;
|
|
}
|
|
}
|
|
|
|
//we are explicitly checking for valServer errors here, since we shouldn't auto clear
|
|
// based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg
|
|
// is the only one, then we'll clear.
|
|
|
|
if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) {
|
|
scope.errorMsg = "";
|
|
formCtrl.$setValidity('valPropertyMsg', true);
|
|
stopWatch();
|
|
}
|
|
}, true);
|
|
}
|
|
}
|
|
|
|
//clear the watch when the property validator is valid again
|
|
function stopWatch() {
|
|
if (watcher) {
|
|
watcher();
|
|
watcher = null;
|
|
}
|
|
}
|
|
|
|
//if there's any remaining errors in the server validation service then we should show them.
|
|
var showValidation = serverValidationManager.items.length > 0;
|
|
var hasError = false;
|
|
|
|
//create properties on our custom scope so we can use it in our template
|
|
scope.errorMsg = "";
|
|
|
|
var unsubscribe = [];
|
|
|
|
//listen for form error changes
|
|
unsubscribe.push(scope.$on("valStatusChanged", function(evt, args) {
|
|
if (args.form.$invalid) {
|
|
|
|
//first we need to check if the valPropertyMsg validity is invalid
|
|
if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) {
|
|
//since we already have an error we'll just return since this means we've already set the
|
|
// hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe
|
|
return;
|
|
}
|
|
else if (element.closest(".umb-control-group").find(".ng-invalid").length > 0) {
|
|
//check if it's one of the properties that is invalid in the current content property
|
|
hasError = true;
|
|
//update the validation message if we don't already have one assigned.
|
|
if (showValidation && scope.errorMsg === "") {
|
|
scope.errorMsg = getErrorMsg();
|
|
}
|
|
}
|
|
else {
|
|
hasError = false;
|
|
scope.errorMsg = "";
|
|
}
|
|
}
|
|
else {
|
|
hasError = false;
|
|
scope.errorMsg = "";
|
|
}
|
|
}, true));
|
|
|
|
//listen for the forms saving event
|
|
unsubscribe.push(scope.$on("formSubmitting", function(ev, args) {
|
|
showValidation = true;
|
|
if (hasError && scope.errorMsg === "") {
|
|
scope.errorMsg = getErrorMsg();
|
|
}
|
|
else if (!hasError) {
|
|
scope.errorMsg = "";
|
|
stopWatch();
|
|
}
|
|
}));
|
|
|
|
//listen for the forms saved event
|
|
unsubscribe.push(scope.$on("formSubmitted", function(ev, args) {
|
|
showValidation = false;
|
|
scope.errorMsg = "";
|
|
formCtrl.$setValidity('valPropertyMsg', true);
|
|
stopWatch();
|
|
}));
|
|
|
|
//listen for server validation changes
|
|
// NOTE: we pass in "" in order to listen for all validation changes to the content property, not for
|
|
// validation changes to fields in the property this is because some server side validators may not
|
|
// return the field name for which the error belongs too, just the property for which it belongs.
|
|
// It's important to note that we need to subscribe to server validation changes here because we always must
|
|
// indicate that a content property is invalid at the property level since developers may not actually implement
|
|
// the correct field validation in their property editors.
|
|
|
|
if (scope.property) { //this can be null if no property was assigned
|
|
serverValidationManager.subscribe(scope.property.alias, "", function (isValid, propertyErrors, allErrors) {
|
|
hasError = !isValid;
|
|
if (hasError) {
|
|
//set the error message to the server message
|
|
scope.errorMsg = propertyErrors[0].errorMsg;
|
|
//flag that the current validator is invalid
|
|
formCtrl.$setValidity('valPropertyMsg', false);
|
|
startWatch();
|
|
}
|
|
else {
|
|
scope.errorMsg = "";
|
|
//flag that the current validator is valid
|
|
formCtrl.$setValidity('valPropertyMsg', true);
|
|
stopWatch();
|
|
}
|
|
});
|
|
|
|
//when the element is disposed we need to unsubscribe!
|
|
// NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain
|
|
// but they are a different callback instance than the above.
|
|
element.bind('$destroy', function () {
|
|
stopWatch();
|
|
serverValidationManager.unsubscribe(scope.property.alias, "");
|
|
});
|
|
}
|
|
|
|
//when the scope is disposed we need to unsubscribe
|
|
scope.$on('$destroy', function () {
|
|
for (var u in unsubscribe) {
|
|
unsubscribe[u]();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
};
|
|
}
|
|
angular.module('umbraco.directives.validation').directive("valPropertyMsg", valPropertyMsg);
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valPropertyValidator
|
|
* @restrict A
|
|
* @description Performs any custom property value validation checks on the client side. This allows property editors to be highly flexible when it comes to validation
|
|
on the client side. Typically if a property editor stores a primitive value (i.e. string) then the client side validation can easily be taken care of
|
|
with standard angular directives such as ng-required. However since some property editors store complex data such as JSON, a given property editor
|
|
might require custom validation. This directive can be used to validate an Umbraco property in any way that a developer would like by specifying a
|
|
callback method to perform the validation. The result of this method must return an object in the format of
|
|
{isValid: true, errorKey: 'required', errorMsg: 'Something went wrong' }
|
|
The error message returned will also be displayed for the property level validation message.
|
|
This directive should only be used when dealing with complex models, if custom validation needs to be performed with primitive values, use the simpler
|
|
angular validation directives instead since this will watch the entire model.
|
|
**/
|
|
|
|
function valPropertyValidator(serverValidationManager) {
|
|
return {
|
|
scope: {
|
|
valPropertyValidator: "="
|
|
},
|
|
|
|
// The element must have ng-model attribute and be inside an umbProperty directive
|
|
require: ['ngModel', '?^umbProperty'],
|
|
|
|
restrict: "A",
|
|
|
|
link: function (scope, element, attrs, ctrls) {
|
|
|
|
var modelCtrl = ctrls[0];
|
|
var propCtrl = ctrls.length > 1 ? ctrls[1] : null;
|
|
|
|
// Check whether the scope has a valPropertyValidator method
|
|
if (!scope.valPropertyValidator || !angular.isFunction(scope.valPropertyValidator)) {
|
|
throw new Error('val-property-validator directive must specify a function to call');
|
|
}
|
|
|
|
var initResult = scope.valPropertyValidator();
|
|
|
|
// Validation method
|
|
var validate = function (viewValue) {
|
|
// Calls the validition method
|
|
var result = scope.valPropertyValidator();
|
|
if (!result.errorKey || result.isValid === undefined || !result.errorMsg) {
|
|
throw "The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg";
|
|
}
|
|
if (result.isValid === true) {
|
|
// Tell the controller that the value is valid
|
|
modelCtrl.$setValidity(result.errorKey, true);
|
|
if (propCtrl) {
|
|
propCtrl.setPropertyError(null);
|
|
}
|
|
}
|
|
else {
|
|
// Tell the controller that the value is invalid
|
|
modelCtrl.$setValidity(result.errorKey, false);
|
|
if (propCtrl) {
|
|
propCtrl.setPropertyError(result.errorMsg);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Parsers are called as soon as the value in the form input is modified
|
|
modelCtrl.$parsers.push(validate);
|
|
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives.validation').directive("valPropertyValidator", valPropertyValidator);
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valRegex
|
|
* @restrict A
|
|
* @description A custom directive to allow for matching a value against a regex string.
|
|
* NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string
|
|
**/
|
|
function valRegex() {
|
|
|
|
return {
|
|
require: 'ngModel',
|
|
restrict: "A",
|
|
link: function (scope, elm, attrs, ctrl) {
|
|
|
|
var flags = "";
|
|
var regex;
|
|
var eventBindings = [];
|
|
|
|
attrs.$observe("valRegexFlags", function (newVal) {
|
|
if (newVal) {
|
|
flags = newVal;
|
|
}
|
|
});
|
|
|
|
attrs.$observe("valRegex", function (newVal) {
|
|
if (newVal) {
|
|
try {
|
|
var resolved = newVal;
|
|
if (resolved) {
|
|
regex = new RegExp(resolved, flags);
|
|
}
|
|
else {
|
|
regex = new RegExp(attrs.valRegex, flags);
|
|
}
|
|
}
|
|
catch (e) {
|
|
regex = new RegExp(attrs.valRegex, flags);
|
|
}
|
|
}
|
|
});
|
|
|
|
eventBindings.push(scope.$watch('ngModel', function(newValue, oldValue){
|
|
if(newValue && newValue !== oldValue) {
|
|
patternValidator(newValue);
|
|
}
|
|
}));
|
|
|
|
var patternValidator = function (viewValue) {
|
|
if (regex) {
|
|
//NOTE: we don't validate on empty values, use required validator for that
|
|
if (!viewValue || regex.test(viewValue.toString())) {
|
|
// it is valid
|
|
ctrl.$setValidity('valRegex', true);
|
|
//assign a message to the validator
|
|
ctrl.errorMsg = "";
|
|
return viewValue;
|
|
}
|
|
else {
|
|
// it is invalid, return undefined (no model update)
|
|
ctrl.$setValidity('valRegex', false);
|
|
//assign a message to the validator
|
|
ctrl.errorMsg = "Value is invalid, it does not match the correct pattern";
|
|
return undefined;
|
|
}
|
|
}
|
|
};
|
|
|
|
scope.$on('$destroy', function(){
|
|
// unbind watchers
|
|
for(var e in eventBindings) {
|
|
eventBindings[e]();
|
|
}
|
|
});
|
|
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives.validation').directive("valRegex", valRegex);
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
function ValRequireComponentDirective() {
|
|
|
|
function link(scope, el, attr, ngModel) {
|
|
|
|
var unbindModelWatcher = scope.$watch(function () {
|
|
return ngModel.$modelValue;
|
|
}, function(newValue) {
|
|
|
|
if(newValue === undefined || newValue === null || newValue === "") {
|
|
ngModel.$setValidity("valRequiredComponent", false);
|
|
} else {
|
|
ngModel.$setValidity("valRequiredComponent", true);
|
|
}
|
|
|
|
});
|
|
|
|
// clean up
|
|
scope.$on('$destroy', function(){
|
|
unbindModelWatcher();
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
require: 'ngModel',
|
|
restrict: "A",
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('valRequireComponent', ValRequireComponentDirective);
|
|
|
|
})();
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valServer
|
|
* @restrict A
|
|
* @description This directive is used to associate a content property with a server-side validation response
|
|
* so that the validators in angular are updated based on server-side feedback.
|
|
**/
|
|
function valServer(serverValidationManager) {
|
|
return {
|
|
require: ['ngModel', '?^umbProperty'],
|
|
restrict: "A",
|
|
link: function (scope, element, attr, ctrls) {
|
|
|
|
var modelCtrl = ctrls[0];
|
|
var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null;
|
|
if (!umbPropCtrl) {
|
|
//we cannot proceed, this validator will be disabled
|
|
return;
|
|
}
|
|
|
|
var watcher = null;
|
|
|
|
//Need to watch the value model for it to change, previously we had subscribed to
|
|
//modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that
|
|
// doesn't specifically have a 2 way ng binding. This is required because when we
|
|
// have a server error we actually invalidate the form which means it cannot be
|
|
// resubmitted. So once a field is changed that has a server error assigned to it
|
|
// we need to re-validate it for the server side validator so the user can resubmit
|
|
// the form. Of course normal client-side validators will continue to execute.
|
|
function startWatch() {
|
|
//if there's not already a watch
|
|
if (!watcher) {
|
|
watcher = scope.$watch(function () {
|
|
return modelCtrl.$modelValue;
|
|
}, function (newValue, oldValue) {
|
|
|
|
if (!newValue || angular.equals(newValue, oldValue)) {
|
|
return;
|
|
}
|
|
|
|
if (modelCtrl.$invalid) {
|
|
modelCtrl.$setValidity('valServer', true);
|
|
stopWatch();
|
|
}
|
|
}, true);
|
|
}
|
|
}
|
|
|
|
function stopWatch() {
|
|
if (watcher) {
|
|
watcher();
|
|
watcher = null;
|
|
}
|
|
}
|
|
|
|
var currentProperty = umbPropCtrl.property;
|
|
|
|
//default to 'value' if nothing is set
|
|
var fieldName = "value";
|
|
if (attr.valServer) {
|
|
fieldName = scope.$eval(attr.valServer);
|
|
if (!fieldName) {
|
|
//eval returned nothing so just use the string
|
|
fieldName = attr.valServer;
|
|
}
|
|
}
|
|
|
|
//subscribe to the server validation changes
|
|
serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) {
|
|
if (!isValid) {
|
|
modelCtrl.$setValidity('valServer', false);
|
|
//assign an error msg property to the current validator
|
|
modelCtrl.errorMsg = propertyErrors[0].errorMsg;
|
|
startWatch();
|
|
}
|
|
else {
|
|
modelCtrl.$setValidity('valServer', true);
|
|
//reset the error message
|
|
modelCtrl.errorMsg = "";
|
|
stopWatch();
|
|
}
|
|
});
|
|
|
|
//when the element is disposed we need to unsubscribe!
|
|
// NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain
|
|
// but they are a different callback instance than the above.
|
|
element.bind('$destroy', function () {
|
|
stopWatch();
|
|
serverValidationManager.unsubscribe(currentProperty.alias, fieldName);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives.validation').directive("valServer", valServer);
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valServerField
|
|
* @restrict A
|
|
* @description This directive is used to associate a content field (not user defined) with a server-side validation response
|
|
* so that the validators in angular are updated based on server-side feedback.
|
|
**/
|
|
function valServerField(serverValidationManager) {
|
|
return {
|
|
require: 'ngModel',
|
|
restrict: "A",
|
|
link: function (scope, element, attr, ctrl) {
|
|
|
|
var fieldName = null;
|
|
var eventBindings = [];
|
|
|
|
attr.$observe("valServerField", function (newVal) {
|
|
if (newVal && fieldName === null) {
|
|
fieldName = newVal;
|
|
|
|
//subscribe to the changed event of the view model. This is required because when we
|
|
// have a server error we actually invalidate the form which means it cannot be
|
|
// resubmitted. So once a field is changed that has a server error assigned to it
|
|
// we need to re-validate it for the server side validator so the user can resubmit
|
|
// the form. Of course normal client-side validators will continue to execute.
|
|
eventBindings.push(scope.$watch('ngModel', function(newValue){
|
|
if (ctrl.$invalid) {
|
|
ctrl.$setValidity('valServerField', true);
|
|
}
|
|
}));
|
|
|
|
//subscribe to the server validation changes
|
|
serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) {
|
|
if (!isValid) {
|
|
ctrl.$setValidity('valServerField', false);
|
|
//assign an error msg property to the current validator
|
|
ctrl.errorMsg = fieldErrors[0].errorMsg;
|
|
}
|
|
else {
|
|
ctrl.$setValidity('valServerField', true);
|
|
//reset the error message
|
|
ctrl.errorMsg = "";
|
|
}
|
|
});
|
|
|
|
//when the element is disposed we need to unsubscribe!
|
|
// NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain
|
|
// but they are a different callback instance than the above.
|
|
element.bind('$destroy', function () {
|
|
serverValidationManager.unsubscribe(null, fieldName);
|
|
});
|
|
}
|
|
});
|
|
|
|
scope.$on('$destroy', function(){
|
|
// unbind watchers
|
|
for(var e in eventBindings) {
|
|
eventBindings[e]();
|
|
}
|
|
});
|
|
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives.validation').directive("valServerField", valServerField);
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valSubView
|
|
* @restrict A
|
|
* @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data.
|
|
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
|
|
**/
|
|
(function() {
|
|
'use strict';
|
|
|
|
function valSubViewDirective() {
|
|
|
|
function link(scope, el, attr, ctrl) {
|
|
|
|
var valFormManager = ctrl[1];
|
|
scope.subView.hasError = false;
|
|
|
|
//listen for form validation changes
|
|
valFormManager.onValidationStatusChanged(function (evt, args) {
|
|
if (!args.form.$valid) {
|
|
|
|
var subViewContent = el.find(".ng-invalid");
|
|
|
|
if (subViewContent.length > 0) {
|
|
scope.subView.hasError = true;
|
|
} else {
|
|
scope.subView.hasError = false;
|
|
}
|
|
|
|
}
|
|
else {
|
|
scope.subView.hasError = false;
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
var directive = {
|
|
require: ['^form', '^valFormManager'],
|
|
restrict: "A",
|
|
link: link
|
|
};
|
|
|
|
return directive;
|
|
}
|
|
|
|
angular.module('umbraco.directives').directive('valSubView', valSubViewDirective);
|
|
|
|
})();
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valTab
|
|
* @restrict A
|
|
* @description Used to show validation warnings for a tab to indicate that the tab content has validations errors in its data.
|
|
* In order for this directive to work, the valFormManager directive must be placed on the containing form.
|
|
**/
|
|
function valTab() {
|
|
return {
|
|
require: ['^form', '^valFormManager'],
|
|
restrict: "A",
|
|
link: function (scope, element, attr, ctrs) {
|
|
|
|
var valFormManager = ctrs[1];
|
|
var tabId = "tab" + scope.tab.id;
|
|
scope.tabHasError = false;
|
|
|
|
//listen for form validation changes
|
|
valFormManager.onValidationStatusChanged(function (evt, args) {
|
|
if (!args.form.$valid) {
|
|
var tabContent = element.closest(".umb-panel").find("#" + tabId);
|
|
//check if the validation messages are contained inside of this tabs
|
|
if (tabContent.find(".ng-invalid").length > 0) {
|
|
scope.tabHasError = true;
|
|
} else {
|
|
scope.tabHasError = false;
|
|
}
|
|
}
|
|
else {
|
|
scope.tabHasError = false;
|
|
}
|
|
});
|
|
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.directives.validation').directive("valTab", valTab);
|
|
function valToggleMsg(serverValidationManager) {
|
|
return {
|
|
require: "^form",
|
|
restrict: "A",
|
|
|
|
/**
|
|
Our directive requries a reference to a form controller which gets passed in to this parameter
|
|
*/
|
|
link: function (scope, element, attr, formCtrl) {
|
|
|
|
if (!attr.valToggleMsg){
|
|
throw "valToggleMsg requires that a reference to a validator is specified";
|
|
}
|
|
if (!attr.valMsgFor){
|
|
throw "valToggleMsg requires that the attribute valMsgFor exists on the element";
|
|
}
|
|
if (!formCtrl[attr.valMsgFor]) {
|
|
throw "valToggleMsg cannot find field " + attr.valMsgFor + " on form " + formCtrl.$name;
|
|
}
|
|
|
|
//if there's any remaining errors in the server validation service then we should show them.
|
|
var showValidation = serverValidationManager.items.length > 0;
|
|
var hasCustomMsg = element.contents().length > 0;
|
|
|
|
//add a watch to the validator for the value (i.e. myForm.value.$error.required )
|
|
scope.$watch(function () {
|
|
//sometimes if a dialog closes in the middle of digest we can get null references here
|
|
|
|
return (formCtrl && formCtrl[attr.valMsgFor]) ? formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] : null;
|
|
}, function () {
|
|
//sometimes if a dialog closes in the middle of digest we can get null references here
|
|
if ((formCtrl && formCtrl[attr.valMsgFor])) {
|
|
if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] && showValidation) {
|
|
element.show();
|
|
//display the error message if this element has no contents
|
|
if (!hasCustomMsg) {
|
|
element.html(formCtrl[attr.valMsgFor].errorMsg);
|
|
}
|
|
}
|
|
else {
|
|
element.hide();
|
|
}
|
|
}
|
|
});
|
|
|
|
var unsubscribe = [];
|
|
|
|
//listen for the saving event (the result is a callback method which is called to unsubscribe)
|
|
unsubscribe.push(scope.$on("formSubmitting", function(ev, args) {
|
|
showValidation = true;
|
|
if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg]) {
|
|
element.show();
|
|
//display the error message if this element has no contents
|
|
if (!hasCustomMsg) {
|
|
element.html(formCtrl[attr.valMsgFor].errorMsg);
|
|
}
|
|
}
|
|
else {
|
|
element.hide();
|
|
}
|
|
}));
|
|
|
|
//listen for the saved event (the result is a callback method which is called to unsubscribe)
|
|
unsubscribe.push(scope.$on("formSubmitted", function(ev, args) {
|
|
showValidation = false;
|
|
element.hide();
|
|
}));
|
|
|
|
//when the element is disposed we need to unsubscribe!
|
|
// NOTE: this is very important otherwise if this directive is part of a modal, the listener still exists because the dom
|
|
// element might still be there even after the modal has been hidden.
|
|
element.bind('$destroy', function () {
|
|
for (var u in unsubscribe) {
|
|
unsubscribe[u]();
|
|
}
|
|
});
|
|
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name umbraco.directives.directive:valToggleMsg
|
|
* @restrict A
|
|
* @element input
|
|
* @requires formController
|
|
* @description This directive will show/hide an error based on: is the value + the given validator invalid? AND, has the form been submitted ?
|
|
**/
|
|
angular.module('umbraco.directives.validation').directive("valToggleMsg", valToggleMsg);
|
|
angular.module('umbraco.directives.validation')
|
|
.directive('valTriggerChange', function($sniffer) {
|
|
return {
|
|
link : function(scope, elem, attrs) {
|
|
elem.bind('click', function(){
|
|
$(attrs.valTriggerChange).trigger($sniffer.hasEvent('input') ? 'input' : 'change');
|
|
});
|
|
},
|
|
priority : 1
|
|
};
|
|
});
|
|
|
|
})(); |