X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/e91ef54cc9f8ce6b264bced8191275b6a33e594f..refs/pull/3908/head:/resources/js/components/entity-selector.js diff --git a/resources/js/components/entity-selector.js b/resources/js/components/entity-selector.js index 58879a20c..1384b33a9 100644 --- a/resources/js/components/entity-selector.js +++ b/resources/js/components/entity-selector.js @@ -1,22 +1,32 @@ +import {onChildEvent} from "../services/dom"; +import {Component} from "./component"; -class EntitySelector { +/** + * Entity Selector + */ +export class EntitySelector extends Component { + + 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; - constructor(elem) { - this.elem = elem; 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,10 +52,51 @@ class EntitySelector { }); } + // 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(); + } + } + + reset() { + this.searchInput.value = ''; this.showLoading(); this.initialLoad(); } + focusSearch() { + this.searchInput.focus(); + } + showLoading() { this.loading.style.display = 'block'; this.resultsContainer.style.display = 'none'; @@ -57,15 +108,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 `/search/entity-selector?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 +128,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,13 +178,11 @@ 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; } -} - -export default EntitySelector; \ No newline at end of file +} \ No newline at end of file