UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

610 lines (517 loc) 15.2 kB
import { Point, Path } from '../../geometry' import * as Dom from '../dom/core' export class Vector { public node: SVGElement protected get [Symbol.toStringTag]() { return Vector.toStringTag } public get type() { return this.node.nodeName } public get id() { return this.node.id } public set id(id: string) { this.node.id = id } constructor( elem: Vector | SVGElement | string, attrs?: Dom.Attributes, children?: SVGElement | Vector | (SVGElement | Vector)[], ) { if (!elem) { throw new TypeError('Invalid element to create vector') } let node: SVGElement if (Vector.isVector(elem)) { node = elem.node } else if (typeof elem === 'string') { if (elem.toLowerCase() === 'svg') { node = Dom.createSvgDocument() } else if (elem[0] === '<') { const doc = Dom.createSvgDocument(elem) // only import the first child node = document.importNode(doc.firstChild!, true) as SVGElement } else { node = document.createElementNS(Dom.ns.svg, elem) as SVGElement } } else { node = elem } this.node = node if (attrs) { this.setAttributes(attrs) } if (children) { this.append(children) } } /** * Returns the current transformation matrix of the Vector element. */ transform(): DOMMatrix /** * Applies the provided transformation matrix to the Vector element. */ transform(matrix: DOMMatrix, options?: Dom.TransformOptions): this transform(matrix?: DOMMatrix, options?: Dom.TransformOptions) { if (matrix == null) { return Dom.transform(this.node) } Dom.transform(this.node, matrix, options) return this } /** * Returns the current translate metadata of the Vector element. */ translate(): Dom.Translation /** * Translates the element by `tx` pixels in x axis and `ty` pixels * in y axis. `ty` is optional in which case the translation in y axis * is considered zero. */ translate(tx: number, ty?: number, options?: Dom.TransformOptions): this translate(tx?: number, ty = 0, options: Dom.TransformOptions = {}) { if (tx == null) { return Dom.translate(this.node) } Dom.translate(this.node, tx, ty, options) return this } /** * Returns the current rotate metadata of the Vector element. */ rotate(): Dom.Rotation /** * Rotates the element by `angle` degrees. If the optional `cx` and `cy` * coordinates are passed, they will be used as an origin for the rotation. */ rotate( angle: number, cx?: number, cy?: number, options?: Dom.TransformOptions, ): this rotate( angle?: number, cx?: number, cy?: number, options: Dom.TransformOptions = {}, ) { if (angle == null) { return Dom.rotate(this.node) } Dom.rotate(this.node, angle, cx, cy, options) return this } /** * Returns the current scale metadata of the Vector element. */ scale(): Dom.Scale /** * Scale the element by `sx` and `sy` factors. If `sy` is not specified, * it will be considered the same as `sx`. */ scale(sx: number, sy?: number): this scale(sx?: number, sy?: number) { if (sx == null) { return Dom.scale(this.node) } Dom.scale(this.node, sx, sy) return this } /** * Returns an SVGMatrix that specifies the transformation necessary * to convert this coordinate system into `target` coordinate system. */ getTransformToElement(target: SVGElement | Vector) { const ref = Vector.toNode(target) as SVGGraphicsElement return Dom.getTransformToElement(this.node, ref) } removeAttribute(name: string) { Dom.removeAttribute(this.node, name) return this } getAttribute(name: string) { return Dom.getAttribute(this.node, name) } setAttribute(name: string, value?: string | number | null) { Dom.setAttribute(this.node, name, value) return this } setAttributes(attrs: { [attr: string]: string | number | null | undefined }) { Dom.setAttributes(this.node, attrs) return this } attr(): { [attr: string]: string } attr(name: string): string attr(attrs: { [attr: string]: string | number | null | undefined }): this attr(name: string, value: string | number): this attr( name?: string | { [attr: string]: string | number | null | undefined }, value?: string | number | null, ) { if (name == null) { return Dom.attr(this.node) } if (typeof name === 'string' && value === undefined) { return Dom.attr(this.node, name) } if (typeof name === 'object') { Dom.attr(this.node, name) } else { Dom.attr(this.node, name, value!) } return this } svg() { return this.node instanceof SVGSVGElement ? this : Vector.create(this.node.ownerSVGElement as SVGSVGElement) } defs() { const context = this.svg() || this const defsNode = context.node.getElementsByTagName('defs')[0] if (defsNode) { return Vector.create(defsNode) } return Vector.create('defs').appendTo(context) } text(content: string, options: Dom.TextOptions = {}) { Dom.text(this.node, content, options) return this } tagName() { return Dom.tagName(this.node) } clone() { return Vector.create(this.node.cloneNode(true) as SVGElement) } remove() { Dom.remove(this.node) return this } empty() { Dom.empty(this.node) return this } append( elems: | SVGElement | DocumentFragment | Vector | (SVGElement | DocumentFragment | Vector)[], ) { Dom.append(this.node, Vector.toNodes(elems)) return this } appendTo(target: Element | Vector) { Dom.appendTo(this.node, Vector.isVector(target) ? target.node : target) return this } prepend( elems: | SVGElement | DocumentFragment | Vector | (SVGElement | DocumentFragment | Vector)[], ) { Dom.prepend(this.node, Vector.toNodes(elems)) return this } before( elems: | SVGElement | DocumentFragment | Vector | (SVGElement | DocumentFragment | Vector)[], ) { Dom.before(this.node, Vector.toNodes(elems)) return this } replace(elem: SVGElement | Vector) { if (this.node.parentNode) { this.node.parentNode.replaceChild(Vector.toNode(elem), this.node) } return Vector.create(elem) } first() { return this.node.firstChild ? Vector.create(this.node.firstChild as SVGElement) : null } last() { return this.node.lastChild ? Vector.create(this.node.lastChild as SVGElement) : null } get(index: number) { const child = this.node.childNodes[index] as SVGElement return child ? Vector.create(child) : null } indexOf(elem: SVGElement | Vector) { const children: SVGElement[] = Array.prototype.slice.call( this.node.childNodes, ) return children.indexOf(Vector.toNode(elem)) } find(selector: string) { const vels: Vector[] = [] const nodes = Dom.find(this.node, selector) if (nodes) { for (let i = 0, ii = nodes.length; i < ii; i += 1) { vels.push(Vector.create(nodes[i] as SVGElement)) } } return vels } findOne(selector: string) { const found = Dom.findOne(this.node, selector) return found ? Vector.create(found as SVGElement) : null } findParentByClass(className: string, terminator?: SVGElement) { const node = Dom.findParentByClass(this.node, className, terminator) return node ? Vector.create(node as SVGElement) : null } matches(selector: string): boolean { const node = this.node as any const matches = this.node.matches const matcher: typeof matches = node.matches || node.matchesSelector || node.msMatchesSelector || node.mozMatchesSelector || node.webkitMatchesSelector || node.oMatchesSelector || null return matcher && matcher.call(node, selector) } contains(child: SVGElement | Vector) { return Dom.contains(this.node, Vector.isVector(child) ? child.node : child) } wrap(node: SVGElement | Vector) { const vel = Vector.create(node) const parentNode = this.node.parentNode as SVGElement if (parentNode != null) { parentNode.insertBefore(vel.node, this.node) } return vel.append(this) } parent(type?: string) { let parent: Vector = this // eslint-disable-line @typescript-eslint/no-this-alias // check for parent if (parent.node.parentNode == null) { return null } // get parent element parent = Vector.create(parent.node.parentNode as SVGElement) if (type == null) { return parent } // loop trough ancestors if type is given do { if ( typeof type === 'string' ? parent.matches(type) : parent instanceof type ) { return parent } } while ((parent = Vector.create(parent.node.parentNode as SVGElement))) return parent } children() { const children = this.node.childNodes const vels: Vector[] = [] for (let i = 0; i < children.length; i += 1) { const currentChild = children[i] if (currentChild.nodeType === 1) { vels.push(Vector.create(children[i] as SVGElement)) } } return vels } eachChild( fn: ( this: Vector, currentValue: Vector, index: number, children: Vector[], ) => void, deep?: boolean, ) { const children = this.children() for (let i = 0, l = children.length; i < l; i += 1) { fn.call(children[i], children[i], i, children) if (deep) { children[i].eachChild(fn, deep) } } return this } index() { return Dom.index(this.node) } hasClass(className: string) { return Dom.hasClass(this.node, className) } addClass(className: string) { Dom.addClass(this.node, className) return this } removeClass(className?: string) { Dom.removeClass(this.node, className) return this } toggleClass(className: string, stateVal?: boolean) { Dom.toggleClass(this.node, className, stateVal) return this } toLocalPoint(x: number, y: number) { return Dom.toLocalPoint(this.node, x, y) } toGeometryShape() { return Dom.toGeometryShape(this.node) } translateCenterToPoint(p: Point.PointLike) { const bbox = this.getBBox({ target: this.svg() }) const center = bbox.getCenter() this.translate(p.x - center.x, p.y - center.y) return this } translateAndAutoOrient( position: Point.PointLike | Point.PointData, reference: Point.PointLike | Point.PointData, target?: SVGElement, ) { Dom.translateAndAutoOrient(this.node, position, reference, target) return this } animate(options: Dom.AnimationOptions) { return Dom.animate(this.node, options) } animateTransform(options: Dom.AnimationOptions) { return Dom.animateTransform(this.node, options) } animateAlongPath(options: Dom.AnimationOptions, path: SVGPathElement) { return Dom.animateAlongPath(this.node, options, path) } /** * Normalize this element's d attribute. SVGPathElements without * a path data attribute obtain a value of 'M 0 0'. */ normalizePath() { const tagName = this.tagName() if (tagName === 'path') { this.attr('d', Path.normalize(this.attr('d'))) } return this } /** * Returns the bounding box of the element after transformations are applied. * If `withoutTransformations` is `true`, transformations of the element * will not be considered when computing the bounding box. If `target` is * specified, bounding box will be computed relatively to the target element. */ bbox(withoutTransformations?: boolean, target?: SVGElement) { return Dom.bbox(this.node, withoutTransformations, target) } getBBox( options: { target?: SVGElement | Vector | null recursive?: boolean } = {}, ) { return Dom.getBBox(this.node, { recursive: options.recursive, target: options.target ? Vector.toNode(options.target) : null, }) } /** * Samples the underlying SVG element (it currently works only on * paths - where it is most useful anyway). Returns an array of objects * of the form `{ x: Number, y: Number, distance: Number }`. Each of these * objects represent a point on the path. This basically creates a discrete * representation of the path (which is possible a curve). The sampling * interval defines the accuracy of the sampling. In other words, we travel * from the beginning of the path to the end by interval distance (on the * path, not between the resulting points) and collect the discrete points * on the path. This is very useful in many situations. For example, SVG * does not provide a built-in mechanism to find intersections between two * paths. Using sampling, we can just generate bunch of points for each of * the path and find the closest ones from each set. */ sample(interval = 1) { if (this.node instanceof SVGPathElement) { return Dom.sample(this.node, interval) } return [] } toPath() { return Vector.create(Dom.toPath(this.node as any)) } toPathData() { return Dom.toPathData(this.node as any) } } export namespace Vector { export const toStringTag = `X6.${Vector.name}` export function isVector(instance: any): instance is Vector { if (instance == null) { return false } if (instance instanceof Vector) { return true } const tag = instance[Symbol.toStringTag] const vector = instance as Vector if ( (tag == null || tag === toStringTag) && vector.node instanceof SVGElement && typeof vector.animate === 'function' && typeof vector.sample === 'function' && typeof vector.normalizePath === 'function' && typeof vector.toPath === 'function' ) { return true } return false } export function create( elem: Vector | SVGElement | string, attrs?: Dom.Attributes, children?: SVGElement | Vector | (SVGElement | Vector)[], ) { return new Vector(elem, attrs, children) } export function createVectors(markup: string) { if (markup[0] === '<') { const svgDoc = Dom.createSvgDocument(markup) const vels: Vector[] = [] for (let i = 0, ii = svgDoc.childNodes.length; i < ii; i += 1) { const childNode = svgDoc.childNodes[i]! vels.push(create(document.importNode(childNode, true) as SVGElement)) } return vels } return [create(markup)] } export function toNode<T extends SVGElement = SVGElement>( elem: SVGElement | DocumentFragment | Vector, ): T { if (isVector(elem)) { return elem.node as T } return elem as T } export function toNodes( elems: | SVGElement | DocumentFragment | Vector | (SVGElement | DocumentFragment | Vector)[], ) { if (Array.isArray(elems)) { return elems.map((elem) => toNode(elem)) } return [toNode(elems)] } }