UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

595 lines (537 loc) 16.7 kB
import { ObjectExt, ArrayExt, Dom, FunctionExt, StringExt, Scheduler, } from '../util' import { Rectangle, Point } from '../geometry' import { Dictionary } from '../common' import { Attr } from '../registry/attr' import { View } from './view' import { Markup } from './markup' import { CellView } from './cell' export class AttrManager { constructor(protected view: CellView) {} protected get cell() { return this.view.cell } protected getDefinition(attrName: string): Attr.Definition | null { return this.cell.getAttrDefinition(attrName) } protected processAttrs( elem: Element, raw: Attr.ComplexAttrs, ): AttrManager.ProcessedAttrs { let normal: Attr.SimpleAttrs | undefined let set: Attr.ComplexAttrs | undefined let offset: Attr.ComplexAttrs | undefined let position: Attr.ComplexAttrs | undefined let delay: Attr.ComplexAttrs | undefined const specials: { name: string; definition: Attr.Definition }[] = [] // divide the attributes between normal and special Object.keys(raw).forEach((name) => { const val = raw[name] const definition = this.getDefinition(name) const isValid = FunctionExt.call( Attr.isValidDefinition, this.view, definition, val, { elem, attrs: raw, cell: this.cell, view: this.view, }, ) if (definition && isValid) { if (typeof definition === 'string') { if (normal == null) { normal = {} } normal[definition] = val as Attr.SimpleAttrValue } else if (val !== null) { specials.push({ name, definition }) } } else { if (normal == null) { normal = {} } const normalName = AttrManager.CASE_SENSITIVE_ATTR.includes(name) ? name : StringExt.kebabCase(name) normal[normalName] = val as Attr.SimpleAttrValue } }) specials.forEach(({ name, definition }) => { const val = raw[name] const setDefine = definition as Attr.SetDefinition if (typeof setDefine.set === 'function') { if ( !Dom.isHTMLElement(elem) && AttrManager.DELAY_ATTRS.includes(name) ) { if (delay == null) { delay = {} } delay[name] = val } else { if (set == null) { set = {} } set[name] = val } } const offsetDefine = definition as Attr.OffsetDefinition if (typeof offsetDefine.offset === 'function') { if (offset == null) { offset = {} } offset[name] = val } const positionDefine = definition as Attr.PositionDefinition if (typeof positionDefine.position === 'function') { if (position == null) { position = {} } position[name] = val } }) return { raw, normal, set, offset, position, delay, } } protected mergeProcessedAttrs( allProcessedAttrs: AttrManager.ProcessedAttrs, roProcessedAttrs: AttrManager.ProcessedAttrs, ) { allProcessedAttrs.set = { ...allProcessedAttrs.set, ...roProcessedAttrs.set, } allProcessedAttrs.position = { ...allProcessedAttrs.position, ...roProcessedAttrs.position, } allProcessedAttrs.offset = { ...allProcessedAttrs.offset, ...roProcessedAttrs.offset, } // Handle also the special transform property. const transform = allProcessedAttrs.normal && allProcessedAttrs.normal.transform if (transform != null && roProcessedAttrs.normal) { roProcessedAttrs.normal.transform = transform } allProcessedAttrs.normal = roProcessedAttrs.normal } protected findAttrs( cellAttrs: Attr.CellAttrs, rootNode: Element, selectorCache: { [selector: string]: Element[] }, selectors: Markup.Selectors, ) { const merge: Element[] = [] const result: Dictionary< Element, { elem: Element array: boolean priority: number | number[] attrs: Attr.ComplexAttrs | Attr.ComplexAttrs[] } > = new Dictionary() Object.keys(cellAttrs).forEach((selector) => { const attrs = cellAttrs[selector] if (!ObjectExt.isPlainObject(attrs)) { return } const { isCSSSelector, elems } = View.find(selector, rootNode, selectors) selectorCache[selector] = elems for (let i = 0, l = elems.length; i < l; i += 1) { const elem = elems[i] const unique = selectors && selectors[selector] === elem const prev = result.get(elem) if (prev) { if (!prev.array) { merge.push(elem) prev.array = true prev.attrs = [prev.attrs as Attr.ComplexAttrs] prev.priority = [prev.priority as number] } const attributes = prev.attrs as Attr.ComplexAttrs[] const selectedLength = prev.priority as number[] if (unique) { // node referenced by `selector` attributes.unshift(attrs) selectedLength.unshift(-1) } else { // node referenced by `groupSelector` or CSSSelector const sortIndex = ArrayExt.sortedIndex( selectedLength, isCSSSelector ? -1 : l, ) attributes.splice(sortIndex, 0, attrs) selectedLength.splice(sortIndex, 0, l) } } else { result.set(elem, { elem, attrs, priority: unique ? -1 : l, array: false, }) } } }) merge.forEach((node) => { const item = result.get(node)! const arr = item.attrs as Attr.ComplexAttrs[] item.attrs = arr.reduceRight( (memo, attrs) => ObjectExt.merge(memo, attrs), {}, ) }) return result as Dictionary< Element, { elem: Element array: boolean priority: number | number[] attrs: Attr.ComplexAttrs } > } protected updateRelativeAttrs( elem: Element, processedAttrs: AttrManager.ProcessedAttrs, refBBox: Rectangle, options: AttrManager.UpdateOptions, ) { const rawAttrs = processedAttrs.raw || {} let nodeAttrs = processedAttrs.normal || {} const setAttrs = processedAttrs.set const positionAttrs = processedAttrs.position const offsetAttrs = processedAttrs.offset const delayAttrs = processedAttrs.delay const getOptions = () => ({ elem, cell: this.cell, view: this.view, attrs: rawAttrs, refBBox: refBBox.clone(), }) if (setAttrs != null) { Object.keys(setAttrs).forEach((name) => { const val = setAttrs[name] const def = this.getDefinition(name) if (def != null) { const ret = FunctionExt.call( (def as Attr.SetDefinition).set, this.view, val, getOptions(), ) if (typeof ret === 'object') { nodeAttrs = { ...nodeAttrs, ...ret, } } else if (ret != null) { nodeAttrs[name] = ret } } }) } if (Dom.isHTMLElement(elem)) { // TODO: setting the `transform` attribute on HTMLElements // via `node.style.transform = 'matrix(...)';` would introduce // a breaking change (e.g. basic.TextBlock). this.view.setAttrs(nodeAttrs, elem) return } // The final translation of the subelement. const nodeTransform = nodeAttrs.transform const transform = nodeTransform ? `${nodeTransform}` : null const nodeMatrix = Dom.transformStringToMatrix(transform) const nodePosition = new Point(nodeMatrix.e, nodeMatrix.f) if (nodeTransform) { delete nodeAttrs.transform nodeMatrix.e = 0 nodeMatrix.f = 0 } // Calculates node scale determined by the scalable group. let sx = 1 let sy = 1 if (positionAttrs || offsetAttrs) { const scale = this.view.getScaleOfElement( elem, options.scalableNode as SVGElement, ) sx = scale.sx sy = scale.sy } let positioned = false if (positionAttrs != null) { Object.keys(positionAttrs).forEach((name) => { const val = positionAttrs[name] const def = this.getDefinition(name) if (def != null) { const ts = FunctionExt.call( (def as Attr.PositionDefinition).position, this.view, val, getOptions(), ) if (ts != null) { positioned = true nodePosition.translate(Point.create(ts).scale(sx, sy)) } } }) } // The node bounding box could depend on the `size` // set from the previous loop. this.view.setAttrs(nodeAttrs, elem) let offseted = false if (offsetAttrs != null) { // Check if the node is visible const nodeBoundingRect = this.view.getBoundingRectOfElement(elem) if (nodeBoundingRect.width > 0 && nodeBoundingRect.height > 0) { const nodeBBox = Dom.transformRectangle( nodeBoundingRect, nodeMatrix, ).scale(1 / sx, 1 / sy) Object.keys(offsetAttrs).forEach((name) => { const val = offsetAttrs[name] const def = this.getDefinition(name) if (def != null) { const ts = FunctionExt.call( (def as Attr.OffsetDefinition).offset, this.view, val, { elem, cell: this.cell, view: this.view, attrs: rawAttrs, refBBox: nodeBBox, }, ) if (ts != null) { offseted = true nodePosition.translate(Point.create(ts).scale(sx, sy)) } } }) } } if (nodeTransform != null || positioned || offseted) { nodePosition.round(1) nodeMatrix.e = nodePosition.x nodeMatrix.f = nodePosition.y elem.setAttribute('transform', Dom.matrixToTransformString(nodeMatrix)) } // delay render const updateDelayAttrs = () => { if (delayAttrs != null) { Object.keys(delayAttrs).forEach((name) => { const val = delayAttrs[name] const def = this.getDefinition(name) if (def != null) { const ret = FunctionExt.call( (def as Attr.SetDefinition).set, this.view, val, getOptions(), ) if (typeof ret === 'object') { this.view.setAttrs(ret, elem) } else if (ret != null) { this.view.setAttrs( { [name]: ret, }, elem, ) } } }) } } if (options.forceSync) { updateDelayAttrs() } else { Scheduler.scheduleTask(updateDelayAttrs) } } update( rootNode: Element, attrs: Attr.CellAttrs, options: AttrManager.UpdateOptions, ) { const selectorCache: { [selector: string]: Element[] } = {} const nodesAttrs = this.findAttrs( options.attrs || attrs, rootNode, selectorCache, options.selectors, ) // `nodesAttrs` are different from all attributes, when // rendering only attributes sent to this method. const nodesAllAttrs = options.attrs ? this.findAttrs(attrs, rootNode, selectorCache, options.selectors) : nodesAttrs const specialItems: { node: Element refNode: Element | null attributes: Attr.ComplexAttrs | null processedAttributes: AttrManager.ProcessedAttrs }[] = [] nodesAttrs.each((data) => { const node = data.elem const nodeAttrs = data.attrs const processed = this.processAttrs(node, nodeAttrs) if ( processed.set == null && processed.position == null && processed.offset == null && processed.delay == null ) { this.view.setAttrs(processed.normal, node) } else { const data = nodesAllAttrs.get(node) const nodeAllAttrs = data ? data.attrs : null const refSelector = nodeAllAttrs && nodeAttrs.ref == null ? nodeAllAttrs.ref : nodeAttrs.ref let refNode: Element | null if (refSelector) { refNode = (selectorCache[refSelector as string] || this.view.find( refSelector as string, rootNode, options.selectors, ))[0] if (!refNode) { throw new Error(`"${refSelector}" reference does not exist.`) } } else { refNode = null } const item = { node, refNode, attributes: nodeAllAttrs, processedAttributes: processed, } // If an element in the list is positioned relative to this one, then // we want to insert this one before it in the list. const index = specialItems.findIndex((item) => item.refNode === node) if (index > -1) { specialItems.splice(index, 0, item) } else { specialItems.push(item) } } }) const bboxCache: Dictionary<Element, Rectangle> = new Dictionary() let rotatableMatrix: DOMMatrix specialItems.forEach((item) => { const node = item.node const refNode = item.refNode let unrotatedRefBBox: Rectangle | undefined const isRefNodeRotatable = refNode != null && options.rotatableNode != null && Dom.contains(options.rotatableNode, refNode) // Find the reference element bounding box. If no reference was // provided, we use the optional bounding box. if (refNode) { unrotatedRefBBox = bboxCache.get(refNode) } if (!unrotatedRefBBox) { const target = ( isRefNodeRotatable ? options.rotatableNode! : rootNode ) as SVGElement unrotatedRefBBox = refNode ? Dom.getBBox(refNode as SVGElement, { target }) : options.rootBBox if (refNode) { bboxCache.set(refNode, unrotatedRefBBox!) } } let processedAttrs if (options.attrs && item.attributes) { // If there was a special attribute affecting the position amongst // passed-in attributes we have to merge it with the rest of the // element's attributes as they are necessary to update the position // relatively (i.e `ref-x` && 'ref-dx'). processedAttrs = this.processAttrs(node, item.attributes) this.mergeProcessedAttrs(processedAttrs, item.processedAttributes) } else { processedAttrs = item.processedAttributes } let refBBox = unrotatedRefBBox! if ( isRefNodeRotatable && options.rotatableNode != null && !options.rotatableNode.contains(node) ) { // If the referenced node is inside the rotatable group while the // updated node is outside, we need to take the rotatable node // transformation into account. if (!rotatableMatrix) { rotatableMatrix = Dom.transformStringToMatrix( Dom.attr(options.rotatableNode, 'transform'), ) } refBBox = Dom.transformRectangle(unrotatedRefBBox!, rotatableMatrix) } const caller = specialItems.find((item) => item.refNode === node) if (caller) { options.forceSync = true } this.updateRelativeAttrs(node, processedAttrs, refBBox, options) }) } } export namespace AttrManager { export interface UpdateOptions { rootBBox: Rectangle selectors: Markup.Selectors scalableNode?: Element | null rotatableNode?: Element | null /** * Rendering only the specified attributes. */ attrs?: Attr.CellAttrs | null /** * Whether to force synchronous rendering */ forceSync?: boolean } export interface ProcessedAttrs { raw: Attr.ComplexAttrs normal?: Attr.SimpleAttrs | undefined set?: Attr.ComplexAttrs | undefined offset?: Attr.ComplexAttrs | undefined position?: Attr.ComplexAttrs | undefined delay?: Attr.ComplexAttrs | undefined } export const CASE_SENSITIVE_ATTR = ['viewBox'] export const DELAY_ATTRS = [ 'text', 'textWrap', 'sourceMarker', 'targetMarker', ] }