Much better tree syncing

Is now pluggable, without those directive watches, and only active when
used from external controllers, so dialog trees become much lighter
This commit is contained in:
perploug
2013-10-22 14:57:41 +02:00
parent d4a069451f
commit 3ba05aca8f
8 changed files with 257 additions and 160 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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 = '<ul class="umb-tree ' + hideoptions + '">' +
'<li class="root">';
@@ -37,7 +35,7 @@ angular.module("umbraco.directives")
'</div>';
}
template += '<ul>' +
'<umb-tree-item ng-repeat="child in tree.root.children" eventhandler="eventhandler" path="{{path}}" activetree="{{activetree}}" node="child" tree="child" section="{{section}}" ng-animate="animation()"></umb-tree-item>' +
'<umb-tree-item ng-repeat="child in tree.root.children" eventhandler="eventhandler" path="{{path}}" activetree="{{activetree}}" node="child" current-node="currentNode" tree="child" section="{{section}}" ng-animate="animation()"></umb-tree-item>' +
'</ul>' +
'</li>' +
'</ul>';
@@ -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();
}
});

View File

@@ -27,15 +27,14 @@ angular.module("umbraco.directives")
section: '@',
cachekey: '@',
eventhandler: '=',
path: '@',
currentNode:'=',
node:'=',
activetree:'@',
tree:'='
},
template: '<li ng-swipe-right="options(this, node, $event)"><div ng-style="setTreePadding(node)" class="{{node.stateCssClass}}" ng-class="{\'loading\': node.loading}">' +
template: '<li ng-class="{\'current\': (node == currentNode)}"><div ng-style="setTreePadding(node)" class="{{node.stateCssClass}}" ng-class="{\'loading\': node.loading}" ng-swipe-right="options(this, node, $event)" >' +
'<ins ng-hide="node.hasChildren" style="background:none;width:18px;"></ins>' +
'<ins ng-show="node.hasChildren" ng-class="{\'icon-navigation-right\': !node.expanded, \'icon-navigation-down\': node.expanded}" ng-click="load(this, node)"></ins>' +
'<ins ng-show="node.hasChildren" ng-class="{\'icon-navigation-right\': !node.expanded, \'icon-navigation-down\': node.expanded}" ng-click="load(node)"></ins>' +
'<i title="#{{node.routePath}}" class="{{node.cssClass}}" style="{{node.style}}"></i>' +
'<a href ng-click="select(this, node, $event)" on-right-click="altSelect(this, node, $event)" >{{node.name}}</a>' +
'<a href class="umb-options" ng-hide="!node.menuUrl" ng-click="options(this, node, $event)"><i></i><i></i><i></i></a>' +
@@ -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 = '<ul ng-class="{collapsed: !node.expanded}"><umb-tree-item ng-repeat="child in node.children" eventhandler="eventhandler" activetree="{{activetree}}" path="{{path}}" tree="tree" node="child" section="{{section}}" ng-animate="animation()"></umb-tree-item></ul>';
var template = '<ul ng-class="{collapsed: !node.expanded}"><umb-tree-item ng-repeat="child in node.children" eventhandler="eventhandler" activetree="{{activetree}}" tree="tree" current-node="currentNode" node="child" section="{{section}}" ng-animate="animation()"></umb-tree-item></ul>';
var newElement = angular.element(template);
$compile(newElement)(scope);
element.append(newElement);

View File

@@ -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:
* <pre>
* navigationService.syncTree("content", "nodeTypes", [-1,1023,3453]);
* navigationService.syncPath(["-1","123d"], true);
* </pre>
* @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");
}
};

View File

@@ -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({

View File

@@ -1,7 +1,8 @@
<div id="leftcolumn" ng-controller="Umbraco.NavigationController"
ng-mouseleave="nav.leaveTree($event)" ng-mouseenter="nav.enterTree($event)">
<umb-sections sections="sections" current-section="currentSection"></umb-sections>
<umb-sections sections="sections">
</umb-sections>
<!-- navigation container -->
<div id="navigation" ng-show="nav.ui.showNavigation" class="fill shadow umb-modalcolumn" ng-animate="'slide'">
@@ -11,6 +12,8 @@
<!-- the search -->
<div id="search-form">
<div class="umb-modalcolumn-header">
{{nav.ui.currentSection}}
<form class="form-search" ng-controller="Umbraco.SearchController" novalidate>
<i class="icon-search"></i>
<input type="text"

View File

@@ -1,7 +1,4 @@
<div>
<div style="position:absolute; top: 1; background: red">
{{overflowingSections}} {{maxSections}} {{totalSections}}
</div>
<div id="applications" ng-class="{faded:nav.ui.stickyNavigation}">
<ul class="sections">
<li class="avatar">
@@ -9,7 +6,7 @@
<img ng-src="{{avatar}}" />
</a>
</li>
<li ng-repeat="section in sections | limitTo: maxSections" ng-class="{current: section.alias == currentSection}">
<li ng-repeat="section in sections | limitTo: maxSections" ng-class="{current: section.alias == nav.ui.currentSection}">
<a href="#/{{section.alias}}"
ng-dblclick="sectionDblClick(section)"
ng-click="sectionClick(section)"