/**
* @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;
}
}