@aseity/idocs-preview 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,11 +22,12 @@ export declare function findMatches(index: TextIndex, query: string): FindMatch[
22
22
  */
23
23
  export declare class Finder {
24
24
  #private;
25
- readonly index: TextIndex;
25
+ index: TextIndex;
26
26
  matches: FindMatch[];
27
27
  /** Index of the current match in `matches`; -1 without matches. */
28
28
  current: number;
29
29
  constructor(index: TextIndex);
30
+ setIndex(index: TextIndex, activeQuery?: string, fromPage?: number): void;
30
31
  /**
31
32
  * Search for `query`; the current match is the first on or after page
32
33
  * `fromPage` (the first match when none is). Returns the number of matches.
package/dist/view/find.js CHANGED
@@ -50,6 +50,13 @@ export class Finder {
50
50
  constructor(index) {
51
51
  this.index = index;
52
52
  }
53
+ setIndex(index, activeQuery, fromPage = 0) {
54
+ this.index = index;
55
+ if (activeQuery)
56
+ this.search(activeQuery, fromPage);
57
+ else
58
+ this.clear();
59
+ }
53
60
  /**
54
61
  * Search for `query`; the current match is the first on or after page
55
62
  * `fromPage` (the first match when none is). Returns the number of matches.
@@ -0,0 +1,63 @@
1
+ import type { Commit, EditRange, Mark, Revision, Selection } from '../core/index.js';
2
+ import type { CaretBox } from './edit-selection.js';
3
+ import { type PageLayout } from './viewport.js';
4
+ export type InputMode = {
5
+ kind: 'idle';
6
+ } | {
7
+ kind: 'composing';
8
+ baseRevision: Revision;
9
+ replaceRange: EditRange;
10
+ text: string;
11
+ caret: CaretBox;
12
+ };
13
+ export interface CompositionCommit {
14
+ readonly base: Revision;
15
+ readonly range: EditRange;
16
+ readonly text: string;
17
+ }
18
+ export declare function startComposition(baseRevision: Revision, replaceRange: EditRange, caret: CaretBox): InputMode;
19
+ export declare function updateComposition(mode: InputMode, text: string): InputMode;
20
+ export declare function finishComposition(mode: InputMode, committedText: string): {
21
+ mode: InputMode;
22
+ commit: CompositionCommit | null;
23
+ };
24
+ export interface ImeHost {
25
+ readonly frame: HTMLElement;
26
+ readonly scroller: HTMLElement;
27
+ readonly content: HTMLElement;
28
+ layout(): PageLayout;
29
+ zoom(): number;
30
+ isFindOpen(): boolean;
31
+ openFind(): void;
32
+ closeFind(): void;
33
+ currentRevision(): Revision;
34
+ currentSelection(): Selection | null;
35
+ currentCaret(): CaretBox | null;
36
+ selectedText(): string;
37
+ hasReadOnlyTarget(): boolean;
38
+ clearReadOnlyTarget(): void;
39
+ collapseSelection(): void;
40
+ selectAllInFlow(): void;
41
+ moveHorizontal(direction: 'backward' | 'forward', granularity: 'character' | 'word', extend: boolean): Promise<void>;
42
+ moveVertical(direction: 'up' | 'down', extend: boolean): Promise<void>;
43
+ moveLineBoundary(edge: 'start' | 'end', extend: boolean): Promise<void>;
44
+ replaceSelection(text: string): Promise<Commit | null>;
45
+ applyRangeText(base: Revision, range: EditRange, text: string): Promise<Commit | null>;
46
+ deleteBackward(): Promise<Commit | null>;
47
+ deleteForward(): Promise<Commit | null>;
48
+ splitBlock(): Promise<Commit | null>;
49
+ toggleMark(mark: Mark): Promise<Commit | null>;
50
+ undo(): Promise<Commit | null>;
51
+ redo(): Promise<Commit | null>;
52
+ onFocusChange(focused: boolean): void;
53
+ onModeChange(mode: InputMode): void;
54
+ }
55
+ export interface ImeBridge {
56
+ readonly mode: InputMode;
57
+ readonly focused: boolean;
58
+ focus(): void;
59
+ position(caret: CaretBox | null): void;
60
+ enqueue<T>(task: () => Promise<T>): Promise<T>;
61
+ destroy(): void;
62
+ }
63
+ export declare function createImeBridge(host: ImeHost): ImeBridge;
@@ -0,0 +1,352 @@
1
+ import { turnedToPage } from './selection.js';
2
+ import { PX_PER_PT } from './viewport.js';
3
+ export function startComposition(baseRevision, replaceRange, caret) {
4
+ return {
5
+ kind: 'composing',
6
+ baseRevision,
7
+ replaceRange,
8
+ text: '',
9
+ caret,
10
+ };
11
+ }
12
+ export function updateComposition(mode, text) {
13
+ if (mode.kind !== 'composing')
14
+ return mode;
15
+ return { ...mode, text };
16
+ }
17
+ export function finishComposition(mode, committedText) {
18
+ if (mode.kind !== 'composing') {
19
+ return { mode, commit: null };
20
+ }
21
+ const hasRange = mode.replaceRange.start.offset !== mode.replaceRange.end.offset;
22
+ const commit = committedText.length > 0 || hasRange
23
+ ? {
24
+ base: mode.baseRevision,
25
+ range: mode.replaceRange,
26
+ text: committedText,
27
+ }
28
+ : null;
29
+ return { mode: { kind: 'idle' }, commit };
30
+ }
31
+ export function createImeBridge(host) {
32
+ const textarea = document.createElement('textarea');
33
+ textarea.className = 'idocs-editor-ime';
34
+ textarea.tabIndex = -1;
35
+ textarea.autocapitalize = 'off';
36
+ textarea.autocomplete = 'off';
37
+ textarea.spellcheck = false;
38
+ textarea.setAttribute('aria-label', 'Document editor');
39
+ Object.assign(textarea.style, {
40
+ position: 'absolute',
41
+ left: '0px',
42
+ top: '0px',
43
+ width: '1px',
44
+ height: '16px',
45
+ padding: '0',
46
+ margin: '0',
47
+ border: '0',
48
+ outline: 'none',
49
+ resize: 'none',
50
+ overflow: 'hidden',
51
+ background: 'transparent',
52
+ color: 'transparent',
53
+ caretColor: 'transparent',
54
+ opacity: '0',
55
+ pointerEvents: 'none',
56
+ zIndex: '0',
57
+ });
58
+ host.frame.append(textarea);
59
+ let mode = { kind: 'idle' };
60
+ let focused = false;
61
+ let destroyed = false;
62
+ let queue = Promise.resolve();
63
+ const enqueue = (task) => {
64
+ const run = queue.then(() => {
65
+ if (destroyed)
66
+ return undefined;
67
+ return task();
68
+ });
69
+ queue = run.catch(() => { });
70
+ return run;
71
+ };
72
+ const setMode = (next) => {
73
+ mode = next;
74
+ host.onModeChange(mode);
75
+ };
76
+ const position = (caret) => {
77
+ if (!caret) {
78
+ textarea.style.left = '0px';
79
+ textarea.style.top = '0px';
80
+ textarea.style.height = '1px';
81
+ return;
82
+ }
83
+ const layout = host.layout();
84
+ const placed = layout.pages[caret.page];
85
+ if (!placed)
86
+ return;
87
+ const scale = PX_PER_PT * host.zoom();
88
+ const contentWidth = Math.max(host.scroller.clientWidth, host.content.clientWidth, layout.width);
89
+ const pageLeft = contentWidth / 2 - placed.width / 2;
90
+ const [px, py] = caret.turn ? turnedToPage(caret.turn, caret.x, caret.top) : [caret.x, caret.top];
91
+ const rawLeft = pageLeft + px * scale - host.scroller.scrollLeft;
92
+ const rawTop = placed.top + py * scale - host.scroller.scrollTop;
93
+ const maxLeft = Math.max(0, host.frame.clientWidth - 2);
94
+ const maxTop = Math.max(0, host.frame.clientHeight - 2);
95
+ const left = Math.min(maxLeft, Math.max(0, rawLeft));
96
+ const top = Math.min(maxTop, Math.max(0, rawTop));
97
+ const height = Math.max(1, Math.round(caret.height * scale));
98
+ textarea.style.left = `${left}px`;
99
+ textarea.style.top = `${top}px`;
100
+ textarea.style.height = `${height}px`;
101
+ textarea.style.fontSize = `${Math.max(10, Math.round(height * 0.8))}px`;
102
+ };
103
+ const onFocus = () => {
104
+ focused = true;
105
+ host.onFocusChange(true);
106
+ };
107
+ const onBlur = () => {
108
+ focused = false;
109
+ host.onFocusChange(false);
110
+ };
111
+ const onCompositionStart = () => {
112
+ if (destroyed)
113
+ return;
114
+ const sel = host.currentSelection();
115
+ const caret = host.currentCaret();
116
+ if (!sel || !caret)
117
+ return;
118
+ setMode(startComposition(host.currentRevision(), sel.range, caret));
119
+ };
120
+ const onCompositionUpdate = (event) => {
121
+ if (destroyed || mode.kind !== 'composing')
122
+ return;
123
+ setMode(updateComposition(mode, event.data ?? textarea.value));
124
+ };
125
+ const onCompositionEnd = (event) => {
126
+ if (destroyed || mode.kind !== 'composing')
127
+ return;
128
+ const committedText = event.data ?? textarea.value;
129
+ textarea.value = '';
130
+ const result = finishComposition(mode, committedText);
131
+ setMode(result.mode);
132
+ if (result.commit) {
133
+ const { base, range, text } = result.commit;
134
+ void enqueue(async () => {
135
+ const latestSel = host.currentSelection();
136
+ const latestRev = host.currentRevision();
137
+ if (latestRev === base) {
138
+ return host.applyRangeText(base, range, text);
139
+ }
140
+ if (latestSel) {
141
+ return host.applyRangeText(latestRev, latestSel.range, text);
142
+ }
143
+ return null;
144
+ });
145
+ }
146
+ };
147
+ const onBeforeInput = (event) => {
148
+ if (destroyed || event.isComposing || mode.kind !== 'idle')
149
+ return;
150
+ if ((event.inputType === 'insertText' || event.inputType === 'insertReplacementText') && event.data) {
151
+ event.preventDefault();
152
+ textarea.value = '';
153
+ const data = event.data;
154
+ void enqueue(() => host.replaceSelection(data));
155
+ }
156
+ else if (event.inputType === 'insertParagraph') {
157
+ event.preventDefault();
158
+ textarea.value = '';
159
+ void enqueue(() => host.splitBlock());
160
+ }
161
+ };
162
+ const onInput = (event) => {
163
+ if (destroyed)
164
+ return;
165
+ const inputEvent = event;
166
+ if (inputEvent.isComposing || mode.kind !== 'idle')
167
+ return;
168
+ if (inputEvent.inputType === 'insertFromComposition' ||
169
+ inputEvent.inputType === 'deleteCompositionText') {
170
+ textarea.value = '';
171
+ return;
172
+ }
173
+ const value = textarea.value;
174
+ textarea.value = '';
175
+ if (value.length > 0) {
176
+ void enqueue(() => host.replaceSelection(value));
177
+ }
178
+ };
179
+ const onCopy = (event) => {
180
+ const text = host.selectedText();
181
+ if (!text || !event.clipboardData)
182
+ return;
183
+ event.clipboardData.setData('text/plain', text);
184
+ event.preventDefault();
185
+ };
186
+ const onCut = (event) => {
187
+ const text = host.selectedText();
188
+ if (!text || !event.clipboardData)
189
+ return;
190
+ event.clipboardData.setData('text/plain', text);
191
+ event.preventDefault();
192
+ void enqueue(() => host.replaceSelection(''));
193
+ };
194
+ const onPaste = (event) => {
195
+ if (destroyed || mode.kind !== 'idle')
196
+ return;
197
+ const text = event.clipboardData?.getData('text/plain');
198
+ if (text === undefined)
199
+ return;
200
+ event.preventDefault();
201
+ if (text.length > 0) {
202
+ void enqueue(() => host.replaceSelection(text));
203
+ }
204
+ };
205
+ const onKeyDown = (event) => {
206
+ if (destroyed)
207
+ return;
208
+ if (event.isComposing || event.key === 'Process' || event.keyCode === 229 || mode.kind !== 'idle') {
209
+ return;
210
+ }
211
+ const mod = (event.ctrlKey || event.metaKey) && !event.altKey;
212
+ const key = event.key;
213
+ const lower = key.toLowerCase();
214
+ if (mod && lower === 'f') {
215
+ event.preventDefault();
216
+ host.openFind();
217
+ return;
218
+ }
219
+ if (mod && lower === 'a') {
220
+ event.preventDefault();
221
+ host.selectAllInFlow();
222
+ return;
223
+ }
224
+ if (mod && lower === 'b' && !event.shiftKey) {
225
+ event.preventDefault();
226
+ void enqueue(() => host.toggleMark('bold'));
227
+ return;
228
+ }
229
+ if (mod && lower === 'i' && !event.shiftKey) {
230
+ event.preventDefault();
231
+ void enqueue(() => host.toggleMark('italic'));
232
+ return;
233
+ }
234
+ if (mod && lower === 'u' && !event.shiftKey) {
235
+ event.preventDefault();
236
+ void enqueue(() => host.toggleMark('underline'));
237
+ return;
238
+ }
239
+ if (mod && lower === 'x' && event.shiftKey) {
240
+ event.preventDefault();
241
+ void enqueue(() => host.toggleMark('strike'));
242
+ return;
243
+ }
244
+ if (mod && lower === 'z') {
245
+ event.preventDefault();
246
+ void enqueue(() => (event.shiftKey ? host.redo() : host.undo()));
247
+ return;
248
+ }
249
+ if (mod && lower === 'y' && !event.shiftKey) {
250
+ event.preventDefault();
251
+ void enqueue(() => host.redo());
252
+ return;
253
+ }
254
+ if (key === 'Escape') {
255
+ if (host.isFindOpen()) {
256
+ event.preventDefault();
257
+ host.closeFind();
258
+ return;
259
+ }
260
+ const sel = host.currentSelection();
261
+ if (sel && sel.range.start.offset !== sel.range.end.offset) {
262
+ event.preventDefault();
263
+ host.collapseSelection();
264
+ return;
265
+ }
266
+ if (host.hasReadOnlyTarget()) {
267
+ event.preventDefault();
268
+ host.clearReadOnlyTarget();
269
+ }
270
+ return;
271
+ }
272
+ if (key === 'Backspace' && !mod) {
273
+ event.preventDefault();
274
+ void enqueue(() => host.deleteBackward());
275
+ return;
276
+ }
277
+ if (key === 'Delete' && !mod) {
278
+ event.preventDefault();
279
+ void enqueue(() => host.deleteForward());
280
+ return;
281
+ }
282
+ if (key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
283
+ event.preventDefault();
284
+ void enqueue(() => host.splitBlock());
285
+ return;
286
+ }
287
+ if (key === 'ArrowLeft' || key === 'ArrowRight') {
288
+ event.preventDefault();
289
+ const word = (event.altKey || event.ctrlKey) && !event.metaKey;
290
+ const dir = key === 'ArrowLeft' ? 'backward' : 'forward';
291
+ const extend = event.shiftKey;
292
+ void enqueue(() => host.moveHorizontal(dir, word ? 'word' : 'character', extend));
293
+ return;
294
+ }
295
+ if (key === 'ArrowUp' || key === 'ArrowDown') {
296
+ event.preventDefault();
297
+ const dir = key === 'ArrowUp' ? 'up' : 'down';
298
+ const extend = event.shiftKey;
299
+ void enqueue(() => host.moveVertical(dir, extend));
300
+ return;
301
+ }
302
+ if ((key === 'Home' || key === 'End') && !mod) {
303
+ event.preventDefault();
304
+ const edge = key === 'Home' ? 'start' : 'end';
305
+ const extend = event.shiftKey;
306
+ void enqueue(() => host.moveLineBoundary(edge, extend));
307
+ }
308
+ };
309
+ textarea.addEventListener('focus', onFocus);
310
+ textarea.addEventListener('blur', onBlur);
311
+ textarea.addEventListener('compositionstart', onCompositionStart);
312
+ textarea.addEventListener('compositionupdate', onCompositionUpdate);
313
+ textarea.addEventListener('compositionend', onCompositionEnd);
314
+ textarea.addEventListener('beforeinput', onBeforeInput);
315
+ textarea.addEventListener('input', onInput);
316
+ textarea.addEventListener('copy', onCopy);
317
+ textarea.addEventListener('cut', onCut);
318
+ textarea.addEventListener('paste', onPaste);
319
+ textarea.addEventListener('keydown', onKeyDown);
320
+ return {
321
+ get mode() {
322
+ return mode;
323
+ },
324
+ get focused() {
325
+ return focused;
326
+ },
327
+ focus() {
328
+ if (!destroyed)
329
+ textarea.focus({ preventScroll: true });
330
+ },
331
+ position,
332
+ enqueue,
333
+ destroy() {
334
+ if (destroyed)
335
+ return;
336
+ destroyed = true;
337
+ mode = { kind: 'idle' };
338
+ textarea.removeEventListener('focus', onFocus);
339
+ textarea.removeEventListener('blur', onBlur);
340
+ textarea.removeEventListener('compositionstart', onCompositionStart);
341
+ textarea.removeEventListener('compositionupdate', onCompositionUpdate);
342
+ textarea.removeEventListener('compositionend', onCompositionEnd);
343
+ textarea.removeEventListener('beforeinput', onBeforeInput);
344
+ textarea.removeEventListener('input', onInput);
345
+ textarea.removeEventListener('copy', onCopy);
346
+ textarea.removeEventListener('cut', onCut);
347
+ textarea.removeEventListener('paste', onPaste);
348
+ textarea.removeEventListener('keydown', onKeyDown);
349
+ textarea.remove();
350
+ },
351
+ };
352
+ }
@@ -25,3 +25,7 @@ export * from './viewport.js';
25
25
  export * from './selection.js';
26
26
  export * from './find.js';
27
27
  export type { FindBar } from './find-bar.js';
28
+ export * from './edit-selection.js';
29
+ export * from './editor.js';
30
+ export type { InputMode } from './ime.js';
31
+ export { type EditorWorkspace, type EditorWorkspaceOptions, formatAskAssistantLabel, mountEditorWorkspace, type PickedImage, } from './workspace.js';
@@ -272,3 +272,6 @@ export * from './viewport.js';
272
272
  export * from './selection.js';
273
273
  // The find bar (#451): the model is pure too (find.ts).
274
274
  export * from './find.js';
275
+ export * from './edit-selection.js';
276
+ export * from './editor.js';
277
+ export { formatAskAssistantLabel, mountEditorWorkspace, } from './workspace.js';
@@ -24,4 +24,5 @@ export interface SelectionLayer {
24
24
  paint(index: number, overlay: HTMLCanvasElement, pixelsPerPoint: number): void;
25
25
  destroy(): void;
26
26
  }
27
+ export declare function paintHighlightBoxes(ctx: CanvasRenderingContext2D, boxes: HighlightBox[], color: string): void;
27
28
  export declare function createSelectionLayer(host: SelectionHost): SelectionLayer;
@@ -67,7 +67,7 @@ function rounded(ctx, points, radius) {
67
67
  }
68
68
  ctx.closePath();
69
69
  }
70
- function paintBoxes(ctx, boxes, color) {
70
+ export function paintHighlightBoxes(ctx, boxes, color) {
71
71
  if (boxes.length === 0)
72
72
  return;
73
73
  ctx.beginPath();
@@ -250,11 +250,11 @@ export function createSelectionLayer(host) {
250
250
  ctx.clearRect(0, 0, overlay.width, overlay.height);
251
251
  ctx.setTransform(pixelsPerPoint, 0, 0, pixelsPerPoint, 0, 0);
252
252
  const marks = host.marks(index);
253
- paintBoxes(ctx, marks.matches, MATCH);
254
- paintBoxes(ctx, marks.current, CURRENT_MATCH);
253
+ paintHighlightBoxes(ctx, marks.matches, MATCH);
254
+ paintHighlightBoxes(ctx, marks.current, CURRENT_MATCH);
255
255
  const selection = selector.current;
256
256
  if (selection)
257
- paintBoxes(ctx, selector.index.highlights(selection, index), HIGHLIGHT);
257
+ paintHighlightBoxes(ctx, selector.index.highlights(selection, index), HIGHLIGHT);
258
258
  },
259
259
  destroy() {
260
260
  if (autoscroll)
@@ -0,0 +1 @@
1
+ export declare function ensureWorkspaceStyles(): void;