/AarhusWebDevCoop/AarhusWebDevCoop/Umbraco/Js/umbraco.services.js
https://bitbucket.org/andreas_nuppenau/obligatorisk_opgave · JavaScript · 9774 lines · 8591 code · 0 blank · 1183 comment · 265 complexity · a7832eb5bc67218b527fd6e0e3a7c7ce MD5 · raw file
- (function () {
- angular.module('umbraco.services', [
- 'umbraco.security',
- 'umbraco.resources'
- ]);
- /**
- * @ngdoc service
- * @name umbraco.services.angularHelper
- * @function
- *
- * @description
- * Some angular helper/extension methods
- */
- function angularHelper($log, $q) {
- return {
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#rejectedPromise
- * @methodOf umbraco.services.angularHelper
- * @function
- *
- * @description
- * In some situations we need to return a promise as a rejection, normally based on invalid data. This
- * is a wrapper to do that so we can save on writing a bit of code.
- *
- * @param {object} objReject The object to send back with the promise rejection
- */
- rejectedPromise: function (objReject) {
- var deferred = $q.defer();
- //return an error object including the error message for UI
- deferred.reject(objReject);
- return deferred.promise;
- },
- /**
- * @ngdoc function
- * @name safeApply
- * @methodOf umbraco.services.angularHelper
- * @function
- *
- * @description
- * This checks if a digest/apply is already occuring, if not it will force an apply call
- */
- safeApply: function (scope, fn) {
- if (scope.$$phase || scope.$root.$$phase) {
- if (angular.isFunction(fn)) {
- fn();
- }
- } else {
- if (angular.isFunction(fn)) {
- scope.$apply(fn);
- } else {
- scope.$apply();
- }
- }
- },
- /**
- * @ngdoc function
- * @name getCurrentForm
- * @methodOf umbraco.services.angularHelper
- * @function
- *
- * @description
- * Returns the current form object applied to the scope or null if one is not found
- */
- getCurrentForm: function (scope) {
- //NOTE: There isn't a way in angular to get a reference to the current form object since the form object
- // is just defined as a property of the scope when it is named but you'll always need to know the name which
- // isn't very convenient. If we want to watch for validation changes we need to get a form reference.
- // The way that we detect the form object is a bit hackerific in that we detect all of the required properties
- // that exist on a form object.
- //
- //The other way to do it in a directive is to require "^form", but in a controller the only other way to do it
- // is to inject the $element object and use: $element.inheritedData('$formController');
- var form = null;
- //var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$invalid", "$addControl", "$removeControl", "$setValidity", "$setDirty"];
- var requiredFormProps = [
- '$addControl',
- '$removeControl',
- '$setValidity',
- '$setDirty',
- '$setPristine'
- ];
- // a method to check that the collection of object prop names contains the property name expected
- function propertyExists(objectPropNames) {
- //ensure that every required property name exists on the current scope property
- return _.every(requiredFormProps, function (item) {
- return _.contains(objectPropNames, item);
- });
- }
- for (var p in scope) {
- if (_.isObject(scope[p]) && p !== 'this' && p.substr(0, 1) !== '$') {
- //get the keys of the property names for the current property
- var props = _.keys(scope[p]);
- //if the length isn't correct, try the next prop
- if (props.length < requiredFormProps.length) {
- continue;
- }
- //ensure that every required property name exists on the current scope property
- var containProperty = propertyExists(props);
- if (containProperty) {
- form = scope[p];
- break;
- }
- }
- }
- return form;
- },
- /**
- * @ngdoc function
- * @name validateHasForm
- * @methodOf umbraco.services.angularHelper
- * @function
- *
- * @description
- * This will validate that the current scope has an assigned form object, if it doesn't an exception is thrown, if
- * it does we return the form object.
- */
- getRequiredCurrentForm: function (scope) {
- var currentForm = this.getCurrentForm(scope);
- if (!currentForm || !currentForm.$name) {
- throw 'The current scope requires a current form object (or ng-form) with a name assigned to it';
- }
- return currentForm;
- },
- /**
- * @ngdoc function
- * @name getNullForm
- * @methodOf umbraco.services.angularHelper
- * @function
- *
- * @description
- * Returns a null angular FormController, mostly for use in unit tests
- * NOTE: This is actually the same construct as angular uses internally for creating a null form but they don't expose
- * any of this publicly to us, so we need to create our own.
- *
- * @param {string} formName The form name to assign
- */
- getNullForm: function (formName) {
- return {
- $addControl: angular.noop,
- $removeControl: angular.noop,
- $setValidity: angular.noop,
- $setDirty: angular.noop,
- $setPristine: angular.noop,
- $name: formName //NOTE: we don't include the 'properties', just the methods.
- };
- }
- };
- }
- angular.module('umbraco.services').factory('angularHelper', angularHelper);
- /**
- * @ngdoc service
- * @name umbraco.services.appState
- * @function
- *
- * @description
- * Tracks the various application state variables when working in the back office, raises events when state changes.
- *
- * ##Samples
- *
- * ####Subscribe to global state changes:
- *
- * <pre>
- * scope.showTree = appState.getGlobalState("showNavigation");
- *
- * eventsService.on("appState.globalState.changed", function (e, args) {
- * if (args.key === "showNavigation") {
- * scope.showTree = args.value;
- * }
- * });
- * </pre>
- *
- * ####Subscribe to section-state changes
- *
- * <pre>
- * scope.currentSection = appState.getSectionState("currentSection");
- *
- * eventsService.on("appState.sectionState.changed", function (e, args) {
- * if (args.key === "currentSection") {
- * scope.currentSection = args.value;
- * }
- * });
- * </pre>
- */
- function appState(eventsService) {
- //Define all variables here - we are never returning this objects so they cannot be publicly mutable
- // changed, we only expose methods to interact with the values.
- var globalState = {
- showNavigation: null,
- touchDevice: null,
- showTray: null,
- stickyNavigation: null,
- navMode: null,
- isReady: null,
- isTablet: null
- };
- var sectionState = {
- //The currently active section
- currentSection: null,
- showSearchResults: null
- };
- var treeState = {
- //The currently selected node
- selectedNode: null,
- //The currently loaded root node reference - depending on the section loaded this could be a section root or a normal root.
- //We keep this reference so we can lookup nodes to interact with in the UI via the tree service
- currentRootNode: null
- };
- var menuState = {
- //this list of menu items to display
- menuActions: null,
- //the title to display in the context menu dialog
- dialogTitle: null,
- //The tree node that the ctx menu is launched for
- currentNode: null,
- //Whether the menu's dialog is being shown or not
- showMenuDialog: null,
- //Whether the context menu is being shown or not
- showMenu: null
- };
- /** function to validate and set the state on a state object */
- function setState(stateObj, key, value, stateObjName) {
- if (!_.has(stateObj, key)) {
- throw 'The variable ' + key + ' does not exist in ' + stateObjName;
- }
- var changed = stateObj[key] !== value;
- stateObj[key] = value;
- if (changed) {
- eventsService.emit('appState.' + stateObjName + '.changed', {
- key: key,
- value: value
- });
- }
- }
- /** function to validate and set the state on a state object */
- function getState(stateObj, key, stateObjName) {
- if (!_.has(stateObj, key)) {
- throw 'The variable ' + key + ' does not exist in ' + stateObjName;
- }
- return stateObj[key];
- }
- return {
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#getGlobalState
- * @methodOf umbraco.services.appState
- * @function
- *
- * @description
- * Returns the current global state value by key - we do not return an object reference here - we do NOT want this
- * to be publicly mutable and allow setting arbitrary values
- *
- */
- getGlobalState: function (key) {
- return getState(globalState, key, 'globalState');
- },
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#setGlobalState
- * @methodOf umbraco.services.appState
- * @function
- *
- * @description
- * Sets a global state value by key
- *
- */
- setGlobalState: function (key, value) {
- setState(globalState, key, value, 'globalState');
- },
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#getSectionState
- * @methodOf umbraco.services.appState
- * @function
- *
- * @description
- * Returns the current section state value by key - we do not return an object here - we do NOT want this
- * to be publicly mutable and allow setting arbitrary values
- *
- */
- getSectionState: function (key) {
- return getState(sectionState, key, 'sectionState');
- },
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#setSectionState
- * @methodOf umbraco.services.appState
- * @function
- *
- * @description
- * Sets a section state value by key
- *
- */
- setSectionState: function (key, value) {
- setState(sectionState, key, value, 'sectionState');
- },
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#getTreeState
- * @methodOf umbraco.services.appState
- * @function
- *
- * @description
- * Returns the current tree state value by key - we do not return an object here - we do NOT want this
- * to be publicly mutable and allow setting arbitrary values
- *
- */
- getTreeState: function (key) {
- return getState(treeState, key, 'treeState');
- },
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#setTreeState
- * @methodOf umbraco.services.appState
- * @function
- *
- * @description
- * Sets a section state value by key
- *
- */
- setTreeState: function (key, value) {
- setState(treeState, key, value, 'treeState');
- },
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#getMenuState
- * @methodOf umbraco.services.appState
- * @function
- *
- * @description
- * Returns the current menu state value by key - we do not return an object here - we do NOT want this
- * to be publicly mutable and allow setting arbitrary values
- *
- */
- getMenuState: function (key) {
- return getState(menuState, key, 'menuState');
- },
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#setMenuState
- * @methodOf umbraco.services.appState
- * @function
- *
- * @description
- * Sets a section state value by key
- *
- */
- setMenuState: function (key, value) {
- setState(menuState, key, value, 'menuState');
- }
- };
- }
- angular.module('umbraco.services').factory('appState', appState);
- /**
- * @ngdoc service
- * @name umbraco.services.editorState
- * @function
- *
- * @description
- * Tracks the parent object for complex editors by exposing it as
- * an object reference via editorState.current.entity
- *
- * it is possible to modify this object, so should be used with care
- */
- angular.module('umbraco.services').factory('editorState', function () {
- var current = null;
- var state = {
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#set
- * @methodOf umbraco.services.editorState
- * @function
- *
- * @description
- * Sets the current entity object for the currently active editor
- * This is only used when implementing an editor with a complex model
- * like the content editor, where the model is modified by several
- * child controllers.
- */
- set: function (entity) {
- current = entity;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#reset
- * @methodOf umbraco.services.editorState
- * @function
- *
- * @description
- * Since the editorstate entity is read-only, you cannot set it to null
- * only through the reset() method
- */
- reset: function () {
- current = null;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.angularHelper#getCurrent
- * @methodOf umbraco.services.editorState
- * @function
- *
- * @description
- * Returns an object reference to the current editor entity.
- * the entity is the root object of the editor.
- * EditorState is used by property/parameter editors that need
- * access to the entire entity being edited, not just the property/parameter
- *
- * editorState.current can not be overwritten, you should only read values from it
- * since modifying individual properties should be handled by the property editors
- */
- getCurrent: function () {
- return current;
- }
- };
- //TODO: This shouldn't be removed! use getCurrent() method instead of a hacked readonly property which is confusing.
- //create a get/set property but don't allow setting
- Object.defineProperty(state, 'current', {
- get: function () {
- return current;
- },
- set: function (value) {
- throw 'Use editorState.set to set the value of the current entity';
- }
- });
- return state;
- });
- /**
- * @ngdoc service
- * @name umbraco.services.assetsService
- *
- * @requires $q
- * @requires angularHelper
- *
- * @description
- * Promise-based utillity service to lazy-load client-side dependencies inside angular controllers.
- *
- * ##usage
- * To use, simply inject the assetsService into any controller that needs it, and make
- * sure the umbraco.services module is accesible - which it should be by default.
- *
- * <pre>
- * angular.module("umbraco").controller("my.controller". function(assetsService){
- * assetsService.load(["script.js", "styles.css"], $scope).then(function(){
- * //this code executes when the dependencies are done loading
- * });
- * });
- * </pre>
- *
- * You can also load individual files, which gives you greater control over what attibutes are passed to the file, as well as timeout
- *
- * <pre>
- * angular.module("umbraco").controller("my.controller". function(assetsService){
- * assetsService.loadJs("script.js", $scope, {charset: 'utf-8'}, 10000 }).then(function(){
- * //this code executes when the script is done loading
- * });
- * });
- * </pre>
- *
- * For these cases, there are 2 individual methods, one for javascript, and one for stylesheets:
- *
- * <pre>
- * angular.module("umbraco").controller("my.controller". function(assetsService){
- * assetsService.loadCss("stye.css", $scope, {media: 'print'}, 10000 }).then(function(){
- * //loadcss cannot determine when the css is done loading, so this will trigger instantly
- * });
- * });
- * </pre>
- */
- angular.module('umbraco.services').factory('assetsService', function ($q, $log, angularHelper, umbRequestHelper, $rootScope, $http) {
- var initAssetsLoaded = false;
- function appendRnd(url) {
- //if we don't have a global umbraco obj yet, the app is bootstrapping
- if (!Umbraco.Sys.ServerVariables.application) {
- return url;
- }
- var rnd = Umbraco.Sys.ServerVariables.application.cacheBuster;
- var _op = url.indexOf('?') > 0 ? '&' : '?';
- url = url + _op + 'umb__rnd=' + rnd;
- return url;
- }
- ;
- function convertVirtualPath(path) {
- //make this work for virtual paths
- if (path.startsWith('~/')) {
- path = umbRequestHelper.convertVirtualToAbsolutePath(path);
- }
- return path;
- }
- var service = {
- loadedAssets: {},
- _getAssetPromise: function (path) {
- if (this.loadedAssets[path]) {
- return this.loadedAssets[path];
- } else {
- var deferred = $q.defer();
- this.loadedAssets[path] = {
- deferred: deferred,
- state: 'new',
- path: path
- };
- return this.loadedAssets[path];
- }
- },
- /**
- Internal method. This is called when the application is loading and the user is already authenticated, or once the user is authenticated.
- There's a few assets the need to be loaded for the application to function but these assets require authentication to load.
- */
- _loadInitAssets: function () {
- var deferred = $q.defer();
- //here we need to ensure the required application assets are loaded
- if (initAssetsLoaded === false) {
- var self = this;
- self.loadJs(umbRequestHelper.getApiUrl('serverVarsJs', '', ''), $rootScope).then(function () {
- initAssetsLoaded = true;
- //now we need to go get the legacyTreeJs - but this can be done async without waiting.
- self.loadJs(umbRequestHelper.getApiUrl('legacyTreeJs', '', ''), $rootScope);
- deferred.resolve();
- });
- } else {
- deferred.resolve();
- }
- return deferred.promise;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.assetsService#loadCss
- * @methodOf umbraco.services.assetsService
- *
- * @description
- * Injects a file as a stylesheet into the document head
- *
- * @param {String} path path to the css file to load
- * @param {Scope} scope optional scope to pass into the loader
- * @param {Object} keyvalue collection of attributes to pass to the stylesheet element
- * @param {Number} timeout in milliseconds
- * @returns {Promise} Promise object which resolves when the file has loaded
- */
- loadCss: function (path, scope, attributes, timeout) {
- path = convertVirtualPath(path);
- var asset = this._getAssetPromise(path);
- // $q.defer();
- var t = timeout || 5000;
- var a = attributes || undefined;
- if (asset.state === 'new') {
- asset.state = 'loading';
- LazyLoad.css(appendRnd(path), function () {
- if (!scope) {
- asset.state = 'loaded';
- asset.deferred.resolve(true);
- } else {
- asset.state = 'loaded';
- angularHelper.safeApply(scope, function () {
- asset.deferred.resolve(true);
- });
- }
- });
- } else if (asset.state === 'loaded') {
- asset.deferred.resolve(true);
- }
- return asset.deferred.promise;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.assetsService#loadJs
- * @methodOf umbraco.services.assetsService
- *
- * @description
- * Injects a file as a javascript into the document
- *
- * @param {String} path path to the js file to load
- * @param {Scope} scope optional scope to pass into the loader
- * @param {Object} keyvalue collection of attributes to pass to the script element
- * @param {Number} timeout in milliseconds
- * @returns {Promise} Promise object which resolves when the file has loaded
- */
- loadJs: function (path, scope, attributes, timeout) {
- path = convertVirtualPath(path);
- var asset = this._getAssetPromise(path);
- // $q.defer();
- var t = timeout || 5000;
- var a = attributes || undefined;
- if (asset.state === 'new') {
- asset.state = 'loading';
- LazyLoad.js(appendRnd(path), function () {
- if (!scope) {
- asset.state = 'loaded';
- asset.deferred.resolve(true);
- } else {
- asset.state = 'loaded';
- angularHelper.safeApply(scope, function () {
- asset.deferred.resolve(true);
- });
- }
- });
- } else if (asset.state === 'loaded') {
- asset.deferred.resolve(true);
- }
- return asset.deferred.promise;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.assetsService#load
- * @methodOf umbraco.services.assetsService
- *
- * @description
- * Injects a collection of css and js files
- *
- *
- * @param {Array} pathArray string array of paths to the files to load
- * @param {Scope} scope optional scope to pass into the loader
- * @returns {Promise} Promise object which resolves when all the files has loaded
- */
- load: function (pathArray, scope) {
- var promise;
- if (!angular.isArray(pathArray)) {
- throw 'pathArray must be an array';
- }
- // Check to see if there's anything to load, resolve promise if not
- var nonEmpty = _.reject(pathArray, function (item) {
- return item === undefined || item === '';
- });
- if (nonEmpty.length === 0) {
- var deferred = $q.defer();
- promise = deferred.promise;
- deferred.resolve(true);
- return promise;
- }
- //compile a list of promises
- //blocking
- var promises = [];
- var assets = [];
- _.each(nonEmpty, function (path) {
- path = convertVirtualPath(path);
- var asset = service._getAssetPromise(path);
- //if not previously loaded, add to list of promises
- if (asset.state !== 'loaded') {
- if (asset.state === 'new') {
- asset.state = 'loading';
- assets.push(asset);
- }
- //we need to always push to the promises collection to monitor correct
- //execution
- promises.push(asset.deferred.promise);
- }
- });
- //gives a central monitoring of all assets to load
- promise = $q.all(promises);
- // Split into css and js asset arrays, and use LazyLoad on each array
- var cssAssets = _.filter(assets, function (asset) {
- return asset.path.match(/(\.css$|\.css\?)/ig);
- });
- var jsAssets = _.filter(assets, function (asset) {
- return asset.path.match(/(\.js$|\.js\?)/ig);
- });
- function assetLoaded(asset) {
- asset.state = 'loaded';
- if (!scope) {
- asset.deferred.resolve(true);
- return;
- }
- angularHelper.safeApply(scope, function () {
- asset.deferred.resolve(true);
- });
- }
- if (cssAssets.length > 0) {
- var cssPaths = _.map(cssAssets, function (asset) {
- return appendRnd(asset.path);
- });
- LazyLoad.css(cssPaths, function () {
- _.each(cssAssets, assetLoaded);
- });
- }
- if (jsAssets.length > 0) {
- var jsPaths = _.map(jsAssets, function (asset) {
- return appendRnd(asset.path);
- });
- LazyLoad.js(jsPaths, function () {
- _.each(jsAssets, assetLoaded);
- });
- }
- return promise;
- }
- };
- return service;
- });
- /**
- * @ngdoc service
- * @name umbraco.services.contentEditingHelper
- * @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by
- * all editors to share logic and reduce the amount of replicated code among editors.
- **/
- function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState) {
- function isValidIdentifier(id) {
- //empty id <= 0
- if (angular.isNumber(id) && id > 0) {
- return true;
- }
- //empty guid
- if (id === '00000000-0000-0000-0000-000000000000') {
- return false;
- }
- //empty string / alias
- if (id === '') {
- return false;
- }
- return true;
- }
- return {
- /** Used by the content editor and mini content editor to perform saving operations */
- //TODO: Make this a more helpful/reusable method for other form operations! we can simplify this form most forms
- contentEditorPerformSave: function (args) {
- if (!angular.isObject(args)) {
- throw 'args must be an object';
- }
- if (!args.scope) {
- throw 'args.scope is not defined';
- }
- if (!args.content) {
- throw 'args.content is not defined';
- }
- if (!args.statusMessage) {
- throw 'args.statusMessage is not defined';
- }
- if (!args.saveMethod) {
- throw 'args.saveMethod is not defined';
- }
- var redirectOnFailure = args.redirectOnFailure !== undefined ? args.redirectOnFailure : true;
- var self = this;
- //we will use the default one for content if not specified
- var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback;
- var deferred = $q.defer();
- if (!args.scope.busy && formHelper.submitForm({
- scope: args.scope,
- statusMessage: args.statusMessage,
- action: args.action
- })) {
- args.scope.busy = true;
- args.saveMethod(args.content, $routeParams.create, fileManager.getFiles()).then(function (data) {
- formHelper.resetForm({
- scope: args.scope,
- notifications: data.notifications
- });
- self.handleSuccessfulSave({
- scope: args.scope,
- savedContent: data,
- rebindCallback: function () {
- rebindCallback.apply(self, [
- args.content,
- data
- ]);
- }
- });
- args.scope.busy = false;
- deferred.resolve(data);
- }, function (err) {
- self.handleSaveError({
- redirectOnFailure: redirectOnFailure,
- err: err,
- rebindCallback: function () {
- rebindCallback.apply(self, [
- args.content,
- err.data
- ]);
- }
- });
- //show any notifications
- if (angular.isArray(err.data.notifications)) {
- for (var i = 0; i < err.data.notifications.length; i++) {
- notificationsService.showNotification(err.data.notifications[i]);
- }
- }
- args.scope.busy = false;
- deferred.reject(err);
- });
- } else {
- deferred.reject();
- }
- return deferred.promise;
- },
- /** Returns the action button definitions based on what permissions the user has.
- The content.allowedActions parameter contains a list of chars, each represents a button by permission so
- here we'll build the buttons according to the chars of the user. */
- configureContentEditorButtons: function (args) {
- if (!angular.isObject(args)) {
- throw 'args must be an object';
- }
- if (!args.content) {
- throw 'args.content is not defined';
- }
- if (!args.methods) {
- throw 'args.methods is not defined';
- }
- if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.save || !args.methods.unPublish) {
- throw 'args.methods does not contain all required defined methods';
- }
- var buttons = {
- defaultButton: null,
- subButtons: []
- };
- function createButtonDefinition(ch) {
- switch (ch) {
- case 'U':
- //publish action
- return {
- letter: ch,
- labelKey: 'buttons_saveAndPublish',
- handler: args.methods.saveAndPublish,
- hotKey: 'ctrl+p',
- hotKeyWhenHidden: true
- };
- case 'H':
- //send to publish
- return {
- letter: ch,
- labelKey: 'buttons_saveToPublish',
- handler: args.methods.sendToPublish,
- hotKey: 'ctrl+p',
- hotKeyWhenHidden: true
- };
- case 'A':
- //save
- return {
- letter: ch,
- labelKey: 'buttons_save',
- handler: args.methods.save,
- hotKey: 'ctrl+s',
- hotKeyWhenHidden: true
- };
- case 'Z':
- //unpublish
- return {
- letter: ch,
- labelKey: 'content_unPublish',
- handler: args.methods.unPublish,
- hotKey: 'ctrl+u',
- hotKeyWhenHidden: true
- };
- default:
- return null;
- }
- }
- //reset
- buttons.subButtons = [];
- //This is the ideal button order but depends on circumstance, we'll use this array to create the button list
- // Publish, SendToPublish, Save
- var buttonOrder = [
- 'U',
- 'H',
- 'A'
- ];
- //Create the first button (primary button)
- //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item.
- //Another tricky rule is if they only have Create + Browse permissions but not Save but if it's being created then they will
- // require the Save button in order to create.
- //So this code is going to create the primary button (either Publish, SendToPublish, Save) if we are not in create mode
- // or if the user has access to create.
- if (!args.create || _.contains(args.content.allowedActions, 'C')) {
- for (var b in buttonOrder) {
- if (_.contains(args.content.allowedActions, buttonOrder[b])) {
- buttons.defaultButton = createButtonDefinition(buttonOrder[b]);
- break;
- }
- }
- //Here's the special check, if the button still isn't set and we are creating and they have create access
- //we need to add the Save button
- if (!buttons.defaultButton && args.create && _.contains(args.content.allowedActions, 'C')) {
- buttons.defaultButton = createButtonDefinition('A');
- }
- }
- //Now we need to make the drop down button list, this is also slightly tricky because:
- //We cannot have any buttons if there's no default button above.
- //We cannot have the unpublish button (Z) when there's no publish permission.
- //We cannot have the unpublish button (Z) when the item is not published.
- if (buttons.defaultButton) {
- //get the last index of the button order
- var lastIndex = _.indexOf(buttonOrder, buttons.defaultButton.letter);
- //add the remaining
- for (var i = lastIndex + 1; i < buttonOrder.length; i++) {
- if (_.contains(args.content.allowedActions, buttonOrder[i])) {
- buttons.subButtons.push(createButtonDefinition(buttonOrder[i]));
- }
- }
- //if we are not creating, then we should add unpublish too,
- // so long as it's already published and if the user has access to publish
- if (!args.create) {
- if (args.content.publishDate && _.contains(args.content.allowedActions, 'U')) {
- buttons.subButtons.push(createButtonDefinition('Z'));
- }
- }
- }
- return buttons;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.contentEditingHelper#getAllProps
- * @methodOf umbraco.services.contentEditingHelper
- * @function
- *
- * @description
- * Returns all propertes contained for the content item (since the normal model has properties contained inside of tabs)
- */
- getAllProps: function (content) {
- var allProps = [];
- for (var i = 0; i < content.tabs.length; i++) {
- for (var p = 0; p < content.tabs[i].properties.length; p++) {
- allProps.push(content.tabs[i].properties[p]);
- }
- }
- return allProps;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.contentEditingHelper#configureButtons
- * @methodOf umbraco.services.contentEditingHelper
- * @function
- *
- * @description
- * Returns a letter array for buttons, with the primary one first based on content model, permissions and editor state
- */
- getAllowedActions: function (content, creating) {
- //This is the ideal button order but depends on circumstance, we'll use this array to create the button list
- // Publish, SendToPublish, Save
- var actionOrder = [
- 'U',
- 'H',
- 'A'
- ];
- var defaultActions;
- var actions = [];
- //Create the first button (primary button)
- //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item.
- if (!creating || _.contains(content.allowedActions, 'C')) {
- for (var b in actionOrder) {
- if (_.contains(content.allowedActions, actionOrder[b])) {
- defaultAction = actionOrder[b];
- break;
- }
- }
- }
- actions.push(defaultAction);
- //Now we need to make the drop down button list, this is also slightly tricky because:
- //We cannot have any buttons if there's no default button above.
- //We cannot have the unpublish button (Z) when there's no publish permission.
- //We cannot have the unpublish button (Z) when the item is not published.
- if (defaultAction) {
- //get the last index of the button order
- var lastIndex = _.indexOf(actionOrder, defaultAction);
- //add the remaining
- for (var i = lastIndex + 1; i < actionOrder.length; i++) {
- if (_.contains(content.allowedActions, actionOrder[i])) {
- actions.push(actionOrder[i]);
- }
- }
- //if we are not creating, then we should add unpublish too,
- // so long as it's already published and if the user has access to publish
- if (!creating) {
- if (content.publishDate && _.contains(content.allowedActions, 'U')) {
- actions.push('Z');
- }
- }
- }
- return actions;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.contentEditingHelper#getButtonFromAction
- * @methodOf umbraco.services.contentEditingHelper
- * @function
- *
- * @description
- * Returns a button object to render a button for the tabbed editor
- * currently only returns built in system buttons for content and media actions
- * returns label, alias, action char and hot-key
- */
- getButtonFromAction: function (ch) {
- switch (ch) {
- case 'U':
- return {
- letter: ch,
- labelKey: 'buttons_saveAndPublish',
- handler: 'saveAndPublish',
- hotKey: 'ctrl+p'
- };
- case 'H':
- //send to publish
- return {
- letter: ch,
- labelKey: 'buttons_saveToPublish',
- handler: 'sendToPublish',
- hotKey: 'ctrl+p'
- };
- case 'A':
- return {
- letter: ch,
- labelKey: 'buttons_save',
- handler: 'save',
- hotKey: 'ctrl+s'
- };
- case 'Z':
- return {
- letter: ch,
- labelKey: 'content_unPublish',
- handler: 'unPublish'
- };
- default:
- return null;
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.contentEditingHelper#reBindChangedProperties
- * @methodOf umbraco.services.contentEditingHelper
- * @function
- *
- * @description
- * re-binds all changed property values to the origContent object from the savedContent object and returns an array of changed properties.
- */
- reBindChangedProperties: function (origContent, savedContent) {
- var changed = [];
- //get a list of properties since they are contained in tabs
- var allOrigProps = this.getAllProps(origContent);
- var allNewProps = this.getAllProps(savedContent);
- function getNewProp(alias) {
- return _.find(allNewProps, function (item) {
- return item.alias === alias;
- });
- }
- //a method to ignore built-in prop changes
- var shouldIgnore = function (propName) {
- return _.some([
- 'tabs',
- 'notifications',
- 'ModelState',
- 'tabs',
- 'properties'
- ], function (i) {
- return i === propName;
- });
- };
- //check for changed built-in properties of the content
- for (var o in origContent) {
- //ignore the ones listed in the array
- if (shouldIgnore(o)) {
- continue;
- }
- if (!_.isEqual(origContent[o], savedContent[o])) {
- origContent[o] = savedContent[o];
- }
- }
- //check for changed properties of the content
- for (var p in allOrigProps) {
- var newProp = getNewProp(allOrigProps[p].alias);
- if (newProp && !_.isEqual(allOrigProps[p].value, newProp.value)) {
- //they have changed so set the origContent prop to the new one
- var origVal = allOrigProps[p].value;
- allOrigProps[p].value = newProp.value;
- //instead of having a property editor $watch their expression to check if it has
- // been updated, instead we'll check for the existence of a special method on their model
- // and just call it.
- if (angular.isFunction(allOrigProps[p].onValueChanged)) {
- //send the newVal + oldVal
- allOrigProps[p].onValueChanged(allOrigProps[p].value, origVal);
- }
- changed.push(allOrigProps[p]);
- }
- }
- return changed;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.contentEditingHelper#handleSaveError
- * @methodOf umbraco.services.contentEditingHelper
- * @function
- *
- * @description
- * A function to handle what happens when we have validation issues from the server side
- */
- handleSaveError: function (args) {
- if (!args.err) {
- throw 'args.err cannot be null';
- }
- if (args.redirectOnFailure === undefined || args.redirectOnFailure === null) {
- throw 'args.redirectOnFailure must be set to true or false';
- }
- //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors.
- //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
- //Or, some strange server error
- if (args.err.status === 400) {
- //now we need to look through all the validation errors
- if (args.err.data && args.err.data.ModelState) {
- //wire up the server validation errs
- formHelper.handleServerValidation(args.err.data.ModelState);
- if (!args.redirectOnFailure || !this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) {
- //we are not redirecting because this is not new content, it is existing content. In this case
- // we need to detect what properties have changed and re-bind them with the server data. Then we need
- // to re-bind any server validation errors after the digest takes place.
- if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
- args.rebindCallback();
- }
- serverValidationManager.executeAndClearAllSubscriptions();
- }
- //indicates we've handled the server result
- return true;
- }
- }
- return false;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.contentEditingHelper#handleSuccessfulSave
- * @methodOf umbraco.services.contentEditingHelper
- * @function
- *
- * @description
- * A function to handle when saving a content item is successful. This will rebind the values of the model that have changed
- * ensure the notifications are displayed and that the appropriate events are fired. This will also check if we need to redirect
- * when we're creating new content.
- */
- handleSuccessfulSave: function (args) {
- if (!args) {
- throw 'args cannot be null';
- }
- if (!args.savedContent) {
- throw 'args.savedContent cannot be null';
- }
- if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id)) {
- //we are not redirecting because this is not new content, it is existing content. In this case
- // we need to detect what properties have changed and re-bind them with the server data.
- //call the callback
- if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
- args.rebindCallback();
- }
- }
- },
- /**
- * @ngdoc function
- * @name umbraco.services.contentEditingHelper#redirectToCreatedContent
- * @methodOf umbraco.services.contentEditingHelper
- * @function
- *
- * @description
- * Changes the location to be editing the newly created content after create was successful.
- * We need to decide if we need to redirect to edito mode or if we will remain in create mode.
- * We will only need to maintain create mode if we have not fulfilled the basic requirements for creating an entity which is at least having a name and ID
- */
- redirectToCreatedContent: function (id, modelState) {
- //only continue if we are currently in create mode and if there is no 'Name' modelstate errors
- // since we need at least a name to create content.
- if ($routeParams.create && (isValidIdentifier(id) && (!modelState || !modelState['Name']))) {
- //need to change the location to not be in 'create' mode. Currently the route will be something like:
- // /belle/#/content/edit/1234?doctype=newsArticle&create=true
- // but we need to remove everything after the query so that it is just:
- // /belle/#/content/edit/9876 (where 9876 is the new id)
- //clear the query strings
- $location.search('');
- //change to new path
- $location.path('/' + $routeParams.section + '/' + $routeParams.tree + '/' + $routeParams.method + '/' + id);
- //don't add a browser history for this
- $location.replace();
- return true;
- }
- return false;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.contentEditingHelper#redirectToRenamedContent
- * @methodOf umbraco.services.contentEditingHelper
- * @function
- *
- * @description
- * For some editors like scripts or entites that have names as ids, these names can change and we need to redirect
- * to their new paths, this is helper method to do that.
- */
- redirectToRenamedContent: function (id) {
- //clear the query strings
- $location.search('');
- //change to new path
- $location.path('/' + $routeParams.section + '/' + $routeParams.tree + '/' + $routeParams.method + '/' + id);
- //don't add a browser history for this
- $location.replace();
- return true;
- }
- };
- }
- angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper);
- /**
- * @ngdoc service
- * @name umbraco.services.contentTypeHelper
- * @description A helper service for the content type editor
- **/
- function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $injector, $q) {
- var contentTypeHelperService = {
- createIdArray: function (array) {
- var newArray = [];
- angular.forEach(array, function (arrayItem) {
- if (angular.isObject(arrayItem)) {
- newArray.push(arrayItem.id);
- } else {
- newArray.push(arrayItem);
- }
- });
- return newArray;
- },
- generateModels: function () {
- var deferred = $q.defer();
- var modelsResource = $injector.has('modelsBuilderResource') ? $injector.get('modelsBuilderResource') : null;
- var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled;
- if (modelsBuilderEnabled && modelsResource) {
- modelsResource.buildModels().then(function (result) {
- deferred.resolve(result);
- //just calling this to get the servar back to life
- modelsResource.getModelsOutOfDateStatus();
- }, function (e) {
- deferred.reject(e);
- });
- } else {
- deferred.resolve(false);
- }
- return deferred.promise;
- },
- checkModelsBuilderStatus: function () {
- var deferred = $q.defer();
- var modelsResource = $injector.has('modelsBuilderResource') ? $injector.get('modelsBuilderResource') : null;
- var modelsBuilderEnabled = Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true;
- if (modelsBuilderEnabled && modelsResource) {
- modelsResource.getModelsOutOfDateStatus().then(function (result) {
- //Generate models buttons should be enabled if it is 0
- deferred.resolve(result.status === 0);
- });
- } else {
- deferred.resolve(false);
- }
- return deferred.promise;
- },
- makeObjectArrayFromId: function (idArray, objectArray) {
- var newArray = [];
- for (var idIndex = 0; idArray.length > idIndex; idIndex++) {
- var id = idArray[idIndex];
- for (var objectIndex = 0; objectArray.length > objectIndex; objectIndex++) {
- var object = objectArray[objectIndex];
- if (id === object.id) {
- newArray.push(object);
- }
- }
- }
- return newArray;
- },
- validateAddingComposition: function (contentType, compositeContentType) {
- //Validate that by adding this group that we are not adding duplicate property type aliases
- var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function (g) {
- return _.map(g.properties, function (p) {
- return p.alias;
- });
- }));
- var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function (g) {
- return _.map(g.properties, function (p) {
- return p.alias;
- });
- })), function (f) {
- return f !== null && f !== undefined;
- });
- var intersec = _.intersection(propertiesAdding, propAliasesExisting);
- if (intersec.length > 0) {
- //return the overlapping property aliases
- return intersec;
- }
- //no overlapping property aliases
- return [];
- },
- mergeCompositeContentType: function (contentType, compositeContentType) {
- //Validate that there are no overlapping aliases
- var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType);
- if (overlappingAliases.length > 0) {
- throw new Error('Cannot add this composition, these properties already exist on the content type: ' + overlappingAliases.join());
- }
- angular.forEach(compositeContentType.groups, function (compositionGroup) {
- // order composition groups based on sort order
- compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder');
- // get data type details
- angular.forEach(compositionGroup.properties, function (property) {
- dataTypeResource.getById(property.dataTypeId).then(function (dataType) {
- property.dataTypeIcon = dataType.icon;
- property.dataTypeName = dataType.name;
- });
- });
- // set inherited state on tab
- compositionGroup.inherited = true;
- // set inherited state on properties
- angular.forEach(compositionGroup.properties, function (compositionProperty) {
- compositionProperty.inherited = true;
- });
- // set tab state
- compositionGroup.tabState = 'inActive';
- // if groups are named the same - merge the groups
- angular.forEach(contentType.groups, function (contentTypeGroup) {
- if (contentTypeGroup.name === compositionGroup.name) {
- // set flag to show if properties has been merged into a tab
- compositionGroup.groupIsMerged = true;
- // make group inherited
- contentTypeGroup.inherited = true;
- // add properties to the top of the array
- contentTypeGroup.properties = compositionGroup.properties.concat(contentTypeGroup.properties);
- // update sort order on all properties in merged group
- contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties);
- // make parentTabContentTypeNames to an array so we can push values
- if (contentTypeGroup.parentTabContentTypeNames === null || contentTypeGroup.parentTabContentTypeNames === undefined) {
- contentTypeGroup.parentTabContentTypeNames = [];
- }
- // push name to array of merged composite content types
- contentTypeGroup.parentTabContentTypeNames.push(compositeContentType.name);
- // make parentTabContentTypes to an array so we can push values
- if (contentTypeGroup.parentTabContentTypes === null || contentTypeGroup.parentTabContentTypes === undefined) {
- contentTypeGroup.parentTabContentTypes = [];
- }
- // push id to array of merged composite content types
- contentTypeGroup.parentTabContentTypes.push(compositeContentType.id);
- // get sort order from composition
- contentTypeGroup.sortOrder = compositionGroup.sortOrder;
- // splice group to the top of the array
- var contentTypeGroupCopy = angular.copy(contentTypeGroup);
- var index = contentType.groups.indexOf(contentTypeGroup);
- contentType.groups.splice(index, 1);
- contentType.groups.unshift(contentTypeGroupCopy);
- }
- });
- // if group is not merged - push it to the end of the array - before init tab
- if (compositionGroup.groupIsMerged === false || compositionGroup.groupIsMerged === undefined) {
- // make parentTabContentTypeNames to an array so we can push values
- if (compositionGroup.parentTabContentTypeNames === null || compositionGroup.parentTabContentTypeNames === undefined) {
- compositionGroup.parentTabContentTypeNames = [];
- }
- // push name to array of merged composite content types
- compositionGroup.parentTabContentTypeNames.push(compositeContentType.name);
- // make parentTabContentTypes to an array so we can push values
- if (compositionGroup.parentTabContentTypes === null || compositionGroup.parentTabContentTypes === undefined) {
- compositionGroup.parentTabContentTypes = [];
- }
- // push id to array of merged composite content types
- compositionGroup.parentTabContentTypes.push(compositeContentType.id);
- // push group before placeholder tab
- contentType.groups.unshift(compositionGroup);
- }
- });
- // sort all groups by sortOrder property
- contentType.groups = $filter('orderBy')(contentType.groups, 'sortOrder');
- return contentType;
- },
- splitCompositeContentType: function (contentType, compositeContentType) {
- var groups = [];
- angular.forEach(contentType.groups, function (contentTypeGroup) {
- if (contentTypeGroup.tabState !== 'init') {
- var idIndex = contentTypeGroup.parentTabContentTypes.indexOf(compositeContentType.id);
- var nameIndex = contentTypeGroup.parentTabContentTypeNames.indexOf(compositeContentType.name);
- var groupIndex = contentType.groups.indexOf(contentTypeGroup);
- if (idIndex !== -1) {
- var properties = [];
- // remove all properties from composite content type
- angular.forEach(contentTypeGroup.properties, function (property) {
- if (property.contentTypeId !== compositeContentType.id) {
- properties.push(property);
- }
- });
- // set new properties array to properties
- contentTypeGroup.properties = properties;
- // remove composite content type name and id from inherited arrays
- contentTypeGroup.parentTabContentTypes.splice(idIndex, 1);
- contentTypeGroup.parentTabContentTypeNames.splice(nameIndex, 1);
- // remove inherited state if there are no inherited properties
- if (contentTypeGroup.parentTabContentTypes.length === 0) {
- contentTypeGroup.inherited = false;
- }
- // remove group if there are no properties left
- if (contentTypeGroup.properties.length > 1) {
- //contentType.groups.splice(groupIndex, 1);
- groups.push(contentTypeGroup);
- }
- } else {
- groups.push(contentTypeGroup);
- }
- } else {
- groups.push(contentTypeGroup);
- }
- // update sort order on properties
- contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties);
- });
- contentType.groups = groups;
- },
- updatePropertiesSortOrder: function (properties) {
- var sortOrder = 0;
- angular.forEach(properties, function (property) {
- if (!property.inherited && property.propertyState !== 'init') {
- property.sortOrder = sortOrder;
- }
- sortOrder++;
- });
- return properties;
- },
- getTemplatePlaceholder: function () {
- var templatePlaceholder = {
- 'name': '',
- 'icon': 'icon-layout',
- 'alias': 'templatePlaceholder',
- 'placeholder': true
- };
- return templatePlaceholder;
- },
- insertDefaultTemplatePlaceholder: function (defaultTemplate) {
- // get template placeholder
- var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder();
- // add as default template
- defaultTemplate = templatePlaceholder;
- return defaultTemplate;
- },
- insertTemplatePlaceholder: function (array) {
- // get template placeholder
- var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder();
- // add as selected item
- array.push(templatePlaceholder);
- return array;
- },
- insertChildNodePlaceholder: function (array, name, icon, id) {
- var placeholder = {
- 'name': name,
- 'icon': icon,
- 'id': id
- };
- array.push(placeholder);
- }
- };
- return contentTypeHelperService;
- }
- angular.module('umbraco.services').factory('contentTypeHelper', contentTypeHelper);
- /**
- * @ngdoc service
- * @name umbraco.services.cropperHelper
- * @description A helper object used for dealing with image cropper data
- **/
- function cropperHelper(umbRequestHelper, $http) {
- var service = {
- /**
- * @ngdoc method
- * @name umbraco.services.cropperHelper#configuration
- * @methodOf umbraco.services.cropperHelper
- *
- * @description
- * Returns a collection of plugins available to the tinyMCE editor
- *
- */
- configuration: function (mediaTypeAlias) {
- return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('imageCropperApiBaseUrl', 'GetConfiguration', [{ mediaTypeAlias: mediaTypeAlias }])), 'Failed to retrieve tinymce configuration');
- },
- //utill for getting either min/max aspect ratio to scale image after
- calculateAspectRatioFit: function (srcWidth, srcHeight, maxWidth, maxHeight, maximize) {
- var ratio = [
- maxWidth / srcWidth,
- maxHeight / srcHeight
- ];
- if (maximize) {
- ratio = Math.max(ratio[0], ratio[1]);
- } else {
- ratio = Math.min(ratio[0], ratio[1]);
- }
- return {
- width: srcWidth * ratio,
- height: srcHeight * ratio,
- ratio: ratio
- };
- },
- //utill for scaling width / height given a ratio
- calculateSizeToRatio: function (srcWidth, srcHeight, ratio) {
- return {
- width: srcWidth * ratio,
- height: srcHeight * ratio,
- ratio: ratio
- };
- },
- scaleToMaxSize: function (srcWidth, srcHeight, maxSize) {
- var retVal = {
- height: srcHeight,
- width: srcWidth
- };
- if (srcWidth > maxSize || srcHeight > maxSize) {
- var ratio = [
- maxSize / srcWidth,
- maxSize / srcHeight
- ];
- ratio = Math.min(ratio[0], ratio[1]);
- retVal.height = srcHeight * ratio;
- retVal.width = srcWidth * ratio;
- }
- return retVal;
- },
- //returns a ng-style object with top,left,width,height pixel measurements
- //expects {left,right,top,bottom} - {width,height}, {width,height}, int
- //offset is just to push the image position a number of pixels from top,left
- convertToStyle: function (coordinates, originalSize, viewPort, offset) {
- var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset);
- var _offset = offset || 0;
- var x = 1 - (coordinates.x1 + Math.abs(coordinates.x2));
- var left_of_x = originalSize.width * x;
- var ratio = viewPort.width / left_of_x;
- var style = {
- position: 'absolute',
- top: -(coordinates_px.y1 * ratio) + _offset,
- left: -(coordinates_px.x1 * ratio) + _offset,
- width: Math.floor(originalSize.width * ratio),
- height: Math.floor(originalSize.height * ratio),
- originalWidth: originalSize.width,
- originalHeight: originalSize.height,
- ratio: ratio
- };
- return style;
- },
- coordinatesToPixels: function (coordinates, originalSize, offset) {
- var coordinates_px = {
- x1: Math.floor(coordinates.x1 * originalSize.width),
- y1: Math.floor(coordinates.y1 * originalSize.height),
- x2: Math.floor(coordinates.x2 * originalSize.width),
- y2: Math.floor(coordinates.y2 * originalSize.height)
- };
- return coordinates_px;
- },
- pixelsToCoordinates: function (image, width, height, offset) {
- var x1_px = Math.abs(image.left - offset);
- var y1_px = Math.abs(image.top - offset);
- var x2_px = image.width - (x1_px + width);
- var y2_px = image.height - (y1_px + height);
- //crop coordinates in %
- var crop = {};
- crop.x1 = x1_px / image.width;
- crop.y1 = y1_px / image.height;
- crop.x2 = x2_px / image.width;
- crop.y2 = y2_px / image.height;
- for (var coord in crop) {
- if (crop[coord] < 0) {
- crop[coord] = 0;
- }
- }
- return crop;
- },
- centerInsideViewPort: function (img, viewport) {
- var left = viewport.width / 2 - img.width / 2, top = viewport.height / 2 - img.height / 2;
- return {
- left: left,
- top: top
- };
- },
- alignToCoordinates: function (image, center, viewport) {
- var min_left = image.width - viewport.width;
- var min_top = image.height - viewport.height;
- var c_top = -(center.top * image.height) + viewport.height / 2;
- var c_left = -(center.left * image.width) + viewport.width / 2;
- if (c_top < -min_top) {
- c_top = -min_top;
- }
- if (c_top > 0) {
- c_top = 0;
- }
- if (c_left < -min_left) {
- c_left = -min_left;
- }
- if (c_left > 0) {
- c_left = 0;
- }
- return {
- left: c_left,
- top: c_top
- };
- },
- syncElements: function (source, target) {
- target.height(source.height());
- target.width(source.width());
- target.css({
- 'top': source[0].offsetTop,
- 'left': source[0].offsetLeft
- });
- }
- };
- return service;
- }
- angular.module('umbraco.services').factory('cropperHelper', cropperHelper);
- /**
- * @ngdoc service
- * @name umbraco.services.dataTypeHelper
- * @description A helper service for data types
- **/
- function dataTypeHelper() {
- var dataTypeHelperService = {
- createPreValueProps: function (preVals) {
- var preValues = [];
- for (var i = 0; i < preVals.length; i++) {
- preValues.push({
- hideLabel: preVals[i].hideLabel,
- alias: preVals[i].key,
- description: preVals[i].description,
- label: preVals[i].label,
- view: preVals[i].view,
- value: preVals[i].value
- });
- }
- return preValues;
- },
- rebindChangedProperties: function (origContent, savedContent) {
- //a method to ignore built-in prop changes
- var shouldIgnore = function (propName) {
- return _.some([
- 'notifications',
- 'ModelState'
- ], function (i) {
- return i === propName;
- });
- };
- //check for changed built-in properties of the content
- for (var o in origContent) {
- //ignore the ones listed in the array
- if (shouldIgnore(o)) {
- continue;
- }
- if (!_.isEqual(origContent[o], savedContent[o])) {
- origContent[o] = savedContent[o];
- }
- }
- }
- };
- return dataTypeHelperService;
- }
- angular.module('umbraco.services').factory('dataTypeHelper', dataTypeHelper);
- /**
- * @ngdoc service
- * @name umbraco.services.dialogService
- *
- * @requires $rootScope
- * @requires $compile
- * @requires $http
- * @requires $log
- * @requires $q
- * @requires $templateCache
- *
- * @description
- * Application-wide service for handling modals, overlays and dialogs
- * By default it injects the passed template url into a div to body of the document
- * And renders it, but does also support rendering items in an iframe, incase
- * serverside processing is needed, or its a non-angular page
- *
- * ##usage
- * To use, simply inject the dialogService into any controller that needs it, and make
- * sure the umbraco.services module is accesible - which it should be by default.
- *
- * <pre>
- * var dialog = dialogService.open({template: 'path/to/page.html', show: true, callback: done});
- * functon done(data){
- * //The dialog has been submitted
- * //data contains whatever the dialog has selected / attached
- * }
- * </pre>
- */
- angular.module('umbraco.services').factory('dialogService', function ($rootScope, $compile, $http, $timeout, $q, $templateCache, appState, eventsService) {
- var dialogs = [];
- /** Internal method that removes all dialogs */
- function removeAllDialogs(args) {
- for (var i = 0; i < dialogs.length; i++) {
- var dialog = dialogs[i];
- //very special flag which means that global events cannot close this dialog - currently only used on the login
- // dialog since it's special and cannot be closed without logging in.
- if (!dialog.manualClose) {
- dialog.close(args);
- }
- }
- }
- /** Internal method that closes the dialog properly and cleans up resources */
- function closeDialog(dialog) {
- if (dialog.element) {
- dialog.element.modal('hide');
- //this is not entirely enough since the damn webforms scriploader still complains
- if (dialog.iframe) {
- dialog.element.find('iframe').attr('src', 'about:blank');
- }
- dialog.scope.$destroy();
- //we need to do more than just remove the element, this will not destroy the
- // scope in angular 1.1x, in angular 1.2x this is taken care of but if we dont
- // take care of this ourselves we have memory leaks.
- dialog.element.remove();
- //remove 'this' dialog from the dialogs array
- dialogs = _.reject(dialogs, function (i) {
- return i === dialog;
- });
- }
- }
- /** Internal method that handles opening all dialogs */
- function openDialog(options) {
- var defaults = {
- container: $('body'),
- animation: 'fade',
- modalClass: 'umb-modal',
- width: '100%',
- inline: false,
- iframe: false,
- show: true,
- template: 'views/common/notfound.html',
- callback: undefined,
- closeCallback: undefined,
- element: undefined,
- // It will set this value as a property on the dialog controller's scope as dialogData,
- // used to pass in custom data to the dialog controller's $scope. Though this is near identical to
- // the dialogOptions property that is also set the the dialog controller's $scope object.
- // So there's basically 2 ways of doing the same thing which we're now stuck with and in fact
- // dialogData has another specially attached property called .selection which gets used.
- dialogData: undefined
- };
- var dialog = angular.extend(defaults, options);
- //NOTE: People should NOT pass in a scope object that is legacy functoinality and causes problems. We will ALWAYS
- // destroy the scope when the dialog is closed regardless if it is in use elsewhere which is why it shouldn't be done.
- var scope = options.scope || $rootScope.$new();
- //Modal dom obj and set id to old-dialog-service - used until we get all dialogs moved the the new overlay directive
- dialog.element = $('<div ng-swipe-right="swipeHide($event)" data-backdrop="false"></div>');
- var id = 'old-dialog-service';
- if (options.inline) {
- dialog.animation = '';
- } else {
- dialog.element.addClass('modal');
- dialog.element.addClass('hide');
- }
- //set the id and add classes
- dialog.element.attr('id', id).addClass(dialog.animation).addClass(dialog.modalClass);
- //push the modal into the global modal collection
- //we halt the .push because a link click will trigger a closeAll right away
- $timeout(function () {
- dialogs.push(dialog);
- }, 500);
- dialog.close = function (data) {
- if (dialog.closeCallback) {
- dialog.closeCallback(data);
- }
- closeDialog(dialog);
- };
- //if iframe is enabled, inject that instead of a template
- if (dialog.iframe) {
- var html = $('<iframe src=\'' + dialog.template + '\' class=\'auto-expand\' style=\'border: none; width: 100%; height: 100%;\'></iframe>');
- dialog.element.html(html);
- //append to body or whatever element is passed in as options.containerElement
- dialog.container.append(dialog.element);
- // Compile modal content
- $timeout(function () {
- $compile(dialog.element)(dialog.scope);
- });
- dialog.element.css('width', dialog.width);
- //Autoshow
- if (dialog.show) {
- dialog.element.modal('show');
- }
- dialog.scope = scope;
- return dialog;
- } else {
- //We need to load the template with an httpget and once it's loaded we'll compile and assign the result to the container
- // object. However since the result could be a promise or just data we need to use a $q.when. We still need to return the
- // $modal object so we'll actually return the modal object synchronously without waiting for the promise. Otherwise this openDialog
- // method will always need to return a promise which gets nasty because of promises in promises plus the result just needs a reference
- // to the $modal object which will not change (only it's contents will change).
- $q.when($templateCache.get(dialog.template) || $http.get(dialog.template, { cache: true }).then(function (res) {
- return res.data;
- })).then(function onSuccess(template) {
- // Build modal object
- dialog.element.html(template);
- //append to body or other container element
- dialog.container.append(dialog.element);
- // Compile modal content
- $timeout(function () {
- $compile(dialog.element)(scope);
- });
- scope.dialogOptions = dialog;
- //Scope to handle data from the modal form
- scope.dialogData = dialog.dialogData ? dialog.dialogData : {};
- scope.dialogData.selection = [];
- // Provide scope display functions
- //this passes the modal to the current scope
- scope.$modal = function (name) {
- dialog.element.modal(name);
- };
- scope.swipeHide = function (e) {
- if (appState.getGlobalState('touchDevice')) {
- var selection = window.getSelection();
- if (selection.type !== 'Range') {
- scope.hide();
- }
- }
- };
- //NOTE: Same as 'close' without the callbacks
- scope.hide = function () {
- closeDialog(dialog);
- };
- //basic events for submitting and closing
- scope.submit = function (data) {
- if (dialog.callback) {
- dialog.callback(data);
- }
- closeDialog(dialog);
- };
- scope.close = function (data) {
- dialog.close(data);
- };
- //NOTE: This can ONLY ever be used to show the dialog if dialog.show is false (autoshow).
- // You CANNOT call show() after you call hide(). hide = close, they are the same thing and once
- // a dialog is closed it's resources are disposed of.
- scope.show = function () {
- if (dialog.manualClose === true) {
- //show and configure that the keyboard events are not enabled on this modal
- dialog.element.modal({ keyboard: false });
- } else {
- //just show normally
- dialog.element.modal('show');
- }
- };
- scope.select = function (item) {
- var i = scope.dialogData.selection.indexOf(item);
- if (i < 0) {
- scope.dialogData.selection.push(item);
- } else {
- scope.dialogData.selection.splice(i, 1);
- }
- };
- //NOTE: Same as 'close' without the callbacks
- scope.dismiss = scope.hide;
- // Emit modal events
- angular.forEach([
- 'show',
- 'shown',
- 'hide',
- 'hidden'
- ], function (name) {
- dialog.element.on(name, function (ev) {
- scope.$emit('modal-' + name, ev);
- });
- });
- // Support autofocus attribute
- dialog.element.on('shown', function (event) {
- $('input[autofocus]', dialog.element).first().trigger('focus');
- });
- dialog.scope = scope;
- //Autoshow
- if (dialog.show) {
- scope.show();
- }
- });
- //Return the modal object outside of the promise!
- return dialog;
- }
- }
- /** Handles the closeDialogs event */
- eventsService.on('app.closeDialogs', function (evt, args) {
- removeAllDialogs(args);
- });
- return {
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#open
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a modal rendering a given template url.
- *
- * @param {Object} options rendering options
- * @param {DomElement} options.container the DOM element to inject the modal into, by default set to body
- * @param {Function} options.callback function called when the modal is submitted
- * @param {String} options.template the url of the template
- * @param {String} options.animation animation csss class, by default set to "fade"
- * @param {String} options.modalClass modal css class, by default "umb-modal"
- * @param {Bool} options.show show the modal instantly
- * @param {Bool} options.iframe load template in an iframe, only needed for serverside templates
- * @param {Int} options.width set a width on the modal, only needed for iframes
- * @param {Bool} options.inline strips the modal from any animation and wrappers, used when you want to inject a dialog into an existing container
- * @returns {Object} modal object
- */
- open: function (options) {
- return openDialog(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#close
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Closes a specific dialog
- * @param {Object} dialog the dialog object to close
- * @param {Object} args if specified this object will be sent to any callbacks registered on the dialogs.
- */
- close: function (dialog, args) {
- if (dialog) {
- dialog.close(args);
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#closeAll
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Closes all dialogs
- * @param {Object} args if specified this object will be sent to any callbacks registered on the dialogs.
- */
- closeAll: function (args) {
- removeAllDialogs(args);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#mediaPicker
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a media picker in a modal, the callback returns an array of selected media items
- * @param {Object} options mediapicker dialog options object
- * @param {Boolean} options.onlyImages Only display files that have an image file-extension
- * @param {Function} options.callback callback function
- * @returns {Object} modal object
- */
- mediaPicker: function (options) {
- options.template = 'views/common/dialogs/mediaPicker.html';
- options.show = true;
- return openDialog(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#contentPicker
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a content picker tree in a modal, the callback returns an array of selected documents
- * @param {Object} options content picker dialog options object
- * @param {Boolean} options.multiPicker should the picker return one or multiple items
- * @param {Function} options.callback callback function
- * @returns {Object} modal object
- */
- contentPicker: function (options) {
- options.treeAlias = 'content';
- options.section = 'content';
- return this.treePicker(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#linkPicker
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a link picker tree in a modal, the callback returns a single link
- * @param {Object} options content picker dialog options object
- * @param {Function} options.callback callback function
- * @returns {Object} modal object
- */
- linkPicker: function (options) {
- options.template = 'views/common/dialogs/linkPicker.html';
- options.show = true;
- return openDialog(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#macroPicker
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a mcaro picker in a modal, the callback returns a object representing the macro and it's parameters
- * @param {Object} options macropicker dialog options object
- * @param {Function} options.callback callback function
- * @returns {Object} modal object
- */
- macroPicker: function (options) {
- options.template = 'views/common/dialogs/insertmacro.html';
- options.show = true;
- options.modalClass = 'span7 umb-modal';
- return openDialog(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#memberPicker
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a member picker in a modal, the callback returns a object representing the selected member
- * @param {Object} options member picker dialog options object
- * @param {Boolean} options.multiPicker should the tree pick one or multiple members before returning
- * @param {Function} options.callback callback function
- * @returns {Object} modal object
- */
- memberPicker: function (options) {
- options.treeAlias = 'member';
- options.section = 'member';
- return this.treePicker(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#memberGroupPicker
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a member group picker in a modal, the callback returns a object representing the selected member
- * @param {Object} options member group picker dialog options object
- * @param {Boolean} options.multiPicker should the tree pick one or multiple members before returning
- * @param {Function} options.callback callback function
- * @returns {Object} modal object
- */
- memberGroupPicker: function (options) {
- options.template = 'views/common/dialogs/memberGroupPicker.html';
- options.show = true;
- return openDialog(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#iconPicker
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a icon picker in a modal, the callback returns a object representing the selected icon
- * @param {Object} options iconpicker dialog options object
- * @param {Function} options.callback callback function
- * @returns {Object} modal object
- */
- iconPicker: function (options) {
- options.template = 'views/common/dialogs/iconPicker.html';
- options.show = true;
- return openDialog(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#treePicker
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a tree picker in a modal, the callback returns a object representing the selected tree item
- * @param {Object} options iconpicker dialog options object
- * @param {String} options.section tree section to display
- * @param {String} options.treeAlias specific tree to display
- * @param {Boolean} options.multiPicker should the tree pick one or multiple items before returning
- * @param {Function} options.callback callback function
- * @returns {Object} modal object
- */
- treePicker: function (options) {
- options.template = 'views/common/dialogs/treePicker.html';
- options.show = true;
- return openDialog(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#propertyDialog
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a dialog with a chosen property editor in, a value can be passed to the modal, and this value is returned in the callback
- * @param {Object} options mediapicker dialog options object
- * @param {Function} options.callback callback function
- * @param {String} editor editor to use to edit a given value and return on callback
- * @param {Object} value value sent to the property editor
- * @returns {Object} modal object
- */
- //TODO: Wtf does this do? I don't think anything!
- propertyDialog: function (options) {
- options.template = 'views/common/dialogs/property.html';
- options.show = true;
- return openDialog(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#embedDialog
- * @methodOf umbraco.services.dialogService
- * @description
- * Opens a dialog to an embed dialog
- */
- embedDialog: function (options) {
- options.template = 'views/common/dialogs/rteembed.html';
- options.show = true;
- return openDialog(options);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.dialogService#ysodDialog
- * @methodOf umbraco.services.dialogService
- *
- * @description
- * Opens a dialog to show a custom YSOD
- */
- ysodDialog: function (ysodError) {
- var newScope = $rootScope.$new();
- newScope.error = ysodError;
- return openDialog({
- modalClass: 'umb-modal wide ysod',
- scope: newScope,
- //callback: options.callback,
- template: 'views/common/dialogs/ysod.html',
- show: true
- });
- },
- confirmDialog: function (ysodError) {
- options.template = 'views/common/dialogs/confirm.html';
- options.show = true;
- return openDialog(options);
- }
- };
- });
- (function () {
- 'use strict';
- function entityHelper() {
- function getEntityTypeFromSection(section) {
- if (section === 'member') {
- return 'Member';
- } else if (section === 'media') {
- return 'Media';
- } else {
- return 'Document';
- }
- }
- ////////////
- var service = { getEntityTypeFromSection: getEntityTypeFromSection };
- return service;
- }
- angular.module('umbraco.services').factory('entityHelper', entityHelper);
- }());
- /** Used to broadcast and listen for global events and allow the ability to add async listeners to the callbacks */
- /*
- Core app events:
- app.ready
- app.authenticated
- app.notAuthenticated
- app.closeDialogs
- app.ysod
- app.reInitialize
- app.userRefresh
- */
- function eventsService($q, $rootScope) {
- return {
- /** raise an event with a given name, returns an array of promises for each listener */
- emit: function (name, args) {
- //there are no listeners
- if (!$rootScope.$$listeners[name]) {
- return; //return [];
- }
- //send the event
- $rootScope.$emit(name, args); //PP: I've commented out the below, since we currently dont
- // expose the eventsService as a documented api
- // and think we need to figure out our usecases for this
- // since the below modifies the return value of the then on() method
- /*
- //setup a deferred promise for each listener
- var deferred = [];
- for (var i = 0; i < $rootScope.$$listeners[name].length; i++) {
- deferred.push($q.defer());
- }*/
- //create a new event args object to pass to the
- // $emit containing methods that will allow listeners
- // to return data in an async if required
- /*
- var eventArgs = {
- args: args,
- reject: function (a) {
- deferred.pop().reject(a);
- },
- resolve: function (a) {
- deferred.pop().resolve(a);
- }
- };*/
- /*
- //return an array of promises
- var promises = _.map(deferred, function(p) {
- return p.promise;
- });
- return promises;*/
- },
- /** subscribe to a method, or use scope.$on = same thing */
- on: function (name, callback) {
- return $rootScope.$on(name, callback);
- },
- /** pass in the result of 'on' to this method, or just call the method returned from 'on' to unsubscribe */
- unsubscribe: function (handle) {
- if (angular.isFunction(handle)) {
- handle();
- }
- }
- };
- }
- angular.module('umbraco.services').factory('eventsService', eventsService);
- /**
- * @ngdoc service
- * @name umbraco.services.fileManager
- * @function
- *
- * @description
- * Used by editors to manage any files that require uploading with the posted data, normally called by property editors
- * that need to attach files.
- * When a route changes successfully, we ensure that the collection is cleared.
- */
- function fileManager() {
- var fileCollection = [];
- return {
- /**
- * @ngdoc function
- * @name umbraco.services.fileManager#addFiles
- * @methodOf umbraco.services.fileManager
- * @function
- *
- * @description
- * Attaches files to the current manager for the current editor for a particular property, if an empty array is set
- * for the files collection that effectively clears the files for the specified editor.
- */
- setFiles: function (propertyAlias, files) {
- //this will clear the files for the current property and then add the new ones for the current property
- fileCollection = _.reject(fileCollection, function (item) {
- return item.alias === propertyAlias;
- });
- for (var i = 0; i < files.length; i++) {
- //save the file object to the files collection
- fileCollection.push({
- alias: propertyAlias,
- file: files[i]
- });
- }
- },
- /**
- * @ngdoc function
- * @name umbraco.services.fileManager#getFiles
- * @methodOf umbraco.services.fileManager
- * @function
- *
- * @description
- * Returns all of the files attached to the file manager
- */
- getFiles: function () {
- return fileCollection;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.fileManager#clearFiles
- * @methodOf umbraco.services.fileManager
- * @function
- *
- * @description
- * Removes all files from the manager
- */
- clearFiles: function () {
- fileCollection = [];
- }
- };
- }
- angular.module('umbraco.services').factory('fileManager', fileManager);
- /**
- * @ngdoc service
- * @name umbraco.services.formHelper
- * @function
- *
- * @description
- * A utility class used to streamline how forms are developed, to ensure that validation is check and displayed consistently and to ensure that the correct events
- * fire when they need to.
- */
- function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService, localizationService) {
- return {
- /**
- * @ngdoc function
- * @name umbraco.services.formHelper#submitForm
- * @methodOf umbraco.services.formHelper
- * @function
- *
- * @description
- * Called by controllers when submitting a form - this ensures that all client validation is checked,
- * server validation is cleared, that the correct events execute and status messages are displayed.
- * This returns true if the form is valid, otherwise false if form submission cannot continue.
- *
- * @param {object} args An object containing arguments for form submission
- */
- submitForm: function (args) {
- var currentForm;
- if (!args) {
- throw 'args cannot be null';
- }
- if (!args.scope) {
- throw 'args.scope cannot be null';
- }
- if (!args.formCtrl) {
- //try to get the closest form controller
- currentForm = angularHelper.getRequiredCurrentForm(args.scope);
- } else {
- currentForm = args.formCtrl;
- }
- //if no statusPropertyName is set we'll default to formStatus.
- if (!args.statusPropertyName) {
- args.statusPropertyName = 'formStatus';
- }
- //if no statusTimeout is set, we'll default to 2500 ms
- if (!args.statusTimeout) {
- args.statusTimeout = 2500;
- }
- //the first thing any form must do is broadcast the formSubmitting event
- args.scope.$broadcast('formSubmitting', {
- scope: args.scope,
- action: args.action
- });
- //then check if the form is valid
- if (!args.skipValidation) {
- if (currentForm.$invalid) {
- return false;
- }
- }
- //reset the server validations
- serverValidationManager.reset();
- //check if a form status should be set on the scope
- if (args.statusMessage) {
- args.scope[args.statusPropertyName] = args.statusMessage;
- //clear the message after the timeout
- $timeout(function () {
- args.scope[args.statusPropertyName] = undefined;
- }, args.statusTimeout);
- }
- return true;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.formHelper#submitForm
- * @methodOf umbraco.services.formHelper
- * @function
- *
- * @description
- * Called by controllers when a form has been successfully submitted. the correct events execute
- * and that the notifications are displayed if there are any.
- *
- * @param {object} args An object containing arguments for form submission
- */
- resetForm: function (args) {
- if (!args) {
- throw 'args cannot be null';
- }
- if (!args.scope) {
- throw 'args.scope cannot be null';
- }
- //if no statusPropertyName is set we'll default to formStatus.
- if (!args.statusPropertyName) {
- args.statusPropertyName = 'formStatus';
- }
- //clear the status
- args.scope[args.statusPropertyName] = null;
- this.showNotifications(args);
- args.scope.$broadcast('formSubmitted', { scope: args.scope });
- },
- showNotifications: function (args) {
- if (!args || !args.notifications) {
- return false;
- }
- if (angular.isArray(args.notifications)) {
- for (var i = 0; i < args.notifications.length; i++) {
- notificationsService.showNotification(args.notifications[i]);
- }
- return true;
- }
- return false;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.formHelper#handleError
- * @methodOf umbraco.services.formHelper
- * @function
- *
- * @description
- * Needs to be called when a form submission fails, this will wire up all server validation errors in ModelState and
- * add the correct messages to the notifications. If a server error has occurred this will show a ysod.
- *
- * @param {object} err The error object returned from the http promise
- */
- handleError: function (err) {
- //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors.
- //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
- //Or, some strange server error
- if (err.status === 400) {
- //now we need to look through all the validation errors
- if (err.data && err.data.ModelState) {
- //wire up the server validation errs
- this.handleServerValidation(err.data.ModelState);
- //execute all server validation events and subscribers
- serverValidationManager.executeAndClearAllSubscriptions();
- } else {
- dialogService.ysodDialog(err);
- }
- }
- },
- /**
- * @ngdoc function
- * @name umbraco.services.formHelper#handleServerValidation
- * @methodOf umbraco.services.formHelper
- * @function
- *
- * @description
- * This wires up all of the server validation model state so that valServer and valServerField directives work
- *
- * @param {object} err The error object returned from the http promise
- */
- handleServerValidation: function (modelState) {
- for (var e in modelState) {
- //This is where things get interesting....
- // We need to support validation for all editor types such as both the content and content type editors.
- // The Content editor ModelState is quite specific with the way that Properties are validated especially considering
- // that each property is a User Developer property editor.
- // The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations
- // system.
- // So, to do this (since we need to support backwards compat), we need to hack a little bit. For Content Properties,
- // which are user defined, we know that they will exist with a prefixed ModelState of "_Properties.", so if we detect
- // this, then we know it's a Property.
- //the alias in model state can be in dot notation which indicates
- // * the first part is the content property alias
- // * the second part is the field to which the valiation msg is associated with
- //There will always be at least 2 parts for properties since all model errors for properties are prefixed with "Properties"
- //If it is not prefixed with "Properties" that means the error is for a field of the object directly.
- var parts = e.split('.');
- //Check if this is for content properties - specific to content/media/member editors because those are special
- // user defined properties with custom controls.
- if (parts.length > 1 && parts[0] === '_Properties') {
- var propertyAlias = parts[1];
- //if it contains 2 '.' then we will wire it up to a property's field
- if (parts.length > 2) {
- //add an error with a reference to the field for which the validation belongs too
- serverValidationManager.addPropertyError(propertyAlias, parts[2], modelState[e][0]);
- } else {
- //add a generic error for the property, no reference to a specific field
- serverValidationManager.addPropertyError(propertyAlias, '', modelState[e][0]);
- }
- } else {
- //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example:
- // Groups[0].Properties[2].Alias
- serverValidationManager.addFieldError(e, modelState[e][0]);
- }
- //add to notifications
- notificationsService.error('Validation', modelState[e][0]);
- }
- }
- };
- }
- angular.module('umbraco.services').factory('formHelper', formHelper);
- angular.module('umbraco.services').factory('gridService', function ($http, $q) {
- var configPath = Umbraco.Sys.ServerVariables.umbracoUrls.gridConfig;
- var service = {
- getGridEditors: function () {
- return $http.get(configPath);
- }
- };
- return service;
- });
- angular.module('umbraco.services').factory('helpService', function ($http, $q) {
- var helpTopics = {};
- var defaultUrl = 'http://our.umbraco.org/rss/help';
- var tvUrl = 'http://umbraco.tv/feeds/help';
- function getCachedHelp(url) {
- if (helpTopics[url]) {
- return helpTopics[cacheKey];
- } else {
- return null;
- }
- }
- function setCachedHelp(url, data) {
- helpTopics[url] = data;
- }
- function fetchUrl(url) {
- var deferred = $q.defer();
- var found = getCachedHelp(url);
- if (found) {
- deferred.resolve(found);
- } else {
- var proxyUrl = 'dashboard/feedproxy.aspx?url=' + url;
- $http.get(proxyUrl).then(function (data) {
- var feed = $(data.data);
- var topics = [];
- $('item', feed).each(function (i, item) {
- var topic = {};
- topic.thumbnail = $(item).find('thumbnail').attr('url');
- topic.title = $('title', item).text();
- topic.link = $('guid', item).text();
- topic.description = $('description', item).text();
- topics.push(topic);
- });
- setCachedHelp(topics);
- deferred.resolve(topics);
- });
- }
- return deferred.promise;
- }
- var service = {
- findHelp: function (args) {
- var url = service.getUrl(defaultUrl, args);
- return fetchUrl(url);
- },
- findVideos: function (args) {
- var url = service.getUrl(tvUrl, args);
- return fetchUrl(url);
- },
- getUrl: function (url, args) {
- return url + '?' + $.param(args);
- }
- };
- return service;
- });
- /**
- * @ngdoc service
- * @name umbraco.services.historyService
- *
- * @requires $rootScope
- * @requires $timeout
- * @requires angularHelper
- *
- * @description
- * Service to handle the main application navigation history. Responsible for keeping track
- * of where a user navigates to, stores an icon, url and name in a collection, to make it easy
- * for the user to go back to a previous editor / action
- *
- * **Note:** only works with new angular-based editors, not legacy ones
- *
- * ##usage
- * To use, simply inject the historyService into any controller that needs it, and make
- * sure the umbraco.services module is accesible - which it should be by default.
- *
- * <pre>
- * angular.module("umbraco").controller("my.controller". function(historyService){
- * historyService.add({
- * icon: "icon-class",
- * name: "Editing 'articles',
- * link: "/content/edit/1234"}
- * );
- * });
- * </pre>
- */
- angular.module('umbraco.services').factory('historyService', function ($rootScope, $timeout, angularHelper, eventsService) {
- var nArray = [];
- function add(item) {
- if (!item) {
- return null;
- }
- var listWithoutThisItem = _.reject(nArray, function (i) {
- return i.link === item.link;
- });
- //put it at the top and reassign
- listWithoutThisItem.splice(0, 0, item);
- nArray = listWithoutThisItem;
- return nArray[0];
- }
- return {
- /**
- * @ngdoc method
- * @name umbraco.services.historyService#add
- * @methodOf umbraco.services.historyService
- *
- * @description
- * Adds a given history item to the users history collection.
- *
- * @param {Object} item the history item
- * @param {String} item.icon icon css class for the list, ex: "icon-image", "icon-doc"
- * @param {String} item.link route to the editor, ex: "/content/edit/1234"
- * @param {String} item.name friendly name for the history listing
- * @returns {Object} history item object
- */
- add: function (item) {
- var icon = item.icon || 'icon-file';
- angularHelper.safeApply($rootScope, function () {
- var result = add({
- name: item.name,
- icon: icon,
- link: item.link,
- time: new Date()
- });
- eventsService.emit('historyService.add', {
- added: result,
- all: nArray
- });
- return result;
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.historyService#remove
- * @methodOf umbraco.services.historyService
- *
- * @description
- * Removes a history item from the users history collection, given an index to remove from.
- *
- * @param {Int} index index to remove item from
- */
- remove: function (index) {
- angularHelper.safeApply($rootScope, function () {
- var result = nArray.splice(index, 1);
- eventsService.emit('historyService.remove', {
- removed: result,
- all: nArray
- });
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.historyService#removeAll
- * @methodOf umbraco.services.historyService
- *
- * @description
- * Removes all history items from the users history collection
- */
- removeAll: function () {
- angularHelper.safeApply($rootScope, function () {
- nArray = [];
- eventsService.emit('historyService.removeAll');
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.historyService#getCurrent
- * @methodOf umbraco.services.historyService
- *
- * @description
- * Method to return the current history collection.
- *
- */
- getCurrent: function () {
- return nArray;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.historyService#getLastAccessedItemForSection
- * @methodOf umbraco.services.historyService
- *
- * @description
- * Method to return the item that was last accessed in the given section
- *
- * @param {string} sectionAlias Alias of the section to return the last accessed item for.
- */
- getLastAccessedItemForSection: function (sectionAlias) {
- for (var i = 0, len = nArray.length; i < len; i++) {
- var item = nArray[i];
- if (item.link.indexOf(sectionAlias + '/') === 0) {
- return item;
- }
- }
- return null;
- }
- };
- });
- /**
- * @ngdoc service
- * @name umbraco.services.iconHelper
- * @description A helper service for dealing with icons, mostly dealing with legacy tree icons
- **/
- function iconHelper($q, $timeout) {
- var converter = [
- {
- oldIcon: '.sprNew',
- newIcon: 'add'
- },
- {
- oldIcon: '.sprDelete',
- newIcon: 'remove'
- },
- {
- oldIcon: '.sprMove',
- newIcon: 'enter'
- },
- {
- oldIcon: '.sprCopy',
- newIcon: 'documents'
- },
- {
- oldIcon: '.sprSort',
- newIcon: 'navigation-vertical'
- },
- {
- oldIcon: '.sprPublish',
- newIcon: 'globe'
- },
- {
- oldIcon: '.sprRollback',
- newIcon: 'undo'
- },
- {
- oldIcon: '.sprProtect',
- newIcon: 'lock'
- },
- {
- oldIcon: '.sprAudit',
- newIcon: 'time'
- },
- {
- oldIcon: '.sprNotify',
- newIcon: 'envelope'
- },
- {
- oldIcon: '.sprDomain',
- newIcon: 'home'
- },
- {
- oldIcon: '.sprPermission',
- newIcon: 'lock'
- },
- {
- oldIcon: '.sprRefresh',
- newIcon: 'refresh'
- },
- {
- oldIcon: '.sprBinEmpty',
- newIcon: 'trash'
- },
- {
- oldIcon: '.sprExportDocumentType',
- newIcon: 'download-alt'
- },
- {
- oldIcon: '.sprImportDocumentType',
- newIcon: 'page-up'
- },
- {
- oldIcon: '.sprLiveEdit',
- newIcon: 'edit'
- },
- {
- oldIcon: '.sprCreateFolder',
- newIcon: 'add'
- },
- {
- oldIcon: '.sprPackage2',
- newIcon: 'box'
- },
- {
- oldIcon: '.sprLogout',
- newIcon: 'logout'
- },
- {
- oldIcon: '.sprSave',
- newIcon: 'save'
- },
- {
- oldIcon: '.sprSendToTranslate',
- newIcon: 'envelope-alt'
- },
- {
- oldIcon: '.sprToPublish',
- newIcon: 'mail-forward'
- },
- {
- oldIcon: '.sprTranslate',
- newIcon: 'comments'
- },
- {
- oldIcon: '.sprUpdate',
- newIcon: 'save'
- },
- {
- oldIcon: '.sprTreeSettingDomain',
- newIcon: 'icon-home'
- },
- {
- oldIcon: '.sprTreeDoc',
- newIcon: 'icon-document'
- },
- {
- oldIcon: '.sprTreeDoc2',
- newIcon: 'icon-diploma-alt'
- },
- {
- oldIcon: '.sprTreeDoc3',
- newIcon: 'icon-notepad'
- },
- {
- oldIcon: '.sprTreeDoc4',
- newIcon: 'icon-newspaper-alt'
- },
- {
- oldIcon: '.sprTreeDoc5',
- newIcon: 'icon-notepad-alt'
- },
- {
- oldIcon: '.sprTreeDocPic',
- newIcon: 'icon-picture'
- },
- {
- oldIcon: '.sprTreeFolder',
- newIcon: 'icon-folder'
- },
- {
- oldIcon: '.sprTreeFolder_o',
- newIcon: 'icon-folder'
- },
- {
- oldIcon: '.sprTreeMediaFile',
- newIcon: 'icon-music'
- },
- {
- oldIcon: '.sprTreeMediaMovie',
- newIcon: 'icon-movie'
- },
- {
- oldIcon: '.sprTreeMediaPhoto',
- newIcon: 'icon-picture'
- },
- {
- oldIcon: '.sprTreeMember',
- newIcon: 'icon-user'
- },
- {
- oldIcon: '.sprTreeMemberGroup',
- newIcon: 'icon-users'
- },
- {
- oldIcon: '.sprTreeMemberType',
- newIcon: 'icon-users'
- },
- {
- oldIcon: '.sprTreeNewsletter',
- newIcon: 'icon-file-text-alt'
- },
- {
- oldIcon: '.sprTreePackage',
- newIcon: 'icon-box'
- },
- {
- oldIcon: '.sprTreeRepository',
- newIcon: 'icon-server-alt'
- },
- {
- oldIcon: '.sprTreeSettingDataType',
- newIcon: 'icon-autofill'
- },
- //TODO:
- /*
- { oldIcon: ".sprTreeSettingAgent", newIcon: "" },
- { oldIcon: ".sprTreeSettingCss", newIcon: "" },
- { oldIcon: ".sprTreeSettingCssItem", newIcon: "" },
-
- { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" },
- { oldIcon: ".sprTreeSettingDomain", newIcon: "" },
- { oldIcon: ".sprTreeSettingLanguage", newIcon: "" },
- { oldIcon: ".sprTreeSettingScript", newIcon: "" },
- { oldIcon: ".sprTreeSettingTemplate", newIcon: "" },
- { oldIcon: ".sprTreeSettingXml", newIcon: "" },
- { oldIcon: ".sprTreeStatistik", newIcon: "" },
- { oldIcon: ".sprTreeUser", newIcon: "" },
- { oldIcon: ".sprTreeUserGroup", newIcon: "" },
- { oldIcon: ".sprTreeUserType", newIcon: "" },
- */
- {
- oldIcon: 'folder.png',
- newIcon: 'icon-folder'
- },
- {
- oldIcon: 'mediaphoto.gif',
- newIcon: 'icon-picture'
- },
- {
- oldIcon: 'mediafile.gif',
- newIcon: 'icon-document'
- },
- {
- oldIcon: '.sprTreeDeveloperCacheItem',
- newIcon: 'icon-box'
- },
- {
- oldIcon: '.sprTreeDeveloperCacheTypes',
- newIcon: 'icon-box'
- },
- {
- oldIcon: '.sprTreeDeveloperMacro',
- newIcon: 'icon-cogs'
- },
- {
- oldIcon: '.sprTreeDeveloperRegistry',
- newIcon: 'icon-windows'
- },
- {
- oldIcon: '.sprTreeDeveloperPython',
- newIcon: 'icon-linux'
- }
- ];
- var imageConverter = [{
- oldImage: 'contour.png',
- newIcon: 'icon-umb-contour'
- }];
- var collectedIcons;
- return {
- /** Used by the create dialogs for content/media types to format the data so that the thumbnails are styled properly */
- formatContentTypeThumbnails: function (contentTypes) {
- for (var i = 0; i < contentTypes.length; i++) {
- if (contentTypes[i].thumbnailIsClass === undefined || contentTypes[i].thumbnailIsClass) {
- contentTypes[i].cssClass = this.convertFromLegacyIcon(contentTypes[i].thumbnail);
- } else {
- contentTypes[i].style = 'background-image: url(\'' + contentTypes[i].thumbnailFilePath + '\');height:36px; background-position:4px 0px; background-repeat: no-repeat;background-size: 35px 35px;';
- //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this
- contentTypes[i].cssClass = 'custom-file';
- }
- }
- return contentTypes;
- },
- formatContentTypeIcons: function (contentTypes) {
- for (var i = 0; i < contentTypes.length; i++) {
- if (!contentTypes[i].icon) {
- //just to be safe (e.g. when focus was on close link and hitting save)
- contentTypes[i].icon = 'icon-document'; // default icon
- } else {
- contentTypes[i].icon = this.convertFromLegacyIcon(contentTypes[i].icon);
- }
- //couldnt find replacement
- if (contentTypes[i].icon.indexOf('.') > 0) {
- contentTypes[i].icon = 'icon-document-dashed-line';
- }
- }
- return contentTypes;
- },
- /** If the icon is file based (i.e. it has a file path) */
- isFileBasedIcon: function (icon) {
- //if it doesn't start with a '.' but contains one then we'll assume it's file based
- if (icon.startsWith('..') || !icon.startsWith('.') && icon.indexOf('.') > 1) {
- return true;
- }
- return false;
- },
- /** If the icon is legacy */
- isLegacyIcon: function (icon) {
- if (!icon) {
- return false;
- }
- if (icon.startsWith('..')) {
- return false;
- }
- if (icon.startsWith('.')) {
- return true;
- }
- return false;
- },
- /** If the tree node has a legacy icon */
- isLegacyTreeNodeIcon: function (treeNode) {
- if (treeNode.iconIsClass) {
- return this.isLegacyIcon(treeNode.icon);
- }
- return false;
- },
- /** Return a list of icons, optionally filter them */
- /** It fetches them directly from the active stylesheets in the browser */
- getIcons: function () {
- var deferred = $q.defer();
- $timeout(function () {
- if (collectedIcons) {
- deferred.resolve(collectedIcons);
- } else {
- collectedIcons = [];
- var c = '.icon-';
- for (var i = document.styleSheets.length - 1; i >= 0; i--) {
- var classes = document.styleSheets[i].rules || document.styleSheets[i].cssRules;
- if (classes !== null) {
- for (var x = 0; x < classes.length; x++) {
- var cur = classes[x];
- if (cur.selectorText && cur.selectorText.indexOf(c) === 0) {
- var s = cur.selectorText.substring(1);
- var hasSpace = s.indexOf(' ');
- if (hasSpace > 0) {
- s = s.substring(0, hasSpace);
- }
- var hasPseudo = s.indexOf(':');
- if (hasPseudo > 0) {
- s = s.substring(0, hasPseudo);
- }
- if (collectedIcons.indexOf(s) < 0) {
- collectedIcons.push(s);
- }
- }
- }
- }
- }
- deferred.resolve(collectedIcons);
- }
- }, 100);
- return deferred.promise;
- },
- /** Converts the icon from legacy to a new one if an old one is detected */
- convertFromLegacyIcon: function (icon) {
- if (this.isLegacyIcon(icon)) {
- //its legacy so convert it if we can
- var found = _.find(converter, function (item) {
- return item.oldIcon.toLowerCase() === icon.toLowerCase();
- });
- return found ? found.newIcon : icon;
- }
- return icon;
- },
- convertFromLegacyImage: function (icon) {
- var found = _.find(imageConverter, function (item) {
- return item.oldImage.toLowerCase() === icon.toLowerCase();
- });
- return found ? found.newIcon : undefined;
- },
- /** If we detect that the tree node has legacy icons that can be converted, this will convert them */
- convertFromLegacyTreeNodeIcon: function (treeNode) {
- if (this.isLegacyTreeNodeIcon(treeNode)) {
- return this.convertFromLegacyIcon(treeNode.icon);
- }
- return treeNode.icon;
- }
- };
- }
- angular.module('umbraco.services').factory('iconHelper', iconHelper);
- /**
- * @ngdoc service
- * @name umbraco.services.imageHelper
- * @deprecated
- **/
- function imageHelper(umbRequestHelper, mediaHelper) {
- return {
- /**
- * @ngdoc function
- * @name umbraco.services.imageHelper#getImagePropertyValue
- * @methodOf umbraco.services.imageHelper
- * @function
- *
- * @deprecated
- */
- getImagePropertyValue: function (options) {
- return mediaHelper.getImagePropertyValue(options);
- },
- /**
- * @ngdoc function
- * @name umbraco.services.imageHelper#getThumbnail
- * @methodOf umbraco.services.imageHelper
- * @function
- *
- * @deprecated
- */
- getThumbnail: function (options) {
- return mediaHelper.getThumbnail(options);
- },
- /**
- * @ngdoc function
- * @name umbraco.services.imageHelper#scaleToMaxSize
- * @methodOf umbraco.services.imageHelper
- * @function
- *
- * @deprecated
- */
- scaleToMaxSize: function (maxSize, width, height) {
- return mediaHelper.scaleToMaxSize(maxSize, width, height);
- },
- /**
- * @ngdoc function
- * @name umbraco.services.imageHelper#getThumbnailFromPath
- * @methodOf umbraco.services.imageHelper
- * @function
- *
- * @deprecated
- */
- getThumbnailFromPath: function (imagePath) {
- return mediaHelper.getThumbnailFromPath(imagePath);
- },
- /**
- * @ngdoc function
- * @name umbraco.services.imageHelper#detectIfImageByExtension
- * @methodOf umbraco.services.imageHelper
- * @function
- *
- * @deprecated
- */
- detectIfImageByExtension: function (imagePath) {
- return mediaHelper.detectIfImageByExtension(imagePath);
- }
- };
- }
- angular.module('umbraco.services').factory('imageHelper', imageHelper);
- // This service was based on OpenJS library available in BSD License
- // http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php
- function keyboardService($window, $timeout) {
- var keyboardManagerService = {};
- var defaultOpt = {
- 'type': 'keydown',
- 'propagate': false,
- 'inputDisabled': false,
- 'target': $window.document,
- 'keyCode': false
- };
- // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken
- var shift_nums = {
- '`': '~',
- '1': '!',
- '2': '@',
- '3': '#',
- '4': '$',
- '5': '%',
- '6': '^',
- '7': '&',
- '8': '*',
- '9': '(',
- '0': ')',
- '-': '_',
- '=': '+',
- ';': ':',
- '\'': '"',
- ',': '<',
- '.': '>',
- '/': '?',
- '\\': '|'
- };
- // Special Keys - and their codes
- var special_keys = {
- 'esc': 27,
- 'escape': 27,
- 'tab': 9,
- 'space': 32,
- 'return': 13,
- 'enter': 13,
- 'backspace': 8,
- 'scrolllock': 145,
- 'scroll_lock': 145,
- 'scroll': 145,
- 'capslock': 20,
- 'caps_lock': 20,
- 'caps': 20,
- 'numlock': 144,
- 'num_lock': 144,
- 'num': 144,
- 'pause': 19,
- 'break': 19,
- 'insert': 45,
- 'home': 36,
- 'delete': 46,
- 'end': 35,
- 'pageup': 33,
- 'page_up': 33,
- 'pu': 33,
- 'pagedown': 34,
- 'page_down': 34,
- 'pd': 34,
- 'left': 37,
- 'up': 38,
- 'right': 39,
- 'down': 40,
- 'f1': 112,
- 'f2': 113,
- 'f3': 114,
- 'f4': 115,
- 'f5': 116,
- 'f6': 117,
- 'f7': 118,
- 'f8': 119,
- 'f9': 120,
- 'f10': 121,
- 'f11': 122,
- 'f12': 123
- };
- var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
- // The event handler for bound element events
- function eventHandler(e) {
- e = e || $window.event;
- var code, k;
- // Find out which key is pressed
- if (e.keyCode) {
- code = e.keyCode;
- } else if (e.which) {
- code = e.which;
- }
- var character = String.fromCharCode(code).toLowerCase();
- if (code === 188) {
- character = ',';
- }
- // If the user presses , when the type is onkeydown
- if (code === 190) {
- character = '.';
- }
- // If the user presses , when the type is onkeydown
- var propagate = true;
- //Now we need to determine which shortcut this event is for, we'll do this by iterating over each
- //registered shortcut to find the match. We use Find here so that the loop exits as soon
- //as we've found the one we're looking for
- _.find(_.keys(keyboardManagerService.keyboardEvent), function (key) {
- var shortcutLabel = key;
- var shortcutVal = keyboardManagerService.keyboardEvent[key];
- // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
- var kp = 0;
- // Some modifiers key
- var modifiers = {
- shift: {
- wanted: false,
- pressed: e.shiftKey ? true : false
- },
- ctrl: {
- wanted: false,
- pressed: e.ctrlKey ? true : false
- },
- alt: {
- wanted: false,
- pressed: e.altKey ? true : false
- },
- meta: {
- //Meta is Mac specific
- wanted: false,
- pressed: e.metaKey ? true : false
- }
- };
- var keys = shortcutLabel.split('+');
- var opt = shortcutVal.opt;
- var callback = shortcutVal.callback;
- // Foreach keys in label (split on +)
- var l = keys.length;
- for (var i = 0; i < l; i++) {
- var k = keys[i];
- switch (k) {
- case 'ctrl':
- case 'control':
- kp++;
- modifiers.ctrl.wanted = true;
- break;
- case 'shift':
- case 'alt':
- case 'meta':
- kp++;
- modifiers[k].wanted = true;
- break;
- }
- if (k.length > 1) {
- // If it is a special key
- if (special_keys[k] === code) {
- kp++;
- }
- } else if (opt['keyCode']) {
- // If a specific key is set into the config
- if (opt['keyCode'] === code) {
- kp++;
- }
- } else {
- // The special keys did not match
- if (character === k) {
- kp++;
- } else {
- if (shift_nums[character] && e.shiftKey) {
- // Stupid Shift key bug created by using lowercase
- character = shift_nums[character];
- if (character === k) {
- kp++;
- }
- }
- }
- }
- }
- //for end
- if (kp === keys.length && modifiers.ctrl.pressed === modifiers.ctrl.wanted && modifiers.shift.pressed === modifiers.shift.wanted && modifiers.alt.pressed === modifiers.alt.wanted && modifiers.meta.pressed === modifiers.meta.wanted) {
- //found the right callback!
- // Disable event handler when focus input and textarea
- if (opt['inputDisabled']) {
- var elt;
- if (e.target) {
- elt = e.target;
- } else if (e.srcElement) {
- elt = e.srcElement;
- }
- if (elt.nodeType === 3) {
- elt = elt.parentNode;
- }
- if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA') {
- //This exits the Find loop
- return true;
- }
- }
- $timeout(function () {
- callback(e);
- }, 1);
- if (!opt['propagate']) {
- // Stop the event
- propagate = false;
- }
- //This exits the Find loop
- return true;
- }
- //we haven't found one so continue looking
- return false;
- });
- // Stop the event if required
- if (!propagate) {
- // e.cancelBubble is supported by IE - this will kill the bubbling process.
- e.cancelBubble = true;
- e.returnValue = false;
- // e.stopPropagation works in Firefox.
- if (e.stopPropagation) {
- e.stopPropagation();
- e.preventDefault();
- }
- return false;
- }
- }
- // Store all keyboard combination shortcuts
- keyboardManagerService.keyboardEvent = {};
- // Add a new keyboard combination shortcut
- keyboardManagerService.bind = function (label, callback, opt) {
- //replace ctrl key with meta key
- if (isMac && label !== 'ctrl+space') {
- label = label.replace('ctrl', 'meta');
- }
- var elt;
- // Initialize opt object
- opt = angular.extend({}, defaultOpt, opt);
- label = label.toLowerCase();
- elt = opt.target;
- if (typeof opt.target === 'string') {
- elt = document.getElementById(opt.target);
- }
- //Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding
- // and raising events for now reason. So here we'll check if the event is already registered for the element
- var boundValues = _.values(keyboardManagerService.keyboardEvent);
- var found = _.find(boundValues, function (i) {
- return i.target === elt && i.event === opt['type'];
- });
- // Store shortcut
- keyboardManagerService.keyboardEvent[label] = {
- 'callback': callback,
- 'target': elt,
- 'opt': opt
- };
- if (!found) {
- //Attach the function with the event
- if (elt.addEventListener) {
- elt.addEventListener(opt['type'], eventHandler, false);
- } else if (elt.attachEvent) {
- elt.attachEvent('on' + opt['type'], eventHandler);
- } else {
- elt['on' + opt['type']] = eventHandler;
- }
- }
- };
- // Remove the shortcut - just specify the shortcut and I will remove the binding
- keyboardManagerService.unbind = function (label) {
- label = label.toLowerCase();
- var binding = keyboardManagerService.keyboardEvent[label];
- delete keyboardManagerService.keyboardEvent[label];
- if (!binding) {
- return;
- }
- var type = binding['event'], elt = binding['target'], callback = binding['callback'];
- if (elt.detachEvent) {
- elt.detachEvent('on' + type, callback);
- } else if (elt.removeEventListener) {
- elt.removeEventListener(type, callback, false);
- } else {
- elt['on' + type] = false;
- }
- };
- //
- return keyboardManagerService;
- }
- angular.module('umbraco.services').factory('keyboardService', [
- '$window',
- '$timeout',
- keyboardService
- ]);
- /**
- @ngdoc service
- * @name umbraco.services.listViewHelper
- *
- *
- * @description
- * Service for performing operations against items in the list view UI. Used by the built-in internal listviews
- * as well as custom listview.
- *
- * A custom listview is always used inside a wrapper listview, so there are a number of inherited values on its
- * scope by default:
- *
- * **$scope.selection**: Array containing all items currently selected in the listview
- *
- * **$scope.items**: Array containing all items currently displayed in the listview
- *
- * **$scope.folders**: Array containing all folders in the current listview (only for media)
- *
- * **$scope.options**: configuration object containing information such as pagesize, permissions, order direction etc.
- *
- * **$scope.model.config.layouts**: array of available layouts to apply to the listview (grid, list or custom layout)
- *
- * ##Usage##
- * To use, inject listViewHelper into custom listview controller, listviewhelper expects you
- * to pass in the full collection of items in the listview in several of its methods
- * this collection is inherited from the parent controller and is available on $scope.selection
- *
- * <pre>
- * angular.module("umbraco").controller("my.listVieweditor". function($scope, listViewHelper){
- *
- * //current items in the listview
- * var items = $scope.items;
- *
- * //current selection
- * var selection = $scope.selection;
- *
- * //deselect an item , $scope.selection is inherited, item is picked from inherited $scope.items
- * listViewHelper.deselectItem(item, $scope.selection);
- *
- * //test if all items are selected, $scope.items + $scope.selection are inherited
- * listViewhelper.isSelectedAll($scope.items, $scope.selection);
- * });
- * </pre>
- */
- (function () {
- 'use strict';
- function listViewHelper(localStorageService) {
- var firstSelectedIndex = 0;
- var localStorageKey = 'umblistViewLayout';
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#getLayout
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Method for internal use, based on the collection of layouts passed, the method selects either
- * any previous layout from local storage, or picks the first allowed layout
- *
- * @param {Number} nodeId The id of the current node displayed in the content editor
- * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts
- */
- function getLayout(nodeId, availableLayouts) {
- var storedLayouts = [];
- if (localStorageService.get(localStorageKey)) {
- storedLayouts = localStorageService.get(localStorageKey);
- }
- if (storedLayouts && storedLayouts.length > 0) {
- for (var i = 0; storedLayouts.length > i; i++) {
- var layout = storedLayouts[i];
- if (layout.nodeId === nodeId) {
- return setLayout(nodeId, layout, availableLayouts);
- }
- }
- }
- return getFirstAllowedLayout(availableLayouts);
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#setLayout
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage
- *
- * @param {Number} nodeID Id of the current node displayed in the content editor
- * @param {Object} selectedLayout Layout selected as the layout to set as the current layout
- * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts
- */
- function setLayout(nodeId, selectedLayout, availableLayouts) {
- var activeLayout = {};
- var layoutFound = false;
- for (var i = 0; availableLayouts.length > i; i++) {
- var layout = availableLayouts[i];
- if (layout.path === selectedLayout.path) {
- activeLayout = layout;
- layout.active = true;
- layoutFound = true;
- } else {
- layout.active = false;
- }
- }
- if (!layoutFound) {
- activeLayout = getFirstAllowedLayout(availableLayouts);
- }
- saveLayoutInLocalStorage(nodeId, activeLayout);
- return activeLayout;
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#saveLayoutInLocalStorage
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Stores a given layout as the current default selection in local storage
- *
- * @param {Number} nodeId Id of the current node displayed in the content editor
- * @param {Object} selectedLayout Layout selected as the layout to set as the current layout
- */
- function saveLayoutInLocalStorage(nodeId, selectedLayout) {
- var layoutFound = false;
- var storedLayouts = [];
- if (localStorageService.get(localStorageKey)) {
- storedLayouts = localStorageService.get(localStorageKey);
- }
- if (storedLayouts.length > 0) {
- for (var i = 0; storedLayouts.length > i; i++) {
- var layout = storedLayouts[i];
- if (layout.nodeId === nodeId) {
- layout.path = selectedLayout.path;
- layoutFound = true;
- }
- }
- }
- if (!layoutFound) {
- var storageObject = {
- 'nodeId': nodeId,
- 'path': selectedLayout.path
- };
- storedLayouts.push(storageObject);
- }
- localStorageService.set(localStorageKey, storedLayouts);
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#getFirstAllowedLayout
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Returns currently selected layout, or alternatively the first layout in the available layouts collection
- *
- * @param {Array} layouts Array of all allowed layouts, available from $scope.model.config.layouts
- */
- function getFirstAllowedLayout(layouts) {
- var firstAllowedLayout = {};
- for (var i = 0; layouts.length > i; i++) {
- var layout = layouts[i];
- if (layout.selected === true) {
- firstAllowedLayout = layout;
- break;
- }
- }
- return firstAllowedLayout;
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#selectHandler
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Helper method for working with item selection via a checkbox, internally it uses selectItem and deselectItem.
- * Working with this method, requires its triggered via a checkbox which can then pass in its triggered $event
- * When the checkbox is clicked, this method will toggle selection of the associated item so it matches the state of the checkbox
- *
- * @param {Object} selectedItem Item being selected or deselected by the checkbox
- * @param {Number} selectedIndex Index of item being selected/deselected, usually passed as $index
- * @param {Array} items All items in the current listview, available as $scope.items
- * @param {Array} selection All selected items in the current listview, available as $scope.selection
- * @param {Event} $event Event triggered by the checkbox being checked to select / deselect an item
- */
- function selectHandler(selectedItem, selectedIndex, items, selection, $event) {
- var start = 0;
- var end = 0;
- var item = null;
- if ($event.shiftKey === true) {
- if (selectedIndex > firstSelectedIndex) {
- start = firstSelectedIndex;
- end = selectedIndex;
- for (; end >= start; start++) {
- item = items[start];
- selectItem(item, selection);
- }
- } else {
- start = firstSelectedIndex;
- end = selectedIndex;
- for (; end <= start; start--) {
- item = items[start];
- selectItem(item, selection);
- }
- }
- } else {
- if (selectedItem.selected) {
- deselectItem(selectedItem, selection);
- } else {
- selectItem(selectedItem, selection);
- }
- firstSelectedIndex = selectedIndex;
- }
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#selectItem
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Selects a given item to the listview selection array, requires you pass in the inherited $scope.selection collection
- *
- * @param {Object} item Item to select
- * @param {Array} selection Listview selection, available as $scope.selection
- */
- function selectItem(item, selection) {
- var isSelected = false;
- for (var i = 0; selection.length > i; i++) {
- var selectedItem = selection[i];
- // if item.id is 2147483647 (int.MaxValue) use item.key
- if (item.id !== 2147483647 && item.id === selectedItem.id || item.key === selectedItem.key) {
- isSelected = true;
- }
- }
- if (!isSelected) {
- selection.push({
- id: item.id,
- key: item.key
- });
- item.selected = true;
- }
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#deselectItem
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Deselects a given item from the listviews selection array, requires you pass in the inherited $scope.selection collection
- *
- * @param {Object} item Item to deselect
- * @param {Array} selection Listview selection, available as $scope.selection
- */
- function deselectItem(item, selection) {
- for (var i = 0; selection.length > i; i++) {
- var selectedItem = selection[i];
- // if item.id is 2147483647 (int.MaxValue) use item.key
- if (item.id !== 2147483647 && item.id === selectedItem.id || item.key === selectedItem.key) {
- selection.splice(i, 1);
- item.selected = false;
- }
- }
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#clearSelection
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Removes a given number of items and folders from the listviews selection array
- * Folders can only be passed in if the listview is used in the media section which has a concept of folders.
- *
- * @param {Array} items Items to remove, can be null
- * @param {Array} folders Folders to remove, can be null
- * @param {Array} selection Listview selection, available as $scope.selection
- */
- function clearSelection(items, folders, selection) {
- var i = 0;
- selection.length = 0;
- if (angular.isArray(items)) {
- for (i = 0; items.length > i; i++) {
- var item = items[i];
- item.selected = false;
- }
- }
- if (angular.isArray(folders)) {
- for (i = 0; folders.length > i; i++) {
- var folder = folders[i];
- folder.selected = false;
- }
- }
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#selectAllItems
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Helper method for toggling the select state on all items in the active listview
- * Can only be used from a checkbox as a checkbox $event is required to pass in.
- *
- * @param {Array} items Items to toggle selection on, should be $scope.items
- * @param {Array} selection Listview selection, available as $scope.selection
- * @param {$event} $event Event passed from the checkbox being toggled
- */
- function selectAllItems(items, selection, $event) {
- var checkbox = $event.target;
- var clearSelection = false;
- if (!angular.isArray(items)) {
- return;
- }
- selection.length = 0;
- for (var i = 0; i < items.length; i++) {
- var item = items[i];
- if (checkbox.checked) {
- selection.push({
- id: item.id,
- key: item.key
- });
- } else {
- clearSelection = true;
- }
- item.selected = checkbox.checked;
- }
- if (clearSelection) {
- selection.length = 0;
- }
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#isSelectedAll
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Method to determine if all items on the current page in the list has been selected
- * Given the current items in the view, and the current selection, it will return true/false
- *
- * @param {Array} items Items to test if all are selected, should be $scope.items
- * @param {Array} selection Listview selection, available as $scope.selection
- * @returns {Boolean} boolean indicate if all items in the listview have been selected
- */
- function isSelectedAll(items, selection) {
- var numberOfSelectedItem = 0;
- for (var itemIndex = 0; items.length > itemIndex; itemIndex++) {
- var item = items[itemIndex];
- for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) {
- var selectedItem = selection[selectedIndex];
- // if item.id is 2147483647 (int.MaxValue) use item.key
- if (item.id !== 2147483647 && item.id === selectedItem.id || item.key === selectedItem.key) {
- numberOfSelectedItem++;
- }
- }
- }
- if (numberOfSelectedItem === items.length) {
- return true;
- }
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#setSortingDirection
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * *Internal* method for changing sort order icon
- * @param {String} col Column alias to order after
- * @param {String} direction Order direction `asc` or `desc`
- * @param {Object} options object passed from the parent listview available as $scope.options
- */
- function setSortingDirection(col, direction, options) {
- return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction;
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewHelper#setSorting
- * @methodOf umbraco.services.listViewHelper
- *
- * @description
- * Method for setting the field on which the listview will order its items after.
- *
- * @param {String} field Field alias to order after
- * @param {Boolean} allow Determines if the user is allowed to set this field, normally true
- * @param {Object} options Options object passed from the parent listview available as $scope.options
- */
- function setSorting(field, allow, options) {
- if (allow) {
- if (options.orderBy === field && options.orderDirection === 'asc') {
- options.orderDirection = 'desc';
- } else {
- options.orderDirection = 'asc';
- }
- options.orderBy = field;
- }
- }
- //This takes in a dictionary of Ids with Permissions and determines
- // the intersect of all permissions to return an object representing the
- // listview button permissions
- function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) {
- if (currentIdsWithPermissions == null) {
- currentIdsWithPermissions = {};
- }
- //merge the newly retrieved permissions to the main dictionary
- _.each(unmergedPermissions, function (value, key, list) {
- currentIdsWithPermissions[key] = value;
- });
- //get the intersect permissions
- var arr = [];
- _.each(currentIdsWithPermissions, function (value, key, list) {
- arr.push(value);
- });
- //we need to use 'apply' to call intersection with an array of arrays,
- //see: http://stackoverflow.com/a/16229480/694494
- var intersectPermissions = _.intersection.apply(_, arr);
- return {
- canCopy: _.contains(intersectPermissions, 'O'),
- //Magic Char = O
- canCreate: _.contains(intersectPermissions, 'C'),
- //Magic Char = C
- canDelete: _.contains(intersectPermissions, 'D'),
- //Magic Char = D
- canMove: _.contains(intersectPermissions, 'M'),
- //Magic Char = M
- canPublish: _.contains(intersectPermissions, 'U'),
- //Magic Char = U
- canUnpublish: _.contains(intersectPermissions, 'U')
- };
- }
- var service = {
- getLayout: getLayout,
- getFirstAllowedLayout: getFirstAllowedLayout,
- setLayout: setLayout,
- saveLayoutInLocalStorage: saveLayoutInLocalStorage,
- selectHandler: selectHandler,
- selectItem: selectItem,
- deselectItem: deselectItem,
- clearSelection: clearSelection,
- selectAllItems: selectAllItems,
- isSelectedAll: isSelectedAll,
- setSortingDirection: setSortingDirection,
- setSorting: setSorting,
- getButtonPermissions: getButtonPermissions
- };
- return service;
- }
- angular.module('umbraco.services').factory('listViewHelper', listViewHelper);
- }());
- /**
- @ngdoc service
- * @name umbraco.services.listViewPrevalueHelper
- *
- *
- * @description
- * Service for accessing the prevalues of a list view being edited in the inline list view editor in the doctype editor
- */
- (function () {
- 'use strict';
- function listViewPrevalueHelper() {
- var prevalues = [];
- /**
- * @ngdoc method
- * @name umbraco.services.listViewPrevalueHelper#getPrevalues
- * @methodOf umbraco.services.listViewPrevalueHelper
- *
- * @description
- * Set the collection of prevalues
- */
- function getPrevalues() {
- return prevalues;
- }
- /**
- * @ngdoc method
- * @name umbraco.services.listViewPrevalueHelper#setPrevalues
- * @methodOf umbraco.services.listViewPrevalueHelper
- *
- * @description
- * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage
- *
- * @param {Array} values Array of prevalues
- */
- function setPrevalues(values) {
- prevalues = values;
- }
- var service = {
- getPrevalues: getPrevalues,
- setPrevalues: setPrevalues
- };
- return service;
- }
- angular.module('umbraco.services').factory('listViewPrevalueHelper', listViewPrevalueHelper);
- }());
- /**
- * @ngdoc service
- * @name umbraco.services.localizationService
- *
- * @requires $http
- * @requires $q
- * @requires $window
- * @requires $filter
- *
- * @description
- * Application-wide service for handling localization
- *
- * ##usage
- * To use, simply inject the localizationService into any controller that needs it, and make
- * sure the umbraco.services module is accesible - which it should be by default.
- *
- * <pre>
- * localizationService.localize("area_key").then(function(value){
- * element.html(value);
- * });
- * </pre>
- */
- angular.module('umbraco.services').factory('localizationService', function ($http, $q, eventsService, $window, $filter, userService) {
- //TODO: This should be injected as server vars
- var url = 'LocalizedText';
- var resourceFileLoadStatus = 'none';
- var resourceLoadingPromise = [];
- function _lookup(value, tokens, dictionary) {
- //strip the key identifier if its there
- if (value && value[0] === '@') {
- value = value.substring(1);
- }
- //if no area specified, add general_
- if (value && value.indexOf('_') < 0) {
- value = 'general_' + value;
- }
- var entry = dictionary[value];
- if (entry) {
- if (tokens) {
- for (var i = 0; i < tokens.length; i++) {
- entry = entry.replace('%' + i + '%', tokens[i]);
- }
- }
- return entry;
- }
- return '[' + value + ']';
- }
- var service = {
- // array to hold the localized resource string entries
- dictionary: [],
- // loads the language resource file from the server
- initLocalizedResources: function () {
- var deferred = $q.defer();
- if (resourceFileLoadStatus === 'loaded') {
- deferred.resolve(service.dictionary);
- return deferred.promise;
- }
- //if the resource is already loading, we don't want to force it to load another one in tandem, we'd rather
- // wait for that initial http promise to finish and then return this one with the dictionary loaded
- if (resourceFileLoadStatus === 'loading') {
- //add to the list of promises waiting
- resourceLoadingPromise.push(deferred);
- //exit now it's already loading
- return deferred.promise;
- }
- resourceFileLoadStatus = 'loading';
- // build the url to retrieve the localized resource file
- $http({
- method: 'GET',
- url: url,
- cache: false
- }).then(function (response) {
- resourceFileLoadStatus = 'loaded';
- service.dictionary = response.data;
- eventsService.emit('localizationService.updated', response.data);
- deferred.resolve(response.data);
- //ensure all other queued promises are resolved
- for (var p in resourceLoadingPromise) {
- resourceLoadingPromise[p].resolve(response.data);
- }
- }, function (err) {
- deferred.reject('Something broke');
- //ensure all other queued promises are resolved
- for (var p in resourceLoadingPromise) {
- resourceLoadingPromise[p].reject('Something broke');
- }
- });
- return deferred.promise;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.localizationService#tokenize
- * @methodOf umbraco.services.localizationService
- *
- * @description
- * Helper to tokenize and compile a localization string
- * @param {String} value the value to tokenize
- * @param {Object} scope the $scope object
- * @returns {String} tokenized resource string
- */
- tokenize: function (value, scope) {
- if (value) {
- var localizer = value.split(':');
- var retval = {
- tokens: undefined,
- key: localizer[0].substring(0)
- };
- if (localizer.length > 1) {
- retval.tokens = localizer[1].split(',');
- for (var x = 0; x < retval.tokens.length; x++) {
- retval.tokens[x] = scope.$eval(retval.tokens[x]);
- }
- }
- return retval;
- }
- return value;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.localizationService#localize
- * @methodOf umbraco.services.localizationService
- *
- * @description
- * Checks the dictionary for a localized resource string
- * @param {String} value the area/key to localize in the format of 'section_key'
- * alternatively if no section is set such as 'key' then we assume the key is to be looked in
- * the 'general' section
- *
- * @param {Array} tokens if specified this array will be sent as parameter values
- * This replaces %0% and %1% etc in the dictionary key value with the passed in strings
- *
- * @returns {String} localized resource string
- */
- localize: function (value, tokens) {
- return service.initLocalizedResources().then(function (dic) {
- var val = _lookup(value, tokens, dic);
- return val;
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.localizationService#localizeMany
- * @methodOf umbraco.services.localizationService
- *
- * @description
- * Checks the dictionary for multipe localized resource strings at once, preventing the need for nested promises
- * with localizationService.localize
- *
- * ##Usage
- * <pre>
- * localizationService.localizeMany(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){
- * var header = data[0];
- * var message = data[1];
- * notificationService.error(header, message);
- * });
- * </pre>
- *
- * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key'
- * alternatively if no section is set such as 'key' then we assume the key is to be looked in
- * the 'general' section
- *
- * @returns {Array} An array of localized resource string in the same order
- */
- localizeMany: function (keys) {
- if (keys) {
- //The LocalizationService.localize promises we want to resolve
- var promises = [];
- for (var i = 0; i < keys.length; i++) {
- promises.push(service.localize(keys[i], undefined));
- }
- return $q.all(promises).then(function (localizedValues) {
- return localizedValues;
- });
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.localizationService#concat
- * @methodOf umbraco.services.localizationService
- *
- * @description
- * Checks the dictionary for multipe localized resource strings at once & concats them to a single string
- * Which was not possible with localizationSerivce.localize() due to returning a promise
- *
- * ##Usage
- * <pre>
- * localizationService.concat(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){
- * var combinedText = data;
- * });
- * </pre>
- *
- * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key'
- * alternatively if no section is set such as 'key' then we assume the key is to be looked in
- * the 'general' section
- *
- * @returns {String} An concatenated string of localized resource string passed into the function in the same order
- */
- concat: function (keys) {
- if (keys) {
- //The LocalizationService.localize promises we want to resolve
- var promises = [];
- for (var i = 0; i < keys.length; i++) {
- promises.push(service.localize(keys[i], undefined));
- }
- return $q.all(promises).then(function (localizedValues) {
- //Build a concat string by looping over the array of resolved promises/translations
- var returnValue = '';
- for (var i = 0; i < localizedValues.length; i++) {
- returnValue += localizedValues[i];
- }
- return returnValue;
- });
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.localizationService#format
- * @methodOf umbraco.services.localizationService
- *
- * @description
- * Checks the dictionary for multipe localized resource strings at once & formats a tokenized message
- * Which was not possible with localizationSerivce.localize() due to returning a promise
- *
- * ##Usage
- * <pre>
- * localizationService.format(["template_insert", "template_insertSections"], "%0% %1%").then(function(data){
- * //Will return 'Insert Sections'
- * var formattedResult = data;
- * });
- * </pre>
- *
- * @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key'
- * alternatively if no section is set such as 'key' then we assume the key is to be looked in
- * the 'general' section
- *
- * @param {String} message is the string you wish to replace containing tokens in the format of %0% and %1%
- * with the localized resource strings
- *
- * @returns {String} An concatenated string of localized resource string passed into the function in the same order
- */
- format: function (keys, message) {
- if (keys) {
- //The LocalizationService.localize promises we want to resolve
- var promises = [];
- for (var i = 0; i < keys.length; i++) {
- promises.push(service.localize(keys[i], undefined));
- }
- return $q.all(promises).then(function (localizedValues) {
- //Replace {0} and {1} etc in message with the localized values
- for (var i = 0; i < localizedValues.length; i++) {
- var token = '%' + i + '%';
- var regex = new RegExp(token, 'g');
- message = message.replace(regex, localizedValues[i]);
- }
- return message;
- });
- }
- }
- };
- //This happens after login / auth and assets loading
- eventsService.on('app.authenticated', function () {
- resourceFileLoadStatus = 'none';
- resourceLoadingPromise = [];
- });
- // return the local instance when called
- return service;
- });
- /**
- * @ngdoc service
- * @name umbraco.services.macroService
- *
- *
- * @description
- * A service to return macro information such as generating syntax to insert a macro into an editor
- */
- function macroService() {
- return {
- /** parses the special macro syntax like <?UMBRACO_MACRO macroAlias="Map" /> and returns an object with the macro alias and it's parameters */
- parseMacroSyntax: function (syntax) {
- //This regex will match an alias of anything except characters that are quotes or new lines (for legacy reasons, when new macros are created
- // their aliases are cleaned an invalid chars are stripped)
- var expression = /(<\?UMBRACO_MACRO (?:.+?)?macroAlias=["']([^\"\'\n\r]+?)["'][\s\S]+?)(\/>|>.*?<\/\?UMBRACO_MACRO>)/i;
- var match = expression.exec(syntax);
- if (!match || match.length < 3) {
- return null;
- }
- var alias = match[2];
- //this will leave us with just the parameters
- var paramsChunk = match[1].trim().replace(new RegExp('UMBRACO_MACRO macroAlias=["\']' + alias + '["\']'), '').trim();
- var paramExpression = /(\w+?)=['\"]([\s\S]*?)['\"]/g;
- var paramMatch;
- var returnVal = {
- macroAlias: alias,
- macroParamsDictionary: {}
- };
- while (paramMatch = paramExpression.exec(paramsChunk)) {
- returnVal.macroParamsDictionary[paramMatch[1]] = paramMatch[2];
- }
- return returnVal;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.macroService#generateWebFormsSyntax
- * @methodOf umbraco.services.macroService
- * @function
- *
- * @description
- * generates the syntax for inserting a macro into a rich text editor - this is the very old umbraco style syntax
- *
- * @param {object} args an object containing the macro alias and it's parameter values
- */
- generateMacroSyntax: function (args) {
- // <?UMBRACO_MACRO macroAlias="BlogListPosts" />
- var macroString = '<?UMBRACO_MACRO macroAlias="' + args.macroAlias + '" ';
- if (args.macroParamsDictionary) {
- _.each(args.macroParamsDictionary, function (val, key) {
- //check for null
- val = val ? val : '';
- //need to detect if the val is a string or an object
- var keyVal;
- if (angular.isString(val)) {
- keyVal = key + '="' + (val ? val : '') + '" ';
- } else {
- //if it's not a string we'll send it through the json serializer
- var json = angular.toJson(val);
- //then we need to url encode it so that it's safe
- var encoded = encodeURIComponent(json);
- keyVal = key + '="' + encoded + '" ';
- }
- macroString += keyVal;
- });
- }
- macroString += '/>';
- return macroString;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.macroService#generateWebFormsSyntax
- * @methodOf umbraco.services.macroService
- * @function
- *
- * @description
- * generates the syntax for inserting a macro into a webforms templates
- *
- * @param {object} args an object containing the macro alias and it's parameter values
- */
- generateWebFormsSyntax: function (args) {
- var macroString = '<umbraco:Macro ';
- if (args.macroParamsDictionary) {
- _.each(args.macroParamsDictionary, function (val, key) {
- var keyVal = key + '="' + (val ? val : '') + '" ';
- macroString += keyVal;
- });
- }
- macroString += 'Alias="' + args.macroAlias + '" runat="server"></umbraco:Macro>';
- return macroString;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.macroService#generateMvcSyntax
- * @methodOf umbraco.services.macroService
- * @function
- *
- * @description
- * generates the syntax for inserting a macro into an mvc template
- *
- * @param {object} args an object containing the macro alias and it's parameter values
- */
- generateMvcSyntax: function (args) {
- var macroString = '@Umbraco.RenderMacro("' + args.macroAlias + '"';
- var hasParams = false;
- var paramString;
- if (args.macroParamsDictionary) {
- paramString = ', new {';
- _.each(args.macroParamsDictionary, function (val, key) {
- hasParams = true;
- var keyVal = key + '="' + (val ? val : '') + '", ';
- paramString += keyVal;
- });
- //remove the last ,
- paramString = paramString.trimEnd(', ');
- paramString += '}';
- }
- if (hasParams) {
- macroString += paramString;
- }
- macroString += ')';
- return macroString;
- },
- collectValueData: function (macro, macroParams, renderingEngine) {
- var paramDictionary = {};
- var macroAlias = macro.alias;
- var syntax;
- _.each(macroParams, function (item) {
- var val = item.value;
- if (item.value !== null && item.value !== undefined && !_.isString(item.value)) {
- try {
- val = angular.toJson(val);
- } catch (e) {
- }
- }
- //each value needs to be xml escaped!! since the value get's stored as an xml attribute
- paramDictionary[item.alias] = _.escape(val);
- });
- //get the syntax based on the rendering engine
- if (renderingEngine && renderingEngine === 'WebForms') {
- syntax = this.generateWebFormsSyntax({
- macroAlias: macroAlias,
- macroParamsDictionary: paramDictionary
- });
- } else if (renderingEngine && renderingEngine === 'Mvc') {
- syntax = this.generateMvcSyntax({
- macroAlias: macroAlias,
- macroParamsDictionary: paramDictionary
- });
- } else {
- syntax = this.generateMacroSyntax({
- macroAlias: macroAlias,
- macroParamsDictionary: paramDictionary
- });
- }
- var macroObject = {
- 'macroParamsDictionary': paramDictionary,
- 'macroAlias': macroAlias,
- 'syntax': syntax
- };
- return macroObject;
- }
- };
- }
- angular.module('umbraco.services').factory('macroService', macroService);
- /**
- * @ngdoc service
- * @name umbraco.services.mediaHelper
- * @description A helper object used for dealing with media items
- **/
- function mediaHelper(umbRequestHelper) {
- //container of fileresolvers
- var _mediaFileResolvers = {};
- return {
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#getImagePropertyValue
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * Returns the file path associated with the media property if there is one
- *
- * @param {object} options Options object
- * @param {object} options.mediaModel The media object to retrieve the image path from
- * @param {object} options.imageOnly Optional, if true then will only return a path if the media item is an image
- */
- getMediaPropertyValue: function (options) {
- if (!options || !options.mediaModel) {
- throw 'The options objet does not contain the required parameters: mediaModel';
- }
- //combine all props, TODO: we really need a better way then this
- var props = [];
- if (options.mediaModel.properties) {
- props = options.mediaModel.properties;
- } else {
- $(options.mediaModel.tabs).each(function (i, tab) {
- props = props.concat(tab.properties);
- });
- }
- var mediaRoot = Umbraco.Sys.ServerVariables.umbracoSettings.mediaPath;
- var imageProp = _.find(props, function (item) {
- if (item.alias === 'umbracoFile') {
- return true;
- }
- //this performs a simple check to see if we have a media file as value
- //it doesnt catch everything, but better then nothing
- if (angular.isString(item.value) && item.value.indexOf(mediaRoot) === 0) {
- return true;
- }
- return false;
- });
- if (!imageProp) {
- return '';
- }
- var mediaVal;
- //our default images might store one or many images (as csv)
- var split = imageProp.value.split(',');
- var self = this;
- mediaVal = _.map(split, function (item) {
- return {
- file: item,
- isImage: self.detectIfImageByExtension(item)
- };
- });
- //for now we'll just return the first image in the collection.
- //TODO: we should enable returning many to be displayed in the picker if the uploader supports many.
- if (mediaVal.length && mediaVal.length > 0) {
- if (!options.imageOnly || options.imageOnly === true && mediaVal[0].isImage) {
- return mediaVal[0].file;
- }
- }
- return '';
- },
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#getImagePropertyValue
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * Returns the actual image path associated with the image property if there is one
- *
- * @param {object} options Options object
- * @param {object} options.imageModel The media object to retrieve the image path from
- */
- getImagePropertyValue: function (options) {
- if (!options || !options.imageModel && !options.mediaModel) {
- throw 'The options objet does not contain the required parameters: imageModel';
- }
- //required to support backwards compatibility.
- options.mediaModel = options.imageModel ? options.imageModel : options.mediaModel;
- options.imageOnly = true;
- return this.getMediaPropertyValue(options);
- },
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#getThumbnail
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * formats the display model used to display the content to the model used to save the content
- *
- * @param {object} options Options object
- * @param {object} options.imageModel The media object to retrieve the image path from
- */
- getThumbnail: function (options) {
- if (!options || !options.imageModel) {
- throw 'The options objet does not contain the required parameters: imageModel';
- }
- var imagePropVal = this.getImagePropertyValue(options);
- if (imagePropVal !== '') {
- return this.getThumbnailFromPath(imagePropVal);
- }
- return '';
- },
- registerFileResolver: function (propertyEditorAlias, func) {
- _mediaFileResolvers[propertyEditorAlias] = func;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#resolveFileFromEntity
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * Gets the media file url for a media entity returned with the entityResource
- *
- * @param {object} mediaEntity A media Entity returned from the entityResource
- * @param {boolean} thumbnail Whether to return the thumbnail url or normal url
- */
- resolveFileFromEntity: function (mediaEntity, thumbnail) {
- if (!angular.isObject(mediaEntity.metaData)) {
- throw 'Cannot resolve the file url from the mediaEntity, it does not contain the required metaData';
- }
- var values = _.values(mediaEntity.metaData);
- for (var i = 0; i < values.length; i++) {
- var val = values[i];
- if (angular.isObject(val) && val.PropertyEditorAlias) {
- for (var resolver in _mediaFileResolvers) {
- if (val.PropertyEditorAlias === resolver) {
- //we need to format a property variable that coincides with how the property would be structured
- // if it came from the mediaResource just to keep things slightly easier for the file resolvers.
- var property = { value: val.Value };
- return _mediaFileResolvers[resolver](property, mediaEntity, thumbnail);
- }
- }
- }
- }
- return '';
- },
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#resolveFile
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * Gets the media file url for a media object returned with the mediaResource
- *
- * @param {object} mediaEntity A media Entity returned from the entityResource
- * @param {boolean} thumbnail Whether to return the thumbnail url or normal url
- */
- /*jshint loopfunc: true */
- resolveFile: function (mediaItem, thumbnail) {
- function iterateProps(props) {
- var res = null;
- for (var resolver in _mediaFileResolvers) {
- var property = _.find(props, function (prop) {
- return prop.editor === resolver;
- });
- if (property) {
- res = _mediaFileResolvers[resolver](property, mediaItem, thumbnail);
- break;
- }
- }
- return res;
- }
- //we either have properties raw on the object, or spread out on tabs
- var result = '';
- if (mediaItem.properties) {
- result = iterateProps(mediaItem.properties);
- } else if (mediaItem.tabs) {
- for (var tab in mediaItem.tabs) {
- if (mediaItem.tabs[tab].properties) {
- result = iterateProps(mediaItem.tabs[tab].properties);
- if (result) {
- break;
- }
- }
- }
- }
- return result;
- },
- /*jshint loopfunc: true */
- hasFilePropertyType: function (mediaItem) {
- function iterateProps(props) {
- var res = false;
- for (var resolver in _mediaFileResolvers) {
- var property = _.find(props, function (prop) {
- return prop.editor === resolver;
- });
- if (property) {
- res = true;
- break;
- }
- }
- return res;
- }
- //we either have properties raw on the object, or spread out on tabs
- var result = false;
- if (mediaItem.properties) {
- result = iterateProps(mediaItem.properties);
- } else if (mediaItem.tabs) {
- for (var tab in mediaItem.tabs) {
- if (mediaItem.tabs[tab].properties) {
- result = iterateProps(mediaItem.tabs[tab].properties);
- if (result) {
- break;
- }
- }
- }
- }
- return result;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#scaleToMaxSize
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * Finds the corrct max width and max height, given maximum dimensions and keeping aspect ratios
- *
- * @param {number} maxSize Maximum width & height
- * @param {number} width Current width
- * @param {number} height Current height
- */
- scaleToMaxSize: function (maxSize, width, height) {
- var retval = {
- width: width,
- height: height
- };
- var maxWidth = maxSize;
- // Max width for the image
- var maxHeight = maxSize;
- // Max height for the image
- var ratio = 0;
- // Used for aspect ratio
- // Check if the current width is larger than the max
- if (width > maxWidth) {
- ratio = maxWidth / width;
- // get ratio for scaling image
- retval.width = maxWidth;
- retval.height = height * ratio;
- height = height * ratio;
- // Reset height to match scaled image
- width = width * ratio; // Reset width to match scaled image
- }
- // Check if current height is larger than max
- if (height > maxHeight) {
- ratio = maxHeight / height;
- // get ratio for scaling image
- retval.height = maxHeight;
- retval.width = width * ratio;
- width = width * ratio; // Reset width to match scaled image
- }
- return retval;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#getThumbnailFromPath
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * Returns the path to the thumbnail version of a given media library image path
- *
- * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg
- */
- getThumbnailFromPath: function (imagePath) {
- //If the path is not an image we cannot get a thumb
- if (!this.detectIfImageByExtension(imagePath)) {
- return null;
- }
- //get the proxy url for big thumbnails (this ensures one is always generated)
- var thumbnailUrl = umbRequestHelper.getApiUrl('imagesApiBaseUrl', 'GetBigThumbnail', [{ originalImagePath: imagePath }]);
- //var ext = imagePath.substr(imagePath.lastIndexOf('.'));
- //return imagePath.substr(0, imagePath.lastIndexOf('.')) + "_big-thumb" + ".jpg";
- return thumbnailUrl;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#detectIfImageByExtension
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * Returns true/false, indicating if the given path has an allowed image extension
- *
- * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg
- */
- detectIfImageByExtension: function (imagePath) {
- if (!imagePath) {
- return false;
- }
- var lowered = imagePath.toLowerCase();
- var ext = lowered.substr(lowered.lastIndexOf('.') + 1);
- return (',' + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ',').indexOf(',' + ext + ',') !== -1;
- },
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#formatFileTypes
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * Returns a string with correctly formated file types for ng-file-upload
- *
- * @param {string} file types, ex: jpg,png,tiff
- */
- formatFileTypes: function (fileTypes) {
- var fileTypesArray = fileTypes.split(',');
- var newFileTypesArray = [];
- for (var i = 0; i < fileTypesArray.length; i++) {
- var fileType = fileTypesArray[i];
- if (fileType.indexOf('.') !== 0) {
- fileType = '.'.concat(fileType);
- }
- newFileTypesArray.push(fileType);
- }
- return newFileTypesArray.join(',');
- },
- /**
- * @ngdoc function
- * @name umbraco.services.mediaHelper#getFileExtension
- * @methodOf umbraco.services.mediaHelper
- * @function
- *
- * @description
- * Returns file extension
- *
- * @param {string} filePath File path, ex /media/1234/my-image.jpg
- */
- getFileExtension: function (filePath) {
- if (!filePath) {
- return false;
- }
- var lowered = filePath.toLowerCase();
- var ext = lowered.substr(lowered.lastIndexOf('.') + 1);
- return ext;
- }
- };
- }
- angular.module('umbraco.services').factory('mediaHelper', mediaHelper);
- /**
- * @ngdoc service
- * @name umbraco.services.mediaTypeHelper
- * @description A helper service for the media types
- **/
- function mediaTypeHelper(mediaTypeResource, $q) {
- var mediaTypeHelperService = {
- isFolderType: function (mediaEntity) {
- if (!mediaEntity) {
- throw 'mediaEntity is null';
- }
- if (!mediaEntity.contentTypeAlias) {
- throw 'mediaEntity.contentTypeAlias is null';
- }
- //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder"
- //this is the exact same logic that is performed in MediaController.GetChildFolders
- return mediaEntity.contentTypeAlias.endsWith('Folder');
- },
- getAllowedImagetypes: function (mediaId) {
- //TODO: This is horribly inneficient - why make one request per type!?
- //This should make a call to c# to get exactly what it's looking for instead of returning every single media type and doing
- //some filtering on the client side.
- //This is also called multiple times when it's not needed! Example, when launching the media picker, this will be called twice
- //which means we'll be making at least 6 REST calls to fetch each media type
- // Get All allowedTypes
- return mediaTypeResource.getAllowedTypes(mediaId).then(function (types) {
- var allowedQ = types.map(function (type) {
- return mediaTypeResource.getById(type.id);
- });
- // Get full list
- return $q.all(allowedQ).then(function (fullTypes) {
- // Find all the media types with an Image Cropper property editor
- var filteredTypes = mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper']);
- // If there is only one media type with an Image Cropper we will return this one
- if (filteredTypes.length === 1) {
- return filteredTypes; // If there is more than one Image cropper, custom media types have been added, and we return all media types with and Image cropper or UploadField
- } else {
- return mediaTypeHelperService.getTypeWithEditor(fullTypes, [
- 'Umbraco.ImageCropper',
- 'Umbraco.UploadField'
- ]);
- }
- });
- });
- },
- getTypeWithEditor: function (types, editors) {
- return types.filter(function (mediatype) {
- for (var i = 0; i < mediatype.groups.length; i++) {
- var group = mediatype.groups[i];
- for (var j = 0; j < group.properties.length; j++) {
- var property = group.properties[j];
- if (editors.indexOf(property.editor) !== -1) {
- return mediatype;
- }
- }
- }
- });
- }
- };
- return mediaTypeHelperService;
- }
- angular.module('umbraco.services').factory('mediaTypeHelper', mediaTypeHelper);
- /**
- * @ngdoc service
- * @name umbraco.services.umbracoMenuActions
- *
- * @requires q
- * @requires treeService
- *
- * @description
- * Defines the methods that are called when menu items declare only an action to execute
- */
- function umbracoMenuActions($q, treeService, $location, navigationService, appState) {
- return {
- /**
- * @ngdoc method
- * @name umbraco.services.umbracoMenuActions#RefreshNode
- * @methodOf umbraco.services.umbracoMenuActions
- * @function
- *
- * @description
- * Clears all node children and then gets it's up-to-date children from the server and re-assigns them
- * @param {object} args An arguments object
- * @param {object} args.entity The basic entity being acted upon
- * @param {object} args.treeAlias The tree alias associated with this entity
- * @param {object} args.section The current section
- */
- 'RefreshNode': function (args) {
- ////just in case clear any tree cache for this node/section
- //treeService.clearCache({
- // cacheKey: "__" + args.section, //each item in the tree cache is cached by the section name
- // childrenOf: args.entity.parentId //clear the children of the parent
- //});
- //since we're dealing with an entity, we need to attempt to find it's tree node, in the main tree
- // this action is purely a UI thing so if for whatever reason there is no loaded tree node in the UI
- // we can safely ignore this process.
- //to find a visible tree node, we'll go get the currently loaded root node from appState
- var treeRoot = appState.getTreeState('currentRootNode');
- if (treeRoot && treeRoot.root) {
- var treeNode = treeService.getDescendantNode(treeRoot.root, args.entity.id, args.treeAlias);
- if (treeNode) {
- treeService.loadNodeChildren({
- node: treeNode,
- section: args.section
- });
- }
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.umbracoMenuActions#CreateChildEntity
- * @methodOf umbraco.services.umbracoMenuActions
- * @function
- *
- * @description
- * This will re-route to a route for creating a new entity as a child of the current node
- * @param {object} args An arguments object
- * @param {object} args.entity The basic entity being acted upon
- * @param {object} args.treeAlias The tree alias associated with this entity
- * @param {object} args.section The current section
- */
- 'CreateChildEntity': function (args) {
- navigationService.hideNavigation();
- var route = '/' + args.section + '/' + args.treeAlias + '/edit/' + args.entity.id;
- //change to new path
- $location.path(route).search({ create: true });
- }
- };
- }
- angular.module('umbraco.services').factory('umbracoMenuActions', umbracoMenuActions);
- (function () {
- 'use strict';
- function miniEditorHelper(dialogService, editorState, fileManager, contentEditingHelper, $q) {
- var launched = false;
- function launchMiniEditor(node) {
- var deferred = $q.defer();
- 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: node.id,
- closeOnSave: true,
- tabFilter: ['Generic properties'],
- callback: function (data) {
- //set the node name back
- 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;
- deferred.resolve(data);
- },
- 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;
- deferred.reject();
- }
- });
- return deferred.promise;
- }
- var service = { launchMiniEditor: launchMiniEditor };
- return service;
- }
- angular.module('umbraco.services').factory('miniEditorHelper', miniEditorHelper);
- }());
- /**
- * @ngdoc service
- * @name umbraco.services.navigationService
- *
- * @requires $rootScope
- * @requires $routeParams
- * @requires $log
- * @requires $location
- * @requires dialogService
- * @requires treeService
- * @requires sectionResource
- *
- * @description
- * Service to handle the main application navigation. Responsible for invoking the tree
- * Section navigation and search, and maintain their state for the entire application lifetime
- *
- */
- function navigationService($rootScope, $routeParams, $log, $location, $q, $timeout, $injector, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) {
- //used to track the current dialog object
- var currentDialog = null;
- //the main tree event handler, which gets assigned via the setupTreeEvents method
- var mainTreeEventHandler = null;
- //tracks the user profile dialog
- var userDialog = null;
- function setMode(mode) {
- switch (mode) {
- case 'tree':
- appState.setGlobalState('navMode', 'tree');
- appState.setGlobalState('showNavigation', true);
- appState.setMenuState('showMenu', false);
- appState.setMenuState('showMenuDialog', false);
- appState.setGlobalState('stickyNavigation', false);
- appState.setGlobalState('showTray', false);
- //$("#search-form input").focus();
- break;
- case 'menu':
- appState.setGlobalState('navMode', 'menu');
- appState.setGlobalState('showNavigation', true);
- appState.setMenuState('showMenu', true);
- appState.setMenuState('showMenuDialog', false);
- appState.setGlobalState('stickyNavigation', true);
- break;
- case 'dialog':
- appState.setGlobalState('navMode', 'dialog');
- appState.setGlobalState('stickyNavigation', true);
- appState.setGlobalState('showNavigation', true);
- appState.setMenuState('showMenu', false);
- appState.setMenuState('showMenuDialog', true);
- break;
- case 'search':
- appState.setGlobalState('navMode', 'search');
- appState.setGlobalState('stickyNavigation', false);
- appState.setGlobalState('showNavigation', true);
- appState.setMenuState('showMenu', false);
- appState.setSectionState('showSearchResults', true);
- appState.setMenuState('showMenuDialog', false);
- //TODO: This would be much better off in the search field controller listening to appState changes
- $timeout(function () {
- $('#search-field').focus();
- });
- break;
- default:
- appState.setGlobalState('navMode', 'default');
- appState.setMenuState('showMenu', false);
- appState.setMenuState('showMenuDialog', false);
- appState.setSectionState('showSearchResults', false);
- appState.setGlobalState('stickyNavigation', false);
- appState.setGlobalState('showTray', false);
- if (appState.getGlobalState('isTablet') === true) {
- appState.setGlobalState('showNavigation', false);
- }
- break;
- }
- }
- var service = {
- /** initializes the navigation service */
- init: function () {
- //keep track of the current section - initially this will always be undefined so
- // no point in setting it now until it changes.
- $rootScope.$watch(function () {
- return $routeParams.section;
- }, function (newVal, oldVal) {
- appState.setSectionState('currentSection', newVal);
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#load
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Shows the legacy iframe and loads in the content based on the source url
- * @param {String} source The URL to load into the iframe
- */
- loadLegacyIFrame: function (source) {
- $location.path('/' + appState.getSectionState('currentSection') + '/framed/' + encodeURIComponent(source));
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#changeSection
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Changes the active section to a given section alias
- * If the navigation is 'sticky' this will load the associated tree
- * and load the dashboard related to the section
- * @param {string} sectionAlias The alias of the section
- */
- changeSection: function (sectionAlias, force) {
- setMode('default-opensection');
- if (force && appState.getSectionState('currentSection') === sectionAlias) {
- appState.setSectionState('currentSection', '');
- }
- appState.setSectionState('currentSection', sectionAlias);
- this.showTree(sectionAlias);
- $location.path(sectionAlias);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#showTree
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Displays the tree for a given section alias but turning on the containing dom element
- * only changes if the section is different from the current one
- * @param {string} sectionAlias The alias of the section to load
- * @param {Object} syncArgs Optional object of arguments for syncing the tree for the section being shown
- */
- showTree: function (sectionAlias, syncArgs) {
- if (sectionAlias !== appState.getSectionState('currentSection')) {
- appState.setSectionState('currentSection', sectionAlias);
- if (syncArgs) {
- this.syncTree(syncArgs);
- }
- }
- setMode('tree');
- },
- showTray: function () {
- appState.setGlobalState('showTray', true);
- },
- hideTray: function () {
- appState.setGlobalState('showTray', false);
- },
- /**
- Called to assign the main tree event handler - this is called by the navigation controller.
- TODO: Potentially another dev could call this which would kind of mung the whole app so potentially there's a better way.
- */
- setupTreeEvents: function (treeEventHandler) {
- mainTreeEventHandler = treeEventHandler;
- //when a tree is loaded into a section, we need to put it into appState
- mainTreeEventHandler.bind('treeLoaded', function (ev, args) {
- appState.setTreeState('currentRootNode', args.tree);
- });
- //when a tree node is synced this event will fire, this allows us to set the currentNode
- mainTreeEventHandler.bind('treeSynced', function (ev, args) {
- if (args.activate === undefined || args.activate === true) {
- //set the current selected node
- appState.setTreeState('selectedNode', args.node);
- //when a node is activated, this is the same as clicking it and we need to set the
- //current menu item to be this node as well.
- appState.setMenuState('currentNode', args.node);
- }
- });
- //this reacts to the options item in the tree
- mainTreeEventHandler.bind('treeOptionsClick', function (ev, args) {
- ev.stopPropagation();
- ev.preventDefault();
- //Set the current action node (this is not the same as the current selected node!)
- appState.setMenuState('currentNode', args.node);
- if (args.event && args.event.altKey) {
- args.skipDefault = true;
- }
- service.showMenu(ev, args);
- });
- mainTreeEventHandler.bind('treeNodeAltSelect', function (ev, args) {
- ev.stopPropagation();
- ev.preventDefault();
- args.skipDefault = true;
- service.showMenu(ev, args);
- });
- //this reacts to tree items themselves being clicked
- //the tree directive should not contain any handling, simply just bubble events
- mainTreeEventHandler.bind('treeNodeSelect', function (ev, args) {
- var n = args.node;
- ev.stopPropagation();
- ev.preventDefault();
- if (n.metaData && n.metaData['jsClickCallback'] && angular.isString(n.metaData['jsClickCallback']) && n.metaData['jsClickCallback'] !== '') {
- //this is a legacy tree node!
- var jsPrefix = 'javascript:';
- var js;
- if (n.metaData['jsClickCallback'].startsWith(jsPrefix)) {
- js = n.metaData['jsClickCallback'].substr(jsPrefix.length);
- } else {
- js = n.metaData['jsClickCallback'];
- }
- try {
- var func = eval(js);
- //this is normally not necessary since the eval above should execute the method and will return nothing.
- if (func != null && typeof func === 'function') {
- func.call();
- }
- } catch (ex) {
- $log.error('Error evaluating js callback from legacy tree node: ' + ex);
- }
- } else if (n.routePath) {
- //add action to the history service
- historyService.add({
- name: n.name,
- link: n.routePath,
- icon: n.icon
- });
- //put this node into the tree state
- appState.setTreeState('selectedNode', args.node);
- //when a node is clicked we also need to set the active menu node to this node
- appState.setMenuState('currentNode', args.node);
- //not legacy, lets just set the route value and clear the query string if there is one.
- $location.path(n.routePath).search('');
- } else if (args.element.section) {
- $location.path(args.element.section).search('');
- }
- service.hideNavigation();
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#syncTree
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Syncs a tree with a given path, returns a promise
- * The path format is: ["itemId","itemId"], and so on
- * so to sync to a specific document type node do:
- * <pre>
- * navigationService.syncTree({tree: 'content', path: ["-1","123d"], forceReload: true});
- * </pre>
- * @param {Object} args arguments passed to the function
- * @param {String} args.tree the tree alias to sync to
- * @param {Array} args.path the path to sync the tree to
- * @param {Boolean} args.forceReload optional, specifies whether to force reload the node data from the server even if it already exists in the tree currently
- * @param {Boolean} args.activate optional, specifies whether to set the synced node to be the active node, this will default to true if not specified
- */
- syncTree: function (args) {
- if (!args) {
- throw 'args cannot be null';
- }
- if (!args.path) {
- throw 'args.path cannot be null';
- }
- if (!args.tree) {
- throw 'args.tree cannot be null';
- }
- if (mainTreeEventHandler) {
- //returns a promise
- return mainTreeEventHandler.syncTree(args);
- }
- //couldn't sync
- return angularHelper.rejectedPromise();
- },
- /**
- 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
- */
- _syncPath: function (path, forceReload) {
- if (mainTreeEventHandler) {
- mainTreeEventHandler.syncTree({
- path: path,
- forceReload: forceReload
- });
- }
- },
- //TODO: This should return a promise
- reloadNode: function (node) {
- if (mainTreeEventHandler) {
- mainTreeEventHandler.reloadNode(node);
- }
- },
- //TODO: This should return a promise
- reloadSection: function (sectionAlias) {
- if (mainTreeEventHandler) {
- mainTreeEventHandler.clearCache({ section: sectionAlias });
- mainTreeEventHandler.load(sectionAlias);
- }
- },
- /**
- 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 syncTreePath
- */
- _setActiveTreeType: function (treeAlias, loadChildren) {
- if (mainTreeEventHandler) {
- mainTreeEventHandler._setActiveTreeType(treeAlias, loadChildren);
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#hideTree
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Hides the tree by hiding the containing dom element
- */
- hideTree: function () {
- if (appState.getGlobalState('isTablet') === true && !appState.getGlobalState('stickyNavigation')) {
- //reset it to whatever is in the url
- appState.setSectionState('currentSection', $routeParams.section);
- setMode('default-hidesectiontree');
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#showMenu
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Hides the tree by hiding the containing dom element.
- * This always returns a promise!
- *
- * @param {Event} event the click event triggering the method, passed from the DOM element
- */
- showMenu: function (event, args) {
- var deferred = $q.defer();
- var self = this;
- treeService.getMenu({ treeNode: args.node }).then(function (data) {
- //check for a default
- //NOTE: event will be undefined when a call to hideDialog is made so it won't re-load the default again.
- // but perhaps there's a better way to deal with with an additional parameter in the args ? it works though.
- if (data.defaultAlias && !args.skipDefault) {
- var found = _.find(data.menuItems, function (item) {
- return item.alias = data.defaultAlias;
- });
- if (found) {
- //NOTE: This is assigning the current action node - this is not the same as the currently selected node!
- appState.setMenuState('currentNode', args.node);
- //ensure the current dialog is cleared before creating another!
- if (currentDialog) {
- dialogService.close(currentDialog);
- }
- var dialog = self.showDialog({
- node: args.node,
- action: found,
- section: appState.getSectionState('currentSection')
- });
- //return the dialog this is opening.
- deferred.resolve(dialog);
- return;
- }
- }
- //there is no default or we couldn't find one so just continue showing the menu
- setMode('menu');
- appState.setMenuState('currentNode', args.node);
- appState.setMenuState('menuActions', data.menuItems);
- appState.setMenuState('dialogTitle', args.node.name);
- //we're not opening a dialog, return null.
- deferred.resolve(null);
- });
- return deferred.promise;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#hideMenu
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Hides the menu by hiding the containing dom element
- */
- hideMenu: function () {
- //SD: Would we ever want to access the last action'd node instead of clearing it here?
- appState.setMenuState('currentNode', null);
- appState.setMenuState('menuActions', []);
- setMode('tree');
- },
- /** Executes a given menu action */
- executeMenuAction: function (action, node, section) {
- if (!action) {
- throw 'action cannot be null';
- }
- if (!node) {
- throw 'node cannot be null';
- }
- if (!section) {
- throw 'section cannot be null';
- }
- if (action.metaData && action.metaData['actionRoute'] && angular.isString(action.metaData['actionRoute'])) {
- //first check if the menu item simply navigates to a route
- var parts = action.metaData['actionRoute'].split('?');
- $location.path(parts[0]).search(parts.length > 1 ? parts[1] : '');
- this.hideNavigation();
- return;
- } else if (action.metaData && action.metaData['jsAction'] && angular.isString(action.metaData['jsAction'])) {
- //we'll try to get the jsAction from the injector
- var menuAction = action.metaData['jsAction'].split('.');
- if (menuAction.length !== 2) {
- //if it is not two parts long then this most likely means that it's a legacy action
- var js = action.metaData['jsAction'].replace('javascript:', '');
- //there's not really a different way to acheive this except for eval
- eval(js);
- } else {
- var menuActionService = $injector.get(menuAction[0]);
- if (!menuActionService) {
- throw 'The angular service ' + menuAction[0] + ' could not be found';
- }
- var method = menuActionService[menuAction[1]];
- if (!method) {
- throw 'The method ' + menuAction[1] + ' on the angular service ' + menuAction[0] + ' could not be found';
- }
- method.apply(this, [{
- //map our content object to a basic entity to pass in to the menu handlers,
- //this is required for consistency since a menu item needs to be decoupled from a tree node since the menu can
- //exist standalone in the editor for which it can only pass in an entity (not tree node).
- entity: umbModelMapper.convertToEntityBasic(node),
- action: action,
- section: section,
- treeAlias: treeService.getTreeAlias(node)
- }]);
- }
- } else {
- service.showDialog({
- node: node,
- action: action,
- section: section
- });
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#showUserDialog
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Opens the user dialog, next to the sections navigation
- * template is located in views/common/dialogs/user.html
- */
- showUserDialog: function () {
- // hide tray and close help dialog
- if (service.helpDialog) {
- service.helpDialog.close();
- }
- service.hideTray();
- if (service.userDialog) {
- service.userDialog.close();
- service.userDialog = undefined;
- }
- service.userDialog = dialogService.open({
- template: 'views/common/dialogs/user.html',
- modalClass: 'umb-modal-left',
- show: true
- });
- return service.userDialog;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#showUserDialog
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Opens the user dialog, next to the sections navigation
- * template is located in views/common/dialogs/user.html
- */
- showHelpDialog: function () {
- // hide tray and close user dialog
- service.hideTray();
- if (service.userDialog) {
- service.userDialog.close();
- }
- if (service.helpDialog) {
- service.helpDialog.close();
- service.helpDialog = undefined;
- }
- service.helpDialog = dialogService.open({
- template: 'views/common/dialogs/help.html',
- modalClass: 'umb-modal-left',
- show: true
- });
- return service.helpDialog;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#showDialog
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * Opens a dialog, for a given action on a given tree node
- * uses the dialogService to inject the selected action dialog
- * into #dialog div.umb-panel-body
- * the path to the dialog view is determined by:
- * "views/" + current tree + "/" + action alias + ".html"
- * The dialog controller will get passed a scope object that is created here with the properties:
- * scope.currentNode = the selected tree node
- * scope.currentAction = the selected menu item
- * so that the dialog controllers can use these properties
- *
- * @param {Object} args arguments passed to the function
- * @param {Scope} args.scope current scope passed to the dialog
- * @param {Object} args.action the clicked action containing `name` and `alias`
- */
- showDialog: function (args) {
- if (!args) {
- throw 'showDialog is missing the args parameter';
- }
- if (!args.action) {
- throw 'The args parameter must have an \'action\' property as the clicked menu action object';
- }
- if (!args.node) {
- throw 'The args parameter must have a \'node\' as the active tree node';
- }
- //ensure the current dialog is cleared before creating another!
- if (currentDialog) {
- dialogService.close(currentDialog);
- currentDialog = null;
- }
- setMode('dialog');
- //NOTE: Set up the scope object and assign properties, this is legacy functionality but we have to live with it now.
- // we should be passing in currentNode and currentAction using 'dialogData' for the dialog, not attaching it to a scope.
- // This scope instance will be destroyed by the dialog so it cannot be a scope that exists outside of the dialog.
- // If a scope instance has been passed in, we'll have to create a child scope of it, otherwise a new root scope.
- var dialogScope = args.scope ? args.scope.$new() : $rootScope.$new();
- dialogScope.currentNode = args.node;
- dialogScope.currentAction = args.action;
- //the title might be in the meta data, check there first
- if (args.action.metaData['dialogTitle']) {
- appState.setMenuState('dialogTitle', args.action.metaData['dialogTitle']);
- } else {
- appState.setMenuState('dialogTitle', args.action.name);
- }
- var templateUrl;
- var iframe;
- if (args.action.metaData['actionUrl']) {
- templateUrl = args.action.metaData['actionUrl'];
- iframe = true;
- } else if (args.action.metaData['actionView']) {
- templateUrl = args.action.metaData['actionView'];
- iframe = false;
- } else {
- //by convention we will look into the /views/{treetype}/{action}.html
- // for example: /views/content/create.html
- //we will also check for a 'packageName' for the current tree, if it exists then the convention will be:
- // for example: /App_Plugins/{mypackage}/backoffice/{treetype}/create.html
- var treeAlias = treeService.getTreeAlias(args.node);
- var packageTreeFolder = treeService.getTreePackageFolder(treeAlias);
- if (!treeAlias) {
- throw 'Could not get tree alias for node ' + args.node.id;
- }
- if (packageTreeFolder) {
- templateUrl = Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + '/' + packageTreeFolder + '/backoffice/' + treeAlias + '/' + args.action.alias + '.html';
- } else {
- templateUrl = 'views/' + treeAlias + '/' + args.action.alias + '.html';
- }
- iframe = false;
- }
- //TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with
- // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog
- // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window,
- // though would be v-easy, just not sure we want to ever support that?
- var dialog = dialogService.open({
- container: $('#dialog div.umb-modalcolumn-body'),
- //The ONLY reason we're passing in scope to the dialogService (which is legacy functionality) is
- // for backwards compatibility since many dialogs require $scope.currentNode or $scope.currentAction
- // to exist
- scope: dialogScope,
- inline: true,
- show: true,
- iframe: iframe,
- modalClass: 'umb-dialog',
- template: templateUrl,
- //These will show up on the dialog controller's $scope under dialogOptions
- currentNode: args.node,
- currentAction: args.action
- });
- //save the currently assigned dialog so it can be removed before a new one is created
- currentDialog = dialog;
- return dialog;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#hideDialog
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * hides the currently open dialog
- */
- hideDialog: function (showMenu) {
- setMode('default');
- if (showMenu) {
- this.showMenu(undefined, {
- skipDefault: true,
- node: appState.getMenuState('currentNode')
- });
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#showSearch
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * shows the search pane
- */
- showSearch: function () {
- setMode('search');
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#hideSearch
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * hides the search pane
- */
- hideSearch: function () {
- setMode('default-hidesearch');
- },
- /**
- * @ngdoc method
- * @name umbraco.services.navigationService#hideNavigation
- * @methodOf umbraco.services.navigationService
- *
- * @description
- * hides any open navigation panes and resets the tree, actions and the currently selected node
- */
- hideNavigation: function () {
- appState.setMenuState('menuActions', []);
- setMode('default');
- }
- };
- return service;
- }
- angular.module('umbraco.services').factory('navigationService', navigationService);
- /**
- * @ngdoc service
- * @name umbraco.services.notificationsService
- *
- * @requires $rootScope
- * @requires $timeout
- * @requires angularHelper
- *
- * @description
- * Application-wide service for handling notifications, the umbraco application
- * maintains a single collection of notications, which the UI watches for changes.
- * By default when a notication is added, it is automaticly removed 7 seconds after
- * This can be changed on add()
- *
- * ##usage
- * To use, simply inject the notificationsService into any controller that needs it, and make
- * sure the umbraco.services module is accesible - which it should be by default.
- *
- * <pre>
- * notificationsService.success("Document Published", "hooraaaay for you!");
- * notificationsService.error("Document Failed", "booooh");
- * </pre>
- */
- angular.module('umbraco.services').factory('notificationsService', function ($rootScope, $timeout, angularHelper) {
- var nArray = [];
- function setViewPath(view) {
- if (view.indexOf('/') < 0) {
- view = 'views/common/notifications/' + view;
- }
- if (view.indexOf('.html') < 0) {
- view = view + '.html';
- }
- return view;
- }
- var service = {
- /**
- * @ngdoc method
- * @name umbraco.services.notificationsService#add
- * @methodOf umbraco.services.notificationsService
- *
- * @description
- * Lower level api for adding notifcations, support more advanced options
- * @param {Object} item The notification item
- * @param {String} item.headline Short headline
- * @param {String} item.message longer text for the notication, trimmed after 200 characters, which can then be exanded
- * @param {String} item.type Notification type, can be: "success","warning","error" or "info"
- * @param {String} item.url url to open when notification is clicked
- * @param {String} item.view path to custom view to load into the notification box
- * @param {Array} item.actions Collection of button actions to append (label, func, cssClass)
- * @param {Boolean} item.sticky if set to true, the notification will not auto-close
- * @returns {Object} args notification object
- */
- add: function (item) {
- angularHelper.safeApply($rootScope, function () {
- if (item.view) {
- item.view = setViewPath(item.view);
- item.sticky = true;
- item.type = 'form';
- item.headline = null;
- }
- //add a colon after the headline if there is a message as well
- if (item.message) {
- item.headline += ': ';
- if (item.message.length > 200) {
- item.sticky = true;
- }
- }
- //we need to ID the item, going by index isn't good enough because people can remove at different indexes
- // whenever they want. Plus once we remove one, then the next index will be different. The only way to
- // effectively remove an item is by an Id.
- item.id = String.CreateGuid();
- nArray.push(item);
- if (!item.sticky) {
- $timeout(function () {
- var found = _.find(nArray, function (i) {
- return i.id === item.id;
- });
- if (found) {
- var index = nArray.indexOf(found);
- nArray.splice(index, 1);
- }
- }, 7000);
- }
- return item;
- });
- },
- hasView: function (view) {
- if (!view) {
- return _.find(nArray, function (notification) {
- return notification.view;
- });
- } else {
- view = setViewPath(view).toLowerCase();
- return _.find(nArray, function (notification) {
- return notification.view.toLowerCase() === view;
- });
- }
- },
- addView: function (view, args) {
- var item = {
- args: args,
- view: view
- };
- service.add(item);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.notificationsService#showNotification
- * @methodOf umbraco.services.notificationsService
- *
- * @description
- * Shows a notification based on the object passed in, normally used to render notifications sent back from the server
- *
- * @returns {Object} args notification object
- */
- showNotification: function (args) {
- if (!args) {
- throw 'args cannot be null';
- }
- if (args.type === undefined || args.type === null) {
- throw 'args.type cannot be null';
- }
- if (!args.header) {
- throw 'args.header cannot be null';
- }
- switch (args.type) {
- case 0:
- //save
- this.success(args.header, args.message);
- break;
- case 1:
- //info
- this.success(args.header, args.message);
- break;
- case 2:
- //error
- this.error(args.header, args.message);
- break;
- case 3:
- //success
- this.success(args.header, args.message);
- break;
- case 4:
- //warning
- this.warning(args.header, args.message);
- break;
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.notificationsService#success
- * @methodOf umbraco.services.notificationsService
- *
- * @description
- * Adds a green success notication to the notications collection
- * This should be used when an operations *completes* without errors
- *
- * @param {String} headline Headline of the notification
- * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded
- * @returns {Object} notification object
- */
- success: function (headline, message) {
- return service.add({
- headline: headline,
- message: message,
- type: 'success',
- time: new Date()
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.notificationsService#error
- * @methodOf umbraco.services.notificationsService
- *
- * @description
- * Adds a red error notication to the notications collection
- * This should be used when an operations *fails* and could not complete
- *
- * @param {String} headline Headline of the notification
- * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded
- * @returns {Object} notification object
- */
- error: function (headline, message) {
- return service.add({
- headline: headline,
- message: message,
- type: 'error',
- time: new Date()
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.notificationsService#warning
- * @methodOf umbraco.services.notificationsService
- *
- * @description
- * Adds a yellow warning notication to the notications collection
- * This should be used when an operations *completes* but something was not as expected
- *
- *
- * @param {String} headline Headline of the notification
- * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded
- * @returns {Object} notification object
- */
- warning: function (headline, message) {
- return service.add({
- headline: headline,
- message: message,
- type: 'warning',
- time: new Date()
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.notificationsService#warning
- * @methodOf umbraco.services.notificationsService
- *
- * @description
- * Adds a yellow warning notication to the notications collection
- * This should be used when an operations *completes* but something was not as expected
- *
- *
- * @param {String} headline Headline of the notification
- * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded
- * @returns {Object} notification object
- */
- info: function (headline, message) {
- return service.add({
- headline: headline,
- message: message,
- type: 'info',
- time: new Date()
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.notificationsService#remove
- * @methodOf umbraco.services.notificationsService
- *
- * @description
- * Removes a notification from the notifcations collection at a given index
- *
- * @param {Int} index index where the notication should be removed from
- */
- remove: function (index) {
- if (angular.isObject(index)) {
- var i = nArray.indexOf(index);
- angularHelper.safeApply($rootScope, function () {
- nArray.splice(i, 1);
- });
- } else {
- angularHelper.safeApply($rootScope, function () {
- nArray.splice(index, 1);
- });
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.notificationsService#removeAll
- * @methodOf umbraco.services.notificationsService
- *
- * @description
- * Removes all notifications from the notifcations collection
- */
- removeAll: function () {
- angularHelper.safeApply($rootScope, function () {
- nArray = [];
- });
- },
- /**
- * @ngdoc property
- * @name umbraco.services.notificationsService#current
- * @propertyOf umbraco.services.notificationsService
- *
- * @description
- * Returns an array of current notifications to display
- *
- * @returns {string} returns an array
- */
- current: nArray,
- /**
- * @ngdoc method
- * @name umbraco.services.notificationsService#getCurrent
- * @methodOf umbraco.services.notificationsService
- *
- * @description
- * Method to return all notifications from the notifcations collection
- */
- getCurrent: function () {
- return nArray;
- }
- };
- return service;
- });
- (function () {
- 'use strict';
- function overlayHelper() {
- var numberOfOverlays = 0;
- function registerOverlay() {
- numberOfOverlays++;
- return numberOfOverlays;
- }
- function unregisterOverlay() {
- numberOfOverlays--;
- return numberOfOverlays;
- }
- function getNumberOfOverlays() {
- return numberOfOverlays;
- }
- var service = {
- numberOfOverlays: numberOfOverlays,
- registerOverlay: registerOverlay,
- unregisterOverlay: unregisterOverlay,
- getNumberOfOverlays: getNumberOfOverlays
- };
- return service;
- }
- angular.module('umbraco.services').factory('overlayHelper', overlayHelper);
- }());
- (function () {
- 'use strict';
- function platformService() {
- function isMac() {
- return navigator.platform.toUpperCase().indexOf('MAC') >= 0;
- }
- ////////////
- var service = { isMac: isMac };
- return service;
- }
- angular.module('umbraco.services').factory('platformService', platformService);
- }());
- /**
- * @ngdoc service
- * @name umbraco.services.searchService
- *
- *
- * @description
- * Service for handling the main application search, can currently search content, media and members
- *
- * ##usage
- * To use, simply inject the searchService into any controller that needs it, and make
- * sure the umbraco.services module is accesible - which it should be by default.
- *
- * <pre>
- * searchService.searchMembers({term: 'bob'}).then(function(results){
- * angular.forEach(results, function(result){
- * //returns:
- * {name: "name", id: 1234, menuUrl: "url", editorPath: "url", metaData: {}, subtitle: "/path/etc" }
- * })
- * var result =
- * })
- * </pre>
- */
- angular.module('umbraco.services').factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper, $injector, searchResultFormatter) {
- return {
- /**
- * @ngdoc method
- * @name umbraco.services.searchService#searchMembers
- * @methodOf umbraco.services.searchService
- *
- * @description
- * Searches the default member search index
- * @param {Object} args argument object
- * @param {String} args.term seach term
- * @returns {Promise} returns promise containing all matching members
- */
- searchMembers: function (args) {
- if (!args.term) {
- throw 'args.term is required';
- }
- return entityResource.search(args.term, 'Member', args.searchFrom).then(function (data) {
- _.each(data, function (item) {
- searchResultFormatter.configureMemberResult(item);
- });
- return data;
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.searchService#searchContent
- * @methodOf umbraco.services.searchService
- *
- * @description
- * Searches the default internal content search index
- * @param {Object} args argument object
- * @param {String} args.term seach term
- * @returns {Promise} returns promise containing all matching content items
- */
- searchContent: function (args) {
- if (!args.term) {
- throw 'args.term is required';
- }
- return entityResource.search(args.term, 'Document', args.searchFrom, args.canceler).then(function (data) {
- _.each(data, function (item) {
- searchResultFormatter.configureContentResult(item);
- });
- return data;
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.searchService#searchMedia
- * @methodOf umbraco.services.searchService
- *
- * @description
- * Searches the default media search index
- * @param {Object} args argument object
- * @param {String} args.term seach term
- * @returns {Promise} returns promise containing all matching media items
- */
- searchMedia: function (args) {
- if (!args.term) {
- throw 'args.term is required';
- }
- return entityResource.search(args.term, 'Media', args.searchFrom).then(function (data) {
- _.each(data, function (item) {
- searchResultFormatter.configureMediaResult(item);
- });
- return data;
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.searchService#searchAll
- * @methodOf umbraco.services.searchService
- *
- * @description
- * Searches all available indexes and returns all results in one collection
- * @param {Object} args argument object
- * @param {String} args.term seach term
- * @returns {Promise} returns promise containing all matching items
- */
- searchAll: function (args) {
- if (!args.term) {
- throw 'args.term is required';
- }
- return entityResource.searchAll(args.term, args.canceler).then(function (data) {
- _.each(data, function (resultByType) {
- //we need to format the search result data to include things like the subtitle, urls, etc...
- // this is done with registered angular services as part of the SearchableTreeAttribute, if that
- // is not found, than we format with the default formatter
- var formatterMethod = searchResultFormatter.configureDefaultResult;
- //check if a custom formatter is specified...
- if (resultByType.jsSvc) {
- var searchFormatterService = $injector.get(resultByType.jsSvc);
- if (searchFormatterService) {
- if (!resultByType.jsMethod) {
- resultByType.jsMethod = 'format';
- }
- formatterMethod = searchFormatterService[resultByType.jsMethod];
- if (!formatterMethod) {
- throw 'The method ' + resultByType.jsMethod + ' on the angular service ' + resultByType.jsSvc + ' could not be found';
- }
- }
- }
- //now apply the formatter for each result
- _.each(resultByType.results, function (item) {
- formatterMethod.apply(this, [
- item,
- resultByType.treeAlias,
- resultByType.appAlias
- ]);
- });
- });
- return data;
- });
- },
- //TODO: This doesn't do anything!
- setCurrent: function (sectionAlias) {
- var currentSection = sectionAlias;
- }
- };
- });
- function searchResultFormatter(umbRequestHelper) {
- function configureDefaultResult(content, treeAlias, appAlias) {
- content.editorPath = appAlias + '/' + treeAlias + '/edit/' + content.id;
- angular.extend(content.metaData, { treeAlias: treeAlias });
- }
- function configureContentResult(content, treeAlias, appAlias) {
- content.menuUrl = umbRequestHelper.getApiUrl('contentTreeBaseUrl', 'GetMenu', [
- { id: content.id },
- { application: appAlias }
- ]);
- content.editorPath = appAlias + '/' + treeAlias + '/edit/' + content.id;
- angular.extend(content.metaData, { treeAlias: treeAlias });
- content.subTitle = content.metaData.Url;
- }
- function configureMemberResult(member, treeAlias, appAlias) {
- member.menuUrl = umbRequestHelper.getApiUrl('memberTreeBaseUrl', 'GetMenu', [
- { id: member.id },
- { application: appAlias }
- ]);
- member.editorPath = appAlias + '/' + treeAlias + '/edit/' + (member.key ? member.key : member.id);
- angular.extend(member.metaData, { treeAlias: treeAlias });
- member.subTitle = member.metaData.Email;
- }
- function configureMediaResult(media, treeAlias, appAlias) {
- media.menuUrl = umbRequestHelper.getApiUrl('mediaTreeBaseUrl', 'GetMenu', [
- { id: media.id },
- { application: appAlias }
- ]);
- media.editorPath = appAlias + '/' + treeAlias + '/edit/' + media.id;
- angular.extend(media.metaData, { treeAlias: treeAlias });
- }
- return {
- configureContentResult: configureContentResult,
- configureMemberResult: configureMemberResult,
- configureMediaResult: configureMediaResult,
- configureDefaultResult: configureDefaultResult
- };
- }
- angular.module('umbraco.services').factory('searchResultFormatter', searchResultFormatter);
- /**
- * @ngdoc service
- * @name umbraco.services.sectionService
- *
- *
- * @description
- * A service to return the sections (applications) to be listed in the navigation which are contextual to the current user
- */
- (function () {
- 'use strict';
- function sectionService(userService, $q, sectionResource) {
- function getSectionsForUser() {
- var deferred = $q.defer();
- userService.getCurrentUser().then(function (u) {
- //if they've already loaded, return them
- if (u.sections) {
- deferred.resolve(u.sections);
- } else {
- sectionResource.getSections().then(function (sections) {
- //set these to the user (cached), then the user changes, these will be wiped
- u.sections = sections;
- deferred.resolve(u.sections);
- });
- }
- });
- return deferred.promise;
- }
- var service = { getSectionsForUser: getSectionsForUser };
- return service;
- }
- angular.module('umbraco.services').factory('sectionService', sectionService);
- }());
- /**
- * @ngdoc service
- * @name umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Used to handle server side validation and wires up the UI with the messages. There are 2 types of validation messages, one
- * is for user defined properties (called Properties) and the other is for field properties which are attached to the native
- * model objects (not user defined). The methods below are named according to these rules: Properties vs Fields.
- */
- function serverValidationManager($timeout) {
- var callbacks = [];
- /** calls the callback specified with the errors specified, used internally */
- function executeCallback(self, errorsForCallback, callback) {
- callback.apply(self, [
- false,
- //pass in a value indicating it is invalid
- errorsForCallback,
- //pass in the errors for this item
- self.items
- ]); //pass in all errors in total
- }
- function getFieldErrors(self, fieldName) {
- if (!angular.isString(fieldName)) {
- throw 'fieldName must be a string';
- }
- //find errors for this field name
- return _.filter(self.items, function (item) {
- return item.propertyAlias === null && item.fieldName === fieldName;
- });
- }
- function getPropertyErrors(self, propertyAlias, fieldName) {
- if (!angular.isString(propertyAlias)) {
- throw 'propertyAlias must be a string';
- }
- if (fieldName && !angular.isString(fieldName)) {
- throw 'fieldName must be a string';
- }
- //find all errors for this property
- return _.filter(self.items, function (item) {
- return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ''));
- });
- }
- return {
- /**
- * @ngdoc function
- * @name umbraco.services.serverValidationManager#subscribe
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * This method needs to be called once all field and property errors are wired up.
- *
- * In some scenarios where the error collection needs to be persisted over a route change
- * (i.e. when a content item (or any item) is created and the route redirects to the editor)
- * the controller should call this method once the data is bound to the scope
- * so that any persisted validation errors are re-bound to their controls. Once they are re-binded this then clears the validation
- * colleciton so that if another route change occurs, the previously persisted validation errors are not re-bound to the new item.
- */
- executeAndClearAllSubscriptions: function () {
- var self = this;
- $timeout(function () {
- for (var cb in callbacks) {
- if (callbacks[cb].propertyAlias === null) {
- //its a field error callback
- var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName);
- if (fieldErrors.length > 0) {
- executeCallback(self, fieldErrors, callbacks[cb].callback);
- }
- } else {
- //its a property error
- var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].fieldName);
- if (propErrors.length > 0) {
- executeCallback(self, propErrors, callbacks[cb].callback);
- }
- }
- }
- //now that they are all executed, we're gonna clear all of the errors we have
- self.clear();
- });
- },
- /**
- * @ngdoc function
- * @name umbraco.services.serverValidationManager#subscribe
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Adds a callback method that is executed whenever validation changes for the field name + property specified.
- * This is generally used for server side validation in order to match up a server side validation error with
- * a particular field, otherwise we can only pinpoint that there is an error for a content property, not the
- * property's specific field. This is used with the val-server directive in which the directive specifies the
- * field alias to listen for.
- * If propertyAlias is null, then this subscription is for a field property (not a user defined property).
- */
- subscribe: function (propertyAlias, fieldName, callback) {
- if (!callback) {
- return;
- }
- if (propertyAlias === null) {
- //don't add it if it already exists
- var exists1 = _.find(callbacks, function (item) {
- return item.propertyAlias === null && item.fieldName === fieldName;
- });
- if (!exists1) {
- callbacks.push({
- propertyAlias: null,
- fieldName: fieldName,
- callback: callback
- });
- }
- } else if (propertyAlias !== undefined) {
- //don't add it if it already exists
- var exists2 = _.find(callbacks, function (item) {
- return item.propertyAlias === propertyAlias && item.fieldName === fieldName;
- });
- if (!exists2) {
- callbacks.push({
- propertyAlias: propertyAlias,
- fieldName: fieldName,
- callback: callback
- });
- }
- }
- },
- unsubscribe: function (propertyAlias, fieldName) {
- if (propertyAlias === null) {
- //remove all callbacks for the content field
- callbacks = _.reject(callbacks, function (item) {
- return item.propertyAlias === null && item.fieldName === fieldName;
- });
- } else if (propertyAlias !== undefined) {
- //remove all callbacks for the content property
- callbacks = _.reject(callbacks, function (item) {
- return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === '') && (fieldName === undefined || fieldName === ''));
- });
- }
- },
- /**
- * @ngdoc function
- * @name getPropertyCallbacks
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Gets all callbacks that has been registered using the subscribe method for the propertyAlias + fieldName combo.
- * This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an
- * explicit field name set.
- */
- getPropertyCallbacks: function (propertyAlias, fieldName) {
- var found = _.filter(callbacks, function (item) {
- //returns any callback that have been registered directly against the field and for only the property
- return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ''));
- });
- return found;
- },
- /**
- * @ngdoc function
- * @name getFieldCallbacks
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Gets all callbacks that has been registered using the subscribe method for the field.
- */
- getFieldCallbacks: function (fieldName) {
- var found = _.filter(callbacks, function (item) {
- //returns any callback that have been registered directly against the field
- return item.propertyAlias === null && item.fieldName === fieldName;
- });
- return found;
- },
- /**
- * @ngdoc function
- * @name addFieldError
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Adds an error message for a native content item field (not a user defined property, for Example, 'Name')
- */
- addFieldError: function (fieldName, errorMsg) {
- if (!fieldName) {
- return;
- }
- //only add the item if it doesn't exist
- if (!this.hasFieldError(fieldName)) {
- this.items.push({
- propertyAlias: null,
- fieldName: fieldName,
- errorMsg: errorMsg
- });
- }
- //find all errors for this item
- var errorsForCallback = getFieldErrors(this, fieldName);
- //we should now call all of the call backs registered for this error
- var cbs = this.getFieldCallbacks(fieldName);
- //call each callback for this error
- for (var cb in cbs) {
- executeCallback(this, errorsForCallback, cbs[cb].callback);
- }
- },
- /**
- * @ngdoc function
- * @name addPropertyError
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Adds an error message for the content property
- */
- addPropertyError: function (propertyAlias, fieldName, errorMsg) {
- if (!propertyAlias) {
- return;
- }
- //only add the item if it doesn't exist
- if (!this.hasPropertyError(propertyAlias, fieldName)) {
- this.items.push({
- propertyAlias: propertyAlias,
- fieldName: fieldName,
- errorMsg: errorMsg
- });
- }
- //find all errors for this item
- var errorsForCallback = getPropertyErrors(this, propertyAlias, fieldName);
- //we should now call all of the call backs registered for this error
- var cbs = this.getPropertyCallbacks(propertyAlias, fieldName);
- //call each callback for this error
- for (var cb in cbs) {
- executeCallback(this, errorsForCallback, cbs[cb].callback);
- }
- },
- /**
- * @ngdoc function
- * @name removePropertyError
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Removes an error message for the content property
- */
- removePropertyError: function (propertyAlias, fieldName) {
- if (!propertyAlias) {
- return;
- }
- //remove the item
- this.items = _.reject(this.items, function (item) {
- return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ''));
- });
- },
- /**
- * @ngdoc function
- * @name reset
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Clears all errors and notifies all callbacks that all server errros are now valid - used when submitting a form
- */
- reset: function () {
- this.clear();
- for (var cb in callbacks) {
- callbacks[cb].callback.apply(this, [
- true,
- //pass in a value indicating it is VALID
- [],
- //pass in empty collection
- []
- ]); //pass in empty collection
- }
- },
- /**
- * @ngdoc function
- * @name clear
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Clears all errors
- */
- clear: function () {
- this.items = [];
- },
- /**
- * @ngdoc function
- * @name getPropertyError
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Gets the error message for the content property
- */
- getPropertyError: function (propertyAlias, fieldName) {
- var err = _.find(this.items, function (item) {
- //return true if the property alias matches and if an empty field name is specified or the field name matches
- return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ''));
- });
- return err;
- },
- /**
- * @ngdoc function
- * @name getFieldError
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Gets the error message for a content field
- */
- getFieldError: function (fieldName) {
- var err = _.find(this.items, function (item) {
- //return true if the property alias matches and if an empty field name is specified or the field name matches
- return item.propertyAlias === null && item.fieldName === fieldName;
- });
- return err;
- },
- /**
- * @ngdoc function
- * @name hasPropertyError
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Checks if the content property + field name combo has an error
- */
- hasPropertyError: function (propertyAlias, fieldName) {
- var err = _.find(this.items, function (item) {
- //return true if the property alias matches and if an empty field name is specified or the field name matches
- return item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ''));
- });
- return err ? true : false;
- },
- /**
- * @ngdoc function
- * @name hasFieldError
- * @methodOf umbraco.services.serverValidationManager
- * @function
- *
- * @description
- * Checks if a content field has an error
- */
- hasFieldError: function (fieldName) {
- var err = _.find(this.items, function (item) {
- //return true if the property alias matches and if an empty field name is specified or the field name matches
- return item.propertyAlias === null && item.fieldName === fieldName;
- });
- return err ? true : false;
- },
- /** The array of error messages */
- items: []
- };
- }
- angular.module('umbraco.services').factory('serverValidationManager', serverValidationManager);
- (function () {
- 'use strict';
- function templateHelperService(localizationService) {
- //crappy hack due to dictionary items not in umbracoNode table
- function getInsertDictionarySnippet(nodeName) {
- return '@Umbraco.GetDictionaryValue("' + nodeName + '")';
- }
- function getInsertPartialSnippet(parentId, nodeName) {
- var partialViewName = nodeName.replace('.cshtml', '');
- if (parentId) {
- partialViewName = parentId + '/' + partialViewName;
- }
- return '@Html.Partial("' + partialViewName + '")';
- }
- function getQuerySnippet(queryExpression) {
- var code = '\n@{\n' + '\tvar selection = ' + queryExpression + ';\n}\n';
- code += '<ul>\n' + '\t@foreach(var item in selection){\n' + '\t\t<li>\n' + '\t\t\t<a href="@item.Url">@item.Name</a>\n' + '\t\t</li>\n' + '\t}\n' + '</ul>\n\n';
- return code;
- }
- function getRenderBodySnippet() {
- return '@RenderBody()';
- }
- function getRenderSectionSnippet(sectionName, mandatory) {
- return '@RenderSection("' + sectionName + '", ' + mandatory + ')';
- }
- function getAddSectionSnippet(sectionName) {
- return '@section ' + sectionName + '\r\n{\r\n\r\n\t{0}\r\n\r\n}\r\n';
- }
- function getGeneralShortcuts() {
- return {
- 'name': localizationService.localize('shortcuts_generalHeader'),
- 'shortcuts': [
- {
- 'description': localizationService.localize('buttons_undo'),
- 'keys': [
- { 'key': 'ctrl' },
- { 'key': 'z' }
- ]
- },
- {
- 'description': localizationService.localize('buttons_redo'),
- 'keys': [
- { 'key': 'ctrl' },
- { 'key': 'y' }
- ]
- },
- {
- 'description': localizationService.localize('buttons_save'),
- 'keys': [
- { 'key': 'ctrl' },
- { 'key': 's' }
- ]
- }
- ]
- };
- }
- function getEditorShortcuts() {
- return {
- 'name': localizationService.localize('shortcuts_editorHeader'),
- 'shortcuts': [
- {
- 'description': localizationService.localize('shortcuts_commentLine'),
- 'keys': [
- { 'key': 'ctrl' },
- { 'key': '/' }
- ]
- },
- {
- 'description': localizationService.localize('shortcuts_removeLine'),
- 'keys': [
- { 'key': 'ctrl' },
- { 'key': 'd' }
- ]
- },
- {
- 'description': localizationService.localize('shortcuts_copyLineUp'),
- 'keys': {
- 'win': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'up' }
- ],
- 'mac': [
- { 'key': 'cmd' },
- { 'key': 'alt' },
- { 'key': 'up' }
- ]
- }
- },
- {
- 'description': localizationService.localize('shortcuts_copyLineDown'),
- 'keys': {
- 'win': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'down' }
- ],
- 'mac': [
- { 'key': 'cmd' },
- { 'key': 'alt' },
- { 'key': 'down' }
- ]
- }
- },
- {
- 'description': localizationService.localize('shortcuts_moveLineUp'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'up' }
- ]
- },
- {
- 'description': localizationService.localize('shortcuts_moveLineDown'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'down' }
- ]
- }
- ]
- };
- }
- function getTemplateEditorShortcuts() {
- return {
- 'name': 'Umbraco',
- //No need to localise Umbraco is the same in all languages :)
- 'shortcuts': [
- {
- 'description': localizationService.format([
- 'template_insert',
- 'template_insertPageField'
- ], '%0% %1%'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'v' }
- ]
- },
- {
- 'description': localizationService.format([
- 'template_insert',
- 'template_insertPartialView'
- ], '%0% %1%'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'p' }
- ]
- },
- {
- 'description': localizationService.format([
- 'template_insert',
- 'template_insertDictionaryItem'
- ], '%0% %1%'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'd' }
- ]
- },
- {
- 'description': localizationService.format([
- 'template_insert',
- 'template_insertMacro'
- ], '%0% %1%'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'm' }
- ]
- },
- {
- 'description': localizationService.localize('template_queryBuilder'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'q' }
- ]
- },
- {
- 'description': localizationService.format([
- 'template_insert',
- 'template_insertSections'
- ], '%0% %1%'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 's' }
- ]
- },
- {
- 'description': localizationService.localize('template_mastertemplate'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 't' }
- ]
- }
- ]
- };
- }
- function getPartialViewEditorShortcuts() {
- return {
- 'name': 'Umbraco',
- //No need to localise Umbraco is the same in all languages :)
- 'shortcuts': [
- {
- 'description': localizationService.format([
- 'template_insert',
- 'template_insertPageField'
- ], '%0% %1%'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'v' }
- ]
- },
- {
- 'description': localizationService.format([
- 'template_insert',
- 'template_insertDictionaryItem'
- ], '%0% %1%'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'd' }
- ]
- },
- {
- 'description': localizationService.format([
- 'template_insert',
- 'template_insertMacro'
- ], '%0% %1%'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'm' }
- ]
- },
- {
- 'description': localizationService.localize('template_queryBuilder'),
- 'keys': [
- { 'key': 'alt' },
- { 'key': 'shift' },
- { 'key': 'q' }
- ]
- }
- ]
- };
- }
- ////////////
- var service = {
- getInsertDictionarySnippet: getInsertDictionarySnippet,
- getInsertPartialSnippet: getInsertPartialSnippet,
- getQuerySnippet: getQuerySnippet,
- getRenderBodySnippet: getRenderBodySnippet,
- getRenderSectionSnippet: getRenderSectionSnippet,
- getAddSectionSnippet: getAddSectionSnippet,
- getGeneralShortcuts: getGeneralShortcuts,
- getEditorShortcuts: getEditorShortcuts,
- getTemplateEditorShortcuts: getTemplateEditorShortcuts,
- getPartialViewEditorShortcuts: getPartialViewEditorShortcuts
- };
- return service;
- }
- angular.module('umbraco.services').factory('templateHelper', templateHelperService);
- }());
- /**
- * @ngdoc service
- * @name umbraco.services.tinyMceService
- *
- *
- * @description
- * A service containing all logic for all of the Umbraco TinyMCE plugins
- */
- function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) {
- return {
- /**
- * @ngdoc method
- * @name umbraco.services.tinyMceService#configuration
- * @methodOf umbraco.services.tinyMceService
- *
- * @description
- * Returns a collection of plugins available to the tinyMCE editor
- *
- */
- configuration: function () {
- return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('rteApiBaseUrl', 'GetConfiguration'), { cache: true }), 'Failed to retrieve tinymce configuration');
- },
- /**
- * @ngdoc method
- * @name umbraco.services.tinyMceService#defaultPrevalues
- * @methodOf umbraco.services.tinyMceService
- *
- * @description
- * Returns a default configration to fallback on in case none is provided
- *
- */
- defaultPrevalues: function () {
- var cfg = {};
- cfg.toolbar = [
- 'code',
- 'bold',
- 'italic',
- 'styleselect',
- 'alignleft',
- 'aligncenter',
- 'alignright',
- 'bullist',
- 'numlist',
- 'outdent',
- 'indent',
- 'link',
- 'image',
- 'umbmediapicker',
- 'umbembeddialog',
- 'umbmacro'
- ];
- cfg.stylesheets = [];
- cfg.dimensions = { height: 500 };
- cfg.maxImageSize = 500;
- return cfg;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia
- * @methodOf umbraco.services.tinyMceService
- *
- * @description
- * Creates the umbrco insert embedded media tinymce plugin
- *
- * @param {Object} editor the TinyMCE editor instance
- * @param {Object} $scope the current controller scope
- */
- createInsertEmbeddedMedia: function (editor, scope, callback) {
- editor.addButton('umbembeddialog', {
- icon: 'custom icon-tv',
- tooltip: 'Embed',
- onclick: function () {
- if (callback) {
- callback();
- }
- }
- });
- },
- insertEmbeddedMediaInEditor: function (editor, preview) {
- editor.insertContent(preview);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.tinyMceService#createMediaPicker
- * @methodOf umbraco.services.tinyMceService
- *
- * @description
- * Creates the umbrco insert media tinymce plugin
- *
- * @param {Object} editor the TinyMCE editor instance
- * @param {Object} $scope the current controller scope
- */
- createMediaPicker: function (editor, scope, callback) {
- editor.addButton('umbmediapicker', {
- icon: 'custom icon-picture',
- tooltip: 'Media Picker',
- stateSelector: 'img',
- onclick: function () {
- var selectedElm = editor.selection.getNode(), currentTarget;
- if (selectedElm.nodeName === 'IMG') {
- var img = $(selectedElm);
- var hasUdi = img.attr('data-udi') ? true : false;
- currentTarget = {
- altText: img.attr('alt'),
- url: img.attr('src')
- };
- if (hasUdi) {
- currentTarget['udi'] = img.attr('data-udi');
- } else {
- currentTarget['id'] = img.attr('rel');
- }
- }
- userService.getCurrentUser().then(function (userData) {
- if (callback) {
- callback(currentTarget, userData);
- }
- });
- }
- });
- },
- insertMediaInEditor: function (editor, img) {
- if (img) {
- var hasUdi = img.udi ? true : false;
- var data = {
- alt: img.altText || '',
- src: img.url ? img.url : 'nothing.jpg',
- id: '__mcenew'
- };
- if (hasUdi) {
- data['data-udi'] = img.udi;
- } else {
- //Considering these fixed because UDI will now be used and thus
- // we have no need for rel http://issues.umbraco.org/issue/U4-6228, http://issues.umbraco.org/issue/U4-6595
- data['rel'] = img.id;
- data['data-id'] = img.id;
- }
- editor.insertContent(editor.dom.createHTML('img', data));
- $timeout(function () {
- var imgElm = editor.dom.get('__mcenew');
- var size = editor.dom.getSize(imgElm);
- if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) {
- var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h);
- var s = 'width: ' + newSize.width + 'px; height:' + newSize.height + 'px;';
- editor.dom.setAttrib(imgElm, 'style', s);
- editor.dom.setAttrib(imgElm, 'id', null);
- if (img.url) {
- var src = img.url + '?width=' + newSize.width + '&height=' + newSize.height;
- editor.dom.setAttrib(imgElm, 'data-mce-src', src);
- }
- }
- }, 500);
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.tinyMceService#createUmbracoMacro
- * @methodOf umbraco.services.tinyMceService
- *
- * @description
- * Creates the insert umbrco macro tinymce plugin
- *
- * @param {Object} editor the TinyMCE editor instance
- * @param {Object} $scope the current controller scope
- */
- createInsertMacro: function (editor, $scope, callback) {
- var createInsertMacroScope = this;
- /** Adds custom rules for the macro plugin and custom serialization */
- editor.on('preInit', function (args) {
- //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out
- editor.serializer.addRules('div');
- /** This checks if the div is a macro container, if so, checks if its wrapped in a p tag and then unwraps it (removes p tag) */
- editor.serializer.addNodeFilter('div', function (nodes, name) {
- for (var i = 0; i < nodes.length; i++) {
- if (nodes[i].attr('class') === 'umb-macro-holder' && nodes[i].parent && nodes[i].parent.name.toUpperCase() === 'P') {
- nodes[i].parent.unwrap();
- }
- }
- });
- });
- /**
- * Because the macro gets wrapped in a P tag because of the way 'enter' works, this
- * method will return the macro element if not wrapped in a p, or the p if the macro
- * element is the only one inside of it even if we are deep inside an element inside the macro
- */
- function getRealMacroElem(element) {
- var e = $(element).closest('.umb-macro-holder');
- if (e.length > 0) {
- if (e.get(0).parentNode.nodeName === 'P') {
- //now check if we're the only element
- if (element.parentNode.childNodes.length === 1) {
- return e.get(0).parentNode;
- }
- }
- return e.get(0);
- }
- return null;
- }
- /** Adds the button instance */
- editor.addButton('umbmacro', {
- icon: 'custom icon-settings-alt',
- tooltip: 'Insert macro',
- onPostRender: function () {
- var ctrl = this;
- var isOnMacroElement = false;
- /**
- if the selection comes from a different element that is not the macro's
- we need to check if the selection includes part of the macro, if so we'll force the selection
- to clear to the next element since if people can select part of the macro markup they can then modify it.
- */
- function handleSelectionChange() {
- if (!editor.selection.isCollapsed()) {
- var endSelection = tinymce.activeEditor.selection.getEnd();
- var startSelection = tinymce.activeEditor.selection.getStart();
- //don't proceed if it's an entire element selected
- if (endSelection !== startSelection) {
- //if the end selection is a macro then move the cursor
- //NOTE: we don't have to handle when the selection comes from a previous parent because
- // that is automatically taken care of with the normal onNodeChanged logic since the
- // evt.element will be the macro once it becomes part of the selection.
- var $testForMacro = $(endSelection).closest('.umb-macro-holder');
- if ($testForMacro.length > 0) {
- //it came from before so move after, if there is no after then select ourselves
- var next = $testForMacro.next();
- if (next.length > 0) {
- editor.selection.setCursorLocation($testForMacro.next().get(0));
- } else {
- selectMacroElement($testForMacro.get(0));
- }
- }
- }
- }
- }
- /** helper method to select the macro element */
- function selectMacroElement(macroElement) {
- // move selection to top element to ensure we can't edit this
- editor.selection.select(macroElement);
- // check if the current selection *is* the element (ie bug)
- var currentSelection = editor.selection.getStart();
- if (tinymce.isIE) {
- if (!editor.dom.hasClass(currentSelection, 'umb-macro-holder')) {
- while (!editor.dom.hasClass(currentSelection, 'umb-macro-holder') && currentSelection.parentNode) {
- currentSelection = currentSelection.parentNode;
- }
- editor.selection.select(currentSelection);
- }
- }
- }
- /**
- * Add a node change handler, test if we're editing a macro and select the whole thing, then set our isOnMacroElement flag.
- * If we change the selection inside this method, then we end up in an infinite loop, so we have to remove ourselves
- * from the event listener before changing selection, however, it seems that putting a break point in this method
- * will always cause an 'infinite' loop as the caret keeps changing.
- */
- function onNodeChanged(evt) {
- //set our macro button active when on a node of class umb-macro-holder
- var $macroElement = $(evt.element).closest('.umb-macro-holder');
- handleSelectionChange();
- //set the button active
- ctrl.active($macroElement.length !== 0);
- if ($macroElement.length > 0) {
- var macroElement = $macroElement.get(0);
- //remove the event listener before re-selecting
- editor.off('NodeChange', onNodeChanged);
- selectMacroElement(macroElement);
- //set the flag
- isOnMacroElement = true;
- //re-add the event listener
- editor.on('NodeChange', onNodeChanged);
- } else {
- isOnMacroElement = false;
- }
- }
- /** when the contents load we need to find any macros declared and load in their content */
- editor.on('LoadContent', function (o) {
- //get all macro divs and load their content
- $(editor.dom.select('.umb-macro-holder.mceNonEditable')).each(function () {
- createInsertMacroScope.loadMacroContent($(this), null, $scope);
- });
- });
- /** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */
- editor.on('BeforeExecCommand', function (o) {
- if (isOnMacroElement) {
- if (o.preventDefault) {
- o.preventDefault();
- }
- if (o.stopImmediatePropagation) {
- o.stopImmediatePropagation();
- }
- return;
- }
- });
- /** This double checks and ensures you can't paste content into the rendered macro */
- editor.on('Paste', function (o) {
- if (isOnMacroElement) {
- if (o.preventDefault) {
- o.preventDefault();
- }
- if (o.stopImmediatePropagation) {
- o.stopImmediatePropagation();
- }
- return;
- }
- });
- //set onNodeChanged event listener
- editor.on('NodeChange', onNodeChanged);
- /**
- * Listen for the keydown in the editor, we'll check if we are currently on a macro element, if so
- * we'll check if the key down is a supported key which requires an action, otherwise we ignore the request
- * so the macro cannot be edited.
- */
- editor.on('KeyDown', function (e) {
- if (isOnMacroElement) {
- var macroElement = editor.selection.getNode();
- //get the 'real' element (either p or the real one)
- macroElement = getRealMacroElem(macroElement);
- //prevent editing
- e.preventDefault();
- e.stopPropagation();
- var moveSibling = function (element, isNext) {
- var $e = $(element);
- var $sibling = isNext ? $e.next() : $e.prev();
- if ($sibling.length > 0) {
- editor.selection.select($sibling.get(0));
- editor.selection.collapse(true);
- } else {
- //if we're moving previous and there is no sibling, then lets recurse and just select the next one
- if (!isNext) {
- moveSibling(element, true);
- return;
- }
- //if there is no sibling we'll generate a new p at the end and select it
- editor.setContent(editor.getContent() + '<p> </p>');
- editor.selection.select($(editor.dom.getRoot()).children().last().get(0));
- editor.selection.collapse(true);
- }
- };
- //supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left)
- //supported keys to remove the macro (8-backspace, 46-delete)
- //TODO: Should we make the enter key insert a line break before or leave it as moving to the next element?
- if ($.inArray(e.keyCode, [
- 13,
- 40,
- 39
- ]) !== -1) {
- //move to next element
- moveSibling(macroElement, true);
- } else if ($.inArray(e.keyCode, [
- 27,
- 38,
- 37
- ]) !== -1) {
- //move to prev element
- moveSibling(macroElement, false);
- } else if ($.inArray(e.keyCode, [
- 8,
- 46
- ]) !== -1) {
- //delete macro element
- //move first, then delete
- moveSibling(macroElement, false);
- editor.dom.remove(macroElement);
- }
- return;
- }
- });
- },
- /** The insert macro button click event handler */
- onclick: function () {
- var dialogData = {
- //flag for use in rte so we only show macros flagged for the editor
- richTextEditor: true
- };
- //when we click we could have a macro already selected and in that case we'll want to edit the current parameters
- //so we'll need to extract them and submit them to the dialog.
- var macroElement = editor.selection.getNode();
- macroElement = getRealMacroElem(macroElement);
- if (macroElement) {
- //we have a macro selected so we'll need to parse it's alias and parameters
- var contents = $(macroElement).contents();
- var comment = _.find(contents, function (item) {
- return item.nodeType === 8;
- });
- if (!comment) {
- throw 'Cannot parse the current macro, the syntax in the editor is invalid';
- }
- var syntax = comment.textContent.trim();
- var parsed = macroService.parseMacroSyntax(syntax);
- dialogData = { macroData: parsed };
- }
- if (callback) {
- callback(dialogData);
- }
- }
- });
- },
- insertMacroInEditor: function (editor, macroObject, $scope) {
- //put the macro syntax in comments, we will parse this out on the server side to be used
- //for persisting.
- var macroSyntaxComment = '<!-- ' + macroObject.syntax + ' -->';
- //create an id class for this element so we can re-select it after inserting
- var uniqueId = 'umb-macro-' + editor.dom.uniqueId();
- var macroDiv = editor.dom.create('div', { 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId }, macroSyntaxComment + '<ins>Macro alias: <strong>' + macroObject.macroAlias + '</strong></ins>');
- editor.selection.setNode(macroDiv);
- var $macroDiv = $(editor.dom.select('div.umb-macro-holder.' + uniqueId));
- //async load the macro content
- this.loadMacroContent($macroDiv, macroObject, $scope);
- },
- /** loads in the macro content async from the server */
- loadMacroContent: function ($macroDiv, macroData, $scope) {
- //if we don't have the macroData, then we'll need to parse it from the macro div
- if (!macroData) {
- var contents = $macroDiv.contents();
- var comment = _.find(contents, function (item) {
- return item.nodeType === 8;
- });
- if (!comment) {
- throw 'Cannot parse the current macro, the syntax in the editor is invalid';
- }
- var syntax = comment.textContent.trim();
- var parsed = macroService.parseMacroSyntax(syntax);
- macroData = parsed;
- }
- var $ins = $macroDiv.find('ins');
- //show the throbber
- $macroDiv.addClass('loading');
- var contentId = $routeParams.id;
- //need to wrap in safe apply since this might be occuring outside of angular
- angularHelper.safeApply($scope, function () {
- macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary).then(function (htmlResult) {
- $macroDiv.removeClass('loading');
- htmlResult = htmlResult.trim();
- if (htmlResult !== '') {
- $ins.html(htmlResult);
- }
- });
- });
- },
- createLinkPicker: function (editor, $scope, onClick) {
- function createLinkList(callback) {
- return function () {
- var linkList = editor.settings.link_list;
- if (typeof linkList === 'string') {
- tinymce.util.XHR.send({
- url: linkList,
- success: function (text) {
- callback(tinymce.util.JSON.parse(text));
- }
- });
- } else {
- callback(linkList);
- }
- };
- }
- function showDialog(linkList) {
- var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText;
- var win, linkListCtrl, relListCtrl, targetListCtrl;
- function linkListChangeHandler(e) {
- var textCtrl = win.find('#text');
- if (!textCtrl.value() || e.lastControl && textCtrl.value() === e.lastControl.text()) {
- textCtrl.value(e.control.text());
- }
- win.find('#href').value(e.control.value());
- }
- function buildLinkList() {
- var linkListItems = [{
- text: 'None',
- value: ''
- }];
- tinymce.each(linkList, function (link) {
- linkListItems.push({
- text: link.text || link.title,
- value: link.value || link.url,
- menu: link.menu
- });
- });
- return linkListItems;
- }
- function buildRelList(relValue) {
- var relListItems = [{
- text: 'None',
- value: ''
- }];
- tinymce.each(editor.settings.rel_list, function (rel) {
- relListItems.push({
- text: rel.text || rel.title,
- value: rel.value,
- selected: relValue === rel.value
- });
- });
- return relListItems;
- }
- function buildTargetList(targetValue) {
- var targetListItems = [{
- text: 'None',
- value: ''
- }];
- if (!editor.settings.target_list) {
- targetListItems.push({
- text: 'New window',
- value: '_blank'
- });
- }
- tinymce.each(editor.settings.target_list, function (target) {
- targetListItems.push({
- text: target.text || target.title,
- value: target.value,
- selected: targetValue === target.value
- });
- });
- return targetListItems;
- }
- function buildAnchorListControl(url) {
- var anchorList = [];
- tinymce.each(editor.dom.select('a:not([href])'), function (anchor) {
- var id = anchor.name || anchor.id;
- if (id) {
- anchorList.push({
- text: id,
- value: '#' + id,
- selected: url.indexOf('#' + id) !== -1
- });
- }
- });
- if (anchorList.length) {
- anchorList.unshift({
- text: 'None',
- value: ''
- });
- return {
- name: 'anchor',
- type: 'listbox',
- label: 'Anchors',
- values: anchorList,
- onselect: linkListChangeHandler
- };
- }
- }
- function updateText() {
- if (!initialText && data.text.length === 0) {
- this.parent().parent().find('#text')[0].value(this.value());
- }
- }
- selectedElm = selection.getNode();
- anchorElm = dom.getParent(selectedElm, 'a[href]');
- data.text = initialText = anchorElm ? anchorElm.innerText || anchorElm.textContent : selection.getContent({ format: 'text' });
- data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : '';
- data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : '';
- data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : '';
- if (selectedElm.nodeName === 'IMG') {
- data.text = initialText = ' ';
- }
- if (linkList) {
- linkListCtrl = {
- type: 'listbox',
- label: 'Link list',
- values: buildLinkList(),
- onselect: linkListChangeHandler
- };
- }
- if (editor.settings.target_list !== false) {
- targetListCtrl = {
- name: 'target',
- type: 'listbox',
- label: 'Target',
- values: buildTargetList(data.target)
- };
- }
- if (editor.settings.rel_list) {
- relListCtrl = {
- name: 'rel',
- type: 'listbox',
- label: 'Rel',
- values: buildRelList(data.rel)
- };
- }
- var injector = angular.element(document.getElementById('umbracoMainPageBody')).injector();
- var dialogService = injector.get('dialogService');
- var currentTarget = null;
- //if we already have a link selected, we want to pass that data over to the dialog
- if (anchorElm) {
- var anchor = $(anchorElm);
- currentTarget = {
- name: anchor.attr('title'),
- url: anchor.attr('href'),
- target: anchor.attr('target')
- };
- //locallink detection, we do this here, to avoid poluting the dialogservice
- //so the dialog service can just expect to get a node-like structure
- if (currentTarget.url.indexOf('localLink:') > 0) {
- var linkId = currentTarget.url.substring(currentTarget.url.indexOf(':') + 1, currentTarget.url.length - 1);
- //we need to check if this is an INT or a UDI
- var parsedIntId = parseInt(linkId, 10);
- if (isNaN(parsedIntId)) {
- //it's a UDI
- currentTarget.udi = linkId;
- } else {
- currentTarget.id = linkId;
- }
- }
- }
- if (onClick) {
- onClick(currentTarget, anchorElm);
- }
- }
- editor.addButton('link', {
- icon: 'link',
- tooltip: 'Insert/edit link',
- shortcut: 'Ctrl+K',
- onclick: createLinkList(showDialog),
- stateSelector: 'a[href]'
- });
- editor.addButton('unlink', {
- icon: 'unlink',
- tooltip: 'Remove link',
- cmd: 'unlink',
- stateSelector: 'a[href]'
- });
- editor.addShortcut('Ctrl+K', '', createLinkList(showDialog));
- this.showDialog = showDialog;
- editor.addMenuItem('link', {
- icon: 'link',
- text: 'Insert link',
- shortcut: 'Ctrl+K',
- onclick: createLinkList(showDialog),
- stateSelector: 'a[href]',
- context: 'insert',
- prependToContext: true
- });
- },
- insertLinkInEditor: function (editor, target, anchorElm) {
- var href = target.url;
- // We want to use the Udi. If it is set, we use it, else fallback to id, and finally to null
- var hasUdi = target.udi ? true : false;
- var id = hasUdi ? target.udi : target.id ? target.id : null;
- //Create a json obj used to create the attributes for the tag
- function createElemAttributes() {
- var a = {
- href: href,
- title: target.name,
- target: target.target ? target.target : null,
- rel: target.rel ? target.rel : null
- };
- if (hasUdi) {
- a['data-udi'] = target.udi;
- } else if (target.id) {
- a['data-id'] = target.id;
- }
- return a;
- }
- function insertLink() {
- if (anchorElm) {
- editor.dom.setAttribs(anchorElm, createElemAttributes());
- editor.selection.select(anchorElm);
- editor.execCommand('mceEndTyping');
- } else {
- editor.execCommand('mceInsertLink', false, createElemAttributes());
- }
- }
- if (!href) {
- editor.execCommand('unlink');
- return;
- }
- //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set
- if (id && (angular.isUndefined(target.isMedia) || !target.isMedia)) {
- href = '/{localLink:' + id + '}';
- insertLink();
- return;
- }
- // Is email and not //user@domain.com
- if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) {
- href = 'mailto:' + href;
- insertLink();
- return;
- }
- // Is www. prefixed
- if (/^\s*www\./i.test(href)) {
- href = 'http://' + href;
- insertLink();
- return;
- }
- insertLink();
- }
- };
- }
- angular.module('umbraco.services').factory('tinyMceService', tinyMceService);
- /**
- * @ngdoc service
- * @name umbraco.services.treeService
- * @function
- *
- * @description
- * The tree service factory, used internally by the umbTree and umbTreeItem directives
- */
- function treeService($q, treeResource, iconHelper, notificationsService, eventsService) {
- //SD: Have looked at putting this in sessionStorage (not localStorage since that means you wouldn't be able to work
- // in multiple tabs) - however our tree structure is cyclical, meaning a node has a reference to it's parent and it's children
- // which you cannot serialize to sessionStorage. There's really no benefit of session storage except that you could refresh
- // a tab and have the trees where they used to be - supposed that is kind of nice but would mean we'd have to store the parent
- // as a nodeid reference instead of a variable with a getParent() method.
- var treeCache = {};
- var standardCssClass = 'icon umb-tree-icon sprTree';
- function getCacheKey(args) {
- //if there is no cache key they return null - it won't be cached.
- if (!args || !args.cacheKey) {
- return null;
- }
- var cacheKey = args.cacheKey;
- cacheKey += '_' + args.section;
- return cacheKey;
- }
- return {
- /** Internal method to return the tree cache */
- _getTreeCache: function () {
- return treeCache;
- },
- /** Internal method that ensures there's a routePath, parent and level property on each tree node and adds some icon specific properties so that the nodes display properly */
- _formatNodeDataForUseInUI: function (parentNode, treeNodes, section, level) {
- //if no level is set, then we make it 1
- var childLevel = level ? level : 1;
- //set the section if it's not already set
- if (!parentNode.section) {
- parentNode.section = section;
- }
- if (parentNode.metaData && parentNode.metaData.noAccess === true) {
- if (!parentNode.cssClasses) {
- parentNode.cssClasses = [];
- }
- parentNode.cssClasses.push('no-access');
- }
- //create a method outside of the loop to return the parent - otherwise jshint blows up
- var funcParent = function () {
- return parentNode;
- };
- for (var i = 0; i < treeNodes.length; i++) {
- var treeNode = treeNodes[i];
- treeNode.level = childLevel;
- //create a function to get the parent node, we could assign the parent node but
- // then we cannot serialize this entity because we have a cyclical reference.
- // Instead we just make a function to return the parentNode.
- treeNode.parent = funcParent;
- //set the section for each tree node - this allows us to reference this easily when accessing tree nodes
- treeNode.section = section;
- //if there is not route path specified, then set it automatically,
- //if this is a tree root node then we want to route to the section's dashboard
- if (!treeNode.routePath) {
- if (treeNode.metaData && treeNode.metaData['treeAlias']) {
- //this is a root node
- treeNode.routePath = section;
- } else {
- var treeAlias = this.getTreeAlias(treeNode);
- treeNode.routePath = section + '/' + treeAlias + '/edit/' + treeNode.id;
- }
- }
- //now, format the icon data
- if (treeNode.iconIsClass === undefined || treeNode.iconIsClass) {
- var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNode);
- treeNode.cssClass = standardCssClass + ' ' + converted;
- if (converted.startsWith('.')) {
- //its legacy so add some width/height
- treeNode.style = 'height:16px;width:16px;';
- } else {
- treeNode.style = '';
- }
- } else {
- treeNode.style = 'background-image: url(\'' + treeNode.iconFilePath + '\');';
- //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this
- treeNode.cssClass = standardCssClass + ' legacy-custom-file';
- }
- if (treeNode.metaData && treeNode.metaData.noAccess === true) {
- if (!treeNode.cssClasses) {
- treeNode.cssClasses = [];
- }
- treeNode.cssClasses.push('no-access');
- }
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#getTreePackageFolder
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Determines if the current tree is a plugin tree and if so returns the package folder it has declared
- * so we know where to find it's views, otherwise it will just return undefined.
- *
- * @param {String} treeAlias The tree alias to check
- */
- getTreePackageFolder: function (treeAlias) {
- //we determine this based on the server variables
- if (Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.trees && angular.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) {
- var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) {
- return item.alias === treeAlias;
- });
- return found ? found.packageFolder : undefined;
- }
- return undefined;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#clearCache
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Clears the tree cache - with optional cacheKey, optional section or optional filter.
- *
- * @param {Object} args arguments
- * @param {String} args.cacheKey optional cachekey - this is used to clear specific trees in dialogs
- * @param {String} args.section optional section alias - clear tree for a given section
- * @param {String} args.childrenOf optional parent ID - only clear the cache below a specific node
- */
- clearCache: function (args) {
- //clear all if not specified
- if (!args) {
- treeCache = {};
- } else {
- //if section and cache key specified just clear that cache
- if (args.section && args.cacheKey) {
- var cacheKey = getCacheKey(args);
- if (cacheKey && treeCache && treeCache[cacheKey] != null) {
- treeCache = _.omit(treeCache, cacheKey);
- }
- } else if (args.childrenOf) {
- //if childrenOf is supplied a cacheKey must be supplied as well
- if (!args.cacheKey) {
- throw 'args.cacheKey is required if args.childrenOf is supplied';
- }
- //this will clear out all children for the parentId passed in to this parameter, we'll
- // do this by recursing and specifying a filter
- var self = this;
- this.clearCache({
- cacheKey: args.cacheKey,
- filter: function (cc) {
- //get the new parent node from the tree cache
- var parent = self.getDescendantNode(cc.root, args.childrenOf);
- if (parent) {
- //clear it's children and set to not expanded
- parent.children = null;
- parent.expanded = false;
- }
- //return the cache to be saved
- return cc;
- }
- });
- } else if (args.filter && angular.isFunction(args.filter)) {
- //if a filter is supplied a cacheKey must be supplied as well
- if (!args.cacheKey) {
- throw 'args.cacheKey is required if args.filter is supplied';
- }
- //if a filter is supplied the function needs to return the data to keep
- var byKey = treeCache[args.cacheKey];
- if (byKey) {
- var result = args.filter(byKey);
- if (result) {
- //set the result to the filtered data
- treeCache[args.cacheKey] = result;
- } else {
- //remove the cache
- treeCache = _.omit(treeCache, args.cacheKey);
- }
- }
- } else if (args.cacheKey) {
- //if only the cache key is specified, then clear all cache starting with that key
- var allKeys1 = _.keys(treeCache);
- var toRemove1 = _.filter(allKeys1, function (k) {
- return k.startsWith(args.cacheKey + '_');
- });
- treeCache = _.omit(treeCache, toRemove1);
- } else if (args.section) {
- //if only the section is specified then clear all cache regardless of cache key by that section
- var allKeys2 = _.keys(treeCache);
- var toRemove2 = _.filter(allKeys2, function (k) {
- return k.endsWith('_' + args.section);
- });
- treeCache = _.omit(treeCache, toRemove2);
- }
- }
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#loadNodeChildren
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Clears all node children, gets it's up-to-date children from the server and re-assigns them and then
- * returns them in a promise.
- * @param {object} args An arguments object
- * @param {object} args.node The tree node
- * @param {object} args.section The current section
- */
- loadNodeChildren: function (args) {
- if (!args) {
- throw 'No args object defined for loadNodeChildren';
- }
- if (!args.node) {
- throw 'No node defined on args object for loadNodeChildren';
- }
- this.removeChildNodes(args.node);
- args.node.loading = true;
- return this.getChildren(args).then(function (data) {
- //set state to done and expand (only if there actually are children!)
- args.node.loading = false;
- args.node.children = data;
- if (args.node.children && args.node.children.length > 0) {
- args.node.expanded = true;
- args.node.hasChildren = true;
- }
- return data;
- }, function (reason) {
- //in case of error, emit event
- eventsService.emit('treeService.treeNodeLoadError', { error: reason });
- //stop show the loading indicator
- args.node.loading = false;
- //tell notications about the error
- notificationsService.error(reason);
- return reason;
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#removeNode
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Removes a given node from the tree
- * @param {object} treeNode the node to remove
- */
- removeNode: function (treeNode) {
- if (!angular.isFunction(treeNode.parent)) {
- return;
- }
- if (treeNode.parent() == null) {
- throw 'Cannot remove a node that doesn\'t have a parent';
- }
- //remove the current item from it's siblings
- treeNode.parent().children.splice(treeNode.parent().children.indexOf(treeNode), 1);
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#removeChildNodes
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Removes all child nodes from a given tree node
- * @param {object} treeNode the node to remove children from
- */
- removeChildNodes: function (treeNode) {
- treeNode.expanded = false;
- treeNode.children = [];
- treeNode.hasChildren = false;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#getChildNode
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Gets a child node with a given ID, from a specific treeNode
- * @param {object} treeNode to retrive child node from
- * @param {int} id id of child node
- */
- getChildNode: function (treeNode, id) {
- if (!treeNode.children) {
- return null;
- }
- var found = _.find(treeNode.children, function (child) {
- return String(child.id).toLowerCase() === String(id).toLowerCase();
- });
- return found === undefined ? null : found;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#getDescendantNode
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Gets a descendant node by id
- * @param {object} treeNode to retrive descendant node from
- * @param {int} id id of descendant node
- * @param {string} treeAlias - optional tree alias, if fetching descendant node from a child of a listview document
- */
- getDescendantNode: function (treeNode, id, treeAlias) {
- //validate if it is a section container since we'll need a treeAlias if it is one
- if (treeNode.isContainer === true && !treeAlias) {
- throw 'Cannot get a descendant node from a section container node without a treeAlias specified';
- }
- //if it is a section container, we need to find the tree to be searched
- if (treeNode.isContainer) {
- var foundRoot = null;
- for (var c = 0; c < treeNode.children.length; c++) {
- if (this.getTreeAlias(treeNode.children[c]) === treeAlias) {
- foundRoot = treeNode.children[c];
- break;
- }
- }
- if (!foundRoot) {
- throw 'Could not find a tree in the current section with alias ' + treeAlias;
- }
- treeNode = foundRoot;
- }
- //check this node
- if (treeNode.id === id) {
- return treeNode;
- }
- //check the first level
- var found = this.getChildNode(treeNode, id);
- if (found) {
- return found;
- }
- //check each child of this node
- if (!treeNode.children) {
- return null;
- }
- for (var i = 0; i < treeNode.children.length; i++) {
- var child = treeNode.children[i];
- if (child.children && angular.isArray(child.children) && child.children.length > 0) {
- //recurse
- found = this.getDescendantNode(child, id);
- if (found) {
- return found;
- }
- }
- }
- //not found
- return found === undefined ? null : found;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#getTreeRoot
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Gets the root node of the current tree type for a given tree node
- * @param {object} treeNode to retrive tree root node from
- */
- getTreeRoot: function (treeNode) {
- if (!treeNode) {
- throw 'treeNode cannot be null';
- }
- //all root nodes have metadata key 'treeAlias'
- var root = null;
- var current = treeNode;
- while (root === null && current) {
- if (current.metaData && current.metaData['treeAlias']) {
- root = current;
- } else if (angular.isFunction(current.parent)) {
- //we can only continue if there is a parent() method which means this
- // tree node was loaded in as part of a real tree, not just as a single tree
- // node from the server.
- current = current.parent();
- } else {
- current = null;
- }
- }
- return root;
- },
- /** Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node */
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#getTreeAlias
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node
- * @param {object} treeNode to retrive tree alias from
- */
- getTreeAlias: function (treeNode) {
- var root = this.getTreeRoot(treeNode);
- if (root) {
- return root.metaData['treeAlias'];
- }
- return '';
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#getTree
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * gets the tree, returns a promise
- * @param {object} args Arguments
- * @param {string} args.section Section alias
- * @param {string} args.cacheKey Optional cachekey
- */
- getTree: function (args) {
- var deferred = $q.defer();
- //set defaults
- if (!args) {
- args = {
- section: 'content',
- cacheKey: null
- };
- } else if (!args.section) {
- args.section = 'content';
- }
- var cacheKey = getCacheKey(args);
- //return the cache if it exists
- if (cacheKey && treeCache[cacheKey] !== undefined) {
- deferred.resolve(treeCache[cacheKey]);
- return deferred.promise;
- }
- var self = this;
- treeResource.loadApplication(args).then(function (data) {
- //this will be called once the tree app data has loaded
- var result = {
- name: data.name,
- alias: args.section,
- root: data
- };
- //we need to format/modify some of the node data to be used in our app.
- self._formatNodeDataForUseInUI(result.root, result.root.children, args.section);
- //cache this result if a cache key is specified - generally a cache key should ONLY
- // be specified for application trees, dialog trees should not be cached.
- if (cacheKey) {
- treeCache[cacheKey] = result;
- deferred.resolve(treeCache[cacheKey]);
- }
- //return un-cached
- deferred.resolve(result);
- });
- return deferred.promise;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#getMenu
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Returns available menu actions for a given tree node
- * @param {object} args Arguments
- * @param {string} args.treeNode tree node object to retrieve the menu for
- */
- getMenu: function (args) {
- if (!args) {
- throw 'args cannot be null';
- }
- if (!args.treeNode) {
- throw 'args.treeNode cannot be null';
- }
- return treeResource.loadMenu(args.treeNode).then(function (data) {
- //need to convert the icons to new ones
- for (var i = 0; i < data.length; i++) {
- data[i].cssclass = iconHelper.convertFromLegacyIcon(data[i].cssclass);
- }
- return data;
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#getChildren
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Gets the children from the server for a given node
- * @param {object} args Arguments
- * @param {object} args.node tree node object to retrieve the children for
- * @param {string} args.section current section alias
- */
- getChildren: function (args) {
- if (!args) {
- throw 'No args object defined for getChildren';
- }
- if (!args.node) {
- throw 'No node defined on args object for getChildren';
- }
- var section = args.section || 'content';
- var treeItem = args.node;
- var self = this;
- return treeResource.loadNodes({ node: treeItem }).then(function (data) {
- //now that we have the data, we need to add the level property to each item and the view
- self._formatNodeDataForUseInUI(treeItem, data, section, treeItem.level + 1);
- return data;
- });
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#reloadNode
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * Re-loads the single node from the server
- * @param {object} node Tree node to reload
- */
- reloadNode: function (node) {
- if (!node) {
- throw 'node cannot be null';
- }
- if (!node.parent()) {
- throw 'cannot reload a single node without a parent';
- }
- if (!node.section) {
- throw 'cannot reload a single node without an assigned node.section';
- }
- var deferred = $q.defer();
- //set the node to loading
- node.loading = true;
- this.getChildren({
- node: node.parent(),
- section: node.section
- }).then(function (data) {
- //ok, now that we have the children, find the node we're reloading
- var found = _.find(data, function (item) {
- return item.id === node.id;
- });
- if (found) {
- //now we need to find the node in the parent.children collection to replace
- var index = _.indexOf(node.parent().children, node);
- //the trick here is to not actually replace the node - this would cause the delete animations
- //to fire, instead we're just going to replace all the properties of this node.
- //there should always be a method assigned but we'll check anyways
- if (angular.isFunction(node.parent().children[index].updateNodeData)) {
- node.parent().children[index].updateNodeData(found);
- } else {
- //just update as per normal - this means styles, etc.. won't be applied
- _.extend(node.parent().children[index], found);
- }
- //set the node loading
- node.parent().children[index].loading = false;
- //return
- deferred.resolve(node.parent().children[index]);
- } else {
- deferred.reject();
- }
- }, function () {
- deferred.reject();
- });
- return deferred.promise;
- },
- /**
- * @ngdoc method
- * @name umbraco.services.treeService#getPath
- * @methodOf umbraco.services.treeService
- * @function
- *
- * @description
- * This will return the current node's path by walking up the tree
- * @param {object} node Tree node to retrieve path for
- */
- getPath: function (node) {
- if (!node) {
- throw 'node cannot be null';
- }
- if (!angular.isFunction(node.parent)) {
- throw 'node.parent is not a function, the path cannot be resolved';
- }
- //all root nodes have metadata key 'treeAlias'
- var reversePath = [];
- var current = node;
- while (current != null) {
- reversePath.push(current.id);
- if (current.metaData && current.metaData['treeAlias']) {
- current = null;
- } else {
- current = current.parent();
- }
- }
- return reversePath.reverse();
- },
- syncTree: function (args) {
- if (!args) {
- throw 'No args object defined for syncTree';
- }
- if (!args.node) {
- throw 'No node defined on args object for syncTree';
- }
- if (!args.path) {
- throw 'No path defined on args object for syncTree';
- }
- if (!angular.isArray(args.path)) {
- throw 'Path must be an array';
- }
- if (args.path.length < 1) {
- //if there is no path, make -1 the path, and that should sync the tree root
- args.path.push('-1');
- }
- var deferred = $q.defer();
- //get the rootNode for the current node, we'll sync based on that
- var root = this.getTreeRoot(args.node);
- if (!root) {
- throw 'Could not get the root tree node based on the node passed in';
- }
- //now we want to loop through the ids in the path, first we'll check if the first part
- //of the path is the root node, otherwise we'll search it's children.
- var currPathIndex = 0;
- //if the first id is the root node and there's only one... then consider it synced
- if (String(args.path[currPathIndex]).toLowerCase() === String(args.node.id).toLowerCase()) {
- if (args.path.length === 1) {
- //return the root
- deferred.resolve(root);
- return deferred.promise;
- } else {
- //move to the next path part and continue
- currPathIndex = 1;
- }
- }
- //now that we have the first id to lookup, we can start the process
- var self = this;
- var node = args.node;
- var doSync = function () {
- //check if it exists in the already loaded children
- var child = self.getChildNode(node, args.path[currPathIndex]);
- if (child) {
- if (args.path.length === currPathIndex + 1) {
- //woot! synced the node
- if (!args.forceReload) {
- deferred.resolve(child);
- } else {
- //even though we've found the node if forceReload is specified
- //we want to go update this single node from the server
- self.reloadNode(child).then(function (reloaded) {
- deferred.resolve(reloaded);
- }, function () {
- deferred.reject();
- });
- }
- } else {
- //now we need to recurse with the updated node/currPathIndex
- currPathIndex++;
- node = child;
- //recurse
- doSync();
- }
- } else {
- //couldn't find it in the
- self.loadNodeChildren({
- node: node,
- section: node.section
- }).then(function () {
- //ok, got the children, let's find it
- var found = self.getChildNode(node, args.path[currPathIndex]);
- if (found) {
- if (args.path.length === currPathIndex + 1) {
- //woot! synced the node
- deferred.resolve(found);
- } else {
- //now we need to recurse with the updated node/currPathIndex
- currPathIndex++;
- node = found;
- //recurse
- doSync();
- }
- } else {
- //fail!
- deferred.reject();
- }
- }, function () {
- //fail!
- deferred.reject();
- });
- }
- };
- //start
- doSync();
- return deferred.promise;
- }
- };
- }
- angular.module('umbraco.services').factory('treeService', treeService);
- (function () {
- 'use strict';
- /**
- * @ngdoc service
- * @name umbraco.services.umbDataFormatter
- * @description A helper object used to format/transform JSON Umbraco data, mostly used for persisting data to the server
- **/
- function umbDataFormatter() {
- return {
- formatChangePasswordModel: function (model) {
- if (!model) {
- return null;
- }
- var trimmed = _.omit(model, [
- 'confirm',
- 'generatedPassword'
- ]);
- //ensure that the pass value is null if all child properties are null
- var allNull = true;
- var vals = _.values(trimmed);
- for (var k = 0; k < vals.length; k++) {
- if (vals[k] !== null && vals[k] !== undefined) {
- allNull = false;
- }
- }
- if (allNull) {
- return null;
- }
- return trimmed;
- },
- formatContentTypePostData: function (displayModel, action) {
- //create the save model from the display model
- var saveModel = _.pick(displayModel, 'compositeContentTypes', 'isContainer', 'allowAsRoot', 'allowedTemplates', 'allowedContentTypes', 'alias', 'description', 'thumbnail', 'name', 'id', 'icon', 'trashed', 'key', 'parentId', 'alias', 'path');
- //TODO: Map these
- saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) {
- return t.alias;
- });
- saveModel.defaultTemplate = displayModel.defaultTemplate ? displayModel.defaultTemplate.alias : null;
- var realGroups = _.reject(displayModel.groups, function (g) {
- //do not include these tabs
- return g.tabState === 'init';
- });
- saveModel.groups = _.map(realGroups, function (g) {
- var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name');
- var realProperties = _.reject(g.properties, function (p) {
- //do not include these properties
- return p.propertyState === 'init' || p.inherited === true;
- });
- var saveProperties = _.map(realProperties, function (p) {
- var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile');
- return saveProperty;
- });
- saveGroup.properties = saveProperties;
- //if this is an inherited group and there are not non-inherited properties on it, then don't send up the data
- if (saveGroup.inherited === true && saveProperties.length === 0) {
- return null;
- }
- return saveGroup;
- });
- //we don't want any null groups
- saveModel.groups = _.reject(saveModel.groups, function (g) {
- return !g;
- });
- return saveModel;
- },
- /** formats the display model used to display the data type to the model used to save the data type */
- formatDataTypePostData: function (displayModel, preValues, action) {
- var saveModel = {
- parentId: displayModel.parentId,
- id: displayModel.id,
- name: displayModel.name,
- selectedEditor: displayModel.selectedEditor,
- //set the action on the save model
- action: action,
- preValues: []
- };
- for (var i = 0; i < preValues.length; i++) {
- saveModel.preValues.push({
- key: preValues[i].alias,
- value: preValues[i].value
- });
- }
- return saveModel;
- },
- /** formats the display model used to display the user to the model used to save the user */
- formatUserPostData: function (displayModel) {
- //create the save model from the display model
- var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups', 'message', 'changePassword');
- saveModel.changePassword = this.formatChangePasswordModel(saveModel.changePassword);
- //make sure the userGroups are just a string array
- var currGroups = saveModel.userGroups;
- var formattedGroups = [];
- for (var i = 0; i < currGroups.length; i++) {
- if (!angular.isString(currGroups[i])) {
- formattedGroups.push(currGroups[i].alias);
- } else {
- formattedGroups.push(currGroups[i]);
- }
- }
- saveModel.userGroups = formattedGroups;
- //make sure the startnodes are just a string array
- var props = [
- 'startContentIds',
- 'startMediaIds'
- ];
- for (var m = 0; m < props.length; m++) {
- var startIds = saveModel[props[m]];
- if (!startIds) {
- continue;
- }
- var formattedIds = [];
- for (var j = 0; j < startIds.length; j++) {
- formattedIds.push(Number(startIds[j].id));
- }
- saveModel[props[m]] = formattedIds;
- }
- return saveModel;
- },
- /** formats the display model used to display the user group to the model used to save the user group*/
- formatUserGroupPostData: function (displayModel, action) {
- //create the save model from the display model
- var saveModel = _.pick(displayModel, 'id', 'alias', 'name', 'icon', 'sections', 'users', 'defaultPermissions', 'assignedPermissions');
- // the start nodes cannot be picked as the property name needs to change - assign manually
- saveModel.startContentId = displayModel['contentStartNode'];
- saveModel.startMediaId = displayModel['mediaStartNode'];
- //set the action on the save model
- saveModel.action = action;
- if (!saveModel.id) {
- saveModel.id = 0;
- }
- //the permissions need to just be the array of permission letters, currently it will be a dictionary of an array
- var currDefaultPermissions = saveModel.defaultPermissions;
- var saveDefaultPermissions = [];
- _.each(currDefaultPermissions, function (value, key, list) {
- _.each(value, function (element, index, list) {
- if (element.checked) {
- saveDefaultPermissions.push(element.permissionCode);
- }
- });
- });
- saveModel.defaultPermissions = saveDefaultPermissions;
- //now format that assigned/content permissions
- var currAssignedPermissions = saveModel.assignedPermissions;
- var saveAssignedPermissions = {};
- _.each(currAssignedPermissions, function (nodePermissions, index) {
- saveAssignedPermissions[nodePermissions.id] = [];
- _.each(nodePermissions.allowedPermissions, function (permission, index) {
- if (permission.checked) {
- saveAssignedPermissions[nodePermissions.id].push(permission.permissionCode);
- }
- });
- });
- saveModel.assignedPermissions = saveAssignedPermissions;
- //make sure the sections are just a string array
- var currSections = saveModel.sections;
- var formattedSections = [];
- for (var i = 0; i < currSections.length; i++) {
- if (!angular.isString(currSections[i])) {
- formattedSections.push(currSections[i].alias);
- } else {
- formattedSections.push(currSections[i]);
- }
- }
- saveModel.sections = formattedSections;
- //make sure the user are just an int array
- var currUsers = saveModel.users;
- var formattedUsers = [];
- for (var j = 0; j < currUsers.length; j++) {
- if (!angular.isNumber(currUsers[j])) {
- formattedUsers.push(currUsers[j].id);
- } else {
- formattedUsers.push(currUsers[j]);
- }
- }
- saveModel.users = formattedUsers;
- //make sure the startnodes are just an int if one is set
- var props = [
- 'startContentId',
- 'startMediaId'
- ];
- for (var m = 0; m < props.length; m++) {
- var startId = saveModel[props[m]];
- if (!startId) {
- continue;
- }
- saveModel[props[m]] = startId.id;
- }
- saveModel.parentId = -1;
- return saveModel;
- },
- /** formats the display model used to display the member to the model used to save the member */
- formatMemberPostData: function (displayModel, action) {
- //this is basically the same as for media but we need to explicitly add the username,email, password to the save model
- var saveModel = this.formatMediaPostData(displayModel, action);
- saveModel.key = displayModel.key;
- var genericTab = _.find(displayModel.tabs, function (item) {
- return item.id === 0;
- });
- //map the member login, email, password and groups
- var propLogin = _.find(genericTab.properties, function (item) {
- return item.alias === '_umb_login';
- });
- var propEmail = _.find(genericTab.properties, function (item) {
- return item.alias === '_umb_email';
- });
- var propPass = _.find(genericTab.properties, function (item) {
- return item.alias === '_umb_password';
- });
- var propGroups = _.find(genericTab.properties, function (item) {
- return item.alias === '_umb_membergroup';
- });
- saveModel.email = propEmail.value.trim();
- saveModel.username = propLogin.value.trim();
- saveModel.password = this.formatChangePasswordModel(propPass.value);
- var selectedGroups = [];
- for (var n in propGroups.value) {
- if (propGroups.value[n] === true) {
- selectedGroups.push(n);
- }
- }
- saveModel.memberGroups = selectedGroups;
- //turn the dictionary into an array of pairs
- var memberProviderPropAliases = _.pairs(displayModel.fieldConfig);
- _.each(displayModel.tabs, function (tab) {
- _.each(tab.properties, function (prop) {
- var foundAlias = _.find(memberProviderPropAliases, function (item) {
- return prop.alias === item[1];
- });
- if (foundAlias) {
- //we know the current property matches an alias, now we need to determine which membership provider property it was for
- // by looking at the key
- switch (foundAlias[0]) {
- case 'umbracoMemberLockedOut':
- saveModel.isLockedOut = prop.value.toString() === '1' ? true : false;
- break;
- case 'umbracoMemberApproved':
- saveModel.isApproved = prop.value.toString() === '1' ? true : false;
- break;
- case 'umbracoMemberComments':
- saveModel.comments = prop.value;
- break;
- }
- }
- });
- });
- return saveModel;
- },
- /** formats the display model used to display the media to the model used to save the media */
- formatMediaPostData: function (displayModel, action) {
- //NOTE: the display model inherits from the save model so we can in theory just post up the display model but
- // we don't want to post all of the data as it is unecessary.
- var saveModel = {
- id: displayModel.id,
- properties: [],
- name: displayModel.name,
- contentTypeAlias: displayModel.contentTypeAlias,
- parentId: displayModel.parentId,
- //set the action on the save model
- action: action
- };
- _.each(displayModel.tabs, function (tab) {
- _.each(tab.properties, function (prop) {
- //don't include the custom generic tab properties
- if (!prop.alias.startsWith('_umb_')) {
- saveModel.properties.push({
- id: prop.id,
- alias: prop.alias,
- value: prop.value
- });
- }
- });
- });
- return saveModel;
- },
- /** formats the display model used to display the content to the model used to save the content */
- formatContentPostData: function (displayModel, action) {
- //this is basically the same as for media but we need to explicitly add some extra properties
- var saveModel = this.formatMediaPostData(displayModel, action);
- var genericTab = _.find(displayModel.tabs, function (item) {
- return item.id === 0;
- });
- var propExpireDate = _.find(genericTab.properties, function (item) {
- return item.alias === '_umb_expiredate';
- });
- var propReleaseDate = _.find(genericTab.properties, function (item) {
- return item.alias === '_umb_releasedate';
- });
- var propTemplate = _.find(genericTab.properties, function (item) {
- return item.alias === '_umb_template';
- });
- saveModel.expireDate = propExpireDate ? propExpireDate.value : null;
- saveModel.releaseDate = propReleaseDate ? propReleaseDate.value : null;
- saveModel.templateAlias = propTemplate ? propTemplate.value : null;
- return saveModel;
- }
- };
- }
- angular.module('umbraco.services').factory('umbDataFormatter', umbDataFormatter);
- }());
- /**
- * @ngdoc service
- * @name umbraco.services.umbRequestHelper
- * @description A helper object used for sending requests to the server
- **/
- function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService, eventsService) {
- return {
- /**
- * @ngdoc method
- * @name umbraco.services.umbRequestHelper#convertVirtualToAbsolutePath
- * @methodOf umbraco.services.umbRequestHelper
- * @function
- *
- * @description
- * This will convert a virtual path (i.e. ~/App_Plugins/Blah/Test.html ) to an absolute path
- *
- * @param {string} a virtual path, if this is already an absolute path it will just be returned, if this is a relative path an exception will be thrown
- */
- convertVirtualToAbsolutePath: function (virtualPath) {
- if (virtualPath.startsWith('/')) {
- return virtualPath;
- }
- if (!virtualPath.startsWith('~/')) {
- throw 'The path ' + virtualPath + ' is not a virtual path';
- }
- if (!Umbraco.Sys.ServerVariables.application.applicationPath) {
- throw 'No applicationPath defined in Umbraco.ServerVariables.application.applicationPath';
- }
- return Umbraco.Sys.ServerVariables.application.applicationPath + virtualPath.trimStart('~/');
- },
- /**
- * @ngdoc method
- * @name umbraco.services.umbRequestHelper#dictionaryToQueryString
- * @methodOf umbraco.services.umbRequestHelper
- * @function
- *
- * @description
- * This will turn an array of key/value pairs or a standard dictionary into a query string
- *
- * @param {Array} queryStrings An array of key/value pairs
- */
- dictionaryToQueryString: function (queryStrings) {
- if (angular.isArray(queryStrings)) {
- return _.map(queryStrings, function (item) {
- var key = null;
- var val = null;
- for (var k in item) {
- key = k;
- val = item[k];
- break;
- }
- if (key === null || val === null) {
- throw 'The object in the array was not formatted as a key/value pair';
- }
- return encodeURIComponent(key) + '=' + encodeURIComponent(val);
- }).join('&');
- } else if (angular.isObject(queryStrings)) {
- //this allows for a normal object to be passed in (ie. a dictionary)
- return decodeURIComponent($.param(queryStrings));
- }
- throw 'The queryString parameter is not an array or object of key value pairs';
- },
- /**
- * @ngdoc method
- * @name umbraco.services.umbRequestHelper#getApiUrl
- * @methodOf umbraco.services.umbRequestHelper
- * @function
- *
- * @description
- * This will return the webapi Url for the requested key based on the servervariables collection
- *
- * @param {string} apiName The webapi name that is found in the servervariables["umbracoUrls"] dictionary
- * @param {string} actionName The webapi action name
- * @param {object} queryStrings Can be either a string or an array containing key/value pairs
- */
- getApiUrl: function (apiName, actionName, queryStrings) {
- if (!Umbraco || !Umbraco.Sys || !Umbraco.Sys.ServerVariables || !Umbraco.Sys.ServerVariables['umbracoUrls']) {
- throw 'No server variables defined!';
- }
- if (!Umbraco.Sys.ServerVariables['umbracoUrls'][apiName]) {
- throw 'No url found for api name ' + apiName;
- }
- return Umbraco.Sys.ServerVariables['umbracoUrls'][apiName] + actionName + (!queryStrings ? '' : '?' + (angular.isString(queryStrings) ? queryStrings : this.dictionaryToQueryString(queryStrings)));
- },
- /**
- * @ngdoc function
- * @name umbraco.services.umbRequestHelper#resourcePromise
- * @methodOf umbraco.services.umbRequestHelper
- * @function
- *
- * @description
- * This returns a promise with an underlying http call, it is a helper method to reduce
- * the amount of duplicate code needed to query http resources and automatically handle any
- * Http errors. See /docs/source/using-promises-resources.md
- *
- * @param {object} opts A mixed object which can either be a string representing the error message to be
- * returned OR an object containing either:
- * { success: successCallback, errorMsg: errorMessage }
- * OR
- * { success: successCallback, error: errorCallback }
- * In both of the above, the successCallback must accept these parameters: data, status, headers, config
- * If using the errorCallback it must accept these parameters: data, status, headers, config
- * The success callback must return the data which will be resolved by the deferred object.
- * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status }
- */
- resourcePromise: function (httpPromise, opts) {
- var deferred = $q.defer();
- /** The default success callback used if one is not supplied in the opts */
- function defaultSuccess(data, status, headers, config) {
- //when it's successful, just return the data
- return data;
- }
- /** The default error callback used if one is not supplied in the opts */
- function defaultError(data, status, headers, config) {
- return {
- //NOTE: the default error message here should never be used based on the above docs!
- errorMsg: angular.isString(opts) ? opts : 'An error occurred!',
- data: data,
- status: status
- };
- }
- //create the callbacs based on whats been passed in.
- var callbacks = {
- success: !opts || !opts.success ? defaultSuccess : opts.success,
- error: !opts || !opts.error ? defaultError : opts.error
- };
- httpPromise.success(function (data, status, headers, config) {
- //invoke the callback
- var result = callbacks.success.apply(this, [
- data,
- status,
- headers,
- config
- ]);
- //when it's successful, just return the data
- deferred.resolve(result);
- }).error(function (data, status, headers, config) {
- //invoke the callback
- var result = callbacks.error.apply(this, [
- data,
- status,
- headers,
- config
- ]);
- //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled.
- if (status >= 500 && status < 600) {
- //show a ysod dialog
- if (Umbraco.Sys.ServerVariables['isDebuggingEnabled'] === true) {
- eventsService.emit('app.ysod', {
- errorMsg: 'An error occured',
- data: data
- });
- } else {
- //show a simple error notification
- notificationsService.error('Server error', 'Contact administrator, see log for full details.<br/><i>' + result.errorMsg + '</i>');
- }
- }
- //return an error object including the error message for UI
- deferred.reject({
- errorMsg: result.errorMsg,
- data: result.data,
- status: result.status
- });
- });
- return deferred.promise;
- },
- /** Used for saving media/content specifically */
- postSaveContent: function (args) {
- if (!args.restApiUrl) {
- throw 'args.restApiUrl is a required argument';
- }
- if (!args.content) {
- throw 'args.content is a required argument';
- }
- if (!args.action) {
- throw 'args.action is a required argument';
- }
- if (!args.files) {
- throw 'args.files is a required argument';
- }
- if (!args.dataFormatter) {
- throw 'args.dataFormatter is a required argument';
- }
- var deferred = $q.defer();
- //save the active tab id so we can set it when the data is returned.
- var activeTab = _.find(args.content.tabs, function (item) {
- return item.active;
- });
- var activeTabIndex = activeTab === undefined ? 0 : _.indexOf(args.content.tabs, activeTab);
- //save the data
- this.postMultiPartRequest(args.restApiUrl, {
- key: 'contentItem',
- value: args.dataFormatter(args.content, args.action)
- }, function (data, formData) {
- //now add all of the assigned files
- for (var f in args.files) {
- //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key
- // so we know which property it belongs to on the server side
- formData.append('file_' + args.files[f].alias, args.files[f].file);
- }
- }, function (data, status, headers, config) {
- //success callback
- //reset the tabs and set the active one
- _.each(data.tabs, function (item) {
- item.active = false;
- });
- data.tabs[activeTabIndex].active = true;
- //the data returned is the up-to-date data so the UI will refresh
- deferred.resolve(data);
- }, function (data, status, headers, config) {
- //failure callback
- //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled.
- if (status >= 500 && status < 600) {
- //This is a bit of a hack to check if the error is due to a file being uploaded that is too large,
- // we have to just check for the existence of a string value but currently that is the best way to
- // do this since it's very hacky/difficult to catch this on the server
- if (typeof data !== 'undefined' && typeof data.indexOf === 'function' && data.indexOf('Maximum request length exceeded') >= 0) {
- notificationsService.error('Server error', 'The uploaded file was too large, check with your site administrator to adjust the maximum size allowed');
- } else if (Umbraco.Sys.ServerVariables['isDebuggingEnabled'] === true) {
- //show a ysod dialog
- eventsService.emit('app.ysod', {
- errorMsg: 'An error occured',
- data: data
- });
- } else {
- //show a simple error notification
- notificationsService.error('Server error', 'Contact administrator, see log for full details.<br/><i>' + data.ExceptionMessage + '</i>');
- }
- }
- //return an error object including the error message for UI
- deferred.reject({
- errorMsg: 'An error occurred',
- data: data,
- status: status
- });
- });
- return deferred.promise;
- },
- /** Posts a multi-part mime request to the server */
- postMultiPartRequest: function (url, jsonData, transformCallback, successCallback, failureCallback) {
- //validate input, jsonData can be an array of key/value pairs or just one key/value pair.
- if (!jsonData) {
- throw 'jsonData cannot be null';
- }
- if (angular.isArray(jsonData)) {
- _.each(jsonData, function (item) {
- if (!item.key || !item.value) {
- throw 'jsonData array item must have both a key and a value property';
- }
- });
- } else if (!jsonData.key || !jsonData.value) {
- throw 'jsonData object must have both a key and a value property';
- }
- $http({
- method: 'POST',
- url: url,
- //IMPORTANT!!! You might think this should be set to 'multipart/form-data' but this is not true because when we are sending up files
- // the request needs to include a 'boundary' parameter which identifies the boundary name between parts in this multi-part request
- // and setting the Content-type manually will not set this boundary parameter. For whatever reason, setting the Content-type to 'false'
- // will force the request to automatically populate the headers properly including the boundary parameter.
- headers: { 'Content-Type': false },
- transformRequest: function (data) {
- var formData = new FormData();
- //add the json data
- if (angular.isArray(data)) {
- _.each(data, function (item) {
- formData.append(item.key, !angular.isString(item.value) ? angular.toJson(item.value) : item.value);
- });
- } else {
- formData.append(data.key, !angular.isString(data.value) ? angular.toJson(data.value) : data.value);
- }
- //call the callback
- if (transformCallback) {
- transformCallback.apply(this, [
- data,
- formData
- ]);
- }
- return formData;
- },
- data: jsonData
- }).success(function (data, status, headers, config) {
- if (successCallback) {
- successCallback.apply(this, [
- data,
- status,
- headers,
- config
- ]);
- }
- }).error(function (data, status, headers, config) {
- if (failureCallback) {
- failureCallback.apply(this, [
- data,
- status,
- headers,
- config
- ]);
- }
- });
- }
- };
- }
- angular.module('umbraco.services').factory('umbRequestHelper', umbRequestHelper);
- angular.module('umbraco.services').factory('userService', function ($rootScope, eventsService, $q, $location, $log, securityRetryQueue, authResource, dialogService, $timeout, angularHelper, $http) {
- var currentUser = null;
- var lastUserId = null;
- var loginDialog = null;
- //this tracks the last date/time that the user's remainingAuthSeconds was updated from the server
- // this is used so that we know when to go and get the user's remaining seconds directly.
- var lastServerTimeoutSet = null;
- function openLoginDialog(isTimedOut) {
- if (!loginDialog) {
- loginDialog = dialogService.open({
- //very special flag which means that global events cannot close this dialog
- manualClose: true,
- template: 'views/common/dialogs/login.html',
- modalClass: 'login-overlay',
- animation: 'slide',
- show: true,
- callback: onLoginDialogClose,
- dialogData: { isTimedOut: isTimedOut }
- });
- }
- }
- function onLoginDialogClose(success) {
- loginDialog = null;
- if (success) {
- securityRetryQueue.retryAll(currentUser.name);
- } else {
- securityRetryQueue.cancelAll();
- $location.path('/');
- }
- }
- /**
- This methods will set the current user when it is resolved and
- will then start the counter to count in-memory how many seconds they have
- remaining on the auth session
- */
- function setCurrentUser(usr) {
- if (!usr.remainingAuthSeconds) {
- throw 'The user object is invalid, the remainingAuthSeconds is required.';
- }
- currentUser = usr;
- lastServerTimeoutSet = new Date();
- //start the timer
- countdownUserTimeout();
- }
- /**
- Method to count down the current user's timeout seconds,
- this will continually count down their current remaining seconds every 5 seconds until
- there are no more seconds remaining.
- */
- function countdownUserTimeout() {
- $timeout(function () {
- if (currentUser) {
- //countdown by 5 seconds since that is how long our timer is for.
- currentUser.remainingAuthSeconds -= 5;
- //if there are more than 30 remaining seconds, recurse!
- if (currentUser.remainingAuthSeconds > 30) {
- //we need to check when the last time the timeout was set from the server, if
- // it has been more than 30 seconds then we'll manually go and retrieve it from the
- // server - this helps to keep our local countdown in check with the true timeout.
- if (lastServerTimeoutSet != null) {
- var now = new Date();
- var seconds = (now.getTime() - lastServerTimeoutSet.getTime()) / 1000;
- if (seconds > 30) {
- //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
- // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
- lastServerTimeoutSet = null;
- //now go get it from the server
- //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
- angularHelper.safeApply($rootScope, function () {
- authResource.getRemainingTimeoutSeconds().then(function (result) {
- setUserTimeoutInternal(result);
- });
- });
- }
- }
- //recurse the countdown!
- countdownUserTimeout();
- } else {
- //we are either timed out or very close to timing out so we need to show the login dialog.
- if (Umbraco.Sys.ServerVariables.umbracoSettings.keepUserLoggedIn !== true) {
- //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
- angularHelper.safeApply($rootScope, function () {
- try {
- //NOTE: We are calling this again so that the server can create a log that the timeout has expired, we
- // don't actually care about this result.
- authResource.getRemainingTimeoutSeconds();
- } finally {
- userAuthExpired();
- }
- });
- } else {
- //we've got less than 30 seconds remaining so let's check the server
- if (lastServerTimeoutSet != null) {
- //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
- // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
- lastServerTimeoutSet = null;
- //now go get it from the server
- //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
- angularHelper.safeApply($rootScope, function () {
- authResource.getRemainingTimeoutSeconds().then(function (result) {
- setUserTimeoutInternal(result);
- });
- });
- }
- //recurse the countdown!
- countdownUserTimeout();
- }
- }
- }
- }, 5000, //every 5 seconds
- false); //false = do NOT execute a digest for every iteration
- }
- /** Called to update the current user's timeout */
- function setUserTimeoutInternal(newTimeout) {
- var asNumber = parseFloat(newTimeout);
- if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) {
- currentUser.remainingAuthSeconds = newTimeout;
- lastServerTimeoutSet = new Date();
- }
- }
- /** resets all user data, broadcasts the notAuthenticated event and shows the login dialog */
- function userAuthExpired(isLogout) {
- //store the last user id and clear the user
- if (currentUser && currentUser.id !== undefined) {
- lastUserId = currentUser.id;
- }
- if (currentUser) {
- currentUser.remainingAuthSeconds = 0;
- }
- lastServerTimeoutSet = null;
- currentUser = null;
- //broadcast a global event that the user is no longer logged in
- eventsService.emit('app.notAuthenticated');
- openLoginDialog(isLogout === undefined ? true : !isLogout);
- }
- // Register a handler for when an item is added to the retry queue
- securityRetryQueue.onItemAddedCallbacks.push(function (retryItem) {
- if (securityRetryQueue.hasMore()) {
- userAuthExpired();
- }
- });
- return {
- /** Internal method to display the login dialog */
- _showLoginDialog: function () {
- openLoginDialog();
- },
- /** Returns a promise, sends a request to the server to check if the current cookie is authorized */
- isAuthenticated: function () {
- //if we've got a current user then just return true
- if (currentUser) {
- var deferred = $q.defer();
- deferred.resolve(true);
- return deferred.promise;
- }
- return authResource.isAuthenticated();
- },
- /** Returns a promise, sends a request to the server to validate the credentials */
- authenticate: function (login, password) {
- return authResource.performLogin(login, password).then(this.setAuthenticationSuccessful);
- },
- setAuthenticationSuccessful: function (data) {
- //when it's successful, return the user data
- setCurrentUser(data);
- var result = {
- user: data,
- authenticated: true,
- lastUserId: lastUserId,
- loginType: 'credentials'
- };
- //broadcast a global event
- eventsService.emit('app.authenticated', result);
- return result;
- },
- /** Logs the user out
- */
- logout: function () {
- return authResource.performLogout().then(function (data) {
- userAuthExpired();
- //done!
- return null;
- });
- },
- /** Refreshes the current user data with the data stored for the user on the server and returns it */
- refreshCurrentUser: function () {
- var deferred = $q.defer();
- authResource.getCurrentUser().then(function (data) {
- var result = {
- user: data,
- authenticated: true,
- lastUserId: lastUserId,
- loginType: 'implicit'
- };
- setCurrentUser(data);
- deferred.resolve(currentUser);
- }, function () {
- //it failed, so they are not logged in
- deferred.reject();
- });
- return deferred.promise;
- },
- /** Returns the current user object in a promise */
- getCurrentUser: function (args) {
- var deferred = $q.defer();
- if (!currentUser) {
- authResource.getCurrentUser().then(function (data) {
- var result = {
- user: data,
- authenticated: true,
- lastUserId: lastUserId,
- loginType: 'implicit'
- };
- if (args && args.broadcastEvent) {
- //broadcast a global event, will inform listening controllers to load in the user specific data
- eventsService.emit('app.authenticated', result);
- }
- setCurrentUser(data);
- deferred.resolve(currentUser);
- }, function () {
- //it failed, so they are not logged in
- deferred.reject();
- });
- } else {
- deferred.resolve(currentUser);
- }
- return deferred.promise;
- },
- /** Called whenever a server request is made that contains a x-umb-user-seconds response header for which we can update the user's remaining timeout seconds */
- setUserTimeout: function (newTimeout) {
- setUserTimeoutInternal(newTimeout);
- }
- };
- });
- (function () {
- 'use strict';
- function usersHelperService(localizationService) {
- var userStates = [
- {
- 'name': 'All',
- 'key': 'All'
- },
- {
- 'value': 0,
- 'name': 'Active',
- 'key': 'Active',
- 'color': 'success'
- },
- {
- 'value': 1,
- 'name': 'Disabled',
- 'key': 'Disabled',
- 'color': 'danger'
- },
- {
- 'value': 2,
- 'name': 'Locked out',
- 'key': 'LockedOut',
- 'color': 'danger'
- },
- {
- 'value': 3,
- 'name': 'Invited',
- 'key': 'Invited',
- 'color': 'warning'
- }
- ];
- angular.forEach(userStates, function (userState) {
- var key = 'user_state' + userState.key;
- localizationService.localize(key).then(function (value) {
- var reg = /^\[[\S\s]*]$/g;
- var result = reg.test(value);
- if (result === false) {
- // Only translate if key exists
- userState.name = value;
- }
- });
- });
- function getUserStateFromValue(value) {
- var foundUserState;
- angular.forEach(userStates, function (userState) {
- if (userState.value === value) {
- foundUserState = userState;
- }
- });
- return foundUserState;
- }
- function getUserStateByKey(key) {
- var foundUserState;
- angular.forEach(userStates, function (userState) {
- if (userState.key === key) {
- foundUserState = userState;
- }
- });
- return foundUserState;
- }
- function getUserStatesFilter(userStatesObject) {
- var userStatesFilter = [];
- for (var key in userStatesObject) {
- if (userStatesObject.hasOwnProperty(key)) {
- var userState = getUserStateByKey(key);
- if (userState) {
- userState.count = userStatesObject[key];
- userStatesFilter.push(userState);
- }
- }
- }
- return userStatesFilter;
- }
- ////////////
- var service = {
- getUserStateFromValue: getUserStateFromValue,
- getUserStateByKey: getUserStateByKey,
- getUserStatesFilter: getUserStatesFilter
- };
- return service;
- }
- angular.module('umbraco.services').factory('usersHelper', usersHelperService);
- }());
- /*Contains multiple services for various helper tasks */
- function versionHelper() {
- return {
- //see: https://gist.github.com/TheDistantSea/8021359
- versionCompare: function (v1, v2, options) {
- var lexicographical = options && options.lexicographical, zeroExtend = options && options.zeroExtend, v1parts = v1.split('.'), v2parts = v2.split('.');
- function isValidPart(x) {
- return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
- }
- if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
- return NaN;
- }
- if (zeroExtend) {
- while (v1parts.length < v2parts.length) {
- v1parts.push('0');
- }
- while (v2parts.length < v1parts.length) {
- v2parts.push('0');
- }
- }
- if (!lexicographical) {
- v1parts = v1parts.map(Number);
- v2parts = v2parts.map(Number);
- }
- for (var i = 0; i < v1parts.length; ++i) {
- if (v2parts.length === i) {
- return 1;
- }
- if (v1parts[i] === v2parts[i]) {
- continue;
- } else if (v1parts[i] > v2parts[i]) {
- return 1;
- } else {
- return -1;
- }
- }
- if (v1parts.length !== v2parts.length) {
- return -1;
- }
- return 0;
- }
- };
- }
- angular.module('umbraco.services').factory('versionHelper', versionHelper);
- function dateHelper() {
- return {
- convertToServerStringTime: function (momentLocal, serverOffsetMinutes, format) {
- //get the formatted offset time in HH:mm (server time offset is in minutes)
- var formattedOffset = (serverOffsetMinutes > 0 ? '+' : '-') + moment().startOf('day').minutes(Math.abs(serverOffsetMinutes)).format('HH:mm');
- var server = moment.utc(momentLocal).utcOffset(formattedOffset);
- return server.format(format ? format : 'YYYY-MM-DD HH:mm:ss');
- },
- convertToLocalMomentTime: function (strVal, serverOffsetMinutes) {
- //get the formatted offset time in HH:mm (server time offset is in minutes)
- var formattedOffset = (serverOffsetMinutes > 0 ? '+' : '-') + moment().startOf('day').minutes(Math.abs(serverOffsetMinutes)).format('HH:mm');
- //convert to the iso string format
- var isoFormat = moment(strVal).format('YYYY-MM-DDTHH:mm:ss') + formattedOffset;
- //create a moment with the iso format which will include the offset with the correct time
- // then convert it to local time
- return moment.parseZone(isoFormat).local();
- }
- };
- }
- angular.module('umbraco.services').factory('dateHelper', dateHelper);
- function packageHelper(assetsService, treeService, eventsService, $templateCache) {
- return {
- /** Called when a package is installed, this resets a bunch of data and ensures the new package assets are loaded in */
- packageInstalled: function () {
- //clears the tree
- treeService.clearCache();
- //clears the template cache
- $templateCache.removeAll();
- //emit event to notify anything else
- eventsService.emit('app.reInitialize');
- }
- };
- }
- angular.module('umbraco.services').factory('packageHelper', packageHelper);
- //TODO: I believe this is obsolete
- function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) {
- return {
- /** sets the image's url, thumbnail and if its a folder */
- setImageData: function (img) {
- img.isFolder = !mediaHelper.hasFilePropertyType(img);
- if (!img.isFolder) {
- img.thumbnail = mediaHelper.resolveFile(img, true);
- img.image = mediaHelper.resolveFile(img, false);
- }
- },
- /** sets the images original size properties - will check if it is a folder and if so will just make it square */
- setOriginalSize: function (img, maxHeight) {
- //set to a square by default
- img.originalWidth = maxHeight;
- img.originalHeight = maxHeight;
- var widthProp = _.find(img.properties, function (v) {
- return v.alias === 'umbracoWidth';
- });
- if (widthProp && widthProp.value) {
- img.originalWidth = parseInt(widthProp.value, 10);
- if (isNaN(img.originalWidth)) {
- img.originalWidth = maxHeight;
- }
- }
- var heightProp = _.find(img.properties, function (v) {
- return v.alias === 'umbracoHeight';
- });
- if (heightProp && heightProp.value) {
- img.originalHeight = parseInt(heightProp.value, 10);
- if (isNaN(img.originalHeight)) {
- img.originalHeight = maxHeight;
- }
- }
- },
- /** sets the image style which get's used in the angular markup */
- setImageStyle: function (img, width, height, rightMargin, bottomMargin) {
- img.style = {
- width: width + 'px',
- height: height + 'px',
- 'margin-right': rightMargin + 'px',
- 'margin-bottom': bottomMargin + 'px'
- };
- img.thumbStyle = {
- 'background-image': 'url(\'' + img.thumbnail + '\')',
- 'background-repeat': 'no-repeat',
- 'background-position': 'center',
- 'background-size': Math.min(width, img.originalWidth) + 'px ' + Math.min(height, img.originalHeight) + 'px'
- };
- },
- /** gets the image's scaled wdith based on the max row height */
- getScaledWidth: function (img, maxHeight) {
- var scaled = img.originalWidth * maxHeight / img.originalHeight;
- return scaled; //round down, we don't want it too big even by half a pixel otherwise it'll drop to the next row
- //return Math.floor(scaled);
- },
- /** returns the target row width taking into account how many images will be in the row and removing what the margin is */
- getTargetWidth: function (imgsPerRow, maxRowWidth, margin) {
- //take into account the margin, we will have 1 less margin item than we have total images
- return maxRowWidth - (imgsPerRow - 1) * margin;
- },
- /**
- This will determine the row/image height for the next collection of images which takes into account the
- ideal image count per row. It will check if a row can be filled with this ideal count and if not - if there
- are additional images available to fill the row it will keep calculating until they fit.
- It will return the calculated height and the number of images for the row.
- targetHeight = optional;
- */
- getRowHeightForImages: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) {
- var idealImages = imgs.slice(0, idealImgPerRow);
- //get the target row width without margin
- var targetRowWidth = this.getTargetWidth(idealImages.length, maxRowWidth, margin);
- //this gets the image with the smallest height which equals the maximum we can scale up for this image block
- var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight);
- //if the max scale height is smaller than the min display height, we'll use the min display height
- targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight);
- var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight);
- if (attemptedRowHeight != null) {
- //if this is smaller than the min display then we need to use the min display,
- // which means we'll need to remove one from the row so we can scale up to fill the row
- if (attemptedRowHeight < minDisplayHeight) {
- if (idealImages.length > 1) {
- //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight
- targetHeight += Math.floor((maxRowHeight - targetHeight) / 2);
- return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight);
- } else {
- //this will occur when we only have one image remaining in the row but it's still going to be too wide even when
- // using the minimum display height specified. In this case we're going to have to just crop the image in it's center
- // using the minimum display height and the full row width
- return {
- height: minDisplayHeight,
- imgCount: 1
- };
- }
- } else {
- //success!
- return {
- height: attemptedRowHeight,
- imgCount: idealImages.length
- };
- }
- }
- //we know the width will fit in a row, but we now need to figure out if we can fill
- // the entire row in the case that we have more images remaining than the idealImgPerRow.
- if (idealImages.length === imgs.length) {
- //we have no more remaining images to fill the space, so we'll just use the calc height
- return {
- height: targetHeight,
- imgCount: idealImages.length
- };
- } else if (idealImages.length === 1) {
- //this will occur when we only have one image remaining in the row to process but it's not really going to fit ideally
- // in the row.
- return {
- height: minDisplayHeight,
- imgCount: 1
- };
- } else if (idealImages.length === idealImgPerRow && targetHeight < maxRowHeight) {
- //if we're already dealing with the ideal images per row and it's not quite wide enough, we can scale up a little bit so
- // long as the targetHeight is currently less than the maxRowHeight. The scale up will be half-way between our current
- // target height and the maxRowHeight (we won't loop forever though - if there's a difference of 5 px we'll just quit)
- while (targetHeight < maxRowHeight && maxRowHeight - targetHeight > 5) {
- targetHeight += Math.floor((maxRowHeight - targetHeight) / 2);
- attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight);
- if (attemptedRowHeight != null) {
- //success!
- return {
- height: attemptedRowHeight,
- imgCount: idealImages.length
- };
- }
- }
- //Ok, we couldn't actually scale it up with the ideal row count we'll just recurse with a lesser image count.
- return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin);
- } else if (targetHeight === maxRowHeight) {
- //This is going to happen when:
- // * We can fit a list of images in a row, but they come up too short (based on minDisplayHeight)
- // * Then we'll try to remove an image, but when we try to scale to fit, the width comes up too narrow but the images are already at their
- // maximum height (maxRowHeight)
- // * So we're stuck, we cannot precicely fit the current list of images, so we'll render a row that will be max height but won't be wide enough
- // which is better than rendering a row that is shorter than the minimum since that could be quite small.
- return {
- height: targetHeight,
- imgCount: idealImages.length
- };
- } else {
- //we have additional images so we'll recurse and add 1 to the idealImgPerRow until it fits
- return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow + 1, margin);
- }
- },
- performGetRowHeight: function (idealImages, targetRowWidth, minDisplayHeight, targetHeight) {
- var currRowWidth = 0;
- for (var i = 0; i < idealImages.length; i++) {
- var scaledW = this.getScaledWidth(idealImages[i], targetHeight);
- currRowWidth += scaledW;
- }
- if (currRowWidth > targetRowWidth) {
- //get the new scaled height to fit
- var newHeight = targetRowWidth * targetHeight / currRowWidth;
- return newHeight;
- } else if (idealImages.length === 1 && currRowWidth <= targetRowWidth && !idealImages[0].isFolder) {
- //if there is only one image, then return the target height
- return targetHeight;
- } else if (currRowWidth / targetRowWidth > 0.9) {
- //it's close enough, it's at least 90% of the width so we'll accept it with the target height
- return targetHeight;
- } else {
- //if it's not successful, return null
- return null;
- }
- },
- /** builds an image grid row */
- buildRow: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) {
- var currRowWidth = 0;
- var row = { images: [] };
- var imageRowHeight = this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin);
- var targetWidth = this.getTargetWidth(imageRowHeight.imgCount, maxRowWidth, margin);
- var sizes = [];
- //loop through the images we know fit into the height
- for (var i = 0; i < imageRowHeight.imgCount; i++) {
- //get the lower width to ensure it always fits
- var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height));
- if (currRowWidth + scaledWidth <= targetWidth) {
- currRowWidth += scaledWidth;
- sizes.push({
- width: scaledWidth,
- //ensure that the height is rounded
- height: Math.round(imageRowHeight.height)
- });
- row.images.push(imgs[i]);
- } else if (imageRowHeight.imgCount === 1 && row.images.length === 0) {
- //the image is simply too wide, we'll crop/center it
- sizes.push({
- width: maxRowWidth,
- //ensure that the height is rounded
- height: Math.round(imageRowHeight.height)
- });
- row.images.push(imgs[i]);
- } else {
- //the max width has been reached
- break;
- }
- }
- //loop through the images for the row and apply the styles
- for (var j = 0; j < row.images.length; j++) {
- var bottomMargin = margin;
- //make the margin 0 for the last one
- if (j === row.images.length - 1) {
- margin = 0;
- }
- this.setImageStyle(row.images[j], sizes[j].width, sizes[j].height, margin, bottomMargin);
- }
- if (row.images.length === 1 && totalRemaining > 1) {
- //if there's only one image on the row and there are more images remaining, set the container to max width
- row.images[0].style.width = maxRowWidth + 'px';
- }
- return row;
- },
- /** Returns the maximum image scaling height for the current image collection */
- getMaxScaleableHeight: function (imgs, maxRowHeight) {
- var smallestHeight = _.min(imgs, function (item) {
- return item.originalHeight;
- }).originalHeight;
- //adjust the smallestHeight if it is larger than the static max row height
- if (smallestHeight > maxRowHeight) {
- smallestHeight = maxRowHeight;
- }
- return smallestHeight;
- },
- /** Creates the image grid with calculated widths/heights for images to fill the grid nicely */
- buildGrid: function (images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin, imagesOnly) {
- var rows = [];
- var imagesProcessed = 0;
- //first fill in all of the original image sizes and URLs
- for (var i = startingIndex; i < images.length; i++) {
- var item = images[i];
- this.setImageData(item);
- this.setOriginalSize(item, maxRowHeight);
- if (imagesOnly && !item.isFolder && !item.thumbnail) {
- images.splice(i, 1);
- i--;
- }
- }
- while (imagesProcessed + startingIndex < images.length) {
- //get the maxHeight for the current un-processed images
- var currImgs = images.slice(imagesProcessed);
- //build the row
- var remaining = images.length - imagesProcessed;
- var row = this.buildRow(currImgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, remaining);
- if (row.images.length > 0) {
- rows.push(row);
- imagesProcessed += row.images.length;
- } else {
- if (currImgs.length > 0) {
- throw 'Could not fill grid with all images, images remaining: ' + currImgs.length;
- }
- //if there was nothing processed, exit
- break;
- }
- }
- return rows;
- }
- };
- }
- angular.module('umbraco.services').factory('umbPhotoFolderHelper', umbPhotoFolderHelper);
- /**
- * @ngdoc function
- * @name umbraco.services.umbModelMapper
- * @function
- *
- * @description
- * Utility class to map/convert models
- */
- function umbModelMapper() {
- return {
- /**
- * @ngdoc function
- * @name umbraco.services.umbModelMapper#convertToEntityBasic
- * @methodOf umbraco.services.umbModelMapper
- * @function
- *
- * @description
- * Converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model.
- * @param {Object} source The source model
- * @param {Number} source.id The node id of the model
- * @param {String} source.name The node name
- * @param {String} source.icon The models icon as a css class (.icon-doc)
- * @param {Number} source.parentId The parentID, if no parent, set to -1
- * @param {path} source.path comma-separated string of ancestor IDs (-1,1234,1782,1234)
- */
- /** This converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model */
- convertToEntityBasic: function (source) {
- var required = [
- 'id',
- 'name',
- 'icon',
- 'parentId',
- 'path'
- ];
- _.each(required, function (k) {
- if (!_.has(source, k)) {
- throw 'The source object does not contain the property ' + k;
- }
- });
- var optional = [
- 'metaData',
- 'key',
- 'alias'
- ];
- //now get the basic object
- var result = _.pick(source, required.concat(optional));
- return result;
- }
- };
- }
- angular.module('umbraco.services').factory('umbModelMapper', umbModelMapper);
- /**
- * @ngdoc function
- * @name umbraco.services.umbSessionStorage
- * @function
- *
- * @description
- * Used to get/set things in browser sessionStorage but always prefixes keys with "umb_" and converts json vals so there is no overlap
- * with any sessionStorage created by a developer.
- */
- function umbSessionStorage($window) {
- //gets the sessionStorage object if available, otherwise just uses a normal object
- // - required for unit tests.
- var storage = $window['sessionStorage'] ? $window['sessionStorage'] : {};
- return {
- get: function (key) {
- return angular.fromJson(storage['umb_' + key]);
- },
- set: function (key, value) {
- storage['umb_' + key] = angular.toJson(value);
- }
- };
- }
- angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorage);
- /**
- * @ngdoc function
- * @name umbraco.services.updateChecker
- * @function
- *
- * @description
- * used to check for updates and display a notifcation
- */
- function updateChecker($http, umbRequestHelper) {
- return {
- /**
- * @ngdoc function
- * @name umbraco.services.updateChecker#check
- * @methodOf umbraco.services.updateChecker
- * @function
- *
- * @description
- * Called to load in the legacy tree js which is required on startup if a user is logged in or
- * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded.
- */
- check: function () {
- return umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('updateCheckApiBaseUrl', 'GetCheck')), 'Failed to retrieve update status');
- }
- };
- }
- angular.module('umbraco.services').factory('updateChecker', updateChecker);
- /**
- * @ngdoc service
- * @name umbraco.services.umbPropertyEditorHelper
- * @description A helper object used for property editors
- **/
- function umbPropEditorHelper() {
- return {
- /**
- * @ngdoc function
- * @name getImagePropertyValue
- * @methodOf umbraco.services.umbPropertyEditorHelper
- * @function
- *
- * @description
- * Returns the correct view path for a property editor, it will detect if it is a full virtual path but if not then default to the internal umbraco one
- *
- * @param {string} input the view path currently stored for the property editor
- */
- getViewPath: function (input, isPreValue) {
- var path = String(input);
- if (path.startsWith('/')) {
- //This is an absolute path, so just leave it
- return path;
- } else {
- if (path.indexOf('/') >= 0) {
- //This is a relative path, so just leave it
- return path;
- } else {
- if (!isPreValue) {
- //i.e. views/propertyeditors/fileupload/fileupload.html
- return 'views/propertyeditors/' + path + '/' + path + '.html';
- } else {
- //i.e. views/prevalueeditors/requiredfield.html
- return 'views/prevalueeditors/' + path + '.html';
- }
- }
- }
- }
- };
- }
- angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorHelper);
- /**
- * @ngdoc service
- * @name umbraco.services.queryStrings
- * @description A helper used to get query strings in the real URL (not the hash URL)
- **/
- function queryStrings($window) {
- var pl = /\+/g;
- // Regex for replacing addition symbol with a space
- var search = /([^&=]+)=?([^&]*)/g;
- var decode = function (s) {
- return decodeURIComponent(s.replace(pl, ' '));
- };
- return {
- getParams: function () {
- var match;
- var query = $window.location.search.substring(1);
- var urlParams = {};
- while (match = search.exec(query)) {
- urlParams[decode(match[1])] = decode(match[2]);
- }
- return urlParams;
- }
- };
- }
- angular.module('umbraco.services').factory('queryStrings', queryStrings);
- /**
- * @ngdoc service
- * @name umbraco.services.windowResizeListener
- * @function
- *
- * @description
- * A single window resize listener... we don't want to have more than one in theory to ensure that
- * there aren't too many events raised. This will debounce the event with 100 ms intervals and force
- * a $rootScope.$apply when changed and notify all listeners
- *
- */
- function windowResizeListener($rootScope) {
- var WinReszier = function () {
- var registered = [];
- var inited = false;
- var resize = _.debounce(function (ev) {
- notify();
- }, 100);
- var notify = function () {
- var h = $(window).height();
- var w = $(window).width();
- //execute all registrations inside of a digest
- $rootScope.$apply(function () {
- for (var i = 0, cnt = registered.length; i < cnt; i++) {
- registered[i].apply($(window), [{
- width: w,
- height: h
- }]);
- }
- });
- };
- return {
- register: function (fn) {
- registered.push(fn);
- if (inited === false) {
- $(window).bind('resize', resize);
- inited = true;
- }
- },
- unregister: function (fn) {
- var index = registered.indexOf(fn);
- if (index > -1) {
- registered.splice(index, 1);
- }
- }
- };
- }();
- return {
- /**
- * Register a callback for resizing
- * @param {Function} cb
- */
- register: function (cb) {
- WinReszier.register(cb);
- },
- /**
- * Removes a registered callback
- * @param {Function} cb
- */
- unregister: function (cb) {
- WinReszier.unregister(cb);
- }
- };
- }
- angular.module('umbraco.services').factory('windowResizeListener', windowResizeListener);
- /**
- * @ngdoc service
- * @name umbraco.services.xmlhelper
- * @function
- *
- * @description
- * Used to convert legacy xml data to json and back again
- */
- function xmlhelper($http) {
- /*
- Copyright 2011 Abdulla Abdurakhmanov
- Original sources are available at https://code.google.com/p/x2js/
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- function X2JS() {
- var VERSION = '1.0.11';
- var escapeMode = false;
- var DOMNodeTypes = {
- ELEMENT_NODE: 1,
- TEXT_NODE: 3,
- CDATA_SECTION_NODE: 4,
- DOCUMENT_NODE: 9
- };
- function getNodeLocalName(node) {
- var nodeLocalName = node.localName;
- if (nodeLocalName == null) {
- nodeLocalName = node.baseName;
- }
- // Yeah, this is IE!!
- if (nodeLocalName === null || nodeLocalName === '') {
- nodeLocalName = node.nodeName;
- }
- // =="" is IE too
- return nodeLocalName;
- }
- function getNodePrefix(node) {
- return node.prefix;
- }
- function escapeXmlChars(str) {
- if (typeof str === 'string') {
- return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/');
- } else {
- return str;
- }
- }
- function unescapeXmlChars(str) {
- return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '\'').replace(///g, '/');
- }
- function parseDOMChildren(node) {
- var result, child, childName;
- if (node.nodeType === DOMNodeTypes.DOCUMENT_NODE) {
- result = {};
- child = node.firstChild;
- childName = getNodeLocalName(child);
- result[childName] = parseDOMChildren(child);
- return result;
- } else {
- if (node.nodeType === DOMNodeTypes.ELEMENT_NODE) {
- result = {};
- result.__cnt = 0;
- var nodeChildren = node.childNodes;
- // Children nodes
- for (var cidx = 0; cidx < nodeChildren.length; cidx++) {
- child = nodeChildren.item(cidx);
- // nodeChildren[cidx];
- childName = getNodeLocalName(child);
- result.__cnt++;
- if (result[childName] === null) {
- result[childName] = parseDOMChildren(child);
- result[childName + '_asArray'] = new Array(1);
- result[childName + '_asArray'][0] = result[childName];
- } else {
- if (result[childName] !== null) {
- if (!(result[childName] instanceof Array)) {
- var tmpObj = result[childName];
- result[childName] = [];
- result[childName][0] = tmpObj;
- result[childName + '_asArray'] = result[childName];
- }
- }
- var aridx = 0;
- while (result[childName][aridx] !== null) {
- aridx++;
- }
- result[childName][aridx] = parseDOMChildren(child);
- }
- }
- // Attributes
- for (var aidx = 0; aidx < node.attributes.length; aidx++) {
- var attr = node.attributes.item(aidx);
- // [aidx];
- result.__cnt++;
- result['_' + attr.name] = attr.value;
- }
- // Node namespace prefix
- var nodePrefix = getNodePrefix(node);
- if (nodePrefix !== null && nodePrefix !== '') {
- result.__cnt++;
- result.__prefix = nodePrefix;
- }
- if (result.__cnt === 1 && result['#text'] !== null) {
- result = result['#text'];
- }
- if (result['#text'] !== null) {
- result.__text = result['#text'];
- if (escapeMode) {
- result.__text = unescapeXmlChars(result.__text);
- }
- delete result['#text'];
- delete result['#text_asArray'];
- }
- if (result['#cdata-section'] != null) {
- result.__cdata = result['#cdata-section'];
- delete result['#cdata-section'];
- delete result['#cdata-section_asArray'];
- }
- if (result.__text != null || result.__cdata != null) {
- result.toString = function () {
- return (this.__text != null ? this.__text : '') + (this.__cdata != null ? this.__cdata : '');
- };
- }
- return result;
- } else {
- if (node.nodeType === DOMNodeTypes.TEXT_NODE || node.nodeType === DOMNodeTypes.CDATA_SECTION_NODE) {
- return node.nodeValue;
- }
- }
- }
- }
- function startTag(jsonObj, element, attrList, closed) {
- var resultStr = '<' + (jsonObj != null && jsonObj.__prefix != null ? jsonObj.__prefix + ':' : '') + element;
- if (attrList != null) {
- for (var aidx = 0; aidx < attrList.length; aidx++) {
- var attrName = attrList[aidx];
- var attrVal = jsonObj[attrName];
- resultStr += ' ' + attrName.substr(1) + '=\'' + attrVal + '\'';
- }
- }
- if (!closed) {
- resultStr += '>';
- } else {
- resultStr += '/>';
- }
- return resultStr;
- }
- function endTag(jsonObj, elementName) {
- return '</' + (jsonObj.__prefix !== null ? jsonObj.__prefix + ':' : '') + elementName + '>';
- }
- function endsWith(str, suffix) {
- return str.indexOf(suffix, str.length - suffix.length) !== -1;
- }
- function jsonXmlSpecialElem(jsonObj, jsonObjField) {
- if (endsWith(jsonObjField.toString(), '_asArray') || jsonObjField.toString().indexOf('_') === 0 || jsonObj[jsonObjField] instanceof Function) {
- return true;
- } else {
- return false;
- }
- }
- function jsonXmlElemCount(jsonObj) {
- var elementsCnt = 0;
- if (jsonObj instanceof Object) {
- for (var it in jsonObj) {
- if (jsonXmlSpecialElem(jsonObj, it)) {
- continue;
- }
- elementsCnt++;
- }
- }
- return elementsCnt;
- }
- function parseJSONAttributes(jsonObj) {
- var attrList = [];
- if (jsonObj instanceof Object) {
- for (var ait in jsonObj) {
- if (ait.toString().indexOf('__') === -1 && ait.toString().indexOf('_') === 0) {
- attrList.push(ait);
- }
- }
- }
- return attrList;
- }
- function parseJSONTextAttrs(jsonTxtObj) {
- var result = '';
- if (jsonTxtObj.__cdata != null) {
- result += '<![CDATA[' + jsonTxtObj.__cdata + ']]>';
- }
- if (jsonTxtObj.__text != null) {
- if (escapeMode) {
- result += escapeXmlChars(jsonTxtObj.__text);
- } else {
- result += jsonTxtObj.__text;
- }
- }
- return result;
- }
- function parseJSONTextObject(jsonTxtObj) {
- var result = '';
- if (jsonTxtObj instanceof Object) {
- result += parseJSONTextAttrs(jsonTxtObj);
- } else {
- if (jsonTxtObj != null) {
- if (escapeMode) {
- result += escapeXmlChars(jsonTxtObj);
- } else {
- result += jsonTxtObj;
- }
- }
- }
- return result;
- }
- function parseJSONArray(jsonArrRoot, jsonArrObj, attrList) {
- var result = '';
- if (jsonArrRoot.length === 0) {
- result += startTag(jsonArrRoot, jsonArrObj, attrList, true);
- } else {
- for (var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) {
- result += startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false);
- result += parseJSONObject(jsonArrRoot[arIdx]);
- result += endTag(jsonArrRoot[arIdx], jsonArrObj);
- }
- }
- return result;
- }
- function parseJSONObject(jsonObj) {
- var result = '';
- var elementsCnt = jsonXmlElemCount(jsonObj);
- if (elementsCnt > 0) {
- for (var it in jsonObj) {
- if (jsonXmlSpecialElem(jsonObj, it)) {
- continue;
- }
- var subObj = jsonObj[it];
- var attrList = parseJSONAttributes(subObj);
- if (subObj === null || subObj === undefined) {
- result += startTag(subObj, it, attrList, true);
- } else {
- if (subObj instanceof Object) {
- if (subObj instanceof Array) {
- result += parseJSONArray(subObj, it, attrList);
- } else {
- var subObjElementsCnt = jsonXmlElemCount(subObj);
- if (subObjElementsCnt > 0 || subObj.__text !== null || subObj.__cdata !== null) {
- result += startTag(subObj, it, attrList, false);
- result += parseJSONObject(subObj);
- result += endTag(subObj, it);
- } else {
- result += startTag(subObj, it, attrList, true);
- }
- }
- } else {
- result += startTag(subObj, it, attrList, false);
- result += parseJSONTextObject(subObj);
- result += endTag(subObj, it);
- }
- }
- }
- }
- result += parseJSONTextObject(jsonObj);
- return result;
- }
- this.parseXmlString = function (xmlDocStr) {
- var xmlDoc;
- if (window.DOMParser) {
- var parser = new window.DOMParser();
- xmlDoc = parser.parseFromString(xmlDocStr, 'text/xml');
- } else {
- // IE :(
- if (xmlDocStr.indexOf('<?') === 0) {
- xmlDocStr = xmlDocStr.substr(xmlDocStr.indexOf('?>') + 2);
- }
- xmlDoc = new ActiveXObject('Microsoft.XMLDOM');
- xmlDoc.async = 'false';
- xmlDoc.loadXML(xmlDocStr);
- }
- return xmlDoc;
- };
- this.xml2json = function (xmlDoc) {
- return parseDOMChildren(xmlDoc);
- };
- this.xml_str2json = function (xmlDocStr) {
- var xmlDoc = this.parseXmlString(xmlDocStr);
- return this.xml2json(xmlDoc);
- };
- this.json2xml_str = function (jsonObj) {
- return parseJSONObject(jsonObj);
- };
- this.json2xml = function (jsonObj) {
- var xmlDocStr = this.json2xml_str(jsonObj);
- return this.parseXmlString(xmlDocStr);
- };
- this.getVersion = function () {
- return VERSION;
- };
- this.escapeMode = function (enabled) {
- escapeMode = enabled;
- };
- }
- var x2js = new X2JS();
- return {
- /** Called to load in the legacy tree js which is required on startup if a user is logged in or
- after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */
- toJson: function (xml) {
- var json = x2js.xml_str2json(xml);
- return json;
- },
- fromJson: function (json) {
- var xml = x2js.json2xml_str(json);
- return xml;
- }
- };
- }
- angular.module('umbraco.services').factory('xmlhelper', xmlhelper);
- }());