@aicayzer/inkkit 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/callouts.js +2 -1
- package/dist/clipboard.d.ts +3 -3
- package/dist/clipboard.js +64 -25
- package/dist/dialect.d.ts +1 -1
- package/dist/dialect.js +36 -2
- package/dist/editor.d.ts +27 -1
- package/dist/editor.js +478 -35
- package/dist/files.d.ts +40 -0
- package/dist/files.js +133 -0
- package/dist/images.d.ts +20 -4
- package/dist/images.js +242 -25
- package/dist/index.d.ts +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +8 -0
- package/dist/linked-syntax.d.ts +48 -0
- package/dist/linked-syntax.js +308 -0
- package/dist/literals.js +40 -1
- package/dist/portable-files.d.ts +10 -0
- package/dist/portable-files.js +36 -0
- package/dist/preserve.js +7 -0
- package/dist/print.d.ts +2 -2
- package/dist/print.js +49 -17
- package/dist/search.d.ts +2 -1
- package/dist/search.js +53 -22
- package/dist/source.d.ts +0 -1
- package/dist/source.js +0 -60
- package/dist/style.css +39 -0
- package/dist/text-ranges.d.ts +26 -0
- package/dist/text-ranges.js +233 -0
- package/dist/types.d.ts +84 -2
- package/dist/wiki-view.d.ts +21 -0
- package/dist/wiki-view.js +78 -0
- package/package.json +1 -1
package/dist/editor.js
CHANGED
|
@@ -17,19 +17,22 @@ import { codeCopyPlugin, placeholderPlugin } from './decorations.js';
|
|
|
17
17
|
import { labelsCtx, defaultLabels } from './labels.js';
|
|
18
18
|
import { createDialect, stringifyOptions } from './dialect.js';
|
|
19
19
|
import { Preservation } from './preserve.js';
|
|
20
|
-
import { sourceAttribute, cleanSourceDoc, editedPlainSource, sourceSelection,
|
|
20
|
+
import { sourceAttribute, cleanSourceDoc, editedPlainSource, sourceSelection, rawOffset, normalizedOffset, } from './source.js';
|
|
21
21
|
import { normaliseLabel, referenceDefinitions, footnoteDefinitions, } from './references.js';
|
|
22
22
|
import { selectionMarkdown, selectionContent, referenceMetadata, withReferenceMetadata, } from './reference-clipboard.js';
|
|
23
23
|
import { imageView } from './images.js';
|
|
24
|
+
import { wikiLinkView } from './wiki-view.js';
|
|
25
|
+
import { withFileSignal } from './files.js';
|
|
24
26
|
import { tablePlugins, tableCommand, tableCommands, tableAvailability, tableContext, configureTableMovement, } from './tables.js';
|
|
25
27
|
import { InkKitError, } from './types.js';
|
|
28
|
+
import { readableProjection, projectionPosition, projectionOffset, validateOffsets, formattedRects, literalRects, literalVisibleRanges, intersectsViewport, textRect, } from './text-ranges.js';
|
|
26
29
|
import { highlightPlugin } from './highlight.js';
|
|
27
30
|
import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
|
|
28
31
|
import { commentSelectionContent, setCommentVisibility } from './comments.js';
|
|
29
32
|
import { PasteController } from './paste.js';
|
|
30
33
|
import { selectionPlugin } from './selection.js';
|
|
31
34
|
import { search, SearchQuery, setSearchState } from 'prosemirror-search';
|
|
32
|
-
import { findFormatted, findLiteral, replaceFormatted, replaceLiteral, } from './search.js';
|
|
35
|
+
import { findFormatted, findLiteral, replaceFormatted, replaceLiteral, replaceFormattedRange, } from './search.js';
|
|
33
36
|
import { collectFormattedHeadings, collectSourceHeadings, navigateFormattedHeading, sourceHeadingPosition, } from './outline.js';
|
|
34
37
|
import { taskListPlugin, toggleTaskList } from './tasks.js';
|
|
35
38
|
import { isMermaid, mermaidPreview } from './mermaid.js';
|
|
@@ -239,6 +242,7 @@ function joinLists(tr, start, index) {
|
|
|
239
242
|
if (before && after && isList(before) && after.type === before.type)
|
|
240
243
|
tr.join($start.posAtIndex(index));
|
|
241
244
|
}
|
|
245
|
+
let editorSequence = 0;
|
|
242
246
|
export class InkKitEditor {
|
|
243
247
|
events;
|
|
244
248
|
options;
|
|
@@ -249,6 +253,7 @@ export class InkKitEditor {
|
|
|
249
253
|
revision = 0;
|
|
250
254
|
documentEpoch = 0;
|
|
251
255
|
operationSequence = 0;
|
|
256
|
+
exportControllers = new Set();
|
|
252
257
|
plainComposing = false;
|
|
253
258
|
plainSearch = '';
|
|
254
259
|
literalMatch;
|
|
@@ -270,6 +275,15 @@ export class InkKitEditor {
|
|
|
270
275
|
generation = 0;
|
|
271
276
|
reportingSuppressed = false;
|
|
272
277
|
loading = false;
|
|
278
|
+
textIdentity = ++editorSequence;
|
|
279
|
+
textScope;
|
|
280
|
+
viewportContainer;
|
|
281
|
+
viewportInsets = {
|
|
282
|
+
top: 0,
|
|
283
|
+
right: 0,
|
|
284
|
+
bottom: 0,
|
|
285
|
+
left: 0,
|
|
286
|
+
};
|
|
273
287
|
writable = true;
|
|
274
288
|
inputPreferences = {};
|
|
275
289
|
policyEpoch = 0;
|
|
@@ -283,6 +297,8 @@ export class InkKitEditor {
|
|
|
283
297
|
if (this.loading || view.state.doc.eq(previous.doc))
|
|
284
298
|
return;
|
|
285
299
|
this.revision += 1;
|
|
300
|
+
this.cancelExports();
|
|
301
|
+
this.mediaPolicy();
|
|
286
302
|
this.literalMatch = undefined;
|
|
287
303
|
let markdown;
|
|
288
304
|
try {
|
|
@@ -300,7 +316,7 @@ export class InkKitEditor {
|
|
|
300
316
|
this.plain.value = markdown;
|
|
301
317
|
if (!this.plainComposing) {
|
|
302
318
|
this.plain.setSelectionRange(Math.min(from, this.plain.value.length), Math.min(to, this.plain.value.length));
|
|
303
|
-
|
|
319
|
+
this.revealLiteralSelection();
|
|
304
320
|
}
|
|
305
321
|
}
|
|
306
322
|
if (markdown === this.lastMarkdown)
|
|
@@ -324,6 +340,36 @@ export class InkKitEditor {
|
|
|
324
340
|
operationId: `${this.documentEpoch}:${this.generation}:${++this.operationSequence}`,
|
|
325
341
|
};
|
|
326
342
|
}
|
|
343
|
+
cancelExports() {
|
|
344
|
+
for (const controller of this.exportControllers)
|
|
345
|
+
controller.abort();
|
|
346
|
+
this.exportControllers.clear();
|
|
347
|
+
}
|
|
348
|
+
async exportContent(snapshot, operation) {
|
|
349
|
+
const epoch = this.documentEpoch;
|
|
350
|
+
const mode = this.editingMode;
|
|
351
|
+
const controller = new AbortController();
|
|
352
|
+
this.exportControllers.add(controller);
|
|
353
|
+
try {
|
|
354
|
+
return await withFileSignal(operation(controller.signal), controller.signal);
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
this.exportControllers.delete(controller);
|
|
358
|
+
this.assertCurrent(snapshot.generation);
|
|
359
|
+
if (controller.signal.aborted ||
|
|
360
|
+
epoch !== this.documentEpoch ||
|
|
361
|
+
snapshot.documentId !== this.documentId ||
|
|
362
|
+
snapshot.revision !== this.revision ||
|
|
363
|
+
mode !== this.editingMode)
|
|
364
|
+
throw new InkKitError('stale-document', 'Document changed while preparing output');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
mediaPolicy() {
|
|
368
|
+
if (this.editor && this.root)
|
|
369
|
+
this.editor.ctx
|
|
370
|
+
.get(editorViewCtx)
|
|
371
|
+
.dom.dispatchEvent(new Event('inkkit-media-policy'));
|
|
372
|
+
}
|
|
327
373
|
assertAlive() {
|
|
328
374
|
if (this.destroyed)
|
|
329
375
|
throw new InkKitError('destroyed', 'The editor has been destroyed');
|
|
@@ -372,6 +418,7 @@ export class InkKitEditor {
|
|
|
372
418
|
.get(editorViewCtx)
|
|
373
419
|
.dom.dispatchEvent(new Event('inkkit-cancel-resize'));
|
|
374
420
|
this.applyInputPolicy();
|
|
421
|
+
this.mediaPolicy();
|
|
375
422
|
}
|
|
376
423
|
finally {
|
|
377
424
|
this.reportingSuppressed = suppressed;
|
|
@@ -574,8 +621,15 @@ export class InkKitEditor {
|
|
|
574
621
|
throw new RangeError('Editing mode must be source or formatted');
|
|
575
622
|
if (this.formatType === 'txt' || this.mode === mode)
|
|
576
623
|
return false;
|
|
624
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
625
|
+
const focused = this.literalSurface
|
|
626
|
+
? this.plain.ownerDocument.activeElement === this.plain
|
|
627
|
+
: view.hasFocus();
|
|
628
|
+
const container = this.viewportContainer ?? this.root;
|
|
629
|
+
const top = container.scrollTop, left = container.scrollLeft;
|
|
577
630
|
const text = this.snapshot().text;
|
|
578
631
|
this.mode = mode;
|
|
632
|
+
this.cancelExports();
|
|
579
633
|
this.outlineEpoch += 1;
|
|
580
634
|
this.plainSearch = '';
|
|
581
635
|
this.literalMatch = undefined;
|
|
@@ -591,6 +645,14 @@ export class InkKitEditor {
|
|
|
591
645
|
finally {
|
|
592
646
|
this.reportingSuppressed = suppressed;
|
|
593
647
|
}
|
|
648
|
+
if (focused) {
|
|
649
|
+
if (this.literalSurface)
|
|
650
|
+
this.plain.focus({ preventScroll: true });
|
|
651
|
+
else
|
|
652
|
+
view.focus();
|
|
653
|
+
}
|
|
654
|
+
container.scrollTop = top;
|
|
655
|
+
container.scrollLeft = left;
|
|
594
656
|
this.events.stateChanged(this.literalSurface
|
|
595
657
|
? { marks: [], block: { type: 'paragraph' }, quoted: false }
|
|
596
658
|
: caretState(this.editor.ctx.get(editorViewCtx).state));
|
|
@@ -606,6 +668,7 @@ export class InkKitEditor {
|
|
|
606
668
|
: (this.options.labels?.sourceEditor ?? defaultLabels.sourceEditor));
|
|
607
669
|
this.editor.ctx.get(editorViewCtx).dom.parentElement.hidden =
|
|
608
670
|
this.literalSurface;
|
|
671
|
+
this.mediaPolicy();
|
|
609
672
|
}
|
|
610
673
|
replaceSource(text, expectedGeneration) {
|
|
611
674
|
this.assertMutation(expectedGeneration);
|
|
@@ -710,26 +773,17 @@ export class InkKitEditor {
|
|
|
710
773
|
}
|
|
711
774
|
async printableSnapshot(expectedGeneration) {
|
|
712
775
|
const snapshot = this.snapshot(expectedGeneration);
|
|
713
|
-
const epoch = this.documentEpoch;
|
|
714
776
|
const state = this.editor.ctx.get(editorViewCtx).state;
|
|
715
|
-
|
|
716
|
-
try {
|
|
777
|
+
const output = await this.exportContent(snapshot, async (signal) => {
|
|
717
778
|
if (snapshot.format === 'md' &&
|
|
718
779
|
!this.options.images &&
|
|
780
|
+
!this.options.files &&
|
|
719
781
|
hasAuthoredImagesInLiterals(state.doc.content, (source) => this.editor.ctx.get(remarkCtx).parse(source)))
|
|
720
782
|
throw new InkKitError('image-unavailable', 'No image adapter is configured for the authored images.');
|
|
721
|
-
|
|
722
|
-
snapshot.
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
finally {
|
|
727
|
-
this.assertCurrent(snapshot.generation);
|
|
728
|
-
if (snapshot.documentId !== this.documentId ||
|
|
729
|
-
epoch !== this.documentEpoch ||
|
|
730
|
-
snapshot.revision !== this.revision)
|
|
731
|
-
throw new InkKitError('stale-document', 'Document changed while preparing printable content');
|
|
732
|
-
}
|
|
783
|
+
return snapshot.format === 'txt'
|
|
784
|
+
? printableText(snapshot.text)
|
|
785
|
+
: printableMarkdown(state.doc.content, state.schema, this.options.images, this.context(), this.options.files, signal);
|
|
786
|
+
});
|
|
733
787
|
return Object.freeze({
|
|
734
788
|
documentId: snapshot.documentId,
|
|
735
789
|
generation: snapshot.generation,
|
|
@@ -802,7 +856,7 @@ export class InkKitEditor {
|
|
|
802
856
|
? error.message
|
|
803
857
|
: 'Cannot copy the selected Markdown');
|
|
804
858
|
}
|
|
805
|
-
const result = await portableClipboard(all || selectedDoc ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams);
|
|
859
|
+
const result = await this.exportContent(snapshot, (signal) => portableClipboard(all || selectedDoc ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams, this.options.files, signal));
|
|
806
860
|
const metadata = referenceMetadata(this.editor.ctx, selectedDoc ?? doc, content);
|
|
807
861
|
if (metadata != null)
|
|
808
862
|
result.html = withReferenceMetadata(result.html, metadata);
|
|
@@ -881,6 +935,8 @@ export class InkKitEditor {
|
|
|
881
935
|
return;
|
|
882
936
|
this.destroyed = true;
|
|
883
937
|
this.ready = false;
|
|
938
|
+
this.cancelExports();
|
|
939
|
+
this.mediaPolicy();
|
|
884
940
|
this.pasteController.destroy();
|
|
885
941
|
if (this.clickHandler)
|
|
886
942
|
this.root.removeEventListener('click', this.clickHandler);
|
|
@@ -1013,11 +1069,13 @@ export class InkKitEditor {
|
|
|
1013
1069
|
unbind(codeBlockKeymap);
|
|
1014
1070
|
unbind(bulletListKeymap);
|
|
1015
1071
|
unbind(orderedListKeymap);
|
|
1016
|
-
// The caret is kept above the fade under the formatting bar.
|
|
1017
1072
|
ctx.update(editorViewOptionsCtx, (options) => ({
|
|
1018
1073
|
...options,
|
|
1019
|
-
|
|
1020
|
-
|
|
1074
|
+
handleScrollToSelection: () => {
|
|
1075
|
+
if (!instance.loading && instance.ready)
|
|
1076
|
+
instance.revealCurrentSelection();
|
|
1077
|
+
return true;
|
|
1078
|
+
},
|
|
1021
1079
|
}));
|
|
1022
1080
|
})
|
|
1023
1081
|
.use(labelsCtx)
|
|
@@ -1025,7 +1083,7 @@ export class InkKitEditor {
|
|
|
1025
1083
|
.use(instance.pasteController.plugin)
|
|
1026
1084
|
.use(visibleClipboard)
|
|
1027
1085
|
.use(instance.keymapPlugin)
|
|
1028
|
-
.use(createDialect(Boolean(options.images)))
|
|
1086
|
+
.use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files)))
|
|
1029
1087
|
.use(tablePlugins)
|
|
1030
1088
|
.use(instance.changePlugin)
|
|
1031
1089
|
.use(history)
|
|
@@ -1043,7 +1101,57 @@ export class InkKitEditor {
|
|
|
1043
1101
|
.use(selectionPlugin)
|
|
1044
1102
|
.use($prose(() => search()))
|
|
1045
1103
|
.use(instance.policyPlugin)
|
|
1046
|
-
.use(options.
|
|
1104
|
+
.use(options.wikiLinks
|
|
1105
|
+
? wikiLinkView(options.wikiLinks, {
|
|
1106
|
+
capture: () => {
|
|
1107
|
+
const epoch = instance.documentEpoch;
|
|
1108
|
+
const context = instance.context();
|
|
1109
|
+
return {
|
|
1110
|
+
context,
|
|
1111
|
+
isCurrent: () => !instance.destroyed &&
|
|
1112
|
+
epoch === instance.documentEpoch &&
|
|
1113
|
+
context.documentId === instance.documentId &&
|
|
1114
|
+
context.generation === instance.generation,
|
|
1115
|
+
};
|
|
1116
|
+
},
|
|
1117
|
+
active: () => instance.ready &&
|
|
1118
|
+
!instance.destroyed &&
|
|
1119
|
+
!instance.literalSurface,
|
|
1120
|
+
labels: { ...defaultLabels, ...options.labels },
|
|
1121
|
+
error: (error) => events.error?.(error),
|
|
1122
|
+
})
|
|
1123
|
+
: [])
|
|
1124
|
+
.use(options.images || options.files
|
|
1125
|
+
? imageView(options.images, options.files
|
|
1126
|
+
? {
|
|
1127
|
+
adapter: options.files,
|
|
1128
|
+
capture: () => {
|
|
1129
|
+
const epoch = instance.documentEpoch;
|
|
1130
|
+
const context = instance.context();
|
|
1131
|
+
return {
|
|
1132
|
+
context,
|
|
1133
|
+
isCurrent: () => !instance.destroyed &&
|
|
1134
|
+
epoch === instance.documentEpoch &&
|
|
1135
|
+
context.documentId === instance.documentId &&
|
|
1136
|
+
context.generation === instance.generation,
|
|
1137
|
+
};
|
|
1138
|
+
},
|
|
1139
|
+
active: () => instance.ready &&
|
|
1140
|
+
!instance.destroyed &&
|
|
1141
|
+
!instance.literalSurface,
|
|
1142
|
+
labels: { ...defaultLabels, ...options.labels },
|
|
1143
|
+
error: (error) => events.error?.(error),
|
|
1144
|
+
}
|
|
1145
|
+
: undefined, () => {
|
|
1146
|
+
try {
|
|
1147
|
+
instance.assertMutation();
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
})
|
|
1154
|
+
: [])
|
|
1047
1155
|
.create();
|
|
1048
1156
|
configureTableMovement(instance.editor.ctx, () => {
|
|
1049
1157
|
try {
|
|
@@ -1242,7 +1350,14 @@ export class InkKitEditor {
|
|
|
1242
1350
|
}
|
|
1243
1351
|
loadDocument(input) {
|
|
1244
1352
|
this.assertAlive();
|
|
1353
|
+
const container = this.viewportContainer ?? this.root;
|
|
1354
|
+
const top = container.scrollTop, left = container.scrollLeft;
|
|
1355
|
+
const focused = this.literalSurface
|
|
1356
|
+
? this.plain.ownerDocument.activeElement === this.plain
|
|
1357
|
+
: this.editor.ctx.get(editorViewCtx).hasFocus();
|
|
1245
1358
|
this.ready = false;
|
|
1359
|
+
this.cancelExports();
|
|
1360
|
+
this.mediaPolicy();
|
|
1246
1361
|
this.pasteController.cancelPending();
|
|
1247
1362
|
this.documentEpoch += 1;
|
|
1248
1363
|
this.outlineEpoch += 1;
|
|
@@ -1256,11 +1371,21 @@ export class InkKitEditor {
|
|
|
1256
1371
|
this.formatType = input.format;
|
|
1257
1372
|
this.revision = 0;
|
|
1258
1373
|
this.plain.value = input.text;
|
|
1374
|
+
this.plain.setSelectionRange(this.plain.value.length, this.plain.value.length);
|
|
1259
1375
|
this.load(input.format === 'txt' ? '' : input.text, input.generation);
|
|
1260
1376
|
this.originalSource = input.text;
|
|
1261
1377
|
this.lastMarkdown = input.text;
|
|
1262
1378
|
this.updateSurface();
|
|
1263
1379
|
this.ready = true;
|
|
1380
|
+
this.mediaPolicy();
|
|
1381
|
+
if (focused) {
|
|
1382
|
+
if (this.literalSurface)
|
|
1383
|
+
this.plain.focus({ preventScroll: true });
|
|
1384
|
+
else
|
|
1385
|
+
this.editor.ctx.get(editorViewCtx).focus();
|
|
1386
|
+
}
|
|
1387
|
+
container.scrollTop = top;
|
|
1388
|
+
container.scrollLeft = left;
|
|
1264
1389
|
this.events.stateChanged(this.literalSurface
|
|
1265
1390
|
? { marks: [], block: { type: 'paragraph' }, quoted: false }
|
|
1266
1391
|
: caretState(this.editor.ctx.get(editorViewCtx).state));
|
|
@@ -1272,11 +1397,19 @@ export class InkKitEditor {
|
|
|
1272
1397
|
const wasLiteral = this.literalSurface;
|
|
1273
1398
|
const previousMode = this.editingMode;
|
|
1274
1399
|
const previousFormat = this.formatType;
|
|
1400
|
+
const selectedNode = !wasLiteral && view.state.selection instanceof NodeSelection
|
|
1401
|
+
? view.state.selection.node
|
|
1402
|
+
: undefined;
|
|
1275
1403
|
const at = wasLiteral
|
|
1276
1404
|
? this.plain.selectionStart
|
|
1277
|
-
: view.state.selection.
|
|
1278
|
-
const end = wasLiteral ? this.plain.selectionEnd :
|
|
1279
|
-
const
|
|
1405
|
+
: view.state.selection.anchor;
|
|
1406
|
+
const end = wasLiteral ? this.plain.selectionEnd : view.state.selection.head;
|
|
1407
|
+
const direction = this.plain.selectionDirection;
|
|
1408
|
+
const container = this.viewportContainer ?? this.root;
|
|
1409
|
+
const top = container.scrollTop, left = container.scrollLeft;
|
|
1410
|
+
const focused = wasLiteral
|
|
1411
|
+
? document.activeElement === this.plain
|
|
1412
|
+
: view.hasFocus();
|
|
1280
1413
|
const plainTop = this.plain.scrollTop, plainLeft = this.plain.scrollLeft;
|
|
1281
1414
|
this.reportingSuppressed = true;
|
|
1282
1415
|
try {
|
|
@@ -1286,10 +1419,24 @@ export class InkKitEditor {
|
|
|
1286
1419
|
previousMode === 'source')
|
|
1287
1420
|
this.setEditingMode('source', input.generation);
|
|
1288
1421
|
if (this.literalSurface)
|
|
1289
|
-
this.plain.setSelectionRange(Math.min(at, this.plain.value.length), Math.min(end, this.plain.value.length));
|
|
1290
|
-
else
|
|
1291
|
-
|
|
1292
|
-
|
|
1422
|
+
this.plain.setSelectionRange(Math.min(at, this.plain.value.length), Math.min(end, this.plain.value.length), direction);
|
|
1423
|
+
else {
|
|
1424
|
+
const anchor = Math.min(at, view.state.doc.content.size);
|
|
1425
|
+
const node = view.state.doc.nodeAt(anchor);
|
|
1426
|
+
view.dispatch(view.state.tr.setSelection(selectedNode &&
|
|
1427
|
+
node?.type === selectedNode.type &&
|
|
1428
|
+
NodeSelection.isSelectable(node)
|
|
1429
|
+
? NodeSelection.create(view.state.doc, anchor)
|
|
1430
|
+
: TextSelection.between(view.state.doc.resolve(anchor), view.state.doc.resolve(Math.min(end, view.state.doc.content.size)))));
|
|
1431
|
+
}
|
|
1432
|
+
if (focused) {
|
|
1433
|
+
if (this.literalSurface)
|
|
1434
|
+
this.plain.focus({ preventScroll: true });
|
|
1435
|
+
else
|
|
1436
|
+
view.focus();
|
|
1437
|
+
}
|
|
1438
|
+
container.scrollTop = top;
|
|
1439
|
+
container.scrollLeft = left;
|
|
1293
1440
|
if (wasLiteral && this.literalSurface) {
|
|
1294
1441
|
this.plain.scrollTop = plainTop;
|
|
1295
1442
|
this.plain.scrollLeft = plainLeft;
|
|
@@ -1300,6 +1447,301 @@ export class InkKitEditor {
|
|
|
1300
1447
|
this.publishCommandState();
|
|
1301
1448
|
}
|
|
1302
1449
|
}
|
|
1450
|
+
setViewport(options) {
|
|
1451
|
+
this.assertAlive();
|
|
1452
|
+
const container = options.scrollContainer ?? this.root;
|
|
1453
|
+
if (container !== this.root && !container.contains(this.root))
|
|
1454
|
+
throw new RangeError('The scroll container must be the editor root or an ancestor');
|
|
1455
|
+
const insets = { top: 0, right: 0, bottom: 0, left: 0, ...options.insets };
|
|
1456
|
+
if (Object.values(insets).some((value) => !Number.isFinite(value) || value < 0))
|
|
1457
|
+
throw new RangeError('Viewport insets must be finite non-negative CSS pixel values');
|
|
1458
|
+
this.viewportContainer = container;
|
|
1459
|
+
this.viewportInsets = insets;
|
|
1460
|
+
}
|
|
1461
|
+
viewport() {
|
|
1462
|
+
this.assertAlive();
|
|
1463
|
+
const container = this.viewportContainer ?? this.root;
|
|
1464
|
+
const bounds = container.getBoundingClientRect();
|
|
1465
|
+
const root = this.root.getBoundingClientRect();
|
|
1466
|
+
const insets = this.viewportInsets;
|
|
1467
|
+
const window = this.root.ownerDocument.defaultView;
|
|
1468
|
+
const left = Math.max(bounds.left + container.clientLeft, root.left, 0);
|
|
1469
|
+
const top = Math.max(bounds.top + container.clientTop, root.top, 0);
|
|
1470
|
+
const right = Math.max(left, Math.min(bounds.left + container.clientLeft + container.clientWidth, root.right, window.innerWidth));
|
|
1471
|
+
const bottom = Math.max(top, Math.min(bounds.top + container.clientTop + container.clientHeight, root.bottom, window.innerHeight));
|
|
1472
|
+
let rect = textRect({ left, top, right, bottom });
|
|
1473
|
+
if (this.literalSurface) {
|
|
1474
|
+
const plain = this.plain.getBoundingClientRect();
|
|
1475
|
+
const literalLeft = Math.max(rect.left, plain.left + this.plain.clientLeft);
|
|
1476
|
+
const literalTop = Math.max(rect.top, plain.top + this.plain.clientTop);
|
|
1477
|
+
rect = textRect({
|
|
1478
|
+
left: literalLeft,
|
|
1479
|
+
top: literalTop,
|
|
1480
|
+
right: Math.max(literalLeft, Math.min(rect.right, plain.left + this.plain.clientLeft + this.plain.clientWidth)),
|
|
1481
|
+
bottom: Math.max(literalTop, Math.min(rect.bottom, plain.top + this.plain.clientTop + this.plain.clientHeight)),
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
const insetLeft = Math.min(rect.right, rect.left + insets.left);
|
|
1485
|
+
const insetTop = Math.min(rect.bottom, rect.top + insets.top);
|
|
1486
|
+
rect = textRect({
|
|
1487
|
+
left: insetLeft,
|
|
1488
|
+
top: insetTop,
|
|
1489
|
+
right: Math.max(insetLeft, rect.right - insets.right),
|
|
1490
|
+
bottom: Math.max(insetTop, rect.bottom - insets.bottom),
|
|
1491
|
+
});
|
|
1492
|
+
return Object.freeze({
|
|
1493
|
+
rect: Object.freeze(rect),
|
|
1494
|
+
insets: Object.freeze({ ...insets }),
|
|
1495
|
+
scrollTop: container.scrollTop + (this.literalSurface ? this.plain.scrollTop : 0),
|
|
1496
|
+
scrollLeft: container.scrollLeft +
|
|
1497
|
+
(this.literalSurface ? this.plain.scrollLeft : 0),
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
readableScope() {
|
|
1501
|
+
const key = `${this.documentEpoch}:${this.revision}:${this.outlineEpoch}:${this.editingMode}`;
|
|
1502
|
+
if (this.textScope?.key !== key)
|
|
1503
|
+
this.textScope = {
|
|
1504
|
+
key,
|
|
1505
|
+
id: `${this.textIdentity}:${key}`,
|
|
1506
|
+
projection: this.literalSurface
|
|
1507
|
+
? { text: this.plain.value, spans: [] }
|
|
1508
|
+
: readableProjection(this.editor.ctx.get(editorViewCtx).state.doc),
|
|
1509
|
+
};
|
|
1510
|
+
return this.textScope;
|
|
1511
|
+
}
|
|
1512
|
+
textSnapshot(expectedGeneration) {
|
|
1513
|
+
this.assertCurrent(expectedGeneration);
|
|
1514
|
+
const scope = this.readableScope();
|
|
1515
|
+
const selection = this.editor.ctx.get(editorViewCtx).state.selection;
|
|
1516
|
+
return Object.freeze({
|
|
1517
|
+
snapshotId: scope.id,
|
|
1518
|
+
documentId: this.documentId,
|
|
1519
|
+
generation: this.generation,
|
|
1520
|
+
revision: this.revision,
|
|
1521
|
+
format: this.formatType,
|
|
1522
|
+
mode: this.editingMode,
|
|
1523
|
+
text: scope.projection.text,
|
|
1524
|
+
selection: Object.freeze({
|
|
1525
|
+
snapshotId: scope.id,
|
|
1526
|
+
from: this.literalSurface
|
|
1527
|
+
? this.plain.selectionStart
|
|
1528
|
+
: projectionOffset(scope.projection, selection.from),
|
|
1529
|
+
to: this.literalSurface
|
|
1530
|
+
? this.plain.selectionEnd
|
|
1531
|
+
: projectionOffset(scope.projection, selection.to),
|
|
1532
|
+
}),
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
validateTextRange(range) {
|
|
1536
|
+
this.assertCurrent();
|
|
1537
|
+
const scope = this.readableScope();
|
|
1538
|
+
if (range.snapshotId !== scope.id)
|
|
1539
|
+
throw new InkKitError('stale-document', 'The readable text snapshot changed');
|
|
1540
|
+
validateOffsets(scope.projection.text, range.from, range.to);
|
|
1541
|
+
return scope.projection;
|
|
1542
|
+
}
|
|
1543
|
+
selectTextRange(range, options = {}) {
|
|
1544
|
+
const projection = this.validateTextRange(range);
|
|
1545
|
+
if (this.literalSurface)
|
|
1546
|
+
this.plain.setSelectionRange(range.from, range.to);
|
|
1547
|
+
else {
|
|
1548
|
+
for (const span of projection.spans)
|
|
1549
|
+
if (span.kind === 'embed' &&
|
|
1550
|
+
((range.from > span.from && range.from < span.to) ||
|
|
1551
|
+
(range.to > span.from && range.to < span.to)))
|
|
1552
|
+
throw new InkKitError('invalid-range', 'Embedded labels must be selected as complete ranges');
|
|
1553
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1554
|
+
const from = projectionPosition(projection, range.from, 1);
|
|
1555
|
+
const to = projectionPosition(projection, range.to, range.from === range.to ? 1 : -1);
|
|
1556
|
+
const embed = projection.spans.find((span) => span.kind === 'embed' &&
|
|
1557
|
+
span.from === range.from &&
|
|
1558
|
+
span.to === range.to);
|
|
1559
|
+
view.dispatch(view.state.tr.setSelection(embed && view.state.doc.nodeAt(embed.start)
|
|
1560
|
+
? NodeSelection.create(view.state.doc, embed.start)
|
|
1561
|
+
: TextSelection.between(view.state.doc.resolve(from), view.state.doc.resolve(to))));
|
|
1562
|
+
}
|
|
1563
|
+
if (options.focus) {
|
|
1564
|
+
if (this.literalSurface)
|
|
1565
|
+
this.plain.focus({ preventScroll: true });
|
|
1566
|
+
else
|
|
1567
|
+
this.editor.ctx.get(editorViewCtx).focus();
|
|
1568
|
+
}
|
|
1569
|
+
if (options.reveal)
|
|
1570
|
+
this.revealTextRange(range);
|
|
1571
|
+
this.publishCommandState();
|
|
1572
|
+
}
|
|
1573
|
+
replaceTextRange(range, text) {
|
|
1574
|
+
const projection = this.validateTextRange(range);
|
|
1575
|
+
this.assertMutation();
|
|
1576
|
+
if (typeof text !== 'string')
|
|
1577
|
+
throw new TypeError('Replacement must be a string');
|
|
1578
|
+
if (this.literalSurface) {
|
|
1579
|
+
const raw = this.currentText();
|
|
1580
|
+
const from = rawOffset(raw, range.from), to = rawOffset(raw, range.to);
|
|
1581
|
+
const ending = /\r\n|\r|\n/.exec(raw)?.[0] ?? '\n';
|
|
1582
|
+
const value = text.replace(/\r\n?/g, '\n');
|
|
1583
|
+
const replaced = raw.slice(0, from) + value.replaceAll('\n', ending) + raw.slice(to);
|
|
1584
|
+
return this.applySource(replaced, true, range.from, range.from + value.length);
|
|
1585
|
+
}
|
|
1586
|
+
if (projection.spans.some((span) => (span.kind === 'embed' &&
|
|
1587
|
+
range.from > span.from &&
|
|
1588
|
+
range.from < span.to) ||
|
|
1589
|
+
(span.kind === 'embed' &&
|
|
1590
|
+
range.to > span.from &&
|
|
1591
|
+
range.to < span.to) ||
|
|
1592
|
+
(span.to > range.from &&
|
|
1593
|
+
span.from < range.to &&
|
|
1594
|
+
span.kind !== 'text')))
|
|
1595
|
+
throw new InkKitError('invalid-range', 'Replacement cannot remove embedded content or structural separators');
|
|
1596
|
+
const from = projectionPosition(projection, range.from, 1);
|
|
1597
|
+
const to = projectionPosition(projection, range.to, range.from === range.to ? 1 : -1);
|
|
1598
|
+
return replaceFormattedRange(this.editor.ctx.get(editorViewCtx), from, to, text, (doc) => {
|
|
1599
|
+
this.currentText(doc);
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
textRangeRects(range) {
|
|
1603
|
+
const projection = this.validateTextRange(range);
|
|
1604
|
+
const rects = this.literalSurface
|
|
1605
|
+
? literalRects(this.plain, range.from, range.to)
|
|
1606
|
+
: formattedRects(this.editor.ctx.get(editorViewCtx), projection, range.from, range.to);
|
|
1607
|
+
return Object.freeze(rects.map((rect) => Object.freeze(rect)));
|
|
1608
|
+
}
|
|
1609
|
+
visibleTextRanges(snapshotId) {
|
|
1610
|
+
const snapshot = this.textSnapshot();
|
|
1611
|
+
this.validateTextRange({ snapshotId, from: 0, to: 0 });
|
|
1612
|
+
const viewport = this.viewport().rect;
|
|
1613
|
+
if (this.literalSurface)
|
|
1614
|
+
return Object.freeze(literalVisibleRanges(this.plain, viewport).map((range) => Object.freeze({ snapshotId, ...range })));
|
|
1615
|
+
const visible = (rect) => intersectsViewport(rect, viewport);
|
|
1616
|
+
const ranges = [];
|
|
1617
|
+
const projection = this.readableScope().projection;
|
|
1618
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1619
|
+
for (const span of projection.spans) {
|
|
1620
|
+
const current = { text: projection.text, spans: [span] };
|
|
1621
|
+
if (span.kind === 'separator' ||
|
|
1622
|
+
!formattedRects(view, current, span.from, span.to).some(visible))
|
|
1623
|
+
continue;
|
|
1624
|
+
for (let from = span.from; from < span.to;) {
|
|
1625
|
+
const to = from + (snapshot.text.codePointAt(from) > 0xffff ? 2 : 1);
|
|
1626
|
+
if (formattedRects(view, current, from, to).some(visible)) {
|
|
1627
|
+
const previous = ranges.at(-1);
|
|
1628
|
+
if (previous?.to === from)
|
|
1629
|
+
previous.to = to;
|
|
1630
|
+
else
|
|
1631
|
+
ranges.push({ snapshotId, from, to });
|
|
1632
|
+
}
|
|
1633
|
+
from = to;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return Object.freeze(ranges.map((range) => Object.freeze(range)));
|
|
1637
|
+
}
|
|
1638
|
+
revealTextRange(range) {
|
|
1639
|
+
const projection = this.validateTextRange(range);
|
|
1640
|
+
if (!this.literalSurface) {
|
|
1641
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1642
|
+
const position = projectionPosition(projection, range.from, 1);
|
|
1643
|
+
const at = view.domAtPos(position).node;
|
|
1644
|
+
let element = at instanceof Element ? at : at.parentElement;
|
|
1645
|
+
while (element && this.root.contains(element)) {
|
|
1646
|
+
if (element.matches('.inkkit-callout[data-inkkit-folded="true"]'))
|
|
1647
|
+
element
|
|
1648
|
+
.querySelector(':scope > .inkkit-callout-header [data-inkkit-callout-toggle]')
|
|
1649
|
+
?.click();
|
|
1650
|
+
element = element.parentElement;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
this.revealRects(this.textRangeRects(range));
|
|
1654
|
+
}
|
|
1655
|
+
revealRects(rects) {
|
|
1656
|
+
if (!rects.length)
|
|
1657
|
+
return;
|
|
1658
|
+
const viewport = this.viewport().rect;
|
|
1659
|
+
const leading = rects[0];
|
|
1660
|
+
const envelope = {
|
|
1661
|
+
top: leading.top,
|
|
1662
|
+
bottom: leading.bottom,
|
|
1663
|
+
left: leading.left,
|
|
1664
|
+
right: leading.right,
|
|
1665
|
+
};
|
|
1666
|
+
for (const rect of rects) {
|
|
1667
|
+
envelope.top = Math.min(envelope.top, rect.top);
|
|
1668
|
+
envelope.bottom = Math.max(envelope.bottom, rect.bottom);
|
|
1669
|
+
envelope.left = Math.min(envelope.left, rect.left);
|
|
1670
|
+
envelope.right = Math.max(envelope.right, rect.right);
|
|
1671
|
+
}
|
|
1672
|
+
const fitsHeight = envelope.bottom - envelope.top <= viewport.height;
|
|
1673
|
+
const fitsWidth = envelope.right - envelope.left <= viewport.width;
|
|
1674
|
+
const rect = {
|
|
1675
|
+
top: fitsHeight ? envelope.top : leading.top,
|
|
1676
|
+
bottom: fitsHeight
|
|
1677
|
+
? envelope.bottom
|
|
1678
|
+
: Math.min(leading.bottom, leading.top + viewport.height),
|
|
1679
|
+
left: fitsWidth ? envelope.left : leading.left,
|
|
1680
|
+
right: fitsWidth
|
|
1681
|
+
? envelope.right
|
|
1682
|
+
: Math.min(leading.right, leading.left + viewport.width),
|
|
1683
|
+
};
|
|
1684
|
+
const vertical = viewport.height <= 0
|
|
1685
|
+
? 0
|
|
1686
|
+
: rect.top < viewport.top
|
|
1687
|
+
? rect.top - viewport.top
|
|
1688
|
+
: rect.bottom > viewport.bottom
|
|
1689
|
+
? rect.bottom - viewport.bottom
|
|
1690
|
+
: 0;
|
|
1691
|
+
const horizontal = viewport.width <= 0
|
|
1692
|
+
? 0
|
|
1693
|
+
: rect.left < viewport.left
|
|
1694
|
+
? rect.left - viewport.left
|
|
1695
|
+
: rect.right > viewport.right
|
|
1696
|
+
? rect.right - viewport.right
|
|
1697
|
+
: 0;
|
|
1698
|
+
let remainingTop = vertical, remainingLeft = horizontal;
|
|
1699
|
+
if (this.literalSurface) {
|
|
1700
|
+
const top = this.plain.scrollTop, left = this.plain.scrollLeft;
|
|
1701
|
+
this.plain.scrollTop += vertical;
|
|
1702
|
+
this.plain.scrollLeft += horizontal;
|
|
1703
|
+
remainingTop -= this.plain.scrollTop - top;
|
|
1704
|
+
remainingLeft -= this.plain.scrollLeft - left;
|
|
1705
|
+
}
|
|
1706
|
+
let container = this.viewportContainer ?? this.root;
|
|
1707
|
+
const document = this.root.ownerDocument;
|
|
1708
|
+
while (container &&
|
|
1709
|
+
container !== document.body &&
|
|
1710
|
+
container !== document.documentElement) {
|
|
1711
|
+
const top = container.scrollTop, left = container.scrollLeft;
|
|
1712
|
+
container.scrollTop += remainingTop;
|
|
1713
|
+
container.scrollLeft += remainingLeft;
|
|
1714
|
+
remainingTop -= container.scrollTop - top;
|
|
1715
|
+
remainingLeft -= container.scrollLeft - left;
|
|
1716
|
+
container = container.parentElement;
|
|
1717
|
+
}
|
|
1718
|
+
if (remainingTop || remainingLeft)
|
|
1719
|
+
document.defaultView.scrollBy(remainingLeft, remainingTop);
|
|
1720
|
+
}
|
|
1721
|
+
revealCurrentSelection() {
|
|
1722
|
+
if (this.loading || !this.ready)
|
|
1723
|
+
return;
|
|
1724
|
+
if (this.literalSurface) {
|
|
1725
|
+
this.revealLiteralSelection();
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1729
|
+
const projection = this.readableScope().projection;
|
|
1730
|
+
const from = projectionOffset(projection, view.state.selection.from);
|
|
1731
|
+
const to = projectionOffset(projection, view.state.selection.to);
|
|
1732
|
+
if (this.composing || this.pasteController.pending) {
|
|
1733
|
+
this.revealRects(formattedRects(view, projection, from, to));
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
this.revealTextRange({
|
|
1737
|
+
snapshotId: this.readableScope().id,
|
|
1738
|
+
from,
|
|
1739
|
+
to,
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
revealLiteralSelection() {
|
|
1743
|
+
this.revealRects(literalRects(this.plain, this.plain.selectionStart, this.plain.selectionEnd));
|
|
1744
|
+
}
|
|
1303
1745
|
load(markdown, generation) {
|
|
1304
1746
|
this.generation = generation;
|
|
1305
1747
|
this.originalSource = markdown;
|
|
@@ -1319,7 +1761,7 @@ export class InkKitEditor {
|
|
|
1319
1761
|
const { doc } = view.state;
|
|
1320
1762
|
const end = Selection.findFrom(doc.resolve(doc.content.size), -1, true) ??
|
|
1321
1763
|
Selection.atEnd(doc);
|
|
1322
|
-
view.dispatch(setSearchState(view.state.tr.setSelection(end)
|
|
1764
|
+
view.dispatch(setSearchState(view.state.tr.setSelection(end), new SearchQuery({ search: '' })));
|
|
1323
1765
|
}
|
|
1324
1766
|
find(text, expectedGeneration) {
|
|
1325
1767
|
this.assertCurrent(expectedGeneration);
|
|
@@ -1349,10 +1791,11 @@ export class InkKitEditor {
|
|
|
1349
1791
|
else if (this.plainSearch)
|
|
1350
1792
|
this.plain.setSelectionRange(this.plain.selectionEnd, this.plain.selectionEnd);
|
|
1351
1793
|
this.plainSearch = text;
|
|
1352
|
-
|
|
1794
|
+
this.revealLiteralSelection();
|
|
1353
1795
|
return;
|
|
1354
1796
|
}
|
|
1355
|
-
findFormatted(this.editor.ctx.get(editorViewCtx), text)
|
|
1797
|
+
if (findFormatted(this.editor.ctx.get(editorViewCtx), text))
|
|
1798
|
+
this.revealCurrentSelection();
|
|
1356
1799
|
}
|
|
1357
1800
|
replace(search, replacement, expectedGeneration) {
|
|
1358
1801
|
this.assertMutation(expectedGeneration);
|
|
@@ -1417,7 +1860,7 @@ export class InkKitEditor {
|
|
|
1417
1860
|
const position = sourceHeadingPosition(this.editor.ctx, this.plain.value, heading, this.outlineContext());
|
|
1418
1861
|
this.plain.setSelectionRange(position, position);
|
|
1419
1862
|
this.plain.focus({ preventScroll: true });
|
|
1420
|
-
|
|
1863
|
+
this.revealLiteralSelection();
|
|
1421
1864
|
return true;
|
|
1422
1865
|
}
|
|
1423
1866
|
return navigateFormattedHeading(this.editor.ctx.get(editorViewCtx), heading, this.outlineContext());
|