var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
define(["require", "exports", "jquery", "@wedxml/common/browsers", "../key", "../key-constants", "../transformation", "./context-menu", "./icon"], function (require, exports, jquery_1, browsers, keyMod, keyConstants, transformation_1, context_menu_1, icon) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    jquery_1 = __importDefault(jquery_1);
    browsers = __importStar(browsers);
    keyMod = __importStar(keyMod);
    keyConstants = __importStar(keyConstants);
    icon = __importStar(icon);
    const KINDS = ["transform", "add", "delete", "wrap", "unwrap"];
    // ``undefined`` is "other kinds".
    const KIND_FILTERS = KINDS.concat(undefined);
    // Sort order.
    const KIND_ORDER = [undefined].concat(KINDS);
    const TYPES = ["element", "attribute"];
    const TYPE_FILTERS = TYPES.concat(undefined);
    const ITEM_SELECTOR = "li:not(.divider):visible a";
    const plus = keyMod.makeKey("+");
    const minus = keyMod.makeKey("-");
    const period = keyMod.makeKey(".");
    const comma = keyMod.makeKey(",");
    const question = keyMod.makeKey("?");
    const less = keyMod.makeKey("<");
    const at = keyMod.makeKey("@");
    const exclamation = keyMod.makeKey("!");
    const KEY_TO_FILTER = [
        { key: plus, filter: "add", which: "kind" },
        { key: minus, filter: "delete", which: "kind" },
        { key: comma, filter: "wrap", which: "kind" },
        { key: period, filter: "unwrap", which: "kind" },
        { key: question, filter: undefined, which: "kind" },
        { key: less, filter: "element", which: "type" },
        { key: at, filter: "attribute", which: "type" },
        { key: exclamation, filter: undefined, which: "type" },
    ];
    function compareItems(a, b) {
        const aKind = (a.action !== null && (a.action instanceof transformation_1.Transformation)) ?
            a.action.kind : undefined;
        const bKind = (b.action !== null && (b.action instanceof transformation_1.Transformation)) ?
            b.action.kind : undefined;
        if (aKind !== bKind) {
            const aOrder = KIND_ORDER.indexOf(aKind);
            const bOrder = KIND_ORDER.indexOf(bKind);
            return aOrder - bOrder;
        }
        const aText = a.item.textContent;
        const bText = b.item.textContent;
        if (aText === bText) {
            return 0;
        }
        if (aText < bText) {
            return -1;
        }
        return 1;
    }
    /**
     * A context menu for displaying actions. This class is designed to know how to
     * sort [["wed/action".Action]] objects and
     * [["wed/transformation".Transformation]] objects and how to filter them. Even
     * though the names used here suggest that ``Action`` objects are the focus of
     * this class, the fact is that it is really performing its work on
     * ``Transformation`` objects. It does accept ``Action`` as a kind of lame
     * ``Transformation``. So the following description will focus on
     * ``Transformation`` objects rather than ``Action`` objects.
     *
     * Sorting is performed first by the ``kind`` of the ``Transformation`` and then
     * by the text associated with the ``Transformation``. The kinds, in order, are:
     *
     * - other kinds than those listed below
     *
     * - undefined ``kind``
     *
     * - ``"add"``
     *
     * - ``"delete"``
     *
     * - ``"wrap"``
     *
     * - ``"unwrap"``
     *
     * The text associated with the transformation is the text value of the DOM
     * ``Element`` object stored in the ``item`` field of the object given in the
     * ``items`` array passed to the constructor. ``Actions`` are considered to have
     * an undefined ``kind``.
     *
     * Filtering is performed by ``kind`` and on the text of the **element name**
     * associated with a transformation. This class presents to the user a row of
     * buttons that represent graphically the possible filters. Clicking on a button
     * will reduce the list of displayed items only to those elements that
     * correspond to the ``kind`` to which the button corresponds.
     *
     * Typing text (e.g. "foo") will narrow the list of items to the text that the
     * user typed. Let's suppose that ``item`` is successively taking the values in
     * the ``items`` array. The filtering algorithm first checks whether there is an
     * ``item.data.name`` field. If there is, the match is performed against this
     * field. If not, the match is performed against the text of ``item.item``.
     *
     * If the text typed begins with a caret (^), the text will be interpreted as a
     * regular expression.
     *
     * Typing ESCAPE will reset filtering.
     *
     * When no option is focused, typing ENTER will select the first option of the
     * menu.
     */
    class ActionContextMenu extends context_menu_1.ContextMenu {
        /**
         * @param document The DOM document for which to make this context menu.
         *
         * @param x Position of the menu. The context menu may ignore this position if
         * the menu would appear off-screen.
         *
         * @param y Position of the menu.
         *
         * @param items An array of action information in the form of anonymous
         * objects. It is valid to have some items in the array be of the form
         * ``{action: null, item: some_element, data: null}`` to insert arbitrary menu
         * items.
         *
         * @param dismissCallback Function to call when the menu is dismissed.
         */
        constructor(document, x, y, items, dismissCallback) {
            super(document, x, y, [], dismissCallback, false);
            this.filters = {
                kind: null,
                type: null,
            };
            this.actionTextFilter = "";
            // Sort the items once and for all.
            items.sort(compareItems);
            this.actionItems = items;
            // Create the filtering GUI...
            // <li><div><button>... allows us to have this button group inserted in the
            // menu and yet be ignored by Bootstrap's Dropdown class.
            const li = document.createElement("li");
            li.className = "wed-menu-filter";
            li.style.whiteSpace = "nowrap";
            const groupGroup = document.createElement("div");
            const kindGroup = this.makeKindGroup(document);
            const typeGroup = this.makeTypeGroup(document);
            // Prevent clicks in the group from closing the context menu.
            jquery_1.default(li).on("click", false);
            li.appendChild(groupGroup);
            groupGroup.appendChild(kindGroup);
            groupGroup.appendChild(document.createTextNode("\u00a0"));
            groupGroup.appendChild(typeGroup);
            const textInput = document.createElement("input");
            textInput.className = "form-control input-sm";
            textInput.setAttribute("placeholder", "Filter choices by text.");
            const textDiv = document.createElement("div");
            textDiv.appendChild(textInput);
            li.appendChild(textDiv);
            const $textInput = jquery_1.default(textInput);
            $textInput.on("input", this.inputChangeHandler.bind(this));
            $textInput.on("keydown", this.inputKeydownHandler.bind(this));
            this.actionFilterItem = li;
            this.actionFilterInput = textInput;
            const $menu = this.$menu;
            $menu.parent().on("hidden.bs.dropdown", () => {
                // Manually destroy the tooltips so that they are not
                // left behind.
                jquery_1.default(textInput).tooltip("destroy");
                jquery_1.default(kindGroup).children().tooltip("destroy");
            });
            $menu.on("keydown", this.actionKeydownHandler.bind(this));
            $menu.on("keypress", this.actionKeypressHandler.bind(this));
            this.display([]);
            textInput.focus();
        }
        makeKindGroup(document) {
            const kindGroup = document.createElement("div");
            kindGroup.className = "btn-group btn-group-xs";
            for (const kind of KIND_FILTERS) {
                const child = document.createElement("button");
                child.className = "btn btn-default";
                let title;
                if (kind !== undefined) {
                    // tslint:disable-next-line:no-inner-html
                    child.innerHTML = icon.makeHTML(kind);
                    title = `Show only ${kind} operations.`;
                }
                else {
                    // tslint:disable-next-line:no-inner-html
                    child.innerHTML = icon.makeHTML("other");
                    title = "Show operations not covered by other filter buttons.";
                }
                jquery_1.default(child).tooltip({
                    title: title,
                    // If we don't set it to be on the body, then the tooltip will be
                    // clipped by the dropdown. However, we then run into the problem that
                    // when the dropdown menu is removed, the tooltip may remain displayed.
                    container: "body",
                    // Cast is necessary due to bug in Bootstrap definitions.
                    placement: "auto top",
                    trigger: "hover",
                });
                jquery_1.default(child).on("click", this.makeKindHandler(kind));
                kindGroup.appendChild(child);
            }
            return kindGroup;
        }
        makeTypeGroup(document) {
            const typeGroup = document.createElement("div");
            typeGroup.className = "btn-group btn-group-xs";
            for (const actionType of TYPE_FILTERS) {
                const child = document.createElement("button");
                child.className = "btn btn-default";
                let title;
                if (actionType !== undefined) {
                    // tslint:disable-next-line:no-inner-html
                    child.innerHTML = icon.makeHTML(actionType);
                    title = `Show only ${actionType} operations.`;
                }
                else {
                    // tslint:disable-next-line:no-inner-html
                    child.innerHTML = icon.makeHTML("other");
                    title = "Show operations not covered by other filter buttons.";
                }
                jquery_1.default(child).tooltip({
                    title: title,
                    // If we don't set it to be on the body, then the tooltip will be
                    // clipped by the dropdown. However, we then run into the problem that
                    // when the dropdown menu is removed, the tooltip may remain displayed.
                    container: "body",
                    // Cast is necessary due to bug in Bootstrap definitions.
                    placement: "auto top",
                    trigger: "hover",
                });
                jquery_1.default(child).on("click", this.makeTypeHandler(actionType));
                typeGroup.appendChild(child);
            }
            return typeGroup;
        }
        makeKindHandler(kind) {
            return () => {
                this.filters.kind = kind;
                this.render();
            };
        }
        makeTypeHandler(actionType) {
            return () => {
                this.filters.type = actionType;
                this.render();
            };
        }
        handleToggleFocus() {
            this.actionFilterInput.focus();
        }
        actionKeydownHandler(ev) {
            if (keyConstants.ESCAPE.matchesEvent(ev) &&
                (this.filters.kind !== null ||
                    this.filters.type !== null ||
                    this.actionTextFilter !== "")) {
                this.filters.kind = null;
                this.filters.type = null;
                this.actionTextFilter = "";
                // For some reason, on FF 24, stopping propagation and
                // preventing the default is not enough.
                if (!browsers.FIREFOX_24) {
                    this.actionFilterInput.value = "";
                    this.render();
                }
                else {
                    setTimeout(() => {
                        this.actionFilterInput.value = "";
                        this.render();
                    }, 0);
                }
                ev.stopPropagation();
                ev.preventDefault();
                return false;
            }
            return true;
        }
        actionKeypressHandler(ev) {
            // If the user has started filtering on text, we don't interpret
            // the key as setting a kind or type filter.
            if (this.actionTextFilter !== "") {
                return true;
            }
            for (const spec of KEY_TO_FILTER) {
                const key = spec.key;
                if (key.matchesEvent(ev)) {
                    const whichFilter = spec.which;
                    // Don't treat the key specially if the filter is already set.
                    if (this.filters[whichFilter] !== null) {
                        continue;
                    }
                    this.filters[whichFilter] = spec.filter;
                    this.render();
                    ev.stopPropagation();
                    ev.preventDefault();
                    return false;
                }
            }
            return true;
        }
        inputChangeHandler(ev) {
            const previous = this.actionTextFilter;
            const newval = ev.target.value;
            // IE11 generates input events when focus is lost/gained. These
            // events do not change anything to the contents of the field so
            // we protect against unnecessary renders a bit of logic here.
            if (previous !== newval) {
                this.actionTextFilter = newval;
                this.render();
            }
        }
        inputKeydownHandler(ev) {
            if (keyConstants.ENTER.matchesEvent(ev)) {
                this.$menu.find(ITEM_SELECTOR).first().focus().click();
                ev.stopPropagation();
                ev.preventDefault();
                return false;
            }
            // Bootstrap 3.3.2 (and probably some versions before this one) introduces a
            // change that prevents these events from being processed by the dropdown
            // menu. We have to manually forward them. See bug report:
            //
            // https://github.com/twbs/bootstrap/issues/15757
            //
            let matches;
            for (const check of ["UP_ARROW", "DOWN_ARROW", "ESCAPE"]) {
                // tslint:disable-next-line:no-any
                const key = keyConstants[check];
                if (key.matchesEvent(ev)) {
                    matches = key;
                    break;
                }
            }
            if (matches !== undefined) {
                const fakeEv = new jquery_1.default.Event("keydown");
                matches.setEventToMatch(fakeEv);
                // We have to pass the event to ``actionKeypressHandler`` so that it can
                // act in the same way as if the event had been directly on the menu. If
                // ``actionKeypressHandler`` does not handle it, then pass it on to the
                // toggle. We forward to the toggle because that's how Bootstrap normally
                // works.
                if (this.actionKeydownHandler(fakeEv) !== false) {
                    this.$toggle.trigger(fakeEv);
                }
                // We have to return `false` to make sure it is not mishandled.
                return false;
            }
            return true;
        }
        render() {
            const menu = this.menu;
            const actionFilterItem = this.actionFilterItem;
            const actionKindFilter = this.filters.kind;
            const actionTypeFilter = this.filters.type;
            // On IE 10, we don't want to remove and then add back this.actionFilterItem
            // on each render because that makes this.actionFilterInput lose the
            // focus. Yes, even with the call at the end of _render, IE 10 inexplicably
            // makes the field lose focus **later**.
            while (menu.lastChild !== null && menu.lastChild !== actionFilterItem) {
                menu.removeChild(menu.lastChild);
            }
            let child = actionFilterItem
                .firstElementChild.firstElementChild.firstElementChild;
            for (const kind of KIND_FILTERS) {
                const cl = child.classList;
                const method = (actionKindFilter === kind) ? cl.add : cl.remove;
                method.call(cl, "active");
                child = child.nextElementSibling;
            }
            child = actionFilterItem
                .firstElementChild.lastElementChild.firstElementChild;
            for (const actionType of TYPE_FILTERS) {
                const cl = child.classList;
                const method = (actionTypeFilter === actionType) ? cl.add : cl.remove;
                method.call(cl, "active");
                child = child.nextElementSibling;
            }
            if (actionFilterItem.parentNode === null) {
                menu.appendChild(actionFilterItem);
            }
            const items = this.computeActionItemsToDisplay(this.actionItems);
            super.render(items);
        }
        // The type NamedDataItem
        computeActionItemsToDisplay(items) {
            const kindFilter = this.filters.kind;
            const typeFilter = this.filters.type;
            const textFilter = this.actionTextFilter;
            let kindMatch;
            switch (kindFilter) {
                case null:
                    kindMatch = () => true;
                    break;
                case undefined:
                    kindMatch = (item) => !(item.action instanceof transformation_1.Transformation) ||
                        KINDS.indexOf(item.action.kind) === -1;
                    break;
                default:
                    kindMatch = (item) => (item.action instanceof transformation_1.Transformation) &&
                        item.action.kind === kindFilter;
            }
            let typeMatch;
            switch (typeFilter) {
                case null:
                    typeMatch = () => true;
                    break;
                case undefined:
                    typeMatch = (item) => !(item.action instanceof transformation_1.Transformation) ||
                        TYPES.indexOf(item.action.nodeType) === -1;
                    break;
                default:
                    typeMatch = (item) => (item.action instanceof transformation_1.Transformation) &&
                        item.action.nodeType === typeFilter;
            }
            let textMatch;
            if (textFilter !== "") {
                if (textFilter[0] === "^") {
                    const textFilterRe = RegExp(textFilter);
                    textMatch = (item) => {
                        const { data } = item;
                        const text = (data !== null && data.name !== undefined) ? data.name :
                            item.item.textContent;
                        return textFilterRe.test(text);
                    };
                }
                else {
                    textMatch = (item) => {
                        const { data } = item;
                        const text = (data !== null && data.name !== undefined) ?
                            data.name : item.item.textContent;
                        return text.indexOf(textFilter) !== -1;
                    };
                }
            }
            else {
                textMatch = () => true;
            }
            const ret = [];
            for (const item of items) {
                if (kindMatch(item) && typeMatch(item) && textMatch(item)) {
                    ret.push(item.item);
                }
            }
            return ret;
        }
    }
    exports.ActionContextMenu = ActionContextMenu;
});
//  LocalWords:  MPL li Dropdown nowrap sm keydown tooltips keypress btn xs
//  LocalWords:  tooltip dropdown actionType actionFilterItem actionFilterInput
//  LocalWords:  actionKeypressHandler
//# sourceMappingURL=action-context-menu.js.map