[autocomplete] new from-scratch autocomplete implementation
This commit is contained in:
parent
85b1e54904
commit
b55b5a0a0f
10 changed files with 222 additions and 345 deletions
7
ext/autocomplete/lib/jquery-ui.min.css
vendored
7
ext/autocomplete/lib/jquery-ui.min.css
vendored
File diff suppressed because one or more lines are too long
13
ext/autocomplete/lib/jquery-ui.min.js
vendored
13
ext/autocomplete/lib/jquery-ui.min.js
vendored
File diff suppressed because one or more lines are too long
5
ext/autocomplete/lib/jquery-ui.theme.min.css
vendored
5
ext/autocomplete/lib/jquery-ui.theme.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -1,69 +0,0 @@
|
|||
ul.tagit {
|
||||
padding: 1px 5px;
|
||||
overflow: auto;
|
||||
margin-left: inherit; /* usually we don't want the regular ul margins. */
|
||||
margin-right: inherit;
|
||||
}
|
||||
ul.tagit li {
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 2px 5px 2px 0;
|
||||
}
|
||||
ul.tagit li.tagit-choice {
|
||||
position: relative;
|
||||
line-height: inherit;
|
||||
}
|
||||
input.tagit-hidden-field {
|
||||
display: none;
|
||||
}
|
||||
ul.tagit li.tagit-choice-read-only {
|
||||
padding: .2em .5em .2em .5em;
|
||||
}
|
||||
|
||||
ul.tagit li.tagit-choice-editable {
|
||||
padding: .2em 18px .2em .5em;
|
||||
}
|
||||
|
||||
ul.tagit li.tagit-new {
|
||||
padding: .25em 4px .25em 0;
|
||||
}
|
||||
|
||||
ul.tagit li.tagit-choice a.tagit-label {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.tagit li.tagit-choice .tagit-close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: .1em;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
/* used for some custom themes that don't need image icons */
|
||||
ul.tagit li.tagit-choice .tagit-close .text-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
ul.tagit li.tagit-choice input {
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 2px 5px 2px 0;
|
||||
}
|
||||
ul.tagit input[type="text"] {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
-moz-box-shadow: none;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: inherit;
|
||||
background-color: inherit;
|
||||
outline: none;
|
||||
}
|
18
ext/autocomplete/lib/tag-it.min.js
vendored
18
ext/autocomplete/lib/tag-it.min.js
vendored
|
@ -1,18 +0,0 @@
|
|||
//Removed TAB keybind
|
||||
;(function(b){b.widget("ui.tagit",{options:{allowDuplicates:!1,caseSensitive:!0,fieldName:"tags",placeholderText:null,readOnly:!1,removeConfirmation:!1,tagLimit:null,availableTags:[],autocomplete:{},showAutocompleteOnFocus:!1,allowSpaces:!1,singleField:!1,singleFieldDelimiter:",",singleFieldNode:null,animate:!0,tabIndex:null,beforeTagAdded:null,afterTagAdded:null,beforeTagRemoved:null,afterTagRemoved:null,onTagClicked:null,onTagLimitExceeded:null,onTagAdded:null,onTagRemoved:null,tagSource:null},_create:function(){var a=
|
||||
this;this.element.is("input")?(this.tagList=b("<ul></ul>").insertAfter(this.element),this.options.singleField=!0,this.options.singleFieldNode=this.element,this.element.addClass("tagit-hidden-field")):this.tagList=this.element.find("ul, ol").andSelf().last();this.tagInput=b('<input type="text" />').addClass("ui-widget-content");this.options.readOnly&&this.tagInput.attr("disabled","disabled");this.options.tabIndex&&this.tagInput.attr("tabindex",this.options.tabIndex);this.options.placeholderText&&this.tagInput.attr("placeholder",
|
||||
this.options.placeholderText);this.options.autocomplete.source||(this.options.autocomplete.source=function(a,e){var d=a.term.toLowerCase(),c=b.grep(this.options.availableTags,function(a){return 0===a.toLowerCase().indexOf(d)});this.options.allowDuplicates||(c=this._subtractArray(c,this.assignedTags()));e(c)});this.options.showAutocompleteOnFocus&&(this.tagInput.focus(function(b,d){a._showAutocomplete()}),"undefined"===typeof this.options.autocomplete.minLength&&(this.options.autocomplete.minLength=
|
||||
0));b.isFunction(this.options.autocomplete.source)&&(this.options.autocomplete.source=b.proxy(this.options.autocomplete.source,this));b.isFunction(this.options.tagSource)&&(this.options.tagSource=b.proxy(this.options.tagSource,this));this.tagList.addClass("tagit").addClass("ui-widget ui-widget-content ui-corner-all").append(b('<li class="tagit-new"></li>').append(this.tagInput)).click(function(d){var c=b(d.target);c.hasClass("tagit-label")?(c=c.closest(".tagit-choice"),c.hasClass("removed")||a._trigger("onTagClicked",
|
||||
d,{tag:c,tagLabel:a.tagLabel(c)})):a.tagInput.focus()});var c=!1;if(this.options.singleField)if(this.options.singleFieldNode){var d=b(this.options.singleFieldNode),f=d.val().split(this.options.singleFieldDelimiter);d.val("");b.each(f,function(b,d){a.createTag(d,null,!0);c=!0})}else this.options.singleFieldNode=b('<input type="hidden" style="display:none;" value="" name="'+this.options.fieldName+'" />'),this.tagList.after(this.options.singleFieldNode);c||this.tagList.children("li").each(function(){b(this).hasClass("tagit-new")||
|
||||
(a.createTag(b(this).text(),b(this).attr("class"),!0),b(this).remove())});this.tagInput.keydown(function(c){if(c.which==b.ui.keyCode.BACKSPACE&&""===a.tagInput.val()){var d=a._lastTag();!a.options.removeConfirmation||d.hasClass("remove")?a.removeTag(d):a.options.removeConfirmation&&d.addClass("remove ui-state-highlight")}else a.options.removeConfirmation&&a._lastTag().removeClass("remove ui-state-highlight");if(c.which===b.ui.keyCode.COMMA&&!1===c.shiftKey||c.which===b.ui.keyCode.ENTER||c.which==
|
||||
c.which==b.ui.keyCode.SPACE&&!0!==a.options.allowSpaces&&('"'!=b.trim(a.tagInput.val()).replace(/^s*/,"").charAt(0)||'"'==b.trim(a.tagInput.val()).charAt(0)&&'"'==b.trim(a.tagInput.val()).charAt(b.trim(a.tagInput.val()).length-1)&&0!==b.trim(a.tagInput.val()).length-1))c.which===b.ui.keyCode.ENTER&&""===a.tagInput.val()||c.preventDefault(),a.options.autocomplete.autoFocus&&a.tagInput.data("autocomplete-open")||(a.tagInput.autocomplete("close"),a.createTag(a._cleanedInput()))}).blur(function(b){a.tagInput.data("autocomplete-open")||
|
||||
a.createTag(a._cleanedInput())});if(this.options.availableTags||this.options.tagSource||this.options.autocomplete.source)d={select:function(b,c){a.createTag(c.item.value);return!1}},b.extend(d,this.options.autocomplete),d.source=this.options.tagSource||d.source,this.tagInput.autocomplete(d).bind("autocompleteopen.tagit",function(b,c){a.tagInput.data("autocomplete-open",!0)}).bind("autocompleteclose.tagit",function(b,c){a.tagInput.data("autocomplete-open",!1)}),this.tagInput.autocomplete("widget").addClass("tagit-autocomplete")},
|
||||
destroy:function(){b.Widget.prototype.destroy.call(this);this.element.unbind(".tagit");this.tagList.unbind(".tagit");this.tagInput.removeData("autocomplete-open");this.tagList.removeClass("tagit ui-widget ui-widget-content ui-corner-all tagit-hidden-field");this.element.is("input")?(this.element.removeClass("tagit-hidden-field"),this.tagList.remove()):(this.element.children("li").each(function(){b(this).hasClass("tagit-new")?b(this).remove():(b(this).removeClass("tagit-choice ui-widget-content ui-state-default ui-state-highlight ui-corner-all remove tagit-choice-editable tagit-choice-read-only"),
|
||||
b(this).text(b(this).children(".tagit-label").text()))}),this.singleFieldNode&&this.singleFieldNode.remove());return this},_cleanedInput:function(){return b.trim(this.tagInput.val().replace(/^"(.*)"$/,"$1"))},_lastTag:function(){return this.tagList.find(".tagit-choice:last:not(.removed)")},_tags:function(){return this.tagList.find(".tagit-choice:not(.removed)")},assignedTags:function(){var a=this,c=[];this.options.singleField?(c=b(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter),
|
||||
""===c[0]&&(c=[])):this._tags().each(function(){c.push(a.tagLabel(this))});return c},_updateSingleTagsField:function(a){b(this.options.singleFieldNode).val(a.join(this.options.singleFieldDelimiter)).trigger("change")},_subtractArray:function(a,c){for(var d=[],f=0;f<a.length;f++)-1==b.inArray(a[f],c)&&d.push(a[f]);return d},tagLabel:function(a){return this.options.singleField?b(a).find(".tagit-label:first").text():b(a).find("input:first").val()},_showAutocomplete:function(){this.tagInput.autocomplete("search",
|
||||
"")},_findTagByLabel:function(a){var c=this,d=null;this._tags().each(function(f){if(c._formatStr(a)==c._formatStr(c.tagLabel(this)))return d=b(this),!1});return d},_isNew:function(a){return!this._findTagByLabel(a)},_formatStr:function(a){return this.options.caseSensitive?a:b.trim(a.toLowerCase())},_effectExists:function(a){return Boolean(b.effects&&(b.effects[a]||b.effects.effect&&b.effects.effect[a]))},createTag:function(a,c,d){var f=this;a=b.trim(a);this.options.preprocessTag&&(a=this.options.preprocessTag(a));
|
||||
if(""===a)return!1;if(!this.options.allowDuplicates&&!this._isNew(a))return a=this._findTagByLabel(a),!1!==this._trigger("onTagExists",null,{existingTag:a,duringInitialization:d})&&this._effectExists("highlight")&&a.effect("highlight"),!1;if(this.options.tagLimit&&this._tags().length>=this.options.tagLimit)return this._trigger("onTagLimitExceeded",null,{duringInitialization:d}),!1;var g=b(this.options.onTagClicked?'<a class="tagit-label"></a>':'<span class="tagit-label"></span>').text(a),e=b("<li></li>").addClass("tagit-choice ui-widget-content ui-state-default ui-corner-all").addClass(c).append(g);
|
||||
this.options.readOnly?e.addClass("tagit-choice-read-only"):(e.addClass("tagit-choice-editable"),c=b("<span></span>").addClass("ui-icon ui-icon-close"),c=b('<a><span class="text-icon">\u00d7</span></a>').addClass("tagit-close").append(c).click(function(a){f.removeTag(e)}),e.append(c));this.options.singleField||(g=g.html(),e.append('<input type="hidden" value="'+g+'" name="'+this.options.fieldName+'" class="tagit-hidden-field" />'));!1!==this._trigger("beforeTagAdded",null,{tag:e,tagLabel:this.tagLabel(e),
|
||||
duringInitialization:d})&&(this.options.singleField&&(g=this.assignedTags(),g.push(a),this._updateSingleTagsField(g)),this._trigger("onTagAdded",null,e),this.tagInput.val(""),this.tagInput.parent().before(e),this._trigger("afterTagAdded",null,{tag:e,tagLabel:this.tagLabel(e),duringInitialization:d}),this.options.showAutocompleteOnFocus&&!d&&setTimeout(function(){f._showAutocomplete()},0))},removeTag:function(a,c){c="undefined"===typeof c?this.options.animate:c;a=b(a);this._trigger("onTagRemoved",
|
||||
null,a);if(!1!==this._trigger("beforeTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})){if(this.options.singleField){var d=this.assignedTags(),f=this.tagLabel(a),d=b.grep(d,function(a){return a!=f});this._updateSingleTagsField(d)}if(c){a.addClass("removed");var d=this._effectExists("blind")?["blind",{direction:"horizontal"},"fast"]:["fast"],g=this;d.push(function(){a.remove();g._trigger("afterTagRemoved",null,{tag:a,tagLabel:g.tagLabel(a)})});a.fadeOut("fast").hide.apply(a,d).dequeue()}else a.remove(),
|
||||
this._trigger("afterTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})}},removeTagByLabel:function(a,b){var d=this._findTagByLabel(a);if(!d)throw"No such tag exists with the name '"+a+"'";this.removeTag(d,b)},removeAll:function(){var a=this;this._tags().each(function(b,d){a.removeTag(d,!1)})}})})(jQuery);
|
|
@ -1,97 +0,0 @@
|
|||
|
||||
/* Optional scoped theme for tag-it which mimics the zendesk widget. */
|
||||
|
||||
|
||||
ul.tagit {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: #C6C6C6;
|
||||
background: inherit;
|
||||
}
|
||||
ul.tagit li.tagit-choice {
|
||||
-moz-border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
-webkit-border-radius: 6px;
|
||||
border: 1px solid #CAD8F3;
|
||||
|
||||
background: #DEE7F8 none;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
ul.tagit li.tagit-choice .tagit-label:not(a) {
|
||||
color: #555;
|
||||
}
|
||||
ul.tagit li.tagit-choice a.tagit-close {
|
||||
text-decoration: none;
|
||||
}
|
||||
ul.tagit li.tagit-choice .tagit-close {
|
||||
right: .4em;
|
||||
}
|
||||
ul.tagit li.tagit-choice .ui-icon {
|
||||
display: none;
|
||||
}
|
||||
ul.tagit li.tagit-choice .tagit-close .text-icon {
|
||||
display: inline;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: #777;
|
||||
}
|
||||
ul.tagit li.tagit-choice:hover, ul.tagit li.tagit-choice.remove {
|
||||
background-color: #bbcef1;
|
||||
border-color: #6d95e0;
|
||||
}
|
||||
ul.tagit li.tagit-choice a.tagLabel:hover,
|
||||
ul.tagit li.tagit-choice a.tagit-close .text-icon:hover {
|
||||
color: #222;
|
||||
}
|
||||
ul.tagit input[type="text"] {
|
||||
color: #333333;
|
||||
background: none;
|
||||
}
|
||||
.ui-widget {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/* Forked from a jQuery UI theme, so that we don't require the jQuery UI CSS as a dependency. */
|
||||
.tagit-autocomplete.ui-autocomplete { position: absolute; cursor: default; }
|
||||
* html .tagit-autocomplete.ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */
|
||||
.tagit-autocomplete.ui-menu {
|
||||
list-style:none;
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
display:block;
|
||||
float: left;
|
||||
}
|
||||
.tagit-autocomplete.ui-menu .ui-menu {
|
||||
margin-top: -3px;
|
||||
}
|
||||
.tagit-autocomplete.ui-menu .ui-menu-item {
|
||||
margin:0;
|
||||
padding: 0;
|
||||
zoom: 1;
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 100%;
|
||||
}
|
||||
.tagit-autocomplete.ui-menu .ui-menu-item a {
|
||||
text-decoration:none;
|
||||
display:block;
|
||||
padding:.2em .4em;
|
||||
line-height:1.5;
|
||||
zoom:1;
|
||||
}
|
||||
.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-hover,
|
||||
.tagit-autocomplete .ui-menu .ui-menu-item a.ui-state-active {
|
||||
font-weight: normal;
|
||||
margin: -1px;
|
||||
}
|
||||
.tagit-autocomplete.ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff 50% 50% repeat-x; color: #222222; }
|
||||
.tagit-autocomplete.ui-corner-all, .tagit-autocomplete .ui-corner-all { -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; border-radius: 4px; }
|
||||
.tagit-autocomplete .ui-state-hover, .tagit-autocomplete .ui-state-focus { border: 1px solid #999999; background: #dadada; font-weight: normal; color: #212121; }
|
||||
.tagit-autocomplete .ui-state-active { border: 1px solid #aaaaaa; }
|
||||
|
||||
.tagit-autocomplete .ui-widget-content { border: 1px solid #aaaaaa; }
|
||||
.tagit .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px,1px,1px,1px); }
|
||||
|
||||
|
|
@ -6,9 +6,6 @@ namespace Shimmie2;
|
|||
|
||||
class AutoComplete extends Extension
|
||||
{
|
||||
/** @var AutoCompleteTheme */
|
||||
protected Themelet $theme;
|
||||
|
||||
public function get_priority(): int
|
||||
{
|
||||
return 30;
|
||||
|
@ -28,8 +25,6 @@ class AutoComplete extends Extension
|
|||
$page->set_mime(MimeType::JSON);
|
||||
$page->set_data(json_encode($res));
|
||||
}
|
||||
|
||||
$this->theme->build_autocomplete($page);
|
||||
}
|
||||
|
||||
private function complete(string $search, int $limit): array
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
#Navigationleft .blockbody { overflow: visible; }
|
||||
|
||||
.tagit { background: white !important; border: 1px solid grey !important; cursor: text; }
|
||||
.tagit-choice { cursor: initial; }
|
||||
.autocomplete_completions {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
border: 1px solid #ccc;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
padding: 5px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.autocomplete_completions LI {
|
||||
padding: 0.15em;
|
||||
}
|
||||
.autocomplete_completions .selected {
|
||||
background-color: #ccc;
|
||||
outline: none;
|
||||
}
|
||||
input[name=search] ~ input[type=submit] { display: inline-block !important; }
|
||||
|
||||
.tag-negative { background: #ff8080 !important; }
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shimmie2;
|
||||
|
||||
class AutoCompleteTheme extends Themelet
|
||||
{
|
||||
public function build_autocomplete(Page $page)
|
||||
{
|
||||
$base_href = get_base_href();
|
||||
// TODO: AJAX test and fallback.
|
||||
|
||||
$page->add_html_header("<script defer src='$base_href/ext/autocomplete/lib/jquery-ui.min.js' type='text/javascript'></script>");
|
||||
$page->add_html_header("<script defer src='$base_href/ext/autocomplete/lib/tag-it.min.js' type='text/javascript'></script>");
|
||||
$page->add_html_header('<link rel="stylesheet" type="text/css" href="//ajax.googleapis.com/ajax/libs/jqueryui/1/themes/flick/jquery-ui.css">');
|
||||
$page->add_html_header("<link rel='stylesheet' type='text/css' href='$base_href/ext/autocomplete/lib/jquery.tagit.css' />");
|
||||
}
|
||||
}
|
Reference in a new issue