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 { $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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>');
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
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