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