Skip to main content

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