golden-layout
Version:
A multi-screen javascript Layout manager
260 lines (224 loc) • 10 kB
text/typescript
import { UnexpectedNullError, UnexpectedUndefinedError } from '../errors/internal-error';
import { ComponentItem } from '../items/component-item';
import { ContentItem } from '../items/content-item';
import { Stack } from '../items/stack';
import { LayoutManager } from '../layout-manager';
import { DomConstants } from '../utils/dom-constants';
import { DragListener } from '../utils/drag-listener';
import { EventEmitter } from '../utils/event-emitter';
import { Side } from '../utils/types';
import {
numberToPixels
} from '../utils/utils';
/**
* This class creates a temporary container
* for the component whilst it is being dragged
* and handles drag events
* @internal
*/
export class DragProxy extends EventEmitter {
private _area: ContentItem.Area | null = null;
private _lastValidArea: ContentItem.Area | null = null;
private _minX: number;
private _minY: number;
private _maxX: number;
private _maxY: number;
private _sided: boolean;
private _element: HTMLElement;
private _proxyContainerElement: HTMLElement;
private _componentItemFocused: boolean;
get element(): HTMLElement { return this._element; }
/**
* @param x - The initial x position
* @param y - The initial y position
* @internal
*/
constructor(x: number, y: number,
private readonly _dragListener: DragListener,
private readonly _layoutManager: LayoutManager,
private readonly _componentItem: ComponentItem,
private readonly _originalParent: ContentItem) {
super();
this._dragListener.on('drag', (offsetX, offsetY, event) => this.onDrag(offsetX, offsetY, event));
this._dragListener.on('dragStop', () => this.onDrop());
this.createDragProxyElements(x, y);
if (this._componentItem.parent === null) {
// Note that _contentItem will have dummy GroundItem as parent if initiated by a external drag source
throw new UnexpectedNullError('DPC10097');
}
this._componentItemFocused = this._componentItem.focused;
if (this._componentItemFocused) {
this._componentItem.blur();
}
this._componentItem.parent.removeChild(this._componentItem, true);
this.setDimensions();
document.body.appendChild(this._element);
this.determineMinMaxXY();
this._layoutManager.calculateItemAreas();
this.setDropPosition(x, y);
}
/** Create Stack-like structure to contain the dragged component */
private createDragProxyElements(initialX: number, initialY: number): void {
this._element = document.createElement('div');
this._element.classList.add(DomConstants.ClassName.DragProxy);
const headerElement = document.createElement('div');
headerElement.classList.add(DomConstants.ClassName.Header);
const tabsElement = document.createElement('div');
tabsElement.classList.add(DomConstants.ClassName.Tabs);
const tabElement = document.createElement('div');
tabElement.classList.add(DomConstants.ClassName.Tab);
const titleElement = document.createElement('span');
titleElement.classList.add(DomConstants.ClassName.Title);
tabElement.appendChild(titleElement);
tabsElement.appendChild(tabElement);
headerElement.appendChild(tabsElement);
this._proxyContainerElement = document.createElement('div');
this._proxyContainerElement.classList.add(DomConstants.ClassName.Content);
this._element.appendChild(headerElement);
this._element.appendChild(this._proxyContainerElement);
if (this._originalParent instanceof Stack && this._originalParent.headerShow) {
this._sided = this._originalParent.headerLeftRightSided;
this._element.classList.add('lm_' + this._originalParent.headerSide);
if ([Side.right, Side.bottom].indexOf(this._originalParent.headerSide) >= 0) {
this._proxyContainerElement.insertAdjacentElement('afterend', headerElement);
}
}
this._element.style.left = numberToPixels(initialX);
this._element.style.top = numberToPixels(initialY);
tabElement.setAttribute('title', this._componentItem.title);
titleElement.insertAdjacentText('afterbegin', this._componentItem.title);
this._proxyContainerElement.appendChild(this._componentItem.element);
}
private determineMinMaxXY(): void {
const groundItem = this._layoutManager.groundItem;
if (groundItem === undefined) {
throw new UnexpectedUndefinedError('DPDMMXY73109');
} else {
const groundElement = groundItem.element;
const rect = groundElement.getBoundingClientRect();
this._minX = rect.left + document.body.scrollLeft;
this._minY = rect.top + document.body.scrollTop;
this._maxX = this._minX + rect.width;
this._maxY = this._minY + rect.height;
}
}
/**
* Callback on every mouseMove event during a drag. Determines if the drag is
* still within the valid drag area and calls the layoutManager to highlight the
* current drop area
*
* @param offsetX - The difference from the original x position in px
* @param offsetY - The difference from the original y position in px
* @param event -
* @internal
*/
private onDrag(offsetX: number, offsetY: number, event: PointerEvent) {
const x = event.pageX;
const y = event.pageY;
this.setDropPosition(x, y);
this._componentItem.drag();
}
/**
* Sets the target position, highlighting the appropriate area
*
* @param x - The x position in px
* @param y - The y position in px
*
* @internal
*/
private setDropPosition(x: number, y: number): void {
if (this._layoutManager.layoutConfig.settings.constrainDragToContainer) {
if (x <= this._minX) {
x = Math.ceil(this._minX);
} else if (x >= this._maxX) {
x = Math.floor(this._maxX);
}
if (y <= this._minY) {
y = Math.ceil(this._minY);
} else if (y >= this._maxY) {
y = Math.floor(this._maxY);
}
}
this._element.style.left = numberToPixels(x);
this._element.style.top = numberToPixels(y);
this._area = this._layoutManager.getArea(x, y);
if (this._area !== null) {
this._lastValidArea = this._area;
this._area.contentItem.highlightDropZone(x, y, this._area);
}
}
/**
* Callback when the drag has finished. Determines the drop area
* and adds the child to it
* @internal
*/
private onDrop(): void {
const dropTargetIndicator = this._layoutManager.dropTargetIndicator;
if (dropTargetIndicator === null) {
throw new UnexpectedNullError('DPOD30011');
} else {
dropTargetIndicator.hide();
}
this._componentItem.exitDragMode();
/*
* Valid drop area found
*/
let droppedComponentItem: ComponentItem | undefined;
if (this._area !== null) {
droppedComponentItem = this._componentItem;
this._area.contentItem.onDrop(droppedComponentItem, this._area);
/**
* No valid drop area available at present, but one has been found before.
* Use it
*/
} else if (this._lastValidArea !== null) {
droppedComponentItem = this._componentItem;
const newParentContentItem = this._lastValidArea.contentItem;
newParentContentItem.onDrop(droppedComponentItem, this._lastValidArea);
/**
* No valid drop area found during the duration of the drag. Return
* content item to its original position if a original parent is provided.
* (Which is not the case if the drag had been initiated by createDragSource)
*/
} else if (this._originalParent) {
droppedComponentItem = this._componentItem;
this._originalParent.addChild(droppedComponentItem);
/**
* The drag didn't ultimately end up with adding the content item to
* any container. In order to ensure clean up happens, destroy the
* content item.
*/
} else {
this._componentItem.destroy(); // contentItem children are now destroyed as well
}
this._element.remove();
this._layoutManager.emit('itemDropped', this._componentItem);
if (this._componentItemFocused && droppedComponentItem !== undefined) {
droppedComponentItem.focus();
}
}
/**
* Updates the Drag Proxy's dimensions
* @internal
*/
private setDimensions() {
const dimensions = this._layoutManager.layoutConfig.dimensions;
if (dimensions === undefined) {
throw new Error('DragProxy.setDimensions: dimensions undefined');
}
let width = dimensions.dragProxyWidth;
let height = dimensions.dragProxyHeight;
if (width === undefined || height === undefined) {
throw new Error('DragProxy.setDimensions: width and/or height undefined');
}
const headerHeight = this._layoutManager.layoutConfig.header.show === false ? 0 : dimensions.headerHeight;
this._element.style.width = numberToPixels(width);
this._element.style.height = numberToPixels(height)
width -= (this._sided ? headerHeight : 0);
height -= (!this._sided ? headerHeight : 0);
this._proxyContainerElement.style.width = numberToPixels(width);
this._proxyContainerElement.style.height = numberToPixels(height);
this._componentItem.enterDragMode(width, height);
this._componentItem.show();
}
}