mirror of
https://github.com/google/blockly.git
synced 2026-01-07 09:00:11 +01:00
567 lines
19 KiB
HTML
567 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Blockly Demo: Keyboard Navigation</title>
|
|
<script src="../../blockly_compressed.js"></script>
|
|
<script src="../../blocks_compressed.js"></script>
|
|
<script src="../../javascript_compressed.js"></script>
|
|
<script src="../../msg/js/en.js"></script>
|
|
<script src="basic_cursor.js"></script>
|
|
<style>
|
|
body {
|
|
background-color: #fff;
|
|
font-family: sans-serif;
|
|
}
|
|
|
|
h1 {
|
|
font-weight: normal;
|
|
font-size: 140%;
|
|
}
|
|
.wrapper {
|
|
display: flex;
|
|
}
|
|
#keyboard_nav {
|
|
background-color: #ededed;
|
|
border: 1px solid black;
|
|
padding: 1em;
|
|
}
|
|
#keyboard_announce {
|
|
font-size: 1.5em;
|
|
font-weight: 500;
|
|
text-align: center;
|
|
}
|
|
#keyboard_mappings {
|
|
font-size: 1.3em;
|
|
font-weight: 400;
|
|
}
|
|
label {
|
|
margin-right: .5em;
|
|
min-width: 100px;
|
|
}
|
|
div[data-actionname] {
|
|
display: flex;
|
|
width: 100%;
|
|
}
|
|
select {
|
|
font-size: .8em;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1><a href="https://developers.google.com/blockly/">Blockly</a> >
|
|
<a href="../index.html">Demos</a> > Keyboard Navigation</h1>
|
|
|
|
<p>Keyboard Navigation is our first step towards an accessible Blockly.<br />
|
|
You can enter accessibility mode by <b>shift clicking anywhere on the
|
|
workspace or on a block</b>. <br />Some basic commands for moving around are below.
|
|
More complete documentation is still in progress.<br /><br />
|
|
<b>Workspace Navigation</b><br />
|
|
W: Previous block/field/input at the same level<br />
|
|
A: Up one level (Field (or input) -> Block -> Input (or field) -> Block ->
|
|
Stack -> Workspace)<br />
|
|
S: Next block/field/input at the same level<br />
|
|
D: Down one level (Workspace -> Stack -> Block -> Input (or field) -> Block
|
|
-> Field (or input))<br />
|
|
T: Will open the toolbox. Once in there you can moving around using the WASD keys. And insert a block by hitting Enter<br />
|
|
X: While on a connection hit X to disconnect the block after the cursor<br /><br />
|
|
|
|
<b>Pre Order Traversal</b><br />
|
|
Feel free to just play around in accessibility mode or hit the button below to see the demo.
|
|
The demo uses <a href="https://en.wikipedia.org/wiki/Tree_traversal#Pre-order_(NLR)">preorder tree traversal</a>
|
|
as an alternative way to navigate the blocks,
|
|
connections, and fields on the workspace.<br /><br />
|
|
|
|
<!-- TODO: Update when we add keyboard navigation to site -->
|
|
<!-- <p>→ More info on <a href="">Keyboard Navigation</a>.</p> -->
|
|
|
|
<b>Cursor</b><br />
|
|
The cursor controls how the user navigates the blocks, inputs, fields and connections on a workspace.
|
|
This demo shows two different cursors:<br />
|
|
<b>Default Cursor:</b> Allow the user to go to the previous, next, in or out location.<br />
|
|
<b>Basic Cursor:</b> Using the pre order traversal allows the user to go to the next and previous location.
|
|
</p>
|
|
|
|
<p>
|
|
<label for="accessibilityModeCheck">Enable Accessibility Mode:</label>
|
|
<input type="checkbox" onclick="toggleAccessibilityMode(this.checked)" id="accessibilityModeCheck">
|
|
<select id="cursorChanger" name="cursor" onchange="changeCursor(this.value)">
|
|
<option value="default">Default Cursor</option>
|
|
<option value="basic">Basic Cursor</option>
|
|
</select>
|
|
|
|
<button onclick="preOrderDemo()">Start Pre-order Demo</button>
|
|
<button onclick="stopDemo()">Stop Pre-order Demo</button>
|
|
<label for="displayKeyMappings">Open Key Mappings:</label>
|
|
<input type="checkbox" onclick="toggleDisplayKeyMappings(this.checked)" id="displayKeyMappings">
|
|
</p>
|
|
|
|
<div class="wrapper">
|
|
<div id="blocklyDiv" style="height: 480px; width: 600px;"></div>
|
|
<div id="keyboard_nav" style="display:none">
|
|
<p id="keyboard_announce" aria-live="assertive">Set key mappings below</p>
|
|
<form id="keyboard_mappings"></form>
|
|
</div>
|
|
</div>
|
|
|
|
<xml xmlns="https://developers.google.com/blockly/xml" id="toolbox" style="display: none">
|
|
<category name="Logic" colour="%{BKY_LOGIC_HUE}">
|
|
<block type="controls_if"></block>
|
|
<block type="logic_compare"></block>
|
|
<block type="logic_operation"></block>
|
|
<block type="logic_negate"></block>
|
|
<block type="logic_boolean"></block>
|
|
</category>
|
|
<category name="Loops" colour="%{BKY_LOOPS_HUE}">
|
|
<block type="controls_repeat_ext">
|
|
<value name="TIMES">
|
|
<block type="math_number">
|
|
<field name="NUM">10</field>
|
|
</block>
|
|
</value>
|
|
</block>
|
|
<block type="controls_whileUntil"></block>
|
|
</category>
|
|
<category name="Math" colour="%{BKY_MATH_HUE}">
|
|
<block type="math_number">
|
|
<field name="NUM">123</field>
|
|
</block>
|
|
<block type="math_arithmetic"></block>
|
|
<block type="math_single"></block>
|
|
</category>
|
|
<category name="Text" colour="%{BKY_TEXTS_HUE}">
|
|
<block type="text"></block>
|
|
<block type="text_length"></block>
|
|
<block type="text_print"></block>
|
|
</category>
|
|
</xml>
|
|
|
|
<xml xmlns="https://developers.google.com/blockly/xml" id="startBlocks" style="display: none">
|
|
<variables>
|
|
<variable id="~GNXm@Z(wclI]t3zTf.g">list</variable>
|
|
<variable id="8]s[S+Gy+%k7HoFup])m">item</variable>
|
|
</variables>
|
|
<block type="controls_if" x="37" y="162">
|
|
<value name="IF0">
|
|
<block type="logic_compare">
|
|
<field name="OP">EQ</field>
|
|
<value name="A">
|
|
<block type="math_arithmetic">
|
|
<field name="OP">ADD</field>
|
|
<value name="A">
|
|
<shadow type="math_number">
|
|
<field name="NUM">1</field>
|
|
</shadow>
|
|
</value>
|
|
<value name="B">
|
|
<shadow type="math_number">
|
|
<field name="NUM">1</field>
|
|
</shadow>
|
|
</value>
|
|
</block>
|
|
</value>
|
|
<value name="B">
|
|
<block type="math_single">
|
|
<field name="OP">ROOT</field>
|
|
<value name="NUM">
|
|
<shadow type="math_number">
|
|
<field name="NUM">9</field>
|
|
</shadow>
|
|
<block type="math_number">
|
|
<field name="NUM">123</field>
|
|
</block>
|
|
</value>
|
|
</block>
|
|
</value>
|
|
</block>
|
|
</value>
|
|
<statement name="DO0">
|
|
<block type="lists_setIndex">
|
|
<mutation at="true"></mutation>
|
|
<field name="MODE">SET</field>
|
|
<field name="WHERE">FROM_START</field>
|
|
<value name="LIST">
|
|
<block type="variables_get">
|
|
<field name="VAR" id="~GNXm@Z(wclI]t3zTf.g">list</field>
|
|
</block>
|
|
</value>
|
|
<next>
|
|
<block type="text_append">
|
|
<field name="VAR" id="8]s[S+Gy+%k7HoFup])m">item</field>
|
|
<value name="TEXT">
|
|
<shadow type="text">
|
|
<field name="TEXT"></field>
|
|
</shadow>
|
|
</value>
|
|
</block>
|
|
</next>
|
|
</block>
|
|
</statement>
|
|
<next>
|
|
<block type="controls_repeat_ext">
|
|
<value name="TIMES">
|
|
<shadow type="math_number">
|
|
<field name="NUM">10</field>
|
|
</shadow>
|
|
</value>
|
|
</block>
|
|
</next>
|
|
</block>
|
|
</xml>
|
|
|
|
<script>
|
|
var demoWorkspace = Blockly.inject('blocklyDiv',
|
|
{media: '../../media/',
|
|
toolbox: document.getElementById('toolbox')});
|
|
Blockly.Xml.domToWorkspace(document.getElementById('startBlocks'),
|
|
demoWorkspace);
|
|
var timeout;
|
|
|
|
var actions = [
|
|
Blockly.navigation.ACTION_PREVIOUS,
|
|
Blockly.navigation.ACTION_OUT,
|
|
Blockly.navigation.ACTION_NEXT,
|
|
Blockly.navigation.ACTION_IN,
|
|
Blockly.navigation.ACTION_INSERT,
|
|
Blockly.navigation.ACTION_MARK,
|
|
Blockly.navigation.ACTION_DISCONNECT,
|
|
Blockly.navigation.ACTION_TOOLBOX,
|
|
Blockly.navigation.ACTION_EXIT
|
|
];
|
|
createKeyMappingList(actions);
|
|
|
|
/**
|
|
* Shows the next node in the tree traversal every second.
|
|
* @package
|
|
*/
|
|
function demo() {
|
|
var doNext = function() {
|
|
var node = Blockly.getMainWorkspace().getCursor().next();
|
|
if (node) {
|
|
timeout = setTimeout(doNext, 1000);
|
|
}
|
|
}
|
|
doNext();
|
|
}
|
|
|
|
/**
|
|
* Stop the running demo.
|
|
* @package
|
|
*/
|
|
function stopDemo() {
|
|
clearTimeout(timeout);
|
|
document.getElementById('accessibilityModeCheck').disabled = false;
|
|
};
|
|
|
|
/**
|
|
* Sets up accessibility mode and change the cursor to basic cursor so that
|
|
* the demo can successfully run.
|
|
* @package
|
|
*/
|
|
function preOrderDemo() {
|
|
changeCursor('basic');
|
|
document.getElementById('accessibilityModeCheck').disabled = true;
|
|
setTimeout(demo, 1000);
|
|
}
|
|
|
|
/**
|
|
* Turn on/off accessibility mode depending on the state.
|
|
* @param {boolean} state True to turn on accessibility mode, false otherwise.
|
|
* @package
|
|
*/
|
|
function toggleAccessibilityMode(state) {
|
|
if (state) {
|
|
Blockly.navigation.enableKeyboardAccessibility();
|
|
} else {
|
|
Blockly.navigation.disableKeyboardAccessibility();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change the type of the cursor and set to the location of the old cursor.
|
|
* Changing the cursor changes how a user navigates the blocks on the workspace.
|
|
* @param {string} cursorType The type of the cursor.
|
|
* @package
|
|
*/
|
|
function changeCursor(cursorType) {
|
|
Blockly.navigation.enableKeyboardAccessibility();
|
|
document.getElementById('accessibilityModeCheck').checked = true;
|
|
document.getElementById('cursorChanger').value = cursorType;
|
|
var oldCurNode = Blockly.getMainWorkspace().getCursor().getCurNode();
|
|
|
|
if (cursorType === "basic") {
|
|
Blockly.getMainWorkspace().setCursor(new Blockly.BasicCursor());
|
|
} else {
|
|
Blockly.getMainWorkspace().setCursor(new Blockly.Cursor());
|
|
}
|
|
if (oldCurNode) {
|
|
Blockly.getMainWorkspace().getCursor().setCurNode(oldCurNode);
|
|
}
|
|
document.activeElement.blur();
|
|
}
|
|
|
|
// Start key mapping demo functions
|
|
|
|
/**
|
|
* Save the current key map in session storage.
|
|
* @package
|
|
*/
|
|
function saveKeyMap() {
|
|
var currentMap = Blockly.user.keyMap.getKeyMap();
|
|
if (sessionStorage) {
|
|
sessionStorage.setItem('keyMap', JSON.stringify(currentMap));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the key map to the map from session storage.
|
|
* @package
|
|
*/
|
|
function restoreKeyMap() {
|
|
var defaultMap = Blockly.user.keyMap.map_;
|
|
var stringifiedMap = sessionStorage.getItem('keyMap');
|
|
var restoredMap = {};
|
|
if (sessionStorage && stringifiedMap) {
|
|
var keyMap = JSON.parse(stringifiedMap);
|
|
var keys = Object.keys(keyMap);
|
|
for (var i = 0, key; key = keys[i]; i++) {
|
|
restoredMap[key] = Object.assign(new Blockly.Action, keyMap[key]);
|
|
}
|
|
Blockly.user.keyMap.setKeyMap(restoredMap);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given the three dropdowns create the serialized key that will be stored
|
|
* in the key map.
|
|
* @param {Array.<Element>} selectDivs The three dropdown divs that display
|
|
* the key combination.
|
|
* @package
|
|
*/
|
|
function serializeKey(selectDivs) {
|
|
var modifiers = Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys);
|
|
var newModifiers = [];
|
|
var newKeyCode = '';
|
|
var keyValue = selectDivs[2].value;
|
|
|
|
// Get the new modifiers from the first two dropdowns.
|
|
for (var i = 0; i < 2; i++) {
|
|
var selectDiv = selectDivs[i];
|
|
var key = selectDiv.value;
|
|
if (key !== 'None') {
|
|
newModifiers.push(key);
|
|
}
|
|
}
|
|
// Get the key code from the last dropdown.
|
|
if (keyValue !== 'None') {
|
|
if (keyValue === 'Escape') {
|
|
newKeyCode = Blockly.utils.KeyCodes.ESC;
|
|
} else if (keyValue === 'Enter') {
|
|
newKeyCode = Blockly.utils.KeyCodes.ENTER;
|
|
} else {
|
|
newKeyCode = keyValue.toUpperCase().charCodeAt(0);
|
|
}
|
|
}
|
|
return Blockly.user.keyMap.createSerializedKey(newKeyCode, newModifiers);
|
|
}
|
|
|
|
/**
|
|
* Set all dropdowns for that action to none.
|
|
* We clear dropdowns when a user chooses the same key combination for a
|
|
* second action.
|
|
* @param {Blockly.Action} action The action that we want to clear the
|
|
* dropdowns for.
|
|
* @package
|
|
*/
|
|
function clearDropdown(action) {
|
|
var actionDiv = document.querySelectorAll('[data-actionname='+ action.name +']')[0];
|
|
var selectDivs = actionDiv.getElementsByTagName('select');
|
|
for (var i = 0, selectDiv; selectDiv = selectDivs[i]; i++) {
|
|
selectDiv.value = 'None';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given the three dropdowns create a human readable string so the screen reader
|
|
* can read it out.
|
|
* @param {Array.<Element>} selectDivs The three dropdown divs that display
|
|
* the key combination.
|
|
* @package
|
|
*/
|
|
function getReadableKey(selectDivs) {
|
|
var readableKey = '';
|
|
|
|
for (var i = 0, selectDiv; selectDiv = selectDivs[i]; i++) {
|
|
if (selectDiv.value !== 'None') {
|
|
readableKey += selectDiv.value + ' ';
|
|
}
|
|
}
|
|
return readableKey;
|
|
}
|
|
|
|
/**
|
|
* Update the key in the key map when the user selects a new value in one of the
|
|
* dropdowns.
|
|
* @param {Event} e The event dispatched from changing a dropdown.
|
|
* @package
|
|
*/
|
|
function updateKey(e) {
|
|
var keyboardAnnouncerText = '';
|
|
var actionDiv = e.srcElement.parentElement;
|
|
var action = actionDiv.action;
|
|
var selectDivs = actionDiv.getElementsByTagName('select');
|
|
var key = serializeKey(selectDivs);
|
|
var oldAction = Blockly.user.keyMap.getActionByKeyCode(key);
|
|
|
|
if (oldAction) {
|
|
keyboardAnnouncerText += oldAction.name + ' action key was overwritten. \n';
|
|
clearDropdown(oldAction);
|
|
}
|
|
keyboardAnnouncerText += action.name + ' key was set to ' + getReadableKey(selectDivs);
|
|
document.getElementById('keyboard_announce').innerText = keyboardAnnouncerText;
|
|
Blockly.user.keyMap.setActionForKey(key, action);
|
|
saveKeyMap();
|
|
document.activeElement.blur();
|
|
}
|
|
|
|
/**
|
|
* Set the key to be the correct value from the key map.
|
|
* @param {string} actionKey The serialized key for a given action.
|
|
* @param {Element} keyDropdown The dropdown that displays the primary key.
|
|
* @package
|
|
*/
|
|
function setKeyDropdown(actionKey, keyDropdown) {
|
|
// Strip off any modifier to just get the key code.
|
|
var keyCode = actionKey.match(/\d+/)[0];
|
|
var keyValue = String.fromCharCode(keyCode);
|
|
if (parseInt(keyCode) === Blockly.utils.KeyCodes.ESC) {
|
|
keyValue = 'Escape';
|
|
} else if (parseInt(keyCode) === Blockly.utils.KeyCodes.ENTER) {
|
|
keyValue = 'Enter';
|
|
}
|
|
keyDropdown.value = keyValue;
|
|
}
|
|
|
|
/**
|
|
* Set the modifiers to be the correct value from the key map.
|
|
* @param {string} actionKey The key code holding the modifiers and key.
|
|
* @param {Array.<Element>} modifierDropdowns A list of dropdowns for
|
|
* the modifier values.
|
|
* @package
|
|
*/
|
|
function setModifiers(actionKey, modifierDropdowns) {
|
|
var modifiers = Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys);
|
|
for (var i = 0; i < 2; i++) {
|
|
var modifierDropdown = modifierDropdowns[i];
|
|
for (var j = 0, modifier; modifier = modifiers[j]; j++) {
|
|
if (actionKey.indexOf(modifier) > -1) {
|
|
modifierDropdown.value = modifier;
|
|
actionKey = actionKey.replace(modifier, '');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the dropdowns to display the correct combination of modifiers and
|
|
* keys for the action key.
|
|
* @param {Blockly.Action} action The Blockly action.
|
|
* @param {Element} actionDiv The div holding the dropdowns and label for the
|
|
* given action.
|
|
* @param {string} actionKey The key corresponding to the given action.
|
|
* @package
|
|
*/
|
|
function setDropdowns(action, actionDiv, actionKey) {
|
|
var selectDivs = actionDiv.getElementsByTagName('select');
|
|
if (actionKey) {
|
|
setModifiers(actionKey, selectDivs);
|
|
setKeyDropdown(actionKey, selectDivs[selectDivs.length - 1]);
|
|
} else {
|
|
clearDropdown(action);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a dropdown with the given list of possible keys.
|
|
* @param {Blockly.Action} action The Blockly action.
|
|
* @param {Element} actionDiv The div holding the dropdowns and labels for
|
|
* a given action.
|
|
* @param {Array.<string>} keys The list of keys to add to the dropdown.
|
|
* @package
|
|
*/
|
|
function createDropdown(action, actionDiv, keys) {
|
|
var select = document.createElement('select');
|
|
select.addEventListener('change', updateKey);
|
|
select.setAttribute('aria-labelledby', action.name + '_label');
|
|
for (var i = 0, key; key = keys[i]; i++) {
|
|
select.options.add(new Option(key, key));
|
|
}
|
|
actionDiv.appendChild(select);
|
|
}
|
|
|
|
/**
|
|
* Create two dropdowns that display possible modifiers and a single dropdown
|
|
* displaying a list of keys.
|
|
* @param {Blockly.Action} action The Blockly action.
|
|
* @param {string} actionKey The key corresponding to the given action.
|
|
* @param {Element} actionDiv The div holding the dropdowns and label for the
|
|
* given action.
|
|
* @package
|
|
*/
|
|
function createDropdowns(action, actionKey, actionDiv) {
|
|
var modifiers = ['None'].concat(Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys));
|
|
var keys = ['None', 'Enter', 'Escape'].concat("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".split(''));
|
|
createDropdown(action, actionDiv, modifiers);
|
|
createDropdown(action, actionDiv, modifiers);
|
|
createDropdown(action, actionDiv, keys);
|
|
setDropdowns(action, actionDiv, actionKey);
|
|
}
|
|
|
|
/**
|
|
* For each action create a row of 3 dropdowns and an action label. Update
|
|
* the dropdowns to reflect the value in the key map.
|
|
* @param {Array.<Blockly.Action>} actions List of blockly actions.
|
|
* @package
|
|
*/
|
|
function createKeyMappingList(actions) {
|
|
// Update the key map to reflect the key map saved in session storage.
|
|
restoreKeyMap();
|
|
var keyMapDiv = document.getElementById('keyboard_mappings');
|
|
for (var i = 0, action; action = actions[i]; i++) {
|
|
var actionDiv = document.createElement('div');
|
|
actionDiv.setAttribute('data-actionname', action.name);
|
|
actionDiv.action = action;
|
|
|
|
var labelDiv = document.createElement('label');
|
|
labelDiv.innerText = action.name;
|
|
labelDiv.setAttribute('id', action.name + '_label');
|
|
|
|
actionDiv.appendChild(labelDiv);
|
|
keyMapDiv.appendChild(actionDiv);
|
|
|
|
var actionKey = Blockly.user.keyMap.getKeyByAction(action);
|
|
createDropdowns(action, actionKey, actionDiv);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hide/show the key map panel.
|
|
* @param {boolean} state The state of the checkbox. True if checked, false
|
|
* otherwise.
|
|
* @package
|
|
*/
|
|
function toggleDisplayKeyMappings(state) {
|
|
if (state) {
|
|
document.getElementById('keyboard_nav').style.display = 'block';
|
|
} else {
|
|
document.getElementById('keyboard_nav').style.display = 'none';
|
|
}
|
|
}
|
|
// End key mapping demo functions
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|