<commit_before>/**
 * @license
 * 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.
 *
 * Copyright (c) 2011-2013 <NAME>, http://jsoneditoronline.org
 *
 * @author  <NAME>, <<EMAIL>>
 */

// create namespace
var jsoneditor = jsoneditor || {};

/**
 * A context menu
 * @param {Object[]} items    Array containing the menu structure
 *                            TODO: describe structure
 * @param {Object} [options]  Object with options. Available options:
 *                            {function} close    Callback called when the
 *                                                context menu is being closed.
 * @constructor
 */
jsoneditor.ContextMenu = function (items, options) {
    this.dom = {};

    var me = this;
    var dom = this.dom;
    this.anchor = undefined;
    this.items = items;
    this.eventListeners = {};
    this.visibleSubmenu = undefined;
    this.onClose = options ? options.close : undefined;

    // create a container element
    var menu = document.createElement('div');
    menu.className = 'jsoneditor-contextmenu';
    dom.menu = menu;

    // create a list to hold the menu items
    var list = document.createElement('ul');
    list.className = 'menu';
    menu.appendChild(list);
    dom.list = list;
    dom.items = []; // list with all buttons

    // create a (non-visible) button to set the focus to the menu
    var focusButton = document.createElement('button');
    dom.focusButton = focusButton;
    var li = document.createElement('li');
    li.style.overflow = 'hidden';
    li.style.height = '0';
    li.appendChild(focusButton);
    list.appendChild(li);

    function createMenuItems (list, domItems, items) {
        items.forEach(function (item) {
            if (item.type == 'separator') {
                // create a separator
                var separator = document.createElement('div');
                separator.className = 'separator';
                li = document.createElement('li');
                li.appendChild(separator);
                list.appendChild(li);
            }
            else {
                var domItem = {};

                // create a menu item
                var li = document.createElement('li');
                list.appendChild(li);

                // create a button in the menu item
                var button = document.createElement('button');
                button.className = item.className;
                domItem.button = button;
                if (item.title) {
                    button.title = item.title;
                }
                if (item.click) {
                    button.onclick = function () {
                        me.hide();
                        item.click();
                    };
                }
                li.appendChild(button);

                // create the contents of the button
                if (item.submenu) {
                    // add the icon to the button
                    var divIcon = document.createElement('div');
                    divIcon.className = 'icon';
                    button.appendChild(divIcon);
                    button.appendChild(document.createTextNode(item.text));

                    var buttonSubmenu;
                    if (item.click) {
                        // submenu and a button with a click handler
                        button.className += ' default';

                        var buttonExpand = document.createElement('button');
                        domItem.buttonExpand = buttonExpand;
                        buttonExpand.className = 'expand';
                        buttonExpand.innerHTML = '<div class="expand"></div>';
                        li.appendChild(buttonExpand);
                        if (item.submenuTitle) {
                            buttonExpand.title = item.submenuTitle;
                        }

                        buttonSubmenu = buttonExpand;
                    }
                    else {
                        // submenu and a button without a click handler
                        var divExpand = document.createElement('div');
                        divExpand.className = 'expand';
                        button.appendChild(divExpand);

                        buttonSubmenu = button;
                    }

                    // attach a handler to expand/collapse the submenu
                    buttonSubmenu.onclick = function () {
                        me._onExpandItem(domItem);
                        buttonSubmenu.focus();
                    };

                    // create the submenu
                    var domSubItems = [];
                    domItem.subItems = domSubItems;
                    var ul = document.createElement('ul');
                    domItem.ul = ul;
                    ul.className = 'menu';
                    ul.style.height = '0';
                    li.appendChild(ul);
                    createMenuItems(ul, domSubItems, item.submenu);
                }
                else {
                    // no submenu, just a button with clickhandler
                    button.innerHTML = '<div class="icon"></div>' + item.text;
                }

                domItems.push(domItem);
            }
        });
    }
    createMenuItems(list, this.dom.items, items);

    // TODO: when the editor is small, show the submenu on the right instead of inline?

    // calculate the max height of the menu with one submenu expanded
    this.maxHeight = 0; // height in pixels
    items.forEach(function (item) {
        var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
        me.maxHeight = Math.max(me.maxHeight, height);
    });
};

/**
 * Get the currently visible buttons
 * @return {Array.<HTMLElement>} buttons
 * @private
 */
jsoneditor.ContextMenu.prototype._getVisibleButtons = function () {
    var buttons = [];
    var me = this;
    this.dom.items.forEach(function (item) {
        buttons.push(item.button);
        if (item.buttonExpand) {
            buttons.push(item.buttonExpand);
        }
        if (item.subItems && item == me.expandedItem) {
            item.subItems.forEach(function (subItem) {
                buttons.push(subItem.button);
                if (subItem.buttonExpand) {
                    buttons.push(subItem.buttonExpand);
                }
                // TODO: change to fully recursive method
            });
        }
    });

    return buttons;
};

// currently displayed context menu, a singleton. We may only have one visible context menu
jsoneditor.ContextMenu.visibleMenu = undefined;

/**
 * Attach the menu to an anchor
 * @param {HTMLElement} anchor
 */
jsoneditor.ContextMenu.prototype.show = function (anchor) {
    this.hide();

    // calculate whether the menu fits below the anchor
    var windowHeight = jsoneditor.util.getWindowHeight();
    var anchorHeight = anchor.offsetHeight;
    var menuHeight = this.maxHeight;

    // position the menu
    var left = jsoneditor.util.getAbsoluteLeft(anchor);
    var top = jsoneditor.util.getAbsoluteTop(anchor);
    if (top + anchorHeight + menuHeight < windowHeight) {
        // display the menu below the anchor
        this.dom.menu.style.left = left + 'px';
        this.dom.menu.style.top = (top + anchorHeight) + 'px';
        this.dom.menu.style.bottom = '';
    }
    else {
        // display the menu above the anchor
        this.dom.menu.style.left = left + 'px';
        this.dom.menu.style.top = '';
        this.dom.menu.style.bottom = (windowHeight - top) + 'px';
    }

    // attach the menu to the document
    document.body.appendChild(this.dom.menu);

    // create and attach event listeners
    var me = this;
    var list = this.dom.list;
    this.eventListeners.mousedown = jsoneditor.util.addEventListener(
        document, 'mousedown', function (event) {
            // hide menu on click outside of the menu
            event = event || window.event;
            var target = event.target || event.srcElement;
            if ((target != list) && !me._isChildOf(target, list)) {
                me.hide();
                jsoneditor.util.stopPropagation(event);
                jsoneditor.util.preventDefault(event);
            }
        });
    this.eventListeners.mousewheel = jsoneditor.util.addEventListener(
        document, 'mousewheel', function () {
            // hide the menu on mouse scroll
            jsoneditor.util.stopPropagation(event);
            jsoneditor.util.preventDefault(event);
        });
    this.eventListeners.keydown = jsoneditor.util.addEventListener(
        document, 'keydown', function (event) {
            me._onKeyDown(event);
        });

    // move focus to the first button in the context menu
    this.anchor = anchor;
    setTimeout(function () {
        me.dom.focusButton.focus();
    }, 0);

    if (jsoneditor.ContextMenu.visibleMenu) {
        jsoneditor.ContextMenu.visibleMenu.hide();
    }
    jsoneditor.ContextMenu.visibleMenu = this;
};

/**
 * Hide the context menu if visible
 */
jsoneditor.ContextMenu.prototype.hide = function () {
    // move focus to the anchor
    if (this.anchor) {
        this.anchor.focus();
    }

    // remove the menu from the DOM
    if (this.dom.menu.parentNode) {
        this.dom.menu.parentNode.removeChild(this.dom.menu);
        if (this.onClose) {
            this.onClose();
        }
    }

    // remove all event listeners
    // all event listeners are supposed to be attached to document.
    for (var name in this.eventListeners) {
        if (this.eventListeners.hasOwnProperty(name)) {
            var fn = this.eventListeners[name];
            if (fn) {
                jsoneditor.util.removeEventListener(document, name, fn);
            }
            delete this.eventListeners[name];
        }
    }

    if (jsoneditor.ContextMenu.visibleMenu == this) {
        jsoneditor.ContextMenu.visibleMenu = undefined;
    }
};

/**
 * Expand a submenu
 * Any currently expanded submenu will be hided.
 * @param {Object} domItem
 * @private
 */
jsoneditor.ContextMenu.prototype._onExpandItem = function (domItem) {
    var me = this;
    var alreadyVisible = (domItem == this.expandedItem);

    // hide the currently visible submenu
    var expandedItem = this.expandedItem;
    if (expandedItem) {
        //var ul = expandedItem.ul;
        expandedItem.ul.style.height = '0';
        expandedItem.ul.style.padding = '';
        setTimeout(function () {
            if (me.expandedItem != expandedItem) {
                expandedItem.ul.style.display = '';
                jsoneditor.util.removeClassName(expandedItem.ul.parentNode, 'selected');
            }
        }, 300); // timeout duration must match the css transition duration
        this.expandedItem = undefined;
    }

    if (!alreadyVisible) {
        var ul = domItem.ul;
        ul.style.display = 'block';
        var height = ul.clientHeight; // force a reflow in Firefox
        setTimeout(function () {
            if (me.expandedItem == domItem) {
                ul.style.height = (ul.childNodes.length * 24) + 'px';
                ul.style.padding = '5px 10px';
            }
        }, 0);
        jsoneditor.util.addClassName(ul.parentNode, 'selected');
        this.expandedItem = domItem;
    }
};

/**
 * Handle onkeydown event
 * @param {Event} event
 * @private
 */
jsoneditor.ContextMenu.prototype._onKeyDown = function (event) {
    event = event || window.event;
    var target = event.target || event.srcElement;
    var keynum = event.which || event.keyCode;
    var handled = false;
    var buttons, targetIndex, prevButton, nextButton;

    if (keynum == 27) { // ESC
        // hide the menu on ESC key
        this.hide();
        handled = true;<commit_msg>Set focus to anchor only when closing via a shortcut key.<commit_after>/**
 * @license
 * 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.
 *
 * Copyright (c) 2011-2013 <NAME>, http://jsoneditoronline.org
 *
 * @author  <NAME>, <<EMAIL>>
 */

// create namespace
var jsoneditor = jsoneditor || {};

/**
 * A context menu
 * @param {Object[]} items    Array containing the menu structure
 *                            TODO: describe structure
 * @param {Object} [options]  Object with options. Available options:
 *                            {function} close    Callback called when the
 *                                                context menu is being closed.
 * @constructor
 */
jsoneditor.ContextMenu = function (items, options) {
    this.dom = {};

    var me = this;
    var dom = this.dom;
    this.anchor = undefined;
    this.items = items;
    this.eventListeners = {};
    this.visibleSubmenu = undefined;
    this.onClose = options ? options.close : undefined;

    // create a container element
    var menu = document.createElement('div');
    menu.className = 'jsoneditor-contextmenu';
    dom.menu = menu;

    // create a list to hold the menu items
    var list = document.createElement('ul');
    list.className = 'menu';
    menu.appendChild(list);
    dom.list = list;
    dom.items = []; // list with all buttons

    // create a (non-visible) button to set the focus to the menu
    var focusButton = document.createElement('button');
    dom.focusButton = focusButton;
    var li = document.createElement('li');
    li.style.overflow = 'hidden';
    li.style.height = '0';
    li.appendChild(focusButton);
    list.appendChild(li);

    function createMenuItems (list, domItems, items) {
        items.forEach(function (item) {
            if (item.type == 'separator') {
                // create a separator
                var separator = document.createElement('div');
                separator.className = 'separator';
                li = document.createElement('li');
                li.appendChild(separator);
                list.appendChild(li);
            }
            else {
                var domItem = {};

                // create a menu item
                var li = document.createElement('li');
                list.appendChild(li);

                // create a button in the menu item
                var button = document.createElement('button');
                button.className = item.className;
                domItem.button = button;
                if (item.title) {
                    button.title = item.title;
                }
                if (item.click) {
                    button.onclick = function () {
                        me.hide();
                        item.click();
                    };
                }
                li.appendChild(button);

                // create the contents of the button
                if (item.submenu) {
                    // add the icon to the button
                    var divIcon = document.createElement('div');
                    divIcon.className = 'icon';
                    button.appendChild(divIcon);
                    button.appendChild(document.createTextNode(item.text));

                    var buttonSubmenu;
                    if (item.click) {
                        // submenu and a button with a click handler
                        button.className += ' default';

                        var buttonExpand = document.createElement('button');
                        domItem.buttonExpand = buttonExpand;
                        buttonExpand.className = 'expand';
                        buttonExpand.innerHTML = '<div class="expand"></div>';
                        li.appendChild(buttonExpand);
                        if (item.submenuTitle) {
                            buttonExpand.title = item.submenuTitle;
                        }

                        buttonSubmenu = buttonExpand;
                    }
                    else {
                        // submenu and a button without a click handler
                        var divExpand = document.createElement('div');
                        divExpand.className = 'expand';
                        button.appendChild(divExpand);

                        buttonSubmenu = button;
                    }

                    // attach a handler to expand/collapse the submenu
                    buttonSubmenu.onclick = function () {
                        me._onExpandItem(domItem);
                        buttonSubmenu.focus();
                    };

                    // create the submenu
                    var domSubItems = [];
                    domItem.subItems = domSubItems;
                    var ul = document.createElement('ul');
                    domItem.ul = ul;
                    ul.className = 'menu';
                    ul.style.height = '0';
                    li.appendChild(ul);
                    createMenuItems(ul, domSubItems, item.submenu);
                }
                else {
                    // no submenu, just a button with clickhandler
                    button.innerHTML = '<div class="icon"></div>' + item.text;
                }

                domItems.push(domItem);
            }
        });
    }
    createMenuItems(list, this.dom.items, items);

    // TODO: when the editor is small, show the submenu on the right instead of inline?

    // calculate the max height of the menu with one submenu expanded
    this.maxHeight = 0; // height in pixels
    items.forEach(function (item) {
        var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
        me.maxHeight = Math.max(me.maxHeight, height);
    });
};

/**
 * Get the currently visible buttons
 * @return {Array.<HTMLElement>} buttons
 * @private
 */
jsoneditor.ContextMenu.prototype._getVisibleButtons = function () {
    var buttons = [];
    var me = this;
    this.dom.items.forEach(function (item) {
        buttons.push(item.button);
        if (item.buttonExpand) {
            buttons.push(item.buttonExpand);
        }
        if (item.subItems && item == me.expandedItem) {
            item.subItems.forEach(function (subItem) {
                buttons.push(subItem.button);
                if (subItem.buttonExpand) {
                    buttons.push(subItem.buttonExpand);
                }
                // TODO: change to fully recursive method
            });
        }
    });

    return buttons;
};

// currently displayed context menu, a singleton. We may only have one visible context menu
jsoneditor.ContextMenu.visibleMenu = undefined;

/**
 * Attach the menu to an anchor
 * @param {HTMLElement} anchor
 */
jsoneditor.ContextMenu.prototype.show = function (anchor) {
    this.hide();

    // calculate whether the menu fits below the anchor
    var windowHeight = jsoneditor.util.getWindowHeight();
    var anchorHeight = anchor.offsetHeight;
    var menuHeight = this.maxHeight;

    // position the menu
    var left = jsoneditor.util.getAbsoluteLeft(anchor);
    var top = jsoneditor.util.getAbsoluteTop(anchor);
    if (top + anchorHeight + menuHeight < windowHeight) {
        // display the menu below the anchor
        this.dom.menu.style.left = left + 'px';
        this.dom.menu.style.top = (top + anchorHeight) + 'px';
        this.dom.menu.style.bottom = '';
    }
    else {
        // display the menu above the anchor
        this.dom.menu.style.left = left + 'px';
        this.dom.menu.style.top = '';
        this.dom.menu.style.bottom = (windowHeight - top) + 'px';
    }

    // attach the menu to the document
    document.body.appendChild(this.dom.menu);

    // create and attach event listeners
    var me = this;
    var list = this.dom.list;
    this.eventListeners.mousedown = jsoneditor.util.addEventListener(
        document, 'mousedown', function (event) {
            // hide menu on click outside of the menu
            event = event || window.event;
            var target = event.target || event.srcElement;
            if ((target != list) && !me._isChildOf(target, list)) {
                me.hide();
                jsoneditor.util.stopPropagation(event);
                jsoneditor.util.preventDefault(event);
            }
        });
    this.eventListeners.mousewheel = jsoneditor.util.addEventListener(
        document, 'mousewheel', function () {
            // hide the menu on mouse scroll
            jsoneditor.util.stopPropagation(event);
            jsoneditor.util.preventDefault(event);
        });
    this.eventListeners.keydown = jsoneditor.util.addEventListener(
        document, 'keydown', function (event) {
            me._onKeyDown(event);
        });

    // move focus to the first button in the context menu
    this.anchor = anchor;
    setTimeout(function () {
        me.dom.focusButton.focus();
    }, 0);

    if (jsoneditor.ContextMenu.visibleMenu) {
        jsoneditor.ContextMenu.visibleMenu.hide();
    }
    jsoneditor.ContextMenu.visibleMenu = this;
};

/**
 * Hide the context menu if visible
 */
jsoneditor.ContextMenu.prototype.hide = function () {
    // remove the menu from the DOM
    if (this.dom.menu.parentNode) {
        this.dom.menu.parentNode.removeChild(this.dom.menu);
        if (this.onClose) {
            this.onClose();
        }
    }

    // remove all event listeners
    // all event listeners are supposed to be attached to document.
    for (var name in this.eventListeners) {
        if (this.eventListeners.hasOwnProperty(name)) {
            var fn = this.eventListeners[name];
            if (fn) {
                jsoneditor.util.removeEventListener(document, name, fn);
            }
            delete this.eventListeners[name];
        }
    }

    if (jsoneditor.ContextMenu.visibleMenu == this) {
        jsoneditor.ContextMenu.visibleMenu = undefined;
    }
};

/**
 * Expand a submenu
 * Any currently expanded submenu will be hided.
 * @param {Object} domItem
 * @private
 */
jsoneditor.ContextMenu.prototype._onExpandItem = function (domItem) {
    var me = this;
    var alreadyVisible = (domItem == this.expandedItem);

    // hide the currently visible submenu
    var expandedItem = this.expandedItem;
    if (expandedItem) {
        //var ul = expandedItem.ul;
        expandedItem.ul.style.height = '0';
        expandedItem.ul.style.padding = '';
        setTimeout(function () {
            if (me.expandedItem != expandedItem) {
                expandedItem.ul.style.display = '';
                jsoneditor.util.removeClassName(expandedItem.ul.parentNode, 'selected');
            }
        }, 300); // timeout duration must match the css transition duration
        this.expandedItem = undefined;
    }

    if (!alreadyVisible) {
        var ul = domItem.ul;
        ul.style.display = 'block';
        var height = ul.clientHeight; // force a reflow in Firefox
        setTimeout(function () {
            if (me.expandedItem == domItem) {
                ul.style.height = (ul.childNodes.length * 24) + 'px';
                ul.style.padding = '5px 10px';
            }
        }, 0);
        jsoneditor.util.addClassName(ul.parentNode, 'selected');
        this.expandedItem = domItem;
    }
};

/**
 * Handle onkeydown event
 * @param {Event} event
 * @private
 */
jsoneditor.ContextMenu.prototype._onKeyDown = function (event) {
    event = event || window.event;
    var target = event.target || event.srcElement;
    var keynum = event.which || event.keyCode;
    var handled = false;
    var buttons, targetIndex, prevButton, nextButton;

    if (keynum == 27) { // ESC
        // hide the menu on ESC key

        // move focus to the anchor
        if (this.anchor) {
            this.anchor.focus();
        }

        this.hide();

        handled = true;
{'fraction_non_alphanumeric': 0.10583673469387755, 'fraction_numerical': 0.0025306122448979593, 'mean_word_length': 1.7535401213755901, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 21, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3193454', 'n_tokens_mistral': 6402, 'n_tokens_neox': 6113, 'n_words': 1677}