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
|
@ -1,22 +1,32 @@
|
|||
import {onChildEvent} from "../services/dom";
|
||||
|
||||
/**
|
||||
* Entity Selector
|
||||
* @extends {Component}
|
||||
*/
|
||||
class EntitySelector {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
setup() {
|
||||
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.lastClick = 0;
|
||||
this.selectedItemData = null;
|
||||
|
||||
const entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter';
|
||||
const entityPermission = elem.hasAttribute('entity-permission') ? elem.getAttribute('entity-permission') : 'view';
|
||||
this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}&permission=${encodeURIComponent(entityPermission)}`);
|
||||
|
||||
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]');
|
||||
this.setupListeners();
|
||||
this.showLoading();
|
||||
this.initialLoad();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
this.elem.addEventListener('click', this.onClick.bind(this));
|
||||
|
||||
let lastSearch = 0;
|
||||
|
@ -42,8 +52,39 @@ class EntitySelector {
|
|||
});
|
||||
}
|
||||
|
||||
this.showLoading();
|
||||
this.initialLoad();
|
||||
// Keyboard navigation
|
||||
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() {
|
||||
|
@ -57,15 +98,19 @@ class EntitySelector {
|
|||
}
|
||||
|
||||
initialLoad() {
|
||||
window.$http.get(this.searchUrl).then(resp => {
|
||||
window.$http.get(this.searchUrl()).then(resp => {
|
||||
this.resultsContainer.innerHTML = resp.data;
|
||||
this.hideLoading();
|
||||
})
|
||||
}
|
||||
|
||||
searchUrl() {
|
||||
return `/ajax/search/entities?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
|
||||
}
|
||||
|
||||
searchEntities(searchTerm) {
|
||||
this.input.value = '';
|
||||
let url = `${this.searchUrl}&term=${encodeURIComponent(searchTerm)}`;
|
||||
const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
|
||||
window.$http.get(url).then(resp => {
|
||||
this.resultsContainer.innerHTML = resp.data;
|
||||
this.hideLoading();
|
||||
|
@ -73,8 +118,8 @@ class EntitySelector {
|
|||
}
|
||||
|
||||
isDoubleClick() {
|
||||
let now = Date.now();
|
||||
let answer = now - this.lastClick < 300;
|
||||
const now = Date.now();
|
||||
const answer = now - this.lastClick < 300;
|
||||
this.lastClick = now;
|
||||
return answer;
|
||||
}
|
||||
|
@ -123,8 +168,8 @@ class EntitySelector {
|
|||
}
|
||||
|
||||
unselectAll() {
|
||||
let selected = this.elem.querySelectorAll('.selected');
|
||||
for (let selectedElem of selected) {
|
||||
const selected = this.elem.querySelectorAll('.selected');
|
||||
for (const selectedElem of selected) {
|
||||
selectedElem.classList.remove('selected', 'primary-background');
|
||||
}
|
||||
this.selectedItemData = null;
|
||||
|
|
|
@ -193,8 +193,12 @@ $btt-size: 40px;
|
|||
.entity-list-item p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.entity-list-item:focus {
|
||||
outline: 2px dotted var(--color-primary);
|
||||
outline-offset: -4px;
|
||||
}
|
||||
.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 {
|
||||
height: 400px;
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<div class="form-group entity-selector-container">
|
||||
<div entity-selector class="entity-selector {{$selectorSize ?? ''}}" entity-types="{{ $entityTypes ?? 'book,chapter,page' }}" entity-permission="{{ $entityPermission ?? 'view' }}">
|
||||
<input type="hidden" entity-selector-input name="{{$name}}" value="">
|
||||
<input type="text" placeholder="{{ trans('common.search') }}" entity-selector-search>
|
||||
<div class="text-center loading" entity-selector-loading>@include('partials.loading-icon')</div>
|
||||
<div entity-selector-results></div>
|
||||
<div component="entity-selector"
|
||||
class="entity-selector {{$selectorSize ?? ''}}"
|
||||
option:entity-selector:entity-types="{{ $entityTypes ?? 'book,chapter,page' }}"
|
||||
option:entity-selector:entity-permission="{{ $entityPermission ?? 'view' }}">
|
||||
<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)
|
||||
<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>
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
{!! csrf_field() !!}
|
||||
<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">
|
||||
<a href="{{ $page->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||
|
|
Loading…
Reference in a new issue