MDL-46570 gradereport_history: Making the users list an ARIA widget

Part of MDL-46191
This commit is contained in:
Frederic Massart 2014-08-05 19:10:59 +08:00 committed by Ankit Agarwal
parent 855e13784b
commit 7478d85cfc
7 changed files with 457 additions and 87 deletions

View File

@ -57,7 +57,7 @@ $outcome->response = array('users' => $users);
$outcome->response['totalusers'] = count($users);
$extrafields = get_extra_user_fields($context);
$useroptions = array('link' => false);
$useroptions = array('link' => false, 'visibletoscreenreaders' => false);
foreach ($outcome->response['users'] as &$user) {
$user->userid = $user->id;

View File

@ -55,12 +55,12 @@ table#gradestable th.header.selected {
text-align: center;
}
.gradereport_history_usp .user {
.gradereport_history_usp .usp-user {
width: 100%;
text-align: left;
border-top: 1px solid #eee;
}
.gradereport_history_usp .user:nth-child(odd) {
.gradereport_history_usp .usp-user:nth-child(odd) {
background-color: #f9f9f9;
}
.gradereport_history_usp .usp-first-added {
@ -78,14 +78,14 @@ table#gradestable th.header.selected {
margin: 6px 3px 0 3px;
float: left;
}
.gradereport_history_usp .userpicture{
.gradereport_history_usp .usp-userpicture{
cursor: pointer;
}
.gradereport_history_usp .user .details {
.gradereport_history_usp .usp-user .details {
margin-left: 67px;
padding: 3px 6px 0 6px;
}
.gradereport_history_usp .user .details label {
.gradereport_history_usp .usp-user .details label {
margin: 0;
}
.gradereport_history_usp .usp-more-results {
@ -100,6 +100,6 @@ table#gradestable th.header.selected {
margin: 0;
}
.dir-rtl .gradereport_history_usp .usp-search-results .user { text-align: right;}
.dir-rtl .gradereport_history_usp .usp-search-results .usp-user { text-align: right;}
.dir-rtl .gradereport_history_usp .usp-content .usp-controls { text-align: right;}

View File

@ -71,8 +71,8 @@ var CSS = {
SEARCHFIELD : 'usp-search-field',
SEARCHRESULTS : 'usp-search-results',
SELECTED : 'selected',
USER : 'user',
USERS : 'users',
USER : 'usp-user',
USERS : 'usp-users',
WRAP : 'usp-wrap'
};
var SELECTORS = {
@ -128,6 +128,15 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
*/
_usersBufferList: null,
/**
* The Node on which the focus is set.
*
* @property _userTabFocus
* @type Node
* @private
*/
_userTabFocus: null,
/**
* Compiled template function for a user node.
*
@ -149,7 +158,7 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
'<div class="{{CSS.SEARCH}}" role="search">' +
'<form>' +
'<input type="text" class="{{CSS.SEARCHFIELD}}" ' +
'aria-labelledby="{{get_string "search" "moodle"}}" value="" />' +
'aria-label="{{get_string "search" "moodle"}}" value="" />' +
'<input type="submit" class="{{CSS.SEARCHBTN}}"' +
'value="{{get_string "search" "moodle"}}">' +
'</form>' +
@ -194,9 +203,12 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// The button to finalize the selection.
bb.one(SELECTORS.FINISHBTN).on('click', this.finishSelectingUsers, this);
// Delegate the keyboard navigation in the users list.
bb.delegate('key', this.userKeyboardNavigation, 'down:38,40', SELECTORS.AJAXCONTENT, this);
// Delegate the action to select a user.
Y.delegate("click", this.selectUser, bb.one(SELECTORS.AJAXCONTENT), SELECTORS.USERSELECT, this);
Y.delegate("click", this.selectUser, bb.one(SELECTORS.AJAXCONTENT), SELECTORS.PICTURE, this);
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.USERSELECT, this);
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.PICTURE, this);
params = this.get(USP.PARAMS);
params.id = this.get(USP.COURSEID);
@ -221,10 +233,22 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// Leave the content as is, but reset the selection.
this._usersBufferList = Y.clone(this.get(USP.USERFULLNAMES));
bb = this.get('boundingBox');
bb.all(SELECTORS.USERSELECT).set('checked', false);
// Remove all the selected users.
bb.all(SELECTORS.USER).each(function(node) {
this.markUserNode(node, false);
}, this);
// Select the users.
Y.Object.each(this._usersBufferList, function(v, k) {
bb.one(SELECTORS.USERSELECT + '[name=' + USP.CHECKBOX_NAME_PREFIX + k + ']').set('checked', true);
});
var user = bb.one(SELECTORS.USER + '[data-userid="' + k + '"]');
if (user) {
this.markUserNode(user, true);
}
}, this);
// Reset the tab focus.
this.setUserTabFocus(bb.one(SELECTORS.USER));
}
Y.namespace('M.gradereport_history.UserSelector').superclass.show.call(this);
},
@ -296,14 +320,21 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
*/
postSearch: function(unused, args) {
var bb = this.get('boundingBox'),
firstAdded = bb.one(SELECTORS.FIRSTADDED);
firstAdded = bb.one(SELECTORS.FIRSTADDED),
firstUser;
// Hide the lightbox.
bb.one(SELECTORS.LIGHTBOX).addClass(CSS.HIDDEN);
// Sets the focus on the newly added user if we are appending results.
if (args.append && firstAdded) {
this.setUserTabFocus(firstAdded);
firstAdded.one(SELECTORS.USERSELECT).focus();
} else {
firstUser = bb.one(SELECTORS.USER);
if (firstUser) {
this.setUserTabFocus(firstUser);
}
}
},
@ -325,7 +356,6 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
node,
content,
fetchmore,
checked,
totalUsers;
// Decodes the result.
@ -347,7 +377,7 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// Create the div containing the users when it is a fresh search.
if (!args.append) {
users = create('<div class="'+CSS.USERS+'"></div>');
users = create('<div role="listbox" aria-activedescendant="" aria-multiselectable="true" class="'+CSS.USERS+'"></div>');
} else {
users = bb.one(SELECTORS.RESULTSUSERS);
}
@ -355,9 +385,10 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// Compile the template for each user node.
if (!this._userTemplate) {
this._userTemplate = Y.Handlebars.compile(
'<div class="{{CSS.USER}} {{selected}} clearfix" data-userid="{{userId}}">' +
'<div role="option" aria-selected="false" class="{{CSS.USER}} clearfix" ' +
'data-userid="{{userId}}">' +
'<div class="{{CSS.CHECKBOX}}">' +
'<input {{checked}} name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox"' +
'<input name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox" tabindex="-1"' +
'id="{{checkboxId}}" aria-describedby="{{checkboxId}} {{extraFieldsId}}"/>' +
'</div>' +
'<div class="{{CSS.PICTURE}}">{{{picture}}}</div>' +
@ -379,18 +410,15 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
count++;
user = result.response.users[i];
// If already selected, add class.
// If already selected.
if (Y.Object.hasKey(this._usersBufferList, user.userid)) {
selected = ' ' + CSS.SELECTED;
checked = 'checked';
selected = true;
} else {
selected = '';
checked = '';
selected = false;
}
node = create(userTemplate({
checkboxId: Y.guid(),
checked: checked,
COMPONENT: COMPONENT,
count: count,
CSS: CSS,
@ -398,11 +426,12 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
extraFieldsId: Y.guid(),
fullname: user.fullname,
picture: user.picture,
selected: selected,
userId: user.userid,
USP: USP
}));
this.markUserNode(node, selected);
// Noting the first user that was when adding more results.
if (args.append && firstAdded) {
users.all(SELECTORS.FIRSTADDED).removeClass(CSS.FIRSTADDED);
@ -509,18 +538,38 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
if (e.currentTarget !== checkbox) {
// We triggered the selection from another node, so we need to change the checkbox value.
checked = !checked;
checkbox.set('checked', checked);
}
if (checked) {
// Selecting the user.
this._usersBufferList[userId] = fullname;
user.addClass(CSS.SELECTED);
} else {
// De-selecting the user.
delete this._usersBufferList[userId];
delete this._usersBufferList[parseInt(userId, 10)]; // Also remove number'd keys.
user.removeClass(CSS.SELECTED);
}
this.markUserNode(user, checked);
},
/**
* Mark a user node as selected or not.
*
* This only takes care of the DOM side of things, not the internal mechanism
* storing what users have been selected or not.
*
* @param {Node} node The user node.
* @param {Boolean} selected True to mark as selected.
*/
markUserNode: function(node, selected) {
if (selected) {
node.addClass(CSS.SELECTED);
node.set('aria-selected', 'true');
node.one(SELECTORS.USERSELECT).set('checked', true);
} else {
node.removeClass(CSS.SELECTED);
node.set('aria-selected', 'false');
node.one(SELECTORS.USERSELECT).set('checked', false);
}
},
@ -543,7 +592,84 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
var namelist = Y.Object.values(this.get(USP.USERFULLNAMES));
Y.one(SELECTORS.SELECTEDNAMES).set('innerHTML', namelist.join(', '));
Y.one(SELECTORS.USERFULLNAMES).set('value', namelist.join());
},
/**
* User keyboard navigation.
*
* @method userKeyboardNavigation
*/
userKeyboardNavigation: function(e) {
e.preventDefault();
var bb = this.get('boundingBox'),
users = bb.all(SELECTORS.USER),
direction = 1,
user,
current = e.target.ancestor(SELECTORS.USER, true);
if (e.keyCode === 38) {
direction = -1;
}
user = this.findFocusableUser(users, current, direction);
if (user) {
user.one(SELECTORS.USERSELECT).focus();
this.setUserTabFocus(user);
}
},
/**
* Find the next or previous focusable node.
*
* @param {NodeList} users The list of users.
* @param {Node} user The user to start with.
* @param {Number} direction The direction in which to go.
* @return {Node} A user node.
* @method findNextFocusableUser
*/
findFocusableUser: function(users, user, direction) {
var index = users.indexOf(user);
if (users.size() < 1) {
Y.log('The users list is empty');
return null;
}
if (index < 0) {
Y.log('Unable to find the user in the list of users', 'debug', COMPONENT);
return users.item(0);
}
index += direction;
// Wrap the navigation when reaching the top of the bottom.
if (index < 0) {
index = users.size() - 1;
} else if (index >= users.size()) {
index = 0;
}
return users.item(index);
},
/**
* Set the user tab focus.
*
* @param {Node} user The user node.
* @method setUserTabFocus
*/
setUserTabFocus: function(user) {
if (this._userTabFocus) {
this._userTabFocus.setAttribute('tabindex', '-1');
}
this._userTabFocus = user.one(SELECTORS.USERSELECT);
this._userTabFocus.setAttribute('tabindex', '0');
this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID());
}
}, {
NAME : USP.NAME,
CSS_PREFIX : USP.CSS_PREFIX,
@ -691,7 +817,6 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
}
});
// Y.augment(Y.namespace('M.gradereport_history').UserSelector, Y.EventTarget);
Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), {
@ -743,7 +868,6 @@ Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) {
"handlebars",
"io-base",
"json-parse",
"moodle-core-notification-dialogue",
"overlay"
"moodle-core-notification-dialogue"
]
});

View File

@ -71,8 +71,8 @@ var CSS = {
SEARCHFIELD : 'usp-search-field',
SEARCHRESULTS : 'usp-search-results',
SELECTED : 'selected',
USER : 'user',
USERS : 'users',
USER : 'usp-user',
USERS : 'usp-users',
WRAP : 'usp-wrap'
};
var SELECTORS = {
@ -128,6 +128,15 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
*/
_usersBufferList: null,
/**
* The Node on which the focus is set.
*
* @property _userTabFocus
* @type Node
* @private
*/
_userTabFocus: null,
/**
* Compiled template function for a user node.
*
@ -149,7 +158,7 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
'<div class="{{CSS.SEARCH}}" role="search">' +
'<form>' +
'<input type="text" class="{{CSS.SEARCHFIELD}}" ' +
'aria-labelledby="{{get_string "search" "moodle"}}" value="" />' +
'aria-label="{{get_string "search" "moodle"}}" value="" />' +
'<input type="submit" class="{{CSS.SEARCHBTN}}"' +
'value="{{get_string "search" "moodle"}}">' +
'</form>' +
@ -194,9 +203,12 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// The button to finalize the selection.
bb.one(SELECTORS.FINISHBTN).on('click', this.finishSelectingUsers, this);
// Delegate the keyboard navigation in the users list.
bb.delegate('key', this.userKeyboardNavigation, 'down:38,40', SELECTORS.AJAXCONTENT, this);
// Delegate the action to select a user.
Y.delegate("click", this.selectUser, bb.one(SELECTORS.AJAXCONTENT), SELECTORS.USERSELECT, this);
Y.delegate("click", this.selectUser, bb.one(SELECTORS.AJAXCONTENT), SELECTORS.PICTURE, this);
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.USERSELECT, this);
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.PICTURE, this);
params = this.get(USP.PARAMS);
params.id = this.get(USP.COURSEID);
@ -221,10 +233,22 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// Leave the content as is, but reset the selection.
this._usersBufferList = Y.clone(this.get(USP.USERFULLNAMES));
bb = this.get('boundingBox');
bb.all(SELECTORS.USERSELECT).set('checked', false);
// Remove all the selected users.
bb.all(SELECTORS.USER).each(function(node) {
this.markUserNode(node, false);
}, this);
// Select the users.
Y.Object.each(this._usersBufferList, function(v, k) {
bb.one(SELECTORS.USERSELECT + '[name=' + USP.CHECKBOX_NAME_PREFIX + k + ']').set('checked', true);
});
var user = bb.one(SELECTORS.USER + '[data-userid="' + k + '"]');
if (user) {
this.markUserNode(user, true);
}
}, this);
// Reset the tab focus.
this.setUserTabFocus(bb.one(SELECTORS.USER));
}
Y.namespace('M.gradereport_history.UserSelector').superclass.show.call(this);
},
@ -296,14 +320,21 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
*/
postSearch: function(unused, args) {
var bb = this.get('boundingBox'),
firstAdded = bb.one(SELECTORS.FIRSTADDED);
firstAdded = bb.one(SELECTORS.FIRSTADDED),
firstUser;
// Hide the lightbox.
bb.one(SELECTORS.LIGHTBOX).addClass(CSS.HIDDEN);
// Sets the focus on the newly added user if we are appending results.
if (args.append && firstAdded) {
this.setUserTabFocus(firstAdded);
firstAdded.one(SELECTORS.USERSELECT).focus();
} else {
firstUser = bb.one(SELECTORS.USER);
if (firstUser) {
this.setUserTabFocus(firstUser);
}
}
},
@ -325,7 +356,6 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
node,
content,
fetchmore,
checked,
totalUsers;
// Decodes the result.
@ -347,7 +377,7 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// Create the div containing the users when it is a fresh search.
if (!args.append) {
users = create('<div class="'+CSS.USERS+'"></div>');
users = create('<div role="listbox" aria-activedescendant="" aria-multiselectable="true" class="'+CSS.USERS+'"></div>');
} else {
users = bb.one(SELECTORS.RESULTSUSERS);
}
@ -355,9 +385,10 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// Compile the template for each user node.
if (!this._userTemplate) {
this._userTemplate = Y.Handlebars.compile(
'<div class="{{CSS.USER}} {{selected}} clearfix" data-userid="{{userId}}">' +
'<div role="option" aria-selected="false" class="{{CSS.USER}} clearfix" ' +
'data-userid="{{userId}}">' +
'<div class="{{CSS.CHECKBOX}}">' +
'<input {{checked}} name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox"' +
'<input name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox" tabindex="-1"' +
'id="{{checkboxId}}" aria-describedby="{{checkboxId}} {{extraFieldsId}}"/>' +
'</div>' +
'<div class="{{CSS.PICTURE}}">{{{picture}}}</div>' +
@ -379,18 +410,15 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
count++;
user = result.response.users[i];
// If already selected, add class.
// If already selected.
if (Y.Object.hasKey(this._usersBufferList, user.userid)) {
selected = ' ' + CSS.SELECTED;
checked = 'checked';
selected = true;
} else {
selected = '';
checked = '';
selected = false;
}
node = create(userTemplate({
checkboxId: Y.guid(),
checked: checked,
COMPONENT: COMPONENT,
count: count,
CSS: CSS,
@ -398,11 +426,12 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
extraFieldsId: Y.guid(),
fullname: user.fullname,
picture: user.picture,
selected: selected,
userId: user.userid,
USP: USP
}));
this.markUserNode(node, selected);
// Noting the first user that was when adding more results.
if (args.append && firstAdded) {
users.all(SELECTORS.FIRSTADDED).removeClass(CSS.FIRSTADDED);
@ -509,18 +538,38 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
if (e.currentTarget !== checkbox) {
// We triggered the selection from another node, so we need to change the checkbox value.
checked = !checked;
checkbox.set('checked', checked);
}
if (checked) {
// Selecting the user.
this._usersBufferList[userId] = fullname;
user.addClass(CSS.SELECTED);
} else {
// De-selecting the user.
delete this._usersBufferList[userId];
delete this._usersBufferList[parseInt(userId, 10)]; // Also remove number'd keys.
user.removeClass(CSS.SELECTED);
}
this.markUserNode(user, checked);
},
/**
* Mark a user node as selected or not.
*
* This only takes care of the DOM side of things, not the internal mechanism
* storing what users have been selected or not.
*
* @param {Node} node The user node.
* @param {Boolean} selected True to mark as selected.
*/
markUserNode: function(node, selected) {
if (selected) {
node.addClass(CSS.SELECTED);
node.set('aria-selected', 'true');
node.one(SELECTORS.USERSELECT).set('checked', true);
} else {
node.removeClass(CSS.SELECTED);
node.set('aria-selected', 'false');
node.one(SELECTORS.USERSELECT).set('checked', false);
}
},
@ -543,7 +592,82 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
var namelist = Y.Object.values(this.get(USP.USERFULLNAMES));
Y.one(SELECTORS.SELECTEDNAMES).set('innerHTML', namelist.join(', '));
Y.one(SELECTORS.USERFULLNAMES).set('value', namelist.join());
},
/**
* User keyboard navigation.
*
* @method userKeyboardNavigation
*/
userKeyboardNavigation: function(e) {
e.preventDefault();
var bb = this.get('boundingBox'),
users = bb.all(SELECTORS.USER),
direction = 1,
user,
current = e.target.ancestor(SELECTORS.USER, true);
if (e.keyCode === 38) {
direction = -1;
}
user = this.findFocusableUser(users, current, direction);
if (user) {
user.one(SELECTORS.USERSELECT).focus();
this.setUserTabFocus(user);
}
},
/**
* Find the next or previous focusable node.
*
* @param {NodeList} users The list of users.
* @param {Node} user The user to start with.
* @param {Number} direction The direction in which to go.
* @return {Node} A user node.
* @method findNextFocusableUser
*/
findFocusableUser: function(users, user, direction) {
var index = users.indexOf(user);
if (users.size() < 1) {
return null;
}
if (index < 0) {
return users.item(0);
}
index += direction;
// Wrap the navigation when reaching the top of the bottom.
if (index < 0) {
index = users.size() - 1;
} else if (index >= users.size()) {
index = 0;
}
return users.item(index);
},
/**
* Set the user tab focus.
*
* @param {Node} user The user node.
* @method setUserTabFocus
*/
setUserTabFocus: function(user) {
if (this._userTabFocus) {
this._userTabFocus.setAttribute('tabindex', '-1');
}
this._userTabFocus = user.one(SELECTORS.USERSELECT);
this._userTabFocus.setAttribute('tabindex', '0');
this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID());
}
}, {
NAME : USP.NAME,
CSS_PREFIX : USP.CSS_PREFIX,
@ -691,7 +815,6 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
}
});
// Y.augment(Y.namespace('M.gradereport_history').UserSelector, Y.EventTarget);
Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), {
@ -743,7 +866,6 @@ Y.namespace('M.gradereport_history.UserSelector').init = function(cfg) {
"handlebars",
"io-base",
"json-parse",
"moodle-core-notification-dialogue",
"overlay"
"moodle-core-notification-dialogue"
]
});

View File

@ -69,8 +69,8 @@ var CSS = {
SEARCHFIELD : 'usp-search-field',
SEARCHRESULTS : 'usp-search-results',
SELECTED : 'selected',
USER : 'user',
USERS : 'users',
USER : 'usp-user',
USERS : 'usp-users',
WRAP : 'usp-wrap'
};
var SELECTORS = {
@ -126,6 +126,15 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
*/
_usersBufferList: null,
/**
* The Node on which the focus is set.
*
* @property _userTabFocus
* @type Node
* @private
*/
_userTabFocus: null,
/**
* Compiled template function for a user node.
*
@ -147,7 +156,7 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
'<div class="{{CSS.SEARCH}}" role="search">' +
'<form>' +
'<input type="text" class="{{CSS.SEARCHFIELD}}" ' +
'aria-labelledby="{{get_string "search" "moodle"}}" value="" />' +
'aria-label="{{get_string "search" "moodle"}}" value="" />' +
'<input type="submit" class="{{CSS.SEARCHBTN}}"' +
'value="{{get_string "search" "moodle"}}">' +
'</form>' +
@ -192,9 +201,12 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// The button to finalize the selection.
bb.one(SELECTORS.FINISHBTN).on('click', this.finishSelectingUsers, this);
// Delegate the keyboard navigation in the users list.
bb.delegate('key', this.userKeyboardNavigation, 'down:38,40', SELECTORS.AJAXCONTENT, this);
// Delegate the action to select a user.
Y.delegate("click", this.selectUser, bb.one(SELECTORS.AJAXCONTENT), SELECTORS.USERSELECT, this);
Y.delegate("click", this.selectUser, bb.one(SELECTORS.AJAXCONTENT), SELECTORS.PICTURE, this);
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.USERSELECT, this);
Y.delegate('click', this.selectUser, SELECTORS.AJAXCONTENT, SELECTORS.PICTURE, this);
params = this.get(USP.PARAMS);
params.id = this.get(USP.COURSEID);
@ -219,10 +231,22 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// Leave the content as is, but reset the selection.
this._usersBufferList = Y.clone(this.get(USP.USERFULLNAMES));
bb = this.get('boundingBox');
bb.all(SELECTORS.USERSELECT).set('checked', false);
// Remove all the selected users.
bb.all(SELECTORS.USER).each(function(node) {
this.markUserNode(node, false);
}, this);
// Select the users.
Y.Object.each(this._usersBufferList, function(v, k) {
bb.one(SELECTORS.USERSELECT + '[name=' + USP.CHECKBOX_NAME_PREFIX + k + ']').set('checked', true);
});
var user = bb.one(SELECTORS.USER + '[data-userid="' + k + '"]');
if (user) {
this.markUserNode(user, true);
}
}, this);
// Reset the tab focus.
this.setUserTabFocus(bb.one(SELECTORS.USER));
}
Y.namespace('M.gradereport_history.UserSelector').superclass.show.call(this);
},
@ -294,14 +318,21 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
*/
postSearch: function(unused, args) {
var bb = this.get('boundingBox'),
firstAdded = bb.one(SELECTORS.FIRSTADDED);
firstAdded = bb.one(SELECTORS.FIRSTADDED),
firstUser;
// Hide the lightbox.
bb.one(SELECTORS.LIGHTBOX).addClass(CSS.HIDDEN);
// Sets the focus on the newly added user if we are appending results.
if (args.append && firstAdded) {
this.setUserTabFocus(firstAdded);
firstAdded.one(SELECTORS.USERSELECT).focus();
} else {
firstUser = bb.one(SELECTORS.USER);
if (firstUser) {
this.setUserTabFocus(firstUser);
}
}
},
@ -323,7 +354,6 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
node,
content,
fetchmore,
checked,
totalUsers;
// Decodes the result.
@ -345,7 +375,7 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// Create the div containing the users when it is a fresh search.
if (!args.append) {
users = create('<div class="'+CSS.USERS+'"></div>');
users = create('<div role="listbox" aria-activedescendant="" aria-multiselectable="true" class="'+CSS.USERS+'"></div>');
} else {
users = bb.one(SELECTORS.RESULTSUSERS);
}
@ -353,9 +383,10 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
// Compile the template for each user node.
if (!this._userTemplate) {
this._userTemplate = Y.Handlebars.compile(
'<div class="{{CSS.USER}} {{selected}} clearfix" data-userid="{{userId}}">' +
'<div role="option" aria-selected="false" class="{{CSS.USER}} clearfix" ' +
'data-userid="{{userId}}">' +
'<div class="{{CSS.CHECKBOX}}">' +
'<input {{checked}} name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox"' +
'<input name="{{USP.CHECKBOX_NAME_PREFIX}}{{userId}}" type="checkbox" tabindex="-1"' +
'id="{{checkboxId}}" aria-describedby="{{checkboxId}} {{extraFieldsId}}"/>' +
'</div>' +
'<div class="{{CSS.PICTURE}}">{{{picture}}}</div>' +
@ -377,18 +408,15 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
count++;
user = result.response.users[i];
// If already selected, add class.
// If already selected.
if (Y.Object.hasKey(this._usersBufferList, user.userid)) {
selected = ' ' + CSS.SELECTED;
checked = 'checked';
selected = true;
} else {
selected = '';
checked = '';
selected = false;
}
node = create(userTemplate({
checkboxId: Y.guid(),
checked: checked,
COMPONENT: COMPONENT,
count: count,
CSS: CSS,
@ -396,11 +424,12 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
extraFieldsId: Y.guid(),
fullname: user.fullname,
picture: user.picture,
selected: selected,
userId: user.userid,
USP: USP
}));
this.markUserNode(node, selected);
// Noting the first user that was when adding more results.
if (args.append && firstAdded) {
users.all(SELECTORS.FIRSTADDED).removeClass(CSS.FIRSTADDED);
@ -507,18 +536,38 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
if (e.currentTarget !== checkbox) {
// We triggered the selection from another node, so we need to change the checkbox value.
checked = !checked;
checkbox.set('checked', checked);
}
if (checked) {
// Selecting the user.
this._usersBufferList[userId] = fullname;
user.addClass(CSS.SELECTED);
} else {
// De-selecting the user.
delete this._usersBufferList[userId];
delete this._usersBufferList[parseInt(userId, 10)]; // Also remove number'd keys.
user.removeClass(CSS.SELECTED);
}
this.markUserNode(user, checked);
},
/**
* Mark a user node as selected or not.
*
* This only takes care of the DOM side of things, not the internal mechanism
* storing what users have been selected or not.
*
* @param {Node} node The user node.
* @param {Boolean} selected True to mark as selected.
*/
markUserNode: function(node, selected) {
if (selected) {
node.addClass(CSS.SELECTED);
node.set('aria-selected', 'true');
node.one(SELECTORS.USERSELECT).set('checked', true);
} else {
node.removeClass(CSS.SELECTED);
node.set('aria-selected', 'false');
node.one(SELECTORS.USERSELECT).set('checked', false);
}
},
@ -541,7 +590,84 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
var namelist = Y.Object.values(this.get(USP.USERFULLNAMES));
Y.one(SELECTORS.SELECTEDNAMES).set('innerHTML', namelist.join(', '));
Y.one(SELECTORS.USERFULLNAMES).set('value', namelist.join());
},
/**
* User keyboard navigation.
*
* @method userKeyboardNavigation
*/
userKeyboardNavigation: function(e) {
e.preventDefault();
var bb = this.get('boundingBox'),
users = bb.all(SELECTORS.USER),
direction = 1,
user,
current = e.target.ancestor(SELECTORS.USER, true);
if (e.keyCode === 38) {
direction = -1;
}
user = this.findFocusableUser(users, current, direction);
if (user) {
user.one(SELECTORS.USERSELECT).focus();
this.setUserTabFocus(user);
}
},
/**
* Find the next or previous focusable node.
*
* @param {NodeList} users The list of users.
* @param {Node} user The user to start with.
* @param {Number} direction The direction in which to go.
* @return {Node} A user node.
* @method findNextFocusableUser
*/
findFocusableUser: function(users, user, direction) {
var index = users.indexOf(user);
if (users.size() < 1) {
Y.log('The users list is empty');
return null;
}
if (index < 0) {
Y.log('Unable to find the user in the list of users', 'debug', COMPONENT);
return users.item(0);
}
index += direction;
// Wrap the navigation when reaching the top of the bottom.
if (index < 0) {
index = users.size() - 1;
} else if (index >= users.size()) {
index = 0;
}
return users.item(index);
},
/**
* Set the user tab focus.
*
* @param {Node} user The user node.
* @method setUserTabFocus
*/
setUserTabFocus: function(user) {
if (this._userTabFocus) {
this._userTabFocus.setAttribute('tabindex', '-1');
}
this._userTabFocus = user.one(SELECTORS.USERSELECT);
this._userTabFocus.setAttribute('tabindex', '0');
this.get('boundingBox').one(SELECTORS.RESULTSUSERS).setAttribute('aria-activedescendant', this._userTabFocus.generateID());
}
}, {
NAME : USP.NAME,
CSS_PREFIX : USP.CSS_PREFIX,
@ -689,7 +815,6 @@ Y.namespace('M.gradereport_history').UserSelector = Y.extend(USERSELECTOR, M.cor
}
});
// Y.augment(Y.namespace('M.gradereport_history').UserSelector, Y.EventTarget);
Y.Base.modifyAttrs(Y.namespace('M.gradereport_history.UserSelector'), {

View File

@ -7,8 +7,7 @@
"handlebars",
"io-base",
"json-parse",
"moodle-core-notification-dialogue",
"overlay"
"moodle-core-notification-dialogue"
]
}
}