(function () { function MainController($scope, $rootScope, $location, $routeParams, $timeout, $http, $log, appState, treeService, notificationsService, userService, navigationService, historyService, updateChecker, assetsService, eventsService, umbRequestHelper, tmhDynamicLocale, localStorageService, tourService) { //the null is important because we do an explicit bool check on this in the view //the avatar is by default the umbraco logo $scope.authenticated = null; $scope.avatar = [ { value: 'assets/img/application/logo.png' }, { value: 'assets/img/application/logo@2x.png' }, { value: 'assets/img/application/logo@3x.png' } ]; $scope.touchDevice = appState.getGlobalState('touchDevice'); $scope.removeNotification = function (index) { notificationsService.remove(index); }; $scope.closeDialogs = function (event) { //only close dialogs if non-link and non-buttons are clicked var el = event.target.nodeName; var els = [ 'INPUT', 'A', 'BUTTON' ]; if (els.indexOf(el) >= 0) { return; } var parents = $(event.target).parents('a,button'); if (parents.length > 0) { return; } //SD: I've updated this so that we don't close the dialog when clicking inside of the dialog var nav = $(event.target).parents('#dialog'); if (nav.length === 1) { return; } eventsService.emit('app.closeDialogs', event); }; var evts = []; //when a user logs out or timesout evts.push(eventsService.on('app.notAuthenticated', function () { $scope.authenticated = null; $scope.user = null; })); evts.push(eventsService.on('app.userRefresh', function (evt) { userService.refreshCurrentUser().then(function (data) { $scope.user = data; //Load locale file if ($scope.user.locale) { tmhDynamicLocale.set($scope.user.locale); } if ($scope.user.avatars) { $scope.avatar = []; if (angular.isArray($scope.user.avatars)) { for (var i = 0; i < $scope.user.avatars.length; i++) { $scope.avatar.push({ value: $scope.user.avatars[i] }); } } } }); })); //when the app is ready/user is logged in, setup the data evts.push(eventsService.on('app.ready', function (evt, data) { $scope.authenticated = data.authenticated; $scope.user = data.user; updateChecker.check().then(function (update) { if (update && update !== 'null') { if (update.type !== 'None') { var notification = { headline: 'Update available', message: 'Click to download', sticky: true, type: 'info', url: update.url }; notificationsService.add(notification); } } }); //if the user has changed we need to redirect to the root so they don't try to continue editing the //last item in the URL (NOTE: the user id can equal zero, so we cannot just do !data.lastUserId since that will resolve to true) if (data.lastUserId !== undefined && data.lastUserId !== null && data.lastUserId !== data.user.id) { $location.path('/').search(''); historyService.removeAll(); treeService.clearCache(); //if the user changed, clearout local storage too - could contain sensitive data localStorageService.clearAll(); } //if this is a new login (i.e. the user entered credentials), then clear out local storage - could contain sensitive data if (data.loginType === 'credentials') { localStorageService.clearAll(); } //Load locale file if ($scope.user.locale) { tmhDynamicLocale.set($scope.user.locale); } if ($scope.user.avatars) { $scope.avatar = []; if (angular.isArray($scope.user.avatars)) { for (var i = 0; i < $scope.user.avatars.length; i++) { $scope.avatar.push({ value: $scope.user.avatars[i] }); } } } })); evts.push(eventsService.on('app.ysod', function (name, error) { $scope.ysodOverlay = { view: 'ysod', error: error, show: true }; })); // manage the help dialog by subscribing to the showHelp appState $scope.drawer = {}; evts.push(eventsService.on('appState.drawerState.changed', function (e, args) { // set view if (args.key === 'view') { $scope.drawer.view = args.value; } // set custom model if (args.key === 'model') { $scope.drawer.model = args.value; } // show / hide drawer if (args.key === 'showDrawer') { $scope.drawer.show = args.value; } })); evts.push(eventsService.on('appState.tour.start', function (name, args) { $scope.tour = args; $scope.tour.show = true; })); evts.push(eventsService.on('appState.tour.end', function () { $scope.tour = null; })); evts.push(eventsService.on('appState.tour.complete', function () { $scope.tour = null; })); evts.push(eventsService.on('appState.backdrop', function (name, args) { $scope.backdrop = args; })); //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { eventsService.unsubscribe(evts[e]); } }); } //register it angular.module('umbraco').controller('Umbraco.MainController', MainController).config(function (tmhDynamicLocaleProvider) { //Set url for locale files tmhDynamicLocaleProvider.localeLocationPattern('lib/angular/1.1.5/i18n/angular-locale_{{locale}}.js'); }); /** * @ngdoc controller * @name Umbraco.NavigationController * @function * * @description * Handles the section area of the app * * @param {navigationService} navigationService A reference to the navigationService */ function NavigationController($scope, $rootScope, $location, $log, $routeParams, $timeout, appState, navigationService, keyboardService, dialogService, historyService, eventsService, sectionResource, angularHelper) { //TODO: Need to think about this and an nicer way to achieve what this is doing. //the tree event handler i used to subscribe to the main tree click events $scope.treeEventHandler = $({}); navigationService.setupTreeEvents($scope.treeEventHandler); //Put the navigation service on this scope so we can use it's methods/properties in the view. // IMPORTANT: all properties assigned to this scope are generally available on the scope object on dialogs since // when we create a dialog we pass in this scope to be used for the dialog's scope instead of creating a new one. $scope.nav = navigationService; // TODO: Lets fix this, it is less than ideal to be passing in the navigationController scope to something else to be used as it's scope, // this is going to lead to problems/confusion. I really don't think passing scope's around is very good practice. $rootScope.nav = navigationService; //set up our scope vars $scope.showContextMenuDialog = false; $scope.showContextMenu = false; $scope.showSearchResults = false; $scope.menuDialogTitle = null; $scope.menuActions = []; $scope.menuNode = null; $scope.currentSection = appState.getSectionState('currentSection'); $scope.showNavigation = appState.getGlobalState('showNavigation'); //trigger search with a hotkey: keyboardService.bind('ctrl+shift+s', function () { navigationService.showSearch(); }); //trigger dialogs with a hotkey: keyboardService.bind('esc', function () { eventsService.emit('app.closeDialogs'); }); $scope.selectedId = navigationService.currentId; var evts = []; //Listen for global state changes evts.push(eventsService.on('appState.globalState.changed', function (e, args) { if (args.key === 'showNavigation') { $scope.showNavigation = args.value; } })); //Listen for menu state changes evts.push(eventsService.on('appState.menuState.changed', function (e, args) { if (args.key === 'showMenuDialog') { $scope.showContextMenuDialog = args.value; } if (args.key === 'showMenu') { $scope.showContextMenu = args.value; } if (args.key === 'dialogTitle') { $scope.menuDialogTitle = args.value; } if (args.key === 'menuActions') { $scope.menuActions = args.value; } if (args.key === 'currentNode') { $scope.menuNode = args.value; } })); //Listen for section state changes evts.push(eventsService.on('appState.treeState.changed', function (e, args) { var f = args; if (args.value.root && args.value.root.metaData.containsTrees === false) { $rootScope.emptySection = true; } else { $rootScope.emptySection = false; } })); //Listen for section state changes evts.push(eventsService.on('appState.sectionState.changed', function (e, args) { //section changed if (args.key === 'currentSection') { $scope.currentSection = args.value; } //show/hide search results if (args.key === 'showSearchResults') { $scope.showSearchResults = args.value; } })); //This reacts to clicks passed to the body element which emits a global call to close all dialogs evts.push(eventsService.on('app.closeDialogs', function (event) { if (appState.getGlobalState('stickyNavigation')) { navigationService.hideNavigation(); //TODO: don't know why we need this? - we are inside of an angular event listener. angularHelper.safeApply($scope); } })); //when a user logs out or timesout evts.push(eventsService.on('app.notAuthenticated', function () { $scope.authenticated = false; })); //when the application is ready and the user is authorized, setup the data evts.push(eventsService.on('app.ready', function (evt, data) { $scope.authenticated = true; })); //this reacts to the options item in the tree //todo, migrate to nav service $scope.searchShowMenu = function (ev, args) { //always skip default args.skipDefault = true; navigationService.showMenu(ev, args); }; //todo, migrate to nav service $scope.searchHide = function () { navigationService.hideSearch(); }; //the below assists with hiding/showing the tree var treeActive = false; //Sets a service variable as soon as the user hovers the navigation with the mouse //used by the leaveTree method to delay hiding $scope.enterTree = function (event) { treeActive = true; }; // Hides navigation tree, with a short delay, is cancelled if the user moves the mouse over the tree again $scope.leaveTree = function (event) { //this is a hack to handle IE touch events which freaks out due to no mouse events so the tree instantly shuts down if (!event) { return; } if (!appState.getGlobalState('touchDevice')) { treeActive = false; $timeout(function () { if (!treeActive) { navigationService.hideTree(); } }, 300); } }; //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { eventsService.unsubscribe(evts[e]); } }); } //register it angular.module('umbraco').controller('Umbraco.NavigationController', NavigationController); /** * @ngdoc controller * @name Umbraco.SearchController * @function * * @description * Controls the search functionality in the site * */ function SearchController($scope, searchService, $log, $location, navigationService, $q) { $scope.searchTerm = null; $scope.searchResults = []; $scope.isSearching = false; $scope.selectedResult = -1; $scope.navigateResults = function (ev) { //38: up 40: down, 13: enter switch (ev.keyCode) { case 38: iterateResults(true); break; case 40: iterateResults(false); break; case 13: if ($scope.selectedItem) { $location.path($scope.selectedItem.editorPath); navigationService.hideSearch(); } break; } }; var group = undefined; var groupNames = []; var groupIndex = -1; var itemIndex = -1; $scope.selectedItem = undefined; $scope.clearSearch = function () { $scope.searchTerm = null; }; function iterateResults(up) { //default group if (!group) { for (var g in $scope.groups) { if ($scope.groups.hasOwnProperty(g)) { groupNames.push(g); } } //Sorting to match the groups order groupNames.sort(); group = $scope.groups[groupNames[0]]; groupIndex = 0; } if (up) { if (itemIndex === 0) { if (groupIndex === 0) { gotoGroup(Object.keys($scope.groups).length - 1, true); } else { gotoGroup(groupIndex - 1, true); } } else { gotoItem(itemIndex - 1); } } else { if (itemIndex < group.results.length - 1) { gotoItem(itemIndex + 1); } else { if (groupIndex === Object.keys($scope.groups).length - 1) { gotoGroup(0); } else { gotoGroup(groupIndex + 1); } } } } function gotoGroup(index, up) { groupIndex = index; group = $scope.groups[groupNames[groupIndex]]; if (up) { gotoItem(group.results.length - 1); } else { gotoItem(0); } } function gotoItem(index) { itemIndex = index; $scope.selectedItem = group.results[itemIndex]; } //used to cancel any request in progress if another one needs to take it's place var canceler = null; $scope.$watch('searchTerm', _.debounce(function (newVal, oldVal) { $scope.$apply(function () { $scope.hasResults = false; if ($scope.searchTerm) { if (newVal !== null && newVal !== undefined && newVal !== oldVal) { //Resetting for brand new search group = undefined; groupNames = []; groupIndex = -1; itemIndex = -1; $scope.isSearching = true; navigationService.showSearch(); $scope.selectedItem = undefined; //a canceler exists, so perform the cancelation operation and reset if (canceler) { canceler.resolve(); canceler = $q.defer(); } else { canceler = $q.defer(); } searchService.searchAll({ term: $scope.searchTerm, canceler: canceler }).then(function (result) { //result is a dictionary of group Title and it's results var filtered = {}; _.each(result, function (value, key) { if (value.results.length > 0) { filtered[key] = value; } }); $scope.groups = filtered; // check if search has results $scope.hasResults = Object.keys($scope.groups).length > 0; //set back to null so it can be re-created canceler = null; $scope.isSearching = false; }); } } else { $scope.isSearching = false; navigationService.hideSearch(); $scope.selectedItem = undefined; } }); }, 200)); } //register it angular.module('umbraco').controller('Umbraco.SearchController', SearchController); /** * @ngdoc controller * @name Umbraco.MainController * @function * * @description * The controller for the AuthorizeUpgrade login page * */ function AuthorizeUpgradeController($scope, $window) { //Add this method to the scope - this method will be called by the login dialog controller when the login is successful // then we'll handle the redirect. $scope.submit = function (event) { var qry = $window.location.search.trimStart('?').split('&'); var redir = _.find(qry, function (item) { return item.startsWith('redir='); }); if (redir) { $window.location = decodeURIComponent(redir.split('=')[1]); } else { $window.location = '/'; } }; } angular.module('umbraco').controller('Umbraco.AuthorizeUpgradeController', AuthorizeUpgradeController); /** * @ngdoc controller * @name Umbraco.DashboardController * @function * * @description * Controls the dashboards of the application * */ function DashboardController($scope, $routeParams, dashboardResource, localizationService) { $scope.page = {}; $scope.page.nameLocked = true; $scope.page.loading = true; $scope.dashboard = {}; localizationService.localize('sections_' + $routeParams.section).then(function (name) { $scope.dashboard.name = name; }); dashboardResource.getDashboard($routeParams.section).then(function (tabs) { $scope.dashboard.tabs = tabs; $scope.page.loading = false; }); } //register it angular.module('umbraco').controller('Umbraco.DashboardController', DashboardController); angular.module('umbraco').controller('Umbraco.Dialogs.ApprovedColorPickerController', function ($scope, $http, umbPropEditorHelper, assetsService) { assetsService.loadJs('lib/cssparser/cssparser.js', $scope).then(function () { var cssPath = $scope.dialogData.cssPath; $scope.cssClass = $scope.dialogData.cssClass; $scope.classes = []; $scope.change = function (newClass) { $scope.model.value = newClass; }; $http.get(cssPath).success(function (data) { var parser = new CSSParser(); $scope.classes = parser.parse(data, false, false).cssRules; $scope.classes.splice(0, 0, 'noclass'); }); assetsService.loadCss(cssPath, $scope); }); }); function ContentEditDialogController($scope, editorState, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, umbModelMapper, $http) { $scope.defaultButton = null; $scope.subButtons = []; var dialogOptions = $scope.$parent.dialogOptions; // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish function performSave(args) { contentEditingHelper.contentEditorPerformSave({ statusMessage: args.statusMessage, saveMethod: args.saveMethod, scope: $scope, content: $scope.content }).then(function (content) { //success if (dialogOptions.closeOnSave) { $scope.submit(content); } }, function (err) { }); } function filterTabs(entity, blackList) { if (blackList) { _.each(entity.tabs, function (tab) { tab.hide = _.contains(blackList, tab.alias); }); } return entity; } ; function init(content) { var buttons = contentEditingHelper.configureContentEditorButtons({ create: $routeParams.create, content: content, methods: { saveAndPublish: $scope.saveAndPublish, sendToPublish: $scope.sendToPublish, save: $scope.save, unPublish: angular.noop } }); $scope.defaultButton = buttons.defaultButton; $scope.subButtons = buttons.subButtons; //This is a total hack but we have really no other way of sharing data to the property editors of this // content item, so we'll just set the property on the content item directly $scope.content.isDialogEditor = true; editorState.set($scope.content); } //check if the entity is being passed in, otherwise load it from the server if (angular.isObject(dialogOptions.entity)) { $scope.loaded = true; $scope.content = filterTabs(dialogOptions.entity, dialogOptions.tabFilter); init($scope.content); } else { contentResource.getById(dialogOptions.id).then(function (data) { $scope.loaded = true; $scope.content = filterTabs(data, dialogOptions.tabFilter); init($scope.content); //in one particular special case, after we've created a new item we redirect back to the edit // route but there might be server validation errors in the collection which we need to display // after the redirect, so we will bind all subscriptions which will show the server validation errors // if there are any and then clear them so the collection no longer persists them. serverValidationManager.executeAndClearAllSubscriptions(); }); } $scope.sendToPublish = function () { performSave({ saveMethod: contentResource.sendToPublish, statusMessage: 'Sending...' }); }; $scope.saveAndPublish = function () { performSave({ saveMethod: contentResource.publish, statusMessage: 'Publishing...' }); }; $scope.save = function () { performSave({ saveMethod: contentResource.save, statusMessage: 'Saving...' }); }; // this method is called for all action buttons and then we proxy based on the btn definition $scope.performAction = function (btn) { if (!btn || !angular.isFunction(btn.handler)) { throw 'btn.handler must be a function reference'; } if (!$scope.busy) { btn.handler.apply(this); } }; } angular.module('umbraco').controller('Umbraco.Dialogs.Content.EditController', ContentEditDialogController); angular.module('umbraco').controller('Umbraco.Dialogs.HelpController', function ($scope, $location, $routeParams, helpService, userService, localizationService) { $scope.section = $routeParams.section; $scope.version = Umbraco.Sys.ServerVariables.application.version + ' assembly: ' + Umbraco.Sys.ServerVariables.application.assemblyVersion; if (!$scope.section) { $scope.section = 'content'; } $scope.sectionName = $scope.section; var rq = {}; rq.section = $scope.section; //translate section name localizationService.localize('sections_' + rq.section).then(function (value) { $scope.sectionName = value; }); userService.getCurrentUser().then(function (user) { rq.lang = user.locale; if ($routeParams.url) { rq.path = decodeURIComponent($routeParams.url); if (rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0) { rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); } if (rq.path.indexOf('.aspx') > 0) { rq.path = rq.path.substring(0, rq.path.indexOf('.aspx')); } } else { rq.path = rq.section + '/' + $routeParams.tree + '/' + $routeParams.method; } helpService.findHelp(rq).then(function (topics) { $scope.topics = topics; }); helpService.findVideos(rq).then(function (videos) { $scope.videos = videos; }); }); }); //used for the icon picker dialog angular.module('umbraco').controller('Umbraco.Dialogs.IconPickerController', function ($scope, iconHelper) { iconHelper.getIcons().then(function (icons) { $scope.icons = icons; }); $scope.submitClass = function (icon) { if ($scope.color) { $scope.submit(icon + ' ' + $scope.color); } else { $scope.submit(icon); } }; }); /** * @ngdoc controller * @name Umbraco.Dialogs.InsertMacroController * @function * * @description * The controller for the custom insert macro dialog. Until we upgrade the template editor to be angular this * is actually loaded into an iframe with full html. */ function InsertMacroController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper) { /** changes the view to edit the params of the selected macro */ /** if there is pnly one macro, and it has parameters - editor can skip selecting the Macro **/ function editParams(insertIfNoParameters) { //whether to insert the macro in the rich text editor when editParams is called and there are no parameters see U4-10537 insertIfNoParameters = typeof insertIfNoParameters !== 'undefined' ? insertIfNoParameters : true; //get the macro params if there are any macroResource.getMacroParameters($scope.selectedMacro.id).then(function (data) { //go to next page if there are params otherwise we can just exit if (!angular.isArray(data) || data.length === 0) { //we can just exist! if (insertIfNoParameters) { submitForm(); } else { $scope.wizardStep = 'macroSelect'; } } else { $scope.wizardStep = 'paramSelect'; $scope.macroParams = data; //fill in the data if we are editing this macro if ($scope.dialogData && $scope.dialogData.macroData && $scope.dialogData.macroData.macroParamsDictionary) { _.each($scope.dialogData.macroData.macroParamsDictionary, function (val, key) { var prop = _.find($scope.macroParams, function (item) { return item.alias == key; }); if (prop) { if (_.isString(val)) { //we need to unescape values as they have most likely been escaped while inserted val = _.unescape(val); //detect if it is a json string if (val.detectIsJson()) { try { //Parse it to json prop.value = angular.fromJson(val); } catch (e) { // not json prop.value = val; } } else { prop.value = val; } } else { prop.value = val; } } }); } } }); } /** submit the filled out macro params */ function submitForm() { //collect the value data, close the dialog and send the data back to the caller //create a dictionary for the macro params var paramDictionary = {}; _.each($scope.macroParams, function (item) { var val = item.value; if (item.value != null && item.value != undefined && !_.isString(item.value)) { try { val = angular.toJson(val); } catch (e) { } } //each value needs to be xml escaped!! since the value get's stored as an xml attribute paramDictionary[item.alias] = _.escape(val); }); //need to find the macro alias for the selected id var macroAlias = $scope.selectedMacro.alias; //get the syntax based on the rendering engine var syntax; if ($scope.dialogData.renderingEngine && $scope.dialogData.renderingEngine === 'WebForms') { syntax = macroService.generateWebFormsSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); } else if ($scope.dialogData.renderingEngine && $scope.dialogData.renderingEngine === 'Mvc') { syntax = macroService.generateMvcSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); } else { syntax = macroService.generateMacroSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); } $scope.submit({ syntax: syntax, macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); } $scope.macros = []; $scope.selectedMacro = null; $scope.wizardStep = 'macroSelect'; $scope.macroParams = []; $scope.submitForm = function () { if (formHelper.submitForm({ scope: $scope })) { formHelper.resetForm({ scope: $scope }); if ($scope.wizardStep === 'macroSelect') { editParams(true); } else { submitForm(); } } }; //here we check to see if we've been passed a selected macro and if so we'll set the //editor to start with parameter editing if ($scope.dialogData && $scope.dialogData.macroData) { $scope.wizardStep = 'paramSelect'; } //get the macro list - pass in a filter if it is only for rte entityResource.getAll('Macro', $scope.dialogData && $scope.dialogData.richTextEditor && $scope.dialogData.richTextEditor === true ? 'UseInEditor=true' : null).then(function (data) { //if 'allowedMacros' is specified, we need to filter if (angular.isArray($scope.dialogData.allowedMacros) && $scope.dialogData.allowedMacros.length > 0) { $scope.macros = _.filter(data, function (d) { return _.contains($scope.dialogData.allowedMacros, d.alias); }); } else { $scope.macros = data; } //check if there's a pre-selected macro and if it exists if ($scope.dialogData && $scope.dialogData.macroData && $scope.dialogData.macroData.macroAlias) { var found = _.find(data, function (item) { return item.alias === $scope.dialogData.macroData.macroAlias; }); if (found) { //select the macro and go to next screen $scope.selectedMacro = found; editParams(true); return; } } //if there is only one macro in the site and it has parameters, let's not make the editor choose it from a selection of one macro (unless there are no parameters - then weirdly it's a better experience to make that selection) if ($scope.macros.length == 1) { $scope.selectedMacro = $scope.macros[0]; editParams(false); } else { //we don't have a pre-selected macro so ensure the correct step is set $scope.wizardStep = 'macroSelect'; } }); } angular.module('umbraco').controller('Umbraco.Dialogs.InsertMacroController', InsertMacroController); /** * @ngdoc controller * @name Umbraco.Dialogs.LegacyDeleteController * @function * * @description * The controller for deleting content */ function LegacyDeleteController($scope, legacyResource, treeService, navigationService) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; legacyResource.deleteItem({ nodeId: $scope.currentNode.id, nodeType: $scope.currentNode.nodeType, alias: $scope.currentNode.name }).then(function () { $scope.currentNode.loading = false; //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Dialogs.LegacyDeleteController', LegacyDeleteController); //used for the media picker dialog angular.module('umbraco').controller('Umbraco.Dialogs.LinkPickerController', function ($scope, eventsService, dialogService, entityResource, mediaHelper, userService, localizationService, tinyMceService) { var dialogOptions = $scope.dialogOptions; var searchText = 'Search...'; localizationService.localize('general_search').then(function (value) { searchText = value + '...'; }); $scope.dialogTreeEventHandler = $({}); $scope.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, dataTypeId: $scope.model && $scope.model.dataTypeId ? $scope.model.dataTypeId : null, results: [], selectedSearchResults: [] }; if (dialogOptions.currentTarget) { $scope.target = dialogOptions.currentTarget; //if we have a node ID, we fetch the current node to build the form data if ($scope.target.id || $scope.target.udi) { var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; if (!$scope.target.path) { entityResource.getPath(id, 'Document').then(function (path) { $scope.target.path = path; //now sync the tree to this path $scope.dialogTreeEventHandler.syncTree({ path: $scope.target.path, tree: 'content' }); }); } // if a link exists, get the properties to build the anchor name list entityResource.getUrlAndAnchors(id).then(function (resp) { $scope.anchorValues = resp.anchorValues; $scope.target.url = resp.url; }); } else if ($scope.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs // only do the substring if there's a # or a ? var indexOfAnchor = $scope.target.url.search(/(#|\?)/); if (indexOfAnchor > -1) { // populate the anchor $scope.target.anchor = $scope.target.url.substring(indexOfAnchor); // then rewrite the model and populate the link $scope.target.url = $scope.target.url.substring(0, indexOfAnchor); } } } if (dialogOptions.anchors) { $scope.anchorValues = dialogOptions.anchors; } function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); if (args.node.metaData.listViewNode) { //check if list view 'search' node was selected $scope.searchInfo.showSearch = true; $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; } else { eventsService.emit('dialogs.linkPicker.select', args); if ($scope.currentNode) { //un-select if there's a current one selected $scope.currentNode.selected = false; } $scope.currentNode = args.node; $scope.currentNode.selected = true; $scope.target.id = args.node.id; $scope.target.udi = args.node.udi; $scope.target.name = args.node.name; if (args.node.id < 0) { $scope.target.url = '/'; } else { entityResource.getUrlAndAnchors(args.node.id).then(function (resp) { $scope.anchorValues = resp.anchorValues; $scope.target.url = resp.url; }); } if (!angular.isUndefined($scope.target.isMedia)) { delete $scope.target.isMedia; } } } function nodeExpandedHandler(ev, args) { if (angular.isArray(args.children)) { //iterate children _.each(args.children, function (child) { //check if any of the items are list views, if so we need to add a custom // child: A node to activate the search if (child.metaData.isContainer) { child.hasChildren = true; child.children = [{ level: child.level + 1, hasChildren: false, name: searchText, metaData: { listViewNode: child }, cssClass: 'icon umb-tree-icon sprTree icon-search', cssClasses: ['not-published'] }]; } }); } } $scope.switchToMediaPicker = function () { userService.getCurrentUser().then(function (userData) { dialogService.mediaPicker({ startNodeId: userData.startMediaIds.length == 0 ? -1 : userData.startMediaIds[0], callback: function (media) { $scope.target.id = media.id; $scope.target.isMedia = true; $scope.target.name = media.name; $scope.target.url = media.image; } }); }); }; $scope.hideSearch = function () { $scope.searchInfo.showSearch = false; $scope.searchInfo.searchFromId = null; $scope.searchInfo.searchFromName = null; $scope.searchInfo.results = []; }; // method to select a search result $scope.selectResult = function (evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); }; //callback when there are search results $scope.onSearchResults = function (results) { $scope.searchInfo.results = results; $scope.searchInfo.showSearch = true; }; $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); }); angular.module('umbraco').controller('Umbraco.Dialogs.LoginController', function ($scope, $cookies, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService, $q) { $scope.invitedUser = null; $scope.invitedUserPasswordModel = { password: '', confirmPassword: '', buttonState: '', passwordPolicies: null, passwordPolicyText: '' }; $scope.loginStates = { submitButton: 'init' }; $scope.avatarFile = { filesHolder: null, uploadStatus: null, uploadProgress: 0, maxFileSize: Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + 'KB', acceptedFileTypes: mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes), uploaded: false }; $scope.togglePassword = function () { var elem = $('form[name=\'loginForm\'] input[name=\'password\']'); elem.attr('type', elem.attr('type') === 'text' ? 'password' : 'text'); $('.password-text.show, .password-text.hide').toggle(); }; function init() { // Check if it is a new user var inviteVal = $location.search().invite; //1 = enter password, 2 = password set, 3 = invalid token if (inviteVal && (inviteVal === '1' || inviteVal === '2')) { $q.all([ //get the current invite user authResource.getCurrentInvitedUser().then(function (data) { $scope.invitedUser = data; }, function () { //it failed so we should remove the search $location.search('invite', null); }), //get the membership provider config for password policies authResource.getMembershipProviderConfig().then(function (data) { $scope.invitedUserPasswordModel.passwordPolicies = data; //localize the text localizationService.localize('errorHandling_errorInPasswordFormat', [ $scope.invitedUserPasswordModel.passwordPolicies.minPasswordLength, $scope.invitedUserPasswordModel.passwordPolicies.minNonAlphaNumericChars ]).then(function (data) { $scope.invitedUserPasswordModel.passwordPolicyText = data; }); }) ]).then(function () { $scope.inviteStep = Number(inviteVal); }); } else if (inviteVal && inviteVal === '3') { $scope.inviteStep = Number(inviteVal); } } $scope.changeAvatar = function (files, event) { if (files && files.length > 0) { upload(files[0]); } }; $scope.getStarted = function () { $location.search('invite', null); $scope.submit(true); }; function upload(file) { $scope.avatarFile.uploadProgress = 0; Upload.upload({ url: umbRequestHelper.getApiUrl('currentUserApiBaseUrl', 'PostSetAvatar'), fields: {}, file: file }).progress(function (evt) { if ($scope.avatarFile.uploadStatus !== 'done' && $scope.avatarFile.uploadStatus !== 'error') { // set uploading status on file $scope.avatarFile.uploadStatus = 'uploading'; // calculate progress in percentage var progressPercentage = parseInt(100 * evt.loaded / evt.total, 10); // set percentage property on file $scope.avatarFile.uploadProgress = progressPercentage; } }).success(function (data, status, headers, config) { $scope.avatarFile.uploadProgress = 100; // set done status on file $scope.avatarFile.uploadStatus = 'done'; $scope.invitedUser.avatars = data; $scope.avatarFile.uploaded = true; }).error(function (evt, status, headers, config) { // set status done $scope.avatarFile.uploadStatus = 'error'; // If file not found, server will return a 404 and display this message if (status === 404) { $scope.avatarFile.serverErrorMessage = 'File not found'; } else if (status == 400) { //it's a validation error $scope.avatarFile.serverErrorMessage = evt.message; } else { //it's an unhandled error //if the service returns a detailed error if (evt.InnerException) { $scope.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage; //Check if its the common "too large file" exception if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf('ValidateRequestEntityLength') > 0) { $scope.avatarFile.serverErrorMessage = 'File too large to upload'; } } else if (evt.Message) { $scope.avatarFile.serverErrorMessage = evt.Message; } } }); } $scope.inviteSavePassword = function () { if (formHelper.submitForm({ scope: $scope, statusMessage: 'Saving...' })) { $scope.invitedUserPasswordModel.buttonState = 'busy'; currentUserResource.performSetInvitedUserPassword($scope.invitedUserPasswordModel.password).then(function (data) { //success formHelper.resetForm({ scope: $scope, notifications: data.notifications }); $scope.invitedUserPasswordModel.buttonState = 'success'; //set the user and set them as logged in $scope.invitedUser = data; userService.setAuthenticationSuccessful(data); $scope.inviteStep = 2; }, function (err) { //error formHelper.handleError(err); $scope.invitedUserPasswordModel.buttonState = 'error'; }); } }; var setFieldFocus = function (form, field) { $timeout(function () { $('form[name=\'' + form + '\'] input[name=\'' + field + '\']').focus(); }); }; var twoFactorloginDialog = null; function show2FALoginDialog(view, callback) { if (!twoFactorloginDialog) { twoFactorloginDialog = dialogService.open({ //very special flag which means that global events cannot close this dialog manualClose: true, template: view, modalClass: 'login-overlay', animation: 'slide', show: true, callback: callback }); } } function resetInputValidation() { $scope.confirmPassword = ''; $scope.password = ''; $scope.login = ''; if ($scope.loginForm) { $scope.loginForm.username.$setValidity('auth', true); $scope.loginForm.password.$setValidity('auth', true); } if ($scope.requestPasswordResetForm) { $scope.requestPasswordResetForm.email.$setValidity('auth', true); } if ($scope.setPasswordForm) { $scope.setPasswordForm.password.$setValidity('auth', true); $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); } } $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.canSendRequiredEmail && Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; $scope.showLogin = function () { $scope.errorMsg = ''; resetInputValidation(); $scope.view = 'login'; setFieldFocus('loginForm', 'username'); }; $scope.showRequestPasswordReset = function () { $scope.errorMsg = ''; resetInputValidation(); $scope.view = 'request-password-reset'; $scope.showEmailResetConfirmation = false; setFieldFocus('requestPasswordResetForm', 'email'); }; $scope.showSetPassword = function () { $scope.errorMsg = ''; resetInputValidation(); $scope.view = 'set-password'; setFieldFocus('setPasswordForm', 'password'); }; var d = new Date(); var konamiGreetings = new Array('Suze Sunday', 'Malibu Monday', 'Tequila Tuesday', 'Whiskey Wednesday', 'Negroni Day', 'Fernet Friday', 'Sancerre Saturday'); var konamiMode = $cookies.konamiLogin; if (konamiMode == '1') { $scope.greeting = 'Happy ' + konamiGreetings[d.getDay()]; } else { localizationService.localize('login_greeting' + d.getDay()).then(function (label) { $scope.greeting = label; }); // weekday[d.getDay()]; } $scope.errorMsg = ''; $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; $scope.externalLoginProviders = externalLoginInfo.providers; $scope.externalLoginInfo = externalLoginInfo; $scope.resetPasswordCodeInfo = resetPasswordCodeInfo; $scope.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; $scope.activateKonamiMode = function () { if ($cookies.konamiLogin == '1') { // somehow I can't update the cookie value using $cookies, so going native document.cookie = 'konamiLogin=; expires=Thu, 01 Jan 1970 00:00:01 GMT;'; document.location.reload(); } else { document.cookie = 'konamiLogin=1; expires=Tue, 01 Jan 2030 00:00:01 GMT;'; $scope.$apply(function () { $scope.greeting = 'Happy ' + konamiGreetings[d.getDay()]; }); } }; $scope.loginSubmit = function (login, password) { //TODO: Do validation properly like in the invite password update //if the login and password are not empty we need to automatically // validate them - this is because if there are validation errors on the server // then the user has to change both username & password to resubmit which isn't ideal, // so if they're not empty, we'll just make sure to set them to valid. if (login && password && login.length > 0 && password.length > 0) { $scope.loginForm.username.$setValidity('auth', true); $scope.loginForm.password.$setValidity('auth', true); } if ($scope.loginForm.$invalid) { return; } $scope.loginStates.submitButton = 'busy'; userService.authenticate(login, password).then(function (data) { $scope.loginStates.submitButton = 'success'; $scope.submit(true); }, function (reason) { //is Two Factor required? if (reason.status === 402) { $scope.errorMsg = 'Additional authentication required'; show2FALoginDialog(reason.data.twoFactorView, $scope.submit); } else { $scope.loginStates.submitButton = 'error'; $scope.errorMsg = reason.errorMsg; //set the form inputs to invalid $scope.loginForm.username.$setValidity('auth', false); $scope.loginForm.password.$setValidity('auth', false); } }); //setup a watch for both of the model values changing, if they change // while the form is invalid, then revalidate them so that the form can // be submitted again. $scope.loginForm.username.$viewChangeListeners.push(function () { if ($scope.loginForm.$invalid) { $scope.loginForm.username.$setValidity('auth', true); $scope.loginForm.password.$setValidity('auth', true); } }); $scope.loginForm.password.$viewChangeListeners.push(function () { if ($scope.loginForm.$invalid) { $scope.loginForm.username.$setValidity('auth', true); $scope.loginForm.password.$setValidity('auth', true); } }); }; $scope.requestPasswordResetSubmit = function (email) { //TODO: Do validation properly like in the invite password update if (email && email.length > 0) { $scope.requestPasswordResetForm.email.$setValidity('auth', true); } $scope.showEmailResetConfirmation = false; if ($scope.requestPasswordResetForm.$invalid) { return; } $scope.errorMsg = ''; authResource.performRequestPasswordReset(email).then(function () { //remove the email entered $scope.email = ''; $scope.showEmailResetConfirmation = true; }, function (reason) { $scope.errorMsg = reason.errorMsg; $scope.requestPasswordResetForm.email.$setValidity('auth', false); }); $scope.requestPasswordResetForm.email.$viewChangeListeners.push(function () { if ($scope.requestPasswordResetForm.email.$invalid) { $scope.requestPasswordResetForm.email.$setValidity('auth', true); } }); }; $scope.setPasswordSubmit = function (password, confirmPassword) { $scope.showSetPasswordConfirmation = false; if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { $scope.setPasswordForm.password.$setValidity('auth', true); $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); } if ($scope.setPasswordForm.$invalid) { return; } //TODO: All of this logic can/should be shared! We should do validation the nice way instead of all of this manual stuff, see: inviteSavePassword authResource.performSetPassword($scope.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, $scope.resetPasswordCodeInfo.resetCodeModel.resetCode).then(function () { $scope.showSetPasswordConfirmation = true; $scope.resetComplete = true; //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again resetPasswordCodeInfo.resetCodeModel = null; }, function (reason) { if (reason.data && reason.data.Message) { $scope.errorMsg = reason.data.Message; } else { $scope.errorMsg = reason.errorMsg; } $scope.setPasswordForm.password.$setValidity('auth', false); $scope.setPasswordForm.confirmPassword.$setValidity('auth', false); }); $scope.setPasswordForm.password.$viewChangeListeners.push(function () { if ($scope.setPasswordForm.password.$invalid) { $scope.setPasswordForm.password.$setValidity('auth', true); } }); $scope.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { if ($scope.setPasswordForm.confirmPassword.$invalid) { $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); } }); }; //Now, show the correct panel: if ($scope.resetPasswordCodeInfo.resetCodeModel) { $scope.showSetPassword(); } else if ($scope.resetPasswordCodeInfo.errors.length > 0) { $scope.view = 'password-reset-code-expired'; } else { $scope.showLogin(); } init(); }); //used for the macro picker dialog angular.module('umbraco').controller('Umbraco.Dialogs.MacroPickerController', function ($scope, macroFactory, umbPropEditorHelper) { $scope.macros = macroFactory.all(true); $scope.dialogMode = 'list'; $scope.configureMacro = function (macro) { $scope.dialogMode = 'configure'; $scope.dialogData.macro = macroFactory.getMacro(macro.alias); //set the correct view for each item for (var i = 0; i < dialogData.macro.properties.length; i++) { dialogData.macro.properties[i].editorView = umbPropEditorHelper.getViewPath(dialogData.macro.properties[i].view); } }; }); //used for the media picker dialog angular.module('umbraco').controller('Umbraco.Dialogs.MediaPickerController', function ($scope, mediaResource, umbRequestHelper, entityResource, $log, mediaHelper, mediaTypeHelper, eventsService, treeService) { var dialogOptions = $scope.dialogOptions; $scope.onlyImages = dialogOptions.onlyImages; $scope.showDetails = dialogOptions.showDetails; $scope.multiPicker = dialogOptions.multiPicker && dialogOptions.multiPicker !== '0' ? true : false; $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; $scope.cropSize = dialogOptions.cropSize; //preload selected item $scope.target = undefined; if (dialogOptions.currentTarget) { $scope.target = dialogOptions.currentTarget; } $scope.acceptedMediatypes = []; mediaTypeHelper.getAllowedImagetypes($scope.startNodeId).then(function (types) { $scope.acceptedMediatypes = types; }); $scope.upload = function (v) { angular.element('.umb-file-dropzone-directive .file-select').click(); }; $scope.dragLeave = function (el, event) { $scope.activeDrag = false; }; $scope.dragEnter = function (el, event) { $scope.activeDrag = true; }; $scope.submitFolder = function (e) { if (e.keyCode === 13) { e.preventDefault(); mediaResource.addFolder($scope.newFolderName, $scope.currentFolder.id).then(function (data) { $scope.showFolderInput = false; $scope.newFolderName = ''; //we've added a new folder so lets clear the tree cache for that specific item treeService.clearCache({ cacheKey: '__media', //this is the main media tree cache key childrenOf: data.parentId }); $scope.gotoFolder(data); }); } }; $scope.gotoFolder = function (folder) { if (!folder) { folder = { id: -1, name: 'Media', icon: 'icon-folder' }; } if (folder.id > 0) { entityResource.getAncestors(folder.id, 'media').then(function (anc) { // anc.splice(0,1); $scope.path = _.filter(anc, function (f) { return f.path.indexOf($scope.startNodeId) !== -1; }); }); mediaTypeHelper.getAllowedImagetypes(folder.id).then(function (types) { $scope.acceptedMediatypes = types; }); } else { $scope.path = []; } //mediaResource.rootMedia() entityResource.getChildren(folder.id, 'Media').then(function (data) { for (i = 0; i < data.length; i++) { if (data[i].metaData.MediaPath) { data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); } } $scope.searchTerm = ''; $scope.images = data ? data : []; }); $scope.currentFolder = folder; }; $scope.clickHandler = function (image, ev, select) { ev.preventDefault(); if (image.isFolder && !select) { $scope.gotoFolder(image); } else { eventsService.emit('dialogs.mediaPicker.select', image); //we have 3 options add to collection (if multi) show details, or submit it right back to the callback if ($scope.multiPicker) { $scope.select(image); image.cssclass = $scope.dialogData.selection.indexOf(image) > -1 ? 'selected' : ''; } else if ($scope.showDetails) { $scope.target = image; $scope.target.url = mediaHelper.resolveFile(image); } else { $scope.submit(image); } } }; $scope.exitDetails = function () { if (!$scope.currentFolder) { $scope.gotoFolder(); } $scope.target = undefined; }; $scope.onUploadComplete = function () { $scope.gotoFolder($scope.currentFolder); }; $scope.onFilesQueue = function () { $scope.activeDrag = false; }; //default root item if (!$scope.target) { $scope.gotoFolder({ id: $scope.startNodeId, name: 'Media', icon: 'icon-folder' }); } }); //used for the member picker dialog angular.module('umbraco').controller('Umbraco.Dialogs.MemberGroupPickerController', function ($scope, eventsService, entityResource, searchService, $log) { var dialogOptions = $scope.dialogOptions; $scope.dialogTreeEventHandler = $({}); $scope.multiPicker = dialogOptions.multiPicker; /** Method used for selecting a node */ function select(text, id) { if (dialogOptions.multiPicker) { $scope.select(id); } else { $scope.submit(id); } } function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); eventsService.emit('dialogs.memberGroupPicker.select', args); //This is a tree node, so we don't have an entity to pass in, it will need to be looked up //from the server in this method. select(args.node.name, args.node.id); //toggle checked state args.node.selected = args.node.selected === true ? false : true; } $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); }); angular.module('umbraco').controller('Umbraco.Dialogs.RteEmbedController', function ($scope, $http, umbRequestHelper) { $scope.form = {}; $scope.form.url = ''; $scope.form.width = 360; $scope.form.height = 240; $scope.form.constrain = true; $scope.form.preview = ''; $scope.form.success = false; $scope.form.info = ''; $scope.form.supportsDimensions = false; var origWidth = 500; var origHeight = 300; $scope.showPreview = function () { if ($scope.form.url) { $scope.form.show = true; $scope.form.preview = '
'; $scope.form.info = ''; $scope.form.success = false; $http({ method: 'GET', url: umbRequestHelper.getApiUrl('embedApiBaseUrl', 'GetEmbed'), params: { url: $scope.form.url, width: $scope.form.width, height: $scope.form.height } }).success(function (data) { $scope.form.preview = ''; switch (data.Status) { case 0: //not supported $scope.form.info = 'Not supported'; break; case 1: //error $scope.form.info = 'Could not embed media - please ensure the URL is valid'; break; case 2: $scope.form.preview = data.Markup; $scope.form.supportsDimensions = data.SupportsDimensions; $scope.form.success = true; break; } }).error(function () { $scope.form.supportsDimensions = false; $scope.form.preview = ''; $scope.form.info = 'Could not embed media - please ensure the URL is valid'; }); } else { $scope.form.supportsDimensions = false; $scope.form.preview = ''; $scope.form.info = 'Please enter a URL'; } }; $scope.changeSize = function (type) { var width, height; if ($scope.form.constrain) { width = parseInt($scope.form.width, 10); height = parseInt($scope.form.height, 10); if (type == 'width') { origHeight = Math.round(width / origWidth * height); $scope.form.height = origHeight; } else { origWidth = Math.round(height / origHeight * width); $scope.form.width = origWidth; } } if ($scope.form.url != '') { $scope.showPreview(); } }; $scope.insert = function () { $scope.submit($scope.form.preview); }; }); angular.module('umbraco').controller('Umbraco.Dialogs.Template.QueryBuilderController', function ($scope, $http, dialogService) { $http.get('backoffice/UmbracoApi/TemplateQuery/GetAllowedProperties').then(function (response) { $scope.properties = response.data; }); $http.get('backoffice/UmbracoApi/TemplateQuery/GetContentTypes').then(function (response) { $scope.contentTypes = response.data; }); $http.get('backoffice/UmbracoApi/TemplateQuery/GetFilterConditions').then(function (response) { $scope.conditions = response.data; }); $scope.query = { contentType: { name: 'Everything' }, source: { name: 'My website' }, filters: [{ property: undefined, operator: undefined }], sort: { property: { alias: '', name: '' }, direction: 'ascending' } }; $scope.chooseSource = function (query) { dialogService.contentPicker({ callback: function (data) { if (data.id > 0) { query.source = { id: data.id, name: data.name }; } else { query.source.name = 'My website'; delete query.source.id; } } }); }; var throttledFunc = _.throttle(function () { $http.post('backoffice/UmbracoApi/TemplateQuery/PostTemplateQuery', $scope.query).then(function (response) { $scope.result = response.data; }); }, 200); $scope.$watch('query', function (value) { throttledFunc(); }, true); $scope.getPropertyOperators = function (property) { var conditions = _.filter($scope.conditions, function (condition) { var index = condition.appliesTo.indexOf(property.type); return index >= 0; }); return conditions; }; $scope.addFilter = function (query) { query.filters.push({}); }; $scope.trashFilter = function (query) { query.filters.splice(query, 1); }; $scope.changeSortOrder = function (query) { if (query.sort.direction === 'ascending') { query.sort.direction = 'descending'; } else { query.sort.direction = 'ascending'; } }; $scope.setSortProperty = function (query, property) { query.sort.property = property; if (property.type === 'datetime') { query.sort.direction = 'descending'; } else { query.sort.direction = 'ascending'; } }; }); angular.module('umbraco').controller('Umbraco.Dialogs.Template.SnippetController', function ($scope) { $scope.type = $scope.dialogOptions.type; $scope.section = { name: '', required: false }; }); //used for the media picker dialog angular.module('umbraco').controller('Umbraco.Dialogs.TreePickerController', function ($scope, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService) { var tree = null; var dialogOptions = $scope.dialogOptions; $scope.dialogTreeEventHandler = $({}); $scope.section = dialogOptions.section; $scope.treeAlias = dialogOptions.treeAlias; $scope.multiPicker = dialogOptions.multiPicker; $scope.hideHeader = typeof dialogOptions.hideHeader === 'boolean' ? dialogOptions.hideHeader : true; ; $scope.searchInfo = { searchFromId: dialogOptions.startNodeId, searchFromName: null, showSearch: false, results: [], selectedSearchResults: [] }; //create the custom query string param for this tree $scope.customTreeParams = dialogOptions.startNodeId ? 'startNodeId=' + dialogOptions.startNodeId : ''; $scope.customTreeParams += dialogOptions.customTreeParams ? '&' + dialogOptions.customTreeParams : ''; var searchText = 'Search...'; localizationService.localize('general_search').then(function (value) { searchText = value + '...'; }); // Allow the entity type to be passed in but defaults to Document for backwards compatibility. var entityType = dialogOptions.entityType ? dialogOptions.entityType : 'Document'; //min / max values if (dialogOptions.minNumber) { dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); } if (dialogOptions.maxNumber) { dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); } if (dialogOptions.section === 'member') { entityType = 'Member'; } else if (dialogOptions.section === 'media') { entityType = 'Media'; } //Configures filtering if (dialogOptions.filter) { dialogOptions.filterExclude = false; dialogOptions.filterAdvanced = false; //used advanced filtering if (angular.isFunction(dialogOptions.filter)) { dialogOptions.filterAdvanced = true; } else if (angular.isObject(dialogOptions.filter)) { dialogOptions.filterAdvanced = true; } else { if (dialogOptions.filter.startsWith('!')) { dialogOptions.filterExclude = true; dialogOptions.filter = dialogOptions.filter.substring(1); } //used advanced filtering if (dialogOptions.filter.startsWith('{')) { dialogOptions.filterAdvanced = true; //convert to object dialogOptions.filter = angular.fromJson(dialogOptions.filter); } } } function nodeExpandedHandler(ev, args) { if (angular.isArray(args.children)) { //iterate children _.each(args.children, function (child) { //check if any of the items are list views, if so we need to add some custom // children: A node to activate the search, any nodes that have already been // selected in the search if (child.metaData.isContainer) { child.hasChildren = true; child.children = [{ level: child.level + 1, hasChildren: false, parent: function () { return child; }, name: searchText, metaData: { listViewNode: child }, cssClass: 'icon-search', cssClasses: ['not-published'] }]; //add base transition classes to this node child.cssClasses.push('tree-node-slide-up'); var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { return i.parentId == child.id; }); _.each(listViewResults, function (item) { child.children.unshift({ id: item.id, name: item.name, cssClass: 'icon umb-tree-icon sprTree ' + item.icon, level: child.level + 1, metaData: { isSearchResult: true }, hasChildren: false, parent: function () { return child; } }); }); } //now we need to look in the already selected search results and // toggle the check boxes for those ones that are listed var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { return child.id == selected.id; }); if (exists) { child.selected = true; } }); //check filter performFiltering(args.children); } } //gets the tree object when it loads function treeLoadedHandler(ev, args) { tree = args.tree; } //wires up selection function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); if (args.node.metaData.listViewNode) { //check if list view 'search' node was selected $scope.searchInfo.showSearch = true; $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; //add transition classes var listViewNode = args.node.parent(); listViewNode.cssClasses.push('tree-node-slide-up-hide-active'); } else if (args.node.metaData.isSearchResult) { //check if the item selected was a search result from a list view //unselect select(args.node.name, args.node.id); //remove it from the list view children var listView = args.node.parent(); listView.children = _.reject(listView.children, function (child) { return child.id == args.node.id; }); //remove it from the custom tracked search result list $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { return i.id == args.node.id; }); } else { eventsService.emit('dialogs.treePickerController.select', args); if (args.node.filtered) { return; } //This is a tree node, so we don't have an entity to pass in, it will need to be looked up //from the server in this method. select(args.node.name, args.node.id); //toggle checked state args.node.selected = args.node.selected === true ? false : true; } } /** Method used for selecting a node */ function select(text, id, entity) { //if we get the root, we just return a constructed entity, no need for server data if (id < 0) { if ($scope.multiPicker) { $scope.select(id); } else { var node = { alias: null, icon: 'icon-folder', id: id, name: text }; $scope.submit(node); } } else { if ($scope.multiPicker) { $scope.select(Number(id)); } else { $scope.hideSearch(); //if an entity has been passed in, use it if (entity) { $scope.submit(entity); } else { //otherwise we have to get it from the server entityResource.getById(id, entityType).then(function (ent) { $scope.submit(ent); }); } } } } function performFiltering(nodes) { if (!dialogOptions.filter) { return; } //remove any list view search nodes from being filtered since these are special nodes that always must // be allowed to be clicked on nodes = _.filter(nodes, function (n) { return !angular.isObject(n.metaData.listViewNode); }); if (dialogOptions.filterAdvanced) { //filter either based on a method or an object var filtered = angular.isFunction(dialogOptions.filter) ? _.filter(nodes, dialogOptions.filter) : _.where(nodes, dialogOptions.filter); angular.forEach(filtered, function (value, key) { value.filtered = true; if (dialogOptions.filterCssClass) { if (!value.cssClasses) { value.cssClasses = []; } value.cssClasses.push(dialogOptions.filterCssClass); } }); } else { var a = dialogOptions.filter.toLowerCase().replace(/\s/g, '').split(','); angular.forEach(nodes, function (value, key) { var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { value.filtered = true; if (dialogOptions.filterCssClass) { if (!value.cssClasses) { value.cssClasses = []; } value.cssClasses.push(dialogOptions.filterCssClass); } } }); } } $scope.multiSubmit = function (result) { entityResource.getByIds(result, entityType).then(function (ents) { $scope.submit(ents); }); }; /** method to select a search result */ $scope.selectResult = function (evt, result) { if (result.filtered) { return; } result.selected = result.selected === true ? false : true; //since result = an entity, we'll pass it in so we don't have to go back to the server select(result.name, result.id, result); //add/remove to our custom tracked list of selected search results if (result.selected) { $scope.searchInfo.selectedSearchResults.push(result); } else { $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { return i.id == result.id; }); } //ensure the tree node in the tree is checked/unchecked if it already exists there if (tree) { var found = treeService.getDescendantNode(tree.root, result.id); if (found) { found.selected = result.selected; } } }; $scope.hideSearch = function () { //Traverse the entire displayed tree and update each node to sync with the selected search results if (tree) { //we need to ensure that any currently displayed nodes that get selected // from the search get updated to have a check box! function checkChildren(children) { _.each(children, function (child) { //check if the id is in the selection, if so ensure it's flagged as selected var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { return child.id == selected.id; }); //if the curr node exists in selected search results, ensure it's checked if (exists) { child.selected = true; } //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result else if (child.metaData.isSearchResult) { //if this tree node is under a list view it means that the node was added // to the tree dynamically under the list view that was searched, so we actually want to remove // it all together from the tree var listView = child.parent(); listView.children = _.reject(listView.children, function (c) { return c.id == child.id; }); } //check if the current node is a list view and if so, check if there's any new results // that need to be added as child nodes to it based on search results selected if (child.metaData.isContainer) { child.cssClasses = _.reject(child.cssClasses, function (c) { return c === 'tree-node-slide-up-hide-active'; }); var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { return i.parentId == child.id; }); _.each(listViewResults, function (item) { var childExists = _.find(child.children, function (c) { return c.id == item.id; }); if (!childExists) { var parent = child; child.children.unshift({ id: item.id, name: item.name, cssClass: 'icon umb-tree-icon sprTree ' + item.icon, level: child.level + 1, metaData: { isSearchResult: true }, hasChildren: false, parent: function () { return parent; } }); } }); } //recurse if (child.children && child.children.length > 0) { checkChildren(child.children); } }); } checkChildren(tree.root.children); } $scope.searchInfo.showSearch = false; $scope.searchInfo.searchFromId = dialogOptions.startNodeId; $scope.searchInfo.searchFromName = null; $scope.searchInfo.results = []; }; $scope.onSearchResults = function (results) { //filter all items - this will mark an item as filtered performFiltering(results); //now actually remove all filtered items so they are not even displayed results = _.filter(results, function (item) { return !item.filtered; }); $scope.searchInfo.results = results; //sync with the curr selected results _.each($scope.searchInfo.results, function (result) { var exists = _.find($scope.dialogData.selection, function (selectedId) { return result.id == selectedId; }); if (exists) { result.selected = true; } }); $scope.searchInfo.showSearch = true; }; $scope.dialogTreeEventHandler.bind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); }); angular.module('umbraco').controller('Umbraco.Dialogs.UserController', function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper) { $scope.history = historyService.getCurrent(); $scope.version = Umbraco.Sys.ServerVariables.application.version + ' assembly: ' + Umbraco.Sys.ServerVariables.application.assemblyVersion; $scope.showPasswordFields = false; $scope.changePasswordButtonState = 'init'; $scope.externalLoginProviders = externalLoginInfo.providers; $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; var evts = []; evts.push(eventsService.on('historyService.add', function (e, args) { $scope.history = args.all; })); evts.push(eventsService.on('historyService.remove', function (e, args) { $scope.history = args.all; })); evts.push(eventsService.on('historyService.removeAll', function (e, args) { $scope.history = []; })); $scope.logout = function () { //Add event listener for when there are pending changes on an editor which means our route was not successful var pendingChangeEvent = eventsService.on('valFormManager.pendingChanges', function (e, args) { //one time listener, remove the event pendingChangeEvent(); $scope.close(); }); //perform the path change, if it is successful then the promise will resolve otherwise it will fail $scope.close(); $location.path('/logout'); }; $scope.gotoHistory = function (link) { $location.path(link); $scope.close(); }; //Manually update the remaining timeout seconds function updateTimeout() { $timeout(function () { if ($scope.remainingAuthSeconds > 0) { $scope.remainingAuthSeconds--; $scope.$digest(); //recurse updateTimeout(); } }, 1000, false); // 1 second, do NOT execute a global digest } function updateUserInfo() { //get the user userService.getCurrentUser().then(function (user) { $scope.user = user; if ($scope.user) { $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; $scope.canEditProfile = _.indexOf($scope.user.allowedSections, 'users') > -1; //set the timer updateTimeout(); authResource.getCurrentUserLinkedLogins().then(function (logins) { //reset all to be un-linked for (var provider in $scope.externalLoginProviders) { $scope.externalLoginProviders[provider].linkedProviderKey = undefined; } //set the linked logins for (var login in logins) { var found = _.find($scope.externalLoginProviders, function (i) { return i.authType == login; }); if (found) { found.linkedProviderKey = logins[login]; } } }); } }); } $scope.unlink = function (e, loginProvider, providerKey) { var result = confirm('Are you sure you want to unlink this account?'); if (!result) { e.preventDefault(); return; } authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { updateUserInfo(); }); }; updateUserInfo(); //remove all event handlers $scope.$on('$destroy', function () { for (var e = 0; e < evts.length; e++) { evts[e](); } }); /* ---------- UPDATE PASSWORD ---------- */ //create the initial model for change password property editor $scope.changePasswordModel = { alias: '_umb_password', view: 'changepassword', config: {}, value: {} }; //go get the config for the membership provider and add it to the model authResource.getMembershipProviderConfig().then(function (data) { $scope.changePasswordModel.config = data; //ensure the hasPassword config option is set to true (the user of course has a password already assigned) //this will ensure the oldPassword is shown so they can change it // disable reset password functionality beacuse it does not make sense for the current user. $scope.changePasswordModel.config.hasPassword = true; $scope.changePasswordModel.config.disableToggle = true; $scope.changePasswordModel.config.enableReset = false; }); $scope.changePassword = function () { if (formHelper.submitForm({ scope: $scope })) { $scope.changePasswordButtonState = 'busy'; currentUserResource.changePassword($scope.changePasswordModel.value).then(function (data) { //if the password has been reset, then update our model if (data.value) { $scope.changePasswordModel.value.generatedPassword = data.value; } formHelper.resetForm({ scope: $scope, notifications: data.notifications }); $scope.changePasswordButtonState = 'success'; }, function (err) { formHelper.handleError(err); $scope.changePasswordButtonState = 'error'; }); } }; $scope.togglePasswordFields = function () { clearPasswordFields(); $scope.showPasswordFields = !$scope.showPasswordFields; }; function clearPasswordFields() { $scope.changePasswordModel.value.newPassword = ''; $scope.changePasswordModel.confirm = ''; } }); /** * @ngdoc controller * @name Umbraco.Dialogs.LegacyDeleteController * @function * * @description * The controller for deleting content */ function YsodController($scope, legacyResource, treeService, navigationService) { if ($scope.error && $scope.error.data && $scope.error.data.StackTrace) { //trim whitespace $scope.error.data.StackTrace = $scope.error.data.StackTrace.trim(); } $scope.closeDialog = function () { $scope.dismiss(); }; } angular.module('umbraco').controller('Umbraco.Dialogs.YsodController', YsodController); (function () { 'use strict'; function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter) { var vm = this; var evts = []; vm.title = localizationService.localize('general_help'); vm.subtitle = 'Umbraco version' + ' ' + Umbraco.Sys.ServerVariables.application.version; vm.section = $routeParams.section; vm.tree = $routeParams.tree; vm.sectionName = ''; vm.customDashboard = null; vm.tours = []; vm.closeDrawer = closeDrawer; vm.startTour = startTour; vm.getTourGroupCompletedPercentage = getTourGroupCompletedPercentage; vm.showTourButton = showTourButton; function startTour(tour) { tourService.startTour(tour); closeDrawer(); } function oninit() { tourService.getGroupedTours().then(function (groupedTours) { vm.tours = groupedTours; getTourGroupCompletedPercentage(); }); // load custom help dashboard dashboardResource.getDashboard('user-help').then(function (dashboard) { vm.customDashboard = dashboard; }); if (!vm.section) { vm.section = 'content'; } setSectionName(); userService.getCurrentUser().then(function (user) { vm.userType = user.userType; vm.userLang = user.locale; vm.hasAccessToSettings = _.contains(user.allowedSections, 'settings'); evts.push(eventsService.on('appState.treeState.changed', function (e, args) { handleSectionChange(); })); findHelp(vm.section, vm.tree, vm.userType, vm.userLang); }); // check if a tour is running - if it is open the matching group var currentTour = tourService.getCurrentTour(); if (currentTour) { openTourGroup(currentTour.alias); } } function closeDrawer() { appState.setDrawerState('showDrawer', false); } function handleSectionChange() { $timeout(function () { if (vm.section !== $routeParams.section || vm.tree !== $routeParams.tree) { vm.section = $routeParams.section; vm.tree = $routeParams.tree; setSectionName(); findHelp(vm.section, vm.tree, vm.userType, vm.userLang); } }); } function findHelp(section, tree, usertype, userLang) { if (vm.hasAccessToSettings) { helpService.getContextHelpForPage(section, tree).then(function (topics) { vm.topics = topics; }); } var rq = {}; rq.section = vm.section; rq.usertype = usertype; rq.lang = userLang; if ($routeParams.url) { rq.path = decodeURIComponent($routeParams.url); if (rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0) { rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); } if (rq.path.indexOf('.aspx') > 0) { rq.path = rq.path.substring(0, rq.path.indexOf('.aspx')); } } else { rq.path = rq.section + '/' + $routeParams.tree + '/' + $routeParams.method; } if (vm.hasAccessToSettings) { helpService.findVideos(rq).then(function (videos) { vm.videos = videos; }); } } function setSectionName() { // Get section name var languageKey = 'sections_' + vm.section; localizationService.localize(languageKey).then(function (value) { vm.sectionName = value; }); } function showTourButton(index, tourGroup) { if (index !== 0) { var prevTour = tourGroup.tours[index - 1]; if (prevTour.completed) { return true; } } else { return true; } } function openTourGroup(tourAlias) { angular.forEach(vm.tours, function (group) { angular.forEach(group, function (tour) { if (tour.alias === tourAlias) { group.open = true; } }); }); } function getTourGroupCompletedPercentage() { // Finding out, how many tours are completed for the progress circle angular.forEach(vm.tours, function (group) { var completedTours = 0; angular.forEach(group.tours, function (tour) { if (tour.completed) { completedTours++; } }); group.completedPercentage = Math.round(completedTours / group.tours.length * 100); }); } evts.push(eventsService.on('appState.tour.complete', function (event, tour) { tourService.getGroupedTours().then(function (groupedTours) { vm.tours = groupedTours; openTourGroup(tour.alias); getTourGroupCompletedPercentage(); }); })); $scope.$on('$destroy', function () { for (var e in evts) { eventsService.unsubscribe(evts[e]); } }); oninit(); } angular.module('umbraco').controller('Umbraco.Drawers.Help', HelpDrawerController); }()); /** * @ngdoc controller * @name Umbraco.LegacyController * @function * * @description * A controller to control the legacy iframe injection * */ function LegacyController($scope, $routeParams, $element) { var url = decodeURIComponent($routeParams.url.replace(/javascript\:/gi, '')); //split into path and query var urlParts = url.split('?'); var extIndex = urlParts[0].lastIndexOf('.'); var ext = extIndex === -1 ? '' : urlParts[0].substr(extIndex); //path cannot be a js file if (ext !== '.js' || ext === '') { //path cannot contain any of these chars var toClean = '*(){}[];:<>\\|\'"'; for (var i = 0; i < toClean.length; i++) { var reg = new RegExp('\\' + toClean[i], 'g'); urlParts[0] = urlParts[0].replace(reg, ''); } //join cleaned path and query back together url = urlParts[0] + (urlParts.length === 1 ? '' : '?' + urlParts[1]); $scope.legacyPath = url; } else { throw 'Invalid url'; } } angular.module('umbraco').controller('Umbraco.LegacyController', LegacyController); /** This controller is simply here to launch the login dialog when the route is explicitly changed to /login */ angular.module('umbraco').controller('Umbraco.LoginController', function (eventsService, $scope, userService, $location, $rootScope) { userService._showLoginDialog(); var evtOn = eventsService.on('app.ready', function (evt, data) { $scope.avatar = 'assets/img/application/logo.png'; var path = '/'; //check if there's a returnPath query string, if so redirect to it var locationObj = $location.search(); if (locationObj.returnPath) { path = decodeURIComponent(locationObj.returnPath); } $location.url(path); }); $scope.$on('$destroy', function () { eventsService.unsubscribe(evtOn); }); }); //used for the media picker dialog angular.module('umbraco').controller('Umbraco.Notifications.ConfirmRouteChangeController', function ($scope, $location, $log, notificationsService) { $scope.discard = function (not) { not.args.listener(); $location.search(''); //we need to break the path up into path and query var parts = not.args.path.split('?'); var query = {}; if (parts.length > 1) { _.each(parts[1].split('&'), function (q) { var keyVal = q.split('='); query[keyVal[0]] = keyVal[1]; }); } $location.path(parts[0]).search(query); notificationsService.remove(not); }; $scope.stay = function (not) { notificationsService.remove(not); }; }); angular.module('umbraco').controller('Umbraco.Notifications.ConfirmUnpublishController', function ($scope, notificationsService, eventsService) { $scope.confirm = function (not, action) { eventsService.emit('content.confirmUnpublish', action); notificationsService.remove(not); }; }); (function () { 'use strict'; function CompositionsOverlay($scope, $location, $filter) { var vm = this; vm.isSelected = isSelected; vm.openContentType = openContentType; // group the content types by their container paths vm.availableGroups = $filter('orderBy')(_.map(_.groupBy($scope.model.availableCompositeContentTypes, function (compositeContentType) { return compositeContentType.contentType.metaData.containerPath; }), function (group) { return { containerPath: group[0].contentType.metaData.containerPath, compositeContentTypes: group }; }), function (group) { return group.containerPath.replace(/\//g, ' '); }); function isSelected(alias) { if ($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { return true; } } function openContentType(contentType, section) { var url = (section === 'documentType' ? '/settings/documenttypes/edit/' : '/settings/mediaTypes/edit/') + contentType.id; $location.path(url); } } angular.module('umbraco').controller('Umbraco.Overlays.CompositionsOverlay', CompositionsOverlay); }()); /** * @ngdoc controller * @name Umbraco.Editors.DocumentType.PropertyController * @function * * @description * The controller for the content type editor property dialog */ (function () { 'use strict'; function EditorPickerOverlay($scope, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService) { var vm = this; if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectEditor'); } vm.searchTerm = ''; vm.showTabs = false; vm.tabsLoaded = 0; vm.typesAndEditors = []; vm.userConfigured = []; vm.loading = false; vm.tabs = [ { active: true, id: 1, label: localizationService.localize('contentTypeEditor_availableEditors'), alias: 'Default', typesAndEditors: [] }, { active: false, id: 2, label: localizationService.localize('contentTypeEditor_reuse'), alias: 'Reuse', userConfigured: [] } ]; vm.filterItems = filterItems; vm.showDetailsOverlay = showDetailsOverlay; vm.hideDetailsOverlay = hideDetailsOverlay; vm.pickEditor = pickEditor; vm.pickDataType = pickDataType; function activate() { getGroupedDataTypes(); getGroupedPropertyEditors(); } function getGroupedPropertyEditors() { vm.loading = true; dataTypeResource.getGroupedPropertyEditors().then(function (data) { vm.tabs[0].typesAndEditors = data; vm.typesAndEditors = data; vm.tabsLoaded = vm.tabsLoaded + 1; checkIfTabContentIsLoaded(); }); } function getGroupedDataTypes() { vm.loading = true; dataTypeResource.getGroupedDataTypes().then(function (data) { vm.tabs[1].userConfigured = data; vm.userConfigured = data; vm.tabsLoaded = vm.tabsLoaded + 1; checkIfTabContentIsLoaded(); }); } function checkIfTabContentIsLoaded() { if (vm.tabsLoaded === 2) { vm.loading = false; vm.showTabs = true; } } function filterItems() { // clear item details $scope.model.itemDetails = null; if (vm.searchTerm) { vm.showFilterResult = true; vm.showTabs = false; } else { vm.showFilterResult = false; vm.showTabs = true; } } function showDetailsOverlay(property) { var propertyDetails = {}; propertyDetails.icon = property.icon; propertyDetails.title = property.name; $scope.model.itemDetails = propertyDetails; } function hideDetailsOverlay() { $scope.model.itemDetails = null; } function pickEditor(editor) { var parentId = -1; dataTypeResource.getScaffold(parentId).then(function (dataType) { // set alias dataType.selectedEditor = editor.alias; // set name var nameArray = []; if ($scope.model.contentTypeName) { nameArray.push($scope.model.contentTypeName); } if ($scope.model.property.label) { nameArray.push($scope.model.property.label); } if (editor.name) { nameArray.push(editor.name); } // make name dataType.name = nameArray.join(' - '); // get pre values dataTypeResource.getPreValues(dataType.selectedEditor).then(function (preValues) { dataType.preValues = preValues; openEditorSettingsOverlay(dataType, true); }); }); } function pickDataType(selectedDataType) { dataTypeResource.getById(selectedDataType.id).then(function (dataType) { contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function (propertyType) { submitOverlay(dataType, propertyType, false); }); }); } function openEditorSettingsOverlay(dataType, isNew) { vm.editorSettingsOverlay = { title: localizationService.localize('contentTypeEditor_editorSettings'), dataType: dataType, view: 'views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html', show: true, submit: function (model) { var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); dataTypeResource.save(model.dataType, preValues, isNew).then(function (newDataType) { contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function (propertyType) { submitOverlay(newDataType, propertyType, true); vm.editorSettingsOverlay.show = false; vm.editorSettingsOverlay = null; }); }); } }; } function submitOverlay(dataType, propertyType, isNew) { // update property $scope.model.property.config = propertyType.config; $scope.model.property.editor = propertyType.editor; $scope.model.property.view = propertyType.view; $scope.model.property.dataTypeId = dataType.id; $scope.model.property.dataTypeIcon = dataType.icon; $scope.model.property.dataTypeName = dataType.name; $scope.model.updateSameDataTypes = isNew; $scope.model.submit($scope.model); } activate(); } angular.module('umbraco').controller('Umbraco.Overlays.EditorPickerOverlay', EditorPickerOverlay); }()); /** * @ngdoc controller * @name Umbraco.Editors.DocumentType.PropertyController * @function * * @description * The controller for the content type editor property dialog */ (function () { 'use strict'; function PropertySettingsOverlay($scope, contentTypeResource, dataTypeResource, dataTypeHelper, localizationService, userService) { var vm = this; vm.showValidationPattern = false; vm.focusOnPatternField = false; vm.focusOnMandatoryField = false; vm.selectedValidationType = {}; vm.validationTypes = [ { 'name': localizationService.localize('validation_validateAsEmail'), 'key': 'email', 'pattern': '[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+', 'enableEditing': true }, { 'name': localizationService.localize('validation_validateAsNumber'), 'key': 'number', 'pattern': '^[0-9]*$', 'enableEditing': true }, { 'name': localizationService.localize('validation_validateAsUrl'), 'key': 'url', 'pattern': 'https?://[a-zA-Z0-9-.]+.[a-zA-Z]{2,}', 'enableEditing': true }, { 'name': localizationService.localize('validation_enterCustomValidation'), 'key': 'custom', 'pattern': '', 'enableEditing': true } ]; vm.changeValidationType = changeValidationType; vm.changeValidationPattern = changeValidationPattern; vm.openEditorPickerOverlay = openEditorPickerOverlay; vm.openEditorSettingsOverlay = openEditorSettingsOverlay; userService.getCurrentUser().then(function (user) { vm.showSensitiveData = user.userGroups.indexOf('sensitiveData') != -1; }); function activate() { matchValidationType(); } function changeValidationPattern() { matchValidationType(); } function openEditorPickerOverlay(property) { vm.focusOnMandatoryField = false; vm.editorPickerOverlay = {}; vm.editorPickerOverlay.property = $scope.model.property; vm.editorPickerOverlay.contentTypeName = $scope.model.contentTypeName; vm.editorPickerOverlay.view = 'views/common/overlays/contenttypeeditor/editorpicker/editorpicker.html'; vm.editorPickerOverlay.show = true; vm.editorPickerOverlay.submit = function (model) { $scope.model.updateSameDataTypes = model.updateSameDataTypes; vm.focusOnMandatoryField = true; // update property property.config = model.property.config; property.editor = model.property.editor; property.view = model.property.view; property.dataTypeId = model.property.dataTypeId; property.dataTypeIcon = model.property.dataTypeIcon; property.dataTypeName = model.property.dataTypeName; vm.editorPickerOverlay.show = false; vm.editorPickerOverlay = null; }; vm.editorPickerOverlay.close = function (model) { vm.editorPickerOverlay.show = false; vm.editorPickerOverlay = null; }; } function openEditorSettingsOverlay(property) { vm.focusOnMandatoryField = false; // get data type dataTypeResource.getById(property.dataTypeId).then(function (dataType) { vm.editorSettingsOverlay = {}; vm.editorSettingsOverlay.title = 'Editor settings'; vm.editorSettingsOverlay.view = 'views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html'; vm.editorSettingsOverlay.dataType = dataType; vm.editorSettingsOverlay.show = true; vm.editorSettingsOverlay.submit = function (model) { var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); dataTypeResource.save(model.dataType, preValues, false).then(function (newDataType) { contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function (propertyType) { // update editor property.config = propertyType.config; property.editor = propertyType.editor; property.view = propertyType.view; property.dataTypeId = newDataType.id; property.dataTypeIcon = newDataType.icon; property.dataTypeName = newDataType.name; // set flag to update same data types $scope.model.updateSameDataTypes = true; vm.focusOnMandatoryField = true; vm.editorSettingsOverlay.show = false; vm.editorSettingsOverlay = null; }); }); }; vm.editorSettingsOverlay.close = function (oldModel) { vm.editorSettingsOverlay.show = false; vm.editorSettingsOverlay = null; }; }); } function matchValidationType() { if ($scope.model.property.validation.pattern !== null && $scope.model.property.validation.pattern !== '' && $scope.model.property.validation.pattern !== undefined) { var match = false; // find and show if a match from the list has been chosen angular.forEach(vm.validationTypes, function (validationType, index) { if ($scope.model.property.validation.pattern === validationType.pattern) { vm.selectedValidationType = vm.validationTypes[index]; vm.showValidationPattern = true; match = true; } }); // if there is no match - choose the custom validation option. if (!match) { angular.forEach(vm.validationTypes, function (validationType) { if (validationType.key === 'custom') { vm.selectedValidationType = validationType; vm.showValidationPattern = true; } }); } } } function changeValidationType(selectedValidationType) { if (selectedValidationType) { $scope.model.property.validation.pattern = selectedValidationType.pattern; vm.showValidationPattern = true; // set focus on textarea if (selectedValidationType.key === 'custom') { vm.focusOnPatternField = true; } } else { $scope.model.property.validation.pattern = ''; vm.showValidationPattern = false; } } activate(); } angular.module('umbraco').controller('Umbraco.Overlay.PropertySettingsOverlay', PropertySettingsOverlay); }()); (function () { 'use strict'; function CopyOverlay($scope, localizationService, eventsService, entityHelper) { var vm = this; if (!$scope.model.title) { $scope.model.title = localizationService.localize('general_copy'); } vm.hideSearch = hideSearch; vm.selectResult = selectResult; vm.onSearchResults = onSearchResults; var dialogOptions = $scope.model; var searchText = 'Search...'; var node = dialogOptions.currentNode; localizationService.localize('general_search').then(function (value) { searchText = value + '...'; }); $scope.model.relateToOriginal = true; $scope.dialogTreeEventHandler = $({}); vm.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, results: [], selectedSearchResults: [] }; // get entity type based on the section $scope.entityType = entityHelper.getEntityTypeFromSection(dialogOptions.section); function nodeSelectHandler(ev, args) { if (args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); } //eventsService.emit("editors.content.copyController.select", args); if ($scope.model.target) { //un-select if there's a current one selected $scope.model.target.selected = false; } $scope.model.target = args.node; $scope.model.target.selected = true; } function nodeExpandedHandler(ev, args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } } function hideSearch() { vm.searchInfo.showSearch = false; vm.searchInfo.searchFromId = null; vm.searchInfo.searchFromName = null; vm.searchInfo.results = []; } // method to select a search result function selectResult(evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); } //callback when there are search results function onSearchResults(results) { vm.searchInfo.results = results; vm.searchInfo.showSearch = true; } $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; nodeSelectHandler({}, { node: node }); }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; }; function openMiniListView(node) { $scope.miniListView = node; } } angular.module('umbraco').controller('Umbraco.Overlays.CopyOverlay', CopyOverlay); }()); (function () { 'use strict'; function EmbedOverlay($scope, $http, umbRequestHelper, localizationService) { var vm = this; var origWidth = 500; var origHeight = 300; if (!$scope.model.title) { $scope.model.title = localizationService.localize('general_embed'); } $scope.model.embed = { url: '', width: 360, height: 240, constrain: true, preview: '', success: false, info: '', supportsDimensions: '' }; vm.showPreview = showPreview; vm.changeSize = changeSize; function showPreview() { if ($scope.model.embed.url) { $scope.model.embed.show = true; $scope.model.embed.preview = ''; $scope.model.embed.info = ''; $scope.model.embed.success = false; $http({ method: 'GET', url: umbRequestHelper.getApiUrl('embedApiBaseUrl', 'GetEmbed'), params: { url: $scope.model.embed.url, width: $scope.model.embed.width, height: $scope.model.embed.height } }).success(function (data) { $scope.model.embed.preview = ''; switch (data.Status) { case 0: //not supported $scope.model.embed.info = 'Not supported'; break; case 1: //error $scope.model.embed.info = 'Could not embed media - please ensure the URL is valid'; break; case 2: $scope.model.embed.preview = data.Markup; $scope.model.embed.supportsDimensions = data.SupportsDimensions; $scope.model.embed.success = true; break; } }).error(function () { $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ''; $scope.model.embed.info = 'Could not embed media - please ensure the URL is valid'; }); } else { $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ''; $scope.model.embed.info = 'Please enter a URL'; } } function changeSize(type) { var width, height; if ($scope.model.embed.constrain) { width = parseInt($scope.model.embed.width, 10); height = parseInt($scope.model.embed.height, 10); if (type == 'width') { origHeight = Math.round(width / origWidth * height); $scope.model.embed.height = origHeight; } else { origWidth = Math.round(height / origHeight * width); $scope.model.embed.width = origWidth; } } if ($scope.model.embed.url !== '') { showPreview(); } } } angular.module('umbraco').controller('Umbraco.Overlays.EmbedOverlay', EmbedOverlay); }()); angular.module('umbraco').controller('Umbraco.Overlays.HelpController', function ($scope, $location, $routeParams, helpService, userService, localizationService) { $scope.section = $routeParams.section; $scope.version = Umbraco.Sys.ServerVariables.application.version + ' assembly: ' + Umbraco.Sys.ServerVariables.application.assemblyVersion; $scope.model.subtitle = 'Umbraco version' + ' ' + $scope.version; if (!$scope.model.title) { $scope.model.title = localizationService.localize('general_help'); } if (!$scope.section) { $scope.section = 'content'; } $scope.sectionName = $scope.section; var rq = {}; rq.section = $scope.section; //translate section name localizationService.localize('sections_' + rq.section).then(function (value) { $scope.sectionName = value; }); userService.getCurrentUser().then(function (user) { rq.lang = user.locale; if ($routeParams.url) { rq.path = decodeURIComponent($routeParams.url); if (rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0) { rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); } if (rq.path.indexOf('.aspx') > 0) { rq.path = rq.path.substring(0, rq.path.indexOf('.aspx')); } } else { rq.path = rq.section + '/' + $routeParams.tree + '/' + $routeParams.method; } helpService.findHelp(rq).then(function (topics) { $scope.topics = topics; }); helpService.findVideos(rq).then(function (videos) { $scope.videos = videos; }); }); }); /** * @ngdoc controller * @name Umbraco.Editors.DocumentType.PropertyController * @function * * @description * The controller for the content type editor property dialog */ function IconPickerOverlay($scope, iconHelper, localizationService) { $scope.loading = true; $scope.model.hideSubmitButton = false; $scope.colors = [ { label: 'Black', value: 'color-black' }, { label: 'Blue Grey', value: 'color-blue-grey' }, { label: 'Grey', value: 'color-grey' }, { label: 'Brown', value: 'color-brown' }, { label: 'Blue', value: 'color-blue' }, { label: 'Light Blue', value: 'color-light-blue' }, { label: 'Indigo', value: 'color-indigo' }, { label: 'Purple', value: 'color-purple' }, { label: 'Deep Purple', value: 'color-deep-purple' }, { label: 'Cyan', value: 'color-cyan' }, { label: 'Green', value: 'color-green' }, { label: 'Light Green', value: 'color-light-green' }, { label: 'Lime', value: 'color-lime' }, { label: 'Yellow', value: 'color-yellow' }, { label: 'Amber', value: 'color-amber' }, { label: 'Orange', value: 'color-orange' }, { label: 'Deep Orange', value: 'color-deep-orange' }, { label: 'Red', value: 'color-red' }, { label: 'Pink', value: 'color-pink' } ]; if (!$scope.color) { // Set default selected color to black $scope.color = $scope.colors[0].value; } ; if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectIcon'); } ; if ($scope.model.color) { $scope.color = $scope.model.color; } ; if ($scope.model.icon) { $scope.icon = $scope.model.icon; } ; iconHelper.getIcons().then(function (icons) { $scope.icons = icons; $scope.loading = false; }); $scope.selectIcon = function (icon, color) { $scope.model.icon = icon; $scope.model.color = color; $scope.submitForm($scope.model); }; var unsubscribe = $scope.$on('formSubmitting', function () { if ($scope.color) { $scope.model.color = $scope.color; } }); //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); } angular.module('umbraco').controller('Umbraco.Overlays.IconPickerOverlay', IconPickerOverlay); (function () { 'use strict'; function InsertOverlayController($scope, localizationService) { var vm = this; if (!$scope.model.title) { $scope.model.title = localizationService.localize('template_insert'); } if (!$scope.model.subtitle) { $scope.model.subtitle = localizationService.localize('template_insertDesc'); } vm.openMacroPicker = openMacroPicker; vm.openPageFieldOverlay = openPageFieldOverlay; vm.openDictionaryItemOverlay = openDictionaryItemOverlay; vm.openPartialOverlay = openPartialOverlay; function openMacroPicker() { vm.macroPickerOverlay = { view: 'macropicker', title: localizationService.localize('template_insertMacro'), dialogData: {}, show: true, submit: function (model) { $scope.model.insert = { 'type': 'macro', 'macroParams': model.macroParams, 'selectedMacro': model.selectedMacro }; $scope.model.submit($scope.model); vm.macroPickerOverlay.show = false; vm.macroPickerOverlay = null; } }; } function openPageFieldOverlay() { vm.pageFieldOverlay = { title: localizationService.localize('template_insertPageField'), description: localizationService.localize('template_insertPageFieldDesc'), submitButtonLabel: 'Insert', closeButtonlabel: 'Cancel', view: 'insertfield', show: true, submit: function (model) { $scope.model.insert = { 'type': 'umbracoField', 'umbracoField': model.umbracoField }; $scope.model.submit($scope.model); vm.pageFieldOverlay.show = false; vm.pageFieldOverlay = null; }, close: function (model) { vm.pageFieldOverlay.show = false; vm.pageFieldOverlay = null; } }; } function openDictionaryItemOverlay() { vm.dictionaryItemOverlay = { view: 'treepicker', section: 'settings', treeAlias: 'dictionary', entityType: 'dictionary', multiPicker: false, title: localizationService.localize('template_insertDictionaryItem'), description: localizationService.localize('template_insertDictionaryItemDesc'), emptyStateMessage: localizationService.localize('emptyStates_emptyDictionaryTree'), show: true, select: function (node) { $scope.model.insert = { 'type': 'dictionary', 'node': node }; $scope.model.submit($scope.model); vm.dictionaryItemOverlay.show = false; vm.dictionaryItemOverlay = null; }, close: function (model) { vm.dictionaryItemOverlay.show = false; vm.dictionaryItemOverlay = null; } }; } function openPartialOverlay() { vm.partialItemOverlay = { view: 'treepicker', section: 'settings', treeAlias: 'partialViews', entityType: 'partialView', multiPicker: false, filter: function (i) { if (i.name.indexOf('.cshtml') === -1 && i.name.indexOf('.vbhtml') === -1) { return true; } }, filterCssClass: 'not-allowed', title: localizationService.localize('template_insertPartialView'), show: true, select: function (node) { $scope.model.insert = { 'type': 'partial', 'node': node }; $scope.model.submit($scope.model); vm.partialItemOverlay.show = false; vm.partialItemOverlay = null; }, close: function (model) { vm.partialItemOverlay.show = false; vm.partialItemOverlay = null; } }; } } angular.module('umbraco').controller('Umbraco.Overlays.InsertOverlay', InsertOverlayController); }()); (function () { 'use strict'; function InsertFieldController($scope, $http, contentTypeResource) { var vm = this; vm.field; vm.altField; vm.altText; vm.insertBefore; vm.insertAfter; vm.recursive = false; vm.properties = []; vm.standardFields = []; vm.date = false; vm.dateTime = false; vm.dateTimeSeparator = ''; vm.casingUpper = false; vm.casingLower = false; vm.encodeHtml = false; vm.encodeUrl = false; vm.convertLinebreaks = false; vm.showAltField = false; vm.showAltText = false; vm.setDateOption = setDateOption; vm.setCasingOption = setCasingOption; vm.setEncodingOption = setEncodingOption; vm.generateOutputSample = generateOutputSample; function onInit() { // set default title if (!$scope.model.title) { $scope.model.title = 'Insert value'; } // Load all fields contentTypeResource.getAllPropertyTypeAliases().then(function (array) { vm.properties = array; }); // Load all standard fields contentTypeResource.getAllStandardFields().then(function (array) { vm.standardFields = array; }); } // date formatting function setDateOption(option) { if (option === 'date') { if (vm.date) { vm.date = false; } else { vm.date = true; vm.dateTime = false; } } if (option === 'dateWithTime') { if (vm.dateTime) { vm.dateTime = false; } else { vm.date = false; vm.dateTime = true; } } } // casing formatting function setCasingOption(option) { if (option === 'uppercase') { if (vm.casingUpper) { vm.casingUpper = false; } else { vm.casingUpper = true; vm.casingLower = false; } } if (option === 'lowercase') { if (vm.casingLower) { vm.casingLower = false; } else { vm.casingUpper = false; vm.casingLower = true; } } } // encoding formatting function setEncodingOption(option) { if (option === 'html') { if (vm.encodeHtml) { vm.encodeHtml = false; } else { vm.encodeHtml = true; vm.encodeUrl = false; } } if (option === 'url') { if (vm.encodeUrl) { vm.encodeUrl = false; } else { vm.encodeHtml = false; vm.encodeUrl = true; } } } function generateOutputSample() { var pageField = (vm.field !== undefined ? '@Umbraco.Field("' + vm.field + '"' : '') + (vm.altField !== undefined ? ', altFieldAlias:"' + vm.altField + '"' : '') + (vm.altText !== undefined ? ', altText:"' + vm.altText + '"' : '') + (vm.insertBefore !== undefined ? ', insertBefore:"' + vm.insertBefore + '"' : '') + (vm.insertAfter !== undefined ? ', insertAfter:"' + vm.insertAfter + '"' : '') + (vm.recursive !== false ? ', recursive: ' + vm.recursive : '') + (vm.date !== false ? ', formatAsDate: ' + vm.date : '') + (vm.dateTime !== false ? ', formatAsDateWithTimeSeparator:"' + vm.dateTimeSeparator + '"' : '') + (vm.casingUpper !== false ? ', casing: ' + 'RenderFieldCaseType.Upper' : '') + (vm.casingLower !== false ? ', casing: ' + 'RenderFieldCaseType.Lower' : '') + (vm.encodeHtml !== false ? ', encoding: ' + 'RenderFieldEncodingType.Html' : '') + (vm.encodeUrl !== false ? ', encoding: ' + 'RenderFieldEncodingType.Url' : '') + (vm.convertLinebreaks !== false ? ', convertLineBreaks: ' + 'true' : '') + (vm.field ? ')' : ''); $scope.model.umbracoField = pageField; return pageField; } onInit(); } angular.module('umbraco').controller('Umbraco.Overlays.InsertFieldController', InsertFieldController); }()); function ItemPickerOverlay($scope, localizationService) { if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectItem'); } if (!$scope.model.orderBy) { $scope.model.orderBy = 'name'; } $scope.model.hideSubmitButton = true; $scope.selectItem = function (item) { $scope.model.selectedItem = item; $scope.submitForm($scope.model); }; } angular.module('umbraco').controller('Umbraco.Overlays.ItemPickerOverlay', ItemPickerOverlay); //used for the media picker dialog angular.module('umbraco').controller('Umbraco.Overlays.LinkPickerController', function ($scope, eventsService, dialogService, entityResource, mediaHelper, userService, localizationService, tinyMceService) { var dialogOptions = $scope.model; var searchText = 'Search...'; localizationService.localize('general_search').then(function (value) { searchText = value + '...'; }); if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectLink'); } var dataTypeId = null; if (dialogOptions && dialogOptions.dataTypeId) { dataTypeId = dialogOptions.dataTypeId; } $scope.dialogTreeEventHandler = $({}); $scope.model.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, dataTypeId: dataTypeId, results: [], selectedSearchResults: [] }; $scope.customTreeParams = dataTypeId !== null ? 'dataTypeId=' + dataTypeId : ''; $scope.showTarget = $scope.model.hideTarget !== true; if (dialogOptions.currentTarget) { // clone the current target so we don't accidentally update the caller's model while manipulating $scope.model.target $scope.model.target = angular.copy(dialogOptions.currentTarget); //if we have a node ID, we fetch the current node to build the form data if ($scope.model.target.id || $scope.model.target.udi) { //will be either a udi or an int var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; // is it a content link? if (!$scope.model.target.isMedia) { // get the content path entityResource.getPath(id, 'Document').then(function (path) { //now sync the tree to this path $scope.dialogTreeEventHandler.syncTree({ path: path, tree: 'content' }); }); entityResource.getUrlAndAnchors(id).then(function (resp) { $scope.anchorValues = resp.anchorValues; $scope.model.target.url = resp.url; }); } } else if ($scope.model.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs // only do the substring if there's a # or a ? var indexOfAnchor = $scope.model.target.url.search(/(#|\?)/); if (indexOfAnchor > -1) { // populate the anchor $scope.model.target.anchor = $scope.model.target.url.substring(indexOfAnchor); // then rewrite the model and populate the link $scope.model.target.url = $scope.model.target.url.substring(0, indexOfAnchor); } } } else if (dialogOptions.anchors) { $scope.anchorValues = dialogOptions.anchors; } function nodeSelectHandler(ev, args) { if (args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); } eventsService.emit('dialogs.linkPicker.select', args); if ($scope.currentNode) { //un-select if there's a current one selected $scope.currentNode.selected = false; } $scope.currentNode = args.node; $scope.currentNode.selected = true; $scope.model.target.id = args.node.id; $scope.model.target.udi = args.node.udi; $scope.model.target.name = args.node.name; if (args.node.id < 0) { $scope.model.target.url = '/'; } else { entityResource.getUrlAndAnchors(args.node.id).then(function (resp) { $scope.anchorValues = resp.anchorValues; $scope.model.target.url = resp.url; }); } if (!angular.isUndefined($scope.model.target.isMedia)) { delete $scope.model.target.isMedia; } } function nodeExpandedHandler(ev, args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } } $scope.switchToMediaPicker = function () { userService.getCurrentUser().then(function (userData) { var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; var startNodeIsVirtual = userData.startMediaIds.length !== 1; if (dialogOptions.ignoreUserStartNodes) { startNodeId = -1; startNodeIsVirtual = true; } $scope.mediaPickerOverlay = { view: 'mediapicker', startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, show: true, dataTypeId: dataTypeId, submit: function (model) { var media = model.selectedImages[0]; $scope.model.target.id = media.id; $scope.model.target.udi = media.udi; $scope.model.target.isMedia = true; $scope.model.target.name = media.name; $scope.model.target.url = media.image; $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; // make sure the content tree has nothing highlighted $scope.dialogTreeEventHandler.syncTree({ path: '-1', tree: 'content' }); } }; }); }; $scope.hideSearch = function () { $scope.searchInfo.showSearch = false; $scope.searchInfo.searchFromId = null; $scope.searchInfo.searchFromName = null; $scope.searchInfo.results = []; }; // method to select a search result $scope.selectResult = function (evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); }; //callback when there are search results $scope.onSearchResults = function (results) { $scope.searchInfo.results = results; $scope.searchInfo.showSearch = true; }; $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; nodeSelectHandler({}, { node: node }); }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; }; function openMiniListView(node) { $scope.miniListView = node; } }); function MacroPickerController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper, localizationService) { if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectMacro'); } $scope.macros = []; $scope.model.selectedMacro = null; $scope.model.macroParams = []; $scope.wizardStep = 'macroSelect'; $scope.noMacroParams = false; $scope.selectMacro = function (macro) { $scope.model.selectedMacro = macro; if ($scope.wizardStep === 'macroSelect') { editParams(true); } else { $scope.model.submit($scope.model); } }; /** changes the view to edit the params of the selected macro */ /** if there is pnly one macro, and it has parameters - editor can skip selecting the Macro **/ function editParams(insertIfNoParameters) { //whether to insert the macro in the rich text editor when editParams is called and there are no parameters see U4-10537 insertIfNoParameters = typeof insertIfNoParameters !== 'undefined' ? insertIfNoParameters : true; //get the macro params if there are any macroResource.getMacroParameters($scope.model.selectedMacro.id).then(function (data) { //go to next page if there are params otherwise we can just exit if (!angular.isArray(data) || data.length === 0) { if (insertIfNoParameters) { $scope.model.submit($scope.model); } else { $scope.wizardStep = 'macroSelect'; } } else { $scope.wizardStep = 'paramSelect'; $scope.model.macroParams = data; //fill in the data if we are editing this macro if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroParamsDictionary) { _.each($scope.model.dialogData.macroData.macroParamsDictionary, function (val, key) { var prop = _.find($scope.model.macroParams, function (item) { return item.alias == key; }); if (prop) { if (_.isString(val)) { //we need to unescape values as they have most likely been escaped while inserted val = _.unescape(val); //detect if it is a json string if (val.detectIsJson()) { try { //Parse it to json prop.value = angular.fromJson(val); } catch (e) { // not json prop.value = val; } } else { prop.value = val; } } else { prop.value = val; } } }); } } }); } //here we check to see if we've been passed a selected macro and if so we'll set the //editor to start with parameter editing if ($scope.model.dialogData && $scope.model.dialogData.macroData) { $scope.wizardStep = 'paramSelect'; } //get the macro list - pass in a filter if it is only for rte entityResource.getAll('Macro', $scope.model.dialogData && $scope.model.dialogData.richTextEditor && $scope.model.dialogData.richTextEditor === true ? 'UseInEditor=true' : null).then(function (data) { if (angular.isArray(data) && data.length == 0) { $scope.nomacros = true; } //if 'allowedMacros' is specified, we need to filter if (angular.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { $scope.macros = _.filter(data, function (d) { return _.contains($scope.model.dialogData.allowedMacros, d.alias); }); } else { $scope.macros = data; } //check if there's a pre-selected macro and if it exists if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroAlias) { var found = _.find(data, function (item) { return item.alias === $scope.model.dialogData.macroData.macroAlias; }); if (found) { //select the macro and go to next screen $scope.model.selectedMacro = found; editParams(true); return; } } //if there is only one macro in the site and it has parameters, let's not make the editor choose it from a selection of one macro (unless there are no parameters - then weirdly it's a better experience to make that selection) if ($scope.macros.length == 1) { $scope.model.selectedMacro = $scope.macros[0]; editParams(false); } else { //we don't have a pre-selected macro so ensure the correct step is set $scope.wizardStep = 'macroSelect'; } }); } angular.module('umbraco').controller('Umbraco.Overlays.MacroPickerController', MacroPickerController); //used for the media picker dialog angular.module('umbraco').controller('Umbraco.Overlays.MediaPickerController', function ($scope, mediaResource, umbRequestHelper, entityResource, $log, mediaHelper, mediaTypeHelper, eventsService, treeService, $element, $timeout, userService, $cookies, localStorageService, localizationService) { if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectMedia'); } var dialogOptions = $scope.model; $scope.disableFolderSelect = dialogOptions.disableFolderSelect; $scope.onlyImages = dialogOptions.onlyImages; $scope.showDetails = dialogOptions.showDetails; $scope.multiPicker = dialogOptions.multiPicker && dialogOptions.multiPicker !== '0' ? true : false; $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; $scope.cropSize = dialogOptions.cropSize; $scope.lastOpenedNode = localStorageService.get('umbLastOpenedMediaNodeId'); $scope.lockedFolder = true; var userStartNodes = []; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); if ($scope.onlyImages) { $scope.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); } else { // Use whitelist of allowed file types if provided if (allowedUploadFiles !== '') { $scope.acceptedFileTypes = allowedUploadFiles; } else { // If no whitelist, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles $scope.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); } } $scope.maxFileSize = umbracoSettings.maxFileSize + 'KB'; $scope.model.selectedImages = []; $scope.acceptedMediatypes = []; mediaTypeHelper.getAllowedImagetypes($scope.startNodeId).then(function (types) { $scope.acceptedMediatypes = types; }); var dataTypeId = null; if ($scope.model && $scope.model.dataTypeId) { dataTypeId = $scope.model.dataTypeId; } $scope.searchOptions = { pageNumber: 1, pageSize: 100, totalItems: 0, totalPages: 0, filter: '', dataTypeId: dataTypeId }; //preload selected item $scope.target = undefined; if (dialogOptions.currentTarget) { $scope.target = dialogOptions.currentTarget; } function onInit() { userService.getCurrentUser().then(function (userData) { userStartNodes = userData.startMediaIds; if ($scope.startNodeId !== -1) { entityResource.getById($scope.startNodeId, 'media').then(function (ent) { $scope.startNodeId = ent.id; run(); }); } else { run(); } }); } function run() { //default root item if (!$scope.target) { if ($scope.lastOpenedNode && $scope.lastOpenedNode !== -1) { entityResource.getById($scope.lastOpenedNode, 'media').then(ensureWithinStartNode, gotoStartNode); } else { gotoStartNode(); } } else { //if a target is specified, go look it up - generally this target will just contain ids not the actual full //media object so we need to look it up var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; var altText = $scope.target.altText; if (id) { entityResource.getById(id, 'Media').then(function (node) { $scope.target = node; if (ensureWithinStartNode(node)) { selectImage(node); $scope.target.url = mediaHelper.resolveFile(node); $scope.target.altText = altText; $scope.openDetailsDialog(); } }, gotoStartNode); } else { gotoStartNode(); } } } $scope.upload = function (v) { angular.element('.umb-file-dropzone-directive .file-select').click(); }; $scope.dragLeave = function (el, event) { $scope.activeDrag = false; }; $scope.dragEnter = function (el, event) { $scope.activeDrag = true; }; $scope.submitFolder = function () { if ($scope.newFolderName) { $scope.creatingFolder = true; mediaResource.addFolder($scope.newFolderName, $scope.currentFolder.id).then(function (data) { //we've added a new folder so lets clear the tree cache for that specific item treeService.clearCache({ cacheKey: '__media', //this is the main media tree cache key childrenOf: data.parentId }); $scope.creatingFolder = false; $scope.gotoFolder(data); $scope.showFolderInput = false; $scope.newFolderName = ''; }); } else { $scope.showFolderInput = false; } }; $scope.enterSubmitFolder = function (event) { if (event.keyCode === 13) { $scope.submitFolder(); event.stopPropagation(); } }; $scope.gotoFolder = function (folder) { if (!$scope.multiPicker) { deselectAllImages($scope.model.selectedImages); } if (!folder) { folder = { id: -1, name: 'Media', icon: 'icon-folder' }; } if (folder.id > 0) { entityResource.getAncestors(folder.id, 'media', { dataTypeId: dataTypeId }).then(function (anc) { $scope.path = _.filter(anc, function (f) { return f.path.indexOf($scope.startNodeId) !== -1; }); }); } else { $scope.path = []; } mediaTypeHelper.getAllowedImagetypes(folder.id).then(function (types) { $scope.acceptedMediatypes = types; }); $scope.lockedFolder = folder.id === -1 && $scope.model.startNodeIsVirtual || hasFolderAccess(folder) === false; $scope.currentFolder = folder; localStorageService.set('umbLastOpenedMediaNodeId', folder.id); return getChildren(folder.id); }; $scope.clickHandler = function (image, event, index) { if (image.isFolder) { if ($scope.disableFolderSelect) { $scope.gotoFolder(image); } else { eventsService.emit('dialogs.mediaPicker.select', image); selectImage(image); } } else { eventsService.emit('dialogs.mediaPicker.select', image); if ($scope.showDetails) { $scope.target = image; // handle both entity and full media object if (image.image) { $scope.target.url = image.image; } else { $scope.target.url = mediaHelper.resolveFile(image); } $scope.openDetailsDialog(); } else { selectImage(image); } } }; $scope.clickItemName = function (item) { if (item.isFolder) { $scope.gotoFolder(item); } }; function selectImage(image) { if (image.selected) { for (var i = 0; $scope.model.selectedImages.length > i; i++) { var imageInSelection = $scope.model.selectedImages[i]; if (image.key === imageInSelection.key) { image.selected = false; $scope.model.selectedImages.splice(i, 1); } } } else { if (!$scope.multiPicker) { deselectAllImages($scope.model.selectedImages); } image.selected = true; $scope.model.selectedImages.push(image); } } function deselectAllImages(images) { for (var i = 0; i < images.length; i++) { var image = images[i]; image.selected = false; } images.length = 0; } $scope.onUploadComplete = function (files) { $scope.gotoFolder($scope.currentFolder).then(function () { if (files.length === 1 && $scope.model.selectedImages.length === 0) { var image = $scope.images[$scope.images.length - 1]; $scope.target = image; // handle both entity and full media object if (image.image) { $scope.target.url = image.image; } else { $scope.target.url = mediaHelper.resolveFile(image); } selectImage(image); } }); }; $scope.onFilesQueue = function () { $scope.activeDrag = false; }; function ensureWithinStartNode(node) { // make sure that last opened node is on the same path as start node var nodePath = node.path.split(','); // also make sure the node is not trashed if (nodePath.indexOf($scope.startNodeId.toString()) !== -1 && node.trashed === false) { $scope.gotoFolder({ id: $scope.lastOpenedNode, name: 'Media', icon: 'icon-folder', path: node.path }); return true; } else { $scope.gotoFolder({ id: $scope.startNodeId, name: 'Media', icon: 'icon-folder' }); return false; } } function hasFolderAccess(node) { var nodePath = node.path ? node.path.split(',') : [node.id]; for (var i = 0; i < nodePath.length; i++) { if (userStartNodes.indexOf(parseInt(nodePath[i])) !== -1) return true; } return false; } function gotoStartNode(err) { $scope.gotoFolder({ id: $scope.startNodeId, name: 'Media', icon: 'icon-folder' }); } $scope.openDetailsDialog = function () { $scope.mediaPickerDetailsOverlay = {}; $scope.mediaPickerDetailsOverlay.show = true; $scope.mediaPickerDetailsOverlay.submit = function (model) { $scope.model.selectedImages.push($scope.target); $scope.model.submit($scope.model); $scope.mediaPickerDetailsOverlay.show = false; $scope.mediaPickerDetailsOverlay = null; }; $scope.mediaPickerDetailsOverlay.close = function (oldModel) { $scope.mediaPickerDetailsOverlay.show = false; $scope.mediaPickerDetailsOverlay = null; }; }; var debounceSearchMedia = _.debounce(function () { $scope.$apply(function () { if ($scope.searchOptions.filter) { searchMedia(); } else { // reset pagination $scope.searchOptions = { pageNumber: 1, pageSize: 100, totalItems: 0, totalPages: 0, filter: '', dataTypeId: dataTypeId }; getChildren($scope.currentFolder.id); } }); }, 500); $scope.changeSearch = function () { $scope.loading = true; debounceSearchMedia(); }; $scope.toggle = function () { // Make sure to activate the changeSearch function everytime the toggle is clicked $scope.changeSearch(); }; $scope.changePagination = function (pageNumber) { $scope.loading = true; $scope.searchOptions.pageNumber = pageNumber; searchMedia(); }; function searchMedia() { $scope.loading = true; entityResource.getPagedDescendants($scope.currentFolder.id, 'Media', $scope.searchOptions).then(function (data) { // update image data to work with image grid angular.forEach(data.items, function (mediaItem) { // set thumbnail and src mediaItem.thumbnail = mediaHelper.resolveFileFromEntity(mediaItem, true); mediaItem.image = mediaHelper.resolveFileFromEntity(mediaItem, false); // set properties to match a media object mediaItem.properties = []; if (mediaItem.metaData) { if (mediaItem.metaData.umbracoWidth && mediaItem.metaData.umbracoHeight) { mediaItem.properties.push({ alias: 'umbracoWidth', value: mediaItem.metaData.umbracoWidth.Value }); mediaItem.properties.push({ alias: 'umbracoHeight', value: mediaItem.metaData.umbracoHeight.Value }); } if (mediaItem.metaData.umbracoFile) { mediaItem.properties.push({ alias: 'umbracoFile', editor: mediaItem.metaData.umbracoFile.PropertyEditorAlias, value: mediaItem.metaData.umbracoFile.Value }); } } }); // update images $scope.images = data.items ? data.items : []; // update pagination if (data.pageNumber > 0) $scope.searchOptions.pageNumber = data.pageNumber; if (data.pageSize > 0) $scope.searchOptions.pageSize = data.pageSize; $scope.searchOptions.totalItems = data.totalItems; $scope.searchOptions.totalPages = data.totalPages; // set already selected images to selected preSelectImages(); $scope.loading = false; }); } function getChildren(id) { $scope.loading = true; return entityResource.getChildren(id, 'Media', $scope.searchOptions).then(function (data) { for (var i = 0; i < data.length; i++) { if (data[i].metaData.MediaPath !== null) { data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); } } $scope.searchOptions.filter = ''; $scope.images = data ? data : []; // set already selected images to selected preSelectImages(); $scope.loading = false; }); } function preSelectImages() { for (var folderImageIndex = 0; folderImageIndex < $scope.images.length; folderImageIndex++) { var folderImage = $scope.images[folderImageIndex]; var imageIsSelected = false; if ($scope.model && angular.isArray($scope.model.selectedImages)) { for (var selectedImageIndex = 0; selectedImageIndex < $scope.model.selectedImages.length; selectedImageIndex++) { var selectedImage = $scope.model.selectedImages[selectedImageIndex]; if (folderImage.key === selectedImage.key) { imageIsSelected = true; } } } if (imageIsSelected) { folderImage.selected = true; } } } onInit(); }); angular.module('umbraco').controller('Umbraco.Overlays.MediaTypePickerController', function ($scope) { $scope.select = function (mediatype) { $scope.model.selectedType = mediatype; $scope.model.submit($scope.model); $scope.model.show = false; }; }); //used for the member picker dialog angular.module('umbraco').controller('Umbraco.Overlays.MemberGroupPickerController', function ($scope, eventsService, entityResource, searchService, $log, localizationService) { if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectMemberGroup'); } $scope.dialogTreeEventHandler = $({}); $scope.multiPicker = $scope.model.multiPicker; function activate() { if ($scope.multiPicker) { $scope.model.selectedMemberGroups = []; } else { $scope.model.selectedMemberGroup = ''; } } function selectMemberGroup(id) { $scope.model.selectedMemberGroup = id; } function selectMemberGroups(id) { var index = $scope.model.selectedMemberGroups.indexOf(id); if (index === -1) { // If the id does not exists in the array then add it $scope.model.selectedMemberGroups.push(id); } else { // Otherwise we will remove it from the array instead $scope.model.selectedMemberGroups.splice(index, 1); } } /** Method used for selecting a node */ function select(text, id) { if ($scope.model.multiPicker) { selectMemberGroups(id); } else { selectMemberGroup(id); $scope.model.submit($scope.model); } } function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); eventsService.emit('dialogs.memberGroupPicker.select', args); //This is a tree node, so we don't have an entity to pass in, it will need to be looked up //from the server in this method. select(args.node.name, args.node.id); //toggle checked state args.node.selected = args.node.selected === true ? false : true; } $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); activate(); }); (function () { 'use strict'; function MoveOverlay($scope, localizationService, eventsService, entityHelper) { var vm = this; vm.hideSearch = hideSearch; vm.selectResult = selectResult; vm.onSearchResults = onSearchResults; var dialogOptions = $scope.model; var searchText = 'Search...'; var node = dialogOptions.currentNode; localizationService.localize('general_search').then(function (value) { searchText = value + '...'; }); if (!$scope.model.title) { $scope.model.title = localizationService.localize('actions_move'); } $scope.model.relateToOriginal = true; $scope.dialogTreeEventHandler = $({}); vm.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, results: [], selectedSearchResults: [] }; // get entity type based on the section $scope.entityType = entityHelper.getEntityTypeFromSection(dialogOptions.section); function nodeSelectHandler(ev, args) { if (args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); } //eventsService.emit("editors.content.copyController.select", args); if ($scope.model.target) { //un-select if there's a current one selected $scope.model.target.selected = false; } $scope.model.target = args.node; $scope.model.target.selected = true; } function nodeExpandedHandler(ev, args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } } function hideSearch() { vm.searchInfo.showSearch = false; vm.searchInfo.searchFromId = null; vm.searchInfo.searchFromName = null; vm.searchInfo.results = []; } // method to select a search result function selectResult(evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); } //callback when there are search results function onSearchResults(results) { vm.searchInfo.results = results; vm.searchInfo.showSearch = true; } $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; nodeSelectHandler({}, { node: node }); }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; }; function openMiniListView(node) { $scope.miniListView = node; } } angular.module('umbraco').controller('Umbraco.Overlays.MoveOverlay', MoveOverlay); }()); (function () { 'use strict'; function NodePermissionsController($scope, localizationService) { var vm = this; function onInit() { // set default title if (!$scope.model.title) { localizationService.localize('defaultdialogs_permissionsEdit').then(function (value) { $scope.model.title = value + ' ' + $scope.model.node.name; }); } } onInit(); } angular.module('umbraco').controller('Umbraco.Overlays.NodePermissionsController', NodePermissionsController); }()); (function () { 'use strict'; function QueryBuilderOverlayController($scope, templateQueryResource, localizationService) { var everything = ''; var myWebsite = ''; var ascendingTranslation = ''; var descendingTranslation = ''; var vm = this; vm.properties = []; vm.contentTypes = []; vm.conditions = []; vm.datePickerConfig = { pickDate: true, pickTime: false, format: 'YYYY-MM-DD' }; vm.chooseSource = chooseSource; vm.getPropertyOperators = getPropertyOperators; vm.addFilter = addFilter; vm.trashFilter = trashFilter; vm.changeSortOrder = changeSortOrder; vm.setSortProperty = setSortProperty; vm.setContentType = setContentType; vm.setFilterProperty = setFilterProperty; vm.setFilterTerm = setFilterTerm; vm.changeConstraintValue = changeConstraintValue; vm.datePickerChange = datePickerChange; function onInit() { vm.query = { contentType: { name: everything }, source: { name: myWebsite }, filters: [{ property: undefined, operator: undefined }], sort: { property: { alias: '', name: '' }, direction: 'ascending', //This is the value for sorting sent to server translation: { currentLabel: ascendingTranslation, //This is the localized UI value in the the dialog ascending: ascendingTranslation, descending: descendingTranslation } } }; templateQueryResource.getAllowedProperties().then(function (properties) { vm.properties = properties; }); templateQueryResource.getContentTypes().then(function (contentTypes) { vm.contentTypes = contentTypes; }); templateQueryResource.getFilterConditions().then(function (conditions) { vm.conditions = conditions; }); throttledFunc(); } function chooseSource(query) { vm.contentPickerOverlay = { view: 'contentpicker', show: true, submit: function (model) { var selectedNodeId = model.selection[0].id; var selectedNodeName = model.selection[0].name; if (selectedNodeId > 0) { query.source = { id: selectedNodeId, name: selectedNodeName }; } else { query.source.name = myWebsite; delete query.source.id; } throttledFunc(); vm.contentPickerOverlay.show = false; vm.contentPickerOverlay = null; }, close: function (oldModel) { vm.contentPickerOverlay.show = false; vm.contentPickerOverlay = null; } }; } function getPropertyOperators(property) { var conditions = _.filter(vm.conditions, function (condition) { var index = condition.appliesTo.indexOf(property.type); return index >= 0; }); return conditions; } function addFilter(query) { query.filters.push({}); } function trashFilter(query, filter) { for (var i = 0; i < query.filters.length; i++) { if (query.filters[i] == filter) { query.filters.splice(i, 1); } } //if we remove the last one, add a new one to generate ui for it. if (query.filters.length == 0) { query.filters.push({}); } } function changeSortOrder(query) { if (query.sort.direction === 'ascending') { query.sort.direction = 'descending'; query.sort.translation.currentLabel = query.sort.translation.descending; } else { query.sort.direction = 'ascending'; query.sort.translation.currentLabel = query.sort.translation.ascending; } throttledFunc(); } function setSortProperty(query, property) { query.sort.property = property; if (property.type === 'datetime') { query.sort.direction = 'descending'; query.sort.translation.currentLabel = query.sort.translation.descending; } else { query.sort.direction = 'ascending'; query.sort.translation.currentLabel = query.sort.translation.ascending; } throttledFunc(); } function setContentType(contentType) { vm.query.contentType = contentType; throttledFunc(); } function setFilterProperty(filter, property) { filter.property = property; filter.term = {}; filter.constraintValue = ''; } function setFilterTerm(filter, term) { filter.term = term; if (filter.constraintValue) { throttledFunc(); } } function changeConstraintValue() { throttledFunc(); } function datePickerChange(event, filter) { if (event.date && event.date.isValid()) { filter.constraintValue = event.date.format(vm.datePickerConfig.format); throttledFunc(); } } var throttledFunc = _.throttle(function () { templateQueryResource.postTemplateQuery(vm.query).then(function (response) { $scope.model.result = response; }); }, 200); localizationService.localizeMany([ 'template_allContent', 'template_websiteRoot', 'template_ascending', 'template_descending' ]).then(function (res) { everything = res[0]; myWebsite = res[1]; ascendingTranslation = res[2]; descendingTranslation = res[3]; onInit(); }); } angular.module('umbraco').controller('Umbraco.Overlays.QueryBuilderController', QueryBuilderOverlayController); }()); (function () { 'use strict'; function SectionPickerController($scope, sectionResource, localizationService) { var vm = this; vm.sections = []; vm.loading = false; vm.selectSection = selectSection; ////////// function onInit() { vm.loading = true; // set default title if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectSections'); } // make sure we can push to something if (!$scope.model.selection) { $scope.model.selection = []; } // get sections sectionResource.getAllSections().then(function (sections) { vm.sections = sections; setSectionIcon(vm.sections); if ($scope.model.selection && $scope.model.selection.length > 0) { preSelect($scope.model.selection); } vm.loading = false; }); } function preSelect(selection) { angular.forEach(selection, function (selected) { angular.forEach(vm.sections, function (section) { if (selected.alias === section.alias) { section.selected = true; } }); }); } function selectSection(section) { if (!section.selected) { section.selected = true; $scope.model.selection.push(section); } else { angular.forEach($scope.model.selection, function (selectedSection, index) { if (selectedSection.alias === section.alias) { section.selected = false; $scope.model.selection.splice(index, 1); } }); } } function setSectionIcon(sections) { angular.forEach(sections, function (section) { section.icon = 'icon-section ' + section.cssclass; }); } onInit(); } angular.module('umbraco').controller('Umbraco.Overlays.SectionPickerController', SectionPickerController); }()); (function () { 'use strict'; function TemplateSectionsOverlayController($scope) { var vm = this; $scope.model.mandatoryRenderSection = false; if (!$scope.model.title) { $scope.model.title = 'Sections'; } vm.select = select; function onInit() { if ($scope.model.hasMaster) { $scope.model.insertType = 'addSection'; } else { $scope.model.insertType = 'renderBody'; } } function select(type) { $scope.model.insertType = type; } onInit(); } angular.module('umbraco').controller('Umbraco.Overlays.TemplateSectionsOverlay', TemplateSectionsOverlayController); }()); //used for the media picker dialog angular.module('umbraco').controller('Umbraco.Overlays.TreePickerController', function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { var tree = null; var dialogOptions = $scope.model; $scope.treeReady = false; $scope.dialogTreeEventHandler = $({}); $scope.section = dialogOptions.section; $scope.treeAlias = dialogOptions.treeAlias; $scope.multiPicker = dialogOptions.multiPicker; $scope.hideHeader = typeof dialogOptions.hideHeader === 'boolean' ? dialogOptions.hideHeader : true; // if you need to load a not initialized tree set this value to false - default is true $scope.onlyInitialized = dialogOptions.onlyInitialized; $scope.searchInfo = { searchFromId: dialogOptions.startNodeId, searchFromName: null, showSearch: false, dataTypeId: dialogOptions && dialogOptions.dataTypeId ? dialogOptions.dataTypeId : null, results: [], selectedSearchResults: [] }; $scope.model.selection = []; //Used for toggling an empty-state message //Some trees can have no items (dictionary & forms email templates) $scope.hasItems = true; $scope.emptyStateMessage = dialogOptions.emptyStateMessage; var node = dialogOptions.currentNode; //This is called from ng-init //it turns out it is called from the angular html : / Have a look at views/common / overlays / contentpicker / contentpicker.html you'll see ng-init. //this is probably an anti pattern IMO and shouldn't be used $scope.init = function (contentType) { if (contentType === 'content') { $scope.entityType = 'Document'; if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectContent'); } } else if (contentType === 'member') { $scope.entityType = 'Member'; if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectMember'); } } else if (contentType === 'media') { $scope.entityType = 'Media'; if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectMedia'); } } }; var searchText = 'Search...'; localizationService.localize('general_search').then(function (value) { searchText = value + '...'; }); // Allow the entity type to be passed in but defaults to Document for backwards compatibility. $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : 'Document'; //min / max values if (dialogOptions.minNumber) { dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); } if (dialogOptions.maxNumber) { dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); } if (dialogOptions.section === 'member') { $scope.entityType = 'Member'; } else if (dialogOptions.section === 'media') { $scope.entityType = 'Media'; } // Search and listviews is only working for content, media and member section var searchableSections = [ 'content', 'media', 'member' ]; $scope.enableSearh = searchableSections.indexOf($scope.section) !== -1; //if a alternative startnode is used, we need to check if it is a container if ($scope.enableSearh && dialogOptions.startNodeId && dialogOptions.startNodeId !== -1 && dialogOptions.startNodeId !== '-1') { entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function (node) { if (node.metaData.IsContainer) { openMiniListView(node); } initTree(); }); } else { initTree(); } //Configures filtering if (dialogOptions.filter) { dialogOptions.filterExclude = false; dialogOptions.filterAdvanced = false; //used advanced filtering if (angular.isFunction(dialogOptions.filter)) { dialogOptions.filterAdvanced = true; } else if (angular.isObject(dialogOptions.filter)) { dialogOptions.filterAdvanced = true; } else { if (dialogOptions.filter.startsWith('!')) { dialogOptions.filterExclude = true; dialogOptions.filterTypes = dialogOptions.filter.substring(1); } else { dialogOptions.filterExclude = false; dialogOptions.filterTypes = dialogOptions.filter; } //used advanced filtering if (dialogOptions.filter.startsWith('{')) { dialogOptions.filterAdvanced = true; //convert to object dialogOptions.filter = angular.fromJson(dialogOptions.filter); } } $scope.filter = { filterAdvanced: dialogOptions.filterAdvanced, filterExclude: dialogOptions.filterExclude, filter: dialogOptions.filterTypes }; } function initTree() { //create the custom query string param for this tree var params = []; if (dialogOptions.startNodeId) params.push('startNodeId=' + dialogOptions.startNodeId); if (dialogOptions.dataTypeId) params.push('dataTypeId=' + dialogOptions.dataTypeId); if (dialogOptions.customTreeParams) params.push(dialogOptions.customTreeParams); $scope.customTreeParams = params.join('&'); $scope.treeReady = true; } function nodeExpandedHandler(ev, args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } if (angular.isArray(args.children)) { //iterate children _.each(args.children, function (child) { //now we need to look in the already selected search results and // toggle the check boxes for those ones that are listed var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { return child.id == selected.id; }); if (exists) { child.selected = true; } }); //check filter performFiltering(args.children); } } //gets the tree object when it loads function treeLoadedHandler(ev, args) { //args.tree contains children (args.tree.root.children) $scope.hasItems = args.tree.root.children.length > 0; tree = args.tree; var nodeHasPath = typeof node !== 'undefined' && typeof node.path !== 'undefined'; var startNodeNotDefined = typeof dialogOptions.startNodeId === 'undefined' || dialogOptions.startNodeId === '' || dialogOptions.startNodeId === '-1'; if (startNodeNotDefined && nodeHasPath) { $scope.dialogTreeEventHandler.syncTree({ path: node.path, activate: false }); } } //wires up selection function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); if (args.node.metaData.isSearchResult) { //check if the item selected was a search result from a list view //unselect select(args.node.name, args.node.id); //remove it from the list view children var listView = args.node.parent(); listView.children = _.reject(listView.children, function (child) { return child.id == args.node.id; }); //remove it from the custom tracked search result list $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { return i.id == args.node.id; }); } else { eventsService.emit('dialogs.treePickerController.select', args); if (args.node.filtered) { return; } //This is a tree node, so we don't have an entity to pass in, it will need to be looked up //from the server in this method. if ($scope.model.select) { $scope.model.select(args.node); } else { select(args.node.name, args.node.id); //toggle checked state args.node.selected = args.node.selected === true ? false : true; } } } /** Method used for selecting a node */ function select(text, id, entity) { //if we get the root, we just return a constructed entity, no need for server data if (id < 0) { var rootNode = { alias: null, icon: 'icon-folder', id: id, name: text }; if ($scope.multiPicker) { if (entity) { multiSelectItem(entity); } else { multiSelectItem(rootNode); } } else { $scope.model.selection.push(rootNode); $scope.model.submit($scope.model); } } else { if ($scope.multiPicker) { if (entity) { multiSelectItem(entity); } else { //otherwise we have to get it from the server entityResource.getById(id, $scope.entityType).then(function (ent) { multiSelectItem(ent); }); } } else { $scope.hideSearch(); //if an entity has been passed in, use it if (entity) { $scope.model.selection.push(entity); $scope.model.submit($scope.model); } else { //otherwise we have to get it from the server entityResource.getById(id, $scope.entityType).then(function (ent) { $scope.model.selection.push(ent); $scope.model.submit($scope.model); }); } } } } function multiSelectItem(item) { var found = false; var foundIndex = 0; if ($scope.model.selection.length > 0) { for (i = 0; $scope.model.selection.length > i; i++) { var selectedItem = $scope.model.selection[i]; if (selectedItem.id === item.id) { found = true; foundIndex = i; } } } if (found) { $scope.model.selection.splice(foundIndex, 1); } else { $scope.model.selection.push(item); } } function performFiltering(nodes) { if (!dialogOptions.filter) { return; } //remove any list view search nodes from being filtered since these are special nodes that always must // be allowed to be clicked on nodes = _.filter(nodes, function (n) { return !angular.isObject(n.metaData.listViewNode); }); if (dialogOptions.filterAdvanced) { //filter either based on a method or an object var filtered = angular.isFunction(dialogOptions.filter) ? _.filter(nodes, dialogOptions.filter) : _.where(nodes, dialogOptions.filter); angular.forEach(filtered, function (value, key) { value.filtered = true; if (dialogOptions.filterCssClass) { if (!value.cssClasses) { value.cssClasses = []; } value.cssClasses.push(dialogOptions.filterCssClass); } }); } else { var a = dialogOptions.filterTypes.toLowerCase().replace(/\s/g, '').split(','); angular.forEach(nodes, function (value, key) { var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { value.filtered = true; if (dialogOptions.filterCssClass) { if (!value.cssClasses) { value.cssClasses = []; } value.cssClasses.push(dialogOptions.filterCssClass); } } }); } } $scope.multiSubmit = function (result) { entityResource.getByIds(result, $scope.entityType).then(function (ents) { $scope.submit(ents); }); }; /** method to select a search result */ $scope.selectResult = function (evt, result) { if (result.filtered) { return; } result.selected = result.selected === true ? false : true; //since result = an entity, we'll pass it in so we don't have to go back to the server select(result.name, result.id, result); //add/remove to our custom tracked list of selected search results if (result.selected) { $scope.searchInfo.selectedSearchResults.push(result); } else { $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { return i.id == result.id; }); } //ensure the tree node in the tree is checked/unchecked if it already exists there if (tree) { var found = treeService.getDescendantNode(tree.root, result.id); if (found) { found.selected = result.selected; } } }; $scope.hideSearch = function () { //Traverse the entire displayed tree and update each node to sync with the selected search results if (tree) { //we need to ensure that any currently displayed nodes that get selected // from the search get updated to have a check box! function checkChildren(children) { _.each(children, function (child) { //check if the id is in the selection, if so ensure it's flagged as selected var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { return child.id == selected.id; }); //if the curr node exists in selected search results, ensure it's checked if (exists) { child.selected = true; } //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result else if (child.metaData.isSearchResult) { //if this tree node is under a list view it means that the node was added // to the tree dynamically under the list view that was searched, so we actually want to remove // it all together from the tree var listView = child.parent(); listView.children = _.reject(listView.children, function (c) { return c.id == child.id; }); } //check if the current node is a list view and if so, check if there's any new results // that need to be added as child nodes to it based on search results selected if (child.metaData.isContainer) { child.cssClasses = _.reject(child.cssClasses, function (c) { return c === 'tree-node-slide-up-hide-active'; }); var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { return i.parentId == child.id; }); _.each(listViewResults, function (item) { var childExists = _.find(child.children, function (c) { return c.id == item.id; }); if (!childExists) { var parent = child; child.children.unshift({ id: item.id, name: item.name, cssClass: 'icon umb-tree-icon sprTree ' + item.icon, level: child.level + 1, metaData: { isSearchResult: true }, hasChildren: false, parent: function () { return parent; } }); } }); } //recurse if (child.children && child.children.length > 0) { checkChildren(child.children); } }); } checkChildren(tree.root.children); } $scope.searchInfo.showSearch = false; $scope.searchInfo.searchFromId = dialogOptions.startNodeId; $scope.searchInfo.searchFromName = null; $scope.searchInfo.results = []; }; $scope.onSearchResults = function (results) { //filter all items - this will mark an item as filtered performFiltering(results); //now actually remove all filtered items so they are not even displayed results = _.filter(results, function (item) { return !item.filtered; }); $scope.searchInfo.results = results; //sync with the curr selected results _.each($scope.searchInfo.results, function (result) { var exists = _.find($scope.model.selection, function (selectedId) { return result.id == selectedId; }); if (exists) { result.selected = true; } }); $scope.searchInfo.showSearch = true; }; $scope.dialogTreeEventHandler.bind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); $scope.selectListViewNode = function (node) { select(node.name, node.id); //toggle checked state node.selected = node.selected === true ? false : true; }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; }; function openMiniListView(node) { $scope.miniListView = node; } }); angular.module('umbraco').controller('Umbraco.Overlays.UserController', function ($scope, $location, $timeout, dashboardResource, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper, localizationService) { $scope.history = historyService.getCurrent(); $scope.version = Umbraco.Sys.ServerVariables.application.version + ' assembly: ' + Umbraco.Sys.ServerVariables.application.assemblyVersion; $scope.showPasswordFields = false; $scope.changePasswordButtonState = 'init'; $scope.model.subtitle = 'Umbraco version' + ' ' + $scope.version; if (!$scope.model.title) { $scope.model.title = localizationService.localize('general_user'); } $scope.externalLoginProviders = externalLoginInfo.providers; $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; var evts = []; evts.push(eventsService.on('historyService.add', function (e, args) { $scope.history = args.all; })); evts.push(eventsService.on('historyService.remove', function (e, args) { $scope.history = args.all; })); evts.push(eventsService.on('historyService.removeAll', function (e, args) { $scope.history = []; })); $scope.logout = function () { //Add event listener for when there are pending changes on an editor which means our route was not successful var pendingChangeEvent = eventsService.on('valFormManager.pendingChanges', function (e, args) { //one time listener, remove the event pendingChangeEvent(); $scope.model.close(); }); //perform the path change, if it is successful then the promise will resolve otherwise it will fail $scope.model.close(); $location.path('/logout'); }; $scope.gotoHistory = function (link) { $location.path(link); $scope.model.close(); }; //Manually update the remaining timeout seconds function updateTimeout() { $timeout(function () { if ($scope.remainingAuthSeconds > 0) { $scope.remainingAuthSeconds--; $scope.$digest(); //recurse updateTimeout(); } }, 1000, false); // 1 second, do NOT execute a global digest } function updateUserInfo() { //get the user userService.getCurrentUser().then(function (user) { $scope.user = user; if ($scope.user) { $scope.model.title = user.name; $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; $scope.canEditProfile = _.indexOf($scope.user.allowedSections, 'users') > -1; //set the timer updateTimeout(); authResource.getCurrentUserLinkedLogins().then(function (logins) { //reset all to be un-linked for (var provider in $scope.externalLoginProviders) { $scope.externalLoginProviders[provider].linkedProviderKey = undefined; } //set the linked logins for (var login in logins) { var found = _.find($scope.externalLoginProviders, function (i) { return i.authType == login; }); if (found) { found.linkedProviderKey = logins[login]; } } }); } }); } $scope.unlink = function (e, loginProvider, providerKey) { var result = confirm('Are you sure you want to unlink this account?'); if (!result) { e.preventDefault(); return; } authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { updateUserInfo(); }); }; updateUserInfo(); //remove all event handlers $scope.$on('$destroy', function () { for (var e = 0; e < evts.length; e++) { evts[e](); } }); /* ---------- UPDATE PASSWORD ---------- */ //create the initial model for change password $scope.changePasswordModel = { config: {}, value: {} }; //go get the config for the membership provider and add it to the model authResource.getMembershipProviderConfig().then(function (data) { $scope.changePasswordModel.config = data; //ensure the hasPassword config option is set to true (the user of course has a password already assigned) //this will ensure the oldPassword is shown so they can change it // disable reset password functionality beacuse it does not make sense inside the backoffice $scope.changePasswordModel.config.hasPassword = true; $scope.changePasswordModel.config.disableToggle = true; $scope.changePasswordModel.config.enableReset = false; }); $scope.changePassword = function () { if (formHelper.submitForm({ scope: $scope })) { $scope.changePasswordButtonState = 'busy'; currentUserResource.changePassword($scope.changePasswordModel.value).then(function (data) { //reset old data clearPasswordFields(); //if the password has been reset, then update our model if (data.value) { $scope.changePasswordModel.value.generatedPassword = data.value; } formHelper.resetForm({ scope: $scope, notifications: data.notifications }); $scope.changePasswordButtonState = 'success'; $timeout(function () { $scope.togglePasswordFields(); }, 2000); }, function (err) { formHelper.handleError(err); $scope.changePasswordButtonState = 'error'; }); } }; $scope.togglePasswordFields = function () { clearPasswordFields(); $scope.showPasswordFields = !$scope.showPasswordFields; }; function clearPasswordFields() { $scope.changePasswordModel.value.oldPassword = ''; $scope.changePasswordModel.value.newPassword = ''; $scope.changePasswordModel.value.confirm = ''; } dashboardResource.getDashboard('user-dialog').then(function (dashboard) { $scope.dashboard = dashboard; }); }); (function () { 'use strict'; function UserGroupPickerController($scope, userGroupsResource, localizationService) { var vm = this; vm.userGroups = []; vm.loading = false; vm.selectUserGroup = selectUserGroup; ////////// function onInit() { vm.loading = true; // set default title if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectUsers'); } // make sure we can push to something if (!$scope.model.selection) { $scope.model.selection = []; } // get venues userGroupsResource.getUserGroups().then(function (userGroups) { vm.userGroups = userGroups; if ($scope.model.selection && $scope.model.selection.length > 0) { preSelect($scope.model.selection); } vm.loading = false; }); } function preSelect(selection) { angular.forEach(selection, function (selected) { angular.forEach(vm.userGroups, function (userGroup) { if (selected.id === userGroup.id) { userGroup.selected = true; } }); }); } function selectUserGroup(userGroup) { if (!userGroup.selected) { userGroup.selected = true; $scope.model.selection.push(userGroup); } else { angular.forEach($scope.model.selection, function (selectedUserGroup, index) { if (selectedUserGroup.id === userGroup.id) { userGroup.selected = false; $scope.model.selection.splice(index, 1); } }); } } onInit(); } angular.module('umbraco').controller('Umbraco.Overlays.UserGroupPickerController', UserGroupPickerController); }()); (function () { 'use strict'; function UserPickerController($scope, usersResource, localizationService) { var vm = this; vm.users = []; vm.loading = false; vm.usersOptions = {}; vm.selectUser = selectUser; vm.searchUsers = searchUsers; vm.changePageNumber = changePageNumber; ////////// function onInit() { vm.loading = true; // set default title if (!$scope.model.title) { $scope.model.title = localizationService.localize('defaultdialogs_selectUsers'); } // make sure we can push to something if (!$scope.model.selection) { $scope.model.selection = []; } // get users getUsers(); } function preSelect(selection, users) { angular.forEach(selection, function (selected) { angular.forEach(users, function (user) { if (selected.id === user.id) { user.selected = true; } }); }); } function selectUser(user) { if (!user.selected) { user.selected = true; $scope.model.selection.push(user); } else { angular.forEach($scope.model.selection, function (selectedUser, index) { if (selectedUser.id === user.id) { user.selected = false; $scope.model.selection.splice(index, 1); } }); } } var search = _.debounce(function () { $scope.$apply(function () { getUsers(); }); }, 500); function searchUsers() { search(); } function getUsers() { vm.loading = true; // Get users usersResource.getPagedResults(vm.usersOptions).then(function (users) { vm.users = users.items; vm.usersOptions.pageNumber = users.pageNumber; vm.usersOptions.pageSize = users.pageSize; vm.usersOptions.totalItems = users.totalItems; vm.usersOptions.totalPages = users.totalPages; preSelect($scope.model.selection, vm.users); vm.loading = false; }); } function changePageNumber(pageNumber) { vm.usersOptions.pageNumber = pageNumber; getUsers(); } onInit(); } angular.module('umbraco').controller('Umbraco.Overlays.UserPickerController', UserPickerController); }()); angular.module('umbraco').controller('Umbraco.Overlays.YsodController', function ($scope, legacyResource, treeService, navigationService, localizationService) { if (!$scope.model.title) { $scope.model.title = localizationService.localize('errors_receivedErrorFromServer'); } if ($scope.model.error && $scope.model.error.data && $scope.model.error.data.StackTrace) { //trim whitespace $scope.model.error.data.StackTrace = $scope.model.error.data.StackTrace.trim(); } if ($scope.model.error && $scope.model.error.data) { $scope.model.error.data.InnerExceptions = []; var ex = $scope.model.error.data.InnerException; while (ex) { if (ex.StackTrace) { ex.StackTrace = ex.StackTrace.trim(); } $scope.model.error.data.InnerExceptions.push(ex); ex = ex.InnerException; } } }); (function () { 'use strict'; function NodeNameController($scope) { var vm = this; var element = angular.element($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { if (element.val().toLowerCase() === 'home') { $scope.model.nextStep(); } else { vm.error = true; } } } angular.module('umbraco').controller('Umbraco.Tours.UmbIntroCreateContent.NodeNameController', NodeNameController); }()); (function () { 'use strict'; function DocTypeNameController($scope) { var vm = this; var element = angular.element($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { if (element.val().toLowerCase() === 'home page') { $scope.model.nextStep(); } else { vm.error = true; } } } angular.module('umbraco').controller('Umbraco.Tours.UmbIntroCreateDocType.DocTypeNameController', DocTypeNameController); }()); (function () { 'use strict'; function PropertyNameController($scope) { var vm = this; var element = angular.element($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { if (element.val().toLowerCase() === 'welcome text') { $scope.model.nextStep(); } else { vm.error = true; } } } angular.module('umbraco').controller('Umbraco.Tours.UmbIntroCreateDocType.PropertyNameController', PropertyNameController); }()); (function () { 'use strict'; function TabNameController($scope) { var vm = this; var element = angular.element($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { if (element.val().toLowerCase() === 'home') { $scope.model.nextStep(); } else { vm.error = true; } } } angular.module('umbraco').controller('Umbraco.Tours.UmbIntroCreateDocType.TabNameController', TabNameController); }()); (function () { 'use strict'; function FolderNameController($scope) { var vm = this; var element = angular.element($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { if (element.val().toLowerCase() === 'my images') { $scope.model.nextStep(); } else { vm.error = true; } } } angular.module('umbraco').controller('Umbraco.Tours.UmbIntroMediaSection.FolderNameController', FolderNameController); }()); (function () { 'use strict'; function UploadImagesController($scope, editorState, mediaResource) { var vm = this; var element = angular.element($scope.model.currentStep.element); vm.error = false; vm.initNextStep = initNextStep; function initNextStep() { vm.error = false; vm.buttonState = 'busy'; var currentNode = editorState.getCurrent(); // make sure we have uploaded at least one image mediaResource.getChildren(currentNode.id).then(function (data) { var children = data; if (children.items && children.items.length > 0) { $scope.model.nextStep(); } else { vm.error = true; } vm.buttonState = 'init'; }); } } angular.module('umbraco').controller('Umbraco.Tours.UmbIntroMediaSection.UploadImagesController', UploadImagesController); }()); (function () { 'use strict'; function TemplatesTreeController($scope) { var vm = this; var eventElement = angular.element($scope.model.currentStep.eventElement); function onInit() { // check if tree is already open - if it is - go to next step if (eventElement.hasClass('icon-navigation-down')) { $scope.model.nextStep(); } } onInit(); } angular.module('umbraco').controller('Umbraco.Tours.UmbIntroRenderInTemplate.TemplatesTreeController', TemplatesTreeController); }()); angular.module('umbraco').controller('Umbraco.Editors.Content.CopyController', function ($scope, userService, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { var dialogOptions = $scope.dialogOptions; var searchText = 'Search...'; localizationService.localize('general_search').then(function (value) { searchText = value + '...'; }); $scope.relateToOriginal = true; $scope.recursive = true; $scope.dialogTreeEventHandler = $({}); $scope.busy = false; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, results: [], selectedSearchResults: [] }; $scope.treeModel = { hideHeader: false }; $scope.toggle = toggleHandler; userService.getCurrentUser().then(function (userData) { $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); var node = dialogOptions.currentNode; function treeLoadedHandler(ev, args) { if (node && node.path) { $scope.dialogTreeEventHandler.syncTree({ path: node.path, activate: false }); } } function nodeSelectHandler(ev, args) { if (args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); } eventsService.emit('editors.content.copyController.select', args); if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } function nodeExpandedHandler(ev, args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } } function toggleHandler(type) { // If the relateToOriginal toggle is clicked if (type === 'relate') { if ($scope.relateToOriginal) { $scope.relateToOriginal = false; return; } $scope.relateToOriginal = true; } // If the recurvise toggle is clicked if (type === 'recursive') { if ($scope.recursive) { $scope.recursive = false; return; } $scope.recursive = true; } } $scope.hideSearch = function () { $scope.searchInfo.showSearch = false; $scope.searchInfo.searchFromId = null; $scope.searchInfo.searchFromName = null; $scope.searchInfo.results = []; }; // method to select a search result $scope.selectResult = function (evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); }; //callback when there are search results $scope.onSearchResults = function (results) { $scope.searchInfo.results = results; $scope.searchInfo.showSearch = true; }; $scope.copy = function () { $scope.busy = true; $scope.error = false; contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }).then(function (path) { $scope.error = false; $scope.success = true; $scope.busy = false; //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the copied content - but don't activate the node, //then sync to the currenlty edited content (note: this might not be the content that was copied!!) navigationService.syncTree({ tree: 'content', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'content', path: activeNodePath, forceReload: false, activate: true }); } }); }, function (err) { $scope.success = false; $scope.error = err; $scope.busy = false; //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; $scope.dialogTreeEventHandler.bind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; nodeSelectHandler({}, { node: node }); }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; }; function openMiniListView(node) { $scope.miniListView = node; } }); /** * @ngdoc controller * @name Umbraco.Editors.Content.CreateController * @function * * @description * The controller for the content creation dialog */ function contentCreateController($scope, $routeParams, contentTypeResource, iconHelper, $location, navigationService, blueprintConfig) { function initialize() { contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); }); $scope.selectContentType = true; $scope.selectBlueprint = false; $scope.allowBlank = blueprintConfig.allowBlank; } function close() { navigationService.hideMenu(); } function createBlank(docType) { $location.path('/content/content/edit/' + $scope.currentNode.id).search('doctype=' + docType.alias + '&create=true'); close(); } function createOrSelectBlueprintIfAny(docType) { var blueprintIds = _.keys(docType.blueprints || {}); $scope.docType = docType; if (blueprintIds.length) { if (blueprintConfig.skipSelect) { createFromBlueprint(blueprintIds[0]); } else { $scope.selectContentType = false; $scope.selectBlueprint = true; } } else { createBlank(docType); } } function createFromBlueprint(blueprintId) { $location.path('/content/content/edit/' + $scope.currentNode.id).search('doctype=' + $scope.docType.alias + '&create=true&blueprintId=' + blueprintId); close(); } $scope.createBlank = createBlank; $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; $scope.createFromBlueprint = createFromBlueprint; initialize(); } angular.module('umbraco').controller('Umbraco.Editors.Content.CreateController', contentCreateController); angular.module('umbraco').value('blueprintConfig', { skipSelect: false, allowBlank: true }); (function () { function CreateBlueprintController($scope, contentResource, notificationsService, navigationService, localizationService, formHelper, contentEditingHelper) { $scope.message = { name: $scope.currentNode.name }; var successText = {}; localizationService.localize('blueprints_createBlueprintFrom', ['' + $scope.message.name + '']).then(function (localizedVal) { $scope.title = localizedVal; }); $scope.cancel = function () { navigationService.hideMenu(); }; $scope.create = function () { if (formHelper.submitForm({ scope: $scope, formCtrl: this.blueprintForm, statusMessage: 'Creating blueprint...' })) { contentResource.createBlueprintFromContent($scope.currentNode.id, $scope.message.name).then(function (data) { formHelper.resetForm({ scope: $scope, notifications: data.notifications }); navigationService.hideMenu(); }, function (err) { contentEditingHelper.handleSaveError({ redirectOnFailure: false, err: err }); }); } }; } angular.module('umbraco').controller('Umbraco.Editors.Content.CreateBlueprintController', CreateBlueprintController); }()); /** * @ngdoc controller * @name Umbraco.Editors.ContentDeleteController * @function * * @description * The controller for deleting content */ function ContentDeleteController($scope, contentResource, treeService, navigationService, editorState, $location, dialogService, notificationsService) { $scope.performDelete = function () { // stop from firing again on double-click if ($scope.busy) { return false; } //mark it for deletion (used in the UI) $scope.currentNode.loading = true; $scope.busy = true; contentResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); treeService.removeNode($scope.currentNode); if (rootNode) { //ensure the recycle bin has child nodes now var recycleBin = treeService.getDescendantNode(rootNode, -20); if (recycleBin) { recycleBin.hasChildren = true; //reload the recycle bin if it's already expanded so the deleted item is shown if (recycleBin.expanded) { treeService.loadNodeChildren({ node: recycleBin, section: 'content' }); } } } //if the current edited item is the same one as we're deleting, we need to navigate elsewhere if (editorState.current && editorState.current.id == $scope.currentNode.id) { //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent var location = '/content'; if ($scope.currentNode.parentId.toString() === '-20') location = '/content/content/recyclebin'; else if ($scope.currentNode.parentId.toString() !== '-1') location = '/content/content/edit/' + $scope.currentNode.parentId; $location.path(location); } $scope.success = true; }, function (err) { $scope.currentNode.loading = false; $scope.busy = false; //check if response is ysod if (err.status && err.status >= 500) { dialogService.ysodDialog(err); } if (err.data && angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.Content.DeleteController', ContentDeleteController); /** * @ngdoc controller * @name Umbraco.Editors.Content.EditController * @function * * @description * The controller for the content editor */ function ContentEditController($scope, $routeParams, contentResource) { function scaffoldEmpty() { return contentResource.getScaffold($routeParams.id, $routeParams.doctype); } function scaffoldBlueprint() { return contentResource.getBlueprintScaffold($routeParams.id, $routeParams.blueprintId); } $scope.contentId = $routeParams.id; $scope.saveMethod = contentResource.save; $scope.getMethod = contentResource.getById; $scope.getScaffoldMethod = $routeParams.blueprintId ? scaffoldBlueprint : scaffoldEmpty; $scope.page = $routeParams.page; $scope.isNew = $routeParams.create; } angular.module('umbraco').controller('Umbraco.Editors.Content.EditController', ContentEditController); /** * @ngdoc controller * @name Umbraco.Editors.Content.EmptyRecycleBinController * @function * * @description * The controller for deleting content */ function ContentEmptyRecycleBinController($scope, contentResource, treeService, navigationService, notificationsService, $route) { $scope.busy = false; $scope.performDelete = function () { //(used in the UI) $scope.busy = true; $scope.currentNode.loading = true; contentResource.emptyRecycleBin($scope.currentNode.id).then(function (result) { $scope.busy = false; $scope.currentNode.loading = false; //show any notifications if (angular.isArray(result.notifications)) { for (var i = 0; i < result.notifications.length; i++) { notificationsService.showNotification(result.notifications[i]); } } treeService.removeChildNodes($scope.currentNode); navigationService.hideMenu(); //reload the current view $route.reload(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.Content.EmptyRecycleBinController', ContentEmptyRecycleBinController); angular.module('umbraco').controller('Umbraco.Editors.Content.MoveController', function ($scope, userService, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { var dialogOptions = $scope.dialogOptions; var searchText = 'Search...'; localizationService.localize('general_search').then(function (value) { searchText = value + '...'; }); $scope.dialogTreeEventHandler = $({}); $scope.busy = false; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, results: [], selectedSearchResults: [] }; $scope.treeModel = { hideHeader: false }; userService.getCurrentUser().then(function (userData) { $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); var node = dialogOptions.currentNode; function treeLoadedHandler(ev, args) { if (node && node.path) { $scope.dialogTreeEventHandler.syncTree({ path: node.path, activate: false }); } } function nodeSelectHandler(ev, args) { if (args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); } eventsService.emit('editors.content.moveController.select', args); if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } function nodeExpandedHandler(ev, args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } } $scope.hideSearch = function () { $scope.searchInfo.showSearch = false; $scope.searchInfo.searchFromId = null; $scope.searchInfo.searchFromName = null; $scope.searchInfo.results = []; }; // method to select a search result $scope.selectResult = function (evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); }; //callback when there are search results $scope.onSearchResults = function (results) { $scope.searchInfo.results = results; $scope.searchInfo.showSearch = true; }; $scope.move = function () { $scope.busy = true; $scope.error = false; contentResource.move({ parentId: $scope.target.id, id: node.id }).then(function (path) { $scope.error = false; $scope.success = true; $scope.busy = false; //first we need to remove the node that launched the dialog treeService.removeNode($scope.currentNode); //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the moved content - but don't activate the node, //then sync to the currently edited content (note: this might not be the content that was moved!!) navigationService.syncTree({ tree: 'content', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'content', path: activeNodePath, forceReload: false, activate: true }); } }); }, function (err) { $scope.success = false; $scope.error = err; $scope.busy = false; //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; $scope.dialogTreeEventHandler.bind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; nodeSelectHandler({}, { node: node }); }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; }; function openMiniListView(node) { $scope.miniListView = node; } }); (function () { function CreateNotifyController($scope, contentResource, navigationService, angularHelper, localizationService) { var vm = this; var currentForm; vm.notifyOptions = []; vm.save = save; vm.cancel = cancel; vm.message = { name: $scope.currentNode.name }; vm.labels = {}; function onInit() { vm.loading = true; contentResource.getNotifySettingsById($scope.currentNode.id).then(function (options) { currentForm = angularHelper.getCurrentForm($scope); vm.loading = false; vm.notifyOptions = options; }); localizationService.localize('notifications_editNotifications', [$scope.currentNode.name]).then(function (value) { vm.labels.headline = value; }); } function cancel() { navigationService.hideMenu(); } ; function save(notifyOptions) { vm.saveState = 'busy'; vm.saveError = false; vm.saveSuccces = false; var selectedString = ''; angular.forEach(notifyOptions, function (option) { if (option.checked === true && option.notifyCode) { selectedString += option.notifyCode; } }); contentResource.setNotifySettingsById($scope.currentNode.id, selectedString).then(function () { vm.saveState = 'success'; vm.saveSuccces = true; }, function (error) { vm.saveState = 'error'; vm.saveError = error; }); } onInit(); } angular.module('umbraco').controller('Umbraco.Editors.Content.CreateNotifyController', CreateNotifyController); }()); /** * @ngdoc controller * @name Umbraco.Editors.Content.RecycleBinController * @function * * @description * Controls the recycle bin for content * */ function ContentRecycleBinController($scope, $routeParams, contentResource, navigationService, localizationService) { //ensures the list view doesn't actually load until we query for the list view config // for the section $scope.page = {}; $scope.page.name = 'Recycle Bin'; $scope.page.nameLocked = true; //ensures the list view doesn't actually load until we query for the list view config // for the section $scope.listViewPath = null; $routeParams.id = '-20'; contentResource.getRecycleBin().then(function (result) { //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a // single property, so we'll extract that property (list view) and use it's data. var listproperty = result.tabs[0].properties[0]; _.each(listproperty.config, function (val, key) { $scope.model.config[key] = val; }); $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; }); $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; // sync tree node navigationService.syncTree({ tree: 'content', path: [ '-1', $routeParams.id ], forceReload: false }); localizePageName(); function localizePageName() { var pageName = 'general_recycleBin'; localizationService.localize(pageName).then(function (value) { $scope.page.name = value; }); } } angular.module('umbraco').controller('Umbraco.Editors.Content.RecycleBinController', ContentRecycleBinController); angular.module('umbraco').controller('Umbraco.Editors.Content.RestoreController', function ($scope, relationResource, contentResource, navigationService, appState, treeService, userService, localizationService) { var dialogOptions = $scope.dialogOptions; $scope.source = _.clone(dialogOptions.currentNode); $scope.error = null; $scope.loading = true; $scope.moving = false; $scope.success = false; $scope.dialogTreeEventHandler = $({}); $scope.searchInfo = { showSearch: false, results: [], selectedSearchResults: [] }; $scope.treeModel = { hideHeader: false }; userService.getCurrentUser().then(function (userData) { $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); $scope.labels = {}; localizationService.localizeMany(['treeHeaders_content']).then(function (data) { $scope.labels.treeRoot = data[0]; }); function nodeSelectHandler(ev, args) { if (args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); } if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } function nodeExpandedHandler(ev, args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } } $scope.hideSearch = function () { $scope.searchInfo.showSearch = false; $scope.searchInfo.results = []; }; // method to select a search result $scope.selectResult = function (evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); }; //callback when there are search results $scope.onSearchResults = function (results) { $scope.searchInfo.results = results; $scope.searchInfo.showSearch = true; }; $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; nodeSelectHandler({}, { node: node }); }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; }; function openMiniListView(node) { $scope.miniListView = node; } relationResource.getByChildId($scope.source.id, 'relateParentDocumentOnDelete').then(function (data) { $scope.loading = false; if (!data.length) { $scope.moving = true; return; } $scope.relation = data[0]; if ($scope.relation.parentId == -1) { $scope.target = { id: -1, name: $scope.labels.treeRoot }; } else { $scope.loading = true; contentResource.getById($scope.relation.parentId).then(function (data) { $scope.loading = false; $scope.target = data; // make sure the target item isn't in the recycle bin if ($scope.target.path.indexOf('-20') !== -1) { $scope.moving = true; $scope.target = null; } }, function (err) { $scope.loading = false; $scope.error = err; }); } }, function (err) { $scope.loading = false; $scope.error = err; }); $scope.restore = function () { $scope.loading = true; // this code was copied from `content.move.controller.js` contentResource.move({ parentId: $scope.target.id, id: $scope.source.id }).then(function (path) { $scope.loading = false; $scope.success = true; //first we need to remove the node that launched the dialog treeService.removeNode($scope.currentNode); //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the moved content - but don't activate the node, //then sync to the currenlty edited content (note: this might not be the content that was moved!!) navigationService.syncTree({ tree: 'content', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'content', path: activeNodePath, forceReload: false, activate: true }); } }); }, function (err) { $scope.loading = false; $scope.error = err; }); }; }); (function () { 'use strict'; function ContentRightsController($scope, $timeout, contentResource, localizationService, angularHelper) { var vm = this; var currentForm; vm.availableUserGroups = []; vm.selectedUserGroups = []; vm.removedUserGroups = []; vm.viewState = 'manageGroups'; vm.labels = {}; vm.showNotification = false; vm.setViewSate = setViewSate; vm.editPermissions = editPermissions; vm.setPermissions = setPermissions; vm.save = save; vm.removePermissions = removePermissions; vm.cancelManagePermissions = cancelManagePermissions; vm.closeDialog = closeDialog; vm.stay = stay; function onInit() { vm.loading = true; contentResource.getDetailedPermissions($scope.currentNode.id).then(function (userGroups) { initData(userGroups); vm.loading = false; currentForm = angularHelper.getCurrentForm($scope); }); } /** * This will initialize the data and set the correct selectedUserGroups based on the default permissions and explicit permissions assigned * @param {any} userGroups */ function initData(userGroups) { //reset this vm.selectedUserGroups = []; vm.availableUserGroups = userGroups; angular.forEach(vm.availableUserGroups, function (group) { if (group.permissions) { //if there's explicit permissions assigned than it's selected assignGroupPermissions(group); } }); } function setViewSate(state) { vm.viewState = state; } function editPermissions(group) { vm.selectedUserGroup = group; if (!vm.selectedUserGroup.permissions) { //if no permissions are explicitly set this means we need to show the defaults vm.selectedUserGroup.permissions = vm.selectedUserGroup.defaultPermissions; } localizationService.localize('defaultdialogs_permissionsSetForGroup', [ $scope.currentNode.name, vm.selectedUserGroup.name ]).then(function (value) { vm.labels.permissionsSetForGroup = value; }); setViewSate('managePermissions'); } function assignGroupPermissions(group) { // clear allowed permissions before we make the list so we don't have duplicates group.allowedPermissions = []; // get list of checked permissions angular.forEach(group.permissions, function (permissionGroup) { angular.forEach(permissionGroup, function (permission) { if (permission.checked) { //the `allowedPermissions` is what will get sent up to the server for saving group.allowedPermissions.push(permission); } }); }); if (!group.selected) { // set to selected so we can remove from the dropdown easily group.selected = true; vm.selectedUserGroups.push(group); //remove from the removed groups if it's been re-added vm.removedUserGroups = _.reject(vm.removedUserGroups, function (g) { return g.id == group.id; }); } } function setPermissions(group) { assignGroupPermissions(group); setViewSate('manageGroups'); } /** * This essentially resets the permissions for a group for this content item, it will remove it from the selected list * @param {any} index */ function removePermissions(index) { // remove as selected so we can select it from the dropdown again var group = vm.selectedUserGroups[index]; group.selected = false; //reset assigned permissions - so it will default back to default permissions group.permissions = []; group.allowedPermissions = []; vm.selectedUserGroups.splice(index, 1); //track it in the removed so this gets pushed to the server vm.removedUserGroups.push(group); } function cancelManagePermissions() { setViewSate('manageGroups'); } function formatSaveModel(permissionsSave, groupCollection) { angular.forEach(groupCollection, function (g) { permissionsSave[g.id] = []; angular.forEach(g.allowedPermissions, function (p) { permissionsSave[g.id].push(p.permissionCode); }); }); } function save() { vm.saveState = 'busy'; vm.saveError = false; vm.saveSuccces = false; //this is a dictionary that we need to populate var permissionsSave = {}; //format the selectedUserGroups, then the removedUserGroups since we want to pass data from both collections up formatSaveModel(permissionsSave, vm.selectedUserGroups); formatSaveModel(permissionsSave, vm.removedUserGroups); var saveModel = { contentId: $scope.currentNode.id, permissions: permissionsSave }; contentResource.savePermissions(saveModel).then(function (userGroups) { //re-assign model from server since it could have changed initData(userGroups); // clear dirty state on the form so we don't see the discard changes notification // we use a timeout here because in some cases the initData reformats the userGroups model and triggers a change after the form state was changed $timeout(function () { if (currentForm) { currentForm.$dirty = false; } }); vm.saveState = 'success'; vm.saveSuccces = true; }, function (error) { vm.saveState = 'error'; vm.saveError = error; }); } function stay() { vm.showNotification = false; } function closeDialog() { // check if form has been changed. If it has show discard changes notification if (currentForm && currentForm.$dirty) { vm.showNotification = true; } else { $scope.nav.hideDialog(); } } onInit(); } angular.module('umbraco').controller('Umbraco.Editors.Content.RightsController', ContentRightsController); }()); /** * @ngdoc controller * @name Umbraco.Editors.ContentBlueprint.CreateController * @function * * @description * The controller for creating content blueprints */ function ContentBlueprintCreateController($scope, $location, contentTypeResource, navigationService) { var vm = this; var node = $scope.dialogOptions.currentNode; vm.createBlueprint = createBlueprint; function onInit() { vm.loading = true; contentTypeResource.getAll().then(function (documentTypes) { vm.documentTypes = documentTypes; vm.loading = false; }); } function createBlueprint(documentType) { $location.path('/settings/contentBlueprints/edit/' + node.id).search('create', 'true').search('doctype', documentType.alias); navigationService.hideMenu(); } onInit(); } angular.module('umbraco').controller('Umbraco.Editors.ContentBlueprint.CreateController', ContentBlueprintCreateController); /** * @ngdoc controller * @name Umbraco.Editors.ContentBlueprint.DeleteController * @function * * @description * The controller for deleting content blueprints */ function ContentBlueprintDeleteController($scope, contentResource, treeService, navigationService) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; contentResource.deleteBlueprint($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.ContentBlueprint.DeleteController', ContentBlueprintDeleteController); /** * @ngdoc controller * @name Umbraco.Editors.Content.EditController * @function * * @description * The controller for the content editor */ function ContentBlueprintEditController($scope, $routeParams, contentResource) { var excludedProps = [ '_umb_urls', '_umb_releasedate', '_umb_expiredate', '_umb_template' ]; function getScaffold() { return contentResource.getScaffold(-1, $routeParams.doctype).then(function (scaffold) { var lastTab = scaffold.tabs[scaffold.tabs.length - 1]; lastTab.properties = _.filter(lastTab.properties, function (p) { return excludedProps.indexOf(p.alias) === -1; }); scaffold.allowPreview = false; scaffold.allowedActions = [ 'A', 'S', 'C' ]; return scaffold; }); } $scope.contentId = $routeParams.id; $scope.isNew = $routeParams.id === '-1'; $scope.saveMethod = contentResource.saveBlueprint; $scope.getMethod = contentResource.getBlueprintById; $scope.getScaffoldMethod = getScaffold; } angular.module('umbraco').controller('Umbraco.Editors.ContentBlueprint.EditController', ContentBlueprintEditController); function startUpVideosDashboardController($scope, xmlhelper, $log, $http) { $scope.videos = []; $scope.init = function (url) { var proxyUrl = 'dashboard/feedproxy.aspx?url=' + url; $http.get(proxyUrl).then(function (data) { var feed = $(data.data); $('item', feed).each(function (i, item) { var video = {}; video.thumbnail = $(item).find('thumbnail').attr('url'); video.title = $('title', item).text(); video.link = $('guid', item).text(); $scope.videos.push(video); }); }); }; } angular.module('umbraco').controller('Umbraco.Dashboard.StartupVideosController', startUpVideosDashboardController); function startUpDynamicContentController($timeout, $scope, dashboardResource, assetsService, tourService, eventsService) { var vm = this; var evts = []; vm.loading = true; vm.showDefault = false; vm.startTour = startTour; function onInit() { // load tours tourService.getGroupedTours().then(function (groupedTours) { vm.tours = groupedTours; }); } function startTour(tour) { tourService.startTour(tour); } // default dashboard content vm.defaultDashboard = { infoBoxes: [ { title: 'Documentation', description: 'Find the answers to your Umbraco questions', url: 'https://our.umbraco.com/documentation/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=documentation/' }, { title: 'Community', description: 'Find the answers or ask your Umbraco questions', url: 'https://our.umbraco.com/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=our_forum' }, { title: 'Umbraco.tv', description: 'Tutorial videos (some are free, some are on subscription)', url: 'https://umbraco.tv/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=tutorial_videos' }, { title: 'Training', description: 'Real-life training and official Umbraco certifications', url: 'https://umbraco.com/training/?utm_source=core&utm_medium=dashboard&utm_content=text&utm_campaign=training' } ], articles: [ { title: 'Umbraco.TV - Learn from the source!', description: 'Umbraco.TV will help you go from zero to Umbraco hero at a pace that suits you. Our easy to follow online training videos will give you the fundamental knowledge to start building awesome Umbraco websites.', img: 'views/dashboard/default/umbracotv.jpg', url: 'https://umbraco.tv/?utm_source=core&utm_medium=dashboard&utm_content=image&utm_campaign=tv', altText: 'Umbraco.TV - Hours of Umbraco Video Tutorials', buttonText: 'Visit Umbraco.TV' }, { title: 'Our Umbraco - The Friendliest Community', description: 'Our Umbraco - the official community site is your one stop for everything Umbraco. Whether you need a question answered or looking for cool plugins, the world\'s best and friendliest community is just a click away.', img: 'views/dashboard/default/ourumbraco.jpg', url: 'https://our.umbraco.com/?utm_source=core&utm_medium=dashboard&utm_content=image&utm_campaign=our', altText: 'Our Umbraco', buttonText: 'Visit Our Umbraco' } ] }; evts.push(eventsService.on('appState.tour.complete', function (name, completedTour) { $timeout(function () { angular.forEach(vm.tours, function (tourGroup) { angular.forEach(tourGroup, function (tour) { if (tour.alias === completedTour.alias) { tour.completed = true; } }); }); }); })); //proxy remote css through the local server assetsService.loadCss(dashboardResource.getRemoteDashboardCssUrl('content'), $scope); dashboardResource.getRemoteDashboardContent('content').then(function (data) { vm.loading = false; //test if we have received valid data //we capture it like this, so we avoid UI errors - which automatically triggers ui based on http response code if (data && data.sections) { vm.dashboard = data; } else { vm.showDefault = true; } }, function (exception) { console.error(exception); vm.loading = false; vm.showDefault = true; }); onInit(); } angular.module('umbraco').controller('Umbraco.Dashboard.StartUpDynamicContentController', startUpDynamicContentController); function FormsController($scope, $route, $cookieStore, packageResource, localizationService) { $scope.installForms = function () { $scope.state = localizationService.localize('packager_installStateDownloading'); packageResource.fetch('CD44CF39-3D71-4C19-B6EE-948E1FAF0525').then(function (pack) { $scope.state = localizationService.localize('packager_installStateImporting'); return packageResource.import(pack); }, $scope.error).then(function (pack) { $scope.state = localizationService.localize('packager_installStateInstalling'); return packageResource.installFiles(pack); }, $scope.error).then(function (pack) { $scope.state = localizationService.localize('packager_installStateRestarting'); return packageResource.installData(pack); }, $scope.error).then(function (pack) { $scope.state = localizationService.localize('packager_installStateComplete'); return packageResource.cleanUp(pack); }, $scope.error).then($scope.complete, $scope.error); }; $scope.complete = function (result) { var url = window.location.href + '?init=true'; $cookieStore.put('umbPackageInstallId', result.packageGuid); window.location.reload(true); }; $scope.error = function (err) { $scope.state = undefined; $scope.error = err; //This will return a rejection meaning that the promise change above will stop return $q.reject(); }; function Video_player(videoId) { // Get dom elements this.container = document.getElementById(videoId); this.video = this.container.getElementsByTagName('video')[0]; //Create controls this.controls = document.createElement('div'); this.controls.className = 'video-controls'; this.seek_bar = document.createElement('input'); this.seek_bar.className = 'seek-bar'; this.seek_bar.type = 'range'; this.seek_bar.setAttribute('value', '0'); this.loader = document.createElement('div'); this.loader.className = 'loader'; this.progress_bar = document.createElement('span'); this.progress_bar.className = 'progress-bar'; // Insert controls this.controls.appendChild(this.seek_bar); this.container.appendChild(this.controls); this.controls.appendChild(this.loader); this.loader.appendChild(this.progress_bar); } Video_player.prototype.seeking = function () { // get the value of the seekbar (hidden input[type="range"]) var time = this.video.duration * (this.seek_bar.value / 100); // Update video to seekbar value this.video.currentTime = time; }; // Stop video when user initiates seeking Video_player.prototype.start_seek = function () { this.video.pause(); }; // Start video when user stops seeking Video_player.prototype.stop_seek = function () { this.video.play(); }; // Update the progressbar (span.loader) according to video.currentTime Video_player.prototype.update_progress_bar = function () { // Get video progress in % var value = 100 / this.video.duration * this.video.currentTime; // Update progressbar this.progress_bar.style.width = value + '%'; }; // Bind progressbar to mouse when seeking Video_player.prototype.handle_mouse_move = function (event) { // Get position of progressbar relative to browser window var pos = this.progress_bar.getBoundingClientRect().left; // Make sure event is reckonized cross-browser event = event || window.event; // Update progressbar this.progress_bar.style.width = event.clientX - pos + 'px'; }; // Eventlisteners for seeking Video_player.prototype.video_event_handler = function (videoPlayer, interval) { // Update the progress bar var animate_progress_bar = setInterval(function () { videoPlayer.update_progress_bar(); }, interval); // Fire when input value changes (user seeking) videoPlayer.seek_bar.addEventListener('change', function () { videoPlayer.seeking(); }); // Fire when user clicks on seekbar videoPlayer.seek_bar.addEventListener('mousedown', function (clickEvent) { // Pause video playback videoPlayer.start_seek(); // Stop updating progressbar according to video progress clearInterval(animate_progress_bar); // Update progressbar to where user clicks videoPlayer.handle_mouse_move(clickEvent); // Bind progressbar to cursor window.onmousemove = function (moveEvent) { videoPlayer.handle_mouse_move(moveEvent); }; }); // Fire when user releases seekbar videoPlayer.seek_bar.addEventListener('mouseup', function () { // Unbind progressbar from cursor window.onmousemove = null; // Start video playback videoPlayer.stop_seek(); // Animate the progressbar animate_progress_bar = setInterval(function () { videoPlayer.update_progress_bar(); }, interval); }); }; var videoPlayer = new Video_player('video_1'); videoPlayer.video_event_handler(videoPlayer, 17); } angular.module('umbraco').controller('Umbraco.Dashboard.FormsDashboardController', FormsController); function startupLatestEditsController($scope) { } angular.module('umbraco').controller('Umbraco.Dashboard.StartupLatestEditsController', startupLatestEditsController); function MediaFolderBrowserDashboardController($rootScope, $scope, $location, contentTypeResource, userService) { var currentUser = {}; userService.getCurrentUser().then(function (user) { currentUser = user; // check if the user has access to the root which they will require to see this dashboard if (currentUser.startMediaIds.indexOf(-1) >= 0) { //get the system media listview contentTypeResource.getPropertyTypeScaffold(-96).then(function (dt) { $scope.fakeProperty = { alias: 'contents', config: dt.config, description: '', editor: dt.editor, hideLabel: true, id: 1, label: 'Contents:', validation: { mandatory: false, pattern: null }, value: '', view: dt.view }; }); } else if (currentUser.startMediaIds.length > 0) { // redirect to start node $location.path('/media/media/edit/' + (currentUser.startMediaIds.length === 0 ? -1 : currentUser.startMediaIds[0])); } }); } angular.module('umbraco').controller('Umbraco.Dashboard.MediaFolderBrowserDashboardController', MediaFolderBrowserDashboardController); function ExamineMgmtController($scope, umbRequestHelper, $log, $http, $q, $timeout) { $scope.indexerDetails = []; $scope.searcherDetails = []; $scope.loading = true; function checkProcessing(indexer, checkActionName) { umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', checkActionName, { indexerName: indexer.name })), 'Failed to check index processing').then(function (data) { if (data !== null && data !== 'null') { //copy all resulting properties for (var k in data) { indexer[k] = data[k]; } indexer.isProcessing = false; } else { $timeout(function () { //don't continue if we've tried 100 times if (indexer.processingAttempts < 100) { checkProcessing(indexer, checkActionName); //add an attempt indexer.processingAttempts++; } else { //we've exceeded 100 attempts, stop processing indexer.isProcessing = false; } }, 1000); } }); } $scope.search = function (searcher, e) { if (e && e.keyCode !== 13) { return; } umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'GetSearchResults', { searcherName: searcher.name, query: encodeURIComponent(searcher.searchText), queryType: searcher.searchType })), 'Failed to search').then(function (searchResults) { searcher.isSearching = true; searcher.searchResults = searchResults; }); }; $scope.toggle = function (provider, propName) { if (provider[propName] !== undefined) { provider[propName] = !provider[propName]; } else { provider[propName] = true; } }; $scope.rebuildIndex = function (indexer) { if (confirm('This will cause the index to be rebuilt. ' + 'Depending on how much content there is in your site this could take a while. ' + 'It is not recommended to rebuild an index during times of high website traffic ' + 'or when editors are editing content.')) { indexer.isProcessing = true; indexer.processingAttempts = 0; umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'PostRebuildIndex', { indexerName: indexer.name })), 'Failed to rebuild index').then(function () { //rebuilding has started, nothing is returned accept a 200 status code. //lets poll to see if it is done. $timeout(function () { checkProcessing(indexer, 'PostCheckRebuildIndex'); }, 1000); }); } }; $scope.optimizeIndex = function (indexer) { if (confirm('This will cause the index to be optimized which will improve its performance. ' + 'It is not recommended to optimize an index during times of high website traffic ' + 'or when editors are editing content.')) { indexer.isProcessing = true; umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'PostOptimizeIndex', { indexerName: indexer.name })), 'Failed to optimize index').then(function () { //optimizing has started, nothing is returned accept a 200 status code. //lets poll to see if it is done. $timeout(function () { checkProcessing(indexer, 'PostCheckOptimizeIndex'); }, 1000); }); } }; $scope.closeSearch = function (searcher) { searcher.isSearching = true; }; //go get the data //combine two promises and execute when they are both done $q.all([ //get the indexer details umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'GetIndexerDetails')), 'Failed to retrieve indexer details').then(function (data) { $scope.indexerDetails = data; }), //get the searcher details umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('examineMgmtBaseUrl', 'GetSearcherDetails')), 'Failed to retrieve searcher details').then(function (data) { $scope.searcherDetails = data; for (var s in $scope.searcherDetails) { $scope.searcherDetails[s].searchType = 'text'; } }) ]).then(function () { //all init loading is complete $scope.loading = false; }); } angular.module('umbraco').controller('Umbraco.Dashboard.ExamineMgmtController', ExamineMgmtController); (function () { 'use strict'; function HealthCheckController($scope, healthCheckResource) { var SUCCESS = 0; var WARNING = 1; var ERROR = 2; var INFO = 3; var vm = this; vm.viewState = 'list'; vm.groups = []; vm.selectedGroup = {}; vm.getStatus = getStatus; vm.executeAction = executeAction; vm.checkAllGroups = checkAllGroups; vm.checkAllInGroup = checkAllInGroup; vm.openGroup = openGroup; vm.setViewState = setViewState; // Get a (grouped) list of all health checks healthCheckResource.getAllChecks().then(function (response) { vm.groups = response; }); function setGroupGlobalResultType(group) { var totalSuccess = 0; var totalError = 0; var totalWarning = 0; var totalInfo = 0; // count total number of statusses angular.forEach(group.checks, function (check) { angular.forEach(check.status, function (status) { switch (status.resultType) { case SUCCESS: totalSuccess = totalSuccess + 1; break; case WARNING: totalWarning = totalWarning + 1; break; case ERROR: totalError = totalError + 1; break; case INFO: totalInfo = totalInfo + 1; break; } }); }); group.totalSuccess = totalSuccess; group.totalError = totalError; group.totalWarning = totalWarning; group.totalInfo = totalInfo; } // Get the status of an individual check function getStatus(check) { check.loading = true; check.status = null; healthCheckResource.getStatus(check.id).then(function (response) { check.loading = false; check.status = response; }); } function executeAction(check, index, action) { check.loading = true; healthCheckResource.executeAction(action).then(function (response) { check.status[index] = response; check.loading = false; }); } function checkAllGroups(groups) { // set number of checks which has been executed for (var i = 0; i < groups.length; i++) { var group = groups[i]; checkAllInGroup(group, group.checks); } vm.groups = groups; } function checkAllInGroup(group, checks) { group.checkCounter = 0; group.loading = true; angular.forEach(checks, function (check) { check.loading = true; healthCheckResource.getStatus(check.id).then(function (response) { check.status = response; group.checkCounter = group.checkCounter + 1; check.loading = false; // when all checks are done, set global group result if (group.checkCounter === checks.length) { setGroupGlobalResultType(group); group.loading = false; } }); }); } function openGroup(group) { vm.selectedGroup = group; vm.viewState = 'details'; } function setViewState(state) { vm.viewState = state; if (state === 'list') { for (var i = 0; i < vm.groups.length; i++) { var group = vm.groups[i]; setGroupGlobalResultType(group); } } } } angular.module('umbraco').controller('Umbraco.Dashboard.HealthCheckController', HealthCheckController); }()); (function () { 'use strict'; function RedirectUrlsController($scope, redirectUrlsResource, notificationsService, localizationService, $q) { //...todo //search by url or url part //search by domain //display domain in dashboard results? //used to cancel any request in progress if another one needs to take it's place var vm = this; var canceler = null; vm.dashboard = { searchTerm: '', loading: false, urlTrackerDisabled: false, userIsAdmin: false }; vm.pagination = { pageIndex: 0, pageNumber: 1, totalPages: 1, pageSize: 20 }; vm.goToPage = goToPage; vm.search = search; vm.removeRedirect = removeRedirect; vm.disableUrlTracker = disableUrlTracker; vm.enableUrlTracker = enableUrlTracker; vm.filter = filter; vm.checkEnabled = checkEnabled; function activate() { vm.checkEnabled().then(function () { vm.search(); }); } function checkEnabled() { vm.dashboard.loading = true; return redirectUrlsResource.getEnableState().then(function (response) { vm.dashboard.urlTrackerDisabled = response.enabled !== true; vm.dashboard.userIsAdmin = response.userIsAdmin; vm.dashboard.loading = false; }); } function goToPage(pageNumber) { vm.pagination.pageIndex = pageNumber - 1; vm.pagination.pageNumber = pageNumber; vm.search(); } function search() { vm.dashboard.loading = true; var searchTerm = vm.dashboard.searchTerm; if (searchTerm === undefined) { searchTerm = ''; } redirectUrlsResource.searchRedirectUrls(searchTerm, vm.pagination.pageIndex, vm.pagination.pageSize).then(function (response) { vm.redirectUrls = response.searchResults; // update pagination vm.pagination.pageIndex = response.currentPage; vm.pagination.pageNumber = response.currentPage + 1; vm.pagination.totalPages = response.pageCount; vm.dashboard.loading = false; }); } function removeRedirect(redirectToDelete) { localizationService.localize('redirectUrls_confirmRemove', [ redirectToDelete.originalUrl, redirectToDelete.destinationUrl ]).then(function (value) { var toggleConfirm = confirm(value); if (toggleConfirm) { redirectUrlsResource.deleteRedirectUrl(redirectToDelete.redirectId).then(function () { var index = vm.redirectUrls.indexOf(redirectToDelete); vm.redirectUrls.splice(index, 1); notificationsService.success(localizationService.localize('redirectUrls_redirectRemoved')); // check if new redirects needs to be loaded if (vm.redirectUrls.length === 0 && vm.pagination.totalPages > 1) { // if we are not on the first page - get records from the previous if (vm.pagination.pageIndex > 0) { vm.pagination.pageIndex = vm.pagination.pageIndex - 1; vm.pagination.pageNumber = vm.pagination.pageNumber - 1; } search(); } }, function (error) { notificationsService.error(localizationService.localize('redirectUrls_redirectRemoveError')); }); } }); } function disableUrlTracker() { localizationService.localize('redirectUrls_confirmDisable').then(function (value) { var toggleConfirm = confirm(value); if (toggleConfirm) { redirectUrlsResource.toggleUrlTracker(true).then(function () { activate(); notificationsService.success(localizationService.localize('redirectUrls_disabledConfirm')); }, function (error) { notificationsService.warning(localizationService.localize('redirectUrls_disableError')); }); } }); } function enableUrlTracker() { redirectUrlsResource.toggleUrlTracker(false).then(function () { activate(); notificationsService.success(localizationService.localize('redirectUrls_enabledConfirm')); }, function (error) { notificationsService.warning(localizationService.localize('redirectUrls_enableError')); }); } var filterDebounced = _.debounce(function (e) { $scope.$apply(function () { //a canceler exists, so perform the cancelation operation and reset if (canceler) { canceler.resolve(); canceler = $q.defer(); } else { canceler = $q.defer(); } vm.search(); }); }, 200); function filter() { vm.dashboard.loading = true; filterDebounced(); } activate(); } angular.module('umbraco').controller('Umbraco.Dashboard.RedirectUrlsController', RedirectUrlsController); }()); function XmlDataIntegrityReportController($scope, umbRequestHelper, $log, $http) { function check(item) { var action = item.check; umbRequestHelper.resourcePromise($http.get(umbRequestHelper.getApiUrl('xmlDataIntegrityBaseUrl', action)), 'Failed to retrieve data integrity status').then(function (result) { item.checking = false; item.invalid = result === 'false'; }); } $scope.fix = function (item) { var action = item.fix; if (item.fix) { if (confirm('This will cause all xml structures for this type to be rebuilt. ' + 'Depending on how much content there is in your site this could take a while. ' + 'It is not recommended to rebuild xml structures if they are not out of sync, during times of high website traffic ' + 'or when editors are editing content.')) { item.fixing = true; umbRequestHelper.resourcePromise($http.post(umbRequestHelper.getApiUrl('xmlDataIntegrityBaseUrl', action)), 'Failed to retrieve data integrity status').then(function (result) { item.fixing = false; item.invalid = result === 'false'; }); } } }; $scope.items = { 'contentXml': { label: 'Content in the cmsContentXml table', checking: true, fixing: false, fix: 'FixContentXmlTable', check: 'CheckContentXmlTable' }, 'mediaXml': { label: 'Media in the cmsContentXml table', checking: true, fixing: false, fix: 'FixMediaXmlTable', check: 'CheckMediaXmlTable' }, 'memberXml': { label: 'Members in the cmsContentXml table', checking: true, fixing: false, fix: 'FixMembersXmlTable', check: 'CheckMembersXmlTable' } }; for (var i in $scope.items) { check($scope.items[i]); } } angular.module('umbraco').controller('Umbraco.Dashboard.XmlDataIntegrityReportController', XmlDataIntegrityReportController); /** * @ngdoc controller * @name Umbraco.Editors.DataType.CreateController * @function * * @description * The controller for the data type creation dialog */ function DataTypeCreateController($scope, $location, navigationService, dataTypeResource, formHelper, appState) { $scope.model = { folderName: '', creatingFolder: false }; var node = $scope.dialogOptions.currentNode; $scope.showCreateFolder = function () { $scope.model.creatingFolder = true; }; $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm, statusMessage: 'Creating folder...' })) { dataTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { navigationService.hideMenu(); var currPath = node.path ? node.path : '-1'; navigationService.syncTree({ tree: 'datatypes', path: currPath + ',' + folderId, forceReload: true, activate: true }); formHelper.resetForm({ scope: $scope }); var section = appState.getSectionState('currentSection'); }, function (err) { }); } ; }; $scope.createDataType = function () { $location.search('create', null); $location.path('/developer/datatypes/edit/' + node.id).search('create', 'true'); navigationService.hideMenu(); }; } angular.module('umbraco').controller('Umbraco.Editors.DataType.CreateController', DataTypeCreateController); /** * @ngdoc controller * @name Umbraco.Editors.ContentDeleteController * @function * * @description * The controller for deleting content */ function DataTypeDeleteController($scope, dataTypeResource, treeService, navigationService) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; dataTypeResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.performContainerDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; dataTypeResource.deleteContainerById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.DataType.DeleteController', DataTypeDeleteController); /** * @ngdoc controller * @name Umbraco.Editors.DataType.EditController * @function * * @description * The controller for the content editor */ function DataTypeEditController($scope, $routeParams, $location, appState, navigationService, treeService, dataTypeResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, formHelper, editorState, dataTypeHelper, eventsService) { //setup scope vars $scope.page = {}; $scope.page.loading = false; $scope.page.nameLocked = false; $scope.page.menu = {}; $scope.page.menu.currentSection = appState.getSectionState('currentSection'); $scope.page.menu.currentNode = null; var evts = []; //method used to configure the pre-values when we retrieve them from the server function createPreValueProps(preVals) { $scope.preValues = []; for (var i = 0; i < preVals.length; i++) { $scope.preValues.push({ hideLabel: preVals[i].hideLabel, alias: preVals[i].key, description: preVals[i].description, label: preVals[i].label, view: preVals[i].view, value: preVals[i].value, config: preVals[i].config }); } } //set up the standard data type props $scope.properties = { selectedEditor: { alias: 'selectedEditor', description: 'Select a property editor', label: 'Property editor' }, selectedEditorId: { alias: 'selectedEditorId', label: 'Property editor alias' } }; //setup the pre-values as props $scope.preValues = []; if ($routeParams.create) { $scope.page.loading = true; $scope.showIdentifier = false; //we are creating so get an empty data type item dataTypeResource.getScaffold($routeParams.id).then(function (data) { $scope.preValuesLoaded = true; $scope.content = data; setHeaderNameState($scope.content); //set a shared state editorState.set($scope.content); $scope.page.loading = false; }); } else { loadDataType(); } function loadDataType() { $scope.page.loading = true; $scope.showIdentifier = true; //we are editing so get the content item from the server dataTypeResource.getById($routeParams.id).then(function (data) { $scope.preValuesLoaded = true; $scope.content = data; createPreValueProps($scope.content.preValues); setHeaderNameState($scope.content); //share state editorState.set($scope.content); //in one particular special case, after we've created a new item we redirect back to the edit // route but there might be server validation errors in the collection which we need to display // after the redirect, so we will bind all subscriptions which will show the server validation errors // if there are any and then clear them so the collection no longer persists them. serverValidationManager.executeAndClearAllSubscriptions(); navigationService.syncTree({ tree: 'datatypes', path: data.path }).then(function (syncArgs) { $scope.page.menu.currentNode = syncArgs.node; }); $scope.page.loading = false; }); } $scope.$watch('content.selectedEditor', function (newVal, oldVal) { //when the value changes, we need to dynamically load in the new editor if (newVal && (newVal != oldVal && (oldVal || $routeParams.create))) { //we are editing so get the content item from the server var currDataTypeId = $routeParams.create ? undefined : $routeParams.id; dataTypeResource.getPreValues(newVal, currDataTypeId).then(function (data) { $scope.preValuesLoaded = true; $scope.content.preValues = data; createPreValueProps($scope.content.preValues); setHeaderNameState($scope.content); //share state editorState.set($scope.content); }); } }); function setHeaderNameState(content) { if (content.isSystem == 1) { $scope.page.nameLocked = true; } } $scope.save = function () { if (formHelper.submitForm({ scope: $scope, statusMessage: 'Saving...' })) { $scope.page.saveButtonState = 'busy'; dataTypeResource.save($scope.content, $scope.preValues, $routeParams.create).then(function (data) { formHelper.resetForm({ scope: $scope, notifications: data.notifications }); contentEditingHelper.handleSuccessfulSave({ scope: $scope, savedContent: data, rebindCallback: function () { createPreValueProps(data.preValues); } }); setHeaderNameState($scope.content); //share state editorState.set($scope.content); navigationService.syncTree({ tree: 'datatypes', path: data.path, forceReload: true }).then(function (syncArgs) { $scope.page.menu.currentNode = syncArgs.node; }); $scope.page.saveButtonState = 'success'; dataTypeHelper.rebindChangedProperties($scope.content, data); }, function (err) { //NOTE: in the case of data type values we are setting the orig/new props // to be the same thing since that only really matters for content/media. contentEditingHelper.handleSaveError({ redirectOnFailure: false, err: err }); $scope.page.saveButtonState = 'error'; //share state editorState.set($scope.content); }); } }; evts.push(eventsService.on('app.refreshEditor', function (name, error) { loadDataType(); })); //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { eventsService.unsubscribe(evts[e]); } }); } angular.module('umbraco').controller('Umbraco.Editors.DataType.EditController', DataTypeEditController); angular.module('umbraco').controller('Umbraco.Editors.DataType.MoveController', function ($scope, dataTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { var dialogOptions = $scope.dialogOptions; $scope.dialogTreeEventHandler = $({}); function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } $scope.move = function () { $scope.busy = true; $scope.error = false; dataTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }).then(function (path) { $scope.error = false; $scope.success = true; $scope.busy = false; //first we need to remove the node that launched the dialog treeService.removeNode($scope.currentNode); //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the moved content - but don't activate the node, //then sync to the currenlty edited content (note: this might not be the content that was moved!!) navigationService.syncTree({ tree: 'dataTypes', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'dataTypes', path: activeNodePath, forceReload: false, activate: true }); } }); eventsService.emit('app.refreshEditor'); }, function (err) { $scope.success = false; $scope.error = err; $scope.busy = false; //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); }); /** * @ngdoc controller * @name Umbraco.Editors.Dictionary.CreateController * @function * * @description * The controller for creating dictionary items */ function DictionaryCreateController($scope, $location, dictionaryResource, navigationService, notificationsService, formHelper, appState) { var vm = this; vm.itemKey = ''; function createItem() { var node = $scope.dialogOptions.currentNode; dictionaryResource.create(node.id, vm.itemKey).then(function (data) { navigationService.hideMenu(); // set new item as active in tree var currPath = node.path ? node.path : '-1'; navigationService.syncTree({ tree: 'dictionary', path: currPath + ',' + data, forceReload: true, activate: true }); // reset form state formHelper.resetForm({ scope: $scope }); // navigate to edit view var currentSection = appState.getSectionState('currentSection'); $location.path('/' + currentSection + '/dictionary/edit/' + data); }, function (err) { if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); } }); } vm.createItem = createItem; } angular.module('umbraco').controller('Umbraco.Editors.Dictionary.CreateController', DictionaryCreateController); /** * @ngdoc controller * @name Umbraco.Editors.Dictionary.DeleteController * @function * * @description * The controller for deleting dictionary items */ function DictionaryDeleteController($scope, $location, dictionaryResource, treeService, navigationService, appState) { var vm = this; function cancel() { navigationService.hideDialog(); } function performDelete() { // stop from firing again on double-click if ($scope.busy) { return false; } //mark it for deletion (used in the UI) $scope.currentNode.loading = true; $scope.busy = true; dictionaryResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; // get the parent id var parentId = $scope.currentNode.parentId; treeService.removeNode($scope.currentNode); navigationService.hideMenu(); var currentSection = appState.getSectionState('currentSection'); if (parentId !== '-1') { // set the view of the parent item $location.path('/' + currentSection + '/dictionary/edit/' + parentId); } else { // we have no parent, so redirect to section $location.path('/' + currentSection + '/'); } }); } vm.cancel = cancel; vm.performDelete = performDelete; } angular.module('umbraco').controller('Umbraco.Editors.Dictionary.DeleteController', DictionaryDeleteController); /** * @ngdoc controller * @name Umbraco.Editors.Dictionary.EditController * @function * * @description * The controller for editing dictionary items */ function DictionaryEditController($scope, $routeParams, dictionaryResource, treeService, navigationService, appState, editorState, contentEditingHelper, formHelper, notificationsService, localizationService) { var vm = this; //setup scope vars vm.nameDirty = false; vm.page = {}; vm.page.loading = false; vm.page.nameLocked = false; vm.page.menu = {}; vm.page.menu.currentSection = appState.getSectionState('currentSection'); vm.page.menu.currentNode = null; vm.description = ''; function loadDictionary() { vm.page.loading = true; //we are editing so get the content item from the server dictionaryResource.getById($routeParams.id).then(function (data) { bindDictionary(data); vm.page.loading = false; }); } function createTranslationProperty(translation) { return { alias: translation.isoCode, label: translation.displayName, hideLabel: false }; } function bindDictionary(data) { localizationService.localize('dictionaryItem_description').then(function (value) { vm.description = value.replace('%0%', data.name); }); // create data for umb-property displaying for (var i = 0; i < data.translations.length; i++) { data.translations[i].property = createTranslationProperty(data.translations[i]); } contentEditingHelper.handleSuccessfulSave({ scope: $scope, savedContent: data }); // set content vm.content = data; //share state editorState.set(vm.content); navigationService.syncTree({ tree: 'dictionary', path: data.path, forceReload: true }).then(function (syncArgs) { vm.page.menu.currentNode = syncArgs.node; }); } function onInit() { loadDictionary(); } function saveDictionary() { if (formHelper.submitForm({ scope: $scope, statusMessage: 'Saving...' })) { vm.page.saveButtonState = 'busy'; dictionaryResource.save(vm.content, vm.nameDirty).then(function (data) { formHelper.resetForm({ scope: $scope, notifications: data.notifications }); bindDictionary(data); vm.page.saveButtonState = 'success'; }, function (err) { contentEditingHelper.handleSaveError({ redirectOnFailure: false, err: err }); notificationsService.error(err.data.message); vm.page.saveButtonState = 'error'; }); } } vm.save = saveDictionary; $scope.$watch('vm.content.name', function (newVal, oldVal) { //when the value changes, we need to set the name dirty if (newVal && newVal !== oldVal && typeof oldVal !== 'undefined') { vm.nameDirty = true; } }); onInit(); } angular.module('umbraco').controller('Umbraco.Editors.Dictionary.EditController', DictionaryEditController); /** * @ngdoc controller * @name Umbraco.Editors.Dictionary.ListController * @function * * @description * The controller for listting dictionary items */ function DictionaryListController($scope, $location, dictionaryResource, localizationService, appState) { var vm = this; vm.title = 'Dictionary overview'; vm.loading = false; vm.items = []; function loadList() { vm.loading = true; dictionaryResource.getList().then(function (data) { vm.items = data; vm.loading = false; }); } function clickItem(id) { var currentSection = appState.getSectionState('currentSection'); $location.path('/' + currentSection + '/dictionary/edit/' + id); } vm.clickItem = clickItem; function onInit() { localizationService.localize('dictionaryItem_overviewTitle').then(function (value) { vm.title = value; }); loadList(); } onInit(); } angular.module('umbraco').controller('Umbraco.Editors.Dictionary.ListController', DictionaryListController); angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.CopyController', function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { var dialogOptions = $scope.dialogOptions; $scope.dialogTreeEventHandler = $({}); function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } $scope.copy = function () { $scope.busy = true; $scope.error = false; contentTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }).then(function (path) { $scope.error = false; $scope.success = true; $scope.busy = false; //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the copied content - but don't activate the node, //then sync to the currenlty edited content (note: this might not be the content that was copied!!) navigationService.syncTree({ tree: 'documentTypes', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'documentTypes', path: activeNodePath, forceReload: false, activate: true }); } }); }, function (err) { $scope.success = false; $scope.error = err; $scope.busy = false; //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); }); /** * @ngdoc controller * @name Umbraco.Editors.DocumentType.CreateController * @function * * @description * The controller for the doc type creation dialog */ function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService, iconHelper) { $scope.model = { allowCreateFolder: $scope.dialogOptions.currentNode.parentId === null || $scope.dialogOptions.currentNode.nodeType === 'container', folderName: '', creatingFolder: false, creatingDoctypeCollection: false }; var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; $scope.model.disableTemplates = disableTemplates; var node = $scope.dialogOptions.currentNode, localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); $scope.showCreateFolder = function () { $scope.model.creatingFolder = true; }; $scope.showCreateDocTypeCollection = function () { $scope.model.creatingDoctypeCollection = true; $scope.model.collectionCreateTemplate = !$scope.model.disableTemplates; $scope.model.collectionItemCreateTemplate = !$scope.model.disableTemplates; }; $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm, statusMessage: localizeCreateFolder })) { contentTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { navigationService.hideMenu(); var currPath = node.path ? node.path : '-1'; navigationService.syncTree({ tree: 'documenttypes', path: currPath + ',' + folderId, forceReload: true, activate: true }); formHelper.resetForm({ scope: $scope }); var section = appState.getSectionState('currentSection'); }, function (err) { $scope.error = err; //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); } }; $scope.createCollection = function () { if (formHelper.submitForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm, statusMessage: 'Creating Doctype Collection...' })) { // see if we can find matching icons var collectionIcon = 'icon-folders', collectionItemIcon = 'icon-document'; iconHelper.getIcons().then(function (icons) { for (var i = 0; i < icons.length; i++) { // for matching we'll require a full match for collection, partial match for item if (icons[i].substring(5) == $scope.model.collectionName.toLowerCase()) { collectionIcon = icons[i]; } else if (icons[i].substring(5).indexOf($scope.model.collectionItemName.toLowerCase()) > -1) { collectionItemIcon = icons[i]; } } contentTypeResource.createCollection(node.id, $scope.model.collectionName, $scope.model.collectionCreateTemplate, $scope.model.collectionItemName, $scope.model.collectionItemCreateTemplate, collectionIcon, collectionItemIcon).then(function (collectionData) { navigationService.hideMenu(); $location.search('create', null); $location.search('notemplate', null); formHelper.resetForm({ scope: $scope }); var section = appState.getSectionState('currentSection'); // redirect to the item id $location.path('/settings/documenttypes/edit/' + collectionData.ItemId); }, function (err) { $scope.error = err; //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }); } }; // Disabling logic for creating document type with template if disableTemplates is set to true if (!disableTemplates) { $scope.createDocType = function () { $location.search('create', null); $location.search('notemplate', null); $location.path('/settings/documenttypes/edit/' + node.id).search('create', 'true'); navigationService.hideMenu(); }; } $scope.createComponent = function () { $location.search('create', null); $location.search('notemplate', null); $location.path('/settings/documenttypes/edit/' + node.id).search('create', 'true').search('notemplate', 'true'); navigationService.hideMenu(); }; } angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.CreateController', DocumentTypesCreateController); /** * @ngdoc controller * @name Umbraco.Editors.DocumentType.DeleteController * @function * * @description * The controller for deleting content */ function DocumentTypesDeleteController($scope, dataTypeResource, contentTypeResource, treeService, navigationService) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; contentTypeResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.performContainerDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; contentTypeResource.deleteContainerById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.DeleteController', DocumentTypesDeleteController); /** * @ngdoc controller * @name Umbraco.Editors.DocumentType.EditController * @function * * @description * The controller for the content type editor */ (function () { 'use strict'; function DocumentTypesEditController($scope, $routeParams, $injector, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService, angularHelper) { var vm = this; var localizeSaving = localizationService.localize('general_saving'); var evts = []; var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; var buttons = [ { 'name': localizationService.localize('general_design'), 'alias': 'design', 'icon': 'icon-document-dashed-line', 'view': 'views/documenttypes/views/design/design.html', 'active': true }, { 'name': localizationService.localize('general_listView'), 'alias': 'listView', 'icon': 'icon-list', 'view': 'views/documenttypes/views/listview/listview.html' }, { 'name': localizationService.localize('general_rights'), 'alias': 'permissions', 'icon': 'icon-keychain', 'view': 'views/documenttypes/views/permissions/permissions.html' }, { 'name': localizationService.localize('treeHeaders_templates'), 'alias': 'templates', 'icon': 'icon-layout', 'view': 'views/documenttypes/views/templates/templates.html' } ]; vm.save = save; vm.currentNode = null; vm.contentType = {}; vm.page = {}; vm.page.loading = false; vm.page.saveButtonState = 'init'; vm.page.navigation = []; loadButtons(); vm.page.keyboardShortcutsOverview = [ { 'name': localizationService.localize('main_sections'), 'shortcuts': [{ 'description': localizationService.localize('shortcuts_navigateSections'), 'keys': [ { 'key': '1' }, { 'key': '4' } ], 'keyRange': true }] }, { 'name': localizationService.localize('general_design'), 'shortcuts': [ { 'description': localizationService.localize('shortcuts_addTab'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 't' } ] }, { 'description': localizationService.localize('shortcuts_addProperty'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'p' } ] }, { 'description': localizationService.localize('shortcuts_addEditor'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'e' } ] }, { 'description': localizationService.localize('shortcuts_editDataType'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'd' } ] } ] }, { 'name': localizationService.localize('general_listView'), 'shortcuts': [{ 'description': localizationService.localize('shortcuts_toggleListView'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'l' } ] }] }, { 'name': localizationService.localize('general_rights'), 'shortcuts': [ { 'description': localizationService.localize('shortcuts_toggleAllowAsRoot'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'r' } ] }, { 'description': localizationService.localize('shortcuts_addChildNode'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'c' } ] } ] }, { 'name': localizationService.localize('treeHeaders_templates'), 'shortcuts': [{ 'description': localizationService.localize('shortcuts_addTemplate'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 't' } ] }] } ]; contentTypeHelper.checkModelsBuilderStatus().then(function (result) { vm.page.modelsBuilder = result; if (result) { //Models builder mode: vm.page.defaultButton = { alias: 'save', hotKey: 'ctrl+s', hotKeyWhenHidden: true, labelKey: 'buttons_save', letter: 'S', type: 'submit', handler: function () { vm.save(); } }; vm.page.subButtons = [{ alias: 'saveAndGenerateModels', hotKey: 'ctrl+g', hotKeyWhenHidden: true, labelKey: 'buttons_saveAndGenerateModels', letter: 'G', handler: function () { vm.page.saveButtonState = 'busy'; vm.save().then(function (result) { vm.page.saveButtonState = 'busy'; localizationService.localize('modelsBuilder_buildingModels').then(function (headerValue) { localizationService.localize('modelsBuilder_waitingMessage').then(function (msgValue) { notificationsService.info(headerValue, msgValue); }); }); contentTypeHelper.generateModels().then(function (result) { // generateModels() returns the dashboard content if (!result.lastError) { //re-check model status contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { vm.page.modelsBuilder = statusResult; }); //clear and add success vm.page.saveButtonState = 'init'; localizationService.localize('modelsBuilder_modelsGenerated').then(function (value) { notificationsService.success(value); }); } else { vm.page.saveButtonState = 'error'; localizationService.localize('modelsBuilder_modelsExceptionInUlog').then(function (value) { notificationsService.error(value); }); } }, function () { vm.page.saveButtonState = 'error'; localizationService.localize('modelsBuilder_modelsGeneratedError').then(function (value) { notificationsService.error(value); }); }); }); } }]; } }); if ($routeParams.create) { vm.page.loading = true; //we are creating so get an empty data type item contentTypeResource.getScaffold($routeParams.id).then(function (dt) { init(dt); vm.page.loading = false; }); } else { loadDocumentType(); } function loadDocumentType() { vm.page.loading = true; contentTypeResource.getById($routeParams.id).then(function (dt) { init(dt); syncTreeNode(vm.contentType, dt.path, true); vm.page.loading = false; }); } function loadButtons() { angular.forEach(buttons, function (val, index) { if (disableTemplates === true && val.alias === 'templates') { buttons.splice(index, 1); } }); vm.page.navigation = buttons; } /* ---------- SAVE ---------- */ function save() { // only save if there is no overlays open if (overlayHelper.getNumberOfOverlays() === 0) { var deferred = $q.defer(); vm.page.saveButtonState = 'busy'; // reformat allowed content types to array if id's vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); contentEditingHelper.contentEditorPerformSave({ statusMessage: localizeSaving, saveMethod: contentTypeResource.save, scope: $scope, content: vm.contentType, //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc // type when server side validation fails - as opposed to content where we are capable of saving the content // item if server side validation fails redirectOnFailure: false, // we need to rebind... the IDs that have been created! rebindCallback: function (origContentType, savedContentType) { vm.contentType.id = savedContentType.id; vm.contentType.groups.forEach(function (group) { if (!group.name) return; var k = 0; while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) k++; if (k == savedContentType.groups.length) { group.id = 0; return; } var savedGroup = savedContentType.groups[k]; if (!group.id) group.id = savedGroup.id; group.properties.forEach(function (property) { if (property.id || !property.alias) return; k = 0; while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) k++; if (k == savedGroup.properties.length) { property.id = 0; return; } var savedProperty = savedGroup.properties[k]; property.id = savedProperty.id; }); }); } }).then(function (data) { //success syncTreeNode(vm.contentType, data.path); vm.page.saveButtonState = 'success'; deferred.resolve(data); }, function (err) { //error if (err) { editorState.set($scope.content); } else { localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { notificationsService.error(headerValue, msgValue); }); }); } vm.page.saveButtonState = 'error'; deferred.reject(err); }); return deferred.promise; } } function init(contentType) { // set all tab to inactive if (contentType.groups.length !== 0) { angular.forEach(contentType.groups, function (group) { angular.forEach(group.properties, function (property) { // get data type details for each property getDataTypeDetails(property); }); }); } // insert template on new doc types if (!$routeParams.notemplate && contentType.id === 0) { contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); contentType.allowedTemplates = contentTypeHelper.insertTemplatePlaceholder(contentType.allowedTemplates); } // convert icons for content type convertLegacyIcons(contentType); //set a shared state editorState.set(contentType); vm.contentType = contentType; } function convertLegacyIcons(contentType) { // make array to store contentType icon var contentTypeArray = []; // push icon to array contentTypeArray.push({ 'icon': contentType.icon }); // run through icon method iconHelper.formatContentTypeIcons(contentTypeArray); // set icon back on contentType contentType.icon = contentTypeArray[0].icon; } function getDataTypeDetails(property) { if (property.propertyState !== 'init') { dataTypeResource.getById(property.dataTypeId).then(function (dataType) { property.dataTypeIcon = dataType.icon; property.dataTypeName = dataType.name; }); } } /** Syncs the content type to it's tree node - this occurs on first load and after saving */ function syncTreeNode(dt, path, initialLoad) { navigationService.syncTree({ tree: 'documenttypes', path: path.split(','), forceReload: initialLoad !== true }).then(function (syncArgs) { vm.currentNode = syncArgs.node; }); } evts.push(eventsService.on('app.refreshEditor', function (name, error) { loadDocumentType(); })); //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { eventsService.unsubscribe(evts[e]); } }); // #3368 - changes on the other "buttons" do not register on the current form, so we manually have to flag the form as dirty $scope.$watch('vm.contentType.allowedContentTypes.length + vm.contentType.allowAsRoot + vm.contentType.allowedTemplates.length + vm.contentType.isContainer', function (newVal, oldVal) { if (oldVal === undefined) { // still initializing, ignore return; } angularHelper.getCurrentForm($scope).$setDirty(); }); } angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.EditController', DocumentTypesEditController); }()); angular.module('umbraco').controller('Umbraco.Editors.DocumentTypes.MoveController', function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { var dialogOptions = $scope.dialogOptions; $scope.dialogTreeEventHandler = $({}); function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } $scope.move = function () { $scope.busy = true; $scope.error = false; contentTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }).then(function (path) { $scope.error = false; $scope.success = true; $scope.busy = false; //first we need to remove the node that launched the dialog treeService.removeNode($scope.currentNode); //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the moved content - but don't activate the node, //then sync to the currenlty edited content (note: this might not be the content that was moved!!) navigationService.syncTree({ tree: 'documentTypes', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'documentTypes', path: activeNodePath, forceReload: false, activate: true }); } }); eventsService.emit('app.refreshEditor'); }, function (err) { $scope.success = false; $scope.error = err; $scope.busy = false; //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); }); angular.module('umbraco').controller('Umbraco.Editors.ContentTypeContainers.RenameController', [ '$scope', '$injector', 'navigationService', 'notificationsService', 'localizationService', function (scope, injector, navigationService, notificationsService, localizationService) { var notificationHeader; function reportSuccessAndClose(treeName) { var lastComma = scope.currentNode.path.lastIndexOf(','), path = lastComma === -1 ? scope.currentNode.path : scope.currentNode.path.substring(0, lastComma - 1); navigationService.syncTree({ tree: treeName, path: path, forceReload: true, activate: true }); localizationService.localize('renamecontainer_folderWasRenamed', [ scope.currentNode.name, scope.model.folderName ]).then(function (msg) { notificationsService.showNotification({ type: 0, header: notificationHeader, message: msg }); }); navigationService.hideMenu(); } localizationService.localize('renamecontainer_renamed').then(function (s) { notificationHeader = s; }); scope.model = { folderName: scope.currentNode.name }; scope.renameContainer = function (resourceKey, treeName) { var resource = injector.get(resourceKey); resource.renameContainer(scope.currentNode.id, scope.model.folderName).then(function () { reportSuccessAndClose(treeName); }, function (err) { scope.error = err; if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; } ]); /** * @ngdoc controller * @name Umbraco.Editors.DocumentType.PropertyController * @function * * @description * The controller for the content type editor property dialog */ (function () { 'use strict'; function PermissionsController($scope, contentTypeResource, iconHelper, contentTypeHelper, localizationService) { /* ----------- SCOPE VARIABLES ----------- */ var vm = this; var childNodeSelectorOverlayTitle = ''; vm.contentTypes = []; vm.selectedChildren = []; vm.overlayTitle = ''; vm.addChild = addChild; vm.removeChild = removeChild; vm.toggle = toggle; /* ---------- INIT ---------- */ init(); function init() { childNodeSelectorOverlayTitle = localizationService.localize('contentTypeEditor_chooseChildNode'); contentTypeResource.getAll().then(function (contentTypes) { vm.contentTypes = contentTypes; // convert legacy icons iconHelper.formatContentTypeIcons(vm.contentTypes); vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.contentTypes); if ($scope.model.id === 0) { contentTypeHelper.insertChildNodePlaceholder(vm.contentTypes, $scope.model.name, $scope.model.icon, $scope.model.id); } }); } function addChild($event) { vm.childNodeSelectorOverlay = { view: 'itempicker', title: childNodeSelectorOverlayTitle, availableItems: vm.contentTypes, selectedItems: vm.selectedChildren, event: $event, show: true, submit: function (model) { vm.selectedChildren.push(model.selectedItem); $scope.model.allowedContentTypes.push(model.selectedItem.id); vm.childNodeSelectorOverlay.show = false; vm.childNodeSelectorOverlay = null; } }; } function removeChild(selectedChild, index) { // remove from vm vm.selectedChildren.splice(index, 1); // remove from content type model var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); } /** * Toggle the $scope.model.allowAsRoot value to either true or false */ function toggle() { if ($scope.model.allowAsRoot) { $scope.model.allowAsRoot = false; return; } $scope.model.allowAsRoot = true; } } angular.module('umbraco').controller('Umbraco.Editors.DocumentType.PermissionsController', PermissionsController); }()); /** * @ngdoc controller * @name Umbraco.Editors.DocumentType.TemplatesController * @function * * @description * The controller for the content type editor templates sub view */ (function () { 'use strict'; function TemplatesController($scope, entityResource, contentTypeHelper, templateResource, $routeParams) { /* ----------- SCOPE VARIABLES ----------- */ var vm = this; vm.availableTemplates = []; vm.canCreateTemplate = false; vm.updateTemplatePlaceholder = false; vm.createTemplate = createTemplate; /* ---------- INIT ---------- */ function onInit() { entityResource.getAll('Template').then(function (templates) { vm.availableTemplates = templates; // update placeholder template information on new doc types if (!$routeParams.notemplate && $scope.model.id === 0) { vm.updateTemplatePlaceholder = true; vm.availableTemplates = contentTypeHelper.insertTemplatePlaceholder(vm.availableTemplates); } checkIfTemplateExists(); }); } function createTemplate() { vm.createTemplateButtonState = 'busy'; templateResource.getScaffold(-1).then(function (template) { template.alias = $scope.model.alias; template.name = $scope.model.name; templateResource.save(template).then(function (savedTemplate) { // add icon savedTemplate.icon = 'icon-layout'; vm.availableTemplates.push(savedTemplate); vm.canCreateTemplate = false; $scope.model.allowedTemplates.push(savedTemplate); if ($scope.model.defaultTemplate === null) { $scope.model.defaultTemplate = savedTemplate; } vm.createTemplateButtonState = 'success'; }, function () { vm.createTemplateButtonState = 'error'; }); }, function () { vm.createTemplateButtonState = 'error'; }); } ; function checkIfTemplateExists() { var existingTemplate = vm.availableTemplates.find(function (availableTemplate) { return availableTemplate.name === $scope.model.name || availableTemplate.placeholder; }); vm.canCreateTemplate = existingTemplate ? false : true; } onInit(); } angular.module('umbraco').controller('Umbraco.Editors.DocumentType.TemplatesController', TemplatesController); }()); /** * @ngdoc controller * @name Umbraco.Editors.Media.CreateController * @function * * @description * The controller for the media creation dialog */ function mediaCreateController($scope, $routeParams, $location, mediaTypeResource, iconHelper, navigationService) { mediaTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); }); $scope.createMediaItem = function (docType) { $location.path('/media/media/edit/' + $scope.currentNode.id).search('doctype', docType.alias).search('create', 'true'); navigationService.hideMenu(); }; } angular.module('umbraco').controller('Umbraco.Editors.Media.CreateController', mediaCreateController); /** * @ngdoc controller * @name Umbraco.Editors.ContentDeleteController * @function * * @description * The controller for deleting content */ function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, dialogService, notificationsService) { $scope.performDelete = function () { // stop from firing again on double-click if ($scope.busy) { return false; } //mark it for deletion (used in the UI) $scope.currentNode.loading = true; $scope.busy = true; mediaResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); treeService.removeNode($scope.currentNode); if (rootNode) { //ensure the recycle bin has child nodes now var recycleBin = treeService.getDescendantNode(rootNode, -21); if (recycleBin) { recycleBin.hasChildren = true; //reload the recycle bin if it's already expanded so the deleted item is shown if (recycleBin.expanded) { treeService.loadNodeChildren({ node: recycleBin, section: 'media' }); } } } //if the current edited item is the same one as we're deleting, we need to navigate elsewhere if (editorState.current && editorState.current.id == $scope.currentNode.id) { //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent var location = '/media'; if ($scope.currentNode.parentId.toString() === '-21') location = '/media/media/recyclebin'; else if ($scope.currentNode.parentId.toString() !== '-1') location = '/media/media/edit/' + $scope.currentNode.parentId; $location.path(location); } $scope.success = true; }, function (err) { $scope.currentNode.loading = false; $scope.busy = false; //check if response is ysod if (err.status && err.status >= 500) { dialogService.ysodDialog(err); } if (err.data && angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.Media.DeleteController', MediaDeleteController); /** * @ngdoc controller * @name Umbraco.Editors.Media.EditController * @function * * @description * The controller for the media editor */ function mediaEditController($scope, $routeParams, appState, mediaResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, treeService, formHelper, umbModelMapper, editorState, umbRequestHelper, $http) { //setup scope vars $scope.currentSection = appState.getSectionState('currentSection'); $scope.currentNode = null; //the editors affiliated node $scope.page = {}; $scope.page.loading = false; $scope.page.menu = {}; $scope.page.menu.currentSection = appState.getSectionState('currentSection'); $scope.page.menu.currentNode = null; //the editors affiliated node $scope.page.listViewPath = null; $scope.page.saveButtonState = 'init'; /** Syncs the content item to it's tree node - this occurs on first load and after saving */ function syncTreeNode(content, path, initialLoad) { if (!$scope.content.isChildOfListView) { navigationService.syncTree({ tree: 'media', path: path.split(','), forceReload: initialLoad !== true }).then(function (syncArgs) { $scope.page.menu.currentNode = syncArgs.node; }); } else if (initialLoad === true) { //it's a child item, just sync the ui node to the parent navigationService.syncTree({ tree: 'media', path: path.substring(0, path.lastIndexOf(',')).split(','), forceReload: initialLoad !== true }); //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 // from the server so that we can load in the actions menu. umbRequestHelper.resourcePromise($http.get(content.treeNodeUrl), 'Failed to retrieve data for child node ' + content.id).then(function (node) { $scope.page.menu.currentNode = node; }); } } if ($routeParams.create) { $scope.page.loading = true; mediaResource.getScaffold($routeParams.id, $routeParams.doctype).then(function (data) { $scope.content = data; editorState.set($scope.content); // We don't get the info tab from the server from version 7.8 so we need to manually add it contentEditingHelper.addInfoTab($scope.content.tabs); $scope.page.loading = false; }); } else { $scope.page.loading = true; mediaResource.getById($routeParams.id).then(function (data) { $scope.content = data; if (data.isChildOfListView && data.trashed === false) { $scope.page.listViewPath = $routeParams.page ? '/media/media/edit/' + data.parentId + '?page=' + $routeParams.page : '/media/media/edit/' + data.parentId; } editorState.set($scope.content); //in one particular special case, after we've created a new item we redirect back to the edit // route but there might be server validation errors in the collection which we need to display // after the redirect, so we will bind all subscriptions which will show the server validation errors // if there are any and then clear them so the collection no longer persists them. serverValidationManager.executeAndClearAllSubscriptions(); syncTreeNode($scope.content, data.path, true); if ($scope.content.parentId && $scope.content.parentId != -1) { //We fetch all ancestors of the node to generate the footer breadcrump navigation entityResource.getAncestors($routeParams.id, 'media').then(function (anc) { $scope.ancestors = anc; }); } // We don't get the info tab from the server from version 7.8 so we need to manually add it contentEditingHelper.addInfoTab($scope.content.tabs); $scope.page.loading = false; }); } $scope.save = function () { if (!$scope.busy && formHelper.submitForm({ scope: $scope, statusMessage: 'Saving...' })) { $scope.busy = true; $scope.page.saveButtonState = 'busy'; mediaResource.save($scope.content, $routeParams.create, fileManager.getFiles()).then(function (data) { formHelper.resetForm({ scope: $scope, notifications: data.notifications }); contentEditingHelper.handleSuccessfulSave({ scope: $scope, savedContent: data, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) }); editorState.set($scope.content); $scope.busy = false; syncTreeNode($scope.content, data.path); $scope.page.saveButtonState = 'success'; }, function (err) { contentEditingHelper.handleSaveError({ err: err, redirectOnFailure: true, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) }); //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } editorState.set($scope.content); $scope.busy = false; $scope.page.saveButtonState = 'error'; }); } else { $scope.busy = false; } }; } angular.module('umbraco').controller('Umbraco.Editors.Media.EditController', mediaEditController); /** * @ngdoc controller * @name Umbraco.Editors.Media.EmptyRecycleBinController * @function * * @description * The controller for deleting media */ function MediaEmptyRecycleBinController($scope, mediaResource, treeService, navigationService, notificationsService, $route) { $scope.busy = false; $scope.performDelete = function () { //(used in the UI) $scope.busy = true; $scope.currentNode.loading = true; mediaResource.emptyRecycleBin($scope.currentNode.id).then(function (result) { $scope.busy = false; $scope.currentNode.loading = false; //show any notifications if (angular.isArray(result.notifications)) { for (var i = 0; i < result.notifications.length; i++) { notificationsService.showNotification(result.notifications[i]); } } treeService.removeChildNodes($scope.currentNode); navigationService.hideMenu(); //reload the current view $route.reload(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.Media.EmptyRecycleBinController', MediaEmptyRecycleBinController); //used for the media picker dialog angular.module('umbraco').controller('Umbraco.Editors.Media.MoveController', function ($scope, userService, eventsService, mediaResource, appState, treeService, navigationService) { var dialogOptions = $scope.dialogOptions; $scope.dialogTreeEventHandler = $({}); var node = dialogOptions.currentNode; $scope.busy = false; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, results: [], selectedSearchResults: [] }; $scope.treeModel = { hideHeader: false }; userService.getCurrentUser().then(function (userData) { $scope.treeModel.hideHeader = userData.startMediaIds.length > 0 && userData.startMediaIds.indexOf(-1) == -1; }); function treeLoadedHandler(ev, args) { if (node && node.path) { $scope.dialogTreeEventHandler.syncTree({ path: node.path, activate: false }); } } function nodeSelectHandler(ev, args) { if (args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); } eventsService.emit('editors.media.moveController.select', args); if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } function nodeExpandedHandler(ev, args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } } $scope.hideSearch = function () { $scope.searchInfo.showSearch = false; $scope.searchInfo.searchFromId = null; $scope.searchInfo.searchFromName = null; $scope.searchInfo.results = []; }; // method to select a search result $scope.selectResult = function (evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); }; //callback when there are search results $scope.onSearchResults = function (results) { $scope.searchInfo.results = results; $scope.searchInfo.showSearch = true; }; $scope.move = function () { $scope.busy = true; mediaResource.move({ parentId: $scope.target.id, id: node.id }).then(function (path) { $scope.busy = false; $scope.error = false; $scope.success = true; //first we need to remove the node that launched the dialog treeService.removeNode($scope.currentNode); //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the moved content - but don't activate the node, //then sync to the currenlty edited content (note: this might not be the content that was moved!!) navigationService.syncTree({ tree: 'media', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'media', path: activeNodePath, forceReload: false, activate: true }); } }); }, function (err) { $scope.success = false; $scope.error = err; }); }; $scope.dialogTreeEventHandler.bind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeLoaded', treeLoadedHandler); $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; nodeSelectHandler({}, { node: node }); }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; }; function openMiniListView(node) { $scope.miniListView = node; } }); /** * @ngdoc controller * @name Umbraco.Editors.Content.MediaRecycleBinController * @function * * @description * Controls the recycle bin for media * */ function MediaRecycleBinController($scope, $routeParams, mediaResource, navigationService, localizationService) { //ensures the list view doesn't actually load until we query for the list view config // for the section $scope.page = {}; $scope.page.name = 'Recycle Bin'; $scope.page.nameLocked = true; //ensures the list view doesn't actually load until we query for the list view config // for the section $scope.listViewPath = null; $routeParams.id = '-21'; mediaResource.getRecycleBin().then(function (result) { //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a // single property, so we'll extract that property (list view) and use it's data. var listproperty = result.tabs[0].properties[0]; _.each(listproperty.config, function (val, key) { $scope.model.config[key] = val; }); $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; }); $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; // sync tree node navigationService.syncTree({ tree: 'media', path: [ '-1', $routeParams.id ], forceReload: false }); localizePageName(); function localizePageName() { var pageName = 'general_recycleBin'; localizationService.localize(pageName).then(function (value) { $scope.page.name = value; }); } } angular.module('umbraco').controller('Umbraco.Editors.Media.RecycleBinController', MediaRecycleBinController); angular.module('umbraco').controller('Umbraco.Editors.Media.RestoreController', function ($scope, relationResource, mediaResource, navigationService, appState, treeService, userService, localizationService) { var dialogOptions = $scope.dialogOptions; $scope.source = _.clone(dialogOptions.currentNode); $scope.error = null; $scope.loading = true; $scope.moving = false; $scope.success = false; $scope.dialogTreeEventHandler = $({}); $scope.searchInfo = { showSearch: false, results: [], selectedSearchResults: [] }; $scope.treeModel = { hideHeader: false }; userService.getCurrentUser().then(function (userData) { $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); $scope.labels = {}; localizationService.localizeMany(['treeHeaders_media']).then(function (data) { $scope.labels.treeRoot = data[0]; }); function nodeSelectHandler(ev, args) { if (args && args.event) { args.event.preventDefault(); args.event.stopPropagation(); } if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } function nodeExpandedHandler(ev, args) { // open mini list view for list views if (args.node.metaData.isContainer) { openMiniListView(args.node); } } $scope.hideSearch = function () { $scope.searchInfo.showSearch = false; $scope.searchInfo.results = []; }; // method to select a search result $scope.selectResult = function (evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); }; //callback when there are search results $scope.onSearchResults = function (results) { $scope.searchInfo.results = results; $scope.searchInfo.showSearch = true; }; $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.bind('treeNodeExpanded', nodeExpandedHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); $scope.dialogTreeEventHandler.unbind('treeNodeExpanded', nodeExpandedHandler); }); // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; nodeSelectHandler({}, { node: node }); }; $scope.closeMiniListView = function () { $scope.miniListView = undefined; }; function openMiniListView(node) { $scope.miniListView = node; } relationResource.getByChildId($scope.source.id, 'relateParentMediaFolderOnDelete').then(function (data) { $scope.loading = false; if (!data.length) { $scope.moving = true; return; } $scope.relation = data[0]; if ($scope.relation.parentId == -1) { $scope.target = { id: -1, name: $scope.labels.treeRoot }; } else { $scope.loading = true; mediaResource.getById($scope.relation.parentId).then(function (data) { $scope.loading = false; $scope.target = data; // make sure the target item isn't in the recycle bin if ($scope.target.path.indexOf('-21') !== -1) { $scope.moving = true; $scope.target = null; } }, function (err) { $scope.loading = false; $scope.error = err; }); } }, function (err) { $scope.loading = false; $scope.error = err; }); $scope.restore = function () { $scope.loading = true; // this code was copied from `content.move.controller.js` mediaResource.move({ parentId: $scope.target.id, id: $scope.source.id }).then(function (path) { $scope.loading = false; $scope.success = true; //first we need to remove the node that launched the dialog treeService.removeNode($scope.currentNode); //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the moved media item - but don't activate the node, //then sync to the currenlty edited media item (note: this might not be the media item that was moved!!) navigationService.syncTree({ tree: 'media', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'media', path: activeNodePath, forceReload: false, activate: true }); } }); }, function (err) { $scope.loading = false; $scope.error = err; }); }; }); angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.CopyController', function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { var dialogOptions = $scope.dialogOptions; $scope.dialogTreeEventHandler = $({}); function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } $scope.copy = function () { $scope.busy = true; $scope.error = false; mediaTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }).then(function (path) { $scope.error = false; $scope.success = true; $scope.busy = false; //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the copied content - but don't activate the node, //then sync to the currenlty edited content (note: this might not be the content that was copied!!) navigationService.syncTree({ tree: 'mediaTypes', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'mediaTypes', path: activeNodePath, forceReload: false, activate: true }); } }); }, function (err) { $scope.success = false; $scope.error = err; $scope.busy = false; //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); }); /** * @ngdoc controller * @name Umbraco.Editors.MediaType.CreateController * @function * * @description * The controller for the media type creation dialog */ function MediaTypesCreateController($scope, $location, navigationService, mediaTypeResource, formHelper, appState, localizationService) { $scope.model = { folderName: '', creatingFolder: false }; var node = $scope.dialogOptions.currentNode, localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); $scope.showCreateFolder = function () { $scope.model.creatingFolder = true; }; $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm, statusMessage: localizeCreateFolder })) { mediaTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { navigationService.hideMenu(); var currPath = node.path ? node.path : '-1'; navigationService.syncTree({ tree: 'mediatypes', path: currPath + ',' + folderId, forceReload: true, activate: true }); formHelper.resetForm({ scope: $scope }); var section = appState.getSectionState('currentSection'); }, function (err) { $scope.error = err; }); } ; }; $scope.createMediaType = function () { $location.search('create', null); $location.path('/settings/mediatypes/edit/' + node.id).search('create', 'true'); navigationService.hideMenu(); }; } angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.CreateController', MediaTypesCreateController); /** * @ngdoc controller * @name Umbraco.Editors.MediaType.DeleteController * @function * * @description * The controller for the media type delete dialog */ function MediaTypesDeleteController($scope, dataTypeResource, mediaTypeResource, treeService, navigationService) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; mediaTypeResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.performContainerDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; mediaTypeResource.deleteContainerById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.DeleteController', MediaTypesDeleteController); /** * @ngdoc controller * @name Umbraco.Editors.MediaType.EditController * @function * * @description * The controller for the media type editor */ (function () { 'use strict'; function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { var vm = this; var localizeSaving = localizationService.localize('general_saving'); var evts = []; vm.save = save; vm.currentNode = null; vm.contentType = {}; vm.page = {}; vm.page.loading = false; vm.page.saveButtonState = 'init'; vm.page.navigation = [ { 'name': localizationService.localize('general_design'), 'icon': 'icon-document-dashed-line', 'view': 'views/mediatypes/views/design/design.html', 'active': true }, { 'name': localizationService.localize('general_listView'), 'icon': 'icon-list', 'view': 'views/mediatypes/views/listview/listview.html' }, { 'name': localizationService.localize('general_rights'), 'icon': 'icon-keychain', 'view': 'views/mediatypes/views/permissions/permissions.html' } ]; vm.page.keyboardShortcutsOverview = [ { 'name': localizationService.localize('main_sections'), 'shortcuts': [{ 'description': localizationService.localize('shortcuts_navigateSections'), 'keys': [ { 'key': '1' }, { 'key': '3' } ], 'keyRange': true }] }, { 'name': localizationService.localize('general_design'), 'shortcuts': [ { 'description': localizationService.localize('shortcuts_addTab'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 't' } ] }, { 'description': localizationService.localize('shortcuts_addProperty'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'p' } ] }, { 'description': localizationService.localize('shortcuts_addEditor'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'e' } ] }, { 'description': localizationService.localize('shortcuts_editDataType'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'd' } ] } ] }, { 'name': localizationService.localize('general_listView'), 'shortcuts': [{ 'description': localizationService.localize('shortcuts_toggleListView'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'l' } ] }] }, { 'name': localizationService.localize('general_rights'), 'shortcuts': [ { 'description': localizationService.localize('shortcuts_toggleAllowAsRoot'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'r' } ] }, { 'description': localizationService.localize('shortcuts_addChildNode'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'c' } ] } ] } ]; contentTypeHelper.checkModelsBuilderStatus().then(function (result) { vm.page.modelsBuilder = result; if (result) { //Models builder mode: vm.page.defaultButton = { hotKey: 'ctrl+s', hotKeyWhenHidden: true, labelKey: 'buttons_save', letter: 'S', type: 'submit', handler: function () { vm.save(); } }; vm.page.subButtons = [{ hotKey: 'ctrl+g', hotKeyWhenHidden: true, labelKey: 'buttons_saveAndGenerateModels', letter: 'G', handler: function () { vm.page.saveButtonState = 'busy'; vm.save().then(function (result) { vm.page.saveButtonState = 'busy'; localizationService.localize('modelsBuilder_buildingModels').then(function (headerValue) { localizationService.localize('modelsBuilder_waitingMessage').then(function (msgValue) { notificationsService.info(headerValue, msgValue); }); }); contentTypeHelper.generateModels().then(function (result) { if (!result.lastError) { //re-check model status contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { vm.page.modelsBuilder = statusResult; }); //clear and add success vm.page.saveButtonState = 'init'; localizationService.localize('modelsBuilder_modelsGenerated').then(function (value) { notificationsService.success(value); }); } else { vm.page.saveButtonState = 'error'; localizationService.localize('modelsBuilder_modelsExceptionInUlog').then(function (value) { notificationsService.error(value); }); } }, function () { vm.page.saveButtonState = 'error'; localizationService.localize('modelsBuilder_modelsGeneratedError').then(function (value) { notificationsService.error(value); }); }); }); } }]; } }); if ($routeParams.create) { vm.page.loading = true; //we are creating so get an empty data type item mediaTypeResource.getScaffold($routeParams.id).then(function (dt) { init(dt); vm.page.loading = false; }); } else { loadMediaType(); } function loadMediaType() { vm.page.loading = true; mediaTypeResource.getById($routeParams.id).then(function (dt) { init(dt); syncTreeNode(vm.contentType, dt.path, true); vm.page.loading = false; }); } /* ---------- SAVE ---------- */ function save() { // only save if there is no overlays open if (overlayHelper.getNumberOfOverlays() === 0) { var deferred = $q.defer(); vm.page.saveButtonState = 'busy'; // reformat allowed content types to array if id's vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); contentEditingHelper.contentEditorPerformSave({ statusMessage: localizeSaving, saveMethod: mediaTypeResource.save, scope: $scope, content: vm.contentType, //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc // type when server side validation fails - as opposed to content where we are capable of saving the content // item if server side validation fails redirectOnFailure: false, // we need to rebind... the IDs that have been created! rebindCallback: function (origContentType, savedContentType) { vm.contentType.id = savedContentType.id; vm.contentType.groups.forEach(function (group) { if (!group.name) return; var k = 0; while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) k++; if (k == savedContentType.groups.length) { group.id = 0; return; } var savedGroup = savedContentType.groups[k]; if (!group.id) group.id = savedGroup.id; group.properties.forEach(function (property) { if (property.id || !property.alias) return; k = 0; while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) k++; if (k == savedGroup.properties.length) { property.id = 0; return; } var savedProperty = savedGroup.properties[k]; property.id = savedProperty.id; }); }); } }).then(function (data) { //success syncTreeNode(vm.contentType, data.path); vm.page.saveButtonState = 'success'; deferred.resolve(data); }, function (err) { //error if (err) { editorState.set($scope.content); } else { localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { notificationsService.error(headerValue, msgValue); }); }); } vm.page.saveButtonState = 'error'; deferred.reject(err); }); return deferred.promise; } } function init(contentType) { // set all tab to inactive if (contentType.groups.length !== 0) { angular.forEach(contentType.groups, function (group) { angular.forEach(group.properties, function (property) { // get data type details for each property getDataTypeDetails(property); }); }); } // convert icons for content type convertLegacyIcons(contentType); //set a shared state editorState.set(contentType); vm.contentType = contentType; } function convertLegacyIcons(contentType) { // make array to store contentType icon var contentTypeArray = []; // push icon to array contentTypeArray.push({ 'icon': contentType.icon }); // run through icon method iconHelper.formatContentTypeIcons(contentTypeArray); // set icon back on contentType contentType.icon = contentTypeArray[0].icon; } function getDataTypeDetails(property) { if (property.propertyState !== 'init') { dataTypeResource.getById(property.dataTypeId).then(function (dataType) { property.dataTypeIcon = dataType.icon; property.dataTypeName = dataType.name; }); } } /** Syncs the content type to it's tree node - this occurs on first load and after saving */ function syncTreeNode(dt, path, initialLoad) { navigationService.syncTree({ tree: 'mediatypes', path: path.split(','), forceReload: initialLoad !== true }).then(function (syncArgs) { vm.currentNode = syncArgs.node; }); } evts.push(eventsService.on('app.refreshEditor', function (name, error) { loadMediaType(); })); //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { eventsService.unsubscribe(evts[e]); } }); } angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.EditController', MediaTypesEditController); }()); angular.module('umbraco').controller('Umbraco.Editors.MediaTypes.MoveController', function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { var dialogOptions = $scope.dialogOptions; $scope.dialogTreeEventHandler = $({}); function nodeSelectHandler(ev, args) { args.event.preventDefault(); args.event.stopPropagation(); if ($scope.target) { //un-select if there's a current one selected $scope.target.selected = false; } $scope.target = args.node; $scope.target.selected = true; } $scope.move = function () { $scope.busy = true; $scope.error = false; mediaTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }).then(function (path) { $scope.error = false; $scope.success = true; $scope.busy = false; //first we need to remove the node that launched the dialog treeService.removeNode($scope.currentNode); //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); //we need to do a double sync here: first sync to the moved content - but don't activate the node, //then sync to the currenlty edited content (note: this might not be the content that was moved!!) navigationService.syncTree({ tree: 'mediaTypes', path: path, forceReload: true, activate: false }).then(function (args) { if (activeNode) { var activeNodePath = treeService.getPath(activeNode).join(); //sync to this node now - depending on what was copied this might already be synced but might not be navigationService.syncTree({ tree: 'mediaTypes', path: activeNodePath, forceReload: false, activate: true }); } }); eventsService.emit('app.refreshEditor'); }, function (err) { $scope.success = false; $scope.error = err; $scope.busy = false; //show any notifications if (angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } }); }; $scope.dialogTreeEventHandler.bind('treeNodeSelect', nodeSelectHandler); $scope.$on('$destroy', function () { $scope.dialogTreeEventHandler.unbind('treeNodeSelect', nodeSelectHandler); }); }); (function () { 'use strict'; function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService) { /* ----------- SCOPE VARIABLES ----------- */ var vm = this; var childNodeSelectorOverlayTitle = ''; vm.mediaTypes = []; vm.selectedChildren = []; vm.addChild = addChild; vm.removeChild = removeChild; vm.toggle = toggle; /* ---------- INIT ---------- */ init(); function init() { childNodeSelectorOverlayTitle = localizationService.localize('contentTypeEditor_chooseChildNode'); mediaTypeResource.getAll().then(function (mediaTypes) { vm.mediaTypes = mediaTypes; // convert legacy icons iconHelper.formatContentTypeIcons(vm.mediaTypes); vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.mediaTypes); if ($scope.model.id === 0) { contentTypeHelper.insertChildNodePlaceholder(vm.mediaTypes, $scope.model.name, $scope.model.icon, $scope.model.id); } }); } function addChild($event) { vm.childNodeSelectorOverlay = { view: 'itempicker', title: childNodeSelectorOverlayTitle, availableItems: vm.mediaTypes, selectedItems: vm.selectedChildren, event: $event, show: true, submit: function (model) { vm.selectedChildren.push(model.selectedItem); $scope.model.allowedContentTypes.push(model.selectedItem.id); vm.childNodeSelectorOverlay.show = false; vm.childNodeSelectorOverlay = null; } }; } function removeChild(selectedChild, index) { // remove from vm vm.selectedChildren.splice(index, 1); // remove from content type model var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); } /** * Toggle the $scope.model.allowAsRoot value to either true or false */ function toggle() { if ($scope.model.allowAsRoot) { $scope.model.allowAsRoot = false; return; } $scope.model.allowAsRoot = true; } } angular.module('umbraco').controller('Umbraco.Editors.MediaType.PermissionsController', PermissionsController); }()); /** * @ngdoc controller * @name Umbraco.Editors.Member.CreateController * @function * * @description * The controller for the member creation dialog */ function memberCreateController($scope, $routeParams, memberTypeResource, iconHelper) { memberTypeResource.getTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); }); } angular.module('umbraco').controller('Umbraco.Editors.Member.CreateController', memberCreateController); /** * @ngdoc controller * @name Umbraco.Editors.Member.DeleteController * @function * * @description * The controller for deleting content */ function MemberDeleteController($scope, memberResource, treeService, navigationService, editorState, $location, $routeParams) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; memberResource.deleteByKey($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; treeService.removeNode($scope.currentNode); //if the current edited item is the same one as we're deleting, we need to navigate elsewhere if (editorState.current && editorState.current.key == $scope.currentNode.id) { $location.path('/member/member/list/' + ($routeParams.listName ? $routeParams.listName : 'all-members')); } navigationService.hideMenu(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.Member.DeleteController', MemberDeleteController); /** * @ngdoc controller * @name Umbraco.Editors.Member.EditController * @function * * @description * The controller for the member editor */ function MemberEditController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper, editorState, umbRequestHelper, $http) { //setup scope vars $scope.page = {}; $scope.page.loading = true; $scope.page.menu = {}; $scope.page.menu.currentSection = appState.getSectionState('currentSection'); $scope.page.menu.currentNode = null; //the editors affiliated node $scope.page.nameLocked = false; $scope.page.listViewPath = null; $scope.page.saveButtonState = 'init'; $scope.page.exportButton = 'init'; $scope.busy = false; $scope.page.listViewPath = $routeParams.page && $routeParams.listName ? '/member/member/list/' + $routeParams.listName + '?page=' + $routeParams.page : null; //build a path to sync the tree with function buildTreePath(data) { return $routeParams.listName ? '-1,' + $routeParams.listName : '-1'; } if ($routeParams.create) { //if there is no doc type specified then we are going to assume that // we are not using the umbraco membership provider if ($routeParams.doctype) { //we are creating so get an empty member item memberResource.getScaffold($routeParams.doctype).then(function (data) { $scope.content = data; setHeaderNameState($scope.content); editorState.set($scope.content); $scope.page.loading = false; }); } else { memberResource.getScaffold().then(function (data) { $scope.content = data; setHeaderNameState($scope.content); editorState.set($scope.content); $scope.page.loading = false; }); } } else { //so, we usually refernce all editors with the Int ID, but with members we have //a different pattern, adding a route-redirect here to handle this: //isNumber doesnt work here since its seen as a string //TODO: Why is this here - I don't understand why this would ever be an integer? This will not work when we support non-umbraco membership providers. if ($routeParams.id && $routeParams.id.length < 9) { entityResource.getById($routeParams.id, 'Member').then(function (entity) { $location.path('/member/member/edit/' + entity.key); }); } else { //we are editing so get the content item from the server memberResource.getByKey($routeParams.id).then(function (data) { $scope.content = data; setHeaderNameState($scope.content); editorState.set($scope.content); var path = buildTreePath(data); //sync the tree (only for ui purposes) navigationService.syncTree({ tree: 'member', path: path.split(',') }); //it's the initial load of the editor, we need to get the tree node // from the server so that we can load in the actions menu. umbRequestHelper.resourcePromise($http.get(data.treeNodeUrl), 'Failed to retrieve data for child node ' + data.key).then(function (node) { $scope.page.menu.currentNode = node; }); //in one particular special case, after we've created a new item we redirect back to the edit // route but there might be server validation errors in the collection which we need to display // after the redirect, so we will bind all subscriptions which will show the server validation errors // if there are any and then clear them so the collection no longer persists them. serverValidationManager.executeAndClearAllSubscriptions(); $scope.page.loading = false; }); } } function setHeaderNameState(content) { if (content.membershipScenario === 0) { $scope.page.nameLocked = true; } } $scope.save = function () { if (!$scope.busy && formHelper.submitForm({ scope: $scope, statusMessage: 'Saving...' })) { $scope.busy = true; $scope.page.saveButtonState = 'busy'; //anytime a user is changing a member's password without the oldPassword, we are in effect resetting it so we need to set that flag here var passwordProp = _.find(contentEditingHelper.getAllProps($scope.content), function (e) { return e.alias === '_umb_password'; }); if (passwordProp && passwordProp.value && typeof passwordProp.value.reset !== 'undefined' && !passwordProp.value.reset) { //so if the admin is not explicitly resetting the password, flag it for resetting if a new password is being entered passwordProp.value.reset = !passwordProp.value.oldPassword && passwordProp.config.allowManuallyChangingPassword; } memberResource.save($scope.content, $routeParams.create, fileManager.getFiles()).then(function (data) { formHelper.resetForm({ scope: $scope, notifications: data.notifications }); contentEditingHelper.handleSuccessfulSave({ scope: $scope, savedContent: data, //specify a custom id to redirect to since we want to use the GUID redirectId: data.key, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) }); editorState.set($scope.content); $scope.busy = false; $scope.page.saveButtonState = 'success'; var path = buildTreePath(data); //sync the tree (only for ui purposes) navigationService.syncTree({ tree: 'member', path: path.split(','), forceReload: true }); }, function (err) { contentEditingHelper.handleSaveError({ redirectOnFailure: false, err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) }); editorState.set($scope.content); $scope.busy = false; $scope.page.saveButtonState = 'error'; }); } else { $scope.busy = false; } }; $scope.export = function () { var memberKey = $scope.content.key; memberResource.exportMemberData(memberKey); }; } angular.module('umbraco').controller('Umbraco.Editors.Member.EditController', MemberEditController); /** * @ngdoc controller * @name Umbraco.Editors.Member.ListController * @function * * @description * The controller for the member list view */ function MemberListController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper, editorState, localizationService) { //setup scope vars $scope.currentSection = appState.getSectionState('currentSection'); $scope.currentNode = null; //the editors affiliated node $scope.page = {}; $scope.page.lockedName = true; $scope.page.loading = true; //we are editing so get the content item from the server memberResource.getListNode($routeParams.id).then(function (data) { $scope.content = data; //translate "All Members" if ($scope.content != null && $scope.content.name != null && $scope.content.name.replace(' ', '').toLowerCase() == 'allmembers') { localizationService.localize('member_allMembers').then(function (value) { $scope.content.name = value; }); } editorState.set($scope.content); navigationService.syncTree({ tree: 'member', path: data.path.split(',') }).then(function (syncArgs) { $scope.currentNode = syncArgs.node; }); //in one particular special case, after we've created a new item we redirect back to the edit // route but there might be server validation errors in the collection which we need to display // after the redirect, so we will bind all subscriptions which will show the server validation errors // if there are any and then clear them so the collection no longer persists them. serverValidationManager.executeAndClearAllSubscriptions(); $scope.page.loading = false; }); } angular.module('umbraco').controller('Umbraco.Editors.Member.ListController', MemberListController); /** * @ngdoc controller * @name Umbraco.Editors.MemberType.CreateController * @function * * @description * The controller for the member type creation dialog */ function MemberTypesCreateController($scope, $location, navigationService, memberTypeResource, formHelper, appState, localizationService) { $scope.model = { folderName: '', creatingFolder: false }; var node = $scope.dialogOptions.currentNode, localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); $scope.showCreateFolder = function () { $scope.model.creatingFolder = true; }; $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm, statusMessage: localizeCreateFolder })) { memberTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { navigationService.hideMenu(); var currPath = node.path ? node.path : '-1'; navigationService.syncTree({ tree: 'membertypes', path: currPath + ',' + folderId, forceReload: true, activate: true }); formHelper.resetForm({ scope: $scope }); var section = appState.getSectionState('currentSection'); }, function (err) { }); } ; }; $scope.createMemberType = function () { $location.search('create', null); $location.path('/settings/membertypes/edit/' + node.id).search('create', 'true'); navigationService.hideMenu(); }; } angular.module('umbraco').controller('Umbraco.Editors.MemberTypes.CreateController', MemberTypesCreateController); /** * @ngdoc controller * @name Umbraco.Editors.MemberTypes.DeleteController * @function * * @description * The controller for deleting member types */ function MemberTypesDeleteController($scope, memberTypeResource, treeService, navigationService) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; memberTypeResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.MemberTypes.DeleteController', MemberTypesDeleteController); /** * @ngdoc controller * @name Umbraco.Editors.MemberType.EditController * @function * * @description * The controller for the member type editor */ (function () { 'use strict'; function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q, localizationService, overlayHelper, contentTypeHelper) { var vm = this; var localizeSaving = localizationService.localize('general_saving'); vm.save = save; vm.currentNode = null; vm.contentType = {}; vm.page = {}; vm.page.loading = false; vm.page.saveButtonState = 'init'; vm.page.navigation = [{ 'name': localizationService.localize('general_design'), 'icon': 'icon-document-dashed-line', 'view': 'views/membertypes/views/design/design.html', 'active': true }]; vm.page.keyboardShortcutsOverview = [{ 'name': localizationService.localize('shortcuts_shortcut'), 'shortcuts': [ { 'description': localizationService.localize('shortcuts_addTab'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 't' } ] }, { 'description': localizationService.localize('shortcuts_addProperty'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'p' } ] }, { 'description': localizationService.localize('shortcuts_addEditor'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'e' } ] }, { 'description': localizationService.localize('shortcuts_editDataType'), 'keys': [ { 'key': 'alt' }, { 'key': 'shift' }, { 'key': 'd' } ] } ] }]; contentTypeHelper.checkModelsBuilderStatus().then(function (result) { vm.page.modelsBuilder = result; if (result) { //Models builder mode: vm.page.defaultButton = { hotKey: 'ctrl+s', hotKeyWhenHidden: true, labelKey: 'buttons_save', letter: 'S', type: 'submit', handler: function () { vm.save(); } }; vm.page.subButtons = [{ hotKey: 'ctrl+g', hotKeyWhenHidden: true, labelKey: 'buttons_saveAndGenerateModels', letter: 'G', handler: function () { vm.page.saveButtonState = 'busy'; vm.save().then(function (result) { vm.page.saveButtonState = 'busy'; localizationService.localize('modelsBuilder_buildingModels').then(function (headerValue) { localizationService.localize('modelsBuilder_waitingMessage').then(function (msgValue) { notificationsService.info(headerValue, msgValue); }); }); contentTypeHelper.generateModels().then(function (result) { if (!result.lastError) { //re-check model status contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { vm.page.modelsBuilder = statusResult; }); //clear and add success vm.page.saveButtonState = 'init'; localizationService.localize('modelsBuilder_modelsGenerated').then(function (value) { notificationsService.success(value); }); } else { vm.page.saveButtonState = 'error'; localizationService.localize('modelsBuilder_modelsExceptionInUlog').then(function (value) { notificationsService.error(value); }); } }, function () { vm.page.saveButtonState = 'error'; localizationService.localize('modelsBuilder_modelsGeneratedError').then(function (value) { notificationsService.error(value); }); }); }); } }]; } }); if ($routeParams.create) { vm.page.loading = true; //we are creating so get an empty data type item memberTypeResource.getScaffold($routeParams.id).then(function (dt) { init(dt); vm.page.loading = false; }); } else { vm.page.loading = true; memberTypeResource.getById($routeParams.id).then(function (dt) { init(dt); syncTreeNode(vm.contentType, dt.path, true); vm.page.loading = false; }); } function save() { // only save if there is no overlays open if (overlayHelper.getNumberOfOverlays() === 0) { var deferred = $q.defer(); vm.page.saveButtonState = 'busy'; contentEditingHelper.contentEditorPerformSave({ statusMessage: localizeSaving, saveMethod: memberTypeResource.save, scope: $scope, content: vm.contentType, //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc // type when server side validation fails - as opposed to content where we are capable of saving the content // item if server side validation fails redirectOnFailure: false, // we need to rebind... the IDs that have been created! rebindCallback: function (origContentType, savedContentType) { vm.contentType.id = savedContentType.id; vm.contentType.groups.forEach(function (group) { if (!group.name) return; var k = 0; while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) k++; if (k == savedContentType.groups.length) { group.id = 0; return; } var savedGroup = savedContentType.groups[k]; if (!group.id) group.id = savedGroup.id; group.properties.forEach(function (property) { if (property.id || !property.alias) return; k = 0; while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) k++; if (k == savedGroup.properties.length) { property.id = 0; return; } var savedProperty = savedGroup.properties[k]; property.id = savedProperty.id; }); }); } }).then(function (data) { //success syncTreeNode(vm.contentType, data.path); vm.page.saveButtonState = 'success'; deferred.resolve(data); }, function (err) { //error if (err) { editorState.set($scope.content); } else { localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { notificationsService.error(headerValue, msgValue); }); }); } vm.page.saveButtonState = 'error'; deferred.reject(err); }); return deferred.promise; } } function init(contentType) { // set all tab to inactive if (contentType.groups.length !== 0) { angular.forEach(contentType.groups, function (group) { angular.forEach(group.properties, function (property) { // get data type details for each property getDataTypeDetails(property); }); }); } // convert legacy icons convertLegacyIcons(contentType); //set a shared state editorState.set(contentType); vm.contentType = contentType; } function convertLegacyIcons(contentType) { // make array to store contentType icon var contentTypeArray = []; // push icon to array contentTypeArray.push({ 'icon': contentType.icon }); // run through icon method iconHelper.formatContentTypeIcons(contentTypeArray); // set icon back on contentType contentType.icon = contentTypeArray[0].icon; } function getDataTypeDetails(property) { if (property.propertyState !== 'init') { dataTypeResource.getById(property.dataTypeId).then(function (dataType) { property.dataTypeIcon = dataType.icon; property.dataTypeName = dataType.name; }); } } /** Syncs the content type to it's tree node - this occurs on first load and after saving */ function syncTreeNode(dt, path, initialLoad) { navigationService.syncTree({ tree: 'membertypes', path: path.split(','), forceReload: initialLoad !== true }).then(function (syncArgs) { vm.currentNode = syncArgs.node; }); } } angular.module('umbraco').controller('Umbraco.Editors.MemberTypes.EditController', MemberTypesEditController); }()); angular.module('umbraco').controller('Umbraco.Editors.MemberTypes.MoveController', function ($scope) { }); /** * @ngdoc controller * @name Umbraco.Editors.Packages.DeleteController * @function * * @description * The controller for deleting content */ function PackageDeleteController($scope, packageResource, treeService, navigationService) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; packageResource.deleteCreatedPackage($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.Packages.DeleteController', PackageDeleteController); (function () { 'use strict'; function PackagesOverviewController($scope, $route, $location, navigationService, $timeout, localStorageService) { //Hack! // if there is a cookie value for packageInstallUri then we need to redirect there, // the issue is that we still have webforms and we cannot go to a hash location and then window.reload // because it will double load it. // we will refresh and then navigate there. var installPackageUri = localStorageService.get('packageInstallUri'); if (installPackageUri) { localStorageService.remove('packageInstallUri'); } if (installPackageUri && installPackageUri !== 'installed') { //navigate to the custom installer screen, if it is just "installed", then we'll //show the installed view $location.path(installPackageUri).search(''); } else { var vm = this; vm.page = {}; vm.page.name = 'Packages'; vm.page.navigation = [ { 'name': 'Packages', 'icon': 'icon-cloud', 'view': 'views/packager/views/repo.html', 'active': !installPackageUri || installPackageUri === 'navigation' }, { 'name': 'Installed', 'icon': 'icon-box', 'view': 'views/packager/views/installed.html', 'active': installPackageUri === 'installed' }, { 'name': 'Install local', 'icon': 'icon-add', 'view': 'views/packager/views/install-local.html', 'active': installPackageUri === 'local' } ]; $timeout(function () { navigationService.syncTree({ tree: 'packager', path: '-1' }); }); } } angular.module('umbraco').controller('Umbraco.Editors.Packages.OverviewController', PackagesOverviewController); }()); (function () { 'use strict'; function PackagesInstallLocalController($scope, $route, $location, Upload, umbRequestHelper, packageResource, localStorageService, $timeout, $window, localizationService, $q) { var vm = this; vm.state = 'upload'; vm.localPackage = {}; vm.installPackage = installPackage; vm.installState = { status: '', progress: 0 }; vm.installCompleted = false; vm.zipFile = { uploadStatus: 'idle', uploadProgress: 0, serverErrorMessage: null }; $scope.handleFiles = function (files, event) { if (files) { for (var i = 0; i < files.length; i++) { upload(files[i]); } } }; function upload(file) { Upload.upload({ url: umbRequestHelper.getApiUrl('packageInstallApiBaseUrl', 'UploadLocalPackage'), fields: {}, file: file }).progress(function (evt) { // hack: in some browsers the progress event is called after success // this prevents the UI from going back to a uploading state if (vm.zipFile.uploadStatus !== 'done' && vm.zipFile.uploadStatus !== 'error') { // set view state to uploading vm.state = 'uploading'; // calculate progress in percentage var progressPercentage = parseInt(100 * evt.loaded / evt.total, 10); // set percentage property on file vm.zipFile.uploadProgress = progressPercentage; // set uploading status on file vm.zipFile.uploadStatus = 'uploading'; } }).success(function (data, status, headers, config) { if (data.notifications && data.notifications.length > 0) { // set error status on file vm.zipFile.uploadStatus = 'error'; // Throw message back to user with the cause of the error vm.zipFile.serverErrorMessage = data.notifications[0].message; } else { // set done status on file vm.zipFile.uploadStatus = 'done'; loadPackage(); vm.zipFile.uploadProgress = 100; vm.localPackage = data; } }).error(function (evt, status, headers, config) { // set status done vm.zipFile.uploadStatus = 'error'; // If file not found, server will return a 404 and display this message if (status === 404) { vm.zipFile.serverErrorMessage = 'File not found'; } else if (status == 400) { //it's a validation error vm.zipFile.serverErrorMessage = evt.message; } else { //it's an unhandled error //if the service returns a detailed error if (evt.InnerException) { vm.zipFile.serverErrorMessage = evt.InnerException.ExceptionMessage; //Check if its the common "too large file" exception if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf('ValidateRequestEntityLength') > 0) { vm.zipFile.serverErrorMessage = 'File too large to upload'; } } else if (evt.Message) { vm.zipFile.serverErrorMessage = evt.Message; } } }); } function loadPackage() { if (vm.zipFile.uploadStatus === 'done') { vm.state = 'packageDetails'; } } function installPackage() { vm.installState.status = localizationService.localize('packager_installStateImporting'); vm.installState.progress = '0'; packageResource.import(vm.localPackage).then(function (pack) { vm.installState.progress = '25'; vm.installState.status = localizationService.localize('packager_installStateInstalling'); return packageResource.installFiles(pack); }, installError).then(function (pack) { vm.installState.status = localizationService.localize('packager_installStateRestarting'); vm.installState.progress = '50'; var deferred = $q.defer(); //check if the app domain is restarted ever 2 seconds var count = 0; function checkRestart() { $timeout(function () { packageResource.checkRestart(pack).then(function (d) { count++; //if there is an id it means it's not restarted yet but we'll limit it to only check 10 times if (d.isRestarting && count < 10) { checkRestart(); } else { //it's restarted! deferred.resolve(d); } }, installError); }, 2000); } checkRestart(); return deferred.promise; }, installError).then(function (pack) { vm.installState.status = localizationService.localize('packager_installStateRestarting'); vm.installState.progress = '75'; return packageResource.installData(pack); }, installError).then(function (pack) { vm.installState.status = localizationService.localize('packager_installStateComplete'); vm.installState.progress = '100'; return packageResource.cleanUp(pack); }, installError).then(function (result) { if (result.postInstallationPath) { //Put the redirect Uri in a cookie so we can use after reloading localStorageService.set('packageInstallUri', result.postInstallationPath); } else { //set to a constant value so it knows to just go to the installed view localStorageService.set('packageInstallUri', 'installed'); } vm.installState.status = localizationService.localize('packager_installStateCompleted'); vm.installCompleted = true; }, installError); } function installError() { //This will return a rejection meaning that the promise change above will stop return $q.reject(); } vm.reloadPage = function () { //reload on next digest (after cookie) $timeout(function () { $window.location.reload(true); }); }; } angular.module('umbraco').controller('Umbraco.Editors.Packages.InstallLocalController', PackagesInstallLocalController); }()); (function () { 'use strict'; function PackagesInstalledController($scope, $route, $location, packageResource, $timeout, $window, localStorageService, localizationService) { var vm = this; vm.confirmUninstall = confirmUninstall; vm.uninstallPackage = uninstallPackage; vm.state = 'list'; vm.installState = { status: '' }; vm.package = {}; function init() { packageResource.getInstalled().then(function (packs) { vm.installedPackages = packs; }); vm.installState.status = ''; vm.state = 'list'; } function confirmUninstall(pck) { vm.state = 'packageDetails'; vm.package = pck; } function uninstallPackage(installedPackage) { vm.installState.status = localizationService.localize('packager_installStateUninstalling'); vm.installState.progress = '0'; packageResource.uninstall(installedPackage.id).then(function () { if (installedPackage.files.length > 0) { vm.installState.status = localizationService.localize('packager_installStateComplete'); vm.installState.progress = '100'; //set this flag so that on refresh it shows the installed packages list localStorageService.set('packageInstallUri', 'installed'); //reload on next digest (after cookie) $timeout(function () { $window.location.reload(true); }); } else { init(); } }); } init(); } angular.module('umbraco').controller('Umbraco.Editors.Packages.InstalledController', PackagesInstalledController); }()); (function () { 'use strict'; function PackagesRepoController($scope, $route, $location, $timeout, ourPackageRepositoryResource, $q, packageResource, localStorageService, localizationService) { var vm = this; vm.packageViewState = 'packageList'; vm.categories = []; vm.loading = true; vm.pagination = { pageNumber: 1, totalPages: 10, pageSize: 24 }; vm.searchQuery = ''; vm.installState = { status: '', progress: 0, type: 'ok' }; vm.selectCategory = selectCategory; vm.showPackageDetails = showPackageDetails; vm.setPackageViewState = setPackageViewState; vm.nextPage = nextPage; vm.prevPage = prevPage; vm.goToPage = goToPage; vm.installPackage = installPackage; vm.downloadPackage = downloadPackage; vm.openLightbox = openLightbox; vm.closeLightbox = closeLightbox; vm.search = search; vm.installCompleted = false; var currSort = 'Latest'; //used to cancel any request in progress if another one needs to take it's place var canceler = null; function getActiveCategory() { if (vm.searchQuery !== '') { return ''; } for (var i = 0; i < vm.categories.length; i++) { if (vm.categories[i].active === true) { return vm.categories[i].name; } } return ''; } function init() { vm.loading = true; $q.all([ ourPackageRepositoryResource.getCategories().then(function (cats) { vm.categories = cats; }), ourPackageRepositoryResource.getPopular(8).then(function (pack) { vm.popular = pack.packages; }), ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort).then(function (pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); }) ]).then(function () { vm.loading = false; }); } function selectCategory(selectedCategory, categories) { var reset = false; for (var i = 0; i < categories.length; i++) { var category = categories[i]; if (category.name === selectedCategory.name && category.active === true) { //it's already selected, let's unselect to show all again reset = true; } category.active = false; } vm.loading = true; vm.searchQuery = ''; var searchCategory = selectedCategory.name; if (reset === true) { searchCategory = ''; } currSort = 'Latest'; $q.all([ ourPackageRepositoryResource.getPopular(8, searchCategory).then(function (pack) { vm.popular = pack.packages; }), ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, searchCategory, vm.searchQuery).then(function (pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); vm.pagination.pageNumber = 1; }) ]).then(function () { vm.loading = false; selectedCategory.active = reset === false; }); } function showPackageDetails(selectedPackage) { ourPackageRepositoryResource.getDetails(selectedPackage.id).then(function (pack) { packageResource.validateInstalled(pack.name, pack.latestVersion).then(function () { //ok, can install vm.package = pack; vm.package.isValid = true; vm.packageViewState = 'packageDetails'; }, function () { //nope, cannot install vm.package = pack; vm.package.isValid = false; vm.packageViewState = 'packageDetails'; }); }); } function setPackageViewState(state) { if (state) { vm.packageViewState = state; } } function nextPage(pageNumber) { ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery).then(function (pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); }); } function prevPage(pageNumber) { ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery).then(function (pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); }); } function goToPage(pageNumber) { ourPackageRepositoryResource.search(pageNumber - 1, vm.pagination.pageSize, currSort, getActiveCategory(), vm.searchQuery).then(function (pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); }); } function downloadPackage(selectedPackage) { vm.loading = true; packageResource.fetch(selectedPackage.id).then(function (pack) { vm.packageViewState = 'packageInstall'; vm.loading = false; vm.localPackage = pack; vm.localPackage.allowed = true; }, function (evt, status, headers, config) { if (status == 400) { //it's a validation error vm.installState.type = 'error'; vm.zipFile.serverErrorMessage = evt.message; } }); } function error(e, args) { //This will return a rejection meaning that the promise change above will stop return $q.reject(); } function installPackage(selectedPackage) { vm.installState.status = localizationService.localize('packager_installStateImporting'); vm.installState.progress = '0'; packageResource.import(selectedPackage).then(function (pack) { vm.installState.status = localizationService.localize('packager_installStateInstalling'); vm.installState.progress = '25'; return packageResource.installFiles(pack); }, error).then(function (pack) { vm.installState.status = localizationService.localize('packager_installStateRestarting'); vm.installState.progress = '50'; var deferred = $q.defer(); //check if the app domain is restarted ever 2 seconds var count = 0; function checkRestart() { $timeout(function () { packageResource.checkRestart(pack).then(function (d) { count++; //if there is an id it means it's not restarted yet but we'll limit it to only check 10 times if (d.isRestarting && count < 10) { checkRestart(); } else { //it's restarted! deferred.resolve(d); } }, error); }, 2000); } checkRestart(); return deferred.promise; }, error).then(function (pack) { vm.installState.status = localizationService.localize('packager_installStateRestarting'); vm.installState.progress = '75'; return packageResource.installData(pack); }, error).then(function (pack) { vm.installState.status = localizationService.localize('packager_installStateComplete'); vm.installState.progress = '100'; return packageResource.cleanUp(pack); }, error).then(function (result) { if (result.postInstallationPath) { //Put the redirect Uri in a cookie so we can use after reloading localStorageService.set('packageInstallUri', result.postInstallationPath); } vm.installState.status = localizationService.localize('packager_installStateCompleted'); vm.installCompleted = true; }, error); } function openLightbox(itemIndex, items) { vm.lightbox = { show: true, items: items, activeIndex: itemIndex }; } function closeLightbox() { vm.lightbox.show = false; vm.lightbox = null; } var searchDebounced = _.debounce(function (e) { $scope.$apply(function () { //a canceler exists, so perform the cancelation operation and reset if (canceler) { canceler.resolve(); canceler = $q.defer(); } else { canceler = $q.defer(); } currSort = vm.searchQuery ? 'Default' : 'Latest'; ourPackageRepositoryResource.search(vm.pagination.pageNumber - 1, vm.pagination.pageSize, currSort, '', vm.searchQuery, canceler).then(function (pack) { vm.packages = pack.packages; vm.pagination.totalPages = Math.ceil(pack.total / vm.pagination.pageSize); vm.pagination.pageNumber = 1; vm.loading = false; //set back to null so it can be re-created canceler = null; }); }); }, 200); function search(searchQuery) { vm.loading = true; searchDebounced(); } vm.reloadPage = function () { //reload on next digest (after cookie) $timeout(function () { window.location.reload(true); }); }; init(); } angular.module('umbraco').controller('Umbraco.Editors.Packages.RepoController', PackagesRepoController); }()); (function () { 'use strict'; function PartialViewMacrosCreateController($scope, codefileResource, macroResource, $location, navigationService, formHelper, localizationService, appState) { var vm = this; var node = $scope.dialogOptions.currentNode; var localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); vm.snippets = []; vm.createFolderError = ''; vm.folderName = ''; vm.fileName = ''; vm.showSnippets = false; vm.creatingFolder = false; vm.showCreateFolder = showCreateFolder; vm.createFolder = createFolder; vm.createFile = createFile; vm.createFileWithoutMacro = createFileWithoutMacro; vm.showCreateFromSnippet = showCreateFromSnippet; vm.createFileFromSnippet = createFileFromSnippet; function onInit() { codefileResource.getSnippets('partialViewMacros').then(function (snippets) { vm.snippets = snippets; }); } function showCreateFolder() { vm.creatingFolder = true; } function createFolder(form) { if (formHelper.submitForm({ scope: $scope, formCtrl: form, statusMessage: localizeCreateFolder })) { codefileResource.createContainer('partialViewMacros', node.id, vm.folderName).then(function (saved) { navigationService.hideMenu(); navigationService.syncTree({ tree: 'partialViewMacros', path: saved.path, forceReload: true, activate: true }); formHelper.resetForm({ scope: $scope }); var section = appState.getSectionState('currentSection'); }, function (err) { vm.createFolderError = err; //show any notifications formHelper.showNotifications(err.data); }); } } function createFile() { $location.path('/developer/partialviewmacros/edit/' + node.id).search('create', 'true'); navigationService.hideMenu(); } function createFileWithoutMacro() { $location.path('/developer/partialviewmacros/edit/' + node.id).search('create', 'true').search('nomacro', 'true'); navigationService.hideMenu(); } function createFileFromSnippet(snippet) { $location.path('/developer/partialviewmacros/edit/' + node.id).search('create', 'true').search('snippet', snippet.fileName); navigationService.hideMenu(); } function showCreateFromSnippet() { vm.showSnippets = true; } onInit(); } angular.module('umbraco').controller('Umbraco.Editors.PartialViewMacros.CreateController', PartialViewMacrosCreateController); }()); /** * @ngdoc controller * @name Umbraco.Editors.PartialViewMacros.DeleteController * @function * * @description * The controller for deleting partial view macros */ function PartialViewMacrosDeleteController($scope, codefileResource, treeService, navigationService) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; codefileResource.deleteByPath('partialViewMacros', $scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.PartialViewMacros.DeleteController', PartialViewMacrosDeleteController); (function () { 'use strict'; function partialViewMacrosEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, macroResource) { var vm = this; var localizeSaving = localizationService.localize('general_saving'); vm.page = {}; vm.page.loading = true; vm.partialViewMacroFile = {}; //menu vm.page.menu = {}; vm.page.menu.currentSection = appState.getSectionState('currentSection'); vm.page.menu.currentNode = null; // bind functions to view model vm.save = save; vm.openPageFieldOverlay = openPageFieldOverlay; vm.openDictionaryItemOverlay = openDictionaryItemOverlay; vm.openQueryBuilderOverlay = openQueryBuilderOverlay; vm.openMacroOverlay = openMacroOverlay; vm.openInsertOverlay = openInsertOverlay; /* Functions bound to view model */ function save() { vm.page.saveButtonState = 'busy'; vm.partialViewMacro.content = vm.editor.getValue(); contentEditingHelper.contentEditorPerformSave({ statusMessage: localizeSaving, saveMethod: codefileResource.save, scope: $scope, content: vm.partialViewMacro, // We do not redirect on failure for partial view macros - this is because it is not possible to actually save the partial view // when server side validation fails - as opposed to content where we are capable of saving the content // item if server side validation fails redirectOnFailure: false, rebindCallback: function (orignal, saved) { } }).then(function (saved) { // create macro if needed if ($routeParams.create && $routeParams.nomacro !== 'true') { macroResource.createPartialViewMacroWithFile(saved.virtualPath, saved.name).then(function (created) { completeSave(saved); }, function (err) { //show any notifications formHelper.showNotifications(err.data); }); } else { completeSave(saved); } }, function (err) { vm.page.saveButtonState = 'error'; localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { notificationsService.error(headerValue, msgValue); }); }); }); } function completeSave(saved) { localizationService.localize('speechBubbles_partialViewSavedHeader').then(function (headerValue) { localizationService.localize('speechBubbles_partialViewSavedText').then(function (msgValue) { notificationsService.success(headerValue, msgValue); }); }); //check if the name changed, if so we need to redirect if (vm.partialViewMacro.id !== saved.id) { contentEditingHelper.redirectToRenamedContent(saved.id); } else { vm.page.saveButtonState = 'success'; vm.partialViewMacro = saved; //sync state editorState.set(vm.partialViewMacro); // normal tree sync navigationService.syncTree({ tree: 'partialViewMacros', path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { vm.page.menu.currentNode = syncArgs.node; }); // clear $dirty state on form setFormState('pristine'); } } function openInsertOverlay() { vm.insertOverlay = { view: 'insert', allowedTypes: { macro: true, dictionary: true, umbracoField: true }, hideSubmitButton: true, show: true, submit: function (model) { switch (model.insert.type) { case 'macro': var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, 'Mvc'); insert(macroObject.syntax); break; case 'dictionary': var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); insert(code); break; case 'umbracoField': insert(model.insert.umbracoField); break; } vm.insertOverlay.show = false; vm.insertOverlay = null; }, close: function (oldModel) { // close the dialog vm.insertOverlay.show = false; vm.insertOverlay = null; // focus editor vm.editor.focus(); } }; } function openMacroOverlay() { vm.macroPickerOverlay = { view: 'macropicker', dialogData: {}, show: true, title: 'Insert macro', submit: function (model) { var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, 'Mvc'); insert(macroObject.syntax); vm.macroPickerOverlay.show = false; vm.macroPickerOverlay = null; }, close: function (oldModel) { // close the dialog vm.macroPickerOverlay.show = false; vm.macroPickerOverlay = null; // focus editor vm.editor.focus(); } }; } function openPageFieldOverlay() { vm.pageFieldOverlay = { submitButtonLabel: 'Insert', closeButtonlabel: 'Cancel', view: 'insertfield', show: true, submit: function (model) { insert(model.umbracoField); vm.pageFieldOverlay.show = false; vm.pageFieldOverlay = null; }, close: function (model) { // close the dialog vm.pageFieldOverlay.show = false; vm.pageFieldOverlay = null; // focus editor vm.editor.focus(); } }; } function openDictionaryItemOverlay() { vm.dictionaryItemOverlay = { view: 'treepicker', section: 'settings', treeAlias: 'dictionary', entityType: 'dictionary', multiPicker: false, show: true, title: 'Insert dictionary item', emptyStateMessage: localizationService.localize('emptyStates_emptyDictionaryTree'), select: function (node) { var code = templateHelper.getInsertDictionarySnippet(node.name); insert(code); vm.dictionaryItemOverlay.show = false; vm.dictionaryItemOverlay = null; }, close: function (model) { // close dialog vm.dictionaryItemOverlay.show = false; vm.dictionaryItemOverlay = null; // focus editor vm.editor.focus(); } }; } function openQueryBuilderOverlay() { vm.queryBuilderOverlay = { view: 'querybuilder', show: true, title: 'Query for content', submit: function (model) { var code = templateHelper.getQuerySnippet(model.result.queryExpression); insert(code); vm.queryBuilderOverlay.show = false; vm.queryBuilderOverlay = null; }, close: function (model) { // close dialog vm.queryBuilderOverlay.show = false; vm.queryBuilderOverlay = null; // focus editor vm.editor.focus(); } }; } /* Local functions */ function init() { //we need to load this somewhere, for now its here. assetsService.loadCss('lib/ace-razor-mode/theme/razor_chrome.css', $scope); if ($routeParams.create) { var snippet = 'Empty'; if ($routeParams.snippet) { snippet = $routeParams.snippet; } codefileResource.getScaffold('partialViewMacros', $routeParams.id, snippet).then(function (partialViewMacro) { if ($routeParams.name) { partialViewMacro.name = $routeParams.name; } ready(partialViewMacro, false); }); } else { codefileResource.getByPath('partialViewMacros', $routeParams.id).then(function (partialViewMacro) { ready(partialViewMacro, true); }); } } function ready(partialViewMacro, syncTree) { vm.page.loading = false; vm.partialViewMacro = partialViewMacro; //sync state editorState.set(vm.partialViewMacro); if (syncTree) { navigationService.syncTree({ tree: 'partialViewMacros', path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { vm.page.menu.currentNode = syncArgs.node; }); } // ace configuration vm.aceOption = { mode: 'razor', theme: 'chrome', showPrintMargin: false, advanced: { fontSize: '14px' }, onLoad: function (_editor) { vm.editor = _editor; // initial cursor placement // Keep cursor in name field if we are create a new template // else set the cursor at the bottom of the code editor if (!$routeParams.create) { $timeout(function () { vm.editor.navigateFileEnd(); vm.editor.focus(); persistCurrentLocation(); }); } //change on blur, focus vm.editor.on('blur', persistCurrentLocation); vm.editor.on('focus', persistCurrentLocation); vm.editor.on('change', changeAceEditor); } }; } function insert(str) { vm.editor.focus(); vm.editor.moveCursorToPosition(vm.currentPosition); vm.editor.insert(str); // set form state to $dirty setFormState('dirty'); } function persistCurrentLocation() { vm.currentPosition = vm.editor.getCursorPosition(); } function changeAceEditor() { setFormState('dirty'); } function setFormState(state) { // get the current form var currentForm = angularHelper.getCurrentForm($scope); // set state if (state === 'dirty') { currentForm.$setDirty(); } else if (state === 'pristine') { currentForm.$setPristine(); } } init(); } angular.module('umbraco').controller('Umbraco.Editors.PartialViewMacros.EditController', partialViewMacrosEditController); }()); (function () { 'use strict'; function PartialViewsCreateController($scope, codefileResource, $location, navigationService, formHelper, localizationService, appState) { var vm = this; var node = $scope.dialogOptions.currentNode; var localizeCreateFolder = localizationService.localize('defaultdialog_createFolder'); vm.snippets = []; vm.showSnippets = false; vm.creatingFolder = false; vm.createFolderError = ''; vm.folderName = ''; vm.createPartialView = createPartialView; vm.showCreateFolder = showCreateFolder; vm.createFolder = createFolder; vm.showCreateFromSnippet = showCreateFromSnippet; function onInit() { codefileResource.getSnippets('partialViews').then(function (snippets) { vm.snippets = snippets; }); } function createPartialView(selectedSnippet) { var snippet = null; if (selectedSnippet && selectedSnippet.fileName) { snippet = selectedSnippet.fileName; } $location.path('/settings/partialviews/edit/' + node.id).search('create', 'true').search('snippet', snippet); navigationService.hideMenu(); } function showCreateFolder() { vm.creatingFolder = true; } function createFolder(form) { if (formHelper.submitForm({ scope: $scope, formCtrl: form, statusMessage: localizeCreateFolder })) { codefileResource.createContainer('partialViews', node.id, vm.folderName).then(function (saved) { navigationService.hideMenu(); navigationService.syncTree({ tree: 'partialViews', path: saved.path, forceReload: true, activate: true }); formHelper.resetForm({ scope: $scope }); var section = appState.getSectionState('currentSection'); }, function (err) { vm.createFolderError = err; formHelper.showNotifications(err.data); }); } } function showCreateFromSnippet() { vm.showSnippets = true; } onInit(); } angular.module('umbraco').controller('Umbraco.Editors.PartialViews.CreateController', PartialViewsCreateController); }()); /** * @ngdoc controller * @name Umbraco.Editors.PartialViews.DeleteController * @function * * @description * The controller for deleting partial views */ function PartialViewsDeleteController($scope, codefileResource, treeService, navigationService) { $scope.performDelete = function () { //mark it for deletion (used in the UI) $scope.currentNode.loading = true; // Reset the error message $scope.error = null; codefileResource.deleteByPath('partialViews', $scope.currentNode.id).then(function () { $scope.currentNode.loading = false; //get the root node before we remove it var rootNode = treeService.getTreeRoot($scope.currentNode); //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); }, function (err) { $scope.currentNode.loading = false; $scope.error = err; }); }; $scope.cancel = function () { navigationService.hideDialog(); }; } angular.module('umbraco').controller('Umbraco.Editors.PartialViews.DeleteController', PartialViewsDeleteController); (function () { 'use strict'; function PartialViewsEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper) { var vm = this; var localizeSaving = localizationService.localize('general_saving'); vm.page = {}; vm.page.loading = true; vm.partialView = {}; //menu vm.page.menu = {}; vm.page.menu.currentSection = appState.getSectionState('currentSection'); vm.page.menu.currentNode = null; //Used to toggle the keyboard shortcut modal //From a custom keybinding in ace editor - that conflicts with our own to show the dialog vm.showKeyboardShortcut = false; //Keyboard shortcuts for help dialog vm.page.keyboardShortcutsOverview = []; vm.page.keyboardShortcutsOverview.push(templateHelper.getGeneralShortcuts()); vm.page.keyboardShortcutsOverview.push(templateHelper.getEditorShortcuts()); vm.page.keyboardShortcutsOverview.push(templateHelper.getPartialViewEditorShortcuts()); // bind functions to view model vm.save = save; vm.openPageFieldOverlay = openPageFieldOverlay; vm.openDictionaryItemOverlay = openDictionaryItemOverlay; vm.openQueryBuilderOverlay = openQueryBuilderOverlay; vm.openMacroOverlay = openMacroOverlay; vm.openInsertOverlay = openInsertOverlay; /* Functions bound to view model */ function save() { vm.page.saveButtonState = 'busy'; vm.partialView.content = vm.editor.getValue(); contentEditingHelper.contentEditorPerformSave({ statusMessage: localizeSaving, saveMethod: codefileResource.save, scope: $scope, content: vm.partialView, //We do not redirect on failure for partialviews - this is because it is not possible to actually save the partialviews // type when server side validation fails - as opposed to content where we are capable of saving the content // item if server side validation fails redirectOnFailure: false, rebindCallback: function (orignal, saved) { } }).then(function (saved) { localizationService.localize('speechBubbles_partialViewSavedHeader').then(function (headerValue) { localizationService.localize('speechBubbles_partialViewSavedText').then(function (msgValue) { notificationsService.success(headerValue, msgValue); }); }); //check if the name changed, if so we need to redirect if (vm.partialView.id !== saved.id) { contentEditingHelper.redirectToRenamedContent(saved.id); } else { vm.page.saveButtonState = 'success'; vm.partialView = saved; //sync state editorState.set(vm.partialView); // normal tree sync navigationService.syncTree({ tree: 'partialViews', path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { vm.page.menu.currentNode = syncArgs.node; }); // clear $dirty state on form setFormState('pristine'); } }, function (err) { vm.page.saveButtonState = 'error'; localizationService.localize('speechBubbles_validationFailedHeader').then(function (headerValue) { localizationService.localize('speechBubbles_validationFailedMessage').then(function (msgValue) { notificationsService.error(headerValue, msgValue); }); }); }); } function openInsertOverlay() { vm.insertOverlay = { view: 'insert', allowedTypes: { macro: true, dictionary: true, umbracoField: true }, hideSubmitButton: true, show: true, submit: function (model) { switch (model.insert.type) { case 'macro': var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, 'Mvc'); insert(macroObject.syntax); break; case 'dictionary': var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); insert(code); break; case 'umbracoField': insert(model.insert.umbracoField); break; } vm.insertOverlay.show = false; vm.insertOverlay = null; }, close: function (oldModel) { // close the dialog vm.insertOverlay.show = false; vm.insertOverlay = null; // focus editor vm.editor.focus(); } }; } function openMacroOverlay() { vm.macroPickerOverlay = { view: 'macropicker', dialogData: {}, show: true, title: 'Insert macro', submit: function (model) { var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, 'Mvc'); insert(macroObject.syntax); vm.macroPickerOverlay.show = false; vm.macroPickerOverlay = null; }, close: function (oldModel) { // close the dialog vm.macroPickerOverlay.show = false; vm.macroPickerOverlay = null; // focus editor vm.editor.focus(); } }; } function openPageFieldOverlay() { vm.pageFieldOverlay = { submitButtonLabel: 'Insert', closeButtonlabel: 'Cancel', view: 'insertfield', show: true, submit: function (model) { insert(model.umbracoField); vm.pageFieldOverlay.show = false; vm.pageFieldOverlay = null; }, close: function (model) { // close the dialog vm.pageFieldOverlay.show = false; vm.pageFieldOverlay = null; // focus editor vm.editor.focus(); } }; } function openDictionaryItemOverlay() { vm.dictionaryItemOverlay = { view: 'treepicker', section: 'settings', treeAlias: 'dictionary', entityType: 'dictionary', multiPicker: false, show: true, title: 'Insert dictionary item', emptyStateMessage: localizationService.localize('emptyStates_emptyDictionaryTree'), select: function (node) { var code = templateHelper.getInsertDictionarySnippet(node.name); insert(code); vm.dictionaryItemOverlay.show = false; vm.dictionaryItemOverlay = null; }, close: function (model) { // close dialog vm.dictionaryItemOverlay.show = false; vm.dictionaryItemOverlay = null; // focus editor vm.editor.focus(); } }; } function openQueryBuilderOverlay() { vm.queryBuilderOverlay = { view: 'querybuilder', show: true, title: 'Query for content', submit: function (model) { var code = templateHelper.getQuerySnippet(model.result.queryExpression); insert(code); vm.queryBuilderOverlay.show = false; vm.queryBuilderOverlay = null; }, close: function (model) { // close dialog vm.queryBuilderOverlay.show = false; vm.queryBuilderOverlay = null; // focus editor vm.editor.focus(); } }; } /* Local functions */ function init() { //we need to load this somewhere, for now its here. assetsService.loadCss('lib/ace-razor-mode/theme/razor_chrome.css', $scope); if ($routeParams.create) { var snippet = 'Empty'; if ($routeParams.snippet) { snippet = $routeParams.snippet; } codefileResource.getScaffold('partialViews', $routeParams.id, snippet).then(function (partialView) { ready(partialView, false); }); } else { codefileResource.getByPath('partialViews', $routeParams.id).then(function (partialView) { ready(partialView, true); }); } } function ready(partialView, syncTree) { vm.page.loading = false; vm.partialView = partialView; //sync state editorState.set(vm.partialView); if (syncTree) { navigationService.syncTree({ tree: 'partialViews', path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { vm.page.menu.currentNode = syncArgs.node; }); } // ace configuration vm.aceOption = { mode: 'razor', theme: 'chrome', showPrintMargin: false, advanced: { fontSize: '14px' }, onLoad: function (_editor) { vm.editor = _editor; //Update the auto-complete method to use ctrl+alt+space _editor.commands.bindKey('ctrl-alt-space', 'startAutocomplete'); //Unassigns the keybinding (That was previously auto-complete) //As conflicts with our own tree search shortcut _editor.commands.bindKey('ctrl-space', null); // Assign new keybinding _editor.commands.addCommands([ //Disable (alt+shift+K) //Conflicts with our own show shortcuts dialog - this overrides it { name: 'unSelectOrFindPrevious', bindKey: 'Alt-Shift-K', exec: function () { //Toggle the show keyboard shortcuts overlay $scope.$apply(function () { vm.showKeyboardShortcut = !vm.showKeyboardShortcut; }); }, readOnly: true }, { name: 'insertUmbracoValue', bindKey: 'Alt-Shift-V', exec: function () { $scope.$apply(function () { openPageFieldOverlay(); }); }, readOnly: true }, { name: 'insertDictionary', bindKey: 'Alt-Shift-D', exec: function () { $scope.$apply(function () { openDictionaryItemOverlay(); }); }, readOnly: true }, { name: 'insertUmbracoMacro', bindKey: 'Alt-Shift-M', exec: function () { $scope.$apply(function () { openMacroOverlay(); }); }, readOnly: true }, { name: 'insertQuery', bindKey: 'Alt-Shift-Q', exec: function () { $scope.$apply(function () { openQueryBuilderOverlay(); }); }, readOnly: true } ]); // initial cursor placement // Keep cursor in name field if we are create a new template // else set the cursor at the bottom of the code editor if (!$routeParams.create) { $timeout(function () { vm.editor.navigateFileEnd(); vm.editor.focus(); persistCurrentLocation(); }); } //change on blur, focus vm.editor.on('blur', persistCurrentLocation); vm.editor.on('focus', persistCurrentLocation); vm.editor.on('change', changeAceEditor); } }; } function insert(str) { vm.editor.focus(); vm.editor.moveCursorToPosition(vm.currentPosition); vm.editor.insert(str); // set form state to $dirty setFormState('dirty'); } function persistCurrentLocation() { vm.currentPosition = vm.editor.getCursorPosition(); } function changeAceEditor() { setFormState('dirty'); } function setFormState(state) { // get the current form var currentForm = angularHelper.getCurrentForm($scope); // set state if (state === 'dirty') { currentForm.$setDirty(); } else if (state === 'pristine') { currentForm.$setPristine(); } } init(); } angular.module('umbraco').controller('Umbraco.Editors.PartialViews.EditController', PartialViewsEditController); }()); angular.module('umbraco').controller('Umbraco.PrevalueEditors.BooleanController', function ($scope) { function updateToggleValue() { $scope.toggleValue = false; if ($scope.model && $scope.model.value && ($scope.model.value.toString() === '1' || angular.lowercase($scope.model.value) === 'true')) { $scope.toggleValue = true; } } if ($scope.model.value === null) { $scope.model.value = '0'; } updateToggleValue(); $scope.toggle = function () { if ($scope.model.value === 1 || $scope.model.value === '1') { $scope.model.value = '0'; updateToggleValue(); return; } $scope.model.value = '1'; updateToggleValue(); }; }); angular.module('umbraco').controller('Umbraco.PrevalueEditors.ColorPickerController', function ($scope) { //setup the default config var config = { useLabel: false }; //map the user config angular.extend(config, $scope.model.config); //map back to the model $scope.model.config = config; $scope.isConfigured = $scope.model.prevalues && _.keys($scope.model.prevalues).length > 0; $scope.model.items = []; // Make an array from the dictionary var items = []; if (angular.isArray($scope.model.prevalues)) { for (var i in $scope.model.prevalues) { var oldValue = $scope.model.prevalues[i]; if (!isValidHex(oldValue.value || oldValue)) continue; if (oldValue.hasOwnProperty('value')) { var hexCode = toFullHex(oldValue.value); items.push({ value: hexCode.substr(1, hexCode.length), label: oldValue.label, id: i }); } else { var hexCode = toFullHex(oldValue); items.push({ value: hexCode.substr(1, hexCode.length), label: oldValue, id: i }); } } // Now make the editor model the array $scope.model.items = items; } function toFullHex(hex) { if (hex.length === 4 && hex.charAt(0) === '#') { hex = '#' + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2) + hex.charAt(3) + hex.charAt(3); } return hex.toLowerCase(); } function isValidHex(str) { return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(str); } }); function imageFilePickerController($scope) { $scope.add = function () { $scope.mediaPickerOverlay = { view: 'mediapicker', disableFolderSelect: true, onlyImages: true, show: true, submit: function (model) { $scope.model.value = model.selectedImages[0].image; $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; }, close: function () { $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; } }; }; $scope.remove = function () { $scope.model.value = null; }; } angular.module('umbraco').controller('Umbraco.PrevalueEditors.ImageFilePickerController', imageFilePickerController); //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it function mediaPickerController($scope, dialogService, entityResource, $log, iconHelper) { function trim(str, chr) { var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); return str.replace(rgxtrim, ''); } $scope.renderModel = []; $scope.allowRemove = true; $scope.allowEdit = true; $scope.sortable = false; var dialogOptions = { multiPicker: false, entityType: 'Media', section: 'media', treeAlias: 'media', idType: 'int' }; //combine the dialogOptions with any values returned from the server if ($scope.model.config) { angular.extend(dialogOptions, $scope.model.config); } $scope.openContentPicker = function () { $scope.contentPickerOverlay = dialogOptions; $scope.contentPickerOverlay.view = 'treePicker'; $scope.contentPickerOverlay.show = true; $scope.contentPickerOverlay.submit = function (model) { if ($scope.contentPickerOverlay.multiPicker) { _.each(model.selection, function (item, i) { $scope.add(item); }); } else { $scope.clear(); $scope.add(model.selection[0]); } $scope.contentPickerOverlay.show = false; $scope.contentPickerOverlay = null; }; $scope.contentPickerOverlay.close = function (oldModel) { $scope.contentPickerOverlay.show = false; $scope.contentPickerOverlay = null; }; }; $scope.remove = function (index) { $scope.renderModel.splice(index, 1); }; $scope.clear = function () { $scope.renderModel = []; }; $scope.add = function (item) { var itemId = dialogOptions.idType === 'udi' ? item.udi : item.id; var currIds = _.map($scope.renderModel, function (i) { return dialogOptions.idType === 'udi' ? i.udi : i.id; }); if (currIds.indexOf(itemId) < 0) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, udi: item.udi }); // store the index of the new item in the renderModel collection so we can find it again var itemRenderIndex = $scope.renderModel.length - 1; // get and update the path for the picked node entityResource.getUrl(item.id, dialogOptions.entityType).then(function (data) { $scope.renderModel[itemRenderIndex].path = data; }); } }; var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { var currIds = _.map($scope.renderModel, function (i) { return dialogOptions.idType === 'udi' ? i.udi : i.id; }); $scope.model.value = trim(currIds.join(), ','); }); //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); //load media data var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; if (modelIds.length > 0) { entityResource.getByIds(modelIds, dialogOptions.entityType).then(function (data) { _.each(data, function (item, i) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, udi: item.udi }); // store the index of the new item in the renderModel collection so we can find it again var itemRenderIndex = $scope.renderModel.length - 1; // get and update the path for the picked node entityResource.getUrl(item.id, dialogOptions.entityType).then(function (data) { $scope.renderModel[itemRenderIndex].path = data; }); }); }); } } angular.module('umbraco').controller('Umbraco.PrevalueEditors.MediaPickerController', mediaPickerController); angular.module('umbraco').controller('Umbraco.PrevalueEditors.MultiValuesController', function ($scope, $timeout) { //NOTE: We need to make each item an object, not just a string because you cannot 2-way bind to a primitive. $scope.newItem = ''; $scope.hasError = false; $scope.focusOnNew = false; if (!angular.isArray($scope.model.value)) { //make an array from the dictionary var items = []; for (var i in $scope.model.value) { items.push({ value: $scope.model.value[i].value, sortOrder: $scope.model.value[i].sortOrder, id: i }); } //ensure the items are sorted by the provided sort order items.sort(function (a, b) { return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; }); //now make the editor model the array $scope.model.value = items; } $scope.remove = function (item, evt) { evt.preventDefault(); $scope.model.value = _.reject($scope.model.value, function (x) { return x.value === item.value; }); }; $scope.add = function (evt) { evt.preventDefault(); if ($scope.newItem) { if (!_.contains($scope.model.value, $scope.newItem)) { $scope.model.value.push({ value: $scope.newItem }); $scope.newItem = ''; $scope.hasError = false; $scope.focusOnNew = true; return; } } //there was an error, do the highlight (will be set back by the directive) $scope.hasError = true; }; $scope.sortableOptions = { axis: 'y', containment: 'parent', cursor: 'move', items: '> div.control-group', tolerance: 'pointer', update: function (e, ui) { // Get the new and old index for the moved element (using the text as the identifier, so // we'd have a problem if two prevalues were the same, but that would be unlikely) var newIndex = ui.item.index(); var movedPrevalueText = $('input[type="text"]', ui.item).val(); var originalIndex = getElementIndexByPrevalueText(movedPrevalueText); // Move the element in the model if (originalIndex > -1) { var movedElement = $scope.model.value[originalIndex]; $scope.model.value.splice(originalIndex, 1); $scope.model.value.splice(newIndex, 0, movedElement); } } }; $scope.createNew = function (event) { if (event.keyCode == 13) { $scope.add(event); } }; function getElementIndexByPrevalueText(value) { for (var i = 0; i < $scope.model.value.length; i++) { if ($scope.model.value[i].value === value) { return i; } } return -1; } }); //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller('Umbraco.PrevalueEditors.TreePickerController', function ($scope, dialogService, entityResource, $log, iconHelper) { $scope.renderModel = []; $scope.ids = []; $scope.allowRemove = true; $scope.allowEdit = true; $scope.sortable = false; var config = { multiPicker: false, entityType: 'Document', type: 'content', treeAlias: 'content', idType: 'int' }; //combine the config with any values returned from the server if ($scope.model.config) { angular.extend(config, $scope.model.config); } if ($scope.model.value) { $scope.ids = $scope.model.value.split(','); entityResource.getByIds($scope.ids, config.entityType).then(function (data) { _.each(data, function (item, i) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, udi: item.udi }); // store the index of the new item in the renderModel collection so we can find it again var itemRenderIndex = $scope.renderModel.length - 1; // get and update the path for the picked node entityResource.getUrl(item.id, config.entityType).then(function (data) { $scope.renderModel[itemRenderIndex].path = data; }); }); }); } $scope.openContentPicker = function () { $scope.treePickerOverlay = config; $scope.treePickerOverlay.section = config.type; $scope.treePickerOverlay.view = 'treePicker'; $scope.treePickerOverlay.show = true; $scope.treePickerOverlay.submit = function (model) { if (config.multiPicker) { populate(model.selection); } else { populate(model.selection[0]); } $scope.treePickerOverlay.show = false; $scope.treePickerOverlay = null; }; $scope.treePickerOverlay.close = function (oldModel) { $scope.treePickerOverlay.show = false; $scope.treePickerOverlay = null; }; }; $scope.remove = function (index) { $scope.renderModel.splice(index, 1); $scope.ids.splice(index, 1); $scope.model.value = trim($scope.ids.join(), ','); }; $scope.clear = function () { $scope.model.value = ''; $scope.renderModel = []; $scope.ids = []; }; $scope.add = function (item) { var itemId = config.idType === 'udi' ? item.udi : item.id; if ($scope.ids.indexOf(itemId) < 0) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); $scope.ids.push(itemId); $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, udi: item.udi }); $scope.model.value = trim($scope.ids.join(), ','); // store the index of the new item in the renderModel collection so we can find it again var itemRenderIndex = $scope.renderModel.length - 1; // get and update the path for the picked node entityResource.getUrl(item.id, config.entityType).then(function (data) { $scope.renderModel[itemRenderIndex].path = data; }); } }; var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { $scope.model.value = trim($scope.ids.join(), ','); }); //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); function trim(str, chr) { var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); return str.replace(rgxtrim, ''); } function populate(data) { if (angular.isArray(data)) { _.each(data, function (item, i) { $scope.add(item); }); } else { $scope.clear(); $scope.add(data); } } }); //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller('Umbraco.PrevalueEditors.TreeSourceController', function ($scope, dialogService, entityResource, $log, iconHelper) { if (!$scope.model) { $scope.model = {}; } if (!$scope.model.value) { $scope.model.value = { type: 'content' }; } if (!$scope.model.config) { $scope.model.config = { idType: 'int' }; } if ($scope.model.value.id && $scope.model.value.type !== 'member') { var ent = 'Document'; if ($scope.model.value.type === 'media') { ent = 'Media'; } entityResource.getById($scope.model.value.id, ent).then(function (item) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); $scope.node = item; }); } $scope.openContentPicker = function () { $scope.treePickerOverlay = { view: 'treepicker', idType: $scope.model.config.idType, section: $scope.model.value.type, treeAlias: $scope.model.value.type, multiPicker: false, show: true, submit: function (model) { var item = model.selection[0]; populate(item); $scope.treePickerOverlay.show = false; $scope.treePickerOverlay = null; } }; }; $scope.clear = function () { $scope.model.value.id = undefined; $scope.node = undefined; $scope.model.value.query = undefined; }; //we always need to ensure we dont submit anything broken var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { if ($scope.model.value.type === 'member') { $scope.model.value.id = -1; $scope.model.value.query = ''; } }); //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); function populate(item) { $scope.clear(); item.icon = iconHelper.convertFromLegacyIcon(item.icon); $scope.node = item; $scope.model.value.id = $scope.model.config.idType === 'udi' ? item.udi : item.id; } }); function booleanEditorController($scope, angularHelper) { function setupViewModel() { $scope.renderModel = { value: false }; if ($scope.model.config && $scope.model.config.default && $scope.model.config.default.toString() === '1' && $scope.model && !$scope.model.value) { $scope.renderModel.value = true; } if ($scope.model && $scope.model.value && ($scope.model.value.toString() === '1' || angular.lowercase($scope.model.value) === 'true')) { $scope.renderModel.value = true; } } setupViewModel(); if ($scope.model && !$scope.model.value) { $scope.model.value = $scope.renderModel.value === true ? '1' : '0'; } //here we declare a special method which will be called whenever the value has changed from the server //this is instead of doing a watch on the model.value = faster $scope.model.onValueChanged = function (newVal, oldVal) { //update the display val again if it has changed from the server setupViewModel(); }; // Update the value when the toggle is clicked $scope.toggle = function () { angularHelper.getCurrentForm($scope).$setDirty(); if ($scope.renderModel.value) { $scope.model.value = '0'; setupViewModel(); return; } $scope.model.value = '1'; setupViewModel(); }; } angular.module('umbraco').controller('Umbraco.PropertyEditors.BooleanController', booleanEditorController); angular.module('umbraco').controller('Umbraco.PropertyEditors.ChangePasswordController', function ($scope, $routeParams) { $scope.isNew = $routeParams.create; function resetModel() { //the model config will contain an object, if it does not we'll create defaults //NOTE: We will not support doing the password regex on the client side because the regex on the server side //based on the membership provider cannot always be ported to js from .net directly. /* { hasPassword: true/false, requiresQuestionAnswer: true/false, enableReset: true/false, enablePasswordRetrieval: true/false, minPasswordLength: 10 } */ //set defaults if they are not available if (!$scope.model.config || $scope.model.config.disableToggle === undefined) { $scope.model.config.disableToggle = false; } if (!$scope.model.config || $scope.model.config.hasPassword === undefined) { $scope.model.config.hasPassword = false; } if (!$scope.model.config || $scope.model.config.enablePasswordRetrieval === undefined) { $scope.model.config.enablePasswordRetrieval = true; } if (!$scope.model.config || $scope.model.config.requiresQuestionAnswer === undefined) { $scope.model.config.requiresQuestionAnswer = false; } if (!$scope.model.config || $scope.model.config.enableReset === undefined) { $scope.model.config.enableReset = true; } if (!$scope.model.config || $scope.model.config.minPasswordLength === undefined) { $scope.model.config.minPasswordLength = 0; } //set the model defaults if (!angular.isObject($scope.model.value)) { //if it's not an object then just create a new one $scope.model.value = { newPassword: null, oldPassword: null, reset: null, answer: null }; } else { //just reset the values if (!$scope.isNew) { //if it is new, then leave the generated pass displayed $scope.model.value.newPassword = null; $scope.model.value.oldPassword = null; } $scope.model.value.reset = null; $scope.model.value.answer = null; } } resetModel(); }); angular.module('umbraco').controller('Umbraco.PropertyEditors.CheckboxListController', function ($scope) { if (angular.isObject($scope.model.config.items)) { //now we need to format the items in the dictionary because we always want to have an array var newItems = []; var vals = _.values($scope.model.config.items); var keys = _.keys($scope.model.config.items); for (var i = 0; i < vals.length; i++) { newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: vals[i].value }); } //ensure the items are sorted by the provided sort order newItems.sort(function (a, b) { return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; }); //re-assign $scope.model.config.items = newItems; } function setupViewModel() { $scope.selectedItems = []; //now we need to check if the value is null/undefined, if it is we need to set it to "" so that any value that is set // to "" gets selected by default if ($scope.model.value === null || $scope.model.value === undefined) { $scope.model.value = []; } for (var i = 0; i < $scope.model.config.items.length; i++) { var isChecked = _.contains($scope.model.value, $scope.model.config.items[i].id); $scope.selectedItems.push({ checked: isChecked, key: $scope.model.config.items[i].id, val: $scope.model.config.items[i].value }); } } setupViewModel(); //update the model when the items checked changes $scope.$watch('selectedItems', function (newVal, oldVal) { $scope.model.value = []; for (var x = 0; x < $scope.selectedItems.length; x++) { if ($scope.selectedItems[x].checked) { $scope.model.value.push($scope.selectedItems[x].key); } } }, true); //here we declare a special method which will be called whenever the value has changed from the server //this is instead of doing a watch on the model.value = faster $scope.model.onValueChanged = function (newVal, oldVal) { //update the display val again if it has changed from the server setupViewModel(); }; }); function ColorPickerController($scope, angularHelper) { //setup the default config var config = { items: [], multiple: false }; //map the user config angular.extend(config, $scope.model.config); //map back to the model $scope.model.config = config; $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; $scope.model.activeColor = { value: '', label: '' }; if ($scope.isConfigured) { for (var key in $scope.model.config.items) { if (!$scope.model.config.items[key].hasOwnProperty('value')) $scope.model.config.items[key] = { value: $scope.model.config.items[key], label: $scope.model.config.items[key] }; } $scope.model.useLabel = isTrue($scope.model.config.useLabel); initActiveColor(); } if (!angular.isArray($scope.model.config.items)) { //make an array from the dictionary var items = []; for (var i in $scope.model.config.items) { var oldValue = $scope.model.config.items[i]; if (oldValue.hasOwnProperty('value')) { items.push({ value: oldValue.value, label: oldValue.label, sortOrder: oldValue.sortOrder, id: i }); } else { items.push({ value: oldValue, label: oldValue, sortOrder: sortOrder, id: i }); } } //ensure the items are sorted by the provided sort order items.sort(function (a, b) { return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; }); //now make the editor model the array $scope.model.config.items = items; } // Method required by the valPropertyValidator directive (returns true if the property editor has at least one color selected) $scope.validateMandatory = function () { var isValid = !$scope.model.validation.mandatory || $scope.model.value != null && $scope.model.value != '' && (!$scope.model.value.hasOwnProperty('value') || $scope.model.value.value !== ''); return { isValid: isValid, errorMsg: 'Value cannot be empty', errorKey: 'required' }; }; $scope.isConfigured = $scope.model.config && $scope.model.config.items && _.keys($scope.model.config.items).length > 0; $scope.onSelect = function (color) { // did the value change? if ($scope.model.value != null && $scope.model.value.value === color) { // User clicked the currently selected color // to remove the selection, they don't want // to select any color after all. // Unselect the color and mark as dirty $scope.model.activeColor = null; $scope.model.value = null; angularHelper.getCurrentForm($scope).$setDirty(); return; } // yes, update the model (label + value) according to the new color var selectedItem = _.find($scope.model.config.items, function (item) { return item.value === color; }); if (!selectedItem) { return; } $scope.model.value = { label: selectedItem.label, value: selectedItem.value }; // make sure to set dirty angularHelper.getCurrentForm($scope).$setDirty(); }; // Finds the color best matching the model's color, // and sets the model color to that one. This is useful when // either the value or label was changed on the data type. function initActiveColor() { // no value - initialize default value if (!$scope.model.value) return; // Backwards compatibility, the color used to be stored as a hex value only if (typeof $scope.model.value === 'string') { $scope.model.value = { value: $scope.model.value, label: $scope.model.value }; } var modelColor = $scope.model.value.value; var modelLabel = $scope.model.value.label; // Check for a full match or partial match. var foundItem = null; // Look for a fully matching color. for (var key in $scope.model.config.items) { var item = $scope.model.config.items[key]; if (item.value == modelColor && item.label == modelLabel) { foundItem = item; break; } } // Look for a color with a matching value. if (!foundItem) { for (var key in $scope.model.config.items) { var item = $scope.model.config.items[key]; if (item.value == modelColor) { foundItem = item; break; } } } // Look for a color with a matching label. if (!foundItem) { for (var key in $scope.model.config.items) { var item = $scope.model.config.items[key]; if (item.label == modelLabel) { foundItem = item; break; } } } // If a match was found, set it as the active color. if (foundItem) { $scope.model.activeColor.value = foundItem.value; $scope.model.activeColor.label = foundItem.label; } } // figures out if a value is trueish enough function isTrue(bool) { return !!bool && bool !== '0' && angular.lowercase(bool) !== 'false'; } } angular.module('umbraco').controller('Umbraco.PropertyEditors.ColorPickerController', ColorPickerController); angular.module('umbraco').controller('Umbraco.PrevalueEditors.MultiColorPickerController', function ($scope, $timeout, assetsService, angularHelper, $element, localizationService, eventsService) { //NOTE: We need to make each color an object, not just a string because you cannot 2-way bind to a primitive. var defaultColor = '000000'; var defaultLabel = null; $scope.newColor = defaultColor; $scope.newLabel = defaultLabel; $scope.hasError = false; $scope.focusOnNew = false; $scope.labels = {}; var labelKeys = [ 'general_cancel', 'general_choose' ]; $scope.labelEnabled = false; eventsService.on('toggleValue', function (e, args) { $scope.labelEnabled = args.value; }); localizationService.localizeMany(labelKeys).then(function (values) { $scope.labels.cancel = values[0]; $scope.labels.choose = values[1]; }); assetsService.load([//"lib/spectrum/tinycolor.js", 'lib/spectrum/spectrum.js'], $scope).then(function () { var elem = $element.find('input[name=\'newColor\']'); elem.spectrum({ color: null, showInitial: false, chooseText: $scope.labels.choose, cancelText: $scope.labels.cancel, preferredFormat: 'hex', showInput: true, clickoutFiresChange: true, hide: function (color) { //show the add butotn $element.find('.btn.add').show(); }, change: function (color) { angularHelper.safeApply($scope, function () { $scope.newColor = color.toHexString().trimStart('#'); // #ff0000 }); }, show: function () { //hide the add butotn $element.find('.btn.add').hide(); } }); }); if (!angular.isArray($scope.model.value)) { //make an array from the dictionary var items = []; for (var i in $scope.model.value) { var oldValue = $scope.model.value[i]; if (oldValue.hasOwnProperty('value')) { items.push({ value: oldValue.value, label: oldValue.label, sortOrder: oldValue.sortOrder, id: i }); } else { items.push({ value: oldValue, label: oldValue, sortOrder: sortOrder, id: i }); } } //ensure the items are sorted by the provided sort order items.sort(function (a, b) { return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; }); //now make the editor model the array $scope.model.value = items; } // ensure labels for (var i = 0; i < $scope.model.value.length; i++) { var item = $scope.model.value[i]; item.label = item.hasOwnProperty('label') ? item.label : item.value; } function validLabel(label) { return label !== null && typeof label !== 'undefined' && label !== '' && label.length && label.length > 0; } $scope.remove = function (item, evt) { evt.preventDefault(); $scope.model.value = _.reject($scope.model.value, function (x) { return x.value === item.value && x.label === item.label; }); }; $scope.add = function (evt) { evt.preventDefault(); if ($scope.newColor) { var newLabel = validLabel($scope.newLabel) ? $scope.newLabel : $scope.newColor; var exists = _.find($scope.model.value, function (item) { return item.value.toUpperCase() === $scope.newColor.toUpperCase() || item.label.toUpperCase() === newLabel.toUpperCase(); }); if (!exists) { $scope.model.value.push({ value: $scope.newColor, label: newLabel }); $scope.newLabel = ''; $scope.hasError = false; $scope.focusOnNew = true; return; } //there was an error, do the highlight (will be set back by the directive) $scope.hasError = true; } }; $scope.sortableOptions = { axis: 'y', containment: 'parent', cursor: 'move', //handle: ".handle, .thumbnail", items: '> div.control-group', tolerance: 'pointer', update: function (e, ui) { // Get the new and old index for the moved element (using the text as the identifier, so // we'd have a problem if two prevalues were the same, but that would be unlikely) var newIndex = ui.item.index(); var movedPrevalueText = $('pre', ui.item).text(); var originalIndex = getElementIndexByPrevalueText(movedPrevalueText); //// Move the element in the model if (originalIndex > -1) { var movedElement = $scope.model.value[originalIndex]; $scope.model.value.splice(originalIndex, 1); $scope.model.value.splice(newIndex, 0, movedElement); } } }; function getElementIndexByPrevalueText(value) { for (var i = 0; i < $scope.model.value.length; i++) { if ($scope.model.value[i].value === value) { return i; } } return -1; } //load the separate css for the editor to avoid it blocking our js loading assetsService.loadCss('lib/spectrum/spectrum.css', $scope); }); //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it function contentPickerController($scope, entityResource, editorState, iconHelper, $routeParams, angularHelper, navigationService, $location, miniEditorHelper, localizationService) { var unsubscribe; function subscribe() { unsubscribe = $scope.$on('formSubmitting', function (ev, args) { var currIds = _.map($scope.renderModel, function (i) { return $scope.model.config.idType === 'udi' ? i.udi : i.id; }); $scope.model.value = trim(currIds.join(), ','); }); } function trim(str, chr) { var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); return str.replace(rgxtrim, ''); } function startWatch() { //We need to watch our renderModel so that we can update the underlying $scope.model.value properly, this is required // because the ui-sortable doesn't dispatch an event after the digest of the sort operation. Any of the events for UI sortable // occur after the DOM has updated but BEFORE the digest has occured so the model has NOT changed yet - it even states so in the docs. // In their source code there is no event so we need to just subscribe to our model changes here. //This also makes it easier to manage models, we update one and the rest will just work. $scope.$watch(function () { //return the joined Ids as a string to watch return _.map($scope.renderModel, function (i) { return $scope.model.config.idType === 'udi' ? i.udi : i.id; }).join(); }, function (newVal) { var currIds = _.map($scope.renderModel, function (i) { return $scope.model.config.idType === 'udi' ? i.udi : i.id; }); $scope.model.value = trim(currIds.join(), ','); //Validate! if ($scope.model.config && $scope.model.config.minNumber && parseInt($scope.model.config.minNumber) > $scope.renderModel.length) { $scope.contentPickerForm.minCount.$setValidity('minCount', false); } else { $scope.contentPickerForm.minCount.$setValidity('minCount', true); } if ($scope.model.config && $scope.model.config.maxNumber && parseInt($scope.model.config.maxNumber) < $scope.renderModel.length) { $scope.contentPickerForm.maxCount.$setValidity('maxCount', false); } else { $scope.contentPickerForm.maxCount.$setValidity('maxCount', true); } setSortingState($scope.renderModel); }); } $scope.renderModel = []; $scope.dialogEditor = editorState && editorState.current && editorState.current.isDialogEditor === true; //the default pre-values var defaultConfig = { multiPicker: false, showOpenButton: false, showEditButton: false, showPathOnHover: false, dataTypeId: null, maxNumber: 1, minNumber: 0, startNode: { query: '', type: 'content', id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker } }; // sortable options $scope.sortableOptions = { axis: 'y', containment: 'parent', distance: 10, opacity: 0.7, tolerance: 'pointer', scroll: true, zIndex: 6000, update: function (e, ui) { angularHelper.getCurrentForm($scope).$setDirty(); } }; if ($scope.model.config) { //merge the server config on top of the default config, then set the server config to use the result $scope.model.config = angular.extend(defaultConfig, $scope.model.config); } //Umbraco persists boolean for prevalues as "0" or "1" so we need to convert that! $scope.model.config.multiPicker = $scope.model.config.multiPicker === '1' ? true : false; $scope.model.config.showOpenButton = $scope.model.config.showOpenButton === '1' ? true : false; $scope.model.config.showEditButton = $scope.model.config.showEditButton === '1' ? true : false; $scope.model.config.showPathOnHover = $scope.model.config.showPathOnHover === '1' ? true : false; var entityType = $scope.model.config.startNode.type === 'member' ? 'Member' : $scope.model.config.startNode.type === 'media' ? 'Media' : 'Document'; $scope.allowOpenButton = entityType === 'Document'; $scope.allowEditButton = entityType === 'Document'; $scope.allowRemoveButton = true; //the dialog options for the picker var dialogOptions = { multiPicker: $scope.model.config.multiPicker, entityType: entityType, filterCssClass: 'not-allowed not-published', startNodeId: null, currentNode: editorState ? editorState.current : null, callback: function (data) { if (angular.isArray(data)) { _.each(data, function (item, i) { $scope.add(item); }); } else { $scope.clear(); $scope.add(data); } angularHelper.getCurrentForm($scope).$setDirty(); }, treeAlias: $scope.model.config.startNode.type, section: $scope.model.config.startNode.type, idType: 'int' }; //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the // pre-value config on to the dialog options angular.extend(dialogOptions, $scope.model.config); //We need to manually handle the filter for members here since the tree displayed is different and only contains // searchable list views if (entityType === 'Member') { //first change the not allowed filter css class dialogOptions.filterCssClass = 'not-allowed'; var currFilter = dialogOptions.filter; //now change the filter to be a method dialogOptions.filter = function (i) { //filter out the list view nodes if (i.metaData.isContainer) { return true; } if (!currFilter) { return false; } //now we need to filter based on what is stored in the pre-vals, this logic duplicates what is in the treepicker.controller, // but not much we can do about that since members require special filtering. var filterItem = currFilter.toLowerCase().split(','); var found = filterItem.indexOf(i.metaData.contentType.toLowerCase()) >= 0; if (!currFilter.startsWith('!') && !found || currFilter.startsWith('!') && found) { return true; } return false; }; } if ($routeParams.section === 'settings' && $routeParams.tree === 'documentTypes') { //if the content-picker is being rendered inside the document-type editor, we don't need to process the startnode query dialogOptions.startNodeId = -1; } else if ($scope.model.config.startNode.query) { //if we have a query for the startnode, we will use that. var rootId = $routeParams.id; entityResource.getByQuery($scope.model.config.startNode.query, rootId, 'Document').then(function (ent) { dialogOptions.startNodeId = $scope.model.config.idType === 'udi' ? ent.udi : ent.id; }); } else { dialogOptions.startNodeId = $scope.model.config.startNode.id; } //dialog $scope.openContentPicker = function () { $scope.contentPickerOverlay = dialogOptions; $scope.contentPickerOverlay.view = 'treepicker'; $scope.contentPickerOverlay.show = true; $scope.contentPickerOverlay.dataTypeId = $scope.model && $scope.model.dataTypeId ? $scope.model.dataTypeId : null; $scope.contentPickerOverlay.submit = function (model) { if (angular.isArray(model.selection)) { _.each(model.selection, function (item, i) { $scope.add(item); }); angularHelper.getCurrentForm($scope).$setDirty(); } $scope.contentPickerOverlay.show = false; $scope.contentPickerOverlay = null; }; $scope.contentPickerOverlay.close = function (oldModel) { $scope.contentPickerOverlay.show = false; $scope.contentPickerOverlay = null; }; }; $scope.remove = function (index) { $scope.renderModel.splice(index, 1); angularHelper.getCurrentForm($scope).$setDirty(); }; $scope.showNode = function (index) { var item = $scope.renderModel[index]; var id = item.id; var section = $scope.model.config.startNode.type.toLowerCase(); entityResource.getPath(id, entityType).then(function (path) { navigationService.changeSection(section); navigationService.showTree(section, { tree: section, path: path, forceReload: false, activate: true }); var routePath = section + '/' + section + '/edit/' + id.toString(); $location.path(routePath).search(''); }); }; $scope.add = function (item) { var currIds = _.map($scope.renderModel, function (i) { return $scope.model.config.idType === 'udi' ? i.udi : i.id; }); var itemId = $scope.model.config.idType === 'udi' ? item.udi : item.id; if (currIds.indexOf(itemId) < 0) { setEntityUrl(item); } }; $scope.clear = function () { $scope.renderModel = []; }; $scope.openMiniEditor = function (node) { miniEditorHelper.launchMiniEditor(node).then(function (updatedNode) { // update the node node.name = updatedNode.name; node.published = updatedNode.hasPublishedVersion; if (entityType !== 'Member') { entityResource.getUrl(updatedNode.id, entityType).then(function (data) { node.url = data; }); } }); }; //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { if (unsubscribe) { unsubscribe(); } }); var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; //load current data if anything selected if (modelIds.length > 0) { entityResource.getByIds(modelIds, entityType).then(function (data) { _.each(modelIds, function (id, i) { var entity = _.find(data, function (d) { return $scope.model.config.idType === 'udi' ? d.udi == id : d.id == id; }); if (entity) { setEntityUrl(entity); } }); //everything is loaded, start the watch on the model startWatch(); subscribe(); }); } else { //everything is loaded, start the watch on the model startWatch(); subscribe(); } function setEntityUrl(entity) { // get url for content and media items if (entityType !== 'Member') { entityResource.getUrl(entity.id, entityType).then(function (data) { // update url angular.forEach($scope.renderModel, function (item) { if (item.id === entity.id) { if (entity.trashed) { item.url = localizationService.dictionary.general_recycleBin; } else { item.url = data; } } }); }); } // add the selected item to the renderModel // if it needs to show a url the item will get // updated when the url comes back from server addSelectedItem(entity); } function addSelectedItem(item) { // set icon if (item.icon) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); } // set default icon if (!item.icon) { switch (entityType) { case 'Document': item.icon = 'icon-document'; break; case 'Media': item.icon = 'icon-picture'; break; case 'Member': item.icon = 'icon-user'; break; } } $scope.renderModel.push({ 'name': item.name, 'id': item.id, 'udi': item.udi, 'icon': item.icon, 'path': item.path, 'url': item.url, 'trashed': item.trashed, 'published': item.metaData && item.metaData.IsPublished === false && entityType === 'Document' ? false : true // only content supports published/unpublished content so we set everything else to published so the UI looks correct }); } function setSortingState(items) { // disable sorting if the list only consist of one item if (items.length > 1) { $scope.sortableOptions.disabled = false; } else { $scope.sortableOptions.disabled = true; } } } angular.module('umbraco').controller('Umbraco.PropertyEditors.ContentPickerController', contentPickerController); function dateTimePickerController($scope, notificationsService, assetsService, angularHelper, userService, $element, dateHelper) { //setup the default config var config = { pickDate: true, pickTime: true, useSeconds: true, format: 'YYYY-MM-DD HH:mm:ss', icons: { time: 'icon-time', date: 'icon-calendar', up: 'icon-chevron-up', down: 'icon-chevron-down' } }; //map the user config $scope.model.config = angular.extend(config, $scope.model.config); //ensure the format doesn't get overwritten with an empty string if ($scope.model.config.format === '' || $scope.model.config.format === undefined || $scope.model.config.format === null) { $scope.model.config.format = $scope.model.config.pickTime ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'; } $scope.hasDatetimePickerValue = $scope.model.value ? true : false; $scope.datetimePickerValue = null; //hide picker if clicking on the document $scope.hidePicker = function () { //$element.find("div:first").datetimepicker("hide"); // Sometimes the statement above fails and generates errors in the browser console. The following statements fix that. var dtp = $element.find('div:first'); if (dtp && dtp.datetimepicker) { dtp.datetimepicker('hide'); } }; $(document).bind('click', $scope.hidePicker); //here we declare a special method which will be called whenever the value has changed from the server //this is instead of doing a watch on the model.value = faster $scope.model.onValueChanged = function (newVal, oldVal) { if (newVal != oldVal) { //check for c# System.DateTime.MinValue being passed as the clear indicator var minDate = moment('0001-01-01'); var newDate = moment(newVal); if (newDate.isAfter(minDate)) { applyDate({ date: moment(newVal) }); } else { $scope.clearDate(); } } }; //handles the date changing via the api function applyDate(e) { angularHelper.safeApply($scope, function () { // when a date is changed, update the model if (e.date && e.date.isValid()) { $scope.datePickerForm.datepicker.$setValidity('pickerError', true); $scope.hasDatetimePickerValue = true; $scope.datetimePickerValue = e.date.format($scope.model.config.format); } else { $scope.hasDatetimePickerValue = false; $scope.datetimePickerValue = null; } setModelValue(); if (!$scope.model.config.pickTime) { $element.find('div:first').datetimepicker('hide', 0); } }); } //sets the scope model value accordingly - this is the value to be sent up to the server and depends on // if the picker is configured to offset time. We always format the date/time in a specific format for sending // to the server, this is different from the format used to display the date/time. function setModelValue() { if ($scope.hasDatetimePickerValue) { var elementData = $element.find('div:first').data().DateTimePicker; if ($scope.model.config.pickTime) { //check if we are supposed to offset the time if ($scope.model.value && $scope.model.config.offsetTime === '1' && Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { $scope.model.value = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset); $scope.serverTime = dateHelper.convertToServerStringTime(elementData.getDate(), Umbraco.Sys.ServerVariables.application.serverTimeOffset, 'YYYY-MM-DD HH:mm:ss Z'); } else { $scope.model.value = elementData.getDate().format('YYYY-MM-DD HH:mm:ss'); } } else { $scope.model.value = elementData.getDate().format('YYYY-MM-DD'); } } else { $scope.model.value = null; } } var picker = null; $scope.clearDate = function () { $scope.hasDatetimePickerValue = false; $scope.datetimePickerValue = null; $scope.model.value = null; $scope.datePickerForm.datepicker.$setValidity('pickerError', true); }; $scope.serverTime = null; $scope.serverTimeNeedsOffsetting = false; if (Umbraco.Sys.ServerVariables.application.serverTimeOffset !== undefined) { // Will return something like 120 var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; // Will return something like -120 var localOffset = new Date().getTimezoneOffset(); // If these aren't equal then offsetting is needed // note the minus in front of serverOffset needed // because C# and javascript return the inverse offset $scope.serverTimeNeedsOffsetting = -serverOffset !== localOffset; } //get the current user to see if we can localize this picker userService.getCurrentUser().then(function (user) { assetsService.loadCss('lib/datetimepicker/bootstrap-datetimepicker.min.css', $scope).then(function () { var filesToLoad = ['lib/datetimepicker/bootstrap-datetimepicker.js']; $scope.model.config.language = user.locale; assetsService.load(filesToLoad, $scope).then(function () { //The Datepicker js and css files are available and all components are ready to use. // Get the id of the datepicker button that was clicked var pickerId = $scope.model.alias; var element = $element.find('div:first'); // Open the datepicker and add a changeDate eventlistener element.datetimepicker(angular.extend({ useCurrent: $scope.model.config.defaultEmpty !== '1' }, $scope.model.config)).on('dp.change', applyDate).on('dp.error', function (a, b, c) { $scope.hasDatetimePickerValue = false; $scope.datePickerForm.datepicker.$setValidity('pickerError', false); }); if ($scope.hasDatetimePickerValue) { var dateVal; //check if we are supposed to offset the time if ($scope.model.value && $scope.model.config.offsetTime === '1' && $scope.serverTimeNeedsOffsetting) { //get the local time offset from the server dateVal = dateHelper.convertToLocalMomentTime($scope.model.value, Umbraco.Sys.ServerVariables.application.serverTimeOffset); $scope.serverTime = dateHelper.convertToServerStringTime(dateVal, Umbraco.Sys.ServerVariables.application.serverTimeOffset, 'YYYY-MM-DD HH:mm:ss Z'); } else { //create a normal moment , no offset required var dateVal = $scope.model.value ? moment($scope.model.value, 'YYYY-MM-DD HH:mm:ss') : moment(); } element.datetimepicker('setValue', dateVal); $scope.datetimePickerValue = dateVal.format($scope.model.config.format); } element.find('input').bind('blur', function () { //we need to force an apply here $scope.$apply(); }); //Ensure to remove the event handler when this instance is destroyted $scope.$on('$destroy', function () { element.find('input').unbind('blur'); element.datetimepicker('destroy'); }); var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { setModelValue(); }); //unbind doc click event! $scope.$on('$destroy', function () { unsubscribe(); }); }); }); }); var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { setModelValue(); }); //unbind doc click event! $scope.$on('$destroy', function () { $(document).unbind('click', $scope.hidePicker); unsubscribe(); }); } angular.module('umbraco').controller('Umbraco.PropertyEditors.DatepickerController', dateTimePickerController); angular.module('umbraco').controller('Umbraco.PropertyEditors.DropdownController', function ($scope) { //setup the default config var config = { items: [], multiple: false }; //map the user config angular.extend(config, $scope.model.config); //map back to the model $scope.model.config = config; function convertArrayToDictionaryArray(model) { //now we need to format the items in the dictionary because we always want to have an array var newItems = []; for (var i = 0; i < model.length; i++) { newItems.push({ id: model[i], sortOrder: 0, value: model[i] }); } return newItems; } function convertObjectToDictionaryArray(model) { //now we need to format the items in the dictionary because we always want to have an array var newItems = []; var vals = _.values($scope.model.config.items); var keys = _.keys($scope.model.config.items); for (var i = 0; i < vals.length; i++) { var label = vals[i].value ? vals[i].value : vals[i]; newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: label }); } return newItems; } if (angular.isArray($scope.model.config.items)) { //PP: I dont think this will happen, but we have tests that expect it to happen.. //if array is simple values, convert to array of objects if (!angular.isObject($scope.model.config.items[0])) { $scope.model.config.items = convertArrayToDictionaryArray($scope.model.config.items); } } else if (angular.isObject($scope.model.config.items)) { $scope.model.config.items = convertObjectToDictionaryArray($scope.model.config.items); } else { throw 'The items property must be either an array or a dictionary'; } //sort the values $scope.model.config.items.sort(function (a, b) { return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; }); //now we need to check if the value is null/undefined, if it is we need to set it to "" so that any value that is set // to "" gets selected by default if ($scope.model.value === null || $scope.model.value === undefined) { if ($scope.model.config.multiple) { $scope.model.value = []; } else { $scope.model.value = ''; } } }); angular.module('umbraco').controller('Umbraco.PropertyEditors.DropdownFlexibleController', function ($scope) { //setup the default config var config = { items: [], multiple: false }; //map the user config angular.extend(config, $scope.model.config); //map back to the model $scope.model.config = config; function convertArrayToDictionaryArray(model) { //now we need to format the items in the dictionary because we always want to have an array var newItems = []; for (var i = 0; i < model.length; i++) { newItems.push({ id: model[i], sortOrder: 0, value: model[i] }); } return newItems; } function convertObjectToDictionaryArray(model) { //now we need to format the items in the dictionary because we always want to have an array var newItems = []; var vals = _.values($scope.model.config.items); var keys = _.keys($scope.model.config.items); for (var i = 0; i < vals.length; i++) { var label = vals[i].value ? vals[i].value : vals[i]; newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: label }); } return newItems; } $scope.updateSingleDropdownValue = function () { $scope.model.value = [$scope.model.singleDropdownValue]; }; if (angular.isArray($scope.model.config.items)) { //PP: I dont think this will happen, but we have tests that expect it to happen.. //if array is simple values, convert to array of objects if (!angular.isObject($scope.model.config.items[0])) { $scope.model.config.items = convertArrayToDictionaryArray($scope.model.config.items); } } else if (angular.isObject($scope.model.config.items)) { $scope.model.config.items = convertObjectToDictionaryArray($scope.model.config.items); } else { throw 'The items property must be either an array or a dictionary'; } //sort the values $scope.model.config.items.sort(function (a, b) { return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; }); //now we need to check if the value is null/undefined, if it is we need to set it to "" so that any value that is set // to "" gets selected by default if ($scope.model.value === null || $scope.model.value === undefined) { if ($scope.model.config.multiple) { $scope.model.value = []; } else { $scope.model.value = ''; } } // if we run in single mode we'll store the value in a local variable // so we can pass an array as the model as our PropertyValueEditor expects that $scope.model.singleDropdownValue = ''; if ($scope.model.config.multiple === '0' && $scope.model.value) { $scope.model.singleDropdownValue = Array.isArray($scope.model.value) ? $scope.model.value[0] : $scope.model.value; } // if we run in multiple mode, make sure the model is an array (in case the property was previously saved in single mode) // also explicitly set the model to null if it's an empty array, so mandatory validation works on the client if ($scope.model.config.multiple === '1' && $scope.model.value) { $scope.model.value = !Array.isArray($scope.model.value) ? [$scope.model.value] : $scope.model.value; if ($scope.model.value.length === 0) { $scope.model.value = null; } } }); /** A drop down list or multi value select list based on an entity type, this can be re-used for any entity types */ function entityPicker($scope, entityResource) { //set the default to DocumentType if (!$scope.model.config.entityType) { $scope.model.config.entityType = 'DocumentType'; } //Determine the select list options and which value to publish if (!$scope.model.config.publishBy) { $scope.selectOptions = 'entity.id as entity.name for entity in entities'; } else { $scope.selectOptions = 'entity.' + $scope.model.config.publishBy + ' as entity.name for entity in entities'; } entityResource.getAll($scope.model.config.entityType).then(function (data) { //convert the ids to strings so the drop downs work properly when comparing _.each(data, function (d) { d.id = d.id.toString(); }); $scope.entities = data; }); if ($scope.model.value === null || $scope.model.value === undefined) { if ($scope.model.config.multiple) { $scope.model.value = []; } else { $scope.model.value = ''; } } else { //if it's multiple, change the value to an array if ($scope.model.config.multiple === '1') { if (_.isString($scope.model.value)) { $scope.model.value = $scope.model.value.split(','); } } } } angular.module('umbraco').controller('Umbraco.PropertyEditors.EntityPickerController', entityPicker); /** * @ngdoc controller * @name Umbraco.Editors.FileUploadController * @function * * @description * The controller for the file upload property editor. It is important to note that the $scope.model.value * doesn't necessarily depict what is saved for this property editor. $scope.model.value can be empty when we * are submitting files because in that case, we are adding files to the fileManager which is what gets peristed * on the server. However, when we are clearing files, we are setting $scope.model.value to "{clearFiles: true}" * to indicate on the server that we are removing files for this property. We will keep the $scope.model.value to * be the name of the file selected (if it is a newly selected file) or keep it to be it's original value, this allows * for the editors to check if the value has changed and to re-bind the property if that is true. * */ function fileUploadController($scope, $element, $compile, imageHelper, fileManager, umbRequestHelper, mediaHelper) { /** Clears the file collections when content is saving (if we need to clear) or after saved */ function clearFiles() { //clear the files collection (we don't want to upload any!) fileManager.setFiles($scope.model.alias, []); //clear the current files $scope.files = []; if ($scope.propertyForm) { if ($scope.propertyForm.fileCount) { //this is required to re-validate $scope.propertyForm.fileCount.$setViewValue($scope.files.length); } } } /** this method is used to initialize the data and to re-initialize it if the server value is changed */ function initialize(index) { clearFiles(); if (!index) { index = 1; } //this is used in order to tell the umb-single-file-upload directive to //rebuild the html input control (and thus clearing the selected file) since //that is the only way to manipulate the html for the file input control. $scope.rebuildInput = { index: index }; //clear the current files $scope.files = []; //store the original value so we can restore it if the user clears and then cancels clearing. $scope.originalValue = $scope.model.value; //create the property to show the list of files currently saved if ($scope.model.value != '' && $scope.model.value != undefined) { var images = $scope.model.value.split(','); $scope.persistedFiles = _.map(images, function (item) { return { file: item, isImage: imageHelper.detectIfImageByExtension(item) }; }); } else { $scope.persistedFiles = []; } _.each($scope.persistedFiles, function (file) { var thumbnailUrl = umbRequestHelper.getApiUrl('imagesApiBaseUrl', 'GetBigThumbnail', [{ originalImagePath: file.file }]); var extension = file.file.substring(file.file.lastIndexOf('.') + 1, file.file.length); file.thumbnail = thumbnailUrl + '&rnd=' + Math.random(); file.extension = extension.toLowerCase(); }); $scope.clearFiles = false; } initialize(); // Method required by the valPropertyValidator directive (returns true if the property editor has at least one file selected) $scope.validateMandatory = function () { return { isValid: !$scope.model.validation.mandatory || ($scope.persistedFiles != null && $scope.persistedFiles.length > 0 || $scope.files != null && $scope.files.length > 0) && !$scope.clearFiles, errorMsg: 'Value cannot be empty', errorKey: 'required' }; }; //listen for clear files changes to set our model to be sent up to the server $scope.$watch('clearFiles', function (isCleared) { if (isCleared == true) { $scope.model.value = { clearFiles: true }; clearFiles(); } else { //reset to original value $scope.model.value = $scope.originalValue; //this is required to re-validate if ($scope.propertyForm) { $scope.propertyForm.fileCount.$setViewValue($scope.files.length); } } }); //listen for when a file is selected $scope.$on('filesSelected', function (event, args) { $scope.$apply(function () { //set the files collection fileManager.setFiles($scope.model.alias, args.files); //clear the current files $scope.files = []; var newVal = ''; for (var i = 0; i < args.files.length; i++) { //save the file object to the scope's files collection $scope.files.push({ alias: $scope.model.alias, file: args.files[i] }); newVal += args.files[i].name.split(',').join('-') + ','; } //this is required to re-validate $scope.propertyForm.fileCount.$setViewValue($scope.files.length); //set clear files to false, this will reset the model too $scope.clearFiles = false; //set the model value to be the concatenation of files selected. Please see the notes // in the description of this controller, it states that this value isn't actually used for persistence, // but we need to set it so that the editor and the server can detect that it's been changed, and it is used for validation. $scope.model.value = { selectedFiles: newVal.trimEnd(',') }; //need to explicity setDirty here as file upload field can't track dirty & we can't use the fileCount (hidden field/model) $scope.propertyForm.$setDirty(); }); }); //listen for when the model value has changed $scope.$watch('model.value', function (newVal, oldVal) { //cannot just check for !newVal because it might be an empty string which we //want to look for. if (newVal !== null && newVal !== undefined && newVal !== oldVal) { // here we need to check if the value change needs to trigger an update in the UI. // if the value is only changed in the controller and not in the server values, we do not // want to trigger an update yet. // we can however no longer rely on checking values in the controller vs. values from the server // to determine whether to update or not, since you could potentially be uploading a file with // the exact same name - in that case we need to reinitialize to show the newly uploaded file. if (newVal.clearFiles !== true && !newVal.selectedFiles) { initialize($scope.rebuildInput.index + 1); } } }); } ; angular.module('umbraco').controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController).run(function (mediaHelper, umbRequestHelper, assetsService) { if (mediaHelper && mediaHelper.registerFileResolver) { //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource // they contain different data structures so if we need to query against it we need to be aware of this. mediaHelper.registerFileResolver('Umbraco.UploadField', function (property, entity, thumbnail) { if (thumbnail) { if (mediaHelper.detectIfImageByExtension(property.value)) { //get default big thumbnail from image processor var thumbnailUrl = property.value + '?rnd=' + moment(entity.updateDate).format('YYYYMMDDHHmmss') + '&width=500&animationprocessmode=first'; return thumbnailUrl; } else { return null; } } else { return property.value; } }); } }); angular.module('umbraco') //this controller is obsolete and should not be used anymore //it proxies everything to the system media list view which has overtaken //all the work this property editor used to perform .controller('Umbraco.PropertyEditors.FolderBrowserController', function ($rootScope, $scope, contentTypeResource) { //get the system media listview contentTypeResource.getPropertyTypeScaffold(-96).then(function (dt) { $scope.fakeProperty = { alias: 'contents', config: dt.config, description: '', editor: dt.editor, hideLabel: true, id: 1, label: 'Contents:', validation: { mandatory: false, pattern: null }, value: '', view: dt.view }; }); }); angular.module('umbraco').controller('Umbraco.PropertyEditors.GoogleMapsController', function ($element, $rootScope, $scope, notificationsService, dialogService, assetsService, $log, $timeout) { assetsService.loadJs('https://www.google.com/jsapi', $scope).then(function () { google.load('maps', '3', { callback: initMap, other_params: 'sensor=false' }); }); function initMap() { //Google maps is available and all components are ready to use. var valueArray = $scope.model.value.split(','); var latLng = new google.maps.LatLng(valueArray[0], valueArray[1]); var mapDiv = document.getElementById($scope.model.alias + '_map'); var mapOptions = { zoom: $scope.model.config.zoom, center: latLng, mapTypeId: google.maps.MapTypeId[$scope.model.config.mapType] }; var geocoder = new google.maps.Geocoder(); var map = new google.maps.Map(mapDiv, mapOptions); var marker = new google.maps.Marker({ map: map, position: latLng, draggable: true }); google.maps.event.addListener(map, 'click', function (event) { dialogService.mediaPicker({ callback: function (data) { var image = data.selection[0].src; var latLng = event.latLng; var marker = new google.maps.Marker({ map: map, icon: image, position: latLng, draggable: true }); google.maps.event.addListener(marker, 'dragend', function (e) { var newLat = marker.getPosition().lat(); var newLng = marker.getPosition().lng(); codeLatLng(marker.getPosition(), geocoder); //set the model value $scope.model.vvalue = newLat + ',' + newLng; }); } }); }); var tabShown = function (e) { google.maps.event.trigger(map, 'resize'); }; //listen for tab changes if (tabsCtrl != null) { tabsCtrl.onTabShown(function (args) { tabShown(); }); } $element.closest('.umb-panel.tabbable').on('shown', '.nav-tabs a', tabShown); $scope.$on('$destroy', function () { $element.closest('.umb-panel.tabbable').off('shown', '.nav-tabs a', tabShown); }); } function codeLatLng(latLng, geocoder) { geocoder.geocode({ 'latLng': latLng }, function (results, status) { if (status == google.maps.GeocoderStatus.OK) { var location = results[0].formatted_address; $rootScope.$apply(function () { notificationsService.success('Peter just went to: ', location); }); } }); } //here we declare a special method which will be called whenever the value has changed from the server //this is instead of doing a watch on the model.value = faster $scope.model.onValueChanged = function (newVal, oldVal) { //update the display val again if it has changed from the server initMap(); }; }); angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditor.LayoutConfigController', function ($scope) { $scope.currentLayout = $scope.model.currentLayout; $scope.columns = $scope.model.columns; $scope.rows = $scope.model.rows; $scope.scaleUp = function (section, max, overflow) { var add = 1; if (overflow !== true) { add = max > 1 ? 1 : max; } //var add = (max > 1) ? 1 : max; section.grid = section.grid + add; }; $scope.scaleDown = function (section) { var remove = section.grid > 1 ? 1 : 0; section.grid = section.grid - remove; }; $scope.percentage = function (spans) { return (spans / $scope.columns * 100).toFixed(8); }; /**************** Section *****************/ $scope.configureSection = function (section, template) { if (section === undefined) { var space = $scope.availableLayoutSpace > 4 ? 4 : $scope.availableLayoutSpace; section = { grid: space }; template.sections.push(section); } $scope.currentSection = section; $scope.currentSection.allowAll = section.allowAll || !section.allowed || !section.allowed.length; }; $scope.toggleAllowed = function (section) { if (section.allowed) { delete section.allowed; } else { section.allowed = []; } }; $scope.deleteSection = function (section, template) { if ($scope.currentSection === section) { $scope.currentSection = undefined; } var index = template.sections.indexOf(section); template.sections.splice(index, 1); }; $scope.closeSection = function () { $scope.currentSection = undefined; }; $scope.$watch('currentLayout', function (layout) { if (layout) { var total = 0; _.forEach(layout.sections, function (section) { total = total + section.grid; }); $scope.availableLayoutSpace = $scope.columns - total; } }, true); }); function RowConfigController($scope) { $scope.currentRow = $scope.model.currentRow; $scope.editors = $scope.model.editors; $scope.columns = $scope.model.columns; $scope.scaleUp = function (section, max, overflow) { var add = 1; if (overflow !== true) { add = max > 1 ? 1 : max; } //var add = (max > 1) ? 1 : max; section.grid = section.grid + add; }; $scope.scaleDown = function (section) { var remove = section.grid > 1 ? 1 : 0; section.grid = section.grid - remove; }; $scope.percentage = function (spans) { return (spans / $scope.columns * 100).toFixed(8); }; /**************** area *****************/ $scope.configureCell = function (cell, row) { if ($scope.currentCell && $scope.currentCell === cell) { delete $scope.currentCell; } else { if (cell === undefined) { var available = $scope.availableRowSpace; var space = 4; if (available < 4 && available > 0) { space = available; } cell = { grid: space }; row.areas.push(cell); } $scope.currentCell = cell; $scope.currentCell.allowAll = cell.allowAll || !cell.allowed || !cell.allowed.length; } }; $scope.toggleAllowed = function (cell) { if (cell.allowed) { delete cell.allowed; } else { cell.allowed = []; } }; $scope.deleteArea = function (cell, row) { if ($scope.currentCell === cell) { $scope.currentCell = undefined; } var index = row.areas.indexOf(cell); row.areas.splice(index, 1); }; $scope.closeArea = function () { $scope.currentCell = undefined; }; $scope.nameChanged = false; var originalName = $scope.currentRow.name; $scope.$watch('currentRow', function (row) { if (row) { var total = 0; _.forEach(row.areas, function (area) { total = total + area.grid; }); $scope.availableRowSpace = $scope.columns - total; if (originalName) { if (originalName != row.name) { $scope.nameChanged = true; } else { $scope.nameChanged = false; } } } }, true); } angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditor.RowConfigController', RowConfigController); angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.EmbedController', function ($scope, $rootScope, $timeout) { $scope.setEmbed = function () { $scope.embedDialog = {}; $scope.embedDialog.view = 'embed'; $scope.embedDialog.show = true; $scope.embedDialog.submit = function (model) { $scope.control.value = model.embed.preview; $scope.embedDialog.show = false; $scope.embedDialog = null; }; $scope.embedDialog.close = function (oldModel) { $scope.embedDialog.show = false; $scope.embedDialog = null; }; }; $timeout(function () { if ($scope.control.$initializing) { $scope.setEmbed(); } }, 200); }); angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.MacroController', function ($scope, $rootScope, $timeout, dialogService, macroResource, macroService, $routeParams) { $scope.title = 'Click to insert macro'; $scope.setMacro = function () { var dialogData = { richTextEditor: true, macroData: $scope.control.value || { macroAlias: $scope.control.editor.config && $scope.control.editor.config.macroAlias ? $scope.control.editor.config.macroAlias : '' } }; $scope.macroPickerOverlay = {}; $scope.macroPickerOverlay.view = 'macropicker'; $scope.macroPickerOverlay.dialogData = dialogData; $scope.macroPickerOverlay.show = true; $scope.macroPickerOverlay.submit = function (model) { var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); $scope.control.value = { macroAlias: macroObject.macroAlias, macroParamsDictionary: macroObject.macroParamsDictionary }; $scope.setPreview($scope.control.value); $scope.macroPickerOverlay.show = false; $scope.macroPickerOverlay = null; }; $scope.macroPickerOverlay.close = function (oldModel) { $scope.macroPickerOverlay.show = false; $scope.macroPickerOverlay = null; }; }; $scope.setPreview = function (macro) { var contentId = $routeParams.id; macroResource.getMacroResultAsHtmlForEditor(macro.macroAlias, contentId, macro.macroParamsDictionary).then(function (htmlResult) { $scope.title = macro.macroAlias; if (htmlResult.trim().length > 0 && htmlResult.indexOf('Macro:') < 0) { $scope.preview = htmlResult; } }); }; $timeout(function () { if ($scope.control.$initializing) { $scope.setMacro(); } else if ($scope.control.value) { $scope.setPreview($scope.control.value); } }, 200); }); angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.MediaController', function ($scope, $rootScope, $timeout, userService) { if (!$scope.model.config.startNodeId) { if ($scope.model.config.ignoreUserStartNodes === '1') { $scope.model.config.startNodeId = -1; $scope.model.config.startNodeIsVirtual = true; } else { userService.getCurrentUser().then(function (userData) { $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; }); } } $scope.setImage = function () { $scope.mediaPickerOverlay = {}; $scope.mediaPickerOverlay.view = 'mediapicker'; $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : null; $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : null; $scope.mediaPickerOverlay.dataTypeId = $scope.model && $scope.model.dataTypeId ? $scope.model.dataTypeId : null; $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : null; $scope.mediaPickerOverlay.showDetails = true; $scope.mediaPickerOverlay.disableFolderSelect = true; $scope.mediaPickerOverlay.onlyImages = true; $scope.mediaPickerOverlay.show = true; $scope.mediaPickerOverlay.submit = function (model) { var selectedImage = model.selectedImages[0]; $scope.control.value = { focalPoint: selectedImage.focalPoint, id: selectedImage.id, udi: selectedImage.udi, image: selectedImage.image, altText: selectedImage.altText }; $scope.setUrl(); $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; }; $scope.mediaPickerOverlay.close = function (oldModel) { $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; }; }; $scope.setUrl = function () { if ($scope.control.value.image) { var url = $scope.control.value.image; if ($scope.control.editor.config && $scope.control.editor.config.size) { url += '?width=' + $scope.control.editor.config.size.width; url += '&height=' + $scope.control.editor.config.size.height; url += '&animationprocessmode=first'; if ($scope.control.value.focalPoint) { url += '¢er=' + $scope.control.value.focalPoint.top + ',' + $scope.control.value.focalPoint.left; url += '&mode=crop'; } } // set default size if no crop present (moved from the view) if (url.indexOf('?') == -1) { url += '?width=800&upscale=false&animationprocessmode=false'; } $scope.url = url; } }; $timeout(function () { if ($scope.control.$initializing) { $scope.setImage(); } else if ($scope.control.value) { $scope.setUrl(); } }, 200); }); (function () { 'use strict'; function GridRichTextEditorController($scope, tinyMceService, macroService, editorState, entityResource) { var vm = this; vm.openLinkPicker = openLinkPicker; vm.openMediaPicker = openMediaPicker; vm.openMacroPicker = openMacroPicker; vm.openEmbed = openEmbed; var dataTypeId = $scope.model && $scope.model.dataTypeId ? $scope.model.dataTypeId : null; function openLinkPicker(editor, currentTarget, anchorElement) { entityResource.getAnchors(JSON.stringify($scope.model.value)).then(function (anchorValues) { vm.linkPickerOverlay = { view: 'linkpicker', currentTarget: currentTarget, anchors: anchorValues, dataTypeId: dataTypeId, ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes, show: true, submit: function (model) { tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); vm.linkPickerOverlay.show = false; vm.linkPickerOverlay = null; } }; }); } function openMediaPicker(editor, currentTarget, userData) { var startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; var startNodeIsVirtual = userData.startMediaIds.length !== 1; if ($scope.model.config.ignoreUserStartNodes === '1') { startNodeId = -1; startNodeIsVirtual = true; } vm.mediaPickerOverlay = { currentTarget: currentTarget, onlyImages: true, showDetails: true, startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, dataTypeId: dataTypeId, view: 'mediapicker', show: true, submit: function (model) { tinyMceService.insertMediaInEditor(editor, model.selectedImages[0]); vm.mediaPickerOverlay.show = false; vm.mediaPickerOverlay = null; } }; } function openEmbed(editor) { vm.embedOverlay = { view: 'embed', show: true, submit: function (model) { tinyMceService.insertEmbeddedMediaInEditor(editor, model.embed.preview); vm.embedOverlay.show = false; vm.embedOverlay = null; } }; } function openMacroPicker(editor, dialogData) { vm.macroPickerOverlay = { view: 'macropicker', dialogData: dialogData, show: true, submit: function (model) { var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); tinyMceService.insertMacroInEditor(editor, macroObject, $scope); vm.macroPickerOverlay.show = false; vm.macroPickerOverlay = null; } }; } } angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.RichTextEditorController', GridRichTextEditorController); }()); angular.module('umbraco').controller('Umbraco.PropertyEditors.Grid.TextStringController', function ($scope, $rootScope, $timeout, dialogService) { }); angular.module('umbraco').controller('Umbraco.PropertyEditors.GridController', function ($scope, $http, assetsService, localizationService, $rootScope, dialogService, gridService, mediaResource, imageHelper, $timeout, umbRequestHelper, angularHelper, $element, eventsService) { // Grid status variables var placeHolder = ''; var currentForm = angularHelper.getCurrentForm($scope); $scope.currentRow = null; $scope.currentCell = null; $scope.currentToolsControl = null; $scope.currentControl = null; $scope.openRTEToolbarId = null; $scope.hasSettings = false; $scope.showRowConfigurations = true; $scope.sortMode = false; $scope.reorderKey = 'general_reorder'; // ********************************************* // Sortable options // ********************************************* var draggedRteSettings; $scope.sortableOptionsRow = { distance: 10, cursor: 'move', placeholder: 'ui-sortable-placeholder', handle: '.umb-row-title-bar', helper: 'clone', forcePlaceholderSize: true, tolerance: 'pointer', zIndex: 1000000000000000000, scrollSensitivity: 100, cursorAt: { top: 40, left: 60 }, sort: function (event, ui) { /* prevent vertical scroll out of the screen */ var max = $('.umb-grid').width() - 150; if (parseInt(ui.helper.css('left')) > max) { ui.helper.css({ 'left': max + 'px' }); } if (parseInt(ui.helper.css('left')) < 20) { ui.helper.css({ 'left': 20 }); } }, start: function (e, ui) { // Fade out row when sorting ui.item.context.style.display = 'block'; ui.item.context.style.opacity = '0.5'; draggedRteSettings = {}; ui.item.find('.mceNoEditor').each(function () { // remove all RTEs in the dragged row and save their settings var id = $(this).attr('id'); draggedRteSettings[id] = _.findWhere(tinyMCE.editors, { id: id }).settings; // tinyMCE.execCommand("mceRemoveEditor", false, id); }); }, stop: function (e, ui) { // Fade in row when sorting stops ui.item.context.style.opacity = '1'; // reset all RTEs affected by the dragging ui.item.parents('.umb-column').find('.mceNoEditor').each(function () { var id = $(this).attr('id'); draggedRteSettings[id] = draggedRteSettings[id] || _.findWhere(tinyMCE.editors, { id: id }).settings; tinyMCE.execCommand('mceRemoveEditor', false, id); tinyMCE.init(draggedRteSettings[id]); }); currentForm.$setDirty(); } }; var notIncludedRte = []; var cancelMove = false; var startingArea; $scope.sortableOptionsCell = { distance: 10, cursor: 'move', placeholder: 'ui-sortable-placeholder', handle: '.umb-control-handle', helper: 'clone', connectWith: '.umb-cell-inner', forcePlaceholderSize: true, tolerance: 'pointer', zIndex: 1000000000000000000, scrollSensitivity: 100, cursorAt: { top: 45, left: 90 }, sort: function (event, ui) { /* prevent vertical scroll out of the screen */ var position = parseInt(ui.item.parent().offset().left) + parseInt(ui.helper.css('left')) - parseInt($('.umb-grid').offset().left); var max = $('.umb-grid').width() - 220; if (position > max) { ui.helper.css({ 'left': max - parseInt(ui.item.parent().offset().left) + parseInt($('.umb-grid').offset().left) + 'px' }); } if (position < 0) { ui.helper.css({ 'left': 0 - parseInt(ui.item.parent().offset().left) + parseInt($('.umb-grid').offset().left) + 'px' }); } }, over: function (event, ui) { var area = $(event.target).scope().area; var allowedEditors = area.allowed; if ($.inArray(ui.item.scope().control.editor.alias, allowedEditors) < 0 && allowedEditors || startingArea != area && area.maxItems != '' && area.maxItems > 0 && area.maxItems < area.controls.length + 1) { $scope.$apply(function () { $(event.target).scope().area.dropNotAllowed = true; }); ui.placeholder.hide(); cancelMove = true; } else { if ($(event.target).scope().area.controls.length == 0) { $scope.$apply(function () { $(event.target).scope().area.dropOnEmpty = true; }); ui.placeholder.hide(); } else { ui.placeholder.show(); } cancelMove = false; } }, out: function (event, ui) { $scope.$apply(function () { $(event.target).scope().area.dropNotAllowed = false; $(event.target).scope().area.dropOnEmpty = false; }); }, update: function (event, ui) { /* add all RTEs which are affected by the dragging */ if (!ui.sender) { if (cancelMove) { ui.item.sortable.cancel(); } ui.item.parents('.umb-cell.content').find('.mceNoEditor').each(function () { if ($.inArray($(this).attr('id'), notIncludedRte) < 0) { notIncludedRte.splice(0, 0, $(this).attr('id')); } }); } else { $(event.target).find('.mceNoEditor').each(function () { if ($.inArray($(this).attr('id'), notIncludedRte) < 0) { notIncludedRte.splice(0, 0, $(this).attr('id')); } }); } currentForm.$setDirty(); }, start: function (e, ui) { //Get the starting area for reference var area = $(e.target).scope().area; startingArea = area; // fade out control when sorting ui.item.context.style.display = 'block'; ui.item.context.style.opacity = '0.5'; // reset dragged RTE settings in case a RTE isn't dragged draggedRteSettings = undefined; ui.item.context.style.display = 'block'; ui.item.find('.mceNoEditor').each(function () { notIncludedRte = []; var editors = _.findWhere(tinyMCE.editors, { id: $(this).attr('id') }); // save the dragged RTE settings if (editors) { draggedRteSettings = editors.settings; // remove the dragged RTE tinyMCE.execCommand('mceRemoveEditor', false, $(this).attr('id')); } }); }, stop: function (e, ui) { // Fade in control when sorting stops ui.item.context.style.opacity = '1'; ui.item.offsetParent().find('.mceNoEditor').each(function () { if ($.inArray($(this).attr('id'), notIncludedRte) < 0) { // add all dragged's neighbouring RTEs in the new cell notIncludedRte.splice(0, 0, $(this).attr('id')); } }); $timeout(function () { // reconstruct the dragged RTE (could be undefined when dragging something else than RTE) if (draggedRteSettings !== undefined) { tinyMCE.init(draggedRteSettings); } _.forEach(notIncludedRte, function (id) { // reset all the other RTEs if (draggedRteSettings === undefined || id !== draggedRteSettings.id) { var rteSettings = _.findWhere(tinyMCE.editors, { id: id }).settings; tinyMCE.execCommand('mceRemoveEditor', false, id); tinyMCE.init(rteSettings); } }); }, 500, false); $scope.$apply(function () { var cell = $(e.target).scope().area; cell.hasActiveChild = hasActiveChild(cell, cell.controls); cell.active = false; }); } }; $scope.toggleSortMode = function () { $scope.sortMode = !$scope.sortMode; if ($scope.sortMode) { $scope.reorderKey = 'general_reorderDone'; } else { $scope.reorderKey = 'general_reorder'; } }; $scope.showReorderButton = function () { if ($scope.model.value && $scope.model.value.sections) { for (var i = 0; $scope.model.value.sections.length > i; i++) { var section = $scope.model.value.sections[i]; if (section.rows && section.rows.length > 0) { return true; } } } }; // ********************************************* // Add items overlay menu // ********************************************* $scope.openEditorOverlay = function (event, area, index, key) { $scope.editorOverlay = { view: 'itempicker', filter: area.$allowedEditors.length > 15, title: localizationService.localize('grid_insertControl'), availableItems: area.$allowedEditors, event: event, show: true, submit: function (model) { if (model.selectedItem) { $scope.addControl(model.selectedItem, area, index); $scope.editorOverlay.show = false; $scope.editorOverlay = null; } } }; }; // ********************************************* // Template management functions // ********************************************* $scope.addTemplate = function (template) { $scope.model.value = angular.copy(template); //default row data _.forEach($scope.model.value.sections, function (section) { $scope.initSection(section); }); }; // ********************************************* // Row management function // ********************************************* $scope.clickRow = function (index, rows) { rows[index].active = true; }; $scope.clickOutsideRow = function (index, rows) { rows[index].active = false; }; function getAllowedLayouts(section) { var layouts = $scope.model.config.items.layouts; //This will occur if it is a new section which has been // created from a 'template' if (section.allowed && section.allowed.length > 0) { return _.filter(layouts, function (layout) { return _.indexOf(section.allowed, layout.name) >= 0; }); } else { return layouts; } } $scope.addRow = function (section, layout, isInit) { //copy the selected layout into the rows collection var row = angular.copy(layout); // Init row value row = $scope.initRow(row); // Push the new row if (row) { section.rows.push(row); } if (!isInit) { currentForm.$setDirty(); } $scope.showRowConfigurations = false; eventsService.emit('grid.rowAdded', { scope: $scope, element: $element, row: row }); }; $scope.removeRow = function (section, $index) { if (section.rows.length > 0) { section.rows.splice($index, 1); $scope.currentRow = null; $scope.openRTEToolbarId = null; currentForm.$setDirty(); } if (section.rows.length === 0) { $scope.showRowConfigurations = true; } }; var shouldApply = function (item, itemType, gridItem) { if (item.applyTo === undefined || item.applyTo === null || item.applyTo === '') { return true; } if (typeof item.applyTo === 'string') { return item.applyTo === itemType; } if (itemType === 'row') { if (item.applyTo.row === undefined) { return false; } if (item.applyTo.row === null || item.applyTo.row === '') { return true; } var rows = item.applyTo.row.split(','); return _.indexOf(rows, gridItem.name) !== -1; } else if (itemType === 'cell') { if (item.applyTo.cell === undefined) { return false; } if (item.applyTo.cell === null || item.applyTo.cell === '') { return true; } var cells = item.applyTo.cell.split(','); var cellSize = gridItem.grid.toString(); return _.indexOf(cells, cellSize) !== -1; } }; $scope.editGridItemSettings = function (gridItem, itemType) { placeHolder = '{0}'; var styles, config; if (itemType === 'control') { styles = null; config = angular.copy(gridItem.editor.config.settings); } else { styles = _.filter(angular.copy($scope.model.config.items.styles), function (item) { return shouldApply(item, itemType, gridItem); }); config = _.filter(angular.copy($scope.model.config.items.config), function (item) { return shouldApply(item, itemType, gridItem); }); } if (angular.isObject(gridItem.config)) { _.each(config, function (cfg) { var val = gridItem.config[cfg.key]; if (val) { cfg.value = stripModifier(val, cfg.modifier); } }); } if (angular.isObject(gridItem.styles)) { _.each(styles, function (style) { var val = gridItem.styles[style.key]; if (val) { style.value = stripModifier(val, style.modifier); } }); } $scope.gridItemSettingsDialog = {}; $scope.gridItemSettingsDialog.view = 'views/propertyeditors/grid/dialogs/config.html'; $scope.gridItemSettingsDialog.title = 'Settings'; $scope.gridItemSettingsDialog.styles = styles; $scope.gridItemSettingsDialog.config = config; $scope.gridItemSettingsDialog.show = true; $scope.gridItemSettingsDialog.submit = function (model) { var styleObject = {}; var configObject = {}; _.each(model.styles, function (style) { if (style.value) { styleObject[style.key] = addModifier(style.value, style.modifier); } }); _.each(model.config, function (cfg) { if (cfg.value) { configObject[cfg.key] = addModifier(cfg.value, cfg.modifier); } }); gridItem.styles = styleObject; gridItem.config = configObject; gridItem.hasConfig = gridItemHasConfig(styleObject, configObject); currentForm.$setDirty(); $scope.gridItemSettingsDialog.show = false; $scope.gridItemSettingsDialog = null; }; $scope.gridItemSettingsDialog.close = function (oldModel) { $scope.gridItemSettingsDialog.show = false; $scope.gridItemSettingsDialog = null; }; }; function stripModifier(val, modifier) { if (!val || !modifier || modifier.indexOf(placeHolder) < 0) { return val; } else { var paddArray = modifier.split(placeHolder); if (paddArray.length == 1) { if (modifier.indexOf(placeHolder) === 0) { return val.slice(0, -paddArray[0].length); } else { return val.slice(paddArray[0].length, 0); } } else { if (paddArray[1].length === 0) { return val.slice(paddArray[0].length); } return val.slice(paddArray[0].length, -paddArray[1].length); } } } var addModifier = function (val, modifier) { if (!modifier || modifier.indexOf(placeHolder) < 0) { return val; } else { return modifier.replace(placeHolder, val); } }; function gridItemHasConfig(styles, config) { if (_.isEmpty(styles) && _.isEmpty(config)) { return false; } else { return true; } } // ********************************************* // Area management functions // ********************************************* $scope.clickCell = function (index, cells, row) { cells[index].active = true; row.hasActiveChild = true; }; $scope.clickOutsideCell = function (index, cells, row) { cells[index].active = false; row.hasActiveChild = hasActiveChild(row, cells); }; $scope.cellPreview = function (cell) { if (cell && cell.$allowedEditors) { var editor = cell.$allowedEditors[0]; return editor.icon; } else { return 'icon-layout'; } }; // ********************************************* // Control management functions // ********************************************* $scope.clickControl = function (index, controls, cell) { controls[index].active = true; cell.hasActiveChild = true; }; $scope.clickOutsideControl = function (index, controls, cell) { controls[index].active = false; cell.hasActiveChild = hasActiveChild(cell, controls); }; function hasActiveChild(item, children) { var activeChild = false; for (var i = 0; children.length > i; i++) { var child = children[i]; if (child.active) { activeChild = true; } } if (activeChild) { return true; } } var guid = function () { function s4() { return Math.floor((1 + Math.random()) * 65536).toString(16).substring(1); } return function () { return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); }; }(); $scope.setUniqueId = function (cell, index) { return guid(); }; $scope.addControl = function (editor, cell, index, initialize) { initialize = initialize !== false; var newControl = { value: null, editor: editor, $initializing: initialize }; if (index === undefined) { index = cell.controls.length; } newControl.active = true; //populate control $scope.initControl(newControl, index + 1); cell.controls.push(newControl); eventsService.emit('grid.itemAdded', { scope: $scope, element: $element, cell: cell, item: newControl }); }; $scope.addTinyMce = function (cell) { var rte = $scope.getEditor('rte'); $scope.addControl(rte, cell); }; $scope.getEditor = function (alias) { return _.find($scope.availableEditors, function (editor) { return editor.alias === alias; }); }; $scope.removeControl = function (cell, $index) { $scope.currentControl = null; cell.controls.splice($index, 1); }; $scope.percentage = function (spans) { return (spans / $scope.model.config.items.columns * 100).toFixed(8); }; $scope.clearPrompt = function (scopedObject, e) { scopedObject.deletePrompt = false; e.preventDefault(); e.stopPropagation(); }; $scope.togglePrompt = function (scopedObject) { scopedObject.deletePrompt = !scopedObject.deletePrompt; }; $scope.hidePrompt = function (scopedObject) { scopedObject.deletePrompt = false; }; $scope.toggleAddRow = function () { $scope.showRowConfigurations = !$scope.showRowConfigurations; }; // ********************************************* // Initialization // these methods are called from ng-init on the template // so we can controll their first load data // // intialization sets non-saved data like percentage sizing, allowed editors and // other data that should all be pre-fixed with $ to strip it out on save // ********************************************* // ********************************************* // Init template + sections // ********************************************* $scope.initContent = function () { var clear = true; //settings indicator shortcut if ($scope.model.config.items.config && $scope.model.config.items.config.length > 0 || $scope.model.config.items.styles && $scope.model.config.items.styles.length > 0) { $scope.hasSettings = true; } //ensure the grid has a column value set, //if nothing is found, set it to 12 if (!$scope.model.config.items.columns) { $scope.model.config.items.columns = 12; } else if (angular.isString($scope.model.config.items.columns)) { $scope.model.config.items.columns = parseInt($scope.model.config.items.columns); } if ($scope.model.value && $scope.model.value.sections && $scope.model.value.sections.length > 0 && $scope.model.value.sections[0].rows && $scope.model.value.sections[0].rows.length > 0) { if ($scope.model.value.name && angular.isArray($scope.model.config.items.templates)) { //This will occur if it is an existing value, in which case // we need to determine which layout was applied by looking up // the name // TODO: We need to change this to an immutable ID!! var found = _.find($scope.model.config.items.templates, function (t) { return t.name === $scope.model.value.name; }); if (found && angular.isArray(found.sections) && found.sections.length === $scope.model.value.sections.length) { //Cool, we've found the template associated with our current value with matching sections counts, now we need to // merge this template data on to our current value (as if it was new) so that we can preserve what is and isn't // allowed for this template based on the current config. _.each(found.sections, function (templateSection, index) { angular.extend($scope.model.value.sections[index], angular.copy(templateSection)); }); } } _.forEach($scope.model.value.sections, function (section, index) { if (section.grid > 0) { $scope.initSection(section); //we do this to ensure that the grid can be reset by deleting the last row if (section.rows.length > 0) { clear = false; } } else { $scope.model.value.sections.splice(index, 1); } }); } else if ($scope.model.config.items.templates && $scope.model.config.items.templates.length === 1) { $scope.addTemplate($scope.model.config.items.templates[0]); clear = false; } if (clear) { $scope.model.value = undefined; } }; $scope.initSection = function (section) { section.$percentage = $scope.percentage(section.grid); section.$allowedLayouts = getAllowedLayouts(section); if (!section.rows || section.rows.length === 0) { section.rows = []; if (section.$allowedLayouts.length === 1) { $scope.addRow(section, section.$allowedLayouts[0], true); } } else { _.forEach(section.rows, function (row, index) { if (!row.$initialized) { var initd = $scope.initRow(row); //if init fails, remove if (!initd) { section.rows.splice(index, 1); } else { section.rows[index] = initd; } } }); // if there is more than one row added - hide row add tools $scope.showRowConfigurations = false; } }; // ********************************************* // Init layout / row // ********************************************* $scope.initRow = function (row) { //merge the layout data with the original config data //if there are no config info on this, splice it out var original = _.find($scope.model.config.items.layouts, function (o) { return o.name === row.name; }); if (!original) { return null; } else { //make a copy to not touch the original config original = angular.copy(original); original.styles = row.styles; original.config = row.config; original.hasConfig = gridItemHasConfig(row.styles, row.config); //sync area configuration _.each(original.areas, function (area, areaIndex) { if (area.grid > 0) { var currentArea = row.areas[areaIndex]; if (currentArea) { area.config = currentArea.config; area.styles = currentArea.styles; area.hasConfig = gridItemHasConfig(currentArea.styles, currentArea.config); } //set editor permissions if (!area.allowed || area.allowAll === true) { area.$allowedEditors = $scope.availableEditors; area.$allowsRTE = true; } else { area.$allowedEditors = _.filter($scope.availableEditors, function (editor) { return _.indexOf(area.allowed, editor.alias) >= 0; }); if (_.indexOf(area.allowed, 'rte') >= 0) { area.$allowsRTE = true; } } //copy over existing controls into the new areas if (row.areas.length > areaIndex && row.areas[areaIndex].controls) { area.controls = currentArea.controls; _.forEach(area.controls, function (control, controlIndex) { $scope.initControl(control, controlIndex); }); } else { //if empty area.controls = []; //if only one allowed editor if (area.$allowedEditors.length === 1) { $scope.addControl(area.$allowedEditors[0], area, 0, false); } } //set width area.$percentage = $scope.percentage(area.grid); area.$uniqueId = $scope.setUniqueId(); } else { original.areas.splice(areaIndex, 1); } }); //replace the old row original.$initialized = true; //set a disposable unique ID original.$uniqueId = $scope.setUniqueId(); //set a no disposable unique ID (util for row styling) original.id = !row.id ? $scope.setUniqueId() : row.id; return original; } }; // ********************************************* // Init control // ********************************************* $scope.initControl = function (control, index) { control.$index = index; control.$uniqueId = $scope.setUniqueId(); //error handling in case of missing editor.. //should only happen if stripped earlier if (!control.editor) { control.$editorPath = 'views/propertyeditors/grid/editors/error.html'; } if (!control.$editorPath) { var editorConfig = $scope.getEditor(control.editor.alias); if (editorConfig) { control.editor = editorConfig; //if its an absolute path if (control.editor.view.startsWith('/') || control.editor.view.startsWith('~/')) { control.$editorPath = umbRequestHelper.convertVirtualToAbsolutePath(control.editor.view); } else { //use convention control.$editorPath = 'views/propertyeditors/grid/editors/' + control.editor.view + '.html'; } } else { control.$editorPath = 'views/propertyeditors/grid/editors/error.html'; } } }; gridService.getGridEditors().then(function (response) { $scope.availableEditors = response.data; //Localize the grid editor names angular.forEach($scope.availableEditors, function (value, key) { //If no translation is provided, keep using the editor name from the manifest if (localizationService.dictionary.hasOwnProperty('grid_' + value.alias)) { value.name = localizationService.localize('grid_' + value.alias); } }); $scope.contentReady = true; // ********************************************* // Init grid // ********************************************* eventsService.emit('grid.initializing', { scope: $scope, element: $element }); $scope.initContent(); eventsService.emit('grid.initialized', { scope: $scope, element: $element }); }); //Clean the grid value before submitting to the server, we don't need // all of that grid configuration in the value to be stored!! All of that // needs to be merged in at runtime to ensure that the real config values are used // if they are ever updated. var unsubscribe = $scope.$on('formSubmitting', function () { if ($scope.model.value && $scope.model.value.sections) { _.each($scope.model.value.sections, function (section) { if (section.rows) { _.each(section.rows, function (row) { if (row.areas) { _.each(row.areas, function (area) { //Remove the 'editors' - these are the allowed editors, these will // be injected at runtime to this editor, it should not be persisted if (area.editors) { delete area.editors; } if (area.controls) { _.each(area.controls, function (control) { if (control.editor) { //replace var alias = control.editor.alias; control.editor = { alias: alias }; } }); } }); } }); } }); } }); //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); }); angular.module('umbraco').controller('Umbraco.PropertyEditors.GridPrevalueEditorController', function ($scope, $http, assetsService, $rootScope, dialogService, mediaResource, gridService, imageHelper, $timeout) { var emptyModel = { styles: [{ label: 'Set a background image', description: 'Set a row background', key: 'background-image', view: 'imagepicker', modifier: 'url({0})' }], config: [{ label: 'Class', description: 'Set a css class', key: 'class', view: 'textstring' }], columns: 12, templates: [ { name: '1 column layout', sections: [{ grid: 12 }] }, { name: '2 column layout', sections: [ { grid: 4 }, { grid: 8 } ] } ], layouts: [ { label: 'Headline', name: 'Headline', areas: [{ grid: 12, editors: ['headline'] }] }, { label: 'Article', name: 'Article', areas: [ { grid: 4 }, { grid: 8 } ] } ] }; /**************** template *****************/ $scope.configureTemplate = function (template) { var templatesCopy = angular.copy($scope.model.value.templates); if (template === undefined) { template = { name: '', sections: [] }; $scope.model.value.templates.push(template); } $scope.layoutConfigOverlay = {}; $scope.layoutConfigOverlay.view = 'views/propertyEditors/grid/dialogs/layoutconfig.html'; $scope.layoutConfigOverlay.currentLayout = template; $scope.layoutConfigOverlay.rows = $scope.model.value.layouts; $scope.layoutConfigOverlay.columns = $scope.model.value.columns; $scope.layoutConfigOverlay.show = true; $scope.layoutConfigOverlay.submit = function (model) { $scope.layoutConfigOverlay.show = false; $scope.layoutConfigOverlay = null; }; $scope.layoutConfigOverlay.close = function (oldModel) { //reset templates $scope.model.value.templates = templatesCopy; $scope.layoutConfigOverlay.show = false; $scope.layoutConfigOverlay = null; }; }; $scope.deleteTemplate = function (index) { $scope.model.value.templates.splice(index, 1); }; /**************** Row *****************/ $scope.configureLayout = function (layout) { var layoutsCopy = angular.copy($scope.model.value.layouts); if (layout === undefined) { layout = { name: '', areas: [] }; $scope.model.value.layouts.push(layout); } $scope.rowConfigOverlay = {}; $scope.rowConfigOverlay.view = 'views/propertyEditors/grid/dialogs/rowconfig.html'; $scope.rowConfigOverlay.currentRow = layout; $scope.rowConfigOverlay.editors = $scope.editors; $scope.rowConfigOverlay.columns = $scope.model.value.columns; $scope.rowConfigOverlay.show = true; $scope.rowConfigOverlay.submit = function (model) { $scope.rowConfigOverlay.show = false; $scope.rowConfigOverlay = null; }; $scope.rowConfigOverlay.close = function (oldModel) { $scope.model.value.layouts = layoutsCopy; $scope.rowConfigOverlay.show = false; $scope.rowConfigOverlay = null; }; }; //var rowDeletesPending = false; $scope.deleteLayout = function (index) { $scope.rowDeleteOverlay = {}; $scope.rowDeleteOverlay.view = 'views/propertyEditors/grid/dialogs/rowdeleteconfirm.html'; $scope.rowDeleteOverlay.dialogData = { rowName: $scope.model.value.layouts[index].name }; $scope.rowDeleteOverlay.show = true; $scope.rowDeleteOverlay.submit = function (model) { $scope.model.value.layouts.splice(index, 1); $scope.rowDeleteOverlay.show = false; $scope.rowDeleteOverlay = null; }; $scope.rowDeleteOverlay.close = function (oldModel) { $scope.rowDeleteOverlay.show = false; $scope.rowDeleteOverlay = null; }; }; /**************** utillities *****************/ $scope.toggleCollection = function (collection, toggle) { if (toggle) { collection = []; } else { delete collection; } }; $scope.percentage = function (spans) { return (spans / $scope.model.value.columns * 100).toFixed(8); }; $scope.zeroWidthFilter = function (cell) { return cell.grid > 0; }; /**************** Config *****************/ $scope.removeConfigValue = function (collection, index) { collection.splice(index, 1); }; var editConfigCollection = function (configValues, title, callback) { $scope.editConfigCollectionOverlay = {}; $scope.editConfigCollectionOverlay.view = 'views/propertyeditors/grid/dialogs/editconfig.html'; $scope.editConfigCollectionOverlay.config = configValues; $scope.editConfigCollectionOverlay.title = title; $scope.editConfigCollectionOverlay.show = true; $scope.editConfigCollectionOverlay.submit = function (model) { callback(model.config); $scope.editConfigCollectionOverlay.show = false; $scope.editConfigCollectionOverlay = null; }; $scope.editConfigCollectionOverlay.close = function (oldModel) { $scope.editConfigCollectionOverlay.show = false; $scope.editConfigCollectionOverlay = null; }; }; $scope.editConfig = function () { editConfigCollection($scope.model.value.config, 'Settings', function (data) { $scope.model.value.config = data; }); }; $scope.editStyles = function () { editConfigCollection($scope.model.value.styles, 'Styling', function (data) { $scope.model.value.styles = data; }); }; /**************** editors *****************/ gridService.getGridEditors().then(function (response) { $scope.editors = response.data; }); /* init grid data */ if (!$scope.model.value || $scope.model.value === '' || !$scope.model.value.templates) { $scope.model.value = emptyModel; } else { if (!$scope.model.value.columns) { $scope.model.value.columns = emptyModel.columns; } if (!$scope.model.value.config) { $scope.model.value.config = []; } if (!$scope.model.value.styles) { $scope.model.value.styles = []; } } /**************** Clean up *****************/ var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { var ts = $scope.model.value.templates; var ls = $scope.model.value.layouts; _.each(ts, function (t) { _.each(t.sections, function (section, index) { if (section.grid === 0) { t.sections.splice(index, 1); } }); }); _.each(ls, function (l) { _.each(l.areas, function (area, index) { if (area.grid === 0) { l.areas.splice(index, 1); } }); }); }); //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); }); /** * @ngdoc controller * @name Umbraco.Editors.IdWithGuidValueController * @function * * @description * The controller for the idwithguid property editor, which formats the ID as normal * with the GUID in smaller text below, as used across the backoffice. */ function IdWithGuidValueController($rootScope, $scope, $filter) { function formatDisplayValue() { if ($scope.model.value.length > 1) { $scope.displayid = $scope.model.value[0]; $scope.displayguid = $scope.model.value[1]; } else { $scope.displayid = $scope.model.value; } } //format the display value on init: formatDisplayValue(); } angular.module('umbraco').controller('Umbraco.PropertyEditors.IdWithGuidValueController', IdWithGuidValueController); //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller('Umbraco.PropertyEditors.ImageCropperController', function ($rootScope, $routeParams, $scope, $log, mediaHelper, cropperHelper, $timeout, editorState, umbRequestHelper, fileManager, angularHelper) { var config = angular.copy($scope.model.config); $scope.imageIsLoaded = false; //move previously saved value to the editor if ($scope.model.value) { //backwards compat with the old file upload (incase some-one swaps them..) if (angular.isString($scope.model.value)) { config.src = $scope.model.value; $scope.model.value = config; } else if ($scope.model.value.crops) { //sync any config changes with the editor and drop outdated crops _.each($scope.model.value.crops, function (saved) { var configured = _.find(config.crops, function (item) { return item.alias === saved.alias; }); if (configured && configured.height === saved.height && configured.width === saved.width) { configured.coordinates = saved.coordinates; } }); $scope.model.value.crops = config.crops; //restore focalpoint if missing if (!$scope.model.value.focalPoint) { $scope.model.value.focalPoint = { left: 0.5, top: 0.5 }; } } $scope.imageSrc = $scope.model.value.src; } //crop a specific crop $scope.crop = function (crop) { // clone the crop so we can discard the changes $scope.currentCrop = angular.copy(crop); $scope.currentPoint = undefined; }; //done cropping $scope.done = function () { if (!$scope.currentCrop) { return; } // find the original crop by crop alias and update its coordinates var editedCrop = _.find($scope.model.value.crops, function (crop) { return crop.alias === $scope.currentCrop.alias; }); editedCrop.coordinates = $scope.currentCrop.coordinates; $scope.close(); angularHelper.getCurrentForm($scope).$setDirty(); }; //reset the current crop $scope.reset = function () { $scope.currentCrop.coordinates = undefined; $scope.done(); }; //close crop overlay $scope.close = function (crop) { $scope.currentCrop = undefined; $scope.currentPoint = undefined; }; //crop a specific crop $scope.clear = function (crop) { //clear current uploaded files fileManager.setFiles($scope.model.alias, []); //clear the ui $scope.imageSrc = undefined; if ($scope.model.value) { delete $scope.model.value; } // set form to dirty to tricker discard changes dialog var currForm = angularHelper.getCurrentForm($scope); currForm.$setDirty(); }; //show previews $scope.togglePreviews = function () { if ($scope.showPreviews) { $scope.showPreviews = false; $scope.tempShowPreviews = false; } else { $scope.showPreviews = true; } }; $scope.imageLoaded = function (isCroppable, hasDimensions) { $scope.imageIsLoaded = true; $scope.isCroppable = isCroppable; $scope.hasDimensions = hasDimensions; }; $scope.focalPointChanged = function () { angularHelper.getCurrentForm($scope).$setDirty(); }; //on image selected, update the cropper $scope.$on('filesSelected', function (ev, args) { $scope.model.value = config; if (args.files && args.files[0]) { fileManager.setFiles($scope.model.alias, args.files); var reader = new FileReader(); reader.onload = function (e) { $scope.$apply(function () { $scope.imageSrc = e.target.result; }); }; reader.readAsDataURL(args.files[0]); } }); //here we declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = function (newVal, oldVal) { //clear current uploaded files fileManager.setFiles($scope.model.alias, []); }; var unsubscribe = $scope.$on('formSubmitting', function () { $scope.done(); }); $scope.$on('$destroy', function () { unsubscribe(); }); }).run(function (mediaHelper, umbRequestHelper) { if (mediaHelper && mediaHelper.registerFileResolver) { //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource // they contain different data structures so if we need to query against it we need to be aware of this. mediaHelper.registerFileResolver('Umbraco.ImageCropper', function (property, entity, thumbnail) { if (property.value && property.value.src) { if (thumbnail === true) { return property.value.src + '?width=500&mode=max&animationprocessmode=first'; } else { return property.value.src; } //this is a fallback in case the cropper has been asssigned a upload field } else if (angular.isString(property.value)) { if (thumbnail) { if (mediaHelper.detectIfImageByExtension(property.value)) { var thumbnailUrl = umbRequestHelper.getApiUrl('imagesApiBaseUrl', 'GetBigThumbnail', [{ originalImagePath: property.value }]); return thumbnailUrl; } else { return null; } } else { return property.value; } } return null; }); } }); angular.module('umbraco').controller('Umbraco.PrevalueEditors.CropSizesController', function ($scope) { if (!$scope.model.value) { $scope.model.value = []; } $scope.editMode = false; $scope.setFocus = false; $scope.remove = function (item, evt) { evt.preventDefault(); $scope.model.value = _.reject($scope.model.value, function (x) { return x.alias === item.alias; }); }; $scope.edit = function (item, evt) { evt.preventDefault(); $scope.editMode = true; $scope.setFocus = false; $scope.newItem = item; }; $scope.cancel = function (evt) { evt.preventDefault(); $scope.editMode = false; $scope.setFocus = true; $scope.newItem = null; }; $scope.change = function () { // Listen to the change event and set focus 2 false if ($scope.setFocus) { $scope.setFocus = false; return; } }; $scope.add = function (evt) { evt.preventDefault(); $scope.editMode = false; $scope.setFocus = true; if ($scope.newItem && $scope.newItem.alias && angular.isNumber($scope.newItem.width) && angular.isNumber($scope.newItem.height) && $scope.newItem.width > 0 && $scope.newItem.height > 0) { var exists = _.find($scope.model.value, function (item) { return $scope.newItem.alias === item.alias; }); if (!exists) { $scope.model.value.push($scope.newItem); $scope.newItem = {}; $scope.hasError = false; $scope.cropAdded = false; return; } else { $scope.newItem = null; $scope.hasError = false; return; } } //there was an error, do the highlight (will be set back by the directive) $scope.hasError = true; }; $scope.sortableOptions = { axis: 'y' }; }); function includePropsPreValsController($rootScope, $scope, localizationService, contentTypeResource) { if (!$scope.model.value) { $scope.model.value = []; } $scope.hasError = false; $scope.errorMsg = ''; $scope.propertyAliases = []; $scope.selectedField = null; $scope.systemFields = [ { value: 'sortOrder' }, { value: 'updateDate' }, { value: 'updater' }, { value: 'createDate' }, { value: 'owner' }, { value: 'published' }, { value: 'contentTypeAlias' }, { value: 'email' }, { value: 'username' } ]; $scope.getLocalizedKey = function (alias) { switch (alias) { case 'name': return 'general_name'; case 'sortOrder': return 'general_sort'; case 'updateDate': return 'content_updateDate'; case 'updater': return 'content_updatedBy'; case 'createDate': return 'content_createDate'; case 'owner': return 'content_createBy'; case 'published': return 'content_isPublished'; case 'contentTypeAlias': //NOTE: This will just be 'Document' type even if it's for media/members since this is just a pre-val editor and we don't have a key for 'Content Type Alias' return 'content_documentType'; case 'email': return 'general_email'; case 'username': return 'general_username'; } return alias; }; $scope.changeField = function () { $scope.hasError = false; $scope.errorMsg = ''; }; $scope.removeField = function (e) { $scope.model.value = _.reject($scope.model.value, function (x) { return x.alias === e.alias; }); }; //now we'll localize these strings, for some reason the directive doesn't work inside of the select group with an ng-model declared _.each($scope.systemFields, function (e, i) { var key = $scope.getLocalizedKey(e.value); localizationService.localize(key).then(function (v) { e.name = v; switch (e.value) { case 'updater': e.name += ' (Content only)'; break; case 'published': e.name += ' (Content only)'; break; case 'email': e.name += ' (Members only)'; break; case 'username': e.name += ' (Members only)'; break; } }); }); // Return a helper with preserved width of cells var fixHelper = function (e, ui) { ui.children().each(function () { $(this).width($(this).width()); }); var row = ui.clone(); row.css('background-color', 'lightgray'); return row; }; $scope.sortableOptions = { helper: fixHelper, handle: '.handle', opacity: 0.5, axis: 'y', containment: 'parent', cursor: 'move', items: '> tr', tolerance: 'pointer', forcePlaceholderSize: true, start: function (e, ui) { ui.placeholder.height(ui.item.height()); }, update: function (e, ui) { // Get the new and old index for the moved element (using the text as the identifier) var newIndex = ui.item.index(); var movedAlias = $('.alias-value', ui.item).text().trim(); var originalIndex = getAliasIndexByText(movedAlias); // Move the element in the model if (originalIndex > -1) { var movedElement = $scope.model.value[originalIndex]; $scope.model.value.splice(originalIndex, 1); $scope.model.value.splice(newIndex, 0, movedElement); } } }; contentTypeResource.getAllPropertyTypeAliases().then(function (data) { $scope.propertyAliases = data; }); $scope.addField = function () { var val = $scope.selectedField; if (val) { var isSystem = val.startsWith('_system_'); if (isSystem) { val = val.trimStart('_system_'); } var exists = _.find($scope.model.value, function (i) { return i.alias === val; }); if (!exists) { $scope.hasError = false; $scope.errorMsg = ''; $scope.model.value.push({ alias: val, isSystem: isSystem ? 1 : 0 }); } else { //there was an error, do the highlight (will be set back by the directive) $scope.hasError = true; $scope.errorMsg = 'Property is already added'; } } else { $scope.hasError = true; $scope.errorMsg = 'No property selected'; } }; function getAliasIndexByText(value) { for (var i = 0; i < $scope.model.value.length; i++) { if ($scope.model.value[i].alias === value) { return i; } } return -1; } } angular.module('umbraco').controller('Umbraco.PrevalueEditors.IncludePropertiesListViewController', includePropsPreValsController); /** * @ngdoc controller * @name Umbraco.PrevalueEditors.ListViewLayoutsPreValsController * @function * * @description * The controller for configuring layouts for list views */ (function () { 'use strict'; function ListViewLayoutsPreValsController($scope) { var vm = this; vm.focusLayoutName = false; vm.layoutsSortableOptions = { distance: 10, tolerance: 'pointer', opacity: 0.7, scroll: true, cursor: 'move', handle: '.list-view-layout__sort-handle' }; vm.addLayout = addLayout; vm.showPrompt = showPrompt; vm.hidePrompt = hidePrompt; vm.removeLayout = removeLayout; vm.openIconPicker = openIconPicker; function activate() { } function addLayout() { vm.focusLayoutName = false; var layout = { 'name': '', 'path': '', 'icon': 'icon-stop', 'selected': true }; $scope.model.value.push(layout); } function showPrompt(layout) { layout.deletePrompt = true; } function hidePrompt(layout) { layout.deletePrompt = false; } function removeLayout($index, layout) { $scope.model.value.splice($index, 1); } function openIconPicker(layout) { vm.iconPickerDialog = { view: 'iconpicker', show: true, submit: function (model) { if (model.color) { layout.icon = model.icon + ' ' + model.color; } else { layout.icon = model.icon; } vm.focusLayoutName = true; vm.iconPickerDialog.show = false; vm.iconPickerDialog = null; } }; } activate(); } angular.module('umbraco').controller('Umbraco.PrevalueEditors.ListViewLayoutsPreValsController', ListViewLayoutsPreValsController); }()); /** * @ngdoc controller * @name Umbraco.Editors.DocumentType.EditController * @function * * @description * The controller for the content type editor */ (function () { 'use strict'; function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource, $location, listViewHelper, mediaTypeHelper) { var vm = this; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; vm.nodeId = $scope.contentId; // Use whitelist of allowed file types if provided vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); if (vm.acceptedFileTypes === '') { // If not provided, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); } vm.maxFileSize = umbracoSettings.maxFileSize + 'KB'; vm.activeDrag = false; vm.mediaDetailsTooltip = {}; vm.itemsWithoutFolders = []; vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; vm.acceptedMediatypes = []; vm.dragEnter = dragEnter; vm.dragLeave = dragLeave; vm.onFilesQueue = onFilesQueue; vm.onUploadComplete = onUploadComplete; vm.hoverMediaItemDetails = hoverMediaItemDetails; vm.selectContentItem = selectContentItem; vm.selectItem = selectItem; vm.selectFolder = selectFolder; vm.goToItem = goToItem; function activate() { vm.itemsWithoutFolders = filterOutFolders($scope.items); //no need to make another REST/DB call if this data is not used when we are browsing the bin if ($scope.entityType === 'media' && !vm.isRecycleBin) { mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { vm.acceptedMediatypes = types; }); } } function filterOutFolders(items) { var newArray = []; if (items && items.length) { for (var i = 0; items.length > i; i++) { var item = items[i]; var isFolder = !mediaHelper.hasFilePropertyType(item); if (!isFolder) { newArray.push(item); } } } return newArray; } function dragEnter(el, event) { vm.activeDrag = true; } function dragLeave(el, event) { vm.activeDrag = false; } function onFilesQueue() { vm.activeDrag = false; } function onUploadComplete() { $scope.getContent($scope.contentId); } function hoverMediaItemDetails(item, event, hover) { if (hover && !vm.mediaDetailsTooltip.show) { vm.mediaDetailsTooltip.event = event; vm.mediaDetailsTooltip.item = item; vm.mediaDetailsTooltip.show = true; } else if (!hover && vm.mediaDetailsTooltip.show) { vm.mediaDetailsTooltip.show = false; } } function selectContentItem(item, $event, $index) { listViewHelper.selectHandler(item, $index, $scope.items, $scope.selection, $event); } function selectItem(item, $event, $index) { listViewHelper.selectHandler(item, $index, vm.itemsWithoutFolders, $scope.selection, $event); } function selectFolder(folder, $event, $index) { listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event); } function goToItem(item, $event, $index) { $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); } activate(); } angular.module('umbraco').controller('Umbraco.PropertyEditors.ListView.GridLayoutController', ListViewGridLayoutController); }()); (function () { 'use strict'; function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper, mediaTypeHelper) { var vm = this; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; vm.nodeId = $scope.contentId; // Use whitelist of allowed file types if provided vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); if (vm.acceptedFileTypes === '') { // If not provided, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); } vm.maxFileSize = umbracoSettings.maxFileSize + 'KB'; vm.activeDrag = false; vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; vm.acceptedMediatypes = []; vm.selectItem = selectItem; vm.clickItem = clickItem; vm.selectAll = selectAll; vm.isSelectedAll = isSelectedAll; vm.isSortDirection = isSortDirection; vm.sort = sort; vm.dragEnter = dragEnter; vm.dragLeave = dragLeave; vm.onFilesQueue = onFilesQueue; vm.onUploadComplete = onUploadComplete; markAsSensitive(); function activate() { if ($scope.entityType === 'media') { mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { vm.acceptedMediatypes = types; }); } } function selectAll($event) { listViewHelper.selectAllItems($scope.items, $scope.selection, $event); } function isSelectedAll() { return listViewHelper.isSelectedAll($scope.items, $scope.selection); } function selectItem(selectedItem, $index, $event) { listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); } function clickItem(item) { // if item.id is 2147483647 (int.MaxValue) use item.key $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + (item.id === 2147483647 ? item.key : item.id)); } function isSortDirection(col, direction) { return listViewHelper.setSortingDirection(col, direction, $scope.options); } function sort(field, allow, isSystem) { if (allow) { $scope.options.orderBySystemField = isSystem; listViewHelper.setSorting(field, allow, $scope.options); $scope.getContent($scope.contentId); } } // Dropzone upload functions function dragEnter(el, event) { vm.activeDrag = true; } function dragLeave(el, event) { vm.activeDrag = false; } function onFilesQueue() { vm.activeDrag = false; } function onUploadComplete() { $scope.getContent($scope.contentId); } function markAsSensitive() { angular.forEach($scope.options.includeProperties, function (option) { option.isSensitive = false; angular.forEach($scope.items, function (item) { angular.forEach(item.properties, function (property) { if (option.alias === property.alias) { option.isSensitive = property.isSensitive; } }); }); }); } activate(); } angular.module('umbraco').controller('Umbraco.PropertyEditors.ListView.ListLayoutController', ListViewListLayoutController); }()); function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService, navigationService, treeService, mediaHelper) { //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content // that isn't created yet, if we continue this will use the parent id in the route params which isn't what // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove // the list view tab entirely when it's new. if ($routeParams.create) { $scope.isNew = true; return; } //Now we need to check if this is for media, members or content because that will depend on the resources we use var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) if ($scope.model.config.entityType && $scope.model.config.entityType === 'member' || appState.getSectionState('currentSection') === 'member') { $scope.entityType = 'member'; contentResource = $injector.get('memberResource'); getContentTypesCallback = $injector.get('memberTypeResource').getTypes; getListResultsCallback = contentResource.getPagedResults; deleteItemCallback = contentResource.deleteByKey; getIdCallback = function (selected) { return selected.key; }; createEditUrlCallback = function (item) { return '/' + $scope.entityType + '/' + $scope.entityType + '/edit/' + item.key + '?page=' + $scope.options.pageNumber + '&listName=' + $scope.contentId; }; } else { //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) if ($scope.model.config.entityType && $scope.model.config.entityType === 'media' || appState.getSectionState('currentSection') === 'media') { $scope.entityType = 'media'; contentResource = $injector.get('mediaResource'); getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; } else { $scope.entityType = 'content'; contentResource = $injector.get('contentResource'); getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; } getListResultsCallback = contentResource.getChildren; deleteItemCallback = contentResource.deleteById; getIdCallback = function (selected) { return selected.id; }; createEditUrlCallback = function (item) { return '/' + $scope.entityType + '/' + $scope.entityType + '/edit/' + item.id + '?page=' + $scope.options.pageNumber; }; } $scope.pagination = []; $scope.isNew = false; $scope.actionInProgress = false; $scope.selection = []; $scope.folders = []; $scope.listViewResultSet = { totalPages: 0, items: [] }; $scope.createAllowedButtonSingle = false; $scope.createAllowedButtonSingleWithBlueprints = false; $scope.createAllowedButtonMultiWithBlueprints = false; //when this is null, we don't check permissions $scope.currentNodePermissions = null; if ($scope.entityType === 'content') { //Just ensure we do have an editorState if (editorState.current) { //Fetch current node allowed actions for the current user //This is the current node & not each individual child node in the list var currentUserPermissions = editorState.current.allowedActions; //Create a nicer model rather than the funky & hard to remember permissions strings $scope.currentNodePermissions = { 'canCopy': _.contains(currentUserPermissions, 'O'), //Magic Char = O 'canCreate': _.contains(currentUserPermissions, 'C'), //Magic Char = C 'canDelete': _.contains(currentUserPermissions, 'D'), //Magic Char = D 'canMove': _.contains(currentUserPermissions, 'M'), //Magic Char = M 'canPublish': _.contains(currentUserPermissions, 'U'), //Magic Char = U 'canUnpublish': _.contains(currentUserPermissions, 'U') //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) }; } } //when this is null, we don't check permissions $scope.buttonPermissions = null; //When we are dealing with 'content', we need to deal with permissions on child nodes. // Currently there is no real good way to if ($scope.entityType === 'content') { var idsWithPermissions = null; $scope.buttonPermissions = { canCopy: true, canCreate: true, canDelete: true, canMove: true, canPublish: true, canUnpublish: true }; $scope.$watch(function () { return $scope.selection.length; }, function (newVal, oldVal) { if (idsWithPermissions == null && newVal > 0 || idsWithPermissions != null) { //get all of the selected ids var ids = _.map($scope.selection, function (i) { return i.id.toString(); }); //remove the dictionary items that don't have matching ids var filtered = {}; _.each(idsWithPermissions, function (value, key, list) { if (_.contains(ids, key)) { filtered[key] = value; } }); idsWithPermissions = filtered; //find all ids that we haven't looked up permissions for var existingIds = _.keys(idsWithPermissions); var missingLookup = _.map(_.difference(ids, existingIds), function (i) { return Number(i); }); if (missingLookup.length > 0) { contentResource.getPermissions(missingLookup).then(function (p) { $scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions); }); } else { $scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions); } } }); } $scope.options = { displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, pageNumber: $routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0 ? $routeParams.page : 1, filter: '', orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : 'desc', orderBySystemField: true, includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, { alias: 'updater', header: 'Last edited by', isSystem: 1 } ], layout: { layouts: $scope.model.config.layouts, activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) }, allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete }; // Check if selected order by field is actually custom field for (var j = 0; j < $scope.options.includeProperties.length; j++) { var includedProperty = $scope.options.includeProperties[j]; if (includedProperty.alias.toLowerCase() === $scope.options.orderBy.toLowerCase()) { $scope.options.orderBySystemField = includedProperty.isSystem === 1; break; } } //update all of the system includeProperties to enable sorting _.each($scope.options.includeProperties, function (e, i) { //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted // to do that, we'd need to update the base query for content to include the content type alias column // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? if (e.alias != 'contentTypeAlias') { e.allowSorting = true; } // Another special case for members, only fields on the base table (cmsMember) can be used for sorting if (e.isSystem && $scope.entityType == 'member') { e.allowSorting = e.alias == 'username' || e.alias == 'email' || e.alias == 'updateDate'; } if (e.isSystem) { //localize the header var key = getLocalizedKey(e.alias); localizationService.localize(key).then(function (v) { e.header = v; }); } }); $scope.selectLayout = function (selectedLayout) { $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); }; function showNotificationsAndReset(err, reload, successMsg) { //check if response is ysod if (err.status && err.status >= 500) { // Open ysod overlay $scope.ysodOverlay = { view: 'ysod', error: err, show: true }; } $timeout(function () { $scope.bulkStatus = ''; $scope.actionInProgress = false; }, 500); if (err.data && angular.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } } else if (successMsg) { localizationService.localize('bulk_done').then(function (v) { notificationsService.success(v, successMsg); }); } } $scope.next = function (pageNumber) { $scope.options.pageNumber = pageNumber; $scope.reloadView($scope.contentId); }; $scope.goToPage = function (pageNumber) { $scope.options.pageNumber = pageNumber; $scope.reloadView($scope.contentId); }; $scope.prev = function (pageNumber) { $scope.options.pageNumber = pageNumber; $scope.reloadView($scope.contentId); }; /*Loads the search results, based on parameters set in prev,next,sort and so on*/ /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state with simple values */ $scope.getContent = function () { $scope.reloadView($scope.contentId); }; $scope.reloadView = function (id) { $scope.viewLoaded = false; $scope.folders = []; listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); getListResultsCallback(id, $scope.options).then(function (data) { $scope.actionInProgress = false; $scope.listViewResultSet = data; //update all values for display var section = appState.getSectionState('currentSection'); if ($scope.listViewResultSet.items) { _.each($scope.listViewResultSet.items, function (e, index) { setPropertyValues(e); // create the folders collection (only for media list views) if (section === 'media' && !mediaHelper.hasFilePropertyType(e)) { $scope.folders.push(e); } }); } $scope.viewLoaded = true; //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last // available page and then re-load again if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { $scope.options.pageNumber = $scope.listViewResultSet.totalPages; //reload! $scope.reloadView(id); } }); }; var searchListView = _.debounce(function () { $scope.$apply(function () { makeSearch(); }); }, 500); $scope.forceSearch = function (ev) { //13: enter switch (ev.keyCode) { case 13: makeSearch(); break; } }; $scope.enterSearch = function () { $scope.viewLoaded = false; searchListView(); }; function makeSearch() { if ($scope.options.filter !== null && $scope.options.filter !== undefined) { $scope.options.pageNumber = 1; $scope.reloadView($scope.contentId); } } $scope.isAnythingSelected = function () { if ($scope.selection.length === 0) { return false; } else { return true; } }; $scope.selectedItemsCount = function () { return $scope.selection.length; }; $scope.clearSelection = function () { listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); }; $scope.getIcon = function (entry) { return iconHelper.convertFromLegacyIcon(entry.icon); }; function serial(selected, fn, getStatusMsg, index) { return fn(selected, index).then(function (content) { index++; $scope.bulkStatus = getStatusMsg(index, selected.length); return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; }, function (err) { var reload = index > 0; showNotificationsAndReset(err, reload); return err; }); } function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { var selected = $scope.selection; if (selected.length === 0) return; if (confirmMsg && !confirm(confirmMsg)) return; $scope.actionInProgress = true; $scope.bulkStatus = getStatusMsg(0, selected.length); return serial(selected, fn, getStatusMsg, 0).then(function (result) { // executes once the whole selection has been processed // in case of an error (caught by serial), result will be the error if (!(result.data && angular.isArray(result.data.notifications))) showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); }); } $scope.delete = function () { var confirmDeleteText = ''; localizationService.localize('defaultdialogs_confirmdelete').then(function (value) { confirmDeleteText = value; var attempt = applySelected(function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, function (count, total) { var key = total === 1 ? 'bulk_deletedItemOfItem' : 'bulk_deletedItemOfItems'; return localizationService.localize(key, [ count, total ]); }, function (total) { var key = total === 1 ? 'bulk_deletedItem' : 'bulk_deletedItems'; return localizationService.localize(key, [total]); }, confirmDeleteText + '?'); if (attempt) { attempt.then(function () { //executes if all is successful, let's sync the tree var activeNode = appState.getTreeState('selectedNode'); if (activeNode) { navigationService.reloadNode(activeNode); } $scope.getContent(); }); } }); }; $scope.publish = function () { var attempt = applySelected(function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, function (count, total) { var key = total === 1 ? 'bulk_publishedItemOfItem' : 'bulk_publishedItemOfItems'; return localizationService.localize(key, [ count, total ]); }, function (total) { var key = total === 1 ? 'bulk_publishedItem' : 'bulk_publishedItems'; return localizationService.localize(key, [total]); }); if (attempt) { attempt.then(function () { $scope.getContent(); }); } }; $scope.unpublish = function () { var attempt = applySelected(function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, function (count, total) { var key = total === 1 ? 'bulk_unpublishedItemOfItem' : 'bulk_unpublishedItemOfItems'; return localizationService.localize(key, [ count, total ]); }, function (total) { var key = total === 1 ? 'bulk_unpublishedItem' : 'bulk_unpublishedItems'; return localizationService.localize(key, [total]); }); if (attempt) { attempt.then(function () { $scope.getContent(); }); } }; $scope.move = function () { $scope.moveDialog = {}; $scope.moveDialog.title = localizationService.localize('general_move'); $scope.moveDialog.section = $scope.entityType; $scope.moveDialog.currentNode = $scope.contentId; $scope.moveDialog.view = 'move'; $scope.moveDialog.show = true; $scope.moveDialog.submit = function (model) { if (model.target) { performMove(model.target); } $scope.moveDialog.show = false; $scope.moveDialog = null; }; $scope.moveDialog.close = function (oldModel) { $scope.moveDialog.show = false; $scope.moveDialog = null; }; }; function performMove(target) { //NOTE: With the way this applySelected/serial works, I'm not sure there's a better way currently to return // a specific value from one of the methods, so we'll have to try this way. Even though the first method // will fire once per every node moved, the destination path will be the same and we need to use that to sync. var newPath = null; var attempt = applySelected(function (selected, index) { return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }).then(function (path) { newPath = path; return path; }); }, function (count, total) { var key = total === 1 ? 'bulk_movedItemOfItem' : 'bulk_movedItemOfItems'; return localizationService.localize(key, [ count, total ]); }, function (total) { var key = total === 1 ? 'bulk_movedItem' : 'bulk_movedItems'; return localizationService.localize(key, [total]); }).then(function () { //executes if all is successful, let's sync the tree if (newPath) { //we need to do a double sync here: first refresh the node where the content was moved, // then refresh the node where the content was moved from navigationService.syncTree({ tree: target.nodeType ? target.nodeType : target.metaData.treeAlias, path: newPath, forceReload: true, activate: false }).then(function (args) { //get the currently edited node (if any) var activeNode = appState.getTreeState('selectedNode'); if (activeNode) { navigationService.reloadNode(activeNode); } }); } }); if (attempt) { attempt.then(function () { $scope.getContent(); }); } } $scope.copy = function () { $scope.copyDialog = {}; $scope.copyDialog.title = localizationService.localize('general_copy'); $scope.copyDialog.section = $scope.entityType; $scope.copyDialog.currentNode = $scope.contentId; $scope.copyDialog.view = 'copy'; $scope.copyDialog.show = true; $scope.copyDialog.submit = function (model) { if (model.target) { performCopy(model.target, model.relateToOriginal); } $scope.copyDialog.show = false; $scope.copyDialog = null; }; $scope.copyDialog.close = function (oldModel) { $scope.copyDialog.show = false; $scope.copyDialog = null; }; }; function performCopy(target, relateToOriginal) { var attempt = applySelected(function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, function (count, total) { var key = total === 1 ? 'bulk_copiedItemOfItem' : 'bulk_copiedItemOfItems'; return localizationService.localize(key, [ count, total ]); }, function (total) { var key = total === 1 ? 'bulk_copiedItem' : 'bulk_copiedItems'; return localizationService.localize(key, [total]); }); if (attempt) { attempt.then(function () { $scope.getContent(); }); } } function getCustomPropertyValue(alias, properties) { var value = ''; var index = 0; var foundAlias = false; for (var i = 0; i < properties.length; i++) { if (properties[i].alias == alias) { foundAlias = true; break; } index++; } if (foundAlias) { value = properties[index].value; } return value; } /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ function setPropertyValues(result) { //set the edit url result.editPath = createEditUrlCallback(result); _.each($scope.options.includeProperties, function (e, i) { var alias = e.alias; // First try to pull the value directly from the alias (e.g. updatedBy) var value = result[alias]; // If this returns an object, look for the name property of that (e.g. owner.name) if (value === Object(value)) { value = value['name']; } // If we've got nothing yet, look at a user defined property if (typeof value === 'undefined') { value = getCustomPropertyValue(alias, result.properties); } // If we have a date, format it if (isDate(value)) { value = value.substring(0, value.length - 3); } // set what we've got on the result result[alias] = value; }); } function isDate(val) { if (angular.isString(val)) { return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); } return false; } function initView() { //default to root id if the id is undefined var id = $routeParams.id; if (id === undefined) { id = -1; } //$scope.listViewAllowedTypes = getContentTypesCallback(id); getContentTypesCallback(id).then(function (listViewAllowedTypes) { $scope.listViewAllowedTypes = listViewAllowedTypes; var blueprints = false; _.each(listViewAllowedTypes, function (allowedType) { if (_.isEmpty(allowedType.blueprints)) { // this helps the view understand that there are no blueprints available allowedType.blueprints = null; } else { blueprints = true; } }); if (listViewAllowedTypes.length === 1 && blueprints === false) { $scope.createAllowedButtonSingle = true; } if (listViewAllowedTypes.length === 1 && blueprints === true) { $scope.createAllowedButtonSingleWithBlueprints = true; } if (listViewAllowedTypes.length > 1) { $scope.createAllowedButtonMultiWithBlueprints = true; } }); $scope.contentId = id; $scope.isTrashed = id === '-20' || id === '-21'; $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || $scope.options.allowBulkUnpublish || $scope.options.allowBulkCopy || $scope.options.allowBulkMove || $scope.options.allowBulkDelete; $scope.reloadView($scope.contentId); } function getLocalizedKey(alias) { switch (alias) { case 'sortOrder': return 'general_sort'; case 'updateDate': return 'content_updateDate'; case 'updater': return 'content_updatedBy'; case 'createDate': return 'content_createDate'; case 'owner': return 'content_createBy'; case 'published': return 'content_isPublished'; case 'contentTypeAlias': //TODO: Check for members return $scope.entityType === 'content' ? 'content_documentType' : 'content_mediatype'; case 'email': return 'general_email'; case 'username': return 'general_username'; } return alias; } function getItemKey(itemId) { for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { var item = $scope.listViewResultSet.items[i]; if (item.id === itemId) { return item.key; } } } function createBlank(entityType, docTypeAlias) { $location.path('/' + entityType + '/' + entityType + '/edit/' + $scope.contentId).search('doctype=' + docTypeAlias + '&create=true'); } function createFromBlueprint(entityType, docTypeAlias, blueprintId) { $location.path('/' + entityType + '/' + entityType + '/edit/' + $scope.contentId).search('doctype=' + docTypeAlias + '&create=true&blueprintId=' + blueprintId); } $scope.createBlank = createBlank; $scope.createFromBlueprint = createFromBlueprint; //GO! initView(); } angular.module('umbraco').controller('Umbraco.PropertyEditors.ListViewController', listViewController); function sortByPreValsController($rootScope, $scope, localizationService, editorState, listViewPrevalueHelper) { //Get the prevalue from the correct place function getPrevalues() { if (editorState.current.preValues) { return editorState.current.preValues; } else { return listViewPrevalueHelper.getPrevalues(); } } //Watch the prevalues $scope.$watch(function () { return _.findWhere(getPrevalues(), { key: 'includeProperties' }).value; }, function () { populateFields(); }, true); //Use deep watching, otherwise we won't pick up header changes function populateFields() { // Helper to find a particular value from the list of sort by options function findFromSortByFields(value) { return _.find($scope.sortByFields, function (e) { return e.value.toLowerCase() === value.toLowerCase(); }); } // Get list of properties assigned as columns of the list view var propsPreValue = _.findWhere(getPrevalues(), { key: 'includeProperties' }); // Populate list of options for the default sort (all the columns plus then node name) $scope.sortByFields = []; $scope.sortByFields.push({ value: 'name', name: 'Name', isSystem: 1 }); if (propsPreValue != undefined) { for (var i = 0; i < propsPreValue.value.length; i++) { var value = propsPreValue.value[i]; $scope.sortByFields.push({ value: value.alias, name: value.header, isSystem: value.isSystem }); } } // Localize the system fields, for some reason the directive doesn't work inside of the select group with an ng-model declared var systemFields = [ { value: 'SortOrder', key: 'general_sort' }, { value: 'Name', key: 'general_name' }, { value: 'VersionDate', key: 'content_updateDate' }, { value: 'Updater', key: 'content_updatedBy' }, { value: 'CreateDate', key: 'content_createDate' }, { value: 'Owner', key: 'content_createBy' }, { value: 'ContentTypeAlias', key: 'content_documentType' }, { value: 'Published', key: 'content_isPublished' }, { value: 'Email', key: 'general_email' }, { value: 'Username', key: 'general_username' } ]; _.each(systemFields, function (e) { localizationService.localize(e.key).then(function (v) { var sortByListValue = findFromSortByFields(e.value); if (sortByListValue) { sortByListValue.name = v; switch (e.value) { case 'Updater': e.name += ' (Content only)'; break; case 'Published': e.name += ' (Content only)'; break; case 'Email': e.name += ' (Members only)'; break; case 'Username': e.name += ' (Members only)'; break; } } }); }); // Check existing model value is available in list and ensure a value is set var existingValue = findFromSortByFields($scope.model.value); if (existingValue) { // Set the existing value // The old implementation pre Umbraco 7.5 used PascalCase aliases, this uses camelCase, so this ensures that any previous value is set $scope.model.value = existingValue.value; } else { // Existing value not found, set to first value $scope.model.value = $scope.sortByFields[0].value; } } } angular.module('umbraco').controller('Umbraco.PrevalueEditors.SortByListViewController', sortByPreValsController); //DO NOT DELETE THIS, this is in use... angular.module('umbraco').controller('Umbraco.PropertyEditors.MacroContainerController', function ($scope, dialogService, entityResource, macroService) { $scope.renderModel = []; $scope.allowOpenButton = true; $scope.allowRemoveButton = true; $scope.sortableOptions = {}; if ($scope.model.value) { var macros = $scope.model.value.split('>'); angular.forEach(macros, function (syntax, key) { if (syntax && syntax.length > 10) { //re-add the char we split on syntax = syntax + '>'; var parsed = macroService.parseMacroSyntax(syntax); if (!parsed) { parsed = {}; } parsed.syntax = syntax; collectDetails(parsed); $scope.renderModel.push(parsed); setSortingState($scope.renderModel); } }); } function collectDetails(macro) { macro.details = ''; macro.icon = 'icon-settings-alt'; if (macro.macroParamsDictionary) { angular.forEach(macro.macroParamsDictionary, function (value, key) { macro.details += key + ': ' + value + ' '; }); } } function openDialog(index) { var dialogData = { allowedMacros: $scope.model.config.allowed }; if (index !== null && $scope.renderModel[index]) { var macro = $scope.renderModel[index]; dialogData['macroData'] = macro; } $scope.macroPickerOverlay = {}; $scope.macroPickerOverlay.view = 'macropicker'; $scope.macroPickerOverlay.dialogData = dialogData; $scope.macroPickerOverlay.show = true; $scope.macroPickerOverlay.submit = function (model) { var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); collectDetails(macroObject); //update the raw syntax and the list... if (index !== null && $scope.renderModel[index]) { $scope.renderModel[index] = macroObject; } else { $scope.renderModel.push(macroObject); } setSortingState($scope.renderModel); $scope.macroPickerOverlay.show = false; $scope.macroPickerOverlay = null; }; $scope.macroPickerOverlay.close = function (oldModel) { $scope.macroPickerOverlay.show = false; $scope.macroPickerOverlay = null; }; } $scope.edit = function (index) { openDialog(index); }; $scope.add = function () { if ($scope.model.config.max && $scope.model.config.max > 0 && $scope.renderModel.length >= $scope.model.config.max) { //cannot add more than the max return; } openDialog(); }; $scope.remove = function (index) { $scope.renderModel.splice(index, 1); setSortingState($scope.renderModel); }; $scope.clear = function () { $scope.model.value = ''; $scope.renderModel = []; }; var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { var syntax = []; angular.forEach($scope.renderModel, function (value, key) { syntax.push(value.syntax); }); $scope.model.value = syntax.join(''); }); //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); function trim(str, chr) { var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); return str.replace(rgxtrim, ''); } function setSortingState(items) { // disable sorting if the list only consist of one item if (items.length > 1) { $scope.sortableOptions.disabled = false; } else { $scope.sortableOptions.disabled = true; } } }); function MacroListController($scope, entityResource) { $scope.items = []; entityResource.getAll('Macro').then(function (items) { _.each(items, function (i) { $scope.items.push({ name: i.name, alias: i.alias }); }); }); } angular.module('umbraco').controller('Umbraco.PrevalueEditors.MacroList', MacroListController); //inject umbracos assetsServce and dialog service function MarkdownEditorController($scope, $element, assetsService, dialogService, angularHelper, $timeout) { //tell the assets service to load the markdown.editor libs from the markdown editors //plugin folder if ($scope.model.value === null || $scope.model.value === '') { $scope.model.value = $scope.model.config.defaultValue; } function openMediaPicker(callback) { $scope.mediaPickerOverlay = {}; $scope.mediaPickerOverlay.view = 'mediaPicker'; $scope.mediaPickerOverlay.show = true; $scope.mediaPickerOverlay.disableFolderSelect = true; $scope.mediaPickerOverlay.submit = function (model) { var selectedImagePath = model.selectedImages[0].image; callback(selectedImagePath); $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; }; $scope.mediaPickerOverlay.close = function (model) { $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; }; } assetsService.load([ 'lib/markdown/markdown.converter.js', 'lib/markdown/markdown.sanitizer.js', 'lib/markdown/markdown.editor.js' ]).then(function () { // we need a short delay to wait for the textbox to appear. setTimeout(function () { //this function will execute when all dependencies have loaded // but in the case that they've been previously loaded, we can only // init the md editor after this digest because the DOM needs to be ready first // so run the init on a timeout $timeout(function () { $scope.markdownEditorInitComplete = false; var converter2 = new Markdown.Converter(); var editor2 = new Markdown.Editor(converter2, '-' + $scope.model.alias); editor2.run(); //subscribe to the image dialog clicks editor2.hooks.set('insertImageDialog', function (callback) { openMediaPicker(callback); return true; // tell the editor that we'll take care of getting the image url }); editor2.hooks.set('onPreviewRefresh', function () { // We must manually update the model as there is no way to hook into the markdown editor events without exstensive edits to the library. if ($scope.model.value !== $('textarea', $element).val()) { if ($scope.markdownEditorInitComplete) { //only set dirty after init load to avoid "unsaved" dialogue when we don't want it angularHelper.getCurrentForm($scope).$setDirty(); } else { $scope.markdownEditorInitComplete = true; } $scope.model.value = $('textarea', $element).val(); } }); }, 200); }); //load the seperat css for the editor to avoid it blocking our js loading TEMP HACK assetsService.loadCss('lib/markdown/markdown.css', $scope); }); } angular.module('umbraco').controller('Umbraco.PropertyEditors.MarkdownEditorController', MarkdownEditorController); //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller('Umbraco.PropertyEditors.MediaPickerController', function ($rootScope, $scope, dialogService, entityResource, mediaHelper, $timeout, userService, $location, localizationService) { //check the pre-values for multi-picker var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false; var disableFolderSelect = $scope.model.config.disableFolderSelect && $scope.model.config.disableFolderSelect !== '0' ? true : false; if (!$scope.model.config.startNodeId) { if ($scope.model.config.ignoreUserStartNodes === '1') { $scope.model.config.startNodeId = -1; $scope.model.config.startNodeIsVirtual = true; } else { userService.getCurrentUser().then(function (userData) { $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; }); } } function setupViewModel() { $scope.mediaItems = []; $scope.ids = []; $scope.isMultiPicker = multiPicker; if ($scope.model.value) { var ids = $scope.model.value.split(','); //NOTE: We need to use the entityResource NOT the mediaResource here because // the mediaResource has server side auth configured for which the user must have // access to the media section, if they don't they'll get auth errors. The entityResource // acts differently in that it allows access if the user has access to any of the apps that // might require it's use. Therefore we need to use the metaData property to get at the thumbnail // value. entityResource.getByIds(ids, 'Media').then(function (medias) { // The service only returns item results for ids that exist (deleted items are silently ignored). // This results in the picked items value to be set to contain only ids of picked items that could actually be found. // Since a referenced item could potentially be restored later on, instead of changing the selected values here based // on whether the items exist during a save event - we should keep "placeholder" items for picked items that currently // could not be fetched. This will preserve references and ensure that the state of an item does not differ depending // on whether it is simply resaved or not. // This is done by remapping the int/guid ids into a new array of items, where we create "Deleted item" placeholders // when there is no match for a selected id. This will ensure that the values being set on save, are the same as before. medias = _.map(ids, function (id) { var found = _.find(medias, function (m) { // We could use coercion (two ='s) here .. but not sure if this works equally well in all browsers and // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() // compares and be completely sure it works. return m.udi.toString() === id.toString() || m.id.toString() === id.toString(); }); if (found) { return found; } else { return { name: localizationService.dictionary.mediaPicker_deletedItem, id: $scope.model.config.idType !== 'udi' ? id : null, udi: $scope.model.config.idType === 'udi' ? id : null, icon: 'icon-picture', thumbnail: null, trashed: true }; } }); _.each(medias, function (media, i) { // if there is no thumbnail, try getting one if the media is not a placeholder item if (!media.thumbnail && media.id && media.metaData) { media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } $scope.mediaItems.push(media); if ($scope.model.config.idType === 'udi') { $scope.ids.push(media.udi); } else { $scope.ids.push(media.id); } }); $scope.sync(); }); } } setupViewModel(); $scope.remove = function (index) { $scope.mediaItems.splice(index, 1); $scope.ids.splice(index, 1); $scope.sync(); }; $scope.goToItem = function (item) { $location.path('media/media/edit/' + item.id); }; $scope.add = function () { $scope.mediaPickerOverlay = { view: 'mediapicker', title: 'Select media', startNodeId: $scope.model.config.startNodeId, startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, dataTypeId: $scope.model && $scope.model.dataTypeId ? $scope.model.dataTypeId : null, multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, show: true, submit: function (model) { _.each(model.selectedImages, function (media, i) { // if there is no thumbnail, try getting one if the media is not a placeholder item if (!media.thumbnail && media.id && media.metaData) { media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } $scope.mediaItems.push(media); if ($scope.model.config.idType === 'udi') { $scope.ids.push(media.udi); } else { $scope.ids.push(media.id); } }); $scope.sync(); $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; } }; }; $scope.sortableOptions = { disabled: !$scope.isMultiPicker, items: 'li:not(.add-wrapper)', cancel: '.unsortable', update: function (e, ui) { var r = []; // TODO: Instead of doing this with a half second delay would be better to use a watch like we do in the // content picker. Then we don't have to worry about setting ids, render models, models, we just set one and let the // watch do all the rest. $timeout(function () { angular.forEach($scope.mediaItems, function (value, key) { r.push($scope.model.config.idType === 'udi' ? value.udi : value.id); }); $scope.ids = r; $scope.sync(); }, 500, false); } }; $scope.sync = function () { $scope.model.value = $scope.ids.join(); }; $scope.showAdd = function () { if (!multiPicker) { if ($scope.model.value && $scope.model.value !== '') { return false; } } return true; }; //here we declare a special method which will be called whenever the value has changed from the server //this is instead of doing a watch on the model.value = faster $scope.model.onValueChanged = function (newVal, oldVal) { //update the display val again if it has changed from the server setupViewModel(); }; }); //this controller simply tells the dialogs service to open a memberPicker window //with a specified callback, this callback will receive an object with a selection on it function memberGroupPicker($scope, dialogService) { function trim(str, chr) { var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); return str.replace(rgxtrim, ''); } $scope.renderModel = []; $scope.allowRemove = true; if ($scope.model.value) { var modelIds = $scope.model.value.split(','); _.each(modelIds, function (item, i) { $scope.renderModel.push({ name: item, id: item, icon: 'icon-users' }); }); } $scope.openMemberGroupPicker = function () { $scope.memberGroupPicker = {}; $scope.memberGroupPicker.multiPicker = true; $scope.memberGroupPicker.view = 'memberGroupPicker'; $scope.memberGroupPicker.show = true; $scope.memberGroupPicker.submit = function (model) { if (model.selectedMemberGroups) { _.each(model.selectedMemberGroups, function (item, i) { $scope.add(item); }); } if (model.selectedMemberGroup) { $scope.clear(); $scope.add(model.selectedMemberGroup); } $scope.memberGroupPicker.show = false; $scope.memberGroupPicker = null; }; $scope.memberGroupPicker.close = function (oldModel) { $scope.memberGroupPicker.show = false; $scope.memberGroupPicker = null; }; }; $scope.remove = function (index) { $scope.renderModel.splice(index, 1); }; $scope.add = function (item) { var currIds = _.map($scope.renderModel, function (i) { return i.id; }); if (currIds.indexOf(item) < 0) { $scope.renderModel.push({ name: item, id: item, icon: 'icon-users' }); } }; $scope.clear = function () { $scope.renderModel = []; }; var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { var currIds = _.map($scope.renderModel, function (i) { return i.id; }); $scope.model.value = trim(currIds.join(), ','); }); //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); } angular.module('umbraco').controller('Umbraco.PropertyEditors.MemberGroupPickerController', memberGroupPicker); function memberGroupController($rootScope, $scope, dialogService, mediaResource, imageHelper, $log) { //set the available to the keys of the dictionary who's value is true $scope.getAvailable = function () { var available = []; for (var n in $scope.model.value) { if ($scope.model.value[n] === false) { available.push(n); } } return available; }; //set the selected to the keys of the dictionary who's value is true $scope.getSelected = function () { var selected = []; for (var n in $scope.model.value) { if ($scope.model.value[n] === true) { selected.push(n); } } return selected; }; $scope.addItem = function (item) { //keep the model up to date $scope.model.value[item] = true; }; $scope.removeItem = function (item) { //keep the model up to date $scope.model.value[item] = false; }; } angular.module('umbraco').controller('Umbraco.PropertyEditors.MemberGroupController', memberGroupController); //this controller simply tells the dialogs service to open a memberPicker window //with a specified callback, this callback will receive an object with a selection on it function memberPickerController($scope, dialogService, entityResource, $log, iconHelper, angularHelper) { function trim(str, chr) { var rgxtrim = !chr ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); return str.replace(rgxtrim, ''); } $scope.renderModel = []; $scope.allowRemove = true; var dialogOptions = { multiPicker: false, entityType: 'Member', section: 'member', treeAlias: 'member', filter: function (i) { return i.metaData.isContainer == true; }, filterCssClass: 'not-allowed', callback: function (data) { if (angular.isArray(data)) { _.each(data, function (item, i) { $scope.add(item); }); } else { $scope.clear(); $scope.add(data); } angularHelper.getCurrentForm($scope).$setDirty(); } }; //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the // pre-value config on to the dialog options if ($scope.model.config) { angular.extend(dialogOptions, $scope.model.config); } $scope.openMemberPicker = function () { $scope.memberPickerOverlay = dialogOptions; $scope.memberPickerOverlay.view = 'memberPicker'; $scope.memberPickerOverlay.show = true; $scope.memberPickerOverlay.submit = function (model) { if (model.selection) { _.each(model.selection, function (item, i) { $scope.add(item); }); } $scope.memberPickerOverlay.show = false; $scope.memberPickerOverlay = null; }; $scope.memberPickerOverlay.close = function (oldModel) { $scope.memberPickerOverlay.show = false; $scope.memberPickerOverlay = null; }; }; $scope.remove = function (index) { $scope.renderModel.splice(index, 1); }; $scope.add = function (item) { var currIds = _.map($scope.renderModel, function (i) { if ($scope.model.config.idType === 'udi') { return i.udi; } else { return i.id; } }); var itemId = $scope.model.config.idType === 'udi' ? item.udi : item.id; if (currIds.indexOf(itemId) < 0) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon }); } }; $scope.clear = function () { $scope.renderModel = []; }; var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { var currIds = _.map($scope.renderModel, function (i) { if ($scope.model.config.idType === 'udi') { return i.udi; } else { return i.id; } }); $scope.model.value = trim(currIds.join(), ','); }); //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); //load member data var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; entityResource.getByIds(modelIds, 'Member').then(function (data) { _.each(data, function (item, i) { // set default icon if it's missing item.icon = item.icon ? iconHelper.convertFromLegacyIcon(item.icon) : 'icon-user'; $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon }); }); }); } angular.module('umbraco').controller('Umbraco.PropertyEditors.MemberPickerController', memberPickerController); function MultipleTextBoxController($scope, $timeout) { var backspaceHits = 0; // Set the visible prompt to -1 to ensure it will not be visible $scope.promptIsVisible = '-1'; $scope.sortableOptions = { axis: 'y', containment: 'parent', cursor: 'move', items: '> div.textbox-wrapper', tolerance: 'pointer' }; if (!$scope.model.value) { $scope.model.value = []; } //add any fields that there isn't values for if ($scope.model.config.min > 0) { for (var i = 0; i < $scope.model.config.min; i++) { if (i + 1 > $scope.model.value.length) { $scope.model.value.push({ value: '' }); } } } $scope.addRemoveOnKeyDown = function (event, index) { var txtBoxValue = $scope.model.value[index]; event.preventDefault(); switch (event.keyCode) { case 13: if ($scope.model.config.max <= 0 && txtBoxValue.value || $scope.model.value.length < $scope.model.config.max && txtBoxValue.value) { var newItemIndex = index + 1; $scope.model.value.splice(newItemIndex, 0, { value: '' }); //Focus on the newly added value $scope.model.value[newItemIndex].hasFocus = true; } break; case 8: if ($scope.model.value.length > $scope.model.config.min) { var remainder = []; // Used to require an extra hit on backspace for the field to be removed if (txtBoxValue.value === '') { backspaceHits++; } else { backspaceHits = 0; } if (txtBoxValue.value === '' && backspaceHits === 2) { for (var x = 0; x < $scope.model.value.length; x++) { if (x !== index) { remainder.push($scope.model.value[x]); } } $scope.model.value = remainder; var prevItemIndex = index - 1; //Set focus back on false as the directive only watches for true if (prevItemIndex >= 0) { $scope.model.value[prevItemIndex].hasFocus = false; $timeout(function () { //Focus on the previous value $scope.model.value[prevItemIndex].hasFocus = true; }); } backspaceHits = 0; } } break; default: } }; $scope.add = function () { if ($scope.model.config.max <= 0 || $scope.model.value.length < $scope.model.config.max) { $scope.model.value.push({ value: '' }); // focus new value var newItemIndex = $scope.model.value.length - 1; $scope.model.value[newItemIndex].hasFocus = true; } }; $scope.remove = function (index) { // Make sure not to trigger other prompts when remove is triggered $scope.hidePrompt(); var remainder = []; for (var x = 0; x < $scope.model.value.length; x++) { if (x !== index) { remainder.push($scope.model.value[x]); } } $scope.model.value = remainder; }; $scope.showPrompt = function (idx, item) { var i = $scope.model.value.indexOf(item); // Make the prompt visible for the clicked tag only if (i === idx) { $scope.promptIsVisible = i; } }; $scope.hidePrompt = function () { $scope.promptIsVisible = '-1'; }; } angular.module('umbraco').controller('Umbraco.PropertyEditors.MultipleTextBoxController', MultipleTextBoxController); function multiUrlPickerController($scope, angularHelper, localizationService, entityResource, iconHelper) { $scope.renderModel = []; if ($scope.preview) { return; } if (!Array.isArray($scope.model.value)) { $scope.model.value = []; } var currentForm = angularHelper.getCurrentForm($scope); $scope.sortableOptions = { distance: 10, tolerance: 'pointer', scroll: true, zIndex: 6000, update: function () { currentForm.$setDirty(); } }; $scope.model.value.forEach(function (link) { link.icon = iconHelper.convertFromLegacyIcon(link.icon); $scope.renderModel.push(link); }); $scope.$on('formSubmitting', function () { $scope.model.value = $scope.renderModel; }); $scope.$watch(function () { return $scope.renderModel.length; }, function () { if ($scope.model.config && $scope.model.config.minNumber) { $scope.multiUrlPickerForm.minCount.$setValidity('minCount', +$scope.model.config.minNumber <= $scope.renderModel.length); } if ($scope.model.config && $scope.model.config.maxNumber) { $scope.multiUrlPickerForm.maxCount.$setValidity('maxCount', +$scope.model.config.maxNumber >= $scope.renderModel.length); } $scope.sortableOptions.disabled = $scope.renderModel.length === 1; }); $scope.remove = function ($index) { $scope.renderModel.splice($index, 1); currentForm.$setDirty(); }; $scope.openLinkPicker = function (link, $index) { var target = link ? { name: link.name, anchor: link.queryString, // the linkPicker breaks if it get an udi for media udi: link.isMedia ? null : link.udi, url: link.url, target: link.target } : null; $scope.linkPickerOverlay = { view: 'linkpicker', currentTarget: target, dataTypeId: $scope.model && $scope.model.dataTypeId ? $scope.model.dataTypeId : null, ignoreUserStartNodes: $scope.model.config && $scope.model.config.ignoreUserStartNodes ? $scope.model.config.ignoreUserStartNodes : '0', show: true, submit: function (model) { if (model.target.url || model.target.anchor) { // if an anchor exists, check that it is appropriately prefixed if (model.target.anchor && model.target.anchor[0] !== '?' && model.target.anchor[0] !== '#') { model.target.anchor = (model.target.anchor.indexOf('=') === -1 ? '#' : '?') + model.target.anchor; } if (link) { if (link.isMedia && link.url === model.target.url) { } else { link.udi = model.target.udi; link.isMedia = model.target.isMedia; } link.name = model.target.name || model.target.url || model.target.anchor; link.queryString = model.target.anchor; link.target = model.target.target; link.url = model.target.url; } else { link = { isMedia: model.target.isMedia, name: model.target.name || model.target.url || model.target.anchor, queryString: model.target.anchor, target: model.target.target, udi: model.target.udi, url: model.target.url }; $scope.renderModel.push(link); } if (link.udi) { var entityType = link.isMedia ? 'media' : 'document'; entityResource.getById(link.udi, entityType).then(function (data) { link.icon = iconHelper.convertFromLegacyIcon(data.icon); link.published = data.metaData && data.metaData.IsPublished === false && entityType === 'Document' ? false : true; link.trashed = data.trashed; if (link.trashed) { item.url = localizationService.dictionary.general_recycleBin; } }); } else { link.icon = 'icon-link'; link.published = true; } currentForm.$setDirty(); } $scope.linkPickerOverlay.show = false; $scope.linkPickerOverlay = null; } }; }; } angular.module('umbraco').controller('Umbraco.PropertyEditors.MultiUrlPickerController', multiUrlPickerController); angular.module('umbraco').controller('Umbraco.PropertyEditors.NestedContent.DocTypePickerController', [ '$scope', 'Umbraco.PropertyEditors.NestedContent.Resources', function ($scope, ncResources) { $scope.add = function () { $scope.model.value.push({ // As per PR #4, all stored content type aliases must be prefixed "nc" for easier recognition. // For good measure we'll also prefix the tab alias "nc" ncAlias: '', ncTabAlias: '', nameTemplate: '' }); }; $scope.remove = function (index) { $scope.model.value.splice(index, 1); }; $scope.sortableOptions = { axis: 'y', cursor: 'move', handle: '.icon-navigation' }; $scope.docTypeTabs = {}; ncResources.getContentTypes().then(function (docTypes) { $scope.model.docTypes = docTypes; // Populate document type tab dictionary docTypes.forEach(function (value) { $scope.docTypeTabs[value.alias] = value.tabs; }); }); $scope.selectableDocTypesFor = function (config) { // return all doctypes that are: // 1. either already selected for this config, or // 2. not selected in any other config return _.filter($scope.model.docTypes, function (docType) { return docType.alias === config.ncAlias || !_.find($scope.model.value, function (c) { return docType.alias === c.ncAlias; }); }); }; if (!$scope.model.value) { $scope.model.value = []; $scope.add(); } } ]); angular.module('umbraco').controller('Umbraco.PropertyEditors.NestedContent.PropertyEditorController', [ '$scope', '$interpolate', '$filter', '$timeout', 'contentResource', 'localizationService', 'iconHelper', function ($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper) { //$scope.model.config.contentTypes; //$scope.model.config.minItems; //$scope.model.config.maxItems; //console.log($scope); var inited = false; _.each($scope.model.config.contentTypes, function (contentType) { contentType.nameExp = !!contentType.nameTemplate ? $interpolate(contentType.nameTemplate) : undefined; }); $scope.editIconTitle = ''; $scope.moveIconTitle = ''; $scope.deleteIconTitle = ''; // localize the edit icon title localizationService.localize('general_edit').then(function (value) { $scope.editIconTitle = value; }); // localize the delete icon title localizationService.localize('general_delete').then(function (value) { $scope.deleteIconTitle = value; }); // localize the move icon title localizationService.localize('actions_move').then(function (value) { $scope.moveIconTitle = value; }); $scope.nodes = []; $scope.currentNode = undefined; $scope.realCurrentNode = undefined; $scope.scaffolds = undefined; $scope.sorting = false; $scope.minItems = $scope.model.config.minItems || 0; $scope.maxItems = $scope.model.config.maxItems || 0; if ($scope.maxItems == 0) $scope.maxItems = 1000; $scope.singleMode = $scope.minItems == 1 && $scope.maxItems == 1; $scope.showIcons = $scope.model.config.showIcons || true; $scope.wideMode = $scope.model.config.hideLabel == '1'; // helper to force the current form into the dirty state $scope.setDirty = function () { if ($scope.propertyForm) { $scope.propertyForm.$setDirty(); } }; $scope.addNode = function (alias) { var scaffold = $scope.getScaffold(alias); var newNode = initNode(scaffold, null); $scope.currentNode = newNode; $scope.setDirty(); }; $scope.openNodeTypePicker = function ($event) { if ($scope.nodes.length >= $scope.maxItems) { return; } $scope.overlayMenu = { title: localizationService.localize('grid_insertControl'), show: false, style: {}, filter: $scope.scaffolds.length > 15 ? true : false, orderBy: '$index', view: 'itempicker', event: $event, submit: function (model) { if (model && model.selectedItem) { $scope.addNode(model.selectedItem.alias); } $scope.overlayMenu.show = false; $scope.overlayMenu = null; }, close: function () { $scope.overlayMenu.show = false; $scope.overlayMenu = null; } }; // this could be used for future limiting on node types $scope.overlayMenu.availableItems = []; _.each($scope.scaffolds, function (scaffold) { $scope.overlayMenu.availableItems.push({ alias: scaffold.contentTypeAlias, name: scaffold.contentTypeName, icon: iconHelper.convertFromLegacyIcon(scaffold.icon) }); }); if ($scope.overlayMenu.availableItems.length === 0) { return; } if ($scope.overlayMenu.availableItems.length === 1) { // only one scaffold type - no need to display the picker $scope.addNode($scope.scaffolds[0].contentTypeAlias); return; } $scope.overlayMenu.show = true; }; $scope.editNode = function (idx) { if ($scope.currentNode && $scope.currentNode.key == $scope.nodes[idx].key) { $scope.currentNode = undefined; } else { $scope.currentNode = $scope.nodes[idx]; } }; $scope.deleteNode = function (idx) { if ($scope.nodes.length > $scope.model.config.minItems) { if ($scope.model.config.confirmDeletes && $scope.model.config.confirmDeletes == 1) { localizationService.localize('content_nestedContentDeleteItem').then(function (value) { if (confirm(value)) { $scope.nodes.splice(idx, 1); $scope.setDirty(); updateModel(); } }); } else { $scope.nodes.splice(idx, 1); $scope.setDirty(); updateModel(); } } }; $scope.getName = function (idx) { var name = 'Item ' + (idx + 1); if ($scope.model.value[idx]) { var contentType = $scope.getContentTypeConfig($scope.model.value[idx].ncContentTypeAlias); if (contentType != null && contentType.nameExp) { // Run the expression against the stored dictionary value, NOT the node object var item = $scope.model.value[idx]; // Add a temporary index property item['$index'] = idx + 1; var newName = contentType.nameExp(item); if (newName && (newName = $.trim(newName))) { name = newName; } // Delete the index property as we don't want to persist it delete item['$index']; } } // Update the nodes actual name value if ($scope.nodes[idx].name !== name) { $scope.nodes[idx].name = name; } return name; }; $scope.getIcon = function (idx) { var scaffold = $scope.getScaffold($scope.model.value[idx].ncContentTypeAlias); return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : 'icon-folder'; }; $scope.sortableOptions = { axis: 'y', cursor: 'move', handle: '.umb-nested-content__icon--move', start: function (ev, ui) { updateModel(); // Yea, yea, we shouldn't modify the dom, sue me $('#umb-nested-content--' + $scope.model.id + ' .umb-rte textarea').each(function () { tinymce.execCommand('mceRemoveEditor', false, $(this).attr('id')); $(this).css('visibility', 'hidden'); }); $scope.$apply(function () { $scope.sorting = true; }); }, update: function (ev, ui) { $scope.setDirty(); }, stop: function (ev, ui) { $('#umb-nested-content--' + $scope.model.id + ' .umb-rte textarea').each(function () { tinymce.execCommand('mceAddEditor', true, $(this).attr('id')); $(this).css('visibility', 'visible'); }); $scope.$apply(function () { $scope.sorting = false; updateModel(); }); } }; $scope.getScaffold = function (alias) { return _.find($scope.scaffolds, function (scaffold) { return scaffold.contentTypeAlias == alias; }); }; $scope.getContentTypeConfig = function (alias) { return _.find($scope.model.config.contentTypes, function (contentType) { return contentType.ncAlias == alias; }); }; var notSupported = [ 'Umbraco.Tags', 'Umbraco.UploadField', 'Umbraco.ImageCropper' ]; // Initialize var scaffoldsLoaded = 0; $scope.scaffolds = []; _.each($scope.model.config.contentTypes, function (contentType) { contentResource.getScaffold(-20, contentType.ncAlias).then(function (scaffold) { // remove all tabs except the specified tab var tab = _.find(scaffold.tabs, function (tab) { return tab.id != 0 && (tab.alias.toLowerCase() == contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias == ''); }); scaffold.tabs = []; if (tab) { scaffold.tabs.push(tab); angular.forEach(tab.properties, function (property) { if (_.find(notSupported, function (x) { return x === property.editor; })) { property.notSupported = true; //TODO: Not supported message to be replaced with 'content_nestedContentEditorNotSupported' dictionary key. Currently not possible due to async/timing quirk. property.notSupportedMessage = 'Property ' + property.label + ' uses editor ' + property.editor + ' which is not supported by Nested Content.'; } }); } // Store the scaffold object $scope.scaffolds.push(scaffold); scaffoldsLoaded++; initIfAllScaffoldsHaveLoaded(); }, function (error) { scaffoldsLoaded++; initIfAllScaffoldsHaveLoaded(); }); }); var initIfAllScaffoldsHaveLoaded = function () { // Initialize when all scaffolds have loaded if ($scope.model.config.contentTypes.length == scaffoldsLoaded) { // Because we're loading the scaffolds async one at a time, we need to // sort them explicitly according to the sort order defined by the data type. var contentTypeAliases = []; _.each($scope.model.config.contentTypes, function (contentType) { contentTypeAliases.push(contentType.ncAlias); }); $scope.scaffolds = $filter('orderBy')($scope.scaffolds, function (s) { return contentTypeAliases.indexOf(s.contentTypeAlias); }); // Convert stored nodes if ($scope.model.value) { for (var i = 0; i < $scope.model.value.length; i++) { var item = $scope.model.value[i]; var scaffold = $scope.getScaffold(item.ncContentTypeAlias); if (scaffold == null) { // No such scaffold - the content type might have been deleted. We need to skip it. continue; } initNode(scaffold, item); } } // Enforce min items if ($scope.nodes.length < $scope.model.config.minItems) { for (var i = $scope.nodes.length; i < $scope.model.config.minItems; i++) { $scope.addNode($scope.scaffolds[0].contentTypeAlias); } } // If there is only one item, set it as current node if ($scope.singleMode || $scope.nodes.length == 1 && $scope.maxItems == 1) { $scope.currentNode = $scope.nodes[0]; } inited = true; } }; var initNode = function (scaffold, item) { var node = angular.copy(scaffold); node.key = item && item.key ? item.key : UUID.generate(); node.ncContentTypeAlias = scaffold.contentTypeAlias; for (var t = 0; t < node.tabs.length; t++) { var tab = node.tabs[t]; for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; prop.propertyAlias = prop.alias; prop.alias = $scope.model.alias + '___' + prop.alias; // Force validation to occur server side as this is the // only way we can have consistancy between mandatory and // regex validation messages. Not ideal, but it works. prop.validation = { mandatory: false, pattern: '' }; if (item) { if (item[prop.propertyAlias]) { prop.value = item[prop.propertyAlias]; } } } } $scope.nodes.push(node); return node; }; var updateModel = function () { if ($scope.realCurrentNode) { $scope.$broadcast('ncSyncVal', { key: $scope.realCurrentNode.key }); } if (inited) { var newValues = []; for (var i = 0; i < $scope.nodes.length; i++) { var node = $scope.nodes[i]; var newValue = { key: node.key, name: node.name, ncContentTypeAlias: node.ncContentTypeAlias }; for (var t = 0; t < node.tabs.length; t++) { var tab = node.tabs[t]; for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; if (typeof prop.value !== 'function') { newValue[prop.propertyAlias] = prop.value; } } } newValues.push(newValue); } $scope.model.value = newValues; } }; $scope.$watch('currentNode', function (newVal) { updateModel(); $scope.realCurrentNode = newVal; }); var unsubscribe = $scope.$on('formSubmitting', function (ev, args) { updateModel(); }); $scope.$on('$destroy', function () { unsubscribe(); }); //TODO: Move this into a shared location? var UUID = function () { var self = {}; var lut = []; for (var i = 0; i < 256; i++) { lut[i] = (i < 16 ? '0' : '') + i.toString(16); } self.generate = function () { var d0 = Math.random() * 4294967295 | 0; var d1 = Math.random() * 4294967295 | 0; var d2 = Math.random() * 4294967295 | 0; var d3 = Math.random() * 4294967295 | 0; return lut[d0 & 255] + lut[d0 >> 8 & 255] + lut[d0 >> 16 & 255] + lut[d0 >> 24 & 255] + '-' + lut[d1 & 255] + lut[d1 >> 8 & 255] + '-' + lut[d1 >> 16 & 15 | 64] + lut[d1 >> 24 & 255] + '-' + lut[d2 & 63 | 128] + lut[d2 >> 8 & 255] + '-' + lut[d2 >> 16 & 255] + lut[d2 >> 24 & 255] + lut[d3 & 255] + lut[d3 >> 8 & 255] + lut[d3 >> 16 & 255] + lut[d3 >> 24 & 255]; }; return self; }(); } ]); angular.module('umbraco').controller('Umbraco.PropertyEditors.RadioButtonsController', function ($scope) { if (angular.isObject($scope.model.config.items)) { //now we need to format the items in the dictionary because we always want to have an array var newItems = []; var vals = _.values($scope.model.config.items); var keys = _.keys($scope.model.config.items); for (var i = 0; i < vals.length; i++) { newItems.push({ id: keys[i], sortOrder: vals[i].sortOrder, value: vals[i].value }); } //ensure the items are sorted by the provided sort order newItems.sort(function (a, b) { return a.sortOrder > b.sortOrder ? 1 : b.sortOrder > a.sortOrder ? -1 : 0; }); //re-assign $scope.model.config.items = newItems; } }); /** * @ngdoc controller * @name Umbraco.Editors.ReadOnlyValueController * @function * * @description * The controller for the readonlyvalue property editor. * This controller offer more functionality than just a simple label as it will be able to apply formatting to the * value to be displayed. This means that we also have to apply more complex logic of watching the model value when * it changes because we are creating a new scope value called displayvalue which will never change based on the server data. * In some cases after a form submission, the server will modify the data that has been persisted, especially in the cases of * readonlyvalues so we need to ensure that after the form is submitted that the new data is reflected here. */ function ReadOnlyValueController($rootScope, $scope, $filter) { function formatDisplayValue() { if ($scope.model.config && angular.isArray($scope.model.config) && $scope.model.config.length > 0 && $scope.model.config[0] && $scope.model.config.filter) { if ($scope.model.config.format) { $scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value, $scope.model.config.format); } else { $scope.displayvalue = $filter($scope.model.config.filter)($scope.model.value); } } else { $scope.displayvalue = $scope.model.value; } } //format the display value on init: formatDisplayValue(); $scope.$watch('model.value', function (newVal, oldVal) { //cannot just check for !newVal because it might be an empty string which we //want to look for. if (newVal !== null && newVal !== undefined && newVal !== oldVal) { //update the display val again formatDisplayValue(); } }); } angular.module('umbraco').controller('Umbraco.PropertyEditors.ReadOnlyValueController', ReadOnlyValueController); angular.module('umbraco').controller('Umbraco.PropertyEditors.RelatedLinksController', function ($rootScope, $scope, dialogService, iconHelper) { if (!$scope.model.value) { $scope.model.value = []; } $scope.model.config.max = isNumeric($scope.model.config.max) && $scope.model.config.max !== 0 ? $scope.model.config.max : Number.MAX_VALUE; $scope.newCaption = ''; $scope.newLink = 'http://'; $scope.newNewWindow = false; $scope.newInternal = null; $scope.newInternalName = ''; $scope.newInternalIcon = null; $scope.addExternal = true; $scope.currentEditLink = null; $scope.hasError = false; var dataTypeId = $scope.model && $scope.model.dataTypeId ? $scope.model.dataTypeId : null; $scope.internal = function ($event) { $scope.currentEditLink = null; $scope.contentPickerOverlay = {}; $scope.contentPickerOverlay.view = 'contentpicker'; $scope.contentPickerOverlay.multiPicker = false; $scope.contentPickerOverlay.show = true; $scope.contentPickerOverlay.dataTypeId = dataTypeId; $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : 'int'; $scope.contentPickerOverlay.submit = function (model) { select(model.selection[0]); $scope.contentPickerOverlay.show = false; $scope.contentPickerOverlay = null; }; $scope.contentPickerOverlay.close = function (oldModel) { $scope.contentPickerOverlay.show = false; $scope.contentPickerOverlay = null; }; $event.preventDefault(); }; $scope.selectInternal = function ($event, link) { $scope.currentEditLink = link; $scope.contentPickerOverlay = {}; $scope.contentPickerOverlay.view = 'contentpicker'; $scope.contentPickerOverlay.multiPicker = false; $scope.contentPickerOverlay.show = true; $scope.contentPickerOverlay.dataTypeId = dataTypeId; $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : 'int'; $scope.contentPickerOverlay.submit = function (model) { select(model.selection[0]); $scope.contentPickerOverlay.show = false; $scope.contentPickerOverlay = null; }; $scope.contentPickerOverlay.close = function (oldModel) { $scope.contentPickerOverlay.show = false; $scope.contentPickerOverlay = null; }; $event.preventDefault(); }; $scope.edit = function (idx) { for (var i = 0; i < $scope.model.value.length; i++) { $scope.model.value[i].edit = false; } $scope.model.value[idx].edit = true; }; $scope.saveEdit = function (idx) { $scope.model.value[idx].title = $scope.model.value[idx].caption; $scope.model.value[idx].edit = false; }; $scope.delete = function (idx) { $scope.model.value.splice(idx, 1); }; $scope.add = function ($event) { if (!angular.isArray($scope.model.value)) { $scope.model.value = []; } if ($scope.newCaption == '') { $scope.hasError = true; } else { if ($scope.addExternal) { var newExtLink = new function () { this.caption = $scope.newCaption; this.link = $scope.newLink; this.newWindow = $scope.newNewWindow; this.edit = false; this.isInternal = false; this.type = 'external'; this.title = $scope.newCaption; }(); $scope.model.value.push(newExtLink); } else { var newIntLink = new function () { this.caption = $scope.newCaption; this.link = $scope.newInternal; this.newWindow = $scope.newNewWindow; this.internal = $scope.newInternal; this.edit = false; this.isInternal = true; this.internalName = $scope.newInternalName; this.internalIcon = $scope.newInternalIcon; this.type = 'internal'; this.title = $scope.newCaption; }(); $scope.model.value.push(newIntLink); } $scope.newCaption = ''; $scope.newLink = 'http://'; $scope.newNewWindow = false; $scope.newInternal = null; $scope.newInternalName = ''; $scope.newInternalIcon = null; } $event.preventDefault(); }; $scope.switch = function ($event) { $scope.addExternal = !$scope.addExternal; $event.preventDefault(); }; $scope.switchLinkType = function ($event, link) { link.isInternal = !link.isInternal; link.type = link.isInternal ? 'internal' : 'external'; if (!link.isInternal) link.link = $scope.newLink; $event.preventDefault(); }; $scope.move = function (index, direction) { var temp = $scope.model.value[index]; $scope.model.value[index] = $scope.model.value[index + direction]; $scope.model.value[index + direction] = temp; }; //helper for determining if a user can add items $scope.canAdd = function () { return $scope.model.config.max <= 0 || $scope.model.config.max > countVisible(); }; //helper that returns if an item can be sorted $scope.canSort = function () { return countVisible() > 1; }; $scope.sortableOptions = { axis: 'y', handle: '.handle', cursor: 'move', cancel: '.no-drag', containment: 'parent', placeholder: 'sortable-placeholder', forcePlaceholderSize: true, helper: function (e, ui) { // When sorting table rows, the cells collapse. This helper fixes that: https://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/ ui.children().each(function () { $(this).width($(this).width()); }); return ui; }, items: '> tr:not(.unsortable)', tolerance: 'pointer', update: function (e, ui) { // Get the new and old index for the moved element (using the URL as the identifier) var newIndex = ui.item.index(); var movedLinkUrl = ui.item.attr('data-link'); var originalIndex = getElementIndexByUrl(movedLinkUrl); // Move the element in the model var movedElement = $scope.model.value[originalIndex]; $scope.model.value.splice(originalIndex, 1); $scope.model.value.splice(newIndex, 0, movedElement); }, start: function (e, ui) { //ui.placeholder.html("