]> BookStack Code Mirror - bookstack/blob - resources/js/components/entity-selector.js
Default templates: Added page picker and working forms
[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         this.searchEndpoint = this.$opts.searchEndpoint || '/search/entity-selector';
14
15         this.input = this.$refs.input;
16         this.searchInput = this.$refs.search;
17         this.loading = this.$refs.loading;
18         this.resultsContainer = this.$refs.results;
19
20         this.search = '';
21         this.lastClick = 0;
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     searchText(queryText) {
91         this.searchInput.value = queryText;
92         this.searchEntities(queryText);
93     }
94
95     showLoading() {
96         this.loading.style.display = 'block';
97         this.resultsContainer.style.display = 'none';
98     }
99
100     hideLoading() {
101         this.loading.style.display = 'none';
102         this.resultsContainer.style.display = 'block';
103     }
104
105     initialLoad() {
106         window.$http.get(this.searchUrl()).then(resp => {
107             this.resultsContainer.innerHTML = resp.data;
108             this.hideLoading();
109         });
110     }
111
112     searchUrl() {
113         return `${this.searchEndpoint}?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
114     }
115
116     searchEntities(searchTerm) {
117         this.input.value = '';
118         const url = `${this.searchUrl()}&term=${encodeURIComponent(searchTerm)}`;
119         window.$http.get(url).then(resp => {
120             this.resultsContainer.innerHTML = resp.data;
121             this.hideLoading();
122         });
123     }
124
125     isDoubleClick() {
126         const now = Date.now();
127         const answer = now - this.lastClick < 300;
128         this.lastClick = now;
129         return answer;
130     }
131
132     onClick(event) {
133         const listItem = event.target.closest('[data-entity-type]');
134         if (listItem) {
135             event.preventDefault();
136             event.stopPropagation();
137             this.selectItem(listItem);
138         }
139     }
140
141     selectItem(item) {
142         const isDblClick = this.isDoubleClick();
143         const type = item.getAttribute('data-entity-type');
144         const id = item.getAttribute('data-entity-id');
145         const isSelected = (!item.classList.contains('selected') || isDblClick);
146
147         this.unselectAll();
148         this.input.value = isSelected ? `${type}:${id}` : '';
149
150         const link = item.getAttribute('href');
151         const name = item.querySelector('.entity-list-item-name').textContent;
152         const data = {id: Number(id), name, link};
153
154         if (isSelected) {
155             item.classList.add('selected');
156         } else {
157             window.$events.emit('entity-select-change', null);
158         }
159
160         if (!isDblClick && !isSelected) return;
161
162         if (isDblClick) {
163             this.confirmSelection(data);
164         }
165         if (isSelected) {
166             window.$events.emit('entity-select-change', data);
167         }
168     }
169
170     confirmSelection(data) {
171         window.$events.emit('entity-select-confirm', data);
172     }
173
174     unselectAll() {
175         const selected = this.elem.querySelectorAll('.selected');
176         for (const selectedElem of selected) {
177             selectedElem.classList.remove('selected', 'primary-background');
178         }
179     }
180
181 }