@svgedit/svgcanvas 7.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1297 @@
1
+ /**
2
+ * Tools for SVG selected element operation.
3
+ * @module selected-elem
4
+ * @license MIT
5
+ *
6
+ * @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
7
+ */
8
+
9
+ import { NS } from './namespaces.js'
10
+ import * as hstry from './history.js'
11
+ import * as pathModule from './path.js'
12
+ import {
13
+ getStrokedBBoxDefaultVisible,
14
+ setHref,
15
+ getElement,
16
+ getHref,
17
+ getVisibleElements,
18
+ findDefs,
19
+ getRotationAngle,
20
+ getRefElem,
21
+ getBBox as utilsGetBBox,
22
+ walkTreePost,
23
+ assignAttributes,
24
+ getFeGaussianBlur
25
+ } from './utilities.js'
26
+ import {
27
+ transformPoint,
28
+ matrixMultiply,
29
+ transformListToTransform
30
+ } from './math.js'
31
+ import { recalculateDimensions } from './recalculate.js'
32
+ import { isGecko } from '../../src/common/browser.js'
33
+ import { getParents } from '../../src/common/util.js'
34
+
35
+ const {
36
+ MoveElementCommand,
37
+ BatchCommand,
38
+ InsertElementCommand,
39
+ RemoveElementCommand,
40
+ ChangeElementCommand
41
+ } = hstry
42
+
43
+ let svgCanvas = null
44
+
45
+ /**
46
+ * @function module:selected-elem.init
47
+ * @param {module:selected-elem.elementContext} elementContext
48
+ * @returns {void}
49
+ */
50
+ export const init = canvas => {
51
+ svgCanvas = canvas
52
+ svgCanvas.copySelectedElements = copySelectedElements
53
+ svgCanvas.groupSelectedElements = groupSelectedElements // Wraps all the selected elements in a group (`g`) element.
54
+ svgCanvas.pushGroupProperties = pushGroupProperty // Pushes all appropriate parent group properties down to its children
55
+ svgCanvas.ungroupSelectedElement = ungroupSelectedElement // Unwraps all the elements in a selected group (`g`) element
56
+ svgCanvas.moveToTopSelectedElement = moveToTopSelectedElem // Repositions the selected element to the bottom in the DOM to appear on top
57
+ svgCanvas.moveToBottomSelectedElement = moveToBottomSelectedElem // Repositions the selected element to the top in the DOM to appear under other elements
58
+ svgCanvas.moveUpDownSelected = moveUpDownSelected // Moves the select element up or down the stack, based on the visibly
59
+ svgCanvas.moveSelectedElements = moveSelectedElements // Moves selected elements on the X/Y axis.
60
+ svgCanvas.cloneSelectedElements = cloneSelectedElements // Create deep DOM copies (clones) of all selected elements and move them slightly
61
+ svgCanvas.alignSelectedElements = alignSelectedElements // Aligns selected elements.
62
+ svgCanvas.updateCanvas = updateCanvas // Updates the editor canvas width/height/position after a zoom has occurred.
63
+ svgCanvas.cycleElement = cycleElement // Select the next/previous element within the current layer.
64
+ svgCanvas.deleteSelectedElements = deleteSelectedElements // Removes all selected elements from the DOM and adds the change to the history
65
+ }
66
+
67
+ /**
68
+ * Repositions the selected element to the bottom in the DOM to appear on top of
69
+ * other elements.
70
+ * @function module:selected-elem.SvgCanvas#moveToTopSelectedElem
71
+ * @fires module:selected-elem.SvgCanvas#event:changed
72
+ * @returns {void}
73
+ */
74
+ const moveToTopSelectedElem = () => {
75
+ const [selected] = svgCanvas.getSelectedElements()
76
+ if (selected) {
77
+ const t = selected
78
+ const oldParent = t.parentNode
79
+ const oldNextSibling = t.nextSibling
80
+ t.parentNode.append(t)
81
+ // If the element actually moved position, add the command and fire the changed
82
+ // event handler.
83
+ if (oldNextSibling !== t.nextSibling) {
84
+ svgCanvas.addCommandToHistory(
85
+ new MoveElementCommand(t, oldNextSibling, oldParent, 'top')
86
+ )
87
+ svgCanvas.call('changed', [t])
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Repositions the selected element to the top in the DOM to appear under
94
+ * other elements.
95
+ * @function module:selected-elem.SvgCanvas#moveToBottomSelectedElement
96
+ * @fires module:selected-elem.SvgCanvas#event:changed
97
+ * @returns {void}
98
+ */
99
+ const moveToBottomSelectedElem = () => {
100
+ const [selected] = svgCanvas.getSelectedElements()
101
+ if (selected) {
102
+ let t = selected
103
+ const oldParent = t.parentNode
104
+ const oldNextSibling = t.nextSibling
105
+ let { firstChild } = t.parentNode
106
+ if (firstChild.tagName === 'title') {
107
+ firstChild = firstChild.nextSibling
108
+ }
109
+ // This can probably be removed, as the defs should not ever apppear
110
+ // inside a layer group
111
+ if (firstChild.tagName === 'defs') {
112
+ firstChild = firstChild.nextSibling
113
+ }
114
+ t = t.parentNode.insertBefore(t, firstChild)
115
+ // If the element actually moved position, add the command and fire the changed
116
+ // event handler.
117
+ if (oldNextSibling !== t.nextSibling) {
118
+ svgCanvas.addCommandToHistory(
119
+ new MoveElementCommand(t, oldNextSibling, oldParent, 'bottom')
120
+ )
121
+ svgCanvas.call('changed', [t])
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Moves the select element up or down the stack, based on the visibly
128
+ * intersecting elements.
129
+ * @function module:selected-elem.SvgCanvas#moveUpDownSelected
130
+ * @param {"Up"|"Down"} dir - String that's either 'Up' or 'Down'
131
+ * @fires module:selected-elem.SvgCanvas#event:changed
132
+ * @returns {void}
133
+ */
134
+ const moveUpDownSelected = dir => {
135
+ const selectedElements = svgCanvas.getSelectedElements()
136
+ const selected = selectedElements[0]
137
+ if (!selected) {
138
+ return
139
+ }
140
+
141
+ svgCanvas.setCurBBoxes([])
142
+ let closest
143
+ let foundCur
144
+ // jQuery sorts this list
145
+ const list = svgCanvas.getIntersectionList(
146
+ getStrokedBBoxDefaultVisible([selected])
147
+ )
148
+ if (dir === 'Down') {
149
+ list.reverse()
150
+ }
151
+
152
+ Array.prototype.forEach.call(list, el => {
153
+ if (!foundCur) {
154
+ if (el === selected) {
155
+ foundCur = true
156
+ }
157
+ return true
158
+ }
159
+ if (closest === undefined) {
160
+ closest = el
161
+ }
162
+ return false
163
+ })
164
+ if (!closest) {
165
+ return
166
+ }
167
+
168
+ const t = selected
169
+ const oldParent = t.parentNode
170
+ const oldNextSibling = t.nextSibling
171
+ if (dir === 'Down') {
172
+ closest.insertAdjacentElement('beforebegin', t)
173
+ } else {
174
+ closest.insertAdjacentElement('afterend', t)
175
+ }
176
+ // If the element actually moved position, add the command and fire the changed
177
+ // event handler.
178
+ if (oldNextSibling !== t.nextSibling) {
179
+ svgCanvas.addCommandToHistory(
180
+ new MoveElementCommand(t, oldNextSibling, oldParent, 'Move ' + dir)
181
+ )
182
+ svgCanvas.call('changed', [t])
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Moves selected elements on the X/Y axis.
188
+ * @function module:selected-elem.SvgCanvas#moveSelectedElements
189
+ * @param {Float} dx - Float with the distance to move on the x-axis
190
+ * @param {Float} dy - Float with the distance to move on the y-axis
191
+ * @param {boolean} undoable - Boolean indicating whether or not the action should be undoable
192
+ * @fires module:selected-elem.SvgCanvas#event:changed
193
+ * @returns {BatchCommand|void} Batch command for the move
194
+ */
195
+
196
+ const moveSelectedElements = (dx, dy, undoable = true) => {
197
+ const selectedElements = svgCanvas.getSelectedElements()
198
+ const zoom = svgCanvas.getZoom()
199
+ // if undoable is not sent, default to true
200
+ // if single values, scale them to the zoom
201
+ if (!Array.isArray(dx)) {
202
+ dx /= zoom
203
+ dy /= zoom
204
+ }
205
+
206
+ const batchCmd = new BatchCommand('position')
207
+ selectedElements.forEach((selected, i) => {
208
+ if (selected) {
209
+ const xform = svgCanvas.getSvgRoot().createSVGTransform()
210
+ const tlist = selected.transform?.baseVal
211
+
212
+ // dx and dy could be arrays
213
+ if (Array.isArray(dx)) {
214
+ xform.setTranslate(dx[i], dy[i])
215
+ } else {
216
+ xform.setTranslate(dx, dy)
217
+ }
218
+
219
+ if (tlist.numberOfItems) {
220
+ tlist.insertItemBefore(xform, 0)
221
+ } else {
222
+ tlist.appendItem(xform)
223
+ }
224
+
225
+ const cmd = recalculateDimensions(selected)
226
+ if (cmd) {
227
+ batchCmd.addSubCommand(cmd)
228
+ }
229
+
230
+ svgCanvas
231
+ .gettingSelectorManager()
232
+ .requestSelector(selected)
233
+ .resize()
234
+ }
235
+ })
236
+ if (!batchCmd.isEmpty()) {
237
+ if (undoable) {
238
+ svgCanvas.addCommandToHistory(batchCmd)
239
+ }
240
+ svgCanvas.call('changed', selectedElements)
241
+ return batchCmd
242
+ }
243
+ return undefined
244
+ }
245
+
246
+ /**
247
+ * Create deep DOM copies (clones) of all selected elements and move them slightly
248
+ * from their originals.
249
+ * @function module:selected-elem.SvgCanvas#cloneSelectedElements
250
+ * @param {Float} x Float with the distance to move on the x-axis
251
+ * @param {Float} y Float with the distance to move on the y-axis
252
+ * @returns {void}
253
+ */
254
+ const cloneSelectedElements = (x, y) => {
255
+ const selectedElements = svgCanvas.getSelectedElements()
256
+ const currentGroup = svgCanvas.getCurrentGroup()
257
+ let i
258
+ let elem
259
+ const batchCmd = new BatchCommand('Clone Elements')
260
+ // find all the elements selected (stop at first null)
261
+ const len = selectedElements.length
262
+
263
+ const index = el => {
264
+ if (!el) return -1
265
+ let i = 0
266
+ do {
267
+ i++
268
+ } while (el === el.previousElementSibling)
269
+ return i
270
+ }
271
+
272
+ /**
273
+ * Sorts an array numerically and ascending.
274
+ * @param {Element} a
275
+ * @param {Element} b
276
+ * @returns {Integer}
277
+ */
278
+ const sortfunction = (a, b) => {
279
+ return index(b) - index(a)
280
+ }
281
+ selectedElements.sort(sortfunction)
282
+ for (i = 0; i < len; ++i) {
283
+ elem = selectedElements[i]
284
+ if (!elem) {
285
+ break
286
+ }
287
+ }
288
+ // use slice to quickly get the subset of elements we need
289
+ const copiedElements = selectedElements.slice(0, i)
290
+ svgCanvas.clearSelection(true)
291
+ // note that we loop in the reverse way because of the way elements are added
292
+ // to the selectedElements array (top-first)
293
+ const drawing = svgCanvas.getDrawing()
294
+ i = copiedElements.length
295
+ while (i--) {
296
+ // clone each element and replace it within copiedElements
297
+ elem = copiedElements[i] = drawing.copyElem(copiedElements[i])
298
+ ;(currentGroup || drawing.getCurrentLayer()).append(elem)
299
+ batchCmd.addSubCommand(new InsertElementCommand(elem))
300
+ }
301
+
302
+ if (!batchCmd.isEmpty()) {
303
+ svgCanvas.addToSelection(copiedElements.reverse()) // Need to reverse for correct selection-adding
304
+ moveSelectedElements(x, y, false)
305
+ svgCanvas.addCommandToHistory(batchCmd)
306
+ }
307
+ }
308
+ /**
309
+ * Aligns selected elements.
310
+ * @function module:selected-elem.SvgCanvas#alignSelectedElements
311
+ * @param {string} type - String with single character indicating the alignment type
312
+ * @param {"selected"|"largest"|"smallest"|"page"} relativeTo
313
+ * @returns {void}
314
+ */
315
+ const alignSelectedElements = (type, relativeTo) => {
316
+ const selectedElements = svgCanvas.getSelectedElements()
317
+ const bboxes = [] // angles = [];
318
+ const len = selectedElements.length
319
+ if (!len) {
320
+ return
321
+ }
322
+ let minx = Number.MAX_VALUE
323
+ let maxx = Number.MIN_VALUE
324
+ let miny = Number.MAX_VALUE
325
+ let maxy = Number.MIN_VALUE
326
+
327
+ const isHorizontalAlign = (type) => ['l', 'c', 'r', 'left', 'center', 'right'].includes(type)
328
+ const isVerticalAlign = (type) => ['t', 'm', 'b', 'top', 'middle', 'bottom'].includes(type)
329
+
330
+ for (let i = 0; i < len; ++i) {
331
+ if (!selectedElements[i]) {
332
+ break
333
+ }
334
+ const elem = selectedElements[i]
335
+ bboxes[i] = getStrokedBBoxDefaultVisible([elem])
336
+ }
337
+
338
+ // distribute horizontal and vertical align is not support smallest and largest
339
+ if (['smallest', 'largest'].includes(relativeTo) && ['dh', 'distrib_horiz', 'dv', 'distrib_verti'].includes(type)) {
340
+ relativeTo = 'selected'
341
+ }
342
+
343
+ switch (relativeTo) {
344
+ case 'smallest':
345
+ if (isHorizontalAlign(type) || isVerticalAlign(type)) {
346
+ const sortedBboxes = bboxes.slice().sort((a, b) => a.width - b.width)
347
+ const minBbox = sortedBboxes[0]
348
+ minx = minBbox.x
349
+ miny = minBbox.y
350
+ maxx = minBbox.x + minBbox.width
351
+ maxy = minBbox.y + minBbox.height
352
+ }
353
+ break
354
+ case 'largest':
355
+ if (isHorizontalAlign(type) || isVerticalAlign(type)) {
356
+ const sortedBboxes = bboxes.slice().sort((a, b) => a.width - b.width)
357
+ const maxBbox = sortedBboxes[bboxes.length - 1]
358
+ minx = maxBbox.x
359
+ miny = maxBbox.y
360
+ maxx = maxBbox.x + maxBbox.width
361
+ maxy = maxBbox.y + maxBbox.height
362
+ }
363
+ break
364
+ case 'page':
365
+ minx = 0
366
+ miny = 0
367
+ maxx = svgCanvas.getContentW()
368
+ maxy = svgCanvas.getContentH()
369
+ break
370
+ default:
371
+ // 'selected'
372
+ minx = Math.min(...bboxes.map(box => box.x))
373
+ miny = Math.min(...bboxes.map(box => box.y))
374
+ maxx = Math.max(...bboxes.map(box => box.x + box.width))
375
+ maxy = Math.max(...bboxes.map(box => box.y + box.height))
376
+ break
377
+ } // adjust min/max
378
+
379
+ let dx = []
380
+ let dy = []
381
+
382
+ if (['dh', 'distrib_horiz'].includes(type)) { // distribute horizontal align
383
+ [dx, dy] = _getDistributeHorizontalDistances(relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy)
384
+ } else if (['dv', 'distrib_verti'].includes(type)) { // distribute vertical align
385
+ [dx, dy] = _getDistributeVerticalDistances(relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy)
386
+ } else { // normal align (top, left, right, ...)
387
+ [dx, dy] = _getNormalDistances(type, selectedElements, bboxes, minx, maxx, miny, maxy)
388
+ }
389
+
390
+ moveSelectedElements(dx, dy)
391
+ }
392
+
393
+ /**
394
+ * Aligns selected elements.
395
+ * @function module:selected-elem.SvgCanvas#alignSelectedElements
396
+ * @param {string} type - String with single character indicating the alignment type
397
+ * @param {"selected"|"largest"|"smallest"|"page"} relativeTo
398
+ * @returns {void}
399
+ */
400
+
401
+ /**
402
+ * get distribution horizontal distances.
403
+ * (internal call only)
404
+ *
405
+ * @param {string} relativeTo
406
+ * @param {Element[]} selectedElements - the array with selected DOM elements
407
+ * @param {module:utilities.BBoxObject} bboxes - bounding box objects
408
+ * @param {Float} minx - selected area min-x
409
+ * @param {Float} maxx - selected area max-x
410
+ * @param {Float} miny - selected area min-y
411
+ * @param {Float} maxy - selected area max-y
412
+ * @returns {Array.Float[]} x and y distances array
413
+ * @private
414
+ */
415
+ const _getDistributeHorizontalDistances = (relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy) => {
416
+ const dx = []
417
+ const dy = []
418
+
419
+ for (let i = 0; i < selectedElements.length; i++) {
420
+ dy[i] = 0
421
+ }
422
+
423
+ const bboxesSortedClone = bboxes
424
+ .slice()
425
+ .sort((firstBox, secondBox) => {
426
+ const firstMaxX = firstBox.x + firstBox.width
427
+ const secondMaxX = secondBox.x + secondBox.width
428
+
429
+ if (firstMaxX === secondMaxX) { return 0 } else if (firstMaxX > secondMaxX) { return 1 } else { return -1 }
430
+ })
431
+
432
+ if (relativeTo === 'page') {
433
+ bboxesSortedClone.unshift({ x: 0, y: 0, width: 0, height: maxy }) // virtual left box
434
+ bboxesSortedClone.push({ x: maxx, y: 0, width: 0, height: maxy }) // virtual right box
435
+ }
436
+
437
+ const totalWidth = maxx - minx
438
+ const totalBoxWidth = bboxesSortedClone.map(b => b.width).reduce((w1, w2) => w1 + w2, 0)
439
+ const space = (totalWidth - totalBoxWidth) / (bboxesSortedClone.length - 1)
440
+ const _dx = []
441
+
442
+ for (let i = 0; i < bboxesSortedClone.length; ++i) {
443
+ _dx[i] = 0
444
+
445
+ if (i === 0) { continue }
446
+
447
+ const orgX = bboxesSortedClone[i].x
448
+ bboxesSortedClone[i].x = bboxesSortedClone[i - 1].x + bboxesSortedClone[i - 1].width + space
449
+ _dx[i] = bboxesSortedClone[i].x - orgX
450
+ }
451
+
452
+ bboxesSortedClone.forEach((boxClone, idx) => {
453
+ const orgIdx = bboxes.findIndex(box => box === boxClone)
454
+ if (orgIdx !== -1) {
455
+ dx[orgIdx] = _dx[idx]
456
+ }
457
+ })
458
+
459
+ return [dx, dy]
460
+ }
461
+
462
+ /**
463
+ * get distribution vertical distances.
464
+ * (internal call only)
465
+ *
466
+ * @param {string} relativeTo
467
+ * @param {Element[]} selectedElements - the array with selected DOM elements
468
+ * @param {module:utilities.BBoxObject} bboxes - bounding box objects
469
+ * @param {Float} minx - selected area min-x
470
+ * @param {Float} maxx - selected area max-x
471
+ * @param {Float} miny - selected area min-y
472
+ * @param {Float} maxy - selected area max-y
473
+ * @returns {Array.Float[]}} x and y distances array
474
+ * @private
475
+ */
476
+ const _getDistributeVerticalDistances = (relativeTo, selectedElements, bboxes, minx, maxx, miny, maxy) => {
477
+ const dx = []
478
+ const dy = []
479
+
480
+ for (let i = 0; i < selectedElements.length; i++) {
481
+ dx[i] = 0
482
+ }
483
+
484
+ const bboxesSortedClone = bboxes
485
+ .slice()
486
+ .sort((firstBox, secondBox) => {
487
+ const firstMaxY = firstBox.y + firstBox.height
488
+ const secondMaxY = secondBox.y + secondBox.height
489
+
490
+ if (firstMaxY === secondMaxY) { return 0 } else if (firstMaxY > secondMaxY) { return 1 } else { return -1 }
491
+ })
492
+
493
+ if (relativeTo === 'page') {
494
+ bboxesSortedClone.unshift({ x: 0, y: 0, width: maxx, height: 0 }) // virtual top box
495
+ bboxesSortedClone.push({ x: 0, y: maxy, width: maxx, height: 0 }) // virtual bottom box
496
+ }
497
+
498
+ const totalHeight = maxy - miny
499
+ const totalBoxHeight = bboxesSortedClone.map(b => b.height).reduce((h1, h2) => h1 + h2, 0)
500
+ const space = (totalHeight - totalBoxHeight) / (bboxesSortedClone.length - 1)
501
+ const _dy = []
502
+
503
+ for (let i = 0; i < bboxesSortedClone.length; ++i) {
504
+ _dy[i] = 0
505
+
506
+ if (i === 0) { continue }
507
+
508
+ const orgY = bboxesSortedClone[i].y
509
+ bboxesSortedClone[i].y = bboxesSortedClone[i - 1].y + bboxesSortedClone[i - 1].height + space
510
+ _dy[i] = bboxesSortedClone[i].y - orgY
511
+ }
512
+
513
+ bboxesSortedClone.forEach((boxClone, idx) => {
514
+ const orgIdx = bboxes.findIndex(box => box === boxClone)
515
+ if (orgIdx !== -1) {
516
+ dy[orgIdx] = _dy[idx]
517
+ }
518
+ })
519
+
520
+ return [dx, dy]
521
+ }
522
+
523
+ /**
524
+ * get normal align distances.
525
+ * (internal call only)
526
+ *
527
+ * @param {string} type
528
+ * @param {Element[]} selectedElements - the array with selected DOM elements
529
+ * @param {module:utilities.BBoxObject} bboxes - bounding box objects
530
+ * @param {Float} minx - selected area min-x
531
+ * @param {Float} maxx - selected area max-x
532
+ * @param {Float} miny - selected area min-y
533
+ * @param {Float} maxy - selected area max-y
534
+ * @returns {Array.Float[]} x and y distances array
535
+ * @private
536
+ */
537
+ const _getNormalDistances = (type, selectedElements, bboxes, minx, maxx, miny, maxy) => {
538
+ const len = selectedElements.length
539
+ const dx = new Array(len)
540
+ const dy = new Array(len)
541
+
542
+ for (let i = 0; i < len; ++i) {
543
+ if (!selectedElements[i]) {
544
+ break
545
+ }
546
+ // const elem = selectedElements[i];
547
+ const bbox = bboxes[i]
548
+ dx[i] = 0
549
+ dy[i] = 0
550
+
551
+ switch (type) {
552
+ case 'l': // left (horizontal)
553
+ case 'left': // left (horizontal)
554
+ dx[i] = minx - bbox.x
555
+ break
556
+ case 'c': // center (horizontal)
557
+ case 'center': // center (horizontal)
558
+ dx[i] = (minx + maxx) / 2 - (bbox.x + bbox.width / 2)
559
+ break
560
+ case 'r': // right (horizontal)
561
+ case 'right': // right (horizontal)
562
+ dx[i] = maxx - (bbox.x + bbox.width)
563
+ break
564
+ case 't': // top (vertical)
565
+ case 'top': // top (vertical)
566
+ dy[i] = miny - bbox.y
567
+ break
568
+ case 'm': // middle (vertical)
569
+ case 'middle': // middle (vertical)
570
+ dy[i] = (miny + maxy) / 2 - (bbox.y + bbox.height / 2)
571
+ break
572
+ case 'b': // bottom (vertical)
573
+ case 'bottom': // bottom (vertical)
574
+ dy[i] = maxy - (bbox.y + bbox.height)
575
+ break
576
+ }
577
+ }
578
+
579
+ return [dx, dy]
580
+ }
581
+
582
+ /**
583
+ * Removes all selected elements from the DOM and adds the change to the
584
+ * history stack.
585
+ * @function module:selected-elem.SvgCanvas#deleteSelectedElements
586
+ * @fires module:selected-elem.SvgCanvas#event:changed
587
+ * @returns {void}
588
+ */
589
+ const deleteSelectedElements = () => {
590
+ const selectedElements = svgCanvas.getSelectedElements()
591
+ const batchCmd = new BatchCommand('Delete Elements')
592
+ const selectedCopy = [] // selectedElements is being deleted
593
+
594
+ selectedElements.forEach(selected => {
595
+ if (selected) {
596
+ let parent = selected.parentNode
597
+ let t = selected
598
+ // this will unselect the element and remove the selectedOutline
599
+ svgCanvas.gettingSelectorManager().releaseSelector(t)
600
+ // Remove the path if present.
601
+ pathModule.removePath_(t.id)
602
+ // Get the parent if it's a single-child anchor
603
+ if (parent.tagName === 'a' && parent.childNodes.length === 1) {
604
+ t = parent
605
+ parent = parent.parentNode
606
+ }
607
+ const { nextSibling } = t
608
+ t.remove()
609
+ const elem = t
610
+ selectedCopy.push(selected) // for the copy
611
+ batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent))
612
+ }
613
+ })
614
+ svgCanvas.setEmptySelectedElements()
615
+
616
+ if (!batchCmd.isEmpty()) {
617
+ svgCanvas.addCommandToHistory(batchCmd)
618
+ }
619
+ svgCanvas.call('changed', selectedCopy)
620
+ svgCanvas.clearSelection()
621
+ }
622
+
623
+ /**
624
+ * Remembers the current selected elements on the clipboard.
625
+ * @function module:selected-elem.SvgCanvas#copySelectedElements
626
+ * @returns {void}
627
+ */
628
+ const copySelectedElements = () => {
629
+ const selectedElements = svgCanvas.getSelectedElements()
630
+ const data = JSON.stringify(
631
+ selectedElements.map(x => svgCanvas.getJsonFromSvgElements(x))
632
+ )
633
+ // Use sessionStorage for the clipboard data.
634
+ sessionStorage.setItem(svgCanvas.getClipboardID(), data)
635
+ svgCanvas.flashStorage()
636
+
637
+ // Context menu might not exist (it is provided by editor.js).
638
+ const canvMenu = document.getElementById('se-cmenu_canvas')
639
+ canvMenu.setAttribute('enablemenuitems', '#paste,#paste_in_place')
640
+ }
641
+
642
+ /**
643
+ * Wraps all the selected elements in a group (`g`) element.
644
+ * @function module:selected-elem.SvgCanvas#groupSelectedElements
645
+ * @param {"a"|"g"} [type="g"] - type of element to group into, defaults to `<g>`
646
+ * @param {string} [urlArg]
647
+ * @returns {void}
648
+ */
649
+ const groupSelectedElements = (type, urlArg) => {
650
+ const selectedElements = svgCanvas.getSelectedElements()
651
+ if (!type) {
652
+ type = 'g'
653
+ }
654
+ let cmdStr = ''
655
+ let url
656
+
657
+ switch (type) {
658
+ case 'a': {
659
+ cmdStr = 'Make hyperlink'
660
+ url = urlArg || ''
661
+ break
662
+ }
663
+ default: {
664
+ type = 'g'
665
+ cmdStr = 'Group Elements'
666
+ break
667
+ }
668
+ }
669
+
670
+ const batchCmd = new BatchCommand(cmdStr)
671
+
672
+ // create and insert the group element
673
+ const g = svgCanvas.addSVGElementsFromJson({
674
+ element: type,
675
+ attr: {
676
+ id: svgCanvas.getNextId()
677
+ }
678
+ })
679
+ if (type === 'a') {
680
+ setHref(g, url)
681
+ }
682
+ batchCmd.addSubCommand(new InsertElementCommand(g))
683
+
684
+ // now move all children into the group
685
+ let i = selectedElements.length
686
+ while (i--) {
687
+ let elem = selectedElements[i]
688
+ if (!elem) {
689
+ continue
690
+ }
691
+
692
+ if (
693
+ elem.parentNode.tagName === 'a' &&
694
+ elem.parentNode.childNodes.length === 1
695
+ ) {
696
+ elem = elem.parentNode
697
+ }
698
+
699
+ const oldNextSibling = elem.nextSibling
700
+ const oldParent = elem.parentNode
701
+ g.append(elem)
702
+ batchCmd.addSubCommand(
703
+ new MoveElementCommand(elem, oldNextSibling, oldParent)
704
+ )
705
+ }
706
+ if (!batchCmd.isEmpty()) {
707
+ svgCanvas.addCommandToHistory(batchCmd)
708
+ }
709
+
710
+ // update selection
711
+ svgCanvas.selectOnly([g], true)
712
+ }
713
+
714
+ /**
715
+ * Pushes all appropriate parent group properties down to its children, then
716
+ * removes them from the group.
717
+ * @function module:selected-elem.SvgCanvas#pushGroupProperty
718
+ * @param {SVGAElement|SVGGElement} g
719
+ * @param {boolean} undoable
720
+ * @returns {BatchCommand|void}
721
+ */
722
+ const pushGroupProperty = (g, undoable) => {
723
+ const children = g.childNodes
724
+ const len = children.length
725
+ const xform = g.getAttribute('transform')
726
+
727
+ const glist = g.transform.baseVal
728
+ const m = transformListToTransform(glist).matrix
729
+
730
+ const batchCmd = new BatchCommand('Push group properties')
731
+
732
+ // TODO: get all fill/stroke properties from the group that we are about to destroy
733
+ // "fill", "fill-opacity", "fill-rule", "stroke", "stroke-dasharray", "stroke-dashoffset",
734
+ // "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity",
735
+ // "stroke-width"
736
+ // and then for each child, if they do not have the attribute (or the value is 'inherit')
737
+ // then set the child's attribute
738
+
739
+ const gangle = getRotationAngle(g)
740
+
741
+ const gattrs = {
742
+ filter: g.getAttribute('filter'),
743
+ opacity: g.getAttribute('opacity')
744
+ }
745
+ let gfilter
746
+ let gblur
747
+ let changes
748
+ const drawing = svgCanvas.getDrawing()
749
+
750
+ for (let i = 0; i < len; i++) {
751
+ const elem = children[i]
752
+
753
+ if (elem.nodeType !== 1) {
754
+ continue
755
+ }
756
+
757
+ if (gattrs.opacity !== null && gattrs.opacity !== 1) {
758
+ // const c_opac = elem.getAttribute('opacity') || 1;
759
+ const newOpac =
760
+ Math.round((elem.getAttribute('opacity') || 1) * gattrs.opacity * 100) /
761
+ 100
762
+ svgCanvas.changeSelectedAttribute('opacity', newOpac, [elem])
763
+ }
764
+
765
+ if (gattrs.filter) {
766
+ let cblur = svgCanvas.getBlur(elem)
767
+ const origCblur = cblur
768
+ if (!gblur) {
769
+ gblur = svgCanvas.getBlur(g)
770
+ }
771
+ if (cblur) {
772
+ // Is this formula correct?
773
+ cblur = Number(gblur) + Number(cblur)
774
+ } else if (cblur === 0) {
775
+ cblur = gblur
776
+ }
777
+
778
+ // If child has no current filter, get group's filter or clone it.
779
+ if (!origCblur) {
780
+ // Set group's filter to use first child's ID
781
+ if (!gfilter) {
782
+ gfilter = getRefElem(gattrs.filter)
783
+ } else {
784
+ // Clone the group's filter
785
+ gfilter = drawing.copyElem(gfilter)
786
+ findDefs().append(gfilter)
787
+
788
+ // const filterElem = getRefElem(gfilter);
789
+ const blurElem = getFeGaussianBlur(gfilter)
790
+ // Change this in future for different filters
791
+ const suffix =
792
+ blurElem?.tagName === 'feGaussianBlur' ? 'blur' : 'filter'
793
+ gfilter.id = elem.id + '_' + suffix
794
+ svgCanvas.changeSelectedAttribute(
795
+ 'filter',
796
+ 'url(#' + gfilter.id + ')',
797
+ [elem]
798
+ )
799
+ }
800
+ } else {
801
+ gfilter = getRefElem(elem.getAttribute('filter'))
802
+ }
803
+ // const filterElem = getRefElem(gfilter);
804
+ const blurElem = getFeGaussianBlur(gfilter)
805
+
806
+ // Update blur value
807
+ if (cblur) {
808
+ svgCanvas.changeSelectedAttribute('stdDeviation', cblur, [blurElem])
809
+ svgCanvas.setBlurOffsets(gfilter, cblur)
810
+ }
811
+ }
812
+
813
+ let chtlist = elem.transform?.baseVal
814
+
815
+ // Don't process gradient transforms
816
+ if (elem.tagName.includes('Gradient')) {
817
+ chtlist = null
818
+ }
819
+
820
+ // Hopefully not a problem to add this. Necessary for elements like <desc/>
821
+ if (!chtlist) {
822
+ continue
823
+ }
824
+
825
+ // Apparently <defs> can get get a transformlist, but we don't want it to have one!
826
+ if (elem.tagName === 'defs') {
827
+ continue
828
+ }
829
+
830
+ if (glist.numberOfItems) {
831
+ // TODO: if the group's transform is just a rotate, we can always transfer the
832
+ // rotate() down to the children (collapsing consecutive rotates and factoring
833
+ // out any translates)
834
+ if (gangle && glist.numberOfItems === 1) {
835
+ // [Rg] [Rc] [Mc]
836
+ // we want [Tr] [Rc2] [Mc] where:
837
+ // - [Rc2] is at the child's current center but has the
838
+ // sum of the group and child's rotation angles
839
+ // - [Tr] is the equivalent translation that this child
840
+ // undergoes if the group wasn't there
841
+
842
+ // [Tr] = [Rg] [Rc] [Rc2_inv]
843
+
844
+ // get group's rotation matrix (Rg)
845
+ const rgm = glist.getItem(0).matrix
846
+
847
+ // get child's rotation matrix (Rc)
848
+ let rcm = svgCanvas.getSvgRoot().createSVGMatrix()
849
+ const cangle = getRotationAngle(elem)
850
+ if (cangle) {
851
+ rcm = chtlist.getItem(0).matrix
852
+ }
853
+
854
+ // get child's old center of rotation
855
+ const cbox = utilsGetBBox(elem)
856
+ const ceqm = transformListToTransform(chtlist).matrix
857
+ const coldc = transformPoint(
858
+ cbox.x + cbox.width / 2,
859
+ cbox.y + cbox.height / 2,
860
+ ceqm
861
+ )
862
+
863
+ // sum group and child's angles
864
+ const sangle = gangle + cangle
865
+
866
+ // get child's rotation at the old center (Rc2_inv)
867
+ const r2 = svgCanvas.getSvgRoot().createSVGTransform()
868
+ r2.setRotate(sangle, coldc.x, coldc.y)
869
+
870
+ // calculate equivalent translate
871
+ const trm = matrixMultiply(rgm, rcm, r2.matrix.inverse())
872
+
873
+ // set up tlist
874
+ if (cangle) {
875
+ chtlist.removeItem(0)
876
+ }
877
+
878
+ if (sangle) {
879
+ if (chtlist.numberOfItems) {
880
+ chtlist.insertItemBefore(r2, 0)
881
+ } else {
882
+ chtlist.appendItem(r2)
883
+ }
884
+ }
885
+
886
+ if (trm.e || trm.f) {
887
+ const tr = svgCanvas.getSvgRoot().createSVGTransform()
888
+ tr.setTranslate(trm.e, trm.f)
889
+ if (chtlist.numberOfItems) {
890
+ chtlist.insertItemBefore(tr, 0)
891
+ } else {
892
+ chtlist.appendItem(tr)
893
+ }
894
+ }
895
+ } else {
896
+ // more complicated than just a rotate
897
+ // transfer the group's transform down to each child and then
898
+ // call recalculateDimensions()
899
+ const oldxform = elem.getAttribute('transform')
900
+ changes = {}
901
+ changes.transform = oldxform || ''
902
+
903
+ const newxform = svgCanvas.getSvgRoot().createSVGTransform()
904
+
905
+ // [ gm ] [ chm ] = [ chm ] [ gm' ]
906
+ // [ gm' ] = [ chmInv ] [ gm ] [ chm ]
907
+ const chm = transformListToTransform(chtlist).matrix
908
+ const chmInv = chm.inverse()
909
+ const gm = matrixMultiply(chmInv, m, chm)
910
+ newxform.setMatrix(gm)
911
+ chtlist.appendItem(newxform)
912
+ }
913
+ const cmd = recalculateDimensions(elem)
914
+ if (cmd) {
915
+ batchCmd.addSubCommand(cmd)
916
+ }
917
+ }
918
+ }
919
+
920
+ // remove transform and make it undo-able
921
+ if (xform) {
922
+ changes = {}
923
+ changes.transform = xform
924
+ g.setAttribute('transform', '')
925
+ g.removeAttribute('transform')
926
+ batchCmd.addSubCommand(new ChangeElementCommand(g, changes))
927
+ }
928
+
929
+ if (undoable && !batchCmd.isEmpty()) {
930
+ return batchCmd
931
+ }
932
+ return undefined
933
+ }
934
+
935
+ /**
936
+ * Converts selected/given `<use>` or child SVG element to a group.
937
+ * @function module:selected-elem.SvgCanvas#convertToGroup
938
+ * @param {Element} elem
939
+ * @fires module:selected-elem.SvgCanvas#event:selected
940
+ * @returns {void}
941
+ */
942
+ const convertToGroup = elem => {
943
+ const selectedElements = svgCanvas.getSelectedElements()
944
+ if (!elem) {
945
+ elem = selectedElements[0]
946
+ }
947
+ const $elem = elem
948
+ const batchCmd = new BatchCommand()
949
+ let ts
950
+ const dataStorage = svgCanvas.getDataStorage()
951
+ if (dataStorage.has($elem, 'gsvg')) {
952
+ // Use the gsvg as the new group
953
+ const svg = elem.firstChild
954
+ const pt = {
955
+ x: Number(svg.getAttribute('x')),
956
+ y: Number(svg.getAttribute('y'))
957
+ }
958
+
959
+ // $(elem.firstChild.firstChild).unwrap();
960
+ const firstChild = elem.firstChild.firstChild
961
+ if (firstChild) {
962
+ firstChild.outerHTML = firstChild.innerHTML
963
+ }
964
+ dataStorage.remove(elem, 'gsvg')
965
+
966
+ const tlist = elem.transform.baseVal
967
+ const xform = svgCanvas.getSvgRoot().createSVGTransform()
968
+ xform.setTranslate(pt.x, pt.y)
969
+ tlist.appendItem(xform)
970
+ recalculateDimensions(elem)
971
+ svgCanvas.call('selected', [elem])
972
+ } else if (dataStorage.has($elem, 'symbol')) {
973
+ elem = dataStorage.get($elem, 'symbol')
974
+
975
+ ts = $elem.getAttribute('transform')
976
+ const pos = {
977
+ x: Number($elem.getAttribute('x')),
978
+ y: Number($elem.getAttribute('y'))
979
+ }
980
+
981
+ const vb = elem.getAttribute('viewBox')
982
+
983
+ if (vb) {
984
+ const nums = vb.split(' ')
985
+ pos.x -= Number(nums[0])
986
+ pos.y -= Number(nums[1])
987
+ }
988
+
989
+ // Not ideal, but works
990
+ ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')'
991
+
992
+ const prev = $elem.previousElementSibling
993
+
994
+ // Remove <use> element
995
+ batchCmd.addSubCommand(
996
+ new RemoveElementCommand(
997
+ $elem,
998
+ $elem.nextElementSibling,
999
+ $elem.parentNode
1000
+ )
1001
+ )
1002
+ $elem.remove()
1003
+
1004
+ // See if other elements reference this symbol
1005
+ const svgContent = svgCanvas.getSvgContent()
1006
+ // const hasMore = svgContent.querySelectorAll('use:data(symbol)').length;
1007
+ // @todo review this logic
1008
+ const hasMore = svgContent.querySelectorAll('use').length
1009
+
1010
+ const g = svgCanvas.getDOMDocument().createElementNS(NS.SVG, 'g')
1011
+ const childs = elem.childNodes
1012
+
1013
+ let i
1014
+ for (i = 0; i < childs.length; i++) {
1015
+ g.append(childs[i].cloneNode(true))
1016
+ }
1017
+
1018
+ // Duplicate the gradients for Gecko, since they weren't included in the <symbol>
1019
+ if (isGecko()) {
1020
+ const svgElement = findDefs()
1021
+ const gradients = svgElement.querySelectorAll(
1022
+ 'linearGradient,radialGradient,pattern'
1023
+ )
1024
+ for (let i = 0, im = gradients.length; im > i; i++) {
1025
+ g.appendChild(gradients[i].cloneNode(true))
1026
+ }
1027
+ }
1028
+
1029
+ if (ts) {
1030
+ g.setAttribute('transform', ts)
1031
+ }
1032
+
1033
+ const parent = elem.parentNode
1034
+
1035
+ svgCanvas.uniquifyElems(g)
1036
+
1037
+ // Put the dupe gradients back into <defs> (after uniquifying them)
1038
+ if (isGecko()) {
1039
+ const svgElement = findDefs()
1040
+ const elements = g.querySelectorAll(
1041
+ 'linearGradient,radialGradient,pattern'
1042
+ )
1043
+ for (let i = 0, im = elements.length; im > i; i++) {
1044
+ svgElement.appendChild(elements[i])
1045
+ }
1046
+ }
1047
+
1048
+ // now give the g itself a new id
1049
+ g.id = svgCanvas.getNextId()
1050
+
1051
+ prev.after(g)
1052
+
1053
+ if (parent) {
1054
+ if (!hasMore) {
1055
+ // remove symbol/svg element
1056
+ const { nextSibling } = elem
1057
+ elem.remove()
1058
+ batchCmd.addSubCommand(
1059
+ new RemoveElementCommand(elem, nextSibling, parent)
1060
+ )
1061
+ }
1062
+ batchCmd.addSubCommand(new InsertElementCommand(g))
1063
+ }
1064
+
1065
+ svgCanvas.setUseData(g)
1066
+
1067
+ if (isGecko()) {
1068
+ svgCanvas.convertGradients(findDefs())
1069
+ } else {
1070
+ svgCanvas.convertGradients(g)
1071
+ }
1072
+
1073
+ // recalculate dimensions on the top-level children so that unnecessary transforms
1074
+ // are removed
1075
+ walkTreePost(g, n => {
1076
+ try {
1077
+ recalculateDimensions(n)
1078
+ } catch (e) {
1079
+ console.error(e)
1080
+ }
1081
+ })
1082
+
1083
+ // Give ID for any visible element missing one
1084
+ const visElems = g.querySelectorAll(svgCanvas.getVisElems())
1085
+ Array.prototype.forEach.call(visElems, el => {
1086
+ if (!el.id) {
1087
+ el.id = svgCanvas.getNextId()
1088
+ }
1089
+ })
1090
+
1091
+ svgCanvas.selectOnly([g])
1092
+
1093
+ const cm = pushGroupProperty(g, true)
1094
+ if (cm) {
1095
+ batchCmd.addSubCommand(cm)
1096
+ }
1097
+
1098
+ svgCanvas.addCommandToHistory(batchCmd)
1099
+ } else {
1100
+ console.warn('Unexpected element to ungroup:', elem)
1101
+ }
1102
+ }
1103
+
1104
+ /**
1105
+ * Unwraps all the elements in a selected group (`g`) element. This requires
1106
+ * significant recalculations to apply group's transforms, etc. to its children.
1107
+ * @function module:selected-elem.SvgCanvas#ungroupSelectedElement
1108
+ * @returns {void}
1109
+ */
1110
+ const ungroupSelectedElement = () => {
1111
+ const selectedElements = svgCanvas.getSelectedElements()
1112
+ const dataStorage = svgCanvas.getDataStorage()
1113
+ let g = selectedElements[0]
1114
+ if (!g) {
1115
+ return
1116
+ }
1117
+ if (dataStorage.has(g, 'gsvg') || dataStorage.has(g, 'symbol')) {
1118
+ // Is svg, so actually convert to group
1119
+ convertToGroup(g)
1120
+ return
1121
+ }
1122
+ if (g.tagName === 'use') {
1123
+ // Somehow doesn't have data set, so retrieve
1124
+ const symbol = getElement(getHref(g).substr(1))
1125
+ dataStorage.put(g, 'symbol', symbol)
1126
+ dataStorage.put(g, 'ref', symbol)
1127
+ convertToGroup(g)
1128
+ return
1129
+ }
1130
+ const parentsA = getParents(g.parentNode, 'a')
1131
+ if (parentsA?.length) {
1132
+ g = parentsA[0]
1133
+ }
1134
+
1135
+ // Look for parent "a"
1136
+ if (g.tagName === 'g' || g.tagName === 'a') {
1137
+ const batchCmd = new BatchCommand('Ungroup Elements')
1138
+ const cmd = pushGroupProperty(g, true)
1139
+ if (cmd) {
1140
+ batchCmd.addSubCommand(cmd)
1141
+ }
1142
+
1143
+ const parent = g.parentNode
1144
+ const anchor = g.nextSibling
1145
+ const children = new Array(g.childNodes.length)
1146
+
1147
+ let i = 0
1148
+ while (g.firstChild) {
1149
+ const elem = g.firstChild
1150
+ const oldNextSibling = elem.nextSibling
1151
+ const oldParent = elem.parentNode
1152
+
1153
+ // Remove child title elements
1154
+ if (elem.tagName === 'title') {
1155
+ const { nextSibling } = elem
1156
+ batchCmd.addSubCommand(
1157
+ new RemoveElementCommand(elem, nextSibling, oldParent)
1158
+ )
1159
+ elem.remove()
1160
+ continue
1161
+ }
1162
+
1163
+ children[i++] = parent.insertBefore(elem, anchor)
1164
+ batchCmd.addSubCommand(
1165
+ new MoveElementCommand(elem, oldNextSibling, oldParent)
1166
+ )
1167
+ }
1168
+
1169
+ // remove the group from the selection
1170
+ svgCanvas.clearSelection()
1171
+
1172
+ // delete the group element (but make undo-able)
1173
+ const gNextSibling = g.nextSibling
1174
+ g.remove()
1175
+ batchCmd.addSubCommand(new RemoveElementCommand(g, gNextSibling, parent))
1176
+
1177
+ if (!batchCmd.isEmpty()) {
1178
+ svgCanvas.addCommandToHistory(batchCmd)
1179
+ }
1180
+
1181
+ // update selection
1182
+ svgCanvas.addToSelection(children)
1183
+ }
1184
+ }
1185
+ /**
1186
+ * Updates the editor canvas width/height/position after a zoom has occurred.
1187
+ * @function module:svgcanvas.SvgCanvas#updateCanvas
1188
+ * @param {Float} w - Float with the new width
1189
+ * @param {Float} h - Float with the new height
1190
+ * @fires module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
1191
+ * @returns {module:svgcanvas.CanvasInfo}
1192
+ */
1193
+ const updateCanvas = (w, h) => {
1194
+ svgCanvas.getSvgRoot().setAttribute('width', w)
1195
+ svgCanvas.getSvgRoot().setAttribute('height', h)
1196
+ const zoom = svgCanvas.getZoom()
1197
+ const bg = document.getElementById('canvasBackground')
1198
+ const oldX = Number(svgCanvas.getSvgContent().getAttribute('x'))
1199
+ const oldY = Number(svgCanvas.getSvgContent().getAttribute('y'))
1200
+ const x = (w - svgCanvas.contentW * zoom) / 2
1201
+ const y = (h - svgCanvas.contentH * zoom) / 2
1202
+
1203
+ assignAttributes(svgCanvas.getSvgContent(), {
1204
+ width: svgCanvas.contentW * zoom,
1205
+ height: svgCanvas.contentH * zoom,
1206
+ x,
1207
+ y,
1208
+ viewBox: '0 0 ' + svgCanvas.contentW + ' ' + svgCanvas.contentH
1209
+ })
1210
+
1211
+ assignAttributes(bg, {
1212
+ width: svgCanvas.getSvgContent().getAttribute('width'),
1213
+ height: svgCanvas.getSvgContent().getAttribute('height'),
1214
+ x,
1215
+ y
1216
+ })
1217
+
1218
+ const bgImg = getElement('background_image')
1219
+ if (bgImg) {
1220
+ assignAttributes(bgImg, {
1221
+ width: '100%',
1222
+ height: '100%'
1223
+ })
1224
+ }
1225
+
1226
+ svgCanvas.selectorManager.selectorParentGroup.setAttribute(
1227
+ 'transform',
1228
+ 'translate(' + x + ',' + y + ')'
1229
+ )
1230
+
1231
+ /**
1232
+ * Invoked upon updates to the canvas.
1233
+ * @event module:svgcanvas.SvgCanvas#event:ext_canvasUpdated
1234
+ * @type {PlainObject}
1235
+ * @property {Integer} new_x
1236
+ * @property {Integer} new_y
1237
+ * @property {string} old_x (Of Integer)
1238
+ * @property {string} old_y (Of Integer)
1239
+ * @property {Integer} d_x
1240
+ * @property {Integer} d_y
1241
+ */
1242
+ svgCanvas.runExtensions(
1243
+ 'canvasUpdated',
1244
+ /**
1245
+ * @type {module:svgcanvas.SvgCanvas#event:ext_canvasUpdated}
1246
+ */
1247
+ {
1248
+ new_x: x,
1249
+ new_y: y,
1250
+ old_x: oldX,
1251
+ old_y: oldY,
1252
+ d_x: x - oldX,
1253
+ d_y: y - oldY
1254
+ }
1255
+ )
1256
+ return { x, y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY }
1257
+ }
1258
+ /**
1259
+ * Select the next/previous element within the current layer.
1260
+ * @function module:svgcanvas.SvgCanvas#cycleElement
1261
+ * @param {boolean} next - true = next and false = previous element
1262
+ * @fires module:svgcanvas.SvgCanvas#event:selected
1263
+ * @returns {void}
1264
+ */
1265
+ const cycleElement = next => {
1266
+ const selectedElements = svgCanvas.getSelectedElements()
1267
+ const currentGroup = svgCanvas.getCurrentGroup()
1268
+ let num
1269
+ const curElem = selectedElements[0]
1270
+ let elem = false
1271
+ const allElems = getVisibleElements(
1272
+ currentGroup || svgCanvas.getCurrentDrawing().getCurrentLayer()
1273
+ )
1274
+ if (!allElems.length) {
1275
+ return
1276
+ }
1277
+ if (!curElem) {
1278
+ num = next ? allElems.length - 1 : 0
1279
+ elem = allElems[num]
1280
+ } else {
1281
+ let i = allElems.length
1282
+ while (i--) {
1283
+ if (allElems[i] === curElem) {
1284
+ num = next ? i - 1 : i + 1
1285
+ if (num >= allElems.length) {
1286
+ num = 0
1287
+ } else if (num < 0) {
1288
+ num = allElems.length - 1
1289
+ }
1290
+ elem = allElems[num]
1291
+ break
1292
+ }
1293
+ }
1294
+ }
1295
+ svgCanvas.selectOnly([elem], true)
1296
+ svgCanvas.call('selected', selectedElements)
1297
+ }