mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
Support search field autocompletion, enabled autocomplete for header
This commit is contained in:
parent
c0ddf55b48
commit
8c988b002d
2 changed files with 67 additions and 6 deletions
|
@ -4,9 +4,19 @@
|
||||||
|
|
||||||
import { LocalAutocompleter } from './utils/local-autocompleter';
|
import { LocalAutocompleter } from './utils/local-autocompleter';
|
||||||
import { handleError } from './utils/requests';
|
import { handleError } from './utils/requests';
|
||||||
|
import { getTermContexts } from "./match_query";
|
||||||
|
|
||||||
const cache = {};
|
const cache = {};
|
||||||
let inputField, originalTerm;
|
/** @type {HTMLInputElement} */
|
||||||
|
let inputField,
|
||||||
|
/** @type {string} */
|
||||||
|
originalTerm,
|
||||||
|
/** @type {string} */
|
||||||
|
originalQuery,
|
||||||
|
/** @type {TermContext[]} */
|
||||||
|
searchTokens,
|
||||||
|
/** @type {TermContext} */
|
||||||
|
selectedTerm;
|
||||||
|
|
||||||
function removeParent() {
|
function removeParent() {
|
||||||
const parent = document.querySelector('.autocomplete');
|
const parent = document.querySelector('.autocomplete');
|
||||||
|
@ -18,13 +28,37 @@ function removeSelected() {
|
||||||
if (selected) selected.classList.remove('autocomplete__item--selected');
|
if (selected) selected.classList.remove('autocomplete__item--selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSearchField() {
|
||||||
|
return inputField && inputField.name === 'q';
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOriginalValue() {
|
||||||
|
inputField.value = isSearchField() ? originalQuery : originalTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySelectedValue(selection) {
|
||||||
|
if (!isSearchField()) {
|
||||||
|
inputField.value = selection;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedTerm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [startIndex, endIndex] = selectedTerm[0];
|
||||||
|
inputField.value = originalQuery.slice(0, startIndex) + selection + originalQuery.slice(endIndex);
|
||||||
|
inputField.setSelectionRange(startIndex + selection.length, startIndex + selection.length);
|
||||||
|
inputField.focus();
|
||||||
|
}
|
||||||
|
|
||||||
function changeSelected(firstOrLast, current, sibling) {
|
function changeSelected(firstOrLast, current, sibling) {
|
||||||
if (current && sibling) { // if the currently selected item has a sibling, move selection to it
|
if (current && sibling) { // if the currently selected item has a sibling, move selection to it
|
||||||
current.classList.remove('autocomplete__item--selected');
|
current.classList.remove('autocomplete__item--selected');
|
||||||
sibling.classList.add('autocomplete__item--selected');
|
sibling.classList.add('autocomplete__item--selected');
|
||||||
}
|
}
|
||||||
else if (current) { // if the next keypress will take the user outside the list, restore the unautocompleted term
|
else if (current) { // if the next keypress will take the user outside the list, restore the unautocompleted term
|
||||||
inputField.value = originalTerm;
|
restoreOriginalValue();
|
||||||
removeSelected();
|
removeSelected();
|
||||||
}
|
}
|
||||||
else if (firstOrLast) { // if no item in the list is selected, select the first or last
|
else if (firstOrLast) { // if no item in the list is selected, select the first or last
|
||||||
|
@ -37,12 +71,15 @@ function keydownHandler(event) {
|
||||||
firstItem = document.querySelector('.autocomplete__item:first-of-type'),
|
firstItem = document.querySelector('.autocomplete__item:first-of-type'),
|
||||||
lastItem = document.querySelector('.autocomplete__item:last-of-type');
|
lastItem = document.querySelector('.autocomplete__item:last-of-type');
|
||||||
|
|
||||||
|
// Prevent submission of the search field when Enter was hit
|
||||||
|
if (event.keyCode === 13 && isSearchField() && selected) event.preventDefault(); // Enter
|
||||||
|
|
||||||
if (event.keyCode === 38) changeSelected(lastItem, selected, selected && selected.previousSibling); // ArrowUp
|
if (event.keyCode === 38) changeSelected(lastItem, selected, selected && selected.previousSibling); // ArrowUp
|
||||||
if (event.keyCode === 40) changeSelected(firstItem, selected, selected && selected.nextSibling); // ArrowDown
|
if (event.keyCode === 40) changeSelected(firstItem, selected, selected && selected.nextSibling); // ArrowDown
|
||||||
if (event.keyCode === 13 || event.keyCode === 27 || event.keyCode === 188) removeParent(); // Enter || Esc || Comma
|
if (event.keyCode === 13 || event.keyCode === 27 || event.keyCode === 188) removeParent(); // Enter || Esc || Comma
|
||||||
if (event.keyCode === 38 || event.keyCode === 40) { // ArrowUp || ArrowDown
|
if (event.keyCode === 38 || event.keyCode === 40) { // ArrowUp || ArrowDown
|
||||||
const newSelected = document.querySelector('.autocomplete__item--selected');
|
const newSelected = document.querySelector('.autocomplete__item--selected');
|
||||||
if (newSelected) inputField.value = newSelected.dataset.value;
|
if (newSelected) applySelectedValue(newSelected.dataset.value);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +101,7 @@ function createItem(list, suggestion) {
|
||||||
});
|
});
|
||||||
|
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
inputField.value = item.dataset.value;
|
applySelectedValue(item.dataset.value);
|
||||||
inputField.dispatchEvent(
|
inputField.dispatchEvent(
|
||||||
new CustomEvent('autocomplete', {
|
new CustomEvent('autocomplete', {
|
||||||
detail: {
|
detail: {
|
||||||
|
@ -122,6 +159,17 @@ function getSuggestions(term) {
|
||||||
return fetch(`${inputField.dataset.acSource}${term}`).then(response => response.json());
|
return fetch(`${inputField.dataset.acSource}${term}`).then(response => response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelectedTerm() {
|
||||||
|
if (!inputField || !originalQuery) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectionIndex = Math.min(inputField.selectionStart, inputField.selectionEnd);
|
||||||
|
const terms = getTermContexts(originalQuery);
|
||||||
|
|
||||||
|
return terms.find(([range]) => range[0] < selectionIndex && range[1] >= selectionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
function listenAutocomplete() {
|
function listenAutocomplete() {
|
||||||
let timeout;
|
let timeout;
|
||||||
|
|
||||||
|
@ -138,7 +186,20 @@ function listenAutocomplete() {
|
||||||
|
|
||||||
if (localAc !== null && 'ac' in event.target.dataset) {
|
if (localAc !== null && 'ac' in event.target.dataset) {
|
||||||
inputField = event.target;
|
inputField = event.target;
|
||||||
originalTerm = `${inputField.value}`.toLowerCase();
|
|
||||||
|
if (isSearchField()) {
|
||||||
|
originalQuery = inputField.value;
|
||||||
|
selectedTerm = getSelectedTerm();
|
||||||
|
|
||||||
|
// We don't need to run auto-completion if user is not selecting tag at all
|
||||||
|
if (!selectedTerm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
originalTerm = selectedTerm[1];
|
||||||
|
} else {
|
||||||
|
originalTerm = `${inputField.value}`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
const suggestions = localAc.topK(originalTerm, 5).map(({ name, imageCount }) => ({ label: `${name} (${imageCount})`, value: name }));
|
const suggestions = localAc.topK(originalTerm, 5).map(({ name, imageCount }) => ({ label: `${name} (${imageCount})`, value: name }));
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ header.header
|
||||||
i.fa.fa-upload
|
i.fa.fa-upload
|
||||||
|
|
||||||
= form_for @conn, Routes.search_path(@conn, :index), [method: "get", class: "header__search flex flex--no-wrap flex--centered", enforce_utf8: false], fn f ->
|
= form_for @conn, Routes.search_path(@conn, :index), [method: "get", class: "header__search flex flex--no-wrap flex--centered", enforce_utf8: false], fn f ->
|
||||||
input.input.header__input.header__input--search#q name="q" title="For terms all required, separate with ',' or 'AND'; also supports 'OR' for optional terms and '-' or 'NOT' for negation. Search with a blank query for more options or click the ? for syntax help." value=@conn.params["q"] placeholder="Search" autocapitalize="none"
|
input.input.header__input.header__input--search#q name="q" title="For terms all required, separate with ',' or 'AND'; also supports 'OR' for optional terms and '-' or 'NOT' for negation. Search with a blank query for more options or click the ? for syntax help." value=@conn.params["q"] placeholder="Search" autocapitalize="none" autocomplete="off" data-ac="true" data-ac-min-length="3" data-ac-source="/autocomplete/tags?term="
|
||||||
|
|
||||||
= if present?(@conn.params["sf"]) do
|
= if present?(@conn.params["sf"]) do
|
||||||
input type="hidden" name="sf" value=@conn.params["sf"]
|
input type="hidden" name="sf" value=@conn.params["sf"]
|
||||||
|
|
Loading…
Reference in a new issue