import { Component } from /**/"./Component.js"; let Toolkit; /////////////////////////////////////////////////////////////////////////////// // Window // /////////////////////////////////////////////////////////////////////////////// // Standalone movable dialog class Window extends Component { static Component = Component; ///////////////////////// Initialization Methods ////////////////////////// constructor(gui, options) { super(gui, options, { className : "tk tk-window", focusable : true, role : "dialog", tabStop : false, tagName : "div", visibility: true, style : { position: "absolute" } }); // Configure instance fields this.firstShown = false; this.lastFocus = null; // DOM container this.contents = document.createElement("div"); this.contents.style.display = "flex"; this.contents.style.flexDirection = "column"; this.element.append(this.contents); // Sizing borders this.borders = {} this.border("n" ); this.border("w" ); this.border("e" ); this.border("s" ); this.border("nw"); this.border("ne"); this.border("sw"); this.border("se"); // Title bar this.titleBar = document.createElement("div"); this.titleBar.className = "tk tk-title"; this.titleBar.style.display = "flex"; this.contents.append(this.titleBar); this.titleBar.addEventListener( "pointerdown", e=>this.onTitlePointerDown(e)); this.titleBar.addEventListener( "pointermove", e=>this.onTitlePointerMove(e)); this.titleBar.addEventListener( "pointerup" , e=>this.onTitlePointerUp (e)); // Title bar text this.titleText = document.createElement("div"); this.titleText.className = "tk tk-text"; this.titleText.id = Toolkit.id(); this.titleText.style.flexGrow = "1"; this.titleText.style.position = "relative"; this.titleBar.append(this.titleText); this.setAttribute("aria-labelledby", this.titleText.id); // Close button this.titleClose = document.createElement("div"); this.titleClose.className = "tk tk-close"; this.titleClose.setAttribute("aria-hidden", "true"); this.titleBar.append(this.titleClose); this.titleClose.addEventListener( "pointerdown", e=>this.onClosePointerDown(e)); this.titleClose.addEventListener( "pointermove", e=>this.onClosePointerMove(e)); this.titleClose.addEventListener( "pointerup" , e=>this.onClosePointerUp (e)); // Window client area this.client = document.createElement("div"); this.client.className = "tk tk-client"; this.client.style.flexGrow = "1"; this.client.style.minHeight = "0"; this.client.style.minWidth = "0"; this.client.style.overflow = "hidden"; this.client.style.position = "relative"; this.contents.append(this.client); // User agent behavior override let observer = new ResizeObserver( ()=>this.titleBar.style.width = this.client.getBoundingClientRect().width + "px" ); observer.observe(this.client); // Configure element this.setAttribute("aria-modal", "false"); this.setBounds( options.x , options.y, options.width, options.height ); // Configure component this.gui.localize(this); this.setTitle (options.title ); this.setCloseToolTip(options.closeToolTip); this.addEventListener("focus" , e=>this.onFocus(e), true); this.addEventListener("keydown" , e=>this.onWindowKeyDown (e)); this.addEventListener("pointerdown", e=>this.onWindowPointerDown(e)); } ///////////////////////////// Event Handlers ////////////////////////////// // Border pointer down onBorderPointerDown(e, edge) { if (e.target.hasPointerCapture(e.pointerId) || e.button != 0) return; e.target.setPointerCapture(e.pointerId); e.preventDefault(); let bndClient = this.client.getBoundingClientRect(); let bndWindow = this .getBounds (); let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow; let coords = Toolkit.screenCoords(e); this.drag = { clickX : coords.x, clickY : coords.y, mode : "resize", pointerId : e.pointerId, startHeight: bndClient.height, startWidth : bndClient.width, startX : bndWindow.x - bndDesktop.x, startY : bndWindow.y - bndDesktop.y, target : e.target }; } // Border pointer move onBorderPointerMove(e, edge) { if (!e.target.hasPointerCapture(e.pointerId)) return; e.stopPropagation(); e.preventDefault(); let bndWindow = this.getBounds(); this["resize" + edge.toUpperCase()]( Toolkit.screenCoords(e), this.client .getBoundingClientRect(), this.parent ? this.parent.getBounds() : bndWindow, bndWindow, this.titleBar.getBoundingClientRect() ); } // Border pointer up onBorderPointerUp(e, edge) { if (!e.target.hasPointerCapture(e.pointerId) || e.button != 0) return; e.target.releasePointerCapture(e.pointerId); e.stopPropagation(); e.preventDefault(); } // Close pointer down onClosePointerDown(e) { if (this.titleClose.hasPointerCapture(e.pointerId) || e.button != 0) return; this.titleClose.setPointerCapture(e.pointerId); e.stopPropagation(); e.preventDefault(); this.titleClose.classList.add("active"); this.drag = { mode: "close", x : e.offsetX, y : e.offsetY }; } // Close pointer move onClosePointerMove(e) { if (!this.titleClose.hasPointerCapture(e.pointerId)) return; e.stopPropagation(); e.preventDefault(); if (Toolkit.isInside(this.titleClose, e)) this.titleClose.classList.add("active"); else this.titleClose.classList.remove("active"); } // Close pointer up onClosePointerUp(e) { if (!this.titleClose.hasPointerCapture(e.pointerId) || e.button != 0) return; this.titleClose.releasePointerCapture(e.pointerId); e.stopPropagation(); e.preventDefault(); this.titleClose.classList.remove("active"); if (Toolkit.isInside(this.titleClose, e)) this.element.dispatchEvent(Toolkit.event("close", this)); this.drag = null; } // Focus capture onFocus(e) { // Bring this window to the foreground of its siblings if (!this.contains(e.relatedTarget) && this.parent) this.parent.bringToFront(this); // The target is not the window itself if (e.target != this.element) { this.lastFocus = e.target; return; } // Select the first focusable child if (this.lastFocus == null) this.lastFocus = Toolkit.listFocusables(this.element)[0] || null; // Send focus to the most recently focused element if (this.lastFocus) this.lastFocus.focus(); } // Title pointer down onTitlePointerDown(e) { if (this.titleBar.hasPointerCapture(e.pointerId) || e.button != 0) return; this.titleBar.setPointerCapture(e.pointerId); e.preventDefault(); let bndWindow = this.getBounds(); let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow; let coords = Toolkit.screenCoords(e); this.drag = { clickX : coords.x, clickY : coords.y, mode : "move", pointerId: e.pointerId, startX : bndWindow.x - bndDesktop.x, startY : bndWindow.y - bndDesktop.y }; } // Title pointer move onTitlePointerMove(e) { if (!this.titleBar.hasPointerCapture(e.pointerId)) return; e.stopPropagation(); e.preventDefault(); let coords = Toolkit.screenCoords(e); let valid = this.getValidLocations( this.drag.startX + coords.x - this.drag.clickX, this.drag.startY + coords.y - this.drag.clickY ); this.setLocation(valid.x, valid.y); } // Title pointer up onTitlePointerUp(e) { if (!this.titleBar.hasPointerCapture(e.pointerId) || e.button != 0) return; this.titleBar.releasePointerCapture(e.pointerId); e.stopPropagation(); e.preventDefault(); this.drag = null; } // Window key press onWindowKeyDown(e) { // Process by key switch (e.key) { // Undo un-committed bounds modifications case "Escape": // Not dragging if (this.drag == null) return; // Moving if (this.drag.mode == "move") { this.titleBar.releasePointerCapture(this.drag.pointerId); this.setLocation(this.drag.startX, this.drag.startY); this.drag = null; } // Resizing else if (this.drag.mode == "resize") { this.drag.target .releasePointerCapture(this.drag.pointerId); this.setBounds( this.drag.startX , this.drag.startY, this.drag.startWidth, this.drag.startHeight ); this.drag = null; } break; // Transfer focus to another element case "Tab": default: return; } // The event was handled e.stopPropagation(); e.preventDefault(); } // Window pointer down onWindowPointerDown(e) { this.focus(e); e.stopPropagation(); e.preventDefault(); } ///////////////////////////// Public Methods ////////////////////////////// // Add a DOM element to this component's element append(child) { let element = child instanceof Element ? child : child.element; this.client.append(element); } // Position the window in the center of the parent Desktop center() { if (!this.parent) return; let bndParent = this.parent.getBounds(); let bndWindow = this .getBounds(); this.setLocation( Math.max(Math.floor((bndParent.width - bndWindow.width ) / 2), 0), Math.max(Math.floor((bndParent.height - bndWindow.height) / 2), 0) ); } // Programmatically close the window close() { this.event("close"); } // Add a DOM element to the beginning of this component's children prepend(child) { let element = child instanceof Element ? child : child.element; this.element.prepend(element); } // Specify a new position and size for the window setBounds(x, y, width, height) { this.setSize(width, height); this.setLocation(x, y); } // Specify the over text for the close button setCloseToolTip(key) { this.closeToolTip = key; this.translate(); } // Specify a new position for the window setLocation(x, y) { Object.assign(this.element.style, { left: Math.round(parseFloat(x) || 0) + "px", top : Math.round(parseFloat(y) || 0) + "px" }); } // Specify a new size for the window setSize(width, height) { Object.assign(this.client.style, { width : Math.max(Math.round(parseFloat(width ) || 0, 32)) + "px", height: Math.max(Math.round(parseFloat(height) || 0, 32)) + "px" }); } // Specify the window title text setTitle(key) { this.title = key; this.translate(); } // Specify whether the component is visible setVisible(visible) { super.setVisible(visible); if (!visible || this.firstShown) return; this.firstShown = true; this.event("firstshow", this); } ///////////////////////////// Package Methods ///////////////////////////// // Ensure the window is partially visible within its desktop contain() { let valid = this.getValidLocations(); this.setLocation(valid.x, valid.y); } // Determine the range of valid window coordinates getValidLocations(x, y) { // Measure the bounding boxes of the relevant elements let bndClient = this.client .getBoundingClientRect(); let bndWindow = this .getBounds (); let bndTitleBar = this.titleBar.getBoundingClientRect(); let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow; // Compute the minimum and maximum valid window coordinates let ret = { maxX: bndDesktop .width - bndTitleBar.height - bndTitleBar.x + bndWindow .x, maxY: bndDesktop .height - bndClient .y + bndWindow .y, minX: bndTitleBar.height - bndWindow .width + bndWindow .right - bndTitleBar.right, minY: 0 }; // Compute the effective "best" window coordinates ret.x = Math.max(ret.minX, Math.min(ret.maxX, x === undefined ? bndWindow.x - bndDesktop.x : x)); ret.y = Math.max(ret.minY, Math.min(ret.maxY, y === undefined ? bndWindow.y - bndDesktop.y : y)); return ret; } // Update the global Toolkit object static setToolkit(toolkit) { Toolkit = toolkit; } // Regenerate localized display text translate() { if (!this.titleText) return; this.titleText.innerText = this.gui.translate(this.title, this); if (this.closeToolTip) this.titleClose.setAttribute("title", this.gui.translate(this.closeToolTip, this)); else this.titleClose.removeAttribute("title"); } ///////////////////////////// Private Methods ///////////////////////////// // Produce a border element and add it to the window border(edge) { let border = this.borders[edge] = document.createElement("div"); border.className = "tk tk-" + edge; border.style.cursor = edge + "-resize"; border.style.position = "absolute"; this.contents.append(border); border.addEventListener( "pointerdown", e=>this.onBorderPointerDown(e, edge)); border.addEventListener( "pointermove", e=>this.onBorderPointerMove(e, edge)); border.addEventListener( "pointerup" , e=>this.onBorderPointerUp (e, edge)); } // Compute client bounds when resizing on the east border constrainE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { let w = this.drag.startWidth + coords.x - this.drag.clickX; w = Math.max(w, bndTitleBar.height * 4); if (bndClient.x - bndDesktop.x < 0) w = Math.max(w, bndDesktop.x - bndClient.x + bndTitleBar.height); return w; } // Compute client bounds when resizing on the north border constrainN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { let delta = coords.y - this.drag.clickY; let y = this.drag.startY + delta; let h = this.drag.startHeight - delta; let min = Math.max(0, bndClient.bottom - bndDesktop.bottom); if (h < min) { delta = min - h; h += delta; y -= delta; } if (y < 0) { h += y; y = 0; } return { height: h, y : y }; } // Compute client bounds when resizing on the south border constrainS(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { return Math.max(0, this.drag.startHeight+coords.y-this.drag.clickY); } // Compute client bounds when resizing on the west border constrainW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { let delta = coords.x - this.drag.clickX; let x = this.drag.startX + delta; let w = this.drag.startWidth - delta; let min = bndTitleBar.height * 4; if (bndClient.right - bndDesktop.right > 0) { min = Math.max(min, bndClient.right - bndDesktop.right + bndTitleBar.height); } if (w < min) { delta = min - w; w += delta; x -= delta; } return { x : x, width: w }; } // Resize on the east border resizeE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { this.setSize( this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar), this.drag.startHeight ); } // Resize on the north border resizeN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { let con = this.constrainN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar); this.setBounds( this.drag.startX , con.y, this.drag.startWidth, con.height ); } // Resize on the northeast border resizeNE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { let con = this.constrainN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar); this.setBounds( this.drag.startX, con.y, this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar), con.height ); } // Resize on the northwest border resizeNW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { let conN = this.constrainN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar); let conW = this.constrainW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar); this.setBounds(conW.x, conN.y, conW.width, conN.height); } // Resize on the south border resizeS(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { this.setSize( this.drag.startWidth, this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar), ); } // Resize on the southeast border resizeSE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { this.setSize( this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar), this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar) ); } // Resize on the southwest border resizeSW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { let con = this.constrainW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar); this.setBounds( con.x , this.drag.startY, con.width, this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar) ); } // Resize on the west border resizeW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { let con = this.constrainW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar); this.setBounds( con.x , this.drag.startY, con.width, this.drag.startHeight ); } }; /////////////////////////////////////////////////////////////////////////////// // Desktop // /////////////////////////////////////////////////////////////////////////////// // Parent container for encapsulating groups of Windows class Desktop extends Component { ///////////////////////// Initialization Methods ////////////////////////// constructor(gui, options) { super(gui, options, { className: "tk tk-desktop", role : "group", tagName : "div", style : { position: "relative" } }); // Configure event handlers this.addEventListener("resize", e=>this.onResize(e)); } ///////////////////////////// Event Handlers ////////////////////////////// // Element resized onResize(e) { for (let wnd of this.children) wnd.contain(); } ///////////////////////////// Public Methods ////////////////////////////// // Re-order windows to bring a particular one to the foreground bringToFront(wnd) { // The window is not a child of this Desktop let index = this.children.indexOf(wnd); if (index == -1) return; // The window is already in the foreground let afters = this.children.slice(index + 1).map(c=>c.element); if (afters.length == 0) return; // Record scroll pane positions let scrolls = []; for (let after of afters) for (let scroll of after.querySelectorAll(".tk-scrollpane > .tk-viewport")) { scrolls.push({ element: scroll, left : scroll.scrollLeft, top : scroll.scrollTop }); } // Update window collection wnd.element.before(... this.children.slice(index+1).map(c=>c.element)); this.children.splice(index, 1); this.children.push(wnd); // Restore scroll pane positions for (let scroll of scrolls) { Object.assign(scroll.element, { scrollLeft: scroll.left, scrollTop : scroll.top }); } } // Position a window in the center of the viewable area center(wnd) { // The window is not a child of the desktop pane if (this.children.indexOf(wnd) == -1) return; let bndDesktop = this.getBounds(); let bndWindow = wnd .getBounds(); wnd.setLocation( Math.max(0, Math.round((bndDesktop.width - bndWindow.width) / 2)), Math.max(0, Math.round((bndDesktop.height-bndWindow.height) / 2)) ); } }; export { Desktop, Window };