diff --git a/administrator/language/en-GB/plg_system_shortcut.ini b/administrator/language/en-GB/plg_system_shortcut.ini new file mode 100644 index 00000000000..8a60db197bf --- /dev/null +++ b/administrator/language/en-GB/plg_system_shortcut.ini @@ -0,0 +1,13 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_SHORTCUT="System - Keyboard Shortcut" +PLG_SYSTEM_SHORTCUT_ACTIONS_FORM_LABEL="Form View Actions" +PLG_SYSTEM_SHORTCUT_ACTIONS_GENERAL_LABEL="General Actions" +PLG_SYSTEM_SHORTCUT_ACTIONS_LIST_LABEL="List View Actions" +PLG_SYSTEM_SHORTCUT_CORE_SHORTCUTS_TAB="Core Shortcuts" +PLG_SYSTEM_SHORTCUT_OVERVIEW_HINT="Press J + X for our keyboard shortcut overview" +PLG_SYSTEM_SHORTCUT_OVERVIEW_TITLE="Available Joomla Shortcuts" +PLG_SYSTEM_SHORTCUT_XML_DESCRIPTION="

Enables keyboard shortcuts on the administrator site, which can be provided by other plugins and includes directly the following list of shortcuts:

" diff --git a/administrator/language/en-GB/plg_system_shortcut.sys.ini b/administrator/language/en-GB/plg_system_shortcut.sys.ini new file mode 100644 index 00000000000..ec6e761eb55 --- /dev/null +++ b/administrator/language/en-GB/plg_system_shortcut.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; (C) 2022 Open Source Matters, Inc. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_SHORTCUT="System - Keyboard Shortcut" +PLG_SYSTEM_SHORTCUT_XML_DESCRIPTION="

Enables keyboard shortcuts on the administrator site, which can be provided by other plugins and includes directly the following list of shortcuts:

" diff --git a/build/build-modules-js/settings.json b/build/build-modules-js/settings.json index c43eb8ca94e..8e9ea310db9 100644 --- a/build/build-modules-js/settings.json +++ b/build/build-modules-js/settings.json @@ -386,6 +386,24 @@ "dependencies": [], "licenseFilename": "LICENSE.txt" }, + "hotkeys-js": { + "name": "hotkeys.js", + "licenseFilename": "LICENSE", + "js" : { + "dist/hotkeys.js": "js/hotkeys.js", + "dist/hotkeys.min.js": "js/hotkeys.min.js" + }, + "provideAssets": [ + { + "name": "hotkeys.js", + "type": "script", + "uri": "hotkeys.min.js", + "attributes": { + "defer": true + } + } + ] + }, "jquery": { "name": "jquery", "js": { diff --git a/build/media_source/plg_system_shortcut/js/shortcut.es6.js b/build/media_source/plg_system_shortcut/js/shortcut.es6.js new file mode 100644 index 00000000000..8ad4235fb66 --- /dev/null +++ b/build/media_source/plg_system_shortcut/js/shortcut.es6.js @@ -0,0 +1,133 @@ +((document, Joomla) => { + 'use strict'; + + if (!Joomla) { + throw new Error('Joomla API is not properly initialised'); + } + + /* global hotkeys */ + Joomla.addShortcut = (hotkey, callback) => { + hotkeys(hotkey, (event) => { + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + + callback.call(); + }); + }; + + Joomla.addClickShortcut = (hotkey, selector) => { + Joomla.addShortcut(hotkey, () => { + const element = document.querySelector(selector); + if (element) { + element.click(); + } + }); + }; + + Joomla.addFocusShortcut = (hotkey, selector) => { + Joomla.addShortcut(hotkey, () => { + const element = document.querySelector(selector); + if (element) { + element.focus(); + } + }); + }; + + const setShortcutFilter = () => { + hotkeys.filter = (event) => { + const target = event.target || event.srcElement; + const { tagName } = target; + + // Checkboxes should not block a shortcut event + if (target.type === 'checkbox') { + return true; + } + // Default hotkeys filter behavior + return !(target.isContentEditable || tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA'); + }; + }; + + const addOverviewHint = () => { + const iconElement = document.createElement('span'); + iconElement.className = 'icon-keyboard fa-keyboard me-2'; + iconElement.setAttribute('aria-hidden', true); + const textElement = document.createElement('span'); + textElement.innerText = Joomla.Text._('PLG_SYSTEM_SHORTCUT_OVERVIEW_HINT'); + const hintElement = document.createElement('p'); + hintElement.appendChild(iconElement); + hintElement.appendChild(textElement); + const containerElement = document.createElement('section'); + containerElement.className = 'content pt-4'; + containerElement.appendChild(hintElement); + document.querySelector('.container-main').appendChild(containerElement); + }; + + const initOverviewModal = (options) => { + const dlItems = new Map(); + Object.values(options).forEach((value) => { + let titles = []; + if (dlItems.has(value.shortcut)) { + titles = dlItems.get(value.shortcut); + titles.push(value.title); + } else { + titles = [value.title]; + } + dlItems.set(value.shortcut, titles); + }); + + let dl = '
'; + dlItems.forEach((titles, shortcut) => { + titles.forEach((title) => { + dl += `
${title}
`; + }); + dl += `
${shortcut}
`; + }); + dl += '
'; + + const modal = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modal); + + const bootstrapModal = new bootstrap.Modal(document.getElementById('shortcutOverviewModal'), { + keyboard: true, + backdrop: true, + }); + hotkeys('J + X', () => bootstrapModal.show()); + }; + + document.addEventListener('DOMContentLoaded', () => { + const options = Joomla.getOptions('plg_system_shortcut.shortcuts'); + Object.values(options).forEach((value) => { + if (value.selector.includes('input')) { + Joomla.addFocusShortcut(value.shortcut, value.selector); + } else { + Joomla.addClickShortcut(value.shortcut, value.selector); + } + }); + // Show hint and overview on logged in backend only (not login page) + if (document.querySelector('nav')) { + initOverviewModal(options); + addOverviewHint(); + } + setShortcutFilter(); + }); +})(document, Joomla); diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 454d2898761..756486889a4 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -342,6 +342,7 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, (0, 'plg_system_sef', 'plugin', 'sef', 'system', 0, 1, 1, 0, 1, '', '', '', 18, 0), (0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, 1, '', '', '', 19, 0), (0, 'plg_system_skipto', 'plugin', 'skipto', 'system', 0, 1, 1, 0, 1, '', '{}', '', 20, 0), +(0, 'plg_system_shortcut', 'plugin', 'shortcut', 'system', 0, 1, 1, 0, 0, '', '{}', '', 0, 0), (0, 'plg_system_stats', 'plugin', 'stats', 'system', 0, 1, 1, 0, 1, '', '', '', 21, 0), (0, 'plg_system_tasknotification', 'plugin', 'tasknotification', 'system', 0, 1, 1, 0, 1, '', '', '', 22, 0), (0, 'plg_system_updatenotification', 'plugin', 'updatenotification', 'system', 0, 1, 1, 0, 1, '', '', '', 23, 0), diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index 1078a8cf3aa..6a8360df2ae 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -348,6 +348,7 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", (0, 'plg_system_sef', 'plugin', 'sef', 'system', 0, 1, 1, 0, 1, '', '', '', 18, 0), (0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, 1, '', '', '', 19, 0), (0, 'plg_system_skipto', 'plugin', 'skipto', 'system', 0, 1, 1, 0, 1, '', '{}', '', 20, 0), +(0, 'plg_system_shortcut', 'plugin', 'shortcut', 'system', 0, 1, 1, 0, 0, '', '{}', '', 0, 0), (0, 'plg_system_stats', 'plugin', 'stats', 'system', 0, 1, 1, 0, 1, '', '', '', 21, 0), (0, 'plg_system_tasknotification', 'plugin', 'tasknotification', 'system', 0, 1, 1, 0, 1, '', '', '', 22, 0), (0, 'plg_system_updatenotification', 'plugin', 'updatenotification', 'system', 0, 1, 1, 0, 1, '', '', '', 23, 0), diff --git a/package-lock.json b/package-lock.json index 3c759daef23..64f791af2bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "diff": "^5.0.0", "dragula": "^3.7.3", "focus-visible": "^5.2.0", + "hotkeys-js": "^3.9.3", "joomla-ui-custom-elements": "^0.2.0", "jquery": "^3.6.0", "jquery-migrate": "^3.3.2", @@ -4540,6 +4541,11 @@ "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", "dev": true }, + "node_modules/hotkeys-js": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.9.3.tgz", + "integrity": "sha512-s+f0xyvDmf6+DyrFQ2SY+eA7lbvMbjqkqi0I0SpMgnN5tZx7DeH8nsWhkJR4KEq3pxDPHJppDUhdt1rZFW5LeQ==" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -11725,6 +11731,11 @@ "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", "dev": true }, + "hotkeys-js": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/hotkeys-js/-/hotkeys-js-3.9.3.tgz", + "integrity": "sha512-s+f0xyvDmf6+DyrFQ2SY+eA7lbvMbjqkqi0I0SpMgnN5tZx7DeH8nsWhkJR4KEq3pxDPHJppDUhdt1rZFW5LeQ==" + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", diff --git a/package.json b/package.json index 70026bd1074..3ce192ec7e6 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "diff": "^5.0.0", "dragula": "^3.7.3", "focus-visible": "^5.2.0", + "hotkeys-js": "^3.9.3", "joomla-ui-custom-elements": "^0.2.0", "jquery": "^3.6.0", "jquery-migrate": "^3.3.2", diff --git a/plugins/system/shortcut/shortcut.php b/plugins/system/shortcut/shortcut.php new file mode 100644 index 00000000000..55557695708 --- /dev/null +++ b/plugins/system/shortcut/shortcut.php @@ -0,0 +1,147 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Application\AdministratorApplication; +use Joomla\CMS\Event\GenericEvent; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\Event\Event; +use Joomla\Event\SubscriberInterface; + +/** + * Shortcut plugin to add accessible keyboard shortcuts to the administrator templates. + * + * @since __DEPLOY_VERSION__ + */ +class PlgSystemShortcut extends CMSPlugin implements SubscriberInterface +{ + /** + * Application object. + * + * @var AdministratorApplication + * + * @since __DEPLOY_VERSION__ + */ + protected $app; + + /** + * Load the language file on instantiation. + * + * @var boolean + * @since __DEPLOY_VERSION__ + */ + protected $autoloadLanguage = true; + + /** + * Returns an array of events this subscriber will listen to. + * + * The array keys are event names and the value can be: + * + * - The method name to call (priority defaults to 0) + * - An array composed of the method name to call and the priority + * + * For instance: + * + * * array('eventName' => 'methodName') + * * array('eventName' => array('methodName', $priority)) + * + * @return array + * + * @since __DEPLOY_VERSION__ + */ + public static function getSubscribedEvents(): array + { + return [ + 'onBeforeCompileHead' => 'initialize', + 'onLoadShortcuts' => 'addShortcuts' + ]; + } + + + /** + * Add the javascript for the shortcuts + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function initialize() + { + if (!$this->app->isClient('administrator')) + { + return; + } + + PluginHelper::importPlugin('shortcut'); + + $context = $this->app->input->get('option') . '.' . $this->app->input->get('view'); + + $shortcuts = []; + + $event = new GenericEvent('onLoadShortcuts', [ + 'context' => $context, + 'shortcuts' => $shortcuts + ]); + + $this->app->getDispatcher()->dispatch('onLoadShortcuts', $event); + + $shortcuts = $event->getArgument('shortcuts'); + + Text::script('JAPPLY'); + Text::script('JCANCEL'); + Text::script('JHELP'); + Text::script('JOPTIONS'); + Text::script('JSEARCH_FILTER'); + Text::script('JTOOLBAR_CLOSE'); + Text::script('JTOOLBAR_NEW'); + Text::script('JTOOLBAR_SAVE'); + Text::script('JTOOLBAR_SAVE_AND_NEW'); + Text::script('PLG_SYSTEM_SHORTCUT_ACTIONS_FORM_LABEL'); + Text::script('PLG_SYSTEM_SHORTCUT_ACTIONS_GENERAL_LABEL'); + Text::script('PLG_SYSTEM_SHORTCUT_ACTIONS_LIST_LABEL'); + Text::script('PLG_SYSTEM_SHORTCUT_OVERVIEW_HINT'); + Text::script('PLG_SYSTEM_SHORTCUT_OVERVIEW_TITLE'); + + $wa = $this->app->getDocument()->getWebAssetManager(); + $wa->useScript('bootstrap.modal'); + $wa->registerAndUseScript('script', 'plg_system_shortcut/shortcut.min.js', ['dependencies' => ['hotkeys.js']]); + + $this->app->getDocument()->addScriptOptions('plg_system_shortcut.shortcuts', $shortcuts); + } + + /** + * Add default shortcuts to the document + * + * @param Event $event The event + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function addShortcuts(Event $event) + { + $shortcuts = $event->getArgument('shortcuts'); + + $keys = [ + 'applyKey' => (object) ['selector' => 'joomla-toolbar-button .button-apply', 'shortcut' => 'J + A', 'title' => Text::_('JAPPLY')], + 'cancelKey' => (object) ['selector' => 'joomla-toolbar-button .button-cancel', 'shortcut' => 'J + Q', 'title' => Text::_('JCANCEL')], + 'helpKey' => (object) ['selector' => 'joomla-toolbar-button .button-help', 'shortcut' => 'J + H', 'title' => Text::_('JHELP')], + 'newKey' => (object) ['selector' => 'joomla-toolbar-button .button-new', 'shortcut' => 'J + N', 'title' => Text::_('JTOOLBAR_NEW')], + 'optionKey' => (object) ['selector' => 'joomla-toolbar-button .button-options', 'shortcut' => 'J + O', 'title' => Text::_('JOPTIONS')], + 'saveKey' => (object) ['selector' => 'joomla-toolbar-button .button-save', 'shortcut' => 'J + S', 'title' => Text::_('JTOOLBAR_SAVE')], + 'saveNewKey' => (object) ['selector' => 'joomla-toolbar-button .button-save-new', 'shortcut' => 'J + N', 'title' => Text::_('JTOOLBAR_SAVE_AND_NEW')], + 'searchKey' => (object) ['selector' => 'input[placeholder=' . Text::_('JSEARCH_FILTER') . ']', 'shortcut' => 'J + S', 'title' => Text::_('JSEARCH_FILTER')], + ]; + + $event->setArgument('shortcuts', $keys); + } +} diff --git a/plugins/system/shortcut/shortcut.xml b/plugins/system/shortcut/shortcut.xml new file mode 100644 index 00000000000..724dbb4c54d --- /dev/null +++ b/plugins/system/shortcut/shortcut.xml @@ -0,0 +1,23 @@ + + + plg_system_shortcut + Joomla! Project + Jun 2022 + (C) 2022 Open Source Matters, Inc. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + __DEPLOY_VERSION__ + PLG_SYSTEM_SHORTCUT_XML_DESCRIPTION + Joomla\Plugin\System\Shortcut + + js + + + shortcut.php + + + language/en-GB/plg_system_shortcut.ini + language/en-GB/plg_system_shortcut.sys.ini + +