@@ -4,6 +4,20 @@ let config = {
44 cellsToVerify : [ ]
55} ;
66
7+ // Escape HTML to prevent XSS attacks
8+ function escapeHtml ( text ) {
9+ if ( text == null ) return '' ;
10+ const map = {
11+ '&' : '&' ,
12+ '<' : '<' ,
13+ '>' : '>' ,
14+ '"' : '"' ,
15+ "'" : ''' ,
16+ '/' : '/'
17+ } ;
18+ return String ( text ) . replace ( / [ & < > " ' / ] / g, s => map [ s ] ) ;
19+ }
20+
721// Load configuration on startup
822async function loadConfig ( ) {
923 try {
@@ -88,6 +102,7 @@ function updateStatus(message, type = 'info') {
88102// Handle spreadsheet URL change
89103function handleSpreadsheetURLChange ( event ) {
90104 config . spreadsheetURL = event . target . value ;
105+ render ( ) ;
91106 saveConfig ( ) ;
92107}
93108
@@ -112,13 +127,37 @@ function handleVerificationTypeChange(index, type) {
112127
113128// Add new cell verification
114129function addCellVerification ( ) {
130+ const newIndex = config . cellsToVerify . length ;
115131 config . cellsToVerify . push ( {
116132 cellName : '' ,
117133 expectedValue : '' ,
118134 expectedFunction : '' ,
119135 verificationType : 'value' // 'value' or 'function'
120136 } ) ;
121137 render ( ) ;
138+
139+ // Scroll the newly added cell into view, ensuring the "Add Cell" button is also visible
140+ setTimeout ( ( ) => {
141+ const cellItems = document . querySelectorAll ( '.cell-item' ) ;
142+ const configEditor = document . querySelector ( '.config-editor' ) ;
143+ if ( cellItems [ newIndex ] && configEditor ) {
144+ // Scroll the cell into view
145+ cellItems [ newIndex ] . scrollIntoView ( { behavior : 'smooth' , block : 'nearest' } ) ;
146+
147+ // Then scroll a bit more to ensure the "Add Cell" button is visible
148+ setTimeout ( ( ) => {
149+ const addButton = document . querySelector ( '.button-primary' ) ;
150+ if ( addButton ) {
151+ // Scroll the container a bit more to show the button
152+ const scrollAmount = addButton . getBoundingClientRect ( ) . bottom - configEditor . getBoundingClientRect ( ) . bottom + 20 ;
153+ if ( scrollAmount > 0 ) {
154+ configEditor . scrollBy ( { top : scrollAmount , behavior : 'smooth' } ) ;
155+ }
156+ }
157+ } , 300 ) ; // Wait for the first scroll to complete
158+ }
159+ } , 0 ) ;
160+
122161 saveConfig ( ) ;
123162}
124163
@@ -134,9 +173,147 @@ function render() {
134173 const app = document . getElementById ( 'app' ) ;
135174 if ( ! app ) return ;
136175
176+ // Convert Google Sheets URL to embeddable format
177+ function getEmbedUrl ( url ) {
178+ if ( ! url ) return '' ;
179+ // Extract spreadsheet ID from URL
180+ const match = url . match ( / \/ s p r e a d s h e e t s \/ d \/ ( [ a - z A - Z 0 - 9 - _ ] + ) / ) ;
181+ if ( match ) {
182+ const spreadsheetId = match [ 1 ] ;
183+ // Extract gid (sheet ID) if present
184+ const gidMatch = url . match ( / [ # & ] g i d = ( [ 0 - 9 ] + ) / ) ;
185+ const gid = gidMatch ? gidMatch [ 1 ] : '' ;
186+
187+ // Use edit endpoint which shows headers - clipboard access depends on Google's iframe policies
188+ let embedUrl = `https://docs.google.com/spreadsheets/d/${ spreadsheetId } /edit` ;
189+ const params = [ ] ;
190+ if ( gid ) {
191+ params . push ( `gid=${ gid } ` ) ;
192+ }
193+ // rm=minimal removes some UI but keeps headers visible
194+ params . push ( 'rm=minimal' ) ;
195+ if ( params . length > 0 ) {
196+ embedUrl += '?' + params . join ( '&' ) ;
197+ }
198+ return embedUrl ;
199+ }
200+ return url ;
201+ }
202+
203+ const spreadsheetUrl = config . spreadsheetURL || '' ;
204+ const embedUrl = getEmbedUrl ( spreadsheetUrl ) ;
205+
206+ // Check if URL changed - if not, we can preserve the iframe
207+ const existingIframe = document . getElementById ( 'spreadsheet-iframe' ) ;
208+ const urlChanged = ! existingIframe || existingIframe . src !== embedUrl ;
209+
210+ // If URL hasn't changed, preserve the iframe by updating only the cells section
211+ if ( ! urlChanged && existingIframe && spreadsheetUrl ) {
212+ // Only update the cells list, not the entire app
213+ const cellsList = document . getElementById ( 'cells-list' ) ;
214+ if ( cellsList ) {
215+ cellsList . innerHTML = `
216+ ${ config . cellsToVerify . map ( ( cell , index ) => {
217+ const verificationType = cell . verificationType || ( cell . expectedFunction ? 'function' : 'value' ) ;
218+ return `
219+ <div class="cell-item" style="border: 1px solid var(--Colors-Box-Stroke); border-radius: var(--UI-Radius-radius-s); padding: var(--UI-Spacing-spacing-l); margin-bottom: var(--UI-Spacing-spacing-m); background: var(--Colors-Box-Background);">
220+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--UI-Spacing-spacing-m);">
221+ <div style="display: flex; align-items: center; gap: var(--UI-Spacing-spacing-l);">
222+ <label class="input-radio input-radio-small" style="margin: 0;">
223+ <input
224+ type="radio"
225+ name="verification-type-${ index } "
226+ value="value"
227+ ${ verificationType === 'value' ? 'checked' : '' }
228+ onchange="handleVerificationTypeChange(${ index } , 'value')"
229+ />
230+ <span class="input-radio-circle">
231+ <span class="input-radio-dot"></span>
232+ </span>
233+ <span class="input-radio-label">Value</span>
234+ </label>
235+
236+ <label class="input-radio input-radio-small" style="margin: 0;">
237+ <input
238+ type="radio"
239+ name="verification-type-${ index } "
240+ value="function"
241+ ${ verificationType === 'function' ? 'checked' : '' }
242+ onchange="handleVerificationTypeChange(${ index } , 'function')"
243+ />
244+ <span class="input-radio-circle">
245+ <span class="input-radio-dot"></span>
246+ </span>
247+ <span class="input-radio-label">Function</span>
248+ </label>
249+ </div>
250+ <button class="button button-text" onclick="removeCellVerification(${ index } )" style="color: var(--Colors-Base-Accent-Red-600);">
251+ Remove
252+ </button>
253+ </div>
254+
255+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--UI-Spacing-spacing-m); margin-bottom: var(--UI-Spacing-spacing-m);">
256+ <div>
257+ <label style="display: block; margin-bottom: var(--UI-Spacing-spacing-xs); font-weight: 500; font-size: var(--Fonts-Body-Default-sm); color: var(--Colors-Text-Body-Medium);">
258+ Cell Name
259+ </label>
260+ <input
261+ type="text"
262+ class="input"
263+ style="width: 100%; box-sizing: border-box;"
264+ value="${ escapeHtml ( cell . cellName || '' ) } "
265+ placeholder="e.g., A1"
266+ onchange="handleCellChange(${ index } , 'cellName', event.target.value)"
267+ />
268+ </div>
269+
270+ ${ verificationType === 'value' ? `
271+ <div>
272+ <label style="display: block; margin-bottom: var(--UI-Spacing-spacing-xs); font-weight: 500; font-size: var(--Fonts-Body-Default-sm); color: var(--Colors-Text-Body-Medium);">
273+ Expected Value
274+ </label>
275+ <input
276+ type="text"
277+ class="input"
278+ style="width: 100%; box-sizing: border-box;"
279+ value="${ escapeHtml ( cell . expectedValue || '' ) } "
280+ placeholder="e.g., 10"
281+ onchange="handleCellChange(${ index } , 'expectedValue', event.target.value)"
282+ />
283+ </div>
284+ ` : `
285+ <div>
286+ <label style="display: block; margin-bottom: var(--UI-Spacing-spacing-xs); font-weight: 500; font-size: var(--Fonts-Body-Default-sm); color: var(--Colors-Text-Body-Medium);">
287+ Expected Function
288+ </label>
289+ <input
290+ type="text"
291+ class="input"
292+ style="width: 100%; box-sizing: border-box;"
293+ value="${ escapeHtml ( cell . expectedFunction || '' ) } "
294+ placeholder="e.g., =SUM(A1:A10)"
295+ onchange="handleCellChange(${ index } , 'expectedFunction', event.target.value)"
296+ />
297+ </div>
298+ ` }
299+ </div>
300+ </div>
301+ ` ;
302+ } ) . join ( '' ) }
303+
304+ ${ config . cellsToVerify . length === 0 ? `
305+ <div style="text-align: left; padding: var(--UI-Spacing-spacing-xl); color: var(--Colors-Text-Body-Light);">
306+ No cells configured. Click "Add Cell" below to get started.
307+ </div>
308+ ` : '' }
309+ ` ;
310+ return ; // Early return to avoid full re-render
311+ }
312+ }
313+
137314 app . innerHTML = `
138- <div class="config-editor ">
139- <div class="box box-elevated" style="margin-bottom: var(--UI-Spacing-spacing-xl); ">
315+ <div style="grid-column: 1 / -1; margin-bottom: var(--UI-Spacing-spacing-xl); ">
316+ <div class="box card ">
140317 <div style="width: 100%;">
141318 <label for="spreadsheet-url" class="label-medium" style="display: block; margin-bottom: var(--UI-Spacing-spacing-xs); color: var(--Colors-Text-Body-Medium); text-align: left;">
142319 Spreadsheet URL
@@ -146,16 +323,18 @@ function render() {
146323 id="spreadsheet-url"
147324 class="input"
148325 style="width: 100%; max-width: 100%; box-sizing: border-box;"
149- value="${ config . spreadsheetURL || '' } "
326+ value="${ escapeHtml ( spreadsheetUrl ) } "
150327 placeholder="https://docs.google.com/spreadsheets/d/..."
151328 onchange="handleSpreadsheetURLChange(event)"
152329 />
153330 </div>
154331 </div>
332+ </div>
155333
156- <div class="box box-elevated" style="display: flex; flex-direction: column; align-items: flex-start; ">
157- <h2 class="heading-medium" style="margin-top: 0; margin-bottom: var(--UI-Spacing-spacing-xl); color: var(--Colors-Text-Body-Strong); width: 100% ;">Cells to Verify</h2>
334+ <div class="config-editor ">
335+ <h2 class="heading-medium" style="margin-top: 0; margin-bottom: var(--UI-Spacing-spacing-xl); color: var(--Colors-Text-Body-Strong);">Cells to Verify</h2>
158336
337+ <div class="box card" style="display: flex; flex-direction: column; align-items: flex-start;">
159338 <div id="cells-list" style="width: 100%;">
160339 ${ config . cellsToVerify . map ( ( cell , index ) => {
161340 const verificationType = cell . verificationType || ( cell . expectedFunction ? 'function' : 'value' ) ;
@@ -205,7 +384,7 @@ function render() {
205384 type="text"
206385 class="input"
207386 style="width: 100%; box-sizing: border-box;"
208- value="${ cell . cellName || '' } "
387+ value="${ escapeHtml ( cell . cellName || '' ) } "
209388 placeholder="e.g., A1"
210389 onchange="handleCellChange(${ index } , 'cellName', event.target.value)"
211390 />
@@ -220,7 +399,7 @@ function render() {
220399 type="text"
221400 class="input"
222401 style="width: 100%; box-sizing: border-box;"
223- value="${ cell . expectedValue || '' } "
402+ value="${ escapeHtml ( cell . expectedValue || '' ) } "
224403 placeholder="e.g., 10"
225404 onchange="handleCellChange(${ index } , 'expectedValue', event.target.value)"
226405 />
@@ -234,7 +413,7 @@ function render() {
234413 type="text"
235414 class="input"
236415 style="width: 100%; box-sizing: border-box;"
237- value="${ cell . expectedFunction || '' } "
416+ value="${ escapeHtml ( cell . expectedFunction || '' ) } "
238417 placeholder="e.g., =SUM(A1:A10)"
239418 onchange="handleCellChange(${ index } , 'expectedFunction', event.target.value)"
240419 />
@@ -259,6 +438,26 @@ function render() {
259438 </div>
260439 </div>
261440 </div>
441+
442+ <div class="spreadsheet-preview">
443+ ${ spreadsheetUrl ? `
444+ <iframe
445+ id="spreadsheet-iframe"
446+ src="${ escapeHtml ( embedUrl ) } "
447+ frameborder="0"
448+ allowfullscreen
449+ allow="clipboard-read; clipboard-write"
450+ ></iframe>
451+ ` : `
452+ <div class="spreadsheet-preview-placeholder" style="flex: 1; min-height: 0;">
453+ <div>
454+ <p style="font-size: var(--Fonts-Body-Default-md); color: var(--Colors-Text-Body-Light);">
455+ Enter a spreadsheet URL to see a preview here.
456+ </p>
457+ </div>
458+ </div>
459+ ` }
460+ </div>
262461 ` ;
263462}
264463
0 commit comments