]> BookStack Code Mirror - bookstack/blobdiff - resources/js/components/entity-selector.js
add tests for priority
[bookstack] / resources / js / components / entity-selector.js
index 58879a20c0c5d8c76534e3af2bfc5a2da59c9e46..f12108fbb497877d9d96cd345e66cdf6a24f51bf 100644 (file)
@@ -1,26 +1,35 @@
+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;
 
-    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;
-        this.searchInput.addEventListener('input', event => {
+        this.searchInput.addEventListener('input', () => {
             lastSearch = Date.now();
             this.showLoading();
             setTimeout(() => {
@@ -33,19 +42,51 @@ class EntitySelector {
             if (event.keyCode === 13) event.preventDefault();
         });
 
-        if (this.addButton) {
-            this.addButton.addEventListener('click', event => {
-                if (this.selectedItemData) {
-                    this.confirmSelection(this.selectedItemData);
-                    this.unselectAll();
+        // Keyboard navigation
+        onChildEvent(this.$el, '[data-entity-type]', 'keydown', event => {
+            if (event.ctrlKey && event.code === 'Enter') {
+                const form = this.$el.closest('form');
+                if (form) {
+                    form.submit();
+                    event.preventDefault();
+                    return;
                 }
-            });
+            }
+
+            if (event.code === 'ArrowDown') {
+                this.focusAdjacent(true);
+            }
+            if (event.code === 'ArrowUp') {
+                this.focusAdjacent(false);
+            }
+        });
+
+        this.searchInput.addEventListener('keydown', event => {
+            if (event.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 +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 `/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 +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;
     }
@@ -99,13 +144,13 @@ class EntitySelector {
 
         const link = item.getAttribute('href');
         const name = item.querySelector('.entity-list-item-name').textContent;
-        const data = {id: Number(id), name: name, link: link};
+        const data = {id: Number(id), name, link};
 
         if (isSelected) {
             item.classList.add('selected');
             this.selectedItemData = data;
         } else {
-            window.$events.emit('entity-select-change', null)
+            window.$events.emit('entity-select-change', null);
         }
 
         if (!isDblClick && !isSelected) return;
@@ -114,7 +159,7 @@ class EntitySelector {
             this.confirmSelection(data);
         }
         if (isSelected) {
-            window.$events.emit('entity-select-change', data)
+            window.$events.emit('entity-select-change', data);
         }
     }
 
@@ -123,13 +168,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