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