252 lines
7.6 KiB
JavaScript
252 lines
7.6 KiB
JavaScript
/* global define, document, Element, window */
|
|
|
|
/* ! Jets.js - v0.14.1 - 2018-06-22
|
|
* http://NeXTs.github.com/Jets.js/
|
|
* Copyright (c) 2015 Denis Lukov; Refactored 2018 Paul Anthony Webb; Licensed MIT */
|
|
|
|
(function(root, definition) {
|
|
if (typeof module !== "undefined") module.exports = definition();
|
|
else if (typeof define === "function" && typeof define.amd === "object") define(definition);
|
|
else root["Jets"] = definition();
|
|
}(this, function() {
|
|
"use strict";
|
|
|
|
function Jets(opts) {
|
|
if (!(this instanceof Jets)) return new Jets(opts);
|
|
|
|
const defaults = {
|
|
diacriticsMap: {},
|
|
hideBy: "display: none",
|
|
searchSelector: "*AND"
|
|
};
|
|
|
|
const self = this;
|
|
|
|
self.options = {};
|
|
|
|
[
|
|
"addImportant",
|
|
"callSearchManually",
|
|
"columns",
|
|
"diacriticsMap",
|
|
"didSearch",
|
|
"hideBy",
|
|
"invert",
|
|
"manualContentHandling",
|
|
"searchInSpecificColumn",
|
|
"searchSelector"
|
|
].forEach(name => self.options[name] = opts[name] || defaults[name]);
|
|
|
|
if (this.options.searchSelector.length > 1) {
|
|
const searchSelector = self.options["searchSelector"].trim();
|
|
|
|
self.options.searchSelector = searchSelector.substr(0, 1);
|
|
self.options.searchSelectorMode = searchSelector.substr(1).toUpperCase();
|
|
}
|
|
|
|
self.content_tag = document.querySelectorAll(opts.contentTag);
|
|
if (!self.content_tag) throw new Error("Error! Could not find contentTag element");
|
|
self.content_param = opts.contentTag;
|
|
self.search_tag = document.querySelector(opts.searchTag);
|
|
|
|
if (
|
|
!self.search_tag &&
|
|
!self.options.callSearchManually
|
|
) throw new Error("Error! Provide one of search methods: searchTag or callSearchManually and call .search(\"phrase\") manually");
|
|
|
|
let last_search_query = self.search_tag && self.search_tag.value || "";
|
|
|
|
self.search = (search_query, optional_column) => {
|
|
const new_search_query =
|
|
self.options.callSearchManually &&
|
|
typeof search_query !== "undefined" ?
|
|
search_query :
|
|
self.search_tag ?
|
|
self.search_tag.value :
|
|
"";
|
|
|
|
if (last_search_query === (last_search_query = new_search_query)) return;
|
|
(0, self._applyCSS(last_search_query, optional_column));
|
|
self.options.didSearch && self.options.didSearch(last_search_query);
|
|
};
|
|
|
|
self._onSearch = function(event) {
|
|
if (event.type === "keydown") return setTimeout(self.search, 0);
|
|
self.search();
|
|
};
|
|
|
|
self.destroy = function() {
|
|
if (!self.options.callSearchManually) self._processEventListeners("remove");
|
|
self._destroy();
|
|
};
|
|
|
|
if (!self.options.callSearchManually) self._processEventListeners("add");
|
|
|
|
self._addStyleTag();
|
|
self._setJets();
|
|
self._applyCSS(last_search_query);
|
|
}
|
|
|
|
Jets.prototype = {
|
|
constructor: Jets,
|
|
_processEventListeners: function(action) {
|
|
[
|
|
"change",
|
|
"input",
|
|
"keydown"
|
|
].forEach(function(event_type) {
|
|
this.search_tag[action + "EventListener"](event_type, this._onSearch);
|
|
}.bind(this));
|
|
},
|
|
|
|
_applyCSS: function(search_query, optional_column) {
|
|
const options = this.options;
|
|
|
|
const search_phrase = this.replaceDiacritics(
|
|
search_query
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/\s\s+/g, " ")
|
|
).replace(/\\/g, "\\\\");
|
|
|
|
const words = options.searchSelectorMode ?
|
|
search_phrase.split(" ").filter((item, pos, arr) => arr.indexOf(item) === pos) :
|
|
[search_phrase];
|
|
|
|
const is_strict_selector = options.searchSelectorMode === "AND";
|
|
const selectors = new Array(words.length);
|
|
|
|
for (let i = 0, ii = words.length; i < ii; i++) {
|
|
selectors[i] =
|
|
(is_strict_selector ? this.content_param + ">" : "") +
|
|
(options.invert ? "" : ":not(") +
|
|
"[data-jets" + (typeof optional_column !== "undefined" ? "-col-" + optional_column : "") + options.searchSelector + `="${words[i]}"]` +
|
|
(options.invert ? "" : ")");
|
|
}
|
|
|
|
const hide_rules =
|
|
options.hideBy
|
|
.split(";")
|
|
.filter(Boolean)
|
|
.map(rule => rule + (options.addImportant ? "!important" : ""));
|
|
|
|
const css_rule = (is_strict_selector ? "" : this.content_param + ">") + selectors.join(is_strict_selector ? "," : "") + "{" + hide_rules.join(";") + "}";
|
|
|
|
this.styleTag.innerHTML = search_phrase.length ? css_rule : "";
|
|
},
|
|
|
|
_addStyleTag: function() {
|
|
this.styleTag = document.createElement("style");
|
|
document.head.appendChild(this.styleTag);
|
|
},
|
|
|
|
_getText: function(tag) {
|
|
return tag && (tag.textContent || tag.innerText) || "";
|
|
},
|
|
|
|
_sanitize: function(text) {
|
|
return this.replaceDiacritics(text).trim()
|
|
.replace(/\s+/g, " ")
|
|
.toLowerCase();
|
|
},
|
|
|
|
_getContentTags: function(query) {
|
|
return Array.prototype.slice.call(this.content_tag).reduce((all, elem) => {
|
|
return all.concat(Array.prototype.slice.call(elem.querySelectorAll(query || ":scope > *")));
|
|
}, []);
|
|
},
|
|
|
|
_handleSpecificColumns: function(tag, set) {
|
|
const self = this;
|
|
|
|
if (!self.options.searchInSpecificColumn) return;
|
|
|
|
Array.prototype.slice.call(tag.children).map((children, i) => {
|
|
if (
|
|
self.options.columns &&
|
|
self.options.columns.length &&
|
|
self.options.columns.indexOf(i) === -1
|
|
) return;
|
|
|
|
tag[(set || "remove") + "Attribute"]("data-jets-col-" + i, set && self._sanitize(self._getText(children)));
|
|
});
|
|
},
|
|
|
|
_setJets: function(query, force) {
|
|
const self = this;
|
|
const tags = self._getContentTags(force ? "" : query);
|
|
let text;
|
|
|
|
for (const tag of tags) {
|
|
if (tag.hasAttribute("data-jets") && !force) continue;
|
|
|
|
text = this.options.manualContentHandling ?
|
|
this.options.manualContentHandling(tag) :
|
|
self.options.columns &&
|
|
self.options.columns.length ?
|
|
self.options.columns.map(column => self._getText(tag.children[column])).join(" ") :
|
|
self._getText(tag);
|
|
|
|
tag.setAttribute("data-jets", self._sanitize(text));
|
|
self._handleSpecificColumns(tag, "set");
|
|
}
|
|
},
|
|
|
|
replaceDiacritics: function(text) {
|
|
const diacritics = this.options.diacriticsMap;
|
|
|
|
for (const letter in diacritics) if (diacritics.hasOwnProperty(letter)) {
|
|
for (let i = 0, ii = diacritics[letter].length; i < ii; i++) {
|
|
text = text.replace(new RegExp(diacritics[letter][i], "g"), letter);
|
|
}
|
|
}
|
|
|
|
return text;
|
|
},
|
|
|
|
update: function(force) {
|
|
this._setJets(":scope > :not([data-jets])", force);
|
|
},
|
|
|
|
_destroy: function() {
|
|
this.styleTag.parentNode && document.head.removeChild(this.styleTag);
|
|
const tags = this._getContentTags();
|
|
|
|
for (const tag of tags) {
|
|
tag.removeAttribute("data-jets");
|
|
this._handleSpecificColumns(tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
// :scope polyfill
|
|
// https://stackoverflow.com/a/17989803/1221082
|
|
;(function(doc, proto) {
|
|
try {
|
|
doc.querySelector(":scope body");
|
|
} catch(err) {
|
|
["querySelector", "querySelectorAll"].forEach(method => {
|
|
const nativ = proto[method];
|
|
|
|
proto[method] = function(selectors) {
|
|
if (/(^|,)\s*:scope/.test(selectors)) {
|
|
const id = this.getAttribute("id");
|
|
|
|
this.id = "ID_" + Date.now();
|
|
selectors = selectors.replace(/((^|,)\s*):scope/g, "$1#" + this.getAttribute("id"));
|
|
|
|
const result = doc[method](selectors);
|
|
|
|
this.id = id;
|
|
|
|
return result;
|
|
}
|
|
|
|
return nativ.call(this, selectors);
|
|
};
|
|
});
|
|
}
|
|
})(window.document, Element.prototype);
|
|
|
|
return Jets;
|
|
}));
|