mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-18 02:22:39 +00:00
Started interface user shortcut form interface
Built controller actions and initual UI. Still needs JS logic for shortcut input handling.
This commit is contained in:
parent
1fc994177f
commit
66c8809799
13 changed files with 232 additions and 40 deletions
app
resources
icons
js/components
sass
views
common
entities
layouts
users/preferences
routes
|
@ -26,6 +26,8 @@ return [
|
||||||
|
|
||||||
// User-level default settings
|
// User-level default settings
|
||||||
'user' => [
|
'user' => [
|
||||||
|
'ui-shortcuts' => '{}',
|
||||||
|
'ui-shortcuts-enabled' => false,
|
||||||
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
|
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
|
||||||
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
|
||||||
'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'),
|
'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'),
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\UserRepo;
|
||||||
|
use BookStack\Settings\UserShortcutMap;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class UserPreferencesController extends Controller
|
class UserPreferencesController extends Controller
|
||||||
|
@ -14,6 +15,37 @@ class UserPreferencesController extends Controller
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the user-specific interface shortcuts.
|
||||||
|
*/
|
||||||
|
public function showShortcuts()
|
||||||
|
{
|
||||||
|
$shortcuts = UserShortcutMap::fromUserPreferences();
|
||||||
|
$enabled = setting()->getForCurrentUser('ui-shortcuts-enabled', false);
|
||||||
|
|
||||||
|
return view('users.preferences.shortcuts', [
|
||||||
|
'shortcuts' => $shortcuts,
|
||||||
|
'enabled' => $enabled,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user-specific interface shortcuts.
|
||||||
|
*/
|
||||||
|
public function updateShortcuts(Request $request)
|
||||||
|
{
|
||||||
|
$enabled = $request->get('enabled') === 'true';
|
||||||
|
$providedShortcuts = $request->get('shortcuts', []);
|
||||||
|
$shortcuts = new UserShortcutMap($providedShortcuts);
|
||||||
|
|
||||||
|
setting()->putUser(user(), 'ui-shortcuts', $shortcuts->toJson());
|
||||||
|
setting()->putUser(user(), 'ui-shortcuts-enabled', $enabled);
|
||||||
|
|
||||||
|
$this->showSuccessNotification('Shortcuts preferences have been updated!');
|
||||||
|
|
||||||
|
return redirect('/preferences/shortcuts');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the user's preferred book-list display setting.
|
* Update the user's preferred book-list display setting.
|
||||||
*/
|
*/
|
||||||
|
|
82
app/Settings/UserShortcutMap.php
Normal file
82
app/Settings/UserShortcutMap.php
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Settings;
|
||||||
|
|
||||||
|
class UserShortcutMap
|
||||||
|
{
|
||||||
|
protected const DEFAULTS = [
|
||||||
|
// Header actions
|
||||||
|
"home_view" => "1",
|
||||||
|
"shelves_view" => "2",
|
||||||
|
"books_view" => "3",
|
||||||
|
"settings_view" => "4",
|
||||||
|
"favourites_view" => "5",
|
||||||
|
"profile_view" => "6",
|
||||||
|
"global_search" => "/",
|
||||||
|
"logout" => "0",
|
||||||
|
|
||||||
|
// Common actions
|
||||||
|
"edit" => "e",
|
||||||
|
"new" => "n",
|
||||||
|
"copy" => "c",
|
||||||
|
"delete" => "d",
|
||||||
|
"favourite" => "f",
|
||||||
|
"export" => "x",
|
||||||
|
"sort" => "s",
|
||||||
|
"permissions" => "p",
|
||||||
|
"move" => "m",
|
||||||
|
"revisions" => "r",
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
"next" => "ArrowRight",
|
||||||
|
"previous" => "ArrowLeft",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
protected array $mapping;
|
||||||
|
|
||||||
|
public function __construct(array $map)
|
||||||
|
{
|
||||||
|
$this->mapping = static::DEFAULTS;
|
||||||
|
$this->merge($map);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge the given map into the current shortcut mapping.
|
||||||
|
*/
|
||||||
|
protected function merge(array $map): void
|
||||||
|
{
|
||||||
|
foreach ($map as $key => $value) {
|
||||||
|
if (is_string($value) && isset($this->mapping[$key])) {
|
||||||
|
$this->mapping[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the shortcut defined for the given ID.
|
||||||
|
*/
|
||||||
|
public function getShortcut(string $id): string
|
||||||
|
{
|
||||||
|
return $this->mapping[$id] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this mapping to JSON.
|
||||||
|
*/
|
||||||
|
public function toJson(): string
|
||||||
|
{
|
||||||
|
return json_encode($this->mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new instance from the current user's preferences.
|
||||||
|
*/
|
||||||
|
public static function fromUserPreferences(): self
|
||||||
|
{
|
||||||
|
$userKeyMap = setting()->getForCurrentUser('ui-shortcuts');
|
||||||
|
return new self(json_decode($userKeyMap, true) ?: []);
|
||||||
|
}
|
||||||
|
}
|
1
resources/icons/shortcuts.svg
Normal file
1
resources/icons/shortcuts.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm8 7H9c-.55 0-1-.45-1-1s.45-1 1-1h6c.55 0 1 .45 1 1s-.45 1-1 1zm1-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z"/></svg>
|
After (image error) Size: 367 B |
|
@ -1,35 +1,3 @@
|
||||||
/**
|
|
||||||
* The default mapping of unique id to shortcut key.
|
|
||||||
* @type {Object<string, string>}
|
|
||||||
*/
|
|
||||||
const defaultMap = {
|
|
||||||
// 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) {
|
function reverseMap(map) {
|
||||||
const reversed = {};
|
const reversed = {};
|
||||||
for (const [key, value] of Object.entries(map)) {
|
for (const [key, value] of Object.entries(map)) {
|
||||||
|
@ -45,14 +13,12 @@ class Shortcuts {
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.container = this.$el;
|
this.container = this.$el;
|
||||||
this.mapById = defaultMap;
|
this.mapById = JSON.parse(this.$opts.keyMap);
|
||||||
this.mapByShortcut = reverseMap(this.mapById);
|
this.mapByShortcut = reverseMap(this.mapById);
|
||||||
|
|
||||||
this.hintsShowing = false;
|
this.hintsShowing = false;
|
||||||
|
|
||||||
this.hideHints = this.hideHints.bind(this);
|
this.hideHints = this.hideHints.bind(this);
|
||||||
// TODO - Allow custom key maps
|
|
||||||
// TODO - Allow turning off shortcuts
|
|
||||||
|
|
||||||
this.setupListeners();
|
this.setupListeners();
|
||||||
}
|
}
|
||||||
|
|
|
@ -473,4 +473,10 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
|
||||||
.custom-file-input:focus + label {
|
.custom-file-input:focus + label {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
outline: 1px solid var(--color-primary);
|
outline: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.shortcut-input {
|
||||||
|
width: auto;
|
||||||
|
max-width: 120px;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="grid mx-l">
|
<div class="grid mx-l">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ url('/') }}" data-shortcut="home" class="logo">
|
<a href="{{ url('/') }}" data-shortcut="home_view" 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
|
||||||
|
@ -62,7 +62,7 @@
|
||||||
</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') }}" data-shortcut="favorites_view" class="icon-item">
|
<a href="{{ url('/favourites') }}" data-shortcut="favourites_view" class="icon-item">
|
||||||
@icon('star')
|
@icon('star')
|
||||||
<div>{{ trans('entities.my_favourites') }}</div>
|
<div>{{ trans('entities.my_favourites') }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -90,6 +90,12 @@
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
<li><hr></li>
|
<li><hr></li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url('/preferences/shortcuts') }}" class="icon-item">
|
||||||
|
@icon('shortcuts')
|
||||||
|
<div>{{ 'Shortcuts' }}</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@include('common.dark-mode-toggle', ['classes' => 'icon-item'])
|
@include('common.dark-mode-toggle', ['classes' => 'icon-item'])
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -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" data-shortcut="favorite" class="icon-list-item text-primary">
|
<button type="submit" data-shortcut="favourite" 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>
|
||||||
|
|
|
@ -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() }}" data-shortcut="prev" class="outline-hover no-link-style block rounded">
|
<a href="{{ $previous->getUrl() }}" data-shortcut="previous" 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">
|
||||||
|
|
|
@ -31,7 +31,12 @@
|
||||||
<!-- Translations for JS -->
|
<!-- Translations for JS -->
|
||||||
@stack('translations')
|
@stack('translations')
|
||||||
</head>
|
</head>
|
||||||
<body component="shortcuts" class="@stack('body-class')">
|
<body
|
||||||
|
@if(setting()->getForCurrentUser('ui-shortcuts-enabled', false))
|
||||||
|
component="shortcuts"
|
||||||
|
option:shortcuts:key-map="{{ \BookStack\Settings\UserShortcutMap::fromUserPreferences()->toJson() }}"
|
||||||
|
@endif
|
||||||
|
class="@stack('body-class')">
|
||||||
|
|
||||||
@include('layouts.parts.base-body-start')
|
@include('layouts.parts.base-body-start')
|
||||||
@include('common.skip-to-content')
|
@include('common.skip-to-content')
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="flex-container-row justify-space-between items-center gap-m item-list-row">
|
||||||
|
<label for="shortcut-{{ $label }}" class="bold flex px-m py-xs">{{ $label }}</label>
|
||||||
|
<div class="px-m py-xs">
|
||||||
|
<input type="text"
|
||||||
|
class="small flex-none shortcut-input px-s py-xs"
|
||||||
|
id="shortcut-{{ $id }}"
|
||||||
|
name="shortcut[{{ $id }}]"
|
||||||
|
readonly
|
||||||
|
value="{{ $shortcuts->getShortcut($id) }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
78
resources/views/users/preferences/shortcuts.blade.php
Normal file
78
resources/views/users/preferences/shortcuts.blade.php
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
@extends('layouts.simple')
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<div class="container small my-xl">
|
||||||
|
|
||||||
|
<section class="card content-wrap">
|
||||||
|
<form action="{{ url('/preferences/shortcuts') }}" method="post">
|
||||||
|
{{ method_field('put') }}
|
||||||
|
{{ csrf_field() }}
|
||||||
|
|
||||||
|
<h1 class="list-heading">Interface Keyboard Shortcuts</h1>
|
||||||
|
|
||||||
|
<div class="flex-container-row items-center gap-m wrap mb-m">
|
||||||
|
<p class="flex mb-none min-width-m text-small text-muted">
|
||||||
|
Here you can enable or disable keyboard system interface shortcuts, used for navigation
|
||||||
|
and actions. You can customize each of the shortcuts below.
|
||||||
|
</p>
|
||||||
|
<div class="flex min-width-m text-m-right">
|
||||||
|
@include('form.toggle-switch', [
|
||||||
|
'name' => 'enabled',
|
||||||
|
'value' => $enabled,
|
||||||
|
'label' => 'Keyboard shortcuts enabled',
|
||||||
|
])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 class="list-heading mb-m">Navigation</h2>
|
||||||
|
<div class="flex-container-row wrap gap-m mb-xl">
|
||||||
|
<div class="flex min-width-l item-list">
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => 'Homepage', 'id' => 'home_view'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.shelves'), 'id' => 'shelves_view'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.books'), 'id' => 'books_view'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('settings.settings'), 'id' => 'settings_view'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.my_favourites'), 'id' => 'favourites_view'])
|
||||||
|
</div>
|
||||||
|
<div class="flex min-width-l item-list">
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.view_profile'), 'id' => 'profile_view'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('auth.logout'), 'id' => 'logout'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => 'Global Search', 'id' => 'global_search'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.next'), 'id' => 'next'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.previous'), 'id' => 'previous'])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="list-heading mb-m">Common Actions</h2>
|
||||||
|
<div class="flex-container-row wrap gap-m mb-xl">
|
||||||
|
<div class="flex min-width-l item-list">
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.new'), 'id' => 'new'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.edit'), 'id' => 'edit'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.copy'), 'id' => 'copy'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.delete'), 'id' => 'delete'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.favourite'), 'id' => 'favourite'])
|
||||||
|
</div>
|
||||||
|
<div class="flex min-width-l item-list">
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.export'), 'id' => 'export'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.sort'), 'id' => 'sort'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.permissions'), 'id' => 'permissions'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('common.move'), 'id' => 'move'])
|
||||||
|
@include('users.preferences.parts.shortcut-control', ['label' => trans('entities.revisions'), 'id' => 'revisions'])
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-small text-muted">
|
||||||
|
Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will
|
||||||
|
highlight the available shortcuts for actions currently visible on the screen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-group text-right">
|
||||||
|
<button class="button">{{ 'Save Shortcuts' }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
@stop
|
|
@ -246,6 +246,9 @@ Route::middleware('auth')->group(function () {
|
||||||
Route::delete('/settings/users/{id}', [UserController::class, 'destroy']);
|
Route::delete('/settings/users/{id}', [UserController::class, 'destroy']);
|
||||||
|
|
||||||
// User Preferences
|
// User Preferences
|
||||||
|
Route::redirect('/preferences', '/');
|
||||||
|
Route::get('/preferences/shortcuts', [UserPreferencesController::class, 'showShortcuts']);
|
||||||
|
Route::put('/preferences/shortcuts', [UserPreferencesController::class, 'updateShortcuts']);
|
||||||
Route::patch('/settings/users/{id}/switch-books-view', [UserPreferencesController::class, 'switchBooksView']);
|
Route::patch('/settings/users/{id}/switch-books-view', [UserPreferencesController::class, 'switchBooksView']);
|
||||||
Route::patch('/settings/users/{id}/switch-shelves-view', [UserPreferencesController::class, 'switchShelvesView']);
|
Route::patch('/settings/users/{id}/switch-shelves-view', [UserPreferencesController::class, 'switchShelvesView']);
|
||||||
Route::patch('/settings/users/{id}/switch-shelf-view', [UserPreferencesController::class, 'switchShelfView']);
|
Route::patch('/settings/users/{id}/switch-shelf-view', [UserPreferencesController::class, 'switchShelfView']);
|
||||||
|
|
Loading…
Add table
Reference in a new issue