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:
@@ -259,5 +259,9 @@ export default withMermaid({
|
||||
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">
|
||||
<!-- Editor Container -->
|
||||
<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">
|
||||
<button v-if="!hideRunButton" @click="executeCode" class="primary-button ">Run Code</button>
|
||||
</div>
|
||||
@@ -51,6 +51,8 @@ import iframeTemplateHtml from './CodeSandboxTemplate.html?raw';
|
||||
import CodeEditor from './CodeEditor.vue';
|
||||
import { ImportedLib, useXOCashLibs } from '../utils/useXOCashLibs';
|
||||
|
||||
import { StorageLocalStorage, BaseStore } from '@xocash/stack';
|
||||
|
||||
const { isDark } = useData();
|
||||
const xocashLibs = useXOCashLibs();
|
||||
|
||||
@@ -62,6 +64,7 @@ export type CodeSandboxProps = {
|
||||
outputHeight?: string;
|
||||
hideRunButton?: boolean;
|
||||
libs?: Record<string, ImportedLib>;
|
||||
saveable?: boolean;
|
||||
}
|
||||
|
||||
// Define props for the component
|
||||
@@ -73,6 +76,7 @@ const props = withDefaults(defineProps<CodeSandboxProps>(), {
|
||||
outputHeight: 'auto',
|
||||
hideRunButton: false,
|
||||
libs: (): Record<string, ImportedLib> => ({}),
|
||||
saveable: false,
|
||||
});
|
||||
|
||||
// Define types for console logs
|
||||
@@ -97,6 +101,10 @@ const showLogs = ref(false);
|
||||
const sandboxIframe = ref<HTMLIFrameElement | 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
|
||||
const sandboxStyle = computed(() => ({
|
||||
height: props.fullHeight ? '100%' : props.height,
|
||||
@@ -313,25 +321,26 @@ const handleMessage = (event: MessageEvent) => {
|
||||
};
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
// Check if there's code in the URL hash fragment
|
||||
if (window.location.hash && window.location.hash.startsWith('#code=')) {
|
||||
const codeParam = window.location.hash.substring(6); // Remove '#code='
|
||||
onMounted(async () => {
|
||||
sandboxStorage.value = await StorageLocalStorage.createOrOpen('xoCodeSandbox_V0.0.1');
|
||||
sandboxStore.value = await sandboxStorage.value.createOrGetStore('codeSandbox');
|
||||
|
||||
// If code exists in URL hash, use it
|
||||
try {
|
||||
// Always assume Base64 encoding
|
||||
code.value = decodeURIComponent(escape(atob(decodeURIComponent(codeParam))));
|
||||
} catch (error) {
|
||||
console.error('Failed to decode URL hash parameter:', error);
|
||||
logs.value.push({
|
||||
type: 'error',
|
||||
content: 'Failed to decode URL hash parameter: ' + error
|
||||
});
|
||||
// Fallback to slot content if URL decoding fails
|
||||
loadCodeFromSlot();
|
||||
const fromCodeParam = (): string | undefined => {
|
||||
if (window.location.hash && window.location.hash.startsWith('#code=')) {
|
||||
const codeParam = window.location.hash.substring(6); // Remove '#code='
|
||||
|
||||
try {
|
||||
// Always assume Base64 encoding
|
||||
return decodeURIComponent(escape(atob(decodeURIComponent(codeParam))));
|
||||
} catch (error) {
|
||||
console.error('Failed to decode URL hash parameter:', error);
|
||||
logs.value.push({
|
||||
type: 'error',
|
||||
content: 'Failed to decode URL hash parameter: ' + error
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Otherwise check query parameters (for backward compatibility)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const queryCodeParam = urlParams.get('code');
|
||||
@@ -339,19 +348,68 @@ onMounted(() => {
|
||||
if (queryCodeParam) {
|
||||
try {
|
||||
// Try to decode from Base64
|
||||
code.value = decodeURIComponent(escape(atob(decodeURIComponent(queryCodeParam))));
|
||||
return decodeURIComponent(escape(atob(decodeURIComponent(queryCodeParam))));
|
||||
} catch (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
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const versionParam = urlParams.get('version');
|
||||
@@ -364,19 +422,55 @@ onMounted(() => {
|
||||
window.addEventListener('message', handleMessage);
|
||||
});
|
||||
|
||||
// Function to load code from slot
|
||||
const loadCodeFromSlot = () => {
|
||||
if (codeRef.value) {
|
||||
const template = codeRef.value;
|
||||
if (template) {
|
||||
code.value = template.innerText.trim();
|
||||
}
|
||||
/**
|
||||
* Saves the sandbox code to both local and session storage
|
||||
* This function is debounced to avoid excessive saves during typing
|
||||
*/
|
||||
const saveSandbox = async () => {
|
||||
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(() => {
|
||||
// Clean up event listener
|
||||
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.
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
hideRunButton
|
||||
:libs="xocashLibs"
|
||||
:isDark="isDark"
|
||||
:saveable="true"
|
||||
>
|
||||
<slot></slot>
|
||||
</CodeSandbox>
|
||||
|
||||
Reference in New Issue
Block a user