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>}
*/
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;
}
}

View File

@ -37,7 +37,7 @@
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@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>{{ trans('entities.books_create') }}</span>
</a>

View File

@ -94,13 +94,13 @@
<div class="icon-list text-primary">
@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>{{ trans('entities.pages_new') }}</span>
</a>
@endif
@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>{{ trans('entities.chapters_new') }}</span>
</a>
@ -113,25 +113,25 @@
<span>@icon('edit')</span>
<span>{{ trans('common.edit') }}</span>
</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>{{ trans('common.sort') }}</span>
</a>
@endif
@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>{{ trans('common.copy') }}</span>
</a>
@endif
@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>{{ trans('entities.permissions') }}</span>
</a>
@endif
@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>{{ trans('common.delete') }}</span>
</a>

View File

@ -108,7 +108,7 @@
<div class="icon-list text-primary">
@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>{{ trans('entities.pages_new') }}</span>
</a>
@ -117,31 +117,31 @@
<hr class="primary-background"/>
@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>{{ trans('common.edit') }}</span>
</a>
@endif
@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>{{ trans('common.copy') }}</span>
</a>
@endif
@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>{{ trans('common.move') }}</span>
</a>
@endif
@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>{{ trans('entities.permissions') }}</span>
</a>
@endif
@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>{{ trans('common.delete') }}</span>
</a>
@ -149,7 +149,7 @@
@if($chapter->book && userCan('book-update', $chapter->book))
<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>{{ trans('entities.chapter_sort_book') }}</span>
</a>

View File

@ -2,7 +2,7 @@
<div class="grid mx-l">
<div>
<a href="{{ url('/') }}" class="logo">
<a href="{{ url('/') }}" data-shortcut="home" class="logo">
@if(setting('app-logo', '') !== 'none')
<img class="logo-image" src="{{ setting('app-logo', '') === '' ? url('/logo.png') : url(setting('app-logo', '')) }}" alt="Logo">
@endif
@ -34,14 +34,14 @@
@if (hasAppAccess())
<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'))
<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
<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'))
<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
@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
@ -62,13 +62,13 @@
</span>
<ul refs="dropdown@menu" class="dropdown-menu" role="menu">
<li>
<a href="{{ url('/favourites') }}" class="icon-item">
<a href="{{ url('/favourites') }}" data-shortcut="favorites_view" class="icon-item">
@icon('star')
<div>{{ trans('entities.my_favourites') }}</div>
</a>
</li>
<li>
<a href="{{ $currentUser->getProfileUrl() }}" class="icon-item">
<a href="{{ $currentUser->getProfileUrl() }}" data-shortcut="profile_view" class="icon-item">
@icon('user')
<div>{{ trans('common.view_profile') }}</div>
</a>
@ -83,7 +83,7 @@
<form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
method="post">
{{ csrf_field() }}
<button class="icon-item">
<button class="icon-item" data-shortcut="logout">
@icon('logout')
<div>{{ trans('auth.logout') }}</div>
</button>

View File

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

View File

@ -5,7 +5,7 @@
{{ csrf_field() }}
<input type="hidden" name="type" value="{{ get_class($entity) }}">
<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>{{ $isFavourite ? trans('common.unfavourite') : trans('common.favourite') }}</span>
</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>
@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="inline-block">
<div class="icon-list-item no-hover">
@ -14,7 +14,7 @@
</div>
<div>
@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="inline block">
<div class="icon-list-item no-hover">

View File

@ -145,37 +145,37 @@
{{--User Actions--}}
@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>{{ trans('common.edit') }}</span>
</a>
@endif
@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>{{ trans('common.copy') }}</span>
</a>
@endif
@if(userCan('page-update', $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>{{ trans('common.move') }}</span>
</a>
@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>{{ trans('entities.revisions') }}</span>
</a>
@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>{{ trans('entities.permissions') }}</span>
</a>
@endif
@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>{{ trans('common.delete') }}</span>
</a>

View File

@ -10,7 +10,7 @@
<h5>{{ trans('common.actions') }}</h5>
<div class="icon-list text-primary">
@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>{{ trans('entities.shelves_new_action') }}</span>
</a>

View File

@ -112,7 +112,7 @@
<div class="icon-list text-primary">
@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>{{ trans('entities.books_new_action') }}</span>
</a>
@ -123,21 +123,21 @@
<hr class="primary-background">
@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>{{ trans('common.edit') }}</span>
</a>
@endif
@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>{{ trans('entities.permissions') }}</span>
</a>
@endif
@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>{{ trans('common.delete') }}</span>
</a>