Home Manual Reference Source Repository

docs/stack/StackTab.js

import { VNode } from 'snabbdom/vnode';
import h from 'snabbdom/h';

import { Inject, Injector } from '../di'
import { 
  Subject, 
  Observable, 
  Cancellable,
  BeforeDestroyEvent
} from '../events';
import { TabSelectionEvent } from './TabSelectionEvent';
import { Renderable, ConfiguredRenderable, MemoizeFrom, RenderableConfig } from '../dom';
import { Draggable } from '../Draggable';
import { ContainerRef, ConfigurationRef, DocumentRef, DragEvent, DragStatus } from '../common';
import { StackHeader } from './StackHeader';
import { TabCloseEvent } from './TabCloseEvent';
import { TabDragEvent } from './TabDragEvent';
import { Stack } from './Stack';
import { StackItemContainer } from './StackItemContainer';
import { DragHost } from '../DragHost';
import { get } from '../utils';
import { TabControl } from './tabControls';
import { LockState, LOCK_DRAGGING } from '../LockState';

export interface StackTabConfig extends RenderableConfig {
  maxSize: number;
  title: string;
}

export type StackTabConfigArgs = {
  [P in keyof StackTabConfig]?: StackTabConfig[P];
}

/**
 * Renderable representing a stack tab.
 * @export
 * @class StackTab
 * @extends {Renderable}
 */
export class StackTab extends Renderable {
  private _element: HTMLElement;
  private _isDragging: boolean = false;
  protected _container: StackHeader;
  
  /**
   * Creates an instance of StackTab.
   * @param {StackTabConfig} _config 
   * @param {Draggable<StackTab>} _draggable 
   * @param {DragHost} _dragHost 
   * @param {Document} _document 
   */
  constructor(
    @Inject(ConfigurationRef) private _config: StackTabConfig,
    @Inject(Draggable) private _draggable: Draggable<StackTab>,
    @Inject(DragHost) private _dragHost: DragHost,
    @Inject(DocumentRef) private _document: Document,
    @Inject(LockState) private _lockState: LockState
  ) {
    super();
    
    this._config = Object.assign({
      maxSize: 150,
      title: ''   
    }, this._config || {});
  }
  
  get width(): number {
    return this._container.isHorizontal ? this._width : this._container.width;
  }

  get height(): number {
    return this._container.isHorizontal ? this._container.height : this._height;
  }

  /**
   * The HTML element for this tab.
   * @readonly
   * @type {HTMLElement}
   */
  get element(): HTMLElement {
    return this._element;
  }

  /**
   * The StackItemContainer associated with this tab.
   * @see {@link StackItemContainer}
   * @readonly
   * @type {(StackItemContainer|null)}
   */
  get item(): StackItemContainer|null {
    return this._container.getItemFromTab(this);
  }

  get offsetX(): number {
    return this._container.getOffsetXForTab(this);
  }
  
  get offsetY(): number {
    return this._container.getOffsetYForTab(this);
  }

  /**
   * Whether this tab is dragging.
   * @readonly
   * @type {boolean}
   */
  get isDragging(): boolean {
    return this._isDragging;
  }

  /**
   * The stack that this tab belongs to.
   * @readonly
   * @type {(Stack|null)}
   */
  get stack(): Stack|null {
    return (this._container ? this._container.container : null) as Stack|null;
  }

  /**
   * The list of tab controls.
   * @readonly
   * @type {TabControl[]}
   */
  get controls(): TabControl[] {
    const { item } = this;
    
    return item ? item.controls : [];
  }

  get isDraggable(): boolean {
    const { item } = this

    if (this._lockState.get(LOCK_DRAGGING) || !item || !item.draggable) {
      return false;
    }

    return true;
  }

  private get _resizeHashId(): string {
    return [
      get(this.item, 'title'),
      this._element ? 1 : 0,
      this.controls.map(c => c.isActive() ? c.uid : '').join('')
    ].join(':');
  }

  initialize(): void {
    super.initialize();
    
    this._draggable.drag
      .filter(Draggable.isDraggingEvent)
      .subscribe(this._onDragMove.bind(this));
    
    this._draggable.drag
      .filter(Draggable.isDragStopEvent)
      .subscribe(this._onDragStop.bind(this));
      
    this._draggable.drag
      .filter(Draggable.isDragStartEvent)
      .subscribe(this._onDragStart.bind(this));

    this._dragHost.start
      .takeUntil(this.destroyed)
      .subscribe(this._onDragHostStart.bind(this));
    
    this._dragHost.dropped
      .takeUntil(this.destroyed)
      .subscribe(this._onDragHostDropped.bind(this));
  }

  @MemoizeFrom('_resizeHashId')
  resize(): void {
    if (this._element) {
      const rect = this._element.getBoundingClientRect();

      this._width = rect.width;
      this._height = rect.height;
    }
  }
  
  render(): VNode {
    const { item } = this;
    
    return h(`div.ug-layout__stack-tab`, {
      key: this.uid,
      style: this._getStyles(),
      class: {
        'ug-layout__stack-tab-active': this._container.isTabActive(this),
        'ug-layout__stack-tab-distributed': this._container.isDistributed,
        'ug-layout__stack-tab-x': this._container.isHorizontal,
        'ug-layout__stack-tab-y': !this._container.isHorizontal,
        'ug-layout__stack-tab-draggable': this.isDraggable
      },
      hook: {
        create: (oldNode, newNode) => this._element = newNode.elm as HTMLElement,
        insert: () => this.resize()
      },
      on: {
        mousedown: e => this._onMouseDown(e),
        click: () => this._onClick()
      }
    }, [
      h('div', get(item, 'title', '')),
      h('div.ug-layout__stack-tab-controls', this.controls.filter(c => c.isActive()).map(c => c.render()))
    ]);
  }  

  destroy(): void {
    this._draggable.destroy();
    super.destroy();
  }

  private _getStyles(): { [key: string]: string } {
    let result = {};

    if (this._container.isHorizontal) {
      if (!this._container.isDistributed) {
        result['max-width'] = `${this._config.maxSize}px`;
      }
      
      result['max-height'] = `${this._container.height}px`;
      result['height'] = `${this.height}px`;
    } else {
      if (!this._container.isDistributed) {
        result['max-height'] = `${this._config.maxSize}px`;
      }
      
      result['max-width'] = `${this._container.width}px`;
      result['width'] = `${this.width}px`;
    }
    
    return result;
  }

  private _onMouseDown(e: MouseEvent): void {
    const { item } = this;
    
    if (!this.isDraggable) {
      return;  
    }
    
    this._draggable.startDrag({
      host: this, 
      startX: e.pageX,
      startY: e.pageY
    });
  }

  private _onDragStart(e: DragEvent<StackTab>): void {
    const item = this._container.getItemFromTab(this);
    const originStack = this.stack;
    const originIndex = this._container.getIndexOf(this);
    
    this._isDragging = true;
    this.emit(new TabDragEvent(this));
    this._element.classList.add('ug-layout__tab-dragging');
    this._document.body.appendChild(this._element);
    
    if (item) {
      this._dragHost.initialize({
        item: <Renderable>item, 
        draggable: this._draggable, 
        dragArea: this.getArea()
      });
      
      this._dragHost.fail
        .takeUntil(this._dragHost.dropped)
        .subscribe(() => {
          if (originStack) {
            originStack.addChild(item, { index: originIndex });
          }
        });
        
      this._dragHost.dropped
        .first()
        .subscribe(() => this.destroy());
    } 
  }

  private _onDragMove(e: DragEvent<StackTab>): void {
    const { bounds } = this._dragHost;

    let x = e.pageX - (this.width / 2);
    let y = e.pageY - (this.height / 2);

    if (bounds) {
      x = bounds.clampX(x);
      y = bounds.clampY(y);
    }
    
    this._element.style.transform = `translateX(${x}px) translateY(${y}px)`;
  }
  
  private _onDragStop(e: DragEvent<StackTab>): void {
    this._isDragging = false;
    this._element.style.transform = `translateX(0px) translateY(0px)`;
    this._element.classList.remove('ug-layout__tab-dragging');
    this._document.body.removeChild(this._element);
  }

  private _onDragHostStart(): void {
    this._element.classList.add('ug-layout__tab-drag-enabled');
  }
  
  private _onDragHostDropped(): void {
    this._element.classList.remove('ug-layout__tab-drag-enabled');
  }

  private _onClick(): void {
    this.emit(new TabSelectionEvent(this));
  }
}