diff --git a/.flowconfig b/.flowconfig index a702f2a33..2f076788b 100644 --- a/.flowconfig +++ b/.flowconfig @@ -34,6 +34,7 @@ module.name_mapper='^lexical/text' -> '/packages/lexical/src/helpe module.name_mapper='^lexical/nodes' -> '/packages/lexical/src/helpers/LexicalNodeHelpers.js' module.name_mapper='^lexical/elements' -> '/packages/lexical/src/helpers/LexicalElementHelpers.js' module.name_mapper='^lexical/events' -> '/packages/lexical/src/helpers/LexicalEventHelpers.js' +module.name_mapper='^lexical/file' -> '/packages/lexical/src/helpers/LexicalFileHelpers.js' module.name_mapper='^lexical/offsets' -> '/packages/lexical/src/helpers/LexicalOffsetHelpers.js' module.name_mapper='^lexical/root' -> '/packages/lexical/src/helpers/LexicalRootHelpers.js' diff --git a/jest.config.js b/jest.config.js index 870d6ca3c..e7f4eb91d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -57,6 +57,8 @@ module.exports = { '/packages/lexical/src/helpers/LexicalElementHelpers.js', '^lexical/events$': '/packages/lexical/src/helpers/LexicalEventHelpers.js', + '^lexical/file$': + '/packages/lexical/src/helpers/LexicalFileHelpers.js', '^lexical/offsets$': '/packages/lexical/src/helpers/LexicalOffsetHelpers.js', '^lexical/root$': diff --git a/packages/lexical-playground/craco.config.js b/packages/lexical-playground/craco.config.js index b6abd291f..b26ea1c74 100644 --- a/packages/lexical-playground/craco.config.js +++ b/packages/lexical-playground/craco.config.js @@ -22,6 +22,7 @@ module.exports = { 'lexical/nodes': 'lexical/dist/LexicalNodeHelpers', 'lexical/elements': 'lexical/dist/LexicalElementHelpers', 'lexical/events': 'lexical/dist/LexicalEventHelpers', + 'lexical/file': 'lexical/dist/LexicalFileHelpers', 'lexical/offsets': 'lexical/dist/LexicalOffsetHelpers', 'lexical/root': 'lexical/dist/LexicalRootHelpers', diff --git a/packages/lexical-playground/src/images/icons/upload.svg b/packages/lexical-playground/src/images/icons/upload.svg new file mode 100644 index 000000000..be3f8e378 --- /dev/null +++ b/packages/lexical-playground/src/images/icons/upload.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/lexical-playground/src/index.css b/packages/lexical-playground/src/index.css index bed816763..d06c3c1fe 100644 --- a/packages/lexical-playground/src/index.css +++ b/packages/lexical-playground/src/index.css @@ -397,6 +397,14 @@ i.sticky { 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 { background-color: rgb(223, 232, 250); } diff --git a/packages/lexical-playground/src/plugins/ActionsPlugin.js b/packages/lexical-playground/src/plugins/ActionsPlugin.js index 11409acf9..383fedbec 100644 --- a/packages/lexical-playground/src/plugins/ActionsPlugin.js +++ b/packages/lexical-playground/src/plugins/ActionsPlugin.js @@ -16,6 +16,7 @@ import {useCallback, useEffect, useState} from 'react'; import {$createStickyNode} from '../nodes/StickyNode'; import {$log, $getRoot, createEditorStateRef} from 'lexical'; import useLexicalList from 'lexical-react/useLexicalList'; +import {importFile, exportFile} from 'lexical/file'; const EditorPriority: CommandListenerEditorPriority = 0; @@ -67,6 +68,21 @@ export default function ActionsPlugins({ return (
+ + diff --git a/packages/lexical-react/src/useLexicalHistory.js b/packages/lexical-react/src/useLexicalHistory.js index 72cf48db9..50f108609 100644 --- a/packages/lexical-react/src/useLexicalHistory.js +++ b/packages/lexical-react/src/useLexicalHistory.js @@ -257,6 +257,12 @@ export function useLexicalHistory( [externalHistoryState], ); + const clearHistory = useCallback(() => { + historyState.undoStack = []; + historyState.redoStack = []; + historyState.current = null; + }, [historyState]); + useEffect(() => { const getMergeAction = createMergeActionGetter(editor, delay); const applyChange = ({ @@ -356,28 +362,26 @@ export function useLexicalHistory( }; const applyCommand = (type) => { - if (type === 'undo') { - undo(); - return true; + switch (type) { + case 'undo': + 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( editor.addListener('command', applyCommand, EditorPriority), editor.addListener('update', applyChange), ); - }, [delay, editor, historyState]); - - const clearHistory = useCallback(() => { - historyState.undoStack = []; - historyState.redoStack = []; - historyState.current = null; - }, [historyState]); + }, [clearHistory, delay, editor, historyState]); return clearHistory; } diff --git a/packages/lexical/src/core/LexicalConstants.js b/packages/lexical/src/core/LexicalConstants.js index 23841010b..bbac4d354 100644 --- a/packages/lexical/src/core/LexicalConstants.js +++ b/packages/lexical/src/core/LexicalConstants.js @@ -10,6 +10,8 @@ import type {TextFormatType, TextModeType} from './LexicalTextNode'; import type {ElementFormatType} from './LexicalElementNode'; +export const VERSION = '0.1.1'; + // Reconciling export const NO_DIRTY_NODES = 0; export const HAS_DIRTY_NODES = 1; diff --git a/packages/lexical/src/core/index.js b/packages/lexical/src/core/index.js index 26a934396..d70d4ad53 100644 --- a/packages/lexical/src/core/index.js +++ b/packages/lexical/src/core/index.js @@ -34,6 +34,7 @@ export type {LineBreakNode} from './LexicalLineBreakNode'; export type {RootNode} from './LexicalRootNode'; export type {ElementFormatType} from './LexicalElementNode'; +import {VERSION} from './LexicalConstants'; import {createEditor} from './LexicalEditor'; import {$createTextNode, $isTextNode, TextNode} from './LexicalTextNode'; import {$isElementNode, ElementNode} from './LexicalElementNode'; @@ -60,6 +61,7 @@ import {$createNodeFromParse} from './LexicalParsing'; import {createEditorStateRef, isEditorStateRef} from './LexicalReference'; export { + VERSION, createEditor, ElementNode, DecoratorNode, diff --git a/packages/lexical/src/helpers/LexicalFileHelpers.js b/packages/lexical/src/helpers/LexicalFileHelpers.js new file mode 100644 index 000000000..673b8a784 --- /dev/null +++ b/packages/lexical/src/helpers/LexicalFileHelpers.js @@ -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(); +} diff --git a/scripts/build.js b/scripts/build.js index b32970802..dde925d46 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -167,6 +167,12 @@ async function build(name, inputFile, outputFile) { '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', replacement: path.resolve( diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index f659d80da..7bdd85f0f 100644 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -20,6 +20,9 @@ async function prepareLexicalPackage() { await exec( `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( `mv ./packages/lexical/npm/LexicalOffsetHelpers.js ./packages/lexical/npm/offsets.js`, );