VulpesOS/assets/webapps/communications.gaiamobile.org/shared/js/option_menu.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

214 lines
5.8 KiB
JavaScript

'use strict';
/*
Generic action menu. Options should have the following structure:
new OptionMenu(options);
options {
items: An array of menu options to render
eg.
[
{
name: 'Lorem ipsum',
l10nId: 'lorem',
l10nArgs: 'ipsum',
method: function optionMethod(param1, param2) {
// Method and params if needed
},
params: ['param1', '123123123']
},
....
,
Last option has a different UI compared with the previous one.
This is because it's recommended to use as a 'Cancel' option
{
name: 'Cancel',
l10nId: 'Cancel'
method: function optionMethod(param) {
// Method and param if needed
},
params: ['Optional params'],
// Optional boolean flag to tell the
// menu button handlers that this option
// will not execute the "complete" callback.
// Defaults to "false"
incomplete: false [true]
}
],
// Optional header text or node
header: ...,
// additional classes on the dialog, as an array of strings
classes: ...
// Optional section text or node
section: ...
// Optional data-type: confirm or action
type: 'confirm'
// Optional callback to be invoked when a
// button in the menu is pressed. Can be
// overridden by an "incomplete: true" set
// on the menu item in the items array.
complete: function() {...}
}
*/
var OptionMenu = function(options) {
if (!options || !options.items || options.items.length === 0) {
return;
}
// Create a private, weakly held entry for
// this instances DOM object references
// More info:
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/WeakMap
var handlers = new WeakMap();
// Retrieve items to be rendered
var items = options.items;
// Create the structure
this.form = document.createElement('form');
this.form.dataset.type = options.type || 'action';
this.form.dataset.subtype = 'menu';
this.form.setAttribute('role', 'dialog');
this.form.tabIndex = -1;
var classList = this.form.classList;
if (options.classes) {
classList.add.apply(classList, options.classes);
}
// We append title if needed
if (options.header) {
var header = document.createElement('header');
if (typeof options.header === 'string') {
header.textContent = options.header || '';
} else if (options.header.l10nId) {
header.setAttribute('data-l10n-id', options.header.l10nId);
} else {
header.appendChild(options.header);
}
this.form.appendChild(header);
}
if (options.section) {
var section = document.createElement('section');
if (typeof options.section === 'string') {
section.textContent = options.section || '';
} else {
section.appendChild(options.section);
}
this.form.appendChild(section);
}
// We append a menu with the list of options
var menu = document.createElement('menu');
menu.dataset.items = items.length;
// For each option, we append the item and listener
items.forEach(function renderOption(item, index) {
var button = document.createElement('button');
button.type = 'button';
if (item.l10nId) {
navigator.mozL10n.setAttributes(button, item.l10nId, item.l10nArgs);
} else if (item.name && item.name.length) {
button.textContent = item.name || '';
} else {
// no l10n or name, just empty item, don't add to the menu
return;
}
// Wrap the last button in a container in order to style it differently.
if (index === items.length - 1) {
var lastButtonWrapper = document.createElement('span');
lastButtonWrapper.className = 'last-button-container';
menu.appendChild(lastButtonWrapper);
lastButtonWrapper.appendChild(button);
} else {
menu.appendChild(button);
}
// Add a mapping from the button object
// directly to its options item.
item.incomplete = item.incomplete || false;
handlers.set(button, item);
});
this.form.addEventListener('submit', function(event) {
event.preventDefault();
});
this.form.addEventListener('transitionend', function(event) {
if (event.target !== this.form) {
return;
}
if (!this.form.classList.contains('visible') && this.form.parentNode) {
this.form.remove();
} else {
// Focus form for accessibility
this.form.focus();
}
document.body.classList.remove('dialog-animating');
}.bind(this));
menu.addEventListener('click', function(event) {
var action = handlers.get(event.target);
var method;
// Delegate operation to target method. This allows
// for a custom "Cancel" to be provided by calling program.
//
// Further operations should only be processed if
// an actual button was pressed.
if (typeof action !== 'undefined') {
method = action.method || function() {};
method.apply(null, action.params || []);
// Hide action menu when click is received
this.hide();
if (typeof options.complete === 'function' && !action.incomplete) {
options.complete();
}
}
}.bind(this));
// Appending the action menu to the form
this.form.appendChild(menu);
};
// We prototype functions to show/hide the UI of action-menu
OptionMenu.prototype.show = function() {
// Remove the focus to hide the keyboard asap
document.activeElement && document.activeElement.blur();
if (!this.form.parentNode) {
document.body.appendChild(this.form);
// Flush style on form so that the show transition plays once we add
// the visible class.
this.form.clientTop;
}
this.form.classList.add('visible');
document.body.classList.add('dialog-animating');
};
OptionMenu.prototype.hide = function() {
this.form.classList.remove('visible');
};