]> BookStack Code Mirror - bookstack/commitdiff
Distributed shortcut actions to common ui elements
authorDan Brown <redacted>
Sat, 5 Nov 2022 13:39:17 +0000 (13:39 +0000)
committerDan Brown <redacted>
Sat, 5 Nov 2022 13:39:17 +0000 (13:39 +0000)
resources/js/components/shortcuts.js
resources/views/books/index.blade.php
resources/views/books/show.blade.php
resources/views/chapters/show.blade.php
resources/views/common/header.blade.php
resources/views/entities/export-menu.blade.php
resources/views/entities/favourite-action.blade.php
resources/views/entities/sibling-navigation.blade.php
resources/views/pages/show.blade.php
resources/views/shelves/index.blade.php
resources/views/shelves/show.blade.php

index 799f0e629e8ea3927dd236ddc39f921ac2ae9a9a..7feb9bed047f4c7f7fb480c305fa0b9fcbcf703b 100644 (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;
     }
 }
 
index 447d6fd44c21a3d5a313c9da913e778da2cecaaf..dc51a3a80af86cac004f03fb76335aec4af6b6ac 100644 (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>
index dbd2cbb35b95c17017011b741a8e03a7ed89338b..884082456b6c10dc39e05948590367349112c24f 100644 (file)
         <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>
index b3496eae23717f8bb648430ba149d06a84ab7945..d2f8cec97f7353548d948ba6fec308c6f6303a4c 100644 (file)
         <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>
index 1b0e64ac49ab0e22b745fe4aeb3a3c6a04f31f0c..0481b3412a6f3f567d41231e882be09bac05bdc3 100644 (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
                 @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>
@@ -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>
index bac240b1eed0c2a493b3ba8b09727067c08bf4c6..a55ab56d199cf174b6144f26bf0b7306bfaaa72d 100644 (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>
index 49ba6aa5db426224bfc0e9c6d367e4b31ab23a29..f38899e3ee6f457f9afc46da4a9044211a0c431e 100644 (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>
index 1f64bac3e5d56aa13f7733b0c0d24b98a67d15b9..629a8ff327301666e3b43df115c17f711e090af6 100644 (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">
index 66c3762a89f675bd0d2083864a1fafb44df81d38..32718b7f1e47873b98483fa9b0d5bbebebb55662 100644 (file)
 
             {{--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>
index 75d46318f019a3f04827f196cc63e61349bda0bc..3de4247cecbc1cc5f1d58adb03ce72c061069f27 100644 (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>
index 0195759d818592ee421cac2ba46b1c22cad5c0c1..0f65874071e446dc0e502f7cbebd78308663f1f5 100644 (file)
         <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>