@blankdotpage/cake 0.1.37 → 0.1.39
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/dist/cake/core/runtime.d.ts.map +1 -1
- package/dist/cake/core/runtime.js +53 -20
- package/dist/cake/editor/cake-editor.d.ts +43 -2
- package/dist/cake/editor/cake-editor.d.ts.map +1 -1
- package/dist/cake/editor/cake-editor.js +155 -29
- package/dist/cake/extensions/index.d.ts +2 -1
- package/dist/cake/extensions/index.d.ts.map +1 -1
- package/dist/cake/extensions/index.js +2 -1
- package/dist/cake/extensions/link/link-popover.d.ts.map +1 -1
- package/dist/cake/extensions/link/link-popover.js +3 -11
- package/dist/cake/extensions/mention/index.d.ts +2 -0
- package/dist/cake/extensions/mention/index.d.ts.map +1 -0
- package/dist/cake/extensions/mention/index.js +1 -0
- package/dist/cake/extensions/mention/mention.d.ts +61 -0
- package/dist/cake/extensions/mention/mention.d.ts.map +1 -0
- package/dist/cake/extensions/mention/mention.js +460 -0
- package/dist/cake/react/index.d.ts +2 -0
- package/dist/cake/react/index.d.ts.map +1 -1
- package/dist/cake/react/index.js +70 -10
- package/dist/cake/test/harness.d.ts.map +1 -1
- package/dist/cake/test/harness.js +5 -1
- package/package.json +2 -1
|
@@ -32,20 +32,11 @@ function cx(...parts) {
|
|
|
32
32
|
return parts.filter(Boolean).join(" ");
|
|
33
33
|
}
|
|
34
34
|
export function CakeLinkPopover({ editor, styles, }) {
|
|
35
|
-
const container = editor.getContainer();
|
|
36
35
|
const contentRoot = editor.getContentRoot();
|
|
37
36
|
if (!contentRoot) {
|
|
38
37
|
return null;
|
|
39
38
|
}
|
|
40
|
-
const toOverlayRect = useCallback((rect) =>
|
|
41
|
-
const containerRect = container.getBoundingClientRect();
|
|
42
|
-
return {
|
|
43
|
-
top: rect.top - containerRect.top,
|
|
44
|
-
left: rect.left - containerRect.left,
|
|
45
|
-
width: rect.width,
|
|
46
|
-
height: rect.height,
|
|
47
|
-
};
|
|
48
|
-
}, [container]);
|
|
39
|
+
const toOverlayRect = useCallback((rect) => editor.toOverlayRect(rect), [editor]);
|
|
49
40
|
const getSelection = useCallback(() => {
|
|
50
41
|
const selection = editor.getSelection();
|
|
51
42
|
const focus = selection.start === selection.end
|
|
@@ -200,13 +191,14 @@ export function CakeLinkPopover({ editor, styles, }) {
|
|
|
200
191
|
if (state.status !== "open") {
|
|
201
192
|
return;
|
|
202
193
|
}
|
|
194
|
+
const container = editor.getContainer();
|
|
203
195
|
container.addEventListener("scroll", close, { passive: true });
|
|
204
196
|
window.addEventListener("resize", reposition);
|
|
205
197
|
return () => {
|
|
206
198
|
container.removeEventListener("scroll", close);
|
|
207
199
|
window.removeEventListener("resize", reposition);
|
|
208
200
|
};
|
|
209
|
-
}, [close,
|
|
201
|
+
}, [close, editor, reposition, state.status]);
|
|
210
202
|
const handleMouseDown = useCallback((event) => {
|
|
211
203
|
event.stopPropagation();
|
|
212
204
|
if (event.target instanceof HTMLInputElement) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cake/extensions/mention/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./mention";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { CakeExtension } from "../../core/runtime";
|
|
3
|
+
export type MentionItem = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
};
|
|
7
|
+
export type MentionExtensionOptions<Item extends MentionItem = MentionItem> = {
|
|
8
|
+
getItems: (query: string) => Promise<Item[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Optional item accessors, for non-standard item shapes.
|
|
11
|
+
* Defaults assume { id, label }.
|
|
12
|
+
*/
|
|
13
|
+
getItemId?: (item: Item) => string;
|
|
14
|
+
getItemLabel?: (item: Item) => string;
|
|
15
|
+
/**
|
|
16
|
+
* Customize how mentions are encoded in markdown.
|
|
17
|
+
*
|
|
18
|
+
* Default: "@[id](label)".
|
|
19
|
+
*/
|
|
20
|
+
serializeMention?: (mention: {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
}) => string;
|
|
24
|
+
/**
|
|
25
|
+
* Customize how mentions are parsed from markdown.
|
|
26
|
+
*
|
|
27
|
+
* Return null to indicate no match at `start`.
|
|
28
|
+
*/
|
|
29
|
+
parseMention?: (source: string, start: number, end: number) => {
|
|
30
|
+
mention: {
|
|
31
|
+
id: string;
|
|
32
|
+
label: string;
|
|
33
|
+
};
|
|
34
|
+
nextPos: number;
|
|
35
|
+
} | null;
|
|
36
|
+
renderItem?: (item: Item, context: {
|
|
37
|
+
query: string;
|
|
38
|
+
isActive: boolean;
|
|
39
|
+
}) => ReactNode;
|
|
40
|
+
/**
|
|
41
|
+
* Allows callers to add attributes/classes/styles to the mention element.
|
|
42
|
+
*
|
|
43
|
+
* Note: the element must remain a single cursor unit; do not add extra text nodes.
|
|
44
|
+
*/
|
|
45
|
+
decorateMentionElement?: (params: {
|
|
46
|
+
element: HTMLSpanElement;
|
|
47
|
+
mention: {
|
|
48
|
+
id: string;
|
|
49
|
+
label: string;
|
|
50
|
+
};
|
|
51
|
+
}) => void;
|
|
52
|
+
styles?: {
|
|
53
|
+
popover?: string;
|
|
54
|
+
popoverList?: string;
|
|
55
|
+
popoverItem?: string;
|
|
56
|
+
popoverItemActive?: string;
|
|
57
|
+
mention?: string;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
export declare function mentionExtension<Item extends MentionItem = MentionItem>(options: MentionExtensionOptions<Item>): CakeExtension;
|
|
61
|
+
//# sourceMappingURL=mention.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mention.d.ts","sourceRoot":"","sources":["../../../../src/cake/extensions/mention/mention.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EACV,aAAa,EAGd,MAAM,oBAAoB,CAAC;AAK5B,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,IAAI,SAAS,WAAW,GAAG,WAAW,IAAI;IAC5E,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IACnC,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IACtC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC;IACtE;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK;QAC7D,OAAO,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;QACvC,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,IAAI,CAAC;IACT,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,KAAK,SAAS,CAAC;IACtF;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,CAAC,MAAM,EAAE;QAChC,OAAO,EAAE,eAAe,CAAC;QACzB,OAAO,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC;KACxC,KAAK,IAAI,CAAC;IACX,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,IAAI,SAAS,WAAW,GAAG,WAAW,EACrE,OAAO,EAAE,uBAAuB,CAAC,IAAI,CAAC,GACrC,aAAa,CAsHf"}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
3
|
+
import { CursorSourceBuilder } from "../../core/mapping/cursor-source-map";
|
|
4
|
+
export function mentionExtension(options) {
|
|
5
|
+
const getItemId = options.getItemId ?? ((item) => String(item.id ?? ""));
|
|
6
|
+
const getItemLabel = options.getItemLabel ?? ((item) => String(item.label ?? ""));
|
|
7
|
+
const serializeMention = options.serializeMention ??
|
|
8
|
+
((mention) => `@[${mention.id}](${mention.label})`);
|
|
9
|
+
const parseMention = options.parseMention ?? defaultParseMention;
|
|
10
|
+
return (editor) => {
|
|
11
|
+
const disposers = [];
|
|
12
|
+
disposers.push(editor.registerParseInline((source, start, end) => {
|
|
13
|
+
const result = parseMention(source, start, end);
|
|
14
|
+
if (!result) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
inline: {
|
|
19
|
+
type: "inline-atom",
|
|
20
|
+
kind: "mention",
|
|
21
|
+
data: result.mention,
|
|
22
|
+
},
|
|
23
|
+
nextPos: result.nextPos,
|
|
24
|
+
};
|
|
25
|
+
}));
|
|
26
|
+
disposers.push(editor.registerSerializeInline((inline) => {
|
|
27
|
+
if (inline.type !== "inline-atom" || inline.kind !== "mention") {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const data = inline.data;
|
|
31
|
+
const id = typeof data?.id === "string" ? data.id : "";
|
|
32
|
+
const label = typeof data?.label === "string" ? data.label : "";
|
|
33
|
+
const source = serializeMention({ id, label });
|
|
34
|
+
const builder = new CursorSourceBuilder();
|
|
35
|
+
builder.appendCursorAtom(source, 1);
|
|
36
|
+
return builder.build();
|
|
37
|
+
}));
|
|
38
|
+
disposers.push(editor.registerNormalizeInline((inline) => {
|
|
39
|
+
if (inline.type !== "inline-atom" || inline.kind !== "mention") {
|
|
40
|
+
return inline;
|
|
41
|
+
}
|
|
42
|
+
const data = inline.data;
|
|
43
|
+
if (typeof data?.id !== "string") {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return inline;
|
|
47
|
+
}));
|
|
48
|
+
disposers.push(editor.registerInlineRenderer((inline, context) => {
|
|
49
|
+
if (inline.type !== "inline-atom" || inline.kind !== "mention") {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const data = inline.data;
|
|
53
|
+
const id = typeof data?.id === "string" ? data.id : "";
|
|
54
|
+
const label = typeof data?.label === "string" ? data.label : "";
|
|
55
|
+
const element = document.createElement("span");
|
|
56
|
+
element.className = [
|
|
57
|
+
"cake-inline-atom",
|
|
58
|
+
"cake-inline-atom--mention",
|
|
59
|
+
"cake-mention",
|
|
60
|
+
options.styles?.mention,
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(" ");
|
|
64
|
+
element.setAttribute("data-cake-mention", "true");
|
|
65
|
+
element.setAttribute("data-mention-id", id);
|
|
66
|
+
element.setAttribute("data-mention-label", label);
|
|
67
|
+
options.decorateMentionElement?.({
|
|
68
|
+
element,
|
|
69
|
+
mention: { id, label },
|
|
70
|
+
});
|
|
71
|
+
// IMPORTANT: keep exactly one text node so DOM<->cursor mapping stays 1:1.
|
|
72
|
+
const placeholder = document.createTextNode(" ");
|
|
73
|
+
context.createTextRun(placeholder);
|
|
74
|
+
element.append(placeholder);
|
|
75
|
+
return element;
|
|
76
|
+
}));
|
|
77
|
+
const MentionUI = ({ editor }) => (_jsx(CakeMentionUI, { editor: editor, getItems: options.getItems, getItemId: getItemId, getItemLabel: getItemLabel, serializeMention: serializeMention, renderItem: options.renderItem, styles: options.styles }));
|
|
78
|
+
disposers.push(editor.registerUI(MentionUI));
|
|
79
|
+
return () => disposers.reverse().forEach((d) => d());
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function defaultParseMention(source, start, end) {
|
|
83
|
+
// Default format: @[id](label)
|
|
84
|
+
if (source[start] !== "@" || source[start + 1] !== "[") {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const idStart = start + 2;
|
|
88
|
+
const idClose = source.indexOf("]", idStart);
|
|
89
|
+
if (idClose === -1 || idClose >= end) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (source[idClose + 1] !== "(") {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const labelStart = idClose + 2;
|
|
96
|
+
const labelClose = source.indexOf(")", labelStart);
|
|
97
|
+
if (labelClose === -1 || labelClose >= end) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const id = source.slice(idStart, idClose);
|
|
101
|
+
const label = source.slice(labelStart, labelClose);
|
|
102
|
+
return {
|
|
103
|
+
mention: { id, label },
|
|
104
|
+
nextPos: labelClose + 1,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function ensureMentionStyles() {
|
|
108
|
+
const id = "cake-mention-styles";
|
|
109
|
+
if (document.getElementById(id)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const style = document.createElement("style");
|
|
113
|
+
style.id = id;
|
|
114
|
+
style.textContent = `
|
|
115
|
+
.cake-mention {
|
|
116
|
+
display: inline-flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
border-radius: 9999px;
|
|
119
|
+
padding: 0 6px;
|
|
120
|
+
background: rgba(59, 130, 246, 0.15);
|
|
121
|
+
color: rgb(37, 99, 235);
|
|
122
|
+
font-weight: 500;
|
|
123
|
+
white-space: nowrap;
|
|
124
|
+
}
|
|
125
|
+
.cake-mention::before {
|
|
126
|
+
content: "@" attr(data-mention-label);
|
|
127
|
+
}
|
|
128
|
+
.cake-mention > * {
|
|
129
|
+
display: none;
|
|
130
|
+
}
|
|
131
|
+
.cake-mention-popover {
|
|
132
|
+
min-width: 220px;
|
|
133
|
+
max-width: 320px;
|
|
134
|
+
max-height: 220px;
|
|
135
|
+
overflow: auto;
|
|
136
|
+
background: white;
|
|
137
|
+
border: 1px solid rgba(0,0,0,0.12);
|
|
138
|
+
border-radius: 10px;
|
|
139
|
+
box-shadow: 0 12px 32px rgba(0,0,0,0.16);
|
|
140
|
+
padding: 6px;
|
|
141
|
+
}
|
|
142
|
+
.cake-mention-popover button {
|
|
143
|
+
width: 100%;
|
|
144
|
+
text-align: left;
|
|
145
|
+
border: 0;
|
|
146
|
+
background: transparent;
|
|
147
|
+
padding: 6px 8px;
|
|
148
|
+
border-radius: 8px;
|
|
149
|
+
cursor: pointer;
|
|
150
|
+
}
|
|
151
|
+
.cake-mention-popover button:hover {
|
|
152
|
+
background: rgba(0,0,0,0.06);
|
|
153
|
+
}
|
|
154
|
+
.cake-mention-popover button[aria-selected="true"] {
|
|
155
|
+
background: rgba(59, 130, 246, 0.12);
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
document.head.appendChild(style);
|
|
159
|
+
}
|
|
160
|
+
function getTriggerQuery(textBeforeCursor) {
|
|
161
|
+
// Require the trigger to be at the start or preceded by a non-word character
|
|
162
|
+
// to avoid matching emails/words like "foo@bar", while still supporting
|
|
163
|
+
// punctuation boundaries like "(@alice".
|
|
164
|
+
const match = textBeforeCursor.match(/(?:^|[^\w])@([^\s@]*)$/);
|
|
165
|
+
return match ? match[1] ?? "" : null;
|
|
166
|
+
}
|
|
167
|
+
function getPopoverPositionFromCaret(editor) {
|
|
168
|
+
const rect = editor.getCursorOverlayRect();
|
|
169
|
+
if (!rect) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
top: rect.top + rect.height + 6,
|
|
174
|
+
left: rect.left,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function getPopoverPositionFromElement(editor, anchor) {
|
|
178
|
+
const rect = editor.toOverlayRect(anchor.getBoundingClientRect());
|
|
179
|
+
return {
|
|
180
|
+
top: rect.top + rect.height + 6,
|
|
181
|
+
left: rect.left,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function CakeMentionUI({ editor, getItems, getItemId, getItemLabel, serializeMention, renderItem, styles, }) {
|
|
185
|
+
const container = editor.getContainer();
|
|
186
|
+
const requestIdRef = useRef(0);
|
|
187
|
+
const stateRef = useRef({ status: "closed" });
|
|
188
|
+
const [state, setState] = useState({
|
|
189
|
+
status: "closed",
|
|
190
|
+
});
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
ensureMentionStyles();
|
|
193
|
+
}, []);
|
|
194
|
+
useLayoutEffect(() => {
|
|
195
|
+
stateRef.current = state;
|
|
196
|
+
}, [state]);
|
|
197
|
+
const close = useCallback(() => {
|
|
198
|
+
setState({ status: "closed" });
|
|
199
|
+
}, []);
|
|
200
|
+
const handleChoose = useCallback((item) => {
|
|
201
|
+
const current = stateRef.current;
|
|
202
|
+
if (current.status !== "open") {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (current.replaceAdvanceCursor > 0) {
|
|
206
|
+
const sel = editor.getSelection();
|
|
207
|
+
const cursorLength = editor.getCursorLength();
|
|
208
|
+
const next = Math.min(Math.max(0, sel.start + current.replaceAdvanceCursor), cursorLength);
|
|
209
|
+
editor.setSelection({ start: next, end: next, affinity: "forward" });
|
|
210
|
+
}
|
|
211
|
+
const id = getItemId(item);
|
|
212
|
+
const label = getItemLabel(item);
|
|
213
|
+
const text = serializeMention({ id, label });
|
|
214
|
+
editor.replaceTextBeforeCursor(current.replaceChars, text);
|
|
215
|
+
close();
|
|
216
|
+
editor.focus();
|
|
217
|
+
}, [close, editor, getItemId, getItemLabel, serializeMention]);
|
|
218
|
+
const open = useCallback((next) => {
|
|
219
|
+
setState({
|
|
220
|
+
status: "open",
|
|
221
|
+
mode: next.mode,
|
|
222
|
+
query: next.query,
|
|
223
|
+
replaceChars: next.replaceChars,
|
|
224
|
+
replaceAdvanceCursor: next.replaceAdvanceCursor ?? 0,
|
|
225
|
+
position: next.position,
|
|
226
|
+
items: next.items ?? [],
|
|
227
|
+
loading: true,
|
|
228
|
+
activeIndex: 0,
|
|
229
|
+
});
|
|
230
|
+
}, []);
|
|
231
|
+
const fetch = useCallback((query) => {
|
|
232
|
+
const requestId = (requestIdRef.current += 1);
|
|
233
|
+
let promise;
|
|
234
|
+
try {
|
|
235
|
+
promise = Promise.resolve(getItems(query));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
promise = Promise.resolve([]);
|
|
239
|
+
}
|
|
240
|
+
return promise
|
|
241
|
+
.then((items) => {
|
|
242
|
+
if (requestId !== requestIdRef.current) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
setState((current) => {
|
|
246
|
+
if (current.status !== "open") {
|
|
247
|
+
return current;
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
...current,
|
|
251
|
+
items,
|
|
252
|
+
loading: false,
|
|
253
|
+
activeIndex: Math.min(current.activeIndex, Math.max(0, items.length - 1)),
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
})
|
|
257
|
+
.catch(() => {
|
|
258
|
+
if (requestId !== requestIdRef.current) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
setState((current) => {
|
|
262
|
+
if (current.status !== "open") {
|
|
263
|
+
return current;
|
|
264
|
+
}
|
|
265
|
+
return { ...current, items: [], loading: false };
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}, [getItems]);
|
|
269
|
+
useLayoutEffect(() => {
|
|
270
|
+
// Trigger mode: driven by typing in the editor.
|
|
271
|
+
const updateFromEditor = () => {
|
|
272
|
+
const selection = editor.getSelection();
|
|
273
|
+
if (selection.start !== selection.end) {
|
|
274
|
+
if (stateRef.current.status === "open" &&
|
|
275
|
+
stateRef.current.mode === "trigger") {
|
|
276
|
+
close();
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const query = getTriggerQuery(editor.getTextBeforeCursor(80));
|
|
281
|
+
if (query === null) {
|
|
282
|
+
if (stateRef.current.status === "open" &&
|
|
283
|
+
stateRef.current.mode === "trigger") {
|
|
284
|
+
close();
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const position = getPopoverPositionFromCaret(editor);
|
|
289
|
+
if (!position) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const replaceChars = query.length + 1;
|
|
293
|
+
open({
|
|
294
|
+
status: "open",
|
|
295
|
+
mode: "trigger",
|
|
296
|
+
query,
|
|
297
|
+
replaceChars,
|
|
298
|
+
replaceAdvanceCursor: 0,
|
|
299
|
+
position,
|
|
300
|
+
});
|
|
301
|
+
fetch(query);
|
|
302
|
+
};
|
|
303
|
+
const unsubscribe = editor.onChange(updateFromEditor);
|
|
304
|
+
// In case the user typed before React effects ran, sync from current state.
|
|
305
|
+
updateFromEditor();
|
|
306
|
+
return unsubscribe;
|
|
307
|
+
}, [close, editor, fetch, open]);
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
function handleClick(event) {
|
|
310
|
+
const target = event.target;
|
|
311
|
+
if (!(target instanceof Element)) {
|
|
312
|
+
close();
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Ignore clicks inside the popover itself.
|
|
316
|
+
if (target.closest("[data-testid=\"cake-mention-popover\"]")) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const mention = target.closest("[data-cake-mention]");
|
|
320
|
+
if (!mention) {
|
|
321
|
+
if (stateRef.current.status === "open" &&
|
|
322
|
+
stateRef.current.mode === "replace") {
|
|
323
|
+
close();
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const rect = mention.getBoundingClientRect();
|
|
328
|
+
const midpoint = rect.left + rect.width / 2;
|
|
329
|
+
const placeBefore = event.clientX < midpoint;
|
|
330
|
+
queueMicrotask(() => {
|
|
331
|
+
const domSelection = window.getSelection();
|
|
332
|
+
if (!domSelection) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const walker = document.createTreeWalker(mention, NodeFilter.SHOW_TEXT);
|
|
336
|
+
const node = walker.nextNode();
|
|
337
|
+
const placeholder = node instanceof Text ? node : null;
|
|
338
|
+
const range = document.createRange();
|
|
339
|
+
if (placeholder && placeholder.data.length >= 1) {
|
|
340
|
+
const offset = placeBefore ? 0 : 1;
|
|
341
|
+
range.setStart(placeholder, offset);
|
|
342
|
+
range.setEnd(placeholder, offset);
|
|
343
|
+
domSelection.removeAllRanges();
|
|
344
|
+
domSelection.addRange(range);
|
|
345
|
+
editor.syncSelectionFromDOM();
|
|
346
|
+
}
|
|
347
|
+
// Defer opening until the next frame so any caret auto-scroll triggered
|
|
348
|
+
// by the selection sync finishes before we show the popover.
|
|
349
|
+
requestAnimationFrame(() => {
|
|
350
|
+
const position = getPopoverPositionFromCaret(editor) ??
|
|
351
|
+
getPopoverPositionFromElement(editor, mention);
|
|
352
|
+
open({
|
|
353
|
+
status: "open",
|
|
354
|
+
mode: "replace",
|
|
355
|
+
query: "",
|
|
356
|
+
replaceChars: 1,
|
|
357
|
+
replaceAdvanceCursor: placeBefore ? 1 : 0,
|
|
358
|
+
position,
|
|
359
|
+
});
|
|
360
|
+
fetch("");
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
container.addEventListener("click", handleClick);
|
|
365
|
+
return () => {
|
|
366
|
+
container.removeEventListener("click", handleClick);
|
|
367
|
+
};
|
|
368
|
+
}, [close, container, editor, fetch, open]);
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
return editor.registerKeyDownInterceptor((event) => {
|
|
371
|
+
const current = stateRef.current;
|
|
372
|
+
if (current.status !== "open") {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
if (event.key === "Escape") {
|
|
376
|
+
event.preventDefault();
|
|
377
|
+
close();
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
const delta = event.key === "ArrowDown" ? 1 : -1;
|
|
383
|
+
setState((prev) => {
|
|
384
|
+
if (prev.status !== "open") {
|
|
385
|
+
return prev;
|
|
386
|
+
}
|
|
387
|
+
const length = prev.items.length;
|
|
388
|
+
if (length === 0) {
|
|
389
|
+
return prev;
|
|
390
|
+
}
|
|
391
|
+
const nextIndex = (prev.activeIndex + delta + length) % length;
|
|
392
|
+
return { ...prev, activeIndex: nextIndex };
|
|
393
|
+
});
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
if (event.key === "Enter") {
|
|
397
|
+
event.preventDefault();
|
|
398
|
+
editor.suppressNextBeforeInput();
|
|
399
|
+
const item = current.items[current.activeIndex];
|
|
400
|
+
if (item) {
|
|
401
|
+
handleChoose(item);
|
|
402
|
+
}
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
return false;
|
|
406
|
+
});
|
|
407
|
+
}, [close, editor, handleChoose]);
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
if (state.status !== "open") {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
container.addEventListener("scroll", close, { passive: true });
|
|
413
|
+
window.addEventListener("resize", close);
|
|
414
|
+
return () => {
|
|
415
|
+
container.removeEventListener("scroll", close);
|
|
416
|
+
window.removeEventListener("resize", close);
|
|
417
|
+
};
|
|
418
|
+
}, [close, container, state.status]);
|
|
419
|
+
const className = useMemo(() => ["cake-mention-popover", styles?.popover]
|
|
420
|
+
.filter(Boolean)
|
|
421
|
+
.join(" "), [styles?.popover]);
|
|
422
|
+
if (state.status !== "open") {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
return (_jsx("div", { "data-testid": "cake-mention-popover", className: className, style: {
|
|
426
|
+
position: "absolute",
|
|
427
|
+
top: state.position.top,
|
|
428
|
+
left: state.position.left,
|
|
429
|
+
pointerEvents: "auto",
|
|
430
|
+
zIndex: 10,
|
|
431
|
+
}, onMouseDown: (event) => {
|
|
432
|
+
// Keep focus from leaving the editor on click-drag.
|
|
433
|
+
event.stopPropagation();
|
|
434
|
+
event.preventDefault();
|
|
435
|
+
}, children: _jsxs("div", { className: styles?.popoverList, children: [state.items.map((item, index) => {
|
|
436
|
+
const label = getItemLabel(item);
|
|
437
|
+
const isActive = index === state.activeIndex;
|
|
438
|
+
const itemClass = [
|
|
439
|
+
styles?.popoverItem,
|
|
440
|
+
isActive ? styles?.popoverItemActive : null,
|
|
441
|
+
]
|
|
442
|
+
.filter(Boolean)
|
|
443
|
+
.join(" ");
|
|
444
|
+
const content = renderItem
|
|
445
|
+
? renderItem(item, { query: state.query, isActive })
|
|
446
|
+
: label;
|
|
447
|
+
return (_jsx("button", { type: "button", className: itemClass, "aria-label": label, "aria-selected": isActive, "data-active": isActive ? "true" : undefined, onMouseEnter: () => {
|
|
448
|
+
setState((prev) => {
|
|
449
|
+
if (prev.status !== "open") {
|
|
450
|
+
return prev;
|
|
451
|
+
}
|
|
452
|
+
return { ...prev, activeIndex: index };
|
|
453
|
+
});
|
|
454
|
+
}, onClick: (event) => {
|
|
455
|
+
event.preventDefault();
|
|
456
|
+
event.stopPropagation();
|
|
457
|
+
handleChoose(item);
|
|
458
|
+
}, children: content }, getItemId(item) || String(index)));
|
|
459
|
+
}), state.items.length === 0 && !state.loading ? (_jsx("div", { style: { padding: 8, opacity: 0.6 }, children: "No results" })) : null, state.items.length === 0 && state.loading ? (_jsx("div", { style: { padding: 8, opacity: 0.6 }, children: "Searching\u2026" })) : null] }) }));
|
|
460
|
+
}
|
|
@@ -19,6 +19,8 @@ export interface CakeEditorProps {
|
|
|
19
19
|
spellCheck?: boolean;
|
|
20
20
|
className?: string;
|
|
21
21
|
style?: React.CSSProperties;
|
|
22
|
+
scrollerStyle?: React.CSSProperties;
|
|
23
|
+
scrollerClassName?: string;
|
|
22
24
|
extensions: CakeExtension[];
|
|
23
25
|
onBlur?: (event?: FocusEvent) => void;
|
|
24
26
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cake/react/index.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cake/react/index.tsx"],"names":[],"mappings":"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"}
|