diff --git a/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js b/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js index 150bbab7cc..9be27d585a 100644 --- a/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js +++ b/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js @@ -87,10 +87,10 @@ Umbraco.Sys.registerNamespace("Umbraco.Application"); //mimic the API of the legacy tree var tree = { setActiveTreeType : function(treeType){ - navService.syncTree(null, treeType, null); + navService.setActiveTreeType(treeType); }, syncTree : function(path,forceReload){ - navService.syncPath(path); + navService.syncPath(path, forceReload); }, getActionNode: function () { //need to replicate the legacy tree node diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js index 5c8c856796..13203372b5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js @@ -13,6 +13,14 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se scope.maxSections = 7; scope.overflowingSections = 0; scope.sections = []; + scope.nav = navigationService; + + +/* + scope.$watch("currentSection", function (newVal, oldVal) { + scope.currentSection = newVal; + }); +*/ function loadSections(){ sectionResource.getSections() diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js index 111b343019..cb98f33bf4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js @@ -14,8 +14,6 @@ angular.module("umbraco.directives") scope: { section: '@', treealias: '@', - path: '@', - activetree: '@', showoptions: '@', showheader: '@', cachekey: '@', @@ -26,7 +24,7 @@ angular.module("umbraco.directives") //config var hideheader = (attrs.showheader === 'false') ? true : false; var hideoptions = (attrs.showoptions === 'false') ? "hide-options" : ""; - + var template = ''; @@ -53,6 +51,9 @@ angular.module("umbraco.directives") // reload it. This saves a lot on processing if someone is navigating in and out of the same section many times // since it saves on data retreival and DOM processing. var lastSection = ""; + + //keeps track of the currently active tree being called by editors syncing + var activeTree; //flag to enable/disable delete animations var enableDeleteAnimations = false; @@ -64,10 +65,36 @@ angular.module("umbraco.directives") } } + function setupExternalEvents() { + if (scope.eventhandler) { + + scope.eventhandler.clearCache = function(treeAlias){ + treeService.clearCache(treeAlias); + }; + + scope.eventhandler.syncPath = function(path, forceReload){ + if(!angular.isArray(path)){ + path = path.split(','); + } + + path = _.filter(path, function(item){ return (item !== "init" && item !== "-1"); }); + + //if we have a active tree, we sync based on that. + var root = activeTree ? activeTree : scope.tree.root; + + //tell the tree to sync the children below the root + syncTree(root, path, forceReload); + }; + + scope.eventhandler.setActiveTreeType = function(treeAlias){ + activeTree = _.find(scope.tree.root.children, function(node){ return node.metaData.treeAlias === treeAlias; }); + }; + } + } + /** Method to load in the tree data */ function loadTree() { if (!scope.loading && scope.section) { - scope.loading = true; //anytime we want to load the tree we need to disable the delete animations @@ -94,6 +121,31 @@ angular.module("umbraco.directives") } } + function syncTree(node, array, forceReload) { + if(!node || !array || array.length === 0){ + return; + } + + scope.loadChildren(node, forceReload) + .then(function(children){ + var next = _.where(children, {id: array[0]}); + if(next && next.length > 0){ + + if(array.length > 0){ + array.splice(0,1); + }else{ + + } + + if(array.length === 0){ + scope.currentNode = next[0]; + } + + syncTree(next[0], array, forceReload); + } + }); + } + /** method to set the current animation for the node. * This changes dynamically based on if we are changing sections or just loading normal tree data. * When changing sections we don't want all of the tree-ndoes to do their 'leave' animations. @@ -107,6 +159,35 @@ angular.module("umbraco.directives") } }; + /* helper to force reloading children of a tree node */ + scope.loadChildren = function(node, forceReload){ + var deferred = $q.defer(); + + //emit treeNodeExpanding event, if a callback object is set on the tree + emitEvent("treeNodeExpanding", {tree: scope.tree, node: node }); + + if (node.hasChildren && (forceReload || !node.children || (angular.isArray(node.children) && node.children.length === 0))) { + //get the children from the tree service + treeService.loadNodeChildren({ node: node, section: scope.section }) + .then(function(data) { + //emit expanded event + emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: data }); + enableDeleteAnimations = true; + + deferred.resolve(data); + }); + } + else { + emitEvent("treeNodeExpanded", {tree: scope.tree, node: node, children: node.children }); + node.expanded = true; + enableDeleteAnimations = true; + + deferred.resolve(node.children); + } + + return deferred.promise; + }; + /** Method called when the options button next to the root node is called. The tree doesnt know about this, so it raises an event to tell the parent controller @@ -115,7 +196,7 @@ angular.module("umbraco.directives") scope.options = function (e, n, ev) { emitEvent("treeOptionsClick", { element: e, node: n, event: ev }); }; - + /** Method called when an item is clicked in the tree, this passes the DOM element, the tree node object and the original click @@ -132,7 +213,7 @@ angular.module("umbraco.directives") //watch for section changes scope.$watch("section", function (newVal, oldVal) { - + if(!scope.tree){ loadTree(); } @@ -146,28 +227,10 @@ angular.module("umbraco.directives") loadTree(); //store the new section to be loaded as the last section + //clear any active trees to reset lookups lastSection = newVal; - } - - }); - - //watch for path changes - scope.$watch("path", function (newVal, oldVal) { - - //resetting the path destroys the tree - if(newVal && newVal !== oldVal){ - scope.tree = null; - } - - }); - - //watch for active tree changes - scope.$watch("activetree", function (newVal, oldVal) { - - if (newVal && newVal !== oldVal) { - scope.tree = null; - //only reload the tree data and Dom if the newval is different from the old one - } + activeTree = undefined; + } }); //When the user logs in @@ -176,6 +239,8 @@ angular.module("umbraco.directives") if (data.lastUserId !== data.user.id) { treeService.clearCache(); scope.tree = null; + + setupExternalEvents(); loadTree(); } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js index dc63e7e7be..32d0ddcce7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js @@ -27,15 +27,14 @@ angular.module("umbraco.directives") section: '@', cachekey: '@', eventhandler: '=', - path: '@', + currentNode:'=', node:'=', - activetree:'@', tree:'=' }, - template: '
  • ' + + template: '
  • ' + '' + - '' + + '' + '' + '{{node.name}}' + '' + @@ -103,33 +102,33 @@ angular.module("umbraco.directives") takes the arrow DOM element and node data as parameters emits treeNodeCollapsing event if already expanded and treeNodeExpanding if collapsed */ - scope.load = function(arrow, node) { + scope.load = function(node) { if (node.expanded) { enableDeleteAnimations = false; - emitEvent("treeNodeCollapsing", { element: arrow, tree: scope.tree, node: node }); + emitEvent("treeNodeCollapsing", {tree: scope.tree, node: node }); node.expanded = false; } else { - scope.loadChildren(arrow, node, false); + scope.loadChildren(node, false); } }; /* helper to force reloading children of a tree node */ - scope.loadChildren = function(arrow, node, forceReload){ + scope.loadChildren = function(node, forceReload){ //emit treeNodeExpanding event, if a callback object is set on the tree - emitEvent("treeNodeExpanding", { element: arrow, tree: scope.tree, node: node }); + emitEvent("treeNodeExpanding", { tree: scope.tree, node: node }); if (node.hasChildren && (forceReload || !node.children || (angular.isArray(node.children) && node.children.length === 0))) { //get the children from the tree service treeService.loadNodeChildren({ node: node, section: scope.section }) .then(function(data) { //emit expanded event - emitEvent("treeNodeExpanded", { element: arrow, tree: scope.tree, node: node, children: data }); + emitEvent("treeNodeExpanded", {tree: scope.tree, node: node, children: data }); enableDeleteAnimations = true; }); } else { - emitEvent("treeNodeExpanded", { element: arrow, tree: scope.tree, node: node, children: node.children }); + emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: node.children }); node.expanded = true; enableDeleteAnimations = true; } @@ -159,7 +158,7 @@ angular.module("umbraco.directives") scope.expandActivePath(scope.node, scope.activetree, scope.path); scope.node.stateCssClass = scope.node.cssClasses.join(" "); - var template = ''; + var template = ''; var newElement = angular.element(template); $compile(newElement)(scope); element.append(newElement); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index a986d37f56..50c95dc228 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -17,10 +17,11 @@ */ angular.module('umbraco.services') -.factory('navigationService', function ($rootScope, $routeParams, $log, $location, $q, $timeout, dialogService, treeService, notificationsService) { +.factory('navigationService', function ($rootScope, $routeParams, $log, $location, $q, $timeout, dialogService, treeService, notificationsService, historyService) { //Define all sub-properties for the UI object here var ui = { + tablet: false, showNavigation: false, showContextMenu: false, showContextMenuDialog: false, @@ -30,16 +31,23 @@ angular.module('umbraco.services') currentSection: undefined, currentPath: undefined, currentTree: undefined, + treeEventHandler: undefined, currentNode: undefined, actions: undefined, currentDialog: undefined, dialogTitle: undefined, + //a string/name reference for the currently set ui mode currentMode: "default" }; $rootScope.$on("closeDialogs", function(){}); + function setTreeMode() { + ui.tablet = ($(window).width() <= 1000); + ui.showNavigation = !ui.tablet; + } + function setMode(mode) { switch (mode) { case 'tree': @@ -82,12 +90,16 @@ angular.module('umbraco.services') break; default: ui.currentMode = "default"; - ui.showNavigation = false; ui.showContextMenu = false; ui.showContextMenuDialog = false; ui.showSearchResults = false; ui.stickyNavigation = false; ui.showTray = false; + + if(ui.tablet){ + ui.showNavigation = false; + } + break; } } @@ -98,6 +110,18 @@ angular.module('umbraco.services') userDialog: undefined, ui: ui, + init: function(){ + + //TODO: detect tablet mode, subscribe to window resizing + //for now we just hardcode it to non-tablet mode + setTreeMode(); + this.ui.currentSection = $routeParams.section; + + $(window).bind("resize", function () { + setTreeMode(); + }); + }, + /** * @ngdoc method * @name umbraco.services.navigationService#load @@ -144,7 +168,13 @@ angular.module('umbraco.services') */ showTree: function (sectionAlias, treeAlias, path) { if (sectionAlias !== this.ui.currentSection) { - this.syncTree(sectionAlias, treeAlias, path); + this.ui.currentSection = sectionAlias; + if(treeAlias){ + this.setActiveTreeType(treeAlias); + } + if(path){ + this.syncpath(path, true); + } } setMode("tree"); }, @@ -156,6 +186,80 @@ angular.module('umbraco.services') hideTray: function () { ui.showTray = false; }, + + //adding this to get clean global access to the main tree directive + //there will only ever be one main tree event handler + //we need to pass in the current scope for binding these actions + setupTreeEvents: function(treeEventHandler, scope){ + this.ui.treeEventHandler = treeEventHandler; + + //this reacts to the options item in the tree + this.ui.treeEventHandler.bind("treeOptionsClick", function (ev, args) { + ev.stopPropagation(); + ev.preventDefault(); + + scope.currentNode = args.node; + args.scope = scope; + + if(args.event && args.event.altKey){ + args.skipDefault = true; + } + + service.showMenu(ev, args); + }); + + this.ui.treeEventHandler.bind("treeNodeAltSelect", function (ev, args) { + ev.stopPropagation(); + ev.preventDefault(); + + scope.currentNode = args.node; + args.scope = scope; + + args.skipDefault = true; + service.showMenu(ev, args); + }); + + //this reacts to tree items themselves being clicked + //the tree directive should not contain any handling, simply just bubble events + this.ui.treeEventHandler.bind("treeNodeSelect", function (ev, args) { + var n = args.node; + ev.stopPropagation(); + ev.preventDefault(); + + + if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { + //this is a legacy tree node! + var jsPrefix = "javascript:"; + var js; + if (n.metaData["jsClickCallback"].startsWith(jsPrefix)) { + js = n.metaData["jsClickCallback"].substr(jsPrefix.length); + } + else { + js = n.metaData["jsClickCallback"]; + } + try { + var func = eval(js); + //this is normally not necessary since the eval above should execute the method and will return nothing. + if (func != null && (typeof func === "function")) { + func.call(); + } + } + catch(ex) { + $log.error("Error evaluating js callback from legacy tree node: " + ex); + } + } + else if(n.routePath){ + //add action to the history service + historyService.add({ name: n.name, link: n.routePath, icon: n.icon }); + //not legacy, lets just set the route value and clear the query string if there is one. + $location.path(n.routePath).search(""); + } else if(args.element.section){ + $location.path(args.element.section).search(""); + } + + service.hideNavigation(); + }); + }, /** * @ngdoc method * @name umbraco.services.navigationService#syncTree @@ -163,42 +267,24 @@ angular.module('umbraco.services') * * @description * Syncs the tree with a given section alias and a given path - * The path format is: ["treeAlias","itemId","itemId"], and so on + * The path format is: ["itemId","itemId"], and so on * so to sync to a specific document type node do: *
    -         * navigationService.syncTree("content", "nodeTypes", [-1,1023,3453]);  
    +         * navigationService.syncPath(["-1","123d"], true);  
              * 
    - * @param {string} sectionAlias The alias of the section the tree should load data from - * @param {string} treeAlias The alias of tree to auto-expand - * @param {array} path array of ascendant ids, ex: [,1023,1243] (loads a specific document type into the settings tree) + * @param {array} path array of ascendant ids, ex: ["1023","1243"] (loads a specific document type into the settings tree) + * @param {bool} forceReload forces a reload of data from the server */ - syncTree: function (sectionAlias, treeAlias, path) { - //TODO: investicate if we need to halt watch triggers - //and instead pause them and then manually tell the tree to digest path changes - //as this might be a bit heavy loading - if(sectionAlias){ - this.ui.currentSection = sectionAlias; - } - if(treeAlias){ - this.ui.currentTree = treeAlias; - } - if(path){ - this.ui.currentPath = path; - } + syncPath: function (path, forceReload) { + if(this.ui.treeEventHandler){ + this.ui.treeEventHandler.syncPath(path,forceReload); + } }, - /* this is to support the legacy ways to sync the tree, so you can do it in 2 steps - For all new operations, its recommend to just use syncTree() - */ - syncPath: function (path) { - //TODO: investicate if we need to halt watch triggers - //and instead pause them and then manually tell the tree to digest path changes - //as this might be a bit heavy loading - if(!angular.isArray(path)){ - path = path.split(","); - } - - this.ui.currentPath = path; + setActiveTreeType: function (treeAlias) { + if(this.ui.treeEventHandler){ + this.ui.treeEventHandler.setActiveTreeType(treeAlias); + } }, /** @@ -249,10 +335,13 @@ angular.module('umbraco.services') * Hides the tree by hiding the containing dom element */ hideTree: function () { - if (!this.ui.stickyNavigation) { - this.ui.currentSection = ""; + + if (this.ui.tablet && !this.ui.stickyNavigation) { + //reset it to whatever is in the url + this.ui.currentSection = $routeParams.section; setMode("default-hidesectiontree"); } + }, /** @@ -552,7 +641,7 @@ angular.module('umbraco.services') */ hideNavigation: function () { this.ui.actions = []; - this.ui.currentNode = undefined; + //this.ui.currentNode = undefined; setMode("default"); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/navigation.controller.js index b299ece618..d9e7a8c97b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/navigation.controller.js @@ -16,113 +16,49 @@ function NavigationController($scope,$rootScope, $location, $log, $routeParams, // 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; + //wire up the screensize and tree mode detection + $scope.nav.init(); + + //the tree event handler i used to subscribe to the main tree click events + $scope.treeEventHandler = $({}); + $scope.nav.setupTreeEvents($scope.treeEventHandler, $scope); + + //keep track of $scope.$watch(function () { //watch the route parameters section return $routeParams.section; }, function(newVal, oldVal) { - $scope.currentSection = newVal; + $scope.nav.ui.currentSection = newVal; }); + //trigger search with a hotkey: keyboardService.bind("ctrl+shift+s", function(){ $scope.nav.showSearch(); }); - //the tree event handler i used to subscribe to the main tree click events - $scope.treeEventHandler = $({}); $scope.selectedId = navigationService.currentId; - //This reacts to clicks passed to the body element which emits a global call to close all dialogs $rootScope.$on("closeDialogs", function (event) { if (navigationService.ui.stickyNavigation) { - navigationService.hideNavigation(); + navigationService.hideNavigation(); angularHelper.safeApply($scope); } }); //this reacts to the options item in the tree - $scope.treeEventHandler.bind("treeOptionsClick", function (ev, args) { - ev.stopPropagation(); - ev.preventDefault(); - - $scope.currentNode = args.node; - args.scope = $scope; - - if(args.event && args.event.altKey){ - args.skipDefault = true; - } - - navigationService.showMenu(ev, args); - }); - - $scope.treeEventHandler.bind("treeNodeAltSelect", function (ev, args) { - ev.stopPropagation(); - ev.preventDefault(); - - $scope.currentNode = args.node; - args.scope = $scope; - - args.skipDefault = true; - navigationService.showMenu(ev, args); - }); - - - //this reacts to the options item in the tree - $scope.searchShowMenu = function (ev, args) { - + //todo, migrate to nav service + $scope.searchShowMenu = function (ev, args) { $scope.currentNode = args.node; args.scope = $scope; //always skip default args.skipDefault = true; - navigationService.showMenu(ev, args); }; - //this reacts to tree items themselves being clicked - //the tree directive should not contain any handling, simply just bubble events - $scope.treeEventHandler.bind("treeNodeSelect", function (ev, args) { - var n = args.node; - ev.stopPropagation(); - ev.preventDefault(); - - - if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { - //this is a legacy tree node! - var jsPrefix = "javascript:"; - var js; - if (n.metaData["jsClickCallback"].startsWith(jsPrefix)) { - js = n.metaData["jsClickCallback"].substr(jsPrefix.length); - } - else { - js = n.metaData["jsClickCallback"]; - } - try { - var func = eval(js); - //this is normally not necessary since the eval above should execute the method and will return nothing. - if (func != null && (typeof func === "function")) { - func.call(); - } - } - catch(ex) { - $log.error("Error evaluating js callback from legacy tree node: " + ex); - } - } - else if(n.routePath){ - //add action to the history service - historyService.add({ name: n.name, link: n.routePath, icon: n.icon }); - //not legacy, lets just set the route value and clear the query string if there is one. - $location.path(n.routePath).search(""); - } else if(args.element.section){ - $location.path(args.element.section).search(""); - } - - navigationService.hideNavigation(); - }); - - /** Opens a dialog but passes in this scope instance to be used for the dialog */ $scope.openDialog = function (currentNode, action, currentSection) { navigationService.showDialog({ diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/directives/umb-navigation.html index 0c8287bed0..3a337f1922 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/directives/umb-navigation.html @@ -1,7 +1,8 @@
    - + +