UNPKG

@lightweightform/theme-common

Version:

Common utilities for Lightweightform themes

666 lines (655 loc) 27.4 kB
import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { Directive, SkipSelf, Input, NgModule, Injectable } from '@angular/core'; import { __decorate } from 'tslib'; import { makeObservable } from 'mobx'; import { observable, computed } from 'mobx-angular'; import { isArrayLike } from '@lightweightform/storage'; /** * Class that represents a container of columns: super class of both the table * header and of the column (which may have sub-columns as children). */ // tslint:disable-next-line: directive-class-suffix class TableColumnContainer { constructor(parentTableColumnContainer, elementRef) { Object.defineProperty(this, "parentTableColumnContainer", { enumerable: true, configurable: true, writable: true, value: parentTableColumnContainer }); Object.defineProperty(this, "elementRef", { enumerable: true, configurable: true, writable: true, value: elementRef }); Object.defineProperty(this, "_childrenColumns", { enumerable: true, configurable: true, writable: true, value: observable.array() }); makeObservable(this); } /** * List of column controllers that are direct children of this table column * container. * @returns List of column children. */ get childrenColumns() { return this._childrenColumns.slice(); } /** * Height of a column container (height of the tree represented by the column * container as root, and its columns as children). * @returns Height of the column container. */ get height() { return (this._childrenColumns.reduce((height, column) => Math.max(height, column.height), 0) + 1); } /** * Width of a column container (width of the tree represented by the column * container as root, and its columns as children). * @returns Width of the column container. */ get width() { // Use the `colspan` of a leaf column as its width return this instanceof TableColumnDirective && this.childrenColumns.length === 0 ? this.colspan != null ? +this.colspan : 1 : this._childrenColumns.reduce((sum, column) => sum + column.width, 0); } ngOnInit() { // Register column in parent column container in same order as declared if (this.parentTableColumnContainer) { const childrenColumns = this.parentTableColumnContainer._childrenColumns; // Find the index of the column which has an index in the parent container // greater than this column and insert this column before it; if no such // column is found, insert this column at the end const indexInParentEl = this.columnIndexInParentEl(); let insertionIndex = -1; for (let i = 0, l = childrenColumns.length; i < l; ++i) { if (this.columnIndexInParentEl(childrenColumns[i]) > indexInParentEl) { insertionIndex = i; break; } } if (insertionIndex === -1) { childrenColumns.push(this); } else { childrenColumns.splice(insertionIndex, 0, this); } } } ngOnDestroy() { // Unregister column if (this.parentTableColumnContainer) { this.parentTableColumnContainer._childrenColumns.splice(this.parentTableColumnContainer._childrenColumns.indexOf(this), 1); } } /** * Index of the column's element in the parent container's element. Used to * make sure that we're inserting columns in the order declared in the HTML * file (needed when conditionally rendering columns). * @param column Column for which to get index. * @returns Index of column in parent container's element. */ columnIndexInParentEl(column = this) { return Array.from(column.parentTableColumnContainer.elementRef.nativeElement.children).indexOf(column.elementRef.nativeElement); } } Object.defineProperty(TableColumnContainer, "\u0275fac", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: TableColumnContainer, deps: [{ token: TableColumnContainer }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }) }); Object.defineProperty(TableColumnContainer, "\u0275dir", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.2.16", type: TableColumnContainer, ngImport: i0 }) }); __decorate([ computed ], TableColumnContainer.prototype, "childrenColumns", null); __decorate([ computed ], TableColumnContainer.prototype, "height", null); __decorate([ computed ], TableColumnContainer.prototype, "width", null); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: TableColumnContainer, decorators: [{ type: Directive }], ctorParameters: function () { return [{ type: TableColumnContainer }, { type: i0.ElementRef }]; }, propDecorators: { childrenColumns: [], height: [], width: [] } }); /** * Directive representing a table column (which may have sub-columns). Leaf * columns (columns with no sub-columns) may specify `minWidth` and `fixed` to * impose sizing restrictions on their respective table columns. */ class TableColumnDirective extends TableColumnContainer { constructor(parentTableColumnContainer = null, elementRef) { super(parentTableColumnContainer, elementRef); Object.defineProperty(this, "elementRef", { enumerable: true, configurable: true, writable: true, value: elementRef }); /** * Identifier of this column (used, for example, to set a label on the * respective table-header cell). */ Object.defineProperty(this, "id", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Colspan of the column; this binding only has an effect if the column is a * leaf column. */ Object.defineProperty(this, "colspan", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * Minimum width of a column (in pixels); this binding is only relevant when * applied to leaf columns. Note that the actual width of a column will be * relative the minimum width of all other columns; as an example a column * with a `minWidth` of `200` should always be twice as large as a column with * a `minWidth` of `100`. * * An array may be provided (with a length matching `colspan`) to specify the * `minWidth` of each column that this column spans over. */ Object.defineProperty(this, "minWidth", { enumerable: true, configurable: true, writable: true, value: void 0 }); /** * `fixed` columns have their width set to `minWidth` and their width won't * ever vary. */ Object.defineProperty(this, "fixed", { enumerable: true, configurable: true, writable: true, value: false }); makeObservable(this); } /** * `rowSpan` that should be set on the header table cell that represents this * column. * @returns`rowSpan` of the cell representing this column. */ get rowSpan() { // Iterate until `columnContainer` is a `TableHeaderDirective` and // `rowIndex` represents the index of the header row (`<tr>`) to which the // cell (`<th>`) representing this column belongs let columnContainer; let rowIndex = 0; for (columnContainer = this.parentTableColumnContainer; columnContainer.parentTableColumnContainer; columnContainer = columnContainer.parentTableColumnContainer) { ++rowIndex; } return this.height > 1 ? 1 : columnContainer.numberOfRows - rowIndex; } /** * `colSpan` that should be set on the header table cell that represents this * column.colSpan * @returns `colSpan` of the cell representing this column. */ get colSpan() { return this.width; } } Object.defineProperty(TableColumnDirective, "\u0275fac", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: TableColumnDirective, deps: [{ token: TableColumnContainer, skipSelf: true }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }) }); Object.defineProperty(TableColumnDirective, "\u0275dir", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.2.16", type: TableColumnDirective, selector: "lf-table-column, [lfTableColumn]", inputs: { id: "id", colspan: "colspan", minWidth: "minWidth", fixed: "fixed" }, providers: [ { provide: TableColumnContainer, useExisting: TableColumnDirective }, ], exportAs: ["lfTableColumn"], usesInheritance: true, ngImport: i0 }) }); __decorate([ observable ], TableColumnDirective.prototype, "id", void 0); __decorate([ observable ], TableColumnDirective.prototype, "colspan", void 0); __decorate([ observable ], TableColumnDirective.prototype, "minWidth", void 0); __decorate([ observable ], TableColumnDirective.prototype, "fixed", void 0); __decorate([ computed ], TableColumnDirective.prototype, "rowSpan", null); __decorate([ computed ], TableColumnDirective.prototype, "colSpan", null); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: TableColumnDirective, decorators: [{ type: Directive, args: [{ selector: 'lf-table-column, [lfTableColumn]', exportAs: 'lfTableColumn', providers: [ { provide: TableColumnContainer, useExisting: TableColumnDirective }, ], }] }], ctorParameters: function () { return [{ type: TableColumnContainer, decorators: [{ type: SkipSelf }] }, { type: i0.ElementRef }]; }, propDecorators: { id: [{ type: Input }], colspan: [{ type: Input }], minWidth: [{ type: Input }], fixed: [{ type: Input }], rowSpan: [], colSpan: [] } }); /** * Directive used to simplify the creation of table headers and manage the size * of a table's max width, min width, and all columns. Tables using these * functionalities should have their `table-layout` CSS property set to `fixed`. * * This directive provides utilities to transform the following: * ```html * <lf-table-header> * <lf-table-column id="id" [fixed]="true" [minWidth]="40"></lf-table-column> * <lf-table-column id="name"> * <lf-table-column id="first"></lf-table-column> * <lf-table-column id="last"></lf-table-column> * </lf-table-column> * <lf-table-column id="age"></lf-table-column> * </lf-table-header> * ``` * Into a table header similar to the following diagram, where the `id` column * has a fixed size and the remaining columns expand (with similar width) to the * table's width. * ``` * | | name | | * | id |---------------------| age | * | | first | last | | * ``` * * The directive provides utilities to help a table set its minimum and maximum * widths, as well as column sizes (using `<colgroup>`) when necessary. The * following example shows the creation of a table that uses an instance of this * directive (named `header`) to manage width and column sizes. The example * assumes that the table's `table-layout` CSS property is set to `fixed`: * ```html * <table * [style.minWidth]="header.tableMinWidth + 'px'" * [style.maxWidth]="header.tableMaxWidth + 'px'" * > * <colgroup> * <col *ngFor="let width of header.columnWidths" [attr.width]="width"/> * </colgroup> * * <thead> * <tr *ngFor="let row of header.rows"> * <th * *ngFor="let column of row" * [attr.rowSpan]="column.rowSpan" * [attr.colSpan]="column.colSpan" * > * {{ column.id }} <!-- Can be used to fetch a label, for example --> * </th> * </tr> * </thead> * * <tbody><!-- ... --></tbody> * </table> * ``` */ class TableHeaderDirective extends TableColumnContainer { constructor(elementRef) { super(null, elementRef); Object.defineProperty(this, "elementRef", { enumerable: true, configurable: true, writable: true, value: elementRef }); /** * Default min width (in `px`) of columns. */ Object.defineProperty(this, "defaultColumnsMinWidth", { enumerable: true, configurable: true, writable: true, value: 100 }); // Sane default makeObservable(this); } /** * Total number of rows of the header (should map to the number of `<tr>` of * the actual header). * @returns Number of rows of the header. */ get numberOfRows() { return this.height - 1; } /** * Number of (leaf) columns of the header (number of columns actually * representing table cells/that don't have sub-columns). * @returns Number of leaf columns of the header. */ get numberOfColumns() { return this.width; } /** * List of rows of the header, where each row contains a list of its columns. * This should directly map each row to a `<tr>` of the header, and each * column to a `<th>`. For the example depicted in the class description, this * method would return: `[[id, name, age], [first, last]]`. * @returns List of rows, where each row is a list of columns. */ get rows() { const rows = []; for (let i = 0; i < this.numberOfRows; ++i) { rows.push(this.columnsOfRow(i)); } return rows; } /** * Minimum width (in pixels) that should be set on the table that contains * this header. If the table does not have a min width, then the columns will * collapse indefinitely, we thus provide a minimum width that amounts to the * sum of the minimum width of all columns. * @returns Minimum width (in pixels) that should be set on the table. */ get tableMinWidth() { return this.leafColumns().reduce((sum, column) => sum + this.columnMinWidth(column), 0); } /** * Maximum width (in pixels) that should be set on the table that contains * this header. Tables where all the columns have a fixed width cannot ever * expand, for that we must set a max width on the table, otherwise this * method returns `Infinity`. * @returns Maximum width (in pixels) that should be set on the table. */ get tableMaxWidth() { return this.leafColumns().every((column) => column.fixed) ? this.tableMinWidth : Infinity; } /** * Width for all table columns, as they should be set in CSS (strings ending * in `'px'` or `'%'` depending on whether the column is fixed). Each of these * widths should be put on a `<col>` element inside a `<colgroup>` before the * definition of the `<thead>` within a `<table>` with the `table-layout` CSS * property set to `fixed` * @returns List of widths of each leaf column. */ get columnWidths() { const leaves = this.leafColumns(); // Columns not set as `fixed` should expand as necessary, as such we provide // them with widths that together add up to `100%` const dynamicMinWidthSum = leaves.reduce((sum, column) => sum + (column.fixed ? 0 : this.columnMinWidth(column)), 0); return leaves.reduce((array, column) => { var _a, _b; const colSpan = (_a = column.colSpan) !== null && _a !== void 0 ? _a : 1; // Determine the widths of each column being spanned by `column` const colMinWidthArray = isArrayLike(column.minWidth) ? column.minWidth : // Distribute the width amongst the columns being spanned [...Array(colSpan)].map(() => this.columnMinWidth(column) / colSpan); for (let i = 0; i < colSpan; ++i) { // Columns will end up with a width of `0` when a `minWidth` array is // provided with length smaller than `colSpan` const colMinWidth = (_b = colMinWidthArray[i]) !== null && _b !== void 0 ? _b : 0; array.push(column.fixed ? `${colMinWidth}px` : `${(colMinWidth / dynamicMinWidthSum) * 100}%`); } return array; }, []); } /** * Method that returns the list of leaf columns: columns whose label directly * represents a table cell/columns that contain no sub-columns. For the * example depicted in the class description, this method would return: * `[id, first, last, age]`. * @param columnContainer For internal use only (to recurse over columns). * @returns List of columns that are leaves. */ leafColumns(columnContainer = this) { return columnContainer.childrenColumns.reduce((leaves, column) => leaves.concat(column.childrenColumns.length === 0 ? [column] : this.leafColumns(column)), []); } /** * Method that returns the list of columns of a given row (`<tr>`) of the * table header. For the example depicted in the class description, for row * `0` this method would return `[id, name, age]`; for row `1` it would return * `[first, last]`. * @param index Index of row from which to fetch columns. * @param columnContainer For internal use only (to recurse over columns). * @returns List of columns of row with index `index`. */ columnsOfRow(index, columnContainer = this) { return index === 0 ? columnContainer.childrenColumns : columnContainer.childrenColumns.reduce((rowColumns, column) => rowColumns.concat(this.columnsOfRow(index - 1, column)), []); } /** * Minimum width of a column (or a default minimum width when none has been * created). * @param column Column whose minimum width we are interested in. * @returns Minimum width of the column. */ columnMinWidth(column) { var _a; return isArrayLike(column.minWidth) ? column.minWidth.reduce((sum, width) => sum + width, 0) : (_a = column.minWidth) !== null && _a !== void 0 ? _a : this.defaultColumnsMinWidth * column.colSpan; } } Object.defineProperty(TableHeaderDirective, "\u0275fac", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: TableHeaderDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }) }); Object.defineProperty(TableHeaderDirective, "\u0275dir", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareDirective({ minVersion: "12.0.0", version: "12.2.16", type: TableHeaderDirective, selector: "lf-table-header, [lfTableHeader]", inputs: { defaultColumnsMinWidth: "defaultColumnsMinWidth" }, providers: [ { provide: TableColumnContainer, useExisting: TableHeaderDirective }, ], exportAs: ["lfTableHeader"], usesInheritance: true, ngImport: i0 }) }); __decorate([ observable ], TableHeaderDirective.prototype, "defaultColumnsMinWidth", void 0); __decorate([ computed ], TableHeaderDirective.prototype, "numberOfRows", null); __decorate([ computed ], TableHeaderDirective.prototype, "numberOfColumns", null); __decorate([ computed ], TableHeaderDirective.prototype, "rows", null); __decorate([ computed ], TableHeaderDirective.prototype, "tableMinWidth", null); __decorate([ computed ], TableHeaderDirective.prototype, "tableMaxWidth", null); __decorate([ computed ], TableHeaderDirective.prototype, "columnWidths", null); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: TableHeaderDirective, decorators: [{ type: Directive, args: [{ selector: 'lf-table-header, [lfTableHeader]', exportAs: 'lfTableHeader', providers: [ { provide: TableColumnContainer, useExisting: TableHeaderDirective }, ], }] }], ctorParameters: function () { return [{ type: i0.ElementRef }]; }, propDecorators: { defaultColumnsMinWidth: [{ type: Input }], numberOfRows: [], numberOfColumns: [], rows: [], tableMinWidth: [], tableMaxWidth: [], columnWidths: [] } }); /** * Exported/declared components and directives. */ const DECLARATIONS = [TableColumnDirective, TableHeaderDirective]; /** * LF table header module. */ class LfTableHeaderModule { } Object.defineProperty(LfTableHeaderModule, "\u0275fac", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: LfTableHeaderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }) }); Object.defineProperty(LfTableHeaderModule, "\u0275mod", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: LfTableHeaderModule, declarations: [TableColumnDirective, TableHeaderDirective], imports: [CommonModule], exports: [TableColumnDirective, TableHeaderDirective] }) }); Object.defineProperty(LfTableHeaderModule, "\u0275inj", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: LfTableHeaderModule, providers: [], imports: [[CommonModule]] }) }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: LfTableHeaderModule, decorators: [{ type: NgModule, args: [{ imports: [CommonModule], declarations: DECLARATIONS, exports: DECLARATIONS, providers: [], }] }] }); // Module /** * Service providing scrolling-related utilities. */ class ScrollbarUtils { /** * Computes the width/height (in px) of vertical/horizontal scrollbars in the * user's browser and caches the results. This method should only ever be * called once. */ static computeScrollbarSizes() { const div = document.createElement('div'); div.innerHTML = `<div style="overflow:auto; position:absolute; top:0; width:100px; height:100px;"> <div style="width:200px; height:200px;"></div> </div>`; const child = div.firstChild; document.body.appendChild(div); ScrollbarUtils.scrollbarWidth = child.offsetWidth - child.clientWidth; ScrollbarUtils.scrollbarHeight = child.offsetHeight - child.clientHeight; document.body.removeChild(div); } /** * Width (in px) of a vertical scrollbar in the user's browser. This value is * only computed once and cached for subsequent calls. * @returns Width of the vertical scrollbar in the user's browser. */ get verticalScrollbarWidth() { if (ScrollbarUtils.scrollbarWidth === undefined) { ScrollbarUtils.computeScrollbarSizes(); } return ScrollbarUtils.scrollbarWidth; } /** * Height (in px) of a horizontal scrollbar in the user's browser. This value * is only computed once and cached for subsequent calls. * @returns Height of the horizontal scrollbar in the user's browser. */ get horizontalScrollbarHeight() { if (ScrollbarUtils.scrollbarHeight === undefined) { ScrollbarUtils.computeScrollbarSizes(); } return ScrollbarUtils.scrollbarHeight; } /** * Whether a given DOM element has a vertical scrollbar. * @param element Element to check for vertical scrollbar. * @returns Whether the provided element has a vertical scrollbar. */ // FIXME: Unreliable in IE/Edge hasVerticalOverflow(element) { return element.scrollHeight > element.clientHeight; } /** * Whether a given DOM element has a horizontal scrollbar. * @param element Element to check for horizontal scrollbar. * @returns Whether the provided element has a horizontal scrollbar. */ // FIXME: Unreliable in IE/Edge hasHorizontalOverflow(element) { return element.scrollWidth > element.clientWidth; } } // Only compute sizes once (this assumes that browser scrollbar sizes don't // change over time, which seems like a valid assumption) Object.defineProperty(ScrollbarUtils, "scrollbarWidth", { enumerable: true, configurable: true, writable: true, value: undefined }); Object.defineProperty(ScrollbarUtils, "scrollbarHeight", { enumerable: true, configurable: true, writable: true, value: undefined }); Object.defineProperty(ScrollbarUtils, "\u0275fac", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: ScrollbarUtils, deps: [], target: i0.ɵɵFactoryTarget.Injectable }) }); Object.defineProperty(ScrollbarUtils, "\u0275prov", { enumerable: true, configurable: true, writable: true, value: i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: ScrollbarUtils, providedIn: 'root' }) }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "12.2.16", ngImport: i0, type: ScrollbarUtils, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Executes a task as soon as possible. * @param task Function or any object that implements `call()`. */ // There doesn't seem to be a way of importing this module using ES6 syntax that // both TypeScript and Rollup accept const asap = require('asap'); // tslint:disable-line:no-var-requires // Modules /** * Generated bundle index. Do not edit. */ export { LfTableHeaderModule, ScrollbarUtils, TableColumnContainer, TableColumnDirective, TableHeaderDirective, asap }; //# sourceMappingURL=lightweightform-theme-common.js.map