pvbemu/app/toolkit/Window.js

403 lines
12 KiB
JavaScript

"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.closeListeners = [];
this.dragBounds = null;
this.dragClient = null;
this.dragCursor = { x: 0, y: 0 };
this.dragEdge = null;
this.dragPointer = null;
this.initialCenter = "center" in options ? !!options.center : false;
this.lastFocus = this.element;
this.shown = this.visible;
// Configure element
this.setLayout("grid", { columns: "auto" });
this.setRole("dialog");
this.setLocation(0, 0);
this.element.style.position = "absolute";
this.element.setAttribute("aria-modal", "false");
this.element.setAttribute("focus" , "false");
this.element.setAttribute("tabindex" , "0" );
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));
// Primary visible container
this.body = this.add(this.newPanel({
layout : "grid",
overflowX: "visible",
overflowY: "visible",
rows : "max-content auto"
}));
this.body.element.setAttribute("name", "body");
// Title bar
this.titleBar = this.body.add(this.newPanel({
layout : "grid",
columns: "max-content auto max-content",
hollow : false
}));
this.titleBar.element.setAttribute("name", "title-bar");
// Title bar icon
this.titleIcon = this.titleBar.add(this.newPanel());
this.titleIcon.element.setAttribute("name", "icon");
this.titleIcon.element.setAttribute("aria-hidden", "true");
// Title bar title
this.title = this.titleBar.add(this.newLabel({
localized: true,
text : options.title
}));
this.title.element.setAttribute("name", "title");
this.title.setProperty("sim", "");
// Title bar close
this.titleClose = this.titleBar.add(this.newButton({
toolTip: "{app.close}"
}));
this.titleClose.element.setAttribute("name", "close");
this.titleClose.element.setAttribute("aria-hidden", "true");
this.titleClose.addClickListener(e=>this.onclose(e));
// Client area
this.client = this.body.add(this.newPanel());
this.client.element.setAttribute("name", "client");
this.setSize(options.width, options.height);
}
///////////////////////////// Public Methods //////////////////////////////
// Add a callback for close events
addCloseListener(listener) {
if (this.closeListeners.indexOf(listener) == -1)
this.closeListeners.push(listener);
}
// Retrieve the window's title text
getTitle() {
return this.title.getText();
}
// Specify the height of the component
setHeight(height, minimum) {
this.client && this.client.setHeight(height, minimum);
}
// Specify the window's title text
setTitle(title) {
this.title.setText(title);
}
// Specify whether the component is visible
setVisible(visible, focus) {
super.setVisible(visible);
if (this.client === undefined)
return;
if (visible)
this.contain();
if (focus)
this.focus();
if (!this.shown)
this.firstShow();
}
// Specify the width of the component
setWidth(width, minimum) {
this.client && this.client.setWidth(
Math.max(64, width || 0), minimum);
}
///////////////////////////// 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 in the center of the desktop
center() {
let bounds = this.getBounds();
let desktop = this.parent.getBounds();
this.contain(
Math.floor((desktop.width - bounds.width ) / 2),
Math.ceil ((desktop.height - bounds.height) / 2),
desktop, bounds
);
}
// Position the window using a tentative location in the desktop
contain(x, y, desktop, bounds, client) {
desktop = desktop || this.parent.getBounds();
bounds = bounds || this.getBounds();
client = client || this.client.getBounds();
if (x === undefined)
x = bounds.x - desktop.x;
if (y === undefined)
y = bounds.y - desktop.y;
// 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;
}
// The window is being displayed for the first time
firstShow() {
this.shown = true;
}
// Focus lost event capture
onblur(e) {
if (!this.contains(e.relatedTarget))
this.element.setAttribute("focus", "false");
}
// Window close
onclose(e) {
for (let listener of this.closeListeners)
listener(e);
}
// Focus gained event capture
onfocus(e) {
this.element.setAttribute("focus", "true");
let target = e.target;
if (target == this.element)
target = this.lastFocus;
this.lastFocus = target;
if (target != e.target)
target.focus();
this.parent.bringToFront(this);
}
// Key pressed event handler
onkeydown(e) {
// Only listening for Escape while dragging
if (e.key != "Escape" || this.dragPointer === null)
return;
// Restore the window's position
let desktop = this.parent.getBounds();
this.setLocation(
this.dragBounds.x - desktop.x,
this.dragBounds.y - desktop.y
);
// Restore the window's size
if (this.dragEdge != null)
this.setSize(this.dragClient.width, this.dragClient.height);
// Configure instance fields
this.element.releasePointerCapture(this.dragPointer);
this.dragPointer = null;
}
// Pointer down event handler
onpointerdown(e) {
// Configure event
e.stopPropagation();
// Error checking
if (
e.button != 0 ||
this.element.hasPointerCapture(e.pointerId)
) return;
// Configure instance fields
this.dragBounds = this.getBounds();
this.dragClient = this.client.getBounds();
this.dragCursor.x = e.x;
this.dragCursor.y = e.y;
this.dragEdge = this.edge(e, this.dragBounds);
// Don't perform a move if the cursor isn't in the title bar
let title = this.titleBar.getBounds();
if (this.dragEdge == null && e.y >= title.y + title.height)
return;
// Configure element
this.element.setPointerCapture(e.pointerId);
this.dragPointer = 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();
// Move the window
if (this.dragEdge == null) {
this.contain(
this.dragBounds.x - desktop.x + rX,
this.dragBounds.y - desktop.y + rY,
desktop, bounds, 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.dragClient.height - rY;
// Restrict window bounds
if (top > maxTop) {
height -= maxTop - top;
top = maxTop;
}
if (top < 0) {
height += top;
top = 0;
}
if (height < 0) {
top += height;
height = 0;
}
// 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.dragClient.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.dragClient.width + rX;
// Restrict window bounds
width = Math.max(64, width);
width = Math.max(width, -this.dragClient.x + 16);
// Configure element
this.setWidth(width);
}
// Resizing on the south edge
if (this.dragEdge.startsWith("s")) {
let height = this.dragClient.height + rY;
// Restrict window bounds
height = Math.max(0, 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);
this.dragPointer = null;
}
};