* @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) {
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();
}
window.addEventListener('keydown', event => {
if (event.key === '?') {
this.hintsShowing ? this.hideHints() : this.showHints();
- this.hintsShowing = !this.hintsShowing;
}
});
}
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;
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) {
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;
}
}
<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>
<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>
<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>
<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>
<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>
@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>
<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
@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
</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>
<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>
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>
{{ 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>
<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">
</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">
{{--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>
<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>
<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>
<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>