Introducing toolkit and color themes
This commit is contained in:
parent
0927a42107
commit
dd066e0bbb
|
@ -1,13 +1,10 @@
|
|||
* text=auto
|
||||
|
||||
*.c text diff=c
|
||||
*.css text diff=css
|
||||
*.h text diff=c
|
||||
*.html text diff=html
|
||||
*.java text diff=java
|
||||
*.js text diff=js
|
||||
*.sample text
|
||||
*.txt text
|
||||
*.c text eol=lf diff=c
|
||||
*.css text eol=lf diff=css
|
||||
*.h text eol=lf diff=c
|
||||
*.html text eol=lf diff=html
|
||||
*.java text eol=lf diff=java
|
||||
*.js text eol=lf diff=js
|
||||
*.txt text eol=lf
|
||||
|
||||
*.class binary
|
||||
*.dll binary
|
||||
|
|
103
app/App.js
103
app/App.js
|
@ -1,5 +1,108 @@
|
|||
"use strict";
|
||||
|
||||
// Top-level state and UI manager
|
||||
globalThis.App = class App {
|
||||
|
||||
// Object constructor
|
||||
constructor() {
|
||||
|
||||
// Configure themes
|
||||
Bundle.get("app/theme/base.css").style();
|
||||
this.themes = {
|
||||
dark : Bundle.get("app/theme/dark.css" ).style(false),
|
||||
light : Bundle.get("app/theme/light.css" ).style(true ),
|
||||
virtual: Bundle.get("app/theme/virtual.css").style(false)
|
||||
};
|
||||
this.theme = this.themes["light"];
|
||||
|
||||
// Produce toolkit instance
|
||||
this.gui = new Toolkit.Application();
|
||||
document.body.appendChild(this.gui.element);
|
||||
window.addEventListener("resize", ()=>{
|
||||
this.gui.element.style.height = window.innerHeight + "px";
|
||||
this.gui.element.style.width = window.innerWidth + "px";
|
||||
});
|
||||
|
||||
// Configure locales
|
||||
this.gui.addLocale(Bundle.get("app/locale/en-US.js").toString());
|
||||
this.gui.setLocale(navigator.language);
|
||||
|
||||
// Configure GUI
|
||||
this.gui.setSplitLayout("top", false);
|
||||
|
||||
// Menu bar
|
||||
this.mainMenu = this.gui.newMenuBar({ name: "{menu._}" });
|
||||
this.gui.add(this.mainMenu);
|
||||
|
||||
// File menu
|
||||
let menu = this.mainMenu.newMenu({ text: "{menu.file._}"});
|
||||
let item = menu.newMenuItem({ text: "{menu.file.loadROM}"});
|
||||
item.addClickListener(()=>this.loadROM());
|
||||
|
||||
// Theme menu
|
||||
menu = this.mainMenu.newMenu({ text: "{menu.theme._}"});
|
||||
item = menu.newMenuItem({ text: "{menu.theme.light}"});
|
||||
item.addClickListener(()=>this.setTheme("light"));
|
||||
item = menu.newMenuItem({ text: "{menu.theme.dark}"});
|
||||
item.addClickListener(()=>this.setTheme("dark"));
|
||||
item = menu.newMenuItem({ text: "{menu.theme.virtual}"});
|
||||
item.addClickListener(()=>this.setTheme("virtual"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Prompt the user to select a ROM file
|
||||
loadROM() {
|
||||
let file = document.createElement("input");
|
||||
file.type = "file";
|
||||
file.addEventListener("input", ()=>this.setROM(file.files[0]));
|
||||
file.click();
|
||||
}
|
||||
|
||||
// Specify a ROM file
|
||||
async setROM(file) {
|
||||
|
||||
// No file is specified (perhaps the user canceled)
|
||||
if (file == null)
|
||||
return;
|
||||
|
||||
// Check the file's size
|
||||
if (
|
||||
file.size < 1024 ||
|
||||
file.size > 0x1000000 ||
|
||||
(file.size - 1 & file.size) != 0
|
||||
) {
|
||||
alert(this.gui.translate("{app.romNotVB}"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the file data into a byte buffer
|
||||
let filename = file.name;
|
||||
try { file = new Uint8Array(await file.arrayBuffer()); }
|
||||
catch {
|
||||
alert(this.gui.translate("{app.readFileError}"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Testing output pending further features
|
||||
alert(this.gui.translate("{app.romLoaded}", {
|
||||
filename: filename,
|
||||
size : file.length + " byte" + (file.length == 1 ? "" : "s")
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
// Specify the current color theme
|
||||
setTheme(key) {
|
||||
let theme = this.themes[key];
|
||||
if (theme == this.theme)
|
||||
return;
|
||||
let old = this.theme;
|
||||
this.theme = theme;
|
||||
theme.setEnabled(true);
|
||||
old.setEnabled(false);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
32
app/_boot.js
32
app/_boot.js
|
@ -34,8 +34,8 @@ globalThis.Bundle = class BundledFile {
|
|||
await Bundle.files[name].run();
|
||||
}
|
||||
|
||||
// Resolve a URL for a script source file
|
||||
static script(name) {
|
||||
// Resolve a URL for a source file
|
||||
static source(name) {
|
||||
return debug ? name : Bundle.files[name].toDataURL();
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ globalThis.Bundle = class BundledFile {
|
|||
}
|
||||
|
||||
// Running in debug mode
|
||||
return new Promise((resolve,reject)=>{
|
||||
await new Promise((resolve,reject)=>{
|
||||
let script = document.createElement("script");
|
||||
document.head.appendChild(script);
|
||||
script.addEventListener("load", ()=>resolve());
|
||||
|
@ -88,6 +88,22 @@ globalThis.Bundle = class BundledFile {
|
|||
|
||||
}
|
||||
|
||||
// Register the file as a CSS stylesheet
|
||||
style(enabled) {
|
||||
let link = document.createElement("link");
|
||||
link.href = debug ? this.name : this.toDataURL();
|
||||
link.rel = "stylesheet";
|
||||
link.type = "text/css";
|
||||
link.setEnabled = enabled=>{
|
||||
if (enabled)
|
||||
link.removeAttribute("disabled");
|
||||
else link.setAttribute("disabled", null);
|
||||
};
|
||||
link.setEnabled(enabled === undefined || !!enabled);
|
||||
document.head.appendChild(link);
|
||||
return link;
|
||||
}
|
||||
|
||||
// Produce a blob from the file data
|
||||
toBlob() {
|
||||
return new Blob(this.data, { type: this.mime });
|
||||
|
@ -268,7 +284,15 @@ for (let file of manifest) {
|
|||
|
||||
// Program startup
|
||||
let run = async function() {
|
||||
Bundle.run("app/App.js");
|
||||
await Bundle.run("app/App.js");
|
||||
await Bundle.run("app/toolkit/Toolkit.js");
|
||||
await Bundle.run("app/toolkit/Component.js");
|
||||
await Bundle.run("app/toolkit/Panel.js");
|
||||
await Bundle.run("app/toolkit/Application.js");
|
||||
await Bundle.run("app/toolkit/Button.js");
|
||||
await Bundle.run("app/toolkit/MenuBar.js");
|
||||
await Bundle.run("app/toolkit/MenuItem.js");
|
||||
await Bundle.run("app/toolkit/Menu.js");
|
||||
new App();
|
||||
};
|
||||
run();
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
key : "en-US",
|
||||
name: "English (United States)",
|
||||
app : {
|
||||
romLoaded : "Successfully loaded file \"{filename}\" ({size})",
|
||||
romNotVB : "The selected file is not a Virtual Boy ROM.",
|
||||
readFileError: "Unable to read the selected file."
|
||||
},
|
||||
menu: {
|
||||
_ : "Main application menu",
|
||||
file: {
|
||||
_ : "File",
|
||||
loadROM: "Load ROM..."
|
||||
},
|
||||
theme: {
|
||||
_ : "Theme",
|
||||
dark : "Dark",
|
||||
light : "Light",
|
||||
virtual: "Virtual"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/* Common styles for all themes */
|
||||
|
||||
:root {
|
||||
color : var(--text);
|
||||
font-family: Arial, sans-serif;
|
||||
font-size : 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
margin : 0;
|
||||
overflow : hidden;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
box-shadow:
|
||||
0 0 0 1px var(--background),
|
||||
0 0 0 3px var(--focus-ring),
|
||||
0 0 0 4px var(--background);
|
||||
outline : none;
|
||||
z-index : 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************** Button ***********************************/
|
||||
|
||||
[role="button"] {
|
||||
align-items : center;
|
||||
background : var(--button);
|
||||
border-color :
|
||||
var(--border-light)
|
||||
var(--border-dark )
|
||||
var(--border-dark )
|
||||
var(--border-light)
|
||||
;
|
||||
border-radius : 3px;
|
||||
border-style : solid;
|
||||
border-width : 1px;
|
||||
display : flex;
|
||||
justify-content: center;
|
||||
padding : 4px;
|
||||
}
|
||||
|
||||
[role="button"][aria-pressed="true"]:not([active]) {
|
||||
background : var(--button-pressed);
|
||||
border-color:
|
||||
var(--border-dark )
|
||||
var(--border-light)
|
||||
var(--border-light)
|
||||
var(--border-dark )
|
||||
;
|
||||
padding: 5px 3px 3px 5px;
|
||||
}
|
||||
|
||||
[role="button"]:not([active]):hover {
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
[role="button"][active]:not([aria-pressed="true"]) {
|
||||
background : var(--button-pressed);
|
||||
border-color:
|
||||
var(--border-dark )
|
||||
var(--border-light)
|
||||
var(--border-light)
|
||||
var(--border-dark )
|
||||
;
|
||||
padding: 5px 3px 3px 5px;
|
||||
}
|
||||
|
||||
[role="button"][aria-pressed="true"][active] {
|
||||
background : var(--button-pressed);
|
||||
border-color:
|
||||
var(--border-dark )
|
||||
var(--border-light)
|
||||
var(--border-light)
|
||||
var(--border-dark )
|
||||
;
|
||||
padding: 6px 2px 2px 6px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************** MenuBar **********************************/
|
||||
|
||||
[role="menubar"] {
|
||||
border-bottom: 1px solid var(--border-weak);
|
||||
column-gap : 4px;
|
||||
padding : 4px;
|
||||
}
|
||||
|
||||
[role="menubar"] > [role="menuitem"] {
|
||||
border-radius: 3px;
|
||||
padding : 4px;
|
||||
}
|
||||
|
||||
[role="menubar"] > * [role="menuitem"] {
|
||||
border-radius: 3px;
|
||||
padding : 4px;
|
||||
}
|
||||
|
||||
[role="menubar"] [role="menuitem"]:hover,
|
||||
[role="menubar"] [role="menuitem"]:focus,
|
||||
[role="menubar"] [role="menuitem"][aria-expanded="true"] {
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
[role="menubar"] [role="menu"] {
|
||||
background : var(--background);
|
||||
border-radius: 4px;
|
||||
box-shadow :
|
||||
0 0 0 1px var(--border-strong),
|
||||
4px 4px 5px var(--shadow)
|
||||
;
|
||||
min-height : 16px;
|
||||
min-width : 16px;
|
||||
padding : 4px;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
:root {
|
||||
--background : #222222;
|
||||
--border-dark : #555555;
|
||||
--border-light : #999999;
|
||||
--border-weak : #666666;
|
||||
--border-strong : #999999;
|
||||
--button : #444444;
|
||||
--button-hover : #4c4c4c;
|
||||
--button-pressed: #555555;
|
||||
--focus-ring : #0099ff;
|
||||
--shadow : #00000080;
|
||||
--text : #cccccc;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
:root {
|
||||
--background : #ffffff;
|
||||
--border-dark : #666666;
|
||||
--border-light : #aaaaaa;
|
||||
--border-weak : #aaaaaa;
|
||||
--border-strong : #666666;
|
||||
--button : #cccccc;
|
||||
--button-hover : #d5d5d5;
|
||||
--button-pressed: #dddddd;
|
||||
--focus-ring : #0099ff;
|
||||
--shadow : #00000055;
|
||||
--text : #000000;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
:root {
|
||||
--background : #000000;
|
||||
--border-dark : #550000;
|
||||
--border-light : #aa0000;
|
||||
--border-weak : #550000;
|
||||
--border-strong : #aa0000;
|
||||
--button : #000000;
|
||||
--button-hover : #550000;
|
||||
--button-pressed: #aa0000;
|
||||
--focus-ring : #ff0000;
|
||||
--shadow : #ff000080;
|
||||
--text : #ff0000;
|
||||
}
|
||||
|
||||
body { filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v"); }
|
|
@ -0,0 +1,192 @@
|
|||
"use strict";
|
||||
|
||||
// Root element and localization manager for a Toolkit application
|
||||
Toolkit.Application = class Application extends Toolkit.Panel {
|
||||
|
||||
// Object constructor
|
||||
constructor() {
|
||||
super(null);
|
||||
|
||||
// Configure instance fields
|
||||
this.application = this;
|
||||
this.components = [];
|
||||
this.locale = null;
|
||||
this.locales = { first: null };
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("application", "");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component for localization management
|
||||
addComponent(component) {
|
||||
if (this.components.indexOf(component) != -1)
|
||||
return;
|
||||
this.components.push(component);
|
||||
component.localize();
|
||||
}
|
||||
|
||||
// Register a locale with the application
|
||||
addLocale(source) {
|
||||
let loc = null;
|
||||
|
||||
// Process the locale object from the source
|
||||
try { loc = new Function("return (" + source + ");")(); }
|
||||
catch(e) { console.log(e); }
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!loc || typeof loc != "object" ||
|
||||
!("key" in loc) || !("name" in loc)
|
||||
) return null;
|
||||
|
||||
// Register the locale
|
||||
if (this.locales.first == null)
|
||||
this.locales.first = loc;
|
||||
this.locales[loc.key] = loc;
|
||||
return loc.key;
|
||||
}
|
||||
|
||||
// Produce a list of all registered locale keys
|
||||
listLocales() {
|
||||
return Object.values(this.locales);
|
||||
}
|
||||
|
||||
// Remove a compnent from being localized
|
||||
removeComponent(component) {
|
||||
let index = this.components.indexOf(component);
|
||||
if (index == -1)
|
||||
return false;
|
||||
this.components.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Specify which localized strings to use for application controls
|
||||
setLocale(lang) {
|
||||
|
||||
// Error checking
|
||||
if (this.locales.first == null)
|
||||
return null;
|
||||
|
||||
// Working variables
|
||||
lang = lang.toLowerCase();
|
||||
let parts = lang.split("-");
|
||||
let best = null;
|
||||
|
||||
// Check all locales
|
||||
for (let loc of Object.values(this.locales)) {
|
||||
let key = loc.key.toLowerCase();
|
||||
|
||||
// The language is an exact match
|
||||
if (key == lang) {
|
||||
best = loc;
|
||||
break;
|
||||
}
|
||||
|
||||
// The language matches, but the region may not
|
||||
if (best == null && key.split("-")[0] == parts[0])
|
||||
best = loc;
|
||||
}
|
||||
|
||||
// The language did not match: use the first locale that was registered
|
||||
if (best == null)
|
||||
best = this.locales.first;
|
||||
|
||||
// Select the locale
|
||||
this.locale = best;
|
||||
return best.key;
|
||||
}
|
||||
|
||||
// Localize text for a component
|
||||
translate(text, properties) {
|
||||
properties = !properties ? {} :
|
||||
properties instanceof Toolkit.Component ? properties.properties :
|
||||
properties;
|
||||
|
||||
// Process all characters from the input
|
||||
let sub = { text: "", parent: null };
|
||||
for (let x = 0; x < text.length; x++) {
|
||||
let c = text[x];
|
||||
let last = x == text.length - 1;
|
||||
|
||||
// Left curly brace
|
||||
if (c == '{') {
|
||||
|
||||
// Literal left curly brace
|
||||
if (!last && text[x + 1] == '{') {
|
||||
sub.text += c;
|
||||
x++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Open a substring
|
||||
sub = { text: "", parent: sub };
|
||||
continue;
|
||||
}
|
||||
|
||||
// Right curly brace
|
||||
if (c == '}') {
|
||||
|
||||
// Literal right curly brace
|
||||
if (!last && text[x + 1] == '}') {
|
||||
sub.text += c;
|
||||
x++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Close a sub (if there are any to close)
|
||||
if (sub.parent != null) {
|
||||
|
||||
// Text comes from component property
|
||||
if (sub.text in properties) {
|
||||
sub.parent.text += properties[sub.text];
|
||||
sub = sub.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Text comes from locale
|
||||
let value = this.fromLocale(sub.text, true);
|
||||
if (value !== null) {
|
||||
text = value + text.substring(x + 1);
|
||||
x = -1;
|
||||
sub = sub.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Take the text as-is
|
||||
sub.parent.text += "{" + sub.text + "}";
|
||||
sub = sub.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Append the character to the sub's text
|
||||
sub.text += c;
|
||||
}
|
||||
|
||||
// Close any remaining subs (should never happen)
|
||||
for (; sub.parent != null; sub = sub.parent)
|
||||
sub.parent.text += sub.text;
|
||||
return sub.text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Retrieve the text for a key in the locale
|
||||
fromLocale(key) {
|
||||
let locale = this.locale || {};
|
||||
for (let part of key.split(".")) {
|
||||
if (!(part in locale))
|
||||
return null;
|
||||
locale = locale[part];
|
||||
}
|
||||
return typeof locale == "string" ? locale : null;
|
||||
}
|
||||
|
||||
};
|
|
@ -0,0 +1,244 @@
|
|||
"use strict";
|
||||
|
||||
// Clickable button
|
||||
Toolkit.Button = class Button extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div");
|
||||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
this.blurListeners = [];
|
||||
this.clickListeners = [];
|
||||
this.enabled = "enabled" in options ? options.enabled : true;
|
||||
this.focusListeners = [];
|
||||
this.pressed = "pressed" in options ? options.pressed : false;
|
||||
this.tabStop = true;
|
||||
this.text = options.text || "";
|
||||
this.toggleable = "toggleable"in options?options.toggleable:false;
|
||||
|
||||
// Configure element
|
||||
this.element.type = "button";
|
||||
this.element.setAttribute("role", "button");
|
||||
this.element.style.cursor = "default";
|
||||
this.element.style.position = "relative";
|
||||
this.element.style.userSelect = "none";
|
||||
this.element.addEventListener("blur" , e=>this.onblur (e));
|
||||
this.element.addEventListener("focus" , e=>this.onfocus (e));
|
||||
this.element.addEventListener("keydown" , e=>this.onkeydown (e));
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||
|
||||
// Configure properties
|
||||
this.setEnabled (this.enabled );
|
||||
this.setPressed (this.pressed );
|
||||
this.setTabStop (this.tabStop );
|
||||
this.setText (this.text );
|
||||
this.setToggleable(this.toggleable);
|
||||
application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for blur events
|
||||
addBlurListener(listener) {
|
||||
if (this.blurListeners.indexOf(listener) == -1)
|
||||
this.blurListeners.push(listener);
|
||||
}
|
||||
|
||||
// Add a callback for click events
|
||||
addClickListener(listener) {
|
||||
if (this.clickListeners.indexOf(listener) == -1)
|
||||
this.clickListeners.push(listener);
|
||||
}
|
||||
|
||||
// Add a callback for focus events
|
||||
addFocusListener(listener) {
|
||||
if (this.focusListeners.indexOf(listener) == -1)
|
||||
this.focusListeners.push(listener);
|
||||
}
|
||||
|
||||
// Determine whether the component participates in the tab sequence
|
||||
getTabStop() {
|
||||
return this.tabStop;
|
||||
}
|
||||
|
||||
// Retrieve the control's text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Determine whether the control is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Determine the toggle button's active state
|
||||
isPressed() {
|
||||
return this.pressed;
|
||||
}
|
||||
|
||||
// Determine whether the button is a toggle button
|
||||
isToggleable() {
|
||||
return this.toggleable;
|
||||
}
|
||||
|
||||
// Specify whether the control is enabled
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
if (enabled)
|
||||
this.element.removeAttribute("disabled");
|
||||
else this.element.setAttribute("disabled", "");
|
||||
}
|
||||
|
||||
// Specify whether the component participates in the regular tab sequence
|
||||
setTabStop(tabStop) {
|
||||
this.tabStop = tabStop = !!tabStop;
|
||||
this.element.setAttribute("tabindex", tabStop ? "0" : "-1");
|
||||
}
|
||||
|
||||
// Specify the toggle button's active state
|
||||
setPressed(pressed) {
|
||||
this.pressed = pressed = !!pressed;
|
||||
if (this.toggleable)
|
||||
this.element.setAttribute("aria-pressed", pressed);
|
||||
}
|
||||
|
||||
// Specify the control's text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Specify whether the button is a toggle button
|
||||
setToggleable(toggleable) {
|
||||
this.toggleable = toggleable = !!toggleable;
|
||||
if (toggleable)
|
||||
this.element.setAttribute("aria-pressed", this.pressed);
|
||||
else this.element.removeAttribute("aria-pressed");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Actions when the button is activated
|
||||
activate(e) {
|
||||
if (this.toggleable)
|
||||
this.setPressed(!this.pressed);
|
||||
for (let listener of this.clickListeners)
|
||||
listener(e, this);
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.text;
|
||||
if (this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.element.innerText = text;
|
||||
this.element.setAttribute("aria-label", text);
|
||||
}
|
||||
|
||||
// Blur event handler
|
||||
onblur(e) {
|
||||
for (let listener of this.blurListeners)
|
||||
listener(e, this);
|
||||
this.focusChanged(
|
||||
this, e.relatedTarget ? e.relatedTarget.component : null);
|
||||
}
|
||||
|
||||
// Focus event handler
|
||||
onfocus(e) {
|
||||
for (let listener of this.focusListeners)
|
||||
listener(e, this);
|
||||
this.focusChanged(
|
||||
e.relatedTarget ? e.relatedTarget.component : null, this);
|
||||
}
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.key != " " && e.key != "Enter")
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// The button was activated
|
||||
this.activate(e);
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Configure element
|
||||
this.element.focus();
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.setAttribute("active", "");
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Working variables
|
||||
let bounds = this.element.getBoundingClientRect();
|
||||
let active =
|
||||
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
|
||||
e.y >= bounds.y && e.y < bounds.y + bounds.height
|
||||
;
|
||||
|
||||
// Configure event
|
||||
if (active)
|
||||
this.element.setAttribute("active", "");
|
||||
else this.element.removeAttribute("active");
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Working variables
|
||||
let active = this.element.hasAttribute("active");
|
||||
|
||||
// Configure element
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
this.element.removeAttribute("active");
|
||||
|
||||
// The pointer was released without activating the button
|
||||
if (!active)
|
||||
return;
|
||||
|
||||
// The button was activated
|
||||
this.activate(e);
|
||||
}
|
||||
|
||||
};
|
|
@ -0,0 +1,92 @@
|
|||
"use strict";
|
||||
|
||||
// Base features for all components
|
||||
Toolkit.Component = class Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, tagname) {
|
||||
|
||||
// Configure instance fields
|
||||
this.application = application;
|
||||
this.containers = [ this ];
|
||||
this.element = document.createElement(tagname);
|
||||
this.id = this.element.id = Toolkit.id();
|
||||
this.parent = null;
|
||||
this.properties = {};
|
||||
this.resizeListeners = [];
|
||||
|
||||
// Configure component
|
||||
this.element.component = this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for resize events
|
||||
addResizeListener(listener) {
|
||||
if (this.resizeListeners.indexOf(listener) != -1)
|
||||
return;
|
||||
if (this.resizeListeners.length == 0)
|
||||
new ResizeObserver(()=>this.onresize()).observe(this.element);
|
||||
this.resizeListeners.push(listener);
|
||||
}
|
||||
|
||||
// Remove the component from its parent
|
||||
remove() {
|
||||
this.parent && this.parent.remove(this);
|
||||
}
|
||||
|
||||
// Retrieve the bounding box of the element
|
||||
getBounds() {
|
||||
return this.element.getBoundingClientRect();
|
||||
}
|
||||
|
||||
// Specify the height of the element
|
||||
setHeight(height) {
|
||||
if (height === null)
|
||||
this.element.style.removeProperty("height");
|
||||
else this.element.style.height = height;
|
||||
}
|
||||
|
||||
// Specify the width of the element
|
||||
setWidth(width) {
|
||||
if (width === null)
|
||||
this.element.style.removeProperty("width");
|
||||
else this.element.style.width = width;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Determine whether this component contains another
|
||||
contains(comp) {
|
||||
if (comp == null)
|
||||
return false;
|
||||
if (comp instanceof Toolkit.Component)
|
||||
comp = comp.element;
|
||||
for (let cont of this.containers)
|
||||
if ((cont instanceof Toolkit.Component ? cont.element : cont)
|
||||
.contains(comp)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Notify of a change to component focus
|
||||
focusChanged(from, to) {
|
||||
if (this.parent != null)
|
||||
this.parent.focusChanged(from, to);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Resize event handler
|
||||
onresize() {
|
||||
let bounds = this.getBounds();
|
||||
for (let listener of this.resizeListeners)
|
||||
listener(bounds, this);
|
||||
}
|
||||
|
||||
};
|
|
@ -0,0 +1,275 @@
|
|||
"use strict";
|
||||
|
||||
// Selection within a MenuBar
|
||||
Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||
|
||||
// Object constructor
|
||||
constructor(parent, options) {
|
||||
super(parent, options);
|
||||
|
||||
// Configure instance fields
|
||||
this.items = [];
|
||||
this.parent = parent;
|
||||
|
||||
// Configure menu element
|
||||
this.menu = document.createElement("div");
|
||||
this.menu.id = Toolkit.id();
|
||||
this.menu.style.display = "none";
|
||||
this.menu.style.flexDirection = "column";
|
||||
this.menu.style.position = "absolute";
|
||||
this.menu.setAttribute("role", "menu");
|
||||
this.menu.setAttribute("aria-labelledby", this.id);
|
||||
this.containers.push(this.menu);
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("aria-expanded", "false");
|
||||
this.element.setAttribute("aria-haspopup", "menu");
|
||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Create a MenuItem and associate it with the application and component
|
||||
newMenuItem(options, index) {
|
||||
let item = new Toolkit.MenuItem(this, options);
|
||||
|
||||
// Determine the ordinal position of the element within the container
|
||||
index = !(typeof index == "number") ? this.items.length :
|
||||
Math.floor(Math.min(Math.max(0, index), this.items.length));
|
||||
|
||||
// Add the item to the menu
|
||||
let ref = this.items[index];
|
||||
this.menu.insertBefore(item.element, ref ? ref.element : null);
|
||||
this.items.splice(index, 0, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
// Specify whether the menu is enabled
|
||||
setEnabled(enabled) {
|
||||
super.setEnabled(enabled);
|
||||
if (!this.enabled && this.parent.expanded == this)
|
||||
this.setExpanded(false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// The menu item was activated
|
||||
activate(deeper) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
this.setExpanded(true);
|
||||
if (deeper && this.items.length > 0)
|
||||
this.items[0].element.focus();
|
||||
}
|
||||
|
||||
// Notify of a change to component focus
|
||||
focusChanged(from, to) {
|
||||
if (!this.contains(to)) {
|
||||
let expanded = this.parent.expanded;
|
||||
this.setExpanded(false);
|
||||
this.parent.expanded = expanded == this ? null : expanded;
|
||||
}
|
||||
super.focusChanged(from, to);
|
||||
}
|
||||
|
||||
// Show or hide the pop-up menu
|
||||
setExpanded(expanded) {
|
||||
|
||||
// Setting expanded to false
|
||||
if (!expanded) {
|
||||
|
||||
// Hide the pop-up menu
|
||||
this.element.setAttribute("aria-expanded", "false");
|
||||
this.menu.style.display = "none";
|
||||
this.parent.expanded = null;
|
||||
|
||||
// Close any expanded submenus
|
||||
if (this.expanded != null)
|
||||
this.expanded.setExpanded(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide the existing submenu of the parent
|
||||
if (this.parent.expanded != null && this.parent.expanded != this)
|
||||
this.parent.expanded.setExpanded(false);
|
||||
this.parent.expanded = this;
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("aria-expanded", "true");
|
||||
|
||||
// Configure pop-up menu
|
||||
let barBounds = this.menuBar.element.getBoundingClientRect();
|
||||
let bounds = this.element.getBoundingClientRect();
|
||||
this.menu.style.display = "flex";
|
||||
this.menu.style.left = (bounds.x - barBounds.x) + "px";
|
||||
this.menu.style.top = (bounds.y + bounds.height - barBounds.y) + "px";
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
// Open the menu and select its first item (if any)
|
||||
case " ":
|
||||
case "ArrowDown":
|
||||
case "Enter":
|
||||
this.activate(true);
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "ArrowLeft":
|
||||
|
||||
// Move to the previous item in the menu bar
|
||||
if (this.parent == this.menuBar) {
|
||||
let menu = this.parent.menus[
|
||||
(this.parent.menus.indexOf(this) +
|
||||
this.parent.menus.length - 1) %
|
||||
this.parent.menus.length
|
||||
];
|
||||
if (menu != this && this.parent.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.element.focus();
|
||||
}
|
||||
|
||||
// Close the menu and return to the parent menu
|
||||
else {
|
||||
this.setExpanded(false);
|
||||
this.parent.element.focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "ArrowRight":
|
||||
|
||||
// Move to the next item in the menu bar
|
||||
if (this.parent == this.menuBar) {
|
||||
let menu = this.parent.menus[
|
||||
(this.parent.menus.indexOf(this) + 1) %
|
||||
this.parent.menus.length
|
||||
];
|
||||
if (menu != this) {
|
||||
if (this.parent.expanded != null) {
|
||||
menu.activate(true);
|
||||
} else menu.element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Open the menu and select its first item (if any)
|
||||
else this.activate(true);
|
||||
|
||||
break;
|
||||
|
||||
// Open the menu and select the last item (if any)
|
||||
case "ArrowUp":
|
||||
this.activate(false);
|
||||
if (this.items.length > 0)
|
||||
this.items[this.items.length - 1].element.focus();
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "End":
|
||||
|
||||
// Select the Last menu in the menu bar
|
||||
if (this.parent == this.menuBar) {
|
||||
let menu = this.parent.menus[this.parent.menus.length - 1];
|
||||
if (menu != this) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Select the last item in the parent menu
|
||||
else {
|
||||
let item = this.parent.items[this.parent.items.length - 1];
|
||||
if (item != this) {
|
||||
this.setExpanded(false);
|
||||
item.element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Return focus to the original element
|
||||
case "Escape":
|
||||
this.menuBar.expanded.setExpanded(false);
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "Home":
|
||||
|
||||
// Select the first menu in the menu bar
|
||||
if (this.parent == this.menuBar) {
|
||||
let menu = this.parent.menus[0];
|
||||
if (menu != this) {
|
||||
if (this.menuBar.expanded != null)
|
||||
menu.activate(true);
|
||||
else menu.element.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Select the last item in the parent menu
|
||||
else {
|
||||
let item = this.parent.items[0];
|
||||
if (item != this) {
|
||||
this.setExpanded(false);
|
||||
item.element.focus();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Pointer down event handler
|
||||
onpointerdown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0)
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Activate the menu
|
||||
this.element.focus();
|
||||
this.activate(false);
|
||||
}
|
||||
|
||||
// Pointer move event handler
|
||||
onpointermove(e) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
this.parent != this.menuBar ||
|
||||
this.parent.expanded == null ||
|
||||
this.parent.expanded == this
|
||||
) return;
|
||||
|
||||
// Activate the menu
|
||||
this.parent.expanded.setExpanded(false);
|
||||
this.parent.expanded = this;
|
||||
this.element.focus();
|
||||
this.setExpanded(true);
|
||||
}
|
||||
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
"use strict";
|
||||
|
||||
// Main application menu bar
|
||||
Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application, options) {
|
||||
super(application, "div");
|
||||
|
||||
// Configure instance fields
|
||||
this.expanded = null;
|
||||
this.lastFocus = null;
|
||||
this.menuBar = this;
|
||||
this.menus = [];
|
||||
this.name = options.name || "";
|
||||
|
||||
// Configure element
|
||||
this.element.style.display = "flex";
|
||||
this.element.style.position = "relative";
|
||||
this.element.style.zIndex = "1";
|
||||
this.element.setAttribute("role", "menubar");
|
||||
|
||||
// Configure properties
|
||||
this.setName(this.name);
|
||||
application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Create a Menu and associate it with the application and component
|
||||
newMenu(options, index) {
|
||||
let menu = new Toolkit.Menu(this, options);
|
||||
|
||||
// Determine the ordinal position of the element within the container
|
||||
index = !(typeof index == "number") ? this.menus.length :
|
||||
Math.floor(Math.min(Math.max(0, index), this.menus.length));
|
||||
|
||||
// Add the menu to the menu bar
|
||||
let ref = this.menus[index];
|
||||
this.element.insertBefore(menu.element, ref ? ref.element : null);
|
||||
this.menus.splice(index, 0, menu);
|
||||
menu.element.insertAdjacentElement("afterend", menu.menu);
|
||||
|
||||
// Ensure only the first menu is focusable
|
||||
for (let x = 0; x < this.menus.length; x++)
|
||||
this.menus[x].element.setAttribute("tabindex", x==0 ? "0" : "-1");
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
// Specify the menu's accessible name
|
||||
setName(name) {
|
||||
this.name = name || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Notify of a change to component focus
|
||||
focusChanged(from, to) {
|
||||
|
||||
// Configure tabstop on the first menu
|
||||
if (this.menus.length > 0)
|
||||
this.menus[0].element.setAttribute("tabindex",
|
||||
this.contains(to) ? "-1" : "0");
|
||||
|
||||
// Retain a reference to the previously focused element
|
||||
if (!this.contains(from)) {
|
||||
if (from == null)
|
||||
from = document.body;
|
||||
if ("component" in from)
|
||||
from = from.component;
|
||||
this.lastFocus = from;
|
||||
}
|
||||
|
||||
super.focusChanged(from, to);
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.name;
|
||||
if (this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.element.setAttribute("aria-label", text);
|
||||
}
|
||||
|
||||
// Return focus to where it was before the menu was activated
|
||||
restoreFocus() {
|
||||
let elm = this.lastFocus;
|
||||
if (elm == null)
|
||||
elm = document.body;
|
||||
elm.focus();
|
||||
}
|
||||
|
||||
};
|
|
@ -0,0 +1,214 @@
|
|||
"use strict";
|
||||
|
||||
// Selection within a Menu
|
||||
Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(parent, options) {
|
||||
super(parent && parent.application, "div");
|
||||
|
||||
// Configure instance fields
|
||||
this.clickListeners = [];
|
||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.icon = options.icon || null;
|
||||
this.menuBar = parent.menuBar;
|
||||
this.parent = parent;
|
||||
this.text = options.text || "";
|
||||
this.shortcut = options.shortcut || null;
|
||||
|
||||
// Configure base element
|
||||
this.element.style.display = "flex";
|
||||
this.element.setAttribute("role", "menuitem");
|
||||
this.element.setAttribute("tabindex", "-1");
|
||||
this.element.addEventListener("blur" , e=>this.onblur (e));
|
||||
this.element.addEventListener("focus" , e=>this.onfocus (e));
|
||||
this.element.addEventListener("keydown", e=>this.onkeydown(e));
|
||||
if (this.parent != this.menuBar)
|
||||
this.element.addEventListener("pointerup", e=>this.onpointerup(e));
|
||||
|
||||
// Configure display text element
|
||||
this.textElement = document.createElement("div");
|
||||
this.textElement.style.cursor = "default";
|
||||
this.textElement.style.flexGrow = "1";
|
||||
this.textElement.style.userSelect = "none";
|
||||
this.element.appendChild(this.textElement);
|
||||
|
||||
// Configure properties
|
||||
this.setEnabled(this.enabled);
|
||||
this.setText (this.text);
|
||||
this.application.addComponent(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a callback for click events
|
||||
addClickListener(listener) {
|
||||
if (this.clickListeners.indexOf(listener) == -1)
|
||||
this.clickListeners.push(listener);
|
||||
}
|
||||
|
||||
// Retrieve the item's display text
|
||||
getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
// Determine whether the item is enabled
|
||||
isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
// Specify whether the item is enabled
|
||||
setEnabled(enabled) {
|
||||
this.enabled = enabled = !!enabled;
|
||||
if (enabled)
|
||||
this.element.removeAttribute("disabled");
|
||||
else this.element.setAttribute("disabled", "");
|
||||
}
|
||||
|
||||
// Specify the item's display text
|
||||
setText(text) {
|
||||
this.text = text || "";
|
||||
this.localize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.text;
|
||||
if (this.application)
|
||||
text = this.application.translate(text, this);
|
||||
this.element.setAttribute("aria-label", text);
|
||||
this.textElement.innerText = text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// The menu item was activated
|
||||
activate(e) {
|
||||
if (!this.enabled)
|
||||
return;
|
||||
this.menuBar.restoreFocus();
|
||||
for (let listener of this.clickListeners)
|
||||
listener(e, this);
|
||||
this.menuBar.expanded.setExpanded(false);
|
||||
}
|
||||
|
||||
// Focus lost event handler
|
||||
onblur(e) {
|
||||
this.focusChanged(
|
||||
this, e.relatedTarget ? e.relatedTarget.component : null);
|
||||
}
|
||||
|
||||
// Focus gained event handler
|
||||
onfocus(e) {
|
||||
this.focusChanged(
|
||||
e.relatedTarget ? e.relatedTarget.component : null, this);
|
||||
}
|
||||
|
||||
// Key press event handler
|
||||
onkeydown(e) {
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
// Activate the item
|
||||
case " ":
|
||||
case "Enter":
|
||||
this.activate(e);
|
||||
break;
|
||||
|
||||
// Select the next item
|
||||
case "ArrowDown":
|
||||
this.parent.items[
|
||||
(this.parent.items.indexOf(this) + 1) %
|
||||
this.parent.items.length
|
||||
].element.focus();
|
||||
break;
|
||||
|
||||
// Conditional
|
||||
case "ArrowLeft":
|
||||
|
||||
// Move to the previous menu in the menu bar
|
||||
if (this.parent.parent == this.menuBar) {
|
||||
let menu = this.menuBar.menus[
|
||||
(this.menuBar.menus.indexOf(this.parent) +
|
||||
this.menuBar.menus.length - 1) %
|
||||
this.menuBar.menus.length
|
||||
];
|
||||
if (menu != this.parent)
|
||||
menu.activate(true);
|
||||
}
|
||||
|
||||
// Close the containing submenu
|
||||
else {
|
||||
this.parent.setExpanded(false);
|
||||
this.parent.parent.element.focus();
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Move to the next menu in the menu bar
|
||||
case "ArrowRight":
|
||||
let menu = this.menuBar.menus[
|
||||
(this.menuBar.menus.indexOf(this.menuBar.expanded) + 1) %
|
||||
this.menuBar.menus.length
|
||||
];
|
||||
if (this.menuBar.expanded != menu)
|
||||
menu.activate(true);
|
||||
break;
|
||||
|
||||
// Select the previous item
|
||||
case "ArrowUp":
|
||||
this.parent.items[
|
||||
(this.parent.items.indexOf(this) +
|
||||
this.parent.items.length - 1) %
|
||||
this.parent.items.length
|
||||
].element.focus();
|
||||
break;
|
||||
|
||||
// Select the last item in the menu
|
||||
case "End":
|
||||
this.parent.items[this.parent.items.length-1].element.focus();
|
||||
break;
|
||||
|
||||
// Return focus to the original element
|
||||
case "Escape":
|
||||
this.menuBar.expanded.setExpanded(false);
|
||||
break;
|
||||
|
||||
// Select the first item in the menu
|
||||
case "Home":
|
||||
this.parent.items[0].element.focus();
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Configure element
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
// Pointer up event handler
|
||||
onpointerup(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || document.activeElement != this.element)
|
||||
return;
|
||||
|
||||
// Configure event
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Activate the menu item
|
||||
this.activate(e);
|
||||
}
|
||||
|
||||
};
|
|
@ -0,0 +1,188 @@
|
|||
"use strict";
|
||||
|
||||
// Box that can contain other components
|
||||
Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||
|
||||
// Object constructor
|
||||
constructor(application) {
|
||||
super(application, "div");
|
||||
|
||||
// Configure instance fields
|
||||
this.application = application;
|
||||
this.children = [];
|
||||
this.crossAlign = "start";
|
||||
this.direction = "row";
|
||||
this.edge = "left";
|
||||
this.hGap = "0";
|
||||
this.layout = "split";
|
||||
this.mainAlign = "start";
|
||||
this.sizeable = false;
|
||||
this.vGap = "0";
|
||||
this.wrap = false;
|
||||
|
||||
// Configure element
|
||||
this.element.style.minHeight = "0";
|
||||
this.element.style.minWidth = "0";
|
||||
|
||||
// Configure layout
|
||||
this.setSplitLayout("left", false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a component as a child of this container
|
||||
add(component, index) {
|
||||
|
||||
// Determine the ordinal position of the element within the container
|
||||
index = !(typeof index == "number") ? this.children.length :
|
||||
Math.floor(Math.min(Math.max(0, index), this.children.length));
|
||||
|
||||
// Add the component to the container
|
||||
component.parent = this;
|
||||
this.children.splice(index, 0, component);
|
||||
this.arrange();
|
||||
}
|
||||
|
||||
// Create a Button and associate it with the application
|
||||
newButton(options) {
|
||||
return new Toolkit.Button(this.application, options);
|
||||
}
|
||||
|
||||
// Create a MenuBar and associate it with the application
|
||||
newMenuBar(options) {
|
||||
return new Toolkit.MenuBar(this.application, options);
|
||||
}
|
||||
|
||||
// Create a Panel and associate it with the application
|
||||
newPanel(options) {
|
||||
return new Toolkit.Panel(this.application, options);
|
||||
}
|
||||
|
||||
// Remove a component from the container
|
||||
remove(component) {
|
||||
|
||||
// Locate the component in the children
|
||||
let index = this.children.indexOf(component);
|
||||
if (index == -1)
|
||||
return;
|
||||
|
||||
// Remove the component
|
||||
component.parent = null;
|
||||
component.element.remove();
|
||||
this.children.splice(index, 1);
|
||||
this.arrange();
|
||||
}
|
||||
|
||||
// Configure the element with a flex layout
|
||||
setFlexLayout(direction, mainAlign, crossAlign, gap, wrap) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = "flex";
|
||||
this.crossAlign = crossAlign;
|
||||
this.direction = direction;
|
||||
this.mainAlign = mainAlign;
|
||||
this.wrap = wrap;
|
||||
|
||||
// Working variables
|
||||
switch (crossAlign) {
|
||||
case "end" : crossAlign = "flex-end" ;
|
||||
case "start": crossAlign = "flex-start";
|
||||
}
|
||||
switch (mainAlign) {
|
||||
case "end" : mainAlign = "flex-end" ;
|
||||
case "start": mainAlign = "flex-start";
|
||||
}
|
||||
|
||||
// Configure element
|
||||
this.element.style.alignItems = crossAlign;
|
||||
this.element.style.display = "flex";
|
||||
this.element.style.flexDirection = direction;
|
||||
this.element.style.justifyContent = mainAlign;
|
||||
if (direction == "column") {
|
||||
this.hGap = gap;
|
||||
this.element.style.rowGap = gap;
|
||||
}
|
||||
if (direction == "row") {
|
||||
this.vGap = gap;
|
||||
this.element.style.columnGap = gap;
|
||||
}
|
||||
if (wrap)
|
||||
this.element.style.flexWrap = "wrap";
|
||||
else this.element.style.removeProperty("flex-wrap");
|
||||
|
||||
// Manage components
|
||||
this.arrange();
|
||||
}
|
||||
|
||||
// Configure the element with a split layout
|
||||
setSplitLayout(edge, sizeable) {
|
||||
|
||||
// Configure instance fields
|
||||
this.layout = "split";
|
||||
this.edge = edge;
|
||||
this.sizeable = sizeable;
|
||||
|
||||
// Working variables
|
||||
let rows = null, cols = null;
|
||||
let tracks = [ "max-content", "auto" ];
|
||||
if (sizeable)
|
||||
tracks.splice(1, 0, "max-content");
|
||||
|
||||
// Processing by edge
|
||||
switch (edge) {
|
||||
case "bottom": tracks.reverse(); // Fallthrough
|
||||
case "top" : rows = tracks.join(" "); break;
|
||||
case "right" : tracks.reverse(); // Fallthrough
|
||||
case "left" : cols = tracks.join(" ");
|
||||
}
|
||||
|
||||
// Configure element
|
||||
this.element.style.display = "grid";
|
||||
if (cols != null)
|
||||
this.element.style.gridTemplateColumns = cols;
|
||||
else this.element.style.removeProperty("grid-template-columns");
|
||||
if (rows != null)
|
||||
this.element.style.gridTemplateRows = rows;
|
||||
else this.element.style.removeProperty("grid-template-rows");
|
||||
|
||||
// Manage components
|
||||
this.arrange();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Configure the panel's DOM elements
|
||||
arrange() {
|
||||
let components = [];
|
||||
|
||||
// Remove all children from the DOM
|
||||
for (let comp of this.children)
|
||||
comp.element.remove();
|
||||
|
||||
// Split layout
|
||||
if (this.layout == "split") {
|
||||
components.push(this.children[0]);
|
||||
components.push(this.children[1]);
|
||||
if (this.sizeable)
|
||||
components.splice(1, 0, this.splitter);
|
||||
if (this.edge == "bottom" || this.edge == "right")
|
||||
components.reverse();
|
||||
}
|
||||
|
||||
// Flex and grid layouts
|
||||
else {
|
||||
for (let comp of this.children)
|
||||
components.push(comp);
|
||||
}
|
||||
|
||||
// Add the resulting elements to the DOM
|
||||
for (let comp of components)
|
||||
if (comp)
|
||||
this.element.appendChild(comp.element);
|
||||
}
|
||||
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
"use strict";
|
||||
|
||||
// Widget toolkit manager
|
||||
(globalThis.Toolkit = class Toolkit {
|
||||
|
||||
// Static initializer
|
||||
static initializer() {
|
||||
|
||||
// Static fields
|
||||
Toolkit.lastId = 0;
|
||||
}
|
||||
|
||||
// Produce a unique element ID
|
||||
static id() {
|
||||
return "i" + (Toolkit.lastId++);
|
||||
}
|
||||
|
||||
}).initializer();
|
Loading…
Reference in New Issue