From b4cb375a02d2c81ac72ce9002d9f9ee2ab7f3922 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 4 Nov 2022 15:20:19 +0000 Subject: [PATCH 1/8] Started implementation of UI shortcuts system --- resources/js/components/index.js | 2 + resources/js/components/shortcuts.js | 119 ++++++++++++++++++++++++ resources/sass/_components.scss | 14 +++ resources/views/books/show.blade.php | 2 +- resources/views/common/header.blade.php | 1 + resources/views/layouts/base.blade.php | 2 +- 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 resources/js/components/shortcuts.js diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 5b84edba0..6203d0b74 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -43,6 +43,7 @@ import popup from "./popup.js" import settingAppColorPicker from "./setting-app-color-picker.js" import settingColorPicker from "./setting-color-picker.js" import shelfSort from "./shelf-sort.js" +import shortcuts from "./shortcuts"; import sidebar from "./sidebar.js" import sortableList from "./sortable-list.js" import submitOnChange from "./submit-on-change.js" @@ -101,6 +102,7 @@ const componentMapping = { "setting-app-color-picker": settingAppColorPicker, "setting-color-picker": settingColorPicker, "shelf-sort": shelfSort, + "shortcuts": shortcuts, "sidebar": sidebar, "sortable-list": sortableList, "submit-on-change": submitOnChange, diff --git a/resources/js/components/shortcuts.js b/resources/js/components/shortcuts.js new file mode 100644 index 000000000..799f0e629 --- /dev/null +++ b/resources/js/components/shortcuts.js @@ -0,0 +1,119 @@ +/** + * The default mapping of unique id to shortcut key. + * @type {Object} + */ +const defaultMap = { + "edit": "e", + "global_search": "/", +}; + +function reverseMap(map) { + const reversed = {}; + for (const [key, value] of Object.entries(map)) { + reversed[value] = key; + } + return reversed; +} + +/** + * @extends {Component} + */ +class Shortcuts { + + setup() { + this.container = this.$el; + this.mapById = defaultMap; + this.mapByShortcut = reverseMap(this.mapById); + + this.hintsShowing = false; + // TODO - Allow custom key maps + // TODO - Allow turning off shortcuts + // TODO - Roll out to interface elements + // TODO - Hide hints on focus, scroll, click + + this.setupListeners(); + } + + setupListeners() { + window.addEventListener('keydown', event => { + + if (event.target.closest('input, select, textarea')) { + return; + } + + const shortcutId = this.mapByShortcut[event.key]; + if (shortcutId) { + const wasHandled = this.runShortcut(shortcutId); + if (wasHandled) { + event.preventDefault(); + } + } + }); + + window.addEventListener('keydown', event => { + if (event.key === '?') { + this.hintsShowing ? this.hideHints() : this.showHints(); + this.hintsShowing = !this.hintsShowing; + } + }); + } + + /** + * Run the given shortcut, and return a boolean to indicate if the event + * was successfully handled by a shortcut action. + * @param {String} id + * @return {boolean} + */ + runShortcut(id) { + const el = this.container.querySelector(`[data-shortcut="${id}"]`); + console.info('Shortcut run', el); + if (!el) { + return false; + } + + if (el.matches('input, textarea, select')) { + el.focus(); + return true; + } + + if (el.matches('a, button')) { + el.click(); + return true; + } + + console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el); + + return false; + } + + showHints() { + const shortcutEls = this.container.querySelectorAll('[data-shortcut]'); + for (const shortcutEl of shortcutEls) { + const id = shortcutEl.getAttribute('data-shortcut'); + const key = this.mapById[id]; + this.showHintLabel(shortcutEl, key); + } + } + + showHintLabel(targetEl, key) { + const targetBounds = targetEl.getBoundingClientRect(); + const label = document.createElement('div'); + label.classList.add('shortcut-hint'); + label.textContent = key; + this.container.append(label); + + const labelBounds = label.getBoundingClientRect(); + + label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 12))}px`; + label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`; + } + + hideHints() { + const hints = this.container.querySelectorAll('.shortcut-hint'); + for (const hint of hints) { + hint.remove(); + } + } +} + +export default Shortcuts; \ No newline at end of file diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index acb45100f..661fce758 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -982,4 +982,18 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } .status-indicator-inactive { background-color: $negative; +} + +.shortcut-hint { + position: fixed; + padding: $-xxs $-xxs; + font-size: .85rem; + font-weight: 700; + line-height: 1; + z-index: 99; + background-color: #eee; + border-radius: 3px; + border: 1px solid #b4b4b4; + box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset; + color: #333; } \ No newline at end of file diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index b95b69d1b..dbd2cbb35 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -109,7 +109,7 @@
@if(userCan('book-update', $book)) - + @icon('edit') {{ trans('common.edit') }} diff --git a/resources/views/common/header.blade.php b/resources/views/common/header.blade.php index 197b80c27..1b0e64ac4 100644 --- a/resources/views/common/header.blade.php +++ b/resources/views/common/header.blade.php @@ -22,6 +22,7 @@ diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index 9f6e9f89a..928eb17a0 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -31,7 +31,7 @@ @stack('translations') - + @include('layouts.parts.base-body-start') @include('common.skip-to-content') From 78b645003187d0c709de0accf0e435e064400805 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 5 Nov 2022 13:39:17 +0000 Subject: [PATCH 2/8] Distributed shortcut actions to common ui elements --- resources/js/components/shortcuts.js | 56 +++++++++++++++++-- resources/views/books/index.blade.php | 2 +- resources/views/books/show.blade.php | 12 ++-- resources/views/chapters/show.blade.php | 14 ++--- resources/views/common/header.blade.php | 16 +++--- .../views/entities/export-menu.blade.php | 9 ++- .../views/entities/favourite-action.blade.php | 2 +- .../entities/sibling-navigation.blade.php | 4 +- resources/views/pages/show.blade.php | 12 ++-- resources/views/shelves/index.blade.php | 2 +- resources/views/shelves/show.blade.php | 8 +-- 11 files changed, 95 insertions(+), 42 deletions(-) diff --git a/resources/js/components/shortcuts.js b/resources/js/components/shortcuts.js index 799f0e629..7feb9bed0 100644 --- a/resources/js/components/shortcuts.js +++ b/resources/js/components/shortcuts.js @@ -3,8 +3,31 @@ * @type {Object} */ const defaultMap = { - "edit": "e", + // Header actions + "home": "1", + "shelves_view": "2", + "books_view": "3", + "settings_view": "4", + "favorites_view": "5", + "profile_view": "6", "global_search": "/", + "logout": "0", + + // Generic actions + "edit": "e", + "new": "n", + "copy": "c", + "delete": "d", + "favorite": "f", + "export": "x", + "sort": "s", + "permissions": "p", + "move": "m", + "revisions": "r", + + // Navigation + "next": "ArrowRight", + "prev": "ArrowLeft", }; function reverseMap(map) { @@ -26,10 +49,10 @@ class Shortcuts { this.mapByShortcut = reverseMap(this.mapById); this.hintsShowing = false; + + this.hideHints = this.hideHints.bind(this); // TODO - Allow custom key maps // TODO - Allow turning off shortcuts - // TODO - Roll out to interface elements - // TODO - Hide hints on focus, scroll, click this.setupListeners(); } @@ -53,7 +76,6 @@ class Shortcuts { window.addEventListener('keydown', event => { if (event.key === '?') { this.hintsShowing ? this.hideHints() : this.showHints(); - this.hintsShowing = !this.hintsShowing; } }); } @@ -81,6 +103,12 @@ class Shortcuts { return true; } + if (el.matches('div[tabindex]')) { + el.click(); + el.focus(); + return true; + } + console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el); return false; @@ -88,11 +116,24 @@ class Shortcuts { showHints() { const shortcutEls = this.container.querySelectorAll('[data-shortcut]'); + const displayedIds = new Set(); for (const shortcutEl of shortcutEls) { const id = shortcutEl.getAttribute('data-shortcut'); + if (displayedIds.has(id)) { + continue; + } + const key = this.mapById[id]; this.showHintLabel(shortcutEl, key); + displayedIds.add(id); } + + window.addEventListener('scroll', this.hideHints); + window.addEventListener('focus', this.hideHints); + window.addEventListener('blur', this.hideHints); + window.addEventListener('click', this.hideHints); + + this.hintsShowing = true; } showHintLabel(targetEl, key) { @@ -113,6 +154,13 @@ class Shortcuts { for (const hint of hints) { hint.remove(); } + + window.removeEventListener('scroll', this.hideHints); + window.removeEventListener('focus', this.hideHints); + window.removeEventListener('blur', this.hideHints); + window.removeEventListener('click', this.hideHints); + + this.hintsShowing = false; } } diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php index 447d6fd44..dc51a3a80 100644 --- a/resources/views/books/index.blade.php +++ b/resources/views/books/index.blade.php @@ -37,7 +37,7 @@
{{ trans('common.actions') }}
@if(user()->can('book-create-all')) - + @icon('add') {{ trans('entities.books_create') }} diff --git a/resources/views/books/show.blade.php b/resources/views/books/show.blade.php index dbd2cbb35..884082456 100644 --- a/resources/views/books/show.blade.php +++ b/resources/views/books/show.blade.php @@ -94,13 +94,13 @@
@if(userCan('page-create', $book)) - + @icon('add') {{ trans('entities.pages_new') }} @endif @if(userCan('chapter-create', $book)) - + @icon('add') {{ trans('entities.chapters_new') }} @@ -113,25 +113,25 @@ @icon('edit') {{ trans('common.edit') }} - + @icon('sort') {{ trans('common.sort') }} @endif @if(userCan('book-create-all')) - + @icon('copy') {{ trans('common.copy') }} @endif @if(userCan('restrictions-manage', $book)) - + @icon('lock') {{ trans('entities.permissions') }} @endif @if(userCan('book-delete', $book)) - + @icon('delete') {{ trans('common.delete') }} diff --git a/resources/views/chapters/show.blade.php b/resources/views/chapters/show.blade.php index b3496eae2..d2f8cec97 100644 --- a/resources/views/chapters/show.blade.php +++ b/resources/views/chapters/show.blade.php @@ -108,7 +108,7 @@
@if(userCan('page-create', $chapter)) - + @icon('add') {{ trans('entities.pages_new') }} @@ -117,31 +117,31 @@
@if(userCan('chapter-update', $chapter)) - + @icon('edit') {{ trans('common.edit') }} @endif @if(userCanOnAny('create', \BookStack\Entities\Models\Book::class) || userCan('chapter-create-all') || userCan('chapter-create-own')) - + @icon('copy') {{ trans('common.copy') }} @endif @if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter)) - + @icon('folder') {{ trans('common.move') }} @endif @if(userCan('restrictions-manage', $chapter)) - + @icon('lock') {{ trans('entities.permissions') }} @endif @if(userCan('chapter-delete', $chapter)) - + @icon('delete') {{ trans('common.delete') }} @@ -149,7 +149,7 @@ @if($chapter->book && userCan('book-update', $chapter->book))
- + @icon('sort') {{ trans('entities.chapter_sort_book') }} diff --git a/resources/views/common/header.blade.php b/resources/views/common/header.blade.php index 1b0e64ac4..0481b3412 100644 --- a/resources/views/common/header.blade.php +++ b/resources/views/common/header.blade.php @@ -2,7 +2,7 @@
- @icon('search'){{ trans('common.search') }} @if(userCanOnAny('view', \BookStack\Entities\Models\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own')) - @icon('bookshelf'){{ trans('entities.shelves') }} + @icon('bookshelf'){{ trans('entities.shelves') }} @endif - @icon('books'){{ trans('entities.books') }} + @icon('books'){{ trans('entities.books') }} @if(signedInUser() && userCan('settings-manage')) - @icon('settings'){{ trans('settings.settings') }} + @icon('settings'){{ trans('settings.settings') }} @endif @if(signedInUser() && userCan('users-manage') && !userCan('settings-manage')) - @icon('users'){{ trans('settings.users') }} + @icon('users'){{ trans('settings.users') }} @endif @endif @@ -62,13 +62,13 @@