UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

1,217 lines (1,059 loc) 33.7 kB
import { Util } from '../../global' import { KeyValue } from '../../types' import { Rectangle, Angle, Point } from '../../geometry' import { ObjectExt, StringExt, FunctionExt } from '../../util' import { Cell } from '../../model/cell' import { Node } from '../../model/node' import { Edge } from '../../model/edge' import { Model } from '../../model/model' import { Collection } from '../../model/collection' import { View } from '../../view/view' import { CellView } from '../../view/cell' import { NodeView } from '../../view/node' import { Graph } from '../../graph/graph' import { Renderer } from '../../graph/renderer' import { notify } from '../transform/util' import { Handle } from '../common' export class Selection extends View<Selection.EventArgs> { public readonly options: Selection.Options protected readonly collection: Collection protected $container: JQuery<HTMLElement> protected $selectionContainer: JQuery<HTMLElement> protected $selectionContent: JQuery<HTMLElement> protected boxCount: number protected boxesUpdated: boolean public get graph() { return this.options.graph } protected get boxClassName() { return this.prefixClassName(Private.classNames.box) } protected get $boxes() { return this.$container.children(`.${this.boxClassName}`) } protected get handleOptions() { return this.options } constructor(options: Selection.Options) { super() this.options = ObjectExt.merge({}, Private.defaultOptions, options) if (this.options.model) { this.options.collection = this.options.model.collection } if (this.options.collection) { this.collection = this.options.collection } else { this.collection = new Collection([], { comparator: Private.depthComparator, }) this.options.collection = this.collection } this.boxCount = 0 this.createContainer() this.initHandles() this.startListening() } protected startListening() { const graph = this.graph const collection = this.collection this.delegateEvents( { [`mousedown .${this.boxClassName}`]: 'onSelectionBoxMouseDown', [`touchstart .${this.boxClassName}`]: 'onSelectionBoxMouseDown', }, true, ) graph.on('scale', this.onGraphTransformed, this) graph.on('translate', this.onGraphTransformed, this) graph.model.on('updated', this.onModelUpdated, this) collection.on('added', this.onCellAdded, this) collection.on('removed', this.onCellRemoved, this) collection.on('reseted', this.onReseted, this) collection.on('updated', this.onCollectionUpdated, this) collection.on('node:change:position', this.onNodePositionChanged, this) collection.on('cell:changed', this.onCellChanged, this) } protected stopListening() { const graph = this.graph const collection = this.collection this.undelegateEvents() graph.off('scale', this.onGraphTransformed, this) graph.off('translate', this.onGraphTransformed, this) graph.model.off('updated', this.onModelUpdated, this) collection.off('added', this.onCellAdded, this) collection.off('removed', this.onCellRemoved, this) collection.off('reseted', this.onReseted, this) collection.off('updated', this.onCollectionUpdated, this) collection.off('node:change:position', this.onNodePositionChanged, this) collection.off('cell:changed', this.onCellChanged, this) } protected onRemove() { this.stopListening() } protected onGraphTransformed() { this.updateSelectionBoxes({ async: false }) } protected onCellChanged() { this.updateSelectionBoxes() } protected translating: boolean protected onNodePositionChanged({ node, options, }: Collection.EventArgs['node:change:position']) { const { showNodeSelectionBox, pointerEvents } = this.options const { ui, selection } = options let allowTranslating = !this.translating /* Scenarios where this method is not called: * 1. ShowNodeSelection is true or ponterEvents is none * 2. Avoid circular calls with the selection tag */ allowTranslating = allowTranslating && (showNodeSelectionBox !== true || pointerEvents === 'none') allowTranslating = allowTranslating && ui && !selection if (allowTranslating) { this.translating = true const current = node.position() const previous = node.previous('position')! const dx = current.x - previous.x const dy = current.y - previous.y if (dx !== 0 || dy !== 0) { this.translateSelectedNodes(dx, dy, node, options) } this.translating = false } } protected onModelUpdated({ removed }: Collection.EventArgs['updated']) { if (removed && removed.length) { this.unselect(removed) } } isEmpty() { return this.length <= 0 } isSelected(cell: Cell | string) { return this.collection.has(cell) } get length() { return this.collection.length } get cells() { return this.collection.toArray() } select(cells: Cell | Cell[], options: Collection.AddOptions = {}) { options.dryrun = true const items = this.filter(Array.isArray(cells) ? cells : [cells]) this.collection.add(items, options) return this } unselect(cells: Cell | Cell[], options: Collection.RemoveOptions = {}) { // dryrun to prevent cell be removed from graph options.dryrun = true this.collection.remove(Array.isArray(cells) ? cells : [cells], options) return this } reset(cells?: Cell | Cell[], options: Collection.SetOptions = {}) { if (cells) { const prev = this.cells const next = this.filter(Array.isArray(cells) ? cells : [cells]) const prevMap: KeyValue<Cell> = {} const nextMap: KeyValue<Cell> = {} prev.forEach((cell) => (prevMap[cell.id] = cell)) next.forEach((cell) => (nextMap[cell.id] = cell)) const added: Cell[] = [] const removed: Cell[] = [] next.forEach((cell) => { if (!prevMap[cell.id]) { added.push(cell) } }) prev.forEach((cell) => { if (!nextMap[cell.id]) { removed.push(cell) } }) if (removed.length) { this.unselect(removed, { ...options, ui: true }) } if (added.length) { this.select(added, { ...options, ui: true }) } if (removed.length === 0 && added.length === 0) { this.updateContainer() } return this } return this.clean(options) } clean(options: Collection.SetOptions = {}) { if (this.length) { this.collection.reset([], { ...options, ui: true }) } return this } setFilter(filter?: Selection.Filter) { this.options.filter = filter } setContent(content?: Selection.Content) { this.options.content = content } startSelecting(evt: JQuery.MouseDownEvent) { // Flow: startSelecting => adjustSelection => stopSelecting evt = this.normalizeEvent(evt) // eslint-disable-line this.clean() let x let y const graphContainer = this.graph.container if ( evt.offsetX != null && evt.offsetY != null && graphContainer.contains(evt.target) ) { x = evt.offsetX y = evt.offsetY } else { const offset = this.$(graphContainer).offset()! const scrollLeft = graphContainer.scrollLeft const scrollTop = graphContainer.scrollTop x = evt.clientX - offset.left + window.pageXOffset + scrollLeft y = evt.clientY - offset.top + window.pageYOffset + scrollTop } this.$container.css({ top: y, left: x, width: 1, height: 1, }) this.setEventData<EventData.Selecting>(evt, { action: 'selecting', clientX: evt.clientX, clientY: evt.clientY, offsetX: x, offsetY: y, scrollerX: 0, scrollerY: 0, }) this.delegateDocumentEvents(Private.documentEvents, evt.data) } filter(cells: Cell[]) { const filter = this.options.filter if (Array.isArray(filter)) { return cells.filter( (cell) => !filter.includes(cell) && !filter.includes(cell.shape), ) } if (typeof filter === 'function') { return cells.filter((cell) => FunctionExt.call(filter, this.graph, cell)) } return cells } protected stopSelecting(evt: JQuery.MouseUpEvent) { const graph = this.graph const eventData = this.getEventData<EventData.Common>(evt) const action = eventData.action switch (action) { case 'selecting': { let width = this.$container.width()! let height = this.$container.height()! const offset = this.$container.offset()! const origin = graph.pageToLocal(offset.left, offset.top) const scale = graph.transform.getScale() width /= scale.sx height /= scale.sy const rect = new Rectangle(origin.x, origin.y, width, height) const cells = this.getCellViewsInArea(rect).map((view) => view.cell) this.reset(cells) this.hideRubberband() break } case 'translating': { const client = graph.snapToGrid(evt.clientX, evt.clientY) if (!this.options.following) { const data = eventData as EventData.Translating this.updateSelectedNodesPosition({ dx: data.clientX - data.originX, dy: data.clientY - data.originY, }) } this.graph.model.stopBatch('move-selection') this.notifyBoxEvent('box:mouseup', evt, client.x, client.y) break } default: { this.clean() break } } } protected onMouseUp(evt: JQuery.MouseUpEvent) { const action = this.getEventData<EventData.Common>(evt).action if (action) { this.stopSelecting(evt) this.undelegateDocumentEvents() } } protected onSelectionBoxMouseDown(evt: JQuery.MouseDownEvent) { // evt.stopPropagation() const e = this.normalizeEvent(evt) if (this.options.movable) { this.startTranslating(e) } const activeView = this.getCellViewFromElem(e.target)! this.setEventData<EventData.SelectionBox>(e, { activeView }) const client = this.graph.snapToGrid(e.clientX, e.clientY) this.notifyBoxEvent('box:mousedown', e, client.x, client.y) this.delegateDocumentEvents(Private.documentEvents, e.data) } protected startTranslating(evt: JQuery.MouseDownEvent) { this.graph.model.startBatch('move-selection') const client = this.graph.snapToGrid(evt.clientX, evt.clientY) this.setEventData<EventData.Translating>(evt, { action: 'translating', clientX: client.x, clientY: client.y, originX: client.x, originY: client.y, }) } protected getSelectionOffset(client: Point, data: EventData.Translating) { let dx = client.x - data.clientX let dy = client.y - data.clientY const restrict = this.graph.hook.getRestrictArea() if (restrict) { const cells = this.collection.toArray() const totalBBox = Cell.getCellsBBox(cells, { deep: true }) || Rectangle.create() const minDx = restrict.x - totalBBox.x const minDy = restrict.y - totalBBox.y const maxDx = restrict.x + restrict.width - (totalBBox.x + totalBBox.width) const maxDy = restrict.y + restrict.height - (totalBBox.y + totalBBox.height) if (dx < minDx) { dx = minDx } if (dy < minDy) { dy = minDy } if (maxDx < dx) { dx = maxDx } if (maxDy < dy) { dy = maxDy } if (!this.options.following) { const offsetX = client.x - data.originX const offsetY = client.y - data.originY dx = offsetX <= minDx || offsetX >= maxDx ? 0 : dx dy = offsetY <= minDy || offsetY >= maxDy ? 0 : dy } } return { dx, dy, } } protected updateSelectedNodesPosition(offset: { dx: number; dy: number }) { const { dx, dy } = offset if (dx || dy) { if ((this.translateSelectedNodes(dx, dy), this.boxesUpdated)) { if (this.collection.length > 1) { this.updateSelectionBoxes() } } else { const scale = this.graph.transform.getScale() this.$boxes.add(this.$selectionContainer).css({ left: `+=${dx * scale.sx}`, top: `+=${dy * scale.sy}`, }) } } } protected autoScrollGraph(x: number, y: number) { const scroller = this.graph.scroller.widget if (scroller) { return scroller.autoScroll(x, y) } return { scrollerX: 0, scrollerY: 0 } } protected adjustSelection(evt: JQuery.MouseMoveEvent) { const e = this.normalizeEvent(evt) const eventData = this.getEventData<EventData.Common>(e) const action = eventData.action switch (action) { case 'selecting': { const data = eventData as EventData.Selecting if (data.moving !== true) { this.$container.appendTo(this.graph.container) this.showRubberband() data.moving = true } const { scrollerX, scrollerY } = this.autoScrollGraph( e.clientX, e.clientY, ) data.scrollerX += scrollerX data.scrollerY += scrollerY const dx = e.clientX - data.clientX + data.scrollerX const dy = e.clientY - data.clientY + data.scrollerY const left = parseInt(this.$container.css('left'), 10) const top = parseInt(this.$container.css('top'), 10) this.$container.css({ left: dx < 0 ? data.offsetX + dx : left, top: dy < 0 ? data.offsetY + dy : top, width: Math.abs(dx), height: Math.abs(dy), }) break } case 'translating': { const client = this.graph.snapToGrid(e.clientX, e.clientY) const data = eventData as EventData.Translating const offset = this.getSelectionOffset(client, data) if (this.options.following) { this.updateSelectedNodesPosition(offset) } else { this.updateContainerPosition(offset) } if (offset.dx) { data.clientX = client.x } if (offset.dy) { data.clientY = client.y } this.notifyBoxEvent('box:mousemove', evt, client.x, client.y) break } default: break } this.boxesUpdated = false } protected translateSelectedNodes( dx: number, dy: number, exclude?: Cell, otherOptions?: KeyValue, ) { const map: { [id: string]: boolean } = {} const excluded: Cell[] = [] if (exclude) { map[exclude.id] = true } this.collection.toArray().forEach((cell) => { cell.getDescendants({ deep: true }).forEach((child) => { map[child.id] = true }) }) if (otherOptions && otherOptions.translateBy) { const currentCell = this.graph.getCellById(otherOptions.translateBy) if (currentCell) { map[currentCell.id] = true currentCell.getDescendants({ deep: true }).forEach((child) => { map[child.id] = true }) excluded.push(currentCell) } } this.collection.toArray().forEach((cell) => { if (!map[cell.id]) { const options = { ...otherOptions, selection: this.cid, exclude: excluded, } cell.translate(dx, dy, options) this.graph.model.getConnectedEdges(cell).forEach((edge) => { if (!map[edge.id]) { edge.translate(dx, dy, options) map[edge.id] = true } }) } }) } protected getCellViewsInArea(rect: Rectangle) { const graph = this.graph const options = { strict: this.options.strict, } let views: CellView[] = [] if (this.options.rubberNode) { if (this.options.useCellGeometry) { views = views.concat( graph.model .getNodesInArea(rect, options) .map((node) => graph.renderer.findViewByCell(node)) .filter((view) => view != null) as CellView[], ) } else { views = views.concat(graph.renderer.findViewsInArea(rect, options)) } } if (this.options.rubberEdge) { if (this.options.useCellGeometry) { views = views.concat( graph.model .getEdgesInArea(rect, options) .map((edge) => graph.renderer.findViewByCell(edge)) .filter((view) => view != null) as CellView[], ) } else { views = views.concat(graph.renderer.findEdgeViewsInArea(rect, options)) } } return views } protected notifyBoxEvent< K extends keyof Selection.BoxEventArgs, T extends JQuery.TriggeredEvent, >(name: K, e: T, x: number, y: number) { const data = this.getEventData<EventData.SelectionBox>(e) const view = data.activeView this.trigger(name, { e, view, x, y, cell: view.cell }) } protected getSelectedClassName(cell: Cell) { return this.prefixClassName(`${cell.isNode() ? 'node' : 'edge'}-selected`) } protected addCellSelectedClassName(cell: Cell) { const view = this.graph.renderer.findViewByCell(cell) if (view) { view.addClass(this.getSelectedClassName(cell)) } } protected removeCellUnSelectedClassName(cell: Cell) { const view = this.graph.renderer.findViewByCell(cell) if (view) { view.removeClass(this.getSelectedClassName(cell)) } } protected destroySelectionBox(cell: Cell) { this.removeCellUnSelectedClassName(cell) if (this.canShowSelectionBox(cell)) { this.$container.find(`[data-cell-id="${cell.id}"]`).remove() if (this.$boxes.length === 0) { this.hide() } this.boxCount = Math.max(0, this.boxCount - 1) } } protected destroyAllSelectionBoxes(cells: Cell[]) { cells.forEach((cell) => this.removeCellUnSelectedClassName(cell)) this.hide() this.$boxes.remove() this.boxCount = 0 } hide() { this.$container .removeClass(this.prefixClassName(Private.classNames.rubberband)) .removeClass(this.prefixClassName(Private.classNames.selected)) } protected showRubberband() { this.$container.addClass( this.prefixClassName(Private.classNames.rubberband), ) } protected hideRubberband() { this.$container.removeClass( this.prefixClassName(Private.classNames.rubberband), ) } protected showSelected() { this.$container .removeAttr('style') .addClass(this.prefixClassName(Private.classNames.selected)) } protected createContainer() { this.container = document.createElement('div') this.$container = this.$(this.container) this.$container.addClass(this.prefixClassName(Private.classNames.root)) if (this.options.className) { this.$container.addClass(this.options.className) } this.$selectionContainer = this.$('<div/>').addClass( this.prefixClassName(Private.classNames.inner), ) this.$selectionContent = this.$('<div/>').addClass( this.prefixClassName(Private.classNames.content), ) this.$selectionContainer.append(this.$selectionContent) this.$selectionContainer.attr( 'data-selection-length', this.collection.length, ) this.$container.prepend(this.$selectionContainer) this.$handleContainer = this.$selectionContainer } protected updateContainerPosition(offset: { dx: number; dy: number }) { if (offset.dx || offset.dy) { this.$selectionContainer.css({ left: `+=${offset.dx}`, top: `+=${offset.dy}`, }) } } protected updateContainer() { const origin = { x: Infinity, y: Infinity } const corner = { x: 0, y: 0 } const cells = this.collection .toArray() .filter((cell) => this.canShowSelectionBox(cell)) cells.forEach((cell) => { const view = this.graph.renderer.findViewByCell(cell) if (view) { const bbox = view.getBBox({ useCellGeometry: this.options.useCellGeometry, }) origin.x = Math.min(origin.x, bbox.x) origin.y = Math.min(origin.y, bbox.y) corner.x = Math.max(corner.x, bbox.x + bbox.width) corner.y = Math.max(corner.y, bbox.y + bbox.height) } }) this.$selectionContainer .css({ position: 'absolute', pointerEvents: 'none', left: origin.x, top: origin.y, width: corner.x - origin.x, height: corner.y - origin.y, }) .attr('data-selection-length', this.collection.length) const boxContent = this.options.content if (boxContent) { if (typeof boxContent === 'function') { const content = FunctionExt.call( boxContent, this.graph, this, this.$selectionContent[0], ) if (content) { this.$selectionContent.html(content) } } else { this.$selectionContent.html(boxContent) } } if (this.collection.length > 0 && !this.container.parentNode) { this.$container.appendTo(this.graph.container) } else if (this.collection.length <= 0 && this.container.parentNode) { this.container.parentNode.removeChild(this.container) } } protected canShowSelectionBox(cell: Cell) { return ( (cell.isNode() && this.options.showNodeSelectionBox === true) || (cell.isEdge() && this.options.showEdgeSelectionBox === true) ) } protected createSelectionBox(cell: Cell) { this.addCellSelectedClassName(cell) if (this.canShowSelectionBox(cell)) { const view = this.graph.renderer.findViewByCell(cell) if (view) { const bbox = view.getBBox({ useCellGeometry: this.options.useCellGeometry, }) const className = this.boxClassName this.$('<div/>') .addClass(className) .addClass(`${className}-${cell.isNode() ? 'node' : 'edge'}`) .attr('data-cell-id', cell.id) .css({ position: 'absolute', left: bbox.x, top: bbox.y, width: bbox.width, height: bbox.height, pointerEvents: this.options.pointerEvents || 'auto', }) .appendTo(this.container) this.showSelected() this.boxCount += 1 } } } protected updateSelectionBoxes( options: Renderer.RequestViewUpdateOptions = {}, ) { if (this.collection.length > 0) { this.boxesUpdated = true this.graph.renderer.requestViewUpdate(this as any, 1, 2, options) } } confirmUpdate() { if (this.boxCount) { this.hide() this.$boxes.each((_, elem) => { const cellId = this.$(elem).remove().attr('data-cell-id') const cell = this.collection.get(cellId) if (cell) { this.createSelectionBox(cell) } }) this.updateContainer() } return 0 } protected getCellViewFromElem(elem: Element) { const id = elem.getAttribute('data-cell-id') if (id) { const cell = this.collection.get(id) if (cell) { return this.graph.renderer.findViewByCell(cell) } } return null } protected onCellRemoved({ cell }: Collection.EventArgs['removed']) { this.destroySelectionBox(cell) this.updateContainer() } protected onReseted({ previous, current }: Collection.EventArgs['reseted']) { this.destroyAllSelectionBoxes(previous) current.forEach((cell) => { this.listenCellRemoveEvent(cell) this.createSelectionBox(cell) }) this.updateContainer() } protected onCellAdded({ cell }: Collection.EventArgs['added']) { // The collection do not known the cell was removed when cell was // removed by interaction(such as, by "delete" shortcut), so we should // manually listen to cell's remove evnet. this.listenCellRemoveEvent(cell) this.createSelectionBox(cell) this.updateContainer() } protected listenCellRemoveEvent(cell: Cell) { cell.off('removed', this.onCellRemoved, this) cell.on('removed', this.onCellRemoved, this) } protected onCollectionUpdated({ added, removed, options, }: Collection.EventArgs['updated']) { added.forEach((cell) => { this.trigger('cell:selected', { cell, options }) this.graph.trigger('cell:selected', { cell, options }) if (cell.isNode()) { this.trigger('node:selected', { cell, options, node: cell }) this.graph.trigger('node:selected', { cell, options, node: cell }) } else if (cell.isEdge()) { this.trigger('edge:selected', { cell, options, edge: cell }) this.graph.trigger('edge:selected', { cell, options, edge: cell }) } }) removed.forEach((cell) => { this.trigger('cell:unselected', { cell, options }) this.graph.trigger('cell:unselected', { cell, options }) if (cell.isNode()) { this.trigger('node:unselected', { cell, options, node: cell }) this.graph.trigger('node:unselected', { cell, options, node: cell }) } else if (cell.isEdge()) { this.trigger('edge:unselected', { cell, options, edge: cell }) this.graph.trigger('edge:unselected', { cell, options, edge: cell }) } }) const args = { added, removed, options, selected: this.cells, } this.trigger('selection:changed', args) this.graph.trigger('selection:changed', args) } // #region handle protected deleteSelectedCells() { const cells = this.collection.toArray() this.clean() this.graph.model.removeCells(cells, { selection: this.cid }) } protected startRotate({ e }: Handle.EventArgs) { const cells = this.collection.toArray() const center = Cell.getCellsBBox(cells)!.getCenter() const client = this.graph.snapToGrid(e.clientX!, e.clientY!) const angles = cells.reduce<{ [id: string]: number }>( (memo, cell: Node) => { memo[cell.id] = Angle.normalize(cell.getAngle()) return memo }, {}, ) this.setEventData<EventData.Rotation>(e, { center, angles, start: client.theta(center), }) } protected doRotate({ e }: Handle.EventArgs) { const data = this.getEventData<EventData.Rotation>(e) const grid = this.graph.options.rotating.grid const gridSize = typeof grid === 'function' ? FunctionExt.call(grid, this.graph, null as any) : grid const client = this.graph.snapToGrid(e.clientX!, e.clientY!) const delta = data.start - client.theta(data.center) if (!data.rotated) { data.rotated = true } if (Math.abs(delta) > 0.001) { this.collection.toArray().forEach((node: Node) => { const angle = Util.snapToGrid( data.angles[node.id] + delta, gridSize || 15, ) node.rotate(angle, { absolute: true, center: data.center, selection: this.cid, }) }) this.updateSelectionBoxes() } } protected stopRotate({ e }: Handle.EventArgs) { const data = this.getEventData<EventData.Rotation>(e) if (data.rotated) { data.rotated = false this.collection.toArray().forEach((node: Node) => { notify( 'node:rotated', e as JQuery.MouseUpEvent, this.graph.findViewByCell(node) as NodeView, ) }) } } protected startResize({ e }: Handle.EventArgs) { const gridSize = this.graph.getGridSize() const cells = this.collection.toArray() const bbox = Cell.getCellsBBox(cells)! const bboxes = cells.map((cell) => cell.getBBox()) const maxWidth = bboxes.reduce((maxWidth, bbox) => { return bbox.width < maxWidth ? bbox.width : maxWidth }, Infinity) const maxHeight = bboxes.reduce((maxHeight, bbox) => { return bbox.height < maxHeight ? bbox.height : maxHeight }, Infinity) this.setEventData<EventData.Resizing>(e, { bbox, cells: this.graph.model.getSubGraph(cells), minWidth: (gridSize * bbox.width) / maxWidth, minHeight: (gridSize * bbox.height) / maxHeight, }) } protected doResize({ e, dx, dy }: Handle.EventArgs) { const data = this.eventData<EventData.Resizing>(e) const bbox = data.bbox const width = bbox.width const height = bbox.height const newWidth = Math.max(width + dx, data.minWidth) const newHeight = Math.max(height + dy, data.minHeight) if (!data.resized) { data.resized = true } if ( Math.abs(width - newWidth) > 0.001 || Math.abs(height - newHeight) > 0.001 ) { this.graph.model.resizeCells(newWidth, newHeight, data.cells, { selection: this.cid, }) bbox.width = newWidth bbox.height = newHeight this.updateSelectionBoxes() } } protected stopResize({ e }: Handle.EventArgs) { const data = this.eventData<EventData.Resizing>(e) if (data.resized) { data.resized = false this.collection.toArray().forEach((node: Node) => { notify( 'node:resized', e as JQuery.MouseUpEvent, this.graph.findViewByCell(node) as NodeView, ) }) } } // #endregion @View.dispose() dispose() { this.clean() this.remove() } } export namespace Selection { export interface CommonOptions extends Handle.Options { model?: Model collection?: Collection className?: string strict?: boolean filter?: Filter showEdgeSelectionBox?: boolean showNodeSelectionBox?: boolean movable?: boolean following?: boolean useCellGeometry?: boolean content?: Content // Can select node or edge when rubberband rubberNode?: boolean rubberEdge?: boolean // Whether to respond event on the selectionBox pointerEvents?: 'none' | 'auto' } export interface Options extends CommonOptions { graph: Graph } export type Content = | null | false | string | (( this: Graph, selection: Selection, contentElement: HTMLElement, ) => string) export type Filter = | null | (string | { id: string })[] | ((this: Graph, cell: Cell) => boolean) } export namespace Selection { interface SelectionBoxEventArgs<T> { e: T view: CellView cell: Cell x: number y: number } export interface BoxEventArgs { 'box:mousedown': SelectionBoxEventArgs<JQuery.MouseDownEvent> 'box:mousemove': SelectionBoxEventArgs<JQuery.MouseMoveEvent> 'box:mouseup': SelectionBoxEventArgs<JQuery.MouseUpEvent> } export interface SelectionEventArgs { 'cell:selected': { cell: Cell; options: Model.SetOptions } 'node:selected': { cell: Cell; node: Node; options: Model.SetOptions } 'edge:selected': { cell: Cell; edge: Edge; options: Model.SetOptions } 'cell:unselected': { cell: Cell; options: Model.SetOptions } 'node:unselected': { cell: Cell; node: Node; options: Model.SetOptions } 'edge:unselected': { cell: Cell; edge: Edge; options: Model.SetOptions } 'selection:changed': { added: Cell[] removed: Cell[] selected: Cell[] options: Model.SetOptions } } export interface EventArgs extends BoxEventArgs, SelectionEventArgs {} } export interface Selection extends Handle {} ObjectExt.applyMixins(Selection, Handle) // private // ------- namespace Private { const base = 'widget-selection' export const classNames = { root: base, inner: `${base}-inner`, box: `${base}-box`, content: `${base}-content`, rubberband: `${base}-rubberband`, selected: `${base}-selected`, } export const documentEvents = { mousemove: 'adjustSelection', touchmove: 'adjustSelection', mouseup: 'onMouseUp', touchend: 'onMouseUp', touchcancel: 'onMouseUp', } export const defaultOptions: Partial<Selection.Options> = { movable: true, following: true, strict: false, useCellGeometry: false, content(selection) { return StringExt.template( '<%= length %> node<%= length > 1 ? "s":"" %> selected.', )({ length: selection.length }) }, handles: [ { name: 'remove', position: 'nw', events: { mousedown: 'deleteSelectedCells', }, }, { name: 'rotate', position: 'sw', events: { mousedown: 'startRotate', mousemove: 'doRotate', mouseup: 'stopRotate', }, }, { name: 'resize', position: 'se', events: { mousedown: 'startResize', mousemove: 'doResize', mouseup: 'stopResize', }, }, ], } export function depthComparator(cell: Cell) { return cell.getAncestors().length } } namespace EventData { export interface Common { action: 'selecting' | 'translating' } export interface Selecting extends Common { action: 'selecting' moving?: boolean clientX: number clientY: number offsetX: number offsetY: number scrollerX: number scrollerY: number } export interface Translating extends Common { action: 'translating' clientX: number clientY: number originX: number originY: number } export interface SelectionBox { activeView: CellView } export interface Rotation { rotated?: boolean center: Point.PointLike start: number angles: { [id: string]: number } } export interface Resizing { resized?: boolean bbox: Rectangle cells: Cell[] minWidth: number minHeight: number } }