diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.js index 58879a20c..6d9d06f86 100644 --- a/resources/js/components/entity-selector.js +++ b/resources/js/components/entity-selector.js @@ -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; diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index 614b7f295..743db9888 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -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; diff --git a/resources/views/components/entity-selector.blade.php b/resources/views/components/entity-selector.blade.php index cb41950cb..c71fdff63 100644 --- a/resources/views/components/entity-selector.blade.php +++ b/resources/views/components/entity-selector.blade.php @@ -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 diff --git a/resources/views/pages/move.blade.php b/resources/views/pages/move.blade.php index 3bf1db5e4..26b872cdd 100644 --- a/resources/views/pages/move.blade.php +++ b/resources/views/pages/move.blade.php @@ -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>