From a54d07fc07e309805173780f54066c010eebd77d Mon Sep 17 00:00:00 2001 From: oscarotero Date: Sun, 17 Jun 2018 19:21:15 +0200 Subject: [PATCH] moved all code to one file --- demo/index.html | 2 +- demo/script.js | 10 +- src/ajax-source.js | 68 ----- src/datalist-source.js | 61 ----- src/source.js | 328 ------------------------ src/suggestions.js | 549 ++++++++++++++++++++++++++++++++++++----- 6 files changed, 488 insertions(+), 530 deletions(-) delete mode 100644 src/ajax-source.js delete mode 100644 src/datalist-source.js delete mode 100644 src/source.js diff --git a/demo/index.html b/demo/index.html index 560e838..4ab910d 100644 --- a/demo/index.html +++ b/demo/index.html @@ -30,7 +30,7 @@ diff --git a/demo/script.js b/demo/script.js index 9298ac7..93c1787 100644 --- a/demo/script.js +++ b/demo/script.js @@ -1,13 +1,13 @@ -import Suggestions from '../src/suggestions.js'; -import DatalistSource from '../src/datalist-source.js'; +import { Suggestions, Source, AjaxSource } from '../src/suggestions.js'; const datalistInput = document.getElementById('input-datalist'); const suggestions = new Suggestions( datalistInput, - new DatalistSource(datalistInput) + new AjaxSource('data.json', datalistInput.closest('fieldset')) + // Source.createFromElement(document.getElementById('colors')) ); -suggestions.on('select', function(value) { - console.log(this.query, value); +datalistInput.addEventListener('suggestion:choosen', e => { + console.log(e.detail); }); diff --git a/src/ajax-source.js b/src/ajax-source.js deleted file mode 100644 index 6e2469e..0000000 --- a/src/ajax-source.js +++ /dev/null @@ -1,68 +0,0 @@ -import Source from './source.js'; - -export default class AjaxSource extends Source { - constructor(endpoint, settings = {}) { - super(settings); - this.endpoint = endpoint; - this.cache = {}; - } - - refresh(query) { - if (query.length < 2) { - return this.close(); - } - - if (this.timeout) { - clearTimeout(this.timeout); - } - - this.query = query; - - if (this.cache[query]) { - this.data = this.cache[query]; - delete this.query; - this.update(); - return; - } - - this.timeout = setTimeout(() => { - getJson(this.endpoint + '?q=' + query) - .catch(err => console.error(err)) - .then(data => { - this.load(data); - this.cache[query] = this.data; - this.update(); - - clearTimeout(this.timeout); - delete this.timeout; - - if (this.query && query !== this.query) { - query = this.query; - delete this.query; - return this.update(); - } - - delete this.query; - }); - }, 200); - } -} - -function getJson(url) { - return new Promise((resolve, reject) => { - const request = new XMLHttpRequest(); - - request.open('GET', url, true); - request.setRequestHeader('Accept', 'application/json'); - - request.onload = () => { - if (request.status >= 200 && request.status < 400) { - resolve(JSON.parse(request.responseText)); - } else { - reject(`The request status code is ${request.status}`); - } - }; - - request.send(); - }); -} diff --git a/src/datalist-source.js b/src/datalist-source.js deleted file mode 100644 index 063b239..0000000 --- a/src/datalist-source.js +++ /dev/null @@ -1,61 +0,0 @@ -import Source from './source.js'; - -export default class DatalistSource extends Source { - constructor(input, parent) { - const listElement = input.ownerDocument.getElementById( - input.getAttribute('list') - ); - - input.removeAttribute('list'); - super(getAvailableOptions(listElement), parent || listElement.parentElement); - - this.input = input; - this.listId = input.getAttribute('list'); - } - - destroy() { - this.input.setAttribute('list', this.listId); - super.destroy(); - } -} - -function getAvailableOptions(element) { - const data = []; - - element.querySelectorAll('optgroup').forEach(optgroup => { - const options = []; - - optgroup.querySelectorAll('option').forEach(option => - options.push( - Object.assign( - { - label: option.label, - value: option.value - }, - option.dataset - ) - ) - ); - - data.push({ - label: optgroup.label, - options: options - }); - }); - - element.querySelectorAll('option').forEach(option => { - if (option.parentElement.tagName !== 'OPTGROUP') { - data.push( - Object.assign( - { - label: option.label, - value: option.value - }, - option.dataset - ) - ); - } - }); - - return data; -} diff --git a/src/source.js b/src/source.js deleted file mode 100644 index 09402a0..0000000 --- a/src/source.js +++ /dev/null @@ -1,328 +0,0 @@ -/** - * Class to manage an individual suggestion - */ -class Suggestion { - static create(data, parent) { - return new Suggestion(data, parent); - } - - constructor(data, parent) { - this.data = data; - this.search = data.search; - this.label = data.label; - this.value = data.value; - this.parent = parent; - this.element = this.render(); - } - - render() { - const element = document.createElement('li'); - element.innerHTML = this.label; - - return element; - } - - refresh(filter) { - this.unselect(); - - if (filter(this)) { - this.parent.appendChild(this.element); - } else { - this.element.remove(); - } - } - - scroll(scrollGroup) { - let rect = this.element.getBoundingClientRect(); - const parentRect = this.parent.getBoundingClientRect(); - - if (parentRect.top - rect.top > 0) { - this.parent.scrollTop -= parentRect.top - rect.top; - } else if (parentRect.bottom < rect.bottom) { - this.element.scrollIntoView(false); - } - - if (scrollGroup && this.group && !this.element.previousElementSibling) { - rect = this.group.wrapperElement.getBoundingClientRect(); - - if (parentRect.top - rect.top > 0) { - this.parent.scrollTop -= parentRect.top - rect.top; - } - } - } - - select() { - this.element.classList.add('is-selected'); - } - - unselect() { - this.element.classList.remove('is-selected'); - } -} - -/** - * Class to group suggestions - */ -class Group { - static create(data, parent) { - const group = new Group(data, parent); - - if (data.options) { - data.options.forEach(option => group.addSuggestion(Suggestion.create(option))); - } - - return group; - } - - constructor(data, parent) { - this.data = data; - this.label = data.label; - this.parent = parent; - this.element = this.render(); - this.suggestions = []; - } - - addSuggestion(suggestion) { - suggestion.parent = this.parent.content; - this.suggestions.push(suggestion); - } - - render() { - const container = document.createElement('li'); - const content = document.createElement('ul'); - - container.innerHTML = `${this.label}`; - container.appendChild(content); - - return {container, content}; - } - - refresh(filter) { - this.suggestions.forEach(suggestion => suggestion.refresh(filter)); - - if (this.element.content.childElementCount) { - this.parent.appendChild(this.element.container); - } else { - this.element.container.remove(); - } - } -} - -export default class Source { - constructor(data, parent = document.body) { - this.isClosed = true; - this.suggestions = []; - this.result = []; - this.current = 0; - - this.element = this.render(); - parent.appendChild(this.element); - - if (data) { - this.load(data); - } - - delegate(this.element, 'mouseenter', 'li', (e, target) => { - this.selectByElement(target); - }); - } - - render() { - return document.createElement('ul'); - } - - load(options) { - this.suggestions = options.map(option => - option.options ? Group.create(option, this.element) : Suggestion.create(option, this.element) - ); - } - - selectFirst() { - this.current = 0; - - if (this.result[this.current]) { - this.result[this.current].select(); - } - } - - selectNext() { - if (this.result[this.current + 1]) { - this.result[this.current].unselect(); - this.current++; - - if (this.result[this.current]) { - this.result[this.current].select(); - this.result[this.current].scroll(this.element); - } - } - } - - selectPrevious() { - if (this.result[this.current - 1]) { - this.result[this.current].unselect(); - this.current--; - - if (this.result[this.current]) { - this.result[this.current].select(); - this.result[this.current].scroll(this.element, true); - } - } - } - - selectByElement(element) { - if (this.result[this.current]) { - const key = this.result.findIndex(item => item.element === element); - - if (key !== -1) { - this.result[this.current].unselect(); - this.current = key; - this.result[this.current].select(); - } - } - } - - getByElement(element) { - const item = this.result.find(item => item.element === element); - - if (item) { - return item; - } - } - - getCurrent() { - if (this.result[this.current]) { - return this.result[this.current]; - } - } - - close() { - this.isClosed = true; - //this.element.innerHTML = ''; - this.element.classList.remove('is-open'); - } - - open() { - this.isClosed = false; - this.element.classList.add('is-open'); - } - - each(callback) { - this.data.forEach(suggestion => { - if (suggestion instanceof Group) { - suggestion.data.forEach(item => callback(item, suggestion)); - - if (suggestion.element.childElementCount) { - if ( - suggestion.wrapperElement.parentElement !== this.element - ) { - this.element.appendChild(suggestion.wrapperElement); - } - } else if ( - suggestion.wrapperElement.parentElement === this.element - ) { - suggestion.wrapperElement.remove(); - } - } else { - callback(suggestion, this); - } - }); - } - - refresh(filter) { - this.suggestions.forEach(suggestion => suggestion.refresh(filter)); - - if (this.element.childElementCount) { - this.open(); - } else { - this.close(); - } - } - - filter(query) { - query = cleanString(query); - - if (!query.length) { - return this.close(); - } - - query = query.split(' '); - - this.refresh(suggestion => { - if (!suggestion.search) { - suggestion.search = cleanString( - suggestion.label + suggestion.value - ); - } - - return query.every(q => suggestion.search.indexOf(q) !== -1); - }); - } - - update(filter) { - this.element.innerHTML = ''; - this.result = []; - this.current = 0; - - this.each((suggestion, parent) => { - suggestion.unselect(); - - if (!filter || filter(suggestion)) { - parent.element.appendChild(suggestion.element); - this.result.push(suggestion); - } else if (suggestion.element.parentElement === parent.element) { - suggestion.element.remove(); - } - }); - - if (this.result.length) { - this.selectFirst(); - this.open(); - } else { - this.close(); - } - } - - destroy() { - this.element.remove(); - } -} - -function cleanString(str) { - const replace = { - a: /á/gi, - e: /é/gi, - i: /í/gi, - o: /ó/gi, - u: /ú/gi - }; - - str = str.toLowerCase(); - - for (let r in replace) { - str = str.replace(replace[r], r); - } - - return str - .replace(/[^\wñç\s]/gi, '') - .replace(/\s+/g, ' ') - .trim(); -} - -function delegate(context, event, selector, callback) { - context.addEventListener( - event, - function(event) { - for ( - let target = event.target; - target && target != this; - target = target.parentNode - ) { - if (target.matches(selector)) { - callback.call(target, event, target); - break; - } - } - }, - true - ); -} diff --git a/src/suggestions.js b/src/suggestions.js index f80525b..31d9b88 100644 --- a/src/suggestions.js +++ b/src/suggestions.js @@ -1,16 +1,388 @@ -const keys = { - 40: 'ArrowDown', - 38: 'ArrowUp', - 13: 'Enter', - 27: 'Escape' -}; - -export default class Suggestions { +/** + * An individual suggestion + * ------------------------ + */ +export class Suggestion { + static create(data, parent) { + return new Suggestion(data, parent); + } + + constructor(data, parent) { + this.data = data; + this.search = data.search; + this.label = data.label; + this.value = data.value; + this.parent = parent; + + this.element = document.createElement('li'); + this.render(this.element); + + this.element.addEventListener('mouseenter', e => { + this.element.dispatchEvent( + new CustomEvent('suggestion:hover', { + detail: this, + bubbles: true + }) + ); + }); + + this.element.addEventListener('click', e => { + this.element.dispatchEvent( + new CustomEvent('suggestion:click', { + detail: this, + bubbles: true + }) + ); + }); + } + + get selected() { + return this.element.classList.contains('is-selected'); + } + + render(element) { + element.innerHTML = this.label; + } + + refresh(result, filter) { + this.unselect(); + + if (filter(this)) { + this.parent.appendChild(this.element); + result.push(this); + } else { + this.element.remove(); + } + } + + scroll(scrollGroup) { + let rect = this.element.getBoundingClientRect(); + const parentRect = this.parent.getBoundingClientRect(); + + if (parentRect.top - rect.top > 0) { + this.parent.scrollTop -= parentRect.top - rect.top; + } else if (parentRect.bottom < rect.bottom) { + this.element.scrollIntoView(false); + } + + if (scrollGroup && this.group && !this.element.previousElementSibling) { + rect = this.group.wrapperElement.getBoundingClientRect(); + + if (parentRect.top - rect.top > 0) { + this.parent.scrollTop -= parentRect.top - rect.top; + } + } + } + + select() { + this.element.classList.add('is-selected'); + this.element.dispatchEvent( + new CustomEvent('suggestion:select', { + detail: this, + bubbles: true + }) + ); + } + + unselect() { + this.element.classList.remove('is-selected'); + this.element.dispatchEvent( + new CustomEvent('suggestion:unselect', { + detail: this, + bubbles: true + }) + ); + } +} + +/** + * Suggestions groups + * ------------------ + */ +export class Group { + static create(data, parent) { + const group = new Group(data, parent); + + if (data.options) { + data.options.forEach(option => + group.addSuggestion(Suggestion.create(option)) + ); + } + + return group; + } + + constructor(data, parent) { + this.data = data; + this.label = data.label; + this.parent = parent; + this.suggestions = []; + + this.element = document.createElement('li'); + this.contentElement = document.createElement('ul'); + + this.render(this.element); + this.element.appendChild(this.contentElement); + } + + addSuggestion(suggestion) { + suggestion.parent = this.contentElement; + this.suggestions.push(suggestion); + } + + render(element) { + element.innerHTML = `${this.label}`; + } + + refresh(result, filter) { + this.suggestions.forEach(suggestion => + suggestion.refresh(result, filter) + ); + + if (this.contentElement.childElementCount) { + this.parent.appendChild(this.element); + } else { + this.element.remove(); + } + } +} + +/** + * Manage a data source + * (groups and suggestions) + * ------------------------ + */ +export class Source { + //Create a source from a or