@bpmn-io/form-js-playground 1.7.3 → 1.8.0

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/index.cjs CHANGED
@@ -147,7 +147,7 @@ function completions(context) {
147
147
  };
148
148
  }
149
149
 
150
- const NO_LINT_CLS = 'fjs-no-json-lint';
150
+ const NO_LINT_CLS = 'fjs-cm-no-lint';
151
151
 
152
152
  /**
153
153
  * @param {object} options
@@ -162,67 +162,53 @@ function JSONEditor(options = {}) {
162
162
  readonly = false
163
163
  } = options;
164
164
  const emitter = mitt();
165
- let language = new state.Compartment().of(langJson.json());
166
- let tabSize = new state.Compartment().of(state.EditorState.tabSize.of(2));
165
+ const languageCompartment = new state.Compartment().of(langJson.json());
166
+ const tabSizeCompartment = new state.Compartment().of(state.EditorState.tabSize.of(2));
167
+ const autocompletionConfCompartment = new state.Compartment();
168
+ const placeholderLinterExtension = createPlaceholderLinterExtension();
167
169
  let container = null;
168
-
169
- /**
170
- * @typedef {Array<string>} Variables
171
- */
172
-
173
- const autocompletionConf = new state.Compartment();
174
- const linterExtension = lint.linter(langJson.jsonParseLinter());
175
-
176
- // this sets no-linting mode if placeholders are present
177
- const placeholderLinterExtension = lint.linter(view => {
178
- const placeholders = view.dom.querySelectorAll('.cm-placeholder');
179
- if (placeholders.length > 0) {
180
- set(container, NO_LINT_CLS);
181
- } else {
182
- unset(container, NO_LINT_CLS);
183
- }
184
- return [];
185
- });
186
- function createState(doc, extensions = [], variables = []) {
187
- return state.EditorState.create({
188
- doc,
189
- extensions: [codemirror.basicSetup, language, tabSize, linterExtension, placeholderLinterExtension, lint.lintGutter(), autocompletionConf.of(variablesFacet.of(variables)), autocompletionExtension(), view.keymap.of([commands.indentWithTab]), editorPlaceholder ? view.placeholder(editorPlaceholder) : [], ...extensions]
190
- });
191
- }
192
- function createView(readonly) {
193
- const updateListener = view.EditorView.updateListener.of(update => {
170
+ function createState(doc, variables = []) {
171
+ const extensions = [codemirror.basicSetup, languageCompartment, tabSizeCompartment, lint.lintGutter(), lint.linter(langJson.jsonParseLinter()), placeholderLinterExtension, autocompletionConfCompartment.of(variablesFacet.of(variables)), autocompletionExtension(), view.keymap.of([commands.indentWithTab]), editorPlaceholder ? view.placeholder(editorPlaceholder) : [], view.EditorView.updateListener.of(update => {
194
172
  if (update.docChanged) {
195
173
  emitter.emit('changed', {
196
- value: update.view.state.doc.toString()
174
+ value: update.state.doc.toString()
197
175
  });
198
176
  }
177
+ }), view.EditorView.editable.of(!readonly), view.EditorView.contentAttributes.of(contentAttributes)];
178
+ return state.EditorState.create({
179
+ doc,
180
+ extensions
199
181
  });
200
- const editable = view.EditorView.editable.of(!readonly);
201
- const contentAttributesExtension = view.EditorView.contentAttributes.of(contentAttributes);
202
- const view$1 = new view.EditorView({
203
- state: createState('', [updateListener, editable, contentAttributesExtension])
204
- });
205
- view$1.setValue = function (value) {
206
- this.setState(createState(value, [updateListener, editable, contentAttributesExtension]));
207
- };
208
- view$1.setVariables = function (variables) {
209
- this.setState(createState(view$1.state.doc.toString(), [updateListener, editable, contentAttributesExtension], variables));
210
- };
211
- return view$1;
212
182
  }
213
- const view$1 = this._view = createView(readonly);
214
- this.setValue = function (value) {
215
- view$1.setValue(value);
183
+ const view$1 = new view.EditorView({
184
+ state: createState('')
185
+ });
186
+ this.setValue = function (newValue) {
187
+ const oldValue = view$1.state.doc.toString();
188
+ const diff = findDiff(oldValue, newValue);
189
+ if (diff) {
190
+ view$1.dispatch({
191
+ changes: {
192
+ from: diff.start,
193
+ to: diff.end,
194
+ insert: diff.text
195
+ },
196
+ selection: {
197
+ anchor: diff.start + diff.text.length
198
+ }
199
+ });
200
+ }
216
201
  };
217
202
  this.getValue = function () {
218
203
  return view$1.state.doc.toString();
219
204
  };
220
-
221
- /**
222
- * @param {Variables} variables
223
- */
224
205
  this.setVariables = function (variables) {
225
- view$1.setVariables(variables);
206
+ view$1.dispatch({
207
+ effects: autocompletionConfCompartment.reconfigure(variablesFacet.of(variables))
208
+ });
209
+ };
210
+ this.getView = function () {
211
+ return view$1;
226
212
  };
227
213
  this.on = emitter.on;
228
214
  this.off = emitter.off;
@@ -230,24 +216,56 @@ function JSONEditor(options = {}) {
230
216
  this.attachTo = function (_container) {
231
217
  container = _container;
232
218
  container.appendChild(view$1.dom);
219
+ minDom.classes(container, document.body).add('fjs-json-editor');
233
220
  };
234
221
  this.destroy = function () {
235
- if (view$1.dom.parentNode) {
236
- view$1.dom.parentNode.removeChild(view$1.dom);
222
+ if (container && view$1.dom) {
223
+ container.removeChild(view$1.dom);
224
+ minDom.classes(container, document.body).remove('fjs-json-editor');
237
225
  }
238
226
  view$1.destroy();
239
227
  };
228
+ function createPlaceholderLinterExtension() {
229
+ return lint.linter(view => {
230
+ const placeholders = view.dom.querySelectorAll('.cm-placeholder');
231
+ if (placeholders.length > 0) {
232
+ minDom.classes(container, document.body).add(NO_LINT_CLS);
233
+ } else {
234
+ minDom.classes(container, document.body).remove(NO_LINT_CLS);
235
+ }
236
+ return [];
237
+ });
238
+ }
240
239
  }
241
-
242
- // helpers //////////////////////
243
-
244
- function set(node, cls) {
245
- const classes = minDom.classes(node, document.body);
246
- classes.add(cls);
247
- }
248
- function unset(node, cls) {
249
- const classes = minDom.classes(node, document.body);
250
- classes.remove(cls);
240
+ function findDiff(oldStr, newStr) {
241
+ if (oldStr === newStr) {
242
+ return null;
243
+ }
244
+ oldStr = oldStr || '';
245
+ newStr = newStr || '';
246
+ let minLength = Math.min(oldStr.length, newStr.length);
247
+ let start = 0;
248
+ while (start < minLength && oldStr[start] === newStr[start]) {
249
+ start++;
250
+ }
251
+ if (start === minLength) {
252
+ return {
253
+ start: start,
254
+ text: newStr.slice(start),
255
+ end: oldStr.length
256
+ };
257
+ }
258
+ let endOld = oldStr.length;
259
+ let endNew = newStr.length;
260
+ while (endOld > start && endNew > start && oldStr[endOld - 1] === newStr[endNew - 1]) {
261
+ endOld--;
262
+ endNew--;
263
+ }
264
+ return {
265
+ start: start,
266
+ text: newStr.slice(start, endNew),
267
+ end: endOld
268
+ };
251
269
  }
252
270
 
253
271
  function Section(props) {
@@ -281,90 +299,69 @@ Section.HeaderItem = function (props) {
281
299
  return props.children;
282
300
  };
283
301
 
284
- function PlaygroundRoot(props) {
302
+ function PlaygroundRoot(config) {
285
303
  const {
286
- additionalModules = [],
304
+ additionalModules,
287
305
  // goes into both editor + viewer
288
- actions: actionsConfig = {},
306
+ actions: actionsConfig,
289
307
  emit,
290
- exporter: exporterConfig = {},
291
- viewerProperties = {},
292
- editorProperties = {},
293
- viewerAdditionalModules = [],
294
- editorAdditionalModules = [],
295
- propertiesPanel: propertiesPanelConfig = {},
296
- onInit: onPlaygroundInit,
297
- onStateChanged
298
- } = props;
308
+ exporter: exporterConfig,
309
+ viewerProperties,
310
+ editorProperties,
311
+ viewerAdditionalModules,
312
+ editorAdditionalModules,
313
+ propertiesPanel: propertiesPanelConfig,
314
+ apiLinkTarget
315
+ } = config;
299
316
  const {
300
317
  display: displayActions = true
301
- } = actionsConfig;
302
- const paletteContainerRef = hooks.useRef();
318
+ } = actionsConfig || {};
303
319
  const editorContainerRef = hooks.useRef();
304
- const formContainerRef = hooks.useRef();
305
- const dataContainerRef = hooks.useRef();
306
- const resultContainerRef = hooks.useRef();
320
+ const paletteContainerRef = hooks.useRef();
307
321
  const propertiesPanelContainerRef = hooks.useRef();
308
- const paletteRef = hooks.useRef();
322
+ const viewerContainerRef = hooks.useRef();
323
+ const inputDataContainerRef = hooks.useRef();
324
+ const outputDataContainerRef = hooks.useRef();
309
325
  const formEditorRef = hooks.useRef();
310
- const formRef = hooks.useRef();
311
- const dataEditorRef = hooks.useRef();
312
- const resultViewRef = hooks.useRef();
313
- const propertiesPanelRef = hooks.useRef();
326
+ const formViewerRef = hooks.useRef();
327
+ const inputDataRef = hooks.useRef();
328
+ const outputDataRef = hooks.useRef();
314
329
  const [showEmbed, setShowEmbed] = hooks.useState(false);
315
- const [initialData] = hooks.useState(props.data || undefined);
316
- const [initialSchema, setInitialSchema] = hooks.useState(props.schema);
317
- const [data, setData] = hooks.useState(props.data || {});
318
- const [schema, setSchema] = hooks.useState(props.schema);
319
- const [resultData, setResultData] = hooks.useState({});
330
+ const [schema, setSchema] = hooks.useState();
331
+ const [data, setData] = hooks.useState();
332
+ const load = hooks.useCallback((schema, data) => {
333
+ formEditorRef.current.importSchema(schema, data);
334
+ inputDataRef.current.setValue(toString(data));
335
+ setSchema(schema);
336
+ setData(data);
337
+ }, []);
320
338
 
321
- // pipe to playground API
339
+ // initialize and link the editors
322
340
  hooks.useEffect(() => {
323
- onPlaygroundInit({
324
- attachDataContainer: node => dataEditorRef.current.attachTo(node),
325
- attachEditorContainer: node => formEditorRef.current.attachTo(node),
326
- attachFormContainer: node => formRef.current.attachTo(node),
327
- attachPaletteContainer: node => paletteRef.current.attachTo(node),
328
- attachPropertiesPanelContainer: node => propertiesPanelRef.current.attachTo(node),
329
- attachResultContainer: node => resultViewRef.current.attachTo(node),
330
- get: (name, strict) => formEditorRef.current.get(name, strict),
331
- getDataEditor: () => dataEditorRef.current,
332
- getEditor: () => formEditorRef.current,
333
- getForm: () => formRef.current,
334
- getResultView: () => resultViewRef.current,
335
- getSchema: () => formEditorRef.current.getSchema(),
336
- setSchema: setInitialSchema,
337
- saveSchema: () => formEditorRef.current.saveSchema()
338
- });
339
- }, [onPlaygroundInit]);
340
- hooks.useEffect(() => {
341
- setInitialSchema(props.schema || {});
342
- }, [props.schema]);
343
- hooks.useEffect(() => {
344
- const dataEditor = dataEditorRef.current = new JSONEditor({
345
- value: toString(data),
341
+ const inputDataEditor = inputDataRef.current = new JSONEditor({
346
342
  contentAttributes: {
347
343
  'aria-label': 'Form Input',
348
344
  tabIndex: 0
349
345
  },
350
346
  placeholder: createDataEditorPlaceholder()
351
347
  });
352
- const resultView = resultViewRef.current = new JSONEditor({
348
+ const outputDataEditor = outputDataRef.current = new JSONEditor({
353
349
  readonly: true,
354
- value: toString(resultData),
355
350
  contentAttributes: {
356
351
  'aria-label': 'Form Output',
357
352
  tabIndex: 0
358
353
  }
359
354
  });
360
- const form = formRef.current = new formJsViewer.Form({
361
- additionalModules: [...additionalModules, ...viewerAdditionalModules],
355
+ const formViewer = formViewerRef.current = new formJsViewer.Form({
356
+ container: viewerContainerRef.current,
357
+ additionalModules: [...(additionalModules || []), ...(viewerAdditionalModules || [])],
362
358
  properties: {
363
- ...viewerProperties,
359
+ ...(viewerProperties || {}),
364
360
  'ariaLabel': 'Form Preview'
365
361
  }
366
362
  });
367
363
  const formEditor = formEditorRef.current = new formJsEditor.FormEditor({
364
+ container: editorContainerRef.current,
368
365
  renderer: {
369
366
  compact: true
370
367
  },
@@ -373,17 +370,15 @@ function PlaygroundRoot(props) {
373
370
  },
374
371
  propertiesPanel: {
375
372
  parent: propertiesPanelContainerRef.current,
376
- ...propertiesPanelConfig
373
+ ...(propertiesPanelConfig || {})
377
374
  },
378
375
  exporter: exporterConfig,
379
376
  properties: {
380
- ...editorProperties,
377
+ ...(editorProperties || {}),
381
378
  'ariaLabel': 'Form Definition'
382
379
  },
383
- additionalModules: [...additionalModules, ...editorAdditionalModules]
380
+ additionalModules: [...(additionalModules || []), ...(editorAdditionalModules || [])]
384
381
  });
385
- paletteRef.current = formEditor.get('palette');
386
- propertiesPanelRef.current = formEditor.get('propertiesPanel');
387
382
  formEditor.on('formField.add', ({
388
383
  formField
389
384
  }) => {
@@ -409,7 +404,7 @@ function PlaygroundRoot(props) {
409
404
  ...currentData,
410
405
  [id]: initialDemoData
411
406
  };
412
- dataEditorRef.current.setValue(toString(newData));
407
+ inputDataRef.current.setValue(toString(newData));
413
408
  return newData;
414
409
  });
415
410
  });
@@ -420,10 +415,13 @@ function PlaygroundRoot(props) {
420
415
  // notify interested parties after render
421
416
  emit('formPlayground.rendered');
422
417
  });
423
- form.on('changed', () => {
424
- setResultData(form._getSubmitData());
418
+
419
+ // pipe viewer changes to output data editor
420
+ formViewer.on('changed', () => {
421
+ const submitData = formViewer._getSubmitData();
422
+ outputDataEditor.setValue(toString(submitData));
425
423
  });
426
- dataEditor.on('changed', event => {
424
+ inputDataEditor.on('changed', event => {
427
425
  try {
428
426
  setData(JSON.parse(event.value));
429
427
  } catch (error) {
@@ -431,48 +429,68 @@ function PlaygroundRoot(props) {
431
429
  emit('formPlayground.inputDataError', error);
432
430
  }
433
431
  });
434
- const formContainer = formContainerRef.current;
435
- const editorContainer = editorContainerRef.current;
436
- const dataContainer = dataContainerRef.current;
437
- const resultContainer = resultContainerRef.current;
438
- dataEditor.attachTo(dataContainer);
439
- resultView.attachTo(resultContainer);
440
- form.attachTo(formContainer);
441
- formEditor.attachTo(editorContainer);
432
+ inputDataEditor.attachTo(inputDataContainerRef.current);
433
+ outputDataEditor.attachTo(outputDataContainerRef.current);
442
434
  return () => {
443
- dataEditor.destroy();
444
- resultView.destroy();
445
- form.destroy();
435
+ inputDataEditor.destroy();
436
+ outputDataEditor.destroy();
437
+ formViewer.destroy();
446
438
  formEditor.destroy();
447
439
  };
448
- }, []);
449
- hooks.useEffect(() => {
450
- dataEditorRef.current.setValue(toString(initialData));
451
- }, [initialData]);
440
+ }, [additionalModules, editorAdditionalModules, editorProperties, emit, exporterConfig, propertiesPanelConfig, viewerAdditionalModules, viewerProperties]);
441
+
442
+ // initialize data through props
452
443
  hooks.useEffect(() => {
453
- if (initialSchema) {
454
- formEditorRef.current.importSchema(initialSchema);
455
- dataEditorRef.current.setVariables(formJsViewer.getSchemaVariables(initialSchema));
444
+ if (!config.initialSchema) {
445
+ return;
456
446
  }
457
- }, [initialSchema]);
447
+ load(config.initialSchema, config.initialData || {});
448
+ }, [config.initialSchema, config.initialData, load]);
449
+ hooks.useEffect(() => {
450
+ schema && formViewerRef.current.importSchema(schema, data);
451
+ }, [schema, data]);
458
452
  hooks.useEffect(() => {
459
- if (schema && dataContainerRef.current) {
453
+ if (schema && inputDataContainerRef.current) {
460
454
  const variables = formJsViewer.getSchemaVariables(schema);
461
- dataEditorRef.current.setVariables(variables);
455
+ inputDataRef.current.setVariables(variables);
462
456
  }
463
457
  }, [schema]);
458
+
459
+ // exposes api to parent
464
460
  hooks.useEffect(() => {
465
- schema && formRef.current.importSchema(schema, data);
466
- }, [schema, data]);
467
- hooks.useEffect(() => {
468
- resultViewRef.current.setValue(toString(resultData));
469
- }, [resultData]);
461
+ if (!apiLinkTarget) {
462
+ return;
463
+ }
464
+ apiLinkTarget.api = {
465
+ attachDataContainer: node => inputDataRef.current.attachTo(node),
466
+ attachResultContainer: node => outputDataRef.current.attachTo(node),
467
+ attachFormContainer: node => formViewerRef.current.attachTo(node),
468
+ attachEditorContainer: node => formEditorRef.current.attachTo(node),
469
+ attachPaletteContainer: node => formEditorRef.current.get('palette').attachTo(node),
470
+ attachPropertiesPanelContainer: node => formEditorRef.current.get('propertiesPanel').attachTo(node),
471
+ get: (name, strict) => formEditorRef.current.get(name, strict),
472
+ getDataEditor: () => inputDataRef.current,
473
+ getEditor: () => formEditorRef.current,
474
+ getForm: () => formViewerRef.current,
475
+ getResultView: () => outputDataRef.current,
476
+ getSchema: () => formEditorRef.current.getSchema(),
477
+ saveSchema: () => formEditorRef.current.saveSchema(),
478
+ setSchema: setSchema,
479
+ setData: setData
480
+ };
481
+ }, [apiLinkTarget]);
482
+
483
+ // separate effect for state to avoid re-creating the api object every time
470
484
  hooks.useEffect(() => {
471
- onStateChanged && onStateChanged({
485
+ if (!apiLinkTarget) {
486
+ return;
487
+ }
488
+ apiLinkTarget.api.getState = () => ({
472
489
  schema,
473
490
  data
474
491
  });
475
- }, [onStateChanged, schema, data]);
492
+ apiLinkTarget.api.load = load;
493
+ }, [apiLinkTarget, schema, data, load]);
476
494
  const handleDownload = hooks.useCallback(() => {
477
495
  download(JSON.stringify(schema, null, ' '), 'form.json', 'text/json');
478
496
  }, [schema]);
@@ -520,19 +538,19 @@ function PlaygroundRoot(props) {
520
538
  }), jsxRuntime.jsx(Section, {
521
539
  name: "Form Preview",
522
540
  children: jsxRuntime.jsx("div", {
523
- ref: formContainerRef,
541
+ ref: viewerContainerRef,
524
542
  class: "fjs-pgl-form-container"
525
543
  })
526
544
  }), jsxRuntime.jsx(Section, {
527
545
  name: "Form Input",
528
546
  children: jsxRuntime.jsx("div", {
529
- ref: dataContainerRef,
547
+ ref: inputDataContainerRef,
530
548
  class: "fjs-pgl-text-container"
531
549
  })
532
550
  }), jsxRuntime.jsx(Section, {
533
551
  name: "Form Output",
534
552
  children: jsxRuntime.jsx("div", {
535
- ref: resultContainerRef,
553
+ ref: outputDataContainerRef,
536
554
  class: "fjs-pgl-text-container"
537
555
  })
538
556
  })]
@@ -557,16 +575,11 @@ function createDataEditorPlaceholder() {
557
575
  function Playground(options) {
558
576
  const {
559
577
  container: parent,
560
- schema,
561
- data,
578
+ schema: initialSchema,
579
+ data: initialData,
562
580
  ...rest
563
581
  } = options;
564
582
  const emitter = mitt();
565
- let state = {
566
- data,
567
- schema
568
- };
569
- let ref;
570
583
  const container = document.createElement('div');
571
584
  container.classList.add('fjs-pgl-parent');
572
585
  if (parent) {
@@ -576,63 +589,52 @@ function Playground(options) {
576
589
  const file = files[0];
577
590
  if (file) {
578
591
  try {
579
- ref.setSchema(JSON.parse(file.contents));
592
+ this.api.setSchema(JSON.parse(file.contents));
580
593
  } catch (err) {
581
594
 
582
595
  // TODO(nikku): indicate JSON parse error
583
596
  }
584
597
  }
585
598
  });
586
- const withRef = function (fn) {
599
+ const safe = function (fn) {
587
600
  return function (...args) {
588
- if (!ref) {
601
+ if (!this.api) {
589
602
  throw new Error('Playground is not initialized.');
590
603
  }
591
604
  return fn(...args);
592
605
  };
593
606
  };
594
- const onInit = function (_ref) {
595
- ref = _ref;
596
- emitter.emit('formPlayground.init');
597
- };
598
607
  container.addEventListener('dragover', handleDrop);
599
608
  preact.render(jsxRuntime.jsx(PlaygroundRoot, {
600
- data: data,
609
+ initialSchema: initialSchema,
610
+ initialData: initialData,
601
611
  emit: emitter.emit,
602
- onInit: onInit,
603
- onStateChanged: _state => state = _state,
604
- schema: schema,
612
+ apiLinkTarget: this,
605
613
  ...rest
606
614
  }), container);
607
615
  this.on = emitter.on;
608
616
  this.off = emitter.off;
609
617
  this.emit = emitter.emit;
610
- this.on('destroy', function () {
618
+ this.on('destroy', () => {
611
619
  preact.render(null, container);
612
- });
613
- this.on('destroy', function () {
614
620
  parent.removeChild(container);
615
621
  });
616
- this.getState = function () {
617
- return state;
618
- };
619
- this.getSchema = withRef(() => ref.getSchema());
620
- this.setSchema = withRef(schema => ref.setSchema(schema));
621
- this.saveSchema = withRef(() => ref.saveSchema());
622
- this.get = withRef((name, strict) => ref.get(name, strict));
623
- this.getDataEditor = withRef(() => ref.getDataEditor());
624
- this.getEditor = withRef(() => ref.getEditor());
625
- this.getForm = withRef((name, strict) => ref.getForm(name, strict));
626
- this.getResultView = withRef(() => ref.getResultView());
627
- this.destroy = function () {
628
- this.emit('destroy');
629
- };
630
- this.attachEditorContainer = withRef(node => ref.attachEditorContainer(node));
631
- this.attachPreviewContainer = withRef(node => ref.attachFormContainer(node));
632
- this.attachDataContainer = withRef(node => ref.attachDataContainer(node));
633
- this.attachResultContainer = withRef(node => ref.attachResultContainer(node));
634
- this.attachPaletteContainer = withRef(node => ref.attachPaletteContainer(node));
635
- this.attachPropertiesPanelContainer = withRef(node => ref.attachPropertiesPanelContainer(node));
622
+ this.destroy = () => this.emit('destroy');
623
+ this.getState = safe(() => this.api.getState());
624
+ this.getSchema = safe(() => this.api.getSchema());
625
+ this.setSchema = safe(schema => this.api.setSchema(schema));
626
+ this.saveSchema = safe(() => this.api.saveSchema());
627
+ this.get = safe((name, strict) => this.api.get(name, strict));
628
+ this.getDataEditor = safe(() => this.api.getDataEditor());
629
+ this.getEditor = safe(() => this.api.getEditor());
630
+ this.getForm = safe((name, strict) => this.api.getForm(name, strict));
631
+ this.getResultView = safe(() => this.api.getResultView());
632
+ this.attachEditorContainer = safe(node => this.api.attachEditorContainer(node));
633
+ this.attachPreviewContainer = safe(node => this.api.attachFormContainer(node));
634
+ this.attachDataContainer = safe(node => this.api.attachDataContainer(node));
635
+ this.attachResultContainer = safe(node => this.api.attachResultContainer(node));
636
+ this.attachPaletteContainer = safe(node => this.api.attachPaletteContainer(node));
637
+ this.attachPropertiesPanelContainer = safe(node => this.api.attachPropertiesPanelContainer(node));
636
638
  }
637
639
 
638
640
  exports.Playground = Playground;