1- /**
2- * Copyright 2025 Google LLC
1+ /*
2+ * Copyright 2024 Google LLC
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
@@ -26,24 +26,22 @@ const ModelProperty = {
2626} ;
2727
2828const Event = {
29- CHANGE : "change" ,
30- CHANGE_TABLE_HTML : `change:${ ModelProperty . TABLE_HTML } ` ,
3129 CLICK : "click" ,
30+ CHANGE : "change" ,
31+ CHANGE_TABLE_HTML : "change:table_html" ,
3232} ;
3333
3434/**
3535 * Renders the interactive table widget.
36- * @param {{
37- * model: any,
38- * el: HTMLElement
39- * }} options
36+ * @param {{ model: any, el: HTMLElement } } props - The widget properties.
37+ * @param {Document } doc - The document object to use for creating elements.
4038 */
41- function render ( { model, el } ) {
39+ function render ( { model, el } , doc ) {
4240 // Main container with a unique class for CSS scoping
4341 el . classList . add ( "bigframes-widget" ) ;
4442
4543 // Add error message container at the top
46- const errorContainer = document . createElement ( "div" ) ;
44+ const errorContainer = doc . createElement ( "div" ) ;
4745 errorContainer . classList . add ( "error-message" ) ;
4846 errorContainer . style . display = "none" ;
4947 errorContainer . style . color = "red" ;
@@ -53,94 +51,95 @@ function render({ model, el }) {
5351 errorContainer . style . borderRadius = "4px" ;
5452 errorContainer . style . backgroundColor = "#ffebee" ;
5553
56- const tableContainer = document . createElement ( "div" ) ;
57- const footer = document . createElement ( "div" ) ;
58-
59- // Footer: Total rows label
60- const rowCountLabel = document . createElement ( "div" ) ;
61-
62- // Footer: Pagination controls
63- const paginationContainer = document . createElement ( "div" ) ;
64- const prevPage = document . createElement ( "button" ) ;
65- const paginationLabel = document . createElement ( "span" ) ;
66- const nextPage = document . createElement ( "button" ) ;
67-
68- // Footer: Page size controls
69- const pageSizeContainer = document . createElement ( "div" ) ;
70- const pageSizeLabel = document . createElement ( "label" ) ;
71- const pageSizeSelect = document . createElement ( "select" ) ;
72-
73- // Add CSS classes
74- tableContainer . classList . add ( "table-container" ) ;
75- footer . classList . add ( "footer" ) ;
76- paginationContainer . classList . add ( "pagination" ) ;
77- pageSizeContainer . classList . add ( "page-size" ) ;
78-
79- // Configure pagination buttons
80- prevPage . type = "button" ;
81- nextPage . type = "button" ;
82- prevPage . textContent = "Prev" ;
83- nextPage . textContent = "Next" ;
84-
85- // Configure page size selector
86- pageSizeLabel . textContent = "Page Size" ;
87- for ( const size of [ 10 , 25 , 50 , 100 ] ) {
88- const option = document . createElement ( "option" ) ;
54+ const tableContainer = doc . createElement ( "div" ) ;
55+ const footer = doc . createElement ( "div" ) ;
56+
57+ // Footer styles
58+ footer . style . display = "flex" ;
59+ footer . style . justifyContent = "space-between" ;
60+ footer . style . alignItems = "center" ;
61+ footer . style . padding = "8px" ;
62+ footer . style . fontFamily =
63+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' ;
64+
65+ // Pagination controls
66+ const paginationContainer = doc . createElement ( "div" ) ;
67+ const prevPage = doc . createElement ( "button" ) ;
68+ const pageIndicator = doc . createElement ( "span" ) ;
69+ const nextPage = doc . createElement ( "button" ) ;
70+ const rowCountLabel = doc . createElement ( "span" ) ;
71+
72+ // Page size controls
73+ const pageSizeContainer = doc . createElement ( "div" ) ;
74+ const pageSizeLabel = doc . createElement ( "label" ) ;
75+ const pageSizeInput = doc . createElement ( "select" ) ;
76+
77+ prevPage . textContent = "<" ;
78+ nextPage . textContent = ">" ;
79+ pageSizeLabel . textContent = "Page size:" ;
80+ pageSizeLabel . style . marginRight = "8px" ;
81+ pageIndicator . style . margin = "0 8px" ;
82+ rowCountLabel . style . margin = "0 8px" ;
83+
84+ // Page size options
85+ const pageSizes = [ 10 , 20 , 50 , 100 , 200 , 500 , 1000 ] ;
86+ for ( const size of pageSizes ) {
87+ const option = doc . createElement ( "option" ) ;
8988 option . value = size ;
9089 option . textContent = size ;
9190 if ( size === model . get ( ModelProperty . PAGE_SIZE ) ) {
9291 option . selected = true ;
9392 }
94- pageSizeSelect . appendChild ( option ) ;
93+ pageSizeInput . appendChild ( option ) ;
9594 }
9695
9796 /** Updates the footer states and page label based on the model. */
9897 function updateButtonStates ( ) {
99- const rowCount = model . get ( ModelProperty . ROW_COUNT ) ;
100- const pageSize = model . get ( ModelProperty . PAGE_SIZE ) ;
10198 const currentPage = model . get ( ModelProperty . PAGE ) ;
99+ const pageSize = model . get ( ModelProperty . PAGE_SIZE ) ;
100+ const rowCount = model . get ( ModelProperty . ROW_COUNT ) ;
102101
103102 if ( rowCount === null ) {
104103 // Unknown total rows
105104 rowCountLabel . textContent = "Total rows unknown" ;
106- paginationLabel . textContent = `Page ${ ( currentPage + 1 ) . toLocaleString ( ) } of many` ;
105+ pageIndicator . textContent = `Page ${ ( currentPage + 1 ) . toLocaleString ( ) } of many` ;
107106 prevPage . disabled = currentPage === 0 ;
108107 nextPage . disabled = false ; // Allow navigation until we hit the end
109108 } else {
110109 // Known total rows
111110 const totalPages = Math . ceil ( rowCount / pageSize ) ;
112111 rowCountLabel . textContent = `${ rowCount . toLocaleString ( ) } total rows` ;
113- paginationLabel . textContent = `Page ${ ( currentPage + 1 ) . toLocaleString ( ) } of ${ rowCount . toLocaleString ( ) } ` ;
112+ pageIndicator . textContent = `Page ${ ( currentPage + 1 ) . toLocaleString ( ) } of ${ totalPages . toLocaleString ( ) } ` ;
114113 prevPage . disabled = currentPage === 0 ;
115114 nextPage . disabled = currentPage >= totalPages - 1 ;
116115 }
117- pageSizeSelect . value = pageSize ;
116+ pageSizeInput . value = pageSize ;
118117 }
119118
120119 /**
121- * Increments or decrements the page in the model .
122- * @param {number } direction - `1` for next, `-1` for previous .
120+ * Handles page navigation .
121+ * @param {number } direction - The direction to navigate (-1 for previous, 1 for next) .
123122 */
124123 function handlePageChange ( direction ) {
125- const current = model . get ( ModelProperty . PAGE ) ;
126- const next = current + direction ;
127- model . set ( ModelProperty . PAGE , next ) ;
124+ const currentPage = model . get ( ModelProperty . PAGE ) ;
125+ model . set ( ModelProperty . PAGE , currentPage + direction ) ;
128126 model . save_changes ( ) ;
129127 }
130128
131129 /**
132- * Handles changes to the page size from the dropdown .
133- * @param {number } size - The new page size.
130+ * Handles page size changes .
131+ * @param {number } newSize - The new page size.
134132 */
135- function handlePageSizeChange ( size ) {
136- const currentSize = model . get ( ModelProperty . PAGE_SIZE ) ;
137- if ( size !== currentSize ) {
138- model . set ( ModelProperty . PAGE_SIZE , size ) ;
139- model . save_changes ( ) ;
140- }
133+ function handlePageSizeChange ( newSize ) {
134+ model . set ( ModelProperty . PAGE_SIZE , newSize ) ;
135+ model . set ( ModelProperty . PAGE , 0 ) ; // Reset to first page
136+ model . save_changes ( ) ;
141137 }
142138
139+ /** Updates the HTML in the table container and refreshes button states. */
143140 function handleTableHTMLChange ( ) {
141+ // Note: Using innerHTML is safe here because the content is generated
142+ // by a trusted backend (DataFrame.to_html).
144143 tableContainer . innerHTML = model . get ( ModelProperty . TABLE_HTML ) ;
145144
146145 // Get sortable columns from backend
@@ -159,7 +158,7 @@ function render({ model, el }) {
159158 header . style . cursor = "pointer" ;
160159
161160 // Create a span for the indicator
162- const indicatorSpan = document . createElement ( "span" ) ;
161+ const indicatorSpan = doc . createElement ( "span" ) ;
163162 indicatorSpan . classList . add ( "sort-indicator" ) ;
164163 indicatorSpan . style . paddingLeft = "5px" ;
165164
@@ -230,7 +229,7 @@ function render({ model, el }) {
230229 // Add event listeners
231230 prevPage . addEventListener ( Event . CLICK , ( ) => handlePageChange ( - 1 ) ) ;
232231 nextPage . addEventListener ( Event . CLICK , ( ) => handlePageChange ( 1 ) ) ;
233- pageSizeSelect . addEventListener ( Event . CHANGE , ( e ) => {
232+ pageSizeInput . addEventListener ( Event . CHANGE , ( e ) => {
234233 const newSize = Number ( e . target . value ) ;
235234 if ( newSize ) {
236235 handlePageSizeChange ( newSize ) ;
@@ -244,14 +243,15 @@ function render({ model, el }) {
244243 updateButtonStates ( ) ;
245244 }
246245 } ) ;
246+ model . on ( `change:${ ModelProperty . PAGE } ` , updateButtonStates ) ;
247247
248248 // Assemble the DOM
249249 paginationContainer . appendChild ( prevPage ) ;
250- paginationContainer . appendChild ( paginationLabel ) ;
250+ paginationContainer . appendChild ( pageIndicator ) ;
251251 paginationContainer . appendChild ( nextPage ) ;
252252
253253 pageSizeContainer . appendChild ( pageSizeLabel ) ;
254- pageSizeContainer . appendChild ( pageSizeSelect ) ;
254+ pageSizeContainer . appendChild ( pageSizeInput ) ;
255255
256256 footer . appendChild ( rowCountLabel ) ;
257257 footer . appendChild ( paginationContainer ) ;
0 commit comments