File import/export (#1056)

* File import/export

* LexicalFileHelpers
This commit is contained in:
Gerard Rovira
2022-01-06 11:54:13 +00:00
committed by acywatson
parent 29637141e5
commit a8daa27e7f
12 changed files with 140 additions and 15 deletions

View File

@ -34,6 +34,7 @@ module.name_mapper='^lexical/text' -> '<PROJECT_ROOT>/packages/lexical/src/helpe
module.name_mapper='^lexical/nodes' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalNodeHelpers.js' module.name_mapper='^lexical/nodes' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalNodeHelpers.js'
module.name_mapper='^lexical/elements' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalElementHelpers.js' module.name_mapper='^lexical/elements' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalElementHelpers.js'
module.name_mapper='^lexical/events' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalEventHelpers.js' module.name_mapper='^lexical/events' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalEventHelpers.js'
module.name_mapper='^lexical/file' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalFileHelpers.js'
module.name_mapper='^lexical/offsets' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalOffsetHelpers.js' module.name_mapper='^lexical/offsets' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalOffsetHelpers.js'
module.name_mapper='^lexical/root' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalRootHelpers.js' module.name_mapper='^lexical/root' -> '<PROJECT_ROOT>/packages/lexical/src/helpers/LexicalRootHelpers.js'

View File

@ -57,6 +57,8 @@ module.exports = {
'<rootDir>/packages/lexical/src/helpers/LexicalElementHelpers.js', '<rootDir>/packages/lexical/src/helpers/LexicalElementHelpers.js',
'^lexical/events$': '^lexical/events$':
'<rootDir>/packages/lexical/src/helpers/LexicalEventHelpers.js', '<rootDir>/packages/lexical/src/helpers/LexicalEventHelpers.js',
'^lexical/file$':
'<rootDir>/packages/lexical/src/helpers/LexicalFileHelpers.js',
'^lexical/offsets$': '^lexical/offsets$':
'<rootDir>/packages/lexical/src/helpers/LexicalOffsetHelpers.js', '<rootDir>/packages/lexical/src/helpers/LexicalOffsetHelpers.js',
'^lexical/root$': '^lexical/root$':

View File

@ -22,6 +22,7 @@ module.exports = {
'lexical/nodes': 'lexical/dist/LexicalNodeHelpers', 'lexical/nodes': 'lexical/dist/LexicalNodeHelpers',
'lexical/elements': 'lexical/dist/LexicalElementHelpers', 'lexical/elements': 'lexical/dist/LexicalElementHelpers',
'lexical/events': 'lexical/dist/LexicalEventHelpers', 'lexical/events': 'lexical/dist/LexicalEventHelpers',
'lexical/file': 'lexical/dist/LexicalFileHelpers',
'lexical/offsets': 'lexical/dist/LexicalOffsetHelpers', 'lexical/offsets': 'lexical/dist/LexicalOffsetHelpers',
'lexical/root': 'lexical/dist/LexicalRootHelpers', 'lexical/root': 'lexical/dist/LexicalRootHelpers',

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-upload" viewBox="0 0 16 16">
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/>
</svg>

After

Width:  |  Height:  |  Size: 424 B

View File

@ -397,6 +397,14 @@ i.sticky {
background-image: url(images/icons/sticky.svg); background-image: url(images/icons/sticky.svg);
} }
i.import {
background-image: url(images/icons/upload.svg);
}
i.export {
background-image: url(images/icons/download.svg);
}
.link-editor .button.active, .toolbar .button.active { .link-editor .button.active, .toolbar .button.active {
background-color: rgb(223, 232, 250); background-color: rgb(223, 232, 250);
} }

View File

@ -16,6 +16,7 @@ import {useCallback, useEffect, useState} from 'react';
import {$createStickyNode} from '../nodes/StickyNode'; import {$createStickyNode} from '../nodes/StickyNode';
import {$log, $getRoot, createEditorStateRef} from 'lexical'; import {$log, $getRoot, createEditorStateRef} from 'lexical';
import useLexicalList from 'lexical-react/useLexicalList'; import useLexicalList from 'lexical-react/useLexicalList';
import {importFile, exportFile} from 'lexical/file';
const EditorPriority: CommandListenerEditorPriority = 0; const EditorPriority: CommandListenerEditorPriority = 0;
@ -67,6 +68,21 @@ export default function ActionsPlugins({
return ( return (
<div className="actions"> <div className="actions">
<button
className="action-button import"
onClick={() => importFile(editor)}>
<i className="import" />
</button>
<button
className="action-button export"
onClick={() =>
exportFile(editor, {
fileName: `Playground ${new Date().toISOString()}`,
source: 'Playground',
})
}>
<i className="export" />
</button>
<button className="action-button sticky" onClick={insertSticky}> <button className="action-button sticky" onClick={insertSticky}>
<i className="sticky" /> <i className="sticky" />
</button> </button>

View File

@ -257,6 +257,12 @@ export function useLexicalHistory(
[externalHistoryState], [externalHistoryState],
); );
const clearHistory = useCallback(() => {
historyState.undoStack = [];
historyState.redoStack = [];
historyState.current = null;
}, [historyState]);
useEffect(() => { useEffect(() => {
const getMergeAction = createMergeActionGetter(editor, delay); const getMergeAction = createMergeActionGetter(editor, delay);
const applyChange = ({ const applyChange = ({
@ -356,28 +362,26 @@ export function useLexicalHistory(
}; };
const applyCommand = (type) => { const applyCommand = (type) => {
if (type === 'undo') { switch (type) {
undo(); case 'undo':
return true; undo();
return true;
case 'redo':
redo();
return true;
case 'clear-history':
clearHistory();
return true;
default:
return false;
} }
if (type === 'redo') {
redo();
return true;
}
return false;
}; };
return withSubscriptions( return withSubscriptions(
editor.addListener('command', applyCommand, EditorPriority), editor.addListener('command', applyCommand, EditorPriority),
editor.addListener('update', applyChange), editor.addListener('update', applyChange),
); );
}, [delay, editor, historyState]); }, [clearHistory, delay, editor, historyState]);
const clearHistory = useCallback(() => {
historyState.undoStack = [];
historyState.redoStack = [];
historyState.current = null;
}, [historyState]);
return clearHistory; return clearHistory;
} }

View File

@ -10,6 +10,8 @@
import type {TextFormatType, TextModeType} from './LexicalTextNode'; import type {TextFormatType, TextModeType} from './LexicalTextNode';
import type {ElementFormatType} from './LexicalElementNode'; import type {ElementFormatType} from './LexicalElementNode';
export const VERSION = '0.1.1';
// Reconciling // Reconciling
export const NO_DIRTY_NODES = 0; export const NO_DIRTY_NODES = 0;
export const HAS_DIRTY_NODES = 1; export const HAS_DIRTY_NODES = 1;

View File

@ -34,6 +34,7 @@ export type {LineBreakNode} from './LexicalLineBreakNode';
export type {RootNode} from './LexicalRootNode'; export type {RootNode} from './LexicalRootNode';
export type {ElementFormatType} from './LexicalElementNode'; export type {ElementFormatType} from './LexicalElementNode';
import {VERSION} from './LexicalConstants';
import {createEditor} from './LexicalEditor'; import {createEditor} from './LexicalEditor';
import {$createTextNode, $isTextNode, TextNode} from './LexicalTextNode'; import {$createTextNode, $isTextNode, TextNode} from './LexicalTextNode';
import {$isElementNode, ElementNode} from './LexicalElementNode'; import {$isElementNode, ElementNode} from './LexicalElementNode';
@ -60,6 +61,7 @@ import {$createNodeFromParse} from './LexicalParsing';
import {createEditorStateRef, isEditorStateRef} from './LexicalReference'; import {createEditorStateRef, isEditorStateRef} from './LexicalReference';
export { export {
VERSION,
createEditor, createEditor,
ElementNode, ElementNode,
DecoratorNode, DecoratorNode,

View File

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
import type {LexicalEditor} from 'lexical';
import {VERSION} from 'lexical';
export function importFile(editor: LexicalEditor) {
readTextFileFromSystem((text) => {
const json = JSON.parse(text);
const editorState = editor.parseEditorState(
JSON.stringify(json.editorState),
);
editor.setEditorState(editorState);
editor.execCommand('clear-history');
});
}
function readTextFileFromSystem(callback: (text: string) => void) {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.lexical';
input.addEventListener('change', (e: Event) => {
// $FlowFixMe
const file = e.target.files[0];
const reader = new FileReader();
reader.readAsText(file, 'UTF-8');
reader.onload = (readerEvent) => {
// $FlowFixMe
const content = readerEvent.target.result;
callback(content);
};
});
input.click();
}
export function exportFile(
editor: LexicalEditor,
config?: $ReadOnly<{source?: string, fileName?: string}> = {},
) {
const now = new Date();
const editorState = editor.getEditorState();
const documentJSON = {
source: config.source || 'Lexical',
version: VERSION,
lastSaved: now.getTime(),
editorState: editorState,
};
const fileName = config.fileName || now.toISOString();
exportBlob(documentJSON, `${fileName}.lexical`);
}
// Adapted from https://stackoverflow.com/a/19328891/2013580
function exportBlob(data, 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);
const blob = new Blob([json], {type: 'octet/stream'});
const url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
a.remove();
}

View File

@ -167,6 +167,12 @@ async function build(name, inputFile, outputFile) {
'packages/lexical/src/helpers/LexicalEventHelpers', 'packages/lexical/src/helpers/LexicalEventHelpers',
), ),
}, },
{
find: isWWW ? 'Lexical/file' : 'lexical/file',
replacement: path.resolve(
'packages/lexical/src/helpers/LexicalFileHelpers',
),
},
{ {
find: isWWW ? 'Lexical/offsets' : 'lexical/offsets', find: isWWW ? 'Lexical/offsets' : 'lexical/offsets',
replacement: path.resolve( replacement: path.resolve(

View File

@ -20,6 +20,9 @@ async function prepareLexicalPackage() {
await exec( await exec(
`mv ./packages/lexical/npm/LexicalEventHelpers.js ./packages/lexical/npm/events.js`, `mv ./packages/lexical/npm/LexicalEventHelpers.js ./packages/lexical/npm/events.js`,
); );
await exec(
`mv ./packages/lexical/npm/LexicalFileHelpers.js ./packages/lexical/npm/file.js`,
);
await exec( await exec(
`mv ./packages/lexical/npm/LexicalOffsetHelpers.js ./packages/lexical/npm/offsets.js`, `mv ./packages/lexical/npm/LexicalOffsetHelpers.js ./packages/lexical/npm/offsets.js`,
); );