@scidian/osui
Version:
Lightweight JavaScript UI library.
250 lines (214 loc) • 9.62 kB
JavaScript
import { Css } from '../utils/Css.js';
import { Div } from '../core/Div.js';
import { Interaction } from '../utils/Interaction.js';
import { Panel, PANEL_STYLES } from './Panel.js';
import { RESIZERS } from '../constants.js';
const MIN_W = 300;
const MIN_H = 150;
class Window extends Panel {
#initialWidth;
#initialHeight;
#lastKnownRect;
#maximized = false;
#titleBar = undefined;
constructor({
style = PANEL_STYLES.FANCY,
width = 600,
height = 600,
resizers = [ RESIZERS.TOP, RESIZERS.BOTTOM, RESIZERS.LEFT, RESIZERS.RIGHT ],
} = {}) {
super({ style });
const self = this;
this.addClass('osui-window');
this.allowFocus();
// Properties
this.isWindow = true;
this.#initialWidth = width;
this.#initialHeight = height;
// Stacks
this.dom.addEventListener('focusout', () => { self.removeClass('osui-active-window'); });
this.dom.addEventListener('focusin', () => { self.activeWindow(); });
this.dom.addEventListener('displayed', () => { self.activeWindow(); } );
this.dom.addEventListener('pointerdown', () => { self.activeWindow(); } );
// Resizers
let rect = {};
function resizerDown() {
self.focus();
rect = self.dom.getBoundingClientRect();
}
function resizerMove(resizer, diffX, diffY) {
if (resizer.hasClassWithString('left')) {
const newLeft = Math.max(0, Math.min(rect.right - MIN_W, rect.left + diffX));
const newWidth = rect.right - newLeft;
self.setStyle('left', `${newLeft}px`);
self.setStyle('width', `${newWidth}px`);
}
if (resizer.hasClassWithString('top')) {
const newTop = Math.max(0, Math.min(rect.bottom - MIN_H, rect.top + diffY));
const newHeight = rect.bottom - newTop;
self.setStyle('top', `${newTop}px`);
self.setStyle('height', `${newHeight}px`);
}
if (resizer.hasClassWithString('right')) {
const newWidth = Math.min(Math.max(MIN_W, rect.width + diffX), window.innerWidth - rect.left);
self.setStyle('width', `${newWidth}px`);
}
if (resizer.hasClassWithString('bottom')) {
const newHeight = Math.min(Math.max(MIN_H, rect.height + diffY), window.innerHeight - rect.top);
self.setStyle('height', `${newHeight}px`);
}
self.dom.dispatchEvent(new Event('resizer'));
}
function resizerUp() {
keepInWindow();
}
Interaction.makeResizeable(this, this, resizers, resizerDown, resizerMove, resizerUp);
// Initial Size
this.setStyle('left', '0', 'top', '0', 'width', '0', 'height', '0');
// Wait for document to load
if (document.readyState === 'complete') self.setInitialSize();
else window.addEventListener('load', () => { self.setInitialSize(); }, { once: true });
// Keep In Window
function keepInWindow() {
// // OPTION: Limit to Title Bar
const computed = getComputedStyle(self.dom);
const rect = {
left: parseFloat(computed.left),
top: parseFloat(computed.top),
width: parseFloat(computed.width),
height: parseFloat(computed.height),
};
const titleHeight = parseInt(Css.toPx('4em'));
let newLeft = Math.min(window.innerWidth - (rect.width / 2), rect.left);
let newTop = Math.min(window.innerHeight - titleHeight, rect.top);
newLeft = Math.max(- (rect.width / 2), newLeft);
newTop = Math.max(0, newTop);
self.setStyle('top', `${newTop}px`);
self.setStyle('left', `${newLeft}px`);
// // OPTION: Limit to Window
// const rect = self.dom.getBoundingClientRect();
// if (rect.right > window.innerWidth) self.setStyle('left', `${Math.max(0, window.innerWidth - rect.width)}px`);
// if (rect.bottom > window.innerHeight) self.setStyle('top', `${Math.max(0, window.innerHeight - rect.height)}px`);
// if (rect.top < 0) self.setStyle('top', '0px');
// if (rect.left < 0) self.setStyle('left', '0px');
// if (rect.width > window.innerWidth) self.setStyle('width', `${window.innerWidth}px`);
// if (rect.height > window.innerHeight) self.setStyle('height', `${window.innerHeight}px`);
}
window.addEventListener('resize', () => { keepInWindow(); });
let firstTime = true;
this.dom.addEventListener('displayed', () => {
// Center first time shown
if (firstTime) {
self.center();
firstTime = false;
}
// Resize if necessary
keepInWindow();
});
}
/******************** WIDGETS */
addTitleBar(title = '', draggable = false, scale = 1.3) {
if (!this.#titleBar) {
this.#titleBar = new TitleBar(this, title, draggable, scale);
this.addToSelf(this.#titleBar);
} else {
this.#titleBar.setTitle(title);
}
}
setTitle(title = '') {
if (this.#titleBar) this.#titleBar.setTitle(title);
}
/******************** POSITION */
/** Applies 'osui-active-window', ensures this is the only element with this special class */
activeWindow() {
if (this.hasClass('osui-active-window')) return;
this.addClass('osui-active-window');
// Ensure only active window
const windows = document.querySelectorAll('.osui-window');
windows.forEach((element) => {
if (element !== this.dom) element.classList.remove('osui-active-window');
});
// Arrange Z-Orders
const topZ = windows.length + 200;
Css.setVariable('--window-z-index', `${topZ}`, this);
windows.forEach((element) => {
if (element !== this.dom) {
let currentZ = Css.getVariable('--window-z-index', element);
if (currentZ >= topZ) currentZ = topZ;
currentZ--;
if (currentZ < 200) currentZ = 200;
Css.setVariable('--window-z-index', `${currentZ}`, element);
}
});
}
/** Centers 'Window' panel in document window */
center() {
const side = (window.innerWidth - this.getWidth()) / 2;
const top = (window.innerHeight - this.getHeight()) / 2;
this.setStyle('left', `${side}px`, 'top', `${top}px`);
}
setInitialSize() {
const width = Css.toPx(Css.parseSize(this.#initialWidth), this, 'w');
const height = Css.toPx(Css.parseSize(this.#initialHeight), this, 'h');
this.setStyle('width', width);
this.setStyle('height', height);
this.dom.dispatchEvent(new Event('resizer'));
}
/** Displays window, with focus */
showWindow() {
this.display();
this.focus();
}
toggleMinMax() {
if (!this.#maximized) {
this.#lastKnownRect = this.dom.getBoundingClientRect();
this.setStyle('left', `0`);
this.setStyle('top', `0`);
this.setStyle('width', `${window.innerWidth}px`);
this.setStyle('height', `${window.innerHeight}px`);
this.#maximized = true;
} else {
const newLeft = Math.max(0, Math.min(window.innerWidth - this.#lastKnownRect.width, this.#lastKnownRect.left));
const newTop = Math.max(0, Math.min(window.innerHeight - this.#lastKnownRect.height, this.#lastKnownRect.top));
this.setStyle('left', `${newLeft}px`);
this.setStyle('top', `${newTop}px`);
this.setStyle('width', `${this.#lastKnownRect.width}px`);
this.setStyle('height', `${this.#lastKnownRect.height}px`);
this.#maximized = false;
}
this.dom.dispatchEvent(new Event('resizer'));
}
}
export { Window };
/******************** INTERNAL ********************/
class TitleBar extends Div {
constructor(parent, title = '', draggable = false, scale = 1.3) {
if (!parent || !parent.isElement) return console.warn(`TitleBar: Missing parent element`);
super();
const self = this;
this.setClass('osui-title-bar');
this.addClass('osui-panel-button');
this.setStyle('height', `${scale}em`, 'width', `${scale * 6}em`);
this.setStyle('top', `${0.8 - ((scale + 0.28571 + 0.071) / 2)}em`);
this.setTitle(title);
function titleDown() {
if (parent && typeof parent.focus === 'function') parent.focus();
}
if (draggable) {
Interaction.makeDraggable(this, parent, true /* limitToWindow */, titleDown);
}
this.onDblClick(() => {
if (self.parent && self.parent.isElement) {
if (typeof self.parent.setInitialSize === 'function') self.parent.setInitialSize();
if (typeof self.parent.center === 'function') self.parent.center();
window.dispatchEvent(new Event('resize'));
}
});
}
setTitle(title = '') {
this.setInnerHtml(title);
let width = parseFloat(Css.getTextWidth(title, Css.getFontCssFromElement(this.dom)));
width += parseFloat(Css.toPx('4em'));
this.setStyle('width', Css.toEm(`${width}px`));
}
}