pict-docuserve 1.4.0 → 1.4.2
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/dist/pict-docuserve.js +400 -36
- package/dist/pict-docuserve.js.map +1 -1
- package/dist/pict-docuserve.min.js +2 -2
- package/dist/pict-docuserve.min.js.map +1 -1
- package/package.json +5 -7
- package/source/Pict-Application-Docuserve.js +86 -20
- package/source/cli/Docuserve-CLI-Program.js +1 -0
- package/source/cli/commands/Docuserve-Command-StagePlayground.js +279 -0
- package/source/providers/Pict-Provider-Docuserve-Documentation.js +32 -12
- package/source/views/PictView-Docuserve-Section-Playground.js +1472 -0
- package/source/views/PictView-Docuserve-Splash.js +54 -0
|
@@ -0,0 +1,1472 @@
|
|
|
1
|
+
const libPictView = require('pict-view');
|
|
2
|
+
const libPictSectionCode = require('pict-section-code');
|
|
3
|
+
|
|
4
|
+
// CodeJar (used by pict-section-code) ships as an ES module. Browserify
|
|
5
|
+
// can't `require()` it, so we lazy-load from jsDelivr via dynamic import
|
|
6
|
+
// the same way Pict-Docuserve's Fable playground does, then hand the
|
|
7
|
+
// constructor to each editor via connectCodeJarPrototype().
|
|
8
|
+
const _CodeJarCDN = 'https://cdn.jsdelivr.net/npm/codejar@4.2.0/dist/codejar.min.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Docuserve-Section-Playground — a multi-editor + iframe sandbox for
|
|
12
|
+
* trying section configurations (manifests, app configs, AppData, ...)
|
|
13
|
+
* against any pict-section-* UI library and seeing the result live.
|
|
14
|
+
*
|
|
15
|
+
* ┌─────────────────────────────────────────────────────────────────┐
|
|
16
|
+
* │ [Manifest] [Pict] [App] [AppData] [▶ Run] [⤴ Reset] │ toolbar
|
|
17
|
+
* ├─────────────────────────────────────────────────────────────────┤
|
|
18
|
+
* │ │
|
|
19
|
+
* │ pict-section-code editor for the active tab │
|
|
20
|
+
* │ (one per tab; the others are display:none'd, not torn down) │
|
|
21
|
+
* │ │
|
|
22
|
+
* ├══════════════════ resize handle ════════════════════════════════│
|
|
23
|
+
* │ │
|
|
24
|
+
* │ <iframe srcdoc=...> │
|
|
25
|
+
* │ loads pict + the section's UMD + a theme picker, │
|
|
26
|
+
* │ bootstraps an application from the user's edited configs, │
|
|
27
|
+
* │ renders the section. Theme switching is fully scoped to │
|
|
28
|
+
* │ the iframe. │
|
|
29
|
+
* │ │
|
|
30
|
+
* └─────────────────────────────────────────────────────────────────┘
|
|
31
|
+
*
|
|
32
|
+
* The playground is configured per-module via `docs/_playground.json`
|
|
33
|
+
* with `Kind: "section"`. Each entry in the `Editors` array declares
|
|
34
|
+
* one tab (Hash, Label, Language, optional DefaultPath pointing at a
|
|
35
|
+
* starter JSON file under docs/playground/).
|
|
36
|
+
*
|
|
37
|
+
* On Run, the view:
|
|
38
|
+
* 1. Pulls the latest text from each editor's CodeDataAddress.
|
|
39
|
+
* 2. Validates JSON / JS as appropriate (errors surface in a toast
|
|
40
|
+
* and the Run is short-circuited so the iframe doesn't blank out
|
|
41
|
+
* on a typo).
|
|
42
|
+
* 3. Builds the iframe srcdoc from the configured BootstrapTemplate
|
|
43
|
+
* with the verbatim configs inlined as JSON literals.
|
|
44
|
+
* 4. Replaces the iframe's srcdoc. Each Run is a clean slate —
|
|
45
|
+
* no in-place state to invalidate.
|
|
46
|
+
*
|
|
47
|
+
* Edits are persisted to localStorage scoped to `<group>/<module>` so
|
|
48
|
+
* the user's session survives reloads and route navigations.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
// AppData root for the playground state. Per-module: each module's
|
|
52
|
+
// playground reuses the same address (the view re-mounts on every
|
|
53
|
+
// navigation, so there's only one live instance at a time).
|
|
54
|
+
const _AppDataRoot = 'AppData.Docuserve.SectionPlayground';
|
|
55
|
+
const _ContentDestinationId = 'Docuserve-Section-Playground-Container';
|
|
56
|
+
|
|
57
|
+
// Persistence key prefix — final key is
|
|
58
|
+
// `docuserve-section-playground:<group>/<module>:<editorHash>`.
|
|
59
|
+
const _LocalStorageKeyPrefix = 'docuserve-section-playground';
|
|
60
|
+
|
|
61
|
+
const _ViewConfiguration =
|
|
62
|
+
{
|
|
63
|
+
ViewIdentifier: "Docuserve-Section-Playground",
|
|
64
|
+
|
|
65
|
+
DefaultRenderable: "Docuserve-Section-Playground-Content",
|
|
66
|
+
DefaultDestinationAddress: '#Docuserve-Content-Container',
|
|
67
|
+
|
|
68
|
+
AutoRender: false,
|
|
69
|
+
|
|
70
|
+
CSS: /*css*/`
|
|
71
|
+
/* The content container is provisioned by the layout shell as
|
|
72
|
+
flex: 1 1 auto with min-height: 0. Promote it to a flex
|
|
73
|
+
column so the playground inside can use flex: 1 to fill. */
|
|
74
|
+
#Docuserve-Content-Container.docuserve-section-playground-host {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
padding: 0;
|
|
78
|
+
}
|
|
79
|
+
.docuserve-section-playground {
|
|
80
|
+
flex: 1 1 0;
|
|
81
|
+
min-height: 0;
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
color: var(--theme-color-text-primary, #2A241E);
|
|
85
|
+
background: var(--theme-color-background-primary, #FDFBF7);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Toolbar — tabs on the left, action buttons on the right. */
|
|
89
|
+
.docuserve-section-playground-toolbar {
|
|
90
|
+
flex: 0 0 auto;
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: stretch;
|
|
93
|
+
justify-content: space-between;
|
|
94
|
+
gap: 0.5em;
|
|
95
|
+
padding: 0 0.5em;
|
|
96
|
+
background: var(--theme-color-background-panel, #FFFFFF);
|
|
97
|
+
border-bottom: 1px solid var(--theme-color-border-default, #DDD6CA);
|
|
98
|
+
}
|
|
99
|
+
.docuserve-section-playground-tabs {
|
|
100
|
+
display: flex;
|
|
101
|
+
gap: 0;
|
|
102
|
+
}
|
|
103
|
+
.docuserve-section-playground-tab {
|
|
104
|
+
padding: 0.55em 0.95em;
|
|
105
|
+
font-size: 0.85em;
|
|
106
|
+
color: var(--theme-color-text-muted, #8A7F72);
|
|
107
|
+
background: transparent;
|
|
108
|
+
border: 0;
|
|
109
|
+
border-bottom: 2px solid transparent;
|
|
110
|
+
cursor: pointer;
|
|
111
|
+
transition: color 0.12s, border-color 0.12s, background-color 0.12s;
|
|
112
|
+
}
|
|
113
|
+
.docuserve-section-playground-tab:hover {
|
|
114
|
+
color: var(--theme-color-text-primary, #2A241E);
|
|
115
|
+
background: var(--theme-color-background-hover, #EAE3D8);
|
|
116
|
+
}
|
|
117
|
+
.docuserve-section-playground-tab.active {
|
|
118
|
+
color: var(--theme-color-brand-primary, #2E7D74);
|
|
119
|
+
border-bottom-color: var(--theme-color-brand-primary, #2E7D74);
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
}
|
|
122
|
+
.docuserve-section-playground-tab-dirty::after {
|
|
123
|
+
content: '*';
|
|
124
|
+
margin-left: 0.25em;
|
|
125
|
+
color: var(--theme-color-brand-primary, #2E7D74);
|
|
126
|
+
}
|
|
127
|
+
.docuserve-section-playground-actions {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: 0.25em;
|
|
131
|
+
padding: 0.25em 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* Icon button — same visual language as the Fable playground for
|
|
135
|
+
continuity. Run is brighter to draw the eye. */
|
|
136
|
+
.docuserve-section-playground-iconbtn {
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
gap: 0.35em;
|
|
140
|
+
padding: 0.35em 0.7em;
|
|
141
|
+
font-size: 0.82em;
|
|
142
|
+
color: var(--theme-color-text-muted, #8A7F72);
|
|
143
|
+
background: transparent;
|
|
144
|
+
border: 1px solid transparent;
|
|
145
|
+
border-radius: 4px;
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
transition: color 0.12s, background-color 0.12s, border-color 0.12s, opacity 0.12s;
|
|
148
|
+
opacity: 0.75;
|
|
149
|
+
}
|
|
150
|
+
.docuserve-section-playground-iconbtn:hover {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
color: var(--theme-color-brand-primary, #2E7D74);
|
|
153
|
+
background: var(--theme-color-background-hover, #EAE3D8);
|
|
154
|
+
border-color: var(--theme-color-border-default, #DDD6CA);
|
|
155
|
+
}
|
|
156
|
+
.docuserve-section-playground-iconbtn svg {
|
|
157
|
+
width: 1em;
|
|
158
|
+
height: 1em;
|
|
159
|
+
display: block;
|
|
160
|
+
}
|
|
161
|
+
.docuserve-section-playground-iconbtn-run {
|
|
162
|
+
color: var(--theme-color-brand-primary, #2E7D74);
|
|
163
|
+
opacity: 0.9;
|
|
164
|
+
}
|
|
165
|
+
.docuserve-section-playground-iconbtn-run:hover {
|
|
166
|
+
background: var(--theme-color-brand-primary, #2E7D74);
|
|
167
|
+
color: var(--theme-color-background-panel, #FFFFFF);
|
|
168
|
+
border-color: var(--theme-color-brand-primary, #2E7D74);
|
|
169
|
+
opacity: 1;
|
|
170
|
+
}
|
|
171
|
+
.docuserve-section-playground-iconbtn-run svg { fill: currentColor; stroke: none; }
|
|
172
|
+
|
|
173
|
+
/* Body — pict-section-modal shell with the editor stack in the
|
|
174
|
+
center and the iframe sandbox as a resizable + collapsible
|
|
175
|
+
bottom panel. Layout, drag-to-resize, collapse-tab, and
|
|
176
|
+
persistence are all owned by the shell; this view just hosts
|
|
177
|
+
it inside the playground's content area. */
|
|
178
|
+
.docuserve-section-playground-shell-mount {
|
|
179
|
+
flex: 1 1 0;
|
|
180
|
+
min-height: 0;
|
|
181
|
+
position: relative;
|
|
182
|
+
}
|
|
183
|
+
.docuserve-section-playground-shell-mount .pict-modal-shell-host { height: 100%; }
|
|
184
|
+
|
|
185
|
+
/* Editor stack (the shell's center). The tab-slot divs are
|
|
186
|
+
stacked; only the active one is display:flex. pict-section-code
|
|
187
|
+
itself draws the editor surface. */
|
|
188
|
+
.docuserve-section-playground-editor-mount {
|
|
189
|
+
height: 100%;
|
|
190
|
+
min-height: 0;
|
|
191
|
+
display: flex;
|
|
192
|
+
flex-direction: column;
|
|
193
|
+
background: var(--theme-color-background-panel, #FFFFFF);
|
|
194
|
+
}
|
|
195
|
+
.docuserve-section-playground-editor {
|
|
196
|
+
flex: 1 1 0;
|
|
197
|
+
min-height: 0;
|
|
198
|
+
display: none;
|
|
199
|
+
}
|
|
200
|
+
.docuserve-section-playground-editor.active {
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
}
|
|
204
|
+
.docuserve-section-playground-editor > * {
|
|
205
|
+
flex: 1 1 0;
|
|
206
|
+
min-height: 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Iframe pane — the rendered section + its theme switcher.
|
|
210
|
+
Lives inside the shell's bottom panel; the panel owns its
|
|
211
|
+
own border + sizing chrome. */
|
|
212
|
+
.docuserve-section-playground-iframe-pane {
|
|
213
|
+
height: 100%;
|
|
214
|
+
min-height: 0;
|
|
215
|
+
position: relative;
|
|
216
|
+
background: var(--theme-color-background-secondary, #F6F3EE);
|
|
217
|
+
}
|
|
218
|
+
.docuserve-section-playground-iframe {
|
|
219
|
+
width: 100%;
|
|
220
|
+
height: 100%;
|
|
221
|
+
border: 0;
|
|
222
|
+
background: var(--theme-color-background-panel, #FFFFFF);
|
|
223
|
+
}
|
|
224
|
+
.docuserve-section-playground-status {
|
|
225
|
+
position: absolute;
|
|
226
|
+
top: 0.5em;
|
|
227
|
+
right: 0.7em;
|
|
228
|
+
font-size: 0.7em;
|
|
229
|
+
color: var(--theme-color-text-muted, #8A7F72);
|
|
230
|
+
background: var(--theme-color-background-panel, #FFFFFF);
|
|
231
|
+
padding: 0.15em 0.5em;
|
|
232
|
+
border-radius: 4px;
|
|
233
|
+
border: 1px solid var(--theme-color-border-default, #DDD6CA);
|
|
234
|
+
pointer-events: none;
|
|
235
|
+
opacity: 0;
|
|
236
|
+
transition: opacity 0.2s;
|
|
237
|
+
}
|
|
238
|
+
.docuserve-section-playground-status.show { opacity: 0.85; }
|
|
239
|
+
.docuserve-section-playground-status.error {
|
|
240
|
+
color: var(--theme-color-status-error, #B43A2E);
|
|
241
|
+
border-color: var(--theme-color-status-error, #B43A2E);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Empty state for the iframe pane before the first Run. */
|
|
245
|
+
.docuserve-section-playground-emptystate {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
align-items: center;
|
|
249
|
+
justify-content: center;
|
|
250
|
+
height: 100%;
|
|
251
|
+
color: var(--theme-color-text-muted, #8A7F72);
|
|
252
|
+
font-size: 0.9em;
|
|
253
|
+
text-align: center;
|
|
254
|
+
padding: 2em;
|
|
255
|
+
}
|
|
256
|
+
.docuserve-section-playground-emptystate-title {
|
|
257
|
+
font-size: 1.1em;
|
|
258
|
+
font-weight: 600;
|
|
259
|
+
color: var(--theme-color-text-secondary, #5E5549);
|
|
260
|
+
margin-bottom: 0.4em;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/* Wider middle-positioned collapse tab on the sandbox panel —
|
|
264
|
+
replaces the stock 28×6 right-anchored sliver with a labelled
|
|
265
|
+
"Sandbox" pill centered above the panel's top edge. The pill
|
|
266
|
+
is wide enough to read at a glance and lives at the boundary
|
|
267
|
+
between editor and sandbox so the user's eye finds it
|
|
268
|
+
immediately. */
|
|
269
|
+
.docuserve-section-playground-shell-mount .pict-modal-shell-panel-bottom > .pict-modal-shell-panel-collapse-tab
|
|
270
|
+
{
|
|
271
|
+
/* Geometry: wide pill positioned fully ABOVE the panel's
|
|
272
|
+
top edge (matches the section-modal default of "tab
|
|
273
|
+
entirely outside the panel"; top = -height places the
|
|
274
|
+
tab's bottom edge flush against the panel boundary). */
|
|
275
|
+
width: 160px;
|
|
276
|
+
height: 18px;
|
|
277
|
+
left: 50%;
|
|
278
|
+
right: auto;
|
|
279
|
+
top: -18px;
|
|
280
|
+
margin-left: -80px;
|
|
281
|
+
border-radius: 5px 5px 0 0;
|
|
282
|
+
border-bottom: 0;
|
|
283
|
+
/* Visual: always readable, not just on hover. */
|
|
284
|
+
opacity: 0.95;
|
|
285
|
+
color: var(--theme-color-text-secondary, #5E5549);
|
|
286
|
+
background: var(--theme-color-background-secondary, #F6F3EE);
|
|
287
|
+
border-color: var(--theme-color-border-default, #DDD6CA);
|
|
288
|
+
padding: 0 10px;
|
|
289
|
+
gap: 6px;
|
|
290
|
+
line-height: 16px;
|
|
291
|
+
font-size: 10px;
|
|
292
|
+
font-weight: 600;
|
|
293
|
+
letter-spacing: 0.08em;
|
|
294
|
+
text-transform: uppercase;
|
|
295
|
+
}
|
|
296
|
+
/* Title text always visible (the stock CSS only shows it when
|
|
297
|
+
collapsed); the chevron pseudo is hidden — the label carries
|
|
298
|
+
the affordance. */
|
|
299
|
+
.docuserve-section-playground-shell-mount .pict-modal-shell-panel-bottom > .pict-modal-shell-panel-collapse-tab .pict-modal-shell-panel-collapse-tab-title
|
|
300
|
+
{
|
|
301
|
+
display: inline;
|
|
302
|
+
}
|
|
303
|
+
.docuserve-section-playground-shell-mount .pict-modal-shell-panel-bottom > .pict-modal-shell-panel-collapse-tab::before
|
|
304
|
+
{
|
|
305
|
+
display: none;
|
|
306
|
+
}
|
|
307
|
+
/* Keep the size stable on hover (stock CSS grows it to 36×18) —
|
|
308
|
+
only color shifts so the user knows it's interactive. */
|
|
309
|
+
.docuserve-section-playground-shell-mount .pict-modal-shell-panel-bottom:hover > .pict-modal-shell-panel-collapse-tab,
|
|
310
|
+
.docuserve-section-playground-shell-mount .pict-modal-shell-panel-bottom > .pict-modal-shell-panel-collapse-tab:hover
|
|
311
|
+
{
|
|
312
|
+
width: 160px;
|
|
313
|
+
height: 18px;
|
|
314
|
+
top: -18px;
|
|
315
|
+
margin-left: -80px;
|
|
316
|
+
opacity: 1;
|
|
317
|
+
color: var(--theme-color-brand-primary, #2E7D74);
|
|
318
|
+
border-color: var(--theme-color-brand-primary, #2E7D74);
|
|
319
|
+
}
|
|
320
|
+
`,
|
|
321
|
+
|
|
322
|
+
CSSPriority: 500,
|
|
323
|
+
|
|
324
|
+
Templates:
|
|
325
|
+
[
|
|
326
|
+
{
|
|
327
|
+
Hash: "Docuserve-Section-Playground-Template",
|
|
328
|
+
Template: /*html*/`
|
|
329
|
+
<div class="docuserve-section-playground">
|
|
330
|
+
<div class="docuserve-section-playground-toolbar">
|
|
331
|
+
<div class="docuserve-section-playground-tabs" id="Docuserve-Section-Playground-Tabs">
|
|
332
|
+
{~TS:Docuserve-Section-Playground-Tab-Template:AppData.Docuserve.SectionPlayground.Editors~}
|
|
333
|
+
</div>
|
|
334
|
+
<div class="docuserve-section-playground-actions">
|
|
335
|
+
<button type="button" class="docuserve-section-playground-iconbtn"
|
|
336
|
+
title="Reset all editors to their starter content"
|
|
337
|
+
onclick="{~P~}.views['Docuserve-Section-Playground'].resetAll()">
|
|
338
|
+
{~I:Refresh~} Reset
|
|
339
|
+
</button>
|
|
340
|
+
<button type="button" class="docuserve-section-playground-iconbtn docuserve-section-playground-iconbtn-run"
|
|
341
|
+
title="Run — reload the iframe with the current editor contents"
|
|
342
|
+
onclick="{~P~}.views['Docuserve-Section-Playground'].run()">
|
|
343
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><polygon points="6 4 20 12 6 20"/></svg>
|
|
344
|
+
Run
|
|
345
|
+
</button>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
<!-- pict-section-modal shell mount. _mountAndRender() calls
|
|
349
|
+
modal.shell(thisDiv) and addPanel() for the bottom sandbox
|
|
350
|
+
panel + center() for the editor stack. The shell builds its
|
|
351
|
+
own destination divs (#Section-Playground-Editor-Mount and
|
|
352
|
+
#Section-Playground-Iframe-Mount) inside this wrapper. -->
|
|
353
|
+
<div class="docuserve-section-playground-shell-mount" id="Docuserve-Section-Playground-Shell-Mount"></div>
|
|
354
|
+
</div>`
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
Hash: "Docuserve-Section-Playground-Tab-Template",
|
|
358
|
+
Template: /*html*/`<button type="button"
|
|
359
|
+
class="docuserve-section-playground-tab{~D:Record.ActiveClass~}"
|
|
360
|
+
onclick="{~P~}.views['Docuserve-Section-Playground'].selectTab('{~D:Record.Hash~}')"
|
|
361
|
+
>{~D:Record.Label~}</button>`
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
Hash: "Docuserve-Section-Playground-Editor-Slot-Template",
|
|
365
|
+
Template: /*html*/`<div class="docuserve-section-playground-editor{~D:Record.ActiveClass~}"
|
|
366
|
+
id="Docuserve-Section-Playground-Editor-{~D:Record.Hash~}"
|
|
367
|
+
data-editor-hash="{~D:Record.Hash~}"></div>`
|
|
368
|
+
}
|
|
369
|
+
],
|
|
370
|
+
|
|
371
|
+
Renderables:
|
|
372
|
+
[
|
|
373
|
+
{
|
|
374
|
+
RenderableHash: "Docuserve-Section-Playground-Content",
|
|
375
|
+
TemplateHash: "Docuserve-Section-Playground-Template",
|
|
376
|
+
ContentDestinationAddress: "#Docuserve-Content-Container",
|
|
377
|
+
RenderMethod: "replace"
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
383
|
+
// Iframe srcdoc — the page loaded inside the sandbox iframe.
|
|
384
|
+
//
|
|
385
|
+
// Loads the runtime pieces from jsDelivr @1 (so the playground tracks
|
|
386
|
+
// the same major version as the docs site), wires up an instance of
|
|
387
|
+
// the configured `SectionType`, mounts a theme picker, and renders.
|
|
388
|
+
// Configs are inlined as JSON literals at srcdoc build time — no
|
|
389
|
+
// postMessage required.
|
|
390
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
391
|
+
function buildIframeSrcdoc(pConfig, pSpec, pBaseURL)
|
|
392
|
+
{
|
|
393
|
+
// Defaults if the per-module _playground.json doesn't override.
|
|
394
|
+
let tmpDefaults =
|
|
395
|
+
{
|
|
396
|
+
SectionType: 'pict-section-form',
|
|
397
|
+
ApplicationGlobal: 'PictFormApplication',
|
|
398
|
+
ApplicationModule: 'PictSectionForm',
|
|
399
|
+
ManifestKey: 'DefaultFormManifest',
|
|
400
|
+
// WrapperKind controls whether the resolved class is treated as a
|
|
401
|
+
// PictApplication subclass (default) or as a PictView subclass that
|
|
402
|
+
// the bootstrap will wrap in a synthesized PictApplication.
|
|
403
|
+
// - "application": `window[ApplicationModule][ApplicationGlobal]`
|
|
404
|
+
// already IS the PictApplication. (pict-section-form's pattern.)
|
|
405
|
+
// - "view": the resolved class is a PictView; the bootstrap
|
|
406
|
+
// synthesizes a wrapper PictApplication that registers it under
|
|
407
|
+
// `ViewName` with config from `pictConfig[ViewConfigKey]`. This
|
|
408
|
+
// is the path most UI-control modules take — they ship a view,
|
|
409
|
+
// not an application, and don't need to author a wrapper class.
|
|
410
|
+
WrapperKind: 'application',
|
|
411
|
+
ViewName: 'Section-Playground-View',
|
|
412
|
+
ViewConfigKey: 'ViewConfig',
|
|
413
|
+
// Optional DOM id where the section should mount. When set, the
|
|
414
|
+
// iframe template includes <div id="<MountID>"></div> next to the
|
|
415
|
+
// default #Section-Playground-Mount, AND the wrapper-synthesizer's
|
|
416
|
+
// auto-target uses it instead of #Section-Playground-Mount. Lets
|
|
417
|
+
// sections whose DefaultDestinationAddress doesn't already match
|
|
418
|
+
// either the iframe slots or #Section-Playground-Mount declare
|
|
419
|
+
// their own ID without overriding Renderables by hand.
|
|
420
|
+
MountID: '',
|
|
421
|
+
// Optional method on the view to call once after initialization
|
|
422
|
+
// with seeded data. Use this for sections whose data is loaded
|
|
423
|
+
// imperatively (e.g. pict-editor-timeline's `loadStoryboard`,
|
|
424
|
+
// pict-section-equation's `setSolveResult`) rather than via a
|
|
425
|
+
// `<X>DataAddress` config option. The value at
|
|
426
|
+
// `BootstrapSeedAddress` (in `pict.AppData`) is the argument.
|
|
427
|
+
BootstrapMethod: '',
|
|
428
|
+
BootstrapSeedAddress: '',
|
|
429
|
+
// Imports — each one is { Name, Source: 'cdn'|'bundled'|'local'|'esm', Version?, Path?, URL?, GlobalName?, ExportName? }.
|
|
430
|
+
// Loading shapes:
|
|
431
|
+
// cdn — <script src="https://cdn.jsdelivr.net/npm/<Name>@<Version>/dist/<Name>.min.js"></script>
|
|
432
|
+
// local — <script src="<Path>"></script>; Path resolves against the docs root
|
|
433
|
+
// esm — <script type="module">import { <ExportName> } from "<URL>"; window["<GlobalName>"] = <ExportName>;</script>
|
|
434
|
+
// Use for ES-module-only packages (CodeJar 4.x, etc.) that
|
|
435
|
+
// can't be loaded via plain <script src>. Bootstrap waits
|
|
436
|
+
// on `window.<GlobalName>` before running the application.
|
|
437
|
+
// Order matters: pict first, then anything that depends on it
|
|
438
|
+
// (pict-application before any wrapper that needs synthesis),
|
|
439
|
+
// then the section module last.
|
|
440
|
+
Imports: [],
|
|
441
|
+
// Stylesheets — each is { Source: 'cdn'|'local', Name?, Version?, Path? }.
|
|
442
|
+
// Emitted as <link rel="stylesheet"> tags in the iframe head. Used by
|
|
443
|
+
// sections that wrap external libraries with CSS (Toast UI Grid, KaTeX,
|
|
444
|
+
// Mermaid pre-styled themes, …) so module authors don't have to inject
|
|
445
|
+
// <link> tags from Application Code. Local sources are staged by the
|
|
446
|
+
// `stage-playground` command alongside Imports.
|
|
447
|
+
Stylesheets: []
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
let tmpSpec = Object.assign({}, tmpDefaults, pSpec || {});
|
|
451
|
+
|
|
452
|
+
// Default Imports if none provided — minimum needed for a pict-section-form
|
|
453
|
+
// playground. Most callers will override this in _playground.json.
|
|
454
|
+
let tmpImports = (tmpSpec.Imports && tmpSpec.Imports.length > 0) ? tmpSpec.Imports :
|
|
455
|
+
[
|
|
456
|
+
{ Name: 'pict', Source: 'cdn' },
|
|
457
|
+
{ Name: 'pict-application', Source: 'cdn' },
|
|
458
|
+
{ Name: 'pict-section-form', Source: 'cdn' },
|
|
459
|
+
{ Name: 'pict-section-modal', Source: 'cdn' },
|
|
460
|
+
{ Name: 'pict-section-theme', Source: 'cdn' }
|
|
461
|
+
];
|
|
462
|
+
|
|
463
|
+
// Build script + stylesheet tags for every Import / Stylesheet.
|
|
464
|
+
//
|
|
465
|
+
// Imports — four sources:
|
|
466
|
+
// cdn — jsDelivr URL built from Name + Version
|
|
467
|
+
// local — Path relative to the docs root (resolved via <base href> tag
|
|
468
|
+
// we emit below, so the iframe's about:srcdoc origin doesn't
|
|
469
|
+
// break relative paths)
|
|
470
|
+
// esm — ES-module dynamic import emitted as <script type="module">; the
|
|
471
|
+
// bootstrap waits on `window[GlobalName]` before running the app.
|
|
472
|
+
// bundled — legacy alias, treated as cdn for compatibility.
|
|
473
|
+
//
|
|
474
|
+
// Stylesheets — same Source values minus 'esm' (CSS has no ESM concept).
|
|
475
|
+
let tmpScriptTags = '';
|
|
476
|
+
let tmpESMImports = [];
|
|
477
|
+
for (let i = 0; i < tmpImports.length; i++)
|
|
478
|
+
{
|
|
479
|
+
let tmpImport = tmpImports[i];
|
|
480
|
+
if (tmpImport.Source === 'esm')
|
|
481
|
+
{
|
|
482
|
+
// Defer to a single coalesced <script type="module"> at the
|
|
483
|
+
// bottom so we can wait on all ESM globals before app init.
|
|
484
|
+
tmpESMImports.push(tmpImport);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
let tmpSrc;
|
|
488
|
+
if (tmpImport.Source === 'local')
|
|
489
|
+
{
|
|
490
|
+
tmpSrc = tmpImport.Path;
|
|
491
|
+
}
|
|
492
|
+
else
|
|
493
|
+
{
|
|
494
|
+
let tmpVersion = tmpImport.Version || '1';
|
|
495
|
+
tmpSrc = 'https://cdn.jsdelivr.net/npm/' + tmpImport.Name + '@' + tmpVersion
|
|
496
|
+
+ '/dist/' + tmpImport.Name + '.min.js';
|
|
497
|
+
}
|
|
498
|
+
tmpScriptTags += '<script src="' + tmpSrc + '"></script>\n';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Stylesheet <link> tags — emitted into <head> before the script tags so
|
|
502
|
+
// the section's first paint already has its external styles applied.
|
|
503
|
+
let tmpStylesheets = Array.isArray(tmpSpec.Stylesheets) ? tmpSpec.Stylesheets : [];
|
|
504
|
+
let tmpLinkTags = '';
|
|
505
|
+
for (let i = 0; i < tmpStylesheets.length; i++)
|
|
506
|
+
{
|
|
507
|
+
let tmpStyle = tmpStylesheets[i];
|
|
508
|
+
let tmpHref;
|
|
509
|
+
if (tmpStyle.Source === 'local')
|
|
510
|
+
{
|
|
511
|
+
tmpHref = tmpStyle.Path;
|
|
512
|
+
}
|
|
513
|
+
else
|
|
514
|
+
{
|
|
515
|
+
let tmpVersion = tmpStyle.Version || '1';
|
|
516
|
+
let tmpStylePath = tmpStyle.Path || ('dist/' + tmpStyle.Name + '.min.css');
|
|
517
|
+
tmpHref = 'https://cdn.jsdelivr.net/npm/' + tmpStyle.Name + '@' + tmpVersion + '/' + tmpStylePath;
|
|
518
|
+
}
|
|
519
|
+
tmpLinkTags += '<link rel="stylesheet" href="' + tmpHref + '">\n';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ESM imports — coalesced into one <script type="module"> that imports
|
|
523
|
+
// each module, stamps the named export onto window[GlobalName], and
|
|
524
|
+
// signals readiness via a single flag the bootstrap waits on.
|
|
525
|
+
let tmpESMScript = '';
|
|
526
|
+
if (tmpESMImports.length > 0)
|
|
527
|
+
{
|
|
528
|
+
tmpESMScript += '<script type="module">\n';
|
|
529
|
+
tmpESMScript += 'window.__SectionPlaygroundESMReady = (async () => {\n';
|
|
530
|
+
for (let i = 0; i < tmpESMImports.length; i++)
|
|
531
|
+
{
|
|
532
|
+
let tmpESM = tmpESMImports[i];
|
|
533
|
+
let tmpURL = tmpESM.URL;
|
|
534
|
+
if (!tmpURL && tmpESM.Name)
|
|
535
|
+
{
|
|
536
|
+
let tmpVersion = tmpESM.Version || '1';
|
|
537
|
+
tmpURL = 'https://cdn.jsdelivr.net/npm/' + tmpESM.Name + '@' + tmpVersion + '/dist/' + tmpESM.Name + '.min.js';
|
|
538
|
+
}
|
|
539
|
+
let tmpExportName = tmpESM.ExportName || tmpESM.GlobalName || tmpESM.Name;
|
|
540
|
+
let tmpGlobalName = tmpESM.GlobalName || tmpESM.ExportName || tmpESM.Name;
|
|
541
|
+
tmpESMScript += ' try {\n';
|
|
542
|
+
tmpESMScript += ' const mod = await import(' + JSON.stringify(tmpURL) + ');\n';
|
|
543
|
+
tmpESMScript += ' window[' + JSON.stringify(tmpGlobalName) + '] = mod[' + JSON.stringify(tmpExportName) + '] || mod.default || mod;\n';
|
|
544
|
+
tmpESMScript += ' } catch (err) {\n';
|
|
545
|
+
tmpESMScript += ' console.error("ESM import failed for " + ' + JSON.stringify(tmpURL) + ', err);\n';
|
|
546
|
+
tmpESMScript += ' throw err;\n';
|
|
547
|
+
tmpESMScript += ' }\n';
|
|
548
|
+
}
|
|
549
|
+
tmpESMScript += '})();\n';
|
|
550
|
+
tmpESMScript += '</script>\n';
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Caller passes an absolute base URL so local Imports + asset paths
|
|
554
|
+
// resolve against the parent's docs root. An about:srcdoc iframe
|
|
555
|
+
// has no base of its own — without the <base> tag, "playground/runtime/
|
|
556
|
+
// pict.min.js" 404s as "about:srcdoc/playground/runtime/pict.min.js".
|
|
557
|
+
let tmpBaseHref = pBaseURL || '';
|
|
558
|
+
|
|
559
|
+
// JSON-encode each user config. We embed them verbatim in a <script>
|
|
560
|
+
// block so the iframe boots from in-memory literals — no need for the
|
|
561
|
+
// iframe to call back to the parent.
|
|
562
|
+
function encode(pValue)
|
|
563
|
+
{
|
|
564
|
+
// JSON.stringify of undefined → undefined; coerce to empty object.
|
|
565
|
+
let tmpEncoded = JSON.stringify(pValue === undefined ? {} : pValue);
|
|
566
|
+
// Defang </script> just in case a user pastes one in.
|
|
567
|
+
return tmpEncoded.replace(/<\/script>/g, '<\\/script>');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let tmpManifestJSON = encode(pConfig.manifest);
|
|
571
|
+
let tmpPictConfigJSON = encode(pConfig.pictConfig);
|
|
572
|
+
let tmpAppConfigJSON = encode(pConfig.appConfig);
|
|
573
|
+
let tmpAppDataJSON = encode(pConfig.appData);
|
|
574
|
+
|
|
575
|
+
// Application Code editor — optional raw JS that runs as the body
|
|
576
|
+
// of a function `(Base) => class`. Encoded as a JSON string so it
|
|
577
|
+
// survives the srcdoc round-trip; the iframe wraps it with
|
|
578
|
+
// `new Function('Base', source)` and expects a class back.
|
|
579
|
+
let tmpApplicationJS = (typeof pConfig.application === 'string') ? pConfig.application : '';
|
|
580
|
+
let tmpApplicationJSON = JSON.stringify(tmpApplicationJS).replace(/<\/script>/g, '<\\/script>');
|
|
581
|
+
|
|
582
|
+
// HTML. Theme picker is rendered into a slim topbar inside the iframe
|
|
583
|
+
// so the user can switch themes without leaving the page. The section
|
|
584
|
+
// itself mounts into the main content div below it.
|
|
585
|
+
return '<!DOCTYPE html>\n'
|
|
586
|
+
+ '<html lang="en">\n'
|
|
587
|
+
+ '<head>\n'
|
|
588
|
+
+ '<meta charset="UTF-8">\n'
|
|
589
|
+
+ '<title>Section Playground</title>\n'
|
|
590
|
+
+ (tmpBaseHref ? '<base href="' + tmpBaseHref + '">\n' : '')
|
|
591
|
+
+ '<style>\n'
|
|
592
|
+
+ ' html, body { height: 100%; margin: 0; }\n'
|
|
593
|
+
+ ' body { display: flex; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }\n'
|
|
594
|
+
+ ' #playground-topbar { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 14px; border-bottom: 1px solid var(--theme-color-border-default, #DDD6CA); background: var(--theme-color-background-panel, #FFFFFF); }\n'
|
|
595
|
+
+ ' #playground-topbar-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--theme-color-text-muted, #8A7F72); }\n'
|
|
596
|
+
+ ' #playground-topbar-controls { display: flex; align-items: center; gap: 8px; }\n'
|
|
597
|
+
+ ' #playground-content { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 16px; background: var(--theme-color-background-primary, #FDFBF7); }\n'
|
|
598
|
+
+ ' #playground-error { display: none; padding: 14px 18px; background: #FFF4F2; color: #B43A2E; border-bottom: 1px solid #B43A2E; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; white-space: pre-wrap; }\n'
|
|
599
|
+
+ ' #playground-error.show { display: block; }\n'
|
|
600
|
+
+ '</style>\n'
|
|
601
|
+
+ tmpLinkTags
|
|
602
|
+
+ tmpScriptTags
|
|
603
|
+
+ tmpESMScript
|
|
604
|
+
+ '</head>\n'
|
|
605
|
+
+ '<body>\n'
|
|
606
|
+
+ '<div id="playground-error"></div>\n'
|
|
607
|
+
+ '<div id="playground-topbar">\n'
|
|
608
|
+
+ ' <div id="playground-topbar-title">Section Sandbox</div>\n'
|
|
609
|
+
+ ' <div id="playground-topbar-controls">\n'
|
|
610
|
+
+ ' <div id="Theme-Picker"></div>\n'
|
|
611
|
+
+ ' <div id="Theme-ModeToggle"></div>\n'
|
|
612
|
+
+ ' <div id="Theme-ScaleSelect"></div>\n'
|
|
613
|
+
+ ' </div>\n'
|
|
614
|
+
+ '</div>\n'
|
|
615
|
+
+ '<div id="playground-content">\n'
|
|
616
|
+
+ ' <!-- The section-form metacontroller renders into #Pict-Form-Container by default. -->\n'
|
|
617
|
+
+ ' <!-- Other section types may use a different default; the spec drives the markup. -->\n'
|
|
618
|
+
+ ' <div id="Pict-Form-Container"></div>\n'
|
|
619
|
+
+ ' <div id="Section-Playground-Mount"></div>\n'
|
|
620
|
+
+ (tmpSpec.MountID ? ' <div id="' + tmpSpec.MountID + '"></div>\n' : '')
|
|
621
|
+
+ '</div>\n'
|
|
622
|
+
+ '<script>\n'
|
|
623
|
+
+ '(function() {\n'
|
|
624
|
+
+ ' var manifest = ' + tmpManifestJSON + ';\n'
|
|
625
|
+
+ ' var pictConfig = ' + tmpPictConfigJSON + ';\n'
|
|
626
|
+
+ ' var appConfig = ' + tmpAppConfigJSON + ';\n'
|
|
627
|
+
+ ' var appData = ' + tmpAppDataJSON + ';\n'
|
|
628
|
+
+ ' var applicationSource = ' + tmpApplicationJSON + ';\n'
|
|
629
|
+
+ ' var sectionType = ' + JSON.stringify(tmpSpec.SectionType) + ';\n'
|
|
630
|
+
+ ' var applicationModule = ' + JSON.stringify(tmpSpec.ApplicationModule) + ';\n'
|
|
631
|
+
+ ' var applicationGlobal = ' + JSON.stringify(tmpSpec.ApplicationGlobal) + ';\n'
|
|
632
|
+
+ ' var manifestKey = ' + JSON.stringify(tmpSpec.ManifestKey) + ';\n'
|
|
633
|
+
+ ' var wrapperKind = ' + JSON.stringify(tmpSpec.WrapperKind) + ';\n'
|
|
634
|
+
+ ' var viewName = ' + JSON.stringify(tmpSpec.ViewName) + ';\n'
|
|
635
|
+
+ ' var viewConfigKey = ' + JSON.stringify(tmpSpec.ViewConfigKey) + ';\n'
|
|
636
|
+
+ ' var mountID = ' + JSON.stringify(tmpSpec.MountID) + ';\n'
|
|
637
|
+
+ ' var bootstrapMethod = ' + JSON.stringify(tmpSpec.BootstrapMethod) + ';\n'
|
|
638
|
+
+ ' var bootstrapSeedAddr = ' + JSON.stringify(tmpSpec.BootstrapSeedAddress) + ';\n'
|
|
639
|
+
+ '\n'
|
|
640
|
+
+ ' function showError(msg) {\n'
|
|
641
|
+
+ ' var el = document.getElementById("playground-error");\n'
|
|
642
|
+
+ ' if (el) { el.textContent = msg; el.classList.add("show"); }\n'
|
|
643
|
+
+ ' try { parent.postMessage({ type: "section-playground-error", message: msg }, "*"); } catch(e) {}\n'
|
|
644
|
+
+ ' }\n'
|
|
645
|
+
+ '\n'
|
|
646
|
+
+ ' function ready(fn) {\n'
|
|
647
|
+
+ ' if (document.readyState !== "loading") { fn(); }\n'
|
|
648
|
+
+ ' else { document.addEventListener("DOMContentLoaded", fn); }\n'
|
|
649
|
+
+ ' }\n'
|
|
650
|
+
+ '\n'
|
|
651
|
+
+ ' function awaitESM() {\n'
|
|
652
|
+
+ ' return (window.__SectionPlaygroundESMReady && typeof window.__SectionPlaygroundESMReady.then === "function")\n'
|
|
653
|
+
+ ' ? window.__SectionPlaygroundESMReady\n'
|
|
654
|
+
+ ' : Promise.resolve();\n'
|
|
655
|
+
+ ' }\n'
|
|
656
|
+
+ '\n'
|
|
657
|
+
+ ' ready(function() {\n'
|
|
658
|
+
+ ' awaitESM().then(function() { try {\n'
|
|
659
|
+
+ ' if (typeof Pict === "undefined") { throw new Error("pict bundle did not load — check the CDN imports in _playground.json"); }\n'
|
|
660
|
+
+ ' var libSectionModule = window[applicationModule];\n'
|
|
661
|
+
+ ' if (!libSectionModule) { throw new Error("Section module " + applicationModule + " is not loaded; check the Imports in _playground.json"); }\n'
|
|
662
|
+
+ ' var ResolvedClass = libSectionModule[applicationGlobal] || libSectionModule;\n'
|
|
663
|
+
+ ' if (typeof ResolvedClass !== "function") { throw new Error("Could not resolve " + applicationGlobal + " on " + applicationModule); }\n'
|
|
664
|
+
+ '\n'
|
|
665
|
+
+ ' // Wrapper resolution. Two paths:\n'
|
|
666
|
+
+ ' // 1. WrapperKind: "application" (default) — the resolved class\n'
|
|
667
|
+
+ ' // is already a PictApplication subclass; use it directly.\n'
|
|
668
|
+
+ ' // pict-section-form\'s pattern.\n'
|
|
669
|
+
+ ' // 2. WrapperKind: "view" — the resolved class is a PictView; the\n'
|
|
670
|
+
+ ' // bootstrap synthesizes a PictApplication subclass that\n'
|
|
671
|
+
+ ' // registers the view under `viewName` with config drawn\n'
|
|
672
|
+
+ ' // from pictConfig[viewConfigKey]. This is the no-wrapper\n'
|
|
673
|
+
+ ' // path: section modules ship just their view class and a\n'
|
|
674
|
+
+ ' // _playground.json — no per-module Application file needed.\n'
|
|
675
|
+
+ ' var BaseApplicationClass;\n'
|
|
676
|
+
+ ' if (wrapperKind === "view") {\n'
|
|
677
|
+
+ ' if (typeof window.PictApplication !== "function") { throw new Error("WrapperKind: \\"view\\" requires pict-application to be loaded — add it to Imports in _playground.json"); }\n'
|
|
678
|
+
+ ' var ViewClass = ResolvedClass;\n'
|
|
679
|
+
+ ' // PictApplication is an ES6 class, so the wrapper MUST be one\n'
|
|
680
|
+
+ ' // too — ES6 classes throw "cannot be invoked without new" when\n'
|
|
681
|
+
+ ' // called via `.call(this, ...)`, so the prototype-style pattern\n'
|
|
682
|
+
+ ' // (function + Object.create) silently fails at construction.\n'
|
|
683
|
+
+ ' // Build the class via a Function constructor so we can keep\n'
|
|
684
|
+
+ ' // closure access to viewName / viewConfigKey / mountID / etc.\n'
|
|
685
|
+
+ ' var BuildWrapperClass = new Function(\n'
|
|
686
|
+
+ ' "PictApplication", "ViewClass", "pictConfig", "viewName", "viewConfigKey", "mountID", "bootstrapMethod", "bootstrapSeedAddr",\n'
|
|
687
|
+
+ ' "return class ViewWrapperApplication extends PictApplication {"\n'
|
|
688
|
+
+ ' + " constructor(pFable, pOptions, pServiceHash) {"\n'
|
|
689
|
+
+ ' + " super(pFable, pOptions, pServiceHash);"\n'
|
|
690
|
+
+ ' + " var tmpDefaultViewConfig = (ViewClass.default_configuration || {});"\n'
|
|
691
|
+
+ ' + " var tmpInjectedViewConfig = (pictConfig && pictConfig[viewConfigKey]) || {};"\n'
|
|
692
|
+
+ ' + " var tmpMergedViewConfig = Object.assign({}, tmpDefaultViewConfig, tmpInjectedViewConfig);"\n'
|
|
693
|
+
+ ' + " var tmpAutoMount = \\"#\\" + (mountID || \\"Section-Playground-Mount\\");"\n'
|
|
694
|
+
+ ' + " if (typeof tmpInjectedViewConfig.DefaultDestinationAddress === \\"undefined\\") {"\n'
|
|
695
|
+
+ ' + " tmpMergedViewConfig.DefaultDestinationAddress = tmpAutoMount;"\n'
|
|
696
|
+
+ ' + " if (Array.isArray(tmpDefaultViewConfig.Renderables)) {"\n'
|
|
697
|
+
+ ' + " tmpMergedViewConfig.Renderables = tmpDefaultViewConfig.Renderables.map(function(pRenderable) {"\n'
|
|
698
|
+
+ ' + " return Object.assign({}, pRenderable, { DestinationAddress: tmpAutoMount });"\n'
|
|
699
|
+
+ ' + " });"\n'
|
|
700
|
+
+ ' + " }"\n'
|
|
701
|
+
+ ' + " }"\n'
|
|
702
|
+
+ ' + " this.pict.addView(viewName, tmpMergedViewConfig, ViewClass);"\n'
|
|
703
|
+
+ ' + " }"\n'
|
|
704
|
+
+ ' + " onAfterInitialize() {"\n'
|
|
705
|
+
+ ' + " super.onAfterInitialize();"\n'
|
|
706
|
+
+ ' + " var tmpView = this.pict.views[viewName];"\n'
|
|
707
|
+
+ ' + " if (bootstrapMethod && tmpView && typeof tmpView[bootstrapMethod] === \\"function\\") {"\n'
|
|
708
|
+
+ ' + " try {"\n'
|
|
709
|
+
+ ' + " var tmpSeed;"\n'
|
|
710
|
+
+ ' + " if (bootstrapSeedAddr && this.pict && this.pict.manifest && typeof this.pict.manifest.getValueByHash === \\"function\\") {"\n'
|
|
711
|
+
+ ' + " tmpSeed = this.pict.manifest.getValueByHash(this.pict.AppData, bootstrapSeedAddr);"\n'
|
|
712
|
+
+ ' + " }"\n'
|
|
713
|
+
+ ' + " tmpView[bootstrapMethod](tmpSeed);"\n'
|
|
714
|
+
+ ' + " } catch (seedErr) { console.warn(\\"BootstrapMethod \\" + bootstrapMethod + \\" threw:\\", seedErr); }"\n'
|
|
715
|
+
+ ' + " }"\n'
|
|
716
|
+
+ ' + " if (tmpView && typeof tmpView.render === \\"function\\") { tmpView.render(); }"\n'
|
|
717
|
+
+ ' + " }"\n'
|
|
718
|
+
+ ' + "};"\n'
|
|
719
|
+
+ ' );\n'
|
|
720
|
+
+ ' BaseApplicationClass = BuildWrapperClass(window.PictApplication, ViewClass, pictConfig, viewName, viewConfigKey, mountID, bootstrapMethod, bootstrapSeedAddr);\n'
|
|
721
|
+
+ ' // Carry through whatever defaults the view itself ships, so\n'
|
|
722
|
+
+ ' // pict_configuration / Product / Hash stay sensible if the\n'
|
|
723
|
+
+ ' // user has not provided their own appConfig / pictConfig.\n'
|
|
724
|
+
+ ' BaseApplicationClass.default_configuration = (ViewClass.default_configuration && ViewClass.default_configuration.pict_configuration)\n'
|
|
725
|
+
+ ' ? ViewClass.default_configuration\n'
|
|
726
|
+
+ ' : { pict_configuration: {} };\n'
|
|
727
|
+
+ ' } else {\n'
|
|
728
|
+
+ ' BaseApplicationClass = ResolvedClass;\n'
|
|
729
|
+
+ ' }\n'
|
|
730
|
+
+ '\n'
|
|
731
|
+
+ ' // Subclass. If the user supplied Application Code,\n'
|
|
732
|
+
+ ' // wrap it as `function (Base) { ...userBody... }` and\n'
|
|
733
|
+
+ ' // expect it to return a class. Otherwise fall back to\n'
|
|
734
|
+
+ ' // a no-op extends-Base subclass. class extends handles\n'
|
|
735
|
+
+ ' // both ES6 classes and old-style prototype constructors.\n'
|
|
736
|
+
+ ' var PlaygroundApplication;\n'
|
|
737
|
+
+ ' if (typeof applicationSource === "string" && applicationSource.trim().length > 0) {\n'
|
|
738
|
+
+ ' try {\n'
|
|
739
|
+
+ ' var customizerFn = new Function("Base", applicationSource);\n'
|
|
740
|
+
+ ' var customizerResult = customizerFn(BaseApplicationClass);\n'
|
|
741
|
+
+ ' if (typeof customizerResult !== "function") {\n'
|
|
742
|
+
+ ' throw new Error("Application Code must `return` a class. Got " + (typeof customizerResult) + ".");\n'
|
|
743
|
+
+ ' }\n'
|
|
744
|
+
+ ' PlaygroundApplication = customizerResult;\n'
|
|
745
|
+
+ ' } catch (customizerErr) {\n'
|
|
746
|
+
+ ' throw new Error("Application Code error: " + (customizerErr && customizerErr.message ? customizerErr.message : customizerErr));\n'
|
|
747
|
+
+ ' }\n'
|
|
748
|
+
+ ' } else {\n'
|
|
749
|
+
+ ' PlaygroundApplication = class extends BaseApplicationClass {};\n'
|
|
750
|
+
+ ' }\n'
|
|
751
|
+
+ '\n'
|
|
752
|
+
+ ' // Precedence: Base class defaults < user-class defaults\n'
|
|
753
|
+
+ ' // (if Application Code set its own) < the four editor tabs.\n'
|
|
754
|
+
+ ' // The editor tabs are what the playground exists to drive,\n'
|
|
755
|
+
+ ' // so they always win.\n'
|
|
756
|
+
+ ' var userDefault = PlaygroundApplication.default_configuration || BaseApplicationClass.default_configuration || {};\n'
|
|
757
|
+
+ ' var basePict = userDefault.pict_configuration || {};\n'
|
|
758
|
+
+ ' var mergedPict = Object.assign({}, basePict, pictConfig);\n'
|
|
759
|
+
+ ' mergedPict[manifestKey] = manifest;\n'
|
|
760
|
+
+ ' if (appData !== undefined) { mergedPict.DefaultAppData = appData; }\n'
|
|
761
|
+
+ '\n'
|
|
762
|
+
+ ' var defaultConfig = Object.assign({}, userDefault, appConfig);\n'
|
|
763
|
+
+ ' defaultConfig.pict_configuration = mergedPict;\n'
|
|
764
|
+
+ ' PlaygroundApplication.default_configuration = defaultConfig;\n'
|
|
765
|
+
+ '\n'
|
|
766
|
+
+ ' // Pict.safeLoadPictApplication will instantiate and run lifecycle.\n'
|
|
767
|
+
+ ' Pict.safeOnDocumentReady(function() {\n'
|
|
768
|
+
+ ' Pict.safeLoadPictApplication(PlaygroundApplication, 2);\n'
|
|
769
|
+
+ ' // Mount the theme controls once the app is up.\n'
|
|
770
|
+
+ ' setTimeout(function() {\n'
|
|
771
|
+
+ ' try {\n'
|
|
772
|
+
+ ' if (window.PictSectionTheme && window._Pict && typeof window._Pict.addProvider === "function") {\n'
|
|
773
|
+
+ ' if (!window._Pict.providers || !window._Pict.providers["Theme-Section"]) {\n'
|
|
774
|
+
+ ' window._Pict.addProvider("Theme-Section", { ApplyDefault: "pict-default", DefaultMode: "system", DefaultScale: 1.0, Views: ["Picker", "ModeToggle", "ScaleSelect"] }, window.PictSectionTheme);\n'
|
|
775
|
+
+ ' window._Pict.views["Theme-Picker"].render();\n'
|
|
776
|
+
+ ' if (window._Pict.views["Theme-ModeToggle"]) { window._Pict.views["Theme-ModeToggle"].render(); }\n'
|
|
777
|
+
+ ' if (window._Pict.views["Theme-ScaleSelect"]) { window._Pict.views["Theme-ScaleSelect"].render(); }\n'
|
|
778
|
+
+ ' }\n'
|
|
779
|
+
+ ' }\n'
|
|
780
|
+
+ ' } catch (themeErr) { /* theme bootstrap is best-effort */ }\n'
|
|
781
|
+
+ ' }, 50);\n'
|
|
782
|
+
+ ' try { parent.postMessage({ type: "section-playground-ready" }, "*"); } catch(e) {}\n'
|
|
783
|
+
+ ' });\n'
|
|
784
|
+
+ ' } catch (err) {\n'
|
|
785
|
+
+ ' showError(String(err && err.stack ? err.stack : err));\n'
|
|
786
|
+
+ ' } }).catch(function(esmErr) {\n'
|
|
787
|
+
+ ' showError("ESM import failed: " + String(esmErr && esmErr.message ? esmErr.message : esmErr));\n'
|
|
788
|
+
+ ' });\n'
|
|
789
|
+
+ ' });\n'
|
|
790
|
+
+ '}());\n'
|
|
791
|
+
+ '</script>\n'
|
|
792
|
+
+ '</body>\n'
|
|
793
|
+
+ '</html>';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
class DocuserveSectionPlaygroundView extends libPictView
|
|
798
|
+
{
|
|
799
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
800
|
+
{
|
|
801
|
+
super(pFable, pOptions, pServiceHash);
|
|
802
|
+
|
|
803
|
+
// Editor metadata — populated when the playground is opened for
|
|
804
|
+
// a specific module. Each entry mirrors the _playground.json
|
|
805
|
+
// Editors[] descriptor + activation state.
|
|
806
|
+
this._editors = [];
|
|
807
|
+
this._activeHash = null;
|
|
808
|
+
// (group, module) currently mounted; used to scope localStorage.
|
|
809
|
+
this._scope = { Group: '', Module: '' };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// ────────────────────────────────────────────────────────────────
|
|
813
|
+
// Public API — called by the application's router.
|
|
814
|
+
// ────────────────────────────────────────────────────────────────
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Open the playground for `<group>/<module>`. Reads the module's
|
|
818
|
+
* `_playground.json`, hydrates editors from starter files or
|
|
819
|
+
* localStorage, renders the layout, and mounts a pict-section-code
|
|
820
|
+
* view per editor tab.
|
|
821
|
+
*
|
|
822
|
+
* @param {string} pGroup
|
|
823
|
+
* @param {string} pModule
|
|
824
|
+
*/
|
|
825
|
+
openPlayground(pGroup, pModule)
|
|
826
|
+
{
|
|
827
|
+
this._scope = { Group: pGroup, Module: pModule };
|
|
828
|
+
|
|
829
|
+
let tmpDoc = this.pict.providers['Docuserve-Documentation'];
|
|
830
|
+
if (!tmpDoc || typeof tmpDoc.loadPlaygroundConfig !== 'function')
|
|
831
|
+
{
|
|
832
|
+
this._renderError('Documentation provider is not registered.');
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
let tmpSelf = this;
|
|
837
|
+
tmpDoc.loadPlaygroundConfig(pGroup, pModule).then(function (pConfig)
|
|
838
|
+
{
|
|
839
|
+
if (!pConfig || pConfig.Kind !== 'section')
|
|
840
|
+
{
|
|
841
|
+
tmpSelf._renderError('This module does not have a section playground configured. Add `Kind: "section"` to `docs/_playground.json` and declare an `Editors` array.');
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
tmpSelf._spec = pConfig;
|
|
845
|
+
tmpSelf._loadEditors(pGroup, pModule, pConfig).then(function ()
|
|
846
|
+
{
|
|
847
|
+
tmpSelf._mountAndRender();
|
|
848
|
+
});
|
|
849
|
+
})
|
|
850
|
+
.catch(function (pError)
|
|
851
|
+
{
|
|
852
|
+
tmpSelf._renderError('Failed to load _playground.json: ' + (pError && pError.message ? pError.message : pError));
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Switch the visible editor tab. Each tab is a separate
|
|
858
|
+
* pict-section-code instance mounted into its own slot div; we
|
|
859
|
+
* just toggle the .active class.
|
|
860
|
+
*/
|
|
861
|
+
selectTab(pHash)
|
|
862
|
+
{
|
|
863
|
+
this._activeHash = pHash;
|
|
864
|
+
this._refreshEditorRecords();
|
|
865
|
+
// Toggle active class on the live DOM without re-rendering the
|
|
866
|
+
// whole view (editor mounts must stay stable so CodeJar keeps
|
|
867
|
+
// its state).
|
|
868
|
+
let tmpTabs = document.querySelectorAll('.docuserve-section-playground-tab');
|
|
869
|
+
for (let i = 0; i < tmpTabs.length; i++) { tmpTabs[i].classList.remove('active'); }
|
|
870
|
+
let tmpEditors = document.querySelectorAll('.docuserve-section-playground-editor');
|
|
871
|
+
for (let j = 0; j < tmpEditors.length; j++) { tmpEditors[j].classList.remove('active'); }
|
|
872
|
+
for (let k = 0; k < this._editors.length; k++)
|
|
873
|
+
{
|
|
874
|
+
let tmpEd = this._editors[k];
|
|
875
|
+
if (tmpEd.Hash === pHash)
|
|
876
|
+
{
|
|
877
|
+
let tmpTabEl = document.querySelector('.docuserve-section-playground-tab[onclick*="\'' + tmpEd.Hash + '\'"]');
|
|
878
|
+
if (tmpTabEl) { tmpTabEl.classList.add('active'); }
|
|
879
|
+
let tmpEditorEl = document.getElementById('Docuserve-Section-Playground-Editor-' + tmpEd.Hash);
|
|
880
|
+
if (tmpEditorEl) { tmpEditorEl.classList.add('active'); }
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* "Run" — read each editor's current value, validate, build a
|
|
887
|
+
* fresh srcdoc, and replace the iframe contents.
|
|
888
|
+
*/
|
|
889
|
+
run()
|
|
890
|
+
{
|
|
891
|
+
let tmpConfig;
|
|
892
|
+
try
|
|
893
|
+
{
|
|
894
|
+
tmpConfig = this._readAndValidateEditors();
|
|
895
|
+
}
|
|
896
|
+
catch (pError)
|
|
897
|
+
{
|
|
898
|
+
this._setStatus(pError.message, true);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
this._setStatus('running…', false);
|
|
903
|
+
|
|
904
|
+
let tmpIframe = document.getElementById('Docuserve-Section-Playground-Iframe');
|
|
905
|
+
if (!tmpIframe) { return; }
|
|
906
|
+
|
|
907
|
+
// Compute an absolute base href for the iframe's <base> tag.
|
|
908
|
+
// The iframe lives at about:srcdoc — relative URLs there don't
|
|
909
|
+
// resolve to the parent's docs root, so we anchor a real URL.
|
|
910
|
+
let tmpBaseURL = '';
|
|
911
|
+
try
|
|
912
|
+
{
|
|
913
|
+
// Trim hash off the parent location so the base resolves to the
|
|
914
|
+
// docs directory, not the SPA route.
|
|
915
|
+
let tmpHref = window.location.href.split('#')[0];
|
|
916
|
+
// Strip the final 'index.html' if present so the base is the dir.
|
|
917
|
+
tmpBaseURL = tmpHref.replace(/index\.html$/, '');
|
|
918
|
+
}
|
|
919
|
+
catch (pErr) { tmpBaseURL = ''; }
|
|
920
|
+
|
|
921
|
+
let tmpSrcdoc = buildIframeSrcdoc(tmpConfig, this._spec || {}, tmpBaseURL);
|
|
922
|
+
tmpIframe.srcdoc = tmpSrcdoc;
|
|
923
|
+
|
|
924
|
+
// One-shot ready listener — re-attached each Run.
|
|
925
|
+
let tmpSelf = this;
|
|
926
|
+
let tmpListener = function (pEvent)
|
|
927
|
+
{
|
|
928
|
+
if (!pEvent || !pEvent.data) return;
|
|
929
|
+
if (pEvent.data.type === 'section-playground-ready')
|
|
930
|
+
{
|
|
931
|
+
tmpSelf._setStatus('rendered', false);
|
|
932
|
+
}
|
|
933
|
+
else if (pEvent.data.type === 'section-playground-error')
|
|
934
|
+
{
|
|
935
|
+
tmpSelf._setStatus('error in sandbox', true);
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
window.addEventListener('message', tmpListener);
|
|
939
|
+
// Auto-cleanup after a generous timeout so we don't leak.
|
|
940
|
+
setTimeout(function () { window.removeEventListener('message', tmpListener); }, 30000);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Reset every editor back to its starter content (DefaultPath JSON
|
|
945
|
+
* file or the inline Default in the spec). Clears the matching
|
|
946
|
+
* localStorage entries so subsequent reloads re-read the starters.
|
|
947
|
+
*/
|
|
948
|
+
resetAll()
|
|
949
|
+
{
|
|
950
|
+
let tmpModal = this.pict.views['Pict-Section-Modal'];
|
|
951
|
+
let tmpSelf = this;
|
|
952
|
+
let tmpProceed = function ()
|
|
953
|
+
{
|
|
954
|
+
for (let i = 0; i < tmpSelf._editors.length; i++)
|
|
955
|
+
{
|
|
956
|
+
let tmpEd = tmpSelf._editors[i];
|
|
957
|
+
tmpEd.Code = tmpEd.StarterCode || '';
|
|
958
|
+
let tmpKey = tmpSelf._storageKey(tmpEd.Hash);
|
|
959
|
+
try { window.localStorage.removeItem(tmpKey); } catch (e) { /* */ }
|
|
960
|
+
let tmpAddress = tmpSelf._editorCodeAddress(tmpEd.Hash);
|
|
961
|
+
tmpSelf.pict.manifest.setValueByHash(tmpSelf.pict.AppData, tmpAddress, tmpEd.Code);
|
|
962
|
+
let tmpEditorView = tmpSelf.pict.views[tmpSelf._editorViewIdentifier(tmpEd.Hash)];
|
|
963
|
+
if (tmpEditorView && typeof tmpEditorView.setCode === 'function')
|
|
964
|
+
{
|
|
965
|
+
tmpEditorView.setCode(tmpEd.Code);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
tmpSelf._setStatus('reset to starters', false);
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
if (tmpModal && typeof tmpModal.confirm === 'function')
|
|
972
|
+
{
|
|
973
|
+
tmpModal.confirm(
|
|
974
|
+
'Reset all editors to their starter content? Your current edits will be lost.',
|
|
975
|
+
{
|
|
976
|
+
title: 'Reset playground',
|
|
977
|
+
confirmLabel: 'Reset',
|
|
978
|
+
cancelLabel: 'Keep edits',
|
|
979
|
+
dangerous: true
|
|
980
|
+
}
|
|
981
|
+
).then(function (pOK) { if (pOK) tmpProceed(); });
|
|
982
|
+
}
|
|
983
|
+
else
|
|
984
|
+
{
|
|
985
|
+
tmpProceed();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ────────────────────────────────────────────────────────────────
|
|
990
|
+
// Internals
|
|
991
|
+
// ────────────────────────────────────────────────────────────────
|
|
992
|
+
|
|
993
|
+
_storageKey(pEditorHash)
|
|
994
|
+
{
|
|
995
|
+
return _LocalStorageKeyPrefix + ':' + this._scope.Group + '/' + this._scope.Module + ':' + pEditorHash;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
_editorViewIdentifier(pEditorHash)
|
|
999
|
+
{
|
|
1000
|
+
return 'Docuserve-Section-Playground-Editor-View-' + pEditorHash;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
_editorCodeAddress(pEditorHash)
|
|
1004
|
+
{
|
|
1005
|
+
return _AppDataRoot + '.Code.' + pEditorHash;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Hydrate `this._editors` from the spec + starters + localStorage.
|
|
1010
|
+
* Returns a Promise so callers can await all fetches.
|
|
1011
|
+
*/
|
|
1012
|
+
_loadEditors(pGroup, pModule, pSpec)
|
|
1013
|
+
{
|
|
1014
|
+
let tmpEditors = pSpec.Editors || [];
|
|
1015
|
+
let tmpDoc = this.pict.providers['Docuserve-Documentation'];
|
|
1016
|
+
let tmpSelf = this;
|
|
1017
|
+
|
|
1018
|
+
this._editors = [];
|
|
1019
|
+
|
|
1020
|
+
let tmpPromises = [];
|
|
1021
|
+
for (let i = 0; i < tmpEditors.length; i++)
|
|
1022
|
+
{
|
|
1023
|
+
(function (pEditorSpec, pIndex)
|
|
1024
|
+
{
|
|
1025
|
+
let tmpEntry =
|
|
1026
|
+
{
|
|
1027
|
+
Hash: pEditorSpec.Hash,
|
|
1028
|
+
Label: pEditorSpec.Label || pEditorSpec.Hash,
|
|
1029
|
+
Language: pEditorSpec.Language || 'json',
|
|
1030
|
+
StarterCode: pEditorSpec.Default || '',
|
|
1031
|
+
Code: ''
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
// Try localStorage first (user's last edits).
|
|
1035
|
+
let tmpStored = null;
|
|
1036
|
+
try { tmpStored = window.localStorage.getItem(tmpSelf._storageKey(pEditorSpec.Hash)); }
|
|
1037
|
+
catch (e) { tmpStored = null; }
|
|
1038
|
+
|
|
1039
|
+
// Then fetch the starter file if DefaultPath is set.
|
|
1040
|
+
//
|
|
1041
|
+
// URL resolution has two modes:
|
|
1042
|
+
// * Catalog mode (group + module + Catalog provider) — use
|
|
1043
|
+
// resolveDocumentURL() which routes to the right GitHub
|
|
1044
|
+
// pages site for the target module.
|
|
1045
|
+
// * Standalone mode (no group/module, or empty strings) —
|
|
1046
|
+
// the docs site IS the module's own docs, so a
|
|
1047
|
+
// docs-root-relative path resolves correctly as-is.
|
|
1048
|
+
let tmpStarterPromise;
|
|
1049
|
+
if (pEditorSpec.DefaultPath)
|
|
1050
|
+
{
|
|
1051
|
+
let tmpURL = null;
|
|
1052
|
+
if (pGroup && pModule && tmpDoc && typeof tmpDoc.resolveDocumentURL === 'function')
|
|
1053
|
+
{
|
|
1054
|
+
tmpURL = tmpDoc.resolveDocumentURL(pGroup, pModule, pEditorSpec.DefaultPath);
|
|
1055
|
+
}
|
|
1056
|
+
if (!tmpURL)
|
|
1057
|
+
{
|
|
1058
|
+
let tmpDocsBase = tmpSelf.pict.AppData.Docuserve.DocsBaseURL || '';
|
|
1059
|
+
tmpURL = tmpDocsBase + pEditorSpec.DefaultPath;
|
|
1060
|
+
}
|
|
1061
|
+
tmpStarterPromise = fetch(tmpURL).then(function (pResponse)
|
|
1062
|
+
{
|
|
1063
|
+
if (!pResponse.ok) { return ''; }
|
|
1064
|
+
return pResponse.text();
|
|
1065
|
+
}).catch(function () { return ''; });
|
|
1066
|
+
}
|
|
1067
|
+
else
|
|
1068
|
+
{
|
|
1069
|
+
tmpStarterPromise = Promise.resolve(tmpEntry.StarterCode);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
tmpPromises.push(tmpStarterPromise.then(function (pStarter)
|
|
1073
|
+
{
|
|
1074
|
+
tmpEntry.StarterCode = pStarter || tmpEntry.StarterCode;
|
|
1075
|
+
tmpEntry.Code = (tmpStored !== null && tmpStored !== undefined)
|
|
1076
|
+
? tmpStored
|
|
1077
|
+
: tmpEntry.StarterCode;
|
|
1078
|
+
// Park in the same slot so re-mounts preserve order.
|
|
1079
|
+
tmpSelf._editors[pIndex] = tmpEntry;
|
|
1080
|
+
}));
|
|
1081
|
+
}(tmpEditors[i], i));
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return Promise.all(tmpPromises).then(function ()
|
|
1085
|
+
{
|
|
1086
|
+
// Compact in case any slot ended up undefined.
|
|
1087
|
+
tmpSelf._editors = tmpSelf._editors.filter(function (e) { return !!e; });
|
|
1088
|
+
if (tmpSelf._editors.length > 0)
|
|
1089
|
+
{
|
|
1090
|
+
tmpSelf._activeHash = tmpSelf._editors[0].Hash;
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
_refreshEditorRecords()
|
|
1096
|
+
{
|
|
1097
|
+
let tmpRecords = [];
|
|
1098
|
+
for (let i = 0; i < this._editors.length; i++)
|
|
1099
|
+
{
|
|
1100
|
+
let tmpEd = this._editors[i];
|
|
1101
|
+
tmpRecords.push(
|
|
1102
|
+
{
|
|
1103
|
+
Hash: tmpEd.Hash,
|
|
1104
|
+
Label: tmpEd.Label,
|
|
1105
|
+
ActiveClass: (tmpEd.Hash === this._activeHash) ? ' active' : ''
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
this.pict.AppData.Docuserve = this.pict.AppData.Docuserve || {};
|
|
1109
|
+
this.pict.AppData.Docuserve.SectionPlayground = this.pict.AppData.Docuserve.SectionPlayground || {};
|
|
1110
|
+
this.pict.AppData.Docuserve.SectionPlayground.Editors = tmpRecords;
|
|
1111
|
+
// Seed each editor's Code address so pict-section-code finds the
|
|
1112
|
+
// initial value when it mounts.
|
|
1113
|
+
this.pict.AppData.Docuserve.SectionPlayground.Code = this.pict.AppData.Docuserve.SectionPlayground.Code || {};
|
|
1114
|
+
for (let j = 0; j < this._editors.length; j++)
|
|
1115
|
+
{
|
|
1116
|
+
let tmpEd = this._editors[j];
|
|
1117
|
+
this.pict.AppData.Docuserve.SectionPlayground.Code[tmpEd.Hash] = tmpEd.Code;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Build the pict-section-modal shell that hosts the editor stack
|
|
1123
|
+
* (center) + iframe sandbox (resizable + collapsible bottom panel).
|
|
1124
|
+
*
|
|
1125
|
+
* The shell creates two destination divs inside its mount:
|
|
1126
|
+
* * #Docuserve-Section-Playground-Editor-Mount (center)
|
|
1127
|
+
* * #Docuserve-Section-Playground-Iframe-Mount (bottom panel)
|
|
1128
|
+
*
|
|
1129
|
+
* Once the shell is up, we stamp the editor slot divs (one per tab)
|
|
1130
|
+
* into the editor mount and the iframe + status badge into the
|
|
1131
|
+
* sandbox mount. pict-section-code instances mount into the per-
|
|
1132
|
+
* tab slots from there.
|
|
1133
|
+
*
|
|
1134
|
+
* Re-entrant: a second call after teardown rebuilds the shell on
|
|
1135
|
+
* the same mount div. PersistenceKey scopes the saved split size
|
|
1136
|
+
* per-module so each playground remembers its own layout.
|
|
1137
|
+
*/
|
|
1138
|
+
_buildShell()
|
|
1139
|
+
{
|
|
1140
|
+
let tmpModal = this.pict.views['Pict-Section-Modal'];
|
|
1141
|
+
let tmpMountEl = document.getElementById('Docuserve-Section-Playground-Shell-Mount');
|
|
1142
|
+
if (!tmpModal || typeof tmpModal.shell !== 'function' || !tmpMountEl)
|
|
1143
|
+
{
|
|
1144
|
+
// Shell isn't available — fall back to a static layout so the
|
|
1145
|
+
// playground still works (no resize/collapse) without the
|
|
1146
|
+
// section-modal dependency.
|
|
1147
|
+
tmpMountEl.innerHTML = ''
|
|
1148
|
+
+ '<div class="docuserve-section-playground-editor-mount" id="Docuserve-Section-Playground-Editor-Mount" style="height:55%;border-bottom:1px solid var(--theme-color-border-default,#DDD6CA)"></div>'
|
|
1149
|
+
+ '<div class="docuserve-section-playground-iframe-pane" id="Docuserve-Section-Playground-Iframe-Mount" style="height:45%"></div>';
|
|
1150
|
+
this._populateShellDestinations();
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Per-module persistence key so each module remembers its own
|
|
1155
|
+
// editor/sandbox split independently.
|
|
1156
|
+
let tmpPersistenceKey = 'docuserve-section-playground:' + this._scope.Group + '/' + this._scope.Module + ':split';
|
|
1157
|
+
|
|
1158
|
+
this._shell = tmpModal.shell(tmpMountEl, { PersistenceKey: tmpPersistenceKey });
|
|
1159
|
+
|
|
1160
|
+
// Bottom panel — the iframe sandbox. Resizable + collapsible
|
|
1161
|
+
// (handled by the shell). CSS at the top of this view widens
|
|
1162
|
+
// the collapse tab and centers it horizontally; the Title text
|
|
1163
|
+
// renders inside it as the always-visible label.
|
|
1164
|
+
this._shell.addPanel(
|
|
1165
|
+
{
|
|
1166
|
+
Hash: 'sandbox',
|
|
1167
|
+
Side: 'bottom',
|
|
1168
|
+
Mode: 'resizable',
|
|
1169
|
+
Size: 360,
|
|
1170
|
+
MinSize: 140,
|
|
1171
|
+
MaxSize: 1000,
|
|
1172
|
+
Title: 'Sandbox',
|
|
1173
|
+
ContentDestinationId: 'Docuserve-Section-Playground-Iframe-Mount'
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
// Center — the editor stack (one slot per tab; only the active
|
|
1177
|
+
// slot is display:flex). No ContentView; we DOM-inject the
|
|
1178
|
+
// per-tab slot divs below.
|
|
1179
|
+
this._shell.center({ ContentDestinationId: 'Docuserve-Section-Playground-Editor-Mount' });
|
|
1180
|
+
|
|
1181
|
+
this._populateShellDestinations();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Fill the shell's two destinations with their per-render content:
|
|
1186
|
+
* * editor mount → one slot div per editor (active tab has .active)
|
|
1187
|
+
* * sandbox mount → the iframe + the status badge
|
|
1188
|
+
*
|
|
1189
|
+
* Called from _buildShell on first mount AND from the static
|
|
1190
|
+
* fallback layout when the shell isn't available.
|
|
1191
|
+
*/
|
|
1192
|
+
_populateShellDestinations()
|
|
1193
|
+
{
|
|
1194
|
+
// Editor slot divs — one per configured editor. Tag the mount
|
|
1195
|
+
// with our flex-column class so the active slot fills the
|
|
1196
|
+
// shell's center area instead of collapsing to its content
|
|
1197
|
+
// height (pict-section-code itself has zero intrinsic height).
|
|
1198
|
+
let tmpEditorMount = document.getElementById('Docuserve-Section-Playground-Editor-Mount');
|
|
1199
|
+
if (tmpEditorMount)
|
|
1200
|
+
{
|
|
1201
|
+
tmpEditorMount.classList.add('docuserve-section-playground-editor-mount');
|
|
1202
|
+
let tmpSlotsHTML = '';
|
|
1203
|
+
for (let i = 0; i < this._editors.length; i++)
|
|
1204
|
+
{
|
|
1205
|
+
let tmpEd = this._editors[i];
|
|
1206
|
+
let tmpActive = (tmpEd.Hash === this._activeHash) ? ' active' : '';
|
|
1207
|
+
tmpSlotsHTML += '<div class="docuserve-section-playground-editor' + tmpActive + '"'
|
|
1208
|
+
+ ' id="Docuserve-Section-Playground-Editor-' + tmpEd.Hash + '"'
|
|
1209
|
+
+ ' data-editor-hash="' + tmpEd.Hash + '"></div>';
|
|
1210
|
+
}
|
|
1211
|
+
tmpEditorMount.innerHTML = tmpSlotsHTML;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Iframe + status badge. The status badge is absolute-positioned
|
|
1215
|
+
// inside the sandbox pane so it overlays the iframe.
|
|
1216
|
+
let tmpSandboxMount = document.getElementById('Docuserve-Section-Playground-Iframe-Mount');
|
|
1217
|
+
if (tmpSandboxMount)
|
|
1218
|
+
{
|
|
1219
|
+
tmpSandboxMount.classList.add('docuserve-section-playground-iframe-pane');
|
|
1220
|
+
tmpSandboxMount.innerHTML = ''
|
|
1221
|
+
+ '<iframe id="Docuserve-Section-Playground-Iframe"'
|
|
1222
|
+
+ ' class="docuserve-section-playground-iframe"'
|
|
1223
|
+
+ ' title="Section playground sandbox"'
|
|
1224
|
+
+ ' sandbox="allow-scripts allow-same-origin allow-modals allow-popups"></iframe>'
|
|
1225
|
+
+ '<div id="Docuserve-Section-Playground-Status" class="docuserve-section-playground-status">ready</div>';
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/**
|
|
1230
|
+
* Render the layout + mount one pict-section-code instance per
|
|
1231
|
+
* editor tab. Each editor's view gets its own ViewIdentifier so
|
|
1232
|
+
* pict.views[] stays distinct per tab.
|
|
1233
|
+
*/
|
|
1234
|
+
_mountAndRender()
|
|
1235
|
+
{
|
|
1236
|
+
// Tag the container so our flex CSS applies.
|
|
1237
|
+
let tmpContainer = document.getElementById('Docuserve-Content-Container');
|
|
1238
|
+
if (tmpContainer) { tmpContainer.classList.add('docuserve-section-playground-host'); }
|
|
1239
|
+
|
|
1240
|
+
this._refreshEditorRecords();
|
|
1241
|
+
this.render();
|
|
1242
|
+
|
|
1243
|
+
// Build the shell — center holds the editor stack, bottom panel
|
|
1244
|
+
// holds the iframe sandbox with a wider middle-tab collapse
|
|
1245
|
+
// affordance. Persistence is scoped per-module so each
|
|
1246
|
+
// playground remembers its split independently.
|
|
1247
|
+
this._buildShell();
|
|
1248
|
+
|
|
1249
|
+
// Mount editor views. Each is a fresh pict-section-code subclass
|
|
1250
|
+
// that persists on change.
|
|
1251
|
+
let tmpSelf = this;
|
|
1252
|
+
for (let i = 0; i < this._editors.length; i++)
|
|
1253
|
+
{
|
|
1254
|
+
let tmpEd = this._editors[i];
|
|
1255
|
+
let tmpViewId = this._editorViewIdentifier(tmpEd.Hash);
|
|
1256
|
+
let tmpAddress = this._editorCodeAddress(tmpEd.Hash);
|
|
1257
|
+
let tmpDestId = 'Docuserve-Section-Playground-Editor-' + tmpEd.Hash;
|
|
1258
|
+
|
|
1259
|
+
// If a previous mount left a view around, dispose it first
|
|
1260
|
+
// so we don't double-mount inside the same destination.
|
|
1261
|
+
if (this.pict.views[tmpViewId])
|
|
1262
|
+
{
|
|
1263
|
+
try { delete this.pict.views[tmpViewId]; } catch (e) { /* */ }
|
|
1264
|
+
if (this.pict.servicesMap && this.pict.servicesMap.PictView)
|
|
1265
|
+
{
|
|
1266
|
+
try { delete this.pict.servicesMap.PictView[tmpViewId]; } catch (e) { /* */ }
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// pict-section-code needs Templates + Renderables wired to the
|
|
1271
|
+
// mount slot so its onAfterRender can find the destination.
|
|
1272
|
+
// We register a no-op container template per editor and tie
|
|
1273
|
+
// the renderable to the per-tab slot.
|
|
1274
|
+
let tmpTemplateHash = 'Section-Playground-CodeMount-' + tmpEd.Hash;
|
|
1275
|
+
let tmpEditorOpts = Object.assign({}, libPictSectionCode.default_configuration,
|
|
1276
|
+
{
|
|
1277
|
+
ViewIdentifier: tmpViewId,
|
|
1278
|
+
DefaultDestinationAddress: '#' + tmpDestId,
|
|
1279
|
+
TargetElementAddress: '#' + tmpDestId,
|
|
1280
|
+
Templates:
|
|
1281
|
+
[
|
|
1282
|
+
{ Hash: tmpTemplateHash, Template: '<!-- pict-section-code mount: ' + tmpEd.Hash + ' -->' }
|
|
1283
|
+
],
|
|
1284
|
+
Renderables:
|
|
1285
|
+
[
|
|
1286
|
+
{ RenderableHash: 'Section-Playground-CodeRenderable-' + tmpEd.Hash,
|
|
1287
|
+
TemplateHash: tmpTemplateHash,
|
|
1288
|
+
DestinationAddress: '#' + tmpDestId }
|
|
1289
|
+
],
|
|
1290
|
+
Language: tmpEd.Language || 'json',
|
|
1291
|
+
ReadOnly: false,
|
|
1292
|
+
LineNumbers: true,
|
|
1293
|
+
Tab: '\t',
|
|
1294
|
+
AddClosing: true,
|
|
1295
|
+
CatchTab: true,
|
|
1296
|
+
DefaultCode: tmpEd.Code,
|
|
1297
|
+
CodeDataAddress: tmpAddress,
|
|
1298
|
+
// Defer render — we connect the CodeJar prototype first
|
|
1299
|
+
// (loaded from CDN in _loadCodeJar), then call render() on
|
|
1300
|
+
// each editor once.
|
|
1301
|
+
AutoRender: false,
|
|
1302
|
+
RenderOnLoad: false
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// Subclass that persists on every change.
|
|
1306
|
+
let SectionPlaygroundEditor = class extends libPictSectionCode
|
|
1307
|
+
{
|
|
1308
|
+
onCodeChange(pCode)
|
|
1309
|
+
{
|
|
1310
|
+
super.onCodeChange(pCode);
|
|
1311
|
+
if (this._lsTimer) { clearTimeout(this._lsTimer); }
|
|
1312
|
+
let tmpEditorHash = this.options.ViewIdentifier.replace('Docuserve-Section-Playground-Editor-View-', '');
|
|
1313
|
+
this._lsTimer = setTimeout(() =>
|
|
1314
|
+
{
|
|
1315
|
+
this._lsTimer = null;
|
|
1316
|
+
try
|
|
1317
|
+
{
|
|
1318
|
+
let tmpView = this.fable.pict.views['Docuserve-Section-Playground'];
|
|
1319
|
+
let tmpKey = tmpView && typeof tmpView._storageKey === 'function'
|
|
1320
|
+
? tmpView._storageKey(tmpEditorHash)
|
|
1321
|
+
: (_LocalStorageKeyPrefix + ':' + tmpEditorHash);
|
|
1322
|
+
window.localStorage.setItem(tmpKey, pCode);
|
|
1323
|
+
if (tmpView && typeof tmpView._setStatus === 'function')
|
|
1324
|
+
{
|
|
1325
|
+
tmpView._setStatus('saved', false);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
catch (pError) { /* quota or no LS — silent */ }
|
|
1329
|
+
}, 500);
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
this.pict.addView(tmpViewId, tmpEditorOpts, SectionPlaygroundEditor);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Lazy-load CodeJar from CDN, then connect + render each editor.
|
|
1337
|
+
// pict-section-code can't initialize without a CodeJar prototype.
|
|
1338
|
+
this._loadCodeJar().then(function (pCodeJar)
|
|
1339
|
+
{
|
|
1340
|
+
for (let n = 0; n < tmpSelf._editors.length; n++)
|
|
1341
|
+
{
|
|
1342
|
+
let tmpEd = tmpSelf._editors[n];
|
|
1343
|
+
let tmpEditorView = tmpSelf.pict.views[tmpSelf._editorViewIdentifier(tmpEd.Hash)];
|
|
1344
|
+
if (!tmpEditorView) { continue; }
|
|
1345
|
+
if (!tmpEditorView._codeJarPrototype)
|
|
1346
|
+
{
|
|
1347
|
+
tmpEditorView.connectCodeJarPrototype(pCodeJar);
|
|
1348
|
+
}
|
|
1349
|
+
try { tmpEditorView.render(); }
|
|
1350
|
+
catch (pError) { /* best effort */ }
|
|
1351
|
+
}
|
|
1352
|
+
tmpSelf._setStatus('press Run to render', false, 2500);
|
|
1353
|
+
}).catch(function (pError)
|
|
1354
|
+
{
|
|
1355
|
+
tmpSelf._setStatus('CodeJar failed to load — editors disabled', true, 6000);
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Dynamic-import CodeJar from jsDelivr. Memoized. Wrapped in
|
|
1361
|
+
* `new Function('u','return import(u)')` so browserify doesn't try to
|
|
1362
|
+
* rewrite the import() at build time.
|
|
1363
|
+
*/
|
|
1364
|
+
_loadCodeJar()
|
|
1365
|
+
{
|
|
1366
|
+
if (this._codeJarPromise) { return this._codeJarPromise; }
|
|
1367
|
+
this._codeJarPromise = new Function('u', 'return import(u)')(_CodeJarCDN)
|
|
1368
|
+
.then(function (pModule)
|
|
1369
|
+
{
|
|
1370
|
+
if (!pModule || typeof pModule.CodeJar !== 'function')
|
|
1371
|
+
{
|
|
1372
|
+
throw new Error('CodeJar export not found in module');
|
|
1373
|
+
}
|
|
1374
|
+
return pModule.CodeJar;
|
|
1375
|
+
});
|
|
1376
|
+
return this._codeJarPromise;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Read every editor's current text out of AppData (it's kept up to
|
|
1381
|
+
* date by pict-section-code's CodeDataAddress wiring). JSON-language
|
|
1382
|
+
* editors are parsed and validated; others are returned verbatim.
|
|
1383
|
+
*
|
|
1384
|
+
* @throws Error on a parse failure — caller surfaces to the UI.
|
|
1385
|
+
*/
|
|
1386
|
+
_readAndValidateEditors()
|
|
1387
|
+
{
|
|
1388
|
+
let tmpOut = { manifest: undefined, pictConfig: undefined, appConfig: undefined, appData: undefined };
|
|
1389
|
+
for (let i = 0; i < this._editors.length; i++)
|
|
1390
|
+
{
|
|
1391
|
+
let tmpEd = this._editors[i];
|
|
1392
|
+
let tmpAddress = this._editorCodeAddress(tmpEd.Hash);
|
|
1393
|
+
let tmpRaw = this.pict.manifest.getValueByHash(this.pict.AppData, tmpAddress);
|
|
1394
|
+
if (typeof tmpRaw !== 'string') { tmpRaw = tmpEd.Code || ''; }
|
|
1395
|
+
|
|
1396
|
+
let tmpValue;
|
|
1397
|
+
if ((tmpEd.Language || 'json').toLowerCase() === 'json')
|
|
1398
|
+
{
|
|
1399
|
+
if (tmpRaw.trim() === '')
|
|
1400
|
+
{
|
|
1401
|
+
tmpValue = {};
|
|
1402
|
+
}
|
|
1403
|
+
else
|
|
1404
|
+
{
|
|
1405
|
+
try { tmpValue = JSON.parse(tmpRaw); }
|
|
1406
|
+
catch (pError)
|
|
1407
|
+
{
|
|
1408
|
+
throw new Error('JSON parse error in "' + tmpEd.Label + '": ' + pError.message);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
else
|
|
1413
|
+
{
|
|
1414
|
+
tmpValue = tmpRaw;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Map editor hash to a known config slot, or carry through unchanged.
|
|
1418
|
+
if (tmpOut.hasOwnProperty(tmpEd.Hash))
|
|
1419
|
+
{
|
|
1420
|
+
tmpOut[tmpEd.Hash] = tmpValue;
|
|
1421
|
+
}
|
|
1422
|
+
else
|
|
1423
|
+
{
|
|
1424
|
+
tmpOut[tmpEd.Hash] = tmpValue;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return tmpOut;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
_setStatus(pMessage, pIsError, pAutoHideMs)
|
|
1431
|
+
{
|
|
1432
|
+
let tmpEl = document.getElementById('Docuserve-Section-Playground-Status');
|
|
1433
|
+
if (!tmpEl) return;
|
|
1434
|
+
tmpEl.textContent = pMessage;
|
|
1435
|
+
tmpEl.classList.add('show');
|
|
1436
|
+
if (pIsError) { tmpEl.classList.add('error'); }
|
|
1437
|
+
else { tmpEl.classList.remove('error'); }
|
|
1438
|
+
if (this._statusTimer) { clearTimeout(this._statusTimer); }
|
|
1439
|
+
let tmpDelay = (typeof pAutoHideMs === 'number') ? pAutoHideMs : 2500;
|
|
1440
|
+
this._statusTimer = setTimeout(function ()
|
|
1441
|
+
{
|
|
1442
|
+
tmpEl.classList.remove('show');
|
|
1443
|
+
tmpEl.classList.remove('error');
|
|
1444
|
+
}, tmpDelay);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
_renderError(pMessage)
|
|
1448
|
+
{
|
|
1449
|
+
let tmpHTML = '<div class="docuserve-section-playground-emptystate">'
|
|
1450
|
+
+ '<div class="docuserve-section-playground-emptystate-title">Playground unavailable</div>'
|
|
1451
|
+
+ '<div>' + this._escape(pMessage) + '</div>'
|
|
1452
|
+
+ '</div>';
|
|
1453
|
+
this.pict.ContentAssignment.assignContent('#Docuserve-Content-Container', tmpHTML);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
_escape(pText)
|
|
1457
|
+
{
|
|
1458
|
+
return String(pText || '').replace(/[&<>"']/g, function (pChar)
|
|
1459
|
+
{
|
|
1460
|
+
return ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' })[pChar];
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
|
|
1465
|
+
{
|
|
1466
|
+
this.pict.CSSMap.injectCSS();
|
|
1467
|
+
return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
module.exports = DocuserveSectionPlaygroundView;
|
|
1472
|
+
module.exports.default_configuration = _ViewConfiguration;
|