dmn-js-decision-table
Version:
A decision table view for dmn-js
365 lines (348 loc) • 11 kB
JavaScript
import { createVNode, createTextVNode } from "inferno";
import { classes as domClasses, domify, queryAll as domQueryAll, remove as domRemove } from 'min-dom';
import { isInput, isOutput } from 'dmn-js-shared/lib/util/ModelUtil';
import { Row, Col } from 'table-js/lib/model';
import { forEach } from 'min-dash';
const TOP = 'top',
RIGHT = 'right',
BOTTOM = 'bottom',
LEFT = 'left';
export default class DragAndDrop {
constructor(components, elementRegistry, eventBus, dragAndDrop, renderer, rules, sheet, translate) {
this._elementRegistry = elementRegistry;
this._dragAndDrop = dragAndDrop;
this._renderer = renderer;
this._rules = rules;
this._sheet = sheet;
this._translate = translate;
// provide drag handle for drag and drop
components.onGetComponent('cell-inner', ({
cellType,
col,
row
}) => {
if (cellType === 'rule-index') {
return () => createVNode(1, "span", "dmn-icon-drag vertical", createTextVNode("\xA0"), 2, {
"draggable": "true",
"onDragStart": e => this.startDrag(row, e),
"title": this._translate('Move rule')
});
} else if (cellType === 'input-cell' || cellType === 'output-cell') {
let title = `Move ${isInput(col) ? 'Input' : 'Output'}`;
return () => createVNode(1, "span", "dmn-icon-drag horizontal", null, 1, {
"draggable": "true",
"onDragStart": e => this.startDrag(col, e),
"title": title
});
}
});
// validate allowed rules
eventBus.on('dragAndDrop.dragEnter', event => {
const {
dragContext
} = event;
const {
draggedElement,
hoverEl
} = dragContext;
// can always drag rows
if (draggedElement instanceof Row) {
return true;
}
if (draggedElement instanceof Col) {
const dropIndex = getTargetColIndex(hoverEl, this._elementRegistry, this._sheet);
// cannot drop as we cannot compute the drop index
if (dropIndex === -1) {
return false;
}
const allowed = this._rules.allowed('col.move', {
col: draggedElement,
index: dropIndex
});
return allowed;
}
return false;
});
// clear previous UI
eventBus.on('dragAndDrop.dragLeave', event => {
const {
dragContext
} = event;
const {
targetEl
} = dragContext;
if (!targetEl) {
return;
}
const container = this._renderer.getContainer();
removeHighlight(container);
});
// update UI
eventBus.on('dragAndDrop.dragOver', event => {
const {
dragContext,
originalEvent
} = event;
const {
draggedElement,
lastPosition,
targetEl
} = dragContext;
const container = this._renderer.getContainer();
if (!targetEl) {
return false;
}
let newPosition;
if (draggedElement instanceof Row) {
newPosition = getVerticalPosition(originalEvent, targetEl);
}
if (draggedElement instanceof Col) {
newPosition = getHorizontalPosition(originalEvent, targetEl);
}
// nothing to do
if (lastPosition === newPosition) {
return true;
}
// remove old highlight
removeHighlight(container);
if (draggedElement instanceof Row) {
if (newPosition === TOP) {
// drop above
highlightRow(targetEl, container, 'top');
} else {
// drop below
highlightRow(targetEl, container, 'bottom');
}
}
if (draggedElement instanceof Col) {
if (newPosition === LEFT) {
// drop left
highlightCol(targetEl, container, 'left');
} else {
// drop right
highlightCol(targetEl, container, 'right');
}
}
// remember position
dragContext.lastPosition = newPosition;
// allowed
return true;
});
// perform drop operation
eventBus.on('dragAndDrop.drop', event => {
const {
dragContext,
originalEvent
} = event;
const {
draggedElement,
targetEl
} = dragContext;
if (!targetEl) {
return false;
}
if (draggedElement instanceof Row) {
const verticalPosition = getVerticalPosition(originalEvent, targetEl);
const rowId = targetEl.dataset.rowId,
row = this._elementRegistry.get(rowId);
if (!row || row === draggedElement) {
return;
}
const targetRow = getTargetRow(draggedElement, row, verticalPosition, this._sheet.getRoot().rows);
if (targetRow === draggedElement) {
return;
}
return targetRow;
}
if (draggedElement instanceof Col) {
const horizontalPosition = getHorizontalPosition(originalEvent, targetEl);
// no need to check rules; we verified on
// dragEnter that dropping is O.K.
const colId = targetEl.dataset.colId,
col = this._elementRegistry.get(colId);
if (!col || col === draggedElement) {
return;
}
const targetCol = getTargetCol(draggedElement, col, horizontalPosition, this._sheet.getRoot().cols);
if (targetCol === draggedElement) {
return;
}
return targetCol;
}
});
eventBus.on('dragAndDrop.dragEnd', this._cleanup);
}
startDrag(element, event) {
const container = this._renderer.getContainer();
this._dragImage = domify(`<span style="
visibility: hidden;
position: fixed;
top: -10000px
"></span>`);
// needs to be present in DOM
document.body.appendChild(this._dragImage);
// QUIRK: not supported by Edge and Internet Explorer
if (event.dataTransfer.setDragImage) {
event.dataTransfer.setDragImage(this._dragImage, 0, 0);
}
if (element instanceof Row) {
fadeOutRow(element, container);
} else if (element instanceof Col) {
fadeOutCol(element, container);
}
this._dragAndDrop.startDrag(element, event);
}
_cleanup = () => {
const container = this._renderer.getContainer();
removeHighlight(container);
removeFadeOut(container);
if (this._dragImage) {
domRemove(this._dragImage);
this._dragImage = null;
}
};
}
DragAndDrop.$inject = ['components', 'elementRegistry', 'eventBus', 'dragAndDrop', 'renderer', 'rules', 'sheet', 'translate'];
// helpers //////////
function getTargetColIndex(cellEl, elementRegistry, sheet) {
const targetCol = elementRegistry.get(cellEl.dataset.colId);
if (!targetCol) {
return -1;
}
const {
cols
} = sheet.getRoot();
return cols.indexOf(targetCol);
}
function highlightRow(dragOverCell, container, position) {
const rowId = dragOverCell.dataset.rowId;
if (!rowId) {
return;
}
const cells = domQueryAll(`[data-row-id=${rowId}]`, container);
forEach(cells, cell => {
// QUIRK: PhantomJS might return object instead of NodeList
if (isNode(cell)) {
domClasses(cell).add('dragover');
domClasses(cell).add(position);
}
});
}
function highlightCol(dragOverCell, container, position) {
const colId = dragOverCell.dataset.colId;
if (!colId) {
return;
}
const cells = domQueryAll(`[data-col-id=${colId}]`, container);
forEach(cells, cell => {
// QUIRK: PhantomJS might return object instead of NodeList
if (isNode(cell)) {
domClasses(cell).add('dragover');
domClasses(cell).add(position);
}
});
}
function removeHighlight(container) {
const cells = domQueryAll('.dragover', container);
forEach(cells, cell => {
// QUIRK: PhantomJS might return object instead of NodeList
if (isNode(cell)) {
domClasses(cell).remove('dragover');
domClasses(cell).remove('top');
domClasses(cell).remove('right');
domClasses(cell).remove('bottom');
domClasses(cell).remove('left');
}
});
}
function fadeOutRow(row, container) {
const cells = domQueryAll(`[data-row-id=${row.id}]`, container);
forEach(cells, cell => {
// QUIRK: PhantomJS might return object instead of NodeList
if (isNode(cell)) {
domClasses(cell).add('dragged');
}
});
}
function fadeOutCol(col, container) {
const cells = domQueryAll(`[data-col-id=${col.id}]`, container);
forEach(cells, cell => {
// QUIRK: PhantomJS might return object instead of NodeList
if (isNode(cell)) {
domClasses(cell).add('dragged');
}
});
}
function removeFadeOut(container) {
const cells = domQueryAll('.dragged', container);
forEach(cells, cell => {
// QUIRK: PhantomJS might return object instead of NodeList
if (isNode(cell)) {
domClasses(cell).remove('dragged');
}
});
}
function getHorizontalPosition(event, dragOverElement) {
const bounds = dragOverElement.getBoundingClientRect();
return event.clientX < bounds.left + bounds.width / 2 ? LEFT : RIGHT;
}
function getVerticalPosition(event, dragOverElement) {
const bounds = dragOverElement.getBoundingClientRect();
return event.clientY < bounds.top + bounds.height / 2 ? TOP : BOTTOM;
}
function getTargetRow(draggedRow, targetRow, verticalPosition, rows) {
if (rows.indexOf(draggedRow) > rows.indexOf(targetRow)) {
targetRow = getRowBelow(targetRow, rows);
}
if (verticalPosition === TOP) {
// return row above or row
return getRowAbove(targetRow, rows);
} else {
// return row
return targetRow;
}
}
function getTargetCol(draggedCol, targetCol, horizontalPosition, cols) {
if (cols.indexOf(draggedCol) > cols.indexOf(targetCol)) {
targetCol = getColRight(targetCol, cols);
}
if (horizontalPosition === LEFT) {
// return col left or col
return getColLeft(targetCol, cols);
} else {
// return col
return targetCol;
}
}
function getRowAbove(row, rows) {
const index = rows.indexOf(row);
return rows[Math.max(0, index - 1)];
}
function getRowBelow(row, rows) {
const index = rows.indexOf(row);
return rows[Math.min(rows.length - 1, index + 1)];
}
function getColLeft(col, cols) {
const index = cols.indexOf(col);
if (isOutput(col)) {
const firstOutput = cols.filter(col => isOutput(col))[0];
const firstOutputIndex = cols.indexOf(firstOutput);
return cols[Math.max(firstOutputIndex, index - 1)];
}
return cols[Math.max(0, index - 1)];
}
function getColRight(col, cols) {
const index = cols.indexOf(col);
if (isInput(col)) {
const inputs = cols.filter(col => isInput(col));
const lastInput = inputs[inputs.length - 1];
const lastInputIndex = cols.indexOf(lastInput);
return cols[Math.min(lastInputIndex, index + 1)];
}
return cols[Math.min(cols.length - 1, index + 1)];
}
// QUIRK: PhantomJS requires check if actual DOM node
function isNode(node) {
return node && (node.nodeType === 1 || node.nodeType == 11);
}
//# sourceMappingURL=DragAndDrop.js.map