@@ -3,7 +3,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
3
3
import useBaseUrl from '@docusaurus/useBaseUrl' ;
4
4
import './playground.css' ;
5
5
import { EditorOptions , openAngularEditor , openHtmlEditor , openReactEditor , openVueEditor } from './stackblitz.utils' ;
6
- import { Mode , UsageTarget } from './playground.types' ;
6
+ import { ConsoleItem , Mode , UsageTarget } from './playground.types' ;
7
7
import useThemeContext from '@theme/hooks/useThemeContext' ;
8
8
9
9
import Tippy from '@tippyjs/react' ;
@@ -109,6 +109,7 @@ interface UsageTargetOptions {
109
109
* @param src The absolute path to the playground demo. For example: `/usage/button/basic/demo.html`
110
110
* @param size The height of the playground. Supports `xsmall`, `small`, `medium`, `large`, 'xlarge' or any string value.
111
111
* @param devicePreview `true` if the playground example should render in a device frame (iOS/MD).
112
+ * @param showConsole `true` if the playground should render a console UI that reflects console logs, warnings, and errors.
112
113
*/
113
114
export default function Playground ( {
114
115
code,
@@ -118,6 +119,7 @@ export default function Playground({
118
119
size = 'small' ,
119
120
mode,
120
121
devicePreview,
122
+ showConsole,
121
123
includeIonContent = true ,
122
124
version,
123
125
} : {
@@ -133,6 +135,7 @@ export default function Playground({
133
135
mode ?: 'ios' | 'md' ;
134
136
description ?: string ;
135
137
devicePreview ?: boolean ;
138
+ showConsole ?: boolean ;
136
139
includeIonContent : boolean ;
137
140
/**
138
141
* The major version of Ionic to use in the generated Stackblitz examples.
@@ -159,6 +162,7 @@ export default function Playground({
159
162
const codeRef = useRef ( null ) ;
160
163
const frameiOS = useRef < HTMLIFrameElement | null > ( null ) ;
161
164
const frameMD = useRef < HTMLIFrameElement | null > ( null ) ;
165
+ const consoleBodyRef = useRef < HTMLDivElement | null > ( null ) ;
162
166
163
167
const defaultMode = typeof mode !== 'undefined' ? mode : Mode . iOS ;
164
168
@@ -182,6 +186,15 @@ export default function Playground({
182
186
const [ codeSnippets , setCodeSnippets ] = useState ( { } ) ;
183
187
const [ renderIframes , setRenderIframes ] = useState ( false ) ;
184
188
const [ iframesLoaded , setIframesLoaded ] = useState ( false ) ;
189
+ const [ mdConsoleItems , setMDConsoleItems ] = useState < ConsoleItem [ ] > ( [ ] ) ;
190
+ const [ iosConsoleItems , setiOSConsoleItems ] = useState < ConsoleItem [ ] > ( [ ] ) ;
191
+
192
+ /**
193
+ * We don't actually care about the count, but this lets us
194
+ * re-trigger useEffect hooks when the demo is reset and the
195
+ * iframes are refreshed.
196
+ */
197
+ const [ resetCount , setResetCount ] = useState ( 0 ) ;
185
198
186
199
/**
187
200
* Rather than encode isDarkTheme into the frame source
@@ -258,6 +271,24 @@ export default function Playground({
258
271
setFramesLoaded ( ) ;
259
272
} , [ renderIframes ] ) ;
260
273
274
+ useEffect ( ( ) => {
275
+ if ( showConsole ) {
276
+ if ( frameiOS . current ) {
277
+ frameiOS . current . contentWindow . addEventListener ( 'console' , ( ev : CustomEvent ) => {
278
+ setiOSConsoleItems ( ( oldConsoleItems ) => [ ...oldConsoleItems , ev . detail ] ) ;
279
+ consoleBodyRef . current . scrollTo ( 0 , consoleBodyRef . current . scrollHeight ) ;
280
+ } ) ;
281
+ }
282
+
283
+ if ( frameMD . current ) {
284
+ frameMD . current . contentWindow . addEventListener ( 'console' , ( ev : CustomEvent ) => {
285
+ setMDConsoleItems ( ( oldConsoleItems ) => [ ...oldConsoleItems , ev . detail ] ) ;
286
+ consoleBodyRef . current . scrollTo ( 0 , consoleBodyRef . current . scrollHeight ) ;
287
+ } ) ;
288
+ }
289
+ }
290
+ } , [ iframesLoaded , resetCount ] ) ; // including resetCount re-runs this when iframes are reloaded
291
+
261
292
useEffect ( ( ) => {
262
293
/**
263
294
* Using a dynamic import here to avoid SSR errors when trying to extend `HTMLElement`
@@ -311,13 +342,19 @@ export default function Playground({
311
342
/**
312
343
* Reloads the iOS and MD iframe sources back to their original state.
313
344
*/
314
- function resetDemo ( ) {
345
+ async function resetDemo ( ) {
315
346
if ( frameiOS . current ) {
316
347
frameiOS . current . contentWindow . location . reload ( ) ;
317
348
}
318
349
if ( frameMD . current ) {
319
350
frameMD . current . contentWindow . location . reload ( ) ;
320
351
}
352
+
353
+ setiOSConsoleItems ( [ ] ) ;
354
+ setMDConsoleItems ( [ ] ) ;
355
+
356
+ await Promise . all ( [ waitForNextFrameLoadEvent ( frameiOS . current ) , waitForNextFrameLoadEvent ( frameMD . current ) ] ) ;
357
+ setResetCount ( ( oldCount ) => oldCount + 1 ) ;
321
358
}
322
359
323
360
function openEditor ( event ) {
@@ -444,11 +481,39 @@ export default function Playground({
444
481
) ;
445
482
}
446
483
484
+ function renderConsole ( ) {
485
+ const consoleItems = ionicMode === Mode . iOS ? iosConsoleItems : mdConsoleItems ;
486
+
487
+ return (
488
+ < div className = "playground__console" >
489
+ < div className = "playground__console-header" >
490
+ < code > Console</ code >
491
+ </ div >
492
+ < div className = "playground__console-body" ref = { consoleBodyRef } >
493
+ { consoleItems . length === 0 ? (
494
+ < div className = "playground__console-item playground__console-item--placeholder" >
495
+ < code > Console messages will appear here when logged from the example above.</ code >
496
+ </ div >
497
+ ) : (
498
+ consoleItems . map ( ( consoleItem , i ) => (
499
+ < div key = { i } className = { `playground__console-item playground__console-item--${ consoleItem . type } ` } >
500
+ { consoleItem . type !== 'log' && (
501
+ < div className = "playground__console-icon" > { consoleItem . type === 'warning' ? '⚠' : '❌' } </ div >
502
+ ) }
503
+ < code > { consoleItem . message } </ code >
504
+ </ div >
505
+ ) )
506
+ ) }
507
+ </ div >
508
+ </ div >
509
+ ) ;
510
+ }
511
+
447
512
const sortedUsageTargets = useMemo ( ( ) => Object . keys ( UsageTarget ) . sort ( ) , [ ] ) ;
448
513
449
514
return (
450
515
< div className = "playground" ref = { hostRef } >
451
- < div className = " playground__container" >
516
+ < div className = { ` playground__container ${ showConsole ? 'playground__container--has-console' : '' } ` } >
452
517
< div className = "playground__control-toolbar" >
453
518
< div className = "playground__control-group" >
454
519
{ sortedUsageTargets . map ( ( lang ) => {
@@ -633,6 +698,7 @@ export default function Playground({
633
698
]
634
699
: [ ] }
635
700
</ div >
701
+ { showConsole && renderConsole ( ) }
636
702
< div ref = { codeRef } className = "playground__code-block" >
637
703
{ renderCodeSnippets ( ) }
638
704
</ div >
@@ -660,6 +726,26 @@ const waitForFrame = (frame: HTMLIFrameElement) => {
660
726
} ) ;
661
727
} ;
662
728
729
+ /**
730
+ * Returns a promise that resolves on the *next* load event of the
731
+ * given iframe. We intentionally don't check if it's already loaded
732
+ * because this is used when the demo is reset and the iframe is
733
+ * refreshed, so we don't want to return too early and catch the
734
+ * pre-reset version of the window.
735
+ */
736
+ const waitForNextFrameLoadEvent = ( frame : HTMLIFrameElement ) => {
737
+ return new Promise < void > ( ( resolve ) => {
738
+ const handleLoad = ( ) => {
739
+ frame . removeEventListener ( 'load' , handleLoad ) ;
740
+ resolve ( ) ;
741
+ } ;
742
+
743
+ if ( frame ) {
744
+ frame . addEventListener ( 'load' , handleLoad ) ;
745
+ }
746
+ } ) ;
747
+ } ;
748
+
663
749
const isFrameReady = ( frame : HTMLIFrameElement ) => {
664
750
if ( ! frame ) {
665
751
return false ;
0 commit comments