@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
476 lines • 19.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransformImpl = exports.POSITIONS = exports.DIRECTIONS = exports.NODE_CLS = void 0;
const tslib_1 = require("tslib");
const common_1 = require("../../common");
const constants_1 = require("../../constants");
const geometry_1 = require("../../geometry");
const Angle = tslib_1.__importStar(require("../../geometry/angle"));
const view_1 = require("../../view");
exports.NODE_CLS = 'has-widget-transform';
exports.DIRECTIONS = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
exports.POSITIONS = [
'top-left',
'top',
'top-right',
'right',
'bottom-right',
'bottom',
'bottom-left',
'left',
];
const defaultOptions = {
minWidth: 0,
minHeight: 0,
maxWidth: Infinity,
maxHeight: Infinity,
rotateGrid: 15,
rotatable: true,
preserveAspectRatio: false,
orthogonalResizing: true,
restrictedResizing: false,
autoScrollOnResizing: true,
allowReverse: true,
};
class TransformImpl extends view_1.View {
get model() {
return this.graph.model;
}
get view() {
return this.graph.renderer.findViewByCell(this.node);
}
get containerClassName() {
return this.prefixClassName('widget-transform');
}
get resizeClassName() {
return `${this.containerClassName}-resize`;
}
get rotateClassName() {
return `${this.containerClassName}-rotate`;
}
constructor(options, node, graph) {
super();
this.node = node;
this.graph = graph;
this.options = Object.assign(Object.assign({}, defaultOptions), options);
this.render();
this.startListening();
}
startListening() {
this.delegateEvents({
[`mousedown .${this.resizeClassName}`]: 'startResizing',
[`touchstart .${this.resizeClassName}`]: 'startResizing',
[`mousedown .${this.rotateClassName}`]: 'startRotating',
[`touchstart .${this.rotateClassName}`]: 'startRotating',
});
this.model.on('*', this.update, this);
this.graph.on('scale', this.update, this);
this.graph.on('translate', this.update, this);
this.node.on('removed', this.remove, this);
this.model.on('reseted', this.remove, this);
this.view.on('cell:knob:mousedown', this.onKnobMouseDown, this);
this.view.on('cell:knob:mouseup', this.onKnobMouseUp, this);
}
stopListening() {
this.undelegateEvents();
this.model.off('*', this.update, this);
this.graph.off('scale', this.update, this);
this.graph.off('translate', this.update, this);
this.node.off('removed', this.remove, this);
this.model.off('reseted', this.remove, this);
this.view.off('cell:knob:mousedown', this.onKnobMouseDown, this);
this.view.off('cell:knob:mouseup', this.onKnobMouseUp, this);
}
renderHandles() {
this.container = document.createElement('div');
const knob = document.createElement('div');
common_1.Dom.attr(knob, 'draggable', 'false');
const rotate = knob.cloneNode(true);
common_1.Dom.addClass(rotate, this.rotateClassName);
const resizes = exports.POSITIONS.map((pos) => {
const elem = knob.cloneNode(true);
common_1.Dom.addClass(elem, this.resizeClassName);
common_1.Dom.attr(elem, 'data-position', pos);
return elem;
});
this.empty();
common_1.Dom.append(this.container, [...resizes, rotate]);
}
render() {
this.renderHandles();
if (this.view) {
this.view.addClass(exports.NODE_CLS);
}
common_1.Dom.addClass(this.container, this.containerClassName);
common_1.Dom.toggleClass(this.container, 'no-orth-resize', this.options.preserveAspectRatio || !this.options.orthogonalResizing);
common_1.Dom.toggleClass(this.container, 'no-resize', !this.options.resizable);
common_1.Dom.toggleClass(this.container, 'no-rotate', !this.options.rotatable);
if (this.options.className) {
common_1.Dom.addClass(this.container, this.options.className);
}
this.graph.container.appendChild(this.container);
return this.update();
}
update() {
const ctm = this.graph.matrix();
const bbox = this.node.getBBox();
bbox.x *= ctm.a;
bbox.x += ctm.e;
bbox.y *= ctm.d;
bbox.y += ctm.f;
bbox.width *= ctm.a;
bbox.height *= ctm.d;
const angle = Angle.normalize(this.node.getAngle());
const transform = angle !== 0 ? `rotate(${angle}deg)` : '';
common_1.Dom.css(this.container, {
transform,
width: bbox.width,
height: bbox.height,
left: bbox.x,
top: bbox.y,
});
this.updateResizerDirections();
return this;
}
remove() {
if (this.view) {
this.view.removeClass(exports.NODE_CLS);
}
return super.remove();
}
onKnobMouseDown() {
this.startHandle();
}
onKnobMouseUp() {
this.stopHandle();
}
updateResizerDirections() {
// Update the directions on the resizer divs while the node being rotated.
// The directions are represented by cardinal points (N,S,E,W). For example
// the div originally pointed to north needs to be changed to point to south
// if the node was rotated by 180 degrees.
const angle = Angle.normalize(this.node.getAngle());
const shift = Math.floor(angle * (exports.DIRECTIONS.length / 360));
if (shift !== this.prevShift) {
// Create the current directions array based on the calculated shift.
const directions = exports.DIRECTIONS.slice(shift).concat(exports.DIRECTIONS.slice(0, shift));
const className = (dir) => `${this.containerClassName}-cursor-${dir}`;
const resizes = this.container.querySelectorAll(`.${this.resizeClassName}`);
resizes.forEach((resize, index) => {
common_1.Dom.removeClass(resize, exports.DIRECTIONS.map((dir) => className(dir)).join(' '));
common_1.Dom.addClass(resize, className(directions[index]));
});
this.prevShift = shift;
}
}
getTrueDirection(dir) {
const angle = Angle.normalize(this.node.getAngle());
let index = exports.POSITIONS.indexOf(dir);
index += Math.floor(angle * (exports.POSITIONS.length / 360));
index %= exports.POSITIONS.length;
return exports.POSITIONS[index];
}
toValidResizeDirection(dir) {
return ({
top: 'top-left',
bottom: 'bottom-right',
left: 'bottom-left',
right: 'top-right',
}[dir] || dir);
}
startResizing(evt) {
evt.stopPropagation();
this.model.startBatch('resize', { cid: this.cid });
const dir = common_1.Dom.attr(evt.target, 'data-position');
this.prepareResizing(evt, dir);
this.startAction(evt);
}
prepareResizing(evt, relativeDirection) {
const trueDirection = this.getTrueDirection(relativeDirection);
let rx = 0;
let ry = 0;
relativeDirection.split('-').forEach((direction) => {
rx = { left: -1, right: 1 }[direction] || rx;
ry = { top: -1, bottom: 1 }[direction] || ry;
});
const direction = this.toValidResizeDirection(relativeDirection);
const selector = {
'top-right': 'bottomLeft',
'top-left': 'bottomRight',
'bottom-left': 'topRight',
'bottom-right': 'topLeft',
}[direction];
const angle = Angle.normalize(this.node.getAngle());
this.setEventData(evt, {
selector,
direction,
trueDirection,
relativeDirection,
angle,
resizeX: rx,
resizeY: ry,
action: 'resizing',
});
}
startRotating(evt) {
evt.stopPropagation();
this.model.startBatch('rotate', { cid: this.cid });
const center = this.node.getBBox().getCenter();
const e = this.normalizeEvent(evt);
const client = this.graph.snapToGrid(e.clientX, e.clientY);
this.setEventData(evt, {
center,
action: 'rotating',
angle: Angle.normalize(this.node.getAngle()),
start: geometry_1.Point.create(client).theta(center),
});
this.startAction(evt);
}
onMouseMove(evt) {
const view = this.graph.findViewByCell(this.node);
let data = this.getEventData(evt);
if (data.action) {
const e = this.normalizeEvent(evt);
let clientX = e.clientX;
let clientY = e.clientY;
const scroller = this.graph.getPlugin('scroller');
const restrict = this.options.restrictedResizing;
if (restrict === true || typeof restrict === 'number') {
const factor = restrict === true ? 0 : restrict;
const fix = scroller ? Math.max(factor, 8) : factor;
const rect = this.graph.container.getBoundingClientRect();
clientX = common_1.NumberExt.clamp(clientX, rect.left + fix, rect.right - fix);
clientY = common_1.NumberExt.clamp(clientY, rect.top + fix, rect.bottom - fix);
}
else if (this.options.autoScrollOnResizing && scroller) {
scroller.autoScroll(clientX, clientY);
}
const pos = this.graph.snapToGrid(clientX, clientY);
const gridSize = this.graph.getGridSize();
const node = this.node;
const options = this.options;
if (data.action === 'resizing') {
data = data;
if (!data.resized) {
if (view) {
view.addClass('node-resizing');
this.notify('node:resize', evt, view);
}
data.resized = true;
}
const currentBBox = node.getBBox();
const requestedSize = geometry_1.Point.create(pos)
.rotate(data.angle, currentBBox.getCenter())
.diff(currentBBox[data.selector]);
let width = data.resizeX
? requestedSize.x * data.resizeX
: currentBBox.width;
let height = data.resizeY
? requestedSize.y * data.resizeY
: currentBBox.height;
const rawWidth = width;
const rawHeight = height;
width = (0, geometry_1.snapToGrid)(width, gridSize);
height = (0, geometry_1.snapToGrid)(height, gridSize);
width = Math.max(width, options.minWidth || gridSize);
height = Math.max(height, options.minHeight || gridSize);
width = Math.min(width, options.maxWidth || Infinity);
height = Math.min(height, options.maxHeight || Infinity);
if (options.preserveAspectRatio) {
const candidateWidth = (currentBBox.width * height) / currentBBox.height;
const candidateHeight = (currentBBox.height * width) / currentBBox.width;
if (width < candidateWidth) {
height = candidateHeight;
}
else {
width = candidateWidth;
}
}
const relativeDirection = data.relativeDirection;
if (options.allowReverse &&
(rawWidth <= -width || rawHeight <= -height)) {
let reverted;
if (relativeDirection === 'left') {
if (rawWidth <= -width) {
reverted = 'right';
}
}
else if (relativeDirection === 'right') {
if (rawWidth <= -width) {
reverted = 'left';
}
}
else if (relativeDirection === 'top') {
if (rawHeight <= -height) {
reverted = 'bottom';
}
}
else if (relativeDirection === 'bottom') {
if (rawHeight <= -height) {
reverted = 'top';
}
}
else if (relativeDirection === 'top-left') {
if (rawWidth <= -width && rawHeight <= -height) {
reverted = 'bottom-right';
}
else if (rawWidth <= -width) {
reverted = 'top-right';
}
else if (rawHeight <= -height) {
reverted = 'bottom-left';
}
}
else if (relativeDirection === 'top-right') {
if (rawWidth <= -width && rawHeight <= -height) {
reverted = 'bottom-left';
}
else if (rawWidth <= -width) {
reverted = 'top-left';
}
else if (rawHeight <= -height) {
reverted = 'bottom-right';
}
}
else if (relativeDirection === 'bottom-left') {
if (rawWidth <= -width && rawHeight <= -height) {
reverted = 'top-right';
}
else if (rawWidth <= -width) {
reverted = 'bottom-right';
}
else if (rawHeight <= -height) {
reverted = 'top-left';
}
}
else if (relativeDirection === 'bottom-right') {
if (rawWidth <= -width && rawHeight <= -height) {
reverted = 'top-left';
}
else if (rawWidth <= -width) {
reverted = 'bottom-left';
}
else if (rawHeight <= -height) {
reverted = 'top-right';
}
}
const revertedDir = reverted;
this.stopHandle();
const handle = this.container.querySelector(`.${this.resizeClassName}[data-position="${revertedDir}"]`);
this.startHandle(handle);
this.prepareResizing(evt, revertedDir);
this.onMouseMove(evt);
}
if (currentBBox.width !== width || currentBBox.height !== height) {
const resizeOptions = {
ui: true,
direction: data.direction,
relativeDirection: data.relativeDirection,
trueDirection: data.trueDirection,
minWidth: options.minWidth,
minHeight: options.minHeight,
maxWidth: options.maxWidth,
maxHeight: options.maxHeight,
preserveAspectRatio: options.preserveAspectRatio === true,
};
node.resize(width, height, resizeOptions);
this.notify('node:resizing', evt, view);
}
}
else if (data.action === 'rotating') {
data = data;
if (!data.rotated) {
if (view) {
view.addClass('node-rotating');
this.notify('node:rotate', evt, view);
}
data.rotated = true;
}
const currentAngle = node.getAngle();
const theta = data.start - geometry_1.Point.create(pos).theta(data.center);
let target = data.angle + theta;
if (options.rotateGrid) {
target = (0, geometry_1.snapToGrid)(target, options.rotateGrid);
}
target = Angle.normalize(target);
if (currentAngle !== target) {
node.rotate(target, { absolute: true });
this.notify('node:rotating', evt, view);
}
}
}
}
onMouseUp(evt) {
const data = this.getEventData(evt);
if (data.action) {
this.stopAction(evt);
this.model.stopBatch(data.action === 'resizing' ? 'resize' : 'rotate', {
cid: this.cid,
});
}
}
startHandle(handle) {
this.handle = handle || null;
common_1.Dom.addClass(this.container, `${this.containerClassName}-active`);
if (handle) {
common_1.Dom.addClass(handle, `${this.containerClassName}-active-handle`);
const pos = handle.getAttribute('data-position');
if (pos) {
const dir = exports.DIRECTIONS[exports.POSITIONS.indexOf(pos)];
common_1.Dom.addClass(this.container, `${this.containerClassName}-cursor-${dir}`);
}
}
}
stopHandle() {
common_1.Dom.removeClass(this.container, `${this.containerClassName}-active`);
if (this.handle) {
common_1.Dom.removeClass(this.handle, `${this.containerClassName}-active-handle`);
const pos = this.handle.getAttribute('data-position');
if (pos) {
const dir = exports.DIRECTIONS[exports.POSITIONS.indexOf(pos)];
common_1.Dom.removeClass(this.container, `${this.containerClassName}-cursor-${dir}`);
}
this.handle = null;
}
}
startAction(evt) {
this.startHandle(evt.target);
this.graph.view.undelegateEvents();
this.delegateDocumentEvents(constants_1.DocumentEvents, evt.data);
}
stopAction(evt) {
this.stopHandle();
this.undelegateDocumentEvents();
this.graph.view.delegateEvents();
const view = this.graph.findViewByCell(this.node);
const data = this.getEventData(evt);
if (view) {
view.removeClass(`node-${data.action}`);
if (data.action === 'resizing' && data.resized) {
this.notify('node:resized', evt, view);
}
else if (data.action === 'rotating' && data.rotated) {
this.notify('node:rotated', evt, view);
}
}
}
notify(name, evt, view, args = {}) {
if (view) {
const graph = view.graph;
const e = graph.view.normalizeEvent(evt);
const localPoint = graph.snapToGrid(e.clientX, e.clientY);
this.trigger(name, Object.assign({ e,
view, node: view.cell, cell: view.cell, x: localPoint.x, y: localPoint.y }, args));
}
}
dispose() {
this.stopListening();
this.remove();
this.off();
}
}
exports.TransformImpl = TransformImpl;
tslib_1.__decorate([
(0, common_1.disposable)()
], TransformImpl.prototype, "dispose", null);
//# sourceMappingURL=transform.js.map