@sex-editor/slash 0.0.1 → 0.0.3-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +488 -418
- package/package.json +4 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 sexandviolence
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @sex-editor/slash
|
|
2
|
+
|
|
3
|
+
Hello World usage:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createSexEditor } from "@sex-editor/core";
|
|
7
|
+
import { registerSlashPlugin } from "@sex-editor/slash";
|
|
8
|
+
|
|
9
|
+
const editor = createSexEditor("editor", {
|
|
10
|
+
language: "en",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
registerSlashPlugin(editor);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Install:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i @sex-editor/slash
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @sex-editor/slash
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
License: MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -8,8 +8,8 @@ declare class SlashMenuPlugin {
|
|
|
8
8
|
private filteredItems;
|
|
9
9
|
private active;
|
|
10
10
|
private items;
|
|
11
|
+
private commandRange;
|
|
11
12
|
constructor(editor: SexEditor);
|
|
12
|
-
private getMenuItems;
|
|
13
13
|
register(): void;
|
|
14
14
|
private filterItems;
|
|
15
15
|
private showMenu;
|
|
@@ -18,6 +18,27 @@ declare class SlashMenuPlugin {
|
|
|
18
18
|
private updateMenuUI;
|
|
19
19
|
private executeSelection;
|
|
20
20
|
}
|
|
21
|
-
declare function registerSlashPlugin(editor: SexEditor): SlashMenuPlugin;
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
declare const registerSlashPlugin: (editor: SexEditor) => SlashMenuPlugin;
|
|
23
|
+
|
|
24
|
+
type SlashMenuGroup = "transform" | "insertInline" | "insertBlock";
|
|
25
|
+
interface SlashMenuItem {
|
|
26
|
+
key: string;
|
|
27
|
+
label: string;
|
|
28
|
+
icon: string;
|
|
29
|
+
keywords: string[];
|
|
30
|
+
group: SlashMenuGroup;
|
|
31
|
+
execute: (editor: SexEditor) => void;
|
|
32
|
+
}
|
|
33
|
+
type SlashCommandRange = {
|
|
34
|
+
startKey: string;
|
|
35
|
+
startOffset: number;
|
|
36
|
+
endKey: string;
|
|
37
|
+
endOffset: number;
|
|
38
|
+
};
|
|
39
|
+
type SlashMatch = {
|
|
40
|
+
query: string;
|
|
41
|
+
range: SlashCommandRange;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export { type SlashCommandRange, type SlashMatch, type SlashMenuGroup, type SlashMenuItem, SlashMenuPlugin, registerSlashPlugin };
|
package/dist/index.js
CHANGED
|
@@ -1,32 +1,36 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/plugin/SlashMenuPlugin.ts
|
|
2
2
|
import {
|
|
3
|
-
$
|
|
4
|
-
$
|
|
5
|
-
$
|
|
3
|
+
$createRangeSelection,
|
|
4
|
+
$getNodeByKey,
|
|
5
|
+
$getSelection as $getSelection2,
|
|
6
|
+
$isRangeSelection as $isRangeSelection2,
|
|
7
|
+
$isTextNode as $isTextNode2,
|
|
8
|
+
$setSelection,
|
|
6
9
|
COMMAND_PRIORITY_LOW,
|
|
7
10
|
KEY_ARROW_DOWN_COMMAND,
|
|
8
11
|
KEY_ARROW_UP_COMMAND,
|
|
9
12
|
KEY_ENTER_COMMAND,
|
|
10
|
-
KEY_ESCAPE_COMMAND
|
|
11
|
-
TextNode,
|
|
12
|
-
$insertNodes
|
|
13
|
+
KEY_ESCAPE_COMMAND
|
|
13
14
|
} from "lexical";
|
|
15
|
+
|
|
16
|
+
// src/constants/menuItems.ts
|
|
14
17
|
import {
|
|
15
|
-
$
|
|
16
|
-
$
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from "@lexical/
|
|
18
|
+
$createParagraphNode,
|
|
19
|
+
$getRoot,
|
|
20
|
+
$getSelection,
|
|
21
|
+
$isRangeSelection,
|
|
22
|
+
$insertNodes
|
|
23
|
+
} from "lexical";
|
|
24
|
+
import { $createHeadingNode, $createQuoteNode } from "@lexical/rich-text";
|
|
25
|
+
import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND } from "@lexical/list";
|
|
22
26
|
import { $createCodeNode } from "@lexical/code";
|
|
23
27
|
import {
|
|
28
|
+
$createAnchorNode,
|
|
24
29
|
$createHorizontalRuleNode,
|
|
25
|
-
$createPageBreakNode,
|
|
26
|
-
$createTableNode,
|
|
27
30
|
$createImageNode,
|
|
28
31
|
$createInlineImageNode,
|
|
29
|
-
$
|
|
32
|
+
$createPageBreakNode,
|
|
33
|
+
$createTableNode
|
|
30
34
|
} from "@sex-editor/core";
|
|
31
35
|
|
|
32
36
|
// ../ui/dist/index.js
|
|
@@ -731,408 +735,371 @@ ensureStyle(
|
|
|
731
735
|
`
|
|
732
736
|
);
|
|
733
737
|
|
|
734
|
-
// src/
|
|
735
|
-
var
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
738
|
+
// src/constants/menuItems.ts
|
|
739
|
+
var transformCurrentBlock = (createNode) => (editor) => {
|
|
740
|
+
editor.update(() => {
|
|
741
|
+
const selection = $getSelection();
|
|
742
|
+
if (!$isRangeSelection(selection)) return;
|
|
743
|
+
const anchor = selection.anchor.getNode();
|
|
744
|
+
const topLevel = anchor.getTopLevelElement();
|
|
745
|
+
const newNode = createNode();
|
|
746
|
+
if (!topLevel) {
|
|
747
|
+
$getRoot().append(newNode);
|
|
748
|
+
} else {
|
|
749
|
+
topLevel.replace(newNode);
|
|
750
|
+
newNode.append(...topLevel.getChildren());
|
|
751
|
+
}
|
|
752
|
+
newNode.select();
|
|
753
|
+
});
|
|
742
754
|
};
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
.
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
.
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
editor.update(() => {
|
|
874
|
-
const selection = $getSelection();
|
|
875
|
-
if ($isRangeSelection(selection)) {
|
|
876
|
-
$insertNodes([$createHeadingNode("h3")]);
|
|
877
|
-
}
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
},
|
|
881
|
-
{
|
|
882
|
-
key: "h4",
|
|
883
|
-
label: t("toolbar.block.h4") || "Heading 4",
|
|
884
|
-
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
|
|
885
|
-
keywords: ["h4", "heading4"],
|
|
886
|
-
execute: (editor) => {
|
|
887
|
-
editor.update(() => {
|
|
888
|
-
const selection = $getSelection();
|
|
889
|
-
if ($isRangeSelection(selection)) {
|
|
890
|
-
$insertNodes([$createHeadingNode("h4")]);
|
|
891
|
-
}
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
},
|
|
895
|
-
{
|
|
896
|
-
key: "h5",
|
|
897
|
-
label: t("toolbar.block.h5") || "Heading 5",
|
|
898
|
-
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
|
|
899
|
-
keywords: ["h5", "heading5"],
|
|
900
|
-
execute: (editor) => {
|
|
901
|
-
editor.update(() => {
|
|
902
|
-
const selection = $getSelection();
|
|
903
|
-
if ($isRangeSelection(selection)) {
|
|
904
|
-
$insertNodes([$createHeadingNode("h5")]);
|
|
905
|
-
}
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
},
|
|
909
|
-
{
|
|
910
|
-
key: "h6",
|
|
911
|
-
label: t("toolbar.block.h6") || "Heading 6",
|
|
912
|
-
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
|
|
913
|
-
keywords: ["h6", "heading6"],
|
|
914
|
-
execute: (editor) => {
|
|
915
|
-
editor.update(() => {
|
|
916
|
-
const selection = $getSelection();
|
|
917
|
-
if ($isRangeSelection(selection)) {
|
|
918
|
-
$insertNodes([$createHeadingNode("h6")]);
|
|
919
|
-
}
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
},
|
|
923
|
-
{
|
|
924
|
-
key: "ul",
|
|
925
|
-
label: t("toolbar.block.bullet") || "Bullet List",
|
|
926
|
-
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`,
|
|
927
|
-
keywords: ["ul", "list", "bullet"],
|
|
928
|
-
execute: (editor) => {
|
|
929
|
-
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0);
|
|
930
|
-
}
|
|
931
|
-
},
|
|
932
|
-
{
|
|
933
|
-
key: "ol",
|
|
934
|
-
label: t("toolbar.block.number") || "Numbered List",
|
|
935
|
-
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg>`,
|
|
936
|
-
keywords: ["ol", "list", "number", "ordered"],
|
|
937
|
-
execute: (editor) => {
|
|
938
|
-
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0);
|
|
939
|
-
}
|
|
940
|
-
},
|
|
941
|
-
{
|
|
942
|
-
key: "code",
|
|
943
|
-
label: t("toolbar.block.code") || "Code Block",
|
|
944
|
-
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>`,
|
|
945
|
-
keywords: ["code", "block", "javascript", "ts"],
|
|
946
|
-
execute: (editor) => {
|
|
947
|
-
editor.update(() => {
|
|
948
|
-
const selection = $getSelection();
|
|
949
|
-
if ($isRangeSelection(selection)) {
|
|
950
|
-
$insertNodes([$createCodeNode()]);
|
|
951
|
-
}
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
},
|
|
955
|
-
{
|
|
956
|
-
key: "quote",
|
|
957
|
-
label: t("toolbar.block.quote") || "Quote",
|
|
958
|
-
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>`,
|
|
959
|
-
keywords: ["quote", "blockquote"],
|
|
960
|
-
execute: (editor) => {
|
|
961
|
-
editor.update(() => {
|
|
962
|
-
const selection = $getSelection();
|
|
963
|
-
if ($isRangeSelection(selection)) {
|
|
964
|
-
$insertNodes([$createQuoteNode()]);
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
key: "image",
|
|
971
|
-
label: t("toolbar.image") || "Image",
|
|
972
|
-
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`,
|
|
973
|
-
keywords: ["image", "photo", "picture"],
|
|
974
|
-
execute: (editor) => {
|
|
975
|
-
const dialog = new ImageInsertDialog({
|
|
976
|
-
t: editor.t,
|
|
977
|
-
onSubmit: async (data) => {
|
|
978
|
-
try {
|
|
979
|
-
let src = data.src || "";
|
|
980
|
-
const meta = data.type === "file" && data.file ? await Promise.resolve(editor._imageMetaResolver?.(data.file)).catch(() => void 0) : void 0;
|
|
981
|
-
if (data.type === "file" && data.file) {
|
|
982
|
-
const file = data.file;
|
|
983
|
-
const strategy = editor._imageStorageStrategy ?? "auto";
|
|
984
|
-
const shouldBase64 = strategy === "base64" || strategy === "auto" && !editor._imageUploadHandler;
|
|
985
|
-
if (!shouldBase64) {
|
|
986
|
-
if (!editor._imageUploadHandler) {
|
|
987
|
-
throw new Error("imageUploadHandler is required when imageStorage is 'upload'");
|
|
988
|
-
}
|
|
989
|
-
src = await editor._imageUploadHandler(file);
|
|
990
|
-
} else {
|
|
991
|
-
src = await new Promise((resolve) => {
|
|
992
|
-
const reader = new FileReader();
|
|
993
|
-
reader.onload = () => resolve(reader.result);
|
|
994
|
-
reader.readAsDataURL(file);
|
|
995
|
-
});
|
|
755
|
+
var createSlashMenuItems = (editor) => {
|
|
756
|
+
const t = editor.t;
|
|
757
|
+
return [
|
|
758
|
+
{
|
|
759
|
+
key: "paragraph",
|
|
760
|
+
label: t("toolbar.block.paragraph") || "Paragraph",
|
|
761
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M14 17H4v2h10v-2zm0-8H4v2h10V9zM4 15h16v-2H4v2zM4 5v2h16V5H4z"/></svg>`,
|
|
762
|
+
keywords: ["p", "paragraph", "text"],
|
|
763
|
+
group: "transform",
|
|
764
|
+
execute: transformCurrentBlock(() => $createParagraphNode())
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
key: "anchor",
|
|
768
|
+
label: t("toolbar.anchor") || "Anchor",
|
|
769
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>`,
|
|
770
|
+
keywords: ["anchor", "bookmark", "id"],
|
|
771
|
+
group: "insertInline",
|
|
772
|
+
execute: (editor2) => {
|
|
773
|
+
const dialog = new AnchorInsertDialog({
|
|
774
|
+
t: (key) => editor2.t ? editor2.t(key) : key,
|
|
775
|
+
onSubmit: (data) => {
|
|
776
|
+
editor2.update(() => {
|
|
777
|
+
const node = $createAnchorNode(data.id);
|
|
778
|
+
$insertNodes([node]);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
dialog.show();
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
key: "h1",
|
|
787
|
+
label: t("toolbar.block.h1") || "Heading 1",
|
|
788
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
|
|
789
|
+
keywords: ["h1", "heading1", "title"],
|
|
790
|
+
group: "transform",
|
|
791
|
+
execute: transformCurrentBlock(() => $createHeadingNode("h1"))
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
key: "h2",
|
|
795
|
+
label: t("toolbar.block.h2") || "Heading 2",
|
|
796
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
|
|
797
|
+
keywords: ["h2", "heading2", "subtitle"],
|
|
798
|
+
group: "transform",
|
|
799
|
+
execute: transformCurrentBlock(() => $createHeadingNode("h2"))
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
key: "h3",
|
|
803
|
+
label: t("toolbar.block.h3") || "Heading 3",
|
|
804
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
|
|
805
|
+
keywords: ["h3", "heading3"],
|
|
806
|
+
group: "transform",
|
|
807
|
+
execute: transformCurrentBlock(() => $createHeadingNode("h3"))
|
|
808
|
+
},
|
|
809
|
+
{
|
|
810
|
+
key: "h4",
|
|
811
|
+
label: t("toolbar.block.h4") || "Heading 4",
|
|
812
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
|
|
813
|
+
keywords: ["h4", "heading4"],
|
|
814
|
+
group: "transform",
|
|
815
|
+
execute: transformCurrentBlock(() => $createHeadingNode("h4"))
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
key: "h5",
|
|
819
|
+
label: t("toolbar.block.h5") || "Heading 5",
|
|
820
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
|
|
821
|
+
keywords: ["h5", "heading5"],
|
|
822
|
+
group: "transform",
|
|
823
|
+
execute: transformCurrentBlock(() => $createHeadingNode("h5"))
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
key: "h6",
|
|
827
|
+
label: t("toolbar.block.h6") || "Heading 6",
|
|
828
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
|
|
829
|
+
keywords: ["h6", "heading6"],
|
|
830
|
+
group: "transform",
|
|
831
|
+
execute: transformCurrentBlock(() => $createHeadingNode("h6"))
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
key: "ul",
|
|
835
|
+
label: t("toolbar.block.bullet") || "Bullet List",
|
|
836
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`,
|
|
837
|
+
keywords: ["ul", "list", "bullet"],
|
|
838
|
+
group: "transform",
|
|
839
|
+
execute: (editor2) => editor2.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0)
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
key: "ol",
|
|
843
|
+
label: t("toolbar.block.number") || "Numbered List",
|
|
844
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg>`,
|
|
845
|
+
keywords: ["ol", "list", "number", "ordered"],
|
|
846
|
+
group: "transform",
|
|
847
|
+
execute: (editor2) => editor2.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0)
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
key: "code",
|
|
851
|
+
label: t("toolbar.block.code") || "Code Block",
|
|
852
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>`,
|
|
853
|
+
keywords: ["code", "block", "javascript", "ts"],
|
|
854
|
+
group: "transform",
|
|
855
|
+
execute: transformCurrentBlock(() => $createCodeNode())
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
key: "quote",
|
|
859
|
+
label: t("toolbar.block.quote") || "Quote",
|
|
860
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>`,
|
|
861
|
+
keywords: ["quote", "blockquote"],
|
|
862
|
+
group: "transform",
|
|
863
|
+
execute: transformCurrentBlock(() => $createQuoteNode())
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
key: "image",
|
|
867
|
+
label: t("toolbar.image") || "Image",
|
|
868
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`,
|
|
869
|
+
keywords: ["image", "photo", "picture"],
|
|
870
|
+
group: "insertBlock",
|
|
871
|
+
execute: (editor2) => {
|
|
872
|
+
const dialog = new ImageInsertDialog({
|
|
873
|
+
t: editor2.t,
|
|
874
|
+
onSubmit: async (data) => {
|
|
875
|
+
try {
|
|
876
|
+
let src = data.src || "";
|
|
877
|
+
const meta = data.type === "file" && data.file ? await Promise.resolve(editor2._imageMetaResolver?.(data.file)).catch(() => void 0) : void 0;
|
|
878
|
+
if (data.type === "file" && data.file) {
|
|
879
|
+
const file = data.file;
|
|
880
|
+
const strategy = editor2._imageStorageStrategy ?? "auto";
|
|
881
|
+
const shouldBase64 = strategy === "base64" || strategy === "auto" && !editor2._imageUploadHandler;
|
|
882
|
+
if (!shouldBase64) {
|
|
883
|
+
if (!editor2._imageUploadHandler) {
|
|
884
|
+
throw new Error("imageUploadHandler is required when imageStorage is 'upload'");
|
|
996
885
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
maxWidth: 500,
|
|
1004
|
-
meta: meta || void 0
|
|
1005
|
-
});
|
|
1006
|
-
$insertNodes([node]);
|
|
886
|
+
src = await editor2._imageUploadHandler(file);
|
|
887
|
+
} else {
|
|
888
|
+
src = await new Promise((resolve) => {
|
|
889
|
+
const reader = new FileReader();
|
|
890
|
+
reader.onload = () => resolve(reader.result);
|
|
891
|
+
reader.readAsDataURL(file);
|
|
1007
892
|
});
|
|
1008
893
|
}
|
|
1009
|
-
} catch (e) {
|
|
1010
|
-
console.error("Image insert failed", e);
|
|
1011
894
|
}
|
|
895
|
+
if (src) {
|
|
896
|
+
editor2.update(() => {
|
|
897
|
+
const node = $createImageNode({
|
|
898
|
+
src,
|
|
899
|
+
altText: data.alt,
|
|
900
|
+
maxWidth: 500,
|
|
901
|
+
meta: meta || void 0
|
|
902
|
+
});
|
|
903
|
+
$insertNodes([node]);
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
} catch (e) {
|
|
907
|
+
console.error("Image insert failed", e);
|
|
1012
908
|
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
} else {
|
|
1039
|
-
src = await new Promise((resolve) => {
|
|
1040
|
-
const reader = new FileReader();
|
|
1041
|
-
reader.onload = () => resolve(reader.result);
|
|
1042
|
-
reader.readAsDataURL(file);
|
|
1043
|
-
});
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
dialog.show();
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
key: "inline-image",
|
|
916
|
+
label: t("toolbar.inlineImage") || "Inline Image",
|
|
917
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`,
|
|
918
|
+
keywords: ["inline-image", "inline", "image", "img"],
|
|
919
|
+
group: "insertInline",
|
|
920
|
+
execute: (editor2) => {
|
|
921
|
+
const dialog = new ImageInsertDialog({
|
|
922
|
+
t: editor2.t,
|
|
923
|
+
onSubmit: async (data) => {
|
|
924
|
+
try {
|
|
925
|
+
let src = data.src || "";
|
|
926
|
+
const meta = data.type === "file" && data.file ? await Promise.resolve(editor2._imageMetaResolver?.(data.file)).catch(() => void 0) : void 0;
|
|
927
|
+
if (data.type === "file" && data.file) {
|
|
928
|
+
const file = data.file;
|
|
929
|
+
const strategy = editor2._imageStorageStrategy ?? "auto";
|
|
930
|
+
const shouldBase64 = strategy === "base64" || strategy === "auto" && !editor2._imageUploadHandler;
|
|
931
|
+
if (!shouldBase64) {
|
|
932
|
+
if (!editor2._imageUploadHandler) {
|
|
933
|
+
throw new Error("imageUploadHandler is required when imageStorage is 'upload'");
|
|
1044
934
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
maxWidth: 500,
|
|
1052
|
-
width: 100,
|
|
1053
|
-
meta: meta || void 0
|
|
1054
|
-
});
|
|
1055
|
-
$insertNodes([node]);
|
|
935
|
+
src = await editor2._imageUploadHandler(file);
|
|
936
|
+
} else {
|
|
937
|
+
src = await new Promise((resolve) => {
|
|
938
|
+
const reader = new FileReader();
|
|
939
|
+
reader.onload = () => resolve(reader.result);
|
|
940
|
+
reader.readAsDataURL(file);
|
|
1056
941
|
});
|
|
1057
942
|
}
|
|
1058
|
-
} catch (e) {
|
|
1059
|
-
console.error("Inline image insert failed", e);
|
|
1060
943
|
}
|
|
944
|
+
if (src) {
|
|
945
|
+
editor2.update(() => {
|
|
946
|
+
const node = $createInlineImageNode({
|
|
947
|
+
src,
|
|
948
|
+
altText: data.alt,
|
|
949
|
+
maxWidth: 500,
|
|
950
|
+
width: 100,
|
|
951
|
+
meta: meta || void 0
|
|
952
|
+
});
|
|
953
|
+
$insertNodes([node]);
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
} catch (e) {
|
|
957
|
+
console.error("Inline image insert failed", e);
|
|
1061
958
|
}
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
}
|
|
1097
|
-
},
|
|
1098
|
-
{
|
|
1099
|
-
key: "page-break",
|
|
1100
|
-
label: t("toolbar.pageBreak") || "Page Break",
|
|
1101
|
-
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9 13h6v-2h-6v2zm-2 2H5v-2h2v2zm-2-4H3v2h2v-2zm0 8h2v-2H5v2zm14-2v-2h-2v2h2zm-2 4h2v-2h-2v2zM6 7H4v2h2V7zm14 0v2h-2V7h2zm-8-2h6v2h-6V5zM8 7H6v2h2V7zm8 2v2h2V9h-2zm-4 4h2v-2h-2v2zm-4 0h2v-2H8v2zm4 4h2v-2h-2v2z"/></svg>`,
|
|
1102
|
-
keywords: ["page", "break", "split"],
|
|
1103
|
-
execute: (editor) => {
|
|
1104
|
-
editor.update(() => {
|
|
1105
|
-
const selection = $getSelection();
|
|
1106
|
-
if ($isRangeSelection(selection)) {
|
|
1107
|
-
$insertNodes([$createPageBreakNode()]);
|
|
1108
|
-
}
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
dialog.show();
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
key: "table",
|
|
966
|
+
label: t("toolbar.insertTable") || "Table",
|
|
967
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM8 20H4v-4h4v4zm0-6H4v-4h4v4zm0-6H4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4z"/></svg>`,
|
|
968
|
+
keywords: ["table", "grid"],
|
|
969
|
+
group: "insertBlock",
|
|
970
|
+
execute: (editor2) => {
|
|
971
|
+
const dialog = new TableInsertDialog({
|
|
972
|
+
t,
|
|
973
|
+
onSubmit: (rows, cols) => {
|
|
974
|
+
editor2.update(() => {
|
|
975
|
+
const node = $createTableNode(rows, cols);
|
|
976
|
+
$insertNodes([node]);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
dialog.show();
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
key: "hr",
|
|
985
|
+
label: t("toolbar.horizontalRule") || "Horizontal Rule",
|
|
986
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M4 11h16v2H4z"/></svg>`,
|
|
987
|
+
keywords: ["hr", "line", "separator"],
|
|
988
|
+
group: "insertBlock",
|
|
989
|
+
execute: (editor2) => {
|
|
990
|
+
editor2.update(() => {
|
|
991
|
+
const selection = $getSelection();
|
|
992
|
+
if ($isRangeSelection(selection)) $insertNodes([$createHorizontalRuleNode()]);
|
|
993
|
+
});
|
|
1111
994
|
}
|
|
1112
|
-
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
key: "page-break",
|
|
998
|
+
label: t("toolbar.pageBreak") || "Page Break",
|
|
999
|
+
icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9 13h6v-2h-6v2zm-2 2H5v-2h2v2zm-2-4H3v2h2v-2zm0 8h2v-2H5v2zm14-2v-2h-2v2h2zm-2 4h2v-2h-2v2zM6 7H4v2h2V7zm14 0v2h-2V7h2zm-8-2h6v2h-6V5zM8 7H6v2h2V7zm8 2v2h2V9h-2zm-4 4h2v-2h-2v2zm-4 0h2v-2H8v2zm4 4h2v-2h-2v2z"/></svg>`,
|
|
1000
|
+
keywords: ["page", "break", "split"],
|
|
1001
|
+
group: "insertBlock",
|
|
1002
|
+
execute: (editor2) => {
|
|
1003
|
+
editor2.update(() => {
|
|
1004
|
+
const selection = $getSelection();
|
|
1005
|
+
if ($isRangeSelection(selection)) $insertNodes([$createPageBreakNode()]);
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
];
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
// src/utils/getSlashMatch.ts
|
|
1013
|
+
import {
|
|
1014
|
+
$isElementNode,
|
|
1015
|
+
$isTextNode
|
|
1016
|
+
} from "lexical";
|
|
1017
|
+
var collectTextNodes = (node, out) => {
|
|
1018
|
+
if ($isTextNode(node)) {
|
|
1019
|
+
out.push(node);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if ($isElementNode(node)) {
|
|
1023
|
+
const children = node.getChildren();
|
|
1024
|
+
for (const child of children) collectTextNodes(child, out);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
var getSlashMatch = (selection) => {
|
|
1028
|
+
const anchorNode = selection.anchor.getNode();
|
|
1029
|
+
if (!$isTextNode(anchorNode)) return null;
|
|
1030
|
+
const block = anchorNode.getTopLevelElement();
|
|
1031
|
+
if (!block) return null;
|
|
1032
|
+
const textNodes = [];
|
|
1033
|
+
collectTextNodes(block, textNodes);
|
|
1034
|
+
if (textNodes.length === 0) return null;
|
|
1035
|
+
let cursorIndex = null;
|
|
1036
|
+
let running = 0;
|
|
1037
|
+
for (const n of textNodes) {
|
|
1038
|
+
if (n.getKey() === anchorNode.getKey()) {
|
|
1039
|
+
cursorIndex = running + selection.anchor.offset;
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
running += n.getTextContentSize();
|
|
1043
|
+
}
|
|
1044
|
+
if (cursorIndex === null) return null;
|
|
1045
|
+
const blockText = block.getTextContent();
|
|
1046
|
+
const beforeCursor = blockText.slice(0, cursorIndex);
|
|
1047
|
+
const slashIndex = beforeCursor.lastIndexOf("/");
|
|
1048
|
+
if (slashIndex === -1) return null;
|
|
1049
|
+
const query = beforeCursor.slice(slashIndex + 1);
|
|
1050
|
+
if (/\s/.test(query)) return null;
|
|
1051
|
+
const indexToPoint = (index) => {
|
|
1052
|
+
let remaining = index;
|
|
1053
|
+
for (const n of textNodes) {
|
|
1054
|
+
const size = n.getTextContentSize();
|
|
1055
|
+
if (remaining <= size) return { key: n.getKey(), offset: remaining };
|
|
1056
|
+
remaining -= size;
|
|
1057
|
+
}
|
|
1058
|
+
const last = textNodes[textNodes.length - 1];
|
|
1059
|
+
return { key: last.getKey(), offset: last.getTextContentSize() };
|
|
1060
|
+
};
|
|
1061
|
+
const start = indexToPoint(slashIndex);
|
|
1062
|
+
const end = indexToPoint(cursorIndex);
|
|
1063
|
+
if (!start || !end) return null;
|
|
1064
|
+
return {
|
|
1065
|
+
query,
|
|
1066
|
+
range: {
|
|
1067
|
+
startKey: start.key,
|
|
1068
|
+
startOffset: start.offset,
|
|
1069
|
+
endKey: end.key,
|
|
1070
|
+
endOffset: end.offset
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
// src/plugin/SlashMenuPlugin.ts
|
|
1076
|
+
var SlashMenuPlugin = class {
|
|
1077
|
+
constructor(editor) {
|
|
1078
|
+
this.menuElement = null;
|
|
1079
|
+
this.selectedIndex = 0;
|
|
1080
|
+
this.queryString = null;
|
|
1081
|
+
this.filteredItems = [];
|
|
1082
|
+
this.active = false;
|
|
1083
|
+
this.commandRange = null;
|
|
1084
|
+
this.editor = editor;
|
|
1085
|
+
this.items = createSlashMenuItems(editor);
|
|
1113
1086
|
}
|
|
1114
1087
|
register() {
|
|
1115
1088
|
this.editor.registerUpdateListener(({ editorState }) => {
|
|
1116
1089
|
editorState.read(() => {
|
|
1117
|
-
const selection = $
|
|
1118
|
-
if (!$
|
|
1090
|
+
const selection = $getSelection2();
|
|
1091
|
+
if (!$isRangeSelection2(selection) || !selection.isCollapsed()) {
|
|
1119
1092
|
this.hideMenu();
|
|
1120
1093
|
return;
|
|
1121
1094
|
}
|
|
1122
|
-
const
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
const block = node.getTopLevelElementOrThrow();
|
|
1127
|
-
const blockText = block.getTextContent();
|
|
1128
|
-
if (blockText.startsWith("/") && selection.anchor.offset <= blockText.length) {
|
|
1129
|
-
const query = blockText.substring(1);
|
|
1130
|
-
this.queryString = query;
|
|
1131
|
-
this.showMenu(selection);
|
|
1132
|
-
return;
|
|
1133
|
-
}
|
|
1095
|
+
const match = getSlashMatch(selection);
|
|
1096
|
+
if (!match) {
|
|
1097
|
+
this.hideMenu();
|
|
1098
|
+
return;
|
|
1134
1099
|
}
|
|
1135
|
-
this.
|
|
1100
|
+
this.queryString = match.query;
|
|
1101
|
+
this.commandRange = match.range;
|
|
1102
|
+
this.showMenu(selection);
|
|
1136
1103
|
});
|
|
1137
1104
|
});
|
|
1138
1105
|
this.editor.registerCommand(
|
|
@@ -1193,6 +1160,16 @@ var SlashMenuPlugin = class {
|
|
|
1193
1160
|
this.filteredItems = this.items.filter(
|
|
1194
1161
|
(item) => item.label.toLowerCase().includes(q) || item.keywords.some((k) => k.toLowerCase().includes(q))
|
|
1195
1162
|
);
|
|
1163
|
+
const groupOrder = ["transform", "insertInline", "insertBlock"];
|
|
1164
|
+
const groupIndex = (g) => groupOrder.indexOf(g);
|
|
1165
|
+
const keyOrder = /* @__PURE__ */ new Map();
|
|
1166
|
+
this.items.forEach((it, idx) => keyOrder.set(it.key, idx));
|
|
1167
|
+
this.filteredItems.sort((a, b) => {
|
|
1168
|
+
const ga = groupIndex(a.group);
|
|
1169
|
+
const gb = groupIndex(b.group);
|
|
1170
|
+
if (ga !== gb) return ga - gb;
|
|
1171
|
+
return (keyOrder.get(a.key) ?? 0) - (keyOrder.get(b.key) ?? 0);
|
|
1172
|
+
});
|
|
1196
1173
|
}
|
|
1197
1174
|
showMenu(selection) {
|
|
1198
1175
|
this.active = true;
|
|
@@ -1201,6 +1178,9 @@ var SlashMenuPlugin = class {
|
|
|
1201
1178
|
this.hideMenu();
|
|
1202
1179
|
return;
|
|
1203
1180
|
}
|
|
1181
|
+
if (this.selectedIndex >= this.filteredItems.length) {
|
|
1182
|
+
this.selectedIndex = 0;
|
|
1183
|
+
}
|
|
1204
1184
|
if (!this.menuElement) {
|
|
1205
1185
|
this.menuElement = document.createElement("div");
|
|
1206
1186
|
this.menuElement.className = "sex-slash-menu";
|
|
@@ -1208,7 +1188,21 @@ var SlashMenuPlugin = class {
|
|
|
1208
1188
|
}
|
|
1209
1189
|
this.menuElement.innerHTML = "";
|
|
1210
1190
|
this.menuElement.style.display = "block";
|
|
1191
|
+
const translate = (key) => this.editor.t?.(key) ?? key;
|
|
1192
|
+
const groupLabels = {
|
|
1193
|
+
transform: translate("slash.group.transform"),
|
|
1194
|
+
insertInline: translate("slash.group.insertInline"),
|
|
1195
|
+
insertBlock: translate("slash.group.insertBlock")
|
|
1196
|
+
};
|
|
1197
|
+
let lastGroup = null;
|
|
1211
1198
|
this.filteredItems.forEach((item, index) => {
|
|
1199
|
+
if (item.group !== lastGroup) {
|
|
1200
|
+
lastGroup = item.group;
|
|
1201
|
+
const groupEl = document.createElement("div");
|
|
1202
|
+
groupEl.className = "sex-slash-menu-group";
|
|
1203
|
+
groupEl.textContent = groupLabels[item.group];
|
|
1204
|
+
this.menuElement?.appendChild(groupEl);
|
|
1205
|
+
}
|
|
1212
1206
|
const el = document.createElement("div");
|
|
1213
1207
|
el.className = `sex-slash-menu-item ${index === this.selectedIndex ? "selected" : ""}`;
|
|
1214
1208
|
el.innerHTML = `
|
|
@@ -1235,9 +1229,8 @@ var SlashMenuPlugin = class {
|
|
|
1235
1229
|
this.active = false;
|
|
1236
1230
|
this.queryString = null;
|
|
1237
1231
|
this.selectedIndex = 0;
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
}
|
|
1232
|
+
this.commandRange = null;
|
|
1233
|
+
if (this.menuElement) this.menuElement.style.display = "none";
|
|
1241
1234
|
}
|
|
1242
1235
|
moveSelection(delta) {
|
|
1243
1236
|
this.selectedIndex += delta;
|
|
@@ -1262,31 +1255,108 @@ var SlashMenuPlugin = class {
|
|
|
1262
1255
|
}
|
|
1263
1256
|
executeSelection() {
|
|
1264
1257
|
const item = this.filteredItems[this.selectedIndex];
|
|
1265
|
-
if (item)
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
if (
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1258
|
+
if (!item) return;
|
|
1259
|
+
const commandRange = this.commandRange;
|
|
1260
|
+
this.hideMenu();
|
|
1261
|
+
this.editor.update(
|
|
1262
|
+
() => {
|
|
1263
|
+
if (!commandRange) return;
|
|
1264
|
+
const startNode = $getNodeByKey(commandRange.startKey);
|
|
1265
|
+
const endNode = $getNodeByKey(commandRange.endKey);
|
|
1266
|
+
if (!startNode || !endNode) return;
|
|
1267
|
+
if (!$isTextNode2(startNode) || !$isTextNode2(endNode)) return;
|
|
1268
|
+
const rangeSelection = $createRangeSelection();
|
|
1269
|
+
rangeSelection.setTextNodeRange(
|
|
1270
|
+
startNode,
|
|
1271
|
+
commandRange.startOffset,
|
|
1272
|
+
endNode,
|
|
1273
|
+
commandRange.endOffset
|
|
1274
|
+
);
|
|
1275
|
+
$setSelection(rangeSelection);
|
|
1276
|
+
rangeSelection.removeText();
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1278
1279
|
onUpdate: () => {
|
|
1279
1280
|
item.execute(this.editor);
|
|
1280
1281
|
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1282
|
+
}
|
|
1283
|
+
);
|
|
1283
1284
|
}
|
|
1284
1285
|
};
|
|
1285
|
-
|
|
1286
|
+
|
|
1287
|
+
// src/plugin/registerSlashPlugin.ts
|
|
1288
|
+
var registerSlashPlugin = (editor) => {
|
|
1286
1289
|
const plugin = new SlashMenuPlugin(editor);
|
|
1287
1290
|
plugin.register();
|
|
1288
1291
|
return plugin;
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
// src/index.ts
|
|
1295
|
+
var ensureStyle2 = (id, cssText) => {
|
|
1296
|
+
if (typeof document === "undefined") return;
|
|
1297
|
+
if (document.getElementById(id)) return;
|
|
1298
|
+
const style = document.createElement("style");
|
|
1299
|
+
style.id = id;
|
|
1300
|
+
style.textContent = cssText;
|
|
1301
|
+
(document.head ?? document.documentElement).appendChild(style);
|
|
1302
|
+
};
|
|
1303
|
+
ensureStyle2(
|
|
1304
|
+
"sex-slash-styles",
|
|
1305
|
+
`.sex-slash-menu {
|
|
1306
|
+
position: absolute;
|
|
1307
|
+
z-index: 100;
|
|
1308
|
+
background: white;
|
|
1309
|
+
border: 1px solid #e5e7eb;
|
|
1310
|
+
border-radius: 8px;
|
|
1311
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
1312
|
+
width: 280px;
|
|
1313
|
+
max-height: 300px;
|
|
1314
|
+
overflow-y: auto;
|
|
1315
|
+
padding: 4px;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.sex-slash-menu-item {
|
|
1319
|
+
display: flex;
|
|
1320
|
+
align-items: center;
|
|
1321
|
+
padding: 8px 12px;
|
|
1322
|
+
cursor: pointer;
|
|
1323
|
+
border-radius: 4px;
|
|
1324
|
+
font-size: 14px;
|
|
1325
|
+
color: #374151;
|
|
1326
|
+
transition: background-color 0.2s;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
.sex-slash-menu-item:hover,
|
|
1330
|
+
.sex-slash-menu-item.selected {
|
|
1331
|
+
background-color: #f3f4f6;
|
|
1289
1332
|
}
|
|
1333
|
+
|
|
1334
|
+
.sex-slash-menu-icon {
|
|
1335
|
+
margin-right: 12px;
|
|
1336
|
+
width: 20px;
|
|
1337
|
+
height: 20px;
|
|
1338
|
+
display: flex;
|
|
1339
|
+
align-items: center;
|
|
1340
|
+
justify-content: center;
|
|
1341
|
+
color: #6b7280;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
.sex-slash-menu-label {
|
|
1345
|
+
flex: 1;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
.sex-slash-menu-group {
|
|
1349
|
+
padding: 6px 12px;
|
|
1350
|
+
font-size: 12px;
|
|
1351
|
+
color: #6b7280;
|
|
1352
|
+
user-select: none;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
.sex-slash-menu-hidden {
|
|
1356
|
+
display: none;
|
|
1357
|
+
}
|
|
1358
|
+
`
|
|
1359
|
+
);
|
|
1290
1360
|
export {
|
|
1291
1361
|
SlashMenuPlugin,
|
|
1292
1362
|
registerSlashPlugin
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sex-editor/slash",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3-dev.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"tsup": "8.3.5",
|
|
17
17
|
"typescript": "^5.6.3",
|
|
18
|
-
"@sex-editor/
|
|
19
|
-
"@sex-editor/
|
|
18
|
+
"@sex-editor/ui": "0.0.1",
|
|
19
|
+
"@sex-editor/core": "0.0.3-dev.0"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"@sex-editor/core": ">=0.0.2 <0.0.3",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"keywords": [],
|
|
37
37
|
"author": "",
|
|
38
|
-
"license": "
|
|
38
|
+
"license": "MIT",
|
|
39
39
|
"scripts": {
|
|
40
40
|
"dev": "tsup --watch",
|
|
41
41
|
"build": "tsup"
|