Source code for wildewidgets.widgets.tables.static

import uuid

from ..base import Block

# -----------------------------------------------------------------------------
# Widgets
# -----------------------------------------------------------------------------


[docs]class StaticTableRowWidget(Block): """ Simple static table row widget. This widget is used to create a row in a :class:`~workshop_admin.core.wildewidgets.StaticTableWidget`. Args: cells: the cells to add to the row cell_tag: the HTML tag for the cells cell_css_class: the CSS class for the cells **kwargs: additional keyword arguments to pass to the block """ #: The HTML tag for the row tag: str = "tr" #: The HTML tag for the cells cell_tag: str = "td" def __init__( self, cells: list[str | Block], cell_tag: str | None = None, cell_css_class: str | None = None, **kwargs, ) -> None: if cell_tag is not None: self.cell_tag = cell_tag rendered_cells: list[Block] = [] for cell in cells: if isinstance(cell, Block) and cell.tag == self.cell_tag: rendered_cells.append(cell) continue rendered_cells.append( Block(cell, tag=self.cell_tag, css_class=cell_css_class) ) super().__init__(*rendered_cells, **kwargs)
[docs]class StaticTableWidget(Block): """ Simple static table widget. This differs from the :class:`~wildewidgets.DataTable` type widgets in that it does not use dataTables.js and is therefore simpler for small lists of data. It offers a sorting mechanism by clicking on the "Sort" button in the heading of the column. The table is sorted by the text content of the cells in the column that the button is in. If every cell in that column is numeric (optional minus, digits, optional decimal), the column is sorted numerically; otherwise locale-aware alphabetical sort is used. Empty cells in numeric columns sort last (ascending) or first (descending). Examples: With constructor arguments: .. code-block:: python from wildewidgets import StaticTableWidget table = StaticTableWidget( headings=["Name", "Age", "City"], rows=[["John", 25, "New York"], ["Jane", 30, "Los Angeles"]] ) print(table) With constructor overrides: .. code-block:: python from wildewidgets import StaticTableWidget class MyStaticTableWidget(StaticTableWidget): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.add_heading("Name") self.add_heading("Age") self.add_heading("City") self.add_row(["John", 25, "New York"]) self.add_row(["Jane", 30, "Los Angeles"]) table = MyStaticTableWidget() print(table) With method calls: .. code-block:: python from wildewidgets import StaticTableWidget table = StaticTableWidget() table.add_heading("Name") table.add_heading("Age") table.add_heading("City") table.add_row(["John", 25, "New York"]) table.add_row(["Jane", 30, "Los Angeles"]) print(table) .. code-block:: python from wildewidgets import StaticTableWidget table = StaticTableWidget( headings=["Name", "Age", "City"], rows=[["John", 25, "New York"], ["Jane", 30, "Los Angeles"]] ) table.add_heading("Country") table.add_heading("Occupation") table.add_heading("City"sjj table.add_row( ["John", 25, "New York", "United States", "Software Engineer"] ) table.add_row( ["Jane", 30, "Los Angeles", "United States", "Software Engineer"] ) print(table) Keyword Args: headings: the headings for the table rows: the rows for the table cell_css_class: the CSS class for the cells **kwargs: additional keyword arguments to pass to the block """ #: The HTML tag for the table tag: str = "table" #: The HTML name for the widget for CSS styling and JavaScript name: str = "static-table" @property def script(self) -> str: """ A JavaScript script to sort the table by clicking on a button in the headings. Iniitially, the table is sorted in the order they are listed in the data source. It draws a button in the ``<th>`` elements named "Sort" with the current sort order represented by an arrow (if we are sorting) or a blank (if we are not sorting). When the button is clicked, the table is sorted by the opposite of the current sort order or ascending (if no sort order is set), and the button text is updated to the opposite of the selected sort order. Note: The table is sorted by the text content of the cells in the column that the button is in. If every cell in that column is numeric (optional minus, digits, optional decimal), the column is sorted numerically; otherwise locale-aware alphabetical sort is used. Empty cells in numeric columns sort last (ascending) or first (descending). Args: self: the widget instance Returns: A string of JavaScript code to sort the table by clicking on the headings. """ sort_button = '<button class="btn btn-link btn-sm" style="font-size: 0.8em;">Sort</button>' # noqa: E501 return f""" document.addEventListener("DOMContentLoaded", function() {{ var table = document.querySelector('table#{self._css_id}'); // Add the up and down arrows to the headings. var headings = table.querySelectorAll('thead th'); headings.forEach(function(heading) {{ heading.innerHTML = heading.innerHTML + '{sort_button}'; }}); function isNumericColumn(rows, colIdx) {{ var numericPattern = /^-?\\d*\\.?\\d+$/; for (var i = 0; i < rows.length; i++) {{ var cell = rows[i].cells[colIdx]; if (!cell) return false; var text = (cell.textContent || '').trim(); if (text !== '' && !numericPattern.test(text)) return false; }} return true; }} function compareCells(aVal, bVal, numeric, asc) {{ if (numeric) {{ var na = parseFloat(aVal); var nb = parseFloat(bVal); var aEmpty = (aVal.trim() === '') || isNaN(na); var bEmpty = (bVal.trim() === '') || isNaN(nb); if (aEmpty && bEmpty) return 0; if (aEmpty) return asc ? 1 : -1; if (bEmpty) return asc ? -1 : 1; return asc ? (na - nb) : (nb - na); }} return asc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); }} if (table) {{ table.addEventListener('click', function(event) {{ var target = event.target; if (target.tagName === 'BUTTON') {{ var columnIndex = target.parentElement.cellIndex; var is_ascending = target.textContent.includes('↓'); var rows = Array.from(table.querySelectorAll('tbody tr')); var numeric = isNumericColumn(rows, columnIndex); if (!is_ascending) {{ rows.sort(function(a, b) {{ var aText = (a.cells[columnIndex].textContent || '').trim(); var bText = (b.cells[columnIndex].textContent || '').trim(); return compareCells(aText, bText, numeric, true); }}); }} else {{ rows.sort(function(a, b) {{ var aText = (a.cells[columnIndex].textContent || '').trim(); var bText = (b.cells[columnIndex].textContent || '').trim(); return compareCells(aText, bText, numeric, false); }}); }} var tbody = table.querySelector('tbody'); tbody.innerHTML = ''; rows.forEach(function(row) {{ tbody.appendChild(row); }}); // update the button text to the opposite of the current // sort order target.textContent = "Sort " + (!is_ascending ? '↓' : '↑'); // Set the buttons in the other columns to no sort var otherColumns = table.querySelectorAll('thead th'); otherColumns.forEach(function(column) {{ if (column !== target.parentElement) {{ column.querySelector('button').textContent = 'Sort'; }} }}); }} }}); }} }}); """ # noqa: E501, S608 @script.setter def script(self, value: str) -> None: self._script = value def __init__( self, headings: list[str | Block] | None = None, rows: list[list[str | Block]] | None = None, cell_css_class: str | None = None, **kwargs, ) -> None: if headings is not None and rows is not None: assert len(headings) == len(rows[0]), ( # noqa: S101 "Headings and rows must have the same length" ) super().__init__(css_class="table table-sm mb-0", **kwargs) self.headings = headings or [] self.rows = rows or [] self.header = Block(tag="thead") self.body = Block(tag="tbody") self.cell_css_class = cell_css_class if self.headings: for heading in self.headings: self.header.add_block( Block(heading, tag="th", css_class=self.cell_css_class) ) for row in self.rows: self.body.add_block( StaticTableRowWidget(row, cell_css_class=self.cell_css_class) ) if self.headings: self.add_block(self.header) if self.rows: self.add_block(self.body) if not self._css_id: self._css_id = f"static-table-{uuid.uuid4()}"
[docs] def add_heading(self, heading: str | Block) -> None: """ Add a single heading to the table. Do this before adding any rows. Args: heading: the heading to add to the table """ self.header.add_block(Block(heading, tag="th", css_class=self.cell_css_class)) if self.header not in self.blocks: self.add_block(self.header)
[docs] def add_row(self, row: list[str | Block]) -> None: """ Add a single row to the table. Do this after adding headings. """ self.body.add_block( StaticTableRowWidget(row, cell_css_class=self.cell_css_class) ) if self.body not in self.blocks: self.add_block(self.body)