UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering

547 lines (465 loc) 13.1 kB
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 } @disposable() dispose() { this.stopListening() this.off() this.selectionImpl.dispose() CssLoader.clean(this.name) } }