cinny/src/util/AsyncSearch.js
ginnyTheCat ab3d9ddb13
Normalize unicode search (#263)
* Normalize unicode search

* Add option to setup function

* Make the call to normalize more explicit
2022-01-27 15:35:53 +05:30

135 lines
3.8 KiB
JavaScript

import EventEmitter from 'events';
class AsyncSearch extends EventEmitter {
constructor() {
super();
this._reset();
this.RESULT_SENT = 'RESULT_SENT';
}
_reset() {
this.dataList = null;
this.term = null;
this.searchKeys = null;
this.isContain = false;
this.isCaseSensitive = false;
this.normalizeUnicode = true;
this.ignoreWhitespace = true;
this.limit = null;
this.findingList = [];
this.searchUptoIndex = 0;
this.sessionStartTimestamp = 0;
}
_softReset() {
this.term = null;
this.findingList = [];
this.searchUptoIndex = 0;
this.sessionStartTimestamp = 0;
}
/**
* Setup the search.
* opts.keys are required when dataList items are object.
*
* @param {[string | object]} dataList - A list to search in
* @param {object} opts - Options
* @param {string | [string]} [opts.keys=null]
* @param {boolean} [opts.isContain=false] - Add finding to result if it contain search term
* @param {boolean} [opts.isCaseSensitive=false]
* @param {boolean} [opts.normalizeUnicode=true]
* @param {boolean} [opts.ignoreWhitespace=true]
* @param {number} [opts.limit=null] - Stop search after limit
*/
setup(dataList, opts) {
this._reset();
this.dataList = dataList;
this.searchKeys = opts?.keys || null;
this.isContain = opts?.isContain || false;
this.isCaseSensitive = opts?.isCaseSensitive || false;
this.normalizeUnicode = opts?.normalizeUnicode || true;
this.ignoreWhitespace = opts?.ignoreWhitespace || true;
this.limit = opts?.limit || null;
}
search(term) {
this._softReset();
this.term = this._normalize(term);
if (this.term === '') {
this._sendFindings();
return;
}
this._find(this.sessionStartTimestamp, 0);
}
_find(sessionTimestamp, lastFindingCount) {
if (sessionTimestamp !== this.sessionStartTimestamp) return;
this.sessionStartTimestamp = window.performance.now();
for (
let searchIndex = this.searchUptoIndex;
searchIndex < this.dataList.length;
searchIndex += 1
) {
if (this._match(this.dataList[searchIndex])) {
this.findingList.push(this.dataList[searchIndex]);
if (typeof this.limit === 'number' && this.findingList.length >= this.limit) break;
}
const calcFinishTime = window.performance.now();
if (calcFinishTime - this.sessionStartTimestamp > 8) {
const thisFindingCount = this.findingList.length;
const thisSessionTimestamp = this.sessionStartTimestamp;
if (lastFindingCount !== thisFindingCount) this._sendFindings();
this.searchUptoIndex = searchIndex + 1;
setTimeout(() => this._find(thisSessionTimestamp, thisFindingCount));
return;
}
}
if (lastFindingCount !== this.findingList.length
|| lastFindingCount === 0) this._sendFindings();
this._softReset();
}
_match(item) {
if (typeof item === 'string') {
return this._compare(item);
}
if (typeof item === 'object') {
if (Array.isArray(this.searchKeys)) {
return !!this.searchKeys.find((key) => this._compare(item[key]));
}
if (typeof this.searchKeys === 'string') {
return this._compare(item[this.searchKeys]);
}
}
return false;
}
_compare(item) {
if (typeof item !== 'string') return false;
const myItem = this._normalize(item);
if (this.isContain) return myItem.indexOf(this.term) !== -1;
return myItem.startsWith(this.term);
}
_normalize(item) {
let myItem = item.normalize(this.normalizeUnicode ? 'NFKC' : 'NFC');
if (!this.isCaseSensitive) myItem = myItem.toLocaleLowerCase();
if (this.ignoreWhitespace) myItem = myItem.replaceAll(' ', '');
return myItem;
}
_sendFindings() {
this.emit(this.RESULT_SENT, this.findingList, this.term);
}
}
export default AsyncSearch;