++ ++ +++ + +
+ (function () {
+ "use strict";
+
+ function DrawerController(appState) {
+
+ var vm = this;
+
+ vm.toggleDrawer = toggleDrawer;
+
+ function toggleDrawer() {
+
+ var showDrawer = appState.getDrawerState("showDrawer");
+
+ var model = {
+ firstName: "Super",
+ lastName: "Man"
+ };
+
+ appState.setDrawerState("view", "/App_Plugins/path/to/drawer.html");
+ appState.setDrawerState("model", model);
+ appState.setDrawerState("showDrawer", !showDrawer);
+
+ }
+
+ }
+
+ angular.module("umbraco").controller("My.DrawerController", DrawerController);
+
+ })();
+
+
+binding): Set the drawer view
+@param {string} model (binding): Pass in custom data to the drawer
+
+**/
+
+function Drawer($location, $routeParams, helpService, userService, localizationService, dashboardResource) {
+
+ return {
+
+ restrict: "E", // restrict to an element
+ replace: true, // replace the html element with the template
+ templateUrl: 'views/components/application/umbdrawer/umb-drawer.html',
+ transclude: true,
+ scope: {
+ view: "=?",
+ model: "=?"
+ },
+
+ link: function (scope, element, attr, ctrl) {
+
+ function onInit() {
+ setView();
+ }
+
+ function setView() {
+ if (scope.view) {
+ //we do this to avoid a hidden dialog to start loading unconfigured views before the first activation
+ var configuredView = scope.view;
+ if (scope.view.indexOf(".html") === -1) {
+ var viewAlias = scope.view.toLowerCase();
+ configuredView = "views/common/drawers/" + viewAlias + "/" + viewAlias + ".html";
+ }
+ if (configuredView !== scope.configuredView) {
+ scope.configuredView = configuredView;
+ }
+ }
+ }
+
+ onInit();
+
+ }
+
+ };
+ }
+
+ angular.module('umbraco.directives').directive("umbDrawer", Drawer);
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawercontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawercontent.directive.js
new file mode 100644
index 0000000000..d4aa76f70e
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawercontent.directive.js
@@ -0,0 +1,59 @@
+/**
+@ngdoc directive
+@name umbraco.directives.directive:umbDrawerContent
+@restrict E
+@scope
+
+@description
+Use this directive to render drawer content
+
+++ + ++ + ++ + ++ + + +{{ model | json }}++ + + +
++ ++ + ++ + ++ + + +{{ model | json }}++ + + +
++ ++ + ++ + ++ + + +{{ model | json }}++ + + +
attribute): Set a drawer title.
+@param {string} description (attribute): Set a drawer description.
+**/
+
+(function() {
+ 'use strict';
+
+ function DrawerHeaderDirective() {
+
+ var directive = {
+ restrict: 'E',
+ replace: true,
+ templateUrl: 'views/components/application/umbdrawer/umb-drawer-header.html',
+ scope: {
+ "title": "@?",
+ "description": "@?"
+ }
+ };
+
+ return directive;
+
+ }
+
+ angular.module('umbraco.directives').directive('umbDrawerHeader', DrawerHeaderDirective);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerview.directive.js
new file mode 100644
index 0000000000..54cfea0857
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerview.directive.js
@@ -0,0 +1,58 @@
+/**
+@ngdoc directive
+@name umbraco.directives.directive:umbDrawerView
+@restrict E
+@scope
+
+@description
+Use this directive to render drawer view
+
+++ ++ + ++ + ++ + + +{{ model | json }}++ + + +
+// The tour config object
+{
+ "name": "My Custom Tour", // (required)
+ "alias": "myCustomTour", // A unique tour alias (required)
+ "group": "My Custom Group" // Used to group tours in the help drawer
+ "groupOrder": 200 // Control the order of tour groups
+ "allowDisable": // Adds a "Don't" show this tour again"-button to the intro step
+ "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.
+ "steps": [] // tour steps - see next example
+}
+
+
+// A tour step object
+{
+ "title": "Title",
+ "content": "Step content
",
+ "type": "intro" // makes the step an introduction step,
+ "element": "[data-element='my-table-row']", // the highlighted element
+ "event": "click" // forces the user to click the UI to go to next step
+ "eventElement": "[data-element='my-table-row'] [data-element='my-tour-button']" // specify an element to click inside a highlighted element
+ "elementPreventClick": false // prevents user interaction in the highlighted element
+ "backdropOpacity": 0.4 // the backdrop opacity
+ "view": "" // add a custom view
+ "customProperties" : {} // add any custom properties needed for the custom view
+}
+
+
+++ ++ ++{{vm.tour.name}}+ + + + + +
+ (function () {
+ "use strict";
+
+ function TourController(tourService) {
+
+ var vm = this;
+
+ vm.tour = {
+ "name": "My Custom Tour",
+ "alias": "myCustomTour",
+ "steps": [
+ {
+ "title": "Welcome to My Custom Tour",
+ "content": "",
+ "type": "intro"
+ },
+ {
+ "element": "[data-element='my-tour-button']",
+ "title": "Click the button",
+ "content": "Click the button",
+ "event": "click"
+ }
+ ]
+ };
+
+ vm.startTour = startTour;
+
+ function startTour() {
+ tourService.startTour(vm.tour);
+ }
+
+ }
+
+ angular.module("umbraco").controller("My.TourController", TourController);
+
+ })();
+
+
+++ ++ +++ + + ++ + ++ + + + + ++ + + ++ + +++ ++ +
+ (function () {
+ "use strict";
+
+ function StepController() {
+
+ var vm = this;
+
+ vm.initNextStep = initNextStep;
+
+ function initNextStep() {
+ // run logic here before going to the next step
+ $scope.model.nextStep();
+ }
+
+ }
+
+ angular.module("umbraco").controller("My.TourStep", StepController);
+
+ })();
+
+
+
+binding): Tour object
+
+**/
+
+(function () {
+ 'use strict';
+
+ function TourDirective($timeout, $http, $q, tourService, backdropService) {
+
+ function link(scope, el, attr, ctrl) {
+
+ var popover;
+ var pulseElement;
+ var pulseTimer;
+
+ scope.loadingStep = false;
+ scope.elementNotFound = false;
+
+ scope.model.nextStep = function() {
+ nextStep();
+ };
+
+ scope.model.endTour = function() {
+ unbindEvent();
+ tourService.endTour(scope.model);
+ backdropService.close();
+ };
+
+ scope.model.completeTour = function() {
+ unbindEvent();
+ tourService.completeTour(scope.model).then(function() {
+ backdropService.close();
+ });
+ };
+
+ scope.model.disableTour = function() {
+ unbindEvent();
+ tourService.disableTour(scope.model).then(function() {
+ backdropService.close();
+ });
+ }
+
+ function onInit() {
+ popover = el.find(".umb-tour__popover");
+ pulseElement = el.find(".umb-tour__pulse");
+ popover.hide();
+ scope.model.currentStepIndex = 0;
+ backdropService.open({disableEventsOnClick: true});
+ startStep();
+ }
+
+ function setView() {
+ if (scope.model.currentStep.view && scope.model.alias) {
+ //we do this to avoid a hidden dialog to start loading unconfigured views before the first activation
+ var configuredView = scope.model.currentStep.view;
+ if (scope.model.currentStep.view.indexOf(".html") === -1) {
+ var viewAlias = scope.model.currentStep.view.toLowerCase();
+ var tourAlias = scope.model.alias.toLowerCase();
+ configuredView = "views/common/tours/" + tourAlias + "/" + viewAlias + "/" + viewAlias + ".html";
+ }
+ if (configuredView !== scope.configuredView) {
+ scope.configuredView = configuredView;
+ }
+ } else {
+ scope.configuredView = null;
+ }
+ }
+
+ function nextStep() {
+
+ popover.hide();
+ pulseElement.hide();
+ $timeout.cancel(pulseTimer);
+ scope.model.currentStepIndex++;
+
+ // make sure we don't go too far
+ if(scope.model.currentStepIndex !== scope.model.steps.length) {
+ startStep();
+ // tour completed - final step
+ } else {
+ scope.loadingStep = true;
+
+ waitForPendingRerequests().then(function(){
+ scope.loadingStep = false;
+ // clear current step
+ scope.model.currentStep = {};
+ // set popover position to center
+ setPopoverPosition(null);
+ // remove backdrop hightlight and custom opacity
+ backdropService.setHighlight(null);
+ backdropService.setOpacity(null);
+ });
+ }
+ }
+
+ function startStep() {
+ scope.loadingStep = true;
+ backdropService.setOpacity(scope.model.steps[scope.model.currentStepIndex].backdropOpacity);
+ backdropService.setHighlight(null);
+
+ waitForPendingRerequests().then(function() {
+
+ scope.model.currentStep = scope.model.steps[scope.model.currentStepIndex];
+
+ setView();
+
+ // if highlight element is set - find it
+ findHighlightElement();
+
+ // if a custom event needs to be bound we do it now
+ if(scope.model.currentStep.event) {
+ bindEvent();
+ }
+
+ scope.loadingStep = false;
+
+ });
+ }
+
+ function findHighlightElement() {
+
+ scope.elementNotFound = false;
+
+ $timeout(function () {
+
+ // if an element isn't set - show the popover in the center
+ if(scope.model.currentStep && !scope.model.currentStep.element) {
+ setPopoverPosition(null);
+ return;
+ }
+
+ var element = angular.element(scope.model.currentStep.element);
+
+ // we couldn't find the element in the dom - abort and show error
+ if(element.length === 0) {
+ scope.elementNotFound = true;
+ setPopoverPosition(null);
+ return;
+ }
+
+ var scrollParent = element.scrollParent();
+ var scrollToCenterOfContainer = element[0].offsetTop - (scrollParent[0].clientHeight / 2 ) + (element[0].clientHeight / 2);
+
+ // Detect if scroll is needed
+ if (element[0].offsetTop > scrollParent[0].clientHeight) {
+ scrollParent.animate({
+ scrollTop: scrollToCenterOfContainer
+ }, function () {
+ // Animation complete.
+ setPopoverPosition(element);
+ setPulsePosition();
+ backdropService.setHighlight(scope.model.currentStep.element, scope.model.currentStep.elementPreventClick);
+ });
+ } else {
+ setPopoverPosition(element);
+ setPulsePosition();
+ backdropService.setHighlight(scope.model.currentStep.element, scope.model.currentStep.elementPreventClick);
+ }
+
+ });
+
+ }
+
+ function setPopoverPosition(element) {
+
+ $timeout(function () {
+
+ var position = "center";
+ var margin = 20;
+ var css = {};
+
+ var popoverWidth = popover.outerWidth();
+ var popoverHeight = popover.outerHeight();
+ var popoverOffset = popover.offset();
+ var documentWidth = angular.element(document).width();
+ var documentHeight = angular.element(document).height();
+
+ if(element) {
+
+ var offset = element.offset();
+ var width = element.outerWidth();
+ var height = element.outerHeight();
+
+ // messure available space on each side of the target element
+ var space = {
+ "top": offset.top,
+ "right": documentWidth - (offset.left + width),
+ "bottom": documentHeight - (offset.top + height),
+ "left": offset.left
+ };
+
+ // get the posistion with most available space
+ position = findMax(space);
+
+ if (position === "top") {
+ if (offset.left < documentWidth / 2) {
+ css.top = offset.top - popoverHeight - margin;
+ css.left = offset.left;
+ } else {
+ css.top = offset.top - popoverHeight - margin;
+ css.left = offset.left - popoverWidth + width;
+ }
+ }
+
+ if (position === "right") {
+ if (offset.top < documentHeight / 2) {
+ css.top = offset.top;
+ css.left = offset.left + width + margin;
+ } else {
+ css.top = offset.top + height - popoverHeight;
+ css.left = offset.left + width + margin;
+ }
+ }
+
+ if (position === "bottom") {
+ if (offset.left < documentWidth / 2) {
+ css.top = offset.top + height + margin;
+ css.left = offset.left;
+ } else {
+ css.top = offset.top + height + margin;
+ css.left = offset.left - popoverWidth + width;
+ }
+ }
+
+ if (position === "left") {
+ if (offset.top < documentHeight / 2) {
+ css.top = offset.top;
+ css.left = offset.left - popoverWidth - margin;
+ } else {
+ css.top = offset.top + height - popoverHeight;
+ css.left = offset.left - popoverWidth - margin;
+ }
+ }
+
+ } else {
+ // if there is no dom element center the popover
+ css.top = "calc(50% - " + popoverHeight/2 + "px)";
+ css.left = "calc(50% - " + popoverWidth/2 + "px)";
+ }
+
+ popover.css(css).fadeIn("fast");
+
+ });
+
+
+ }
+
+ function setPulsePosition() {
+ if(scope.model.currentStep.event) {
+
+ pulseTimer = $timeout(function(){
+
+ var clickElementSelector = scope.model.currentStep.eventElement ? scope.model.currentStep.eventElement : scope.model.currentStep.element;
+ var clickElement = $(clickElementSelector);
+
+ var offset = clickElement.offset();
+ var width = clickElement.outerWidth();
+ var height = clickElement.outerHeight();
+
+ pulseElement.css({ "width": width, "height": height, "left": offset.left, "top": offset.top });
+ pulseElement.fadeIn();
+
+ }, 1000);
+ }
+ }
+
+ function waitForPendingRerequests() {
+ var deferred = $q.defer();
+ var timer = window.setInterval(function(){
+ // check for pending requests both in angular and on the document
+ if($http.pendingRequests.length === 0 && document.readyState === "complete") {
+ $timeout(function(){
+ deferred.resolve();
+ clearInterval(timer);
+ });
+ }
+ }, 50);
+ return deferred.promise;
+ }
+
+ function findMax(obj) {
+ var keys = Object.keys(obj);
+ var max = keys[0];
+ for (var i = 1, n = keys.length; i < n; ++i) {
+ var k = keys[i];
+ if (obj[k] > obj[max]) {
+ max = k;
+ }
+ }
+ return max;
+ }
+
+ function bindEvent() {
+
+ var bindToElement = scope.model.currentStep.element;
+ var eventName = scope.model.currentStep.event + ".step-" + scope.model.currentStepIndex;
+ var removeEventName = "remove.step-" + scope.model.currentStepIndex;
+ var handled = false;
+
+ if(scope.model.currentStep.eventElement) {
+ bindToElement = scope.model.currentStep.eventElement;
+ }
+
+ $(bindToElement).on(eventName, function(){
+ if(!handled) {
+ unbindEvent();
+ nextStep();
+ handled = true;
+ }
+ });
+
+ // Hack: we do this to handle cases where ng-if is used and removes the element we need to click.
+ // for some reason it seems the elements gets removed before the event is raised. This is a temp solution which assumes:
+ // "if you ask me to click on an element, and it suddenly gets removed from the dom, let's go on to the next step".
+ $(bindToElement).on(removeEventName, function () {
+ if(!handled) {
+ unbindEvent();
+ nextStep();
+ handled = true;
+ }
+ });
+
+ }
+
+ function unbindEvent() {
+ var eventName = scope.model.currentStep.event + ".step-" + scope.model.currentStepIndex;
+ var removeEventName = "remove.step-" + scope.model.currentStepIndex;
+
+ if(scope.model.currentStep.eventElement) {
+ angular.element(scope.model.currentStep.eventElement).off(eventName);
+ angular.element(scope.model.currentStep.eventElement).off(removeEventName);
+ } else {
+ angular.element(scope.model.currentStep.element).off(eventName);
+ angular.element(scope.model.currentStep.element).off(removeEventName);
+ }
+ }
+
+ function resize() {
+ findHighlightElement();
+ }
+
+ onInit();
+
+ $(window).on('resize.umbTour', resize);
+
+ scope.$on('$destroy', function () {
+ $(window).off('resize.umbTour');
+ unbindEvent();
+ $timeout.cancel(pulseTimer);
+ });
+
+ }
+
+ var directive = {
+ transclude: true,
+ restrict: 'E',
+ replace: true,
+ templateUrl: 'views/components/application/umb-tour.html',
+ link: link,
+ scope: {
+ model: "="
+ }
+ };
+
+ return directive;
+
+ }
+
+ angular.module('umbraco.directives').directive('umbTour', TourDirective);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js
new file mode 100644
index 0000000000..08e0a44c0b
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js
@@ -0,0 +1,35 @@
+(function() {
+ 'use strict';
+
+ function TourStepDirective() {
+
+ function link(scope, element, attrs, ctrl) {
+
+ scope.close = function() {
+ if(scope.onClose) {
+ scope.onClose();
+ }
+ }
+
+ }
+
+ var directive = {
+ restrict: 'E',
+ replace: true,
+ transclude: true,
+ templateUrl: 'views/components/application/umbtour/umb-tour-step.html',
+ scope: {
+ size: "@?",
+ onClose: "&?",
+ hideClose: "=?"
+ },
+ link: link
+ };
+
+ return directive;
+
+ }
+
+ angular.module('umbraco.directives').directive('umbTourStep', TourStepDirective);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js
new file mode 100644
index 0000000000..52ed358b61
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js
@@ -0,0 +1,22 @@
+(function() {
+ 'use strict';
+
+ function TourStepContentDirective() {
+
+ var directive = {
+ restrict: 'E',
+ replace: true,
+ transclude: true,
+ templateUrl: 'views/components/application/umbtour/umb-tour-step-content.html',
+ scope: {
+ content: "="
+ }
+ };
+
+ return directive;
+
+ }
+
+ angular.module('umbraco.directives').directive('umbTourStepContent', TourStepContentDirective);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js
new file mode 100644
index 0000000000..7e04ef5d00
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js
@@ -0,0 +1,22 @@
+(function() {
+ 'use strict';
+
+ function TourStepCounterDirective() {
+
+ var directive = {
+ restrict: 'E',
+ replace: true,
+ templateUrl: 'views/components/application/umbtour/umb-tour-step-counter.html',
+ scope: {
+ currentStep: "=",
+ totalSteps: "="
+ }
+ };
+
+ return directive;
+
+ }
+
+ angular.module('umbraco.directives').directive('umbTourStepCounter', TourStepCounterDirective);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js
new file mode 100644
index 0000000000..fedb527972
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js
@@ -0,0 +1,19 @@
+(function() {
+ 'use strict';
+
+ function TourStepFooterDirective() {
+
+ var directive = {
+ restrict: 'E',
+ replace: true,
+ transclude: true,
+ templateUrl: 'views/components/application/umbtour/umb-tour-step-footer.html'
+ };
+
+ return directive;
+
+ }
+
+ angular.module('umbraco.directives').directive('umbTourStepFooter', TourStepFooterDirective);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js
new file mode 100644
index 0000000000..9d32ad87a4
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js
@@ -0,0 +1,22 @@
+(function() {
+ 'use strict';
+
+ function TourStepHeaderDirective() {
+
+ var directive = {
+ restrict: 'E',
+ replace: true,
+ transclude: true,
+ templateUrl: 'views/components/application/umbtour/umb-tour-step-header.html',
+ scope: {
+ title: "="
+ }
+ };
+
+ return directive;
+
+ }
+
+ angular.module('umbraco.directives').directive('umbTourStepHeader', TourStepHeaderDirective);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js
index b503648317..4b0bdb6c71 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js
@@ -149,7 +149,8 @@ Use this directive to render an umbraco button. The directive can be used to gen
labelKey: "@?",
icon: "@?",
disabled: "=",
- size: "@?"
+ size: "@?",
+ alias: "@?"
}
};
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
index 7d60a83b89..1ecb2d7403 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
@@ -1,7 +1,9 @@
(function () {
'use strict';
- function ContentEditController($rootScope, $scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) {
+ function ContentEditController($rootScope, $scope, $routeParams, $q, $timeout, $window, $location, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http, eventsService, relationResource) {
+
+ var evts = [];
//setup scope vars
$scope.defaultButton = null;
@@ -14,22 +16,13 @@
$scope.page.menu.currentSection = appState.getSectionState("currentSection");
$scope.page.listViewPath = null;
$scope.page.isNew = $scope.isNew ? true : false;
- $scope.page.buttonGroupState = "init";
+ $scope.page.buttonGroupState = "init";
+ $scope.allowOpen = true;
+
function init(content) {
- var buttons = contentEditingHelper.configureContentEditorButtons({
- create: $scope.page.isNew,
- content: content,
- methods: {
- saveAndPublish: $scope.saveAndPublish,
- sendToPublish: $scope.sendToPublish,
- save: $scope.save,
- unPublish: $scope.unPublish
- }
- });
- $scope.defaultButton = buttons.defaultButton;
- $scope.subButtons = buttons.subButtons;
+ createButtons(content);
editorState.set($scope.content);
@@ -42,6 +35,70 @@
});
}
}
+
+ evts.push(eventsService.on("editors.content.changePublishDate", function (event, args) {
+ createButtons(args.node);
+ }));
+
+ evts.push(eventsService.on("editors.content.changeUnpublishDate", function (event, args) {
+ createButtons(args.node);
+ }));
+
+ // 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);
+
+ }
+
+ function getNode() {
+
+ $scope.page.loading = true;
+
+ //we are editing so get the content item from the server
+ $scope.getMethod()($scope.contentId)
+ .then(function (data) {
+
+ $scope.content = data;
+
+ if (data.isChildOfListView && data.trashed === false) {
+ $scope.page.listViewPath = ($routeParams.page) ?
+ "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page :
+ "/content/content/edit/" + data.parentId;
+ }
+
+ 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();
+
+ syncTreeNode($scope.content, data.path, true);
+
+ resetLastListPageNumber($scope.content);
+
+ $scope.page.loading = false;
+
+ });
+
+ }
+
+ function createButtons(content) {
+ $scope.page.buttonGroupState = "init";
+ var buttons = contentEditingHelper.configureContentEditorButtons({
+ create: $scope.page.isNew,
+ content: content,
+ methods: {
+ saveAndPublish: $scope.saveAndPublish,
+ sendToPublish: $scope.sendToPublish,
+ save: $scope.save,
+ unPublish: $scope.unPublish
+ }
+ });
+
+ $scope.defaultButton = buttons.defaultButton;
+ $scope.subButtons = buttons.subButtons;
+
}
/** Syncs the content item to it's tree node - this occurs on first load and after saving */
@@ -130,35 +187,8 @@
}
else {
- $scope.page.loading = true;
+ getNode();
- //we are editing so get the content item from the server
- $scope.getMethod()($scope.contentId)
- .then(function (data) {
-
- $scope.content = data;
-
- if (data.isChildOfListView && data.trashed === false) {
- $scope.page.listViewPath = ($routeParams.page) ?
- "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page :
- "/content/content/edit/" + data.parentId;
- }
-
- 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();
-
- syncTreeNode($scope.content, data.path, true);
-
- resetLastListPageNumber($scope.content);
-
- $scope.page.loading = false;
-
- });
}
@@ -185,6 +215,8 @@
$scope.page.buttonGroupState = "success";
+ }, function(err) {
+ $scope.page.buttonGroupState = 'error';
});
}
@@ -208,9 +240,9 @@
if (!$scope.busy) {
// Chromes popup blocker will kick in if a window is opened
- // outwith the initial scoped request. This trick will fix that.
+ // without the initial scoped request. This trick will fix that.
//
- var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview');
+ var previewWindow = $window.open('preview/?init=true&id=' + content.id, 'umbpreview');
// Build the correct path so both /#/ and #/ work.
var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + content.id;
@@ -230,6 +262,87 @@
};
+ $scope.restore = function (content) {
+
+ $scope.page.buttonRestore = "busy";
+
+ relationResource.getByChildId(content.id, "relateParentDocumentOnDelete").then(function (data) {
+
+ var relation = null;
+ var target = null;
+ var error = { headline: "Cannot automatically restore this item", content: "Use the Move menu item to move it manually"};
+
+ if (data.length == 0) {
+ notificationsService.error(error.headline, "There is no 'restore' relation found for this node. Use the Move menu item to move it manually.");
+ $scope.page.buttonRestore = "error";
+ return;
+ }
+
+ relation = data[0];
+
+ if (relation.parentId == -1) {
+ target = { id: -1, name: "Root" };
+ moveNode(content, target);
+ } else {
+ contentResource.getById(relation.parentId).then(function (data) {
+ target = data;
+
+ // make sure the target item isn't in the recycle bin
+ if(target.path.indexOf("-20") !== -1) {
+ notificationsService.error(error.headline, "The item you want to restore it under (" + target.name + ") is in the recycle bin. Use the Move menu item to move the item manually.");
+ $scope.page.buttonRestore = "error";
+ return;
+ }
+
+ moveNode(content, target);
+
+ }, function (err) {
+ $scope.page.buttonRestore = "error";
+ notificationsService.error(error.headline, error.content);
+ });
+ }
+
+ }, function (err) {
+ $scope.page.buttonRestore = "error";
+ notificationsService.error(error.headline, error.content);
+ });
+
+
+ };
+
+ function moveNode(node, target) {
+
+ contentResource.move({ "parentId": target.id, "id": node.id })
+ .then(function (path) {
+
+ // remove the node that we're working on
+ if($scope.page.menu.currentNode) {
+ treeService.removeNode($scope.page.menu.currentNode);
+ }
+
+ // sync the destination node
+ navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false });
+
+ $scope.page.buttonRestore = "success";
+ notificationsService.success("Successfully restored " + node.name + " to " + target.name);
+
+ // reload the node
+ getNode();
+
+ }, function (err) {
+ $scope.page.buttonRestore = "error";
+ notificationsService.error("Cannot automatically restore this item", err);
+ });
+
+ }
+
+ //ensure to unregister from all events!
+ $scope.$on('$destroy', function () {
+ for (var e in evts) {
+ eventsService.unsubscribe(evts[e]);
+ }
+ });
+
}
function createDirective() {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js
new file mode 100644
index 0000000000..2ce0c5a22e
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js
@@ -0,0 +1,293 @@
+(function () {
+ 'use strict';
+
+ function ContentNodeInfoDirective($timeout, $location, logResource, eventsService, userService, localizationService, dateHelper) {
+
+ function link(scope, element, attrs, ctrl) {
+
+ var evts = [];
+ var isInfoTab = false;
+ scope.publishStatus = {};
+
+ function onInit() {
+
+ scope.allowOpen = true;
+
+ scope.datePickerConfig = {
+ pickDate: true,
+ pickTime: true,
+ useSeconds: false,
+ format: "YYYY-MM-DD HH:mm",
+ icons: {
+ time: "icon-time",
+ date: "icon-calendar",
+ up: "icon-chevron-up",
+ down: "icon-chevron-down"
+ }
+ };
+
+ scope.auditTrailOptions = {
+ "id": scope.node.id
+ };
+
+ // get available templates
+ scope.availableTemplates = scope.node.allowedTemplates;
+
+ // get document type details
+ scope.documentType = scope.node.documentType;
+
+ // make sure dates are formatted to the user's locale
+ formatDatesToLocal();
+
+ setNodePublishStatus(scope.node);
+
+ }
+
+ scope.auditTrailPageChange = function (pageNumber) {
+ scope.auditTrailOptions.pageNumber = pageNumber;
+ loadAuditTrail();
+ };
+
+ scope.openDocumentType = function (documentType) {
+ var url = "/settings/documenttypes/edit/" + documentType.id;
+ $location.url(url);
+ };
+
+ scope.updateTemplate = function (templateAlias) {
+
+ // update template value
+ scope.node.template = templateAlias;
+
+ };
+
+ scope.datePickerChange = function (event, type) {
+ if (type === 'publish') {
+ setPublishDate(event.date.format("YYYY-MM-DD HH:mm"));
+ } else if (type === 'unpublish') {
+ setUnpublishDate(event.date.format("YYYY-MM-DD HH:mm"));
+ }
+ };
+
+ scope.clearPublishDate = function () {
+ clearPublishDate();
+ };
+
+ scope.clearUnpublishDate = function () {
+ clearUnpublishDate();
+ };
+
+ function loadAuditTrail() {
+
+ scope.loadingAuditTrail = true;
+
+ logResource.getPagedEntityLog(scope.auditTrailOptions)
+ .then(function (data) {
+
+ // get current backoffice user and format dates
+ userService.getCurrentUser().then(function (currentUser) {
+ angular.forEach(data.items, function(item) {
+ item.timestampFormatted = dateHelper.getLocalDate(item.timestamp, currentUser.locale, 'LLL');
+ });
+ });
+
+ scope.auditTrail = data.items;
+ scope.auditTrailOptions.pageNumber = data.pageNumber;
+ scope.auditTrailOptions.pageSize = data.pageSize;
+ scope.auditTrailOptions.totalItems = data.totalItems;
+ scope.auditTrailOptions.totalPages = data.totalPages;
+
+ setAuditTrailLogTypeColor(scope.auditTrail);
+
+ scope.loadingAuditTrail = false;
+ });
+
+ }
+
+ function setAuditTrailLogTypeColor(auditTrail) {
+ angular.forEach(auditTrail, function (item) {
+ switch (item.logType) {
+ case "Publish":
+ item.logTypeColor = "success";
+ break;
+ case "UnPublish":
+ case "Delete":
+ item.logTypeColor = "danger";
+ break;
+ default:
+ item.logTypeColor = "gray";
+ }
+ });
+ }
+
+ function setNodePublishStatus(node) {
+
+ // deleted node
+ if(node.trashed === true) {
+ scope.publishStatus.label = localizationService.localize("general_deleted");
+ scope.publishStatus.color = "danger";
+ }
+
+ // unpublished node
+ if(node.published === false && node.trashed === false) {
+ scope.publishStatus.label = localizationService.localize("content_unpublished");
+ scope.publishStatus.color = "gray";
+ }
+
+ // published node
+ if(node.hasPublishedVersion === true && node.publishDate && node.published === true) {
+ scope.publishStatus.label = localizationService.localize("content_published");
+ scope.publishStatus.color = "success";
+ }
+
+ // published node with pending changes
+ if(node.hasPublishedVersion === true && node.publishDate && node.published === false) {
+ scope.publishStatus.label = localizationService.localize("content_publishedPendingChanges");
+ scope.publishStatus.color = "success"
+ }
+
+ }
+
+ function setPublishDate(date) {
+
+ if (!date) {
+ return;
+ }
+
+ //The date being passed in here is the user's local date/time that they have selected
+ //we need to convert this date back to the server date on the model.
+
+ var serverTime = dateHelper.convertToServerStringTime(moment(date), Umbraco.Sys.ServerVariables.application.serverTimeOffset);
+
+ // update publish value
+ scope.node.releaseDate = serverTime;
+
+ // make sure dates are formatted to the user's locale
+ formatDatesToLocal();
+
+ // emit event
+ var args = { node: scope.node, date: date };
+ eventsService.emit("editors.content.changePublishDate", args);
+
+ }
+
+ function clearPublishDate() {
+
+ // update publish value
+ scope.node.releaseDate = null;
+
+ // emit event
+ var args = { node: scope.node, date: null };
+ eventsService.emit("editors.content.changePublishDate", args);
+
+ }
+
+ function setUnpublishDate(date) {
+
+ if (!date) {
+ return;
+ }
+
+ //The date being passed in here is the user's local date/time that they have selected
+ //we need to convert this date back to the server date on the model.
+
+ var serverTime = dateHelper.convertToServerStringTime(moment(date), Umbraco.Sys.ServerVariables.application.serverTimeOffset);
+
+ // update publish value
+ scope.node.removeDate = serverTime;
+
+ // make sure dates are formatted to the user's locale
+ formatDatesToLocal();
+
+ // emit event
+ var args = { node: scope.node, date: date };
+ eventsService.emit("editors.content.changeUnpublishDate", args);
+
+ }
+
+ function clearUnpublishDate() {
+
+ // update publish value
+ scope.node.removeDate = null;
+
+ // emit event
+ var args = { node: scope.node, date: null };
+ eventsService.emit("editors.content.changeUnpublishDate", args);
+
+ }
+
+ function ucfirst(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+ }
+
+ function formatDatesToLocal() {
+ // get current backoffice user and format dates
+ userService.getCurrentUser().then(function (currentUser) {
+ scope.node.createDateFormatted = dateHelper.getLocalDate(scope.node.createDate, currentUser.locale, 'LLL');
+
+ scope.node.releaseDateYear = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'YYYY')) : null;
+ scope.node.releaseDateMonth = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'MMMM')) : null;
+ scope.node.releaseDateDayNumber = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'DD')) : null;
+ scope.node.releaseDateDay = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'dddd')) : null;
+ scope.node.releaseDateTime = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'HH:mm')) : null;
+
+ scope.node.removeDateYear = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'YYYY')) : null;
+ scope.node.removeDateMonth = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'MMMM')) : null;
+ scope.node.removeDateDayNumber = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'DD')) : null;
+ scope.node.removeDateDay = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'dddd')) : null;
+ scope.node.removeDateTime = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'HH:mm')) : null;
+ });
+ }
+
+ // load audit trail when on the info tab
+ evts.push(eventsService.on("app.tabChange", function (event, args) {
+ $timeout(function(){
+ if (args.id === -1) {
+ isInfoTab = true;
+ loadAuditTrail();
+ } else {
+ isInfoTab = false;
+ }
+ });
+ }));
+
+ // watch for content updates - reload content when node is saved, published etc.
+ scope.$watch('node.updateDate', function(newValue, oldValue){
+
+ if(!newValue) { return; }
+ if(newValue === oldValue) { return; }
+
+ if(isInfoTab) {
+ loadAuditTrail();
+ formatDatesToLocal();
+ setNodePublishStatus(scope.node);
+ }
+ });
+
+ //ensure to unregister from all events!
+ scope.$on('$destroy', function () {
+ for (var e in evts) {
+ eventsService.unsubscribe(evts[e]);
+ }
+ });
+
+ onInit();
+
+ }
+
+ var directive = {
+ restrict: 'E',
+ replace: true,
+ templateUrl: 'views/components/content/umb-content-node-info.html',
+ scope: {
+ node: "="
+ },
+ link: link
+ };
+
+ return directive;
+
+ }
+
+ angular.module('umbraco.directives').directive('umbContentNodeInfo', ContentNodeInfoDirective);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js
index de497ccffe..6f4111373d 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js
@@ -156,7 +156,7 @@ angular.module('umbraco.directives')
if(els.indexOf(el) >= 0){return;}
// ignore clicks on new overlay
- var parents = $(event.target).parents("a,button,.umb-overlay");
+ var parents = $(event.target).parents("a,button,.umb-overlay,.umb-tour");
if(parents.length > 0){
return;
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
index e3a50b482c..90daac73db 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js
@@ -115,7 +115,9 @@ angular.module("umbraco.directives")
toolbar: toolbar,
content_css: stylesheets,
style_formats: styleFormats,
- autoresize_bottom_margin: 0
+ autoresize_bottom_margin: 0,
+ //see http://archive.tinymce.com/wiki.php/Configuration:cache_suffix
+ cache_suffix: "?umb__rnd=" + Umbraco.Sys.ServerVariables.application.cacheBuster
};
@@ -366,6 +368,9 @@ angular.module("umbraco.directives")
// element might still be there even after the modal has been hidden.
scope.$on('$destroy', function () {
unsubscribe();
+ if (tinyMceEditor !== undefined && tinyMceEditor != null) {
+ tinyMceEditor.destroy()
+ }
});
});
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js
new file mode 100644
index 0000000000..1aa026cb3a
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js
@@ -0,0 +1,67 @@
+(function () {
+ 'use strict';
+
+ function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper) {
+
+ function link(scope, element, attrs, ctrl) {
+
+ var evts = [];
+
+ function onInit() {
+ scope.allowOpenMediaType = true;
+ // get document type details
+ scope.mediaType = scope.node.contentType;
+ // get node url
+ scope.nodeUrl = scope.node.mediaLink;
+ // make sure dates are formatted to the user's locale
+ formatDatesToLocal();
+ }
+
+ function formatDatesToLocal() {
+ // get current backoffice user and format dates
+ userService.getCurrentUser().then(function (currentUser) {
+ scope.node.createDateFormatted = dateHelper.getLocalDate(scope.node.createDate, currentUser.locale, 'LLL');
+ scope.node.updateDateFormatted = dateHelper.getLocalDate(scope.node.updateDate, currentUser.locale, 'LLL');
+ });
+ }
+
+ scope.openMediaType = function (mediaType) {
+ // remove first "#" from url if it is prefixed else the path won't work
+ var url = "/settings/mediaTypes/edit/" + mediaType.id;
+ $location.path(url);
+ };
+
+ // watch for content updates - reload content when node is saved, published etc.
+ scope.$watch('node.updateDate', function(newValue, oldValue){
+ if(!newValue) { return; }
+ if(newValue === oldValue) { return; }
+ formatDatesToLocal();
+ });
+
+ //ensure to unregister from all events!
+ scope.$on('$destroy', function () {
+ for (var e in evts) {
+ eventsService.unsubscribe(evts[e]);
+ }
+ });
+
+ onInit();
+
+ }
+
+ var directive = {
+ restrict: 'E',
+ replace: true,
+ templateUrl: 'views/components/media/umb-media-node-info.html',
+ scope: {
+ node: "="
+ },
+ link: link
+ };
+
+ return directive;
+ }
+
+ angular.module('umbraco.directives').directive('umbMediaNodeInfo', MediaNodeInfoDirective);
+
+})();
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js
index d728865015..6e705da52d 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js
@@ -59,7 +59,6 @@
| Set the title of the overlay. | |||
| model.subTitle | +model.subtitle | String | Set the subtitle of the overlay. |
|
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js
index 799cc5894c..00e6c6edb4 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js
@@ -30,15 +30,14 @@
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) {
@@ -87,6 +86,27 @@
$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();
}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js
index 53424a7d6e..7a3abdd0e6 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js
@@ -46,6 +46,7 @@ function MarkdownEditorController($scope, $element, assetsService, dialogService
// 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();
@@ -59,7 +60,12 @@ function MarkdownEditorController($scope, $element, assetsService, dialogService
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()) {
- angularHelper.getCurrentForm($scope).$setDirty();
+ 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();
}
});
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js
index 8a7b20498d..bf7462942a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js
@@ -1,7 +1,7 @@
//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, mediaResource, mediaHelper, $timeout, userService, $location) {
+ function ($rootScope, $scope, dialogService, entityResource, mediaResource, 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;
@@ -19,6 +19,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
$scope.images = [];
$scope.ids = [];
+ $scope.isMultiPicker = multiPicker;
+
if ($scope.model.value) {
var ids = $scope.model.value.split(',');
@@ -26,17 +28,47 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
// 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 metatData property to get at the thumbnail
+ // 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) {
+ entityResource.getByIds(ids, "Media").then(function(medias) {
- _.each(medias, function (media, i) {
+ // 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
+ };
+ }
+ });
- //only show non-trashed items
- if (media.parentId >= -1) {
-
- if (!media.thumbnail) {
+ _.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);
}
@@ -44,12 +76,10 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
if ($scope.model.config.idType === "udi") {
$scope.ids.push(media.udi);
- }
- else {
+ } else {
$scope.ids.push(media.id);
}
- }
- });
+ });
$scope.sync();
});
@@ -82,8 +112,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
submit: function(model) {
_.each(model.selectedImages, function(media, i) {
-
- if (!media.thumbnail) {
+ // 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);
}
@@ -101,17 +131,18 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
$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
+ // 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.images, function(value, key) {
@@ -142,5 +173,4 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl
//update the display val again if it has changed from the server
setupViewModel();
};
-
});
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html
index f3aab992dd..7607a9391d 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html
@@ -1,47 +1,48 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js
index 9b4fb217eb..51c5baf55b 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js
@@ -86,6 +86,10 @@
};
$scope.add = function ($event) {
+ if (!angular.isArray($scope.model.value)) {
+ $scope.model.value = [];
+ }
+
if ($scope.newCaption == "") {
$scope.hasError = true;
} else {
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js
index d17310d65a..0a6bededae 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js
@@ -139,6 +139,7 @@ angular.module("umbraco")
//wait for queue to end
$q.all(await).then(function () {
+
//create a baseline Config to exten upon
var baseLineConfigObj = {
mode: "exact",
@@ -149,14 +150,16 @@ angular.module("umbraco")
extended_valid_elements: extendedValidElements,
menubar: false,
statusbar: false,
+ relative_urls: false,
height: editorConfig.dimensions.height,
width: editorConfig.dimensions.width,
maxImageSize: editorConfig.maxImageSize,
toolbar: toolbar,
content_css: stylesheets,
- relative_urls: false,
style_formats: styleFormats,
- language: language
+ language: language,
+ //see http://archive.tinymce.com/wiki.php/Configuration:cache_suffix
+ cache_suffix: "?umb__rnd=" + Umbraco.Sys.ServerVariables.application.cacheBuster
};
if (tinyMceConfig.customConfig) {
@@ -356,7 +359,8 @@ angular.module("umbraco")
//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;
- tinyMceEditor.setContent(newVal, { format: 'raw' });
+ //uses an empty string in the editor when the value is null
+ tinyMceEditor.setContent(newVal || "", { format: 'raw' });
//we need to manually fire this event since it is only ever fired based on loading from the DOM, this
// is required for our plugins listening to this event to execute
tinyMceEditor.fire('LoadContent', null);
@@ -374,6 +378,9 @@ angular.module("umbraco")
// element might still be there even after the modal has been hidden.
$scope.$on('$destroy', function () {
unsubscribe();
+ if (tinyMceEditor !== undefined && tinyMceEditor != null) {
+ tinyMceEditor.destroy()
+ }
});
});
});
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js
index 702d19a509..65a62f599c 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js
@@ -76,6 +76,7 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController",
icon.isCustom = false;
break;
case "styleselect":
+ case "fontsizeselect":
icon.name = "icon-list";
icon.isCustom = true;
break;
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/sensitivevalue/sensitivevalue.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/sensitivevalue/sensitivevalue.html
new file mode 100644
index 0000000000..6460d882b2
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/sensitivevalue/sensitivevalue.html
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js
index 404c311d8b..321ac13555 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js
@@ -92,7 +92,6 @@
$scope.model.config.ticksPositions = _.map($scope.model.config.ticksPositions.split(','), function (item) {
return parseInt(item.trim());
});
- console.log($scope.model.config.ticksPositions);
}
if (!$scope.model.config.ticksLabels) {
@@ -215,4 +214,4 @@
assetsService.loadCss("lib/slider/bootstrap-slider.css");
assetsService.loadCss("lib/slider/bootstrap-slider-custom.css");
}
-angular.module("umbraco").controller("Umbraco.PropertyEditors.SliderController", sliderController);
\ No newline at end of file
+angular.module("umbraco").controller("Umbraco.PropertyEditors.SliderController", sliderController);
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js
index 3bb909c717..734903e46c 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js
@@ -1,37 +1,42 @@
function textboxController($scope) {
-
// macro parameter editor doesn't contains a config object,
- // so we create a new one to hold any properties
+ // so we create a new one to hold any properties
if (!$scope.model.config) {
$scope.model.config = {};
}
- if (!$scope.model.config.maxChars) {
- $scope.model.config.maxChars = false;
- }
-
$scope.model.maxlength = false;
if ($scope.model.config && $scope.model.config.maxChars) {
$scope.model.maxlength = true;
- if($scope.model.value == undefined) {
+ }
+
+ if (!$scope.model.config.maxChars) {
+ // 500 is the maximum number that can be stored
+ // in the database, so set it to the max, even
+ // if no max is specified in the config
+ $scope.model.config.maxChars = 500;
+ }
+
+ if ($scope.model.maxlength) {
+ if ($scope.model.value === undefined) {
$scope.model.count = ($scope.model.config.maxChars * 1);
} else {
$scope.model.count = ($scope.model.config.maxChars * 1) - $scope.model.value.length;
}
}
- $scope.model.change = function() {
+ $scope.model.change = function () {
if ($scope.model.config && $scope.model.config.maxChars) {
- if($scope.model.value == undefined) {
+ if ($scope.model.value === undefined) {
$scope.model.count = ($scope.model.config.maxChars * 1);
} else {
$scope.model.count = ($scope.model.config.maxChars * 1) - $scope.model.value.length;
}
- if($scope.model.count < 0) {
+ if ($scope.model.count < 0) {
$scope.model.value = $scope.model.value.substring(0, ($scope.model.config.maxChars * 1));
$scope.model.count = 0;
}
}
}
}
-angular.module('umbraco').controller("Umbraco.PropertyEditors.textboxController", textboxController);
\ No newline at end of file
+angular.module('umbraco').controller("Umbraco.PropertyEditors.textboxController", textboxController);
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html
index d8c51ce9e0..77656ac618 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html
@@ -4,11 +4,11 @@
val-server="value"
ng-required="model.validation.mandatory"
ng-trim="false"
- ng-keyup="model.change()" />
+ ng-keyup="model.change()" />
- {{model.count}}
-
-
\ No newline at end of file
+
+ {{model.count}}
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/scripts/edit.html b/src/Umbraco.Web.UI.Client/src/views/scripts/edit.html
index 4c58356732..9b03935efc 100644
--- a/src/Umbraco.Web.UI.Client/src/views/scripts/edit.html
+++ b/src/Umbraco.Web.UI.Client/src/views/scripts/edit.html
@@ -1,4 +1,4 @@
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html
index 2d59b644d0..279cdc538d 100644
--- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html
+++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html
@@ -1,4 +1,4 @@
-
+
@@ -113,6 +118,7 @@
+
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html
index 931ab64775..efb88f6297 100644
--- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html
+++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html
@@ -53,6 +53,7 @@
-
@@ -408,33 +31,30 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js
index 96ebf4f07b..e60fd4be74 100644
--- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js
@@ -1,7 +1,7 @@
(function () {
"use strict";
- function UsersController($scope, $timeout, $location, usersResource, userGroupsResource, localizationService, contentEditingHelper, usersHelper, formHelper, notificationsService, dateHelper) {
+ function UsersController($scope, $timeout, $location, $routeParams, usersResource, userGroupsResource, userService, localizationService, contentEditingHelper, usersHelper, formHelper, notificationsService, dateHelper) {
var vm = this;
var localizeSaving = localizationService.localize("general_saving");
@@ -20,6 +20,19 @@
{ label: "Oldest", key: "CreateDate", direction: "Ascending" },
{ label: "Last login", key: "LastLoginDate", direction: "Descending" }
];
+
+ angular.forEach(vm.userSortData, function (userSortData) {
+ var key = "user_sort" + userSortData.key + userSortData.direction;
+ localizationService.localize(key).then(function (value) {
+ var reg = /^\[[\S\s]*]$/g;
+ var result = reg.test(value);
+ if (result === false) {
+ // Only translate if key exists
+ userSortData.label = value;
+ }
+ });
+ });
+
vm.userStatesFilter = [];
vm.newUser.userGroups = [];
vm.usersViewState = 'overview';
@@ -111,6 +124,13 @@
vm.usersOptions.orderBy = "Name";
vm.usersOptions.orderDirection = "Ascending";
+ if ($routeParams.create) {
+ setUsersViewState("createUser");
+ }
+ else if ($routeParams.invite) {
+ setUsersViewState("inviteUser");
+ }
+
// Get users
getUsers();
@@ -155,6 +175,17 @@
if (state === "createUser") {
clearAddUserForm();
+
+ $location.search("create", "true");
+ $location.search("invite", null);
+ }
+ else if (state === "inviteUser") {
+ $location.search("create", null);
+ $location.search("invite", "true");
+ }
+ else if (state === "overview") {
+ $location.search("create", null);
+ $location.search("invite", null);
}
vm.usersViewState = state;
@@ -284,10 +315,10 @@
vm.selectedBulkUserGroups = _.clone(firstSelectedUser.userGroups);
vm.userGroupPicker = {
- title: "Select user groups",
+ title: localizationService.localize("user_selectUserGroups"),
view: "usergrouppicker",
selection: vm.selectedBulkUserGroups,
- closeButtonLabel: "Cancel",
+ closeButtonLabel: localizationService.localize("general_cancel"),
show: true,
submit: function (model) {
usersResource.setUserGroupsOnUsers(model.selection, vm.selection).then(function (data) {
@@ -320,10 +351,10 @@
function openUserGroupPicker(event) {
vm.userGroupPicker = {
- title: "Select user groups",
+ title: localizationService.localize("user_selectUserGroups"),
view: "usergrouppicker",
selection: vm.newUser.userGroups,
- closeButtonLabel: "Cancel",
+ closeButtonLabel: localizationService.localize("general_cancel"),
show: true,
submit: function (model) {
// apply changes
@@ -583,7 +614,10 @@
dateVal = moment(user.lastLoginDate, "YYYY-MM-DD HH:mm:ss");
}
- user.formattedLastLogin = dateVal.format("MMMM Do YYYY, HH:mm");
+ // get current backoffice user and format date
+ userService.getCurrentUser().then(function (currentUser) {
+ user.formattedLastLogin = dateVal.locale(currentUser.locale).format("LLL");
+ });
}
});
}
diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js
index 1c7b99cc01..e9e41f9073 100644
--- a/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js
+++ b/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js
@@ -1,6 +1,6 @@
describe('edit media controller tests', function () {
var scope, controller, routeParams, httpBackend;
- routeParams = {id: 1234, create: false};
+ routeParams = { id: 1234, create: false };
beforeEach(module('umbraco'));
@@ -11,7 +11,7 @@ describe('edit media controller tests', function () {
httpBackend = $httpBackend;
scope = $rootScope.$new();
-
+
//have the contentMocks register its expect urls on the httpbackend
//see /mocks/content.mocks.js for how its setup
mediaMocks.register();
@@ -20,7 +20,7 @@ describe('edit media controller tests', function () {
//this controller requires an angular form controller applied to it
scope.contentForm = angularHelper.getNullForm("contentForm");
-
+
controller = $controller('Umbraco.Editors.Media.EditController', {
$scope: scope,
$routeParams: routeParams
@@ -37,8 +37,8 @@ describe('edit media controller tests', function () {
}));
describe('media edit controller save', function () {
-
- it('it should have an media object', function() {
+
+ it('it should have an media object', function () {
//controller should have a content object
expect(scope.content).not.toBeUndefined();
@@ -48,22 +48,29 @@ describe('edit media controller tests', function () {
});
it('it should have a tabs collection', function () {
- expect(scope.content.tabs.length).toBe(1);
+ expect(scope.content.tabs.length).toBe(2);
});
- it('it should have a properties collection on each tab', function () {
- $(scope.content.tabs).each(function(i, tab){
- expect(tab.properties.length).toBeGreaterThan(0);
- });
+ it('it should have added an info tab', function () {
+ expect(scope.content.tabs[1].id).toBe(-1);
+ expect(scope.content.tabs[1].alias).toBe("_umb_infoTab");
+ });
+
+ it('all other tabs than the info tab should have a properties collection', function () {
+ $(scope.content.tabs).each(function (i, tab) {
+ if (tab.id !== -1 && tab.alias !== '_umb_infoTab') {
+ expect(tab.properties.length).toBeGreaterThan(0);
+ }
+ });
});
it('it should change updateDate on save', function () {
- var currentUpdateDate = scope.content.updateDate;
+ var currentUpdateDate = scope.content.updateDate;
- setTimeout(function(){
- scope.save(scope.content);
- expect(scope.content.updateDate).toBeGreaterThan(currentUpdateDate);
- }, 1000);
+ setTimeout(function () {
+ scope.save(scope.content);
+ expect(scope.content.updateDate).toBeGreaterThan(currentUpdateDate);
+ }, 1000);
});
});
diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/directives/val-email.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/directives/val-email.spec.js
index 265689988d..9a6e884ff6 100644
--- a/src/Umbraco.Web.UI.Client/test/unit/common/directives/val-email.spec.js
+++ b/src/Umbraco.Web.UI.Client/test/unit/common/directives/val-email.spec.js
@@ -27,7 +27,8 @@
expect(valEmailExpression.EMAIL_REGEXP.test('a@3b.c')).toBe(true);
expect(valEmailExpression.EMAIL_REGEXP.test('a@b')).toBe(true);
expect(valEmailExpression.EMAIL_REGEXP.test('abc@xyz.financial')).toBe(true);
-
+ expect(valEmailExpression.EMAIL_REGEXP.test('admin@c.pizza')).toBe(true);
+ expect(valEmailExpression.EMAIL_REGEXP.test('admin+gmail-syntax@c.pizza')).toBe(true);
});
});
diff --git a/src/WebPi/TBEX.xml b/src/WebPi/TBEX.xml
deleted file mode 100644
index 827bd01574..0000000000
--- a/src/WebPi/TBEX.xml
+++ /dev/null
@@ -1,76 +0,0 @@
-
- |