tjs-lang 0.5.3 → 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 +6 -6
- package/demo/src/demo-nav.ts +51 -143
- 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 +69 -17
- package/src/lang/emitters/from-ts.ts +7 -32
- package/src/lang/emitters/js.ts +97 -7
- 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/test-examples.test.ts +4 -12
- package/src/test-examples.ts +3 -1
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' | 'ts' = '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,113 +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
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
126
105
|
this.rebuildNav()
|
|
127
|
-
// Update indicator after rebuild (DOM now exists)
|
|
128
106
|
this.updateCurrentIndicator()
|
|
129
107
|
}
|
|
130
108
|
|
|
131
|
-
get
|
|
132
|
-
return this.
|
|
109
|
+
private get _currentView(): string {
|
|
110
|
+
return this._appState?.currentView?.valueOf() ?? 'home'
|
|
133
111
|
}
|
|
134
112
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
138
121
|
}
|
|
139
122
|
|
|
140
123
|
private updateCurrentIndicator() {
|
|
141
|
-
|
|
124
|
+
const exName = this._currentExampleName
|
|
142
125
|
const items = this.querySelectorAll('.nav-item')
|
|
143
126
|
items.forEach((item) => {
|
|
144
127
|
const itemName = item.textContent?.trim()
|
|
145
|
-
|
|
146
|
-
item.classList.toggle('current', isCurrent)
|
|
128
|
+
item.classList.toggle('current', itemName === exName)
|
|
147
129
|
})
|
|
148
|
-
// Update home link
|
|
149
130
|
const homeLink = this.querySelector('.home-link')
|
|
150
131
|
homeLink?.classList.toggle('current', this._currentView === 'home')
|
|
151
132
|
}
|
|
152
133
|
|
|
153
|
-
private loadStateFromURL() {
|
|
154
|
-
const hash = window.location.hash.slice(1) // Remove '#'
|
|
155
|
-
if (!hash) return
|
|
156
|
-
|
|
157
|
-
const params = new URLSearchParams(hash)
|
|
158
|
-
const section = params.get('section')
|
|
159
|
-
const view = params.get('view')
|
|
160
|
-
const example = params.get('example')
|
|
161
|
-
|
|
162
|
-
// Set view and open appropriate section
|
|
163
|
-
if (view === 'ajs') {
|
|
164
|
-
this._currentView = 'ajs'
|
|
165
|
-
this.openSection = 'ajs-demos'
|
|
166
|
-
} else if (view === 'tjs') {
|
|
167
|
-
this._currentView = 'tjs'
|
|
168
|
-
this.openSection = 'tjs-demos'
|
|
169
|
-
} else if (view === 'ts') {
|
|
170
|
-
this._currentView = 'ts'
|
|
171
|
-
this.openSection = 'ts-demos'
|
|
172
|
-
} else if (view === 'home') {
|
|
173
|
-
this._currentView = 'home'
|
|
174
|
-
} else if (
|
|
175
|
-
section &&
|
|
176
|
-
['ajs-demos', 'tjs-demos', 'ts-demos', 'ajs-docs', 'tjs-docs'].includes(
|
|
177
|
-
section
|
|
178
|
-
)
|
|
179
|
-
) {
|
|
180
|
-
this.openSection = section
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Set current example for highlighting
|
|
184
|
-
if (example) {
|
|
185
|
-
this._currentExample = example
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
this.rebuildNav()
|
|
189
|
-
this.updateCurrentIndicator()
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private saveStateToURL() {
|
|
193
|
-
const params = new URLSearchParams(window.location.hash.slice(1))
|
|
194
|
-
if (this.openSection) {
|
|
195
|
-
params.set('section', this.openSection)
|
|
196
|
-
}
|
|
197
|
-
const newHash = params.toString()
|
|
198
|
-
if (newHash !== window.location.hash.slice(1)) {
|
|
199
|
-
window.history.replaceState(null, '', `#${newHash}`)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
134
|
get docs(): DocItem[] {
|
|
204
135
|
return this._docs
|
|
205
136
|
}
|
|
206
137
|
|
|
207
138
|
set docs(value: DocItem[]) {
|
|
208
139
|
this._docs = value
|
|
209
|
-
// Re-render when docs are set
|
|
210
140
|
this.rebuildNav()
|
|
141
|
+
this.updateCurrentIndicator()
|
|
211
142
|
}
|
|
212
143
|
|
|
213
144
|
// Light DOM styles (no static styleSpec)
|
|
@@ -439,7 +370,7 @@ export class DemoNav extends Component {
|
|
|
439
370
|
// TypeScript Examples (TS -> TJS -> JS pipeline)
|
|
440
371
|
details(
|
|
441
372
|
{
|
|
442
|
-
open: this.
|
|
373
|
+
open: this._openSection === 'ts-demos',
|
|
443
374
|
'data-section': 'ts-demos',
|
|
444
375
|
onToggle: this.handleToggle,
|
|
445
376
|
},
|
|
@@ -465,7 +396,7 @@ export class DemoNav extends Component {
|
|
|
465
396
|
// TJS Examples
|
|
466
397
|
details(
|
|
467
398
|
{
|
|
468
|
-
open: this.
|
|
399
|
+
open: this._openSection === 'tjs-demos',
|
|
469
400
|
'data-section': 'tjs-demos',
|
|
470
401
|
onToggle: this.handleToggle,
|
|
471
402
|
},
|
|
@@ -491,7 +422,7 @@ export class DemoNav extends Component {
|
|
|
491
422
|
// AJS Examples
|
|
492
423
|
details(
|
|
493
424
|
{
|
|
494
|
-
open: this.
|
|
425
|
+
open: this._openSection === 'ajs-demos',
|
|
495
426
|
'data-section': 'ajs-demos',
|
|
496
427
|
onToggle: this.handleToggle,
|
|
497
428
|
},
|
|
@@ -517,7 +448,7 @@ export class DemoNav extends Component {
|
|
|
517
448
|
// TJS Docs
|
|
518
449
|
details(
|
|
519
450
|
{
|
|
520
|
-
open: this.
|
|
451
|
+
open: this._openSection === 'tjs-docs',
|
|
521
452
|
'data-section': 'tjs-docs',
|
|
522
453
|
onToggle: this.handleToggle,
|
|
523
454
|
},
|
|
@@ -542,7 +473,7 @@ export class DemoNav extends Component {
|
|
|
542
473
|
// AJS Docs
|
|
543
474
|
details(
|
|
544
475
|
{
|
|
545
|
-
open: this.
|
|
476
|
+
open: this._openSection === 'ajs-docs',
|
|
546
477
|
'data-section': 'ajs-docs',
|
|
547
478
|
onToggle: this.handleToggle,
|
|
548
479
|
},
|
|
@@ -571,16 +502,13 @@ export class DemoNav extends Component {
|
|
|
571
502
|
const section = details.getAttribute('data-section')
|
|
572
503
|
|
|
573
504
|
if (details.open) {
|
|
505
|
+
if (this._appState) this._appState.openSection.value = section
|
|
574
506
|
// Close other sections (accordion behavior)
|
|
575
|
-
this.
|
|
576
|
-
const allDetails = this.querySelectorAll('details')
|
|
577
|
-
allDetails.forEach((d) => {
|
|
507
|
+
this.querySelectorAll('details').forEach((d) => {
|
|
578
508
|
if (d !== details && d.open) {
|
|
579
509
|
d.open = false
|
|
580
510
|
}
|
|
581
511
|
})
|
|
582
|
-
// Save to URL
|
|
583
|
-
this.saveStateToURL()
|
|
584
512
|
}
|
|
585
513
|
}
|
|
586
514
|
|
|
@@ -605,50 +533,30 @@ export class DemoNav extends Component {
|
|
|
605
533
|
}
|
|
606
534
|
|
|
607
535
|
selectHome() {
|
|
608
|
-
this.
|
|
609
|
-
this.
|
|
610
|
-
this.
|
|
611
|
-
this.dispatchEvent(
|
|
612
|
-
new CustomEvent('select-home', {
|
|
613
|
-
bubbles: true,
|
|
614
|
-
})
|
|
615
|
-
)
|
|
536
|
+
if (!this._appState) return
|
|
537
|
+
this._appState.currentView.value = 'home'
|
|
538
|
+
this._appState.currentExample.value = null
|
|
616
539
|
}
|
|
617
540
|
|
|
618
541
|
selectAjsExample(example: Example) {
|
|
619
|
-
this.
|
|
620
|
-
this.
|
|
621
|
-
this.
|
|
622
|
-
this.
|
|
623
|
-
new CustomEvent('select-ajs-example', {
|
|
624
|
-
detail: { example: exampleToLegacy(example) },
|
|
625
|
-
bubbles: true,
|
|
626
|
-
})
|
|
627
|
-
)
|
|
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'
|
|
628
546
|
}
|
|
629
547
|
|
|
630
548
|
selectTjsExample(example: Example) {
|
|
631
|
-
this.
|
|
632
|
-
this.
|
|
633
|
-
this.
|
|
634
|
-
this.
|
|
635
|
-
new CustomEvent('select-tjs-example', {
|
|
636
|
-
detail: { example: exampleToLegacy(example) },
|
|
637
|
-
bubbles: true,
|
|
638
|
-
})
|
|
639
|
-
)
|
|
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'
|
|
640
553
|
}
|
|
641
554
|
|
|
642
555
|
selectTsExample(example: TSExample) {
|
|
643
|
-
this.
|
|
644
|
-
this.
|
|
645
|
-
this.
|
|
646
|
-
this.
|
|
647
|
-
new CustomEvent('select-ts-example', {
|
|
648
|
-
detail: { example: exampleToLegacy(example) },
|
|
649
|
-
bubbles: true,
|
|
650
|
-
})
|
|
651
|
-
)
|
|
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'
|
|
652
560
|
}
|
|
653
561
|
|
|
654
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'
|