Add clipboard package, remove lexical event helpers (#1361)

* add clipboard package, remove lexica event helpers

* bump version after rebase

* rename - wip

* rename things
This commit is contained in:
Acy Watson
2022-02-25 13:48:12 -08:00
committed by acywatson
parent 06f4602d46
commit 5503ca6e64
22 changed files with 7385 additions and 4669 deletions

View File

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

View File

@ -21,11 +21,11 @@ module.exports = {
},
moduleNameMapper: {
'^./dist/(.+)': './src/$1',
'^@lexical/clipboard$':
'<rootDir>/packages/lexical-clipboard/src/index.js',
'^@lexical/file$': '<rootDir>/packages/lexical-file/src/index.js',
'^@lexical/helpers/elements$':
'<rootDir>/packages/lexical-helpers/src/LexicalElementHelpers.js',
'^@lexical/helpers/events$':
'<rootDir>/packages/lexical-helpers/src/LexicalEventHelpers.js',
'^@lexical/helpers/nodes$':
'<rootDir>/packages/lexical-helpers/src/LexicalNodeHelpers.js',
'^@lexical/helpers/offsets$':

11487
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
'use strict';
module.exports = require('./dist/LexicalClipboard.js');

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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,
RangeSelection,
} from "lexical";
/*
* Rich Text
*/
declare export function $insertDataTransferForRichText(
dataTransfer: DataTransfer,
selection: RangeSelection,
editor: LexicalEditor,
): void;
declare export function getHtmlContent(editor: LexicalEditor): string;
declare export function getLexicalContent(editor: LexicalEditor): string;
/*
* Plain Text
*/
declare export function $insertDataTransferForPlainText(
dataTransfer: DataTransfer,
selection: RangeSelection,
): void;

View File

@ -0,0 +1,3 @@
# `@lexical/clipboard`
This package contains the functionality for the clipboard feature of Lexical.

View File

@ -0,0 +1,27 @@
{
"name": "@lexical/clipboard",
"author": {
"name": "Dominic Gannaway",
"email": "dg@domgan.com"
},
"description": "This package provides the copy/paste functionality for Lexical.",
"keywords": [
"lexical",
"editor",
"rich-text",
"copy",
"paste"
],
"license": "MIT",
"version": "0.1.11",
"main": "LexicalClipboard.js",
"peerDependencies": {
"lexical": "0.1.11",
"@lexical/helpers": "0.1.11"
},
"repository": {
"type": "git",
"url": "https://github.com/facebook/lexical",
"directory": "packages/lexical-clipboard"
}
}

View File

@ -22,19 +22,99 @@ import {$cloneContents} from '@lexical/helpers/selection';
import {
$createNodeFromParse,
$createParagraphNode,
$getDecoratorNode,
$getSelection,
$isDecoratorNode,
$isElementNode,
$isRangeSelection,
} from 'lexical';
// TODO the Flow types here needs fixing
export type EventHandler = (
// $FlowFixMe: not sure how to handle this generic properly
event: Object,
export function getHtmlContent(editor: LexicalEditor): string | null {
const domSelection = window.getSelection();
// If we haven't selected a range, then don't copy anything
if (domSelection.isCollapsed) {
return null;
}
const range = domSelection.getRangeAt(0);
if (range) {
const container = document.createElement('div');
const frag = range.cloneContents();
container.appendChild(frag);
return container.innerHTML;
}
return null;
}
export function $getLexicalContent(editor: LexicalEditor): string | null {
const selection = $getSelection();
if (selection !== null) {
const namespace = editor._config.namespace;
return JSON.stringify({namespace, state: $cloneContents(selection)});
}
return null;
}
export function $insertDataTransferForPlainText(
dataTransfer: DataTransfer,
selection: RangeSelection,
): void {
const text = dataTransfer.getData('text/plain');
if (text != null) {
selection.insertRawText(text);
}
}
export function $insertDataTransferForRichText(
dataTransfer: DataTransfer,
selection: RangeSelection,
editor: LexicalEditor,
) => void;
): void {
const lexicalNodesString = dataTransfer.getData(
'application/x-lexical-editor',
);
if (lexicalNodesString) {
const namespace = editor._config.namespace;
try {
const lexicalClipboardData = JSON.parse(lexicalNodesString);
if (lexicalClipboardData.namespace === namespace) {
const nodeRange = lexicalClipboardData.state;
const nodes = $generateNodes(nodeRange);
selection.insertNodes(nodes);
return;
}
} catch (e) {
// Malformed, missing nodes..
}
}
const textHtmlMimeType = 'text/html';
const htmlString = dataTransfer.getData(textHtmlMimeType);
if (htmlString) {
const parser = new DOMParser();
const dom = parser.parseFromString(htmlString, textHtmlMimeType);
const nodes = $generateNodesFromDOM(dom, editor);
// Wrap text and inline nodes in paragraph nodes so we have all blocks at the top-level
const topLevelBlocks = [];
let currentBlock = null;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (!$isElementNode(node) || node.isInline()) {
if (currentBlock === null) {
currentBlock = $createParagraphNode();
topLevelBlocks.push(currentBlock);
}
if (currentBlock !== null) {
currentBlock.append(node);
}
} else {
topLevelBlocks.push(node);
currentBlock = null;
}
}
selection.insertNodes(topLevelBlocks);
return;
}
$insertDataTransferForPlainText(dataTransfer, selection);
}
function $generateNodes(nodeRange: {
nodeMap: ParsedNodeMap,
@ -77,7 +157,7 @@ function getConversionFunction(
return currentConversion !== null ? currentConversion.conversion : null;
}
export function $createNodesFromDOM(
function $createNodesFromDOM(
node: Node,
editor: LexicalEditor,
forChildMap: Map<string, DOMChildConversion> = new Map(),
@ -145,191 +225,3 @@ function $generateNodesFromDOM(
}
return lexicalNodes;
}
export function $insertDataTransferForRichText(
dataTransfer: DataTransfer,
selection: RangeSelection,
editor: LexicalEditor,
): void {
const lexicalNodesString = dataTransfer.getData(
'application/x-lexical-editor',
);
if (lexicalNodesString) {
const namespace = editor._config.namespace;
try {
const lexicalClipboardData = JSON.parse(lexicalNodesString);
if (lexicalClipboardData.namespace === namespace) {
const nodeRange = lexicalClipboardData.state;
const nodes = $generateNodes(nodeRange);
selection.insertNodes(nodes);
return;
}
} catch (e) {
// Malformed, missing nodes..
}
}
const textHtmlMimeType = 'text/html';
const htmlString = dataTransfer.getData(textHtmlMimeType);
if (htmlString) {
const parser = new DOMParser();
const dom = parser.parseFromString(htmlString, textHtmlMimeType);
const nodes = $generateNodesFromDOM(dom, editor);
// Wrap text and inline nodes in paragraph nodes so we have all blocks at the top-level
const topLevelBlocks = [];
let currentBlock = null;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (!$isElementNode(node) || node.isInline()) {
if (currentBlock === null) {
currentBlock = $createParagraphNode();
topLevelBlocks.push(currentBlock);
}
if (currentBlock !== null) {
currentBlock.append(node);
}
} else {
topLevelBlocks.push(node);
currentBlock = null;
}
}
selection.insertNodes(topLevelBlocks);
return;
}
$insertDataTransferForPlainText(dataTransfer, selection);
}
export function $insertDataTransferForPlainText(
dataTransfer: DataTransfer,
selection: RangeSelection,
): void {
const text = dataTransfer.getData('text/plain');
if (text != null) {
selection.insertRawText(text);
}
}
export function $shouldOverrideDefaultCharacterSelection(
selection: RangeSelection,
isBackward: boolean,
): boolean {
const possibleNode = $getDecoratorNode(selection.focus, isBackward);
return $isDecoratorNode(possibleNode) && !possibleNode.isIsolated();
}
export function onPasteForPlainText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
event.preventDefault();
editor.update(() => {
const selection = $getSelection();
const clipboardData = event.clipboardData;
if (clipboardData != null && $isRangeSelection(selection)) {
$insertDataTransferForPlainText(clipboardData, selection);
}
});
}
export function onPasteForRichText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
event.preventDefault();
editor.update(() => {
const selection = $getSelection();
const clipboardData = event.clipboardData;
if (clipboardData != null && $isRangeSelection(selection)) {
$insertDataTransferForRichText(clipboardData, selection, editor);
}
});
}
export function onCutForPlainText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
onCopyForPlainText(event, editor);
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.removeText();
}
});
}
export function onCutForRichText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
onCopyForRichText(event, editor);
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.removeText();
}
});
}
export function onCopyForPlainText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
event.preventDefault();
editor.update(() => {
const clipboardData = event.clipboardData;
const selection = $getSelection();
if (selection !== null) {
if (clipboardData != null) {
const domSelection = window.getSelection();
// If we haven't selected a range, then don't copy anything
if (domSelection.isCollapsed) {
return;
}
const range = domSelection.getRangeAt(0);
if (range) {
const container = document.createElement('div');
const frag = range.cloneContents();
container.appendChild(frag);
clipboardData.setData('text/html', container.innerHTML);
}
clipboardData.setData('text/plain', selection.getTextContent());
}
}
});
}
export function onCopyForRichText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
event.preventDefault();
editor.update(() => {
const clipboardData = event.clipboardData;
const selection = $getSelection();
if (selection !== null) {
if (clipboardData != null) {
const domSelection = window.getSelection();
// If we haven't selected a range, then don't copy anything
if (domSelection.isCollapsed) {
return;
}
const range = domSelection.getRangeAt(0);
if (range) {
const container = document.createElement('div');
const frag = range.cloneContents();
container.appendChild(frag);
clipboardData.setData('text/html', container.innerHTML);
}
clipboardData.setData('text/plain', selection.getTextContent());
const namespace = editor._config.namespace;
clipboardData.setData(
'application/x-lexical-editor',
JSON.stringify({namespace, state: $cloneContents(selection)}),
);
}
}
});
}

View File

@ -0,0 +1,22 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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 {
$getLexicalContent,
$insertDataTransferForPlainText,
$insertDataTransferForRichText,
getHtmlContent,
} from './clipboard';
export {
$getLexicalContent,
$insertDataTransferForPlainText,
$insertDataTransferForRichText,
getHtmlContent,
};

View File

@ -19,6 +19,7 @@ import type {
} from 'lexical';
import {
$getDecoratorNode,
$isDecoratorNode,
$isElementNode,
$isLeafNode,
@ -608,3 +609,11 @@ export function $isAtNodeEnd(point: Point): boolean {
}
return point.offset === point.getNode().getChildrenSize();
}
export function $shouldOverrideDefaultCharacterSelection(
selection: RangeSelection,
isBackward: boolean,
): boolean {
const possibleNode = $getDecoratorNode(selection.focus, isBackward);
return $isDecoratorNode(possibleNode) && !possibleNode.isIsolated();
}

View File

@ -28,7 +28,6 @@ module.exports = {
'@lexical/helpers/nodes': '@lexical/helpers/dist/LexicalNodeHelpers',
'@lexical/helpers/elements':
'@lexical/helpers/dist/LexicalElementHelpers',
'@lexical/helpers/events': '@lexical/helpers/dist/LexicalEventHelpers',
'@lexical/helpers/file': '@lexical/helpers/dist/LexicalFileHelpers',
'@lexical/helpers/offsets': '@lexical/helpers/dist/LexicalOffsetHelpers',
'@lexical/helpers/root': '@lexical/helpers/dist/LexicalRootHelpers',
@ -37,6 +36,7 @@ module.exports = {
'@lexical/list': '@lexical/list/dist/LexicalList.js',
'@lexical/table': '@lexical/table/dist/LexicalTable.js',
'@lexical/file': '@lexical/file/dist/LexicalFile.js',
'@lexical/clipboard': '@lexical/clipboard/dist/LexicalClipboard.js',
// Lexical React
'@lexical/react/LexicalTreeView': '@lexical/react/dist/LexicalTreeView',

View File

@ -12,6 +12,7 @@
"@lexical/list": "0.1.11",
"@lexical/table": "0.1.11",
"@lexical/file": "0.1.11",
"@lexical/clipboard": "0.1.11",
"link-preview-generator": "1.0.7",
"@craco/craco": "6.1.2",
"@excalidraw/excalidraw": "0.11.0",

View File

@ -18,6 +18,7 @@
"@lexical/helpers": "0.1.11",
"@lexical/table": "0.1.11",
"@lexical/yjs": "0.1.11",
"@lexical/clipboard": "0.1.11",
"react": ">=17.x",
"react-dom": ">=17.x"
},

View File

@ -0,0 +1,107 @@
import type {LexicalEditor} from 'lexical';
import {
$getLexicalContent,
$insertDataTransferForPlainText,
$insertDataTransferForRichText,
getHtmlContent,
} from '@lexical/clipboard';
import {$getSelection, $isRangeSelection} from 'lexical';
export function onPasteForPlainText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
event.preventDefault();
editor.update(() => {
const selection = $getSelection();
const clipboardData = event.clipboardData;
if (clipboardData != null && $isRangeSelection(selection)) {
$insertDataTransferForPlainText(clipboardData, selection);
}
});
}
export function onCutForPlainText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
onCopyForPlainText(event, editor);
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.removeText();
}
});
}
export function onCopyForPlainText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
event.preventDefault();
editor.update(() => {
const clipboardData = event.clipboardData;
const selection = $getSelection();
if (selection !== null) {
if (clipboardData != null) {
const htmlString = getHtmlContent(editor);
if (htmlString !== null) {
clipboardData.setData('text/html', htmlString);
}
clipboardData.setData('text/plain', selection.getTextContent());
}
}
});
}
export function onCutForRichText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
onCopyForRichText(event, editor);
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.removeText();
}
});
}
export function onCopyForRichText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
event.preventDefault();
editor.update(() => {
const clipboardData = event.clipboardData;
const selection = $getSelection();
if (selection !== null) {
if (clipboardData != null) {
const htmlString = getHtmlContent(editor);
const lexicalString = $getLexicalContent(editor);
if (htmlString !== null) {
clipboardData.setData('text/html', htmlString);
}
if (lexicalString !== null) {
clipboardData.setData('application/x-lexical-editor', lexicalString);
}
clipboardData.setData('text/plain', selection.getTextContent());
}
}
});
}
export function onPasteForRichText(
event: ClipboardEvent,
editor: LexicalEditor,
): void {
event.preventDefault();
editor.update(() => {
const selection = $getSelection();
const clipboardData = event.clipboardData;
if (clipboardData != null && $isRangeSelection(selection)) {
$insertDataTransferForRichText(clipboardData, selection, editor);
}
});
}

View File

@ -7,8 +7,7 @@
* @flow strict
*/
import type {EventHandler} from '@lexical/helpers/events';
import type {LexicalEditor} from 'lexical';
import type {EventHandler, LexicalEditor} from 'lexical';
import useLayoutEffect from 'shared/useLayoutEffect';

View File

@ -10,17 +10,19 @@
import type {InitialEditorStateType} from './PlainRichTextUtils';
import type {CommandListenerEditorPriority, LexicalEditor} from 'lexical';
import {$insertDataTransferForPlainText} from '@lexical/clipboard';
import {
$insertDataTransferForPlainText,
$moveCharacter,
$shouldOverrideDefaultCharacterSelection,
onCopyForPlainText,
onCutForPlainText,
onPasteForPlainText,
} from '@lexical/helpers/events';
import {$moveCharacter} from '@lexical/helpers/selection';
} from '@lexical/helpers/selection';
import {$getSelection, $isRangeSelection} from 'lexical';
import {useEffect} from 'react';
import {
onCopyForPlainText,
onCutForPlainText,
onPasteForPlainText,
} from './clipboardEvents';
import {initializeEditor} from './PlainRichTextUtils';
import useLexicalDragonSupport from './useLexicalDragonSupport';

View File

@ -15,14 +15,11 @@ import type {
TextFormatType,
} from 'lexical';
import {$insertDataTransferForRichText} from '@lexical/clipboard';
import {
$insertDataTransferForRichText,
$moveCharacter,
$shouldOverrideDefaultCharacterSelection,
onCopyForRichText,
onCutForRichText,
onPasteForRichText,
} from '@lexical/helpers/events';
import {$moveCharacter} from '@lexical/helpers/selection';
} from '@lexical/helpers/selection';
import {
$getSelection,
$isElementNode,
@ -31,6 +28,11 @@ import {
} from 'lexical';
import {useLayoutEffect} from 'react';
import {
onCopyForRichText,
onCutForRichText,
onPasteForRichText,
} from './clipboardEvents';
import {initializeEditor} from './PlainRichTextUtils';
import useLexicalDragonSupport from './useLexicalDragonSupport';

View File

@ -930,6 +930,14 @@ declare export function $getDecoratorNode(
isBackward: boolean,
): null | LexicalNode;
// TODO the Flow types here needs fixing
export type EventHandler = (
// $FlowFixMe: not sure how to handle this generic properly
event: Object,
editor: LexicalEditor,
) => void;
/**
* LexicalVersion
*/

View File

@ -52,10 +52,12 @@ if (isClean) {
fs.removeSync(path.resolve('./packages/lexical-list/dist'));
fs.removeSync(path.resolve('./packages/lexical-table/dist'));
fs.removeSync(path.resolve('./packages/lexical-file/dist'));
fs.removeSync(path.resolve('./packages/lexical-clipboard/dist'));
fs.removeSync(path.resolve('./packages/lexical-yjs/dist'));
}
const wwwMappings = {
'@lexical/clipboard': 'LexicalClipboard',
'@lexical/file': 'LexicalFile',
'@lexical/list': 'LexicalList',
'@lexical/table': 'LexicalTable',
@ -107,6 +109,7 @@ const externals = [
'@lexical/list',
'@lexical/table',
'@lexical/file',
'@lexical/clipboard',
'@lexical/yjs',
'react-dom',
'react',
@ -188,12 +191,6 @@ async function build(name, inputFile, outputFile, isProd) {
'packages/lexical-helpers/src/LexicalTextHelpers',
),
},
{
find: '@lexical/helpers/events',
replacement: path.resolve(
'packages/lexical-helpers/src/LexicalEventHelpers',
),
},
{
find: '@lexical/helpers/offsets',
replacement: path.resolve(
@ -365,6 +362,17 @@ const packages = [
outputPath: './packages/lexical-file/dist/',
sourcePath: './packages/lexical-file/src/',
},
{
modules: [
{
outputFileName: 'LexicalClipboard',
sourceFileName: 'index.js',
},
],
name: 'Lexical File',
outputPath: './packages/lexical-clipboard/dist/',
sourcePath: './packages/lexical-clipboard/src/',
},
{
modules: lexicalNodes.map((module) => ({
name: module,

View File

@ -10,6 +10,7 @@ const DEFAULT_PKGS = [
'lexical-list',
'lexical-table',
'lexical-file',
'lexical-clipboard',
];
module.exports = {

View File

@ -93,9 +93,6 @@ async function prepareLexicalHelpersPackage() {
await exec(
`mv ./packages/${LEXICAL_HELPERS_PKG}/npm/LexicalTextHelpers.js ./packages/${LEXICAL_HELPERS_PKG}/npm/text.js`,
);
await exec(
`mv ./packages/${LEXICAL_HELPERS_PKG}/npm/LexicalEventHelpers.js ./packages/${LEXICAL_HELPERS_PKG}/npm/events.js`,
);
await exec(
`mv ./packages/${LEXICAL_HELPERS_PKG}/npm/LexicalOffsetHelpers.js ./packages/${LEXICAL_HELPERS_PKG}/npm/offsets.js`,
);

View File

@ -10,6 +10,7 @@
const fs = require('fs-extra');
const packages = {
'@lexical/clipboard': 'lexical-clipboard',
'@lexical/file': 'lexical-file',
'@lexical/helpers': 'lexical-helpers',
'@lexical/list': 'lexical-list',