UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

405 lines 16.6 kB
import { ObjectExt, ArrayExt, Dom, FunctionExt, StringExt, Scheduler, } from '../util'; import { Point } from '../geometry'; import { Dictionary } from '../common'; import { Attr } from '../registry/attr'; import { View } from './view'; export class AttrManager { constructor(view) { this.view = view; } get cell() { return this.view.cell; } getDefinition(attrName) { return this.cell.getAttrDefinition(attrName); } processAttrs(elem, raw) { let normal; let set; let offset; let position; let delay; const specials = []; // 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; } 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; } }); specials.forEach(({ name, definition }) => { const val = raw[name]; const setDefine = definition; 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; if (typeof offsetDefine.offset === 'function') { if (offset == null) { offset = {}; } offset[name] = val; } const positionDefine = definition; if (typeof positionDefine.position === 'function') { if (position == null) { position = {}; } position[name] = val; } }); return { raw, normal, set, offset, position, delay, }; } mergeProcessedAttrs(allProcessedAttrs, roProcessedAttrs) { allProcessedAttrs.set = Object.assign(Object.assign({}, allProcessedAttrs.set), roProcessedAttrs.set); allProcessedAttrs.position = Object.assign(Object.assign({}, allProcessedAttrs.position), roProcessedAttrs.position); allProcessedAttrs.offset = Object.assign(Object.assign({}, 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; } findAttrs(cellAttrs, rootNode, selectorCache, selectors) { const merge = []; const result = 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]; prev.priority = [prev.priority]; } const attributes = prev.attrs; const selectedLength = prev.priority; 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; item.attrs = arr.reduceRight((memo, attrs) => ObjectExt.merge(memo, attrs), {}); }); return result; } updateRelativeAttrs(elem, processedAttrs, refBBox, options) { 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.set, this.view, val, getOptions()); if (typeof ret === 'object') { nodeAttrs = Object.assign(Object.assign({}, 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); 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.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.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.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, attrs, options) { const selectorCache = {}; 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 = []; 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; if (refSelector) { refNode = (selectorCache[refSelector] || this.view.find(refSelector, 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 = new Dictionary(); let rotatableMatrix; specialItems.forEach((item) => { const node = item.node; const refNode = item.refNode; let unrotatedRefBBox; 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); unrotatedRefBBox = refNode ? Dom.getBBox(refNode, { 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); }); } } (function (AttrManager) { AttrManager.CASE_SENSITIVE_ATTR = ['viewBox']; AttrManager.DELAY_ATTRS = [ 'text', 'textWrap', 'sourceMarker', 'targetMarker', ]; })(AttrManager || (AttrManager = {})); //# sourceMappingURL=attr.js.map