@kerebron/extension-ui 0.8.1
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 +23 -0
- package/README.md +40 -0
- package/assets/ui.css +32 -0
- package/esm/ExtensionUi.d.ts +6 -0
- package/esm/ExtensionUi.d.ts.map +1 -0
- package/esm/ExtensionUi.js +9 -0
- package/esm/ExtensionUi.js.map +1 -0
- package/esm/autocomplete/AutocompletePlugin.d.ts +53 -0
- package/esm/autocomplete/AutocompletePlugin.d.ts.map +1 -0
- package/esm/autocomplete/AutocompletePlugin.js +417 -0
- package/esm/autocomplete/AutocompletePlugin.js.map +1 -0
- package/esm/autocomplete/DefaultRenderer.d.ts +25 -0
- package/esm/autocomplete/DefaultRenderer.d.ts.map +1 -0
- package/esm/autocomplete/DefaultRenderer.js +162 -0
- package/esm/autocomplete/DefaultRenderer.js.map +1 -0
- package/esm/autocomplete/ExtensionAutocomplete.d.ts +11 -0
- package/esm/autocomplete/ExtensionAutocomplete.d.ts.map +1 -0
- package/esm/autocomplete/ExtensionAutocomplete.js +33 -0
- package/esm/autocomplete/ExtensionAutocomplete.js.map +1 -0
- package/esm/autocomplete/createDefaultMatcher.d.ts +11 -0
- package/esm/autocomplete/createDefaultMatcher.d.ts.map +1 -0
- package/esm/autocomplete/createDefaultMatcher.js +58 -0
- package/esm/autocomplete/createDefaultMatcher.js.map +1 -0
- package/esm/autocomplete/createPosMatcher.d.ts +3 -0
- package/esm/autocomplete/createPosMatcher.d.ts.map +1 -0
- package/esm/autocomplete/createPosMatcher.js +16 -0
- package/esm/autocomplete/createPosMatcher.js.map +1 -0
- package/esm/autocomplete/createRegexMatcher.d.ts +4 -0
- package/esm/autocomplete/createRegexMatcher.d.ts.map +1 -0
- package/esm/autocomplete/createRegexMatcher.js +50 -0
- package/esm/autocomplete/createRegexMatcher.js.map +1 -0
- package/esm/autocomplete/mod.d.ts +6 -0
- package/esm/autocomplete/mod.d.ts.map +1 -0
- package/esm/autocomplete/mod.js +6 -0
- package/esm/autocomplete/mod.js.map +1 -0
- package/esm/autocomplete/types.d.ts +49 -0
- package/esm/autocomplete/types.d.ts.map +1 -0
- package/esm/autocomplete/types.js +2 -0
- package/esm/autocomplete/types.js.map +1 -0
- package/esm/hover/ExtensionHover.d.ts +11 -0
- package/esm/hover/ExtensionHover.d.ts.map +1 -0
- package/esm/hover/ExtensionHover.js +33 -0
- package/esm/hover/ExtensionHover.js.map +1 -0
- package/esm/hover/HoverPlugin.d.ts +49 -0
- package/esm/hover/HoverPlugin.d.ts.map +1 -0
- package/esm/hover/HoverPlugin.js +368 -0
- package/esm/hover/HoverPlugin.js.map +1 -0
- package/esm/hover/MarkdownRenderer.d.ts +23 -0
- package/esm/hover/MarkdownRenderer.d.ts.map +1 -0
- package/esm/hover/MarkdownRenderer.js +54 -0
- package/esm/hover/MarkdownRenderer.js.map +1 -0
- package/esm/hover/mod.d.ts +3 -0
- package/esm/hover/mod.d.ts.map +1 -0
- package/esm/hover/mod.js +3 -0
- package/esm/hover/mod.js.map +1 -0
- package/esm/hover/types.d.ts +23 -0
- package/esm/hover/types.d.ts.map +1 -0
- package/esm/hover/types.js +2 -0
- package/esm/hover/types.js.map +1 -0
- package/esm/mod.d.ts +2 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +2 -0
- package/esm/mod.js.map +1 -0
- package/esm/overlayer/mod.d.ts +13 -0
- package/esm/overlayer/mod.d.ts.map +1 -0
- package/esm/overlayer/mod.js +111 -0
- package/esm/overlayer/mod.js.map +1 -0
- package/esm/package.json +3 -0
- package/package.json +43 -0
- package/src/ExtensionUi.ts +10 -0
- package/src/autocomplete/AutocompletePlugin.ts +580 -0
- package/src/autocomplete/DefaultRenderer.ts +189 -0
- package/src/autocomplete/ExtensionAutocomplete.ts +49 -0
- package/src/autocomplete/createDefaultMatcher.ts +94 -0
- package/src/autocomplete/createPosMatcher.ts +21 -0
- package/src/autocomplete/createRegexMatcher.ts +70 -0
- package/src/autocomplete/mod.ts +5 -0
- package/src/autocomplete/types.ts +90 -0
- package/src/hover/ExtensionHover.ts +46 -0
- package/src/hover/HoverPlugin.ts +467 -0
- package/src/hover/MarkdownRenderer.ts +68 -0
- package/src/hover/mod.ts +2 -0
- package/src/hover/types.ts +26 -0
- package/src/mod.ts +1 -0
- package/src/overlayer/mod.ts +146 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { CoreEditor } from '@kerebron/editor';
|
|
2
|
+
import {
|
|
3
|
+
AutocompleteRenderer,
|
|
4
|
+
SuggestionKeyDownProps,
|
|
5
|
+
SuggestionProps,
|
|
6
|
+
} from './types.js';
|
|
7
|
+
import { anchorElement, OverLayer } from '../overlayer/mod.js';
|
|
8
|
+
|
|
9
|
+
const CSS_PREFIX = 'kb-autocomplete';
|
|
10
|
+
|
|
11
|
+
export class DefaultRenderer<Item> extends EventTarget
|
|
12
|
+
implements AutocompleteRenderer {
|
|
13
|
+
command: (props: any) => void;
|
|
14
|
+
wrapper: HTMLElement | undefined;
|
|
15
|
+
list: HTMLElement | undefined;
|
|
16
|
+
items: Array<Item> = [];
|
|
17
|
+
pos: number = -1;
|
|
18
|
+
props: SuggestionProps<Item, any> | undefined;
|
|
19
|
+
readonly keyDownHandler: (this: HTMLElement, ev: KeyboardEvent) => any;
|
|
20
|
+
overlayer: OverLayer;
|
|
21
|
+
anchor?: string;
|
|
22
|
+
|
|
23
|
+
constructor(private editor: CoreEditor) {
|
|
24
|
+
super();
|
|
25
|
+
|
|
26
|
+
this.overlayer = this.editor.ci.resolve('overlayer');
|
|
27
|
+
|
|
28
|
+
this.command = () => {};
|
|
29
|
+
|
|
30
|
+
this.keyDownHandler = (event) => {
|
|
31
|
+
if (this.onKeyDown({ event })) {
|
|
32
|
+
event.stopPropagation();
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setCommand(command: (props: any) => void) {
|
|
39
|
+
this.command = command;
|
|
40
|
+
this.refresh();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setResponse() {
|
|
44
|
+
this.refresh();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
onUpdate(props: SuggestionProps<Item>) {
|
|
48
|
+
this.items.splice(0, this.items.length, ...props.items);
|
|
49
|
+
this.props = props;
|
|
50
|
+
this.refresh();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
destroy() {
|
|
54
|
+
document.body.removeEventListener('keydown', this.keyDownHandler, {
|
|
55
|
+
capture: true,
|
|
56
|
+
});
|
|
57
|
+
if (this.wrapper) {
|
|
58
|
+
this.wrapper.parentNode?.removeChild(this.wrapper);
|
|
59
|
+
this.wrapper = undefined;
|
|
60
|
+
this.dispatchEvent(new Event('close'));
|
|
61
|
+
}
|
|
62
|
+
this.pos = -1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onKeyDown(props: SuggestionKeyDownProps) {
|
|
66
|
+
if (!this.wrapper) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (this.items.length === 0) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (props.event.key === 'Escape') {
|
|
74
|
+
if (this.wrapper) {
|
|
75
|
+
this.wrapper.parentNode?.removeChild(this.wrapper);
|
|
76
|
+
this.wrapper = undefined;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (props.event.key === 'ArrowUp') {
|
|
81
|
+
if (this.pos > -1) {
|
|
82
|
+
this.pos = this.pos - 1;
|
|
83
|
+
this.refresh();
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (props.event.key === 'ArrowDown') {
|
|
89
|
+
if (this.pos < this.items.length - 1) {
|
|
90
|
+
this.pos++;
|
|
91
|
+
this.refresh();
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (props.event.key === 'Enter') {
|
|
97
|
+
if (this.pos > -1 && this.pos < this.items.length) {
|
|
98
|
+
const item = this.items[this.pos];
|
|
99
|
+
this.items.splice(0, this.items.length);
|
|
100
|
+
this.destroy();
|
|
101
|
+
this.command(item);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
createListItem(item: Item, cnt: number) { // override
|
|
109
|
+
const li = document.createElement('li');
|
|
110
|
+
if (cnt === this.pos) {
|
|
111
|
+
li.classList.add('active');
|
|
112
|
+
}
|
|
113
|
+
li.innerText = '' + item; // TODO item to string and item formatting
|
|
114
|
+
li.style.cursor = 'pointer';
|
|
115
|
+
li.addEventListener('click', () => {
|
|
116
|
+
this.destroy();
|
|
117
|
+
this.command(item);
|
|
118
|
+
});
|
|
119
|
+
return li;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
refresh() {
|
|
123
|
+
if (!this.wrapper) {
|
|
124
|
+
this.wrapper = this.overlayer.createElement('div');
|
|
125
|
+
this.wrapper.classList.add(CSS_PREFIX + '__wrapper');
|
|
126
|
+
|
|
127
|
+
this.list = document.createElement('ul');
|
|
128
|
+
this.wrapper.appendChild(this.list);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
document.body.removeEventListener('keydown', this.keyDownHandler, {
|
|
132
|
+
capture: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!this.list) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.list.innerHTML = '';
|
|
140
|
+
for (let cnt = 0; cnt < this.items.length; cnt++) {
|
|
141
|
+
const item = this.items[cnt];
|
|
142
|
+
this.list.appendChild(this.createListItem(item, cnt));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.items.length > 0) {
|
|
146
|
+
document.body.addEventListener('keydown', this.keyDownHandler, {
|
|
147
|
+
capture: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let visible = false;
|
|
152
|
+
if (this.items.length === 0) {
|
|
153
|
+
// this.wrapper.style.display = 'none';
|
|
154
|
+
} else {
|
|
155
|
+
visible = true;
|
|
156
|
+
// this.wrapper.style.display = '';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (this.anchor) {
|
|
160
|
+
anchorElement(this.wrapper, this.anchor, {
|
|
161
|
+
container: this.editor.config.element,
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
visible = false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// if (visible) {
|
|
168
|
+
// if (!this.wrapper.matches(':popover-open')) {
|
|
169
|
+
// const el = document.activeElement;
|
|
170
|
+
// const previousFocus = el instanceof HTMLElement ? el : null;
|
|
171
|
+
// this.wrapper.showPopover();
|
|
172
|
+
// requestAnimationFrame(() => {
|
|
173
|
+
// if (previousFocus && previousFocus.isConnected) {
|
|
174
|
+
// previousFocus.focus();
|
|
175
|
+
// }
|
|
176
|
+
// });
|
|
177
|
+
// }
|
|
178
|
+
// } else {
|
|
179
|
+
// if (this.wrapper.matches(':popover-open')) {
|
|
180
|
+
// this.wrapper.hidePopover();
|
|
181
|
+
// }
|
|
182
|
+
// }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
setAnchorSelector(anchor: string): void {
|
|
186
|
+
this.anchor = anchor;
|
|
187
|
+
this.refresh();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EditorState, Plugin, Transaction } from 'prosemirror-state';
|
|
2
|
+
|
|
3
|
+
import { CommandFactories, Extension } from '@kerebron/editor';
|
|
4
|
+
import { CommandFactory } from '@kerebron/editor/commands';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
AutocompletePlugin,
|
|
8
|
+
AutocompletePluginKey,
|
|
9
|
+
} from './AutocompletePlugin.js';
|
|
10
|
+
import { AutocompleteConfig, AutocompleteSource } from './types.js';
|
|
11
|
+
|
|
12
|
+
export class ExtensionAutocomplete extends Extension {
|
|
13
|
+
name = 'autocomplete';
|
|
14
|
+
|
|
15
|
+
public constructor(
|
|
16
|
+
public override config: AutocompleteConfig = {},
|
|
17
|
+
) {
|
|
18
|
+
super(config);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override getCommandFactories(): Partial<CommandFactories> {
|
|
22
|
+
const addAutocompleteSource: CommandFactory = (
|
|
23
|
+
autocompleteSource: AutocompleteSource,
|
|
24
|
+
) => {
|
|
25
|
+
return (state: EditorState, dispatch?: (tr: Transaction) => void) => {
|
|
26
|
+
const tr = state.tr;
|
|
27
|
+
tr.setMeta(AutocompletePluginKey, {
|
|
28
|
+
addAutocompleteSource: { autocompleteSource },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (dispatch) {
|
|
32
|
+
dispatch(tr);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
addAutocompleteSource,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override getProseMirrorPlugins(): Plugin[] {
|
|
45
|
+
return [
|
|
46
|
+
new AutocompletePlugin(this.config, this.editor),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ResolvedPos } from 'prosemirror-model';
|
|
2
|
+
import { AutocompleteMatcher, SuggestionMatch } from './types.js';
|
|
3
|
+
|
|
4
|
+
// source: https://stackoverflow.com/a/6969486
|
|
5
|
+
export function escapeForRegEx(string: string): string {
|
|
6
|
+
return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MatcherConfig {
|
|
10
|
+
char?: string;
|
|
11
|
+
allowSpaces?: boolean;
|
|
12
|
+
allowToIncludeChar?: boolean;
|
|
13
|
+
allowedPrefixes?: string[] | null;
|
|
14
|
+
startOfLine?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createDefaultMatcher(
|
|
18
|
+
config: MatcherConfig = {},
|
|
19
|
+
): AutocompleteMatcher {
|
|
20
|
+
const char = config.char || '@';
|
|
21
|
+
const allowToIncludeChar = config.allowToIncludeChar || false;
|
|
22
|
+
const allowedPrefixes = config.allowedPrefixes || [' '];
|
|
23
|
+
const startOfLine = config.startOfLine || false;
|
|
24
|
+
|
|
25
|
+
return ($position: ResolvedPos): SuggestionMatch | undefined => {
|
|
26
|
+
const allowSpaces = config.allowSpaces && !allowToIncludeChar;
|
|
27
|
+
|
|
28
|
+
const escapedChar = escapeForRegEx(char);
|
|
29
|
+
const suffix = new RegExp(`\\s${escapedChar}$`);
|
|
30
|
+
const prefix = startOfLine ? '^' : '';
|
|
31
|
+
const finalEscapedChar = allowToIncludeChar ? '' : escapedChar;
|
|
32
|
+
const regexp = allowSpaces
|
|
33
|
+
? new RegExp(
|
|
34
|
+
`${prefix}${escapedChar}.*?(?=\\s${finalEscapedChar}|$)`,
|
|
35
|
+
'gm',
|
|
36
|
+
)
|
|
37
|
+
: new RegExp(
|
|
38
|
+
`${prefix}(?:^)?${escapedChar}[^\\s${finalEscapedChar}]*`,
|
|
39
|
+
'gm',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const text = $position.nodeBefore?.isText && $position.nodeBefore.text;
|
|
43
|
+
|
|
44
|
+
if (!text) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const textFrom = $position.pos - text.length;
|
|
49
|
+
const match = Array.from(text.matchAll(regexp)).pop();
|
|
50
|
+
|
|
51
|
+
if (!match || match.input === undefined || match.index === undefined) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// JavaScript doesn't have lookbehinds. This hacks a check that first character
|
|
56
|
+
// is a space or the start of the line
|
|
57
|
+
const matchPrefix = match.input.slice(
|
|
58
|
+
Math.max(0, match.index - 1),
|
|
59
|
+
match.index,
|
|
60
|
+
);
|
|
61
|
+
const matchPrefixIsAllowed = new RegExp(
|
|
62
|
+
`^[${allowedPrefixes?.join('')}\0]?$`,
|
|
63
|
+
)
|
|
64
|
+
.test(matchPrefix);
|
|
65
|
+
|
|
66
|
+
if (allowedPrefixes !== null && !matchPrefixIsAllowed) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The absolute position of the match in the document
|
|
71
|
+
const from = textFrom + match.index;
|
|
72
|
+
let to = from + match[0].length;
|
|
73
|
+
|
|
74
|
+
// Edge case handling; if spaces are allowed and we're directly in between
|
|
75
|
+
// two triggers
|
|
76
|
+
if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) {
|
|
77
|
+
match[0] += ' ';
|
|
78
|
+
to += 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If the $position is located within the matched substring, return that range
|
|
82
|
+
if (from < $position.pos && to >= $position.pos) {
|
|
83
|
+
return {
|
|
84
|
+
range: {
|
|
85
|
+
from,
|
|
86
|
+
to,
|
|
87
|
+
},
|
|
88
|
+
query: match[0],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return undefined;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ResolvedPos } from 'prosemirror-model';
|
|
2
|
+
import { AutocompleteMatcher, SuggestionMatch } from './types.js';
|
|
3
|
+
|
|
4
|
+
export function createPosMatcher(): AutocompleteMatcher {
|
|
5
|
+
return ($position: ResolvedPos): SuggestionMatch | undefined => {
|
|
6
|
+
const textFrom = $position.pos;
|
|
7
|
+
|
|
8
|
+
const query = '';
|
|
9
|
+
|
|
10
|
+
const from = textFrom;
|
|
11
|
+
const to = from + query.length;
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
range: {
|
|
15
|
+
from,
|
|
16
|
+
to,
|
|
17
|
+
},
|
|
18
|
+
query,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ResolvedPos } from 'prosemirror-model';
|
|
2
|
+
import { AutocompleteMatcher, SuggestionMatch } from './types.js';
|
|
3
|
+
|
|
4
|
+
export function ensureAnchor(expr: RegExp, start: boolean) {
|
|
5
|
+
let { source } = expr;
|
|
6
|
+
let addStart = start && source[0] != '^',
|
|
7
|
+
addEnd = source[source.length - 1] != '$';
|
|
8
|
+
if (!addStart && !addEnd) return expr;
|
|
9
|
+
return new RegExp(
|
|
10
|
+
`${addStart ? '^' : ''}(?:${source})${addEnd ? '$' : ''}`,
|
|
11
|
+
expr.flags ?? (expr.ignoreCase ? 'i' : ''),
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function matchBefore($position: ResolvedPos, expr: RegExp) {
|
|
16
|
+
const text = $position.nodeBefore?.isText && $position.nodeBefore.text;
|
|
17
|
+
|
|
18
|
+
if (!text) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const textFrom = $position.pos - text.length;
|
|
23
|
+
|
|
24
|
+
const start = Math.max(textFrom, $position.pos - 250);
|
|
25
|
+
const str = text.slice();
|
|
26
|
+
|
|
27
|
+
const found = str.search(ensureAnchor(expr, false));
|
|
28
|
+
|
|
29
|
+
return found < 0
|
|
30
|
+
? null
|
|
31
|
+
: { from: start + found, to: $position.pos, text: str.slice(found) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createRegexMatcher(
|
|
35
|
+
regexes: RegExp[],
|
|
36
|
+
): AutocompleteMatcher {
|
|
37
|
+
return ($position: ResolvedPos): SuggestionMatch | undefined => {
|
|
38
|
+
const text = $position.nodeBefore?.isText && $position.nodeBefore.text;
|
|
39
|
+
|
|
40
|
+
if (!text) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const textFrom = $position.pos - text.length;
|
|
44
|
+
|
|
45
|
+
const matches = regexes.map((regex) => matchBefore($position, regex))
|
|
46
|
+
.filter((m) => !!m);
|
|
47
|
+
|
|
48
|
+
if (matches.length === 0) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
matches.sort((a, b) => b.text.length - a.text.length);
|
|
53
|
+
|
|
54
|
+
let from = matches[0].from;
|
|
55
|
+
let matchedText = matches[0].text;
|
|
56
|
+
let to = matches[0].to;
|
|
57
|
+
while (matchedText.match(/^\s/)) {
|
|
58
|
+
matchedText = matchedText.substring(1);
|
|
59
|
+
from++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
range: {
|
|
64
|
+
from,
|
|
65
|
+
to,
|
|
66
|
+
},
|
|
67
|
+
query: matchedText,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ResolvedPos } from 'prosemirror-model';
|
|
2
|
+
import { EditorState } from 'prosemirror-state';
|
|
3
|
+
|
|
4
|
+
import type { TextRange } from '@kerebron/editor';
|
|
5
|
+
|
|
6
|
+
export interface AutocompleteConfig {
|
|
7
|
+
decorationTag?: string;
|
|
8
|
+
decorationClass?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AutocompleteProps {
|
|
12
|
+
range: TextRange;
|
|
13
|
+
isActive?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type AutocompleteMatcher = (
|
|
17
|
+
pos: ResolvedPos,
|
|
18
|
+
) => SuggestionMatch | undefined;
|
|
19
|
+
|
|
20
|
+
export interface AutocompleteSource<I = any, TSelected = any> {
|
|
21
|
+
getItems: (query: string, props: AutocompleteProps) => I[] | Promise<I[]>;
|
|
22
|
+
|
|
23
|
+
onSelect: (selected: TSelected, range: TextRange) => void;
|
|
24
|
+
allow?: (
|
|
25
|
+
props: AutocompleteProps,
|
|
26
|
+
) => boolean;
|
|
27
|
+
|
|
28
|
+
matchers?: AutocompleteMatcher[];
|
|
29
|
+
renderer?: AutocompleteRenderer<I, TSelected>;
|
|
30
|
+
triggerKeys?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SuggestionKeyDownProps {
|
|
34
|
+
event: KeyboardEvent;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SuggestionMatch = {
|
|
38
|
+
range: TextRange;
|
|
39
|
+
query: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface SuggestionProps<I = any, TSelected = any> {
|
|
43
|
+
match: SuggestionMatch;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The suggestion items array.
|
|
47
|
+
*/
|
|
48
|
+
items: I[];
|
|
49
|
+
|
|
50
|
+
// /**
|
|
51
|
+
// * The decoration node HTML element
|
|
52
|
+
// * @default null
|
|
53
|
+
// */
|
|
54
|
+
// decorationNode: Element | null;
|
|
55
|
+
|
|
56
|
+
// anchor: HTMLElement;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The function that returns the client rect
|
|
60
|
+
* @default null
|
|
61
|
+
* @example () => new DOMRect(0, 0, 0, 0)
|
|
62
|
+
*/
|
|
63
|
+
// clientRect?: (() => DOMRect | null) | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AutocompleteRenderer<I = any, TSelected = any> {
|
|
67
|
+
setAnchorSelector(anchor: string): void;
|
|
68
|
+
setCommand: (command: (props: TSelected) => void) => void;
|
|
69
|
+
setResponse: () => void;
|
|
70
|
+
onUpdate: (props: SuggestionProps<I, TSelected>) => void;
|
|
71
|
+
onKeyDown?: (props: SuggestionKeyDownProps) => boolean;
|
|
72
|
+
destroy: () => void;
|
|
73
|
+
refresh: () => void;
|
|
74
|
+
|
|
75
|
+
addEventListener(
|
|
76
|
+
type: string,
|
|
77
|
+
listener: EventListenerOrEventListenerObject,
|
|
78
|
+
options?: boolean | AddEventListenerOptions,
|
|
79
|
+
): void;
|
|
80
|
+
removeEventListener(
|
|
81
|
+
type: string,
|
|
82
|
+
callback: EventListenerOrEventListenerObject | null,
|
|
83
|
+
options?: EventListenerOptions | boolean,
|
|
84
|
+
): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export type MatchedSource = {
|
|
88
|
+
match: SuggestionMatch;
|
|
89
|
+
source: AutocompleteSource;
|
|
90
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { EditorState, Plugin, Transaction } from 'prosemirror-state';
|
|
2
|
+
|
|
3
|
+
import { CommandFactories, Extension } from '@kerebron/editor';
|
|
4
|
+
import { CommandFactory } from '@kerebron/editor/commands';
|
|
5
|
+
|
|
6
|
+
import { HoverPlugin, HoverPluginKey } from './HoverPlugin.js';
|
|
7
|
+
import { HoverConfig, HoverSource } from './types.js';
|
|
8
|
+
|
|
9
|
+
export class ExtensionHover extends Extension {
|
|
10
|
+
name = 'hover';
|
|
11
|
+
|
|
12
|
+
public constructor(
|
|
13
|
+
public override config: HoverConfig = {},
|
|
14
|
+
) {
|
|
15
|
+
super(config);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override getCommandFactories(): Partial<CommandFactories> {
|
|
19
|
+
const addHoverSource: CommandFactory = (
|
|
20
|
+
hoverSource: HoverSource,
|
|
21
|
+
) => {
|
|
22
|
+
return (state: EditorState, dispatch?: (tr: Transaction) => void) => {
|
|
23
|
+
const tr = state.tr;
|
|
24
|
+
tr.setMeta(HoverPluginKey, {
|
|
25
|
+
addHoverSource: { hoverSource },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (dispatch) {
|
|
29
|
+
dispatch(tr);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
addHoverSource,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override getProseMirrorPlugins(): Plugin[] {
|
|
42
|
+
return [
|
|
43
|
+
new HoverPlugin(this.config, this.editor),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
}
|