/AarhusWebDevCoop/AarhusWebDevCoop/Umbraco/Js/umbraco.directives.js

https://bitbucket.org/andreas_nuppenau/obligatorisk_opgave · JavaScript · 11772 lines · 8821 code · 0 blank · 2951 comment · 627 complexity · f1ae19a5fa6ba03b62e106fad6525912 MD5 · raw file

  1. (function () {
  2. angular.module('umbraco.directives', [
  3. 'umbraco.directives.editors',
  4. 'umbraco.directives.html',
  5. 'umbraco.directives.validation',
  6. 'ui.sortable'
  7. ]);
  8. angular.module('umbraco.directives.editors', []);
  9. angular.module('umbraco.directives.html', []);
  10. angular.module('umbraco.directives.validation', []);
  11. /**
  12. * @ngdoc directive
  13. * @name umbraco.directives.directive:autoScale
  14. * @element div
  15. * @deprecated
  16. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  17. * @function
  18. * @description
  19. * Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set
  20. * So if you only want to scale the div to 70 pixels from the bottom you pass "70"
  21. * @example
  22. * <example module="umbraco.directives">
  23. * <file name="index.html">
  24. * <div auto-scale="70" class="input-block-level"></div>
  25. * </file>
  26. * </example>
  27. **/
  28. angular.module('umbraco.directives').directive('autoScale', function ($window) {
  29. return function (scope, el, attrs) {
  30. var totalOffset = 0;
  31. var offsety = parseInt(attrs.autoScale, 10);
  32. var window = angular.element($window);
  33. if (offsety !== undefined) {
  34. totalOffset += offsety;
  35. }
  36. setTimeout(function () {
  37. el.height(window.height() - (el.offset().top + totalOffset));
  38. }, 500);
  39. window.bind('resize', function () {
  40. el.height(window.height() - (el.offset().top + totalOffset));
  41. });
  42. };
  43. });
  44. /**
  45. * @ngdoc directive
  46. * @name umbraco.directives.directive:detectFold
  47. * @deprecated
  48. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  49. * @description This is used for the editor buttons to ensure they are displayed correctly if the horizontal overflow of the editor
  50. * exceeds the height of the window
  51. **/
  52. angular.module('umbraco.directives.html').directive('detectFold', function ($timeout, $log, windowResizeListener) {
  53. return {
  54. require: '^?umbTabs',
  55. restrict: 'A',
  56. link: function (scope, el, attrs, tabsCtrl) {
  57. var firstRun = false;
  58. var parent = $('.umb-panel-body');
  59. var winHeight = $(window).height();
  60. var calculate = function () {
  61. if (el && el.is(':visible') && !el.hasClass('umb-bottom-bar')) {
  62. //now that the element is visible, set the flag in a couple of seconds,
  63. // this will ensure that loading time of a current tab get's completed and that
  64. // we eventually stop watching to save on CPU time
  65. $timeout(function () {
  66. firstRun = true;
  67. }, 4000);
  68. //var parent = el.parent();
  69. var hasOverflow = parent.innerHeight() < parent[0].scrollHeight;
  70. //var belowFold = (el.offset().top + el.height()) > winHeight;
  71. if (hasOverflow) {
  72. el.addClass('umb-bottom-bar');
  73. //I wish we didn't have to put this logic here but unfortunately we
  74. // do. This needs to calculate the left offest to place the bottom bar
  75. // depending on if the left column splitter has been moved by the user
  76. // (based on the nav-resize directive)
  77. var wrapper = $('#mainwrapper');
  78. var contentPanel = $('#leftcolumn').next();
  79. var contentPanelLeftPx = contentPanel.css('left');
  80. el.css({ left: contentPanelLeftPx });
  81. }
  82. }
  83. return firstRun;
  84. };
  85. var resizeCallback = function (size) {
  86. winHeight = size.height;
  87. el.removeClass('umb-bottom-bar');
  88. calculate();
  89. };
  90. windowResizeListener.register(resizeCallback);
  91. //Only execute the watcher if this tab is the active (first) tab on load, otherwise there's no reason to execute
  92. // the watcher since it will be recalculated when the tab changes!
  93. if (el.closest('.umb-tab-pane').index() === 0) {
  94. //run a watcher to ensure that the calculation occurs until it's firstRun but ensure
  95. // the calculations are throttled to save a bit of CPU
  96. var listener = scope.$watch(_.throttle(calculate, 1000), function (newVal, oldVal) {
  97. if (newVal !== oldVal) {
  98. listener();
  99. }
  100. });
  101. }
  102. //listen for tab changes
  103. if (tabsCtrl != null) {
  104. tabsCtrl.onTabShown(function (args) {
  105. calculate();
  106. });
  107. }
  108. //ensure to unregister
  109. scope.$on('$destroy', function () {
  110. windowResizeListener.unregister(resizeCallback);
  111. });
  112. }
  113. };
  114. });
  115. /**
  116. * @ngdoc directive
  117. * @name umbraco.directives.directive:umbContentName
  118. * @deprecated
  119. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  120. * @restrict E
  121. * @function
  122. * @description
  123. * Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form.
  124. **/
  125. angular.module('umbraco.directives').directive('umbContentName', function ($timeout, localizationService) {
  126. return {
  127. require: 'ngModel',
  128. restrict: 'E',
  129. replace: true,
  130. templateUrl: 'views/directives/_obsolete/umb-content-name.html',
  131. scope: {
  132. placeholder: '@placeholder',
  133. model: '=ngModel',
  134. ngDisabled: '='
  135. },
  136. link: function (scope, element, attrs, ngModel) {
  137. var inputElement = element.find('input');
  138. if (scope.placeholder && scope.placeholder[0] === '@') {
  139. localizationService.localize(scope.placeholder.substring(1)).then(function (value) {
  140. scope.placeholder = value;
  141. });
  142. }
  143. var mX, mY, distance;
  144. function calculateDistance(elem, mouseX, mouseY) {
  145. var cx = Math.max(Math.min(mouseX, elem.offset().left + elem.width()), elem.offset().left);
  146. var cy = Math.max(Math.min(mouseY, elem.offset().top + elem.height()), elem.offset().top);
  147. return Math.sqrt((mouseX - cx) * (mouseX - cx) + (mouseY - cy) * (mouseY - cy));
  148. }
  149. var mouseMoveDebounce = _.throttle(function (e) {
  150. mX = e.pageX;
  151. mY = e.pageY;
  152. // not focused and not over element
  153. if (!inputElement.is(':focus') && !inputElement.hasClass('ng-invalid')) {
  154. // on page
  155. if (mX >= inputElement.offset().left) {
  156. distance = calculateDistance(inputElement, mX, mY);
  157. if (distance <= 155) {
  158. distance = 1 - 100 / 150 * distance / 100;
  159. inputElement.css('border', '1px solid rgba(175,175,175, ' + distance + ')');
  160. inputElement.css('background-color', 'rgba(255,255,255, ' + distance + ')');
  161. }
  162. }
  163. }
  164. }, 15);
  165. $(document).bind('mousemove', mouseMoveDebounce);
  166. $timeout(function () {
  167. if (!scope.model) {
  168. scope.goEdit();
  169. }
  170. }, 100, false);
  171. scope.goEdit = function () {
  172. scope.editMode = true;
  173. $timeout(function () {
  174. inputElement.focus();
  175. }, 100, false);
  176. };
  177. scope.exitEdit = function () {
  178. if (scope.model && scope.model !== '') {
  179. scope.editMode = false;
  180. }
  181. };
  182. //unbind doc event!
  183. scope.$on('$destroy', function () {
  184. $(document).unbind('mousemove', mouseMoveDebounce);
  185. });
  186. }
  187. };
  188. });
  189. /**
  190. * @ngdoc directive
  191. * @name umbraco.directives.directive:umbHeader
  192. * @deprecated
  193. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  194. * @restrict E
  195. * @function
  196. * @description
  197. * The header on an editor that contains tabs using bootstrap tabs - THIS IS OBSOLETE, use umbTabHeader instead
  198. **/
  199. angular.module('umbraco.directives').directive('umbHeader', function ($parse, $timeout) {
  200. return {
  201. restrict: 'E',
  202. replace: true,
  203. transclude: 'true',
  204. templateUrl: 'views/directives/_obsolete/umb-header.html',
  205. //create a new isolated scope assigning a tabs property from the attribute 'tabs'
  206. //which is bound to the parent scope property passed in
  207. scope: { tabs: '=' },
  208. link: function (scope, iElement, iAttrs) {
  209. scope.showTabs = iAttrs.tabs ? true : false;
  210. scope.visibleTabs = [];
  211. //since tabs are loaded async, we need to put a watch on them to determine
  212. // when they are loaded, then we can close the watch
  213. var tabWatch = scope.$watch('tabs', function (newValue, oldValue) {
  214. angular.forEach(newValue, function (val, index) {
  215. var tab = {
  216. id: val.id,
  217. label: val.label
  218. };
  219. scope.visibleTabs.push(tab);
  220. });
  221. //don't process if we cannot or have already done so
  222. if (!newValue) {
  223. return;
  224. }
  225. if (!newValue.length || newValue.length === 0) {
  226. return;
  227. }
  228. //we need to do a timeout here so that the current sync operation can complete
  229. // and update the UI, then this will fire and the UI elements will be available.
  230. $timeout(function () {
  231. //use bootstrap tabs API to show the first one
  232. iElement.find('.nav-tabs a:first').tab('show');
  233. //enable the tab drop
  234. iElement.find('.nav-pills, .nav-tabs').tabdrop();
  235. //ensure to destroy tabdrop (unbinds window resize listeners)
  236. scope.$on('$destroy', function () {
  237. iElement.find('.nav-pills, .nav-tabs').tabdrop('destroy');
  238. });
  239. //stop watching now
  240. tabWatch();
  241. }, 200);
  242. });
  243. }
  244. };
  245. });
  246. /**
  247. * @ngdoc directive
  248. * @name umbraco.directives.directive:umbItemSorter
  249. * @deprecated
  250. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  251. * @function
  252. * @element ANY
  253. * @restrict E
  254. * @description A re-usable directive for sorting items
  255. **/
  256. function umbItemSorter(angularHelper) {
  257. return {
  258. scope: { model: '=' },
  259. restrict: 'E',
  260. // restrict to an element
  261. replace: true,
  262. // replace the html element with the template
  263. templateUrl: 'views/directives/_obsolete/umb-item-sorter.html',
  264. link: function (scope, element, attrs, ctrl) {
  265. var defaultModel = {
  266. okButton: 'Ok',
  267. successMsg: 'Sorting successful',
  268. complete: false
  269. };
  270. //assign user vals to default
  271. angular.extend(defaultModel, scope.model);
  272. //re-assign merged to user
  273. scope.model = defaultModel;
  274. scope.performSort = function () {
  275. scope.$emit('umbItemSorter.sorting', { sortedItems: scope.model.itemsToSort });
  276. };
  277. scope.handleCancel = function () {
  278. scope.$emit('umbItemSorter.cancel');
  279. };
  280. scope.handleOk = function () {
  281. scope.$emit('umbItemSorter.ok');
  282. };
  283. //defines the options for the jquery sortable
  284. scope.sortableOptions = {
  285. axis: 'y',
  286. cursor: 'move',
  287. placeholder: 'ui-sortable-placeholder',
  288. update: function (ev, ui) {
  289. //highlight the item when the position is changed
  290. $(ui.item).effect('highlight', { color: '#049cdb' }, 500);
  291. },
  292. stop: function (ev, ui) {
  293. //the ui-sortable directive already ensures that our list is re-sorted, so now we just
  294. // need to update the sortOrder to the index of each item
  295. angularHelper.safeApply(scope, function () {
  296. angular.forEach(scope.itemsToSort, function (val, index) {
  297. val.sortOrder = index + 1;
  298. });
  299. });
  300. }
  301. };
  302. }
  303. };
  304. }
  305. angular.module('umbraco.directives').directive('umbItemSorter', umbItemSorter);
  306. /**
  307. * @ngdoc directive
  308. * @name umbraco.directives.directive:umbLogin
  309. * @deprecated
  310. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  311. * @function
  312. * @element ANY
  313. * @restrict E
  314. **/
  315. function loginDirective() {
  316. return {
  317. restrict: 'E',
  318. // restrict to an element
  319. replace: true,
  320. // replace the html element with the template
  321. templateUrl: 'views/directives/_obsolete/umb-login.html'
  322. };
  323. }
  324. angular.module('umbraco.directives').directive('umbLogin', loginDirective);
  325. /**
  326. * @ngdoc directive
  327. * @name umbraco.directives.directive:umbOptionsMenu
  328. * @deprecated
  329. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  330. * @function
  331. * @element ANY
  332. * @restrict E
  333. **/
  334. angular.module('umbraco.directives').directive('umbOptionsMenu', function ($injector, treeService, navigationService, umbModelMapper, appState) {
  335. return {
  336. scope: {
  337. currentSection: '@',
  338. currentNode: '='
  339. },
  340. restrict: 'E',
  341. replace: true,
  342. templateUrl: 'views/directives/_obsolete/umb-optionsmenu.html',
  343. link: function (scope, element, attrs, ctrl) {
  344. //adds a handler to the context menu item click, we need to handle this differently
  345. //depending on what the menu item is supposed to do.
  346. scope.executeMenuItem = function (action) {
  347. navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection);
  348. };
  349. //callback method to go and get the options async
  350. scope.getOptions = function () {
  351. if (!scope.currentNode) {
  352. return;
  353. }
  354. //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu)
  355. appState.setMenuState('currentNode', scope.currentNode);
  356. if (!scope.actions) {
  357. treeService.getMenu({ treeNode: scope.currentNode }).then(function (data) {
  358. scope.actions = data.menuItems;
  359. });
  360. }
  361. };
  362. }
  363. };
  364. });
  365. /**
  366. * @ngdoc directive
  367. * @name umbraco.directives.directive:umbPhotoFolder
  368. * @deprecated
  369. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  370. * @restrict E
  371. **/
  372. angular.module('umbraco.directives.html').directive('umbPhotoFolder', function ($compile, $log, $timeout, $filter, umbPhotoFolderHelper) {
  373. return {
  374. restrict: 'E',
  375. replace: true,
  376. require: '?ngModel',
  377. terminate: true,
  378. templateUrl: 'views/directives/_obsolete/umb-photo-folder.html',
  379. link: function (scope, element, attrs, ngModel) {
  380. var lastWatch = null;
  381. ngModel.$render = function () {
  382. if (ngModel.$modelValue) {
  383. $timeout(function () {
  384. var photos = ngModel.$modelValue;
  385. scope.clickHandler = scope.$eval(element.attr('on-click'));
  386. var imagesOnly = element.attr('images-only') === 'true';
  387. var margin = element.attr('border') ? parseInt(element.attr('border'), 10) : 5;
  388. var startingIndex = element.attr('baseline') ? parseInt(element.attr('baseline'), 10) : 0;
  389. var minWidth = element.attr('min-width') ? parseInt(element.attr('min-width'), 10) : 420;
  390. var minHeight = element.attr('min-height') ? parseInt(element.attr('min-height'), 10) : 100;
  391. var maxHeight = element.attr('max-height') ? parseInt(element.attr('max-height'), 10) : 300;
  392. var idealImgPerRow = element.attr('ideal-items-per-row') ? parseInt(element.attr('ideal-items-per-row'), 10) : 5;
  393. var fixedRowWidth = Math.max(element.width(), minWidth);
  394. scope.containerStyle = { width: fixedRowWidth + 'px' };
  395. scope.rows = umbPhotoFolderHelper.buildGrid(photos, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly);
  396. if (attrs.filterBy) {
  397. //we track the watches that we create, we don't want to create multiple, so clear it
  398. // if it already exists before creating another.
  399. if (lastWatch) {
  400. lastWatch();
  401. }
  402. //TODO: Need to debounce this so it doesn't filter too often!
  403. lastWatch = scope.$watch(attrs.filterBy, function (newVal, oldVal) {
  404. if (newVal && newVal !== oldVal) {
  405. var p = $filter('filter')(photos, newVal, false);
  406. scope.baseline = 0;
  407. var m = umbPhotoFolderHelper.buildGrid(p, fixedRowWidth, maxHeight, startingIndex, minHeight, idealImgPerRow, margin, imagesOnly);
  408. scope.rows = m;
  409. }
  410. });
  411. }
  412. }, 500); //end timeout
  413. } //end if modelValue
  414. }; //end $render
  415. }
  416. };
  417. });
  418. /**
  419. * @ngdoc directive
  420. * @name umbraco.directives.directive:umbSort
  421. * @deprecated
  422. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  423. *
  424. * @element div
  425. * @function
  426. *
  427. * @description
  428. * Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set
  429. * So if you only want to scale the div to 70 pixels from the bottom you pass "70"
  430. *
  431. * @example
  432. * <example module="umbraco.directives">
  433. * <file name="index.html">
  434. * <div umb-sort="70" class="input-block-level"></div>
  435. * </file>
  436. * </example>
  437. **/
  438. angular.module('umbraco.directives').value('umbSortContextInternal', {}).directive('umbSort', function ($log, umbSortContextInternal) {
  439. return {
  440. require: '?ngModel',
  441. link: function (scope, element, attrs, ngModel) {
  442. var adjustment;
  443. var cfg = scope.$eval(element.attr('umb-sort')) || {};
  444. scope.model = ngModel;
  445. scope.opts = cfg;
  446. scope.opts.containerSelector = cfg.containerSelector || '.umb-' + cfg.group + '-container', scope.opts.nested = cfg.nested || true, scope.opts.drop = cfg.drop || true, scope.opts.drag = cfg.drag || true, scope.opts.clone = cfg.clone || '<li/>';
  447. scope.opts.mode = cfg.mode || 'list';
  448. scope.opts.itemSelectorFull = $.trim(scope.opts.itemPath + ' ' + scope.opts.itemSelector);
  449. /*
  450. scope.opts.isValidTarget = function(item, container) {
  451. if(container.el.is(".umb-" + scope.opts.group + "-container")){
  452. return true;
  453. }
  454. return false;
  455. };
  456. */
  457. element.addClass('umb-sort');
  458. element.addClass('umb-' + cfg.group + '-container');
  459. scope.opts.onDrag = function (item, position) {
  460. if (scope.opts.mode === 'list') {
  461. item.css({
  462. left: position.left - adjustment.left,
  463. top: position.top - adjustment.top
  464. });
  465. }
  466. };
  467. scope.opts.onDrop = function (item, targetContainer, _super) {
  468. if (scope.opts.mode === 'list') {
  469. //list mode
  470. var clonedItem = $(scope.opts.clone).css({ height: 0 });
  471. item.after(clonedItem);
  472. clonedItem.animate({ 'height': item.height() });
  473. item.animate(clonedItem.position(), function () {
  474. clonedItem.detach();
  475. _super(item);
  476. });
  477. }
  478. var children = $(scope.opts.itemSelectorFull, targetContainer.el);
  479. var targetIndex = children.index(item);
  480. var targetScope = $(targetContainer.el[0]).scope();
  481. if (targetScope === umbSortContextInternal.sourceScope) {
  482. if (umbSortContextInternal.sourceScope.opts.onSortHandler) {
  483. var _largs = {
  484. oldIndex: umbSortContextInternal.sourceIndex,
  485. newIndex: targetIndex,
  486. scope: umbSortContextInternal.sourceScope
  487. };
  488. umbSortContextInternal.sourceScope.opts.onSortHandler.call(this, item, _largs);
  489. }
  490. } else {
  491. if (targetScope.opts.onDropHandler) {
  492. var args = {
  493. sourceScope: umbSortContextInternal.sourceScope,
  494. sourceIndex: umbSortContextInternal.sourceIndex,
  495. sourceContainer: umbSortContextInternal.sourceContainer,
  496. targetScope: targetScope,
  497. targetIndex: targetIndex,
  498. targetContainer: targetContainer
  499. };
  500. targetScope.opts.onDropHandler.call(this, item, args);
  501. }
  502. if (umbSortContextInternal.sourceScope.opts.onReleaseHandler) {
  503. var _args = {
  504. sourceScope: umbSortContextInternal.sourceScope,
  505. sourceIndex: umbSortContextInternal.sourceIndex,
  506. sourceContainer: umbSortContextInternal.sourceContainer,
  507. targetScope: targetScope,
  508. targetIndex: targetIndex,
  509. targetContainer: targetContainer
  510. };
  511. umbSortContextInternal.sourceScope.opts.onReleaseHandler.call(this, item, _args);
  512. }
  513. }
  514. };
  515. scope.changeIndex = function (from, to) {
  516. scope.$apply(function () {
  517. var i = ngModel.$modelValue.splice(from, 1)[0];
  518. ngModel.$modelValue.splice(to, 0, i);
  519. });
  520. };
  521. scope.move = function (args) {
  522. var from = args.sourceIndex;
  523. var to = args.targetIndex;
  524. if (args.sourceContainer === args.targetContainer) {
  525. scope.changeIndex(from, to);
  526. } else {
  527. scope.$apply(function () {
  528. var i = args.sourceScope.model.$modelValue.splice(from, 1)[0];
  529. args.targetScope.model.$modelvalue.splice(to, 0, i);
  530. });
  531. }
  532. };
  533. scope.opts.onDragStart = function (item, container, _super) {
  534. var children = $(scope.opts.itemSelectorFull, container.el);
  535. var offset = item.offset();
  536. umbSortContextInternal.sourceIndex = children.index(item);
  537. umbSortContextInternal.sourceScope = $(container.el[0]).scope();
  538. umbSortContextInternal.sourceContainer = container;
  539. //current.item = ngModel.$modelValue.splice(current.index, 1)[0];
  540. var pointer = container.rootGroup.pointer;
  541. adjustment = {
  542. left: pointer.left - offset.left,
  543. top: pointer.top - offset.top
  544. };
  545. _super(item, container);
  546. };
  547. element.sortable(scope.opts);
  548. }
  549. };
  550. });
  551. /**
  552. * @ngdoc directive
  553. * @name umbraco.directives.directive:umbTabView
  554. * @deprecated
  555. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  556. *
  557. * @restrict E
  558. **/
  559. angular.module('umbraco.directives').directive('umbTabView', function ($timeout, $log) {
  560. return {
  561. restrict: 'E',
  562. replace: true,
  563. transclude: 'true',
  564. templateUrl: 'views/directives/_obsolete/umb-tab-view.html'
  565. };
  566. });
  567. /**
  568. * @ngdoc directive
  569. * @name umbraco.directives.directive:umbUploadDropzone
  570. * @deprecated
  571. * We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use.
  572. *
  573. * @restrict E
  574. **/
  575. angular.module('umbraco.directives.html').directive('umbUploadDropzone', function () {
  576. return {
  577. restrict: 'E',
  578. replace: true,
  579. templateUrl: 'views/directives/_obsolete/umb-upload-dropzone.html'
  580. };
  581. });
  582. /**
  583. * @ngdoc directive
  584. * @name umbraco.directives.directive:navResize
  585. * @restrict A
  586. *
  587. * @description
  588. * Handles how the navigation responds to window resizing and controls how the draggable resize panel works
  589. **/
  590. angular.module('umbraco.directives').directive('navResize', function (appState, eventsService, windowResizeListener) {
  591. return {
  592. restrict: 'A',
  593. link: function (scope, element, attrs, ctrl) {
  594. var minScreenSize = 1100;
  595. var resizeEnabled = false;
  596. function setTreeMode() {
  597. appState.setGlobalState('showNavigation', appState.getGlobalState('isTablet') === false);
  598. }
  599. function enableResize() {
  600. //only enable when the size is correct and it's not already enabled
  601. if (!resizeEnabled && appState.getGlobalState('isTablet') === false) {
  602. element.resizable({
  603. containment: $('#mainwrapper'),
  604. autoHide: true,
  605. handles: 'e',
  606. alsoResize: '.navigation-inner-container',
  607. resize: function (e, ui) {
  608. var wrapper = $('#mainwrapper');
  609. var contentPanel = $('#contentwrapper');
  610. var umbNotification = $('#umb-notifications-wrapper');
  611. var apps = $('#applications');
  612. var bottomBar = contentPanel.find('.umb-bottom-bar');
  613. var navOffeset = $('#navOffset');
  614. var leftPanelWidth = ui.element.width() + apps.width();
  615. contentPanel.css({ left: leftPanelWidth });
  616. bottomBar.css({ left: leftPanelWidth });
  617. umbNotification.css({ left: leftPanelWidth });
  618. navOffeset.css({ 'margin-left': ui.element.outerWidth() });
  619. },
  620. stop: function (e, ui) {
  621. }
  622. });
  623. resizeEnabled = true;
  624. }
  625. }
  626. function resetResize() {
  627. if (resizeEnabled) {
  628. //kill the resize
  629. element.resizable('destroy');
  630. element.css('width', '');
  631. var navInnerContainer = element.find('.navigation-inner-container');
  632. navInnerContainer.css('width', '');
  633. $('#contentwrapper').css('left', '');
  634. $('#umb-notifications-wrapper').css('left', '');
  635. $('#navOffset').css('margin-left', '');
  636. resizeEnabled = false;
  637. }
  638. }
  639. var evts = [];
  640. //Listen for global state changes
  641. evts.push(eventsService.on('appState.globalState.changed', function (e, args) {
  642. if (args.key === 'showNavigation') {
  643. if (args.value === false) {
  644. resetResize();
  645. } else {
  646. enableResize();
  647. }
  648. }
  649. }));
  650. var resizeCallback = function (size) {
  651. //set the global app state
  652. appState.setGlobalState('isTablet', size.width <= minScreenSize);
  653. setTreeMode();
  654. };
  655. windowResizeListener.register(resizeCallback);
  656. //ensure to unregister from all events and kill jquery plugins
  657. scope.$on('$destroy', function () {
  658. windowResizeListener.unregister(resizeCallback);
  659. for (var e in evts) {
  660. eventsService.unsubscribe(evts[e]);
  661. }
  662. var navInnerContainer = element.find('.navigation-inner-container');
  663. navInnerContainer.resizable('destroy');
  664. });
  665. //init
  666. //set the global app state
  667. appState.setGlobalState('isTablet', $(window).width() <= minScreenSize);
  668. setTreeMode();
  669. }
  670. };
  671. });
  672. angular.module('umbraco.directives').directive('sectionIcon', function ($compile, iconHelper) {
  673. return {
  674. restrict: 'E',
  675. replace: true,
  676. link: function (scope, element, attrs) {
  677. var icon = attrs.icon;
  678. if (iconHelper.isLegacyIcon(icon)) {
  679. //its a known legacy icon, convert to a new one
  680. element.html('<i class=\'' + iconHelper.convertFromLegacyIcon(icon) + '\'></i>');
  681. } else if (iconHelper.isFileBasedIcon(icon)) {
  682. var convert = iconHelper.convertFromLegacyImage(icon);
  683. if (convert) {
  684. element.html('<i class=\'icon-section ' + convert + '\'></i>');
  685. } else {
  686. element.html('<img class=\'icon-section\' src=\'images/tray/' + icon + '\'>');
  687. } //it's a file, normally legacy so look in the icon tray images
  688. } else {
  689. //it's normal
  690. element.html('<i class=\'icon-section ' + icon + '\'></i>');
  691. }
  692. }
  693. };
  694. });
  695. angular.module('umbraco.directives').directive('umbContextMenu', function (navigationService) {
  696. return {
  697. scope: {
  698. menuDialogTitle: '@',
  699. currentSection: '@',
  700. currentNode: '=',
  701. menuActions: '='
  702. },
  703. restrict: 'E',
  704. replace: true,
  705. templateUrl: 'views/components/application/umb-contextmenu.html',
  706. link: function (scope, element, attrs, ctrl) {
  707. //adds a handler to the context menu item click, we need to handle this differently
  708. //depending on what the menu item is supposed to do.
  709. scope.executeMenuItem = function (action) {
  710. navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection);
  711. };
  712. }
  713. };
  714. });
  715. /**
  716. * @ngdoc directive
  717. * @name umbraco.directives.directive:umbNavigation
  718. * @restrict E
  719. **/
  720. function umbNavigationDirective() {
  721. return {
  722. restrict: 'E',
  723. // restrict to an element
  724. replace: true,
  725. // replace the html element with the template
  726. templateUrl: 'views/components/application/umb-navigation.html'
  727. };
  728. }
  729. angular.module('umbraco.directives').directive('umbNavigation', umbNavigationDirective);
  730. /**
  731. * @ngdoc directive
  732. * @name umbraco.directives.directive:umbSections
  733. * @restrict E
  734. **/
  735. function sectionsDirective($timeout, $window, navigationService, treeService, sectionService, appState, eventsService, $location, historyService) {
  736. return {
  737. restrict: 'E',
  738. // restrict to an element
  739. replace: true,
  740. // replace the html element with the template
  741. templateUrl: 'views/components/application/umb-sections.html',
  742. link: function (scope, element, attr, ctrl) {
  743. //setup scope vars
  744. scope.maxSections = 7;
  745. scope.overflowingSections = 0;
  746. scope.sections = [];
  747. scope.currentSection = appState.getSectionState('currentSection');
  748. scope.showTray = false;
  749. //appState.getGlobalState("showTray");
  750. scope.stickyNavigation = appState.getGlobalState('stickyNavigation');
  751. scope.needTray = false;
  752. scope.trayAnimation = function () {
  753. if (scope.showTray) {
  754. return 'slide';
  755. } else if (scope.showTray === false) {
  756. return 'slide';
  757. } else {
  758. return '';
  759. }
  760. };
  761. function loadSections() {
  762. sectionService.getSectionsForUser().then(function (result) {
  763. scope.sections = result;
  764. calculateHeight();
  765. });
  766. }
  767. function calculateHeight() {
  768. $timeout(function () {
  769. //total height minus room for avatar and help icon
  770. var height = $(window).height() - 200;
  771. scope.totalSections = scope.sections.length;
  772. scope.maxSections = Math.floor(height / 70);
  773. scope.needTray = false;
  774. if (scope.totalSections > scope.maxSections) {
  775. scope.needTray = true;
  776. scope.overflowingSections = scope.maxSections - scope.totalSections;
  777. }
  778. });
  779. }
  780. var evts = [];
  781. //Listen for global state changes
  782. evts.push(eventsService.on('appState.globalState.changed', function (e, args) {
  783. if (args.key === 'showTray') {
  784. scope.showTray = args.value;
  785. }
  786. if (args.key === 'stickyNavigation') {
  787. scope.stickyNavigation = args.value;
  788. }
  789. }));
  790. evts.push(eventsService.on('appState.sectionState.changed', function (e, args) {
  791. if (args.key === 'currentSection') {
  792. scope.currentSection = args.value;
  793. }
  794. }));
  795. evts.push(eventsService.on('app.reInitialize', function (e, args) {
  796. //re-load the sections if we're re-initializing (i.e. package installed)
  797. loadSections();
  798. }));
  799. //ensure to unregister from all events!
  800. scope.$on('$destroy', function () {
  801. for (var e in evts) {
  802. eventsService.unsubscribe(evts[e]);
  803. }
  804. });
  805. //on page resize
  806. window.onresize = calculateHeight;
  807. scope.avatarClick = function () {
  808. if (scope.helpDialog) {
  809. closeHelpDialog();
  810. }
  811. if (!scope.userDialog) {
  812. scope.userDialog = {
  813. view: 'user',
  814. show: true,
  815. close: function (oldModel) {
  816. closeUserDialog();
  817. }
  818. };
  819. } else {
  820. closeUserDialog();
  821. }
  822. };
  823. function closeUserDialog() {
  824. scope.userDialog.show = false;
  825. scope.userDialog = null;
  826. }
  827. scope.helpClick = function () {
  828. if (scope.userDialog) {
  829. closeUserDialog();
  830. }
  831. if (!scope.helpDialog) {
  832. scope.helpDialog = {
  833. view: 'help',
  834. show: true,
  835. close: function (oldModel) {
  836. closeHelpDialog();
  837. }
  838. };
  839. } else {
  840. closeHelpDialog();
  841. }
  842. };
  843. function closeHelpDialog() {
  844. scope.helpDialog.show = false;
  845. scope.helpDialog = null;
  846. }
  847. scope.sectionClick = function (event, section) {
  848. if (event.ctrlKey || event.shiftKey || event.metaKey || event.button && event.button === 1 // middle click, >IE9 + everyone else
  849. ) {
  850. return;
  851. }
  852. if (scope.userDialog) {
  853. closeUserDialog();
  854. }
  855. if (scope.helpDialog) {
  856. closeHelpDialog();
  857. }
  858. navigationService.hideSearch();
  859. navigationService.showTree(section.alias);
  860. //in some cases the section will have a custom route path specified, if there is one we'll use it
  861. if (section.routePath) {
  862. $location.path(section.routePath);
  863. } else {
  864. var lastAccessed = historyService.getLastAccessedItemForSection(section.alias);
  865. var path = lastAccessed != null ? lastAccessed.link : section.alias;
  866. $location.path(path).search('');
  867. }
  868. };
  869. scope.sectionDblClick = function (section) {
  870. navigationService.reloadSection(section.alias);
  871. };
  872. scope.trayClick = function () {
  873. // close dialogs
  874. if (scope.userDialog) {
  875. closeUserDialog();
  876. }
  877. if (scope.helpDialog) {
  878. closeHelpDialog();
  879. }
  880. if (appState.getGlobalState('showTray') === true) {
  881. navigationService.hideTray();
  882. } else {
  883. navigationService.showTray();
  884. }
  885. };
  886. loadSections();
  887. }
  888. };
  889. }
  890. angular.module('umbraco.directives').directive('umbSections', sectionsDirective);
  891. /**
  892. @ngdoc directive
  893. @name umbraco.directives.directive:umbButton
  894. @restrict E
  895. @scope
  896. @description
  897. Use this directive to render an umbraco button. The directive can be used to generate all types of buttons, set type, style, translation, shortcut and much more.
  898. <h3>Markup example</h3>
  899. <pre>
  900. <div ng-controller="My.Controller as vm">
  901. <umb-button
  902. action="vm.clickButton()"
  903. type="button"
  904. button-style="success"
  905. state="vm.buttonState"
  906. shortcut="ctrl+c"
  907. label="My button"
  908. disabled="vm.buttonState === 'busy'">
  909. </umb-button>
  910. </div>
  911. </pre>
  912. <h3>Controller example</h3>
  913. <pre>
  914. (function () {
  915. "use strict";
  916. function Controller(myService) {
  917. var vm = this;
  918. vm.buttonState = "init";
  919. vm.clickButton = clickButton;
  920. function clickButton() {
  921. vm.buttonState = "busy";
  922. myService.clickButton().then(function() {
  923. vm.buttonState = "success";
  924. }, function() {
  925. vm.buttonState = "error";
  926. });
  927. }
  928. }
  929. angular.module("umbraco").controller("My.Controller", Controller);
  930. })();
  931. </pre>
  932. @param {callback} action The button action which should be performed when the button is clicked.
  933. @param {string=} href Url/Path to navigato to.
  934. @param {string=} type Set the button type ("button" or "submit").
  935. @param {string=} buttonStyle Set the style of the button. The directive uses the default bootstrap styles ("primary", "info", "success", "warning", "danger", "inverse", "link", "block"). Pass in array to add multple styles [success,block].
  936. @param {string=} state Set a progress state on the button ("init", "busy", "success", "error").
  937. @param {string=} shortcut Set a keyboard shortcut for the button ("ctrl+c").
  938. @param {string=} label Set the button label.
  939. @param {string=} labelKey Set a localization key to make a multi lingual button ("general_buttonText").
  940. @param {string=} icon Set a button icon.
  941. @param {string=} size Set a button icon ("xs", "m", "l", "xl").
  942. @param {boolean=} disabled Set to <code>true</code> to disable the button.
  943. **/
  944. (function () {
  945. 'use strict';
  946. function ButtonDirective($timeout) {
  947. function link(scope, el, attr, ctrl) {
  948. scope.style = null;
  949. function activate() {
  950. scope.blockElement = false;
  951. if (!scope.state) {
  952. scope.state = 'init';
  953. }
  954. if (scope.buttonStyle) {
  955. // make it possible to pass in multiple styles
  956. if (scope.buttonStyle.startsWith('[') && scope.buttonStyle.endsWith(']')) {
  957. // when using an attr it will always be a string so we need to remove square brackets
  958. // and turn it into and array
  959. var withoutBrackets = scope.buttonStyle.replace(/[\[\]']+/g, '');
  960. // split array by , + make sure to catch whitespaces
  961. var array = withoutBrackets.split(/\s?,\s?/g);
  962. angular.forEach(array, function (item) {
  963. scope.style = scope.style + ' ' + 'btn-' + item;
  964. if (item === 'block') {
  965. scope.blockElement = true;
  966. }
  967. });
  968. } else {
  969. scope.style = 'btn-' + scope.buttonStyle;
  970. if (scope.buttonStyle === 'block') {
  971. scope.blockElement = true;
  972. }
  973. }
  974. }
  975. }
  976. activate();
  977. var unbindStateWatcher = scope.$watch('state', function (newValue, oldValue) {
  978. if (newValue === 'success' || newValue === 'error') {
  979. $timeout(function () {
  980. scope.state = 'init';
  981. }, 2000);
  982. }
  983. });
  984. scope.$on('$destroy', function () {
  985. unbindStateWatcher();
  986. });
  987. }
  988. var directive = {
  989. transclude: true,
  990. restrict: 'E',
  991. replace: true,
  992. templateUrl: 'views/components/buttons/umb-button.html',
  993. link: link,
  994. scope: {
  995. action: '&?',
  996. href: '@?',
  997. type: '@',
  998. buttonStyle: '@?',
  999. state: '=?',
  1000. shortcut: '@?',
  1001. shortcutWhenHidden: '@',
  1002. label: '@?',
  1003. labelKey: '@?',
  1004. icon: '@?',
  1005. disabled: '=',
  1006. size: '@?'
  1007. }
  1008. };
  1009. return directive;
  1010. }
  1011. angular.module('umbraco.directives').directive('umbButton', ButtonDirective);
  1012. }());
  1013. /**
  1014. @ngdoc directive
  1015. @name umbraco.directives.directive:umbButtonGroup
  1016. @restrict E
  1017. @scope
  1018. @description
  1019. Use this directive to render a button with a dropdown of alternative actions.
  1020. <h3>Markup example</h3>
  1021. <pre>
  1022. <div ng-controller="My.Controller as vm">
  1023. <umb-button-group
  1024. ng-if="vm.buttonGroup"
  1025. default-button="vm.buttonGroup.defaultButton"
  1026. sub-buttons="vm.buttonGroup.subButtons"
  1027. direction="down"
  1028. float="right">
  1029. </umb-button-group>
  1030. </div>
  1031. </pre>
  1032. <h3>Controller example</h3>
  1033. <pre>
  1034. (function () {
  1035. "use strict";
  1036. function Controller() {
  1037. var vm = this;
  1038. vm.buttonGroup = {
  1039. defaultButton: {
  1040. labelKey: "general_defaultButton",
  1041. hotKey: "ctrl+d",
  1042. hotKeyWhenHidden: true,
  1043. handler: function() {
  1044. // do magic here
  1045. }
  1046. },
  1047. subButtons: [
  1048. {
  1049. labelKey: "general_subButton",
  1050. hotKey: "ctrl+b",
  1051. hotKeyWhenHidden: true,
  1052. handler: function() {
  1053. // do magic here
  1054. }
  1055. }
  1056. ]
  1057. };
  1058. }
  1059. angular.module("umbraco").controller("My.Controller", Controller);
  1060. })();
  1061. </pre>
  1062. <h3>Button model description</h3>
  1063. <ul>
  1064. <li>
  1065. <strong>labekKey</strong>
  1066. <small>(string)</small> -
  1067. Set a localization key to make a multi lingual button ("general_buttonText").
  1068. </li>
  1069. <li>
  1070. <strong>hotKey</strong>
  1071. <small>(array)</small> -
  1072. Set a keyboard shortcut for the button ("ctrl+c").
  1073. </li>
  1074. <li>
  1075. <strong>hotKeyWhenHidden</strong>
  1076. <small>(boolean)</small> -
  1077. As a default the hotkeys only works on elements visible in the UI. Set to <code>true</code> to set a hotkey on the hidden sub buttons.
  1078. </li>
  1079. <li>
  1080. <strong>handler</strong>
  1081. <small>(callback)</small> -
  1082. Set a callback to handle button click events.
  1083. </li>
  1084. </ul>
  1085. @param {object} defaultButton The model of the default button.
  1086. @param {array} subButtons Array of sub buttons.
  1087. @param {string=} state Set a progress state on the button ("init", "busy", "success", "error").
  1088. @param {string=} direction Set the direction of the dropdown ("up", "down").
  1089. @param {string=} float Set the float of the dropdown. ("left", "right").
  1090. **/
  1091. (function () {
  1092. 'use strict';
  1093. function ButtonGroupDirective() {
  1094. var directive = {
  1095. restrict: 'E',
  1096. replace: true,
  1097. templateUrl: 'views/components/buttons/umb-button-group.html',
  1098. scope: {
  1099. defaultButton: '=',
  1100. subButtons: '=',
  1101. state: '=?',
  1102. direction: '@?',
  1103. float: '@?'
  1104. }
  1105. };
  1106. return directive;
  1107. }
  1108. angular.module('umbraco.directives').directive('umbButtonGroup', ButtonGroupDirective);
  1109. }());
  1110. /**
  1111. @ngdoc directive
  1112. @name umbraco.directives.directive:umbToggle
  1113. @restrict E
  1114. @scope
  1115. @description
  1116. <b>Added in Umbraco version 7.7.0</b> Use this directive to render an umbraco toggle.
  1117. <h3>Markup example</h3>
  1118. <pre>
  1119. <div ng-controller="My.Controller as vm">
  1120. <umb-toggle
  1121. checked="vm.checked"
  1122. on-click="vm.toggle()">
  1123. </umb-toggle>
  1124. <umb-toggle
  1125. checked="vm.checked"
  1126. on-click="vm.toggle()"
  1127. show-labels="true"
  1128. label-on="Start"
  1129. label-off="Stop"
  1130. label-position="right"
  1131. hide-icons="true">
  1132. </umb-toggle>
  1133. </div>
  1134. </pre>
  1135. <h3>Controller example</h3>
  1136. <pre>
  1137. (function () {
  1138. "use strict";
  1139. function Controller() {
  1140. var vm = this;
  1141. vm.checked = false;
  1142. vm.toggle = toggle;
  1143. function toggle() {
  1144. vm.checked = !vm.checked;
  1145. }
  1146. }
  1147. angular.module("umbraco").controller("My.Controller", Controller);
  1148. })();
  1149. </pre>
  1150. @param {boolean} checked Set to <code>true</code> or <code>false</code> to toggle the switch.
  1151. @param {callback} onClick The function which should be called when the toggle is clicked.
  1152. @param {string=} showLabels Set to <code>true</code> or <code>false</code> to show a "On" or "Off" label next to the switch.
  1153. @param {string=} labelOn Set a custom label for when the switched is turned on. It will default to "On".
  1154. @param {string=} labelOff Set a custom label for when the switched is turned off. It will default to "Off".
  1155. @param {string=} labelPosition Sets the label position to the left or right of the switch. It will default to "left" ("left", "right").
  1156. @param {string=} hideIcons Set to <code>true</code> or <code>false</code> to hide the icons on the switch.
  1157. **/
  1158. (function () {
  1159. 'use strict';
  1160. function ToggleDirective(localizationService) {
  1161. function link(scope, el, attr, ctrl) {
  1162. scope.displayLabelOn = '';
  1163. scope.displayLabelOff = '';
  1164. function onInit() {
  1165. setLabelText();
  1166. }
  1167. function setLabelText() {
  1168. // set default label for "on"
  1169. if (scope.labelOn) {
  1170. scope.displayLabelOn = scope.labelOn;
  1171. } else {
  1172. localizationService.localize('general_on').then(function (value) {
  1173. scope.displayLabelOn = value;
  1174. });
  1175. }
  1176. // set default label for "Off"
  1177. if (scope.labelOff) {
  1178. scope.displayLabelOff = scope.labelOff;
  1179. } else {
  1180. localizationService.localize('general_off').then(function (value) {
  1181. scope.displayLabelOff = value;
  1182. });
  1183. }
  1184. }
  1185. scope.click = function () {
  1186. if (scope.onClick) {
  1187. scope.onClick();
  1188. }
  1189. };
  1190. onInit();
  1191. }
  1192. var directive = {
  1193. restrict: 'E',
  1194. replace: true,
  1195. templateUrl: 'views/components/buttons/umb-toggle.html',
  1196. scope: {
  1197. checked: '=',
  1198. onClick: '&',
  1199. labelOn: '@?',
  1200. labelOff: '@?',
  1201. labelPosition: '@?',
  1202. showLabels: '@?',
  1203. hideIcons: '@?'
  1204. },
  1205. link: link
  1206. };
  1207. return directive;
  1208. }
  1209. angular.module('umbraco.directives').directive('umbToggle', ToggleDirective);
  1210. }());
  1211. (function () {
  1212. 'use strict';
  1213. function ContentEditController($rootScope, $scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) {
  1214. //setup scope vars
  1215. $scope.defaultButton = null;
  1216. $scope.subButtons = [];
  1217. $scope.page = {};
  1218. $scope.page.loading = false;
  1219. $scope.page.menu = {};
  1220. $scope.page.menu.currentNode = null;
  1221. $scope.page.menu.currentSection = appState.getSectionState('currentSection');
  1222. $scope.page.listViewPath = null;
  1223. $scope.page.isNew = $scope.isNew ? true : false;
  1224. $scope.page.buttonGroupState = 'init';
  1225. function init(content) {
  1226. var buttons = contentEditingHelper.configureContentEditorButtons({
  1227. create: $scope.page.isNew,
  1228. content: content,
  1229. methods: {
  1230. saveAndPublish: $scope.saveAndPublish,
  1231. sendToPublish: $scope.sendToPublish,
  1232. save: $scope.save,
  1233. unPublish: $scope.unPublish
  1234. }
  1235. });
  1236. $scope.defaultButton = buttons.defaultButton;
  1237. $scope.subButtons = buttons.subButtons;
  1238. editorState.set($scope.content);
  1239. //We fetch all ancestors of the node to generate the footer breadcrumb navigation
  1240. if (!$scope.page.isNew) {
  1241. if (content.parentId && content.parentId !== -1) {
  1242. entityResource.getAncestors(content.id, 'document').then(function (anc) {
  1243. $scope.ancestors = anc;
  1244. });
  1245. }
  1246. }
  1247. }
  1248. /** Syncs the content item to it's tree node - this occurs on first load and after saving */
  1249. function syncTreeNode(content, path, initialLoad) {
  1250. if (!$scope.content.isChildOfListView) {
  1251. navigationService.syncTree({
  1252. tree: $scope.treeAlias,
  1253. path: path.split(','),
  1254. forceReload: initialLoad !== true
  1255. }).then(function (syncArgs) {
  1256. $scope.page.menu.currentNode = syncArgs.node;
  1257. });
  1258. } else if (initialLoad === true) {
  1259. //it's a child item, just sync the ui node to the parent
  1260. navigationService.syncTree({
  1261. tree: $scope.treeAlias,
  1262. path: path.substring(0, path.lastIndexOf(',')).split(','),
  1263. forceReload: initialLoad !== true
  1264. });
  1265. //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node
  1266. // from the server so that we can load in the actions menu.
  1267. umbRequestHelper.resourcePromise($http.get(content.treeNodeUrl), 'Failed to retrieve data for child node ' + content.id).then(function (node) {
  1268. $scope.page.menu.currentNode = node;
  1269. });
  1270. }
  1271. }
  1272. // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish
  1273. function performSave(args) {
  1274. var deferred = $q.defer();
  1275. $scope.page.buttonGroupState = 'busy';
  1276. contentEditingHelper.contentEditorPerformSave({
  1277. statusMessage: args.statusMessage,
  1278. saveMethod: args.saveMethod,
  1279. scope: $scope,
  1280. content: $scope.content,
  1281. action: args.action
  1282. }).then(function (data) {
  1283. //success
  1284. init($scope.content);
  1285. syncTreeNode($scope.content, data.path);
  1286. $scope.page.buttonGroupState = 'success';
  1287. deferred.resolve(data);
  1288. }, function (err) {
  1289. //error
  1290. if (err) {
  1291. editorState.set($scope.content);
  1292. }
  1293. $scope.page.buttonGroupState = 'error';
  1294. deferred.reject(err);
  1295. });
  1296. return deferred.promise;
  1297. }
  1298. function resetLastListPageNumber(content) {
  1299. // We're using rootScope to store the page number for list views, so if returning to the list
  1300. // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children
  1301. // we should remove this so as not to confuse if navigating to a different list
  1302. if (!content.isChildOfListView && !content.isContainer) {
  1303. $rootScope.lastListViewPageViewed = null;
  1304. }
  1305. }
  1306. if ($scope.page.isNew) {
  1307. $scope.page.loading = true;
  1308. //we are creating so get an empty content item
  1309. $scope.getScaffoldMethod()().then(function (data) {
  1310. $scope.content = data;
  1311. init($scope.content);
  1312. resetLastListPageNumber($scope.content);
  1313. $scope.page.loading = false;
  1314. });
  1315. } else {
  1316. $scope.page.loading = true;
  1317. //we are editing so get the content item from the server
  1318. $scope.getMethod()($scope.contentId).then(function (data) {
  1319. $scope.content = data;
  1320. if (data.isChildOfListView && data.trashed === false) {
  1321. $scope.page.listViewPath = $routeParams.page ? '/content/content/edit/' + data.parentId + '?page=' + $routeParams.page : '/content/content/edit/' + data.parentId;
  1322. }
  1323. init($scope.content);
  1324. //in one particular special case, after we've created a new item we redirect back to the edit
  1325. // route but there might be server validation errors in the collection which we need to display
  1326. // after the redirect, so we will bind all subscriptions which will show the server validation errors
  1327. // if there are any and then clear them so the collection no longer persists them.
  1328. serverValidationManager.executeAndClearAllSubscriptions();
  1329. syncTreeNode($scope.content, data.path, true);
  1330. resetLastListPageNumber($scope.content);
  1331. $scope.page.loading = false;
  1332. });
  1333. }
  1334. $scope.unPublish = function () {
  1335. if (formHelper.submitForm({
  1336. scope: $scope,
  1337. statusMessage: 'Unpublishing...',
  1338. skipValidation: true
  1339. })) {
  1340. $scope.page.buttonGroupState = 'busy';
  1341. contentResource.unPublish($scope.content.id).then(function (data) {
  1342. formHelper.resetForm({
  1343. scope: $scope,
  1344. notifications: data.notifications
  1345. });
  1346. contentEditingHelper.handleSuccessfulSave({
  1347. scope: $scope,
  1348. savedContent: data,
  1349. rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data)
  1350. });
  1351. init($scope.content);
  1352. syncTreeNode($scope.content, data.path);
  1353. $scope.page.buttonGroupState = 'success';
  1354. }, function (err) {
  1355. $scope.page.buttonGroupState = 'error';
  1356. });
  1357. }
  1358. };
  1359. $scope.sendToPublish = function () {
  1360. return performSave({
  1361. saveMethod: contentResource.sendToPublish,
  1362. statusMessage: 'Sending...',
  1363. action: 'sendToPublish'
  1364. });
  1365. };
  1366. $scope.saveAndPublish = function () {
  1367. return performSave({
  1368. saveMethod: contentResource.publish,
  1369. statusMessage: 'Publishing...',
  1370. action: 'publish'
  1371. });
  1372. };
  1373. $scope.save = function () {
  1374. return performSave({
  1375. saveMethod: $scope.saveMethod(),
  1376. statusMessage: 'Saving...',
  1377. action: 'save'
  1378. });
  1379. };
  1380. $scope.preview = function (content) {
  1381. if (!$scope.busy) {
  1382. // Chromes popup blocker will kick in if a window is opened
  1383. // without the initial scoped request. This trick will fix that.
  1384. //
  1385. var previewWindow = $window.open('preview/?init=true&id=' + content.id, 'umbpreview');
  1386. // Build the correct path so both /#/ and #/ work.
  1387. var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + content.id;
  1388. //The user cannot save if they don't have access to do that, in which case we just want to preview
  1389. //and that's it otherwise they'll get an unauthorized access message
  1390. if (!_.contains(content.allowedActions, 'A')) {
  1391. previewWindow.location.href = redirect;
  1392. } else {
  1393. $scope.save().then(function (data) {
  1394. previewWindow.location.href = redirect;
  1395. });
  1396. }
  1397. }
  1398. };
  1399. }
  1400. function createDirective() {
  1401. var directive = {
  1402. restrict: 'E',
  1403. replace: true,
  1404. templateUrl: 'views/components/content/edit.html',
  1405. controller: 'Umbraco.Editors.Content.EditorDirectiveController',
  1406. scope: {
  1407. contentId: '=',
  1408. isNew: '=?',
  1409. treeAlias: '@',
  1410. page: '=?',
  1411. saveMethod: '&',
  1412. getMethod: '&',
  1413. getScaffoldMethod: '&?'
  1414. }
  1415. };
  1416. return directive;
  1417. }
  1418. angular.module('umbraco.directives').controller('Umbraco.Editors.Content.EditorDirectiveController', ContentEditController);
  1419. angular.module('umbraco.directives').directive('contentEditor', createDirective);
  1420. }());
  1421. /**
  1422. @ngdoc directive
  1423. @name umbraco.directives.directive:umbEditorSubHeader
  1424. @restrict E
  1425. @description
  1426. Use this directive to construct a sub header in the main editor window.
  1427. The sub header is sticky and will follow along down the page when scrolling.
  1428. <h3>Markup example</h3>
  1429. <pre>
  1430. <div ng-controller="MySection.Controller as vm">
  1431. <form name="mySectionForm" novalidate>
  1432. <umb-editor-view>
  1433. <umb-editor-container>
  1434. <umb-editor-sub-header>
  1435. // sub header content here
  1436. </umb-editor-sub-header>
  1437. </umb-editor-container>
  1438. </umb-editor-view>
  1439. </form>
  1440. </div>
  1441. </pre>
  1442. <h3>Use in combination with</h3>
  1443. <ul>
  1444. <li>{@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}</li>
  1445. <li>{@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}</li>
  1446. <li>{@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}</li>
  1447. </ul>
  1448. **/
  1449. (function () {
  1450. 'use strict';
  1451. function EditorSubHeaderDirective() {
  1452. var directive = {
  1453. transclude: true,
  1454. restrict: 'E',
  1455. replace: true,
  1456. templateUrl: 'views/components/editor/subheader/umb-editor-sub-header.html'
  1457. };
  1458. return directive;
  1459. }
  1460. angular.module('umbraco.directives').directive('umbEditorSubHeader', EditorSubHeaderDirective);
  1461. }());
  1462. /**
  1463. @ngdoc directive
  1464. @name umbraco.directives.directive:umbEditorSubHeaderContentLeft
  1465. @restrict E
  1466. @description
  1467. Use this directive to left align content in a sub header in the main editor window.
  1468. <h3>Markup example</h3>
  1469. <pre>
  1470. <div ng-controller="MySection.Controller as vm">
  1471. <form name="mySectionForm" novalidate>
  1472. <umb-editor-view>
  1473. <umb-editor-container>
  1474. <umb-editor-sub-header>
  1475. <umb-editor-sub-header-content-left>
  1476. // left content here
  1477. </umb-editor-sub-header-content-left>
  1478. <umb-editor-sub-header-content-right>
  1479. // right content here
  1480. </umb-editor-sub-header-content-right>
  1481. </umb-editor-sub-header>
  1482. </umb-editor-container>
  1483. </umb-editor-view>
  1484. </form>
  1485. </div>
  1486. </pre>
  1487. <h3>Use in combination with</h3>
  1488. <ul>
  1489. <li>{@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}</li>
  1490. <li>{@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}</li>
  1491. <li>{@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}</li>
  1492. </ul>
  1493. **/
  1494. (function () {
  1495. 'use strict';
  1496. function EditorSubHeaderContentLeftDirective() {
  1497. var directive = {
  1498. transclude: true,
  1499. restrict: 'E',
  1500. replace: true,
  1501. templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-left.html'
  1502. };
  1503. return directive;
  1504. }
  1505. angular.module('umbraco.directives').directive('umbEditorSubHeaderContentLeft', EditorSubHeaderContentLeftDirective);
  1506. }());
  1507. /**
  1508. @ngdoc directive
  1509. @name umbraco.directives.directive:umbEditorSubHeaderContentRight
  1510. @restrict E
  1511. @description
  1512. Use this directive to rigt align content in a sub header in the main editor window.
  1513. <h3>Markup example</h3>
  1514. <pre>
  1515. <div ng-controller="MySection.Controller as vm">
  1516. <form name="mySectionForm" novalidate>
  1517. <umb-editor-view>
  1518. <umb-editor-container>
  1519. <umb-editor-sub-header>
  1520. <umb-editor-sub-header-content-left>
  1521. // left content here
  1522. </umb-editor-sub-header-content-left>
  1523. <umb-editor-sub-header-content-right>
  1524. // right content here
  1525. </umb-editor-sub-header-content-right>
  1526. </umb-editor-sub-header>
  1527. </umb-editor-container>
  1528. </umb-editor-view>
  1529. </form>
  1530. </div>
  1531. </pre>
  1532. <h3>Use in combination with</h3>
  1533. <ul>
  1534. <li>{@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}</li>
  1535. <li>{@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}</li>
  1536. <li>{@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}</li>
  1537. </ul>
  1538. **/
  1539. (function () {
  1540. 'use strict';
  1541. function EditorSubHeaderContentRightDirective() {
  1542. var directive = {
  1543. transclude: true,
  1544. restrict: 'E',
  1545. replace: true,
  1546. templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-right.html'
  1547. };
  1548. return directive;
  1549. }
  1550. angular.module('umbraco.directives').directive('umbEditorSubHeaderContentRight', EditorSubHeaderContentRightDirective);
  1551. }());
  1552. /**
  1553. @ngdoc directive
  1554. @name umbraco.directives.directive:umbEditorSubHeaderSection
  1555. @restrict E
  1556. @description
  1557. Use this directive to create sections, divided by borders, in a sub header in the main editor window.
  1558. <h3>Markup example</h3>
  1559. <pre>
  1560. <div ng-controller="MySection.Controller as vm">
  1561. <form name="mySectionForm" novalidate>
  1562. <umb-editor-view>
  1563. <umb-editor-container>
  1564. <umb-editor-sub-header>
  1565. <umb-editor-sub-header-content-right>
  1566. <umb-editor-sub-header-section>
  1567. // section content here
  1568. </umb-editor-sub-header-section>
  1569. <umb-editor-sub-header-section>
  1570. // section content here
  1571. </umb-editor-sub-header-section>
  1572. <umb-editor-sub-header-section>
  1573. // section content here
  1574. </umb-editor-sub-header-section>
  1575. </umb-editor-sub-header-content-right>
  1576. </umb-editor-sub-header>
  1577. </umb-editor-container>
  1578. </umb-editor-view>
  1579. </form>
  1580. </div>
  1581. </pre>
  1582. <h3>Use in combination with</h3>
  1583. <ul>
  1584. <li>{@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}</li>
  1585. <li>{@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}</li>
  1586. <li>{@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}</li>
  1587. </ul>
  1588. **/
  1589. (function () {
  1590. 'use strict';
  1591. function EditorSubHeaderSectionDirective() {
  1592. var directive = {
  1593. transclude: true,
  1594. restrict: 'E',
  1595. replace: true,
  1596. templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-section.html'
  1597. };
  1598. return directive;
  1599. }
  1600. angular.module('umbraco.directives').directive('umbEditorSubHeaderSection', EditorSubHeaderSectionDirective);
  1601. }());
  1602. /**
  1603. @ngdoc directive
  1604. @name umbraco.directives.directive:umbBreadcrumbs
  1605. @restrict E
  1606. @scope
  1607. @description
  1608. Use this directive to generate a list of breadcrumbs.
  1609. <h3>Markup example</h3>
  1610. <pre>
  1611. <div ng-controller="My.Controller as vm">
  1612. <umb-breadcrumbs
  1613. ng-if="vm.ancestors && vm.ancestors.length > 0"
  1614. ancestors="vm.ancestors"
  1615. entity-type="content">
  1616. </umb-breadcrumbs>
  1617. </div>
  1618. </pre>
  1619. <h3>Controller example</h3>
  1620. <pre>
  1621. (function () {
  1622. "use strict";
  1623. function Controller(myService) {
  1624. var vm = this;
  1625. vm.ancestors = [];
  1626. myService.getAncestors().then(function(ancestors){
  1627. vm.ancestors = ancestors;
  1628. });
  1629. }
  1630. angular.module("umbraco").controller("My.Controller", Controller);
  1631. })();
  1632. </pre>
  1633. @param {array} ancestors Array of ancestors
  1634. @param {string} entityType The content entity type (member, media, content).
  1635. @param {callback} Callback when an ancestor is clicked. It will override the default link behaviour.
  1636. **/
  1637. (function () {
  1638. 'use strict';
  1639. function BreadcrumbsDirective() {
  1640. function link(scope, el, attr, ctrl) {
  1641. scope.allowOnOpen = false;
  1642. scope.open = function (ancestor) {
  1643. if (scope.onOpen && scope.allowOnOpen) {
  1644. scope.onOpen({ 'ancestor': ancestor });
  1645. }
  1646. };
  1647. function onInit() {
  1648. if ('onOpen' in attr) {
  1649. scope.allowOnOpen = true;
  1650. }
  1651. }
  1652. onInit();
  1653. }
  1654. var directive = {
  1655. restrict: 'E',
  1656. replace: true,
  1657. templateUrl: 'views/components/editor/umb-breadcrumbs.html',
  1658. scope: {
  1659. ancestors: '=',
  1660. entityType: '@',
  1661. onOpen: '&'
  1662. },
  1663. link: link
  1664. };
  1665. return directive;
  1666. }
  1667. angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective);
  1668. }());
  1669. /**
  1670. @ngdoc directive
  1671. @name umbraco.directives.directive:umbEditorContainer
  1672. @restrict E
  1673. @description
  1674. Use this directive to construct a main content area inside the main editor window.
  1675. <h3>Markup example</h3>
  1676. <pre>
  1677. <div ng-controller="Umbraco.Controller as vm">
  1678. <umb-editor-view>
  1679. <umb-editor-header
  1680. // header configuration>
  1681. </umb-editor-header>
  1682. <umb-editor-container>
  1683. // main content here
  1684. </umb-editor-container>
  1685. <umb-editor-footer>
  1686. // footer content here
  1687. </umb-editor-footer>
  1688. </umb-editor-view>
  1689. </div>
  1690. </pre>
  1691. <h3>Use in combination with</h3>
  1692. <ul>
  1693. <li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
  1694. <li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
  1695. <li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
  1696. </ul>
  1697. **/
  1698. (function () {
  1699. 'use strict';
  1700. function EditorContainerDirective(overlayHelper) {
  1701. function link(scope, el, attr, ctrl) {
  1702. scope.numberOfOverlays = 0;
  1703. scope.$watch(function () {
  1704. return overlayHelper.getNumberOfOverlays();
  1705. }, function (newValue) {
  1706. scope.numberOfOverlays = newValue;
  1707. });
  1708. }
  1709. var directive = {
  1710. transclude: true,
  1711. restrict: 'E',
  1712. replace: true,
  1713. templateUrl: 'views/components/editor/umb-editor-container.html',
  1714. link: link
  1715. };
  1716. return directive;
  1717. }
  1718. angular.module('umbraco.directives').directive('umbEditorContainer', EditorContainerDirective);
  1719. }());
  1720. /**
  1721. @ngdoc directive
  1722. @name umbraco.directives.directive:umbEditorFooter
  1723. @restrict E
  1724. @description
  1725. Use this directive to construct a footer inside the main editor window.
  1726. <h3>Markup example</h3>
  1727. <pre>
  1728. <div ng-controller="MySection.Controller as vm">
  1729. <form name="mySectionForm" novalidate>
  1730. <umb-editor-view>
  1731. <umb-editor-header
  1732. // header configuration>
  1733. </umb-editor-header>
  1734. <umb-editor-container>
  1735. // main content here
  1736. </umb-editor-container>
  1737. <umb-editor-footer>
  1738. // footer content here
  1739. </umb-editor-footer>
  1740. </umb-editor-view>
  1741. </form>
  1742. </div>
  1743. </pre>
  1744. <h3>Use in combination with</h3>
  1745. <ul>
  1746. <li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
  1747. <li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
  1748. <li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
  1749. <li>{@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}</li>
  1750. <li>{@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}</li>
  1751. </ul>
  1752. **/
  1753. (function () {
  1754. 'use strict';
  1755. function EditorFooterDirective() {
  1756. var directive = {
  1757. transclude: true,
  1758. restrict: 'E',
  1759. replace: true,
  1760. templateUrl: 'views/components/editor/umb-editor-footer.html'
  1761. };
  1762. return directive;
  1763. }
  1764. angular.module('umbraco.directives').directive('umbEditorFooter', EditorFooterDirective);
  1765. }());
  1766. /**
  1767. @ngdoc directive
  1768. @name umbraco.directives.directive:umbEditorFooterContentLeft
  1769. @restrict E
  1770. @description
  1771. Use this directive to align content left inside the main editor footer.
  1772. <h3>Markup example</h3>
  1773. <pre>
  1774. <div ng-controller="MySection.Controller as vm">
  1775. <form name="mySectionForm" novalidate>
  1776. <umb-editor-view>
  1777. <umb-editor-footer>
  1778. <umb-editor-footer-content-left>
  1779. // align content left
  1780. </umb-editor-footer-content-left>
  1781. <umb-editor-footer-content-right>
  1782. // align content right
  1783. </umb-editor-footer-content-right>
  1784. </umb-editor-footer>
  1785. </umb-editor-view>
  1786. </form>
  1787. </div>
  1788. </pre>
  1789. <h3>Use in combination with</h3>
  1790. <ul>
  1791. <li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
  1792. <li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
  1793. <li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
  1794. <li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
  1795. <li>{@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}</li>
  1796. </ul>
  1797. **/
  1798. (function () {
  1799. 'use strict';
  1800. function EditorFooterContentLeftDirective() {
  1801. var directive = {
  1802. transclude: true,
  1803. restrict: 'E',
  1804. replace: true,
  1805. templateUrl: 'views/components/editor/umb-editor-footer-content-left.html'
  1806. };
  1807. return directive;
  1808. }
  1809. angular.module('umbraco.directives').directive('umbEditorFooterContentLeft', EditorFooterContentLeftDirective);
  1810. }());
  1811. /**
  1812. @ngdoc directive
  1813. @name umbraco.directives.directive:umbEditorFooterContentRight
  1814. @restrict E
  1815. @description
  1816. Use this directive to align content right inside the main editor footer.
  1817. <h3>Markup example</h3>
  1818. <pre>
  1819. <div ng-controller="MySection.Controller as vm">
  1820. <form name="mySectionForm" novalidate>
  1821. <umb-editor-view>
  1822. <umb-editor-footer>
  1823. <umb-editor-footer-content-left>
  1824. // align content left
  1825. </umb-editor-footer-content-left>
  1826. <umb-editor-footer-content-right>
  1827. // align content right
  1828. </umb-editor-footer-content-right>
  1829. </umb-editor-footer>
  1830. </umb-editor-view>
  1831. </form>
  1832. </div>
  1833. </pre>
  1834. <h3>Use in combination with</h3>
  1835. <ul>
  1836. <li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
  1837. <li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
  1838. <li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
  1839. <li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
  1840. <li>{@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}</li>
  1841. </ul>
  1842. **/
  1843. (function () {
  1844. 'use strict';
  1845. function EditorFooterContentRightDirective() {
  1846. var directive = {
  1847. transclude: true,
  1848. restrict: 'E',
  1849. replace: true,
  1850. templateUrl: 'views/components/editor/umb-editor-footer-content-right.html'
  1851. };
  1852. return directive;
  1853. }
  1854. angular.module('umbraco.directives').directive('umbEditorFooterContentRight', EditorFooterContentRightDirective);
  1855. }());
  1856. /**
  1857. @ngdoc directive
  1858. @name umbraco.directives.directive:umbEditorHeader
  1859. @restrict E
  1860. @scope
  1861. @description
  1862. Use this directive to construct a header inside the main editor window.
  1863. <h3>Markup example</h3>
  1864. <pre>
  1865. <div ng-controller="MySection.Controller as vm">
  1866. <form name="mySectionForm" novalidate>
  1867. <umb-editor-view>
  1868. <umb-editor-header
  1869. name="vm.content.name"
  1870. hide-alias="true"
  1871. hide-description="true"
  1872. hide-icon="true">
  1873. </umb-editor-header>
  1874. <umb-editor-container>
  1875. // main content here
  1876. </umb-editor-container>
  1877. <umb-editor-footer>
  1878. // footer content here
  1879. </umb-editor-footer>
  1880. </umb-editor-view>
  1881. </form>
  1882. </div>
  1883. </pre>
  1884. <h3>Markup example - with tabs</h3>
  1885. <pre>
  1886. <div ng-controller="MySection.Controller as vm">
  1887. <form name="mySectionForm" val-form-manager novalidate>
  1888. <umb-editor-view umb-tabs>
  1889. <umb-editor-header
  1890. name="vm.content.name"
  1891. tabs="vm.content.tabs"
  1892. hide-alias="true"
  1893. hide-description="true"
  1894. hide-icon="true">
  1895. </umb-editor-header>
  1896. <umb-editor-container>
  1897. <umb-tabs-content class="form-horizontal" view="true">
  1898. <umb-tab id="tab{{tab.id}}" ng-repeat="tab in vm.content.tabs" rel="{{tab.id}}">
  1899. <div ng-show="tab.alias==='tab1'">
  1900. // tab 1 content
  1901. </div>
  1902. <div ng-show="tab.alias==='tab2'">
  1903. // tab 2 content
  1904. </div>
  1905. </umb-tab>
  1906. </umb-tabs-content>
  1907. </umb-editor-container>
  1908. <umb-editor-footer>
  1909. // footer content here
  1910. </umb-editor-footer>
  1911. </umb-editor-view>
  1912. </form>
  1913. </div>
  1914. </pre>
  1915. <h3>Controller example - with tabs</h3>
  1916. <pre>
  1917. (function () {
  1918. "use strict";
  1919. function Controller() {
  1920. var vm = this;
  1921. vm.content = {
  1922. name: "",
  1923. tabs: [
  1924. {
  1925. id: 1,
  1926. label: "Tab 1",
  1927. alias: "tab1",
  1928. active: true
  1929. },
  1930. {
  1931. id: 2,
  1932. label: "Tab 2",
  1933. alias: "tab2",
  1934. active: false
  1935. }
  1936. ]
  1937. };
  1938. }
  1939. angular.module("umbraco").controller("MySection.Controller", Controller);
  1940. })();
  1941. </pre>
  1942. <h3>Markup example - with sub views</h3>
  1943. <pre>
  1944. <div ng-controller="MySection.Controller as vm">
  1945. <form name="mySectionForm" val-form-manager novalidate>
  1946. <umb-editor-view>
  1947. <umb-editor-header
  1948. name="vm.content.name"
  1949. navigation="vm.content.navigation"
  1950. hide-alias="true"
  1951. hide-description="true"
  1952. hide-icon="true">
  1953. </umb-editor-header>
  1954. <umb-editor-container>
  1955. <umb-editor-sub-views
  1956. sub-views="vm.content.navigation"
  1957. model="vm.content">
  1958. </umb-editor-sub-views>
  1959. </umb-editor-container>
  1960. <umb-editor-footer>
  1961. // footer content here
  1962. </umb-editor-footer>
  1963. </umb-editor-view>
  1964. </form>
  1965. </div>
  1966. </pre>
  1967. <h3>Controller example - with sub views</h3>
  1968. <pre>
  1969. (function () {
  1970. "use strict";
  1971. function Controller() {
  1972. var vm = this;
  1973. vm.content = {
  1974. name: "",
  1975. navigation: [
  1976. {
  1977. "name": "Section 1",
  1978. "icon": "icon-document-dashed-line",
  1979. "view": "/App_Plugins/path/to/html.html",
  1980. "active": true
  1981. },
  1982. {
  1983. "name": "Section 2",
  1984. "icon": "icon-list",
  1985. "view": "/App_Plugins/path/to/html.html",
  1986. }
  1987. ]
  1988. };
  1989. }
  1990. angular.module("umbraco").controller("MySection.Controller", Controller);
  1991. })();
  1992. </pre>
  1993. <h3>Use in combination with</h3>
  1994. <ul>
  1995. <li>{@link umbraco.directives.directive:umbEditorView umbEditorView}</li>
  1996. <li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
  1997. <li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
  1998. </ul>
  1999. @param {string} name The content name.
  2000. @param {array=} tabs Array of tabs. See example above.
  2001. @param {array=} navigation Array of sub views. See example above.
  2002. @param {boolean=} nameLocked Set to <code>true</code> to lock the name.
  2003. @param {object=} menu Add a context menu to the editor.
  2004. @param {string=} icon Show and edit the content icon. Opens an overlay to change the icon.
  2005. @param {boolean=} hideIcon Set to <code>true</code> to hide icon.
  2006. @param {string=} alias show and edit the content alias.
  2007. @param {boolean=} hideAlias Set to <code>true</code> to hide alias.
  2008. @param {string=} description Add a description to the content.
  2009. @param {boolean=} hideDescription Set to <code>true</code> to hide description.
  2010. **/
  2011. (function () {
  2012. 'use strict';
  2013. function EditorHeaderDirective(iconHelper) {
  2014. function link(scope, el, attr, ctrl) {
  2015. scope.openIconPicker = function () {
  2016. scope.dialogModel = {
  2017. view: 'iconpicker',
  2018. show: true,
  2019. submit: function (model) {
  2020. /* ensure an icon is selected, because on focus on close button
  2021. or an element in background no icon is submitted. So don't clear/update existing icon/preview.
  2022. */
  2023. if (model.icon) {
  2024. if (model.color) {
  2025. scope.icon = model.icon + ' ' + model.color;
  2026. } else {
  2027. scope.icon = model.icon;
  2028. }
  2029. // set the icon form to dirty
  2030. scope.iconForm.$setDirty();
  2031. }
  2032. scope.dialogModel.show = false;
  2033. scope.dialogModel = null;
  2034. }
  2035. };
  2036. };
  2037. }
  2038. var directive = {
  2039. transclude: true,
  2040. restrict: 'E',
  2041. replace: true,
  2042. templateUrl: 'views/components/editor/umb-editor-header.html',
  2043. scope: {
  2044. tabs: '=',
  2045. actions: '=',
  2046. name: '=',
  2047. nameLocked: '=',
  2048. menu: '=',
  2049. icon: '=',
  2050. hideIcon: '@',
  2051. alias: '=',
  2052. hideAlias: '@',
  2053. description: '=',
  2054. hideDescription: '@',
  2055. descriptionLocked: '@',
  2056. navigation: '=',
  2057. key: '='
  2058. },
  2059. link: link
  2060. };
  2061. return directive;
  2062. }
  2063. angular.module('umbraco.directives').directive('umbEditorHeader', EditorHeaderDirective);
  2064. }());
  2065. (function () {
  2066. 'use strict';
  2067. function EditorMenuDirective($injector, treeService, navigationService, umbModelMapper, appState) {
  2068. function link(scope, el, attr, ctrl) {
  2069. //adds a handler to the context menu item click, we need to handle this differently
  2070. //depending on what the menu item is supposed to do.
  2071. scope.executeMenuItem = function (action) {
  2072. navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection);
  2073. };
  2074. //callback method to go and get the options async
  2075. scope.getOptions = function () {
  2076. if (!scope.currentNode) {
  2077. return;
  2078. }
  2079. //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu)
  2080. appState.setMenuState('currentNode', scope.currentNode);
  2081. if (!scope.actions) {
  2082. treeService.getMenu({ treeNode: scope.currentNode }).then(function (data) {
  2083. scope.actions = data.menuItems;
  2084. });
  2085. }
  2086. };
  2087. }
  2088. var directive = {
  2089. restrict: 'E',
  2090. replace: true,
  2091. templateUrl: 'views/components/editor/umb-editor-menu.html',
  2092. link: link,
  2093. scope: {
  2094. currentNode: '=',
  2095. currentSection: '@'
  2096. }
  2097. };
  2098. return directive;
  2099. }
  2100. angular.module('umbraco.directives').directive('umbEditorMenu', EditorMenuDirective);
  2101. }());
  2102. (function () {
  2103. 'use strict';
  2104. function EditorNavigationDirective() {
  2105. function link(scope, el, attr, ctrl) {
  2106. scope.showNavigation = true;
  2107. scope.clickNavigationItem = function (selectedItem) {
  2108. setItemToActive(selectedItem);
  2109. runItemAction(selectedItem);
  2110. };
  2111. function runItemAction(selectedItem) {
  2112. if (selectedItem.action) {
  2113. selectedItem.action(selectedItem);
  2114. }
  2115. }
  2116. function setItemToActive(selectedItem) {
  2117. // set all other views to inactive
  2118. if (selectedItem.view) {
  2119. for (var index = 0; index < scope.navigation.length; index++) {
  2120. var item = scope.navigation[index];
  2121. item.active = false;
  2122. }
  2123. // set view to active
  2124. selectedItem.active = true;
  2125. }
  2126. }
  2127. function activate() {
  2128. // hide navigation if there is only 1 item
  2129. if (scope.navigation.length <= 1) {
  2130. scope.showNavigation = false;
  2131. }
  2132. }
  2133. activate();
  2134. }
  2135. var directive = {
  2136. restrict: 'E',
  2137. replace: true,
  2138. templateUrl: 'views/components/editor/umb-editor-navigation.html',
  2139. scope: { navigation: '=' },
  2140. link: link
  2141. };
  2142. return directive;
  2143. }
  2144. angular.module('umbraco.directives.html').directive('umbEditorNavigation', EditorNavigationDirective);
  2145. }());
  2146. (function () {
  2147. 'use strict';
  2148. function EditorSubViewsDirective() {
  2149. function link(scope, el, attr, ctrl) {
  2150. scope.activeView = {};
  2151. // set toolbar from selected navigation item
  2152. function setActiveView(items) {
  2153. for (var index = 0; index < items.length; index++) {
  2154. var item = items[index];
  2155. if (item.active && item.view) {
  2156. scope.activeView = item;
  2157. }
  2158. }
  2159. }
  2160. // watch for navigation changes
  2161. scope.$watch('subViews', function (newValue, oldValue) {
  2162. if (newValue) {
  2163. setActiveView(newValue);
  2164. }
  2165. }, true);
  2166. }
  2167. var directive = {
  2168. restrict: 'E',
  2169. replace: true,
  2170. templateUrl: 'views/components/editor/umb-editor-sub-views.html',
  2171. scope: {
  2172. subViews: '=',
  2173. model: '='
  2174. },
  2175. link: link
  2176. };
  2177. return directive;
  2178. }
  2179. angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective);
  2180. }());
  2181. /**
  2182. @ngdoc directive
  2183. @name umbraco.directives.directive:umbEditorView
  2184. @restrict E
  2185. @scope
  2186. @description
  2187. Use this directive to construct the main editor window.
  2188. <h3>Markup example</h3>
  2189. <pre>
  2190. <div ng-controller="MySection.Controller as vm">
  2191. <form name="mySectionForm" novalidate>
  2192. <umb-editor-view>
  2193. <umb-editor-header
  2194. name="vm.content.name"
  2195. hide-alias="true"
  2196. hide-description="true"
  2197. hide-icon="true">
  2198. </umb-editor-header>
  2199. <umb-editor-container>
  2200. // main content here
  2201. </umb-editor-container>
  2202. <umb-editor-footer>
  2203. // footer content here
  2204. </umb-editor-footer>
  2205. </umb-editor-view>
  2206. </form>
  2207. </div>
  2208. </pre>
  2209. <h3>Controller example</h3>
  2210. <pre>
  2211. (function () {
  2212. "use strict";
  2213. function Controller() {
  2214. var vm = this;
  2215. }
  2216. angular.module("umbraco").controller("MySection.Controller", Controller);
  2217. })();
  2218. </pre>
  2219. <h3>Use in combination with</h3>
  2220. <ul>
  2221. <li>{@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}</li>
  2222. <li>{@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}</li>
  2223. <li>{@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}</li>
  2224. </ul>
  2225. **/
  2226. (function () {
  2227. 'use strict';
  2228. function EditorViewDirective() {
  2229. function link(scope, el, attr) {
  2230. if (attr.footer) {
  2231. scope.footer = attr.footer;
  2232. }
  2233. }
  2234. var directive = {
  2235. transclude: true,
  2236. restrict: 'E',
  2237. replace: true,
  2238. templateUrl: 'views/components/editor/umb-editor-view.html',
  2239. link: link
  2240. };
  2241. return directive;
  2242. }
  2243. angular.module('umbraco.directives').directive('umbEditorView', EditorViewDirective);
  2244. }());
  2245. /**
  2246. * @description Utillity directives for key and field events
  2247. **/
  2248. angular.module('umbraco.directives').directive('onKeyup', function () {
  2249. return {
  2250. link: function (scope, elm, attrs) {
  2251. var f = function () {
  2252. scope.$apply(attrs.onKeyup);
  2253. };
  2254. elm.on('keyup', f);
  2255. scope.$on('$destroy', function () {
  2256. elm.off('keyup', f);
  2257. });
  2258. }
  2259. };
  2260. }).directive('onKeydown', function () {
  2261. return {
  2262. link: function (scope, elm, attrs) {
  2263. var f = function () {
  2264. scope.$apply(attrs.onKeydown);
  2265. };
  2266. elm.on('keydown', f);
  2267. scope.$on('$destroy', function () {
  2268. elm.off('keydown', f);
  2269. });
  2270. }
  2271. };
  2272. }).directive('onBlur', function () {
  2273. return {
  2274. link: function (scope, elm, attrs) {
  2275. var f = function () {
  2276. scope.$apply(attrs.onBlur);
  2277. };
  2278. elm.on('blur', f);
  2279. scope.$on('$destroy', function () {
  2280. elm.off('blur', f);
  2281. });
  2282. }
  2283. };
  2284. }).directive('onFocus', function () {
  2285. return {
  2286. link: function (scope, elm, attrs) {
  2287. var f = function () {
  2288. scope.$apply(attrs.onFocus);
  2289. };
  2290. elm.on('focus', f);
  2291. scope.$on('$destroy', function () {
  2292. elm.off('focus', f);
  2293. });
  2294. }
  2295. };
  2296. }).directive('onDragEnter', function () {
  2297. return {
  2298. link: function (scope, elm, attrs) {
  2299. var f = function () {
  2300. scope.$apply(attrs.onDragEnter);
  2301. };
  2302. elm.on('dragenter', f);
  2303. scope.$on('$destroy', function () {
  2304. elm.off('dragenter', f);
  2305. });
  2306. }
  2307. };
  2308. }).directive('onDragLeave', function () {
  2309. return function (scope, elm, attrs) {
  2310. var f = function (event) {
  2311. var rect = this.getBoundingClientRect();
  2312. var getXY = function getCursorPosition(event) {
  2313. var x, y;
  2314. if (typeof event.clientX === 'undefined') {
  2315. // try touch screen
  2316. x = event.pageX + document.documentElement.scrollLeft;
  2317. y = event.pageY + document.documentElement.scrollTop;
  2318. } else {
  2319. x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
  2320. y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
  2321. }
  2322. return {
  2323. x: x,
  2324. y: y
  2325. };
  2326. };
  2327. var e = getXY(event.originalEvent);
  2328. // Check the mouseEvent coordinates are outside of the rectangle
  2329. if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) {
  2330. scope.$apply(attrs.onDragLeave);
  2331. }
  2332. };
  2333. elm.on('dragleave', f);
  2334. scope.$on('$destroy', function () {
  2335. elm.off('dragleave', f);
  2336. });
  2337. };
  2338. }).directive('onDragOver', function () {
  2339. return {
  2340. link: function (scope, elm, attrs) {
  2341. var f = function () {
  2342. scope.$apply(attrs.onDragOver);
  2343. };
  2344. elm.on('dragover', f);
  2345. scope.$on('$destroy', function () {
  2346. elm.off('dragover', f);
  2347. });
  2348. }
  2349. };
  2350. }).directive('onDragStart', function () {
  2351. return {
  2352. link: function (scope, elm, attrs) {
  2353. var f = function () {
  2354. scope.$apply(attrs.onDragStart);
  2355. };
  2356. elm.on('dragstart', f);
  2357. scope.$on('$destroy', function () {
  2358. elm.off('dragstart', f);
  2359. });
  2360. }
  2361. };
  2362. }).directive('onDragEnd', function () {
  2363. return {
  2364. link: function (scope, elm, attrs) {
  2365. var f = function () {
  2366. scope.$apply(attrs.onDragEnd);
  2367. };
  2368. elm.on('dragend', f);
  2369. scope.$on('$destroy', function () {
  2370. elm.off('dragend', f);
  2371. });
  2372. }
  2373. };
  2374. }).directive('onDrop', function () {
  2375. return {
  2376. link: function (scope, elm, attrs) {
  2377. var f = function () {
  2378. scope.$apply(attrs.onDrop);
  2379. };
  2380. elm.on('drop', f);
  2381. scope.$on('$destroy', function () {
  2382. elm.off('drop', f);
  2383. });
  2384. }
  2385. };
  2386. }).directive('onOutsideClick', function ($timeout) {
  2387. return function (scope, element, attrs) {
  2388. var eventBindings = [];
  2389. function oneTimeClick(event) {
  2390. var el = event.target.nodeName;
  2391. //ignore link and button clicks
  2392. var els = [
  2393. 'INPUT',
  2394. 'A',
  2395. 'BUTTON'
  2396. ];
  2397. if (els.indexOf(el) >= 0) {
  2398. return;
  2399. }
  2400. // ignore clicks on new overlay
  2401. var parents = $(event.target).parents('a,button,.umb-overlay');
  2402. if (parents.length > 0) {
  2403. return;
  2404. }
  2405. // ignore clicks on dialog from old dialog service
  2406. var oldDialog = $(event.target).parents('#old-dialog-service');
  2407. if (oldDialog.length === 1) {
  2408. return;
  2409. }
  2410. // ignore clicks in tinyMCE dropdown(floatpanel)
  2411. var floatpanel = $(event.target).closest('.mce-floatpanel');
  2412. if (floatpanel.length === 1) {
  2413. return;
  2414. }
  2415. //ignore clicks inside this element
  2416. if ($(element).has($(event.target)).length > 0) {
  2417. return;
  2418. }
  2419. scope.$apply(attrs.onOutsideClick);
  2420. }
  2421. $timeout(function () {
  2422. if ('bindClickOn' in attrs) {
  2423. eventBindings.push(scope.$watch(function () {
  2424. return attrs.bindClickOn;
  2425. }, function (newValue) {
  2426. if (newValue === 'true') {
  2427. $(document).on('click', oneTimeClick);
  2428. } else {
  2429. $(document).off('click', oneTimeClick);
  2430. }
  2431. }));
  2432. } else {
  2433. $(document).on('click', oneTimeClick);
  2434. }
  2435. scope.$on('$destroy', function () {
  2436. $(document).off('click', oneTimeClick);
  2437. // unbind watchers
  2438. for (var e in eventBindings) {
  2439. eventBindings[e]();
  2440. }
  2441. });
  2442. }); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution.
  2443. };
  2444. }).directive('onRightClick', function () {
  2445. document.oncontextmenu = function (e) {
  2446. if (e.target.hasAttribute('on-right-click')) {
  2447. e.preventDefault();
  2448. e.stopPropagation();
  2449. return false;
  2450. }
  2451. };
  2452. return function (scope, el, attrs) {
  2453. el.on('contextmenu', function (e) {
  2454. e.preventDefault();
  2455. e.stopPropagation();
  2456. scope.$apply(attrs.onRightClick);
  2457. return false;
  2458. });
  2459. };
  2460. }).directive('onDelayedMouseleave', function ($timeout, $parse) {
  2461. return {
  2462. restrict: 'A',
  2463. link: function (scope, element, attrs, ctrl) {
  2464. var active = false;
  2465. var fn = $parse(attrs.onDelayedMouseleave);
  2466. var leave_f = function (event) {
  2467. var callback = function () {
  2468. fn(scope, { $event: event });
  2469. };
  2470. active = false;
  2471. $timeout(function () {
  2472. if (active === false) {
  2473. scope.$apply(callback);
  2474. }
  2475. }, 650);
  2476. };
  2477. var enter_f = function (event, args) {
  2478. active = true;
  2479. };
  2480. element.on('mouseleave', leave_f);
  2481. element.on('mouseenter', enter_f);
  2482. //unsub events
  2483. scope.$on('$destroy', function () {
  2484. element.off('mouseleave', leave_f);
  2485. element.off('mouseenter', enter_f);
  2486. });
  2487. }
  2488. };
  2489. });
  2490. /*
  2491. http://vitalets.github.io/checklist-model/
  2492. <label ng-repeat="role in roles">
  2493. <input type="checkbox" checklist-model="user.roles" checklist-value="role.id"> {{role.text}}
  2494. </label>
  2495. */
  2496. angular.module('umbraco.directives').directive('checklistModel', [
  2497. '$parse',
  2498. '$compile',
  2499. function ($parse, $compile) {
  2500. // contains
  2501. function contains(arr, item) {
  2502. if (angular.isArray(arr)) {
  2503. for (var i = 0; i < arr.length; i++) {
  2504. if (angular.equals(arr[i], item)) {
  2505. return true;
  2506. }
  2507. }
  2508. }
  2509. return false;
  2510. }
  2511. // add
  2512. function add(arr, item) {
  2513. arr = angular.isArray(arr) ? arr : [];
  2514. for (var i = 0; i < arr.length; i++) {
  2515. if (angular.equals(arr[i], item)) {
  2516. return arr;
  2517. }
  2518. }
  2519. arr.push(item);
  2520. return arr;
  2521. }
  2522. // remove
  2523. function remove(arr, item) {
  2524. if (angular.isArray(arr)) {
  2525. for (var i = 0; i < arr.length; i++) {
  2526. if (angular.equals(arr[i], item)) {
  2527. arr.splice(i, 1);
  2528. break;
  2529. }
  2530. }
  2531. }
  2532. return arr;
  2533. }
  2534. // http://stackoverflow.com/a/19228302/1458162
  2535. function postLinkFn(scope, elem, attrs) {
  2536. // compile with `ng-model` pointing to `checked`
  2537. $compile(elem)(scope);
  2538. // getter / setter for original model
  2539. var getter = $parse(attrs.checklistModel);
  2540. var setter = getter.assign;
  2541. // value added to list
  2542. var value = $parse(attrs.checklistValue)(scope.$parent);
  2543. // watch UI checked change
  2544. scope.$watch('checked', function (newValue, oldValue) {
  2545. if (newValue === oldValue) {
  2546. return;
  2547. }
  2548. var current = getter(scope.$parent);
  2549. if (newValue === true) {
  2550. setter(scope.$parent, add(current, value));
  2551. } else {
  2552. setter(scope.$parent, remove(current, value));
  2553. }
  2554. });
  2555. // watch original model change
  2556. scope.$parent.$watch(attrs.checklistModel, function (newArr, oldArr) {
  2557. scope.checked = contains(newArr, value);
  2558. }, true);
  2559. }
  2560. return {
  2561. restrict: 'A',
  2562. priority: 1000,
  2563. terminal: true,
  2564. scope: true,
  2565. compile: function (tElement, tAttrs) {
  2566. if (tElement[0].tagName !== 'INPUT' || !tElement.attr('type', 'checkbox')) {
  2567. throw 'checklist-model should be applied to `input[type="checkbox"]`.';
  2568. }
  2569. if (!tAttrs.checklistValue) {
  2570. throw 'You should provide `checklist-value`.';
  2571. }
  2572. // exclude recursion
  2573. tElement.removeAttr('checklist-model');
  2574. // local scope var storing individual checkbox model
  2575. tElement.attr('ng-model', 'checked');
  2576. return postLinkFn;
  2577. }
  2578. };
  2579. }
  2580. ]);
  2581. angular.module('umbraco.directives').directive('contenteditable', function () {
  2582. return {
  2583. require: 'ngModel',
  2584. link: function (scope, element, attrs, ngModel) {
  2585. function read() {
  2586. ngModel.$setViewValue(element.html());
  2587. }
  2588. ngModel.$render = function () {
  2589. element.html(ngModel.$viewValue || '');
  2590. };
  2591. element.bind('focus', function () {
  2592. var range = document.createRange();
  2593. range.selectNodeContents(element[0]);
  2594. var sel = window.getSelection();
  2595. sel.removeAllRanges();
  2596. sel.addRange(range);
  2597. });
  2598. element.bind('blur keyup change', function () {
  2599. scope.$apply(read);
  2600. });
  2601. }
  2602. };
  2603. });
  2604. /**
  2605. * @ngdoc directive
  2606. * @name umbraco.directives.directive:fixNumber
  2607. * @restrict A
  2608. * @description Used in conjunction with type='number' input fields to ensure that the bound value is converted to a number when using ng-model
  2609. * because normally it thinks it's a string and also validation doesn't work correctly due to an angular bug.
  2610. **/
  2611. function fixNumber($parse) {
  2612. return {
  2613. restrict: 'A',
  2614. require: 'ngModel',
  2615. link: function (scope, elem, attrs, ctrl) {
  2616. //parse ngModel onload
  2617. var modelVal = scope.$eval(attrs.ngModel);
  2618. if (modelVal) {
  2619. var asNum = parseFloat(modelVal, 10);
  2620. if (!isNaN(asNum)) {
  2621. $parse(attrs.ngModel).assign(scope, asNum);
  2622. }
  2623. }
  2624. //always return an int to the model
  2625. ctrl.$parsers.push(function (value) {
  2626. if (value === 0) {
  2627. return 0;
  2628. }
  2629. return parseFloat(value || '', 10);
  2630. });
  2631. //always try to format the model value as an int
  2632. ctrl.$formatters.push(function (value) {
  2633. if (angular.isString(value)) {
  2634. return parseFloat(value, 10);
  2635. }
  2636. return value;
  2637. });
  2638. //This fixes this angular issue:
  2639. //https://github.com/angular/angular.js/issues/2144
  2640. // which doesn't actually validate the number input properly since the model only changes when a real number is entered
  2641. // but the input box still allows non-numbers to be entered which do not validate (only via html5)
  2642. if (typeof elem.prop('validity') === 'undefined') {
  2643. return;
  2644. }
  2645. elem.bind('input', function (e) {
  2646. var validity = elem.prop('validity');
  2647. scope.$apply(function () {
  2648. ctrl.$setValidity('number', !validity.badInput);
  2649. });
  2650. });
  2651. }
  2652. };
  2653. }
  2654. angular.module('umbraco.directives').directive('fixNumber', fixNumber);
  2655. angular.module('umbraco.directives').directive('focusWhen', function ($timeout) {
  2656. return {
  2657. restrict: 'A',
  2658. link: function (scope, elm, attrs, ctrl) {
  2659. attrs.$observe('focusWhen', function (newValue) {
  2660. if (newValue === 'true') {
  2661. $timeout(function () {
  2662. elm.focus();
  2663. });
  2664. }
  2665. });
  2666. }
  2667. };
  2668. });
  2669. /**
  2670. * @ngdoc directive
  2671. * @name umbraco.directives.directive:hexBgColor
  2672. * @restrict A
  2673. * @description Used to set a hex background color on an element, this will detect valid hex and when it is valid it will set the color, otherwise
  2674. * a color will not be set.
  2675. **/
  2676. function hexBgColor() {
  2677. return {
  2678. restrict: 'A',
  2679. link: function (scope, element, attr, formCtrl) {
  2680. var origColor = null;
  2681. if (attr.hexBgOrig) {
  2682. //set the orig based on the attribute if there is one
  2683. origColor = attr.hexBgOrig;
  2684. }
  2685. attr.$observe('hexBgColor', function (newVal) {
  2686. if (newVal) {
  2687. if (!origColor) {
  2688. //get the orig color before changing it
  2689. origColor = element.css('border-color');
  2690. }
  2691. //validate it - test with and without the leading hash.
  2692. if (/^([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) {
  2693. element.css('background-color', '#' + newVal);
  2694. return;
  2695. }
  2696. if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(newVal)) {
  2697. element.css('background-color', newVal);
  2698. return;
  2699. }
  2700. }
  2701. element.css('background-color', origColor);
  2702. });
  2703. }
  2704. };
  2705. }
  2706. angular.module('umbraco.directives').directive('hexBgColor', hexBgColor);
  2707. /**
  2708. * @ngdoc directive
  2709. * @name umbraco.directives.directive:hotkey
  2710. **/
  2711. angular.module('umbraco.directives').directive('hotkey', function ($window, keyboardService, $log) {
  2712. return function (scope, el, attrs) {
  2713. var options = {};
  2714. var keyCombo = attrs.hotkey;
  2715. if (!keyCombo) {
  2716. //support data binding
  2717. keyCombo = scope.$eval(attrs['hotkey']);
  2718. }
  2719. function activate() {
  2720. if (keyCombo) {
  2721. // disable shortcuts in input fields if keycombo is 1 character
  2722. if (keyCombo.length === 1) {
  2723. options = { inputDisabled: true };
  2724. }
  2725. keyboardService.bind(keyCombo, function () {
  2726. var element = $(el);
  2727. var activeElementType = document.activeElement.tagName;
  2728. var clickableElements = [
  2729. 'A',
  2730. 'BUTTON'
  2731. ];
  2732. if (element.is('a,div,button,input[type=\'button\'],input[type=\'submit\'],input[type=\'checkbox\']') && !element.is(':disabled')) {
  2733. if (element.is(':visible') || attrs.hotkeyWhenHidden) {
  2734. if (attrs.hotkeyWhen && attrs.hotkeyWhen === 'false') {
  2735. return;
  2736. }
  2737. // when keycombo is enter and a link or button has focus - click the link or button instead of using the hotkey
  2738. if (keyCombo === 'enter' && clickableElements.indexOf(activeElementType) === 0) {
  2739. document.activeElement.click();
  2740. } else {
  2741. element.click();
  2742. }
  2743. }
  2744. } else {
  2745. element.focus();
  2746. }
  2747. }, options);
  2748. el.on('$destroy', function () {
  2749. keyboardService.unbind(keyCombo);
  2750. });
  2751. }
  2752. }
  2753. activate();
  2754. };
  2755. });
  2756. /**
  2757. @ngdoc directive
  2758. @name umbraco.directives.directive:preventDefault
  2759. @description
  2760. Use this directive to prevent default action of an element. Effectively implementing <a href="https://api.jquery.com/event.preventdefault/">jQuery's preventdefault</a>
  2761. <h3>Markup example</h3>
  2762. <pre>
  2763. <a href="https://umbraco.com" prevent-default>Don't go to Umbraco.com</a>
  2764. </pre>
  2765. **/
  2766. angular.module('umbraco.directives').directive('preventDefault', function () {
  2767. return function (scope, element, attrs) {
  2768. var enabled = true;
  2769. //check if there's a value for the attribute, if there is and it's false then we conditionally don't
  2770. //prevent default.
  2771. if (attrs.preventDefault) {
  2772. attrs.$observe('preventDefault', function (newVal) {
  2773. enabled = newVal === 'false' || newVal === 0 || newVal === false ? false : true;
  2774. });
  2775. }
  2776. $(element).click(function (event) {
  2777. if (event.metaKey || event.ctrlKey) {
  2778. return;
  2779. } else {
  2780. if (enabled === true) {
  2781. event.preventDefault();
  2782. }
  2783. }
  2784. });
  2785. };
  2786. });
  2787. /**
  2788. * @ngdoc directive
  2789. * @name umbraco.directives.directive:preventEnterSubmit
  2790. * @description prevents a form from submitting when the enter key is pressed on an input field
  2791. **/
  2792. angular.module('umbraco.directives').directive('preventEnterSubmit', function () {
  2793. return function (scope, element, attrs) {
  2794. var enabled = true;
  2795. //check if there's a value for the attribute, if there is and it's false then we conditionally don't
  2796. //prevent default.
  2797. if (attrs.preventEnterSubmit) {
  2798. attrs.$observe('preventEnterSubmit', function (newVal) {
  2799. enabled = newVal === 'false' || newVal === 0 || newVal === false ? false : true;
  2800. });
  2801. }
  2802. $(element).keypress(function (event) {
  2803. if (event.which === 13) {
  2804. event.preventDefault();
  2805. }
  2806. });
  2807. };
  2808. });
  2809. /**
  2810. * @ngdoc directive
  2811. * @name umbraco.directives.directive:resizeToContent
  2812. * @element div
  2813. * @function
  2814. *
  2815. * @description
  2816. * Resize iframe's automatically to fit to the content they contain
  2817. *
  2818. * @example
  2819. <example module="umbraco.directives">
  2820. <file name="index.html">
  2821. <iframe resize-to-content src="meh.html"></iframe>
  2822. </file>
  2823. </example>
  2824. */
  2825. angular.module('umbraco.directives').directive('resizeToContent', function ($window, $timeout) {
  2826. return function (scope, el, attrs) {
  2827. var iframe = el[0];
  2828. var iframeWin = iframe.contentWindow || iframe.contentDocument.parentWindow;
  2829. if (iframeWin.document.body) {
  2830. $timeout(function () {
  2831. var height = iframeWin.document.documentElement.scrollHeight || iframeWin.document.body.scrollHeight;
  2832. el.height(height);
  2833. }, 3000);
  2834. }
  2835. };
  2836. });
  2837. angular.module('umbraco.directives').directive('selectOnFocus', function () {
  2838. return function (scope, el, attrs) {
  2839. $(el).bind('click', function () {
  2840. var editmode = $(el).data('editmode');
  2841. //If editmode is true a click is handled like a normal click
  2842. if (!editmode) {
  2843. //Initial click, select entire text
  2844. this.select();
  2845. //Set the edit mode so subsequent clicks work normally
  2846. $(el).data('editmode', true);
  2847. }
  2848. }).bind('blur', function () {
  2849. //Reset on focus lost
  2850. $(el).data('editmode', false);
  2851. });
  2852. };
  2853. });
  2854. angular.module('umbraco.directives').directive('umbAutoFocus', function ($timeout) {
  2855. return function (scope, element, attr) {
  2856. var update = function () {
  2857. //if it uses its default naming
  2858. if (element.val() === '' || attr.focusOnFilled) {
  2859. element.focus();
  2860. }
  2861. };
  2862. $timeout(function () {
  2863. update();
  2864. });
  2865. };
  2866. });
  2867. angular.module('umbraco.directives').directive('umbAutoResize', function ($timeout) {
  2868. return {
  2869. require: [
  2870. '^?umbTabs',
  2871. 'ngModel'
  2872. ],
  2873. link: function (scope, element, attr, controllersArr) {
  2874. var domEl = element[0];
  2875. var domElType = domEl.type;
  2876. var umbTabsController = controllersArr[0];
  2877. var ngModelController = controllersArr[1];
  2878. // IE elements
  2879. var isIEFlag = false;
  2880. var wrapper = angular.element('#umb-ie-resize-input-wrapper');
  2881. var mirror = angular.element('<span style="white-space:pre;"></span>');
  2882. function isIE() {
  2883. var ua = window.navigator.userAgent;
  2884. var msie = ua.indexOf('MSIE ');
  2885. if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./) || navigator.userAgent.match(/Edge\/\d+/)) {
  2886. return true;
  2887. } else {
  2888. return false;
  2889. }
  2890. }
  2891. function activate() {
  2892. // check if browser is Internet Explorere
  2893. isIEFlag = isIE();
  2894. // scrollWidth on element does not work in IE on inputs
  2895. // we have to do some dirty dom element copying.
  2896. if (isIEFlag === true && domElType === 'text') {
  2897. setupInternetExplorerElements();
  2898. }
  2899. }
  2900. function setupInternetExplorerElements() {
  2901. if (!wrapper.length) {
  2902. wrapper = angular.element('<div id="umb-ie-resize-input-wrapper" style="position:fixed; top:-999px; left:0;"></div>');
  2903. angular.element('body').append(wrapper);
  2904. }
  2905. angular.forEach([
  2906. 'fontFamily',
  2907. 'fontSize',
  2908. 'fontWeight',
  2909. 'fontStyle',
  2910. 'letterSpacing',
  2911. 'textTransform',
  2912. 'wordSpacing',
  2913. 'textIndent',
  2914. 'boxSizing',
  2915. 'borderRightWidth',
  2916. 'borderLeftWidth',
  2917. 'borderLeftStyle',
  2918. 'borderRightStyle',
  2919. 'paddingLeft',
  2920. 'paddingRight',
  2921. 'marginLeft',
  2922. 'marginRight'
  2923. ], function (value) {
  2924. mirror.css(value, element.css(value));
  2925. });
  2926. wrapper.append(mirror);
  2927. }
  2928. function resizeInternetExplorerInput() {
  2929. mirror.text(element.val() || attr.placeholder);
  2930. element.css('width', mirror.outerWidth() + 1);
  2931. }
  2932. function resizeInput() {
  2933. if (domEl.scrollWidth !== domEl.clientWidth) {
  2934. if (ngModelController.$modelValue) {
  2935. element.width(domEl.scrollWidth);
  2936. }
  2937. }
  2938. if (!ngModelController.$modelValue && attr.placeholder) {
  2939. attr.$set('size', attr.placeholder.length);
  2940. element.width('auto');
  2941. }
  2942. }
  2943. function resizeTextarea() {
  2944. if (domEl.scrollHeight !== domEl.clientHeight) {
  2945. element.height(domEl.scrollHeight);
  2946. }
  2947. }
  2948. var update = function (force) {
  2949. if (force === true) {
  2950. if (domElType === 'textarea') {
  2951. element.height(0);
  2952. } else if (domElType === 'text') {
  2953. element.width(0);
  2954. }
  2955. }
  2956. if (isIEFlag === true && domElType === 'text') {
  2957. resizeInternetExplorerInput();
  2958. } else {
  2959. if (domElType === 'textarea') {
  2960. resizeTextarea();
  2961. } else if (domElType === 'text') {
  2962. resizeInput();
  2963. }
  2964. }
  2965. };
  2966. activate();
  2967. //listen for tab changes
  2968. if (umbTabsController != null) {
  2969. umbTabsController.onTabShown(function (args) {
  2970. update();
  2971. });
  2972. }
  2973. // listen for ng-model changes
  2974. var unbindModelWatcher = scope.$watch(function () {
  2975. return ngModelController.$modelValue;
  2976. }, function (newValue) {
  2977. update(true);
  2978. });
  2979. scope.$on('$destroy', function () {
  2980. element.unbind('keyup keydown keypress change', update);
  2981. element.unbind('blur', update(true));
  2982. unbindModelWatcher();
  2983. // clean up IE dom element
  2984. if (isIEFlag === true && domElType === 'text') {
  2985. mirror.remove();
  2986. }
  2987. });
  2988. }
  2989. };
  2990. });
  2991. /*
  2992. example usage: <textarea json-edit="myObject" rows="8" class="form-control"></textarea>
  2993. jsonEditing is a string which we edit in a textarea. we try parsing to JSON with each change. when it is valid, propagate model changes via ngModelCtrl
  2994. use isolate scope to prevent model propagation when invalid - will update manually. cannot replace with template, or will override ngModelCtrl, and not hide behind facade
  2995. will override element type to textarea and add own attribute ngModel tied to jsonEditing
  2996. */
  2997. angular.module('umbraco.directives').directive('umbRawModel', function () {
  2998. return {
  2999. restrict: 'A',
  3000. require: 'ngModel',
  3001. template: '<textarea ng-model="jsonEditing"></textarea>',
  3002. replace: true,
  3003. scope: {
  3004. model: '=umbRawModel',
  3005. validateOn: '='
  3006. },
  3007. link: function (scope, element, attrs, ngModelCtrl) {
  3008. function setEditing(value) {
  3009. scope.jsonEditing = angular.copy(jsonToString(value));
  3010. }
  3011. function updateModel(value) {
  3012. scope.model = stringToJson(value);
  3013. }
  3014. function setValid() {
  3015. ngModelCtrl.$setValidity('json', true);
  3016. }
  3017. function setInvalid() {
  3018. ngModelCtrl.$setValidity('json', false);
  3019. }
  3020. function stringToJson(text) {
  3021. try {
  3022. return angular.fromJson(text);
  3023. } catch (err) {
  3024. setInvalid();
  3025. return text;
  3026. }
  3027. }
  3028. function jsonToString(object) {
  3029. // better than JSON.stringify(), because it formats + filters $$hashKey etc.
  3030. // NOTE that this will remove all $-prefixed values
  3031. return angular.toJson(object, true);
  3032. }
  3033. function isValidJson(model) {
  3034. var flag = true;
  3035. try {
  3036. angular.fromJson(model);
  3037. } catch (err) {
  3038. flag = false;
  3039. }
  3040. return flag;
  3041. }
  3042. //init
  3043. setEditing(scope.model);
  3044. var onInputChange = function (newval, oldval) {
  3045. if (newval !== oldval) {
  3046. if (isValidJson(newval)) {
  3047. setValid();
  3048. updateModel(newval);
  3049. } else {
  3050. setInvalid();
  3051. }
  3052. }
  3053. };
  3054. if (scope.validateOn) {
  3055. element.on(scope.validateOn, function () {
  3056. scope.$apply(function () {
  3057. onInputChange(scope.jsonEditing);
  3058. });
  3059. });
  3060. } else {
  3061. //check for changes going out
  3062. scope.$watch('jsonEditing', onInputChange, true);
  3063. }
  3064. //check for changes coming in
  3065. scope.$watch('model', function (newval, oldval) {
  3066. if (newval !== oldval) {
  3067. setEditing(newval);
  3068. }
  3069. }, true);
  3070. }
  3071. };
  3072. });
  3073. (function () {
  3074. 'use strict';
  3075. function SelectWhen($timeout) {
  3076. function link(scope, el, attr, ctrl) {
  3077. attr.$observe('umbSelectWhen', function (newValue) {
  3078. if (newValue === 'true') {
  3079. $timeout(function () {
  3080. el.select();
  3081. });
  3082. }
  3083. });
  3084. }
  3085. var directive = {
  3086. restrict: 'A',
  3087. link: link
  3088. };
  3089. return directive;
  3090. }
  3091. angular.module('umbraco.directives').directive('umbSelectWhen', SelectWhen);
  3092. }());
  3093. angular.module('umbraco.directives').directive('gridRte', function (tinyMceService, stylesheetResource, angularHelper, assetsService, $q, $timeout) {
  3094. return {
  3095. scope: {
  3096. uniqueId: '=',
  3097. value: '=',
  3098. onClick: '&',
  3099. onFocus: '&',
  3100. onBlur: '&',
  3101. configuration: '=',
  3102. onMediaPickerClick: '=',
  3103. onEmbedClick: '=',
  3104. onMacroPickerClick: '=',
  3105. onLinkPickerClick: '='
  3106. },
  3107. template: '<textarea ng-model="value" rows="10" class="mceNoEditor" style="overflow:hidden" id="{{uniqueId}}"></textarea>',
  3108. replace: true,
  3109. link: function (scope, element, attrs) {
  3110. var initTiny = function () {
  3111. //we always fetch the default one, and then override parts with our own
  3112. tinyMceService.configuration().then(function (tinyMceConfig) {
  3113. //config value from general tinymce.config file
  3114. var validElements = tinyMceConfig.validElements;
  3115. var fallbackStyles = [
  3116. {
  3117. title: 'Page header',
  3118. block: 'h2'
  3119. },
  3120. {
  3121. title: 'Section header',
  3122. block: 'h3'
  3123. },
  3124. {
  3125. title: 'Paragraph header',
  3126. block: 'h4'
  3127. },
  3128. {
  3129. title: 'Normal',
  3130. block: 'p'
  3131. },
  3132. {
  3133. title: 'Quote',
  3134. block: 'blockquote'
  3135. },
  3136. {
  3137. title: 'Code',
  3138. block: 'code'
  3139. }
  3140. ];
  3141. //These are absolutely required in order for the macros to render inline
  3142. //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce
  3143. var extendedValidElements = '@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]';
  3144. var invalidElements = tinyMceConfig.inValidElements;
  3145. var plugins = _.map(tinyMceConfig.plugins, function (plugin) {
  3146. if (plugin.useOnFrontend) {
  3147. return plugin.name;
  3148. }
  3149. }).join(' ') + ' autoresize';
  3150. //config value on the data type
  3151. var toolbar = [
  3152. 'code',
  3153. 'styleselect',
  3154. 'bold',
  3155. 'italic',
  3156. 'alignleft',
  3157. 'aligncenter',
  3158. 'alignright',
  3159. 'bullist',
  3160. 'numlist',
  3161. 'link',
  3162. 'umbmediapicker',
  3163. 'umbembeddialog'
  3164. ].join(' | ');
  3165. var stylesheets = [];
  3166. var styleFormats = [];
  3167. var await = [];
  3168. //queue file loading
  3169. if (typeof tinymce === 'undefined') {
  3170. await.push(assetsService.loadJs('lib/tinymce/tinymce.min.js', scope));
  3171. }
  3172. if (scope.configuration && scope.configuration.toolbar) {
  3173. toolbar = scope.configuration.toolbar.join(' | ');
  3174. }
  3175. if (scope.configuration && scope.configuration.stylesheets) {
  3176. angular.forEach(scope.configuration.stylesheets, function (stylesheet, key) {
  3177. stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + '/' + stylesheet + '.css');
  3178. await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) {
  3179. angular.forEach(rules, function (rule) {
  3180. var r = {};
  3181. var split = '';
  3182. r.title = rule.name;
  3183. if (rule.selector[0] === '.') {
  3184. r.inline = 'span';
  3185. r.classes = rule.selector.substring(1);
  3186. } else if (rule.selector[0] === '#') {
  3187. //Even though this will render in the style drop down, it will not actually be applied
  3188. // to the elements, don't think TinyMCE even supports this and it doesn't really make much sense
  3189. // since only one element can have one id.
  3190. r.inline = 'span';
  3191. r.attributes = { id: rule.selector.substring(1) };
  3192. } else if (rule.selector[0] !== '.' && rule.selector.indexOf('.') > -1) {
  3193. split = rule.selector.split('.');
  3194. r.block = split[0];
  3195. r.classes = rule.selector.substring(rule.selector.indexOf('.') + 1).replace('.', ' ');
  3196. } else if (rule.selector[0] !== '#' && rule.selector.indexOf('#') > -1) {
  3197. split = rule.selector.split('#');
  3198. r.block = split[0];
  3199. r.classes = rule.selector.substring(rule.selector.indexOf('#') + 1);
  3200. } else {
  3201. r.block = rule.selector;
  3202. }
  3203. styleFormats.push(r);
  3204. });
  3205. }));
  3206. });
  3207. } else {
  3208. stylesheets.push('views/propertyeditors/grid/config/grid.default.rtestyles.css');
  3209. styleFormats = fallbackStyles;
  3210. }
  3211. //stores a reference to the editor
  3212. var tinyMceEditor = null;
  3213. $q.all(await).then(function () {
  3214. var uniqueId = scope.uniqueId;
  3215. //create a baseline Config to exten upon
  3216. var baseLineConfigObj = {
  3217. mode: 'exact',
  3218. skin: 'umbraco',
  3219. plugins: plugins,
  3220. valid_elements: validElements,
  3221. invalid_elements: invalidElements,
  3222. extended_valid_elements: extendedValidElements,
  3223. menubar: false,
  3224. statusbar: false,
  3225. relative_urls: false,
  3226. toolbar: toolbar,
  3227. content_css: stylesheets,
  3228. style_formats: styleFormats,
  3229. autoresize_bottom_margin: 0
  3230. };
  3231. if (tinyMceConfig.customConfig) {
  3232. //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to
  3233. // convert it to json instead of having it as a string since this is what tinymce requires
  3234. for (var i in tinyMceConfig.customConfig) {
  3235. var val = tinyMceConfig.customConfig[i];
  3236. if (val) {
  3237. val = val.toString().trim();
  3238. if (val.detectIsJson()) {
  3239. try {
  3240. tinyMceConfig.customConfig[i] = JSON.parse(val);
  3241. //now we need to check if this custom config key is defined in our baseline, if it is we don't want to
  3242. //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise
  3243. //if it's an object it will overwrite the baseline
  3244. if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) {
  3245. //concat it and below this concat'd array will overwrite the baseline in angular.extend
  3246. tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]);
  3247. }
  3248. } catch (e) {
  3249. }
  3250. }
  3251. }
  3252. }
  3253. angular.extend(baseLineConfigObj, tinyMceConfig.customConfig);
  3254. }
  3255. //set all the things that user configs should not be able to override
  3256. baseLineConfigObj.elements = uniqueId;
  3257. baseLineConfigObj.setup = function (editor) {
  3258. //set the reference
  3259. tinyMceEditor = editor;
  3260. //enable browser based spell checking
  3261. editor.on('init', function (e) {
  3262. editor.getBody().setAttribute('spellcheck', true);
  3263. //force overflow to hidden to prevent no needed scroll
  3264. editor.getBody().style.overflow = 'hidden';
  3265. $timeout(function () {
  3266. if (scope.value === null) {
  3267. editor.focus();
  3268. }
  3269. }, 400);
  3270. });
  3271. // pin toolbar to top of screen if we have focus and it scrolls off the screen
  3272. var pinToolbar = function () {
  3273. var _toolbar = $(editor.editorContainer).find('.mce-toolbar');
  3274. var toolbarHeight = _toolbar.height();
  3275. var _tinyMce = $(editor.editorContainer);
  3276. var tinyMceRect = _tinyMce[0].getBoundingClientRect();
  3277. var tinyMceTop = tinyMceRect.top;
  3278. var tinyMceBottom = tinyMceRect.bottom;
  3279. var tinyMceWidth = tinyMceRect.width;
  3280. var _tinyMceEditArea = _tinyMce.find('.mce-edit-area');
  3281. // set padding in top of mce so the content does not "jump" up
  3282. _tinyMceEditArea.css('padding-top', toolbarHeight);
  3283. if (tinyMceTop < 160 && 160 + toolbarHeight < tinyMceBottom) {
  3284. _toolbar.css('visibility', 'visible').css('position', 'fixed').css('top', '160px').css('margin-top', '0').css('width', tinyMceWidth);
  3285. } else {
  3286. _toolbar.css('visibility', 'visible').css('position', 'absolute').css('top', 'auto').css('margin-top', '0').css('width', tinyMceWidth);
  3287. }
  3288. };
  3289. // unpin toolbar to top of screen
  3290. var unpinToolbar = function () {
  3291. var _toolbar = $(editor.editorContainer).find('.mce-toolbar');
  3292. var _tinyMce = $(editor.editorContainer);
  3293. var _tinyMceEditArea = _tinyMce.find('.mce-edit-area');
  3294. // reset padding in top of mce so the content does not "jump" up
  3295. _tinyMceEditArea.css('padding-top', '0');
  3296. _toolbar.css('position', 'static');
  3297. };
  3298. //when we leave the editor (maybe)
  3299. editor.on('blur', function (e) {
  3300. editor.save();
  3301. angularHelper.safeApply(scope, function () {
  3302. scope.value = editor.getContent();
  3303. var _toolbar = $(editor.editorContainer).find('.mce-toolbar');
  3304. if (scope.onBlur) {
  3305. scope.onBlur();
  3306. }
  3307. unpinToolbar();
  3308. $('.umb-panel-body').off('scroll', pinToolbar);
  3309. });
  3310. });
  3311. // Focus on editor
  3312. editor.on('focus', function (e) {
  3313. angularHelper.safeApply(scope, function () {
  3314. if (scope.onFocus) {
  3315. scope.onFocus();
  3316. }
  3317. pinToolbar();
  3318. $('.umb-panel-body').on('scroll', pinToolbar);
  3319. });
  3320. });
  3321. // Click on editor
  3322. editor.on('click', function (e) {
  3323. angularHelper.safeApply(scope, function () {
  3324. if (scope.onClick) {
  3325. scope.onClick();
  3326. }
  3327. pinToolbar();
  3328. $('.umb-panel-body').on('scroll', pinToolbar);
  3329. });
  3330. });
  3331. //when buttons modify content
  3332. editor.on('ExecCommand', function (e) {
  3333. editor.save();
  3334. angularHelper.safeApply(scope, function () {
  3335. scope.value = editor.getContent();
  3336. });
  3337. });
  3338. // Update model on keypress
  3339. editor.on('KeyUp', function (e) {
  3340. editor.save();
  3341. angularHelper.safeApply(scope, function () {
  3342. scope.value = editor.getContent();
  3343. });
  3344. });
  3345. // Update model on change, i.e. copy/pasted text, plugins altering content
  3346. editor.on('SetContent', function (e) {
  3347. if (!e.initial) {
  3348. editor.save();
  3349. angularHelper.safeApply(scope, function () {
  3350. scope.value = editor.getContent();
  3351. });
  3352. }
  3353. });
  3354. editor.on('ObjectResized', function (e) {
  3355. var qs = '?width=' + e.width + '&height=' + e.height;
  3356. var srcAttr = $(e.target).attr('src');
  3357. var path = srcAttr.split('?')[0];
  3358. $(e.target).attr('data-mce-src', path + qs);
  3359. });
  3360. //Create the insert link plugin
  3361. tinyMceService.createLinkPicker(editor, scope, function (currentTarget, anchorElement) {
  3362. if (scope.onLinkPickerClick) {
  3363. scope.onLinkPickerClick(editor, currentTarget, anchorElement);
  3364. }
  3365. });
  3366. //Create the insert media plugin
  3367. tinyMceService.createMediaPicker(editor, scope, function (currentTarget, userData) {
  3368. if (scope.onMediaPickerClick) {
  3369. scope.onMediaPickerClick(editor, currentTarget, userData);
  3370. }
  3371. });
  3372. //Create the embedded plugin
  3373. tinyMceService.createInsertEmbeddedMedia(editor, scope, function () {
  3374. if (scope.onEmbedClick) {
  3375. scope.onEmbedClick(editor);
  3376. }
  3377. });
  3378. //Create the insert macro plugin
  3379. tinyMceService.createInsertMacro(editor, scope, function (dialogData) {
  3380. if (scope.onMacroPickerClick) {
  3381. scope.onMacroPickerClick(editor, dialogData);
  3382. }
  3383. });
  3384. };
  3385. /** Loads in the editor */
  3386. function loadTinyMce() {
  3387. //we need to add a timeout here, to force a redraw so TinyMCE can find
  3388. //the elements needed
  3389. $timeout(function () {
  3390. tinymce.DOM.events.domLoaded = true;
  3391. tinymce.init(baseLineConfigObj);
  3392. }, 150, false);
  3393. }
  3394. loadTinyMce();
  3395. //here we declare a special method which will be called whenever the value has changed from the server
  3396. //this is instead of doing a watch on the model.value = faster
  3397. //scope.model.onValueChanged = function (newVal, oldVal) {
  3398. // //update the display val again if it has changed from the server;
  3399. // tinyMceEditor.setContent(newVal, { format: 'raw' });
  3400. // //we need to manually fire this event since it is only ever fired based on loading from the DOM, this
  3401. // // is required for our plugins listening to this event to execute
  3402. // tinyMceEditor.fire('LoadContent', null);
  3403. //};
  3404. //listen for formSubmitting event (the result is callback used to remove the event subscription)
  3405. var unsubscribe = scope.$on('formSubmitting', function () {
  3406. //TODO: Here we should parse out the macro rendered content so we can save on a lot of bytes in data xfer
  3407. // we do parse it out on the server side but would be nice to do that on the client side before as well.
  3408. scope.value = tinyMceEditor ? tinyMceEditor.getContent() : null;
  3409. });
  3410. //when the element is disposed we need to unsubscribe!
  3411. // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom
  3412. // element might still be there even after the modal has been hidden.
  3413. scope.$on('$destroy', function () {
  3414. unsubscribe();
  3415. if (tinyMceEditor !== undefined && tinyMceEditor != null) {
  3416. tinyMceEditor.destroy();
  3417. }
  3418. });
  3419. });
  3420. });
  3421. };
  3422. initTiny();
  3423. }
  3424. };
  3425. });
  3426. /**
  3427. @ngdoc directive
  3428. @name umbraco.directives.directive:umbBox
  3429. @restrict E
  3430. @description
  3431. Use this directive to render an already styled empty div tag.
  3432. <h3>Markup example</h3>
  3433. <pre>
  3434. <umb-box>
  3435. <umb-box-header title="this is a title"></umb-box-header>
  3436. <umb-box-content>
  3437. // Content here
  3438. </umb-box-content>
  3439. </umb-box>
  3440. </pre>
  3441. <h3>Use in combination with:</h3>
  3442. <ul>
  3443. <li>{@link umbraco.directives.directive:umbBoxHeader umbBoxHeader}</li>
  3444. <li>{@link umbraco.directives.directive:umbBoxContent umbBoxContent}</li>
  3445. </ul>
  3446. **/
  3447. (function () {
  3448. 'use strict';
  3449. function BoxDirective() {
  3450. var directive = {
  3451. restrict: 'E',
  3452. replace: true,
  3453. transclude: true,
  3454. templateUrl: 'views/components/html/umb-box/umb-box.html'
  3455. };
  3456. return directive;
  3457. }
  3458. angular.module('umbraco.directives').directive('umbBox', BoxDirective);
  3459. }());
  3460. /**
  3461. @ngdoc directive
  3462. @name umbraco.directives.directive:umbBoxContent
  3463. @restrict E
  3464. @description
  3465. Use this directive to render an empty container. Recommended to use it inside an {@link umbraco.directives.directive:umbBox umbBox} directive. See documentation for {@link umbraco.directives.directive:umbBox umbBox}.
  3466. <h3>Markup example</h3>
  3467. <pre>
  3468. <umb-box>
  3469. <umb-box-header title="this is a title"></umb-box-header>
  3470. <umb-box-content>
  3471. // Content here
  3472. </umb-box-content>
  3473. </umb-box>
  3474. </pre>
  3475. <h3>Use in combination with:</h3>
  3476. <ul>
  3477. <li>{@link umbraco.directives.directive:umbBox umbBox}</li>
  3478. <li>{@link umbraco.directives.directive:umbBoxHeader umbBoxHeader}</li>
  3479. </ul>
  3480. **/
  3481. (function () {
  3482. 'use strict';
  3483. function BoxContentDirective() {
  3484. var directive = {
  3485. restrict: 'E',
  3486. replace: true,
  3487. transclude: true,
  3488. templateUrl: 'views/components/html/umb-box/umb-box-content.html'
  3489. };
  3490. return directive;
  3491. }
  3492. angular.module('umbraco.directives').directive('umbBoxContent', BoxContentDirective);
  3493. }());
  3494. /**
  3495. @ngdoc directive
  3496. @name umbraco.directives.directive:umbBoxHeader
  3497. @restrict E
  3498. @scope
  3499. @description
  3500. Use this directive to construct a title. Recommended to use it inside an {@link umbraco.directives.directive:umbBox umbBox} directive. See documentation for {@link umbraco.directives.directive:umbBox umbBox}.
  3501. <h3>Markup example</h3>
  3502. <pre>
  3503. <umb-box>
  3504. <umb-box-header title="This is a title" description="I can enter a description right here"></umb-box-header>
  3505. <umb-box-content>
  3506. // Content here
  3507. </umb-box-content>
  3508. </umb-box>
  3509. </pre>
  3510. <h3>Markup example with using titleKey</h3>
  3511. <pre>
  3512. <umb-box>
  3513. // the title-key property needs an areaAlias_keyAlias from the language files
  3514. <umb-box-header title-key="areaAlias_keyAlias" description-key="areaAlias_keyAlias"></umb-box-header>
  3515. <umb-box-content>
  3516. // Content here
  3517. </umb-box-content>
  3518. </umb-box>
  3519. </pre>
  3520. {@link https://our.umbraco.org/documentation/extending/language-files/ Here you can see more about the language files}
  3521. <h3>Use in combination with:</h3>
  3522. <ul>
  3523. <li>{@link umbraco.directives.directive:umbBox umbBox}</li>
  3524. <li>{@link umbraco.directives.directive:umbBoxContent umbBoxContent}</li>
  3525. </ul>
  3526. @param {string=} title (<code>attrbute</code>): Custom title text.
  3527. @param {string=} titleKey (<code>attrbute</code>): The translation key from the language xml files.
  3528. @param {string=} description (<code>attrbute</code>): Custom description text.
  3529. @param {string=} descriptionKey (<code>attrbute</code>): The translation key from the language xml files.
  3530. **/
  3531. (function () {
  3532. 'use strict';
  3533. function BoxHeaderDirective() {
  3534. var directive = {
  3535. restrict: 'E',
  3536. replace: true,
  3537. transclude: true,
  3538. templateUrl: 'views/components/html/umb-box/umb-box-header.html',
  3539. scope: {
  3540. titleKey: '@?',
  3541. title: '@?',
  3542. descriptionKey: '@?',
  3543. description: '@?'
  3544. }
  3545. };
  3546. return directive;
  3547. }
  3548. angular.module('umbraco.directives').directive('umbBoxHeader', BoxHeaderDirective);
  3549. }());
  3550. /**
  3551. * @ngdoc directive
  3552. * @name umbraco.directives.directive:umbControlGroup
  3553. * @restrict E
  3554. **/
  3555. angular.module('umbraco.directives.html').directive('umbControlGroup', function (localizationService) {
  3556. return {
  3557. scope: {
  3558. label: '@label',
  3559. description: '@',
  3560. hideLabel: '@',
  3561. alias: '@',
  3562. labelFor: '@',
  3563. required: '@?'
  3564. },
  3565. require: '?^form',
  3566. transclude: true,
  3567. restrict: 'E',
  3568. replace: true,
  3569. templateUrl: 'views/components/html/umb-control-group.html',
  3570. link: function (scope, element, attr, formCtrl) {
  3571. scope.formValid = function () {
  3572. if (formCtrl && scope.labelFor) {
  3573. //if a label-for has been set, use that for the validation
  3574. return formCtrl[scope.labelFor].$valid;
  3575. }
  3576. //there is no form.
  3577. return true;
  3578. };
  3579. if (scope.label && scope.label[0] === '@') {
  3580. scope.labelstring = localizationService.localize(scope.label.substring(1));
  3581. } else {
  3582. scope.labelstring = scope.label;
  3583. }
  3584. if (scope.description && scope.description[0] === '@') {
  3585. scope.descriptionstring = localizationService.localize(scope.description.substring(1));
  3586. } else {
  3587. scope.descriptionstring = scope.description;
  3588. }
  3589. }
  3590. };
  3591. });
  3592. /**
  3593. * @ngdoc directive
  3594. * @name umbraco.directives.directive:umbPane
  3595. * @restrict E
  3596. **/
  3597. angular.module('umbraco.directives.html').directive('umbPane', function () {
  3598. return {
  3599. transclude: true,
  3600. restrict: 'E',
  3601. replace: true,
  3602. templateUrl: 'views/components/html/umb-pane.html'
  3603. };
  3604. });
  3605. /**
  3606. * @ngdoc directive
  3607. * @name umbraco.directives.directive:umbPanel
  3608. * @restrict E
  3609. **/
  3610. angular.module('umbraco.directives.html').directive('umbPanel', function ($timeout, $log) {
  3611. return {
  3612. restrict: 'E',
  3613. replace: true,
  3614. transclude: 'true',
  3615. templateUrl: 'views/components/html/umb-panel.html'
  3616. };
  3617. });
  3618. /**
  3619. * @ngdoc directive
  3620. * @name umbraco.directives.directive:umbImageCrop
  3621. * @restrict E
  3622. * @function
  3623. **/
  3624. angular.module('umbraco.directives').directive('umbImageCrop', function ($timeout, localizationService, cropperHelper, $log) {
  3625. return {
  3626. restrict: 'E',
  3627. replace: true,
  3628. templateUrl: 'views/components/imaging/umb-image-crop.html',
  3629. scope: {
  3630. src: '=',
  3631. width: '@',
  3632. height: '@',
  3633. crop: '=',
  3634. center: '=',
  3635. maxSize: '@'
  3636. },
  3637. link: function (scope, element, attrs) {
  3638. scope.width = 400;
  3639. scope.height = 320;
  3640. scope.dimensions = {
  3641. image: {},
  3642. cropper: {},
  3643. viewport: {},
  3644. margin: 20,
  3645. scale: {
  3646. min: 0.3,
  3647. max: 3,
  3648. current: 1
  3649. }
  3650. };
  3651. //live rendering of viewport and image styles
  3652. scope.style = function () {
  3653. return {
  3654. 'height': parseInt(scope.dimensions.viewport.height, 10) + 'px',
  3655. 'width': parseInt(scope.dimensions.viewport.width, 10) + 'px'
  3656. };
  3657. };
  3658. //elements
  3659. var $viewport = element.find('.viewport');
  3660. var $image = element.find('img');
  3661. var $overlay = element.find('.overlay');
  3662. var $container = element.find('.crop-container');
  3663. //default constraints for drag n drop
  3664. var constraints = {
  3665. left: {
  3666. max: scope.dimensions.margin,
  3667. min: scope.dimensions.margin
  3668. },
  3669. top: {
  3670. max: scope.dimensions.margin,
  3671. min: scope.dimensions.margin
  3672. }
  3673. };
  3674. scope.constraints = constraints;
  3675. //set constaints for cropping drag and drop
  3676. var setConstraints = function () {
  3677. constraints.left.min = scope.dimensions.margin + scope.dimensions.cropper.width - scope.dimensions.image.width;
  3678. constraints.top.min = scope.dimensions.margin + scope.dimensions.cropper.height - scope.dimensions.image.height;
  3679. };
  3680. var setDimensions = function (originalImage) {
  3681. originalImage.width('auto');
  3682. originalImage.height('auto');
  3683. var image = {};
  3684. image.originalWidth = originalImage.width();
  3685. image.originalHeight = originalImage.height();
  3686. image.width = image.originalWidth;
  3687. image.height = image.originalHeight;
  3688. image.left = originalImage[0].offsetLeft;
  3689. image.top = originalImage[0].offsetTop;
  3690. scope.dimensions.image = image;
  3691. //unscaled editor size
  3692. //var viewPortW = $viewport.width();
  3693. //var viewPortH = $viewport.height();
  3694. var _viewPortW = parseInt(scope.width, 10);
  3695. var _viewPortH = parseInt(scope.height, 10);
  3696. //if we set a constraint we will scale it down if needed
  3697. if (scope.maxSize) {
  3698. var ratioCalculation = cropperHelper.scaleToMaxSize(_viewPortW, _viewPortH, scope.maxSize);
  3699. //so if we have a max size, override the thumb sizes
  3700. _viewPortW = ratioCalculation.width;
  3701. _viewPortH = ratioCalculation.height;
  3702. }
  3703. scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin;
  3704. scope.dimensions.viewport.height = _viewPortH + 2 * scope.dimensions.margin;
  3705. scope.dimensions.cropper.width = _viewPortW;
  3706. // scope.dimensions.viewport.width - 2 * scope.dimensions.margin;
  3707. scope.dimensions.cropper.height = _viewPortH; // scope.dimensions.viewport.height - 2 * scope.dimensions.margin;
  3708. };
  3709. //when loading an image without any crop info, we center and fit it
  3710. var resizeImageToEditor = function () {
  3711. //returns size fitting the cropper
  3712. var size = cropperHelper.calculateAspectRatioFit(scope.dimensions.image.width, scope.dimensions.image.height, scope.dimensions.cropper.width, scope.dimensions.cropper.height, true);
  3713. //sets the image size and updates the scope
  3714. scope.dimensions.image.width = size.width;
  3715. scope.dimensions.image.height = size.height;
  3716. //calculate the best suited ratios
  3717. scope.dimensions.scale.min = size.ratio;
  3718. scope.dimensions.scale.max = 2;
  3719. scope.dimensions.scale.current = size.ratio;
  3720. //center the image
  3721. var position = cropperHelper.centerInsideViewPort(scope.dimensions.image, scope.dimensions.cropper);
  3722. scope.dimensions.top = position.top;
  3723. scope.dimensions.left = position.left;
  3724. setConstraints();
  3725. };
  3726. //resize to a given ratio
  3727. var resizeImageToScale = function (ratio) {
  3728. //do stuff
  3729. var size = cropperHelper.calculateSizeToRatio(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, ratio);
  3730. scope.dimensions.image.width = size.width;
  3731. scope.dimensions.image.height = size.height;
  3732. setConstraints();
  3733. validatePosition(scope.dimensions.image.left, scope.dimensions.image.top);
  3734. };
  3735. //resize the image to a predefined crop coordinate
  3736. var resizeImageToCrop = function () {
  3737. scope.dimensions.image = cropperHelper.convertToStyle(scope.crop, {
  3738. width: scope.dimensions.image.originalWidth,
  3739. height: scope.dimensions.image.originalHeight
  3740. }, scope.dimensions.cropper, scope.dimensions.margin);
  3741. var ratioCalculation = cropperHelper.calculateAspectRatioFit(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, scope.dimensions.cropper.width, scope.dimensions.cropper.height, true);
  3742. scope.dimensions.scale.current = scope.dimensions.image.ratio;
  3743. //min max based on original width/height
  3744. scope.dimensions.scale.min = ratioCalculation.ratio;
  3745. scope.dimensions.scale.max = 2;
  3746. };
  3747. var validatePosition = function (left, top) {
  3748. if (left > constraints.left.max) {
  3749. left = constraints.left.max;
  3750. }
  3751. if (left <= constraints.left.min) {
  3752. left = constraints.left.min;
  3753. }
  3754. if (top > constraints.top.max) {
  3755. top = constraints.top.max;
  3756. }
  3757. if (top <= constraints.top.min) {
  3758. top = constraints.top.min;
  3759. }
  3760. if (scope.dimensions.image.left !== left) {
  3761. scope.dimensions.image.left = left;
  3762. }
  3763. if (scope.dimensions.image.top !== top) {
  3764. scope.dimensions.image.top = top;
  3765. }
  3766. };
  3767. //sets scope.crop to the recalculated % based crop
  3768. var calculateCropBox = function () {
  3769. scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin);
  3770. };
  3771. //Drag and drop positioning, using jquery ui draggable
  3772. var onStartDragPosition, top, left;
  3773. $overlay.draggable({
  3774. drag: function (event, ui) {
  3775. scope.$apply(function () {
  3776. validatePosition(ui.position.left, ui.position.top);
  3777. });
  3778. },
  3779. stop: function (event, ui) {
  3780. scope.$apply(function () {
  3781. //make sure that every validates one more time...
  3782. validatePosition(ui.position.left, ui.position.top);
  3783. calculateCropBox();
  3784. scope.dimensions.image.rnd = Math.random();
  3785. });
  3786. }
  3787. });
  3788. var init = function (image) {
  3789. scope.loaded = false;
  3790. //set dimensions on image, viewport, cropper etc
  3791. setDimensions(image);
  3792. //if we have a crop already position the image
  3793. if (scope.crop) {
  3794. resizeImageToCrop();
  3795. } else {
  3796. resizeImageToEditor();
  3797. }
  3798. //sets constaints for the cropper
  3799. setConstraints();
  3800. scope.loaded = true;
  3801. };
  3802. /// WATCHERS ////
  3803. scope.$watchCollection('[width, height]', function (newValues, oldValues) {
  3804. //we have to reinit the whole thing if
  3805. //one of the external params changes
  3806. if (newValues !== oldValues) {
  3807. setDimensions($image);
  3808. setConstraints();
  3809. }
  3810. });
  3811. var throttledResizing = _.throttle(function () {
  3812. resizeImageToScale(scope.dimensions.scale.current);
  3813. calculateCropBox();
  3814. }, 100);
  3815. //happens when we change the scale
  3816. scope.$watch('dimensions.scale.current', function () {
  3817. if (scope.loaded) {
  3818. throttledResizing();
  3819. }
  3820. });
  3821. //ie hack
  3822. if (window.navigator.userAgent.indexOf('MSIE ')) {
  3823. var ranger = element.find('input');
  3824. ranger.bind('change', function () {
  3825. scope.$apply(function () {
  3826. scope.dimensions.scale.current = ranger.val();
  3827. });
  3828. });
  3829. }
  3830. //// INIT /////
  3831. $image.load(function () {
  3832. $timeout(function () {
  3833. init($image);
  3834. });
  3835. });
  3836. }
  3837. };
  3838. });
  3839. /**
  3840. * @ngdoc directive
  3841. * @name umbraco.directives.directive:umbImageGravity
  3842. * @restrict E
  3843. * @function
  3844. * @description
  3845. **/
  3846. angular.module('umbraco.directives').directive('umbImageGravity', function ($timeout, localizationService, $log) {
  3847. return {
  3848. restrict: 'E',
  3849. replace: true,
  3850. templateUrl: 'views/components/imaging/umb-image-gravity.html',
  3851. scope: {
  3852. src: '=',
  3853. center: '=',
  3854. onImageLoaded: '='
  3855. },
  3856. link: function (scope, element, attrs) {
  3857. //Internal values for keeping track of the dot and the size of the editor
  3858. scope.dimensions = {
  3859. width: 0,
  3860. height: 0,
  3861. left: 0,
  3862. top: 0
  3863. };
  3864. scope.loaded = false;
  3865. //elements
  3866. var $viewport = element.find('.viewport');
  3867. var $image = element.find('img');
  3868. var $overlay = element.find('.overlay');
  3869. scope.style = function () {
  3870. if (scope.dimensions.width <= 0) {
  3871. setDimensions();
  3872. }
  3873. return {
  3874. 'top': scope.dimensions.top + 'px',
  3875. 'left': scope.dimensions.left + 'px'
  3876. };
  3877. };
  3878. scope.setFocalPoint = function (event) {
  3879. scope.$emit('imageFocalPointStart');
  3880. var offsetX = event.offsetX - 10;
  3881. var offsetY = event.offsetY - 10;
  3882. calculateGravity(offsetX, offsetY);
  3883. lazyEndEvent();
  3884. };
  3885. var setDimensions = function () {
  3886. scope.dimensions.width = $image.width();
  3887. scope.dimensions.height = $image.height();
  3888. if (scope.center) {
  3889. scope.dimensions.left = scope.center.left * scope.dimensions.width - 10;
  3890. scope.dimensions.top = scope.center.top * scope.dimensions.height - 10;
  3891. } else {
  3892. scope.center = {
  3893. left: 0.5,
  3894. top: 0.5
  3895. };
  3896. }
  3897. };
  3898. var calculateGravity = function (offsetX, offsetY) {
  3899. scope.dimensions.left = offsetX;
  3900. scope.dimensions.top = offsetY;
  3901. scope.center.left = (scope.dimensions.left + 10) / scope.dimensions.width;
  3902. scope.center.top = (scope.dimensions.top + 10) / scope.dimensions.height;
  3903. };
  3904. var lazyEndEvent = _.debounce(function () {
  3905. scope.$apply(function () {
  3906. scope.$emit('imageFocalPointStop');
  3907. });
  3908. }, 2000);
  3909. //Drag and drop positioning, using jquery ui draggable
  3910. //TODO ensure that the point doesnt go outside the box
  3911. $overlay.draggable({
  3912. containment: 'parent',
  3913. start: function () {
  3914. scope.$apply(function () {
  3915. scope.$emit('imageFocalPointStart');
  3916. });
  3917. },
  3918. stop: function () {
  3919. scope.$apply(function () {
  3920. var offsetX = $overlay[0].offsetLeft;
  3921. var offsetY = $overlay[0].offsetTop;
  3922. calculateGravity(offsetX, offsetY);
  3923. });
  3924. lazyEndEvent();
  3925. }
  3926. });
  3927. //// INIT /////
  3928. $image.load(function () {
  3929. $timeout(function () {
  3930. setDimensions();
  3931. scope.loaded = true;
  3932. if (angular.isFunction(scope.onImageLoaded)) {
  3933. scope.onImageLoaded();
  3934. }
  3935. });
  3936. });
  3937. $(window).on('resize.umbImageGravity', function () {
  3938. scope.$apply(function () {
  3939. $timeout(function () {
  3940. setDimensions();
  3941. });
  3942. // Make sure we can find the offset values for the overlay(dot) before calculating
  3943. // fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte)
  3944. if ($overlay.is(':visible')) {
  3945. var offsetX = $overlay[0].offsetLeft;
  3946. var offsetY = $overlay[0].offsetTop;
  3947. calculateGravity(offsetX, offsetY);
  3948. }
  3949. });
  3950. });
  3951. scope.$on('$destroy', function () {
  3952. $(window).off('.umbImageGravity');
  3953. });
  3954. }
  3955. };
  3956. });
  3957. /**
  3958. * @ngdoc directive
  3959. * @name umbraco.directives.directive:umbImageThumbnail
  3960. * @restrict E
  3961. * @function
  3962. * @description
  3963. **/
  3964. angular.module('umbraco.directives').directive('umbImageThumbnail', function ($timeout, localizationService, cropperHelper, $log) {
  3965. return {
  3966. restrict: 'E',
  3967. replace: true,
  3968. templateUrl: 'views/components/imaging/umb-image-thumbnail.html',
  3969. scope: {
  3970. src: '=',
  3971. width: '@',
  3972. height: '@',
  3973. center: '=',
  3974. crop: '=',
  3975. maxSize: '@'
  3976. },
  3977. link: function (scope, element, attrs) {
  3978. //// INIT /////
  3979. var $image = element.find('img');
  3980. scope.loaded = false;
  3981. $image.load(function () {
  3982. $timeout(function () {
  3983. $image.width('auto');
  3984. $image.height('auto');
  3985. scope.image = {};
  3986. scope.image.width = $image[0].width;
  3987. scope.image.height = $image[0].height;
  3988. //we force a lower thumbnail size to fit the max size
  3989. //we do not compare to the image dimensions, but the thumbs
  3990. if (scope.maxSize) {
  3991. var ratioCalculation = cropperHelper.calculateAspectRatioFit(scope.width, scope.height, scope.maxSize, scope.maxSize, false);
  3992. //so if we have a max size, override the thumb sizes
  3993. scope.width = ratioCalculation.width;
  3994. scope.height = ratioCalculation.height;
  3995. }
  3996. setPreviewStyle();
  3997. scope.loaded = true;
  3998. });
  3999. });
  4000. /// WATCHERS ////
  4001. scope.$watchCollection('[crop, center]', function (newValues, oldValues) {
  4002. //we have to reinit the whole thing if
  4003. //one of the external params changes
  4004. setPreviewStyle();
  4005. });
  4006. scope.$watch('center', function () {
  4007. setPreviewStyle();
  4008. }, true);
  4009. function setPreviewStyle() {
  4010. if (scope.crop && scope.image) {
  4011. scope.preview = cropperHelper.convertToStyle(scope.crop, scope.image, {
  4012. width: scope.width,
  4013. height: scope.height
  4014. }, 0);
  4015. } else if (scope.image) {
  4016. //returns size fitting the cropper
  4017. var p = cropperHelper.calculateAspectRatioFit(scope.image.width, scope.image.height, scope.width, scope.height, true);
  4018. if (scope.center) {
  4019. var xy = cropperHelper.alignToCoordinates(p, scope.center, {
  4020. width: scope.width,
  4021. height: scope.height
  4022. });
  4023. p.top = xy.top;
  4024. p.left = xy.left;
  4025. } else {
  4026. }
  4027. p.position = 'absolute';
  4028. scope.preview = p;
  4029. }
  4030. }
  4031. }
  4032. };
  4033. });
  4034. angular.module('umbraco.directives') /**
  4035. * @ngdoc directive
  4036. * @name umbraco.directives.directive:localize
  4037. * @restrict EA
  4038. * @function
  4039. * @description
  4040. * <div>
  4041. * <strong>Component</strong><br />
  4042. * Localize a specific token to put into the HTML as an item
  4043. * </div>
  4044. * <div>
  4045. * <strong>Attribute</strong><br />
  4046. * Add a HTML attribute to an element containing the HTML attribute name you wish to localise
  4047. * Using the format of '@section_key' or 'section_key'
  4048. * </div>
  4049. * ##Usage
  4050. * <pre>
  4051. * <!-- Component -->
  4052. * <localize key="general_close">Close</localize>
  4053. * <localize key="section_key">Fallback value</localize>
  4054. *
  4055. * <!-- Attribute -->
  4056. * <input type="text" localize="placeholder" placeholder="@placeholders_entername" />
  4057. * <input type="text" localize="placeholder,title" title="@section_key" placeholder="@placeholders_entername" />
  4058. * <div localize="title" title="@section_key"></div>
  4059. * </pre>
  4060. **/.directive('localize', function ($log, localizationService) {
  4061. return {
  4062. restrict: 'E',
  4063. scope: { key: '@' },
  4064. replace: true,
  4065. link: function (scope, element, attrs) {
  4066. var key = scope.key;
  4067. localizationService.localize(key).then(function (value) {
  4068. element.html(value);
  4069. });
  4070. }
  4071. };
  4072. }).directive('localize', function ($log, localizationService) {
  4073. return {
  4074. restrict: 'A',
  4075. link: function (scope, element, attrs) {
  4076. //Support one or more attribute properties to update
  4077. var keys = attrs.localize.split(',');
  4078. angular.forEach(keys, function (value, key) {
  4079. var attr = element.attr(value);
  4080. if (attr) {
  4081. if (attr[0] === '@') {
  4082. //If the translation key starts with @ then remove it
  4083. attr = attr.substring(1);
  4084. }
  4085. var t = localizationService.tokenize(attr, scope);
  4086. localizationService.localize(t.key, t.tokens).then(function (val) {
  4087. element.attr(value, val);
  4088. });
  4089. }
  4090. });
  4091. }
  4092. };
  4093. });
  4094. /**
  4095. * @ngdoc directive
  4096. * @name umbraco.directives.directive:umbNotifications
  4097. */
  4098. (function () {
  4099. 'use strict';
  4100. function NotificationDirective(notificationsService) {
  4101. function link(scope, el, attr, ctrl) {
  4102. //subscribes to notifications in the notification service
  4103. scope.notifications = notificationsService.current;
  4104. scope.$watch('notificationsService.current', function (newVal, oldVal, scope) {
  4105. if (newVal) {
  4106. scope.notifications = newVal;
  4107. }
  4108. });
  4109. }
  4110. var directive = {
  4111. restrict: 'E',
  4112. replace: true,
  4113. templateUrl: 'views/components/notifications/umb-notifications.html',
  4114. link: link
  4115. };
  4116. return directive;
  4117. }
  4118. angular.module('umbraco.directives').directive('umbNotifications', NotificationDirective);
  4119. }());
  4120. /**
  4121. @ngdoc directive
  4122. @name umbraco.directives.directive:umbOverlay
  4123. @restrict E
  4124. @scope
  4125. @description
  4126. <h3>Markup example</h3>
  4127. <pre>
  4128. <div ng-controller="My.Controller as vm">
  4129. <button type="button" ng-click="vm.openOverlay()"></button>
  4130. <umb-overlay
  4131. ng-if="vm.overlay.show"
  4132. model="vm.overlay"
  4133. view="vm.overlay.view"
  4134. position="right">
  4135. </umb-overlay>
  4136. </div>
  4137. </pre>
  4138. <h3>Controller example</h3>
  4139. <pre>
  4140. (function () {
  4141. "use strict";
  4142. function Controller() {
  4143. var vm = this;
  4144. vm.openOverlay = openOverlay;
  4145. function openOverlay() {
  4146. vm.overlay = {
  4147. view: "mediapicker",
  4148. show: true,
  4149. submit: function(model) {
  4150. vm.overlay.show = false;
  4151. vm.overlay = null;
  4152. },
  4153. close: function(oldModel) {
  4154. vm.overlay.show = false;
  4155. vm.overlay = null;
  4156. }
  4157. }
  4158. };
  4159. }
  4160. angular.module("umbraco").controller("My.Controller", Controller);
  4161. })();
  4162. </pre>
  4163. <h1>General Options</h1>
  4164. Lorem ipsum dolor sit amet..
  4165. <table>
  4166. <thead>
  4167. <tr>
  4168. <th>Param</th>
  4169. <th>Type</th>
  4170. <th>Details</th>
  4171. </tr>
  4172. </thead>
  4173. <tr>
  4174. <td>model.title</td>
  4175. <td>String</td>
  4176. <td>Set the title of the overlay.</td>
  4177. </tr>
  4178. <tr>
  4179. <td>model.subTitle</td>
  4180. <td>String</td>
  4181. <td>Set the subtitle of the overlay.</td>
  4182. </tr>
  4183. <tr>
  4184. <td>model.submitButtonLabel</td>
  4185. <td>String</td>
  4186. <td>Set an alternate submit button text</td>
  4187. </tr>
  4188. <tr>
  4189. <td>model.submitButtonLabelKey</td>
  4190. <td>String</td>
  4191. <td>Set an alternate submit button label key for localized texts</td>
  4192. </tr>
  4193. <tr>
  4194. <td>model.hideSubmitButton</td>
  4195. <td>Boolean</td>
  4196. <td>Hides the submit button</td>
  4197. </tr>
  4198. <tr>
  4199. <td>model.closeButtonLabel</td>
  4200. <td>String</td>
  4201. <td>Set an alternate close button text</td>
  4202. </tr>
  4203. <tr>
  4204. <td>model.closeButtonLabelKey</td>
  4205. <td>String</td>
  4206. <td>Set an alternate close button label key for localized texts</td>
  4207. </tr>
  4208. <tr>
  4209. <td>model.show</td>
  4210. <td>Boolean</td>
  4211. <td>Show/hide the overlay</td>
  4212. </tr>
  4213. <tr>
  4214. <td>model.submit</td>
  4215. <td>Function</td>
  4216. <td>Callback function when the overlay submits. Returns the overlay model object</td>
  4217. </tr>
  4218. <tr>
  4219. <td>model.close</td>
  4220. <td>Function</td>
  4221. <td>Callback function when the overlay closes. Returns a copy of the overlay model object before being modified</td>
  4222. </tr>
  4223. </table>
  4224. <h1>Content Picker</h1>
  4225. Opens a content picker.</br>
  4226. <strong>view: </strong>contentpicker
  4227. <table>
  4228. <thead>
  4229. <tr>
  4230. <th>Param</th>
  4231. <th>Type</th>
  4232. <th>Details</th>
  4233. </tr>
  4234. </thead>
  4235. <tr>
  4236. <td>model.multiPicker</td>
  4237. <td>Boolean</td>
  4238. <td>Pick one or multiple items</td>
  4239. </tr>
  4240. </table>
  4241. <table>
  4242. <thead>
  4243. <tr>
  4244. <th>Returns</th>
  4245. <th>Type</th>
  4246. <th>Details</th>
  4247. </tr>
  4248. </thead>
  4249. <tr>
  4250. <td>model.selection</td>
  4251. <td>Array</td>
  4252. <td>Array of content objects</td>
  4253. </tr>
  4254. </table>
  4255. <h1>Icon Picker</h1>
  4256. Opens an icon picker.</br>
  4257. <strong>view: </strong>iconpicker
  4258. <table>
  4259. <thead>
  4260. <tr>
  4261. <th>Returns</th>
  4262. <th>Type</th>
  4263. <th>Details</th>
  4264. </tr>
  4265. </thead>
  4266. <tr>
  4267. <td>model.icon</td>
  4268. <td>String</td>
  4269. <td>The icon class</td>
  4270. </tr>
  4271. </table>
  4272. <h1>Item Picker</h1>
  4273. Opens an item picker.</br>
  4274. <strong>view: </strong>itempicker
  4275. <table>
  4276. <thead>
  4277. <tr>
  4278. <th>Param</th>
  4279. <th>Type</th>
  4280. <th>Details</th>
  4281. </tr>
  4282. </thead>
  4283. <tbody>
  4284. <tr>
  4285. <td>model.availableItems</td>
  4286. <td>Array</td>
  4287. <td>Array of available items</td>
  4288. </tr>
  4289. <tr>
  4290. <td>model.selectedItems</td>
  4291. <td>Array</td>
  4292. <td>Array of selected items. When passed in the selected items will be filtered from the available items.</td>
  4293. </tr>
  4294. <tr>
  4295. <td>model.filter</td>
  4296. <td>Boolean</td>
  4297. <td>Set to false to hide the filter</td>
  4298. </tr>
  4299. </tbody>
  4300. </table>
  4301. <table>
  4302. <thead>
  4303. <tr>
  4304. <th>Returns</th>
  4305. <th>Type</th>
  4306. <th>Details</th>
  4307. </tr>
  4308. </thead>
  4309. <tr>
  4310. <td>model.selectedItem</td>
  4311. <td>Object</td>
  4312. <td>The selected item</td>
  4313. </tr>
  4314. </table>
  4315. <h1>Macro Picker</h1>
  4316. Opens a media picker.</br>
  4317. <strong>view: </strong>macropicker
  4318. <table>
  4319. <thead>
  4320. <tr>
  4321. <th>Param</th>
  4322. <th>Type</th>
  4323. <th>Details</th>
  4324. </tr>
  4325. </thead>
  4326. <tbody>
  4327. <tr>
  4328. <td>model.dialogData</td>
  4329. <td>Object</td>
  4330. <td>Object which contains array of allowedMacros. Set to <code>null</code> to allow all.</td>
  4331. </tr>
  4332. </tbody>
  4333. </table>
  4334. <table>
  4335. <thead>
  4336. <tr>
  4337. <th>Returns</th>
  4338. <th>Type</th>
  4339. <th>Details</th>
  4340. </tr>
  4341. </thead>
  4342. <tbody>
  4343. <tr>
  4344. <td>model.macroParams</td>
  4345. <td>Array</td>
  4346. <td>Array of macro params</td>
  4347. </tr>
  4348. <tr>
  4349. <td>model.selectedMacro</td>
  4350. <td>Object</td>
  4351. <td>The selected macro</td>
  4352. </tr>
  4353. </tbody>
  4354. </table>
  4355. <h1>Media Picker</h1>
  4356. Opens a media picker.</br>
  4357. <strong>view: </strong>mediapicker
  4358. <table>
  4359. <thead>
  4360. <tr>
  4361. <th>Param</th>
  4362. <th>Type</th>
  4363. <th>Details</th>
  4364. </tr>
  4365. </thead>
  4366. <tbody>
  4367. <tr>
  4368. <td>model.multiPicker</td>
  4369. <td>Boolean</td>
  4370. <td>Pick one or multiple items</td>
  4371. </tr>
  4372. <tr>
  4373. <td>model.onlyImages</td>
  4374. <td>Boolean</td>
  4375. <td>Only display files that have an image file-extension</td>
  4376. </tr>
  4377. <tr>
  4378. <td>model.disableFolderSelect</td>
  4379. <td>Boolean</td>
  4380. <td>Disable folder selection</td>
  4381. </tr>
  4382. </tbody>
  4383. </table>
  4384. <table>
  4385. <thead>
  4386. <tr>
  4387. <th>Returns</th>
  4388. <th>Type</th>
  4389. <th>Details</th>
  4390. </tr>
  4391. </thead>
  4392. <tbody>
  4393. <tr>
  4394. <td>model.selectedImages</td>
  4395. <td>Array</td>
  4396. <td>Array of selected images</td>
  4397. </tr>
  4398. </tbody>
  4399. </table>
  4400. <h1>Member Group Picker</h1>
  4401. Opens a member group picker.</br>
  4402. <strong>view: </strong>membergrouppicker
  4403. <table>
  4404. <thead>
  4405. <tr>
  4406. <th>Param</th>
  4407. <th>Type</th>
  4408. <th>Details</th>
  4409. </tr>
  4410. </thead>
  4411. <tbody>
  4412. <tr>
  4413. <td>model.multiPicker</td>
  4414. <td>Boolean</td>
  4415. <td>Pick one or multiple items</td>
  4416. </tr>
  4417. </tbody>
  4418. </table>
  4419. <table>
  4420. <thead>
  4421. <tr>
  4422. <th>Returns</th>
  4423. <th>Type</th>
  4424. <th>Details</th>
  4425. </tr>
  4426. </thead>
  4427. <tbody>
  4428. <tr>
  4429. <td>model.selectedMemberGroup</td>
  4430. <td>String</td>
  4431. <td>The selected member group</td>
  4432. </tr>
  4433. <tr>
  4434. <td>model.selectedMemberGroups (multiPicker)</td>
  4435. <td>Array</td>
  4436. <td>The selected member groups</td>
  4437. </tr>
  4438. </tbody>
  4439. </table>
  4440. <h1>Member Picker</h1>
  4441. Opens a member picker. </br>
  4442. <strong>view: </strong>memberpicker
  4443. <table>
  4444. <thead>
  4445. <tr>
  4446. <th>Param</th>
  4447. <th>Type</th>
  4448. <th>Details</th>
  4449. </tr>
  4450. </thead>
  4451. <tbody>
  4452. <tr>
  4453. <td>model.multiPicker</td>
  4454. <td>Boolean</td>
  4455. <td>Pick one or multiple items</td>
  4456. </tr>
  4457. </tbody>
  4458. </table>
  4459. <table>
  4460. <thead>
  4461. <tr>
  4462. <th>Returns</th>
  4463. <th>Type</th>
  4464. <th>Details</th>
  4465. </tr>
  4466. </thead>
  4467. <tbody>
  4468. <tr>
  4469. <td>model.selection</td>
  4470. <td>Array</td>
  4471. <td>Array of selected members/td>
  4472. </tr>
  4473. </tbody>
  4474. </table>
  4475. <h1>YSOD</h1>
  4476. Opens an overlay to show a custom YSOD. </br>
  4477. <strong>view: </strong>ysod
  4478. <table>
  4479. <thead>
  4480. <tr>
  4481. <th>Param</th>
  4482. <th>Type</th>
  4483. <th>Details</th>
  4484. </tr>
  4485. </thead>
  4486. <tbody>
  4487. <tr>
  4488. <td>model.error</td>
  4489. <td>Object</td>
  4490. <td>Error object</td>
  4491. </tr>
  4492. </tbody>
  4493. </table>
  4494. @param {object} model Overlay options.
  4495. @param {string} view Path to view or one of the default view names.
  4496. @param {string} position The overlay position ("left", "right", "center": "target").
  4497. **/
  4498. (function () {
  4499. 'use strict';
  4500. function OverlayDirective($timeout, formHelper, overlayHelper, localizationService) {
  4501. function link(scope, el, attr, ctrl) {
  4502. scope.directive = { enableConfirmButton: false };
  4503. var overlayNumber = 0;
  4504. var numberOfOverlays = 0;
  4505. var isRegistered = false;
  4506. var modelCopy = {};
  4507. function activate() {
  4508. setView();
  4509. setButtonText();
  4510. modelCopy = makeModelCopy(scope.model);
  4511. $timeout(function () {
  4512. if (scope.position === 'target') {
  4513. setTargetPosition();
  4514. }
  4515. // this has to be done inside a timeout to ensure the destroy
  4516. // event on other overlays is run before registering a new one
  4517. registerOverlay();
  4518. setOverlayIndent();
  4519. });
  4520. }
  4521. function setView() {
  4522. if (scope.view) {
  4523. if (scope.view.indexOf('.html') === -1) {
  4524. var viewAlias = scope.view.toLowerCase();
  4525. scope.view = 'views/common/overlays/' + viewAlias + '/' + viewAlias + '.html';
  4526. }
  4527. }
  4528. }
  4529. function setButtonText() {
  4530. if (!scope.model.closeButtonLabelKey && !scope.model.closeButtonLabel) {
  4531. scope.model.closeButtonLabel = localizationService.localize('general_close');
  4532. }
  4533. if (!scope.model.submitButtonLabelKey && !scope.model.submitButtonLabel) {
  4534. scope.model.submitButtonLabel = localizationService.localize('general_submit');
  4535. }
  4536. }
  4537. function registerOverlay() {
  4538. overlayNumber = overlayHelper.registerOverlay();
  4539. $(document).bind('keydown.overlay-' + overlayNumber, function (event) {
  4540. if (event.which === 27) {
  4541. numberOfOverlays = overlayHelper.getNumberOfOverlays();
  4542. if (numberOfOverlays === overlayNumber) {
  4543. scope.$apply(function () {
  4544. scope.closeOverLay();
  4545. });
  4546. }
  4547. event.preventDefault();
  4548. }
  4549. if (event.which === 13) {
  4550. numberOfOverlays = overlayHelper.getNumberOfOverlays();
  4551. if (numberOfOverlays === overlayNumber) {
  4552. var activeElementType = document.activeElement.tagName;
  4553. var clickableElements = [
  4554. 'A',
  4555. 'BUTTON'
  4556. ];
  4557. var submitOnEnter = document.activeElement.hasAttribute('overlay-submit-on-enter');
  4558. if (clickableElements.indexOf(activeElementType) === 0) {
  4559. document.activeElement.click();
  4560. event.preventDefault();
  4561. } else if (activeElementType === 'TEXTAREA' && !submitOnEnter) {
  4562. } else {
  4563. scope.$apply(function () {
  4564. scope.submitForm(scope.model);
  4565. });
  4566. event.preventDefault();
  4567. }
  4568. }
  4569. }
  4570. });
  4571. isRegistered = true;
  4572. }
  4573. function unregisterOverlay() {
  4574. if (isRegistered) {
  4575. overlayHelper.unregisterOverlay();
  4576. $(document).unbind('keydown.overlay-' + overlayNumber);
  4577. isRegistered = false;
  4578. }
  4579. }
  4580. function makeModelCopy(object) {
  4581. var newObject = {};
  4582. for (var key in object) {
  4583. if (key !== 'event') {
  4584. newObject[key] = angular.copy(object[key]);
  4585. }
  4586. }
  4587. return newObject;
  4588. }
  4589. function setOverlayIndent() {
  4590. var overlayIndex = overlayNumber - 1;
  4591. var indentSize = overlayIndex * 20;
  4592. var overlayWidth = el.context.clientWidth;
  4593. el.css('width', overlayWidth - indentSize);
  4594. if (scope.position === 'center' || scope.position === 'target') {
  4595. var overlayTopPosition = el.context.offsetTop;
  4596. el.css('top', overlayTopPosition + indentSize);
  4597. }
  4598. }
  4599. function setTargetPosition() {
  4600. var container = $('#contentwrapper');
  4601. var containerLeft = container[0].offsetLeft;
  4602. var containerRight = containerLeft + container[0].offsetWidth;
  4603. var containerTop = container[0].offsetTop;
  4604. var containerBottom = containerTop + container[0].offsetHeight;
  4605. var mousePositionClickX = null;
  4606. var mousePositionClickY = null;
  4607. var elementHeight = null;
  4608. var elementWidth = null;
  4609. var position = {
  4610. right: 'inherit',
  4611. left: 'inherit',
  4612. top: 'inherit',
  4613. bottom: 'inherit'
  4614. };
  4615. // if mouse click position is know place element with mouse in center
  4616. if (scope.model.event && scope.model.event) {
  4617. // click position
  4618. mousePositionClickX = scope.model.event.pageX;
  4619. mousePositionClickY = scope.model.event.pageY;
  4620. // element size
  4621. elementHeight = el.context.clientHeight;
  4622. elementWidth = el.context.clientWidth;
  4623. // move element to this position
  4624. position.left = mousePositionClickX - elementWidth / 2;
  4625. position.top = mousePositionClickY - elementHeight / 2;
  4626. // check to see if element is outside screen
  4627. // outside right
  4628. if (position.left + elementWidth > containerRight) {
  4629. position.right = 10;
  4630. position.left = 'inherit';
  4631. }
  4632. // outside bottom
  4633. if (position.top + elementHeight > containerBottom) {
  4634. position.bottom = 10;
  4635. position.top = 'inherit';
  4636. }
  4637. // outside left
  4638. if (position.left < containerLeft) {
  4639. position.left = containerLeft + 10;
  4640. position.right = 'inherit';
  4641. }
  4642. // outside top
  4643. if (position.top < containerTop) {
  4644. position.top = 10;
  4645. position.bottom = 'inherit';
  4646. }
  4647. el.css(position);
  4648. }
  4649. }
  4650. scope.submitForm = function (model) {
  4651. if (scope.model.submit) {
  4652. if (formHelper.submitForm({ scope: scope })) {
  4653. formHelper.resetForm({ scope: scope });
  4654. if (scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) {
  4655. scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton);
  4656. } else {
  4657. unregisterOverlay();
  4658. scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton);
  4659. }
  4660. }
  4661. }
  4662. };
  4663. scope.cancelConfirmSubmit = function () {
  4664. scope.model.confirmSubmit.show = false;
  4665. };
  4666. scope.closeOverLay = function () {
  4667. unregisterOverlay();
  4668. if (scope.model.close) {
  4669. scope.model = modelCopy;
  4670. scope.model.close(scope.model);
  4671. } else {
  4672. scope.model.show = false;
  4673. scope.model = null;
  4674. }
  4675. };
  4676. // angular does not support ng-show on custom directives
  4677. // width isolated scopes. So we have to make our own.
  4678. if (attr.hasOwnProperty('ngShow')) {
  4679. scope.$watch('ngShow', function (value) {
  4680. if (value) {
  4681. el.show();
  4682. activate();
  4683. } else {
  4684. unregisterOverlay();
  4685. el.hide();
  4686. }
  4687. });
  4688. } else {
  4689. activate();
  4690. }
  4691. scope.$on('$destroy', function () {
  4692. unregisterOverlay();
  4693. });
  4694. }
  4695. var directive = {
  4696. transclude: true,
  4697. restrict: 'E',
  4698. replace: true,
  4699. templateUrl: 'views/components/overlays/umb-overlay.html',
  4700. scope: {
  4701. ngShow: '=',
  4702. model: '=',
  4703. view: '=',
  4704. position: '@'
  4705. },
  4706. link: link
  4707. };
  4708. return directive;
  4709. }
  4710. angular.module('umbraco.directives').directive('umbOverlay', OverlayDirective);
  4711. }());
  4712. (function () {
  4713. 'use strict';
  4714. function OverlayBackdropDirective(overlayHelper) {
  4715. function link(scope, el, attr, ctrl) {
  4716. scope.numberOfOverlays = 0;
  4717. scope.$watch(function () {
  4718. return overlayHelper.getNumberOfOverlays();
  4719. }, function (newValue) {
  4720. scope.numberOfOverlays = newValue;
  4721. });
  4722. }
  4723. var directive = {
  4724. restrict: 'E',
  4725. replace: true,
  4726. templateUrl: 'views/components/overlays/umb-overlay-backdrop.html',
  4727. link: link
  4728. };
  4729. return directive;
  4730. }
  4731. angular.module('umbraco.directives').directive('umbOverlayBackdrop', OverlayBackdropDirective);
  4732. }());
  4733. /**
  4734. * @ngdoc directive
  4735. * @name umbraco.directives.directive:umbProperty
  4736. * @restrict E
  4737. **/
  4738. angular.module('umbraco.directives').directive('umbProperty', function (umbPropEditorHelper) {
  4739. return {
  4740. scope: { property: '=' },
  4741. transclude: true,
  4742. restrict: 'E',
  4743. replace: true,
  4744. templateUrl: 'views/components/property/umb-property.html',
  4745. link: function (scope) {
  4746. scope.propertyAlias = Umbraco.Sys.ServerVariables.isDebuggingEnabled === true ? scope.property.alias : null;
  4747. },
  4748. //Define a controller for this directive to expose APIs to other directives
  4749. controller: function ($scope, $timeout) {
  4750. var self = this;
  4751. //set the API properties/methods
  4752. self.property = $scope.property;
  4753. self.setPropertyError = function (errorMsg) {
  4754. $scope.property.propertyErrorMessage = errorMsg;
  4755. };
  4756. }
  4757. };
  4758. });
  4759. /**
  4760. * @ngdoc directive
  4761. * @function
  4762. * @name umbraco.directives.directive:umbPropertyEditor
  4763. * @requires formController
  4764. * @restrict E
  4765. **/
  4766. //share property editor directive function
  4767. var _umbPropertyEditor = function (umbPropEditorHelper) {
  4768. return {
  4769. scope: {
  4770. model: '=',
  4771. isPreValue: '@',
  4772. preview: '@'
  4773. },
  4774. require: '^form',
  4775. restrict: 'E',
  4776. replace: true,
  4777. templateUrl: 'views/components/property/umb-property-editor.html',
  4778. link: function (scope, element, attrs, ctrl) {
  4779. //we need to copy the form controller val to our isolated scope so that
  4780. //it get's carried down to the child scopes of this!
  4781. //we'll also maintain the current form name.
  4782. scope[ctrl.$name] = ctrl;
  4783. if (!scope.model.alias) {
  4784. scope.model.alias = Math.random().toString(36).slice(2);
  4785. }
  4786. scope.$watch('model.view', function (val) {
  4787. scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue);
  4788. });
  4789. }
  4790. };
  4791. };
  4792. //Preffered is the umb-property-editor as its more explicit - but we keep umb-editor for backwards compat
  4793. angular.module('umbraco.directives').directive('umbPropertyEditor', _umbPropertyEditor);
  4794. angular.module('umbraco.directives').directive('umbEditor', _umbPropertyEditor);
  4795. angular.module('umbraco.directives.html').directive('umbPropertyGroup', function () {
  4796. return {
  4797. transclude: true,
  4798. restrict: 'E',
  4799. replace: true,
  4800. templateUrl: 'views/components/property/umb-property-group.html'
  4801. };
  4802. });
  4803. /**
  4804. * @ngdoc directive
  4805. * @name umbraco.directives.directive:umbTab
  4806. * @restrict E
  4807. **/
  4808. angular.module('umbraco.directives').directive('umbTab', function ($parse, $timeout) {
  4809. return {
  4810. restrict: 'E',
  4811. replace: true,
  4812. transclude: 'true',
  4813. templateUrl: 'views/components/tabs/umb-tab.html'
  4814. };
  4815. });
  4816. /**
  4817. * @ngdoc directive
  4818. * @name umbraco.directives.directive:umbTabs
  4819. * @restrict A
  4820. * @description Used to bind to bootstrap tab events so that sub directives can use this API to listen to tab changes
  4821. **/
  4822. angular.module('umbraco.directives').directive('umbTabs', function () {
  4823. return {
  4824. restrict: 'A',
  4825. controller: function ($scope, $element, $attrs) {
  4826. var callbacks = [];
  4827. this.onTabShown = function (cb) {
  4828. callbacks.push(cb);
  4829. };
  4830. function tabShown(event) {
  4831. var curr = $(event.target);
  4832. // active tab
  4833. var prev = $(event.relatedTarget);
  4834. // previous tab
  4835. $scope.$apply();
  4836. for (var c in callbacks) {
  4837. callbacks[c].apply(this, [{
  4838. current: curr,
  4839. previous: prev
  4840. }]);
  4841. }
  4842. }
  4843. //NOTE: it MUST be done this way - binding to an ancestor element that exists
  4844. // in the DOM to bind to the dynamic elements that will be created.
  4845. // It would be nicer to create this event handler as a directive for which child
  4846. // directives can attach to.
  4847. $element.on('shown', '.nav-tabs a', tabShown);
  4848. //ensure to unregister
  4849. $scope.$on('$destroy', function () {
  4850. $element.off('shown', '.nav-tabs a', tabShown);
  4851. for (var c in callbacks) {
  4852. delete callbacks[c];
  4853. }
  4854. callbacks = null;
  4855. });
  4856. }
  4857. };
  4858. });
  4859. (function () {
  4860. 'use strict';
  4861. function UmbTabsContentDirective() {
  4862. function link(scope, el, attr, ctrl) {
  4863. scope.view = attr.view;
  4864. }
  4865. var directive = {
  4866. restrict: 'E',
  4867. replace: true,
  4868. transclude: 'true',
  4869. templateUrl: 'views/components/tabs/umb-tabs-content.html',
  4870. link: link
  4871. };
  4872. return directive;
  4873. }
  4874. angular.module('umbraco.directives').directive('umbTabsContent', UmbTabsContentDirective);
  4875. }());
  4876. (function () {
  4877. 'use strict';
  4878. function UmbTabsNavDirective($timeout) {
  4879. function link(scope, el, attr) {
  4880. function activate() {
  4881. $timeout(function () {
  4882. //use bootstrap tabs API to show the first one
  4883. el.find('a:first').tab('show');
  4884. //enable the tab drop
  4885. el.tabdrop();
  4886. });
  4887. }
  4888. var unbindModelWatch = scope.$watch('model', function (newValue, oldValue) {
  4889. activate();
  4890. });
  4891. scope.$on('$destroy', function () {
  4892. //ensure to destroy tabdrop (unbinds window resize listeners)
  4893. el.tabdrop('destroy');
  4894. unbindModelWatch();
  4895. });
  4896. }
  4897. var directive = {
  4898. restrict: 'E',
  4899. replace: true,
  4900. templateUrl: 'views/components/tabs/umb-tabs-nav.html',
  4901. scope: {
  4902. model: '=',
  4903. tabdrop: '=',
  4904. idSuffix: '@'
  4905. },
  4906. link: link
  4907. };
  4908. return directive;
  4909. }
  4910. angular.module('umbraco.directives').directive('umbTabsNav', UmbTabsNavDirective);
  4911. }());
  4912. /**
  4913. * @ngdoc directive
  4914. * @name umbraco.directives.directive:umbTree
  4915. * @restrict E
  4916. **/
  4917. function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificationsService, $timeout, userService) {
  4918. return {
  4919. restrict: 'E',
  4920. replace: true,
  4921. terminal: false,
  4922. scope: {
  4923. section: '@',
  4924. treealias: '@',
  4925. hideoptions: '@',
  4926. hideheader: '@',
  4927. cachekey: '@',
  4928. isdialog: '@',
  4929. onlyinitialized: '@',
  4930. //Custom query string arguments to pass in to the tree as a string, example: "startnodeid=123&something=value"
  4931. customtreeparams: '@',
  4932. eventhandler: '=',
  4933. enablecheckboxes: '@',
  4934. enablelistviewsearch: '@',
  4935. enablelistviewexpand: '@'
  4936. },
  4937. compile: function (element, attrs) {
  4938. //config
  4939. //var showheader = (attrs.showheader !== 'false');
  4940. var hideoptions = attrs.hideoptions === 'true' ? 'hide-options' : '';
  4941. var template = '<ul class="umb-tree ' + hideoptions + '"><li class="root">';
  4942. template += '<div ng-class="getNodeCssClass(tree.root)" ng-hide="hideheader" on-right-click="altSelect(tree.root, $event)">' + '<h5>' + '<a href="#/{{section}}" ng-click="select(tree.root, $event)" class="root-link"><i ng-if="enablecheckboxes == \'true\'" ng-class="selectEnabledNodeClass(tree.root)"></i> {{tree.name}}</a></h5>' + '<a class="umb-options" ng-hide="tree.root.isContainer || !tree.root.menuUrl" ng-click="options(tree.root, $event)" ng-swipe-right="options(tree.root, $event)"><i></i><i></i><i></i></a>' + '</div>';
  4943. template += '<ul>' + '<umb-tree-item ng-repeat="child in tree.root.children" enablelistviewexpand="{{enablelistviewexpand}}" eventhandler="eventhandler" node="child" current-node="currentNode" tree="this" section="{{section}}" ng-animate="animation()"></umb-tree-item>' + '</ul>' + '</li>' + '</ul>';
  4944. element.replaceWith(template);
  4945. return function (scope, elem, attr, controller) {
  4946. //flag to track the last loaded section when the tree 'un-loads'. We use this to determine if we should
  4947. // re-load the tree again. For example, if we hover over 'content' the content tree is shown. Then we hover
  4948. // outside of the tree and the tree 'un-loads'. When we re-hover over 'content', we don't want to re-load the
  4949. // entire tree again since we already still have it in memory. Of course if the section is different we will
  4950. // reload it. This saves a lot on processing if someone is navigating in and out of the same section many times
  4951. // since it saves on data retreival and DOM processing.
  4952. var lastSection = '';
  4953. //setup a default internal handler
  4954. if (!scope.eventhandler) {
  4955. scope.eventhandler = $({});
  4956. }
  4957. //flag to enable/disable delete animations
  4958. var deleteAnimations = false;
  4959. /** Helper function to emit tree events */
  4960. function emitEvent(eventName, args) {
  4961. if (scope.eventhandler) {
  4962. $(scope.eventhandler).trigger(eventName, args);
  4963. }
  4964. }
  4965. /** This will deleteAnimations to true after the current digest */
  4966. function enableDeleteAnimations() {
  4967. //do timeout so that it re-enables them after this digest
  4968. $timeout(function () {
  4969. //enable delete animations
  4970. deleteAnimations = true;
  4971. }, 0, false);
  4972. }
  4973. /*this is the only external interface a tree has */
  4974. function setupExternalEvents() {
  4975. if (scope.eventhandler) {
  4976. scope.eventhandler.clearCache = function (section) {
  4977. treeService.clearCache({ section: section });
  4978. };
  4979. scope.eventhandler.load = function (section) {
  4980. scope.section = section;
  4981. loadTree();
  4982. };
  4983. scope.eventhandler.reloadNode = function (node) {
  4984. if (!node) {
  4985. node = scope.currentNode;
  4986. }
  4987. if (node) {
  4988. scope.loadChildren(node, true);
  4989. }
  4990. };
  4991. /**
  4992. Used to do the tree syncing. If the args.tree is not specified we are assuming it has been
  4993. specified previously using the _setActiveTreeType
  4994. */
  4995. scope.eventhandler.syncTree = function (args) {
  4996. if (!args) {
  4997. throw 'args cannot be null';
  4998. }
  4999. if (!args.path) {
  5000. throw 'args.path cannot be null';
  5001. }
  5002. var deferred = $q.defer();
  5003. //this is super complex but seems to be working in other places, here we're listening for our
  5004. // own events, once the tree is sycned we'll resolve our promise.
  5005. scope.eventhandler.one('treeSynced', function (e, syncArgs) {
  5006. deferred.resolve(syncArgs);
  5007. });
  5008. //this should normally be set unless it is being called from legacy
  5009. // code, so set the active tree type before proceeding.
  5010. if (args.tree) {
  5011. loadActiveTree(args.tree);
  5012. }
  5013. if (angular.isString(args.path)) {
  5014. args.path = args.path.replace('"', '').split(',');
  5015. }
  5016. //reset current node selection
  5017. //scope.currentNode = null;
  5018. //Filter the path for root node ids (we don't want to pass in -1 or 'init')
  5019. args.path = _.filter(args.path, function (item) {
  5020. return item !== 'init' && item !== '-1';
  5021. });
  5022. //Once those are filtered we need to check if the current user has a special start node id,
  5023. // if they do, then we're going to trim the start of the array for anything found from that start node
  5024. // and previous so that the tree syncs properly. The tree syncs from the top down and if there are parts
  5025. // of the tree's path in there that don't actually exist in the dom/model then syncing will not work.
  5026. userService.getCurrentUser().then(function (userData) {
  5027. var startNodes = [];
  5028. for (var i = 0; i < userData.startContentIds; i++) {
  5029. startNodes.push(userData.startContentIds[i]);
  5030. }
  5031. for (var j = 0; j < userData.startMediaIds; j++) {
  5032. startNodes.push(userData.startMediaIds[j]);
  5033. }
  5034. _.each(startNodes, function (i) {
  5035. var found = _.find(args.path, function (p) {
  5036. return String(p) === String(i);
  5037. });
  5038. if (found) {
  5039. args.path = args.path.splice(_.indexOf(args.path, found));
  5040. }
  5041. });
  5042. loadPath(args.path, args.forceReload, args.activate);
  5043. });
  5044. return deferred.promise;
  5045. };
  5046. /**
  5047. Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to
  5048. have to set an active tree and then sync, the new API does this in one method by using syncTree.
  5049. loadChildren is optional but if it is set, it will set the current active tree and load the root
  5050. node's children - this is synonymous with the legacy refreshTree method - again should not be used
  5051. and should only be used for the legacy code to work.
  5052. */
  5053. scope.eventhandler._setActiveTreeType = function (treeAlias, loadChildren) {
  5054. loadActiveTree(treeAlias, loadChildren);
  5055. };
  5056. }
  5057. }
  5058. //helper to load a specific path on the active tree as soon as its ready
  5059. function loadPath(path, forceReload, activate) {
  5060. if (scope.activeTree) {
  5061. syncTree(scope.activeTree, path, forceReload, activate);
  5062. } else {
  5063. scope.eventhandler.one('activeTreeLoaded', function (e, args) {
  5064. syncTree(args.tree, path, forceReload, activate);
  5065. });
  5066. }
  5067. }
  5068. //given a tree alias, this will search the current section tree for the specified tree alias and
  5069. //set that to the activeTree
  5070. //NOTE: loadChildren is ONLY used for legacy purposes, do not use this when syncing the tree as it will cause problems
  5071. // since there will be double request and event handling operations.
  5072. function loadActiveTree(treeAlias, loadChildren) {
  5073. if (!treeAlias) {
  5074. return;
  5075. }
  5076. scope.activeTree = undefined;
  5077. function doLoad(tree) {
  5078. var childrenAndSelf = [tree].concat(tree.children);
  5079. scope.activeTree = _.find(childrenAndSelf, function (node) {
  5080. if (node && node.metaData && node.metaData.treeAlias) {
  5081. return node.metaData.treeAlias.toUpperCase() === treeAlias.toUpperCase();
  5082. }
  5083. return false;
  5084. });
  5085. if (!scope.activeTree) {
  5086. throw 'Could not find the tree ' + treeAlias + ', activeTree has not been set';
  5087. }
  5088. //This is only used for the legacy tree method refreshTree!
  5089. if (loadChildren) {
  5090. scope.activeTree.expanded = true;
  5091. scope.loadChildren(scope.activeTree, false).then(function () {
  5092. emitEvent('activeTreeLoaded', { tree: scope.activeTree });
  5093. });
  5094. } else {
  5095. emitEvent('activeTreeLoaded', { tree: scope.activeTree });
  5096. }
  5097. }
  5098. if (scope.tree) {
  5099. doLoad(scope.tree.root);
  5100. } else {
  5101. scope.eventhandler.one('treeLoaded', function (e, args) {
  5102. doLoad(args.tree.root);
  5103. });
  5104. }
  5105. }
  5106. /** Method to load in the tree data */
  5107. function loadTree() {
  5108. if (!scope.loading && scope.section) {
  5109. scope.loading = true;
  5110. //anytime we want to load the tree we need to disable the delete animations
  5111. deleteAnimations = false;
  5112. //default args
  5113. var args = {
  5114. section: scope.section,
  5115. tree: scope.treealias,
  5116. cacheKey: scope.cachekey,
  5117. isDialog: scope.isdialog ? scope.isdialog : false,
  5118. onlyinitialized: scope.onlyinitialized
  5119. };
  5120. //add the extra query string params if specified
  5121. if (scope.customtreeparams) {
  5122. args['queryString'] = scope.customtreeparams;
  5123. }
  5124. treeService.getTree(args).then(function (data) {
  5125. //set the data once we have it
  5126. scope.tree = data;
  5127. enableDeleteAnimations();
  5128. scope.loading = false;
  5129. //set the root as the current active tree
  5130. scope.activeTree = scope.tree.root;
  5131. emitEvent('treeLoaded', { tree: scope.tree });
  5132. emitEvent('treeNodeExpanded', {
  5133. tree: scope.tree,
  5134. node: scope.tree.root,
  5135. children: scope.tree.root.children
  5136. });
  5137. }, function (reason) {
  5138. scope.loading = false;
  5139. notificationsService.error('Tree Error', reason);
  5140. });
  5141. }
  5142. }
  5143. /** syncs the tree, the treeNode can be ANY tree node in the tree that requires syncing */
  5144. function syncTree(treeNode, path, forceReload, activate) {
  5145. deleteAnimations = false;
  5146. treeService.syncTree({
  5147. node: treeNode,
  5148. path: path,
  5149. forceReload: forceReload
  5150. }).then(function (data) {
  5151. if (activate === undefined || activate === true) {
  5152. scope.currentNode = data;
  5153. }
  5154. emitEvent('treeSynced', {
  5155. node: data,
  5156. activate: activate
  5157. });
  5158. enableDeleteAnimations();
  5159. });
  5160. }
  5161. /** Returns the css classses assigned to the node (div element) */
  5162. scope.getNodeCssClass = function (node) {
  5163. if (!node) {
  5164. return '';
  5165. }
  5166. //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time
  5167. // it would be better if we could cache the processing. The problem is that some of these things are dynamic.
  5168. var css = [];
  5169. if (node.cssClasses) {
  5170. _.each(node.cssClasses, function (c) {
  5171. css.push(c);
  5172. });
  5173. }
  5174. return css.join(' ');
  5175. };
  5176. scope.selectEnabledNodeClass = function (node) {
  5177. return node ? node.selected ? 'icon umb-tree-icon sprTree icon-check green temporary' : '' : '';
  5178. };
  5179. /** method to set the current animation for the node.
  5180. * This changes dynamically based on if we are changing sections or just loading normal tree data.
  5181. * When changing sections we don't want all of the tree-ndoes to do their 'leave' animations.
  5182. */
  5183. scope.animation = function () {
  5184. if (deleteAnimations && scope.tree && scope.tree.root && scope.tree.root.expanded) {
  5185. return { leave: 'tree-node-delete-leave' };
  5186. } else {
  5187. return {};
  5188. }
  5189. };
  5190. /* helper to force reloading children of a tree node */
  5191. scope.loadChildren = function (node, forceReload) {
  5192. var deferred = $q.defer();
  5193. //emit treeNodeExpanding event, if a callback object is set on the tree
  5194. emitEvent('treeNodeExpanding', {
  5195. tree: scope.tree,
  5196. node: node
  5197. });
  5198. //standardising
  5199. if (!node.children) {
  5200. node.children = [];
  5201. }
  5202. if (forceReload || node.hasChildren && node.children.length === 0) {
  5203. //get the children from the tree service
  5204. treeService.loadNodeChildren({
  5205. node: node,
  5206. section: scope.section
  5207. }).then(function (data) {
  5208. //emit expanded event
  5209. emitEvent('treeNodeExpanded', {
  5210. tree: scope.tree,
  5211. node: node,
  5212. children: data
  5213. });
  5214. enableDeleteAnimations();
  5215. deferred.resolve(data);
  5216. });
  5217. } else {
  5218. emitEvent('treeNodeExpanded', {
  5219. tree: scope.tree,
  5220. node: node,
  5221. children: node.children
  5222. });
  5223. node.expanded = true;
  5224. enableDeleteAnimations();
  5225. deferred.resolve(node.children);
  5226. }
  5227. return deferred.promise;
  5228. };
  5229. /**
  5230. Method called when the options button next to the root node is called.
  5231. The tree doesnt know about this, so it raises an event to tell the parent controller
  5232. about it.
  5233. */
  5234. scope.options = function (n, ev) {
  5235. emitEvent('treeOptionsClick', {
  5236. element: elem,
  5237. node: n,
  5238. event: ev
  5239. });
  5240. };
  5241. /**
  5242. Method called when an item is clicked in the tree, this passes the
  5243. DOM element, the tree node object and the original click
  5244. and emits it as a treeNodeSelect element if there is a callback object
  5245. defined on the tree
  5246. */
  5247. scope.select = function (n, ev) {
  5248. if (n.metaData && n.metaData.noAccess === true) {
  5249. ev.preventDefault();
  5250. return;
  5251. }
  5252. //on tree select we need to remove the current node -
  5253. // whoever handles this will need to make sure the correct node is selected
  5254. //reset current node selection
  5255. scope.currentNode = null;
  5256. emitEvent('treeNodeSelect', {
  5257. element: elem,
  5258. node: n,
  5259. event: ev
  5260. });
  5261. };
  5262. scope.altSelect = function (n, ev) {
  5263. emitEvent('treeNodeAltSelect', {
  5264. element: elem,
  5265. tree: scope.tree,
  5266. node: n,
  5267. event: ev
  5268. });
  5269. };
  5270. //watch for section changes
  5271. scope.$watch('section', function (newVal, oldVal) {
  5272. if (!scope.tree) {
  5273. loadTree();
  5274. }
  5275. if (!newVal) {
  5276. //store the last section loaded
  5277. lastSection = oldVal;
  5278. } else if (newVal !== oldVal && newVal !== lastSection) {
  5279. //only reload the tree data and Dom if the newval is different from the old one
  5280. // and if the last section loaded is different from the requested one.
  5281. loadTree();
  5282. //store the new section to be loaded as the last section
  5283. //clear any active trees to reset lookups
  5284. lastSection = newVal;
  5285. }
  5286. });
  5287. setupExternalEvents();
  5288. loadTree();
  5289. };
  5290. }
  5291. };
  5292. }
  5293. angular.module('umbraco.directives').directive('umbTree', umbTreeDirective);
  5294. /**
  5295. * @ngdoc directive
  5296. * @name umbraco.directives.directive:umbTreeItem
  5297. * @element li
  5298. * @function
  5299. *
  5300. * @description
  5301. * Renders a list item, representing a single node in the tree.
  5302. * Includes element to toggle children, and a menu toggling button
  5303. *
  5304. * **note:** This directive is only used internally in the umbTree directive
  5305. *
  5306. * @example
  5307. <example module="umbraco">
  5308. <file name="index.html">
  5309. <umb-tree-item ng-repeat="child in tree.children" node="child" callback="callback" section="content"></umb-tree-item>
  5310. </file>
  5311. </example>
  5312. */
  5313. angular.module('umbraco.directives').directive('umbTreeItem', function ($compile, $http, $templateCache, $interpolate, $log, $location, $rootScope, $window, treeService, $timeout, localizationService) {
  5314. return {
  5315. restrict: 'E',
  5316. replace: true,
  5317. scope: {
  5318. section: '@',
  5319. eventhandler: '=',
  5320. currentNode: '=',
  5321. enablelistviewexpand: '@',
  5322. node: '=',
  5323. tree: '='
  5324. },
  5325. //TODO: Remove more of the binding from this template and move the DOM manipulation to be manually done in the link function,
  5326. // this will greatly improve performance since there's potentially a lot of nodes being rendered = a LOT of watches!
  5327. template: '<li ng-class="{\'current\': (node == currentNode), \'has-children\': node.hasChildren}" on-right-click="altSelect(node, $event)">' + '<div ng-class="getNodeCssClass(node)" ng-swipe-right="options(node, $event)" ng-dblclick="load(node)" >' + //NOTE: This ins element is used to display the search icon if the node is a container/listview and the tree is currently in dialog
  5328. //'<ins ng-if="tree.enablelistviewsearch && node.metaData.isContainer" class="umb-tree-node-search icon-search" ng-click="searchNode(node, $event)" alt="searchAltText"></ins>' +
  5329. '<ins ng-class="{\'icon-navigation-right\': !node.expanded || node.metaData.isContainer, \'icon-navigation-down\': node.expanded && !node.metaData.isContainer}" ng-click="load(node)">&nbsp;</ins>' + '<i class="icon umb-tree-icon sprTree" ng-click="select(node, $event)"></i>' + '<a class="umb-tree-item__label" href="#/{{node.routePath}}" ng-click="select(node, $event)"></a>' + //NOTE: These are the 'option' elipses
  5330. '<a class="umb-options" ng-click="options(node, $event)"><i></i><i></i><i></i></a>' + '<div ng-show="node.loading" class="l"><div></div></div>' + '</div>' + '</li>',
  5331. link: function (scope, element, attrs) {
  5332. localizationService.localize('general_search').then(function (value) {
  5333. scope.searchAltText = value;
  5334. });
  5335. //flag to enable/disable delete animations, default for an item is true
  5336. var deleteAnimations = true;
  5337. // Helper function to emit tree events
  5338. function emitEvent(eventName, args) {
  5339. if (scope.eventhandler) {
  5340. $(scope.eventhandler).trigger(eventName, args);
  5341. }
  5342. }
  5343. // updates the node's DOM/styles
  5344. function setupNodeDom(node, tree) {
  5345. //get the first div element
  5346. element.children(':first') //set the padding
  5347. .css('padding-left', node.level * 20 + 'px');
  5348. //toggle visibility of last 'ins' depending on children
  5349. //visibility still ensure the space is "reserved", so both nodes with and without children are aligned.
  5350. if (node.hasChildren || node.metaData.isContainer && scope.enablelistviewexpand === 'true') {
  5351. element.find('ins').last().css('visibility', 'visible');
  5352. } else {
  5353. element.find('ins').last().css('visibility', 'hidden');
  5354. }
  5355. var icon = element.find('i:first');
  5356. icon.addClass(node.cssClass);
  5357. icon.attr('title', node.routePath);
  5358. element.find('a:first').text(node.name);
  5359. if (!node.menuUrl) {
  5360. element.find('a.umb-options').remove();
  5361. }
  5362. if (node.style) {
  5363. element.find('i:first').attr('style', node.style);
  5364. }
  5365. }
  5366. //This will deleteAnimations to true after the current digest
  5367. function enableDeleteAnimations() {
  5368. //do timeout so that it re-enables them after this digest
  5369. $timeout(function () {
  5370. //enable delete animations
  5371. deleteAnimations = true;
  5372. }, 0, false);
  5373. }
  5374. /** Returns the css classses assigned to the node (div element) */
  5375. scope.getNodeCssClass = function (node) {
  5376. if (!node) {
  5377. return '';
  5378. }
  5379. //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time
  5380. // it would be better if we could cache the processing. The problem is that some of these things are dynamic.
  5381. var css = [];
  5382. if (node.cssClasses) {
  5383. _.each(node.cssClasses, function (c) {
  5384. css.push(c);
  5385. });
  5386. }
  5387. if (node.selected) {
  5388. css.push('umb-tree-node-checked');
  5389. }
  5390. return css.join(' ');
  5391. };
  5392. //add a method to the node which we can use to call to update the node data if we need to ,
  5393. // this is done by sync tree, we don't want to add a $watch for each node as that would be crazy insane slow
  5394. // so we have to do this
  5395. scope.node.updateNodeData = function (newNode) {
  5396. _.extend(scope.node, newNode);
  5397. //now update the styles
  5398. setupNodeDom(scope.node, scope.tree);
  5399. };
  5400. /**
  5401. Method called when the options button next to a node is called
  5402. In the main tree this opens the menu, but internally the tree doesnt
  5403. know about this, so it simply raises an event to tell the parent controller
  5404. about it.
  5405. */
  5406. scope.options = function (n, ev) {
  5407. emitEvent('treeOptionsClick', {
  5408. element: element,
  5409. tree: scope.tree,
  5410. node: n,
  5411. event: ev
  5412. });
  5413. };
  5414. /**
  5415. Method called when an item is clicked in the tree, this passes the
  5416. DOM element, the tree node object and the original click
  5417. and emits it as a treeNodeSelect element if there is a callback object
  5418. defined on the tree
  5419. */
  5420. scope.select = function (n, ev) {
  5421. if (ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.button && ev.button === 1 // middle click, >IE9 + everyone else
  5422. ) {
  5423. return;
  5424. }
  5425. if (n.metaData && n.metaData.noAccess === true) {
  5426. ev.preventDefault();
  5427. return;
  5428. }
  5429. emitEvent('treeNodeSelect', {
  5430. element: element,
  5431. tree: scope.tree,
  5432. node: n,
  5433. event: ev
  5434. });
  5435. ev.preventDefault();
  5436. };
  5437. /**
  5438. Method called when an item is right-clicked in the tree, this passes the
  5439. DOM element, the tree node object and the original click
  5440. and emits it as a treeNodeSelect element if there is a callback object
  5441. defined on the tree
  5442. */
  5443. scope.altSelect = function (n, ev) {
  5444. emitEvent('treeNodeAltSelect', {
  5445. element: element,
  5446. tree: scope.tree,
  5447. node: n,
  5448. event: ev
  5449. });
  5450. };
  5451. /** method to set the current animation for the node.
  5452. * This changes dynamically based on if we are changing sections or just loading normal tree data.
  5453. * When changing sections we don't want all of the tree-ndoes to do their 'leave' animations.
  5454. */
  5455. scope.animation = function () {
  5456. if (scope.node.showHideAnimation) {
  5457. return scope.node.showHideAnimation;
  5458. }
  5459. if (deleteAnimations && scope.node.expanded) {
  5460. return { leave: 'tree-node-delete-leave' };
  5461. } else {
  5462. return {};
  5463. }
  5464. };
  5465. /**
  5466. Method called when a node in the tree is expanded, when clicking the arrow
  5467. takes the arrow DOM element and node data as parameters
  5468. emits treeNodeCollapsing event if already expanded and treeNodeExpanding if collapsed
  5469. */
  5470. scope.load = function (node) {
  5471. if (node.expanded && !node.metaData.isContainer) {
  5472. deleteAnimations = false;
  5473. emitEvent('treeNodeCollapsing', {
  5474. tree: scope.tree,
  5475. node: node,
  5476. element: element
  5477. });
  5478. node.expanded = false;
  5479. } else {
  5480. scope.loadChildren(node, false);
  5481. }
  5482. };
  5483. /* helper to force reloading children of a tree node */
  5484. scope.loadChildren = function (node, forceReload) {
  5485. //emit treeNodeExpanding event, if a callback object is set on the tree
  5486. emitEvent('treeNodeExpanding', {
  5487. tree: scope.tree,
  5488. node: node
  5489. });
  5490. if (node.hasChildren && (forceReload || !node.children || angular.isArray(node.children) && node.children.length === 0)) {
  5491. //get the children from the tree service
  5492. treeService.loadNodeChildren({
  5493. node: node,
  5494. section: scope.section
  5495. }).then(function (data) {
  5496. //emit expanded event
  5497. emitEvent('treeNodeExpanded', {
  5498. tree: scope.tree,
  5499. node: node,
  5500. children: data
  5501. });
  5502. enableDeleteAnimations();
  5503. });
  5504. } else {
  5505. emitEvent('treeNodeExpanded', {
  5506. tree: scope.tree,
  5507. node: node,
  5508. children: node.children
  5509. });
  5510. node.expanded = true;
  5511. enableDeleteAnimations();
  5512. }
  5513. };
  5514. //if the current path contains the node id, we will auto-expand the tree item children
  5515. setupNodeDom(scope.node, scope.tree);
  5516. // load the children if the current user don't have access to the node
  5517. // it is used to auto expand the tree to the start nodes the user has access to
  5518. if (scope.node.hasChildren && scope.node.metaData.noAccess) {
  5519. scope.loadChildren(scope.node);
  5520. }
  5521. var template = '<ul ng-class="{collapsed: !node.expanded}"><umb-tree-item ng-repeat="child in node.children" enablelistviewexpand="{{enablelistviewexpand}}" eventhandler="eventhandler" tree="tree" current-node="currentNode" node="child" section="{{section}}" ng-animate="animation()"></umb-tree-item></ul>';
  5522. var newElement = angular.element(template);
  5523. $compile(newElement)(scope);
  5524. element.append(newElement);
  5525. }
  5526. };
  5527. });
  5528. /**
  5529. * @ngdoc directive
  5530. * @name umbraco.directives.directive:umbTreeSearchBox
  5531. * @function
  5532. * @element ANY
  5533. * @restrict E
  5534. **/
  5535. function treeSearchBox(localizationService, searchService, $q) {
  5536. return {
  5537. scope: {
  5538. searchFromId: '@',
  5539. searchFromName: '@',
  5540. showSearch: '@',
  5541. section: '@',
  5542. hideSearchCallback: '=',
  5543. searchCallback: '='
  5544. },
  5545. restrict: 'E',
  5546. // restrict to an element
  5547. replace: true,
  5548. // replace the html element with the template
  5549. templateUrl: 'views/components/tree/umb-tree-search-box.html',
  5550. link: function (scope, element, attrs, ctrl) {
  5551. scope.term = '';
  5552. scope.hideSearch = function () {
  5553. scope.term = '';
  5554. scope.hideSearchCallback();
  5555. };
  5556. localizationService.localize('general_typeToSearch').then(function (value) {
  5557. scope.searchPlaceholderText = value;
  5558. });
  5559. if (!scope.showSearch) {
  5560. scope.showSearch = 'false';
  5561. }
  5562. //used to cancel any request in progress if another one needs to take it's place
  5563. var canceler = null;
  5564. function performSearch() {
  5565. if (scope.term) {
  5566. scope.results = [];
  5567. //a canceler exists, so perform the cancelation operation and reset
  5568. if (canceler) {
  5569. canceler.resolve();
  5570. canceler = $q.defer();
  5571. } else {
  5572. canceler = $q.defer();
  5573. }
  5574. var searchArgs = {
  5575. term: scope.term,
  5576. canceler: canceler
  5577. };
  5578. //append a start node context if there is one
  5579. if (scope.searchFromId) {
  5580. searchArgs['searchFrom'] = scope.searchFromId;
  5581. }
  5582. searcher(searchArgs).then(function (data) {
  5583. scope.searchCallback(data);
  5584. //set back to null so it can be re-created
  5585. canceler = null;
  5586. });
  5587. }
  5588. }
  5589. scope.$watch('term', _.debounce(function (newVal, oldVal) {
  5590. scope.$apply(function () {
  5591. if (newVal !== null && newVal !== undefined && newVal !== oldVal) {
  5592. performSearch();
  5593. }
  5594. });
  5595. }, 200));
  5596. var searcher = searchService.searchContent;
  5597. //search
  5598. if (scope.section === 'member') {
  5599. searcher = searchService.searchMembers;
  5600. } else if (scope.section === 'media') {
  5601. searcher = searchService.searchMedia;
  5602. }
  5603. }
  5604. };
  5605. }
  5606. angular.module('umbraco.directives').directive('umbTreeSearchBox', treeSearchBox);
  5607. /**
  5608. * @ngdoc directive
  5609. * @name umbraco.directives.directive:umbTreeSearchResults
  5610. * @function
  5611. * @element ANY
  5612. * @restrict E
  5613. **/
  5614. function treeSearchResults() {
  5615. return {
  5616. scope: {
  5617. results: '=',
  5618. selectResultCallback: '='
  5619. },
  5620. restrict: 'E',
  5621. // restrict to an element
  5622. replace: true,
  5623. // replace the html element with the template
  5624. templateUrl: 'views/components/tree/umb-tree-search-results.html',
  5625. link: function (scope, element, attrs, ctrl) {
  5626. }
  5627. };
  5628. }
  5629. angular.module('umbraco.directives').directive('umbTreeSearchResults', treeSearchResults);
  5630. (function () {
  5631. 'use strict';
  5632. function AceEditorDirective(umbAceEditorConfig, assetsService, angularHelper) {
  5633. /**
  5634. * Sets editor options such as the wrapping mode or the syntax checker.
  5635. *
  5636. * The supported options are:
  5637. *
  5638. * <ul>
  5639. * <li>showGutter</li>
  5640. * <li>useWrapMode</li>
  5641. * <li>onLoad</li>
  5642. * <li>theme</li>
  5643. * <li>mode</li>
  5644. * </ul>
  5645. *
  5646. * @param acee
  5647. * @param session ACE editor session
  5648. * @param {object} opts Options to be set
  5649. */
  5650. var setOptions = function (acee, session, opts) {
  5651. // sets the ace worker path, if running from concatenated
  5652. // or minified source
  5653. if (angular.isDefined(opts.workerPath)) {
  5654. var config = window.ace.require('ace/config');
  5655. config.set('workerPath', opts.workerPath);
  5656. }
  5657. // ace requires loading
  5658. if (angular.isDefined(opts.require)) {
  5659. opts.require.forEach(function (n) {
  5660. window.ace.require(n);
  5661. });
  5662. }
  5663. // Boolean options
  5664. if (angular.isDefined(opts.showGutter)) {
  5665. acee.renderer.setShowGutter(opts.showGutter);
  5666. }
  5667. if (angular.isDefined(opts.useWrapMode)) {
  5668. session.setUseWrapMode(opts.useWrapMode);
  5669. }
  5670. if (angular.isDefined(opts.showInvisibles)) {
  5671. acee.renderer.setShowInvisibles(opts.showInvisibles);
  5672. }
  5673. if (angular.isDefined(opts.showIndentGuides)) {
  5674. acee.renderer.setDisplayIndentGuides(opts.showIndentGuides);
  5675. }
  5676. if (angular.isDefined(opts.useSoftTabs)) {
  5677. session.setUseSoftTabs(opts.useSoftTabs);
  5678. }
  5679. if (angular.isDefined(opts.showPrintMargin)) {
  5680. acee.setShowPrintMargin(opts.showPrintMargin);
  5681. }
  5682. // commands
  5683. if (angular.isDefined(opts.disableSearch) && opts.disableSearch) {
  5684. acee.commands.addCommands([{
  5685. name: 'unfind',
  5686. bindKey: {
  5687. win: 'Ctrl-F',
  5688. mac: 'Command-F'
  5689. },
  5690. exec: function () {
  5691. return false;
  5692. },
  5693. readOnly: true
  5694. }]);
  5695. }
  5696. // Basic options
  5697. if (angular.isString(opts.theme)) {
  5698. acee.setTheme('ace/theme/' + opts.theme);
  5699. }
  5700. if (angular.isString(opts.mode)) {
  5701. session.setMode('ace/mode/' + opts.mode);
  5702. }
  5703. // Advanced options
  5704. if (angular.isDefined(opts.firstLineNumber)) {
  5705. if (angular.isNumber(opts.firstLineNumber)) {
  5706. session.setOption('firstLineNumber', opts.firstLineNumber);
  5707. } else if (angular.isFunction(opts.firstLineNumber)) {
  5708. session.setOption('firstLineNumber', opts.firstLineNumber());
  5709. }
  5710. }
  5711. // advanced options
  5712. var key, obj;
  5713. if (angular.isDefined(opts.advanced)) {
  5714. for (key in opts.advanced) {
  5715. // create a javascript object with the key and value
  5716. obj = {
  5717. name: key,
  5718. value: opts.advanced[key]
  5719. };
  5720. // try to assign the option to the ace editor
  5721. acee.setOption(obj.name, obj.value);
  5722. }
  5723. }
  5724. // advanced options for the renderer
  5725. if (angular.isDefined(opts.rendererOptions)) {
  5726. for (key in opts.rendererOptions) {
  5727. // create a javascript object with the key and value
  5728. obj = {
  5729. name: key,
  5730. value: opts.rendererOptions[key]
  5731. };
  5732. // try to assign the option to the ace editor
  5733. acee.renderer.setOption(obj.name, obj.value);
  5734. }
  5735. }
  5736. // onLoad callbacks
  5737. angular.forEach(opts.callbacks, function (cb) {
  5738. if (angular.isFunction(cb)) {
  5739. cb(acee);
  5740. }
  5741. });
  5742. };
  5743. function link(scope, el, attr, ngModel) {
  5744. // Load in ace library
  5745. assetsService.load([
  5746. 'lib/ace-builds/src-min-noconflict/ace.js',
  5747. 'lib/ace-builds/src-min-noconflict/ext-language_tools.js'
  5748. ]).then(function () {
  5749. if (angular.isUndefined(window.ace)) {
  5750. throw new Error('ui-ace need ace to work... (o rly?)');
  5751. } else {
  5752. // init editor
  5753. init();
  5754. }
  5755. });
  5756. function init() {
  5757. /**
  5758. * Corresponds the umbAceEditorConfig ACE configuration.
  5759. * @type object
  5760. */
  5761. var options = umbAceEditorConfig.ace || {};
  5762. /**
  5763. * umbAceEditorConfig merged with user options via json in attribute or data binding
  5764. * @type object
  5765. */
  5766. var opts = angular.extend({}, options, scope.umbAceEditor);
  5767. //load ace libraries here...
  5768. /**
  5769. * ACE editor
  5770. * @type object
  5771. */
  5772. var acee = window.ace.edit(el[0]);
  5773. acee.$blockScrolling = Infinity;
  5774. /**
  5775. * ACE editor session.
  5776. * @type object
  5777. * @see [EditSession]{@link http://ace.c9.io/#nav=api&api=edit_session}
  5778. */
  5779. var session = acee.getSession();
  5780. /**
  5781. * Reference to a change listener created by the listener factory.
  5782. * @function
  5783. * @see listenerFactory.onChange
  5784. */
  5785. var onChangeListener;
  5786. /**
  5787. * Reference to a blur listener created by the listener factory.
  5788. * @function
  5789. * @see listenerFactory.onBlur
  5790. */
  5791. var onBlurListener;
  5792. /**
  5793. * Calls a callback by checking its existing. The argument list
  5794. * is variable and thus this function is relying on the arguments
  5795. * object.
  5796. * @throws {Error} If the callback isn't a function
  5797. */
  5798. var executeUserCallback = function () {
  5799. /**
  5800. * The callback function grabbed from the array-like arguments
  5801. * object. The first argument should always be the callback.
  5802. *
  5803. * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments}
  5804. * @type {*}
  5805. */
  5806. var callback = arguments[0];
  5807. /**
  5808. * Arguments to be passed to the callback. These are taken
  5809. * from the array-like arguments object. The first argument
  5810. * is stripped because that should be the callback function.
  5811. *
  5812. * @see [arguments]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/arguments}
  5813. * @type {Array}
  5814. */
  5815. var args = Array.prototype.slice.call(arguments, 1);
  5816. if (angular.isDefined(callback)) {
  5817. scope.$evalAsync(function () {
  5818. if (angular.isFunction(callback)) {
  5819. callback(args);
  5820. } else {
  5821. throw new Error('ui-ace use a function as callback.');
  5822. }
  5823. });
  5824. }
  5825. };
  5826. /**
  5827. * Listener factory. Until now only change listeners can be created.
  5828. * @type object
  5829. */
  5830. var listenerFactory = {
  5831. /**
  5832. * Creates a change listener which propagates the change event
  5833. * and the editor session to the callback from the user option
  5834. * onChange. It might be exchanged during runtime, if this
  5835. * happens the old listener will be unbound.
  5836. *
  5837. * @param callback callback function defined in the user options
  5838. * @see onChangeListener
  5839. */
  5840. onChange: function (callback) {
  5841. return function (e) {
  5842. var newValue = session.getValue();
  5843. angularHelper.safeApply(scope, function () {
  5844. scope.model = newValue;
  5845. });
  5846. executeUserCallback(callback, e, acee);
  5847. };
  5848. },
  5849. /**
  5850. * Creates a blur listener which propagates the editor session
  5851. * to the callback from the user option onBlur. It might be
  5852. * exchanged during runtime, if this happens the old listener
  5853. * will be unbound.
  5854. *
  5855. * @param callback callback function defined in the user options
  5856. * @see onBlurListener
  5857. */
  5858. onBlur: function (callback) {
  5859. return function () {
  5860. executeUserCallback(callback, acee);
  5861. };
  5862. }
  5863. };
  5864. attr.$observe('readonly', function (value) {
  5865. acee.setReadOnly(!!value || value === '');
  5866. });
  5867. // Value Blind
  5868. if (scope.model) {
  5869. session.setValue(scope.model);
  5870. }
  5871. // Listen for option updates
  5872. var updateOptions = function (current, previous) {
  5873. if (current === previous) {
  5874. return;
  5875. }
  5876. opts = angular.extend({}, options, scope.umbAceEditor);
  5877. opts.callbacks = [opts.onLoad];
  5878. if (opts.onLoad !== options.onLoad) {
  5879. // also call the global onLoad handler
  5880. opts.callbacks.unshift(options.onLoad);
  5881. }
  5882. // EVENTS
  5883. // unbind old change listener
  5884. session.removeListener('change', onChangeListener);
  5885. // bind new change listener
  5886. onChangeListener = listenerFactory.onChange(opts.onChange);
  5887. session.on('change', onChangeListener);
  5888. // unbind old blur listener
  5889. //session.removeListener('blur', onBlurListener);
  5890. acee.removeListener('blur', onBlurListener);
  5891. // bind new blur listener
  5892. onBlurListener = listenerFactory.onBlur(opts.onBlur);
  5893. acee.on('blur', onBlurListener);
  5894. setOptions(acee, session, opts);
  5895. };
  5896. scope.$watch(scope.umbAceEditor, updateOptions, /* deep watch */
  5897. true);
  5898. // set the options here, even if we try to watch later, if this
  5899. // line is missing things go wrong (and the tests will also fail)
  5900. updateOptions(options);
  5901. el.on('$destroy', function () {
  5902. acee.session.$stopWorker();
  5903. acee.destroy();
  5904. });
  5905. scope.$watch(function () {
  5906. return [
  5907. el[0].offsetWidth,
  5908. el[0].offsetHeight
  5909. ];
  5910. }, function () {
  5911. acee.resize();
  5912. acee.renderer.updateFull();
  5913. }, true);
  5914. }
  5915. }
  5916. var directive = {
  5917. restrict: 'EA',
  5918. scope: {
  5919. 'umbAceEditor': '=',
  5920. 'model': '='
  5921. },
  5922. link: link
  5923. };
  5924. return directive;
  5925. }
  5926. angular.module('umbraco.directives').constant('umbAceEditorConfig', {}).directive('umbAceEditor', AceEditorDirective);
  5927. }());
  5928. /**
  5929. @ngdoc directive
  5930. @name umbraco.directives.directive:umbAvatar
  5931. @restrict E
  5932. @scope
  5933. @description
  5934. Use this directive to render an avatar.
  5935. <h3>Markup example</h3>
  5936. <pre>
  5937. <div ng-controller="My.Controller as vm">
  5938. <umb-avatar
  5939. size="xs"
  5940. img-src="{{vm.avatar[0].value}}"
  5941. img-srcset="{{vm.avatar[1].value}} 2x, {{vm.avatar[2].value}} 3x">
  5942. </umb-avatar>
  5943. </div>
  5944. </pre>
  5945. <h3>Controller example</h3>
  5946. <pre>
  5947. (function () {
  5948. "use strict";
  5949. function Controller() {
  5950. var vm = this;
  5951. vm.avatar = [
  5952. { value: "assets/logo.png" },
  5953. { value: "assets/logo@2x.png" },
  5954. { value: "assets/logo@3x.png" }
  5955. ];
  5956. }
  5957. angular.module("umbraco").controller("My.Controller", Controller);
  5958. })();
  5959. </pre>
  5960. @param {string} size (<code>attribute</code>): The size of the avatar (xs, s, m, l, xl).
  5961. @param {string} img-src (<code>attribute</code>): The image source to the avatar.
  5962. @param {string} img-srcset (<code>atribute</code>): Reponsive support for the image source.
  5963. **/
  5964. (function () {
  5965. 'use strict';
  5966. function AvatarDirective() {
  5967. function link(scope, element, attrs, ctrl) {
  5968. var eventBindings = [];
  5969. scope.initials = '';
  5970. function onInit() {
  5971. if (!scope.unknownChar) {
  5972. scope.unknownChar = '?';
  5973. }
  5974. scope.initials = getNameInitials(scope.name);
  5975. }
  5976. function getNameInitials(name) {
  5977. if (name) {
  5978. var names = name.split(' '), initials = names[0].substring(0, 1);
  5979. if (names.length > 1) {
  5980. initials += names[names.length - 1].substring(0, 1);
  5981. }
  5982. return initials.toUpperCase();
  5983. }
  5984. return null;
  5985. }
  5986. eventBindings.push(scope.$watch('name', function (newValue, oldValue) {
  5987. if (newValue === oldValue) {
  5988. return;
  5989. }
  5990. if (oldValue === undefined || newValue === undefined) {
  5991. return;
  5992. }
  5993. scope.initials = getNameInitials(newValue);
  5994. }));
  5995. onInit();
  5996. }
  5997. var directive = {
  5998. restrict: 'E',
  5999. replace: true,
  6000. templateUrl: 'views/components/umb-avatar.html',
  6001. scope: {
  6002. size: '@',
  6003. name: '@',
  6004. color: '@',
  6005. imgSrc: '@',
  6006. imgSrcset: '@',
  6007. unknownChar: '@'
  6008. },
  6009. link: link
  6010. };
  6011. return directive;
  6012. }
  6013. angular.module('umbraco.directives').directive('umbAvatar', AvatarDirective);
  6014. }());
  6015. (function () {
  6016. 'use strict';
  6017. function BadgeDirective() {
  6018. var directive = {
  6019. restrict: 'E',
  6020. replace: true,
  6021. transclude: true,
  6022. templateUrl: 'views/components/umb-badge.html',
  6023. scope: {
  6024. size: '@?',
  6025. color: '@?'
  6026. }
  6027. };
  6028. return directive;
  6029. }
  6030. angular.module('umbraco.directives').directive('umbBadge', BadgeDirective);
  6031. }());
  6032. (function () {
  6033. 'use strict';
  6034. function CheckmarkDirective() {
  6035. var directive = {
  6036. restrict: 'E',
  6037. replace: true,
  6038. transclude: true,
  6039. templateUrl: 'views/components/umb-checkmark.html',
  6040. scope: {
  6041. size: '@?',
  6042. checked: '='
  6043. }
  6044. };
  6045. return directive;
  6046. }
  6047. angular.module('umbraco.directives').directive('umbCheckmark', CheckmarkDirective);
  6048. }());
  6049. /**
  6050. @ngdoc directive
  6051. @name umbraco.directives.directive:umbChildSelector
  6052. @restrict E
  6053. @scope
  6054. @description
  6055. Use this directive to render a ui component for selecting child items to a parent node.
  6056. <h3>Markup example</h3>
  6057. <pre>
  6058. <div ng-controller="My.Controller as vm">
  6059. <umb-child-selector
  6060. selected-children="vm.selectedChildren"
  6061. available-children="vm.availableChildren"
  6062. parent-name="vm.name"
  6063. parent-icon="vm.icon"
  6064. parent-id="vm.id"
  6065. on-add="vm.addChild"
  6066. on-remove="vm.removeChild">
  6067. </umb-child-selector>
  6068. <!-- use overlay to select children from -->
  6069. <umb-overlay
  6070. ng-if="vm.overlay.show"
  6071. model="vm.overlay"
  6072. position="target"
  6073. view="vm.overlay.view">
  6074. </umb-overlay>
  6075. </div>
  6076. </pre>
  6077. <h3>Controller example</h3>
  6078. <pre>
  6079. (function () {
  6080. "use strict";
  6081. function Controller() {
  6082. var vm = this;
  6083. vm.id = 1;
  6084. vm.name = "My Parent element";
  6085. vm.icon = "icon-document";
  6086. vm.selectedChildren = [];
  6087. vm.availableChildren = [
  6088. {
  6089. id: 1,
  6090. alias: "item1",
  6091. name: "Item 1",
  6092. icon: "icon-document"
  6093. },
  6094. {
  6095. id: 2,
  6096. alias: "item2",
  6097. name: "Item 2",
  6098. icon: "icon-document"
  6099. }
  6100. ];
  6101. vm.addChild = addChild;
  6102. vm.removeChild = removeChild;
  6103. function addChild($event) {
  6104. vm.overlay = {
  6105. view: "itempicker",
  6106. title: "Choose child",
  6107. availableItems: vm.availableChildren,
  6108. selectedItems: vm.selectedChildren,
  6109. event: $event,
  6110. show: true,
  6111. submit: function(model) {
  6112. // add selected child
  6113. vm.selectedChildren.push(model.selectedItem);
  6114. // close overlay
  6115. vm.overlay.show = false;
  6116. vm.overlay = null;
  6117. }
  6118. };
  6119. }
  6120. function removeChild($index) {
  6121. vm.selectedChildren.splice($index, 1);
  6122. }
  6123. }
  6124. angular.module("umbraco").controller("My.Controller", Controller);
  6125. })();
  6126. </pre>
  6127. @param {array} selectedChildren (<code>binding</code>): Array of selected children.
  6128. @param {array} availableChildren (<code>binding</code>: Array of items available for selection.
  6129. @param {string} parentName (<code>binding</code>): The parent name.
  6130. @param {string} parentIcon (<code>binding</code>): The parent icon.
  6131. @param {number} parentId (<code>binding</code>): The parent id.
  6132. @param {callback} onRemove (<code>binding</code>): Callback when the remove button is clicked on an item.
  6133. <h3>The callback returns:</h3>
  6134. <ul>
  6135. <li><code>child</code>: The selected item.</li>
  6136. <li><code>$index</code>: The selected item index.</li>
  6137. </ul>
  6138. @param {callback} onAdd (<code>binding</code>): Callback when the add button is clicked.
  6139. <h3>The callback returns:</h3>
  6140. <ul>
  6141. <li><code>$event</code>: The select event.</li>
  6142. </ul>
  6143. **/
  6144. (function () {
  6145. 'use strict';
  6146. function ChildSelectorDirective() {
  6147. function link(scope, el, attr, ctrl) {
  6148. var eventBindings = [];
  6149. scope.dialogModel = {};
  6150. scope.showDialog = false;
  6151. scope.removeChild = function (selectedChild, $index) {
  6152. if (scope.onRemove) {
  6153. scope.onRemove(selectedChild, $index);
  6154. }
  6155. };
  6156. scope.addChild = function ($event) {
  6157. if (scope.onAdd) {
  6158. scope.onAdd($event);
  6159. }
  6160. };
  6161. function syncParentName() {
  6162. // update name on available item
  6163. angular.forEach(scope.availableChildren, function (availableChild) {
  6164. if (availableChild.id === scope.parentId) {
  6165. availableChild.name = scope.parentName;
  6166. }
  6167. });
  6168. // update name on selected child
  6169. angular.forEach(scope.selectedChildren, function (selectedChild) {
  6170. if (selectedChild.id === scope.parentId) {
  6171. selectedChild.name = scope.parentName;
  6172. }
  6173. });
  6174. }
  6175. function syncParentIcon() {
  6176. // update icon on available item
  6177. angular.forEach(scope.availableChildren, function (availableChild) {
  6178. if (availableChild.id === scope.parentId) {
  6179. availableChild.icon = scope.parentIcon;
  6180. }
  6181. });
  6182. // update icon on selected child
  6183. angular.forEach(scope.selectedChildren, function (selectedChild) {
  6184. if (selectedChild.id === scope.parentId) {
  6185. selectedChild.icon = scope.parentIcon;
  6186. }
  6187. });
  6188. }
  6189. eventBindings.push(scope.$watch('parentName', function (newValue, oldValue) {
  6190. if (newValue === oldValue) {
  6191. return;
  6192. }
  6193. if (oldValue === undefined || newValue === undefined) {
  6194. return;
  6195. }
  6196. syncParentName();
  6197. }));
  6198. eventBindings.push(scope.$watch('parentIcon', function (newValue, oldValue) {
  6199. if (newValue === oldValue) {
  6200. return;
  6201. }
  6202. if (oldValue === undefined || newValue === undefined) {
  6203. return;
  6204. }
  6205. syncParentIcon();
  6206. }));
  6207. // clean up
  6208. scope.$on('$destroy', function () {
  6209. // unbind watchers
  6210. for (var e in eventBindings) {
  6211. eventBindings[e]();
  6212. }
  6213. });
  6214. }
  6215. var directive = {
  6216. restrict: 'E',
  6217. replace: true,
  6218. templateUrl: 'views/components/umb-child-selector.html',
  6219. scope: {
  6220. selectedChildren: '=',
  6221. availableChildren: '=',
  6222. parentName: '=',
  6223. parentIcon: '=',
  6224. parentId: '=',
  6225. onRemove: '=',
  6226. onAdd: '='
  6227. },
  6228. link: link
  6229. };
  6230. return directive;
  6231. }
  6232. angular.module('umbraco.directives').directive('umbChildSelector', ChildSelectorDirective);
  6233. }());
  6234. /**
  6235. @ngdoc directive
  6236. @name umbraco.directives.directive:umbClipboard
  6237. @restrict E
  6238. @scope
  6239. @description
  6240. <strong>Added in Umbraco v. 7.7:</strong> Use this directive to copy content to the clipboard
  6241. <h3>Markup example</h3>
  6242. <pre>
  6243. <div ng-controller="My.ClipBoardController as vm">
  6244. <!-- Copy text from an element -->
  6245. <div id="copy-text">Copy me!</div>
  6246. <umb-button
  6247. umb-clipboard
  6248. umb-clipboard-success="vm.copySuccess()"
  6249. umb-clipboard-error="vm.copyError()"
  6250. umb-clipboard-target="#copy-text"
  6251. state="vm.clipboardButtonState"
  6252. type="button"
  6253. label="Copy">
  6254. </umb-button>
  6255. <!-- Cut text from a textarea -->
  6256. <textarea id="cut-text" ng-model="vm.cutText"></textarea>
  6257. <umb-button
  6258. umb-clipboard
  6259. umb-clipboard-success="vm.copySuccess()"
  6260. umb-clipboard-error="vm.copyError()"
  6261. umb-clipboard-target="#cut-text"
  6262. umb-clipboard-action="cut"
  6263. state="vm.clipboardButtonState"
  6264. type="button"
  6265. label="Copy">
  6266. </umb-button>
  6267. <!-- Copy text without an element -->
  6268. <umb-button
  6269. ng-if="vm.copyText"
  6270. umb-clipboard
  6271. umb-clipboard-success="vm.copySuccess()"
  6272. umb-clipboard-error="vm.copyError()"
  6273. umb-clipboard-text="vm.copyText"
  6274. state="vm.clipboardButtonState"
  6275. type="button"
  6276. label="Copy">
  6277. </umb-button>
  6278. </div>
  6279. </pre>
  6280. <h3>Controller example</h3>
  6281. <pre>
  6282. (function () {
  6283. "use strict";
  6284. function Controller() {
  6285. var vm = this;
  6286. vm.copyText = "Copy text without element";
  6287. vm.cutText = "Text to cut";
  6288. vm.copySuccess = copySuccess;
  6289. vm.copyError = copyError;
  6290. function copySuccess() {
  6291. vm.clipboardButtonState = "success";
  6292. }
  6293. function copyError() {
  6294. vm.clipboardButtonState = "error";
  6295. }
  6296. }
  6297. angular.module("umbraco").controller("My.ClipBoardController", Controller);
  6298. })();
  6299. </pre>
  6300. @param {callback} umbClipboardSuccess (<code>expression</code>): Callback function when the content is copied.
  6301. @param {callback} umbClipboardError (<code>expression</code>): Callback function if the copy fails.
  6302. @param {string} umbClipboardTarget (<code>attribute</code>): The target element to copy.
  6303. @param {string} umbClipboardAction (<code>attribute</code>): Specify if you want to copy or cut content ("copy", "cut"). Cut only works on <code>input</code> and <code>textarea</code> elements.
  6304. @param {string} umbClipboardText (<code>attribute</code>): Use this attribute if you don't have an element to copy from.
  6305. **/
  6306. (function () {
  6307. 'use strict';
  6308. function umbClipboardDirective($timeout, assetsService) {
  6309. function link(scope, element, attrs, ctrl) {
  6310. var clipboard;
  6311. var target = element[0];
  6312. assetsService.loadJs('lib/clipboard/clipboard.min.js').then(function () {
  6313. if (scope.umbClipboardTarget) {
  6314. target.setAttribute('data-clipboard-target', scope.umbClipboardTarget);
  6315. }
  6316. if (scope.umbClipboardAction) {
  6317. target.setAttribute('data-clipboard-action', scope.umbClipboardAction);
  6318. }
  6319. if (scope.umbClipboardText) {
  6320. target.setAttribute('data-clipboard-text', scope.umbClipboardText);
  6321. }
  6322. clipboard = new Clipboard(target);
  6323. clipboard.on('success', function (e) {
  6324. e.clearSelection();
  6325. if (scope.umbClipboardSuccess) {
  6326. scope.$apply(function () {
  6327. scope.umbClipboardSuccess({ e: e });
  6328. });
  6329. }
  6330. });
  6331. clipboard.on('error', function (e) {
  6332. if (scope.umbClipboardError) {
  6333. scope.$apply(function () {
  6334. scope.umbClipboardError({ e: e });
  6335. });
  6336. }
  6337. });
  6338. });
  6339. // clean up
  6340. scope.$on('$destroy', function () {
  6341. clipboard.destroy();
  6342. });
  6343. }
  6344. ////////////
  6345. var directive = {
  6346. restrict: 'A',
  6347. scope: {
  6348. umbClipboardSuccess: '&?',
  6349. umbClipboardError: '&?',
  6350. umbClipboardTarget: '@?',
  6351. umbClipboardAction: '@?',
  6352. umbClipboardText: '=?'
  6353. },
  6354. link: link
  6355. };
  6356. return directive;
  6357. }
  6358. angular.module('umbraco.directives').directive('umbClipboard', umbClipboardDirective);
  6359. }());
  6360. /**
  6361. * @ngdoc directive
  6362. * @name umbraco.directives.directive:umbConfirm
  6363. * @function
  6364. * @description
  6365. * A confirmation dialog
  6366. *
  6367. * @restrict E
  6368. */
  6369. function confirmDirective() {
  6370. return {
  6371. restrict: 'E',
  6372. // restrict to an element
  6373. replace: true,
  6374. // replace the html element with the template
  6375. templateUrl: 'views/components/umb-confirm.html',
  6376. scope: {
  6377. onConfirm: '=',
  6378. onCancel: '=',
  6379. caption: '@'
  6380. },
  6381. link: function (scope, element, attr, ctrl) {
  6382. }
  6383. };
  6384. }
  6385. angular.module('umbraco.directives').directive('umbConfirm', confirmDirective);
  6386. /**
  6387. @ngdoc directive
  6388. @name umbraco.directives.directive:umbConfirmAction
  6389. @restrict E
  6390. @scope
  6391. @description
  6392. <p>Use this directive to toggle a confirmation prompt for an action.
  6393. The prompt consists of a checkmark and a cross to confirm or cancel the action.
  6394. The prompt can be opened in four direction up, down, left or right.</p>
  6395. <h3>Markup example</h3>
  6396. <pre>
  6397. <div ng-controller="My.Controller as vm">
  6398. <div class="my-action" style="position:relative;">
  6399. <i class="icon-trash" ng-click="vm.showPrompt()"></i>
  6400. <umb-confirm-action
  6401. ng-if="vm.promptIsVisible"
  6402. direction="left"
  6403. on-confirm="vm.confirmAction()"
  6404. on-cancel="vm.hidePrompt()">
  6405. </umb-confirm-action>
  6406. </div>
  6407. </div>
  6408. </pre>
  6409. <h3>Controller example</h3>
  6410. <pre>
  6411. (function () {
  6412. "use strict";
  6413. function Controller() {
  6414. var vm = this;
  6415. vm.promptIsVisible = false;
  6416. vm.confirmAction = confirmAction;
  6417. vm.showPrompt = showPrompt;
  6418. vm.hidePrompt = hidePrompt;
  6419. function confirmAction() {
  6420. // confirm logic here
  6421. }
  6422. function showPrompt() {
  6423. vm.promptIsVisible = true;
  6424. }
  6425. function hidePrompt() {
  6426. vm.promptIsVisible = false;
  6427. }
  6428. }
  6429. angular.module("umbraco").controller("My.Controller", Controller);
  6430. })();
  6431. </pre>
  6432. @param {string} direction The direction the prompt opens ("up", "down", "left", "right").
  6433. @param {callback} onConfirm Callback when the checkmark is clicked.
  6434. @param {callback} onCancel Callback when the cross is clicked.
  6435. **/
  6436. (function () {
  6437. 'use strict';
  6438. function ConfirmAction() {
  6439. function link(scope, el, attr, ctrl) {
  6440. scope.clickConfirm = function () {
  6441. if (scope.onConfirm) {
  6442. scope.onConfirm();
  6443. }
  6444. };
  6445. scope.clickCancel = function () {
  6446. if (scope.onCancel) {
  6447. scope.onCancel();
  6448. }
  6449. };
  6450. }
  6451. var directive = {
  6452. restrict: 'E',
  6453. replace: true,
  6454. templateUrl: 'views/components/umb-confirm-action.html',
  6455. scope: {
  6456. direction: '@',
  6457. onConfirm: '&',
  6458. onCancel: '&'
  6459. },
  6460. link: link
  6461. };
  6462. return directive;
  6463. }
  6464. angular.module('umbraco.directives').directive('umbConfirmAction', ConfirmAction);
  6465. }());
  6466. /**
  6467. @ngdoc directive
  6468. @name umbraco.directives.directive:umbContentGrid
  6469. @restrict E
  6470. @scope
  6471. @description
  6472. Use this directive to generate a list of content items presented as a flexbox grid.
  6473. <h3>Markup example</h3>
  6474. <pre>
  6475. <div ng-controller="My.Controller as vm">
  6476. <umb-content-grid
  6477. content="vm.contentItems"
  6478. content-properties="vm.includeProperties"
  6479. on-click="vm.selectItem"
  6480. on-click-name="vm.clickItem">
  6481. </umb-content-grid>
  6482. </div>
  6483. </pre>
  6484. <h3>Controller example</h3>
  6485. <pre>
  6486. (function () {
  6487. "use strict";
  6488. function Controller() {
  6489. var vm = this;
  6490. vm.contentItems = [
  6491. {
  6492. "name": "Cape",
  6493. "published": true,
  6494. "icon": "icon-document",
  6495. "updateDate": "15-02-2016",
  6496. "owner": "Mr. Batman",
  6497. "selected": false
  6498. },
  6499. {
  6500. "name": "Utility Belt",
  6501. "published": true,
  6502. "icon": "icon-document",
  6503. "updateDate": "15-02-2016",
  6504. "owner": "Mr. Batman",
  6505. "selected": false
  6506. },
  6507. {
  6508. "name": "Cave",
  6509. "published": true,
  6510. "icon": "icon-document",
  6511. "updateDate": "15-02-2016",
  6512. "owner": "Mr. Batman",
  6513. "selected": false
  6514. }
  6515. ];
  6516. vm.includeProperties = [
  6517. {
  6518. "alias": "updateDate",
  6519. "header": "Last edited"
  6520. },
  6521. {
  6522. "alias": "owner",
  6523. "header": "Created by"
  6524. }
  6525. ];
  6526. vm.clickItem = clickItem;
  6527. vm.selectItem = selectItem;
  6528. function clickItem(item, $event, $index){
  6529. // do magic here
  6530. }
  6531. function selectItem(item, $event, $index) {
  6532. // set item.selected = true; to select the item
  6533. // do magic here
  6534. }
  6535. }
  6536. angular.module("umbraco").controller("My.Controller", Controller);
  6537. })();
  6538. </pre>
  6539. @param {array} content (<code>binding</code>): Array of content items.
  6540. @param {array=} contentProperties (<code>binding</code>): Array of content item properties to include in the item. If left empty the item will only show the item icon and name.
  6541. @param {callback=} onClick (<code>binding</code>): Callback method to handle click events on the content item.
  6542. <h3>The callback returns:</h3>
  6543. <ul>
  6544. <li><code>item</code>: The clicked item</li>
  6545. <li><code>$event</code>: The select event</li>
  6546. <li><code>$index</code>: The item index</li>
  6547. </ul>
  6548. @param {callback=} onClickName (<code>binding</code>): Callback method to handle click events on the checkmark icon.
  6549. <h3>The callback returns:</h3>
  6550. <ul>
  6551. <li><code>item</code>: The selected item</li>
  6552. <li><code>$event</code>: The select event</li>
  6553. <li><code>$index</code>: The item index</li>
  6554. </ul>
  6555. **/
  6556. (function () {
  6557. 'use strict';
  6558. function ContentGridDirective() {
  6559. function link(scope, el, attr, ctrl) {
  6560. scope.clickItem = function (item, $event, $index) {
  6561. if (scope.onClick) {
  6562. scope.onClick(item, $event, $index);
  6563. }
  6564. };
  6565. scope.clickItemName = function (item, $event, $index) {
  6566. if (scope.onClickName) {
  6567. scope.onClickName(item, $event, $index);
  6568. }
  6569. };
  6570. }
  6571. var directive = {
  6572. restrict: 'E',
  6573. replace: true,
  6574. templateUrl: 'views/components/umb-content-grid.html',
  6575. scope: {
  6576. content: '=',
  6577. contentProperties: '=',
  6578. onClick: '=',
  6579. onClickName: '='
  6580. },
  6581. link: link
  6582. };
  6583. return directive;
  6584. }
  6585. angular.module('umbraco.directives').directive('umbContentGrid', ContentGridDirective);
  6586. }());
  6587. /**
  6588. @ngdoc directive
  6589. @name umbraco.directives.directive:umbDateTimePicker
  6590. @restrict E
  6591. @scope
  6592. @description
  6593. <b>Added in Umbraco version 7.6</b>
  6594. This directive is a wrapper of the bootstrap datetime picker version 3.1.3. Use it to render a date time picker.
  6595. For extra details about options and events take a look here: http://eonasdan.github.io/bootstrap-datetimepicker/
  6596. Use this directive to render a date time picker
  6597. <h3>Markup example</h3>
  6598. <pre>
  6599. <div ng-controller="My.Controller as vm">
  6600. <umb-date-time-picker
  6601. options="vm.config"
  6602. on-change="vm.datePickerChange(event)"
  6603. on-error="vm.datePickerError(event)">
  6604. </umb-date-time-picker>
  6605. </div>
  6606. </pre>
  6607. <h3>Controller example</h3>
  6608. <pre>
  6609. (function () {
  6610. "use strict";
  6611. function Controller() {
  6612. var vm = this;
  6613. vm.date = "";
  6614. vm.config = {
  6615. pickDate: true,
  6616. pickTime: true,
  6617. useSeconds: true,
  6618. format: "YYYY-MM-DD HH:mm:ss",
  6619. icons: {
  6620. time: "icon-time",
  6621. date: "icon-calendar",
  6622. up: "icon-chevron-up",
  6623. down: "icon-chevron-down"
  6624. }
  6625. };
  6626. vm.datePickerChange = datePickerChange;
  6627. vm.datePickerError = datePickerError;
  6628. function datePickerChange(event) {
  6629. // handle change
  6630. if(event.date && event.date.isValid()) {
  6631. var date = event.date.format(vm.datePickerConfig.format);
  6632. }
  6633. }
  6634. function datePickerError(event) {
  6635. // handle error
  6636. }
  6637. }
  6638. angular.module("umbraco").controller("My.Controller", Controller);
  6639. })();
  6640. </pre>
  6641. @param {object} options (<code>binding</code>): Config object for the date picker.
  6642. @param {callback} onHide (<code>callback</code>): Hide callback.
  6643. @param {callback} onShow (<code>callback</code>): Show callback.
  6644. @param {callback} onChange (<code>callback</code>): Change callback.
  6645. @param {callback} onError (<code>callback</code>): Error callback.
  6646. @param {callback} onUpdate (<code>callback</code>): Update callback.
  6647. **/
  6648. (function () {
  6649. 'use strict';
  6650. function DateTimePickerDirective(assetsService) {
  6651. function link(scope, element, attrs, ctrl) {
  6652. function onInit() {
  6653. // load css file for the date picker
  6654. assetsService.loadCss('lib/datetimepicker/bootstrap-datetimepicker.min.css');
  6655. // load the js file for the date picker
  6656. assetsService.loadJs('lib/datetimepicker/bootstrap-datetimepicker.js').then(function () {
  6657. // init date picker
  6658. initDatePicker();
  6659. });
  6660. }
  6661. function onHide(event) {
  6662. if (scope.onHide) {
  6663. scope.$apply(function () {
  6664. // callback
  6665. scope.onHide({ event: event });
  6666. });
  6667. }
  6668. }
  6669. function onShow() {
  6670. if (scope.onShow) {
  6671. scope.$apply(function () {
  6672. // callback
  6673. scope.onShow();
  6674. });
  6675. }
  6676. }
  6677. function onChange(event) {
  6678. if (scope.onChange && event.date && event.date.isValid()) {
  6679. scope.$apply(function () {
  6680. // callback
  6681. scope.onChange({ event: event });
  6682. });
  6683. }
  6684. }
  6685. function onError(event) {
  6686. if (scope.onError) {
  6687. scope.$apply(function () {
  6688. // callback
  6689. scope.onError({ event: event });
  6690. });
  6691. }
  6692. }
  6693. function onUpdate(event) {
  6694. if (scope.onUpdate) {
  6695. scope.$apply(function () {
  6696. // callback
  6697. scope.onUpdate({ event: event });
  6698. });
  6699. }
  6700. }
  6701. function initDatePicker() {
  6702. // Open the datepicker and add a changeDate eventlistener
  6703. element.datetimepicker(scope.options).on('dp.hide', onHide).on('dp.show', onShow).on('dp.change', onChange).on('dp.error', onError).on('dp.update', onUpdate);
  6704. }
  6705. onInit();
  6706. }
  6707. var directive = {
  6708. restrict: 'E',
  6709. replace: true,
  6710. templateUrl: 'views/components/umb-date-time-picker.html',
  6711. scope: {
  6712. options: '=',
  6713. onHide: '&',
  6714. onShow: '&',
  6715. onChange: '&',
  6716. onError: '&',
  6717. onUpdate: '&'
  6718. },
  6719. link: link
  6720. };
  6721. return directive;
  6722. }
  6723. angular.module('umbraco.directives').directive('umbDateTimePicker', DateTimePickerDirective);
  6724. }());
  6725. (function () {
  6726. 'use strict';
  6727. function UmbDisableFormValidation() {
  6728. var directive = {
  6729. restrict: 'A',
  6730. require: '?form',
  6731. link: function (scope, elm, attrs, ctrl) {
  6732. //override the $setValidity function of the form to disable validation
  6733. ctrl.$setValidity = function () {
  6734. };
  6735. }
  6736. };
  6737. return directive;
  6738. }
  6739. angular.module('umbraco.directives').directive('umbDisableFormValidation', UmbDisableFormValidation);
  6740. }());
  6741. /**
  6742. @ngdoc directive
  6743. @name umbraco.directives.directive:umbDropdown
  6744. @restrict E
  6745. @scope
  6746. @description
  6747. <b>Added in versions 7.7.0</b>: Use this component to render a dropdown menu.
  6748. <h3>Markup example</h3>
  6749. <pre>
  6750. <div ng-controller="MyDropdown.Controller as vm">
  6751. <div style="position: relative;">
  6752. <umb-button
  6753. type="button"
  6754. label="Toggle dropdown"
  6755. action="vm.toggle()">
  6756. </umb-button>
  6757. <umb-dropdown ng-if="vm.dropdownOpen" on-close="vm.close()" umb-keyboard-list>
  6758. <umb-dropdown-item
  6759. ng-repeat="item in vm.items">
  6760. <a href="" ng-click="vm.select(item)">{{ item.name }}</a>
  6761. </umb-dropdown-item>
  6762. </umb-dropdown>
  6763. </div>
  6764. </div>
  6765. </pre>
  6766. <h3>Controller example</h3>
  6767. <pre>
  6768. (function () {
  6769. "use strict";
  6770. function Controller() {
  6771. var vm = this;
  6772. vm.dropdownOpen = false;
  6773. vm.items = [
  6774. { "name": "Item 1" },
  6775. { "name": "Item 2" },
  6776. { "name": "Item 3" }
  6777. ];
  6778. vm.toggle = toggle;
  6779. vm.close = close;
  6780. vm.select = select;
  6781. function toggle() {
  6782. vm.dropdownOpen = true;
  6783. }
  6784. function close() {
  6785. vm.dropdownOpen = false;
  6786. }
  6787. function select(item) {
  6788. // Do your magic here
  6789. }
  6790. }
  6791. angular.module("umbraco").controller("MyDropdown.Controller", Controller);
  6792. })();
  6793. </pre>
  6794. <h3>Use in combination with</h3>
  6795. <ul>
  6796. <li>{@link umbraco.directives.directive:umbDropdownItem umbDropdownItem}</li>
  6797. <li>{@link umbraco.directives.directive:umbKeyboardList umbKeyboardList}</li>
  6798. </ul>
  6799. @param {callback} onClose Callback when the dropdown menu closes. When you click outside or press esc.
  6800. **/
  6801. (function () {
  6802. 'use strict';
  6803. function umbDropdown($document) {
  6804. function link(scope, element, attr, ctrl) {
  6805. scope.close = function () {
  6806. if (scope.onClose) {
  6807. scope.onClose();
  6808. }
  6809. };
  6810. // Handle keydown events
  6811. function keydown(event) {
  6812. // press escape
  6813. if (event.keyCode === 27) {
  6814. scope.onClose();
  6815. }
  6816. }
  6817. // Stop to listen typing.
  6818. function stopListening() {
  6819. $document.off('keydown', keydown);
  6820. }
  6821. // Start listening to key typing.
  6822. $document.on('keydown', keydown);
  6823. // Stop listening when scope is destroyed.
  6824. scope.$on('$destroy', stopListening);
  6825. }
  6826. var directive = {
  6827. restrict: 'E',
  6828. replace: true,
  6829. transclude: true,
  6830. templateUrl: 'views/components/umb-dropdown.html',
  6831. scope: { onClose: '&' },
  6832. link: link
  6833. };
  6834. return directive;
  6835. }
  6836. angular.module('umbraco.directives').directive('umbDropdown', umbDropdown);
  6837. }());
  6838. /**
  6839. @ngdoc directive
  6840. @name umbraco.directives.directive:umbDropdownItem
  6841. @restrict E
  6842. @description
  6843. <b>Added in versions 7.7.0</b>: Use this directive to construct a dropdown item. See documentation for {@link umbraco.directives.directive:umbDropdown umbDropdown}.
  6844. **/
  6845. (function () {
  6846. 'use strict';
  6847. function umbDropdownItem() {
  6848. var directive = {
  6849. restrict: 'E',
  6850. replace: true,
  6851. transclude: true,
  6852. templateUrl: 'views/components/umb-dropdown-item.html'
  6853. };
  6854. return directive;
  6855. }
  6856. angular.module('umbraco.directives').directive('umbDropdownItem', umbDropdownItem);
  6857. }());
  6858. /**
  6859. @ngdoc directive
  6860. @name umbraco.directives.directive:umbEmptyState
  6861. @restrict E
  6862. @scope
  6863. @description
  6864. Use this directive to show an empty state message.
  6865. <h3>Markup example</h3>
  6866. <pre>
  6867. <div ng-controller="My.Controller as vm">
  6868. <umb-empty-state
  6869. ng-if="!vm.items"
  6870. position="center">
  6871. // Empty state content
  6872. </umb-empty-state>
  6873. </div>
  6874. </pre>
  6875. @param {string=} size Set the size of the text ("small", "large").
  6876. @param {string=} position Set the position of the text ("center").
  6877. **/
  6878. (function () {
  6879. 'use strict';
  6880. function EmptyStateDirective() {
  6881. var directive = {
  6882. restrict: 'E',
  6883. replace: true,
  6884. transclude: true,
  6885. templateUrl: 'views/components/umb-empty-state.html',
  6886. scope: {
  6887. size: '@',
  6888. position: '@'
  6889. }
  6890. };
  6891. return directive;
  6892. }
  6893. angular.module('umbraco.directives').directive('umbEmptyState', EmptyStateDirective);
  6894. }());
  6895. /**
  6896. @ngdoc directive
  6897. @name umbraco.directives.directive:umbFolderGrid
  6898. @restrict E
  6899. @scope
  6900. @description
  6901. Use this directive to generate a list of folders presented as a flexbox grid.
  6902. <h3>Markup example</h3>
  6903. <pre>
  6904. <div ng-controller="My.Controller as vm">
  6905. <umb-folder-grid
  6906. ng-if="vm.folders.length > 0"
  6907. folders="vm.folders"
  6908. on-click="vm.clickFolder"
  6909. on-select="vm.selectFolder">
  6910. </umb-folder-grid>
  6911. </div>
  6912. </pre>
  6913. <h3>Controller example</h3>
  6914. <pre>
  6915. (function () {
  6916. "use strict";
  6917. function Controller(myService) {
  6918. var vm = this;
  6919. vm.folders = [
  6920. {
  6921. "name": "Folder 1",
  6922. "icon": "icon-folder",
  6923. "selected": false
  6924. },
  6925. {
  6926. "name": "Folder 2",
  6927. "icon": "icon-folder",
  6928. "selected": false
  6929. }
  6930. ];
  6931. vm.clickFolder = clickFolder;
  6932. vm.selectFolder = selectFolder;
  6933. myService.getFolders().then(function(folders){
  6934. vm.folders = folders;
  6935. });
  6936. function clickFolder(folder){
  6937. // Execute when clicking folder name/link
  6938. }
  6939. function selectFolder(folder, event, index) {
  6940. // Execute when clicking folder
  6941. // set folder.selected = true; to show checkmark icon
  6942. }
  6943. }
  6944. angular.module("umbraco").controller("My.Controller", Controller);
  6945. })();
  6946. </pre>
  6947. @param {array} folders (<code>binding</code>): Array of folders
  6948. @param {callback=} onClick (<code>binding</code>): Callback method to handle click events on the folder.
  6949. <h3>The callback returns:</h3>
  6950. <ul>
  6951. <li><code>folder</code>: The selected folder</li>
  6952. </ul>
  6953. @param {callback=} onSelect (<code>binding</code>): Callback method to handle click events on the checkmark icon.
  6954. <h3>The callback returns:</h3>
  6955. <ul>
  6956. <li><code>folder</code>: The selected folder</li>
  6957. <li><code>$event</code>: The select event</li>
  6958. <li><code>$index</code>: The folder index</li>
  6959. </ul>
  6960. **/
  6961. (function () {
  6962. 'use strict';
  6963. function FolderGridDirective() {
  6964. function link(scope, el, attr, ctrl) {
  6965. scope.clickFolder = function (folder, $event, $index) {
  6966. if (scope.onClick) {
  6967. scope.onClick(folder, $event, $index);
  6968. $event.stopPropagation();
  6969. }
  6970. };
  6971. scope.clickFolderName = function (folder, $event, $index) {
  6972. if (scope.onClickName) {
  6973. scope.onClickName(folder, $event, $index);
  6974. $event.stopPropagation();
  6975. }
  6976. };
  6977. }
  6978. var directive = {
  6979. restrict: 'E',
  6980. replace: true,
  6981. templateUrl: 'views/components/umb-folder-grid.html',
  6982. scope: {
  6983. folders: '=',
  6984. onClick: '=',
  6985. onClickName: '='
  6986. },
  6987. link: link
  6988. };
  6989. return directive;
  6990. }
  6991. angular.module('umbraco.directives').directive('umbFolderGrid', FolderGridDirective);
  6992. }());
  6993. /**
  6994. @ngdoc directive
  6995. @name umbraco.directives.directive:umbGenerateAlias
  6996. @restrict E
  6997. @scope
  6998. @description
  6999. Use this directive to generate a camelCased umbraco alias.
  7000. When the aliasFrom value is changed the directive will get a formatted alias from the server and update the alias model. If "enableLock" is set to <code>true</code>
  7001. the directive will use {@link umbraco.directives.directive:umbLockedField umbLockedField} to lock and unlock the alias.
  7002. <h3>Markup example</h3>
  7003. <pre>
  7004. <div ng-controller="My.Controller as vm">
  7005. <input type="text" ng-model="vm.name" />
  7006. <umb-generate-alias
  7007. enable-lock="true"
  7008. alias-from="vm.name"
  7009. alias="vm.alias">
  7010. </umb-generate-alias>
  7011. </div>
  7012. </pre>
  7013. <h3>Controller example</h3>
  7014. <pre>
  7015. (function () {
  7016. "use strict";
  7017. function Controller() {
  7018. var vm = this;
  7019. vm.name = "";
  7020. vm.alias = "";
  7021. }
  7022. angular.module("umbraco").controller("My.Controller", Controller);
  7023. })();
  7024. </pre>
  7025. @param {string} alias (<code>binding</code>): The model where the alias is bound.
  7026. @param {string} aliasFrom (<code>binding</code>): The model to generate the alias from.
  7027. @param {boolean=} enableLock (<code>binding</code>): Set to <code>true</code> to add a lock next to the alias from where it can be unlocked and changed.
  7028. **/
  7029. angular.module('umbraco.directives').directive('umbGenerateAlias', function ($timeout, entityResource) {
  7030. return {
  7031. restrict: 'E',
  7032. templateUrl: 'views/components/umb-generate-alias.html',
  7033. replace: true,
  7034. scope: {
  7035. alias: '=',
  7036. aliasFrom: '=',
  7037. enableLock: '=?',
  7038. serverValidationField: '@'
  7039. },
  7040. link: function (scope, element, attrs, ctrl) {
  7041. var eventBindings = [];
  7042. var bindWatcher = true;
  7043. var generateAliasTimeout = '';
  7044. var updateAlias = false;
  7045. scope.locked = true;
  7046. scope.placeholderText = 'Enter alias...';
  7047. function generateAlias(value) {
  7048. if (generateAliasTimeout) {
  7049. $timeout.cancel(generateAliasTimeout);
  7050. }
  7051. if (value !== undefined && value !== '' && value !== null) {
  7052. scope.alias = '';
  7053. scope.placeholderText = 'Generating Alias...';
  7054. generateAliasTimeout = $timeout(function () {
  7055. updateAlias = true;
  7056. entityResource.getSafeAlias(value, true).then(function (safeAlias) {
  7057. if (updateAlias) {
  7058. scope.alias = safeAlias.alias;
  7059. }
  7060. });
  7061. }, 500);
  7062. } else {
  7063. updateAlias = true;
  7064. scope.alias = '';
  7065. scope.placeholderText = 'Enter alias...';
  7066. }
  7067. }
  7068. // if alias gets unlocked - stop watching alias
  7069. eventBindings.push(scope.$watch('locked', function (newValue, oldValue) {
  7070. if (newValue === false) {
  7071. bindWatcher = false;
  7072. }
  7073. }));
  7074. // validate custom entered alias
  7075. eventBindings.push(scope.$watch('alias', function (newValue, oldValue) {
  7076. if (scope.alias === '' && bindWatcher === true || scope.alias === null && bindWatcher === true) {
  7077. // add watcher
  7078. eventBindings.push(scope.$watch('aliasFrom', function (newValue, oldValue) {
  7079. if (bindWatcher) {
  7080. generateAlias(newValue);
  7081. }
  7082. }));
  7083. }
  7084. }));
  7085. // clean up
  7086. scope.$on('$destroy', function () {
  7087. // unbind watchers
  7088. for (var e in eventBindings) {
  7089. eventBindings[e]();
  7090. }
  7091. });
  7092. }
  7093. };
  7094. });
  7095. (function () {
  7096. 'use strict';
  7097. function GridSelector() {
  7098. function link(scope, el, attr, ctrl) {
  7099. var eventBindings = [];
  7100. scope.dialogModel = {};
  7101. scope.showDialog = false;
  7102. scope.itemLabel = '';
  7103. // set default item name
  7104. if (!scope.itemName) {
  7105. scope.itemLabel = 'item';
  7106. } else {
  7107. scope.itemLabel = scope.itemName;
  7108. }
  7109. scope.removeItem = function (selectedItem) {
  7110. var selectedItemIndex = scope.selectedItems.indexOf(selectedItem);
  7111. scope.selectedItems.splice(selectedItemIndex, 1);
  7112. };
  7113. scope.removeDefaultItem = function () {
  7114. // it will be the last item so we can clear the array
  7115. scope.selectedItems = [];
  7116. // remove as default item
  7117. scope.defaultItem = null;
  7118. };
  7119. scope.openItemPicker = function ($event) {
  7120. scope.dialogModel = {
  7121. view: 'itempicker',
  7122. title: 'Choose ' + scope.itemLabel,
  7123. availableItems: scope.availableItems,
  7124. selectedItems: scope.selectedItems,
  7125. event: $event,
  7126. show: true,
  7127. submit: function (model) {
  7128. scope.selectedItems.push(model.selectedItem);
  7129. // if no default item - set item as default
  7130. if (scope.defaultItem === null) {
  7131. scope.setAsDefaultItem(model.selectedItem);
  7132. }
  7133. scope.dialogModel.show = false;
  7134. scope.dialogModel = null;
  7135. }
  7136. };
  7137. };
  7138. scope.setAsDefaultItem = function (selectedItem) {
  7139. // clear default item
  7140. scope.defaultItem = {};
  7141. // set as default item
  7142. scope.defaultItem = selectedItem;
  7143. };
  7144. function updatePlaceholders() {
  7145. // update default item
  7146. if (scope.defaultItem !== null && scope.defaultItem.placeholder) {
  7147. scope.defaultItem.name = scope.name;
  7148. if (scope.alias !== null && scope.alias !== undefined) {
  7149. scope.defaultItem.alias = scope.alias;
  7150. }
  7151. }
  7152. // update selected items
  7153. angular.forEach(scope.selectedItems, function (selectedItem) {
  7154. if (selectedItem.placeholder) {
  7155. selectedItem.name = scope.name;
  7156. if (scope.alias !== null && scope.alias !== undefined) {
  7157. selectedItem.alias = scope.alias;
  7158. }
  7159. }
  7160. });
  7161. // update availableItems
  7162. angular.forEach(scope.availableItems, function (availableItem) {
  7163. if (availableItem.placeholder) {
  7164. availableItem.name = scope.name;
  7165. if (scope.alias !== null && scope.alias !== undefined) {
  7166. availableItem.alias = scope.alias;
  7167. }
  7168. }
  7169. });
  7170. }
  7171. function activate() {
  7172. // add watchers for updating placeholde name and alias
  7173. if (scope.updatePlaceholder) {
  7174. eventBindings.push(scope.$watch('name', function (newValue, oldValue) {
  7175. updatePlaceholders();
  7176. }));
  7177. eventBindings.push(scope.$watch('alias', function (newValue, oldValue) {
  7178. updatePlaceholders();
  7179. }));
  7180. }
  7181. }
  7182. activate();
  7183. // clean up
  7184. scope.$on('$destroy', function () {
  7185. // clear watchers
  7186. for (var e in eventBindings) {
  7187. eventBindings[e]();
  7188. }
  7189. });
  7190. }
  7191. var directive = {
  7192. restrict: 'E',
  7193. replace: true,
  7194. templateUrl: 'views/components/umb-grid-selector.html',
  7195. scope: {
  7196. name: '=',
  7197. alias: '=',
  7198. selectedItems: '=',
  7199. availableItems: '=',
  7200. defaultItem: '=',
  7201. itemName: '@',
  7202. updatePlaceholder: '='
  7203. },
  7204. link: link
  7205. };
  7206. return directive;
  7207. }
  7208. angular.module('umbraco.directives').directive('umbGridSelector', GridSelector);
  7209. }());
  7210. (function () {
  7211. 'use strict';
  7212. function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService) {
  7213. function link(scope, el, attr, ctrl) {
  7214. var validationTranslated = '';
  7215. var tabNoSortOrderTranslated = '';
  7216. scope.sortingMode = false;
  7217. scope.toolbar = [];
  7218. scope.sortableOptionsGroup = {};
  7219. scope.sortableOptionsProperty = {};
  7220. scope.sortingButtonKey = 'general_reorder';
  7221. function activate() {
  7222. setSortingOptions();
  7223. // set placeholder property on each group
  7224. if (scope.model.groups.length !== 0) {
  7225. angular.forEach(scope.model.groups, function (group) {
  7226. addInitProperty(group);
  7227. });
  7228. }
  7229. // add init tab
  7230. addInitGroup(scope.model.groups);
  7231. activateFirstGroup(scope.model.groups);
  7232. // localize texts
  7233. localizationService.localize('validation_validation').then(function (value) {
  7234. validationTranslated = value;
  7235. });
  7236. localizationService.localize('contentTypeEditor_tabHasNoSortOrder').then(function (value) {
  7237. tabNoSortOrderTranslated = value;
  7238. });
  7239. }
  7240. function setSortingOptions() {
  7241. scope.sortableOptionsGroup = {
  7242. distance: 10,
  7243. tolerance: 'pointer',
  7244. opacity: 0.7,
  7245. scroll: true,
  7246. cursor: 'move',
  7247. placeholder: 'umb-group-builder__group-sortable-placeholder',
  7248. zIndex: 6000,
  7249. handle: '.umb-group-builder__group-handle',
  7250. items: '.umb-group-builder__group-sortable',
  7251. start: function (e, ui) {
  7252. ui.placeholder.height(ui.item.height());
  7253. },
  7254. stop: function (e, ui) {
  7255. updateTabsSortOrder();
  7256. }
  7257. };
  7258. scope.sortableOptionsProperty = {
  7259. distance: 10,
  7260. tolerance: 'pointer',
  7261. connectWith: '.umb-group-builder__properties',
  7262. opacity: 0.7,
  7263. scroll: true,
  7264. cursor: 'move',
  7265. placeholder: 'umb-group-builder__property_sortable-placeholder',
  7266. zIndex: 6000,
  7267. handle: '.umb-group-builder__property-handle',
  7268. items: '.umb-group-builder__property-sortable',
  7269. start: function (e, ui) {
  7270. ui.placeholder.height(ui.item.height());
  7271. },
  7272. stop: function (e, ui) {
  7273. updatePropertiesSortOrder();
  7274. }
  7275. };
  7276. }
  7277. function updateTabsSortOrder() {
  7278. var first = true;
  7279. var prevSortOrder = 0;
  7280. scope.model.groups.map(function (group) {
  7281. var index = scope.model.groups.indexOf(group);
  7282. if (group.tabState !== 'init') {
  7283. // set the first not inherited tab to sort order 0
  7284. if (!group.inherited && first) {
  7285. // set the first tab sort order to 0 if prev is 0
  7286. if (prevSortOrder === 0) {
  7287. group.sortOrder = 0; // when the first tab is inherited and sort order is not 0
  7288. } else {
  7289. group.sortOrder = prevSortOrder + 1;
  7290. }
  7291. first = false;
  7292. } else if (!group.inherited && !first) {
  7293. // find next group
  7294. var nextGroup = scope.model.groups[index + 1];
  7295. // if a groups is dropped in the middle of to groups with
  7296. // same sort order. Give it the dropped group same sort order
  7297. if (prevSortOrder === nextGroup.sortOrder) {
  7298. group.sortOrder = prevSortOrder;
  7299. } else {
  7300. group.sortOrder = prevSortOrder + 1;
  7301. }
  7302. }
  7303. // store this tabs sort order as reference for the next
  7304. prevSortOrder = group.sortOrder;
  7305. }
  7306. });
  7307. }
  7308. function filterAvailableCompositions(selectedContentType, selecting) {
  7309. //selecting = true if the user has check the item, false if the user has unchecked the item
  7310. var selectedContentTypeAliases = selecting ? //the user has selected the item so add to the current list
  7311. _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : //the user has unselected the item so remove from the current list
  7312. _.reject(scope.compositionsDialogModel.compositeContentTypes, function (i) {
  7313. return i === selectedContentType.alias;
  7314. });
  7315. //get the currently assigned property type aliases - ensure we pass these to the server side filer
  7316. var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function (g) {
  7317. return _.map(g.properties, function (p) {
  7318. return p.alias;
  7319. });
  7320. })), function (f) {
  7321. return f !== null && f !== undefined;
  7322. });
  7323. //use a different resource lookup depending on the content type type
  7324. var resourceLookup = scope.contentType === 'documentType' ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
  7325. return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) {
  7326. _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) {
  7327. //reset first
  7328. current.allowed = true;
  7329. //see if this list item is found in the response (allowed) list
  7330. var found = _.find(filteredAvailableCompositeTypes, function (f) {
  7331. return current.contentType.alias === f.contentType.alias;
  7332. });
  7333. //allow if the item was found in the response (allowed) list -
  7334. // and ensure its set to allowed if it is currently checked,
  7335. // DO not allow if it's a locked content type.
  7336. current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 && selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1 || (found !== null && found !== undefined ? found.allowed : false);
  7337. });
  7338. });
  7339. }
  7340. function updatePropertiesSortOrder() {
  7341. angular.forEach(scope.model.groups, function (group) {
  7342. if (group.tabState !== 'init') {
  7343. group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties);
  7344. }
  7345. });
  7346. }
  7347. function setupAvailableContentTypesModel(result) {
  7348. scope.compositionsDialogModel.availableCompositeContentTypes = result;
  7349. //iterate each one and set it up
  7350. _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) {
  7351. //enable it if it's part of the selected model
  7352. if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) {
  7353. c.allowed = true;
  7354. }
  7355. //set the inherited flags
  7356. c.inherited = false;
  7357. if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) {
  7358. c.inherited = true;
  7359. }
  7360. // convert icons for composite content types
  7361. iconHelper.formatContentTypeIcons([c.contentType]);
  7362. });
  7363. }
  7364. /* ---------- DELETE PROMT ---------- */
  7365. scope.togglePrompt = function (object) {
  7366. object.deletePrompt = !object.deletePrompt;
  7367. };
  7368. scope.hidePrompt = function (object) {
  7369. object.deletePrompt = false;
  7370. };
  7371. /* ---------- TOOLBAR ---------- */
  7372. scope.toggleSortingMode = function (tool) {
  7373. if (scope.sortingMode === true) {
  7374. var sortOrderMissing = false;
  7375. for (var i = 0; i < scope.model.groups.length; i++) {
  7376. var group = scope.model.groups[i];
  7377. if (group.tabState !== 'init' && group.sortOrder === undefined) {
  7378. sortOrderMissing = true;
  7379. group.showSortOrderMissing = true;
  7380. notificationsService.error(validationTranslated + ': ' + group.name + ' ' + tabNoSortOrderTranslated);
  7381. }
  7382. }
  7383. if (!sortOrderMissing) {
  7384. scope.sortingMode = false;
  7385. scope.sortingButtonKey = 'general_reorder';
  7386. }
  7387. } else {
  7388. scope.sortingMode = true;
  7389. scope.sortingButtonKey = 'general_reorderDone';
  7390. }
  7391. };
  7392. scope.openCompositionsDialog = function () {
  7393. scope.compositionsDialogModel = {
  7394. title: 'Compositions',
  7395. contentType: scope.model,
  7396. compositeContentTypes: scope.model.compositeContentTypes,
  7397. view: 'views/common/overlays/contenttypeeditor/compositions/compositions.html',
  7398. confirmSubmit: {
  7399. title: 'Warning',
  7400. description: 'Removing a composition will delete all the associated property data. Once you save the document type there\'s no way back, are you sure?',
  7401. checkboxLabel: 'I know what I\'m doing',
  7402. enable: true
  7403. },
  7404. submit: function (model, oldModel, confirmed) {
  7405. var compositionRemoved = false;
  7406. // check if any compositions has been removed
  7407. for (var i = 0; oldModel.compositeContentTypes.length > i; i++) {
  7408. var oldComposition = oldModel.compositeContentTypes[i];
  7409. if (_.contains(model.compositeContentTypes, oldComposition) === false) {
  7410. compositionRemoved = true;
  7411. }
  7412. }
  7413. // show overlay confirm box if compositions has been removed.
  7414. if (compositionRemoved && confirmed === false) {
  7415. scope.compositionsDialogModel.confirmSubmit.show = true; // submit overlay if no compositions has been removed
  7416. // or the action has been confirmed
  7417. } else {
  7418. // make sure that all tabs has an init property
  7419. if (scope.model.groups.length !== 0) {
  7420. angular.forEach(scope.model.groups, function (group) {
  7421. addInitProperty(group);
  7422. });
  7423. }
  7424. // remove overlay
  7425. scope.compositionsDialogModel.show = false;
  7426. scope.compositionsDialogModel = null;
  7427. }
  7428. },
  7429. close: function (oldModel) {
  7430. // reset composition changes
  7431. scope.model.groups = oldModel.contentType.groups;
  7432. scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes;
  7433. // remove overlay
  7434. scope.compositionsDialogModel.show = false;
  7435. scope.compositionsDialogModel = null;
  7436. },
  7437. selectCompositeContentType: function (selectedContentType) {
  7438. //first check if this is a new selection - we need to store this value here before any further digests/async
  7439. // because after that the scope.model.compositeContentTypes will be populated with the selected value.
  7440. var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1;
  7441. if (newSelection) {
  7442. //merge composition with content type
  7443. //use a different resource lookup depending on the content type type
  7444. var resourceLookup = scope.contentType === 'documentType' ? contentTypeResource.getById : mediaTypeResource.getById;
  7445. resourceLookup(selectedContentType.id).then(function (composition) {
  7446. //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and
  7447. // double check here.
  7448. var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition);
  7449. if (overlappingAliases.length > 0) {
  7450. //this will create an invalid composition, need to uncheck it
  7451. scope.compositionsDialogModel.compositeContentTypes.splice(scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1);
  7452. //dissallow this until something else is unchecked
  7453. selectedContentType.allowed = false;
  7454. } else {
  7455. contentTypeHelper.mergeCompositeContentType(scope.model, composition);
  7456. }
  7457. //based on the selection, we need to filter the available composite types list
  7458. filterAvailableCompositions(selectedContentType, newSelection).then(function () {
  7459. });
  7460. });
  7461. } else {
  7462. // split composition from content type
  7463. contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType);
  7464. //based on the selection, we need to filter the available composite types list
  7465. filterAvailableCompositions(selectedContentType, newSelection).then(function () {
  7466. });
  7467. }
  7468. }
  7469. };
  7470. var availableContentTypeResource = scope.contentType === 'documentType' ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
  7471. var countContentTypeResource = scope.contentType === 'documentType' ? contentTypeResource.getCount : mediaTypeResource.getCount;
  7472. //get the currently assigned property type aliases - ensure we pass these to the server side filer
  7473. var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function (g) {
  7474. return _.map(g.properties, function (p) {
  7475. return p.alias;
  7476. });
  7477. })), function (f) {
  7478. return f !== null && f !== undefined;
  7479. });
  7480. $q.all([
  7481. //get available composite types
  7482. availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) {
  7483. setupAvailableContentTypesModel(result);
  7484. }),
  7485. //get content type count
  7486. countContentTypeResource().then(function (result) {
  7487. scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10);
  7488. })
  7489. ]).then(function () {
  7490. //resolves when both other promises are done, now show it
  7491. scope.compositionsDialogModel.show = true;
  7492. });
  7493. };
  7494. /* ---------- GROUPS ---------- */
  7495. scope.addGroup = function (group) {
  7496. // set group sort order
  7497. var index = scope.model.groups.indexOf(group);
  7498. var prevGroup = scope.model.groups[index - 1];
  7499. if (index > 0) {
  7500. // set index to 1 higher than the previous groups sort order
  7501. group.sortOrder = prevGroup.sortOrder + 1;
  7502. } else {
  7503. // first group - sort order will be 0
  7504. group.sortOrder = 0;
  7505. }
  7506. // activate group
  7507. scope.activateGroup(group);
  7508. };
  7509. scope.activateGroup = function (selectedGroup) {
  7510. // set all other groups that are inactive to active
  7511. angular.forEach(scope.model.groups, function (group) {
  7512. // skip init tab
  7513. if (group.tabState !== 'init') {
  7514. group.tabState = 'inActive';
  7515. }
  7516. });
  7517. selectedGroup.tabState = 'active';
  7518. };
  7519. scope.removeGroup = function (groupIndex) {
  7520. scope.model.groups.splice(groupIndex, 1);
  7521. addInitGroup(scope.model.groups);
  7522. };
  7523. scope.updateGroupTitle = function (group) {
  7524. if (group.properties.length === 0) {
  7525. addInitProperty(group);
  7526. }
  7527. };
  7528. scope.changeSortOrderValue = function (group) {
  7529. if (group.sortOrder !== undefined) {
  7530. group.showSortOrderMissing = false;
  7531. }
  7532. scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder');
  7533. };
  7534. function addInitGroup(groups) {
  7535. // check i init tab already exists
  7536. var addGroup = true;
  7537. angular.forEach(groups, function (group) {
  7538. if (group.tabState === 'init') {
  7539. addGroup = false;
  7540. }
  7541. });
  7542. if (addGroup) {
  7543. groups.push({
  7544. properties: [],
  7545. parentTabContentTypes: [],
  7546. parentTabContentTypeNames: [],
  7547. name: '',
  7548. tabState: 'init'
  7549. });
  7550. }
  7551. return groups;
  7552. }
  7553. function activateFirstGroup(groups) {
  7554. if (groups && groups.length > 0) {
  7555. var firstGroup = groups[0];
  7556. if (!firstGroup.tabState || firstGroup.tabState === 'inActive') {
  7557. firstGroup.tabState = 'active';
  7558. }
  7559. }
  7560. }
  7561. /* ---------- PROPERTIES ---------- */
  7562. scope.addProperty = function (property, group) {
  7563. // set property sort order
  7564. var index = group.properties.indexOf(property);
  7565. var prevProperty = group.properties[index - 1];
  7566. if (index > 0) {
  7567. // set index to 1 higher than the previous property sort order
  7568. property.sortOrder = prevProperty.sortOrder + 1;
  7569. } else {
  7570. // first property - sort order will be 0
  7571. property.sortOrder = 0;
  7572. }
  7573. // open property settings dialog
  7574. scope.editPropertyTypeSettings(property, group);
  7575. };
  7576. scope.editPropertyTypeSettings = function (property, group) {
  7577. if (!property.inherited && !property.locked) {
  7578. scope.propertySettingsDialogModel = {};
  7579. scope.propertySettingsDialogModel.title = 'Property settings';
  7580. scope.propertySettingsDialogModel.property = property;
  7581. scope.propertySettingsDialogModel.contentType = scope.contentType;
  7582. scope.propertySettingsDialogModel.contentTypeName = scope.model.name;
  7583. scope.propertySettingsDialogModel.view = 'views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html';
  7584. scope.propertySettingsDialogModel.show = true;
  7585. // set state to active to access the preview
  7586. property.propertyState = 'active';
  7587. // set property states
  7588. property.dialogIsOpen = true;
  7589. scope.propertySettingsDialogModel.submit = function (model) {
  7590. property.inherited = false;
  7591. property.dialogIsOpen = false;
  7592. // update existing data types
  7593. if (model.updateSameDataTypes) {
  7594. updateSameDataTypes(property);
  7595. }
  7596. // remove dialog
  7597. scope.propertySettingsDialogModel.show = false;
  7598. scope.propertySettingsDialogModel = null;
  7599. // push new init property to group
  7600. addInitProperty(group);
  7601. // set focus on init property
  7602. var numberOfProperties = group.properties.length;
  7603. group.properties[numberOfProperties - 1].focus = true;
  7604. // push new init tab to the scope
  7605. addInitGroup(scope.model.groups);
  7606. };
  7607. scope.propertySettingsDialogModel.close = function (oldModel) {
  7608. // reset all property changes
  7609. property.label = oldModel.property.label;
  7610. property.alias = oldModel.property.alias;
  7611. property.description = oldModel.property.description;
  7612. property.config = oldModel.property.config;
  7613. property.editor = oldModel.property.editor;
  7614. property.view = oldModel.property.view;
  7615. property.dataTypeId = oldModel.property.dataTypeId;
  7616. property.dataTypeIcon = oldModel.property.dataTypeIcon;
  7617. property.dataTypeName = oldModel.property.dataTypeName;
  7618. property.validation.mandatory = oldModel.property.validation.mandatory;
  7619. property.validation.pattern = oldModel.property.validation.pattern;
  7620. property.showOnMemberProfile = oldModel.property.showOnMemberProfile;
  7621. property.memberCanEdit = oldModel.property.memberCanEdit;
  7622. // because we set state to active, to show a preview, we have to check if has been filled out
  7623. // label is required so if it is not filled we know it is a placeholder
  7624. if (oldModel.property.editor === undefined || oldModel.property.editor === null || oldModel.property.editor === '') {
  7625. property.propertyState = 'init';
  7626. } else {
  7627. property.propertyState = oldModel.property.propertyState;
  7628. }
  7629. // remove dialog
  7630. scope.propertySettingsDialogModel.show = false;
  7631. scope.propertySettingsDialogModel = null;
  7632. };
  7633. }
  7634. };
  7635. scope.deleteProperty = function (tab, propertyIndex) {
  7636. // remove property
  7637. tab.properties.splice(propertyIndex, 1);
  7638. // if the last property in group is an placeholder - remove add new tab placeholder
  7639. if (tab.properties.length === 1 && tab.properties[0].propertyState === 'init') {
  7640. angular.forEach(scope.model.groups, function (group, index, groups) {
  7641. if (group.tabState === 'init') {
  7642. groups.splice(index, 1);
  7643. }
  7644. });
  7645. }
  7646. };
  7647. function addInitProperty(group) {
  7648. var addInitPropertyBool = true;
  7649. var initProperty = {
  7650. label: null,
  7651. alias: null,
  7652. propertyState: 'init',
  7653. validation: {
  7654. mandatory: false,
  7655. pattern: null
  7656. }
  7657. };
  7658. // check if there already is an init property
  7659. angular.forEach(group.properties, function (property) {
  7660. if (property.propertyState === 'init') {
  7661. addInitPropertyBool = false;
  7662. }
  7663. });
  7664. if (addInitPropertyBool) {
  7665. group.properties.push(initProperty);
  7666. }
  7667. return group;
  7668. }
  7669. function updateSameDataTypes(newProperty) {
  7670. // find each property
  7671. angular.forEach(scope.model.groups, function (group) {
  7672. angular.forEach(group.properties, function (property) {
  7673. if (property.dataTypeId === newProperty.dataTypeId) {
  7674. // update property data
  7675. property.config = newProperty.config;
  7676. property.editor = newProperty.editor;
  7677. property.view = newProperty.view;
  7678. property.dataTypeId = newProperty.dataTypeId;
  7679. property.dataTypeIcon = newProperty.dataTypeIcon;
  7680. property.dataTypeName = newProperty.dataTypeName;
  7681. }
  7682. });
  7683. });
  7684. }
  7685. var unbindModelWatcher = scope.$watch('model', function (newValue, oldValue) {
  7686. if (newValue !== undefined && newValue.groups !== undefined) {
  7687. activate();
  7688. }
  7689. });
  7690. // clean up
  7691. scope.$on('$destroy', function () {
  7692. unbindModelWatcher();
  7693. });
  7694. }
  7695. var directive = {
  7696. restrict: 'E',
  7697. replace: true,
  7698. templateUrl: 'views/components/umb-groups-builder.html',
  7699. scope: {
  7700. model: '=',
  7701. compositions: '=',
  7702. sorting: '=',
  7703. contentType: '@'
  7704. },
  7705. link: link
  7706. };
  7707. return directive;
  7708. }
  7709. angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective);
  7710. }());
  7711. /**
  7712. @ngdoc directive
  7713. @name umbraco.directives.directive:umbkeyboardShortcutsOverview
  7714. @restrict E
  7715. @scope
  7716. @description
  7717. <p>Use this directive to show an overview of keyboard shortcuts in an editor.
  7718. The directive will render an overview trigger wich shows how the overview is opened.
  7719. When this combination is hit an overview is opened with shortcuts based on the model sent to the directive.</p>
  7720. <h3>Markup example</h3>
  7721. <pre>
  7722. <div ng-controller="My.Controller as vm">
  7723. <umb-keyboard-shortcuts-overview
  7724. model="vm.keyboardShortcutsOverview">
  7725. </umb-keyboard-shortcuts-overview>
  7726. </div>
  7727. </pre>
  7728. <h3>Controller example</h3>
  7729. <pre>
  7730. (function () {
  7731. "use strict";
  7732. function Controller() {
  7733. var vm = this;
  7734. vm.keyboardShortcutsOverview = [
  7735. {
  7736. "name": "Sections",
  7737. "shortcuts": [
  7738. {
  7739. "description": "Navigate sections",
  7740. "keys": [
  7741. {"key": "1"},
  7742. {"key": "4"}
  7743. ],
  7744. "keyRange": true
  7745. }
  7746. ]
  7747. },
  7748. {
  7749. "name": "Design",
  7750. "shortcuts": [
  7751. {
  7752. "description": "Add tab",
  7753. "keys": [
  7754. {"key": "alt"},
  7755. {"key": "shift"},
  7756. {"key": "t"}
  7757. ]
  7758. }
  7759. ]
  7760. }
  7761. ];
  7762. }
  7763. angular.module("umbraco").controller("My.Controller", Controller);
  7764. })();
  7765. </pre>
  7766. <h3>Model description</h3>
  7767. <ul>
  7768. <li>
  7769. <strong>name</strong>
  7770. <small>(string)</small> -
  7771. Sets the shortcut section name.
  7772. </li>
  7773. <li>
  7774. <strong>shortcuts</strong>
  7775. <small>(array)</small> -
  7776. Array of available shortcuts in the section.
  7777. </li>
  7778. <ul>
  7779. <li>
  7780. <strong>description</strong>
  7781. <small>(string)</small> -
  7782. Short description of the shortcut.
  7783. </li>
  7784. <li>
  7785. <strong>keys</strong>
  7786. <small>(array)</small> -
  7787. Array of keys in the shortcut.
  7788. </li>
  7789. <ul>
  7790. <li>
  7791. <strong>key</strong>
  7792. <small>(string)</small> -
  7793. The invidual key in the shortcut.
  7794. </li>
  7795. </ul>
  7796. <li>
  7797. <strong>keyRange</strong>
  7798. <small>(boolean)</small> -
  7799. Set to <code>true</code> to show a key range. It combines the shortcut keys with "-" instead of "+".
  7800. </li>
  7801. </ul>
  7802. </ul>
  7803. @param {object} model keyboard shortcut model. See description and example above.
  7804. **/
  7805. (function () {
  7806. 'use strict';
  7807. function KeyboardShortcutsOverviewDirective(platformService) {
  7808. function link(scope, el, attr, ctrl) {
  7809. var eventBindings = [];
  7810. var isMac = platformService.isMac();
  7811. scope.toggleShortcutsOverlay = function () {
  7812. scope.showOverlay = !scope.showOverlay;
  7813. scope.onToggle();
  7814. };
  7815. function onInit() {
  7816. angular.forEach(scope.model, function (shortcutGroup) {
  7817. angular.forEach(shortcutGroup.shortcuts, function (shortcut) {
  7818. shortcut.platformKeys = [];
  7819. // get shortcut keys for mac
  7820. if (isMac && shortcut.keys && shortcut.keys.mac) {
  7821. shortcut.platformKeys = shortcut.keys.mac; // get shortcut keys for windows
  7822. } else if (!isMac && shortcut.keys && shortcut.keys.win) {
  7823. shortcut.platformKeys = shortcut.keys.win; // get default shortcut keys
  7824. } else if (shortcut.keys && shortcut && shortcut.keys.length > 0) {
  7825. shortcut.platformKeys = shortcut.keys;
  7826. }
  7827. });
  7828. });
  7829. }
  7830. onInit();
  7831. eventBindings.push(scope.$watch('model', function (newValue, oldValue) {
  7832. if (newValue !== oldValue) {
  7833. onInit();
  7834. }
  7835. }));
  7836. // clean up
  7837. scope.$on('$destroy', function () {
  7838. // unbind watchers
  7839. for (var e in eventBindings) {
  7840. eventBindings[e]();
  7841. }
  7842. });
  7843. }
  7844. var directive = {
  7845. restrict: 'E',
  7846. replace: true,
  7847. templateUrl: 'views/components/umb-keyboard-shortcuts-overview.html',
  7848. link: link,
  7849. scope: {
  7850. model: '=',
  7851. onToggle: '&',
  7852. showOverlay: '=?'
  7853. }
  7854. };
  7855. return directive;
  7856. }
  7857. angular.module('umbraco.directives').directive('umbKeyboardShortcutsOverview', KeyboardShortcutsOverviewDirective);
  7858. }());
  7859. /**
  7860. * @ngdoc directive
  7861. * @name umbraco.directives.directive:umbLaunchMiniEditor
  7862. * @restrict E
  7863. * @function
  7864. * @description
  7865. * Used on a button to launch a mini content editor editor dialog
  7866. **/
  7867. angular.module('umbraco.directives').directive('umbLaunchMiniEditor', function (miniEditorHelper) {
  7868. return {
  7869. restrict: 'A',
  7870. replace: false,
  7871. scope: { node: '=umbLaunchMiniEditor' },
  7872. link: function (scope, element, attrs) {
  7873. element.click(function () {
  7874. miniEditorHelper.launchMiniEditor(scope.node);
  7875. });
  7876. }
  7877. };
  7878. });
  7879. (function () {
  7880. 'use strict';
  7881. function LayoutSelectorDirective() {
  7882. function link(scope, el, attr, ctrl) {
  7883. scope.layoutDropDownIsOpen = false;
  7884. scope.showLayoutSelector = true;
  7885. function activate() {
  7886. setVisibility();
  7887. setActiveLayout(scope.layouts);
  7888. }
  7889. function setVisibility() {
  7890. var numberOfAllowedLayouts = getNumberOfAllowedLayouts(scope.layouts);
  7891. if (numberOfAllowedLayouts === 1) {
  7892. scope.showLayoutSelector = false;
  7893. }
  7894. }
  7895. function getNumberOfAllowedLayouts(layouts) {
  7896. var allowedLayouts = 0;
  7897. for (var i = 0; layouts.length > i; i++) {
  7898. var layout = layouts[i];
  7899. if (layout.selected === true) {
  7900. allowedLayouts++;
  7901. }
  7902. }
  7903. return allowedLayouts;
  7904. }
  7905. function setActiveLayout(layouts) {
  7906. for (var i = 0; layouts.length > i; i++) {
  7907. var layout = layouts[i];
  7908. if (layout.path === scope.activeLayout.path) {
  7909. layout.active = true;
  7910. }
  7911. }
  7912. }
  7913. scope.pickLayout = function (selectedLayout) {
  7914. if (scope.onLayoutSelect) {
  7915. scope.onLayoutSelect(selectedLayout);
  7916. scope.layoutDropDownIsOpen = false;
  7917. }
  7918. };
  7919. scope.toggleLayoutDropdown = function () {
  7920. scope.layoutDropDownIsOpen = !scope.layoutDropDownIsOpen;
  7921. };
  7922. scope.closeLayoutDropdown = function () {
  7923. scope.layoutDropDownIsOpen = false;
  7924. };
  7925. activate();
  7926. }
  7927. var directive = {
  7928. restrict: 'E',
  7929. replace: true,
  7930. templateUrl: 'views/components/umb-layout-selector.html',
  7931. scope: {
  7932. layouts: '=',
  7933. activeLayout: '=',
  7934. onLayoutSelect: '='
  7935. },
  7936. link: link
  7937. };
  7938. return directive;
  7939. }
  7940. angular.module('umbraco.directives').directive('umbLayoutSelector', LayoutSelectorDirective);
  7941. }());
  7942. /**
  7943. @ngdoc directive
  7944. @name umbraco.directives.directive:umbLightbox
  7945. @restrict E
  7946. @scope
  7947. @description
  7948. <p>Use this directive to open a gallery in a lightbox overlay.</p>
  7949. <h3>Markup example</h3>
  7950. <pre>
  7951. <div ng-controller="My.Controller as vm">
  7952. <div class="my-gallery">
  7953. <a href="" ng-repeat="image in images" ng-click="vm.openLightbox($index, images)">
  7954. <img ng-src="image.source" />
  7955. </a>
  7956. </div>
  7957. <umb-lightbox
  7958. ng-if="vm.lightbox.show"
  7959. items="vm.lightbox.items"
  7960. active-item-index="vm.lightbox.activeIndex"
  7961. on-close="vm.closeLightbox">
  7962. </umb-lightbox>
  7963. </div>
  7964. </pre>
  7965. <h3>Controller example</h3>
  7966. <pre>
  7967. (function () {
  7968. "use strict";
  7969. function Controller() {
  7970. var vm = this;
  7971. vm.images = [
  7972. {
  7973. "source": "linkToImage"
  7974. },
  7975. {
  7976. "source": "linkToImage"
  7977. }
  7978. ]
  7979. vm.openLightbox = openLightbox;
  7980. vm.closeLightbox = closeLightbox;
  7981. function openLightbox(itemIndex, items) {
  7982. vm.lightbox = {
  7983. show: true,
  7984. items: items,
  7985. activeIndex: itemIndex
  7986. };
  7987. }
  7988. function closeLightbox() {
  7989. vm.lightbox.show = false;
  7990. vm.lightbox = null;
  7991. }
  7992. }
  7993. angular.module("umbraco").controller("My.Controller", Controller);
  7994. })();
  7995. </pre>
  7996. @param {array} items Array of gallery items.
  7997. @param {callback} onClose Callback when the lightbox is closed.
  7998. @param {number} activeItemIndex Index of active item.
  7999. **/
  8000. (function () {
  8001. 'use strict';
  8002. function LightboxDirective() {
  8003. function link(scope, el, attr, ctrl) {
  8004. function activate() {
  8005. var eventBindings = [];
  8006. el.appendTo('body');
  8007. // clean up
  8008. scope.$on('$destroy', function () {
  8009. // unbind watchers
  8010. for (var e in eventBindings) {
  8011. eventBindings[e]();
  8012. }
  8013. });
  8014. }
  8015. scope.next = function () {
  8016. var nextItemIndex = scope.activeItemIndex + 1;
  8017. if (nextItemIndex < scope.items.length) {
  8018. scope.items[scope.activeItemIndex].active = false;
  8019. scope.items[nextItemIndex].active = true;
  8020. scope.activeItemIndex = nextItemIndex;
  8021. }
  8022. };
  8023. scope.prev = function () {
  8024. var prevItemIndex = scope.activeItemIndex - 1;
  8025. if (prevItemIndex >= 0) {
  8026. scope.items[scope.activeItemIndex].active = false;
  8027. scope.items[prevItemIndex].active = true;
  8028. scope.activeItemIndex = prevItemIndex;
  8029. }
  8030. };
  8031. scope.close = function () {
  8032. if (scope.onClose) {
  8033. scope.onClose();
  8034. }
  8035. };
  8036. activate();
  8037. }
  8038. var directive = {
  8039. restrict: 'E',
  8040. replace: true,
  8041. templateUrl: 'views/components/umb-lightbox.html',
  8042. scope: {
  8043. items: '=',
  8044. onClose: '=',
  8045. activeItemIndex: '='
  8046. },
  8047. link: link
  8048. };
  8049. return directive;
  8050. }
  8051. angular.module('umbraco.directives').directive('umbLightbox', LightboxDirective);
  8052. }());
  8053. (function () {
  8054. 'use strict';
  8055. function ListViewLayoutDirective() {
  8056. function link(scope, el, attr, ctrl) {
  8057. scope.getContent = function (contentId) {
  8058. if (scope.onGetContent) {
  8059. scope.onGetContent(contentId);
  8060. }
  8061. };
  8062. }
  8063. var directive = {
  8064. restrict: 'E',
  8065. replace: true,
  8066. templateUrl: 'views/components/umb-list-view-layout.html',
  8067. scope: {
  8068. contentId: '=',
  8069. folders: '=',
  8070. items: '=',
  8071. selection: '=',
  8072. options: '=',
  8073. entityType: '@',
  8074. onGetContent: '='
  8075. },
  8076. link: link
  8077. };
  8078. return directive;
  8079. }
  8080. angular.module('umbraco.directives').directive('umbListViewLayout', ListViewLayoutDirective);
  8081. }());
  8082. (function () {
  8083. 'use strict';
  8084. function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper, listViewPrevalueHelper) {
  8085. function link(scope, el, attr, ctrl) {
  8086. scope.dataType = {};
  8087. scope.editDataTypeSettings = false;
  8088. scope.customListViewCreated = false;
  8089. /* ---------- INIT ---------- */
  8090. function activate() {
  8091. if (scope.enableListView) {
  8092. dataTypeResource.getByName(scope.listViewName).then(function (dataType) {
  8093. scope.dataType = dataType;
  8094. listViewPrevalueHelper.setPrevalues(dataType.preValues);
  8095. scope.customListViewCreated = checkForCustomListView();
  8096. });
  8097. } else {
  8098. scope.dataType = {};
  8099. }
  8100. }
  8101. /* ----------- LIST VIEW SETTINGS --------- */
  8102. scope.toggleEditListViewDataTypeSettings = function () {
  8103. scope.editDataTypeSettings = !scope.editDataTypeSettings;
  8104. };
  8105. scope.saveListViewDataType = function () {
  8106. var preValues = dataTypeHelper.createPreValueProps(scope.dataType.preValues);
  8107. dataTypeResource.save(scope.dataType, preValues, false).then(function (dataType) {
  8108. // store data type
  8109. scope.dataType = dataType;
  8110. // hide settings panel
  8111. scope.editDataTypeSettings = false;
  8112. });
  8113. };
  8114. /* ---------- CUSTOM LIST VIEW ---------- */
  8115. scope.createCustomListViewDataType = function () {
  8116. dataTypeResource.createCustomListView(scope.modelAlias).then(function (dataType) {
  8117. // store data type
  8118. scope.dataType = dataType;
  8119. // set list view name on scope
  8120. scope.listViewName = dataType.name;
  8121. // change state to custom list view
  8122. scope.customListViewCreated = true;
  8123. // show settings panel
  8124. scope.editDataTypeSettings = true;
  8125. });
  8126. };
  8127. scope.removeCustomListDataType = function () {
  8128. scope.editDataTypeSettings = false;
  8129. // delete custom list view data type
  8130. dataTypeResource.deleteById(scope.dataType.id).then(function (dataType) {
  8131. // set list view name on scope
  8132. if (scope.contentType === 'documentType') {
  8133. scope.listViewName = 'List View - Content';
  8134. } else if (scope.contentType === 'mediaType') {
  8135. scope.listViewName = 'List View - Media';
  8136. }
  8137. // get default data type
  8138. dataTypeResource.getByName(scope.listViewName).then(function (dataType) {
  8139. // store data type
  8140. scope.dataType = dataType;
  8141. // change state to default list view
  8142. scope.customListViewCreated = false;
  8143. });
  8144. });
  8145. };
  8146. /* ----------- SCOPE WATCHERS ----------- */
  8147. var unbindEnableListViewWatcher = scope.$watch('enableListView', function (newValue, oldValue) {
  8148. if (newValue !== undefined) {
  8149. activate();
  8150. }
  8151. });
  8152. // clean up
  8153. scope.$on('$destroy', function () {
  8154. unbindEnableListViewWatcher();
  8155. });
  8156. /* ----------- METHODS ---------- */
  8157. function checkForCustomListView() {
  8158. return scope.dataType.name === 'List View - ' + scope.modelAlias;
  8159. }
  8160. }
  8161. var directive = {
  8162. restrict: 'E',
  8163. replace: true,
  8164. templateUrl: 'views/components/umb-list-view-settings.html',
  8165. scope: {
  8166. enableListView: '=',
  8167. listViewName: '=',
  8168. modelAlias: '=',
  8169. contentType: '@'
  8170. },
  8171. link: link
  8172. };
  8173. return directive;
  8174. }
  8175. angular.module('umbraco.directives').directive('umbListViewSettings', ListViewSettingsDirective);
  8176. }());
  8177. /**
  8178. @ngdoc directive
  8179. @name umbraco.directives.directive:umbLoadIndicator
  8180. @restrict E
  8181. @description
  8182. Use this directive to generate a loading indicator.
  8183. <h3>Markup example</h3>
  8184. <pre>
  8185. <div ng-controller="My.Controller as vm">
  8186. <umb-load-indicator
  8187. ng-if="vm.loading">
  8188. </umb-load-indicator>
  8189. <div class="content" ng-if="!vm.loading">
  8190. <p>{{content}}</p>
  8191. </div>
  8192. </div>
  8193. </pre>
  8194. <h3>Controller example</h3>
  8195. <pre>
  8196. (function () {
  8197. "use strict";
  8198. function Controller(myService) {
  8199. var vm = this;
  8200. vm.content = "";
  8201. vm.loading = true;
  8202. myService.getContent().then(function(content){
  8203. vm.content = content;
  8204. vm.loading = false;
  8205. });
  8206. }
  8207. ½
  8208. angular.module("umbraco").controller("My.Controller", Controller);
  8209. })();
  8210. </pre>
  8211. **/
  8212. (function () {
  8213. 'use strict';
  8214. function UmbLoadIndicatorDirective() {
  8215. var directive = {
  8216. restrict: 'E',
  8217. replace: true,
  8218. templateUrl: 'views/components/umb-load-indicator.html'
  8219. };
  8220. return directive;
  8221. }
  8222. angular.module('umbraco.directives').directive('umbLoadIndicator', UmbLoadIndicatorDirective);
  8223. }());
  8224. /**
  8225. @ngdoc directive
  8226. @name umbraco.directives.directive:umbLockedField
  8227. @restrict E
  8228. @scope
  8229. @description
  8230. Use this directive to render a value with a lock next to it. When the lock is clicked the value gets unlocked and can be edited.
  8231. <h3>Markup example</h3>
  8232. <pre>
  8233. <div ng-controller="My.Controller as vm">
  8234. <umb-locked-field
  8235. ng-model="vm.value"
  8236. placeholder-text="'Click to unlock...'">
  8237. </umb-locked-field>
  8238. </div>
  8239. </pre>
  8240. <h3>Controller example</h3>
  8241. <pre>
  8242. (function () {
  8243. "use strict";
  8244. function Controller() {
  8245. var vm = this;
  8246. vm.value = "My locked text";
  8247. }
  8248. angular.module("umbraco").controller("My.Controller", Controller);
  8249. })();
  8250. </pre>
  8251. @param {string} ngModel (<code>binding</code>): The locked text.
  8252. @param {boolean=} locked (<code>binding</code>): <Code>true</code> by default. Set to <code>false</code> to unlock the text.
  8253. @param {string=} placeholderText (<code>binding</code>): If ngModel is empty this text will be shown.
  8254. @param {string=} regexValidation (<code>binding</code>): Set a regex expression for validation of the field.
  8255. @param {string=} serverValidationField (<code>attribute</code>): Set a server validation field.
  8256. **/
  8257. (function () {
  8258. 'use strict';
  8259. function LockedFieldDirective($timeout, localizationService) {
  8260. function link(scope, el, attr, ngModelCtrl) {
  8261. function activate() {
  8262. // if locked state is not defined as an attr set default state
  8263. if (scope.locked === undefined || scope.locked === null) {
  8264. scope.locked = true;
  8265. }
  8266. // if regex validation is not defined as an attr set default state
  8267. // if this is set to an empty string then regex validation can be ignored.
  8268. if (scope.regexValidation === undefined || scope.regexValidation === null) {
  8269. scope.regexValidation = '^[a-zA-Z]\\w.*$';
  8270. }
  8271. if (scope.serverValidationField === undefined || scope.serverValidationField === null) {
  8272. scope.serverValidationField = '';
  8273. }
  8274. // if locked state is not defined as an attr set default state
  8275. if (scope.placeholderText === undefined || scope.placeholderText === null) {
  8276. scope.placeholderText = 'Enter value...';
  8277. }
  8278. }
  8279. scope.lock = function () {
  8280. scope.locked = true;
  8281. };
  8282. scope.unlock = function () {
  8283. scope.locked = false;
  8284. };
  8285. activate();
  8286. }
  8287. var directive = {
  8288. require: 'ngModel',
  8289. restrict: 'E',
  8290. replace: true,
  8291. templateUrl: 'views/components/umb-locked-field.html',
  8292. scope: {
  8293. ngModel: '=',
  8294. locked: '=?',
  8295. placeholderText: '=?',
  8296. regexValidation: '=?',
  8297. serverValidationField: '@'
  8298. },
  8299. link: link
  8300. };
  8301. return directive;
  8302. }
  8303. angular.module('umbraco.directives').directive('umbLockedField', LockedFieldDirective);
  8304. }());
  8305. /**
  8306. @ngdoc directive
  8307. @name umbraco.directives.directive:umbMediaGrid
  8308. @restrict E
  8309. @scope
  8310. @description
  8311. Use this directive to generate a thumbnail grid of media items.
  8312. <h3>Markup example</h3>
  8313. <pre>
  8314. <div ng-controller="My.Controller as vm">
  8315. <umb-media-grid
  8316. items="vm.mediaItems"
  8317. on-click="vm.clickItem"
  8318. on-click-name="vm.clickItemName">
  8319. </umb-media-grid>
  8320. </div>
  8321. </pre>
  8322. <h3>Controller example</h3>
  8323. <pre>
  8324. (function () {
  8325. "use strict";
  8326. function Controller() {
  8327. var vm = this;
  8328. vm.mediaItems = [];
  8329. vm.clickItem = clickItem;
  8330. vm.clickItemName = clickItemName;
  8331. myService.getMediaItems().then(function (mediaItems) {
  8332. vm.mediaItems = mediaItems;
  8333. });
  8334. function clickItem(item, $event, $index){
  8335. // do magic here
  8336. }
  8337. function clickItemName(item, $event, $index) {
  8338. // set item.selected = true; to select the item
  8339. // do magic here
  8340. }
  8341. }
  8342. angular.module("umbraco").controller("My.Controller", Controller);
  8343. })();
  8344. </pre>
  8345. @param {array} items (<code>binding</code>): Array of media items.
  8346. @param {callback=} onDetailsHover (<code>binding</code>): Callback method when the details icon is hovered.
  8347. <h3>The callback returns:</h3>
  8348. <ul>
  8349. <li><code>item</code>: The hovered item</li>
  8350. <li><code>$event</code>: The hover event</li>
  8351. <li><code>hover</code>: Boolean to tell if the item is hovered or not</li>
  8352. </ul>
  8353. @param {callback=} onClick (<code>binding</code>): Callback method to handle click events on the media item.
  8354. <h3>The callback returns:</h3>
  8355. <ul>
  8356. <li><code>item</code>: The clicked item</li>
  8357. <li><code>$event</code>: The click event</li>
  8358. <li><code>$index</code>: The item index</li>
  8359. </ul>
  8360. @param {callback=} onClickName (<code>binding</code>): Callback method to handle click events on the media item name.
  8361. <h3>The callback returns:</h3>
  8362. <ul>
  8363. <li><code>item</code>: The clicked item</li>
  8364. <li><code>$event</code>: The click event</li>
  8365. <li><code>$index</code>: The item index</li>
  8366. </ul>
  8367. @param {string=} filterBy (<code>binding</code>): String to filter media items by
  8368. @param {string=} itemMaxWidth (<code>attribute</code>): Sets a max width on the media item thumbnails.
  8369. @param {string=} itemMaxHeight (<code>attribute</code>): Sets a max height on the media item thumbnails.
  8370. @param {string=} itemMinWidth (<code>attribute</code>): Sets a min width on the media item thumbnails.
  8371. @param {string=} itemMinHeight (<code>attribute</code>): Sets a min height on the media item thumbnails.
  8372. **/
  8373. (function () {
  8374. 'use strict';
  8375. function MediaGridDirective($filter, mediaHelper) {
  8376. function link(scope, el, attr, ctrl) {
  8377. var itemDefaultHeight = 200;
  8378. var itemDefaultWidth = 200;
  8379. var itemMaxWidth = 200;
  8380. var itemMaxHeight = 200;
  8381. var itemMinWidth = 125;
  8382. var itemMinHeight = 125;
  8383. function activate() {
  8384. if (scope.itemMaxWidth) {
  8385. itemMaxWidth = scope.itemMaxWidth;
  8386. }
  8387. if (scope.itemMaxHeight) {
  8388. itemMaxHeight = scope.itemMaxHeight;
  8389. }
  8390. if (scope.itemMinWidth) {
  8391. itemMinWidth = scope.itemMinWidth;
  8392. }
  8393. if (scope.itemMinHeight) {
  8394. itemMinHeight = scope.itemMinHeight;
  8395. }
  8396. for (var i = 0; scope.items.length > i; i++) {
  8397. var item = scope.items[i];
  8398. setItemData(item);
  8399. setOriginalSize(item, itemMaxHeight);
  8400. // remove non images when onlyImages is set to true
  8401. if (scope.onlyImages === 'true' && !item.isFolder && !item.thumbnail) {
  8402. scope.items.splice(i, 1);
  8403. i--;
  8404. }
  8405. }
  8406. if (scope.items.length > 0) {
  8407. setFlexValues(scope.items);
  8408. }
  8409. }
  8410. function setItemData(item) {
  8411. // check if item is a folder
  8412. if (item.image) {
  8413. // if is has an image path, it is not a folder
  8414. item.isFolder = false;
  8415. } else {
  8416. item.isFolder = !mediaHelper.hasFilePropertyType(item);
  8417. }
  8418. if (!item.isFolder) {
  8419. // handle entity
  8420. if (item.image) {
  8421. item.thumbnail = mediaHelper.resolveFileFromEntity(item, true);
  8422. item.extension = mediaHelper.getFileExtension(item.image); // handle full media object
  8423. } else {
  8424. item.thumbnail = mediaHelper.resolveFile(item, true);
  8425. item.image = mediaHelper.resolveFile(item, false);
  8426. var fileProp = _.find(item.properties, function (v) {
  8427. return v.alias === 'umbracoFile';
  8428. });
  8429. if (fileProp && fileProp.value) {
  8430. item.file = fileProp.value;
  8431. }
  8432. var extensionProp = _.find(item.properties, function (v) {
  8433. return v.alias === 'umbracoExtension';
  8434. });
  8435. if (extensionProp && extensionProp.value) {
  8436. item.extension = extensionProp.value;
  8437. }
  8438. }
  8439. }
  8440. }
  8441. function setOriginalSize(item, maxHeight) {
  8442. //set to a square by default
  8443. item.width = itemDefaultWidth;
  8444. item.height = itemDefaultHeight;
  8445. item.aspectRatio = 1;
  8446. var widthProp = _.find(item.properties, function (v) {
  8447. return v.alias === 'umbracoWidth';
  8448. });
  8449. if (widthProp && widthProp.value) {
  8450. item.width = parseInt(widthProp.value, 10);
  8451. if (isNaN(item.width)) {
  8452. item.width = itemDefaultWidth;
  8453. }
  8454. }
  8455. var heightProp = _.find(item.properties, function (v) {
  8456. return v.alias === 'umbracoHeight';
  8457. });
  8458. if (heightProp && heightProp.value) {
  8459. item.height = parseInt(heightProp.value, 10);
  8460. if (isNaN(item.height)) {
  8461. item.height = itemDefaultWidth;
  8462. }
  8463. }
  8464. item.aspectRatio = item.width / item.height;
  8465. // set max width and height
  8466. // landscape
  8467. if (item.aspectRatio >= 1) {
  8468. if (item.width > itemMaxWidth) {
  8469. item.width = itemMaxWidth;
  8470. item.height = itemMaxWidth / item.aspectRatio;
  8471. } // portrait
  8472. } else {
  8473. if (item.height > itemMaxHeight) {
  8474. item.height = itemMaxHeight;
  8475. item.width = itemMaxHeight * item.aspectRatio;
  8476. }
  8477. }
  8478. }
  8479. function setFlexValues(mediaItems) {
  8480. var flexSortArray = mediaItems;
  8481. var smallestImageWidth = null;
  8482. var widestImageAspectRatio = null;
  8483. // sort array after image width with the widest image first
  8484. flexSortArray = $filter('orderBy')(flexSortArray, 'width', true);
  8485. // find widest image aspect ratio
  8486. widestImageAspectRatio = flexSortArray[0].aspectRatio;
  8487. // find smallest image width
  8488. smallestImageWidth = flexSortArray[flexSortArray.length - 1].width;
  8489. for (var i = 0; flexSortArray.length > i; i++) {
  8490. var mediaItem = flexSortArray[i];
  8491. var flex = 1 / (widestImageAspectRatio / mediaItem.aspectRatio);
  8492. if (flex === 0) {
  8493. flex = 1;
  8494. }
  8495. var imageMinFlexWidth = smallestImageWidth * flex;
  8496. var flexStyle = {
  8497. 'flex': flex + ' 1 ' + imageMinFlexWidth + 'px',
  8498. 'max-width': mediaItem.width + 'px',
  8499. 'min-width': itemMinWidth + 'px',
  8500. 'min-height': itemMinHeight + 'px'
  8501. };
  8502. mediaItem.flexStyle = flexStyle;
  8503. }
  8504. }
  8505. scope.clickItem = function (item, $event, $index) {
  8506. if (scope.onClick) {
  8507. scope.onClick(item, $event, $index);
  8508. $event.stopPropagation();
  8509. }
  8510. };
  8511. scope.clickItemName = function (item, $event, $index) {
  8512. if (scope.onClickName) {
  8513. scope.onClickName(item, $event, $index);
  8514. $event.stopPropagation();
  8515. }
  8516. };
  8517. scope.hoverItemDetails = function (item, $event, hover) {
  8518. if (scope.onDetailsHover) {
  8519. scope.onDetailsHover(item, $event, hover);
  8520. }
  8521. };
  8522. var unbindItemsWatcher = scope.$watch('items', function (newValue, oldValue) {
  8523. if (angular.isArray(newValue)) {
  8524. activate();
  8525. }
  8526. });
  8527. scope.$on('$destroy', function () {
  8528. unbindItemsWatcher();
  8529. });
  8530. }
  8531. var directive = {
  8532. restrict: 'E',
  8533. replace: true,
  8534. templateUrl: 'views/components/umb-media-grid.html',
  8535. scope: {
  8536. items: '=',
  8537. onDetailsHover: '=',
  8538. onClick: '=',
  8539. onClickName: '=',
  8540. filterBy: '=',
  8541. itemMaxWidth: '@',
  8542. itemMaxHeight: '@',
  8543. itemMinWidth: '@',
  8544. itemMinHeight: '@',
  8545. onlyImages: '@'
  8546. },
  8547. link: link
  8548. };
  8549. return directive;
  8550. }
  8551. angular.module('umbraco.directives').directive('umbMediaGrid', MediaGridDirective);
  8552. }());
  8553. (function () {
  8554. 'use strict';
  8555. function MiniListViewDirective(entityResource, iconHelper) {
  8556. function link(scope, el, attr, ctrl) {
  8557. scope.search = '';
  8558. scope.miniListViews = [];
  8559. scope.breadcrumb = [];
  8560. var miniListViewsHistory = [];
  8561. var goingForward = true;
  8562. var skipAnimation = true;
  8563. function onInit() {
  8564. open(scope.node);
  8565. }
  8566. function open(node) {
  8567. // convert legacy icon for node
  8568. if (node && node.icon) {
  8569. node.icon = iconHelper.convertFromLegacyIcon(node.icon);
  8570. }
  8571. goingForward = true;
  8572. var miniListView = {
  8573. node: node,
  8574. loading: true,
  8575. pagination: {
  8576. pageSize: 10,
  8577. pageNumber: 1,
  8578. filter: '',
  8579. orderDirection: 'Ascending',
  8580. orderBy: 'SortOrder',
  8581. orderBySystemField: true
  8582. }
  8583. };
  8584. // clear and push mini list view in dom so we only render 1 view
  8585. scope.miniListViews = [];
  8586. scope.miniListViews.push(miniListView);
  8587. // store in history so we quickly can navigate back
  8588. miniListViewsHistory.push(miniListView);
  8589. // get children
  8590. getChildrenForMiniListView(miniListView);
  8591. makeBreadcrumb();
  8592. }
  8593. function getChildrenForMiniListView(miniListView) {
  8594. // start loading animation list view
  8595. miniListView.loading = true;
  8596. entityResource.getPagedChildren(miniListView.node.id, scope.entityType, miniListView.pagination).then(function (data) {
  8597. // update children
  8598. miniListView.children = data.items;
  8599. _.each(miniListView.children, function (c) {
  8600. // convert legacy icon for node
  8601. if (c.icon) {
  8602. c.icon = iconHelper.convertFromLegacyIcon(c.icon);
  8603. }
  8604. // set published state for content
  8605. if (c.metaData) {
  8606. c.hasChildren = c.metaData.HasChildren;
  8607. if (scope.entityType === 'Document') {
  8608. c.published = c.metaData.IsPublished;
  8609. }
  8610. }
  8611. });
  8612. // update pagination
  8613. miniListView.pagination.totalItems = data.totalItems;
  8614. miniListView.pagination.totalPages = data.totalPages;
  8615. // stop load indicator
  8616. miniListView.loading = false;
  8617. });
  8618. }
  8619. scope.openNode = function (event, node) {
  8620. open(node);
  8621. event.stopPropagation();
  8622. };
  8623. scope.selectNode = function (node) {
  8624. if (scope.onSelect) {
  8625. scope.onSelect({ 'node': node });
  8626. }
  8627. };
  8628. /* Pagination */
  8629. scope.goToPage = function (pageNumber, miniListView) {
  8630. // set new page number
  8631. miniListView.pagination.pageNumber = pageNumber;
  8632. // get children
  8633. getChildrenForMiniListView(miniListView);
  8634. };
  8635. /* Breadcrumb */
  8636. scope.clickBreadcrumb = function (ancestor) {
  8637. var found = false;
  8638. goingForward = false;
  8639. angular.forEach(miniListViewsHistory, function (historyItem, index) {
  8640. // We need to make sure we can compare the two id's.
  8641. // Some id's are integers and others are strings.
  8642. // Members have string ids like "all-members".
  8643. if (historyItem.node.id.toString() === ancestor.id.toString()) {
  8644. // load the list view from history
  8645. scope.miniListViews = [];
  8646. scope.miniListViews.push(historyItem);
  8647. // clean up history - remove all children after
  8648. miniListViewsHistory.splice(index + 1, miniListViewsHistory.length);
  8649. found = true;
  8650. }
  8651. });
  8652. if (!found) {
  8653. // if we can't find the view in the history - close the list view
  8654. scope.exitMiniListView();
  8655. }
  8656. // update the breadcrumb
  8657. makeBreadcrumb();
  8658. };
  8659. scope.showBackButton = function () {
  8660. // don't show the back button if the start node is a list view
  8661. if (scope.node.metaData && scope.node.metaData.IsContainer || scope.node.isContainer) {
  8662. return false;
  8663. } else {
  8664. return true;
  8665. }
  8666. };
  8667. scope.exitMiniListView = function () {
  8668. miniListViewsHistory = [];
  8669. scope.miniListViews = [];
  8670. if (scope.onClose) {
  8671. scope.onClose();
  8672. }
  8673. };
  8674. function makeBreadcrumb() {
  8675. scope.breadcrumb = [];
  8676. angular.forEach(miniListViewsHistory, function (historyItem) {
  8677. scope.breadcrumb.push(historyItem.node);
  8678. });
  8679. }
  8680. /* Search */
  8681. scope.searchMiniListView = function (search, miniListView) {
  8682. // set search value
  8683. miniListView.pagination.filter = search;
  8684. // reset pagination
  8685. miniListView.pagination.pageNumber = 1;
  8686. // start loading animation list view
  8687. miniListView.loading = true;
  8688. searchMiniListView(miniListView);
  8689. };
  8690. var searchMiniListView = _.debounce(function (miniListView) {
  8691. scope.$apply(function () {
  8692. getChildrenForMiniListView(miniListView);
  8693. });
  8694. }, 500);
  8695. /* Animation */
  8696. scope.getMiniListViewAnimation = function () {
  8697. // disable the first "slide-in-animation"" if the start node is a list view
  8698. if (scope.node.metaData && scope.node.metaData.IsContainer && skipAnimation || scope.node.isContainer && skipAnimation) {
  8699. skipAnimation = false;
  8700. return;
  8701. }
  8702. if (goingForward) {
  8703. return 'umb-mini-list-view--forward';
  8704. } else {
  8705. return 'umb-mini-list-view--backwards';
  8706. }
  8707. };
  8708. onInit();
  8709. }
  8710. var directive = {
  8711. restrict: 'E',
  8712. replace: true,
  8713. templateUrl: 'views/components/umb-mini-list-view.html',
  8714. scope: {
  8715. node: '=',
  8716. entityType: '@',
  8717. startNodeId: '=',
  8718. onSelect: '&',
  8719. onClose: '&'
  8720. },
  8721. link: link
  8722. };
  8723. return directive;
  8724. }
  8725. angular.module('umbraco.directives').directive('umbMiniListView', MiniListViewDirective);
  8726. }());
  8727. angular.module('umbraco.directives').directive('umbNestedContentEditor', [function () {
  8728. var link = function ($scope) {
  8729. // Clone the model because some property editors
  8730. // do weird things like updating and config values
  8731. // so we want to ensure we start from a fresh every
  8732. // time, we'll just sync the value back when we need to
  8733. $scope.model = angular.copy($scope.ngModel);
  8734. $scope.nodeContext = $scope.model;
  8735. // Find the selected tab
  8736. var selectedTab = $scope.model.tabs[0];
  8737. if ($scope.tabAlias) {
  8738. angular.forEach($scope.model.tabs, function (tab) {
  8739. if (tab.alias.toLowerCase() === $scope.tabAlias.toLowerCase()) {
  8740. selectedTab = tab;
  8741. return;
  8742. }
  8743. });
  8744. }
  8745. $scope.tab = selectedTab;
  8746. // Listen for sync request
  8747. var unsubscribe = $scope.$on('ncSyncVal', function (ev, args) {
  8748. if (args.key === $scope.model.key) {
  8749. // Tell inner controls we are submitting
  8750. $scope.$broadcast('formSubmitting', { scope: $scope });
  8751. // Sync the values back
  8752. angular.forEach($scope.ngModel.tabs, function (tab) {
  8753. if (tab.alias.toLowerCase() === selectedTab.alias.toLowerCase()) {
  8754. var localPropsMap = selectedTab.properties.reduce(function (map, obj) {
  8755. map[obj.alias] = obj;
  8756. return map;
  8757. }, {});
  8758. angular.forEach(tab.properties, function (prop) {
  8759. if (localPropsMap.hasOwnProperty(prop.alias)) {
  8760. prop.value = localPropsMap[prop.alias].value;
  8761. }
  8762. });
  8763. }
  8764. });
  8765. }
  8766. });
  8767. $scope.$on('$destroy', function () {
  8768. unsubscribe();
  8769. });
  8770. };
  8771. return {
  8772. restrict: 'E',
  8773. replace: true,
  8774. templateUrl: Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/views/propertyeditors/nestedcontent/nestedcontent.editor.html',
  8775. scope: {
  8776. ngModel: '=',
  8777. tabAlias: '='
  8778. },
  8779. link: link
  8780. };
  8781. }]);
  8782. //angular.module("umbraco.directives").directive('nestedContentSubmitWatcher', function () {
  8783. // var link = function (scope) {
  8784. // // call the load callback on scope to obtain the ID of this submit watcher
  8785. // var id = scope.loadCallback();
  8786. // scope.$on("formSubmitting", function (ev, args) {
  8787. // // on the "formSubmitting" event, call the submit callback on scope to notify the nestedContent controller to do it's magic
  8788. // if (id === scope.activeSubmitWatcher) {
  8789. // scope.submitCallback();
  8790. // }
  8791. // });
  8792. // }
  8793. // return {
  8794. // restrict: "E",
  8795. // replace: true,
  8796. // template: "",
  8797. // scope: {
  8798. // loadCallback: '=',
  8799. // submitCallback: '=',
  8800. // activeSubmitWatcher: '='
  8801. // },
  8802. // link: link
  8803. // }
  8804. //});
  8805. /**
  8806. @ngdoc directive
  8807. @name umbraco.directives.directive:umbNodePreview
  8808. @restrict E
  8809. @scope
  8810. @description
  8811. <strong>Added in Umbraco v. 7.6:</strong> Use this directive to render a node preview.
  8812. <h3>Markup example</h3>
  8813. <pre>
  8814. <div ng-controller="My.NodePreviewController as vm">
  8815. <div ui-sortable ng-model="vm.nodes">
  8816. <umb-node-preview
  8817. ng-repeat="node in vm.nodes"
  8818. icon="node.icon"
  8819. name="node.name"
  8820. published="node.published"
  8821. description="node.description"
  8822. sortable="vm.sortable"
  8823. allow-remove="vm.allowRemove"
  8824. allow-open="vm.allowOpen"
  8825. on-remove="vm.remove($index, vm.nodes)"
  8826. on-open="vm.open(node)">
  8827. </umb-node-preview>
  8828. </div>
  8829. </div>
  8830. </pre>
  8831. <h3>Controller example</h3>
  8832. <pre>
  8833. (function () {
  8834. "use strict";
  8835. function Controller() {
  8836. var vm = this;
  8837. vm.allowRemove = true;
  8838. vm.allowOpen = true;
  8839. vm.sortable = true;
  8840. vm.nodes = [
  8841. {
  8842. "icon": "icon-document",
  8843. "name": "My node 1",
  8844. "published": true,
  8845. "description": "A short description of my node"
  8846. },
  8847. {
  8848. "icon": "icon-document",
  8849. "name": "My node 2",
  8850. "published": true,
  8851. "description": "A short description of my node"
  8852. }
  8853. ];
  8854. vm.remove = remove;
  8855. vm.open = open;
  8856. function remove(index, nodes) {
  8857. alert("remove node");
  8858. }
  8859. function open(node) {
  8860. alert("open node");
  8861. }
  8862. }
  8863. angular.module("umbraco").controller("My.NodePreviewController", Controller);
  8864. })();
  8865. </pre>
  8866. @param {string} icon (<code>binding</code>): The node icon.
  8867. @param {string} name (<code>binding</code>): The node name.
  8868. @param {boolean} published (<code>binding</code>): The node published state.
  8869. @param {string} description (<code>binding</code>): A short description.
  8870. @param {boolean} sortable (<code>binding</code>): Will add a move cursor on the node preview. Can used in combination with ui-sortable.
  8871. @param {boolean} allowRemove (<code>binding</code>): Show/Hide the remove button.
  8872. @param {boolean} allowOpen (<code>binding</code>): Show/Hide the open button.
  8873. @param {boolean} allowEdit (<code>binding</code>): Show/Hide the edit button (Added in version 7.7.0).
  8874. @param {function} onRemove (<code>expression</code>): Callback function when the remove button is clicked.
  8875. @param {function} onOpen (<code>expression</code>): Callback function when the open button is clicked.
  8876. @param {function} onEdit (<code>expression</code>): Callback function when the edit button is clicked (Added in version 7.7.0).
  8877. **/
  8878. (function () {
  8879. 'use strict';
  8880. function NodePreviewDirective() {
  8881. function link(scope, el, attr, ctrl) {
  8882. if (!scope.editLabelKey) {
  8883. scope.editLabelKey = 'general_edit';
  8884. }
  8885. }
  8886. var directive = {
  8887. restrict: 'E',
  8888. replace: true,
  8889. templateUrl: 'views/components/umb-node-preview.html',
  8890. scope: {
  8891. icon: '=?',
  8892. name: '=',
  8893. description: '=?',
  8894. permissions: '=?',
  8895. published: '=?',
  8896. sortable: '=?',
  8897. allowOpen: '=?',
  8898. allowRemove: '=?',
  8899. allowEdit: '=?',
  8900. onOpen: '&?',
  8901. onRemove: '&?',
  8902. onEdit: '&?'
  8903. },
  8904. link: link
  8905. };
  8906. return directive;
  8907. }
  8908. angular.module('umbraco.directives').directive('umbNodePreview', NodePreviewDirective);
  8909. }());
  8910. /**
  8911. @ngdoc directive
  8912. @name umbraco.directives.directive:umbPagination
  8913. @restrict E
  8914. @scope
  8915. @description
  8916. Use this directive to generate a pagination.
  8917. <h3>Markup example</h3>
  8918. <pre>
  8919. <div ng-controller="My.Controller as vm">
  8920. <umb-pagination
  8921. page-number="vm.pagination.pageNumber"
  8922. total-pages="vm.pagination.totalPages"
  8923. on-next="vm.nextPage"
  8924. on-prev="vm.prevPage"
  8925. on-go-to-page="vm.goToPage">
  8926. </umb-pagination>
  8927. </div>
  8928. </pre>
  8929. <h3>Controller example</h3>
  8930. <pre>
  8931. (function () {
  8932. "use strict";
  8933. function Controller() {
  8934. var vm = this;
  8935. vm.pagination = {
  8936. pageNumber: 1,
  8937. totalPages: 10
  8938. }
  8939. vm.nextPage = nextPage;
  8940. vm.prevPage = prevPage;
  8941. vm.goToPage = goToPage;
  8942. function nextPage(pageNumber) {
  8943. // do magic here
  8944. console.log(pageNumber);
  8945. alert("nextpage");
  8946. }
  8947. function prevPage(pageNumber) {
  8948. // do magic here
  8949. console.log(pageNumber);
  8950. alert("prevpage");
  8951. }
  8952. function goToPage(pageNumber) {
  8953. // do magic here
  8954. console.log(pageNumber);
  8955. alert("go to");
  8956. }
  8957. }
  8958. angular.module("umbraco").controller("My.Controller", Controller);
  8959. })();
  8960. </pre>
  8961. @param {number} pageNumber (<code>binding</code>): Current page number.
  8962. @param {number} totalPages (<code>binding</code>): The total number of pages.
  8963. @param {callback} onNext (<code>binding</code>): Callback method to go to the next page.
  8964. <h3>The callback returns:</h3>
  8965. <ul>
  8966. <li><code>pageNumber</code>: The page number</li>
  8967. </ul>
  8968. @param {callback=} onPrev (<code>binding</code>): Callback method to go to the previous page.
  8969. <h3>The callback returns:</h3>
  8970. <ul>
  8971. <li><code>pageNumber</code>: The page number</li>
  8972. </ul>
  8973. @param {callback=} onGoToPage (<code>binding</code>): Callback method to go to a specific page.
  8974. <h3>The callback returns:</h3>
  8975. <ul>
  8976. <li><code>pageNumber</code>: The page number</li>
  8977. </ul>
  8978. **/
  8979. (function () {
  8980. 'use strict';
  8981. function PaginationDirective(localizationService) {
  8982. function link(scope, el, attr, ctrl) {
  8983. function activate() {
  8984. scope.pagination = [];
  8985. var i = 0;
  8986. if (scope.totalPages <= 10) {
  8987. for (i = 0; i < scope.totalPages; i++) {
  8988. scope.pagination.push({
  8989. val: i + 1,
  8990. isActive: scope.pageNumber === i + 1
  8991. });
  8992. }
  8993. } else {
  8994. //if there is more than 10 pages, we need to do some fancy bits
  8995. //get the max index to start
  8996. var maxIndex = scope.totalPages - 10;
  8997. //set the start, but it can't be below zero
  8998. var start = Math.max(scope.pageNumber - 5, 0);
  8999. //ensure that it's not too far either
  9000. start = Math.min(maxIndex, start);
  9001. for (i = start; i < 10 + start; i++) {
  9002. scope.pagination.push({
  9003. val: i + 1,
  9004. isActive: scope.pageNumber === i + 1
  9005. });
  9006. }
  9007. //now, if the start is greater than 0 then '1' will not be displayed, so do the elipses thing
  9008. if (start > 0) {
  9009. scope.pagination.unshift({
  9010. name: localizationService.localize('general_first'),
  9011. val: 1,
  9012. isActive: false
  9013. }, {
  9014. val: '...',
  9015. isActive: false
  9016. });
  9017. }
  9018. //same for the end
  9019. if (start < maxIndex) {
  9020. scope.pagination.push({
  9021. val: '...',
  9022. isActive: false
  9023. }, {
  9024. name: localizationService.localize('general_last'),
  9025. val: scope.totalPages,
  9026. isActive: false
  9027. });
  9028. }
  9029. }
  9030. }
  9031. scope.next = function () {
  9032. if (scope.pageNumber < scope.totalPages) {
  9033. scope.pageNumber++;
  9034. if (scope.onNext) {
  9035. scope.onNext(scope.pageNumber);
  9036. }
  9037. if (scope.onChange) {
  9038. scope.onChange({ 'pageNumber': scope.pageNumber });
  9039. }
  9040. }
  9041. };
  9042. scope.prev = function (pageNumber) {
  9043. if (scope.pageNumber > 1) {
  9044. scope.pageNumber--;
  9045. if (scope.onPrev) {
  9046. scope.onPrev(scope.pageNumber);
  9047. }
  9048. if (scope.onChange) {
  9049. scope.onChange({ 'pageNumber': scope.pageNumber });
  9050. }
  9051. }
  9052. };
  9053. scope.goToPage = function (pageNumber) {
  9054. scope.pageNumber = pageNumber + 1;
  9055. if (scope.onGoToPage) {
  9056. scope.onGoToPage(scope.pageNumber);
  9057. }
  9058. if (scope.onChange) {
  9059. if (scope.onChange) {
  9060. scope.onChange({ 'pageNumber': scope.pageNumber });
  9061. }
  9062. }
  9063. };
  9064. var unbindPageNumberWatcher = scope.$watch('pageNumber', function (newValue, oldValue) {
  9065. activate();
  9066. });
  9067. scope.$on('$destroy', function () {
  9068. unbindPageNumberWatcher();
  9069. });
  9070. activate();
  9071. }
  9072. var directive = {
  9073. restrict: 'E',
  9074. replace: true,
  9075. templateUrl: 'views/components/umb-pagination.html',
  9076. scope: {
  9077. pageNumber: '=',
  9078. totalPages: '=',
  9079. onNext: '=',
  9080. onPrev: '=',
  9081. onGoToPage: '=',
  9082. onChange: '&'
  9083. },
  9084. link: link
  9085. };
  9086. return directive;
  9087. }
  9088. angular.module('umbraco.directives').directive('umbPagination', PaginationDirective);
  9089. }());
  9090. /**
  9091. @ngdoc directive
  9092. @name umbraco.directives.directive:umbPasswordToggle
  9093. @restrict E
  9094. @scope
  9095. @description
  9096. <strong>Added in Umbraco v. 7.7.4:</strong> Use this directive to render a password toggle.
  9097. **/
  9098. (function () {
  9099. 'use strict';
  9100. // comes from https://codepen.io/jakob-e/pen/eNBQaP
  9101. // works fine with Angular 1.6.5 - alas not with 1.1.5 - binding issue
  9102. function PasswordToggleDirective($compile) {
  9103. var directive = {
  9104. restrict: 'A',
  9105. scope: {},
  9106. link: function (scope, elem, attrs) {
  9107. scope.tgl = function () {
  9108. elem.attr('type', elem.attr('type') === 'text' ? 'password' : 'text');
  9109. };
  9110. var lnk = angular.element('<a data-ng-click="tgl()">Toggle</a>');
  9111. $compile(lnk)(scope);
  9112. elem.wrap('<div class="password-toggle"/>').after(lnk);
  9113. }
  9114. };
  9115. return directive;
  9116. }
  9117. angular.module('umbraco.directives').directive('umbPasswordToggle', PasswordToggleDirective);
  9118. }());
  9119. /**
  9120. @ngdoc directive
  9121. @name umbraco.directives.directive:umbProgressBar
  9122. @restrict E
  9123. @scope
  9124. @description
  9125. Use this directive to generate a progress bar.
  9126. <h3>Markup example</h3>
  9127. <pre>
  9128. <umb-progress-bar
  9129. percentage="60">
  9130. </umb-progress-bar>
  9131. </pre>
  9132. @param {number} percentage (<code>attribute</code>): The progress in percentage.
  9133. @param {string} size (<code>attribute</code>): The size (s, m).
  9134. **/
  9135. (function () {
  9136. 'use strict';
  9137. function ProgressBarDirective() {
  9138. var directive = {
  9139. restrict: 'E',
  9140. replace: true,
  9141. templateUrl: 'views/components/umb-progress-bar.html',
  9142. scope: {
  9143. percentage: '@',
  9144. size: '@?'
  9145. }
  9146. };
  9147. return directive;
  9148. }
  9149. angular.module('umbraco.directives').directive('umbProgressBar', ProgressBarDirective);
  9150. }());
  9151. /**
  9152. @ngdoc directive
  9153. @name umbraco.directives.directive:umbStickyBar
  9154. @restrict A
  9155. @description
  9156. Use this directive make an element sticky and follow the page when scrolling.
  9157. <h3>Markup example</h3>
  9158. <pre>
  9159. <div ng-controller="My.Controller as vm">
  9160. <div
  9161. class="my-sticky-bar"
  9162. umb-sticky-bar
  9163. scrollable-container=".container">
  9164. </div>
  9165. </div>
  9166. </pre>
  9167. <h3>CSS example</h3>
  9168. <pre>
  9169. .my-sticky-bar {
  9170. padding: 15px 0;
  9171. background: #000000;
  9172. position: relative;
  9173. top: 0;
  9174. }
  9175. .my-sticky-bar.-umb-sticky-bar {
  9176. top: 100px;
  9177. }
  9178. </pre>
  9179. @param {string} scrollableContainer Set the class (".element") or the id ("#element") of the scrollable container element.
  9180. **/
  9181. (function () {
  9182. 'use strict';
  9183. function StickyBarDirective($rootScope) {
  9184. function link(scope, el, attr, ctrl) {
  9185. var bar = $(el);
  9186. var scrollableContainer = null;
  9187. var clonedBar = null;
  9188. var cloneIsMade = false;
  9189. function activate() {
  9190. if (attr.scrollableContainer) {
  9191. scrollableContainer = $(attr.scrollableContainer);
  9192. } else {
  9193. scrollableContainer = $(window);
  9194. }
  9195. scrollableContainer.on('scroll.umbStickyBar', determineVisibility).trigger('scroll');
  9196. $(window).on('resize.umbStickyBar', determineVisibility);
  9197. scope.$on('$destroy', function () {
  9198. scrollableContainer.off('.umbStickyBar');
  9199. $(window).off('.umbStickyBar');
  9200. });
  9201. }
  9202. function determineVisibility() {
  9203. var barTop = bar[0].offsetTop;
  9204. var scrollTop = scrollableContainer.scrollTop();
  9205. if (scrollTop > barTop) {
  9206. if (!cloneIsMade) {
  9207. createClone();
  9208. clonedBar.css({ 'visibility': 'visible' });
  9209. } else {
  9210. calculateSize();
  9211. }
  9212. } else {
  9213. if (cloneIsMade) {
  9214. //remove cloned element (switched places with original on creation)
  9215. bar.remove();
  9216. bar = clonedBar;
  9217. clonedBar = null;
  9218. bar.removeClass('-umb-sticky-bar');
  9219. bar.css({
  9220. position: 'relative',
  9221. 'width': 'auto',
  9222. 'height': 'auto',
  9223. 'z-index': 'auto',
  9224. 'visibility': 'visible'
  9225. });
  9226. cloneIsMade = false;
  9227. }
  9228. }
  9229. }
  9230. function calculateSize() {
  9231. clonedBar.css({
  9232. width: bar.outerWidth(),
  9233. height: bar.height()
  9234. });
  9235. }
  9236. function createClone() {
  9237. //switch place with cloned element, to keep binding intact
  9238. clonedBar = bar;
  9239. bar = clonedBar.clone();
  9240. clonedBar.after(bar);
  9241. clonedBar.addClass('-umb-sticky-bar');
  9242. clonedBar.css({
  9243. 'position': 'fixed',
  9244. 'z-index': 500,
  9245. 'visibility': 'hidden'
  9246. });
  9247. cloneIsMade = true;
  9248. calculateSize();
  9249. }
  9250. activate();
  9251. }
  9252. var directive = {
  9253. restrict: 'A',
  9254. link: link
  9255. };
  9256. return directive;
  9257. }
  9258. angular.module('umbraco.directives').directive('umbStickyBar', StickyBarDirective);
  9259. }());
  9260. /**
  9261. @ngdoc directive
  9262. @name umbraco.directives.directive:umbTable
  9263. @restrict E
  9264. @scope
  9265. @description
  9266. <strong>Added in Umbraco v. 7.4:</strong> Use this directive to render a data table.
  9267. <h3>Markup example</h3>
  9268. <pre>
  9269. <div ng-controller="My.TableController as vm">
  9270. <umb-table
  9271. ng-if="items"
  9272. items="vm.items"
  9273. item-properties="vm.options.includeProperties"
  9274. allow-select-all="vm.allowSelectAll"
  9275. on-select="vm.selectItem"
  9276. on-click="vm.clickItem"
  9277. on-select-all="vm.selectAll"
  9278. on-selected-all="vm.isSelectedAll"
  9279. on-sorting-direction="vm.isSortDirection"
  9280. on-sort="vm.sort">
  9281. </umb-table>
  9282. </div>
  9283. </pre>
  9284. <h3>Controller example</h3>
  9285. <pre>
  9286. (function () {
  9287. "use strict";
  9288. function Controller() {
  9289. var vm = this;
  9290. vm.items = [
  9291. {
  9292. "icon": "icon-document",
  9293. "name": "My node 1",
  9294. "published": true,
  9295. "description": "A short description of my node",
  9296. "author": "Author 1"
  9297. },
  9298. {
  9299. "icon": "icon-document",
  9300. "name": "My node 2",
  9301. "published": true,
  9302. "description": "A short description of my node",
  9303. "author": "Author 2"
  9304. }
  9305. ];
  9306. vm.options = {
  9307. includeProperties: [
  9308. { alias: "description", header: "Description" },
  9309. { alias: "author", header: "Author" }
  9310. ]
  9311. };
  9312. vm.selectItem = selectItem;
  9313. vm.clickItem = clickItem;
  9314. vm.selectAll = selectAll;
  9315. vm.isSelectedAll = isSelectedAll;
  9316. vm.isSortDirection = isSortDirection;
  9317. vm.sort = sort;
  9318. function selectAll($event) {
  9319. alert("select all");
  9320. }
  9321. function isSelectedAll() {
  9322. }
  9323. function clickItem(item) {
  9324. alert("click node");
  9325. }
  9326. function selectItem(selectedItem, $index, $event) {
  9327. alert("select node");
  9328. }
  9329. function isSortDirection(col, direction) {
  9330. }
  9331. function sort(field, allow, isSystem) {
  9332. }
  9333. }
  9334. angular.module("umbraco").controller("My.TableController", Controller);
  9335. })();
  9336. </pre>
  9337. @param {string} icon (<code>binding</code>): The node icon.
  9338. @param {string} name (<code>binding</code>): The node name.
  9339. @param {string} published (<code>binding</code>): The node published state.
  9340. @param {function} onSelect (<code>expression</code>): Callback function when the row is selected.
  9341. @param {function} onClick (<code>expression</code>): Callback function when the "Name" column link is clicked.
  9342. @param {function} onSelectAll (<code>expression</code>): Callback function when selecting all items.
  9343. @param {function} onSelectedAll (<code>expression</code>): Callback function when all items are selected.
  9344. @param {function} onSortingDirection (<code>expression</code>): Callback function when sorting direction is changed.
  9345. @param {function} onSort (<code>expression</code>): Callback function when sorting items.
  9346. **/
  9347. (function () {
  9348. 'use strict';
  9349. function TableDirective(iconHelper) {
  9350. function link(scope, el, attr, ctrl) {
  9351. scope.clickItem = function (item, $event) {
  9352. if (scope.onClick) {
  9353. scope.onClick(item);
  9354. $event.stopPropagation();
  9355. }
  9356. };
  9357. scope.selectItem = function (item, $index, $event) {
  9358. if (scope.onSelect) {
  9359. scope.onSelect(item, $index, $event);
  9360. $event.stopPropagation();
  9361. }
  9362. };
  9363. scope.selectAll = function ($event) {
  9364. if (scope.onSelectAll) {
  9365. scope.onSelectAll($event);
  9366. }
  9367. };
  9368. scope.isSelectedAll = function () {
  9369. if (scope.onSelectedAll && scope.items && scope.items.length > 0) {
  9370. return scope.onSelectedAll();
  9371. }
  9372. };
  9373. scope.isSortDirection = function (col, direction) {
  9374. if (scope.onSortingDirection) {
  9375. return scope.onSortingDirection(col, direction);
  9376. }
  9377. };
  9378. scope.sort = function (field, allow, isSystem) {
  9379. if (scope.onSort) {
  9380. scope.onSort(field, allow, isSystem);
  9381. }
  9382. };
  9383. scope.getIcon = function (entry) {
  9384. return iconHelper.convertFromLegacyIcon(entry.icon);
  9385. };
  9386. }
  9387. var directive = {
  9388. restrict: 'E',
  9389. replace: true,
  9390. templateUrl: 'views/components/umb-table.html',
  9391. scope: {
  9392. items: '=',
  9393. itemProperties: '=',
  9394. allowSelectAll: '=',
  9395. onSelect: '=',
  9396. onClick: '=',
  9397. onSelectAll: '=',
  9398. onSelectedAll: '=',
  9399. onSortingDirection: '=',
  9400. onSort: '='
  9401. },
  9402. link: link
  9403. };
  9404. return directive;
  9405. }
  9406. angular.module('umbraco.directives').directive('umbTable', TableDirective);
  9407. }());
  9408. /**
  9409. @ngdoc directive
  9410. @name umbraco.directives.directive:umbTooltip
  9411. @restrict E
  9412. @scope
  9413. @description
  9414. Use this directive to render a tooltip.
  9415. <h3>Markup example</h3>
  9416. <pre>
  9417. <div ng-controller="My.Controller as vm">
  9418. <div
  9419. ng-mouseover="vm.mouseOver($event)"
  9420. ng-mouseleave="vm.mouseLeave()">
  9421. Hover me
  9422. </div>
  9423. <umb-tooltip
  9424. ng-if="vm.tooltip.show"
  9425. event="vm.tooltip.event">
  9426. // tooltip content here
  9427. </umb-tooltip>
  9428. </div>
  9429. </pre>
  9430. <h3>Controller example</h3>
  9431. <pre>
  9432. (function () {
  9433. "use strict";
  9434. function Controller() {
  9435. var vm = this;
  9436. vm.tooltip = {
  9437. show: false,
  9438. event: null
  9439. };
  9440. vm.mouseOver = mouseOver;
  9441. vm.mouseLeave = mouseLeave;
  9442. function mouseOver($event) {
  9443. vm.tooltip = {
  9444. show: true,
  9445. event: $event
  9446. };
  9447. }
  9448. function mouseLeave() {
  9449. vm.tooltip = {
  9450. show: false,
  9451. event: null
  9452. };
  9453. }
  9454. }
  9455. angular.module("umbraco").controller("My.Controller", Controller);
  9456. })();
  9457. </pre>
  9458. @param {string} event Set the $event from the target element to position the tooltip relative to the mouse cursor.
  9459. **/
  9460. (function () {
  9461. 'use strict';
  9462. function TooltipDirective($timeout) {
  9463. function link(scope, el, attr, ctrl) {
  9464. scope.tooltipStyles = {};
  9465. scope.tooltipStyles.left = 0;
  9466. scope.tooltipStyles.top = 0;
  9467. function activate() {
  9468. $timeout(function () {
  9469. setTooltipPosition(scope.event);
  9470. });
  9471. }
  9472. function setTooltipPosition(event) {
  9473. var container = $('#contentwrapper');
  9474. var containerLeft = container[0].offsetLeft;
  9475. var containerRight = containerLeft + container[0].offsetWidth;
  9476. var containerTop = container[0].offsetTop;
  9477. var containerBottom = containerTop + container[0].offsetHeight;
  9478. var elementHeight = null;
  9479. var elementWidth = null;
  9480. var position = {
  9481. right: 'inherit',
  9482. left: 'inherit',
  9483. top: 'inherit',
  9484. bottom: 'inherit'
  9485. };
  9486. // element size
  9487. elementHeight = el.context.clientHeight;
  9488. elementWidth = el.context.clientWidth;
  9489. position.left = event.pageX - elementWidth / 2;
  9490. position.top = event.pageY;
  9491. // check to see if element is outside screen
  9492. // outside right
  9493. if (position.left + elementWidth > containerRight) {
  9494. position.right = 10;
  9495. position.left = 'inherit';
  9496. }
  9497. // outside bottom
  9498. if (position.top + elementHeight > containerBottom) {
  9499. position.bottom = 10;
  9500. position.top = 'inherit';
  9501. }
  9502. // outside left
  9503. if (position.left < containerLeft) {
  9504. position.left = containerLeft + 10;
  9505. position.right = 'inherit';
  9506. }
  9507. // outside top
  9508. if (position.top < containerTop) {
  9509. position.top = 10;
  9510. position.bottom = 'inherit';
  9511. }
  9512. scope.tooltipStyles = position;
  9513. el.css(position);
  9514. }
  9515. activate();
  9516. }
  9517. var directive = {
  9518. restrict: 'E',
  9519. transclude: true,
  9520. replace: true,
  9521. templateUrl: 'views/components/umb-tooltip.html',
  9522. scope: { event: '=' },
  9523. link: link
  9524. };
  9525. return directive;
  9526. }
  9527. angular.module('umbraco.directives').directive('umbTooltip', TooltipDirective);
  9528. }());
  9529. /**
  9530. * @ngdoc directive
  9531. * @name umbraco.directives.directive:umbFileDropzone
  9532. * @restrict E
  9533. * @function
  9534. * @description
  9535. * Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form.
  9536. **/
  9537. /*
  9538. TODO
  9539. .directive("umbFileDrop", function ($timeout, $upload, localizationService, umbRequestHelper){
  9540. return{
  9541. restrict: "A",
  9542. link: function(scope, element, attrs){
  9543. //load in the options model
  9544. }
  9545. }
  9546. })
  9547. */
  9548. angular.module('umbraco.directives').directive('umbFileDropzone', function ($timeout, Upload, localizationService, umbRequestHelper) {
  9549. return {
  9550. restrict: 'E',
  9551. replace: true,
  9552. templateUrl: 'views/components/upload/umb-file-dropzone.html',
  9553. scope: {
  9554. parentId: '@',
  9555. contentTypeAlias: '@',
  9556. propertyAlias: '@',
  9557. accept: '@',
  9558. maxFileSize: '@',
  9559. compact: '@',
  9560. hideDropzone: '@',
  9561. acceptedMediatypes: '=',
  9562. filesQueued: '=',
  9563. handleFile: '=',
  9564. filesUploaded: '='
  9565. },
  9566. link: function (scope, element, attrs) {
  9567. scope.queue = [];
  9568. scope.done = [];
  9569. scope.rejected = [];
  9570. scope.currentFile = undefined;
  9571. function _filterFile(file) {
  9572. var ignoreFileNames = ['Thumbs.db'];
  9573. var ignoreFileTypes = ['directory'];
  9574. // ignore files with names from the list
  9575. // ignore files with types from the list
  9576. // ignore files which starts with "."
  9577. if (ignoreFileNames.indexOf(file.name) === -1 && ignoreFileTypes.indexOf(file.type) === -1 && file.name.indexOf('.') !== 0) {
  9578. return true;
  9579. } else {
  9580. return false;
  9581. }
  9582. }
  9583. function _filesQueued(files, event) {
  9584. //Push into the queue
  9585. angular.forEach(files, function (file) {
  9586. if (_filterFile(file) === true) {
  9587. if (file.$error) {
  9588. scope.rejected.push(file);
  9589. } else {
  9590. scope.queue.push(file);
  9591. }
  9592. }
  9593. });
  9594. //when queue is done, kick the uploader
  9595. if (!scope.working) {
  9596. // Upload not allowed
  9597. if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) {
  9598. files.map(function (file) {
  9599. file.uploadStatus = 'error';
  9600. file.serverErrorMessage = 'File type is not allowed here';
  9601. scope.rejected.push(file);
  9602. });
  9603. scope.queue = [];
  9604. }
  9605. // One allowed type
  9606. if (scope.acceptedMediatypes && scope.acceptedMediatypes.length === 1) {
  9607. // Standard setup - set alias to auto select to let the server best decide which media type to use
  9608. if (scope.acceptedMediatypes[0].alias === 'Image') {
  9609. scope.contentTypeAlias = 'umbracoAutoSelect';
  9610. } else {
  9611. scope.contentTypeAlias = scope.acceptedMediatypes[0].alias;
  9612. }
  9613. _processQueueItem();
  9614. }
  9615. // More than one, open dialog
  9616. if (scope.acceptedMediatypes && scope.acceptedMediatypes.length > 1) {
  9617. _chooseMediaType();
  9618. }
  9619. }
  9620. }
  9621. function _processQueueItem() {
  9622. if (scope.queue.length > 0) {
  9623. scope.currentFile = scope.queue.shift();
  9624. _upload(scope.currentFile);
  9625. } else if (scope.done.length > 0) {
  9626. if (scope.filesUploaded) {
  9627. //queue is empty, trigger the done action
  9628. scope.filesUploaded(scope.done);
  9629. }
  9630. //auto-clear the done queue after 3 secs
  9631. var currentLength = scope.done.length;
  9632. $timeout(function () {
  9633. scope.done.splice(0, currentLength);
  9634. }, 3000);
  9635. }
  9636. }
  9637. function _upload(file) {
  9638. scope.propertyAlias = scope.propertyAlias ? scope.propertyAlias : 'umbracoFile';
  9639. scope.contentTypeAlias = scope.contentTypeAlias ? scope.contentTypeAlias : 'Image';
  9640. Upload.upload({
  9641. url: umbRequestHelper.getApiUrl('mediaApiBaseUrl', 'PostAddFile'),
  9642. fields: {
  9643. 'currentFolder': scope.parentId,
  9644. 'contentTypeAlias': scope.contentTypeAlias,
  9645. 'propertyAlias': scope.propertyAlias,
  9646. 'path': file.path
  9647. },
  9648. file: file
  9649. }).progress(function (evt) {
  9650. if (file.uploadStat !== 'done' && file.uploadStat !== 'error') {
  9651. // calculate progress in percentage
  9652. var progressPercentage = parseInt(100 * evt.loaded / evt.total, 10);
  9653. // set percentage property on file
  9654. file.uploadProgress = progressPercentage;
  9655. // set uploading status on file
  9656. file.uploadStatus = 'uploading';
  9657. }
  9658. }).success(function (data, status, headers, config) {
  9659. if (data.notifications && data.notifications.length > 0) {
  9660. // set error status on file
  9661. file.uploadStatus = 'error';
  9662. // Throw message back to user with the cause of the error
  9663. file.serverErrorMessage = data.notifications[0].message;
  9664. // Put the file in the rejected pool
  9665. scope.rejected.push(file);
  9666. } else {
  9667. // set done status on file
  9668. file.uploadStatus = 'done';
  9669. file.uploadProgress = 100;
  9670. // set date/time for when done - used for sorting
  9671. file.doneDate = new Date();
  9672. // Put the file in the done pool
  9673. scope.done.push(file);
  9674. }
  9675. scope.currentFile = undefined;
  9676. //after processing, test if everthing is done
  9677. _processQueueItem();
  9678. }).error(function (evt, status, headers, config) {
  9679. // set status done
  9680. file.uploadStatus = 'error';
  9681. //if the service returns a detailed error
  9682. if (evt.InnerException) {
  9683. file.serverErrorMessage = evt.InnerException.ExceptionMessage;
  9684. //Check if its the common "too large file" exception
  9685. if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf('ValidateRequestEntityLength') > 0) {
  9686. file.serverErrorMessage = 'File too large to upload';
  9687. }
  9688. } else if (evt.Message) {
  9689. file.serverErrorMessage = evt.Message;
  9690. }
  9691. // If file not found, server will return a 404 and display this message
  9692. if (status === 404) {
  9693. file.serverErrorMessage = 'File not found';
  9694. }
  9695. //after processing, test if everthing is done
  9696. scope.rejected.push(file);
  9697. scope.currentFile = undefined;
  9698. _processQueueItem();
  9699. });
  9700. }
  9701. function _chooseMediaType() {
  9702. scope.mediatypepickerOverlay = {
  9703. view: 'mediatypepicker',
  9704. title: 'Choose media type',
  9705. acceptedMediatypes: scope.acceptedMediatypes,
  9706. hideSubmitButton: true,
  9707. show: true,
  9708. submit: function (model) {
  9709. scope.contentTypeAlias = model.selectedType.alias;
  9710. scope.mediatypepickerOverlay.show = false;
  9711. scope.mediatypepickerOverlay = null;
  9712. _processQueueItem();
  9713. },
  9714. close: function (oldModel) {
  9715. scope.queue.map(function (file) {
  9716. file.uploadStatus = 'error';
  9717. file.serverErrorMessage = 'Cannot upload this file, no mediatype selected';
  9718. scope.rejected.push(file);
  9719. });
  9720. scope.queue = [];
  9721. scope.mediatypepickerOverlay.show = false;
  9722. scope.mediatypepickerOverlay = null;
  9723. }
  9724. };
  9725. }
  9726. scope.handleFiles = function (files, event) {
  9727. if (scope.filesQueued) {
  9728. scope.filesQueued(files, event);
  9729. }
  9730. _filesQueued(files, event);
  9731. };
  9732. }
  9733. };
  9734. });
  9735. /**
  9736. * @ngdoc directive
  9737. * @name umbraco.directives.directive:umbFileUpload
  9738. * @function
  9739. * @restrict A
  9740. * @scope
  9741. * @description
  9742. * Listens for file input control changes and emits events when files are selected for use in other controllers.
  9743. **/
  9744. function umbFileUpload() {
  9745. return {
  9746. restrict: 'A',
  9747. scope: true,
  9748. //create a new scope
  9749. link: function (scope, el, attrs) {
  9750. el.bind('change', function (event) {
  9751. var files = event.target.files;
  9752. //emit event upward
  9753. scope.$emit('filesSelected', { files: files });
  9754. });
  9755. }
  9756. };
  9757. }
  9758. angular.module('umbraco.directives').directive('umbFileUpload', umbFileUpload);
  9759. /**
  9760. * @ngdoc directive
  9761. * @name umbraco.directives.directive:umbSingleFileUpload
  9762. * @function
  9763. * @restrict A
  9764. * @scope
  9765. * @description
  9766. * A single file upload field that will reset itself based on the object passed in for the rebuild parameter. This
  9767. * is required because the only way to reset an upload control is to replace it's html.
  9768. **/
  9769. function umbSingleFileUpload($compile) {
  9770. return {
  9771. restrict: 'E',
  9772. scope: { rebuild: '=' },
  9773. replace: true,
  9774. template: '<div><input type=\'file\' umb-file-upload /></div>',
  9775. link: function (scope, el, attrs) {
  9776. scope.$watch('rebuild', function (newVal, oldVal) {
  9777. if (newVal && newVal !== oldVal) {
  9778. //recompile it!
  9779. el.html('<input type=\'file\' umb-file-upload />');
  9780. $compile(el.contents())(scope);
  9781. }
  9782. });
  9783. }
  9784. };
  9785. }
  9786. angular.module('umbraco.directives').directive('umbSingleFileUpload', umbSingleFileUpload);
  9787. (function () {
  9788. 'use strict';
  9789. function ChangePasswordController($scope) {
  9790. function resetModel(isNew) {
  9791. //the model config will contain an object, if it does not we'll create defaults
  9792. //NOTE: We will not support doing the password regex on the client side because the regex on the server side
  9793. //based on the membership provider cannot always be ported to js from .net directly.
  9794. /*
  9795. {
  9796. hasPassword: true/false,
  9797. requiresQuestionAnswer: true/false,
  9798. enableReset: true/false,
  9799. enablePasswordRetrieval: true/false,
  9800. minPasswordLength: 10
  9801. }
  9802. */
  9803. $scope.showReset = false;
  9804. //set defaults if they are not available
  9805. if ($scope.config.disableToggle === undefined) {
  9806. $scope.config.disableToggle = false;
  9807. }
  9808. if ($scope.config.hasPassword === undefined) {
  9809. $scope.config.hasPassword = false;
  9810. }
  9811. if ($scope.config.enablePasswordRetrieval === undefined) {
  9812. $scope.config.enablePasswordRetrieval = true;
  9813. }
  9814. if ($scope.config.requiresQuestionAnswer === undefined) {
  9815. $scope.config.requiresQuestionAnswer = false;
  9816. }
  9817. //don't enable reset if it is new - that doesn't make sense
  9818. if (isNew === 'true') {
  9819. $scope.config.enableReset = false;
  9820. } else if ($scope.config.enableReset === undefined) {
  9821. $scope.config.enableReset = true;
  9822. }
  9823. if ($scope.config.minPasswordLength === undefined) {
  9824. $scope.config.minPasswordLength = 0;
  9825. }
  9826. //set the model defaults
  9827. if (!angular.isObject($scope.passwordValues)) {
  9828. //if it's not an object then just create a new one
  9829. $scope.passwordValues = {
  9830. newPassword: null,
  9831. oldPassword: null,
  9832. reset: null,
  9833. answer: null
  9834. };
  9835. } else {
  9836. //just reset the values
  9837. if (!isNew) {
  9838. //if it is new, then leave the generated pass displayed
  9839. $scope.passwordValues.newPassword = null;
  9840. $scope.passwordValues.oldPassword = null;
  9841. }
  9842. $scope.passwordValues.reset = null;
  9843. $scope.passwordValues.answer = null;
  9844. }
  9845. //the value to compare to match passwords
  9846. if (!isNew) {
  9847. $scope.passwordValues.confirm = '';
  9848. } else if ($scope.passwordValues.newPassword && $scope.passwordValues.newPassword.length > 0) {
  9849. //if it is new and a new password has been set, then set the confirm password too
  9850. $scope.passwordValues.confirm = $scope.passwordValues.newPassword;
  9851. }
  9852. }
  9853. resetModel($scope.isNew);
  9854. //if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there
  9855. //with validators turned on.
  9856. $scope.changing = $scope.config.disableToggle === true || !$scope.config.hasPassword;
  9857. //we're not currently changing so set the model to null
  9858. if (!$scope.changing) {
  9859. $scope.passwordValues = null;
  9860. }
  9861. $scope.doChange = function () {
  9862. resetModel();
  9863. $scope.changing = true;
  9864. //if there was a previously generated password displaying, clear it
  9865. $scope.passwordValues.generatedPassword = null;
  9866. $scope.passwordValues.confirm = null;
  9867. };
  9868. $scope.cancelChange = function () {
  9869. $scope.changing = false;
  9870. //set model to null
  9871. $scope.passwordValues = null;
  9872. };
  9873. var unsubscribe = [];
  9874. //listen for the saved event, when that occurs we'll
  9875. //change to changing = false;
  9876. unsubscribe.push($scope.$on('formSubmitted', function () {
  9877. if ($scope.config.disableToggle === false) {
  9878. $scope.changing = false;
  9879. }
  9880. }));
  9881. unsubscribe.push($scope.$on('formSubmitting', function () {
  9882. //if there was a previously generated password displaying, clear it
  9883. if ($scope.changing && $scope.passwordValues) {
  9884. $scope.passwordValues.generatedPassword = null;
  9885. } else if (!$scope.changing) {
  9886. //we are not changing, so the model needs to be null
  9887. $scope.passwordValues = null;
  9888. }
  9889. }));
  9890. //when the scope is destroyed we need to unsubscribe
  9891. $scope.$on('$destroy', function () {
  9892. for (var u in unsubscribe) {
  9893. unsubscribe[u]();
  9894. }
  9895. });
  9896. $scope.showOldPass = function () {
  9897. return $scope.config.hasPassword && !$scope.config.allowManuallyChangingPassword && !$scope.config.enablePasswordRetrieval && !$scope.showReset;
  9898. };
  9899. //TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this directive
  9900. $scope.showCancelBtn = function () {
  9901. return $scope.config.disableToggle !== true && $scope.config.hasPassword;
  9902. };
  9903. }
  9904. function ChangePasswordDirective() {
  9905. var directive = {
  9906. restrict: 'E',
  9907. replace: true,
  9908. templateUrl: 'views/components/users/change-password.html',
  9909. controller: 'Umbraco.Editors.Users.ChangePasswordDirectiveController',
  9910. scope: {
  9911. isNew: '=?',
  9912. passwordValues: '=',
  9913. config: '='
  9914. }
  9915. };
  9916. return directive;
  9917. }
  9918. angular.module('umbraco.directives').controller('Umbraco.Editors.Users.ChangePasswordDirectiveController', ChangePasswordController);
  9919. angular.module('umbraco.directives').directive('changePassword', ChangePasswordDirective);
  9920. }());
  9921. (function () {
  9922. 'use strict';
  9923. function PermissionDirective() {
  9924. function link(scope, el, attr, ctrl) {
  9925. scope.change = function () {
  9926. scope.selected = !scope.selected;
  9927. if (scope.onChange) {
  9928. scope.onChange({ 'selected': scope.selected });
  9929. }
  9930. };
  9931. }
  9932. var directive = {
  9933. restrict: 'E',
  9934. replace: true,
  9935. templateUrl: 'views/components/users/umb-permission.html',
  9936. scope: {
  9937. name: '=',
  9938. description: '=?',
  9939. selected: '=',
  9940. onChange: '&'
  9941. },
  9942. link: link
  9943. };
  9944. return directive;
  9945. }
  9946. angular.module('umbraco.directives').directive('umbPermission', PermissionDirective);
  9947. }());
  9948. /**
  9949. @ngdoc directive
  9950. @name umbraco.directives.directive:umbUserGroupPreview
  9951. @restrict E
  9952. @scope
  9953. @description
  9954. Use this directive to render a user group preview, where you can see the permissions the user or group has in the back office.
  9955. <h3>Markup example</h3>
  9956. <pre>
  9957. <div>
  9958. <umb-user-group-preview
  9959. ng-repeat="userGroup in vm.user.userGroups"
  9960. icon="userGroup.icon"
  9961. name="userGroup.name"
  9962. sections="userGroup.sections"
  9963. content-start-node="userGroup.contentStartNode"
  9964. media-start-node="userGroup.mediaStartNode"
  9965. allow-remove="!vm.user.isCurrentUser"
  9966. on-remove="vm.removeSelectedItem($index, vm.user.userGroups)">
  9967. </umb-user-group-preview>
  9968. </div>
  9969. </pre>
  9970. @param {string} icon (<code>binding</code>): The user group icon.
  9971. @param {string} name (<code>binding</code>): The user group name.
  9972. @param {array} sections (<code>binding</code>) Lists out the sections where the user has authority to edit.
  9973. @param {string} contentStartNode (<code>binding</code>)
  9974. <ul>
  9975. <li>The starting point in the tree of the content section.</li>
  9976. <li>So the user has no authority to work on other branches, only on this branch in the content section.</li>
  9977. </ul>
  9978. @param {boolean} hideContentStartNode (<code>binding</code>) Hides the contentStartNode.
  9979. @param {string} mediaStartNode (<code>binding</code>)
  9980. <ul>
  9981. <li> The starting point in the tree of the media section.</li>
  9982. <li> So the user has no authority to work on other branches, only on this branch in the media section.</li>
  9983. </ul>
  9984. @param {boolean} hideMediaStartNode (<code>binding</code>) Hides the mediaStartNode.
  9985. @param {array} permissions (<code>binding<code>) A list of permissions, the user can have.
  9986. @param {boolean} allowRemove (<code>binding</code>): Shows or Hides the remove button.
  9987. @param {function} onRemove (<code>expression</code>): Callback function when the remove button is clicked.
  9988. @param {boolean} allowEdit (<code>binding</code>): Shows or Hides the edit button.
  9989. @param {function} onEdit (<code>expression</code>): Callback function when the edit button is clicked.
  9990. **/
  9991. (function () {
  9992. 'use strict';
  9993. function UserGroupPreviewDirective() {
  9994. function link(scope, el, attr, ctrl) {
  9995. }
  9996. var directive = {
  9997. restrict: 'E',
  9998. replace: true,
  9999. templateUrl: 'views/components/users/umb-user-group-preview.html',
  10000. scope: {
  10001. icon: '=?',
  10002. name: '=',
  10003. sections: '=?',
  10004. contentStartNode: '=?',
  10005. hideContentStartNode: '@?',
  10006. mediaStartNode: '=?',
  10007. hideMediaStartNode: '@?',
  10008. permissions: '=?',
  10009. allowRemove: '=?',
  10010. allowEdit: '=?',
  10011. onRemove: '&?',
  10012. onEdit: '&?'
  10013. },
  10014. link: link
  10015. };
  10016. return directive;
  10017. }
  10018. angular.module('umbraco.directives').directive('umbUserGroupPreview', UserGroupPreviewDirective);
  10019. }());
  10020. (function () {
  10021. 'use strict';
  10022. function UserPreviewDirective() {
  10023. function link(scope, el, attr, ctrl) {
  10024. }
  10025. var directive = {
  10026. restrict: 'E',
  10027. replace: true,
  10028. templateUrl: 'views/components/users/umb-user-preview.html',
  10029. scope: {
  10030. avatars: '=?',
  10031. name: '=',
  10032. allowRemove: '=?',
  10033. onRemove: '&?'
  10034. },
  10035. link: link
  10036. };
  10037. return directive;
  10038. }
  10039. angular.module('umbraco.directives').directive('umbUserPreview', UserPreviewDirective);
  10040. }());
  10041. /**
  10042. * Konami Code directive for AngularJS
  10043. * @version v0.0.1
  10044. * @license MIT License, http://www.opensource.org/licenses/MIT
  10045. */
  10046. angular.module('umbraco.directives').directive('konamiCode', [
  10047. '$document',
  10048. function ($document) {
  10049. var konamiKeysDefault = [
  10050. 38,
  10051. 38,
  10052. 40,
  10053. 40,
  10054. 37,
  10055. 39,
  10056. 37,
  10057. 39,
  10058. 66,
  10059. 65
  10060. ];
  10061. return {
  10062. restrict: 'A',
  10063. link: function (scope, element, attr) {
  10064. if (!attr.konamiCode) {
  10065. throw 'Konami directive must receive an expression as value.';
  10066. }
  10067. // Let user define a custom code.
  10068. var konamiKeys = attr.konamiKeys || konamiKeysDefault;
  10069. var keyIndex = 0;
  10070. /**
  10071. * Fired when konami code is type.
  10072. */
  10073. function activated() {
  10074. if ('konamiOnce' in attr) {
  10075. stopListening();
  10076. }
  10077. // Execute expression.
  10078. scope.$eval(attr.konamiCode);
  10079. }
  10080. /**
  10081. * Handle keydown events.
  10082. */
  10083. function keydown(e) {
  10084. if (e.keyCode === konamiKeys[keyIndex++]) {
  10085. if (keyIndex === konamiKeys.length) {
  10086. keyIndex = 0;
  10087. activated();
  10088. }
  10089. } else {
  10090. keyIndex = 0;
  10091. }
  10092. }
  10093. /**
  10094. * Stop to listen typing.
  10095. */
  10096. function stopListening() {
  10097. $document.off('keydown', keydown);
  10098. }
  10099. // Start listening to key typing.
  10100. $document.on('keydown', keydown);
  10101. // Stop listening when scope is destroyed.
  10102. scope.$on('$destroy', stopListening);
  10103. }
  10104. };
  10105. }
  10106. ]);
  10107. /**
  10108. @ngdoc directive
  10109. @name umbraco.directives.directive:umbKeyboardList
  10110. @restrict E
  10111. @description
  10112. <b>Added in versions 7.7.0</b>: Use this directive to add arrow up and down keyboard shortcuts to a list. Use this together with the {@link umbraco.directives.directive:umbDropdown umbDropdown} component to make easy accessible dropdown menus.
  10113. <h3>Markup example</h3>
  10114. <pre>
  10115. <div>
  10116. <ul umb-keyboard-list>
  10117. <li><a href="">Item 1</a></li>
  10118. <li><a href="">Item 2</a></li>
  10119. <li><a href="">Item 3</a></li>
  10120. <li><a href="">Item 4</a></li>
  10121. <li><a href="">Item 5</a></li>
  10122. <li><a href="">Item 6</a></li>
  10123. </ul>
  10124. </div>
  10125. </pre>
  10126. <h3>Use in combination with</h3>
  10127. <ul>
  10128. <li>{@link umbraco.directives.directive:umbDropdown umbDropdown}</li>
  10129. </ul>
  10130. **/
  10131. angular.module('umbraco.directives').directive('umbKeyboardList', [
  10132. '$document',
  10133. '$timeout',
  10134. function ($document, $timeout) {
  10135. return {
  10136. restrict: 'A',
  10137. link: function (scope, element, attr) {
  10138. var listItems = [];
  10139. var currentIndex = 0;
  10140. var focusSet = false;
  10141. $timeout(function () {
  10142. // get list of all links in the list
  10143. listItems = element.find('li a');
  10144. });
  10145. // Handle keydown events
  10146. function keydown(event) {
  10147. $timeout(function () {
  10148. checkFocus();
  10149. // arrow down
  10150. if (event.keyCode === 40) {
  10151. arrowDown();
  10152. }
  10153. // arrow up
  10154. if (event.keyCode === 38) {
  10155. arrowUp();
  10156. }
  10157. });
  10158. }
  10159. function checkFocus() {
  10160. var found = false;
  10161. // check if any element has focus
  10162. angular.forEach(listItems, function (item, index) {
  10163. if ($(item).is(':focus')) {
  10164. // if an element already has focus set the
  10165. // currentIndex so we navigate from that element
  10166. currentIndex = index;
  10167. focusSet = true;
  10168. found = true;
  10169. }
  10170. });
  10171. // If we don't find an element with focus we reset the currentIndex and the focusSet flag
  10172. // we do this because you can have navigated away from the list with tab and we want to reset it if you navigate back
  10173. if (!found) {
  10174. currentIndex = 0;
  10175. focusSet = false;
  10176. }
  10177. }
  10178. function arrowDown() {
  10179. if (currentIndex < listItems.length - 1) {
  10180. // only bump the current index if the focus is already
  10181. // set else we just want to focus the first element
  10182. if (focusSet) {
  10183. currentIndex++;
  10184. }
  10185. listItems[currentIndex].focus();
  10186. focusSet = true;
  10187. }
  10188. }
  10189. function arrowUp() {
  10190. if (currentIndex > 0) {
  10191. currentIndex--;
  10192. listItems[currentIndex].focus();
  10193. }
  10194. }
  10195. // Stop to listen typing.
  10196. function stopListening() {
  10197. $document.off('keydown', keydown);
  10198. }
  10199. // Start listening to key typing.
  10200. $document.on('keydown', keydown);
  10201. // Stop listening when scope is destroyed.
  10202. scope.$on('$destroy', stopListening);
  10203. }
  10204. };
  10205. }
  10206. ]);
  10207. /**
  10208. * @ngdoc directive
  10209. * @name umbraco.directives.directive:noDirtyCheck
  10210. * @restrict A
  10211. * @description Can be attached to form inputs to prevent them from setting the form as dirty (http://stackoverflow.com/questions/17089090/prevent-input-from-setting-form-dirty-angularjs)
  10212. **/
  10213. function noDirtyCheck() {
  10214. return {
  10215. restrict: 'A',
  10216. require: 'ngModel',
  10217. link: function (scope, elm, attrs, ctrl) {
  10218. elm.focus(function () {
  10219. scope.$watch(function () {
  10220. ctrl.$pristine = false;
  10221. });
  10222. });
  10223. }
  10224. };
  10225. }
  10226. angular.module('umbraco.directives.validation').directive('noDirtyCheck', noDirtyCheck);
  10227. (function () {
  10228. 'use strict';
  10229. function SetDirtyOnChange() {
  10230. function link(scope, el, attr, ctrl) {
  10231. if (attr.ngModel) {
  10232. scope.$watch(attr.ngModel, function (newValue, oldValue) {
  10233. if (!newValue) {
  10234. return;
  10235. }
  10236. if (newValue === oldValue) {
  10237. return;
  10238. }
  10239. ctrl.$setDirty();
  10240. }, true);
  10241. } else {
  10242. var initValue = attr.umbSetDirtyOnChange;
  10243. attr.$observe('umbSetDirtyOnChange', function (newValue) {
  10244. if (newValue !== initValue) {
  10245. ctrl.$setDirty();
  10246. }
  10247. });
  10248. }
  10249. }
  10250. var directive = {
  10251. require: '^form',
  10252. restrict: 'A',
  10253. link: link
  10254. };
  10255. return directive;
  10256. }
  10257. angular.module('umbraco.directives').directive('umbSetDirtyOnChange', SetDirtyOnChange);
  10258. }());
  10259. angular.module('umbraco.directives.validation').directive('valCompare', function () {
  10260. return {
  10261. require: [
  10262. 'ngModel',
  10263. '^form'
  10264. ],
  10265. link: function (scope, elem, attrs, ctrls) {
  10266. var ctrl = ctrls[0];
  10267. var formCtrl = ctrls[1];
  10268. var otherInput = formCtrl[attrs.valCompare];
  10269. ctrl.$parsers.push(function (value) {
  10270. if (value === otherInput.$viewValue) {
  10271. ctrl.$setValidity('valCompare', true);
  10272. return value;
  10273. }
  10274. ctrl.$setValidity('valCompare', false);
  10275. });
  10276. otherInput.$parsers.push(function (value) {
  10277. ctrl.$setValidity('valCompare', value === ctrl.$viewValue);
  10278. return value;
  10279. });
  10280. }
  10281. };
  10282. });
  10283. /**
  10284. * General-purpose validator for ngModel.
  10285. * angular.js comes with several built-in validation mechanism for input fields (ngRequired, ngPattern etc.) but using
  10286. * an arbitrary validation function requires creation of a custom formatters and / or parsers.
  10287. * The ui-validate directive makes it easy to use any function(s) defined in scope as a validator function(s).
  10288. * A validator function will trigger validation on both model and input changes.
  10289. *
  10290. * @example <input val-custom=" 'myValidatorFunction($value)' ">
  10291. * @example <input val-custom="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }">
  10292. * @example <input val-custom="{ foo : '$value > anotherModel' }" val-custom-watch=" 'anotherModel' ">
  10293. * @example <input val-custom="{ foo : '$value > anotherModel', bar : 'validateFoo($value)' }" val-custom-watch=" { foo : 'anotherModel' } ">
  10294. *
  10295. * @param val-custom {string|object literal} If strings is passed it should be a scope's function to be used as a validator.
  10296. * If an object literal is passed a key denotes a validation error key while a value should be a validator function.
  10297. * In both cases validator function should take a value to validate as its argument and should return true/false indicating a validation result.
  10298. */
  10299. /*
  10300. This code comes from the angular UI project, we had to change the directive name and module
  10301. but other then that its unmodified
  10302. */
  10303. angular.module('umbraco.directives.validation').directive('valCustom', function () {
  10304. return {
  10305. restrict: 'A',
  10306. require: 'ngModel',
  10307. link: function (scope, elm, attrs, ctrl) {
  10308. var validateFn, watch, validators = {}, validateExpr = scope.$eval(attrs.valCustom);
  10309. if (!validateExpr) {
  10310. return;
  10311. }
  10312. if (angular.isString(validateExpr)) {
  10313. validateExpr = { validator: validateExpr };
  10314. }
  10315. angular.forEach(validateExpr, function (exprssn, key) {
  10316. validateFn = function (valueToValidate) {
  10317. var expression = scope.$eval(exprssn, { '$value': valueToValidate });
  10318. if (angular.isObject(expression) && angular.isFunction(expression.then)) {
  10319. // expression is a promise
  10320. expression.then(function () {
  10321. ctrl.$setValidity(key, true);
  10322. }, function () {
  10323. ctrl.$setValidity(key, false);
  10324. });
  10325. return valueToValidate;
  10326. } else if (expression) {
  10327. // expression is true
  10328. ctrl.$setValidity(key, true);
  10329. return valueToValidate;
  10330. } else {
  10331. // expression is false
  10332. ctrl.$setValidity(key, false);
  10333. return undefined;
  10334. }
  10335. };
  10336. validators[key] = validateFn;
  10337. ctrl.$parsers.push(validateFn);
  10338. });
  10339. function apply_watch(watch) {
  10340. //string - update all validators on expression change
  10341. if (angular.isString(watch)) {
  10342. scope.$watch(watch, function () {
  10343. angular.forEach(validators, function (validatorFn) {
  10344. validatorFn(ctrl.$modelValue);
  10345. });
  10346. });
  10347. return;
  10348. }
  10349. //array - update all validators on change of any expression
  10350. if (angular.isArray(watch)) {
  10351. angular.forEach(watch, function (expression) {
  10352. scope.$watch(expression, function () {
  10353. angular.forEach(validators, function (validatorFn) {
  10354. validatorFn(ctrl.$modelValue);
  10355. });
  10356. });
  10357. });
  10358. return;
  10359. }
  10360. //object - update appropriate validator
  10361. if (angular.isObject(watch)) {
  10362. angular.forEach(watch, function (expression, validatorKey) {
  10363. //value is string - look after one expression
  10364. if (angular.isString(expression)) {
  10365. scope.$watch(expression, function () {
  10366. validators[validatorKey](ctrl.$modelValue);
  10367. });
  10368. }
  10369. //value is array - look after all expressions in array
  10370. if (angular.isArray(expression)) {
  10371. angular.forEach(expression, function (intExpression) {
  10372. scope.$watch(intExpression, function () {
  10373. validators[validatorKey](ctrl.$modelValue);
  10374. });
  10375. });
  10376. }
  10377. });
  10378. }
  10379. }
  10380. // Support for val-custom-watch
  10381. if (attrs.valCustomWatch) {
  10382. apply_watch(scope.$eval(attrs.valCustomWatch));
  10383. }
  10384. }
  10385. };
  10386. });
  10387. /**
  10388. * @ngdoc directive
  10389. * @name umbraco.directives.directive:valEmail
  10390. * @restrict A
  10391. * @description A custom directive to validate an email address string, this is required because angular's default validator is incorrect.
  10392. **/
  10393. function valEmail(valEmailExpression) {
  10394. return {
  10395. require: 'ngModel',
  10396. restrict: 'A',
  10397. link: function (scope, elm, attrs, ctrl) {
  10398. var patternValidator = function (viewValue) {
  10399. //NOTE: we don't validate on empty values, use required validator for that
  10400. if (!viewValue || valEmailExpression.EMAIL_REGEXP.test(viewValue)) {
  10401. // it is valid
  10402. ctrl.$setValidity('valEmail', true);
  10403. //assign a message to the validator
  10404. ctrl.errorMsg = '';
  10405. return viewValue;
  10406. } else {
  10407. // it is invalid, return undefined (no model update)
  10408. ctrl.$setValidity('valEmail', false);
  10409. //assign a message to the validator
  10410. ctrl.errorMsg = 'Invalid email';
  10411. return undefined;
  10412. }
  10413. };
  10414. //if there is an attribute: type="email" then we need to remove those formatters and parsers
  10415. if (attrs.type === 'email') {
  10416. //we need to remove the existing parsers = the default angular one which is created by
  10417. // type="email", but this has a regex issue, so we'll remove that and add our custom one
  10418. ctrl.$parsers.pop();
  10419. //we also need to remove the existing formatter - the default angular one will not render
  10420. // what it thinks is an invalid email address, so it will just be blank
  10421. ctrl.$formatters.pop();
  10422. }
  10423. ctrl.$parsers.push(patternValidator);
  10424. }
  10425. };
  10426. }
  10427. angular.module('umbraco.directives.validation').directive('valEmail', valEmail).factory('valEmailExpression', function () {
  10428. var emailRegex = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
  10429. return { EMAIL_REGEXP: emailRegex };
  10430. });
  10431. /**
  10432. * @ngdoc directive
  10433. * @name umbraco.directives.directive:valFormManager
  10434. * @restrict A
  10435. * @require formController
  10436. * @description Used to broadcast an event to all elements inside this one to notify that form validation has
  10437. * changed. If we don't use this that means you have to put a watch for each directive on a form's validation
  10438. * changing which would result in much higher processing. We need to actually watch the whole $error collection of a form
  10439. * because just watching $valid or $invalid doesn't acurrately trigger form validation changing.
  10440. * This also sets the show-validation (or a custom) css class on the element when the form is invalid - this lets
  10441. * us css target elements to be displayed when the form is submitting/submitted.
  10442. * Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will
  10443. * be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly.
  10444. **/
  10445. function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) {
  10446. return {
  10447. require: 'form',
  10448. restrict: 'A',
  10449. controller: function ($scope) {
  10450. //This exposes an API for direct use with this directive
  10451. var unsubscribe = [];
  10452. var self = this;
  10453. //This is basically the same as a directive subscribing to an event but maybe a little
  10454. // nicer since the other directive can use this directive's API instead of a magical event
  10455. this.onValidationStatusChanged = function (cb) {
  10456. unsubscribe.push($scope.$on('valStatusChanged', function (evt, args) {
  10457. cb.apply(self, [
  10458. evt,
  10459. args
  10460. ]);
  10461. }));
  10462. };
  10463. //Ensure to remove the event handlers when this instance is destroyted
  10464. $scope.$on('$destroy', function () {
  10465. for (var u in unsubscribe) {
  10466. unsubscribe[u]();
  10467. }
  10468. });
  10469. },
  10470. link: function (scope, element, attr, formCtrl) {
  10471. scope.$watch(function () {
  10472. return formCtrl.$error;
  10473. }, function (e) {
  10474. scope.$broadcast('valStatusChanged', { form: formCtrl });
  10475. //find all invalid elements' .control-group's and apply the error class
  10476. var inError = element.find('.control-group .ng-invalid').closest('.control-group');
  10477. inError.addClass('error');
  10478. //find all control group's that have no error and ensure the class is removed
  10479. var noInError = element.find('.control-group .ng-valid').closest('.control-group').not(inError);
  10480. noInError.removeClass('error');
  10481. }, true);
  10482. var className = attr.valShowValidation ? attr.valShowValidation : 'show-validation';
  10483. var savingEventName = attr.savingEvent ? attr.savingEvent : 'formSubmitting';
  10484. var savedEvent = attr.savedEvent ? attr.savingEvent : 'formSubmitted';
  10485. //This tracks if the user is currently saving a new item, we use this to determine
  10486. // if we should display the warning dialog that they are leaving the page - if a new item
  10487. // is being saved we never want to display that dialog, this will also cause problems when there
  10488. // are server side validation issues.
  10489. var isSavingNewItem = false;
  10490. //we should show validation if there are any msgs in the server validation collection
  10491. if (serverValidationManager.items.length > 0) {
  10492. element.addClass(className);
  10493. }
  10494. var unsubscribe = [];
  10495. //listen for the forms saving event
  10496. unsubscribe.push(scope.$on(savingEventName, function (ev, args) {
  10497. element.addClass(className);
  10498. //set the flag so we can check to see if we should display the error.
  10499. isSavingNewItem = $routeParams.create;
  10500. }));
  10501. //listen for the forms saved event
  10502. unsubscribe.push(scope.$on(savedEvent, function (ev, args) {
  10503. //remove validation class
  10504. element.removeClass(className);
  10505. //clear form state as at this point we retrieve new data from the server
  10506. //and all validation will have cleared at this point
  10507. formCtrl.$setPristine();
  10508. }));
  10509. //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but
  10510. // the form has pending changes
  10511. var locationEvent = $rootScope.$on('$locationChangeStart', function (event, nextLocation, currentLocation) {
  10512. if (!formCtrl.$dirty || isSavingNewItem) {
  10513. return;
  10514. }
  10515. var path = nextLocation.split('#')[1];
  10516. if (path) {
  10517. if (path.indexOf('%253') || path.indexOf('%252')) {
  10518. path = decodeURIComponent(path);
  10519. }
  10520. if (!notificationsService.hasView()) {
  10521. var msg = {
  10522. view: 'confirmroutechange',
  10523. args: {
  10524. path: path,
  10525. listener: locationEvent
  10526. }
  10527. };
  10528. notificationsService.add(msg);
  10529. }
  10530. //prevent the route!
  10531. event.preventDefault();
  10532. //raise an event
  10533. eventsService.emit('valFormManager.pendingChanges', true);
  10534. }
  10535. });
  10536. unsubscribe.push(locationEvent);
  10537. //Ensure to remove the event handler when this instance is destroyted
  10538. scope.$on('$destroy', function () {
  10539. for (var u in unsubscribe) {
  10540. unsubscribe[u]();
  10541. }
  10542. });
  10543. $timeout(function () {
  10544. formCtrl.$setPristine();
  10545. }, 1000);
  10546. }
  10547. };
  10548. }
  10549. angular.module('umbraco.directives.validation').directive('valFormManager', valFormManager);
  10550. /**
  10551. * @ngdoc directive
  10552. * @name umbraco.directives.directive:valHighlight
  10553. * @restrict A
  10554. * @description Used on input fields when you want to signal that they are in error, this will highlight the item for 1 second
  10555. **/
  10556. function valHighlight($timeout) {
  10557. return {
  10558. restrict: 'A',
  10559. link: function (scope, element, attrs, ctrl) {
  10560. attrs.$observe('valHighlight', function (newVal) {
  10561. if (newVal === 'true') {
  10562. element.addClass('highlight-error');
  10563. $timeout(function () {
  10564. //set the bound scope property to false
  10565. scope[attrs.valHighlight] = false;
  10566. }, 1000);
  10567. } else {
  10568. element.removeClass('highlight-error');
  10569. }
  10570. });
  10571. }
  10572. };
  10573. }
  10574. angular.module('umbraco.directives.validation').directive('valHighlight', valHighlight);
  10575. /**
  10576. * @ngdoc directive
  10577. * @name umbraco.directives.directive:valPropertyMsg
  10578. * @restrict A
  10579. * @element textarea
  10580. * @requires formController
  10581. * @description This directive is used to control the display of the property level validation message.
  10582. * We will listen for server side validation changes
  10583. * and when an error is detected for this property we'll show the error message.
  10584. * In order for this directive to work, the valStatusChanged directive must be placed on the containing form.
  10585. **/
  10586. function valPropertyMsg(serverValidationManager) {
  10587. return {
  10588. scope: { property: '=' },
  10589. require: '^form',
  10590. //require that this directive is contained within an ngForm
  10591. replace: true,
  10592. //replace the element with the template
  10593. restrict: 'E',
  10594. //restrict to element
  10595. template: '<div ng-show="errorMsg != \'\'" class=\'alert alert-error property-error\' >{{errorMsg}}</div>',
  10596. /**
  10597. Our directive requries a reference to a form controller
  10598. which gets passed in to this parameter
  10599. */
  10600. link: function (scope, element, attrs, formCtrl) {
  10601. var watcher = null;
  10602. // Gets the error message to display
  10603. function getErrorMsg() {
  10604. //this can be null if no property was assigned
  10605. if (scope.property) {
  10606. //first try to get the error msg from the server collection
  10607. var err = serverValidationManager.getPropertyError(scope.property.alias, '');
  10608. //if there's an error message use it
  10609. if (err && err.errorMsg) {
  10610. return err.errorMsg;
  10611. } else {
  10612. return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : 'Property has errors';
  10613. }
  10614. }
  10615. return 'Property has errors';
  10616. }
  10617. // We need to subscribe to any changes to our model (based on user input)
  10618. // This is required because when we have a server error we actually invalidate
  10619. // the form which means it cannot be resubmitted.
  10620. // So once a field is changed that has a server error assigned to it
  10621. // we need to re-validate it for the server side validator so the user can resubmit
  10622. // the form. Of course normal client-side validators will continue to execute.
  10623. function startWatch() {
  10624. //if there's not already a watch
  10625. if (!watcher) {
  10626. watcher = scope.$watch('property.value', function (newValue, oldValue) {
  10627. if (!newValue || angular.equals(newValue, oldValue)) {
  10628. return;
  10629. }
  10630. var errCount = 0;
  10631. for (var e in formCtrl.$error) {
  10632. if (angular.isArray(formCtrl.$error[e])) {
  10633. errCount++;
  10634. }
  10635. }
  10636. //we are explicitly checking for valServer errors here, since we shouldn't auto clear
  10637. // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg
  10638. // is the only one, then we'll clear.
  10639. if (errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg) || formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer)) {
  10640. scope.errorMsg = '';
  10641. formCtrl.$setValidity('valPropertyMsg', true);
  10642. stopWatch();
  10643. }
  10644. }, true);
  10645. }
  10646. }
  10647. //clear the watch when the property validator is valid again
  10648. function stopWatch() {
  10649. if (watcher) {
  10650. watcher();
  10651. watcher = null;
  10652. }
  10653. }
  10654. //if there's any remaining errors in the server validation service then we should show them.
  10655. var showValidation = serverValidationManager.items.length > 0;
  10656. var hasError = false;
  10657. //create properties on our custom scope so we can use it in our template
  10658. scope.errorMsg = '';
  10659. var unsubscribe = [];
  10660. //listen for form error changes
  10661. unsubscribe.push(scope.$on('valStatusChanged', function (evt, args) {
  10662. if (args.form.$invalid) {
  10663. //first we need to check if the valPropertyMsg validity is invalid
  10664. if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) {
  10665. //since we already have an error we'll just return since this means we've already set the
  10666. // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe
  10667. return;
  10668. } else if (element.closest('.umb-control-group').find('.ng-invalid').length > 0) {
  10669. //check if it's one of the properties that is invalid in the current content property
  10670. hasError = true;
  10671. //update the validation message if we don't already have one assigned.
  10672. if (showValidation && scope.errorMsg === '') {
  10673. scope.errorMsg = getErrorMsg();
  10674. }
  10675. } else {
  10676. hasError = false;
  10677. scope.errorMsg = '';
  10678. }
  10679. } else {
  10680. hasError = false;
  10681. scope.errorMsg = '';
  10682. }
  10683. }, true));
  10684. //listen for the forms saving event
  10685. unsubscribe.push(scope.$on('formSubmitting', function (ev, args) {
  10686. showValidation = true;
  10687. if (hasError && scope.errorMsg === '') {
  10688. scope.errorMsg = getErrorMsg();
  10689. } else if (!hasError) {
  10690. scope.errorMsg = '';
  10691. stopWatch();
  10692. }
  10693. }));
  10694. //listen for the forms saved event
  10695. unsubscribe.push(scope.$on('formSubmitted', function (ev, args) {
  10696. showValidation = false;
  10697. scope.errorMsg = '';
  10698. formCtrl.$setValidity('valPropertyMsg', true);
  10699. stopWatch();
  10700. }));
  10701. //listen for server validation changes
  10702. // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for
  10703. // validation changes to fields in the property this is because some server side validators may not
  10704. // return the field name for which the error belongs too, just the property for which it belongs.
  10705. // It's important to note that we need to subscribe to server validation changes here because we always must
  10706. // indicate that a content property is invalid at the property level since developers may not actually implement
  10707. // the correct field validation in their property editors.
  10708. if (scope.property) {
  10709. //this can be null if no property was assigned
  10710. serverValidationManager.subscribe(scope.property.alias, '', function (isValid, propertyErrors, allErrors) {
  10711. hasError = !isValid;
  10712. if (hasError) {
  10713. //set the error message to the server message
  10714. scope.errorMsg = propertyErrors[0].errorMsg;
  10715. //flag that the current validator is invalid
  10716. formCtrl.$setValidity('valPropertyMsg', false);
  10717. startWatch();
  10718. } else {
  10719. scope.errorMsg = '';
  10720. //flag that the current validator is valid
  10721. formCtrl.$setValidity('valPropertyMsg', true);
  10722. stopWatch();
  10723. }
  10724. });
  10725. //when the element is disposed we need to unsubscribe!
  10726. // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain
  10727. // but they are a different callback instance than the above.
  10728. element.bind('$destroy', function () {
  10729. stopWatch();
  10730. serverValidationManager.unsubscribe(scope.property.alias, '');
  10731. });
  10732. }
  10733. //when the scope is disposed we need to unsubscribe
  10734. scope.$on('$destroy', function () {
  10735. for (var u in unsubscribe) {
  10736. unsubscribe[u]();
  10737. }
  10738. });
  10739. }
  10740. };
  10741. }
  10742. angular.module('umbraco.directives.validation').directive('valPropertyMsg', valPropertyMsg);
  10743. /**
  10744. * @ngdoc directive
  10745. * @name umbraco.directives.directive:valPropertyValidator
  10746. * @restrict A
  10747. * @description Performs any custom property value validation checks on the client side. This allows property editors to be highly flexible when it comes to validation
  10748. on the client side. Typically if a property editor stores a primitive value (i.e. string) then the client side validation can easily be taken care of
  10749. with standard angular directives such as ng-required. However since some property editors store complex data such as JSON, a given property editor
  10750. might require custom validation. This directive can be used to validate an Umbraco property in any way that a developer would like by specifying a
  10751. callback method to perform the validation. The result of this method must return an object in the format of
  10752. {isValid: true, errorKey: 'required', errorMsg: 'Something went wrong' }
  10753. The error message returned will also be displayed for the property level validation message.
  10754. This directive should only be used when dealing with complex models, if custom validation needs to be performed with primitive values, use the simpler
  10755. angular validation directives instead since this will watch the entire model.
  10756. **/
  10757. function valPropertyValidator(serverValidationManager) {
  10758. return {
  10759. scope: { valPropertyValidator: '=' },
  10760. // The element must have ng-model attribute and be inside an umbProperty directive
  10761. require: [
  10762. 'ngModel',
  10763. '?^umbProperty'
  10764. ],
  10765. restrict: 'A',
  10766. link: function (scope, element, attrs, ctrls) {
  10767. var modelCtrl = ctrls[0];
  10768. var propCtrl = ctrls.length > 1 ? ctrls[1] : null;
  10769. // Check whether the scope has a valPropertyValidator method
  10770. if (!scope.valPropertyValidator || !angular.isFunction(scope.valPropertyValidator)) {
  10771. throw new Error('val-property-validator directive must specify a function to call');
  10772. }
  10773. var initResult = scope.valPropertyValidator();
  10774. // Validation method
  10775. var validate = function (viewValue) {
  10776. // Calls the validition method
  10777. var result = scope.valPropertyValidator();
  10778. if (!result.errorKey || result.isValid === undefined || !result.errorMsg) {
  10779. throw 'The result object from valPropertyValidator does not contain required properties: isValid, errorKey, errorMsg';
  10780. }
  10781. if (result.isValid === true) {
  10782. // Tell the controller that the value is valid
  10783. modelCtrl.$setValidity(result.errorKey, true);
  10784. if (propCtrl) {
  10785. propCtrl.setPropertyError(null);
  10786. }
  10787. } else {
  10788. // Tell the controller that the value is invalid
  10789. modelCtrl.$setValidity(result.errorKey, false);
  10790. if (propCtrl) {
  10791. propCtrl.setPropertyError(result.errorMsg);
  10792. }
  10793. }
  10794. };
  10795. // Parsers are called as soon as the value in the form input is modified
  10796. modelCtrl.$parsers.push(validate);
  10797. }
  10798. };
  10799. }
  10800. angular.module('umbraco.directives.validation').directive('valPropertyValidator', valPropertyValidator);
  10801. /**
  10802. * @ngdoc directive
  10803. * @name umbraco.directives.directive:valRegex
  10804. * @restrict A
  10805. * @description A custom directive to allow for matching a value against a regex string.
  10806. * NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string
  10807. **/
  10808. function valRegex() {
  10809. return {
  10810. require: 'ngModel',
  10811. restrict: 'A',
  10812. link: function (scope, elm, attrs, ctrl) {
  10813. var flags = '';
  10814. var regex;
  10815. var eventBindings = [];
  10816. attrs.$observe('valRegexFlags', function (newVal) {
  10817. if (newVal) {
  10818. flags = newVal;
  10819. }
  10820. });
  10821. attrs.$observe('valRegex', function (newVal) {
  10822. if (newVal) {
  10823. try {
  10824. var resolved = newVal;
  10825. if (resolved) {
  10826. regex = new RegExp(resolved, flags);
  10827. } else {
  10828. regex = new RegExp(attrs.valRegex, flags);
  10829. }
  10830. } catch (e) {
  10831. regex = new RegExp(attrs.valRegex, flags);
  10832. }
  10833. }
  10834. });
  10835. eventBindings.push(scope.$watch('ngModel', function (newValue, oldValue) {
  10836. if (newValue && newValue !== oldValue) {
  10837. patternValidator(newValue);
  10838. }
  10839. }));
  10840. var patternValidator = function (viewValue) {
  10841. if (regex) {
  10842. //NOTE: we don't validate on empty values, use required validator for that
  10843. if (!viewValue || regex.test(viewValue.toString())) {
  10844. // it is valid
  10845. ctrl.$setValidity('valRegex', true);
  10846. //assign a message to the validator
  10847. ctrl.errorMsg = '';
  10848. return viewValue;
  10849. } else {
  10850. // it is invalid, return undefined (no model update)
  10851. ctrl.$setValidity('valRegex', false);
  10852. //assign a message to the validator
  10853. ctrl.errorMsg = 'Value is invalid, it does not match the correct pattern';
  10854. return undefined;
  10855. }
  10856. }
  10857. };
  10858. scope.$on('$destroy', function () {
  10859. // unbind watchers
  10860. for (var e in eventBindings) {
  10861. eventBindings[e]();
  10862. }
  10863. });
  10864. }
  10865. };
  10866. }
  10867. angular.module('umbraco.directives.validation').directive('valRegex', valRegex);
  10868. (function () {
  10869. 'use strict';
  10870. function ValRequireComponentDirective() {
  10871. function link(scope, el, attr, ngModel) {
  10872. var unbindModelWatcher = scope.$watch(function () {
  10873. return ngModel.$modelValue;
  10874. }, function (newValue) {
  10875. if (newValue === undefined || newValue === null || newValue === '') {
  10876. ngModel.$setValidity('valRequiredComponent', false);
  10877. } else {
  10878. ngModel.$setValidity('valRequiredComponent', true);
  10879. }
  10880. });
  10881. // clean up
  10882. scope.$on('$destroy', function () {
  10883. unbindModelWatcher();
  10884. });
  10885. }
  10886. var directive = {
  10887. require: 'ngModel',
  10888. restrict: 'A',
  10889. link: link
  10890. };
  10891. return directive;
  10892. }
  10893. angular.module('umbraco.directives').directive('valRequireComponent', ValRequireComponentDirective);
  10894. }());
  10895. /**
  10896. * @ngdoc directive
  10897. * @name umbraco.directives.directive:valServer
  10898. * @restrict A
  10899. * @description This directive is used to associate a content property with a server-side validation response
  10900. * so that the validators in angular are updated based on server-side feedback.
  10901. **/
  10902. function valServer(serverValidationManager) {
  10903. return {
  10904. require: [
  10905. 'ngModel',
  10906. '?^umbProperty'
  10907. ],
  10908. restrict: 'A',
  10909. link: function (scope, element, attr, ctrls) {
  10910. var modelCtrl = ctrls[0];
  10911. var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null;
  10912. if (!umbPropCtrl) {
  10913. //we cannot proceed, this validator will be disabled
  10914. return;
  10915. }
  10916. var watcher = null;
  10917. //Need to watch the value model for it to change, previously we had subscribed to
  10918. //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that
  10919. // doesn't specifically have a 2 way ng binding. This is required because when we
  10920. // have a server error we actually invalidate the form which means it cannot be
  10921. // resubmitted. So once a field is changed that has a server error assigned to it
  10922. // we need to re-validate it for the server side validator so the user can resubmit
  10923. // the form. Of course normal client-side validators will continue to execute.
  10924. function startWatch() {
  10925. //if there's not already a watch
  10926. if (!watcher) {
  10927. watcher = scope.$watch(function () {
  10928. return modelCtrl.$modelValue;
  10929. }, function (newValue, oldValue) {
  10930. if (!newValue || angular.equals(newValue, oldValue)) {
  10931. return;
  10932. }
  10933. if (modelCtrl.$invalid) {
  10934. modelCtrl.$setValidity('valServer', true);
  10935. stopWatch();
  10936. }
  10937. }, true);
  10938. }
  10939. }
  10940. function stopWatch() {
  10941. if (watcher) {
  10942. watcher();
  10943. watcher = null;
  10944. }
  10945. }
  10946. var currentProperty = umbPropCtrl.property;
  10947. //default to 'value' if nothing is set
  10948. var fieldName = 'value';
  10949. if (attr.valServer) {
  10950. fieldName = scope.$eval(attr.valServer);
  10951. if (!fieldName) {
  10952. //eval returned nothing so just use the string
  10953. fieldName = attr.valServer;
  10954. }
  10955. }
  10956. //subscribe to the server validation changes
  10957. serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) {
  10958. if (!isValid) {
  10959. modelCtrl.$setValidity('valServer', false);
  10960. //assign an error msg property to the current validator
  10961. modelCtrl.errorMsg = propertyErrors[0].errorMsg;
  10962. startWatch();
  10963. } else {
  10964. modelCtrl.$setValidity('valServer', true);
  10965. //reset the error message
  10966. modelCtrl.errorMsg = '';
  10967. stopWatch();
  10968. }
  10969. });
  10970. //when the element is disposed we need to unsubscribe!
  10971. // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain
  10972. // but they are a different callback instance than the above.
  10973. element.bind('$destroy', function () {
  10974. stopWatch();
  10975. serverValidationManager.unsubscribe(currentProperty.alias, fieldName);
  10976. });
  10977. }
  10978. };
  10979. }
  10980. angular.module('umbraco.directives.validation').directive('valServer', valServer);
  10981. /**
  10982. * @ngdoc directive
  10983. * @name umbraco.directives.directive:valServerField
  10984. * @restrict A
  10985. * @description This directive is used to associate a content field (not user defined) with a server-side validation response
  10986. * so that the validators in angular are updated based on server-side feedback.
  10987. **/
  10988. function valServerField(serverValidationManager) {
  10989. return {
  10990. require: 'ngModel',
  10991. restrict: 'A',
  10992. link: function (scope, element, attr, ngModel) {
  10993. var fieldName = null;
  10994. var eventBindings = [];
  10995. attr.$observe('valServerField', function (newVal) {
  10996. if (newVal && fieldName === null) {
  10997. fieldName = newVal;
  10998. //subscribe to the changed event of the view model. This is required because when we
  10999. // have a server error we actually invalidate the form which means it cannot be
  11000. // resubmitted. So once a field is changed that has a server error assigned to it
  11001. // we need to re-validate it for the server side validator so the user can resubmit
  11002. // the form. Of course normal client-side validators will continue to execute.
  11003. eventBindings.push(scope.$watch(function () {
  11004. return ngModel.$modelValue;
  11005. }, function (newValue) {
  11006. if (ngModel.$invalid) {
  11007. ngModel.$setValidity('valServerField', true);
  11008. }
  11009. }));
  11010. //subscribe to the server validation changes
  11011. serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) {
  11012. if (!isValid) {
  11013. ngModel.$setValidity('valServerField', false);
  11014. //assign an error msg property to the current validator
  11015. ngModel.errorMsg = fieldErrors[0].errorMsg;
  11016. } else {
  11017. ngModel.$setValidity('valServerField', true);
  11018. //reset the error message
  11019. ngModel.errorMsg = '';
  11020. }
  11021. });
  11022. //when the element is disposed we need to unsubscribe!
  11023. // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain
  11024. // but they are a different callback instance than the above.
  11025. element.bind('$destroy', function () {
  11026. serverValidationManager.unsubscribe(null, fieldName);
  11027. });
  11028. }
  11029. });
  11030. scope.$on('$destroy', function () {
  11031. // unbind watchers
  11032. for (var e in eventBindings) {
  11033. eventBindings[e]();
  11034. }
  11035. });
  11036. }
  11037. };
  11038. }
  11039. angular.module('umbraco.directives.validation').directive('valServerField', valServerField);
  11040. /**
  11041. * @ngdoc directive
  11042. * @name umbraco.directives.directive:valSubView
  11043. * @restrict A
  11044. * @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data.
  11045. * In order for this directive to work, the valFormManager directive must be placed on the containing form.
  11046. **/
  11047. (function () {
  11048. 'use strict';
  11049. function valSubViewDirective() {
  11050. function link(scope, el, attr, ctrl) {
  11051. //if there are no containing form or valFormManager controllers, then we do nothing
  11052. if (!ctrl || !angular.isArray(ctrl) || ctrl.length !== 2 || !ctrl[0] || !ctrl[1]) {
  11053. return;
  11054. }
  11055. var valFormManager = ctrl[1];
  11056. scope.subView.hasError = false;
  11057. //listen for form validation changes
  11058. valFormManager.onValidationStatusChanged(function (evt, args) {
  11059. if (!args.form.$valid) {
  11060. var subViewContent = el.find('.ng-invalid');
  11061. if (subViewContent.length > 0) {
  11062. scope.subView.hasError = true;
  11063. } else {
  11064. scope.subView.hasError = false;
  11065. }
  11066. } else {
  11067. scope.subView.hasError = false;
  11068. }
  11069. });
  11070. }
  11071. var directive = {
  11072. require: [
  11073. '?^form',
  11074. '?^valFormManager'
  11075. ],
  11076. restrict: 'A',
  11077. link: link
  11078. };
  11079. return directive;
  11080. }
  11081. angular.module('umbraco.directives').directive('valSubView', valSubViewDirective);
  11082. }());
  11083. /**
  11084. * @ngdoc directive
  11085. * @name umbraco.directives.directive:valTab
  11086. * @restrict A
  11087. * @description Used to show validation warnings for a tab to indicate that the tab content has validations errors in its data.
  11088. * In order for this directive to work, the valFormManager directive must be placed on the containing form.
  11089. **/
  11090. function valTab() {
  11091. return {
  11092. require: [
  11093. '^form',
  11094. '^valFormManager'
  11095. ],
  11096. restrict: 'A',
  11097. link: function (scope, element, attr, ctrs) {
  11098. var valFormManager = ctrs[1];
  11099. var tabId = 'tab' + scope.tab.id;
  11100. scope.tabHasError = false;
  11101. //listen for form validation changes
  11102. valFormManager.onValidationStatusChanged(function (evt, args) {
  11103. if (!args.form.$valid) {
  11104. var tabContent = element.closest('.umb-panel').find('#' + tabId);
  11105. //check if the validation messages are contained inside of this tabs
  11106. if (tabContent.find('.ng-invalid').length > 0) {
  11107. scope.tabHasError = true;
  11108. } else {
  11109. scope.tabHasError = false;
  11110. }
  11111. } else {
  11112. scope.tabHasError = false;
  11113. }
  11114. });
  11115. }
  11116. };
  11117. }
  11118. angular.module('umbraco.directives.validation').directive('valTab', valTab);
  11119. function valToggleMsg(serverValidationManager) {
  11120. return {
  11121. require: '^form',
  11122. restrict: 'A',
  11123. /**
  11124. Our directive requries a reference to a form controller which gets passed in to this parameter
  11125. */
  11126. link: function (scope, element, attr, formCtrl) {
  11127. if (!attr.valToggleMsg) {
  11128. throw 'valToggleMsg requires that a reference to a validator is specified';
  11129. }
  11130. if (!attr.valMsgFor) {
  11131. throw 'valToggleMsg requires that the attribute valMsgFor exists on the element';
  11132. }
  11133. if (!formCtrl[attr.valMsgFor]) {
  11134. throw 'valToggleMsg cannot find field ' + attr.valMsgFor + ' on form ' + formCtrl.$name;
  11135. }
  11136. //if there's any remaining errors in the server validation service then we should show them.
  11137. var showValidation = serverValidationManager.items.length > 0;
  11138. var hasCustomMsg = element.contents().length > 0;
  11139. //add a watch to the validator for the value (i.e. myForm.value.$error.required )
  11140. scope.$watch(function () {
  11141. //sometimes if a dialog closes in the middle of digest we can get null references here
  11142. return formCtrl && formCtrl[attr.valMsgFor] ? formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] : null;
  11143. }, function () {
  11144. //sometimes if a dialog closes in the middle of digest we can get null references here
  11145. if (formCtrl && formCtrl[attr.valMsgFor]) {
  11146. if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] && showValidation) {
  11147. element.show();
  11148. //display the error message if this element has no contents
  11149. if (!hasCustomMsg) {
  11150. element.html(formCtrl[attr.valMsgFor].errorMsg);
  11151. }
  11152. } else {
  11153. element.hide();
  11154. }
  11155. }
  11156. });
  11157. var unsubscribe = [];
  11158. //listen for the saving event (the result is a callback method which is called to unsubscribe)
  11159. unsubscribe.push(scope.$on('formSubmitting', function (ev, args) {
  11160. showValidation = true;
  11161. if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg]) {
  11162. element.show();
  11163. //display the error message if this element has no contents
  11164. if (!hasCustomMsg) {
  11165. element.html(formCtrl[attr.valMsgFor].errorMsg);
  11166. }
  11167. } else {
  11168. element.hide();
  11169. }
  11170. }));
  11171. //listen for the saved event (the result is a callback method which is called to unsubscribe)
  11172. unsubscribe.push(scope.$on('formSubmitted', function (ev, args) {
  11173. showValidation = false;
  11174. element.hide();
  11175. }));
  11176. //when the element is disposed we need to unsubscribe!
  11177. // NOTE: this is very important otherwise if this directive is part of a modal, the listener still exists because the dom
  11178. // element might still be there even after the modal has been hidden.
  11179. element.bind('$destroy', function () {
  11180. for (var u in unsubscribe) {
  11181. unsubscribe[u]();
  11182. }
  11183. });
  11184. }
  11185. };
  11186. }
  11187. /**
  11188. * @ngdoc directive
  11189. * @name umbraco.directives.directive:valToggleMsg
  11190. * @restrict A
  11191. * @element input
  11192. * @requires formController
  11193. * @description This directive will show/hide an error based on: is the value + the given validator invalid? AND, has the form been submitted ?
  11194. **/
  11195. angular.module('umbraco.directives.validation').directive('valToggleMsg', valToggleMsg);
  11196. angular.module('umbraco.directives.validation').directive('valTriggerChange', function ($sniffer) {
  11197. return {
  11198. link: function (scope, elem, attrs) {
  11199. elem.bind('click', function () {
  11200. $(attrs.valTriggerChange).trigger($sniffer.hasEvent('input') ? 'input' : 'change');
  11201. });
  11202. },
  11203. priority: 1
  11204. };
  11205. });
  11206. }());