@aicayzer/inkkit 0.0.7 → 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 -16
- package/dist/callouts.js +6 -3
- package/dist/decorations.js +7 -6
- package/dist/editor.d.ts +65 -4
- package/dist/editor.js +396 -61
- package/dist/images.d.ts +1 -0
- package/dist/images.js +11 -0
- package/dist/index.d.ts +2 -1
- package/dist/labels.d.ts +28 -0
- package/dist/labels.js +26 -0
- package/dist/mermaid.js +6 -3
- package/dist/paste.d.ts +1 -0
- package/dist/paste.js +5 -1
- package/dist/references.js +5 -4
- package/dist/selection.js +22 -10
- 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,25 +1,27 @@
|
|
|
1
|
-
import { Editor, defaultValueCtx, editorViewCtx, parserCtx, 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
5
|
import { closeHistory, undo as undoHistory, redo as redoHistory, } from '@milkdown/kit/prose/history';
|
|
6
6
|
import { cursor } from '@milkdown/kit/plugin/cursor';
|
|
7
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';
|
|
8
|
-
import { strikethroughKeymap, toggleStrikethroughCommand, } from '@milkdown/kit/preset/gfm';
|
|
8
|
+
import { strikethroughKeymap, tableKeymap, toggleStrikethroughCommand, } from '@milkdown/kit/preset/gfm';
|
|
9
9
|
import { NodeRange, Fragment, Slice, } from '@milkdown/kit/prose/model';
|
|
10
10
|
import { keydownHandler, keymap } from '@milkdown/kit/prose/keymap';
|
|
11
|
+
import { goToNextCell } from '@milkdown/kit/prose/tables';
|
|
11
12
|
import { liftListItem } from '@milkdown/kit/prose/schema-list';
|
|
12
13
|
import { findWrapping, liftTarget } from '@milkdown/kit/prose/transform';
|
|
13
14
|
import { AllSelection, NodeSelection, Plugin, PluginKey, Selection, TextSelection, } from '@milkdown/kit/prose/state';
|
|
14
15
|
import { $prose, callCommand, replaceAll, } from '@milkdown/kit/utils';
|
|
15
16
|
import { codeCopyPlugin, placeholderPlugin } from './decorations.js';
|
|
17
|
+
import { labelsCtx, defaultLabels } from './labels.js';
|
|
16
18
|
import { createDialect, stringifyOptions } from './dialect.js';
|
|
17
19
|
import { Preservation } from './preserve.js';
|
|
18
20
|
import { sourceAttribute, cleanSourceDoc, editedPlainSource, sourceSelection, revealSourceCaret, rawOffset, normalizedOffset, } from './source.js';
|
|
19
21
|
import { normaliseLabel, referenceDefinitions, footnoteDefinitions, } from './references.js';
|
|
20
22
|
import { selectionMarkdown, selectionContent, referenceMetadata, withReferenceMetadata, } from './reference-clipboard.js';
|
|
21
23
|
import { imageView } from './images.js';
|
|
22
|
-
import { tablePlugins, tableCommand, configureTableMovement, } from './tables.js';
|
|
24
|
+
import { tablePlugins, tableCommand, tableCommands, tableAvailability, tableContext, configureTableMovement, } from './tables.js';
|
|
23
25
|
import { InkKitError, } from './types.js';
|
|
24
26
|
import { highlightPlugin } from './highlight.js';
|
|
25
27
|
import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
|
|
@@ -36,6 +38,9 @@ const shortcutCommands = {
|
|
|
36
38
|
heading1: ['heading', 1],
|
|
37
39
|
heading2: ['heading', 2],
|
|
38
40
|
heading3: ['heading', 3],
|
|
41
|
+
heading4: ['heading', 4],
|
|
42
|
+
heading5: ['heading', 5],
|
|
43
|
+
heading6: ['heading', 6],
|
|
39
44
|
paragraph: ['paragraph'],
|
|
40
45
|
bold: ['bold'],
|
|
41
46
|
italic: ['italic'],
|
|
@@ -257,12 +262,20 @@ export class InkKitEditor {
|
|
|
257
262
|
pasteController;
|
|
258
263
|
plain;
|
|
259
264
|
root;
|
|
265
|
+
ownsRootClass = false;
|
|
260
266
|
clickHandler;
|
|
261
267
|
footnoteKeyHandler;
|
|
262
268
|
copyHandler;
|
|
263
269
|
originalSource = '';
|
|
264
270
|
generation = 0;
|
|
271
|
+
reportingSuppressed = false;
|
|
265
272
|
loading = false;
|
|
273
|
+
writable = true;
|
|
274
|
+
inputPreferences = {};
|
|
275
|
+
policyEpoch = 0;
|
|
276
|
+
literalKeys = () => false;
|
|
277
|
+
configuredKeys = {};
|
|
278
|
+
readOnlyKeys = () => false;
|
|
266
279
|
changePlugin = $prose(() => new Plugin({
|
|
267
280
|
key: new PluginKey('documentChanges'),
|
|
268
281
|
view: () => ({
|
|
@@ -301,6 +314,8 @@ export class InkKitEditor {
|
|
|
301
314
|
constructor(events, options) {
|
|
302
315
|
this.events = events;
|
|
303
316
|
this.options = options;
|
|
317
|
+
this.writable = options.editable ?? true;
|
|
318
|
+
this.inputPreferences = { ...options.textInput };
|
|
304
319
|
}
|
|
305
320
|
context() {
|
|
306
321
|
return {
|
|
@@ -326,6 +341,212 @@ export class InkKitEditor {
|
|
|
326
341
|
if (this.pasteController.pending)
|
|
327
342
|
throw new InkKitError('operation-pending', 'An image import is in progress');
|
|
328
343
|
}
|
|
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
|
+
}));
|
|
329
550
|
get literalSurface() {
|
|
330
551
|
return this.formatType === 'txt' || this.mode === 'source';
|
|
331
552
|
}
|
|
@@ -362,23 +583,37 @@ export class InkKitEditor {
|
|
|
362
583
|
this.plain.value = text;
|
|
363
584
|
this.plain.setSelectionRange(this.plain.value.length, this.plain.value.length);
|
|
364
585
|
}
|
|
365
|
-
this.
|
|
586
|
+
const suppressed = this.reportingSuppressed;
|
|
587
|
+
this.reportingSuppressed = true;
|
|
588
|
+
try {
|
|
589
|
+
this.updateSurface();
|
|
590
|
+
}
|
|
591
|
+
finally {
|
|
592
|
+
this.reportingSuppressed = suppressed;
|
|
593
|
+
}
|
|
366
594
|
this.events.stateChanged(this.literalSurface
|
|
367
595
|
? { marks: [], block: { type: 'paragraph' }, quoted: false }
|
|
368
596
|
: caretState(this.editor.ctx.get(editorViewCtx).state));
|
|
597
|
+
this.publishCommandState();
|
|
369
598
|
return true;
|
|
370
599
|
}
|
|
371
600
|
updateSurface() {
|
|
372
601
|
this.plain.hidden = !this.literalSurface;
|
|
373
|
-
this.
|
|
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));
|
|
374
607
|
this.editor.ctx.get(editorViewCtx).dom.parentElement.hidden =
|
|
375
608
|
this.literalSurface;
|
|
376
609
|
}
|
|
377
610
|
replaceSource(text, expectedGeneration) {
|
|
378
|
-
this.
|
|
611
|
+
this.assertMutation(expectedGeneration);
|
|
379
612
|
return this.applySource(text, true);
|
|
380
613
|
}
|
|
381
614
|
applySource(text, isolated, from, to) {
|
|
615
|
+
if (!this.writable)
|
|
616
|
+
throw new InkKitError('read-only', 'The editor is read-only');
|
|
382
617
|
if (typeof text !== 'string')
|
|
383
618
|
throw new TypeError('Source must be a string');
|
|
384
619
|
if (text === this.currentText())
|
|
@@ -433,12 +668,12 @@ export class InkKitEditor {
|
|
|
433
668
|
return true;
|
|
434
669
|
}
|
|
435
670
|
undo(expectedGeneration) {
|
|
436
|
-
this.
|
|
671
|
+
this.assertMutation(expectedGeneration);
|
|
437
672
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
438
673
|
return undoHistory(view.state, view.dispatch);
|
|
439
674
|
}
|
|
440
675
|
redo(expectedGeneration) {
|
|
441
|
-
this.
|
|
676
|
+
this.assertMutation(expectedGeneration);
|
|
442
677
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
443
678
|
return redoHistory(view.state, view.dispatch);
|
|
444
679
|
}
|
|
@@ -583,7 +818,7 @@ export class InkKitEditor {
|
|
|
583
818
|
return result;
|
|
584
819
|
}
|
|
585
820
|
async paste(input) {
|
|
586
|
-
this.
|
|
821
|
+
this.assertMutation();
|
|
587
822
|
if (this.literalSurface || input.plainText) {
|
|
588
823
|
this.pasteAsPlainText(this.mode === 'source' && this.formatType === 'md'
|
|
589
824
|
? (input.markdown ?? input.text)
|
|
@@ -597,7 +832,7 @@ export class InkKitEditor {
|
|
|
597
832
|
setCommentVisibility(this.editor.ctx.get(editorViewCtx), visible);
|
|
598
833
|
}
|
|
599
834
|
pasteAsPlainText(text) {
|
|
600
|
-
this.
|
|
835
|
+
this.assertMutation();
|
|
601
836
|
if (this.literalSurface) {
|
|
602
837
|
this.replaceLiteralSelection(text, true, this.formatType === 'md');
|
|
603
838
|
}
|
|
@@ -636,7 +871,7 @@ export class InkKitEditor {
|
|
|
636
871
|
}
|
|
637
872
|
}
|
|
638
873
|
table(command, options) {
|
|
639
|
-
this.
|
|
874
|
+
this.assertMutation();
|
|
640
875
|
if (this.literalSurface)
|
|
641
876
|
return false;
|
|
642
877
|
return tableCommand(this.editor.ctx, command, options);
|
|
@@ -657,6 +892,8 @@ export class InkKitEditor {
|
|
|
657
892
|
}
|
|
658
893
|
this.plain.remove();
|
|
659
894
|
await this.editor.destroy();
|
|
895
|
+
if (this.ownsRootClass)
|
|
896
|
+
this.root.classList.remove('inkkit-root');
|
|
660
897
|
}
|
|
661
898
|
// The app's bindings, replaced whole whenever they change; the plugin stays.
|
|
662
899
|
keymapPlugin = $prose(() => new Plugin({
|
|
@@ -683,7 +920,8 @@ export class InkKitEditor {
|
|
|
683
920
|
if (node.type.name === 'image' || isMermaid(node))
|
|
684
921
|
portable = true;
|
|
685
922
|
});
|
|
686
|
-
const cut = event.type === 'cut';
|
|
923
|
+
const cut = event.type === 'cut' && this.writable;
|
|
924
|
+
const policyEpoch = this.policyEpoch;
|
|
687
925
|
const epoch = this.documentEpoch;
|
|
688
926
|
if (portable && this.events.clipboard) {
|
|
689
927
|
void this.clipboardSnapshot(false)
|
|
@@ -694,8 +932,9 @@ export class InkKitEditor {
|
|
|
694
932
|
throw new InkKitError('image-unavailable', 'The selected content could not be cut safely');
|
|
695
933
|
await this.events.clipboard(content);
|
|
696
934
|
if (cut) {
|
|
697
|
-
this.
|
|
935
|
+
this.assertMutation(snapshot.generation);
|
|
698
936
|
if (epoch !== this.documentEpoch ||
|
|
937
|
+
policyEpoch !== this.policyEpoch ||
|
|
699
938
|
snapshot.revision !== this.revision)
|
|
700
939
|
throw new InkKitError('stale-document', 'Document changed before cutting');
|
|
701
940
|
this.plain.setSelectionRange(from, to);
|
|
@@ -725,17 +964,24 @@ export class InkKitEditor {
|
|
|
725
964
|
static async mount(root, events, options = {}) {
|
|
726
965
|
const instance = new InkKitEditor(events, options);
|
|
727
966
|
instance.root = root;
|
|
967
|
+
instance.ownsRootClass = !root.classList.contains('inkkit-root');
|
|
968
|
+
root.classList.add('inkkit-root');
|
|
728
969
|
instance.pasteController = new PasteController({
|
|
729
970
|
ctx: () => instance.editor.ctx,
|
|
730
971
|
context: () => instance.context(),
|
|
731
972
|
adapter: options.images,
|
|
732
973
|
onError: (error) => events.error?.(error instanceof Error ? error : new Error(String(error))),
|
|
733
974
|
literalText: () => instance.literalSurface,
|
|
734
|
-
editable: () => !instance.literalSurface,
|
|
975
|
+
editable: () => instance.writable && !instance.literalSurface,
|
|
976
|
+
onPendingChanged: () => instance.publishCommandState(),
|
|
735
977
|
});
|
|
736
978
|
instance.editor = await Editor.make()
|
|
737
979
|
.config((ctx) => {
|
|
738
980
|
ctx.set(rootCtx, root);
|
|
981
|
+
ctx.update(labelsCtx.key, () => ({
|
|
982
|
+
...defaultLabels,
|
|
983
|
+
...options.labels,
|
|
984
|
+
}));
|
|
739
985
|
ctx.set(defaultValueCtx, '');
|
|
740
986
|
ctx.set(remarkStringifyOptionsCtx, stringifyOptions);
|
|
741
987
|
// The preset also binds Mod-[ and Mod-] here; the app uses those for back and forward.
|
|
@@ -754,6 +1000,8 @@ export class InkKitEditor {
|
|
|
754
1000
|
}
|
|
755
1001
|
return cleared;
|
|
756
1002
|
});
|
|
1003
|
+
unbind(tableKeymap, ['NextCell', 'PrevCell']);
|
|
1004
|
+
unbind(historyKeymap);
|
|
757
1005
|
unbind(strongKeymap);
|
|
758
1006
|
unbind(emphasisKeymap);
|
|
759
1007
|
unbind(inlineCodeKeymap);
|
|
@@ -772,7 +1020,8 @@ export class InkKitEditor {
|
|
|
772
1020
|
scrollMargin: { top: 8, right: 0, bottom: 24, left: 0 },
|
|
773
1021
|
}));
|
|
774
1022
|
})
|
|
775
|
-
.use(
|
|
1023
|
+
.use(labelsCtx)
|
|
1024
|
+
.use(caretStatePlugin(events, () => !instance.loading && !instance.literalSurface))
|
|
776
1025
|
.use(instance.pasteController.plugin)
|
|
777
1026
|
.use(visibleClipboard)
|
|
778
1027
|
.use(instance.keymapPlugin)
|
|
@@ -793,12 +1042,13 @@ export class InkKitEditor {
|
|
|
793
1042
|
.use(mermaidPreview((error) => events.error?.(error)))
|
|
794
1043
|
.use(selectionPlugin)
|
|
795
1044
|
.use($prose(() => search()))
|
|
1045
|
+
.use(instance.policyPlugin)
|
|
796
1046
|
.use(options.images ? imageView(options.images) : [])
|
|
797
1047
|
.create();
|
|
798
1048
|
configureTableMovement(instance.editor.ctx, () => {
|
|
799
1049
|
try {
|
|
800
1050
|
instance.assertCurrent();
|
|
801
|
-
return !instance.literalSurface;
|
|
1051
|
+
return instance.writable && !instance.literalSurface;
|
|
802
1052
|
}
|
|
803
1053
|
catch {
|
|
804
1054
|
return false;
|
|
@@ -809,7 +1059,11 @@ export class InkKitEditor {
|
|
|
809
1059
|
instance.plain.setAttribute('aria-label', 'Plain text editor');
|
|
810
1060
|
instance.plain.hidden = true;
|
|
811
1061
|
root.append(instance.plain);
|
|
812
|
-
instance.plain.addEventListener('beforeinput', () => {
|
|
1062
|
+
instance.plain.addEventListener('beforeinput', (event) => {
|
|
1063
|
+
if (!instance.writable) {
|
|
1064
|
+
event.preventDefault();
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
813
1067
|
if (instance.ready && instance.literalSurface)
|
|
814
1068
|
instance.beforePlainInput = {
|
|
815
1069
|
from: instance.plain.selectionStart,
|
|
@@ -819,6 +1073,10 @@ export class InkKitEditor {
|
|
|
819
1073
|
instance.plain.addEventListener('input', () => {
|
|
820
1074
|
if (!instance.ready || !instance.literalSurface)
|
|
821
1075
|
return;
|
|
1076
|
+
if (!instance.writable) {
|
|
1077
|
+
instance.plain.value = instance.currentText();
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
822
1080
|
try {
|
|
823
1081
|
instance.applySource(editedPlainSource(instance.currentText(), instance.plain.value), false, instance.plain.selectionStart, instance.plain.selectionEnd);
|
|
824
1082
|
}
|
|
@@ -830,22 +1088,18 @@ export class InkKitEditor {
|
|
|
830
1088
|
}
|
|
831
1089
|
});
|
|
832
1090
|
instance.plain.addEventListener('keydown', (event) => {
|
|
833
|
-
if (instance.plainComposing
|
|
1091
|
+
if (instance.plainComposing)
|
|
834
1092
|
return;
|
|
835
|
-
if (
|
|
836
|
-
|
|
837
|
-
event.preventDefault();
|
|
838
|
-
try {
|
|
839
|
-
if (event.key.toLowerCase() === 'y' || event.shiftKey)
|
|
840
|
-
instance.redo();
|
|
841
|
-
else
|
|
842
|
-
instance.undo();
|
|
843
|
-
}
|
|
844
|
-
catch (error) {
|
|
845
|
-
events.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
846
|
-
}
|
|
1093
|
+
if (instance.literalKeys(instance.editor.ctx.get(editorViewCtx), event))
|
|
1094
|
+
event.preventDefault();
|
|
847
1095
|
});
|
|
1096
|
+
for (const event of ['select', 'keyup', 'click'])
|
|
1097
|
+
instance.plain.addEventListener(event, () => instance.publishCommandState());
|
|
848
1098
|
instance.plain.addEventListener('paste', (event) => {
|
|
1099
|
+
if (!instance.writable) {
|
|
1100
|
+
event.preventDefault();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
849
1103
|
if (!event.clipboardData)
|
|
850
1104
|
return;
|
|
851
1105
|
event.preventDefault();
|
|
@@ -858,9 +1112,11 @@ export class InkKitEditor {
|
|
|
858
1112
|
});
|
|
859
1113
|
instance.plain.addEventListener('compositionstart', () => {
|
|
860
1114
|
instance.plainComposing = true;
|
|
1115
|
+
instance.publishCommandState();
|
|
861
1116
|
});
|
|
862
1117
|
instance.plain.addEventListener('compositionend', () => {
|
|
863
1118
|
instance.plainComposing = false;
|
|
1119
|
+
instance.publishCommandState();
|
|
864
1120
|
});
|
|
865
1121
|
const activateFootnote = (event) => {
|
|
866
1122
|
const element = event.target.closest('[data-inkkit-footnote-reference]');
|
|
@@ -936,7 +1192,7 @@ export class InkKitEditor {
|
|
|
936
1192
|
event.clipboardData.setData('text/html', metadata != null
|
|
937
1193
|
? withReferenceMetadata(output.html, metadata)
|
|
938
1194
|
: output.html);
|
|
939
|
-
if (event.type === 'cut')
|
|
1195
|
+
if (event.type === 'cut' && instance.writable)
|
|
940
1196
|
view.dispatch(view.state.tr.deleteSelection().scrollIntoView());
|
|
941
1197
|
}
|
|
942
1198
|
catch (error) {
|
|
@@ -949,7 +1205,8 @@ export class InkKitEditor {
|
|
|
949
1205
|
if (events.clipboard) {
|
|
950
1206
|
const epoch = instance.documentEpoch, revision = instance.revision;
|
|
951
1207
|
const { from, to } = view.state.selection;
|
|
952
|
-
const cut = event.type === 'cut';
|
|
1208
|
+
const cut = event.type === 'cut' && instance.writable;
|
|
1209
|
+
const policyEpoch = instance.policyEpoch;
|
|
953
1210
|
void instance
|
|
954
1211
|
.clipboardSnapshot(false)
|
|
955
1212
|
.then(async (content) => {
|
|
@@ -959,8 +1216,9 @@ export class InkKitEditor {
|
|
|
959
1216
|
throw new InkKitError('image-unavailable', 'The selected content could not be cut safely');
|
|
960
1217
|
await events.clipboard(content);
|
|
961
1218
|
if (cut) {
|
|
962
|
-
instance.
|
|
1219
|
+
instance.assertMutation();
|
|
963
1220
|
if (epoch !== instance.documentEpoch ||
|
|
1221
|
+
policyEpoch !== instance.policyEpoch ||
|
|
964
1222
|
revision !== instance.revision)
|
|
965
1223
|
throw new InkKitError('stale-document', 'Document changed before cutting');
|
|
966
1224
|
view.dispatch(view.state.tr.deleteRange(from, to).scrollIntoView());
|
|
@@ -978,10 +1236,13 @@ export class InkKitEditor {
|
|
|
978
1236
|
};
|
|
979
1237
|
root.addEventListener('copy', instance.copyHandler, true);
|
|
980
1238
|
root.addEventListener('cut', instance.copyHandler, true);
|
|
1239
|
+
instance.applyInputPolicy();
|
|
1240
|
+
instance.setKeymap(options.keymap ?? {});
|
|
981
1241
|
return instance;
|
|
982
1242
|
}
|
|
983
1243
|
loadDocument(input) {
|
|
984
1244
|
this.assertAlive();
|
|
1245
|
+
this.ready = false;
|
|
985
1246
|
this.pasteController.cancelPending();
|
|
986
1247
|
this.documentEpoch += 1;
|
|
987
1248
|
this.outlineEpoch += 1;
|
|
@@ -998,8 +1259,12 @@ export class InkKitEditor {
|
|
|
998
1259
|
this.load(input.format === 'txt' ? '' : input.text, input.generation);
|
|
999
1260
|
this.originalSource = input.text;
|
|
1000
1261
|
this.lastMarkdown = input.text;
|
|
1001
|
-
this.ready = true;
|
|
1002
1262
|
this.updateSurface();
|
|
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();
|
|
1003
1268
|
}
|
|
1004
1269
|
reloadDocument(input) {
|
|
1005
1270
|
this.assertAlive();
|
|
@@ -1013,19 +1278,26 @@ export class InkKitEditor {
|
|
|
1013
1278
|
const end = wasLiteral ? this.plain.selectionEnd : at;
|
|
1014
1279
|
const top = this.root.scrollTop;
|
|
1015
1280
|
const plainTop = this.plain.scrollTop, plainLeft = this.plain.scrollLeft;
|
|
1016
|
-
this.
|
|
1017
|
-
|
|
1018
|
-
input
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
this.
|
|
1028
|
-
this.
|
|
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();
|
|
1029
1301
|
}
|
|
1030
1302
|
}
|
|
1031
1303
|
load(markdown, generation) {
|
|
@@ -1048,7 +1320,6 @@ export class InkKitEditor {
|
|
|
1048
1320
|
const end = Selection.findFrom(doc.resolve(doc.content.size), -1, true) ??
|
|
1049
1321
|
Selection.atEnd(doc);
|
|
1050
1322
|
view.dispatch(setSearchState(view.state.tr.setSelection(end).scrollIntoView(), new SearchQuery({ search: '' })));
|
|
1051
|
-
this.events.stateChanged(caretState(view.state));
|
|
1052
1323
|
}
|
|
1053
1324
|
find(text, expectedGeneration) {
|
|
1054
1325
|
this.assertCurrent(expectedGeneration);
|
|
@@ -1084,11 +1355,11 @@ export class InkKitEditor {
|
|
|
1084
1355
|
findFormatted(this.editor.ctx.get(editorViewCtx), text);
|
|
1085
1356
|
}
|
|
1086
1357
|
replace(search, replacement, expectedGeneration) {
|
|
1087
|
-
this.
|
|
1358
|
+
this.assertMutation(expectedGeneration);
|
|
1088
1359
|
return this.replaceMatches(search, replacement, false) > 0;
|
|
1089
1360
|
}
|
|
1090
1361
|
replaceAll(search, replacement, expectedGeneration) {
|
|
1091
|
-
this.
|
|
1362
|
+
this.assertMutation(expectedGeneration);
|
|
1092
1363
|
return this.replaceMatches(search, replacement, true);
|
|
1093
1364
|
}
|
|
1094
1365
|
replaceMatches(search, replacement, all) {
|
|
@@ -1159,7 +1430,7 @@ export class InkKitEditor {
|
|
|
1159
1430
|
: this.editor.ctx.get(editorViewCtx).composing) &&
|
|
1160
1431
|
generation === this.generation)
|
|
1161
1432
|
return false;
|
|
1162
|
-
this.
|
|
1433
|
+
this.assertMutation(generation);
|
|
1163
1434
|
if (this.literalSurface) {
|
|
1164
1435
|
this.replaceLiteralSelection(text, false);
|
|
1165
1436
|
return true;
|
|
@@ -1183,11 +1454,38 @@ export class InkKitEditor {
|
|
|
1183
1454
|
generation === this.generation)
|
|
1184
1455
|
return false;
|
|
1185
1456
|
this.assertCurrent(generation);
|
|
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
|
+
}
|
|
1186
1483
|
if (this.literalSurface) {
|
|
1187
|
-
if ((
|
|
1188
|
-
return
|
|
1189
|
-
if (
|
|
1190
|
-
|
|
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');
|
|
1191
1489
|
if (metaKey || ctrlKey || altKey)
|
|
1192
1490
|
return false;
|
|
1193
1491
|
if (key === 'Enter' || key === 'Tab') {
|
|
@@ -1237,21 +1535,58 @@ export class InkKitEditor {
|
|
|
1237
1535
|
* order, so a key given to two shortcuts lands the same way every time. */
|
|
1238
1536
|
setKeymap(keymap) {
|
|
1239
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
|
+
};
|
|
1240
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);
|
|
1241
1558
|
for (const [name, command] of Object.entries(shortcutCommands)) {
|
|
1242
1559
|
for (const key of keymap[name] ?? []) {
|
|
1243
1560
|
bindings[key] = () => {
|
|
1561
|
+
if (!this.ready || !this.commandState().commands.format[command[0]])
|
|
1562
|
+
return true;
|
|
1244
1563
|
this.format(...command);
|
|
1245
1564
|
return true;
|
|
1246
1565
|
};
|
|
1247
1566
|
}
|
|
1248
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
|
+
])));
|
|
1249
1583
|
this.keys = keydownHandler(bindings);
|
|
1584
|
+
this.literalKeys = keydownHandler(historyBindings);
|
|
1250
1585
|
}
|
|
1251
1586
|
/** Dropped files land as one paragraph per path at the drop point: in place of an empty block, after
|
|
1252
1587
|
* the top-level block otherwise, so a list or quote is not opened up by them. */
|
|
1253
1588
|
insertPaths(paths, x, y) {
|
|
1254
|
-
this.
|
|
1589
|
+
this.assertMutation();
|
|
1255
1590
|
if (paths.length === 0)
|
|
1256
1591
|
return;
|
|
1257
1592
|
if (this.literalSurface) {
|
|
@@ -1262,7 +1597,7 @@ export class InkKitEditor {
|
|
|
1262
1597
|
this.insertBlocks(paths.map((path) => schema.nodes.paragraph.create(null, schema.text(path))), x, y);
|
|
1263
1598
|
}
|
|
1264
1599
|
insertImages(images, x, y) {
|
|
1265
|
-
this.
|
|
1600
|
+
this.assertMutation();
|
|
1266
1601
|
if (!images.length)
|
|
1267
1602
|
return;
|
|
1268
1603
|
if (!this.options.images || this.literalSurface)
|
|
@@ -1420,7 +1755,7 @@ export class InkKitEditor {
|
|
|
1420
1755
|
view.dispatch(view.state.tr.lift(range, target));
|
|
1421
1756
|
}
|
|
1422
1757
|
format(command, arg) {
|
|
1423
|
-
this.
|
|
1758
|
+
this.assertMutation();
|
|
1424
1759
|
if (this.literalSurface)
|
|
1425
1760
|
return;
|
|
1426
1761
|
const run = (cmd, payload) => this.editor.action(callCommand(cmd, payload));
|
|
@@ -1494,7 +1829,7 @@ export class InkKitEditor {
|
|
|
1494
1829
|
this.focus();
|
|
1495
1830
|
}
|
|
1496
1831
|
editReferenceDefinition(label, destination, title) {
|
|
1497
|
-
this.
|
|
1832
|
+
this.assertMutation();
|
|
1498
1833
|
if (this.literalSurface)
|
|
1499
1834
|
return false;
|
|
1500
1835
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
@@ -1511,7 +1846,7 @@ export class InkKitEditor {
|
|
|
1511
1846
|
return true;
|
|
1512
1847
|
}
|
|
1513
1848
|
insertFootnote(label) {
|
|
1514
|
-
this.
|
|
1849
|
+
this.assertMutation();
|
|
1515
1850
|
if (this.literalSurface)
|
|
1516
1851
|
return;
|
|
1517
1852
|
const view = this.editor.ctx.get(editorViewCtx);
|