docs/stack/StackHeader.js
import { VNode } from 'snabbdom/vnode';
import h from 'snabbdom/h';
import { Inject, Injector } from '../di';
import {
Renderable,
RenderableInjector,
ConfiguredRenderable,
RenderableArea,
AddChildArgs,
RemoveChildArgs,
RenderableConfig
} from '../dom';
import { Stack } from './Stack';
import { Draggable } from '../Draggable';
import { DragHost } from '../DragHost';
import { StackTab, StackTabConfigArgs } from './StackTab';
import { TabCloseEvent } from './TabCloseEvent';
import { TabSelectionEvent } from './TabSelectionEvent';
import { TabDragEvent } from './TabDragEvent';
import { get, isNumber, partition, propEq } from '../utils';
import {
ConfigurationRef,
ContainerRef,
DropTarget,
DropArea,
HighlightCoordinateArgs,
DragEvent,
RenderableArg
} from '../common';
import { Subject, Observable, BeforeDestroyEvent } from '../events';
import { StackControl, StackControlPosition } from './controls';
import { StackItemContainer } from './StackItemContainer';
export interface StackHeaderConfig extends RenderableConfig {
size: number;
distribute: boolean;
droppable: boolean;
}
export type StackHeaderConfigArgs = {
[P in keyof StackHeaderConfig]?: StackHeaderConfig[P];
}
export const DEFAULT_STACK_HEADER_SIZE = 25;
export class StackHeader extends Renderable implements DropTarget {
protected _contentItems: StackTab[] = [];
private _controls: StackControl[] = [];
private _config: StackHeaderConfig;
private _tabAreas: RenderableArea[];
constructor(
@Inject(ConfigurationRef) _config: StackHeaderConfigArgs|null,
@Inject(ContainerRef) protected _container: Stack,
@Inject(DragHost) protected _dragHost: DragHost
) {
super();
this._config = Object.assign({
controls: [],
size: DEFAULT_STACK_HEADER_SIZE,
droppable: true,
distribute: false
}, _config);
}
get width(): number {
return this._container.isHorizontal ? this._container.width : this._config.size;
}
get height(): number {
return this._container.isHorizontal ? this._config.size : this._container.height;
}
get size(): number {
return get(this._config, 'size', DEFAULT_STACK_HEADER_SIZE);
}
get droppable(): boolean {
return get(this._config, 'droppable', true);
}
get isHorizontal(): boolean {
return this._container.isHorizontal;
}
get isDistributed(): boolean {
return get(this._config, 'distribute', false);
}
get controls(): StackControl[] {
return this._controls.slice(0);
}
initialize(): void {
super.initialize();
this._dragHost.start
.takeUntil(this.destroyed)
.subscribe(this._onDragHostStart.bind(this));
this._dragHost.dropped
.takeUntil(this.destroyed)
.subscribe(this._onDragHostDropped.bind(this));
}
addTab(config: StackTabConfigArgs, options: AddChildArgs = {}): StackTab {
const tab = this.createChild(new ConfiguredRenderable(StackTab, config), [ Draggable ]);
tab.subscribe(TabSelectionEvent, e => this.emit(e));
tab.subscribe(TabCloseEvent, e => this.emit(e));
tab.subscribe(TabDragEvent, e => this.emit(e));
this.addChild(tab, options);
return tab;
}
addControl(_control: RenderableArg<StackControl>): void {
this._controls.push(this.createChild(_control));
}
isTabActive(tab: StackTab): boolean {
return this._container.isActiveTab(tab);
}
getItemFromTab(tab: StackTab): StackItemContainer|null {
return this._container.getAtIndex(this.getIndexOf(tab)) as StackItemContainer|null;
}
render(): VNode {
const [ postTabControls, preTabControls ] = partition(
this._controls.filter(c => c.isActive()),
propEq('position', StackControlPosition.POST_TAB)
);
return h('div.ug-layout__stack-header', {
style: {
height: `${this.height}px`,
width: `${this.width}px`
}
},
[
h('div.ug-layout__stack-controls', preTabControls.map(c => c.render())),
h('div.ug-layout__tab-container', this._contentItems.map(tab => tab.render())),
h('div.ug-layout__stack-controls', postTabControls.map(c => c.render()))
]
);
}
isDroppable(): boolean {
return this.droppable;
}
getChildren(): Renderable[] {
return [
...this._controls,
...this._contentItems
];
}
handleDrop(item: Renderable, dropArea: DropArea, e: DragEvent<Renderable>): void {
const index = this._getIndexFromArea(e.pageX, e.pageY, dropArea.area) + 1;
if (item instanceof StackItemContainer && this._container.getIndexOf(item) === -1) {
this._container.addChild(item, { index });
this._container.setActiveIndex(index);
}
this.onDropHighlightExit();
}
getHighlightCoordinates(args: HighlightCoordinateArgs): RenderableArea {
let { pageX, pageY, dragArea, dropArea: { item, area: { x, x2, y, y2, height } } } = args;
const highlightArea = new RenderableArea(x, dragArea.width + x, y, y2);
let leftMostTabIndex = this._getIndexFromArea(pageX, pageY, args.dropArea.area);
let leftMostTabArea = this._tabAreas[leftMostTabIndex];
if (leftMostTabArea) {
highlightArea.x = leftMostTabArea.x2;
highlightArea.x2 = leftMostTabArea.x2 + dragArea.width;
}
for (const [ index, tab ] of this._contentItems.entries()) {
if (!tab.isDragging) {
tab.element.style.transform = index > leftMostTabIndex ? `translateX(${dragArea.width}px)` : 'translateX(0px)';
}
}
return highlightArea;
}
onDropHighlightExit(): void {
for (const tab of this._contentItems) {
if (!tab.isDragging) {
tab.element.style.transform = 'translateX(0px)';
}
}
}
getOffsetXForTab(tab: StackTab): number {
if (this.isHorizontal) {
return this._contentItems.slice(0, this.getIndexOf(tab)).reduce((result, tab) => result + tab.width, this.offsetX);
}
return this.offsetX;
}
getOffsetYForTab(tab: StackTab): number {
if (!this.isHorizontal) {
return this._contentItems.slice(0, this.getIndexOf(tab)).reduce((result, tab) => result + tab.height, this.offsetY);
}
return this.offsetY;
}
private _onDragHostStart(): void {
this._tabAreas = this._contentItems.map(tab => tab.getArea());
}
private _onDragHostDropped(): void {
this._tabAreas = [];
}
private _getIndexFromArea(pageX: number, pageY: number, area: RenderableArea): number {
let { x, y } = area;
const deltaX = pageX - x;
const deltaY = pageY - y;
let result = -1;
for (const [ index, tabArea ] of this._tabAreas.entries()) {
if (deltaX >= (tabArea.x - x) + (tabArea.width / 2)) {
result = index;
}
}
return result;
}
}