Skip to content

Commit c55680a

Browse files
DavertMikclaude
andcommitted
fix: bound rich editor hidden-ancestor walk to avoid cross-form binds
The hidden-element fallback in detectAndMark used to climb to document.body looking for a nearby editor, so calling fillField on an unrelated hidden input could accidentally bind it to any editor on the same page. Cap the ancestor walk at 3 levels (covers CKE4 / Summernote / TinyMCE-legacy wrapper depth of 1-2 with margin) and skip the walk entirely for <input type=\"hidden\"> since those are form-state inputs, not editor-backing elements. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 12cbb5d commit c55680a

1 file changed

Lines changed: 17 additions & 16 deletions

File tree

lib/helper/extras/richTextEditor.js

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ function detectAndMark(el, opts) {
1313
const marker = opts.marker
1414
const kinds = opts.kinds
1515
const CE = '[contenteditable="true"], [contenteditable=""]'
16+
const MAX_HIDDEN_ASCENT = 3
1617

1718
function mark(kind, target) {
1819
document.querySelectorAll('[' + marker + ']').forEach(n => n.removeAttribute(marker))
@@ -26,7 +27,8 @@ function detectAndMark(el, opts) {
2627
if (tag === 'IFRAME') return mark(kinds.IFRAME, el)
2728
if (el.isContentEditable) return mark(kinds.CONTENTEDITABLE, el)
2829

29-
if (tag !== 'INPUT' && tag !== 'TEXTAREA') {
30+
const canSearchDescendants = tag !== 'INPUT' && tag !== 'TEXTAREA'
31+
if (canSearchDescendants) {
3032
const iframe = el.querySelector('iframe')
3133
if (iframe) return mark(kinds.IFRAME, iframe)
3234
const ce = el.querySelector(CE)
@@ -36,25 +38,24 @@ function detectAndMark(el, opts) {
3638
}
3739

3840
const style = window.getComputedStyle(el)
39-
const hidden =
41+
const isHidden =
4042
el.offsetParent === null ||
4143
(el.offsetWidth === 0 && el.offsetHeight === 0) ||
4244
style.display === 'none' ||
4345
style.visibility === 'hidden'
44-
45-
if (hidden) {
46-
let scope = el.parentElement
47-
while (scope) {
48-
const iframeNear = scope.querySelector('iframe')
49-
if (iframeNear) return mark(kinds.IFRAME, iframeNear)
50-
const ceNear = scope.querySelector(CE)
51-
if (ceNear) return mark(kinds.CONTENTEDITABLE, ceNear)
52-
for (const t of scope.querySelectorAll('textarea')) {
53-
if (t !== el) return mark(kinds.HIDDEN_TEXTAREA, t)
54-
}
55-
if (scope === document.body) break
56-
scope = scope.parentElement
57-
}
46+
if (!isHidden) return mark(kinds.STANDARD, el)
47+
48+
const isFormHidden = tag === 'INPUT' && el.type === 'hidden'
49+
if (isFormHidden) return mark(kinds.STANDARD, el)
50+
51+
let scope = el.parentElement
52+
for (let depth = 0; scope && depth < MAX_HIDDEN_ASCENT; depth++, scope = scope.parentElement) {
53+
const iframeNear = scope.querySelector('iframe')
54+
if (iframeNear) return mark(kinds.IFRAME, iframeNear)
55+
const ceNear = scope.querySelector(CE)
56+
if (ceNear) return mark(kinds.CONTENTEDITABLE, ceNear)
57+
const textareaNear = [...scope.querySelectorAll('textarea')].find(t => t !== el)
58+
if (textareaNear) return mark(kinds.HIDDEN_TEXTAREA, textareaNear)
5859
}
5960

6061
return mark(kinds.STANDARD, el)

0 commit comments

Comments
 (0)