mirror of
https://github.com/lexmount/moli.git
synced 2026-10-06 00:00:56 +00:00
fix(forms): preserve image sources in live named lists
Carry the selected control or image query into RadioNodeList descriptors and reuse the same matching helpers when resolving live collections. Retained image lists keep their filter when matching controls are added, and retained control lists never switch to image fallback. Cover image membership, descriptor access, name changes, tree order, removal, and adoption while preserving form.elements control filtering.
This commit is contained in:
@@ -90,6 +90,7 @@ pub(crate) use forms::{
|
||||
form_control_elements, form_data_control_elements, is_valid_submit_button,
|
||||
submit_form_with_submit_event,
|
||||
};
|
||||
pub(in crate::native_bridge) use forms::{form_named_control_matches, form_named_image_matches};
|
||||
#[cfg(test)]
|
||||
pub(crate) use styles::iframe_width_attribute_viewport_width;
|
||||
pub(crate) use styles::{
|
||||
|
||||
@@ -126,9 +126,10 @@ pub(in crate::native_bridge) use self::form_element::{
|
||||
form_indexed_deleter, form_indexed_descriptor, form_indexed_enumerator, form_indexed_getter,
|
||||
form_indexed_query, form_indexed_setter, form_length_getter_function,
|
||||
form_method_getter_function, form_method_setter_function, form_name_getter_function,
|
||||
form_name_setter_function, form_named_definer, form_named_deleter, form_named_descriptor,
|
||||
form_named_getter, form_named_query, form_no_validate_getter_function,
|
||||
form_no_validate_setter_function, form_target_getter_function, form_target_setter_function,
|
||||
form_name_setter_function, form_named_control_matches, form_named_definer, form_named_deleter,
|
||||
form_named_descriptor, form_named_getter, form_named_image_matches, form_named_query,
|
||||
form_no_validate_getter_function, form_no_validate_setter_function,
|
||||
form_target_getter_function, form_target_setter_function,
|
||||
};
|
||||
pub(crate) use self::input::{cache_input_files_from_selected_files, input_files_for_object};
|
||||
pub(in crate::native_bridge) use self::input::{
|
||||
|
||||
@@ -569,11 +569,11 @@ pub(in crate::native_bridge) fn form_named_getter<'s>(
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let runtime = unsafe { &mut *runtime_ptr };
|
||||
let matches = form_named_item_matches(runtime, handle, &key);
|
||||
let (query_kind, matches) = form_named_item_matches(runtime, handle, &key);
|
||||
if matches.len() > 1 {
|
||||
let descriptor = LiveCollectionDescriptor {
|
||||
collection_kind: CollectionKind::RadioNodeList,
|
||||
query_kind: LiveCollectionQueryKind::FormControlsByName,
|
||||
query_kind,
|
||||
root: handle,
|
||||
query: Some(key),
|
||||
include_root: false,
|
||||
@@ -632,11 +632,11 @@ pub(in crate::native_bridge) fn form_named_descriptor<'s>(
|
||||
};
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
let runtime = unsafe { &mut *runtime_ptr };
|
||||
let matches = form_named_item_matches(runtime, handle, &key);
|
||||
let (query_kind, matches) = form_named_item_matches(runtime, handle, &key);
|
||||
let value = if matches.len() > 1 {
|
||||
let descriptor = LiveCollectionDescriptor {
|
||||
collection_kind: CollectionKind::RadioNodeList,
|
||||
query_kind: LiveCollectionQueryKind::FormControlsByName,
|
||||
query_kind,
|
||||
root: handle,
|
||||
query: Some(key),
|
||||
include_root: false,
|
||||
@@ -745,7 +745,9 @@ fn form_has_named_item_or_past_name(
|
||||
form_handle: DomHandle,
|
||||
key: &str,
|
||||
) -> bool {
|
||||
!form_named_item_matches(runtime, form_handle, key).is_empty()
|
||||
!form_named_item_matches(runtime, form_handle, key)
|
||||
.1
|
||||
.is_empty()
|
||||
|| runtime.form_past_named_item(form_handle, key).is_some()
|
||||
}
|
||||
|
||||
@@ -942,14 +944,30 @@ fn form_named_item_matches(
|
||||
runtime: &JsContextHost,
|
||||
form_handle: DomHandle,
|
||||
key: &str,
|
||||
) -> Vec<DomHandle> {
|
||||
) -> (LiveCollectionQueryKind, Vec<DomHandle>) {
|
||||
// Only a miss is conclusive. A hit still needs the existing form-owner,
|
||||
// custom-element, shadow-tree and image-fallback rules below. The caller
|
||||
// separately checks the past-names map even when this returns no matches.
|
||||
if !runtime.dom_host().has_element_with_named_item_key(key) {
|
||||
return Vec::new();
|
||||
return (LiveCollectionQueryKind::FormControlsByName, Vec::new());
|
||||
}
|
||||
let controls = form_control_elements(runtime, form_handle)
|
||||
let controls = form_named_control_matches(runtime, form_handle, key);
|
||||
if controls.is_empty() {
|
||||
(
|
||||
LiveCollectionQueryKind::FormImagesByName,
|
||||
form_named_image_matches(runtime, form_handle, key),
|
||||
)
|
||||
} else {
|
||||
(LiveCollectionQueryKind::FormControlsByName, controls)
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::native_bridge) fn form_named_control_matches(
|
||||
runtime: &JsContextHost,
|
||||
form_handle: DomHandle,
|
||||
key: &str,
|
||||
) -> Vec<DomHandle> {
|
||||
form_control_elements(runtime, form_handle)
|
||||
.into_iter()
|
||||
.filter(|handle| {
|
||||
runtime
|
||||
@@ -958,15 +976,10 @@ fn form_named_item_matches(
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| element.matches_named_item_key(key))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if controls.is_empty() {
|
||||
form_named_image_matches(runtime, form_handle, key)
|
||||
} else {
|
||||
controls
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn form_named_image_matches(
|
||||
pub(in crate::native_bridge) fn form_named_image_matches(
|
||||
runtime: &JsContextHost,
|
||||
form_handle: DomHandle,
|
||||
key: &str,
|
||||
|
||||
@@ -9,7 +9,10 @@ use std::{
|
||||
use indexmap::IndexSet;
|
||||
|
||||
use super::super::document_runtime::DomHandle;
|
||||
use super::element::{control_label_handles, form_control_elements};
|
||||
use super::element::{
|
||||
control_label_handles, form_control_elements, form_named_control_matches,
|
||||
form_named_image_matches,
|
||||
};
|
||||
use super::{JsContextHost, RuntimeObservableContextToken};
|
||||
use dense_reflector_map::DenseReflectorMap;
|
||||
|
||||
@@ -175,6 +178,7 @@ pub(super) enum LiveCollectionQueryKind {
|
||||
WindowNamedItems,
|
||||
DocumentAllNamedItems,
|
||||
FormControlsByName,
|
||||
FormImagesByName,
|
||||
Forms,
|
||||
Images,
|
||||
Scripts,
|
||||
@@ -204,6 +208,7 @@ impl LiveCollectionQueryKind {
|
||||
Self::WindowNamedItems => "windowNamedItems",
|
||||
Self::DocumentAllNamedItems => "documentAllNamedItems",
|
||||
Self::FormControlsByName => "formControlsByName",
|
||||
Self::FormImagesByName => "formImagesByName",
|
||||
Self::Forms => "forms",
|
||||
Self::Images => "images",
|
||||
Self::Scripts => "scripts",
|
||||
@@ -302,16 +307,11 @@ impl LiveCollectionDescriptor {
|
||||
} else if self.query_kind == LiveCollectionQueryKind::FormControls {
|
||||
form_control_elements(host, self.root)
|
||||
} else if self.query_kind == LiveCollectionQueryKind::FormControlsByName {
|
||||
let query = self.query.as_deref().unwrap_or_default();
|
||||
form_control_elements(host, self.root)
|
||||
.into_iter()
|
||||
.filter(|handle| {
|
||||
host.dom_host()
|
||||
.node(*handle)
|
||||
.and_then(crate::dom::native::Node::as_element)
|
||||
.is_some_and(|element| element.matches_named_item_key(query))
|
||||
})
|
||||
.collect()
|
||||
form_named_control_matches(host, self.root, self.query.as_deref().unwrap_or_default())
|
||||
} else if self.query_kind == LiveCollectionQueryKind::FormImagesByName {
|
||||
// A retained image list keeps this filter even if a later form[name]
|
||||
// lookup finds controls. Query kind also distinguishes wrapper caches.
|
||||
form_named_image_matches(host, self.root, self.query.as_deref().unwrap_or_default())
|
||||
} else if self.query_kind == LiveCollectionQueryKind::WindowNamedItems {
|
||||
crate::native_bridge::named_access::window_named_item_handles(
|
||||
host.dom_host(),
|
||||
|
||||
@@ -101,6 +101,73 @@ fn form_named_lookup_define_property_uses_supported_names_not_visibility() {
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_named_lookup_live_lists_preserve_their_control_or_image_source() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://form-lookup-image-lists.test/",
|
||||
"<!doctype html><html><body><form id='parsed'></form></body></html>",
|
||||
);
|
||||
let result = vm.eval(r#"
|
||||
(() => {
|
||||
const detachedDocument = document.implementation.createHTMLDocument('forms');
|
||||
const forms = [
|
||||
document.getElementById('parsed'),
|
||||
document.createElement('form'),
|
||||
detachedDocument.body.appendChild(detachedDocument.createElement('form'))
|
||||
];
|
||||
for (const [mode, form] of forms.entries()) {
|
||||
const doc = form.ownerDocument;
|
||||
const first = form.appendChild(doc.createElement('img'));
|
||||
first.name = 'photos';
|
||||
first.id = 'photos';
|
||||
const second = form.appendChild(doc.createElement('img'));
|
||||
second.id = 'photos';
|
||||
const nested = form.appendChild(doc.createElement('form'));
|
||||
nested.appendChild(doc.createElement('img')).name = 'photos';
|
||||
const imageInput = form.appendChild(doc.createElement('input'));
|
||||
imageInput.type = 'image';
|
||||
imageInput.name = 'photos';
|
||||
const images = form.photos;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(form, 'photos');
|
||||
const descriptorImages = descriptor.value;
|
||||
if (!(images instanceof RadioNodeList) || images.length !== 2 || images[0] !== first || images[1] !== second)
|
||||
throw Error(mode + ': image list membership');
|
||||
if (descriptor.writable || descriptor.enumerable || !descriptor.configurable || descriptorImages.length !== 2)
|
||||
throw Error(mode + ': image descriptor');
|
||||
if (form.elements.namedItem('photos') !== null) throw Error(mode + ': elements must exclude images');
|
||||
const control = form.appendChild(doc.createElement('input'));
|
||||
control.name = 'photos';
|
||||
if (form.photos !== control || images.length !== 2 || images[0] !== first)
|
||||
throw Error(mode + ': existing image list changed source');
|
||||
const otherControl = form.appendChild(doc.createElement('input'));
|
||||
otherControl.name = 'photos';
|
||||
const controls = form.photos;
|
||||
if (controls.length !== 2 || controls[0] !== control || controls[1] !== otherControl)
|
||||
throw Error(mode + ': control list membership');
|
||||
control.remove();
|
||||
otherControl.remove();
|
||||
if (controls.length !== 0 || form.photos.length !== 2) throw Error(mode + ': control list fell back to images');
|
||||
form.insertBefore(second, first);
|
||||
if (images[0] !== second || descriptorImages[0] !== second) throw Error(mode + ': image tree order');
|
||||
second.id = 'renamed';
|
||||
if (images.length !== 1 || images[0] !== first || descriptorImages.length !== 1 || form.photos !== first)
|
||||
throw Error(mode + ': live image rename');
|
||||
first.remove();
|
||||
if (images.length !== 0 || descriptorImages.length !== 0) throw Error(mode + ': live image removal');
|
||||
second.name = 'photos';
|
||||
if (images.length !== 1 || images[0] !== second) throw Error(mode + ': live image insertion');
|
||||
if (mode === 2) {
|
||||
document.body.appendChild(document.adoptNode(form));
|
||||
if (images.length !== 1 || images[0] !== second || form.photos !== second)
|
||||
throw Error('adoption changed image list identity');
|
||||
}
|
||||
}
|
||||
return 'ok';
|
||||
})()
|
||||
"#).expect("live RadioNodeLists must retain the source selected by the initial named lookup");
|
||||
assert_eq!(result, "ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_named_lookup_misses_do_not_enumerate_or_traverse_controls() {
|
||||
let mut vm = new_parsed_test_vm(
|
||||
|
||||
Reference in New Issue
Block a user