tjs-lang 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1 -1
- package/demo/docs.json +9 -9
- package/demo/src/demo-nav.ts +51 -136
- package/demo/src/index.ts +35 -68
- package/demo/src/ts-playground.ts +18 -8
- package/dist/index.js +126 -112
- package/dist/index.js.map +11 -11
- package/dist/src/lang/emitters/js.d.ts +2 -2
- package/dist/src/lang/inference.d.ts +2 -2
- package/dist/src/test-examples.d.ts +41 -0
- package/dist/tjs-full.js +126 -112
- package/dist/tjs-full.js.map +11 -11
- package/dist/tjs-transpiler.js +102 -88
- package/dist/tjs-transpiler.js.map +8 -8
- package/dist/tjs-vm.js +18 -18
- package/dist/tjs-vm.js.map +5 -5
- package/package.json +1 -1
- package/src/lang/codegen.test.ts +76 -16
- package/src/lang/emitters/from-ts.ts +8 -28
- package/src/lang/emitters/js.ts +97 -7
- package/src/lang/eval.ts +41 -2
- package/src/lang/from-ts.test.ts +3 -3
- package/src/lang/inference.ts +34 -20
- package/src/lang/parser.test.ts +4 -4
- package/src/lang/transpiler.test.ts +5 -3
- package/src/lang/typescript-syntax.test.ts +20 -17
- package/src/runtime.test.ts +144 -13
- package/src/test-examples.test.ts +4 -12
- package/src/test-examples.ts +3 -1
- package/src/vm/runtime.ts +19 -0
package/demo/src/demo-nav.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - TJS Docs (documentation that opens in floating viewer)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { Component, elements, ElementCreator, vars } from 'tosijs'
|
|
11
|
+
import { Component, elements, ElementCreator, vars, observe } from 'tosijs'
|
|
12
12
|
import {
|
|
13
13
|
xinFloat,
|
|
14
14
|
XinFloat,
|
|
@@ -72,27 +72,12 @@ interface DocItem {
|
|
|
72
72
|
requiresApi?: boolean
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
interface DemoNavEvents {
|
|
76
|
-
'select-ajs-example': {
|
|
77
|
-
example: { name: string; description: string; code: string; group?: string }
|
|
78
|
-
}
|
|
79
|
-
'select-tjs-example': {
|
|
80
|
-
example: { name: string; description: string; code: string; group?: string }
|
|
81
|
-
}
|
|
82
|
-
'select-ts-example': { example: TSExample }
|
|
83
|
-
'select-doc': { doc: DocItem }
|
|
84
|
-
}
|
|
85
|
-
|
|
86
75
|
export class DemoNav extends Component {
|
|
87
76
|
private _docs: DocItem[] = []
|
|
88
|
-
private
|
|
77
|
+
private _appState: any = null // boxed proxy from index.ts
|
|
89
78
|
private floatViewer: XinFloat | null = null
|
|
90
79
|
private mdViewer: MarkdownViewer | null = null
|
|
91
80
|
|
|
92
|
-
// Track current selection for highlighting
|
|
93
|
-
private _currentView: 'home' | 'ajs' | 'tjs' = 'home'
|
|
94
|
-
private _currentExample: string | null = null
|
|
95
|
-
|
|
96
81
|
// Computed example arrays from docs
|
|
97
82
|
private get tjsExamples(): Example[] {
|
|
98
83
|
return this._docs.filter((d) => d.type === 'example' && d.section === 'tjs')
|
|
@@ -101,106 +86,59 @@ export class DemoNav extends Component {
|
|
|
101
86
|
private get ajsExamples(): Example[] {
|
|
102
87
|
return this._docs.filter((d) => d.type === 'example' && d.section === 'ajs')
|
|
103
88
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// Initialize from URL hash
|
|
107
|
-
this.loadStateFromURL()
|
|
108
|
-
// Listen for hash changes
|
|
109
|
-
window.addEventListener('hashchange', () => this.loadStateFromURL())
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
get currentView() {
|
|
113
|
-
return this._currentView
|
|
89
|
+
get appState() {
|
|
90
|
+
return this._appState
|
|
114
91
|
}
|
|
115
92
|
|
|
116
|
-
set
|
|
117
|
-
this.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
this.
|
|
123
|
-
}
|
|
93
|
+
set appState(state: any) {
|
|
94
|
+
this._appState = state
|
|
95
|
+
if (!state) return
|
|
96
|
+
// Observe state changes to update nav
|
|
97
|
+
observe(/^app\.(currentView|currentExample)/, () => {
|
|
98
|
+
this.rebuildNav()
|
|
99
|
+
this.updateCurrentIndicator()
|
|
100
|
+
})
|
|
101
|
+
observe('app.openSection', () => {
|
|
102
|
+
this.rebuildNav()
|
|
103
|
+
})
|
|
104
|
+
// Initial sync
|
|
124
105
|
this.rebuildNav()
|
|
125
|
-
// Update indicator after rebuild (DOM now exists)
|
|
126
106
|
this.updateCurrentIndicator()
|
|
127
107
|
}
|
|
128
108
|
|
|
129
|
-
get
|
|
130
|
-
return this.
|
|
109
|
+
private get _currentView(): string {
|
|
110
|
+
return this._appState?.currentView?.valueOf() ?? 'home'
|
|
131
111
|
}
|
|
132
112
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
113
|
+
private get _currentExampleName(): string | null {
|
|
114
|
+
const ex = this._appState?.currentExample?.valueOf()
|
|
115
|
+
if (!ex) return null
|
|
116
|
+
return ex.name || ex.title || null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private get _openSection(): string | null {
|
|
120
|
+
return this._appState?.openSection?.valueOf() ?? null
|
|
136
121
|
}
|
|
137
122
|
|
|
138
123
|
private updateCurrentIndicator() {
|
|
139
|
-
|
|
124
|
+
const exName = this._currentExampleName
|
|
140
125
|
const items = this.querySelectorAll('.nav-item')
|
|
141
126
|
items.forEach((item) => {
|
|
142
127
|
const itemName = item.textContent?.trim()
|
|
143
|
-
|
|
144
|
-
item.classList.toggle('current', isCurrent)
|
|
128
|
+
item.classList.toggle('current', itemName === exName)
|
|
145
129
|
})
|
|
146
|
-
// Update home link
|
|
147
130
|
const homeLink = this.querySelector('.home-link')
|
|
148
131
|
homeLink?.classList.toggle('current', this._currentView === 'home')
|
|
149
132
|
}
|
|
150
133
|
|
|
151
|
-
private loadStateFromURL() {
|
|
152
|
-
const hash = window.location.hash.slice(1) // Remove '#'
|
|
153
|
-
if (!hash) return
|
|
154
|
-
|
|
155
|
-
const params = new URLSearchParams(hash)
|
|
156
|
-
const section = params.get('section')
|
|
157
|
-
const view = params.get('view')
|
|
158
|
-
const example = params.get('example')
|
|
159
|
-
|
|
160
|
-
// Set view and open appropriate section
|
|
161
|
-
if (view === 'ajs') {
|
|
162
|
-
this._currentView = 'ajs'
|
|
163
|
-
this.openSection = 'ajs-demos'
|
|
164
|
-
} else if (view === 'tjs') {
|
|
165
|
-
this._currentView = 'tjs'
|
|
166
|
-
this.openSection = 'tjs-demos'
|
|
167
|
-
} else if (view === 'home') {
|
|
168
|
-
this._currentView = 'home'
|
|
169
|
-
} else if (
|
|
170
|
-
section &&
|
|
171
|
-
['ajs-demos', 'tjs-demos', 'ajs-docs', 'tjs-docs'].includes(section)
|
|
172
|
-
) {
|
|
173
|
-
this.openSection = section
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Set current example for highlighting
|
|
177
|
-
if (example) {
|
|
178
|
-
this._currentExample = example
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
this.rebuildNav()
|
|
182
|
-
this.updateCurrentIndicator()
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private saveStateToURL() {
|
|
186
|
-
const params = new URLSearchParams(window.location.hash.slice(1))
|
|
187
|
-
if (this.openSection) {
|
|
188
|
-
params.set('section', this.openSection)
|
|
189
|
-
}
|
|
190
|
-
const newHash = params.toString()
|
|
191
|
-
if (newHash !== window.location.hash.slice(1)) {
|
|
192
|
-
window.history.replaceState(null, '', `#${newHash}`)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
134
|
get docs(): DocItem[] {
|
|
197
135
|
return this._docs
|
|
198
136
|
}
|
|
199
137
|
|
|
200
138
|
set docs(value: DocItem[]) {
|
|
201
139
|
this._docs = value
|
|
202
|
-
// Re-render when docs are set
|
|
203
140
|
this.rebuildNav()
|
|
141
|
+
this.updateCurrentIndicator()
|
|
204
142
|
}
|
|
205
143
|
|
|
206
144
|
// Light DOM styles (no static styleSpec)
|
|
@@ -432,7 +370,7 @@ export class DemoNav extends Component {
|
|
|
432
370
|
// TypeScript Examples (TS -> TJS -> JS pipeline)
|
|
433
371
|
details(
|
|
434
372
|
{
|
|
435
|
-
open: this.
|
|
373
|
+
open: this._openSection === 'ts-demos',
|
|
436
374
|
'data-section': 'ts-demos',
|
|
437
375
|
onToggle: this.handleToggle,
|
|
438
376
|
},
|
|
@@ -458,7 +396,7 @@ export class DemoNav extends Component {
|
|
|
458
396
|
// TJS Examples
|
|
459
397
|
details(
|
|
460
398
|
{
|
|
461
|
-
open: this.
|
|
399
|
+
open: this._openSection === 'tjs-demos',
|
|
462
400
|
'data-section': 'tjs-demos',
|
|
463
401
|
onToggle: this.handleToggle,
|
|
464
402
|
},
|
|
@@ -484,7 +422,7 @@ export class DemoNav extends Component {
|
|
|
484
422
|
// AJS Examples
|
|
485
423
|
details(
|
|
486
424
|
{
|
|
487
|
-
open: this.
|
|
425
|
+
open: this._openSection === 'ajs-demos',
|
|
488
426
|
'data-section': 'ajs-demos',
|
|
489
427
|
onToggle: this.handleToggle,
|
|
490
428
|
},
|
|
@@ -510,7 +448,7 @@ export class DemoNav extends Component {
|
|
|
510
448
|
// TJS Docs
|
|
511
449
|
details(
|
|
512
450
|
{
|
|
513
|
-
open: this.
|
|
451
|
+
open: this._openSection === 'tjs-docs',
|
|
514
452
|
'data-section': 'tjs-docs',
|
|
515
453
|
onToggle: this.handleToggle,
|
|
516
454
|
},
|
|
@@ -535,7 +473,7 @@ export class DemoNav extends Component {
|
|
|
535
473
|
// AJS Docs
|
|
536
474
|
details(
|
|
537
475
|
{
|
|
538
|
-
open: this.
|
|
476
|
+
open: this._openSection === 'ajs-docs',
|
|
539
477
|
'data-section': 'ajs-docs',
|
|
540
478
|
onToggle: this.handleToggle,
|
|
541
479
|
},
|
|
@@ -564,16 +502,13 @@ export class DemoNav extends Component {
|
|
|
564
502
|
const section = details.getAttribute('data-section')
|
|
565
503
|
|
|
566
504
|
if (details.open) {
|
|
505
|
+
if (this._appState) this._appState.openSection.value = section
|
|
567
506
|
// Close other sections (accordion behavior)
|
|
568
|
-
this.
|
|
569
|
-
const allDetails = this.querySelectorAll('details')
|
|
570
|
-
allDetails.forEach((d) => {
|
|
507
|
+
this.querySelectorAll('details').forEach((d) => {
|
|
571
508
|
if (d !== details && d.open) {
|
|
572
509
|
d.open = false
|
|
573
510
|
}
|
|
574
511
|
})
|
|
575
|
-
// Save to URL
|
|
576
|
-
this.saveStateToURL()
|
|
577
512
|
}
|
|
578
513
|
}
|
|
579
514
|
|
|
@@ -598,50 +533,30 @@ export class DemoNav extends Component {
|
|
|
598
533
|
}
|
|
599
534
|
|
|
600
535
|
selectHome() {
|
|
601
|
-
this.
|
|
602
|
-
this.
|
|
603
|
-
this.
|
|
604
|
-
this.dispatchEvent(
|
|
605
|
-
new CustomEvent('select-home', {
|
|
606
|
-
bubbles: true,
|
|
607
|
-
})
|
|
608
|
-
)
|
|
536
|
+
if (!this._appState) return
|
|
537
|
+
this._appState.currentView.value = 'home'
|
|
538
|
+
this._appState.currentExample.value = null
|
|
609
539
|
}
|
|
610
540
|
|
|
611
541
|
selectAjsExample(example: Example) {
|
|
612
|
-
this.
|
|
613
|
-
this.
|
|
614
|
-
this.
|
|
615
|
-
this.
|
|
616
|
-
new CustomEvent('select-ajs-example', {
|
|
617
|
-
detail: { example: exampleToLegacy(example) },
|
|
618
|
-
bubbles: true,
|
|
619
|
-
})
|
|
620
|
-
)
|
|
542
|
+
if (!this._appState) return
|
|
543
|
+
this._appState.currentView.value = 'ajs'
|
|
544
|
+
this._appState.currentExample.value = exampleToLegacy(example)
|
|
545
|
+
this._appState.openSection.value = 'ajs-demos'
|
|
621
546
|
}
|
|
622
547
|
|
|
623
548
|
selectTjsExample(example: Example) {
|
|
624
|
-
this.
|
|
625
|
-
this.
|
|
626
|
-
this.
|
|
627
|
-
this.
|
|
628
|
-
new CustomEvent('select-tjs-example', {
|
|
629
|
-
detail: { example: exampleToLegacy(example) },
|
|
630
|
-
bubbles: true,
|
|
631
|
-
})
|
|
632
|
-
)
|
|
549
|
+
if (!this._appState) return
|
|
550
|
+
this._appState.currentView.value = 'tjs'
|
|
551
|
+
this._appState.currentExample.value = exampleToLegacy(example)
|
|
552
|
+
this._appState.openSection.value = 'tjs-demos'
|
|
633
553
|
}
|
|
634
554
|
|
|
635
555
|
selectTsExample(example: TSExample) {
|
|
636
|
-
this.
|
|
637
|
-
this.
|
|
638
|
-
this.
|
|
639
|
-
this.
|
|
640
|
-
new CustomEvent('select-ts-example', {
|
|
641
|
-
detail: { example: exampleToLegacy(example) },
|
|
642
|
-
bubbles: true,
|
|
643
|
-
})
|
|
644
|
-
)
|
|
556
|
+
if (!this._appState) return
|
|
557
|
+
this._appState.currentView.value = 'ts'
|
|
558
|
+
this._appState.currentExample.value = exampleToLegacy(example)
|
|
559
|
+
this._appState.openSection.value = 'ts-demos'
|
|
645
560
|
}
|
|
646
561
|
|
|
647
562
|
selectDoc(doc: DocItem) {
|
package/demo/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - Markdown content with live examples
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { elements, tosi, bindings, StyleSheet, bind } from 'tosijs'
|
|
10
|
+
import { elements, tosi, bindings, StyleSheet, bind, observe } from 'tosijs'
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
icons,
|
|
@@ -137,6 +137,7 @@ const { app, prefs, auth } = tosi({
|
|
|
137
137
|
compact: false,
|
|
138
138
|
currentView: 'home' as 'home' | 'ajs' | 'tjs' | 'ts',
|
|
139
139
|
currentExample: null as any,
|
|
140
|
+
openSection: null as string | null,
|
|
140
141
|
},
|
|
141
142
|
auth: {
|
|
142
143
|
user: null as {
|
|
@@ -248,35 +249,32 @@ window.addEventListener('popstate', () => {
|
|
|
248
249
|
app.docs.find((doc: any) => doc.filename === filename) || app.docs[0]
|
|
249
250
|
})
|
|
250
251
|
|
|
251
|
-
// URL state
|
|
252
|
-
|
|
252
|
+
// URL ↔ state sync (single source of truth: app state)
|
|
253
|
+
let _suppressHashUpdate = false
|
|
254
|
+
|
|
255
|
+
function syncURLToState() {
|
|
253
256
|
const hash = window.location.hash.slice(1)
|
|
254
|
-
if (!hash)
|
|
255
|
-
app.currentView = 'home'
|
|
256
|
-
return
|
|
257
|
-
}
|
|
257
|
+
if (!hash) return
|
|
258
258
|
|
|
259
|
+
_suppressHashUpdate = true
|
|
259
260
|
const params = new URLSearchParams(hash)
|
|
260
261
|
const view = params.get('view')
|
|
261
262
|
const example = params.get('example')
|
|
263
|
+
const section = params.get('section')
|
|
262
264
|
|
|
263
|
-
if (view === 'home') {
|
|
264
|
-
app.currentView.
|
|
265
|
-
app.currentExample = null
|
|
266
|
-
} else if (view === 'ajs' || view === 'tjs' || view === 'ts') {
|
|
267
|
-
app.currentView = view
|
|
265
|
+
if (view === 'home' || view === 'ajs' || view === 'tjs' || view === 'ts') {
|
|
266
|
+
app.currentView.value = view
|
|
268
267
|
}
|
|
268
|
+
if (section) app.openSection.value = section
|
|
269
269
|
|
|
270
270
|
if (example) {
|
|
271
|
-
// Find example by name in appropriate list
|
|
272
271
|
if (view === 'ts') {
|
|
273
272
|
const found = tsExamples.find((e: any) => e.name === example)
|
|
274
|
-
if (found) app.currentExample = found
|
|
273
|
+
if (found) app.currentExample.value = found
|
|
275
274
|
} else if (view === 'tjs') {
|
|
276
|
-
// Find in docs by title
|
|
277
275
|
const found = findExampleInDocs(docs, 'tjs', example)
|
|
278
276
|
if (found) {
|
|
279
|
-
app.currentExample = {
|
|
277
|
+
app.currentExample.value = {
|
|
280
278
|
name: found.title,
|
|
281
279
|
description: found.description || found.title,
|
|
282
280
|
code: found.code || '',
|
|
@@ -284,10 +282,9 @@ function loadViewStateFromURL() {
|
|
|
284
282
|
}
|
|
285
283
|
}
|
|
286
284
|
} else if (view === 'ajs') {
|
|
287
|
-
// Find in docs by title
|
|
288
285
|
const found = findExampleInDocs(docs, 'ajs', example)
|
|
289
286
|
if (found) {
|
|
290
|
-
app.currentExample = {
|
|
287
|
+
app.currentExample.value = {
|
|
291
288
|
name: found.title,
|
|
292
289
|
description: found.description || found.title,
|
|
293
290
|
code: found.code || '',
|
|
@@ -295,26 +292,29 @@ function loadViewStateFromURL() {
|
|
|
295
292
|
}
|
|
296
293
|
}
|
|
297
294
|
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function saveViewStateToURL(view: string, exampleName?: string) {
|
|
302
|
-
const params = new URLSearchParams(window.location.hash.slice(1))
|
|
303
|
-
params.set('view', view)
|
|
304
|
-
if (exampleName) {
|
|
305
|
-
params.set('example', exampleName)
|
|
306
295
|
} else {
|
|
307
|
-
|
|
296
|
+
app.currentExample.value = null
|
|
308
297
|
}
|
|
309
|
-
|
|
310
|
-
window.history.replaceState(null, '', `#${newHash}`)
|
|
298
|
+
_suppressHashUpdate = false
|
|
311
299
|
}
|
|
312
300
|
|
|
313
|
-
|
|
314
|
-
|
|
301
|
+
syncURLToState()
|
|
302
|
+
window.addEventListener('hashchange', syncURLToState)
|
|
315
303
|
|
|
316
|
-
//
|
|
317
|
-
|
|
304
|
+
// State → URL (observe changes, update hash)
|
|
305
|
+
observe(/^app\.(currentView|currentExample|openSection)/, () => {
|
|
306
|
+
if (_suppressHashUpdate) return
|
|
307
|
+
const params = new URLSearchParams()
|
|
308
|
+
const view = app.currentView.valueOf() as string
|
|
309
|
+
params.set('view', view)
|
|
310
|
+
const example = app.currentExample.valueOf() as any
|
|
311
|
+
if (example) {
|
|
312
|
+
params.set('example', example.name || example.title || '')
|
|
313
|
+
}
|
|
314
|
+
const section = app.openSection.valueOf() as string | null
|
|
315
|
+
if (section) params.set('section', section)
|
|
316
|
+
window.history.replaceState(null, '', `#${params.toString()}`)
|
|
317
|
+
})
|
|
318
318
|
|
|
319
319
|
// Main app
|
|
320
320
|
const main = document.querySelector('main') as HTMLElement
|
|
@@ -596,7 +596,7 @@ if (main) {
|
|
|
596
596
|
},
|
|
597
597
|
},
|
|
598
598
|
|
|
599
|
-
// Demo navigation
|
|
599
|
+
// Demo navigation — shares app state directly
|
|
600
600
|
(() => {
|
|
601
601
|
const nav = demoNav({
|
|
602
602
|
slot: 'nav',
|
|
@@ -606,41 +606,8 @@ if (main) {
|
|
|
606
606
|
},
|
|
607
607
|
}) as DemoNav
|
|
608
608
|
|
|
609
|
-
// Pass docs to the nav component
|
|
610
609
|
nav.docs = docs
|
|
611
|
-
|
|
612
|
-
// Handle AJS example selection - load into AJS playground
|
|
613
|
-
nav.addEventListener('select-ajs-example', ((event: CustomEvent) => {
|
|
614
|
-
const { example } = event.detail
|
|
615
|
-
app.currentView = 'ajs'
|
|
616
|
-
app.currentExample = example
|
|
617
|
-
saveViewStateToURL('ajs', example.name)
|
|
618
|
-
}) as EventListener)
|
|
619
|
-
|
|
620
|
-
// Handle TJS example selection - load into TJS playground
|
|
621
|
-
nav.addEventListener('select-tjs-example', ((event: CustomEvent) => {
|
|
622
|
-
const { example } = event.detail
|
|
623
|
-
app.currentView = 'tjs'
|
|
624
|
-
app.currentExample = example
|
|
625
|
-
saveViewStateToURL('tjs', example.name)
|
|
626
|
-
}) as EventListener)
|
|
627
|
-
|
|
628
|
-
// Handle TS example selection - load into TS playground
|
|
629
|
-
nav.addEventListener('select-ts-example', ((event: CustomEvent) => {
|
|
630
|
-
const { example } = event.detail
|
|
631
|
-
app.currentView = 'ts'
|
|
632
|
-
app.currentExample = example
|
|
633
|
-
saveViewStateToURL('ts', example.name)
|
|
634
|
-
}) as EventListener)
|
|
635
|
-
|
|
636
|
-
// Handle Home selection - show README
|
|
637
|
-
nav.addEventListener('select-home', (() => {
|
|
638
|
-
app.currentView = 'home'
|
|
639
|
-
app.currentExample = null
|
|
640
|
-
saveViewStateToURL('home')
|
|
641
|
-
}) as EventListener)
|
|
642
|
-
|
|
643
|
-
// Nav syncs its own state from URL - no need to overwrite on initial load
|
|
610
|
+
nav.appState = app
|
|
644
611
|
|
|
645
612
|
return nav
|
|
646
613
|
})(),
|
|
@@ -81,6 +81,7 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
|
|
|
81
81
|
private lastTjsCode: string = ''
|
|
82
82
|
private lastJsCode: string = ''
|
|
83
83
|
private consoleMessages: string[] = []
|
|
84
|
+
private _messageHandler: ((e: MessageEvent) => void) | null = null
|
|
84
85
|
|
|
85
86
|
// Editor state persistence
|
|
86
87
|
private currentExampleName: string | null = null
|
|
@@ -514,8 +515,13 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
|
|
|
514
515
|
jsCode,
|
|
515
516
|
})
|
|
516
517
|
|
|
518
|
+
// Clean up any previous message handler
|
|
519
|
+
if (this._messageHandler) {
|
|
520
|
+
window.removeEventListener('message', this._messageHandler)
|
|
521
|
+
}
|
|
522
|
+
|
|
517
523
|
// Listen for messages from iframe
|
|
518
|
-
|
|
524
|
+
this._messageHandler = createIframeMessageHandler({
|
|
519
525
|
onConsole: (message) => this.log(message),
|
|
520
526
|
onTiming: (execTime) => {
|
|
521
527
|
this.parts.consoleHeader.textContent = `Console — executed in ${formatExecTime(
|
|
@@ -531,15 +537,19 @@ export class TSPlayground extends Component<TSPlaygroundParts> {
|
|
|
531
537
|
this.parts.statusBar.classList.add('error')
|
|
532
538
|
},
|
|
533
539
|
})
|
|
534
|
-
window.addEventListener('message',
|
|
540
|
+
window.addEventListener('message', this._messageHandler)
|
|
535
541
|
|
|
536
|
-
// Set iframe content
|
|
537
|
-
|
|
542
|
+
// Set iframe content using blob URL instead of srcdoc
|
|
543
|
+
// (srcdoc can cause double-execution in some browsers)
|
|
544
|
+
const iframe = this.parts.previewFrame
|
|
545
|
+
const blob = new Blob([iframeDoc], { type: 'text/html' })
|
|
546
|
+
const blobUrl = URL.createObjectURL(blob)
|
|
538
547
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
548
|
+
if (iframe.dataset.blobUrl) {
|
|
549
|
+
URL.revokeObjectURL(iframe.dataset.blobUrl)
|
|
550
|
+
}
|
|
551
|
+
iframe.dataset.blobUrl = blobUrl
|
|
552
|
+
iframe.src = blobUrl
|
|
543
553
|
} catch (e: any) {
|
|
544
554
|
this.log(`Error: ${e.message}`)
|
|
545
555
|
this.parts.statusBar.textContent = 'Error'
|