@lightweightform/theme-common
Version:
Common utilities for Lightweightform themes
666 lines (655 loc) • 27.4 kB
JavaScript
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