Adding desktop/window components
This commit is contained in:
parent
dd066e0bbb
commit
f753f9f59b
28
app/App.js
28
app/App.js
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
--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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
--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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
--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");
|
||||
}
|
||||
|
|
|
@ -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.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);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -1,31 +1,26 @@
|
|||
"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.enabled = "enabled" in options ? !!options.enabled : true;
|
||||
this.focusable = "focusable" in options?!!options.focusable:true;
|
||||
this.name = options.name || "";
|
||||
this.text = options.text || "";
|
||||
this.toggleable = "toggleable"in options?options.toggleable:false;
|
||||
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));
|
||||
|
@ -33,160 +28,157 @@ Toolkit.Button = class Button extends Toolkit.Component {
|
|||
|
||||
// Configure properties
|
||||
this.setEnabled (this.enabled );
|
||||
this.setPressed (this.pressed );
|
||||
this.setTabStop (this.tabStop );
|
||||
this.setFocusable(this.focusable);
|
||||
this.setName (this.name );
|
||||
this.setText (this.text );
|
||||
this.setToggleable(this.toggleable);
|
||||
application.addComponent(this);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
// Delegate to the MenuItem handler for these keys
|
||||
case " " :
|
||||
case "ArrowDown":
|
||||
case "ArrowLeft":
|
||||
case "End" :
|
||||
case "Enter" :
|
||||
this.activate(true);
|
||||
break;
|
||||
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;
|
||||
|
||||
// Conditional
|
||||
case "ArrowUp":
|
||||
|
||||
// Open the menu and select the last item (if any)
|
||||
case "ArrowUp":
|
||||
if (this.parent == this.menuBar) {
|
||||
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();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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,16 +80,27 @@ 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 (this.contains(e.relatedTarget))
|
||||
return;
|
||||
let from = e.relatedTarget;
|
||||
if (from == null)
|
||||
from = document.body;
|
||||
if ("component" in from)
|
||||
|
@ -77,9 +108,6 @@ Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
|
|||
this.lastFocus = from;
|
||||
}
|
||||
|
||||
super.focusChanged(from, to);
|
||||
}
|
||||
|
||||
// Update display text with localized strings
|
||||
localize() {
|
||||
let text = this.name;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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("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)
|
||||
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)
|
||||
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":
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 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 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 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" );
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
};
|
Loading…
Reference in New Issue