@qalma/editor 0.0.1-alpha.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/README.md +45 -0
- package/fesm2022/qalma-editor.mjs +3228 -0
- package/fesm2022/qalma-editor.mjs.map +1 -0
- package/package.json +52 -0
- package/types/qalma-editor.d.ts +274 -0
- package/types/qalma-editor.d.ts.map +1 -0
|
@@ -0,0 +1,3228 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, input, inject, computed, Directive, DestroyRef, viewChild, afterNextRender, ChangeDetectionStrategy, Component, forwardRef, signal } from '@angular/core';
|
|
3
|
+
import { EditorView, DecorationSet, Decoration } from 'prosemirror-view';
|
|
4
|
+
import { DOMParser, DOMSerializer, Schema } from 'prosemirror-model';
|
|
5
|
+
import { keymap } from 'prosemirror-keymap';
|
|
6
|
+
import { baseKeymap, lift, wrapIn, setBlockType, toggleMark } from 'prosemirror-commands';
|
|
7
|
+
import { EditorState, Plugin, NodeSelection, TextSelection } from 'prosemirror-state';
|
|
8
|
+
import { redo, undo, history } from 'prosemirror-history';
|
|
9
|
+
import { bulletList, orderedList, listItem, liftListItem, sinkListItem, splitListItemKeepMarks, wrapInList } from 'prosemirror-schema-list';
|
|
10
|
+
|
|
11
|
+
const QALMA_EDITOR_CONTEXT = new InjectionToken('QALMA_EDITOR_CONTEXT');
|
|
12
|
+
|
|
13
|
+
class QalmaCommand {
|
|
14
|
+
command = input.required({ ...(ngDevMode ? { debugName: "command" } : /* istanbul ignore next */ {}), alias: 'qalmaCommand' });
|
|
15
|
+
qalmaCommandValue = input(...(ngDevMode ? [undefined, { debugName: "qalmaCommandValue" }] : /* istanbul ignore next */ []));
|
|
16
|
+
context = inject(QALMA_EDITOR_CONTEXT);
|
|
17
|
+
editor = computed(() => this.context.editor(), ...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
18
|
+
active = computed(() => this.editor().isCommandActive(this.command()), ...(ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []));
|
|
19
|
+
disabled = computed(() => !this.editor().canExecute(this.command(), this.qalmaCommandValue()), ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
20
|
+
ariaPressed = computed(() => this.editor().hasCommandState(this.command())
|
|
21
|
+
? String(this.active())
|
|
22
|
+
: null, ...(ngDevMode ? [{ debugName: "ariaPressed" }] : /* istanbul ignore next */ []));
|
|
23
|
+
execute() {
|
|
24
|
+
this.editor().execute(this.command(), this.qalmaCommandValue());
|
|
25
|
+
}
|
|
26
|
+
preserveSelection(event) {
|
|
27
|
+
event.preventDefault();
|
|
28
|
+
}
|
|
29
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QalmaCommand, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
30
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.16", type: QalmaCommand, isStandalone: true, selector: "button[qalmaCommand]", inputs: { command: { classPropertyName: "command", publicName: "qalmaCommand", isSignal: true, isRequired: true, transformFunction: null }, qalmaCommandValue: { classPropertyName: "qalmaCommandValue", publicName: "qalmaCommandValue", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "execute()", "mousedown": "preserveSelection($event)" }, properties: { "attr.aria-pressed": "ariaPressed()", "class.qalma-command-active": "active()", "disabled": "disabled()" } }, ngImport: i0 });
|
|
31
|
+
}
|
|
32
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QalmaCommand, decorators: [{
|
|
33
|
+
type: Directive,
|
|
34
|
+
args: [{
|
|
35
|
+
selector: 'button[qalmaCommand]',
|
|
36
|
+
host: {
|
|
37
|
+
'(click)': 'execute()',
|
|
38
|
+
'(mousedown)': 'preserveSelection($event)',
|
|
39
|
+
'[attr.aria-pressed]': 'ariaPressed()',
|
|
40
|
+
'[class.qalma-command-active]': 'active()',
|
|
41
|
+
'[disabled]': 'disabled()',
|
|
42
|
+
},
|
|
43
|
+
}]
|
|
44
|
+
}], propDecorators: { command: [{ type: i0.Input, args: [{ isSignal: true, alias: "qalmaCommand", required: true }] }], qalmaCommandValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "qalmaCommandValue", required: false }] }] } });
|
|
45
|
+
|
|
46
|
+
class QalmaContent {
|
|
47
|
+
context = inject(QALMA_EDITOR_CONTEXT);
|
|
48
|
+
destroyRef = inject(DestroyRef);
|
|
49
|
+
editorHost = viewChild.required('editorHost');
|
|
50
|
+
mountedHost;
|
|
51
|
+
constructor() {
|
|
52
|
+
afterNextRender(() => {
|
|
53
|
+
const host = this.editorHost().nativeElement;
|
|
54
|
+
this.context.editor().mount(host);
|
|
55
|
+
this.mountedHost = host;
|
|
56
|
+
});
|
|
57
|
+
this.destroyRef.onDestroy(() => {
|
|
58
|
+
if (this.mountedHost) {
|
|
59
|
+
this.context.editor().unmount(this.mountedHost);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QalmaContent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
64
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.16", type: QalmaContent, isStandalone: true, selector: "qalma-content", host: { properties: { "class.qalma-content": "true" } }, viewQueries: [{ propertyName: "editorHost", first: true, predicate: ["editorHost"], descendants: true, isSignal: true }], ngImport: i0, template: `<div #editorHost class="qalma-content-surface"></div>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
65
|
+
}
|
|
66
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QalmaContent, decorators: [{
|
|
67
|
+
type: Component,
|
|
68
|
+
args: [{
|
|
69
|
+
selector: 'qalma-content',
|
|
70
|
+
imports: [],
|
|
71
|
+
template: `<div #editorHost class="qalma-content-surface"></div>`,
|
|
72
|
+
host: {
|
|
73
|
+
'[class.qalma-content]': 'true',
|
|
74
|
+
},
|
|
75
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
76
|
+
}]
|
|
77
|
+
}], ctorParameters: () => [], propDecorators: { editorHost: [{ type: i0.ViewChild, args: ['editorHost', { isSignal: true }] }] } });
|
|
78
|
+
|
|
79
|
+
class QalmaEditor {
|
|
80
|
+
editor = input.required(...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
|
|
81
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QalmaEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
82
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.16", type: QalmaEditor, isStandalone: true, selector: "qalma-editor", inputs: { editor: { classPropertyName: "editor", publicName: "editor", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.qalma-editor": "true" } }, providers: [
|
|
83
|
+
{
|
|
84
|
+
provide: QALMA_EDITOR_CONTEXT,
|
|
85
|
+
useExisting: forwardRef(() => QalmaEditor),
|
|
86
|
+
},
|
|
87
|
+
], ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
88
|
+
}
|
|
89
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QalmaEditor, decorators: [{
|
|
90
|
+
type: Component,
|
|
91
|
+
args: [{
|
|
92
|
+
selector: 'qalma-editor',
|
|
93
|
+
imports: [],
|
|
94
|
+
providers: [
|
|
95
|
+
{
|
|
96
|
+
provide: QALMA_EDITOR_CONTEXT,
|
|
97
|
+
useExisting: forwardRef(() => QalmaEditor),
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
template: `<ng-content />`,
|
|
101
|
+
host: {
|
|
102
|
+
'[class.qalma-editor]': 'true',
|
|
103
|
+
},
|
|
104
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
105
|
+
}]
|
|
106
|
+
}], propDecorators: { editor: [{ type: i0.Input, args: [{ isSignal: true, alias: "editor", required: true }] }] } });
|
|
107
|
+
|
|
108
|
+
function parseHtmlDocument(html, schema) {
|
|
109
|
+
const container = document.createElement('div');
|
|
110
|
+
container.innerHTML = html.trim() || '<p></p>';
|
|
111
|
+
return DOMParser.fromSchema(schema).parse(container);
|
|
112
|
+
}
|
|
113
|
+
function serializeHtmlDocument(doc, schema) {
|
|
114
|
+
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content);
|
|
115
|
+
const container = document.createElement('div');
|
|
116
|
+
container.appendChild(fragment);
|
|
117
|
+
return container.innerHTML.replace(/\n/g, ' ');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createBasePlugins(schema, qalmaPlugins) {
|
|
121
|
+
const shortcuts = createShortcutRegistry(schema, qalmaPlugins);
|
|
122
|
+
return [
|
|
123
|
+
...qalmaPlugins.flatMap((plugin) => plugin.prosemirrorPlugins?.(schema) ?? []),
|
|
124
|
+
...(Object.keys(shortcuts).length > 0 ? [keymap(shortcuts)] : []),
|
|
125
|
+
keymap(baseKeymap),
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
function createCommandRegistry(schema, qalmaPlugins) {
|
|
129
|
+
const commands = {};
|
|
130
|
+
for (const plugin of qalmaPlugins) {
|
|
131
|
+
for (const [name, command] of Object.entries(plugin.commands?.(schema) ?? {})) {
|
|
132
|
+
if (commands[name]) {
|
|
133
|
+
throw new Error(`QALMA plugin "${plugin.key}" defines duplicate command "${name}".`);
|
|
134
|
+
}
|
|
135
|
+
commands[name] = command;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return commands;
|
|
139
|
+
}
|
|
140
|
+
function createCommandStateRegistry(schema, qalmaPlugins) {
|
|
141
|
+
const states = {};
|
|
142
|
+
for (const plugin of qalmaPlugins) {
|
|
143
|
+
for (const [commandName, query] of Object.entries(plugin.commandStates?.(schema) ?? {})) {
|
|
144
|
+
if (states[commandName]) {
|
|
145
|
+
throw new Error(`QALMA plugin "${plugin.key}" defines duplicate command state "${commandName}".`);
|
|
146
|
+
}
|
|
147
|
+
states[commandName] = query;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return states;
|
|
151
|
+
}
|
|
152
|
+
function createQueryRegistry(schema, qalmaPlugins) {
|
|
153
|
+
const queries = {};
|
|
154
|
+
for (const plugin of qalmaPlugins) {
|
|
155
|
+
for (const [name, query] of Object.entries(plugin.queries?.(schema) ?? {})) {
|
|
156
|
+
if (queries[name]) {
|
|
157
|
+
throw new Error(`QALMA plugin "${plugin.key}" defines duplicate query "${name}".`);
|
|
158
|
+
}
|
|
159
|
+
queries[name] = query;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return queries;
|
|
163
|
+
}
|
|
164
|
+
function createShortcutRegistry(schema, qalmaPlugins) {
|
|
165
|
+
const shortcuts = {};
|
|
166
|
+
for (const plugin of qalmaPlugins) {
|
|
167
|
+
for (const [shortcut, command] of Object.entries(plugin.shortcuts?.(schema) ?? {})) {
|
|
168
|
+
if (shortcuts[shortcut]) {
|
|
169
|
+
throw new Error(`QALMA plugin "${plugin.key}" defines duplicate shortcut "${shortcut}".`);
|
|
170
|
+
}
|
|
171
|
+
shortcuts[shortcut] = command;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return shortcuts;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const baseNodes = {
|
|
178
|
+
doc: {
|
|
179
|
+
content: 'block+',
|
|
180
|
+
},
|
|
181
|
+
paragraph: {
|
|
182
|
+
content: 'inline*',
|
|
183
|
+
group: 'block',
|
|
184
|
+
parseDOM: [{ tag: 'p' }],
|
|
185
|
+
toDOM: () => ['p', 0],
|
|
186
|
+
},
|
|
187
|
+
text: {
|
|
188
|
+
group: 'inline',
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
function createQalmaSchema(plugins) {
|
|
192
|
+
assertUniquePluginKeys(plugins);
|
|
193
|
+
const nodes = { ...baseNodes };
|
|
194
|
+
const marks = {};
|
|
195
|
+
for (const plugin of plugins) {
|
|
196
|
+
addUniqueEntries(nodes, plugin.nodes, plugin.key, 'node');
|
|
197
|
+
addUniqueEntries(marks, plugin.marks, plugin.key, 'mark');
|
|
198
|
+
}
|
|
199
|
+
for (const plugin of plugins) {
|
|
200
|
+
extendExistingNodes(nodes, plugin.extendNodes?.(Object.freeze({ ...nodes })), plugin.key);
|
|
201
|
+
}
|
|
202
|
+
return new Schema({
|
|
203
|
+
nodes,
|
|
204
|
+
marks,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function assertUniquePluginKeys(plugins) {
|
|
208
|
+
const keys = new Set();
|
|
209
|
+
for (const plugin of plugins) {
|
|
210
|
+
if (keys.has(plugin.key)) {
|
|
211
|
+
throw new Error(`Duplicate QALMA plugin key "${plugin.key}".`);
|
|
212
|
+
}
|
|
213
|
+
keys.add(plugin.key);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function addUniqueEntries(target, entries, pluginKey, type) {
|
|
217
|
+
for (const [name, spec] of Object.entries(entries ?? {})) {
|
|
218
|
+
if (target[name]) {
|
|
219
|
+
throw new Error(`QALMA plugin "${pluginKey}" defines duplicate ${type} "${name}".`);
|
|
220
|
+
}
|
|
221
|
+
target[name] = spec;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function extendExistingNodes(nodes, extensions, pluginKey) {
|
|
225
|
+
for (const [name, spec] of Object.entries(extensions ?? {})) {
|
|
226
|
+
if (!nodes[name]) {
|
|
227
|
+
throw new Error(`QALMA plugin "${pluginKey}" extends unknown node "${name}".`);
|
|
228
|
+
}
|
|
229
|
+
nodes[name] = spec;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function createQalmaState(options) {
|
|
234
|
+
return EditorState.create({
|
|
235
|
+
doc: parseHtmlDocument(options.html, options.schema),
|
|
236
|
+
schema: options.schema,
|
|
237
|
+
plugins: createBasePlugins(options.schema, options.plugins),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
class QalmaEditorController {
|
|
242
|
+
html;
|
|
243
|
+
editable;
|
|
244
|
+
plugins;
|
|
245
|
+
schema;
|
|
246
|
+
htmlState;
|
|
247
|
+
editableState;
|
|
248
|
+
viewVersion = signal(0, ...(ngDevMode ? [{ debugName: "viewVersion" }] : /* istanbul ignore next */ []));
|
|
249
|
+
commands;
|
|
250
|
+
commandStates;
|
|
251
|
+
queries;
|
|
252
|
+
editorState;
|
|
253
|
+
editorView;
|
|
254
|
+
host;
|
|
255
|
+
constructor(options = {}) {
|
|
256
|
+
this.plugins = [...(options.plugins ?? [])];
|
|
257
|
+
this.schema = createQalmaSchema(this.plugins);
|
|
258
|
+
this.commands = createCommandRegistry(this.schema, this.plugins);
|
|
259
|
+
this.commandStates = createCommandStateRegistry(this.schema, this.plugins);
|
|
260
|
+
this.queries = createQueryRegistry(this.schema, this.plugins);
|
|
261
|
+
this.htmlState = signal(options.content ?? '<p></p>', ...(ngDevMode ? [{ debugName: "htmlState" }] : /* istanbul ignore next */ []));
|
|
262
|
+
this.editableState = signal(options.editable ?? true, ...(ngDevMode ? [{ debugName: "editableState" }] : /* istanbul ignore next */ []));
|
|
263
|
+
this.html = this.htmlState.asReadonly();
|
|
264
|
+
this.editable = this.editableState.asReadonly();
|
|
265
|
+
}
|
|
266
|
+
mount(host) {
|
|
267
|
+
if (this.host === host && this.editorView) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.unmount();
|
|
271
|
+
host.replaceChildren();
|
|
272
|
+
this.editorState = createQalmaState({
|
|
273
|
+
html: this.html(),
|
|
274
|
+
plugins: this.plugins,
|
|
275
|
+
schema: this.schema,
|
|
276
|
+
});
|
|
277
|
+
this.host = host;
|
|
278
|
+
this.editorView = new EditorView(host, {
|
|
279
|
+
state: this.editorState,
|
|
280
|
+
editable: () => this.editable(),
|
|
281
|
+
attributes: this.createEditorAttributes(),
|
|
282
|
+
dispatchTransaction: (transaction) => this.dispatchTransaction(transaction),
|
|
283
|
+
});
|
|
284
|
+
this.syncHtmlFromEditorState();
|
|
285
|
+
this.bumpViewVersion();
|
|
286
|
+
}
|
|
287
|
+
unmount(host) {
|
|
288
|
+
if (host && host !== this.host) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.editorView?.destroy();
|
|
292
|
+
this.editorView = undefined;
|
|
293
|
+
this.editorState = undefined;
|
|
294
|
+
this.host = undefined;
|
|
295
|
+
this.bumpViewVersion();
|
|
296
|
+
}
|
|
297
|
+
execute(commandName, value) {
|
|
298
|
+
const command = this.commands[commandName];
|
|
299
|
+
if (!this.editable() || !command || !this.editorState) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
const executed = command(this.editorState, (transaction) => this.dispatchTransaction(transaction), this.editorView, value);
|
|
303
|
+
if (executed) {
|
|
304
|
+
this.editorView?.focus();
|
|
305
|
+
}
|
|
306
|
+
return executed;
|
|
307
|
+
}
|
|
308
|
+
canExecute(commandName, value) {
|
|
309
|
+
this.viewVersion();
|
|
310
|
+
const command = this.commands[commandName];
|
|
311
|
+
return Boolean(this.editable() &&
|
|
312
|
+
command &&
|
|
313
|
+
this.editorState &&
|
|
314
|
+
command(this.editorState, undefined, this.editorView, value));
|
|
315
|
+
}
|
|
316
|
+
hasCommandState(commandName) {
|
|
317
|
+
return Boolean(this.commandStates[commandName]);
|
|
318
|
+
}
|
|
319
|
+
isCommandActive(commandName) {
|
|
320
|
+
this.viewVersion();
|
|
321
|
+
const query = this.commandStates[commandName];
|
|
322
|
+
return Boolean(query && this.editorState && query(this.editorState));
|
|
323
|
+
}
|
|
324
|
+
hasQuery(queryName) {
|
|
325
|
+
return Boolean(this.queries[queryName]);
|
|
326
|
+
}
|
|
327
|
+
query(queryName) {
|
|
328
|
+
this.viewVersion();
|
|
329
|
+
const query = this.queries[queryName];
|
|
330
|
+
return query && this.editorState
|
|
331
|
+
? query(this.editorState)
|
|
332
|
+
: null;
|
|
333
|
+
}
|
|
334
|
+
setHtml(html) {
|
|
335
|
+
if (html === this.html()) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!this.editorView) {
|
|
339
|
+
this.htmlState.set(html);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.editorState = createQalmaState({
|
|
343
|
+
html,
|
|
344
|
+
plugins: this.plugins,
|
|
345
|
+
schema: this.schema,
|
|
346
|
+
});
|
|
347
|
+
this.editorView.updateState(this.editorState);
|
|
348
|
+
this.syncHtmlFromEditorState();
|
|
349
|
+
this.bumpViewVersion();
|
|
350
|
+
}
|
|
351
|
+
setEditable(editable) {
|
|
352
|
+
this.editableState.set(editable);
|
|
353
|
+
this.editorView?.setProps({
|
|
354
|
+
editable: () => editable,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
focus() {
|
|
358
|
+
this.editorView?.focus();
|
|
359
|
+
}
|
|
360
|
+
dispatchTransaction(transaction) {
|
|
361
|
+
if (!this.editorState) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
this.editorState = this.editorState.apply(transaction);
|
|
365
|
+
this.editorView?.updateState(this.editorState);
|
|
366
|
+
if (transaction.docChanged) {
|
|
367
|
+
this.syncHtmlFromEditorState();
|
|
368
|
+
}
|
|
369
|
+
this.bumpViewVersion();
|
|
370
|
+
}
|
|
371
|
+
syncHtmlFromEditorState() {
|
|
372
|
+
if (!this.editorState) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const html = serializeHtmlDocument(this.editorState.doc, this.schema);
|
|
376
|
+
if (html !== this.html()) {
|
|
377
|
+
this.htmlState.set(html);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
bumpViewVersion() {
|
|
381
|
+
this.viewVersion.update((value) => value + 1);
|
|
382
|
+
}
|
|
383
|
+
createEditorAttributes() {
|
|
384
|
+
return {
|
|
385
|
+
'aria-label': 'Rich text editor',
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function createQalmaEditor(options = {}) {
|
|
390
|
+
return new QalmaEditorController(options);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
class QalmaToolbar {
|
|
394
|
+
label = input('Editor toolbar', ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
|
|
395
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QalmaToolbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
396
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.16", type: QalmaToolbar, isStandalone: true, selector: "qalma-toolbar", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "toolbar" }, properties: { "class.qalma-toolbar": "true", "attr.aria-label": "label()" } }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
397
|
+
}
|
|
398
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.16", ngImport: i0, type: QalmaToolbar, decorators: [{
|
|
399
|
+
type: Component,
|
|
400
|
+
args: [{
|
|
401
|
+
selector: 'qalma-toolbar',
|
|
402
|
+
imports: [],
|
|
403
|
+
template: `<ng-content />`,
|
|
404
|
+
host: {
|
|
405
|
+
'[class.qalma-toolbar]': 'true',
|
|
406
|
+
role: 'toolbar',
|
|
407
|
+
'[attr.aria-label]': 'label()',
|
|
408
|
+
},
|
|
409
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
410
|
+
}]
|
|
411
|
+
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
|
|
412
|
+
|
|
413
|
+
function createQalmaPlugin(plugin) {
|
|
414
|
+
return plugin;
|
|
415
|
+
}
|
|
416
|
+
function createConfigurableQalmaPlugin(defaultOptions, createPlugin, options = {}) {
|
|
417
|
+
const resolvedOptions = Object.freeze({
|
|
418
|
+
...defaultOptions,
|
|
419
|
+
...options,
|
|
420
|
+
});
|
|
421
|
+
const plugin = createPlugin(resolvedOptions);
|
|
422
|
+
return Object.assign(plugin, {
|
|
423
|
+
options: resolvedOptions,
|
|
424
|
+
configure(nextOptions = {}) {
|
|
425
|
+
return createConfigurableQalmaPlugin(defaultOptions, createPlugin, {
|
|
426
|
+
...resolvedOptions,
|
|
427
|
+
...nextOptions,
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const blockquoteNode = {
|
|
434
|
+
content: 'block+',
|
|
435
|
+
group: 'block',
|
|
436
|
+
defining: true,
|
|
437
|
+
parseDOM: [{ tag: 'blockquote' }],
|
|
438
|
+
toDOM: () => ['blockquote', 0],
|
|
439
|
+
};
|
|
440
|
+
const BlockquotePlugin = createQalmaPlugin({
|
|
441
|
+
key: 'blockquote',
|
|
442
|
+
nodes: {
|
|
443
|
+
blockquote: blockquoteNode,
|
|
444
|
+
},
|
|
445
|
+
commands: (schema) => ({
|
|
446
|
+
toggleBlockquote: createToggleBlockquoteCommand(schema.nodes['blockquote']),
|
|
447
|
+
}),
|
|
448
|
+
commandStates: (schema) => ({
|
|
449
|
+
toggleBlockquote: (state) => isBlockquoteActive(state, schema.nodes['blockquote']),
|
|
450
|
+
}),
|
|
451
|
+
});
|
|
452
|
+
const BlockquoteKit = [BlockquotePlugin];
|
|
453
|
+
function createToggleBlockquoteCommand(blockquote) {
|
|
454
|
+
return (state, dispatch) => {
|
|
455
|
+
if (isBlockquoteActive(state, blockquote)) {
|
|
456
|
+
return lift(state, dispatch);
|
|
457
|
+
}
|
|
458
|
+
return wrapIn(blockquote)(state, dispatch);
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function isBlockquoteActive(state, blockquote) {
|
|
462
|
+
const { $from } = state.selection;
|
|
463
|
+
for (let depth = $from.depth; depth > 0; depth -= 1) {
|
|
464
|
+
if ($from.node(depth).type === blockquote) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const ClearFormattingPlugin = createQalmaPlugin({
|
|
472
|
+
key: 'clearFormatting',
|
|
473
|
+
commands: (schema) => ({
|
|
474
|
+
clearFormatting: createClearFormattingCommand(schema.nodes['paragraph']),
|
|
475
|
+
}),
|
|
476
|
+
});
|
|
477
|
+
const ClearFormattingKit = [
|
|
478
|
+
ClearFormattingPlugin,
|
|
479
|
+
];
|
|
480
|
+
function createClearFormattingCommand(paragraph) {
|
|
481
|
+
return (state, dispatch) => {
|
|
482
|
+
const markRanges = getMarkRangesToClear(state);
|
|
483
|
+
const hasMarksToClear = Boolean(state.storedMarks?.length) ||
|
|
484
|
+
markRanges.some((range) => rangeHasInlineMarks(state.doc, range.from, range.to));
|
|
485
|
+
const hasTextblocksToClear = selectionHasTextblockFormattingToClear(state, paragraph);
|
|
486
|
+
if (!hasMarksToClear && !hasTextblocksToClear) {
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
if (dispatch) {
|
|
490
|
+
const transaction = state.tr;
|
|
491
|
+
if (state.storedMarks?.length) {
|
|
492
|
+
transaction.setStoredMarks([]);
|
|
493
|
+
}
|
|
494
|
+
if (hasMarksToClear) {
|
|
495
|
+
for (const range of markRanges) {
|
|
496
|
+
if (range.from < range.to) {
|
|
497
|
+
transaction.removeMark(range.from, range.to, null);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (hasTextblocksToClear) {
|
|
502
|
+
for (const range of state.selection.ranges) {
|
|
503
|
+
transaction.setBlockType(range.$from.pos, range.$to.pos, paragraph);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
dispatch(transaction.scrollIntoView());
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function getMarkRangesToClear(state) {
|
|
512
|
+
if (!state.selection.empty) {
|
|
513
|
+
return state.selection.ranges.map((range) => ({
|
|
514
|
+
from: range.$from.pos,
|
|
515
|
+
to: range.$to.pos,
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
const { $from } = state.selection;
|
|
519
|
+
if ($from.depth === 0 || !$from.parent.inlineContent) {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
return [
|
|
523
|
+
{
|
|
524
|
+
from: $from.start(),
|
|
525
|
+
to: $from.end(),
|
|
526
|
+
},
|
|
527
|
+
];
|
|
528
|
+
}
|
|
529
|
+
function rangeHasInlineMarks(doc, from, to) {
|
|
530
|
+
if (from >= to) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
let hasMarks = false;
|
|
534
|
+
doc.nodesBetween(from, to, (node) => {
|
|
535
|
+
if (hasMarks) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
if (node.isInline && node.marks.length > 0) {
|
|
539
|
+
hasMarks = true;
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
return undefined;
|
|
543
|
+
});
|
|
544
|
+
return hasMarks;
|
|
545
|
+
}
|
|
546
|
+
function selectionHasTextblockFormattingToClear(state, paragraph) {
|
|
547
|
+
let applicable = false;
|
|
548
|
+
for (const range of state.selection.ranges) {
|
|
549
|
+
if (applicable) {
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
state.doc.nodesBetween(range.$from.pos, range.$to.pos, (node, pos) => {
|
|
553
|
+
if (applicable) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
if (!node.isTextblock || node.hasMarkup(paragraph)) {
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
if (node.type === paragraph) {
|
|
560
|
+
applicable = true;
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
const $pos = state.doc.resolve(pos);
|
|
564
|
+
const index = $pos.index();
|
|
565
|
+
if ($pos.parent.canReplaceWith(index, index + 1, paragraph)) {
|
|
566
|
+
applicable = true;
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
return undefined;
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return applicable;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const CODE_BLOCK_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
576
|
+
languages: Object.freeze(['plaintext']),
|
|
577
|
+
defaultLanguage: 'plaintext',
|
|
578
|
+
languageClassPrefix: 'language-',
|
|
579
|
+
indentText: ' ',
|
|
580
|
+
});
|
|
581
|
+
const CodeBlockPlugin = createConfigurableQalmaPlugin(CODE_BLOCK_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
582
|
+
assertCodeBlockPluginOptions(options);
|
|
583
|
+
const codeBlockNode = {
|
|
584
|
+
attrs: {
|
|
585
|
+
language: { default: options.defaultLanguage },
|
|
586
|
+
},
|
|
587
|
+
content: 'text*',
|
|
588
|
+
marks: '',
|
|
589
|
+
group: 'block',
|
|
590
|
+
code: true,
|
|
591
|
+
defining: true,
|
|
592
|
+
parseDOM: [
|
|
593
|
+
{
|
|
594
|
+
tag: 'pre',
|
|
595
|
+
preserveWhitespace: 'full',
|
|
596
|
+
getAttrs: (node) => ({
|
|
597
|
+
language: resolveParsedLanguage(node, options),
|
|
598
|
+
}),
|
|
599
|
+
},
|
|
600
|
+
],
|
|
601
|
+
toDOM: (node) => [
|
|
602
|
+
'pre',
|
|
603
|
+
[
|
|
604
|
+
'code',
|
|
605
|
+
{
|
|
606
|
+
class: `${options.languageClassPrefix}${resolveNodeLanguage(node.attrs['language'], options)}`,
|
|
607
|
+
},
|
|
608
|
+
0,
|
|
609
|
+
],
|
|
610
|
+
],
|
|
611
|
+
};
|
|
612
|
+
return createQalmaPlugin({
|
|
613
|
+
key: 'codeBlock',
|
|
614
|
+
nodes: {
|
|
615
|
+
codeBlock: codeBlockNode,
|
|
616
|
+
},
|
|
617
|
+
commands: (schema) => ({
|
|
618
|
+
toggleCodeBlock: createToggleCodeBlockCommand(schema.nodes['codeBlock'], schema.nodes['paragraph'], options),
|
|
619
|
+
setCodeBlockLanguage: createSetCodeBlockLanguageCommand(schema.nodes['codeBlock'], options),
|
|
620
|
+
}),
|
|
621
|
+
commandStates: (schema) => ({
|
|
622
|
+
toggleCodeBlock: (state) => isCodeBlockActive(state, schema.nodes['codeBlock']),
|
|
623
|
+
}),
|
|
624
|
+
queries: (schema) => ({
|
|
625
|
+
codeBlockLanguage: (state) => getActiveCodeBlock(state, schema.nodes['codeBlock'])?.node.attrs['language'] ?? null,
|
|
626
|
+
}),
|
|
627
|
+
shortcuts: (schema) => ({
|
|
628
|
+
'Mod-Alt-c': createToggleCodeBlockCommand(schema.nodes['codeBlock'], schema.nodes['paragraph'], options),
|
|
629
|
+
}),
|
|
630
|
+
prosemirrorPlugins: (schema) => [
|
|
631
|
+
createCodeBlockKeyboardPlugin(schema.nodes['codeBlock'], options),
|
|
632
|
+
],
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
const CodeBlockKit = [CodeBlockPlugin];
|
|
636
|
+
function createToggleCodeBlockCommand(codeBlock, paragraph, options) {
|
|
637
|
+
return (state, dispatch, _view, value) => {
|
|
638
|
+
if (isCodeBlockActive(state, codeBlock)) {
|
|
639
|
+
return setBlockType(paragraph)(state, dispatch);
|
|
640
|
+
}
|
|
641
|
+
const language = value === undefined
|
|
642
|
+
? options.defaultLanguage
|
|
643
|
+
: resolveCommandLanguage(value, options);
|
|
644
|
+
if (!language) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
return setBlockType(codeBlock, { language })(state, dispatch);
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function createSetCodeBlockLanguageCommand(codeBlock, options) {
|
|
651
|
+
return (state, dispatch, _view, value) => {
|
|
652
|
+
const language = resolveCommandLanguage(value, options);
|
|
653
|
+
const activeCodeBlock = getActiveCodeBlock(state, codeBlock);
|
|
654
|
+
if (!language || !activeCodeBlock) {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
if (activeCodeBlock.node.attrs['language'] === language) {
|
|
658
|
+
return true;
|
|
659
|
+
}
|
|
660
|
+
if (dispatch) {
|
|
661
|
+
dispatch(state.tr
|
|
662
|
+
.setNodeMarkup(activeCodeBlock.position, codeBlock, {
|
|
663
|
+
...activeCodeBlock.node.attrs,
|
|
664
|
+
language,
|
|
665
|
+
})
|
|
666
|
+
.scrollIntoView());
|
|
667
|
+
}
|
|
668
|
+
return true;
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function isCodeBlockActive(state, codeBlock) {
|
|
672
|
+
return Boolean(getActiveCodeBlock(state, codeBlock));
|
|
673
|
+
}
|
|
674
|
+
function createCodeBlockKeyboardPlugin(codeBlock, options) {
|
|
675
|
+
return new Plugin({
|
|
676
|
+
props: {
|
|
677
|
+
handleKeyDown: (view, event) => {
|
|
678
|
+
if (event.key !== 'Tab') {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
const activeCodeBlock = getActiveCodeBlock(view.state, codeBlock);
|
|
682
|
+
if (!activeCodeBlock) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
event.preventDefault();
|
|
686
|
+
const transaction = event.shiftKey
|
|
687
|
+
? outdentCodeBlockSelection(view.state, activeCodeBlock, options)
|
|
688
|
+
: indentCodeBlockSelection(view.state, activeCodeBlock, options);
|
|
689
|
+
if (transaction) {
|
|
690
|
+
view.dispatch(transaction.scrollIntoView());
|
|
691
|
+
}
|
|
692
|
+
return true;
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function indentCodeBlockSelection(state, activeCodeBlock, options) {
|
|
698
|
+
const contentStart = activeCodeBlock.position + 1;
|
|
699
|
+
const lineStarts = getSelectedLineStarts(state, activeCodeBlock);
|
|
700
|
+
const transaction = state.tr;
|
|
701
|
+
for (const lineStart of [...lineStarts].reverse()) {
|
|
702
|
+
transaction.insertText(options.indentText, contentStart + lineStart);
|
|
703
|
+
}
|
|
704
|
+
return transaction;
|
|
705
|
+
}
|
|
706
|
+
function outdentCodeBlockSelection(state, activeCodeBlock, options) {
|
|
707
|
+
const contentStart = activeCodeBlock.position + 1;
|
|
708
|
+
const lineStarts = getSelectedLineStarts(state, activeCodeBlock);
|
|
709
|
+
const text = activeCodeBlock.node.textContent;
|
|
710
|
+
const transaction = state.tr;
|
|
711
|
+
let changed = false;
|
|
712
|
+
for (const lineStart of [...lineStarts].reverse()) {
|
|
713
|
+
const deleteLength = getOutdentLength(text.slice(lineStart), options);
|
|
714
|
+
if (deleteLength === 0) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
transaction.delete(contentStart + lineStart, contentStart + lineStart + deleteLength);
|
|
718
|
+
changed = true;
|
|
719
|
+
}
|
|
720
|
+
return changed ? transaction : null;
|
|
721
|
+
}
|
|
722
|
+
function getSelectedLineStarts(state, activeCodeBlock) {
|
|
723
|
+
const contentStart = activeCodeBlock.position + 1;
|
|
724
|
+
const text = activeCodeBlock.node.textContent;
|
|
725
|
+
const selectionFrom = Math.max(0, state.selection.from - contentStart);
|
|
726
|
+
const selectionTo = Math.max(0, state.selection.to - contentStart);
|
|
727
|
+
const firstLineStart = getLineStart(text, selectionFrom);
|
|
728
|
+
const lastSelectedOffset = state.selection.empty || selectionTo === 0
|
|
729
|
+
? selectionFrom
|
|
730
|
+
: selectionTo - 1;
|
|
731
|
+
const lastLineStart = getLineStart(text, lastSelectedOffset);
|
|
732
|
+
const lineStarts = [firstLineStart];
|
|
733
|
+
let nextLineBreak = text.indexOf('\n', firstLineStart);
|
|
734
|
+
while (nextLineBreak !== -1 && nextLineBreak + 1 <= lastLineStart) {
|
|
735
|
+
lineStarts.push(nextLineBreak + 1);
|
|
736
|
+
nextLineBreak = text.indexOf('\n', nextLineBreak + 1);
|
|
737
|
+
}
|
|
738
|
+
return lineStarts;
|
|
739
|
+
}
|
|
740
|
+
function getLineStart(text, offset) {
|
|
741
|
+
return text.lastIndexOf('\n', offset - 1) + 1;
|
|
742
|
+
}
|
|
743
|
+
function getOutdentLength(line, options) {
|
|
744
|
+
if (line.startsWith(options.indentText)) {
|
|
745
|
+
return options.indentText.length;
|
|
746
|
+
}
|
|
747
|
+
if (line.startsWith('\t')) {
|
|
748
|
+
return 1;
|
|
749
|
+
}
|
|
750
|
+
const leadingSpaces = line.match(/^ +/)?.[0].length ?? 0;
|
|
751
|
+
return Math.min(leadingSpaces, options.indentText.length);
|
|
752
|
+
}
|
|
753
|
+
function getActiveCodeBlock(state, codeBlock) {
|
|
754
|
+
const { $from, $to } = state.selection;
|
|
755
|
+
if ($from.parent.type !== codeBlock ||
|
|
756
|
+
$to.parent.type !== codeBlock ||
|
|
757
|
+
$from.depth === 0) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
return {
|
|
761
|
+
node: $from.parent,
|
|
762
|
+
position: $from.before($from.depth),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
function resolveParsedLanguage(node, options) {
|
|
766
|
+
if (!(node instanceof HTMLElement)) {
|
|
767
|
+
return options.defaultLanguage;
|
|
768
|
+
}
|
|
769
|
+
const code = node.matches('code') ? node : node.querySelector('code');
|
|
770
|
+
const language = resolveLanguageFromElement(code, options) ??
|
|
771
|
+
resolveLanguageFromElement(node, options);
|
|
772
|
+
return language ?? options.defaultLanguage;
|
|
773
|
+
}
|
|
774
|
+
function resolveLanguageFromElement(element, options) {
|
|
775
|
+
if (!element) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
const classNames = Array.from(element.classList);
|
|
779
|
+
const prefixes = Array.from(new Set([options.languageClassPrefix, 'language-', 'lang-']));
|
|
780
|
+
for (const className of classNames) {
|
|
781
|
+
for (const prefix of prefixes) {
|
|
782
|
+
if (className.startsWith(prefix)) {
|
|
783
|
+
return resolveLanguageId(className.slice(prefix.length), options);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
function resolveNodeLanguage(language, options) {
|
|
790
|
+
return resolveLanguageId(language, options) ?? options.defaultLanguage;
|
|
791
|
+
}
|
|
792
|
+
function resolveCommandLanguage(value, options) {
|
|
793
|
+
return resolveLanguageId(value, options);
|
|
794
|
+
}
|
|
795
|
+
function resolveLanguageId(value, options) {
|
|
796
|
+
if (typeof value !== 'string') {
|
|
797
|
+
return null;
|
|
798
|
+
}
|
|
799
|
+
const language = value.trim().toLowerCase();
|
|
800
|
+
return options.languages.includes(language) ? language : null;
|
|
801
|
+
}
|
|
802
|
+
function assertCodeBlockPluginOptions(options) {
|
|
803
|
+
if (!Array.isArray(options.languages)) {
|
|
804
|
+
throw new TypeError('CodeBlockPlugin languages must be an array.');
|
|
805
|
+
}
|
|
806
|
+
if (options.languages.length === 0) {
|
|
807
|
+
throw new RangeError('CodeBlockPlugin languages must include at least one language.');
|
|
808
|
+
}
|
|
809
|
+
const seen = new Set();
|
|
810
|
+
for (const language of options.languages) {
|
|
811
|
+
assertLanguageId(language, 'CodeBlockPlugin languages entries');
|
|
812
|
+
if (seen.has(language)) {
|
|
813
|
+
throw new Error('CodeBlockPlugin languages entries must be unique.');
|
|
814
|
+
}
|
|
815
|
+
seen.add(language);
|
|
816
|
+
}
|
|
817
|
+
assertLanguageId(options.defaultLanguage, 'CodeBlockPlugin defaultLanguage');
|
|
818
|
+
if (!seen.has(options.defaultLanguage)) {
|
|
819
|
+
throw new Error('CodeBlockPlugin defaultLanguage must be included in languages.');
|
|
820
|
+
}
|
|
821
|
+
if (typeof options.languageClassPrefix !== 'string' ||
|
|
822
|
+
options.languageClassPrefix.trim() !== options.languageClassPrefix ||
|
|
823
|
+
options.languageClassPrefix.length === 0 ||
|
|
824
|
+
/\s/.test(options.languageClassPrefix)) {
|
|
825
|
+
throw new Error('CodeBlockPlugin languageClassPrefix must be a non-empty string without whitespace.');
|
|
826
|
+
}
|
|
827
|
+
if (typeof options.indentText !== 'string' ||
|
|
828
|
+
options.indentText.length === 0 ||
|
|
829
|
+
!/^[\t ]+$/.test(options.indentText)) {
|
|
830
|
+
throw new Error('CodeBlockPlugin indentText must be a non-empty string containing only spaces or tabs.');
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
function assertLanguageId(value, label) {
|
|
834
|
+
if (typeof value !== 'string' ||
|
|
835
|
+
value.trim() !== value ||
|
|
836
|
+
value.toLowerCase() !== value ||
|
|
837
|
+
!/^[a-z][a-z0-9-]*$/.test(value)) {
|
|
838
|
+
throw new Error(`${label} must be lowercase language identifiers without whitespace.`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const ColorPlugin = createQalmaPlugin({
|
|
843
|
+
key: 'color',
|
|
844
|
+
marks: {
|
|
845
|
+
textStyle: createTextStyleMark(),
|
|
846
|
+
},
|
|
847
|
+
commands: (schema) => ({
|
|
848
|
+
setTextColor: createSetTextStyleCommand(schema.marks['textStyle'], 'color'),
|
|
849
|
+
unsetTextColor: createUnsetTextStyleCommand(schema.marks['textStyle'], 'color'),
|
|
850
|
+
setBackgroundColor: createSetTextStyleCommand(schema.marks['textStyle'], 'backgroundColor'),
|
|
851
|
+
unsetBackgroundColor: createUnsetTextStyleCommand(schema.marks['textStyle'], 'backgroundColor'),
|
|
852
|
+
}),
|
|
853
|
+
commandStates: (schema) => ({
|
|
854
|
+
unsetTextColor: (state) => isTextStyleAttributeActive(state, schema.marks['textStyle'], 'color'),
|
|
855
|
+
unsetBackgroundColor: (state) => isTextStyleAttributeActive(state, schema.marks['textStyle'], 'backgroundColor'),
|
|
856
|
+
}),
|
|
857
|
+
queries: (schema) => ({
|
|
858
|
+
textColor: (state) => getActiveTextStyleAttrs(state, schema.marks['textStyle'])?.color ?? null,
|
|
859
|
+
backgroundColor: (state) => getActiveTextStyleAttrs(state, schema.marks['textStyle'])
|
|
860
|
+
?.backgroundColor ?? null,
|
|
861
|
+
}),
|
|
862
|
+
});
|
|
863
|
+
const ColorKit = [ColorPlugin];
|
|
864
|
+
function createTextStyleMark() {
|
|
865
|
+
return {
|
|
866
|
+
attrs: {
|
|
867
|
+
color: { default: null },
|
|
868
|
+
backgroundColor: { default: null },
|
|
869
|
+
},
|
|
870
|
+
parseDOM: [
|
|
871
|
+
{
|
|
872
|
+
tag: '[style]',
|
|
873
|
+
getAttrs: (node) => {
|
|
874
|
+
if (!(node instanceof HTMLElement)) {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
return getTextStyleAttrsFromElement(node) ?? false;
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
tag: 'font[color]',
|
|
882
|
+
getAttrs: (node) => {
|
|
883
|
+
if (!(node instanceof HTMLElement)) {
|
|
884
|
+
return false;
|
|
885
|
+
}
|
|
886
|
+
const color = normalizeCssColor$1(node.getAttribute('color'), 'color');
|
|
887
|
+
return color ? { color, backgroundColor: null } : false;
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
],
|
|
891
|
+
toDOM: (mark) => [
|
|
892
|
+
'span',
|
|
893
|
+
{
|
|
894
|
+
style: createTextStyleAttribute(getTextStyleAttrs(mark)),
|
|
895
|
+
},
|
|
896
|
+
0,
|
|
897
|
+
],
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function createSetTextStyleCommand(mark, attribute) {
|
|
901
|
+
return (state, dispatch, _view, value) => {
|
|
902
|
+
const color = normalizeCommandColor$1(value, getCssPropertyName(attribute));
|
|
903
|
+
if (!color || !markApplies$1(state, mark)) {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
if (dispatch) {
|
|
907
|
+
dispatch(updateTextStyleMarks(state, mark, (attrs) => ({
|
|
908
|
+
...attrs,
|
|
909
|
+
[attribute]: color,
|
|
910
|
+
})).scrollIntoView());
|
|
911
|
+
}
|
|
912
|
+
return true;
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function createUnsetTextStyleCommand(mark, attribute) {
|
|
916
|
+
return (state, dispatch) => {
|
|
917
|
+
if (!isTextStyleAttributeActive(state, mark, attribute)) {
|
|
918
|
+
return false;
|
|
919
|
+
}
|
|
920
|
+
if (dispatch) {
|
|
921
|
+
dispatch(updateTextStyleMarks(state, mark, (attrs) => ({
|
|
922
|
+
...attrs,
|
|
923
|
+
[attribute]: null,
|
|
924
|
+
})).scrollIntoView());
|
|
925
|
+
}
|
|
926
|
+
return true;
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function updateTextStyleMarks(state, mark, updateAttrs) {
|
|
930
|
+
if (state.selection.empty) {
|
|
931
|
+
return updateStoredTextStyleMark(state, mark, updateAttrs);
|
|
932
|
+
}
|
|
933
|
+
const transaction = state.tr;
|
|
934
|
+
for (const range of state.selection.ranges) {
|
|
935
|
+
state.doc.nodesBetween(range.$from.pos, range.$to.pos, (node, position) => {
|
|
936
|
+
if (!node.isText || position >= range.$to.pos) {
|
|
937
|
+
return undefined;
|
|
938
|
+
}
|
|
939
|
+
const from = Math.max(position, range.$from.pos);
|
|
940
|
+
const to = Math.min(position + node.nodeSize, range.$to.pos);
|
|
941
|
+
const currentMark = mark.isInSet(node.marks);
|
|
942
|
+
const nextAttrs = updateAttrs(getTextStyleAttrs(currentMark));
|
|
943
|
+
transaction.removeMark(from, to, mark);
|
|
944
|
+
if (hasTextStyleAttrs(nextAttrs)) {
|
|
945
|
+
transaction.addMark(from, to, mark.create(nextAttrs));
|
|
946
|
+
}
|
|
947
|
+
return undefined;
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
return transaction;
|
|
951
|
+
}
|
|
952
|
+
function updateStoredTextStyleMark(state, mark, updateAttrs) {
|
|
953
|
+
const marks = state.storedMarks ?? state.selection.$from.marks();
|
|
954
|
+
const nextAttrs = updateAttrs(getTextStyleAttrs(mark.isInSet(marks)));
|
|
955
|
+
const transaction = state.tr.removeStoredMark(mark);
|
|
956
|
+
if (hasTextStyleAttrs(nextAttrs)) {
|
|
957
|
+
transaction.addStoredMark(mark.create(nextAttrs));
|
|
958
|
+
}
|
|
959
|
+
return transaction;
|
|
960
|
+
}
|
|
961
|
+
function isTextStyleAttributeActive(state, mark, attribute) {
|
|
962
|
+
const attrs = getActiveTextStyleAttrs(state, mark);
|
|
963
|
+
return Boolean(attrs?.[attribute]);
|
|
964
|
+
}
|
|
965
|
+
function getActiveTextStyleAttrs(state, mark) {
|
|
966
|
+
if (state.selection.empty) {
|
|
967
|
+
const activeMark = mark.isInSet(state.storedMarks ?? state.selection.$from.marks());
|
|
968
|
+
return activeMark ? getTextStyleAttrs(activeMark) : null;
|
|
969
|
+
}
|
|
970
|
+
let activeMark = null;
|
|
971
|
+
state.doc.nodesBetween(state.selection.from, state.selection.to, (node) => {
|
|
972
|
+
if (activeMark) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
if (node.isText) {
|
|
976
|
+
activeMark = mark.isInSet(node.marks) ?? null;
|
|
977
|
+
}
|
|
978
|
+
return undefined;
|
|
979
|
+
});
|
|
980
|
+
return activeMark ? getTextStyleAttrs(activeMark) : null;
|
|
981
|
+
}
|
|
982
|
+
function getTextStyleAttrs(mark) {
|
|
983
|
+
return {
|
|
984
|
+
color: normalizeCssColor$1(mark?.attrs['color'], 'color'),
|
|
985
|
+
backgroundColor: normalizeCssColor$1(mark?.attrs['backgroundColor'], 'background-color'),
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function getTextStyleAttrsFromElement(element) {
|
|
989
|
+
const attrs = {
|
|
990
|
+
color: normalizeCssColor$1(element.style.color, 'color'),
|
|
991
|
+
backgroundColor: normalizeCssColor$1(element.style.backgroundColor, 'background-color'),
|
|
992
|
+
};
|
|
993
|
+
return hasTextStyleAttrs(attrs) ? attrs : null;
|
|
994
|
+
}
|
|
995
|
+
function createTextStyleAttribute(attrs) {
|
|
996
|
+
return [
|
|
997
|
+
attrs.color ? `color: ${attrs.color};` : '',
|
|
998
|
+
attrs.backgroundColor ? `background-color: ${attrs.backgroundColor};` : '',
|
|
999
|
+
]
|
|
1000
|
+
.filter(Boolean)
|
|
1001
|
+
.join(' ');
|
|
1002
|
+
}
|
|
1003
|
+
function hasTextStyleAttrs(attrs) {
|
|
1004
|
+
return Boolean(attrs.color || attrs.backgroundColor);
|
|
1005
|
+
}
|
|
1006
|
+
function normalizeCommandColor$1(value, cssPropertyName) {
|
|
1007
|
+
return typeof value === 'string'
|
|
1008
|
+
? normalizeCssColor$1(value, cssPropertyName)
|
|
1009
|
+
: null;
|
|
1010
|
+
}
|
|
1011
|
+
function normalizeCssColor$1(value, cssPropertyName) {
|
|
1012
|
+
if (typeof value !== 'string') {
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
const color = value.trim();
|
|
1016
|
+
if (!color || /[;{}<>]/.test(color)) {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
if (typeof document === 'undefined') {
|
|
1020
|
+
return color;
|
|
1021
|
+
}
|
|
1022
|
+
const element = document.createElement('span');
|
|
1023
|
+
element.style.setProperty(cssPropertyName, color);
|
|
1024
|
+
return element.style.getPropertyValue(cssPropertyName) || null;
|
|
1025
|
+
}
|
|
1026
|
+
function getCssPropertyName(attribute) {
|
|
1027
|
+
return attribute === 'color' ? 'color' : 'background-color';
|
|
1028
|
+
}
|
|
1029
|
+
function markApplies$1(state, mark) {
|
|
1030
|
+
if (state.selection.empty) {
|
|
1031
|
+
const { $from } = state.selection;
|
|
1032
|
+
return $from.parent.inlineContent && $from.parent.type.allowsMarkType(mark);
|
|
1033
|
+
}
|
|
1034
|
+
for (const range of state.selection.ranges) {
|
|
1035
|
+
let applies = false;
|
|
1036
|
+
state.doc.nodesBetween(range.$from.pos, range.$to.pos, (node) => {
|
|
1037
|
+
if (applies) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
applies = node.inlineContent && node.type.allowsMarkType(mark);
|
|
1041
|
+
return undefined;
|
|
1042
|
+
});
|
|
1043
|
+
if (applies) {
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
const hardBreakNode = {
|
|
1051
|
+
inline: true,
|
|
1052
|
+
group: 'inline',
|
|
1053
|
+
selectable: false,
|
|
1054
|
+
linebreakReplacement: true,
|
|
1055
|
+
parseDOM: [{ tag: 'br' }],
|
|
1056
|
+
toDOM: () => ['br'],
|
|
1057
|
+
leafText: () => '\n',
|
|
1058
|
+
};
|
|
1059
|
+
const HardBreakPlugin = createQalmaPlugin({
|
|
1060
|
+
key: 'hardBreak',
|
|
1061
|
+
nodes: {
|
|
1062
|
+
hardBreak: hardBreakNode,
|
|
1063
|
+
},
|
|
1064
|
+
commands: (schema) => ({
|
|
1065
|
+
insertHardBreak: createInsertHardBreakCommand(schema.nodes['hardBreak']),
|
|
1066
|
+
}),
|
|
1067
|
+
shortcuts: (schema) => ({
|
|
1068
|
+
'Shift-Enter': createInsertHardBreakCommand(schema.nodes['hardBreak']),
|
|
1069
|
+
}),
|
|
1070
|
+
});
|
|
1071
|
+
const HardBreakKit = [HardBreakPlugin];
|
|
1072
|
+
function createInsertHardBreakCommand(hardBreak) {
|
|
1073
|
+
return (state, dispatch) => {
|
|
1074
|
+
if (!canInsertHardBreak(state, hardBreak)) {
|
|
1075
|
+
return false;
|
|
1076
|
+
}
|
|
1077
|
+
if (dispatch) {
|
|
1078
|
+
dispatch(state.tr.replaceSelectionWith(hardBreak.create()).scrollIntoView());
|
|
1079
|
+
}
|
|
1080
|
+
return true;
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
function canInsertHardBreak(state, hardBreak) {
|
|
1084
|
+
const { $from, $to } = state.selection;
|
|
1085
|
+
return ($from.sameParent($to) &&
|
|
1086
|
+
$from.parent.inlineContent &&
|
|
1087
|
+
$from.parent.canReplaceWith($from.index(), $to.index(), hardBreak));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const HIGHLIGHT_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
1091
|
+
defaultColor: 'rgb(254, 240, 138)',
|
|
1092
|
+
});
|
|
1093
|
+
const HighlightPlugin = createConfigurableQalmaPlugin(HIGHLIGHT_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
1094
|
+
const defaultColor = normalizeCssColor(options.defaultColor, 'background-color');
|
|
1095
|
+
assertDefaultHighlightColor(defaultColor);
|
|
1096
|
+
return createQalmaPlugin({
|
|
1097
|
+
key: 'highlight',
|
|
1098
|
+
marks: {
|
|
1099
|
+
highlight: createHighlightMark(defaultColor),
|
|
1100
|
+
},
|
|
1101
|
+
commands: (schema) => ({
|
|
1102
|
+
setHighlight: createSetHighlightCommand(schema.marks['highlight'], defaultColor),
|
|
1103
|
+
unsetHighlight: createUnsetHighlightCommand(schema.marks['highlight']),
|
|
1104
|
+
}),
|
|
1105
|
+
commandStates: (schema) => ({
|
|
1106
|
+
setHighlight: (state) => isHighlightActive(state, schema.marks['highlight']),
|
|
1107
|
+
}),
|
|
1108
|
+
queries: (schema) => ({
|
|
1109
|
+
highlightColor: (state) => getActiveHighlightColor(state, schema.marks['highlight']),
|
|
1110
|
+
}),
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
const HighlightKit = [HighlightPlugin];
|
|
1114
|
+
function createHighlightMark(defaultColor) {
|
|
1115
|
+
return {
|
|
1116
|
+
attrs: {
|
|
1117
|
+
color: { default: defaultColor },
|
|
1118
|
+
},
|
|
1119
|
+
parseDOM: [
|
|
1120
|
+
{
|
|
1121
|
+
tag: 'mark',
|
|
1122
|
+
getAttrs: (node) => {
|
|
1123
|
+
if (!(node instanceof HTMLElement)) {
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
color: normalizeCssColor(node.style.backgroundColor, 'background-color') ?? defaultColor,
|
|
1128
|
+
};
|
|
1129
|
+
},
|
|
1130
|
+
},
|
|
1131
|
+
],
|
|
1132
|
+
toDOM: (mark) => {
|
|
1133
|
+
const color = getHighlightColor(mark, defaultColor);
|
|
1134
|
+
return color === defaultColor
|
|
1135
|
+
? ['mark', 0]
|
|
1136
|
+
: ['mark', { style: `background-color: ${color};` }, 0];
|
|
1137
|
+
},
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function createSetHighlightCommand(mark, defaultColor) {
|
|
1141
|
+
return (state, dispatch, _view, value) => {
|
|
1142
|
+
const color = value === undefined
|
|
1143
|
+
? defaultColor
|
|
1144
|
+
: normalizeCommandColor(value, 'background-color');
|
|
1145
|
+
if (!color || !markApplies(state, mark)) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
if (dispatch) {
|
|
1149
|
+
dispatch(updateHighlightMarks(state, mark, () => color).scrollIntoView());
|
|
1150
|
+
}
|
|
1151
|
+
return true;
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function createUnsetHighlightCommand(mark) {
|
|
1155
|
+
return (state, dispatch) => {
|
|
1156
|
+
if (!isHighlightActive(state, mark)) {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
if (dispatch) {
|
|
1160
|
+
dispatch(updateHighlightMarks(state, mark, () => null).scrollIntoView());
|
|
1161
|
+
}
|
|
1162
|
+
return true;
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
function updateHighlightMarks(state, mark, getNextColor) {
|
|
1166
|
+
if (state.selection.empty) {
|
|
1167
|
+
return updateStoredHighlightMark(state, mark, getNextColor);
|
|
1168
|
+
}
|
|
1169
|
+
const transaction = state.tr;
|
|
1170
|
+
for (const range of state.selection.ranges) {
|
|
1171
|
+
state.doc.nodesBetween(range.$from.pos, range.$to.pos, (node, position) => {
|
|
1172
|
+
if (!node.isText || position >= range.$to.pos) {
|
|
1173
|
+
return undefined;
|
|
1174
|
+
}
|
|
1175
|
+
const from = Math.max(position, range.$from.pos);
|
|
1176
|
+
const to = Math.min(position + node.nodeSize, range.$to.pos);
|
|
1177
|
+
const color = getNextColor();
|
|
1178
|
+
transaction.removeMark(from, to, mark);
|
|
1179
|
+
if (color) {
|
|
1180
|
+
transaction.addMark(from, to, mark.create({ color }));
|
|
1181
|
+
}
|
|
1182
|
+
return undefined;
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
return transaction;
|
|
1186
|
+
}
|
|
1187
|
+
function updateStoredHighlightMark(state, mark, getNextColor) {
|
|
1188
|
+
const color = getNextColor();
|
|
1189
|
+
const transaction = state.tr.removeStoredMark(mark);
|
|
1190
|
+
if (color) {
|
|
1191
|
+
transaction.addStoredMark(mark.create({ color }));
|
|
1192
|
+
}
|
|
1193
|
+
return transaction;
|
|
1194
|
+
}
|
|
1195
|
+
function isHighlightActive(state, mark) {
|
|
1196
|
+
return Boolean(getActiveHighlightMark(state, mark));
|
|
1197
|
+
}
|
|
1198
|
+
function getActiveHighlightColor(state, mark) {
|
|
1199
|
+
return getActiveHighlightMark(state, mark)?.attrs['color'] ?? null;
|
|
1200
|
+
}
|
|
1201
|
+
function getActiveHighlightMark(state, mark) {
|
|
1202
|
+
if (state.selection.empty) {
|
|
1203
|
+
return (mark.isInSet(state.storedMarks ?? state.selection.$from.marks()) ?? null);
|
|
1204
|
+
}
|
|
1205
|
+
let activeMark = null;
|
|
1206
|
+
state.doc.nodesBetween(state.selection.from, state.selection.to, (node) => {
|
|
1207
|
+
if (activeMark) {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
if (node.isText) {
|
|
1211
|
+
activeMark = mark.isInSet(node.marks) ?? null;
|
|
1212
|
+
}
|
|
1213
|
+
return undefined;
|
|
1214
|
+
});
|
|
1215
|
+
return activeMark;
|
|
1216
|
+
}
|
|
1217
|
+
function getHighlightColor(mark, defaultColor) {
|
|
1218
|
+
return (normalizeCssColor(mark.attrs['color'], 'background-color') ?? defaultColor);
|
|
1219
|
+
}
|
|
1220
|
+
function normalizeCommandColor(value, cssPropertyName) {
|
|
1221
|
+
return typeof value === 'string'
|
|
1222
|
+
? normalizeCssColor(value, cssPropertyName)
|
|
1223
|
+
: null;
|
|
1224
|
+
}
|
|
1225
|
+
function normalizeCssColor(value, cssPropertyName) {
|
|
1226
|
+
if (typeof value !== 'string') {
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
const color = value.trim();
|
|
1230
|
+
if (!color || /[;{}<>]/.test(color)) {
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
if (typeof document === 'undefined') {
|
|
1234
|
+
return color;
|
|
1235
|
+
}
|
|
1236
|
+
const element = document.createElement('span');
|
|
1237
|
+
element.style.setProperty(cssPropertyName, color);
|
|
1238
|
+
return element.style.getPropertyValue(cssPropertyName) || null;
|
|
1239
|
+
}
|
|
1240
|
+
function markApplies(state, mark) {
|
|
1241
|
+
if (state.selection.empty) {
|
|
1242
|
+
const { $from } = state.selection;
|
|
1243
|
+
return $from.parent.inlineContent && $from.parent.type.allowsMarkType(mark);
|
|
1244
|
+
}
|
|
1245
|
+
for (const range of state.selection.ranges) {
|
|
1246
|
+
let applies = false;
|
|
1247
|
+
state.doc.nodesBetween(range.$from.pos, range.$to.pos, (node) => {
|
|
1248
|
+
if (applies) {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
applies = node.inlineContent && node.type.allowsMarkType(mark);
|
|
1252
|
+
return undefined;
|
|
1253
|
+
});
|
|
1254
|
+
if (applies) {
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
function assertDefaultHighlightColor(defaultColor) {
|
|
1261
|
+
if (!defaultColor) {
|
|
1262
|
+
throw new Error('HighlightPlugin defaultColor must be a valid CSS color.');
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const HISTORY_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
1267
|
+
depth: 100,
|
|
1268
|
+
newGroupDelay: 500,
|
|
1269
|
+
});
|
|
1270
|
+
const HistoryPlugin = createConfigurableQalmaPlugin(HISTORY_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
1271
|
+
assertHistoryPluginOptions(options);
|
|
1272
|
+
return createQalmaPlugin({
|
|
1273
|
+
key: 'history',
|
|
1274
|
+
prosemirrorPlugins: () => [history(options)],
|
|
1275
|
+
commands: () => ({
|
|
1276
|
+
undo,
|
|
1277
|
+
redo,
|
|
1278
|
+
}),
|
|
1279
|
+
shortcuts: () => ({
|
|
1280
|
+
'Mod-z': undo,
|
|
1281
|
+
'Shift-Mod-z': redo,
|
|
1282
|
+
'Mod-y': redo,
|
|
1283
|
+
}),
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
function assertHistoryPluginOptions(options) {
|
|
1287
|
+
if (!Number.isInteger(options.depth) || options.depth < 1) {
|
|
1288
|
+
throw new RangeError('HistoryPlugin depth must be a positive integer.');
|
|
1289
|
+
}
|
|
1290
|
+
if (!Number.isFinite(options.newGroupDelay) || options.newGroupDelay < 0) {
|
|
1291
|
+
throw new RangeError('HistoryPlugin newGroupDelay must be a non-negative finite number.');
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const HEADING_LEVELS = Object.freeze([
|
|
1296
|
+
1, 2, 3, 4, 5, 6,
|
|
1297
|
+
]);
|
|
1298
|
+
const HEADINGS_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
1299
|
+
levels: Object.freeze([1, 2, 3]),
|
|
1300
|
+
});
|
|
1301
|
+
const HeadingsPlugin = createConfigurableQalmaPlugin(HEADINGS_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
1302
|
+
assertHeadingsPluginOptions(options);
|
|
1303
|
+
const headingNode = {
|
|
1304
|
+
attrs: {
|
|
1305
|
+
level: { default: options.levels[0] },
|
|
1306
|
+
},
|
|
1307
|
+
content: 'inline*',
|
|
1308
|
+
defining: true,
|
|
1309
|
+
group: 'block',
|
|
1310
|
+
parseDOM: options.levels.map((level) => ({
|
|
1311
|
+
tag: `h${level}`,
|
|
1312
|
+
attrs: { level },
|
|
1313
|
+
})),
|
|
1314
|
+
toDOM: (node) => [`h${node.attrs['level']}`, 0],
|
|
1315
|
+
};
|
|
1316
|
+
return createQalmaPlugin({
|
|
1317
|
+
key: 'headings',
|
|
1318
|
+
nodes: {
|
|
1319
|
+
heading: headingNode,
|
|
1320
|
+
},
|
|
1321
|
+
commands: (schema) => ({
|
|
1322
|
+
setParagraph: createSetParagraphCommand(schema.nodes['paragraph']),
|
|
1323
|
+
...createHeadingCommands(schema.nodes['heading'], schema.nodes['paragraph'], options.levels),
|
|
1324
|
+
}),
|
|
1325
|
+
commandStates: (schema) => ({
|
|
1326
|
+
setParagraph: (state) => isParagraphActive(state),
|
|
1327
|
+
...createHeadingCommandStates(schema.nodes['heading'], options.levels),
|
|
1328
|
+
}),
|
|
1329
|
+
shortcuts: (schema) => createHeadingShortcuts(schema, options.levels),
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
const HeadingsKit = [HeadingsPlugin];
|
|
1333
|
+
function createSetParagraphCommand(paragraph) {
|
|
1334
|
+
return (state, dispatch) => {
|
|
1335
|
+
if (isParagraphActive(state)) {
|
|
1336
|
+
return true;
|
|
1337
|
+
}
|
|
1338
|
+
return setBlockType(paragraph)(state, dispatch);
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
function createHeadingCommands(heading, paragraph, levels) {
|
|
1342
|
+
return Object.fromEntries(levels.map((level) => [
|
|
1343
|
+
getHeadingCommandName(level),
|
|
1344
|
+
createToggleHeadingCommand(heading, paragraph, level),
|
|
1345
|
+
]));
|
|
1346
|
+
}
|
|
1347
|
+
function createToggleHeadingCommand(heading, paragraph, level) {
|
|
1348
|
+
return (state, dispatch) => {
|
|
1349
|
+
if (isHeadingActive(state, heading, level)) {
|
|
1350
|
+
return setBlockType(paragraph)(state, dispatch);
|
|
1351
|
+
}
|
|
1352
|
+
return setBlockType(heading, { level })(state, dispatch);
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function createHeadingCommandStates(heading, levels) {
|
|
1356
|
+
return Object.fromEntries(levels.map((level) => [
|
|
1357
|
+
getHeadingCommandName(level),
|
|
1358
|
+
(state) => isHeadingActive(state, heading, level),
|
|
1359
|
+
]));
|
|
1360
|
+
}
|
|
1361
|
+
function createHeadingShortcuts(schema, levels) {
|
|
1362
|
+
return Object.fromEntries(levels.map((level) => [
|
|
1363
|
+
`Mod-Alt-${level}`,
|
|
1364
|
+
createToggleHeadingCommand(schema.nodes['heading'], schema.nodes['paragraph'], level),
|
|
1365
|
+
]));
|
|
1366
|
+
}
|
|
1367
|
+
function getHeadingCommandName(level) {
|
|
1368
|
+
return `toggleHeading${level}`;
|
|
1369
|
+
}
|
|
1370
|
+
function isParagraphActive(state) {
|
|
1371
|
+
return state.selection.$from.parent.type === state.schema.nodes['paragraph'];
|
|
1372
|
+
}
|
|
1373
|
+
function isHeadingActive(state, heading, level) {
|
|
1374
|
+
const parent = state.selection.$from.parent;
|
|
1375
|
+
return parent.type === heading && parent.attrs['level'] === level;
|
|
1376
|
+
}
|
|
1377
|
+
function assertHeadingsPluginOptions(options) {
|
|
1378
|
+
if (!Array.isArray(options.levels)) {
|
|
1379
|
+
throw new TypeError('HeadingsPlugin levels must be an array.');
|
|
1380
|
+
}
|
|
1381
|
+
if (options.levels.length === 0) {
|
|
1382
|
+
throw new RangeError('HeadingsPlugin levels must include at least one heading level.');
|
|
1383
|
+
}
|
|
1384
|
+
const seen = new Set();
|
|
1385
|
+
for (const level of options.levels) {
|
|
1386
|
+
if (!HEADING_LEVELS.includes(level)) {
|
|
1387
|
+
throw new RangeError('HeadingsPlugin levels entries must be integers from 1 through 6.');
|
|
1388
|
+
}
|
|
1389
|
+
if (seen.has(level)) {
|
|
1390
|
+
throw new Error('HeadingsPlugin levels entries must be unique.');
|
|
1391
|
+
}
|
|
1392
|
+
seen.add(level);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const IMAGE_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
1397
|
+
allowedProtocols: Object.freeze(['http', 'https']),
|
|
1398
|
+
allowRelativeImages: true,
|
|
1399
|
+
defaultAlt: '',
|
|
1400
|
+
});
|
|
1401
|
+
const ImagePlugin = createConfigurableQalmaPlugin(IMAGE_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
1402
|
+
assertImagePluginOptions(options);
|
|
1403
|
+
const imageNode = {
|
|
1404
|
+
attrs: {
|
|
1405
|
+
src: {},
|
|
1406
|
+
alt: { default: options.defaultAlt },
|
|
1407
|
+
title: { default: null },
|
|
1408
|
+
previewSrc: { default: null },
|
|
1409
|
+
},
|
|
1410
|
+
atom: true,
|
|
1411
|
+
draggable: true,
|
|
1412
|
+
inline: true,
|
|
1413
|
+
group: 'inline',
|
|
1414
|
+
parseDOM: [
|
|
1415
|
+
{
|
|
1416
|
+
tag: 'img[src]',
|
|
1417
|
+
getAttrs: (node) => {
|
|
1418
|
+
if (!(node instanceof HTMLElement)) {
|
|
1419
|
+
return false;
|
|
1420
|
+
}
|
|
1421
|
+
const src = normalizeImageSrc(node.getAttribute('src'), options);
|
|
1422
|
+
if (!src) {
|
|
1423
|
+
return false;
|
|
1424
|
+
}
|
|
1425
|
+
return {
|
|
1426
|
+
src,
|
|
1427
|
+
alt: normalizeAlt(node.getAttribute('alt') ?? options.defaultAlt),
|
|
1428
|
+
title: normalizeTitle(node.getAttribute('title')),
|
|
1429
|
+
previewSrc: null,
|
|
1430
|
+
};
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
],
|
|
1434
|
+
selectable: true,
|
|
1435
|
+
toDOM: (node) => {
|
|
1436
|
+
const attrs = {
|
|
1437
|
+
src: node.attrs['src'],
|
|
1438
|
+
alt: normalizeAlt(node.attrs['alt']),
|
|
1439
|
+
};
|
|
1440
|
+
const title = normalizeTitle(node.attrs['title']);
|
|
1441
|
+
if (title) {
|
|
1442
|
+
attrs['title'] = title;
|
|
1443
|
+
}
|
|
1444
|
+
return ['img', attrs];
|
|
1445
|
+
},
|
|
1446
|
+
};
|
|
1447
|
+
return createQalmaPlugin({
|
|
1448
|
+
key: 'image',
|
|
1449
|
+
nodes: {
|
|
1450
|
+
image: imageNode,
|
|
1451
|
+
},
|
|
1452
|
+
commands: (schema) => ({
|
|
1453
|
+
insertImage: createInsertImageCommand(schema.nodes['image'], options),
|
|
1454
|
+
updateImage: createUpdateImageCommand(schema.nodes['image'], options),
|
|
1455
|
+
removeImage: createRemoveImageCommand(schema.nodes['image']),
|
|
1456
|
+
}),
|
|
1457
|
+
commandStates: (schema) => ({
|
|
1458
|
+
insertImage: (state) => Boolean(getSelectedImage(state, schema.nodes['image'])),
|
|
1459
|
+
}),
|
|
1460
|
+
queries: (schema) => ({
|
|
1461
|
+
image: (state) => getImageState(state, schema.nodes['image']),
|
|
1462
|
+
}),
|
|
1463
|
+
prosemirrorPlugins: () => [createImagePreviewPlugin()],
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
const ImageKit = [ImagePlugin];
|
|
1467
|
+
function createInsertImageCommand(image, options) {
|
|
1468
|
+
return (state, dispatch, _view, value) => {
|
|
1469
|
+
const attrs = resolveImageAttrs(value, options);
|
|
1470
|
+
if (!attrs || !canReplaceSelectionWithImage(state, image)) {
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
if (dispatch) {
|
|
1474
|
+
const imageNode = image.create(attrs);
|
|
1475
|
+
const transaction = state.tr.replaceSelectionWith(imageNode, false);
|
|
1476
|
+
const insertedPosition = transaction.selection.from - imageNode.nodeSize;
|
|
1477
|
+
if (transaction.doc.nodeAt(insertedPosition)?.type === image) {
|
|
1478
|
+
transaction.setSelection(NodeSelection.create(transaction.doc, insertedPosition));
|
|
1479
|
+
}
|
|
1480
|
+
dispatch(transaction.scrollIntoView());
|
|
1481
|
+
}
|
|
1482
|
+
return true;
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
function createUpdateImageCommand(image, options) {
|
|
1486
|
+
return (state, dispatch, _view, value) => {
|
|
1487
|
+
const selectedImage = getSelectedImage(state, image);
|
|
1488
|
+
const attrs = resolveImageAttrs({
|
|
1489
|
+
...selectedImage?.node.attrs,
|
|
1490
|
+
...resolveImageUpdateValue(value),
|
|
1491
|
+
}, options);
|
|
1492
|
+
if (!selectedImage || !attrs) {
|
|
1493
|
+
return false;
|
|
1494
|
+
}
|
|
1495
|
+
if (dispatch) {
|
|
1496
|
+
dispatch(state.tr
|
|
1497
|
+
.setNodeMarkup(selectedImage.from, image, attrs)
|
|
1498
|
+
.scrollIntoView());
|
|
1499
|
+
}
|
|
1500
|
+
return true;
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
function createRemoveImageCommand(image) {
|
|
1504
|
+
return (state, dispatch) => {
|
|
1505
|
+
const selectedImage = getSelectedImage(state, image);
|
|
1506
|
+
if (!selectedImage) {
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
if (dispatch) {
|
|
1510
|
+
dispatch(state.tr.delete(selectedImage.from, selectedImage.to).scrollIntoView());
|
|
1511
|
+
}
|
|
1512
|
+
return true;
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function getSelectedImage(state, image) {
|
|
1516
|
+
const { selection } = state;
|
|
1517
|
+
if (!(selection instanceof NodeSelection) || selection.node.type !== image) {
|
|
1518
|
+
return null;
|
|
1519
|
+
}
|
|
1520
|
+
return {
|
|
1521
|
+
node: selection.node,
|
|
1522
|
+
from: selection.from,
|
|
1523
|
+
to: selection.to,
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
function getImageState(state, image) {
|
|
1527
|
+
const selectedImage = getSelectedImage(state, image);
|
|
1528
|
+
return selectedImage
|
|
1529
|
+
? {
|
|
1530
|
+
from: selectedImage.from,
|
|
1531
|
+
to: selectedImage.to,
|
|
1532
|
+
src: selectedImage.node.attrs['src'],
|
|
1533
|
+
alt: normalizeAlt(selectedImage.node.attrs['alt']),
|
|
1534
|
+
title: normalizeTitle(selectedImage.node.attrs['title']),
|
|
1535
|
+
previewSrc: normalizePreviewSrc(selectedImage.node.attrs['previewSrc']),
|
|
1536
|
+
}
|
|
1537
|
+
: null;
|
|
1538
|
+
}
|
|
1539
|
+
function canReplaceSelectionWithImage(state, image) {
|
|
1540
|
+
const { $from } = state.selection;
|
|
1541
|
+
for (let depth = $from.depth; depth >= 0; depth--) {
|
|
1542
|
+
const index = $from.index(depth);
|
|
1543
|
+
if ($from.node(depth).canReplaceWith(index, index, image)) {
|
|
1544
|
+
return true;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
function resolveImageAttrs(value, options) {
|
|
1550
|
+
const rawValue = typeof value === 'string'
|
|
1551
|
+
? { src: value }
|
|
1552
|
+
: isImageCommandValue(value)
|
|
1553
|
+
? value
|
|
1554
|
+
: null;
|
|
1555
|
+
if (!rawValue) {
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
const src = normalizeImageSrc(rawValue.src, options);
|
|
1559
|
+
if (!src) {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
src,
|
|
1564
|
+
alt: normalizeAlt(rawValue.alt ?? options.defaultAlt),
|
|
1565
|
+
title: normalizeTitle(rawValue.title),
|
|
1566
|
+
previewSrc: normalizePreviewSrc(rawValue.previewSrc),
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function normalizeImageSrc(value, options) {
|
|
1570
|
+
const src = value?.trim();
|
|
1571
|
+
if (!src) {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
const protocol = src.match(/^([a-z][a-z0-9+.-]*):/i)?.[1].toLowerCase();
|
|
1575
|
+
if (!protocol) {
|
|
1576
|
+
return options.allowRelativeImages && !src.startsWith('//') ? src : null;
|
|
1577
|
+
}
|
|
1578
|
+
return options.allowedProtocols.includes(protocol) ? src : null;
|
|
1579
|
+
}
|
|
1580
|
+
function normalizeAlt(value) {
|
|
1581
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
1582
|
+
}
|
|
1583
|
+
function normalizeTitle(value) {
|
|
1584
|
+
if (typeof value !== 'string') {
|
|
1585
|
+
return null;
|
|
1586
|
+
}
|
|
1587
|
+
return value.trim() || null;
|
|
1588
|
+
}
|
|
1589
|
+
function normalizePreviewSrc(value) {
|
|
1590
|
+
if (typeof value !== 'string') {
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
const src = value.trim();
|
|
1594
|
+
if (!src) {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
return src.startsWith('blob:') || /^data:image\//i.test(src) ? src : null;
|
|
1598
|
+
}
|
|
1599
|
+
function isImageCommandValue(value) {
|
|
1600
|
+
return (typeof value === 'object' &&
|
|
1601
|
+
value !== null &&
|
|
1602
|
+
'src' in value &&
|
|
1603
|
+
typeof value.src === 'string');
|
|
1604
|
+
}
|
|
1605
|
+
function createImagePreviewPlugin() {
|
|
1606
|
+
return new Plugin({
|
|
1607
|
+
props: {
|
|
1608
|
+
nodeViews: {
|
|
1609
|
+
image: (node) => {
|
|
1610
|
+
return new ImageNodeView(node);
|
|
1611
|
+
},
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
class ImageNodeView {
|
|
1617
|
+
node;
|
|
1618
|
+
dom = document.createElement('img');
|
|
1619
|
+
constructor(node) {
|
|
1620
|
+
this.node = node;
|
|
1621
|
+
this.render();
|
|
1622
|
+
}
|
|
1623
|
+
update(node) {
|
|
1624
|
+
if (node.type !== this.node.type) {
|
|
1625
|
+
return false;
|
|
1626
|
+
}
|
|
1627
|
+
this.node = node;
|
|
1628
|
+
this.render();
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
render() {
|
|
1632
|
+
const src = normalizePreviewSrc(this.node.attrs['previewSrc']) ??
|
|
1633
|
+
this.node.attrs['src'];
|
|
1634
|
+
const title = normalizeTitle(this.node.attrs['title']);
|
|
1635
|
+
this.dom.src = src;
|
|
1636
|
+
this.dom.alt = normalizeAlt(this.node.attrs['alt']);
|
|
1637
|
+
if (title) {
|
|
1638
|
+
this.dom.title = title;
|
|
1639
|
+
}
|
|
1640
|
+
else {
|
|
1641
|
+
this.dom.removeAttribute('title');
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function resolveImageUpdateValue(value) {
|
|
1646
|
+
if (typeof value === 'string') {
|
|
1647
|
+
return { src: value };
|
|
1648
|
+
}
|
|
1649
|
+
return typeof value === 'object' && value !== null ? value : {};
|
|
1650
|
+
}
|
|
1651
|
+
function assertImagePluginOptions(options) {
|
|
1652
|
+
if (!Array.isArray(options.allowedProtocols)) {
|
|
1653
|
+
throw new TypeError('ImagePlugin allowedProtocols must be an array.');
|
|
1654
|
+
}
|
|
1655
|
+
if (options.allowedProtocols.length === 0) {
|
|
1656
|
+
throw new RangeError('ImagePlugin allowedProtocols must include at least one protocol.');
|
|
1657
|
+
}
|
|
1658
|
+
const seen = new Set();
|
|
1659
|
+
for (const protocol of options.allowedProtocols) {
|
|
1660
|
+
if (typeof protocol !== 'string' || !/^[a-z][a-z0-9+.-]*$/.test(protocol)) {
|
|
1661
|
+
throw new TypeError('ImagePlugin allowedProtocols entries must be protocol names without colons.');
|
|
1662
|
+
}
|
|
1663
|
+
if (seen.has(protocol)) {
|
|
1664
|
+
throw new Error('ImagePlugin allowedProtocols entries must be unique.');
|
|
1665
|
+
}
|
|
1666
|
+
seen.add(protocol);
|
|
1667
|
+
}
|
|
1668
|
+
if (typeof options.allowRelativeImages !== 'boolean') {
|
|
1669
|
+
throw new TypeError('ImagePlugin allowRelativeImages must be a boolean.');
|
|
1670
|
+
}
|
|
1671
|
+
if (typeof options.defaultAlt !== 'string') {
|
|
1672
|
+
throw new TypeError('ImagePlugin defaultAlt must be a string.');
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function isMarkActive(state, mark) {
|
|
1677
|
+
const { from, $from, to, empty } = state.selection;
|
|
1678
|
+
if (empty) {
|
|
1679
|
+
return Boolean(mark.isInSet(state.storedMarks ?? $from.marks()));
|
|
1680
|
+
}
|
|
1681
|
+
return state.doc.rangeHasMark(from, to, mark);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const LINK_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
1685
|
+
allowedProtocols: Object.freeze(['http', 'https', 'mailto', 'tel']),
|
|
1686
|
+
allowRelativeLinks: true,
|
|
1687
|
+
defaultTarget: '_blank',
|
|
1688
|
+
defaultRel: 'noopener noreferrer',
|
|
1689
|
+
});
|
|
1690
|
+
const LinkPlugin = createConfigurableQalmaPlugin(LINK_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
1691
|
+
assertLinkPluginOptions(options);
|
|
1692
|
+
const linkMark = {
|
|
1693
|
+
attrs: {
|
|
1694
|
+
href: {},
|
|
1695
|
+
target: { default: null },
|
|
1696
|
+
rel: { default: null },
|
|
1697
|
+
},
|
|
1698
|
+
inclusive: false,
|
|
1699
|
+
parseDOM: [
|
|
1700
|
+
{
|
|
1701
|
+
tag: 'a[href]',
|
|
1702
|
+
getAttrs: (node) => {
|
|
1703
|
+
if (!(node instanceof HTMLElement)) {
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
const href = normalizeHref$1(node.getAttribute('href'), options);
|
|
1707
|
+
if (!href) {
|
|
1708
|
+
return false;
|
|
1709
|
+
}
|
|
1710
|
+
return {
|
|
1711
|
+
href,
|
|
1712
|
+
target: normalizeTarget(node.getAttribute('target') ?? options.defaultTarget),
|
|
1713
|
+
rel: normalizeRel(node.getAttribute('rel') ?? options.defaultRel),
|
|
1714
|
+
};
|
|
1715
|
+
},
|
|
1716
|
+
},
|
|
1717
|
+
],
|
|
1718
|
+
toDOM: (mark) => {
|
|
1719
|
+
const attrs = {
|
|
1720
|
+
href: mark.attrs['href'],
|
|
1721
|
+
};
|
|
1722
|
+
const target = normalizeTarget(mark.attrs['target']);
|
|
1723
|
+
const rel = normalizeRel(mark.attrs['rel']);
|
|
1724
|
+
if (target) {
|
|
1725
|
+
attrs['target'] = target;
|
|
1726
|
+
}
|
|
1727
|
+
if (rel) {
|
|
1728
|
+
attrs['rel'] = rel;
|
|
1729
|
+
}
|
|
1730
|
+
return ['a', attrs, 0];
|
|
1731
|
+
},
|
|
1732
|
+
};
|
|
1733
|
+
return createQalmaPlugin({
|
|
1734
|
+
key: 'link',
|
|
1735
|
+
marks: {
|
|
1736
|
+
link: linkMark,
|
|
1737
|
+
},
|
|
1738
|
+
commands: (schema) => ({
|
|
1739
|
+
setLink: createSetLinkCommand(schema.marks['link'], options),
|
|
1740
|
+
selectLink: createSelectLinkCommand(schema.marks['link']),
|
|
1741
|
+
unsetLink: createUnsetLinkCommand(schema.marks['link']),
|
|
1742
|
+
}),
|
|
1743
|
+
commandStates: (schema) => ({
|
|
1744
|
+
setLink: (state) => isMarkActive(state, schema.marks['link']),
|
|
1745
|
+
}),
|
|
1746
|
+
queries: (schema) => ({
|
|
1747
|
+
link: (state) => getLinkState(state, schema.marks['link']),
|
|
1748
|
+
}),
|
|
1749
|
+
prosemirrorPlugins: () => [createOpenLinkPlugin(options)],
|
|
1750
|
+
});
|
|
1751
|
+
});
|
|
1752
|
+
function createSetLinkCommand(mark, options) {
|
|
1753
|
+
return (state, dispatch, _view, value) => {
|
|
1754
|
+
const attrs = resolveLinkAttrs(value, options);
|
|
1755
|
+
if (!attrs || !selectionAllowsMark(state, mark)) {
|
|
1756
|
+
return false;
|
|
1757
|
+
}
|
|
1758
|
+
if (dispatch) {
|
|
1759
|
+
const transaction = state.tr;
|
|
1760
|
+
if (state.selection.empty) {
|
|
1761
|
+
transaction.addStoredMark(mark.create(attrs));
|
|
1762
|
+
}
|
|
1763
|
+
else {
|
|
1764
|
+
for (const range of state.selection.ranges) {
|
|
1765
|
+
transaction.removeMark(range.$from.pos, range.$to.pos, mark);
|
|
1766
|
+
transaction.addMark(range.$from.pos, range.$to.pos, mark.create(attrs));
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
dispatch(transaction.scrollIntoView());
|
|
1770
|
+
}
|
|
1771
|
+
return true;
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
function createUnsetLinkCommand(mark) {
|
|
1775
|
+
return (state, dispatch) => {
|
|
1776
|
+
if (!isMarkActive(state, mark)) {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
if (dispatch) {
|
|
1780
|
+
const transaction = state.tr.removeStoredMark(mark);
|
|
1781
|
+
for (const range of state.selection.ranges) {
|
|
1782
|
+
transaction.removeMark(range.$from.pos, range.$to.pos, mark);
|
|
1783
|
+
}
|
|
1784
|
+
dispatch(transaction.scrollIntoView());
|
|
1785
|
+
}
|
|
1786
|
+
return true;
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
function createSelectLinkCommand(mark) {
|
|
1790
|
+
return (state, dispatch, view, value) => {
|
|
1791
|
+
const range = resolveLinkSelectionRange(state, mark, view, value);
|
|
1792
|
+
if (!range) {
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
if (dispatch) {
|
|
1796
|
+
dispatch(state.tr
|
|
1797
|
+
.setSelection(TextSelection.create(state.doc, range.from, range.to))
|
|
1798
|
+
.scrollIntoView());
|
|
1799
|
+
}
|
|
1800
|
+
return true;
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function createOpenLinkPlugin(options) {
|
|
1804
|
+
return new Plugin({
|
|
1805
|
+
props: {
|
|
1806
|
+
handleDOMEvents: {
|
|
1807
|
+
click: (_view, event) => {
|
|
1808
|
+
const link = findClickedLink(event.target);
|
|
1809
|
+
if (!link) {
|
|
1810
|
+
return false;
|
|
1811
|
+
}
|
|
1812
|
+
const href = normalizeHref$1(link.getAttribute('href'), options);
|
|
1813
|
+
if (!href) {
|
|
1814
|
+
return false;
|
|
1815
|
+
}
|
|
1816
|
+
event.preventDefault();
|
|
1817
|
+
const linkTarget = normalizeTarget(link.getAttribute('target')) ??
|
|
1818
|
+
options.defaultTarget;
|
|
1819
|
+
if (linkTarget === '_blank') {
|
|
1820
|
+
window.open(href, '_blank', 'noopener,noreferrer');
|
|
1821
|
+
}
|
|
1822
|
+
else {
|
|
1823
|
+
window.location.href = href;
|
|
1824
|
+
}
|
|
1825
|
+
return true;
|
|
1826
|
+
},
|
|
1827
|
+
},
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
function findClickedLink(target) {
|
|
1832
|
+
if (!(target instanceof Node)) {
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1835
|
+
const element = target instanceof Element ? target : target.parentElement;
|
|
1836
|
+
if (!element) {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
const link = element.closest('a[href]');
|
|
1840
|
+
return link instanceof HTMLAnchorElement ? link : null;
|
|
1841
|
+
}
|
|
1842
|
+
function resolveLinkSelectionRange(state, mark, view, value) {
|
|
1843
|
+
if (isLinkRangeTarget(value)) {
|
|
1844
|
+
return hasLinkBetween(state, mark, value.from, value.to) ? value : null;
|
|
1845
|
+
}
|
|
1846
|
+
if (isLinkElementTarget(value) && view) {
|
|
1847
|
+
const position = getElementDocumentPosition(view, value.element);
|
|
1848
|
+
if (position === null) {
|
|
1849
|
+
return null;
|
|
1850
|
+
}
|
|
1851
|
+
const range = findLinkRange(state.doc.resolve(position), mark);
|
|
1852
|
+
return range ? { from: range.from, to: range.to } : null;
|
|
1853
|
+
}
|
|
1854
|
+
const stateValue = getLinkState(state, mark);
|
|
1855
|
+
return stateValue
|
|
1856
|
+
? {
|
|
1857
|
+
from: stateValue.from,
|
|
1858
|
+
to: stateValue.to,
|
|
1859
|
+
}
|
|
1860
|
+
: null;
|
|
1861
|
+
}
|
|
1862
|
+
function getElementDocumentPosition(view, element) {
|
|
1863
|
+
try {
|
|
1864
|
+
return view.posAtDOM(element.firstChild ?? element, 0);
|
|
1865
|
+
}
|
|
1866
|
+
catch {
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
function getLinkState(state, mark) {
|
|
1871
|
+
if (state.selection.empty) {
|
|
1872
|
+
const range = findLinkRange(state.selection.$from, mark);
|
|
1873
|
+
return range ? createLinkState(state, range) : null;
|
|
1874
|
+
}
|
|
1875
|
+
let found = null;
|
|
1876
|
+
state.doc.nodesBetween(state.selection.from, state.selection.to, (_node, pos) => {
|
|
1877
|
+
if (found) {
|
|
1878
|
+
return false;
|
|
1879
|
+
}
|
|
1880
|
+
const range = findLinkRange(state.doc.resolve(pos), mark);
|
|
1881
|
+
if (range) {
|
|
1882
|
+
found = range;
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
return undefined;
|
|
1886
|
+
});
|
|
1887
|
+
return found ? createLinkState(state, found) : null;
|
|
1888
|
+
}
|
|
1889
|
+
function findLinkRange($position, mark) {
|
|
1890
|
+
const parent = $position.parent;
|
|
1891
|
+
const after = parent.childAfter($position.parentOffset);
|
|
1892
|
+
const before = parent.childBefore($position.parentOffset);
|
|
1893
|
+
const current = mark.isInSet(after.node?.marks ?? []);
|
|
1894
|
+
const previous = mark.isInSet(before.node?.marks ?? []);
|
|
1895
|
+
const link = current ?? previous;
|
|
1896
|
+
if (!link) {
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
let startIndex = current ? after.index : before.index;
|
|
1900
|
+
let endIndex = startIndex + 1;
|
|
1901
|
+
let from = $position.start() + (current ? after.offset : before.offset);
|
|
1902
|
+
let to = from + (current ? after.node?.nodeSize ?? 0 : before.node?.nodeSize ?? 0);
|
|
1903
|
+
while (startIndex > 0 &&
|
|
1904
|
+
hasSameMark(parent.child(startIndex - 1).marks, link)) {
|
|
1905
|
+
startIndex -= 1;
|
|
1906
|
+
from -= parent.child(startIndex).nodeSize;
|
|
1907
|
+
}
|
|
1908
|
+
while (endIndex < parent.childCount &&
|
|
1909
|
+
hasSameMark(parent.child(endIndex).marks, link)) {
|
|
1910
|
+
to += parent.child(endIndex).nodeSize;
|
|
1911
|
+
endIndex += 1;
|
|
1912
|
+
}
|
|
1913
|
+
return {
|
|
1914
|
+
from,
|
|
1915
|
+
to,
|
|
1916
|
+
mark: link,
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
function hasSameMark(marks, mark) {
|
|
1920
|
+
return marks.some((candidate) => candidate.eq(mark));
|
|
1921
|
+
}
|
|
1922
|
+
function createLinkState(state, range) {
|
|
1923
|
+
return {
|
|
1924
|
+
from: range.from,
|
|
1925
|
+
to: range.to,
|
|
1926
|
+
href: range.mark.attrs['href'],
|
|
1927
|
+
target: normalizeTarget(range.mark.attrs['target']),
|
|
1928
|
+
rel: normalizeRel(range.mark.attrs['rel']),
|
|
1929
|
+
text: state.doc.textBetween(range.from, range.to, ''),
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
function resolveLinkAttrs(value, options) {
|
|
1933
|
+
const rawValue = typeof value === 'string'
|
|
1934
|
+
? { href: value }
|
|
1935
|
+
: isLinkCommandValue(value)
|
|
1936
|
+
? value
|
|
1937
|
+
: null;
|
|
1938
|
+
if (!rawValue) {
|
|
1939
|
+
return null;
|
|
1940
|
+
}
|
|
1941
|
+
const href = normalizeHref$1(rawValue.href, options);
|
|
1942
|
+
if (!href) {
|
|
1943
|
+
return null;
|
|
1944
|
+
}
|
|
1945
|
+
return {
|
|
1946
|
+
href,
|
|
1947
|
+
target: normalizeTarget(rawValue.target ?? options.defaultTarget),
|
|
1948
|
+
rel: normalizeRel(rawValue.rel ?? options.defaultRel),
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
function normalizeHref$1(value, options) {
|
|
1952
|
+
const href = value?.trim();
|
|
1953
|
+
if (!href) {
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
const protocol = href.match(/^([a-z][a-z0-9+.-]*):/i)?.[1].toLowerCase();
|
|
1957
|
+
if (!protocol) {
|
|
1958
|
+
return options.allowRelativeLinks ? href : null;
|
|
1959
|
+
}
|
|
1960
|
+
return options.allowedProtocols.includes(protocol) ? href : null;
|
|
1961
|
+
}
|
|
1962
|
+
function normalizeTarget(value) {
|
|
1963
|
+
return value === '_blank' ? value : null;
|
|
1964
|
+
}
|
|
1965
|
+
function normalizeRel(value) {
|
|
1966
|
+
if (typeof value !== 'string') {
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
return value.trim() || null;
|
|
1970
|
+
}
|
|
1971
|
+
function isLinkCommandValue(value) {
|
|
1972
|
+
return (typeof value === 'object' &&
|
|
1973
|
+
value !== null &&
|
|
1974
|
+
'href' in value &&
|
|
1975
|
+
typeof value.href === 'string');
|
|
1976
|
+
}
|
|
1977
|
+
function isLinkElementTarget(value) {
|
|
1978
|
+
return (typeof value === 'object' &&
|
|
1979
|
+
value !== null &&
|
|
1980
|
+
'element' in value &&
|
|
1981
|
+
value.element instanceof HTMLAnchorElement);
|
|
1982
|
+
}
|
|
1983
|
+
function isLinkRangeTarget(value) {
|
|
1984
|
+
return (typeof value === 'object' &&
|
|
1985
|
+
value !== null &&
|
|
1986
|
+
'from' in value &&
|
|
1987
|
+
'to' in value &&
|
|
1988
|
+
typeof value.from === 'number' &&
|
|
1989
|
+
typeof value.to === 'number');
|
|
1990
|
+
}
|
|
1991
|
+
function hasLinkBetween(state, mark, from, to) {
|
|
1992
|
+
if (from < 0 || to > state.doc.content.size || from >= to) {
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
return state.doc.rangeHasMark(from, to, mark);
|
|
1996
|
+
}
|
|
1997
|
+
function selectionAllowsMark(state, mark) {
|
|
1998
|
+
if (state.selection.empty) {
|
|
1999
|
+
return state.selection.$from.parent.type.allowsMarkType(mark);
|
|
2000
|
+
}
|
|
2001
|
+
return state.selection.ranges.some((range) => {
|
|
2002
|
+
let allowsMark = false;
|
|
2003
|
+
state.doc.nodesBetween(range.$from.pos, range.$to.pos, (node, _pos, parent) => {
|
|
2004
|
+
if (allowsMark) {
|
|
2005
|
+
return false;
|
|
2006
|
+
}
|
|
2007
|
+
if (node.isInline && parent?.type.allowsMarkType(mark)) {
|
|
2008
|
+
allowsMark = true;
|
|
2009
|
+
return false;
|
|
2010
|
+
}
|
|
2011
|
+
return undefined;
|
|
2012
|
+
});
|
|
2013
|
+
return allowsMark;
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
function assertLinkPluginOptions(options) {
|
|
2017
|
+
if (!Array.isArray(options.allowedProtocols)) {
|
|
2018
|
+
throw new TypeError('LinkPlugin allowedProtocols must be an array.');
|
|
2019
|
+
}
|
|
2020
|
+
if (options.allowedProtocols.length === 0) {
|
|
2021
|
+
throw new RangeError('LinkPlugin allowedProtocols must include at least one protocol.');
|
|
2022
|
+
}
|
|
2023
|
+
for (const protocol of options.allowedProtocols) {
|
|
2024
|
+
if (typeof protocol !== 'string' ||
|
|
2025
|
+
!/^[a-z][a-z0-9+.-]*$/.test(protocol)) {
|
|
2026
|
+
throw new TypeError('LinkPlugin allowedProtocols entries must be protocol names without colons.');
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if (typeof options.allowRelativeLinks !== 'boolean') {
|
|
2030
|
+
throw new TypeError('LinkPlugin allowRelativeLinks must be a boolean.');
|
|
2031
|
+
}
|
|
2032
|
+
if (options.defaultTarget !== null &&
|
|
2033
|
+
options.defaultTarget !== '_blank') {
|
|
2034
|
+
throw new TypeError('LinkPlugin defaultTarget must be "_blank" or null.');
|
|
2035
|
+
}
|
|
2036
|
+
if (options.defaultRel !== null &&
|
|
2037
|
+
(typeof options.defaultRel !== 'string' || options.defaultRel.trim() === '')) {
|
|
2038
|
+
throw new TypeError('LinkPlugin defaultRel must be a non-empty string or null.');
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
const bulletListNode = {
|
|
2043
|
+
...bulletList,
|
|
2044
|
+
content: 'listItem+',
|
|
2045
|
+
group: 'block',
|
|
2046
|
+
};
|
|
2047
|
+
const orderedListNode = {
|
|
2048
|
+
...orderedList,
|
|
2049
|
+
content: 'listItem+',
|
|
2050
|
+
group: 'block',
|
|
2051
|
+
};
|
|
2052
|
+
const listItemNode = {
|
|
2053
|
+
...listItem,
|
|
2054
|
+
content: 'paragraph block*',
|
|
2055
|
+
};
|
|
2056
|
+
const ListsPlugin = createQalmaPlugin({
|
|
2057
|
+
key: 'lists',
|
|
2058
|
+
nodes: {
|
|
2059
|
+
bulletList: bulletListNode,
|
|
2060
|
+
orderedList: orderedListNode,
|
|
2061
|
+
listItem: listItemNode,
|
|
2062
|
+
},
|
|
2063
|
+
commands: (schema) => ({
|
|
2064
|
+
toggleBulletList: createToggleListCommand(schema.nodes['bulletList'], schema.nodes['orderedList'], schema.nodes['listItem']),
|
|
2065
|
+
toggleOrderedList: createToggleListCommand(schema.nodes['orderedList'], schema.nodes['bulletList'], schema.nodes['listItem']),
|
|
2066
|
+
splitListItem: splitListItemKeepMarks(schema.nodes['listItem']),
|
|
2067
|
+
liftListItem: liftListItem(schema.nodes['listItem']),
|
|
2068
|
+
sinkListItem: sinkListItem(schema.nodes['listItem']),
|
|
2069
|
+
}),
|
|
2070
|
+
commandStates: (schema) => ({
|
|
2071
|
+
toggleBulletList: (state) => isClosestListActive(state, [
|
|
2072
|
+
schema.nodes['bulletList'],
|
|
2073
|
+
schema.nodes['orderedList'],
|
|
2074
|
+
]),
|
|
2075
|
+
toggleOrderedList: (state) => isClosestListActive(state, [
|
|
2076
|
+
schema.nodes['orderedList'],
|
|
2077
|
+
schema.nodes['bulletList'],
|
|
2078
|
+
]),
|
|
2079
|
+
}),
|
|
2080
|
+
shortcuts: (schema) => ({
|
|
2081
|
+
'Mod-Shift-8': createToggleListCommand(schema.nodes['bulletList'], schema.nodes['orderedList'], schema.nodes['listItem']),
|
|
2082
|
+
'Mod-Shift-7': createToggleListCommand(schema.nodes['orderedList'], schema.nodes['bulletList'], schema.nodes['listItem']),
|
|
2083
|
+
Enter: splitListItemKeepMarks(schema.nodes['listItem']),
|
|
2084
|
+
Tab: createListTabCommand([schema.nodes['bulletList'], schema.nodes['orderedList']], sinkListItem(schema.nodes['listItem'])),
|
|
2085
|
+
'Shift-Tab': createListTabCommand([schema.nodes['bulletList'], schema.nodes['orderedList']], liftListItem(schema.nodes['listItem'])),
|
|
2086
|
+
}),
|
|
2087
|
+
prosemirrorPlugins: () => [createLeaveEditorPlugin()],
|
|
2088
|
+
});
|
|
2089
|
+
const ListsKit = [ListsPlugin];
|
|
2090
|
+
function createToggleListCommand(list, alternateList, listItem) {
|
|
2091
|
+
return (state, dispatch) => {
|
|
2092
|
+
if (isListActive(state, list)) {
|
|
2093
|
+
return liftListItem(listItem)(state, dispatch);
|
|
2094
|
+
}
|
|
2095
|
+
const activeList = findClosestList(state, [list, alternateList]);
|
|
2096
|
+
if (activeList) {
|
|
2097
|
+
if (!list.validContent(activeList.node.content)) {
|
|
2098
|
+
return false;
|
|
2099
|
+
}
|
|
2100
|
+
if (dispatch) {
|
|
2101
|
+
dispatch(state.tr
|
|
2102
|
+
.setNodeMarkup(activeList.position, list, getListAttrs(list))
|
|
2103
|
+
.scrollIntoView());
|
|
2104
|
+
}
|
|
2105
|
+
return true;
|
|
2106
|
+
}
|
|
2107
|
+
return wrapInList(list, getListAttrs(list))(state, dispatch);
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
function createListTabCommand(lists, command) {
|
|
2111
|
+
return (state, dispatch, view) => {
|
|
2112
|
+
if (command(state, dispatch, view)) {
|
|
2113
|
+
return true;
|
|
2114
|
+
}
|
|
2115
|
+
return Boolean(findClosestList(state, lists));
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
function createLeaveEditorPlugin() {
|
|
2119
|
+
return new Plugin({
|
|
2120
|
+
props: {
|
|
2121
|
+
handleKeyDown: (view, event) => {
|
|
2122
|
+
if (event.key !== 'Escape') {
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
const target = findAdjacentFocusable(view.dom, event.shiftKey ? 'previous' : 'next');
|
|
2126
|
+
event.preventDefault();
|
|
2127
|
+
if (target) {
|
|
2128
|
+
target.focus();
|
|
2129
|
+
}
|
|
2130
|
+
else {
|
|
2131
|
+
view.dom.blur();
|
|
2132
|
+
}
|
|
2133
|
+
return true;
|
|
2134
|
+
},
|
|
2135
|
+
},
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
function findClosestList(state, listTypes) {
|
|
2139
|
+
const { $from } = state.selection;
|
|
2140
|
+
for (let depth = $from.depth; depth > 0; depth -= 1) {
|
|
2141
|
+
const node = $from.node(depth);
|
|
2142
|
+
if (listTypes.includes(node.type)) {
|
|
2143
|
+
return {
|
|
2144
|
+
node,
|
|
2145
|
+
position: $from.before(depth),
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return null;
|
|
2150
|
+
}
|
|
2151
|
+
function isListActive(state, list) {
|
|
2152
|
+
return Boolean(findClosestList(state, [list]));
|
|
2153
|
+
}
|
|
2154
|
+
function isClosestListActive(state, [list, alternateList]) {
|
|
2155
|
+
return findClosestList(state, [list, alternateList])?.node.type === list;
|
|
2156
|
+
}
|
|
2157
|
+
function getListAttrs(list) {
|
|
2158
|
+
return list.name === 'orderedList' ? { order: 1 } : null;
|
|
2159
|
+
}
|
|
2160
|
+
const focusableSelector = [
|
|
2161
|
+
'a[href]',
|
|
2162
|
+
'button:not([disabled])',
|
|
2163
|
+
'input:not([disabled])',
|
|
2164
|
+
'select:not([disabled])',
|
|
2165
|
+
'textarea:not([disabled])',
|
|
2166
|
+
'[contenteditable="true"]',
|
|
2167
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
2168
|
+
].join(',');
|
|
2169
|
+
function findAdjacentFocusable(editor, direction) {
|
|
2170
|
+
const root = editor.getRootNode();
|
|
2171
|
+
if (!(root instanceof Document || root instanceof ShadowRoot)) {
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
const focusableElements = Array.from(root.querySelectorAll(focusableSelector)).filter((element) => isFocusable(element) && !editor.contains(element));
|
|
2175
|
+
const orderedElements = direction === 'next' ? focusableElements : focusableElements.reverse();
|
|
2176
|
+
return (orderedElements.find((element) => direction === 'next'
|
|
2177
|
+
? isAfterEditor(editor, element)
|
|
2178
|
+
: isBeforeEditor(editor, element)) ?? null);
|
|
2179
|
+
}
|
|
2180
|
+
function isFocusable(element) {
|
|
2181
|
+
if (element.hidden || element.closest('[inert]')) {
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
const style = getComputedStyle(element);
|
|
2185
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
2186
|
+
}
|
|
2187
|
+
function isAfterEditor(editor, element) {
|
|
2188
|
+
return Boolean(editor.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING);
|
|
2189
|
+
}
|
|
2190
|
+
function isBeforeEditor(editor, element) {
|
|
2191
|
+
return Boolean(editor.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_PRECEDING);
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
const MENTION_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
2195
|
+
trigger: '@',
|
|
2196
|
+
minQueryLength: 0,
|
|
2197
|
+
maxQueryLength: 64,
|
|
2198
|
+
appendSpaceOnInsert: true,
|
|
2199
|
+
});
|
|
2200
|
+
const MentionPlugin = createConfigurableQalmaPlugin(MENTION_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
2201
|
+
assertMentionPluginOptions(options);
|
|
2202
|
+
const mentionNode = {
|
|
2203
|
+
attrs: {
|
|
2204
|
+
id: {},
|
|
2205
|
+
label: {},
|
|
2206
|
+
trigger: { default: options.trigger },
|
|
2207
|
+
},
|
|
2208
|
+
group: 'inline',
|
|
2209
|
+
inline: true,
|
|
2210
|
+
atom: true,
|
|
2211
|
+
selectable: false,
|
|
2212
|
+
parseDOM: [
|
|
2213
|
+
{
|
|
2214
|
+
tag: 'span[data-qalma-mention]',
|
|
2215
|
+
getAttrs: (node) => {
|
|
2216
|
+
if (!(node instanceof HTMLElement)) {
|
|
2217
|
+
return false;
|
|
2218
|
+
}
|
|
2219
|
+
const id = normalizeMentionText(node.dataset['mentionId']);
|
|
2220
|
+
const trigger = normalizeTrigger(node.dataset['mentionTrigger'] ?? options.trigger);
|
|
2221
|
+
const label = normalizeMentionText(node.dataset['mentionLabel'] ??
|
|
2222
|
+
stripMentionTrigger(node.textContent ?? '', trigger));
|
|
2223
|
+
return id && label
|
|
2224
|
+
? {
|
|
2225
|
+
id,
|
|
2226
|
+
label,
|
|
2227
|
+
trigger,
|
|
2228
|
+
}
|
|
2229
|
+
: false;
|
|
2230
|
+
},
|
|
2231
|
+
},
|
|
2232
|
+
],
|
|
2233
|
+
toDOM: (node) => {
|
|
2234
|
+
const trigger = normalizeTrigger(node.attrs['trigger']);
|
|
2235
|
+
const label = normalizeMentionText(node.attrs['label']) ?? '';
|
|
2236
|
+
const attrs = {
|
|
2237
|
+
'data-qalma-mention': '',
|
|
2238
|
+
'data-mention-id': String(node.attrs['id']),
|
|
2239
|
+
'data-mention-label': label,
|
|
2240
|
+
'data-mention-trigger': trigger,
|
|
2241
|
+
contenteditable: 'false',
|
|
2242
|
+
};
|
|
2243
|
+
return ['span', attrs, `${trigger}${label}`];
|
|
2244
|
+
},
|
|
2245
|
+
};
|
|
2246
|
+
return createQalmaPlugin({
|
|
2247
|
+
key: 'mention',
|
|
2248
|
+
nodes: {
|
|
2249
|
+
mention: mentionNode,
|
|
2250
|
+
},
|
|
2251
|
+
commands: (schema) => ({
|
|
2252
|
+
insertMention: createInsertMentionCommand(schema.nodes['mention'], options),
|
|
2253
|
+
}),
|
|
2254
|
+
queries: () => ({
|
|
2255
|
+
mention: (state) => getMentionState(state, options),
|
|
2256
|
+
}),
|
|
2257
|
+
prosemirrorPlugins: () => [createMentionInteractionPlugin(options)],
|
|
2258
|
+
});
|
|
2259
|
+
});
|
|
2260
|
+
const MentionKit = [MentionPlugin];
|
|
2261
|
+
function createInsertMentionCommand(mention, options) {
|
|
2262
|
+
return (state, dispatch, _view, value) => {
|
|
2263
|
+
const attrs = resolveMentionAttrs(value, options);
|
|
2264
|
+
const range = getMentionState(state, options) ?? {
|
|
2265
|
+
from: state.selection.from,
|
|
2266
|
+
to: state.selection.to,
|
|
2267
|
+
query: '',
|
|
2268
|
+
trigger: attrs?.trigger ?? options.trigger,
|
|
2269
|
+
};
|
|
2270
|
+
if (!attrs || !selectionAllowsMention(state, mention, range.from, range.to)) {
|
|
2271
|
+
return false;
|
|
2272
|
+
}
|
|
2273
|
+
if (dispatch) {
|
|
2274
|
+
const mentionNode = mention.create(attrs);
|
|
2275
|
+
const nextCharacter = state.doc.textBetween(range.to, Math.min(range.to + 1, state.doc.content.size), '', '');
|
|
2276
|
+
const shouldAppendSpace = options.appendSpaceOnInsert && !/^\s$/.test(nextCharacter);
|
|
2277
|
+
let transaction = state.tr.replaceWith(range.from, range.to, mentionNode);
|
|
2278
|
+
const selectionPosition = range.from + mentionNode.nodeSize;
|
|
2279
|
+
if (shouldAppendSpace) {
|
|
2280
|
+
transaction = transaction.insertText(' ', selectionPosition);
|
|
2281
|
+
}
|
|
2282
|
+
dispatch(transaction
|
|
2283
|
+
.setSelection(TextSelection.create(transaction.doc, selectionPosition))
|
|
2284
|
+
.scrollIntoView());
|
|
2285
|
+
}
|
|
2286
|
+
return true;
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
function createMentionInteractionPlugin(options) {
|
|
2290
|
+
return new Plugin({
|
|
2291
|
+
props: {
|
|
2292
|
+
handleKeyDown: (view, event) => {
|
|
2293
|
+
if (!getMentionState(view.state, options) ||
|
|
2294
|
+
!isMentionNavigationKey(event.key)) {
|
|
2295
|
+
return false;
|
|
2296
|
+
}
|
|
2297
|
+
const mentionEvent = new CustomEvent('qalma-mention-keydown', {
|
|
2298
|
+
bubbles: true,
|
|
2299
|
+
cancelable: true,
|
|
2300
|
+
detail: {
|
|
2301
|
+
key: event.key,
|
|
2302
|
+
},
|
|
2303
|
+
});
|
|
2304
|
+
view.dom.dispatchEvent(mentionEvent);
|
|
2305
|
+
if (!mentionEvent.defaultPrevented) {
|
|
2306
|
+
return false;
|
|
2307
|
+
}
|
|
2308
|
+
event.preventDefault();
|
|
2309
|
+
return true;
|
|
2310
|
+
},
|
|
2311
|
+
},
|
|
2312
|
+
view: (view) => ({
|
|
2313
|
+
update: () => {
|
|
2314
|
+
view.dom.dispatchEvent(new CustomEvent('qalma-mention-update', {
|
|
2315
|
+
bubbles: true,
|
|
2316
|
+
}));
|
|
2317
|
+
},
|
|
2318
|
+
}),
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
function isMentionNavigationKey(key) {
|
|
2322
|
+
return (key === 'ArrowDown' ||
|
|
2323
|
+
key === 'ArrowUp' ||
|
|
2324
|
+
key === 'Escape' ||
|
|
2325
|
+
key === 'Enter' ||
|
|
2326
|
+
key === 'Tab' ||
|
|
2327
|
+
key === ' ' ||
|
|
2328
|
+
key === 'Spacebar');
|
|
2329
|
+
}
|
|
2330
|
+
function getMentionState(state, options) {
|
|
2331
|
+
if (!state.selection.empty) {
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
const $cursor = state.selection.$from;
|
|
2335
|
+
if (isCodeTextblock($cursor.parent.type)) {
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
const textBeforeCursor = $cursor.parent.textBetween(0, $cursor.parentOffset, '', '');
|
|
2339
|
+
const triggerIndex = textBeforeCursor.lastIndexOf(options.trigger);
|
|
2340
|
+
if (triggerIndex < 0 || !hasMentionBoundary(textBeforeCursor, triggerIndex)) {
|
|
2341
|
+
return null;
|
|
2342
|
+
}
|
|
2343
|
+
const query = textBeforeCursor.slice(triggerIndex + options.trigger.length);
|
|
2344
|
+
if (/\s/.test(query) ||
|
|
2345
|
+
query.length < options.minQueryLength ||
|
|
2346
|
+
query.length > options.maxQueryLength) {
|
|
2347
|
+
return null;
|
|
2348
|
+
}
|
|
2349
|
+
return {
|
|
2350
|
+
from: $cursor.start() + triggerIndex,
|
|
2351
|
+
to: state.selection.from,
|
|
2352
|
+
query,
|
|
2353
|
+
trigger: options.trigger,
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
function hasMentionBoundary(text, triggerIndex) {
|
|
2357
|
+
return triggerIndex === 0 || /\s/.test(text.charAt(triggerIndex - 1));
|
|
2358
|
+
}
|
|
2359
|
+
function resolveMentionAttrs(value, options) {
|
|
2360
|
+
if (!isMentionCommandValue(value)) {
|
|
2361
|
+
return null;
|
|
2362
|
+
}
|
|
2363
|
+
const id = normalizeMentionText(value.id);
|
|
2364
|
+
const label = normalizeMentionText(value.label);
|
|
2365
|
+
const trigger = normalizeTrigger(value.trigger ?? options.trigger);
|
|
2366
|
+
return id && label
|
|
2367
|
+
? {
|
|
2368
|
+
id,
|
|
2369
|
+
label,
|
|
2370
|
+
trigger,
|
|
2371
|
+
}
|
|
2372
|
+
: null;
|
|
2373
|
+
}
|
|
2374
|
+
function isMentionCommandValue(value) {
|
|
2375
|
+
return (typeof value === 'object' &&
|
|
2376
|
+
value !== null &&
|
|
2377
|
+
'id' in value &&
|
|
2378
|
+
'label' in value &&
|
|
2379
|
+
typeof value.id === 'string' &&
|
|
2380
|
+
typeof value.label === 'string' &&
|
|
2381
|
+
(!('trigger' in value) ||
|
|
2382
|
+
value.trigger === undefined ||
|
|
2383
|
+
typeof value.trigger === 'string'));
|
|
2384
|
+
}
|
|
2385
|
+
function selectionAllowsMention(state, mention, from, to) {
|
|
2386
|
+
if (from < 0 || to > state.doc.content.size || from > to) {
|
|
2387
|
+
return false;
|
|
2388
|
+
}
|
|
2389
|
+
const $from = state.doc.resolve(from);
|
|
2390
|
+
const $to = state.doc.resolve(to);
|
|
2391
|
+
return (!isCodeTextblock($from.parent.type) &&
|
|
2392
|
+
$from.sameParent($to) &&
|
|
2393
|
+
$from.parent.canReplaceWith($from.index(), $to.index(), mention));
|
|
2394
|
+
}
|
|
2395
|
+
function isCodeTextblock(type) {
|
|
2396
|
+
return Boolean(type.spec.code);
|
|
2397
|
+
}
|
|
2398
|
+
function normalizeMentionText(value) {
|
|
2399
|
+
if (typeof value !== 'string') {
|
|
2400
|
+
return null;
|
|
2401
|
+
}
|
|
2402
|
+
return value.trim() || null;
|
|
2403
|
+
}
|
|
2404
|
+
function normalizeTrigger(value) {
|
|
2405
|
+
return typeof value === 'string' && value.length > 0 ? value : '@';
|
|
2406
|
+
}
|
|
2407
|
+
function stripMentionTrigger(text, trigger) {
|
|
2408
|
+
return text.startsWith(trigger) ? text.slice(trigger.length) : text;
|
|
2409
|
+
}
|
|
2410
|
+
function assertMentionPluginOptions(options) {
|
|
2411
|
+
if (typeof options.trigger !== 'string' ||
|
|
2412
|
+
Array.from(options.trigger).length !== 1 ||
|
|
2413
|
+
/\s/.test(options.trigger)) {
|
|
2414
|
+
throw new TypeError('MentionPlugin trigger must be a single non-whitespace character.');
|
|
2415
|
+
}
|
|
2416
|
+
if (!Number.isInteger(options.minQueryLength) ||
|
|
2417
|
+
options.minQueryLength < 0) {
|
|
2418
|
+
throw new RangeError('MentionPlugin minQueryLength must be a non-negative integer.');
|
|
2419
|
+
}
|
|
2420
|
+
if (!Number.isInteger(options.maxQueryLength) ||
|
|
2421
|
+
options.maxQueryLength < options.minQueryLength) {
|
|
2422
|
+
throw new RangeError('MentionPlugin maxQueryLength must be greater than or equal to minQueryLength.');
|
|
2423
|
+
}
|
|
2424
|
+
if (typeof options.appendSpaceOnInsert !== 'boolean') {
|
|
2425
|
+
throw new TypeError('MentionPlugin appendSpaceOnInsert must be a boolean.');
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
const PASTE_RULES_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
2430
|
+
autolink: true,
|
|
2431
|
+
allowedProtocols: Object.freeze(['http', 'https', 'mailto', 'tel']),
|
|
2432
|
+
allowRelativeLinks: true,
|
|
2433
|
+
cleanHtml: true,
|
|
2434
|
+
defaultProtocol: 'https',
|
|
2435
|
+
});
|
|
2436
|
+
const PasteRulesPlugin = createConfigurableQalmaPlugin(PASTE_RULES_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
2437
|
+
assertPasteRulesPluginOptions(options);
|
|
2438
|
+
return createQalmaPlugin({
|
|
2439
|
+
key: 'pasteRules',
|
|
2440
|
+
prosemirrorPlugins: (schema) => [
|
|
2441
|
+
createPasteRulesPlugin(schema, options),
|
|
2442
|
+
],
|
|
2443
|
+
});
|
|
2444
|
+
});
|
|
2445
|
+
function createPasteRulesPlugin(schema, options) {
|
|
2446
|
+
return new Plugin({
|
|
2447
|
+
props: {
|
|
2448
|
+
handlePaste: (view, event) => {
|
|
2449
|
+
const clipboardData = event.clipboardData;
|
|
2450
|
+
if (!clipboardData) {
|
|
2451
|
+
return false;
|
|
2452
|
+
}
|
|
2453
|
+
const html = createPasteHtml(clipboardData, schema, options);
|
|
2454
|
+
if (!html) {
|
|
2455
|
+
return false;
|
|
2456
|
+
}
|
|
2457
|
+
const container = document.createElement('div');
|
|
2458
|
+
container.innerHTML = html;
|
|
2459
|
+
event.preventDefault();
|
|
2460
|
+
view.dispatch(view.state.tr
|
|
2461
|
+
.replaceSelection(DOMParser.fromSchema(view.state.schema).parseSlice(container, { preserveWhitespace: true }))
|
|
2462
|
+
.scrollIntoView());
|
|
2463
|
+
return true;
|
|
2464
|
+
},
|
|
2465
|
+
},
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
function createPasteHtml(clipboardData, schema, options) {
|
|
2469
|
+
const html = clipboardData.getData('text/html');
|
|
2470
|
+
const text = clipboardData.getData('text/plain');
|
|
2471
|
+
if (html && !options.cleanHtml) {
|
|
2472
|
+
return null;
|
|
2473
|
+
}
|
|
2474
|
+
if (html) {
|
|
2475
|
+
const cleanedHtml = cleanPastedHtml(html, options);
|
|
2476
|
+
const clipboardHref = getClipboardHref(clipboardData, options);
|
|
2477
|
+
if (schema.marks['link'] &&
|
|
2478
|
+
clipboardHref &&
|
|
2479
|
+
!hasAnchor(cleanedHtml) &&
|
|
2480
|
+
hasEquivalentText(cleanedHtml, text)) {
|
|
2481
|
+
return createLinkedTextHtml(text, clipboardHref);
|
|
2482
|
+
}
|
|
2483
|
+
return cleanedHtml;
|
|
2484
|
+
}
|
|
2485
|
+
if (!options.autolink || !schema.marks['link']) {
|
|
2486
|
+
return null;
|
|
2487
|
+
}
|
|
2488
|
+
return createAutolinkHtml(text, options);
|
|
2489
|
+
}
|
|
2490
|
+
function cleanPastedHtml(html, options) {
|
|
2491
|
+
const source = document.implementation.createHTMLDocument('');
|
|
2492
|
+
const target = document.createElement('div');
|
|
2493
|
+
source.body.innerHTML = html;
|
|
2494
|
+
for (const child of Array.from(source.body.childNodes)) {
|
|
2495
|
+
appendCleanNode(target, child, options);
|
|
2496
|
+
}
|
|
2497
|
+
return target.innerHTML;
|
|
2498
|
+
}
|
|
2499
|
+
function appendCleanNode(parent, source, options) {
|
|
2500
|
+
const cleanNode = createCleanNode(source, options);
|
|
2501
|
+
if (cleanNode) {
|
|
2502
|
+
parent.appendChild(cleanNode);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
function createCleanNode(source, options) {
|
|
2506
|
+
if (source.nodeType === Node.TEXT_NODE) {
|
|
2507
|
+
return document.createTextNode(source.textContent ?? '');
|
|
2508
|
+
}
|
|
2509
|
+
if (!(source instanceof HTMLElement)) {
|
|
2510
|
+
return null;
|
|
2511
|
+
}
|
|
2512
|
+
const tagName = source.tagName.toLowerCase();
|
|
2513
|
+
if (tagName === 'script' || tagName === 'style') {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
if (tagName === 'br') {
|
|
2517
|
+
return document.createElement('br');
|
|
2518
|
+
}
|
|
2519
|
+
if (tagName === 'a') {
|
|
2520
|
+
return createCleanLink(source, options);
|
|
2521
|
+
}
|
|
2522
|
+
if (shouldUnwrapElement(tagName)) {
|
|
2523
|
+
return createCleanFragment(source, options);
|
|
2524
|
+
}
|
|
2525
|
+
if (!isAllowedElement(tagName)) {
|
|
2526
|
+
return createCleanFragment(source, options);
|
|
2527
|
+
}
|
|
2528
|
+
const element = document.createElement(tagName);
|
|
2529
|
+
for (const child of Array.from(source.childNodes)) {
|
|
2530
|
+
appendCleanNode(element, child, options);
|
|
2531
|
+
}
|
|
2532
|
+
return element;
|
|
2533
|
+
}
|
|
2534
|
+
function createCleanLink(source, options) {
|
|
2535
|
+
const href = normalizeHref(source.getAttribute('href') ?? '', options);
|
|
2536
|
+
if (!href) {
|
|
2537
|
+
return createCleanFragment(source, options);
|
|
2538
|
+
}
|
|
2539
|
+
const link = document.createElement('a');
|
|
2540
|
+
link.setAttribute('href', href);
|
|
2541
|
+
for (const child of Array.from(source.childNodes)) {
|
|
2542
|
+
appendCleanNode(link, child, options);
|
|
2543
|
+
}
|
|
2544
|
+
if (!link.textContent) {
|
|
2545
|
+
link.textContent = href;
|
|
2546
|
+
}
|
|
2547
|
+
return link;
|
|
2548
|
+
}
|
|
2549
|
+
function createCleanFragment(source, options) {
|
|
2550
|
+
const fragment = document.createDocumentFragment();
|
|
2551
|
+
for (const child of Array.from(source.childNodes)) {
|
|
2552
|
+
appendCleanNode(fragment, child, options);
|
|
2553
|
+
}
|
|
2554
|
+
return fragment;
|
|
2555
|
+
}
|
|
2556
|
+
function shouldUnwrapElement(tagName) {
|
|
2557
|
+
return (tagName === 'html' ||
|
|
2558
|
+
tagName === 'body' ||
|
|
2559
|
+
tagName === 'span' ||
|
|
2560
|
+
tagName === 'font');
|
|
2561
|
+
}
|
|
2562
|
+
function isAllowedElement(tagName) {
|
|
2563
|
+
return [
|
|
2564
|
+
'blockquote',
|
|
2565
|
+
'code',
|
|
2566
|
+
'del',
|
|
2567
|
+
'em',
|
|
2568
|
+
'h1',
|
|
2569
|
+
'h2',
|
|
2570
|
+
'h3',
|
|
2571
|
+
'h4',
|
|
2572
|
+
'h5',
|
|
2573
|
+
'h6',
|
|
2574
|
+
'i',
|
|
2575
|
+
'li',
|
|
2576
|
+
'mark',
|
|
2577
|
+
'ol',
|
|
2578
|
+
'p',
|
|
2579
|
+
'pre',
|
|
2580
|
+
's',
|
|
2581
|
+
'strike',
|
|
2582
|
+
'strong',
|
|
2583
|
+
'sub',
|
|
2584
|
+
'sup',
|
|
2585
|
+
'u',
|
|
2586
|
+
'ul',
|
|
2587
|
+
].includes(tagName);
|
|
2588
|
+
}
|
|
2589
|
+
function hasAnchor(html) {
|
|
2590
|
+
const container = document.createElement('div');
|
|
2591
|
+
container.innerHTML = html;
|
|
2592
|
+
return Boolean(container.querySelector('a[href]'));
|
|
2593
|
+
}
|
|
2594
|
+
function hasEquivalentText(html, text) {
|
|
2595
|
+
const container = document.createElement('div');
|
|
2596
|
+
container.innerHTML = html;
|
|
2597
|
+
return normalizeText(container.textContent) === normalizeText(text);
|
|
2598
|
+
}
|
|
2599
|
+
function normalizeText(value) {
|
|
2600
|
+
return (value ?? '').replace(/\s+/g, ' ').trim();
|
|
2601
|
+
}
|
|
2602
|
+
function getClipboardHref(clipboardData, options) {
|
|
2603
|
+
const uriList = clipboardData
|
|
2604
|
+
.getData('text/uri-list')
|
|
2605
|
+
.split(/\r?\n/)
|
|
2606
|
+
.find((line) => line && !line.startsWith('#'));
|
|
2607
|
+
const normalizedUriList = normalizeHref(uriList ?? '', options);
|
|
2608
|
+
if (normalizedUriList) {
|
|
2609
|
+
return normalizedUriList;
|
|
2610
|
+
}
|
|
2611
|
+
const text = clipboardData.getData('text/plain').trim();
|
|
2612
|
+
if (!isHrefLikeText(text)) {
|
|
2613
|
+
return null;
|
|
2614
|
+
}
|
|
2615
|
+
return normalizeHref(text, options);
|
|
2616
|
+
}
|
|
2617
|
+
function isHrefLikeText(text) {
|
|
2618
|
+
return (/^(?:[a-z][a-z0-9+.-]*:|www\.|\/|#)/i.test(text) && !/\s/.test(text));
|
|
2619
|
+
}
|
|
2620
|
+
function createLinkedTextHtml(text, href) {
|
|
2621
|
+
if (!text.trim()) {
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2624
|
+
return text
|
|
2625
|
+
.replace(/\r\n?/g, '\n')
|
|
2626
|
+
.split('\n')
|
|
2627
|
+
.map((line) => `<p><a href="${escapeAttribute(href)}">${escapeHtml(line)}</a></p>`)
|
|
2628
|
+
.join('');
|
|
2629
|
+
}
|
|
2630
|
+
function createAutolinkHtml(text, options) {
|
|
2631
|
+
if (!text.trim()) {
|
|
2632
|
+
return null;
|
|
2633
|
+
}
|
|
2634
|
+
const lines = text.replace(/\r\n?/g, '\n').split('\n');
|
|
2635
|
+
const htmlLines = lines.map((line) => autolinkLine(line, options));
|
|
2636
|
+
if (!htmlLines.some((line) => line.changed)) {
|
|
2637
|
+
return null;
|
|
2638
|
+
}
|
|
2639
|
+
return htmlLines
|
|
2640
|
+
.map((line) => `<p>${line.html || '<br>'}</p>`)
|
|
2641
|
+
.join('');
|
|
2642
|
+
}
|
|
2643
|
+
function autolinkLine(line, options) {
|
|
2644
|
+
const pattern = /(?:https?:\/\/|mailto:|tel:|www\.)[^\s<>"']+/gi;
|
|
2645
|
+
let html = '';
|
|
2646
|
+
let changed = false;
|
|
2647
|
+
let position = 0;
|
|
2648
|
+
for (const match of line.matchAll(pattern)) {
|
|
2649
|
+
const rawMatch = match[0];
|
|
2650
|
+
const index = match.index ?? 0;
|
|
2651
|
+
const candidate = trimTrailingPunctuation(rawMatch);
|
|
2652
|
+
const href = normalizeHref(candidate.text, options);
|
|
2653
|
+
if (!href) {
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
html += escapeHtml(line.slice(position, index));
|
|
2657
|
+
html += `<a href="${escapeAttribute(href)}">${escapeHtml(candidate.text)}</a>`;
|
|
2658
|
+
html += escapeHtml(candidate.trailing);
|
|
2659
|
+
position = index + rawMatch.length;
|
|
2660
|
+
changed = true;
|
|
2661
|
+
}
|
|
2662
|
+
html += escapeHtml(line.slice(position));
|
|
2663
|
+
return {
|
|
2664
|
+
changed,
|
|
2665
|
+
html,
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
function trimTrailingPunctuation(text) {
|
|
2669
|
+
let candidate = text;
|
|
2670
|
+
let trailing = '';
|
|
2671
|
+
while (/[.,;:!?]$/.test(candidate)) {
|
|
2672
|
+
trailing = `${candidate[candidate.length - 1] ?? ''}${trailing}`;
|
|
2673
|
+
candidate = candidate.slice(0, -1);
|
|
2674
|
+
}
|
|
2675
|
+
while (/[)\]}]$/.test(candidate) && hasUnmatchedClosingPunctuation(candidate)) {
|
|
2676
|
+
trailing = `${candidate[candidate.length - 1] ?? ''}${trailing}`;
|
|
2677
|
+
candidate = candidate.slice(0, -1);
|
|
2678
|
+
}
|
|
2679
|
+
return {
|
|
2680
|
+
text: candidate,
|
|
2681
|
+
trailing,
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
function hasUnmatchedClosingPunctuation(value) {
|
|
2685
|
+
const last = value[value.length - 1];
|
|
2686
|
+
if (last === ')') {
|
|
2687
|
+
return countCharacters(value, ')') > countCharacters(value, '(');
|
|
2688
|
+
}
|
|
2689
|
+
if (last === ']') {
|
|
2690
|
+
return countCharacters(value, ']') > countCharacters(value, '[');
|
|
2691
|
+
}
|
|
2692
|
+
if (last === '}') {
|
|
2693
|
+
return countCharacters(value, '}') > countCharacters(value, '{');
|
|
2694
|
+
}
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
2697
|
+
function countCharacters(value, character) {
|
|
2698
|
+
return Array.from(value).filter((candidate) => candidate === character)
|
|
2699
|
+
.length;
|
|
2700
|
+
}
|
|
2701
|
+
function normalizeHref(text, options) {
|
|
2702
|
+
if (!text) {
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
const href = text.toLowerCase().startsWith('www.')
|
|
2706
|
+
? `${options.defaultProtocol}://${text}`
|
|
2707
|
+
: text;
|
|
2708
|
+
const protocol = href.match(/^([a-z][a-z0-9+.-]*):/i)?.[1].toLowerCase();
|
|
2709
|
+
if (!protocol) {
|
|
2710
|
+
return options.allowRelativeLinks ? href : null;
|
|
2711
|
+
}
|
|
2712
|
+
if (!options.allowedProtocols.includes(protocol)) {
|
|
2713
|
+
return null;
|
|
2714
|
+
}
|
|
2715
|
+
return href;
|
|
2716
|
+
}
|
|
2717
|
+
function escapeHtml(value) {
|
|
2718
|
+
return value
|
|
2719
|
+
.replace(/&/g, '&')
|
|
2720
|
+
.replace(/</g, '<')
|
|
2721
|
+
.replace(/>/g, '>');
|
|
2722
|
+
}
|
|
2723
|
+
function escapeAttribute(value) {
|
|
2724
|
+
return escapeHtml(value).replace(/"/g, '"');
|
|
2725
|
+
}
|
|
2726
|
+
function assertPasteRulesPluginOptions(options) {
|
|
2727
|
+
if (typeof options.autolink !== 'boolean') {
|
|
2728
|
+
throw new TypeError('PasteRulesPlugin autolink must be a boolean.');
|
|
2729
|
+
}
|
|
2730
|
+
if (!Array.isArray(options.allowedProtocols)) {
|
|
2731
|
+
throw new TypeError('PasteRulesPlugin allowedProtocols must be an array.');
|
|
2732
|
+
}
|
|
2733
|
+
if (options.allowedProtocols.length === 0) {
|
|
2734
|
+
throw new RangeError('PasteRulesPlugin allowedProtocols must include at least one protocol.');
|
|
2735
|
+
}
|
|
2736
|
+
for (const protocol of options.allowedProtocols) {
|
|
2737
|
+
if (typeof protocol !== 'string' ||
|
|
2738
|
+
!/^[a-z][a-z0-9+.-]*$/.test(protocol)) {
|
|
2739
|
+
throw new TypeError('PasteRulesPlugin allowedProtocols entries must be protocol names without colons.');
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
if (typeof options.allowRelativeLinks !== 'boolean') {
|
|
2743
|
+
throw new TypeError('PasteRulesPlugin allowRelativeLinks must be a boolean.');
|
|
2744
|
+
}
|
|
2745
|
+
if (typeof options.cleanHtml !== 'boolean') {
|
|
2746
|
+
throw new TypeError('PasteRulesPlugin cleanHtml must be a boolean.');
|
|
2747
|
+
}
|
|
2748
|
+
if (options.defaultProtocol !== 'http' && options.defaultProtocol !== 'https') {
|
|
2749
|
+
throw new TypeError('PasteRulesPlugin defaultProtocol must be "http" or "https".');
|
|
2750
|
+
}
|
|
2751
|
+
if (!options.allowedProtocols.includes(options.defaultProtocol)) {
|
|
2752
|
+
throw new Error('PasteRulesPlugin defaultProtocol must be included in allowedProtocols.');
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
const PLACEHOLDER_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
2757
|
+
placeholder: 'Write something...',
|
|
2758
|
+
className: 'qalma-placeholder',
|
|
2759
|
+
});
|
|
2760
|
+
const PlaceholderPlugin = createConfigurableQalmaPlugin(PLACEHOLDER_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
2761
|
+
assertPlaceholderPluginOptions(options);
|
|
2762
|
+
return createQalmaPlugin({
|
|
2763
|
+
key: 'placeholder',
|
|
2764
|
+
prosemirrorPlugins: () => [createPlaceholderProseMirrorPlugin(options)],
|
|
2765
|
+
});
|
|
2766
|
+
});
|
|
2767
|
+
const PlaceholderKit = [PlaceholderPlugin];
|
|
2768
|
+
function createPlaceholderProseMirrorPlugin(options) {
|
|
2769
|
+
return new Plugin({
|
|
2770
|
+
props: {
|
|
2771
|
+
decorations: (state) => {
|
|
2772
|
+
const target = getPlaceholderTarget(state.doc);
|
|
2773
|
+
if (!target) {
|
|
2774
|
+
return DecorationSet.empty;
|
|
2775
|
+
}
|
|
2776
|
+
return DecorationSet.create(state.doc, [
|
|
2777
|
+
Decoration.node(target.from, target.to, {
|
|
2778
|
+
class: options.className,
|
|
2779
|
+
'data-placeholder': options.placeholder,
|
|
2780
|
+
}),
|
|
2781
|
+
]);
|
|
2782
|
+
},
|
|
2783
|
+
},
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
function getPlaceholderTarget(doc) {
|
|
2787
|
+
if (doc.childCount !== 1) {
|
|
2788
|
+
return null;
|
|
2789
|
+
}
|
|
2790
|
+
const node = doc.child(0);
|
|
2791
|
+
if (!node.isTextblock || node.content.size > 0) {
|
|
2792
|
+
return null;
|
|
2793
|
+
}
|
|
2794
|
+
return {
|
|
2795
|
+
from: 0,
|
|
2796
|
+
to: node.nodeSize,
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
function assertPlaceholderPluginOptions(options) {
|
|
2800
|
+
if (typeof options.placeholder !== 'string' ||
|
|
2801
|
+
options.placeholder.trim().length === 0) {
|
|
2802
|
+
throw new Error('PlaceholderPlugin placeholder must be a non-empty string.');
|
|
2803
|
+
}
|
|
2804
|
+
if (typeof options.className !== 'string' ||
|
|
2805
|
+
options.className.trim().length === 0 ||
|
|
2806
|
+
/\s/.test(options.className)) {
|
|
2807
|
+
throw new Error('PlaceholderPlugin className must be a non-empty CSS class name.');
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
const subscriptMark = {
|
|
2812
|
+
excludes: 'superscript',
|
|
2813
|
+
parseDOM: [
|
|
2814
|
+
{ tag: 'sub' },
|
|
2815
|
+
{
|
|
2816
|
+
style: 'vertical-align',
|
|
2817
|
+
getAttrs: (value) => (String(value) === 'sub' ? null : false),
|
|
2818
|
+
},
|
|
2819
|
+
],
|
|
2820
|
+
toDOM: () => ['sub', 0],
|
|
2821
|
+
};
|
|
2822
|
+
const superscriptMark = {
|
|
2823
|
+
excludes: 'subscript',
|
|
2824
|
+
parseDOM: [
|
|
2825
|
+
{ tag: 'sup' },
|
|
2826
|
+
{
|
|
2827
|
+
style: 'vertical-align',
|
|
2828
|
+
getAttrs: (value) => (String(value) === 'super' ? null : false),
|
|
2829
|
+
},
|
|
2830
|
+
],
|
|
2831
|
+
toDOM: () => ['sup', 0],
|
|
2832
|
+
};
|
|
2833
|
+
const SubscriptSuperscriptPlugin = createQalmaPlugin({
|
|
2834
|
+
key: 'subscriptSuperscript',
|
|
2835
|
+
marks: {
|
|
2836
|
+
subscript: subscriptMark,
|
|
2837
|
+
superscript: superscriptMark,
|
|
2838
|
+
},
|
|
2839
|
+
commands: (schema) => ({
|
|
2840
|
+
toggleSubscript: toggleMark(schema.marks['subscript']),
|
|
2841
|
+
toggleSuperscript: toggleMark(schema.marks['superscript']),
|
|
2842
|
+
}),
|
|
2843
|
+
commandStates: (schema) => ({
|
|
2844
|
+
toggleSubscript: (state) => isMarkActive(state, schema.marks['subscript']),
|
|
2845
|
+
toggleSuperscript: (state) => isMarkActive(state, schema.marks['superscript']),
|
|
2846
|
+
}),
|
|
2847
|
+
});
|
|
2848
|
+
const SubscriptSuperscriptKit = [
|
|
2849
|
+
SubscriptSuperscriptPlugin,
|
|
2850
|
+
];
|
|
2851
|
+
|
|
2852
|
+
const TEXT_ALIGNMENTS = Object.freeze([
|
|
2853
|
+
'left',
|
|
2854
|
+
'center',
|
|
2855
|
+
'right',
|
|
2856
|
+
'justify',
|
|
2857
|
+
]);
|
|
2858
|
+
const TEXT_ALIGN_NODES = Object.freeze([
|
|
2859
|
+
'paragraph',
|
|
2860
|
+
'heading',
|
|
2861
|
+
'listItem',
|
|
2862
|
+
'blockquote',
|
|
2863
|
+
]);
|
|
2864
|
+
const TEXT_ALIGN_PLUGIN_DEFAULT_OPTIONS = Object.freeze({
|
|
2865
|
+
alignments: TEXT_ALIGNMENTS,
|
|
2866
|
+
nodes: TEXT_ALIGN_NODES,
|
|
2867
|
+
});
|
|
2868
|
+
const TextAlignPlugin = createConfigurableQalmaPlugin(TEXT_ALIGN_PLUGIN_DEFAULT_OPTIONS, (options) => {
|
|
2869
|
+
assertTextAlignPluginOptions(options);
|
|
2870
|
+
return createQalmaPlugin({
|
|
2871
|
+
key: 'textAlign',
|
|
2872
|
+
extendNodes: (nodes) => createTextAlignNodeExtensions(nodes, options),
|
|
2873
|
+
commands: (schema) => createTextAlignCommands(schema, options.alignments, options.nodes),
|
|
2874
|
+
commandStates: (schema) => createTextAlignCommandStates(schema, options.alignments, options.nodes),
|
|
2875
|
+
queries: (schema) => ({
|
|
2876
|
+
textAlign: (state) => getActiveTextAlign(state, getNodeTypes(schema, options.nodes)),
|
|
2877
|
+
}),
|
|
2878
|
+
});
|
|
2879
|
+
});
|
|
2880
|
+
const TextAlignKit = [TextAlignPlugin];
|
|
2881
|
+
function createTextAlignNodeExtensions(nodes, options) {
|
|
2882
|
+
return Object.fromEntries(options.nodes
|
|
2883
|
+
.filter((nodeName) => nodes[nodeName])
|
|
2884
|
+
.map((nodeName) => [
|
|
2885
|
+
nodeName,
|
|
2886
|
+
extendNodeSpecWithTextAlign(nodes[nodeName]),
|
|
2887
|
+
]));
|
|
2888
|
+
}
|
|
2889
|
+
function extendNodeSpecWithTextAlign(nodeSpec) {
|
|
2890
|
+
return {
|
|
2891
|
+
...nodeSpec,
|
|
2892
|
+
attrs: {
|
|
2893
|
+
...nodeSpec.attrs,
|
|
2894
|
+
textAlign: { default: null },
|
|
2895
|
+
},
|
|
2896
|
+
parseDOM: nodeSpec.parseDOM?.map((rule) => extendParseRuleWithTextAlign(rule)),
|
|
2897
|
+
toDOM: (node) => addTextAlignToDomSpec(nodeSpec.toDOM?.(node) ?? ['div', 0], parseTextAlignment(node.attrs['textAlign'])),
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
function extendParseRuleWithTextAlign(rule) {
|
|
2901
|
+
return {
|
|
2902
|
+
...rule,
|
|
2903
|
+
getAttrs: (dom) => {
|
|
2904
|
+
const attrs = getParseRuleAttrs(rule, dom);
|
|
2905
|
+
if (attrs === false) {
|
|
2906
|
+
return false;
|
|
2907
|
+
}
|
|
2908
|
+
return {
|
|
2909
|
+
...(attrs ?? {}),
|
|
2910
|
+
textAlign: getTextAlignFromDom(dom),
|
|
2911
|
+
};
|
|
2912
|
+
},
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
function getParseRuleAttrs(rule, dom) {
|
|
2916
|
+
const attrs = rule.getAttrs ? rule.getAttrs(dom) : (rule.attrs ?? null);
|
|
2917
|
+
return attrs === false || attrs === null ? attrs : { ...attrs };
|
|
2918
|
+
}
|
|
2919
|
+
function getTextAlignFromDom(dom) {
|
|
2920
|
+
return parseTextAlignment(dom.style.textAlign || dom.getAttribute('align'));
|
|
2921
|
+
}
|
|
2922
|
+
function addTextAlignToDomSpec(domSpec, alignment) {
|
|
2923
|
+
if (!alignment || alignment === 'left' || !Array.isArray(domSpec)) {
|
|
2924
|
+
return domSpec;
|
|
2925
|
+
}
|
|
2926
|
+
const [tagName, maybeAttrs, ...children] = domSpec;
|
|
2927
|
+
const textAlignStyle = `text-align: ${alignment};`;
|
|
2928
|
+
if (isDomAttrs(maybeAttrs)) {
|
|
2929
|
+
return [
|
|
2930
|
+
tagName,
|
|
2931
|
+
{
|
|
2932
|
+
...maybeAttrs,
|
|
2933
|
+
style: joinStyles(maybeAttrs['style'], textAlignStyle),
|
|
2934
|
+
},
|
|
2935
|
+
...children,
|
|
2936
|
+
];
|
|
2937
|
+
}
|
|
2938
|
+
return [tagName, { style: textAlignStyle }, maybeAttrs, ...children];
|
|
2939
|
+
}
|
|
2940
|
+
function isDomAttrs(value) {
|
|
2941
|
+
return (typeof value === 'object' &&
|
|
2942
|
+
value !== null &&
|
|
2943
|
+
!Array.isArray(value) &&
|
|
2944
|
+
!('nodeType' in value));
|
|
2945
|
+
}
|
|
2946
|
+
function joinStyles(existingStyle, textAlignStyle) {
|
|
2947
|
+
return existingStyle
|
|
2948
|
+
? `${existingStyle.replace(/;?\s*$/, ';')} ${textAlignStyle}`
|
|
2949
|
+
: textAlignStyle;
|
|
2950
|
+
}
|
|
2951
|
+
function createTextAlignCommands(schema, alignments, nodes) {
|
|
2952
|
+
const nodeTypes = getNodeTypes(schema, nodes);
|
|
2953
|
+
return Object.fromEntries(alignments.map((alignment) => [
|
|
2954
|
+
getTextAlignCommandName(alignment),
|
|
2955
|
+
createSetTextAlignCommand(nodeTypes, alignment),
|
|
2956
|
+
]));
|
|
2957
|
+
}
|
|
2958
|
+
function createTextAlignCommandStates(schema, alignments, nodes) {
|
|
2959
|
+
const nodeTypes = getNodeTypes(schema, nodes);
|
|
2960
|
+
return Object.fromEntries(alignments.map((alignment) => [
|
|
2961
|
+
getTextAlignCommandName(alignment),
|
|
2962
|
+
(state) => getActiveTextAlign(state, nodeTypes) === alignment,
|
|
2963
|
+
]));
|
|
2964
|
+
}
|
|
2965
|
+
function createSetTextAlignCommand(nodeTypes, alignment) {
|
|
2966
|
+
return (state, dispatch) => {
|
|
2967
|
+
const targets = getSelectedAlignmentTargets(state, nodeTypes);
|
|
2968
|
+
if (targets.length === 0) {
|
|
2969
|
+
return false;
|
|
2970
|
+
}
|
|
2971
|
+
if (dispatch) {
|
|
2972
|
+
const transaction = state.tr;
|
|
2973
|
+
const textAlign = alignment === 'left' ? null : alignment;
|
|
2974
|
+
for (const target of targets) {
|
|
2975
|
+
transaction.setNodeMarkup(target.position, undefined, {
|
|
2976
|
+
...target.node.attrs,
|
|
2977
|
+
textAlign,
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
dispatch(transaction.scrollIntoView());
|
|
2981
|
+
}
|
|
2982
|
+
return true;
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
function getSelectedAlignmentTargets(state, nodeTypes) {
|
|
2986
|
+
if (nodeTypes.length === 0) {
|
|
2987
|
+
return [];
|
|
2988
|
+
}
|
|
2989
|
+
if (state.selection.empty) {
|
|
2990
|
+
const target = findAlignmentTarget(state.selection.$from, nodeTypes);
|
|
2991
|
+
return target ? [target] : [];
|
|
2992
|
+
}
|
|
2993
|
+
const targets = new Map();
|
|
2994
|
+
for (const range of state.selection.ranges) {
|
|
2995
|
+
state.doc.nodesBetween(range.$from.pos, range.$to.pos, (node, position) => {
|
|
2996
|
+
if (!node.isTextblock && !nodeTypes.includes(node.type)) {
|
|
2997
|
+
return undefined;
|
|
2998
|
+
}
|
|
2999
|
+
const target = findAlignmentTarget(state.doc.resolve(position + 1), nodeTypes);
|
|
3000
|
+
if (target) {
|
|
3001
|
+
targets.set(target.position, target);
|
|
3002
|
+
}
|
|
3003
|
+
if (nodeTypes.includes(node.type)) {
|
|
3004
|
+
return false;
|
|
3005
|
+
}
|
|
3006
|
+
return undefined;
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
return Array.from(targets.values());
|
|
3010
|
+
}
|
|
3011
|
+
function findAlignmentTarget($pos, nodeTypes) {
|
|
3012
|
+
let textblockTarget = null;
|
|
3013
|
+
for (let depth = $pos.depth; depth > 0; depth -= 1) {
|
|
3014
|
+
const node = $pos.node(depth);
|
|
3015
|
+
if (!nodeTypes.includes(node.type)) {
|
|
3016
|
+
continue;
|
|
3017
|
+
}
|
|
3018
|
+
const target = { node, position: $pos.before(depth) };
|
|
3019
|
+
if (!node.isTextblock) {
|
|
3020
|
+
return target;
|
|
3021
|
+
}
|
|
3022
|
+
textblockTarget = target;
|
|
3023
|
+
}
|
|
3024
|
+
return textblockTarget;
|
|
3025
|
+
}
|
|
3026
|
+
function getActiveTextAlign(state, nodeTypes) {
|
|
3027
|
+
const target = getSelectedAlignmentTargets(state, nodeTypes)[0];
|
|
3028
|
+
return target ? getNodeTextAlign(target.node) : null;
|
|
3029
|
+
}
|
|
3030
|
+
function getNodeTextAlign(node) {
|
|
3031
|
+
return parseTextAlignment(node.attrs['textAlign']) ?? 'left';
|
|
3032
|
+
}
|
|
3033
|
+
function getNodeTypes(schema, nodeNames) {
|
|
3034
|
+
return nodeNames
|
|
3035
|
+
.map((nodeName) => schema.nodes[nodeName])
|
|
3036
|
+
.filter((node) => Boolean(node));
|
|
3037
|
+
}
|
|
3038
|
+
function getTextAlignCommandName(alignment) {
|
|
3039
|
+
return `setTextAlign${alignment[0].toUpperCase()}${alignment.slice(1)}`;
|
|
3040
|
+
}
|
|
3041
|
+
function parseTextAlignment(value) {
|
|
3042
|
+
return TEXT_ALIGNMENTS.includes(value)
|
|
3043
|
+
? value
|
|
3044
|
+
: null;
|
|
3045
|
+
}
|
|
3046
|
+
function assertTextAlignPluginOptions(options) {
|
|
3047
|
+
assertUniqueOptionEntries(options.alignments, TEXT_ALIGNMENTS, 'TextAlignPlugin alignments');
|
|
3048
|
+
assertUniqueOptionEntries(options.nodes, TEXT_ALIGN_NODES, 'TextAlignPlugin nodes');
|
|
3049
|
+
}
|
|
3050
|
+
function assertUniqueOptionEntries(values, allowedValues, optionName) {
|
|
3051
|
+
if (!Array.isArray(values)) {
|
|
3052
|
+
throw new TypeError(`${optionName} must be an array.`);
|
|
3053
|
+
}
|
|
3054
|
+
if (values.length === 0) {
|
|
3055
|
+
throw new RangeError(`${optionName} must include at least one value.`);
|
|
3056
|
+
}
|
|
3057
|
+
const seen = new Set();
|
|
3058
|
+
for (const value of values) {
|
|
3059
|
+
if (!allowedValues.includes(value)) {
|
|
3060
|
+
throw new RangeError(`${optionName} entries include an unsupported value.`);
|
|
3061
|
+
}
|
|
3062
|
+
if (seen.has(value)) {
|
|
3063
|
+
throw new Error(`${optionName} entries must be unique.`);
|
|
3064
|
+
}
|
|
3065
|
+
seen.add(value);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
const strongMark = {
|
|
3070
|
+
parseDOM: [
|
|
3071
|
+
{ tag: 'strong' },
|
|
3072
|
+
{ tag: 'b' },
|
|
3073
|
+
{
|
|
3074
|
+
style: 'font-weight',
|
|
3075
|
+
getAttrs: (value) => {
|
|
3076
|
+
const weight = String(value);
|
|
3077
|
+
return weight === 'bold' || weight === 'bolder' || Number(weight) >= 500
|
|
3078
|
+
? null
|
|
3079
|
+
: false;
|
|
3080
|
+
},
|
|
3081
|
+
},
|
|
3082
|
+
],
|
|
3083
|
+
toDOM: () => ['strong', 0],
|
|
3084
|
+
};
|
|
3085
|
+
const emMark = {
|
|
3086
|
+
parseDOM: [{ tag: 'em' }, { tag: 'i' }, { style: 'font-style=italic' }],
|
|
3087
|
+
toDOM: () => ['em', 0],
|
|
3088
|
+
};
|
|
3089
|
+
const underlineMark = {
|
|
3090
|
+
parseDOM: [
|
|
3091
|
+
{ tag: 'u' },
|
|
3092
|
+
{
|
|
3093
|
+
style: 'text-decoration',
|
|
3094
|
+
getAttrs: (value) => (String(value).includes('underline') ? null : false),
|
|
3095
|
+
},
|
|
3096
|
+
],
|
|
3097
|
+
toDOM: () => ['u', 0],
|
|
3098
|
+
};
|
|
3099
|
+
const strikeMark = {
|
|
3100
|
+
parseDOM: [
|
|
3101
|
+
{ tag: 's' },
|
|
3102
|
+
{ tag: 'strike' },
|
|
3103
|
+
{ tag: 'del' },
|
|
3104
|
+
{
|
|
3105
|
+
style: 'text-decoration',
|
|
3106
|
+
getAttrs: (value) => String(value).includes('line-through') ? null : false,
|
|
3107
|
+
},
|
|
3108
|
+
],
|
|
3109
|
+
toDOM: () => ['s', 0],
|
|
3110
|
+
};
|
|
3111
|
+
const BoldPlugin = createQalmaPlugin({
|
|
3112
|
+
key: 'bold',
|
|
3113
|
+
marks: {
|
|
3114
|
+
strong: strongMark,
|
|
3115
|
+
},
|
|
3116
|
+
commands: (schema) => ({
|
|
3117
|
+
toggleBold: toggleMark(schema.marks['strong']),
|
|
3118
|
+
}),
|
|
3119
|
+
commandStates: (schema) => ({
|
|
3120
|
+
toggleBold: (state) => isMarkActive(state, schema.marks['strong']),
|
|
3121
|
+
}),
|
|
3122
|
+
shortcuts: (schema) => ({
|
|
3123
|
+
'Mod-b': toggleMark(schema.marks['strong']),
|
|
3124
|
+
}),
|
|
3125
|
+
});
|
|
3126
|
+
const ItalicPlugin = createQalmaPlugin({
|
|
3127
|
+
key: 'italic',
|
|
3128
|
+
marks: {
|
|
3129
|
+
em: emMark,
|
|
3130
|
+
},
|
|
3131
|
+
commands: (schema) => ({
|
|
3132
|
+
toggleItalic: toggleMark(schema.marks['em']),
|
|
3133
|
+
}),
|
|
3134
|
+
commandStates: (schema) => ({
|
|
3135
|
+
toggleItalic: (state) => isMarkActive(state, schema.marks['em']),
|
|
3136
|
+
}),
|
|
3137
|
+
shortcuts: (schema) => ({
|
|
3138
|
+
'Mod-i': toggleMark(schema.marks['em']),
|
|
3139
|
+
}),
|
|
3140
|
+
});
|
|
3141
|
+
const UnderlinePlugin = createQalmaPlugin({
|
|
3142
|
+
key: 'underline',
|
|
3143
|
+
marks: {
|
|
3144
|
+
underline: underlineMark,
|
|
3145
|
+
},
|
|
3146
|
+
commands: (schema) => ({
|
|
3147
|
+
toggleUnderline: toggleMark(schema.marks['underline']),
|
|
3148
|
+
}),
|
|
3149
|
+
commandStates: (schema) => ({
|
|
3150
|
+
toggleUnderline: (state) => isMarkActive(state, schema.marks['underline']),
|
|
3151
|
+
}),
|
|
3152
|
+
shortcuts: (schema) => ({
|
|
3153
|
+
'Mod-u': toggleMark(schema.marks['underline']),
|
|
3154
|
+
}),
|
|
3155
|
+
});
|
|
3156
|
+
const StrikePlugin = createQalmaPlugin({
|
|
3157
|
+
key: 'strike',
|
|
3158
|
+
marks: {
|
|
3159
|
+
strike: strikeMark,
|
|
3160
|
+
},
|
|
3161
|
+
commands: (schema) => ({
|
|
3162
|
+
toggleStrike: toggleMark(schema.marks['strike']),
|
|
3163
|
+
}),
|
|
3164
|
+
commandStates: (schema) => ({
|
|
3165
|
+
toggleStrike: (state) => isMarkActive(state, schema.marks['strike']),
|
|
3166
|
+
}),
|
|
3167
|
+
});
|
|
3168
|
+
const TextFormattingKit = [
|
|
3169
|
+
BoldPlugin,
|
|
3170
|
+
ItalicPlugin,
|
|
3171
|
+
UnderlinePlugin,
|
|
3172
|
+
StrikePlugin,
|
|
3173
|
+
];
|
|
3174
|
+
|
|
3175
|
+
const TrailingParagraphPlugin = createQalmaPlugin({
|
|
3176
|
+
key: 'trailingParagraph',
|
|
3177
|
+
prosemirrorPlugins: (schema) => [
|
|
3178
|
+
createTrailingParagraphProseMirrorPlugin(schema.nodes['paragraph']),
|
|
3179
|
+
],
|
|
3180
|
+
});
|
|
3181
|
+
const TrailingParagraphKit = [
|
|
3182
|
+
TrailingParagraphPlugin,
|
|
3183
|
+
];
|
|
3184
|
+
function createTrailingParagraphProseMirrorPlugin(paragraph) {
|
|
3185
|
+
return new Plugin({
|
|
3186
|
+
appendTransaction: (_transactions, _oldState, newState) => createTrailingParagraphTransaction(newState, paragraph),
|
|
3187
|
+
view: (view) => {
|
|
3188
|
+
queueTrailingParagraphUpdate(view, paragraph);
|
|
3189
|
+
return {
|
|
3190
|
+
update: (updatedView) => queueTrailingParagraphUpdate(updatedView, paragraph),
|
|
3191
|
+
};
|
|
3192
|
+
},
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
3195
|
+
function queueTrailingParagraphUpdate(view, paragraph) {
|
|
3196
|
+
queueMicrotask(() => {
|
|
3197
|
+
if (view.isDestroyed) {
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
const transaction = createTrailingParagraphTransaction(view.state, paragraph);
|
|
3201
|
+
if (transaction) {
|
|
3202
|
+
view.dispatch(transaction);
|
|
3203
|
+
}
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
3206
|
+
function createTrailingParagraphTransaction(state, paragraph) {
|
|
3207
|
+
const lastChild = state.doc.lastChild;
|
|
3208
|
+
if (lastChild?.type === paragraph &&
|
|
3209
|
+
lastChild.isTextblock &&
|
|
3210
|
+
lastChild.content.size === 0) {
|
|
3211
|
+
return null;
|
|
3212
|
+
}
|
|
3213
|
+
const paragraphNode = paragraph.createAndFill();
|
|
3214
|
+
if (!paragraphNode ||
|
|
3215
|
+
!state.doc.canReplaceWith(state.doc.childCount, state.doc.childCount, paragraph)) {
|
|
3216
|
+
return null;
|
|
3217
|
+
}
|
|
3218
|
+
return state.tr
|
|
3219
|
+
.insert(state.doc.content.size, paragraphNode)
|
|
3220
|
+
.setMeta('addToHistory', false);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
/**
|
|
3224
|
+
* Generated bundle index. Do not edit.
|
|
3225
|
+
*/
|
|
3226
|
+
|
|
3227
|
+
export { BlockquoteKit, BlockquotePlugin, BoldPlugin, CODE_BLOCK_PLUGIN_DEFAULT_OPTIONS, ClearFormattingKit, ClearFormattingPlugin, CodeBlockKit, CodeBlockPlugin, ColorKit, ColorPlugin, HEADINGS_PLUGIN_DEFAULT_OPTIONS, HEADING_LEVELS, HIGHLIGHT_PLUGIN_DEFAULT_OPTIONS, HISTORY_PLUGIN_DEFAULT_OPTIONS, HardBreakKit, HardBreakPlugin, HeadingsKit, HeadingsPlugin, HighlightKit, HighlightPlugin, HistoryPlugin, IMAGE_PLUGIN_DEFAULT_OPTIONS, ImageKit, ImagePlugin, ItalicPlugin, LINK_PLUGIN_DEFAULT_OPTIONS, LinkPlugin, ListsKit, ListsPlugin, MENTION_PLUGIN_DEFAULT_OPTIONS, MentionKit, MentionPlugin, PASTE_RULES_PLUGIN_DEFAULT_OPTIONS, PLACEHOLDER_PLUGIN_DEFAULT_OPTIONS, PasteRulesPlugin, PlaceholderKit, PlaceholderPlugin, QalmaCommand, QalmaContent, QalmaEditor, QalmaEditorController, QalmaToolbar, StrikePlugin, SubscriptSuperscriptKit, SubscriptSuperscriptPlugin, TEXT_ALIGNMENTS, TEXT_ALIGN_NODES, TEXT_ALIGN_PLUGIN_DEFAULT_OPTIONS, TextAlignKit, TextAlignPlugin, TextFormattingKit, TrailingParagraphKit, TrailingParagraphPlugin, UnderlinePlugin, createConfigurableQalmaPlugin, createQalmaEditor, createQalmaPlugin };
|
|
3228
|
+
//# sourceMappingURL=qalma-editor.mjs.map
|