--- /dev/null
+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.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.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.ignoreWhitespace = opts?.ignoreWhitespace || true;
+ this.limit = opts?.limit || null;
+ }
+
+ search(term) {
+ this._softReset();
+
+ this.term = (this.isCaseSensitive) ? term : term.toLocaleLowerCase();
+ if (this.ignoreWhitespace) this.term = this.term.replace(' ', '');
+
+ 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;
+ queueMicrotask(() => 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;
+ let myItem = (this.isCaseSensitive) ? item : item.toLocaleLowerCase();
+ if (this.ignoreWhitespace) myItem = myItem.replace(' ', '');
+
+ if (this.isContain) return myItem.indexOf(this.term) !== -1;
+ return myItem.startsWith(this.term);
+ }
+
+ _sendFindings() {
+ this.emit(this.RESULT_SENT, this.findingList);
+ }
+}
+
+export default AsyncSearch;