mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-05-06 09:10:06 +00:00
Started migrating tag manager JS to HTML-first component
This commit is contained in:
parent
10305a4446
commit
4e107b9160
8 changed files with 201 additions and 16 deletions
resources/js
144
resources/js/components/auto-suggest.js
Normal file
144
resources/js/components/auto-suggest.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
import {escapeHtml} from "../services/util";
|
||||
import {onChildEvent} from "../services/dom";
|
||||
|
||||
const ajaxCache = {};
|
||||
|
||||
/**
|
||||
* AutoSuggest
|
||||
* @extends {Component}
|
||||
*/
|
||||
class AutoSuggest {
|
||||
setup() {
|
||||
this.parent = this.$el.parentElement;
|
||||
this.container = this.$el;
|
||||
this.type = this.$opts.type;
|
||||
this.url = this.$opts.url;
|
||||
this.input = this.$refs.input;
|
||||
this.list = this.$refs.list;
|
||||
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
this.input.addEventListener('input', this.requestSuggestions.bind(this));
|
||||
this.input.addEventListener('focus', this.requestSuggestions.bind(this));
|
||||
this.input.addEventListener('keydown', event => {
|
||||
if (event.key === 'Tab') {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
});
|
||||
|
||||
this.input.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
|
||||
this.container.addEventListener('keydown', this.containerKeyDown.bind(this));
|
||||
|
||||
onChildEvent(this.list, 'button', 'click', (event, el) => {
|
||||
this.selectSuggestion(el.textContent);
|
||||
});
|
||||
onChildEvent(this.list, 'button', 'keydown', (event, el) => {
|
||||
if (event.key === 'Enter') {
|
||||
this.selectSuggestion(el.textContent);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
selectSuggestion(value) {
|
||||
this.input.value = value;
|
||||
this.input.focus();
|
||||
this.hideSuggestions();
|
||||
}
|
||||
|
||||
containerKeyDown(event) {
|
||||
if (event.key === 'Enter') event.preventDefault();
|
||||
if (this.list.classList.contains('hidden')) return;
|
||||
|
||||
// Down arrow
|
||||
if (event.key === 'ArrowDown') {
|
||||
this.moveFocus(true);
|
||||
event.preventDefault();
|
||||
}
|
||||
// Up Arrow
|
||||
else if (event.key === 'ArrowUp') {
|
||||
this.moveFocus(false);
|
||||
event.preventDefault();
|
||||
}
|
||||
// Escape key
|
||||
else if (event.key === 'Escape') {
|
||||
this.hideSuggestions();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
moveFocus(forward = true) {
|
||||
const focusables = Array.from(this.container.querySelectorAll('input,button'));
|
||||
const index = focusables.indexOf(document.activeElement);
|
||||
const newFocus = focusables[index + (forward ? 1 : -1)];
|
||||
if (newFocus) {
|
||||
newFocus.focus()
|
||||
}
|
||||
}
|
||||
|
||||
async requestSuggestions() {
|
||||
const nameFilter = this.getNameFilterIfNeeded();
|
||||
const search = this.input.value.slice(0, 3);
|
||||
const suggestions = await this.loadSuggestions(search, nameFilter);
|
||||
let toShow = suggestions.slice(0, 6);
|
||||
if (search.length > 0) {
|
||||
toShow = suggestions.filter(val => {
|
||||
return val.toLowerCase().includes(search);
|
||||
}).slice(0, 6);
|
||||
}
|
||||
|
||||
this.displaySuggestions(toShow);
|
||||
}
|
||||
|
||||
getNameFilterIfNeeded() {
|
||||
if (this.type !== 'value') return null;
|
||||
return this.parent.querySelector('input').value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} search
|
||||
* @param {String|null} nameFilter
|
||||
* @returns {Promise<Object|String|*>}
|
||||
*/
|
||||
async loadSuggestions(search, nameFilter = null) {
|
||||
const params = {search, name: nameFilter};
|
||||
const cacheKey = `${this.url}:${JSON.stringify(params)}`;
|
||||
|
||||
if (ajaxCache[cacheKey]) {
|
||||
return ajaxCache[cacheKey];
|
||||
}
|
||||
|
||||
const resp = await window.$http.get(this.url, params);
|
||||
ajaxCache[cacheKey] = resp.data;
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String[]} suggestions
|
||||
*/
|
||||
displaySuggestions(suggestions) {
|
||||
if (suggestions.length === 0) {
|
||||
return this.hideSuggestions();
|
||||
}
|
||||
|
||||
this.list.innerHTML = suggestions.map(value => `<li><button type="button">${escapeHtml(value)}</button></li>`).join('');
|
||||
this.list.style.display = 'block';
|
||||
for (const button of this.list.querySelectorAll('button')) {
|
||||
button.addEventListener('blur', this.hideSuggestionsIfFocusedLost.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
hideSuggestions() {
|
||||
this.list.style.display = 'none';
|
||||
}
|
||||
|
||||
hideSuggestionsIfFocusedLost(event) {
|
||||
if (!this.container.contains(event.relatedTarget)) {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AutoSuggest;
|
|
@ -45,4 +45,19 @@ export function scrollAndHighlightElement(element) {
|
|||
element.classList.remove('selectFade');
|
||||
element.style.backgroundColor = '';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape any HTML in the given 'unsafe' string.
|
||||
* Take from https://stackoverflow.com/a/6234804.
|
||||
* @param {String} unsafe
|
||||
* @returns {string}
|
||||
*/
|
||||
export function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
|
@ -2,12 +2,15 @@
|
|||
const template = `
|
||||
<div>
|
||||
<input :value="value" :autosuggest-type="type" ref="input"
|
||||
:placeholder="placeholder" :name="name"
|
||||
:placeholder="placeholder"
|
||||
:name="name"
|
||||
type="text"
|
||||
@input="inputUpdate($event.target.value)" @focus="inputUpdate($event.target.value)"
|
||||
@input="inputUpdate($event.target.value)"
|
||||
@focus="inputUpdate($event.target.value)"
|
||||
@blur="inputBlur"
|
||||
@keydown="inputKeydown"
|
||||
:aria-label="placeholder"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<ul class="suggestion-box" v-if="showSuggestions">
|
||||
<li v-for="(suggestion, i) in suggestions"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue