480 lines
15 KiB
JavaScript
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"; //
|
||
|
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 };
|