- Added keyboard navigation with arrows/pageUp/pageDown/enter for tag selection ( Issue #3001 )

- The color when hovering over a modal element is now grey, to differentiate from blue when choosing tags using keyboard
pull/3012/head
Émilio Gonzalez 2018-03-08 11:32:31 -05:00
parent 6e1528db1a
commit 3510f7b745
4 changed files with 190 additions and 6 deletions

View File

@ -18,7 +18,7 @@
<div class="popover_choice_main" id ="popover_choice_main">
<table style="width:100%;">
<?php foreach ($options as $k => &$option): ?>
<tr style="border-top:1px solid black;" class="templateChoiceButton" id="field_<?php echo h($k); ?>">
<tr style="border-top:1px solid black;" class="templateChoiceButton shown" id="field_<?php echo h($k); ?>">
<?php if (isset($attributeTag)): ?>
<td style="padding-left:10px;padding-right:10px; text-align:center;width:100%;" onClick="quickSubmitAttributeTagForm('<?php echo h($object_id);?>', '<?php echo h($k); ?>');" title="<?php echo h($expanded[$k]);?>" role="button" tabindex="0" aria-label="Attach tag <?php echo h($option); ?>"><?php echo h($option); ?></td>
<?php else: ?>
@ -48,9 +48,13 @@
var filterString = $("#filterField").val().toLowerCase();
$.each(tags, function(index, value) {
if (value.toLowerCase().indexOf(filterString) == -1) {
$('#field_' + index).hide();
let element = $('#field_' + index);
element.hide();
element.removeClass('shown');
} else {
$('#field_' + index).show();
let element = $('#field_' + index);
element.show();
element.addClass('shown');
}
});
}
@ -60,3 +64,4 @@
resizePopoverBody();
});
</script>
<?php echo $this->Html->script('tag-selection-keyboard-navigation.js'); ?>

View File

@ -1430,6 +1430,11 @@ a.proposal_link_red:hover {
}
.templateChoiceButton:hover {
background-color:#bbb !important;
color:white;
}
.selected-tag {
background-color:#0088cc !important;
color:white;
}

View File

@ -1,11 +1,19 @@
/**
* This classes deals with the front-end keyboard shortcuts and is included in every page.
* This object deals with the front-end keyboard shortcuts and is included in every page.
*/
let keyboardShortcutsManager = {
NAVIGATION_KEYS: ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Enter"],
EVENTS: {
"ArrowUp": "upArrowPressed",
"ArrowDown": "downArrowPressed",
"Enter": "enterPressed",
"PageUp": "pageUpPressed",
"PageDown": "pageDownPressed"
},
ESCAPED_TAG_NAMES: ["INPUT", "TEXTAREA", "SELECT"],
shortcutKeys: new Map(),
shortcutListToggled: false,
escapedTagNames: ["INPUT", "TEXTAREA", "SELECT"],
/**
* Fetches the keyboard shortcut config files and populates this.shortcutJSON.
@ -59,14 +67,18 @@ let keyboardShortcutsManager = {
/**
* Sets the event to listen to and the routine to call on keypress.
* If it's a shortcut key, execute its code. If its a navigation
* key, trigger an event depending on the key.
*/
setKeyboardListener() {
window.onkeyup = (keyboardEvent) => {
if(this.shortcutKeys.has(keyboardEvent.key)) {
let activeElement = document.activeElement.tagName;
if( !this.escapedTagNames.includes(activeElement)) {
if( !this.ESCAPED_TAG_NAMES.includes(activeElement)) {
eval(this.shortcutKeys.get(keyboardEvent.key).action);
}
} else if(this.NAVIGATION_KEYS.includes(keyboardEvent.key)) {
window.dispatchEvent(new CustomEvent(this.EVENTS[keyboardEvent.key], {detail: keyboardEvent}));
}
}
},

View File

@ -0,0 +1,162 @@
/**
* This object deals with keyboard navigation when choosing a tag using the "Select Tag" modal.
* It relies on events dispatched by "keyboard-shortcuts.js".
*/
let keyboardTagSelection = {
TAG_CLASS: "#popover_choice_main .templateChoiceButton.shown",
DISPLAYED_TAGS_CLASS: "shown",
KEYS: ["ArrowUp", "ArrowDown", "Enter"],
SELECTED_TAG_CLASS: "selected-tag",
PAGE_OFFSET: 5,
selectedElement: null,
selectedElementIndex: -1,
elements: $(this.TAG_CLASS),
oldElementLength: 0,
/**
* Sets the event to listen to and the routine to call on keypress, if not already done.
*/
init() {
if(!window.tagSelectionPopupLoaded) {
window.tagSelectionPopupLoaded = true;
window.addEventListener('upArrowPressed', () => this.onUpArrowPress());
window.addEventListener('downArrowPressed', () => this.onDownArrowPress());
window.addEventListener('pageUpPressed', () => this.onPageUpPress());
window.addEventListener('pageDownPressed', () => this.onPageDownPress());
window.addEventListener('enterPressed', () => this.onEnterPress());
}
},
/**
* Selects the previous tag in the list. If reached the beginning of the list,
* select the last element.
*/
onUpArrowPress() {
this.oldElementLength = this.elements.length;
this.elements = $(this.TAG_CLASS);
if(this.oldElementLength != this.elements.length) {
if(!this.selectedElement || !this.selectedElement.hasClass(this.DISPLAYED_TAGS_CLASS)) {
if(this.elements.length > 0) {
this.selectPreviousTag();
}
} else {
this.selectLastTag();
}
} else {
this.selectPreviousTag();
}
},
/**
* Selects the next tag in the list. If reached the end of the list,
* select the first element.
*/
onDownArrowPress() {
this.oldElementLength = this.elements.length;
this.elements = $(this.TAG_CLASS);
if(this.oldElementLength != this.elements.length) {
if(!this.selectedElement || !this.selectedElement.hasClass(this.DISPLAYED_TAGS_CLASS)) {
if(this.elements.length > 0) {
this.selectNextTag();
}
} else {
this.selectFirstTag();
}
} else {
this.selectNextTag();
}
},
/**
* Selects a tag at several previous position (say, from index 15 to index 9).
* Circular, so if we reach beginning of the list, goes at the end.
*/
onPageUpPress() {
this.selectedElementIndex -= this.PAGE_OFFSET;
this.onUpArrowPress();
},
/**
* Selects a tag at several next position (say, from index 9 to index 15).
* Circular, so if we reach beginning of the list, goes at the beginning.
*/
onPageDownPress() {
this.selectedElementIndex += this.PAGE_OFFSET;
this.onDownArrowPress();
},
/**
* Clicks on the selected element's child if the selected element exists.
*/
onEnterPress() {
if(this.selectedElement && document.contains(this.selectedElement[0])) {
this.selectedElement.children().click();
}
},
/**
* Removes the "selected" class from the old selected element (if it exists)
* and sets the "selected" class on newSelectedElement. Also deal with scrolling
* of the tag modal.
* @param {Element} index The index of the selected tag in this.elements.
*/
updateSelectedElement(index) {
if(this.selectedElement) {
this.selectedElement.removeClass(this.SELECTED_TAG_CLASS);
}
this.selectedElement = $(this.elements[index]);
this.selectedElementIndex = index;
this.selectedElement.addClass(this.SELECTED_TAG_CLASS);
let container = $('#popover_choice_main');
container.animate({
scrollTop: this.selectedElement.offset().top - $('#popover_choice_main').offset().top
}, 100);
},
/**
* If there is at least a tag displayed, select the first of them.
*/
selectFirstTag() {
if(this.elements.length > 0) {
this.updateSelectedElement(0);
}
},
/**
* If there is at least a tag displayed, select the last of them.
*/
selectLastTag() {
if(this.elements.length > 0) {
this.updateSelectedElement(this.elements.length - 1);
}
},
/**
* Selects the previous tag, or the last tag if we're at the top of the list.
*/
selectPreviousTag() {
let newIndex;
if(this.selectedElementIndex <= 0) {
newIndex = this.elements.length - 1;
} else {
newIndex = this.selectedElementIndex - 1;
}
this.updateSelectedElement(newIndex);
},
/**
* Selects the next tag, or the first tag if we're at the bottom of the list.
*/
selectNextTag() {
let newIndex;
if(this.selectedElementIndex >= this.elements.length - 1) {
newIndex = 0;
} else {
newIndex = this.selectedElementIndex + 1;
}
this.updateSelectedElement(newIndex);
}
}
// Inits the keyboard tag selection's main routine.
$(document).ready(() => keyboardTagSelection.init());