@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.
package/draw.js ADDED
@@ -0,0 +1,1064 @@
1
+ /**
2
+ * Tools for drawing.
3
+ * @module draw
4
+ * @license MIT
5
+ * @copyright 2011 Jeff Schiller
6
+ */
7
+
8
+ import Layer from './layer.js'
9
+ import HistoryRecordingService from './historyrecording.js'
10
+
11
+ import { NS } from './namespaces.js'
12
+ import {
13
+ toXml, getElement
14
+ } from './utilities.js'
15
+ import {
16
+ copyElem as utilCopyElem
17
+ } from './copy-elem.js'
18
+ import { getParentsUntil } from '../../src/common/util.js'
19
+
20
+ const visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'.split(',')
21
+
22
+ const RandomizeModes = {
23
+ LET_DOCUMENT_DECIDE: 0,
24
+ ALWAYS_RANDOMIZE: 1,
25
+ NEVER_RANDOMIZE: 2
26
+ }
27
+ let randIds = RandomizeModes.LET_DOCUMENT_DECIDE
28
+ // Array with current disabled elements (for in-group editing)
29
+ let disabledElems = []
30
+
31
+ /**
32
+ * Get a HistoryRecordingService.
33
+ * @param {module:history.HistoryRecordingService} [hrService] - if exists, return it instead of creating a new service.
34
+ * @returns {module:history.HistoryRecordingService}
35
+ */
36
+ function historyRecordingService (hrService) {
37
+ return hrService || new HistoryRecordingService(svgCanvas.undoMgr)
38
+ }
39
+
40
+ /**
41
+ * Find the layer name in a group element.
42
+ * @param {Element} group The group element to search in.
43
+ * @returns {string} The layer name or empty string.
44
+ */
45
+ function findLayerNameInGroup (group) {
46
+ const sel = group.querySelector('title')
47
+ return sel ? sel.textContent : ''
48
+ }
49
+
50
+ /**
51
+ * Given a set of names, return a new unique name.
52
+ * @param {string[]} existingLayerNames - Existing layer names.
53
+ * @returns {string} - The new name.
54
+ */
55
+ function getNewLayerName (existingLayerNames) {
56
+ let i = 1
57
+ // TODO(codedread): What about internationalization of "Layer"?
58
+ while (existingLayerNames.includes(('Layer ' + i))) { i++ }
59
+ return 'Layer ' + i
60
+ }
61
+
62
+ /**
63
+ * This class encapsulates the concept of a SVG-edit drawing.
64
+ */
65
+ export class Drawing {
66
+ /**
67
+ * @param {SVGSVGElement} svgElem - The SVG DOM Element that this JS object
68
+ * encapsulates. If the svgElem has a se:nonce attribute on it, then
69
+ * IDs will use the nonce as they are generated.
70
+ * @param {string} [optIdPrefix=svg_] - The ID prefix to use.
71
+ * @throws {Error} If not initialized with an SVG element
72
+ */
73
+ constructor (svgElem, optIdPrefix) {
74
+ if (!svgElem || !svgElem.tagName || !svgElem.namespaceURI ||
75
+ svgElem.tagName !== 'svg' || svgElem.namespaceURI !== NS.SVG) {
76
+ throw new Error('Error: svgedit.draw.Drawing instance initialized without a <svg> element')
77
+ }
78
+
79
+ /**
80
+ * The SVG DOM Element that represents this drawing.
81
+ * @type {SVGSVGElement}
82
+ */
83
+ this.svgElem_ = svgElem
84
+
85
+ /**
86
+ * The latest object number used in this drawing.
87
+ * @type {Integer}
88
+ */
89
+ this.obj_num = 0
90
+
91
+ /**
92
+ * The prefix to prepend to each element id in the drawing.
93
+ * @type {string}
94
+ */
95
+ this.idPrefix = optIdPrefix || 'svg_'
96
+
97
+ /**
98
+ * An array of released element ids to immediately reuse.
99
+ * @type {Integer[]}
100
+ */
101
+ this.releasedNums = []
102
+
103
+ /**
104
+ * The z-ordered array of Layer objects. Each layer has a name
105
+ * and group element.
106
+ * The first layer is the one at the bottom of the rendering.
107
+ * @type {Layer[]}
108
+ */
109
+ this.all_layers = []
110
+
111
+ /**
112
+ * Map of all_layers by name.
113
+ *
114
+ * Note: Layers are ordered, but referenced externally by name; so, we need both container
115
+ * types depending on which function is called (i.e. all_layers and layer_map).
116
+ *
117
+ * @type {PlainObject<string, Layer>}
118
+ */
119
+ this.layer_map = {}
120
+
121
+ /**
122
+ * The current layer being used.
123
+ * @type {Layer}
124
+ */
125
+ this.current_layer = null
126
+
127
+ /**
128
+ * The nonce to use to uniquely identify elements across drawings.
129
+ * @type {!string}
130
+ */
131
+ this.nonce_ = ''
132
+ const n = this.svgElem_.getAttributeNS(NS.SE, 'nonce')
133
+ // If already set in the DOM, use the nonce throughout the document
134
+ // else, if randomizeIds(true) has been called, create and set the nonce.
135
+ if (n && randIds !== RandomizeModes.NEVER_RANDOMIZE) {
136
+ this.nonce_ = n
137
+ } else if (randIds === RandomizeModes.ALWAYS_RANDOMIZE) {
138
+ this.setNonce(Math.floor(Math.random() * 100001))
139
+ }
140
+ }
141
+
142
+ /**
143
+ * @param {string} id Element ID to retrieve
144
+ * @returns {Element} SVG element within the root SVGSVGElement
145
+ */
146
+ getElem_ (id) {
147
+ if (this.svgElem_.querySelector) {
148
+ // querySelector lookup
149
+ return this.svgElem_.querySelector('#' + id)
150
+ }
151
+ // jQuery lookup: twice as slow as xpath in FF
152
+ return this.svgElem_.querySelector('[id=' + id + ']')
153
+ }
154
+
155
+ /**
156
+ * @returns {SVGSVGElement}
157
+ */
158
+ getSvgElem () {
159
+ return this.svgElem_
160
+ }
161
+
162
+ /**
163
+ * @returns {!(string|Integer)} The previously set nonce
164
+ */
165
+ getNonce () {
166
+ return this.nonce_
167
+ }
168
+
169
+ /**
170
+ * @param {!(string|Integer)} n The nonce to set
171
+ * @returns {void}
172
+ */
173
+ setNonce (n) {
174
+ this.svgElem_.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE)
175
+ this.svgElem_.setAttributeNS(NS.SE, 'se:nonce', n)
176
+ this.nonce_ = n
177
+ }
178
+
179
+ /**
180
+ * Clears any previously set nonce.
181
+ * @returns {void}
182
+ */
183
+ clearNonce () {
184
+ // We deliberately leave any se:nonce attributes alone,
185
+ // we just don't use it to randomize ids.
186
+ this.nonce_ = ''
187
+ }
188
+
189
+ /**
190
+ * Returns the latest object id as a string.
191
+ * @returns {string} The latest object Id.
192
+ */
193
+ getId () {
194
+ return this.nonce_
195
+ ? this.idPrefix + this.nonce_ + '_' + this.obj_num
196
+ : this.idPrefix + this.obj_num
197
+ }
198
+
199
+ /**
200
+ * Returns the next object Id as a string.
201
+ * @returns {string} The next object Id to use.
202
+ */
203
+ getNextId () {
204
+ const oldObjNum = this.obj_num
205
+ let restoreOldObjNum = false
206
+
207
+ // If there are any released numbers in the release stack,
208
+ // use the last one instead of the next obj_num.
209
+ // We need to temporarily use obj_num as that is what getId() depends on.
210
+ if (this.releasedNums.length > 0) {
211
+ this.obj_num = this.releasedNums.pop()
212
+ restoreOldObjNum = true
213
+ } else {
214
+ // If we are not using a released id, then increment the obj_num.
215
+ this.obj_num++
216
+ }
217
+
218
+ // Ensure the ID does not exist.
219
+ let id = this.getId()
220
+ while (this.getElem_(id)) {
221
+ if (restoreOldObjNum) {
222
+ this.obj_num = oldObjNum
223
+ restoreOldObjNum = false
224
+ }
225
+ this.obj_num++
226
+ id = this.getId()
227
+ }
228
+ // Restore the old object number if required.
229
+ if (restoreOldObjNum) {
230
+ this.obj_num = oldObjNum
231
+ }
232
+ return id
233
+ }
234
+
235
+ /**
236
+ * Releases the object Id, letting it be used as the next id in getNextId().
237
+ * This method DOES NOT remove any elements from the DOM, it is expected
238
+ * that client code will do this.
239
+ * @param {string} id - The id to release.
240
+ * @returns {boolean} True if the id was valid to be released, false otherwise.
241
+ */
242
+ releaseId (id) {
243
+ // confirm if this is a valid id for this Document, else return false
244
+ const front = this.idPrefix + (this.nonce_ ? this.nonce_ + '_' : '')
245
+ if (typeof id !== 'string' || !id.startsWith(front)) {
246
+ return false
247
+ }
248
+ // extract the obj_num of this id
249
+ const num = Number.parseInt(id.substr(front.length))
250
+
251
+ // if we didn't get a positive number or we already released this number
252
+ // then return false.
253
+ if (typeof num !== 'number' || num <= 0 || this.releasedNums.includes(num)) {
254
+ return false
255
+ }
256
+
257
+ // push the released number into the released queue
258
+ this.releasedNums.push(num)
259
+
260
+ return true
261
+ }
262
+
263
+ /**
264
+ * Returns the number of layers in the current drawing.
265
+ * @returns {Integer} The number of layers in the current drawing.
266
+ */
267
+ getNumLayers () {
268
+ return this.all_layers.length
269
+ }
270
+
271
+ /**
272
+ * Check if layer with given name already exists.
273
+ * @param {string} name - The layer name to check
274
+ * @returns {boolean}
275
+ */
276
+ hasLayer (name) {
277
+ return this.layer_map[name] !== undefined
278
+ }
279
+
280
+ /**
281
+ * Returns the name of the ith layer. If the index is out of range, an empty string is returned.
282
+ * @param {Integer} i - The zero-based index of the layer you are querying.
283
+ * @returns {string} The name of the ith layer (or the empty string if none found)
284
+ */
285
+ getLayerName (i) {
286
+ return i >= 0 && i < this.getNumLayers() ? this.all_layers[i].getName() : ''
287
+ }
288
+
289
+ /**
290
+ * @returns {SVGGElement|null} The SVGGElement representing the current layer.
291
+ */
292
+ getCurrentLayer () {
293
+ return this.current_layer ? this.current_layer.getGroup() : null
294
+ }
295
+
296
+ /**
297
+ * Get a layer by name.
298
+ * @param {string} name
299
+ * @returns {SVGGElement} The SVGGElement representing the named layer or null.
300
+ */
301
+ getLayerByName (name) {
302
+ const layer = this.layer_map[name]
303
+ return layer ? layer.getGroup() : null
304
+ }
305
+
306
+ /**
307
+ * Returns the name of the currently selected layer. If an error occurs, an empty string
308
+ * is returned.
309
+ * @returns {string} The name of the currently active layer (or the empty string if none found).
310
+ */
311
+ getCurrentLayerName () {
312
+ return this.current_layer ? this.current_layer.getName() : ''
313
+ }
314
+
315
+ /**
316
+ * Set the current layer's name.
317
+ * @param {string} name - The new name.
318
+ * @param {module:history.HistoryRecordingService} hrService - History recording service
319
+ * @returns {string|null} The new name if changed; otherwise, null.
320
+ */
321
+ setCurrentLayerName (name, hrService) {
322
+ let finalName = null
323
+ if (this.current_layer) {
324
+ const oldName = this.current_layer.getName()
325
+ finalName = this.current_layer.setName(name, hrService)
326
+ if (finalName) {
327
+ delete this.layer_map[oldName]
328
+ this.layer_map[finalName] = this.current_layer
329
+ }
330
+ }
331
+ return finalName
332
+ }
333
+
334
+ /**
335
+ * Set the current layer's position.
336
+ * @param {Integer} newpos - The zero-based index of the new position of the layer. Range should be 0 to layers-1
337
+ * @returns {{title: SVGGElement, previousName: string}|null} If the name was changed, returns {title:SVGGElement, previousName:string}; otherwise null.
338
+ */
339
+ setCurrentLayerPosition (newpos) {
340
+ const layerCount = this.getNumLayers()
341
+ if (!this.current_layer || newpos < 0 || newpos >= layerCount) {
342
+ return null
343
+ }
344
+
345
+ const oldpos = this.indexCurrentLayer()
346
+ if ((oldpos === -1) || (oldpos === newpos)) { return null }
347
+
348
+ // if our new position is below us, we need to insert before the node after newpos
349
+ const currentGroup = this.current_layer.getGroup()
350
+ const oldNextSibling = currentGroup.nextSibling
351
+
352
+ let refGroup = null
353
+ if (newpos > oldpos) {
354
+ if (newpos < layerCount - 1) {
355
+ refGroup = this.all_layers[newpos + 1].getGroup()
356
+ }
357
+ // if our new position is above us, we need to insert before the node at newpos
358
+ } else {
359
+ refGroup = this.all_layers[newpos].getGroup()
360
+ }
361
+ this.svgElem_.insertBefore(currentGroup, refGroup) // Ok to replace with `refGroup.before(currentGroup);`?
362
+
363
+ this.identifyLayers()
364
+ this.setCurrentLayer(this.getLayerName(newpos))
365
+
366
+ return {
367
+ currentGroup,
368
+ oldNextSibling
369
+ }
370
+ }
371
+
372
+ /**
373
+ * @param {module:history.HistoryRecordingService} hrService
374
+ * @returns {void}
375
+ */
376
+ mergeLayer (hrService) {
377
+ const currentGroup = this.current_layer.getGroup()
378
+ const prevGroup = currentGroup.previousElementSibling
379
+ if (!prevGroup) { return }
380
+
381
+ hrService.startBatchCommand('Merge Layer')
382
+
383
+ const layerNextSibling = currentGroup.nextSibling
384
+ hrService.removeElement(currentGroup, layerNextSibling, this.svgElem_)
385
+
386
+ while (currentGroup.firstChild) {
387
+ const child = currentGroup.firstChild
388
+ if (child.localName === 'title') {
389
+ hrService.removeElement(child, child.nextSibling, currentGroup)
390
+ child.remove()
391
+ continue
392
+ }
393
+ const oldNextSibling = child.nextSibling
394
+ prevGroup.append(child)
395
+ hrService.moveElement(child, oldNextSibling, currentGroup)
396
+ }
397
+
398
+ // Remove current layer's group
399
+ this.current_layer.removeGroup()
400
+ // Remove the current layer and set the previous layer as the new current layer
401
+ const index = this.indexCurrentLayer()
402
+ if (index > 0) {
403
+ const name = this.current_layer.getName()
404
+ this.current_layer = this.all_layers[index - 1]
405
+ this.all_layers.splice(index, 1)
406
+ delete this.layer_map[name]
407
+ }
408
+
409
+ hrService.endBatchCommand()
410
+ }
411
+
412
+ /**
413
+ * @param {module:history.HistoryRecordingService} hrService
414
+ * @returns {void}
415
+ */
416
+ mergeAllLayers (hrService) {
417
+ // Set the current layer to the last layer.
418
+ this.current_layer = this.all_layers[this.all_layers.length - 1]
419
+
420
+ hrService.startBatchCommand('Merge all Layers')
421
+ while (this.all_layers.length > 1) {
422
+ this.mergeLayer(hrService)
423
+ }
424
+ hrService.endBatchCommand()
425
+ }
426
+
427
+ /**
428
+ * Sets the current layer. If the name is not a valid layer name, then this
429
+ * function returns `false`. Otherwise it returns `true`. This is not an
430
+ * undo-able action.
431
+ * @param {string} name - The name of the layer you want to switch to.
432
+ * @returns {boolean} `true` if the current layer was switched, otherwise `false`
433
+ */
434
+ setCurrentLayer (name) {
435
+ const layer = this.layer_map[name]
436
+ if (layer) {
437
+ if (this.current_layer) {
438
+ this.current_layer.deactivate()
439
+ }
440
+ this.current_layer = layer
441
+ this.current_layer.activate()
442
+ return true
443
+ }
444
+ return false
445
+ }
446
+
447
+ /**
448
+ * Sets the current layer. If the name is not a valid layer name, then this
449
+ * function returns `false`. Otherwise it returns `true`. This is not an
450
+ * undo-able action.
451
+ * @param {string} name - The name of the layer you want to switch to.
452
+ * @returns {boolean} `true` if the current layer was switched, otherwise `false`
453
+ */
454
+ indexCurrentLayer () {
455
+ return this.all_layers.indexOf(this.current_layer)
456
+ }
457
+
458
+ /**
459
+ * Deletes the current layer from the drawing and then clears the selection.
460
+ * This function then calls the 'changed' handler. This is an undoable action.
461
+ * @todo Does this actually call the 'changed' handler?
462
+ * @returns {SVGGElement} The SVGGElement of the layer removed or null.
463
+ */
464
+ deleteCurrentLayer () {
465
+ if (this.current_layer && this.getNumLayers() > 1) {
466
+ const oldLayerGroup = this.current_layer.removeGroup()
467
+ this.identifyLayers()
468
+ return oldLayerGroup
469
+ }
470
+ return null
471
+ }
472
+
473
+ /**
474
+ * Updates layer system and sets the current layer to the
475
+ * top-most layer (last `<g>` child of this drawing).
476
+ * @returns {void}
477
+ */
478
+ identifyLayers () {
479
+ this.all_layers = []
480
+ this.layer_map = {}
481
+ const numchildren = this.svgElem_.childNodes.length
482
+ // loop through all children of SVG element
483
+ const orphans = []; const layernames = []
484
+ let layer = null
485
+ let childgroups = false
486
+ for (let i = 0; i < numchildren; ++i) {
487
+ const child = this.svgElem_.childNodes.item(i)
488
+ // for each g, find its layer name
489
+ if (child?.nodeType === 1) {
490
+ if (child.tagName === 'g') {
491
+ childgroups = true
492
+ const name = findLayerNameInGroup(child)
493
+ if (name) {
494
+ layernames.push(name)
495
+ layer = new Layer(name, child)
496
+ this.all_layers.push(layer)
497
+ this.layer_map[name] = layer
498
+ } else {
499
+ // if group did not have a name, it is an orphan
500
+ orphans.push(child)
501
+ }
502
+ } else if (visElems.includes(child.nodeName)) {
503
+ // Child is "visible" (i.e. not a <title> or <defs> element), so it is an orphan
504
+ orphans.push(child)
505
+ }
506
+ }
507
+ }
508
+
509
+ // If orphans or no layers found, create a new layer and add all the orphans to it
510
+ if (orphans.length > 0 || !childgroups) {
511
+ layer = new Layer(getNewLayerName(layernames), null, this.svgElem_)
512
+ layer.appendChildren(orphans)
513
+ this.all_layers.push(layer)
514
+ this.layer_map[name] = layer
515
+ } else {
516
+ layer.activate()
517
+ }
518
+ this.current_layer = layer
519
+ }
520
+
521
+ /**
522
+ * Creates a new top-level layer in the drawing with the given name and
523
+ * makes it the current layer.
524
+ * @param {string} name - The given name. If the layer name exists, a new name will be generated.
525
+ * @param {module:history.HistoryRecordingService} hrService - History recording service
526
+ * @returns {SVGGElement} The SVGGElement of the new layer, which is
527
+ * also the current layer of this drawing.
528
+ */
529
+ createLayer (name, hrService) {
530
+ if (this.current_layer) {
531
+ this.current_layer.deactivate()
532
+ }
533
+ // Check for duplicate name.
534
+ if (name === undefined || name === null || name === '' || this.layer_map[name]) {
535
+ name = getNewLayerName(Object.keys(this.layer_map))
536
+ }
537
+
538
+ // Crate new layer and add to DOM as last layer
539
+ const layer = new Layer(name, null, this.svgElem_)
540
+ // Like to assume hrService exists, but this is backwards compatible with old version of createLayer.
541
+ if (hrService) {
542
+ hrService.startBatchCommand('Create Layer')
543
+ hrService.insertElement(layer.getGroup())
544
+ hrService.endBatchCommand()
545
+ }
546
+
547
+ this.all_layers.push(layer)
548
+ this.layer_map[name] = layer
549
+ this.current_layer = layer
550
+ return layer.getGroup()
551
+ }
552
+
553
+ /**
554
+ * Creates a copy of the current layer with the given name and makes it the current layer.
555
+ * @param {string} name - The given name. If the layer name exists, a new name will be generated.
556
+ * @param {module:history.HistoryRecordingService} hrService - History recording service
557
+ * @returns {SVGGElement} The SVGGElement of the new layer, which is
558
+ * also the current layer of this drawing.
559
+ */
560
+ cloneLayer (name, hrService) {
561
+ if (!this.current_layer) { return null }
562
+ this.current_layer.deactivate()
563
+ // Check for duplicate name.
564
+ if (name === undefined || name === null || name === '' || this.layer_map[name]) {
565
+ name = getNewLayerName(Object.keys(this.layer_map))
566
+ }
567
+
568
+ // Create new group and add to DOM just after current_layer
569
+ const currentGroup = this.current_layer.getGroup()
570
+ const layer = new Layer(name, currentGroup, this.svgElem_)
571
+ const group = layer.getGroup()
572
+
573
+ // Clone children
574
+ const children = [...currentGroup.childNodes]
575
+ children.forEach((child) => {
576
+ if (child.localName === 'title') { return }
577
+ group.append(this.copyElem(child))
578
+ })
579
+
580
+ if (hrService) {
581
+ hrService.startBatchCommand('Duplicate Layer')
582
+ hrService.insertElement(group)
583
+ hrService.endBatchCommand()
584
+ }
585
+
586
+ // Update layer containers and current_layer.
587
+ const index = this.indexCurrentLayer()
588
+ if (index >= 0) {
589
+ this.all_layers.splice(index + 1, 0, layer)
590
+ } else {
591
+ this.all_layers.push(layer)
592
+ }
593
+ this.layer_map[name] = layer
594
+ this.current_layer = layer
595
+ return group
596
+ }
597
+
598
+ /**
599
+ * Returns whether the layer is visible. If the layer name is not valid,
600
+ * then this function returns `false`.
601
+ * @param {string} layerName - The name of the layer which you want to query.
602
+ * @returns {boolean} The visibility state of the layer, or `false` if the layer name was invalid.
603
+ */
604
+ getLayerVisibility (layerName) {
605
+ const layer = this.layer_map[layerName]
606
+ return layer ? layer.isVisible() : false
607
+ }
608
+
609
+ /**
610
+ * Sets the visibility of the layer. If the layer name is not valid, this
611
+ * function returns `null`, otherwise it returns the `SVGElement` representing
612
+ * the layer. This is an undo-able action.
613
+ * @param {string} layerName - The name of the layer to change the visibility
614
+ * @param {boolean} bVisible - Whether the layer should be visible
615
+ * @returns {?SVGGElement} The SVGGElement representing the layer if the
616
+ * `layerName` was valid, otherwise `null`.
617
+ */
618
+ setLayerVisibility (layerName, bVisible) {
619
+ if (typeof bVisible !== 'boolean') {
620
+ return null
621
+ }
622
+ const layer = this.layer_map[layerName]
623
+ if (!layer) { return null }
624
+ layer.setVisible(bVisible)
625
+ return layer.getGroup()
626
+ }
627
+
628
+ /**
629
+ * Returns the opacity of the given layer. If the input name is not a layer, `null` is returned.
630
+ * @param {string} layerName - name of the layer on which to get the opacity
631
+ * @returns {?Float} The opacity value of the given layer. This will be a value between 0.0 and 1.0, or `null`
632
+ * if `layerName` is not a valid layer
633
+ */
634
+ getLayerOpacity (layerName) {
635
+ const layer = this.layer_map[layerName]
636
+ if (!layer) { return null }
637
+ return layer.getOpacity()
638
+ }
639
+
640
+ /**
641
+ * Sets the opacity of the given layer. If the input name is not a layer,
642
+ * nothing happens. If opacity is not a value between 0.0 and 1.0, then
643
+ * nothing happens.
644
+ * NOTE: this function exists solely to apply a highlighting/de-emphasis
645
+ * effect to a layer. When it is possible for a user to affect the opacity
646
+ * of a layer, we will need to allow this function to produce an undo-able
647
+ * action.
648
+ * @param {string} layerName - Name of the layer on which to set the opacity
649
+ * @param {Float} opacity - A float value in the range 0.0-1.0
650
+ * @returns {void}
651
+ */
652
+ setLayerOpacity (layerName, opacity) {
653
+ if (typeof opacity !== 'number' || opacity < 0.0 || opacity > 1.0) {
654
+ return
655
+ }
656
+ const layer = this.layer_map[layerName]
657
+ if (layer) {
658
+ layer.setOpacity(opacity)
659
+ }
660
+ }
661
+
662
+ /**
663
+ * Create a clone of an element, updating its ID and its children's IDs when needed.
664
+ * @param {Element} el - DOM element to clone
665
+ * @returns {Element}
666
+ */
667
+ copyElem (el) {
668
+ const that = this
669
+ const getNextIdClosure = function () { return that.getNextId() }
670
+ return utilCopyElem(el, getNextIdClosure)
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Called to ensure that drawings will or will not have randomized ids.
676
+ * The currentDrawing will have its nonce set if it doesn't already.
677
+ * @function module:draw.randomizeIds
678
+ * @param {boolean} enableRandomization - flag indicating if documents should have randomized ids
679
+ * @param {draw.Drawing} currentDrawing
680
+ * @returns {void}
681
+ */
682
+ export const randomizeIds = function (enableRandomization, currentDrawing) {
683
+ randIds = enableRandomization === false
684
+ ? RandomizeModes.NEVER_RANDOMIZE
685
+ : RandomizeModes.ALWAYS_RANDOMIZE
686
+
687
+ if (randIds === RandomizeModes.ALWAYS_RANDOMIZE && !currentDrawing.getNonce()) {
688
+ currentDrawing.setNonce(Math.floor(Math.random() * 100001))
689
+ } else if (randIds === RandomizeModes.NEVER_RANDOMIZE && currentDrawing.getNonce()) {
690
+ currentDrawing.clearNonce()
691
+ }
692
+ }
693
+
694
+ // Layer API Functions
695
+
696
+ /**
697
+ * Group: Layers.
698
+ */
699
+
700
+ /**
701
+ * @see {@link https://api.jquery.com/jQuery.data/}
702
+ * @name external:jQuery.data
703
+ */
704
+
705
+ /**
706
+ * @interface module:draw.DrawCanvasInit
707
+ * @property {module:path.pathActions} pathActions
708
+ * @property {module:history.UndoManager} undoMgr
709
+ */
710
+ /**
711
+ * @function module:draw.DrawCanvasInit#getCurrentGroup
712
+ * @returns {Element}
713
+ */
714
+ /**
715
+ * @function module:draw.DrawCanvasInit#setCurrentGroup
716
+ * @param {Element} cg
717
+ * @returns {void}
718
+ */
719
+ /**
720
+ * @function module:draw.DrawCanvasInit#getSelectedElements
721
+ * @returns {Element[]} the array with selected DOM elements
722
+ */
723
+ /**
724
+ * @function module:draw.DrawCanvasInit#getSvgContent
725
+ * @returns {SVGSVGElement}
726
+ */
727
+ /**
728
+ * @function module:draw.DrawCanvasInit#getCurrentDrawing
729
+ * @returns {module:draw.Drawing}
730
+ */
731
+ /**
732
+ * @function module:draw.DrawCanvasInit#clearSelection
733
+ * @param {boolean} [noCall] - When `true`, does not call the "selected" handler
734
+ * @returns {void}
735
+ */
736
+ /**
737
+ * Run the callback function associated with the given event.
738
+ * @function module:draw.DrawCanvasInit#call
739
+ * @param {"changed"|"contextset"} ev - String with the event name
740
+ * @param {module:svgcanvas.SvgCanvas#event:changed|module:svgcanvas.SvgCanvas#event:contextset} arg - Argument to pass through to the callback
741
+ * function. If the event is "changed", a (single-item) array of `Element`s is
742
+ * passed. If the event is "contextset", the arg is `null` or `Element`.
743
+ * @returns {void}
744
+ */
745
+ /**
746
+ * @function module:draw.DrawCanvasInit#addCommandToHistory
747
+ * @param {Command} cmd
748
+ * @returns {void}
749
+ */
750
+ /**
751
+ * @function module:draw.DrawCanvasInit#changeSvgContent
752
+ * @returns {void}
753
+ */
754
+
755
+ let svgCanvas
756
+ /**
757
+ * @function module:draw.init
758
+ * @param {module:draw.DrawCanvasInit} canvas
759
+ * @returns {void}
760
+ */
761
+ export const init = (canvas) => {
762
+ svgCanvas = canvas
763
+ }
764
+
765
+ /**
766
+ * Updates layer system.
767
+ * @function module:draw.identifyLayers
768
+ * @returns {void}
769
+ */
770
+ export const identifyLayers = () => {
771
+ leaveContext()
772
+ svgCanvas.getCurrentDrawing().identifyLayers()
773
+ }
774
+
775
+ /**
776
+ * get current index
777
+ * @function module:draw.identifyLayers
778
+ * @returns {void}
779
+ */
780
+ export const indexCurrentLayer = () => {
781
+ return svgCanvas.getCurrentDrawing().indexCurrentLayer()
782
+ }
783
+
784
+ /**
785
+ * Creates a new top-level layer in the drawing with the given name, sets the current layer
786
+ * to it, and then clears the selection. This function then calls the 'changed' handler.
787
+ * This is an undoable action.
788
+ * @function module:draw.createLayer
789
+ * @param {string} name - The given name
790
+ * @param {module:history.HistoryRecordingService} hrService
791
+ * @fires module:svgcanvas.SvgCanvas#event:changed
792
+ * @returns {void}
793
+ */
794
+ export const createLayer = (name, hrService) => {
795
+ const newLayer = svgCanvas.getCurrentDrawing().createLayer(
796
+ name,
797
+ historyRecordingService(hrService)
798
+ )
799
+ svgCanvas.clearSelection()
800
+ svgCanvas.call('changed', [newLayer])
801
+ }
802
+
803
+ /**
804
+ * Creates a new top-level layer in the drawing with the given name, copies all the current layer's contents
805
+ * to it, and then clears the selection. This function then calls the 'changed' handler.
806
+ * This is an undoable action.
807
+ * @function module:draw.cloneLayer
808
+ * @param {string} name - The given name. If the layer name exists, a new name will be generated.
809
+ * @param {module:history.HistoryRecordingService} hrService - History recording service
810
+ * @fires module:svgcanvas.SvgCanvas#event:changed
811
+ * @returns {void}
812
+ */
813
+ export const cloneLayer = (name, hrService) => {
814
+ // Clone the current layer and make the cloned layer the new current layer
815
+ const newLayer = svgCanvas.getCurrentDrawing().cloneLayer(name, historyRecordingService(hrService))
816
+
817
+ svgCanvas.clearSelection()
818
+ leaveContext()
819
+ svgCanvas.call('changed', [newLayer])
820
+ }
821
+
822
+ /**
823
+ * Deletes the current layer from the drawing and then clears the selection. This function
824
+ * then calls the 'changed' handler. This is an undoable action.
825
+ * @function module:draw.deleteCurrentLayer
826
+ * @fires module:svgcanvas.SvgCanvas#event:changed
827
+ * @returns {boolean} `true` if an old layer group was found to delete
828
+ */
829
+ export const deleteCurrentLayer = () => {
830
+ const { BatchCommand, RemoveElementCommand } = svgCanvas.history
831
+ let currentLayer = svgCanvas.getCurrentDrawing().getCurrentLayer()
832
+ const { nextSibling } = currentLayer
833
+ const parent = currentLayer.parentNode
834
+ currentLayer = svgCanvas.getCurrentDrawing().deleteCurrentLayer()
835
+ if (currentLayer) {
836
+ const batchCmd = new BatchCommand('Delete Layer')
837
+ // store in our Undo History
838
+ batchCmd.addSubCommand(new RemoveElementCommand(currentLayer, nextSibling, parent))
839
+ svgCanvas.addCommandToHistory(batchCmd)
840
+ svgCanvas.clearSelection()
841
+ svgCanvas.call('changed', [parent])
842
+ return true
843
+ }
844
+ return false
845
+ }
846
+
847
+ /**
848
+ * Sets the current layer. If the name is not a valid layer name, then this function returns
849
+ * false. Otherwise it returns true. This is not an undo-able action.
850
+ * @function module:draw.setCurrentLayer
851
+ * @param {string} name - The name of the layer you want to switch to.
852
+ * @returns {boolean} true if the current layer was switched, otherwise false
853
+ */
854
+ export const setCurrentLayer = (name) => {
855
+ const result = svgCanvas.getCurrentDrawing().setCurrentLayer(toXml(name))
856
+ if (result) {
857
+ svgCanvas.clearSelection()
858
+ }
859
+ return result
860
+ }
861
+
862
+ /**
863
+ * Renames the current layer. If the layer name is not valid (i.e. unique), then this function
864
+ * does nothing and returns `false`, otherwise it returns `true`. This is an undo-able action.
865
+ * @function module:draw.renameCurrentLayer
866
+ * @param {string} newName - the new name you want to give the current layer. This name must
867
+ * be unique among all layer names.
868
+ * @fires module:svgcanvas.SvgCanvas#event:changed
869
+ * @returns {boolean} Whether the rename succeeded
870
+ */
871
+ export const renameCurrentLayer = (newName) => {
872
+ const drawing = svgCanvas.getCurrentDrawing()
873
+ const layer = drawing.getCurrentLayer()
874
+ if (layer) {
875
+ const result = drawing.setCurrentLayerName(newName, historyRecordingService())
876
+ if (result) {
877
+ svgCanvas.call('changed', [layer])
878
+ return true
879
+ }
880
+ }
881
+ return false
882
+ }
883
+
884
+ /**
885
+ * Changes the position of the current layer to the new value. If the new index is not valid,
886
+ * this function does nothing and returns false, otherwise it returns true. This is an
887
+ * undo-able action.
888
+ * @function module:draw.setCurrentLayerPosition
889
+ * @param {Integer} newPos - The zero-based index of the new position of the layer. This should be between
890
+ * 0 and (number of layers - 1)
891
+ * @returns {boolean} `true` if the current layer position was changed, `false` otherwise.
892
+ */
893
+ export const setCurrentLayerPosition = (newPos) => {
894
+ const { MoveElementCommand } = svgCanvas.history
895
+ const drawing = svgCanvas.getCurrentDrawing()
896
+ const result = drawing.setCurrentLayerPosition(newPos)
897
+ if (result) {
898
+ svgCanvas.addCommandToHistory(new MoveElementCommand(result.currentGroup, result.oldNextSibling, svgCanvas.getSvgContent()))
899
+ return true
900
+ }
901
+ return false
902
+ }
903
+
904
+ /**
905
+ * Sets the visibility of the layer. If the layer name is not valid, this function return
906
+ * `false`, otherwise it returns `true`. This is an undo-able action.
907
+ * @function module:draw.setLayerVisibility
908
+ * @param {string} layerName - The name of the layer to change the visibility
909
+ * @param {boolean} bVisible - Whether the layer should be visible
910
+ * @returns {boolean} true if the layer's visibility was set, false otherwise
911
+ */
912
+ export const setLayerVisibility = (layerName, bVisible) => {
913
+ const { ChangeElementCommand } = svgCanvas.history
914
+ const drawing = svgCanvas.getCurrentDrawing()
915
+ const prevVisibility = drawing.getLayerVisibility(layerName)
916
+ const layer = drawing.setLayerVisibility(layerName, bVisible)
917
+ if (layer) {
918
+ const oldDisplay = prevVisibility ? 'inline' : 'none'
919
+ svgCanvas.addCommandToHistory(new ChangeElementCommand(layer, { display: oldDisplay }, 'Layer Visibility'))
920
+ } else {
921
+ return false
922
+ }
923
+
924
+ if (layer === drawing.getCurrentLayer()) {
925
+ svgCanvas.clearSelection()
926
+ svgCanvas.pathActions.clear()
927
+ }
928
+ // call('changed', [selected]);
929
+ return true
930
+ }
931
+
932
+ /**
933
+ * Moves the selected elements to layerName. If the name is not a valid layer name, then `false`
934
+ * is returned. Otherwise it returns `true`. This is an undo-able action.
935
+ * @function module:draw.moveSelectedToLayer
936
+ * @param {string} layerName - The name of the layer you want to which you want to move the selected elements
937
+ * @returns {boolean} Whether the selected elements were moved to the layer.
938
+ */
939
+ export const moveSelectedToLayer = (layerName) => {
940
+ const { BatchCommand, MoveElementCommand } = svgCanvas.history
941
+ // find the layer
942
+ const drawing = svgCanvas.getCurrentDrawing()
943
+ const layer = drawing.getLayerByName(layerName)
944
+ if (!layer) { return false }
945
+
946
+ const batchCmd = new BatchCommand('Move Elements to Layer')
947
+
948
+ // loop for each selected element and move it
949
+ const selElems = svgCanvas.getSelectedElements()
950
+ let i = selElems.length
951
+ while (i--) {
952
+ const elem = selElems[i]
953
+ if (!elem) { continue }
954
+ const oldNextSibling = elem.nextSibling
955
+ // TODO: this is pretty brittle!
956
+ const oldLayer = elem.parentNode
957
+ layer.append(elem)
958
+ batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldLayer))
959
+ }
960
+
961
+ svgCanvas.addCommandToHistory(batchCmd)
962
+
963
+ return true
964
+ }
965
+
966
+ /**
967
+ * @function module:draw.mergeLayer
968
+ * @param {module:history.HistoryRecordingService} hrService
969
+ * @returns {void}
970
+ */
971
+ export const mergeLayer = (hrService) => {
972
+ svgCanvas.getCurrentDrawing().mergeLayer(historyRecordingService(hrService))
973
+ svgCanvas.clearSelection()
974
+ leaveContext()
975
+ svgCanvas.changeSvgContent()
976
+ }
977
+
978
+ /**
979
+ * @function module:draw.mergeAllLayers
980
+ * @param {module:history.HistoryRecordingService} hrService
981
+ * @returns {void}
982
+ */
983
+ export const mergeAllLayers = (hrService) => {
984
+ svgCanvas.getCurrentDrawing().mergeAllLayers(historyRecordingService(hrService))
985
+ svgCanvas.clearSelection()
986
+ leaveContext()
987
+ svgCanvas.changeSvgContent()
988
+ }
989
+
990
+ /**
991
+ * Return from a group context to the regular kind, make any previously
992
+ * disabled elements enabled again.
993
+ * @function module:draw.leaveContext
994
+ * @fires module:svgcanvas.SvgCanvas#event:contextset
995
+ * @returns {void}
996
+ */
997
+ export const leaveContext = () => {
998
+ const len = disabledElems.length
999
+ const dataStorage = svgCanvas.getDataStorage()
1000
+ if (len) {
1001
+ for (let i = 0; i < len; i++) {
1002
+ const elem = disabledElems[i]
1003
+ const orig = dataStorage.get(elem, 'orig_opac')
1004
+ if (orig !== 1) {
1005
+ elem.setAttribute('opacity', orig)
1006
+ } else {
1007
+ elem.removeAttribute('opacity')
1008
+ }
1009
+ elem.setAttribute('style', 'pointer-events: inherit')
1010
+ }
1011
+ disabledElems = []
1012
+ svgCanvas.clearSelection(true)
1013
+ svgCanvas.call('contextset', null)
1014
+ }
1015
+ svgCanvas.setCurrentGroup(null)
1016
+ }
1017
+
1018
+ /**
1019
+ * Set the current context (for in-group editing).
1020
+ * @function module:draw.setContext
1021
+ * @param {Element} elem
1022
+ * @fires module:svgcanvas.SvgCanvas#event:contextset
1023
+ * @returns {void}
1024
+ */
1025
+ export const setContext = (elem) => {
1026
+ const dataStorage = svgCanvas.getDataStorage()
1027
+ leaveContext()
1028
+ if (typeof elem === 'string') {
1029
+ elem = getElement(elem)
1030
+ }
1031
+
1032
+ // Edit inside this group
1033
+ svgCanvas.setCurrentGroup(elem)
1034
+
1035
+ // Disable other elements
1036
+ const parentsUntil = getParentsUntil(elem, '#svgcontent')
1037
+ const siblings = []
1038
+ parentsUntil.forEach(function (parent) {
1039
+ const elements = Array.prototype.filter.call(parent.parentNode.children, function (child) {
1040
+ return child !== parent
1041
+ })
1042
+ elements.forEach(function (element) {
1043
+ siblings.push(element)
1044
+ })
1045
+ })
1046
+
1047
+ siblings.forEach(function (curthis) {
1048
+ const opac = curthis.getAttribute('opacity') || 1
1049
+ // Store the original's opacity
1050
+ dataStorage.put(curthis, 'orig_opac', opac)
1051
+ curthis.setAttribute('opacity', opac * 0.33)
1052
+ curthis.setAttribute('style', 'pointer-events: none')
1053
+ disabledElems.push(curthis)
1054
+ })
1055
+ svgCanvas.clearSelection()
1056
+ svgCanvas.call('contextset', svgCanvas.getCurrentGroup())
1057
+ }
1058
+
1059
+ /**
1060
+ * @memberof module:draw
1061
+ * @class Layer
1062
+ * @see {@link module:layer.Layer}
1063
+ */
1064
+ export { Layer }