Skip to content

Commit 81446cd

Browse files
Kepronetrepum
andauthored
[lexical][lexical-clipboard][lexical-playground] Feature: ImageNode caption support for exportDOM and importDOM (#7900)
Co-authored-by: Bob Ippolito <bob@redivi.com>
1 parent 2414b97 commit 81446cd

File tree

5 files changed

+258
-24
lines changed

5 files changed

+258
-24
lines changed

packages/lexical-clipboard/src/clipboard.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,11 @@ function $appendNodesToJSON(
320320
$isElementNode(currentNode) && currentNode.excludeFromCopy('html');
321321
let target = currentNode;
322322

323-
if (selection !== null) {
324-
let clone = $cloneWithProperties(currentNode);
325-
clone =
326-
$isTextNode(clone) && selection !== null
327-
? $sliceSelectedTextNodeContent(selection, clone)
328-
: clone;
329-
target = clone;
323+
if (selection !== null && $isTextNode(target)) {
324+
target = $sliceSelectedTextNodeContent(
325+
selection,
326+
$cloneWithProperties(target),
327+
);
330328
}
331329
const children = $isElementNode(target) ? target.getChildren() : [];
332330

packages/lexical-playground/__tests__/e2e/CopyAndPaste/html/ImageHTMLCopyAndPaste.spec.mjs

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,130 @@
66
*
77
*/
88

9-
import {moveLeft, undo} from '../../../keyboardShortcuts/index.mjs';
9+
import {expect} from '@playwright/test';
10+
11+
import {moveLeft, selectAll, undo} from '../../../keyboardShortcuts/index.mjs';
1012
import {
1113
assertHTML,
14+
copyToClipboard,
1215
focusEditor,
1316
html,
1417
initialize,
1518
LEXICAL_IMAGE_BASE64,
1619
pasteFromClipboard,
20+
prettifyHTML,
21+
SAMPLE_IMAGE_URL,
1722
sleepInsertImage,
1823
test,
1924
} from '../../../utils/index.mjs';
2025

2126
test.describe('HTML Image CopyAndPaste', () => {
22-
test.beforeEach(({isCollab, page}) => initialize({isCollab, page}));
27+
test.beforeEach(({isCollab, page}) =>
28+
initialize({isCollab, page, showNestedEditorTreeView: false}),
29+
);
30+
31+
test('Copy + paste HTML of a figure with img and figcaption', async ({
32+
page,
33+
isPlainText,
34+
isCollab,
35+
browserName,
36+
}) => {
37+
test.skip(isPlainText || isCollab);
38+
let clipboard = {
39+
'text/html': html`
40+
<meta charset="utf-8" />
41+
<figure>
42+
<img
43+
alt="sample image alt"
44+
height="inherit"
45+
src="${SAMPLE_IMAGE_URL}"
46+
width="inherit" />
47+
<figcaption>
48+
this is a caption with
49+
<b>rich text</b>
50+
</figcaption>
51+
</figure>
52+
`,
53+
};
54+
await page.keyboard.type('An image');
55+
await moveLeft(page, 'image'.length);
56+
await pasteFromClipboard(page, clipboard);
57+
await sleepInsertImage();
58+
await page.keyboard.type(' inline ');
59+
await page.pause();
60+
const captionEditorStyle =
61+
(browserName === 'webkit' ? '' : `user-select: text; `) +
62+
`white-space: pre-wrap; word-break: break-word`;
63+
64+
await assertHTML(
65+
page,
66+
html`
67+
<p class="PlaygroundEditorTheme__paragraph" dir="auto">
68+
<span data-lexical-text="true">An</span>
69+
<span
70+
class="editor-image"
71+
contenteditable="false"
72+
data-lexical-decorator="true">
73+
<div draggable="false">
74+
<img
75+
alt="sample image alt"
76+
draggable="false"
77+
src="${SAMPLE_IMAGE_URL}"
78+
style="height: inherit; max-width: 500px; width: inherit" />
79+
</div>
80+
<div class="image-caption-container">
81+
<div
82+
class="ImageNode__contentEditable"
83+
contenteditable="true"
84+
role="textbox"
85+
spellcheck="true"
86+
style="${captionEditorStyle}"
87+
aria-placeholder="Enter a caption..."
88+
data-lexical-editor="true">
89+
<p class="PlaygroundEditorTheme__paragraph" dir="auto">
90+
<span data-lexical-text="true">this is a caption with</span>
91+
<strong
92+
class="PlaygroundEditorTheme__textBold"
93+
data-lexical-text="true">
94+
rich text
95+
</strong>
96+
</p>
97+
</div>
98+
</div>
99+
</span>
100+
<span data-lexical-text="true">inline image</span>
101+
</p>
102+
`,
103+
);
104+
105+
await selectAll(page);
106+
clipboard = await copyToClipboard(page);
107+
expect(await prettifyHTML(clipboard['text/html'])).toEqual(
108+
await prettifyHTML(
109+
html`
110+
<span style="white-space: pre-wrap;">An</span>
111+
<figure>
112+
<img
113+
alt="sample image alt"
114+
height="inherit"
115+
src="${SAMPLE_IMAGE_URL}"
116+
width="inherit" />
117+
<figcaption>
118+
<span style="white-space: pre-wrap;">this is a caption with</span>
119+
<b>
120+
<strong
121+
class="PlaygroundEditorTheme__textBold"
122+
style="white-space: pre-wrap;">
123+
rich text
124+
</strong>
125+
</b>
126+
</figcaption>
127+
</figure>
128+
<span style="white-space: pre-wrap;">inline image</span>
129+
`.trim(),
130+
),
131+
);
132+
});
23133

24134
test('Copy + paste an image', async ({page, isPlainText}) => {
25135
test.skip(isPlainText);

packages/lexical-playground/src/nodes/ImageComponent.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type {JSX} from 'react';
1111

1212
import './ImageNode.css';
1313

14-
import {AutoFocusPlugin} from '@lexical/react/LexicalAutoFocusPlugin';
1514
import {useCollaborationContext} from '@lexical/react/LexicalCollaborationContext';
1615
import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
1716
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
@@ -25,11 +24,14 @@ import {useLexicalNodeSelection} from '@lexical/react/useLexicalNodeSelection';
2524
import {mergeRegister} from '@lexical/utils';
2625
import {
2726
$getNodeByKey,
27+
$getRoot,
2828
$getSelection,
2929
$isNodeSelection,
3030
$isRangeSelection,
3131
$setSelection,
32+
BLUR_COMMAND,
3233
CLICK_COMMAND,
34+
COMMAND_PRIORITY_EDITOR,
3335
COMMAND_PRIORITY_LOW,
3436
createCommand,
3537
DRAGSTART_COMMAND,
@@ -58,7 +60,7 @@ import MentionsPlugin from '../plugins/MentionsPlugin';
5860
import TreeViewPlugin from '../plugins/TreeViewPlugin';
5961
import ContentEditable from '../ui/ContentEditable';
6062
import ImageResizer from '../ui/ImageResizer';
61-
import {$isImageNode} from './ImageNode';
63+
import {$isCaptionEditorEmpty, $isImageNode} from './ImageNode';
6264

6365
type ImageStatus =
6466
| {error: true}
@@ -69,6 +71,27 @@ const imageCache = new Map<string, Promise<ImageStatus> | ImageStatus>();
6971
export const RIGHT_CLICK_IMAGE_COMMAND: LexicalCommand<MouseEvent> =
7072
createCommand('RIGHT_CLICK_IMAGE_COMMAND');
7173

74+
function DisableCaptionOnBlur({
75+
setShowCaption,
76+
}: {
77+
setShowCaption: (show: boolean) => void;
78+
}) {
79+
const [editor] = useLexicalComposerContext();
80+
useEffect(() =>
81+
editor.registerCommand(
82+
BLUR_COMMAND,
83+
() => {
84+
if ($isCaptionEditorEmpty()) {
85+
setShowCaption(false);
86+
}
87+
return false;
88+
},
89+
COMMAND_PRIORITY_EDITOR,
90+
),
91+
);
92+
return null;
93+
}
94+
7295
function useSuspenseImage(src: string): ImageStatus {
7396
let cached = imageCache.get(src);
7497
if (cached && 'error' in cached && typeof cached.error === 'boolean') {
@@ -390,11 +413,18 @@ export default function ImageComponent({
390413
);
391414
}, [editor, $onEnter, $onEscape, onClick, onRightClick]);
392415

393-
const setShowCaption = () => {
416+
const setShowCaption = (show: boolean) => {
394417
editor.update(() => {
395418
const node = $getNodeByKey(nodeKey);
396419
if ($isImageNode(node)) {
397-
node.setShowCaption(true);
420+
node.setShowCaption(show);
421+
if (show) {
422+
node.__caption.update(() => {
423+
if (!$getSelection()) {
424+
$getRoot().selectEnd();
425+
}
426+
});
427+
}
398428
}
399429
});
400430
};
@@ -454,7 +484,7 @@ export default function ImageComponent({
454484
{showCaption && (
455485
<div className="image-caption-container">
456486
<LexicalNestedComposer initialEditor={caption}>
457-
<AutoFocusPlugin />
487+
<DisableCaptionOnBlur setShowCaption={setShowCaption} />
458488
<MentionsPlugin />
459489
<LinkPlugin />
460490
<EmojisPlugin />

0 commit comments

Comments
 (0)