@@ -14,6 +14,15 @@ import {
1414 isValidTheme ,
1515 type ThemeMode ,
1616} from './theme' ;
17+ import {
18+ escapeHtml ,
19+ sanitizeCssColor ,
20+ sanitizeCssFontFamily ,
21+ sanitizeNonNegativeNumber ,
22+ sanitizePositiveInteger ,
23+ sanitizeShadowPreset ,
24+ sanitizeUrl ,
25+ } from './sanitize' ;
1726
1827type FeedbackCategory = 'bug' | 'feature' | 'question' ;
1928type CategoryLabelConfig = Partial < Record < FeedbackCategory , string | string [ ] > > ;
@@ -323,42 +332,68 @@ if (!document.currentScript) {
323332 '[BugDrop] document.currentScript is null — do not use async or defer on the BugDrop script tag.'
324333 ) ;
325334}
326- const rawTheme = script ?. dataset . theme as WidgetConfig [ 'theme' ] | undefined ;
335+ const rawTheme = script ?. dataset . theme ;
336+ if ( rawTheme && ! isValidTheme ( rawTheme ) ) {
337+ console . warn ( `[BugDrop] Invalid data-theme "${ rawTheme } ". Expected "light", "dark", or "auto".` ) ;
338+ }
339+ const requireName = script ?. dataset . requireName === 'true' ;
340+ const requireEmail = script ?. dataset . requireEmail === 'true' ;
341+ const rawPosition = script ?. dataset . position ;
342+ if ( rawPosition && rawPosition !== 'bottom-right' && rawPosition !== 'bottom-left' ) {
343+ console . warn (
344+ `[BugDrop] Invalid data-position "${ rawPosition } ". Expected "bottom-right" or "bottom-left".`
345+ ) ;
346+ }
347+ const rawDismissDuration = script ?. dataset . dismissDuration ;
348+ const dismissDuration = sanitizePositiveInteger ( rawDismissDuration ) ;
349+ if ( rawDismissDuration && dismissDuration === undefined ) {
350+ console . warn (
351+ '[BugDrop] Invalid data-dismiss-duration. Expected a positive whole number of days.'
352+ ) ;
353+ }
354+ const rawScreenshotScale = script ?. dataset . screenshotScale ;
355+ const screenshotScale = sanitizeNonNegativeNumber ( rawScreenshotScale ) ;
356+ if ( rawScreenshotScale && screenshotScale === undefined ) {
357+ console . warn ( '[BugDrop] Invalid data-screenshot-scale. Expected a non-negative number.' ) ;
358+ }
359+ const rawShadow = script ?. dataset . shadow ;
360+ const shadow = sanitizeShadowPreset ( rawShadow ) ;
361+ if ( rawShadow && ! shadow ) {
362+ console . warn ( '[BugDrop] Invalid data-shadow. Expected "soft", "hard", or "none".' ) ;
363+ }
327364const config : WidgetConfig = {
328365 repo : script ?. dataset . repo || '' ,
329366 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
367+ position : rawPosition === 'bottom-left' ? 'bottom-left' : 'bottom-right' ,
368+ theme : isValidTheme ( rawTheme ) ? rawTheme : 'auto' , // Default to auto-detection
332369 // 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' ,
370+ showName : script ?. dataset . showName === 'true' || requireName ,
371+ requireName,
372+ showEmail : script ?. dataset . showEmail === 'true' || requireEmail ,
373+ requireEmail,
337374 // Dismissible button configuration
338375 buttonDismissible : script ?. dataset . buttonDismissible === 'true' ,
339- dismissDuration : script ?. dataset . dismissDuration
340- ? parseInt ( script . dataset . dismissDuration , 10 )
341- : undefined ,
376+ dismissDuration,
342377 // Show restore pill after dismissing (default true when dismissible, unless explicitly false)
343378 showRestore : script ?. dataset . showRestore !== 'false' ,
344379 // Button visibility (default true, set to false for API-only mode)
345380 showButton : script ?. dataset . button !== 'false' ,
346381 // Custom accent color (e.g., "#FF6B35")
347- accentColor : script ?. dataset . color || undefined ,
382+ accentColor : sanitizeCssColor ( script ?. dataset . color ) ,
348383 // Custom icon URL (or 'none' to hide)
349- iconUrl : script ?. dataset . icon || undefined ,
384+ iconUrl : sanitizeUrl ( script ?. dataset . icon ) ,
350385 // Custom trigger label
351386 label : script ?. dataset . label || undefined ,
352387 categoryLabels : parseCategoryLabels ( script ?. dataset . categoryLabels ) ,
353388 // 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 ,
389+ font : sanitizeCssFontFamily ( script ?. dataset . font ) ,
390+ radius : sanitizeNonNegativeNumber ( script ?. dataset . radius ) ?. toString ( ) ,
391+ bgColor : sanitizeCssColor ( script ?. dataset . bg ) ,
392+ textColor : sanitizeCssColor ( script ?. dataset . text ) ,
358393 // Tier 2 styling customization
359- borderWidth : script ?. dataset . borderWidth || undefined ,
360- borderColor : script ?. dataset . borderColor || undefined ,
361- shadow : script ?. dataset . shadow || undefined ,
394+ borderWidth : sanitizeNonNegativeNumber ( script ?. dataset . borderWidth ) ?. toString ( ) ,
395+ borderColor : sanitizeCssColor ( script ?. dataset . borderColor ) ,
396+ shadow,
362397 // Welcome screen behavior (default: 'once')
363398 welcome : ( ( ) => {
364399 const val = script ?. dataset . welcome ;
@@ -377,9 +412,7 @@ const config: WidgetConfig = {
377412 }
378413 return 'optional' as const ;
379414 } ) ( ) ,
380- screenshotScale : script ?. dataset . screenshotScale
381- ? parseFloat ( script . dataset . screenshotScale )
382- : undefined ,
415+ screenshotScale,
383416} ;
384417
385418// Validate config
@@ -393,22 +426,41 @@ if (!config.repo) {
393426 initWidget ( config ) ;
394427}
395428
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-
407429// Build the trigger button label text
408430function getTriggerLabel ( config : WidgetConfig ) : string {
409431 return config . label !== undefined ? config . label : 'Feedback' ;
410432}
411433
434+ function appendTriggerContent ( trigger : HTMLElement , config : WidgetConfig ) : void {
435+ if ( config . iconUrl !== 'none' ) {
436+ const icon = document . createElement ( 'span' ) ;
437+ icon . className = 'bd-trigger-icon' ;
438+
439+ if ( config . iconUrl ) {
440+ const image = document . createElement ( 'img' ) ;
441+ image . src = config . iconUrl ;
442+ image . alt = '' ;
443+ const fallback = document . createElement ( 'span' ) ;
444+ fallback . textContent = '🐛' ;
445+ fallback . style . display = 'none' ;
446+ image . addEventListener ( 'error' , ( ) => {
447+ image . style . display = 'none' ;
448+ fallback . style . display = '' ;
449+ } ) ;
450+ icon . append ( image , fallback ) ;
451+ } else {
452+ icon . textContent = '🐛' ;
453+ }
454+
455+ trigger . appendChild ( icon ) ;
456+ }
457+
458+ const label = document . createElement ( 'span' ) ;
459+ label . className = 'bd-trigger-label' ;
460+ label . textContent = getTriggerLabel ( config ) ;
461+ trigger . appendChild ( label ) ;
462+ }
463+
412464// Create the pull tab shown after dismissing the button
413465function createPullTab ( root : HTMLElement , config : WidgetConfig ) : HTMLElement {
414466 const tab = document . createElement ( 'div' ) ;
@@ -490,15 +542,14 @@ function initWidget(config: WidgetConfig) {
490542 if ( shouldShowButton ) {
491543 const trigger = document . createElement ( 'button' ) ;
492544 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>` ;
545+ appendTriggerContent ( trigger , config ) ;
495546 trigger . setAttribute ( 'aria-label' , 'Report a bug or send feedback' ) ;
496547
497548 // Add close button if dismissible
498549 if ( config . buttonDismissible ) {
499550 const closeBtn = document . createElement ( 'button' ) ;
500551 closeBtn . className = 'bd-trigger-close' ;
501- closeBtn . innerHTML = '×' ;
552+ closeBtn . textContent = '×' ;
502553 closeBtn . setAttribute ( 'aria-label' , 'Dismiss feedback button' ) ;
503554 trigger . appendChild ( closeBtn ) ;
504555
@@ -651,14 +702,13 @@ function exposeBugDropAPI(root: HTMLElement, config: WidgetConfig) {
651702function createTriggerButton ( root : HTMLElement , config : WidgetConfig , isRestoring = false ) {
652703 const trigger = document . createElement ( 'button' ) ;
653704 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>` ;
705+ appendTriggerContent ( trigger , config ) ;
656706 trigger . setAttribute ( 'aria-label' , 'Report a bug or send feedback' ) ;
657707
658708 if ( config . buttonDismissible ) {
659709 const closeBtn = document . createElement ( 'button' ) ;
660710 closeBtn . className = 'bd-trigger-close' ;
661- closeBtn . innerHTML = '×' ;
711+ closeBtn . textContent = '×' ;
662712 closeBtn . setAttribute ( 'aria-label' , 'Dismiss feedback button' ) ;
663713 trigger . appendChild ( closeBtn ) ;
664714
@@ -1060,15 +1110,6 @@ function getCategoryChecked(
10601110 return ( initialValues ?. category || 'bug' ) === category ? 'checked' : '' ;
10611111}
10621112
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-
10721113async function submitFeedback ( root : HTMLElement , config : WidgetConfig , data : FeedbackData ) {
10731114 // Show submitting modal with loading state
10741115 const modal = createModal (
0 commit comments