Adding desktop/window components

This commit is contained in:
Guy Perfect 2021-08-26 19:23:18 +00:00
parent dd066e0bbb
commit f753f9f59b
17 changed files with 1297 additions and 632 deletions

View File

@ -7,7 +7,7 @@ globalThis.App = class App {
constructor() {
// Configure themes
Bundle.get("app/theme/base.css").style();
Bundle.get("app/theme/kiosk.css").style();
this.themes = {
dark : Bundle.get("app/theme/dark.css" ).style(false),
light : Bundle.get("app/theme/light.css" ).style(true ),
@ -16,23 +16,24 @@ globalThis.App = class App {
this.theme = this.themes["light"];
// Produce toolkit instance
this.gui = new Toolkit.Application();
this.gui = new Toolkit.Application({
layout: "grid",
rows : "max-content auto"
});
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";
this.gui.setSize(window.innerWidth+"px", window.innerHeight+"px");
});
window.dispatchEvent(new Event("resize"));
// 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);
this.gui.addPropagationListener(e=>this.mainMenu.restoreFocus());
// File menu
let menu = this.mainMenu.newMenu({ text: "{menu.file._}"});
@ -47,6 +48,19 @@ globalThis.App = class App {
item.addClickListener(()=>this.setTheme("dark"));
item = menu.newMenuItem({ text: "{menu.theme.virtual}"});
item.addClickListener(()=>this.setTheme("virtual"));
// Desktop pane
let desktop = this.gui.newPanel({ layout: "desktop" });
desktop.setRole("group");
desktop.element.setAttribute("desktop", "");
this.gui.add(desktop);
let wnd = this.gui.newWindow({
title: "{memory._}"
});
desktop.add(wnd);
wnd.setLocation ( 20, 10);
wnd.setClientSize(384, 224);
}

View File

@ -293,6 +293,7 @@ let run = async function() {
await Bundle.run("app/toolkit/MenuBar.js");
await Bundle.run("app/toolkit/MenuItem.js");
await Bundle.run("app/toolkit/Menu.js");
await Bundle.run("app/toolkit/Window.js");
new App();
};
run();

View File

@ -2,10 +2,15 @@
key : "en-US",
name: "English (United States)",
app : {
close : "Close",
console : "Console",
romLoaded : "Successfully loaded file \"{filename}\" ({size})",
romNotVB : "The selected file is not a Virtual Boy ROM.",
readFileError: "Unable to read the selected file."
},
memory: {
_: "Memory"
},
menu: {
_ : "Main application menu",
file: {

View File

@ -1,118 +0,0 @@
/* 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;
}

View File

@ -1,13 +1,12 @@
: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;
}
--control : #222222;
--control-focus : #444444;
--control-shadow : #999999;
--desktop : #111111;
--text : #cccccc;
--window-blur : #555555;
--window-blur-text : #cccccc;
--window-close-text: #cccccc;
--window-focus : #007ACC;
--window-focus-text: #ffffff;
}

188
app/theme/kiosk.css Normal file
View File

@ -0,0 +1,188 @@
/* Common styles for all themes */
:root {
color : var(--text);
font-family: Arial, sans-serif;
font-size : 12px;
}
body {
margin : 0;
overflow: hidden;
}
*:focus {
outline: none;
}
[desktop] {
background: var(--desktop);
}
/********************************** Button ***********************************/
[role="button"] {
box-shadow:
0 0 0 1px var(--control-shadow),
1px 1px 0 1px var(--control-shadow)
;
margin : 1px;
padding : 3px;
}
[role="button"]:focus {
background: var(--control-focus);
}
[role="button"][active] {
box-shadow: 0 0 0 1px var(--control-shadow);
margin : 2px 0 0 2px;
}
/********************************** MenuBar **********************************/
[role="menubar"] {
background : var(--control);
border-bottom: 1px solid var(--text);
padding : 2px 3px 3px 2px;
}
[role="menubar"] [role="menuitem"] {
margin : 1px;
padding: 3px;
}
[role="menubar"] [role="menuitem"]:focus {
background: var(--control-focus);
}
[role="menubar"] [role="menuitem"]:not([active],[aria-expanded="true"]):focus,
[role="menubar"] [role="menuitem"]:not([active],[aria-expanded="true"]):hover {
box-shadow:
0 0 0 1px var(--control-shadow),
1px 1px 0 1px var(--control-shadow)
;
}
[role="menubar"] [role="menuitem"][active],
[role="menubar"] [role="menuitem"][aria-expanded="true"] {
box-shadow: 0 0 0 1px var(--control-shadow);
margin : 2px 0 0 2px;
}
[role="menubar"] [role="menu"] {
background: var(--control);
box-shadow:
0 0 0 1px var(--text),
1px 1px 0 1px var(--text)
;
min-height: 16px;
min-width : 16px;
padding : 2px 3px 3px 2px;
}
/********************************** Window ***********************************/
[role="dialog"] {
/*background: #cc0000;*/
}
[role="dialog"] [name="body"] {
background: var(--control);
box-shadow:
0 0 0 1px var(--control),
0 0 0 2px var(--text),
1px 1px 0 2px var(--text)
;
row-gap : 3px;
}
[role="dialog"] [name="title-bar"] {
}
[role="dialog"][focus="true"] [name="title-bar"] {
box-shadow:
0 0 0 1px var(--window-focus),
0 1px 0 1px var(--control-shadow)
;
background : var(--window-focus);
}
[role="dialog"][focus="false"] [name="title-bar"] {
box-shadow:
0 0 0 1px var(--window-blur),
0 1px 0 1px var(--control-shadow)
;
background: var(--window-blur);
}
[role="dialog"] [name="title-icon"],
[role="dialog"] [name="title-close-box"] {
align-self : stretch;
min-width : calc(1em + 4px);
width : calc(1em + 4px);
}
[role="dialog"] [name="title"] {
color : var(--window-focus-text);
font-weight : bold;
overflow : hidden;
padding : 2px;
text-align : center;
text-overflow: ellipsis;
white-space : nowrap;
}
[role="dialog"][focus="false"] [name="title"] {
color : var(--window-blur-text);
}
[role="dialog"] [name="title-close-box"] {
align-items : center;
display : flex;
justify-content: flex-start;
}
[role="dialog"] [name="title-close"] {
align-items : center;
background : var(--control);
box-shadow :
0 0 0 1px var(--control),
0 0 0 2px var(--control-shadow)
;
display : flex;
height : 11px;
justify-content: center;
overflow : hidden;
padding : 0;
width : 11px;
}
[role="dialog"] [name="title-close"]:focus {
background: var(--control-focus);
}
[role="dialog"] [name="title-close"][active] {
box-shadow :
-1px -1px 0 1px var(--control),
-1px -1px 0 2px var(--control-shadow)
;
margin: 2px 0 0 2px;
}
[role="dialog"] [name="title-close"]:after {
color : var(--window-close-text);
content : '\00d7';
font-size : 12px;
line-height: 1em;
}
[role="dialog"] [name="client"] {
background: var(--control);
}

View File

@ -1,13 +1,12 @@
: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;
--control : #eeeeee;
--control-focus : #cccccc;
--control-shadow : #999999;
--desktop : #cccccc;
--text : #000000;
--window-blur : #cccccc;
--window-blur-text : #444444;
--window-close-text: #444444;
--window-focus : #80ccff;
--window-focus-text: #000000;
}

View File

@ -1,15 +1,16 @@
: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;
--control : #000000;
--control-focus : #550000;
--control-shadow : #aa0000;
--desktop : #000000;
--text : #ff0000;
--window-blur : #000000;
--window-blur-text : #aa0000;
--window-close-text: #ff0000;
--window-focus : #550000;
--window-focus-text: #ff0000;
}
body { filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v"); }
[filter="true"] {
filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v");
}

View File

@ -4,17 +4,20 @@
Toolkit.Application = class Application extends Toolkit.Panel {
// Object constructor
constructor() {
super(null);
constructor(options) {
super(null, options);
// Configure instance fields
this.application = this;
this.components = [];
this.locale = null;
this.locales = { first: null };
this.application = this;
this.components = [];
this.locale = null;
this.locales = { first: null };
this.propagationListeners = [];
// Configure element
this.element.setAttribute("application", "");
this.element.addEventListener("mousedown" , e=>this.onpropagation(e));
this.element.addEventListener("pointerdown", e=>this.onpropagation(e));
}
@ -50,6 +53,12 @@ Toolkit.Application = class Application extends Toolkit.Panel {
return loc.key;
}
// Add a callback for propagation events
addPropagationListener(listener) {
if (this.propagationListeners.indexOf(listener) == -1)
this.propagationListeners.push(listener);
}
// Produce a list of all registered locale keys
listLocales() {
return Object.values(this.locales);
@ -189,4 +198,12 @@ Toolkit.Application = class Application extends Toolkit.Panel {
return typeof locale == "string" ? locale : null;
}
// A pointer or mouse down even has propagated
onpropagation(e) {
e.preventDefault();
e.stopPropagation();
for (let listener of this.propagationListeners)
listener(e, this);
}
};

View File

@ -1,192 +1,184 @@
"use strict";
// Clickable button
// Push button
Toolkit.Button = class Button extends Toolkit.Component {
// Object constructor
constructor(application, options) {
super(application, "div");
super(application, "div", options);
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;
this.enabled = "enabled" in options ? !!options.enabled : true;
this.focusable = "focusable" in options?!!options.focusable:true;
this.name = options.name || "";
this.text = options.text || "";
this.toolTip = options.toolTip || "";
// Configure element
this.element.type = "button";
this.element.setAttribute("role", "button");
this.element.setAttribute("tabindex", "0");
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);
this.setEnabled (this.enabled );
this.setFocusable(this.focusable);
this.setName (this.name );
this.setText (this.text );
this.setToolTip (this.toolTip );
this.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);
// Request focus on the appropriate element
focus() {
this.element.focus();
}
// Determine whether the component participates in the tab sequence
getTabStop() {
return this.tabStop;
// Retrieve the button's accessible name
getName() {
return this.name;
}
// Retrieve the control's text
// Retrieve the button's display text
getText() {
return this.text;
}
// Determine whether the control is enabled
// Retrieve the button's tool tip text
getToolTip() {
return this.toolTip;
}
// Determine whether the button is enabled
isEnabled() {
return this.enabled;
}
// Determine the toggle button's active state
isPressed() {
return this.pressed;
// Determine whether the button is focusable
isFocusable() {
return this.focusable;
}
// Determine whether the button is a toggle button
isToggleable() {
return this.toggleable;
}
// Specify whether the control is enabled
// Specify whether the button is enabled
setEnabled(enabled) {
this.enabled = enabled = !!enabled;
if (enabled)
this.element.removeAttribute("disabled");
else this.element.setAttribute("disabled", "");
this.element.setAttribute("aria-disabled", !enabled);
}
// Specify whether the component participates in the regular tab sequence
setTabStop(tabStop) {
this.tabStop = tabStop = !!tabStop;
this.element.setAttribute("tabindex", tabStop ? "0" : "-1");
// Specify whether the button can receive focus
setFocusable(focusable) {
this.focusable = focusable = !!focusable;
if (focusable)
this.element.setAttribute("tabindex", "0");
else this.element.removeAttribute("tabindex");
}
// Specify the toggle button's active state
setPressed(pressed) {
this.pressed = pressed = !!pressed;
if (this.toggleable)
this.element.setAttribute("aria-pressed", pressed);
// Specify the button's accessible name
setName(name) {
this.name = name || "";
this.localize();
}
// Specify the control's text
// Specify the button's display 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");
// Specify the button's tool tip text
setToolTip(toolTip) {
this.toolTip = toolTip || "";
this.localize();
}
///////////////////////////// Package Methods /////////////////////////////
// Update display text with localized strings
localize() {
let name = this.name || this.text;
let text = this.text;
let toolTip = this.toolTip;
if (this.application) {
name = this.application.translate(name, this);
text = this.application.translate(text, this);
if (toolTip)
toolTip = this.application.translate(toolTip, this);
}
this.element.setAttribute("aria-label", name);
this.element.innerText = text;
if (toolTip)
this.element.setAttribute("title", toolTip);
else this.element.removeAttribute("title");
}
///////////////////////////// Private Methods /////////////////////////////
// Actions when the button is activated
// The button was activated
activate(e) {
if (this.toggleable)
this.setPressed(!this.pressed);
if (!this.enabled)
return;
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
// Key down event handler
onkeydown(e) {
// Error checking
if (e.key != " " && e.key != "Enter")
if (!this.enabled)
return;
// Processing by key
switch (e.key) {
case " ":
case "Enter":
this.activate(e);
break;
default: 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 focus
if (this.enabled)
this.focus();
else return;
// Error checking
if (e.button != 0 || this.element.hasPointerCapture(e.captureId))
return;
// Configure element
this.element.focus();
this.element.setPointerCapture(e.pointerId);
this.element.setAttribute("active", "");
}
@ -194,22 +186,22 @@ Toolkit.Button = class Button extends Toolkit.Component {
// Pointer move event handler
onpointermove(e) {
// Error checking
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure event
e.preventDefault();
e.stopPropagation();
// Error checking
if (!this.element.hasPointerCapture(e))
return;
// Working variables
let bounds = this.element.getBoundingClientRect();
let bounds = this.getBounds();
let active =
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
e.y >= bounds.y && e.y < bounds.y + bounds.height
;
// Configure event
// Configure element
if (active)
this.element.setAttribute("active", "");
else this.element.removeAttribute("active");
@ -218,26 +210,21 @@ Toolkit.Button = class Button extends Toolkit.Component {
// 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");
// Error checking
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure element
this.element.releasePointerCapture(e.pointerId);
this.element.removeAttribute("active");
// The pointer was released without activating the button
if (!active)
// Activate the menu item if it is active
if (!this.element.hasAttribute("active"))
return;
// The button was activated
this.element.removeAttribute("active");
this.activate(e);
}

View File

@ -4,19 +4,24 @@
Toolkit.Component = class Component {
// Object constructor
constructor(application, tagname) {
constructor(application, tagname, options) {
options = options || {};
// Configure instance fields
this.application = application;
this.containers = [ this ];
this.display = null;
this.element = document.createElement(tagname);
this.id = this.element.id = Toolkit.id();
this.parent = null;
this.properties = {};
this.resizeListeners = [];
this.resizeObserver = null;
this.visible = "visible" in options ? !!options.visible : true;
// Configure component
this.element.component = this;
this.setVisible(this.visible);
}
@ -27,8 +32,10 @@ Toolkit.Component = class Component {
addResizeListener(listener) {
if (this.resizeListeners.indexOf(listener) != -1)
return;
if (this.resizeListeners.length == 0)
new ResizeObserver(()=>this.onresize()).observe(this.element);
if (this.resizeObserver == null) {
this.resizeObserver = new ResizeObserver(()=>this.onresized());
this.resizeObserver.observe(this.element);
}
this.resizeListeners.push(listener);
}
@ -42,18 +49,77 @@ Toolkit.Component = class Component {
return this.element.getBoundingClientRect();
}
// Retrieve the display CSS property of the visible element
getDisplay() {
return this.display;
}
// Determine whether the component is visible
isVisible() {
for (let comp = this; comp != null; comp = comp.parent)
if (!comp.visible)
return false;
return true;
}
// Specify the display CSS property of the visible element
setDisplay(display) {
this.display = display || null;
this.setVisible(this.visible);
}
// Specify the height of the element
setHeight(height) {
if (height === null)
this.element.style.removeProperty("height");
else this.element.style.height = height;
else this.element.style.height =
typeof height == "number" ? height + "px" : height
}
// Specify the horizontal position of the component
setLeft(left) {
if (left === null)
this.element.style.removeProperty("left");
else this.element.style.left =
typeof left == "number" ? left + "px" : left ;
}
// Specify the absolute position of the component
setLocation(left, top) {
this.setLeft(left);
this.setTop (top );
}
// Specify both the width and the height of the component
setSize(width, height) {
this.setHeight(height);
this.setWidth (width );
}
// Specify the vertical position of the component
setTop(top) {
if (top === null)
this.element.style.removeProperty("top");
else this.element.style.top =
typeof top == "number" ? top + "px" : top ;
}
// Specify whether the component is visible
setVisible(visible) {
this.visible = visible = !!visible;
if (visible) {
if (this.display == null)
this.element.style.removeProperty("display");
else this.element.style.display = this.display;
} else this.element.style.display = "none";
}
// Specify the width of the element
setWidth(width) {
if (width === null)
this.element.style.removeProperty("width");
else this.element.style.width = width;
else this.element.style.width =
typeof width == "number" ? width + "px" : width ;
}
@ -72,18 +138,12 @@ Toolkit.Component = class Component {
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() {
onresized() {
let bounds = this.getBounds();
for (let listener of this.resizeListeners)
listener(bounds, this);

View File

@ -4,46 +4,45 @@
Toolkit.Menu = class Menu extends Toolkit.MenuItem {
// Object constructor
constructor(parent, options) {
super(parent, options);
// Configure instance fields
this.items = [];
this.parent = parent;
constructor(application, options) {
super(application, options);
// 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.menu = this.add(this.application.newPanel({
layout : "flex",
alignCross: "stretch",
direction : "column",
visible : false
}));
this.menu.element.style.position = "absolute";
this.menu.element.setAttribute("role", "menu");
this.menu.element.setAttribute("aria-labelledby", this.id);
this.containers.push(this.menu);
this.children = this.menu.children;
// 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
newMenu(options, index) {
let menu = this.menu.add(new Toolkit.Menu(
this.application, options), index);
menu.child();
menu.element.insertAdjacentElement("afterend", menu.menu.element);
return menu;
}
// 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);
let item = this.menu.add(new Toolkit.MenuItem(
this.application, options), index);
item.child();
return item;
}
@ -63,18 +62,8 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
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);
if (deeper && this.children.length > 0)
this.children[0].focus();
}
// Show or hide the pop-up menu
@ -85,7 +74,7 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
// Hide the pop-up menu
this.element.setAttribute("aria-expanded", "false");
this.menu.style.display = "none";
this.menu.setVisible(false);
this.parent.expanded = null;
// Close any expanded submenus
@ -106,9 +95,11 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
// 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";
this.menu.setVisible(true);
this.menu.setLocation(
(bounds.x - barBounds.x) + "px",
(bounds.y + bounds.height - barBounds.y) + "px"
);
}
@ -117,120 +108,60 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
// Key press event handler
onkeydown(e) {
let index;
// 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;
// Delegate to the MenuItem handler for these keys
case " " :
case "ArrowLeft":
case "End" :
case "Enter" :
case "Escape" :
case "Home" :
return super.onkeydown(e);
// Conditional
case "ArrowLeft":
case "ArrowDown":
// 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();
}
// Open the menu and select the first item (if any)
if (this.parent == this.menuBar)
this.activate(true);
// Close the menu and return to the parent menu
else {
this.setExpanded(false);
this.parent.element.focus();
}
// Delegate to the MenuItem handler
else return super.onkeydown(e);
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 the first item (if any)
if (this.parent != this.menuBar)
this.activate(true);
// Open the menu and select its first item (if any)
else this.activate(true);
// Delegate to the MenuItem handler
else return super.onkeydown(e);
break;
// Open the menu and select the last item (if any)
// Conditional
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
// Open the menu and select the last item (if any)
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();
}
this.activate(false);
index = this.previousChild(0);
if (index != -1)
this.children[index].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();
}
}
// Delegate to the MenuItem handler
else return super.onkeydown(e);
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;
}
@ -242,22 +173,26 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
// Pointer down event handler
onpointerdown(e) {
// Error checking
if (e.button != 0)
return;
// Configure event
e.preventDefault();
e.stopPropagation();
// Error checking
if (!this.enabled || e.button != 0)
return;
// Activate the menu
this.element.focus();
this.focus();
this.activate(false);
}
// Pointer move event handler
onpointermove(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Error checking
if (
this.parent != this.menuBar ||
@ -268,8 +203,14 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
// Activate the menu
this.parent.expanded.setExpanded(false);
this.parent.expanded = this;
this.element.focus();
this.focus();
this.setExpanded(true);
}
// Pointer up event handler (prevent superclass behavior)
onpointerup(e) {
e.preventDefault();
e.stopPropagation();
}
};

View File

@ -1,24 +1,31 @@
"use strict";
// Main application menu bar
Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
Toolkit.MenuBar = class MenuBar extends Toolkit.Panel {
// Object constructor
constructor(application, options) {
super(application, "div");
super(application, options);
// 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");
this.setLayout("flex", {
direction: "row",
wrap : "false"
});
this.setOverflow("visible", "visible");
this.element.addEventListener(
"blur" , e=>this.onblur (e), { capture: true });
this.element.addEventListener(
"focus", e=>this.onfocus(e), { capture: true });
// Configure properties
this.setName(this.name);
@ -29,27 +36,40 @@ Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
///////////////////////////// Public Methods //////////////////////////////
// Add a component as a child of this container
add(component, index) {
super.add(component, index);
component.child();
return component;
}
// 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);
// Create and add a new menu
let menu = this.add(new Toolkit.Menu(this.application,options), index);
menu.element.insertAdjacentElement("afterend", menu.menu.element);
// 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");
for (let x = 0; x < this.children.length; x++)
this.children[x].element
.setAttribute("tabindex", x == 0 ? "0" : "-1");
return menu;
}
// Return focus to where it was before the menu was activated
restoreFocus() {
if (!this.contains(document.activeElement))
return;
let elm = this.lastFocus;
if (elm == null)
elm = document.body;
elm.focus();
if (this.contains(document.activeElement))
document.activeElement.blur();
}
// Specify the menu's accessible name
setName(name) {
this.name = name || "";
@ -60,24 +80,32 @@ Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
///////////////////////////// Package Methods /////////////////////////////
// Notify of a change to component focus
focusChanged(from, to) {
// Blur event capture
onblur(e) {
if (this.contains(e.relatedTarget))
return;
if (this.children.length > 0)
this.children[0].element.setAttribute("tabindex", "0");
if (this.expanded != null)
this.expanded.setExpanded(false);
}
// Focus event capture
onfocus(e) {
// Configure tabstop on the first menu
if (this.menus.length > 0)
this.menus[0].element.setAttribute("tabindex",
this.contains(to) ? "-1" : "0");
if (this.children.length > 0)
this.children[0].element.setAttribute("tabindex", "-1");
// 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);
if (this.contains(e.relatedTarget))
return;
let from = e.relatedTarget;
if (from == null)
from = document.body;
if ("component" in from)
from = from.component;
this.lastFocus = from;
}
// Update display text with localized strings
@ -88,12 +116,4 @@ Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
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();
}
};

View File

@ -1,37 +1,37 @@
"use strict";
// Selection within a Menu
Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
Toolkit.MenuItem = class MenuItem extends Toolkit.Panel {
// Object constructor
constructor(parent, options) {
super(parent && parent.application, "div");
constructor(application, options) {
super(application, options);
// 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.setLayout("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));
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 display text element
this.textElement = document.createElement("div");
this.textElement.id = Toolkit.id();
this.textElement.style.cursor = "default";
this.textElement.style.flexGrow = "1";
this.textElement.style.userSelect = "none";
this.textElement.setAttribute("name", "text");
this.element.appendChild(this.textElement);
this.element.setAttribute("aria-labelledby", this.textElement.id);
// Configure properties
this.setEnabled(this.enabled);
@ -49,6 +49,11 @@ Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
this.clickListeners.push(listener);
}
// Request focus on the appropriate element
focus() {
this.element.focus();
}
// Retrieve the item's display text
getText() {
return this.text;
@ -77,12 +82,24 @@ Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
///////////////////////////// Package Methods /////////////////////////////
// Configure this component to be a child of its parent
child() {
this.menuBar = this.parent;
while (!(this.menuBar instanceof Toolkit.MenuBar))
this.menuBar = this.menuBar.parent;
this.menuItem = this instanceof Toolkit.Menu ? this : this.parent;
while (!(this.menuItem instanceof Toolkit.Menu))
this.menuItem = this.menuItem.parent;
this.menuTop = this instanceof Toolkit.Menu ? this : this.parent;
while (this.menuTop.parent != this.menuBar)
this.menuTop = this.menuTop.parent;
}
// 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;
}
@ -97,23 +114,11 @@ Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
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) {
let index;
// Processing by key
switch (e.key) {
@ -126,88 +131,189 @@ Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
// Select the next item
case "ArrowDown":
this.parent.items[
(this.parent.items.indexOf(this) + 1) %
this.parent.items.length
].element.focus();
index = this.parent.nextChild(
this.parent.children.indexOf(this));
if (index != -1)
this.parent.children[index].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);
if (this.menuItem.parent == this.menuBar) {
index = this.menuBar.previousChild(
this.menuBar.children.indexOf(this.menuItem));
if (index != -1) {
let menu = this.menuBar.children[index];
if (menu != this.menuTop) {
if (this.menuBar.expanded != null)
menu.activate(true);
else menu.focus();
}
}
}
// Close the containing submenu
else {
this.parent.setExpanded(false);
this.parent.parent.element.focus();
this.menuItem.setExpanded(false);
this.menuItem.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);
index = this.menuBar.nextChild(
this.menuBar.children.indexOf(this.menuTop));
if (index != -1) {
let menu = this.menuBar.children[index];
if (menu != this.menuTop) {
if (this.menuBar.expanded != null)
menu.activate(true);
else menu.focus();
}
}
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();
index = this.parent.previousChild(
this.parent.children.indexOf(this));
if (index != -1)
this.parent.children[index].focus();
break;
// Select the last item in the menu
// Conditional
case "End":
this.parent.items[this.parent.items.length-1].element.focus();
// Select the last menu in the menu bar
if (this.parent == this.menuBar) {
index = this.menuBar.previousChild(
this.menuBar.children.length);
if (index != -1) {
let menu = this.menuBar.children[index];
if (menu != this.menuTop) {
if (this.menuBar.expanded != null)
menu.activate(true);
else menu.focus();
}
}
}
// Select the last item in the menu
else {
index = this.menuItem.previousChild(
this.menuItem.children.length);
if (index != -1)
this.menuItem.children[index].focus();
}
break;
// Return focus to the original element
case "Escape":
this.menuBar.expanded.setExpanded(false);
if (this.menuBar.expanded != null)
this.menuBar.expanded.setExpanded(false);
this.menuBar.restoreFocus();
break;
// Select the first item in the menu
// Conditional
case "Home":
this.parent.items[0].element.focus();
// Select the first menu in the menu bar
if (this.parent == this.menuBar) {
index = this.menuBar.nextChild(-1);
if (index != -1) {
let menu = this.menuBar.children[index];
if (menu != this.menuTop) {
if (this.menuBar.expanded != null)
menu.activate(true);
else menu.focus();
}
}
}
// Select the last item in the menu
else {
index = this.menuItem.nextChild(-1);
if (index != -1)
this.menuItem.children[index].focus();
}
break;
default: return;
}
// Configure element
// Configure event
e.preventDefault();
e.stopPropagation();
}
// Pointer up event handler
onpointerup(e) {
// Error checking
if (e.button != 0 || document.activeElement != this.element)
return;
// Pointer down event handler
onpointerdown(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Activate the menu item
// Configure focus
if (this.enabled)
this.focus();
else return;
// Error checking
if (e.button != 0 || this.element.hasPointerCapture(e.pointerId))
return;
// Configure element
this.element.setPointerCapture(e.pointerId);
this.element.setAttribute("active", "");
}
// Pointer move event handler
onpointermove(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Error checking
if (!this.element.hasPointerCapture(e.pointerid))
return;
// Working variables
let bounds = this.getBounds();
let active =
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
e.y >= bounds.y && e.y < bounds.y + bounds.height
;
// Configure element
if (active)
this.element.setAttribute("active", "");
else this.element.removeAttribute("active");
}
// Pointer up event handler
onpointerup(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Error checking
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure element
this.element.releasePointerCapture(e.pointerId);
// Activate the menu item if it is active
if (!this.element.hasAttribute("active"))
return;
this.element.removeAttribute("active");
this.activate(e);
}

View File

@ -4,28 +4,31 @@
Toolkit.Panel = class Panel extends Toolkit.Component {
// Object constructor
constructor(application) {
super(application, "div");
constructor(application, options) {
super(application, "div", options);
// Configure instance fields
options = options || {};
this.alignCross = "start";
this.alignMain = "start";
this.application = application;
this.children = [];
this.crossAlign = "start";
this.columns = null;
this.direction = "row";
this.edge = "left";
this.hGap = "0";
this.layout = "split";
this.mainAlign = "start";
this.sizeable = false;
this.vGap = "0";
this.layout = null;
this.overflowX = options.overflowX || "hidden";
this.overflowY = options.overflowY || "hidden";
this.rows = null;
this.wrap = false;
// Configure element
this.element.style.minHeight = "0";
this.element.style.minWidth = "0";
this.setOverflow(this.overflowX, this.overflowY);
// Configure layout
this.setSplitLayout("left", false);
options = options || {};
this.setLayout(options.layout || null, options);
}
@ -40,9 +43,13 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
Math.floor(Math.min(Math.max(0, index), this.children.length));
// Add the component to the container
let ref = this.children[index] || null;
component.parent = this;
this.element.insertBefore(component.element,
ref == null ? null : ref.element);
this.children.splice(index, 0, component);
this.arrange();
return component;
}
// Create a Button and associate it with the application
@ -60,6 +67,33 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
return new Toolkit.Panel(this.application, options);
}
// Create a Window and associate it with the application
newWindow(options) {
return new Toolkit.Window(this.application, options);
}
// Determine the index of the next visible child
nextChild(index) {
for (let x = 0; x <= this.children.length; x++) {
index = (index + 1) % this.children.length;
let comp = this.children[index];
if (comp.isVisible())
return index;
}
return -1;
}
// Determine the index of the previous visible child
previousChild(index) {
for (let x = 0; x <= this.children.length; x++) {
index = (index + this.children.length - 1) % this.children.length;
let comp = this.children[index];
if (comp.isVisible())
return index;
}
return -1;
}
// Remove a component from the container
remove(component) {
@ -72,117 +106,146 @@ Toolkit.Panel = class Panel extends Toolkit.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 the element's layout
setLayout(layout, options) {
// Configure instance fields
this.layout = "flex";
this.crossAlign = crossAlign;
this.direction = direction;
this.mainAlign = mainAlign;
this.wrap = wrap;
this.layout = layout;
// 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";
// Processing by layout
switch (layout) {
case "block" : this.setBlockLayout (options); break;
case "desktop": this.setDesktopLayout(options); break;
case "flex" : this.setFlexLayout (options); break;
case "grid" : this.setGridLayout (options); break;
default : this.setNullLayout (options); break;
}
// 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 the panel's overflow scrolling behavior
setOverflow(x, y) {
this.overflowX = x || "hidden";
this.overflowY = y || "hidden";
this.element.style.overflow = this.overflowX + " " + this.overflowY;
}
// 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();
// Specify the semantic role of the panel
setRole(role) {
if (!role)
this.element.removeAttribute("role");
else this.element.setAttribute("role", "" + role);
}
///////////////////////////// Private Methods /////////////////////////////
// Configure the panel's DOM elements
arrange() {
let components = [];
// Resize event handler
onresize(desktop) {
// Remove all children from the DOM
for (let comp of this.children)
comp.element.remove();
// Error checking
if (this.layout != "desktop")
return;
// 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();
// Ensure all child windows are visible in the viewport
for (let wnd of this.children) {
let bounds = wnd.getBounds();
wnd.contain(
desktop,
bounds,
bounds.x - desktop.x,
bounds.y - desktop.y
);
}
// 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);
// Configure a block layout
setBlockLayout(options) {
// Configure instance fields
this.layout = "block";
// Configure element
this.setDisplay("block");
}
// Configure a desktop layout
setDesktopLayout(options) {
// Configure instance fields
this.layout = "desktop";
// Configure element
this.setDisplay("block");
this.element.style.position = "relative";
if (this.resizeObserver == null)
this.addResizeListener(b=>this.onresize(b));
}
// Configure a flex layout
setFlexLayout(options) {
// Configure instance fields
this.alignCross = options.alignCross || "start";
this.alignMain = options.alignMain || "start";
this.direction = options.direction || this.direction;
this.layout = "flex";
this.wrap = !!options.wrap;
// Working variables
let alignCross = this.alignCross;
let alignMain = this.alignMain;
if (alignCross == "start" || alignCross == "end")
alignCross = "flex-" + alignCross;
if (alignMain == "start" || alignMain == "end")
alignMain = "flex-" + alignMain;
// Configure element
this.setDisplay("flex");
this.element.style.alignItems = alignCross;
this.element.style.flexDirection = this.direction;
this.element.style.justifyContent = alignMain;
this.element.style.flexWrap = this.wrap ? "wrap" : "nowrap";
}
// Configure a grid layout
setGridLayout(options) {
// Configure instance fields
this.columns = options.columns || null;
this.layout = "grid";
this.rows = options.rows || null;
// Configure element
this.setDisplay("grid");
if (this.columns == null)
this.element.style.removeProperty("grid-template-columns");
else this.element.style.gridTemplateColumns = this.columns;
if (this.rows == null)
this.element.style.removeProperty("grid-template-rows");
else this.element.style.gridTemplateRows = this.rows;
}
// Configure a null layout
setNullLayout(options) {
// Configure instance fields
this.layout = null;
// Configure element
this.setDisplay(null);
this.element.style.removeProperty("align-items" );
this.element.style.removeProperty("flex-wrap" );
this.element.style.removeProperty("grid-template-columns");
this.element.style.removeProperty("grid-template-rows" );
this.element.style.removeProperty("justify-content" );
this.element.style.removeProperty("flex-direction" );
this.element.style.removeProperty("overflow-x" );
this.element.style.removeProperty("overflow-y" );
}
};

382
app/toolkit/Window.js Normal file
View File

@ -0,0 +1,382 @@
"use strict";
// Movable, sizeable child window
Toolkit.Window = class Window extends Toolkit.Panel {
// Object constructor
constructor(application, options) {
super(application, options);
options = options || {};
// Configure instance fields
this.dragBounds = null;
this.dragCursor = { x: 0, y: 0 };
this.dragEdge = null;
this.lastFocus = this.element;
this.title = options.title || "";
// Configure element
this.setLayout("flex", {
alignCross: "stretch",
direction : "column",
overflowX : "visible",
overflowY : "visible"
});
this.setRole("dialog");
this.element.style.position = "absolute";
this.element.setAttribute("aria-modal", "false");
this.element.setAttribute("focus" , "false");
this.element.setAttribute("tabindex" , "-1" );
this.element.addEventListener(
"blur" , e=>this.onblur (e), { capture: true });
this.element.addEventListener(
"focus", e=>this.onfocus(e), { capture: true });
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 body container
this.body = this.add(this.newPanel({
layout : "flex",
alignCross: "stretch",
direction : "column",
overflowX : "visible",
overflowY : "visible"
}));
this.body.element.style.flexGrow = "1";
this.body.element.style.margin = "3px";
this.body.element.setAttribute("name", "body");
// Configure title bar
this.titleBar = this.body.add(this.newPanel({
layout : "flex",
alignCross: "center",
direction : "row",
overflowX : "visible",
overflowY : "visible"
}));
this.titleBar.element.setAttribute("name", "title-bar");
// Configure title icon element
this.titleIcon = this.titleBar.add(this.newPanel({}));
this.titleIcon.element.setAttribute("name", "title-icon");
this.titleIcon.element.style.removeProperty("min-width");
// Configure title text element
this.titleElement = this.titleBar.add(this.newPanel({}));
this.titleElement.element.setAttribute("name", "title");
this.titleElement.element.style.cursor = "default";
this.titleElement.element.style.flexGrow = "1";
this.titleElement.element.style.userSelect = "none";
this.element.setAttribute("aria-labelledby", this.titleElement.id);
// Configure title close element
this.titleCloseBox = this.titleBar.add(this.newPanel({}));
this.titleCloseBox.element.setAttribute("name", "title-close-box");
this.titleCloseBox.element.style.removeProperty("min-width");
this.titleClose = this.titleCloseBox.add(this.newButton({
focusable: false,
name : "{app.close}",
toolTip : "{app.close}"
}));
this.titleClose.element.setAttribute("name", "title-close");
// Configure client area
this.client = this.body.add(this.newPanel({}));
this.client.element.style.flexGrow = "1";
this.client.element.setAttribute("name", "client");
this.client.element.addEventListener(
"pointerdown", e=>this.onclientdown(e));
// Configure properties
this.setTitle(this.title);
application.addComponent(this);
}
///////////////////////////// Public Methods //////////////////////////////
// Specify the size of the client rectangle in pixels
setClientSize(width, height) {
let bounds = this.getBounds();
let client = this.client.getBounds();
this.setSize(
width + bounds.width - client.width,
height + bounds.height - client.height
);
}
// Specify the window's title text
setTitle(title) {
this.title = title || "";
this.localize();
}
///////////////////////////// Package Methods /////////////////////////////
// Request focus on the appropriate element
focus() {
if (this.lastFocus != this)
this.lastFocus.focus();
else this.element.focus();
}
///////////////////////////// Private Methods /////////////////////////////
// Position the window using a tentative location in the desktop
contain(desktop, bounds, x, y, client) {
bounds = bounds || this.getBounds();
client = client || this.client.getBounds();
// Restrict window position
x = Math.min(x, desktop.width - 16);
x = Math.max(x, -bounds.width + 16);
y = Math.min(y, desktop.height - (client.y - bounds.y));
y = Math.max(y, 0);
// Configure element
this.setLocation(x, y);
}
// Detect where in the window the pointer is
edge(e, bounds) {
bounds = bounds || this.getBounds();
let x = e.x - bounds.x;
let y = e.y - bounds.y;
if (y < 3) {
if (x < 8) return "nw";
if (x < bounds.width - 8) return "n" ;
return "ne";
}
if (y >= bounds.height - 3) {
if (x < 8) return "sw";
if (x < bounds.width - 8) return "s" ;
return "se";
}
if (x < 3) {
if (y < 8) return "nw";
if (y < bounds.height - 8) return "w" ;
return "sw";
}
if (x >= bounds.width - 3) {
if (y < 8) return "ne";
if (y < bounds.height - 8) return "e" ;
return "se";
}
return null;
}
// Update display text with localized strings
localize() {
let title = this.title;
if (this.application)
title = this.application.translate(title, this);
this.titleElement.element.innerText = title;
}
// Focus lost event capture
onblur(e) {
if (!this.contains(e.relatedTarget))
this.element.setAttribute("focus", "false");
}
// Client pointer down event handler
onclientdown(e) {
e.preventDefault();
e.stopPropagation();
this.focus();
}
// Focus gained event capture
onfocus(e) {
// Configure element
this.element.setAttribute("focus", "true");
// Working variables
let target = e.target;
// Delegate focus to the most recently focused component
if (target == this.element)
target = this.lastFocus;
// The component is not visible: focus on self instead
if ("component" in target && !target.component.isVisible())
target = this.element;
// Configure instance fields
this.lastFocus = target;
// Transfer focus to the correct component
if (target != e.target)
target.focus();
}
// Key down event handler
onkeydown(e) {
// Processing by key
switch (e.key) {
default: return;
}
// Configure event
e.preventDefault();
e.stopPropagation();
}
// Pointer down event handler
onpointerdown(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Configure element
this.focus();
// Error checking
if (
e.button != 0 ||
this.element.hasPointerCapture(e.pointerId)
) return;
// Configure instance fields
this.dragBounds = this.getBounds();
this.dragEdge = this.edge(e, this.dragBounds);
this.dragCursor.x = e.x;
this.dragCursor.y = e.y;
// Configure element
this.element.setPointerCapture(e.pointerId);
}
// Pointer move event handler
onpointermove(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Not dragging: set the cursor based on pointer location
if (!this.element.hasPointerCapture(e.pointerId)) {
let region = this.edge(e);
if (region == null)
this.element.style.removeProperty("cursor");
else this.element.style.cursor = region + "-resize";
return;
}
// Working variables
let rX = e.x - this.dragCursor.x;
let rY = e.y - this.dragCursor.y;
let bounds = this.getBounds();
let desktop = this.parent.getBounds();
let client = this.client.getBounds();
let minHeight = bounds.height - client.height;
// Move the window
if (this.dragEdge == null) {
this.contain(
desktop,
bounds,
this.dragBounds.x - desktop.x + rX,
this.dragBounds.y - desktop.y + rY,
client
);
return;
}
// Resizing on the north edge
if (this.dragEdge.startsWith("n")) {
let maxTop = desktop.height - client.y + bounds.y;
let top = this.dragBounds.y - desktop.y + rY;
let height = this.dragBounds.height - rY;
// Restrict window bounds
if (top > maxTop) {
height -= maxTop - top;
top = maxTop;
}
if (top < 0) {
height += top;
top = 0;
}
if (height < minHeight) {
top -= minHeight - height;
height = minHeight;
}
// Configure element
this.setTop (top );
this.setHeight(height);
}
// Resizing on the west edge
if (this.dragEdge.endsWith("w")) {
let maxLeft = desktop.width - 16;
let left = this.dragBounds.x - desktop.x + rX;
let width = this.dragBounds.width - rX;
// Restrict window bounds
if (left > maxLeft) {
width -= maxLeft - left;
left = maxLeft;
}
if (width < 64) {
left -= 64 - width;
width = 64;
}
// Configure element
this.setLeft (left );
this.setWidth(width);
}
// Resizing on the east edge
if (this.dragEdge.endsWith("e")) {
let width = this.dragBounds.width + rX;
// Restrict window bounds
width = Math.max(64, width);
width = Math.max(width, -this.dragBounds.x + 16);
// Configure element
this.setWidth(width);
}
// Resizing on the south edge
if (this.dragEdge.startsWith("s")) {
let height = this.dragBounds.height + rY;
// Restrict window bounds
height = Math.max(minHeight, height);
// Configure element
this.setHeight(height);
}
}
// Pointer up event handler
onpointerup(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Error checking
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure element
this.element.releasePointerCapture(e.pointerId);
}
};

View File

@ -1,7 +1,7 @@
.PHONY: help
help:
@echo
@echo "Virtual Boy Emulator - August 22, 2021"
@echo "Virtual Boy Emulator - August 26, 2021"
@echo
@echo "Target build environment is any Debian with the following packages:"
@echo " emscripten"