selection.ts 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127
  1. import {
  2. Rectangle,
  3. Point,
  4. ModifierKey,
  5. FunctionExt,
  6. Dom,
  7. KeyValue,
  8. Cell,
  9. Node,
  10. Edge,
  11. Model,
  12. Collection,
  13. View,
  14. CellView,
  15. Graph,
  16. } from '@antv/x6'
  17. export class SelectionImpl extends View<SelectionImpl.EventArgs> {
  18. public readonly options: SelectionImpl.Options
  19. protected readonly collection: Collection
  20. protected selectionContainer: HTMLElement
  21. protected selectionContent: HTMLElement
  22. protected boxCount: number
  23. protected boxesUpdated: boolean
  24. public get graph() {
  25. return this.options.graph
  26. }
  27. protected get boxClassName() {
  28. return this.prefixClassName(Private.classNames.box)
  29. }
  30. protected get $boxes() {
  31. return Dom.children(this.container, this.boxClassName)
  32. }
  33. protected get handleOptions() {
  34. return this.options
  35. }
  36. constructor(options: SelectionImpl.Options) {
  37. super()
  38. this.options = options
  39. if (this.options.model) {
  40. this.options.collection = this.options.model.collection
  41. }
  42. if (this.options.collection) {
  43. this.collection = this.options.collection
  44. } else {
  45. this.collection = new Collection([], {
  46. comparator: Private.depthComparator,
  47. })
  48. this.options.collection = this.collection
  49. }
  50. this.boxCount = 0
  51. this.createContainer()
  52. this.startListening()
  53. }
  54. protected startListening() {
  55. const graph = this.graph
  56. const collection = this.collection
  57. this.delegateEvents(
  58. {
  59. [`mousedown .${this.boxClassName}`]: 'onSelectionBoxMouseDown',
  60. [`touchstart .${this.boxClassName}`]: 'onSelectionBoxMouseDown',
  61. },
  62. true,
  63. )
  64. graph.on('scale', this.onGraphTransformed, this)
  65. graph.on('translate', this.onGraphTransformed, this)
  66. graph.model.on('updated', this.onModelUpdated, this)
  67. collection.on('added', this.onCellAdded, this)
  68. collection.on('removed', this.onCellRemoved, this)
  69. collection.on('reseted', this.onReseted, this)
  70. collection.on('updated', this.onCollectionUpdated, this)
  71. collection.on('node:change:position', this.onNodePositionChanged, this)
  72. collection.on('cell:changed', this.onCellChanged, this)
  73. }
  74. protected stopListening() {
  75. const graph = this.graph
  76. const collection = this.collection
  77. this.undelegateEvents()
  78. graph.off('scale', this.onGraphTransformed, this)
  79. graph.off('translate', this.onGraphTransformed, this)
  80. graph.model.off('updated', this.onModelUpdated, this)
  81. collection.off('added', this.onCellAdded, this)
  82. collection.off('removed', this.onCellRemoved, this)
  83. collection.off('reseted', this.onReseted, this)
  84. collection.off('updated', this.onCollectionUpdated, this)
  85. collection.off('node:change:position', this.onNodePositionChanged, this)
  86. collection.off('cell:changed', this.onCellChanged, this)
  87. }
  88. protected onRemove() {
  89. this.stopListening()
  90. }
  91. protected onGraphTransformed() {
  92. this.updateSelectionBoxes()
  93. }
  94. protected onCellChanged() {
  95. this.updateSelectionBoxes()
  96. }
  97. protected translating: boolean
  98. protected onNodePositionChanged({
  99. node,
  100. options,
  101. }: Collection.EventArgs['node:change:position']) {
  102. const { showNodeSelectionBox, pointerEvents } = this.options
  103. const { ui, selection, translateBy, snapped } = options
  104. const allowTranslating =
  105. (showNodeSelectionBox !== true || (pointerEvents && this.getPointerEventsValue(pointerEvents) === 'none')) &&
  106. !this.translating &&
  107. !selection
  108. const translateByUi = ui && translateBy && node.id === translateBy
  109. if (allowTranslating && (translateByUi || snapped)) {
  110. this.translating = true
  111. const current = node.position()
  112. const previous = node.previous('position')!
  113. const dx = current.x - previous.x
  114. const dy = current.y - previous.y
  115. if (dx !== 0 || dy !== 0) {
  116. this.translateSelectedNodes(dx, dy, node, options)
  117. }
  118. this.translating = false
  119. }
  120. }
  121. protected onModelUpdated({ removed }: Collection.EventArgs['updated']) {
  122. if (removed && removed.length) {
  123. this.unselect(removed)
  124. }
  125. }
  126. isEmpty() {
  127. return this.length <= 0
  128. }
  129. isSelected(cell: Cell | string) {
  130. return this.collection.has(cell)
  131. }
  132. get length() {
  133. return this.collection.length
  134. }
  135. get cells() {
  136. return this.collection.toArray()
  137. }
  138. select(cells: Cell | Cell[], options: SelectionImpl.AddOptions = {}) {
  139. options.dryrun = true
  140. const items = this.filter(Array.isArray(cells) ? cells : [cells])
  141. this.collection.add(items, options)
  142. return this
  143. }
  144. unselect(cells: Cell | Cell[], options: SelectionImpl.RemoveOptions = {}) {
  145. // dryrun to prevent cell be removed from graph
  146. options.dryrun = true
  147. this.collection.remove(Array.isArray(cells) ? cells : [cells], options)
  148. return this
  149. }
  150. reset(cells?: Cell | Cell[], options: SelectionImpl.SetOptions = {}) {
  151. if (cells) {
  152. if (options.batch) {
  153. const filterCells = this.filter(Array.isArray(cells) ? cells : [cells])
  154. this.collection.reset(filterCells, { ...options, ui: true })
  155. return this
  156. }
  157. const prev = this.cells
  158. const next = this.filter(Array.isArray(cells) ? cells : [cells])
  159. const prevMap: KeyValue<Cell> = {}
  160. const nextMap: KeyValue<Cell> = {}
  161. prev.forEach((cell) => (prevMap[cell.id] = cell))
  162. next.forEach((cell) => (nextMap[cell.id] = cell))
  163. const added: Cell[] = []
  164. const removed: Cell[] = []
  165. next.forEach((cell) => {
  166. if (!prevMap[cell.id]) {
  167. added.push(cell)
  168. }
  169. })
  170. prev.forEach((cell) => {
  171. if (!nextMap[cell.id]) {
  172. removed.push(cell)
  173. }
  174. })
  175. if (removed.length) {
  176. this.unselect(removed, { ...options, ui: true })
  177. }
  178. if (added.length) {
  179. this.select(added, { ...options, ui: true })
  180. }
  181. if (removed.length === 0 && added.length === 0) {
  182. this.updateContainer()
  183. }
  184. return this
  185. }
  186. return this.clean(options)
  187. }
  188. clean(options: SelectionImpl.SetOptions = {}) {
  189. if (this.length) {
  190. if (options.batch === false) {
  191. this.unselect(this.cells, options)
  192. } else {
  193. this.collection.reset([], { ...options, ui: true })
  194. }
  195. }
  196. return this
  197. }
  198. setFilter(filter?: SelectionImpl.Filter) {
  199. this.options.filter = filter
  200. }
  201. setContent(content?: SelectionImpl.Content) {
  202. this.options.content = content
  203. }
  204. startSelecting(evt: Dom.MouseDownEvent) {
  205. // Flow: startSelecting => adjustSelection => stopSelecting
  206. evt = this.normalizeEvent(evt) // eslint-disable-line
  207. this.clean()
  208. let x
  209. let y
  210. const graphContainer = this.graph.container
  211. if (
  212. evt.offsetX != null &&
  213. evt.offsetY != null &&
  214. graphContainer.contains(evt.target)
  215. ) {
  216. x = evt.offsetX
  217. y = evt.offsetY
  218. } else {
  219. const offset = Dom.offset(graphContainer)
  220. const scrollLeft = graphContainer.scrollLeft
  221. const scrollTop = graphContainer.scrollTop
  222. x = evt.clientX - offset.left + window.pageXOffset + scrollLeft
  223. y = evt.clientY - offset.top + window.pageYOffset + scrollTop
  224. }
  225. Dom.css(this.container, {
  226. top: y,
  227. left: x,
  228. width: 1,
  229. height: 1,
  230. })
  231. this.setEventData<EventData.Selecting>(evt, {
  232. action: 'selecting',
  233. clientX: evt.clientX,
  234. clientY: evt.clientY,
  235. offsetX: x,
  236. offsetY: y,
  237. scrollerX: 0,
  238. scrollerY: 0,
  239. moving: false,
  240. })
  241. this.delegateDocumentEvents(Private.documentEvents, evt.data)
  242. }
  243. filter(cells: Cell[]) {
  244. const filter = this.options.filter
  245. return cells.filter((cell) => {
  246. if (Array.isArray(filter)) {
  247. return filter.some((item) => {
  248. if (typeof item === 'string') {
  249. return cell.shape === item
  250. }
  251. return cell.id === item.id
  252. })
  253. }
  254. if (typeof filter === 'function') {
  255. return FunctionExt.call(filter, this.graph, cell)
  256. }
  257. return true
  258. })
  259. }
  260. protected stopSelecting(evt: Dom.MouseUpEvent) {
  261. const graph = this.graph
  262. const eventData = this.getEventData<EventData.Common>(evt)
  263. const action = eventData.action
  264. switch (action) {
  265. case 'selecting': {
  266. let width = Dom.width(this.container)
  267. let height = Dom.height(this.container)
  268. const offset = Dom.offset(this.container)
  269. const origin = graph.pageToLocal(offset.left, offset.top)
  270. const scale = graph.transform.getScale()
  271. width /= scale.sx
  272. height /= scale.sy
  273. const rect = new Rectangle(origin.x, origin.y, width, height)
  274. const cells = this.getCellViewsInArea(rect).map((view) => view.cell)
  275. this.reset(cells, { batch: true })
  276. this.hideRubberband()
  277. break
  278. }
  279. case 'translating': {
  280. const client = graph.snapToGrid(evt.clientX, evt.clientY)
  281. if (!this.options.following) {
  282. const data = eventData as EventData.Translating
  283. this.updateSelectedNodesPosition({
  284. dx: data.clientX - data.originX,
  285. dy: data.clientY - data.originY,
  286. })
  287. }
  288. this.graph.model.stopBatch('move-selection')
  289. this.notifyBoxEvent('box:mouseup', evt, client.x, client.y)
  290. break
  291. }
  292. default: {
  293. this.clean()
  294. break
  295. }
  296. }
  297. }
  298. protected onMouseUp(evt: Dom.MouseUpEvent) {
  299. const action = this.getEventData<EventData.Common>(evt).action
  300. if (action) {
  301. this.stopSelecting(evt)
  302. this.undelegateDocumentEvents()
  303. }
  304. }
  305. protected onSelectionBoxMouseDown(evt: Dom.MouseDownEvent) {
  306. if (!this.options.following) {
  307. evt.stopPropagation()
  308. }
  309. const e = this.normalizeEvent(evt)
  310. if (this.options.movable) {
  311. this.startTranslating(e)
  312. }
  313. const activeView = this.getCellViewFromElem(e.target)!
  314. this.setEventData<EventData.SelectionBox>(e, { activeView })
  315. const client = this.graph.snapToGrid(e.clientX, e.clientY)
  316. this.notifyBoxEvent('box:mousedown', e, client.x, client.y)
  317. this.delegateDocumentEvents(Private.documentEvents, e.data)
  318. }
  319. protected startTranslating(evt: Dom.MouseDownEvent) {
  320. this.graph.model.startBatch('move-selection')
  321. const client = this.graph.snapToGrid(evt.clientX, evt.clientY)
  322. this.setEventData<EventData.Translating>(evt, {
  323. action: 'translating',
  324. clientX: client.x,
  325. clientY: client.y,
  326. originX: client.x,
  327. originY: client.y,
  328. })
  329. }
  330. private getRestrictArea(): Rectangle.RectangleLike | null {
  331. const restrict = this.graph.options.translating.restrict
  332. const area =
  333. typeof restrict === 'function'
  334. ? FunctionExt.call(restrict, this.graph, null)
  335. : restrict
  336. if (typeof area === 'number') {
  337. return this.graph.transform.getGraphArea().inflate(area)
  338. }
  339. if (area === true) {
  340. return this.graph.transform.getGraphArea()
  341. }
  342. return area || null
  343. }
  344. protected getSelectionOffset(client: Point, data: EventData.Translating) {
  345. let dx = client.x - data.clientX
  346. let dy = client.y - data.clientY
  347. const restrict = this.getRestrictArea()
  348. if (restrict) {
  349. const cells = this.collection.toArray()
  350. const totalBBox =
  351. Cell.getCellsBBox(cells, { deep: true }) || Rectangle.create()
  352. const minDx = restrict.x - totalBBox.x
  353. const minDy = restrict.y - totalBBox.y
  354. const maxDx =
  355. restrict.x + restrict.width - (totalBBox.x + totalBBox.width)
  356. const maxDy =
  357. restrict.y + restrict.height - (totalBBox.y + totalBBox.height)
  358. if (dx < minDx) {
  359. dx = minDx
  360. }
  361. if (dy < minDy) {
  362. dy = minDy
  363. }
  364. if (maxDx < dx) {
  365. dx = maxDx
  366. }
  367. if (maxDy < dy) {
  368. dy = maxDy
  369. }
  370. if (!this.options.following) {
  371. const offsetX = client.x - data.originX
  372. const offsetY = client.y - data.originY
  373. dx = offsetX <= minDx || offsetX >= maxDx ? 0 : dx
  374. dy = offsetY <= minDy || offsetY >= maxDy ? 0 : dy
  375. }
  376. }
  377. return {
  378. dx,
  379. dy,
  380. }
  381. }
  382. private updateElementPosition(elem: Element, dLeft: number, dTop: number) {
  383. const strLeft = Dom.css(elem, 'left')
  384. const strTop = Dom.css(elem, 'top')
  385. const left = strLeft ? parseFloat(strLeft) : 0
  386. const top = strTop ? parseFloat(strTop) : 0
  387. Dom.css(elem, 'left', left + dLeft)
  388. Dom.css(elem, 'top', top + dTop)
  389. }
  390. protected updateSelectedNodesPosition(offset: { dx: number; dy: number }) {
  391. const { dx, dy } = offset
  392. if (dx || dy) {
  393. if ((this.translateSelectedNodes(dx, dy), this.boxesUpdated)) {
  394. if (this.collection.length > 1) {
  395. this.updateSelectionBoxes()
  396. }
  397. } else {
  398. const scale = this.graph.transform.getScale()
  399. for (
  400. let i = 0, $boxes = this.$boxes, len = $boxes.length;
  401. i < len;
  402. i += 1
  403. ) {
  404. this.updateElementPosition($boxes[i], dx * scale.sx, dy * scale.sy)
  405. }
  406. this.updateElementPosition(
  407. this.selectionContainer,
  408. dx * scale.sx,
  409. dy * scale.sy,
  410. )
  411. }
  412. }
  413. }
  414. protected autoScrollGraph(x: number, y: number) {
  415. const scroller = this.graph.getPlugin<any>('scroller')
  416. if (scroller) {
  417. return scroller.autoScroll(x, y)
  418. }
  419. return { scrollerX: 0, scrollerY: 0 }
  420. }
  421. protected adjustSelection(evt: Dom.MouseMoveEvent) {
  422. const e = this.normalizeEvent(evt)
  423. const eventData = this.getEventData<EventData.Common>(e)
  424. const action = eventData.action
  425. switch (action) {
  426. case 'selecting': {
  427. const data = eventData as EventData.Selecting
  428. if (data.moving !== true) {
  429. Dom.appendTo(this.container, this.graph.container)
  430. this.showRubberband()
  431. data.moving = true
  432. }
  433. const { scrollerX, scrollerY } = this.autoScrollGraph(
  434. e.clientX,
  435. e.clientY,
  436. )
  437. data.scrollerX += scrollerX
  438. data.scrollerY += scrollerY
  439. const dx = e.clientX - data.clientX + data.scrollerX
  440. const dy = e.clientY - data.clientY + data.scrollerY
  441. const left = parseInt(Dom.css(this.container, 'left') || '0', 10)
  442. const top = parseInt(Dom.css(this.container, 'top') || '0', 10)
  443. Dom.css(this.container, {
  444. left: dx < 0 ? data.offsetX + dx : left,
  445. top: dy < 0 ? data.offsetY + dy : top,
  446. width: Math.abs(dx),
  447. height: Math.abs(dy),
  448. })
  449. break
  450. }
  451. case 'translating': {
  452. const client = this.graph.snapToGrid(e.clientX, e.clientY)
  453. const data = eventData as EventData.Translating
  454. const offset = this.getSelectionOffset(client, data)
  455. if (this.options.following) {
  456. this.updateSelectedNodesPosition(offset)
  457. } else {
  458. this.updateContainerPosition(offset)
  459. }
  460. if (offset.dx) {
  461. data.clientX = client.x
  462. }
  463. if (offset.dy) {
  464. data.clientY = client.y
  465. }
  466. this.notifyBoxEvent('box:mousemove', evt, client.x, client.y)
  467. break
  468. }
  469. default:
  470. break
  471. }
  472. this.boxesUpdated = false
  473. }
  474. protected translateSelectedNodes(
  475. dx: number,
  476. dy: number,
  477. exclude?: Cell,
  478. otherOptions?: KeyValue,
  479. ) {
  480. const map: { [id: string]: boolean } = {}
  481. const excluded: Cell[] = []
  482. if (exclude) {
  483. map[exclude.id] = true
  484. }
  485. this.collection.toArray().forEach((cell) => {
  486. cell.getDescendants({ deep: true }).forEach((child) => {
  487. map[child.id] = true
  488. })
  489. })
  490. if (otherOptions && otherOptions.translateBy) {
  491. const currentCell = this.graph.getCellById(otherOptions.translateBy)
  492. if (currentCell) {
  493. map[currentCell.id] = true
  494. currentCell.getDescendants({ deep: true }).forEach((child) => {
  495. map[child.id] = true
  496. })
  497. excluded.push(currentCell)
  498. }
  499. }
  500. this.collection.toArray().forEach((cell) => {
  501. if (!map[cell.id]) {
  502. const options = {
  503. ...otherOptions,
  504. selection: this.cid,
  505. exclude: excluded,
  506. }
  507. cell.translate(dx, dy, options)
  508. this.graph.model.getConnectedEdges(cell).forEach((edge) => {
  509. if (!map[edge.id]) {
  510. edge.translate(dx, dy, options)
  511. map[edge.id] = true
  512. }
  513. })
  514. }
  515. })
  516. }
  517. protected getCellViewsInArea(rect: Rectangle) {
  518. const graph = this.graph
  519. const options = {
  520. strict: this.options.strict,
  521. }
  522. let views: CellView[] = []
  523. if (this.options.rubberNode) {
  524. views = views.concat(
  525. graph.model
  526. .getNodesInArea(rect, options)
  527. .map((node) => graph.renderer.findViewByCell(node))
  528. .filter((view) => view != null) as CellView[],
  529. )
  530. }
  531. if (this.options.rubberEdge) {
  532. views = views.concat(
  533. graph.model
  534. .getEdgesInArea(rect, options)
  535. .map((edge) => graph.renderer.findViewByCell(edge))
  536. .filter((view) => view != null) as CellView[],
  537. )
  538. }
  539. return views
  540. }
  541. protected notifyBoxEvent<
  542. K extends keyof SelectionImpl.BoxEventArgs,
  543. T extends Dom.EventObject,
  544. >(name: K, e: T, x: number, y: number) {
  545. const data = this.getEventData<EventData.SelectionBox>(e)
  546. const view = data.activeView
  547. this.trigger(name, { e, view, x, y, cell: view.cell })
  548. }
  549. protected getSelectedClassName(cell: Cell) {
  550. return this.prefixClassName(`${cell.isNode() ? 'node' : 'edge'}-selected`)
  551. }
  552. protected addCellSelectedClassName(cell: Cell) {
  553. const view = this.graph.renderer.findViewByCell(cell)
  554. if (view) {
  555. view.addClass(this.getSelectedClassName(cell))
  556. }
  557. }
  558. protected removeCellUnSelectedClassName(cell: Cell) {
  559. const view = this.graph.renderer.findViewByCell(cell)
  560. if (view) {
  561. view.removeClass(this.getSelectedClassName(cell))
  562. }
  563. }
  564. protected destroySelectionBox(cell: Cell) {
  565. this.removeCellUnSelectedClassName(cell)
  566. if (this.canShowSelectionBox(cell)) {
  567. Dom.remove(this.container.querySelector(`[data-cell-id="${cell.id}"]`))
  568. if (this.$boxes.length === 0) {
  569. this.hide()
  570. }
  571. this.boxCount = Math.max(0, this.boxCount - 1)
  572. }
  573. }
  574. protected destroyAllSelectionBoxes(cells: Cell[]) {
  575. cells.forEach((cell) => this.removeCellUnSelectedClassName(cell))
  576. this.hide()
  577. Dom.remove(this.$boxes)
  578. this.boxCount = 0
  579. }
  580. hide() {
  581. Dom.removeClass(
  582. this.container,
  583. this.prefixClassName(Private.classNames.rubberband),
  584. )
  585. Dom.removeClass(
  586. this.container,
  587. this.prefixClassName(Private.classNames.selected),
  588. )
  589. }
  590. protected showRubberband() {
  591. Dom.addClass(
  592. this.container,
  593. this.prefixClassName(Private.classNames.rubberband),
  594. )
  595. }
  596. protected hideRubberband() {
  597. Dom.removeClass(
  598. this.container,
  599. this.prefixClassName(Private.classNames.rubberband),
  600. )
  601. }
  602. protected showSelected() {
  603. Dom.removeAttribute(this.container, 'style')
  604. Dom.addClass(
  605. this.container,
  606. this.prefixClassName(Private.classNames.selected),
  607. )
  608. }
  609. protected createContainer() {
  610. this.container = document.createElement('div')
  611. Dom.addClass(this.container, this.prefixClassName(Private.classNames.root))
  612. if (this.options.className) {
  613. Dom.addClass(this.container, this.options.className)
  614. }
  615. this.selectionContainer = document.createElement('div')
  616. Dom.addClass(
  617. this.selectionContainer,
  618. this.prefixClassName(Private.classNames.inner),
  619. )
  620. this.selectionContent = document.createElement('div')
  621. Dom.addClass(
  622. this.selectionContent,
  623. this.prefixClassName(Private.classNames.content),
  624. )
  625. Dom.append(this.selectionContainer, this.selectionContent)
  626. Dom.attr(
  627. this.selectionContainer,
  628. 'data-selection-length',
  629. this.collection.length,
  630. )
  631. Dom.prepend(this.container, this.selectionContainer)
  632. }
  633. protected updateContainerPosition(offset: { dx: number; dy: number }) {
  634. if (offset.dx || offset.dy) {
  635. this.updateElementPosition(this.selectionContainer, offset.dx, offset.dy)
  636. }
  637. }
  638. protected updateContainer() {
  639. const origin = { x: Infinity, y: Infinity }
  640. const corner = { x: 0, y: 0 }
  641. const cells = this.collection
  642. .toArray()
  643. .filter((cell) => this.canShowSelectionBox(cell))
  644. cells.forEach((cell) => {
  645. const view = this.graph.renderer.findViewByCell(cell)
  646. if (view) {
  647. const bbox = view.getBBox({
  648. useCellGeometry: true,
  649. })
  650. origin.x = Math.min(origin.x, bbox.x)
  651. origin.y = Math.min(origin.y, bbox.y)
  652. corner.x = Math.max(corner.x, bbox.x + bbox.width)
  653. corner.y = Math.max(corner.y, bbox.y + bbox.height)
  654. }
  655. })
  656. Dom.css(this.selectionContainer, {
  657. position: 'absolute',
  658. pointerEvents: 'none',
  659. left: origin.x,
  660. top: origin.y,
  661. width: corner.x - origin.x,
  662. height: corner.y - origin.y,
  663. })
  664. Dom.attr(
  665. this.selectionContainer,
  666. 'data-selection-length',
  667. this.collection.length,
  668. )
  669. const boxContent = this.options.content
  670. if (boxContent) {
  671. if (typeof boxContent === 'function') {
  672. const content = FunctionExt.call(
  673. boxContent,
  674. this.graph,
  675. this,
  676. this.selectionContent,
  677. )
  678. if (content) {
  679. this.selectionContent.innerHTML = content
  680. }
  681. } else {
  682. this.selectionContent.innerHTML = boxContent
  683. }
  684. }
  685. if (this.collection.length > 0 && !this.container.parentNode) {
  686. Dom.appendTo(this.container, this.graph.container)
  687. } else if (this.collection.length <= 0 && this.container.parentNode) {
  688. this.container.parentNode.removeChild(this.container)
  689. }
  690. }
  691. protected canShowSelectionBox(cell: Cell) {
  692. return (
  693. (cell.isNode() && this.options.showNodeSelectionBox === true) ||
  694. (cell.isEdge() && this.options.showEdgeSelectionBox === true)
  695. )
  696. }
  697. protected getPointerEventsValue(pointerEvents: 'none' | 'auto' | ((cells: Cell[]) => 'none' | 'auto')) {
  698. return typeof pointerEvents === 'string'
  699. ? pointerEvents
  700. : pointerEvents(this.cells)
  701. }
  702. protected createSelectionBox(cell: Cell) {
  703. this.addCellSelectedClassName(cell)
  704. if (this.canShowSelectionBox(cell)) {
  705. const view = this.graph.renderer.findViewByCell(cell)
  706. if (view) {
  707. const bbox = view.getBBox({
  708. useCellGeometry: true,
  709. })
  710. const className = this.boxClassName
  711. const box = document.createElement('div')
  712. const pointerEvents = this.options.pointerEvents
  713. Dom.addClass(box, className)
  714. Dom.addClass(box, `${className}-${cell.isNode() ? 'node' : 'edge'}`)
  715. Dom.attr(box, 'data-cell-id', cell.id)
  716. Dom.css(box, {
  717. position: 'absolute',
  718. left: bbox.x,
  719. top: bbox.y,
  720. width: bbox.width,
  721. height: bbox.height,
  722. pointerEvents: pointerEvents
  723. ? this.getPointerEventsValue(pointerEvents)
  724. : 'auto',
  725. })
  726. Dom.appendTo(box, this.container)
  727. this.showSelected()
  728. this.boxCount += 1
  729. }
  730. }
  731. }
  732. protected updateSelectionBoxes() {
  733. if (this.collection.length > 0) {
  734. this.boxesUpdated = true
  735. this.confirmUpdate()
  736. // this.graph.renderer.requestViewUpdate(this as any, 1, options)
  737. }
  738. }
  739. confirmUpdate() {
  740. if (this.boxCount) {
  741. this.hide()
  742. for (
  743. let i = 0, $boxes = this.$boxes, len = $boxes.length;
  744. i < len;
  745. i += 1
  746. ) {
  747. const box = $boxes[i]
  748. const cellId = Dom.attr(box, 'data-cell-id')
  749. Dom.remove(box)
  750. this.boxCount -= 1
  751. const cell = this.collection.get(cellId)
  752. if (cell) {
  753. this.createSelectionBox(cell)
  754. }
  755. }
  756. this.updateContainer()
  757. }
  758. return 0
  759. }
  760. protected getCellViewFromElem(elem: Element) {
  761. const id = elem.getAttribute('data-cell-id')
  762. if (id) {
  763. const cell = this.collection.get(id)
  764. if (cell) {
  765. return this.graph.renderer.findViewByCell(cell)
  766. }
  767. }
  768. return null
  769. }
  770. protected onCellRemoved({ cell }: Collection.EventArgs['removed']) {
  771. this.destroySelectionBox(cell)
  772. this.updateContainer()
  773. }
  774. protected onReseted({ previous, current }: Collection.EventArgs['reseted']) {
  775. this.destroyAllSelectionBoxes(previous)
  776. current.forEach((cell) => {
  777. this.listenCellRemoveEvent(cell)
  778. this.createSelectionBox(cell)
  779. })
  780. this.updateContainer()
  781. }
  782. protected onCellAdded({ cell }: Collection.EventArgs['added']) {
  783. // The collection do not known the cell was removed when cell was
  784. // removed by interaction(such as, by "delete" shortcut), so we should
  785. // manually listen to cell's remove event.
  786. this.listenCellRemoveEvent(cell)
  787. this.createSelectionBox(cell)
  788. this.updateContainer()
  789. }
  790. protected listenCellRemoveEvent(cell: Cell) {
  791. cell.off('removed', this.onCellRemoved, this)
  792. cell.on('removed', this.onCellRemoved, this)
  793. }
  794. protected onCollectionUpdated({
  795. added,
  796. removed,
  797. options,
  798. }: Collection.EventArgs['updated']) {
  799. added.forEach((cell) => {
  800. this.trigger('cell:selected', { cell, options })
  801. if (cell.isNode()) {
  802. this.trigger('node:selected', { cell, options, node: cell })
  803. } else if (cell.isEdge()) {
  804. this.trigger('edge:selected', { cell, options, edge: cell })
  805. }
  806. })
  807. removed.forEach((cell) => {
  808. this.trigger('cell:unselected', { cell, options })
  809. if (cell.isNode()) {
  810. this.trigger('node:unselected', { cell, options, node: cell })
  811. } else if (cell.isEdge()) {
  812. this.trigger('edge:unselected', { cell, options, edge: cell })
  813. }
  814. })
  815. const args = {
  816. added,
  817. removed,
  818. options,
  819. selected: this.cells.filter((cell) => !!this.graph.getCellById(cell.id)),
  820. }
  821. this.trigger('selection:changed', args)
  822. }
  823. // #endregion
  824. @View.dispose()
  825. dispose() {
  826. this.clean()
  827. this.remove()
  828. this.off()
  829. }
  830. }
  831. export namespace SelectionImpl {
  832. type SelectionEventType = 'leftMouseDown' | 'mouseWheelDown'
  833. export interface CommonOptions {
  834. model?: Model
  835. collection?: Collection
  836. className?: string
  837. strict?: boolean
  838. filter?: Filter
  839. modifiers?: string | ModifierKey[] | null
  840. multiple?: boolean
  841. multipleSelectionModifiers?: string | ModifierKey[] | null
  842. selectCellOnMoved?: boolean
  843. selectNodeOnMoved?: boolean
  844. selectEdgeOnMoved?: boolean
  845. showEdgeSelectionBox?: boolean
  846. showNodeSelectionBox?: boolean
  847. movable?: boolean
  848. following?: boolean
  849. content?: Content
  850. // Can select node or edge when rubberband
  851. rubberband?: boolean
  852. rubberNode?: boolean
  853. rubberEdge?: boolean
  854. // Whether to respond event on the selectionBox
  855. pointerEvents?: 'none' | 'auto' | ((cells: Cell[]) => 'none' | 'auto')
  856. // with which mouse button the selection can be started
  857. eventTypes?: SelectionEventType[]
  858. }
  859. export interface Options extends CommonOptions {
  860. graph: Graph
  861. }
  862. export type Content =
  863. | null
  864. | false
  865. | string
  866. | ((
  867. this: Graph,
  868. selection: SelectionImpl,
  869. contentElement: HTMLElement,
  870. ) => string)
  871. export type Filter =
  872. | null
  873. | (string | { id: string })[]
  874. | ((this: Graph, cell: Cell) => boolean)
  875. export interface SetOptions extends Collection.SetOptions {
  876. batch?: boolean
  877. }
  878. export interface AddOptions extends Collection.AddOptions {}
  879. export interface RemoveOptions extends Collection.RemoveOptions {}
  880. }
  881. export namespace SelectionImpl {
  882. interface SelectionBoxEventArgs<T> {
  883. e: T
  884. view: CellView
  885. cell: Cell
  886. x: number
  887. y: number
  888. }
  889. export interface BoxEventArgs {
  890. 'box:mousedown': SelectionBoxEventArgs<Dom.MouseDownEvent>
  891. 'box:mousemove': SelectionBoxEventArgs<Dom.MouseMoveEvent>
  892. 'box:mouseup': SelectionBoxEventArgs<Dom.MouseUpEvent>
  893. }
  894. export interface SelectionEventArgs {
  895. 'cell:selected': { cell: Cell; options: Model.SetOptions }
  896. 'node:selected': { cell: Cell; node: Node; options: Model.SetOptions }
  897. 'edge:selected': { cell: Cell; edge: Edge; options: Model.SetOptions }
  898. 'cell:unselected': { cell: Cell; options: Model.SetOptions }
  899. 'node:unselected': { cell: Cell; node: Node; options: Model.SetOptions }
  900. 'edge:unselected': { cell: Cell; edge: Edge; options: Model.SetOptions }
  901. 'selection:changed': {
  902. added: Cell[]
  903. removed: Cell[]
  904. selected: Cell[]
  905. options: Model.SetOptions
  906. }
  907. }
  908. export interface EventArgs extends BoxEventArgs, SelectionEventArgs {}
  909. }
  910. // private
  911. // -------
  912. namespace Private {
  913. const base = 'widget-selection'
  914. export const classNames = {
  915. root: base,
  916. inner: `${base}-inner`,
  917. box: `${base}-box`,
  918. content: `${base}-content`,
  919. rubberband: `${base}-rubberband`,
  920. selected: `${base}-selected`,
  921. }
  922. export const documentEvents = {
  923. mousemove: 'adjustSelection',
  924. touchmove: 'adjustSelection',
  925. mouseup: 'onMouseUp',
  926. touchend: 'onMouseUp',
  927. touchcancel: 'onMouseUp',
  928. }
  929. export function depthComparator(cell: Cell) {
  930. return cell.getAncestors().length
  931. }
  932. }
  933. namespace EventData {
  934. export interface Common {
  935. action: 'selecting' | 'translating'
  936. }
  937. export interface Selecting extends Common {
  938. action: 'selecting'
  939. moving?: boolean
  940. clientX: number
  941. clientY: number
  942. offsetX: number
  943. offsetY: number
  944. scrollerX: number
  945. scrollerY: number
  946. }
  947. export interface Translating extends Common {
  948. action: 'translating'
  949. clientX: number
  950. clientY: number
  951. originX: number
  952. originY: number
  953. }
  954. export interface SelectionBox {
  955. activeView: CellView
  956. }
  957. export interface Rotation {
  958. rotated?: boolean
  959. center: Point.PointLike
  960. start: number
  961. angles: { [id: string]: number }
  962. }
  963. export interface Resizing {
  964. resized?: boolean
  965. bbox: Rectangle
  966. cells: Cell[]
  967. minWidth: number
  968. minHeight: number
  969. }
  970. }