MDL-51961 cbe: UX improvements to manage competencies

1. The competencies tree should open with all nodes initially collapsed
    (unless there is less than 20 results, in which case they should all be expanded)
2. When the tree is refreshed, all previously selected nodes should be remembered
    and the tree expanded to that point.
3. While the tree is reloading - there should be a "spinner"
This commit is contained in:
Damyon Wiese
2016-04-18 10:58:41 +08:00
committed by Frederic Massart
parent 91f3ad48d9
commit 6bf250f84e
7 changed files with 80 additions and 34 deletions
+1 -1
View File
@@ -1 +1 @@
define(["core/ajax","core/notification","core/templates","tool_lp/tree","jquery"],function(a,b,c,d,e){var f=[],g=0,h="",i="",j=function(a,b){var c=0,d=!1;for(a.haschildren=!1,a.children=[],c=0;c<b.length;c++)d=b[c],d.parentid==a.id&&(a.haschildren=!0,a.children.push(d),j(d,b))},k=function(b){var k=e.Deferred(),l=a.call([{methodname:"tool_lp_search_competencies",args:{searchtext:b,competencyframeworkid:g,includerelated:!0}}]);return l[0].done(function(a){f=[];var b=0;for(b=0;b<a.length;b++)f[a[b].id]=a[b];var g=[],l=!1;for(b=0;b<a.length;b++)l=a[b],0===parseInt(l.parentid,10)&&(g.push(l),j(l,a));var m={shortname:h,competencies:g};c.render("tool_lp/competencies_tree_root",m).done(function(a,b){c.replaceNodeContents(e(i),e(a).html(),b),new d(i),k.resolve(f)}).fail(k.reject)}).fail(k.reject),k.promise()};return{init:function(a,c,d,e){g=a,h=c,i=e,k(d).fail(b.exception)},on:function(a,b){e(i).on(a,b)},getCompetencyFrameworkId:function(){return g},getCompetency:function(a){return f[a]},getCompetencyLevel:function(a){var b=this.getCompetency(a),c=b.path.replace(/^\/|\/$/g,"").split("/").length;return c},reloadCompetencies:function(){return k("").fail(b.exception)},listCompetencies:function(){return f}}});
define(["core/ajax","core/notification","core/templates","tool_lp/tree","jquery"],function(a,b,c,d,e){var f=[],g=0,h="",i="",j="",k=function(a,b){var c=0,d=!1;for(a.haschildren=!1,a.children=[],c=0;c<b.length;c++)d=b[c],d.parentid==a.id&&(a.haschildren=!0,a.children.push(d),k(d,b))},l=function(b){var l=e.Deferred();return c.render("tool_lp/loading",{}).done(function(m,n){c.replaceNodeContents(e(i),m,n);var o=a.call([{methodname:"tool_lp_search_competencies",args:{searchtext:b,competencyframeworkid:g,includerelated:!0}}]);o[0].done(function(a){f=[];var b=0;for(b=0;b<a.length;b++)f[a[b].id]=a[b];var g=[],m=!1;for(b=0;b<a.length;b++)m=a[b],0===parseInt(m.parentid,10)&&(g.push(m),k(m,a));var n={shortname:h,competencies:g};c.render("tool_lp/competencies_tree_root",n).done(function(a,b){c.replaceNodeContents(e(i),e(a).html(),b);var g=new d(i);if(j){var h=e(i).find("[data-id="+j+"]");h.length&&g.updateFocus(h)}l.resolve(f)}).fail(l.reject)}).fail(l.reject)}),l.promise()},m=function(a,b){j=b.attr("data-id")};return{init:function(a,c,d,e){g=a,h=c,i=e,l(d).fail(b.exception),this.on("selectionchanged",m)},on:function(a,b){e(i).on(a,b)},getCompetencyFrameworkId:function(){return g},getCompetency:function(a){return f[a]},getCompetencyLevel:function(a){var b=this.getCompetency(a),c=b.path.replace(/^\/|\/$/g,"").split("/").length;return c},reloadCompetencies:function(){return l("").fail(b.exception)},listCompetencies:function(){return f}}});
+1 -1
View File
@@ -1 +1 @@
define(["jquery","core/url","core/log"],function(a,b,c){var d=a('<img alt="" src="'+b.imageUrl("t/expanded")+'"/>'),e=a('<img alt="" src="'+b.imageUrl("t/collapsed")+'"/>'),f=function(b){this.treeRoot=a(b),this.items=this.treeRoot.find("li"),this.parents=this.treeRoot.find("li:has(ul)"),this.visibleItems=null,this.activeItem=null,this.keys={tab:9,enter:13,space:32,pageup:33,pagedown:34,end:35,home:36,left:37,up:38,right:39,down:40,asterisk:106},this.init(),this.bindEventHandlers()};return f.prototype.init=function(){this.parents.attr("aria-expanded","true"),this.parents.prepend(d.clone()),this.items.attr("role","tree-item"),this.items.attr("tabindex","-1"),this.parents.attr("role","group"),this.treeRoot.attr("role","tree"),this.visibleItems=this.treeRoot.find("li")},f.prototype.expandGroup=function(a){var b=a.children("ul");b.show().attr("aria-hidden","false"),a.attr("aria-expanded","true"),a.children("img").attr("src",d.attr("src")),this.visibleItems=this.treeRoot.find("li:visible")},f.prototype.collapseGroup=function(a){var b=a.children("ul");b.hide().attr("aria-hidden","true"),a.attr("aria-expanded","false"),a.children("img").attr("src",e.attr("src")),this.visibleItems=this.treeRoot.find("li:visible")},f.prototype.toggleGroup=function(a){"true"==a.attr("aria-expanded")?this.collapseGroup(a):this.expandGroup(a)},f.prototype.updateFocus=function(a){this.items.attr("aria-selected","false").attr("tabindex","-1"),a.attr("aria-selected","true").attr("tabindex",0),this.treeRoot.trigger("selectionchanged",[a])},f.prototype.handleKeyDown=function(b,c){var d=this.visibleItems.index(b);if(c.altKey||c.ctrlKey||c.shiftKey&&c.keyCode!=this.keys.tab)return!0;switch(c.keyCode){case this.keys.home:return this.activeItem=this.parents.first(),this.activeItem.focus(),c.stopPropagation(),!1;case this.keys.end:return this.activeItem=this.visibleItems.last(),this.activeItem.focus(),c.stopPropagation(),!1;case this.keys.enter:case this.keys.space:return b.has("ul")&&this.toggleGroup(b,!0),c.stopPropagation(),!1;case this.keys.left:if(b.has("ul")&&"true"==b.attr("aria-expanded"))this.collapseGroup(b);else{var e=b.parent(),f=e.parent();this.activeItem=f,this.activeItem.focus()}return c.stopPropagation(),!1;case this.keys.right:return b.has("ul")&&"false"==b.attr("aria-expanded")?this.expandGroup(b):(this.activeItem=b.children("ul").children("li").first(),this.activeItem.focus()),c.stopPropagation(),!1;case this.keys.up:if(d>0){var g=this.visibleItems.eq(d-1);this.activeItem=g,g.focus()}return c.stopPropagation(),!1;case this.keys.down:if(d<this.visibleItems.length-1){var h=this.visibleItems.eq(d+1);this.activeItem=h,h.focus()}return c.stopPropagation(),!1;case this.keys.asterisk:var i=this;return this.parents.each(function(){i.expandGroup(a(this))}),c.stopPropagation(),!1}return!0},f.prototype.handleKeyPress=function(a,b){if(b.altKey||b.ctrlKey||b.shiftKey)return!0;switch(b.keyCode){case this.keys.tab:return!0;case this.keys.enter:case this.keys.home:case this.keys.end:case this.keys.left:case this.keys.right:case this.keys.up:case this.keys.down:return b.stopPropagation(),!1;default:var c=String.fromCharCode(b.which),d=!1,e=this.visibleItems.index(a),f=this.visibleItems.length,g=e+1;for(g==f&&(g=0);g!=e;){var h=this.visibleItems.eq(g),i=h.text().charAt(0);if(h.has("ul")&&(i=h.find("span").text().charAt(0)),i.toLowerCase()==c){d=!0;break}g+=1,g==f&&(g=0)}return d===!0&&(this.activeItem=this.visibleItems.eq(g),this.activeItem.focus()),b.stopPropagation(),!1}return!0},f.prototype.on=function(a,b){"selectionchanged"!==a?c.warning('Invalid custom event name for tree. Only "selectionchanged" is supported.'):this.treeRoot.on(a,b)},f.prototype.handleDblClick=function(a,b){return b.altKey||b.ctrlKey||b.shiftKey?!0:(this.activeItem=a,this.updateFocus(a),this.toggleGroup(a),b.stopPropagation(),!1)},f.prototype.handleClick=function(a,b){return b.altKey||b.ctrlKey||b.shiftKey?!0:(this.activeItem=a,this.updateFocus(a),b.stopPropagation(),!1)},f.prototype.handleBlur=function(){return!0},f.prototype.handleFocus=function(a){return null===this.activeItem&&(this.activeItem=a),this.updateFocus(this.activeItem),!0},f.prototype.bindEventHandlers=function(){var b=this;this.parents.dblclick(function(c){return b.handleDblClick(a(this),c)}),this.items.click(function(c){return b.handleClick(a(this),c)}),this.items.keydown(function(c){return b.handleKeyDown(a(this),c)}),this.items.keypress(function(c){return b.handleKeyPress(a(this),c)}),this.items.focus(function(c){return b.handleFocus(a(this),c)}),this.items.blur(function(c){return b.handleBlur(a(this),c)})},f});
define(["jquery","core/url","core/log"],function(a,b,c){var d=a('<img alt="" src="'+b.imageUrl("t/expanded")+'"/>'),e=a('<img alt="" src="'+b.imageUrl("t/collapsed")+'"/>'),f=function(b){this.treeRoot=a(b),this.items=this.treeRoot.find("li"),this.expandAll=this.items.length<20,this.parents=this.treeRoot.find("li:has(ul)"),this.visibleItems=null,this.activeItem=null,this.keys={tab:9,enter:13,space:32,pageup:33,pagedown:34,end:35,home:36,left:37,up:38,right:39,down:40,asterisk:106},this.init(),this.bindEventHandlers()};return f.prototype.init=function(){this.parents.attr("aria-expanded","true"),this.parents.prepend(d.clone()),this.items.attr("role","tree-item"),this.items.attr("tabindex","-1"),this.parents.attr("role","group"),this.treeRoot.attr("role","tree"),this.visibleItems=this.treeRoot.find("li");var b=this;this.expandAll||(this.parents.each(function(){b.collapseGroup(a(this))}),this.expandGroup(this.parents.first()))},f.prototype.expandGroup=function(a){var b=a.children("ul");b.show().attr("aria-hidden","false"),a.attr("aria-expanded","true"),a.children("img").attr("src",d.attr("src")),this.visibleItems=this.treeRoot.find("li:visible")},f.prototype.collapseGroup=function(a){var b=a.children("ul");b.hide().attr("aria-hidden","true"),a.attr("aria-expanded","false"),a.children("img").attr("src",e.attr("src")),this.visibleItems=this.treeRoot.find("li:visible")},f.prototype.toggleGroup=function(a){"true"==a.attr("aria-expanded")?this.collapseGroup(a):this.expandGroup(a)},f.prototype.updateFocus=function(a){for(var b=a.parent();"tree"!=b.attr("role");)b=b.parent(),"false"==b.attr("aria-expanded")&&this.expandGroup(b),b=b.parent();this.items.attr("aria-selected","false").attr("tabindex","-1"),a.attr("aria-selected","true").attr("tabindex",0),this.treeRoot.trigger("selectionchanged",[a])},f.prototype.handleKeyDown=function(b,c){var d=this.visibleItems.index(b);if(c.altKey||c.ctrlKey||c.shiftKey&&c.keyCode!=this.keys.tab)return!0;switch(c.keyCode){case this.keys.home:return this.activeItem=this.parents.first(),this.activeItem.focus(),c.stopPropagation(),!1;case this.keys.end:return this.activeItem=this.visibleItems.last(),this.activeItem.focus(),c.stopPropagation(),!1;case this.keys.enter:case this.keys.space:return b.has("ul")&&this.toggleGroup(b,!0),c.stopPropagation(),!1;case this.keys.left:if(b.has("ul")&&"true"==b.attr("aria-expanded"))this.collapseGroup(b);else{var e=b.parent(),f=e.parent();this.activeItem=f,this.activeItem.focus()}return c.stopPropagation(),!1;case this.keys.right:return b.has("ul")&&"false"==b.attr("aria-expanded")?this.expandGroup(b):(this.activeItem=b.children("ul").children("li").first(),this.activeItem.focus()),c.stopPropagation(),!1;case this.keys.up:if(d>0){var g=this.visibleItems.eq(d-1);this.activeItem=g,g.focus()}return c.stopPropagation(),!1;case this.keys.down:if(d<this.visibleItems.length-1){var h=this.visibleItems.eq(d+1);this.activeItem=h,h.focus()}return c.stopPropagation(),!1;case this.keys.asterisk:var i=this;return this.parents.each(function(){i.expandGroup(a(this))}),c.stopPropagation(),!1}return!0},f.prototype.handleKeyPress=function(a,b){if(b.altKey||b.ctrlKey||b.shiftKey)return!0;switch(b.keyCode){case this.keys.tab:return!0;case this.keys.enter:case this.keys.home:case this.keys.end:case this.keys.left:case this.keys.right:case this.keys.up:case this.keys.down:return b.stopPropagation(),!1;default:var c=String.fromCharCode(b.which),d=!1,e=this.visibleItems.index(a),f=this.visibleItems.length,g=e+1;for(g==f&&(g=0);g!=e;){var h=this.visibleItems.eq(g),i=h.text().charAt(0);if(h.has("ul")&&(i=h.find("span").text().charAt(0)),i.toLowerCase()==c){d=!0;break}g+=1,g==f&&(g=0)}return d===!0&&(this.activeItem=this.visibleItems.eq(g),this.activeItem.focus()),b.stopPropagation(),!1}return!0},f.prototype.on=function(a,b){"selectionchanged"!==a?c.warning('Invalid custom event name for tree. Only "selectionchanged" is supported.'):this.treeRoot.on(a,b)},f.prototype.handleDblClick=function(a,b){return b.altKey||b.ctrlKey||b.shiftKey?!0:(this.activeItem=a,this.updateFocus(a),this.toggleGroup(a),b.stopPropagation(),!1)},f.prototype.handleClick=function(a,b){return b.altKey||b.ctrlKey||b.shiftKey?!0:(this.activeItem=a,this.updateFocus(a),b.stopPropagation(),!1)},f.prototype.handleBlur=function(){return!0},f.prototype.handleFocus=function(a){return null===this.activeItem&&(this.activeItem=a),this.updateFocus(this.activeItem),!0},f.prototype.bindEventHandlers=function(){var b=this;this.parents.dblclick(function(c){return b.handleDblClick(a(this),c)}),this.items.click(function(c){return b.handleClick(a(this),c)}),this.items.keydown(function(c){return b.handleKeyDown(a(this),c)}),this.items.keypress(function(c){return b.handleKeyPress(a(this),c)}),this.items.focus(function(c){return b.handleFocus(a(this),c)}),this.items.blur(function(c){return b.handleBlur(a(this),c)})},f});
+57 -32
View File
@@ -37,6 +37,9 @@ define(['core/ajax', 'core/notification', 'core/templates', 'tool_lp/tree', 'jqu
/** @var {String} treeSelector - The selector for the root of the tree. */
var treeSelector = '';
/** @var {String} currentNodeId - The data-id of the current node in the tree. */
var currentNodeId = '';
/**
* Build a tree from the flat list of competencies.
* @param {Object} parent The parent competency.
@@ -64,45 +67,65 @@ define(['core/ajax', 'core/notification', 'core/templates', 'tool_lp/tree', 'jqu
*/
var loadCompetencies = function(searchtext) {
var deferred = $.Deferred();
var promises = ajax.call([{
methodname: 'tool_lp_search_competencies',
args: {
searchtext: searchtext,
competencyframeworkid: competencyFrameworkId,
includerelated: true
}
}]);
promises[0].done(function(result) {
competencies = [];
var i = 0;
for (i = 0; i < result.length; i++) {
competencies[result[i].id] = result[i];
}
var children = [];
var competency = false;
for (i = 0; i < result.length; i++) {
competency = result[i];
if (parseInt(competency.parentid, 10) === 0) {
children.push(competency);
addChildren(competency, result);
templates.render('tool_lp/loading', {}).done(function(loadinghtml, loadingjs) {
templates.replaceNodeContents($(treeSelector), loadinghtml, loadingjs);
var promises = ajax.call([{
methodname: 'tool_lp_search_competencies',
args: {
searchtext: searchtext,
competencyframeworkid: competencyFrameworkId,
includerelated: true
}
}]);
promises[0].done(function(result) {
competencies = [];
var i = 0;
for (i = 0; i < result.length; i++) {
competencies[result[i].id] = result[i];
}
}
var context = {
shortname: competencyFrameworkShortName,
competencies: children
};
templates.render('tool_lp/competencies_tree_root', context).done(function(html, js) {
templates.replaceNodeContents($(treeSelector), $(html).html(), js);
new Ariatree(treeSelector);
deferred.resolve(competencies);
}).fail(deferred.reject);
}).fail(deferred.reject);
var children = [];
var competency = false;
for (i = 0; i < result.length; i++) {
competency = result[i];
if (parseInt(competency.parentid, 10) === 0) {
children.push(competency);
addChildren(competency, result);
}
}
var context = {
shortname: competencyFrameworkShortName,
competencies: children
};
templates.render('tool_lp/competencies_tree_root', context).done(function(html, js) {
templates.replaceNodeContents($(treeSelector), $(html).html(), js);
var tree = new Ariatree(treeSelector);
if (currentNodeId) {
var node = $(treeSelector).find('[data-id=' + currentNodeId + ']');
if (node.length) {
tree.updateFocus(node);
}
}
deferred.resolve(competencies);
}).fail(deferred.reject);
}).fail(deferred.reject);
});
return deferred.promise();
};
/**
* Whenever the current item in the tree is changed - remember the "id".
* @param {Event} evt
* @param {Node} The active node in the tree.
*/
var rememberCurrent = function(evt, node) {
currentNodeId = node.attr('data-id');
};
return /** @alias module:tool_lp/competencytree */ {
// Public variables and functions.
@@ -119,6 +142,8 @@ define(['core/ajax', 'core/notification', 'core/templates', 'tool_lp/tree', 'jqu
competencyFrameworkShortName = shortname;
treeSelector = selector;
loadCompetencies(search).fail(notification.exception);
this.on('selectionchanged', rememberCurrent);
},
/**
+18
View File
@@ -42,6 +42,7 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
this.treeRoot = $(selector);
this.items = this.treeRoot.find('li');
this.expandAll = this.items.length < 20;
this.parents = this.treeRoot.find('li:has(ul)');
this.visibleItems = null;
@@ -82,6 +83,14 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
this.treeRoot.attr('role', 'tree');
this.visibleItems = this.treeRoot.find('li');
var thisObj = this;
if (!this.expandAll) {
this.parents.each(function() {
thisObj.collapseGroup($(this));
});
this.expandGroup(this.parents.first());
}
};
/**
@@ -146,6 +155,15 @@ define(['jquery', 'core/url', 'core/log'], function($, url, log) {
* @param {Object} item is the jquery id of the parent item of the group
*/
Tree.prototype.updateFocus = function(item) {
// Expand all nodes up the tree.
var walk = item.parent();
while (walk.attr('role') != 'tree') {
walk = walk.parent();
if (walk.attr('aria-expanded') == 'false') {
this.expandGroup(walk);
}
walk = walk.parent();
}
this.items.attr('aria-selected', 'false').attr('tabindex', '-1');
item.attr('aria-selected', 'true').attr('tabindex', 0);
this.treeRoot.trigger('selectionchanged', [item]);
+1
View File
@@ -80,6 +80,7 @@ $string['linktemplatecompetencies'] = 'Link template competencies';
$string['listcompetencyframeworkscaption'] = 'List of competency frameworks';
$string['listplanscaption'] = 'List of learning plans';
$string['listtemplatescaption'] = 'List of learning plan templates';
$string['loading'] = 'Loading...';
$string['locatecompetency'] = 'Locate competency';
$string['lp:competencymanage'] = 'Manage competency frameworks';
$string['lp:competencyread'] = 'View competency frameworks';
+1
View File
@@ -0,0 +1 @@
{{#pix}}y/loading, core, {{#str}}loading, tool_lp{{/str}}{{/pix}}
@@ -42,6 +42,7 @@
</form>
</p>
<ul data-enhance="tree">
{{> tool_lp/loading }}
</ul>
</div>