10423 lines
476 KiB
JavaScript
10423 lines
476 KiB
JavaScript
(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
|
|
};
|
|
var drawerState = {
|
|
//this view to show
|
|
view: null,
|
|
// bind custom values to the drawer
|
|
model: null,
|
|
//Whether the drawer is being shown or not
|
|
showDrawer: 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');
|
|
},
|
|
/**
|
|
* @ngdoc function
|
|
* @name umbraco.services.angularHelper#getDrawerState
|
|
* @methodOf umbraco.services.appState
|
|
* @function
|
|
*
|
|
* @description
|
|
* Returns the current drawer 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
|
|
*
|
|
*/
|
|
getDrawerState: function (key) {
|
|
return getState(drawerState, key, 'drawerState');
|
|
},
|
|
/**
|
|
* @ngdoc function
|
|
* @name umbraco.services.angularHelper#setDrawerState
|
|
* @methodOf umbraco.services.appState
|
|
* @function
|
|
*
|
|
* @description
|
|
* Sets a drawer state value by key
|
|
*
|
|
*/
|
|
setDrawerState: function (key, value) {
|
|
setState(drawerState, key, value, 'drawerState');
|
|
}
|
|
};
|
|
}
|
|
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) {
|
|
scope = $rootScope;
|
|
}
|
|
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) {
|
|
scope = $rootScope;
|
|
}
|
|
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) {
|
|
scope = $rootScope;
|
|
}
|
|
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.backdropService
|
|
*
|
|
* @description
|
|
* <b>Added in Umbraco 7.8</b>. Application-wide service for handling backdrops.
|
|
*
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
function backdropService(eventsService) {
|
|
var args = {
|
|
opacity: null,
|
|
element: null,
|
|
elementPreventClick: false,
|
|
disableEventsOnClick: false,
|
|
show: false
|
|
};
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.backdropService#open
|
|
* @methodOf umbraco.services.backdropService
|
|
*
|
|
* @description
|
|
* Raises an event to open a backdrop
|
|
* @param {Object} options The backdrop options
|
|
* @param {Number} options.opacity Sets the opacity on the backdrop (default 0.4)
|
|
* @param {DomElement} options.element Highlights a DOM-element (HTML-selector)
|
|
* @param {Boolean} options.elementPreventClick Adds blocking element on top of highligted area to prevent all clicks
|
|
* @param {Boolean} options.disableEventsOnClick Disables all raised events when the backdrop is clicked
|
|
*/
|
|
function open(options) {
|
|
if (options && options.element) {
|
|
args.element = options.element;
|
|
}
|
|
if (options && options.disableEventsOnClick) {
|
|
args.disableEventsOnClick = options.disableEventsOnClick;
|
|
}
|
|
args.show = true;
|
|
eventsService.emit('appState.backdrop', args);
|
|
}
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.backdropService#close
|
|
* @methodOf umbraco.services.backdropService
|
|
*
|
|
* @description
|
|
* Raises an event to close the backdrop
|
|
*
|
|
*/
|
|
function close() {
|
|
args.element = null;
|
|
args.show = false;
|
|
eventsService.emit('appState.backdrop', args);
|
|
}
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.backdropService#setOpacity
|
|
* @methodOf umbraco.services.backdropService
|
|
*
|
|
* @description
|
|
* Raises an event which updates the opacity option on the backdrop
|
|
*/
|
|
function setOpacity(opacity) {
|
|
args.opacity = opacity;
|
|
eventsService.emit('appState.backdrop', args);
|
|
}
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.backdropService#setHighlight
|
|
* @methodOf umbraco.services.backdropService
|
|
*
|
|
* @description
|
|
* Raises an event which updates the element option on the backdrop
|
|
*/
|
|
function setHighlight(element, preventClick) {
|
|
args.element = element;
|
|
args.elementPreventClick = preventClick;
|
|
eventsService.emit('appState.backdrop', args);
|
|
}
|
|
var service = {
|
|
open: open,
|
|
close: close,
|
|
setOpacity: setOpacity,
|
|
setHighlight: setHighlight
|
|
};
|
|
return service;
|
|
}
|
|
angular.module('umbraco.services').factory('backdropService', backdropService);
|
|
}());
|
|
/**
|
|
* @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, localizationService, 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;
|
|
},
|
|
/** Used by the content editor and media editor to add an info tab to the tabs array (normally known as the properties tab) */
|
|
addInfoTab: function (tabs) {
|
|
var infoTab = {
|
|
'alias': '_umb_infoTab',
|
|
'id': -1,
|
|
'label': 'Info',
|
|
'properties': []
|
|
};
|
|
// first check if tab is already added
|
|
var foundInfoTab = false;
|
|
angular.forEach(tabs, function (tab) {
|
|
if (tab.id === infoTab.id && tab.alias === infoTab.alias) {
|
|
foundInfoTab = true;
|
|
}
|
|
});
|
|
// add info tab if is is not found
|
|
if (!foundInfoTab) {
|
|
localizationService.localize('general_info').then(function (value) {
|
|
infoTab.label = value;
|
|
tabs.push(infoTab);
|
|
});
|
|
}
|
|
},
|
|
/** 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,
|
|
alias: 'saveAndPublish'
|
|
};
|
|
case 'H':
|
|
//send to publish
|
|
return {
|
|
letter: ch,
|
|
labelKey: 'buttons_saveToPublish',
|
|
handler: args.methods.sendToPublish,
|
|
hotKey: 'ctrl+p',
|
|
hotKeyWhenHidden: true,
|
|
alias: 'sendToPublish'
|
|
};
|
|
case 'A':
|
|
//save
|
|
return {
|
|
letter: ch,
|
|
labelKey: 'buttons_save',
|
|
handler: args.methods.save,
|
|
hotKey: 'ctrl+s',
|
|
hotKeyWhenHidden: true,
|
|
alias: 'save'
|
|
};
|
|
case 'Z':
|
|
//unpublish
|
|
return {
|
|
letter: ch,
|
|
labelKey: 'content_unPublish',
|
|
handler: args.methods.unPublish,
|
|
hotKey: 'ctrl+u',
|
|
hotKeyWhenHidden: true,
|
|
alias: 'unpublish'
|
|
};
|
|
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
|
|
// and the user has access to unpublish (may have been removed via Event)
|
|
if (!args.create) {
|
|
if (args.content.publishDate && _.contains(args.content.allowedActions, 'U') && _.contains(args.content.allowedActions, 'Z')) {
|
|
buttons.subButtons.push(createButtonDefinition('Z'));
|
|
}
|
|
}
|
|
}
|
|
// If we have a scheduled publish date change the default button to
|
|
// "save" and update the label to "save and schedule
|
|
if (args.content.releaseDate) {
|
|
// if save button is alread the default don't change it just update the label
|
|
if (buttons.defaultButton && buttons.defaultButton.letter === 'A') {
|
|
buttons.defaultButton.labelKey = 'buttons_saveAndSchedule';
|
|
return buttons;
|
|
}
|
|
if (buttons.defaultButton && buttons.subButtons && buttons.subButtons.length > 0) {
|
|
// save a copy of the default so we can push it to the sub buttons later
|
|
var defaultButtonCopy = angular.copy(buttons.defaultButton);
|
|
var newSubButtons = [];
|
|
// if save button is not the default button - find it and make it the default
|
|
angular.forEach(buttons.subButtons, function (subButton) {
|
|
if (subButton.letter === 'A') {
|
|
buttons.defaultButton = subButton;
|
|
buttons.defaultButton.labelKey = 'buttons_saveAndSchedule';
|
|
} else {
|
|
newSubButtons.push(subButton);
|
|
}
|
|
});
|
|
// push old default button into subbuttons
|
|
newSubButtons.push(defaultButtonCopy);
|
|
buttons.subButtons = newSubButtons;
|
|
}
|
|
}
|
|
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;
|
|
},
|
|
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 */
|
|
emit: function (name, args) {
|
|
//there are no listeners
|
|
if (!$rootScope.$$listeners[name]) {
|
|
return;
|
|
}
|
|
//send the event
|
|
$rootScope.$emit(name, args);
|
|
},
|
|
/** 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, umbRequestHelper) {
|
|
var helpTopics = {};
|
|
var defaultUrl = 'https://our.umbraco.com/rss/help';
|
|
var tvUrl = 'https://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);
|
|
},
|
|
getContextHelpForPage: function (section, tree, baseurl) {
|
|
var qs = '?section=' + section + '&tree=' + tree;
|
|
if (tree) {
|
|
qs += '&tree=' + tree;
|
|
}
|
|
if (baseurl) {
|
|
qs += '&baseurl=' + encodeURIComponent(baseurl);
|
|
}
|
|
var url = umbRequestHelper.getApiUrl('helpApiBaseUrl', 'GetContextHelpForPage' + qs);
|
|
return umbRequestHelper.resourcePromise($http.get(url), 'Failed to get lessons content');
|
|
},
|
|
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 = null;
|
|
try {
|
|
classes = document.styleSheets[i].rules || document.styleSheets[i].cssRules;
|
|
} catch (e) {
|
|
console.warn('Can\'t read the css rules of: ' + document.styleSheets[i].href, e);
|
|
continue;
|
|
}
|
|
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);
|
|
(function () {
|
|
'use strict';
|
|
function javascriptLibraryService($q, $http, umbRequestHelper) {
|
|
var existingLocales = [];
|
|
function getSupportedLocalesForMoment() {
|
|
var deferred = $q.defer();
|
|
if (existingLocales.length === 0) {
|
|
umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('backOfficeAssetsApiBaseUrl', 'GetSupportedMomentLocales')), 'Failed to get cultures').then(function (locales) {
|
|
existingLocales = locales;
|
|
deferred.resolve(existingLocales);
|
|
});
|
|
} else {
|
|
deferred.resolve(existingLocales);
|
|
}
|
|
return deferred.promise;
|
|
}
|
|
var service = { getSupportedLocalesForMoment: getSupportedLocalesForMoment };
|
|
return service;
|
|
}
|
|
angular.module('umbraco.services').factory('javascriptLibraryService', javascriptLibraryService);
|
|
}());
|
|
// 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 && item.key === selectedItem.key) {
|
|
isSelected = true;
|
|
}
|
|
}
|
|
if (!isSelected) {
|
|
var obj = { id: item.id };
|
|
if (item.key) {
|
|
obj.key = item.key;
|
|
}
|
|
selection.push(obj);
|
|
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 && 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];
|
|
var obj = { id: item.id };
|
|
if (item.key) {
|
|
obj.key = item.key;
|
|
}
|
|
if (checkbox.checked) {
|
|
selection.push(obj);
|
|
} 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 && 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: https://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') //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish)
|
|
};
|
|
}
|
|
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, $log) {
|
|
//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) || !mediaEntity.metaData.MediaPath) {
|
|
//don't throw since this image legitimately might not contain a media path, but output a warning
|
|
$log.warn('Cannot resolve the file url from the mediaEntity, it does not contain the required metaData');
|
|
return null;
|
|
}
|
|
if (thumbnail) {
|
|
if (this.detectIfImageByExtension(mediaEntity.metaData.MediaPath)) {
|
|
return this.getThumbnailFromPath(mediaEntity.metaData.MediaPath);
|
|
} else {
|
|
return null;
|
|
}
|
|
} else {
|
|
return mediaEntity.metaData.MediaPath;
|
|
}
|
|
},
|
|
/**
|
|
* @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, umbRequestHelper, notificationsService, localizationService) {
|
|
return {
|
|
'ExportMember': function (args) {
|
|
var url = umbRequestHelper.getApiUrl('memberApiBaseUrl', 'ExportMemberData', [{ key: args.entity.id }]);
|
|
umbRequestHelper.downloadFile(url).then(function () {
|
|
localizationService.localize('speechBubbles_memberExportedSuccess').then(function (value) {
|
|
notificationsService.success(value);
|
|
});
|
|
}, function (data) {
|
|
localizationService.localize('speechBubbles_memberExportedError').then(function (value) {
|
|
notificationsService.error(value);
|
|
});
|
|
});
|
|
},
|
|
/**
|
|
* @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;
|
|
var syncTreePromise;
|
|
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);
|
|
appState.setMenuState('currentNode', null);
|
|
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);
|
|
if (syncTreePromise) {
|
|
mainTreeEventHandler.syncTree(syncTreePromise.args).then(function (syncArgs) {
|
|
syncTreePromise.resolve(syncArgs);
|
|
});
|
|
}
|
|
});
|
|
//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) {
|
|
if (mainTreeEventHandler.syncTree) {
|
|
//returns a promise,
|
|
return mainTreeEventHandler.syncTree(args);
|
|
}
|
|
}
|
|
//create a promise and resolve it later
|
|
syncTreePromise = $q.defer();
|
|
syncTreePromise.args = args;
|
|
return syncTreePromise.promise;
|
|
},
|
|
/**
|
|
Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to
|
|
have to set an active tree and then sync, the new API does this in one method by using syncTree
|
|
*/
|
|
_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 achieve 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#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) {
|
|
if (showMenu) {
|
|
this.showMenu(undefined, {
|
|
skipDefault: true,
|
|
node: appState.getMenuState('currentNode')
|
|
});
|
|
} else {
|
|
setMode('default');
|
|
}
|
|
},
|
|
/**
|
|
* @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, args.dataTypeId).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, args.canceler, args.dataTypeId).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($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);
|
|
if (img.url) {
|
|
var src = img.url + '?width=' + newSize.width + '&height=' + newSize.height;
|
|
editor.dom.setAttrib(imgElm, 'data-mce-src', src);
|
|
}
|
|
}
|
|
editor.dom.setAttrib(imgElm, 'id', null);
|
|
}, 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 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')
|
|
};
|
|
// drop the lead char from the anchor text, if it has a value
|
|
var anchorVal = anchor[0].dataset.anchor;
|
|
if (anchorVal) {
|
|
currentTarget.anchor = anchorVal.substring(1);
|
|
}
|
|
//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) {
|
|
// if the current link has an anchor, it needs to be considered when getting the udi/id
|
|
// if an anchor exists, reduce the substring max by its length plus two to offset the removed prefix and trailing curly brace
|
|
var linkId = currentTarget.url.substring(currentTarget.url.indexOf(':') + 1, currentTarget.url.lastIndexOf('}'));
|
|
//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;
|
|
// if an anchor exists, check that it is appropriately prefixed
|
|
if (target.anchor && target.anchor[0] !== '?' && target.anchor[0] !== '#') {
|
|
target.anchor = (target.anchor.indexOf('=') === -1 ? '#' : '?') + target.anchor;
|
|
}
|
|
// the href might be an external url, so check the value for an anchor/qs
|
|
// href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor
|
|
if (!target.anchor) {
|
|
var urlParts = href.split(/(#|\?)/);
|
|
if (urlParts.length === 3) {
|
|
href = urlParts[0];
|
|
target.anchor = urlParts[1] + urlParts[2];
|
|
}
|
|
}
|
|
//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;
|
|
}
|
|
if (target.anchor) {
|
|
a['data-anchor'] = target.anchor;
|
|
a.href = a.href + target.anchor;
|
|
} else {
|
|
a['data-anchor'] = null;
|
|
}
|
|
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 && !target.anchor) {
|
|
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;
|
|
}
|
|
if (!href) {
|
|
href = '';
|
|
}
|
|
// Is email and not //user@domain.com and protocol (e.g. mailto:, sip:) is not specified
|
|
if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf(':') === -1) {
|
|
// assume it's a mailto link
|
|
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.tourService
|
|
*
|
|
* @description
|
|
* <b>Added in Umbraco 7.8</b>. Application-wide service for handling tours.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
function tourService(eventsService, currentUserResource, $q, tourResource) {
|
|
var tours = [];
|
|
var currentTour = null;
|
|
/**
|
|
* Registers all tours from the server and returns a promise
|
|
*/
|
|
function registerAllTours() {
|
|
tours = [];
|
|
return tourResource.getTours().then(function (tourFiles) {
|
|
angular.forEach(tourFiles, function (tourFile) {
|
|
angular.forEach(tourFile.tours, function (newTour) {
|
|
validateTour(newTour);
|
|
validateTourRegistration(newTour);
|
|
tours.push(newTour);
|
|
});
|
|
});
|
|
eventsService.emit('appState.tour.updatedTours', tours);
|
|
});
|
|
}
|
|
/**
|
|
* Method to return all of the tours as a new instance
|
|
*/
|
|
function getTours() {
|
|
return tours;
|
|
}
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.tourService#startTour
|
|
* @methodOf umbraco.services.tourService
|
|
*
|
|
* @description
|
|
* Raises an event to start a tour
|
|
* @param {Object} tour The tour which should be started
|
|
*/
|
|
function startTour(tour) {
|
|
validateTour(tour);
|
|
eventsService.emit('appState.tour.start', tour);
|
|
currentTour = tour;
|
|
}
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.tourService#endTour
|
|
* @methodOf umbraco.services.tourService
|
|
*
|
|
* @description
|
|
* Raises an event to end the current tour
|
|
*/
|
|
function endTour(tour) {
|
|
eventsService.emit('appState.tour.end', tour);
|
|
currentTour = null;
|
|
}
|
|
/**
|
|
* Disables a tour for the user, raises an event and returns a promise
|
|
* @param {any} tour
|
|
*/
|
|
function disableTour(tour) {
|
|
var deferred = $q.defer();
|
|
tour.disabled = true;
|
|
currentUserResource.saveTourStatus({
|
|
alias: tour.alias,
|
|
disabled: tour.disabled,
|
|
completed: tour.completed
|
|
}).then(function () {
|
|
eventsService.emit('appState.tour.end', tour);
|
|
currentTour = null;
|
|
deferred.resolve(tour);
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.tourService#completeTour
|
|
* @methodOf umbraco.services.tourService
|
|
*
|
|
* @description
|
|
* Completes a tour for the user, raises an event and returns a promise
|
|
* @param {Object} tour The tour which should be completed
|
|
*/
|
|
function completeTour(tour) {
|
|
var deferred = $q.defer();
|
|
tour.completed = true;
|
|
currentUserResource.saveTourStatus({
|
|
alias: tour.alias,
|
|
disabled: tour.disabled,
|
|
completed: tour.completed
|
|
}).then(function () {
|
|
eventsService.emit('appState.tour.complete', tour);
|
|
currentTour = null;
|
|
deferred.resolve(tour);
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.tourService#getCurrentTour
|
|
* @methodOf umbraco.services.tourService
|
|
*
|
|
* @description
|
|
* Returns the current tour
|
|
* @returns {Object} Returns the current tour
|
|
*/
|
|
function getCurrentTour() {
|
|
//TODO: This should be reset if a new user logs in
|
|
return currentTour;
|
|
}
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.tourService#getGroupedTours
|
|
* @methodOf umbraco.services.tourService
|
|
*
|
|
* @description
|
|
* Returns a promise of grouped tours with the current user statuses
|
|
* @returns {Array} All registered tours grouped by tour group
|
|
*/
|
|
function getGroupedTours() {
|
|
var deferred = $q.defer();
|
|
var tours = getTours();
|
|
setTourStatuses(tours).then(function () {
|
|
var groupedTours = [];
|
|
tours.forEach(function (item) {
|
|
var groupExists = false;
|
|
var newGroup = {
|
|
'group': '',
|
|
'tours': []
|
|
};
|
|
groupedTours.forEach(function (group) {
|
|
// extend existing group if it is already added
|
|
if (group.group === item.group) {
|
|
if (item.groupOrder) {
|
|
group.groupOrder = item.groupOrder;
|
|
}
|
|
groupExists = true;
|
|
group.tours.push(item);
|
|
}
|
|
});
|
|
// push new group to array if it doesn't exist
|
|
if (!groupExists) {
|
|
newGroup.group = item.group;
|
|
if (item.groupOrder) {
|
|
newGroup.groupOrder = item.groupOrder;
|
|
}
|
|
newGroup.tours.push(item);
|
|
groupedTours.push(newGroup);
|
|
}
|
|
});
|
|
deferred.resolve(groupedTours);
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
/**
|
|
* @ngdoc method
|
|
* @name umbraco.services.tourService#getTourByAlias
|
|
* @methodOf umbraco.services.tourService
|
|
*
|
|
* @description
|
|
* Returns a promise of the tour found by alias with the current user statuses
|
|
* @param {Object} tourAlias The tour alias of the tour which should be returned
|
|
* @returns {Object} Tour object
|
|
*/
|
|
function getTourByAlias(tourAlias) {
|
|
var deferred = $q.defer();
|
|
var tours = getTours();
|
|
setTourStatuses(tours).then(function () {
|
|
var tour = _.findWhere(tours, { alias: tourAlias });
|
|
deferred.resolve(tour);
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
///////////
|
|
/**
|
|
* Validates a tour object and makes sure it consists of the correct properties needed to start a tour
|
|
* @param {any} tour
|
|
*/
|
|
function validateTour(tour) {
|
|
if (!tour) {
|
|
throw 'A tour is not specified';
|
|
}
|
|
if (!tour.alias) {
|
|
throw 'A tour alias is required';
|
|
}
|
|
if (!tour.steps) {
|
|
throw 'Tour ' + tour.alias + ' is missing tour steps';
|
|
}
|
|
if (tour.steps && tour.steps.length === 0) {
|
|
throw 'Tour ' + tour.alias + ' is missing tour steps';
|
|
}
|
|
if (tour.requiredSections.length === 0) {
|
|
throw 'Tour ' + tour.alias + ' is missing the required sections';
|
|
}
|
|
}
|
|
/**
|
|
* Validates a tour before it gets registered in the service
|
|
* @param {any} tour
|
|
*/
|
|
function validateTourRegistration(tour) {
|
|
// check for existing tours with the same alias
|
|
angular.forEach(tours, function (existingTour) {
|
|
if (existingTour.alias === tour.alias) {
|
|
throw 'A tour with the alias ' + tour.alias + ' is already registered';
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Based on the tours given, this will set each of the tour statuses (disabled/completed) based on what is stored against the current user
|
|
* @param {any} tours
|
|
*/
|
|
function setTourStatuses(tours) {
|
|
var deferred = $q.defer();
|
|
currentUserResource.getTours().then(function (storedTours) {
|
|
angular.forEach(storedTours, function (storedTour) {
|
|
if (storedTour.completed === true) {
|
|
angular.forEach(tours, function (tour) {
|
|
if (storedTour.alias === tour.alias) {
|
|
tour.completed = true;
|
|
}
|
|
});
|
|
}
|
|
if (storedTour.disabled === true) {
|
|
angular.forEach(tours, function (tour) {
|
|
if (storedTour.alias === tour.alias) {
|
|
tour.disabled = true;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
deferred.resolve(tours);
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
var service = {
|
|
registerAllTours: registerAllTours,
|
|
startTour: startTour,
|
|
endTour: endTour,
|
|
disableTour: disableTour,
|
|
completeTour: completeTour,
|
|
getCurrentTour: getCurrentTour,
|
|
getGroupedTours: getGroupedTours,
|
|
getTourByAlias: getTourByAlias
|
|
};
|
|
return service;
|
|
}
|
|
angular.module('umbraco.services').factory('tourService', tourService);
|
|
}());
|
|
/**
|
|
* @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;
|
|
if (angular.isFunction(args.node.updateNodeData)) {
|
|
args.node.updateNodeData();
|
|
}
|
|
}
|
|
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 (children) {
|
|
//we've reloaded a portion of the tree, call the callback if one is specified.
|
|
//TODO: In v8, we can just use deferred.notify
|
|
if (args.treeNodeExpanded && angular.isFunction(args.treeNodeExpanded)) {
|
|
args.treeNodeExpanded({
|
|
node: node,
|
|
children: children
|
|
});
|
|
}
|
|
//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', 'isSensitiveData');
|
|
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 dictionary to the model used to save the dictionary */
|
|
formatDictionaryPostData: function (dictionary, nameIsDirty) {
|
|
var saveModel = {
|
|
parentId: dictionary.parentId,
|
|
id: dictionary.id,
|
|
name: dictionary.name,
|
|
nameIsDirty: nameIsDirty,
|
|
translations: [],
|
|
key: dictionary.key
|
|
};
|
|
for (var i = 0; i < dictionary.translations.length; i++) {
|
|
saveModel.translations.push({
|
|
isoCode: dictionary.translations[i].isoCode,
|
|
languageId: dictionary.translations[i].languageId,
|
|
translation: dictionary.translations[i].translation
|
|
});
|
|
}
|
|
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 ? prop.value.toString() === '1' ? true : false : false;
|
|
break;
|
|
case 'umbracoMemberApproved':
|
|
saveModel.isApproved = prop.value ? prop.value.toString() === '1' ? true : false : true;
|
|
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
|
|
//don't include a property that is marked readonly
|
|
if (!prop.alias.startsWith('_umb_') && !prop.readonly) {
|
|
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 propExpireDate = displayModel.removeDate;
|
|
var propReleaseDate = displayModel.releaseDate;
|
|
var propTemplate = displayModel.template;
|
|
saveModel.expireDate = propExpireDate ? propExpireDate : null;
|
|
saveModel.releaseDate = propReleaseDate ? propReleaseDate : null;
|
|
saveModel.templateAlias = propTemplate ? propTemplate : 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) {
|
|
var err = {
|
|
//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
|
|
};
|
|
// if "opts" is a promise, we set "err.errorMsg" to be that promise
|
|
if (typeof opts == 'object' && typeof opts.then == 'function') {
|
|
err.errorMsg = opts;
|
|
}
|
|
return err;
|
|
}
|
|
//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
|
|
if (data.tabs && data.tabs.length > 0) {
|
|
_.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
|
|
]);
|
|
}
|
|
});
|
|
},
|
|
/**
|
|
* Downloads a file to the client using AJAX/XHR
|
|
* Based on an implementation here: web.student.tuwien.ac.at/~e0427417/jsdownload.html
|
|
* See https://stackoverflow.com/a/24129082/694494
|
|
*/
|
|
downloadFile: function (httpPath) {
|
|
var deferred = $q.defer();
|
|
// Use an arraybuffer
|
|
$http.get(httpPath, { responseType: 'arraybuffer' }).success(function (data, status, headers) {
|
|
var octetStreamMime = 'application/octet-stream';
|
|
var success = false;
|
|
// Get the headers
|
|
headers = headers();
|
|
// Get the filename from the x-filename header or default to "download.bin"
|
|
var filename = headers['x-filename'] || 'download.bin';
|
|
// Determine the content type from the header or default to "application/octet-stream"
|
|
var contentType = headers['content-type'] || octetStreamMime;
|
|
try {
|
|
// Try using msSaveBlob if supported
|
|
console.log('Trying saveBlob method ...');
|
|
var blob = new Blob([data], { type: contentType });
|
|
if (navigator.msSaveBlob)
|
|
navigator.msSaveBlob(blob, filename);
|
|
else {
|
|
// Try using other saveBlob implementations, if available
|
|
var saveBlob = navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob;
|
|
if (saveBlob === undefined)
|
|
throw 'Not supported';
|
|
saveBlob(blob, filename);
|
|
}
|
|
console.log('saveBlob succeeded');
|
|
success = true;
|
|
} catch (ex) {
|
|
console.log('saveBlob method failed with the following exception:');
|
|
console.log(ex);
|
|
}
|
|
if (!success) {
|
|
// Get the blob url creator
|
|
var urlCreator = window.URL || window.webkitURL || window.mozURL || window.msURL;
|
|
if (urlCreator) {
|
|
// Try to use a download link
|
|
var link = document.createElement('a');
|
|
if ('download' in link) {
|
|
// Try to simulate a click
|
|
try {
|
|
// Prepare a blob URL
|
|
console.log('Trying download link method with simulated click ...');
|
|
var blob = new Blob([data], { type: contentType });
|
|
var url = urlCreator.createObjectURL(blob);
|
|
link.setAttribute('href', url);
|
|
// Set the download attribute (Supported in Chrome 14+ / Firefox 20+)
|
|
link.setAttribute('download', filename);
|
|
// Simulate clicking the download link
|
|
var event = document.createEvent('MouseEvents');
|
|
event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
|
|
link.dispatchEvent(event);
|
|
console.log('Download link method with simulated click succeeded');
|
|
success = true;
|
|
} catch (ex) {
|
|
console.log('Download link method with simulated click failed with the following exception:');
|
|
console.log(ex);
|
|
}
|
|
}
|
|
if (!success) {
|
|
// Fallback to window.location method
|
|
try {
|
|
// Prepare a blob URL
|
|
// Use application/octet-stream when using window.location to force download
|
|
console.log('Trying download link method with window.location ...');
|
|
var blob = new Blob([data], { type: octetStreamMime });
|
|
var url = urlCreator.createObjectURL(blob);
|
|
window.location = url;
|
|
console.log('Download link method with window.location succeeded');
|
|
success = true;
|
|
} catch (ex) {
|
|
console.log('Download link method with window.location failed with the following exception:');
|
|
console.log(ex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!success) {
|
|
// Fallback to window.open method
|
|
console.log('No methods worked for saving the arraybuffer, using last resort window.open');
|
|
window.open(httpPath, '_blank', '');
|
|
}
|
|
deferred.resolve();
|
|
}).error(function (data, status) {
|
|
console.log('Request failed with status: ' + status);
|
|
deferred.reject({
|
|
errorMsg: 'An error occurred downloading the file',
|
|
data: data,
|
|
status: status
|
|
});
|
|
});
|
|
return deferred.promise;
|
|
}
|
|
};
|
|
}
|
|
angular.module('umbraco.services').factory('umbRequestHelper', umbRequestHelper);
|
|
angular.module('umbraco.services').factory('userService', function ($rootScope, eventsService, $q, $location, $log, securityRetryQueue, authResource, assetsService, dialogService, $timeout, angularHelper, $http, javascriptLibraryService) {
|
|
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();
|
|
}
|
|
}
|
|
function getMomentLocales(locales, supportedLocales) {
|
|
var localeUrls = [];
|
|
var locales = locales.split(',');
|
|
for (var i = 0; i < locales.length; i++) {
|
|
var locale = locales[i].toString().toLowerCase();
|
|
if (locale !== 'en-us') {
|
|
if (supportedLocales.indexOf(locale + '.js') > -1) {
|
|
localeUrls.push('lib/moment/' + locale + '.js');
|
|
}
|
|
if (locale.indexOf('-') > -1) {
|
|
var majorLocale = locale.split('-')[0] + '.js';
|
|
if (supportedLocales.indexOf(majorLocale) > -1) {
|
|
localeUrls.push('lib/moment/' + majorLocale);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return localeUrls;
|
|
}
|
|
/** 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();
|
|
}
|
|
});
|
|
var services = {
|
|
/** 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;
|
|
},
|
|
/** Loads the Moment.js Locale for the current user. */
|
|
loadMomentLocaleForCurrentUser: function () {
|
|
var promises = {
|
|
currentUser: this.getCurrentUser(),
|
|
supportedLocales: javascriptLibraryService.getSupportedLocalesForMoment()
|
|
};
|
|
return $q.all(promises).then(function (values) {
|
|
return services.loadLocales(values.currentUser.locale, values.supportedLocales);
|
|
});
|
|
},
|
|
/** Loads specific Moment.js Locales. */
|
|
loadLocales: function (locales, supportedLocales) {
|
|
var localeUrls = getMomentLocales(locales, supportedLocales);
|
|
if (localeUrls.length >= 1) {
|
|
return assetsService.load(localeUrls, $rootScope);
|
|
} else {
|
|
//return a noop promise
|
|
var deferred = $q.defer();
|
|
var promise = deferred.promise;
|
|
deferred.resolve(true);
|
|
return 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);
|
|
}
|
|
};
|
|
return services;
|
|
});
|
|
(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'
|
|
},
|
|
{
|
|
'value': 4,
|
|
'name': 'Inactive',
|
|
'key': 'Inactive',
|
|
'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');
|
|
//if the string format already denotes that it's in "Roundtrip UTC" format (i.e. "2018-02-07T00:20:38.173Z")
|
|
//otherwise known as https://en.wikipedia.org/wiki/ISO_8601. This is the default format returned from the server
|
|
//since that is the default formatter for newtonsoft.json. When it is in this format, we need to tell moment
|
|
//to load the date as UTC so it's not changed, otherwise load it normally
|
|
var isoFormat;
|
|
if (strVal.indexOf('T') > -1 && strVal.endsWith('Z')) {
|
|
isoFormat = moment.utc(strVal).format('YYYY-MM-DDTHH:mm:ss') + formattedOffset;
|
|
} else {
|
|
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();
|
|
},
|
|
getLocalDate: function (date, culture, format) {
|
|
if (date) {
|
|
var dateVal;
|
|
var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset;
|
|
var localOffset = new Date().getTimezoneOffset();
|
|
var serverTimeNeedsOffsetting = -serverOffset !== localOffset;
|
|
if (serverTimeNeedsOffsetting) {
|
|
dateVal = this.convertToLocalMomentTime(date, serverOffset);
|
|
} else {
|
|
dateVal = moment(date, 'YYYY-MM-DD HH:mm:ss');
|
|
}
|
|
return dateVal.locale(culture).format(format);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
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);
|
|
}()); |