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() {
|
constructor() {
|
||||||
|
|
||||||
// Configure themes
|
// Configure themes
|
||||||
Bundle.get("app/theme/base.css").style();
|
Bundle.get("app/theme/kiosk.css").style();
|
||||||
this.themes = {
|
this.themes = {
|
||||||
dark : Bundle.get("app/theme/dark.css" ).style(false),
|
dark : Bundle.get("app/theme/dark.css" ).style(false),
|
||||||
light : Bundle.get("app/theme/light.css" ).style(true ),
|
light : Bundle.get("app/theme/light.css" ).style(true ),
|
||||||
|
@ -16,23 +16,24 @@ globalThis.App = class App {
|
||||||
this.theme = this.themes["light"];
|
this.theme = this.themes["light"];
|
||||||
|
|
||||||
// Produce toolkit instance
|
// 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);
|
document.body.appendChild(this.gui.element);
|
||||||
window.addEventListener("resize", ()=>{
|
window.addEventListener("resize", ()=>{
|
||||||
this.gui.element.style.height = window.innerHeight + "px";
|
this.gui.setSize(window.innerWidth+"px", window.innerHeight+"px");
|
||||||
this.gui.element.style.width = window.innerWidth + "px";
|
|
||||||
});
|
});
|
||||||
|
window.dispatchEvent(new Event("resize"));
|
||||||
|
|
||||||
// Configure locales
|
// Configure locales
|
||||||
this.gui.addLocale(Bundle.get("app/locale/en-US.js").toString());
|
this.gui.addLocale(Bundle.get("app/locale/en-US.js").toString());
|
||||||
this.gui.setLocale(navigator.language);
|
this.gui.setLocale(navigator.language);
|
||||||
|
|
||||||
// Configure GUI
|
|
||||||
this.gui.setSplitLayout("top", false);
|
|
||||||
|
|
||||||
// Menu bar
|
// Menu bar
|
||||||
this.mainMenu = this.gui.newMenuBar({ name: "{menu._}" });
|
this.mainMenu = this.gui.newMenuBar({ name: "{menu._}" });
|
||||||
this.gui.add(this.mainMenu);
|
this.gui.add(this.mainMenu);
|
||||||
|
this.gui.addPropagationListener(e=>this.mainMenu.restoreFocus());
|
||||||
|
|
||||||
// File menu
|
// File menu
|
||||||
let menu = this.mainMenu.newMenu({ text: "{menu.file._}"});
|
let menu = this.mainMenu.newMenu({ text: "{menu.file._}"});
|
||||||
|
@ -47,6 +48,19 @@ globalThis.App = class App {
|
||||||
item.addClickListener(()=>this.setTheme("dark"));
|
item.addClickListener(()=>this.setTheme("dark"));
|
||||||
item = menu.newMenuItem({ text: "{menu.theme.virtual}"});
|
item = menu.newMenuItem({ text: "{menu.theme.virtual}"});
|
||||||
item.addClickListener(()=>this.setTheme("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/MenuBar.js");
|
||||||
await Bundle.run("app/toolkit/MenuItem.js");
|
await Bundle.run("app/toolkit/MenuItem.js");
|
||||||
await Bundle.run("app/toolkit/Menu.js");
|
await Bundle.run("app/toolkit/Menu.js");
|
||||||
|
await Bundle.run("app/toolkit/Window.js");
|
||||||
new App();
|
new App();
|
||||||
};
|
};
|
||||||
run();
|
run();
|
||||||
|
|
|
@ -2,10 +2,15 @@
|
||||||
key : "en-US",
|
key : "en-US",
|
||||||
name: "English (United States)",
|
name: "English (United States)",
|
||||||
app : {
|
app : {
|
||||||
|
close : "Close",
|
||||||
|
console : "Console",
|
||||||
romLoaded : "Successfully loaded file \"{filename}\" ({size})",
|
romLoaded : "Successfully loaded file \"{filename}\" ({size})",
|
||||||
romNotVB : "The selected file is not a Virtual Boy ROM.",
|
romNotVB : "The selected file is not a Virtual Boy ROM.",
|
||||||
readFileError: "Unable to read the selected file."
|
readFileError: "Unable to read the selected file."
|
||||||
},
|
},
|
||||||
|
memory: {
|
||||||
|
_: "Memory"
|
||||||
|
},
|
||||||
menu: {
|
menu: {
|
||||||
_ : "Main application menu",
|
_ : "Main application menu",
|
||||||
file: {
|
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 {
|
:root {
|
||||||
--background : #222222;
|
--control : #222222;
|
||||||
--border-dark : #555555;
|
--control-focus : #444444;
|
||||||
--border-light : #999999;
|
--control-shadow : #999999;
|
||||||
--border-weak : #666666;
|
--desktop : #111111;
|
||||||
--border-strong : #999999;
|
--text : #cccccc;
|
||||||
--button : #444444;
|
--window-blur : #555555;
|
||||||
--button-hover : #4c4c4c;
|
--window-blur-text : #cccccc;
|
||||||
--button-pressed: #555555;
|
--window-close-text: #cccccc;
|
||||||
--focus-ring : #0099ff;
|
--window-focus : #007ACC;
|
||||||
--shadow : #00000080;
|
--window-focus-text: #ffffff;
|
||||||
--text : #cccccc;
|
|
||||||
}
|
}
|
|
@ -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 {
|
:root {
|
||||||
--background : #ffffff;
|
--control : #eeeeee;
|
||||||
--border-dark : #666666;
|
--control-focus : #cccccc;
|
||||||
--border-light : #aaaaaa;
|
--control-shadow : #999999;
|
||||||
--border-weak : #aaaaaa;
|
--desktop : #cccccc;
|
||||||
--border-strong : #666666;
|
--text : #000000;
|
||||||
--button : #cccccc;
|
--window-blur : #cccccc;
|
||||||
--button-hover : #d5d5d5;
|
--window-blur-text : #444444;
|
||||||
--button-pressed: #dddddd;
|
--window-close-text: #444444;
|
||||||
--focus-ring : #0099ff;
|
--window-focus : #80ccff;
|
||||||
--shadow : #00000055;
|
--window-focus-text: #000000;
|
||||||
--text : #000000;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
:root {
|
:root {
|
||||||
--background : #000000;
|
--control : #000000;
|
||||||
--border-dark : #550000;
|
--control-focus : #550000;
|
||||||
--border-light : #aa0000;
|
--control-shadow : #aa0000;
|
||||||
--border-weak : #550000;
|
--desktop : #000000;
|
||||||
--border-strong : #aa0000;
|
--text : #ff0000;
|
||||||
--button : #000000;
|
--window-blur : #000000;
|
||||||
--button-hover : #550000;
|
--window-blur-text : #aa0000;
|
||||||
--button-pressed: #aa0000;
|
--window-close-text: #ff0000;
|
||||||
--focus-ring : #ff0000;
|
--window-focus : #550000;
|
||||||
--shadow : #ff000080;
|
--window-focus-text: #ff0000;
|
||||||
--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 {
|
Toolkit.Application = class Application extends Toolkit.Panel {
|
||||||
|
|
||||||
// Object constructor
|
// Object constructor
|
||||||
constructor() {
|
constructor(options) {
|
||||||
super(null);
|
super(null, options);
|
||||||
|
|
||||||
// Configure instance fields
|
// Configure instance fields
|
||||||
this.application = this;
|
this.application = this;
|
||||||
this.components = [];
|
this.components = [];
|
||||||
this.locale = null;
|
this.locale = null;
|
||||||
this.locales = { first: null };
|
this.locales = { first: null };
|
||||||
|
this.propagationListeners = [];
|
||||||
|
|
||||||
// Configure element
|
// Configure element
|
||||||
this.element.setAttribute("application", "");
|
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;
|
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
|
// Produce a list of all registered locale keys
|
||||||
listLocales() {
|
listLocales() {
|
||||||
return Object.values(this.locales);
|
return Object.values(this.locales);
|
||||||
|
@ -189,4 +198,12 @@ Toolkit.Application = class Application extends Toolkit.Panel {
|
||||||
return typeof locale == "string" ? locale : null;
|
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,192 +1,184 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// Clickable button
|
// Push button
|
||||||
Toolkit.Button = class Button extends Toolkit.Component {
|
Toolkit.Button = class Button extends Toolkit.Component {
|
||||||
|
|
||||||
// Object constructor
|
// Object constructor
|
||||||
constructor(application, options) {
|
constructor(application, options) {
|
||||||
super(application, "div");
|
super(application, "div", options);
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
// Configure instance fields
|
// Configure instance fields
|
||||||
this.blurListeners = [];
|
|
||||||
this.clickListeners = [];
|
this.clickListeners = [];
|
||||||
this.enabled = "enabled" in options ? options.enabled : true;
|
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||||
this.focusListeners = [];
|
this.focusable = "focusable" in options?!!options.focusable:true;
|
||||||
this.pressed = "pressed" in options ? options.pressed : false;
|
this.name = options.name || "";
|
||||||
this.tabStop = true;
|
this.text = options.text || "";
|
||||||
this.text = options.text || "";
|
this.toolTip = options.toolTip || "";
|
||||||
this.toggleable = "toggleable"in options?options.toggleable:false;
|
|
||||||
|
|
||||||
// Configure element
|
// Configure element
|
||||||
this.element.type = "button";
|
|
||||||
this.element.setAttribute("role", "button");
|
this.element.setAttribute("role", "button");
|
||||||
|
this.element.setAttribute("tabindex", "0");
|
||||||
this.element.style.cursor = "default";
|
this.element.style.cursor = "default";
|
||||||
this.element.style.position = "relative";
|
|
||||||
this.element.style.userSelect = "none";
|
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("keydown" , e=>this.onkeydown (e));
|
||||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||||
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||||
|
|
||||||
// Configure properties
|
// Configure properties
|
||||||
this.setEnabled (this.enabled );
|
this.setEnabled (this.enabled );
|
||||||
this.setPressed (this.pressed );
|
this.setFocusable(this.focusable);
|
||||||
this.setTabStop (this.tabStop );
|
this.setName (this.name );
|
||||||
this.setText (this.text );
|
this.setText (this.text );
|
||||||
this.setToggleable(this.toggleable);
|
this.setToolTip (this.toolTip );
|
||||||
application.addComponent(this);
|
this.application.addComponent(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////// Public Methods //////////////////////////////
|
///////////////////////////// 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
|
// Add a callback for click events
|
||||||
addClickListener(listener) {
|
addClickListener(listener) {
|
||||||
if (this.clickListeners.indexOf(listener) == -1)
|
if (this.clickListeners.indexOf(listener) == -1)
|
||||||
this.clickListeners.push(listener);
|
this.clickListeners.push(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a callback for focus events
|
// Request focus on the appropriate element
|
||||||
addFocusListener(listener) {
|
focus() {
|
||||||
if (this.focusListeners.indexOf(listener) == -1)
|
this.element.focus();
|
||||||
this.focusListeners.push(listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine whether the component participates in the tab sequence
|
// Retrieve the button's accessible name
|
||||||
getTabStop() {
|
getName() {
|
||||||
return this.tabStop;
|
return this.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the control's text
|
// Retrieve the button's display text
|
||||||
getText() {
|
getText() {
|
||||||
return this.text;
|
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() {
|
isEnabled() {
|
||||||
return this.enabled;
|
return this.enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the toggle button's active state
|
// Determine whether the button is focusable
|
||||||
isPressed() {
|
isFocusable() {
|
||||||
return this.pressed;
|
return this.focusable;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine whether the button is a toggle button
|
// Specify whether the button is enabled
|
||||||
isToggleable() {
|
|
||||||
return this.toggleable;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Specify whether the control is enabled
|
|
||||||
setEnabled(enabled) {
|
setEnabled(enabled) {
|
||||||
this.enabled = enabled = !!enabled;
|
this.enabled = enabled = !!enabled;
|
||||||
if (enabled)
|
this.element.setAttribute("aria-disabled", !enabled);
|
||||||
this.element.removeAttribute("disabled");
|
|
||||||
else this.element.setAttribute("disabled", "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specify whether the component participates in the regular tab sequence
|
// Specify whether the button can receive focus
|
||||||
setTabStop(tabStop) {
|
setFocusable(focusable) {
|
||||||
this.tabStop = tabStop = !!tabStop;
|
this.focusable = focusable = !!focusable;
|
||||||
this.element.setAttribute("tabindex", tabStop ? "0" : "-1");
|
if (focusable)
|
||||||
|
this.element.setAttribute("tabindex", "0");
|
||||||
|
else this.element.removeAttribute("tabindex");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specify the toggle button's active state
|
// Specify the button's accessible name
|
||||||
setPressed(pressed) {
|
setName(name) {
|
||||||
this.pressed = pressed = !!pressed;
|
this.name = name || "";
|
||||||
if (this.toggleable)
|
this.localize();
|
||||||
this.element.setAttribute("aria-pressed", pressed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specify the control's text
|
// Specify the button's display text
|
||||||
setText(text) {
|
setText(text) {
|
||||||
this.text = text || "";
|
this.text = text || "";
|
||||||
this.localize();
|
this.localize();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specify whether the button is a toggle button
|
// Specify the button's tool tip text
|
||||||
setToggleable(toggleable) {
|
setToolTip(toolTip) {
|
||||||
this.toggleable = toggleable = !!toggleable;
|
this.toolTip = toolTip || "";
|
||||||
if (toggleable)
|
this.localize();
|
||||||
this.element.setAttribute("aria-pressed", this.pressed);
|
}
|
||||||
else this.element.removeAttribute("aria-pressed");
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// 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 /////////////////////////////
|
///////////////////////////// Private Methods /////////////////////////////
|
||||||
|
|
||||||
// Actions when the button is activated
|
// The button was activated
|
||||||
activate(e) {
|
activate(e) {
|
||||||
if (this.toggleable)
|
if (!this.enabled)
|
||||||
this.setPressed(!this.pressed);
|
return;
|
||||||
for (let listener of this.clickListeners)
|
for (let listener of this.clickListeners)
|
||||||
listener(e, this);
|
listener(e, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update display text with localized strings
|
// Key down event handler
|
||||||
localize() {
|
|
||||||
let text = this.text;
|
|
||||||
if (this.application)
|
|
||||||
text = this.application.translate(text, this);
|
|
||||||
this.element.innerText = text;
|
|
||||||
this.element.setAttribute("aria-label", text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Blur event handler
|
|
||||||
onblur(e) {
|
|
||||||
for (let listener of this.blurListeners)
|
|
||||||
listener(e, this);
|
|
||||||
this.focusChanged(
|
|
||||||
this, e.relatedTarget ? e.relatedTarget.component : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus event handler
|
|
||||||
onfocus(e) {
|
|
||||||
for (let listener of this.focusListeners)
|
|
||||||
listener(e, this);
|
|
||||||
this.focusChanged(
|
|
||||||
e.relatedTarget ? e.relatedTarget.component : null, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key press event handler
|
|
||||||
onkeydown(e) {
|
onkeydown(e) {
|
||||||
|
|
||||||
// Error checking
|
// Error checking
|
||||||
if (e.key != " " && e.key != "Enter")
|
if (!this.enabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Processing by key
|
||||||
|
switch (e.key) {
|
||||||
|
case " ":
|
||||||
|
case "Enter":
|
||||||
|
this.activate(e);
|
||||||
|
break;
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
|
||||||
// Configure event
|
// Configure event
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// The button was activated
|
|
||||||
this.activate(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pointer down event handler
|
// Pointer down event handler
|
||||||
onpointerdown(e) {
|
onpointerdown(e) {
|
||||||
|
|
||||||
// Error checking
|
|
||||||
if (e.button != 0 || this.element.hasPointerCapture(e.pointerId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Configure event
|
// Configure event
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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
|
// Configure element
|
||||||
this.element.focus();
|
|
||||||
this.element.setPointerCapture(e.pointerId);
|
this.element.setPointerCapture(e.pointerId);
|
||||||
this.element.setAttribute("active", "");
|
this.element.setAttribute("active", "");
|
||||||
}
|
}
|
||||||
|
@ -194,22 +186,22 @@ Toolkit.Button = class Button extends Toolkit.Component {
|
||||||
// Pointer move event handler
|
// Pointer move event handler
|
||||||
onpointermove(e) {
|
onpointermove(e) {
|
||||||
|
|
||||||
// Error checking
|
|
||||||
if (!this.element.hasPointerCapture(e.pointerId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Configure event
|
// Configure event
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Error checking
|
||||||
|
if (!this.element.hasPointerCapture(e))
|
||||||
|
return;
|
||||||
|
|
||||||
// Working variables
|
// Working variables
|
||||||
let bounds = this.element.getBoundingClientRect();
|
let bounds = this.getBounds();
|
||||||
let active =
|
let active =
|
||||||
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
|
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
|
||||||
e.y >= bounds.y && e.y < bounds.y + bounds.height
|
e.y >= bounds.y && e.y < bounds.y + bounds.height
|
||||||
;
|
;
|
||||||
|
|
||||||
// Configure event
|
// Configure element
|
||||||
if (active)
|
if (active)
|
||||||
this.element.setAttribute("active", "");
|
this.element.setAttribute("active", "");
|
||||||
else this.element.removeAttribute("active");
|
else this.element.removeAttribute("active");
|
||||||
|
@ -218,26 +210,21 @@ Toolkit.Button = class Button extends Toolkit.Component {
|
||||||
// Pointer up event handler
|
// Pointer up event handler
|
||||||
onpointerup(e) {
|
onpointerup(e) {
|
||||||
|
|
||||||
// Error checking
|
|
||||||
if (!this.element.hasPointerCapture(e.pointerId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Configure event
|
// Configure event
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// Working variables
|
// Error checking
|
||||||
let active = this.element.hasAttribute("active");
|
if (!this.element.hasPointerCapture(e.pointerId))
|
||||||
|
return;
|
||||||
|
|
||||||
// Configure element
|
// Configure element
|
||||||
this.element.releasePointerCapture(e.pointerId);
|
this.element.releasePointerCapture(e.pointerId);
|
||||||
this.element.removeAttribute("active");
|
|
||||||
|
|
||||||
// The pointer was released without activating the button
|
// Activate the menu item if it is active
|
||||||
if (!active)
|
if (!this.element.hasAttribute("active"))
|
||||||
return;
|
return;
|
||||||
|
this.element.removeAttribute("active");
|
||||||
// The button was activated
|
|
||||||
this.activate(e);
|
this.activate(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,19 +4,24 @@
|
||||||
Toolkit.Component = class Component {
|
Toolkit.Component = class Component {
|
||||||
|
|
||||||
// Object constructor
|
// Object constructor
|
||||||
constructor(application, tagname) {
|
constructor(application, tagname, options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
// Configure instance fields
|
// Configure instance fields
|
||||||
this.application = application;
|
this.application = application;
|
||||||
this.containers = [ this ];
|
this.containers = [ this ];
|
||||||
|
this.display = null;
|
||||||
this.element = document.createElement(tagname);
|
this.element = document.createElement(tagname);
|
||||||
this.id = this.element.id = Toolkit.id();
|
this.id = this.element.id = Toolkit.id();
|
||||||
this.parent = null;
|
this.parent = null;
|
||||||
this.properties = {};
|
this.properties = {};
|
||||||
this.resizeListeners = [];
|
this.resizeListeners = [];
|
||||||
|
this.resizeObserver = null;
|
||||||
|
this.visible = "visible" in options ? !!options.visible : true;
|
||||||
|
|
||||||
// Configure component
|
// Configure component
|
||||||
this.element.component = this;
|
this.element.component = this;
|
||||||
|
this.setVisible(this.visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,8 +32,10 @@ Toolkit.Component = class Component {
|
||||||
addResizeListener(listener) {
|
addResizeListener(listener) {
|
||||||
if (this.resizeListeners.indexOf(listener) != -1)
|
if (this.resizeListeners.indexOf(listener) != -1)
|
||||||
return;
|
return;
|
||||||
if (this.resizeListeners.length == 0)
|
if (this.resizeObserver == null) {
|
||||||
new ResizeObserver(()=>this.onresize()).observe(this.element);
|
this.resizeObserver = new ResizeObserver(()=>this.onresized());
|
||||||
|
this.resizeObserver.observe(this.element);
|
||||||
|
}
|
||||||
this.resizeListeners.push(listener);
|
this.resizeListeners.push(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,18 +49,77 @@ Toolkit.Component = class Component {
|
||||||
return this.element.getBoundingClientRect();
|
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
|
// Specify the height of the element
|
||||||
setHeight(height) {
|
setHeight(height) {
|
||||||
if (height === null)
|
if (height === null)
|
||||||
this.element.style.removeProperty("height");
|
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
|
// Specify the width of the element
|
||||||
setWidth(width) {
|
setWidth(width) {
|
||||||
if (width === null)
|
if (width === null)
|
||||||
this.element.style.removeProperty("width");
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify of a change to component focus
|
|
||||||
focusChanged(from, to) {
|
|
||||||
if (this.parent != null)
|
|
||||||
this.parent.focusChanged(from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////// Private Methods /////////////////////////////
|
///////////////////////////// Private Methods /////////////////////////////
|
||||||
|
|
||||||
// Resize event handler
|
// Resize event handler
|
||||||
onresize() {
|
onresized() {
|
||||||
let bounds = this.getBounds();
|
let bounds = this.getBounds();
|
||||||
for (let listener of this.resizeListeners)
|
for (let listener of this.resizeListeners)
|
||||||
listener(bounds, this);
|
listener(bounds, this);
|
||||||
|
|
|
@ -4,46 +4,45 @@
|
||||||
Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||||
|
|
||||||
// Object constructor
|
// Object constructor
|
||||||
constructor(parent, options) {
|
constructor(application, options) {
|
||||||
super(parent, options);
|
super(application, options);
|
||||||
|
|
||||||
// Configure instance fields
|
|
||||||
this.items = [];
|
|
||||||
this.parent = parent;
|
|
||||||
|
|
||||||
// Configure menu element
|
// Configure menu element
|
||||||
this.menu = document.createElement("div");
|
this.menu = this.add(this.application.newPanel({
|
||||||
this.menu.id = Toolkit.id();
|
layout : "flex",
|
||||||
this.menu.style.display = "none";
|
alignCross: "stretch",
|
||||||
this.menu.style.flexDirection = "column";
|
direction : "column",
|
||||||
this.menu.style.position = "absolute";
|
visible : false
|
||||||
this.menu.setAttribute("role", "menu");
|
}));
|
||||||
this.menu.setAttribute("aria-labelledby", this.id);
|
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.containers.push(this.menu);
|
||||||
|
this.children = this.menu.children;
|
||||||
|
|
||||||
// Configure element
|
// Configure element
|
||||||
this.element.setAttribute("aria-expanded", "false");
|
this.element.setAttribute("aria-expanded", "false");
|
||||||
this.element.setAttribute("aria-haspopup", "menu");
|
this.element.setAttribute("aria-haspopup", "menu");
|
||||||
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
|
||||||
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////// Public Methods //////////////////////////////
|
///////////////////////////// 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
|
// Create a MenuItem and associate it with the application and component
|
||||||
newMenuItem(options, index) {
|
newMenuItem(options, index) {
|
||||||
let item = new Toolkit.MenuItem(this, options);
|
let item = this.menu.add(new Toolkit.MenuItem(
|
||||||
|
this.application, options), index);
|
||||||
// Determine the ordinal position of the element within the container
|
item.child();
|
||||||
index = !(typeof index == "number") ? this.items.length :
|
|
||||||
Math.floor(Math.min(Math.max(0, index), this.items.length));
|
|
||||||
|
|
||||||
// Add the item to the menu
|
|
||||||
let ref = this.items[index];
|
|
||||||
this.menu.insertBefore(item.element, ref ? ref.element : null);
|
|
||||||
this.items.splice(index, 0, item);
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,18 +62,8 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||||
if (!this.enabled)
|
if (!this.enabled)
|
||||||
return;
|
return;
|
||||||
this.setExpanded(true);
|
this.setExpanded(true);
|
||||||
if (deeper && this.items.length > 0)
|
if (deeper && this.children.length > 0)
|
||||||
this.items[0].element.focus();
|
this.children[0].focus();
|
||||||
}
|
|
||||||
|
|
||||||
// Notify of a change to component focus
|
|
||||||
focusChanged(from, to) {
|
|
||||||
if (!this.contains(to)) {
|
|
||||||
let expanded = this.parent.expanded;
|
|
||||||
this.setExpanded(false);
|
|
||||||
this.parent.expanded = expanded == this ? null : expanded;
|
|
||||||
}
|
|
||||||
super.focusChanged(from, to);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show or hide the pop-up menu
|
// Show or hide the pop-up menu
|
||||||
|
@ -85,7 +74,7 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||||
|
|
||||||
// Hide the pop-up menu
|
// Hide the pop-up menu
|
||||||
this.element.setAttribute("aria-expanded", "false");
|
this.element.setAttribute("aria-expanded", "false");
|
||||||
this.menu.style.display = "none";
|
this.menu.setVisible(false);
|
||||||
this.parent.expanded = null;
|
this.parent.expanded = null;
|
||||||
|
|
||||||
// Close any expanded submenus
|
// Close any expanded submenus
|
||||||
|
@ -106,9 +95,11 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||||
// Configure pop-up menu
|
// Configure pop-up menu
|
||||||
let barBounds = this.menuBar.element.getBoundingClientRect();
|
let barBounds = this.menuBar.element.getBoundingClientRect();
|
||||||
let bounds = this.element.getBoundingClientRect();
|
let bounds = this.element.getBoundingClientRect();
|
||||||
this.menu.style.display = "flex";
|
this.menu.setVisible(true);
|
||||||
this.menu.style.left = (bounds.x - barBounds.x) + "px";
|
this.menu.setLocation(
|
||||||
this.menu.style.top = (bounds.y + bounds.height - barBounds.y) + "px";
|
(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
|
// Key press event handler
|
||||||
onkeydown(e) {
|
onkeydown(e) {
|
||||||
|
let index;
|
||||||
|
|
||||||
// Processing by key
|
// Processing by key
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
|
|
||||||
// Open the menu and select its first item (if any)
|
// Delegate to the MenuItem handler for these keys
|
||||||
case " ":
|
case " " :
|
||||||
case "ArrowDown":
|
case "ArrowLeft":
|
||||||
case "Enter":
|
case "End" :
|
||||||
this.activate(true);
|
case "Enter" :
|
||||||
break;
|
case "Escape" :
|
||||||
|
case "Home" :
|
||||||
|
return super.onkeydown(e);
|
||||||
|
|
||||||
// Conditional
|
// Conditional
|
||||||
case "ArrowLeft":
|
case "ArrowDown":
|
||||||
|
|
||||||
// Move to the previous item in the menu bar
|
// Open the menu and select the first item (if any)
|
||||||
if (this.parent == this.menuBar) {
|
if (this.parent == this.menuBar)
|
||||||
let menu = this.parent.menus[
|
this.activate(true);
|
||||||
(this.parent.menus.indexOf(this) +
|
|
||||||
this.parent.menus.length - 1) %
|
|
||||||
this.parent.menus.length
|
|
||||||
];
|
|
||||||
if (menu != this && this.parent.expanded != null)
|
|
||||||
menu.activate(true);
|
|
||||||
else menu.element.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the menu and return to the parent menu
|
// Delegate to the MenuItem handler
|
||||||
else {
|
else return super.onkeydown(e);
|
||||||
this.setExpanded(false);
|
|
||||||
this.parent.element.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Conditional
|
// Conditional
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
|
|
||||||
// Move to the next item in the menu bar
|
// Open the menu and select the first item (if any)
|
||||||
if (this.parent == this.menuBar) {
|
if (this.parent != this.menuBar)
|
||||||
let menu = this.parent.menus[
|
this.activate(true);
|
||||||
(this.parent.menus.indexOf(this) + 1) %
|
|
||||||
this.parent.menus.length
|
|
||||||
];
|
|
||||||
if (menu != this) {
|
|
||||||
if (this.parent.expanded != null) {
|
|
||||||
menu.activate(true);
|
|
||||||
} else menu.element.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the menu and select its first item (if any)
|
// Delegate to the MenuItem handler
|
||||||
else this.activate(true);
|
else return super.onkeydown(e);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Open the menu and select the last item (if any)
|
// Conditional
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
this.activate(false);
|
|
||||||
if (this.items.length > 0)
|
|
||||||
this.items[this.items.length - 1].element.focus();
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Conditional
|
// Open the menu and select the last item (if any)
|
||||||
case "End":
|
|
||||||
|
|
||||||
// Select the Last menu in the menu bar
|
|
||||||
if (this.parent == this.menuBar) {
|
if (this.parent == this.menuBar) {
|
||||||
let menu = this.parent.menus[this.parent.menus.length - 1];
|
this.activate(false);
|
||||||
if (menu != this) {
|
index = this.previousChild(0);
|
||||||
if (this.menuBar.expanded != null)
|
if (index != -1)
|
||||||
menu.activate(true);
|
this.children[index].focus();
|
||||||
else menu.element.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select the last item in the parent menu
|
// Delegate to the MenuItem handler
|
||||||
else {
|
else return super.onkeydown(e);
|
||||||
let item = this.parent.items[this.parent.items.length - 1];
|
|
||||||
if (item != this) {
|
|
||||||
this.setExpanded(false);
|
|
||||||
item.element.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
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;
|
default: return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,22 +173,26 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||||
// Pointer down event handler
|
// Pointer down event handler
|
||||||
onpointerdown(e) {
|
onpointerdown(e) {
|
||||||
|
|
||||||
// Error checking
|
|
||||||
if (e.button != 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Configure event
|
// Configure event
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Error checking
|
||||||
|
if (!this.enabled || e.button != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
// Activate the menu
|
// Activate the menu
|
||||||
this.element.focus();
|
this.focus();
|
||||||
this.activate(false);
|
this.activate(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pointer move event handler
|
// Pointer move event handler
|
||||||
onpointermove(e) {
|
onpointermove(e) {
|
||||||
|
|
||||||
|
// Configure event
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
// Error checking
|
// Error checking
|
||||||
if (
|
if (
|
||||||
this.parent != this.menuBar ||
|
this.parent != this.menuBar ||
|
||||||
|
@ -268,8 +203,14 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
|
||||||
// Activate the menu
|
// Activate the menu
|
||||||
this.parent.expanded.setExpanded(false);
|
this.parent.expanded.setExpanded(false);
|
||||||
this.parent.expanded = this;
|
this.parent.expanded = this;
|
||||||
this.element.focus();
|
this.focus();
|
||||||
this.setExpanded(true);
|
this.setExpanded(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pointer up event handler (prevent superclass behavior)
|
||||||
|
onpointerup(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,31 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// Main application menu bar
|
// Main application menu bar
|
||||||
Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
|
Toolkit.MenuBar = class MenuBar extends Toolkit.Panel {
|
||||||
|
|
||||||
// Object constructor
|
// Object constructor
|
||||||
constructor(application, options) {
|
constructor(application, options) {
|
||||||
super(application, "div");
|
super(application, options);
|
||||||
|
|
||||||
// Configure instance fields
|
// Configure instance fields
|
||||||
this.expanded = null;
|
this.expanded = null;
|
||||||
this.lastFocus = null;
|
this.lastFocus = null;
|
||||||
this.menuBar = this;
|
this.menuBar = this;
|
||||||
this.menus = [];
|
|
||||||
this.name = options.name || "";
|
this.name = options.name || "";
|
||||||
|
|
||||||
// Configure element
|
// Configure element
|
||||||
this.element.style.display = "flex";
|
|
||||||
this.element.style.position = "relative";
|
this.element.style.position = "relative";
|
||||||
this.element.style.zIndex = "1";
|
this.element.style.zIndex = "1";
|
||||||
this.element.setAttribute("role", "menubar");
|
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
|
// Configure properties
|
||||||
this.setName(this.name);
|
this.setName(this.name);
|
||||||
|
@ -29,27 +36,40 @@ Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
|
||||||
|
|
||||||
///////////////////////////// Public Methods //////////////////////////////
|
///////////////////////////// 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
|
// Create a Menu and associate it with the application and component
|
||||||
newMenu(options, index) {
|
newMenu(options, index) {
|
||||||
let menu = new Toolkit.Menu(this, options);
|
|
||||||
|
|
||||||
// Determine the ordinal position of the element within the container
|
// Create and add a new menu
|
||||||
index = !(typeof index == "number") ? this.menus.length :
|
let menu = this.add(new Toolkit.Menu(this.application,options), index);
|
||||||
Math.floor(Math.min(Math.max(0, index), this.menus.length));
|
menu.element.insertAdjacentElement("afterend", menu.menu.element);
|
||||||
|
|
||||||
// Add the menu to the menu bar
|
|
||||||
let ref = this.menus[index];
|
|
||||||
this.element.insertBefore(menu.element, ref ? ref.element : null);
|
|
||||||
this.menus.splice(index, 0, menu);
|
|
||||||
menu.element.insertAdjacentElement("afterend", menu.menu);
|
|
||||||
|
|
||||||
// Ensure only the first menu is focusable
|
// Ensure only the first menu is focusable
|
||||||
for (let x = 0; x < this.menus.length; x++)
|
for (let x = 0; x < this.children.length; x++)
|
||||||
this.menus[x].element.setAttribute("tabindex", x==0 ? "0" : "-1");
|
this.children[x].element
|
||||||
|
.setAttribute("tabindex", x == 0 ? "0" : "-1");
|
||||||
|
|
||||||
return menu;
|
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
|
// Specify the menu's accessible name
|
||||||
setName(name) {
|
setName(name) {
|
||||||
this.name = name || "";
|
this.name = name || "";
|
||||||
|
@ -60,24 +80,32 @@ Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
|
||||||
|
|
||||||
///////////////////////////// Package Methods /////////////////////////////
|
///////////////////////////// Package Methods /////////////////////////////
|
||||||
|
|
||||||
// Notify of a change to component focus
|
// Blur event capture
|
||||||
focusChanged(from, to) {
|
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
|
// Configure tabstop on the first menu
|
||||||
if (this.menus.length > 0)
|
if (this.children.length > 0)
|
||||||
this.menus[0].element.setAttribute("tabindex",
|
this.children[0].element.setAttribute("tabindex", "-1");
|
||||||
this.contains(to) ? "-1" : "0");
|
|
||||||
|
|
||||||
// Retain a reference to the previously focused element
|
// Retain a reference to the previously focused element
|
||||||
if (!this.contains(from)) {
|
if (this.contains(e.relatedTarget))
|
||||||
if (from == null)
|
return;
|
||||||
from = document.body;
|
let from = e.relatedTarget;
|
||||||
if ("component" in from)
|
if (from == null)
|
||||||
from = from.component;
|
from = document.body;
|
||||||
this.lastFocus = from;
|
if ("component" in from)
|
||||||
}
|
from = from.component;
|
||||||
|
this.lastFocus = from;
|
||||||
super.focusChanged(from, to);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update display text with localized strings
|
// Update display text with localized strings
|
||||||
|
@ -88,12 +116,4 @@ Toolkit.MenuBar = class MenuBar extends Toolkit.Component {
|
||||||
this.element.setAttribute("aria-label", text);
|
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";
|
"use strict";
|
||||||
|
|
||||||
// Selection within a Menu
|
// Selection within a Menu
|
||||||
Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
|
Toolkit.MenuItem = class MenuItem extends Toolkit.Panel {
|
||||||
|
|
||||||
// Object constructor
|
// Object constructor
|
||||||
constructor(parent, options) {
|
constructor(application, options) {
|
||||||
super(parent && parent.application, "div");
|
super(application, options);
|
||||||
|
|
||||||
// Configure instance fields
|
// Configure instance fields
|
||||||
this.clickListeners = [];
|
this.clickListeners = [];
|
||||||
this.enabled = "enabled" in options ? !!options.enabled : true;
|
this.enabled = "enabled" in options ? !!options.enabled : true;
|
||||||
this.icon = options.icon || null;
|
this.icon = options.icon || null;
|
||||||
this.menuBar = parent.menuBar;
|
|
||||||
this.parent = parent;
|
|
||||||
this.text = options.text || "";
|
this.text = options.text || "";
|
||||||
this.shortcut = options.shortcut || null;
|
this.shortcut = options.shortcut || null;
|
||||||
|
|
||||||
// Configure base element
|
// Configure base element
|
||||||
this.element.style.display = "flex";
|
this.setLayout("flex", {});
|
||||||
this.element.setAttribute("role", "menuitem");
|
this.element.setAttribute("role", "menuitem");
|
||||||
this.element.setAttribute("tabindex", "-1");
|
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));
|
this.element.addEventListener("keydown", e=>this.onkeydown(e));
|
||||||
if (this.parent != this.menuBar)
|
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
|
||||||
this.element.addEventListener("pointerup", e=>this.onpointerup(e));
|
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
|
||||||
|
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
|
||||||
|
|
||||||
// Configure display text element
|
// Configure display text element
|
||||||
this.textElement = document.createElement("div");
|
this.textElement = document.createElement("div");
|
||||||
|
this.textElement.id = Toolkit.id();
|
||||||
this.textElement.style.cursor = "default";
|
this.textElement.style.cursor = "default";
|
||||||
this.textElement.style.flexGrow = "1";
|
this.textElement.style.flexGrow = "1";
|
||||||
this.textElement.style.userSelect = "none";
|
this.textElement.style.userSelect = "none";
|
||||||
|
this.textElement.setAttribute("name", "text");
|
||||||
this.element.appendChild(this.textElement);
|
this.element.appendChild(this.textElement);
|
||||||
|
this.element.setAttribute("aria-labelledby", this.textElement.id);
|
||||||
|
|
||||||
// Configure properties
|
// Configure properties
|
||||||
this.setEnabled(this.enabled);
|
this.setEnabled(this.enabled);
|
||||||
|
@ -49,6 +49,11 @@ Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
|
||||||
this.clickListeners.push(listener);
|
this.clickListeners.push(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request focus on the appropriate element
|
||||||
|
focus() {
|
||||||
|
this.element.focus();
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve the item's display text
|
// Retrieve the item's display text
|
||||||
getText() {
|
getText() {
|
||||||
return this.text;
|
return this.text;
|
||||||
|
@ -77,12 +82,24 @@ Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
|
||||||
|
|
||||||
///////////////////////////// Package Methods /////////////////////////////
|
///////////////////////////// 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
|
// Update display text with localized strings
|
||||||
localize() {
|
localize() {
|
||||||
let text = this.text;
|
let text = this.text;
|
||||||
if (this.application)
|
if (this.application)
|
||||||
text = this.application.translate(text, this);
|
text = this.application.translate(text, this);
|
||||||
this.element.setAttribute("aria-label", text);
|
|
||||||
this.textElement.innerText = text;
|
this.textElement.innerText = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,23 +114,11 @@ Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
|
||||||
this.menuBar.restoreFocus();
|
this.menuBar.restoreFocus();
|
||||||
for (let listener of this.clickListeners)
|
for (let listener of this.clickListeners)
|
||||||
listener(e, this);
|
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
|
// Key press event handler
|
||||||
onkeydown(e) {
|
onkeydown(e) {
|
||||||
|
let index;
|
||||||
|
|
||||||
// Processing by key
|
// Processing by key
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
|
@ -126,88 +131,189 @@ Toolkit.MenuItem = class MenuItem extends Toolkit.Component {
|
||||||
|
|
||||||
// Select the next item
|
// Select the next item
|
||||||
case "ArrowDown":
|
case "ArrowDown":
|
||||||
this.parent.items[
|
index = this.parent.nextChild(
|
||||||
(this.parent.items.indexOf(this) + 1) %
|
this.parent.children.indexOf(this));
|
||||||
this.parent.items.length
|
if (index != -1)
|
||||||
].element.focus();
|
this.parent.children[index].focus();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Conditional
|
// Conditional
|
||||||
case "ArrowLeft":
|
case "ArrowLeft":
|
||||||
|
|
||||||
// Move to the previous menu in the menu bar
|
// Move to the previous menu in the menu bar
|
||||||
if (this.parent.parent == this.menuBar) {
|
if (this.menuItem.parent == this.menuBar) {
|
||||||
let menu = this.menuBar.menus[
|
index = this.menuBar.previousChild(
|
||||||
(this.menuBar.menus.indexOf(this.parent) +
|
this.menuBar.children.indexOf(this.menuItem));
|
||||||
this.menuBar.menus.length - 1) %
|
if (index != -1) {
|
||||||
this.menuBar.menus.length
|
let menu = this.menuBar.children[index];
|
||||||
];
|
if (menu != this.menuTop) {
|
||||||
if (menu != this.parent)
|
if (this.menuBar.expanded != null)
|
||||||
menu.activate(true);
|
menu.activate(true);
|
||||||
|
else menu.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the containing submenu
|
// Close the containing submenu
|
||||||
else {
|
else {
|
||||||
this.parent.setExpanded(false);
|
this.menuItem.setExpanded(false);
|
||||||
this.parent.parent.element.focus();
|
this.menuItem.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Move to the next menu in the menu bar
|
// Move to the next menu in the menu bar
|
||||||
case "ArrowRight":
|
case "ArrowRight":
|
||||||
let menu = this.menuBar.menus[
|
index = this.menuBar.nextChild(
|
||||||
(this.menuBar.menus.indexOf(this.menuBar.expanded) + 1) %
|
this.menuBar.children.indexOf(this.menuTop));
|
||||||
this.menuBar.menus.length
|
if (index != -1) {
|
||||||
];
|
let menu = this.menuBar.children[index];
|
||||||
if (this.menuBar.expanded != menu)
|
if (menu != this.menuTop) {
|
||||||
menu.activate(true);
|
if (this.menuBar.expanded != null)
|
||||||
|
menu.activate(true);
|
||||||
|
else menu.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Select the previous item
|
// Select the previous item
|
||||||
case "ArrowUp":
|
case "ArrowUp":
|
||||||
this.parent.items[
|
index = this.parent.previousChild(
|
||||||
(this.parent.items.indexOf(this) +
|
this.parent.children.indexOf(this));
|
||||||
this.parent.items.length - 1) %
|
if (index != -1)
|
||||||
this.parent.items.length
|
this.parent.children[index].focus();
|
||||||
].element.focus();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Select the last item in the menu
|
// Conditional
|
||||||
case "End":
|
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;
|
break;
|
||||||
|
|
||||||
// Return focus to the original element
|
// Return focus to the original element
|
||||||
case "Escape":
|
case "Escape":
|
||||||
this.menuBar.expanded.setExpanded(false);
|
if (this.menuBar.expanded != null)
|
||||||
|
this.menuBar.expanded.setExpanded(false);
|
||||||
|
this.menuBar.restoreFocus();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Select the first item in the menu
|
// Conditional
|
||||||
case "Home":
|
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;
|
break;
|
||||||
|
|
||||||
default: return;
|
default: return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure element
|
// Configure event
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pointer up event handler
|
// Pointer down event handler
|
||||||
onpointerup(e) {
|
onpointerdown(e) {
|
||||||
|
|
||||||
// Error checking
|
|
||||||
if (e.button != 0 || document.activeElement != this.element)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Configure event
|
// Configure event
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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);
|
this.activate(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,28 +4,31 @@
|
||||||
Toolkit.Panel = class Panel extends Toolkit.Component {
|
Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||||
|
|
||||||
// Object constructor
|
// Object constructor
|
||||||
constructor(application) {
|
constructor(application, options) {
|
||||||
super(application, "div");
|
super(application, "div", options);
|
||||||
|
|
||||||
// Configure instance fields
|
// Configure instance fields
|
||||||
|
options = options || {};
|
||||||
|
this.alignCross = "start";
|
||||||
|
this.alignMain = "start";
|
||||||
this.application = application;
|
this.application = application;
|
||||||
this.children = [];
|
this.children = [];
|
||||||
this.crossAlign = "start";
|
this.columns = null;
|
||||||
this.direction = "row";
|
this.direction = "row";
|
||||||
this.edge = "left";
|
this.layout = null;
|
||||||
this.hGap = "0";
|
this.overflowX = options.overflowX || "hidden";
|
||||||
this.layout = "split";
|
this.overflowY = options.overflowY || "hidden";
|
||||||
this.mainAlign = "start";
|
this.rows = null;
|
||||||
this.sizeable = false;
|
|
||||||
this.vGap = "0";
|
|
||||||
this.wrap = false;
|
this.wrap = false;
|
||||||
|
|
||||||
// Configure element
|
// Configure element
|
||||||
this.element.style.minHeight = "0";
|
this.element.style.minHeight = "0";
|
||||||
this.element.style.minWidth = "0";
|
this.element.style.minWidth = "0";
|
||||||
|
this.setOverflow(this.overflowX, this.overflowY);
|
||||||
|
|
||||||
// Configure layout
|
// 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));
|
Math.floor(Math.min(Math.max(0, index), this.children.length));
|
||||||
|
|
||||||
// Add the component to the container
|
// Add the component to the container
|
||||||
|
let ref = this.children[index] || null;
|
||||||
component.parent = this;
|
component.parent = this;
|
||||||
|
this.element.insertBefore(component.element,
|
||||||
|
ref == null ? null : ref.element);
|
||||||
this.children.splice(index, 0, component);
|
this.children.splice(index, 0, component);
|
||||||
this.arrange();
|
|
||||||
|
return component;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a Button and associate it with the application
|
// 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);
|
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 a component from the container
|
||||||
remove(component) {
|
remove(component) {
|
||||||
|
|
||||||
|
@ -72,117 +106,146 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
|
||||||
component.parent = null;
|
component.parent = null;
|
||||||
component.element.remove();
|
component.element.remove();
|
||||||
this.children.splice(index, 1);
|
this.children.splice(index, 1);
|
||||||
this.arrange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the element with a flex layout
|
// Configure the element's layout
|
||||||
setFlexLayout(direction, mainAlign, crossAlign, gap, wrap) {
|
setLayout(layout, options) {
|
||||||
|
|
||||||
// Configure instance fields
|
// Configure instance fields
|
||||||
this.layout = "flex";
|
this.layout = layout;
|
||||||
this.crossAlign = crossAlign;
|
|
||||||
this.direction = direction;
|
|
||||||
this.mainAlign = mainAlign;
|
|
||||||
this.wrap = wrap;
|
|
||||||
|
|
||||||
// Working variables
|
// Processing by layout
|
||||||
switch (crossAlign) {
|
switch (layout) {
|
||||||
case "end" : crossAlign = "flex-end" ;
|
case "block" : this.setBlockLayout (options); break;
|
||||||
case "start": crossAlign = "flex-start";
|
case "desktop": this.setDesktopLayout(options); break;
|
||||||
}
|
case "flex" : this.setFlexLayout (options); break;
|
||||||
switch (mainAlign) {
|
case "grid" : this.setGridLayout (options); break;
|
||||||
case "end" : mainAlign = "flex-end" ;
|
default : this.setNullLayout (options); break;
|
||||||
case "start": mainAlign = "flex-start";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure element
|
|
||||||
this.element.style.alignItems = crossAlign;
|
|
||||||
this.element.style.display = "flex";
|
|
||||||
this.element.style.flexDirection = direction;
|
|
||||||
this.element.style.justifyContent = mainAlign;
|
|
||||||
if (direction == "column") {
|
|
||||||
this.hGap = gap;
|
|
||||||
this.element.style.rowGap = gap;
|
|
||||||
}
|
|
||||||
if (direction == "row") {
|
|
||||||
this.vGap = gap;
|
|
||||||
this.element.style.columnGap = gap;
|
|
||||||
}
|
|
||||||
if (wrap)
|
|
||||||
this.element.style.flexWrap = "wrap";
|
|
||||||
else this.element.style.removeProperty("flex-wrap");
|
|
||||||
|
|
||||||
// Manage components
|
|
||||||
this.arrange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the element with a split layout
|
// Configure the panel's overflow scrolling behavior
|
||||||
setSplitLayout(edge, sizeable) {
|
setOverflow(x, y) {
|
||||||
|
this.overflowX = x || "hidden";
|
||||||
|
this.overflowY = y || "hidden";
|
||||||
|
this.element.style.overflow = this.overflowX + " " + this.overflowY;
|
||||||
|
}
|
||||||
|
|
||||||
// Configure instance fields
|
// Specify the semantic role of the panel
|
||||||
this.layout = "split";
|
setRole(role) {
|
||||||
this.edge = edge;
|
if (!role)
|
||||||
this.sizeable = sizeable;
|
this.element.removeAttribute("role");
|
||||||
|
else this.element.setAttribute("role", "" + role);
|
||||||
// Working variables
|
|
||||||
let rows = null, cols = null;
|
|
||||||
let tracks = [ "max-content", "auto" ];
|
|
||||||
if (sizeable)
|
|
||||||
tracks.splice(1, 0, "max-content");
|
|
||||||
|
|
||||||
// Processing by edge
|
|
||||||
switch (edge) {
|
|
||||||
case "bottom": tracks.reverse(); // Fallthrough
|
|
||||||
case "top" : rows = tracks.join(" "); break;
|
|
||||||
case "right" : tracks.reverse(); // Fallthrough
|
|
||||||
case "left" : cols = tracks.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure element
|
|
||||||
this.element.style.display = "grid";
|
|
||||||
if (cols != null)
|
|
||||||
this.element.style.gridTemplateColumns = cols;
|
|
||||||
else this.element.style.removeProperty("grid-template-columns");
|
|
||||||
if (rows != null)
|
|
||||||
this.element.style.gridTemplateRows = rows;
|
|
||||||
else this.element.style.removeProperty("grid-template-rows");
|
|
||||||
|
|
||||||
// Manage components
|
|
||||||
this.arrange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////// Private Methods /////////////////////////////
|
///////////////////////////// Private Methods /////////////////////////////
|
||||||
|
|
||||||
// Configure the panel's DOM elements
|
// Resize event handler
|
||||||
arrange() {
|
onresize(desktop) {
|
||||||
let components = [];
|
|
||||||
|
|
||||||
// Remove all children from the DOM
|
// Error checking
|
||||||
for (let comp of this.children)
|
if (this.layout != "desktop")
|
||||||
comp.element.remove();
|
return;
|
||||||
|
|
||||||
// Split layout
|
// Ensure all child windows are visible in the viewport
|
||||||
if (this.layout == "split") {
|
for (let wnd of this.children) {
|
||||||
components.push(this.children[0]);
|
let bounds = wnd.getBounds();
|
||||||
components.push(this.children[1]);
|
wnd.contain(
|
||||||
if (this.sizeable)
|
desktop,
|
||||||
components.splice(1, 0, this.splitter);
|
bounds,
|
||||||
if (this.edge == "bottom" || this.edge == "right")
|
bounds.x - desktop.x,
|
||||||
components.reverse();
|
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
|
// Configure a block layout
|
||||||
for (let comp of components)
|
setBlockLayout(options) {
|
||||||
if (comp)
|
|
||||||
this.element.appendChild(comp.element);
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
2
makefile
2
makefile
|
@ -1,7 +1,7 @@
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@echo
|
@echo
|
||||||
@echo "Virtual Boy Emulator - August 22, 2021"
|
@echo "Virtual Boy Emulator - August 26, 2021"
|
||||||
@echo
|
@echo
|
||||||
@echo "Target build environment is any Debian with the following packages:"
|
@echo "Target build environment is any Debian with the following packages:"
|
||||||
@echo " emscripten"
|
@echo " emscripten"
|
||||||
|
|
Loading…
Reference in New Issue