]> BookStack Code Mirror - bookstack/blob - resources/js/components/entity-selector.js
ESLINT: Addressed remaining detected issues
[bookstack] / resources / js / components / entity-selector.js
1 import {onChildEvent} from '../services/dom';
2 import {Component} from './component';
3
4 /**
5  * Entity Selector
6  */
7 export class EntitySelector extends Component {
8
9     setup() {
10         this.elem = this.$el;
11         this.entityTypes = this.$opts.entityTypes || 'page,book,chapter';
12         this.entityPermission = this.$opts.entityPermission || 'view';
13
14         this.input = this.$refs.input;
15         this.searchInput = this.$refs.search;
16         this.loading = this.$refs.loading;
17         this.resultsContainer = this.$refs.results;
18
19         this.search = '';
20         this.lastClick = 0;
21         this.selectedItemData = null;
22
23         this.setupListeners();
24         this.showLoading();
25         this.initialLoad();
26     }
27
28     setupListeners() {
29         this.elem.addEventListener('click', this.onClick.bind(this));
30
31         let lastSearch = 0;
32         this.searchInput.addEventListener('input', () => {
33             lastSearch = Date.now();
34             this.showLoading();
35             setTimeout(() => {
36                 if (Date.now() - lastSearch < 199) return;
37                 this.searchEntities(this.searchInput.value);
38             }, 200);
39         });
40
41         this.searchInput.addEventListener('keydown', event => {
42             if (event.keyCode === 13) event.preventDefault();
43         });
44
45         // Keyboard navigation
46         onChildEvent(this.$el, '[data-entity-type]', 'keydown', event => {
47             if (event.ctrlKey && event.code === 'Enter') {
48                 const form = this.$el.closest('form');
49                 if (form) {
50                     form.submit();
51                     event.preventDefault();
52                     return;
53                 }
54             }
55
56             if (event.code === 'ArrowDown') {
57                 this.focusAdjacent(true);
58             }
59             if (event.code === 'ArrowUp') {
60                 this.focusAdjacent(false);
61             }
62         });
63
64         this.searchInput.addEventListener('keydown', event => {
65             if (event.code === 'ArrowDown') {
66                 this.focusAdjacent(true);
67             }
68         });
69     }
70
71     focusAdjacent(forward = true) {
72         const items = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
73         const selectedIndex = items.indexOf(document.activeElement);
74         const newItem = items[selectedIndex + (forward ? 1 : -1)] || items[0];
75         if (newItem) {
76             newItem.focus();
77         }
78     }
79
80     reset() {
81         this.searchInput.value = '';
82         this.showLoading();
83         this.initialLoad();
84     }
85
86     focusSearch() {
87         this.searchInput.focus();
88     }
89
90     showLoading() {
91         this.loading.style.display = 'block';
92         this.resultsContainer.style.display = 'none';
93     }
94
95     hideLoading() {
96         this.loading.style.display = 'none';
97         this.resultsContainer.style.display = 'block';
98     }
99
100     initialLoad() {
101         window.$http.get(this.searchUrl()).then(resp => {
102             this.resultsContainer.innerHTML = resp.data;
103             this.hideLoading();
104         });
105     }
106
107     searchUrl() {
108         return `/search/entity-selector?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
109     }
110
111     searchEntities(searchTerm) {
112         this.input.value = '';
113         const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
114         window.$http.get(url).then(resp => {
115             this.resultsContainer.innerHTML = resp.data;
116             this.hideLoading();
117         });
118     }
119
120     isDoubleClick() {
121         const now = Date.now();
122         const answer = now - this.lastClick < 300;
123         this.lastClick = now;
124         return answer;
125     }
126
127     onClick(event) {
128         const listItem = event.target.closest('[data-entity-type]');
129         if (listItem) {
130             event.preventDefault();
131             event.stopPropagation();
132             this.selectItem(listItem);
133         }
134     }
135
136     selectItem(item) {
137         const isDblClick = this.isDoubleClick();
138         const type = item.getAttribute('data-entity-type');
139         const id = item.getAttribute('data-entity-id');
140         const isSelected = (!item.classList.contains('selected') || isDblClick);
141
142         this.unselectAll();
143         this.input.value = isSelected ? `${type}:${id}` : '';
144
145         const link = item.getAttribute('href');
146         const name = item.querySelector('.entity-list-item-name').textContent;
147         const data = {id: Number(id), name, link};
148
149         if (isSelected) {
150             item.classList.add('selected');
151             this.selectedItemData = data;
152         } else {
153             window.$events.emit('entity-select-change', null);
154         }
155
156         if (!isDblClick && !isSelected) return;
157
158         if (isDblClick) {
159             this.confirmSelection(data);
160         }
161         if (isSelected) {
162             window.$events.emit('entity-select-change', data);
163         }
164     }
165
166     confirmSelection(data) {
167         window.$events.emit('entity-select-confirm', data);
168     }
169
170     unselectAll() {
171         const selected = this.elem.querySelectorAll('.selected');
172         for (const selectedElem of selected) {
173             selectedElem.classList.remove('selected', 'primary-background');
174         }
175         this.selectedItemData = null;
176     }
177
178 }