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