@svgedit/svgcanvas 7.2.6 → 7.4.1

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/core/copy-elem.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { preventClickDefault } from './utilities.js'
2
+ import dataStorage from './dataStorage.js'
2
3
 
3
4
  /**
4
5
  * Create a clone of an element, updating its ID and its children's IDs when needed.
@@ -7,37 +8,50 @@ import { preventClickDefault } from './utilities.js'
7
8
  * @param {module:utilities.GetNextID} getNextId - The getter of the next unique ID.
8
9
  * @returns {Element} The cloned element
9
10
  */
10
- export const copyElem = function (el, getNextId) {
11
+ export const copyElem = (el, getNextId) => {
12
+ const ownerDocument = el?.ownerDocument || document
11
13
  // manually create a copy of the element
12
- const newEl = document.createElementNS(el.namespaceURI, el.nodeName)
13
- Object.values(el.attributes).forEach((attr) => {
14
- newEl.setAttributeNS(attr.namespaceURI, attr.nodeName, attr.value)
14
+ const newEl = ownerDocument.createElementNS(el.namespaceURI, el.nodeName)
15
+ Array.from(el.attributes).forEach((attr) => {
16
+ if (attr.namespaceURI) {
17
+ newEl.setAttributeNS(attr.namespaceURI, attr.name, attr.value)
18
+ } else {
19
+ newEl.setAttribute(attr.name, attr.value)
20
+ }
15
21
  })
16
22
  // set the copied element's new id
17
23
  newEl.removeAttribute('id')
18
24
  newEl.id = getNextId()
19
25
 
20
26
  // now create copies of all children
21
- el.childNodes.forEach(function (child) {
27
+ el.childNodes.forEach((child) => {
22
28
  switch (child.nodeType) {
23
29
  case 1: // element node
24
30
  newEl.append(copyElem(child, getNextId))
25
31
  break
26
32
  case 3: // text node
27
- newEl.textContent = child.nodeValue
33
+ case 4: // cdata section node
34
+ newEl.append(ownerDocument.createTextNode(child.nodeValue ?? ''))
28
35
  break
29
36
  default:
30
37
  break
31
38
  }
32
39
  })
33
40
 
34
- if (el.dataset.gsvg) {
35
- newEl.dataset.gsvg = newEl.firstChild
36
- } else if (el.dataset.symbol) {
37
- const ref = el.dataset.symbol
38
- newEl.dataset.ref = ref
39
- newEl.dataset.symbol = ref
40
- } else if (newEl.tagName === 'image') {
41
+ if (dataStorage.has(el, 'gsvg')) {
42
+ const firstChild = newEl.firstElementChild || newEl.firstChild
43
+ if (firstChild) {
44
+ dataStorage.put(newEl, 'gsvg', firstChild)
45
+ }
46
+ }
47
+ if (dataStorage.has(el, 'symbol')) {
48
+ dataStorage.put(newEl, 'symbol', dataStorage.get(el, 'symbol'))
49
+ }
50
+ if (dataStorage.has(el, 'ref')) {
51
+ dataStorage.put(newEl, 'ref', dataStorage.get(el, 'ref'))
52
+ }
53
+
54
+ if (newEl.tagName === 'image') {
41
55
  preventClickDefault(newEl)
42
56
  }
43
57
 
@@ -1,28 +1,91 @@
1
- /** A storage solution aimed at replacing jQuerys data function.
2
- * Implementation Note: Elements are stored in a (WeakMap)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap].
3
- * This makes sure the data is garbage collected when the node is removed.
4
- */
5
- const dataStorage = {
6
- _storage: new WeakMap(),
7
- put: function (element, key, obj) {
8
- if (!this._storage.has(element)) {
9
- this._storage.set(element, new Map())
1
+ /**
2
+ * A storage solution aimed at replacing jQuery's data function.
3
+ * Implementation Note: Elements are stored in a [WeakMap](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap).
4
+ * This makes sure the data is garbage collected when the node is removed.
5
+ *
6
+ * @module dataStorage
7
+ * @license MIT
8
+ */
9
+ class DataStorage {
10
+ #storage = new WeakMap()
11
+
12
+ /**
13
+ * Checks if the provided element is a valid WeakMap key.
14
+ * @param {any} element - The element to validate
15
+ * @returns {boolean} True if the element can be used as a WeakMap key
16
+ * @private
17
+ */
18
+ #isValidKey = (element) => {
19
+ return element !== null && (typeof element === 'object' || typeof element === 'function')
20
+ }
21
+
22
+ /**
23
+ * Stores data associated with an element.
24
+ * @param {Object|Function} element - The element to store data for
25
+ * @param {string} key - The key to store the data under
26
+ * @param {any} obj - The data to store
27
+ * @returns {void}
28
+ */
29
+ put (element, key, obj) {
30
+ if (!this.#isValidKey(element)) {
31
+ return
32
+ }
33
+ let elementMap = this.#storage.get(element)
34
+ if (!elementMap) {
35
+ elementMap = new Map()
36
+ this.#storage.set(element, elementMap)
37
+ }
38
+ elementMap.set(key, obj)
39
+ }
40
+
41
+ /**
42
+ * Retrieves data associated with an element.
43
+ * @param {Object|Function} element - The element to retrieve data for
44
+ * @param {string} key - The key the data was stored under
45
+ * @returns {any|undefined} The stored data, or undefined if not found
46
+ */
47
+ get (element, key) {
48
+ if (!this.#isValidKey(element)) {
49
+ return undefined
50
+ }
51
+ return this.#storage.get(element)?.get(key)
52
+ }
53
+
54
+ /**
55
+ * Checks if an element has data stored under a specific key.
56
+ * @param {Object|Function} element - The element to check
57
+ * @param {string} key - The key to check for
58
+ * @returns {boolean} True if the element has data stored under the key
59
+ */
60
+ has (element, key) {
61
+ if (!this.#isValidKey(element)) {
62
+ return false
63
+ }
64
+ return this.#storage.get(element)?.has(key) === true
65
+ }
66
+
67
+ /**
68
+ * Removes data associated with an element.
69
+ * @param {Object|Function} element - The element to remove data from
70
+ * @param {string} key - The key the data was stored under
71
+ * @returns {boolean} True if the data was removed, false otherwise
72
+ */
73
+ remove (element, key) {
74
+ if (!this.#isValidKey(element)) {
75
+ return false
76
+ }
77
+ const elementMap = this.#storage.get(element)
78
+ if (!elementMap) {
79
+ return false
10
80
  }
11
- this._storage.get(element).set(key, obj)
12
- },
13
- get: function (element, key) {
14
- return this._storage.get(element)?.get(key)
15
- },
16
- has: function (element, key) {
17
- return this._storage.has(element) && this._storage.get(element).has(key)
18
- },
19
- remove: function (element, key) {
20
- const ret = this._storage.get(element).delete(key)
21
- if (this._storage.get(element).size === 0) {
22
- this._storage.delete(element)
81
+ const ret = elementMap.delete(key)
82
+ if (elementMap.size === 0) {
83
+ this.#storage.delete(element)
23
84
  }
24
85
  return ret
25
86
  }
26
87
  }
27
88
 
89
+ // Export singleton instance for backward compatibility
90
+ const dataStorage = new DataStorage()
28
91
  export default dataStorage
package/core/draw.js CHANGED
@@ -12,6 +12,7 @@ import { NS } from './namespaces.js'
12
12
  import { toXml, getElement } from './utilities.js'
13
13
  import { copyElem as utilCopyElem } from './copy-elem.js'
14
14
  import { getParentsUntil } from '../common/util.js'
15
+ import { warn } from '../common/logger.js'
15
16
 
16
17
  const visElems =
17
18
  'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'.split(
@@ -32,7 +33,7 @@ let disabledElems = []
32
33
  * @param {module:history.HistoryRecordingService} [hrService] - if exists, return it instead of creating a new service.
33
34
  * @returns {module:history.HistoryRecordingService}
34
35
  */
35
- function historyRecordingService (hrService) {
36
+ const historyRecordingService = (hrService) => {
36
37
  return hrService || new HistoryRecordingService(svgCanvas.undoMgr)
37
38
  }
38
39
 
@@ -41,18 +42,18 @@ function historyRecordingService (hrService) {
41
42
  * @param {Element} group The group element to search in.
42
43
  * @returns {string} The layer name or empty string.
43
44
  */
44
- function findLayerNameInGroup (group) {
45
+ const findLayerNameInGroup = (group) => {
45
46
  const sel = group.querySelector('title')
46
47
  return sel ? sel.textContent : ''
47
48
  }
48
49
 
49
50
  /**
50
- * Verify the classList of the given element : if the classList contains 'layer', return true, then return false
51
+ * Checks if the given element's classList contains 'layer'.
51
52
  *
52
53
  * @param {Element} element - The given element
53
- * @returns {boolean} Return true if the classList contains 'layer' then return false
54
+ * @returns {boolean} True if the classList contains 'layer', false otherwise
54
55
  */
55
- function isLayerElement (element) {
56
+ const isLayerElement = (element) => {
56
57
  return element.classList.contains('layer')
57
58
  }
58
59
 
@@ -61,7 +62,7 @@ function isLayerElement (element) {
61
62
  * @param {string[]} existingLayerNames - Existing layer names.
62
63
  * @returns {string} - The new name.
63
64
  */
64
- function getNewLayerName (existingLayerNames) {
65
+ const getNewLayerName = (existingLayerNames) => {
65
66
  let i = 1
66
67
  while (existingLayerNames.includes(`Layer ${i}`)) {
67
68
  i++
@@ -163,10 +164,10 @@ export class Drawing {
163
164
  getElem_ (id) {
164
165
  if (this.svgElem_.querySelector) {
165
166
  // querySelector lookup
166
- return this.svgElem_.querySelector('#' + id)
167
+ return this.svgElem_.querySelector(`#${id}`)
167
168
  }
168
169
  // jQuery lookup: twice as slow as xpath in FF
169
- return this.svgElem_.querySelector('[id=' + id + ']')
170
+ return this.svgElem_.querySelector(`[id=${id}]`)
170
171
  }
171
172
 
172
173
  /**
@@ -209,7 +210,7 @@ export class Drawing {
209
210
  */
210
211
  getId () {
211
212
  return this.nonce_
212
- ? this.idPrefix + this.nonce_ + '_' + this.obj_num
213
+ ? `${this.idPrefix}${this.nonce_}_${this.obj_num}`
213
214
  : this.idPrefix + this.obj_num
214
215
  }
215
216
 
@@ -258,12 +259,16 @@ export class Drawing {
258
259
  */
259
260
  releaseId (id) {
260
261
  // confirm if this is a valid id for this Document, else return false
261
- const front = this.idPrefix + (this.nonce_ ? this.nonce_ + '_' : '')
262
+ const front = `${this.idPrefix}${this.nonce_ ? `${this.nonce_}_` : ''}`
262
263
  if (typeof id !== 'string' || !id.startsWith(front)) {
263
264
  return false
264
265
  }
265
266
  // extract the obj_num of this id
266
- const num = Number.parseInt(id.substr(front.length))
267
+ const suffix = id.slice(front.length)
268
+ if (!/^[0-9]+$/.test(suffix)) {
269
+ return false
270
+ }
271
+ const num = Number.parseInt(suffix)
267
272
 
268
273
  // if we didn't get a positive number or we already released this number
269
274
  // then return false.
@@ -612,6 +617,10 @@ export class Drawing {
612
617
  // Clone children
613
618
  const children = [...currentGroup.childNodes]
614
619
  children.forEach(child => {
620
+ if (child.nodeType !== 1) {
621
+ group.append(child.cloneNode(true))
622
+ return
623
+ }
615
624
  if (child.localName === 'title') {
616
625
  return
617
626
  }
@@ -710,10 +719,7 @@ export class Drawing {
710
719
  * @returns {Element}
711
720
  */
712
721
  copyElem (el) {
713
- const that = this
714
- const getNextIdClosure = function () {
715
- return that.getNextId()
716
- }
722
+ const getNextIdClosure = () => this.getNextId()
717
723
  return utilCopyElem(el, getNextIdClosure)
718
724
  }
719
725
  }
@@ -726,7 +732,7 @@ export class Drawing {
726
732
  * @param {draw.Drawing} currentDrawing
727
733
  * @returns {void}
728
734
  */
729
- export const randomizeIds = function (enableRandomization, currentDrawing) {
735
+ export const randomizeIds = (enableRandomization, currentDrawing) => {
730
736
  randIds =
731
737
  enableRandomization === false
732
738
  ? RandomizeModes.NEVER_RANDOMIZE
@@ -868,6 +874,10 @@ export const cloneLayer = (name, hrService) => {
868
874
  const newLayer = svgCanvas
869
875
  .getCurrentDrawing()
870
876
  .cloneLayer(name, historyRecordingService(hrService))
877
+ if (!newLayer) {
878
+ warn('cloneLayer: no layer returned', null, 'draw')
879
+ return
880
+ }
871
881
 
872
882
  svgCanvas.clearSelection()
873
883
  leaveContext()
@@ -883,15 +893,19 @@ export const cloneLayer = (name, hrService) => {
883
893
  */
884
894
  export const deleteCurrentLayer = () => {
885
895
  const { BatchCommand, RemoveElementCommand } = svgCanvas.history
886
- let currentLayer = svgCanvas.getCurrentDrawing().getCurrentLayer()
896
+ const currentLayer = svgCanvas.getCurrentDrawing().getCurrentLayer()
897
+ if (!currentLayer) {
898
+ warn('deleteCurrentLayer: no current layer', null, 'draw')
899
+ return false
900
+ }
887
901
  const { nextSibling } = currentLayer
888
902
  const parent = currentLayer.parentNode
889
- currentLayer = svgCanvas.getCurrentDrawing().deleteCurrentLayer()
890
- if (currentLayer) {
903
+ const removedLayer = svgCanvas.getCurrentDrawing().deleteCurrentLayer()
904
+ if (removedLayer && parent) {
891
905
  const batchCmd = new BatchCommand('Delete Layer')
892
906
  // store in our Undo History
893
907
  batchCmd.addSubCommand(
894
- new RemoveElementCommand(currentLayer, nextSibling, parent)
908
+ new RemoveElementCommand(removedLayer, nextSibling, parent)
895
909
  )
896
910
  svgCanvas.addCommandToHistory(batchCmd)
897
911
  svgCanvas.clearSelection()
@@ -978,20 +992,19 @@ export const setCurrentLayerPosition = newPos => {
978
992
  export const setLayerVisibility = (layerName, bVisible) => {
979
993
  const { ChangeElementCommand } = svgCanvas.history
980
994
  const drawing = svgCanvas.getCurrentDrawing()
981
- const prevVisibility = drawing.getLayerVisibility(layerName)
995
+ const layerGroup = drawing.getLayerByName(layerName)
996
+ if (!layerGroup) {
997
+ warn('setLayerVisibility: layer not found', layerName, 'draw')
998
+ return false
999
+ }
1000
+ const oldDisplay = layerGroup.getAttribute('display')
982
1001
  const layer = drawing.setLayerVisibility(layerName, bVisible)
983
- if (layer) {
984
- const oldDisplay = prevVisibility ? 'inline' : 'none'
985
- svgCanvas.addCommandToHistory(
986
- new ChangeElementCommand(
987
- layer,
988
- { display: oldDisplay },
989
- 'Layer Visibility'
990
- )
991
- )
992
- } else {
1002
+ if (!layer) {
993
1003
  return false
994
1004
  }
1005
+ svgCanvas.addCommandToHistory(
1006
+ new ChangeElementCommand(layer, { display: oldDisplay }, 'Layer Visibility')
1007
+ )
995
1008
 
996
1009
  if (layer === drawing.getCurrentLayer()) {
997
1010
  svgCanvas.clearSelection()
@@ -1024,18 +1037,21 @@ export const moveSelectedToLayer = layerName => {
1024
1037
  let i = selElems.length
1025
1038
  while (i--) {
1026
1039
  const elem = selElems[i]
1027
- if (!elem) {
1040
+ const oldLayer = elem?.parentNode
1041
+ if (!elem || !oldLayer || oldLayer === layer) {
1028
1042
  continue
1029
1043
  }
1030
1044
  const oldNextSibling = elem.nextSibling
1031
- // TODO: this is pretty brittle!
1032
- const oldLayer = elem.parentNode
1033
1045
  layer.append(elem)
1034
1046
  batchCmd.addSubCommand(
1035
1047
  new MoveElementCommand(elem, oldNextSibling, oldLayer)
1036
1048
  )
1037
1049
  }
1038
1050
 
1051
+ if (batchCmd.isEmpty()) {
1052
+ warn('moveSelectedToLayer: no elements moved', null, 'draw')
1053
+ return false
1054
+ }
1039
1055
  svgCanvas.addCommandToHistory(batchCmd)
1040
1056
 
1041
1057
  return true
@@ -1081,12 +1097,13 @@ export const leaveContext = () => {
1081
1097
  for (let i = 0; i < len; i++) {
1082
1098
  const elem = disabledElems[i]
1083
1099
  const orig = dataStorage.get(elem, 'orig_opac')
1084
- if (orig !== 1) {
1085
- elem.setAttribute('opacity', orig)
1086
- } else {
1100
+ if (orig === null || orig === undefined) {
1087
1101
  elem.removeAttribute('opacity')
1102
+ } else {
1103
+ elem.setAttribute('opacity', orig)
1088
1104
  }
1089
1105
  elem.setAttribute('style', 'pointer-events: inherit')
1106
+ dataStorage.remove(elem, 'orig_opac')
1090
1107
  }
1091
1108
  disabledElems = []
1092
1109
  svgCanvas.clearSelection(true)
@@ -1106,7 +1123,22 @@ export const setContext = elem => {
1106
1123
  const dataStorage = svgCanvas.getDataStorage()
1107
1124
  leaveContext()
1108
1125
  if (typeof elem === 'string') {
1109
- elem = getElement(elem)
1126
+ const id = elem
1127
+ try {
1128
+ elem = getElement(id)
1129
+ } catch (e) {
1130
+ elem = null
1131
+ }
1132
+ if (!elem && typeof document !== 'undefined') {
1133
+ const candidate = document.getElementById(id)
1134
+ const svgContent = svgCanvas.getSvgContent?.()
1135
+ elem = candidate && (svgContent ? svgContent.contains(candidate) : true)
1136
+ ? candidate
1137
+ : null
1138
+ }
1139
+ }
1140
+ if (!elem) {
1141
+ return
1110
1142
  }
1111
1143
 
1112
1144
  // Edit inside this group
@@ -1114,8 +1146,14 @@ export const setContext = elem => {
1114
1146
 
1115
1147
  // Disable other elements
1116
1148
  const parentsUntil = getParentsUntil(elem, '#svgcontent')
1149
+ if (!parentsUntil) {
1150
+ return
1151
+ }
1117
1152
  const siblings = []
1118
1153
  parentsUntil.forEach(function (parent) {
1154
+ if (!parent?.parentNode) {
1155
+ return
1156
+ }
1119
1157
  const elements = Array.prototype.filter.call(
1120
1158
  parent.parentNode.children,
1121
1159
  function (child) {
@@ -1128,9 +1166,11 @@ export const setContext = elem => {
1128
1166
  })
1129
1167
 
1130
1168
  siblings.forEach(function (curthis) {
1131
- const opac = curthis.getAttribute('opacity') || 1
1132
1169
  // Store the original's opacity
1133
- dataStorage.put(curthis, 'orig_opac', opac)
1170
+ const origOpacity = curthis.getAttribute('opacity')
1171
+ dataStorage.put(curthis, 'orig_opac', origOpacity)
1172
+ const parsedOpacity = Number.parseFloat(origOpacity)
1173
+ const opac = Number.isFinite(parsedOpacity) ? parsedOpacity : 1
1134
1174
  curthis.setAttribute('opacity', opac * 0.33)
1135
1175
  curthis.setAttribute('style', 'pointer-events: none')
1136
1176
  disabledElems.push(curthis)