isomorphic-search
Version:
A configurable search interface.
230 lines (206 loc) • 5.97 kB
JavaScript
/**
* Search module's abstraction
*/
const identifier = 'id';
const label = 'name';
let abstraction = {
// Set the identifying property among the objects in the list.
identifier,
// Set the labeling property among the objects in the list.
label,
// The notification system used when set-up fails. See control.set method.
notify: console ? console.log : (error) => { return error; },
// Set default parent selector.
parent: 'header',
// Empowers searching by representing the current search criteria.
searchBy: false,
// Threshold for error when matching search to potential results.
threshold: 0.2,
// Search parameters.
keys: [
{ name: identifier, weight: 0.3 },
{ name: label, weight: 0.7 },
],
// Represents icon classes.
icon: {
search: 'fa-search',
cancel: 'fa-close'
},
// Options passed to the search engine.
set options(options) {
let existing = Object.create(this._options || null);
delete this._options;
this._options = Object.assign(existing, options);
},
get options() {
return this._options = Object.assign({
keys: this.keys,
threshold: this.threshold,
}, this._options);
},
// Classes and other identifiers to be attached to elements.
set attr(attr) {
let existing = Object.create(this._attr || null);
delete this._attr;
this._attr = Object.assign(existing, attr);
},
get attr() {
// @TODO Abstract.
const defaults = {
body: 'body',
container: 'search',
parent: this.parent,
input: 'search-field',
hidden: 'invisible',
instructions: 'instructions',
menu: 'search-menu',
noResults: 'no-results',
result: 'result',
results: 'search-results',
resultsList: 'search-results-list',
searchFieldContainer: 'search-field-container',
searchContainer: 'search-container',
submit: 'search-submit',
title: 'search-title'
};
return this._attr = Object.assign(defaults, this._attr || null);
},
// Elements
set el(element) {
let existing = Object.create(this._el || null);
delete this._el;
this._el = Object.assign(existing, element);
},
get el() {
let existing = this._el,
elements = this.attr,
_el = {};
Object.keys(elements).forEach((el) => {
if (el === 'parent' || el === 'body') {
_el[el] = elements[el];
} else {
_el[el] = `.${elements[el]}`;
}
});
return this._el = _el;
},
// DOM Nodes
set $el(element) {
let existing = Object.create(this._$el || null);
delete this._$el;
this._$el = Object.assign(existing, element);
},
get $el() {
let existing = this._$el,
el = this.el,
_$el = {};
Object.keys(el).forEach((element) => {
let isWrapper = ['body', 'container', 'parent'].indexOf(element) !== -1;
if (!isWrapper) {
_$el[element] = $(el.container).find(el[element]);
} else {
_$el[element] = $(el[element]);
}
});
return this._$el = _$el;
},
// Validate data.
validate(data) {
if (!data) {
return this.messages.unavailable;
}
if (!data.length) {
return this.messages.empty;
}
return true;
},
// Errors
messages: {
empty: {
message: `The search list is empty.`,
name: 'empty',
type: 'warn'
},
engine: {
message: 'The search feature requires an engine.',
name: 'engine',
type: 'warn'
},
noResults: {
message: `Your search returned no results.`,
name: 'no results',
type: 'info'
},
unavailable: {
message: `The search list is unavailable.`,
name: 'unavailable',
type: 'warn'
},
},
// Metadata event default method.
metadataCallback: function(event, search) {
return {event, search};
},
// Elements' metadata
set metadata(metadata) {
let existing = Object.create(this._metadata || null);
delete this._metadata;
this._metadata = Object.assign(existing, metadata);
},
get metadata() {
const defaults = {
input: {
placeholder: 'Search',
cancel: this.metadataCallback,
change: this.metadataCallback,
focus: this.metadataCallback,
},
results: {
instructions: 'Choose from the list.'
}
};
return this._metadata = Object.assign(defaults, this._metadata || null);
},
// Templates.
template: {
error: ({attr}) => {
return `<div class="${attr.noResults}">
We found no results matching your search.
</div>`;
},
form: ({icon, attr, metadata: {input: {placeholder}}}) => {
return `<div class="${attr.container}">
<div class="${attr.searchFieldContainer}">
<input class="${attr.input}" placeholder="${placeholder}" type="text" />
<div class="${attr.menu}">
<i class="${attr.submit} fa ${icon.search}"></i>
</div>
</div>
</div>`;
},
header: (title) => {
return `<h1>${title}</h1>`;
},
// @TODO Compute `id` & `name` from `this.label` & `this.identifier`.
list: ({id, name}, {attr}) => {
return `<li class="${attr.result}" data-id="${id}" data-name="${name}">
${name}
</li>`;
},
noResults: ({attr, messages}) => {
return `<div class="${attr.noResults}">${messages.noResults.message}</div>`;
},
result: ({attr}) => {
return `<div class="${attr.hidden} ${attr.searchResults}"></div>`;
},
results: ({attr, metadata}) => {
const {className, instructions, title} = metadata.results;
return `<div class="${attr.results} ${attr.hidden}">
${title ? `<div class="${attr.title}">${title}</div>` : ''}
${instructions ? `<div class="${attr.instructions}">${instructions}</div>` : ''}
<ul class="${className} ${attr.resultsList}"></ul>
</div>`;
}
}
};
export default abstraction;