@aicayzer/inkkit 0.0.6 → 0.0.8
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/NOTICE +25 -2
- package/README.md +16 -14
- package/dist/callouts.d.ts +2 -0
- package/dist/callouts.js +17 -3
- package/dist/decorations.js +7 -6
- package/dist/dialect.js +10 -2
- package/dist/editor.d.ts +90 -7
- package/dist/editor.js +806 -116
- package/dist/images.d.ts +1 -0
- package/dist/images.js +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/labels.d.ts +28 -0
- package/dist/labels.js +26 -0
- package/dist/mermaid.js +6 -3
- package/dist/outline.d.ts +22 -0
- package/dist/outline.js +79 -0
- package/dist/paste.d.ts +1 -0
- package/dist/paste.js +5 -1
- package/dist/references.js +5 -4
- package/dist/search.d.ts +14 -0
- package/dist/search.js +171 -0
- package/dist/selection.js +22 -10
- package/dist/source.d.ts +17 -0
- package/dist/source.js +117 -0
- package/dist/style.css +377 -299
- package/dist/tables.d.ts +10 -1
- package/dist/tables.js +87 -11
- package/dist/tasks.js +2 -0
- package/dist/types.d.ts +1 -1
- package/package.json +4 -2
package/dist/editor.js
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
|
-
import { Editor, defaultValueCtx, editorViewCtx, editorViewOptionsCtx, remarkCtx, remarkStringifyOptionsCtx, rootCtx, } from '@milkdown/kit/core';
|
|
1
|
+
import { Editor, commandsCtx, defaultValueCtx, editorViewCtx, parserCtx, editorViewOptionsCtx, remarkCtx, remarkStringifyOptionsCtx, rootCtx, } from '@milkdown/kit/core';
|
|
2
2
|
import { visibleClipboard, portableClipboard, clipboardText, clipboardContent, } from './clipboard.js';
|
|
3
3
|
import { clipboard } from '@milkdown/kit/plugin/clipboard';
|
|
4
|
-
import { history } from '@milkdown/kit/plugin/history';
|
|
4
|
+
import { history, historyKeymap } from '@milkdown/kit/plugin/history';
|
|
5
|
+
import { closeHistory, undo as undoHistory, redo as redoHistory, } from '@milkdown/kit/prose/history';
|
|
5
6
|
import { cursor } from '@milkdown/kit/plugin/cursor';
|
|
6
7
|
import { blockquoteKeymap, blockquoteSchema, bulletListKeymap, codeBlockKeymap, createCodeBlockCommand, emphasisKeymap, headingKeymap, inlineCodeKeymap, inlineCodeSchema, linkSchema, listItemKeymap, orderedListKeymap, paragraphKeymap, strongKeymap, toggleEmphasisCommand, toggleInlineCodeCommand, toggleLinkCommand, toggleStrongCommand, turnIntoTextCommand, wrapInBulletListCommand, wrapInHeadingCommand, wrapInOrderedListCommand, } from '@milkdown/kit/preset/commonmark';
|
|
7
|
-
import { strikethroughKeymap, toggleStrikethroughCommand, } from '@milkdown/kit/preset/gfm';
|
|
8
|
+
import { strikethroughKeymap, tableKeymap, toggleStrikethroughCommand, } from '@milkdown/kit/preset/gfm';
|
|
8
9
|
import { NodeRange, Fragment, Slice, } from '@milkdown/kit/prose/model';
|
|
9
10
|
import { keydownHandler, keymap } from '@milkdown/kit/prose/keymap';
|
|
11
|
+
import { goToNextCell } from '@milkdown/kit/prose/tables';
|
|
10
12
|
import { liftListItem } from '@milkdown/kit/prose/schema-list';
|
|
11
13
|
import { findWrapping, liftTarget } from '@milkdown/kit/prose/transform';
|
|
12
14
|
import { AllSelection, NodeSelection, Plugin, PluginKey, Selection, TextSelection, } from '@milkdown/kit/prose/state';
|
|
13
15
|
import { $prose, callCommand, replaceAll, } from '@milkdown/kit/utils';
|
|
14
16
|
import { codeCopyPlugin, placeholderPlugin } from './decorations.js';
|
|
15
|
-
import {
|
|
17
|
+
import { labelsCtx, defaultLabels } from './labels.js';
|
|
18
|
+
import { createDialect, stringifyOptions } from './dialect.js';
|
|
16
19
|
import { Preservation } from './preserve.js';
|
|
20
|
+
import { sourceAttribute, cleanSourceDoc, editedPlainSource, sourceSelection, revealSourceCaret, rawOffset, normalizedOffset, } from './source.js';
|
|
17
21
|
import { normaliseLabel, referenceDefinitions, footnoteDefinitions, } from './references.js';
|
|
18
22
|
import { selectionMarkdown, selectionContent, referenceMetadata, withReferenceMetadata, } from './reference-clipboard.js';
|
|
19
23
|
import { imageView } from './images.js';
|
|
20
|
-
import { tablePlugins, tableCommand, configureTableMovement, } from './tables.js';
|
|
24
|
+
import { tablePlugins, tableCommand, tableCommands, tableAvailability, tableContext, configureTableMovement, } from './tables.js';
|
|
21
25
|
import { InkKitError, } from './types.js';
|
|
22
26
|
import { highlightPlugin } from './highlight.js';
|
|
23
27
|
import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
|
|
24
28
|
import { commentSelectionContent, setCommentVisibility } from './comments.js';
|
|
25
29
|
import { PasteController } from './paste.js';
|
|
26
30
|
import { selectionPlugin } from './selection.js';
|
|
27
|
-
import { search, SearchQuery,
|
|
31
|
+
import { search, SearchQuery, setSearchState } from 'prosemirror-search';
|
|
32
|
+
import { findFormatted, findLiteral, replaceFormatted, replaceLiteral, } from './search.js';
|
|
33
|
+
import { collectFormattedHeadings, collectSourceHeadings, navigateFormattedHeading, sourceHeadingPosition, } from './outline.js';
|
|
28
34
|
import { taskListPlugin, toggleTaskList } from './tasks.js';
|
|
29
35
|
import { isMermaid, mermaidPreview } from './mermaid.js';
|
|
30
36
|
import { hasAuthoredImagesInLiterals, printableMarkdown, printableText, } from './print.js';
|
|
@@ -32,6 +38,9 @@ const shortcutCommands = {
|
|
|
32
38
|
heading1: ['heading', 1],
|
|
33
39
|
heading2: ['heading', 2],
|
|
34
40
|
heading3: ['heading', 3],
|
|
41
|
+
heading4: ['heading', 4],
|
|
42
|
+
heading5: ['heading', 5],
|
|
43
|
+
heading6: ['heading', 6],
|
|
35
44
|
paragraph: ['paragraph'],
|
|
36
45
|
bold: ['bold'],
|
|
37
46
|
italic: ['italic'],
|
|
@@ -187,11 +196,13 @@ const dropControlCharacters = $prose(() => new Plugin({
|
|
|
187
196
|
}));
|
|
188
197
|
// The listener plugin reports selection changes from inside state.apply, before
|
|
189
198
|
// the view holds the new state, so the caret state is read from the view instead.
|
|
190
|
-
function caretStatePlugin(events) {
|
|
199
|
+
function caretStatePlugin(events, active = () => true) {
|
|
191
200
|
return $prose(() => new Plugin({
|
|
192
201
|
key: new PluginKey('caretState'),
|
|
193
202
|
view: () => ({
|
|
194
203
|
update(view, previous) {
|
|
204
|
+
if (!active())
|
|
205
|
+
return;
|
|
195
206
|
const { state } = view;
|
|
196
207
|
if (state.selection.eq(previous.selection) &&
|
|
197
208
|
state.doc.eq(previous.doc) &&
|
|
@@ -233,7 +244,6 @@ export class InkKitEditor {
|
|
|
233
244
|
options;
|
|
234
245
|
editor;
|
|
235
246
|
lastMarkdown = '';
|
|
236
|
-
baseline = '';
|
|
237
247
|
documentId = '';
|
|
238
248
|
formatType = 'md';
|
|
239
249
|
revision = 0;
|
|
@@ -241,18 +251,31 @@ export class InkKitEditor {
|
|
|
241
251
|
operationSequence = 0;
|
|
242
252
|
plainComposing = false;
|
|
243
253
|
plainSearch = '';
|
|
254
|
+
literalMatch;
|
|
255
|
+
beforePlainInput;
|
|
256
|
+
mode = 'formatted';
|
|
257
|
+
sourcePreservations = new WeakMap();
|
|
258
|
+
outlineEpoch = 0;
|
|
244
259
|
ready = false;
|
|
245
260
|
destroyed = false;
|
|
246
261
|
preservation;
|
|
247
262
|
pasteController;
|
|
248
263
|
plain;
|
|
249
264
|
root;
|
|
265
|
+
ownsRootClass = false;
|
|
250
266
|
clickHandler;
|
|
251
267
|
footnoteKeyHandler;
|
|
252
268
|
copyHandler;
|
|
253
269
|
originalSource = '';
|
|
254
270
|
generation = 0;
|
|
271
|
+
reportingSuppressed = false;
|
|
255
272
|
loading = false;
|
|
273
|
+
writable = true;
|
|
274
|
+
inputPreferences = {};
|
|
275
|
+
policyEpoch = 0;
|
|
276
|
+
literalKeys = () => false;
|
|
277
|
+
configuredKeys = {};
|
|
278
|
+
readOnlyKeys = () => false;
|
|
256
279
|
changePlugin = $prose(() => new Plugin({
|
|
257
280
|
key: new PluginKey('documentChanges'),
|
|
258
281
|
view: () => ({
|
|
@@ -260,20 +283,30 @@ export class InkKitEditor {
|
|
|
260
283
|
if (this.loading || view.state.doc.eq(previous.doc))
|
|
261
284
|
return;
|
|
262
285
|
this.revision += 1;
|
|
286
|
+
this.literalMatch = undefined;
|
|
263
287
|
let markdown;
|
|
264
288
|
try {
|
|
265
|
-
markdown =
|
|
266
|
-
this.preservation?.serialize(view.state.doc) ??
|
|
267
|
-
serialize(this.editor.ctx, view.state.doc);
|
|
289
|
+
markdown = this.currentText(view.state.doc);
|
|
268
290
|
}
|
|
269
291
|
catch (error) {
|
|
270
292
|
this.events.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
271
293
|
return;
|
|
272
294
|
}
|
|
295
|
+
if (this.literalSurface) {
|
|
296
|
+
const anchor = view.state.doc.attrs[sourceAttribute];
|
|
297
|
+
const from = anchor?.from ?? this.plain.selectionStart;
|
|
298
|
+
const to = anchor?.to ?? this.plain.selectionEnd;
|
|
299
|
+
if (!this.plainComposing)
|
|
300
|
+
this.plain.value = markdown;
|
|
301
|
+
if (!this.plainComposing) {
|
|
302
|
+
this.plain.setSelectionRange(Math.min(from, this.plain.value.length), Math.min(to, this.plain.value.length));
|
|
303
|
+
revealSourceCaret(this.plain);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
273
306
|
if (markdown === this.lastMarkdown)
|
|
274
307
|
return;
|
|
275
308
|
this.lastMarkdown = markdown;
|
|
276
|
-
this.events.changed(markdown
|
|
309
|
+
this.events.changed(markdown, this.generation);
|
|
277
310
|
},
|
|
278
311
|
}),
|
|
279
312
|
}));
|
|
@@ -281,6 +314,8 @@ export class InkKitEditor {
|
|
|
281
314
|
constructor(events, options) {
|
|
282
315
|
this.events = events;
|
|
283
316
|
this.options = options;
|
|
317
|
+
this.writable = options.editable ?? true;
|
|
318
|
+
this.inputPreferences = { ...options.textInput };
|
|
284
319
|
}
|
|
285
320
|
context() {
|
|
286
321
|
return {
|
|
@@ -299,29 +334,367 @@ export class InkKitEditor {
|
|
|
299
334
|
throw new InkKitError('not-ready', 'No document has been loaded');
|
|
300
335
|
if (generation !== this.generation)
|
|
301
336
|
throw new InkKitError('stale-document', 'Document changed');
|
|
302
|
-
if (this.
|
|
337
|
+
if (this.literalSurface
|
|
303
338
|
? this.plainComposing
|
|
304
339
|
: this.editor.ctx.get(editorViewCtx).composing)
|
|
305
340
|
throw new InkKitError('composition', 'Text composition is in progress');
|
|
306
341
|
if (this.pasteController.pending)
|
|
307
342
|
throw new InkKitError('operation-pending', 'An image import is in progress');
|
|
308
343
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (this.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return
|
|
344
|
+
assertMutation(generation = this.generation) {
|
|
345
|
+
this.assertCurrent(generation);
|
|
346
|
+
if (!this.writable)
|
|
347
|
+
throw new InkKitError('read-only', 'The editor is read-only');
|
|
348
|
+
}
|
|
349
|
+
get composing() {
|
|
350
|
+
return this.literalSurface
|
|
351
|
+
? this.plainComposing
|
|
352
|
+
: this.editor.ctx.get(editorViewCtx).composing;
|
|
353
|
+
}
|
|
354
|
+
get editable() {
|
|
355
|
+
this.assertAlive();
|
|
356
|
+
return this.writable;
|
|
357
|
+
}
|
|
358
|
+
setEditable(editable) {
|
|
359
|
+
this.assertAlive();
|
|
360
|
+
if (this.composing)
|
|
361
|
+
throw new InkKitError('composition', 'Text composition is in progress');
|
|
362
|
+
if (this.writable === editable)
|
|
363
|
+
return;
|
|
364
|
+
const suppressed = this.reportingSuppressed;
|
|
365
|
+
this.reportingSuppressed = true;
|
|
366
|
+
try {
|
|
367
|
+
this.writable = editable;
|
|
368
|
+
this.policyEpoch += 1;
|
|
369
|
+
if (!editable)
|
|
370
|
+
this.pasteController.cancelPending();
|
|
371
|
+
this.editor.ctx
|
|
372
|
+
.get(editorViewCtx)
|
|
373
|
+
.dom.dispatchEvent(new Event('inkkit-cancel-resize'));
|
|
374
|
+
this.applyInputPolicy();
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
this.reportingSuppressed = suppressed;
|
|
378
|
+
}
|
|
379
|
+
this.publishCommandState();
|
|
380
|
+
}
|
|
381
|
+
setTextInputPreferences(preferences) {
|
|
382
|
+
this.assertAlive();
|
|
383
|
+
if (this.composing)
|
|
384
|
+
throw new InkKitError('composition', 'Text composition is in progress');
|
|
385
|
+
const suppressed = this.reportingSuppressed;
|
|
386
|
+
this.reportingSuppressed = true;
|
|
387
|
+
try {
|
|
388
|
+
this.inputPreferences = { ...preferences };
|
|
389
|
+
this.applyInputPolicy();
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
this.reportingSuppressed = suppressed;
|
|
393
|
+
}
|
|
394
|
+
this.publishCommandState();
|
|
395
|
+
}
|
|
396
|
+
applyInputPolicy() {
|
|
397
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
398
|
+
const attributes = typeof view.props.attributes === 'function'
|
|
399
|
+
? view.props.attributes(view.state)
|
|
400
|
+
: view.props.attributes;
|
|
401
|
+
view.setProps({
|
|
402
|
+
editable: () => this.writable && !this.literalSurface,
|
|
403
|
+
attributes: {
|
|
404
|
+
...attributes,
|
|
405
|
+
role: 'textbox',
|
|
406
|
+
'aria-multiline': 'true',
|
|
407
|
+
'aria-label': this.options.labels?.formattedEditor ?? defaultLabels.formattedEditor,
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
this.plain.readOnly = !this.writable;
|
|
411
|
+
for (const surface of [view.dom, this.plain]) {
|
|
412
|
+
for (const name of [
|
|
413
|
+
'spellcheck',
|
|
414
|
+
'autocorrect',
|
|
415
|
+
'autocapitalize',
|
|
416
|
+
]) {
|
|
417
|
+
const value = this.inputPreferences[name];
|
|
418
|
+
if (value == null)
|
|
419
|
+
surface.removeAttribute(name);
|
|
420
|
+
else
|
|
421
|
+
surface.setAttribute(name, name === 'autocorrect' ? (value ? 'on' : 'off') : String(value));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
publishCommandState() {
|
|
426
|
+
if (!this.ready ||
|
|
427
|
+
this.loading ||
|
|
428
|
+
this.reportingSuppressed ||
|
|
429
|
+
this.destroyed)
|
|
430
|
+
return;
|
|
431
|
+
this.events.commandStateChanged?.(this.commandState());
|
|
432
|
+
}
|
|
433
|
+
commandState(expectedGeneration = this.generation) {
|
|
434
|
+
this.assertAlive();
|
|
435
|
+
if (!this.ready)
|
|
436
|
+
throw new InkKitError('not-ready', 'No document has been loaded');
|
|
437
|
+
if (expectedGeneration !== this.generation)
|
|
438
|
+
throw new InkKitError('stale-document', 'Document changed');
|
|
439
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
440
|
+
const { state } = view;
|
|
441
|
+
const mutable = this.writable && !this.composing && !this.pasteController.pending;
|
|
442
|
+
const formatted = mutable && !this.literalSurface;
|
|
443
|
+
const manager = this.editor.ctx.get(commandsCtx);
|
|
444
|
+
const check = (key, payload) => formatted && manager.get(key)(payload)(state);
|
|
445
|
+
const format = {
|
|
446
|
+
heading: check(wrapInHeadingCommand.key, 1) || check(turnIntoTextCommand.key),
|
|
447
|
+
paragraph: check(turnIntoTextCommand.key),
|
|
448
|
+
bold: check(toggleStrongCommand.key),
|
|
449
|
+
italic: check(toggleEmphasisCommand.key),
|
|
450
|
+
strikethrough: check(toggleStrikethroughCommand.key),
|
|
451
|
+
highlight: check(toggleHighlightCommand.key),
|
|
452
|
+
code: formatted &&
|
|
453
|
+
(state.selection.empty
|
|
454
|
+
? state.selection.$from.parent.inlineContent &&
|
|
455
|
+
state.selection.$from.parent.type.allowsMarkType(inlineCodeSchema.type(this.editor.ctx))
|
|
456
|
+
: check(toggleInlineCodeCommand.key)),
|
|
457
|
+
codeBlock: check(createCodeBlockCommand.key, '') || check(turnIntoTextCommand.key),
|
|
458
|
+
quote: formatted && this.canQuote(state),
|
|
459
|
+
bulletList: formatted &&
|
|
460
|
+
(innermostListDepth(textBounds(state).$from) != null ||
|
|
461
|
+
check(wrapInBulletListCommand.key)),
|
|
462
|
+
orderedList: formatted &&
|
|
463
|
+
(innermostListDepth(textBounds(state).$from) != null ||
|
|
464
|
+
check(wrapInOrderedListCommand.key)),
|
|
465
|
+
taskList: formatted &&
|
|
466
|
+
(innermostListDepth(textBounds(state).$from) != null ||
|
|
467
|
+
check(wrapInBulletListCommand.key)),
|
|
468
|
+
link: check(toggleLinkCommand.key, {}),
|
|
469
|
+
};
|
|
470
|
+
const tables = formatted
|
|
471
|
+
? tableAvailability(this.editor.ctx)
|
|
472
|
+
: Object.fromEntries(tableCommands.map((name) => [name, false]));
|
|
473
|
+
return {
|
|
474
|
+
documentId: this.documentId,
|
|
475
|
+
generation: this.generation,
|
|
476
|
+
revision: this.revision,
|
|
477
|
+
format: this.formatType,
|
|
478
|
+
mode: this.editingMode,
|
|
479
|
+
editable: this.writable,
|
|
480
|
+
composing: this.composing,
|
|
481
|
+
pending: this.pasteController.pending,
|
|
482
|
+
caret: this.literalSurface
|
|
483
|
+
? { marks: [], block: { type: 'paragraph' }, quoted: false }
|
|
484
|
+
: caretState(state),
|
|
485
|
+
table: this.literalSurface ? undefined : tableContext(state),
|
|
486
|
+
commands: {
|
|
487
|
+
undo: mutable && undoHistory(state),
|
|
488
|
+
redo: mutable && redoHistory(state),
|
|
489
|
+
insertText: mutable,
|
|
490
|
+
paste: mutable,
|
|
491
|
+
replace: mutable,
|
|
492
|
+
replaceSource: mutable,
|
|
493
|
+
insertImages: formatted && !!this.options.images,
|
|
494
|
+
insertPaths: mutable,
|
|
495
|
+
insertFootnote: formatted,
|
|
496
|
+
editReferenceDefinition: formatted && referenceDefinitions(state.doc).size > 0,
|
|
497
|
+
format,
|
|
498
|
+
table: tables,
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
canQuote(state) {
|
|
503
|
+
const { $from, $to } = textBounds(state);
|
|
504
|
+
if (quoted($from, $to)) {
|
|
505
|
+
const range = $from.blockRange($to, (node) => node.type.name === 'blockquote');
|
|
506
|
+
return !!range && liftTarget(range) != null;
|
|
507
|
+
}
|
|
508
|
+
const type = blockquoteSchema.type(this.editor.ctx);
|
|
509
|
+
let range = $from.blockRange($to);
|
|
510
|
+
if (range && findWrapping(range, type))
|
|
511
|
+
return true;
|
|
512
|
+
const depth = outermostListDepth($from);
|
|
513
|
+
if (depth == null)
|
|
514
|
+
return false;
|
|
515
|
+
range = new NodeRange(state.doc.resolve($from.before(depth)), state.doc.resolve($from.after(depth)), depth - 1);
|
|
516
|
+
return !!findWrapping(range, type);
|
|
517
|
+
}
|
|
518
|
+
policyPlugin = $prose(() => new Plugin({
|
|
519
|
+
key: new PluginKey('inkkitInputPolicy'),
|
|
520
|
+
filterTransaction: (tr) => this.loading ||
|
|
521
|
+
this.writable ||
|
|
522
|
+
(!tr.docChanged && !tr.storedMarksSet),
|
|
523
|
+
props: {
|
|
524
|
+
editable: () => this.writable && !this.literalSurface,
|
|
525
|
+
handleDOMEvents: {
|
|
526
|
+
compositionstart: () => {
|
|
527
|
+
queueMicrotask(() => this.publishCommandState());
|
|
528
|
+
return false;
|
|
529
|
+
},
|
|
530
|
+
compositionend: () => {
|
|
531
|
+
queueMicrotask(() => this.publishCommandState());
|
|
532
|
+
return false;
|
|
533
|
+
},
|
|
534
|
+
beforeinput: (_view, event) => {
|
|
535
|
+
if (this.writable)
|
|
536
|
+
return false;
|
|
537
|
+
event.preventDefault();
|
|
538
|
+
return true;
|
|
539
|
+
},
|
|
540
|
+
drop: (_view, event) => {
|
|
541
|
+
if (this.writable)
|
|
542
|
+
return false;
|
|
543
|
+
event.preventDefault();
|
|
544
|
+
return true;
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
view: () => ({ update: () => this.publishCommandState() }),
|
|
549
|
+
}));
|
|
550
|
+
get literalSurface() {
|
|
551
|
+
return this.formatType === 'txt' || this.mode === 'source';
|
|
552
|
+
}
|
|
553
|
+
get editingMode() {
|
|
554
|
+
this.assertAlive();
|
|
555
|
+
return this.literalSurface ? 'source' : 'formatted';
|
|
556
|
+
}
|
|
557
|
+
currentText(doc = this.editor.ctx.get(editorViewCtx).state.doc) {
|
|
558
|
+
const provenance = doc.attrs[sourceAttribute];
|
|
559
|
+
if (this.formatType === 'txt')
|
|
560
|
+
return provenance?.text ?? this.originalSource;
|
|
561
|
+
let preservation = this.preservation;
|
|
562
|
+
if (provenance) {
|
|
563
|
+
preservation = this.sourcePreservations.get(provenance);
|
|
564
|
+
if (!preservation) {
|
|
565
|
+
preservation = new Preservation(this.editor.ctx, provenance.text);
|
|
566
|
+
this.sourcePreservations.set(provenance, preservation);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return preservation.serialize(cleanSourceDoc(doc));
|
|
570
|
+
}
|
|
571
|
+
setEditingMode(mode, expectedGeneration) {
|
|
572
|
+
this.assertCurrent(expectedGeneration);
|
|
573
|
+
if (mode !== 'source' && mode !== 'formatted')
|
|
574
|
+
throw new RangeError('Editing mode must be source or formatted');
|
|
575
|
+
if (this.formatType === 'txt' || this.mode === mode)
|
|
576
|
+
return false;
|
|
577
|
+
const text = this.snapshot().text;
|
|
578
|
+
this.mode = mode;
|
|
579
|
+
this.outlineEpoch += 1;
|
|
580
|
+
this.plainSearch = '';
|
|
581
|
+
this.literalMatch = undefined;
|
|
582
|
+
if (mode === 'source') {
|
|
583
|
+
this.plain.value = text;
|
|
584
|
+
this.plain.setSelectionRange(this.plain.value.length, this.plain.value.length);
|
|
585
|
+
}
|
|
586
|
+
const suppressed = this.reportingSuppressed;
|
|
587
|
+
this.reportingSuppressed = true;
|
|
588
|
+
try {
|
|
589
|
+
this.updateSurface();
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
this.reportingSuppressed = suppressed;
|
|
593
|
+
}
|
|
594
|
+
this.events.stateChanged(this.literalSurface
|
|
595
|
+
? { marks: [], block: { type: 'paragraph' }, quoted: false }
|
|
596
|
+
: caretState(this.editor.ctx.get(editorViewCtx).state));
|
|
597
|
+
this.publishCommandState();
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
updateSurface() {
|
|
601
|
+
this.plain.hidden = !this.literalSurface;
|
|
602
|
+
this.applyInputPolicy();
|
|
603
|
+
this.plain.setAttribute('aria-label', this.formatType === 'txt'
|
|
604
|
+
? (this.options.labels?.plainTextEditor ??
|
|
605
|
+
defaultLabels.plainTextEditor)
|
|
606
|
+
: (this.options.labels?.sourceEditor ?? defaultLabels.sourceEditor));
|
|
607
|
+
this.editor.ctx.get(editorViewCtx).dom.parentElement.hidden =
|
|
608
|
+
this.literalSurface;
|
|
609
|
+
}
|
|
610
|
+
replaceSource(text, expectedGeneration) {
|
|
611
|
+
this.assertMutation(expectedGeneration);
|
|
612
|
+
return this.applySource(text, true);
|
|
613
|
+
}
|
|
614
|
+
applySource(text, isolated, from, to) {
|
|
615
|
+
if (!this.writable)
|
|
616
|
+
throw new InkKitError('read-only', 'The editor is read-only');
|
|
617
|
+
if (typeof text !== 'string')
|
|
618
|
+
throw new TypeError('Source must be a string');
|
|
619
|
+
if (text === this.currentText())
|
|
620
|
+
return false;
|
|
621
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
622
|
+
const normalizedLength = text.replace(/\r\n?/g, '\n').length;
|
|
623
|
+
const provenance = Object.freeze({
|
|
624
|
+
text,
|
|
625
|
+
format: this.formatType,
|
|
626
|
+
from: from ?? normalizedLength,
|
|
627
|
+
to: to ?? from ?? normalizedLength,
|
|
628
|
+
});
|
|
629
|
+
let parsed;
|
|
630
|
+
try {
|
|
631
|
+
parsed =
|
|
632
|
+
this.formatType === 'txt'
|
|
633
|
+
? view.state.schema.topNodeType.create(null, view.state.schema.nodes.paragraph.create(null, text ? view.state.schema.text(text) : undefined))
|
|
634
|
+
: this.editor.ctx.get(parserCtx)(text);
|
|
635
|
+
if (this.formatType === 'md')
|
|
636
|
+
this.sourcePreservations.set(provenance, new Preservation(this.editor.ctx, text));
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
throw new InkKitError('preservation', error instanceof Error ? error.message : 'Cannot read Markdown source');
|
|
640
|
+
}
|
|
641
|
+
if (this.literalSurface) {
|
|
642
|
+
const previous = Object.freeze({
|
|
643
|
+
text: this.currentText(),
|
|
644
|
+
format: this.formatType,
|
|
645
|
+
from: this.beforePlainInput?.from ?? this.plain.selectionStart,
|
|
646
|
+
to: this.beforePlainInput?.to ?? this.plain.selectionEnd,
|
|
647
|
+
});
|
|
648
|
+
// History must retain the source caret even when the formatted document has no corresponding position.
|
|
649
|
+
this.loading = true;
|
|
650
|
+
try {
|
|
651
|
+
view.dispatch(view.state.tr
|
|
652
|
+
.setDocAttribute(sourceAttribute, previous)
|
|
653
|
+
.setMeta('addToHistory', false));
|
|
654
|
+
}
|
|
655
|
+
finally {
|
|
656
|
+
this.loading = false;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
let tr = view.state.tr
|
|
660
|
+
.replaceWith(0, view.state.doc.content.size, parsed.content)
|
|
661
|
+
.setDocAttribute(sourceAttribute, provenance);
|
|
662
|
+
tr.setSelection(Selection.atEnd(tr.doc));
|
|
663
|
+
if (isolated)
|
|
664
|
+
tr = closeHistory(tr);
|
|
665
|
+
view.dispatch(tr);
|
|
666
|
+
if (isolated)
|
|
667
|
+
view.dispatch(closeHistory(view.state.tr).setMeta('addToHistory', false));
|
|
668
|
+
return true;
|
|
669
|
+
}
|
|
670
|
+
undo(expectedGeneration) {
|
|
671
|
+
this.assertMutation(expectedGeneration);
|
|
672
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
673
|
+
return undoHistory(view.state, view.dispatch);
|
|
674
|
+
}
|
|
675
|
+
redo(expectedGeneration) {
|
|
676
|
+
this.assertMutation(expectedGeneration);
|
|
677
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
678
|
+
return redoHistory(view.state, view.dispatch);
|
|
679
|
+
}
|
|
680
|
+
replaceLiteralSelection(text, isolated = true, retainEndings = false) {
|
|
681
|
+
const value = this.plain.value;
|
|
682
|
+
const start = this.plain.selectionStart, end = this.plain.selectionEnd;
|
|
683
|
+
const next = value.slice(0, start) + text.replace(/\r\n?/g, '\n') + value.slice(end);
|
|
684
|
+
const caret = start + text.replace(/\r\n?/g, '\n').length;
|
|
685
|
+
const previous = this.currentText();
|
|
686
|
+
const source = retainEndings
|
|
687
|
+
? previous.slice(0, rawOffset(previous, start)) +
|
|
688
|
+
text +
|
|
689
|
+
previous.slice(rawOffset(previous, end))
|
|
690
|
+
: editedPlainSource(previous, next);
|
|
691
|
+
this.applySource(source, isolated, caret, caret);
|
|
316
692
|
}
|
|
317
693
|
snapshot(expectedGeneration) {
|
|
318
694
|
this.assertCurrent(expectedGeneration);
|
|
319
695
|
let text;
|
|
320
696
|
try {
|
|
321
|
-
text =
|
|
322
|
-
this.formatType === 'txt'
|
|
323
|
-
? this.plainSource()
|
|
324
|
-
: this.preservation.serialize(this.editor.ctx.get(editorViewCtx).state.doc);
|
|
697
|
+
text = this.currentText();
|
|
325
698
|
}
|
|
326
699
|
catch (error) {
|
|
327
700
|
throw new InkKitError('preservation', error instanceof Error ? error.message : 'Cannot preserve Markdown');
|
|
@@ -377,7 +750,7 @@ export class InkKitEditor {
|
|
|
377
750
|
if (this.formatType === 'txt') {
|
|
378
751
|
const text = all
|
|
379
752
|
? snapshot.text
|
|
380
|
-
:
|
|
753
|
+
: snapshot.text.slice(rawOffset(snapshot.text, this.plain.selectionStart), rawOffset(snapshot.text, this.plain.selectionEnd));
|
|
381
754
|
const pre = document.createElement('pre');
|
|
382
755
|
pre.textContent = text;
|
|
383
756
|
return {
|
|
@@ -389,40 +762,54 @@ export class InkKitEditor {
|
|
|
389
762
|
};
|
|
390
763
|
}
|
|
391
764
|
const { doc, schema, selection } = this.editor.ctx.get(editorViewCtx).state;
|
|
765
|
+
const selectedSource = !all && this.mode === 'source'
|
|
766
|
+
? sourceSelection(snapshot.text, this.plain.selectionStart, this.plain.selectionEnd)
|
|
767
|
+
: undefined;
|
|
768
|
+
const selectedDoc = selectedSource
|
|
769
|
+
? this.editor.ctx.get(parserCtx)(selectedSource.shareable)
|
|
770
|
+
: undefined;
|
|
392
771
|
const completeDiagrams = new Set();
|
|
393
|
-
doc.descendants((node, pos) => {
|
|
772
|
+
(selectedDoc ?? doc).descendants((node, pos) => {
|
|
394
773
|
if (isMermaid(node) &&
|
|
395
|
-
(
|
|
396
|
-
(
|
|
397
|
-
|
|
774
|
+
(selectedDoc
|
|
775
|
+
? Boolean(node.attrs.authoredFence?.close)
|
|
776
|
+
: all ||
|
|
777
|
+
(selection.from <= pos + 1 &&
|
|
778
|
+
selection.to >= pos + node.nodeSize - 1))) {
|
|
398
779
|
completeDiagrams.add(node.textContent.replace(/\r\n?/g, '\n'));
|
|
399
780
|
}
|
|
400
781
|
});
|
|
401
|
-
const content =
|
|
402
|
-
?
|
|
403
|
-
:
|
|
782
|
+
const content = selectedDoc
|
|
783
|
+
? selectedDoc.content
|
|
784
|
+
: all
|
|
785
|
+
? doc.content
|
|
786
|
+
: commentSelectionContent(doc, selection.from, selection.to, selection.content().content);
|
|
404
787
|
const valid = content.firstChild?.isInline
|
|
405
788
|
? Fragment.from(schema.nodes.paragraph.create(null, content))
|
|
406
789
|
: content;
|
|
407
790
|
let markdown;
|
|
408
791
|
try {
|
|
409
|
-
markdown =
|
|
410
|
-
?
|
|
411
|
-
:
|
|
412
|
-
?
|
|
413
|
-
:
|
|
792
|
+
markdown = selectedSource
|
|
793
|
+
? selectedSource.markdown
|
|
794
|
+
: all
|
|
795
|
+
? snapshot.text
|
|
796
|
+
: completeDiagrams.size
|
|
797
|
+
? new Preservation(this.editor.ctx, snapshot.text).serialize(doc.type.create(null, selectionContent(doc, valid)))
|
|
798
|
+
: selectionMarkdown(this.editor.ctx, doc, valid);
|
|
414
799
|
}
|
|
415
800
|
catch (error) {
|
|
416
801
|
throw new InkKitError('preservation', error instanceof Error
|
|
417
802
|
? error.message
|
|
418
803
|
: 'Cannot copy the selected Markdown');
|
|
419
804
|
}
|
|
420
|
-
const result = await portableClipboard(all ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams);
|
|
421
|
-
const metadata = referenceMetadata(this.editor.ctx, doc, content);
|
|
805
|
+
const result = await portableClipboard(all || selectedDoc ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams);
|
|
806
|
+
const metadata = referenceMetadata(this.editor.ctx, selectedDoc ?? doc, content);
|
|
422
807
|
if (metadata != null)
|
|
423
808
|
result.html = withReferenceMetadata(result.html, metadata);
|
|
424
809
|
this.assertCurrent(snapshot.generation);
|
|
425
|
-
if (snapshot.documentId !== this.documentId ||
|
|
810
|
+
if (snapshot.documentId !== this.documentId ||
|
|
811
|
+
epoch !== this.documentEpoch ||
|
|
812
|
+
snapshot.revision !== this.revision)
|
|
426
813
|
throw new InkKitError('stale-document', 'Document changed');
|
|
427
814
|
if (result.images.some((image) => image.error))
|
|
428
815
|
this.events.error?.(new InkKitError('image-unavailable', 'Some copied images were unavailable; their descriptions were retained'));
|
|
@@ -431,9 +818,11 @@ export class InkKitEditor {
|
|
|
431
818
|
return result;
|
|
432
819
|
}
|
|
433
820
|
async paste(input) {
|
|
434
|
-
this.
|
|
435
|
-
if (this.
|
|
436
|
-
this.pasteAsPlainText(
|
|
821
|
+
this.assertMutation();
|
|
822
|
+
if (this.literalSurface || input.plainText) {
|
|
823
|
+
this.pasteAsPlainText(this.mode === 'source' && this.formatType === 'md'
|
|
824
|
+
? (input.markdown ?? input.text)
|
|
825
|
+
: input.text);
|
|
437
826
|
return;
|
|
438
827
|
}
|
|
439
828
|
await this.pasteController.paste(input);
|
|
@@ -443,10 +832,9 @@ export class InkKitEditor {
|
|
|
443
832
|
setCommentVisibility(this.editor.ctx.get(editorViewCtx), visible);
|
|
444
833
|
}
|
|
445
834
|
pasteAsPlainText(text) {
|
|
446
|
-
this.
|
|
447
|
-
if (this.
|
|
448
|
-
this.
|
|
449
|
-
this.plain.dispatchEvent(new Event('input'));
|
|
835
|
+
this.assertMutation();
|
|
836
|
+
if (this.literalSurface) {
|
|
837
|
+
this.replaceLiteralSelection(text, true, this.formatType === 'md');
|
|
450
838
|
}
|
|
451
839
|
else {
|
|
452
840
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
@@ -483,8 +871,8 @@ export class InkKitEditor {
|
|
|
483
871
|
}
|
|
484
872
|
}
|
|
485
873
|
table(command, options) {
|
|
486
|
-
this.
|
|
487
|
-
if (this.
|
|
874
|
+
this.assertMutation();
|
|
875
|
+
if (this.literalSurface)
|
|
488
876
|
return false;
|
|
489
877
|
return tableCommand(this.editor.ctx, command, options);
|
|
490
878
|
}
|
|
@@ -504,6 +892,8 @@ export class InkKitEditor {
|
|
|
504
892
|
}
|
|
505
893
|
this.plain.remove();
|
|
506
894
|
await this.editor.destroy();
|
|
895
|
+
if (this.ownsRootClass)
|
|
896
|
+
this.root.classList.remove('inkkit-root');
|
|
507
897
|
}
|
|
508
898
|
// The app's bindings, replaced whole whenever they change; the plugin stays.
|
|
509
899
|
keymapPlugin = $prose(() => new Plugin({
|
|
@@ -517,19 +907,81 @@ export class InkKitEditor {
|
|
|
517
907
|
},
|
|
518
908
|
},
|
|
519
909
|
}));
|
|
910
|
+
copySourceSelection(event) {
|
|
911
|
+
event.preventDefault();
|
|
912
|
+
event.stopPropagation();
|
|
913
|
+
try {
|
|
914
|
+
const snapshot = this.snapshot();
|
|
915
|
+
const from = this.plain.selectionStart, to = this.plain.selectionEnd;
|
|
916
|
+
const selected = sourceSelection(snapshot.text, from, to);
|
|
917
|
+
const parsed = this.editor.ctx.get(parserCtx)(selected.shareable);
|
|
918
|
+
let portable = false;
|
|
919
|
+
parsed.descendants((node) => {
|
|
920
|
+
if (node.type.name === 'image' || isMermaid(node))
|
|
921
|
+
portable = true;
|
|
922
|
+
});
|
|
923
|
+
const cut = event.type === 'cut' && this.writable;
|
|
924
|
+
const policyEpoch = this.policyEpoch;
|
|
925
|
+
const epoch = this.documentEpoch;
|
|
926
|
+
if (portable && this.events.clipboard) {
|
|
927
|
+
void this.clipboardSnapshot(false)
|
|
928
|
+
.then(async (content) => {
|
|
929
|
+
if (cut &&
|
|
930
|
+
(content.images.some((image) => image.error) ||
|
|
931
|
+
content.diagrams?.some((diagram) => diagram.error)))
|
|
932
|
+
throw new InkKitError('image-unavailable', 'The selected content could not be cut safely');
|
|
933
|
+
await this.events.clipboard(content);
|
|
934
|
+
if (cut) {
|
|
935
|
+
this.assertMutation(snapshot.generation);
|
|
936
|
+
if (epoch !== this.documentEpoch ||
|
|
937
|
+
policyEpoch !== this.policyEpoch ||
|
|
938
|
+
snapshot.revision !== this.revision)
|
|
939
|
+
throw new InkKitError('stale-document', 'Document changed before cutting');
|
|
940
|
+
this.plain.setSelectionRange(from, to);
|
|
941
|
+
this.replaceLiteralSelection('');
|
|
942
|
+
}
|
|
943
|
+
})
|
|
944
|
+
.catch((error) => this.events.error?.(error));
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
const output = clipboardContent(parsed.content, parsed.type.schema);
|
|
948
|
+
if (!event.clipboardData)
|
|
949
|
+
return;
|
|
950
|
+
event.clipboardData.setData('text/plain', output.text);
|
|
951
|
+
event.clipboardData.setData('text/html', output.html);
|
|
952
|
+
if (portable) {
|
|
953
|
+
this.events.error?.(new InkKitError('image-unavailable', 'The host must provide a clipboard handler for portable images and diagrams'));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (cut)
|
|
957
|
+
this.replaceLiteralSelection('');
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
catch (error) {
|
|
961
|
+
this.events.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
962
|
+
}
|
|
963
|
+
}
|
|
520
964
|
static async mount(root, events, options = {}) {
|
|
521
965
|
const instance = new InkKitEditor(events, options);
|
|
522
966
|
instance.root = root;
|
|
967
|
+
instance.ownsRootClass = !root.classList.contains('inkkit-root');
|
|
968
|
+
root.classList.add('inkkit-root');
|
|
523
969
|
instance.pasteController = new PasteController({
|
|
524
970
|
ctx: () => instance.editor.ctx,
|
|
525
971
|
context: () => instance.context(),
|
|
526
972
|
adapter: options.images,
|
|
527
973
|
onError: (error) => events.error?.(error instanceof Error ? error : new Error(String(error))),
|
|
528
|
-
literalText: () => instance.
|
|
974
|
+
literalText: () => instance.literalSurface,
|
|
975
|
+
editable: () => instance.writable && !instance.literalSurface,
|
|
976
|
+
onPendingChanged: () => instance.publishCommandState(),
|
|
529
977
|
});
|
|
530
978
|
instance.editor = await Editor.make()
|
|
531
979
|
.config((ctx) => {
|
|
532
980
|
ctx.set(rootCtx, root);
|
|
981
|
+
ctx.update(labelsCtx.key, () => ({
|
|
982
|
+
...defaultLabels,
|
|
983
|
+
...options.labels,
|
|
984
|
+
}));
|
|
533
985
|
ctx.set(defaultValueCtx, '');
|
|
534
986
|
ctx.set(remarkStringifyOptionsCtx, stringifyOptions);
|
|
535
987
|
// The preset also binds Mod-[ and Mod-] here; the app uses those for back and forward.
|
|
@@ -548,6 +1000,8 @@ export class InkKitEditor {
|
|
|
548
1000
|
}
|
|
549
1001
|
return cleared;
|
|
550
1002
|
});
|
|
1003
|
+
unbind(tableKeymap, ['NextCell', 'PrevCell']);
|
|
1004
|
+
unbind(historyKeymap);
|
|
551
1005
|
unbind(strongKeymap);
|
|
552
1006
|
unbind(emphasisKeymap);
|
|
553
1007
|
unbind(inlineCodeKeymap);
|
|
@@ -566,7 +1020,8 @@ export class InkKitEditor {
|
|
|
566
1020
|
scrollMargin: { top: 8, right: 0, bottom: 24, left: 0 },
|
|
567
1021
|
}));
|
|
568
1022
|
})
|
|
569
|
-
.use(
|
|
1023
|
+
.use(labelsCtx)
|
|
1024
|
+
.use(caretStatePlugin(events, () => !instance.loading && !instance.literalSurface))
|
|
570
1025
|
.use(instance.pasteController.plugin)
|
|
571
1026
|
.use(visibleClipboard)
|
|
572
1027
|
.use(instance.keymapPlugin)
|
|
@@ -587,12 +1042,13 @@ export class InkKitEditor {
|
|
|
587
1042
|
.use(mermaidPreview((error) => events.error?.(error)))
|
|
588
1043
|
.use(selectionPlugin)
|
|
589
1044
|
.use($prose(() => search()))
|
|
1045
|
+
.use(instance.policyPlugin)
|
|
590
1046
|
.use(options.images ? imageView(options.images) : [])
|
|
591
1047
|
.create();
|
|
592
1048
|
configureTableMovement(instance.editor.ctx, () => {
|
|
593
1049
|
try {
|
|
594
1050
|
instance.assertCurrent();
|
|
595
|
-
return instance.
|
|
1051
|
+
return instance.writable && !instance.literalSurface;
|
|
596
1052
|
}
|
|
597
1053
|
catch {
|
|
598
1054
|
return false;
|
|
@@ -603,15 +1059,64 @@ export class InkKitEditor {
|
|
|
603
1059
|
instance.plain.setAttribute('aria-label', 'Plain text editor');
|
|
604
1060
|
instance.plain.hidden = true;
|
|
605
1061
|
root.append(instance.plain);
|
|
1062
|
+
instance.plain.addEventListener('beforeinput', (event) => {
|
|
1063
|
+
if (!instance.writable) {
|
|
1064
|
+
event.preventDefault();
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (instance.ready && instance.literalSurface)
|
|
1068
|
+
instance.beforePlainInput = {
|
|
1069
|
+
from: instance.plain.selectionStart,
|
|
1070
|
+
to: instance.plain.selectionEnd,
|
|
1071
|
+
};
|
|
1072
|
+
});
|
|
606
1073
|
instance.plain.addEventListener('input', () => {
|
|
607
|
-
instance.
|
|
608
|
-
|
|
1074
|
+
if (!instance.ready || !instance.literalSurface)
|
|
1075
|
+
return;
|
|
1076
|
+
if (!instance.writable) {
|
|
1077
|
+
instance.plain.value = instance.currentText();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
instance.applySource(editedPlainSource(instance.currentText(), instance.plain.value), false, instance.plain.selectionStart, instance.plain.selectionEnd);
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
events.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
1085
|
+
}
|
|
1086
|
+
finally {
|
|
1087
|
+
instance.beforePlainInput = undefined;
|
|
1088
|
+
}
|
|
1089
|
+
});
|
|
1090
|
+
instance.plain.addEventListener('keydown', (event) => {
|
|
1091
|
+
if (instance.plainComposing)
|
|
1092
|
+
return;
|
|
1093
|
+
if (instance.literalKeys(instance.editor.ctx.get(editorViewCtx), event))
|
|
1094
|
+
event.preventDefault();
|
|
1095
|
+
});
|
|
1096
|
+
for (const event of ['select', 'keyup', 'click'])
|
|
1097
|
+
instance.plain.addEventListener(event, () => instance.publishCommandState());
|
|
1098
|
+
instance.plain.addEventListener('paste', (event) => {
|
|
1099
|
+
if (!instance.writable) {
|
|
1100
|
+
event.preventDefault();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (!event.clipboardData)
|
|
1104
|
+
return;
|
|
1105
|
+
event.preventDefault();
|
|
1106
|
+
void instance
|
|
1107
|
+
.paste({
|
|
1108
|
+
text: event.clipboardData.getData('text/plain'),
|
|
1109
|
+
markdown: event.clipboardData.getData('text/markdown') || undefined,
|
|
1110
|
+
})
|
|
1111
|
+
.catch((error) => events.error?.(error));
|
|
609
1112
|
});
|
|
610
1113
|
instance.plain.addEventListener('compositionstart', () => {
|
|
611
1114
|
instance.plainComposing = true;
|
|
1115
|
+
instance.publishCommandState();
|
|
612
1116
|
});
|
|
613
1117
|
instance.plain.addEventListener('compositionend', () => {
|
|
614
1118
|
instance.plainComposing = false;
|
|
1119
|
+
instance.publishCommandState();
|
|
615
1120
|
});
|
|
616
1121
|
const activateFootnote = (event) => {
|
|
617
1122
|
const element = event.target.closest('[data-inkkit-footnote-reference]');
|
|
@@ -657,6 +1162,10 @@ export class InkKitEditor {
|
|
|
657
1162
|
instance.copyHandler = (event) => {
|
|
658
1163
|
if (instance.formatType === 'txt')
|
|
659
1164
|
return;
|
|
1165
|
+
if (instance.mode === 'source') {
|
|
1166
|
+
instance.copySourceSelection(event);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
660
1169
|
const view = instance.editor.ctx.get(editorViewCtx);
|
|
661
1170
|
const fragment = commentSelectionContent(view.state.doc, view.state.selection.from, view.state.selection.to, view.state.selection.content().content);
|
|
662
1171
|
const expanded = selectionContent(view.state.doc, fragment);
|
|
@@ -683,7 +1192,7 @@ export class InkKitEditor {
|
|
|
683
1192
|
event.clipboardData.setData('text/html', metadata != null
|
|
684
1193
|
? withReferenceMetadata(output.html, metadata)
|
|
685
1194
|
: output.html);
|
|
686
|
-
if (event.type === 'cut')
|
|
1195
|
+
if (event.type === 'cut' && instance.writable)
|
|
687
1196
|
view.dispatch(view.state.tr.deleteSelection().scrollIntoView());
|
|
688
1197
|
}
|
|
689
1198
|
catch (error) {
|
|
@@ -696,7 +1205,8 @@ export class InkKitEditor {
|
|
|
696
1205
|
if (events.clipboard) {
|
|
697
1206
|
const epoch = instance.documentEpoch, revision = instance.revision;
|
|
698
1207
|
const { from, to } = view.state.selection;
|
|
699
|
-
const cut = event.type === 'cut';
|
|
1208
|
+
const cut = event.type === 'cut' && instance.writable;
|
|
1209
|
+
const policyEpoch = instance.policyEpoch;
|
|
700
1210
|
void instance
|
|
701
1211
|
.clipboardSnapshot(false)
|
|
702
1212
|
.then(async (content) => {
|
|
@@ -706,8 +1216,9 @@ export class InkKitEditor {
|
|
|
706
1216
|
throw new InkKitError('image-unavailable', 'The selected content could not be cut safely');
|
|
707
1217
|
await events.clipboard(content);
|
|
708
1218
|
if (cut) {
|
|
709
|
-
instance.
|
|
1219
|
+
instance.assertMutation();
|
|
710
1220
|
if (epoch !== instance.documentEpoch ||
|
|
1221
|
+
policyEpoch !== instance.policyEpoch ||
|
|
711
1222
|
revision !== instance.revision)
|
|
712
1223
|
throw new InkKitError('stale-document', 'Document changed before cutting');
|
|
713
1224
|
view.dispatch(view.state.tr.deleteRange(from, to).scrollIntoView());
|
|
@@ -725,42 +1236,71 @@ export class InkKitEditor {
|
|
|
725
1236
|
};
|
|
726
1237
|
root.addEventListener('copy', instance.copyHandler, true);
|
|
727
1238
|
root.addEventListener('cut', instance.copyHandler, true);
|
|
1239
|
+
instance.applyInputPolicy();
|
|
1240
|
+
instance.setKeymap(options.keymap ?? {});
|
|
728
1241
|
return instance;
|
|
729
1242
|
}
|
|
730
1243
|
loadDocument(input) {
|
|
731
1244
|
this.assertAlive();
|
|
1245
|
+
this.ready = false;
|
|
732
1246
|
this.pasteController.cancelPending();
|
|
733
1247
|
this.documentEpoch += 1;
|
|
1248
|
+
this.outlineEpoch += 1;
|
|
1249
|
+
this.sourcePreservations = new WeakMap();
|
|
1250
|
+
this.mode = 'formatted';
|
|
734
1251
|
this.plainComposing = false;
|
|
1252
|
+
this.beforePlainInput = undefined;
|
|
735
1253
|
this.plainSearch = '';
|
|
1254
|
+
this.literalMatch = undefined;
|
|
736
1255
|
this.documentId = input.documentId;
|
|
737
1256
|
this.formatType = input.format;
|
|
738
1257
|
this.revision = 0;
|
|
739
|
-
this.plain.hidden = input.format !== 'txt';
|
|
740
|
-
this.editor.ctx.get(editorViewCtx).dom.parentElement.hidden =
|
|
741
|
-
input.format === 'txt';
|
|
742
1258
|
this.plain.value = input.text;
|
|
743
1259
|
this.load(input.format === 'txt' ? '' : input.text, input.generation);
|
|
744
1260
|
this.originalSource = input.text;
|
|
1261
|
+
this.lastMarkdown = input.text;
|
|
1262
|
+
this.updateSurface();
|
|
745
1263
|
this.ready = true;
|
|
1264
|
+
this.events.stateChanged(this.literalSurface
|
|
1265
|
+
? { marks: [], block: { type: 'paragraph' }, quoted: false }
|
|
1266
|
+
: caretState(this.editor.ctx.get(editorViewCtx).state));
|
|
1267
|
+
this.publishCommandState();
|
|
746
1268
|
}
|
|
747
1269
|
reloadDocument(input) {
|
|
748
1270
|
this.assertAlive();
|
|
749
1271
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
750
|
-
const
|
|
1272
|
+
const wasLiteral = this.literalSurface;
|
|
1273
|
+
const previousMode = this.editingMode;
|
|
1274
|
+
const previousFormat = this.formatType;
|
|
1275
|
+
const at = wasLiteral
|
|
751
1276
|
? this.plain.selectionStart
|
|
752
1277
|
: view.state.selection.from;
|
|
1278
|
+
const end = wasLiteral ? this.plain.selectionEnd : at;
|
|
753
1279
|
const top = this.root.scrollTop;
|
|
754
|
-
this.
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1280
|
+
const plainTop = this.plain.scrollTop, plainLeft = this.plain.scrollLeft;
|
|
1281
|
+
this.reportingSuppressed = true;
|
|
1282
|
+
try {
|
|
1283
|
+
this.loadDocument(input);
|
|
1284
|
+
if (previousFormat === 'md' &&
|
|
1285
|
+
input.format === 'md' &&
|
|
1286
|
+
previousMode === 'source')
|
|
1287
|
+
this.setEditingMode('source', input.generation);
|
|
1288
|
+
if (this.literalSurface)
|
|
1289
|
+
this.plain.setSelectionRange(Math.min(at, this.plain.value.length), Math.min(end, this.plain.value.length));
|
|
1290
|
+
else
|
|
1291
|
+
view.dispatch(view.state.tr.setSelection(Selection.near(view.state.doc.resolve(Math.min(at, view.state.doc.content.size)))));
|
|
1292
|
+
this.root.scrollTop = top;
|
|
1293
|
+
if (wasLiteral && this.literalSurface) {
|
|
1294
|
+
this.plain.scrollTop = plainTop;
|
|
1295
|
+
this.plain.scrollLeft = plainLeft;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
finally {
|
|
1299
|
+
this.reportingSuppressed = false;
|
|
1300
|
+
this.publishCommandState();
|
|
1301
|
+
}
|
|
760
1302
|
}
|
|
761
1303
|
load(markdown, generation) {
|
|
762
|
-
// A loaded document only counts as changed once it is edited, so its
|
|
763
|
-
// canonical form is the baseline, not the text as stored.
|
|
764
1304
|
this.generation = generation;
|
|
765
1305
|
this.originalSource = markdown;
|
|
766
1306
|
this.loading = true;
|
|
@@ -771,8 +1311,7 @@ export class InkKitEditor {
|
|
|
771
1311
|
this.loading = false;
|
|
772
1312
|
}
|
|
773
1313
|
this.preservation = new Preservation(this.editor.ctx, markdown);
|
|
774
|
-
this.
|
|
775
|
-
this.lastMarkdown = this.baseline;
|
|
1314
|
+
this.lastMarkdown = markdown;
|
|
776
1315
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
777
1316
|
// A caret at the end, where writing carries on; at the start it would sit in a first-line heading and
|
|
778
1317
|
// show its marks. The last place text can go, so a document ending in a rule takes a caret above it
|
|
@@ -781,56 +1320,119 @@ export class InkKitEditor {
|
|
|
781
1320
|
const end = Selection.findFrom(doc.resolve(doc.content.size), -1, true) ??
|
|
782
1321
|
Selection.atEnd(doc);
|
|
783
1322
|
view.dispatch(setSearchState(view.state.tr.setSelection(end).scrollIntoView(), new SearchQuery({ search: '' })));
|
|
784
|
-
this.events.stateChanged(caretState(view.state));
|
|
785
1323
|
}
|
|
786
|
-
find(text) {
|
|
787
|
-
this.assertCurrent();
|
|
788
|
-
if (this.
|
|
789
|
-
const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
790
|
-
const matches = text
|
|
791
|
-
? [...this.plain.value.matchAll(new RegExp(escaped, 'giu'))]
|
|
792
|
-
: [];
|
|
1324
|
+
find(text, expectedGeneration) {
|
|
1325
|
+
this.assertCurrent(expectedGeneration);
|
|
1326
|
+
if (this.literalSurface) {
|
|
793
1327
|
const from = this.plainSearch === text
|
|
794
1328
|
? this.plain.selectionEnd
|
|
795
1329
|
: this.plain.selectionStart;
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
1330
|
+
const raw = this.currentText();
|
|
1331
|
+
const match = findLiteral(raw, text, rawOffset(raw, from));
|
|
1332
|
+
this.literalMatch = undefined;
|
|
1333
|
+
if (match) {
|
|
1334
|
+
// A textarea displays CRLF as one character; retain a match of either raw half for replacement.
|
|
1335
|
+
const start = raw[match.from] === '\n' && raw[match.from - 1] === '\r'
|
|
1336
|
+
? match.from - 1
|
|
1337
|
+
: match.from;
|
|
1338
|
+
const displayedFrom = normalizedOffset(raw, start), displayedTo = normalizedOffset(raw, match.to);
|
|
1339
|
+
this.plain.setSelectionRange(displayedFrom, displayedTo);
|
|
1340
|
+
this.literalMatch = {
|
|
1341
|
+
...match,
|
|
1342
|
+
query: text,
|
|
1343
|
+
displayedFrom,
|
|
1344
|
+
displayedTo,
|
|
1345
|
+
revision: this.revision,
|
|
1346
|
+
epoch: this.documentEpoch,
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
799
1349
|
else if (this.plainSearch)
|
|
800
1350
|
this.plain.setSelectionRange(this.plain.selectionEnd, this.plain.selectionEnd);
|
|
801
1351
|
this.plainSearch = text;
|
|
1352
|
+
revealSourceCaret(this.plain);
|
|
802
1353
|
return;
|
|
803
1354
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1355
|
+
findFormatted(this.editor.ctx.get(editorViewCtx), text);
|
|
1356
|
+
}
|
|
1357
|
+
replace(search, replacement, expectedGeneration) {
|
|
1358
|
+
this.assertMutation(expectedGeneration);
|
|
1359
|
+
return this.replaceMatches(search, replacement, false) > 0;
|
|
1360
|
+
}
|
|
1361
|
+
replaceAll(search, replacement, expectedGeneration) {
|
|
1362
|
+
this.assertMutation(expectedGeneration);
|
|
1363
|
+
return this.replaceMatches(search, replacement, true);
|
|
1364
|
+
}
|
|
1365
|
+
replaceMatches(search, replacement, all) {
|
|
1366
|
+
if (this.literalSurface) {
|
|
1367
|
+
const raw = this.currentText();
|
|
1368
|
+
const remembered = this.literalMatch;
|
|
1369
|
+
const selection = remembered &&
|
|
1370
|
+
remembered.query === search &&
|
|
1371
|
+
remembered.revision === this.revision &&
|
|
1372
|
+
remembered.epoch === this.documentEpoch &&
|
|
1373
|
+
remembered.displayedFrom === this.plain.selectionStart &&
|
|
1374
|
+
remembered.displayedTo === this.plain.selectionEnd
|
|
1375
|
+
? { from: remembered.from, to: remembered.to }
|
|
1376
|
+
: {
|
|
1377
|
+
from: rawOffset(raw, this.plain.selectionStart),
|
|
1378
|
+
to: rawOffset(raw, this.plain.selectionEnd),
|
|
1379
|
+
};
|
|
1380
|
+
if (remembered?.query !== search)
|
|
1381
|
+
this.literalMatch = undefined;
|
|
1382
|
+
const result = replaceLiteral(raw, search, replacement, selection, all);
|
|
1383
|
+
if (result.count)
|
|
1384
|
+
this.applySource(result.text, true, normalizedOffset(result.text, result.from), normalizedOffset(result.text, result.to));
|
|
1385
|
+
this.plainSearch = search;
|
|
1386
|
+
return result.count;
|
|
817
1387
|
}
|
|
818
|
-
|
|
819
|
-
|
|
1388
|
+
return replaceFormatted(this.editor.ctx.get(editorViewCtx), search, replacement, (doc) => {
|
|
1389
|
+
this.currentText(doc);
|
|
1390
|
+
}, all);
|
|
1391
|
+
}
|
|
1392
|
+
outlineContext() {
|
|
1393
|
+
return {
|
|
1394
|
+
documentId: this.documentId,
|
|
1395
|
+
generation: this.generation,
|
|
1396
|
+
revision: this.revision,
|
|
1397
|
+
epoch: this.outlineEpoch,
|
|
1398
|
+
mode: this.editingMode,
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
headings(expectedGeneration) {
|
|
1402
|
+
this.assertCurrent(expectedGeneration);
|
|
1403
|
+
if (this.formatType === 'txt')
|
|
1404
|
+
return Object.freeze([]);
|
|
1405
|
+
return this.mode === 'source'
|
|
1406
|
+
? collectSourceHeadings(this.editor.ctx, this.plain.value, this.outlineContext())
|
|
1407
|
+
: collectFormattedHeadings(this.editor.ctx.get(editorViewCtx).state.doc, this.outlineContext());
|
|
1408
|
+
}
|
|
1409
|
+
navigateHeading(heading) {
|
|
1410
|
+
this.assertCurrent();
|
|
1411
|
+
if (!heading || typeof heading !== 'object')
|
|
1412
|
+
throw new InkKitError('stale-document', 'The heading is no longer current');
|
|
1413
|
+
this.assertCurrent(heading.generation);
|
|
1414
|
+
if (this.formatType === 'txt')
|
|
1415
|
+
throw new InkKitError('stale-document', 'The heading is no longer current');
|
|
1416
|
+
if (this.mode === 'source') {
|
|
1417
|
+
const position = sourceHeadingPosition(this.editor.ctx, this.plain.value, heading, this.outlineContext());
|
|
1418
|
+
this.plain.setSelectionRange(position, position);
|
|
1419
|
+
this.plain.focus({ preventScroll: true });
|
|
1420
|
+
revealSourceCaret(this.plain);
|
|
1421
|
+
return true;
|
|
820
1422
|
}
|
|
821
|
-
|
|
1423
|
+
return navigateFormattedHeading(this.editor.ctx.get(editorViewCtx), heading, this.outlineContext());
|
|
822
1424
|
}
|
|
823
1425
|
/** Buffered native typing follows the same input rules as direct typing. */
|
|
824
1426
|
insertText(text, generation) {
|
|
825
1427
|
this.assertAlive();
|
|
826
|
-
if ((this.
|
|
1428
|
+
if ((this.literalSurface
|
|
827
1429
|
? this.plainComposing
|
|
828
1430
|
: this.editor.ctx.get(editorViewCtx).composing) &&
|
|
829
1431
|
generation === this.generation)
|
|
830
1432
|
return false;
|
|
831
|
-
this.
|
|
832
|
-
if (this.
|
|
833
|
-
this.
|
|
1433
|
+
this.assertMutation(generation);
|
|
1434
|
+
if (this.literalSurface) {
|
|
1435
|
+
this.replaceLiteralSelection(text, false);
|
|
834
1436
|
return true;
|
|
835
1437
|
}
|
|
836
1438
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
@@ -846,14 +1448,65 @@ export class InkKitEditor {
|
|
|
846
1448
|
/** Let existing keymaps handle buffered editing commands before native fallback. */
|
|
847
1449
|
keyDown(key, code, metaKey, ctrlKey, altKey, shiftKey, generation) {
|
|
848
1450
|
this.assertAlive();
|
|
849
|
-
if ((this.
|
|
1451
|
+
if ((this.literalSurface
|
|
850
1452
|
? this.plainComposing
|
|
851
1453
|
: this.editor.ctx.get(editorViewCtx).composing) &&
|
|
852
1454
|
generation === this.generation)
|
|
853
1455
|
return false;
|
|
854
1456
|
this.assertCurrent(generation);
|
|
855
|
-
|
|
1457
|
+
const configuredEvent = new KeyboardEvent('keydown', {
|
|
1458
|
+
key,
|
|
1459
|
+
code,
|
|
1460
|
+
metaKey,
|
|
1461
|
+
ctrlKey,
|
|
1462
|
+
altKey,
|
|
1463
|
+
shiftKey,
|
|
1464
|
+
bubbles: true,
|
|
1465
|
+
cancelable: true,
|
|
1466
|
+
});
|
|
1467
|
+
if (!this.literalSurface && altKey && key === 'Enter')
|
|
1468
|
+
return this.navigateFootnote(shiftKey ? 'reference' : 'definition');
|
|
1469
|
+
if (!this.writable &&
|
|
1470
|
+
!this.literalSurface &&
|
|
1471
|
+
key === 'Tab' &&
|
|
1472
|
+
!metaKey &&
|
|
1473
|
+
!ctrlKey &&
|
|
1474
|
+
!altKey) {
|
|
1475
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1476
|
+
return goToNextCell(shiftKey ? -1 : 1)(view.state, view.dispatch);
|
|
1477
|
+
}
|
|
1478
|
+
if (!this.writable) {
|
|
1479
|
+
if (['Enter', 'Tab', 'Backspace', 'Delete'].includes(key))
|
|
1480
|
+
this.assertMutation(generation);
|
|
1481
|
+
this.readOnlyKeys(this.editor.ctx.get(editorViewCtx), configuredEvent);
|
|
1482
|
+
}
|
|
1483
|
+
if (this.literalSurface) {
|
|
1484
|
+
if (this.literalKeys(this.editor.ctx.get(editorViewCtx), configuredEvent))
|
|
1485
|
+
return true;
|
|
1486
|
+
if (!this.writable &&
|
|
1487
|
+
['Enter', 'Tab', 'Backspace', 'Delete'].includes(key))
|
|
1488
|
+
throw new InkKitError('read-only', 'The editor is read-only');
|
|
1489
|
+
if (metaKey || ctrlKey || altKey)
|
|
1490
|
+
return false;
|
|
1491
|
+
if (key === 'Enter' || key === 'Tab') {
|
|
1492
|
+
this.replaceLiteralSelection(key === 'Enter' ? '\n' : '\t', false);
|
|
1493
|
+
return true;
|
|
1494
|
+
}
|
|
1495
|
+
if (key === 'Backspace' || key === 'Delete') {
|
|
1496
|
+
const start = this.plain.selectionStart, end = this.plain.selectionEnd;
|
|
1497
|
+
if (start === end) {
|
|
1498
|
+
if (key === 'Backspace' && start > 0)
|
|
1499
|
+
this.plain.setSelectionRange(start - [...this.plain.value.slice(0, start)].at(-1).length, end);
|
|
1500
|
+
else if (key === 'Delete' && end < this.plain.value.length)
|
|
1501
|
+
this.plain.setSelectionRange(start, end + [...this.plain.value.slice(end)][0].length);
|
|
1502
|
+
else
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
this.replaceLiteralSelection('', false);
|
|
1506
|
+
return true;
|
|
1507
|
+
}
|
|
856
1508
|
return false;
|
|
1509
|
+
}
|
|
857
1510
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
858
1511
|
if (view.composing)
|
|
859
1512
|
return false;
|
|
@@ -873,7 +1526,7 @@ export class InkKitEditor {
|
|
|
873
1526
|
this.assertAlive();
|
|
874
1527
|
if (!this.ready)
|
|
875
1528
|
throw new InkKitError('not-ready', 'No document has been loaded');
|
|
876
|
-
if (this.
|
|
1529
|
+
if (this.literalSurface)
|
|
877
1530
|
this.plain.focus();
|
|
878
1531
|
else
|
|
879
1532
|
this.editor.ctx.get(editorViewCtx).focus();
|
|
@@ -882,24 +1535,61 @@ export class InkKitEditor {
|
|
|
882
1535
|
* order, so a key given to two shortcuts lands the same way every time. */
|
|
883
1536
|
setKeymap(keymap) {
|
|
884
1537
|
this.assertAlive();
|
|
1538
|
+
this.configuredKeys = {
|
|
1539
|
+
undo: ['Mod-z'],
|
|
1540
|
+
redo: ['Mod-y', 'Shift-Mod-z'],
|
|
1541
|
+
tableExit: ['Mod-Enter', 'Enter'],
|
|
1542
|
+
...keymap,
|
|
1543
|
+
};
|
|
885
1544
|
const bindings = {};
|
|
1545
|
+
// Consuming unbound history keys prevents the browser's separate undo stack
|
|
1546
|
+
// from changing source/TXT outside the shared document history.
|
|
1547
|
+
const historyBindings = Object.fromEntries(['Mod-z', 'Mod-y', 'Shift-Mod-z'].map((key) => [key, () => true]));
|
|
1548
|
+
for (const name of ['undo', 'redo']) {
|
|
1549
|
+
for (const key of this.configuredKeys[name] ?? []) {
|
|
1550
|
+
historyBindings[key] = () => {
|
|
1551
|
+
if (!this.ready || !this.commandState().commands[name])
|
|
1552
|
+
return true;
|
|
1553
|
+
return this[name]();
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
Object.assign(bindings, historyBindings);
|
|
886
1558
|
for (const [name, command] of Object.entries(shortcutCommands)) {
|
|
887
1559
|
for (const key of keymap[name] ?? []) {
|
|
888
1560
|
bindings[key] = () => {
|
|
1561
|
+
if (!this.ready || !this.commandState().commands.format[command[0]])
|
|
1562
|
+
return true;
|
|
889
1563
|
this.format(...command);
|
|
890
1564
|
return true;
|
|
891
1565
|
};
|
|
892
1566
|
}
|
|
893
1567
|
}
|
|
1568
|
+
for (const command of tableCommands) {
|
|
1569
|
+
const name = 'table' + command[0].toUpperCase() + command.slice(1);
|
|
1570
|
+
for (const key of this.configuredKeys[name] ?? [])
|
|
1571
|
+
bindings[key] = () => {
|
|
1572
|
+
if (!this.ready || !this.commandState().commands.table[command])
|
|
1573
|
+
return false;
|
|
1574
|
+
return this.table(command);
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
this.readOnlyKeys = keydownHandler(Object.fromEntries(Object.keys(bindings).map((key) => [
|
|
1578
|
+
key,
|
|
1579
|
+
() => {
|
|
1580
|
+
throw new InkKitError('read-only', 'The editor is read-only');
|
|
1581
|
+
},
|
|
1582
|
+
])));
|
|
894
1583
|
this.keys = keydownHandler(bindings);
|
|
1584
|
+
this.literalKeys = keydownHandler(historyBindings);
|
|
895
1585
|
}
|
|
896
1586
|
/** Dropped files land as one paragraph per path at the drop point: in place of an empty block, after
|
|
897
1587
|
* the top-level block otherwise, so a list or quote is not opened up by them. */
|
|
898
1588
|
insertPaths(paths, x, y) {
|
|
899
|
-
this.
|
|
1589
|
+
this.assertMutation();
|
|
900
1590
|
if (paths.length === 0)
|
|
901
1591
|
return;
|
|
902
|
-
if (this.
|
|
1592
|
+
if (this.literalSurface) {
|
|
903
1593
|
this.pasteAsPlainText(paths.join('\n\n'));
|
|
904
1594
|
return;
|
|
905
1595
|
}
|
|
@@ -907,10 +1597,10 @@ export class InkKitEditor {
|
|
|
907
1597
|
this.insertBlocks(paths.map((path) => schema.nodes.paragraph.create(null, schema.text(path))), x, y);
|
|
908
1598
|
}
|
|
909
1599
|
insertImages(images, x, y) {
|
|
910
|
-
this.
|
|
1600
|
+
this.assertMutation();
|
|
911
1601
|
if (!images.length)
|
|
912
1602
|
return;
|
|
913
|
-
if (!this.options.images || this.
|
|
1603
|
+
if (!this.options.images || this.literalSurface)
|
|
914
1604
|
throw new InkKitError('image-unavailable', 'This editor has no image adapter');
|
|
915
1605
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
916
1606
|
const { schema } = view.state;
|
|
@@ -1065,8 +1755,8 @@ export class InkKitEditor {
|
|
|
1065
1755
|
view.dispatch(view.state.tr.lift(range, target));
|
|
1066
1756
|
}
|
|
1067
1757
|
format(command, arg) {
|
|
1068
|
-
this.
|
|
1069
|
-
if (this.
|
|
1758
|
+
this.assertMutation();
|
|
1759
|
+
if (this.literalSurface)
|
|
1070
1760
|
return;
|
|
1071
1761
|
const run = (cmd, payload) => this.editor.action(callCommand(cmd, payload));
|
|
1072
1762
|
const state = caretState(this.editor.ctx.get(editorViewCtx).state);
|
|
@@ -1139,8 +1829,8 @@ export class InkKitEditor {
|
|
|
1139
1829
|
this.focus();
|
|
1140
1830
|
}
|
|
1141
1831
|
editReferenceDefinition(label, destination, title) {
|
|
1142
|
-
this.
|
|
1143
|
-
if (this.
|
|
1832
|
+
this.assertMutation();
|
|
1833
|
+
if (this.literalSurface)
|
|
1144
1834
|
return false;
|
|
1145
1835
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
1146
1836
|
const definitions = referenceDefinitions(view.state.doc);
|
|
@@ -1156,8 +1846,8 @@ export class InkKitEditor {
|
|
|
1156
1846
|
return true;
|
|
1157
1847
|
}
|
|
1158
1848
|
insertFootnote(label) {
|
|
1159
|
-
this.
|
|
1160
|
-
if (this.
|
|
1849
|
+
this.assertMutation();
|
|
1850
|
+
if (this.literalSurface)
|
|
1161
1851
|
return;
|
|
1162
1852
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
1163
1853
|
const definitions = footnoteDefinitions(view.state.doc);
|
|
@@ -1192,7 +1882,7 @@ export class InkKitEditor {
|
|
|
1192
1882
|
}
|
|
1193
1883
|
navigateFootnote(target) {
|
|
1194
1884
|
this.assertCurrent();
|
|
1195
|
-
if (this.
|
|
1885
|
+
if (this.literalSurface)
|
|
1196
1886
|
return false;
|
|
1197
1887
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
1198
1888
|
const { $from } = view.state.selection;
|