1use crate::base::CellRef;
9use crate::model::{Anchor, AnchorMode, AutoFilter, FilterCriterion, Slicer, Workbook};
10use serde_json::{Value as Json, json};
11
12use super::{
13 CommandSpec, EngineError, Result, Session, bad, bool_param, cell_param_on, cmd, edit, f64_param, has_doc, str_param, target_sheet, u32_param,
14};
15
16pub fn specs() -> Vec<CommandSpec> {
17 vec![
18 cmd!(
19 "insert.slicer",
20 "Slicer",
21 ["Insert", "Filters"],
22 None,
23 "{table?, column?: \"Region\", columns?: 1, at?: \"H2\", width?, height?}",
24 has_doc,
25 insert_slicer
26 ),
27 cmd!(
28 "table.insertSlicer",
29 "Insert Slicer",
30 ["Table", "Tools"],
31 None,
32 "{table?, column?: \"Region\", columns?: 1, at?, width?, height?}",
33 has_doc,
34 insert_slicer
35 ),
36 cmd!("slicer.toggle", "Toggle Slicer Value", [], None, "{slicer?: id, value: \"East\", multi?: false}", has_doc, slicer_toggle),
37 cmd!("slicer.selectAll", "Clear Slicer Filter", [], None, "{slicer?: id}", has_doc, slicer_select_all),
38 cmd!("slicer.clear", "Clear Slicer Filter", [], None, "{slicer?: id}", has_doc, slicer_select_all),
39 cmd!("slicer.delete", "Delete Slicer", [], None, "{slicer?: id}", has_doc, slicer_delete),
40 ]
41}
42
43pub fn column_values(wb: &Workbook, sheet: usize, table: &str, column: &str) -> Vec<String> {
46 let Some(sh) = wb.sheet(sheet) else { return vec![] };
47 let Some(t) = sh.tables.iter().find(|t| t.name.eq_ignore_ascii_case(table)) else { return vec![] };
48 let Some(ci) = t.column_index(column) else { return vec![] };
49 let Some(dr) = t.data_range() else { return vec![] };
50 let col = dr.start.col + ci as u32;
51 let mut seen = std::collections::BTreeSet::new();
52 let mut out = Vec::new();
53 for row in dr.start.row..=dr.end.row {
54 let text = crate::engine::display::cell_text(wb, sh, CellRef::new(row, col));
55 if text.is_empty() {
56 continue;
57 }
58 if seen.insert(text.to_lowercase()) {
59 out.push(text);
60 }
61 }
62 out
63}
64
65pub fn slicer_items(wb: &Workbook, sheet: usize, sl: &Slicer) -> Vec<(String, bool)> {
68 column_values(wb, sheet, &sl.table, &sl.column)
69 .into_iter()
70 .map(|v| {
71 let on = match &sl.selected {
72 None => true,
73 Some(set) => set.iter().any(|x| x.eq_ignore_ascii_case(&v)),
74 };
75 (v, on)
76 })
77 .collect()
78}
79
80fn reapply_slicer(wb: &mut Workbook, sheet: usize, id: u32) {
82 let info = wb.sheet(sheet).and_then(|sh| {
83 let sl = sh.slicers.iter().find(|s| s.id == id)?;
84 let t = sh.tables.iter().find(|t| t.name.eq_ignore_ascii_case(&sl.table))?;
85 let ci = t.column_index(&sl.column)?;
86 Some((t.range, t.range.start.col + ci as u32, sl.selected.clone()))
87 });
88 let Some((table_range, abs_col, selected)) = info else { return };
89 {
90 let Some(sh) = wb.sheet_mut(sheet) else { return };
91 let af = sh.autofilter.get_or_insert_with(|| AutoFilter { range: table_range, criteria: vec![] });
92 let off = abs_col.saturating_sub(af.range.start.col);
93 af.criteria.retain(|(o, _)| *o != off);
94 if let Some(values) = selected {
95 af.criteria.push((off, FilterCriterion::Values { values, blanks: false }));
96 }
97 }
98 super::data::apply_filter(wb, sheet);
99}
100
101fn insert_slicer(s: &mut Session, p: &Json) -> Result<Json> {
102 let named = match str_param(p, "table") {
106 Some(name) => Some(s.doc()?.wb.table(name).ok_or_else(|| bad("insert.slicer", format!("no table named `{name}`")))?),
107 None => None,
108 };
109 let sheet = match named {
110 Some((si, _)) => si,
111 None => target_sheet(s, p)?,
112 };
113 let d = s.doc()?;
114 let sh = d.wb.sheet(sheet).ok_or(EngineError::NoDocument)?;
115 let t = match named {
116 Some((_, ti)) => sh.tables.get(ti),
117 None => sh.table_at(d.selection.active),
118 }
119 .ok_or_else(|| EngineError::Other("Select a cell inside a table first, or pass `table`.".into()))?;
120 let column = match str_param(p, "column") {
121 Some(c) => t
122 .columns
123 .iter()
124 .find(|tc| tc.name.eq_ignore_ascii_case(c))
125 .map(|tc| tc.name.clone())
126 .ok_or_else(|| bad("insert.slicer", format!("table has no column `{c}`")))?,
127 None => {
128 let idx = d.selection.active.col.checked_sub(t.range.start.col).map(|o| o as usize).filter(|o| *o < t.columns.len()).unwrap_or(0);
130 t.columns.get(idx).map(|tc| tc.name.clone()).ok_or_else(|| bad("insert.slicer", "the table has no columns"))?
131 }
132 };
133 let table = t.name.clone();
134 let at = cell_param_on(s, p, "at", sheet)?
135 .unwrap_or_else(|| CellRef::new(t.range.start.row, t.range.end.col.saturating_add(2).min(crate::base::MAX_COLS - 1)));
136 let columns = u32_param(p, "columns").unwrap_or(1).clamp(1, 10);
137 let width = f64_param(p, "width").unwrap_or(168.0).clamp(60.0, 10000.0) as f32;
138 let height = f64_param(p, "height").unwrap_or(200.0).clamp(60.0, 10000.0) as f32;
139 let id = d.wb.next_object_id();
140 let slicer = Slicer {
141 id,
142 table,
143 column: column.clone(),
144 caption: column,
145 anchor: Anchor { cell: at, dx: 4.0, dy: 4.0, width, height, mode: AnchorMode::MoveOnly },
146 selected: None,
147 columns,
148 };
149 edit(s, |cx| {
150 cx.sheet_mut(sheet)?.slicers.push(slicer);
151 Ok(json!({"slicer": id}))
152 })
153}
154
155fn find_slicer(s: &Session, p: &Json) -> Result<(usize, u32)> {
157 let d = s.doc()?;
158 let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
159 let idx = match u32_param(p, "slicer") {
160 Some(id) => sh.slicers.iter().position(|x| x.id == id),
161 None => sh.slicers.len().checked_sub(1),
162 };
163 idx.and_then(|i| sh.slicers.get(i).map(|x| (i, x.id))).ok_or_else(|| bad("slicer", "no slicer on this sheet"))
164}
165
166fn slicer_toggle(s: &mut Session, p: &Json) -> Result<Json> {
167 let (idx, id) = find_slicer(s, p)?;
168 let value = str_param(p, "value").ok_or_else(|| bad("slicer.toggle", "missing `value`"))?.to_string();
169 let multi = bool_param(p, "multi").unwrap_or(false);
170 let sheet = s.doc()?.wb.active_sheet();
171 let (table, column, current) = {
172 let d = s.doc()?;
173 let sl = d.wb.active().and_then(|sh| sh.slicers.get(idx)).ok_or_else(|| bad("slicer.toggle", "no slicer"))?;
174 (sl.table.clone(), sl.column.clone(), sl.selected.clone())
175 };
176 let all = column_values(&s.doc()?.wb, sheet, &table, &column);
177 if !all.iter().any(|v| v.eq_ignore_ascii_case(&value)) {
178 return Err(bad("slicer.toggle", "value is not in the column"));
179 }
180 let new_selected: Option<Vec<String>> = if multi {
181 let mut set: Vec<String> = current.unwrap_or_else(|| all.clone());
182 match set.iter().position(|x| x.eq_ignore_ascii_case(&value)) {
183 Some(pos) => {
184 set.remove(pos);
185 }
186 None => set.push(value.clone()),
187 }
188 if set.len() == all.len() && all.iter().all(|a| set.iter().any(|x| x.eq_ignore_ascii_case(a))) { None } else { Some(set) }
190 } else {
191 match ¤t {
193 Some(set) if set.len() == 1 && set.iter().any(|x| x.eq_ignore_ascii_case(&value)) => None,
194 _ => Some(vec![value.clone()]),
195 }
196 };
197 edit(s, |cx| {
198 if let Some(sl) = cx.sheet_mut(sheet)?.slicers.get_mut(idx) {
199 sl.selected = new_selected;
200 }
201 reapply_slicer(&mut cx.wb, sheet, id);
202 cx.structural = true;
203 Ok(Json::Null)
204 })
205}
206
207fn slicer_select_all(s: &mut Session, p: &Json) -> Result<Json> {
208 let (idx, id) = find_slicer(s, p)?;
209 let sheet = s.doc()?.wb.active_sheet();
210 edit(s, |cx| {
211 if let Some(sl) = cx.sheet_mut(sheet)?.slicers.get_mut(idx) {
212 sl.selected = None;
213 }
214 reapply_slicer(&mut cx.wb, sheet, id);
215 cx.structural = true;
216 Ok(Json::Null)
217 })
218}
219
220fn slicer_delete(s: &mut Session, p: &Json) -> Result<Json> {
221 let (idx, id) = find_slicer(s, p)?;
222 let sheet = s.doc()?.wb.active_sheet();
223 let abs_col = s.doc().ok().and_then(|d| {
225 let sh = d.wb.active()?;
226 let sl = sh.slicers.get(idx)?;
227 let t = sh.tables.iter().find(|t| t.name.eq_ignore_ascii_case(&sl.table))?;
228 let ci = t.column_index(&sl.column)?;
229 Some(t.range.start.col + ci as u32)
230 });
231 edit(s, |cx| {
232 let sh = cx.sheet_mut(sheet)?;
233 sh.slicers.retain(|x| x.id != id);
234 if let (Some(abs), Some(af)) = (abs_col, sh.autofilter.as_mut()) {
235 let off = abs.saturating_sub(af.range.start.col);
236 af.criteria.retain(|(o, _)| *o != off);
237 }
238 super::data::apply_filter(&mut cx.wb, sheet);
239 cx.structural = true;
240 Ok(Json::Null)
241 })
242}