@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/CHANGES.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # svgcanvas CHANGES
2
2
 
3
+
4
+ ## 7.2.7
5
+ - Prefer href to xlink href (#1059)
6
+ - Fix group rotation (#1058)
7
+ - Fixed a bug where a rotated text or image did not translate correctly. (#1055)
8
+
3
9
  ## 7.2.5
4
10
  - update dependencies
5
11
 
package/common/browser.js CHANGED
@@ -8,60 +8,127 @@
8
8
 
9
9
  const NSSVG = 'http://www.w3.org/2000/svg'
10
10
 
11
- const { userAgent } = navigator
12
-
13
- // Note: Browser sniffing should only be used if no other detection method is possible
14
- const isWebkit_ = userAgent.includes('AppleWebKit')
15
- const isGecko_ = userAgent.includes('Gecko/')
16
- const isChrome_ = userAgent.includes('Chrome/')
17
- const isMac_ = userAgent.includes('Macintosh')
18
-
19
- // text character positioning (for IE9 and now Chrome)
20
- const supportsGoodTextCharPos_ = (function () {
21
- const svgroot = document.createElementNS(NSSVG, 'svg')
22
- const svgContent = document.createElementNS(NSSVG, 'svg')
23
- document.documentElement.append(svgroot)
24
- svgContent.setAttribute('x', 5)
25
- svgroot.append(svgContent)
26
- const text = document.createElementNS(NSSVG, 'text')
27
- text.textContent = 'a'
28
- svgContent.append(text)
29
- try { // Chrome now fails here
30
- const pos = text.getStartPositionOfChar(0).x
31
- return (pos === 0)
32
- } catch (err) {
33
- return false
34
- } finally {
35
- svgroot.remove()
11
+ /**
12
+ * Browser capabilities and detection object.
13
+ * Uses modern feature detection and lazy evaluation patterns.
14
+ */
15
+ class BrowserDetector {
16
+ #userAgent = navigator.userAgent
17
+ #cachedResults = new Map()
18
+
19
+ /**
20
+ * Detects if the browser is WebKit-based
21
+ * @returns {boolean}
22
+ */
23
+ get isWebkit () {
24
+ if (!this.#cachedResults.has('isWebkit')) {
25
+ this.#cachedResults.set('isWebkit', this.#userAgent.includes('AppleWebKit'))
26
+ }
27
+ return this.#cachedResults.get('isWebkit')
28
+ }
29
+
30
+ /**
31
+ * Detects if the browser is Gecko-based
32
+ * @returns {boolean}
33
+ */
34
+ get isGecko () {
35
+ if (!this.#cachedResults.has('isGecko')) {
36
+ this.#cachedResults.set('isGecko', this.#userAgent.includes('Gecko/'))
37
+ }
38
+ return this.#cachedResults.get('isGecko')
39
+ }
40
+
41
+ /**
42
+ * Detects if the browser is Chrome
43
+ * @returns {boolean}
44
+ */
45
+ get isChrome () {
46
+ if (!this.#cachedResults.has('isChrome')) {
47
+ this.#cachedResults.set('isChrome', this.#userAgent.includes('Chrome/'))
48
+ }
49
+ return this.#cachedResults.get('isChrome')
50
+ }
51
+
52
+ /**
53
+ * Detects if the platform is macOS
54
+ * @returns {boolean}
55
+ */
56
+ get isMac () {
57
+ if (!this.#cachedResults.has('isMac')) {
58
+ this.#cachedResults.set('isMac', this.#userAgent.includes('Macintosh'))
59
+ }
60
+ return this.#cachedResults.get('isMac')
61
+ }
62
+
63
+ /**
64
+ * Tests if the browser supports accurate text character positioning
65
+ * @returns {boolean}
66
+ */
67
+ get supportsGoodTextCharPos () {
68
+ if (!this.#cachedResults.has('supportsGoodTextCharPos')) {
69
+ this.#cachedResults.set('supportsGoodTextCharPos', this.#testTextCharPos())
70
+ }
71
+ return this.#cachedResults.get('supportsGoodTextCharPos')
36
72
  }
37
- }())
38
73
 
39
- // Public API
74
+ /**
75
+ * Private method to test text character positioning support
76
+ * @returns {boolean}
77
+ */
78
+ #testTextCharPos () {
79
+ const svgroot = document.createElementNS(NSSVG, 'svg')
80
+ const svgContent = document.createElementNS(NSSVG, 'svg')
81
+ document.documentElement.append(svgroot)
82
+ svgContent.setAttribute('x', 5)
83
+ svgroot.append(svgContent)
84
+ const text = document.createElementNS(NSSVG, 'text')
85
+ text.textContent = 'a'
86
+ svgContent.append(text)
40
87
 
88
+ try {
89
+ const pos = text.getStartPositionOfChar(0).x
90
+ return pos === 0
91
+ } catch (err) {
92
+ return false
93
+ } finally {
94
+ svgroot.remove()
95
+ }
96
+ }
97
+ }
98
+
99
+ // Create singleton instance
100
+ const browser = new BrowserDetector()
101
+
102
+ // Export as functions for backward compatibility
41
103
  /**
42
104
  * @function module:browser.isWebkit
43
105
  * @returns {boolean}
44
- */
45
- export const isWebkit = () => isWebkit_
106
+ */
107
+ export const isWebkit = () => browser.isWebkit
108
+
46
109
  /**
47
110
  * @function module:browser.isGecko
48
111
  * @returns {boolean}
49
- */
50
- export const isGecko = () => isGecko_
112
+ */
113
+ export const isGecko = () => browser.isGecko
114
+
51
115
  /**
52
116
  * @function module:browser.isChrome
53
117
  * @returns {boolean}
54
- */
55
- export const isChrome = () => isChrome_
118
+ */
119
+ export const isChrome = () => browser.isChrome
56
120
 
57
121
  /**
58
122
  * @function module:browser.isMac
59
123
  * @returns {boolean}
60
- */
61
- export const isMac = () => isMac_
124
+ */
125
+ export const isMac = () => browser.isMac
62
126
 
63
127
  /**
64
128
  * @function module:browser.supportsGoodTextCharPos
65
129
  * @returns {boolean}
66
- */
67
- export const supportsGoodTextCharPos = () => supportsGoodTextCharPos_
130
+ */
131
+ export const supportsGoodTextCharPos = () => browser.supportsGoodTextCharPos
132
+
133
+ // Export browser instance for direct access
134
+ export default browser
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Centralized logging utility for SVGCanvas.
3
+ * Provides configurable log levels and the ability to disable logging in production.
4
+ * @module logger
5
+ * @license MIT
6
+ */
7
+
8
+ /**
9
+ * Log levels in order of severity
10
+ * @enum {number}
11
+ */
12
+ export const LogLevel = {
13
+ NONE: 0,
14
+ ERROR: 1,
15
+ WARN: 2,
16
+ INFO: 3,
17
+ DEBUG: 4
18
+ }
19
+
20
+ /**
21
+ * Logger configuration
22
+ * @type {Object}
23
+ */
24
+ const config = {
25
+ currentLevel: LogLevel.WARN,
26
+ enabled: true,
27
+ prefix: '[SVGCanvas]'
28
+ }
29
+
30
+ /**
31
+ * Set the logging level
32
+ * @param {LogLevel} level - The log level to set
33
+ * @returns {void}
34
+ */
35
+ export const setLogLevel = (level) => {
36
+ if (Object.values(LogLevel).includes(level)) {
37
+ config.currentLevel = level
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Enable or disable logging
43
+ * @param {boolean} enabled - Whether logging should be enabled
44
+ * @returns {void}
45
+ */
46
+ export const setLoggingEnabled = (enabled) => {
47
+ config.enabled = Boolean(enabled)
48
+ }
49
+
50
+ /**
51
+ * Set the log prefix
52
+ * @param {string} prefix - The prefix to use for log messages
53
+ * @returns {void}
54
+ */
55
+ export const setLogPrefix = (prefix) => {
56
+ config.prefix = String(prefix)
57
+ }
58
+
59
+ /**
60
+ * Format a log message with prefix and context
61
+ * @param {string} message - The log message
62
+ * @param {string} [context=''] - Optional context information
63
+ * @returns {string} Formatted message
64
+ */
65
+ const formatMessage = (message, context = '') => {
66
+ const contextStr = context ? ` [${context}]` : ''
67
+ return `${config.prefix}${contextStr} ${message}`
68
+ }
69
+
70
+ /**
71
+ * Log an error message
72
+ * @param {string} message - The error message
73
+ * @param {Error|any} [error] - Optional error object or additional data
74
+ * @param {string} [context=''] - Optional context (e.g., module name)
75
+ * @returns {void}
76
+ */
77
+ export const error = (message, error, context = '') => {
78
+ if (!config.enabled || config.currentLevel < LogLevel.ERROR) return
79
+
80
+ console.error(formatMessage(message, context))
81
+ if (error) {
82
+ console.error(error)
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Log a warning message
88
+ * @param {string} message - The warning message
89
+ * @param {any} [data] - Optional additional data
90
+ * @param {string} [context=''] - Optional context (e.g., module name)
91
+ * @returns {void}
92
+ */
93
+ export const warn = (message, data, context = '') => {
94
+ if (!config.enabled || config.currentLevel < LogLevel.WARN) return
95
+
96
+ console.warn(formatMessage(message, context))
97
+ if (data !== undefined) {
98
+ console.warn(data)
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Log an info message
104
+ * @param {string} message - The info message
105
+ * @param {any} [data] - Optional additional data
106
+ * @param {string} [context=''] - Optional context (e.g., module name)
107
+ * @returns {void}
108
+ */
109
+ export const info = (message, data, context = '') => {
110
+ if (!config.enabled || config.currentLevel < LogLevel.INFO) return
111
+
112
+ console.info(formatMessage(message, context))
113
+ if (data !== undefined) {
114
+ console.info(data)
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Log a debug message
120
+ * @param {string} message - The debug message
121
+ * @param {any} [data] - Optional additional data
122
+ * @param {string} [context=''] - Optional context (e.g., module name)
123
+ * @returns {void}
124
+ */
125
+ export const debug = (message, data, context = '') => {
126
+ if (!config.enabled || config.currentLevel < LogLevel.DEBUG) return
127
+
128
+ console.debug(formatMessage(message, context))
129
+ if (data !== undefined) {
130
+ console.debug(data)
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Get current logger configuration
136
+ * @returns {Object} Current configuration
137
+ */
138
+ export const getConfig = () => ({ ...config })
139
+
140
+ // Default export as namespace
141
+ export default {
142
+ LogLevel,
143
+ setLogLevel,
144
+ setLoggingEnabled,
145
+ setLogPrefix,
146
+ error,
147
+ warn,
148
+ info,
149
+ debug,
150
+ getConfig
151
+ }
package/common/util.js CHANGED
@@ -2,197 +2,138 @@
2
2
  * @param {any} obj
3
3
  * @returns {any}
4
4
  */
5
- export function findPos (obj) {
6
- let curleft = 0
7
- let curtop = 0
8
- if (obj.offsetParent) {
5
+ export const findPos = (obj) => {
6
+ let left = 0
7
+ let top = 0
8
+
9
+ if (obj?.offsetParent) {
10
+ let current = obj
9
11
  do {
10
- curleft += obj.offsetLeft
11
- curtop += obj.offsetTop
12
- // eslint-disable-next-line no-cond-assign
13
- } while (obj = obj.offsetParent)
14
- return { left: curleft, top: curtop }
12
+ left += current.offsetLeft
13
+ top += current.offsetTop
14
+ current = current.offsetParent
15
+ } while (current)
15
16
  }
16
- return { left: curleft, top: curtop }
17
- }
18
17
 
19
- export function isObject (item) {
20
- return (item && typeof item === 'object' && !Array.isArray(item))
18
+ return { left, top }
21
19
  }
22
20
 
23
- export function mergeDeep (target, source) {
24
- const output = Object.assign({}, target)
21
+ export const isObject = (item) =>
22
+ item && typeof item === 'object' && !Array.isArray(item)
23
+
24
+ export const mergeDeep = (target, source) => {
25
+ const output = { ...target }
26
+
25
27
  if (isObject(target) && isObject(source)) {
26
- Object.keys(source).forEach((key) => {
28
+ for (const key of Object.keys(source)) {
27
29
  if (isObject(source[key])) {
28
- if (!(key in target)) { Object.assign(output, { [key]: source[key] }) } else { output[key] = mergeDeep(target[key], source[key]) }
30
+ output[key] = key in target
31
+ ? mergeDeep(target[key], source[key])
32
+ : source[key]
29
33
  } else {
30
- Object.assign(output, { [key]: source[key] })
34
+ output[key] = source[key]
31
35
  }
32
- })
36
+ }
33
37
  }
38
+
34
39
  return output
35
40
  }
36
41
 
37
42
  /**
38
43
  * Get the closest matching element up the DOM tree.
44
+ * Uses native Element.closest() when possible for better performance.
39
45
  * @param {Element} elem Starting element
40
46
  * @param {String} selector Selector to match against (class, ID, data attribute, or tag)
41
- * @return {Boolean|Element} Returns null if not match found
47
+ * @return {Element|null} Returns null if no match found
42
48
  */
43
- export function getClosest (elem, selector) {
44
- const firstChar = selector.charAt(0)
45
- const supports = 'classList' in document.documentElement
46
- let attribute; let value
47
- // If selector is a data attribute, split attribute from value
48
- if (firstChar === '[') {
49
- selector = selector.substr(1, selector.length - 2)
50
- attribute = selector.split('=')
51
- if (attribute.length > 1) {
52
- value = true
53
- attribute[1] = attribute[1].replace(/"/g, '').replace(/'/g, '')
49
+ export const getClosest = (elem, selector) => {
50
+ // Use native closest for standard CSS selectors
51
+ if (elem?.closest) {
52
+ try {
53
+ return elem.closest(selector)
54
+ } catch (e) {
55
+ // Fallback for invalid selectors
54
56
  }
55
57
  }
56
- // Get closest match
57
- for (; elem && elem !== document && elem.nodeType === 1; elem = elem.parentNode) {
58
- // If selector is a class
59
- if (firstChar === '.') {
60
- if (supports) {
61
- if (elem.classList.contains(selector.substr(1))) {
62
- return elem
63
- }
64
- } else {
65
- if (new RegExp('(^|\\s)' + selector.substr(1) + '(\\s|$)').test(elem.className)) {
66
- return elem
67
- }
68
- }
69
- }
70
- // If selector is an ID
71
- if (firstChar === '#') {
72
- if (elem.id === selector.substr(1)) {
73
- return elem
74
- }
75
- }
76
- // If selector is a data attribute
77
- if (firstChar === '[') {
78
- if (elem.hasAttribute(attribute[0])) {
79
- if (value) {
80
- if (elem.getAttribute(attribute[0]) === attribute[1]) {
81
- return elem
82
- }
83
- } else {
84
- return elem
85
- }
86
- }
87
- }
88
- // If selector is a tag
89
- if (elem.tagName.toLowerCase() === selector) {
90
- return elem
91
- }
58
+
59
+ // Fallback implementation for edge cases
60
+ const selectorMatcher = {
61
+ '.': (el, sel) => el.classList?.contains(sel.slice(1)),
62
+ '#': (el, sel) => el.id === sel.slice(1),
63
+ '[': (el, sel) => {
64
+ const [attr, val] = sel.slice(1, -1).split('=').map(s => s.replace(/["']/g, ''))
65
+ return val ? el.getAttribute(attr) === val : el.hasAttribute(attr)
66
+ },
67
+ tag: (el, sel) => el.tagName?.toLowerCase() === sel
92
68
  }
69
+
70
+ const firstChar = selector.charAt(0)
71
+ const matcher = selectorMatcher[firstChar] || selectorMatcher.tag
72
+
73
+ for (let current = elem; current && current !== document && current.nodeType === 1; current = current.parentNode) {
74
+ if (matcher(current, selector)) return current
75
+ }
76
+
93
77
  return null
94
78
  }
95
79
 
96
80
  /**
97
- * Get all DOM element up the tree that contain a class, ID, or data attribute
81
+ * Get all DOM elements up the tree that match a selector
98
82
  * @param {Node} elem The base element
99
83
  * @param {String} selector The class, id, data attribute, or tag to look for
100
- * @return {Array} Null if no match
84
+ * @return {Array|null} Array of matching elements or null if no match
101
85
  */
102
- export function getParents (elem, selector) {
86
+ export const getParents = (elem, selector) => {
103
87
  const parents = []
88
+ const matchers = {
89
+ '.': (el, sel) => el.classList?.contains(sel.slice(1)),
90
+ '#': (el, sel) => el.id === sel.slice(1),
91
+ '[': (el, sel) => el.hasAttribute(sel.slice(1, -1)),
92
+ tag: (el, sel) => el.tagName?.toLowerCase() === sel
93
+ }
94
+
104
95
  const firstChar = selector?.charAt(0)
105
- // Get matches
106
- for (; elem && elem !== document; elem = elem.parentNode) {
107
- if (selector) {
108
- // If selector is a class
109
- if (firstChar === '.') {
110
- if (elem.classList.contains(selector.substr(1))) {
111
- parents.push(elem)
112
- }
113
- }
114
- // If selector is an ID
115
- if (firstChar === '#') {
116
- if (elem.id === selector.substr(1)) {
117
- parents.push(elem)
118
- }
119
- }
120
- // If selector is a data attribute
121
- if (firstChar === '[') {
122
- if (elem.hasAttribute(selector.substr(1, selector.length - 1))) {
123
- parents.push(elem)
124
- }
125
- }
126
- // If selector is a tag
127
- if (elem.tagName.toLowerCase() === selector) {
128
- parents.push(elem)
129
- }
130
- } else {
131
- parents.push(elem)
96
+ const matcher = selector ? (matchers[firstChar] || matchers.tag) : null
97
+
98
+ for (let current = elem; current && current !== document; current = current.parentNode) {
99
+ if (!selector || matcher(current, selector)) {
100
+ parents.push(current)
132
101
  }
133
102
  }
134
- // Return parents if any exist
135
- return parents.length ? parents : null
103
+
104
+ return parents.length > 0 ? parents : null
136
105
  }
137
106
 
138
- export function getParentsUntil (elem, parent, selector) {
107
+ export const getParentsUntil = (elem, parent, selector) => {
139
108
  const parents = []
140
- const parentType = parent?.charAt(0)
141
- const selectorType = selector?.selector.charAt(0)
142
- // Get matches
143
- for (; elem && elem !== document; elem = elem.parentNode) {
144
- // Check if parent has been reached
145
- if (parent) {
146
- // If parent is a class
147
- if (parentType === '.') {
148
- if (elem.classList.contains(parent.substr(1))) {
149
- break
150
- }
151
- }
152
- // If parent is an ID
153
- if (parentType === '#') {
154
- if (elem.id === parent.substr(1)) {
155
- break
156
- }
157
- }
158
- // If parent is a data attribute
159
- if (parentType === '[') {
160
- if (elem.hasAttribute(parent.substr(1, parent.length - 1))) {
161
- break
162
- }
163
- }
164
- // If parent is a tag
165
- if (elem.tagName.toLowerCase() === parent) {
166
- break
167
- }
109
+
110
+ const matchers = {
111
+ '.': (el, sel) => el.classList?.contains(sel.slice(1)),
112
+ '#': (el, sel) => el.id === sel.slice(1),
113
+ '[': (el, sel) => el.hasAttribute(sel.slice(1, -1)),
114
+ tag: (el, sel) => el.tagName?.toLowerCase() === sel
115
+ }
116
+
117
+ const getMatcherFn = (selectorStr) => {
118
+ if (!selectorStr) return null
119
+ const firstChar = selectorStr.charAt(0)
120
+ return matchers[firstChar] || matchers.tag
121
+ }
122
+
123
+ const parentMatcher = getMatcherFn(parent)
124
+ const selectorMatcher = getMatcherFn(selector)
125
+
126
+ for (let current = elem; current && current !== document; current = current.parentNode) {
127
+ // Check if we've reached the parent boundary
128
+ if (parent && parentMatcher?.(current, parent)) {
129
+ break
168
130
  }
169
- if (selector) {
170
- // If selector is a class
171
- if (selectorType === '.') {
172
- if (elem.classList.contains(selector.substr(1))) {
173
- parents.push(elem)
174
- }
175
- }
176
- // If selector is an ID
177
- if (selectorType === '#') {
178
- if (elem.id === selector.substr(1)) {
179
- parents.push(elem)
180
- }
181
- }
182
- // If selector is a data attribute
183
- if (selectorType === '[') {
184
- if (elem.hasAttribute(selector.substr(1, selector.length - 1))) {
185
- parents.push(elem)
186
- }
187
- }
188
- // If selector is a tag
189
- if (elem.tagName.toLowerCase() === selector) {
190
- parents.push(elem)
191
- }
192
- } else {
193
- parents.push(elem)
131
+
132
+ // Add to results if matches selector (or no selector specified)
133
+ if (!selector || selectorMatcher?.(current, selector)) {
134
+ parents.push(current)
194
135
  }
195
136
  }
196
- // Return parents if any exist
197
- return parents.length ? parents : null
137
+
138
+ return parents.length > 0 ? parents : null
198
139
  }