@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/sanitize.js CHANGED
@@ -7,7 +7,8 @@
7
7
  */
8
8
 
9
9
  import { getReverseNS, NS } from './namespaces.js'
10
- import { getHref, setHref, getUrlFromAttr } from './utilities.js'
10
+ import { getHref, getRefElem, setHref, getUrlFromAttr } from './utilities.js'
11
+ import { warn } from '../common/logger.js'
11
12
 
12
13
  const REVERSE_NS = getReverseNS()
13
14
 
@@ -21,7 +22,7 @@ const REVERSE_NS = getReverseNS()
21
22
  const svgGenericWhiteList = ['class', 'id', 'display', 'transform', 'style']
22
23
  const svgWhiteList_ = {
23
24
  // SVG Elements
24
- a: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'xlink:href', 'xlink:title'],
25
+ a: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'href', 'mask', 'opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'xlink:href', 'xlink:title'],
25
26
  circle: ['clip-path', 'clip-rule', 'cx', 'cy', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'r', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage'],
26
27
  clipPath: ['clipPathUnits'],
27
28
  defs: [],
@@ -36,20 +37,24 @@ const svgWhiteList_ = {
36
37
  feMergeNode: ['in'],
37
38
  feMorphology: ['in', 'operator', 'radius'],
38
39
  feOffset: ['dx', 'in', 'dy', 'result'],
39
- filter: ['color-interpolation-filters', 'filterRes', 'filterUnits', 'height', 'primitiveUnits', 'requiredFeatures', 'width', 'x', 'xlink:href', 'y'],
40
+ filter: ['color-interpolation-filters', 'filterRes', 'filterUnits', 'height', 'href', 'primitiveUnits', 'requiredFeatures', 'width', 'x', 'xlink:href', 'y'],
40
41
  foreignObject: ['font-size', 'height', 'opacity', 'requiredFeatures', 'width', 'x', 'y'],
41
42
  g: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'font-family', 'font-size', 'font-style', 'font-weight', 'text-anchor'],
42
- image: ['clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity', 'requiredFeatures', 'systemLanguage', 'width', 'x', 'xlink:href', 'xlink:title', 'y'],
43
+ image: [
44
+ 'clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity',
45
+ 'preserveAspectRatio', 'requiredFeatures', 'systemLanguage', 'viewBox',
46
+ 'width', 'x', 'href', 'xlink:href', 'xlink:title', 'y'
47
+ ],
43
48
  line: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'x1', 'x2', 'y1', 'y2'],
44
- linearGradient: ['gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'xlink:href', 'y1', 'y2'],
49
+ linearGradient: ['gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'href', 'xlink:href', 'y1', 'y2'],
45
50
  marker: ['markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'se_type', 'systemLanguage', 'viewBox'],
46
51
  mask: ['height', 'maskContentUnits', 'maskUnits', 'width', 'x', 'y'],
47
52
  metadata: [],
48
53
  path: ['clip-path', 'clip-rule', 'd', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage'],
49
- pattern: ['height', 'patternContentUnits', 'patternTransform', 'patternUnits', 'requiredFeatures', 'systemLanguage', 'viewBox', 'width', 'x', 'xlink:href', 'y'],
54
+ pattern: ['height', 'patternContentUnits', 'patternTransform', 'patternUnits', 'requiredFeatures', 'systemLanguage', 'viewBox', 'width', 'x', 'href', 'xlink:href', 'y'],
50
55
  polygon: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'sides', 'shape', 'edge', 'point', 'starRadiusMultiplier', 'r', 'radialshift', 'r2', 'orient', 'cx', 'cy'],
51
56
  polyline: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'se:connector'],
52
- radialGradient: ['cx', 'cy', 'fx', 'fy', 'gradientTransform', 'gradientUnits', 'r', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'xlink:href'],
57
+ radialGradient: ['cx', 'cy', 'fx', 'fy', 'gradientTransform', 'gradientUnits', 'r', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'href', 'xlink:href'],
53
58
  rect: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'width', 'x', 'y'],
54
59
  stop: ['offset', 'requiredFeatures', 'stop-opacity', 'systemLanguage', 'stop-color', 'gradientUnits', 'gradientTransform'],
55
60
  style: ['type'],
@@ -57,10 +62,10 @@ const svgWhiteList_ = {
57
62
  switch: ['requiredFeatures', 'systemLanguage'],
58
63
  symbol: ['fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'opacity', 'overflow', 'preserveAspectRatio', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'viewBox', 'width', 'height'],
59
64
  text: ['clip-path', 'clip-rule', 'dominant-baseline', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'letter-spacing', 'word-spacing', 'text-decoration', 'textLength', 'lengthAdjust', 'x', 'xml:space', 'y'],
60
- textPath: ['dominant-baseline', 'method', 'requiredFeatures', 'spacing', 'startOffset', 'systemLanguage', 'xlink:href'],
65
+ textPath: ['dominant-baseline', 'href', 'method', 'requiredFeatures', 'spacing', 'startOffset', 'systemLanguage', 'xlink:href'],
61
66
  title: [],
62
67
  tspan: ['clip-path', 'clip-rule', 'dx', 'dy', 'dominant-baseline', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'mask', 'opacity', 'requiredFeatures', 'rotate', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'textLength', 'x', 'xml:space', 'y'],
63
- use: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'mask', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'width', 'x', 'xlink:href', 'y', 'overflow'],
68
+ use: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'href', 'mask', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'width', 'x', 'xlink:href', 'y', 'overflow'],
64
69
  // Filter Primitives
65
70
  feComponentTransfer: ['in', 'result'],
66
71
  feFuncR: ['type', 'tableValues', 'slope', 'intercept', 'amplitude', 'exponent', 'offset'],
@@ -91,7 +96,7 @@ const svgWhiteList_ = {
91
96
  mphantom: [],
92
97
  mprescripts: [],
93
98
  mroot: [],
94
- mrow: ['xlink:href', 'xlink:type', 'xmlns:xlink'],
99
+ mrow: ['href', 'xlink:href', 'xlink:type', 'xmlns:xlink'],
95
100
  mspace: ['depth', 'height', 'width'],
96
101
  msqrt: [],
97
102
  mstyle: ['displaystyle', 'mathbackground', 'mathcolor', 'mathvariant', 'scriptlevel'],
@@ -126,22 +131,24 @@ const svgWhiteList_ = {
126
131
  }
127
132
 
128
133
  // add generic attributes to all elements of the whitelist
129
- Object.keys(svgWhiteList_).forEach((element) => { svgWhiteList_[element] = [...svgWhiteList_[element], ...svgGenericWhiteList] })
134
+ for (const [element, attrs] of Object.entries(svgWhiteList_)) {
135
+ svgWhiteList_[element] = [...attrs, ...svgGenericWhiteList]
136
+ }
130
137
 
131
138
  // Produce a Namespace-aware version of svgWhitelist
132
139
  const svgWhiteListNS_ = {}
133
- Object.entries(svgWhiteList_).forEach(([elt, atts]) => {
140
+ for (const [elt, atts] of Object.entries(svgWhiteList_)) {
134
141
  const attNS = {}
135
- Object.entries(atts).forEach(([_i, att]) => {
142
+ for (const att of atts) {
136
143
  if (att.includes(':')) {
137
- const v = att.split(':')
138
- attNS[v[1]] = NS[(v[0]).toUpperCase()]
144
+ const [prefix, localName] = att.split(':')
145
+ attNS[localName] = NS[prefix.toUpperCase()]
139
146
  } else {
140
147
  attNS[att] = att === 'xmlns' ? NS.XMLNS : null
141
148
  }
142
- })
149
+ }
143
150
  svgWhiteListNS_[elt] = attNS
144
- })
151
+ }
145
152
 
146
153
  /**
147
154
  * Sanitizes the input node and its children.
@@ -190,16 +197,20 @@ export const sanitizeSvg = (node) => {
190
197
  // our whitelist or is a namespace declaration for one of our allowed namespaces
191
198
  if (attrNsURI !== allowedAttrsNS[attrLocalName] && attrNsURI !== NS.XMLNS &&
192
199
  !(attrNsURI === NS.XMLNS && REVERSE_NS[attr.value])) {
193
- // Bypassing the whitelist to allow se: and oi: prefixes
194
- // We can add specific namepaces on demand for now.
195
- // Is there a more appropriate way to do this?
196
- if (attrName.startsWith('se:') || attrName.startsWith('oi:') || attrName.startsWith('data-')) {
197
- // We should bypass the namespace aswell
198
- const seAttrNS = (attrName.startsWith('se:')) ? NS.SE : ((attrName.startsWith('oi:')) ? NS.OI : null)
199
- seAttrs.push([attrName, attr.value, seAttrNS])
200
- } else {
201
- console.warn(`sanitizeSvg: attribute ${attrName} in element ${node.nodeName} not in whitelist is removed`)
202
- node.removeAttributeNS(attrNsURI, attrLocalName)
200
+ // Special case: allow href attribute even without namespace if it's in the whitelist
201
+ const isHrefAttribute = (attrLocalName === 'href' && allowedAttrs.includes('href'))
202
+ if (!isHrefAttribute) {
203
+ // Bypassing the whitelist to allow se: and oi: prefixes
204
+ // We can add specific namepaces on demand for now.
205
+ // Is there a more appropriate way to do this?
206
+ if (attrName.startsWith('se:') || attrName.startsWith('oi:') || attrName.startsWith('data-')) {
207
+ // We should bypass the namespace aswell
208
+ const seAttrNS = (attrName.startsWith('se:')) ? NS.SE : ((attrName.startsWith('oi:')) ? NS.OI : null)
209
+ seAttrs.push([attrName, attr.value, seAttrNS])
210
+ } else {
211
+ warn(`attribute ${attrName} in element ${node.nodeName} not in whitelist is removed: ${node.outerHTML}`, null, 'sanitize')
212
+ node.removeAttributeNS(attrNsURI, attrLocalName)
213
+ }
203
214
  }
204
215
  }
205
216
 
@@ -220,38 +231,64 @@ export const sanitizeSvg = (node) => {
220
231
  }
221
232
  }
222
233
 
234
+ // If legacy xlink:href is present but href is missing, mirror it to href for modern browsers
235
+ const xlinkHref = node.getAttributeNS(NS.XLINK, 'href')
236
+ if (xlinkHref) {
237
+ node.setAttribute('href', xlinkHref)
238
+ node.removeAttributeNS(NS.XLINK, 'href')
239
+ }
240
+
223
241
  Object.values(seAttrs).forEach(([att, val, ns]) => {
224
242
  node.setAttributeNS(ns, att, val)
225
243
  })
226
244
 
227
- // for some elements that have a xlink:href, ensure the URI refers to a local element
228
- // (but not for links)
245
+ // for some elements that have a xlink:href or href, ensure the URI refers to a local element
246
+ // (but not for links and other elements where external hrefs are allowed)
229
247
  const href = getHref(node)
230
248
  if (href &&
231
249
  ['filter', 'linearGradient', 'pattern',
232
250
  'radialGradient', 'textPath', 'use'].includes(node.nodeName) && href[0] !== '#') {
233
251
  // remove the attribute (but keep the element)
234
252
  setHref(node, '')
235
- console.warn(`sanitizeSvg: attribute href in element ${node.nodeName} pointing to a non-local reference (${href}) is removed`)
253
+ warn(`attribute href in element ${node.nodeName} pointing to a non-local reference (${href}) is removed: ${node.outerHTML}`, null, 'sanitize')
236
254
  node.removeAttributeNS(NS.XLINK, 'href')
255
+ node.removeAttribute('href')
237
256
  }
238
257
 
239
258
  // Safari crashes on a <use> without a xlink:href, so we just remove the node here
240
259
  if (node.nodeName === 'use' && !getHref(node)) {
241
- console.warn(`sanitizeSvg: element ${node.nodeName} without a xlink:href is removed`)
260
+ warn(`element ${node.nodeName} without a xlink:href or href is removed: ${node.outerHTML}`, null, 'sanitize')
242
261
  node.remove()
243
262
  return
244
263
  }
264
+ // For <use> elements with missing width/height, derive defaults from referenced viewBox/size for proper sizing/selection
265
+ if (node.nodeName === 'use') {
266
+ const ref = getRefElem(getHref(node))
267
+ if (ref) {
268
+ const refViewBox = ref.getAttribute('viewBox')
269
+ const viewBoxParts = refViewBox ? refViewBox.split(/[\s,]+/).map(Number) : null
270
+ const refWidth = Number(ref.getAttribute('width'))
271
+ const refHeight = Number(ref.getAttribute('height'))
272
+ if (!node.hasAttribute('width')) {
273
+ const width = viewBoxParts?.[2] || refWidth
274
+ if (width) node.setAttribute('width', width)
275
+ }
276
+ if (!node.hasAttribute('height')) {
277
+ const height = viewBoxParts?.[3] || refHeight
278
+ if (height) node.setAttribute('height', height)
279
+ }
280
+ }
281
+ }
245
282
  // if the element has attributes pointing to a non-local reference,
246
283
  // need to remove the attribute
247
- Object.values(['clip-path', 'fill', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke'], (attr) => {
284
+ ['clip-path', 'fill', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke'].forEach((attr) => {
248
285
  let val = node.getAttribute(attr)
249
286
  if (val) {
250
287
  val = getUrlFromAttr(val)
251
288
  // simply check for first character being a '#'
252
289
  if (val && val[0] !== '#') {
253
290
  node.setAttribute(attr, '')
254
- console.warn(`sanitizeSvg: attribute ${attr} in element ${node.nodeName} pointing to a non-local reference (${val}) is removed`)
291
+ warn(`attribute ${attr} in element ${node.nodeName} pointing to a non-local reference (${val}) is removed: ${node.outerHTML}`, null, 'sanitize')
255
292
  node.removeAttribute(attr)
256
293
  }
257
294
  }
@@ -264,7 +301,7 @@ export const sanitizeSvg = (node) => {
264
301
  } else {
265
302
  // remove all children from this node and insert them before this node
266
303
  // TODO: in the case of animation elements this will hardly ever be correct
267
- console.warn(`sanitizeSvg: element ${node.nodeName} not supported is removed`)
304
+ warn(`element ${node.nodeName} not supported is removed: ${node.outerHTML}`, null, 'sanitize')
268
305
  const children = []
269
306
  while (node.hasChildNodes()) {
270
307
  children.push(parent.insertBefore(node.firstChild, node))
package/core/select.js CHANGED
@@ -10,12 +10,37 @@ import { isWebkit } from '../common/browser.js'
10
10
  import { getRotationAngle, getBBox, getStrokedBBox } from './utilities.js'
11
11
  import { transformListToTransform, transformBox, transformPoint, matrixMultiply, getTransformList } from './math.js'
12
12
  import { NS } from './namespaces'
13
+ import { warn } from '../common/logger.js'
13
14
 
14
15
  let svgCanvas
15
- let selectorManager_ // A Singleton
16
16
  // change radius if touch screen
17
17
  const gripRadius = window.ontouchstart ? 10 : 4
18
18
 
19
+ /**
20
+ * Private singleton manager for selector state
21
+ */
22
+ class SelectModule {
23
+ #selectorManager = null
24
+
25
+ /**
26
+ * Initialize the select module with canvas
27
+ * @param {Object} canvas - The SVG canvas instance
28
+ * @returns {void}
29
+ */
30
+ init (canvas) {
31
+ svgCanvas = canvas
32
+ this.#selectorManager = new SelectorManager()
33
+ }
34
+
35
+ /**
36
+ * Get the singleton SelectorManager instance
37
+ * @returns {SelectorManager} The SelectorManager instance
38
+ */
39
+ getSelectorManager () {
40
+ return this.#selectorManager
41
+ }
42
+ }
43
+
19
44
  /**
20
45
  * Private class for DOM element selection boxes.
21
46
  */
@@ -38,14 +63,14 @@ export class Selector {
38
63
  // this holds a reference to the <g> element that holds all visual elements of the selector
39
64
  this.selectorGroup = svgCanvas.createSVGElement({
40
65
  element: 'g',
41
- attr: { id: ('selectorGroup' + this.id) }
66
+ attr: { id: `selectorGroup${this.id}` }
42
67
  })
43
68
 
44
69
  // this holds a reference to the path rect
45
70
  this.selectorRect = svgCanvas.createSVGElement({
46
71
  element: 'path',
47
72
  attr: {
48
- id: ('selectedBox' + this.id),
73
+ id: `selectedBox${this.id}`,
49
74
  fill: 'none',
50
75
  stroke: '#22C',
51
76
  'stroke-width': '1',
@@ -91,11 +116,11 @@ export class Selector {
91
116
  */
92
117
  showGrips (show) {
93
118
  const bShow = show ? 'inline' : 'none'
94
- selectorManager_.selectorGripsGroup.setAttribute('display', bShow)
119
+ selectModule.getSelectorManager().selectorGripsGroup.setAttribute('display', bShow)
95
120
  const elem = this.selectedElement
96
121
  this.hasGrips = show
97
122
  if (elem && show) {
98
- this.selectorGroup.append(selectorManager_.selectorGripsGroup)
123
+ this.selectorGroup.append(selectModule.getSelectorManager().selectorGripsGroup)
99
124
  Selector.updateGripCursors(getRotationAngle(elem))
100
125
  }
101
126
  }
@@ -108,7 +133,7 @@ export class Selector {
108
133
  resize (bbox) {
109
134
  const dataStorage = svgCanvas.getDataStorage()
110
135
  const selectedBox = this.selectorRect
111
- const mgr = selectorManager_
136
+ const mgr = selectModule.getSelectorManager()
112
137
  const selectedGrips = mgr.selectorGrips
113
138
  const selected = this.selectedElement
114
139
  const zoom = svgCanvas.getZoom()
@@ -130,7 +155,7 @@ export class Selector {
130
155
  while (currentElt.parentNode) {
131
156
  if (currentElt.parentNode && currentElt.parentNode.tagName === 'g' && currentElt.parentNode.transform) {
132
157
  if (currentElt.parentNode.transform.baseVal.numberOfItems) {
133
- parentTransformationMatrix = matrixMultiply(transformListToTransform(getTransformList(selected.parentNode)).matrix, parentTransformationMatrix)
158
+ parentTransformationMatrix = matrixMultiply(transformListToTransform(getTransformList(currentElt.parentNode)).matrix, parentTransformationMatrix)
134
159
  }
135
160
  }
136
161
  currentElt = currentElt.parentNode
@@ -213,10 +238,7 @@ export class Selector {
213
238
  nbah = (maxy - miny)
214
239
  }
215
240
 
216
- const dstr = 'M' + nbax + ',' + nbay +
217
- ' L' + (nbax + nbaw) + ',' + nbay +
218
- ' ' + (nbax + nbaw) + ',' + (nbay + nbah) +
219
- ' ' + nbax + ',' + (nbay + nbah) + 'z'
241
+ const dstr = `M${nbax},${nbay} L${nbax + nbaw},${nbay} ${nbax + nbaw},${nbay + nbah} ${nbax},${nbay + nbah}z`
220
242
 
221
243
  const xform = angle ? 'rotate(' + [angle, cx, cy].join(',') + ')' : ''
222
244
 
@@ -257,15 +279,15 @@ export class Selector {
257
279
  * @returns {void}
258
280
  */
259
281
  static updateGripCursors (angle) {
260
- const dirArr = Object.keys(selectorManager_.selectorGrips)
282
+ const dirArr = Object.keys(selectModule.getSelectorManager().selectorGrips)
261
283
  let steps = Math.round(angle / 45)
262
284
  if (steps < 0) { steps += 8 }
263
285
  while (steps > 0) {
264
286
  dirArr.push(dirArr.shift())
265
287
  steps--
266
288
  }
267
- Object.values(selectorManager_.selectorGrips).forEach((gripElement, i) => {
268
- gripElement.setAttribute('style', ('cursor:' + dirArr[i] + '-resize'))
289
+ Object.values(selectModule.getSelectorManager().selectorGrips).forEach((gripElement, i) => {
290
+ gripElement.setAttribute('style', `cursor:${dirArr[i]}-resize`)
269
291
  })
270
292
  }
271
293
  }
@@ -341,10 +363,10 @@ export class SelectorManager {
341
363
  const grip = svgCanvas.createSVGElement({
342
364
  element: 'circle',
343
365
  attr: {
344
- id: ('selectorGrip_resize_' + dir),
366
+ id: `selectorGrip_resize_${dir}`,
345
367
  fill: '#22C',
346
368
  r: gripRadius,
347
- style: ('cursor:' + dir + '-resize'),
369
+ style: `cursor:${dir}-resize`,
348
370
  // This expands the mouse-able area of the grips making them
349
371
  // easier to grab with the mouse.
350
372
  // This works in Opera and WebKit, but does not work in Firefox
@@ -462,7 +484,7 @@ export class SelectorManager {
462
484
  const sel = this.selectorMap[elem.id]
463
485
  if (!sel?.locked) {
464
486
  // TODO(codedread): Ensure this exists in this module.
465
- console.warn('WARNING! selector was released but was already unlocked')
487
+ warn('WARNING! selector was released but was already unlocked', null, 'select')
466
488
  }
467
489
  for (let i = 0; i < N; ++i) {
468
490
  if (this.selectors[i] && this.selectors[i] === sel) {
@@ -541,6 +563,9 @@ export class SelectorManager {
541
563
  * @property {module:select.Dimensions} dimensions
542
564
  */
543
565
 
566
+ // Export singleton instance for backward compatibility
567
+ const selectModule = new SelectModule()
568
+
544
569
  /**
545
570
  * Initializes this module.
546
571
  * @function module:select.init
@@ -549,12 +574,11 @@ export class SelectorManager {
549
574
  * @returns {void}
550
575
  */
551
576
  export const init = (canvas) => {
552
- svgCanvas = canvas
553
- selectorManager_ = new SelectorManager()
577
+ selectModule.init(canvas)
554
578
  }
555
579
 
556
580
  /**
557
581
  * @function module:select.getSelectorManager
558
582
  * @returns {module:select.SelectorManager} The SelectorManager instance.
559
583
  */
560
- export const getSelectorManager = () => selectorManager_
584
+ export const getSelectorManager = () => selectModule.getSelectorManager()