mirror of
https://github.com/lexmount/moli.git
synced 2026-10-04 08:00:45 +00:00
fix(forms): align selectedcontent mutation timing
This commit is contained in:
@@ -1819,6 +1819,24 @@ impl DocumentRuntime {
|
||||
options: RuntimeMutationOptions,
|
||||
prepublished_removals: Vec<devtools_mutations::DevToolsDomPrepublishedRemoval>,
|
||||
) -> bool {
|
||||
let result = self.apply_runtime_mutation_effects_before_runtime_followups(
|
||||
scope,
|
||||
host_ptr,
|
||||
effects,
|
||||
options,
|
||||
prepublished_removals,
|
||||
);
|
||||
finish_runtime_mutation_effects(self, scope, host_ptr, result)
|
||||
}
|
||||
|
||||
pub(super) fn apply_runtime_mutation_effects_before_runtime_followups(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
effects: DomMutationEffects,
|
||||
options: RuntimeMutationOptions,
|
||||
prepublished_removals: Vec<devtools_mutations::DevToolsDomPrepublishedRemoval>,
|
||||
) -> RuntimeMutationApplyResult {
|
||||
let mut result = apply_runtime_mutation_effects_to_dom_host(
|
||||
&mut self.mutations,
|
||||
&self.document,
|
||||
@@ -1834,7 +1852,7 @@ impl DocumentRuntime {
|
||||
&mut result.devtools_dom_mutations,
|
||||
prepublished_removals,
|
||||
);
|
||||
finish_runtime_mutation_effects(self, scope, host_ptr, result)
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1851,6 +1869,12 @@ pub(super) struct RuntimeMutationApplyResult {
|
||||
font_face_use_roots: Vec<DomHandle>,
|
||||
}
|
||||
|
||||
impl RuntimeMutationApplyResult {
|
||||
pub(super) fn did_change(&self) -> bool {
|
||||
self.changed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct InlineStyleAttributeCspMutation {
|
||||
target: DomHandle,
|
||||
@@ -1913,22 +1937,7 @@ pub(super) fn finish_runtime_mutation_effects(
|
||||
}
|
||||
|
||||
for candidate in runtime_script_start_candidates {
|
||||
let (node, host_script_handle) = candidate.into_parts();
|
||||
let Some(plan) = runtime.host_plan_script_start(node, &host_script_handle) else {
|
||||
continue;
|
||||
};
|
||||
match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(runtime, plan) {
|
||||
Ok(Some(committed)) => {
|
||||
execute_committed_inline_classic_script(runtime, scope, host_ptr, committed);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(message) => {
|
||||
if let Some(message) = v8::String::new(scope, &message) {
|
||||
let exception = v8::Exception::error(scope, message);
|
||||
scope.throw_exception(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
finish_runtime_script_start_candidate(runtime, scope, host_ptr, candidate);
|
||||
}
|
||||
|
||||
for popover in removed_open_popovers {
|
||||
@@ -2011,6 +2020,30 @@ pub(super) fn finish_runtime_mutation_effects(
|
||||
changed
|
||||
}
|
||||
|
||||
pub(super) fn finish_runtime_script_start_candidate(
|
||||
runtime: &mut DocumentRuntime,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
candidate: crate::mutation_coordinator::RuntimeScriptStartCandidate,
|
||||
) {
|
||||
let (node, host_script_handle) = candidate.into_parts();
|
||||
let Some(plan) = runtime.host_plan_script_start(node, &host_script_handle) else {
|
||||
return;
|
||||
};
|
||||
match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(runtime, plan) {
|
||||
Ok(Some(committed)) => {
|
||||
execute_committed_inline_classic_script(runtime, scope, host_ptr, committed);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(message) => {
|
||||
if let Some(message) = v8::String::new(scope, &message) {
|
||||
let exception = v8::Exception::error(scope, message);
|
||||
scope.throw_exception(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_committed_inline_classic_script(
|
||||
runtime: &mut DocumentRuntime,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
|
||||
@@ -1,11 +1,234 @@
|
||||
use super::{
|
||||
finish_runtime_mutation_effects, finish_runtime_script_start_candidate,
|
||||
tree::{TreeInsertionPlan, TreeInsertionPostConnectionStep, TreeReactionDispatchPolicy},
|
||||
};
|
||||
use crate::{
|
||||
custom_elements,
|
||||
document_runtime::{DocumentRuntime, DomHandle},
|
||||
dom::native::Node,
|
||||
dom::native::{DomMutationEffects, Node},
|
||||
mutation_coordinator::{
|
||||
ConnectedScriptMutationPolicy, RuntimeMutationOptions, ScriptStartRequest,
|
||||
},
|
||||
native_bridge::JsContextHost,
|
||||
util::context_host_ptr_from_global_bridge,
|
||||
};
|
||||
|
||||
fn selectedcontent_update_microtask_callback(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
args: v8::FunctionCallbackArguments<'_>,
|
||||
_rv: v8::ReturnValue<'_, v8::Value>,
|
||||
) {
|
||||
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
|
||||
return;
|
||||
};
|
||||
let Ok(value) = v8::Local::<v8::BigInt>::try_from(args.data()) else {
|
||||
return;
|
||||
};
|
||||
let (index, lossless) = value.u64_value();
|
||||
if !lossless {
|
||||
return;
|
||||
}
|
||||
let select = DomHandle::new(index as usize);
|
||||
if !unsafe { &mut *host_ptr }.take_pending_selectedcontent_update(select) {
|
||||
return;
|
||||
}
|
||||
let runtime: &mut DocumentRuntime = unsafe { &mut *host_ptr };
|
||||
let _ = runtime.sync_selectedcontents_for_select_in_reaction_scope(scope, host_ptr, select);
|
||||
}
|
||||
|
||||
impl DocumentRuntime {
|
||||
pub(super) fn apply_tree_insertion_mutation_effects_with_post_connection_steps(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
insertion_plan: &TreeInsertionPlan<'_>,
|
||||
effects: DomMutationEffects,
|
||||
options: RuntimeMutationOptions,
|
||||
reaction_policy: TreeReactionDispatchPolicy,
|
||||
run_post_connection_steps: bool,
|
||||
prepublished_removals: Vec<super::devtools_mutations::DevToolsDomPrepublishedRemoval>,
|
||||
) -> bool {
|
||||
if !run_post_connection_steps
|
||||
|| !insertion_plan.requires_interleaved_post_connection_steps()
|
||||
{
|
||||
let changed = self.apply_runtime_mutation_effects_with_prepublished_removals(
|
||||
scope,
|
||||
host_ptr,
|
||||
effects,
|
||||
options,
|
||||
prepublished_removals,
|
||||
);
|
||||
if changed && run_post_connection_steps {
|
||||
self.queue_selectedcontent_updates_after_selected_option_owner_change(
|
||||
scope,
|
||||
host_ptr,
|
||||
&insertion_plan.selected_option_owners_before_insert,
|
||||
);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
let run_script_steps = matches!(
|
||||
options.connected_script_policy,
|
||||
ConnectedScriptMutationPolicy::PrepareAndStart
|
||||
);
|
||||
let script_start_requests = if run_script_steps {
|
||||
self.mutations
|
||||
.plan_connected_script_start_requests(&mut self.dom_host, &effects)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let result = self.apply_runtime_mutation_effects_before_runtime_followups(
|
||||
scope,
|
||||
host_ptr,
|
||||
effects,
|
||||
options.with_connected_script_policy(ConnectedScriptMutationPolicy::DeferToOwner),
|
||||
prepublished_removals,
|
||||
);
|
||||
if result.did_change() {
|
||||
self.queue_selectedcontent_updates_after_selected_option_owner_change(
|
||||
scope,
|
||||
host_ptr,
|
||||
&insertion_plan.selected_option_owners_before_insert,
|
||||
);
|
||||
self.run_tree_insertion_post_connection_steps(
|
||||
scope,
|
||||
host_ptr,
|
||||
insertion_plan,
|
||||
script_start_requests,
|
||||
reaction_policy,
|
||||
);
|
||||
}
|
||||
finish_runtime_mutation_effects(self, scope, host_ptr, result)
|
||||
}
|
||||
|
||||
pub(super) fn run_parser_tree_insertion_post_connection_steps(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
insertion_plan: &TreeInsertionPlan<'_>,
|
||||
) {
|
||||
if insertion_plan.requires_interleaved_post_connection_steps() {
|
||||
self.run_tree_insertion_post_connection_steps(
|
||||
scope,
|
||||
host_ptr,
|
||||
insertion_plan,
|
||||
Vec::new(),
|
||||
TreeReactionDispatchPolicy::AppendToCurrentQueue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_tree_insertion_post_connection_steps(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
insertion_plan: &TreeInsertionPlan<'_>,
|
||||
script_start_requests: Vec<ScriptStartRequest>,
|
||||
reaction_policy: TreeReactionDispatchPolicy,
|
||||
) {
|
||||
match reaction_policy {
|
||||
TreeReactionDispatchPolicy::DispatchNow => {
|
||||
custom_elements::with_custom_element_reaction_scope(scope, host_ptr, |scope| {
|
||||
self.run_tree_insertion_post_connection_steps_appending_to_current_reaction_queue(
|
||||
scope,
|
||||
host_ptr,
|
||||
insertion_plan,
|
||||
script_start_requests,
|
||||
);
|
||||
});
|
||||
}
|
||||
TreeReactionDispatchPolicy::AppendToCurrentQueue => {
|
||||
self.run_tree_insertion_post_connection_steps_appending_to_current_reaction_queue(
|
||||
scope,
|
||||
host_ptr,
|
||||
insertion_plan,
|
||||
script_start_requests,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_tree_insertion_post_connection_steps_appending_to_current_reaction_queue(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
insertion_plan: &TreeInsertionPlan<'_>,
|
||||
script_start_requests: Vec<ScriptStartRequest>,
|
||||
) {
|
||||
let script_step_handles = insertion_plan
|
||||
.post_connection_steps
|
||||
.iter()
|
||||
.filter_map(|step| match step {
|
||||
TreeInsertionPostConnectionStep::Script(handle) => Some(*handle),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let (extra_script_requests, mut subtree_script_requests): (Vec<_>, Vec<_>) =
|
||||
script_start_requests
|
||||
.into_iter()
|
||||
.partition(|request| !script_step_handles.contains(&request.handle()));
|
||||
|
||||
// A connected script parent can be prepared by a child insertion even
|
||||
// though it is outside the inserted subtree. It precedes every
|
||||
// descendant post-connection step in tree order.
|
||||
for request in extra_script_requests {
|
||||
self.run_script_start_request(scope, host_ptr, request);
|
||||
}
|
||||
|
||||
for step in insertion_plan.post_connection_steps.iter().copied() {
|
||||
let handle = step.handle();
|
||||
if !self.dom_host.is_connected(handle) {
|
||||
continue;
|
||||
}
|
||||
match step {
|
||||
TreeInsertionPostConnectionStep::Script(script) => {
|
||||
let Some(index) = subtree_script_requests
|
||||
.iter()
|
||||
.position(|request| request.handle() == script)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let request = subtree_script_requests.remove(index);
|
||||
self.run_script_start_request(scope, host_ptr, request);
|
||||
}
|
||||
TreeInsertionPostConnectionStep::SelectedContent(selectedcontent) => {
|
||||
let _ = self.sync_selectedcontent_after_post_connection(
|
||||
scope,
|
||||
host_ptr,
|
||||
selectedcontent,
|
||||
);
|
||||
}
|
||||
TreeInsertionPostConnectionStep::SelectedOption(option) => {
|
||||
let _ = self.sync_selectedcontents_for_selected_option(scope, host_ptr, option);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_script_start_request(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
request: ScriptStartRequest,
|
||||
) {
|
||||
if request.clears_force_async() {
|
||||
let _ = self
|
||||
.dom_host
|
||||
.set_script_force_async(request.handle(), false);
|
||||
}
|
||||
let candidate = self.mutations.collect_connected_script_start_candidate(
|
||||
scope,
|
||||
host_ptr,
|
||||
&mut self.dom_host,
|
||||
request.handle(),
|
||||
&self.document,
|
||||
);
|
||||
if let Some(candidate) = candidate {
|
||||
finish_runtime_script_start_candidate(self, scope, host_ptr, candidate);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sync_selectedcontents_for_select_in_reaction_scope(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
@@ -24,6 +247,15 @@ impl DocumentRuntime {
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
option: DomHandle,
|
||||
) -> bool {
|
||||
self.sync_selectedcontents_for_selected_option(scope, host_ptr, option)
|
||||
}
|
||||
|
||||
fn sync_selectedcontents_for_selected_option(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
option: DomHandle,
|
||||
) -> bool {
|
||||
let Some(select) = self.dom_host.option_nearest_ancestor_select(option) else {
|
||||
return false;
|
||||
@@ -42,6 +274,125 @@ impl DocumentRuntime {
|
||||
)
|
||||
}
|
||||
|
||||
fn sync_selectedcontent_after_post_connection(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
selectedcontent: DomHandle,
|
||||
) -> bool {
|
||||
let Some(select) = self
|
||||
.dom_host
|
||||
.selectedcontent_nearest_ancestor_select(selectedcontent)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if self
|
||||
.dom_host
|
||||
.node(select)
|
||||
.and_then(Node::as_element)
|
||||
.is_some_and(|element| element.has_attribute("multiple"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let selected_option = self
|
||||
.dom_host
|
||||
.select_selected_option_elements(select)
|
||||
.first()
|
||||
.copied();
|
||||
self.clone_selected_option_contents_into_selectedcontent(
|
||||
scope,
|
||||
host_ptr,
|
||||
selectedcontent,
|
||||
selected_option,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn queue_selectedcontent_update_microtask(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
select: DomHandle,
|
||||
) {
|
||||
if !unsafe { &mut *host_ptr }.mark_pending_selectedcontent_update(select) {
|
||||
return;
|
||||
}
|
||||
let data = v8::BigInt::new_from_u64(scope, select.index() as u64).into();
|
||||
let Some(callback) = v8::Function::builder(selectedcontent_update_microtask_callback)
|
||||
.data(data)
|
||||
.build(scope)
|
||||
else {
|
||||
let _ = unsafe { &mut *host_ptr }.take_pending_selectedcontent_update(select);
|
||||
return;
|
||||
};
|
||||
scope.enqueue_microtask(callback);
|
||||
}
|
||||
|
||||
pub(super) fn queue_selectedcontent_updates_after_tree_removal(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
selected_option_owners_before_remove: &[(DomHandle, DomHandle)],
|
||||
) {
|
||||
self.queue_selectedcontent_updates_after_selected_option_owner_change(
|
||||
scope,
|
||||
host_ptr,
|
||||
selected_option_owners_before_remove,
|
||||
);
|
||||
}
|
||||
|
||||
fn queue_selectedcontent_updates_after_selected_option_owner_change(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
selected_option_owners_before_change: &[(DomHandle, DomHandle)],
|
||||
) {
|
||||
for &(option, previous_select) in selected_option_owners_before_change {
|
||||
if self.dom_host.option_nearest_ancestor_select(option) != Some(previous_select) {
|
||||
self.queue_selectedcontent_update_microtask(scope, host_ptr, previous_select);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn selected_option_owners_in_subtrees(
|
||||
&self,
|
||||
roots: &[DomHandle],
|
||||
) -> Vec<(DomHandle, DomHandle)> {
|
||||
let options = roots
|
||||
.iter()
|
||||
.flat_map(|root| {
|
||||
self.dom_host
|
||||
.collect_matching_elements(*root, true, |handle| {
|
||||
self.dom_host.is_html_element_named(handle, "option")
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut selected_option_by_select = Vec::new();
|
||||
let mut owners = Vec::new();
|
||||
for option in options {
|
||||
let Some(select) = self.dom_host.option_nearest_ancestor_select(option) else {
|
||||
continue;
|
||||
};
|
||||
let selected_option = if let Some((_, selected_option)) = selected_option_by_select
|
||||
.iter()
|
||||
.find(|(cached_select, _)| *cached_select == select)
|
||||
{
|
||||
*selected_option
|
||||
} else {
|
||||
let selected_option = self
|
||||
.dom_host
|
||||
.select_selected_option_elements(select)
|
||||
.first()
|
||||
.copied();
|
||||
selected_option_by_select.push((select, selected_option));
|
||||
selected_option
|
||||
};
|
||||
if selected_option == Some(option) {
|
||||
owners.push((option, select));
|
||||
}
|
||||
}
|
||||
owners
|
||||
}
|
||||
|
||||
pub(crate) fn sync_selectedcontents_for_select_appending_to_current_reaction_queue(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
|
||||
@@ -158,11 +158,14 @@ impl DocumentRuntime {
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
.unwrap_or_default();
|
||||
let mutation_started = cpu_profile_enabled.then(std::time::Instant::now);
|
||||
let changed = self.apply_runtime_mutation_effects_with_prepublished_removals(
|
||||
let changed = self.apply_tree_insertion_mutation_effects_with_post_connection_steps(
|
||||
scope,
|
||||
host_ptr,
|
||||
&insertion_plan,
|
||||
effects,
|
||||
RuntimeMutationOptions::js_dom_api(),
|
||||
source_profile.reaction_policy,
|
||||
true,
|
||||
prepublished_removals,
|
||||
);
|
||||
let mutation_us = mutation_started
|
||||
@@ -410,11 +413,14 @@ impl DocumentRuntime {
|
||||
if effects.did_change() {
|
||||
self.apply_node_iterator_pre_remove_plans(host_ptr, &insertion_plan.node_iterator_plan);
|
||||
}
|
||||
let changed = self.apply_runtime_mutation_effects_with_prepublished_removals(
|
||||
let changed = self.apply_tree_insertion_mutation_effects_with_post_connection_steps(
|
||||
scope,
|
||||
host_ptr,
|
||||
&insertion_plan,
|
||||
effects,
|
||||
mutation_options,
|
||||
source_profile.reaction_policy,
|
||||
!dispatch_atomic_move_callbacks,
|
||||
prepublished_removals,
|
||||
);
|
||||
if changed {
|
||||
|
||||
@@ -9,7 +9,24 @@ use crate::{
|
||||
native_bridge::JsContextHost,
|
||||
};
|
||||
|
||||
pub(super) struct TreeInsertionPlan<'a> {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(in crate::document_runtime::mutation_commands) enum TreeInsertionPostConnectionStep {
|
||||
Script(DomHandle),
|
||||
SelectedContent(DomHandle),
|
||||
SelectedOption(DomHandle),
|
||||
}
|
||||
|
||||
impl TreeInsertionPostConnectionStep {
|
||||
pub(in crate::document_runtime::mutation_commands) fn handle(self) -> DomHandle {
|
||||
match self {
|
||||
Self::Script(handle) | Self::SelectedContent(handle) | Self::SelectedOption(handle) => {
|
||||
handle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::document_runtime::mutation_commands) struct TreeInsertionPlan<'a> {
|
||||
pub(super) parent: DomHandle,
|
||||
pub(super) insertion_roots: &'a [DomHandle],
|
||||
pub(super) inserting_fragment_children: bool,
|
||||
@@ -23,6 +40,10 @@ pub(super) struct TreeInsertionPlan<'a> {
|
||||
pub(super) image_relevant_mutation_plan: ImageRelevantMutationPlan,
|
||||
pub(super) media_relevant_mutation_plan: MediaRelevantMutationPlan,
|
||||
pub(super) option_selectedness_before_insert: Option<Vec<(DomHandle, bool)>>,
|
||||
pub(in crate::document_runtime::mutation_commands) selected_option_owners_before_insert:
|
||||
Vec<(DomHandle, DomHandle)>,
|
||||
pub(in crate::document_runtime::mutation_commands) post_connection_steps:
|
||||
Vec<TreeInsertionPostConnectionStep>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -48,6 +69,13 @@ impl TreeInsertionPlan<'_> {
|
||||
pub(super) fn was_lifecycle_connected_before_insert(&self) -> bool {
|
||||
!self.lifecycle_connected_roots_before_insert.is_empty()
|
||||
}
|
||||
pub(in crate::document_runtime::mutation_commands) fn requires_interleaved_post_connection_steps(
|
||||
&self,
|
||||
) -> bool {
|
||||
self.post_connection_steps
|
||||
.iter()
|
||||
.any(|step| !matches!(step, TreeInsertionPostConnectionStep::Script(_)))
|
||||
}
|
||||
}
|
||||
|
||||
impl TreeInsertionLiveRangeMode {
|
||||
@@ -130,6 +158,12 @@ impl DocumentRuntime {
|
||||
let option_selectedness_before_insert = (options.selectedness_policy.captures()
|
||||
&& subtree_plan.may_have_options)
|
||||
.then(|| self.option_selectedness_before_insert(insertion_roots));
|
||||
let selected_option_owners_before_insert = if subtree_plan.may_have_options {
|
||||
self.selected_option_owners_in_subtrees(insertion_roots)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let post_connection_steps = self.tree_insertion_post_connection_steps(insertion_roots);
|
||||
let adoption = self.tree_adoption_plan_before_insert(host_ptr, insertion_roots, parent);
|
||||
let lifecycle_connected_roots_before_insert: Vec<DomHandle> = insertion_roots
|
||||
.iter()
|
||||
@@ -154,6 +188,36 @@ impl DocumentRuntime {
|
||||
image_relevant_mutation_plan,
|
||||
media_relevant_mutation_plan,
|
||||
option_selectedness_before_insert,
|
||||
selected_option_owners_before_insert,
|
||||
post_connection_steps,
|
||||
}
|
||||
}
|
||||
|
||||
fn tree_insertion_post_connection_steps(
|
||||
&self,
|
||||
roots: &[DomHandle],
|
||||
) -> Vec<TreeInsertionPostConnectionStep> {
|
||||
let mut stack = roots.iter().rev().copied().collect::<Vec<_>>();
|
||||
let mut steps = Vec::new();
|
||||
while let Some(handle) = stack.pop() {
|
||||
let Some(node) = self.dom_host.node(handle) else {
|
||||
continue;
|
||||
};
|
||||
if node.is_script_element() {
|
||||
steps.push(TreeInsertionPostConnectionStep::Script(handle));
|
||||
} else if let Some(element) = node.as_element() {
|
||||
if element.is_html_element("selectedcontent") {
|
||||
steps.push(TreeInsertionPostConnectionStep::SelectedContent(handle));
|
||||
} else if element.is_html_option() {
|
||||
steps.push(TreeInsertionPostConnectionStep::SelectedOption(handle));
|
||||
}
|
||||
}
|
||||
|
||||
stack.extend(self.dom_host.child_handles_reversed(handle));
|
||||
if let Some(shadow_root) = self.dom_host.shadow_root_handle(handle) {
|
||||
stack.push(shadow_root);
|
||||
}
|
||||
}
|
||||
steps
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ mod resources;
|
||||
mod tree_order;
|
||||
|
||||
pub(in crate::document_runtime) use adoption::TreeAdoptionPlan;
|
||||
pub(super) use insertion_plan::{TreeInsertionPlan, TreeInsertionPostConnectionStep};
|
||||
pub(in crate::document_runtime) use parser_post_step::ParserPostStepRuntimeWork;
|
||||
#[cfg(test)]
|
||||
pub(crate) use parser_post_step::ParserPostStepRuntimeWorkForTest;
|
||||
pub(super) use policy::TreeReactionDispatchPolicy;
|
||||
|
||||
@@ -155,6 +155,7 @@ impl DocumentRuntime {
|
||||
if !changed {
|
||||
return;
|
||||
}
|
||||
self.run_parser_tree_insertion_post_connection_steps(scope, host_ptr, &insertion_plan);
|
||||
self.dispatch_tree_insertion_side_effects_after_change(
|
||||
scope,
|
||||
host_ptr,
|
||||
|
||||
@@ -5,7 +5,7 @@ pub(super) enum TreeMutationSideEffectSource {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) enum TreeReactionDispatchPolicy {
|
||||
pub(in crate::document_runtime::mutation_commands) enum TreeReactionDispatchPolicy {
|
||||
DispatchNow,
|
||||
AppendToCurrentQueue,
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ pub(super) struct TreeRemovalPlan {
|
||||
pub(super) live_range_previous_sibling: Option<DomHandle>,
|
||||
pub(super) node_iterator_plan: Option<NodeIteratorRemovalPlan>,
|
||||
pub(super) registry_retargets: Vec<custom_elements::RegistryAssociationRetarget>,
|
||||
pub(super) selected_option_owners_before_remove: Vec<(DomHandle, DomHandle)>,
|
||||
}
|
||||
|
||||
impl DocumentRuntime {
|
||||
@@ -58,6 +59,8 @@ impl DocumentRuntime {
|
||||
};
|
||||
let registry_retargets =
|
||||
custom_elements::registry_association_retargets_before_removal(host_ptr, root);
|
||||
let selected_option_owners_before_remove =
|
||||
self.selected_option_owners_in_subtrees(std::slice::from_ref(&root));
|
||||
TreeRemovalPlan {
|
||||
parent,
|
||||
root,
|
||||
@@ -69,6 +72,7 @@ impl DocumentRuntime {
|
||||
live_range_previous_sibling,
|
||||
node_iterator_plan,
|
||||
registry_retargets,
|
||||
selected_option_owners_before_remove,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ impl DocumentRuntime {
|
||||
removal_plan: &TreeRemovalPlan,
|
||||
profile: TreeMutationSourceProfile,
|
||||
) {
|
||||
self.queue_selectedcontent_updates_after_tree_removal(
|
||||
scope,
|
||||
host_ptr,
|
||||
&removal_plan.selected_option_owners_before_remove,
|
||||
);
|
||||
self.dispatch_tree_removal_pre_reaction_followups_after_change(
|
||||
scope,
|
||||
host_ptr,
|
||||
|
||||
@@ -39,9 +39,11 @@ impl DocumentRuntime {
|
||||
let _ = self
|
||||
.remove_child_appending_to_current_reaction_queue(scope, host_ptr, parent, *child);
|
||||
}
|
||||
let changed = self
|
||||
.append_child_appending_to_current_reaction_queue(scope, host_ptr, parent, fragment)
|
||||
|| removes_existing_children;
|
||||
let inserted_new_children = !added_children.is_empty()
|
||||
&& self.append_child_appending_to_current_reaction_queue(
|
||||
scope, host_ptr, parent, fragment,
|
||||
);
|
||||
let changed = inserted_new_children || removes_existing_children;
|
||||
if changed && records_enabled {
|
||||
crate::observer_runtime::coalesce_child_list_replacement_records(
|
||||
host_ptr,
|
||||
@@ -249,11 +251,14 @@ impl DocumentRuntime {
|
||||
next_sibling,
|
||||
);
|
||||
}
|
||||
let changed = self.apply_runtime_mutation_effects_with_prepublished_removals(
|
||||
let changed = self.apply_tree_insertion_mutation_effects_with_post_connection_steps(
|
||||
scope,
|
||||
host_ptr,
|
||||
&insertion_plan,
|
||||
effects,
|
||||
RuntimeMutationOptions::js_dom_api(),
|
||||
reaction_policy,
|
||||
true,
|
||||
prepublished_removals,
|
||||
);
|
||||
if !changed {
|
||||
@@ -279,6 +284,13 @@ impl DocumentRuntime {
|
||||
replacement_plan: &TreeReplacementPlan<'_>,
|
||||
reaction_policy: TreeReactionDispatchPolicy,
|
||||
) {
|
||||
self.queue_selectedcontent_updates_after_tree_removal(
|
||||
scope,
|
||||
host_ptr,
|
||||
&replacement_plan
|
||||
.removal
|
||||
.selected_option_owners_before_remove,
|
||||
);
|
||||
let insertion_plan = &replacement_plan.insertion;
|
||||
let mut dispatch_reactions = |scope: &mut v8::PinScope<'_, '_>| {
|
||||
self.dispatch_tree_insertion_immediate_side_effects_after_change(
|
||||
|
||||
@@ -113,11 +113,21 @@ impl RuntimeMutationOptions {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct ScriptStartRequest {
|
||||
pub(super) struct ScriptStartRequest {
|
||||
handle: NativeNodeId,
|
||||
clears_force_async: bool,
|
||||
}
|
||||
|
||||
impl ScriptStartRequest {
|
||||
pub(super) fn handle(&self) -> NativeNodeId {
|
||||
self.handle
|
||||
}
|
||||
|
||||
pub(super) fn clears_force_async(&self) -> bool {
|
||||
self.clears_force_async
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ScriptStartRequests {
|
||||
requests: Vec<ScriptStartRequest>,
|
||||
@@ -244,20 +254,11 @@ impl MutationCoordinator {
|
||||
}
|
||||
let mutation_record_count = effects.observer_records().records().len();
|
||||
let script_planning_started = cpu_profile_enabled.then(Instant::now);
|
||||
let mut script_start_requests = ScriptStartRequests::default();
|
||||
if prepare_connected_scripts {
|
||||
for &root in effects.scripts().connected_roots() {
|
||||
self.collect_connected_scripts_in_subtree(
|
||||
dom_host,
|
||||
root,
|
||||
&mut script_start_requests,
|
||||
);
|
||||
}
|
||||
for trigger in effects.scripts().prepare_triggers() {
|
||||
script_start_requests.queue(trigger.handle(), trigger.clears_script_force_async());
|
||||
}
|
||||
}
|
||||
let script_start_requests = script_start_requests.into_tree_order(dom_host);
|
||||
let script_start_requests = if prepare_connected_scripts {
|
||||
self.plan_connected_script_start_requests(dom_host, &effects)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let script_start_request_count = script_start_requests.len();
|
||||
let script_planning_us = script_planning_started
|
||||
.map(|started| started.elapsed().as_micros())
|
||||
@@ -376,7 +377,22 @@ impl MutationCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_connected_script_start_candidate(
|
||||
pub(super) fn plan_connected_script_start_requests(
|
||||
&self,
|
||||
dom_host: &mut DomHost,
|
||||
effects: &DomMutationEffects,
|
||||
) -> Vec<ScriptStartRequest> {
|
||||
let mut requests = ScriptStartRequests::default();
|
||||
for &root in effects.scripts().connected_roots() {
|
||||
self.collect_connected_scripts_in_subtree(dom_host, root, &mut requests);
|
||||
}
|
||||
for trigger in effects.scripts().prepare_triggers() {
|
||||
requests.queue(trigger.handle(), trigger.clears_script_force_async());
|
||||
}
|
||||
requests.into_tree_order(dom_host)
|
||||
}
|
||||
|
||||
pub(super) fn collect_connected_script_start_candidate(
|
||||
&mut self,
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
host_ptr: *mut JsContextHost,
|
||||
|
||||
@@ -302,6 +302,7 @@ impl JsContextHost {
|
||||
super::misc_platform_api_tasks::MiscPlatformApiTaskState::default(),
|
||||
file_entry_file_callbacks:
|
||||
super::file_entry_file_callbacks::FileEntryFileCallbackState::default(),
|
||||
pending_selectedcontent_updates: HashSet::new(),
|
||||
user_interaction_tasks:
|
||||
super::user_interaction_tasks::UserInteractionTaskState::default(),
|
||||
pending_image_load_events: HashMap::new(),
|
||||
@@ -1512,6 +1513,14 @@ impl JsContextHost {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mark_pending_selectedcontent_update(&mut self, select: DomHandle) -> bool {
|
||||
self.pending_selectedcontent_updates.insert(select)
|
||||
}
|
||||
|
||||
pub(crate) fn take_pending_selectedcontent_update(&mut self, select: DomHandle) -> bool {
|
||||
self.pending_selectedcontent_updates.remove(&select)
|
||||
}
|
||||
|
||||
pub(crate) fn pending_image_load_event(
|
||||
&self,
|
||||
handle: DomHandle,
|
||||
|
||||
@@ -909,6 +909,7 @@ pub(crate) struct JsContextHost {
|
||||
directory_reader_callbacks: directory_reader_callbacks::DirectoryReaderCallbackState,
|
||||
misc_platform_api_tasks: misc_platform_api_tasks::MiscPlatformApiTaskState,
|
||||
file_entry_file_callbacks: file_entry_file_callbacks::FileEntryFileCallbackState,
|
||||
pending_selectedcontent_updates: HashSet<DomHandle>,
|
||||
user_interaction_tasks: user_interaction_tasks::UserInteractionTaskState,
|
||||
pending_image_load_events: HashMap<DomHandle, PendingImageLoadEvent>,
|
||||
next_image_load_event_id: u64,
|
||||
|
||||
@@ -636,6 +636,7 @@ pub(in crate::native_bridge) fn select_selected_index_setter_function<'s>(
|
||||
let _ = runtime.set_selected_state(scope, runtime_ptr, option, should_select);
|
||||
}
|
||||
let _ = runtime.set_select_explicit_none(scope, runtime_ptr, handle, !matched);
|
||||
let _ = runtime.sync_selectedcontents_for_select_in_reaction_scope(scope, runtime_ptr, handle);
|
||||
rv.set_undefined();
|
||||
}
|
||||
|
||||
|
||||
@@ -400,6 +400,11 @@ pub(in crate::native_bridge) fn reset_form_default_action(
|
||||
did_change |= runtime
|
||||
.dom_host_mut()
|
||||
.set_select_explicit_none_state(handle, false);
|
||||
did_change |= runtime.sync_selectedcontents_for_select_in_reaction_scope(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
handle,
|
||||
);
|
||||
}
|
||||
FormResetPlan::Output { handle, value } => {
|
||||
did_change |= runtime.dom_host_mut().set_text_content(handle, &value);
|
||||
|
||||
@@ -12400,7 +12400,7 @@ JSON.stringify({
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_option_finish_and_select_value_sync_selectedcontent_clones() {
|
||||
fn parser_option_finish_and_select_setters_sync_selectedcontent_clones() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
@@ -12409,11 +12409,11 @@ JSON.stringify({
|
||||
runtime.block_on(tokio::task::LocalSet::new().run_until(async move {
|
||||
let mut page_vm = parse_phase_one_html_into_page_vm_for_test(
|
||||
r#"<!doctype html><html><body>
|
||||
<select id="select">
|
||||
<form id="form"><select id="select">
|
||||
<button><selectedcontent id="selectedcontent">default</selectedcontent></button>
|
||||
<div><option id="one"><span id="source-span">one</span></option></div>
|
||||
<div><option id="two"><strong>two</strong></option></div>
|
||||
</select>
|
||||
</select></form>
|
||||
<script>
|
||||
const select = document.getElementById('select');
|
||||
const selectedcontent = document.getElementById('selectedcontent');
|
||||
@@ -12427,6 +12427,11 @@ window.selectedcontentState.push(
|
||||
selectedcontent.textContent.trim(),
|
||||
selectedcontent.firstElementChild.tagName,
|
||||
);
|
||||
document.querySelector('#two > strong').textContent = 'updated';
|
||||
select.selectedIndex = select.selectedIndex;
|
||||
window.selectedcontentState.push(selectedcontent.textContent.trim());
|
||||
document.getElementById('form').reset();
|
||||
window.selectedcontentState.push(selectedcontent.textContent.trim());
|
||||
</script>
|
||||
</body></html>"#,
|
||||
)
|
||||
@@ -12437,8 +12442,49 @@ window.selectedcontentState.push(
|
||||
.expect("selectedcontent parser state should evaluate");
|
||||
assert_eq!(
|
||||
result.get("value").and_then(serde_json::Value::as_str),
|
||||
Some(r#"["one",true,"two","STRONG"]"#),
|
||||
"parser option completion and select.value must synchronously clone the selected option children"
|
||||
Some(r#"["one",true,"two","STRONG","updated","one"]"#),
|
||||
"parser option completion and select setters must synchronously clone the selected option children"
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_selected_option_updates_previous_select_selectedcontent_in_a_microtask() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("current-thread runtime should build");
|
||||
|
||||
runtime.block_on(tokio::task::LocalSet::new().run_until(async move {
|
||||
let mut page_vm = parse_phase_one_html_into_page_vm_for_test(
|
||||
r#"<!doctype html><html><body>
|
||||
<select id="source">
|
||||
<button><selectedcontent id="selectedcontent"></selectedcontent></button>
|
||||
<option id="moved">one</option>
|
||||
<option>two</option>
|
||||
</select>
|
||||
<div id="destination"></div>
|
||||
<script>
|
||||
const selectedcontent = document.getElementById('selectedcontent');
|
||||
document.getElementById('destination').appendChild(document.getElementById('moved'));
|
||||
window.selectedcontentMoveState = [selectedcontent.textContent.trim()];
|
||||
</script>
|
||||
</body></html>"#,
|
||||
)
|
||||
.await;
|
||||
|
||||
page_vm
|
||||
.evaluate_expression("0")
|
||||
.expect("selectedcontent removal microtask checkpoint should run");
|
||||
let result = page_vm
|
||||
.evaluate_expression(
|
||||
"JSON.stringify([...window.selectedcontentMoveState, document.getElementById('selectedcontent').textContent.trim()])",
|
||||
)
|
||||
.expect("selectedcontent move state should evaluate");
|
||||
assert_eq!(
|
||||
result.get("value").and_then(serde_json::Value::as_str),
|
||||
Some(r#"["one","two"]"#),
|
||||
"implicit option removal must keep the old clone synchronously and update it at the next microtask checkpoint"
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ fn fill_credit_card_control(
|
||||
};
|
||||
|
||||
if is_select {
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_select_value(handle, &value);
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_select_value(scope, runtime_ptr, handle, &value);
|
||||
} else {
|
||||
let _ = unsafe { &mut *runtime_ptr }.set_input_value(handle, &value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user