Advertisement
Guest User

Services

a guest
May 15th, 2018
1,638
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /*! umbraco
  2.  * https://github.com/umbraco/umbraco-cms/
  3.  * Copyright (c) 2017 Umbraco HQ;
  4.  * Licensed
  5.  */
  6.  
  7. (function() {
  8.  
  9. angular.module("umbraco.services", ["umbraco.security", "umbraco.resources"]);
  10.  
  11. /**
  12.  * @ngdoc service
  13.  * @name umbraco.services.angularHelper
  14.  * @function
  15.  *
  16.  * @description
  17.  * Some angular helper/extension methods
  18.  */
  19. function angularHelper($log, $q) {
  20.     return {
  21.  
  22.         /**
  23.          * @ngdoc function
  24.          * @name umbraco.services.angularHelper#rejectedPromise
  25.          * @methodOf umbraco.services.angularHelper
  26.          * @function
  27.          *
  28.          * @description
  29.          * In some situations we need to return a promise as a rejection, normally based on invalid data. This
  30.          * is a wrapper to do that so we can save on writing a bit of code.
  31.          *
  32.          * @param {object} objReject The object to send back with the promise rejection
  33.          */
  34.         rejectedPromise: function (objReject) {
  35.             var deferred = $q.defer();
  36.             //return an error object including the error message for UI
  37.             deferred.reject(objReject);
  38.             return deferred.promise;
  39.         },
  40.  
  41.         /**
  42.          * @ngdoc function
  43.          * @name safeApply
  44.          * @methodOf umbraco.services.angularHelper
  45.          * @function
  46.          *
  47.          * @description
  48.          * This checks if a digest/apply is already occuring, if not it will force an apply call
  49.          */
  50.         safeApply: function (scope, fn) {
  51.             if (scope.$$phase || scope.$root.$$phase) {
  52.                 if (angular.isFunction(fn)) {
  53.                     fn();
  54.                 }
  55.             }
  56.             else {
  57.                 if (angular.isFunction(fn)) {
  58.                     scope.$apply(fn);
  59.                 }
  60.                 else {
  61.                     scope.$apply();
  62.                 }
  63.             }
  64.         },
  65.  
  66.         /**
  67.          * @ngdoc function
  68.          * @name getCurrentForm
  69.          * @methodOf umbraco.services.angularHelper
  70.          * @function
  71.          *
  72.          * @description
  73.          * Returns the current form object applied to the scope or null if one is not found
  74.          */
  75.         getCurrentForm: function (scope) {
  76.  
  77.             //NOTE: There isn't a way in angular to get a reference to the current form object since the form object
  78.             // is just defined as a property of the scope when it is named but you'll always need to know the name which
  79.             // isn't very convenient. If we want to watch for validation changes we need to get a form reference.
  80.             // The way that we detect the form object is a bit hackerific in that we detect all of the required properties
  81.             // that exist on a form object.
  82.             //
  83.             //The other way to do it in a directive is to require "^form", but in a controller the only other way to do it
  84.             // is to inject the $element object and use: $element.inheritedData('$formController');
  85.  
  86.             var form = null;
  87.             //var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$invalid", "$addControl", "$removeControl", "$setValidity", "$setDirty"];
  88.             var requiredFormProps = ["$addControl", "$removeControl", "$setValidity", "$setDirty", "$setPristine"];
  89.  
  90.             // a method to check that the collection of object prop names contains the property name expected
  91.             function propertyExists(objectPropNames) {
  92.                 //ensure that every required property name exists on the current scope property
  93.                 return _.every(requiredFormProps, function (item) {
  94.  
  95.                     return _.contains(objectPropNames, item);
  96.                 });
  97.             }
  98.  
  99.             for (var p in scope) {
  100.  
  101.                 if (_.isObject(scope[p]) && p !== "this" && p.substr(0, 1) !== "$") {
  102.                     //get the keys of the property names for the current property
  103.                     var props = _.keys(scope[p]);
  104.                     //if the length isn't correct, try the next prop
  105.                     if (props.length < requiredFormProps.length) {
  106.                         continue;
  107.                     }
  108.  
  109.                     //ensure that every required property name exists on the current scope property
  110.                     var containProperty = propertyExists(props);
  111.  
  112.                     if (containProperty) {
  113.                         form = scope[p];
  114.                         break;
  115.                     }
  116.                 }
  117.             }
  118.  
  119.             return form;
  120.         },
  121.  
  122.         /**
  123.          * @ngdoc function
  124.          * @name validateHasForm
  125.          * @methodOf umbraco.services.angularHelper
  126.          * @function
  127.          *
  128.          * @description
  129.          * This will validate that the current scope has an assigned form object, if it doesn't an exception is thrown, if
  130.          * it does we return the form object.
  131.          */
  132.         getRequiredCurrentForm: function (scope) {
  133.             var currentForm = this.getCurrentForm(scope);
  134.             if (!currentForm || !currentForm.$name) {
  135.                 throw "The current scope requires a current form object (or ng-form) with a name assigned to it";
  136.             }
  137.             return currentForm;
  138.         },
  139.  
  140.         /**
  141.          * @ngdoc function
  142.          * @name getNullForm
  143.          * @methodOf umbraco.services.angularHelper
  144.          * @function
  145.          *
  146.          * @description
  147.          * Returns a null angular FormController, mostly for use in unit tests
  148.          *      NOTE: This is actually the same construct as angular uses internally for creating a null form but they don't expose
  149.          *          any of this publicly to us, so we need to create our own.
  150.          *
  151.          * @param {string} formName The form name to assign
  152.          */
  153.         getNullForm: function (formName) {
  154.             return {
  155.                 $addControl: angular.noop,
  156.                 $removeControl: angular.noop,
  157.                 $setValidity: angular.noop,
  158.                 $setDirty: angular.noop,
  159.                 $setPristine: angular.noop,
  160.                 $name: formName
  161.                 //NOTE: we don't include the 'properties', just the methods.
  162.             };
  163.         }
  164.     };
  165. }
  166. angular.module('umbraco.services').factory('angularHelper', angularHelper);
  167. /**
  168.  * @ngdoc service
  169.  * @name umbraco.services.appState
  170.  * @function
  171.  *
  172.  * @description
  173.  * Tracks the various application state variables when working in the back office, raises events when state changes.
  174.  *
  175.  * ##Samples
  176.  *
  177.  * ####Subscribe to global state changes:
  178.  *
  179.  * <pre>
  180.   *    scope.showTree = appState.getGlobalState("showNavigation");
  181.   *
  182.   *    eventsService.on("appState.globalState.changed", function (e, args) {
  183.   *               if (args.key === "showNavigation") {
  184.   *                   scope.showTree = args.value;
  185.   *               }
  186.   *           });  
  187.   * </pre>
  188.  *
  189.  * ####Subscribe to section-state changes
  190.  *
  191.  * <pre>
  192.  *    scope.currentSection = appState.getSectionState("currentSection");
  193.  *
  194.  *    eventsService.on("appState.sectionState.changed", function (e, args) {
  195.  *               if (args.key === "currentSection") {
  196.  *                   scope.currentSection = args.value;
  197.  *               }
  198.  *           });  
  199.  * </pre>
  200.  */
  201. function appState(eventsService) {
  202.    
  203.     //Define all variables here - we are never returning this objects so they cannot be publicly mutable
  204.     // changed, we only expose methods to interact with the values.
  205.  
  206.     var globalState = {
  207.         showNavigation: null,
  208.         touchDevice: null,
  209.         showTray: null,
  210.         stickyNavigation: null,
  211.         navMode: null,
  212.         isReady: null,
  213.         isTablet: null
  214.     };
  215.    
  216.     var sectionState = {
  217.         //The currently active section
  218.         currentSection: null,
  219.         showSearchResults: null
  220.     };
  221.  
  222.     var treeState = {
  223.         //The currently selected node
  224.         selectedNode: null,
  225.         //The currently loaded root node reference - depending on the section loaded this could be a section root or a normal root.
  226.         //We keep this reference so we can lookup nodes to interact with in the UI via the tree service
  227.         currentRootNode: null
  228.     };
  229.    
  230.     var menuState = {
  231.         //this list of menu items to display
  232.         menuActions: null,
  233.         //the title to display in the context menu dialog
  234.         dialogTitle: null,
  235.         //The tree node that the ctx menu is launched for
  236.         currentNode: null,
  237.         //Whether the menu's dialog is being shown or not
  238.         showMenuDialog: null,
  239.         //Whether the context menu is being shown or not
  240.         showMenu: null
  241.     };
  242.  
  243.     /** function to validate and set the state on a state object */
  244.     function setState(stateObj, key, value, stateObjName) {
  245.         if (!_.has(stateObj, key)) {
  246.             throw "The variable " + key + " does not exist in " + stateObjName;
  247.         }
  248.         var changed = stateObj[key] !== value;
  249.         stateObj[key] = value;
  250.         if (changed) {
  251.             eventsService.emit("appState." + stateObjName + ".changed", { key: key, value: value });
  252.         }
  253.     }
  254.    
  255.     /** function to validate and set the state on a state object */
  256.     function getState(stateObj, key, stateObjName) {
  257.         if (!_.has(stateObj, key)) {
  258.             throw "The variable " + key + " does not exist in " + stateObjName;
  259.         }
  260.         return stateObj[key];
  261.     }
  262.  
  263.     return {
  264.  
  265.         /**
  266.          * @ngdoc function
  267.          * @name umbraco.services.angularHelper#getGlobalState
  268.          * @methodOf umbraco.services.appState
  269.          * @function
  270.          *
  271.          * @description
  272.          * Returns the current global state value by key - we do not return an object reference here - we do NOT want this
  273.          * to be publicly mutable and allow setting arbitrary values
  274.          *
  275.          */
  276.         getGlobalState: function (key) {
  277.             return getState(globalState, key, "globalState");
  278.         },
  279.  
  280.         /**
  281.          * @ngdoc function
  282.          * @name umbraco.services.angularHelper#setGlobalState
  283.          * @methodOf umbraco.services.appState
  284.          * @function
  285.          *
  286.          * @description
  287.          * Sets a global state value by key
  288.          *
  289.          */
  290.         setGlobalState: function (key, value) {
  291.             setState(globalState, key, value, "globalState");
  292.         },
  293.  
  294.         /**
  295.          * @ngdoc function
  296.          * @name umbraco.services.angularHelper#getSectionState
  297.          * @methodOf umbraco.services.appState
  298.          * @function
  299.          *
  300.          * @description
  301.          * Returns the current section state value by key - we do not return an object here - we do NOT want this
  302.          * to be publicly mutable and allow setting arbitrary values
  303.          *
  304.          */
  305.         getSectionState: function (key) {
  306.             return getState(sectionState, key, "sectionState");            
  307.         },
  308.        
  309.         /**
  310.          * @ngdoc function
  311.          * @name umbraco.services.angularHelper#setSectionState
  312.          * @methodOf umbraco.services.appState
  313.          * @function
  314.          *
  315.          * @description
  316.          * Sets a section state value by key
  317.          *
  318.          */
  319.         setSectionState: function(key, value) {
  320.             setState(sectionState, key, value, "sectionState");
  321.         },
  322.  
  323.         /**
  324.          * @ngdoc function
  325.          * @name umbraco.services.angularHelper#getTreeState
  326.          * @methodOf umbraco.services.appState
  327.          * @function
  328.          *
  329.          * @description
  330.          * Returns the current tree state value by key - we do not return an object here - we do NOT want this
  331.          * to be publicly mutable and allow setting arbitrary values
  332.          *
  333.          */
  334.         getTreeState: function (key) {
  335.             return getState(treeState, key, "treeState");
  336.         },
  337.        
  338.         /**
  339.          * @ngdoc function
  340.          * @name umbraco.services.angularHelper#setTreeState
  341.          * @methodOf umbraco.services.appState
  342.          * @function
  343.          *
  344.          * @description
  345.          * Sets a section state value by key
  346.          *
  347.          */
  348.         setTreeState: function (key, value) {
  349.             setState(treeState, key, value, "treeState");
  350.         },
  351.  
  352.         /**
  353.          * @ngdoc function
  354.          * @name umbraco.services.angularHelper#getMenuState
  355.          * @methodOf umbraco.services.appState
  356.          * @function
  357.          *
  358.          * @description
  359.          * Returns the current menu state value by key - we do not return an object here - we do NOT want this
  360.          * to be publicly mutable and allow setting arbitrary values
  361.          *
  362.          */
  363.         getMenuState: function (key) {
  364.             return getState(menuState, key, "menuState");
  365.         },
  366.        
  367.         /**
  368.          * @ngdoc function
  369.          * @name umbraco.services.angularHelper#setMenuState
  370.          * @methodOf umbraco.services.appState
  371.          * @function
  372.          *
  373.          * @description
  374.          * Sets a section state value by key
  375.          *
  376.          */
  377.         setMenuState: function (key, value) {
  378.             setState(menuState, key, value, "menuState");
  379.         },
  380.  
  381.     };
  382. }
  383. angular.module('umbraco.services').factory('appState', appState);
  384.  
  385. /**
  386.  * @ngdoc service
  387.  * @name umbraco.services.editorState
  388.  * @function
  389.  *
  390.  * @description
  391.  * Tracks the parent object for complex editors by exposing it as
  392.  * an object reference via editorState.current.entity
  393.  *
  394.  * it is possible to modify this object, so should be used with care
  395.  */
  396. angular.module('umbraco.services').factory("editorState", function() {
  397.  
  398.     var current = null;
  399.     var state = {
  400.  
  401.         /**
  402.          * @ngdoc function
  403.          * @name umbraco.services.angularHelper#set
  404.          * @methodOf umbraco.services.editorState
  405.          * @function
  406.          *
  407.          * @description
  408.          * Sets the current entity object for the currently active editor
  409.          * This is only used when implementing an editor with a complex model
  410.          * like the content editor, where the model is modified by several
  411.          * child controllers.
  412.          */
  413.         set: function (entity) {
  414.             current = entity;
  415.         },
  416.  
  417.         /**
  418.          * @ngdoc function
  419.          * @name umbraco.services.angularHelper#reset
  420.          * @methodOf umbraco.services.editorState
  421.          * @function
  422.          *
  423.          * @description
  424.          * Since the editorstate entity is read-only, you cannot set it to null
  425.          * only through the reset() method
  426.          */
  427.         reset: function() {
  428.             current = null;
  429.         },
  430.  
  431.         /**
  432.          * @ngdoc function
  433.          * @name umbraco.services.angularHelper#getCurrent
  434.          * @methodOf umbraco.services.editorState
  435.          * @function
  436.          *
  437.          * @description
  438.          * Returns an object reference to the current editor entity.
  439.          * the entity is the root object of the editor.
  440.          * EditorState is used by property/parameter editors that need
  441.          * access to the entire entity being edited, not just the property/parameter
  442.          *
  443.          * editorState.current can not be overwritten, you should only read values from it
  444.          * since modifying individual properties should be handled by the property editors
  445.          */
  446.         getCurrent: function() {
  447.             return current;
  448.         }
  449.     };
  450.  
  451.     //TODO: This shouldn't be removed! use getCurrent() method instead of a hacked readonly property which is confusing.
  452.  
  453.     //create a get/set property but don't allow setting
  454.     Object.defineProperty(state, "current", {
  455.         get: function () {
  456.             return current;
  457.         },
  458.         set: function (value) {
  459.             throw "Use editorState.set to set the value of the current entity";
  460.         },
  461.     });
  462.  
  463.     return state;
  464. });
  465. /**
  466.  * @ngdoc service
  467.  * @name umbraco.services.assetsService
  468.  *
  469.  * @requires $q
  470.  * @requires angularHelper
  471.  *  
  472.  * @description
  473.  * Promise-based utillity service to lazy-load client-side dependencies inside angular controllers.
  474.  *
  475.  * ##usage
  476.  * To use, simply inject the assetsService into any controller that needs it, and make
  477.  * sure the umbraco.services module is accesible - which it should be by default.
  478.  *
  479.  * <pre>
  480.  *      angular.module("umbraco").controller("my.controller". function(assetsService){
  481.  *          assetsService.load(["script.js", "styles.css"], $scope).then(function(){
  482.  *                 //this code executes when the dependencies are done loading
  483.  *          });
  484.  *      });
  485.  * </pre>
  486.  *
  487.  * You can also load individual files, which gives you greater control over what attibutes are passed to the file, as well as timeout
  488.  *
  489.  * <pre>
  490.  *      angular.module("umbraco").controller("my.controller". function(assetsService){
  491.  *          assetsService.loadJs("script.js", $scope, {charset: 'utf-8'}, 10000 }).then(function(){
  492.  *                 //this code executes when the script is done loading
  493.  *          });
  494.  *      });
  495.  * </pre>
  496.  *
  497.  * For these cases, there are 2 individual methods, one for javascript, and one for stylesheets:
  498.  *
  499.  * <pre>
  500.  *      angular.module("umbraco").controller("my.controller". function(assetsService){
  501.  *          assetsService.loadCss("stye.css", $scope, {media: 'print'}, 10000 }).then(function(){
  502.  *                 //loadcss cannot determine when the css is done loading, so this will trigger instantly
  503.  *          });
  504.  *      });
  505.  * </pre>  
  506.  */
  507. angular.module('umbraco.services')
  508. .factory('assetsService', function ($q, $log, angularHelper, umbRequestHelper, $rootScope, $http) {
  509.  
  510.     var initAssetsLoaded = false;
  511.     var appendRnd = function (url) {
  512.         //if we don't have a global umbraco obj yet, the app is bootstrapping
  513.         if (!Umbraco.Sys.ServerVariables.application) {
  514.             return url;
  515.         }
  516.  
  517.         var rnd = Umbraco.Sys.ServerVariables.application.version + "." + Umbraco.Sys.ServerVariables.application.cdf;
  518.         var _op = (url.indexOf("?") > 0) ? "&" : "?";
  519.         url = url + _op + "umb__rnd=" + rnd;
  520.         return url;
  521.     };
  522.  
  523.     function convertVirtualPath(path) {
  524.         //make this work for virtual paths
  525.         if (path.startsWith("~/")) {
  526.             path = umbRequestHelper.convertVirtualToAbsolutePath(path);
  527.         }
  528.         return path;
  529.     }
  530.  
  531.     var service = {
  532.         loadedAssets: {},
  533.  
  534.         _getAssetPromise: function (path) {
  535.  
  536.             if (this.loadedAssets[path]) {
  537.                 return this.loadedAssets[path];
  538.             } else {
  539.                 var deferred = $q.defer();
  540.                 this.loadedAssets[path] = { deferred: deferred, state: "new", path: path };
  541.                 return this.loadedAssets[path];
  542.             }
  543.         },
  544.         /**
  545.             Internal method. This is called when the application is loading and the user is already authenticated, or once the user is authenticated.
  546.             There's a few assets the need to be loaded for the application to function but these assets require authentication to load.
  547.         */
  548.         _loadInitAssets: function () {
  549.             var deferred = $q.defer();
  550.             //here we need to ensure the required application assets are loaded
  551.             if (initAssetsLoaded === false) {
  552.                 var self = this;
  553.                 self.loadJs(umbRequestHelper.getApiUrl("serverVarsJs", "", ""), $rootScope).then(function () {
  554.                     initAssetsLoaded = true;
  555.  
  556.                     //now we need to go get the legacyTreeJs - but this can be done async without waiting.
  557.                     self.loadJs(umbRequestHelper.getApiUrl("legacyTreeJs", "", ""), $rootScope);
  558.  
  559.                     deferred.resolve();
  560.                 });
  561.             }
  562.             else {
  563.                 deferred.resolve();
  564.             }
  565.             return deferred.promise;
  566.         },
  567.  
  568.         /**
  569.          * @ngdoc method
  570.          * @name umbraco.services.assetsService#loadCss
  571.          * @methodOf umbraco.services.assetsService
  572.          *
  573.          * @description
  574.          * Injects a file as a stylesheet into the document head
  575.          *
  576.          * @param {String} path path to the css file to load
  577.          * @param {Scope} scope optional scope to pass into the loader
  578.          * @param {Object} keyvalue collection of attributes to pass to the stylesheet element  
  579.          * @param {Number} timeout in milliseconds
  580.          * @returns {Promise} Promise object which resolves when the file has loaded
  581.          */
  582.         loadCss: function (path, scope, attributes, timeout) {
  583.  
  584.             path = convertVirtualPath(path);
  585.  
  586.             var asset = this._getAssetPromise(path); // $q.defer();
  587.             var t = timeout || 5000;
  588.             var a = attributes || undefined;
  589.  
  590.             if (asset.state === "new") {
  591.                 asset.state = "loading";
  592.                 LazyLoad.css(appendRnd(path), function () {
  593.                     if (!scope) {
  594.                         asset.state = "loaded";
  595.                         asset.deferred.resolve(true);
  596.                     } else {
  597.                         asset.state = "loaded";
  598.                         angularHelper.safeApply(scope, function () {
  599.                             asset.deferred.resolve(true);
  600.                         });
  601.                     }
  602.                 });
  603.             } else if (asset.state === "loaded") {
  604.                 asset.deferred.resolve(true);
  605.             }
  606.             return asset.deferred.promise;
  607.         },
  608.  
  609.         /**
  610.          * @ngdoc method
  611.          * @name umbraco.services.assetsService#loadJs
  612.          * @methodOf umbraco.services.assetsService
  613.          *
  614.          * @description
  615.          * Injects a file as a javascript into the document
  616.          *
  617.          * @param {String} path path to the js file to load
  618.          * @param {Scope} scope optional scope to pass into the loader
  619.          * @param {Object} keyvalue collection of attributes to pass to the script element  
  620.          * @param {Number} timeout in milliseconds
  621.          * @returns {Promise} Promise object which resolves when the file has loaded
  622.          */
  623.         loadJs: function (path, scope, attributes, timeout) {
  624.  
  625.             path = convertVirtualPath(path);
  626.  
  627.             var asset = this._getAssetPromise(path); // $q.defer();
  628.             var t = timeout || 5000;
  629.             var a = attributes || undefined;
  630.  
  631.             if (asset.state === "new") {
  632.                 asset.state = "loading";
  633.  
  634.                 LazyLoad.js(appendRnd(path), function () {
  635.                     if (!scope) {
  636.                         asset.state = "loaded";
  637.                         asset.deferred.resolve(true);
  638.                     } else {
  639.                         asset.state = "loaded";
  640.                         angularHelper.safeApply(scope, function () {
  641.                             asset.deferred.resolve(true);
  642.                         });
  643.                     }
  644.                 });
  645.  
  646.             } else if (asset.state === "loaded") {
  647.                 asset.deferred.resolve(true);
  648.             }
  649.  
  650.             return asset.deferred.promise;
  651.         },
  652.  
  653.         /**
  654.          * @ngdoc method
  655.          * @name umbraco.services.assetsService#load
  656.          * @methodOf umbraco.services.assetsService
  657.          *
  658.          * @description
  659.          * Injects a collection of files, this can be ONLY js files
  660.          *
  661.          *
  662.          * @param {Array} pathArray string array of paths to the files to load
  663.          * @param {Scope} scope optional scope to pass into the loader
  664.          * @returns {Promise} Promise object which resolves when all the files has loaded
  665.          */
  666.         load: function (pathArray, scope) {
  667.             var promise;
  668.  
  669.             if (!angular.isArray(pathArray)) {
  670.                 throw "pathArray must be an array";
  671.             }
  672.  
  673.             var nonEmpty = _.reject(pathArray, function (item) {
  674.                 return item === undefined || item === "";
  675.             });
  676.  
  677.  
  678.             //don't load anything if there's nothing to load
  679.             if (nonEmpty.length > 0) {
  680.                 var promises = [];
  681.                 var assets = [];
  682.  
  683.                 //compile a list of promises
  684.                 //blocking
  685.                 _.each(nonEmpty, function (path) {
  686.  
  687.                     path = convertVirtualPath(path);
  688.  
  689.                     var asset = service._getAssetPromise(path);
  690.                     //if not previously loaded, add to list of promises
  691.                     if (asset.state !== "loaded") {
  692.                         if (asset.state === "new") {
  693.                             asset.state = "loading";
  694.                             assets.push(asset);
  695.                         }
  696.  
  697.                         //we need to always push to the promises collection to monitor correct
  698.                         //execution                        
  699.                         promises.push(asset.deferred.promise);
  700.                     }
  701.                 });
  702.  
  703.  
  704.                 //gives a central monitoring of all assets to load
  705.                 promise = $q.all(promises);
  706.  
  707.                 _.each(assets, function (asset) {
  708.                     LazyLoad.js(appendRnd(asset.path), function () {
  709.                         asset.state = "loaded";
  710.                         if (!scope) {
  711.                             asset.deferred.resolve(true);
  712.                         }
  713.                         else {
  714.                             angularHelper.safeApply(scope, function () {
  715.                                 asset.deferred.resolve(true);
  716.                             });
  717.                         }
  718.                     });
  719.                 });
  720.             }
  721.             else {
  722.                 //return and resolve
  723.                 var deferred = $q.defer();
  724.                 promise = deferred.promise;
  725.                 deferred.resolve(true);
  726.             }
  727.  
  728.  
  729.             return promise;
  730.         }
  731.     };
  732.  
  733.     return service;
  734. });
  735.  
  736. /**
  737. * @ngdoc service
  738. * @name umbraco.services.contentEditingHelper
  739. * @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by
  740. * all editors to share logic and reduce the amount of replicated code among editors.
  741. **/
  742. function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState) {
  743.  
  744.     function isValidIdentifier(id){
  745.         //empty id <= 0
  746.         if(angular.isNumber(id) && id > 0){
  747.             return true;
  748.         }
  749.  
  750.         //empty guid
  751.         if(id === "00000000-0000-0000-0000-000000000000"){
  752.             return false;
  753.         }
  754.  
  755.         //empty string / alias
  756.         if(id === ""){
  757.             return false;
  758.         }
  759.  
  760.         return true;
  761.     }
  762.  
  763.     return {
  764.  
  765.         /** Used by the content editor and mini content editor to perform saving operations */
  766.         contentEditorPerformSave: function (args) {
  767.             if (!angular.isObject(args)) {
  768.                 throw "args must be an object";
  769.             }
  770.             if (!args.scope) {
  771.                 throw "args.scope is not defined";
  772.             }
  773.             if (!args.content) {
  774.                 throw "args.content is not defined";
  775.             }
  776.             if (!args.statusMessage) {
  777.                 throw "args.statusMessage is not defined";
  778.             }
  779.             if (!args.saveMethod) {
  780.                 throw "args.saveMethod is not defined";
  781.             }
  782.  
  783.             var redirectOnFailure = args.redirectOnFailure !== undefined ? args.redirectOnFailure : true;
  784.  
  785.             var self = this;
  786.  
  787.             //we will use the default one for content if not specified
  788.             var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback;
  789.  
  790.             var deferred = $q.defer();
  791.  
  792.             if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage, action: args.action })) {
  793.  
  794.                 args.scope.busy = true;
  795.  
  796.                 args.saveMethod(args.content, $routeParams.create, fileManager.getFiles())
  797.                     .then(function (data) {
  798.  
  799.                         formHelper.resetForm({ scope: args.scope, notifications: data.notifications });
  800.  
  801.                         self.handleSuccessfulSave({
  802.                             scope: args.scope,
  803.                             savedContent: data,
  804.                             rebindCallback: function() {
  805.                                 rebindCallback.apply(self, [args.content, data]);
  806.                             }
  807.                         });
  808.  
  809.                         args.scope.busy = false;
  810.                         deferred.resolve(data);
  811.  
  812.                     }, function (err) {
  813.                         self.handleSaveError({
  814.                             redirectOnFailure: redirectOnFailure,
  815.                             err: err,
  816.                             rebindCallback: function() {
  817.                                 rebindCallback.apply(self, [args.content, err.data]);
  818.                             }
  819.                         });
  820.                         //show any notifications
  821.                         if (angular.isArray(err.data.notifications)) {
  822.                             for (var i = 0; i < err.data.notifications.length; i++) {
  823.                                 notificationsService.showNotification(err.data.notifications[i]);
  824.                             }
  825.                         }
  826.                         args.scope.busy = false;
  827.                         deferred.reject(err);
  828.                     });
  829.             }
  830.             else {
  831.                 deferred.reject();
  832.             }
  833.  
  834.             return deferred.promise;
  835.         },
  836.  
  837.  
  838.         /** Returns the action button definitions based on what permissions the user has.
  839.         The content.allowedActions parameter contains a list of chars, each represents a button by permission so
  840.         here we'll build the buttons according to the chars of the user. */
  841.         configureContentEditorButtons: function (args) {
  842.  
  843.             if (!angular.isObject(args)) {
  844.                 throw "args must be an object";
  845.             }
  846.             if (!args.content) {
  847.                 throw "args.content is not defined";
  848.             }
  849.             if (!args.methods) {
  850.                 throw "args.methods is not defined";
  851.             }
  852.             if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.save || !args.methods.unPublish) {
  853.                 throw "args.methods does not contain all required defined methods";
  854.             }
  855.  
  856.             var buttons = {
  857.                 defaultButton: null,
  858.                 subButtons: []
  859.             };
  860.  
  861.             function createButtonDefinition(ch) {
  862.                 switch (ch) {
  863.                     case "U":
  864.                         //publish action
  865.                         return {
  866.                             letter: ch,
  867.                             labelKey: "buttons_saveAndPublish",
  868.                             handler: args.methods.saveAndPublish,
  869.                             hotKey: "ctrl+p",
  870.                             hotKeyWhenHidden: true
  871.                         };
  872.                     case "H":
  873.                         //send to publish
  874.                         return {
  875.                             letter: ch,
  876.                             labelKey: "buttons_saveToPublish",
  877.                             handler: args.methods.sendToPublish,
  878.                             hotKey: "ctrl+p",
  879.                             hotKeyWhenHidden: true
  880.                         };
  881.                     case "A":
  882.                         //save
  883.                         return {
  884.                             letter: ch,
  885.                             labelKey: "buttons_save",
  886.                             handler: args.methods.save,
  887.                             hotKey: "ctrl+s",
  888.                             hotKeyWhenHidden: true
  889.                         };
  890.                     case "Z":
  891.                         //unpublish
  892.                         return {
  893.                             letter: ch,
  894.                             labelKey: "content_unPublish",
  895.                             handler: args.methods.unPublish,
  896.                             hotKey: "ctrl+u",
  897.                             hotKeyWhenHidden: true
  898.                         };
  899.                     default:
  900.                         return null;
  901.                 }
  902.             }
  903.  
  904.             //reset
  905.             buttons.subButtons = [];
  906.  
  907.             //This is the ideal button order but depends on circumstance, we'll use this array to create the button list
  908.             // Publish, SendToPublish, Save
  909.             var buttonOrder = ["U", "H", "A"];
  910.  
  911.             //Create the first button (primary button)
  912.             //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item.
  913.             if (!args.create || _.contains(args.content.allowedActions, "C")) {
  914.                 for (var b in buttonOrder) {
  915.                     if (_.contains(args.content.allowedActions, buttonOrder[b])) {
  916.                         buttons.defaultButton = createButtonDefinition(buttonOrder[b]);
  917.                         break;
  918.                     }
  919.                 }
  920.             }
  921.  
  922.             //Now we need to make the drop down button list, this is also slightly tricky because:
  923.             //We cannot have any buttons if there's no default button above.
  924.             //We cannot have the unpublish button (Z) when there's no publish permission.
  925.             //We cannot have the unpublish button (Z) when the item is not published.
  926.             if (buttons.defaultButton) {
  927.  
  928.                 //get the last index of the button order
  929.                 var lastIndex = _.indexOf(buttonOrder, buttons.defaultButton.letter);
  930.                 //add the remaining
  931.                 for (var i = lastIndex + 1; i < buttonOrder.length; i++) {
  932.                     if (_.contains(args.content.allowedActions, buttonOrder[i])) {
  933.                         buttons.subButtons.push(createButtonDefinition(buttonOrder[i]));
  934.                     }
  935.                 }
  936.  
  937.  
  938.                 //if we are not creating, then we should add unpublish too,
  939.                 // so long as it's already published and if the user has access to publish
  940.                 if (!args.create) {
  941.                     if (args.content.publishDate && _.contains(args.content.allowedActions, "U")) {
  942.                         buttons.subButtons.push(createButtonDefinition("Z"));
  943.                     }
  944.                 }
  945.             }
  946.  
  947.             return buttons;
  948.         },
  949.  
  950.         /**
  951.          * @ngdoc method
  952.          * @name umbraco.services.contentEditingHelper#getAllProps
  953.          * @methodOf umbraco.services.contentEditingHelper
  954.          * @function
  955.          *
  956.          * @description
  957.          * Returns all propertes contained for the content item (since the normal model has properties contained inside of tabs)
  958.          */
  959.         getAllProps: function (content) {
  960.             var allProps = [];
  961.  
  962.             for (var i = 0; i < content.tabs.length; i++) {
  963.                 for (var p = 0; p < content.tabs[i].properties.length; p++) {
  964.                     allProps.push(content.tabs[i].properties[p]);
  965.                 }
  966.             }
  967.  
  968.             return allProps;
  969.         },
  970.  
  971.  
  972.         /**
  973.          * @ngdoc method
  974.          * @name umbraco.services.contentEditingHelper#configureButtons
  975.          * @methodOf umbraco.services.contentEditingHelper
  976.          * @function
  977.          *
  978.          * @description
  979.          * Returns a letter array for buttons, with the primary one first based on content model, permissions and editor state
  980.          */
  981.          getAllowedActions : function(content, creating){
  982.  
  983.                 //This is the ideal button order but depends on circumstance, we'll use this array to create the button list
  984.                 // Publish, SendToPublish, Save
  985.                 var actionOrder = ["U", "H", "A"];
  986.                 var defaultActions;
  987.                 var actions = [];
  988.  
  989.                 //Create the first button (primary button)
  990.                 //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item.
  991.                 if (!creating || _.contains(content.allowedActions, "C")) {
  992.                     for (var b in actionOrder) {
  993.                         if (_.contains(content.allowedActions, actionOrder[b])) {
  994.                             defaultAction = actionOrder[b];
  995.                             break;
  996.                         }
  997.                     }
  998.                 }
  999.  
  1000.                 actions.push(defaultAction);
  1001.  
  1002.                 //Now we need to make the drop down button list, this is also slightly tricky because:
  1003.                 //We cannot have any buttons if there's no default button above.
  1004.                 //We cannot have the unpublish button (Z) when there's no publish permission.
  1005.                 //We cannot have the unpublish button (Z) when the item is not published.
  1006.                 if (defaultAction) {
  1007.                     //get the last index of the button order
  1008.                     var lastIndex = _.indexOf(actionOrder, defaultAction);
  1009.  
  1010.                     //add the remaining
  1011.                     for (var i = lastIndex + 1; i < actionOrder.length; i++) {
  1012.                         if (_.contains(content.allowedActions, actionOrder[i])) {
  1013.                             actions.push(actionOrder[i]);
  1014.                         }
  1015.                     }
  1016.  
  1017.                     //if we are not creating, then we should add unpublish too,
  1018.                     // so long as it's already published and if the user has access to publish
  1019.                     if (!creating) {
  1020.                         if (content.publishDate && _.contains(content.allowedActions,"U")) {
  1021.                             actions.push("Z");
  1022.                         }
  1023.                     }
  1024.                 }
  1025.  
  1026.                 return actions;
  1027.          },
  1028.  
  1029.          /**
  1030.           * @ngdoc method
  1031.           * @name umbraco.services.contentEditingHelper#getButtonFromAction
  1032.           * @methodOf umbraco.services.contentEditingHelper
  1033.           * @function
  1034.           *
  1035.           * @description
  1036.           * Returns a button object to render a button for the tabbed editor
  1037.           * currently only returns built in system buttons for content and media actions
  1038.           * returns label, alias, action char and hot-key
  1039.           */
  1040.           getButtonFromAction : function(ch){
  1041.             switch (ch) {
  1042.                 case "U":
  1043.                     return {
  1044.                         letter: ch,
  1045.                         labelKey: "buttons_saveAndPublish",
  1046.                         handler: "saveAndPublish",
  1047.                         hotKey: "ctrl+p"
  1048.                     };
  1049.                 case "H":
  1050.                     //send to publish
  1051.                     return {
  1052.                         letter: ch,
  1053.                         labelKey: "buttons_saveToPublish",
  1054.                         handler: "sendToPublish",
  1055.                         hotKey: "ctrl+p"
  1056.                     };
  1057.                 case "A":
  1058.                     return {
  1059.                         letter: ch,
  1060.                         labelKey: "buttons_save",
  1061.                         handler: "save",
  1062.                         hotKey: "ctrl+s"
  1063.                     };
  1064.                 case "Z":
  1065.                     return {
  1066.                         letter: ch,
  1067.                         labelKey: "content_unPublish",
  1068.                         handler: "unPublish"
  1069.                     };
  1070.  
  1071.                 default:
  1072.                     return null;
  1073.             }
  1074.  
  1075.           },
  1076.         /**
  1077.          * @ngdoc method
  1078.          * @name umbraco.services.contentEditingHelper#reBindChangedProperties
  1079.          * @methodOf umbraco.services.contentEditingHelper
  1080.          * @function
  1081.          *
  1082.          * @description
  1083.          * re-binds all changed property values to the origContent object from the savedContent object and returns an array of changed properties.
  1084.          */
  1085.         reBindChangedProperties: function (origContent, savedContent) {
  1086.  
  1087.             var changed = [];
  1088.  
  1089.             //get a list of properties since they are contained in tabs
  1090.             var allOrigProps = this.getAllProps(origContent);
  1091.             var allNewProps = this.getAllProps(savedContent);
  1092.  
  1093.             function getNewProp(alias) {
  1094.                 return _.find(allNewProps, function (item) {
  1095.                     return item.alias === alias;
  1096.                 });
  1097.             }
  1098.  
  1099.             //a method to ignore built-in prop changes
  1100.             var shouldIgnore = function(propName) {
  1101.                 return _.some(["tabs", "notifications", "ModelState", "tabs", "properties"], function(i) {
  1102.                     return i === propName;
  1103.                 });
  1104.             };
  1105.             //check for changed built-in properties of the content
  1106.             for (var o in origContent) {
  1107.  
  1108.                 //ignore the ones listed in the array
  1109.                 if (shouldIgnore(o)) {
  1110.                     continue;
  1111.                 }
  1112.  
  1113.                 if (!_.isEqual(origContent[o], savedContent[o])) {
  1114.                     origContent[o] = savedContent[o];
  1115.                 }
  1116.             }
  1117.  
  1118.             //check for changed properties of the content
  1119.             for (var p in allOrigProps) {
  1120.                 var newProp = getNewProp(allOrigProps[p].alias);
  1121.                 if (newProp && !_.isEqual(allOrigProps[p].value, newProp.value)) {
  1122.  
  1123.                     //they have changed so set the origContent prop to the new one
  1124.                     var origVal = allOrigProps[p].value;
  1125.                     allOrigProps[p].value = newProp.value;
  1126.  
  1127.                     //instead of having a property editor $watch their expression to check if it has
  1128.                     // been updated, instead we'll check for the existence of a special method on their model
  1129.                     // and just call it.
  1130.                     if (angular.isFunction(allOrigProps[p].onValueChanged)) {
  1131.                         //send the newVal + oldVal
  1132.                         allOrigProps[p].onValueChanged(allOrigProps[p].value, origVal);
  1133.                     }
  1134.  
  1135.                     changed.push(allOrigProps[p]);
  1136.                 }
  1137.             }
  1138.  
  1139.             return changed;
  1140.         },
  1141.  
  1142.         /**
  1143.          * @ngdoc function
  1144.          * @name umbraco.services.contentEditingHelper#handleSaveError
  1145.          * @methodOf umbraco.services.contentEditingHelper
  1146.          * @function
  1147.          *
  1148.          * @description
  1149.          * A function to handle what happens when we have validation issues from the server side
  1150.          */
  1151.         handleSaveError: function (args) {
  1152.  
  1153.             if (!args.err) {
  1154.                 throw "args.err cannot be null";
  1155.             }
  1156.             if (args.redirectOnFailure === undefined || args.redirectOnFailure === null) {
  1157.                 throw "args.redirectOnFailure must be set to true or false";
  1158.             }
  1159.  
  1160.             //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors.
  1161.             //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
  1162.             //Or, some strange server error
  1163.             if (args.err.status === 400) {
  1164.                 //now we need to look through all the validation errors
  1165.                 if (args.err.data && (args.err.data.ModelState)) {
  1166.  
  1167.                     //wire up the server validation errs
  1168.                     formHelper.handleServerValidation(args.err.data.ModelState);
  1169.  
  1170.                     if (!args.redirectOnFailure || !this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) {
  1171.                         //we are not redirecting because this is not new content, it is existing content. In this case
  1172.                         // we need to detect what properties have changed and re-bind them with the server data. Then we need
  1173.                         // to re-bind any server validation errors after the digest takes place.
  1174.  
  1175.                         if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
  1176.                             args.rebindCallback();
  1177.                         }
  1178.  
  1179.                         serverValidationManager.executeAndClearAllSubscriptions();
  1180.                     }
  1181.  
  1182.                     //indicates we've handled the server result
  1183.                     return true;
  1184.                 }
  1185.                 else {
  1186.                     dialogService.ysodDialog(args.err);
  1187.                 }
  1188.             }
  1189.             else {
  1190.                 dialogService.ysodDialog(args.err);
  1191.             }
  1192.  
  1193.             return false;
  1194.         },
  1195.  
  1196.         /**
  1197.          * @ngdoc function
  1198.          * @name umbraco.services.contentEditingHelper#handleSuccessfulSave
  1199.          * @methodOf umbraco.services.contentEditingHelper
  1200.          * @function
  1201.          *
  1202.          * @description
  1203.          * A function to handle when saving a content item is successful. This will rebind the values of the model that have changed
  1204.          * ensure the notifications are displayed and that the appropriate events are fired. This will also check if we need to redirect
  1205.          * when we're creating new content.
  1206.          */
  1207.         handleSuccessfulSave: function (args) {
  1208.  
  1209.             if (!args) {
  1210.                 throw "args cannot be null";
  1211.             }
  1212.             if (!args.savedContent) {
  1213.                 throw "args.savedContent cannot be null";
  1214.             }
  1215.  
  1216.             if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id)) {
  1217.  
  1218.                 //we are not redirecting because this is not new content, it is existing content. In this case
  1219.                 // we need to detect what properties have changed and re-bind them with the server data.
  1220.                 //call the callback
  1221.                 if (args.rebindCallback && angular.isFunction(args.rebindCallback)) {
  1222.                     args.rebindCallback();
  1223.                 }
  1224.             }
  1225.         },
  1226.  
  1227.         /**
  1228.          * @ngdoc function
  1229.          * @name umbraco.services.contentEditingHelper#redirectToCreatedContent
  1230.          * @methodOf umbraco.services.contentEditingHelper
  1231.          * @function
  1232.          *
  1233.          * @description
  1234.          * Changes the location to be editing the newly created content after create was successful.
  1235.          * We need to decide if we need to redirect to edito mode or if we will remain in create mode.
  1236.          * 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
  1237.          */
  1238.         redirectToCreatedContent: function (id, modelState) {
  1239.  
  1240.             //only continue if we are currently in create mode and if there is no 'Name' modelstate errors
  1241.             // since we need at least a name to create content.
  1242.             if ($routeParams.create && (isValidIdentifier(id) && (!modelState || !modelState["Name"]))) {
  1243.  
  1244.                 //need to change the location to not be in 'create' mode. Currently the route will be something like:
  1245.                 // /belle/#/content/edit/1234?doctype=newsArticle&create=true
  1246.                 // but we need to remove everything after the query so that it is just:
  1247.                 // /belle/#/content/edit/9876 (where 9876 is the new id)
  1248.  
  1249.                 //clear the query strings
  1250.                 $location.search("");
  1251.  
  1252.                 //change to new path
  1253.                 $location.path("/" + $routeParams.section + "/" + $routeParams.tree  + "/" + $routeParams.method + "/" + id);
  1254.                 //don't add a browser history for this
  1255.                 $location.replace();
  1256.                 return true;
  1257.             }
  1258.             return false;
  1259.         }
  1260.     };
  1261. }
  1262. angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper);
  1263.  
  1264. /**
  1265.  * @ngdoc service
  1266.  * @name umbraco.services.contentTypeHelper
  1267.  * @description A helper service for the content type editor
  1268.  **/
  1269. function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $injector, $q) {
  1270.  
  1271.     var contentTypeHelperService = {
  1272.  
  1273.         createIdArray: function(array) {
  1274.  
  1275.           var newArray = [];
  1276.  
  1277.           angular.forEach(array, function(arrayItem){
  1278.  
  1279.             if(angular.isObject(arrayItem)) {
  1280.               newArray.push(arrayItem.id);
  1281.             } else {
  1282.               newArray.push(arrayItem);
  1283.             }
  1284.  
  1285.           });
  1286.  
  1287.           return newArray;
  1288.  
  1289.         },
  1290.  
  1291.         generateModels: function () {
  1292.             var deferred = $q.defer();
  1293.             var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null;
  1294.             var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled;
  1295.             if (modelsBuilderEnabled && modelsResource) {
  1296.                 modelsResource.buildModels().then(function(result) {
  1297.                     deferred.resolve(result);
  1298.  
  1299.                     //just calling this to get the servar back to life
  1300.                     modelsResource.getModelsOutOfDateStatus();
  1301.  
  1302.                 }, function(e) {
  1303.                     deferred.reject(e);
  1304.                 });
  1305.             }
  1306.             else {                
  1307.                 deferred.resolve(false);                
  1308.             }
  1309.             return deferred.promise;
  1310.         },
  1311.  
  1312.         checkModelsBuilderStatus: function () {
  1313.             var deferred = $q.defer();
  1314.             var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null;
  1315.             var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true);            
  1316.            
  1317.             if (modelsBuilderEnabled && modelsResource) {
  1318.                 modelsResource.getModelsOutOfDateStatus().then(function(result) {
  1319.                     //Generate models buttons should be enabled if it is 0
  1320.                     deferred.resolve(result.status === 0);
  1321.                 });
  1322.             }
  1323.             else {
  1324.                 deferred.resolve(false);
  1325.             }
  1326.             return deferred.promise;
  1327.         },
  1328.  
  1329.         makeObjectArrayFromId: function (idArray, objectArray) {
  1330.            var newArray = [];
  1331.  
  1332.            for (var idIndex = 0; idArray.length > idIndex; idIndex++) {
  1333.              var id = idArray[idIndex];
  1334.  
  1335.              for (var objectIndex = 0; objectArray.length > objectIndex; objectIndex++) {
  1336.                  var object = objectArray[objectIndex];
  1337.                  if (id === object.id) {
  1338.                     newArray.push(object);
  1339.                  }
  1340.              }
  1341.  
  1342.            }
  1343.  
  1344.            return newArray;
  1345.         },
  1346.  
  1347.         validateAddingComposition: function(contentType, compositeContentType) {
  1348.  
  1349.             //Validate that by adding this group that we are not adding duplicate property type aliases
  1350.  
  1351.             var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function(g) {
  1352.                 return _.map(g.properties, function(p) {
  1353.                     return p.alias;
  1354.                 });
  1355.             }));
  1356.             var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function(g) {
  1357.                 return _.map(g.properties, function(p) {
  1358.                     return p.alias;
  1359.                 });
  1360.             })), function(f) {
  1361.                 return f !== null && f !== undefined;
  1362.             });
  1363.  
  1364.             var intersec = _.intersection(propertiesAdding, propAliasesExisting);
  1365.             if (intersec.length > 0) {
  1366.                 //return the overlapping property aliases
  1367.                 return intersec;
  1368.             }
  1369.  
  1370.             //no overlapping property aliases
  1371.             return [];
  1372.         },
  1373.  
  1374.         mergeCompositeContentType: function(contentType, compositeContentType) {
  1375.  
  1376.             //Validate that there are no overlapping aliases
  1377.             var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType);
  1378.             if (overlappingAliases.length > 0) {
  1379.                 throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join());
  1380.             }
  1381.  
  1382.            angular.forEach(compositeContentType.groups, function(compositionGroup) {
  1383.  
  1384.               // order composition groups based on sort order
  1385.               compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder');
  1386.  
  1387.               // get data type details
  1388.               angular.forEach(compositionGroup.properties, function(property) {
  1389.                  dataTypeResource.getById(property.dataTypeId)
  1390.                     .then(function(dataType) {
  1391.                        property.dataTypeIcon = dataType.icon;
  1392.                        property.dataTypeName = dataType.name;
  1393.                     });
  1394.               });
  1395.  
  1396.               // set inherited state on tab
  1397.               compositionGroup.inherited = true;
  1398.  
  1399.               // set inherited state on properties
  1400.               angular.forEach(compositionGroup.properties, function(compositionProperty) {
  1401.                  compositionProperty.inherited = true;
  1402.               });
  1403.  
  1404.               // set tab state
  1405.               compositionGroup.tabState = "inActive";
  1406.  
  1407.               // if groups are named the same - merge the groups
  1408.               angular.forEach(contentType.groups, function(contentTypeGroup) {
  1409.  
  1410.                  if (contentTypeGroup.name === compositionGroup.name) {
  1411.  
  1412.                     // set flag to show if properties has been merged into a tab
  1413.                     compositionGroup.groupIsMerged = true;
  1414.  
  1415.                     // make group inherited
  1416.                     contentTypeGroup.inherited = true;
  1417.  
  1418.                     // add properties to the top of the array
  1419.                     contentTypeGroup.properties = compositionGroup.properties.concat(contentTypeGroup.properties);
  1420.  
  1421.                     // update sort order on all properties in merged group
  1422.                     contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties);
  1423.  
  1424.                     // make parentTabContentTypeNames to an array so we can push values
  1425.                     if (contentTypeGroup.parentTabContentTypeNames === null || contentTypeGroup.parentTabContentTypeNames === undefined) {
  1426.                        contentTypeGroup.parentTabContentTypeNames = [];
  1427.                     }
  1428.  
  1429.                     // push name to array of merged composite content types
  1430.                     contentTypeGroup.parentTabContentTypeNames.push(compositeContentType.name);
  1431.  
  1432.                     // make parentTabContentTypes to an array so we can push values
  1433.                     if (contentTypeGroup.parentTabContentTypes === null || contentTypeGroup.parentTabContentTypes === undefined) {
  1434.                        contentTypeGroup.parentTabContentTypes = [];
  1435.                     }
  1436.  
  1437.                     // push id to array of merged composite content types
  1438.                     contentTypeGroup.parentTabContentTypes.push(compositeContentType.id);
  1439.  
  1440.                     // get sort order from composition
  1441.                     contentTypeGroup.sortOrder = compositionGroup.sortOrder;
  1442.  
  1443.                     // splice group to the top of the array
  1444.                     var contentTypeGroupCopy = angular.copy(contentTypeGroup);
  1445.                     var index = contentType.groups.indexOf(contentTypeGroup);
  1446.                     contentType.groups.splice(index, 1);
  1447.                     contentType.groups.unshift(contentTypeGroupCopy);
  1448.  
  1449.                  }
  1450.  
  1451.               });
  1452.  
  1453.               // if group is not merged - push it to the end of the array - before init tab
  1454.               if (compositionGroup.groupIsMerged === false || compositionGroup.groupIsMerged === undefined) {
  1455.  
  1456.                  // make parentTabContentTypeNames to an array so we can push values
  1457.                  if (compositionGroup.parentTabContentTypeNames === null || compositionGroup.parentTabContentTypeNames === undefined) {
  1458.                     compositionGroup.parentTabContentTypeNames = [];
  1459.                  }
  1460.  
  1461.                  // push name to array of merged composite content types
  1462.                  compositionGroup.parentTabContentTypeNames.push(compositeContentType.name);
  1463.  
  1464.                  // make parentTabContentTypes to an array so we can push values
  1465.                  if (compositionGroup.parentTabContentTypes === null || compositionGroup.parentTabContentTypes === undefined) {
  1466.                     compositionGroup.parentTabContentTypes = [];
  1467.                  }
  1468.  
  1469.                  // push id to array of merged composite content types
  1470.                  compositionGroup.parentTabContentTypes.push(compositeContentType.id);
  1471.                  
  1472.                  // push group before placeholder tab
  1473.                  contentType.groups.unshift(compositionGroup);
  1474.  
  1475.               }
  1476.  
  1477.            });
  1478.  
  1479.            // sort all groups by sortOrder property
  1480.            contentType.groups = $filter('orderBy')(contentType.groups, 'sortOrder');
  1481.  
  1482.            return contentType;
  1483.  
  1484.         },
  1485.  
  1486.         splitCompositeContentType: function (contentType, compositeContentType) {
  1487.  
  1488.             var groups = [];
  1489.  
  1490.             angular.forEach(contentType.groups, function(contentTypeGroup){
  1491.  
  1492.                 if( contentTypeGroup.tabState !== "init" ) {
  1493.  
  1494.                     var idIndex = contentTypeGroup.parentTabContentTypes.indexOf(compositeContentType.id);
  1495.                     var nameIndex = contentTypeGroup.parentTabContentTypeNames.indexOf(compositeContentType.name);
  1496.                     var groupIndex = contentType.groups.indexOf(contentTypeGroup);
  1497.  
  1498.  
  1499.                     if( idIndex !== -1  ) {
  1500.  
  1501.                         var properties = [];
  1502.  
  1503.                         // remove all properties from composite content type
  1504.                         angular.forEach(contentTypeGroup.properties, function(property){
  1505.                             if(property.contentTypeId !== compositeContentType.id) {
  1506.                                 properties.push(property);
  1507.                             }
  1508.                         });
  1509.  
  1510.                         // set new properties array to properties
  1511.                         contentTypeGroup.properties = properties;
  1512.  
  1513.                         // remove composite content type name and id from inherited arrays
  1514.                         contentTypeGroup.parentTabContentTypes.splice(idIndex, 1);
  1515.                         contentTypeGroup.parentTabContentTypeNames.splice(nameIndex, 1);
  1516.  
  1517.                         // remove inherited state if there are no inherited properties
  1518.                         if(contentTypeGroup.parentTabContentTypes.length === 0) {
  1519.                             contentTypeGroup.inherited = false;
  1520.                         }
  1521.  
  1522.                         // remove group if there are no properties left
  1523.                         if(contentTypeGroup.properties.length > 1) {
  1524.                             //contentType.groups.splice(groupIndex, 1);
  1525.                             groups.push(contentTypeGroup);
  1526.                         }
  1527.  
  1528.                     } else {
  1529.                       groups.push(contentTypeGroup);
  1530.                     }
  1531.  
  1532.                 } else {
  1533.                   groups.push(contentTypeGroup);
  1534.                 }
  1535.  
  1536.                 // update sort order on properties
  1537.                 contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties);
  1538.  
  1539.             });
  1540.  
  1541.             contentType.groups = groups;
  1542.  
  1543.         },
  1544.  
  1545.         updatePropertiesSortOrder: function (properties) {
  1546.  
  1547.           var sortOrder = 0;
  1548.  
  1549.           angular.forEach(properties, function(property) {
  1550.             if( !property.inherited && property.propertyState !== "init") {
  1551.               property.sortOrder = sortOrder;
  1552.             }
  1553.             sortOrder++;
  1554.           });
  1555.  
  1556.           return properties;
  1557.  
  1558.         },
  1559.  
  1560.         getTemplatePlaceholder: function() {
  1561.  
  1562.           var templatePlaceholder = {
  1563.             "name": "",
  1564.             "icon": "icon-layout",
  1565.             "alias": "templatePlaceholder",
  1566.             "placeholder": true
  1567.           };
  1568.  
  1569.           return templatePlaceholder;
  1570.  
  1571.         },
  1572.  
  1573.         insertDefaultTemplatePlaceholder: function(defaultTemplate) {
  1574.  
  1575.           // get template placeholder
  1576.           var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder();
  1577.  
  1578.           // add as default template
  1579.           defaultTemplate = templatePlaceholder;
  1580.  
  1581.           return defaultTemplate;
  1582.  
  1583.         },
  1584.  
  1585.         insertTemplatePlaceholder: function(array) {
  1586.  
  1587.           // get template placeholder
  1588.           var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder();
  1589.  
  1590.           // add as selected item
  1591.           array.push(templatePlaceholder);
  1592.  
  1593.           return array;
  1594.  
  1595.        },
  1596.  
  1597.        insertChildNodePlaceholder: function (array, name, icon, id) {
  1598.  
  1599.          var placeholder = {
  1600.            "name": name,
  1601.            "icon": icon,
  1602.            "id": id
  1603.          };
  1604.  
  1605.          array.push(placeholder);
  1606.  
  1607.        }
  1608.  
  1609.     };
  1610.  
  1611.     return contentTypeHelperService;
  1612. }
  1613. angular.module('umbraco.services').factory('contentTypeHelper', contentTypeHelper);
  1614.  
  1615. /**
  1616. * @ngdoc service
  1617. * @name umbraco.services.cropperHelper
  1618. * @description A helper object used for dealing with image cropper data
  1619. **/
  1620. function cropperHelper(umbRequestHelper, $http) {
  1621.     var service = {
  1622.  
  1623.         /**
  1624.         * @ngdoc method
  1625.         * @name umbraco.services.cropperHelper#configuration
  1626.         * @methodOf umbraco.services.cropperHelper
  1627.         *
  1628.         * @description
  1629.         * Returns a collection of plugins available to the tinyMCE editor
  1630.         *
  1631.         */
  1632.         configuration: function (mediaTypeAlias) {
  1633.             return umbRequestHelper.resourcePromise(
  1634.                 $http.get(
  1635.                     umbRequestHelper.getApiUrl(
  1636.                         "imageCropperApiBaseUrl",
  1637.                         "GetConfiguration",
  1638.                         [{ mediaTypeAlias: mediaTypeAlias}])),
  1639.                 'Failed to retrieve tinymce configuration');
  1640.         },
  1641.  
  1642.  
  1643.         //utill for getting either min/max aspect ratio to scale image after
  1644.         calculateAspectRatioFit : function(srcWidth, srcHeight, maxWidth, maxHeight, maximize) {
  1645.             var ratio = [maxWidth / srcWidth, maxHeight / srcHeight ];
  1646.  
  1647.             if(maximize){
  1648.                 ratio = Math.max(ratio[0], ratio[1]);
  1649.             }else{
  1650.                 ratio = Math.min(ratio[0], ratio[1]);
  1651.             }
  1652.  
  1653.             return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio};
  1654.         },
  1655.  
  1656.         //utill for scaling width / height given a ratio
  1657.         calculateSizeToRatio : function(srcWidth, srcHeight, ratio) {
  1658.             return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio};
  1659.         },
  1660.  
  1661.         scaleToMaxSize : function(srcWidth, srcHeight, maxSize) {
  1662.            
  1663.             var retVal = {height: srcHeight, width: srcWidth};
  1664.  
  1665.             if(srcWidth > maxSize ||srcHeight > maxSize){
  1666.                 var ratio = [maxSize / srcWidth, maxSize / srcHeight ];
  1667.                 ratio = Math.min(ratio[0], ratio[1]);
  1668.                
  1669.                 retVal.height = srcHeight * ratio;
  1670.                 retVal.width = srcWidth * ratio;
  1671.             }
  1672.            
  1673.             return retVal;         
  1674.         },
  1675.  
  1676.         //returns a ng-style object with top,left,width,height pixel measurements
  1677.         //expects {left,right,top,bottom} - {width,height}, {width,height}, int
  1678.         //offset is just to push the image position a number of pixels from top,left    
  1679.         convertToStyle : function(coordinates, originalSize, viewPort, offset){
  1680.  
  1681.             var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset);
  1682.             var _offset = offset || 0;
  1683.  
  1684.             var x = 1 - (coordinates.x1 + Math.abs(coordinates.x2));
  1685.             var left_of_x =  originalSize.width * x;
  1686.             var ratio = viewPort.width / left_of_x;
  1687.  
  1688.             var style = {
  1689.                 position: "absolute",
  1690.                 top:  -(coordinates_px.y1*ratio)+ _offset,
  1691.                 left:  -(coordinates_px.x1* ratio)+ _offset,
  1692.                 width: Math.floor(originalSize.width * ratio),
  1693.                 height: Math.floor(originalSize.height * ratio),
  1694.                 originalWidth: originalSize.width,
  1695.                 originalHeight: originalSize.height,
  1696.                 ratio: ratio
  1697.             };
  1698.  
  1699.             return style;
  1700.         },
  1701.  
  1702.          
  1703.         coordinatesToPixels : function(coordinates, originalSize, offset){
  1704.  
  1705.             var coordinates_px = {
  1706.                 x1: Math.floor(coordinates.x1 * originalSize.width),
  1707.                 y1: Math.floor(coordinates.y1 * originalSize.height),
  1708.                 x2: Math.floor(coordinates.x2 * originalSize.width),
  1709.                 y2: Math.floor(coordinates.y2 * originalSize.height)                                 
  1710.             };
  1711.  
  1712.             return coordinates_px;
  1713.         },
  1714.  
  1715.         pixelsToCoordinates : function(image, width, height, offset){
  1716.  
  1717.             var x1_px = Math.abs(image.left-offset);
  1718.             var y1_px = Math.abs(image.top-offset);
  1719.  
  1720.             var x2_px = image.width - (x1_px + width);
  1721.             var y2_px = image.height - (y1_px + height);
  1722.  
  1723.  
  1724.             //crop coordinates in %
  1725.             var crop = {};
  1726.             crop.x1 = x1_px / image.width;
  1727.             crop.y1 = y1_px / image.height;
  1728.             crop.x2 = x2_px / image.width;
  1729.             crop.y2 = y2_px / image.height;
  1730.  
  1731.             for(var coord in crop){
  1732.                 if(crop[coord] < 0){
  1733.                     crop[coord] = 0;
  1734.                 }
  1735.             }
  1736.  
  1737.             return crop;
  1738.         },
  1739.  
  1740.         centerInsideViewPort : function(img, viewport){
  1741.             var left = viewport.width/ 2 - img.width / 2,
  1742.                 top = viewport.height / 2 - img.height / 2;
  1743.            
  1744.             return {left: left, top: top};
  1745.         },
  1746.  
  1747.         alignToCoordinates : function(image, center, viewport){
  1748.            
  1749.             var min_left = (image.width) - (viewport.width);
  1750.             var min_top =  (image.height) - (viewport.height);
  1751.  
  1752.             var c_top = -(center.top * image.height) + (viewport.height / 2);
  1753.             var c_left = -(center.left * image.width) + (viewport.width / 2);
  1754.  
  1755.             if(c_top < -min_top){
  1756.                 c_top = -min_top;
  1757.             }
  1758.             if(c_top > 0){
  1759.                 c_top = 0;
  1760.             }
  1761.             if(c_left < -min_left){
  1762.                 c_left = -min_left;
  1763.             }
  1764.             if(c_left > 0){
  1765.                 c_left = 0;
  1766.             }
  1767.             return {left: c_left, top: c_top};
  1768.         },
  1769.  
  1770.  
  1771.         syncElements : function(source, target){
  1772.                 target.height(source.height());
  1773.                 target.width(source.width());
  1774.  
  1775.                 target.css({
  1776.                     "top": source[0].offsetTop,
  1777.                     "left": source[0].offsetLeft
  1778.                 });
  1779.         }
  1780.     };
  1781.  
  1782.     return service;
  1783. }
  1784.  
  1785. angular.module('umbraco.services').factory('cropperHelper', cropperHelper);
  1786.  
  1787. /**
  1788.  * @ngdoc service
  1789.  * @name umbraco.services.dataTypeHelper
  1790.  * @description A helper service for data types
  1791.  **/
  1792. function dataTypeHelper() {
  1793.  
  1794.     var dataTypeHelperService = {
  1795.  
  1796.         createPreValueProps: function(preVals) {
  1797.  
  1798.             var preValues = [];
  1799.  
  1800.             for (var i = 0; i < preVals.length; i++) {
  1801.                 preValues.push({
  1802.                     hideLabel: preVals[i].hideLabel,
  1803.                     alias: preVals[i].key,
  1804.                     description: preVals[i].description,
  1805.                     label: preVals[i].label,
  1806.                     view: preVals[i].view,
  1807.                     value: preVals[i].value
  1808.                 });
  1809.             }
  1810.  
  1811.             return preValues;
  1812.  
  1813.         },
  1814.  
  1815.         rebindChangedProperties: function (origContent, savedContent) {
  1816.  
  1817.             //a method to ignore built-in prop changes
  1818.             var shouldIgnore = function (propName) {
  1819.                 return _.some(["notifications", "ModelState"], function (i) {
  1820.                     return i === propName;
  1821.                 });
  1822.             };
  1823.             //check for changed built-in properties of the content
  1824.             for (var o in origContent) {
  1825.  
  1826.                 //ignore the ones listed in the array
  1827.                 if (shouldIgnore(o)) {
  1828.                     continue;
  1829.                 }
  1830.  
  1831.                 if (!_.isEqual(origContent[o], savedContent[o])) {
  1832.                     origContent[o] = savedContent[o];
  1833.                 }
  1834.             }
  1835.         }
  1836.  
  1837.     };
  1838.  
  1839.     return dataTypeHelperService;
  1840. }
  1841. angular.module('umbraco.services').factory('dataTypeHelper', dataTypeHelper);
  1842. /**
  1843.  * @ngdoc service
  1844.  * @name umbraco.services.dialogService
  1845.  *
  1846.  * @requires $rootScope
  1847.  * @requires $compile
  1848.  * @requires $http
  1849.  * @requires $log
  1850.  * @requires $q
  1851.  * @requires $templateCache
  1852.  *
  1853.  * @description
  1854.  * Application-wide service for handling modals, overlays and dialogs
  1855.  * By default it injects the passed template url into a div to body of the document
  1856.  * And renders it, but does also support rendering items in an iframe, incase
  1857.  * serverside processing is needed, or its a non-angular page
  1858.  *
  1859.  * ##usage
  1860.  * To use, simply inject the dialogService into any controller that needs it, and make
  1861.  * sure the umbraco.services module is accesible - which it should be by default.
  1862.  *
  1863.  * <pre>
  1864.  *    var dialog = dialogService.open({template: 'path/to/page.html', show: true, callback: done});
  1865.  *    functon done(data){
  1866.  *      //The dialog has been submitted
  1867.  *      //data contains whatever the dialog has selected / attached
  1868.  *    }
  1869.  * </pre>
  1870.  */
  1871.  
  1872. angular.module('umbraco.services')
  1873. .factory('dialogService', function ($rootScope, $compile, $http, $timeout, $q, $templateCache, appState, eventsService) {
  1874.  
  1875.     var dialogs = [];
  1876.  
  1877.     /** Internal method that removes all dialogs */
  1878.     function removeAllDialogs(args) {
  1879.         for (var i = 0; i < dialogs.length; i++) {
  1880.             var dialog = dialogs[i];
  1881.  
  1882.             //very special flag which means that global events cannot close this dialog - currently only used on the login
  1883.             // dialog since it's special and cannot be closed without logging in.
  1884.             if (!dialog.manualClose) {
  1885.                 dialog.close(args);
  1886.             }
  1887.  
  1888.         }
  1889.     }
  1890.  
  1891.     /** Internal method that closes the dialog properly and cleans up resources */
  1892.     function closeDialog(dialog) {
  1893.  
  1894.         if (dialog.element) {
  1895.             dialog.element.modal('hide');
  1896.  
  1897.             //this is not entirely enough since the damn webforms scriploader still complains
  1898.             if (dialog.iframe) {
  1899.                 dialog.element.find("iframe").attr("src", "about:blank");
  1900.             }
  1901.  
  1902.             dialog.scope.$destroy();
  1903.  
  1904.             //we need to do more than just remove the element, this will not destroy the
  1905.             // scope in angular 1.1x, in angular 1.2x this is taken care of but if we dont
  1906.             // take care of this ourselves we have memory leaks.
  1907.             dialog.element.remove();
  1908.  
  1909.             //remove 'this' dialog from the dialogs array
  1910.             dialogs = _.reject(dialogs, function (i) { return i === dialog; });
  1911.         }
  1912.     }
  1913.  
  1914.     /** Internal method that handles opening all dialogs */
  1915.     function openDialog(options) {
  1916.         var defaults = {
  1917.             container: $("body"),
  1918.             animation: "fade",
  1919.             modalClass: "umb-modal",
  1920.             width: "100%",
  1921.             inline: false,
  1922.             iframe: false,
  1923.             show: true,
  1924.             template: "views/common/notfound.html",
  1925.             callback: undefined,
  1926.             closeCallback: undefined,
  1927.             element: undefined,
  1928.             // It will set this value as a property on the dialog controller's scope as dialogData,
  1929.             // used to pass in custom data to the dialog controller's $scope. Though this is near identical to
  1930.             // the dialogOptions property that is also set the the dialog controller's $scope object.
  1931.             // So there's basically 2 ways of doing the same thing which we're now stuck with and in fact
  1932.             // dialogData has another specially attached property called .selection which gets used.
  1933.             dialogData: undefined
  1934.         };
  1935.  
  1936.         var dialog = angular.extend(defaults, options);
  1937.  
  1938.         //NOTE: People should NOT pass in a scope object that is legacy functoinality and causes problems. We will ALWAYS
  1939.         // destroy the scope when the dialog is closed regardless if it is in use elsewhere which is why it shouldn't be done.
  1940.         var scope = options.scope || $rootScope.$new();
  1941.  
  1942.         //Modal dom obj and set id to old-dialog-service - used until we get all dialogs moved the the new overlay directive
  1943.         dialog.element = $('<div ng-swipe-right="swipeHide($event)"  data-backdrop="false"></div>');
  1944.         var id = "old-dialog-service";
  1945.  
  1946.         if (options.inline) {
  1947.             dialog.animation = "";
  1948.         }
  1949.         else {
  1950.             dialog.element.addClass("modal");
  1951.             dialog.element.addClass("hide");
  1952.         }
  1953.  
  1954.         //set the id and add classes
  1955.         dialog.element
  1956.             .attr('id', id)
  1957.             .addClass(dialog.animation)
  1958.             .addClass(dialog.modalClass);
  1959.  
  1960.         //push the modal into the global modal collection
  1961.         //we halt the .push because a link click will trigger a closeAll right away
  1962.         $timeout(function () {
  1963.             dialogs.push(dialog);
  1964.         }, 500);
  1965.  
  1966.  
  1967.         dialog.close = function (data) {
  1968.             if (dialog.closeCallback) {
  1969.                 dialog.closeCallback(data);
  1970.             }
  1971.  
  1972.             closeDialog(dialog);
  1973.         };
  1974.  
  1975.         //if iframe is enabled, inject that instead of a template
  1976.         if (dialog.iframe) {
  1977.             var html = $("<iframe src='" + dialog.template + "' class='auto-expand' style='border: none; width: 100%; height: 100%;'></iframe>");
  1978.             dialog.element.html(html);
  1979.  
  1980.             //append to body or whatever element is passed in as options.containerElement
  1981.             dialog.container.append(dialog.element);
  1982.  
  1983.             // Compile modal content
  1984.             $timeout(function () {
  1985.                 $compile(dialog.element)(dialog.scope);
  1986.             });
  1987.  
  1988.             dialog.element.css("width", dialog.width);
  1989.  
  1990.             //Autoshow
  1991.             if (dialog.show) {
  1992.                 dialog.element.modal('show');
  1993.             }
  1994.  
  1995.             dialog.scope = scope;
  1996.             return dialog;
  1997.         }
  1998.         else {
  1999.  
  2000.             //We need to load the template with an httpget and once it's loaded we'll compile and assign the result to the container
  2001.             // 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
  2002.             // $modal object so we'll actually return the modal object synchronously without waiting for the promise. Otherwise this openDialog
  2003.             // method will always need to return a promise which gets nasty because of promises in promises plus the result just needs a reference
  2004.             // to the $modal object which will not change (only it's contents will change).
  2005.             $q.when($templateCache.get(dialog.template) || $http.get(dialog.template, { cache: true }).then(function (res) { return res.data; }))
  2006.                 .then(function onSuccess(template) {
  2007.  
  2008.                     // Build modal object
  2009.                     dialog.element.html(template);
  2010.  
  2011.                     //append to body or other container element
  2012.                     dialog.container.append(dialog.element);
  2013.  
  2014.                     // Compile modal content
  2015.                     $timeout(function () {
  2016.                         $compile(dialog.element)(scope);
  2017.                     });
  2018.  
  2019.                     scope.dialogOptions = dialog;
  2020.  
  2021.                     //Scope to handle data from the modal form
  2022.                     scope.dialogData = dialog.dialogData ? dialog.dialogData : {};
  2023.                     scope.dialogData.selection = [];
  2024.  
  2025.                     // Provide scope display functions
  2026.                     //this passes the modal to the current scope
  2027.                     scope.$modal = function (name) {
  2028.                         dialog.element.modal(name);
  2029.                     };
  2030.  
  2031.                     scope.swipeHide = function (e) {
  2032.  
  2033.                         if (appState.getGlobalState("touchDevice")) {
  2034.                             var selection = window.getSelection();
  2035.                             if (selection.type !== "Range") {
  2036.                                 scope.hide();
  2037.                             }
  2038.                         }
  2039.                     };
  2040.  
  2041.                     //NOTE: Same as 'close' without the callbacks
  2042.                     scope.hide = function () {
  2043.                         closeDialog(dialog);
  2044.                     };
  2045.  
  2046.                     //basic events for submitting and closing
  2047.                     scope.submit = function (data) {
  2048.                         if (dialog.callback) {
  2049.                             dialog.callback(data);
  2050.                         }
  2051.  
  2052.                         closeDialog(dialog);
  2053.                     };
  2054.  
  2055.                     scope.close = function (data) {
  2056.                         dialog.close(data);
  2057.                     };
  2058.  
  2059.                     //NOTE: This can ONLY ever be used to show the dialog if dialog.show is false (autoshow).
  2060.                     // You CANNOT call show() after you call hide(). hide = close, they are the same thing and once
  2061.                     // a dialog is closed it's resources are disposed of.
  2062.                     scope.show = function () {
  2063.                         if (dialog.manualClose === true) {
  2064.                             //show and configure that the keyboard events are not enabled on this modal
  2065.                             dialog.element.modal({ keyboard: false });
  2066.                         }
  2067.                         else {
  2068.                             //just show normally
  2069.                             dialog.element.modal('show');
  2070.                         }
  2071.  
  2072.                     };
  2073.  
  2074.                     scope.select = function (item) {
  2075.                         var i = scope.dialogData.selection.indexOf(item);
  2076.                         if (i < 0) {
  2077.                             scope.dialogData.selection.push(item);
  2078.                         } else {
  2079.                             scope.dialogData.selection.splice(i, 1);
  2080.                         }
  2081.                     };
  2082.  
  2083.                     //NOTE: Same as 'close' without the callbacks
  2084.                     scope.dismiss = scope.hide;
  2085.  
  2086.                     // Emit modal events
  2087.                     angular.forEach(['show', 'shown', 'hide', 'hidden'], function (name) {
  2088.                         dialog.element.on(name, function (ev) {
  2089.                             scope.$emit('modal-' + name, ev);
  2090.                         });
  2091.                     });
  2092.  
  2093.                     // Support autofocus attribute
  2094.                     dialog.element.on('shown', function (event) {
  2095.                         $('input[autofocus]', dialog.element).first().trigger('focus');
  2096.                     });
  2097.  
  2098.                     dialog.scope = scope;
  2099.  
  2100.                     //Autoshow
  2101.                     if (dialog.show) {
  2102.                         scope.show();
  2103.                     }
  2104.  
  2105.                 });
  2106.  
  2107.             //Return the modal object outside of the promise!
  2108.             return dialog;
  2109.         }
  2110.     }
  2111.  
  2112.     /** Handles the closeDialogs event */
  2113.     eventsService.on("app.closeDialogs", function (evt, args) {
  2114.         removeAllDialogs(args);
  2115.     });
  2116.  
  2117.     return {
  2118.         /**
  2119.          * @ngdoc method
  2120.          * @name umbraco.services.dialogService#open
  2121.          * @methodOf umbraco.services.dialogService
  2122.          *
  2123.          * @description
  2124.          * Opens a modal rendering a given template url.
  2125.          *
  2126.          * @param {Object} options rendering options
  2127.          * @param {DomElement} options.container the DOM element to inject the modal into, by default set to body
  2128.          * @param {Function} options.callback function called when the modal is submitted
  2129.          * @param {String} options.template the url of the template
  2130.          * @param {String} options.animation animation csss class, by default set to "fade"
  2131.          * @param {String} options.modalClass modal css class, by default "umb-modal"
  2132.          * @param {Bool} options.show show the modal instantly
  2133.          * @param {Bool} options.iframe load template in an iframe, only needed for serverside templates
  2134.          * @param {Int} options.width set a width on the modal, only needed for iframes
  2135.          * @param {Bool} options.inline strips the modal from any animation and wrappers, used when you want to inject a dialog into an existing container
  2136.          * @returns {Object} modal object
  2137.          */
  2138.         open: function (options) {
  2139.             return openDialog(options);
  2140.         },
  2141.  
  2142.         /**
  2143.          * @ngdoc method
  2144.          * @name umbraco.services.dialogService#close
  2145.          * @methodOf umbraco.services.dialogService
  2146.          *
  2147.          * @description
  2148.          * Closes a specific dialog
  2149.          * @param {Object} dialog the dialog object to close
  2150.          * @param {Object} args if specified this object will be sent to any callbacks registered on the dialogs.
  2151.          */
  2152.         close: function (dialog, args) {
  2153.             if (dialog) {
  2154.                 dialog.close(args);
  2155.             }
  2156.         },
  2157.  
  2158.         /**
  2159.          * @ngdoc method
  2160.          * @name umbraco.services.dialogService#closeAll
  2161.          * @methodOf umbraco.services.dialogService
  2162.          *
  2163.          * @description
  2164.          * Closes all dialogs
  2165.          * @param {Object} args if specified this object will be sent to any callbacks registered on the dialogs.
  2166.          */
  2167.         closeAll: function (args) {
  2168.             removeAllDialogs(args);
  2169.         },
  2170.  
  2171.         /**
  2172.          * @ngdoc method
  2173.          * @name umbraco.services.dialogService#mediaPicker
  2174.          * @methodOf umbraco.services.dialogService
  2175.          *
  2176.          * @description
  2177.          * Opens a media picker in a modal, the callback returns an array of selected media items
  2178.          * @param {Object} options mediapicker dialog options object
  2179.          * @param {Boolean} options.onlyImages Only display files that have an image file-extension
  2180.          * @param {Function} options.callback callback function
  2181.          * @returns {Object} modal object
  2182.          */
  2183.         mediaPicker: function (options) {
  2184.             options.template = 'views/common/dialogs/mediaPicker.html';
  2185.             options.show = true;
  2186.             return openDialog(options);
  2187.         },
  2188.  
  2189.  
  2190.         /**
  2191.          * @ngdoc method
  2192.          * @name umbraco.services.dialogService#contentPicker
  2193.          * @methodOf umbraco.services.dialogService
  2194.          *
  2195.          * @description
  2196.          * Opens a content picker tree in a modal, the callback returns an array of selected documents
  2197.          * @param {Object} options content picker dialog options object
  2198.          * @param {Boolean} options.multiPicker should the picker return one or multiple items
  2199.          * @param {Function} options.callback callback function
  2200.          * @returns {Object} modal object
  2201.          */
  2202.         contentPicker: function (options) {
  2203.  
  2204.             options.treeAlias = "content";
  2205.             options.section = "content";
  2206.  
  2207.             return this.treePicker(options);
  2208.         },
  2209.  
  2210.         /**
  2211.          * @ngdoc method
  2212.          * @name umbraco.services.dialogService#linkPicker
  2213.          * @methodOf umbraco.services.dialogService
  2214.          *
  2215.          * @description
  2216.          * Opens a link picker tree in a modal, the callback returns a single link
  2217.          * @param {Object} options content picker dialog options object
  2218.          * @param {Function} options.callback callback function
  2219.          * @returns {Object} modal object
  2220.          */
  2221.         linkPicker: function (options) {
  2222.             options.template = 'views/common/dialogs/linkPicker.html';
  2223.             options.show = true;
  2224.             return openDialog(options);
  2225.         },
  2226.  
  2227.         /**
  2228.          * @ngdoc method
  2229.          * @name umbraco.services.dialogService#macroPicker
  2230.          * @methodOf umbraco.services.dialogService
  2231.          *
  2232.          * @description
  2233.          * Opens a mcaro picker in a modal, the callback returns a object representing the macro and it's parameters
  2234.          * @param {Object} options macropicker dialog options object
  2235.          * @param {Function} options.callback callback function
  2236.          * @returns {Object} modal object
  2237.          */
  2238.         macroPicker: function (options) {
  2239.             options.template = 'views/common/dialogs/insertmacro.html';
  2240.             options.show = true;
  2241.             options.modalClass = "span7 umb-modal";
  2242.             return openDialog(options);
  2243.         },
  2244.  
  2245.         /**
  2246.          * @ngdoc method
  2247.          * @name umbraco.services.dialogService#memberPicker
  2248.          * @methodOf umbraco.services.dialogService
  2249.          *
  2250.          * @description
  2251.          * Opens a member picker in a modal, the callback returns a object representing the selected member
  2252.          * @param {Object} options member picker dialog options object
  2253.          * @param {Boolean} options.multiPicker should the tree pick one or multiple members before returning
  2254.          * @param {Function} options.callback callback function
  2255.          * @returns {Object} modal object
  2256.          */
  2257.         memberPicker: function (options) {
  2258.  
  2259.             options.treeAlias = "member";
  2260.             options.section = "member";
  2261.  
  2262.             return this.treePicker(options);
  2263.         },
  2264.  
  2265.         /**
  2266.          * @ngdoc method
  2267.          * @name umbraco.services.dialogService#memberGroupPicker
  2268.          * @methodOf umbraco.services.dialogService
  2269.          *
  2270.          * @description
  2271.          * Opens a member group picker in a modal, the callback returns a object representing the selected member
  2272.          * @param {Object} options member group picker dialog options object
  2273.          * @param {Boolean} options.multiPicker should the tree pick one or multiple members before returning
  2274.          * @param {Function} options.callback callback function
  2275.          * @returns {Object} modal object
  2276.          */
  2277.         memberGroupPicker: function (options) {
  2278.             options.template = 'views/common/dialogs/memberGroupPicker.html';
  2279.             options.show = true;
  2280.             return openDialog(options);
  2281.         },
  2282.  
  2283.         /**
  2284.          * @ngdoc method
  2285.          * @name umbraco.services.dialogService#iconPicker
  2286.          * @methodOf umbraco.services.dialogService
  2287.          *
  2288.          * @description
  2289.          * Opens a icon picker in a modal, the callback returns a object representing the selected icon
  2290.          * @param {Object} options iconpicker dialog options object
  2291.          * @param {Function} options.callback callback function
  2292.          * @returns {Object} modal object
  2293.          */
  2294.         iconPicker: function (options) {
  2295.             options.template = 'views/common/dialogs/iconPicker.html';
  2296.             options.show = true;
  2297.             return openDialog(options);
  2298.         },
  2299.  
  2300.         /**
  2301.          * @ngdoc method
  2302.          * @name umbraco.services.dialogService#treePicker
  2303.          * @methodOf umbraco.services.dialogService
  2304.          *
  2305.          * @description
  2306.          * Opens a tree picker in a modal, the callback returns a object representing the selected tree item
  2307.          * @param {Object} options iconpicker dialog options object
  2308.          * @param {String} options.section tree section to display
  2309.          * @param {String} options.treeAlias specific tree to display
  2310.          * @param {Boolean} options.multiPicker should the tree pick one or multiple items before returning
  2311.          * @param {Function} options.callback callback function
  2312.          * @returns {Object} modal object
  2313.          */
  2314.         treePicker: function (options) {
  2315.             options.template = 'views/common/dialogs/treePicker.html';
  2316.             options.show = true;
  2317.             return openDialog(options);
  2318.         },
  2319.  
  2320.         /**
  2321.          * @ngdoc method
  2322.          * @name umbraco.services.dialogService#propertyDialog
  2323.          * @methodOf umbraco.services.dialogService
  2324.          *
  2325.          * @description
  2326.          * 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
  2327.          * @param {Object} options mediapicker dialog options object
  2328.          * @param {Function} options.callback callback function
  2329.          * @param {String} editor editor to use to edit a given value and return on callback
  2330.          * @param {Object} value value sent to the property editor
  2331.          * @returns {Object} modal object
  2332.          */
  2333.         //TODO: Wtf does this do? I don't think anything!
  2334.         propertyDialog: function (options) {
  2335.             options.template = 'views/common/dialogs/property.html';
  2336.             options.show = true;
  2337.             return openDialog(options);
  2338.         },
  2339.  
  2340.         /**
  2341.         * @ngdoc method
  2342.         * @name umbraco.services.dialogService#embedDialog
  2343.         * @methodOf umbraco.services.dialogService
  2344.         * @description
  2345.         * Opens a dialog to an embed dialog
  2346.         */
  2347.         embedDialog: function (options) {
  2348.             options.template = 'views/common/dialogs/rteembed.html';
  2349.             options.show = true;
  2350.             return openDialog(options);
  2351.         },
  2352.         /**
  2353.         * @ngdoc method
  2354.         * @name umbraco.services.dialogService#ysodDialog
  2355.         * @methodOf umbraco.services.dialogService
  2356.         *
  2357.         * @description
  2358.         * Opens a dialog to show a custom YSOD
  2359.         */
  2360.         ysodDialog: function (ysodError) {
  2361.  
  2362.             var newScope = $rootScope.$new();
  2363.             newScope.error = ysodError;
  2364.             return openDialog({
  2365.                 modalClass: "umb-modal wide ysod",
  2366.                 scope: newScope,
  2367.                 //callback: options.callback,
  2368.                 template: 'views/common/dialogs/ysod.html',
  2369.                 show: true
  2370.             });
  2371.         },
  2372.  
  2373.         confirmDialog: function (ysodError) {
  2374.  
  2375.             options.template = 'views/common/dialogs/confirm.html';
  2376.             options.show = true;
  2377.             return openDialog(options);
  2378.         }
  2379.     };
  2380. });
  2381.  
  2382. /** Used to broadcast and listen for global events and allow the ability to add async listeners to the callbacks */
  2383.  
  2384. /*
  2385.     Core app events:
  2386.  
  2387.     app.ready
  2388.     app.authenticated
  2389.     app.notAuthenticated
  2390.     app.closeDialogs
  2391. */
  2392.  
  2393. function eventsService($q, $rootScope) {
  2394.    
  2395.     return {
  2396.        
  2397.         /** raise an event with a given name, returns an array of promises for each listener */
  2398.         emit: function (name, args) {            
  2399.  
  2400.             //there are no listeners
  2401.             if (!$rootScope.$$listeners[name]) {
  2402.                 return;
  2403.                 //return [];
  2404.             }
  2405.  
  2406.             //send the event
  2407.             $rootScope.$emit(name, args);
  2408.  
  2409.  
  2410.             //PP: I've commented out the below, since we currently dont
  2411.             // expose the eventsService as a documented api
  2412.             // and think we need to figure out our usecases for this
  2413.             // since the below modifies the return value of the then on() method
  2414.             /*
  2415.             //setup a deferred promise for each listener
  2416.             var deferred = [];
  2417.             for (var i = 0; i < $rootScope.$$listeners[name].length; i++) {
  2418.                 deferred.push($q.defer());
  2419.             }*/
  2420.  
  2421.             //create a new event args object to pass to the
  2422.             // $emit containing methods that will allow listeners
  2423.             // to return data in an async if required
  2424.             /*
  2425.             var eventArgs = {
  2426.                 args: args,
  2427.                 reject: function (a) {
  2428.                     deferred.pop().reject(a);
  2429.                 },
  2430.                 resolve: function (a) {
  2431.                     deferred.pop().resolve(a);
  2432.                 }
  2433.             };*/
  2434.            
  2435.            
  2436.            
  2437.             /*
  2438.             //return an array of promises
  2439.             var promises = _.map(deferred, function(p) {
  2440.                 return p.promise;
  2441.             });
  2442.             return promises;*/
  2443.         },
  2444.  
  2445.         /** subscribe to a method, or use scope.$on = same thing */
  2446.         on: function(name, callback) {
  2447.             return $rootScope.$on(name, callback);
  2448.         },
  2449.        
  2450.         /** pass in the result of 'on' to this method, or just call the method returned from 'on' to unsubscribe */
  2451.         unsubscribe: function(handle) {
  2452.             if (angular.isFunction(handle)) {
  2453.                 handle();
  2454.             }          
  2455.         }
  2456.     };
  2457. }
  2458.  
  2459. angular.module('umbraco.services').factory('eventsService', eventsService);
  2460. /**
  2461.  * @ngdoc service
  2462.  * @name umbraco.services.fileManager
  2463.  * @function
  2464.  *
  2465.  * @description
  2466.  * Used by editors to manage any files that require uploading with the posted data, normally called by property editors
  2467.  * that need to attach files.
  2468.  * When a route changes successfully, we ensure that the collection is cleared.
  2469.  */
  2470. function fileManager() {
  2471.  
  2472.     var fileCollection = [];
  2473.  
  2474.     return {
  2475.         /**
  2476.          * @ngdoc function
  2477.          * @name umbraco.services.fileManager#addFiles
  2478.          * @methodOf umbraco.services.fileManager
  2479.          * @function
  2480.          *
  2481.          * @description
  2482.          *  Attaches files to the current manager for the current editor for a particular property, if an empty array is set
  2483.          *   for the files collection that effectively clears the files for the specified editor.
  2484.          */
  2485.         setFiles: function(propertyAlias, files) {
  2486.             //this will clear the files for the current property and then add the new ones for the current property
  2487.             fileCollection = _.reject(fileCollection, function (item) {
  2488.                 return item.alias === propertyAlias;
  2489.             });
  2490.             for (var i = 0; i < files.length; i++) {
  2491.                 //save the file object to the files collection
  2492.                 fileCollection.push({ alias: propertyAlias, file: files[i] });
  2493.             }
  2494.         },
  2495.        
  2496.         /**
  2497.          * @ngdoc function
  2498.          * @name umbraco.services.fileManager#getFiles
  2499.          * @methodOf umbraco.services.fileManager
  2500.          * @function
  2501.          *
  2502.          * @description
  2503.          *  Returns all of the files attached to the file manager
  2504.          */
  2505.         getFiles: function() {
  2506.             return fileCollection;
  2507.         },
  2508.        
  2509.         /**
  2510.          * @ngdoc function
  2511.          * @name umbraco.services.fileManager#clearFiles
  2512.          * @methodOf umbraco.services.fileManager
  2513.          * @function
  2514.          *
  2515.          * @description
  2516.          *  Removes all files from the manager
  2517.          */
  2518.         clearFiles: function () {
  2519.             fileCollection = [];
  2520.         }
  2521. };
  2522. }
  2523.  
  2524. angular.module('umbraco.services').factory('fileManager', fileManager);
  2525. /**
  2526.  * @ngdoc service
  2527.  * @name umbraco.services.formHelper
  2528.  * @function
  2529.  *
  2530.  * @description
  2531.  * 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
  2532.  * fire when they need to.
  2533.  */
  2534. function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService, localizationService) {
  2535.     return {
  2536.  
  2537.         /**
  2538.          * @ngdoc function
  2539.          * @name umbraco.services.formHelper#submitForm
  2540.          * @methodOf umbraco.services.formHelper
  2541.          * @function
  2542.          *
  2543.          * @description
  2544.          * Called by controllers when submitting a form - this ensures that all client validation is checked,
  2545.          * server validation is cleared, that the correct events execute and status messages are displayed.
  2546.          * This returns true if the form is valid, otherwise false if form submission cannot continue.
  2547.          *
  2548.          * @param {object} args An object containing arguments for form submission
  2549.          */
  2550.         submitForm: function (args) {
  2551.  
  2552.             var currentForm;
  2553.  
  2554.             if (!args) {
  2555.                 throw "args cannot be null";
  2556.             }
  2557.             if (!args.scope) {
  2558.                 throw "args.scope cannot be null";
  2559.             }
  2560.             if (!args.formCtrl) {
  2561.                 //try to get the closest form controller
  2562.                 currentForm = angularHelper.getRequiredCurrentForm(args.scope);
  2563.             }
  2564.             else {
  2565.                 currentForm = args.formCtrl;
  2566.             }
  2567.             //if no statusPropertyName is set we'll default to formStatus.
  2568.             if (!args.statusPropertyName) {
  2569.                 args.statusPropertyName = "formStatus";
  2570.             }
  2571.             //if no statusTimeout is set, we'll  default to 2500 ms
  2572.             if (!args.statusTimeout) {
  2573.                 args.statusTimeout = 2500;
  2574.             }
  2575.            
  2576.             //the first thing any form must do is broadcast the formSubmitting event
  2577.             args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action });
  2578.  
  2579.             //then check if the form is valid
  2580.             if (!args.skipValidation) {                
  2581.                 if (currentForm.$invalid) {
  2582.                     return false;
  2583.                 }
  2584.             }
  2585.  
  2586.             //reset the server validations
  2587.             serverValidationManager.reset();
  2588.            
  2589.             //check if a form status should be set on the scope
  2590.             if (args.statusMessage) {
  2591.                 args.scope[args.statusPropertyName] = args.statusMessage;
  2592.  
  2593.                 //clear the message after the timeout
  2594.                 $timeout(function () {
  2595.                     args.scope[args.statusPropertyName] = undefined;
  2596.                 }, args.statusTimeout);
  2597.             }
  2598.  
  2599.             return true;
  2600.         },
  2601.        
  2602.         /**
  2603.          * @ngdoc function
  2604.          * @name umbraco.services.formHelper#submitForm
  2605.          * @methodOf umbraco.services.formHelper
  2606.          * @function
  2607.          *
  2608.          * @description
  2609.          * Called by controllers when a form has been successfully submitted. the correct events execute
  2610.          * and that the notifications are displayed if there are any.
  2611.          *
  2612.          * @param {object} args An object containing arguments for form submission
  2613.          */
  2614.         resetForm: function (args) {
  2615.             if (!args) {
  2616.                 throw "args cannot be null";
  2617.             }
  2618.             if (!args.scope) {
  2619.                 throw "args.scope cannot be null";
  2620.             }
  2621.            
  2622.             //if no statusPropertyName is set we'll default to formStatus.
  2623.             if (!args.statusPropertyName) {
  2624.                 args.statusPropertyName = "formStatus";
  2625.             }
  2626.             //clear the status
  2627.             args.scope[args.statusPropertyName] = null;
  2628.  
  2629.             if (angular.isArray(args.notifications)) {
  2630.                 for (var i = 0; i < args.notifications.length; i++) {
  2631.                     notificationsService.showNotification(args.notifications[i]);
  2632.                 }
  2633.             }
  2634.  
  2635.             args.scope.$broadcast("formSubmitted", { scope: args.scope });
  2636.         },
  2637.        
  2638.         /**
  2639.          * @ngdoc function
  2640.          * @name umbraco.services.formHelper#handleError
  2641.          * @methodOf umbraco.services.formHelper
  2642.          * @function
  2643.          *
  2644.          * @description
  2645.          * Needs to be called when a form submission fails, this will wire up all server validation errors in ModelState and
  2646.          * add the correct messages to the notifications. If a server error has occurred this will show a ysod.
  2647.          *
  2648.          * @param {object} err The error object returned from the http promise
  2649.          */
  2650.         handleError: function (err) {            
  2651.             //When the status is a 400 status with a custom header: X-Status-Reason: Validation failed, we have validation errors.
  2652.             //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something).
  2653.             //Or, some strange server error
  2654.             if (err.status === 400) {
  2655.                 //now we need to look through all the validation errors
  2656.                 if (err.data && (err.data.ModelState)) {
  2657.  
  2658.                     //wire up the server validation errs
  2659.                     this.handleServerValidation(err.data.ModelState);
  2660.  
  2661.                     //execute all server validation events and subscribers
  2662.                     serverValidationManager.executeAndClearAllSubscriptions();                    
  2663.                 }
  2664.                 else {
  2665.                     dialogService.ysodDialog(err);
  2666.                 }
  2667.             }
  2668.             else {
  2669.                 dialogService.ysodDialog(err);
  2670.             }
  2671.         },
  2672.  
  2673.         /**
  2674.          * @ngdoc function
  2675.          * @name umbraco.services.formHelper#handleServerValidation
  2676.          * @methodOf umbraco.services.formHelper
  2677.          * @function
  2678.          *
  2679.          * @description
  2680.          * This wires up all of the server validation model state so that valServer and valServerField directives work
  2681.          *
  2682.          * @param {object} err The error object returned from the http promise
  2683.          */
  2684.         handleServerValidation: function (modelState) {
  2685.             for (var e in modelState) {
  2686.  
  2687.                 //This is where things get interesting....
  2688.                 // We need to support validation for all editor types such as both the content and content type editors.
  2689.                 // The Content editor ModelState is quite specific with the way that Properties are validated especially considering
  2690.                 // that each property is a User Developer property editor.
  2691.                 // The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations
  2692.                 // system.
  2693.                 // So, to do this (since we need to support backwards compat), we need to hack a little bit. For Content Properties,
  2694.                 // which are user defined, we know that they will exist with a prefixed ModelState of "_Properties.", so if we detect
  2695.                 // this, then we know it's a Property.
  2696.  
  2697.                 //the alias in model state can be in dot notation which indicates
  2698.                 // * the first part is the content property alias
  2699.                 // * the second part is the field to which the valiation msg is associated with
  2700.                 //There will always be at least 2 parts for properties since all model errors for properties are prefixed with "Properties"
  2701.                 //If it is not prefixed with "Properties" that means the error is for a field of the object directly.
  2702.  
  2703.                 var parts = e.split(".");
  2704.  
  2705.                 //Check if this is for content properties - specific to content/media/member editors because those are special
  2706.                 // user defined properties with custom controls.
  2707.                 if (parts.length > 1 && parts[0] === "_Properties") {
  2708.  
  2709.                     var propertyAlias = parts[1];
  2710.  
  2711.                     //if it contains 2 '.' then we will wire it up to a property's field
  2712.                     if (parts.length > 2) {
  2713.                         //add an error with a reference to the field for which the validation belongs too
  2714.                         serverValidationManager.addPropertyError(propertyAlias, parts[2], modelState[e][0]);
  2715.                     }
  2716.                     else {
  2717.                         //add a generic error for the property, no reference to a specific field
  2718.                         serverValidationManager.addPropertyError(propertyAlias, "", modelState[e][0]);
  2719.                     }
  2720.  
  2721.                 }
  2722.                 else {
  2723.  
  2724.                     //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example:
  2725.                     // Groups[0].Properties[2].Alias
  2726.                     serverValidationManager.addFieldError(e, modelState[e][0]);
  2727.                 }
  2728.  
  2729.                 //add to notifications
  2730.                 notificationsService.error("Validation", modelState[e][0]);
  2731.  
  2732.             }
  2733.         }
  2734.     };
  2735. }
  2736. angular.module('umbraco.services').factory('formHelper', formHelper);
  2737. angular.module('umbraco.services')
  2738.     .factory('gridService', function ($http, $q){
  2739.  
  2740.         var configPath = Umbraco.Sys.ServerVariables.umbracoUrls.gridConfig;
  2741.         var service = {
  2742.             getGridEditors: function () {
  2743.                 return $http.get(configPath);
  2744.             }
  2745.         };
  2746.  
  2747.         return service;
  2748.  
  2749.     });
  2750.  
  2751. angular.module('umbraco.services')
  2752.     .factory('helpService', function ($http, $q){
  2753.         var helpTopics = {};
  2754.  
  2755.         var defaultUrl = "http://our.umbraco.org/rss/help";
  2756.         var tvUrl = "http://umbraco.tv/feeds/help";
  2757.  
  2758.         function getCachedHelp(url){
  2759.             if(helpTopics[url]){
  2760.                 return helpTopics[cacheKey];
  2761.             }else{
  2762.                 return null;
  2763.             }
  2764.         }
  2765.  
  2766.         function setCachedHelp(url, data){
  2767.             helpTopics[url] = data;
  2768.         }
  2769.  
  2770.         function fetchUrl(url){
  2771.             var deferred = $q.defer();
  2772.             var found = getCachedHelp(url);
  2773.  
  2774.             if(found){
  2775.                 deferred.resolve(found);
  2776.             }else{
  2777.  
  2778.                 var proxyUrl = "dashboard/feedproxy.aspx?url=" + url;
  2779.                 $http.get(proxyUrl).then(function(data){
  2780.                     var feed = $(data.data);
  2781.                     var topics = [];
  2782.  
  2783.                     $('item', feed).each(function (i, item) {
  2784.                         var topic = {};
  2785.                         topic.thumbnail = $(item).find('thumbnail').attr('url');
  2786.                         topic.title = $("title", item).text();
  2787.                         topic.link = $("guid", item).text();
  2788.                         topic.description = $("description", item).text();
  2789.                         topics.push(topic);
  2790.                     });
  2791.  
  2792.                     setCachedHelp(topics);
  2793.                     deferred.resolve(topics);
  2794.                 });
  2795.             }
  2796.  
  2797.             return deferred.promise;
  2798.         }
  2799.  
  2800.  
  2801.  
  2802.         var service = {
  2803.             findHelp: function (args) {
  2804.                 var url = service.getUrl(defaultUrl, args);
  2805.                 return fetchUrl(url);
  2806.             },
  2807.  
  2808.             findVideos: function (args) {
  2809.                 var url = service.getUrl(tvUrl, args);
  2810.                 return fetchUrl(url);
  2811.             },
  2812.  
  2813.             getUrl: function(url, args){
  2814.                 return url + "?" + $.param(args);
  2815.             }
  2816.         };
  2817.  
  2818.         return service;
  2819.  
  2820.     });
  2821. /**
  2822.  * @ngdoc service
  2823.  * @name umbraco.services.historyService
  2824.  *
  2825.  * @requires $rootScope
  2826.  * @requires $timeout
  2827.  * @requires angularHelper
  2828.  * 
  2829.  * @description
  2830.  * Service to handle the main application navigation history. Responsible for keeping track
  2831.  * of where a user navigates to, stores an icon, url and name in a collection, to make it easy
  2832.  * for the user to go back to a previous editor / action
  2833.  *
  2834.  * **Note:** only works with new angular-based editors, not legacy ones
  2835.  *
  2836.  * ##usage
  2837.  * To use, simply inject the historyService into any controller that needs it, and make
  2838.  * sure the umbraco.services module is accesible - which it should be by default.
  2839.  *
  2840.  * <pre>
  2841.  *      angular.module("umbraco").controller("my.controller". function(historyService){
  2842.  *         historyService.add({
  2843.  *                              icon: "icon-class",
  2844.  *                              name: "Editing 'articles',
  2845.  *                              link: "/content/edit/1234"}
  2846.  *                          );
  2847.  *      });
  2848.  * </pre>
  2849.  */
  2850. angular.module('umbraco.services')
  2851. .factory('historyService', function ($rootScope, $timeout, angularHelper, eventsService) {
  2852.  
  2853.     var nArray = [];
  2854.  
  2855.     function add(item) {
  2856.  
  2857.         if (!item) {
  2858.             return null;
  2859.         }
  2860.  
  2861.         var listWithoutThisItem = _.reject(nArray, function(i) {
  2862.             return i.link === item.link;
  2863.         });
  2864.  
  2865.         //put it at the top and reassign
  2866.         listWithoutThisItem.splice(0, 0, item);
  2867.         nArray = listWithoutThisItem;
  2868.         return nArray[0];
  2869.  
  2870.     }
  2871.  
  2872.     return {
  2873.         /**
  2874.          * @ngdoc method
  2875.          * @name umbraco.services.historyService#add
  2876.          * @methodOf umbraco.services.historyService
  2877.          *
  2878.          * @description
  2879.          * Adds a given history item to the users history collection.
  2880.          *
  2881.          * @param {Object} item the history item
  2882.          * @param {String} item.icon icon css class for the list, ex: "icon-image", "icon-doc"
  2883.          * @param {String} item.link route to the editor, ex: "/content/edit/1234"
  2884.          * @param {String} item.name friendly name for the history listing
  2885.          * @returns {Object} history item object
  2886.          */
  2887.         add: function (item) {
  2888.             var icon = item.icon || "icon-file";
  2889.             angularHelper.safeApply($rootScope, function () {
  2890.                 var result = add({ name: item.name, icon: icon, link: item.link, time: new Date() });
  2891.                 eventsService.emit("historyService.add", {added: result, all: nArray});
  2892.                 return result;
  2893.             });
  2894.         },
  2895.         /**
  2896.          * @ngdoc method
  2897.          * @name umbraco.services.historyService#remove
  2898.          * @methodOf umbraco.services.historyService
  2899.          *
  2900.          * @description
  2901.          * Removes a history item from the users history collection, given an index to remove from.
  2902.          *
  2903.          * @param {Int} index index to remove item from
  2904.          */
  2905.         remove: function (index) {
  2906.             angularHelper.safeApply($rootScope, function() {
  2907.                 var result = nArray.splice(index, 1);
  2908.                 eventsService.emit("historyService.remove", { removed: result, all: nArray });
  2909.             });
  2910.         },
  2911.  
  2912.         /**
  2913.          * @ngdoc method
  2914.          * @name umbraco.services.historyService#removeAll
  2915.          * @methodOf umbraco.services.historyService
  2916.          *
  2917.          * @description
  2918.          * Removes all history items from the users history collection
  2919.          */
  2920.         removeAll: function () {
  2921.             angularHelper.safeApply($rootScope, function() {
  2922.                 nArray = [];
  2923.                 eventsService.emit("historyService.removeAll");
  2924.             });
  2925.         },
  2926.  
  2927.         /**
  2928.          * @ngdoc method
  2929.          * @name umbraco.services.historyService#getCurrent
  2930.          * @methodOf umbraco.services.historyService
  2931.          *
  2932.          * @description
  2933.          * Method to return the current history collection.
  2934.          *
  2935.          */
  2936.         getCurrent: function(){
  2937.             return nArray;
  2938.         }
  2939.     };
  2940. });
  2941. /**
  2942. * @ngdoc service
  2943. * @name umbraco.services.iconHelper
  2944. * @description A helper service for dealing with icons, mostly dealing with legacy tree icons
  2945. **/
  2946. function iconHelper($q, $timeout) {
  2947.  
  2948.     var converter = [
  2949.         { oldIcon: ".sprNew", newIcon: "add" },
  2950.         { oldIcon: ".sprDelete", newIcon: "remove" },
  2951.         { oldIcon: ".sprMove", newIcon: "enter" },
  2952.         { oldIcon: ".sprCopy", newIcon: "documents" },
  2953.         { oldIcon: ".sprSort", newIcon: "navigation-vertical" },
  2954.         { oldIcon: ".sprPublish", newIcon: "globe" },
  2955.         { oldIcon: ".sprRollback", newIcon: "undo" },
  2956.         { oldIcon: ".sprProtect", newIcon: "lock" },
  2957.         { oldIcon: ".sprAudit", newIcon: "time" },
  2958.         { oldIcon: ".sprNotify", newIcon: "envelope" },
  2959.         { oldIcon: ".sprDomain", newIcon: "home" },
  2960.         { oldIcon: ".sprPermission", newIcon: "lock" },
  2961.         { oldIcon: ".sprRefresh", newIcon: "refresh" },
  2962.         { oldIcon: ".sprBinEmpty", newIcon: "trash" },
  2963.         { oldIcon: ".sprExportDocumentType", newIcon: "download-alt" },
  2964.         { oldIcon: ".sprImportDocumentType", newIcon: "page-up" },
  2965.         { oldIcon: ".sprLiveEdit", newIcon: "edit" },
  2966.         { oldIcon: ".sprCreateFolder", newIcon: "add" },
  2967.         { oldIcon: ".sprPackage2", newIcon: "box" },
  2968.         { oldIcon: ".sprLogout", newIcon: "logout" },
  2969.         { oldIcon: ".sprSave", newIcon: "save" },
  2970.         { oldIcon: ".sprSendToTranslate", newIcon: "envelope-alt" },
  2971.         { oldIcon: ".sprToPublish", newIcon: "mail-forward" },
  2972.         { oldIcon: ".sprTranslate", newIcon: "comments" },
  2973.         { oldIcon: ".sprUpdate", newIcon: "save" },
  2974.        
  2975.         { oldIcon: ".sprTreeSettingDomain", newIcon: "icon-home" },
  2976.         { oldIcon: ".sprTreeDoc", newIcon: "icon-document" },
  2977.         { oldIcon: ".sprTreeDoc2", newIcon: "icon-diploma-alt" },
  2978.         { oldIcon: ".sprTreeDoc3", newIcon: "icon-notepad" },
  2979.         { oldIcon: ".sprTreeDoc4", newIcon: "icon-newspaper-alt" },
  2980.         { oldIcon: ".sprTreeDoc5", newIcon: "icon-notepad-alt" },
  2981.  
  2982.         { oldIcon: ".sprTreeDocPic", newIcon: "icon-picture" },        
  2983.         { oldIcon: ".sprTreeFolder", newIcon: "icon-folder" },
  2984.         { oldIcon: ".sprTreeFolder_o", newIcon: "icon-folder" },
  2985.         { oldIcon: ".sprTreeMediaFile", newIcon: "icon-music" },
  2986.         { oldIcon: ".sprTreeMediaMovie", newIcon: "icon-movie" },
  2987.         { oldIcon: ".sprTreeMediaPhoto", newIcon: "icon-picture" },
  2988.        
  2989.         { oldIcon: ".sprTreeMember", newIcon: "icon-user" },
  2990.         { oldIcon: ".sprTreeMemberGroup", newIcon: "icon-users" },
  2991.         { oldIcon: ".sprTreeMemberType", newIcon: "icon-users" },
  2992.        
  2993.         { oldIcon: ".sprTreeNewsletter", newIcon: "icon-file-text-alt" },
  2994.         { oldIcon: ".sprTreePackage", newIcon: "icon-box" },
  2995.         { oldIcon: ".sprTreeRepository", newIcon: "icon-server-alt" },
  2996.        
  2997.         { oldIcon: ".sprTreeSettingDataType", newIcon: "icon-autofill" },
  2998.  
  2999.         //TODO:
  3000.         /*
  3001.         { oldIcon: ".sprTreeSettingAgent", newIcon: "" },
  3002.         { oldIcon: ".sprTreeSettingCss", newIcon: "" },
  3003.         { oldIcon: ".sprTreeSettingCssItem", newIcon: "" },
  3004.        
  3005.         { oldIcon: ".sprTreeSettingDataTypeChild", newIcon: "" },
  3006.         { oldIcon: ".sprTreeSettingDomain", newIcon: "" },
  3007.         { oldIcon: ".sprTreeSettingLanguage", newIcon: "" },
  3008.         { oldIcon: ".sprTreeSettingScript", newIcon: "" },
  3009.         { oldIcon: ".sprTreeSettingTemplate", newIcon: "" },
  3010.         { oldIcon: ".sprTreeSettingXml", newIcon: "" },
  3011.         { oldIcon: ".sprTreeStatistik", newIcon: "" },
  3012.         { oldIcon: ".sprTreeUser", newIcon: "" },
  3013.         { oldIcon: ".sprTreeUserGroup", newIcon: "" },
  3014.         { oldIcon: ".sprTreeUserType", newIcon: "" },
  3015.         */
  3016.  
  3017.         { oldIcon: "folder.png", newIcon: "icon-folder" },
  3018.         { oldIcon: "mediaphoto.gif", newIcon: "icon-picture" },
  3019.         { oldIcon: "mediafile.gif", newIcon: "icon-document" },
  3020.  
  3021.         { oldIcon: ".sprTreeDeveloperCacheItem", newIcon: "icon-box" },
  3022.         { oldIcon: ".sprTreeDeveloperCacheTypes", newIcon: "icon-box" },
  3023.         { oldIcon: ".sprTreeDeveloperMacro", newIcon: "icon-cogs" },
  3024.         { oldIcon: ".sprTreeDeveloperRegistry", newIcon: "icon-windows" },
  3025.         { oldIcon: ".sprTreeDeveloperPython", newIcon: "icon-linux" }
  3026.     ];
  3027.  
  3028.     var imageConverter = [
  3029.             {oldImage: "contour.png", newIcon: "icon-umb-contour"}
  3030.             ];
  3031.  
  3032.     var collectedIcons;
  3033.            
  3034.     return {
  3035.        
  3036.         /** Used by the create dialogs for content/media types to format the data so that the thumbnails are styled properly */
  3037.         formatContentTypeThumbnails: function (contentTypes) {
  3038.             for (var i = 0; i < contentTypes.length; i++) {
  3039.  
  3040.                 if (contentTypes[i].thumbnailIsClass === undefined || contentTypes[i].thumbnailIsClass) {
  3041.                     contentTypes[i].cssClass = this.convertFromLegacyIcon(contentTypes[i].thumbnail);
  3042.                 }else {
  3043.                     contentTypes[i].style = "background-image: url('" + contentTypes[i].thumbnailFilePath + "');height:36px; background-position:4px 0px; background-repeat: no-repeat;background-size: 35px 35px;";
  3044.                     //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this
  3045.                     contentTypes[i].cssClass = "custom-file";
  3046.                 }
  3047.             }
  3048.             return contentTypes;
  3049.         },
  3050.         formatContentTypeIcons: function (contentTypes) {
  3051.             for (var i = 0; i < contentTypes.length; i++) {
  3052.                 if (!contentTypes[i].icon) {
  3053.                     //just to be safe (e.g. when focus was on close link and hitting save)
  3054.                     contentTypes[i].icon = "icon-document"; // default icon
  3055.                 } else {
  3056.                     contentTypes[i].icon = this.convertFromLegacyIcon(contentTypes[i].icon);
  3057.                 }
  3058.  
  3059.                 //couldnt find replacement
  3060.                 if(contentTypes[i].icon.indexOf(".") > 0){
  3061.                      contentTypes[i].icon = "icon-document-dashed-line";
  3062.                 }
  3063.             }
  3064.             return contentTypes;
  3065.         },
  3066.         /** If the icon is file based (i.e. it has a file path) */
  3067.         isFileBasedIcon: function (icon) {
  3068.             //if it doesn't start with a '.' but contains one then we'll assume it's file based
  3069.             if (icon.startsWith('..') || (!icon.startsWith('.') && icon.indexOf('.') > 1)) {
  3070.                 return true;
  3071.             }
  3072.             return false;
  3073.         },
  3074.         /** If the icon is legacy */
  3075.         isLegacyIcon: function (icon) {
  3076.             if(!icon) {
  3077.                 return false;
  3078.             }
  3079.  
  3080.             if(icon.startsWith('..')){
  3081.                 return false;
  3082.             }
  3083.  
  3084.             if (icon.startsWith('.')) {
  3085.                 return true;
  3086.             }
  3087.             return false;
  3088.         },
  3089.         /** If the tree node has a legacy icon */
  3090.         isLegacyTreeNodeIcon: function(treeNode){
  3091.             if (treeNode.iconIsClass) {
  3092.                 return this.isLegacyIcon(treeNode.icon);
  3093.             }
  3094.             return false;
  3095.         },
  3096.  
  3097.         /** Return a list of icons, optionally filter them */
  3098.         /** It fetches them directly from the active stylesheets in the browser */
  3099.         getIcons: function(){
  3100.             var deferred = $q.defer();
  3101.             $timeout(function(){
  3102.                 if(collectedIcons){
  3103.                     deferred.resolve(collectedIcons);
  3104.                 }else{
  3105.                     collectedIcons = [];
  3106.                     var c = ".icon-";
  3107.  
  3108.                     for (var i = document.styleSheets.length - 1; i >= 0; i--) {
  3109.                         var classes = document.styleSheets[i].rules || document.styleSheets[i].cssRules;
  3110.                        
  3111.                         if (classes !== null) {
  3112.                             for(var x=0;x<classes.length;x++) {
  3113.                                 var cur = classes[x];
  3114.                                 if(cur.selectorText && cur.selectorText.indexOf(c) === 0) {
  3115.                                     var s = cur.selectorText.substring(1);
  3116.                                     var hasSpace = s.indexOf(" ");
  3117.                                     if(hasSpace>0){
  3118.                                         s = s.substring(0, hasSpace);
  3119.                                     }
  3120.                                     var hasPseudo = s.indexOf(":");
  3121.                                     if(hasPseudo>0){
  3122.                                         s = s.substring(0, hasPseudo);
  3123.                                     }
  3124.  
  3125.                                     if(collectedIcons.indexOf(s) < 0){
  3126.                                         collectedIcons.push(s);
  3127.                                     }
  3128.                                 }
  3129.                             }
  3130.                         }
  3131.                     }
  3132.                     deferred.resolve(collectedIcons);
  3133.                 }
  3134.             }, 100);
  3135.            
  3136.             return deferred.promise;
  3137.         },
  3138.  
  3139.         /** Converts the icon from legacy to a new one if an old one is detected */
  3140.         convertFromLegacyIcon: function (icon) {
  3141.             if (this.isLegacyIcon(icon)) {
  3142.                 //its legacy so convert it if we can
  3143.                 var found = _.find(converter, function (item) {
  3144.                     return item.oldIcon.toLowerCase() === icon.toLowerCase();
  3145.                 });
  3146.                 return (found ? found.newIcon : icon);
  3147.             }
  3148.             return icon;
  3149.         },
  3150.  
  3151.         convertFromLegacyImage: function (icon) {
  3152.                 var found = _.find(imageConverter, function (item) {
  3153.                     return item.oldImage.toLowerCase() === icon.toLowerCase();
  3154.                 });
  3155.                 return (found ? found.newIcon : undefined);
  3156.         },
  3157.  
  3158.         /** If we detect that the tree node has legacy icons that can be converted, this will convert them */
  3159.         convertFromLegacyTreeNodeIcon: function (treeNode) {
  3160.             if (this.isLegacyTreeNodeIcon(treeNode)) {
  3161.                 return this.convertFromLegacyIcon(treeNode.icon);
  3162.             }
  3163.             return treeNode.icon;
  3164.         }
  3165.     };
  3166. }
  3167. angular.module('umbraco.services').factory('iconHelper', iconHelper);
  3168. /**
  3169. * @ngdoc service
  3170. * @name umbraco.services.imageHelper
  3171. * @deprecated
  3172. **/
  3173. function imageHelper(umbRequestHelper, mediaHelper) {
  3174.     return {
  3175.         /**
  3176.          * @ngdoc function
  3177.          * @name umbraco.services.imageHelper#getImagePropertyValue
  3178.          * @methodOf umbraco.services.imageHelper
  3179.          * @function    
  3180.          *
  3181.          * @deprecated
  3182.          */
  3183.         getImagePropertyValue: function (options) {
  3184.             return mediaHelper.getImagePropertyValue(options);
  3185.         },
  3186.         /**
  3187.          * @ngdoc function
  3188.          * @name umbraco.services.imageHelper#getThumbnail
  3189.          * @methodOf umbraco.services.imageHelper
  3190.          * @function    
  3191.          *
  3192.          * @deprecated
  3193.          */
  3194.         getThumbnail: function (options) {
  3195.             return mediaHelper.getThumbnail(options);
  3196.         },
  3197.  
  3198.         /**
  3199.          * @ngdoc function
  3200.          * @name umbraco.services.imageHelper#scaleToMaxSize
  3201.          * @methodOf umbraco.services.imageHelper
  3202.          * @function    
  3203.          *
  3204.          * @deprecated
  3205.          */
  3206.         scaleToMaxSize: function (maxSize, width, height) {
  3207.             return mediaHelper.scaleToMaxSize(maxSize, width, height);
  3208.         },
  3209.  
  3210.         /**
  3211.          * @ngdoc function
  3212.          * @name umbraco.services.imageHelper#getThumbnailFromPath
  3213.          * @methodOf umbraco.services.imageHelper
  3214.          * @function    
  3215.          *
  3216.          * @deprecated
  3217.          */
  3218.         getThumbnailFromPath: function (imagePath) {
  3219.             return mediaHelper.getThumbnailFromPath(imagePath);
  3220.         },
  3221.  
  3222.         /**
  3223.          * @ngdoc function
  3224.          * @name umbraco.services.imageHelper#detectIfImageByExtension
  3225.          * @methodOf umbraco.services.imageHelper
  3226.          * @function    
  3227.          *
  3228.          * @deprecated
  3229.          */
  3230.         detectIfImageByExtension: function (imagePath) {
  3231.             return mediaHelper.detectIfImageByExtension(imagePath);
  3232.         }
  3233.     };
  3234. }
  3235. angular.module('umbraco.services').factory('imageHelper', imageHelper);
  3236. // This service was based on OpenJS library available in BSD License
  3237. // http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php
  3238.  
  3239. function keyboardService($window, $timeout) {
  3240.    
  3241.     var keyboardManagerService = {};
  3242.    
  3243.     var defaultOpt = {
  3244.         'type':             'keydown',
  3245.         'propagate':        false,
  3246.         'inputDisabled':    false,
  3247.         'target':           $window.document,
  3248.         'keyCode':          false
  3249.     };
  3250.  
  3251.     // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken
  3252.     var shift_nums = {
  3253.         "`": "~",
  3254.         "1": "!",
  3255.         "2": "@",
  3256.         "3": "#",
  3257.         "4": "$",
  3258.         "5": "%",
  3259.         "6": "^",
  3260.         "7": "&",
  3261.         "8": "*",
  3262.         "9": "(",
  3263.         "0": ")",
  3264.         "-": "_",
  3265.         "=": "+",
  3266.         ";": ":",
  3267.         "'": "\"",
  3268.         ",": "<",
  3269.         ".": ">",
  3270.         "/": "?",
  3271.         "\\": "|"
  3272.     };
  3273.  
  3274.     // Special Keys - and their codes
  3275.     var special_keys = {
  3276.         'esc': 27,
  3277.         'escape': 27,
  3278.         'tab': 9,
  3279.         'space': 32,
  3280.         'return': 13,
  3281.         'enter': 13,
  3282.         'backspace': 8,
  3283.  
  3284.         'scrolllock': 145,
  3285.         'scroll_lock': 145,
  3286.         'scroll': 145,
  3287.         'capslock': 20,
  3288.         'caps_lock': 20,
  3289.         'caps': 20,
  3290.         'numlock': 144,
  3291.         'num_lock': 144,
  3292.         'num': 144,
  3293.  
  3294.         'pause': 19,
  3295.         'break': 19,
  3296.  
  3297.         'insert': 45,
  3298.         'home': 36,
  3299.         'delete': 46,
  3300.         'end': 35,
  3301.  
  3302.         'pageup': 33,
  3303.         'page_up': 33,
  3304.         'pu': 33,
  3305.  
  3306.         'pagedown': 34,
  3307.         'page_down': 34,
  3308.         'pd': 34,
  3309.  
  3310.         'left': 37,
  3311.         'up': 38,
  3312.         'right': 39,
  3313.         'down': 40,
  3314.  
  3315.         'f1': 112,
  3316.         'f2': 113,
  3317.         'f3': 114,
  3318.         'f4': 115,
  3319.         'f5': 116,
  3320.         'f6': 117,
  3321.         'f7': 118,
  3322.         'f8': 119,
  3323.         'f9': 120,
  3324.         'f10': 121,
  3325.         'f11': 122,
  3326.         'f12': 123
  3327.     };
  3328.  
  3329.     var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0;
  3330.  
  3331.     // The event handler for bound element events
  3332.     function eventHandler(e) {
  3333.         e = e || $window.event;
  3334.  
  3335.         var code, k;
  3336.  
  3337.         // Find out which key is pressed
  3338.         if (e.keyCode)
  3339.         {
  3340.             code = e.keyCode;
  3341.         }
  3342.         else if (e.which) {
  3343.             code = e.which;
  3344.         }
  3345.  
  3346.         var character = String.fromCharCode(code).toLowerCase();
  3347.  
  3348.         if (code === 188){character = ",";} // If the user presses , when the type is onkeydown
  3349.         if (code === 190){character = ".";} // If the user presses , when the type is onkeydown
  3350.  
  3351.         var propagate = true;
  3352.  
  3353.         //Now we need to determine which shortcut this event is for, we'll do this by iterating over each
  3354.         //registered shortcut to find the match. We use Find here so that the loop exits as soon
  3355.         //as we've found the one we're looking for
  3356.         _.find(_.keys(keyboardManagerService.keyboardEvent), function(key) {
  3357.  
  3358.             var shortcutLabel = key;
  3359.             var shortcutVal = keyboardManagerService.keyboardEvent[key];
  3360.  
  3361.             // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked
  3362.             var kp = 0;
  3363.  
  3364.             // Some modifiers key
  3365.             var modifiers = {
  3366.                 shift: {
  3367.                     wanted: false,
  3368.                     pressed: e.shiftKey ? true : false
  3369.                 },
  3370.                 ctrl: {
  3371.                     wanted: false,
  3372.                     pressed: e.ctrlKey ? true : false
  3373.                 },
  3374.                 alt: {
  3375.                     wanted: false,
  3376.                     pressed: e.altKey ? true : false
  3377.                 },
  3378.                 meta: { //Meta is Mac specific
  3379.                     wanted: false,
  3380.                     pressed: e.metaKey ? true : false
  3381.                 }
  3382.             };
  3383.  
  3384.             var keys = shortcutLabel.split("+");
  3385.             var opt = shortcutVal.opt;
  3386.             var callback = shortcutVal.callback;
  3387.  
  3388.             // Foreach keys in label (split on +)
  3389.             var l = keys.length;
  3390.             for (var i = 0; i < l; i++) {
  3391.  
  3392.                 var k = keys[i];
  3393.                 switch (k) {
  3394.                     case 'ctrl':
  3395.                     case 'control':
  3396.                         kp++;
  3397.                         modifiers.ctrl.wanted = true;
  3398.                         break;
  3399.                     case 'shift':
  3400.                     case 'alt':
  3401.                     case 'meta':
  3402.                         kp++;
  3403.                         modifiers[k].wanted = true;
  3404.                         break;
  3405.                 }
  3406.  
  3407.                 if (k.length > 1) { // If it is a special key
  3408.                     if (special_keys[k] === code) {
  3409.                         kp++;
  3410.                     }
  3411.                 }
  3412.                 else if (opt['keyCode']) { // If a specific key is set into the config
  3413.                     if (opt['keyCode'] === code) {
  3414.                         kp++;
  3415.                     }
  3416.                 }
  3417.                 else { // The special keys did not match
  3418.                     if (character === k) {
  3419.                         kp++;
  3420.                     }
  3421.                     else {
  3422.                         if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase
  3423.                             character = shift_nums[character];
  3424.                             if (character === k) {
  3425.                                 kp++;
  3426.                             }
  3427.                         }
  3428.                     }
  3429.                 }
  3430.  
  3431.             } //for end
  3432.  
  3433.             if (kp === keys.length &&
  3434.                 modifiers.ctrl.pressed === modifiers.ctrl.wanted &&
  3435.                 modifiers.shift.pressed === modifiers.shift.wanted &&
  3436.                 modifiers.alt.pressed === modifiers.alt.wanted &&
  3437.                 modifiers.meta.pressed === modifiers.meta.wanted) {
  3438.  
  3439.                 //found the right callback!
  3440.  
  3441.                 // Disable event handler when focus input and textarea
  3442.                 if (opt['inputDisabled']) {
  3443.                     var elt;
  3444.                     if (e.target) {
  3445.                         elt = e.target;
  3446.                     } else if (e.srcElement) {
  3447.                         elt = e.srcElement;
  3448.                     }
  3449.  
  3450.                     if (elt.nodeType === 3) { elt = elt.parentNode; }
  3451.                     if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA') {
  3452.                         //This exits the Find loop
  3453.                         return true;
  3454.                     }
  3455.                 }
  3456.  
  3457.                 $timeout(function () {
  3458.                     callback(e);
  3459.                 }, 1);
  3460.  
  3461.                 if (!opt['propagate']) { // Stop the event
  3462.                     propagate = false;
  3463.                 }
  3464.  
  3465.                 //This exits the Find loop
  3466.                 return true;
  3467.             }
  3468.  
  3469.             //we haven't found one so continue looking
  3470.             return false;
  3471.  
  3472.         });
  3473.  
  3474.         // Stop the event if required
  3475.         if (!propagate) {
  3476.             // e.cancelBubble is supported by IE - this will kill the bubbling process.
  3477.             e.cancelBubble = true;
  3478.             e.returnValue = false;
  3479.  
  3480.             // e.stopPropagation works in Firefox.
  3481.             if (e.stopPropagation) {
  3482.                 e.stopPropagation();
  3483.                 e.preventDefault();
  3484.             }
  3485.             return false;
  3486.         }
  3487.     }
  3488.  
  3489.     // Store all keyboard combination shortcuts
  3490.     keyboardManagerService.keyboardEvent = {};
  3491.  
  3492.     // Add a new keyboard combination shortcut
  3493.     keyboardManagerService.bind = function (label, callback, opt) {
  3494.  
  3495.         //replace ctrl key with meta key
  3496.         if(isMac && label !== "ctrl+space"){
  3497.             label = label.replace("ctrl","meta");
  3498.         }
  3499.  
  3500.         var elt;
  3501.         // Initialize opt object
  3502.         opt   = angular.extend({}, defaultOpt, opt);
  3503.         label = label.toLowerCase();
  3504.         elt   = opt.target;
  3505.         if(typeof opt.target === 'string'){
  3506.             elt = document.getElementById(opt.target);
  3507.         }
  3508.        
  3509.         //Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding
  3510.         // and raising events for now reason. So here we'll check if the event is already registered for the element
  3511.         var boundValues = _.values(keyboardManagerService.keyboardEvent);
  3512.         var found = _.find(boundValues, function (i) {
  3513.             return i.target === elt && i.event === opt['type'];
  3514.         });
  3515.  
  3516.         // Store shortcut
  3517.         keyboardManagerService.keyboardEvent[label] = {
  3518.             'callback': callback,
  3519.             'target':   elt,
  3520.             'opt':      opt
  3521.         };
  3522.  
  3523.         if (!found) {
  3524.             //Attach the function with the event
  3525.             if (elt.addEventListener) {
  3526.                 elt.addEventListener(opt['type'], eventHandler, false);
  3527.             } else if (elt.attachEvent) {
  3528.                 elt.attachEvent('on' + opt['type'], eventHandler);
  3529.             } else {
  3530.                 elt['on' + opt['type']] = eventHandler;
  3531.             }
  3532.         }
  3533.        
  3534.     };
  3535.     // Remove the shortcut - just specify the shortcut and I will remove the binding
  3536.     keyboardManagerService.unbind = function (label) {
  3537.         label = label.toLowerCase();
  3538.         var binding = keyboardManagerService.keyboardEvent[label];
  3539.         delete(keyboardManagerService.keyboardEvent[label]);
  3540.  
  3541.         if(!binding){return;}
  3542.  
  3543.         var type    = binding['event'],
  3544.         elt         = binding['target'],
  3545.         callback    = binding['callback'];
  3546.  
  3547.         if(elt.detachEvent){
  3548.             elt.detachEvent('on' + type, callback);
  3549.         }else if(elt.removeEventListener){
  3550.             elt.removeEventListener(type, callback, false);
  3551.         }else{
  3552.             elt['on'+type] = false;
  3553.         }
  3554.     };
  3555.     //
  3556.  
  3557.     return keyboardManagerService;
  3558. }
  3559.  
  3560. angular.module('umbraco.services').factory('keyboardService', ['$window', '$timeout', keyboardService]);
  3561.  
  3562. /**
  3563.  @ngdoc service
  3564.  * @name umbraco.services.listViewHelper
  3565.  *
  3566.  *
  3567.  * @description
  3568.  * Service for performing operations against items in the list view UI. Used by the built-in internal listviews
  3569.  * as well as custom listview.
  3570.  *
  3571.  * A custom listview is always used inside a wrapper listview, so there are a number of inherited values on its
  3572.  * scope by default:
  3573.  *
  3574.  * **$scope.selection**: Array containing all items currently selected in the listview
  3575.  *
  3576.  * **$scope.items**: Array containing all items currently displayed in the listview
  3577.  *
  3578.  * **$scope.folders**: Array containing all folders in the current listview (only for media)
  3579.  *
  3580.  * **$scope.options**: configuration object containing information such as pagesize, permissions, order direction etc.
  3581.  *
  3582.  * **$scope.model.config.layouts**: array of available layouts to apply to the listview (grid, list or custom layout)
  3583.  *
  3584.  * ##Usage##
  3585.  * To use, inject listViewHelper into custom listview controller, listviewhelper expects you
  3586.  * to pass in the full collection of items in the listview in several of its methods
  3587.  * this collection is inherited from the parent controller and is available on $scope.selection
  3588.  *
  3589.  * <pre>
  3590.  *      angular.module("umbraco").controller("my.listVieweditor". function($scope, listViewHelper){
  3591.  *
  3592.  *          //current items in the listview
  3593.  *          var items = $scope.items;
  3594.  *
  3595.  *          //current selection
  3596.  *          var selection = $scope.selection;
  3597.  *
  3598.  *          //deselect an item , $scope.selection is inherited, item is picked from inherited $scope.items
  3599.  *          listViewHelper.deselectItem(item, $scope.selection);
  3600.  *
  3601.  *          //test if all items are selected, $scope.items + $scope.selection are inherited
  3602.  *          listViewhelper.isSelectedAll($scope.items, $scope.selection);
  3603.  *      });
  3604.  * </pre>
  3605.  */
  3606. (function () {
  3607.     'use strict';
  3608.  
  3609.     function listViewHelper(localStorageService) {
  3610.  
  3611.         var firstSelectedIndex = 0;
  3612.         var localStorageKey = "umblistViewLayout";
  3613.  
  3614.         /**
  3615.         * @ngdoc method
  3616.         * @name umbraco.services.listViewHelper#getLayout
  3617.         * @methodOf umbraco.services.listViewHelper
  3618.         *
  3619.         * @description
  3620.         * Method for internal use, based on the collection of layouts passed, the method selects either
  3621.         * any previous layout from local storage, or picks the first allowed layout
  3622.         *
  3623.         * @param {Number} nodeId The id of the current node displayed in the content editor
  3624.         * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts
  3625.         */
  3626.  
  3627.         function getLayout(nodeId, availableLayouts) {
  3628.  
  3629.             var storedLayouts = [];
  3630.  
  3631.             if (localStorageService.get(localStorageKey)) {
  3632.                 storedLayouts = localStorageService.get(localStorageKey);
  3633.             }
  3634.  
  3635.             if (storedLayouts && storedLayouts.length > 0) {
  3636.                 for (var i = 0; storedLayouts.length > i; i++) {
  3637.                     var layout = storedLayouts[i];
  3638.                     if (layout.nodeId === nodeId) {
  3639.                         return setLayout(nodeId, layout, availableLayouts);
  3640.                     }
  3641.                 }
  3642.  
  3643.             }
  3644.  
  3645.             return getFirstAllowedLayout(availableLayouts);
  3646.  
  3647.         }
  3648.  
  3649.         /**
  3650.         * @ngdoc method
  3651.         * @name umbraco.services.listViewHelper#setLayout
  3652.         * @methodOf umbraco.services.listViewHelper
  3653.         *
  3654.         * @description
  3655.         * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage
  3656.         *
  3657.         * @param {Number} nodeID Id of the current node displayed in the content editor
  3658.         * @param {Object} selectedLayout Layout selected as the layout to set as the current layout
  3659.         * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts
  3660.         */
  3661.  
  3662.         function setLayout(nodeId, selectedLayout, availableLayouts) {
  3663.  
  3664.             var activeLayout = {};
  3665.             var layoutFound = false;
  3666.  
  3667.             for (var i = 0; availableLayouts.length > i; i++) {
  3668.                 var layout = availableLayouts[i];
  3669.                 if (layout.path === selectedLayout.path) {
  3670.                     activeLayout = layout;
  3671.                     layout.active = true;
  3672.                     layoutFound = true;
  3673.                 } else {
  3674.                     layout.active = false;
  3675.                 }
  3676.             }
  3677.  
  3678.             if (!layoutFound) {
  3679.                 activeLayout = getFirstAllowedLayout(availableLayouts);
  3680.             }
  3681.  
  3682.             saveLayoutInLocalStorage(nodeId, activeLayout);
  3683.  
  3684.             return activeLayout;
  3685.  
  3686.         }
  3687.  
  3688.         /**
  3689.         * @ngdoc method
  3690.         * @name umbraco.services.listViewHelper#saveLayoutInLocalStorage
  3691.         * @methodOf umbraco.services.listViewHelper
  3692.         *
  3693.         * @description
  3694.         * Stores a given layout as the current default selection in local storage
  3695.         *
  3696.         * @param {Number} nodeId Id of the current node displayed in the content editor
  3697.         * @param {Object} selectedLayout Layout selected as the layout to set as the current layout
  3698.         */
  3699.  
  3700.         function saveLayoutInLocalStorage(nodeId, selectedLayout) {
  3701.             var layoutFound = false;
  3702.             var storedLayouts = [];
  3703.  
  3704.             if (localStorageService.get(localStorageKey)) {
  3705.                 storedLayouts = localStorageService.get(localStorageKey);
  3706.             }
  3707.  
  3708.             if (storedLayouts.length > 0) {
  3709.                 for (var i = 0; storedLayouts.length > i; i++) {
  3710.                     var layout = storedLayouts[i];
  3711.                     if (layout.nodeId === nodeId) {
  3712.                         layout.path = selectedLayout.path;
  3713.                         layoutFound = true;
  3714.                     }
  3715.                 }
  3716.             }
  3717.  
  3718.             if (!layoutFound) {
  3719.                 var storageObject = {
  3720.                     "nodeId": nodeId,
  3721.                     "path": selectedLayout.path
  3722.                 };
  3723.                 storedLayouts.push(storageObject);
  3724.             }
  3725.  
  3726.             localStorageService.set(localStorageKey, storedLayouts);
  3727.  
  3728.         }
  3729.  
  3730.         /**
  3731.         * @ngdoc method
  3732.         * @name umbraco.services.listViewHelper#getFirstAllowedLayout
  3733.         * @methodOf umbraco.services.listViewHelper
  3734.         *
  3735.         * @description
  3736.         * Returns currently selected layout, or alternatively the first layout in the available layouts collection
  3737.         *
  3738.         * @param {Array} layouts Array of all allowed layouts, available from $scope.model.config.layouts
  3739.         */
  3740.  
  3741.         function getFirstAllowedLayout(layouts) {
  3742.  
  3743.             var firstAllowedLayout = {};
  3744.  
  3745.             for (var i = 0; layouts.length > i; i++) {
  3746.                 var layout = layouts[i];
  3747.                 if (layout.selected === true) {
  3748.                     firstAllowedLayout = layout;
  3749.                     break;
  3750.                 }
  3751.             }
  3752.  
  3753.             return firstAllowedLayout;
  3754.         }
  3755.  
  3756.         /**
  3757.         * @ngdoc method
  3758.         * @name umbraco.services.listViewHelper#selectHandler
  3759.         * @methodOf umbraco.services.listViewHelper
  3760.         *
  3761.         * @description
  3762.         * Helper method for working with item selection via a checkbox, internally it uses selectItem and deselectItem.
  3763.         * Working with this method, requires its triggered via a checkbox which can then pass in its triggered $event
  3764.         * When the checkbox is clicked, this method will toggle selection of the associated item so it matches the state of the checkbox
  3765.         *
  3766.         * @param {Object} selectedItem Item being selected or deselected by the checkbox
  3767.         * @param {Number} selectedIndex Index of item being selected/deselected, usually passed as $index
  3768.         * @param {Array} items All items in the current listview, available as $scope.items
  3769.         * @param {Array} selection All selected items in the current listview, available as $scope.selection
  3770.         * @param {Event} $event Event triggered by the checkbox being checked to select / deselect an item
  3771.         */
  3772.  
  3773.         function selectHandler(selectedItem, selectedIndex, items, selection, $event) {
  3774.  
  3775.             var start = 0;
  3776.             var end = 0;
  3777.             var item = null;
  3778.  
  3779.             if ($event.shiftKey === true) {
  3780.  
  3781.                 if (selectedIndex > firstSelectedIndex) {
  3782.  
  3783.                     start = firstSelectedIndex;
  3784.                     end = selectedIndex;
  3785.  
  3786.                     for (; end >= start; start++) {
  3787.                         item = items[start];
  3788.                         selectItem(item, selection);
  3789.                     }
  3790.  
  3791.                 } else {
  3792.  
  3793.                     start = firstSelectedIndex;
  3794.                     end = selectedIndex;
  3795.  
  3796.                     for (; end <= start; start--) {
  3797.                         item = items[start];
  3798.                         selectItem(item, selection);
  3799.                     }
  3800.  
  3801.                 }
  3802.  
  3803.             } else {
  3804.  
  3805.                 if (selectedItem.selected) {
  3806.                     deselectItem(selectedItem, selection);
  3807.                 } else {
  3808.                     selectItem(selectedItem, selection);
  3809.                 }
  3810.  
  3811.                 firstSelectedIndex = selectedIndex;
  3812.  
  3813.             }
  3814.  
  3815.         }
  3816.  
  3817.         /**
  3818.         * @ngdoc method
  3819.         * @name umbraco.services.listViewHelper#selectItem
  3820.         * @methodOf umbraco.services.listViewHelper
  3821.         *
  3822.         * @description
  3823.         * Selects a given item to the listview selection array, requires you pass in the inherited $scope.selection collection
  3824.         *
  3825.         * @param {Object} item Item to select
  3826.         * @param {Array} selection Listview selection, available as $scope.selection
  3827.         */
  3828.  
  3829.         function selectItem(item, selection) {
  3830.             var isSelected = false;
  3831.             for (var i = 0; selection.length > i; i++) {
  3832.                 var selectedItem = selection[i];
  3833.                 if (item.id === selectedItem.id) {
  3834.                     isSelected = true;
  3835.                 }
  3836.             }
  3837.             if (!isSelected) {
  3838.                 selection.push({ id: item.id });
  3839.                 item.selected = true;
  3840.             }
  3841.         }
  3842.  
  3843.         /**
  3844.         * @ngdoc method
  3845.         * @name umbraco.services.listViewHelper#deselectItem
  3846.         * @methodOf umbraco.services.listViewHelper
  3847.         *
  3848.         * @description
  3849.         * Deselects a given item from the listviews selection array, requires you pass in the inherited $scope.selection collection
  3850.         *
  3851.         * @param {Object} item Item to deselect
  3852.         * @param {Array} selection Listview selection, available as $scope.selection
  3853.         */
  3854.  
  3855.         function deselectItem(item, selection) {
  3856.             for (var i = 0; selection.length > i; i++) {
  3857.                 var selectedItem = selection[i];
  3858.                 if (item.id === selectedItem.id) {
  3859.                     selection.splice(i, 1);
  3860.                     item.selected = false;
  3861.                 }
  3862.             }
  3863.         }
  3864.  
  3865.         /**
  3866.         * @ngdoc method
  3867.         * @name umbraco.services.listViewHelper#clearSelection
  3868.         * @methodOf umbraco.services.listViewHelper
  3869.         *
  3870.         * @description
  3871.         * Removes a given number of items and folders from the listviews selection array
  3872.         * Folders can only be passed in if the listview is used in the media section which has a concept of folders.
  3873.         *
  3874.         * @param {Array} items Items to remove, can be null
  3875.         * @param {Array} folders Folders to remove, can be null
  3876.         * @param {Array} selection Listview selection, available as $scope.selection
  3877.         */
  3878.  
  3879.         function clearSelection(items, folders, selection) {
  3880.  
  3881.             var i = 0;
  3882.  
  3883.             selection.length = 0;
  3884.  
  3885.             if (angular.isArray(items)) {
  3886.                 for (i = 0; items.length > i; i++) {
  3887.                     var item = items[i];
  3888.                     item.selected = false;
  3889.                 }
  3890.             }
  3891.  
  3892.          if(angular.isArray(folders)) {
  3893.                 for (i = 0; folders.length > i; i++) {
  3894.                     var folder = folders[i];
  3895.                     folder.selected = false;
  3896.                 }
  3897.             }
  3898.         }
  3899.  
  3900.         /**
  3901.         * @ngdoc method
  3902.         * @name umbraco.services.listViewHelper#selectAllItems
  3903.         * @methodOf umbraco.services.listViewHelper
  3904.         *
  3905.         * @description
  3906.         * Helper method for toggling the select state on all items in the active listview
  3907.         * Can only be used from a checkbox as a checkbox $event is required to pass in.
  3908.         *
  3909.         * @param {Array} items Items to toggle selection on, should be $scope.items
  3910.         * @param {Array} selection Listview selection, available as $scope.selection
  3911.         * @param {$event} $event Event passed from the checkbox being toggled
  3912.         */
  3913.  
  3914.         function selectAllItems(items, selection, $event) {
  3915.  
  3916.             var checkbox = $event.target;
  3917.             var clearSelection = false;
  3918.  
  3919.             if (!angular.isArray(items)) {
  3920.                 return;
  3921.             }
  3922.  
  3923.             selection.length = 0;
  3924.  
  3925.             for (var i = 0; i < items.length; i++) {
  3926.  
  3927.                 var item = items[i];
  3928.  
  3929.                 if (checkbox.checked) {
  3930.                     selection.push({ id: item.id });
  3931.                 } else {
  3932.                     clearSelection = true;
  3933.                 }
  3934.  
  3935.                 item.selected = checkbox.checked;
  3936.  
  3937.             }
  3938.  
  3939.             if (clearSelection) {
  3940.                 selection.length = 0;
  3941.             }
  3942.  
  3943.         }
  3944.  
  3945.         /**
  3946.         * @ngdoc method
  3947.         * @name umbraco.services.listViewHelper#isSelectedAll
  3948.         * @methodOf umbraco.services.listViewHelper
  3949.         *
  3950.         * @description
  3951.         * Method to determine if all items on the current page in the list has been selected
  3952.         * Given the current items in the view, and the current selection, it will return true/false
  3953.         *
  3954.         * @param {Array} items Items to test if all are selected, should be $scope.items
  3955.         * @param {Array} selection Listview selection, available as $scope.selection
  3956.         * @returns {Boolean} boolean indicate if all items in the listview have been selected
  3957.         */
  3958.  
  3959.         function isSelectedAll(items, selection) {
  3960.  
  3961.             var numberOfSelectedItem = 0;
  3962.  
  3963.             for (var itemIndex = 0; items.length > itemIndex; itemIndex++) {
  3964.                 var item = items[itemIndex];
  3965.  
  3966.                 for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) {
  3967.                     var selectedItem = selection[selectedIndex];
  3968.  
  3969.                     if (item.id === selectedItem.id) {
  3970.                         numberOfSelectedItem++;
  3971.                     }
  3972.                 }
  3973.  
  3974.             }
  3975.  
  3976.             if (numberOfSelectedItem === items.length) {
  3977.                 return true;
  3978.             }
  3979.  
  3980.         }
  3981.  
  3982.         /**
  3983.         * @ngdoc method
  3984.         * @name umbraco.services.listViewHelper#setSortingDirection
  3985.         * @methodOf umbraco.services.listViewHelper
  3986.         *
  3987.         * @description
  3988.         * *Internal* method for changing sort order icon
  3989.         * @param {String} col Column alias to order after
  3990.         * @param {String} direction Order direction `asc` or `desc`
  3991.         * @param {Object} options object passed from the parent listview available as $scope.options
  3992.         */
  3993.  
  3994.         function setSortingDirection(col, direction, options) {
  3995.             return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction;
  3996.         }
  3997.  
  3998.         /**
  3999.         * @ngdoc method
  4000.         * @name umbraco.services.listViewHelper#setSorting
  4001.         * @methodOf umbraco.services.listViewHelper
  4002.         *
  4003.         * @description
  4004.         * Method for setting the field on which the listview will order its items after.
  4005.         *
  4006.         * @param {String} field Field alias to order after
  4007.         * @param {Boolean} allow Determines if the user is allowed to set this field, normally true
  4008.         * @param {Object} options Options object passed from the parent listview available as $scope.options
  4009.         */
  4010.  
  4011.         function setSorting(field, allow, options) {
  4012.             if (allow) {
  4013.                 if (options.orderBy === field && options.orderDirection === 'asc') {
  4014.                     options.orderDirection = "desc";
  4015.                 } else {
  4016.                     options.orderDirection = "asc";
  4017.                 }
  4018.                 options.orderBy = field;
  4019.             }
  4020.         }
  4021.  
  4022.         //This takes in a dictionary of Ids with Permissions and determines
  4023.         // the intersect of all permissions to return an object representing the
  4024.         // listview button permissions
  4025.         function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) {
  4026.  
  4027.             if (currentIdsWithPermissions == null) {
  4028.                 currentIdsWithPermissions = {};
  4029.             }
  4030.  
  4031.             //merge the newly retrieved permissions to the main dictionary
  4032.             _.each(unmergedPermissions, function (value, key, list) {
  4033.                 currentIdsWithPermissions[key] = value;
  4034.             });
  4035.  
  4036.             //get the intersect permissions
  4037.             var arr = [];
  4038.             _.each(currentIdsWithPermissions, function (value, key, list) {
  4039.                 arr.push(value);
  4040.             });
  4041.  
  4042.             //we need to use 'apply' to call intersection with an array of arrays,
  4043.             //see: http://stackoverflow.com/a/16229480/694494
  4044.             var intersectPermissions = _.intersection.apply(_, arr);
  4045.  
  4046.             return {
  4047.                 canCopy: _.contains(intersectPermissions, 'O'), //Magic Char = O
  4048.                 canCreate: _.contains(intersectPermissions, 'C'), //Magic Char = C
  4049.                 canDelete: _.contains(intersectPermissions, 'D'), //Magic Char = D
  4050.                 canMove: _.contains(intersectPermissions, 'M'), //Magic Char = M
  4051.                 canPublish: _.contains(intersectPermissions, 'U'), //Magic Char = U
  4052.                 canUnpublish: _.contains(intersectPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish)
  4053.             };
  4054.         }
  4055.  
  4056.         var service = {
  4057.  
  4058.           getLayout: getLayout,
  4059.           getFirstAllowedLayout: getFirstAllowedLayout,
  4060.           setLayout: setLayout,
  4061.           saveLayoutInLocalStorage: saveLayoutInLocalStorage,
  4062.           selectHandler: selectHandler,
  4063.           selectItem: selectItem,
  4064.           deselectItem: deselectItem,
  4065.           clearSelection: clearSelection,
  4066.           selectAllItems: selectAllItems,
  4067.           isSelectedAll: isSelectedAll,
  4068.           setSortingDirection: setSortingDirection,
  4069.           setSorting: setSorting,
  4070.           getButtonPermissions: getButtonPermissions
  4071.  
  4072.         };
  4073.  
  4074.         return service;
  4075.  
  4076.     }
  4077.  
  4078.  
  4079.     angular.module('umbraco.services').factory('listViewHelper', listViewHelper);
  4080.  
  4081.  
  4082. })();
  4083.  
  4084. /**
  4085.  @ngdoc service
  4086.  * @name umbraco.services.listViewPrevalueHelper
  4087.  *
  4088.  *
  4089.  * @description
  4090.  * Service for accessing the prevalues of a list view being edited in the inline list view editor in the doctype editor
  4091.  */
  4092. (function () {
  4093.     'use strict';
  4094.  
  4095.     function listViewPrevalueHelper() {
  4096.  
  4097.         var prevalues = [];
  4098.  
  4099.         /**
  4100.         * @ngdoc method
  4101.         * @name umbraco.services.listViewPrevalueHelper#getPrevalues
  4102.         * @methodOf umbraco.services.listViewPrevalueHelper
  4103.         *
  4104.         * @description
  4105.         * Set the collection of prevalues
  4106.         */
  4107.  
  4108.         function getPrevalues() {
  4109.             return prevalues;
  4110.         }
  4111.  
  4112.         /**
  4113.         * @ngdoc method
  4114.         * @name umbraco.services.listViewPrevalueHelper#setPrevalues
  4115.         * @methodOf umbraco.services.listViewPrevalueHelper
  4116.         *
  4117.         * @description
  4118.         * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage
  4119.         *
  4120.         * @param {Array} values Array of prevalues
  4121.         */
  4122.  
  4123.         function setPrevalues(values) {
  4124.             prevalues = values;
  4125.         }
  4126.  
  4127.        
  4128.  
  4129.         var service = {
  4130.  
  4131.             getPrevalues: getPrevalues,
  4132.             setPrevalues: setPrevalues
  4133.  
  4134.         };
  4135.  
  4136.         return service;
  4137.  
  4138.     }
  4139.  
  4140.  
  4141.     angular.module('umbraco.services').factory('listViewPrevalueHelper', listViewPrevalueHelper);
  4142.  
  4143.  
  4144. })();
  4145.  
  4146. /**
  4147.  * @ngdoc service
  4148.  * @name umbraco.services.localizationService
  4149.  *
  4150.  * @requires $http
  4151.  * @requires $q
  4152.  * @requires $window
  4153.  * @requires $filter
  4154.  *
  4155.  * @description
  4156.  * Application-wide service for handling localization
  4157.  *
  4158.  * ##usage
  4159.  * To use, simply inject the localizationService into any controller that needs it, and make
  4160.  * sure the umbraco.services module is accesible - which it should be by default.
  4161.  *
  4162.  * <pre>
  4163.  *    localizationService.localize("area_key").then(function(value){
  4164.  *        element.html(value);
  4165.  *    });
  4166.  * </pre>
  4167.  */
  4168.  
  4169. angular.module('umbraco.services')
  4170. .factory('localizationService', function ($http, $q, eventsService, $window, $filter, userService) {
  4171.  
  4172.     //TODO: This should be injected as server vars
  4173.     var url = "LocalizedText";
  4174.     var resourceFileLoadStatus = "none";
  4175.     var resourceLoadingPromise = [];
  4176.  
  4177.     function _lookup(value, tokens, dictionary) {
  4178.  
  4179.         //strip the key identifier if its there
  4180.         if (value && value[0] === "@") {
  4181.             value = value.substring(1);
  4182.         }
  4183.  
  4184.         //if no area specified, add general_
  4185.         if (value && value.indexOf("_") < 0) {
  4186.             value = "general_" + value;
  4187.         }
  4188.  
  4189.         var entry = dictionary[value];
  4190.         if (entry) {
  4191.             if (tokens) {
  4192.                 for (var i = 0; i < tokens.length; i++) {
  4193.                     entry = entry.replace("%" + i + "%", tokens[i]);
  4194.                 }
  4195.             }
  4196.             return entry;
  4197.         }
  4198.         return "[" + value + "]";
  4199.     }
  4200.  
  4201.     var service = {
  4202.         // array to hold the localized resource string entries
  4203.         dictionary: [],
  4204.  
  4205.         // loads the language resource file from the server
  4206.         initLocalizedResources: function () {
  4207.             var deferred = $q.defer();
  4208.  
  4209.             if (resourceFileLoadStatus === "loaded") {
  4210.                 deferred.resolve(service.dictionary);
  4211.                 return deferred.promise;
  4212.             }
  4213.  
  4214.             //if the resource is already loading, we don't want to force it to load another one in tandem, we'd rather
  4215.             // wait for that initial http promise to finish and then return this one with the dictionary loaded
  4216.             if (resourceFileLoadStatus === "loading") {
  4217.                 //add to the list of promises waiting
  4218.                 resourceLoadingPromise.push(deferred);
  4219.  
  4220.                 //exit now it's already loading
  4221.                 return deferred.promise;
  4222.             }
  4223.  
  4224.             resourceFileLoadStatus = "loading";
  4225.  
  4226.             // build the url to retrieve the localized resource file
  4227.             $http({ method: "GET", url: url, cache: false })
  4228.                 .then(function (response) {
  4229.                     resourceFileLoadStatus = "loaded";
  4230.                     service.dictionary = response.data;
  4231.  
  4232.                     eventsService.emit("localizationService.updated", response.data);
  4233.  
  4234.                     deferred.resolve(response.data);
  4235.                     //ensure all other queued promises are resolved
  4236.                     for (var p in resourceLoadingPromise) {
  4237.                         resourceLoadingPromise[p].resolve(response.data);
  4238.                     }
  4239.                 }, function (err) {
  4240.                     deferred.reject("Something broke");
  4241.                     //ensure all other queued promises are resolved
  4242.                     for (var p in resourceLoadingPromise) {
  4243.                         resourceLoadingPromise[p].reject("Something broke");
  4244.                     }
  4245.                 });
  4246.             return deferred.promise;
  4247.         },
  4248.  
  4249.         /**
  4250.          * @ngdoc method
  4251.          * @name umbraco.services.localizationService#tokenize
  4252.          * @methodOf umbraco.services.localizationService
  4253.          *
  4254.          * @description
  4255.          * Helper to tokenize and compile a localization string
  4256.          * @param {String} value the value to tokenize
  4257.          * @param {Object} scope the $scope object
  4258.          * @returns {String} tokenized resource string
  4259.          */
  4260.         tokenize: function (value, scope) {
  4261.             if (value) {
  4262.                 var localizer = value.split(':');
  4263.                 var retval = { tokens: undefined, key: localizer[0].substring(0) };
  4264.                 if (localizer.length > 1) {
  4265.                     retval.tokens = localizer[1].split(',');
  4266.                     for (var x = 0; x < retval.tokens.length; x++) {
  4267.                         retval.tokens[x] = scope.$eval(retval.tokens[x]);
  4268.                     }
  4269.                 }
  4270.  
  4271.                 return retval;
  4272.             }
  4273.             return value;
  4274.         },
  4275.  
  4276.         /**
  4277.          * @ngdoc method
  4278.          * @name umbraco.services.localizationService#localize
  4279.          * @methodOf umbraco.services.localizationService
  4280.          *
  4281.          * @description
  4282.          * Checks the dictionary for a localized resource string
  4283.          * @param {String} value the area/key to localize
  4284.          * @param {Array} tokens if specified this array will be sent as parameter values
  4285.          * @returns {String} localized resource string
  4286.          */
  4287.         localize: function (value, tokens) {
  4288.             return service.initLocalizedResources().then(function (dic) {
  4289.                 var val = _lookup(value, tokens, dic);
  4290.                 return val;
  4291.             });
  4292.         },
  4293.  
  4294.     };
  4295.  
  4296.     //This happens after login / auth and assets loading
  4297.     eventsService.on("app.authenticated", function () {
  4298.         resourceFileLoadStatus = "none";
  4299.         resourceLoadingPromise = [];
  4300.     });
  4301.  
  4302.     // return the local instance when called
  4303.     return service;
  4304. });
  4305.  
  4306. /**
  4307.  * @ngdoc service
  4308.  * @name umbraco.services.macroService
  4309.  *
  4310.  *  
  4311.  * @description
  4312.  * A service to return macro information such as generating syntax to insert a macro into an editor
  4313.  */
  4314. function macroService() {
  4315.  
  4316.     return {
  4317.        
  4318.         /** parses the special macro syntax like <?UMBRACO_MACRO macroAlias="Map" /> and returns an object with the macro alias and it's parameters */
  4319.         parseMacroSyntax: function (syntax) {
  4320.            
  4321.             //This regex will match an alias of anything except characters that are quotes or new lines (for legacy reasons, when new macros are created
  4322.             // their aliases are cleaned an invalid chars are stripped)
  4323.             var expression = /(<\?UMBRACO_MACRO (?:.+?)?macroAlias=["']([^\"\'\n\r]+?)["'][\s\S]+?)(\/>|>.*?<\/\?UMBRACO_MACRO>)/i;
  4324.             var match = expression.exec(syntax);
  4325.             if (!match || match.length < 3) {
  4326.                 return null;
  4327.             }
  4328.             var alias = match[2];
  4329.  
  4330.             //this will leave us with just the parameters
  4331.             var paramsChunk = match[1].trim().replace(new RegExp("UMBRACO_MACRO macroAlias=[\"']" + alias + "[\"']"), "").trim();
  4332.            
  4333.             var paramExpression = /(\w+?)=['\"]([\s\S]*?)['\"]/g;
  4334.            
  4335.             var paramMatch;
  4336.             var returnVal = {
  4337.                 macroAlias: alias,
  4338.                 macroParamsDictionary: {}
  4339.             };
  4340.             while (paramMatch = paramExpression.exec(paramsChunk)) {
  4341.                 returnVal.macroParamsDictionary[paramMatch[1]] = paramMatch[2];
  4342.             }
  4343.             return returnVal;
  4344.         },
  4345.  
  4346.         /**
  4347.          * @ngdoc function
  4348.          * @name umbraco.services.macroService#generateWebFormsSyntax
  4349.          * @methodOf umbraco.services.macroService
  4350.          * @function    
  4351.          *
  4352.          * @description
  4353.          * generates the syntax for inserting a macro into a rich text editor - this is the very old umbraco style syntax
  4354.          *
  4355.          * @param {object} args an object containing the macro alias and it's parameter values
  4356.          */
  4357.         generateMacroSyntax: function (args) {
  4358.  
  4359.             // <?UMBRACO_MACRO macroAlias="BlogListPosts" />
  4360.  
  4361.             var macroString = '<?UMBRACO_MACRO macroAlias=\"' + args.macroAlias + "\" ";
  4362.  
  4363.             if (args.macroParamsDictionary) {
  4364.  
  4365.                 _.each(args.macroParamsDictionary, function (val, key) {
  4366.                     //check for null
  4367.                     val = val ? val : "";
  4368.                     //need to detect if the val is a string or an object
  4369.                     var keyVal;
  4370.                     if (angular.isString(val)) {
  4371.                         keyVal = key + "=\"" + (val ? val : "") + "\" ";
  4372.                     }
  4373.                     else {
  4374.                         //if it's not a string we'll send it through the json serializer
  4375.                         var json = angular.toJson(val);
  4376.                         //then we need to url encode it so that it's safe
  4377.                         var encoded = encodeURIComponent(json);
  4378.                         keyVal = key + "=\"" + encoded + "\" ";
  4379.                     }
  4380.                    
  4381.                     macroString += keyVal;
  4382.                 });
  4383.  
  4384.             }
  4385.  
  4386.             macroString += "/>";
  4387.  
  4388.             return macroString;
  4389.         },
  4390.  
  4391.         /**
  4392.          * @ngdoc function
  4393.          * @name umbraco.services.macroService#generateWebFormsSyntax
  4394.          * @methodOf umbraco.services.macroService
  4395.          * @function    
  4396.          *
  4397.          * @description
  4398.          * generates the syntax for inserting a macro into a webforms templates
  4399.          *
  4400.          * @param {object} args an object containing the macro alias and it's parameter values
  4401.          */
  4402.         generateWebFormsSyntax: function(args) {
  4403.            
  4404.             var macroString = '<umbraco:Macro ';
  4405.  
  4406.             if (args.macroParamsDictionary) {
  4407.                
  4408.                 _.each(args.macroParamsDictionary, function (val, key) {
  4409.                     var keyVal = key + "=\"" + (val ? val : "") + "\" ";
  4410.                     macroString += keyVal;
  4411.                 });
  4412.  
  4413.             }
  4414.  
  4415.             macroString += "Alias=\"" + args.macroAlias + "\" runat=\"server\"></umbraco:Macro>";
  4416.  
  4417.             return macroString;
  4418.         },
  4419.        
  4420.         /**
  4421.          * @ngdoc function
  4422.          * @name umbraco.services.macroService#generateMvcSyntax
  4423.          * @methodOf umbraco.services.macroService
  4424.          * @function    
  4425.          *
  4426.          * @description
  4427.          * generates the syntax for inserting a macro into an mvc template
  4428.          *
  4429.          * @param {object} args an object containing the macro alias and it's parameter values
  4430.          */
  4431.         generateMvcSyntax: function (args) {
  4432.  
  4433.             var macroString = "@Umbraco.RenderMacro(\"" + args.macroAlias + "\"";
  4434.  
  4435.             var hasParams = false;
  4436.             var paramString;
  4437.             if (args.macroParamsDictionary) {
  4438.                
  4439.                 paramString = ", new {";
  4440.  
  4441.                 _.each(args.macroParamsDictionary, function(val, key) {
  4442.  
  4443.                     hasParams = true;
  4444.                    
  4445.                     var keyVal = key + "=\"" + (val ? val : "") + "\", ";
  4446.  
  4447.                     paramString += keyVal;
  4448.                 });
  4449.                
  4450.                 //remove the last ,
  4451.                 paramString = paramString.trimEnd(", ");
  4452.  
  4453.                 paramString += "}";
  4454.             }
  4455.             if (hasParams) {
  4456.                 macroString += paramString;
  4457.             }
  4458.  
  4459.             macroString += ")";
  4460.             return macroString;
  4461.         },
  4462.  
  4463.         collectValueData: function(macro, macroParams, renderingEngine) {
  4464.  
  4465.             var paramDictionary = {};
  4466.             var macroAlias = macro.alias;
  4467.             var syntax;
  4468.  
  4469.             _.each(macroParams, function (item) {
  4470.  
  4471.                 var val = item.value;
  4472.  
  4473.                 if (item.value !== null && item.value !== undefined && !_.isString(item.value)) {
  4474.                     try {
  4475.                         val = angular.toJson(val);
  4476.                     }
  4477.                     catch (e) {
  4478.                         // not json
  4479.                     }
  4480.                 }
  4481.  
  4482.                 //each value needs to be xml escaped!! since the value get's stored as an xml attribute
  4483.                 paramDictionary[item.alias] = _.escape(val);
  4484.  
  4485.             });
  4486.  
  4487.             //get the syntax based on the rendering engine
  4488.             if (renderingEngine && renderingEngine === "WebForms") {
  4489.                 syntax = this.generateWebFormsSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary });
  4490.             }
  4491.             else if (renderingEngine && renderingEngine === "Mvc") {
  4492.                 syntax = this.generateMvcSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary });
  4493.             }
  4494.             else {
  4495.                 syntax = this.generateMacroSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary });
  4496.             }
  4497.  
  4498.             var macroObject = {
  4499.                 "macroParamsDictionary": paramDictionary,
  4500.                 "macroAlias": macroAlias,
  4501.                 "syntax": syntax
  4502.             };
  4503.  
  4504.             return macroObject;
  4505.  
  4506.         }
  4507.  
  4508.     };
  4509.  
  4510. }
  4511.  
  4512. angular.module('umbraco.services').factory('macroService', macroService);
  4513.  
  4514. /**
  4515. * @ngdoc service
  4516. * @name umbraco.services.mediaHelper
  4517. * @description A helper object used for dealing with media items
  4518. **/
  4519. function mediaHelper(umbRequestHelper) {
  4520.    
  4521.     //container of fileresolvers
  4522.     var _mediaFileResolvers = {};
  4523.  
  4524.     return {
  4525.         /**
  4526.          * @ngdoc function
  4527.          * @name umbraco.services.mediaHelper#getImagePropertyValue
  4528.          * @methodOf umbraco.services.mediaHelper
  4529.          * @function    
  4530.          *
  4531.          * @description
  4532.          * Returns the file path associated with the media property if there is one
  4533.          *
  4534.          * @param {object} options Options object
  4535.          * @param {object} options.mediaModel The media object to retrieve the image path from
  4536.          * @param {object} options.imageOnly Optional, if true then will only return a path if the media item is an image
  4537.          */
  4538.         getMediaPropertyValue: function (options) {
  4539.             if (!options || !options.mediaModel) {
  4540.                 throw "The options objet does not contain the required parameters: mediaModel";
  4541.             }
  4542.  
  4543.             //combine all props, TODO: we really need a better way then this
  4544.             var props = [];
  4545.             if (options.mediaModel.properties) {
  4546.                 props = options.mediaModel.properties;
  4547.             } else {
  4548.                 $(options.mediaModel.tabs).each(function (i, tab) {
  4549.                     props = props.concat(tab.properties);
  4550.                 });
  4551.             }
  4552.  
  4553.             var mediaRoot = Umbraco.Sys.ServerVariables.umbracoSettings.mediaPath;
  4554.             var imageProp = _.find(props, function (item) {
  4555.                 if (item.alias === "umbracoFile") {
  4556.                     return true;
  4557.                 }
  4558.  
  4559.                 //this performs a simple check to see if we have a media file as value
  4560.                 //it doesnt catch everything, but better then nothing
  4561.                 if (angular.isString(item.value) &&  item.value.indexOf(mediaRoot) === 0) {
  4562.                     return true;
  4563.                 }
  4564.  
  4565.                 return false;
  4566.             });
  4567.  
  4568.             if (!imageProp) {
  4569.                 return "";
  4570.             }
  4571.  
  4572.             var mediaVal;
  4573.  
  4574.             //our default images might store one or many images (as csv)
  4575.             var split = imageProp.value.split(',');
  4576.             var self = this;
  4577.             mediaVal = _.map(split, function (item) {
  4578.                 return { file: item, isImage: self.detectIfImageByExtension(item) };
  4579.             });
  4580.  
  4581.             //for now we'll just return the first image in the collection.
  4582.             //TODO: we should enable returning many to be displayed in the picker if the uploader supports many.
  4583.             if (mediaVal.length && mediaVal.length > 0) {
  4584.                 if (!options.imageOnly || (options.imageOnly === true && mediaVal[0].isImage)) {
  4585.                     return mediaVal[0].file;
  4586.                 }
  4587.             }
  4588.  
  4589.             return "";
  4590.         },
  4591.        
  4592.         /**
  4593.          * @ngdoc function
  4594.          * @name umbraco.services.mediaHelper#getImagePropertyValue
  4595.          * @methodOf umbraco.services.mediaHelper
  4596.          * @function    
  4597.          *
  4598.          * @description
  4599.          * Returns the actual image path associated with the image property if there is one
  4600.          *
  4601.          * @param {object} options Options object
  4602.          * @param {object} options.imageModel The media object to retrieve the image path from
  4603.          */
  4604.         getImagePropertyValue: function (options) {
  4605.             if (!options || (!options.imageModel && !options.mediaModel)) {
  4606.                 throw "The options objet does not contain the required parameters: imageModel";
  4607.             }
  4608.  
  4609.             //required to support backwards compatibility.
  4610.             options.mediaModel = options.imageModel ? options.imageModel : options.mediaModel;
  4611.  
  4612.             options.imageOnly = true;
  4613.  
  4614.             return this.getMediaPropertyValue(options);
  4615.         },
  4616.         /**
  4617.          * @ngdoc function
  4618.          * @name umbraco.services.mediaHelper#getThumbnail
  4619.          * @methodOf umbraco.services.mediaHelper
  4620.          * @function    
  4621.          *
  4622.          * @description
  4623.          * formats the display model used to display the content to the model used to save the content
  4624.          *
  4625.          * @param {object} options Options object
  4626.          * @param {object} options.imageModel The media object to retrieve the image path from
  4627.          */
  4628.         getThumbnail: function (options) {
  4629.  
  4630.             if (!options || !options.imageModel) {
  4631.                 throw "The options objet does not contain the required parameters: imageModel";
  4632.             }
  4633.  
  4634.             var imagePropVal = this.getImagePropertyValue(options);
  4635.             if (imagePropVal !== "") {
  4636.                 return this.getThumbnailFromPath(imagePropVal);
  4637.             }
  4638.             return "";
  4639.         },
  4640.  
  4641.         registerFileResolver: function(propertyEditorAlias, func){
  4642.             _mediaFileResolvers[propertyEditorAlias] = func;
  4643.         },
  4644.  
  4645.         /**
  4646.          * @ngdoc function
  4647.          * @name umbraco.services.mediaHelper#resolveFileFromEntity
  4648.          * @methodOf umbraco.services.mediaHelper
  4649.          * @function    
  4650.          *
  4651.          * @description
  4652.          * Gets the media file url for a media entity returned with the entityResource
  4653.          *
  4654.          * @param {object} mediaEntity A media Entity returned from the entityResource
  4655.          * @param {boolean} thumbnail Whether to return the thumbnail url or normal url
  4656.          */
  4657.         resolveFileFromEntity : function(mediaEntity, thumbnail) {
  4658.            
  4659.             if (!angular.isObject(mediaEntity.metaData)) {
  4660.                 throw "Cannot resolve the file url from the mediaEntity, it does not contain the required metaData";
  4661.             }
  4662.  
  4663.             var values = _.values(mediaEntity.metaData);
  4664.             for (var i = 0; i < values.length; i++) {
  4665.                 var val = values[i];
  4666.                 if (angular.isObject(val) && val.PropertyEditorAlias) {
  4667.                     for (var resolver in _mediaFileResolvers) {
  4668.                         if (val.PropertyEditorAlias === resolver) {
  4669.                             //we need to format a property variable that coincides with how the property would be structured
  4670.                             // if it came from the mediaResource just to keep things slightly easier for the file resolvers.
  4671.                             var property = { value: val.Value };
  4672.  
  4673.                             return _mediaFileResolvers[resolver](property, mediaEntity, thumbnail);
  4674.                         }
  4675.                     }
  4676.                 }
  4677.             }
  4678.  
  4679.             return "";
  4680.         },
  4681.  
  4682.         /**
  4683.          * @ngdoc function
  4684.          * @name umbraco.services.mediaHelper#resolveFile
  4685.          * @methodOf umbraco.services.mediaHelper
  4686.          * @function    
  4687.          *
  4688.          * @description
  4689.          * Gets the media file url for a media object returned with the mediaResource
  4690.          *
  4691.          * @param {object} mediaEntity A media Entity returned from the entityResource
  4692.          * @param {boolean} thumbnail Whether to return the thumbnail url or normal url
  4693.          */
  4694.         /*jshint loopfunc: true */
  4695.         resolveFile : function(mediaItem, thumbnail){
  4696.            
  4697.             function iterateProps(props){
  4698.                 var res = null;
  4699.                 for(var resolver in _mediaFileResolvers) {
  4700.                     var property = _.find(props, function(prop){ return prop.editor === resolver; });
  4701.                     if(property){
  4702.                         res = _mediaFileResolvers[resolver](property, mediaItem, thumbnail);
  4703.                         break;
  4704.                     }
  4705.                 }
  4706.  
  4707.                 return res;    
  4708.             }
  4709.  
  4710.             //we either have properties raw on the object, or spread out on tabs
  4711.             var result = "";
  4712.             if(mediaItem.properties){
  4713.                 result = iterateProps(mediaItem.properties);
  4714.             }else if(mediaItem.tabs){
  4715.                 for(var tab in mediaItem.tabs) {
  4716.                     if(mediaItem.tabs[tab].properties){
  4717.                         result = iterateProps(mediaItem.tabs[tab].properties);
  4718.                         if(result){
  4719.                             break;
  4720.                         }
  4721.                     }
  4722.                 }
  4723.             }
  4724.             return result;            
  4725.         },
  4726.  
  4727.         /*jshint loopfunc: true */
  4728.         hasFilePropertyType : function(mediaItem){
  4729.            function iterateProps(props){
  4730.                var res = false;
  4731.                for(var resolver in _mediaFileResolvers) {
  4732.                    var property = _.find(props, function(prop){ return prop.editor === resolver; });
  4733.                    if(property){
  4734.                        res = true;
  4735.                        break;
  4736.                    }
  4737.                }
  4738.                return res;
  4739.            }
  4740.  
  4741.            //we either have properties raw on the object, or spread out on tabs
  4742.            var result = false;
  4743.            if(mediaItem.properties){
  4744.                result = iterateProps(mediaItem.properties);
  4745.            }else if(mediaItem.tabs){
  4746.                for(var tab in mediaItem.tabs) {
  4747.                    if(mediaItem.tabs[tab].properties){
  4748.                        result = iterateProps(mediaItem.tabs[tab].properties);
  4749.                        if(result){
  4750.                            break;
  4751.                        }
  4752.                    }
  4753.                }
  4754.            }
  4755.            return result;
  4756.         },
  4757.  
  4758.         /**
  4759.          * @ngdoc function
  4760.          * @name umbraco.services.mediaHelper#scaleToMaxSize
  4761.          * @methodOf umbraco.services.mediaHelper
  4762.          * @function    
  4763.          *
  4764.          * @description
  4765.          * Finds the corrct max width and max height, given maximum dimensions and keeping aspect ratios
  4766.          *
  4767.          * @param {number} maxSize Maximum width & height
  4768.          * @param {number} width Current width
  4769.          * @param {number} height Current height
  4770.          */
  4771.         scaleToMaxSize: function (maxSize, width, height) {
  4772.             var retval = { width: width, height: height };
  4773.  
  4774.             var maxWidth = maxSize; // Max width for the image
  4775.             var maxHeight = maxSize;    // Max height for the image
  4776.             var ratio = 0;  // Used for aspect ratio
  4777.  
  4778.             // Check if the current width is larger than the max
  4779.             if (width > maxWidth) {
  4780.                 ratio = maxWidth / width;   // get ratio for scaling image
  4781.  
  4782.                 retval.width = maxWidth;
  4783.                 retval.height = height * ratio;
  4784.  
  4785.                 height = height * ratio;    // Reset height to match scaled image
  4786.                 width = width * ratio;    // Reset width to match scaled image
  4787.             }
  4788.  
  4789.             // Check if current height is larger than max
  4790.             if (height > maxHeight) {
  4791.                 ratio = maxHeight / height; // get ratio for scaling image
  4792.  
  4793.                 retval.height = maxHeight;
  4794.                 retval.width = width * ratio;
  4795.                 width = width * ratio;    // Reset width to match scaled image
  4796.             }
  4797.  
  4798.             return retval;
  4799.         },
  4800.  
  4801.         /**
  4802.          * @ngdoc function
  4803.          * @name umbraco.services.mediaHelper#getThumbnailFromPath
  4804.          * @methodOf umbraco.services.mediaHelper
  4805.          * @function    
  4806.          *
  4807.          * @description
  4808.          * Returns the path to the thumbnail version of a given media library image path
  4809.          *
  4810.          * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg
  4811.          */
  4812.         getThumbnailFromPath: function (imagePath) {
  4813.  
  4814.             //If the path is not an image we cannot get a thumb
  4815.             if (!this.detectIfImageByExtension(imagePath)) {
  4816.                 return null;
  4817.             }
  4818.  
  4819.             //get the proxy url for big thumbnails (this ensures one is always generated)
  4820.             var thumbnailUrl = umbRequestHelper.getApiUrl(
  4821.                 "imagesApiBaseUrl",
  4822.                 "GetBigThumbnail",
  4823.                 [{ originalImagePath: imagePath }]);
  4824.  
  4825.             //var ext = imagePath.substr(imagePath.lastIndexOf('.'));
  4826.             //return imagePath.substr(0, imagePath.lastIndexOf('.')) + "_big-thumb" + ".jpg";
  4827.  
  4828.             return thumbnailUrl;
  4829.         },
  4830.  
  4831.         /**
  4832.          * @ngdoc function
  4833.          * @name umbraco.services.mediaHelper#detectIfImageByExtension
  4834.          * @methodOf umbraco.services.mediaHelper
  4835.          * @function    
  4836.          *
  4837.          * @description
  4838.          * Returns true/false, indicating if the given path has an allowed image extension
  4839.          *
  4840.          * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg
  4841.          */
  4842.         detectIfImageByExtension: function (imagePath) {
  4843.  
  4844.             if (!imagePath) {
  4845.                 return false;
  4846.             }
  4847.            
  4848.             var lowered = imagePath.toLowerCase();
  4849.             var ext = lowered.substr(lowered.lastIndexOf(".") + 1);
  4850.             return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1;
  4851.         },
  4852.  
  4853.         /**
  4854.          * @ngdoc function
  4855.          * @name umbraco.services.mediaHelper#formatFileTypes
  4856.          * @methodOf umbraco.services.mediaHelper
  4857.          * @function
  4858.          *
  4859.          * @description
  4860.          * Returns a string with correctly formated file types for ng-file-upload
  4861.          *
  4862.          * @param {string} file types, ex: jpg,png,tiff
  4863.          */
  4864.         formatFileTypes: function(fileTypes) {
  4865.  
  4866.            var fileTypesArray = fileTypes.split(',');
  4867.            var newFileTypesArray = [];
  4868.  
  4869.            for (var i = 0; i < fileTypesArray.length; i++) {
  4870.               var fileType = fileTypesArray[i];
  4871.  
  4872.               if (fileType.indexOf(".") !== 0) {
  4873.                  fileType = ".".concat(fileType);
  4874.               }
  4875.  
  4876.               newFileTypesArray.push(fileType);
  4877.            }
  4878.  
  4879.            return newFileTypesArray.join(",");
  4880.  
  4881.         }
  4882.        
  4883.     };
  4884. }angular.module('umbraco.services').factory('mediaHelper', mediaHelper);
  4885.  
  4886. /**
  4887.  * @ngdoc service
  4888.  * @name umbraco.services.mediaTypeHelper
  4889.  * @description A helper service for the media types
  4890.  **/
  4891. function mediaTypeHelper(mediaTypeResource, $q) {
  4892.  
  4893.     var mediaTypeHelperService = {
  4894.  
  4895.         isFolderType: function(mediaEntity) {
  4896.             if (!mediaEntity) {
  4897.                 throw "mediaEntity is null";
  4898.             }
  4899.             if (!mediaEntity.contentTypeAlias) {
  4900.                 throw "mediaEntity.contentTypeAlias is null";
  4901.             }
  4902.  
  4903.             //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder"
  4904.             //this is the exact same logic that is performed in MediaController.GetChildFolders
  4905.             return mediaEntity.contentTypeAlias.endsWith("Folder");
  4906.         },
  4907.  
  4908.         getAllowedImagetypes: function (mediaId){
  4909.                
  4910.             // Get All allowedTypes
  4911.             return mediaTypeResource.getAllowedTypes(mediaId)
  4912.                 .then(function(types){
  4913.                    
  4914.                     var allowedQ = types.map(function(type){
  4915.                         return mediaTypeResource.getById(type.id);
  4916.                     });
  4917.  
  4918.                     // Get full list
  4919.                     return $q.all(allowedQ).then(function(fullTypes){
  4920.  
  4921.                         // Find all the media types with an Image Cropper property editor
  4922.                         var filteredTypes = mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper']);
  4923.  
  4924.                         // If there is only one media type with an Image Cropper we will return this one
  4925.                         if(filteredTypes.length === 1) {
  4926.                             return filteredTypes;
  4927.                         // 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
  4928.                         } else {
  4929.                             return mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper', 'Umbraco.UploadField']);
  4930.                         }
  4931.  
  4932.                     });
  4933.             });
  4934.         },
  4935.  
  4936.         getTypeWithEditor: function (types, editors) {
  4937.  
  4938.             return types.filter(function (mediatype) {
  4939.                 for (var i = 0; i < mediatype.groups.length; i++) {
  4940.                     var group = mediatype.groups[i];
  4941.                     for (var j = 0; j < group.properties.length; j++) {
  4942.                         var property = group.properties[j];
  4943.                         if( editors.indexOf(property.editor) !== -1 ) {
  4944.                             return mediatype;
  4945.                         }
  4946.                     }
  4947.                 }
  4948.             });
  4949.  
  4950.         }
  4951.  
  4952.     };
  4953.  
  4954.     return mediaTypeHelperService;
  4955. }
  4956. angular.module('umbraco.services').factory('mediaTypeHelper', mediaTypeHelper);
  4957.  
  4958. /**
  4959.  * @ngdoc service
  4960.  * @name umbraco.services.umbracoMenuActions
  4961.  *
  4962.  * @requires q
  4963.  * @requires treeService
  4964.  * 
  4965.  * @description
  4966.  * Defines the methods that are called when menu items declare only an action to execute
  4967.  */
  4968. function umbracoMenuActions($q, treeService, $location, navigationService, appState) {
  4969.    
  4970.     return {
  4971.        
  4972.         /**
  4973.          * @ngdoc method
  4974.          * @name umbraco.services.umbracoMenuActions#RefreshNode
  4975.          * @methodOf umbraco.services.umbracoMenuActions
  4976.          * @function
  4977.          *
  4978.          * @description
  4979.          * Clears all node children and then gets it's up-to-date children from the server and re-assigns them
  4980.          * @param {object} args An arguments object
  4981.          * @param {object} args.entity The basic entity being acted upon
  4982.          * @param {object} args.treeAlias The tree alias associated with this entity
  4983.          * @param {object} args.section The current section
  4984.          */
  4985.         "RefreshNode": function (args) {
  4986.            
  4987.             ////just in case clear any tree cache for this node/section
  4988.             //treeService.clearCache({
  4989.             //    cacheKey: "__" + args.section, //each item in the tree cache is cached by the section name
  4990.             //    childrenOf: args.entity.parentId //clear the children of the parent
  4991.             //});
  4992.  
  4993.             //since we're dealing with an entity, we need to attempt to find it's tree node, in the main tree
  4994.             // this action is purely a UI thing so if for whatever reason there is no loaded tree node in the UI
  4995.             // we can safely ignore this process.
  4996.            
  4997.             //to find a visible tree node, we'll go get the currently loaded root node from appState
  4998.             var treeRoot = appState.getTreeState("currentRootNode");
  4999.             if (treeRoot && treeRoot.root) {
  5000.                 var treeNode = treeService.getDescendantNode(treeRoot.root, args.entity.id, args.treeAlias);
  5001.                 if (treeNode) {
  5002.                     treeService.loadNodeChildren({ node: treeNode, section: args.section });
  5003.                 }                
  5004.             }
  5005.  
  5006.            
  5007.         },
  5008.        
  5009.         /**
  5010.          * @ngdoc method
  5011.          * @name umbraco.services.umbracoMenuActions#CreateChildEntity
  5012.          * @methodOf umbraco.services.umbracoMenuActions
  5013.          * @function
  5014.          *
  5015.          * @description
  5016.          * This will re-route to a route for creating a new entity as a child of the current node
  5017.          * @param {object} args An arguments object
  5018.          * @param {object} args.entity The basic entity being acted upon
  5019.          * @param {object} args.treeAlias The tree alias associated with this entity
  5020.          * @param {object} args.section The current section
  5021.          */
  5022.         "CreateChildEntity": function (args) {
  5023.  
  5024.             navigationService.hideNavigation();
  5025.  
  5026.             var route = "/" + args.section + "/" + args.treeAlias + "/edit/" + args.entity.id;
  5027.             //change to new path
  5028.             $location.path(route).search({ create: true });
  5029.            
  5030.         }
  5031.     };
  5032. }
  5033.  
  5034. angular.module('umbraco.services').factory('umbracoMenuActions', umbracoMenuActions);
  5035. /**
  5036.  * @ngdoc service
  5037.  * @name umbraco.services.navigationService
  5038.  *
  5039.  * @requires $rootScope
  5040.  * @requires $routeParams
  5041.  * @requires $log
  5042.  * @requires $location
  5043.  * @requires dialogService
  5044.  * @requires treeService
  5045.  * @requires sectionResource
  5046.  *
  5047.  * @description
  5048.  * Service to handle the main application navigation. Responsible for invoking the tree
  5049.  * Section navigation and search, and maintain their state for the entire application lifetime
  5050.  *
  5051.  */
  5052. function navigationService($rootScope, $routeParams, $log, $location, $q, $timeout, $injector, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) {
  5053.  
  5054.  
  5055.     //used to track the current dialog object
  5056.     var currentDialog = null;
  5057.  
  5058.     //the main tree event handler, which gets assigned via the setupTreeEvents method
  5059.     var mainTreeEventHandler = null;
  5060.     //tracks the user profile dialog
  5061.     var userDialog = null;
  5062.  
  5063.     function setMode(mode) {
  5064.         switch (mode) {
  5065.         case 'tree':
  5066.             appState.setGlobalState("navMode", "tree");
  5067.             appState.setGlobalState("showNavigation", true);
  5068.             appState.setMenuState("showMenu", false);
  5069.             appState.setMenuState("showMenuDialog", false);
  5070.             appState.setGlobalState("stickyNavigation", false);
  5071.             appState.setGlobalState("showTray", false);
  5072.  
  5073.             //$("#search-form input").focus();
  5074.             break;
  5075.         case 'menu':
  5076.             appState.setGlobalState("navMode", "menu");
  5077.             appState.setGlobalState("showNavigation", true);
  5078.             appState.setMenuState("showMenu", true);
  5079.             appState.setMenuState("showMenuDialog", false);
  5080.             appState.setGlobalState("stickyNavigation", true);
  5081.             break;
  5082.         case 'dialog':
  5083.             appState.setGlobalState("navMode", "dialog");
  5084.             appState.setGlobalState("stickyNavigation", true);
  5085.             appState.setGlobalState("showNavigation", true);
  5086.             appState.setMenuState("showMenu", false);
  5087.             appState.setMenuState("showMenuDialog", true);
  5088.             break;
  5089.         case 'search':
  5090.             appState.setGlobalState("navMode", "search");
  5091.             appState.setGlobalState("stickyNavigation", false);
  5092.             appState.setGlobalState("showNavigation", true);
  5093.             appState.setMenuState("showMenu", false);
  5094.             appState.setSectionState("showSearchResults", true);
  5095.             appState.setMenuState("showMenuDialog", false);
  5096.  
  5097.             //TODO: This would be much better off in the search field controller listening to appState changes
  5098.             $timeout(function() {
  5099.                 $("#search-field").focus();
  5100.             });
  5101.  
  5102.             break;
  5103.         default:
  5104.             appState.setGlobalState("navMode", "default");
  5105.             appState.setMenuState("showMenu", false);
  5106.             appState.setMenuState("showMenuDialog", false);
  5107.             appState.setSectionState("showSearchResults", false);
  5108.             appState.setGlobalState("stickyNavigation", false);
  5109.             appState.setGlobalState("showTray", false);
  5110.  
  5111.             if (appState.getGlobalState("isTablet") === true) {
  5112.                 appState.setGlobalState("showNavigation", false);
  5113.             }
  5114.  
  5115.             break;
  5116.         }
  5117.     }
  5118.  
  5119.     var service = {
  5120.  
  5121.         /** initializes the navigation service */
  5122.         init: function() {
  5123.  
  5124.             //keep track of the current section - initially this will always be undefined so
  5125.             // no point in setting it now until it changes.
  5126.             $rootScope.$watch(function () {
  5127.                 return $routeParams.section;
  5128.             }, function (newVal, oldVal) {
  5129.                 appState.setSectionState("currentSection", newVal);
  5130.             });
  5131.  
  5132.  
  5133.         },
  5134.  
  5135.         /**
  5136.          * @ngdoc method
  5137.          * @name umbraco.services.navigationService#load
  5138.          * @methodOf umbraco.services.navigationService
  5139.          *
  5140.          * @description
  5141.          * Shows the legacy iframe and loads in the content based on the source url
  5142.          * @param {String} source The URL to load into the iframe
  5143.          */
  5144.         loadLegacyIFrame: function (source) {
  5145.             $location.path("/" + appState.getSectionState("currentSection") + "/framed/" + encodeURIComponent(source));
  5146.         },
  5147.  
  5148.         /**
  5149.          * @ngdoc method
  5150.          * @name umbraco.services.navigationService#changeSection
  5151.          * @methodOf umbraco.services.navigationService
  5152.          *
  5153.          * @description
  5154.          * Changes the active section to a given section alias
  5155.          * If the navigation is 'sticky' this will load the associated tree
  5156.          * and load the dashboard related to the section
  5157.          * @param {string} sectionAlias The alias of the section
  5158.          */
  5159.         changeSection: function(sectionAlias, force) {
  5160.             setMode("default-opensection");
  5161.  
  5162.             if (force && appState.getSectionState("currentSection") === sectionAlias) {
  5163.                 appState.setSectionState("currentSection", "");
  5164.             }
  5165.  
  5166.             appState.setSectionState("currentSection", sectionAlias);
  5167.             this.showTree(sectionAlias);
  5168.  
  5169.             $location.path(sectionAlias);
  5170.         },
  5171.  
  5172.         /**
  5173.          * @ngdoc method
  5174.          * @name umbraco.services.navigationService#showTree
  5175.          * @methodOf umbraco.services.navigationService
  5176.          *
  5177.          * @description
  5178.          * Displays the tree for a given section alias but turning on the containing dom element
  5179.          * only changes if the section is different from the current one
  5180.          * @param {string} sectionAlias The alias of the section to load
  5181.          * @param {Object} syncArgs Optional object of arguments for syncing the tree for the section being shown
  5182.          */
  5183.         showTree: function (sectionAlias, syncArgs) {
  5184.             if (sectionAlias !== appState.getSectionState("currentSection")) {
  5185.                 appState.setSectionState("currentSection", sectionAlias);
  5186.  
  5187.                 if (syncArgs) {
  5188.                     this.syncTree(syncArgs);
  5189.                 }
  5190.             }
  5191.             setMode("tree");
  5192.         },
  5193.  
  5194.         showTray: function () {
  5195.             appState.setGlobalState("showTray", true);
  5196.         },
  5197.  
  5198.         hideTray: function () {
  5199.             appState.setGlobalState("showTray", false);
  5200.         },
  5201.  
  5202.         /**
  5203.             Called to assign the main tree event handler - this is called by the navigation controller.
  5204.             TODO: Potentially another dev could call this which would kind of mung the whole app so potentially there's a better way.
  5205.         */
  5206.         setupTreeEvents: function(treeEventHandler) {
  5207.             mainTreeEventHandler = treeEventHandler;
  5208.  
  5209.             //when a tree is loaded into a section, we need to put it into appState
  5210.             mainTreeEventHandler.bind("treeLoaded", function(ev, args) {
  5211.                 appState.setTreeState("currentRootNode", args.tree);
  5212.             });
  5213.  
  5214.             //when a tree node is synced this event will fire, this allows us to set the currentNode
  5215.             mainTreeEventHandler.bind("treeSynced", function (ev, args) {
  5216.  
  5217.                 if (args.activate === undefined || args.activate === true) {
  5218.                     //set the current selected node
  5219.                     appState.setTreeState("selectedNode", args.node);
  5220.                     //when a node is activated, this is the same as clicking it and we need to set the
  5221.                     //current menu item to be this node as well.
  5222.                     appState.setMenuState("currentNode", args.node);
  5223.                 }
  5224.             });
  5225.  
  5226.             //this reacts to the options item in the tree
  5227.             mainTreeEventHandler.bind("treeOptionsClick", function(ev, args) {
  5228.                 ev.stopPropagation();
  5229.                 ev.preventDefault();
  5230.  
  5231.                 //Set the current action node (this is not the same as the current selected node!)
  5232.                 appState.setMenuState("currentNode", args.node);
  5233.  
  5234.                 if (args.event && args.event.altKey) {
  5235.                     args.skipDefault = true;
  5236.                 }
  5237.  
  5238.                 service.showMenu(ev, args);
  5239.             });
  5240.  
  5241.             mainTreeEventHandler.bind("treeNodeAltSelect", function(ev, args) {
  5242.                 ev.stopPropagation();
  5243.                 ev.preventDefault();
  5244.  
  5245.                 args.skipDefault = true;
  5246.                 service.showMenu(ev, args);
  5247.             });
  5248.  
  5249.             //this reacts to tree items themselves being clicked
  5250.             //the tree directive should not contain any handling, simply just bubble events
  5251.             mainTreeEventHandler.bind("treeNodeSelect", function (ev, args) {
  5252.                 var n = args.node;
  5253.                 ev.stopPropagation();
  5254.                 ev.preventDefault();
  5255.  
  5256.                 if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") {
  5257.                     //this is a legacy tree node!
  5258.                     var jsPrefix = "javascript:";
  5259.                     var js;
  5260.                     if (n.metaData["jsClickCallback"].startsWith(jsPrefix)) {
  5261.                         js = n.metaData["jsClickCallback"].substr(jsPrefix.length);
  5262.                     }
  5263.                     else {
  5264.                         js = n.metaData["jsClickCallback"];
  5265.                     }
  5266.                     try {
  5267.                         var func = eval(js);
  5268.                         //this is normally not necessary since the eval above should execute the method and will return nothing.
  5269.                         if (func != null && (typeof func === "function")) {
  5270.                             func.call();
  5271.                         }
  5272.                     }
  5273.                     catch(ex) {
  5274.                         $log.error("Error evaluating js callback from legacy tree node: " + ex);
  5275.                     }
  5276.                 }
  5277.                 else if (n.routePath) {
  5278.                     //add action to the history service
  5279.                     historyService.add({ name: n.name, link: n.routePath, icon: n.icon });
  5280.  
  5281.                     //put this node into the tree state
  5282.                     appState.setTreeState("selectedNode", args.node);
  5283.                     //when a node is clicked we also need to set the active menu node to this node
  5284.                     appState.setMenuState("currentNode", args.node);
  5285.  
  5286.                     //not legacy, lets just set the route value and clear the query string if there is one.
  5287.                     $location.path(n.routePath).search("");
  5288.                 }
  5289.                 else if (args.element.section) {
  5290.                     $location.path(args.element.section).search("");
  5291.                 }
  5292.  
  5293.                 service.hideNavigation();
  5294.             });
  5295.         },
  5296.         /**
  5297.          * @ngdoc method
  5298.          * @name umbraco.services.navigationService#syncTree
  5299.          * @methodOf umbraco.services.navigationService
  5300.          *
  5301.          * @description
  5302.          * Syncs a tree with a given path, returns a promise
  5303.          * The path format is: ["itemId","itemId"], and so on
  5304.          * so to sync to a specific document type node do:
  5305.          * <pre>
  5306.          * navigationService.syncTree({tree: 'content', path: ["-1","123d"], forceReload: true});
  5307.          * </pre>
  5308.          * @param {Object} args arguments passed to the function
  5309.          * @param {String} args.tree the tree alias to sync to
  5310.          * @param {Array} args.path the path to sync the tree to
  5311.          * @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
  5312.          * @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
  5313.          */
  5314.         syncTree: function (args) {
  5315.             if (!args) {
  5316.                 throw "args cannot be null";
  5317.             }
  5318.             if (!args.path) {
  5319.                 throw "args.path cannot be null";
  5320.             }
  5321.             if (!args.tree) {
  5322.                 throw "args.tree cannot be null";
  5323.             }
  5324.  
  5325.             if (mainTreeEventHandler) {
  5326.                 //returns a promise
  5327.                 return mainTreeEventHandler.syncTree(args);
  5328.             }
  5329.  
  5330.             //couldn't sync
  5331.             return angularHelper.rejectedPromise();
  5332.         },
  5333.  
  5334.         /**
  5335.             Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to
  5336.             have to set an active tree and then sync, the new API does this in one method by using syncTree
  5337.         */
  5338.         _syncPath: function(path, forceReload) {
  5339.             if (mainTreeEventHandler) {
  5340.                 mainTreeEventHandler.syncTree({ path: path, forceReload: forceReload });
  5341.             }
  5342.         },
  5343.  
  5344.         //TODO: This should return a promise
  5345.         reloadNode: function(node) {
  5346.             if (mainTreeEventHandler) {
  5347.                 mainTreeEventHandler.reloadNode(node);
  5348.             }
  5349.         },
  5350.  
  5351.         //TODO: This should return a promise
  5352.         reloadSection: function(sectionAlias) {
  5353.             if (mainTreeEventHandler) {
  5354.                 mainTreeEventHandler.clearCache({ section: sectionAlias });
  5355.                 mainTreeEventHandler.load(sectionAlias);
  5356.             }
  5357.         },
  5358.  
  5359.         /**
  5360.             Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to
  5361.             have to set an active tree and then sync, the new API does this in one method by using syncTreePath
  5362.         */
  5363.         _setActiveTreeType: function (treeAlias, loadChildren) {
  5364.             if (mainTreeEventHandler) {
  5365.                 mainTreeEventHandler._setActiveTreeType(treeAlias, loadChildren);
  5366.             }
  5367.         },
  5368.  
  5369.         /**
  5370.          * @ngdoc method
  5371.          * @name umbraco.services.navigationService#hideTree
  5372.          * @methodOf umbraco.services.navigationService
  5373.          *
  5374.          * @description
  5375.          * Hides the tree by hiding the containing dom element
  5376.          */
  5377.         hideTree: function() {
  5378.  
  5379.             if (appState.getGlobalState("isTablet") === true && !appState.getGlobalState("stickyNavigation")) {
  5380.                 //reset it to whatever is in the url
  5381.                 appState.setSectionState("currentSection", $routeParams.section);
  5382.                 setMode("default-hidesectiontree");
  5383.             }
  5384.  
  5385.         },
  5386.  
  5387.         /**
  5388.          * @ngdoc method
  5389.          * @name umbraco.services.navigationService#showMenu
  5390.          * @methodOf umbraco.services.navigationService
  5391.          *
  5392.          * @description
  5393.          * Hides the tree by hiding the containing dom element.
  5394.          * This always returns a promise!
  5395.          *
  5396.          * @param {Event} event the click event triggering the method, passed from the DOM element
  5397.          */
  5398.         showMenu: function(event, args) {
  5399.  
  5400.             var deferred = $q.defer();
  5401.             var self = this;
  5402.  
  5403.             treeService.getMenu({ treeNode: args.node })
  5404.                 .then(function(data) {
  5405.  
  5406.                     //check for a default
  5407.                     //NOTE: event will be undefined when a call to hideDialog is made so it won't re-load the default again.
  5408.                     // but perhaps there's a better way to deal with with an additional parameter in the args ? it works though.
  5409.                     if (data.defaultAlias && !args.skipDefault) {
  5410.  
  5411.                         var found = _.find(data.menuItems, function(item) {
  5412.                             return item.alias = data.defaultAlias;
  5413.                         });
  5414.  
  5415.                         if (found) {
  5416.  
  5417.                             //NOTE: This is assigning the current action node - this is not the same as the currently selected node!
  5418.                             appState.setMenuState("currentNode", args.node);
  5419.  
  5420.                             //ensure the current dialog is cleared before creating another!
  5421.                             if (currentDialog) {
  5422.                                 dialogService.close(currentDialog);
  5423.                             }
  5424.  
  5425.                             var dialog = self.showDialog({
  5426.                                 node: args.node,
  5427.                                 action: found,
  5428.                                 section: appState.getSectionState("currentSection")
  5429.                             });
  5430.  
  5431.                             //return the dialog this is opening.
  5432.                             deferred.resolve(dialog);
  5433.                             return;
  5434.                         }
  5435.                     }
  5436.  
  5437.                     //there is no default or we couldn't find one so just continue showing the menu
  5438.  
  5439.                     setMode("menu");
  5440.  
  5441.                     appState.setMenuState("currentNode", args.node);
  5442.                     appState.setMenuState("menuActions", data.menuItems);
  5443.                     appState.setMenuState("dialogTitle", args.node.name);
  5444.  
  5445.                     //we're not opening a dialog, return null.
  5446.                     deferred.resolve(null);
  5447.                 });
  5448.  
  5449.             return deferred.promise;
  5450.         },
  5451.  
  5452.         /**
  5453.          * @ngdoc method
  5454.          * @name umbraco.services.navigationService#hideMenu
  5455.          * @methodOf umbraco.services.navigationService
  5456.          *
  5457.          * @description
  5458.          * Hides the menu by hiding the containing dom element
  5459.          */
  5460.         hideMenu: function() {
  5461.             //SD: Would we ever want to access the last action'd node instead of clearing it here?
  5462.             appState.setMenuState("currentNode", null);
  5463.             appState.setMenuState("menuActions", []);
  5464.             setMode("tree");
  5465.         },
  5466.  
  5467.         /** Executes a given menu action */
  5468.         executeMenuAction: function (action, node, section) {
  5469.  
  5470.             if (!action) {
  5471.                 throw "action cannot be null";
  5472.             }
  5473.             if (!node) {
  5474.                 throw "node cannot be null";
  5475.             }
  5476.             if (!section) {
  5477.                 throw "section cannot be null";
  5478.             }
  5479.  
  5480.             if (action.metaData && action.metaData["actionRoute"] && angular.isString(action.metaData["actionRoute"])) {
  5481.                 //first check if the menu item simply navigates to a route
  5482.                 var parts = action.metaData["actionRoute"].split("?");
  5483.                 $location.path(parts[0]).search(parts.length > 1 ? parts[1] : "");
  5484.                 this.hideNavigation();
  5485.                 return;
  5486.             }
  5487.             else if (action.metaData && action.metaData["jsAction"] && angular.isString(action.metaData["jsAction"])) {
  5488.  
  5489.                 //we'll try to get the jsAction from the injector
  5490.                 var menuAction = action.metaData["jsAction"].split('.');
  5491.                 if (menuAction.length !== 2) {
  5492.  
  5493.                     //if it is not two parts long then this most likely means that it's a legacy action
  5494.                     var js = action.metaData["jsAction"].replace("javascript:", "");
  5495.                     //there's not really a different way to acheive this except for eval
  5496.                     eval(js);
  5497.                 }
  5498.                 else {
  5499.                     var menuActionService = $injector.get(menuAction[0]);
  5500.                     if (!menuActionService) {
  5501.                         throw "The angular service " + menuAction[0] + " could not be found";
  5502.                     }
  5503.  
  5504.                     var method = menuActionService[menuAction[1]];
  5505.  
  5506.                     if (!method) {
  5507.                         throw "The method " + menuAction[1] + " on the angular service " + menuAction[0] + " could not be found";
  5508.                     }
  5509.  
  5510.                     method.apply(this, [{
  5511.                         //map our content object to a basic entity to pass in to the menu handlers,
  5512.                         //this is required for consistency since a menu item needs to be decoupled from a tree node since the menu can
  5513.                         //exist standalone in the editor for which it can only pass in an entity (not tree node).
  5514.                         entity: umbModelMapper.convertToEntityBasic(node),
  5515.                         action: action,
  5516.                         section: section,
  5517.                         treeAlias: treeService.getTreeAlias(node)
  5518.                     }]);
  5519.                 }
  5520.             }
  5521.             else {
  5522.                 service.showDialog({
  5523.                     node: node,
  5524.                     action: action,
  5525.                     section: section
  5526.                 });
  5527.             }
  5528.         },
  5529.  
  5530.         /**
  5531.          * @ngdoc method
  5532.          * @name umbraco.services.navigationService#showUserDialog
  5533.          * @methodOf umbraco.services.navigationService
  5534.          *
  5535.          * @description
  5536.          * Opens the user dialog, next to the sections navigation
  5537.          * template is located in views/common/dialogs/user.html
  5538.          */
  5539.         showUserDialog: function () {
  5540.             // hide tray and close help dialog
  5541.             if (service.helpDialog) {
  5542.                 service.helpDialog.close();
  5543.             }
  5544.             service.hideTray();
  5545.  
  5546.             if (service.userDialog) {
  5547.                 service.userDialog.close();
  5548.                 service.userDialog = undefined;
  5549.             }
  5550.  
  5551.             service.userDialog = dialogService.open(
  5552.             {
  5553.                 template: "views/common/dialogs/user.html",
  5554.                 modalClass: "umb-modal-left",
  5555.                 show: true
  5556.             });
  5557.  
  5558.             return service.userDialog;
  5559.         },
  5560.  
  5561.         /**
  5562.          * @ngdoc method
  5563.          * @name umbraco.services.navigationService#showUserDialog
  5564.          * @methodOf umbraco.services.navigationService
  5565.          *
  5566.          * @description
  5567.          * Opens the user dialog, next to the sections navigation
  5568.          * template is located in views/common/dialogs/user.html
  5569.          */
  5570.         showHelpDialog: function () {
  5571.             // hide tray and close user dialog
  5572.             service.hideTray();
  5573.             if (service.userDialog) {
  5574.                 service.userDialog.close();
  5575.             }
  5576.  
  5577.             if(service.helpDialog){
  5578.                 service.helpDialog.close();
  5579.                 service.helpDialog = undefined;
  5580.             }
  5581.  
  5582.             service.helpDialog = dialogService.open(
  5583.             {
  5584.                 template: "views/common/dialogs/help.html",
  5585.                 modalClass: "umb-modal-left",
  5586.                 show: true
  5587.             });
  5588.  
  5589.             return service.helpDialog;
  5590.         },
  5591.  
  5592.         /**
  5593.          * @ngdoc method
  5594.          * @name umbraco.services.navigationService#showDialog
  5595.          * @methodOf umbraco.services.navigationService
  5596.          *
  5597.          * @description
  5598.          * Opens a dialog, for a given action on a given tree node
  5599.          * uses the dialogService to inject the selected action dialog
  5600.          * into #dialog div.umb-panel-body
  5601.          * the path to the dialog view is determined by:
  5602.          * "views/" + current tree + "/" + action alias + ".html"
  5603.          * The dialog controller will get passed a scope object that is created here with the properties:
  5604.          *  scope.currentNode = the selected tree node
  5605.          *  scope.currentAction = the selected menu item
  5606.          *  so that the dialog controllers can use these properties
  5607.          *
  5608.          * @param {Object} args arguments passed to the function
  5609.          * @param {Scope} args.scope current scope passed to the dialog
  5610.          * @param {Object} args.action the clicked action containing `name` and `alias`
  5611.          */
  5612.         showDialog: function(args) {
  5613.  
  5614.             if (!args) {
  5615.                 throw "showDialog is missing the args parameter";
  5616.             }
  5617.             if (!args.action) {
  5618.                 throw "The args parameter must have an 'action' property as the clicked menu action object";
  5619.             }
  5620.             if (!args.node) {
  5621.                 throw "The args parameter must have a 'node' as the active tree node";
  5622.             }
  5623.  
  5624.             //ensure the current dialog is cleared before creating another!
  5625.             if (currentDialog) {
  5626.                 dialogService.close(currentDialog);
  5627.                 currentDialog = null;
  5628.             }
  5629.  
  5630.             setMode("dialog");
  5631.  
  5632.             //NOTE: Set up the scope object and assign properties, this is legacy functionality but we have to live with it now.
  5633.             // we should be passing in currentNode and currentAction using 'dialogData' for the dialog, not attaching it to a scope.
  5634.             // This scope instance will be destroyed by the dialog so it cannot be a scope that exists outside of the dialog.
  5635.             // If a scope instance has been passed in, we'll have to create a child scope of it, otherwise a new root scope.
  5636.             var dialogScope = args.scope ? args.scope.$new() : $rootScope.$new();
  5637.             dialogScope.currentNode = args.node;
  5638.             dialogScope.currentAction = args.action;
  5639.  
  5640.             //the title might be in the meta data, check there first
  5641.             if (args.action.metaData["dialogTitle"]) {
  5642.                 appState.setMenuState("dialogTitle", args.action.metaData["dialogTitle"]);
  5643.             }
  5644.             else {
  5645.                 appState.setMenuState("dialogTitle", args.action.name);
  5646.             }
  5647.  
  5648.             var templateUrl;
  5649.             var iframe;
  5650.  
  5651.             if (args.action.metaData["actionUrl"]) {
  5652.                 templateUrl = args.action.metaData["actionUrl"];
  5653.                 iframe = true;
  5654.             }
  5655.             else if (args.action.metaData["actionView"]) {
  5656.                 templateUrl = args.action.metaData["actionView"];
  5657.                 iframe = false;
  5658.             }
  5659.             else {
  5660.  
  5661.                 //by convention we will look into the /views/{treetype}/{action}.html
  5662.                 // for example: /views/content/create.html
  5663.  
  5664.                 //we will also check for a 'packageName' for the current tree, if it exists then the convention will be:
  5665.                 // for example: /App_Plugins/{mypackage}/backoffice/{treetype}/create.html
  5666.  
  5667.                 var treeAlias = treeService.getTreeAlias(args.node);
  5668.                 var packageTreeFolder = treeService.getTreePackageFolder(treeAlias);
  5669.  
  5670.                 if (!treeAlias) {
  5671.                     throw "Could not get tree alias for node " + args.node.id;
  5672.                 }
  5673.  
  5674.                 if (packageTreeFolder) {
  5675.                     templateUrl = Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath +
  5676.                         "/" + packageTreeFolder +
  5677.                         "/backoffice/" + treeAlias + "/" + args.action.alias + ".html";
  5678.                 }
  5679.                 else {
  5680.                     templateUrl = "views/" + treeAlias + "/" + args.action.alias + ".html";
  5681.                 }
  5682.  
  5683.                 iframe = false;
  5684.             }
  5685.  
  5686.             //TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with
  5687.             // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog
  5688.             // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window,
  5689.             // though would be v-easy, just not sure we want to ever support that?
  5690.  
  5691.             var dialog = dialogService.open(
  5692.                 {
  5693.                     container: $("#dialog div.umb-modalcolumn-body"),
  5694.                     //The ONLY reason we're passing in scope to the dialogService (which is legacy functionality) is
  5695.                     // for backwards compatibility since many dialogs require $scope.currentNode or $scope.currentAction
  5696.                     // to exist
  5697.                     scope: dialogScope,
  5698.                     inline: true,
  5699.                     show: true,
  5700.                     iframe: iframe,
  5701.                     modalClass: "umb-dialog",
  5702.                     template: templateUrl,
  5703.  
  5704.                     //These will show up on the dialog controller's $scope under dialogOptions
  5705.                     currentNode: args.node,
  5706.                     currentAction: args.action,
  5707.                 });
  5708.  
  5709.             //save the currently assigned dialog so it can be removed before a new one is created
  5710.             currentDialog = dialog;
  5711.             return dialog;
  5712.         },
  5713.  
  5714.         /**
  5715.          * @ngdoc method
  5716.          * @name umbraco.services.navigationService#hideDialog
  5717.          * @methodOf umbraco.services.navigationService
  5718.          *
  5719.          * @description
  5720.          * hides the currently open dialog
  5721.          */
  5722.         hideDialog: function (showMenu) {
  5723.  
  5724.             setMode("default");
  5725.  
  5726.             if(showMenu){
  5727.                 this.showMenu(undefined, { skipDefault: true, node: appState.getMenuState("currentNode") });
  5728.             }
  5729.         },
  5730.         /**
  5731.           * @ngdoc method
  5732.           * @name umbraco.services.navigationService#showSearch
  5733.           * @methodOf umbraco.services.navigationService
  5734.           *
  5735.           * @description
  5736.           * shows the search pane
  5737.           */
  5738.         showSearch: function() {
  5739.             setMode("search");
  5740.         },
  5741.         /**
  5742.           * @ngdoc method
  5743.           * @name umbraco.services.navigationService#hideSearch
  5744.           * @methodOf umbraco.services.navigationService
  5745.           *
  5746.           * @description
  5747.           * hides the search pane
  5748.         */
  5749.         hideSearch: function() {
  5750.             setMode("default-hidesearch");
  5751.         },
  5752.         /**
  5753.           * @ngdoc method
  5754.           * @name umbraco.services.navigationService#hideNavigation
  5755.           * @methodOf umbraco.services.navigationService
  5756.           *
  5757.           * @description
  5758.           * hides any open navigation panes and resets the tree, actions and the currently selected node
  5759.           */
  5760.         hideNavigation: function() {
  5761.             appState.setMenuState("menuActions", []);
  5762.             setMode("default");
  5763.         }
  5764.     };
  5765.  
  5766.     return service;
  5767. }
  5768.  
  5769. angular.module('umbraco.services').factory('navigationService', navigationService);
  5770.  
  5771. /**
  5772.  * @ngdoc service
  5773.  * @name umbraco.services.notificationsService
  5774.  *
  5775.  * @requires $rootScope
  5776.  * @requires $timeout
  5777.  * @requires angularHelper
  5778.  * 
  5779.  * @description
  5780.  * Application-wide service for handling notifications, the umbraco application
  5781.  * maintains a single collection of notications, which the UI watches for changes.
  5782.  * By default when a notication is added, it is automaticly removed 7 seconds after
  5783.  * This can be changed on add()
  5784.  *
  5785.  * ##usage
  5786.  * To use, simply inject the notificationsService into any controller that needs it, and make
  5787.  * sure the umbraco.services module is accesible - which it should be by default.
  5788.  *
  5789.  * <pre>
  5790.  *      notificationsService.success("Document Published", "hooraaaay for you!");
  5791.  *      notificationsService.error("Document Failed", "booooh");
  5792.  * </pre>
  5793.  */
  5794. angular.module('umbraco.services')
  5795. .factory('notificationsService', function ($rootScope, $timeout, angularHelper) {
  5796.  
  5797.     var nArray = [];
  5798.     function setViewPath(view){
  5799.         if(view.indexOf('/') < 0)
  5800.         {
  5801.             view = "views/common/notifications/" + view;
  5802.         }
  5803.  
  5804.         if(view.indexOf('.html') < 0)
  5805.         {
  5806.             view = view + ".html";
  5807.         }
  5808.         return view;
  5809.     }
  5810.  
  5811.     var service = {
  5812.  
  5813.         /**
  5814.         * @ngdoc method
  5815.         * @name umbraco.services.notificationsService#add
  5816.         * @methodOf umbraco.services.notificationsService
  5817.         *
  5818.         * @description
  5819.         * Lower level api for adding notifcations, support more advanced options
  5820.         * @param {Object} item The notification item
  5821.         * @param {String} item.headline Short headline
  5822.         * @param {String} item.message longer text for the notication, trimmed after 200 characters, which can then be exanded
  5823.         * @param {String} item.type Notification type, can be: "success","warning","error" or "info"
  5824.         * @param {String} item.url url to open when notification is clicked
  5825.         * @param {String} item.view path to custom view to load into the notification box
  5826.         * @param {Array} item.actions Collection of button actions to append (label, func, cssClass)
  5827.         * @param {Boolean} item.sticky if set to true, the notification will not auto-close
  5828.         * @returns {Object} args notification object
  5829.         */
  5830.  
  5831.         add: function(item) {
  5832.             angularHelper.safeApply($rootScope, function () {
  5833.  
  5834.                 if(item.view){
  5835.                     item.view = setViewPath(item.view);
  5836.                     item.sticky = true;
  5837.                     item.type = "form";
  5838.                     item.headline = null;
  5839.                 }
  5840.  
  5841.  
  5842.                 //add a colon after the headline if there is a message as well
  5843.                 if (item.message) {
  5844.                     item.headline += ": ";
  5845.                     if(item.message.length > 200) {
  5846.                         item.sticky = true;
  5847.                     }
  5848.                 }  
  5849.            
  5850.                 //we need to ID the item, going by index isn't good enough because people can remove at different indexes
  5851.                 // whenever they want. Plus once we remove one, then the next index will be different. The only way to
  5852.                 // effectively remove an item is by an Id.
  5853.                 item.id = String.CreateGuid();
  5854.  
  5855.                 nArray.push(item);
  5856.  
  5857.                 if(!item.sticky) {
  5858.                     $timeout(function() {
  5859.                         var found = _.find(nArray, function(i) {
  5860.                         return i.id === item.id;
  5861.                     });
  5862.  
  5863.                     if (found) {
  5864.                         var index = nArray.indexOf(found);
  5865.                         nArray.splice(index, 1);
  5866.                     }
  5867.  
  5868.                     }, 7000);
  5869.                 }
  5870.  
  5871.                 return item;
  5872.             });
  5873.  
  5874.         },
  5875.  
  5876.         hasView : function(view){
  5877.             if(!view){
  5878.                 return _.find(nArray, function(notification){ return notification.view;});
  5879.             }else{
  5880.                 view = setViewPath(view).toLowerCase();
  5881.                 return _.find(nArray, function(notification){ return notification.view.toLowerCase() === view;});
  5882.             }  
  5883.         },
  5884.         addView: function(view, args){
  5885.             var item = {
  5886.                 args: args,
  5887.                 view: view
  5888.             };
  5889.  
  5890.             service.add(item);
  5891.         },
  5892.  
  5893.         /**
  5894.          * @ngdoc method
  5895.          * @name umbraco.services.notificationsService#showNotification
  5896.          * @methodOf umbraco.services.notificationsService
  5897.          *
  5898.          * @description
  5899.          * Shows a notification based on the object passed in, normally used to render notifications sent back from the server
  5900.          *       
  5901.          * @returns {Object} args notification object
  5902.          */
  5903.         showNotification: function(args) {
  5904.             if (!args) {
  5905.                 throw "args cannot be null";
  5906.             }
  5907.             if (args.type === undefined || args.type === null) {
  5908.                 throw "args.type cannot be null";
  5909.             }
  5910.             if (!args.header) {
  5911.                 throw "args.header cannot be null";
  5912.             }
  5913.            
  5914.             switch(args.type) {
  5915.                 case 0:
  5916.                     //save
  5917.                     this.success(args.header, args.message);
  5918.                     break;
  5919.                 case 1:
  5920.                     //info
  5921.                     this.success(args.header, args.message);
  5922.                     break;
  5923.                 case 2:
  5924.                     //error
  5925.                     this.error(args.header, args.message);
  5926.                     break;
  5927.                 case 3:
  5928.                     //success
  5929.                     this.success(args.header, args.message);
  5930.                     break;
  5931.                 case 4:
  5932.                     //warning
  5933.                     this.warning(args.header, args.message);
  5934.                     break;
  5935.             }
  5936.         },
  5937.  
  5938.         /**
  5939.          * @ngdoc method
  5940.          * @name umbraco.services.notificationsService#success
  5941.          * @methodOf umbraco.services.notificationsService
  5942.          *
  5943.          * @description
  5944.          * Adds a green success notication to the notications collection
  5945.          * This should be used when an operations *completes* without errors
  5946.          *
  5947.          * @param {String} headline Headline of the notification
  5948.          * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded
  5949.          * @returns {Object} notification object
  5950.          */
  5951.         success: function (headline, message) {
  5952.             return service.add({ headline: headline, message: message, type: 'success', time: new Date() });
  5953.         },
  5954.         /**
  5955.          * @ngdoc method
  5956.          * @name umbraco.services.notificationsService#error
  5957.          * @methodOf umbraco.services.notificationsService
  5958.          *
  5959.          * @description
  5960.          * Adds a red error notication to the notications collection
  5961.          * This should be used when an operations *fails* and could not complete
  5962.          *
  5963.          * @param {String} headline Headline of the notification
  5964.          * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded
  5965.          * @returns {Object} notification object
  5966.          */
  5967.         error: function (headline, message) {
  5968.             return service.add({ headline: headline, message: message, type: 'error', time: new Date() });
  5969.         },
  5970.  
  5971.         /**
  5972.          * @ngdoc method
  5973.          * @name umbraco.services.notificationsService#warning
  5974.          * @methodOf umbraco.services.notificationsService
  5975.          *
  5976.          * @description
  5977.          * Adds a yellow warning notication to the notications collection
  5978.          * This should be used when an operations *completes* but something was not as expected
  5979.          *
  5980.          *
  5981.          * @param {String} headline Headline of the notification
  5982.          * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded
  5983.          * @returns {Object} notification object
  5984.          */
  5985.         warning: function (headline, message) {
  5986.             return service.add({ headline: headline, message: message, type: 'warning', time: new Date() });
  5987.         },
  5988.  
  5989.         /**
  5990.          * @ngdoc method
  5991.          * @name umbraco.services.notificationsService#warning
  5992.          * @methodOf umbraco.services.notificationsService
  5993.          *
  5994.          * @description
  5995.          * Adds a yellow warning notication to the notications collection
  5996.          * This should be used when an operations *completes* but something was not as expected
  5997.          *
  5998.          *
  5999.          * @param {String} headline Headline of the notification
  6000.          * @param {String} message longer text for the notication, trimmed after 200 characters, which can then be exanded
  6001.          * @returns {Object} notification object
  6002.          */
  6003.         info: function (headline, message) {
  6004.             return service.add({ headline: headline, message: message, type: 'info', time: new Date() });
  6005.         },
  6006.  
  6007.         /**
  6008.          * @ngdoc method
  6009.          * @name umbraco.services.notificationsService#remove
  6010.          * @methodOf umbraco.services.notificationsService
  6011.          *
  6012.          * @description
  6013.          * Removes a notification from the notifcations collection at a given index
  6014.          *
  6015.          * @param {Int} index index where the notication should be removed from
  6016.          */
  6017.         remove: function (index) {
  6018.             if(angular.isObject(index)){
  6019.                 var i = nArray.indexOf(index);
  6020.                 angularHelper.safeApply($rootScope, function() {
  6021.                     nArray.splice(i, 1);
  6022.                 });
  6023.             }else{
  6024.                 angularHelper.safeApply($rootScope, function() {
  6025.                     nArray.splice(index, 1);
  6026.                 });
  6027.             }
  6028.         },
  6029.  
  6030.         /**
  6031.          * @ngdoc method
  6032.          * @name umbraco.services.notificationsService#removeAll
  6033.          * @methodOf umbraco.services.notificationsService
  6034.          *
  6035.          * @description
  6036.          * Removes all notifications from the notifcations collection
  6037.          */
  6038.         removeAll: function () {
  6039.             angularHelper.safeApply($rootScope, function() {
  6040.                 nArray = [];
  6041.             });
  6042.         },
  6043.  
  6044.         /**
  6045.          * @ngdoc property
  6046.          * @name umbraco.services.notificationsService#current
  6047.          * @propertyOf umbraco.services.notificationsService
  6048.          *
  6049.          * @description
  6050.          * Returns an array of current notifications to display
  6051.          *
  6052.          * @returns {string} returns an array
  6053.          */
  6054.         current: nArray,
  6055.  
  6056.         /**
  6057.          * @ngdoc method
  6058.          * @name umbraco.services.notificationsService#getCurrent
  6059.          * @methodOf umbraco.services.notificationsService
  6060.          *
  6061.          * @description
  6062.          * Method to return all notifications from the notifcations collection
  6063.          */
  6064.         getCurrent: function(){
  6065.             return nArray;
  6066.         }
  6067.     };
  6068.  
  6069.     return service;
  6070. });
  6071. (function() {
  6072.    'use strict';
  6073.  
  6074.    function overlayHelper() {
  6075.  
  6076.       var numberOfOverlays = 0;
  6077.  
  6078.       function registerOverlay() {
  6079.          numberOfOverlays++;
  6080.          return numberOfOverlays;
  6081.       }
  6082.  
  6083.       function unregisterOverlay() {
  6084.          numberOfOverlays--;
  6085.          return numberOfOverlays;
  6086.       }
  6087.  
  6088.       function getNumberOfOverlays() {
  6089.          return numberOfOverlays;
  6090.       }
  6091.  
  6092.       var service = {
  6093.          numberOfOverlays: numberOfOverlays,
  6094.          registerOverlay: registerOverlay,
  6095.          unregisterOverlay: unregisterOverlay,
  6096.          getNumberOfOverlays: getNumberOfOverlays
  6097.       };
  6098.  
  6099.       return service;
  6100.  
  6101.    }
  6102.  
  6103.  
  6104.    angular.module('umbraco.services').factory('overlayHelper', overlayHelper);
  6105.  
  6106.  
  6107. })();
  6108.  
  6109. /**
  6110.  * @ngdoc service
  6111.  * @name umbraco.services.searchService
  6112.  *
  6113.  *  
  6114.  * @description
  6115.  * Service for handling the main application search, can currently search content, media and members
  6116.  *
  6117.  * ##usage
  6118.  * To use, simply inject the searchService into any controller that needs it, and make
  6119.  * sure the umbraco.services module is accesible - which it should be by default.
  6120.  *
  6121.  * <pre>
  6122.  *      searchService.searchMembers({term: 'bob'}).then(function(results){
  6123.  *          angular.forEach(results, function(result){
  6124.  *                  //returns:
  6125.  *                  {name: "name", id: 1234, menuUrl: "url", editorPath: "url", metaData: {}, subtitle: "/path/etc" }
  6126.  *           })          
  6127.  *           var result =
  6128.  *       })
  6129.  * </pre>
  6130.  */
  6131. angular.module('umbraco.services')
  6132. .factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper) {
  6133.  
  6134.     function configureMemberResult(member) {
  6135.         member.menuUrl = umbRequestHelper.getApiUrl("memberTreeBaseUrl", "GetMenu", [{ id: member.id }, { application: 'member' }]);
  6136.         member.editorPath = "member/member/edit/" + (member.key ? member.key : member.id);
  6137.         angular.extend(member.metaData, { treeAlias: "member" });
  6138.         member.subTitle = member.metaData.Email;
  6139.     }
  6140.    
  6141.     function configureMediaResult(media)
  6142.     {
  6143.         media.menuUrl = umbRequestHelper.getApiUrl("mediaTreeBaseUrl", "GetMenu", [{ id: media.id }, { application: 'media' }]);
  6144.         media.editorPath = "media/media/edit/" + media.id;
  6145.         angular.extend(media.metaData, { treeAlias: "media" });
  6146.     }
  6147.    
  6148.     function configureContentResult(content) {
  6149.         content.menuUrl = umbRequestHelper.getApiUrl("contentTreeBaseUrl", "GetMenu", [{ id: content.id }, { application: 'content' }]);
  6150.         content.editorPath = "content/content/edit/" + content.id;
  6151.         angular.extend(content.metaData, { treeAlias: "content" });
  6152.         content.subTitle = content.metaData.Url;        
  6153.     }
  6154.  
  6155.     return {
  6156.  
  6157.         /**
  6158.         * @ngdoc method
  6159.         * @name umbraco.services.searchService#searchMembers
  6160.         * @methodOf umbraco.services.searchService
  6161.         *
  6162.         * @description
  6163.         * Searches the default member search index
  6164.         * @param {Object} args argument object
  6165.         * @param {String} args.term seach term
  6166.         * @returns {Promise} returns promise containing all matching members
  6167.         */
  6168.         searchMembers: function(args) {
  6169.  
  6170.             if (!args.term) {
  6171.                 throw "args.term is required";
  6172.             }
  6173.  
  6174.             return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) {
  6175.                 _.each(data, function(item) {
  6176.                     configureMemberResult(item);
  6177.                 });        
  6178.                 return data;
  6179.             });
  6180.         },
  6181.  
  6182.         /**
  6183.         * @ngdoc method
  6184.         * @name umbraco.services.searchService#searchContent
  6185.         * @methodOf umbraco.services.searchService
  6186.         *
  6187.         * @description
  6188.         * Searches the default internal content search index
  6189.         * @param {Object} args argument object
  6190.         * @param {String} args.term seach term
  6191.         * @returns {Promise} returns promise containing all matching content items
  6192.         */
  6193.         searchContent: function(args) {
  6194.  
  6195.             if (!args.term) {
  6196.                 throw "args.term is required";
  6197.             }
  6198.  
  6199.             return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) {
  6200.                 _.each(data, function (item) {
  6201.                     configureContentResult(item);
  6202.                 });
  6203.                 return data;
  6204.             });
  6205.         },
  6206.  
  6207.         /**
  6208.         * @ngdoc method
  6209.         * @name umbraco.services.searchService#searchMedia
  6210.         * @methodOf umbraco.services.searchService
  6211.         *
  6212.         * @description
  6213.         * Searches the default media search index
  6214.         * @param {Object} args argument object
  6215.         * @param {String} args.term seach term
  6216.         * @returns {Promise} returns promise containing all matching media items
  6217.         */
  6218.         searchMedia: function(args) {
  6219.  
  6220.             if (!args.term) {
  6221.                 throw "args.term is required";
  6222.             }
  6223.  
  6224.             return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) {
  6225.                 _.each(data, function (item) {
  6226.                     configureMediaResult(item);
  6227.                 });
  6228.                 return data;
  6229.             });
  6230.         },
  6231.  
  6232.         /**
  6233.         * @ngdoc method
  6234.         * @name umbraco.services.searchService#searchAll
  6235.         * @methodOf umbraco.services.searchService
  6236.         *
  6237.         * @description
  6238.         * Searches all available indexes and returns all results in one collection
  6239.         * @param {Object} args argument object
  6240.         * @param {String} args.term seach term
  6241.         * @returns {Promise} returns promise containing all matching items
  6242.         */
  6243.         searchAll: function (args) {
  6244.            
  6245.             if (!args.term) {
  6246.                 throw "args.term is required";
  6247.             }
  6248.  
  6249.             return entityResource.searchAll(args.term, args.canceler).then(function (data) {
  6250.  
  6251.                 _.each(data, function(resultByType) {
  6252.                     switch(resultByType.type) {
  6253.                         case "Document":
  6254.                             _.each(resultByType.results, function (item) {
  6255.                                 configureContentResult(item);
  6256.                             });
  6257.                             break;
  6258.                         case "Media":
  6259.                             _.each(resultByType.results, function (item) {
  6260.                                 configureMediaResult(item);
  6261.                             });                            
  6262.                             break;
  6263.                         case "Member":
  6264.                             _.each(resultByType.results, function (item) {
  6265.                                 configureMemberResult(item);
  6266.                             });                            
  6267.                             break;
  6268.                     }
  6269.                 });
  6270.  
  6271.                 return data;
  6272.             });
  6273.            
  6274.         },
  6275.  
  6276.         //TODO: This doesn't do anything!
  6277.         setCurrent: function(sectionAlias) {
  6278.  
  6279.             var currentSection = sectionAlias;
  6280.         }
  6281.     };
  6282. });
  6283. /**
  6284.  * @ngdoc service
  6285.  * @name umbraco.services.serverValidationManager
  6286.  * @function
  6287.  *
  6288.  * @description
  6289.  * Used to handle server side validation and wires up the UI with the messages. There are 2 types of validation messages, one
  6290.  * is for user defined properties (called Properties) and the other is for field properties which are attached to the native
  6291.  * model objects (not user defined). The methods below are named according to these rules: Properties vs Fields.
  6292.  */
  6293. function serverValidationManager($timeout) {
  6294.  
  6295.     var callbacks = [];
  6296.    
  6297.     /** calls the callback specified with the errors specified, used internally */
  6298.     function executeCallback(self, errorsForCallback, callback) {
  6299.  
  6300.         callback.apply(self, [
  6301.                  false,                  //pass in a value indicating it is invalid
  6302.                  errorsForCallback,      //pass in the errors for this item
  6303.                  self.items]);           //pass in all errors in total
  6304.     }
  6305.  
  6306.     function getFieldErrors(self, fieldName) {
  6307.         if (!angular.isString(fieldName)) {
  6308.             throw "fieldName must be a string";
  6309.         }
  6310.  
  6311.         //find errors for this field name
  6312.         return _.filter(self.items, function (item) {
  6313.             return (item.propertyAlias === null && item.fieldName === fieldName);
  6314.         });
  6315.     }
  6316.    
  6317.     function getPropertyErrors(self, propertyAlias, fieldName) {
  6318.         if (!angular.isString(propertyAlias)) {
  6319.             throw "propertyAlias must be a string";
  6320.         }
  6321.         if (fieldName && !angular.isString(fieldName)) {
  6322.             throw "fieldName must be a string";
  6323.         }
  6324.  
  6325.         //find all errors for this property
  6326.         return _.filter(self.items, function (item) {            
  6327.             return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
  6328.         });
  6329.     }
  6330.  
  6331.     return {
  6332.        
  6333.         /**
  6334.          * @ngdoc function
  6335.          * @name umbraco.services.serverValidationManager#subscribe
  6336.          * @methodOf umbraco.services.serverValidationManager
  6337.          * @function
  6338.          *
  6339.          * @description
  6340.          *  This method needs to be called once all field and property errors are wired up.
  6341.          *
  6342.          *  In some scenarios where the error collection needs to be persisted over a route change
  6343.          *   (i.e. when a content item (or any item) is created and the route redirects to the editor)
  6344.          *   the controller should call this method once the data is bound to the scope
  6345.          *   so that any persisted validation errors are re-bound to their controls. Once they are re-binded this then clears the validation
  6346.          *   colleciton so that if another route change occurs, the previously persisted validation errors are not re-bound to the new item.
  6347.          */
  6348.         executeAndClearAllSubscriptions: function() {
  6349.  
  6350.             var self = this;
  6351.  
  6352.             $timeout(function () {
  6353.                
  6354.                 for (var cb in callbacks) {
  6355.                     if (callbacks[cb].propertyAlias === null) {
  6356.                         //its a field error callback
  6357.                         var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName);
  6358.                         if (fieldErrors.length > 0) {
  6359.                             executeCallback(self, fieldErrors, callbacks[cb].callback);
  6360.                         }
  6361.                     }
  6362.                     else {
  6363.                         //its a property error
  6364.                         var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].fieldName);
  6365.                         if (propErrors.length > 0) {
  6366.                             executeCallback(self, propErrors, callbacks[cb].callback);
  6367.                         }
  6368.                     }
  6369.                 }
  6370.                 //now that they are all executed, we're gonna clear all of the errors we have
  6371.                 self.clear();
  6372.             });
  6373.         },
  6374.  
  6375.         /**
  6376.          * @ngdoc function
  6377.          * @name umbraco.services.serverValidationManager#subscribe
  6378.          * @methodOf umbraco.services.serverValidationManager
  6379.          * @function
  6380.          *
  6381.          * @description
  6382.          *  Adds a callback method that is executed whenever validation changes for the field name + property specified.
  6383.          *  This is generally used for server side validation in order to match up a server side validation error with
  6384.          *  a particular field, otherwise we can only pinpoint that there is an error for a content property, not the
  6385.          *  property's specific field. This is used with the val-server directive in which the directive specifies the
  6386.          *  field alias to listen for.
  6387.          *  If propertyAlias is null, then this subscription is for a field property (not a user defined property).
  6388.          */
  6389.         subscribe: function (propertyAlias, fieldName, callback) {
  6390.             if (!callback) {
  6391.                 return;
  6392.             }
  6393.            
  6394.             if (propertyAlias === null) {
  6395.                 //don't add it if it already exists
  6396.                 var exists1 = _.find(callbacks, function (item) {
  6397.                     return item.propertyAlias === null && item.fieldName === fieldName;
  6398.                 });
  6399.                 if (!exists1) {
  6400.                     callbacks.push({ propertyAlias: null, fieldName: fieldName, callback: callback });
  6401.                 }
  6402.             }
  6403.             else if (propertyAlias !== undefined) {
  6404.                 //don't add it if it already exists
  6405.                 var exists2 = _.find(callbacks, function (item) {
  6406.                     return item.propertyAlias === propertyAlias && item.fieldName === fieldName;
  6407.                 });
  6408.                 if (!exists2) {
  6409.                     callbacks.push({ propertyAlias: propertyAlias, fieldName: fieldName, callback: callback });
  6410.                 }
  6411.             }
  6412.         },
  6413.        
  6414.         unsubscribe: function (propertyAlias, fieldName) {
  6415.            
  6416.             if (propertyAlias === null) {
  6417.  
  6418.                 //remove all callbacks for the content field
  6419.                 callbacks = _.reject(callbacks, function (item) {
  6420.                     return item.propertyAlias === null && item.fieldName === fieldName;
  6421.                 });
  6422.  
  6423.             }
  6424.             else if (propertyAlias !== undefined) {
  6425.                
  6426.                 //remove all callbacks for the content property
  6427.                 callbacks = _.reject(callbacks, function (item) {
  6428.                     return item.propertyAlias === propertyAlias &&
  6429.                     (item.fieldName === fieldName ||
  6430.                         ((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === "")));
  6431.                 });
  6432.             }
  6433.  
  6434.            
  6435.         },
  6436.        
  6437.        
  6438.         /**
  6439.          * @ngdoc function
  6440.          * @name getPropertyCallbacks
  6441.          * @methodOf umbraco.services.serverValidationManager
  6442.          * @function
  6443.          *
  6444.          * @description
  6445.          * Gets all callbacks that has been registered using the subscribe method for the propertyAlias + fieldName combo.
  6446.          * This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an
  6447.          * explicit field name set.
  6448.          */
  6449.         getPropertyCallbacks: function (propertyAlias, fieldName) {
  6450.             var found = _.filter(callbacks, function (item) {
  6451.                 //returns any callback that have been registered directly against the field and for only the property
  6452.                 return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === "")));
  6453.             });
  6454.             return found;
  6455.         },
  6456.        
  6457.         /**
  6458.          * @ngdoc function
  6459.          * @name getFieldCallbacks
  6460.          * @methodOf umbraco.services.serverValidationManager
  6461.          * @function
  6462.          *
  6463.          * @description
  6464.          * Gets all callbacks that has been registered using the subscribe method for the field.        
  6465.          */
  6466.         getFieldCallbacks: function (fieldName) {
  6467.             var found = _.filter(callbacks, function (item) {
  6468.                 //returns any callback that have been registered directly against the field
  6469.                 return (item.propertyAlias === null && item.fieldName === fieldName);
  6470.             });
  6471.             return found;
  6472.         },
  6473.        
  6474.         /**
  6475.          * @ngdoc function
  6476.          * @name addFieldError
  6477.          * @methodOf umbraco.services.serverValidationManager
  6478.          * @function
  6479.          *
  6480.          * @description
  6481.          * Adds an error message for a native content item field (not a user defined property, for Example, 'Name')
  6482.          */
  6483.         addFieldError: function(fieldName, errorMsg) {
  6484.             if (!fieldName) {
  6485.                 return;
  6486.             }
  6487.            
  6488.             //only add the item if it doesn't exist                
  6489.             if (!this.hasFieldError(fieldName)) {
  6490.                 this.items.push({
  6491.                     propertyAlias: null,
  6492.                     fieldName: fieldName,
  6493.                     errorMsg: errorMsg
  6494.                 });
  6495.             }
  6496.            
  6497.             //find all errors for this item
  6498.             var errorsForCallback = getFieldErrors(this, fieldName);
  6499.             //we should now call all of the call backs registered for this error
  6500.             var cbs = this.getFieldCallbacks(fieldName);
  6501.             //call each callback for this error
  6502.             for (var cb in cbs) {
  6503.                 executeCallback(this, errorsForCallback, cbs[cb].callback);
  6504.             }
  6505.         },
  6506.  
  6507.         /**
  6508.          * @ngdoc function
  6509.          * @name addPropertyError
  6510.          * @methodOf umbraco.services.serverValidationManager
  6511.          * @function
  6512.          *
  6513.          * @description
  6514.          * Adds an error message for the content property
  6515.          */
  6516.         addPropertyError: function (propertyAlias, fieldName, errorMsg) {
  6517.             if (!propertyAlias) {
  6518.                 return;
  6519.             }
  6520.            
  6521.             //only add the item if it doesn't exist                
  6522.             if (!this.hasPropertyError(propertyAlias, fieldName)) {
  6523.                 this.items.push({
  6524.                     propertyAlias: propertyAlias,
  6525.                     fieldName: fieldName,
  6526.                     errorMsg: errorMsg
  6527.                 });
  6528.             }
  6529.            
  6530.             //find all errors for this item
  6531.             var errorsForCallback = getPropertyErrors(this, propertyAlias, fieldName);
  6532.             //we should now call all of the call backs registered for this error
  6533.             var cbs = this.getPropertyCallbacks(propertyAlias, fieldName);
  6534.             //call each callback for this error
  6535.             for (var cb in cbs) {
  6536.                 executeCallback(this, errorsForCallback, cbs[cb].callback);
  6537.             }
  6538.         },        
  6539.        
  6540.         /**
  6541.          * @ngdoc function
  6542.          * @name removePropertyError
  6543.          * @methodOf umbraco.services.serverValidationManager
  6544.          * @function
  6545.          *
  6546.          * @description
  6547.          * Removes an error message for the content property
  6548.          */
  6549.         removePropertyError: function (propertyAlias, fieldName) {
  6550.  
  6551.             if (!propertyAlias) {
  6552.                 return;
  6553.             }
  6554.             //remove the item
  6555.             this.items = _.reject(this.items, function (item) {
  6556.                 return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
  6557.             });
  6558.         },
  6559.        
  6560.         /**
  6561.          * @ngdoc function
  6562.          * @name reset
  6563.          * @methodOf umbraco.services.serverValidationManager
  6564.          * @function
  6565.          *
  6566.          * @description
  6567.          * Clears all errors and notifies all callbacks that all server errros are now valid - used when submitting a form
  6568.          */
  6569.         reset: function () {
  6570.             this.clear();
  6571.             for (var cb in callbacks) {
  6572.                 callbacks[cb].callback.apply(this, [
  6573.                         true,       //pass in a value indicating it is VALID
  6574.                         [],         //pass in empty collection
  6575.                         []]);       //pass in empty collection
  6576.             }
  6577.         },
  6578.        
  6579.         /**
  6580.          * @ngdoc function
  6581.          * @name clear
  6582.          * @methodOf umbraco.services.serverValidationManager
  6583.          * @function
  6584.          *
  6585.          * @description
  6586.          * Clears all errors
  6587.          */
  6588.         clear: function() {
  6589.             this.items = [];
  6590.         },
  6591.        
  6592.         /**
  6593.          * @ngdoc function
  6594.          * @name getPropertyError
  6595.          * @methodOf umbraco.services.serverValidationManager
  6596.          * @function
  6597.          *
  6598.          * @description
  6599.          * Gets the error message for the content property
  6600.          */
  6601.         getPropertyError: function (propertyAlias, fieldName) {
  6602.             var err = _.find(this.items, function (item) {
  6603.                 //return true if the property alias matches and if an empty field name is specified or the field name matches
  6604.                 return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
  6605.             });
  6606.             return err;
  6607.         },
  6608.        
  6609.         /**
  6610.          * @ngdoc function
  6611.          * @name getFieldError
  6612.          * @methodOf umbraco.services.serverValidationManager
  6613.          * @function
  6614.          *
  6615.          * @description
  6616.          * Gets the error message for a content field
  6617.          */
  6618.         getFieldError: function (fieldName) {
  6619.             var err = _.find(this.items, function (item) {
  6620.                 //return true if the property alias matches and if an empty field name is specified or the field name matches
  6621.                 return (item.propertyAlias === null && item.fieldName === fieldName);
  6622.             });
  6623.             return err;
  6624.         },
  6625.        
  6626.         /**
  6627.          * @ngdoc function
  6628.          * @name hasPropertyError
  6629.          * @methodOf umbraco.services.serverValidationManager
  6630.          * @function
  6631.          *
  6632.          * @description
  6633.          * Checks if the content property + field name combo has an error
  6634.          */
  6635.         hasPropertyError: function (propertyAlias, fieldName) {
  6636.             var err = _.find(this.items, function (item) {
  6637.                 //return true if the property alias matches and if an empty field name is specified or the field name matches
  6638.                 return (item.propertyAlias === propertyAlias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
  6639.             });
  6640.             return err ? true : false;
  6641.         },
  6642.        
  6643.         /**
  6644.          * @ngdoc function
  6645.          * @name hasFieldError
  6646.          * @methodOf umbraco.services.serverValidationManager
  6647.          * @function
  6648.          *
  6649.          * @description
  6650.          * Checks if a content field has an error
  6651.          */
  6652.         hasFieldError: function (fieldName) {
  6653.             var err = _.find(this.items, function (item) {
  6654.                 //return true if the property alias matches and if an empty field name is specified or the field name matches
  6655.                 return (item.propertyAlias === null && item.fieldName === fieldName);
  6656.             });
  6657.             return err ? true : false;
  6658.         },
  6659.        
  6660.         /** The array of error messages */
  6661.         items: []
  6662.     };
  6663. }
  6664.  
  6665. angular.module('umbraco.services').factory('serverValidationManager', serverValidationManager);
  6666. /**
  6667.  * @ngdoc service
  6668.  * @name umbraco.services.tinyMceService
  6669.  *
  6670.  *  
  6671.  * @description
  6672.  * A service containing all logic for all of the Umbraco TinyMCE plugins
  6673.  */
  6674. function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService) {
  6675.     return {
  6676.  
  6677.         /**
  6678.         * @ngdoc method
  6679.         * @name umbraco.services.tinyMceService#configuration
  6680.         * @methodOf umbraco.services.tinyMceService
  6681.         *
  6682.         * @description
  6683.         * Returns a collection of plugins available to the tinyMCE editor
  6684.         *
  6685.         */
  6686.         configuration: function () {
  6687.                return umbRequestHelper.resourcePromise(
  6688.                   $http.get(
  6689.                       umbRequestHelper.getApiUrl(
  6690.                           "rteApiBaseUrl",
  6691.                           "GetConfiguration"), { cache: true }),
  6692.                   'Failed to retrieve tinymce configuration');
  6693.         },
  6694.  
  6695.         /**
  6696.         * @ngdoc method
  6697.         * @name umbraco.services.tinyMceService#defaultPrevalues
  6698.         * @methodOf umbraco.services.tinyMceService
  6699.         *
  6700.         * @description
  6701.         * Returns a default configration to fallback on in case none is provided
  6702.         *
  6703.         */
  6704.         defaultPrevalues: function () {
  6705.                var cfg = {};
  6706.                        cfg.toolbar = ["code", "bold", "italic", "styleselect","alignleft", "aligncenter", "alignright", "bullist","numlist", "outdent", "indent", "link", "image", "umbmediapicker", "umbembeddialog", "umbmacro"];
  6707.                        cfg.stylesheets = [];
  6708.                        cfg.dimensions = { height: 500 };
  6709.                        cfg.maxImageSize = 500;
  6710.                 return cfg;
  6711.         },
  6712.  
  6713.         /**
  6714.         * @ngdoc method
  6715.         * @name umbraco.services.tinyMceService#createInsertEmbeddedMedia
  6716.         * @methodOf umbraco.services.tinyMceService
  6717.         *
  6718.         * @description
  6719.         * Creates the umbrco insert embedded media tinymce plugin
  6720.         *
  6721.         * @param {Object} editor the TinyMCE editor instance        
  6722.         * @param {Object} $scope the current controller scope
  6723.         */
  6724.         createInsertEmbeddedMedia: function (editor, scope, callback) {
  6725.             editor.addButton('umbembeddialog', {
  6726.                 icon: 'custom icon-tv',
  6727.                 tooltip: 'Embed',
  6728.                 onclick: function () {
  6729.                     if (callback) {
  6730.                         callback();
  6731.                     }
  6732.                 }
  6733.             });
  6734.         },
  6735.  
  6736.         insertEmbeddedMediaInEditor: function(editor, preview) {
  6737.             editor.insertContent(preview);
  6738.         },
  6739.  
  6740.         /**
  6741.         * @ngdoc method
  6742.         * @name umbraco.services.tinyMceService#createMediaPicker
  6743.         * @methodOf umbraco.services.tinyMceService
  6744.         *
  6745.         * @description
  6746.         * Creates the umbrco insert media tinymce plugin
  6747.         *
  6748.         * @param {Object} editor the TinyMCE editor instance        
  6749.         * @param {Object} $scope the current controller scope
  6750.         */
  6751.         createMediaPicker: function (editor, scope, callback) {
  6752.             editor.addButton('umbmediapicker', {
  6753.                 icon: 'custom icon-picture',
  6754.                 tooltip: 'Media Picker',
  6755.                 onclick: function () {
  6756.  
  6757.                     var selectedElm = editor.selection.getNode(),
  6758.                         currentTarget;
  6759.  
  6760.  
  6761.                     if(selectedElm.nodeName === 'IMG'){
  6762.                         var img = $(selectedElm);
  6763.                         currentTarget = {
  6764.                             altText: img.attr("alt"),
  6765.                             url: img.attr("src"),
  6766.                             id: img.attr("rel")
  6767.                         };
  6768.                     }
  6769.  
  6770.                     userService.getCurrentUser().then(function(userData) {
  6771.                         if(callback) {
  6772.                             callback(currentTarget, userData);
  6773.                         }
  6774.                     });
  6775.  
  6776.                 }
  6777.             });
  6778.         },
  6779.  
  6780.         insertMediaInEditor: function(editor, img) {
  6781.             if(img) {
  6782.  
  6783.                var data = {
  6784.                    alt: img.altText || "",
  6785.                    src: (img.url) ? img.url : "nothing.jpg",
  6786.                    rel: img.id,
  6787.                    'data-id': img.id,
  6788.                    id: '__mcenew'
  6789.                };
  6790.  
  6791.                editor.insertContent(editor.dom.createHTML('img', data));
  6792.  
  6793.                $timeout(function () {
  6794.                    var imgElm = editor.dom.get('__mcenew');
  6795.                    var size = editor.dom.getSize(imgElm);
  6796.  
  6797.                    if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) {
  6798.                         var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h);
  6799.  
  6800.                         var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;";
  6801.                         editor.dom.setAttrib(imgElm, 'style', s);
  6802.                         editor.dom.setAttrib(imgElm, 'id', null);
  6803.  
  6804.                         if (img.url) {
  6805.                             var src = img.url + "?width=" + newSize.width + "&height=" + newSize.height;
  6806.                             editor.dom.setAttrib(imgElm, 'data-mce-src', src);
  6807.                         }
  6808.                    }
  6809.                }, 500);
  6810.             }
  6811.         },
  6812.  
  6813.         /**
  6814.         * @ngdoc method
  6815.         * @name umbraco.services.tinyMceService#createUmbracoMacro
  6816.         * @methodOf umbraco.services.tinyMceService
  6817.         *
  6818.         * @description
  6819.         * Creates the insert umbrco macro tinymce plugin
  6820.         *
  6821.         * @param {Object} editor the TinyMCE editor instance      
  6822.         * @param {Object} $scope the current controller scope
  6823.         */
  6824.         createInsertMacro: function (editor, $scope, callback) {
  6825.  
  6826.             var createInsertMacroScope = this;
  6827.  
  6828.             /** Adds custom rules for the macro plugin and custom serialization */
  6829.             editor.on('preInit', function (args) {
  6830.                 //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
  6831.                 editor.serializer.addRules('div');
  6832.  
  6833.                 /** 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) */
  6834.                 editor.serializer.addNodeFilter('div', function (nodes, name) {
  6835.                     for (var i = 0; i < nodes.length; i++) {
  6836.                         if (nodes[i].attr("class") === "umb-macro-holder" && nodes[i].parent && nodes[i].parent.name.toUpperCase() === "P") {
  6837.                             nodes[i].parent.unwrap();
  6838.                         }
  6839.                     }
  6840.                 });
  6841.            
  6842.             });
  6843.  
  6844.             /**
  6845.             * Because the macro gets wrapped in a P tag because of the way 'enter' works, this
  6846.             * method will return the macro element if not wrapped in a p, or the p if the macro
  6847.             * element is the only one inside of it even if we are deep inside an element inside the macro
  6848.             */
  6849.             function getRealMacroElem(element) {
  6850.                 var e = $(element).closest(".umb-macro-holder");
  6851.                 if (e.length > 0) {
  6852.                     if (e.get(0).parentNode.nodeName === "P") {
  6853.                         //now check if we're the only element                    
  6854.                         if (element.parentNode.childNodes.length === 1) {
  6855.                             return e.get(0).parentNode;
  6856.                         }
  6857.                     }
  6858.                     return e.get(0);
  6859.                 }
  6860.                 return null;
  6861.             }
  6862.  
  6863.             /** Adds the button instance */
  6864.             editor.addButton('umbmacro', {
  6865.                 icon: 'custom icon-settings-alt',
  6866.                 tooltip: 'Insert macro',
  6867.                 onPostRender: function () {
  6868.  
  6869.                     var ctrl = this;
  6870.                     var isOnMacroElement = false;
  6871.  
  6872.                     /**
  6873.                      if the selection comes from a different element that is not the macro's
  6874.                      we need to check if the selection includes part of the macro, if so we'll force the selection
  6875.                      to clear to the next element since if people can select part of the macro markup they can then modify it.
  6876.                     */
  6877.                     function handleSelectionChange() {
  6878.  
  6879.                         if (!editor.selection.isCollapsed()) {
  6880.                             var endSelection = tinymce.activeEditor.selection.getEnd();
  6881.                             var startSelection = tinymce.activeEditor.selection.getStart();
  6882.                             //don't proceed if it's an entire element selected
  6883.                             if (endSelection !== startSelection) {
  6884.                                
  6885.                                 //if the end selection is a macro then move the cursor
  6886.                                 //NOTE: we don't have to handle when the selection comes from a previous parent because
  6887.                                 // that is automatically taken care of with the normal onNodeChanged logic since the
  6888.                                 // evt.element will be the macro once it becomes part of the selection.
  6889.                                 var $testForMacro = $(endSelection).closest(".umb-macro-holder");
  6890.                                 if ($testForMacro.length > 0) {
  6891.                                    
  6892.                                     //it came from before so move after, if there is no after then select ourselves
  6893.                                     var next = $testForMacro.next();
  6894.                                     if (next.length > 0) {
  6895.                                         editor.selection.setCursorLocation($testForMacro.next().get(0));
  6896.                                     }
  6897.                                     else {
  6898.                                         selectMacroElement($testForMacro.get(0));
  6899.                                     }
  6900.  
  6901.                                 }
  6902.                             }
  6903.                         }
  6904.                     }
  6905.  
  6906.                     /** helper method to select the macro element */
  6907.                     function selectMacroElement(macroElement) {
  6908.  
  6909.                         // move selection to top element to ensure we can't edit this
  6910.                         editor.selection.select(macroElement);
  6911.  
  6912.                         // check if the current selection *is* the element (ie bug)
  6913.                         var currentSelection = editor.selection.getStart();
  6914.                         if (tinymce.isIE) {
  6915.                             if (!editor.dom.hasClass(currentSelection, 'umb-macro-holder')) {
  6916.                                 while (!editor.dom.hasClass(currentSelection, 'umb-macro-holder') && currentSelection.parentNode) {
  6917.                                     currentSelection = currentSelection.parentNode;
  6918.                                 }
  6919.                                 editor.selection.select(currentSelection);
  6920.                             }
  6921.                         }
  6922.                     }
  6923.  
  6924.                     /**
  6925.                     * Add a node change handler, test if we're editing a macro and select the whole thing, then set our isOnMacroElement flag.
  6926.                     * If we change the selection inside this method, then we end up in an infinite loop, so we have to remove ourselves
  6927.                     * from the event listener before changing selection, however, it seems that putting a break point in this method
  6928.                     * will always cause an 'infinite' loop as the caret keeps changing.
  6929.                     */
  6930.                     function onNodeChanged(evt) {
  6931.  
  6932.                         //set our macro button active when on a node of class umb-macro-holder
  6933.                         var $macroElement = $(evt.element).closest(".umb-macro-holder");
  6934.                        
  6935.                         handleSelectionChange();
  6936.  
  6937.                         //set the button active
  6938.                         ctrl.active($macroElement.length !== 0);
  6939.  
  6940.                         if ($macroElement.length > 0) {
  6941.                             var macroElement = $macroElement.get(0);
  6942.  
  6943.                             //remove the event listener before re-selecting
  6944.                             editor.off('NodeChange', onNodeChanged);
  6945.  
  6946.                             selectMacroElement(macroElement);
  6947.  
  6948.                             //set the flag
  6949.                             isOnMacroElement = true;
  6950.  
  6951.                             //re-add the event listener
  6952.                             editor.on('NodeChange', onNodeChanged);
  6953.                         }
  6954.                         else {
  6955.                             isOnMacroElement = false;
  6956.                         }
  6957.  
  6958.                     }
  6959.  
  6960.                     /** when the contents load we need to find any macros declared and load in their content */
  6961.                     editor.on("LoadContent", function (o) {
  6962.                        
  6963.                         //get all macro divs and load their content
  6964.                         $(editor.dom.select(".umb-macro-holder.mceNonEditable")).each(function() {
  6965.                             createInsertMacroScope.loadMacroContent($(this), null, $scope);
  6966.                         });
  6967.  
  6968.                     });
  6969.                    
  6970.                     /** This prevents any other commands from executing when the current element is the macro so the content cannot be edited */
  6971.                     editor.on('BeforeExecCommand', function (o) {                        
  6972.                         if (isOnMacroElement) {
  6973.                             if (o.preventDefault) {
  6974.                                 o.preventDefault();
  6975.                             }
  6976.                             if (o.stopImmediatePropagation) {
  6977.                                 o.stopImmediatePropagation();
  6978.                             }
  6979.                             return;
  6980.                         }
  6981.                     });
  6982.                    
  6983.                     /** This double checks and ensures you can't paste content into the rendered macro */
  6984.                     editor.on("Paste", function (o) {                        
  6985.                         if (isOnMacroElement) {
  6986.                             if (o.preventDefault) {
  6987.                                 o.preventDefault();
  6988.                             }
  6989.                             if (o.stopImmediatePropagation) {
  6990.                                 o.stopImmediatePropagation();
  6991.                             }
  6992.                             return;
  6993.                         }
  6994.                     });
  6995.  
  6996.                     //set onNodeChanged event listener
  6997.                     editor.on('NodeChange', onNodeChanged);
  6998.  
  6999.                     /**
  7000.                     * Listen for the keydown in the editor, we'll check if we are currently on a macro element, if so
  7001.                     * we'll check if the key down is a supported key which requires an action, otherwise we ignore the request
  7002.                     * so the macro cannot be edited.
  7003.                     */
  7004.                     editor.on('KeyDown', function (e) {
  7005.                         if (isOnMacroElement) {
  7006.                             var macroElement = editor.selection.getNode();
  7007.  
  7008.                             //get the 'real' element (either p or the real one)
  7009.                             macroElement = getRealMacroElem(macroElement);
  7010.  
  7011.                             //prevent editing
  7012.                             e.preventDefault();
  7013.                             e.stopPropagation();
  7014.  
  7015.                             var moveSibling = function (element, isNext) {
  7016.                                 var $e = $(element);
  7017.                                 var $sibling = isNext ? $e.next() : $e.prev();
  7018.                                 if ($sibling.length > 0) {
  7019.                                     editor.selection.select($sibling.get(0));
  7020.                                     editor.selection.collapse(true);
  7021.                                 }
  7022.                                 else {
  7023.                                     //if we're moving previous and there is no sibling, then lets recurse and just select the next one
  7024.                                     if (!isNext) {
  7025.                                         moveSibling(element, true);
  7026.                                         return;
  7027.                                     }
  7028.  
  7029.                                     //if there is no sibling we'll generate a new p at the end and select it
  7030.                                     editor.setContent(editor.getContent() + "<p>&nbsp;</p>");
  7031.                                     editor.selection.select($(editor.dom.getRoot()).children().last().get(0));
  7032.                                     editor.selection.collapse(true);
  7033.  
  7034.                                 }
  7035.                             };
  7036.  
  7037.                             //supported keys to move to the next or prev element (13-enter, 27-esc, 38-up, 40-down, 39-right, 37-left)
  7038.                             //supported keys to remove the macro (8-backspace, 46-delete)
  7039.                             //TODO: Should we make the enter key insert a line break before or leave it as moving to the next element?
  7040.                             if ($.inArray(e.keyCode, [13, 40, 39]) !== -1) {
  7041.                                 //move to next element
  7042.                                 moveSibling(macroElement, true);
  7043.                             }
  7044.                             else if ($.inArray(e.keyCode, [27, 38, 37]) !== -1) {
  7045.                                 //move to prev element
  7046.                                 moveSibling(macroElement, false);
  7047.                             }
  7048.                             else if ($.inArray(e.keyCode, [8, 46]) !== -1) {
  7049.                                 //delete macro element
  7050.  
  7051.                                 //move first, then delete
  7052.                                 moveSibling(macroElement, false);
  7053.                                 editor.dom.remove(macroElement);
  7054.                             }
  7055.                             return ;
  7056.                         }
  7057.                     });
  7058.  
  7059.                 },
  7060.                
  7061.                 /** The insert macro button click event handler */
  7062.                 onclick: function () {
  7063.  
  7064.                     var dialogData = {
  7065.                         //flag for use in rte so we only show macros flagged for the editor
  7066.                         richTextEditor: true  
  7067.                     };
  7068.  
  7069.                     //when we click we could have a macro already selected and in that case we'll want to edit the current parameters
  7070.                     //so we'll need to extract them and submit them to the dialog.
  7071.                     var macroElement = editor.selection.getNode();                    
  7072.                     macroElement = getRealMacroElem(macroElement);
  7073.                     if (macroElement) {
  7074.                         //we have a macro selected so we'll need to parse it's alias and parameters
  7075.                         var contents = $(macroElement).contents();                        
  7076.                         var comment = _.find(contents, function(item) {
  7077.                             return item.nodeType === 8;
  7078.                         });
  7079.                         if (!comment) {
  7080.                             throw "Cannot parse the current macro, the syntax in the editor is invalid";
  7081.                         }
  7082.                         var syntax = comment.textContent.trim();
  7083.                         var parsed = macroService.parseMacroSyntax(syntax);
  7084.                         dialogData = {
  7085.                             macroData: parsed  
  7086.                         };
  7087.                     }
  7088.  
  7089.                     if(callback) {
  7090.                         callback(dialogData);
  7091.                     }
  7092.  
  7093.                 }
  7094.             });
  7095.         },
  7096.  
  7097.         insertMacroInEditor: function(editor, macroObject, $scope) {
  7098.  
  7099.             //put the macro syntax in comments, we will parse this out on the server side to be used
  7100.             //for persisting.
  7101.             var macroSyntaxComment = "<!-- " + macroObject.syntax + " -->";
  7102.             //create an id class for this element so we can re-select it after inserting
  7103.             var uniqueId = "umb-macro-" + editor.dom.uniqueId();
  7104.             var macroDiv = editor.dom.create('div',
  7105.                 {
  7106.                     'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId
  7107.                 },
  7108.                 macroSyntaxComment + '<ins>Macro alias: <strong>' + macroObject.macroAlias + '</strong></ins>');
  7109.  
  7110.             editor.selection.setNode(macroDiv);
  7111.  
  7112.             var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId));
  7113.  
  7114.             //async load the macro content
  7115.             this.loadMacroContent($macroDiv, macroObject, $scope);
  7116.  
  7117.         },
  7118.  
  7119.         /** loads in the macro content async from the server */
  7120.         loadMacroContent: function($macroDiv, macroData, $scope) {
  7121.  
  7122.             //if we don't have the macroData, then we'll need to parse it from the macro div
  7123.             if (!macroData) {
  7124.                 var contents = $macroDiv.contents();
  7125.                 var comment = _.find(contents, function (item) {
  7126.                     return item.nodeType === 8;
  7127.                 });
  7128.                 if (!comment) {
  7129.                     throw "Cannot parse the current macro, the syntax in the editor is invalid";
  7130.                 }
  7131.                 var syntax = comment.textContent.trim();
  7132.                 var parsed = macroService.parseMacroSyntax(syntax);
  7133.                 macroData = parsed;
  7134.             }
  7135.  
  7136.             var $ins = $macroDiv.find("ins");
  7137.  
  7138.             //show the throbber
  7139.             $macroDiv.addClass("loading");
  7140.  
  7141.             var contentId = $routeParams.id;
  7142.  
  7143.             //need to wrap in safe apply since this might be occuring outside of angular
  7144.             angularHelper.safeApply($scope, function() {
  7145.                 macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary)
  7146.                 .then(function (htmlResult) {
  7147.  
  7148.                     $macroDiv.removeClass("loading");
  7149.                     htmlResult = htmlResult.trim();
  7150.                     if (htmlResult !== "") {
  7151.                         $ins.html(htmlResult);
  7152.                     }
  7153.                 });
  7154.             });
  7155.  
  7156.         },
  7157.  
  7158.         createLinkPicker: function(editor, $scope, onClick) {
  7159.  
  7160.             function createLinkList(callback) {
  7161.                 return function() {
  7162.                     var linkList = editor.settings.link_list;
  7163.  
  7164.                     if (typeof(linkList) === "string") {
  7165.                         tinymce.util.XHR.send({
  7166.                             url: linkList,
  7167.                             success: function(text) {
  7168.                                 callback(tinymce.util.JSON.parse(text));
  7169.                             }
  7170.                         });
  7171.                     } else {
  7172.                         callback(linkList);
  7173.                     }
  7174.                 };
  7175.             }
  7176.  
  7177.             function showDialog(linkList) {
  7178.                 var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText;
  7179.                 var win, linkListCtrl, relListCtrl, targetListCtrl;
  7180.  
  7181.                 function linkListChangeHandler(e) {
  7182.                     var textCtrl = win.find('#text');
  7183.  
  7184.                     if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) {
  7185.                         textCtrl.value(e.control.text());
  7186.                     }
  7187.  
  7188.                     win.find('#href').value(e.control.value());
  7189.                 }
  7190.  
  7191.                 function buildLinkList() {
  7192.                     var linkListItems = [{
  7193.                         text: 'None',
  7194.                         value: ''
  7195.                     }];
  7196.  
  7197.                     tinymce.each(linkList, function(link) {
  7198.                         linkListItems.push({
  7199.                             text: link.text || link.title,
  7200.                             value: link.value || link.url,
  7201.                             menu: link.menu
  7202.                         });
  7203.                     });
  7204.  
  7205.                     return linkListItems;
  7206.                 }
  7207.  
  7208.                 function buildRelList(relValue) {
  7209.                     var relListItems = [{
  7210.                         text: 'None',
  7211.                         value: ''
  7212.                     }];
  7213.  
  7214.                     tinymce.each(editor.settings.rel_list, function(rel) {
  7215.                         relListItems.push({
  7216.                             text: rel.text || rel.title,
  7217.                             value: rel.value,
  7218.                             selected: relValue === rel.value
  7219.                         });
  7220.                     });
  7221.  
  7222.                     return relListItems;
  7223.                 }
  7224.  
  7225.                 function buildTargetList(targetValue) {
  7226.                     var targetListItems = [{
  7227.                         text: 'None',
  7228.                         value: ''
  7229.                     }];
  7230.  
  7231.                     if (!editor.settings.target_list) {
  7232.                         targetListItems.push({
  7233.                             text: 'New window',
  7234.                             value: '_blank'
  7235.                         });
  7236.                     }
  7237.  
  7238.                     tinymce.each(editor.settings.target_list, function(target) {
  7239.                         targetListItems.push({
  7240.                             text: target.text || target.title,
  7241.                             value: target.value,
  7242.                             selected: targetValue === target.value
  7243.                         });
  7244.                     });
  7245.  
  7246.                     return targetListItems;
  7247.                 }
  7248.  
  7249.                 function buildAnchorListControl(url) {
  7250.                     var anchorList = [];
  7251.  
  7252.                     tinymce.each(editor.dom.select('a:not([href])'), function(anchor) {
  7253.                         var id = anchor.name || anchor.id;
  7254.  
  7255.                         if (id) {
  7256.                             anchorList.push({
  7257.                                 text: id,
  7258.                                 value: '#' + id,
  7259.                                 selected: url.indexOf('#' + id) !== -1
  7260.                             });
  7261.                         }
  7262.                     });
  7263.  
  7264.                     if (anchorList.length) {
  7265.                         anchorList.unshift({
  7266.                             text: 'None',
  7267.                             value: ''
  7268.                         });
  7269.  
  7270.                         return {
  7271.                             name: 'anchor',
  7272.                             type: 'listbox',
  7273.                             label: 'Anchors',
  7274.                             values: anchorList,
  7275.                             onselect: linkListChangeHandler
  7276.                         };
  7277.                     }
  7278.                 }
  7279.  
  7280.                 function updateText() {
  7281.                     if (!initialText && data.text.length === 0) {
  7282.                         this.parent().parent().find('#text')[0].value(this.value());
  7283.                     }
  7284.                 }
  7285.  
  7286.                 selectedElm = selection.getNode();
  7287.                 anchorElm = dom.getParent(selectedElm, 'a[href]');
  7288.  
  7289.                 data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'});
  7290.                 data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : '';
  7291.                 data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : '';
  7292.                 data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : '';
  7293.  
  7294.                 if (selectedElm.nodeName === "IMG") {
  7295.                     data.text = initialText = " ";
  7296.                 }
  7297.  
  7298.                 if (linkList) {
  7299.                     linkListCtrl = {
  7300.                         type: 'listbox',
  7301.                         label: 'Link list',
  7302.                         values: buildLinkList(),
  7303.                         onselect: linkListChangeHandler
  7304.                     };
  7305.                 }
  7306.  
  7307.                 if (editor.settings.target_list !== false) {
  7308.                     targetListCtrl = {
  7309.                         name: 'target',
  7310.                         type: 'listbox',
  7311.                         label: 'Target',
  7312.                         values: buildTargetList(data.target)
  7313.                     };
  7314.                 }
  7315.  
  7316.                 if (editor.settings.rel_list) {
  7317.                     relListCtrl = {
  7318.                         name: 'rel',
  7319.                         type: 'listbox',
  7320.                         label: 'Rel',
  7321.                         values: buildRelList(data.rel)
  7322.                     };
  7323.                 }
  7324.  
  7325.                 var injector = angular.element(document.getElementById("umbracoMainPageBody")).injector();
  7326.                 var dialogService = injector.get("dialogService");
  7327.                 var currentTarget = null;
  7328.  
  7329.                 //if we already have a link selected, we want to pass that data over to the dialog
  7330.                 if(anchorElm){
  7331.                     var anchor = $(anchorElm);
  7332.                     currentTarget = {
  7333.                         name: anchor.attr("title"),
  7334.                         url: anchor.attr("href"),
  7335.                         target: anchor.attr("target")
  7336.                     };
  7337.  
  7338.                     //locallink detection, we do this here, to avoid poluting the dialogservice
  7339.                     //so the dialog service can just expect to get a node-like structure
  7340.                     if(currentTarget.url.indexOf("localLink:") > 0){
  7341.                         currentTarget.id = currentTarget.url.substring(currentTarget.url.indexOf(":")+1,currentTarget.url.length-1);
  7342.                     }
  7343.                 }
  7344.  
  7345.                 if(onClick) {
  7346.                     onClick(currentTarget, anchorElm);
  7347.                 }
  7348.  
  7349.             }
  7350.  
  7351.             editor.addButton('link', {
  7352.                 icon: 'link',
  7353.                 tooltip: 'Insert/edit link',
  7354.                 shortcut: 'Ctrl+K',
  7355.                 onclick: createLinkList(showDialog),
  7356.                 stateSelector: 'a[href]'
  7357.             });
  7358.  
  7359.             editor.addButton('unlink', {
  7360.                 icon: 'unlink',
  7361.                 tooltip: 'Remove link',
  7362.                 cmd: 'unlink',
  7363.                 stateSelector: 'a[href]'
  7364.             });
  7365.  
  7366.             editor.addShortcut('Ctrl+K', '', createLinkList(showDialog));
  7367.             this.showDialog = showDialog;
  7368.  
  7369.             editor.addMenuItem('link', {
  7370.                 icon: 'link',
  7371.                 text: 'Insert link',
  7372.                 shortcut: 'Ctrl+K',
  7373.                 onclick: createLinkList(showDialog),
  7374.                 stateSelector: 'a[href]',
  7375.                 context: 'insert',
  7376.                 prependToContext: true
  7377.             });
  7378.  
  7379.         },
  7380.  
  7381.         insertLinkInEditor: function(editor, target, anchorElm) {
  7382.  
  7383.             var href = target.url;
  7384.  
  7385.             function insertLink() {
  7386.                 if (anchorElm) {
  7387.                     editor.dom.setAttribs(anchorElm, {
  7388.                         href: href,
  7389.                         title: target.name,
  7390.                         target: target.target ? target.target : null,
  7391.                         rel: target.rel ? target.rel : null,
  7392.                         'data-id': target.id ? target.id : null
  7393.                     });
  7394.  
  7395.                     editor.selection.select(anchorElm);
  7396.                     editor.execCommand('mceEndTyping');
  7397.                 } else {
  7398.                     editor.execCommand('mceInsertLink', false, {
  7399.                         href: href,
  7400.                         title: target.name,
  7401.                         target: target.target ? target.target : null,
  7402.                         rel: target.rel ? target.rel : null,
  7403.                         'data-id': target.id ? target.id : null
  7404.                     });
  7405.                 }
  7406.             }
  7407.  
  7408.             if (!href) {
  7409.                 editor.execCommand('unlink');
  7410.                 return;
  7411.             }
  7412.  
  7413.             //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set
  7414.             if(target.id && (angular.isUndefined(target.isMedia) || !target.isMedia)){
  7415.                 href = "/{localLink:" + target.id + "}";
  7416.                 insertLink();
  7417.                 return;
  7418.             }
  7419.  
  7420.             // Is email and not //user@domain.com
  7421.             if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) {
  7422.                 href = 'mailto:' + href;
  7423.                 insertLink();
  7424.                 return;
  7425.             }
  7426.  
  7427.             // Is www. prefixed
  7428.             if (/^\s*www\./i.test(href)) {
  7429.                 href = 'http://' + href;
  7430.                 insertLink();
  7431.                 return;
  7432.             }
  7433.  
  7434.             insertLink();
  7435.  
  7436.         }
  7437.  
  7438.     };
  7439. }
  7440.  
  7441. angular.module('umbraco.services').factory('tinyMceService', tinyMceService);
  7442.  
  7443.  
  7444. /**
  7445.  * @ngdoc service
  7446.  * @name umbraco.services.treeService
  7447.  * @function
  7448.  *
  7449.  * @description
  7450.  * The tree service factory, used internally by the umbTree and umbTreeItem directives
  7451.  */
  7452. function treeService($q, treeResource, iconHelper, notificationsService, eventsService) {
  7453.  
  7454.     //SD: Have looked at putting this in sessionStorage (not localStorage since that means you wouldn't be able to work
  7455.     // in multiple tabs) - however our tree structure is cyclical, meaning a node has a reference to it's parent and it's children
  7456.     // which you cannot serialize to sessionStorage. There's really no benefit of session storage except that you could refresh
  7457.     // 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
  7458.     // as a nodeid reference instead of a variable with a getParent() method.
  7459.     var treeCache = {};
  7460.    
  7461.     var standardCssClass = 'icon umb-tree-icon sprTree';
  7462.  
  7463.     function getCacheKey(args) {
  7464.         //if there is no cache key they return null - it won't be cached.
  7465.         if (!args || !args.cacheKey) {
  7466.             return null;
  7467.         }        
  7468.  
  7469.         var cacheKey = args.cacheKey;
  7470.         cacheKey += "_" + args.section;
  7471.         return cacheKey;
  7472.     }
  7473.  
  7474.     return {  
  7475.  
  7476.         /** Internal method to return the tree cache */
  7477.         _getTreeCache: function() {
  7478.             return treeCache;
  7479.         },
  7480.  
  7481.         /** 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 */
  7482.         _formatNodeDataForUseInUI: function (parentNode, treeNodes, section, level) {
  7483.             //if no level is set, then we make it 1  
  7484.             var childLevel = (level ? level : 1);
  7485.             //set the section if it's not already set
  7486.             if (!parentNode.section) {
  7487.                 parentNode.section = section;
  7488.             }
  7489.             //create a method outside of the loop to return the parent - otherwise jshint blows up
  7490.             var funcParent = function() {
  7491.                 return parentNode;
  7492.             };
  7493.             for (var i = 0; i < treeNodes.length; i++) {
  7494.  
  7495.                 treeNodes[i].level = childLevel;
  7496.  
  7497.                 //create a function to get the parent node, we could assign the parent node but
  7498.                 // then we cannot serialize this entity because we have a cyclical reference.
  7499.                 // Instead we just make a function to return the parentNode.
  7500.                 treeNodes[i].parent = funcParent;
  7501.  
  7502.                 //set the section for each tree node - this allows us to reference this easily when accessing tree nodes
  7503.                 treeNodes[i].section = section;
  7504.  
  7505.                 //if there is not route path specified, then set it automatically,
  7506.                 //if this is a tree root node then we want to route to the section's dashboard
  7507.                 if (!treeNodes[i].routePath) {
  7508.                    
  7509.                     if (treeNodes[i].metaData && treeNodes[i].metaData["treeAlias"]) {
  7510.                         //this is a root node
  7511.                         treeNodes[i].routePath = section;                        
  7512.                     }
  7513.                     else {
  7514.                         var treeAlias = this.getTreeAlias(treeNodes[i]);
  7515.                         treeNodes[i].routePath = section + "/" + treeAlias + "/edit/" + treeNodes[i].id;
  7516.                     }
  7517.                 }
  7518.  
  7519.                 //now, format the icon data
  7520.                 if (treeNodes[i].iconIsClass === undefined || treeNodes[i].iconIsClass) {
  7521.                     var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNodes[i]);
  7522.                     treeNodes[i].cssClass = standardCssClass + " " + converted;
  7523.                     if (converted.startsWith('.')) {
  7524.                         //its legacy so add some width/height
  7525.                         treeNodes[i].style = "height:16px;width:16px;";
  7526.                     }
  7527.                     else {
  7528.                         treeNodes[i].style = "";
  7529.                     }
  7530.                 }
  7531.                 else {
  7532.                     treeNodes[i].style = "background-image: url('" + treeNodes[i].iconFilePath + "');";
  7533.                     //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this
  7534.                     treeNodes[i].cssClass = standardCssClass + " legacy-custom-file";
  7535.                 }
  7536.             }
  7537.         },
  7538.  
  7539.         /**
  7540.          * @ngdoc method
  7541.          * @name umbraco.services.treeService#getTreePackageFolder
  7542.          * @methodOf umbraco.services.treeService
  7543.          * @function
  7544.          *
  7545.          * @description
  7546.          * Determines if the current tree is a plugin tree and if so returns the package folder it has declared
  7547.          * so we know where to find it's views, otherwise it will just return undefined.
  7548.          *
  7549.          * @param {String} treeAlias The tree alias to check
  7550.          */
  7551.         getTreePackageFolder: function(treeAlias) {            
  7552.             //we determine this based on the server variables
  7553.             if (Umbraco.Sys.ServerVariables.umbracoPlugins &&
  7554.                 Umbraco.Sys.ServerVariables.umbracoPlugins.trees &&
  7555.                 angular.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) {
  7556.  
  7557.                 var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function(item) {
  7558.                     return item.alias === treeAlias;
  7559.                 });
  7560.                
  7561.                 return found ? found.packageFolder : undefined;
  7562.             }
  7563.             return undefined;
  7564.         },
  7565.  
  7566.         /**
  7567.          * @ngdoc method
  7568.          * @name umbraco.services.treeService#clearCache
  7569.          * @methodOf umbraco.services.treeService
  7570.          * @function
  7571.          *
  7572.          * @description
  7573.          * Clears the tree cache - with optional cacheKey, optional section or optional filter.
  7574.          *
  7575.          * @param {Object} args arguments
  7576.          * @param {String} args.cacheKey optional cachekey - this is used to clear specific trees in dialogs
  7577.          * @param {String} args.section optional section alias - clear tree for a given section
  7578.          * @param {String} args.childrenOf optional parent ID - only clear the cache below a specific node
  7579.          */
  7580.         clearCache: function (args) {
  7581.             //clear all if not specified
  7582.             if (!args) {
  7583.                 treeCache = {};
  7584.             }
  7585.             else {
  7586.                 //if section and cache key specified just clear that cache
  7587.                 if (args.section && args.cacheKey) {
  7588.                     var cacheKey = getCacheKey(args);
  7589.                     if (cacheKey && treeCache && treeCache[cacheKey] != null) {
  7590.                         treeCache = _.omit(treeCache, cacheKey);
  7591.                     }
  7592.                 }
  7593.                 else if (args.childrenOf) {
  7594.                     //if childrenOf is supplied a cacheKey must be supplied as well
  7595.                     if (!args.cacheKey) {
  7596.                         throw "args.cacheKey is required if args.childrenOf is supplied";
  7597.                     }
  7598.                     //this will clear out all children for the parentId passed in to this parameter, we'll
  7599.                     // do this by recursing and specifying a filter
  7600.                     var self = this;
  7601.                     this.clearCache({
  7602.                         cacheKey: args.cacheKey,
  7603.                         filter: function(cc) {
  7604.                             //get the new parent node from the tree cache
  7605.                             var parent = self.getDescendantNode(cc.root, args.childrenOf);
  7606.                             if (parent) {
  7607.                                 //clear it's children and set to not expanded
  7608.                                 parent.children = null;
  7609.                                 parent.expanded = false;
  7610.                             }
  7611.                             //return the cache to be saved
  7612.                             return cc;
  7613.                         }
  7614.                     });
  7615.                 }
  7616.                 else if (args.filter && angular.isFunction(args.filter)) {
  7617.                     //if a filter is supplied a cacheKey must be supplied as well
  7618.                     if (!args.cacheKey) {
  7619.                         throw "args.cacheKey is required if args.filter is supplied";
  7620.                     }
  7621.  
  7622.                     //if a filter is supplied the function needs to return the data to keep
  7623.                     var byKey = treeCache[args.cacheKey];
  7624.                     if (byKey) {
  7625.                         var result = args.filter(byKey);
  7626.  
  7627.                         if (result) {
  7628.                             //set the result to the filtered data
  7629.                             treeCache[args.cacheKey] = result;
  7630.                         }
  7631.                         else {                            
  7632.                             //remove the cache
  7633.                             treeCache = _.omit(treeCache, args.cacheKey);
  7634.                         }
  7635.  
  7636.                     }
  7637.  
  7638.                 }
  7639.                 else if (args.cacheKey) {
  7640.                     //if only the cache key is specified, then clear all cache starting with that key
  7641.                     var allKeys1 = _.keys(treeCache);
  7642.                     var toRemove1 = _.filter(allKeys1, function (k) {
  7643.                         return k.startsWith(args.cacheKey + "_");
  7644.                     });
  7645.                     treeCache = _.omit(treeCache, toRemove1);
  7646.                 }
  7647.                 else if (args.section) {
  7648.                     //if only the section is specified then clear all cache regardless of cache key by that section
  7649.                     var allKeys2 = _.keys(treeCache);
  7650.                     var toRemove2 = _.filter(allKeys2, function (k) {
  7651.                         return k.endsWith("_" + args.section);
  7652.                     });
  7653.                     treeCache = _.omit(treeCache, toRemove2);
  7654.                 }              
  7655.             }
  7656.         },
  7657.  
  7658.         /**
  7659.          * @ngdoc method
  7660.          * @name umbraco.services.treeService#loadNodeChildren
  7661.          * @methodOf umbraco.services.treeService
  7662.          * @function
  7663.          *
  7664.          * @description
  7665.          * Clears all node children, gets it's up-to-date children from the server and re-assigns them and then
  7666.          * returns them in a promise.
  7667.          * @param {object} args An arguments object
  7668.          * @param {object} args.node The tree node
  7669.          * @param {object} args.section The current section
  7670.          */
  7671.         loadNodeChildren: function(args) {
  7672.             if (!args) {
  7673.                 throw "No args object defined for loadNodeChildren";
  7674.             }
  7675.             if (!args.node) {
  7676.                 throw "No node defined on args object for loadNodeChildren";
  7677.             }
  7678.            
  7679.             this.removeChildNodes(args.node);
  7680.             args.node.loading = true;
  7681.  
  7682.             return this.getChildren(args)
  7683.                 .then(function(data) {
  7684.  
  7685.                     //set state to done and expand (only if there actually are children!)
  7686.                     args.node.loading = false;
  7687.                     args.node.children = data;
  7688.                     if (args.node.children && args.node.children.length > 0) {
  7689.                         args.node.expanded = true;
  7690.                         args.node.hasChildren = true;
  7691.                     }
  7692.                     return data;
  7693.  
  7694.                 }, function(reason) {
  7695.  
  7696.                     //in case of error, emit event
  7697.                     eventsService.emit("treeService.treeNodeLoadError", {error: reason } );
  7698.  
  7699.                     //stop show the loading indicator  
  7700.                     args.node.loading = false;
  7701.  
  7702.                     //tell notications about the error
  7703.                     notificationsService.error(reason);
  7704.  
  7705.                     return reason;
  7706.                 });
  7707.  
  7708.         },
  7709.  
  7710.         /**
  7711.          * @ngdoc method
  7712.          * @name umbraco.services.treeService#removeNode
  7713.          * @methodOf umbraco.services.treeService
  7714.          * @function
  7715.          *
  7716.          * @description
  7717.          * Removes a given node from the tree
  7718.          * @param {object} treeNode the node to remove
  7719.          */
  7720.         removeNode: function(treeNode) {
  7721.             if (!angular.isFunction(treeNode.parent)) {
  7722.                 return;
  7723.             }
  7724.  
  7725.             if (treeNode.parent() == null) {
  7726.                 throw "Cannot remove a node that doesn't have a parent";
  7727.             }
  7728.             //remove the current item from it's siblings
  7729.             treeNode.parent().children.splice(treeNode.parent().children.indexOf(treeNode), 1);            
  7730.         },
  7731.        
  7732.         /**
  7733.          * @ngdoc method
  7734.          * @name umbraco.services.treeService#removeChildNodes
  7735.          * @methodOf umbraco.services.treeService
  7736.          * @function
  7737.          *
  7738.          * @description
  7739.          * Removes all child nodes from a given tree node
  7740.          * @param {object} treeNode the node to remove children from
  7741.          */
  7742.         removeChildNodes : function(treeNode) {
  7743.             treeNode.expanded = false;
  7744.             treeNode.children = [];
  7745.             treeNode.hasChildren = false;
  7746.         },
  7747.  
  7748.         /**
  7749.          * @ngdoc method
  7750.          * @name umbraco.services.treeService#getChildNode
  7751.          * @methodOf umbraco.services.treeService
  7752.          * @function
  7753.          *
  7754.          * @description
  7755.          * Gets a child node with a given ID, from a specific treeNode
  7756.          * @param {object} treeNode to retrive child node from
  7757.          * @param {int} id id of child node
  7758.          */
  7759.         getChildNode: function (treeNode, id) {
  7760.             if (!treeNode.children) {
  7761.                 return null;
  7762.             }
  7763.             var found = _.find(treeNode.children, function (child) {
  7764.                 return String(child.id).toLowerCase() === String(id).toLowerCase();
  7765.             });
  7766.             return found === undefined ? null : found;
  7767.         },
  7768.  
  7769.         /**
  7770.          * @ngdoc method
  7771.          * @name umbraco.services.treeService#getDescendantNode
  7772.          * @methodOf umbraco.services.treeService
  7773.          * @function
  7774.          *
  7775.          * @description
  7776.          * Gets a descendant node by id
  7777.          * @param {object} treeNode to retrive descendant node from
  7778.          * @param {int} id id of descendant node
  7779.          * @param {string} treeAlias - optional tree alias, if fetching descendant node from a child of a listview document
  7780.          */
  7781.         getDescendantNode: function(treeNode, id, treeAlias) {
  7782.  
  7783.             //validate if it is a section container since we'll need a treeAlias if it is one
  7784.             if (treeNode.isContainer === true && !treeAlias) {
  7785.                 throw "Cannot get a descendant node from a section container node without a treeAlias specified";
  7786.             }
  7787.  
  7788.             //if it is a section container, we need to find the tree to be searched
  7789.             if (treeNode.isContainer) {
  7790.                 var foundRoot = null;
  7791.                 for (var c = 0; c < treeNode.children.length; c++) {
  7792.                     if (this.getTreeAlias(treeNode.children[c]) === treeAlias) {
  7793.                         foundRoot = treeNode.children[c];
  7794.                         break;
  7795.                     }
  7796.                 }
  7797.                 if (!foundRoot) {
  7798.                     throw "Could not find a tree in the current section with alias " + treeAlias;
  7799.                 }
  7800.                 treeNode = foundRoot;
  7801.             }
  7802.  
  7803.             //check this node
  7804.             if (treeNode.id === id) {
  7805.                 return treeNode;
  7806.             }
  7807.  
  7808.             //check the first level
  7809.             var found = this.getChildNode(treeNode, id);
  7810.             if (found) {
  7811.                 return found;
  7812.             }
  7813.            
  7814.             //check each child of this node
  7815.             if (!treeNode.children) {
  7816.                 return null;
  7817.             }
  7818.  
  7819.             for (var i = 0; i < treeNode.children.length; i++) {
  7820.                 if (treeNode.children[i].children && angular.isArray(treeNode.children[i].children) && treeNode.children[i].children.length > 0) {
  7821.                     //recurse
  7822.                     found = this.getDescendantNode(treeNode.children[i], id);
  7823.                     if (found) {
  7824.                         return found;
  7825.                     }
  7826.                 }
  7827.             }
  7828.            
  7829.             //not found
  7830.             return found === undefined ? null : found;
  7831.         },
  7832.  
  7833.         /**
  7834.          * @ngdoc method
  7835.          * @name umbraco.services.treeService#getTreeRoot
  7836.          * @methodOf umbraco.services.treeService
  7837.          * @function
  7838.          *
  7839.          * @description
  7840.          * Gets the root node of the current tree type for a given tree node
  7841.          * @param {object} treeNode to retrive tree root node from
  7842.          */
  7843.         getTreeRoot: function (treeNode) {
  7844.             if (!treeNode) {
  7845.                 throw "treeNode cannot be null";
  7846.             }
  7847.  
  7848.             //all root nodes have metadata key 'treeAlias'
  7849.             var root = null;
  7850.             var current = treeNode;            
  7851.             while (root === null && current) {
  7852.                
  7853.                 if (current.metaData && current.metaData["treeAlias"]) {
  7854.                     root = current;
  7855.                 }
  7856.                 else if (angular.isFunction(current.parent)) {
  7857.                     //we can only continue if there is a parent() method which means this
  7858.                     // tree node was loaded in as part of a real tree, not just as a single tree
  7859.                     // node from the server.
  7860.                     current = current.parent();
  7861.                 }
  7862.                 else {
  7863.                     current = null;
  7864.                 }
  7865.             }
  7866.             return root;
  7867.         },
  7868.  
  7869.         /** Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node */
  7870.         /**
  7871.          * @ngdoc method
  7872.          * @name umbraco.services.treeService#getTreeAlias
  7873.          * @methodOf umbraco.services.treeService
  7874.          * @function
  7875.          *
  7876.          * @description
  7877.          * Gets the node's tree alias, this is done by looking up the meta-data of the current node's root node
  7878.          * @param {object} treeNode to retrive tree alias from
  7879.          */
  7880.         getTreeAlias : function(treeNode) {
  7881.             var root = this.getTreeRoot(treeNode);
  7882.             if (root) {
  7883.                 return root.metaData["treeAlias"];
  7884.             }
  7885.             return "";
  7886.         },
  7887.  
  7888.         /**
  7889.          * @ngdoc method
  7890.          * @name umbraco.services.treeService#getTree
  7891.          * @methodOf umbraco.services.treeService
  7892.          * @function
  7893.          *
  7894.          * @description
  7895.          * gets the tree, returns a promise
  7896.          * @param {object} args Arguments
  7897.          * @param {string} args.section Section alias
  7898.          * @param {string} args.cacheKey Optional cachekey
  7899.          */
  7900.         getTree: function (args) {
  7901.  
  7902.             var deferred = $q.defer();
  7903.  
  7904.             //set defaults
  7905.             if (!args) {
  7906.                 args = { section: 'content', cacheKey: null };
  7907.             }
  7908.             else if (!args.section) {
  7909.                 args.section = 'content';
  7910.             }
  7911.  
  7912.             var cacheKey = getCacheKey(args);
  7913.            
  7914.             //return the cache if it exists
  7915.             if (cacheKey && treeCache[cacheKey] !== undefined) {
  7916.                 deferred.resolve(treeCache[cacheKey]);
  7917.                 return deferred.promise;
  7918.             }
  7919.  
  7920.             var self = this;
  7921.             treeResource.loadApplication(args)
  7922.                 .then(function(data) {
  7923.                     //this will be called once the tree app data has loaded
  7924.                     var result = {
  7925.                         name: data.name,
  7926.                         alias: args.section,
  7927.                         root: data
  7928.                     };
  7929.                     //we need to format/modify some of the node data to be used in our app.
  7930.                     self._formatNodeDataForUseInUI(result.root, result.root.children, args.section);
  7931.  
  7932.                     //cache this result if a cache key is specified - generally a cache key should ONLY
  7933.                     // be specified for application trees, dialog trees should not be cached.
  7934.                     if (cacheKey) {                        
  7935.                         treeCache[cacheKey] = result;
  7936.                         deferred.resolve(treeCache[cacheKey]);
  7937.                     }
  7938.  
  7939.                     //return un-cached
  7940.                     deferred.resolve(result);
  7941.                 });
  7942.            
  7943.             return deferred.promise;
  7944.         },
  7945.  
  7946.         /**
  7947.          * @ngdoc method
  7948.          * @name umbraco.services.treeService#getMenu
  7949.          * @methodOf umbraco.services.treeService
  7950.          * @function
  7951.          *
  7952.          * @description
  7953.          * Returns available menu actions for a given tree node
  7954.          * @param {object} args Arguments
  7955.          * @param {string} args.treeNode tree node object to retrieve the menu for
  7956.          */
  7957.         getMenu: function (args) {
  7958.  
  7959.             if (!args) {
  7960.                 throw "args cannot be null";
  7961.             }
  7962.             if (!args.treeNode) {
  7963.                 throw "args.treeNode cannot be null";
  7964.             }
  7965.  
  7966.             return treeResource.loadMenu(args.treeNode)
  7967.                 .then(function(data) {
  7968.                     //need to convert the icons to new ones
  7969.                     for (var i = 0; i < data.length; i++) {
  7970.                         data[i].cssclass = iconHelper.convertFromLegacyIcon(data[i].cssclass);
  7971.                     }
  7972.                     return data;
  7973.                 });
  7974.         },
  7975.        
  7976.         /**
  7977.          * @ngdoc method
  7978.          * @name umbraco.services.treeService#getChildren
  7979.          * @methodOf umbraco.services.treeService
  7980.          * @function
  7981.          *
  7982.          * @description
  7983.          * Gets the children from the server for a given node
  7984.          * @param {object} args Arguments
  7985.          * @param {object} args.node tree node object to retrieve the children for
  7986.          * @param {string} args.section current section alias
  7987.          */
  7988.         getChildren: function (args) {
  7989.  
  7990.             if (!args) {
  7991.                 throw "No args object defined for getChildren";
  7992.             }
  7993.             if (!args.node) {
  7994.                 throw "No node defined on args object for getChildren";
  7995.             }
  7996.  
  7997.             var section = args.section || 'content';
  7998.             var treeItem = args.node;
  7999.  
  8000.             var self = this;
  8001.  
  8002.             return treeResource.loadNodes({ node: treeItem })
  8003.                 .then(function (data) {
  8004.                     //now that we have the data, we need to add the level property to each item and the view
  8005.                     self._formatNodeDataForUseInUI(treeItem, data, section, treeItem.level + 1);
  8006.                     return data;
  8007.                 });
  8008.         },
  8009.        
  8010.         /**
  8011.          * @ngdoc method
  8012.          * @name umbraco.services.treeService#reloadNode
  8013.          * @methodOf umbraco.services.treeService
  8014.          * @function
  8015.          *
  8016.          * @description
  8017.          * Re-loads the single node from the server
  8018.          * @param {object} node Tree node to reload
  8019.          */
  8020.         reloadNode: function(node) {
  8021.             if (!node) {
  8022.                 throw "node cannot be null";
  8023.             }
  8024.             if (!node.parent()) {
  8025.                 throw "cannot reload a single node without a parent";
  8026.             }
  8027.             if (!node.section) {
  8028.                 throw "cannot reload a single node without an assigned node.section";
  8029.             }
  8030.            
  8031.             var deferred = $q.defer();
  8032.            
  8033.             //set the node to loading
  8034.             node.loading = true;
  8035.  
  8036.             this.getChildren({ node: node.parent(), section: node.section }).then(function(data) {
  8037.  
  8038.                 //ok, now that we have the children, find the node we're reloading
  8039.                 var found = _.find(data, function(item) {
  8040.                     return item.id === node.id;
  8041.                 });
  8042.                 if (found) {
  8043.                     //now we need to find the node in the parent.children collection to replace
  8044.                     var index = _.indexOf(node.parent().children, node);
  8045.                     //the trick here is to not actually replace the node - this would cause the delete animations
  8046.                     //to fire, instead we're just going to replace all the properties of this node.
  8047.  
  8048.                     //there should always be a method assigned but we'll check anyways
  8049.                     if (angular.isFunction(node.parent().children[index].updateNodeData)) {
  8050.                         node.parent().children[index].updateNodeData(found);
  8051.                     }
  8052.                     else {
  8053.                         //just update as per normal - this means styles, etc.. won't be applied
  8054.                         _.extend(node.parent().children[index], found);
  8055.                     }
  8056.                    
  8057.                     //set the node loading
  8058.                     node.parent().children[index].loading = false;
  8059.                     //return
  8060.                     deferred.resolve(node.parent().children[index]);
  8061.                 }
  8062.                 else {
  8063.                     deferred.reject();
  8064.                 }
  8065.             }, function() {
  8066.                 deferred.reject();
  8067.             });
  8068.            
  8069.             return deferred.promise;
  8070.         },
  8071.  
  8072.         /**
  8073.          * @ngdoc method
  8074.          * @name umbraco.services.treeService#getPath
  8075.          * @methodOf umbraco.services.treeService
  8076.          * @function
  8077.          *
  8078.          * @description
  8079.          * This will return the current node's path by walking up the tree
  8080.          * @param {object} node Tree node to retrieve path for
  8081.          */
  8082.         getPath: function(node) {
  8083.             if (!node) {
  8084.                 throw "node cannot be null";                
  8085.             }
  8086.             if (!angular.isFunction(node.parent)) {
  8087.                 throw "node.parent is not a function, the path cannot be resolved";
  8088.             }
  8089.             //all root nodes have metadata key 'treeAlias'
  8090.             var reversePath = [];
  8091.             var current = node;
  8092.             while (current != null) {
  8093.                 reversePath.push(current.id);                
  8094.                 if (current.metaData && current.metaData["treeAlias"]) {
  8095.                     current = null;
  8096.                 }
  8097.                 else {
  8098.                     current = current.parent();
  8099.                 }
  8100.             }
  8101.             return reversePath.reverse();
  8102.         },
  8103.  
  8104.         syncTree: function(args) {
  8105.            
  8106.             if (!args) {
  8107.                 throw "No args object defined for syncTree";
  8108.             }
  8109.             if (!args.node) {
  8110.                 throw "No node defined on args object for syncTree";
  8111.             }
  8112.             if (!args.path) {
  8113.                 throw "No path defined on args object for syncTree";
  8114.             }
  8115.             if (!angular.isArray(args.path)) {
  8116.                 throw "Path must be an array";
  8117.             }
  8118.             if (args.path.length < 1) {
  8119.                 //if there is no path, make -1 the path, and that should sync the tree root
  8120.                 args.path.push("-1");
  8121.             }
  8122.  
  8123.             var deferred = $q.defer();
  8124.  
  8125.             //get the rootNode for the current node, we'll sync based on that
  8126.             var root = this.getTreeRoot(args.node);
  8127.             if (!root) {
  8128.                 throw "Could not get the root tree node based on the node passed in";
  8129.             }
  8130.            
  8131.             //now we want to loop through the ids in the path, first we'll check if the first part
  8132.             //of the path is the root node, otherwise we'll search it's children.
  8133.             var currPathIndex = 0;
  8134.             //if the first id is the root node and there's only one... then consider it synced
  8135.             if (String(args.path[currPathIndex]).toLowerCase() === String(args.node.id).toLowerCase()) {
  8136.                 if (args.path.length === 1) {
  8137.                     //return the root
  8138.                     deferred.resolve(root);
  8139.                     return deferred.promise;
  8140.                 }
  8141.                 else {
  8142.                     //move to the next path part and continue
  8143.                     currPathIndex = 1;
  8144.                 }
  8145.             }
  8146.            
  8147.             //now that we have the first id to lookup, we can start the process
  8148.  
  8149.             var self = this;
  8150.             var node = args.node;
  8151.  
  8152.             var doSync = function () {
  8153.                 //check if it exists in the already loaded children
  8154.                 var child = self.getChildNode(node, args.path[currPathIndex]);
  8155.                 if (child) {
  8156.                     if (args.path.length === (currPathIndex + 1)) {
  8157.                         //woot! synced the node
  8158.                         if (!args.forceReload) {
  8159.                             deferred.resolve(child);
  8160.                         }
  8161.                         else {
  8162.                             //even though we've found the node if forceReload is specified
  8163.                             //we want to go update this single node from the server
  8164.                             self.reloadNode(child).then(function (reloaded) {
  8165.                                 deferred.resolve(reloaded);
  8166.                             }, function () {
  8167.                                 deferred.reject();
  8168.                             });
  8169.                         }
  8170.                     }
  8171.                     else {
  8172.                         //now we need to recurse with the updated node/currPathIndex
  8173.                         currPathIndex++;
  8174.                         node = child;
  8175.                         //recurse
  8176.                         doSync();
  8177.                     }
  8178.                 }
  8179.                 else {
  8180.                     //couldn't find it in the
  8181.                     self.loadNodeChildren({ node: node, section: node.section }).then(function () {
  8182.                         //ok, got the children, let's find it
  8183.                         var found = self.getChildNode(node, args.path[currPathIndex]);
  8184.                         if (found) {
  8185.                             if (args.path.length === (currPathIndex + 1)) {
  8186.                                 //woot! synced the node
  8187.                                 deferred.resolve(found);
  8188.                             }
  8189.                             else {
  8190.                                 //now we need to recurse with the updated node/currPathIndex
  8191.                                 currPathIndex++;
  8192.                                 node = found;
  8193.                                 //recurse
  8194.                                 doSync();
  8195.                             }
  8196.                         }
  8197.                         else {
  8198.                             //fail!
  8199.                             deferred.reject();
  8200.                         }
  8201.                     }, function () {
  8202.                         //fail!
  8203.                         deferred.reject();
  8204.                     });
  8205.                 }
  8206.             };
  8207.  
  8208.             //start
  8209.             doSync();
  8210.            
  8211.             return deferred.promise;
  8212.  
  8213.         }
  8214.        
  8215.     };
  8216. }
  8217.  
  8218. angular.module('umbraco.services').factory('treeService', treeService);
  8219. /**
  8220. * @ngdoc service
  8221. * @name umbraco.services.umbRequestHelper
  8222. * @description A helper object used for sending requests to the server
  8223. **/
  8224. function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService, eventsService) {
  8225.     return {
  8226.  
  8227.         /**
  8228.          * @ngdoc method
  8229.          * @name umbraco.services.umbRequestHelper#convertVirtualToAbsolutePath
  8230.          * @methodOf umbraco.services.umbRequestHelper
  8231.          * @function
  8232.          *
  8233.          * @description
  8234.          * This will convert a virtual path (i.e. ~/App_Plugins/Blah/Test.html ) to an absolute path
  8235.          *
  8236.          * @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
  8237.          */
  8238.         convertVirtualToAbsolutePath: function(virtualPath) {
  8239.             if (virtualPath.startsWith("/")) {
  8240.                 return virtualPath;
  8241.             }
  8242.             if (!virtualPath.startsWith("~/")) {
  8243.                 throw "The path " + virtualPath + " is not a virtual path";
  8244.             }
  8245.             if (!Umbraco.Sys.ServerVariables.application.applicationPath) {
  8246.                 throw "No applicationPath defined in Umbraco.ServerVariables.application.applicationPath";
  8247.             }
  8248.             return Umbraco.Sys.ServerVariables.application.applicationPath + virtualPath.trimStart("~/");
  8249.         },
  8250.  
  8251.         /**
  8252.          * @ngdoc method
  8253.          * @name umbraco.services.umbRequestHelper#dictionaryToQueryString
  8254.          * @methodOf umbraco.services.umbRequestHelper
  8255.          * @function
  8256.          *
  8257.          * @description
  8258.          * This will turn an array of key/value pairs into a query string
  8259.          *
  8260.          * @param {Array} queryStrings An array of key/value pairs
  8261.          */
  8262.         dictionaryToQueryString: function (queryStrings) {
  8263.            
  8264.             if (angular.isArray(queryStrings)) {
  8265.                 return _.map(queryStrings, function (item) {
  8266.                     var key = null;
  8267.                     var val = null;
  8268.                     for (var k in item) {
  8269.                         key = k;
  8270.                         val = item[k];
  8271.                         break;
  8272.                     }
  8273.                     if (key === null || val === null) {
  8274.                         throw "The object in the array was not formatted as a key/value pair";
  8275.                     }
  8276.                     return encodeURIComponent(key) + "=" + encodeURIComponent(val);
  8277.                 }).join("&");
  8278.             }
  8279.             else if (angular.isObject(queryStrings)) {
  8280.  
  8281.                 //this allows for a normal object to be passed in (ie. a dictionary)
  8282.                 return decodeURIComponent($.param(queryStrings));
  8283.             }
  8284.            
  8285.             throw "The queryString parameter is not an array or object of key value pairs";
  8286.         },
  8287.  
  8288.         /**
  8289.          * @ngdoc method
  8290.          * @name umbraco.services.umbRequestHelper#getApiUrl
  8291.          * @methodOf umbraco.services.umbRequestHelper
  8292.          * @function
  8293.          *
  8294.          * @description
  8295.          * This will return the webapi Url for the requested key based on the servervariables collection
  8296.          *
  8297.          * @param {string} apiName The webapi name that is found in the servervariables["umbracoUrls"] dictionary
  8298.          * @param {string} actionName The webapi action name
  8299.          * @param {object} queryStrings Can be either a string or an array containing key/value pairs
  8300.          */
  8301.         getApiUrl: function (apiName, actionName, queryStrings) {
  8302.             if (!Umbraco || !Umbraco.Sys || !Umbraco.Sys.ServerVariables || !Umbraco.Sys.ServerVariables["umbracoUrls"]) {
  8303.                 throw "No server variables defined!";
  8304.             }
  8305.  
  8306.             if (!Umbraco.Sys.ServerVariables["umbracoUrls"][apiName]) {
  8307.                 throw "No url found for api name " + apiName;
  8308.             }
  8309.  
  8310.             return Umbraco.Sys.ServerVariables["umbracoUrls"][apiName] + actionName +
  8311.                 (!queryStrings ? "" : "?" + (angular.isString(queryStrings) ? queryStrings : this.dictionaryToQueryString(queryStrings)));
  8312.  
  8313.         },
  8314.  
  8315.         /**
  8316.          * @ngdoc function
  8317.          * @name umbraco.services.umbRequestHelper#resourcePromise
  8318.          * @methodOf umbraco.services.umbRequestHelper
  8319.          * @function
  8320.          *
  8321.          * @description
  8322.          * This returns a promise with an underlying http call, it is a helper method to reduce
  8323.          *  the amount of duplicate code needed to query http resources and automatically handle any
  8324.          *  500 Http server errors.
  8325.          *
  8326.          * @param {object} opts A mixed object which can either be a `string` representing the error message to be
  8327.          *   returned OR an `object` containing either:
  8328.          *     { success: successCallback, errorMsg: errorMessage }
  8329.          *          OR
  8330.          *     { success: successCallback, error: errorCallback }
  8331.          *   In both of the above, the successCallback must accept these parameters: `data`, `status`, `headers`, `config`
  8332.          *   If using the errorCallback it must accept these parameters: `data`, `status`, `headers`, `config`
  8333.          *   The success callback must return the data which will be resolved by the deferred object.
  8334.          *   The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status }
  8335.          */
  8336.         resourcePromise: function (httpPromise, opts) {
  8337.             var deferred = $q.defer();
  8338.  
  8339.             /** The default success callback used if one is not supplied in the opts */
  8340.             function defaultSuccess(data, status, headers, config) {
  8341.                 //when it's successful, just return the data
  8342.                 return data;
  8343.             }
  8344.  
  8345.             /** The default error callback used if one is not supplied in the opts */
  8346.             function defaultError(data, status, headers, config) {
  8347.                 return {
  8348.                     //NOTE: the default error message here should never be used based on the above docs!
  8349.                     errorMsg: (angular.isString(opts) ? opts : 'An error occurred!'),
  8350.                     data: data,
  8351.                     status: status
  8352.                 };
  8353.             }
  8354.  
  8355.             //create the callbacs based on whats been passed in.
  8356.             var callbacks = {
  8357.                 success: ((!opts || !opts.success) ? defaultSuccess : opts.success),
  8358.                 error: ((!opts || !opts.error) ? defaultError : opts.error)
  8359.             };
  8360.  
  8361.             httpPromise.success(function (data, status, headers, config) {
  8362.  
  8363.                 //invoke the callback
  8364.                 var result = callbacks.success.apply(this, [data, status, headers, config]);
  8365.  
  8366.                 //when it's successful, just return the data
  8367.                 deferred.resolve(result);
  8368.  
  8369.             }).error(function (data, status, headers, config) {
  8370.  
  8371.                 //invoke the callback
  8372.                 var result = callbacks.error.apply(this, [data, status, headers, config]);
  8373.  
  8374.                 //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled.
  8375.                 if (status >= 500 && status < 600) {
  8376.  
  8377.                     //show a ysod dialog
  8378.                     if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) {
  8379.                         eventsService.emit('app.ysod',
  8380.                         {
  8381.                             errorMsg: 'An error occured',
  8382.                             data: data
  8383.                         });
  8384.                     }
  8385.                     else {
  8386.                         //show a simple error notification                        
  8387.                         notificationsService.error("Server error", "Contact administrator, see log for full details.<br/><i>" + result.errorMsg + "</i>");
  8388.                     }
  8389.                    
  8390.                 }
  8391.  
  8392.                 //return an error object including the error message for UI
  8393.                 deferred.reject({
  8394.                     errorMsg: result.errorMsg,
  8395.                     data: result.data,
  8396.                     status: result.status
  8397.                 });
  8398.  
  8399.  
  8400.             });
  8401.  
  8402.             return deferred.promise;
  8403.  
  8404.         },
  8405.  
  8406.         /** Used for saving media/content specifically */
  8407.         postSaveContent: function (args) {
  8408.  
  8409.             if (!args.restApiUrl) {
  8410.                 throw "args.restApiUrl is a required argument";
  8411.             }
  8412.             if (!args.content) {
  8413.                 throw "args.content is a required argument";
  8414.             }
  8415.             if (!args.action) {
  8416.                 throw "args.action is a required argument";
  8417.             }
  8418.             if (!args.files) {
  8419.                 throw "args.files is a required argument";
  8420.             }
  8421.             if (!args.dataFormatter) {
  8422.                 throw "args.dataFormatter is a required argument";
  8423.             }
  8424.  
  8425.  
  8426.             var deferred = $q.defer();
  8427.  
  8428.             //save the active tab id so we can set it when the data is returned.
  8429.             var activeTab = _.find(args.content.tabs, function (item) {
  8430.                 return item.active;
  8431.             });
  8432.             var activeTabIndex = (activeTab === undefined ? 0 : _.indexOf(args.content.tabs, activeTab));
  8433.  
  8434.             //save the data
  8435.             this.postMultiPartRequest(
  8436.                 args.restApiUrl,
  8437.                 { key: "contentItem", value: args.dataFormatter(args.content, args.action) },
  8438.                 function (data, formData) {
  8439.                     //now add all of the assigned files
  8440.                     for (var f in args.files) {
  8441.                         //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key
  8442.                         // so we know which property it belongs to on the server side
  8443.                         formData.append("file_" + args.files[f].alias, args.files[f].file);
  8444.                     }
  8445.  
  8446.                 },
  8447.                 function (data, status, headers, config) {
  8448.                     //success callback
  8449.  
  8450.                     //reset the tabs and set the active one
  8451.                     _.each(data.tabs, function (item) {
  8452.                         item.active = false;
  8453.                     });
  8454.                     data.tabs[activeTabIndex].active = true;
  8455.  
  8456.                     //the data returned is the up-to-date data so the UI will refresh
  8457.                     deferred.resolve(data);
  8458.                 },
  8459.                 function (data, status, headers, config) {
  8460.                     //failure callback
  8461.  
  8462.                     //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled.
  8463.                     if (status >= 500 && status < 600) {
  8464.  
  8465.                         //This is a bit of a hack to check if the error is due to a file being uploaded that is too large,
  8466.                         // we have to just check for the existence of a string value but currently that is the best way to
  8467.                         // do this since it's very hacky/difficult to catch this on the server
  8468.                         if (typeof data !== "undefined" && typeof data.indexOf === "function" && data.indexOf("Maximum request length exceeded") >= 0) {
  8469.                             notificationsService.error("Server error", "The uploaded file was too large, check with your site administrator to adjust the maximum size allowed");
  8470.                         }                        
  8471.                         else if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) {
  8472.                             //show a ysod dialog
  8473.                             eventsService.emit('app.ysod',
  8474.                             {
  8475.                                 errorMsg: 'An error occured',
  8476.                                 data: data
  8477.                             });
  8478.                         }
  8479.                         else {
  8480.                             //show a simple error notification                        
  8481.                             notificationsService.error("Server error", "Contact administrator, see log for full details.<br/><i>" + data.ExceptionMessage + "</i>");
  8482.                         }
  8483.                        
  8484.                     }
  8485.                    
  8486.                     //return an error object including the error message for UI
  8487.                     deferred.reject({
  8488.                         errorMsg: 'An error occurred',
  8489.                         data: data,
  8490.                         status: status
  8491.                     });
  8492.                    
  8493.  
  8494.                 });
  8495.  
  8496.             return deferred.promise;
  8497.         },
  8498.  
  8499.         /** Posts a multi-part mime request to the server */
  8500.         postMultiPartRequest: function (url, jsonData, transformCallback, successCallback, failureCallback) {
  8501.  
  8502.             //validate input, jsonData can be an array of key/value pairs or just one key/value pair.
  8503.             if (!jsonData) { throw "jsonData cannot be null"; }
  8504.  
  8505.             if (angular.isArray(jsonData)) {
  8506.                 _.each(jsonData, function (item) {
  8507.                     if (!item.key || !item.value) { throw "jsonData array item must have both a key and a value property"; }
  8508.                 });
  8509.             }
  8510.             else if (!jsonData.key || !jsonData.value) { throw "jsonData object must have both a key and a value property"; }
  8511.  
  8512.  
  8513.             $http({
  8514.                 method: 'POST',
  8515.                 url: url,
  8516.                 //IMPORTANT!!! You might think this should be set to 'multipart/form-data' but this is not true because when we are sending up files
  8517.                 // the request needs to include a 'boundary' parameter which identifies the boundary name between parts in this multi-part request
  8518.                 // and setting the Content-type manually will not set this boundary parameter. For whatever reason, setting the Content-type to 'false'
  8519.                 // will force the request to automatically populate the headers properly including the boundary parameter.
  8520.                 headers: { 'Content-Type': false },
  8521.                 transformRequest: function (data) {
  8522.                     var formData = new FormData();
  8523.                     //add the json data
  8524.                     if (angular.isArray(data)) {
  8525.                         _.each(data, function (item) {
  8526.                             formData.append(item.key, !angular.isString(item.value) ? angular.toJson(item.value) : item.value);
  8527.                         });
  8528.                     }
  8529.                     else {
  8530.                         formData.append(data.key, !angular.isString(data.value) ? angular.toJson(data.value) : data.value);
  8531.                     }
  8532.  
  8533.                     //call the callback
  8534.                     if (transformCallback) {
  8535.                         transformCallback.apply(this, [data, formData]);
  8536.                     }
  8537.  
  8538.                     return formData;
  8539.                 },
  8540.                 data: jsonData
  8541.             }).
  8542.             success(function (data, status, headers, config) {
  8543.                 if (successCallback) {
  8544.                     successCallback.apply(this, [data, status, headers, config]);
  8545.                 }
  8546.             }).
  8547.             error(function (data, status, headers, config) {
  8548.                 if (failureCallback) {
  8549.                     failureCallback.apply(this, [data, status, headers, config]);
  8550.                 }
  8551.             });
  8552.         }
  8553.     };
  8554. }
  8555. angular.module('umbraco.services').factory('umbRequestHelper', umbRequestHelper);
  8556.  
  8557. angular.module('umbraco.services')
  8558.     .factory('userService', function ($rootScope, eventsService, $q, $location, $log, securityRetryQueue, authResource, dialogService, $timeout, angularHelper, $http) {
  8559.  
  8560.         var currentUser = null;
  8561.         var lastUserId = null;
  8562.         var loginDialog = null;
  8563.         //this tracks the last date/time that the user's remainingAuthSeconds was updated from the server
  8564.         // this is used so that we know when to go and get the user's remaining seconds directly.
  8565.         var lastServerTimeoutSet = null;
  8566.  
  8567.         function openLoginDialog(isTimedOut) {
  8568.             if (!loginDialog) {
  8569.                 loginDialog = dialogService.open({
  8570.  
  8571.                     //very special flag which means that global events cannot close this dialog
  8572.                     manualClose: true,
  8573.  
  8574.                     template: 'views/common/dialogs/login.html',
  8575.                     modalClass: "login-overlay",
  8576.                     animation: "slide",
  8577.                     show: true,
  8578.                     callback: onLoginDialogClose,
  8579.                     dialogData: {
  8580.                         isTimedOut: isTimedOut
  8581.                     }
  8582.                 });
  8583.             }
  8584.         }
  8585.  
  8586.         function onLoginDialogClose(success) {
  8587.             loginDialog = null;
  8588.  
  8589.             if (success) {
  8590.                 securityRetryQueue.retryAll(currentUser.name);
  8591.             }
  8592.             else {
  8593.                 securityRetryQueue.cancelAll();
  8594.                 $location.path('/');
  8595.             }
  8596.         }
  8597.  
  8598.         /**
  8599.         This methods will set the current user when it is resolved and
  8600.         will then start the counter to count in-memory how many seconds they have
  8601.         remaining on the auth session
  8602.         */
  8603.         function setCurrentUser(usr) {
  8604.             if (!usr.remainingAuthSeconds) {
  8605.                 throw "The user object is invalid, the remainingAuthSeconds is required.";
  8606.             }
  8607.             currentUser = usr;
  8608.             lastServerTimeoutSet = new Date();
  8609.             //start the timer
  8610.             countdownUserTimeout();
  8611.         }
  8612.  
  8613.         /**
  8614.         Method to count down the current user's timeout seconds,
  8615.         this will continually count down their current remaining seconds every 5 seconds until
  8616.         there are no more seconds remaining.
  8617.         */
  8618.         function countdownUserTimeout() {
  8619.  
  8620.             $timeout(function () {
  8621.  
  8622.                 if (currentUser) {
  8623.                     //countdown by 5 seconds since that is how long our timer is for.
  8624.                     currentUser.remainingAuthSeconds -= 5;
  8625.  
  8626.                     //if there are more than 30 remaining seconds, recurse!
  8627.                     if (currentUser.remainingAuthSeconds > 30) {
  8628.  
  8629.                         //we need to check when the last time the timeout was set from the server, if
  8630.                         // it has been more than 30 seconds then we'll manually go and retrieve it from the
  8631.                         // server - this helps to keep our local countdown in check with the true timeout.
  8632.                         if (lastServerTimeoutSet != null) {
  8633.                             var now = new Date();
  8634.                             var seconds = (now.getTime() - lastServerTimeoutSet.getTime()) / 1000;
  8635.  
  8636.                             if (seconds > 30) {
  8637.  
  8638.                                 //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
  8639.                                 // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
  8640.                                 lastServerTimeoutSet = null;
  8641.  
  8642.                                 //now go get it from the server
  8643.                                 //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
  8644.                                 angularHelper.safeApply($rootScope, function () {
  8645.                                     authResource.getRemainingTimeoutSeconds().then(function (result) {
  8646.                                         setUserTimeoutInternal(result);
  8647.                                     });
  8648.                                 });
  8649.                             }
  8650.                         }
  8651.  
  8652.                         //recurse the countdown!
  8653.                         countdownUserTimeout();
  8654.                     }
  8655.                     else {
  8656.  
  8657.                         //we are either timed out or very close to timing out so we need to show the login dialog.
  8658.                         if (Umbraco.Sys.ServerVariables.umbracoSettings.keepUserLoggedIn !== true) {
  8659.                             //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
  8660.                             angularHelper.safeApply($rootScope, function () {
  8661.                                 try {
  8662.                                     //NOTE: We are calling this again so that the server can create a log that the timeout has expired, we
  8663.                                     // don't actually care about this result.
  8664.                                     authResource.getRemainingTimeoutSeconds();
  8665.                                 }
  8666.                                 finally {
  8667.                                     userAuthExpired();
  8668.                                 }
  8669.                             });
  8670.                         }
  8671.                         else {
  8672.                             //we've got less than 30 seconds remaining so let's check the server
  8673.  
  8674.                             if (lastServerTimeoutSet != null) {
  8675.                                 //first we'll set the lastServerTimeoutSet to null - this is so we don't get back in to this loop while we
  8676.                                 // wait for a response from the server otherwise we'll be making double/triple/etc... calls while we wait.
  8677.                                 lastServerTimeoutSet = null;
  8678.  
  8679.                                 //now go get it from the server
  8680.                                 //NOTE: the safeApply because our timeout is set to not run digests (performance reasons)
  8681.                                 angularHelper.safeApply($rootScope, function () {
  8682.                                     authResource.getRemainingTimeoutSeconds().then(function (result) {
  8683.                                         setUserTimeoutInternal(result);
  8684.                                     });
  8685.                                 });
  8686.                             }
  8687.  
  8688.                             //recurse the countdown!
  8689.                             countdownUserTimeout();
  8690.  
  8691.                         }
  8692.                     }
  8693.                 }
  8694.             }, 5000, //every 5 seconds
  8695.                 false); //false = do NOT execute a digest for every iteration
  8696.         }
  8697.  
  8698.         /** Called to update the current user's timeout */
  8699.         function setUserTimeoutInternal(newTimeout) {
  8700.  
  8701.  
  8702.             var asNumber = parseFloat(newTimeout);
  8703.             if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) {
  8704.                 currentUser.remainingAuthSeconds = newTimeout;
  8705.                 lastServerTimeoutSet = new Date();
  8706.             }
  8707.         }
  8708.  
  8709.         /** resets all user data, broadcasts the notAuthenticated event and shows the login dialog */
  8710.         function userAuthExpired(isLogout) {
  8711.             //store the last user id and clear the user
  8712.             if (currentUser && currentUser.id !== undefined) {
  8713.                 lastUserId = currentUser.id;
  8714.             }
  8715.  
  8716.             if (currentUser) {
  8717.                 currentUser.remainingAuthSeconds = 0;
  8718.             }
  8719.  
  8720.             lastServerTimeoutSet = null;
  8721.             currentUser = null;
  8722.  
  8723.             //broadcast a global event that the user is no longer logged in
  8724.             eventsService.emit("app.notAuthenticated");
  8725.  
  8726.             openLoginDialog(isLogout === undefined ? true : !isLogout);
  8727.         }
  8728.  
  8729.         // Register a handler for when an item is added to the retry queue
  8730.         securityRetryQueue.onItemAddedCallbacks.push(function (retryItem) {
  8731.             if (securityRetryQueue.hasMore()) {
  8732.                 userAuthExpired();
  8733.             }
  8734.         });
  8735.  
  8736.         return {
  8737.  
  8738.             /** Internal method to display the login dialog */
  8739.             _showLoginDialog: function () {
  8740.                 openLoginDialog();
  8741.             },
  8742.  
  8743.             /** Returns a promise, sends a request to the server to check if the current cookie is authorized  */
  8744.             isAuthenticated: function () {
  8745.                 //if we've got a current user then just return true
  8746.                 if (currentUser) {
  8747.                     var deferred = $q.defer();
  8748.                     deferred.resolve(true);
  8749.                     return deferred.promise;
  8750.                 }
  8751.                 return authResource.isAuthenticated();
  8752.             },
  8753.  
  8754.             /** Returns a promise, sends a request to the server to validate the credentials  */
  8755.             authenticate: function (login, password) {
  8756.  
  8757.                 return authResource.performLogin(login, password)
  8758.                     .then(function (data) {
  8759.  
  8760.                         //when it's successful, return the user data
  8761.                         setCurrentUser(data);
  8762.  
  8763.                         var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "credentials" };
  8764.  
  8765.                         //broadcast a global event
  8766.                         eventsService.emit("app.authenticated", result);
  8767.                         return result;
  8768.                     });
  8769.             },
  8770.  
  8771.             /** Logs the user out
  8772.              */
  8773.             logout: function () {
  8774.  
  8775.                 return authResource.performLogout()
  8776.                     .then(function(data) {
  8777.                         userAuthExpired();
  8778.                         //done!
  8779.                         return null;
  8780.                     });
  8781.             },
  8782.  
  8783.             /** Returns the current user object in a promise  */
  8784.             getCurrentUser: function (args) {
  8785.                 var deferred = $q.defer();
  8786.  
  8787.                 if (!currentUser) {
  8788.                     authResource.getCurrentUser()
  8789.                         .then(function (data) {
  8790.  
  8791.                             var result = { user: data, authenticated: true, lastUserId: lastUserId, loginType: "implicit" };
  8792.  
  8793.                             //TODO: This is a mega backwards compatibility hack... These variables SHOULD NOT exist in the server variables
  8794.                             // since they are not supposed to be dynamic but I accidentally added them there in 7.1.5 IIRC so some people might
  8795.                             // now be relying on this :(
  8796.                             if (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables) {
  8797.                                 Umbraco.Sys.ServerVariables["security"] = {
  8798.                                     startContentId: data.startContentId,
  8799.                                     startMediaId: data.startMediaId
  8800.                                 };
  8801.                             }
  8802.  
  8803.                             if (args && args.broadcastEvent) {
  8804.                                 //broadcast a global event, will inform listening controllers to load in the user specific data
  8805.                                 eventsService.emit("app.authenticated", result);
  8806.                             }
  8807.  
  8808.                             setCurrentUser(data);
  8809.  
  8810.                             deferred.resolve(currentUser);
  8811.                         });
  8812.  
  8813.                 }
  8814.                 else {
  8815.                     deferred.resolve(currentUser);
  8816.                 }
  8817.  
  8818.                 return deferred.promise;
  8819.             },
  8820.  
  8821.             /** 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 */
  8822.             setUserTimeout: function (newTimeout) {
  8823.                 setUserTimeoutInternal(newTimeout);
  8824.             }
  8825.         };
  8826.  
  8827.     });
  8828.  
  8829. /*Contains multiple services for various helper tasks */
  8830. function versionHelper() {
  8831.  
  8832.     return {
  8833.  
  8834.         //see: https://gist.github.com/TheDistantSea/8021359
  8835.         versionCompare: function(v1, v2, options) {
  8836.             var lexicographical = options && options.lexicographical,
  8837.                 zeroExtend = options && options.zeroExtend,
  8838.                 v1parts = v1.split('.'),
  8839.                 v2parts = v2.split('.');
  8840.  
  8841.             function isValidPart(x) {
  8842.                 return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
  8843.             }
  8844.  
  8845.             if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
  8846.                 return NaN;
  8847.             }
  8848.  
  8849.             if (zeroExtend) {
  8850.                 while (v1parts.length < v2parts.length) {
  8851.                     v1parts.push("0");
  8852.                 }
  8853.                 while (v2parts.length < v1parts.length) {
  8854.                     v2parts.push("0");
  8855.                 }
  8856.             }
  8857.  
  8858.             if (!lexicographical) {
  8859.                 v1parts = v1parts.map(Number);
  8860.                 v2parts = v2parts.map(Number);
  8861.             }
  8862.  
  8863.             for (var i = 0; i < v1parts.length; ++i) {
  8864.                 if (v2parts.length === i) {
  8865.                     return 1;
  8866.                 }
  8867.  
  8868.                 if (v1parts[i] === v2parts[i]) {
  8869.                     continue;
  8870.                 }
  8871.                 else if (v1parts[i] > v2parts[i]) {
  8872.                     return 1;
  8873.                 }
  8874.                 else {
  8875.                     return -1;
  8876.                 }
  8877.             }
  8878.  
  8879.             if (v1parts.length !== v2parts.length) {
  8880.                 return -1;
  8881.             }
  8882.  
  8883.             return 0;
  8884.         }
  8885.     };
  8886. }
  8887. angular.module('umbraco.services').factory('versionHelper', versionHelper);
  8888.  
  8889. function dateHelper() {
  8890.  
  8891.     return {
  8892.        
  8893.         convertToServerStringTime: function(momentLocal, serverOffsetMinutes, format) {
  8894.  
  8895.             //get the formatted offset time in HH:mm (server time offset is in minutes)
  8896.             var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") +
  8897.                 moment()
  8898.                 .startOf('day')
  8899.                 .minutes(Math.abs(serverOffsetMinutes))
  8900.                 .format('HH:mm');
  8901.  
  8902.             var server = moment.utc(momentLocal).utcOffset(formattedOffset);
  8903.             return server.format(format ? format : "YYYY-MM-DD HH:mm:ss");
  8904.         },
  8905.  
  8906.         convertToLocalMomentTime: function (strVal, serverOffsetMinutes) {
  8907.  
  8908.             //get the formatted offset time in HH:mm (server time offset is in minutes)
  8909.             var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") +
  8910.                 moment()
  8911.                 .startOf('day')
  8912.                 .minutes(Math.abs(serverOffsetMinutes))
  8913.                 .format('HH:mm');
  8914.  
  8915.             //convert to the iso string format
  8916.             var isoFormat = moment(strVal).format("YYYY-MM-DDTHH:mm:ss") + formattedOffset;
  8917.  
  8918.             //create a moment with the iso format which will include the offset with the correct time
  8919.             // then convert it to local time
  8920.             return moment.parseZone(isoFormat).local();
  8921.         }
  8922.  
  8923.     };
  8924. }
  8925. angular.module('umbraco.services').factory('dateHelper', dateHelper);
  8926.  
  8927. function packageHelper(assetsService, treeService, eventsService, $templateCache) {
  8928.  
  8929.     return {
  8930.  
  8931.         /** Called when a package is installed, this resets a bunch of data and ensures the new package assets are loaded in */
  8932.         packageInstalled: function () {
  8933.  
  8934.             //clears the tree
  8935.             treeService.clearCache();
  8936.  
  8937.             //clears the template cache
  8938.             $templateCache.removeAll();
  8939.  
  8940.             //emit event to notify anything else
  8941.             eventsService.emit("app.reInitialize");
  8942.         }
  8943.  
  8944.     };
  8945. }
  8946. angular.module('umbraco.services').factory('packageHelper', packageHelper);
  8947.  
  8948. //TODO: I believe this is obsolete
  8949. function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) {
  8950.     return {
  8951.         /** sets the image's url, thumbnail and if its a folder */
  8952.         setImageData: function(img) {
  8953.            
  8954.             img.isFolder = !mediaHelper.hasFilePropertyType(img);
  8955.  
  8956.             if(!img.isFolder){
  8957.                 img.thumbnail = mediaHelper.resolveFile(img, true);
  8958.                 img.image = mediaHelper.resolveFile(img, false);    
  8959.             }
  8960.         },
  8961.  
  8962.         /** sets the images original size properties - will check if it is a folder and if so will just make it square */
  8963.         setOriginalSize: function(img, maxHeight) {
  8964.             //set to a square by default
  8965.             img.originalWidth = maxHeight;
  8966.             img.originalHeight = maxHeight;
  8967.  
  8968.             var widthProp = _.find(img.properties, function(v) { return (v.alias === "umbracoWidth"); });
  8969.             if (widthProp && widthProp.value) {
  8970.                 img.originalWidth = parseInt(widthProp.value, 10);
  8971.                 if (isNaN(img.originalWidth)) {
  8972.                     img.originalWidth = maxHeight;
  8973.                 }
  8974.             }
  8975.             var heightProp = _.find(img.properties, function(v) { return (v.alias === "umbracoHeight"); });
  8976.             if (heightProp && heightProp.value) {
  8977.                 img.originalHeight = parseInt(heightProp.value, 10);
  8978.                 if (isNaN(img.originalHeight)) {
  8979.                     img.originalHeight = maxHeight;
  8980.                 }
  8981.             }
  8982.         },
  8983.  
  8984.         /** sets the image style which get's used in the angular markup */
  8985.         setImageStyle: function(img, width, height, rightMargin, bottomMargin) {
  8986.             img.style = { width: width + "px", height: height + "px", "margin-right": rightMargin + "px", "margin-bottom": bottomMargin + "px" };
  8987.             img.thumbStyle = {
  8988.                 "background-image": "url('" + img.thumbnail + "')",
  8989.                 "background-repeat": "no-repeat",
  8990.                 "background-position": "center",
  8991.                 "background-size": Math.min(width, img.originalWidth) + "px " + Math.min(height, img.originalHeight) + "px"
  8992.             };
  8993.         },
  8994.  
  8995.         /** gets the image's scaled wdith based on the max row height */
  8996.         getScaledWidth: function(img, maxHeight) {
  8997.             var scaled = img.originalWidth * maxHeight / img.originalHeight;
  8998.             return scaled;
  8999.             //round down, we don't want it too big even by half a pixel otherwise it'll drop to the next row
  9000.             //return Math.floor(scaled);
  9001.         },
  9002.  
  9003.         /** returns the target row width taking into account how many images will be in the row and removing what the margin is */
  9004.         getTargetWidth: function(imgsPerRow, maxRowWidth, margin) {
  9005.             //take into account the margin, we will have 1 less margin item than we have total images
  9006.             return (maxRowWidth - ((imgsPerRow - 1) * margin));
  9007.         },
  9008.  
  9009.         /**
  9010.             This will determine the row/image height for the next collection of images which takes into account the
  9011.             ideal image count per row. It will check if a row can be filled with this ideal count and if not - if there
  9012.             are additional images available to fill the row it will keep calculating until they fit.
  9013.  
  9014.             It will return the calculated height and the number of images for the row.
  9015.  
  9016.             targetHeight = optional;
  9017.         */
  9018.         getRowHeightForImages: function(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) {
  9019.  
  9020.             var idealImages = imgs.slice(0, idealImgPerRow);
  9021.             //get the target row width without margin
  9022.             var targetRowWidth = this.getTargetWidth(idealImages.length, maxRowWidth, margin);
  9023.             //this gets the image with the smallest height which equals the maximum we can scale up for this image block
  9024.             var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight);
  9025.             //if the max scale height is smaller than the min display height, we'll use the min display height
  9026.             targetHeight =  targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight);
  9027.            
  9028.             var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight);
  9029.  
  9030.             if (attemptedRowHeight != null) {
  9031.  
  9032.                 //if this is smaller than the min display then we need to use the min display,
  9033.                 // which means we'll need to remove one from the row so we can scale up to fill the row
  9034.                 if (attemptedRowHeight < minDisplayHeight) {
  9035.  
  9036.                     if (idealImages.length > 1) {
  9037.                        
  9038.                         //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight                        
  9039.                         targetHeight += Math.floor((maxRowHeight - targetHeight) / 2);
  9040.                         return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight);
  9041.                     }
  9042.                     else {                        
  9043.                         //this will occur when we only have one image remaining in the row but it's still going to be too wide even when
  9044.                         // using the minimum display height specified. In this case we're going to have to just crop the image in it's center
  9045.                         // using the minimum display height and the full row width
  9046.                         return { height: minDisplayHeight, imgCount: 1 };
  9047.                     }
  9048.                 }
  9049.                 else {
  9050.                     //success!
  9051.                     return { height: attemptedRowHeight, imgCount: idealImages.length };
  9052.                 }
  9053.             }
  9054.  
  9055.             //we know the width will fit in a row, but we now need to figure out if we can fill
  9056.             // the entire row in the case that we have more images remaining than the idealImgPerRow.
  9057.  
  9058.             if (idealImages.length === imgs.length) {
  9059.                 //we have no more remaining images to fill the space, so we'll just use the calc height
  9060.                 return { height: targetHeight, imgCount: idealImages.length };
  9061.             }
  9062.             else if (idealImages.length === 1) {
  9063.                 //this will occur when we only have one image remaining in the row to process but it's not really going to fit ideally
  9064.                 // in the row.
  9065.                 return { height: minDisplayHeight, imgCount: 1 };
  9066.             }
  9067.             else if (idealImages.length === idealImgPerRow && targetHeight < maxRowHeight) {
  9068.  
  9069.                 //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
  9070.                 // long as the targetHeight is currently less than the maxRowHeight. The scale up will be half-way between our current
  9071.                 // target height and the maxRowHeight (we won't loop forever though - if there's a difference of 5 px we'll just quit)
  9072.                
  9073.                 while (targetHeight < maxRowHeight && (maxRowHeight - targetHeight) > 5) {
  9074.                     targetHeight += Math.floor((maxRowHeight - targetHeight) / 2);
  9075.                     attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight);
  9076.                     if (attemptedRowHeight != null) {
  9077.                         //success!
  9078.                         return { height: attemptedRowHeight, imgCount: idealImages.length };
  9079.                     }
  9080.                 }
  9081.  
  9082.                 //Ok, we couldn't actually scale it up with the ideal row count we'll just recurse with a lesser image count.
  9083.                 return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin);
  9084.             }
  9085.             else if (targetHeight === maxRowHeight) {
  9086.  
  9087.                 //This is going to happen when:
  9088.                 // * We can fit a list of images in a row, but they come up too short (based on minDisplayHeight)
  9089.                 // * 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
  9090.                 //      maximum height (maxRowHeight)
  9091.                 // * 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
  9092.                 //      which is better than rendering a row that is shorter than the minimum since that could be quite small.
  9093.  
  9094.                 return { height: targetHeight, imgCount: idealImages.length };
  9095.             }
  9096.             else {
  9097.  
  9098.                 //we have additional images so we'll recurse and add 1 to the idealImgPerRow until it fits
  9099.                 return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow + 1, margin);
  9100.             }
  9101.  
  9102.         },
  9103.  
  9104.         performGetRowHeight: function(idealImages, targetRowWidth, minDisplayHeight, targetHeight) {
  9105.  
  9106.             var currRowWidth = 0;
  9107.  
  9108.             for (var i = 0; i < idealImages.length; i++) {
  9109.                 var scaledW = this.getScaledWidth(idealImages[i], targetHeight);
  9110.                 currRowWidth += scaledW;
  9111.             }
  9112.  
  9113.             if (currRowWidth > targetRowWidth) {
  9114.                 //get the new scaled height to fit
  9115.                 var newHeight = targetRowWidth * targetHeight / currRowWidth;
  9116.                
  9117.                 return newHeight;
  9118.             }
  9119.             else if (idealImages.length === 1 && (currRowWidth <= targetRowWidth) && !idealImages[0].isFolder) {
  9120.                 //if there is only one image, then return the target height
  9121.                 return targetHeight;
  9122.             }
  9123.             else if (currRowWidth / targetRowWidth > 0.90) {
  9124.                 //it's close enough, it's at least 90% of the width so we'll accept it with the target height
  9125.                 return targetHeight;
  9126.             }
  9127.             else {
  9128.                 //if it's not successful, return null
  9129.                 return null;
  9130.             }
  9131.         },
  9132.  
  9133.         /** builds an image grid row */
  9134.         buildRow: function(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) {
  9135.             var currRowWidth = 0;
  9136.             var row = { images: [] };
  9137.  
  9138.             var imageRowHeight = this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin);
  9139.             var targetWidth = this.getTargetWidth(imageRowHeight.imgCount, maxRowWidth, margin);
  9140.  
  9141.             var sizes = [];
  9142.             //loop through the images we know fit into the height
  9143.             for (var i = 0; i < imageRowHeight.imgCount; i++) {
  9144.                 //get the lower width to ensure it always fits
  9145.                 var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height));
  9146.                
  9147.                 if (currRowWidth + scaledWidth <= targetWidth) {
  9148.                     currRowWidth += scaledWidth;                    
  9149.                     sizes.push({
  9150.                         width:scaledWidth,
  9151.                         //ensure that the height is rounded
  9152.                         height: Math.round(imageRowHeight.height)
  9153.                     });
  9154.                     row.images.push(imgs[i]);
  9155.                 }
  9156.                 else if (imageRowHeight.imgCount === 1 && row.images.length === 0) {
  9157.                     //the image is simply too wide, we'll crop/center it
  9158.                     sizes.push({
  9159.                         width: maxRowWidth,
  9160.                         //ensure that the height is rounded
  9161.                         height: Math.round(imageRowHeight.height)
  9162.                     });
  9163.                     row.images.push(imgs[i]);
  9164.                 }
  9165.                 else {
  9166.                     //the max width has been reached
  9167.                     break;
  9168.                 }
  9169.             }
  9170.  
  9171.             //loop through the images for the row and apply the styles
  9172.             for (var j = 0; j < row.images.length; j++) {
  9173.                 var bottomMargin = margin;
  9174.                 //make the margin 0 for the last one
  9175.                 if (j === (row.images.length - 1)) {
  9176.                     margin = 0;
  9177.                 }
  9178.                 this.setImageStyle(row.images[j], sizes[j].width, sizes[j].height, margin, bottomMargin);
  9179.             }
  9180.  
  9181.             if (row.images.length === 1 && totalRemaining > 1) {
  9182.                 //if there's only one image on the row and there are more images remaining, set the container to max width
  9183.                 row.images[0].style.width = maxRowWidth + "px";
  9184.             }
  9185.            
  9186.  
  9187.             return row;
  9188.         },
  9189.  
  9190.         /** Returns the maximum image scaling height for the current image collection */
  9191.         getMaxScaleableHeight: function(imgs, maxRowHeight) {
  9192.  
  9193.             var smallestHeight = _.min(imgs, function(item) { return item.originalHeight; }).originalHeight;
  9194.  
  9195.             //adjust the smallestHeight if it is larger than the static max row height
  9196.             if (smallestHeight > maxRowHeight) {
  9197.                 smallestHeight = maxRowHeight;
  9198.             }
  9199.             return smallestHeight;
  9200.         },
  9201.  
  9202.         /** Creates the image grid with calculated widths/heights for images to fill the grid nicely */
  9203.         buildGrid: function(images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin,imagesOnly) {
  9204.  
  9205.             var rows = [];
  9206.             var imagesProcessed = 0;
  9207.  
  9208.             //first fill in all of the original image sizes and URLs
  9209.             for (var i = startingIndex; i < images.length; i++) {
  9210.                 var item = images[i];
  9211.  
  9212.                 this.setImageData(item);
  9213.                 this.setOriginalSize(item, maxRowHeight);
  9214.  
  9215.                 if(imagesOnly && !item.isFolder && !item.thumbnail){
  9216.                     images.splice(i, 1);
  9217.                     i--;
  9218.                 }
  9219.             }
  9220.  
  9221.             while ((imagesProcessed + startingIndex) < images.length) {
  9222.                 //get the maxHeight for the current un-processed images
  9223.                 var currImgs = images.slice(imagesProcessed);
  9224.  
  9225.                 //build the row
  9226.                 var remaining = images.length - imagesProcessed;
  9227.                 var row = this.buildRow(currImgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, remaining);
  9228.                 if (row.images.length > 0) {
  9229.                     rows.push(row);
  9230.                     imagesProcessed += row.images.length;
  9231.                 }
  9232.                 else {
  9233.  
  9234.                     if (currImgs.length > 0) {
  9235.                         throw "Could not fill grid with all images, images remaining: " + currImgs.length;
  9236.                     }
  9237.  
  9238.                     //if there was nothing processed, exit
  9239.                     break;
  9240.                 }
  9241.             }
  9242.  
  9243.             return rows;
  9244.         }
  9245.     };
  9246. }
  9247. angular.module("umbraco.services").factory("umbPhotoFolderHelper", umbPhotoFolderHelper);
  9248.  
  9249. /**
  9250.  * @ngdoc function
  9251.  * @name umbraco.services.umbModelMapper
  9252.  * @function
  9253.  *
  9254.  * @description
  9255.  * Utility class to map/convert models
  9256.  */
  9257. function umbModelMapper() {
  9258.  
  9259.     return {
  9260.  
  9261.  
  9262.         /**
  9263.          * @ngdoc function
  9264.          * @name umbraco.services.umbModelMapper#convertToEntityBasic
  9265.          * @methodOf umbraco.services.umbModelMapper
  9266.          * @function
  9267.          *
  9268.          * @description
  9269.          * Converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model.
  9270.          * @param {Object} source The source model
  9271.          * @param {Number} source.id The node id of the model
  9272.          * @param {String} source.name The node name
  9273.          * @param {String} source.icon The models icon as a css class (.icon-doc)
  9274.          * @param {Number} source.parentId The parentID, if no parent, set to -1
  9275.          * @param {path} source.path comma-separated string of ancestor IDs (-1,1234,1782,1234)
  9276.          */
  9277.  
  9278.         /** 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 */
  9279.         convertToEntityBasic: function (source) {
  9280.             var required = ["id", "name", "icon", "parentId", "path"];            
  9281.             _.each(required, function (k) {
  9282.                 if (!_.has(source, k)) {
  9283.                     throw "The source object does not contain the property " + k;
  9284.                 }
  9285.             });
  9286.             var optional = ["metaData", "key", "alias"];
  9287.             //now get the basic object
  9288.             var result = _.pick(source, required.concat(optional));
  9289.             return result;
  9290.         }
  9291.  
  9292.     };
  9293. }
  9294. angular.module('umbraco.services').factory('umbModelMapper', umbModelMapper);
  9295.  
  9296. /**
  9297.  * @ngdoc function
  9298.  * @name umbraco.services.umbSessionStorage
  9299.  * @function
  9300.  *
  9301.  * @description
  9302.  * Used to get/set things in browser sessionStorage but always prefixes keys with "umb_" and converts json vals so there is no overlap
  9303.  * with any sessionStorage created by a developer.
  9304.  */
  9305. function umbSessionStorage($window) {
  9306.  
  9307.     //gets the sessionStorage object if available, otherwise just uses a normal object
  9308.     // - required for unit tests.
  9309.     var storage = $window['sessionStorage'] ? $window['sessionStorage'] : {};
  9310.  
  9311.     return {
  9312.  
  9313.         get: function (key) {
  9314.             return angular.fromJson(storage["umb_" + key]);
  9315.         },
  9316.        
  9317.         set : function(key, value) {
  9318.             storage["umb_" + key] = angular.toJson(value);
  9319.         }
  9320.        
  9321.     };
  9322. }
  9323. angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorage);
  9324.  
  9325. /**
  9326.  * @ngdoc function
  9327.  * @name umbraco.services.updateChecker
  9328.  * @function
  9329.  *
  9330.  * @description
  9331.  * used to check for updates and display a notifcation
  9332.  */
  9333. function updateChecker($http, umbRequestHelper) {
  9334.     return {
  9335.        
  9336.          /**
  9337.           * @ngdoc function
  9338.           * @name umbraco.services.updateChecker#check
  9339.           * @methodOf umbraco.services.updateChecker
  9340.           * @function
  9341.           *
  9342.           * @description
  9343.           * Called to load in the legacy tree js which is required on startup if a user is logged in or
  9344.           * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded.
  9345.           */
  9346.          check: function() {
  9347.                
  9348.             return umbRequestHelper.resourcePromise(
  9349.                $http.get(
  9350.                    umbRequestHelper.getApiUrl(
  9351.                        "updateCheckApiBaseUrl",
  9352.                        "GetCheck")),
  9353.                'Failed to retrieve update status');
  9354.         }  
  9355.     };
  9356. }
  9357. angular.module('umbraco.services').factory('updateChecker', updateChecker);
  9358.  
  9359. /**
  9360. * @ngdoc service
  9361. * @name umbraco.services.umbPropertyEditorHelper
  9362. * @description A helper object used for property editors
  9363. **/
  9364. function umbPropEditorHelper() {
  9365.     return {
  9366.         /**
  9367.          * @ngdoc function
  9368.          * @name getImagePropertyValue
  9369.          * @methodOf umbraco.services.umbPropertyEditorHelper
  9370.          * @function    
  9371.          *
  9372.          * @description
  9373.          * 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
  9374.          *
  9375.          * @param {string} input the view path currently stored for the property editor
  9376.          */
  9377.         getViewPath: function(input, isPreValue) {
  9378.             var path = String(input);
  9379.  
  9380.             if (path.startsWith('/')) {
  9381.  
  9382.                 //This is an absolute path, so just leave it
  9383.                 return path;
  9384.             } else {
  9385.  
  9386.                 if (path.indexOf("/") >= 0) {
  9387.                     //This is a relative path, so just leave it
  9388.                     return path;
  9389.                 } else {
  9390.                     if (!isPreValue) {
  9391.                         //i.e. views/propertyeditors/fileupload/fileupload.html
  9392.                         return "views/propertyeditors/" + path + "/" + path + ".html";
  9393.                     } else {
  9394.                         //i.e. views/prevalueeditors/requiredfield.html
  9395.                         return "views/prevalueeditors/" + path + ".html";
  9396.                     }
  9397.                 }
  9398.  
  9399.             }
  9400.         }
  9401.     };
  9402. }
  9403. angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorHelper);
  9404.  
  9405.  
  9406. /**
  9407. * @ngdoc service
  9408. * @name umbraco.services.umbDataFormatter
  9409. * @description A helper object used to format/transform JSON Umbraco data, mostly used for persisting data to the server
  9410. **/
  9411. function umbDataFormatter() {
  9412.     return {
  9413.        
  9414.         formatContentTypePostData: function (displayModel, action) {
  9415.  
  9416.             //create the save model from the display model
  9417.             var saveModel = _.pick(displayModel,
  9418.                 'compositeContentTypes', 'isContainer', 'allowAsRoot', 'allowedTemplates', 'allowedContentTypes',
  9419.                 'alias', 'description', 'thumbnail', 'name', 'id', 'icon', 'trashed',
  9420.                 'key', 'parentId', 'alias', 'path');
  9421.  
  9422.             //TODO: Map these
  9423.             saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { return t.alias; });
  9424.             saveModel.defaultTemplate = displayModel.defaultTemplate ? displayModel.defaultTemplate.alias : null;
  9425.             var realGroups = _.reject(displayModel.groups, function(g) {
  9426.                 //do not include these tabs
  9427.                 return g.tabState === "init";
  9428.             });
  9429.             saveModel.groups = _.map(realGroups, function (g) {
  9430.  
  9431.                 var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name');
  9432.  
  9433.                 var realProperties = _.reject(g.properties, function (p) {
  9434.                     //do not include these properties
  9435.                     return p.propertyState === "init" || p.inherited === true;
  9436.                 });
  9437.  
  9438.                 var saveProperties = _.map(realProperties, function (p) {
  9439.                     var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile');
  9440.                     return saveProperty;
  9441.                 });
  9442.  
  9443.                 saveGroup.properties = saveProperties;
  9444.  
  9445.                 //if this is an inherited group and there are not non-inherited properties on it, then don't send up the data
  9446.                 if (saveGroup.inherited === true && saveProperties.length === 0) {
  9447.                     return null;
  9448.                 }
  9449.  
  9450.                 return saveGroup;
  9451.             });
  9452.            
  9453.             //we don't want any null groups
  9454.             saveModel.groups = _.reject(saveModel.groups, function(g) {
  9455.                 return !g;
  9456.             });
  9457.  
  9458.             return saveModel;
  9459.         },
  9460.  
  9461.         /** formats the display model used to display the data type to the model used to save the data type */
  9462.         formatDataTypePostData: function(displayModel, preValues, action) {
  9463.             var saveModel = {
  9464.                 parentId: displayModel.parentId,
  9465.                 id: displayModel.id,
  9466.                 name: displayModel.name,
  9467.                 selectedEditor: displayModel.selectedEditor,
  9468.                 //set the action on the save model
  9469.                 action: action,
  9470.                 preValues: []
  9471.             };
  9472.             for (var i = 0; i < preValues.length; i++) {
  9473.  
  9474.                 saveModel.preValues.push({
  9475.                     key: preValues[i].alias,
  9476.                     value: preValues[i].value
  9477.                 });
  9478.             }
  9479.             return saveModel;
  9480.         },
  9481.  
  9482.         /** formats the display model used to display the member to the model used to save the member */
  9483.         formatMemberPostData: function(displayModel, action) {
  9484.             //this is basically the same as for media but we need to explicitly add the username,email, password to the save model
  9485.  
  9486.             var saveModel = this.formatMediaPostData(displayModel, action);
  9487.  
  9488.             saveModel.key = displayModel.key;
  9489.            
  9490.             var genericTab = _.find(displayModel.tabs, function (item) {
  9491.                 return item.id === 0;
  9492.             });
  9493.  
  9494.             //map the member login, email, password and groups
  9495.             var propLogin = _.find(genericTab.properties, function (item) {
  9496.                 return item.alias === "_umb_login";
  9497.             });
  9498.             var propEmail = _.find(genericTab.properties, function (item) {
  9499.                 return item.alias === "_umb_email";
  9500.             });
  9501.             var propPass = _.find(genericTab.properties, function (item) {
  9502.                 return item.alias === "_umb_password";
  9503.             });
  9504.             var propGroups = _.find(genericTab.properties, function (item) {
  9505.                 return item.alias === "_umb_membergroup";
  9506.             });
  9507.             saveModel.email = propEmail.value;
  9508.             saveModel.username = propLogin.value;
  9509.             saveModel.password = propPass.value;
  9510.            
  9511.             var selectedGroups = [];
  9512.             for (var n in propGroups.value) {
  9513.                 if (propGroups.value[n] === true) {
  9514.                     selectedGroups.push(n);
  9515.                 }
  9516.             }
  9517.             saveModel.memberGroups = selectedGroups;
  9518.            
  9519.             //turn the dictionary into an array of pairs
  9520.             var memberProviderPropAliases = _.pairs(displayModel.fieldConfig);
  9521.             _.each(displayModel.tabs, function (tab) {
  9522.                 _.each(tab.properties, function (prop) {
  9523.                     var foundAlias = _.find(memberProviderPropAliases, function(item) {
  9524.                         return prop.alias === item[1];
  9525.                     });
  9526.                     if (foundAlias) {
  9527.                         //we know the current property matches an alias, now we need to determine which membership provider property it was for
  9528.                         // by looking at the key
  9529.                         switch (foundAlias[0]) {
  9530.                             case "umbracoMemberLockedOut":
  9531.                                 saveModel.isLockedOut = prop.value.toString() === "1" ? true : false;
  9532.                                 break;
  9533.                             case "umbracoMemberApproved":
  9534.                                 saveModel.isApproved = prop.value.toString() === "1" ? true : false;
  9535.                                 break;
  9536.                             case "umbracoMemberComments":
  9537.                                 saveModel.comments = prop.value;
  9538.                                 break;
  9539.                         }
  9540.                     }                
  9541.                 });
  9542.             });
  9543.  
  9544.  
  9545.  
  9546.             return saveModel;
  9547.         },
  9548.  
  9549.         /** formats the display model used to display the media to the model used to save the media */
  9550.         formatMediaPostData: function(displayModel, action) {
  9551.             //NOTE: the display model inherits from the save model so we can in theory just post up the display model but
  9552.             // we don't want to post all of the data as it is unecessary.
  9553.             var saveModel = {
  9554.                 id: displayModel.id,
  9555.                 properties: [],
  9556.                 name: displayModel.name,
  9557.                 contentTypeAlias: displayModel.contentTypeAlias,
  9558.                 parentId: displayModel.parentId,
  9559.                 //set the action on the save model
  9560.                 action: action
  9561.             };
  9562.  
  9563.             _.each(displayModel.tabs, function (tab) {
  9564.  
  9565.                 _.each(tab.properties, function (prop) {
  9566.  
  9567.                     //don't include the custom generic tab properties
  9568.                     if (!prop.alias.startsWith("_umb_")) {
  9569.                         saveModel.properties.push({
  9570.                             id: prop.id,
  9571.                             alias: prop.alias,
  9572.                             value: prop.value
  9573.                         });
  9574.                     }
  9575.                    
  9576.                 });
  9577.             });
  9578.  
  9579.             return saveModel;
  9580.         },
  9581.  
  9582.         /** formats the display model used to display the content to the model used to save the content  */
  9583.         formatContentPostData: function (displayModel, action) {
  9584.  
  9585.             //this is basically the same as for media but we need to explicitly add some extra properties
  9586.             var saveModel = this.formatMediaPostData(displayModel, action);
  9587.  
  9588.             var genericTab = _.find(displayModel.tabs, function (item) {
  9589.                 return item.id === 0;
  9590.             });
  9591.            
  9592.             var propExpireDate = _.find(genericTab.properties, function(item) {
  9593.                 return item.alias === "_umb_expiredate";
  9594.             });
  9595.             var propReleaseDate = _.find(genericTab.properties, function (item) {
  9596.                 return item.alias === "_umb_releasedate";
  9597.             });
  9598.             var propTemplate = _.find(genericTab.properties, function (item) {
  9599.                 return item.alias === "_umb_template";
  9600.             });
  9601.             saveModel.expireDate = propExpireDate.value;
  9602.             saveModel.releaseDate = propReleaseDate.value;
  9603.             saveModel.templateAlias = propTemplate.value;
  9604.  
  9605.             return saveModel;
  9606.         }
  9607.     };
  9608. }
  9609. angular.module('umbraco.services').factory('umbDataFormatter', umbDataFormatter);
  9610.  
  9611.  
  9612.  
  9613. /**
  9614.  * @ngdoc service
  9615.  * @name umbraco.services.windowResizeListener
  9616.  * @function
  9617.  *
  9618.  * @description
  9619.  * A single window resize listener... we don't want to have more than one in theory to ensure that
  9620.  * there aren't too many events raised. This will debounce the event with 100 ms intervals and force
  9621.  * a $rootScope.$apply when changed and notify all listeners
  9622.  *
  9623.  */
  9624. function windowResizeListener($rootScope) {
  9625.  
  9626.     var WinReszier = (function () {
  9627.         var registered = [];
  9628.         var inited = false;        
  9629.         var resize = _.debounce(function(ev) {
  9630.             notify();
  9631.         }, 100);
  9632.         var notify = function () {
  9633.             var h = $(window).height();
  9634.             var w = $(window).width();
  9635.             //execute all registrations inside of a digest
  9636.             $rootScope.$apply(function() {
  9637.                 for (var i = 0, cnt = registered.length; i < cnt; i++) {
  9638.                     registered[i].apply($(window), [{ width: w, height: h }]);
  9639.                 }
  9640.             });
  9641.         };
  9642.         return {
  9643.             register: function (fn) {
  9644.                 registered.push(fn);
  9645.                 if (inited === false) {
  9646.                     $(window).bind('resize', resize);
  9647.                     inited = true;
  9648.                 }
  9649.             },
  9650.             unregister: function (fn) {
  9651.                 var index = registered.indexOf(fn);
  9652.                 if (index > -1) {
  9653.                     registered.splice(index, 1);
  9654.                 }
  9655.             }
  9656.         };
  9657.     }());
  9658.  
  9659.     return {
  9660.  
  9661.         /**
  9662.          * Register a callback for resizing
  9663.          * @param {Function} cb
  9664.          */
  9665.         register: function (cb) {
  9666.             WinReszier.register(cb);
  9667.         },
  9668.  
  9669.         /**
  9670.          * Removes a registered callback
  9671.          * @param {Function} cb
  9672.          */
  9673.         unregister: function(cb) {
  9674.             WinReszier.unregister(cb);
  9675.         }
  9676.  
  9677.     };
  9678. }
  9679. angular.module('umbraco.services').factory('windowResizeListener', windowResizeListener);
  9680. /**
  9681.  * @ngdoc service
  9682.  * @name umbraco.services.xmlhelper
  9683.  * @function
  9684.  *
  9685.  * @description
  9686.  * Used to convert legacy xml data to json and back again
  9687.  */
  9688. function xmlhelper($http) {
  9689.     /*
  9690.      Copyright 2011 Abdulla Abdurakhmanov
  9691.      Original sources are available at https://code.google.com/p/x2js/
  9692.  
  9693.      Licensed under the Apache License, Version 2.0 (the "License");
  9694.      you may not use this file except in compliance with the License.
  9695.      You may obtain a copy of the License at
  9696.  
  9697.      http://www.apache.org/licenses/LICENSE-2.0
  9698.  
  9699.      Unless required by applicable law or agreed to in writing, software
  9700.      distributed under the License is distributed on an "AS IS" BASIS,
  9701.      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9702.      See the License for the specific language governing permissions and
  9703.      limitations under the License.
  9704.      */
  9705.  
  9706.     function X2JS() {
  9707.         var VERSION = "1.0.11";
  9708.         var escapeMode = false;
  9709.  
  9710.         var DOMNodeTypes = {
  9711.             ELEMENT_NODE: 1,
  9712.             TEXT_NODE: 3,
  9713.             CDATA_SECTION_NODE: 4,
  9714.             DOCUMENT_NODE: 9
  9715.         };
  9716.  
  9717.         function getNodeLocalName(node) {
  9718.             var nodeLocalName = node.localName;
  9719.             if (nodeLocalName == null) {
  9720.                 nodeLocalName = node.baseName;
  9721.             } // Yeah, this is IE!!
  9722.  
  9723.             if (nodeLocalName === null || nodeLocalName === "") {
  9724.                 nodeLocalName = node.nodeName;
  9725.             } // =="" is IE too
  9726.  
  9727.             return nodeLocalName;
  9728.         }
  9729.  
  9730.         function getNodePrefix(node) {
  9731.             return node.prefix;
  9732.         }
  9733.  
  9734.         function escapeXmlChars(str) {
  9735.             if (typeof (str) === "string") {
  9736.                 return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g, '&#x2F;');
  9737.             } else {
  9738.                 return str;
  9739.             }
  9740.         }
  9741.  
  9742.         function unescapeXmlChars(str) {
  9743.             return str.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/&#x2F;/g, '\/');
  9744.         }
  9745.  
  9746.         function parseDOMChildren(node) {
  9747.             var result, child, childName;
  9748.  
  9749.             if (node.nodeType === DOMNodeTypes.DOCUMENT_NODE) {
  9750.                 result = {};
  9751.                 child = node.firstChild;
  9752.                 childName = getNodeLocalName(child);
  9753.                 result[childName] = parseDOMChildren(child);
  9754.                 return result;
  9755.             }
  9756.             else {
  9757.  
  9758.                 if (node.nodeType === DOMNodeTypes.ELEMENT_NODE) {
  9759.                     result = {};
  9760.                     result.__cnt = 0;
  9761.                     var nodeChildren = node.childNodes;
  9762.  
  9763.                     // Children nodes
  9764.                     for (var cidx = 0; cidx < nodeChildren.length; cidx++) {
  9765.                         child = nodeChildren.item(cidx); // nodeChildren[cidx];
  9766.                         childName = getNodeLocalName(child);
  9767.  
  9768.                         result.__cnt++;
  9769.                         if (result[childName] === null) {
  9770.                             result[childName] = parseDOMChildren(child);
  9771.                             result[childName + "_asArray"] = new Array(1);
  9772.                             result[childName + "_asArray"][0] = result[childName];
  9773.                         }
  9774.                         else {
  9775.                             if (result[childName] !== null) {
  9776.                                 if (!(result[childName] instanceof Array)) {
  9777.                                     var tmpObj = result[childName];
  9778.                                     result[childName] = [];
  9779.                                     result[childName][0] = tmpObj;
  9780.  
  9781.                                     result[childName + "_asArray"] = result[childName];
  9782.                                 }
  9783.                             }
  9784.                             var aridx = 0;
  9785.                             while (result[childName][aridx] !== null) {
  9786.                                 aridx++;
  9787.                             }
  9788.  
  9789.                             (result[childName])[aridx] = parseDOMChildren(child);
  9790.                         }
  9791.                     }
  9792.  
  9793.                     // Attributes
  9794.                     for (var aidx = 0; aidx < node.attributes.length; aidx++) {
  9795.                         var attr = node.attributes.item(aidx); // [aidx];
  9796.                         result.__cnt++;
  9797.                         result["_" + attr.name] = attr.value;
  9798.                     }
  9799.  
  9800.                     // Node namespace prefix
  9801.                     var nodePrefix = getNodePrefix(node);
  9802.                     if (nodePrefix !== null && nodePrefix !== "") {
  9803.                         result.__cnt++;
  9804.                         result.__prefix = nodePrefix;
  9805.                     }
  9806.  
  9807.                     if (result.__cnt === 1 && result["#text"] !== null) {
  9808.                         result = result["#text"];
  9809.                     }
  9810.  
  9811.                     if (result["#text"] !== null) {
  9812.                         result.__text = result["#text"];
  9813.                         if (escapeMode) {
  9814.                             result.__text = unescapeXmlChars(result.__text);
  9815.                         }
  9816.  
  9817.                         delete result["#text"];
  9818.                         delete result["#text_asArray"];
  9819.                     }
  9820.                     if (result["#cdata-section"] != null) {
  9821.                         result.__cdata = result["#cdata-section"];
  9822.                         delete result["#cdata-section"];
  9823.                         delete result["#cdata-section_asArray"];
  9824.                     }
  9825.  
  9826.                     if (result.__text != null || result.__cdata != null) {
  9827.                         result.toString = function () {
  9828.                             return (this.__text != null ? this.__text : '') + (this.__cdata != null ? this.__cdata : '');
  9829.                         };
  9830.                     }
  9831.                     return result;
  9832.                 }
  9833.                 else {
  9834.                     if (node.nodeType === DOMNodeTypes.TEXT_NODE || node.nodeType === DOMNodeTypes.CDATA_SECTION_NODE) {
  9835.                         return node.nodeValue;
  9836.                     }
  9837.                 }
  9838.             }
  9839.         }
  9840.  
  9841.         function startTag(jsonObj, element, attrList, closed) {
  9842.             var resultStr = "<" + ((jsonObj != null && jsonObj.__prefix != null) ? (jsonObj.__prefix + ":") : "") + element;
  9843.             if (attrList != null) {
  9844.                 for (var aidx = 0; aidx < attrList.length; aidx++) {
  9845.                     var attrName = attrList[aidx];
  9846.                     var attrVal = jsonObj[attrName];
  9847.                     resultStr += " " + attrName.substr(1) + "='" + attrVal + "'";
  9848.                 }
  9849.             }
  9850.             if (!closed) {
  9851.                 resultStr += ">";
  9852.             } else {
  9853.                 resultStr += "/>";
  9854.             }
  9855.  
  9856.             return resultStr;
  9857.         }
  9858.  
  9859.         function endTag(jsonObj, elementName) {
  9860.             return "</" + (jsonObj.__prefix !== null ? (jsonObj.__prefix + ":") : "") + elementName + ">";
  9861.         }
  9862.  
  9863.         function endsWith(str, suffix) {
  9864.             return str.indexOf(suffix, str.length - suffix.length) !== -1;
  9865.         }
  9866.  
  9867.         function jsonXmlSpecialElem(jsonObj, jsonObjField) {
  9868.             if (endsWith(jsonObjField.toString(), ("_asArray")) || jsonObjField.toString().indexOf("_") === 0 || (jsonObj[jsonObjField] instanceof Function)) {
  9869.                 return true;
  9870.             } else {
  9871.                 return false;
  9872.             }
  9873.         }
  9874.  
  9875.         function jsonXmlElemCount(jsonObj) {
  9876.             var elementsCnt = 0;
  9877.             if (jsonObj instanceof Object) {
  9878.                 for (var it in jsonObj) {
  9879.                     if (jsonXmlSpecialElem(jsonObj, it)) {
  9880.                         continue;
  9881.                     }
  9882.                     elementsCnt++;
  9883.                 }
  9884.             }
  9885.             return elementsCnt;
  9886.         }
  9887.  
  9888.         function parseJSONAttributes(jsonObj) {
  9889.             var attrList = [];
  9890.             if (jsonObj instanceof Object) {
  9891.                 for (var ait in jsonObj) {
  9892.                     if (ait.toString().indexOf("__") === -1 && ait.toString().indexOf("_") === 0) {
  9893.                         attrList.push(ait);
  9894.                     }
  9895.                 }
  9896.             }
  9897.  
  9898.             return attrList;
  9899.         }
  9900.  
  9901.         function parseJSONTextAttrs(jsonTxtObj) {
  9902.             var result = "";
  9903.  
  9904.             if (jsonTxtObj.__cdata != null) {
  9905.                 result += "<![CDATA[" + jsonTxtObj.__cdata + "]]>";
  9906.             }
  9907.  
  9908.             if (jsonTxtObj.__text != null) {
  9909.                 if (escapeMode) {
  9910.                     result += escapeXmlChars(jsonTxtObj.__text);
  9911.                 } else {
  9912.                     result += jsonTxtObj.__text;
  9913.                 }
  9914.             }
  9915.             return result;
  9916.         }
  9917.  
  9918.         function parseJSONTextObject(jsonTxtObj) {
  9919.             var result = "";
  9920.  
  9921.             if (jsonTxtObj instanceof Object) {
  9922.                 result += parseJSONTextAttrs(jsonTxtObj);
  9923.             }
  9924.             else {
  9925.                 if (jsonTxtObj != null) {
  9926.                     if (escapeMode) {
  9927.                         result += escapeXmlChars(jsonTxtObj);
  9928.                     } else {
  9929.                         result += jsonTxtObj;
  9930.                     }
  9931.                 }
  9932.             }
  9933.  
  9934.  
  9935.             return result;
  9936.         }
  9937.  
  9938.         function parseJSONArray(jsonArrRoot, jsonArrObj, attrList) {
  9939.             var result = "";
  9940.             if (jsonArrRoot.length === 0) {
  9941.                 result += startTag(jsonArrRoot, jsonArrObj, attrList, true);
  9942.             }
  9943.             else {
  9944.                 for (var arIdx = 0; arIdx < jsonArrRoot.length; arIdx++) {
  9945.                     result += startTag(jsonArrRoot[arIdx], jsonArrObj, parseJSONAttributes(jsonArrRoot[arIdx]), false);
  9946.                     result += parseJSONObject(jsonArrRoot[arIdx]);
  9947.                     result += endTag(jsonArrRoot[arIdx], jsonArrObj);
  9948.                 }
  9949.             }
  9950.             return result;
  9951.         }
  9952.  
  9953.         function parseJSONObject(jsonObj) {
  9954.             var result = "";
  9955.  
  9956.             var elementsCnt = jsonXmlElemCount(jsonObj);
  9957.  
  9958.             if (elementsCnt > 0) {
  9959.                 for (var it in jsonObj) {
  9960.                     if (jsonXmlSpecialElem(jsonObj, it)) {
  9961.                         continue;
  9962.                     }
  9963.  
  9964.                     var subObj = jsonObj[it];
  9965.                     var attrList = parseJSONAttributes(subObj);
  9966.  
  9967.                     if (subObj === null || subObj === undefined) {
  9968.                         result += startTag(subObj, it, attrList, true);
  9969.                     } else {
  9970.                         if (subObj instanceof Object) {
  9971.  
  9972.                             if (subObj instanceof Array) {
  9973.                                 result += parseJSONArray(subObj, it, attrList);
  9974.                             } else {
  9975.                                 var subObjElementsCnt = jsonXmlElemCount(subObj);
  9976.                                 if (subObjElementsCnt > 0 || subObj.__text !== null || subObj.__cdata !== null) {
  9977.                                     result += startTag(subObj, it, attrList, false);
  9978.                                     result += parseJSONObject(subObj);
  9979.                                     result += endTag(subObj, it);
  9980.                                 } else {
  9981.                                     result += startTag(subObj, it, attrList, true);
  9982.                                 }
  9983.                             }
  9984.  
  9985.                         } else {
  9986.                             result += startTag(subObj, it, attrList, false);
  9987.                             result += parseJSONTextObject(subObj);
  9988.                             result += endTag(subObj, it);
  9989.                         }
  9990.                     }
  9991.                 }
  9992.             }
  9993.             result += parseJSONTextObject(jsonObj);
  9994.  
  9995.             return result;
  9996.         }
  9997.  
  9998.         this.parseXmlString = function (xmlDocStr) {
  9999.             var xmlDoc;
  10000.             if (window.DOMParser) {
  10001.                 var parser = new window.DOMParser();
  10002.                 xmlDoc = parser.parseFromString(xmlDocStr, "text/xml");
  10003.             }
  10004.             else {
  10005.                 // IE :(
  10006.                 if (xmlDocStr.indexOf("<?") === 0) {
  10007.                     xmlDocStr = xmlDocStr.substr(xmlDocStr.indexOf("?>") + 2);
  10008.                 }
  10009.                 xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
  10010.                 xmlDoc.async = "false";
  10011.                 xmlDoc.loadXML(xmlDocStr);
  10012.             }
  10013.             return xmlDoc;
  10014.         };
  10015.  
  10016.         this.xml2json = function (xmlDoc) {
  10017.             return parseDOMChildren(xmlDoc);
  10018.         };
  10019.  
  10020.         this.xml_str2json = function (xmlDocStr) {
  10021.             var xmlDoc = this.parseXmlString(xmlDocStr);
  10022.             return this.xml2json(xmlDoc);
  10023.         };
  10024.  
  10025.         this.json2xml_str = function (jsonObj) {
  10026.             return parseJSONObject(jsonObj);
  10027.         };
  10028.  
  10029.         this.json2xml = function (jsonObj) {
  10030.             var xmlDocStr = this.json2xml_str(jsonObj);
  10031.             return this.parseXmlString(xmlDocStr);
  10032.         };
  10033.  
  10034.         this.getVersion = function () {
  10035.             return VERSION;
  10036.         };
  10037.  
  10038.         this.escapeMode = function (enabled) {
  10039.             escapeMode = enabled;
  10040.         };
  10041.     }
  10042.  
  10043.     var x2js = new X2JS();
  10044.     return {
  10045.         /** Called to load in the legacy tree js which is required on startup if a user is logged in or
  10046.          after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. */
  10047.         toJson: function (xml) {
  10048.             var json = x2js.xml_str2json(xml);
  10049.             return json;
  10050.         },
  10051.         fromJson: function (json) {
  10052.             var xml = x2js.json2xml_str(json);
  10053.             return xml;
  10054.         }
  10055.     };
  10056. }
  10057. angular.module('umbraco.services').factory('xmlhelper', xmlhelper);
  10058.  
  10059. })();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement