[autocomplete] new from-scratch autocomplete implementation
This commit is contained in:
@@ -1,115 +1,211 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
var metatags = ['order:id', 'order:width', 'order:height', 'order:filesize', 'order:filename', 'order:favorites'];
|
||||
let completions_el = document.createElement('ul');
|
||||
completions_el.className = 'autocomplete_completions';
|
||||
completions_el.id = 'completions';
|
||||
|
||||
$('.autocomplete_tags').tagit({
|
||||
singleFieldDelimiter: ' ',
|
||||
beforeTagAdded: function(event, ui) {
|
||||
if(metatags.indexOf(ui.tagLabel) !== -1) {
|
||||
ui.tag.addClass('tag-metatag');
|
||||
} else {
|
||||
console.log(ui.tagLabel);
|
||||
// give special class to negative tags
|
||||
if(ui.tagLabel[0] === '-') {
|
||||
ui.tag.addClass('tag-negative');
|
||||
}else{
|
||||
ui.tag.addClass('tag-positive');
|
||||
}
|
||||
}
|
||||
},
|
||||
autocomplete : ({
|
||||
source: function (request, response) {
|
||||
var ac_metatags = $.map(
|
||||
$.grep(metatags, function(s) {
|
||||
// Only show metatags for strings longer than one character
|
||||
return (request.term.length > 1 && s.indexOf(request.term) === 0);
|
||||
}),
|
||||
function(item) {
|
||||
return {
|
||||
label : item + ' [metatag]',
|
||||
value : item
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
var isNegative = (request.term[0] === '-');
|
||||
$.ajax({
|
||||
url: base_href + '/api/internal/autocomplete',
|
||||
data: {'s': (isNegative ? request.term.substring(1) : request.term)},
|
||||
dataType : 'json',
|
||||
type : 'GET',
|
||||
success : function (data) {
|
||||
response(
|
||||
$.merge(ac_metatags,
|
||||
$.map(data, function (count, item) {
|
||||
item = (isNegative ? '-'+item : item);
|
||||
return {
|
||||
label : item + ' ('+count+')',
|
||||
value : item
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
error : function (request, status, error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
minLength: 1
|
||||
})
|
||||
});
|
||||
|
||||
$('#tag_editor,[name="bulk_tags"]').tagit({
|
||||
singleFieldDelimiter: ' ',
|
||||
autocomplete : ({
|
||||
source: function (request, response) {
|
||||
$.ajax({
|
||||
url: base_href + '/api/internal/autocomplete',
|
||||
data: {'s': request.term},
|
||||
dataType : 'json',
|
||||
type : 'GET',
|
||||
success : function (data) {
|
||||
response(
|
||||
$.map(data, function (count, item) {
|
||||
return {
|
||||
label : item + ' ('+count+')',
|
||||
value : item
|
||||
};
|
||||
})
|
||||
);
|
||||
},
|
||||
error : function (request, status, error) {
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
minLength: 1
|
||||
})
|
||||
});
|
||||
|
||||
$('.ui-autocomplete-input').keydown(function(e) {
|
||||
var keyCode = e.keyCode || e.which;
|
||||
|
||||
//Stop tags containing space.
|
||||
if(keyCode === 32) {
|
||||
e.preventDefault();
|
||||
var el = $('.ui-widget-content:focus');
|
||||
|
||||
//Find the correct element in a page with multiple tagit input boxes.
|
||||
$('.autocomplete_tags').each(function(_,n){
|
||||
if (n.parentNode.contains(el[0])){
|
||||
$(n.parentNode).find('.autocomplete_tags').tagit('createTag', el.val());
|
||||
/**
|
||||
* Whenever input changes, look at what word is currently
|
||||
* being typed, and fetch completions for it.
|
||||
*
|
||||
* @param {HTMLInputElement} element
|
||||
*/
|
||||
function updateCompletions(element) {
|
||||
highlightCompletion(element, -1);
|
||||
|
||||
let text = element.value;
|
||||
let pos = element.selectionStart;
|
||||
|
||||
// get the word before the cursor
|
||||
var start = text.lastIndexOf(' ', pos-1);
|
||||
if(start === -1) {
|
||||
start = 0;
|
||||
}
|
||||
else {
|
||||
start++; // skip the space
|
||||
}
|
||||
var word = text.substring(start, pos);
|
||||
|
||||
// search for completions
|
||||
if(element.completer_timeout !== null) {
|
||||
clearTimeout(element.completer_timeout);
|
||||
element.completer_timeout = null;
|
||||
}
|
||||
if(word === '') {
|
||||
element.completions = {};
|
||||
renderCompletions(element);
|
||||
}
|
||||
else {
|
||||
element.completer_timeout = setTimeout(() => {
|
||||
fetch(base_href + '/api/internal/autocomplete?s=' + word).then(
|
||||
(response) => response.json()
|
||||
).then((json) => {
|
||||
if(element.selected_completion !== -1) {
|
||||
return; // user has started to navigate the completions, so don't update them
|
||||
}
|
||||
element.completions = json;
|
||||
renderCompletions(element);
|
||||
});
|
||||
$(this).autocomplete('close');
|
||||
} else if (keyCode === 9) {
|
||||
e.preventDefault();
|
||||
}, 250);
|
||||
renderCompletions(element);
|
||||
}
|
||||
}
|
||||
|
||||
var tag = $('.tagit-autocomplete[style*=\"display: block\"] > li:focus, .tagit-autocomplete[style*=\"display: block\"] > li:first').first();
|
||||
if(tag.length){
|
||||
$(tag).click();
|
||||
$('.ui-autocomplete-input').val(''); //If tag already exists, make sure to remove duplicate.
|
||||
}
|
||||
/**
|
||||
* Highlight the nth completion
|
||||
*
|
||||
* @param {HTMLInputElement} element
|
||||
* @param {number} n
|
||||
*/
|
||||
function highlightCompletion(element, n) {
|
||||
if(!element.completions) return;
|
||||
element.selected_completion = Math.min(
|
||||
Math.max(n, -1),
|
||||
Object.keys(element.completions).length-1
|
||||
);
|
||||
renderCompletions(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the completion block
|
||||
*
|
||||
* @param {HTMLInputElement} element
|
||||
*/
|
||||
function renderCompletions(element) {
|
||||
let completions = element.completions;
|
||||
let selected_completion = element.selected_completion;
|
||||
|
||||
// if there are no completions, remove the completion block
|
||||
if(Object.keys(completions).length === 0) {
|
||||
completions_el.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// remove all children
|
||||
while(completions_el.firstChild) {
|
||||
completions_el.removeChild(completions_el.firstChild);
|
||||
}
|
||||
|
||||
// add children for each completion, with the selected one highlighted
|
||||
Object.keys(completions).forEach((key, i) => {
|
||||
let value = completions[key];
|
||||
|
||||
let li = document.createElement('li');
|
||||
li.innerHTML = key + ' (' + value + ')';
|
||||
if(i === selected_completion) {
|
||||
li.className = 'selected';
|
||||
}
|
||||
// on hover, select the completion
|
||||
li.addEventListener('mouseover', () => {
|
||||
highlightCompletion(element, i);
|
||||
});
|
||||
// on click, set the completion
|
||||
// (mousedown is used instead of click because click is
|
||||
// fired after blur, which causes the completion block to
|
||||
// be removed before the click event is handled)
|
||||
li.addEventListener('mousedown', () => {
|
||||
setCompletion(element, key);
|
||||
});
|
||||
completions_el.appendChild(li);
|
||||
});
|
||||
|
||||
// insert the completion block after the element
|
||||
if(element.parentNode) {
|
||||
element.parentNode.insertBefore(completions_el, element.nextSibling);
|
||||
completions_el.style.width = element.clientWidth + 'px';
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Set the current word to the given completion
|
||||
*
|
||||
* @param {HTMLInputElement} element
|
||||
* @param {string} new_word
|
||||
*/
|
||||
function setCompletion(element, new_word) {
|
||||
let text = element.value;
|
||||
let pos = element.selectionStart;
|
||||
|
||||
// get the word before the cursor
|
||||
var start = text.lastIndexOf(' ', pos-1);
|
||||
if(start === -1) {
|
||||
start = 0;
|
||||
}
|
||||
else {
|
||||
start++; // skip the space
|
||||
}
|
||||
var end = text.indexOf(' ', pos);
|
||||
if(end === -1) {
|
||||
end = text.length;
|
||||
}
|
||||
|
||||
// replace the word with the completion
|
||||
new_word += ' ';
|
||||
element.value = text.substring(0, start) + new_word + text.substring(end);
|
||||
element.selectionStart = start + new_word.length;
|
||||
element.selectionEnd = start + new_word.length;
|
||||
|
||||
// reset metadata
|
||||
element.completions = {};
|
||||
element.selected_completion = -1;
|
||||
if(element.completer_timeout) {
|
||||
clearTimeout(element.completer_timeout);
|
||||
element.completer_timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Find all elements with class 'autocomplete_tags'
|
||||
document.querySelectorAll('.autocomplete_tags').forEach((element) => {
|
||||
// set metadata
|
||||
element.completions = {};
|
||||
element.selected_completion = -1;
|
||||
element.completer_timeout = null;
|
||||
|
||||
// disable built-in autocomplete
|
||||
element.setAttribute('autocomplete', 'off');
|
||||
|
||||
// when element is focused, add completion block
|
||||
element.addEventListener('focus', () => {
|
||||
updateCompletions(element);
|
||||
});
|
||||
|
||||
// when element is blurred, remove completion block
|
||||
element.addEventListener('blur', () => {
|
||||
// if we are blurring because we are clicking on a completion,
|
||||
// don't remove the completion block until the click event is done
|
||||
completions_el.remove();
|
||||
});
|
||||
|
||||
// when cursor is moved, change current completion
|
||||
document.addEventListener('selectionchange', () => {
|
||||
// if element is focused
|
||||
if(document.activeElement === element) {
|
||||
updateCompletions(element);
|
||||
}
|
||||
});
|
||||
|
||||
element.addEventListener('keydown', (event) => {
|
||||
// up / down should select previous / next completion
|
||||
if(event.code === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
highlightCompletion(element, element.selected_completion-1);
|
||||
}
|
||||
if(event.code === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
highlightCompletion(element, element.selected_completion+1);
|
||||
}
|
||||
// if enter is pressed, add the selected completion
|
||||
if(event.code === "Enter" && element.selected_completion !== -1) {
|
||||
event.preventDefault();
|
||||
setCompletion(element, Object.keys(element.completions)[element.selected_completion]);
|
||||
}
|
||||
// if escape is pressed, hide the completion block
|
||||
if(event.code === "Escape") {
|
||||
completions_el.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// on change, update completions
|
||||
element.addEventListener('input', () => {
|
||||
updateCompletions(element);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user