feat: add import and export functionality for editor state with JSON support
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 23s

This commit is contained in:
2025-10-22 18:14:18 +08:00
parent 721cc48773
commit 4829c53355
3 changed files with 141 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ import { $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/se
import { $findMatchingParent } from '@lexical/utils'; import { $findMatchingParent } from '@lexical/utils';
import DropdownColorPicker from '../ui/DropdownColorPicker'; import DropdownColorPicker from '../ui/DropdownColorPicker';
import { exportToJSON, importFromJSON } from '../utils/exportImport';
import '../styles/toolbar.css'; import '../styles/toolbar.css';
export default function ToolbarPlugin() { export default function ToolbarPlugin() {
@@ -279,6 +280,22 @@ export default function ToolbarPlugin() {
aria-label="Justify Align"> aria-label="Justify Align">
<i className="format justify-align" /> <i className="format justify-align" />
</button> </button>
<div className="divider" />
<button
onClick={() => importFromJSON(editor)}
className="toolbar-item"
title="Import from JSON"
aria-label="Import editor state from JSON file">
<i className="format import" />
</button>
<button
onClick={() => exportToJSON(editor)}
className="toolbar-item"
title="Export to JSON"
aria-label="Export editor state to JSON file">
<i className="format export" />
</button>
</div> </div>
); );
} }

View File

@@ -144,3 +144,12 @@ i.format.right-align {
i.format.justify-align { i.format.justify-align {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>'); background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>');
} }
i.format.import {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>');
}
i.format.export {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>');
}

View File

@@ -0,0 +1,115 @@
import type { LexicalEditor, SerializedEditorState } from 'lexical';
import { CLEAR_HISTORY_COMMAND } from 'lexical';
export interface SerializedDocument {
/** The serialized editorState produced by editorState.toJSON() */
editorState: SerializedEditorState;
/** The time this document was created in epoch milliseconds (Date.now()) */
lastSaved: number;
/** The source of the document, defaults to Cialloo */
source: string;
}
/**
* Generates a SerializedDocument from the current editor state
*/
function serializedDocumentFromEditorState(
editor: LexicalEditor,
config: Readonly<{
source?: string;
lastSaved?: number;
}> = Object.freeze({}),
): SerializedDocument {
return {
editorState: editor.getEditorState().toJSON(),
lastSaved: config.lastSaved || Date.now(),
source: config.source || 'Cialloo Editor',
};
}
/**
* Exports the editor content as a JSON file
*/
export function exportToJSON(
editor: LexicalEditor,
config: Readonly<{
fileName?: string;
source?: string;
}> = Object.freeze({}),
) {
const now = new Date();
const serializedDocument = serializedDocumentFromEditorState(editor, {
...config,
lastSaved: now.getTime(),
});
const fileName = config.fileName || `document-${now.toISOString()}`;
exportBlob(serializedDocument, `${fileName}.json`);
}
/**
* Creates a downloadable blob and triggers download
*/
function exportBlob(data: SerializedDocument, fileName: string) {
const a = document.createElement('a');
const body = document.body;
if (body === null) {
return;
}
body.appendChild(a);
a.style.display = 'none';
const json = JSON.stringify(data, null, 2); // Pretty print with 2 spaces
const blob = new Blob([json], {
type: 'application/json',
});
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
a.remove();
}
/**
* Imports editor content from a JSON file
*/
export function importFromJSON(editor: LexicalEditor) {
readJSONFileFromSystem((text) => {
try {
const json = JSON.parse(text) as SerializedDocument;
const editorState = editor.parseEditorState(json.editorState);
editor.setEditorState(editorState);
editor.dispatchCommand(CLEAR_HISTORY_COMMAND, undefined);
} catch (error) {
console.error('Failed to import JSON:', error);
alert('Failed to import file. Please make sure it\'s a valid JSON export.');
}
});
}
/**
* Reads a JSON file from the user's system
*/
function readJSONFileFromSystem(callback: (text: string) => void) {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.addEventListener('change', (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files) {
const file = target.files[0];
const reader = new FileReader();
reader.readAsText(file, 'UTF-8');
reader.onload = (readerEvent) => {
if (readerEvent.target) {
const content = readerEvent.target.result;
callback(content as string);
}
};
}
});
input.click();
}