Source: classes/ContextMenuHtml5Builder.js

/**
 * @class ContextMenuHtml5Builder
 * @classdesc considering the following HTML `$.contextMenu.fromMenu($('#html5menu'))` will return a proper items object.

 ```
 <menu id="html5menu" type="context" style="display:none">
    <command label="rotate" onclick="alert('rotate')">
    <command label="resize" onclick="alert('resize')">
    <menu label="share">
        <command label="twitter" onclick="alert('twitter')">
        <hr>
        <command label="facebook" onclick="alert('facebook')">
    </menu>
 </menu>
 ```

 `$.contextMenu.fromMenu()` will properly import (and thus handle) the following elements. Everything else is imported as `{type: "html"}`

 ```
 <menu>
 <hr>
 <a>
 <command type="command|radio|checkbox"> (W3C Specification)
 <menuitem type="command|radio|checkbox"> (Firefox)
 <input type="text|radio|checkbox">
 <select>
 <textarea>
 <label for="someId">
 <label> the text <input|textarea|select>
 ```

 The `<menu>` must be hidden but not removed, as all command events (clicks) are passed-thru to the original command element!

 Note: While the specs note `<option>`s to be renderd as regular commands, `$.contextMenu` will render an actual `<select>`.

 ## HTML5 `<menu>` shiv/polyfill

 Engaging the HTML5 polyfill (ignoring `$.contextMenu` if context menus are available natively):

 ```
 $(function(){
    $.contextMenu("html5");
 });
 ```

 Engaging the HTML5 polyfill (ignoring browser native implementation):

 ```
 $(function(){
    $.contextMenu("html5", true);
 });
 ```

 */
export default class ContextMenuHtml5Builder {
    /**
     * Get the input label for the given node.
     *
     * @method inputLabel
     * @memberOf ContextMenuHtml5Builder
     * @instance
     *
     * @param {HTMLElement} node - Html element
     * @returns {string|JQuery|jQuery} - Input label element
     */
    inputLabel(node) {
        return (node.id && $('label[for="' + node.id + '"]').val()) || node.name;
    }

    /**
     * Helper function to build ContextMenuItems from an html5 menu element.
     *
     * @method fromMenu
     * @memberOf ContextMenuHtml5Builder
     * @instance
     *
     * @param {JQuery|string} element - Menu element to generate the menu from.
     * @returns {Object.<string, ContextMenuItem>} - Collection of {@link ContextMenuItem}
     */
    fromMenu(element) {
        const $this = $(element);
        const items = {};

        this.build(items, $this.children());

        return items;
    }

    /**
     * Helper function for building a menu from a HTML menu element.
     *
     * @method build
     * @memberOf ContextMenuHtml5Builder
     * @instance
     *
     * @param {Object.<string, ContextMenuItem>} items - {@link ContextMenuItem} object to build.
     * @param {(JQuery)} $children - Collection of elements inside the `<menu>` element
     * @param {number?} counter - Counter to generate {@link ContextMenuItem} key names.
     * @returns {number} - Counter to generate {@link ContextMenuItem} key names.
     */
    build(items, $children, counter) {
        if (!counter) {
            counter = 0;
        }

        let builder = this;

        $children.each(function () {
            let $node = $(this);
            let node = this;
            let nodeName = this.nodeName.toLowerCase();
            let label;
            let item;

            // extract <label><input>
            if (nodeName === 'label' && $node.find('input, textarea, select').length) {
                label = $node.text();
                $node = $node.children().first();
                node = $node.get(0);
                nodeName = node.nodeName.toLowerCase();
            }

            /*
             * <menu> accepts flow-content as children. that means <embed>, <canvas> and such are valid menu items.
             * Not being the sadistic kind, $.contextMenu only accepts:
             * <command>, <menuitem>, <hr>, <span>, <p> <input [text, radio, checkbox]>, <textarea>, <select> and of course <menu>.
             * Everything else will be imported as an html node, which is not interfaced with contextMenu.
             */

            // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#concept-command
            switch (nodeName) {
                // http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#the-menu-element
                case 'menu':
                    item = {name: $node.attr('label'), items: {}};
                    counter = builder.build(item.items, $node.children(), counter);
                    break;

                // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-a-element-to-define-a-command
                // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-button-element-to-define-a-command
                case 'a':
                case 'button':
                    item = {
                        name: $node.text(),
                        disabled: !!$node.attr('disabled'),
                        callback: (function () {
                            return function () {
                                $node.get(0).click();
                            };
                        })()
                    };
                    break;

                // http://www.whatwg.org/specs/web-apps/current-work/multipage/commands.html#using-the-command-element-to-define-a-command
                case 'menuitem':
                case 'command':
                    switch ($node.attr('type')) {
                        case undefined:
                        case 'command':
                        case 'menuitem':
                            item = {
                                name: $node.attr('label'),
                                disabled: !!$node.attr('disabled'),
                                icon: $node.attr('icon'),
                                callback: (function () {
                                    return function () {
                                        $node.get(0).click();
                                    };
                                })()
                            };
                            break;

                        case 'checkbox':
                            item = {
                                type: 'checkbox',
                                disabled: !!$node.attr('disabled'),
                                name: $node.attr('label'),
                                selected: !!$node.attr('checked')
                            };
                            break;
                        case 'radio':
                            item = {
                                type: 'radio',
                                disabled: !!$node.attr('disabled'),
                                name: $node.attr('label'),
                                radio: $node.attr('radiogroup'),
                                value: $node.attr('id'),
                                selected: !!$node.attr('checked')
                            };
                            break;

                        default:
                            item = undefined;
                    }
                    break;

                case 'hr':
                    item = '-------';
                    break;

                case 'input':
                    switch ($node.attr('type')) {
                        case 'text':
                            item = {
                                type: 'text',
                                name: label || builder.inputLabel(node),
                                disabled: !!$node.attr('disabled'),
                                value: $node.val()
                            };
                            break;

                        case 'checkbox':
                            item = {
                                type: 'checkbox',
                                name: label || builder.inputLabel(node),
                                disabled: !!$node.attr('disabled'),
                                selected: !!$node.attr('checked')
                            };
                            break;

                        case 'radio':
                            item = {
                                type: 'radio',
                                name: label || builder.inputLabel(node),
                                disabled: !!$node.attr('disabled'),
                                radio: !!$node.attr('name'),
                                value: $node.val(),
                                selected: !!$node.attr('checked')
                            };
                            break;

                        default:
                            item = undefined;
                            break;
                    }
                    break;

                case 'select':
                    item = {
                        type: 'select',
                        name: label || builder.inputLabel(node),
                        disabled: !!$node.attr('disabled'),
                        selected: $node.val(),
                        options: {}
                    };
                    $node.children().each(function () {
                        item.options[this.value] = $(this).text();
                    });
                    break;

                case 'textarea':
                    item = {
                        type: 'textarea',
                        name: label || builder.inputLabel(node),
                        disabled: !!$node.attr('disabled'),
                        value: $node.val()
                    };
                    break;

                case 'label':
                    break;

                default:
                    item = {type: 'html', html: $node.clone(true)};
                    break;
            }

            if (item) {
                counter++;
                items['key' + counter] = item;
            }
        });

        return counter;
    }
}