@highcharts/dashboards
Version:
Highcharts Dashboards framework
494 lines (493 loc) • 19.4 kB
JavaScript
/* *
*
* (c) 2009-2025 Highsoft AS
*
* License: www.highcharts.com/license
*
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
*
* Authors:
* - Sebastian Bochan
* - Wojciech Chmiel
* - Gøran Slettemark
* - Sophie Bremer
*
* */
import U from '../../Core/Utilities.js';
import Globals from '../Globals.js';
import EditGlobals from '../EditMode/EditGlobals.js';
import GUIElement from '../Layout/GUIElement.js';
import ContextDetection from './ContextDetection.js';
const { addEvent, merge, css, fireEvent, createElement } = U;
/**
* Class providing a drag and drop functionality.
* @internal
*/
class DragDrop {
/* *
*
* Constructors
*
* */
/**
* Constructor for the DragDrop class.
* @internal
*
* @param {EditMode} editMode
* The parent editMode reference.
*
* @param {DragDrop.Options} options
* Options for the DragDrop.
*/
constructor(editMode, options) {
this.editMode = editMode;
this.options = merge(DragDrop.defaultOptions, options);
this.mockElement = createElement('div', { className: EditGlobals.classNames.dragMock }, {}, editMode.board.container);
this.dropPointer = {
isVisible: false,
align: '',
element: createElement('div', { className: EditGlobals.classNames.dropPointer }, {}, editMode.board.container)
};
this.isActive = false;
this.initEvents();
}
/* *
*
* Functions
*
* */
/**
* Method for showing and positioning drop pointer.
*
* @param {number} left
* Drop pointer left position.
*
* @param {number} top
* Drop pointer top position.
*
* @param {number} width
* Drop pointer width.
*
* @param {number} height
* Drop pointer height.
*/
showDropPointer(left, top, width, height) {
this.dropPointer.isVisible = true;
css(this.dropPointer.element, {
display: 'block',
left: left + 'px',
top: top + 'px',
height: height + 'px',
width: width + 'px'
});
}
/**
* Method for hiding drop pointer.
*/
hideDropPointer() {
if (this.dropPointer.isVisible) {
this.dropPointer.isVisible = false;
this.dropPointer.align = '';
this.dropPointer.element.style.display = 'none';
}
}
/**
* Method for positioning drag mock element.
*
* @param {PointerEvent} mouseEvent
* Mouse event.
*/
setMockElementPosition(mouseEvent) {
const dragDrop = this, dashBoundingRect = dragDrop.editMode.board.container.getBoundingClientRect(), offset = dragDrop.mockElement.clientWidth / 2, x = mouseEvent.clientX - dashBoundingRect.left - offset, y = mouseEvent.clientY - dashBoundingRect.top - offset;
css(this.mockElement, { left: x + 'px', top: y + 'px' });
}
/**
* Method for initializing drag drop events.
*/
initEvents() {
const dragDrop = this;
// DragDrop events.
addEvent(document, 'mousemove', dragDrop.onDrag.bind(dragDrop));
addEvent(document, 'mouseup', dragDrop.onDragEnd.bind(dragDrop));
}
/**
* General method used on drag start.
*
* @param {PointerEvent} e
* Mouse event.
*
* @param {Cell|Row} context
* Reference to the dragged context.
*
* @param {Function} dragEndCallback
* Callback invoked on drag end.
*/
onDragStart(e, context, dragEndCallback) {
this.isActive = true;
this.editMode.hideToolbars(['cell', 'row']);
if (this.editMode.resizer) {
this.editMode.resizer.disableResizer();
}
this.setMockElementPosition(e);
if (context) {
this.context = context;
context.hide();
if (context.getType() === Globals.guiElementType.cell) {
const draggedCell = context;
// Call cellResize board event.
fireEvent(this.editMode.board, 'cellResize', { cell: context });
fireEvent(draggedCell.row, 'cellChange', { cell: context, row: draggedCell.row });
}
}
else if (dragEndCallback) {
this.dragEndCallback = dragEndCallback;
}
css(this.mockElement, {
cursor: 'grabbing',
display: 'block'
});
}
/**
* General method used while dragging.
*
* @param {PointerEvent} e
* Mouse event.
*/
onDrag(e) {
const dragDrop = this;
if (dragDrop.isActive) {
e.preventDefault();
dragDrop.setMockElementPosition(e);
if (dragDrop.context) {
if (dragDrop.context.getType() ===
Globals.guiElementType.cell) {
dragDrop.onCellDrag(e);
}
else if (dragDrop.context.getType() ===
Globals.guiElementType.row) {
dragDrop.onRowDrag(e);
}
}
else if (dragDrop.dragEndCallback) {
dragDrop.onCellDrag(e);
}
}
}
/**
* General method used when drag finish.
*/
onDragEnd() {
const dragDrop = this;
if (dragDrop.isActive) {
dragDrop.isActive = false;
css(dragDrop.mockElement, {
cursor: 'grab',
display: 'none'
});
if (dragDrop.context) {
if (dragDrop.context.getType() ===
Globals.guiElementType.cell) {
dragDrop.onCellDragEnd();
}
else if (dragDrop.context.getType() ===
Globals.guiElementType.row) {
dragDrop.onRowDragEnd();
}
dragDrop.context = void 0;
// Show toolbars and snaps.
if (dragDrop.editMode.editCellContext) {
dragDrop.editMode.showToolbars(['row', 'cell'], dragDrop.editMode.editCellContext);
if (dragDrop.editMode.resizer) {
dragDrop.editMode.resizer.setSnapPositions(dragDrop.editMode.editCellContext);
}
}
}
else if (dragDrop.dragEndCallback) {
dragDrop.dragEndCallback(dragDrop.dropContext);
dragDrop.dragEndCallback = void 0;
dragDrop.hideDropPointer();
}
}
}
/**
* Sets appropriate drop context and refresh drop pointer position when
* row is dragged or cell is dragged as a row.
*
* @param {PointerEvent} e
* Mouse event.
*
* @param {ContextDetection.ContextDetails} contextDetails
* Context details (cell, side)
*/
onRowDrag(e, contextDetails) {
const dragDrop = this, mouseCellContext = dragDrop.mouseCellContext, dropPointerSize = dragDrop.options.dropPointerSize || 0, offset = dragDrop.options.rowDropOffset || 0;
let updateDropPointer = false;
if (mouseCellContext) {
const context = (contextDetails ||
ContextDetection.getContext(mouseCellContext, e, offset));
const align = context.side;
if (dragDrop.dropPointer.align !== align ||
dragDrop.dropContext !== context.cell.row) {
updateDropPointer = true;
dragDrop.dropPointer.align = align;
dragDrop.dropContext = context.cell.row;
}
if (align) {
const dropContextRowOffsets = GUIElement.getOffsets(dragDrop.dropContext, dragDrop.editMode.board.container);
const { width, height } = GUIElement
.getDimFromOffsets(dropContextRowOffsets);
// Update or show drop pointer.
if (!dragDrop.dropPointer.isVisible || updateDropPointer) {
dragDrop.showDropPointer(dropContextRowOffsets.left, dropContextRowOffsets.top + (dragDrop.dropPointer.align === 'bottom' ?
height :
0) - dropPointerSize / 2, width, dropPointerSize);
}
}
else {
dragDrop.dropContext = void 0;
dragDrop.hideDropPointer();
}
}
}
/**
* Unmounts dropped row and mounts it in a new position.
*
* @fires DragDrop#layoutChanged
*/
onRowDragEnd() {
const dragDrop = this, draggedRow = dragDrop.context, dropContext = dragDrop.dropContext;
if (dragDrop.dropPointer.align) {
draggedRow.layout.unmountRow(draggedRow);
// Destroy layout when empty.
if (draggedRow.layout.rows.length === 0) {
draggedRow.layout.destroy();
}
dropContext.layout.mountRow(draggedRow, (dropContext.layout.getRowIndex(dropContext) || 0) +
(dragDrop.dropPointer.align === 'bottom' ? 1 : 0));
// Call cellResize board event.
if (draggedRow.cells[0]) {
fireEvent(dragDrop.editMode.board, 'cellResize', { cell: draggedRow.cells[0] });
fireEvent(draggedRow, 'cellChange', { cell: draggedRow.cells[0], row: draggedRow });
}
}
dragDrop.hideDropPointer();
draggedRow.show();
fireEvent(dragDrop.editMode, 'layoutChanged', {
type: 'rowDragEnd',
target: draggedRow,
board: dragDrop.editMode.board
});
}
/**
* Method used as middleware when cell is dragged.
* Decides where to pass an event depending on the mouse context.
*
* @param {PointerEvent} e
* Mouse event.
*
* @param {ContextDetection.ContextDetails} contextDetails
* Context details (cell, side)
*/
onCellDrag(e, contextDetails) {
const dragDrop = this, mouseCellContext = dragDrop.mouseCellContext, offset = dragDrop.options.cellDropOffset || 0;
if (mouseCellContext || contextDetails) {
dragDrop.onCellDragCellCtx(e, contextDetails ||
ContextDetection.getContext(mouseCellContext, e, offset));
}
else if (dragDrop.mouseRowContext) {
dragDrop.onCellDragRowCtx(e, dragDrop.mouseRowContext);
}
}
/**
* Sets appropriate drop context and refreshes the drop pointer
* position when a cell is dragged and a cell context is detected.
*
* @param {PointerEvent} e
* Mouse event.
*
* @param {ContextDetection.ContextDetails} context
* Context details (cell, side)
*/
onCellDragCellCtx(e, context) {
const dragDrop = this, dropPointerSize = dragDrop.options.dropPointerSize || 0, align = context.side;
let updateDropPointer = false;
if (dragDrop.dropPointer.align !== align ||
dragDrop.dropContext !== context.cell) {
updateDropPointer = true;
dragDrop.dropPointer.align = align;
dragDrop.dropContext = context.cell;
}
if (align === 'right' || align === 'left') {
const dropContextOffsets = GUIElement.getOffsets(dragDrop.dropContext, dragDrop.editMode.board.container);
const { width, height } = GUIElement.getDimFromOffsets(dropContextOffsets);
// Update or show drop pointer.
if (!dragDrop.dropPointer.isVisible || updateDropPointer) {
const rowLevelInfo = dragDrop.dropContext.row.getRowLevelInfo(e.clientY), pointerHeight = (rowLevelInfo ?
(rowLevelInfo.rowLevel.bottom -
rowLevelInfo.rowLevel.top) :
height);
dragDrop.showDropPointer(dropContextOffsets.left + (align === 'right' ? width : 0) -
dropPointerSize / 2, dropContextOffsets.top, dropPointerSize, pointerHeight);
}
}
else if (align === 'top' || align === 'bottom') {
const dropContextOffsets = GUIElement.getOffsets(dragDrop.dropContext), rowLevelInfo = dragDrop.dropContext.row
.getRowLevelInfo(dropContextOffsets.top);
if (rowLevelInfo &&
((rowLevelInfo.index === 0 && align === 'top') ||
(rowLevelInfo.index ===
rowLevelInfo.rowLevels.length - 1 &&
align === 'bottom'))) {
// Checks if a cell is dragged as a row
// (only when a cell edge is on a row edge)
dragDrop.onRowDrag(e, context);
}
}
else {
dragDrop.dropContext = void 0;
dragDrop.hideDropPointer();
}
}
/**
* Sets appropriate drop context and refreshes the drop pointer
* position when a cell is dragged and a row context is detected.
*
* @param {PointerEvent} e
* Mouse event.
*
* @param {Row} mouseRowContext
* Row context.
*/
onCellDragRowCtx(e, mouseRowContext) {
const dragDrop = this, dropPointerSize = dragDrop.options.dropPointerSize || 0, rowOffsets = GUIElement.getOffsets(mouseRowContext), rowLevelInfo = mouseRowContext.getRowLevelInfo(e.clientY);
let cell, cellOffsets;
if (rowLevelInfo) {
for (let i = 0, iEnd = rowLevelInfo.rowLevel.cells.length; i < iEnd; ++i) {
cell = rowLevelInfo.rowLevel.cells[i];
cellOffsets = GUIElement.getOffsets(cell);
const { width, height } = GUIElement
.getDimFromOffsets(cellOffsets), dashOffsets = dragDrop.editMode.board.container
.getBoundingClientRect(), levelHeight = (rowLevelInfo.rowLevel.bottom -
rowLevelInfo.rowLevel.top);
if (cell.isVisible) {
if (height < 0.8 * levelHeight &&
cellOffsets.left <= e.clientX &&
cellOffsets.right >= e.clientX) {
if (cellOffsets.top > e.clientY) {
// @ToDo - Mouse above the cell.
}
else if (cellOffsets.bottom < e.clientY) {
// Mouse below the cell.
dragDrop.showDropPointer(cellOffsets.left - dashOffsets.left, cellOffsets.top - dashOffsets.top + height, width, levelHeight - height);
dragDrop.dropPointer.align = 'nestedBottom';
dragDrop.dropContext = cell;
}
i = iEnd; // Stop the loop
}
else if ((i === 0 && cellOffsets.left > e.clientX) ||
(i === iEnd - 1 && cellOffsets.right < e.clientX)) {
if (cellOffsets.left > e.clientX) {
// @ToDo - Mouse on the cell left side.
}
else if (cellOffsets.right < e.clientX) {
// Mouse on the cell right side.
const pointerWidth = rowOffsets.right - cellOffsets.right;
dragDrop.showDropPointer(cellOffsets.left + ((i === 0 && cellOffsets.left > e.clientX) ?
0 :
width) - dropPointerSize / 2 - dashOffsets.left, cellOffsets.top - dashOffsets.top, pointerWidth > dropPointerSize ?
pointerWidth :
dropPointerSize, levelHeight || height);
dragDrop.dropPointer.align = 'right';
dragDrop.dropContext = cell;
}
i = iEnd; // Stop the loop
}
}
else if (!cell.isVisible && cell === dragDrop.context) {
// Element is not visible.
dragDrop.dropContext = void 0;
dragDrop.hideDropPointer();
}
}
}
}
/**
* Unmounts dropped cell and mounts it in a new position.
* When cell is dragged as a row also creates a new row
* and mounts cell there.
*
* @param {Cell} contextCell
* Cell used as a dragDrop context.
*
* @fires DragDrop#layoutChanged
*/
onCellDragEnd(contextCell) {
const dragDrop = this, draggedCell = contextCell || dragDrop.context;
let dropContext = dragDrop.dropContext;
if (dragDrop.dropPointer.align && dropContext && draggedCell) {
draggedCell.row.unmountCell(draggedCell);
// Destroy row when empty.
if (draggedCell.row.cells.length === 0) {
draggedCell.row.destroy();
}
if ((dragDrop.dropPointer.align === 'top' ||
dragDrop.dropPointer.align === 'bottom') &&
dropContext.getType() === Globals.guiElementType.row) {
dropContext = dropContext;
const newRow = dropContext.layout.addRow({}, void 0, (dropContext.layout.getRowIndex(dropContext) || 0) +
(dragDrop.dropPointer.align === 'bottom' ? 1 : 0));
newRow.mountCell(draggedCell, 0);
}
else if (dragDrop.dropPointer.align === 'nestedBottom' &&
dropContext.getType() === Globals.guiElementType.cell) {
// Create nesting.
const dropContextCell = dropContext;
const row = dropContextCell.row;
const dropContextCellIndex = row.getCellIndex(dropContextCell);
row.unmountCell(dropContextCell);
const newCell = row.addCell({
id: GUIElement.getElementId('col-nested'),
layout: {
rows: [{}, {}]
}
}, void 0, dropContextCellIndex);
if (newCell.nestedLayout) {
newCell.nestedLayout.rows[0].mountCell(dropContextCell);
newCell.nestedLayout.rows[1].mountCell(draggedCell);
}
}
else if (dropContext.getType() === Globals.guiElementType.cell) {
dropContext = dropContext;
dropContext.row.mountCell(draggedCell, (dropContext.row.getCellIndex(dropContext) || 0) +
(dragDrop.dropPointer.align === 'right' ? 1 : 0));
}
}
// Call cellResize board event.
fireEvent(dragDrop.editMode.board, 'cellResize', { cell: draggedCell });
fireEvent(draggedCell.row, 'cellChange', { cell: draggedCell, row: draggedCell.row });
dragDrop.hideDropPointer();
draggedCell.show();
fireEvent(dragDrop.editMode, 'layoutChanged', {
type: 'cellDragEnd',
target: draggedCell,
board: dragDrop.editMode.board
});
}
}
/* *
*
* Static Properties
*
* */
DragDrop.defaultOptions = {
enabled: true,
rowDropOffset: 30,
cellDropOffset: 30,
dropPointerSize: 16
};
/* *
*
* Default Export
*
* */
export default DragDrop;