mirror of
https://github.com/moodle/moodle.git
synced 2025-07-23 15:22:05 +02:00
MDL-36002 core: Make drag and drop keyboard friendly
This change adds keyboard support to core drag and drop. Selecting a grab handle and hitting enter (or space) will open a menu of possible drop targets and allow you to choose one. The only other UI change this implies is we need to show the grab for blocks.
This commit is contained in:
committed by
Damyon Wiese
parent
b05cfa0e67
commit
7c271b918e
@@ -2965,6 +2965,9 @@ function include_course_ajax($course, $usedmodules = array(), $enabledmodules =
|
||||
'markedthistopic',
|
||||
'move',
|
||||
'movesection',
|
||||
'movecontent',
|
||||
'aftercontent',
|
||||
'emptydragdropregion'
|
||||
), 'moodle');
|
||||
|
||||
// Include format-specific strings
|
||||
|
1
course/yui/dragdrop/dragdrop.js
vendored
1
course/yui/dragdrop/dragdrop.js
vendored
@@ -295,6 +295,7 @@ YUI.add('moodle-course-dragdrop', function(Y) {
|
||||
resources.addClass(CSS.SECTION);
|
||||
sectionnode.one('.'+CSS.CONTENT+' div.'+CSS.SUMMARY).insert(resources, 'after');
|
||||
}
|
||||
resources.setAttribute('data-draggroups', this.groups.join(' '));
|
||||
// Define empty ul as droptarget, so that item could be moved to empty list
|
||||
var tar = new Y.DD.Drop({
|
||||
node: resources,
|
||||
|
@@ -115,6 +115,7 @@ $string['administratorsandteachers'] = 'Administrators and teachers';
|
||||
$string['advanced'] = 'Advanced';
|
||||
$string['advancedfilter'] = 'Advanced search';
|
||||
$string['advancedsettings'] = 'Advanced settings';
|
||||
$string['aftercontent'] = 'After {$a}';
|
||||
$string['again'] = 'again';
|
||||
$string['aimid'] = 'AIM ID';
|
||||
$string['ajaxuse'] = 'AJAX and Javascript';
|
||||
@@ -638,6 +639,7 @@ $string['emailpasswordsent'] = 'Thank you for confirming the change of password.
|
||||
An email containing your new password has been sent to your address at<br /><b>{$a->email}</b>.<br />
|
||||
The new password was automatically generated - you might like to
|
||||
<a href="{$a->link}">change your password</a> to something easier to remember.';
|
||||
$string['emptydragdropregion'] = 'empty region';
|
||||
$string['enable'] = 'Enable';
|
||||
$string['encryptedcode'] = 'Encrypted code';
|
||||
$string['english'] = 'English';
|
||||
@@ -1073,6 +1075,7 @@ $string['moreinformation'] = 'More information about this error';
|
||||
$string['moreprofileinfoneeded'] = 'Please tell us more about yourself';
|
||||
$string['mostrecently'] = 'most recently';
|
||||
$string['move'] = 'Move';
|
||||
$string['movecontent'] = 'Move {$a}';
|
||||
$string['movecategorycontentto'] = 'Move into';
|
||||
$string['movecategoryto'] = 'Move category to:';
|
||||
$string['movecontentstoanothercategory'] = 'Move contents to another category';
|
||||
|
@@ -300,6 +300,11 @@ class page_requirements_manager {
|
||||
if (!empty($page->cm->id)) {
|
||||
$params['cmid'] = $page->cm->id;
|
||||
}
|
||||
// Strings for drag and drop.
|
||||
$this->strings_for_js(array('movecontent',
|
||||
'aftercontent',
|
||||
'emptydragdropregion'),
|
||||
'moodle');
|
||||
$page->requires->yui_module('moodle-core-blocks', 'M.core_blocks.init_dragdrop', array($params), null, true);
|
||||
}
|
||||
}
|
||||
|
@@ -115,7 +115,7 @@ Y.extend(DRAGBLOCK, M.core.dragdrop, {
|
||||
blocklist.each(function(blocknode) {
|
||||
var move = blocknode.one('a.'+CSS.EDITINGMOVE);
|
||||
if (move) {
|
||||
move.remove();
|
||||
move.replace(this.get_drag_handle(move.getAttribute('title'), '', 'icon', true));
|
||||
blocknode.one('.'+CSS.HEADER).setStyle('cursor', 'move');
|
||||
}
|
||||
}, this);
|
||||
@@ -379,7 +379,8 @@ M.core.blockdraganddrop.init = function(params) {
|
||||
M.core_blocks = M.core_blocks || {};
|
||||
M.core_blocks.init_dragdrop = function(params) {
|
||||
M.core.blockdraganddrop.init(params);
|
||||
};/**
|
||||
};
|
||||
/**
|
||||
* This file contains the drag and drop manager class.
|
||||
*
|
||||
* Provides drag and drop functionality for blocks.
|
||||
|
File diff suppressed because one or more lines are too long
@@ -115,7 +115,7 @@ Y.extend(DRAGBLOCK, M.core.dragdrop, {
|
||||
blocklist.each(function(blocknode) {
|
||||
var move = blocknode.one('a.'+CSS.EDITINGMOVE);
|
||||
if (move) {
|
||||
move.remove();
|
||||
move.replace(this.get_drag_handle(move.getAttribute('title'), '', 'icon', true));
|
||||
blocknode.one('.'+CSS.HEADER).setStyle('cursor', 'move');
|
||||
}
|
||||
}, this);
|
||||
@@ -379,7 +379,8 @@ M.core.blockdraganddrop.init = function(params) {
|
||||
M.core_blocks = M.core_blocks || {};
|
||||
M.core_blocks.init_dragdrop = function(params) {
|
||||
M.core.blockdraganddrop.init(params);
|
||||
};/**
|
||||
};
|
||||
/**
|
||||
* This file contains the drag and drop manager class.
|
||||
*
|
||||
* Provides drag and drop functionality for blocks.
|
||||
|
218
lib/yui/dragdrop/dragdrop.js
vendored
218
lib/yui/dragdrop/dragdrop.js
vendored
@@ -2,7 +2,8 @@ YUI.add('moodle-core-dragdrop', function(Y) {
|
||||
var MOVEICON = {
|
||||
pix: "i/move_2d",
|
||||
largepix: "i/dragdrop",
|
||||
component: 'moodle'
|
||||
component: 'moodle',
|
||||
cssclass: 'moodle-core-dragdrop-draghandle'
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -33,6 +34,8 @@ YUI.add('moodle-core-dragdrop', function(Y) {
|
||||
Y.DD.DDM.on('drop:hit', this.global_drop_hit, this);
|
||||
// Listen for all drop:miss events
|
||||
Y.DD.DDM.on('drag:dropmiss', this.global_drag_dropmiss, this);
|
||||
|
||||
Y.on('key', this.global_keydown, window, 'down:space,enter,esc', this);
|
||||
},
|
||||
|
||||
get_drag_handle: function(title, classname, iconclass, large) {
|
||||
@@ -53,7 +56,11 @@ YUI.add('moodle-core-dragdrop', function(Y) {
|
||||
var dragelement = Y.Node.create('<span></span>')
|
||||
.addClass(classname)
|
||||
.setAttribute('title', title)
|
||||
.setAttribute('tabIndex', 0)
|
||||
.setAttribute('data-draggroups', this.groups);
|
||||
dragelement.appendChild(dragicon);
|
||||
dragelement.addClass(MOVEICON.cssclass);
|
||||
|
||||
return dragelement;
|
||||
},
|
||||
|
||||
@@ -194,6 +201,213 @@ YUI.add('moodle-core-dragdrop', function(Y) {
|
||||
this.drop_hit(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* This is used to build the text for the heading of the keyboard
|
||||
* drag drop menu and the text for the nodes in the list.
|
||||
* @method find_element_text
|
||||
* @param {Node} n The node to start searching for a valid text node.
|
||||
* @returns {string} The text of the first text-like child node of n.
|
||||
*/
|
||||
find_element_text : function(n) {
|
||||
// The valid node types to get text from.
|
||||
var nodes = n.all('h2, h3, h4, h5, span, p');
|
||||
var text = '';
|
||||
|
||||
nodes.each(function () {
|
||||
if (text == '') {
|
||||
if (Y.Lang.trim(this.get('text')) != '') {
|
||||
text = this.get('text');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (text != '') {
|
||||
return text;
|
||||
}
|
||||
return M.util.get_string('emptydragdropregion', 'moodle');
|
||||
},
|
||||
|
||||
/**
|
||||
* This is used to initiate a keyboard version of a drag and drop.
|
||||
* A dialog will open listing all the valid drop targets that can be selected
|
||||
* using tab, tab, tab, enter.
|
||||
* @method global_start_keyboard_drag
|
||||
* @param {Event} e The keydown / click event on the grab handle.
|
||||
* @param {Node} dragcontainer The resolved draggable node (an ancestor of the drag handle).
|
||||
*/
|
||||
global_start_keyboard_drag : function(e, dragcontainer) {
|
||||
M.core.dragdrop.keydragcontainer = dragcontainer;
|
||||
|
||||
// Indicate to a screenreader the node that is selected for drag and drop.
|
||||
dragcontainer.setAttribute('aria-grabbed', 'true');
|
||||
// Get the name of the thing to move.
|
||||
var nodetitle = this.find_element_text(dragcontainer);
|
||||
var dialogtitle = M.util.get_string('movecontent', 'moodle', nodetitle);
|
||||
|
||||
// Build the list of drop targets.
|
||||
var droplist = Y.Node.create('<ul></ul>');
|
||||
droplist.addClass('dragdrop-keyboard-drag');
|
||||
var listitem;
|
||||
var listitemtext;
|
||||
|
||||
// Search for possible drop targets.
|
||||
var droptargets = Y.all('.' + this.samenodeclass + ', .' + this.parentnodeclass);
|
||||
|
||||
droptargets.each(function (node) {
|
||||
var validdrop = false, labelroot = node;
|
||||
if (node.drop && node.drop.inGroup(this.groups) && node.drop.get('node') != dragcontainer) {
|
||||
// This is a drag and drop target with the same class as the grabbed node.
|
||||
validdrop = true;
|
||||
} else {
|
||||
var elementgroups = node.getAttribute('data-draggroups').split(' ');
|
||||
var i, j;
|
||||
for (i = 0; i < elementgroups.length; i++) {
|
||||
for (j = 0; j < this.groups.length; j++) {
|
||||
if (elementgroups[i] == this.groups[j]) {
|
||||
// This is a parent node of the grabbed node (used for dropping in empty sections).
|
||||
validdrop = true;
|
||||
// This node will have no text - so we get the first valid text from the parent.
|
||||
labelroot = node.get('parentNode');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (validdrop) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (validdrop) {
|
||||
// It is a valid drop target - create a list item for it.
|
||||
listitem = Y.Node.create('<li></li>');
|
||||
listlink = Y.Node.create('<a></a>');
|
||||
nodetitle = this.find_element_text(labelroot);
|
||||
|
||||
listitemtext = M.util.get_string('aftercontent', 'moodle', nodetitle);
|
||||
listlink.setContent(listitemtext);
|
||||
|
||||
// Add a data attribute so we can get the real drop target.
|
||||
listlink.setAttribute('data-drop-target', node.get('id'));
|
||||
// Notify the screen reader this is a valid drop target.
|
||||
listlink.setAttribute('aria-dropeffect', 'move');
|
||||
// Allow tabbing to the link.
|
||||
listlink.setAttribute('tabindex', '0');
|
||||
|
||||
// Set the event listeners for enter, space or click.
|
||||
listlink.on('click', this.global_keyboard_drop, this);
|
||||
listlink.on('key', this.global_keyboard_drop, 'down:enter,32', this);
|
||||
|
||||
// Add to the list or drop targets.
|
||||
listitem.append(listlink);
|
||||
droplist.append(listitem);
|
||||
}
|
||||
}, this);
|
||||
|
||||
// Create the dialog for the interaction.
|
||||
M.core.dragdrop.dropui = new M.core.dialogue({
|
||||
headerContent : dialogtitle,
|
||||
bodyContent : droplist,
|
||||
draggable : true
|
||||
});
|
||||
|
||||
// Focus the first drop target.
|
||||
if (droplist.one('a')) {
|
||||
droplist.one('a').focus();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This is used to complete a keyboard version of a drag and drop.
|
||||
* A drop event will be simulated based on the drag and drop nodes.
|
||||
* @method global_keyboard_drop
|
||||
* @param {Event} e The keydown / click event on the proxy drop node.
|
||||
*/
|
||||
global_keyboard_drop : function(e) {
|
||||
// The drag node was saved.
|
||||
var dragcontainer = M.core.dragdrop.keydragcontainer;
|
||||
dragcontainer.setAttribute('aria-grabbed', 'false');
|
||||
// The real drop node is stored in an attribute of the proxy.
|
||||
var dragtarget = Y.one('#' + e.target.getAttribute('data-drop-target'));
|
||||
|
||||
// Close the dialog.
|
||||
M.core.dragdrop.dropui.hide();
|
||||
// Cancel the event.
|
||||
e.preventDefault();
|
||||
// Convert to a drag drop event.
|
||||
e.drag = dragcontainer.drop;
|
||||
e.drop = new Y.DD.Drop({
|
||||
node: dragtarget,
|
||||
groups : this.groups
|
||||
});
|
||||
|
||||
this.global_drop_over(e);
|
||||
dragcontainer.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* This is used to cancel a keyboard version of a drag and drop.
|
||||
*
|
||||
* @method global_cancel_keyboard_drag
|
||||
*/
|
||||
global_cancel_keyboard_drag : function() {
|
||||
if (M.core.dragdrop.keydragcontainer) {
|
||||
M.core.dragdrop.keydragcontainer.setAttribute('aria-grabbed', 'false');
|
||||
M.core.dragdrop.keydragcontainer.focus();
|
||||
M.core.dragdrop.keydragcontainer = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Process key events on the drag handles.
|
||||
* @method global_keydown
|
||||
* @param {Event} e The keydown / click event on the drag handle.
|
||||
*/
|
||||
global_keydown : function(e) {
|
||||
var draghandle = e.target,
|
||||
dragcontainer,
|
||||
draggroups;
|
||||
|
||||
if (e.keyCode == 27 ) {
|
||||
// Escape to cancel from anywhere.
|
||||
this.global_cancel_keyboard_drag();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process events on a drag handle.
|
||||
if (!draghandle.hasClass(MOVEICON.cssclass)) {
|
||||
return;
|
||||
}
|
||||
// Do nothing if not space or enter.
|
||||
if (e.keyCode != 13 && e.keyCode != 32) {
|
||||
return;
|
||||
}
|
||||
// Check the drag groups to see if we are the handler for this node.
|
||||
draggroups = e.target.getAttribute('data-draggroups').split(' ');
|
||||
var i, j, validgroup = false;
|
||||
|
||||
for (i = 0; i < draggroups.length; i++) {
|
||||
for (j = 0; j < this.groups.length; j++) {
|
||||
if (draggroups[i] == this.groups[j]) {
|
||||
validgroup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (validgroup) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!validgroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Valid event - start the keyboard drag.
|
||||
dragcontainer = draghandle.ancestor('.yui3-dd-drop');
|
||||
this.global_start_keyboard_drag(e, dragcontainer);
|
||||
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
/*
|
||||
* Abstract functions definitions
|
||||
*/
|
||||
@@ -211,4 +425,4 @@ YUI.add('moodle-core-dragdrop', function(Y) {
|
||||
M.core = M.core || {};
|
||||
M.core.dragdrop = DRAGDROP;
|
||||
|
||||
}, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'moodle-core-notification']});
|
||||
}, '@VERSION@', {requires:['base', 'node', 'io', 'dom', 'dd', 'event-key', 'event-focus', 'moodle-core-notification']});
|
||||
|
4
lib/yui/src/blocks/js/blocks.js
vendored
4
lib/yui/src/blocks/js/blocks.js
vendored
@@ -113,7 +113,7 @@ Y.extend(DRAGBLOCK, M.core.dragdrop, {
|
||||
blocklist.each(function(blocknode) {
|
||||
var move = blocknode.one('a.'+CSS.EDITINGMOVE);
|
||||
if (move) {
|
||||
move.remove();
|
||||
move.replace(this.get_drag_handle(move.getAttribute('title'), '', 'icon', true));
|
||||
blocknode.one('.'+CSS.HEADER).setStyle('cursor', 'move');
|
||||
}
|
||||
}, this);
|
||||
@@ -377,4 +377,4 @@ M.core.blockdraganddrop.init = function(params) {
|
||||
M.core_blocks = M.core_blocks || {};
|
||||
M.core_blocks.init_dragdrop = function(params) {
|
||||
M.core.blockdraganddrop.init(params);
|
||||
};
|
||||
};
|
||||
|
@@ -1439,4 +1439,7 @@ div.badge .expireimage { width: 100px; height: 100px; left: 20px; top: 0px; }
|
||||
.dir-rtl .menu.align-tl-tr {right: 100%;left: auto;}
|
||||
.dir-rtl .menu.align-tr-tr {right: auto;left: 0;}
|
||||
.dir-rtl .menu.align-bl-tr {right: 100%;left: auto;}
|
||||
.dir-rtl .menu.align-br-tr {right: auto;left: 0;}
|
||||
.dir-rtl .menu.align-br-tr {right: auto;left: 0;}
|
||||
|
||||
ul.dragdrop-keyboard-drag li { list-style-type: none; }
|
||||
.block-control-actions .moodle-core-dragdrop-draghandle img { width: 12px; height: 12px; }
|
||||
|
@@ -2065,3 +2065,12 @@ div.badge .expireimage {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
ul.dragdrop-keyboard-drag li {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.block-control-actions .moodle-core-dragdrop-draghandle img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user