pvbemu/web/toolkit/Window.js

480 lines
15 KiB
JavaScript

let register = Toolkit => Toolkit.Window =
class Window extends Toolkit.Component {
//////////////////////////////// Constants ////////////////////////////////
// Resize directions by dragging edge
static RESIZES = {
"nw": { left : true, top : true },
"n" : { top : true },
"ne": { right: true, top : true },
"w" : { left : true },
"e" : { right: true },
"sw": { left : true, bottom: true },
"s" : { bottom: true },
"se": { right: true, bottom: true }
};
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, Object.assign({
"aria-modal": "false",
class : "tk window",
id : Toolkit.id(),
role : "dialog",
tabIndex : -1,
visibility : true,
visible : false
}, options, { style: Object.assign({
boxSizing : "border-box",
display : "grid",
gridTemplateRows: "max-content auto",
position : "absolute"
}, options.style || {})} ));
// Configure instance fields
this.lastFocus = null;
this.style = getComputedStyle(this.element);
this.text = null;
// Configure event listeners
this.addEventListener("focusin", e=>this.onFocus (e));
this.addEventListener("keydown", e=>this.onKeyDown(e));
// Working variables
let onBorderDown = e=>this.onBorderDown(e);
let onBorderMove = e=>this.onBorderMove(e);
let onBorderUp = e=>this.onBorderUp (e);
let onDragKey = e=>this.onDragKey (e);
// Resizing borders
for (let edge of [ "nw1", "nw2", "n", "ne1", "ne2",
"w", "e", "sw1", "sw2", "s", "se1", "se2"]) {
let border = document.createElement("div");
border.className = edge;
border.edge = edge.replace(/[12]/g, "");
Object.assign(border.style, {
cursor : border.edge + "-resize",
position: "absolute"
});
border.addEventListener("keydown" , onDragKey );
border.addEventListener("pointerdown", onBorderDown);
border.addEventListener("pointermove", onBorderMove);
border.addEventListener("pointerup" , onBorderUp );
this.element.append(border);
}
// Title bar
this.titleBar = document.createElement("div");
this.titleBar.className = "title";
this.titleBar.id = Toolkit.id();
Object.assign(this.titleBar.style, {
display : "grid",
gridTemplateColumns: "auto max-content",
minWidth : "0",
overflow : "hidden"
});
this.element.append(this.titleBar);
this.titleBar.addEventListener("keydown" , e=>this.onDragKey (e));
this.titleBar.addEventListener("pointerdown", e=>this.onTitleDown(e));
this.titleBar.addEventListener("pointermove", e=>this.onTitleMove(e));
this.titleBar.addEventListener("pointerup" , e=>this.onTitleUp (e));
// Title text
this.title = document.createElement("div");
this.title.className = "text";
this.title.id = Toolkit.id();
this.title.innerText = "\u00a0";
this.titleBar.append(this.title);
this.element.setAttribute("aria-labelledby", this.title.id);
// Close button
this.close = new Toolkit.Button(app, {
class : "close-button",
doNotFocus: true
});
this.close.addEventListener("action",
e=>this.element.dispatchEvent(new Event("close")));
this.close.setLabel("{window.close}", true);
this.close.setTitle("{window.close}", true);
this.titleBar.append(this.close.element);
// Client area
this.client = document.createElement("div");
this.client.className = "client";
Object.assign(this.client.style, {
minHeight: "0",
minWidth : "0",
overflow : "hidden"
});
this.element.append(this.client);
}
///////////////////////////// Event Handlers //////////////////////////////
// Border pointer down
onBorderDown(e) {
// Do not drag
if (e.button != 0 || this.app.drag != null)
return;
// Initiate dragging
this.drag = {
height: this.outerHeight,
left : this.left,
top : this.top,
width : this.outerWidth,
x : e.clientX,
y : e.clientY
};
this.drag.bottom = this.drag.top + this.drag.height;
this.drag.right = this.drag.left + this.drag.width;
// Configure event
this.focus();
this.app.drag = e;
Toolkit.handle(e);
}
// Border pointer move
onBorderMove(e) {
// Not dragging
if (this.app.drag != e.target)
return;
// Working variables
let resize = Toolkit.Window.RESIZES[e.target.edge];
let dx = e.clientX - this.drag.x;
let dy = e.clientY - this.drag.y;
let style = getComputedStyle(this.element);
let minHeight =
this.client .getBoundingClientRect().top -
this.titleBar.getBoundingClientRect().top +
parseFloat(style.borderTop ) +
parseFloat(style.borderBottom)
;
// Output bounds
let height = this.drag.height;
let left = this.drag.left;
let top = this.drag.top;
let width = this.drag.width;
// Dragging left
if (resize.left) {
let bParent = this.parent.element.getBoundingClientRect();
left += dx;
left = Math.min(left, this.drag.right - 32);
left = Math.min(left, bParent.width - 16);
width = this.drag.width + this.drag.left - left;
}
// Dragging top
if (resize.top) {
let bParent = this.parent.element.getBoundingClientRect();
top += dy;
top = Math.max(top, 0);
top = Math.min(top, this.drag.bottom - minHeight);
top = Math.min(top, bParent.height - minHeight);
height = this.drag.height + this.drag.top - top;
}
// Dragging right
if (resize.right) {
width += dx;
width = Math.max(width, 32);
width = Math.max(width, 16 - this.drag.left);
}
// Dragging bottom
if (resize.bottom) {
height += dy;
height = Math.max(height, minHeight);
}
// Apply bounds
this.element.style.height = height + "px";
this.element.style.left = left + "px";
this.element.style.top = top + "px";
this.element.style.width = width + "px";
// Configure event
Toolkit.handle(e);
}
// Border pointer up
onBorderUp(e, id) {
// Not dragging
if (e.button != 0 || this.app.drag != e.target)
return;
// Configure instance fields
this.drag = null;
// Configure event
this.app.drag = null;
Toolkit.handle(e);
}
// Key down while dragging
onDragKey(e) {
if (
this.drag != null && e.key == "Escape" &&
!e.ctrlKey && !e.altKey && !e.shiftKey
) this.cancelDrag();
}
// Focus gained
onFocus(e) {
// The element receiving focus is self, or Close button from external
if (
e.target == this.element ||
e.target == this.close.element && !this.contains(e.relatedTarget)
) {
let elm = this.lastFocus;
if (!elm) {
elm = this.getFocusable();
elm = elm[Math.min(1, elm.length - 1)];
}
elm.focus({ preventScroll: true });
}
// The element receiving focus is not self
else if (e.target != this.close.element)
this.lastFocus = e.target;
// Bring the window to the front among its siblings
if (this.parent instanceof Toolkit.Desktop)
this.parent.bringToFront(this);
}
// Window key press
onKeyDown(e) {
// Take no action
if (e.altKey || e.ctrlKey || e.key != "Tab")
return;
// Move focus to the next element in the sequence
let focuses = this.getFocusable();
let nowIndex = focuses.indexOf(document.activeElement) || 0;
let nextIndex = nowIndex + focuses.length + (e.shiftKey ? -1 : 1);
let target = focuses[nextIndex % focuses.length];
Toolkit.handle(e);
target.focus();
}
// Title bar pointer down
onTitleDown(e) {
// Do not drag
if (e.button != 0 || this.app.drag != null)
return;
// Initiate dragging
this.drag = {
height: this.outerHeight,
left : this.left,
top : this.top,
width : this.outerWidth,
x : e.clientX,
y : e.clientY
};
// Configure event
this.focus();
this.app.drag = e;
Toolkit.handle(e);
}
// Title bar pointer move
onTitleMove(e) {
// Not dragging
if (this.app.drag != e.target)
return;
// Working variables
let bParent = this.parent.element.getBoundingClientRect();
// Move horizontally
let left = this.drag.left + e.clientX - this.drag.x;
left = Math.min(left, bParent.width - 16);
left = Math.max(left, 16 - this.drag.width);
this.element.style.left = left + "px";
// Move vertically
let top = this.drag.top + e.clientY - this.drag.y;
top = Math.min(top, bParent.height - this.minHeight);
top = Math.max(top, 0);
this.element.style.top = top + "px";
// Configure event
Toolkit.handle(e);
}
// Title bar pointer up
onTitleUp(e) {
// Not dragging
if (e.button != 0 || this.app.drag != e.target)
return;
// Configure instance fields
this.drag = null;
// Configure event
this.app.drag = null;
Toolkit.handle(e);
}
///////////////////////////// Public Methods //////////////////////////////
// Bring the window to the front among its siblings
bringToFront() {
if (this.parent != null)
this.parent.bringToFront(this);
}
// Set focus on the component
focus() {
if (!this.contains(document.activeElement))
(this.lastFocus || this.element).focus({ preventScroll: true });
if (this.parent instanceof Toolkit.Desktop)
this.parent.bringToFront(this);
}
// Height of client
get height() { return this.client.getBoundingClientRect().height; }
set height(height) {
this.element.style.height =
this.outerHeight - this.height + Math.max(height, 0) + "px";
}
// Position of window left edge
get left() {
return this.element.getBoundingClientRect().left - (
this.parent == null ? 0 :
this.parent.element.getBoundingClientRect().left
);
}
set left(left) {
if (this.parent != null) {
left = Math.min(left,
this.parent.element.getBoundingClientRect().width - 16);
}
left = Math.max(left, 16 - this.outerWidth);
this.element.style.left = left + "px";
}
// Height of entire window
get outerHeight() { return this.element.getBoundingClientRect().height; }
set outerHeight(height) {
height = Math.max(height, this.minHeight);
this.element.style.height = height + "px";
}
// Width of entire window
get outerWidth() { return this.element.getBoundingClientRect().width; }
set outerWidth(width) {
width = Math.max(width, 32);
this.element.style.width = width + "px";
let left = this.left;
if (left + width < 16)
this.element.style.left = 16 - width + "px";
}
// Specify the window title
setTitle(title, localize) {
this.setString("text", title, localize);
}
// Position of window top edge
get top() {
return this.element.getBoundingClientRect().top - (
this.parent == null ? 0 :
this.parent.element.getBoundingClientRect().top
);
}
set top(top) {
if (this.parent != null) {
top = Math.min(top, -this.minHeight +
this.parent.element.getBoundingClientRect().height);
}
top = Math.max(top, 0);
this.element.style.top = top + "px";
}
// Specify whether the element is visible
get visible() { return super.visible; }
set visible(visible) {
let prevSetting = super.visible;
let prevActual = this.isVisible();
super.visible = visible;
let nowActual = this.isVisible();
if (!nowActual && this.contains(document.activeElement))
document.activeElement.blur();
}
// Width of client
get width() { return this.client.getBoundingClientRect().width; }
set width(width) {
this.outerWidth = this.outerWidth - this.width + Math.max(width, 32);
}
///////////////////////////// Package Methods /////////////////////////////
// Add a child component to the primary client region of this component
append(element) {
this.client.append(element);
}
// Update localization strings
localize() {
this.localizeText(this.title);
if ((this.title.textContent || "") == "")
this.title.innerText = "\u00a0"; // &nbsp;
this.close.localize();
}
///////////////////////////// Private Methods /////////////////////////////
// Cancel a move or resize dragging operaiton
cancelDrag() {
this.app.drag = null;
this.element.style.height = this.drag.height + "px";
this.element.style.left = this.drag.left + "px";
this.element.style.top = this.drag.top + "px";
this.element.style.width = this.drag.width + "px";
this.drag = null;
}
// Minimum height of window
get minHeight() {
return (
this.client .getBoundingClientRect().top -
this.element.getBoundingClientRect().top +
parseFloat(this.style.borderBottomWidth)
);
}
}
export { register };