mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-05-03 07:49:57 +00:00
Started implementation of UI shortcuts system
This commit is contained in:
parent
33e5c85503
commit
b4cb375a02
6 changed files with 138 additions and 2 deletions
resources
js/components
sass
views
|
@ -43,6 +43,7 @@ import popup from "./popup.js"
|
||||||
import settingAppColorPicker from "./setting-app-color-picker.js"
|
import settingAppColorPicker from "./setting-app-color-picker.js"
|
||||||
import settingColorPicker from "./setting-color-picker.js"
|
import settingColorPicker from "./setting-color-picker.js"
|
||||||
import shelfSort from "./shelf-sort.js"
|
import shelfSort from "./shelf-sort.js"
|
||||||
|
import shortcuts from "./shortcuts";
|
||||||
import sidebar from "./sidebar.js"
|
import sidebar from "./sidebar.js"
|
||||||
import sortableList from "./sortable-list.js"
|
import sortableList from "./sortable-list.js"
|
||||||
import submitOnChange from "./submit-on-change.js"
|
import submitOnChange from "./submit-on-change.js"
|
||||||
|
@ -101,6 +102,7 @@ const componentMapping = {
|
||||||
"setting-app-color-picker": settingAppColorPicker,
|
"setting-app-color-picker": settingAppColorPicker,
|
||||||
"setting-color-picker": settingColorPicker,
|
"setting-color-picker": settingColorPicker,
|
||||||
"shelf-sort": shelfSort,
|
"shelf-sort": shelfSort,
|
||||||
|
"shortcuts": shortcuts,
|
||||||
"sidebar": sidebar,
|
"sidebar": sidebar,
|
||||||
"sortable-list": sortableList,
|
"sortable-list": sortableList,
|
||||||
"submit-on-change": submitOnChange,
|
"submit-on-change": submitOnChange,
|
||||||
|
|
119
resources/js/components/shortcuts.js
Normal file
119
resources/js/components/shortcuts.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
/**
|
||||||
|
* The default mapping of unique id to shortcut key.
|
||||||
|
* @type {Object<string, string>}
|
||||||
|
*/
|
||||||
|
const defaultMap = {
|
||||||
|
"edit": "e",
|
||||||
|
"global_search": "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
function reverseMap(map) {
|
||||||
|
const reversed = {};
|
||||||
|
for (const [key, value] of Object.entries(map)) {
|
||||||
|
reversed[value] = key;
|
||||||
|
}
|
||||||
|
return reversed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends {Component}
|
||||||
|
*/
|
||||||
|
class Shortcuts {
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.container = this.$el;
|
||||||
|
this.mapById = defaultMap;
|
||||||
|
this.mapByShortcut = reverseMap(this.mapById);
|
||||||
|
|
||||||
|
this.hintsShowing = false;
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupListeners() {
|
||||||
|
window.addEventListener('keydown', event => {
|
||||||
|
|
||||||
|
if (event.target.closest('input, select, textarea')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcutId = this.mapByShortcut[event.key];
|
||||||
|
if (shortcutId) {
|
||||||
|
const wasHandled = this.runShortcut(shortcutId);
|
||||||
|
if (wasHandled) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('keydown', event => {
|
||||||
|
if (event.key === '?') {
|
||||||
|
this.hintsShowing ? this.hideHints() : this.showHints();
|
||||||
|
this.hintsShowing = !this.hintsShowing;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the given shortcut, and return a boolean to indicate if the event
|
||||||
|
* was successfully handled by a shortcut action.
|
||||||
|
* @param {String} id
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
runShortcut(id) {
|
||||||
|
const el = this.container.querySelector(`[data-shortcut="${id}"]`);
|
||||||
|
console.info('Shortcut run', el);
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.matches('input, textarea, select')) {
|
||||||
|
el.focus();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.matches('a, button')) {
|
||||||
|
el.click();
|
||||||
|
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]');
|
||||||
|
for (const shortcutEl of shortcutEls) {
|
||||||
|
const id = shortcutEl.getAttribute('data-shortcut');
|
||||||
|
const key = this.mapById[id];
|
||||||
|
this.showHintLabel(shortcutEl, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showHintLabel(targetEl, key) {
|
||||||
|
const targetBounds = targetEl.getBoundingClientRect();
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.classList.add('shortcut-hint');
|
||||||
|
label.textContent = key;
|
||||||
|
this.container.append(label);
|
||||||
|
|
||||||
|
const labelBounds = label.getBoundingClientRect();
|
||||||
|
|
||||||
|
label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 12))}px`;
|
||||||
|
label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideHints() {
|
||||||
|
const hints = this.container.querySelectorAll('.shortcut-hint');
|
||||||
|
for (const hint of hints) {
|
||||||
|
hint.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Shortcuts;
|
|
@ -982,4 +982,18 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
|
||||||
}
|
}
|
||||||
.status-indicator-inactive {
|
.status-indicator-inactive {
|
||||||
background-color: $negative;
|
background-color: $negative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-hint {
|
||||||
|
position: fixed;
|
||||||
|
padding: $-xxs $-xxs;
|
||||||
|
font-size: .85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
z-index: 99;
|
||||||
|
background-color: #eee;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #b4b4b4;
|
||||||
|
box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
|
||||||
|
color: #333;
|
||||||
}
|
}
|
|
@ -109,7 +109,7 @@
|
||||||
<hr class="primary-background">
|
<hr class="primary-background">
|
||||||
|
|
||||||
@if(userCan('book-update', $book))
|
@if(userCan('book-update', $book))
|
||||||
<a href="{{ $book->getUrl('/edit') }}" class="icon-list-item">
|
<a href="{{ $book->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>
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
<form action="{{ url('/search') }}" method="GET" class="search-box" role="search">
|
<form action="{{ url('/search') }}" method="GET" class="search-box" role="search">
|
||||||
<button id="header-search-box-button" type="submit" aria-label="{{ trans('common.search') }}" tabindex="-1">@icon('search') </button>
|
<button id="header-search-box-button" type="submit" aria-label="{{ trans('common.search') }}" tabindex="-1">@icon('search') </button>
|
||||||
<input id="header-search-box-input" type="text" name="term"
|
<input id="header-search-box-input" type="text" name="term"
|
||||||
|
data-shortcut="global_search"
|
||||||
aria-label="{{ trans('common.search') }}" placeholder="{{ trans('common.search') }}"
|
aria-label="{{ trans('common.search') }}" placeholder="{{ trans('common.search') }}"
|
||||||
value="{{ isset($searchTerm) ? $searchTerm : '' }}">
|
value="{{ isset($searchTerm) ? $searchTerm : '' }}">
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<!-- Translations for JS -->
|
<!-- Translations for JS -->
|
||||||
@stack('translations')
|
@stack('translations')
|
||||||
</head>
|
</head>
|
||||||
<body class="@stack('body-class')">
|
<body component="shortcuts" 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')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue