@@ -14,6 +14,16 @@ import {
1414 isValidTheme ,
1515 type ThemeMode ,
1616} from './theme' ;
17+ import {
18+ escapeHtml ,
19+ sanitizeCssColor ,
20+ sanitizeCssFontFamily ,
21+ sanitizeNonNegativeNumber ,
22+ sanitizeNonNegativePixelValue ,
23+ sanitizePositiveInteger ,
24+ sanitizeShadowPreset ,
25+ sanitizeUrl ,
26+ } from './sanitize' ;
1727
1828type FeedbackCategory = 'bug' | 'feature' | 'question' ;
1929type CategoryLabelConfig = Partial < Record < FeedbackCategory , string | string [ ] > > ;
@@ -323,42 +333,68 @@ if (!document.currentScript) {
323333 '[BugDrop] document.currentScript is null — do not use async or defer on the BugDrop script tag.'
324334 ) ;
325335}
326- const rawTheme = script ?. dataset . theme as WidgetConfig [ 'theme' ] | undefined ;
336+ const rawTheme = script ?. dataset . theme ;
337+ if ( rawTheme && ! isValidTheme ( rawTheme ) ) {
338+ console . warn ( `[BugDrop] Invalid data-theme "${ rawTheme } ". Expected "light", "dark", or "auto".` ) ;
339+ }
340+ const requireName = script ?. dataset . requireName === 'true' ;
341+ const requireEmail = script ?. dataset . requireEmail === 'true' ;
342+ const rawPosition = script ?. dataset . position ;
343+ if ( rawPosition && rawPosition !== 'bottom-right' && rawPosition !== 'bottom-left' ) {
344+ console . warn (
345+ `[BugDrop] Invalid data-position "${ rawPosition } ". Expected "bottom-right" or "bottom-left".`
346+ ) ;
347+ }
348+ const rawDismissDuration = script ?. dataset . dismissDuration ;
349+ const dismissDuration = sanitizePositiveInteger ( rawDismissDuration ) ;
350+ if ( rawDismissDuration && dismissDuration === undefined ) {
351+ console . warn (
352+ '[BugDrop] Invalid data-dismiss-duration. Expected a positive whole number of days.'
353+ ) ;
354+ }
355+ const rawScreenshotScale = script ?. dataset . screenshotScale ;
356+ const screenshotScale = sanitizeNonNegativeNumber ( rawScreenshotScale ) ;
357+ if ( rawScreenshotScale && screenshotScale === undefined ) {
358+ console . warn ( '[BugDrop] Invalid data-screenshot-scale. Expected a non-negative number.' ) ;
359+ }
360+ const rawShadow = script ?. dataset . shadow ;
361+ const shadow = sanitizeShadowPreset ( rawShadow ) ;
362+ if ( rawShadow && ! shadow ) {
363+ console . warn ( '[BugDrop] Invalid data-shadow. Expected "soft", "hard", or "none".' ) ;
364+ }
327365const config : WidgetConfig = {
328366 repo : script ?. dataset . repo || '' ,
329367 apiUrl : script ?. src . replace ( / \/ w i d g e t (?: \. v [ \d . ] + ) ? \. j s $ / , '/api' ) || '' ,
330- position : ( script ?. dataset . position as WidgetConfig [ 'position' ] ) || 'bottom-right' ,
331- theme : rawTheme || 'auto' , // Default to auto-detection
368+ position : rawPosition === 'bottom-left' ? 'bottom-left' : 'bottom-right' ,
369+ theme : isValidTheme ( rawTheme ) ? rawTheme : 'auto' , // Default to auto-detection
332370 // Name/email field configuration (all default to false for backwards compatibility)
333- showName : script ?. dataset . showName === 'true' ,
334- requireName : script ?. dataset . requireName === 'true' ,
335- showEmail : script ?. dataset . showEmail === 'true' ,
336- requireEmail : script ?. dataset . requireEmail === 'true' ,
371+ showName : script ?. dataset . showName === 'true' || requireName ,
372+ requireName,
373+ showEmail : script ?. dataset . showEmail === 'true' || requireEmail ,
374+ requireEmail,
337375 // Dismissible button configuration
338376 buttonDismissible : script ?. dataset . buttonDismissible === 'true' ,
339- dismissDuration : script ?. dataset . dismissDuration
340- ? parseInt ( script . dataset . dismissDuration , 10 )
341- : undefined ,
377+ dismissDuration,
342378 // Show restore pill after dismissing (default true when dismissible, unless explicitly false)
343379 showRestore : script ?. dataset . showRestore !== 'false' ,
344380 // Button visibility (default true, set to false for API-only mode)
345381 showButton : script ?. dataset . button !== 'false' ,
346382 // Custom accent color (e.g., "#FF6B35")
347- accentColor : script ?. dataset . color || undefined ,
383+ accentColor : sanitizeCssColor ( script ?. dataset . color ) ,
348384 // Custom icon URL (or 'none' to hide)
349- iconUrl : script ?. dataset . icon || undefined ,
385+ iconUrl : sanitizeUrl ( script ?. dataset . icon ) ,
350386 // Custom trigger label
351387 label : script ?. dataset . label || undefined ,
352388 categoryLabels : parseCategoryLabels ( script ?. dataset . categoryLabels ) ,
353389 // Tier 1 styling customization
354- font : script ?. dataset . font || undefined ,
355- radius : script ?. dataset . radius || undefined ,
356- bgColor : script ?. dataset . bg || undefined ,
357- textColor : script ?. dataset . text || undefined ,
390+ font : sanitizeCssFontFamily ( script ?. dataset . font ) ,
391+ radius : sanitizeNonNegativePixelValue ( script ?. dataset . radius ) ?. toString ( ) ,
392+ bgColor : sanitizeCssColor ( script ?. dataset . bg ) ,
393+ textColor : sanitizeCssColor ( script ?. dataset . text ) ,
358394 // Tier 2 styling customization
359- borderWidth : script ?. dataset . borderWidth || undefined ,
360- borderColor : script ?. dataset . borderColor || undefined ,
361- shadow : script ?. dataset . shadow || undefined ,
395+ borderWidth : sanitizeNonNegativePixelValue ( script ?. dataset . borderWidth ) ?. toString ( ) ,
396+ borderColor : sanitizeCssColor ( script ?. dataset . borderColor ) ,
397+ shadow,
362398 // Welcome screen behavior (default: 'once')
363399 welcome : ( ( ) => {
364400 const val = script ?. dataset . welcome ;
@@ -377,9 +413,7 @@ const config: WidgetConfig = {
377413 }
378414 return 'optional' as const ;
379415 } ) ( ) ,
380- screenshotScale : script ?. dataset . screenshotScale
381- ? parseFloat ( script . dataset . screenshotScale )
382- : undefined ,
416+ screenshotScale,
383417} ;
384418
385419// Validate config
@@ -393,22 +427,41 @@ if (!config.repo) {
393427 initWidget ( config ) ;
394428}
395429
396- // Build the trigger button icon HTML - custom image with emoji fallback, 'none' to hide, or default emoji
397- function getTriggerIconHtml ( config : WidgetConfig ) : string {
398- if ( config . iconUrl === 'none' ) {
399- return '' ;
400- }
401- if ( config . iconUrl ) {
402- return `<img src="${ config . iconUrl } " alt="" onerror="this.style.display='none';this.nextSibling.style.display=''"><span style="display:none">🐛</span>` ;
403- }
404- return '🐛' ;
405- }
406-
407430// Build the trigger button label text
408431function getTriggerLabel ( config : WidgetConfig ) : string {
409432 return config . label !== undefined ? config . label : 'Feedback' ;
410433}
411434
435+ function appendTriggerContent ( trigger : HTMLElement , config : WidgetConfig ) : void {
436+ if ( config . iconUrl !== 'none' ) {
437+ const icon = document . createElement ( 'span' ) ;
438+ icon . className = 'bd-trigger-icon' ;
439+
440+ if ( config . iconUrl ) {
441+ const image = document . createElement ( 'img' ) ;
442+ image . src = config . iconUrl ;
443+ image . alt = '' ;
444+ const fallback = document . createElement ( 'span' ) ;
445+ fallback . textContent = '🐛' ;
446+ fallback . style . display = 'none' ;
447+ image . addEventListener ( 'error' , ( ) => {
448+ image . style . display = 'none' ;
449+ fallback . style . display = '' ;
450+ } ) ;
451+ icon . append ( image , fallback ) ;
452+ } else {
453+ icon . textContent = '🐛' ;
454+ }
455+
456+ trigger . appendChild ( icon ) ;
457+ }
458+
459+ const label = document . createElement ( 'span' ) ;
460+ label . className = 'bd-trigger-label' ;
461+ label . textContent = getTriggerLabel ( config ) ;
462+ trigger . appendChild ( label ) ;
463+ }
464+
412465// Create the pull tab shown after dismissing the button
413466function createPullTab ( root : HTMLElement , config : WidgetConfig ) : HTMLElement {
414467 const tab = document . createElement ( 'div' ) ;
@@ -490,15 +543,14 @@ function initWidget(config: WidgetConfig) {
490543 if ( shouldShowButton ) {
491544 const trigger = document . createElement ( 'button' ) ;
492545 trigger . className = 'bd-trigger' ;
493- const iconHtml = getTriggerIconHtml ( config ) ;
494- trigger . innerHTML = `${ iconHtml ? `<span class="bd-trigger-icon">${ iconHtml } </span>` : '' } <span class="bd-trigger-label">${ getTriggerLabel ( config ) } </span>` ;
546+ appendTriggerContent ( trigger , config ) ;
495547 trigger . setAttribute ( 'aria-label' , 'Report a bug or send feedback' ) ;
496548
497549 // Add close button if dismissible
498550 if ( config . buttonDismissible ) {
499551 const closeBtn = document . createElement ( 'button' ) ;
500552 closeBtn . className = 'bd-trigger-close' ;
501- closeBtn . innerHTML = '×' ;
553+ closeBtn . textContent = '×' ;
502554 closeBtn . setAttribute ( 'aria-label' , 'Dismiss feedback button' ) ;
503555 trigger . appendChild ( closeBtn ) ;
504556
@@ -651,14 +703,13 @@ function exposeBugDropAPI(root: HTMLElement, config: WidgetConfig) {
651703function createTriggerButton ( root : HTMLElement , config : WidgetConfig , isRestoring = false ) {
652704 const trigger = document . createElement ( 'button' ) ;
653705 trigger . className = isRestoring ? 'bd-trigger bd-trigger--restoring' : 'bd-trigger' ;
654- const iconHtml = getTriggerIconHtml ( config ) ;
655- trigger . innerHTML = `${ iconHtml ? `<span class="bd-trigger-icon">${ iconHtml } </span>` : '' } <span class="bd-trigger-label">${ getTriggerLabel ( config ) } </span>` ;
706+ appendTriggerContent ( trigger , config ) ;
656707 trigger . setAttribute ( 'aria-label' , 'Report a bug or send feedback' ) ;
657708
658709 if ( config . buttonDismissible ) {
659710 const closeBtn = document . createElement ( 'button' ) ;
660711 closeBtn . className = 'bd-trigger-close' ;
661- closeBtn . innerHTML = '×' ;
712+ closeBtn . textContent = '×' ;
662713 closeBtn . setAttribute ( 'aria-label' , 'Dismiss feedback button' ) ;
663714 trigger . appendChild ( closeBtn ) ;
664715
@@ -1060,15 +1111,6 @@ function getCategoryChecked(
10601111 return ( initialValues ?. category || 'bug' ) === category ? 'checked' : '' ;
10611112}
10621113
1063- function escapeHtml ( value : string ) : string {
1064- return value
1065- . replace ( / & / g, '&' )
1066- . replace ( / < / g, '<' )
1067- . replace ( / > / g, '>' )
1068- . replace ( / " / g, '"' )
1069- . replace ( / ' / g, ''' ) ;
1070- }
1071-
10721114async function submitFeedback ( root : HTMLElement , config : WidgetConfig , data : FeedbackData ) {
10731115 // Show submitting modal with loading state
10741116 const modal = createModal (
0 commit comments