@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
547 lines (465 loc) • 13.1 kB
text/typescript
import {
Basecoat,
CssLoader,
type Dom,
disposable,
isModifierKeyEqual,
isModifierKeyMatch,
type ModifierKey,
} from '../../common'
import type { EventArgs, Graph, GraphPlugin } from '../../graph'
import type { Cell } from '../../model'
import {
SelectionImpl,
type SelectionImplAddOptions,
type SelectionImplCommonOptions,
type SelectionImplContent,
type SelectionImplEventArgs,
type SelectionImplFilter,
type SelectionImplOptions,
type SelectionImplRemoveOptions,
type SelectionImplSetOptions,
} from './selection'
import { content } from './style/raw'
import './api'
export interface SelectionOptions extends SelectionImplCommonOptions {
enabled?: boolean
}
export type SelectionFilter = SelectionImplFilter
export type SelectionContent = SelectionImplContent
export type SelectionSetOptions = SelectionImplSetOptions
export type SelectionAddOptions = SelectionImplAddOptions
export type SelectionRemoveOptions = SelectionImplRemoveOptions
export const DefaultOptions: Partial<SelectionImplOptions> = {
rubberband: false,
rubberNode: true,
rubberEdge: false, // next version will set to true
pointerEvents: 'auto',
multiple: true,
multipleSelectionModifiers: ['ctrl', 'meta'],
movable: true,
strict: false,
selectCellOnMoved: false,
selectNodeOnMoved: false,
selectEdgeOnMoved: false,
following: true,
content: null,
eventTypes: ['leftMouseDown', 'mouseWheelDown'],
}
export class Selection
extends Basecoat<SelectionImplEventArgs>
implements GraphPlugin
{
public name = 'selection'
private graph: Graph
private selectionImpl: SelectionImpl
private readonly options: SelectionOptions
private movedMap = new WeakMap<Cell, boolean>()
private unselectMap = new WeakMap<Cell, boolean>()
get rubberbandDisabled() {
return this.options.enabled !== true || this.options.rubberband !== true
}
get disabled() {
return this.options.enabled !== true
}
get length() {
return this.selectionImpl.length
}
get cells() {
return this.selectionImpl.cells
}
constructor(options: SelectionOptions = {}) {
super()
this.options = {
enabled: true,
...DefaultOptions,
...options,
}
CssLoader.ensure(this.name, content)
}
public init(graph: Graph) {
this.graph = graph
this.selectionImpl = new SelectionImpl({
...this.options,
graph,
})
this.resolvePanningSelectionConflict()
this.setup()
this.startListening()
}
// #region api
isEnabled() {
return !this.disabled
}
enable() {
if (this.disabled) {
this.options.enabled = true
}
}
disable() {
if (!this.disabled) {
this.options.enabled = false
}
}
toggleEnabled(enabled?: boolean) {
if (enabled != null) {
if (enabled !== this.isEnabled()) {
if (enabled) {
this.enable()
} else {
this.disable()
}
}
} else if (this.isEnabled()) {
this.disable()
} else {
this.enable()
}
return this
}
isMultipleSelection() {
return this.isMultiple()
}
enableMultipleSelection() {
this.enableMultiple()
return this
}
disableMultipleSelection() {
this.disableMultiple()
return this
}
toggleMultipleSelection(multiple?: boolean) {
if (multiple != null) {
if (multiple !== this.isMultipleSelection()) {
if (multiple) {
this.enableMultipleSelection()
} else {
this.disableMultipleSelection()
}
}
} else if (this.isMultipleSelection()) {
this.disableMultipleSelection()
} else {
this.enableMultipleSelection()
}
return this
}
isSelectionMovable() {
return this.options.movable !== false
}
enableSelectionMovable() {
this.selectionImpl.options.movable = true
return this
}
disableSelectionMovable() {
this.selectionImpl.options.movable = false
return this
}
toggleSelectionMovable(movable?: boolean) {
if (movable != null) {
if (movable !== this.isSelectionMovable()) {
if (movable) {
this.enableSelectionMovable()
} else {
this.disableSelectionMovable()
}
}
} else if (this.isSelectionMovable()) {
this.disableSelectionMovable()
} else {
this.enableSelectionMovable()
}
return this
}
isRubberbandEnabled() {
return !this.rubberbandDisabled
}
enableRubberband() {
if (this.rubberbandDisabled) {
this.options.rubberband = true
}
return this
}
disableRubberband() {
if (!this.rubberbandDisabled) {
this.options.rubberband = false
}
return this
}
toggleRubberband(enabled?: boolean) {
if (enabled != null) {
if (enabled !== this.isRubberbandEnabled()) {
if (enabled) {
this.enableRubberband()
} else {
this.disableRubberband()
}
}
} else if (this.isRubberbandEnabled()) {
this.disableRubberband()
} else {
this.enableRubberband()
}
return this
}
isStrictRubberband() {
return this.selectionImpl.options.strict === true
}
enableStrictRubberband() {
this.selectionImpl.options.strict = true
return this
}
disableStrictRubberband() {
this.selectionImpl.options.strict = false
return this
}
toggleStrictRubberband(strict?: boolean) {
if (strict != null) {
if (strict !== this.isStrictRubberband()) {
if (strict) {
this.enableStrictRubberband()
} else {
this.disableStrictRubberband()
}
}
} else if (this.isStrictRubberband()) {
this.disableStrictRubberband()
} else {
this.enableStrictRubberband()
}
return this
}
setRubberbandModifiers(modifiers?: string | ModifierKey[] | null) {
this.setModifiers(modifiers)
}
setSelectionFilter(filter?: SelectionFilter) {
this.setFilter(filter)
return this
}
setSelectionDisplayContent(content?: SelectionContent) {
this.setContent(content)
return this
}
isEmpty() {
return this.length <= 0
}
clean(options: SelectionSetOptions = {}) {
this.selectionImpl.clean(options)
return this
}
reset(
cells?: Cell | string | (Cell | string)[],
options: SelectionSetOptions = {},
) {
this.selectionImpl.reset(cells ? this.getCells(cells) : [], options)
return this
}
getSelectedCells() {
return this.cells
}
getSelectedCellCount() {
return this.length
}
isSelected(cell: Cell | string) {
return this.selectionImpl.isSelected(cell)
}
select(
cells: Cell | string | (Cell | string)[],
options: SelectionAddOptions = {},
) {
const selected = this.getCells(cells)
if (selected.length) {
if (this.isMultiple()) {
this.selectionImpl.select(selected, options)
} else {
this.reset(selected.slice(0, 1), options)
}
}
return this
}
unselect(
cells: Cell | string | (Cell | string)[],
options: SelectionRemoveOptions = {},
) {
this.selectionImpl.unselect(this.getCells(cells), options)
return this
}
// #endregion
protected setup() {
this.selectionImpl.on('*', (name, args) => {
this.trigger(name, args)
this.graph.trigger(name, args)
})
}
protected startListening() {
this.graph.on('blank:mousedown', this.onBlankMouseDown, this)
this.graph.on('blank:click', this.onBlankClick, this)
this.graph.on('cell:mousemove', this.onCellMouseMove, this)
this.graph.on('cell:mouseup', this.onCellMouseUp, this)
this.selectionImpl.on('box:mousedown', this.onBoxMouseDown, this)
}
protected stopListening() {
this.graph.off('blank:mousedown', this.onBlankMouseDown, this)
this.graph.off('blank:click', this.onBlankClick, this)
this.graph.off('cell:mousemove', this.onCellMouseMove, this)
this.graph.off('cell:mouseup', this.onCellMouseUp, this)
this.selectionImpl.off('box:mousedown', this.onBoxMouseDown, this)
}
protected onBlankMouseDown({ e }: EventArgs['blank:mousedown']) {
if (!this.allowBlankMouseDown(e)) {
return
}
const allowGraphPanning = this.graph.panning.allowPanning(e, true)
const scroller = this.graph.getPlugin<any>('scroller')
const allowScrollerPanning = scroller && scroller.allowPanning(e, true)
if (
this.allowRubberband(e, true) ||
(this.allowRubberband(e) && !allowScrollerPanning && !allowGraphPanning)
) {
this.startRubberband(e)
}
}
protected allowBlankMouseDown(e: Dom.MouseDownEvent) {
const eventTypes = this.options.eventTypes
const isTouchEvent =
(typeof e.type === 'string' && e.type.startsWith('touch')) ||
e.pointerType === 'touch'
if (isTouchEvent) return eventTypes?.includes('leftMouseDown')
return (
(eventTypes?.includes('leftMouseDown') && e.button === 0) ||
(eventTypes?.includes('mouseWheelDown') && e.button === 1)
)
}
protected onBlankClick() {
this.clean()
}
protected allowRubberband(e: Dom.MouseDownEvent, strict?: boolean) {
const safeEvent =
e ??
({
altKey: false,
ctrlKey: false,
metaKey: false,
shiftKey: false,
} as Dom.EventObject)
return (
!this.rubberbandDisabled &&
isModifierKeyMatch(safeEvent, this.options.modifiers, strict)
)
}
/**
* 当框选和画布拖拽平移触发条件相同时(相同事件 + 相同修饰键),框选优先触发,否则不互相影响。
*/
protected resolvePanningSelectionConflict() {
if (this.options.enabled !== true || this.options.rubberband !== true)
return
const panningOpts = this.graph.options.panning
if (!panningOpts || panningOpts.enabled === false) return
const checkHasConflict = () => {
const selectionEvents = this.options.eventTypes ?? []
const panningEvents = panningOpts.eventTypes ?? []
const panningEventsSet = new Set(panningEvents)
// 判断是否有相同事件类型(eventTypes)
const hasOverlappingEvents = selectionEvents.some((event) =>
panningEventsSet.has(event),
)
// 判断是否有相同修饰键(modifiers)
const hasSameModifiers = isModifierKeyEqual(
panningOpts.modifiers,
this.options.modifiers,
)
return hasOverlappingEvents && hasSameModifiers
}
if (checkHasConflict()) {
this.graph.panning.disablePanning()
}
}
protected allowMultipleSelection(e: Dom.MouseDownEvent | Dom.MouseUpEvent) {
return (
this.isMultiple() &&
isModifierKeyMatch(e, this.options.multipleSelectionModifiers)
)
}
protected onCellMouseMove({ cell }: EventArgs['cell:mousemove']) {
this.movedMap.set(cell, true)
}
protected onCellMouseUp({ e, cell }: EventArgs['cell:mouseup']) {
const options = this.options
let disabled = this.disabled
if (!disabled && this.movedMap.has(cell)) {
disabled = options.selectCellOnMoved === false
if (!disabled) {
disabled = options.selectNodeOnMoved === false && cell.isNode()
}
if (!disabled) {
disabled = options.selectEdgeOnMoved === false && cell.isEdge()
}
}
if (!disabled) {
if (!this.allowMultipleSelection(e)) {
this.reset(cell)
} else if (this.unselectMap.has(cell)) {
this.unselectMap.delete(cell)
} else if (this.isSelected(cell)) {
this.unselect(cell)
} else {
this.select(cell)
}
}
this.movedMap.delete(cell)
}
protected onBoxMouseDown({
e,
cell,
}: SelectionImplEventArgs['box:mousedown']) {
if (!this.disabled && cell) {
if (this.allowMultipleSelection(e)) {
this.unselect(cell)
this.unselectMap.set(cell, true)
}
}
}
protected getCells(cells: Cell | string | (Cell | string)[]) {
return (Array.isArray(cells) ? cells : [cells])
.map((cell) =>
typeof cell === 'string' ? this.graph.getCellById(cell) : cell,
)
.filter((cell) => cell != null)
}
protected startRubberband(e: Dom.MouseDownEvent) {
if (!this.rubberbandDisabled) {
this.selectionImpl.startSelecting(e)
}
return this
}
protected isMultiple() {
return this.options.multiple !== false
}
protected enableMultiple() {
this.options.multiple = true
return this
}
protected disableMultiple() {
this.options.multiple = false
return this
}
protected setModifiers(modifiers?: string | ModifierKey[] | null) {
this.options.modifiers = modifiers
return this
}
protected setContent(content?: SelectionContent) {
this.selectionImpl.setContent(content)
return this
}
protected setFilter(filter?: SelectionFilter) {
this.selectionImpl.setFilter(filter)
return this
}
dispose() {
this.stopListening()
this.off()
this.selectionImpl.dispose()
CssLoader.clean(this.name)
}
}