mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-23 12:20:21 +00:00
Updtd entity-selector for keyboard nav and new component system
For #2064
This commit is contained in:
parent
6a4b020dd8
commit
f36e6d9917
4 changed files with 79 additions and 27 deletions
resources
js/components
sass
views
|
@ -1,22 +1,32 @@
|
||||||
|
import {onChildEvent} from "../services/dom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity Selector
|
||||||
|
* @extends {Component}
|
||||||
|
*/
|
||||||
class EntitySelector {
|
class EntitySelector {
|
||||||
|
|
||||||
constructor(elem) {
|
setup() {
|
||||||
this.elem = elem;
|
this.elem = this.$el;
|
||||||
|
this.entityTypes = this.$opts.entityTypes || 'page,book,chapter';
|
||||||
|
this.entityPermission = this.$opts.entityPermission || 'view';
|
||||||
|
|
||||||
|
this.input = this.$refs.input;
|
||||||
|
this.searchInput = this.$refs.search;
|
||||||
|
this.loading = this.$refs.loading;
|
||||||
|
this.resultsContainer = this.$refs.results;
|
||||||
|
this.addButton = this.$refs.add;
|
||||||
|
|
||||||
this.search = '';
|
this.search = '';
|
||||||
this.lastClick = 0;
|
this.lastClick = 0;
|
||||||
this.selectedItemData = null;
|
this.selectedItemData = null;
|
||||||
|
|
||||||
const entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
|
this.setupListeners();
|
||||||
const entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view';
|
this.showLoading();
|
||||||
this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`);
|
this.initialLoad();
|
||||||
|
}
|
||||||
this.input = elem.querySelector('[entity-selector-input]');
|
|
||||||
this.searchInput = elem.querySelector('[entity-selector-search]');
|
|
||||||
this.loading = elem.querySelector('[entity-selector-loading]');
|
|
||||||
this.resultsContainer = elem.querySelector('[entity-selector-results]');
|
|
||||||
this.addButton = elem.querySelector('[entity-selector-add-button]');
|
|
||||||
|
|
||||||
|
setupListeners() {
|
||||||
this.elem.addEventListener('click', this.onClick.bind(this));
|
this.elem.addEventListener('click', this.onClick.bind(this));
|
||||||
|
|
||||||
let lastSearch = 0;
|
let lastSearch = 0;
|
||||||
|
@ -42,8 +52,39 @@ class EntitySelector {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showLoading();
|
// Keyboard navigation
|
||||||
this.initialLoad();
|
onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => {
|
||||||
|
if (e.ctrlKey && e.code === 'Enter') {
|
||||||
|
const form = this.$el.closest('form');
|
||||||
|
if (form) {
|
||||||
|
form.submit();
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.code === 'ArrowDown') {
|
||||||
|
this.focusAdjacent(true);
|
||||||
|
}
|
||||||
|
if (e.code === 'ArrowUp') {
|
||||||
|
this.focusAdjacent(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.searchInput.addEventListener('keydown', e => {
|
||||||
|
if (e.code === 'ArrowDown') {
|
||||||
|
this.focusAdjacent(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
focusAdjacent(forward = true) {
|
||||||
|
const items = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
|
||||||
|
const selectedIndex = items.indexOf(document.activeElement);
|
||||||
|
const newItem = items[selectedIndex+ (forward ? 1 : -1)] || items[0];
|
||||||
|
if (newItem) {
|
||||||
|
newItem.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoading() {
|
showLoading() {
|
||||||
|
@ -57,15 +98,19 @@ class EntitySelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
initialLoad() {
|
initialLoad() {
|
||||||
window.$http.get(this.searchUrl).then(resp => {
|
window.$http.get(this.searchUrl()).then(resp => {
|
||||||
this.resultsContainer.innerHTML = resp.data;
|
this.resultsContainer.innerHTML = resp.data;
|
||||||
this.hideLoading();
|
this.hideLoading();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchUrl() {
|
||||||
|
return `/ajax/search/entities?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
|
||||||
|
}
|
||||||
|
|
||||||
searchEntities(searchTerm) {
|
searchEntities(searchTerm) {
|
||||||
this.input.value = '';
|
this.input.value = '';
|
||||||
let url = `${this.searchUrl}&term=${encodeURIComponent(searchTerm)}`;
|
const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
|
||||||
window.$http.get(url).then(resp => {
|
window.$http.get(url).then(resp => {
|
||||||
this.resultsContainer.innerHTML = resp.data;
|
this.resultsContainer.innerHTML = resp.data;
|
||||||
this.hideLoading();
|
this.hideLoading();
|
||||||
|
@ -73,8 +118,8 @@ class EntitySelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
isDoubleClick() {
|
isDoubleClick() {
|
||||||
let now = Date.now();
|
const now = Date.now();
|
||||||
let answer = now - this.lastClick < 300;
|
const answer = now - this.lastClick < 300;
|
||||||
this.lastClick = now;
|
this.lastClick = now;
|
||||||
return answer;
|
return answer;
|
||||||
}
|
}
|
||||||
|
@ -123,8 +168,8 @@ class EntitySelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
unselectAll() {
|
unselectAll() {
|
||||||
let selected = this.elem.querySelectorAll('.selected');
|
const selected = this.elem.querySelectorAll('.selected');
|
||||||
for (let selectedElem of selected) {
|
for (const selectedElem of selected) {
|
||||||
selectedElem.classList.remove('selected', 'primary-background');
|
selectedElem.classList.remove('selected', 'primary-background');
|
||||||
}
|
}
|
||||||
this.selectedItemData = null;
|
this.selectedItemData = null;
|
||||||
|
|
|
@ -193,8 +193,12 @@ $btt-size: 40px;
|
||||||
.entity-list-item p {
|
.entity-list-item p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
.entity-list-item:focus {
|
||||||
|
outline: 2px dotted var(--color-primary);
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
.entity-list-item.selected {
|
.entity-list-item.selected {
|
||||||
background-color: rgba(0, 0, 0, 0.05) !important;
|
@include lightDark(background-color, rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
|
||||||
}
|
}
|
||||||
.loading {
|
.loading {
|
||||||
height: 400px;
|
height: 400px;
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<div class="form-group entity-selector-container">
|
<div class="form-group entity-selector-container">
|
||||||
<div entity-selector class="entity-selector {{$selectorSize ?? ''}}" entity-types="{{ $entityTypes ?? 'book,chapter,page' }}" entity-permission="{{ $entityPermission ?? 'view' }}">
|
<div component="entity-selector"
|
||||||
<input type="hidden" entity-selector-input name="{{$name}}" value="">
|
class="entity-selector {{$selectorSize ?? ''}}"
|
||||||
<input type="text" placeholder="{{ trans('common.search') }}" entity-selector-search>
|
option:entity-selector:entity-types="{{ $entityTypes ?? 'book,chapter,page' }}"
|
||||||
<div class="text-center loading" entity-selector-loading>@include('partials.loading-icon')</div>
|
option:entity-selector:entity-permission="{{ $entityPermission ?? 'view' }}">
|
||||||
<div entity-selector-results></div>
|
<input refs="entity-selector@input" type="hidden" name="{{$name}}" value="">
|
||||||
|
<input type="text" placeholder="{{ trans('common.search') }}" @if($autofocus ?? false) autofocus @endif refs="entity-selector@search">
|
||||||
|
<div class="text-center loading" refs="entity-selector@loading">@include('partials.loading-icon')</div>
|
||||||
|
<div refs="entity-selector@results"></div>
|
||||||
@if($showAdd ?? false)
|
@if($showAdd ?? false)
|
||||||
<div class="entity-selector-add">
|
<div class="entity-selector-add">
|
||||||
<button entity-selector-add-button type="button"
|
<button refs="entity-selector@add" type="button"
|
||||||
class="button outline">@icon('add'){{ trans('common.add') }}</button>
|
class="button outline">@icon('add'){{ trans('common.add') }}</button>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
{!! csrf_field() !!}
|
{!! csrf_field() !!}
|
||||||
<input type="hidden" name="_method" value="PUT">
|
<input type="hidden" name="_method" value="PUT">
|
||||||
|
|
||||||
@include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create'])
|
@include('components.entity-selector', ['name' => 'entity_selection', 'selectorSize' => 'large', 'entityTypes' => 'book,chapter', 'entityPermission' => 'page-create', 'autofocus' => true])
|
||||||
|
|
||||||
<div class="form-group text-right">
|
<div class="form-group text-right">
|
||||||
<a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
|
<a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||||
|
|
Loading…
Add table
Reference in a new issue