From 889302cd36133ee85b374388038a3d8e5420db65 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 15 Feb 2021 11:55:08 +0100 Subject: [PATCH 1/3] Modernise AutoSuggest and TagAutoSuggest classes. --- public/js/autosuggest.js | 284 +++++++++++++++++++-------------------- 1 file changed, 137 insertions(+), 147 deletions(-) diff --git a/public/js/autosuggest.js b/public/js/autosuggest.js index 066ea1b7..22a36aa2 100644 --- a/public/js/autosuggest.js +++ b/public/js/autosuggest.js @@ -13,164 +13,154 @@ provided that the following conditions are met: 'use strict'; -function AutoSuggest(opt) { - if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" || typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") { - return; +class AutoSuggest { + constructor(opt) { + if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" || + typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") { + return; + } + + this.input = document.getElementById(opt.inputElement); + this.input.autocomplete = "off"; + this.list = document.getElementById(opt.listElement); + this.appendCallback = opt.appendCallback; + this.baseurl = opt.baseUrl; + + this.input.addEventListener('keydown', event => this.doSelection(event), false); + this.input.addEventListener('keyup', event => this.onType(event), false); } - this.input = document.getElementById(opt.inputElement); - this.input.autocomplete = "off"; - this.list = document.getElementById(opt.listElement); - this.appendCallback = opt.appendCallback; - this.baseurl = opt.baseUrl; + doSelection(event) { + if (typeof this.container === "undefined" || this.container.children.length === 0) { + return; + } - var self = this; - this.input.addEventListener('keydown', function(event) { - self.doSelection(event); - }, false); - this.input.addEventListener('keyup', function(event) { - self.onType(this, event); - }, false); + switch (event.keyCode) { + case 13: // Enter + event.preventDefault(); + this.container.children[this.selectedIndex].click(); + break; + + case 38: // Arrow up + case 40: // Arrow down + event.preventDefault(); + this.findSelectedElement().className = ''; + this.selectedIndex += event.keyCode === 38 ? -1 : 1; + if (this.selectedIndex < 0) { + this.selectedIndex = this.container.children.length - 1; + } else if (this.selectedIndex === this.container.children.length) { + this.selectedIndex = 0; + } + let new_el = this.findSelectedElement().className = 'selected'; + break; + } + }; + + findSelectedElement() { + return this.container.children[this.selectedIndex]; + }; + + onType(event) { + if (event.keyCode === 13 || event.keyCode === 38 || event.keyCode === 40) { + return; + } + + let tokens = event.target.value.split(/\s+/).filter(token => token.length >= 2); + + if (tokens.length === 0) { + if (typeof this.container !== "undefined") { + this.clearContainer(); + } + return false; + } + + let request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" ")); + let request = new HttpRequest('get', request_uri, {}, this.onReceive, this); + }; + + onReceive(response, self) { + self.openContainer(); + self.clearContainer(); + self.fillContainer(response); + }; + + openContainer() { + if (this.container) { + if (!this.container.parentNode) { + this.input.parentNode.appendChild(this.container); + } + return this.container; + } + + this.container = document.createElement('ul'); + this.container.className = 'autosuggest'; + this.input.parentNode.appendChild(this.container); + return this.container; + }; + + clearContainer() { + while (this.container.children.length > 0) { + this.container.removeChild(this.container.children[0]); + } + }; + + clearInput() { + this.input.value = ""; + this.input.focus(); + }; + + closeContainer() { + this.container.parentNode.removeChild(this.container); + }; + + fillContainer(response) { + this.selectedIndex = 0; + + response.items.forEach((item, i) => { + let node = document.createElement('li'); + let text = document.createTextNode(item.label); + node.jsondata = item; + node.addEventListener('click', event => { + this.appendCallback(event.target.jsondata); + this.closeContainer(); + this.clearInput(); + }); + node.appendChild(text); + this.container.appendChild(node); + if (this.container.children.length === 1) { + node.className = 'selected'; + } + }); + }; } -AutoSuggest.prototype.doSelection = function(event) { - if (typeof this.container === "undefined" || this.container.children.length === 0) { - return; +class TagAutoSuggest extends AutoSuggest { + constructor(opt) { + super(opt); + this.type = "tags"; } - switch (event.keyCode) { - case 13: // Enter - event.preventDefault(); - this.container.children[this.selectedIndex].click(); - break; + fillContainer(response) { + if (response.items.length > 0) { + super.fillContainer.call(this, response); + } else { + let node = document.createElement('li') + node.innerHTML = "Tag does not exist yet. Create it?"; - case 38: // Arrow up - case 40: // Arrow down - event.preventDefault(); - this.findSelectedElement().className = ''; - this.selectedIndex += event.keyCode === 38 ? -1 : 1; - if (this.selectedIndex < 0) { - this.selectedIndex = this.container.children.length - 1; - } else if (this.selectedIndex === this.container.children.length) { - this.selectedIndex = 0; - } - var new_el = this.findSelectedElement().className = 'selected'; - break; - } -}; + node.addEventListener('click', event => { + this.createNewTag(response => this.appendCallback(response)); + this.closeContainer(); + this.clearInput(); + }); -AutoSuggest.prototype.findSelectedElement = function() { - return this.container.children[this.selectedIndex]; -}; - -AutoSuggest.prototype.onType = function(input, event) { - if (event.keyCode === 13 || event.keyCode === 38 || event.keyCode === 40) { - return; - } - - var tokens = input.value.split(/\s+/).filter(function(token) { - return token.length >= 2; - }); - - if (tokens.length === 0) { - if (typeof this.container !== "undefined") { - this.clearContainer(); - } - return false; - } - - var request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" ")); - var request = new HttpRequest('get', request_uri, {}, this.onReceive, this); -}; - -AutoSuggest.prototype.onReceive = function(response, self) { - self.openContainer(); - self.clearContainer(); - self.fillContainer(response); -}; - -AutoSuggest.prototype.openContainer = function() { - if (this.container) { - if (!this.container.parentNode) { - this.input.parentNode.appendChild(this.container); - } - return this.container; - } - - this.container = document.createElement('ul'); - this.container.className = 'autosuggest'; - this.input.parentNode.appendChild(this.container); - return this.container; -}; - -AutoSuggest.prototype.clearContainer = function() { - while (this.container.children.length > 0) { - this.container.removeChild(this.container.children[0]); - } -}; - -AutoSuggest.prototype.clearInput = function() { - this.input.value = ""; - this.input.focus(); -}; - -AutoSuggest.prototype.closeContainer = function() { - this.container.parentNode.removeChild(this.container); -}; - -AutoSuggest.prototype.fillContainer = function(response) { - var self = this; - this.selectedIndex = 0; - response.items.forEach(function(item, i) { - var node = document.createElement('li'); - var text = document.createTextNode(item.label); - node.jsondata = item; - node.addEventListener('click', function(event) { - self.appendCallback(this.jsondata); - self.closeContainer(); - self.clearInput(); - }); - node.appendChild(text); - self.container.appendChild(node); - if (self.container.children.length === 1) { + this.container.appendChild(node); + this.selectedIndex = 0; node.className = 'selected'; } - }); -}; + }; - -function TagAutoSuggest(opt) { - AutoSuggest.prototype.constructor.call(this, opt); - this.type = "tags"; -} - -TagAutoSuggest.prototype = Object.create(AutoSuggest.prototype); - -TagAutoSuggest.prototype.constructor = TagAutoSuggest; - -TagAutoSuggest.prototype.fillContainer = function(response) { - if (response.items.length > 0) { - AutoSuggest.prototype.fillContainer.call(this, response); - } else { - var node = document.createElement('li') - node.innerHTML = "Tag does not exist yet. Create it?"; - - var self = this; - node.addEventListener('click', function(event) { - self.createNewTag(function(response) { - self.appendCallback(response); - }); - self.closeContainer(); - self.clearInput(); - }); - - self.container.appendChild(node); - this.selectedIndex = 0; - node.className = 'selected'; + createNewTag(callback) { + let request_uri = this.baseurl + '/suggest/?type=createtag'; + let request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this); } -}; - -TagAutoSuggest.prototype.createNewTag = function(callback) { - var request_uri = this.baseurl + '/suggest/?type=createtag'; - var request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this); } From 4402521051e9816df10b98ce5f4f49399f790ed8 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Mon, 15 Feb 2021 12:10:09 +0100 Subject: [PATCH 2/3] Highlight matching string in autosuggest entries. --- public/js/autosuggest.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/public/js/autosuggest.js b/public/js/autosuggest.js index 22a36aa2..46407c4d 100644 --- a/public/js/autosuggest.js +++ b/public/js/autosuggest.js @@ -116,22 +116,33 @@ class AutoSuggest { fillContainer(response) { this.selectedIndex = 0; + let query = this.input.value.trim().replace(/[\-\[\]{}()*+?.,\\\/^\$|#]/g, ' '); + let query_tokens = query.split(/ +/).sort((a,b) => a.length - b.length); + response.items.forEach((item, i) => { let node = document.createElement('li'); - let text = document.createTextNode(item.label); + node.innerHTML = this.highlightMatches(query_tokens, item.label); node.jsondata = item; node.addEventListener('click', event => { this.appendCallback(event.target.jsondata); this.closeContainer(); this.clearInput(); }); - node.appendChild(text); this.container.appendChild(node); if (this.container.children.length === 1) { node.className = 'selected'; } }); }; + + highlightMatches(query_tokens, item) { + let itemTokens = item.split(/ +/); + let queryTokens = new RegExp('(' + query_tokens.join('\|') + ')', 'i'); + itemTokens.forEach((token, index) => { + item = item.replace(token, token.replace(queryTokens, ($1, match) => '' + match + '')); + }); + return item; + }; } class TagAutoSuggest extends AutoSuggest { From 70e6001c858e6993c6e9eb8cea0fff0845dda2ac Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Tue, 16 Feb 2021 15:24:42 +0100 Subject: [PATCH 3/3] Replace event.keyCode with event.key equivalents. --- public/js/autosuggest.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/js/autosuggest.js b/public/js/autosuggest.js index 46407c4d..76ab8208 100644 --- a/public/js/autosuggest.js +++ b/public/js/autosuggest.js @@ -35,17 +35,17 @@ class AutoSuggest { return; } - switch (event.keyCode) { - case 13: // Enter + switch (event.key) { + case 'Enter': event.preventDefault(); this.container.children[this.selectedIndex].click(); break; - case 38: // Arrow up - case 40: // Arrow down + case 'ArrowUp': + case 'ArrowDown': event.preventDefault(); this.findSelectedElement().className = ''; - this.selectedIndex += event.keyCode === 38 ? -1 : 1; + this.selectedIndex += event.key === 'ArrowUp' ? -1 : 1; if (this.selectedIndex < 0) { this.selectedIndex = this.container.children.length - 1; } else if (this.selectedIndex === this.container.children.length) { @@ -61,7 +61,7 @@ class AutoSuggest { }; onType(event) { - if (event.keyCode === 13 || event.keyCode === 38 || event.keyCode === 40) { + if (['Enter', 'ArrowDown', 'ArrowUp'].indexOf(event.key) !== -1) { return; }