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
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 23s
This commit is contained in:
@@ -19,6 +19,7 @@ import { $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/se
|
||||
import { $findMatchingParent } from '@lexical/utils';
|
||||
|
||||
import DropdownColorPicker from '../ui/DropdownColorPicker';
|
||||
import { exportToJSON, importFromJSON } from '../utils/exportImport';
|
||||
import '../styles/toolbar.css';
|
||||
|
||||
export default function ToolbarPlugin() {
|
||||
@@ -279,6 +280,22 @@ export default function ToolbarPlugin() {
|
||||
aria-label="Justify Align">
|
||||
<i className="format justify-align" />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,3 +144,12 @@ i.format.right-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>');
|
||||
}
|
||||
|
||||
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>');
|
||||
}
|
||||
|
||||
|
||||
115
src/editor/utils/exportImport.ts
Normal file
115
src/editor/utils/exportImport.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user