@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 +6 -0
- package/common/browser.js +104 -37
- package/common/logger.js +151 -0
- package/common/util.js +96 -155
- package/core/blur-event.js +106 -42
- package/core/clear.js +13 -3
- package/core/coords.js +214 -90
- package/core/copy-elem.js +27 -13
- package/core/dataStorage.js +84 -21
- package/core/draw.js +80 -40
- package/core/elem-get-set.js +161 -77
- package/core/event.js +143 -28
- package/core/history.js +51 -31
- package/core/historyrecording.js +4 -2
- package/core/json.js +54 -12
- package/core/layer.js +11 -17
- package/core/math.js +102 -23
- package/core/namespaces.js +5 -5
- package/core/paint.js +100 -23
- package/core/paste-elem.js +58 -19
- package/core/path-actions.js +812 -791
- package/core/path-method.js +236 -37
- package/core/path.js +45 -10
- package/core/recalculate.js +438 -24
- package/core/sanitize.js +71 -34
- package/core/select.js +44 -20
- package/core/selected-elem.js +146 -31
- package/core/selection.js +16 -6
- package/core/svg-exec.js +103 -29
- package/core/svgroot.js +1 -1
- package/core/text-actions.js +327 -306
- package/core/undo.js +20 -5
- package/core/units.js +8 -6
- package/core/utilities.js +316 -203
- package/dist/svgcanvas.js +31616 -53281
- package/dist/svgcanvas.js.map +1 -1
- package/package.json +55 -54
- package/publish.md +1 -6
- package/svgcanvas.d.ts +225 -0
- package/svgcanvas.js +9 -9
- package/vite.config.mjs +20 -0
- package/rollup.config.mjs +0 -38
package/CHANGES.md
CHANGED
package/common/browser.js
CHANGED
|
@@ -8,60 +8,127 @@
|
|
|
8
8
|
|
|
9
9
|
const NSSVG = 'http://www.w3.org/2000/svg'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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 = () =>
|
|
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 = () =>
|
|
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 = () =>
|
|
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 = () =>
|
|
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 = () =>
|
|
130
|
+
*/
|
|
131
|
+
export const supportsGoodTextCharPos = () => browser.supportsGoodTextCharPos
|
|
132
|
+
|
|
133
|
+
// Export browser instance for direct access
|
|
134
|
+
export default browser
|
package/common/logger.js
ADDED
|
@@ -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
|
|
6
|
-
let
|
|
7
|
-
let
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} while (
|
|
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
|
-
|
|
20
|
-
return (item && typeof item === 'object' && !Array.isArray(item))
|
|
18
|
+
return { left, top }
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
export
|
|
24
|
-
|
|
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)
|
|
28
|
+
for (const key of Object.keys(source)) {
|
|
27
29
|
if (isObject(source[key])) {
|
|
28
|
-
|
|
30
|
+
output[key] = key in target
|
|
31
|
+
? mergeDeep(target[key], source[key])
|
|
32
|
+
: source[key]
|
|
29
33
|
} else {
|
|
30
|
-
|
|
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 {
|
|
47
|
+
* @return {Element|null} Returns null if no match found
|
|
42
48
|
*/
|
|
43
|
-
export
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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}
|
|
84
|
+
* @return {Array|null} Array of matching elements or null if no match
|
|
101
85
|
*/
|
|
102
|
-
export
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
135
|
-
return parents.length ? parents : null
|
|
103
|
+
|
|
104
|
+
return parents.length > 0 ? parents : null
|
|
136
105
|
}
|
|
137
106
|
|
|
138
|
-
export
|
|
107
|
+
export const getParentsUntil = (elem, parent, selector) => {
|
|
139
108
|
const parents = []
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
197
|
-
return parents.length ? parents : null
|
|
137
|
+
|
|
138
|
+
return parents.length > 0 ? parents : null
|
|
198
139
|
}
|