@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
383 lines (326 loc) • 8.3 kB
text/typescript
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,
}