@@ -16,23 +16,27 @@ import {
16
16
SUBMIT_BUTTON_LABEL ,
17
17
SUCCESS_MESSAGE_TEXT ,
18
18
} from '../constants' ;
19
- import type { DialogComponent } from '../modal/components/Dialog' ;
20
19
import type { IFeedback2ModalIntegration } from '../modal/integration' ;
21
20
import 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' ;
23
27
import { DEBUG_BUILD } from '../util/debug-build' ;
24
28
import { mergeOptions } from '../util/mergeOptions' ;
25
29
import { Actor } from './components/Actor' ;
26
30
import { createMainStyles } from './createMainStyles' ;
27
31
import { sendFeedback } from './sendFeedback' ;
28
32
33
+ type Unsubscribe = ( ) => void ;
34
+
29
35
interface PublicFeedback2Integration {
36
+ attachTo : ( el : Element | string , optionOverrides : OverrideFeedbackConfiguration ) => ( ) => void ;
37
+ createWidget : ( optionOverrides : OverrideFeedbackConfiguration & { shouldCreateActor ?: boolean } ) => Promise < Dialog > ;
38
+ getWidget : ( ) => Dialog | null ;
30
39
remove : ( ) => void ;
31
- attachTo : ( el : Element | string , optionOverrides : OptionalFeedbackConfiguration ) => ( ) => void ;
32
- createWidget : (
33
- optionOverrides : OptionalFeedbackConfiguration & { shouldCreateActor ?: boolean } ,
34
- ) => Promise < DialogComponent > ;
35
- getWidget : ( ) => DialogComponent | null ;
36
40
openDialog : ( ) => void ;
37
41
closeDialog : ( ) => void ;
38
42
removeWidget : ( ) => void ;
@@ -119,72 +123,33 @@ export const _feedback2Integration = (({
119
123
onFormSubmitted,
120
124
} ;
121
125
122
- let _host : HTMLElement | null = null ;
123
126
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 [ ] = [ ] ;
141
128
142
129
/**
143
130
* Get the shadow root where we will append css
144
131
*/
145
- const _getShadow = ( options : FeedbackInternalOptions ) : ShadowRoot => {
132
+ const _createShadow = ( options : FeedbackInternalOptions ) : ShadowRoot => {
146
133
if ( ! _shadow ) {
147
- const host = _getHost ( options ) ;
134
+ const host = DOCUMENT . createElement ( 'div' ) ;
135
+ host . id = String ( options . id ) ;
136
+ DOCUMENT . body . appendChild ( host ) ;
148
137
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 ) ) ;
161
140
}
162
-
163
- return _shadow ;
141
+ return _shadow as ShadowRoot ;
164
142
} ;
165
143
166
- const _loadAndRenderDialog = async ( options : FeedbackInternalOptions ) : Promise < DialogComponent > => {
167
- if ( _dialog ) {
168
- return _dialog ;
169
- }
170
-
144
+ const _loadAndRenderDialog = async ( options : FeedbackInternalOptions ) : Promise < Dialog > => {
171
145
const client = getClient ( ) ; // TODO: getClient<BrowserClient>()
172
146
if ( ! client ) {
173
147
throw new Error ( 'Sentry Client is not initialized correctly' ) ;
174
148
}
175
149
const modalIntegration = client . getIntegrationByName < IFeedback2ModalIntegration > ( 'Feedback2Modal' ) ;
176
150
const screenshotIntegration = client . getIntegrationByName < IFeedback2ScreenshotIntegration > ( 'Feedback2Screenshot' ) ;
177
151
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
-
186
152
// START TEMP: Error messages
187
- console . log ( 'ensureRenderer:' , { modalIntegration, showScreenshot, screenshotIntegration } ) ; // eslint-disable-line no-console
188
153
if ( ! modalIntegration && showScreenshot && ! screenshotIntegration ) {
189
154
throw new Error ( 'Async loading of Feedback Modal & Screenshot integrations is not yet implemented' ) ;
190
155
} else if ( ! modalIntegration ) {
@@ -203,21 +168,16 @@ export const _feedback2Integration = (({
203
168
throw new Error ( 'Not implemented yet' ) ;
204
169
}
205
170
206
- const dialog = modalIntegration . createDialog ( {
207
- shadow : _getShadow ( options ) ,
208
- sendFeedback,
171
+ return modalIntegration . createDialog ( {
209
172
options,
210
- onDone : ( ) => {
211
- _dialog = null ;
212
- } ,
213
173
screenshotIntegration,
174
+ sendFeedback,
175
+ shadow : _createShadow ( options ) ,
214
176
} ) ;
215
- _dialog = dialog ;
216
- return dialog ;
217
177
} ;
218
178
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 ) ;
221
181
222
182
const targetEl =
223
183
typeof el === 'string' ? DOCUMENT . querySelector ( el ) : typeof el . addEventListener === 'function' ? el : null ;
@@ -227,14 +187,52 @@ export const _feedback2Integration = (({
227
187
throw new Error ( 'Unable to attach to target element' ) ;
228
188
}
229
189
190
+ let dialog : Dialog | null = null ;
230
191
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 ( ) ;
232
206
dialog . open ( ) ;
233
207
} ;
234
208
targetEl . addEventListener ( 'click' , handleClick ) ;
235
- return ( ) => {
209
+ const unsubscribe = ( ) : void => {
210
+ _subscriptions = _subscriptions . filter ( sub => sub !== unsubscribe ) ;
211
+ dialog && dialog . removeFromDom ( ) ;
212
+ dialog = null ;
236
213
targetEl . removeEventListener ( 'click' , handleClick ) ;
237
214
} ;
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 ( ) ;
238
236
} ;
239
237
240
238
return {
@@ -244,40 +242,7 @@ export const _feedback2Integration = (({
244
242
return ;
245
243
}
246
244
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 ( ) ;
281
246
} ,
282
247
283
248
/**
@@ -290,46 +255,21 @@ export const _feedback2Integration = (({
290
255
/**
291
256
* Creates a new widget. Accepts partial options to override any options passed to constructor.
292
257
*/
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 ) ) ;
299
260
} ,
300
261
301
262
/**
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)
326
264
*/
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 ;
331
269
}
332
- _dialog = null ;
270
+ // Remove any lingering subscriptions
271
+ _subscriptions . forEach ( sub => sub ( ) ) ;
272
+ _subscriptions = [ ] ;
333
273
} ,
334
274
} ;
335
275
} ) satisfies IntegrationFn ;
0 commit comments