@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
366 lines (332 loc) • 9.28 kB
text/typescript
import { Dom, FunctionExt, NumberExt, ObjectExt, Util } from '../../common'
import { Point } from '../../geometry'
import type { Cell, Edge } from '../../model'
import type { CellView, EdgeView, NodeView } from '../../view'
import { ToolItem, type ToolItemOptions } from '../../view/tool'
import { createViewElement } from '../../view/view/util'
export class CellEditor extends ToolItem<
NodeView | EdgeView,
CellEditorOptions & { event: Dom.EventObject }
> {
public static defaults: CellEditorOptions = {
...ToolItem.getDefaults(),
tagName: 'div',
isSVGElement: false,
events: {
mousedown: 'onMouseDown',
touchstart: 'onMouseDown',
},
documentEvents: {
mouseup: 'onDocumentMouseUp',
touchend: 'onDocumentMouseUp',
touchcancel: 'onDocumentMouseUp',
},
}
private editor: HTMLDivElement | null
private labelIndex = -1
private distance = 0.5
private event: Dom.DoubleClickEvent
private dblClick = this.onCellDblClick.bind(this)
onRender() {
const cellView = this.cellView as CellView
if (cellView) {
cellView.on('cell:dblclick', this.dblClick)
}
}
createElement() {
const classNames = [
this.prefixClassName(
`${this.cell.isEdge() ? 'edge' : 'node'}-tool-editor`,
),
this.prefixClassName('cell-tool-editor'),
]
this.editor = createViewElement('div', false) as HTMLDivElement
this.addClass(classNames, this.editor)
this.editor.contentEditable = 'true'
this.container.appendChild(this.editor)
}
removeElement() {
this.undelegateDocumentEvents()
if (this.editor) {
this.container.removeChild(this.editor)
this.editor = null
}
}
updateEditor() {
const { cell, editor } = this
if (!editor) {
return
}
const { style } = editor
if (cell.isNode()) {
this.updateNodeEditorTransform()
} else if (cell.isEdge()) {
this.updateEdgeEditorTransform()
}
// set font style
const { attrs } = this.options
style.fontSize = `${attrs.fontSize}px`
style.fontFamily = attrs.fontFamily
style.color = attrs.color
style.backgroundColor = attrs.backgroundColor
// set init value
const text = this.getCellText() || ''
editor.innerText = text
this.setCellText('') // clear display value when edit status because char ghosting.
return this
}
updateNodeEditorTransform() {
const { graph, cell, editor } = this
if (!editor) {
return
}
let pos = Point.create()
let minWidth = 20
let translate = ''
let { x, y } = this.options
const { width, height } = this.options
if (typeof x !== 'undefined' && typeof y !== 'undefined') {
const bbox = cell.getBBox()
x = NumberExt.normalizePercentage(x, bbox.width)
y = NumberExt.normalizePercentage(y, bbox.height)
pos = bbox.topLeft.translate(x, y)
minWidth = bbox.width - x * 2
} else {
const bbox = cell.getBBox()
pos = bbox.center
minWidth = bbox.width - 4
translate = 'translate(-50%, -50%)'
}
const scale = graph.scale()
const { style } = editor
pos = graph.localToGraph(pos)
style.left = `${pos.x}px`
style.top = `${pos.y}px`
style.transform = `scale(${scale.sx}, ${scale.sy}) ${translate}`
style.minWidth = `${minWidth}px`
if (typeof width === 'number') {
style.width = `${width}px`
}
if (typeof height === 'number') {
style.height = `${height}px`
}
}
updateEdgeEditorTransform() {
if (!this.event) {
return
}
const { graph, editor } = this
if (!editor) {
return
}
let pos = Point.create()
let minWidth = 20
const { style } = editor
const target = this.event.target
const parent = target.parentElement
const isEdgeLabel =
parent && Dom.hasClass(parent, this.prefixClassName('edge-label'))
if (isEdgeLabel) {
const index = parent.getAttribute('data-index') || '0'
this.labelIndex = parseInt(index, 10)
const matrix = parent.getAttribute('transform')
const { translation } = Dom.parseTransformString(matrix)
pos = new Point(translation.tx, translation.ty)
minWidth = Util.getBBox(target).width
} else {
if (!this.options.labelAddable) {
return this
}
pos = graph.clientToLocal(
Point.create(this.event.clientX, this.event.clientY),
)
const view = this.cellView as EdgeView
const d = view.path.closestPointLength(pos)
this.distance = d
this.labelIndex = -1
}
pos = graph.localToGraph(pos)
const scale = graph.scale()
style.left = `${pos.x}px`
style.top = `${pos.y}px`
style.minWidth = `${minWidth}px`
style.transform = `scale(${scale.sx}, ${scale.sy}) translate(-50%, -50%)`
}
updateCell() {
const value = this.editor.innerText.replace(/\n$/, '') || ''
// set value, when value is null, we will remove label in edge
this.setCellText(value !== '' ? value : null)
// remove tool
this.removeElement()
}
onDocumentMouseUp(e: Dom.MouseDownEvent) {
if (this.editor && e.target !== this.editor) {
this.updateCell()
}
}
onCellDblClick({ e }: { e: Dom.DoubleClickEvent }) {
if (!this.editor) {
e.stopPropagation()
this.removeElement()
this.event = e
this.createElement()
this.updateEditor()
this.autoFocus()
const documentEvents = this.options.documentEvents
if (documentEvents) {
this.delegateDocumentEvents(documentEvents)
}
}
}
onMouseDown(e: Dom.MouseDownEvent) {
e.stopPropagation()
}
autoFocus() {
setTimeout(() => {
if (this.editor) {
this.editor.focus()
this.selectText()
}
})
}
selectText() {
if (window.getSelection && this.editor) {
const range = document.createRange()
range.selectNodeContents(this.editor)
const selection = window.getSelection()
selection?.removeAllRanges()
selection?.addRange(range)
}
}
getCellText() {
const { getText } = this.options
if (typeof getText === 'function') {
return FunctionExt.call(getText, this.cellView, {
cell: this.cell,
index: this.labelIndex,
})
}
if (typeof getText === 'string') {
if (this.cell.isNode()) {
return this.cell.attr(getText)
}
if (this.cell.isEdge()) {
if (this.labelIndex !== -1) {
return this.cell.prop(`labels/${this.labelIndex}/attrs/${getText}`)
}
}
}
}
setCellText(value: string | null) {
const setText = this.options.setText
if (typeof setText === 'function') {
FunctionExt.call(setText, this.cellView, {
cell: this.cell,
value,
index: this.labelIndex,
distance: this.distance,
})
return
}
if (typeof setText === 'string') {
if (this.cell.isNode()) {
this.cell.attr(setText, value === null ? '' : value)
return
}
if (this.cell.isEdge()) {
const edge = this.cell as Edge
if (this.labelIndex === -1) {
if (value) {
const newLabel = {
position: {
distance: this.distance,
},
attrs: {},
}
ObjectExt.setByPath(newLabel, `attrs/${setText}`, value)
edge.appendLabel(newLabel)
}
} else {
if (value !== null) {
edge.prop(`labels/${this.labelIndex}/attrs/${setText}`, value)
} else {
edge.removeLabelAt(this.labelIndex)
}
}
}
}
}
protected onRemove() {
const cellView = this.cellView as CellView
if (cellView) {
cellView.off('cell:dblclick', this.dblClick)
}
this.removeElement()
}
}
interface CellEditorOptions extends ToolItemOptions {
x?: number | string
y?: number | string
width?: number
height?: number
attrs: {
fontSize: number
fontFamily: string
color: string
backgroundColor: string
}
labelAddable?: boolean
getText:
| ((
this: CellView,
args: {
cell: Cell
index?: number
},
) => string)
| string
setText:
| ((
this: CellView,
args: {
cell: Cell
value: string | null
index?: number
distance?: number
},
) => void)
| string
}
export class NodeEditor extends CellEditor {
public static defaults: CellEditorOptions = ObjectExt.merge(
{},
CellEditor.defaults,
{
attrs: {
fontSize: 14,
fontFamily: 'Arial, helvetica, sans-serif',
color: '#000',
backgroundColor: '#fff',
},
getText: 'text/text',
setText: 'text/text',
},
)
}
export class EdgeEditor extends CellEditor {
public static defaults: CellEditorOptions = ObjectExt.merge(
{},
CellEditor.defaults,
{
attrs: {
fontSize: 14,
fontFamily: 'Arial, helvetica, sans-serif',
color: '#000',
backgroundColor: '#fff',
},
labelAddable: true,
getText: 'label/text',
setText: 'label/text',
},
)
}