Home Reference Source

src/modules/highlightKeywords.js

import {createText, createElm, getText} from '../dom';
import {isNull} from '../types';
import {rgxEsc} from '../string';
import {defaultsStr} from '../settings';

/**
 * Highlight matched keywords upon filtering
 *
 * @export
 * @class HighlightKeyword
 */
export class HighlightKeyword {

    /**
     * Creates an instance of HighlightKeyword
     * @param {TableFilter} tf TableFilter instance
     */
    constructor(tf) {
        let f = tf.config();

        /**
         * Css class for highlighted term
         * @type {String}
         */
        this.highlightCssClass = defaultsStr(f.highlight_css_class, 'keyword');

        /**
         * TableFilter instance
         * @type {TableFilter}
         */
        this.tf = tf;

        /**
         * TableFilter's emitter instance
         * @type {Emitter}
         */
        this.emitter = tf.emitter;
    }

    /**
     * Initializes HighlightKeyword instance
     */
    init() {
        this.emitter.on(
            ['before-filtering', 'destroy'],
            () => this.unhighlightAll()
        );
        this.emitter.on(
            ['highlight-keyword'],
            (tf, cell, term) => this._processTerm(cell, term)
        );
    }

    /**
     * Highlight occurences of searched term in passed node
     * @param  {Node} node
     * @param  {String} term     Searched term
     * @param  {String} cssClass Css class name
     *
     * TODO: refactor this method
     */
    highlight(node, term, cssClass) {
        // Iterate into this nodes childNodes
        if (node.hasChildNodes) {
            let children = node.childNodes;
            for (let i = 0; i < children.length; i++) {
                this.highlight(children[i], term, cssClass);
            }
        }

        if (node.nodeType === 3) {
            let nodeVal = node.nodeValue.toLowerCase();
            let termIdx = nodeVal.indexOf(term.toLowerCase());

            if (termIdx !== -1) {
                let pn = node.parentNode;
                if (pn && pn.className !== cssClass) {
                    // term not highlighted yet
                    let nv = node.nodeValue,
                        // Create a load of replacement nodes
                        before = createText(nv.substr(0, termIdx)),
                        value = nv.substr(termIdx, term.length),
                        after = createText(nv.substr(termIdx + term.length)),
                        text = createText(value),
                        container = createElm('span');
                    container.className = cssClass;
                    container.appendChild(text);
                    pn.insertBefore(before, node);
                    pn.insertBefore(container, node);
                    pn.insertBefore(after, node);
                    pn.removeChild(node);
                }
            }
        }
    }

    /**
     * Removes highlight to nodes matching passed string
     * @param  {String} term
     * @param  {String} cssClass Css class to remove
     */
    unhighlight(term, cssClass) {
        let highlightedNodes = this.tf.dom().querySelectorAll(`.${cssClass}`);
        for (let i = 0; i < highlightedNodes.length; i++) {
            let n = highlightedNodes[i];
            let nodeVal = getText(n);

            if (isNull(term) ||
                nodeVal.toLowerCase().indexOf(term.toLowerCase()) !== -1) {
                let parentNode = n.parentNode;
                parentNode.replaceChild(createText(nodeVal), n);
                parentNode.normalize();
            }
        }
    }

    /**
     * Clear all occurrences of highlighted nodes
     */
    unhighlightAll() {
        if (!this.tf.highlightKeywords) {
            return;
        }

        this.unhighlight(null, this.highlightCssClass);
    }

    /**  Remove feature */
    destroy() {
        this.emitter.off(
            ['before-filtering', 'destroy'],
            () => this.unhighlightAll()
        );
        this.emitter.off(
            ['highlight-keyword'],
            (tf, cell, term) => this._processTerm(cell, term)
        );
    }

    /**
     * Ensure filtering operators are handled before highlighting any match
     * @param {any} Table cell to look searched term into
     * @param {any} Searched termIdx
     */
    _processTerm(cell, term) {
        let tf = this.tf;
        let reLk = new RegExp(rgxEsc(tf.lkOperator));
        let reEq = new RegExp(tf.eqOperator);
        let reSt = new RegExp(tf.stOperator);
        let reEn = new RegExp(tf.enOperator);
        let reLe = new RegExp(tf.leOperator);
        let reGe = new RegExp(tf.geOperator);
        let reL = new RegExp(tf.lwOperator);
        let reG = new RegExp(tf.grOperator);
        let reD = new RegExp(tf.dfOperator);

        term = term
            .replace(reLk, '')
            .replace(reEq, '')
            .replace(reSt, '')
            .replace(reEn, '');

        if (reLe.test(term) || reGe.test(term) || reL.test(term) ||
            reG.test(term) || reD.test(term)) {
            term = getText(cell);
        }

        if (term === '') {
            return;
        }

        this.highlight(cell, term, this.highlightCssClass);
    }
}

// TODO: remove as soon as feature name is fixed
HighlightKeyword.meta = {
    name: 'highlightKeyword',
    altName: 'highlightKeywords'
};