@@ -16,23 +16,27 @@ import {
1616 SUBMIT_BUTTON_LABEL ,
1717 SUCCESS_MESSAGE_TEXT ,
1818} from '../constants' ;
19- import type { DialogComponent } from '../modal/components/Dialog' ;
2019import type { IFeedback2ModalIntegration } from '../modal/integration' ;
2120import type { IFeedback2ScreenshotIntegration } from '../screenshot/integration' ;
22- import type { FeedbackInternalOptions , OptionalFeedbackConfiguration } from '../types' ;
21+ import type {
22+ Dialog ,
23+ FeedbackInternalOptions ,
24+ OptionalFeedbackConfiguration ,
25+ OverrideFeedbackConfiguration ,
26+ } from '../types' ;
2327import { DEBUG_BUILD } from '../util/debug-build' ;
2428import { mergeOptions } from '../util/mergeOptions' ;
2529import { Actor } from './components/Actor' ;
2630import { createMainStyles } from './createMainStyles' ;
2731import { sendFeedback } from './sendFeedback' ;
2832
33+ type Unsubscribe = ( ) => void ;
34+
2935interface PublicFeedback2Integration {
36+ attachTo : ( el : Element | string , optionOverrides : OverrideFeedbackConfiguration ) => ( ) => void ;
37+ createWidget : ( optionOverrides : OverrideFeedbackConfiguration & { shouldCreateActor ?: boolean } ) => Promise < Dialog > ;
38+ getWidget : ( ) => Dialog | null ;
3039 remove : ( ) => void ;
31- attachTo : ( el : Element | string , optionOverrides : OptionalFeedbackConfiguration ) => ( ) => void ;
32- createWidget : (
33- optionOverrides : OptionalFeedbackConfiguration & { shouldCreateActor ?: boolean } ,
34- ) => Promise < DialogComponent > ;
35- getWidget : ( ) => DialogComponent | null ;
3640 openDialog : ( ) => void ;
3741 closeDialog : ( ) => void ;
3842 removeWidget : ( ) => void ;
@@ -119,72 +123,33 @@ export const _feedback2Integration = (({
119123 onFormSubmitted,
120124 } ;
121125
122- let _host : HTMLElement | null = null ;
123126 let _shadow : ShadowRoot | null = null ;
124- let _dialog : DialogComponent | null = null ;
125-
126- /**
127- * Get the dom root, where DOM nodes will be appended into
128- */
129- const _getHost = ( options : FeedbackInternalOptions ) : HTMLElement => {
130- if ( ! _host ) {
131- const { id, colorScheme } = options ;
132-
133- const host = DOCUMENT . createElement ( 'div' ) ;
134- _host = host ;
135- host . id = String ( id ) ;
136- host . dataset . sentryFeedbackColorscheme = colorScheme ;
137- DOCUMENT . body . appendChild ( _host ) ;
138- }
139- return _host ;
140- } ;
127+ let _subscriptions : Unsubscribe [ ] = [ ] ;
141128
142129 /**
143130 * Get the shadow root where we will append css
144131 */
145- const _getShadow = ( options : FeedbackInternalOptions ) : ShadowRoot => {
132+ const _createShadow = ( options : FeedbackInternalOptions ) : ShadowRoot => {
146133 if ( ! _shadow ) {
147- const host = _getHost ( options ) ;
134+ const host = DOCUMENT . createElement ( 'div' ) ;
135+ host . id = String ( options . id ) ;
136+ DOCUMENT . body . appendChild ( host ) ;
148137
149- const { colorScheme, themeDark, themeLight } = options ;
150- const shadow = host . attachShadow ( { mode : 'open' } ) ;
151- shadow . appendChild (
152- // TODO: inject main styles as part of actor and dialog styles
153- // therefore each render root can have it's own theme
154- // err, everything can just have it's own shadowroot...
155- createMainStyles ( colorScheme , {
156- themeDark,
157- themeLight,
158- } ) ,
159- ) ;
160- _shadow = shadow ;
138+ _shadow = host . attachShadow ( { mode : 'open' } ) ;
139+ _shadow . appendChild ( createMainStyles ( options . colorScheme , options ) ) ;
161140 }
162-
163- return _shadow ;
141+ return _shadow as ShadowRoot ;
164142 } ;
165143
166- const _loadAndRenderDialog = async ( options : FeedbackInternalOptions ) : Promise < DialogComponent > => {
167- if ( _dialog ) {
168- return _dialog ;
169- }
170-
144+ const _loadAndRenderDialog = async ( options : FeedbackInternalOptions ) : Promise < Dialog > => {
171145 const client = getClient ( ) ; // TODO: getClient<BrowserClient>()
172146 if ( ! client ) {
173147 throw new Error ( 'Sentry Client is not initialized correctly' ) ;
174148 }
175149 const modalIntegration = client . getIntegrationByName < IFeedback2ModalIntegration > ( 'Feedback2Modal' ) ;
176150 const screenshotIntegration = client . getIntegrationByName < IFeedback2ScreenshotIntegration > ( 'Feedback2Screenshot' ) ;
177151
178- // Disable this because the site could have multiple feedback buttons, not all of them need to have screenshots enabled.
179- // Must be a better way...
180- //
181- // if (showScreenshot === false && screenshotIntegration) {
182- // // Warn the user that they loaded too much and explicitly asked for screen shots to be off
183- // console.log('WARNING: Feedback2Screenshot is bundled but not rendered.'); // eslint-disable-line no-console
184- // }
185-
186152 // START TEMP: Error messages
187- console . log ( 'ensureRenderer:' , { modalIntegration, showScreenshot, screenshotIntegration } ) ; // eslint-disable-line no-console
188153 if ( ! modalIntegration && showScreenshot && ! screenshotIntegration ) {
189154 throw new Error ( 'Async loading of Feedback Modal & Screenshot integrations is not yet implemented' ) ;
190155 } else if ( ! modalIntegration ) {
@@ -203,21 +168,16 @@ export const _feedback2Integration = (({
203168 throw new Error ( 'Not implemented yet' ) ;
204169 }
205170
206- const dialog = modalIntegration . createDialog ( {
207- shadow : _getShadow ( options ) ,
208- sendFeedback,
171+ return modalIntegration . createDialog ( {
209172 options,
210- onDone : ( ) => {
211- _dialog = null ;
212- } ,
213173 screenshotIntegration,
174+ sendFeedback,
175+ shadow : _createShadow ( options ) ,
214176 } ) ;
215- _dialog = dialog ;
216- return dialog ;
217177 } ;
218178
219- const attachTo = ( el : Element | string , optionOverrides : OptionalFeedbackConfiguration = { } ) : ( ( ) => void ) => {
220- const options = mergeOptions ( _options , optionOverrides ) ;
179+ const attachTo = ( el : Element | string , optionOverrides : OverrideFeedbackConfiguration = { } ) : Unsubscribe => {
180+ const mergedOptions = mergeOptions ( _options , optionOverrides ) ;
221181
222182 const targetEl =
223183 typeof el === 'string' ? DOCUMENT . querySelector ( el ) : typeof el . addEventListener === 'function' ? el : null ;
@@ -227,14 +187,52 @@ export const _feedback2Integration = (({
227187 throw new Error ( 'Unable to attach to target element' ) ;
228188 }
229189
190+ let dialog : Dialog | null = null ;
230191 const handleClick = async ( ) : Promise < void > => {
231- const dialog = await _loadAndRenderDialog ( options ) ;
192+ if ( ! dialog ) {
193+ dialog = await _loadAndRenderDialog ( {
194+ ...mergedOptions ,
195+ onFormClose : ( ) => {
196+ dialog && dialog . close ( ) ;
197+ mergedOptions . onFormClose && mergedOptions . onFormClose ( ) ;
198+ } ,
199+ onFormSubmitted : ( ) => {
200+ dialog && dialog . removeFromDom ( ) ;
201+ mergedOptions . onFormSubmitted && mergedOptions . onFormSubmitted ( ) ;
202+ } ,
203+ } ) ;
204+ }
205+ dialog . appendToDom ( ) ;
232206 dialog . open ( ) ;
233207 } ;
234208 targetEl . addEventListener ( 'click' , handleClick ) ;
235- return ( ) => {
209+ const unsubscribe = ( ) : void => {
210+ _subscriptions = _subscriptions . filter ( sub => sub !== unsubscribe ) ;
211+ dialog && dialog . removeFromDom ( ) ;
212+ dialog = null ;
236213 targetEl . removeEventListener ( 'click' , handleClick ) ;
237214 } ;
215+ _subscriptions . push ( unsubscribe ) ;
216+ return unsubscribe ;
217+ } ;
218+
219+ const autoInjectActor = ( ) : void => {
220+ const shadow = _createShadow ( _options ) ;
221+ const actor = Actor ( { buttonLabel : _options . buttonLabel , shadow } ) ;
222+ const mergedOptions = mergeOptions ( _options , {
223+ onFormOpen ( ) {
224+ actor . removeFromDom ( ) ;
225+ } ,
226+ onFormClose ( ) {
227+ actor . appendToDom ( ) ;
228+ } ,
229+ onFormSubmitted ( ) {
230+ actor . appendToDom ( ) ;
231+ } ,
232+ } ) ;
233+ attachTo ( actor . el , mergedOptions ) ;
234+
235+ actor . appendToDom ( ) ;
238236 } ;
239237
240238 return {
@@ -244,40 +242,7 @@ export const _feedback2Integration = (({
244242 return ;
245243 }
246244
247- const shadow = _getShadow ( _options ) ;
248- const actor = Actor ( { buttonLabel : _options . buttonLabel } ) ;
249- const insertActor = ( ) : void => {
250- shadow . appendChild ( actor . style ) ;
251- shadow . appendChild ( actor . el ) ;
252- } ;
253- attachTo ( actor . el , {
254- onFormOpen ( ) {
255- shadow . removeChild ( actor . el ) ;
256- shadow . removeChild ( actor . style ) ;
257- _options . onFormOpen && _options . onFormOpen ( ) ;
258- } ,
259- onFormClose ( ) {
260- insertActor ( ) ;
261- _options . onFormClose && _options . onFormClose ( ) ;
262- } ,
263- onFormSubmitted ( ) {
264- insertActor ( ) ;
265- _options . onFormSubmitted && _options . onFormSubmitted ( ) ;
266- } ,
267- } ) ;
268-
269- insertActor ( ) ;
270- } ,
271-
272- /**
273- * Removes the Feedback integration (including host, shadow DOM, and all widgets)
274- */
275- remove ( ) : void {
276- if ( _host ) {
277- _host . remove ( ) ;
278- }
279- _host = null ;
280- _shadow = null ;
245+ autoInjectActor ( ) ;
281246 } ,
282247
283248 /**
@@ -290,46 +255,21 @@ export const _feedback2Integration = (({
290255 /**
291256 * Creates a new widget. Accepts partial options to override any options passed to constructor.
292257 */
293- createWidget (
294- optionOverrides : OptionalFeedbackConfiguration & { shouldCreateActor ?: boolean } = { } ,
295- ) : Promise < DialogComponent > {
296- const options = mergeOptions ( _options , optionOverrides ) ;
297-
298- return _loadAndRenderDialog ( options ) ;
258+ async createWidget ( optionOverrides : OverrideFeedbackConfiguration = { } ) : Promise < Dialog > {
259+ return _loadAndRenderDialog ( mergeOptions ( _options , optionOverrides ) ) ;
299260 } ,
300261
301262 /**
302- * Returns the default widget, if it exists
303- */
304- getWidget ( ) : DialogComponent | null {
305- return _dialog ;
306- } ,
307-
308- /**
309- * Allows user to open the dialog box. Creates a new widget if
310- * `autoInject` was false, otherwise re-uses the default widget that was
311- * created during initialization of the integration.
312- */
313- openDialog ( ) : void {
314- _dialog && _dialog . open ( ) ;
315- } ,
316-
317- /**
318- * Closes the dialog for the default widget, if it exists
319- */
320- closeDialog ( ) : void {
321- _dialog && _dialog . close ( ) ;
322- } ,
323-
324- /**
325- * Removes the rendered widget, if it exists
263+ * Removes the Feedback integration (including host, shadow DOM, and all widgets)
326264 */
327- removeWidget ( ) : void {
328- if ( _shadow && _dialog ) {
329- _shadow . removeChild ( _dialog . el ) ;
330- _shadow . removeChild ( _dialog . style ) ;
265+ remove ( ) : void {
266+ if ( _shadow ) {
267+ _shadow . parentElement && _shadow . parentElement . remove ( ) ;
268+ _shadow = null ;
331269 }
332- _dialog = null ;
270+ // Remove any lingering subscriptions
271+ _subscriptions . forEach ( sub => sub ( ) ) ;
272+ _subscriptions = [ ] ;
333273 } ,
334274 } ;
335275} ) satisfies IntegrationFn ;
0 commit comments