|
15 | 15 | */
|
16 | 16 |
|
17 | 17 | const ModelProperty = {
|
18 |
| - TABLE_HTML: "table_html", |
19 |
| - ROW_COUNT: "row_count", |
20 |
| - PAGE_SIZE: "page_size", |
21 | 18 | PAGE: "page",
|
| 19 | + PAGE_SIZE: "page_size", |
| 20 | + ROW_COUNT: "row_count", |
| 21 | + TABLE_HTML: "table_html", |
22 | 22 | };
|
23 | 23 |
|
24 | 24 | const Event = {
|
| 25 | + CHANGE: "change", |
25 | 26 | CHANGE_TABLE_HTML: `change:${ModelProperty.TABLE_HTML}`,
|
26 | 27 | CLICK: "click",
|
27 | 28 | };
|
28 | 29 |
|
29 | 30 | /**
|
30 |
| - * Renders a paginated table and its controls into a given element. |
| 31 | + * Renders the interactive table widget. |
31 | 32 | * @param {{
|
32 |
| - * model: !Backbone.Model, |
33 |
| - * el: !HTMLElement |
| 33 | + * model: any, |
| 34 | + * el: HTMLElement |
34 | 35 | * }} options
|
35 | 36 | */
|
36 | 37 | function render({ model, el }) {
|
| 38 | + // Main container with a unique class for CSS scoping |
37 | 39 | const container = document.createElement("div");
|
38 |
| - container.innerHTML = model.get(ModelProperty.TABLE_HTML); |
| 40 | + container.classList.add("bigframes-widget"); |
| 41 | + |
| 42 | + // Structure |
| 43 | + const tableContainer = document.createElement("div"); |
| 44 | + const footer = document.createElement("div"); |
39 | 45 |
|
40 |
| - const buttonContainer = document.createElement("div"); |
| 46 | + // Footer: Total rows label |
| 47 | + const rowCountLabel = document.createElement("div"); |
| 48 | + |
| 49 | + // Footer: Pagination controls |
| 50 | + const paginationContainer = document.createElement("div"); |
41 | 51 | const prevPage = document.createElement("button");
|
42 |
| - const label = document.createElement("span"); |
| 52 | + const paginationLabel = document.createElement("span"); |
43 | 53 | const nextPage = document.createElement("button");
|
44 | 54 |
|
| 55 | + // Footer: Page size controls |
| 56 | + const pageSizeContainer = document.createElement("div"); |
| 57 | + const pageSizeLabel = document.createElement("label"); |
| 58 | + const pageSizeSelect = document.createElement("select"); |
| 59 | + |
| 60 | + // Add CSS classes |
| 61 | + tableContainer.classList.add("table-container"); |
| 62 | + footer.classList.add("footer"); |
| 63 | + paginationContainer.classList.add("pagination"); |
| 64 | + pageSizeContainer.classList.add("page-size"); |
| 65 | + |
| 66 | + // Configure pagination buttons |
45 | 67 | prevPage.type = "button";
|
46 | 68 | nextPage.type = "button";
|
47 | 69 | prevPage.textContent = "Prev";
|
48 | 70 | nextPage.textContent = "Next";
|
49 | 71 |
|
50 |
| - /** Updates the button states and page label based on the model. */ |
| 72 | + // Configure page size selector |
| 73 | + pageSizeLabel.textContent = "Page Size"; |
| 74 | + for (const size of [10, 25, 50, 100]) { |
| 75 | + const option = document.createElement("option"); |
| 76 | + option.value = size; |
| 77 | + option.textContent = size; |
| 78 | + if (size === model.get(ModelProperty.PAGE_SIZE)) { |
| 79 | + option.selected = true; |
| 80 | + } |
| 81 | + pageSizeSelect.appendChild(option); |
| 82 | + } |
| 83 | + |
| 84 | + /** Updates the footer states and page label based on the model. */ |
51 | 85 | function updateButtonStates() {
|
52 |
| - const totalPages = Math.ceil( |
53 |
| - model.get(ModelProperty.ROW_COUNT) / model.get(ModelProperty.PAGE_SIZE), |
54 |
| - ); |
| 86 | + const rowCount = model.get(ModelProperty.ROW_COUNT); |
| 87 | + const pageSize = model.get(ModelProperty.PAGE_SIZE); |
55 | 88 | const currentPage = model.get(ModelProperty.PAGE);
|
| 89 | + const totalPages = Math.ceil(rowCount / pageSize); |
56 | 90 |
|
57 |
| - label.textContent = `Page ${currentPage + 1} of ${totalPages}`; |
| 91 | + rowCountLabel.textContent = `${rowCount.toLocaleString()} total rows`; |
| 92 | + paginationLabel.textContent = `Page ${currentPage + 1} of ${totalPages || 1}`; |
58 | 93 | prevPage.disabled = currentPage === 0;
|
59 | 94 | nextPage.disabled = currentPage >= totalPages - 1;
|
| 95 | + pageSizeSelect.value = pageSize; |
60 | 96 | }
|
61 | 97 |
|
62 | 98 | /**
|
63 |
| - * Updates the page in the model. |
64 |
| - * @param {number} direction -1 for previous, 1 for next. |
| 99 | + * Increments or decrements the page in the model. |
| 100 | + * @param {number} direction - `1` for next, `-1` for previous. |
65 | 101 | */
|
66 | 102 | function handlePageChange(direction) {
|
67 |
| - const currentPage = model.get(ModelProperty.PAGE); |
68 |
| - const newPage = Math.max(0, currentPage + direction); |
69 |
| - if (newPage !== currentPage) { |
70 |
| - model.set(ModelProperty.PAGE, newPage); |
| 103 | + const current = model.get(ModelProperty.PAGE); |
| 104 | + const next = current + direction; |
| 105 | + model.set(ModelProperty.PAGE, next); |
| 106 | + model.save_changes(); |
| 107 | + } |
| 108 | + |
| 109 | + /** |
| 110 | + * Handles changes to the page size from the dropdown. |
| 111 | + * @param {number} size - The new page size. |
| 112 | + */ |
| 113 | + function handlePageSizeChange(size) { |
| 114 | + const currentSize = model.get(ModelProperty.PAGE_SIZE); |
| 115 | + if (size !== currentSize) { |
| 116 | + model.set(ModelProperty.PAGE_SIZE, size); |
71 | 117 | model.save_changes();
|
72 | 118 | }
|
73 | 119 | }
|
74 | 120 |
|
| 121 | + /** Updates the HTML in the table container and refreshes button states. */ |
| 122 | + function handleTableHTMLChange() { |
| 123 | + // Note: Using innerHTML is safe here because the content is generated |
| 124 | + // by a trusted backend (DataFrame.to_html). |
| 125 | + tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML); |
| 126 | + updateButtonStates(); |
| 127 | + } |
| 128 | + |
| 129 | + // Add event listeners |
75 | 130 | prevPage.addEventListener(Event.CLICK, () => handlePageChange(-1));
|
76 | 131 | nextPage.addEventListener(Event.CLICK, () => handlePageChange(1));
|
77 |
| - |
78 |
| - model.on(Event.CHANGE_TABLE_HTML, () => { |
79 |
| - // Note: Using innerHTML can be a security risk if the content is |
80 |
| - // user-generated. Ensure 'table_html' is properly sanitized. |
81 |
| - container.innerHTML = model.get(ModelProperty.TABLE_HTML); |
82 |
| - updateButtonStates(); |
| 132 | + pageSizeSelect.addEventListener(Event.CHANGE, (e) => { |
| 133 | + const newSize = Number(e.target.value); |
| 134 | + if (newSize) { |
| 135 | + handlePageSizeChange(newSize); |
| 136 | + } |
83 | 137 | });
|
| 138 | + model.on(Event.CHANGE_TABLE_HTML, handleTableHTMLChange); |
| 139 | + |
| 140 | + // Assemble the DOM |
| 141 | + paginationContainer.appendChild(prevPage); |
| 142 | + paginationContainer.appendChild(paginationLabel); |
| 143 | + paginationContainer.appendChild(nextPage); |
84 | 144 |
|
85 |
| - // Initial setup |
86 |
| - updateButtonStates(); |
| 145 | + pageSizeContainer.appendChild(pageSizeLabel); |
| 146 | + pageSizeContainer.appendChild(pageSizeSelect); |
| 147 | + |
| 148 | + footer.appendChild(rowCountLabel); |
| 149 | + footer.appendChild(paginationContainer); |
| 150 | + footer.appendChild(pageSizeContainer); |
| 151 | + |
| 152 | + container.appendChild(tableContainer); |
| 153 | + container.appendChild(footer); |
87 | 154 |
|
88 |
| - buttonContainer.appendChild(prevPage); |
89 |
| - buttonContainer.appendChild(label); |
90 |
| - buttonContainer.appendChild(nextPage); |
91 | 155 | el.appendChild(container);
|
92 |
| - el.appendChild(buttonContainer); |
| 156 | + |
| 157 | + // Initial render |
| 158 | + handleTableHTMLChange(); |
93 | 159 | }
|
94 | 160 |
|
95 | 161 | export default { render };
|
0 commit comments