444 lines
14 KiB
JavaScript
444 lines
14 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.dragCursor = { x: 0, y: 0 };
|
|
this.dragEdge = null;
|
|
this.initialCenter = "center" in options ? !!options.center : false;
|
|
this.initialHeight = options.height || 64;
|
|
this.initialWidth = options.width || 64;
|
|
this.lastFocus = this.element;
|
|
this.shown = this.visible;
|
|
this.title = options.title || "";
|
|
|
|
// Configure element
|
|
this.setLayout("flex", {
|
|
alignCross: "stretch",
|
|
direction : "column",
|
|
overflowX : "visible",
|
|
overflowY : "visible"
|
|
});
|
|
this.setRole("dialog");
|
|
this.setBounds(0, 0, 64, 64);
|
|
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",
|
|
noShrink : true,
|
|
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.newLabel({}));
|
|
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");
|
|
this.titleClose.addClickListener(e=>this.onclose(e));
|
|
|
|
// Configure client area
|
|
this.client = this.body.add(this.newPanel({
|
|
overflowX: "hidden",
|
|
overflowY: "hidden"
|
|
}));
|
|
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);
|
|
if (this.shown)
|
|
this.setClientSize(this.initialHeight, this.initialWidth);
|
|
application.addComponent(this);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Public Methods //////////////////////////////
|
|
|
|
// Add a callback for close events
|
|
addCloseListener(listener) {
|
|
if (this.closeListeners.indexOf(listener) == -1)
|
|
this.closeListeners.push(listener);
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Package Methods /////////////////////////////
|
|
|
|
// Request focus on the appropriate element
|
|
focus() {
|
|
if (this.lastFocus != this)
|
|
this.lastFocus.focus();
|
|
else this.element.focus();
|
|
this.parent.bringToFront(this);
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////// Private Methods /////////////////////////////
|
|
|
|
// 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;
|
|
|
|
// Configure the initial size of the window
|
|
this.setClientSize(this.initialWidth, this.initialHeight);
|
|
|
|
// Configure the initial position of the window
|
|
if (!this.initialCenter)
|
|
return;
|
|
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
|
|
);
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
// Window close
|
|
onclose(e) {
|
|
for (let listener of this.closeListeners)
|
|
listener(e);
|
|
}
|
|
|
|
// 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(
|
|
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.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);
|
|
}
|
|
|
|
};
|