UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

467 lines 19.1 kB
import { Util } from '../../global'; import { NumberExt } from '../../util'; import { Angle, Point } from '../../geometry'; import { Widget } from '../common'; import { notify } from './util'; export class Transform extends Widget { get node() { return this.cell; } get containerClassName() { return this.prefixClassName('widget-transform'); } get resizeClassName() { return `${this.containerClassName}-resize`; } get rotateClassName() { return `${this.containerClassName}-rotate`; } init(options) { this.options = Object.assign(Object.assign({}, Private.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); super.startListening(); } 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); super.stopListening(); } renderHandles() { this.container = document.createElement('div'); this.$container = this.$(this.container); const $knob = this.$('<div/>').prop('draggable', false); const $rotate = $knob.clone().addClass(this.rotateClassName); const $resizes = Private.POSITIONS.map((pos) => { return $knob .clone() .addClass(this.resizeClassName) .attr('data-position', pos); }); this.empty(); this.$container.append($resizes, $rotate); } render() { this.renderHandles(); this.view.addClass(Private.NODE_CLS); this.$container .addClass(this.containerClassName) .toggleClass('no-orth-resize', this.options.preserveAspectRatio || !this.options.orthogonalResizing) .toggleClass('no-resize', !this.options.resizable) .toggleClass('no-rotate', !this.options.rotatable); if (this.options.className) { this.$container.addClass(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)` : ''; this.$container.css({ transform, width: bbox.width, height: bbox.height, left: bbox.x, top: bbox.y, }); this.updateResizerDirections(); return this; } remove() { this.view.removeClass(Private.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 * (Private.DIRECTIONS.length / 360)); if (shift !== this.prevShift) { // Create the current directions array based on the calculated shift. const directions = Private.DIRECTIONS.slice(shift).concat(Private.DIRECTIONS.slice(0, shift)); const className = (dir) => `${this.containerClassName}-cursor-${dir}`; this.$container .find(`.${this.resizeClassName}`) .removeClass(Private.DIRECTIONS.map((dir) => className(dir)).join(' ')) .each((index, elem) => { this.$(elem).addClass(className(directions[index])); }); this.prevShift = shift; } } getTrueDirection(dir) { const angle = Angle.normalize(this.node.getAngle()); let index = Private.POSITIONS.indexOf(dir); index += Math.floor(angle * (Private.POSITIONS.length / 360)); index %= Private.POSITIONS.length; return Private.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 = this.$(evt.target).attr('data-position'); const view = this.graph.findViewByCell(this.node); this.prepareResizing(evt, dir); this.startAction(evt); notify('node:resize:mousedown', evt, view); } 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 view = this.graph.findViewByCell(this.node); const center = this.node.getBBox().getCenter(); const client = this.graph.snapToGrid(evt.clientX, evt.clientY); this.setEventData(evt, { center, action: 'rotating', angle: Angle.normalize(this.node.getAngle()), start: Point.create(client).theta(center), }); this.startAction(evt); notify('node:rotate:mousedown', evt, view); } 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.scroller.widget; 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 = NumberExt.clamp(clientX, rect.left + fix, rect.right - fix); clientY = 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'); notify('node:resize', evt, view); } data.resized = true; } const currentBBox = node.getBBox(); const requestedSize = 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 = Util.snapToGrid(width, gridSize); height = Util.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.find(`.${this.resizeClassName}[data-position="${revertedDir}"]`); this.startHandle($handle[0]); 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); notify('node:resizing', evt, view); } notify('node:resize:mousemove', evt, view); } else if (data.action === 'rotating') { data = data; if (!data.rotated) { if (view) { view.addClass('node-rotating'); notify('node:rotate', evt, view); } data.rotated = true; } const currentAngle = node.getAngle(); const theta = data.start - Point.create(pos).theta(data.center); let target = data.angle + theta; if (options.rotateGrid) { target = Util.snapToGrid(target, options.rotateGrid); } if (currentAngle !== target) { node.rotate(target, { absolute: true }); notify('node:rotating', evt, view); } notify('node:rotate:mousemove', evt, view); } } } onMouseUp(evt) { const view = this.graph.findViewByCell(this.node); const data = this.getEventData(evt); if (data.action) { this.stopAction(evt); this.model.stopBatch(data.action === 'resizing' ? 'resize' : 'rotate', { cid: this.cid, }); if (data.action === 'resizing') { notify('node:resize:mouseup', evt, view); } else if (data.action === 'rotating') { notify('node:rotate:mouseup', evt, view); } } } startHandle(handle) { this.handle = handle || null; this.$container.addClass(`${this.containerClassName}-active`); if (handle) { this.$(handle).addClass(`${this.containerClassName}-active-handle`); const pos = handle.getAttribute('data-position'); if (pos) { const dir = Private.DIRECTIONS[Private.POSITIONS.indexOf(pos)]; this.$container.addClass(`${this.containerClassName}-cursor-${dir}`); } } } stopHandle() { this.$container.removeClass(`${this.containerClassName}-active`); if (this.handle) { this.$(this.handle).removeClass(`${this.containerClassName}-active-handle`); const pos = this.handle.getAttribute('data-position'); if (pos) { const dir = Private.DIRECTIONS[Private.POSITIONS.indexOf(pos)]; this.$container.removeClass(`${this.containerClassName}-cursor-${dir}`); } this.handle = null; } } startAction(evt) { this.startHandle(evt.target); this.graph.view.undelegateEvents(); this.delegateDocumentEvents(Private.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) { notify('node:resized', evt, view); } else if (data.action === 'rotating' && data.rotated) { notify('node:rotated', evt, view); } } } } var Private; (function (Private) { Private.NODE_CLS = 'has-widget-transform'; Private.DIRECTIONS = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w']; Private.POSITIONS = [ 'top-left', 'top', 'top-right', 'right', 'bottom-right', 'bottom', 'bottom-left', 'left', ]; Private.documentEvents = { mousemove: 'onMouseMove', touchmove: 'onMouseMove', mouseup: 'onMouseUp', touchend: 'onMouseUp', }; Private.defaultOptions = { minWidth: 0, minHeight: 0, maxWidth: Infinity, maxHeight: Infinity, rotateGrid: 15, rotatable: true, preserveAspectRatio: false, orthogonalResizing: true, restrictedResizing: false, autoScrollOnResizing: true, allowReverse: true, }; })(Private || (Private = {})); //# sourceMappingURL=index.js.map