dom: support live select option construction
This commit is contained in:
1 parent
792a51ba81
commit
9c71700824
2 files changed
+90
-2
No files matched your search
Vendored
+52
-2
@@ -2135,6 +2135,10 @@ class Element extends Node {
|
||||
}
|
||||
set value(v) {
|
||||
const tag = this.localName;
|
||||
if (tag === 'option') {
|
||||
this.setAttribute('value', String(v));
|
||||
return;
|
||||
}
|
||||
if (tag === 'select') {
|
||||
// Set selected on matching option, clear on others. Puppeteer's
|
||||
// page.select(selector, value) round-trips through this setter.
|
||||
@@ -2236,7 +2240,36 @@ class Element extends Node {
|
||||
if (this._selected !== undefined) return this._selected;
|
||||
return this.hasAttribute("selected");
|
||||
}
|
||||
set selected(v) { this._selected = !!v; }
|
||||
set selected(v) {
|
||||
this._selected = !!v;
|
||||
// Keep the native DOM tree in sync so layout/paint observes live form
|
||||
// state after scripts construct or change an option.
|
||||
if (this.localName === 'option') {
|
||||
if (this._selected) this.setAttribute('selected', '');
|
||||
else this.removeAttribute('selected');
|
||||
}
|
||||
}
|
||||
get text() {
|
||||
if (['option', 'script', 'title', 'a'].includes(this.localName)) {
|
||||
return this.textContent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
set text(v) {
|
||||
if (['option', 'script', 'title', 'a'].includes(this.localName)) {
|
||||
this.textContent = String(v);
|
||||
return;
|
||||
}
|
||||
// Most elements have no platform `text` reflector. Preserve ordinary
|
||||
// expando semantics for them even though all HTML element interfaces
|
||||
// currently share this prototype.
|
||||
Object.defineProperty(this, 'text', {
|
||||
value: v,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
get disabled() { return this.hasAttribute("disabled"); }
|
||||
set disabled(v) { if (v) this.setAttribute("disabled", ""); else this.removeAttribute("disabled"); }
|
||||
get type() { return this.getAttribute("type") || (this.localName === "input" ? "text" : ""); }
|
||||
@@ -2375,12 +2408,29 @@ class Element extends Node {
|
||||
if (this.localName !== 'select') return [];
|
||||
return HTMLCollection._from(this.querySelectorAll('option'));
|
||||
}
|
||||
add(item, before = null) {
|
||||
if (this.localName !== 'select') {
|
||||
throw new TypeError("Illegal invocation");
|
||||
}
|
||||
if (!item || item.nodeType !== 1
|
||||
|| (item.localName !== 'option' && item.localName !== 'optgroup')) {
|
||||
throw new TypeError("Failed to execute 'add' on 'HTMLSelectElement': parameter 1 is not of type 'HTMLOptionElement' or 'HTMLOptGroupElement'.");
|
||||
}
|
||||
if (typeof before === 'number') {
|
||||
const reference = this.options[before] || null;
|
||||
this.insertBefore(item, reference);
|
||||
} else if (before == null) {
|
||||
this.appendChild(item);
|
||||
} else {
|
||||
this.insertBefore(item, before);
|
||||
}
|
||||
}
|
||||
get selectedIndex() {
|
||||
const opts = this.options;
|
||||
for (let i = 0; i < opts.length; i++) {
|
||||
if (opts[i].selected || opts[i].hasAttribute('selected')) return i;
|
||||
}
|
||||
return -1;
|
||||
return opts.length ? 0 : -1;
|
||||
}
|
||||
set selectedIndex(v) {
|
||||
const opts = this.options;
|
||||
|
||||
@@ -3094,6 +3094,44 @@ mod tests {
|
||||
assert_eq!(value["attribute"], "color: red;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_add_and_option_text_update_the_live_dom() {
|
||||
let mut rt = setup_runtime("<html><body><select id='language'></select></body></html>");
|
||||
let result = rt
|
||||
.evaluate(
|
||||
r#"(() => {
|
||||
const select = document.getElementById('language');
|
||||
const english = document.createElement('option');
|
||||
english.value = 'en';
|
||||
english.text = 'English';
|
||||
english.selected = true;
|
||||
select.add(english);
|
||||
const greek = document.createElement('option');
|
||||
greek.value = 'el';
|
||||
greek.text = 'Greek';
|
||||
select.add(greek, 0);
|
||||
return JSON.stringify({
|
||||
labels: [...select.options].map(option => option.textContent),
|
||||
selectedIndex: select.selectedIndex,
|
||||
value: select.value,
|
||||
html: select.outerHTML
|
||||
});
|
||||
})()"#,
|
||||
)
|
||||
.unwrap();
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(result.as_str().unwrap()).unwrap();
|
||||
assert_eq!(value["labels"], serde_json::json!(["Greek", "English"]));
|
||||
assert_eq!(value["selectedIndex"], 1);
|
||||
assert_eq!(value["value"], "en");
|
||||
assert!(
|
||||
value["html"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains(r#"<option value="en" selected="">English</option>"#)
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression for #105: `element.querySelector` and `querySelectorAll`
|
||||
/// must scope to the receiver's subtree, not the whole document.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user