import * as GoldenLayout from "golden-layout";
import {
containersIn, filterState, panelsIn, transformState
} from "./iterators";
import { ComponentConstructor, ItemConfigType } from "./types";
import { Workbench } from "./workbench";
/**
* Builder class with a fluid API that enables the user to build a
* perspective of an existing workbench.
*/
export class PerspectiveBuilder {
/**
* The content of the perspective being built.
*/
private _content: ItemConfigType[];
/**
* The stack of content objects being manipulated by the builder.
*/
private _contentStack: ItemConfigType[];
/**
* When undefined, it means that the perspective is not finalized yet and
* new panels may be added. When not undefined, it contains a GoldenLayout
* configuration object with the finalized set of panels. After finalization,
* no new panels may be added to the perspective; only filtering and
* transformation is allowed.
*
* The first call to `filter()` or `map()` will implicitly finalize the
* perspective and prevents the addition of any further panels.
*/
private _finalizedConfiguration: GoldenLayout.Config | undefined;
/**
* The workbench being manipulated by this builder; undefined
* if the builder has already built a perspective.
*/
private _workbench: Workbench | undefined;
/**
* Constructor.
*
* Creates a new perspective builder that builds a new perspective
* belonging to a given workbench.
*
* @param workbench the workbench that the perspective builder will
* manipulate when it registers new components
*/
constructor(workbench: Workbench) {
this._workbench = workbench;
this._contentStack = [];
this._content = [];
}
/**
* Adds a new component to be shown in the current subdivision.
*/
public add(
nameOrComponent: string | React.ComponentType,
{ eager, title, props, state }: {
props?: TProps,
state?: any,
title?: string,
eager?: boolean
} = {},
id?: string
): this {
this._assertNotFinalized();
const workbench = this._assertHasWorkbench();
const newItem = workbench.createItemConfigurationFor(
nameOrComponent, { eager, props, title }
);
if (state !== undefined) {
(newItem as GoldenLayout.ComponentConfig).componentState = state;
}
const panel = this._currentPanelContent;
if (panel === undefined) {
this._contentStack.push(newItem);
} else {
panel.push(newItem);
}
if (id !== undefined) {
this.setId(id);
}
return this;
}
/**
* Builds the perspective based on the set of configuration methods called
* earlier in this call chain.
*/
public build(): GoldenLayout.ItemConfigType[] {
this._finalize();
this._assertHasWorkbench();
this._workbench = undefined;
const result = this._content;
this._content = [];
return result;
}
/**
* Iterates over each content item currently added to the perspective,
* including containers, and removes those for which the given filter
* predicate returns false.
*
* @param pred the filter predicate
*/
public filter(pred: (item: ItemConfigType) => boolean): this {
filterState(pred, this._finalize());
return this;
}
/**
* Iterates over each container currently added to the perspective,
* not including panels, and removes those for which the given filter
* predicate returns false.
*
* @param pred the filter predicate
*/
public filterContainers(pred: (item: ItemConfigType) => boolean): this {
filterState(pred, containersIn(this._finalize()));
return this;
}
/**
* Iterates over each panel currently added to the perspective,
* not including containers, and removes those for which the given filter
* predicate returns false.
*
* @param pred the filter predicate
*/
public filterPanels(pred: (item: ItemConfigType) => boolean): this {
filterState(pred, panelsIn(this._finalize()));
return this;
}
/**
* Declares that the current subdivision (row, column or stack) being built
* is finished and moves back to the parent element.
*/
public finish(): this {
this._assertNotFinalized();
if (this._contentStack.length === 0) {
throw new Error("no panel is currently being built");
}
const poppedItem = this._contentStack.pop() as ItemConfigType;
if (this._contentStack.length === 0) {
this._content.push(poppedItem);
}
return this;
}
/**
* Subdivides the current panel in the workbench into multiple columns.
* Subsequent calls to add() will add new columns to the workbench
* within the subdivision.
*/
public makeColumns(): this {
return this._makeNewPanel("row");
}
/**
* Subdivides the current panel in the workbench into multiple rows.
* Subsequent calls to add() will add new rows to the workbench
* within the subdivision.
*/
public makeRows(): this {
return this._makeNewPanel("column");
}
/**
* Subdivides the current panel in the workbench into multiple stacked items.
* Subsequent calls to add() will add new items to the workbench
* within the current stack.
*/
public makeStack(): this {
return this._makeNewPanel("stack");
}
/**
* Iterates over each content item currently added to the perspective,
* including containers, and transforms them via a mapping function.
*
* @param func the mapping function
*/
public map(func: (item: ItemConfigType) => ItemConfigType): this {
transformState(func, this._finalize());
return this;
}
/**
* Iterates over each container currently added to the perspective,
* not including panels, and transforms them via a mapping function.
*
* @param func the mapping function
*/
public mapContainers(func: (item: ItemConfigType) => ItemConfigType): this {
transformState(func, containersIn(this._finalize()));
return this;
}
/**
* Iterates over each panel currently added to the perspective,
* not including containers, and transforms them via a mapping function.
*
* @param func the mapping function
*/
public mapPanels(func: (item: ItemConfigType) => ItemConfigType): this {
transformState(func, panelsIn(this._finalize()));
return this;
}
/**
* Prevents the reordering of the component that was added to the
* workbench most recently.
*/
public preventReorder(): this {
return this.setReorderEnabled(false);
}
/**
* Sets the identifier of the component that was added to the workbench
* most recently.
*/
public setId(value: string | string[]): this {
return this.setProperties({
id: typeof value === "string" ? value : value.concat()
});
}
/**
* Sets whether the component that was added to the workbench most recently
* is closable or not.
*/
public setClosable(value = true): this {
return this.setProperties({ isClosable: value });
}
/**
* Sets the relative height of the component that was added to the workbench
* most recently, in percentage, compared to its siblings in the same panel.
*/
public setRelativeHeight(value: number): this {
return this.setProperties({ height: value });
}
/**
* Sets the relative width of the component that was added to the workbench
* most recently, in percentage, compared to its siblings in the same panel.
*/
public setRelativeWidth(value: number): this {
return this.setProperties({ width: value });
}
/**
* Sets whether the component that was added to the workbench most recently
* can be rearranged on the workbench by dragging it around.
*/
public setReorderEnabled(value = true): this {
/* cast needed to "any" because reorderEnabled is not included in the
* typing of the item configuration even though golden-layout
* understands it */
return this.setProperties({ reorderEnabled: value } as any);
}
/**
* Sets the title of the component that was added to the workbench
* most recently.
*/
public setTitle(value: string): this {
return this.setProperties({ title: value });
}
/**
* Sets multiple properties of the last component that was added to the
* workbench.
*/
public setProperties(props: Partial): this {
Object.assign(this._lastAddedComponent, props);
return this;
}
private _assertNotFinalized(): void {
if (this._finalizedConfiguration) {
throw new Error("no new panels may be added to the perspective any more");
}
}
private _assertHasWorkbench(): Workbench {
if (this._workbench === undefined) {
throw new Error("builder has already built a workbench");
}
return this._workbench;
}
private get _currentPanel(): ItemConfigType | undefined {
return this._contentStack.length > 0 ?
this._contentStack[this._contentStack.length - 1] : undefined;
}
private get _currentPanelContent(): ItemConfigType[] | undefined {
const panel = this._currentPanel;
return panel ? panel.content : undefined;
}
private _finalize(): GoldenLayout.Config {
if (this._finalizedConfiguration === undefined) {
while (this._contentStack.length > 0) {
this.finish();
}
if (this._content.length !== 1) {
// Create an empty stack so we have something at the root
this.makeStack().finish();
}
this._finalizedConfiguration = {
content: this._content
};
}
return this._finalizedConfiguration;
}
private get _lastAddedComponent(): ItemConfigType {
const panel = this._currentPanel;
const content = panel && panel.content && panel.content.length > 0 ?
panel.content : undefined;
if (content === undefined) {
throw new Error("no component was added to the current panel yet");
}
return content[content.length - 1];
}
private _makeNewPanel(type: string): this {
this._assertNotFinalized();
const newPanel: GoldenLayout.ItemConfig = {
content: [],
type
};
if (this._currentPanelContent) {
this._currentPanelContent.push(newPanel);
}
this._contentStack.push(newPanel);
return this;
}
}
/**
* Builder class with a fluid API that enables the user to build a complete
* workbench object and its initial layout from scratch.
*/
export class WorkbenchBuilder {
/**
* The workbench being built by this builder; undefined if the
* builder has already built one.
*/
private _workbench: Workbench | undefined;
/**
* The wrapped perspective builder that the workbench builder uses.
*/
private _builder: PerspectiveBuilder;
/**
* The global settings of the workbench being built.
*/
private _settings: GoldenLayout.Settings;
/**
* Constructor.
*
* Creates a new workbench builder that can build new Workbench instances.
*
* @param factory a factory function that returns a new Workbench instance
* when invoked with no arguments. When omitted, it defaults to simply
* calling new Workbench().
*/
constructor(factory?: () => Workbench) {
this._workbench = factory ? factory() : new Workbench();
this._builder = new PerspectiveBuilder(this._workbench);
this._settings = {};
}
/**
* Adds a new component to be shown in the current subdivision.
*/
public add(
nameOrComponent: string | React.ComponentType,
options: {
props?: TProps,
state?: any,
title?: string
} = {},
id?: string
): this {
this._builder.add(nameOrComponent, options, id);
return this;
}
/**
* Builds the workbench based on the set of configuration methods called
* earlier in this call chain.
*/
public build(): Workbench {
const workbench = this._assertHasWorkbench();
const configuration: GoldenLayout.Config = {
content: this._builder.build(),
settings: this._settings
};
this._workbench = undefined;
workbench.configure(configuration);
return workbench;
}
/**
* Iterates over each content item currently added to the perspective,
* including containers, and removes those for which the given filter
* predicate returns false.
*
* @param pred the filter predicate
*/
public filter(pred: (item: ItemConfigType) => boolean): this {
this._builder.filter(pred);
return this;
}
/**
* Iterates over each container currently added to the perspective,
* not including panels, and removes those for which the given filter
* predicate returns false.
*
* @param pred the filter predicate
*/
public filterContainers(pred: (item: ItemConfigType) => boolean): this {
this._builder.filterContainers(pred);
return this;
}
/**
* Iterates over each panel currently added to the perspective,
* not including containers, and removes those for which the given filter
* predicate returns false.
*
* @param pred the filter predicate
*/
public filterPanels(pred: (item: ItemConfigType) => boolean): this {
this._builder.filterPanels(pred);
return this;
}
/**
* Declares that the current subdivision (row, column or stack) being built
* is finished and moves back to the parent element.
*/
public finish(): this {
this._builder.finish();
return this;
}
/**
* Specifies that panel headers should *not* be shown in the workbench.
*/
public hideHeaders(): this {
return this.showHeaders(false);
}
/**
* Subdivides the current panel in the workbench into multiple columns.
* Subsequent calls to add() will add new columns to the workbench
* within the subdivision.
*/
public makeColumns(): this {
this._builder.makeColumns();
return this;
}
/**
* Subdivides the current panel in the workbench into multiple rows.
* Subsequent calls to add() will add new rows to the workbench
* within the subdivision.
*/
public makeRows(): this {
this._builder.makeRows();
return this;
}
/**
* Subdivides the current panel in the workbench into multiple stacked items.
* Subsequent calls to add() will add new items to the workbench
* within the current stack.
*/
public makeStack(): this {
this._builder.makeStack();
return this;
}
/**
* Iterates over each content item currently added to the workbench,
* including containers, and transforms them via a mapping function.
*
* @param func the mapping function
*/
public map(func: (item: ItemConfigType) => ItemConfigType): this {
this._builder.map(func);
return this;
}
/**
* Iterates over each container currently added to the workbench,
* not including panels, and transforms them via a mapping function.
*
* @param func the mapping function
*/
public mapContainers(func: (item: ItemConfigType) => ItemConfigType): this {
this._builder.mapContainers(func);
return this;
}
/**
* Iterates over each panel currently added to the workbench,
* not including containers, and transforms them via a mapping function.
*
* @param func the mapping function
*/
public mapPanels(func: (item: ItemConfigType) => ItemConfigType): this {
this._builder.mapPanels(func);
return this;
}
/**
* Prevents the reordering of the component that was added to the
* workbench most recently.
*/
public preventReorder(): this {
this._builder.preventReorder();
return this;
}
/**
* Sets the identifier of the component that was added to the workbench
* most recently.
*/
public setId(value: string | string[]): this {
this._builder.setId(value);
return this;
}
/**
* Sets whether the component that was added to the workbench most recently
* is closable or not.
*/
public setClosable(value = true): this {
this._builder.setClosable(value);
return this;
}
/**
* Sets the relative height of the component that was added to the workbench
* most recently, in percentage, compared to its siblings in the same panel.
*/
public setRelativeHeight(value: number): this {
this._builder.setRelativeHeight(value);
return this;
}
/**
* Sets the relative width of the component that was added to the workbench
* most recently, in percentage, compared to its siblings in the same panel.
*/
public setRelativeWidth(value: number): this {
this._builder.setRelativeWidth(value);
return this;
}
/**
* Sets whether the component that was added to the workbench most recently
* can be rearranged on the workbench by dragging it around.
*/
public setReorderEnabled(value = true): this {
this._builder.setReorderEnabled(value);
return this;
}
/**
* Sets the title of the component that was added to the workbench
* most recently.
*/
public setTitle(value: string): this {
this._builder.setTitle(value);
return this;
}
/**
* Sets multiple properties of the last component that was added to the
* workbench.
*/
public setProperties(props: Partial): this {
this._builder.setProperties(props);
return this;
}
/**
* Specifies whether panel headers should be shown in the workbench.
*/
public showHeaders(value = true): this {
this._settings.hasHeaders = value;
return this;
}
/**
* Specifies whether maximise icons should be shown in the workbench.
*/
public showMaximiseIcon(value = true): this {
this._settings.showMaximiseIcon = value;
return this;
}
public register(factory: ComponentConstructor): this;
public register(name: string, factory: ComponentConstructor): this;
public register(nameOrFactory: string | ComponentConstructor,
maybeFactory?: ComponentConstructor): this {
const { registry } = this._assertHasWorkbench();
registry.register.apply(registry, arguments);
return this;
}
public registerComponent(component: React.ComponentType): this;
public registerComponent(name: string, component: React.ComponentType): this;
public registerComponent(
nameOrComponent: string | React.ComponentType,
maybeComponent?: React.ComponentType
): any {
const { registry } = this._assertHasWorkbench();
registry.registerComponent.apply(registry, arguments);
return this;
}
private _assertHasWorkbench(): Workbench {
if (this._workbench === undefined) {
throw new Error("builder has already built a workbench");
}
return this._workbench;
}
}