12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127 |
- import {
- Rectangle,
- Point,
- ModifierKey,
- FunctionExt,
- Dom,
- KeyValue,
- Cell,
- Node,
- Edge,
- Model,
- Collection,
- View,
- CellView,
- Graph,
- } from '@antv/x6'
- export class SelectionImpl extends View<SelectionImpl.EventArgs> {
- public readonly options: SelectionImpl.Options
- protected readonly collection: Collection
- protected selectionContainer: HTMLElement
- protected selectionContent: 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 Dom.children(this.container, this.boxClassName)
- }
- protected get handleOptions() {
- return this.options
- }
- constructor(options: SelectionImpl.Options) {
- super()
- this.options = 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.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()
- }
- protected onCellChanged() {
- this.updateSelectionBoxes()
- }
- protected translating: boolean
- protected onNodePositionChanged({
- node,
- options,
- }: Collection.EventArgs['node:change:position']) {
- const { showNodeSelectionBox, pointerEvents } = this.options
- const { ui, selection, translateBy, snapped } = options
- const allowTranslating =
- (showNodeSelectionBox !== true || (pointerEvents && this.getPointerEventsValue(pointerEvents) === 'none')) &&
- !this.translating &&
- !selection
- const translateByUi = ui && translateBy && node.id === translateBy
- if (allowTranslating && (translateByUi || snapped)) {
- 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: SelectionImpl.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: SelectionImpl.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: SelectionImpl.SetOptions = {}) {
- if (cells) {
- if (options.batch) {
- const filterCells = this.filter(Array.isArray(cells) ? cells : [cells])
- this.collection.reset(filterCells, { ...options, ui: true })
- return this
- }
- 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: SelectionImpl.SetOptions = {}) {
- if (this.length) {
- if (options.batch === false) {
- this.unselect(this.cells, options)
- } else {
- this.collection.reset([], { ...options, ui: true })
- }
- }
- return this
- }
- setFilter(filter?: SelectionImpl.Filter) {
- this.options.filter = filter
- }
- setContent(content?: SelectionImpl.Content) {
- this.options.content = content
- }
- startSelecting(evt: Dom.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 = Dom.offset(graphContainer)
- const scrollLeft = graphContainer.scrollLeft
- const scrollTop = graphContainer.scrollTop
- x = evt.clientX - offset.left + window.pageXOffset + scrollLeft
- y = evt.clientY - offset.top + window.pageYOffset + scrollTop
- }
- Dom.css(this.container, {
- 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,
- moving: false,
- })
- this.delegateDocumentEvents(Private.documentEvents, evt.data)
- }
- filter(cells: Cell[]) {
- const filter = this.options.filter
- return cells.filter((cell) => {
- if (Array.isArray(filter)) {
- return filter.some((item) => {
- if (typeof item === 'string') {
- return cell.shape === item
- }
- return cell.id === item.id
- })
- }
- if (typeof filter === 'function') {
- return FunctionExt.call(filter, this.graph, cell)
- }
- return true
- })
- }
- protected stopSelecting(evt: Dom.MouseUpEvent) {
- const graph = this.graph
- const eventData = this.getEventData<EventData.Common>(evt)
- const action = eventData.action
- switch (action) {
- case 'selecting': {
- let width = Dom.width(this.container)
- let height = Dom.height(this.container)
- const offset = Dom.offset(this.container)
- 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, { batch: true })
- 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: Dom.MouseUpEvent) {
- const action = this.getEventData<EventData.Common>(evt).action
- if (action) {
- this.stopSelecting(evt)
- this.undelegateDocumentEvents()
- }
- }
- protected onSelectionBoxMouseDown(evt: Dom.MouseDownEvent) {
- if (!this.options.following) {
- 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: Dom.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,
- })
- }
- private getRestrictArea(): Rectangle.RectangleLike | null {
- const restrict = this.graph.options.translating.restrict
- const area =
- typeof restrict === 'function'
- ? FunctionExt.call(restrict, this.graph, null)
- : restrict
- if (typeof area === 'number') {
- return this.graph.transform.getGraphArea().inflate(area)
- }
- if (area === true) {
- return this.graph.transform.getGraphArea()
- }
- return area || null
- }
- protected getSelectionOffset(client: Point, data: EventData.Translating) {
- let dx = client.x - data.clientX
- let dy = client.y - data.clientY
- const restrict = this.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,
- }
- }
- private updateElementPosition(elem: Element, dLeft: number, dTop: number) {
- const strLeft = Dom.css(elem, 'left')
- const strTop = Dom.css(elem, 'top')
- const left = strLeft ? parseFloat(strLeft) : 0
- const top = strTop ? parseFloat(strTop) : 0
- Dom.css(elem, 'left', left + dLeft)
- Dom.css(elem, 'top', top + dTop)
- }
- 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()
- for (
- let i = 0, $boxes = this.$boxes, len = $boxes.length;
- i < len;
- i += 1
- ) {
- this.updateElementPosition($boxes[i], dx * scale.sx, dy * scale.sy)
- }
- this.updateElementPosition(
- this.selectionContainer,
- dx * scale.sx,
- dy * scale.sy,
- )
- }
- }
- }
- protected autoScrollGraph(x: number, y: number) {
- const scroller = this.graph.getPlugin<any>('scroller')
- if (scroller) {
- return scroller.autoScroll(x, y)
- }
- return { scrollerX: 0, scrollerY: 0 }
- }
- protected adjustSelection(evt: Dom.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) {
- Dom.appendTo(this.container, 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(Dom.css(this.container, 'left') || '0', 10)
- const top = parseInt(Dom.css(this.container, 'top') || '0', 10)
- Dom.css(this.container, {
- 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) {
- views = views.concat(
- graph.model
- .getNodesInArea(rect, options)
- .map((node) => graph.renderer.findViewByCell(node))
- .filter((view) => view != null) as CellView[],
- )
- }
- if (this.options.rubberEdge) {
- views = views.concat(
- graph.model
- .getEdgesInArea(rect, options)
- .map((edge) => graph.renderer.findViewByCell(edge))
- .filter((view) => view != null) as CellView[],
- )
- }
- return views
- }
- protected notifyBoxEvent<
- K extends keyof SelectionImpl.BoxEventArgs,
- T extends Dom.EventObject,
- >(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)) {
- Dom.remove(this.container.querySelector(`[data-cell-id="${cell.id}"]`))
- 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()
- Dom.remove(this.$boxes)
- this.boxCount = 0
- }
- hide() {
- Dom.removeClass(
- this.container,
- this.prefixClassName(Private.classNames.rubberband),
- )
- Dom.removeClass(
- this.container,
- this.prefixClassName(Private.classNames.selected),
- )
- }
- protected showRubberband() {
- Dom.addClass(
- this.container,
- this.prefixClassName(Private.classNames.rubberband),
- )
- }
- protected hideRubberband() {
- Dom.removeClass(
- this.container,
- this.prefixClassName(Private.classNames.rubberband),
- )
- }
- protected showSelected() {
- Dom.removeAttribute(this.container, 'style')
- Dom.addClass(
- this.container,
- this.prefixClassName(Private.classNames.selected),
- )
- }
- protected createContainer() {
- this.container = document.createElement('div')
- Dom.addClass(this.container, this.prefixClassName(Private.classNames.root))
- if (this.options.className) {
- Dom.addClass(this.container, this.options.className)
- }
- this.selectionContainer = document.createElement('div')
- Dom.addClass(
- this.selectionContainer,
- this.prefixClassName(Private.classNames.inner),
- )
- this.selectionContent = document.createElement('div')
- Dom.addClass(
- this.selectionContent,
- this.prefixClassName(Private.classNames.content),
- )
- Dom.append(this.selectionContainer, this.selectionContent)
- Dom.attr(
- this.selectionContainer,
- 'data-selection-length',
- this.collection.length,
- )
- Dom.prepend(this.container, this.selectionContainer)
- }
- protected updateContainerPosition(offset: { dx: number; dy: number }) {
- if (offset.dx || offset.dy) {
- this.updateElementPosition(this.selectionContainer, offset.dx, 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: true,
- })
- 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)
- }
- })
- Dom.css(this.selectionContainer, {
- position: 'absolute',
- pointerEvents: 'none',
- left: origin.x,
- top: origin.y,
- width: corner.x - origin.x,
- height: corner.y - origin.y,
- })
- Dom.attr(
- this.selectionContainer,
- '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,
- )
- if (content) {
- this.selectionContent.innerHTML = content
- }
- } else {
- this.selectionContent.innerHTML = boxContent
- }
- }
- if (this.collection.length > 0 && !this.container.parentNode) {
- Dom.appendTo(this.container, 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 getPointerEventsValue(pointerEvents: 'none' | 'auto' | ((cells: Cell[]) => 'none' | 'auto')) {
- return typeof pointerEvents === 'string'
- ? pointerEvents
- : pointerEvents(this.cells)
- }
- 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: true,
- })
- const className = this.boxClassName
- const box = document.createElement('div')
- const pointerEvents = this.options.pointerEvents
- Dom.addClass(box, className)
- Dom.addClass(box, `${className}-${cell.isNode() ? 'node' : 'edge'}`)
- Dom.attr(box, 'data-cell-id', cell.id)
- Dom.css(box, {
- position: 'absolute',
- left: bbox.x,
- top: bbox.y,
- width: bbox.width,
- height: bbox.height,
- pointerEvents: pointerEvents
- ? this.getPointerEventsValue(pointerEvents)
- : 'auto',
- })
- Dom.appendTo(box, this.container)
- this.showSelected()
- this.boxCount += 1
- }
- }
- }
- protected updateSelectionBoxes() {
- if (this.collection.length > 0) {
- this.boxesUpdated = true
- this.confirmUpdate()
- // this.graph.renderer.requestViewUpdate(this as any, 1, options)
- }
- }
- confirmUpdate() {
- if (this.boxCount) {
- this.hide()
- for (
- let i = 0, $boxes = this.$boxes, len = $boxes.length;
- i < len;
- i += 1
- ) {
- const box = $boxes[i]
- const cellId = Dom.attr(box, 'data-cell-id')
- Dom.remove(box)
- this.boxCount -= 1
- 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 event.
- 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 })
- if (cell.isNode()) {
- this.trigger('node:selected', { cell, options, node: cell })
- } else if (cell.isEdge()) {
- this.trigger('edge:selected', { cell, options, edge: cell })
- }
- })
- removed.forEach((cell) => {
- this.trigger('cell:unselected', { cell, options })
- if (cell.isNode()) {
- this.trigger('node:unselected', { cell, options, node: cell })
- } else if (cell.isEdge()) {
- this.trigger('edge:unselected', { cell, options, edge: cell })
- }
- })
- const args = {
- added,
- removed,
- options,
- selected: this.cells.filter((cell) => !!this.graph.getCellById(cell.id)),
- }
- this.trigger('selection:changed', args)
- }
- // #endregion
- @View.dispose()
- dispose() {
- this.clean()
- this.remove()
- this.off()
- }
- }
- export namespace SelectionImpl {
- type SelectionEventType = 'leftMouseDown' | 'mouseWheelDown'
- export interface CommonOptions {
- model?: Model
- collection?: Collection
- className?: string
- strict?: boolean
- filter?: Filter
- modifiers?: string | ModifierKey[] | null
- multiple?: boolean
- multipleSelectionModifiers?: string | ModifierKey[] | null
- selectCellOnMoved?: boolean
- selectNodeOnMoved?: boolean
- selectEdgeOnMoved?: boolean
- showEdgeSelectionBox?: boolean
- showNodeSelectionBox?: boolean
- movable?: boolean
- following?: boolean
- content?: Content
- // Can select node or edge when rubberband
- rubberband?: boolean
- rubberNode?: boolean
- rubberEdge?: boolean
- // Whether to respond event on the selectionBox
- pointerEvents?: 'none' | 'auto' | ((cells: Cell[]) => 'none' | 'auto')
- // with which mouse button the selection can be started
- eventTypes?: SelectionEventType[]
- }
- export interface Options extends CommonOptions {
- graph: Graph
- }
- export type Content =
- | null
- | false
- | string
- | ((
- this: Graph,
- selection: SelectionImpl,
- contentElement: HTMLElement,
- ) => string)
- export type Filter =
- | null
- | (string | { id: string })[]
- | ((this: Graph, cell: Cell) => boolean)
- export interface SetOptions extends Collection.SetOptions {
- batch?: boolean
- }
- export interface AddOptions extends Collection.AddOptions {}
- export interface RemoveOptions extends Collection.RemoveOptions {}
- }
- export namespace SelectionImpl {
- interface SelectionBoxEventArgs<T> {
- e: T
- view: CellView
- cell: Cell
- x: number
- y: number
- }
- export interface BoxEventArgs {
- 'box:mousedown': SelectionBoxEventArgs<Dom.MouseDownEvent>
- 'box:mousemove': SelectionBoxEventArgs<Dom.MouseMoveEvent>
- 'box:mouseup': SelectionBoxEventArgs<Dom.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 {}
- }
- // 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 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
- }
- }
|