x4js
Version:
403 lines (311 loc) • 8.08 kB
text/typescript
/**
* ___ ___ __
* \ \/ / / _
* \ / /_| |_
* / \____ _|
* /__/\__\ |_|
*
* @file boxes.ts
* @author Etienne Cochard
*
* @copyright (c) 2024 R-libre ingenierie
*
* Use of this source code is governed by an MIT-style license
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
**/
import { asap, class_ns, isArray, isNumber, isString } from '../../core/core_tools';
import { Component, ComponentContent, ComponentEvents, ComponentProps, EvSelectionChange } from "../../core/component"
import "./boxes.module.scss";
import { EventCallback } from '../../core/core_events';
export interface BoxProps extends ComponentProps {
tag?: string;
}
/**
*
*/
export class Box<P extends BoxProps=BoxProps,E extends ComponentEvents=ComponentEvents> extends Component<P,E> {
}
/**
*
*/
export class HBox<P extends BoxProps=BoxProps,E extends ComponentEvents=ComponentEvents> extends Box<P,E> {
}
/**
*
*/
export class VBox<P extends BoxProps=BoxProps,E extends ComponentEvents=ComponentEvents> extends Box<P,E> {
constructor( p: P ) {
super( p );
}
}
/**
* stack of widgets where only one widget is visible at a time
*/
type ContentBuilder = ( ) => Component;
interface StackItem {
name: string;
content: Component | ContentBuilder;
title?: string;
}
/**
*
*/
interface StackeBoxEvents extends ComponentEvents {
pageChange?: EvSelectionChange;
}
export interface StackBoxProps extends Omit<ComponentProps,"content"> {
default: string;
items: StackItem[];
pageChange?: EventCallback<EvSelectionChange>;
}
/**
*
*/
interface StackItemEx extends StackItem {
page: Component;
}
/**
*
*/
export class StackBox<P extends StackBoxProps = StackBoxProps, E extends StackeBoxEvents = StackeBoxEvents> extends Box<StackBoxProps,StackeBoxEvents> {
protected _items: StackItemEx[];
protected _cur: number;
constructor( props: StackBoxProps ) {
super( props );
this.mapPropEvents( props, "pageChange" );
this._items = props.items?.map( itm => {
return { ...itm, page: null as any};
});
if( props.default ) {
this.select( props.default );
}
else if( this._items.length ) {
this.select( this._items[0].name );
}
}
addItem( item: StackItem ) {
this._items.push( {
name: item.name,
content: item.content,
page: null
});
}
removeItem( name: string ) {
const index = this._items.findIndex( x => x.name==name );
if( index>=0 ) {
const pg = this._items[index];
if( pg?.page ) {
this.removeChild( pg.page );
}
this._items.splice( index, 1 );
}
}
select( name: string ) {
let sel = this.query( `:scope > .selected` );
if( sel ) {
sel.setClass( "selected", false );
}
this._cur = this._items.findIndex( x => x.name==name );
const pg = this._items[this._cur];
if( pg ) {
if( !pg.page ) {
pg.page = this._createPage( pg );
this.appendContent( pg.page );
}
sel = pg.page;
if( sel ) {
(sel as any).activate?.( );
sel.setClass( "selected", true );
}
asap( ( ) => this.fire( "pageChange", { selection: [pg.name], empty: !sel } ) );
}
return pg?.page;
}
/**
*
*/
private _createPage( page: StackItemEx ) {
let content: Component;
if( page.content instanceof Function ) {
content = page.content( );
page.content = content; // keep it
}
else {
content = page.content;
}
content?.setData( "stackname", page.name );
return content;
}
/**
*
*/
getPage( name: string ) {
const pg = this._items.find( x => x.name==name );
return pg ? pg.content : null;
}
/**
*
*/
getPageCount( ) {
return this._items.length;
}
/**
*
*/
enumPageNames( ) {
return this._items.map( x => x.name );
}
/**
*
*/
getItem( name: string ) {
const pg = this._items.find( x => x.name==name );
return pg;
}
/**
*
*/
getCurPage( ) {
const c = this._items[this._cur];
return c?.name;
}
}
// :: ASSIST BOX ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
export class AssistBox extends StackBox {
selectNextPage( nxt = true ) {
let p;
if( nxt && this._cur<this._items.length-1 ) {
p = this._items[this._cur+1];
}
else if( !nxt && this._cur>0 ) {
p = this._items[this._cur-1];
}
if( p ) {
this.select( p.name );
}
}
isFirstPage( ) {
return this._cur==0;
}
isLastPage( ) {
return this._cur==this._items.length-1;
}
}
// :: GRIDBOX ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
interface GridBoxItem {
row: number; // starts at 0
col: number; // starts at 0
item: Component;
}
export interface GridBoxProps extends Omit<BoxProps,"content"> {
rows?: number | string | string[];
columns?: number | string | string[];
items?: GridBoxItem[];
}
export class GridBox<P extends GridBoxProps=GridBoxProps,E extends ComponentEvents=ComponentEvents> extends Box<P,E> {
constructor( props: P ) {
super( props );
if( props.rows!==undefined ) {
this.setRows( props.rows );
}
if( props.columns!==undefined ) {
this.setCols( props.columns );
}
if( props.items ) {
this.setItems( props.items );
}
}
setRows( r: number | string | string[] ) {
if( isArray(r) ) {
r = r.join( " " );
}
else if( isNumber(r) ) {
r = `repeat( ${r}, 1fr )`;
}
this.setStyleValue( "gridTemplateRows", r );
}
setCols( r: number | string | string[] ) {
if( isArray(r) ) {
r = r.join( " " );
}
else if( isNumber(r) ) {
r = `repeat( ${r}, 1fr )`;
}
this.setStyleValue( "gridTemplateColumns", r );
}
setRowCount( n: number ) {
this.setStyleValue( "gridTemplateRows", `repeat(${n})` );
}
setColCount( n: number ) {
this.setStyleValue( "gridTemplateColumns", `repeat(${n})` );
}
/**
* @param t "a a a" "b c c" "b c c"
* user item.setAttribute( "grid-area", "a" );
*/
setTemplate( t: string[] ) {
this.setAttribute( "grid-template-area", t.map( x => '"' + x + '"' ).join(" ") );
}
setItems( items: GridBoxItem[] ) {
items.forEach( x => {
x.item.setStyle( {
gridColumn: (x.col+1)+"",
gridRow: (x.row+1)+"",
} );
});
this.setContent( items.map( x => x.item ) );
}
}
// :: MASONRY ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// from a nice article of Andy Barefoot
// https://medium.com/@andybarefoot/a-masonry-style-layout-using-css-grid-8c663d355ebb
interface MasonryProps extends Omit<BoxProps,"content"> {
items: Component[];
}
export class MasonryBox extends Box<MasonryProps> {
constructor(props: MasonryProps ) {
super(props);
this.addDOMEvent( 'resized', () => {
this.resizeAllItems( );
});
if( props.items ) {
this.setItems( props.items );
}
}
resizeItem(item: Component) {
const style = this.getComputedStyle();
const rowHeight = parseInt(style['gridAutoRows']);
const rowGap = parseInt(style['rowGap']);
let content = item.query('.content');
if( !content ) {
content = item;
}
if (content && (rowHeight + rowGap)) {
const rc = content.getBoundingRect();
const rowSpan = Math.ceil( (rc.height + rowGap) / (rowHeight + rowGap) );
item.setStyleValue('gridRowEnd', "span " + rowSpan);
}
}
resizeAllItems( ) {
const els = this.queryAll( ".item" );
els.forEach( itm => {;
this.resizeItem( itm );
} );
}
setItems( items: Component[] ) {
const els = items.map( x => {
return new Box( {
cls: 'item',
content: x
} );
});
this.setContent( els );
}
}