use std::cell::RefCell;
use std::rc::Rc;
use rdom_core::{Mutation, MutationObserver, NodeId};
use super::model::{display_size, enclosing_select, is_multi, option_disabled, options};
use crate::{TuiDom, TuiExt};
pub(crate) fn run(dom: &mut TuiDom, select: NodeId) {
if is_multi(dom, select) {
return;
}
let all = options(dom, select);
let selected: Vec<NodeId> = all
.iter()
.copied()
.filter(|&o| dom.node(o).has_attribute("selected"))
.collect();
match selected.split_last() {
None => {
if display_size(dom, select) != 1 {
return;
}
if let Some(first) = all.into_iter().find(|&o| !option_disabled(dom, o)) {
note_default(dom, first);
let _ = dom.set_attribute(first, "selected", "");
}
}
Some((_, earlier)) => {
for &opt in earlier {
note_default(dom, opt);
let _ = dom.remove_attribute(opt, "selected");
}
}
}
}
pub fn seed_all(dom: &mut TuiDom) {
let mut selects = Vec::new();
collect_selects(dom, dom.root(), &mut selects);
for select in selects {
run(dom, select);
}
}
fn collect_selects(dom: &TuiDom, id: NodeId, out: &mut Vec<NodeId>) {
for child in dom.node(id).child_nodes() {
if child.tag_name() == Some("select") {
out.push(child.id());
}
collect_selects(dom, child.id(), out);
}
}
fn note_default(dom: &mut TuiDom, opt: NodeId) {
let selected = dom.node(opt).has_attribute("selected");
if let Some(ext) = dom.node_mut(opt).ext_mut()
&& ext.default_selected.is_none()
{
ext.default_selected = Some(selected);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Queued {
OptionsChanged(NodeId),
Picked { select: NodeId, option: NodeId },
}
#[derive(Default)]
struct Shared {
queue: RefCell<Vec<Queued>>,
flushing: std::cell::Cell<bool>,
}
type Pending = Rc<Shared>;
pub(crate) struct Selectedness {
pending: Pending,
}
impl Selectedness {
pub(crate) fn install(dom: &mut TuiDom) -> Self {
let pending = Pending::default();
dom.add_mutation_observer(Box::new(OptionListObserver {
pending: pending.clone(),
}));
Self { pending }
}
pub(crate) fn flush(&self, dom: &mut TuiDom) {
let mut queued = std::mem::take(&mut *self.pending.queue.borrow_mut());
if queued.is_empty() {
return;
}
let _muted = Muted::new(&self.pending.flushing);
queued.dedup();
let mut last_pick = std::collections::HashMap::new();
for (i, item) in queued.iter().enumerate() {
if let Queued::Picked { select, .. } = item {
last_pick.insert(*select, i);
}
}
let is_select = |dom: &TuiDom, id: NodeId| {
dom.contains(id) && dom.node(id).tag_name() == Some("select")
};
for (i, item) in queued.into_iter().enumerate() {
match item {
Queued::OptionsChanged(select) if is_select(dom, select) => run(dom, select),
Queued::Picked { select, option }
if last_pick.get(&select) == Some(&i)
&& is_select(dom, select)
&& dom.contains(option) =>
{
make_exclusive(dom, select, option);
}
_ => {}
}
}
}
}
struct Muted<'a>(&'a std::cell::Cell<bool>);
impl<'a> Muted<'a> {
fn new(flag: &'a std::cell::Cell<bool>) -> Self {
flag.set(true);
Self(flag)
}
}
impl Drop for Muted<'_> {
fn drop(&mut self) {
self.0.set(false);
}
}
fn make_exclusive(dom: &mut TuiDom, select: NodeId, option: NodeId) {
if is_multi(dom, select) || !dom.node(option).has_attribute("selected") {
return;
}
let all = options(dom, select);
if !all.contains(&option) {
return;
}
for other in all {
if other != option && dom.node(other).has_attribute("selected") {
note_default(dom, other);
let _ = dom.remove_attribute(other, "selected");
}
}
}
struct OptionListObserver {
pending: Pending,
}
impl MutationObserver<TuiExt> for OptionListObserver {
fn observe(&mut self, dom: &mut TuiDom, record: &Mutation) {
if self.pending.flushing.get() {
return;
}
match record {
Mutation::ChildListChanged {
parent,
added,
removed,
} => {
let touches_options = added
.iter()
.chain(removed)
.any(|&n| matches!(dom.node(n).tag_name(), Some("option" | "optgroup")));
if touches_options && let Some(select) = enclosing_select(dom, *parent) {
self.pending
.queue
.borrow_mut()
.push(Queued::OptionsChanged(select));
}
}
Mutation::AttributeChanged {
id,
name,
old: None,
new: Some(_),
} if name == "selected" && dom.node(*id).tag_name() == Some("option") => {
if let Some(select) = enclosing_select(dom, *id) {
self.pending.queue.borrow_mut().push(Queued::Picked {
select,
option: *id,
});
}
}
_ => {}
}
}
}