From 9f8c2ed4581dfe9026d2ed415265300bd5da14b4 Mon Sep 17 00:00:00 2001 From: Franz Liedke Date: Sun, 15 May 2016 22:34:16 +0900 Subject: [PATCH] First shot at extracting keyboard navigation code to separate util class Refs #264. --- js/forum/dist/app.js | 177 ++++++++++++++++++---- js/forum/src/components/Search.js | 51 +++---- js/forum/src/utils/KeyboardNavigatable.js | 142 +++++++++++++++++ 3 files changed, 311 insertions(+), 59 deletions(-) create mode 100644 js/forum/src/utils/KeyboardNavigatable.js diff --git a/js/forum/dist/app.js b/js/forum/dist/app.js index 5ab57273a..756cea76a 100644 --- a/js/forum/dist/app.js +++ b/js/forum/dist/app.js @@ -26137,8 +26137,8 @@ System.register('flarum/components/RequestErrorModal', ['flarum/components/Modal });; 'use strict'; -System.register('flarum/components/Search', ['flarum/Component', 'flarum/components/LoadingIndicator', 'flarum/utils/ItemList', 'flarum/utils/classList', 'flarum/utils/extractText', 'flarum/helpers/icon', 'flarum/components/DiscussionsSearchSource', 'flarum/components/UsersSearchSource'], function (_export, _context) { - var Component, LoadingIndicator, ItemList, classList, extractText, icon, DiscussionsSearchSource, UsersSearchSource, Search; +System.register('flarum/components/Search', ['flarum/Component', 'flarum/components/LoadingIndicator', 'flarum/utils/ItemList', 'flarum/utils/classList', 'flarum/utils/extractText', 'flarum/utils/KeyboardNavigatable', 'flarum/helpers/icon', 'flarum/components/DiscussionsSearchSource', 'flarum/components/UsersSearchSource'], function (_export, _context) { + var Component, LoadingIndicator, ItemList, classList, extractText, KeyboardNavigatable, icon, DiscussionsSearchSource, UsersSearchSource, Search; return { setters: [function (_flarumComponent) { Component = _flarumComponent.default; @@ -26150,6 +26150,8 @@ System.register('flarum/components/Search', ['flarum/Component', 'flarum/compone classList = _flarumUtilsClassList.default; }, function (_flarumUtilsExtractText) { extractText = _flarumUtilsExtractText.default; + }, function (_flarumUtilsKeyboardNavigatable) { + KeyboardNavigatable = _flarumUtilsKeyboardNavigatable.default; }, function (_flarumHelpersIcon) { icon = _flarumHelpersIcon.default; }, function (_flarumComponentsDiscussionsSearchSource) { @@ -26286,38 +26288,17 @@ System.register('flarum/components/Search', ['flarum/Component', 'flarum/compone search.setIndex(search.selectableItems().index(this)); }); - // Handle navigation key events on the search input. - this.$('input').on('keydown', function (e) { - switch (e.which) { - case 40:case 38: - // Down/Up - _this3.setIndex(_this3.getCurrentNumericIndex() + (e.which === 40 ? 1 : -1), true); - e.preventDefault(); - break; + var $input = this.$('input'); - case 13: - // Return - if (_this3.value()) { - m.route(_this3.getItem(_this3.index).find('a').attr('href')); - } else { - _this3.clear(); - } - _this3.$('input').blur(); - break; + this.navigator = new KeyboardNavigatable(); + this.navigator.onUp(function () { + return _this3.setIndex(_this3.getCurrentNumericIndex() - 1, true); + }).onDown(function () { + return _this3.setIndex(_this3.getCurrentNumericIndex() + 1, true); + }).onSelect(this.selectResult.bind(this)).onCancel(this.clear.bind(this)).bindTo($input); - case 27: - // Escape - _this3.clear(); - break; - - default: - // no default - } - }) - - // Handle input key events on the search input, triggering results to - // load. - .on('input focus', function () { + // Handle input key events on the search input, triggering results to load. + $input.on('input focus', function () { var query = this.value.toLowerCase(); if (!query) return; @@ -26353,6 +26334,17 @@ System.register('flarum/components/Search', ['flarum/Component', 'flarum/compone value: function getCurrentSearch() { return app.current && typeof app.current.searching === 'function' && app.current.searching(); } + }, { + key: 'selectResult', + value: function selectResult() { + if (this.value()) { + m.route(this.getItem(this.index).find('a').attr('href')); + } else { + this.clear(); + } + + this.$('input').blur(); + } }, { key: 'clear', value: function clear() { @@ -31632,4 +31624,125 @@ System.register('flarum/utils/UserControls', ['flarum/components/Button', 'flaru }); } }; +});; +'use strict'; + +System.register('flarum/utils/KeyboardNavigatable', [], function (_export, _context) { + var KeyboardNavigatable; + return { + setters: [], + execute: function () { + KeyboardNavigatable = function () { + function KeyboardNavigatable() { + babelHelpers.classCallCheck(this, KeyboardNavigatable); + + var defaultCallback = function defaultCallback() {/* noop */}; + + // Set all callbacks to a noop function so that not all of them have to be set. + this.upCallback = defaultCallback; + this.downCallback = defaultCallback; + this.selectCallback = defaultCallback; + this.cancelCallback = defaultCallback; + + // By default, always handle keyboard navigation. + this.whenCallback = function () { + return true; + }; + } + + /** + * Provide a callback to be executed when navigating upwards. + * + * This will be triggered by the Up key. + * + * @public + * @param {Function} callback + * @return {KeyboardNavigatable} + */ + + + babelHelpers.createClass(KeyboardNavigatable, [{ + key: 'onUp', + value: function onUp(callback) { + this.upCallback = callback; + + return this; + } + }, { + key: 'onDown', + value: function onDown(callback) { + this.downCallback = callback; + + return this; + } + }, { + key: 'onSelect', + value: function onSelect(callback) { + this.selectCallback = callback; + + return this; + } + }, { + key: 'onCancel', + value: function onCancel(callback) { + this.cancelCallback = callback; + + return this; + } + }, { + key: 'when', + value: function when(callback) { + this.whenCallback = callback; + + return this; + } + }, { + key: 'bindTo', + value: function bindTo($element) { + // Handle navigation key events on the navigatable element. + $element.on('keydown', this.navigate.bind(this)); + } + }, { + key: 'navigate', + value: function navigate(event) { + // This callback determines whether keyboard should be handled or ignored. + if (!this.whenCallback()) return; + + switch (event.which) { + case 9:case 13: + // Tab / Return + this.selectCallback(); + event.preventDefault(); + break; + + case 27: + // Escape + this.cancelCallback(); + event.stopPropagation(); + event.preventDefault(); + break; + + case 38: + // Up + this.upCallback(); + event.preventDefault(); + break; + + case 40: + // Down + this.downCallback(); + event.preventDefault(); + break; + + default: + // no default + } + } + }]); + return KeyboardNavigatable; + }(); + + _export('default', KeyboardNavigatable); + } + }; }); \ No newline at end of file diff --git a/js/forum/src/components/Search.js b/js/forum/src/components/Search.js index 276044a73..c4bfddb21 100644 --- a/js/forum/src/components/Search.js +++ b/js/forum/src/components/Search.js @@ -3,6 +3,7 @@ import LoadingIndicator from 'flarum/components/LoadingIndicator'; import ItemList from 'flarum/utils/ItemList'; import classList from 'flarum/utils/classList'; import extractText from 'flarum/utils/extractText'; +import KeyboardNavigatable from 'flarum/utils/KeyboardNavigatable'; import icon from 'flarum/helpers/icon'; import DiscussionsSearchSource from 'flarum/components/DiscussionsSearchSource'; import UsersSearchSource from 'flarum/components/UsersSearchSource'; @@ -121,35 +122,18 @@ export default class Search extends Component { ); }); - // Handle navigation key events on the search input. - this.$('input') - .on('keydown', e => { - switch (e.which) { - case 40: case 38: // Down/Up - this.setIndex(this.getCurrentNumericIndex() + (e.which === 40 ? 1 : -1), true); - e.preventDefault(); - break; + const $input = this.$('input'); - case 13: // Return - if (this.value()) { - m.route(this.getItem(this.index).find('a').attr('href')); - } else { - this.clear(); - } - this.$('input').blur(); - break; + this.navigator = new KeyboardNavigatable(); + this.navigator + .onUp(() => this.setIndex(this.getCurrentNumericIndex() - 1, true)) + .onDown(() => this.setIndex(this.getCurrentNumericIndex() + 1, true)) + .onSelect(this.selectResult.bind(this)) + .onCancel(this.clear.bind(this)) + .bindTo($input); - case 27: // Escape - this.clear(); - break; - - default: - // no default - } - }) - - // Handle input key events on the search input, triggering results to - // load. + // Handle input key events on the search input, triggering results to load. + $input .on('input focus', function() { const query = this.value.toLowerCase(); @@ -191,6 +175,19 @@ export default class Search extends Component { return app.current && typeof app.current.searching === 'function' && app.current.searching(); } + /** + * Navigate to the currently selected search result and close the list. + */ + selectResult() { + if (this.value()) { + m.route(this.getItem(this.index).find('a').attr('href')); + } else { + this.clear(); + } + + this.$('input').blur(); + } + /** * Clear the search input and the current controller's active search. */ diff --git a/js/forum/src/utils/KeyboardNavigatable.js b/js/forum/src/utils/KeyboardNavigatable.js new file mode 100644 index 000000000..cdf2f3101 --- /dev/null +++ b/js/forum/src/utils/KeyboardNavigatable.js @@ -0,0 +1,142 @@ +/** + * The `KeyboardNavigatable` class manages lists that can be navigated with the + * keyboard, calling callbacks for each actions. + * + * This helper encapsulates the key binding logic, providing a simple fluent + * API for use. + */ +export default class KeyboardNavigatable { + constructor() { + const defaultCallback = () => { /* noop */ }; + + // Set all callbacks to a noop function so that not all of them have to be set. + this.upCallback = defaultCallback; + this.downCallback = defaultCallback; + this.selectCallback = defaultCallback; + this.cancelCallback = defaultCallback; + + // By default, always handle keyboard navigation. + this.whenCallback = () => true; + } + + /** + * Provide a callback to be executed when navigating upwards. + * + * This will be triggered by the Up key. + * + * @public + * @param {Function} callback + * @return {KeyboardNavigatable} + */ + onUp(callback) { + this.upCallback = callback; + + return this; + } + + /** + * Provide a callback to be executed when navigating downwards. + * + * This will be triggered by the Down key. + * + * @public + * @param {Function} callback + * @return {KeyboardNavigatable} + */ + onDown(callback) { + this.downCallback = callback; + + return this; + } + + /** + * Provide a callback to be executed when the current item is selected.. + * + * This will be triggered by the Return and Tab keys.. + * + * @public + * @param {Function} callback + * @return {KeyboardNavigatable} + */ + onSelect(callback) { + this.selectCallback = callback; + + return this; + } + + /** + * Provide a callback to be executed when the navigation is canceled. + * + * This will be triggered by the Escape key. + * + * @public + * @param {Function} callback + * @return {KeyboardNavigatable} + */ + onCancel(callback) { + this.cancelCallback = callback; + + return this; + } + + /** + * Provide a callback that determines whether keyboard input should be handled. + * + * @public + * @param {Function} callback + * @return {KeyboardNavigatable} + */ + when(callback) { + this.whenCallback = callback; + + return this; + } + + /** + * Set up the navigation key bindings on the given jQuery element. + * + * @public + * @param {jQuery} $element + */ + bindTo($element) { + // Handle navigation key events on the navigatable element. + $element.on('keydown', this.navigate.bind(this)); + } + + /** + * Interpret the given keyboard event as navigation commands. + * + * @public + * @param {KeyboardEvent} event + */ + navigate(event) { + // This callback determines whether keyboard should be handled or ignored. + if (!this.whenCallback()) return; + + switch (event.which) { + case 9: case 13: // Tab / Return + this.selectCallback(); + event.preventDefault(); + break; + + case 27: // Escape + this.cancelCallback(); + event.stopPropagation(); + event.preventDefault(); + break; + + case 38: // Up + this.upCallback(); + event.preventDefault(); + break; + + case 40: // Down + this.downCallback(); + event.preventDefault(); + break; + + default: + // no default + } + } +}