UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering

383 lines (326 loc) 8.3 kB
import { Dom, type KeyValue, type Nilable, ObjectExt, Vector } from '../common' import type { SimpleAttrs } from '../registry' export type MarkupSelectors = KeyValue<Element | Element[]> export interface MarkupJSONMarkup { /** * The namespace URI of the element. It defaults to SVG namespace * `"http://www.w3.org/2000/svg"`. */ ns?: string | null /** * The type of element to be created. */ tagName: string /** * A unique selector for targeting the element within the `attr` * cell attribute. */ selector?: string | null /** * A selector for targeting multiple elements within the `attr` * cell attribute. The group selector name must not be the same * as an existing selector name. */ groupSelector?: string | string[] | null attrs?: SimpleAttrs style?: Record<string, string | number> className?: string | string[] children?: MarkupJSONMarkup[] textContent?: string } export interface MarkupParseResult { fragment: DocumentFragment selectors: MarkupSelectors groups: KeyValue<Element[]> } export type MarkupType = string | MarkupJSONMarkup | MarkupJSONMarkup[] function isJSONMarkup(markup?: Nilable<MarkupType>) { return markup != null && !isStringMarkup(markup) } function isStringMarkup(markup?: Nilable<MarkupType>): markup is string { return markup != null && typeof markup === 'string' } function clone(markup?: Nilable<MarkupType>) { return markup == null || isStringMarkup(markup) ? markup : ObjectExt.cloneDeep(markup) } /** * Removes blank space in markup to prevent create empty text node. */ function sanitize(markup: string) { return `${markup}` .trim() .replace(/[\r|\n]/g, ' ') .replace(/>\s+</g, '><') } function parseJSONMarkup( markup: MarkupJSONMarkup | MarkupJSONMarkup[], options: { ns?: string } = { ns: Dom.ns.svg }, ): MarkupParseResult { const fragment = document.createDocumentFragment() const groups: KeyValue<Element[]> = {} const selectors: MarkupSelectors = {} const queue: { markup: MarkupJSONMarkup[] parent: Element | DocumentFragment ns?: string }[] = [ { markup: Array.isArray(markup) ? markup : [markup], parent: fragment, ns: options.ns, }, ] while (queue.length > 0) { const item = queue.pop() let ns = item.ns || Dom.ns.svg const defines = item.markup const parentNode = item.parent defines.forEach((define) => { // tagName const tagName = define.tagName if (!tagName) { throw new TypeError('Invalid tagName') } // ns if (define.ns) { ns = define.ns } const node = ns ? Dom.createElementNS(tagName, ns) : Dom.createElement(tagName) // attrs const attrs = define.attrs if (attrs) { Dom.attr(node, Dom.kebablizeAttrs(attrs)) } // style const style = define.style if (style) { Dom.css(node, style) } // classname const className = define.className if (className != null) { node.setAttribute( 'class', Array.isArray(className) ? className.join(' ') : className, ) } // textContent if (define.textContent) { node.textContent = define.textContent } // selector const selector = define.selector if (selector != null) { if (selectors[selector]) { throw new TypeError('Selector must be unique') } selectors[selector] = node } // group if (define.groupSelector) { let nodeGroups = define.groupSelector if (!Array.isArray(nodeGroups)) { nodeGroups = [nodeGroups] } nodeGroups.forEach((name) => { if (!groups[name]) { groups[name] = [] } groups[name].push(node) }) } parentNode.appendChild(node) // children const children = define.children if (Array.isArray(children)) { queue.push({ ns, markup: children, parent: node }) } }) } Object.keys(groups).forEach((groupName) => { if (selectors[groupName]) { throw new Error('Ambiguous group selector') } selectors[groupName] = groups[groupName] }) return { fragment, selectors, groups } } function createContainer(firstChild: Element) { return firstChild instanceof SVGElement ? Dom.createSvgElement('g') : Dom.createElement('div') } function renderMarkup(markup: MarkupType): { elem?: Element selectors?: MarkupSelectors } { if (isStringMarkup(markup)) { const nodes = Vector.createVectors(markup) const count = nodes.length if (count === 1) { return { elem: nodes[0].node as Element, } } if (count > 1) { const elem = createContainer(nodes[0].node) nodes.forEach((node) => { elem.appendChild(node.node) }) return { elem } } return {} } const result = parseJSONMarkup(markup) const fragment = result.fragment let elem: Element | null = null if (fragment.childNodes.length > 1) { elem = createContainer(fragment.firstChild as Element) elem.appendChild(fragment) } else { elem = fragment.firstChild as Element } return { elem, selectors: result.selectors } } function parseLabelStringMarkup(markup: string) { const children = Vector.createVectors(markup) const fragment = document.createDocumentFragment() for (let i = 0, n = children.length; i < n; i += 1) { const currentChild = children[i].node fragment.appendChild(currentChild) } return { fragment, selectors: {} } } function getSelector( elem: Element, stop: Element, prev?: string, ): string | undefined { if (elem != null) { let selector: string const tagName = elem.tagName.toLowerCase() if (elem === stop) { if (typeof prev === 'string') { selector = `> ${tagName} > ${prev}` } else { selector = `> ${tagName}` } return selector } const parent = elem.parentNode if (parent && parent.childNodes.length > 1) { const nth = Dom.index(elem) + 1 selector = `${tagName}:nth-child(${nth})` } else { selector = tagName } if (prev) { selector += ` > ${prev}` } return getSelector(elem.parentNode as Element, stop, selector) } return prev } function getPortContainerMarkup(): MarkupType { return 'g' } function getPortMarkup(): MarkupType { return { tagName: 'circle', selector: 'circle', attrs: { r: 10, fill: '#FFFFFF', stroke: '#000000', }, } } function getPortLabelMarkup(): MarkupType { return { tagName: 'text', selector: 'text', attrs: { fill: '#000000', }, } } function getEdgeMarkup(): MarkupType { return [ { tagName: 'path', selector: 'wrap', groupSelector: 'lines', attrs: { fill: 'none', cursor: 'pointer', stroke: 'transparent', strokeLinecap: 'round', }, }, { tagName: 'path', selector: 'line', groupSelector: 'lines', attrs: { fill: 'none', pointerEvents: 'none', }, }, ] } function getForeignObjectMarkup(bare = false): MarkupJSONMarkup { return { tagName: 'foreignObject', selector: 'fo', children: [ { ns: Dom.ns.xhtml, tagName: 'body', selector: 'foBody', attrs: { xmlns: Dom.ns.xhtml, }, style: { width: '100%', height: '100%', background: 'transparent', }, children: bare ? [] : [ { tagName: 'div', selector: 'foContent', style: { width: '100%', height: '100%', }, }, ], }, ], } } /** * Markup 所有的方法导出 */ export const Markup = { isJSONMarkup, isStringMarkup, clone, sanitize, parseJSONMarkup, createContainer, renderMarkup, parseLabelStringMarkup, getSelector, getPortContainerMarkup, getPortMarkup, getPortLabelMarkup, getEdgeMarkup, getForeignObjectMarkup, }