"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); } };