Source: modules/search.js

/**
 * The search functionality
 * @namespace
 */

/* global debug */
/* global findAndReplaceDOMText */
/* global highlightElements */
/* global scrollToElement */
/* global getId */
/* global MenubarActions */
/* global errorPacketHandler */
/* global LoadingView */
/* global handleErrorPage */
/* global dataAsJson */
/* global dataHasJSONError */
/* global addPromptToAjax */
/* global addParametersToAJAX */
/* global SACK */
/* global buildBASEURLForPageFile */
/* global getTranslation */
/* global setInnerText */
/* global menubar */
/* global generator */

var documentSearch = {

    searchMenu: null,

    resultCount: null,

    currentResult: 0,

    searchResult: [],

    searchResultsTraversed: [],

    lastPage: -1,

    prevButton: null,

    nextButton: null,

    lastHighlight: null,

    lastPageModeWasSingle: null,

    /**
     * Create the search panel
     * @param {object} parentButton     where to add the search panel
     * @param {boolean} insertBefore    Element to insert the resulting button before
     */
    buildSearch: function (parentButton, insertBefore) {

        var menu = new generator.submenuContainer(parentButton);

        var group = document.createElement("div");
        group.className = "__menuGroup __searchBar";

        var search = document.createElement("div");
        search.className = "__comboBox __menuDropDown";
        group.appendChild(search);

        menu.input = document.createElement("input");
        menu.input.className = "__comboInput";
        search.appendChild(menu.input);
        menu.input.addEvent("keydown", documentSearch.inputKeyEvent);

        documentSearch.resultCount = document.createElement("span");
        search.appendChild(documentSearch.resultCount);

        var bar = new menubar();
        var button = bar.createButton("", "searchfield", null, documentSearch.runSearch, null, getTranslation("menuBar.search"), search, '__comboButton __icon');
        button.removeClassName("__menuButton");

        documentSearch.prevButton = bar.createButton("<", "search.previous", null, documentSearch.searchPrevious, null, getTranslation("menuBar.search.previous"), group, '__icon');
        documentSearch.nextButton = bar.createButton(">", "search.next", null, documentSearch.searchNext, null, getTranslation("menuBar.search.next"), group, '__icon');

        documentSearch.prevButton.setEnabled(false);
        documentSearch.nextButton.setEnabled(false);

        menu.addEntry(group);
        if (typeof insertBefore != 'undefined') {
            getId('__menuBar').insertBefore(menu.submenu, insertBefore);
        } else {
            getId('__menuBar').appendChild(menu.submenu);
        }

        documentSearch.searchMenu = menu;
    },
    
    clearSearch: function( full ) {
        documentSearch.searchResultsTraversed = [];
        documentSearch.currentResult = 0;
        documentSearch.lastHighlight = null;

        // Clear DOM of HighlightElements
        documentSearch.searchResult.forEach(function (element) {
            if (element.highlight) {
                element.highlight.forEach(function (self) {
                    var text = document.createTextNode(self.textContent || self.innerText);
                    self.parentNode.insertBefore(text, self);
                    self.parentNode.removeChild(self);
                });
            }
        });

        documentSearch.searchResult = [];
        documentSearch.updateButtons();
        documentSearch.setCurrentCount();
        documentSearch.setResultCount();

        if ( full === true ) {
            documentSearch.searchMenu.input.value = "";
            documentSearch.showSearch( null, false );
        }
    },

    inputKeyEvent: function (event) {
        let keycode;
        if (event.which) {
            keycode = event.which;
        } else {
            keycode = event.keyCode;
        }

        switch (keycode) {
        case 16:
            break; // Shift / Ignore single modifier 
        case 17:
            break; // CTRL
        case 18:
            break; // Alt
        case 91:
            break; // OSX CMD
        case 13:
            documentSearch.runSearch(event);
            break;
        case 27:
            if (documentSearch.searchResult.length === 0 && documentSearch.searchMenu.menuContainer.style.display == 'block') {
                documentSearch.showSearch(null, false); // Close
                break;
            }

            documentSearch.clearSearch( true );
            /* fall through */
        default: // Keypressed, so reset counts
            if (!event.altKey && !event.metaKey && !event.altGraphKey) {
                documentSearch.clearSearch();
            }
        }
    },

    updateCount: function (attribute, value) {
        if (documentSearch.resultCount) {
            if (parseInt(value) > 0) {
                documentSearch.resultCount.setAttribute(attribute, value);
            } else {
                documentSearch.resultCount.removeAttribute(attribute);
            }
        }
    },

    hasMoreResult: function() {
        return documentSearch.searchResult.length > 0 && documentSearch.lastPage < (new MenubarActions()).maxPages;
    },

    setResultCount: function () {
        var resultCount = documentSearch.searchResult.length;
        documentSearch.updateCount("resultcount", resultCount);
    },

    setCurrentCount: function () {
        documentSearch.updateCount("currentcount", documentSearch.currentResult);
    },

    jumpedOverTimeout: null,
    setJumpedOver: function (element) {
        if (!documentSearch.resultCount) {
            return;
        }

        documentSearch.resultCount.setAttribute("animate", "true");
        setInnerText(documentSearch.resultCount, getTranslation("menuBar.search.jumpedElement") + element);

        if (documentSearch.jumpedOverTimeout != null) {
            window.clearTimeout(documentSearch.jumpedOverTimeout);
            documentSearch.jumpedOverTimeout = null;
        }

        documentSearch.jumpedOverTimeout = window.setTimeout(function () {
            documentSearch.resultCount.removeAttribute("animate");
            documentSearch.jumpedOverTimeout = null;
        }, 2000);
    },

    /** Open the search popup */
    showSearch: function ( event, show ) {
        if ( typeof show == "undefined" ) {
            show = documentSearch.searchMenu.menuContainer.style.display != 'block';
        }
        documentSearch.searchMenu.menuContainer.style.display = show ? 'block' : 'none';
        documentSearch.searchMenu.parentElement.setIsCurrent(show);
        if ( show ) {
            documentSearch.searchMenu.input.focus();
        }
    },

    /** Start the search using the server */
    runSearch: function (event, lastPage) {

        // Called on Enter and Click. If there is a result already, cycle.
        if (documentSearch.searchResult.length != 0 && !lastPage) {
            if (event.shiftKey) {
                documentSearch.searchPrevious();
            } else {
                documentSearch.searchNext();
            }

            return;
        }

        var url = buildBASEURLForPageFile(null, true);

        var ajax = new SACK(url);

        // Set all parameters
        addParametersToAJAX(ajax, {
            phrase: documentSearch.searchMenu.input.value,
            cmd: "search",
            page: lastPage || 0
        });

        addPromptToAjax && addPromptToAjax(ajax);

        ajax.onCompletion = function () {

            /* check for error */
            var data = ajax.response;
            var status = ajax.responseStatus[0];
            (new LoadingView()).hide();

            if (dataHasJSONError(data)) {
                return;
            }

            if (status >= 200 && status < 400) {

                // should be a search response!
                var json = dataAsJson(data) || {};

                documentSearch.searchResult = documentSearch.searchResult.concat( json.results || [] );
                documentSearch.lastPage = json.lastPage || -1;

                // Traverse per page
                documentSearch.searchResult.forEach(function (page) {
                    if (typeof documentSearch.searchResultsTraversed[page.page] != 'object') {
                        documentSearch.searchResultsTraversed[page.page] = [];
                    }

                    page.index = documentSearch.searchResultsTraversed[page.page].length;
                    documentSearch.searchResultsTraversed[page.page].push(page);
                });

                documentSearch.setResultCount();
                documentSearch.updateButtons();
                !lastPage && documentSearch.searchNext();

            } else if (!ajax.canceled && status > 0) {
                handleErrorPage(data, status);
            }
        };

        try {
            (new LoadingView()).show();
            debug("Searching");
            ajax.runAJAX();
        } catch (e) {
            (new LoadingView()).hide();
            new errorPacketHandler(this.pageNotfoundError);
        }
    },

    /** Skip to next result */
    searchNext: function () {

        if (!documentSearch.nextButton.isEnabled()) {
            return false;
        }

        if (documentSearch.searchResult.length === 0) {
            documentSearch.runSearch();
            return false;
        }

        var nextPage = Math.min(++documentSearch.currentResult, documentSearch.searchResult.length);
        documentSearch.showResult(nextPage, documentSearch.searchNext);

        if ( documentSearch.hasMoreResult() && nextPage == documentSearch.searchResult.length ) {
            documentSearch.runSearch( null, documentSearch.lastPage );
        }
    },

    /** Skip to previous result */
    searchPrevious: function () {

        if (!documentSearch.prevButton.isEnabled()) {
            return false;
        }

        documentSearch.showResult(Math.max(1, --documentSearch.currentResult), documentSearch.searchPrevious);
    },

    resetResultPage: function (oldPage) {
        // Reset
        oldPage.highlight = null;
        oldPage.skip = null;
    },

    showResult: function (number, directionFunction) {

        // get result
        var result = documentSearch.searchResult[number - 1];
        var menu = new MenubarActions();

        if (documentSearch.lastPageModeWasSingle === null) {
            documentSearch.lastPageModeWasSingle = getId('single-page').isCurrent();
        } else if (documentSearch.lastPageModeWasSingle != getId('single-page').isCurrent()) {
            debug("Page mode changed! Reset saved highlight Groups.");
            documentSearch.searchResult.forEach(documentSearch.resetResultPage);
            documentSearch.lastPageModeWasSingle = getId('single-page').isCurrent();
        }

        documentSearch.setCurrentCount();
        documentSearch.updateButtons();
        documentSearch.searchMenu.input.focus();

        if (result.skip) {
            documentSearch.setJumpedOver(number);
            return directionFunction();
        }

        menu.assurePageIsPresent(result.page, null, function () {

            var numberOfResultsOnPage = documentSearch.searchResultsTraversed[result.page];
            if (!numberOfResultsOnPage) {
                // WTF
                return;
            }

            var page = menu.getCurrentPageName(result.page);

            debug("current page on search: " + page);

            if (!result.highlight) {
                // Now search the page ...
                // alert("page:" + result.page + " index:" + index + " result:" + numberOfResultsOnPage.length + " " + page);
                documentSearch.highlight(getId(page), result);
            }

            if (documentSearch.lastHighlight) {
                // If previous highlight, remove it
                if (documentSearch.lastHighlight.highlight) {
                    documentSearch.lastHighlight.highlight.forEach(function (self) {
                        self.removeClassName("currenthighlight");
                    });
                }

                // Page Change
                if (documentSearch.lastHighlight.page != result.page && getId('single-page').isCurrent()) {
                    documentSearch.searchResultsTraversed[documentSearch.lastHighlight.page].forEach(documentSearch.resetResultPage);
                }
            }

            if (result.highlight) {
                result.highlight.forEach(function (self) {
                    self.addClassName("currenthighlight");
                });
                documentSearch.lastHighlight = result;
            }

            // Might just have changed
            if (result.skip) {
                documentSearch.setJumpedOver(number);
                directionFunction();
            } else if (result.highlight) {
                scrollToElement(result.highlight[0]);
                highlightElements(result.highlight, true);
            }
        });
    },

    highlightRegExp: function () {

        var regex = [];
        var escapeRegExp = function (str) {
            return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
        };

        documentSearch.searchResult.forEach(function (result) {
            regex.push(escapeRegExp(result.result));
        });

        return new RegExp("(" + regex.getUnique().join("|").replace(new RegExp("\\s+", "g"), "\\s*?") + ")", "ig");
    },

    highlight: function (root, result) {

        var adjust = [];

        root.addClassName('isSearching');

        var addFunction = function (highlight, elementNumber) {
            var currentElem = documentSearch.searchResultsTraversed[result.page][elementNumber];
            if (currentElem) {
                if (typeof currentElem.highlight !== 'object' || currentElem.highlight == null) {
                    currentElem.highlight = [];
                }
                currentElem.highlight.push(highlight); // Next Element
                highlight.result = currentElem;

                adjust.push(highlight);
            }
            return highlight;
        };

        findAndReplaceDOMText(documentSearch.highlightRegExp(), root, function (el, matchIndex, currentNode) {

            // No no empty.
            if (el.trim().length === 0) {
                return document.createTextNode(el);
            }

            var highlight = document.createElement("span");
            highlight.className = "highlightprepare";
            highlight.appendChild(document.createTextNode(el));
            highlight.setAttribute("text", el);

            // Go upward the currentNode - max to root
            while (currentNode.parentNode && currentNode.parentNode != root) {
                currentNode = currentNode.parentNode;
                if (currentNode.elementStyle().overflow == 'hidden' && !currentNode.hasClassName('searchOverflowOverride')) {
                    currentNode.addClassName('searchOverflowOverride');
                }
            }

            return addFunction(highlight, matchIndex);
        }, null, function (shouldConsider, matchIndex) {
            if (shouldConsider.className.indexOf("highlightprepare") > -1) {
                addFunction(shouldConsider, matchIndex);
                return false;
            }
            return true;
        });

        adjust.forEach(function (element) {
            var offsetTop = element.absoluteOffsetTop(root);
            if (offsetTop < 0 || offsetTop > root.offsetHeight) {
                // UhOh. Not visible on Page!
                debug("element: " + element.result.page + "/" + element.result.index + " not visible on Page: " + offsetTop + " (> " + root.offsetHeight + ") ");
                element.result.skip = true;
            }
        });
    },

    updateButtons: function () {
        documentSearch.prevButton.setEnabled(documentSearch.currentResult > 1);
        documentSearch.nextButton.setEnabled(documentSearch.currentResult < documentSearch.searchResult.length);
    }
};