Distributed shortcut actions to common ui elements

This commit is contained in:
Dan Brown 2022-11-05 13:39:17 +00:00
parent b4cb375a02
commit 78b6450031
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
11 changed files with 95 additions and 42 deletions

View File

@ -3,8 +3,31 @@
* @type {Object<string, string>} * @type {Object<string, string>}
*/ */
const defaultMap = { 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": "/", "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) { function reverseMap(map) {
@ -26,10 +49,10 @@ class Shortcuts {
this.mapByShortcut = reverseMap(this.mapById); this.mapByShortcut = reverseMap(this.mapById);
this.hintsShowing = false; this.hintsShowing = false;
this.hideHints = this.hideHints.bind(this);
// TODO - Allow custom key maps // TODO - Allow custom key maps
// TODO - Allow turning off shortcuts // TODO - Allow turning off shortcuts
// TODO - Roll out to interface elements
// TODO - Hide hints on focus, scroll, click
this.setupListeners(); this.setupListeners();
} }
@ -53,7 +76,6 @@ class Shortcuts {
window.addEventListener('keydown', event => { window.addEventListener('keydown', event => {
if (event.key === '?') { if (event.key === '?') {
this.hintsShowing ? this.hideHints() : this.showHints(); this.hintsShowing ? this.hideHints() : this.showHints();
this.hintsShowing = !this.hintsShowing;
} }
}); });
} }
@ -81,6 +103,12 @@ class Shortcuts {
return true; 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); console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el);
return false; return false;
@ -88,11 +116,24 @@ class Shortcuts {
showHints() { showHints() {
const shortcutEls = this.container.querySelectorAll('[data-shortcut]'); const shortcutEls = this.container.querySelectorAll('[data-shortcut]');
const displayedIds = new Set();
for (const shortcutEl of shortcutEls) { for (const shortcutEl of shortcutEls) {
const id = shortcutEl.getAttribute('data-shortcut'); const id = shortcutEl.getAttribute('data-shortcut');
if (displayedIds.has(id)) {
continue;
}
const key = this.mapById[id]; const key = this.mapById[id];
this.showHintLabel(shortcutEl, key); 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) { showHintLabel(targetEl, key) {
@ -113,6 +154,13 @@ class Shortcuts {
for (const hint of hints) { for (const hint of hints) {
hint.remove(); 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;
} }
} }

View File

@ -37,7 +37,7 @@
<h5>{{ trans('common.actions') }}</h5> <h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary"> <div class="icon-list text-primary">
@if(user()->can('book-create-all')) @if(user()->can('book-create-all'))
<a href="{{ url("/create-book") }}" class="icon-list-item"> <a href="{{ url("/create-book") }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span> <span>@icon('add')</span>
<span>{{ trans('entities.books_create') }}</span> <span>{{ trans('entities.books_create') }}</span>
</a> </a>

View File

@ -94,13 +94,13 @@
<div class="icon-list text-primary"> <div class="icon-list text-primary">
@if(userCan('page-create', $book)) @if(userCan('page-create', $book))
<a href="{{ $book->getUrl('/create-page') }}" class="icon-list-item"> <a href="{{ $book->getUrl('/create-page') }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span> <span>@icon('add')</span>
<span>{{ trans('entities.pages_new') }}</span> <span>{{ trans('entities.pages_new') }}</span>
</a> </a>
@endif @endif
@if(userCan('chapter-create', $book)) @if(userCan('chapter-create', $book))
<a href="{{ $book->getUrl('/create-chapter') }}" class="icon-list-item"> <a href="{{ $book->getUrl('/create-chapter') }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span> <span>@icon('add')</span>
<span>{{ trans('entities.chapters_new') }}</span> <span>{{ trans('entities.chapters_new') }}</span>
</a> </a>
@ -113,25 +113,25 @@
<span>@icon('edit')</span> <span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span> <span>{{ trans('common.edit') }}</span>
</a> </a>
<a href="{{ $book->getUrl('/sort') }}" class="icon-list-item"> <a href="{{ $book->getUrl('/sort') }}" data-shortcut="sort" class="icon-list-item">
<span>@icon('sort')</span> <span>@icon('sort')</span>
<span>{{ trans('common.sort') }}</span> <span>{{ trans('common.sort') }}</span>
</a> </a>
@endif @endif
@if(userCan('book-create-all')) @if(userCan('book-create-all'))
<a href="{{ $book->getUrl('/copy') }}" class="icon-list-item"> <a href="{{ $book->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
<span>@icon('copy')</span> <span>@icon('copy')</span>
<span>{{ trans('common.copy') }}</span> <span>{{ trans('common.copy') }}</span>
</a> </a>
@endif @endif
@if(userCan('restrictions-manage', $book)) @if(userCan('restrictions-manage', $book))
<a href="{{ $book->getUrl('/permissions') }}" class="icon-list-item"> <a href="{{ $book->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
<span>@icon('lock')</span> <span>@icon('lock')</span>
<span>{{ trans('entities.permissions') }}</span> <span>{{ trans('entities.permissions') }}</span>
</a> </a>
@endif @endif
@if(userCan('book-delete', $book)) @if(userCan('book-delete', $book))
<a href="{{ $book->getUrl('/delete') }}" class="icon-list-item"> <a href="{{ $book->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
<span>@icon('delete')</span> <span>@icon('delete')</span>
<span>{{ trans('common.delete') }}</span> <span>{{ trans('common.delete') }}</span>
</a> </a>

View File

@ -108,7 +108,7 @@
<div class="icon-list text-primary"> <div class="icon-list text-primary">
@if(userCan('page-create', $chapter)) @if(userCan('page-create', $chapter))
<a href="{{ $chapter->getUrl('/create-page') }}" class="icon-list-item"> <a href="{{ $chapter->getUrl('/create-page') }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span> <span>@icon('add')</span>
<span>{{ trans('entities.pages_new') }}</span> <span>{{ trans('entities.pages_new') }}</span>
</a> </a>
@ -117,31 +117,31 @@
<hr class="primary-background"/> <hr class="primary-background"/>
@if(userCan('chapter-update', $chapter)) @if(userCan('chapter-update', $chapter))
<a href="{{ $chapter->getUrl('/edit') }}" class="icon-list-item"> <a href="{{ $chapter->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
<span>@icon('edit')</span> <span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span> <span>{{ trans('common.edit') }}</span>
</a> </a>
@endif @endif
@if(userCanOnAny('create', \BookStack\Entities\Models\Book::class) || userCan('chapter-create-all') || userCan('chapter-create-own')) @if(userCanOnAny('create', \BookStack\Entities\Models\Book::class) || userCan('chapter-create-all') || userCan('chapter-create-own'))
<a href="{{ $chapter->getUrl('/copy') }}" class="icon-list-item"> <a href="{{ $chapter->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
<span>@icon('copy')</span> <span>@icon('copy')</span>
<span>{{ trans('common.copy') }}</span> <span>{{ trans('common.copy') }}</span>
</a> </a>
@endif @endif
@if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter)) @if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter))
<a href="{{ $chapter->getUrl('/move') }}" class="icon-list-item"> <a href="{{ $chapter->getUrl('/move') }}" data-shortcut="move" class="icon-list-item">
<span>@icon('folder')</span> <span>@icon('folder')</span>
<span>{{ trans('common.move') }}</span> <span>{{ trans('common.move') }}</span>
</a> </a>
@endif @endif
@if(userCan('restrictions-manage', $chapter)) @if(userCan('restrictions-manage', $chapter))
<a href="{{ $chapter->getUrl('/permissions') }}" class="icon-list-item"> <a href="{{ $chapter->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
<span>@icon('lock')</span> <span>@icon('lock')</span>
<span>{{ trans('entities.permissions') }}</span> <span>{{ trans('entities.permissions') }}</span>
</a> </a>
@endif @endif
@if(userCan('chapter-delete', $chapter)) @if(userCan('chapter-delete', $chapter))
<a href="{{ $chapter->getUrl('/delete') }}" class="icon-list-item"> <a href="{{ $chapter->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
<span>@icon('delete')</span> <span>@icon('delete')</span>
<span>{{ trans('common.delete') }}</span> <span>{{ trans('common.delete') }}</span>
</a> </a>
@ -149,7 +149,7 @@
@if($chapter->book && userCan('book-update', $chapter->book)) @if($chapter->book && userCan('book-update', $chapter->book))
<hr class="primary-background"/> <hr class="primary-background"/>
<a href="{{ $chapter->book->getUrl('/sort') }}" class="icon-list-item"> <a href="{{ $chapter->book->getUrl('/sort') }}" data-shortcut="sort" class="icon-list-item">
<span>@icon('sort')</span> <span>@icon('sort')</span>
<span>{{ trans('entities.chapter_sort_book') }}</span> <span>{{ trans('entities.chapter_sort_book') }}</span>
</a> </a>

View File

@ -2,7 +2,7 @@
<div class="grid mx-l"> <div class="grid mx-l">
<div> <div>
<a href="{{ url('/') }}" class="logo"> <a href="{{ url('/') }}" data-shortcut="home" class="logo">
@if(setting('app-logo', '') !== 'none') @if(setting('app-logo', '') !== 'none')
<img class="logo-image" src="{{ setting('app-logo', '') === '' ? url('/logo.png') : url(setting('app-logo', '')) }}" alt="Logo"> <img class="logo-image" src="{{ setting('app-logo', '') === '' ? url('/logo.png') : url(setting('app-logo', '')) }}" alt="Logo">
@endif @endif
@ -34,14 +34,14 @@
@if (hasAppAccess()) @if (hasAppAccess())
<a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a> <a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a>
@if(userCanOnAny('view', \BookStack\Entities\Models\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own')) @if(userCanOnAny('view', \BookStack\Entities\Models\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
<a href="{{ url('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a> <a href="{{ url('/shelves') }}" data-shortcut="shelves_view">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
@endif @endif
<a href="{{ url('/books') }}">@icon('books'){{ trans('entities.books') }}</a> <a href="{{ url('/books') }}" data-shortcut="books_view">@icon('books'){{ trans('entities.books') }}</a>
@if(signedInUser() && userCan('settings-manage')) @if(signedInUser() && userCan('settings-manage'))
<a href="{{ url('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a> <a href="{{ url('/settings') }}" data-shortcut="settings_view">@icon('settings'){{ trans('settings.settings') }}</a>
@endif @endif
@if(signedInUser() && userCan('users-manage') && !userCan('settings-manage')) @if(signedInUser() && userCan('users-manage') && !userCan('settings-manage'))
<a href="{{ url('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a> <a href="{{ url('/settings/users') }}" data-shortcut="settings_view">@icon('users'){{ trans('settings.users') }}</a>
@endif @endif
@endif @endif
@ -62,13 +62,13 @@
</span> </span>
<ul refs="dropdown@menu" class="dropdown-menu" role="menu"> <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
<li> <li>
<a href="{{ url('/favourites') }}" class="icon-item"> <a href="{{ url('/favourites') }}" data-shortcut="favorites_view" class="icon-item">
@icon('star') @icon('star')
<div>{{ trans('entities.my_favourites') }}</div> <div>{{ trans('entities.my_favourites') }}</div>
</a> </a>
</li> </li>
<li> <li>
<a href="{{ $currentUser->getProfileUrl() }}" class="icon-item"> <a href="{{ $currentUser->getProfileUrl() }}" data-shortcut="profile_view" class="icon-item">
@icon('user') @icon('user')
<div>{{ trans('common.view_profile') }}</div> <div>{{ trans('common.view_profile') }}</div>
</a> </a>
@ -83,7 +83,7 @@
<form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}" <form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
method="post"> method="post">
{{ csrf_field() }} {{ csrf_field() }}
<button class="icon-item"> <button class="icon-item" data-shortcut="logout">
@icon('logout') @icon('logout')
<div>{{ trans('auth.logout') }}</div> <div>{{ trans('auth.logout') }}</div>
</button> </button>

View File

@ -2,8 +2,13 @@
class="dropdown-container" class="dropdown-container"
id="export-menu"> id="export-menu">
<div refs="dropdown@toggle" class="icon-list-item" <div refs="dropdown@toggle"
aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('entities.export') }}" tabindex="0"> class="icon-list-item"
aria-haspopup="true"
aria-expanded="false"
aria-label="{{ trans('entities.export') }}"
data-shortcut="export"
tabindex="0">
<span>@icon('export')</span> <span>@icon('export')</span>
<span>{{ trans('entities.export') }}</span> <span>{{ trans('entities.export') }}</span>
</div> </div>

View File

@ -5,7 +5,7 @@
{{ csrf_field() }} {{ csrf_field() }}
<input type="hidden" name="type" value="{{ get_class($entity) }}"> <input type="hidden" name="type" value="{{ get_class($entity) }}">
<input type="hidden" name="id" value="{{ $entity->id }}"> <input type="hidden" name="id" value="{{ $entity->id }}">
<button type="submit" class="icon-list-item text-primary"> <button type="submit" data-shortcut="favorite" class="icon-list-item text-primary">
<span>@icon($isFavourite ? 'star' : 'star-outline')</span> <span>@icon($isFavourite ? 'star' : 'star-outline')</span>
<span>{{ $isFavourite ? trans('common.unfavourite') : trans('common.favourite') }}</span> <span>{{ $isFavourite ? trans('common.unfavourite') : trans('common.favourite') }}</span>
</button> </button>

View File

@ -1,7 +1,7 @@
<div id="sibling-navigation" class="grid half collapse-xs items-center mb-m px-m no-row-gap fade-in-when-active print-hidden"> <div id="sibling-navigation" class="grid half collapse-xs items-center mb-m px-m no-row-gap fade-in-when-active print-hidden">
<div> <div>
@if($previous) @if($previous)
<a href="{{ $previous->getUrl() }}" class="outline-hover no-link-style block rounded"> <a href="{{ $previous->getUrl() }}" data-shortcut="prev" class="outline-hover no-link-style block rounded">
<div class="px-m pt-xs text-muted">{{ trans('common.previous') }}</div> <div class="px-m pt-xs text-muted">{{ trans('common.previous') }}</div>
<div class="inline-block"> <div class="inline-block">
<div class="icon-list-item no-hover"> <div class="icon-list-item no-hover">
@ -14,7 +14,7 @@
</div> </div>
<div> <div>
@if($next) @if($next)
<a href="{{ $next->getUrl() }}" class="outline-hover no-link-style block rounded text-xs-right"> <a href="{{ $next->getUrl() }}" data-shortcut="next" class="outline-hover no-link-style block rounded text-xs-right">
<div class="px-m pt-xs text-muted text-xs-right">{{ trans('common.next') }}</div> <div class="px-m pt-xs text-muted text-xs-right">{{ trans('common.next') }}</div>
<div class="inline block"> <div class="inline block">
<div class="icon-list-item no-hover"> <div class="icon-list-item no-hover">

View File

@ -145,37 +145,37 @@
{{--User Actions--}} {{--User Actions--}}
@if(userCan('page-update', $page)) @if(userCan('page-update', $page))
<a href="{{ $page->getUrl('/edit') }}" class="icon-list-item"> <a href="{{ $page->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
<span>@icon('edit')</span> <span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span> <span>{{ trans('common.edit') }}</span>
</a> </a>
@endif @endif
@if(userCanOnAny('create', \BookStack\Entities\Models\Book::class) || userCanOnAny('create', \BookStack\Entities\Models\Chapter::class) || userCan('page-create-all') || userCan('page-create-own')) @if(userCanOnAny('create', \BookStack\Entities\Models\Book::class) || userCanOnAny('create', \BookStack\Entities\Models\Chapter::class) || userCan('page-create-all') || userCan('page-create-own'))
<a href="{{ $page->getUrl('/copy') }}" class="icon-list-item"> <a href="{{ $page->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
<span>@icon('copy')</span> <span>@icon('copy')</span>
<span>{{ trans('common.copy') }}</span> <span>{{ trans('common.copy') }}</span>
</a> </a>
@endif @endif
@if(userCan('page-update', $page)) @if(userCan('page-update', $page))
@if(userCan('page-delete', $page)) @if(userCan('page-delete', $page))
<a href="{{ $page->getUrl('/move') }}" class="icon-list-item"> <a href="{{ $page->getUrl('/move') }}" data-shortcut="move" class="icon-list-item">
<span>@icon('folder')</span> <span>@icon('folder')</span>
<span>{{ trans('common.move') }}</span> <span>{{ trans('common.move') }}</span>
</a> </a>
@endif @endif
@endif @endif
<a href="{{ $page->getUrl('/revisions') }}" class="icon-list-item"> <a href="{{ $page->getUrl('/revisions') }}" data-shortcut="revisions" class="icon-list-item">
<span>@icon('history')</span> <span>@icon('history')</span>
<span>{{ trans('entities.revisions') }}</span> <span>{{ trans('entities.revisions') }}</span>
</a> </a>
@if(userCan('restrictions-manage', $page)) @if(userCan('restrictions-manage', $page))
<a href="{{ $page->getUrl('/permissions') }}" class="icon-list-item"> <a href="{{ $page->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
<span>@icon('lock')</span> <span>@icon('lock')</span>
<span>{{ trans('entities.permissions') }}</span> <span>{{ trans('entities.permissions') }}</span>
</a> </a>
@endif @endif
@if(userCan('page-delete', $page)) @if(userCan('page-delete', $page))
<a href="{{ $page->getUrl('/delete') }}" class="icon-list-item"> <a href="{{ $page->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
<span>@icon('delete')</span> <span>@icon('delete')</span>
<span>{{ trans('common.delete') }}</span> <span>{{ trans('common.delete') }}</span>
</a> </a>

View File

@ -10,7 +10,7 @@
<h5>{{ trans('common.actions') }}</h5> <h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary"> <div class="icon-list text-primary">
@if(userCan('bookshelf-create-all')) @if(userCan('bookshelf-create-all'))
<a href="{{ url("/create-shelf") }}" class="icon-list-item"> <a href="{{ url("/create-shelf") }}" data-shortcut="new" class="icon-list-item">
<span>@icon('add')</span> <span>@icon('add')</span>
<span>{{ trans('entities.shelves_new_action') }}</span> <span>{{ trans('entities.shelves_new_action') }}</span>
</a> </a>

View File

@ -112,7 +112,7 @@
<div class="icon-list text-primary"> <div class="icon-list text-primary">
@if(userCan('book-create-all') && userCan('bookshelf-update', $shelf)) @if(userCan('book-create-all') && userCan('bookshelf-update', $shelf))
<a href="{{ $shelf->getUrl('/create-book') }}" class="icon-list-item"> <a href="{{ $shelf->getUrl('/create-book') }}" data-shortcut="new" class="icon-list-item">
<span class="icon">@icon('add')</span> <span class="icon">@icon('add')</span>
<span>{{ trans('entities.books_new_action') }}</span> <span>{{ trans('entities.books_new_action') }}</span>
</a> </a>
@ -123,21 +123,21 @@
<hr class="primary-background"> <hr class="primary-background">
@if(userCan('bookshelf-update', $shelf)) @if(userCan('bookshelf-update', $shelf))
<a href="{{ $shelf->getUrl('/edit') }}" class="icon-list-item"> <a href="{{ $shelf->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
<span>@icon('edit')</span> <span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span> <span>{{ trans('common.edit') }}</span>
</a> </a>
@endif @endif
@if(userCan('restrictions-manage', $shelf)) @if(userCan('restrictions-manage', $shelf))
<a href="{{ $shelf->getUrl('/permissions') }}" class="icon-list-item"> <a href="{{ $shelf->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
<span>@icon('lock')</span> <span>@icon('lock')</span>
<span>{{ trans('entities.permissions') }}</span> <span>{{ trans('entities.permissions') }}</span>
</a> </a>
@endif @endif
@if(userCan('bookshelf-delete', $shelf)) @if(userCan('bookshelf-delete', $shelf))
<a href="{{ $shelf->getUrl('/delete') }}" class="icon-list-item"> <a href="{{ $shelf->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
<span>@icon('delete')</span> <span>@icon('delete')</span>
<span>{{ trans('common.delete') }}</span> <span>{{ trans('common.delete') }}</span>
</a> </a>