Merge branch '81-feat-playground-intermittent-save' into 'development'

Resolve "feat(Playground): Intermittent Save"

Closes #81

See merge request GeneralProtocols/xo/stack!122
This commit is contained in:
Jim Hamill
2025-08-12 05:29:34 +00:00
3 changed files with 131 additions and 32 deletions
+4
View File
@@ -259,5 +259,9 @@ export default withMermaid({
target: 'esnext', target: 'esnext',
}, },
}, },
resolve: {
// Configure to use browser version of @xocash/stack
conditions: ['browser', 'import', 'module', 'default'],
},
}, },
}); });
@@ -2,7 +2,7 @@
<div class="code-sandbox" :style="sandboxStyle"> <div class="code-sandbox" :style="sandboxStyle">
<!-- Editor Container --> <!-- Editor Container -->
<div class="editor-container" :style="editorContainerStyle"> <div class="editor-container" :style="editorContainerStyle">
<code-editor ref="editorRef" v-model="code" @execute="executeCode" :style="editorStyle" :libs="libs" :is-dark="isDark"/> <code-editor ref="editorRef" v-model="code" @update:model-value="saveSandbox" @execute="executeCode" :style="editorStyle" :libs="libs" :is-dark="isDark"/>
<div class="editor-buttons"> <div class="editor-buttons">
<button v-if="!hideRunButton" @click="executeCode" class="primary-button ">Run Code</button> <button v-if="!hideRunButton" @click="executeCode" class="primary-button ">Run Code</button>
</div> </div>
@@ -51,6 +51,8 @@ import iframeTemplateHtml from './CodeSandboxTemplate.html?raw';
import CodeEditor from './CodeEditor.vue'; import CodeEditor from './CodeEditor.vue';
import { ImportedLib, useXOCashLibs } from '../utils/useXOCashLibs'; import { ImportedLib, useXOCashLibs } from '../utils/useXOCashLibs';
import { StorageLocalStorage, BaseStore } from '@xocash/stack';
const { isDark } = useData(); const { isDark } = useData();
const xocashLibs = useXOCashLibs(); const xocashLibs = useXOCashLibs();
@@ -62,6 +64,7 @@ export type CodeSandboxProps = {
outputHeight?: string; outputHeight?: string;
hideRunButton?: boolean; hideRunButton?: boolean;
libs?: Record<string, ImportedLib>; libs?: Record<string, ImportedLib>;
saveable?: boolean;
} }
// Define props for the component // Define props for the component
@@ -73,6 +76,7 @@ const props = withDefaults(defineProps<CodeSandboxProps>(), {
outputHeight: 'auto', outputHeight: 'auto',
hideRunButton: false, hideRunButton: false,
libs: (): Record<string, ImportedLib> => ({}), libs: (): Record<string, ImportedLib> => ({}),
saveable: false,
}); });
// Define types for console logs // Define types for console logs
@@ -97,6 +101,10 @@ const showLogs = ref(false);
const sandboxIframe = ref<HTMLIFrameElement | null>(null); const sandboxIframe = ref<HTMLIFrameElement | null>(null);
const editorRef = ref<InstanceType<typeof CodeEditor> | null>(null); const editorRef = ref<InstanceType<typeof CodeEditor> | null>(null);
const sandboxStorage = ref<StorageLocalStorage | null>(null);
const sandboxStore = ref<BaseStore | null>(null);
let saveTimeout: NodeJS.Timeout | null = null;
// Computed styles based on props // Computed styles based on props
const sandboxStyle = computed(() => ({ const sandboxStyle = computed(() => ({
height: props.fullHeight ? '100%' : props.height, height: props.fullHeight ? '100%' : props.height,
@@ -313,25 +321,26 @@ const handleMessage = (event: MessageEvent) => {
}; };
// Lifecycle hooks // Lifecycle hooks
onMounted(() => { onMounted(async () => {
// Check if there's code in the URL hash fragment sandboxStorage.value = await StorageLocalStorage.createOrOpen('xoCodeSandbox_V0.0.1');
sandboxStore.value = await sandboxStorage.value.createOrGetStore('codeSandbox');
const fromCodeParam = (): string | undefined => {
if (window.location.hash && window.location.hash.startsWith('#code=')) { if (window.location.hash && window.location.hash.startsWith('#code=')) {
const codeParam = window.location.hash.substring(6); // Remove '#code=' const codeParam = window.location.hash.substring(6); // Remove '#code='
// If code exists in URL hash, use it
try { try {
// Always assume Base64 encoding // Always assume Base64 encoding
code.value = decodeURIComponent(escape(atob(decodeURIComponent(codeParam)))); return decodeURIComponent(escape(atob(decodeURIComponent(codeParam))));
} catch (error) { } catch (error) {
console.error('Failed to decode URL hash parameter:', error); console.error('Failed to decode URL hash parameter:', error);
logs.value.push({ logs.value.push({
type: 'error', type: 'error',
content: 'Failed to decode URL hash parameter: ' + error content: 'Failed to decode URL hash parameter: ' + error
}); });
// Fallback to slot content if URL decoding fails
loadCodeFromSlot();
} }
} else { }
// Otherwise check query parameters (for backward compatibility) // Otherwise check query parameters (for backward compatibility)
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const queryCodeParam = urlParams.get('code'); const queryCodeParam = urlParams.get('code');
@@ -339,19 +348,68 @@ onMounted(() => {
if (queryCodeParam) { if (queryCodeParam) {
try { try {
// Try to decode from Base64 // Try to decode from Base64
code.value = decodeURIComponent(escape(atob(decodeURIComponent(queryCodeParam)))); return decodeURIComponent(escape(atob(decodeURIComponent(queryCodeParam))));
} catch (error) { } catch (error) {
console.error('Failed to decode URL query parameter:', error); console.error('Failed to decode URL query parameter:', error);
// Fallback to slot content
loadCodeFromSlot();
} }
} else {
// No code in hash or query, load from slot
loadCodeFromSlot();
} }
return undefined;
} }
const fromCodeSlot = (): string | undefined => {
// Try to read the code from the code slot
if (codeRef.value) {
return codeRef.value.innerText.trim();
}
return undefined;
}
const fromSessionStorage = (): string | undefined => {
if (!props.saveable) {
return undefined;
}
// Try to read the code from session storage
const sessionStorageKey = 'xoCodeSandbox_V0.0.1';
const sessionStorage = window.sessionStorage.getItem(sessionStorageKey);
if (!sessionStorage) {
return undefined;
}
// Parse the session storage data
const sessionStorageData = JSON.parse(sessionStorage);
if (sessionStorageData) {
return sessionStorageData.code;
}
return undefined;
}
const fromLocalStorage = async (): Promise<string | undefined> => {
if (!props.saveable || !sandboxStore.value) {
return undefined;
}
// Try to read the code from local storage and update the sandbox code
const sandboxTemp = await sandboxStore.value.get<{ code: string }>('sandbox_temp');
if (sandboxTemp) {
return sandboxTemp.code;
}
return undefined;
}
// Try to load the code from the various sources
// 1. URL hash
// 2. Saveable ? Session storage
// 3. Saveable ? Local storage
// 4. Code slot
const importedCode = await Promise.all([fromCodeParam(), fromSessionStorage(), fromLocalStorage(), fromCodeSlot()]);
const codeToLoad = importedCode.find(code => code !== undefined);
code.value = codeToLoad || '';
// Get the selected version from the url if it exists // Get the selected version from the url if it exists
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const versionParam = urlParams.get('version'); const versionParam = urlParams.get('version');
@@ -364,19 +422,55 @@ onMounted(() => {
window.addEventListener('message', handleMessage); window.addEventListener('message', handleMessage);
}); });
// Function to load code from slot /**
const loadCodeFromSlot = () => { * Saves the sandbox code to both local and session storage
if (codeRef.value) { * This function is debounced to avoid excessive saves during typing
const template = codeRef.value; */
if (template) { const saveSandbox = async () => {
code.value = template.innerText.trim(); if (!props.saveable) {
return;
} }
// Clear any existing timeout to debounce the save operation
if (saveTimeout) {
clearTimeout(saveTimeout);
}
// Define our different methods of saving the sandbox
const toLocalStorage = async () => {
// Save the code to the local storage
if (!sandboxStore.value) {
return;
}
await sandboxStore.value.set('sandbox_temp', {
code: code.value
});
}
const toSessionStorage = async () => {
const sessionStorageKey = 'xoCodeSandbox_V0.0.1';
window.sessionStorage.setItem(sessionStorageKey, JSON.stringify({
code: code.value,
}));
}
// Set a new timeout to save after 500ms of inactivity
saveTimeout = setTimeout(async () => {
await Promise.all([toLocalStorage(), toSessionStorage()]);
saveTimeout = null;
}, 500);
} }
};
onBeforeUnmount(() => { onBeforeUnmount(() => {
// Clean up event listener // Clean up event listener
window.removeEventListener('message', handleMessage); window.removeEventListener('message', handleMessage);
// Clean up any pending save timeout
if (saveTimeout) {
clearTimeout(saveTimeout);
saveTimeout = null;
}
}); });
// Expose our functions so that they can be called from parent components. // Expose our functions so that they can be called from parent components.
@@ -23,6 +23,7 @@
hideRunButton hideRunButton
:libs="xocashLibs" :libs="xocashLibs"
:isDark="isDark" :isDark="isDark"
:saveable="true"
> >
<slot></slot> <slot></slot>
</CodeSandbox> </CodeSandbox>