]> BookStack Code Mirror - bookstack/blob - resources/js/components/entity-selector.js
Updated markdown preview to update on diff-basis
[bookstack] / resources / js / components / entity-selector.js
1 import {onChildEvent} from "../services/dom";
2
3 /**
4  * Entity Selector
5  * @extends {Component}
6  */
7 class EntitySelector {
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         this.addButton = this.$refs.add;
19
20         this.search = '';
21         this.lastClick = 0;
22         this.selectedItemData = null;
23
24         this.setupListeners();
25         this.showLoading();
26         this.initialLoad();
27     }
28
29     setupListeners() {
30         this.elem.addEventListener('click', this.onClick.bind(this));
31
32         let lastSearch = 0;
33         this.searchInput.addEventListener('input', event => {
34             lastSearch = Date.now();
35             this.showLoading();
36             setTimeout(() => {
37                 if (Date.now() - lastSearch < 199) return;
38                 this.searchEntities(this.searchInput.value);
39             }, 200);
40         });
41
42         this.searchInput.addEventListener('keydown', event => {
43             if (event.keyCode === 13) event.preventDefault();
44         });
45
46         if (this.addButton) {
47             this.addButton.addEventListener('click', event => {
48                 if (this.selectedItemData) {
49                     this.confirmSelection(this.selectedItemData);
50                     this.unselectAll();
51                 }
52             });
53         }
54
55         // Keyboard navigation
56         onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => {
57             if (e.ctrlKey && e.code === 'Enter') {
58                 const form = this.$el.closest('form');
59                 if (form) {
60                     form.submit();
61                     e.preventDefault();
62                     return;
63                 }
64             }
65
66             if (e.code === 'ArrowDown') {
67                 this.focusAdjacent(true);
68             }
69             if (e.code === 'ArrowUp') {
70                 this.focusAdjacent(false);
71             }
72         });
73
74         this.searchInput.addEventListener('keydown', e => {
75             if (e.code === 'ArrowDown') {
76                 this.focusAdjacent(true);
77             }
78         })
79     }
80
81     focusAdjacent(forward = true) {
82         const items = Array.from(this.resultsContainer.querySelectorAll('[data-entity-type]'));
83         const selectedIndex = items.indexOf(document.activeElement);
84         const newItem = items[selectedIndex+ (forward ? 1 : -1)] || items[0];
85         if (newItem) {
86             newItem.focus();
87         }
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 `/ajax/search/entities?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: name, link: 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 }
179
180 export default EntitySelector;