From f36e6d991711db598bcdbaa7759a5322d5610584 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 12 Feb 2021 22:10:37 +0000 Subject: [PATCH] Updtd entity-selector for keyboard nav and new component system For #2064 --- resources/js/components/entity-selector.js | 83 ++++++++++++++----- resources/sass/styles.scss | 6 +- .../components/entity-selector.blade.php | 15 ++-- resources/views/pages/move.blade.php | 2 +- 4 files changed, 79 insertions(+), 27 deletions(-) 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 @@
-
- - -
@include('partials.loading-icon')
-
+
+ + +
@include('partials.loading-icon')
+
@if($showAdd ?? false)
-
@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() !!} - @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])