Skip to content

Commit dd07cbf

Browse files
author
Nitesh Kumar Singh
committed
2 parents 61583e0 + bd6d309 commit dd07cbf

File tree

14 files changed

+329
-190
lines changed

14 files changed

+329
-190
lines changed

Extensions/Signum.HtmlEditor/Extensions/BasicCommandsExtension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { HtmlEditorController } from "../HtmlEditorController";
33
import { HtmlEditorExtension } from "./types";
44

55
export class BasicCommandsExtensions implements HtmlEditorExtension {
6+
name = "BasicCommandsExtensions";
7+
68
registerExtension(controller: HtmlEditorController): () => void {
79
return controller.editor.registerCommand(
810
KEY_DOWN_COMMAND,
Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
import { CodeHighlightNode, CodeNode, registerCodeHighlighting } from "@lexical/code";
1+
import {
2+
CodeHighlightNode,
3+
CodeNode,
4+
registerCodeHighlighting,
5+
} from "@lexical/code";
26
import { HtmlEditorController } from "../HtmlEditorController";
37
import {
48
HtmlEditorExtension,
59
LexicalConfigNode,
6-
OptionalCallback
10+
OptionalCallback,
711
} from "./types";
812

913
export class CodeBlockExtension implements HtmlEditorExtension {
14+
name = "CodeBlockExtension";
15+
1016
registerExtension(controller: HtmlEditorController): OptionalCallback {
11-
return registerCodeHighlighting(controller.editor);
17+
return registerCodeHighlighting(controller.editor);
1218
}
1319

1420
getNodes(): LexicalConfigNode {
15-
return [CodeNode, CodeHighlightNode]
21+
return [CodeNode, CodeHighlightNode];
1622
}
1723
}

Extensions/Signum.HtmlEditor/Extensions/ImageExtension/index.tsx

Lines changed: 108 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,63 @@
1-
import { $getRoot, LexicalEditor } from "lexical";
1+
import { $createTextNode, $getRoot, LexicalEditor } from "lexical";
22
import { HtmlEditorController } from "../../HtmlEditorController";
3-
import { HtmlEditorExtension, LexicalConfigNode, OptionalCallback } from "../types";
3+
import {
4+
HtmlEditorExtension,
5+
LexicalConfigNode,
6+
OptionalCallback,
7+
} from "../types";
48
import { ImageConverter } from "./ImageConverter";
59
import { $createImageNode, ImageNode } from "./ImageNode";
610

7-
export class ImageExtension<T extends object = {}> implements HtmlEditorExtension {
11+
export class ImageExtension<T extends object = {}>
12+
implements HtmlEditorExtension
13+
{
14+
name = "ImageExtension";
815
constructor(public imageConverter: ImageConverter<T>) {}
916

1017
registerExtension(controller: HtmlEditorController): OptionalCallback {
1118
const abortController = new AbortController();
1219
const element = controller.editableElement;
1320

14-
if(!element) return;
15-
16-
element.addEventListener("dragover", (event) => {
17-
event.preventDefault();
18-
}, { signal: abortController.signal });
19-
20-
element.addEventListener("drop", (event) => {
21-
event.preventDefault();
22-
const files = event.dataTransfer?.files;
23-
24-
if(!files?.length) return;
25-
this.insertImageNodes(files, controller.editor, this.imageConverter);
26-
}, { signal: abortController.signal });
27-
28-
element.addEventListener("paste", (event) => {
29-
const files = event.clipboardData?.files;
30-
31-
if(!files?.length) return;
32-
event.preventDefault();
33-
this.insertImageNodes(files, controller.editor, this.imageConverter);
34-
}, { signal: abortController.signal });
35-
36-
const unsubscribeUpdateListener = controller.editor.registerUpdateListener(() => {
37-
if(!controller.editor || !this.imageConverter) return;
38-
this.replaceImagePlaceholders(controller);
39-
});
21+
if (!element) return;
22+
23+
element.addEventListener(
24+
"dragover",
25+
(event) => {
26+
event.preventDefault();
27+
},
28+
{ signal: abortController.signal }
29+
);
30+
31+
element.addEventListener(
32+
"drop",
33+
(event) => {
34+
event.preventDefault();
35+
const files = event.dataTransfer?.files;
36+
37+
if (!files?.length) return;
38+
this.insertImageNodes(files, controller.editor, this.imageConverter);
39+
},
40+
{ signal: abortController.signal }
41+
);
42+
43+
element.addEventListener(
44+
"paste",
45+
(event) => {
46+
const files = event.clipboardData?.files;
47+
48+
if (!files?.length) return;
49+
event.preventDefault();
50+
this.insertImageNodes(files, controller.editor, this.imageConverter);
51+
},
52+
{ signal: abortController.signal }
53+
);
54+
55+
const unsubscribeUpdateListener = controller.editor.registerUpdateListener(
56+
() => {
57+
if (!controller.editor || !this.imageConverter) return;
58+
this.replaceImagePlaceholders(controller);
59+
}
60+
);
4061

4162
return () => {
4263
abortController.abort();
@@ -45,70 +66,93 @@ export class ImageExtension<T extends object = {}> implements HtmlEditorExtensio
4566
}
4667

4768
getNodes(): LexicalConfigNode {
48-
return [ImageNode]
49-
69+
return [ImageNode];
5070
}
5171

52-
async insertImageNodes(files: FileList, editor: LexicalEditor, imageConverter: ImageConverter<T>): Promise<void> {
53-
const uploadPromises = Array.from(files).filter(file => file.type.startsWith("image/")).map(file => {
54-
try {
55-
return imageConverter.uploadData(file)
56-
} catch (error) {
57-
console.error("Image uploade failed.", error)
58-
return null;
59-
}
60-
});
61-
62-
const uploadedFiles = (await Promise.all(uploadPromises)).filter(v => v !== null);
63-
if(!uploadedFiles.length) return;
64-
72+
async insertImageNodes(
73+
files: FileList,
74+
editor: LexicalEditor,
75+
imageConverter: ImageConverter<T>
76+
): Promise<void> {
77+
const uploadPromises = Array.from(files)
78+
.filter((file) => file.type.startsWith("image/"))
79+
.map((file) => {
80+
try {
81+
return imageConverter.uploadData(file);
82+
} catch (error) {
83+
console.error("Image uploade failed.", error);
84+
return null;
85+
}
86+
});
87+
88+
const uploadedFiles = (await Promise.all(uploadPromises)).filter(
89+
(v) => v !== null
90+
);
91+
if (!uploadedFiles.length) return;
92+
6593
editor.update(() => {
66-
uploadedFiles.forEach(file => {
94+
uploadedFiles.forEach((file) => {
6795
const imageNode = $createImageNode(file, imageConverter);
68-
$getRoot().append(imageNode);
69-
})
96+
$getRoot().append(imageNode);
97+
});
7098
});
7199
}
72100

73101
replaceImagePlaceholders(controller: HtmlEditorController): void {
74102
const attachments = (() => {
75103
const value = controller.binding.getValue();
76-
if (value)
77-
return [...value.matchAll(/data-attachment-id="(\d+)"/g)].map(m => m[1]);
104+
if (value)
105+
return [...value.matchAll(/data-attachment-id="(\d+)"/g)].map(
106+
(m) => m[1]
107+
);
78108
return [];
79109
})();
80110

81-
if(!attachments.length) return;
82-
83-
const editorState = controller.editor.getEditorState();
84-
let hasUpdatedNodes = false
111+
if (!attachments.length) return;
112+
113+
const editorState = controller.editor.getEditorState();
114+
let hasUpdatedNodes = false;
115+
85116
controller.editor.update(() => {
86117
const nodes = Array.from(editorState._nodeMap.values());
87-
if(!nodes.some(v => isImagePlaceholderRegex(v.getTextContent()))) return;
88-
editorState._nodeMap.forEach((node) => {
118+
if (!nodes.some((v) => isImagePlaceholderRegex(v.getTextContent())))
119+
return;
120+
nodes.forEach((node) => {
121+
if (node.getType() === "text") {
89122
const text = node.getTextContent();
90-
91-
if(node.getType() === "text" && isImagePlaceholderRegex(text)) {
92-
const attachmentId = extractAttachmentId(text);
93-
if(attachmentId && !attachments.includes(attachmentId)) return;
94-
node.replace($createImageNode({ attachmentId } as object, this.imageConverter));
123+
const match = text.match(IMAGE_PLACEHOLDER_REGEX);
124+
125+
if (match) {
126+
const before = text.slice(0, match.index!);
127+
const after = text.slice(match.index! + match[0].length);
128+
const attachmentId = match[1];
129+
130+
const imageNode = $createImageNode({ attachmentId } as object, this.imageConverter);
131+
132+
// Replace the text node with the image node
133+
const replaced = node.replace(imageNode);
134+
135+
// Insert neighbors around the image
136+
if (before) replaced.insertBefore($createTextNode(before));
137+
if (after) replaced.insertAfter($createTextNode(after));
138+
95139
hasUpdatedNodes = true;
96140
}
141+
}
97142
});
98143
}, { discrete: true });
99144

100-
if(hasUpdatedNodes) controller.saveHtml();
145+
if (hasUpdatedNodes) controller.saveHtml();
101146
}
102147
}
103148

104-
export const IMAGE_PLACEHOLDER_REGEX: RegExp = /^\[IMAGE_(\d+)\]$/;
149+
export const IMAGE_PLACEHOLDER_REGEX: RegExp = /\[IMAGE_(\d+)\]/;
105150

106151
export function extractAttachmentId(text: string): string | null {
107152
const match = text.match(IMAGE_PLACEHOLDER_REGEX);
108-
return match ? match[1] : null
153+
return match ? match[1] : null;
109154
}
110155

111156
export function isImagePlaceholderRegex(text: string): boolean {
112157
return IMAGE_PLACEHOLDER_REGEX.test(text);
113158
}
114-
Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,71 @@
11
import { $isLinkNode, AutoLinkNode, LinkNode } from "@lexical/link";
2-
import { AutoLinkPlugin, LinkMatcher } from "@lexical/react/LexicalAutoLinkPlugin";
3-
import { $getSelection, $isRangeSelection, CLICK_COMMAND, COMMAND_PRIORITY_EDITOR } from "lexical";
2+
import {
3+
AutoLinkPlugin,
4+
LinkMatcher,
5+
} from "@lexical/react/LexicalAutoLinkPlugin";
6+
import {
7+
$getSelection,
8+
$isRangeSelection,
9+
CLICK_COMMAND,
10+
COMMAND_PRIORITY_EDITOR,
11+
} from "lexical";
412
import { HtmlEditorController } from "../../HtmlEditorController";
513
import { $findMatchingParent } from "../../Utils/node";
6-
import { ComponentAndProps, HtmlEditorExtension, LexicalConfigNode, OptionalCallback } from "../types";
14+
import {
15+
ComponentAndProps,
16+
HtmlEditorExtension,
17+
LexicalConfigNode,
18+
OptionalCallback,
19+
} from "../types";
720
import { urlRegExp } from "./helper";
821

9-
const MATCHERS: LinkMatcher[] = [(text: string) => {
10-
const match = urlRegExp.exec(text);
22+
const MATCHERS: LinkMatcher[] = [
23+
(text: string) => {
24+
const match = urlRegExp.exec(text);
1125

12-
if(match === null) return null;
26+
if (match === null) return null;
1327

14-
const [fullMatch] = match;
28+
const [fullMatch] = match;
1529

16-
return {
17-
index: match.index,
18-
length: fullMatch.length,
19-
text: fullMatch,
20-
url: fullMatch.startsWith("http") ? fullMatch : `https://${fullMatch}`
21-
}
22-
}];
30+
return {
31+
index: match.index,
32+
length: fullMatch.length,
33+
text: fullMatch,
34+
url: fullMatch.startsWith("http") ? fullMatch : `https://${fullMatch}`,
35+
};
36+
},
37+
];
2338

2439
export class AutoLinkExtension implements HtmlEditorExtension {
40+
name = "AutoLinkExtension";
41+
2542
getBuiltInComponent(): ComponentAndProps<typeof AutoLinkPlugin> {
26-
return { component: AutoLinkPlugin, props: { matchers: MATCHERS } }
43+
return { component: AutoLinkPlugin, props: { matchers: MATCHERS } };
2744
}
2845

2946
getNodes(): LexicalConfigNode {
30-
return [AutoLinkNode]
47+
return [AutoLinkNode];
3148
}
3249

3350
registerExtension(controller: HtmlEditorController): OptionalCallback {
34-
return controller.editor.registerCommand(CLICK_COMMAND, (event) => {
35-
if(!event.ctrlKey) return false;
36-
const selection = $getSelection();
37-
if(!$isRangeSelection(selection)) return false;
38-
const linkNode = $findMatchingParent(selection.anchor.getNode(), node => $isLinkNode(node));
39-
40-
if(linkNode) {
41-
window.open((linkNode as LinkNode).getURL(), "_blank");
42-
return true;
43-
}
44-
return false
45-
}, COMMAND_PRIORITY_EDITOR)
51+
return controller.editor.registerCommand(
52+
CLICK_COMMAND,
53+
(event) => {
54+
if (!event.ctrlKey) return false;
55+
const selection = $getSelection();
56+
if (!$isRangeSelection(selection)) return false;
57+
const linkNode = $findMatchingParent(
58+
selection.anchor.getNode(),
59+
(node) => $isLinkNode(node)
60+
);
61+
62+
if (linkNode) {
63+
window.open((linkNode as LinkNode).getURL(), "_blank");
64+
return true;
65+
}
66+
return false;
67+
},
68+
COMMAND_PRIORITY_EDITOR
69+
);
4670
}
4771
}

0 commit comments

Comments
 (0)