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',
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user