Source: menu_store.js

/*************************************************************
 *
 *  Copyright (c) 2015-2016 The MathJax Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
/**
 * @fileoverview A store that maps HTML elements in a document to context menus.
 *
 * @author v.sorge@mathjax.org (Volker Sorge)
 */
/// <reference path="context_menu.ts" />
/// <reference path="menu_util.ts" />
var ContextMenu;
(function (ContextMenu) {
    class MenuStore {
        /**
         * @constructor
         * @param {ContextMenu} menu The context menu the store belongs to.
         */
        constructor(menu) {
            this.store = [];
            this.active = null;
            this.counter = 0;
            this.attachedClass = ContextMenu.HtmlClasses['ATTACHED'] + '_' +
                ContextMenu.MenuUtil.counter();
            this.taborder = true;
            this.attrMap = {};
            this.menu = menu;
        }
        /**
         * Sets the new active store element if it exists in the store.
         * @param {HTMLElement} element Element to be activated.
         */
        setActive(element) {
            do {
                if (this.store.indexOf(element) !== -1) {
                    this.active = element;
                    break;
                }
                element = element.parentNode;
            } while (element);
        }
        /**
         * @return {HTMLElement} The currently active store element, if one exists.
         */
        getActive() {
            return this.active;
        }
        /**
         * Returns next active element.
         * If store is empty returns null and also unsets active element.
         * If active is not set returns the first element of the store.
         * @return {HTMLElement} The next element if it exists.
         */
        next() {
            let length = this.store.length;
            if (length === 0) {
                this.active = null;
                return null;
            }
            let index = this.store.indexOf(this.active);
            index = index === -1 ? 0 : (index < length - 1 ? index + 1 : 0);
            this.active = this.store[index];
            return this.active;
        }
        /**
         * Returns previous active element.
         * If store is empty returns null and also unsets active element.
         * If active is not set returns the last element of the store.
         * @return {HTMLElement} The previous element if it exists.
         */
        previous() {
            let length = this.store.length;
            if (length === 0) {
                this.active = null;
                return null;
            }
            let last = length - 1;
            let index = this.store.indexOf(this.active);
            index = index === -1 ? last : (index === 0 ? last : index - 1);
            this.active = this.store[index];
            return this.active;
        }
        /**
         * Removes all elements in the store.
         */
        clear() {
            this.remove(this.store);
        }
        /**
         * Inserts DOM elements into the store.
         * @param {HTMLElement|Array.<HTMLElement>|NodeList} elementOrList Elements
         *     to insert.
         */
        insert(elementOrList) {
            let elements = elementOrList instanceof HTMLElement ?
                [elementOrList] : elementOrList;
            for (let i = 0, element; element = elements[i]; i++) {
                this.insertElement(element);
            }
            this.sort();
        }
        /**
         * Removes DOM elements from the store.
         * @param {HTMLElement|Array.<HTMLElement>|NodeList} elementOrList Elements
         *     to remove.
         */
        remove(elementOrList) {
            let elements = elementOrList instanceof HTMLElement ?
                [elementOrList] : elementOrList;
            for (let i = 0, element; element = elements[i]; i++) {
                this.removeElement(element);
            }
            this.sort();
        }
        /**
         * Sets if elements of the store are included in the taborder or not.
         * @param {boolean} flag If true elements are in taborder, o/w not.
         */
        inTaborder(flag) {
            if (this.taborder && !flag) {
                this.removeTaborder();
            }
            if (!this.taborder && flag) {
                this.insertTaborder();
            }
            this.taborder = flag;
        }
        /**
         * Inserts all elements in the store into the tab order.
         */
        insertTaborder() {
            if (this.taborder) {
                this.insertTaborder_();
            }
        }
        /**
         * Removes all elements in the store from the tab order.
         */
        removeTaborder() {
            if (this.taborder) {
                this.removeTaborder_();
            }
        }
        /**
         * Adds a DOM element to the store.
         * @param {HTMLElement} element The DOM element.
         */
        insertElement(element) {
            if (element.classList.contains(this.attachedClass)) {
                return;
            }
            element.classList.add(this.attachedClass);
            if (this.taborder) {
                this.addTabindex(element);
            }
            this.addEvents(element);
        }
        /**
         * Removes a DOM element from the store.
         * @param {HTMLElement} element The DOM element.
         */
        removeElement(element) {
            if (!element.classList.contains(this.attachedClass)) {
                return;
            }
            element.classList.remove(this.attachedClass);
            if (this.taborder) {
                this.removeTabindex(element);
            }
            this.removeEvents(element);
        }
        /**
         * Sorts the elements in the store in DOM order.
         */
        sort() {
            let nodes = document.getElementsByClassName(this.attachedClass);
            this.store = [].slice.call(nodes);
        }
        /**
         * Inserts all elements in the store into the tab order.
         */
        insertTaborder_() {
            this.store.forEach(x => x.setAttribute('tabindex', '0'));
        }
        /**
         * Removes all elements in the store from the tab order.
         */
        removeTaborder_() {
            this.store.forEach(x => x.setAttribute('tabindex', '-1'));
        }
        /**
         * Adds tabindex to an element and possibly safes an existing one.
         * @param {HTMLElement} element The DOM element.
         */
        addTabindex(element) {
            if (element.hasAttribute('tabindex')) {
                element.setAttribute(ContextMenu.HtmlAttrs['OLDTAB'], element.getAttribute('tabindex'));
            }
            element.setAttribute('tabindex', '0');
        }
        /**
         * Removes tabindex from element or restores an old one.
         * @param {HTMLElement} element The DOM element.
         */
        removeTabindex(element) {
            if (element.hasAttribute(ContextMenu.HtmlAttrs['OLDTAB'])) {
                element.setAttribute('tabindex', element.getAttribute(ContextMenu.HtmlAttrs['OLDTAB']));
                element.removeAttribute(ContextMenu.HtmlAttrs['OLDTAB']);
            }
            else {
                element.removeAttribute('tabindex');
            }
        }
        //// TODO: Need to add touch event.
        /**
         * Adds event listeners to activate the context menu to an element.
         *
         * To be able to remove event listeners we have to remember exactly which
         * listeners we have added. We safe them in the attribute mapping attrMap,
         * as a combination of event handler name and counter, which is unique for
         * each HTML element. The counter is stored on the HTML element in an
         * attribute.
         *
         * @param {HTMLElement} element The DOM element.
         */
        addEvents(element) {
            if (element.hasAttribute(ContextMenu.HtmlAttrs['COUNTER'])) {
                return;
            }
            this.addEvent(element, 'contextmenu', this.menu.post.bind(this.menu));
            this.addEvent(element, 'keydown', this.keydown.bind(this));
            element.setAttribute(ContextMenu.HtmlAttrs['COUNTER'], this.counter.toString());
            this.counter++;
        }
        /**
         * Adds a single event listeners and stores them in the attribute mapping.
         * @param {HTMLElement} element The DOM element.
         * @param {string} name The name of the event handler.
         * @param {EventListener} func The event listener.
         */
        addEvent(element, name, func) {
            let attrName = ContextMenu.HtmlAttrs[name.toUpperCase() + 'FUNC'];
            this.attrMap[attrName + this.counter] = func;
            element.addEventListener(name, func);
        }
        /**
         * Removes event listeners that activate the context menu from an element.
         * @param {HTMLElement} element The DOM element.
         */
        removeEvents(element) {
            if (!element.hasAttribute(ContextMenu.HtmlAttrs['COUNTER'])) {
                return;
            }
            let counter = element.getAttribute(ContextMenu.HtmlAttrs['COUNTER']);
            this.removeEvent(element, 'contextmenu', counter);
            this.removeEvent(element, 'keydown', counter);
            element.removeAttribute(ContextMenu.HtmlAttrs['COUNTER']);
        }
        /**
         * Removes a single event listeners from an HTML element.
         * @param {HTMLElement} element The DOM element.
         * @param {string} name The name of the event handler.
         * @param {string} counter The unique counter to identify the handler in the
         *     attribute mappings.
         */
        removeEvent(element, name, counter) {
            let attrName = ContextMenu.HtmlAttrs[name.toUpperCase() + 'FUNC'];
            let menuFunc = this.attrMap[attrName + counter];
            element.removeEventListener(name, menuFunc);
        }
        /**
         * Deals with key down keyboard events.
         * @param {KeyboardEvent} event The keyboard event.
         */
        keydown(event) {
            if (event.keyCode === ContextMenu.KEY.SPACE) {
                this.menu.post(event);
            }
        }
    }
    ContextMenu.MenuStore = MenuStore;
})(ContextMenu || (ContextMenu = {}));