From b2973ef33f1a57429a2b3c602ff2a7121bd052af Mon Sep 17 00:00:00 2001 From: Steve Date: Sat, 24 Feb 2024 21:36:19 +0100 Subject: [PATCH] chore: start v2 for more features with vanilla elegance --- src/index.html | 7 +- src/js/vanilla/kompleter.js | 307 ++++++++++++++++++++++-------------- 2 files changed, 197 insertions(+), 117 deletions(-) diff --git a/src/index.html b/src/index.html index 47a8883..e9eea30 100644 --- a/src/index.html +++ b/src/index.html @@ -51,13 +51,18 @@ }; ready(() => { kompleter.init({ + id: 'auto-complete', dataSource: 'files/kompleter.json', filterOn: 'Name', fieldsToDisplay: [ 'Name', 'CountryCode', 'Population' - ] + ], + animation: { + type: 'fadeIn', + duration: 500 + } }); }); diff --git a/src/js/vanilla/kompleter.js b/src/js/vanilla/kompleter.js index b3e2168..3709409 100644 --- a/src/js/vanilla/kompleter.js +++ b/src/js/vanilla/kompleter.js @@ -4,82 +4,65 @@ } const kompleter = { - HTMLElements: { - focused: null, - input: null, - result: null, - suggestions: [], - }, - props: { - response: {}, // Clarify / refactor the usage of response vs suggestions - pointer: -1, - previousValue: null, - }, - options: { - id: 'default-kompleter', // Todo - dataSource: '', - store: false, // Todo - animation: '', // Todo - animationSpeed: '', // Todo - begin: true, - startOnChar: 2, - maxResults: 10, - filterOn: null, - fieldsToDisplay: null, - beforeDisplayResults: (e, dataset) => {}, - afterDisplayResults: (e, dataset) => {}, - beforeFocusOnItem: (e, dataset, current) => {}, - afterFocusOnItem: (e, dataset, current) => {}, - beforeSelectItem: (e, dataset, current) => {}, - afterSelectItem: (e, dataset, current) => {}, - }, - listeners: { - onType: () => { - kompleter.HTMLElements.input.addEventListener('keyup', (e) => { - const keyCode = e.keyCode; - - if(keyCode === 38 || keyCode === 40) { // Up / down - kompleter.handlers.navigate(keyCode); - } else if (keyCode === 13) { // Enter - kompleter.handlers.select(); - } else if (kompleter.HTMLElements.input.value !== kompleter.props.previousValue) { - kompleter.handlers.suggest(); + animations: { + fadeIn: function(element, display) { + element.style.opacity = 0; + element.style.display = display || 'block'; + (function fade(){ + let value = parseFloat(element.style.opacity); + if (!((value += .1) > 1)) { + element.style.opacity = value; + requestAnimationFrame(fade); } - }); + })() }, - onHide: () => { - const body = document.getElementsByTagName('body')[0]; - body.addEventListener('click', (e) => { - kompleter.HTMLElements.result.style.display = 'none'; - }); - }, - onSelect: (className) => { - kompleter.HTMLElements.suggestions = document.getElementsByClassName(className); - if(typeof kompleter.HTMLElements.suggestions !== 'undefined') { - const numberOfSuggestions = kompleter.HTMLElements.suggestions.length; - if(numberOfSuggestions) { - for(let i = 0; i < numberOfSuggestions; i++) { - ((i) => { - return kompleter.HTMLElements.suggestions[i].addEventListener('click', (e) => { - kompleter.HTMLElements.focused = kompleter.HTMLElements.suggestions[i]; - kompleter.handlers.select(); - }); - })(i) - } + fadeOut: function(element) { + element.style.opacity = 1; + (function fade() { + if ((element.style.opacity -= .1) < 0) { + element.style.display = 'none'; + } else { + requestAnimationFrame(fade); } - } + })(); + }, + slideUp: function() { + + }, + slideDown: function() { + } }, + events: { + 'kompleter.render.result.done': new CustomEvent('kompleter.render.result.done', { + detail: {}, + bubble: true, + cancelable: false, + composed: false, + }), + 'kompleter.request.done': new CustomEvent('kompleter.request.done', { + detail: {}, + bubble: true, + cancelable: false, + composed: false, + }), + 'kompleter.result.show': new CustomEvent('kompleter.result.show', { + detail: {}, + bubble: true, + cancelable: false, + composed: false, + }) + }, handlers: { build: function (element, attributes = []) { const htmlElement = document.createElement(element); attributes.forEach(attribute => { - htmlElement.setAttribute(attribute.key, attribute.value); + htmlElement.setAttribute(Object.keys(attribute)[0], Object.values(attribute)[0]); }); return htmlElement; }, filter: function(records) { - const value = kompleter.HTMLElements.input.value.toLowerCase(); + const value = kompleter.htmlElements.input.value.toLowerCase(); return records.filter(record => { if(isNaN(value)) { return kompleter.options.begin === true ? record[kompleter.options.filterOn].toLowerCase().lastIndexOf(value, 0) === 0 : record[kompleter.options.filterOn].toLowerCase().lastIndexOf(value) !== -1; @@ -94,31 +77,32 @@ } switch (action) { case 'remove': - kompleter.HTMLElements.focused = null; - Array.from(kompleter.HTMLElements.suggestions).forEach(suggestion => { + kompleter.htmlElements.focused = null; + Array.from(kompleter.htmlElements.suggestions).forEach(suggestion => { ((suggestion) => { suggestion.className = 'item--result'; })(suggestion) }); break; case 'add': - kompleter.HTMLElements.focused = kompleter.HTMLElements.suggestions[kompleter.props.pointer]; - kompleter.HTMLElements.suggestions[kompleter.props.pointer].className += ' focus'; + kompleter.htmlElements.focused = kompleter.htmlElements.suggestions[kompleter.props.pointer]; + kompleter.htmlElements.suggestions[kompleter.props.pointer].className += ' focus'; break; } }, navigate: function (keyCode) { + // TODO fix navigation after last element is broken this.point(keyCode); this.focus('remove'); this.focus('add'); }, point: function(keyCode) { // The pointer is in the range: after or as the initial position and before the last element in suggestions - if(kompleter.props.pointer >= -1 && kompleter.props.pointer <= kompleter.HTMLElements.suggestions.length - 1) { + if(kompleter.props.pointer >= -1 && kompleter.props.pointer <= kompleter.htmlElements.suggestions.length - 1) { // Pointer in initial position, and we switch down -> up index of the pointer if(kompleter.props.pointer === -1 && keyCode === 40) { kompleter.props.pointer++; - } else if (kompleter.props.pointer === kompleter.HTMLElements.suggestions.length - 1 && keyCode === 38) { // Pointer in last position, and we switch up -> down index of the pointer + } else if (kompleter.props.pointer === kompleter.htmlElements.suggestions.length - 1 && keyCode === 38) { // Pointer in last position, and we switch up -> down index of the pointer kompleter.props.pointer--; } else if (keyCode === 38) { // Pointer in range, down index of the pointer kompleter.props.pointer--; @@ -127,58 +111,146 @@ } } }, - select: function () { - let id = null; - id = kompleter.HTMLElements.focused.id || 0; - kompleter.HTMLElements.input.value = kompleter.props.response[id][0]; - kompleter.props.pointer = -1; - kompleter.HTMLElements.result.style.display = 'none'; - }, - suggest: function () { - kompleter.HTMLElements.result.style.display = 'block'; - kompleter.props.pointer = -1; - - // TODO requestExpression should be managed somewhere else. This method is just responsible to retrieve the data. To challenge vs the cache/store - + request: function() { const headers = new Headers(); headers.append('content-type', 'application/x-www-form-urlencoded'); headers.append('method', 'GET'); - fetch(`${kompleter.options.dataSource}?'requestExpression=${kompleter.HTMLElements.input.value}`, headers) + fetch(`${kompleter.options.dataSource}?'r=${kompleter.htmlElements.input.value}`, headers) .then(result => result.json()) .then(result => { - console.log('result', result) - console.log('result', typeof result) - let text = ""; - if(result && result.length) { - kompleter.props.response = this.filter(result); - const properties = kompleter.options.fieldsToDisplay.length; - for(let i = 0; i < result.length ; i++) { - if(typeof kompleter.props.response[i] !== 'undefined') { - let cls; - i + 1 === result.length ? cls = 'last' : cls = ''; - text += '
'; - for(let j = 0; j < properties; j++) { - text += '' + kompleter.props.response[i][j] + ''; - } - text += '
'; - } - } - } else { - text = '
Not found
'; - } - - // text = '
Error
'; - - kompleter.HTMLElements.result.innerHTML = text; - kompleter.listeners.onSelect('item--result'); + kompleter.props.response = this.filter(result); + document.dispatchEvent(kompleter.events['kompleter.request.done']); + }) + .catch(e => { + console.error(e.message); + kompleter.htmlElements.result.innerHTML = '
Error
'; + kompleter.animations.fadeIn(kompleter.htmlElements.result); }); }, + select: function () { + let id = null; + id = kompleter.htmlElements.focused.id || 0; + kompleter.htmlElements.input.value = kompleter.props.response[id][0]; + kompleter.props.pointer = -1; + kompleter.htmlElements.result.style.display = 'none'; + }, validate: function(options) { // Ne valider que ce qui est donné ou requis // Le reste doit fallback sur des valeurs par défaut quand c'est possible } }, + htmlElements: { + focused: null, + input: null, + result: null, + suggestions: [], + }, + listeners: { + onHide: () => { + const body = document.getElementsByTagName('body')[0]; + body.addEventListener('click', (e) => { + kompleter.animations.fadeOut(kompleter.htmlElements.result); + }); + }, + onSelect: (className) => { + kompleter.htmlElements.suggestions = document.getElementsByClassName(className); + if(typeof kompleter.htmlElements.suggestions !== 'undefined') { + const numberOfSuggestions = kompleter.htmlElements.suggestions.length; + if(numberOfSuggestions) { + for(let i = 0; i < numberOfSuggestions; i++) { + ((i) => { + return kompleter.htmlElements.suggestions[i].addEventListener('click', (e) => { + kompleter.htmlElements.focused = kompleter.htmlElements.suggestions[i]; + kompleter.handlers.select(); + }); + })(i) + } + } + } + }, + onRequestDone: () => { + document.addEventListener('kompleter.request.done', (e) => { + kompleter.renders.results(e) + }); + }, + onRenderDone: () => { + document.addEventListener('kompleter.render.result.done', (e) => { + console.log('kompleter.render.result.done', e) + kompleter.animations.fadeIn(kompleter.htmlElements.result); + kompleter.listeners.onSelect('item--result'); + }); + }, + onShow: () => { + document.addEventListener('kompleter.result.show', (e) => { + kompleter.animations.fadeIn(kompleter.htmlElements.result); + kompleter.listeners.onSelect('item--result'); + }); + }, + onType: () => { + kompleter.htmlElements.input.addEventListener('keyup', (e) => { + const keyCode = e.keyCode; + if(keyCode === 38 || keyCode === 40) { // Up / down + kompleter.handlers.navigate(keyCode); + } else if (keyCode === 13) { // Enter + kompleter.handlers.select(); + } else if (kompleter.htmlElements.input.value !== kompleter.props.previousValue) { + kompleter.handlers.request(); + } + }); + }, + }, + options: { + id: null, + dataSource: null, + store: { + type: 'memory', // memory | indexedDB | localStorage + timelife: 50000, + }, + animation: { + type: 'fadeIn', + duration: 500 + }, + begin: true, + startOnChar: 2, + maxResults: 10, + filterOn: null, + fieldsToDisplay: null, + beforeDisplayResults: (e, dataset) => {}, + afterDisplayResults: (e, dataset) => {}, + beforeFocusOnItem: (e, dataset, current) => {}, + afterFocusOnItem: (e, dataset, current) => {}, + beforeSelectItem: (e, dataset, current) => {}, + afterSelectItem: (e, dataset, current) => {}, + }, + props: { + response: {}, // Clarify / refactor the usage of response vs suggestions + pointer: -1, + previousValue: null, + }, + renders: { + results: function(e) { + console.log('render.results', e) + let html = ''; + if(kompleter.props.response && kompleter.props.response.length) { + const properties = kompleter.options.fieldsToDisplay.length; // TODO should be validated as 3 or 4 max + flexbox design + for(let i = 0; i < kompleter.props.response.length ; i++) { + if(typeof kompleter.props.response[i] !== 'undefined') { + html += `
`; + for(let j = 0; j < properties; j++) { + html += '' + kompleter.props.response[i][j] + ''; + } + html += '
'; + } + } + } else { + html = '
Not found
'; + } + kompleter.htmlElements.result.innerHTML = html; + console.log('kompleter.htmlElements.result', kompleter.htmlElements.result) + document.dispatchEvent(kompleter.events['kompleter.render.result.done']) + } + }, init: function(options) { // Préfixer sur les valeurs @@ -193,12 +265,12 @@ kompleter.options = Object.assign(kompleter.options, options); - kompleter.HTMLElements.result = kompleter.handlers.build('div', [ { id: 'result', className: 'form--lightsearch__result' } ]); + kompleter.htmlElements.result = kompleter.handlers.build('div', [ { id: 'result' }, { className: 'form--lightsearch__result' } ]); const searcher = document.getElementById('wrapper'); - searcher.appendChild(kompleter.HTMLElements.result); + searcher.appendChild(kompleter.htmlElements.result); - kompleter.HTMLElements.input = document.getElementById('auto-complete'); + kompleter.htmlElements.input = document.getElementById(kompleter.options.id); // ---------- Gérer les paramètres opts @@ -212,9 +284,9 @@ // --- Currently managed as data-attributes - const dataSource = kompleter.HTMLElements.input.dataset['url']; - const filterOn = kompleter.HTMLElements.input.dataset['filter-on']; - const fieldsToDisplay = kompleter.HTMLElements.input.dataset['fields-to-display']; + const dataSource = kompleter.htmlElements.input.dataset['url']; + const filterOn = kompleter.htmlElements.input.dataset['filter-on']; + const fieldsToDisplay = kompleter.htmlElements.input.dataset['fields-to-display']; console.log('v', dataSource) console.log('v', filterOn) @@ -261,9 +333,12 @@ */ // --- Special case: the id -> ? Pass the input by id reference or HTMLElement ? - - kompleter.listeners.onType(); + kompleter.listeners.onHide(); + kompleter.listeners.onShow(); + kompleter.listeners.onType(); + kompleter.listeners.onRequestDone(); + kompleter.listeners.onRenderDone(); }, };