UNPKG

@scidian/osui

Version:

Lightweight JavaScript UI library.

323 lines (270 loc) 11.5 kB
import { ColorScheme } from '../utils/ColorScheme.js'; import { Css } from '../utils/Css.js'; import { Div } from '../core/Div.js'; import { IMAGE_EMPTY } from '../constants.js'; import { Interaction } from '../utils/Interaction.js'; import { Iris } from '../utils/Iris.js'; import { Panel, PANEL_STYLES } from './Panel.js'; import { TRAIT } from '../constants.js'; import { VectorBox } from '../layout/VectorBox.js'; export const TAB_SIDES = { LEFT: 'left', RIGHT: 'right', } class Tabbed extends Panel { #startWidth = null; #startHeight = null; #minWidth = 0; #maxWidth = Infinity; #minHeight = 0; #maxHeight = Infinity; constructor({ tabSide = TAB_SIDES.RIGHT, style = PANEL_STYLES.FANCY, resizers = [], startWidth = null, startHeight = null, minWidth = 0, maxWidth = Infinity, minHeight = 0, maxHeight = Infinity, } = {}) { super({ style }); const self = this; this.addClass('osui-tabbed'); this.setName('osui-tabbed'); // Private Properties this.#startWidth = startWidth; this.#minWidth = minWidth; this.#maxWidth = maxWidth; this.#startHeight = startHeight; this.#minHeight = minHeight; this.#maxHeight = maxHeight; // Public Properties this.tabs = []; this.panels = []; this.selectedId = ''; this.selectedCount = 0; // Resizers const rect = {}; function resizerDown() { rect.width = self.getWidth(); rect.height = self.getHeight(); self.dom.dispatchEvent(new Event('clicked', { 'bubbles': true, 'cancelable': true })); } function resizerMove(resizer, diffX, diffY) { if (resizer.hasClassWithString('left')) self.changeWidth(rect.width - diffX); if (resizer.hasClassWithString('right')) self.changeWidth(rect.width + diffX); if (resizer.hasClassWithString('top')) self.changeHeight(rect.height - diffY); if (resizer.hasClassWithString('bottom')) self.changeHeight(rect.height + diffY); } Interaction.makeResizeable(this, this, resizers, resizerDown, resizerMove); // Children Elements this.tabsDiv = new Div().setClass('osui-tabs').setDisplay('none'); this.panelsDiv = new Div().setClass('osui-tab-panels'); this.add(this.tabsDiv); this.add(this.panelsDiv); // Set side (LEFT / RIGHT) that tabs should appear this.setTabSide(tabSide); } /******************** RESIZE ********************/ changeWidth(width) { if (typeof width !== 'number' || Number.isNaN(width) || !Number.isFinite(width)) width = this.#startWidth; if (width == null) { this.dom.style.removeProperty('width'); return null; } const scaledMinWidth = this.#minWidth * Css.guiScale(this.dom); const scaledMaxWidth = this.#maxWidth * Css.guiScale(this.dom); width = Math.min(scaledMaxWidth, Math.max(scaledMinWidth, parseFloat(width))).toFixed(1); this.setStyle('width', Css.toEm(width, this.dom)); this.dom.dispatchEvent(new Event('resized')); return width; } changeHeight(height) { if (typeof height !== 'number' || Number.isNaN(height) || !Number.isFinite(height)) height = this.#startHeight; if (height == null) { this.dom.style.removeProperty('height'); return null; } const scaledMinHeight = this.#minHeight * Css.guiScale(this.dom); const scaledMaxHeight = this.#maxHeight * Css.guiScale(this.dom); height = Math.min(scaledMaxHeight, Math.max(scaledMinHeight, parseFloat(height))).toFixed(1); this.setStyle('height', Css.toEm(height, this.dom)); this.dom.dispatchEvent(new Event('resized')); return height; } /******************** TABS ********************/ /** Add Tab */ addTab(id, content, options = {}) { if (typeof options !== 'object') options = {}; if (!('color' in options) || options.color == null) options.color = ColorScheme.color(TRAIT.ICON); if (!('alpha' in options)) options.alpha = 1.0; if (!('icon' in options))options.icon = IMAGE_EMPTY; if (!('shadow' in options)) options.shadow = 0x000000; if (!('shrink' in options)) options.shrink = 1; if (options.shrink === true) options.shrink = 0.7; if (typeof options.shrink === 'string') { options.shrink = parseFloat(options.shrink) / (options.shrink.includes('%') ? 100 : 1); } // Count ID's let numTabsWithId = 0; for (let i = 0; i < this.tabs.length; i++) { const tab = this.tabs[i]; if (tab.dom.id === id) numTabsWithId++; } function capitalize(string) { const words = String(string).split(' '); for (let i = 0; i < words.length; i++) words[i] = words[i][0].toUpperCase() + words[i].substring(1); return words.join(' '); } // Create tab const label = capitalize(id); const tab = new TabButton(this, label, options); tab.setId(id); tab.count = numTabsWithId; // Push onto containers this.tabs.push(tab); this.tabsDiv.add(tab); // // NOTE: If below is changed from '0' to '1', tabs will be hidden when there is only 1 tab const hideWhenNumberOfTabs = 0; if (this.tabs.length > hideWhenNumberOfTabs) this.tabsDiv.setDisplay(''); const panel = new Panel().setId(id); panel.addClass('osui-tab-panel', 'osui-hidden'); panel.add(content); panel.count = numTabsWithId; this.panels.push(panel); this.panelsDiv.add(panel); // Minimum height (so tab buttons dont float over nothing) this.setContentsStyle('minHeight', ''); if (this.tabsDiv.hasClass('osui-left-side') || this.tabsDiv.hasClass('osui-right-side')) { this.setContentsStyle('minHeight', ((2.2 * this.tabs.length) + 0.4) + 'em'); } return panel; } /** Select first tab */ selectFirst() { if (this.tabs.length > 0) { return this.selectTab(this.tabs[0].getId()); } return false; } /** Select last known tab */ selectLastKnownTab() { // TO BE IMPLEMENTED IN APP } /** Select Tab */ selectTab(id, count = 0, wasClicked = false) { if (this.tabs == undefined) return this; const self = this; // Find tab / panel by id const tab = this.tabs.find(function(item) { return (item.dom.id === id && item.count === count); }); const panel = this.panels.find(function(item) { return (item.dom.id === id && item.count === count); }); if (tab && panel) { // Disable animations while rebuilding if (!wasClicked) Css.setVariable('--tab-timing', '0', tab.dom); // Deselect current selection const currentTab = this.tabs.find(function(item) { return (item.dom.id === self.selectedId && item.count === self.selectedCount); }); const currentPanel = this.panels.find(function(item) { return (item.dom.id === self.selectedId && item.count === self.selectedCount); }); if (currentTab) currentTab.removeClass('osui-selected'); if (currentPanel) currentPanel.addClass('osui-hidden'); // Select new tab and panel tab.addClass('osui-selected'); panel.removeClass('osui-hidden'); // Set id this.selectedId = id; this.selectedCount = count; // Emit event if (wasClicked) { const tabChange = new Event('tab-changed'); tabChange.value = id; this.dom.dispatchEvent(tabChange); } // Re-enable animationss if (!wasClicked) setTimeout(() => Css.setVariable('--tab-timing', '200ms', tab.dom), 50); return true; } return false; } destroy() { this.clearTabs(); super.destroy(); } clearTabs() { if (this.tabsDiv) this.tabsDiv.clearContents(); if (this.panelsDiv) this.panelsDiv.clearContents(); if (this.tabs) { for (let i = 0; i < this.tabs.length; i++) this.tabs[i].destroy(); this.tabs.length = 0; } if (this.panels) { for (let i = 0; i < this.panels.length; i++) this.panels[i].destroy(); this.panels.length = 0; } this.setStyle('minHeight', ''); } currentId() { return this.selectedId; } setTabSide(side) { side = String(side).toLowerCase(); this.tabsDiv.removeClass('osui-left-side', 'osui-right-side'); this.tabsDiv.addClass((side === TAB_SIDES.RIGHT) ? 'osui-right-side' : 'osui-left-side'); } tabIndex(id) { return this.tabs.indexOf(id); } } /******************** TAB BUTTON ********************/ const _color = new Iris(); class TabButton extends Div { constructor(parent, label, options = {}) { super(); const self = this; this.setClass('osui-tab-button'); this.setStyle('cursor', 'default'); if (options.shadow) this.addClass('osui-tab-shadow'); // Icon / Label this.iconVector = new VectorBox(options.icon); this.iconBorder = new Div().setClass('osui-tab-icon-border'); this.add(this.iconVector, this.iconBorder); this.setLabel = function(label) { self.iconBorder.dom.setAttribute('tooltip', label); }; this.setLabel(label); // Background Color if (typeof options.color === 'string' && options.color.includes('var(--')) { this.iconVector.setStyle('background-color', `rgba(${options.color}, ${options.alpha})`); } else { _color.set(options.color); const light = `rgba(${_color.rgbString(options.alpha)})`; const dark = `rgba(${_color.darken(0.75).rgbString(options.alpha)})`; const background = `linear-gradient(to bottom left, ${light}, ${dark})`; this.iconVector.setStyle('background-image', background); } // Drop Shadow const shadow = options.shadow; if (this.iconVector.img && shadow !== false) { _color.set(shadow); const dropShadow = `drop-shadow(0 0 var(--pad-micro) rgba(${_color.rgbString()}, 0.8))`; this.iconVector.img.setStyle('filter', dropShadow); } // Shrink? const shrink = options.shrink; if (this.iconVector.img && !isNaN(shrink)) { this.iconVector.img.setStyle('position', 'absolute'); this.iconVector.img.setStyle('left', '0', 'right', '0', 'top', '0', 'bottom', '0'); this.iconVector.img.setStyle('margin', 'auto'); this.iconVector.img.setStyle('width', `${shrink * 100}%`); this.iconVector.img.setStyle('height', `${shrink * 100}%`); } // Events this.onClick(() => { parent.selectTab(self.dom.id, self.count, true); parent.dom.dispatchEvent(new Event('resized')); }); } } export { Tabbed };