pvbemu/app/toolkit/Button.js

388 lines
10 KiB
JavaScript

import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Button //
///////////////////////////////////////////////////////////////////////////////
// Push, toggle or radio button
class Button extends Component {
static Component = Component;
//////////////////////////////// Constants ////////////////////////////////
// Types
static BUTTON = 0;
static RADIO = 1;
static TOGGLE = 2;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-button",
focusable: true,
role : "button",
tagName : "div",
style : {
display : "inline-block",
userSelect: "none"
}
});
// Configure instance fields
options = options || {};
this.attribute = options.attribute || "aria-pressed";
this.group = null;
this.isEnabled = null;
this.isSelected = false;
this.text = null;
this.type = Button.BUTTON;
// Configure contents
this.contents = document.createElement("div");
this.append(this.contents);
// Configure component
this.setEnabled(!("enabled" in options) || options.enabled);
if ("group" in options)
options.group.add(this);
this.setText (options.text);
this.setType (options.type);
if ("selected" in options)
this.setSelected(options.selected);
// Configure event handlers
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("pointermove", e=>this.onPointerMove(e));
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Processing by key
switch (e.key) {
case "Enter": // Fallthrough
case " " :
this.click();
break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Pointer down
onPointerDown(e) {
this.focus();
// Error checking
if (
!this.isEnabled ||
this.element.hasPointerCapture(e.pointerId) ||
e.button != 0
) return;
// Configure event
this.element.setPointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Configure component
this.element.classList.add("active");
}
// Pointer move
onPointerMove(e) {
// Error checking
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure event
e.stopPropagation();
e.preventDefault();
// Configure component
this.element.classList[
Toolkit.isInside(this.element, e) ? "add" : "remove"]("active");
}
// Pointer up
onPointerUp(e) {
// Error checking
if (
!this.isEnabled ||
e.button != 0 ||
!this.element.hasPointerCapture(e.pointerId)
) return;
// Configure event
this.element.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Configure component
this.element.classList.remove("active");
// Item is an action
let bounds = this.getBounds();
if (this.menu == null && Toolkit.isInside(this.element, e))
this.click();
}
///////////////////////////// Public Methods //////////////////////////////
// Programmatically activate the button
click() {
if (this instanceof Toolkit.CheckBox)
this.setSelected(this instanceof Toolkit.Radio||!this.isSelected);
this.event("action");
}
// Specify whether the button can be activated
setEnabled(enabled) {
this.isEnabled = enabled = !!enabled;
this.setAttribute("aria-disabled", enabled ? null : "true");
}
// Specify whether the toggle or radio button is selected
setSelected(selected) {
selected = !!selected;
// Take no action
if (selected == this.isSelected)
return;
// Processing by button type
switch (this.type) {
case Button.RADIO :
if (selected && this.group != null)
this.group.deselect();
// Fallthrough
case Button.TOGGLE:
this.isSelected = selected;
this.setAttribute(this.attribute, selected);
}
}
// Specify the widget's display text
setText(text) {
this.text = (text || "").toString().trim();
this.translate();
}
// Specify what kind of button this is
setType(type) {
switch (type) {
case Button.BUTTON:
this.type = type;
this.setAttribute(this.attribute, null);
this.setSelected(false);
break;
case Button.RADIO : // Fallthrough
case Button.TOGGLE:
this.type = type;
this.setAttribute(this.attribute, this.isSelected);
this.setSelected(this.isSelected);
break;
default: return;
}
}
///////////////////////////// Package Methods /////////////////////////////
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
super.translate();
if (this.contents != null)
this.contents.innerText = this.gui.translate(this.text, this);
}
}
///////////////////////////////////////////////////////////////////////////////
// CheckBox //
///////////////////////////////////////////////////////////////////////////////
// On/off toggle box
class CheckBox extends Button {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options) {
// Default options override
let uptions = {};
Object.assign(uptions, options || {});
for (let entry of Object.entries({
attribute: "aria-checked",
className: "tk tk-checkbox",
role : "checkbox",
style : {},
type : Button.TOGGLE
})) if (!(entry[0] in uptions))
uptions[entry[0]] = entry[1];
// Default styles override
for (let entry of Object.entries({
display : "inline-grid",
gridTemplateColumns: "max-content auto"
})) if (!(entry[0] in uptions.style))
uptions.style[entry[0]] = entry[1];
// Component overrides
super(app, uptions);
this.contents.classList.add("tk-contents");
// Configure icon
this.icon = document.createElement("div");
this.icon.className = "tk tk-icon";
this.icon.setAttribute("aria-hidden", "true");
this.prepend(this.icon);
}
}
///////////////////////////////////////////////////////////////////////////////
// Radio //
///////////////////////////////////////////////////////////////////////////////
// Single selection box
class Radio extends CheckBox {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options) {
// Default options override
let uptions = {};
Object.assign(uptions, options || {});
for (let entry of Object.entries({
className: "tk tk-radio",
role : "radio",
type : Button.RADIO
})) if (!(entry[0] in uptions))
uptions[entry[0]] = entry[1];
// Component overrides
super(app, uptions);
}
}
///////////////////////////////////////////////////////////////////////////////
// Group //
///////////////////////////////////////////////////////////////////////////////
// Radio button or menu item group
class Group extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app) {
super(app, {
tagName: "div",
style : {
height : "0",
position: "absolute",
width : "0"
}
});
// Configure instance fields
this.items = [];
}
///////////////////////////// Public Methods //////////////////////////////
// Add an item
add(item) {
// Error checking
if (!Toolkit.isComponent(item) || this.items.indexOf(item) != -1)
return item;
// Configure component
this.setAttribute("role",
item instanceof Toolkit.Radio ? "radiogroup" : "group");
// Configure item
if (item.group != null)
item.group.remove(item);
item.group = this;
// Add the item to the collection
item.id = item.id || Toolkit.id();
this.items.push(item);
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
return item;
}
// Remove all items
clear() {
this.items.splice();
this.setAttribute("aria-owns", "");
}
// Un-check all items in the group
deselect() {
for (let item of this.items)
if (item.isSelected && "setSelected" in item)
item.setSelected(false);
}
// Remove an item
remove(item) {
// Error checking
let index = this.items.indexOf(item);
if (index == -1)
return;
// Remove the item from the collection
this.items.splice(index, 1);
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
item.group = null;
return item;
}
}
export { Button, CheckBox, Group, Radio };