/AarhusWebDevCoop/AarhusWebDevCoop/Umbraco/Js/umbraco.services.js

https://bitbucket.org/andreas_nuppenau/obligatorisk_opgave · JavaScript · 9774 lines · 8591 code · 0 blank · 1183 comment · 265 complexity · a7832eb5bc67218b527fd6e0e3a7c7ce MD5 · raw file

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