UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

292 lines (243 loc) 7.59 kB
import { ObjectExt, FunctionExt } from '../../util' import { Rectangle } from '../../geometry' import { Cell } from '../../model/cell' import { Edge } from '../../model/edge' import { CellView } from '../../view/cell' import { Widget, Handle } from '../common' import { NodePreset } from './node-preset' import { EdgePreset } from './edge-preset' export class Halo extends Widget<Halo.Options> implements Handle { protected $container: JQuery<HTMLElement> protected $content: JQuery<HTMLElement> protected get type() { return this.options.type || 'surround' } protected get handleOptions() { return this.options } init(options: Halo.Options) { this.options = ObjectExt.merge( Halo.defaultOptions, this.cell.isNode() ? new NodePreset(this).getPresets() : this.cell.isEdge() ? new EdgePreset(this).getPresets() : null, options, ) this.render() this.initHandles() this.update() this.startListening() } protected startListening() { const model = this.model const graph = this.graph const cell = this.view.cell cell.on('removed', this.remove, this) model.on('reseted', this.remove, this) graph.on('halo:destroy', this.remove, this) model.on('*', this.update, this) graph.on('scale', this.update, this) graph.on('translate', this.update, this) super.startListening() } protected stopListening() { const model = this.model const graph = this.graph const cell = this.view.cell this.undelegateEvents() cell.off('removed', this.remove, this) model.off('reseted', this.remove, this) graph.off('halo:destroy', this.remove, this) model.off('*', this.update, this) graph.off('scale', this.update, this) graph.off('translate', this.update, this) super.stopListening() } protected render() { const options = this.options const cls = this.prefixClassName('widget-halo') this.view.addClass(Private.NODE_CLS) this.container = document.createElement('div') this.$container = this.$(this.container) .addClass(cls) .attr('data-shape', this.view.cell.shape) if (options.className) { this.$container.addClass(options.className) } this.$handleContainer = this.$('<div/>') .addClass(`${cls}-handles`) .appendTo(this.container) this.$content = this.$('<div/>') .addClass(`${cls}-content`) .appendTo(this.container) this.$container.appendTo(this.graph.container) return this } remove() { this.stopBatch() this.view.removeClass(Private.NODE_CLS) return super.remove() } protected update() { if (this.isRendered()) { this.updateContent() const bbox = this.getBBox() const tinyThreshold = this.options.tinyThreshold || 0 const smallThreshold = this.options.smallThreshold || 0 this.$handleContainer.toggleClass( `${this.handleClassName}-tiny`, bbox.width < tinyThreshold && bbox.height < tinyThreshold, ) const className = `${this.handleClassName}-small` this.$handleContainer.toggleClass( className, !this.$handleContainer.hasClass(className) && bbox.width < smallThreshold && bbox.height < smallThreshold, ) this.$container.css({ width: bbox.width, height: bbox.height, left: bbox.x, top: bbox.y, }) if (this.hasHandle('unlink')) { this.toggleUnlink() } if (this.type === 'surround' || this.type === 'toolbar') { if (this.hasHandle('fork')) { this.toggleFork() } } } } protected updateContent() { const content = this.options.content if (typeof content === 'function') { const ret = FunctionExt.call(content, this, this.view, this.$content[0]) if (ret) { this.$content.html(ret) } } else if (content) { this.$content.html(content) } else { this.$content.remove() } } protected getBBox() { const view = this.view const bbox = this.options.bbox const rect = typeof bbox === 'function' ? FunctionExt.call(bbox, this, view) : bbox return Rectangle.create({ x: 0, y: 0, width: 1, height: 1, ...rect, }) } protected removeCell() { this.cell.remove() } protected toggleFork() { const cell = this.view.cell.clone() const view = this.graph.hook.createCellView(cell)! const valid = this.graph.hook.validateConnection( this.view, null, view, null, 'target', ) this.$handleContainer.children('.fork').toggleClass('hidden', !valid) view.remove() } protected toggleUnlink() { const hasEdges = this.model.getConnectedEdges(this.view.cell).length > 0 this.$handleContainer.children('.unlink').toggleClass('hidden', !hasEdges) } // #region batch startBatch() { this.model.startBatch('halo', { halo: this.cid, }) } stopBatch() { if (this.model.hasActiveBatch('halo')) { this.model.stopBatch('halo', { halo: this.cid, }) } } // #endregion } export namespace Halo { export interface Options extends Handle.Options, Widget.Options { className?: string /** * The preferred side for a self-loop edge created from Halo */ loopEdgePreferredSide?: 'top' | 'bottom' | 'left' | 'right' loopEdgeWidth?: number rotateGrid?: number rotateEmbeds?: boolean content?: | false | string | ((cellView: CellView, boxElement: HTMLElement) => string) /** * If set to true, the cell position and dimensions will be used as a * basis for the Halo tools position. By default, this is set to `false` * which causes the Halo tools position be based on the bounding box of * the element view. Sometimes though, your shapes can have certain SVG * sub elements that stick out of the view and you don't want these sub * elements to affect the Halo tools position. In this case, set the * `useCellGeometry` to true. */ useCellGeometry?: boolean /** * This function will be called when cloning or forking actions take * place and it should return a clone of the original cell. This is * useful e.g. if you want the clone to be moved by an offset after * the user clicks the clone handle. */ clone?: (cell: Cell, opt: any) => Cell /** * A bounding box within which the Halo view will be rendered. */ bbox?: | Partial<Rectangle.RectangleLike> | ((this: Halo, view: CellView) => Partial<Rectangle.RectangleLike>) magnet?: (cellView: CellView, terminal: Edge.TerminalType) => Element } export type OrthPosition = 'e' | 'w' | 's' | 'n' export type Position = OrthPosition | 'se' | 'sw' | 'ne' | 'nw' export interface PieToggle { name: string position?: OrthPosition attrs?: { [selector: string]: JQuery.PlainObject } } export const defaultOptions: Options = { type: 'surround', clearAll: true, clearOnBlankMouseDown: true, useCellGeometry: false, clone: (cell) => cell.clone().removeZIndex(), } } export interface Halo extends Handle {} Object.getOwnPropertyNames(Handle.prototype).forEach((name) => { if (name !== 'constructor') { Object.defineProperty( Halo.prototype, name, Object.getOwnPropertyDescriptor(Handle.prototype, name)!, ) } }) namespace Private { export const NODE_CLS = 'has-widget-halo' }