script/dom/html/
htmltableelement.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6
7use dom_struct::dom_struct;
8use html5ever::{LocalName, Prefix, QualName, local_name, ns};
9use js::context::JSContext;
10use js::rust::HandleObject;
11use style::attr::{AttrValue, LengthOrPercentageOrAuto, parse_unsigned_integer};
12use style::color::AbsoluteColor;
13
14use crate::dom::attr::Attr;
15use crate::dom::bindings::codegen::Bindings::HTMLCollectionBinding::HTMLCollectionMethods;
16use crate::dom::bindings::codegen::Bindings::HTMLTableElementBinding::HTMLTableElementMethods;
17use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
18use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
19use crate::dom::bindings::inheritance::Castable;
20use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom};
21use crate::dom::bindings::str::DOMString;
22use crate::dom::document::Document;
23use crate::dom::element::{
24    AttributeMutation, CustomElementCreationMode, Element, ElementCreator, LayoutElementHelpers,
25};
26use crate::dom::html::htmlcollection::{CollectionFilter, HTMLCollection};
27use crate::dom::html::htmlelement::HTMLElement;
28use crate::dom::html::htmltablecaptionelement::HTMLTableCaptionElement;
29use crate::dom::html::htmltablecolelement::HTMLTableColElement;
30use crate::dom::html::htmltablerowelement::HTMLTableRowElement;
31use crate::dom::html::htmltablesectionelement::HTMLTableSectionElement;
32use crate::dom::node::{Node, NodeTraits};
33use crate::dom::virtualmethods::VirtualMethods;
34use crate::script_runtime::CanGc;
35
36#[dom_struct]
37pub(crate) struct HTMLTableElement {
38    htmlelement: HTMLElement,
39    border: Cell<Option<u32>>,
40    cellpadding: Cell<Option<u32>>,
41    cellspacing: Cell<Option<u32>>,
42    tbodies: MutNullableDom<HTMLCollection>,
43}
44
45#[cfg_attr(crown, expect(crown::unrooted_must_root))]
46#[derive(JSTraceable, MallocSizeOf)]
47struct TableRowFilter {
48    sections: Vec<Dom<Node>>,
49}
50
51impl CollectionFilter for TableRowFilter {
52    fn filter(&self, elem: &Element, root: &Node) -> bool {
53        elem.is::<HTMLTableRowElement>() &&
54            (root.is_parent_of(elem.upcast()) ||
55                self.sections
56                    .iter()
57                    .any(|section| section.is_parent_of(elem.upcast())))
58    }
59}
60
61impl HTMLTableElement {
62    fn new_inherited(
63        local_name: LocalName,
64        prefix: Option<Prefix>,
65        document: &Document,
66    ) -> HTMLTableElement {
67        HTMLTableElement {
68            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
69            border: Cell::new(None),
70            cellpadding: Cell::new(None),
71            cellspacing: Cell::new(None),
72            tbodies: Default::default(),
73        }
74    }
75
76    pub(crate) fn new(
77        cx: &mut js::context::JSContext,
78        local_name: LocalName,
79        prefix: Option<Prefix>,
80        document: &Document,
81        proto: Option<HandleObject>,
82    ) -> DomRoot<HTMLTableElement> {
83        let n = Node::reflect_node_with_proto(
84            cx,
85            Box::new(HTMLTableElement::new_inherited(
86                local_name, prefix, document,
87            )),
88            document,
89            proto,
90        );
91
92        n.upcast::<Node>().set_weird_parser_insertion_mode();
93        n
94    }
95
96    pub(crate) fn get_border(&self) -> Option<u32> {
97        self.border.get()
98    }
99
100    // https://html.spec.whatwg.org/multipage/#dom-table-thead
101    /// <https://html.spec.whatwg.org/multipage/#dom-table-tfoot>
102    fn get_first_section_of_type(
103        &self,
104        atom: &LocalName,
105    ) -> Option<DomRoot<HTMLTableSectionElement>> {
106        self.upcast::<Node>()
107            .child_elements()
108            .find(|n| n.is::<HTMLTableSectionElement>() && n.local_name() == atom)
109            .and_then(|n| n.downcast().map(DomRoot::from_ref))
110    }
111
112    // https://html.spec.whatwg.org/multipage/#dom-table-thead
113    /// <https://html.spec.whatwg.org/multipage/#dom-table-tfoot>
114    fn set_first_section_of_type<P>(
115        &self,
116        cx: &mut JSContext,
117        atom: &LocalName,
118        section: Option<&HTMLTableSectionElement>,
119        reference_predicate: P,
120    ) -> ErrorResult
121    where
122        P: FnMut(&DomRoot<Element>) -> bool,
123    {
124        if let Some(e) = section {
125            if e.upcast::<Element>().local_name() != atom {
126                return Err(Error::HierarchyRequest(None));
127            }
128        }
129
130        self.delete_first_section_of_type(cx, atom);
131
132        let node = self.upcast::<Node>();
133
134        if let Some(section) = section {
135            let reference_element = node.child_elements().find(reference_predicate);
136            let reference_node = reference_element.as_ref().map(|e| e.upcast());
137
138            node.InsertBefore(cx, section.upcast(), reference_node)?;
139        }
140
141        Ok(())
142    }
143
144    /// <https://html.spec.whatwg.org/multipage/#dom-table-createthead>
145    /// <https://html.spec.whatwg.org/multipage/#dom-table-createtfoot>
146    fn create_section_of_type(
147        &self,
148        cx: &mut JSContext,
149        atom: &LocalName,
150    ) -> DomRoot<HTMLTableSectionElement> {
151        if let Some(section) = self.get_first_section_of_type(atom) {
152            return section;
153        }
154
155        let section = Element::create(
156            cx,
157            QualName::new(None, ns!(html), atom.clone()),
158            None,
159            &self.owner_document(),
160            ElementCreator::ScriptCreated,
161            CustomElementCreationMode::Asynchronous,
162            None,
163        );
164
165        let section = DomRoot::downcast::<HTMLTableSectionElement>(section).unwrap();
166
167        match *atom {
168            local_name!("thead") => self.SetTHead(cx, Some(&section)),
169            local_name!("tfoot") => self.SetTFoot(cx, Some(&section)),
170            _ => unreachable!("unexpected section type"),
171        }
172        .expect("unexpected section type");
173
174        section
175    }
176
177    /// <https://html.spec.whatwg.org/multipage/#dom-table-deletethead>
178    /// <https://html.spec.whatwg.org/multipage/#dom-table-deletetfoot>
179    fn delete_first_section_of_type(&self, cx: &mut JSContext, atom: &LocalName) {
180        if let Some(thead) = self.get_first_section_of_type(atom) {
181            thead.upcast::<Node>().remove_self(cx);
182        }
183    }
184
185    fn get_rows(&self) -> TableRowFilter {
186        TableRowFilter {
187            sections: self
188                .upcast::<Node>()
189                .children()
190                .filter_map(|ref node| {
191                    node.downcast::<HTMLTableSectionElement>()
192                        .map(|_| Dom::from_ref(&**node))
193                })
194                .collect(),
195        }
196    }
197}
198
199impl HTMLTableElementMethods<crate::DomTypeHolder> for HTMLTableElement {
200    /// <https://html.spec.whatwg.org/multipage/#dom-table-rows>
201    fn Rows(&self) -> DomRoot<HTMLCollection> {
202        let filter = self.get_rows();
203        HTMLCollection::new(
204            &self.owner_window(),
205            self.upcast(),
206            Box::new(filter),
207            CanGc::note(),
208        )
209    }
210
211    /// <https://html.spec.whatwg.org/multipage/#dom-table-caption>
212    fn GetCaption(&self) -> Option<DomRoot<HTMLTableCaptionElement>> {
213        self.upcast::<Node>().children().find_map(DomRoot::downcast)
214    }
215
216    /// <https://html.spec.whatwg.org/multipage/#dom-table-caption>
217    fn SetCaption(
218        &self,
219        cx: &mut JSContext,
220        new_caption: Option<&HTMLTableCaptionElement>,
221    ) -> Fallible<()> {
222        if let Some(ref caption) = self.GetCaption() {
223            caption.upcast::<Node>().remove_self(cx);
224        }
225
226        if let Some(caption) = new_caption {
227            let node = self.upcast::<Node>();
228            node.InsertBefore(cx, caption.upcast(), node.GetFirstChild().as_deref())?;
229        }
230
231        Ok(())
232    }
233
234    /// <https://html.spec.whatwg.org/multipage/#dom-table-createcaption>
235    fn CreateCaption(&self, cx: &mut JSContext) -> DomRoot<HTMLTableCaptionElement> {
236        match self.GetCaption() {
237            Some(caption) => caption,
238            None => {
239                let caption = Element::create(
240                    cx,
241                    QualName::new(None, ns!(html), local_name!("caption")),
242                    None,
243                    &self.owner_document(),
244                    ElementCreator::ScriptCreated,
245                    CustomElementCreationMode::Asynchronous,
246                    None,
247                );
248                let caption = DomRoot::downcast::<HTMLTableCaptionElement>(caption).unwrap();
249
250                self.SetCaption(cx, Some(&caption))
251                    .expect("Generated caption is invalid");
252                caption
253            },
254        }
255    }
256
257    /// <https://html.spec.whatwg.org/multipage/#dom-table-deletecaption>
258    fn DeleteCaption(&self, cx: &mut JSContext) {
259        if let Some(caption) = self.GetCaption() {
260            caption.upcast::<Node>().remove_self(cx);
261        }
262    }
263
264    /// <https://html.spec.whatwg.org/multipage/#dom-table-thead>
265    fn GetTHead(&self) -> Option<DomRoot<HTMLTableSectionElement>> {
266        self.get_first_section_of_type(&local_name!("thead"))
267    }
268
269    /// <https://html.spec.whatwg.org/multipage/#dom-table-thead>
270    fn SetTHead(&self, cx: &mut JSContext, thead: Option<&HTMLTableSectionElement>) -> ErrorResult {
271        self.set_first_section_of_type(cx, &local_name!("thead"), thead, |n| {
272            !n.is::<HTMLTableCaptionElement>() && !n.is::<HTMLTableColElement>()
273        })
274    }
275
276    /// <https://html.spec.whatwg.org/multipage/#dom-table-createthead>
277    fn CreateTHead(&self, cx: &mut JSContext) -> DomRoot<HTMLTableSectionElement> {
278        self.create_section_of_type(cx, &local_name!("thead"))
279    }
280
281    /// <https://html.spec.whatwg.org/multipage/#dom-table-deletethead>
282    fn DeleteTHead(&self, cx: &mut JSContext) {
283        self.delete_first_section_of_type(cx, &local_name!("thead"))
284    }
285
286    /// <https://html.spec.whatwg.org/multipage/#dom-table-tfoot>
287    fn GetTFoot(&self) -> Option<DomRoot<HTMLTableSectionElement>> {
288        self.get_first_section_of_type(&local_name!("tfoot"))
289    }
290
291    /// <https://html.spec.whatwg.org/multipage/#dom-table-tfoot>
292    fn SetTFoot(&self, cx: &mut JSContext, tfoot: Option<&HTMLTableSectionElement>) -> ErrorResult {
293        self.set_first_section_of_type(cx, &local_name!("tfoot"), tfoot, |n| {
294            if n.is::<HTMLTableCaptionElement>() || n.is::<HTMLTableColElement>() {
295                return false;
296            }
297
298            if n.is::<HTMLTableSectionElement>() {
299                let name = n.local_name();
300                if name == &local_name!("thead") || name == &local_name!("tbody") {
301                    return false;
302                }
303            }
304
305            true
306        })
307    }
308
309    /// <https://html.spec.whatwg.org/multipage/#dom-table-createtfoot>
310    fn CreateTFoot(&self, cx: &mut JSContext) -> DomRoot<HTMLTableSectionElement> {
311        self.create_section_of_type(cx, &local_name!("tfoot"))
312    }
313
314    /// <https://html.spec.whatwg.org/multipage/#dom-table-deletetfoot>
315    fn DeleteTFoot(&self, cx: &mut JSContext) {
316        self.delete_first_section_of_type(cx, &local_name!("tfoot"))
317    }
318
319    /// <https://html.spec.whatwg.org/multipage/#dom-table-tbodies>
320    fn TBodies(&self) -> DomRoot<HTMLCollection> {
321        self.tbodies.or_init(|| {
322            HTMLCollection::new_with_filter_fn(
323                &self.owner_window(),
324                self.upcast(),
325                |element, root| {
326                    element.is::<HTMLTableSectionElement>() &&
327                        element.local_name() == &local_name!("tbody") &&
328                        element.upcast::<Node>().GetParentNode().as_deref() == Some(root)
329                },
330                CanGc::note(),
331            )
332        })
333    }
334
335    /// <https://html.spec.whatwg.org/multipage/#dom-table-createtbody>
336    fn CreateTBody(&self, cx: &mut JSContext) -> DomRoot<HTMLTableSectionElement> {
337        let tbody = Element::create(
338            cx,
339            QualName::new(None, ns!(html), local_name!("tbody")),
340            None,
341            &self.owner_document(),
342            ElementCreator::ScriptCreated,
343            CustomElementCreationMode::Asynchronous,
344            None,
345        );
346        let tbody = DomRoot::downcast::<HTMLTableSectionElement>(tbody).unwrap();
347        let node = self.upcast::<Node>();
348        let last_tbody = node
349            .rev_children()
350            .filter_map(DomRoot::downcast::<Element>)
351            .find(|n| n.is::<HTMLTableSectionElement>() && n.local_name() == &local_name!("tbody"));
352        let reference_element = last_tbody.and_then(|t| t.upcast::<Node>().GetNextSibling());
353
354        node.InsertBefore(cx, tbody.upcast(), reference_element.as_deref())
355            .expect("Insertion failed");
356        tbody
357    }
358
359    /// <https://html.spec.whatwg.org/multipage/#dom-table-insertrow>
360    fn InsertRow(&self, cx: &mut JSContext, index: i32) -> Fallible<DomRoot<HTMLTableRowElement>> {
361        let rows = self.Rows();
362        let number_of_row_elements = rows.Length();
363
364        if index < -1 || index > number_of_row_elements as i32 {
365            return Err(Error::IndexSize(None));
366        }
367
368        let new_row = Element::create(
369            cx,
370            QualName::new(None, ns!(html), local_name!("tr")),
371            None,
372            &self.owner_document(),
373            ElementCreator::ScriptCreated,
374            CustomElementCreationMode::Asynchronous,
375            None,
376        );
377        let new_row = DomRoot::downcast::<HTMLTableRowElement>(new_row).unwrap();
378        let node = self.upcast::<Node>();
379
380        if number_of_row_elements == 0 {
381            // append new row to last or new tbody in table
382            if let Some(last_tbody) = node
383                .rev_children()
384                .filter_map(DomRoot::downcast::<Element>)
385                .find(|n| {
386                    n.is::<HTMLTableSectionElement>() && n.local_name() == &local_name!("tbody")
387                })
388            {
389                last_tbody
390                    .upcast::<Node>()
391                    .AppendChild(cx, new_row.upcast::<Node>())
392                    .expect("InsertRow failed to append first row.");
393            } else {
394                let tbody = self.CreateTBody(cx);
395                node.AppendChild(cx, tbody.upcast())
396                    .expect("InsertRow failed to append new tbody.");
397
398                tbody
399                    .upcast::<Node>()
400                    .AppendChild(cx, new_row.upcast::<Node>())
401                    .expect("InsertRow failed to append first row.");
402            }
403        } else if index == number_of_row_elements as i32 || index == -1 {
404            // append new row to parent of last row in table
405            let last_row = rows
406                .Item(number_of_row_elements - 1)
407                .expect("InsertRow failed to find last row in table.");
408
409            let last_row_parent = last_row
410                .upcast::<Node>()
411                .GetParentNode()
412                .expect("InsertRow failed to find parent of last row in table.");
413
414            last_row_parent
415                .upcast::<Node>()
416                .AppendChild(cx, new_row.upcast::<Node>())
417                .expect("InsertRow failed to append last row.");
418        } else {
419            // insert new row before the index-th row in rows using the same parent
420            let ith_row = rows
421                .Item(index as u32)
422                .expect("InsertRow failed to find a row in table.");
423
424            let ith_row_parent = ith_row
425                .upcast::<Node>()
426                .GetParentNode()
427                .expect("InsertRow failed to find parent of a row in table.");
428
429            ith_row_parent
430                .upcast::<Node>()
431                .InsertBefore(cx, new_row.upcast::<Node>(), Some(ith_row.upcast::<Node>()))
432                .expect("InsertRow failed to append row");
433        }
434
435        Ok(new_row)
436    }
437
438    /// <https://html.spec.whatwg.org/multipage/#dom-table-deleterow>
439    fn DeleteRow(&self, cx: &mut JSContext, mut index: i32) -> Fallible<()> {
440        let rows = self.Rows();
441        let num_rows = rows.Length() as i32;
442
443        // Step 1: If index is less than −1 or greater than or equal to the number of elements
444        // in the rows collection, then throw an "IndexSizeError".
445        if !(-1..num_rows).contains(&index) {
446            return Err(Error::IndexSize(None));
447        }
448
449        let num_rows = rows.Length() as i32;
450
451        // Step 2: If index is −1, then remove the last element in the rows collection from its
452        // parent, or do nothing if the rows collection is empty.
453        if index == -1 {
454            index = num_rows - 1;
455        }
456
457        if num_rows == 0 {
458            return Ok(());
459        }
460
461        // Step 3: Otherwise, remove the indexth element in the rows collection from its parent.
462        DomRoot::upcast::<Node>(rows.Item(index as u32).unwrap()).remove_self(cx);
463
464        Ok(())
465    }
466
467    // https://html.spec.whatwg.org/multipage/#dom-table-bgcolor
468    make_getter!(BgColor, "bgcolor");
469
470    // https://html.spec.whatwg.org/multipage/#dom-table-bgcolor
471    make_legacy_color_setter!(SetBgColor, "bgcolor");
472
473    // <https://html.spec.whatwg.org/multipage/#dom-table-width>
474    make_getter!(Width, "width");
475
476    // <https://html.spec.whatwg.org/multipage/#dom-table-width>
477    make_nonzero_dimension_setter!(SetWidth, "width");
478
479    // <https://html.spec.whatwg.org/multipage/#dom-table-align>
480    make_setter!(SetAlign, "align");
481    make_getter!(Align, "align");
482
483    // <https://html.spec.whatwg.org/multipage/#dom-table-cellpadding>
484    make_setter!(SetCellPadding, "cellpadding");
485    make_getter!(CellPadding, "cellpadding");
486
487    // <https://html.spec.whatwg.org/multipage/#dom-table-cellspacing>
488    make_setter!(SetCellSpacing, "cellspacing");
489    make_getter!(CellSpacing, "cellspacing");
490}
491
492pub(crate) trait HTMLTableElementLayoutHelpers {
493    fn get_background_color(self) -> Option<AbsoluteColor>;
494    fn get_border(self) -> Option<u32>;
495    fn get_cellpadding(self) -> Option<u32>;
496    fn get_cellspacing(self) -> Option<u32>;
497    fn get_width(self) -> LengthOrPercentageOrAuto;
498    fn get_height(self) -> LengthOrPercentageOrAuto;
499}
500
501impl HTMLTableElementLayoutHelpers for LayoutDom<'_, HTMLTableElement> {
502    fn get_background_color(self) -> Option<AbsoluteColor> {
503        self.upcast::<Element>()
504            .get_attr_for_layout(&ns!(), &local_name!("bgcolor"))
505            .and_then(AttrValue::as_color)
506            .cloned()
507    }
508
509    fn get_border(self) -> Option<u32> {
510        (self.unsafe_get()).border.get()
511    }
512
513    fn get_cellpadding(self) -> Option<u32> {
514        (self.unsafe_get()).cellpadding.get()
515    }
516
517    fn get_cellspacing(self) -> Option<u32> {
518        (self.unsafe_get()).cellspacing.get()
519    }
520
521    fn get_width(self) -> LengthOrPercentageOrAuto {
522        self.upcast::<Element>()
523            .get_attr_for_layout(&ns!(), &local_name!("width"))
524            .map(AttrValue::as_dimension)
525            .cloned()
526            .unwrap_or(LengthOrPercentageOrAuto::Auto)
527    }
528
529    fn get_height(self) -> LengthOrPercentageOrAuto {
530        self.upcast::<Element>()
531            .get_attr_for_layout(&ns!(), &local_name!("height"))
532            .map(AttrValue::as_dimension)
533            .cloned()
534            .unwrap_or(LengthOrPercentageOrAuto::Auto)
535    }
536}
537
538impl VirtualMethods for HTMLTableElement {
539    fn super_type(&self) -> Option<&dyn VirtualMethods> {
540        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
541    }
542
543    fn attribute_mutated(
544        &self,
545        cx: &mut js::context::JSContext,
546        attr: &Attr,
547        mutation: AttributeMutation,
548    ) {
549        self.super_type()
550            .unwrap()
551            .attribute_mutated(cx, attr, mutation);
552        match *attr.local_name() {
553            local_name!("border") => {
554                // According to HTML5 § 14.3.9, invalid values map to 1px.
555                self.border.set(
556                    mutation
557                        .new_value(attr)
558                        .map(|value| parse_unsigned_integer(value.chars()).unwrap_or(1)),
559                );
560            },
561            local_name!("cellpadding") => {
562                self.cellpadding.set(
563                    mutation
564                        .new_value(attr)
565                        .and_then(|value| parse_unsigned_integer(value.chars()).ok()),
566                );
567            },
568            local_name!("cellspacing") => {
569                self.cellspacing.set(
570                    mutation
571                        .new_value(attr)
572                        .and_then(|value| parse_unsigned_integer(value.chars()).ok()),
573                );
574            },
575            _ => {},
576        }
577    }
578
579    fn attribute_affects_presentational_hints(&self, attr: &Attr) -> bool {
580        match attr.local_name() {
581            &local_name!("width") | &local_name!("height") => true,
582            _ => self
583                .super_type()
584                .unwrap()
585                .attribute_affects_presentational_hints(attr),
586        }
587    }
588
589    fn parse_plain_attribute(&self, local_name: &LocalName, value: DOMString) -> AttrValue {
590        match *local_name {
591            local_name!("border") => AttrValue::from_u32(value.into(), 1),
592            local_name!("width") => AttrValue::from_nonzero_dimension(value.into()),
593            local_name!("height") => AttrValue::from_dimension(value.into()),
594            local_name!("bgcolor") => AttrValue::from_legacy_color(value.into()),
595            _ => self
596                .super_type()
597                .unwrap()
598                .parse_plain_attribute(local_name, value),
599        }
600    }
601}