From 0478f108ff0b9811763467cdfc83a25514ec3220 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 18 Aug 2025 10:09:35 -0700 Subject: [PATCH 1/6] optional toolbar --- packages/host/src/listener.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index 3ef33f8f..6ebfba7a 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -80,7 +80,10 @@ export class Listener { this.uploadButton = getElement(options.uploadButton); this.fileInput = getElement(options.fileInput); this.textarea = getElement(options.textarea); - this.toolbar = new Toolbar(options.toolbar); + + if (options.toolbar) { + this.toolbar = new Toolbar(options.toolbar); + } if (!this.appDiv) { throw new Error('App container not found'); From 896dba9bbf24b42a667ed6669bcdb3425b9732e8 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 18 Aug 2025 15:08:27 -0700 Subject: [PATCH 2/6] host & toolbar --- docs/dist/v1/chartifact-toolbar.css | 27 +++--- docs/view/index.html | 10 +- packages/host/dev/index.ts | 12 ++- packages/host/index.html | 10 +- packages/host/src/listener.ts | 92 +++++-------------- packages/toolbar/chartifact-toolbar.css | 27 +++--- packages/toolbar/src/toolbar.ts | 13 ++- packages/vscode-resources/html/html-json.html | 12 +-- .../vscode-resources/html/html-markdown.html | 12 +-- packages/vscode-resources/html/preview.html | 4 +- packages/vscode-resources/src/html-json.ts | 6 +- .../vscode-resources/src/html-markdown.ts | 6 +- packages/vscode-resources/src/preview.ts | 14 +-- packages/web-frontend/src/view.ts | 49 +++++++++- 14 files changed, 149 insertions(+), 145 deletions(-) diff --git a/docs/dist/v1/chartifact-toolbar.css b/docs/dist/v1/chartifact-toolbar.css index e3b27765..15abb1d7 100644 --- a/docs/dist/v1/chartifact-toolbar.css +++ b/docs/dist/v1/chartifact-toolbar.css @@ -1,39 +1,42 @@ -body { +.chartifact-body { display: flex; flex-direction: column; height: 100vh; margin: 0; } -main { +.chartifact-main { display: flex; flex: 1; } -textarea { - flex: 1; +.chartifact-main textarea { resize: none; } -#app { +.chartifact-source { + flex: 1; +} + +.chartifact-preview { flex: 2; } /* Base styles for the toolbar */ -#toolbar { +.chartifact-toolbar { padding: 0.5em 1em; font-weight: bold; } /* Default (OS light theme) */ -#toolbar { +.chartifact-toolbar { background-color: #f3f3f3; color: #000; } /* OS dark theme fallback */ @media (prefers-color-scheme: dark) { - #toolbar { + .chartifact-toolbar { background-color: #1e1e1e; color: #ffffff; border-bottom-color: rgba(255, 255, 255, 0.1); @@ -41,22 +44,22 @@ textarea { } /* VS Code light theme */ -body.vscode-light #toolbar { +body.vscode-light .chartifact-toolbar { background-color: #f3f3f3; color: #000; border-bottom-color: rgba(0, 0, 0, 0.1); } /* VS Code dark theme */ -body.vscode-dark #toolbar { +body.vscode-dark .chartifact-toolbar { background-color: #1e1e1e; color: #fff; border-bottom-color: rgba(255, 255, 255, 0.1); } /* VS Code high contrast */ -body.vscode-high-contrast #toolbar, -body.vscode-high-contrast-light #toolbar { +body.vscode-high-contrast .chartifact-toolbar, +body.vscode-high-contrast-light .chartifact-toolbar { background-color: #000; color: #fff; border-bottom: 2px solid #fff; diff --git a/docs/view/index.html b/docs/view/index.html index c0877b24..fe2fe4e5 100644 --- a/docs/view/index.html +++ b/docs/view/index.html @@ -20,16 +20,16 @@ - + -
+
-
+
- -
+
Loading... diff --git a/packages/host/dev/index.ts b/packages/host/dev/index.ts index 1825a773..7b1def8a 100644 --- a/packages/host/dev/index.ts +++ b/packages/host/dev/index.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. */ import { SandboxOptions, Sandbox } from '@microsoft/chartifact-sandbox'; +import { Toolbar } from 'toolbar'; import { rendererCss } from '@microsoft/chartifact-sandbox/src/resources/rendererCss.ts'; import { rendererUmdJs } from '@microsoft/chartifact-sandbox/src/resources/rendererUmdJs.ts'; +import { Listener } from '../src/index.ts'; class LocalSandbox extends Sandbox { constructor(elementOrSelector: string | HTMLElement, markdown: string, options: SandboxOptions) { @@ -26,18 +28,18 @@ class LocalSandbox extends Sandbox { } -import { Listener } from '../src/index.ts'; +const toolbar = new Toolbar('.chartifact-toolbar', { textarea: document.querySelector('#source') as HTMLTextAreaElement, tweakButton: true }); + new Listener({ - app: '#app', + preview: '#preview', loading: '#loading', help: '#help', uploadButton: '#upload-btn', fileInput: '#file-input', - textarea: '#textarea', - toolbar: '#toolbar', + toolbar, onApprove: (message) => { const { specs } = message; return specs; }, sandboxConstructor: LocalSandbox, -}); \ No newline at end of file +}); diff --git a/packages/host/index.html b/packages/host/index.html index 1f0f093d..a4f27e51 100644 --- a/packages/host/index.html +++ b/packages/host/index.html @@ -13,15 +13,15 @@ - + -
+
-
+
- + -
+
Loading... diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index 6ebfba7a..6d50a850 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -9,7 +9,7 @@ import { setupDragDropHandling } from './dragdrop.js'; import { setupFileUpload } from './upload.js'; import { checkUrlForFile } from './url.js'; import { setupPostMessageHandling } from './post-receive.js'; -import { InteractiveDocument, InteractiveDocumentWithSchema } from '@microsoft/chartifact-schema'; +import { InteractiveDocument } from '@microsoft/chartifact-schema'; import { postStatus } from './post-send.js'; import { ListenOptions } from './types.js'; import { SpecReview, SandboxedPreHydrateMessage } from 'common'; @@ -30,15 +30,15 @@ function show(element: HTMLElement, shown: boolean) { } export interface InitializeOptions { - app: string | HTMLElement; + preview: string | HTMLElement; loading?: string | HTMLElement; help?: string | HTMLElement; uploadButton?: string | HTMLElement; fileInput?: string | HTMLElement; - textarea?: string | HTMLTextAreaElement; - toolbar?: string | HTMLElement; + toolbar?: Toolbar; options?: ListenOptions; onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; + onSetMode?: (mode: 'markdown' | 'json', markdown: string, interactiveDocument: InteractiveDocument) => void; sandboxConstructor?: typeof Sandbox; } @@ -54,16 +54,16 @@ const defaultOptions: ListenOptions = { export class Listener { public options: ListenOptions; - public appDiv: HTMLElement; + public previewDiv: HTMLElement; public loadingDiv: HTMLElement; public helpDiv: HTMLElement; public uploadButton: HTMLElement; public fileInput: HTMLElement; - public textarea: HTMLTextAreaElement; public toolbar: Toolbar; public sandbox: Sandbox; public sandboxReady: boolean = false; public onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; + public onSetMode: (mode: 'markdown' | 'json', markdown: string, interactiveDocument: InteractiveDocument) => void; private removeInteractionHandlers: (() => void)[]; private sandboxConstructor?: typeof Sandbox; @@ -72,20 +72,20 @@ export class Listener { this.sandboxConstructor = options.sandboxConstructor || Sandbox; this.options = { ...defaultOptions, ...options?.options }; this.onApprove = options.onApprove; + this.onSetMode = options.onSetMode || (() => { }); this.removeInteractionHandlers = []; - this.appDiv = getElement(options.app); + this.previewDiv = getElement(options.preview); this.loadingDiv = getElement(options.loading); this.helpDiv = getElement(options.help); this.uploadButton = getElement(options.uploadButton); this.fileInput = getElement(options.fileInput); - this.textarea = getElement(options.textarea); if (options.toolbar) { - this.toolbar = new Toolbar(options.toolbar); + this.toolbar = options.toolbar; } - if (!this.appDiv) { + if (!this.previewDiv) { throw new Error('App container not found'); } @@ -123,7 +123,7 @@ export class Listener { this.sandboxReady = false; - this.sandbox = new (this.sandboxConstructor)(this.appDiv, markdown, { + this.sandbox = new (this.sandboxConstructor)(this.previewDiv, markdown, { onReady: () => { this.sandboxReady = true; @@ -147,7 +147,7 @@ export class Listener { public errorHandler(error: Error | string, details: string) { show(this.loadingDiv, false); show(this.helpDiv, false); - show(this.appDiv, true); + show(this.previewDiv, true); let message: string; if (typeof error === 'string') { @@ -168,74 +168,26 @@ export class Listener { this.render(markdown, undefined); } else { // Clear previous content - this.appDiv.innerHTML = ''; + this.previewDiv.innerHTML = ''; const h1 = document.createElement('h1'); h1.textContent = 'Error'; const pMessage = document.createElement('p'); pMessage.textContent = message; const pDetails = document.createElement('p'); pDetails.textContent = details; - this.appDiv.appendChild(h1); - this.appDiv.appendChild(pMessage); - this.appDiv.appendChild(pDetails); + this.previewDiv.appendChild(h1); + this.previewDiv.appendChild(pMessage); + this.previewDiv.appendChild(pDetails); } } - private bindTextareaToCompiler() { - const render = () => { - const json = this.textarea.value; - try { - const interactiveDocument = JSON.parse(json) as InteractiveDocumentWithSchema; - if (typeof interactiveDocument !== 'object') { - this.errorHandler( - 'Invalid JSON format', - 'Please provide a valid Interactive Document JSON.' - ); - return; - } - this.renderInteractiveDocument(interactiveDocument); - } catch (error) { - this.errorHandler( - error, - 'Failed to parse Interactive Document JSON' - ); - } - }; - - this.textarea.addEventListener('input', render); - - render(); // Initial render - } - - private bindTextareaToMarkdown() { - const render = () => { - const markdown = this.textarea.value; - this.renderMarkdown(markdown); - }; - - this.textarea.addEventListener('input', render); - - render(); // Initial render - } - public render(markdown?: string, interactiveDocument?: InteractiveDocument) { if (interactiveDocument) { - if (this.textarea) { - this.textarea.value = JSON.stringify(interactiveDocument, null, 2); - this.hideLoadingAndHelp(); - this.bindTextareaToCompiler(); - } else { - this.renderInteractiveDocument(interactiveDocument); - } - + this.onSetMode('json', null, interactiveDocument); + this.renderInteractiveDocument(interactiveDocument); } else if (markdown) { - if (this.textarea) { - this.textarea.value = markdown; - this.hideLoadingAndHelp(); - this.bindTextareaToMarkdown(); - } else { - this.renderMarkdown(markdown); - } + this.onSetMode('markdown', markdown, null); + this.renderMarkdown(markdown); } else { this.errorHandler( 'No content provided', @@ -247,7 +199,7 @@ export class Listener { this.removeInteractionHandlers = []; // Clear handlers after rendering } - private renderInteractiveDocument(content: InteractiveDocument) { + public renderInteractiveDocument(content: InteractiveDocument) { postStatus(this.options.postMessageTarget, { type: 'hostStatus', hostStatus: 'compiling', details: 'Starting interactive document compilation' }); const markdown = targetMarkdown(content); this.renderMarkdown(markdown); @@ -258,7 +210,7 @@ export class Listener { show(this.helpDiv, false); } - private renderMarkdown(markdown: string) { + public renderMarkdown(markdown: string) { this.hideLoadingAndHelp(); try { diff --git a/packages/toolbar/chartifact-toolbar.css b/packages/toolbar/chartifact-toolbar.css index e3b27765..15abb1d7 100644 --- a/packages/toolbar/chartifact-toolbar.css +++ b/packages/toolbar/chartifact-toolbar.css @@ -1,39 +1,42 @@ -body { +.chartifact-body { display: flex; flex-direction: column; height: 100vh; margin: 0; } -main { +.chartifact-main { display: flex; flex: 1; } -textarea { - flex: 1; +.chartifact-main textarea { resize: none; } -#app { +.chartifact-source { + flex: 1; +} + +.chartifact-preview { flex: 2; } /* Base styles for the toolbar */ -#toolbar { +.chartifact-toolbar { padding: 0.5em 1em; font-weight: bold; } /* Default (OS light theme) */ -#toolbar { +.chartifact-toolbar { background-color: #f3f3f3; color: #000; } /* OS dark theme fallback */ @media (prefers-color-scheme: dark) { - #toolbar { + .chartifact-toolbar { background-color: #1e1e1e; color: #ffffff; border-bottom-color: rgba(255, 255, 255, 0.1); @@ -41,22 +44,22 @@ textarea { } /* VS Code light theme */ -body.vscode-light #toolbar { +body.vscode-light .chartifact-toolbar { background-color: #f3f3f3; color: #000; border-bottom-color: rgba(0, 0, 0, 0.1); } /* VS Code dark theme */ -body.vscode-dark #toolbar { +body.vscode-dark .chartifact-toolbar { background-color: #1e1e1e; color: #fff; border-bottom-color: rgba(255, 255, 255, 0.1); } /* VS Code high contrast */ -body.vscode-high-contrast #toolbar, -body.vscode-high-contrast-light #toolbar { +body.vscode-high-contrast .chartifact-toolbar, +body.vscode-high-contrast-light .chartifact-toolbar { background-color: #000; color: #fff; border-bottom: 2px solid #fff; diff --git a/packages/toolbar/src/toolbar.ts b/packages/toolbar/src/toolbar.ts index d0ef9d52..11d4b8d6 100644 --- a/packages/toolbar/src/toolbar.ts +++ b/packages/toolbar/src/toolbar.ts @@ -26,13 +26,18 @@ export class Toolbar { this.toolbarElement.innerHTML = html; if (this.options.tweakButton) { - const tweakButton = this.toolbarElement.querySelector('#tweak') as HTMLButtonElement; - tweakButton?.addEventListener('click', () => { - this.options.textarea.style.display = this.options.textarea.style.display === 'none' ? '' : 'none'; - }); + this.showTweakButton(); } } + showTweakButton() { + const tweakButton = this.toolbarElement.querySelector('#tweak') as HTMLButtonElement; + tweakButton.style.display = ''; + tweakButton?.addEventListener('click', () => { + this.options.textarea.style.display = this.options.textarea.style.display === 'none' ? '' : 'none'; + }); + } + manageTextareaVisibilityForAgents() { const { textarea } = this.options; diff --git a/packages/vscode-resources/html/html-json.html b/packages/vscode-resources/html/html-json.html index 77b34fa5..9ea19f29 100644 --- a/packages/vscode-resources/html/html-json.html +++ b/packages/vscode-resources/html/html-json.html @@ -10,16 +10,16 @@ - + -
+
-
- -
- +
+
{{HTML_JSON_JS}} diff --git a/packages/vscode-resources/html/html-markdown.html b/packages/vscode-resources/html/html-markdown.html index fbe24bb9..6b6b6595 100644 --- a/packages/vscode-resources/html/html-markdown.html +++ b/packages/vscode-resources/html/html-markdown.html @@ -9,16 +9,16 @@ - + -
+
-
- -
- +
+
{{HTML_MARKDOWN_JS}} diff --git a/packages/vscode-resources/html/preview.html b/packages/vscode-resources/html/preview.html index 63cb77a8..e23c4ce0 100644 --- a/packages/vscode-resources/html/preview.html +++ b/packages/vscode-resources/html/preview.html @@ -25,12 +25,12 @@ {{HOST_OPTIONS}} - +
Loading...
-
+
diff --git a/packages/vscode-resources/src/html-json.ts b/packages/vscode-resources/src/html-json.ts index 7fd4c75f..708fa53f 100644 --- a/packages/vscode-resources/src/html-json.ts +++ b/packages/vscode-resources/src/html-json.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ window.addEventListener('DOMContentLoaded', () => { - const textarea = document.getElementById('markdown-input') as HTMLTextAreaElement; + const textarea = document.querySelector('#source') as HTMLTextAreaElement; let sandbox: Chartifact.sandbox.Sandbox; const render = () => { const json = textarea.value; @@ -19,7 +19,7 @@ window.addEventListener('DOMContentLoaded', () => { markdown = 'Failed to parse Interactive Document JSON'; } if (!sandbox) { - sandbox = new Chartifact.sandbox.Sandbox('#app', markdown, { + sandbox = new Chartifact.sandbox.Sandbox('#preview', markdown, { onApprove: (message) => { //Here you can approve unapproved specs per your own policy const { specs } = message; @@ -36,6 +36,6 @@ window.addEventListener('DOMContentLoaded', () => { textarea.addEventListener('input', render); render(); - const toolbar = Chartifact.toolbar.create('#toolbar', { tweakButton: true, textarea }); + const toolbar = Chartifact.toolbar.create('.chartifact-toolbar', { tweakButton: true, textarea }); toolbar.manageTextareaVisibilityForAgents(); }); diff --git a/packages/vscode-resources/src/html-markdown.ts b/packages/vscode-resources/src/html-markdown.ts index f7f64522..4a3a8e7f 100644 --- a/packages/vscode-resources/src/html-markdown.ts +++ b/packages/vscode-resources/src/html-markdown.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. */ window.addEventListener('DOMContentLoaded', () => { - const textarea = document.getElementById('markdown-input') as HTMLTextAreaElement; - const sandbox = new Chartifact.sandbox.Sandbox('#app', textarea.value, { + const textarea = document.querySelector('#source') as HTMLTextAreaElement; + const sandbox = new Chartifact.sandbox.Sandbox('#preview', textarea.value, { onApprove: (message) => { //Here you can approve unapproved specs per your own policy const { specs } = message; @@ -19,6 +19,6 @@ window.addEventListener('DOMContentLoaded', () => { sandbox.send(textarea.value); }); - const toolbar = Chartifact.toolbar.create('#toolbar', { tweakButton: true, textarea }); + const toolbar = Chartifact.toolbar.create('.chartifact-toolbar', { tweakButton: true, textarea }); toolbar.manageTextareaVisibilityForAgents(); }); diff --git a/packages/vscode-resources/src/preview.ts b/packages/vscode-resources/src/preview.ts index 001c032f..855702eb 100644 --- a/packages/vscode-resources/src/preview.ts +++ b/packages/vscode-resources/src/preview.ts @@ -8,29 +8,25 @@ window.addEventListener('DOMContentLoaded', () => { const vscode = acquireVsCodeApi(); - const x = (event: MessageEvent) => { + const messageListener = (event: MessageEvent) => { const options: Chartifact.host.ListenOptions = { ...hostOptions, ...{ postMessageTarget: vscode } }; - let offlineDeps = ''; - const message = event.data as Chartifact.common.EditorSetOfflineDependenciesMessage; if (message.type === 'editorSetOfflineDependencies') { - offlineDeps = message.offlineDeps; - class OfflineSandbox extends Chartifact.sandbox.Sandbox { constructor(element: string | HTMLElement, markdown: string, options: Chartifact.sandbox.SandboxOptions) { super(element, markdown, options); } getDependencies() { - return offlineDeps; + return message.offlineDeps; } } const host = new Chartifact.host.Listener({ - app: '#app', + preview: '#preview', loading: '#loading', options, onApprove: (message: Chartifact.common.SandboxedPreHydrateMessage) => { @@ -42,11 +38,11 @@ window.addEventListener('DOMContentLoaded', () => { sandboxConstructor: OfflineSandbox, }); - window.removeEventListener('message', x); + window.removeEventListener('message', messageListener); } }; - window.addEventListener('message', x); + window.addEventListener('message', messageListener); const editorGetOfflineDependenciesMessage: Chartifact.common.EditorGetOfflineDependenciesMessage = { type: 'editorGetOfflineDependencies', diff --git a/packages/web-frontend/src/view.ts b/packages/web-frontend/src/view.ts index ff8496eb..ae72e2b7 100644 --- a/packages/web-frontend/src/view.ts +++ b/packages/web-frontend/src/view.ts @@ -5,18 +5,61 @@ declare let host: Chartifact.host.Listener; window.addEventListener('DOMContentLoaded', () => { + + let render = () => { }; + + const textarea = document.querySelector('#source') as HTMLTextAreaElement; + textarea.addEventListener('input', render); + const toolbar = new Chartifact.toolbar.Toolbar('.chartifact-toolbar', { textarea }); + host = new Chartifact.host.Listener({ - app: '#app', + preview: '#preview', loading: '#loading', help: '#help', uploadButton: '#upload-btn', fileInput: '#file-input', - textarea: '#textarea', - toolbar: '#toolbar', + toolbar, onApprove: (message: Chartifact.common.SandboxedPreHydrateMessage) => { // TODO look through each spec and override policy to approve unapproved for https://microsoft.github.io/chartifact/ const { specs } = message; return specs; }, + onSetMode: (mode, markdown, interactiveDocument) => { + switch (mode) { + case 'json': + textarea.value = JSON.stringify(interactiveDocument, null, 2); + render = () => { + const json = textarea.value; + try { + const interactiveDocument = JSON.parse(json) as Chartifact.schema.InteractiveDocumentWithSchema; + if (typeof interactiveDocument !== 'object') { + host.errorHandler( + 'Invalid JSON format', + 'Please provide a valid Interactive Document JSON.' + ); + return; + } + host.renderInteractiveDocument(interactiveDocument); + } catch (error) { + host.errorHandler( + error, + 'Failed to parse Interactive Document JSON' + ); + } + }; + break; + case 'markdown': + textarea.value = markdown; + render = () => { + const markdown = textarea.value; + host.renderMarkdown(markdown); + }; + break; + default: + return; + } + toolbar.showTweakButton(); + }, }); + }); From 3097b1438573455679fca80bf4545656f30c4b5d Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 18 Aug 2025 15:09:39 -0700 Subject: [PATCH 3/6] deactivate signal bus --- packages/markdown/src/plugins/checkbox.ts | 3 +- packages/markdown/src/plugins/dropdown.ts | 5 ++- packages/markdown/src/plugins/presets.ts | 9 ++-- packages/markdown/src/plugins/slider.ts | 3 +- packages/markdown/src/plugins/tabulator.ts | 7 ++-- packages/markdown/src/plugins/textbox.ts | 3 +- packages/markdown/src/plugins/vega.ts | 49 +++++++++++----------- packages/markdown/src/renderer.ts | 11 +++-- packages/markdown/src/signalbus.ts | 24 ++++++++--- 9 files changed, 69 insertions(+), 45 deletions(-) diff --git a/packages/markdown/src/plugins/checkbox.ts b/packages/markdown/src/plugins/checkbox.ts index 41289590..ae3d53d1 100644 --- a/packages/markdown/src/plugins/checkbox.ts +++ b/packages/markdown/src/plugins/checkbox.ts @@ -25,6 +25,7 @@ const className = pluginClassName(pluginName); export const checkboxPlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const checkboxInstances: CheckboxInstance[] = []; for (let index = 0; index < specs.length; index++) { const specReview = specs[index]; @@ -78,7 +79,7 @@ export const checkboxPlugin: Plugin = { isData: false, }, }; - renderer.signalBus.broadcast(checkboxInstance.id, batch); + signalBus.broadcast(checkboxInstance.id, batch); }); }, getCurrentSignalValue: () => { diff --git a/packages/markdown/src/plugins/dropdown.ts b/packages/markdown/src/plugins/dropdown.ts index d6352b5a..218f9530 100644 --- a/packages/markdown/src/plugins/dropdown.ts +++ b/packages/markdown/src/plugins/dropdown.ts @@ -25,6 +25,7 @@ const className = pluginClassName(pluginName); export const dropdownPlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const dropdownInstances: DropdownInstance[] = []; for (let index = 0; index < specs.length; index++) { const specReview = specs[index]; @@ -53,7 +54,7 @@ export const dropdownPlugin: Plugin = { const dropdownInstance: DropdownInstance = { id: `${pluginName}-${index}`, spec, element }; dropdownInstances.push(dropdownInstance); } - const instances = dropdownInstances.map((dropdownInstance, index) : IInstance => { + const instances = dropdownInstances.map((dropdownInstance, index): IInstance => { const { element, spec } = dropdownInstance; const initialSignals = [{ name: spec.variableId, @@ -129,7 +130,7 @@ export const dropdownPlugin: Plugin = { isData: false, }, }; - renderer.signalBus.broadcast(dropdownInstance.id, batch); + signalBus.broadcast(dropdownInstance.id, batch); }); }, getCurrentSignalValue: () => { diff --git a/packages/markdown/src/plugins/presets.ts b/packages/markdown/src/plugins/presets.ts index aafef4fd..478f32ed 100644 --- a/packages/markdown/src/plugins/presets.ts +++ b/packages/markdown/src/plugins/presets.ts @@ -23,6 +23,7 @@ const className = pluginClassName(pluginName); export const presetsPlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const presetsInstances: PresetsInstance[] = []; for (let index = 0; index < specs.length; index++) { const specReview = specs[index]; @@ -58,7 +59,7 @@ export const presetsPlugin: Plugin = { for (const [signalName, value] of Object.entries(preset.state)) { batch[signalName] = { value, isData: false }; } - renderer.signalBus.broadcast(id, batch); + signalBus.broadcast(id, batch); }; li.appendChild(button); li.appendChild(document.createTextNode('\u00A0')); @@ -85,10 +86,10 @@ export const presetsPlugin: Plugin = { ...presetsInstance, initialSignals, broadcastComplete: async () => { - //populate state from the renderer.signalBus.signalDeps + //populate state from the signalBus.signalDeps const state: { [signalName: string]: unknown } = {}; - for (const signalName of Object.keys(renderer.signalBus.signalDeps)) { - state[signalName] = renderer.signalBus.signalDeps[signalName].value; + for (const signalName of Object.keys(signalBus.signalDeps)) { + state[signalName] = signalBus.signalDeps[signalName].value; } // highlight any presets that have the same signals and values as the current state setAllPresetsActiveState(presetsInstance, state); diff --git a/packages/markdown/src/plugins/slider.ts b/packages/markdown/src/plugins/slider.ts index 4b43785a..5497d3ec 100644 --- a/packages/markdown/src/plugins/slider.ts +++ b/packages/markdown/src/plugins/slider.ts @@ -25,6 +25,7 @@ const className = pluginClassName(pluginName); export const sliderPlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const sliderInstances: SliderInstance[] = []; for (let index = 0; index < specs.length; index++) { const specReview = specs[index]; @@ -88,7 +89,7 @@ export const sliderPlugin: Plugin = { isData: false, }, }; - renderer.signalBus.broadcast(sliderInstance.id, batch); + signalBus.broadcast(sliderInstance.id, batch); }; element.addEventListener('input', updateValue); diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index 662aaf2e..26cf1719 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -39,6 +39,7 @@ const className = pluginClassName(pluginName); export const tabulatorPlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className, inspectTabulatorSpec, { style: 'box-sizing: border-box;' }), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const tabulatorInstances: TabulatorInstance[] = []; // Generate a unique field name for the delete column, used for all tables in this hydration @@ -154,8 +155,8 @@ export const tabulatorPlugin: Plugin = { isData: true, }, }; - renderer.signalBus.log(tabulatorInstance.id, 'sending batch', batch); - renderer.signalBus.broadcast(tabulatorInstance.id, batch); + signalBus.log(tabulatorInstance.id, 'sending batch', batch); + signalBus.broadcast(tabulatorInstance.id, batch); } const setData = (data: object[]) => { table.setData(data).then(() => { @@ -225,7 +226,7 @@ export const tabulatorPlugin: Plugin = { } if (resetBtn) { resetBtn.onclick = () => { - const value = renderer.signalBus.signalDeps[spec.dataSourceName].value; + const value = signalBus.signalDeps[spec.dataSourceName].value; if (Array.isArray(value)) { setData(value); } diff --git a/packages/markdown/src/plugins/textbox.ts b/packages/markdown/src/plugins/textbox.ts index 7460101a..5f02b90b 100644 --- a/packages/markdown/src/plugins/textbox.ts +++ b/packages/markdown/src/plugins/textbox.ts @@ -25,6 +25,7 @@ const className = pluginClassName(pluginName); export const textboxPlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const textboxInstances: TextboxInstance[] = []; for (let index = 0; index < specs.length; index++) { const specReview = specs[index]; @@ -83,7 +84,7 @@ export const textboxPlugin: Plugin = { isData: false, }, }; - renderer.signalBus.broadcast(textboxInstance.id, batch); + signalBus.broadcast(textboxInstance.id, batch); }; element.addEventListener('input', updateValue); diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index a0eebe70..5bf7ce02 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -7,7 +7,7 @@ import { changeset, parse, View, expressionFunction, LoggerInterface } from 'veg import { Batch, IInstance, Plugin, PrioritizedSignal, RawFlaggableSpec } from '../factory.js'; import { BaseSignal, InitSignal, NewSignal, Runtime, Spec, ValuesData } from 'vega-typings'; import { ErrorHandler, Renderer } from '../renderer.js'; -import { LogLevel } from '../signalbus.js'; +import { LogLevel, SignalBus } from '../signalbus.js'; import { pluginClassName } from './util.js'; import { defaultCommonOptions } from 'common'; import { flaggableJsonPlugin, } from './config.js'; @@ -44,6 +44,7 @@ export function inspectVegaSpec(spec: Spec) { export const vegaPlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className, inspectVegaSpec), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; //initialize the expressionFunction only once if (!expressionsInitialized) { expressionFunction('encodeURIComponent', encodeURIComponent); @@ -96,13 +97,13 @@ export const vegaPlugin: Plugin = { const { spec, view, initialSignals } = vegaInstance; const startBatch = (from: string) => { if (!vegaInstance.batch) { - renderer.signalBus.log(vegaInstance.id, 'starting batch', from); + signalBus.log(vegaInstance.id, 'starting batch', from); vegaInstance.batch = {}; view.runAfter(() => { const { batch } = vegaInstance; vegaInstance.batch = undefined; - renderer.signalBus.log(vegaInstance.id, 'sending batch', batch); - renderer.signalBus.broadcast(vegaInstance.id, batch); + signalBus.log(vegaInstance.id, 'sending batch', batch); + signalBus.broadcast(vegaInstance.id, batch); }); } }; @@ -110,27 +111,27 @@ export const vegaPlugin: Plugin = { ...vegaInstance, initialSignals, receiveBatch: async (batch, from) => { - renderer.signalBus.log(vegaInstance.id, 'received batch', batch, from); + signalBus.log(vegaInstance.id, 'received batch', batch, from); return new Promise(resolve => { view.runAfter(async () => { - if (receiveBatch(batch, renderer, vegaInstance)) { - renderer.signalBus.log(vegaInstance.id, 'running after _pulse, changes from', from); + if (receiveBatch(batch, signalBus, vegaInstance)) { + signalBus.log(vegaInstance.id, 'running after _pulse, changes from', from); vegaInstance.needToRun = true; } else { - renderer.signalBus.log(vegaInstance.id, 'no changes'); + signalBus.log(vegaInstance.id, 'no changes'); } - renderer.signalBus.log(vegaInstance.id, 'running view after _pulse finished'); + signalBus.log(vegaInstance.id, 'running view after _pulse finished'); resolve(); }); }); }, broadcastComplete: async () => { - renderer.signalBus.log(vegaInstance.id, 'broadcastComplete'); + signalBus.log(vegaInstance.id, 'broadcastComplete'); if (vegaInstance.needToRun) { view.runAfter(() => { view.runAsync(); //do not await, since we are already in a runAfter vegaInstance.needToRun = false; - renderer.signalBus.log(vegaInstance.id, 'running view after broadcastComplete'); + signalBus.log(vegaInstance.id, 'running view after broadcastComplete'); }); } }, @@ -140,15 +141,15 @@ export const vegaPlugin: Plugin = { if (isData) { const matchData = spec.data?.find(data => data.name === signalName); if (matchData && vegaInstance.dataSignals.includes(matchData.name)) { - renderer.signalBus.log(vegaInstance.id, 'listening to data', signalName); + signalBus.log(vegaInstance.id, 'listening to data', signalName); //if current signalbus value has not been initialized and we have data, send it through - if (renderer.signalBus.signalDeps[signalName].value === undefined + if (signalBus.signalDeps[signalName].value === undefined && view.data(signalName)?.length > 0) { - renderer.signalBus.log(vegaInstance.id, 'un-initialized', signalName); + signalBus.log(vegaInstance.id, 'un-initialized', signalName); const batch: Batch = {}; batch[signalName] = { value: view.data(signalName), isData: true }; - renderer.signalBus.broadcast(vegaInstance.id, batch); + signalBus.broadcast(vegaInstance.id, batch); } view.addDataListener(signalName, async (name, value) => { @@ -166,16 +167,16 @@ export const vegaPlugin: Plugin = { (matchSignal as NewSignal).update // calculations ; if (isChangeSource) { - renderer.signalBus.log(vegaInstance.id, 'listening to signal', signalName); + signalBus.log(vegaInstance.id, 'listening to signal', signalName); view.addSignalListener(signalName, async (name, value) => { startBatch(`signal:${signalName}`); vegaInstance.batch[name] = { value, isData }; }); } else { - //renderer.signalBus.log(vegaInstance.id, 'not listening to signal, not a change source', signalName); + //signalBus.log(vegaInstance.id, 'not listening to signal, not a change source', signalName); } } else { - //renderer.signalBus.log(vegaInstance.id, 'not listening to signal, no match', signalName); + //signalBus.log(vegaInstance.id, 'not listening to signal, no match', signalName); } } }, @@ -196,15 +197,15 @@ export const vegaPlugin: Plugin = { }, }; -function receiveBatch(batch: Batch, renderer: Renderer, vegaInstance: VegaInstance) { +function receiveBatch(batch: Batch, signalBus: SignalBus, vegaInstance: VegaInstance) { const { spec, view } = vegaInstance; - const doLog = renderer.signalBus.logLevel === LogLevel.all; - doLog && renderer.signalBus.log(vegaInstance.id, 'receiveBatch', batch); + const doLog = signalBus.logLevel === LogLevel.all; + doLog && signalBus.log(vegaInstance.id, 'receiveBatch', batch); let hasAnyChange = false; for (const signalName in batch) { const batchItem = batch[signalName]; if (ignoredSignals.includes(signalName)) { - doLog && renderer.signalBus.log(vegaInstance.id, 'ignoring reserved signal name', signalName, batchItem.value); + doLog && signalBus.log(vegaInstance.id, 'ignoring reserved signal name', signalName, batchItem.value); continue; } if (batchItem.isData) { @@ -221,7 +222,7 @@ function receiveBatch(batch: Batch, renderer: Renderer, vegaInstance: VegaInstan hasAnyChange = true; } } - doLog && renderer.signalBus.log(vegaInstance.id, `(isData) ${logReason}`, signalName, batchItem.value); + doLog && signalBus.log(vegaInstance.id, `(isData) ${logReason}`, signalName, batchItem.value); } let logReason = ''; const matchSignal = spec.signals?.find(signal => signal.name === signalName); @@ -245,7 +246,7 @@ function receiveBatch(batch: Batch, renderer: Renderer, vegaInstance: VegaInstan } } } - doLog && renderer.signalBus.log(vegaInstance.id, logReason, signalName, batchItem.value); + doLog && signalBus.log(vegaInstance.id, logReason, signalName, batchItem.value); } return hasAnyChange; } diff --git a/packages/markdown/src/renderer.ts b/packages/markdown/src/renderer.ts index 78f107c9..ed9169ba 100644 --- a/packages/markdown/src/renderer.ts +++ b/packages/markdown/src/renderer.ts @@ -15,7 +15,6 @@ export interface ErrorHandler { export interface RendererOptions { vegaRenderer?: Renderers; - signalBus?: SignalBus; errorHandler?: ErrorHandler; useShadowDom?: boolean; openLinksInNewTab?: boolean; @@ -46,7 +45,7 @@ export class Renderer { constructor(_element: HTMLElement, options?: RendererOptions) { this.options = { ...defaultRendererOptions, ...options }; - this.signalBus = this.options.signalBus || new SignalBus(defaultCommonOptions.dataSignalPrefix!); + this.signalBus = new SignalBus(defaultCommonOptions.dataSignalPrefix!); this.instances = {}; // Create shadow DOM or use regular DOM @@ -176,7 +175,13 @@ export class Renderer { } reset() { - this.signalBus.reset(); + + //cancel the old signal bus, which may have active listeners + this.signalBus.deactivate(); + + //create a new signal bus + this.signalBus = new SignalBus(defaultCommonOptions.dataSignalPrefix!); + for (const pluginName of Object.keys(this.instances)) { const instances = this.instances[pluginName]; for (const instance of instances) { diff --git a/packages/markdown/src/signalbus.ts b/packages/markdown/src/signalbus.ts index 541200a8..6834b7e3 100644 --- a/packages/markdown/src/signalbus.ts +++ b/packages/markdown/src/signalbus.ts @@ -24,7 +24,12 @@ export class SignalBus { constructor(public dataSignalPrefix: string) { this.logLevel = LogLevel.none; this.logWatchIds = []; - this.reset(); + this.signalDeps = {}; + this.active = false; + this.peers = []; + this.broadcastingStack = []; + this.peerDependencies = {}; + } public log(id: string, message: string, ...optionalParams: unknown[]) { @@ -34,6 +39,11 @@ export class SignalBus { } async broadcast(originId: string, batch: Batch) { + if (!this.active) { + this.log(originId, 'Broadcast called but bus is not active'); + return; + } + //TODO handle multiple broadcasts //TODO handle circular dependencies @@ -179,12 +189,14 @@ export class SignalBus { this.active = true; } - reset() { - this.signalDeps = {}; + deactivate() { + if (this.signalDeps) { + for (const signalName in this.signalDeps) { + //remove all references to instances + this.signalDeps[signalName].deps = []; + } + } this.active = false; - this.peers = []; - this.broadcastingStack = []; - this.peerDependencies = {}; } } From 5e6dfaa7062b41f732eaec7ac7d4964676bc5131 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 18 Aug 2025 15:29:14 -0700 Subject: [PATCH 4/6] render handler --- packages/host/dev/index.ts | 45 +++++++++++++++++++++++++++++-- packages/web-frontend/src/view.ts | 2 +- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/host/dev/index.ts b/packages/host/dev/index.ts index 7b1def8a..0b1ee11d 100644 --- a/packages/host/dev/index.ts +++ b/packages/host/dev/index.ts @@ -7,6 +7,7 @@ import { Toolbar } from 'toolbar'; import { rendererCss } from '@microsoft/chartifact-sandbox/src/resources/rendererCss.ts'; import { rendererUmdJs } from '@microsoft/chartifact-sandbox/src/resources/rendererUmdJs.ts'; import { Listener } from '../src/index.ts'; +import { InteractiveDocumentWithSchema } from '@microsoft/chartifact-schema'; class LocalSandbox extends Sandbox { constructor(elementOrSelector: string | HTMLElement, markdown: string, options: SandboxOptions) { @@ -28,9 +29,13 @@ class LocalSandbox extends Sandbox { } -const toolbar = new Toolbar('.chartifact-toolbar', { textarea: document.querySelector('#source') as HTMLTextAreaElement, tweakButton: true }); +let render = () => { }; -new Listener({ +const textarea = document.querySelector('#source') as HTMLTextAreaElement; +textarea.addEventListener('input', () => render()); +const toolbar = new Toolbar('.chartifact-toolbar', { textarea }); + +const host = new Listener({ preview: '#preview', loading: '#loading', help: '#help', @@ -42,4 +47,40 @@ new Listener({ return specs; }, sandboxConstructor: LocalSandbox, + onSetMode: (mode, markdown, interactiveDocument) => { + switch (mode) { + case 'json': + textarea.value = JSON.stringify(interactiveDocument, null, 2); + render = () => { + const json = textarea.value; + try { + const interactiveDocument = JSON.parse(json) as InteractiveDocumentWithSchema; + if (typeof interactiveDocument !== 'object') { + host.errorHandler( + 'Invalid JSON format', + 'Please provide a valid Interactive Document JSON.' + ); + return; + } + host.renderInteractiveDocument(interactiveDocument); + } catch (error) { + host.errorHandler( + error, + 'Failed to parse Interactive Document JSON' + ); + } + }; + break; + case 'markdown': + textarea.value = markdown; + render = () => { + const markdown = textarea.value; + host.renderMarkdown(markdown); + }; + break; + default: + return; + } + toolbar.showTweakButton(); + }, }); diff --git a/packages/web-frontend/src/view.ts b/packages/web-frontend/src/view.ts index ae72e2b7..a0362145 100644 --- a/packages/web-frontend/src/view.ts +++ b/packages/web-frontend/src/view.ts @@ -9,7 +9,7 @@ window.addEventListener('DOMContentLoaded', () => { let render = () => { }; const textarea = document.querySelector('#source') as HTMLTextAreaElement; - textarea.addEventListener('input', render); + textarea.addEventListener('input', () => render()); const toolbar = new Chartifact.toolbar.Toolbar('.chartifact-toolbar', { textarea }); host = new Chartifact.host.Listener({ From add8b8fa160f46dd18dc348cb3c5368cd9909c3f Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 18 Aug 2025 15:29:28 -0700 Subject: [PATCH 5/6] rebuild --- docs/assets/js/view.js | 40 +++++- docs/dist/v1/chartifact.host.umd.js | 180 ++++++++++-------------- docs/dist/v1/chartifact.markdown.umd.js | 95 ++++++++----- docs/dist/v1/chartifact.sandbox.umd.js | 12 +- 4 files changed, 174 insertions(+), 153 deletions(-) diff --git a/docs/assets/js/view.js b/docs/assets/js/view.js index c3e9ba0e..6001709a 100644 --- a/docs/assets/js/view.js +++ b/docs/assets/js/view.js @@ -1,16 +1,50 @@ window.addEventListener('DOMContentLoaded', () => { + let render = () => { }; + const textarea = document.querySelector('#source'); + textarea.addEventListener('input', () => render()); + const toolbar = new Chartifact.toolbar.Toolbar('.chartifact-toolbar', { textarea }); host = new Chartifact.host.Listener({ - app: '#app', + preview: '#preview', loading: '#loading', help: '#help', uploadButton: '#upload-btn', fileInput: '#file-input', - textarea: '#textarea', - toolbar: '#toolbar', + toolbar, onApprove: (message) => { // TODO look through each spec and override policy to approve unapproved for https://microsoft.github.io/chartifact/ const { specs } = message; return specs; }, + onSetMode: (mode, markdown, interactiveDocument) => { + switch (mode) { + case 'json': + textarea.value = JSON.stringify(interactiveDocument, null, 2); + render = () => { + const json = textarea.value; + try { + const interactiveDocument = JSON.parse(json); + if (typeof interactiveDocument !== 'object') { + host.errorHandler('Invalid JSON format', 'Please provide a valid Interactive Document JSON.'); + return; + } + host.renderInteractiveDocument(interactiveDocument); + } + catch (error) { + host.errorHandler(error, 'Failed to parse Interactive Document JSON'); + } + }; + break; + case 'markdown': + textarea.value = markdown; + render = () => { + const markdown = textarea.value; + host.renderMarkdown(markdown); + }; + break; + default: + return; + } + toolbar.showTweakButton(); + }, }); }); diff --git a/docs/dist/v1/chartifact.host.umd.js b/docs/dist/v1/chartifact.host.umd.js index 61394ca9..40869953 100644 --- a/docs/dist/v1/chartifact.host.umd.js +++ b/docs/dist/v1/chartifact.host.umd.js @@ -1278,53 +1278,6 @@ ${guardedJs} target.postMessage(message, "*"); } } - class Toolbar { - constructor(toolbarElementOrSelector, options = {}) { - __publicField(this, "options"); - __publicField(this, "toolbarElement"); - this.options = options; - this.toolbarElement = typeof toolbarElementOrSelector === "string" ? document.querySelector(toolbarElementOrSelector) : toolbarElementOrSelector; - if (!this.toolbarElement) { - throw new Error("Toolbar element not found"); - } - const html = `Chartifact viewer - - - `; - this.toolbarElement.innerHTML = html; - if (this.options.tweakButton) { - const tweakButton = this.toolbarElement.querySelector("#tweak"); - tweakButton == null ? void 0 : tweakButton.addEventListener("click", () => { - this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none"; - }); - } - } - manageTextareaVisibilityForAgents() { - const { textarea } = this.options; - if (!textarea) { - throw new Error("Textarea element not found"); - } - textarea.style.flex = "0"; - textarea.style.padding = "0"; - textarea.style.border = "0"; - setTimeout(() => { - textarea.style.flex = ""; - textarea.style.padding = ""; - textarea.style.border = ""; - textarea.style.display = "none"; - }, 300); - } - } - function create(toolbarElementOrSelector, options = {}) { - const toolbar = new Toolbar(toolbarElementOrSelector, options); - toolbar.manageTextareaVisibilityForAgents(); - return toolbar; - } - const index$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - Toolbar, - create - }, Symbol.toStringTag, { value: "Module" })); function getElement(elementOrSelector) { if (typeof elementOrSelector === "string") { return document.querySelector(elementOrSelector); @@ -1349,30 +1302,33 @@ ${guardedJs} class Listener { constructor(options) { __publicField(this, "options"); - __publicField(this, "appDiv"); + __publicField(this, "previewDiv"); __publicField(this, "loadingDiv"); __publicField(this, "helpDiv"); __publicField(this, "uploadButton"); __publicField(this, "fileInput"); - __publicField(this, "textarea"); __publicField(this, "toolbar"); __publicField(this, "sandbox"); __publicField(this, "sandboxReady", false); __publicField(this, "onApprove"); + __publicField(this, "onSetMode"); __publicField(this, "removeInteractionHandlers"); __publicField(this, "sandboxConstructor"); this.sandboxConstructor = options.sandboxConstructor || Sandbox; this.options = { ...defaultOptions, ...options == null ? void 0 : options.options }; this.onApprove = options.onApprove; + this.onSetMode = options.onSetMode || (() => { + }); this.removeInteractionHandlers = []; - this.appDiv = getElement(options.app); + this.previewDiv = getElement(options.preview); this.loadingDiv = getElement(options.loading); this.helpDiv = getElement(options.help); this.uploadButton = getElement(options.uploadButton); this.fileInput = getElement(options.fileInput); - this.textarea = getElement(options.textarea); - this.toolbar = new Toolbar(options.toolbar); - if (!this.appDiv) { + if (options.toolbar) { + this.toolbar = options.toolbar; + } + if (!this.previewDiv) { throw new Error("App container not found"); } show(this.loadingDiv, true); @@ -1400,7 +1356,7 @@ ${guardedJs} this.sandbox.destroy(); } this.sandboxReady = false; - this.sandbox = new this.sandboxConstructor(this.appDiv, markdown, { + this.sandbox = new this.sandboxConstructor(this.previewDiv, markdown, { onReady: () => { this.sandboxReady = true; postStatus(this.options.postMessageTarget, { type: "hostStatus", hostStatus: "ready" }); @@ -1420,7 +1376,7 @@ ${guardedJs} errorHandler(error, details) { show(this.loadingDiv, false); show(this.helpDiv, false); - show(this.appDiv, true); + show(this.previewDiv, true); let message; if (typeof error === "string") { message = error; @@ -1440,66 +1396,25 @@ ${message} ${details}`; this.render(markdown, void 0); } else { - this.appDiv.innerHTML = ""; + this.previewDiv.innerHTML = ""; const h1 = document.createElement("h1"); h1.textContent = "Error"; const pMessage = document.createElement("p"); pMessage.textContent = message; const pDetails = document.createElement("p"); pDetails.textContent = details; - this.appDiv.appendChild(h1); - this.appDiv.appendChild(pMessage); - this.appDiv.appendChild(pDetails); + this.previewDiv.appendChild(h1); + this.previewDiv.appendChild(pMessage); + this.previewDiv.appendChild(pDetails); } } - bindTextareaToCompiler() { - const render = () => { - const json = this.textarea.value; - try { - const interactiveDocument = JSON.parse(json); - if (typeof interactiveDocument !== "object") { - this.errorHandler( - "Invalid JSON format", - "Please provide a valid Interactive Document JSON." - ); - return; - } - this.renderInteractiveDocument(interactiveDocument); - } catch (error) { - this.errorHandler( - error, - "Failed to parse Interactive Document JSON" - ); - } - }; - this.textarea.addEventListener("input", render); - render(); - } - bindTextareaToMarkdown() { - const render = () => { - const markdown = this.textarea.value; - this.renderMarkdown(markdown); - }; - this.textarea.addEventListener("input", render); - render(); - } render(markdown, interactiveDocument) { if (interactiveDocument) { - if (this.textarea) { - this.textarea.value = JSON.stringify(interactiveDocument, null, 2); - this.hideLoadingAndHelp(); - this.bindTextareaToCompiler(); - } else { - this.renderInteractiveDocument(interactiveDocument); - } + this.onSetMode("json", null, interactiveDocument); + this.renderInteractiveDocument(interactiveDocument); } else if (markdown) { - if (this.textarea) { - this.textarea.value = markdown; - this.hideLoadingAndHelp(); - this.bindTextareaToMarkdown(); - } else { - this.renderMarkdown(markdown); - } + this.onSetMode("markdown", markdown, null); + this.renderMarkdown(markdown); } else { this.errorHandler( "No content provided", @@ -1538,14 +1453,65 @@ ${details}`; } } } - const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + const index$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, Listener }, Symbol.toStringTag, { value: "Module" })); + class Toolbar { + constructor(toolbarElementOrSelector, options = {}) { + __publicField(this, "options"); + __publicField(this, "toolbarElement"); + this.options = options; + this.toolbarElement = typeof toolbarElementOrSelector === "string" ? document.querySelector(toolbarElementOrSelector) : toolbarElementOrSelector; + if (!this.toolbarElement) { + throw new Error("Toolbar element not found"); + } + const html = `Chartifact viewer + + + `; + this.toolbarElement.innerHTML = html; + if (this.options.tweakButton) { + this.showTweakButton(); + } + } + showTweakButton() { + const tweakButton = this.toolbarElement.querySelector("#tweak"); + tweakButton.style.display = ""; + tweakButton == null ? void 0 : tweakButton.addEventListener("click", () => { + this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none"; + }); + } + manageTextareaVisibilityForAgents() { + const { textarea } = this.options; + if (!textarea) { + throw new Error("Textarea element not found"); + } + textarea.style.flex = "0"; + textarea.style.padding = "0"; + textarea.style.border = "0"; + setTimeout(() => { + textarea.style.flex = ""; + textarea.style.padding = ""; + textarea.style.border = ""; + textarea.style.display = "none"; + }, 300); + } + } + function create(toolbarElementOrSelector, options = {}) { + const toolbar = new Toolbar(toolbarElementOrSelector, options); + toolbar.manageTextareaVisibilityForAgents(); + return toolbar; + } + const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + Toolbar, + create + }, Symbol.toStringTag, { value: "Module" })); exports2.common = index$4; exports2.compiler = index$3; - exports2.host = index; + exports2.host = index$1; exports2.sandbox = index$2; - exports2.toolbar = index$1; + exports2.toolbar = index; Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); })); diff --git a/docs/dist/v1/chartifact.markdown.umd.js b/docs/dist/v1/chartifact.markdown.umd.js index 3313935b..13d144d6 100644 --- a/docs/dist/v1/chartifact.markdown.umd.js +++ b/docs/dist/v1/chartifact.markdown.umd.js @@ -594,6 +594,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const checkboxPlugin = { ...flaggableJsonPlugin(pluginName$c, className$a), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const checkboxInstances = []; for (let index2 = 0; index2 < specs.length; index2++) { const specReview = specs[index2]; @@ -641,7 +642,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy isData: false } }; - renderer.signalBus.broadcast(checkboxInstance.id, batch); + signalBus.broadcast(checkboxInstance.id, batch); }); }, getCurrentSignalValue: () => { @@ -1097,6 +1098,7 @@ ${reconstitutedRules.join("\n\n")} const dropdownPlugin = { ...flaggableJsonPlugin(pluginName$8, className$7), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const dropdownInstances = []; for (let index2 = 0; index2 < specs.length; index2++) { const specReview = specs[index2]; @@ -1190,7 +1192,7 @@ ${reconstitutedRules.join("\n\n")} isData: false } }; - renderer.signalBus.broadcast(dropdownInstance.id, batch); + signalBus.broadcast(dropdownInstance.id, batch); }); }, getCurrentSignalValue: () => { @@ -1646,6 +1648,7 @@ ${reconstitutedRules.join("\n\n")} const presetsPlugin = { ...flaggableJsonPlugin(pluginName$5, className$5), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const presetsInstances = []; for (let index2 = 0; index2 < specs.length; index2++) { const specReview = specs[index2]; @@ -1678,7 +1681,7 @@ ${reconstitutedRules.join("\n\n")} for (const [signalName, value] of Object.entries(preset.state)) { batch[signalName] = { value, isData: false }; } - renderer.signalBus.broadcast(id, batch); + signalBus.broadcast(id, batch); }; li.appendChild(button); li.appendChild(document.createTextNode(" ")); @@ -1707,8 +1710,8 @@ ${reconstitutedRules.join("\n\n")} initialSignals, broadcastComplete: async () => { const state = {}; - for (const signalName of Object.keys(renderer.signalBus.signalDeps)) { - state[signalName] = renderer.signalBus.signalDeps[signalName].value; + for (const signalName of Object.keys(signalBus.signalDeps)) { + state[signalName] = signalBus.signalDeps[signalName].value; } setAllPresetsActiveState(presetsInstance, state); } @@ -1740,6 +1743,7 @@ ${reconstitutedRules.join("\n\n")} const sliderPlugin = { ...flaggableJsonPlugin(pluginName$4, className$4), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const sliderInstances = []; for (let index2 = 0; index2 < specs.length; index2++) { const specReview = specs[index2]; @@ -1797,7 +1801,7 @@ ${reconstitutedRules.join("\n\n")} isData: false } }; - renderer.signalBus.broadcast(sliderInstance.id, batch); + signalBus.broadcast(sliderInstance.id, batch); }; element.addEventListener("input", updateValue); element.addEventListener("change", updateValue); @@ -1825,6 +1829,7 @@ ${reconstitutedRules.join("\n\n")} const tabulatorPlugin = { ...flaggableJsonPlugin(pluginName$3, className$3, inspectTabulatorSpec, { style: "box-sizing: border-box;" }), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const tabulatorInstances = []; const deleteFieldname = newId(); for (let index2 = 0; index2 < specs.length; index2++) { @@ -1917,8 +1922,8 @@ ${reconstitutedRules.join("\n\n")} isData: true } }; - renderer.signalBus.log(tabulatorInstance.id, "sending batch", batch); - renderer.signalBus.broadcast(tabulatorInstance.id, batch); + signalBus.log(tabulatorInstance.id, "sending batch", batch); + signalBus.broadcast(tabulatorInstance.id, batch); }; const setData = (data) => { table.setData(data).then(() => { @@ -1971,7 +1976,7 @@ ${reconstitutedRules.join("\n\n")} } if (resetBtn) { resetBtn.onclick = () => { - const value = renderer.signalBus.signalDeps[spec.dataSourceName].value; + const value = signalBus.signalDeps[spec.dataSourceName].value; if (Array.isArray(value)) { setData(value); } @@ -2034,6 +2039,7 @@ ${reconstitutedRules.join("\n\n")} const textboxPlugin = { ...flaggableJsonPlugin(pluginName$2, className$2), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; const textboxInstances = []; for (let index2 = 0; index2 < specs.length; index2++) { const specReview = specs[index2]; @@ -2083,7 +2089,7 @@ ${reconstitutedRules.join("\n\n")} isData: false } }; - renderer.signalBus.broadcast(textboxInstance.id, batch); + signalBus.broadcast(textboxInstance.id, batch); }; element.addEventListener("input", updateValue); element.addEventListener("change", updateValue); @@ -2118,7 +2124,11 @@ ${reconstitutedRules.join("\n\n")} this.dataSignalPrefix = dataSignalPrefix; this.logLevel = 0; this.logWatchIds = []; - this.reset(); + this.signalDeps = {}; + this.active = false; + this.peers = []; + this.broadcastingStack = []; + this.peerDependencies = {}; } log(id, message, ...optionalParams) { if (this.logLevel === 0) return; @@ -2126,6 +2136,10 @@ ${reconstitutedRules.join("\n\n")} console.log(`[Signal Bus][${id}] ${message}`, ...optionalParams); } async broadcast(originId, batch) { + if (!this.active) { + this.log(originId, "Broadcast called but bus is not active"); + return; + } if (this.broadcastingStack.includes(originId)) { this.log(originId, "Additional broadcast from", originId, this.broadcastingStack.join(" -> ")); } @@ -2234,12 +2248,13 @@ ${reconstitutedRules.join("\n\n")} } this.active = true; } - reset() { - this.signalDeps = {}; + deactivate() { + if (this.signalDeps) { + for (const signalName in this.signalDeps) { + this.signalDeps[signalName].deps = []; + } + } this.active = false; - this.peers = []; - this.broadcastingStack = []; - this.peerDependencies = {}; } } const ignoredSignals = ["width", "height", "padding", "autosize", "background", "style", "parent", "datum", "item", "event", "cursor"]; @@ -2254,6 +2269,7 @@ ${reconstitutedRules.join("\n\n")} const vegaPlugin = { ...flaggableJsonPlugin(pluginName$1, className$1, inspectVegaSpec), hydrateComponent: async (renderer, errorHandler, specs) => { + const { signalBus } = renderer; if (!expressionsInitialized) { vega.expressionFunction("encodeURIComponent", encodeURIComponent); expressionsInitialized = true; @@ -2297,13 +2313,13 @@ ${reconstitutedRules.join("\n\n")} const { spec, view, initialSignals } = vegaInstance; const startBatch = (from) => { if (!vegaInstance.batch) { - renderer.signalBus.log(vegaInstance.id, "starting batch", from); + signalBus.log(vegaInstance.id, "starting batch", from); vegaInstance.batch = {}; view.runAfter(() => { const { batch } = vegaInstance; vegaInstance.batch = void 0; - renderer.signalBus.log(vegaInstance.id, "sending batch", batch); - renderer.signalBus.broadcast(vegaInstance.id, batch); + signalBus.log(vegaInstance.id, "sending batch", batch); + signalBus.broadcast(vegaInstance.id, batch); }); } }; @@ -2311,27 +2327,27 @@ ${reconstitutedRules.join("\n\n")} ...vegaInstance, initialSignals, receiveBatch: async (batch, from) => { - renderer.signalBus.log(vegaInstance.id, "received batch", batch, from); + signalBus.log(vegaInstance.id, "received batch", batch, from); return new Promise((resolve) => { view.runAfter(async () => { - if (receiveBatch(batch, renderer, vegaInstance)) { - renderer.signalBus.log(vegaInstance.id, "running after _pulse, changes from", from); + if (receiveBatch(batch, signalBus, vegaInstance)) { + signalBus.log(vegaInstance.id, "running after _pulse, changes from", from); vegaInstance.needToRun = true; } else { - renderer.signalBus.log(vegaInstance.id, "no changes"); + signalBus.log(vegaInstance.id, "no changes"); } - renderer.signalBus.log(vegaInstance.id, "running view after _pulse finished"); + signalBus.log(vegaInstance.id, "running view after _pulse finished"); resolve(); }); }); }, broadcastComplete: async () => { - renderer.signalBus.log(vegaInstance.id, "broadcastComplete"); + signalBus.log(vegaInstance.id, "broadcastComplete"); if (vegaInstance.needToRun) { view.runAfter(() => { view.runAsync(); vegaInstance.needToRun = false; - renderer.signalBus.log(vegaInstance.id, "running view after broadcastComplete"); + signalBus.log(vegaInstance.id, "running view after broadcastComplete"); }); } }, @@ -2342,12 +2358,12 @@ ${reconstitutedRules.join("\n\n")} if (isData) { const matchData = (_a = spec.data) == null ? void 0 : _a.find((data) => data.name === signalName); if (matchData && vegaInstance.dataSignals.includes(matchData.name)) { - renderer.signalBus.log(vegaInstance.id, "listening to data", signalName); - if (renderer.signalBus.signalDeps[signalName].value === void 0 && ((_b = view.data(signalName)) == null ? void 0 : _b.length) > 0) { - renderer.signalBus.log(vegaInstance.id, "un-initialized", signalName); + signalBus.log(vegaInstance.id, "listening to data", signalName); + if (signalBus.signalDeps[signalName].value === void 0 && ((_b = view.data(signalName)) == null ? void 0 : _b.length) > 0) { + signalBus.log(vegaInstance.id, "un-initialized", signalName); const batch = {}; batch[signalName] = { value: view.data(signalName), isData: true }; - renderer.signalBus.broadcast(vegaInstance.id, batch); + signalBus.broadcast(vegaInstance.id, batch); } view.addDataListener(signalName, async (name, value) => { startBatch(`data:${signalName}`); @@ -2361,7 +2377,7 @@ ${reconstitutedRules.join("\n\n")} matchSignal.bind || // ui elements matchSignal.update; if (isChangeSource) { - renderer.signalBus.log(vegaInstance.id, "listening to signal", signalName); + signalBus.log(vegaInstance.id, "listening to signal", signalName); view.addSignalListener(signalName, async (name, value) => { startBatch(`signal:${signalName}`); vegaInstance.batch[name] = { value, isData }; @@ -2387,16 +2403,16 @@ ${reconstitutedRules.join("\n\n")} return instances; } }; - function receiveBatch(batch, renderer, vegaInstance) { + function receiveBatch(batch, signalBus, vegaInstance) { var _a, _b; const { spec, view } = vegaInstance; - const doLog = renderer.signalBus.logLevel === LogLevel.all; - doLog && renderer.signalBus.log(vegaInstance.id, "receiveBatch", batch); + const doLog = signalBus.logLevel === LogLevel.all; + doLog && signalBus.log(vegaInstance.id, "receiveBatch", batch); let hasAnyChange = false; for (const signalName in batch) { const batchItem = batch[signalName]; if (ignoredSignals.includes(signalName)) { - doLog && renderer.signalBus.log(vegaInstance.id, "ignoring reserved signal name", signalName, batchItem.value); + doLog && signalBus.log(vegaInstance.id, "ignoring reserved signal name", signalName, batchItem.value); continue; } if (batchItem.isData) { @@ -2413,7 +2429,7 @@ ${reconstitutedRules.join("\n\n")} hasAnyChange = true; } } - doLog && renderer.signalBus.log(vegaInstance.id, `(isData) ${logReason2}`, signalName, batchItem.value); + doLog && signalBus.log(vegaInstance.id, `(isData) ${logReason2}`, signalName, batchItem.value); } let logReason = ""; const matchSignal = (_b = spec.signals) == null ? void 0 : _b.find((signal) => signal.name === signalName); @@ -2437,7 +2453,7 @@ ${reconstitutedRules.join("\n\n")} } } } - doLog && renderer.signalBus.log(vegaInstance.id, logReason, signalName, batchItem.value); + doLog && signalBus.log(vegaInstance.id, logReason, signalName, batchItem.value); } return hasAnyChange; } @@ -2628,7 +2644,7 @@ ${reconstitutedRules.join("\n\n")} __publicField(this, "shadowRoot"); __publicField(this, "element"); this.options = { ...defaultRendererOptions, ...options }; - this.signalBus = this.options.signalBus || new SignalBus(defaultCommonOptions.dataSignalPrefix); + this.signalBus = new SignalBus(defaultCommonOptions.dataSignalPrefix); this.instances = {}; if (this.options.useShadowDom) { this.shadowRoot = _element.attachShadow({ mode: "open" }); @@ -2723,7 +2739,8 @@ ${reconstitutedRules.join("\n\n")} } } reset() { - this.signalBus.reset(); + this.signalBus.deactivate(); + this.signalBus = new SignalBus(defaultCommonOptions.dataSignalPrefix); for (const pluginName2 of Object.keys(this.instances)) { const instances = this.instances[pluginName2]; for (const instance of instances) { diff --git a/docs/dist/v1/chartifact.sandbox.umd.js b/docs/dist/v1/chartifact.sandbox.umd.js index d7a7e0ca..f4a5a234 100644 --- a/docs/dist/v1/chartifact.sandbox.umd.js +++ b/docs/dist/v1/chartifact.sandbox.umd.js @@ -333,12 +333,16 @@ document.addEventListener('DOMContentLoaded', () => { `; this.toolbarElement.innerHTML = html; if (this.options.tweakButton) { - const tweakButton = this.toolbarElement.querySelector("#tweak"); - tweakButton == null ? void 0 : tweakButton.addEventListener("click", () => { - this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none"; - }); + this.showTweakButton(); } } + showTweakButton() { + const tweakButton = this.toolbarElement.querySelector("#tweak"); + tweakButton.style.display = ""; + tweakButton == null ? void 0 : tweakButton.addEventListener("click", () => { + this.options.textarea.style.display = this.options.textarea.style.display === "none" ? "" : "none"; + }); + } manageTextareaVisibilityForAgents() { const { textarea } = this.options; if (!textarea) { From 0fd6f3c16fc4e7cc25a980443b113902775ff661 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 18 Aug 2025 15:59:43 -0700 Subject: [PATCH 6/6] move active flag upward --- packages/markdown/src/signalbus.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/markdown/src/signalbus.ts b/packages/markdown/src/signalbus.ts index 6834b7e3..31ea94cb 100644 --- a/packages/markdown/src/signalbus.ts +++ b/packages/markdown/src/signalbus.ts @@ -29,7 +29,6 @@ export class SignalBus { this.peers = []; this.broadcastingStack = []; this.peerDependencies = {}; - } public log(id: string, message: string, ...optionalParams: unknown[]) { @@ -127,6 +126,8 @@ export class SignalBus { } async beginListening() { + this.active = true; + //set the initial batch on each peer this.log('beginListening', 'begin initial batch', this.signalDeps); @@ -185,8 +186,6 @@ export class SignalBus { this.log(peer.id, 'No shared signals'); } } - - this.active = true; } deactivate() {