@twip/web 0.0.0-canary-f16fa12

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.
Files changed (82) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +35 -0
  3. package/dist/THIRD-PARTY-NOTICES.md +5457 -0
  4. package/dist/commands.d.ts +637 -0
  5. package/dist/commands.js +188 -0
  6. package/dist/editor.d.ts +218 -0
  7. package/dist/editor.js +460 -0
  8. package/dist/errors.d.ts +21 -0
  9. package/dist/errors.js +26 -0
  10. package/dist/events.d.ts +126 -0
  11. package/dist/events.js +1 -0
  12. package/dist/fonts/google.d.ts +3 -0
  13. package/dist/fonts/google.js +39 -0
  14. package/dist/fonts/library.d.ts +65 -0
  15. package/dist/fonts/library.js +130 -0
  16. package/dist/host/a11y.d.ts +3 -0
  17. package/dist/host/a11y.js +62 -0
  18. package/dist/host/chrome.d.ts +28 -0
  19. package/dist/host/chrome.js +144 -0
  20. package/dist/host/clipboard.d.ts +16 -0
  21. package/dist/host/clipboard.js +128 -0
  22. package/dist/host/cursors.d.ts +15 -0
  23. package/dist/host/cursors.js +5 -0
  24. package/dist/host/dom.d.ts +45 -0
  25. package/dist/host/dom.js +122 -0
  26. package/dist/host/frames.d.ts +14 -0
  27. package/dist/host/frames.js +87 -0
  28. package/dist/host/gpu.d.ts +58 -0
  29. package/dist/host/gpu.js +79 -0
  30. package/dist/host/ime.d.ts +3 -0
  31. package/dist/host/ime.js +11 -0
  32. package/dist/host/input.d.ts +12 -0
  33. package/dist/host/input.js +86 -0
  34. package/dist/host/keys.d.ts +16 -0
  35. package/dist/host/keys.js +27 -0
  36. package/dist/host/mount.d.ts +11 -0
  37. package/dist/host/mount.js +72 -0
  38. package/dist/host/pointer.d.ts +4 -0
  39. package/dist/host/pointer.js +196 -0
  40. package/dist/host/scroll.d.ts +14 -0
  41. package/dist/host/scroll.js +95 -0
  42. package/dist/index.d.ts +27 -0
  43. package/dist/index.js +10 -0
  44. package/dist/review.d.ts +156 -0
  45. package/dist/review.js +38 -0
  46. package/dist/services/accessibility.d.ts +52 -0
  47. package/dist/services/accessibility.js +71 -0
  48. package/dist/services/dialogs.d.ts +181 -0
  49. package/dist/services/dialogs.js +18 -0
  50. package/dist/services/elements.d.ts +146 -0
  51. package/dist/services/elements.js +18 -0
  52. package/dist/services/find.d.ts +50 -0
  53. package/dist/services/find.js +12 -0
  54. package/dist/services/menus.d.ts +82 -0
  55. package/dist/services/menus.js +8 -0
  56. package/dist/services/navigation.d.ts +58 -0
  57. package/dist/services/navigation.js +12 -0
  58. package/dist/services/ruler.d.ts +95 -0
  59. package/dist/services/ruler.js +16 -0
  60. package/dist/services/spelling.d.ts +34 -0
  61. package/dist/services/spelling.js +14 -0
  62. package/dist/services/view.d.ts +70 -0
  63. package/dist/services/view.js +19 -0
  64. package/dist/state.d.ts +483 -0
  65. package/dist/state.js +1 -0
  66. package/dist/transport/direct.d.ts +31 -0
  67. package/dist/transport/direct.js +405 -0
  68. package/dist/transport/module.d.ts +51 -0
  69. package/dist/transport/module.js +17 -0
  70. package/dist/transport/protocol.d.ts +265 -0
  71. package/dist/transport/protocol.js +132 -0
  72. package/dist/transport/worker.d.ts +54 -0
  73. package/dist/transport/worker.js +203 -0
  74. package/dist/twip-wasm.js +2 -0
  75. package/dist/twip_wasm.wasm +0 -0
  76. package/dist/types.d.ts +134 -0
  77. package/dist/types.js +16 -0
  78. package/dist/worker/entry.d.ts +1 -0
  79. package/dist/worker/entry.js +53 -0
  80. package/dist/worker.d.ts +1 -0
  81. package/dist/worker.js +2 -0
  82. package/package.json +30 -0
@@ -0,0 +1,4 @@
1
+ import type { Session } from '../editor.js';
2
+ import type { Dom, Ledger } from './dom.js';
3
+ import type { Scroll } from './scroll.js';
4
+ export declare function startPointer(s: Session, dom: Dom, ledger: Ledger, scroll: Scroll, report: (e: unknown) => void, focus: () => void, ignore: (target: EventTarget | null) => boolean): void;
@@ -0,0 +1,196 @@
1
+ import { Kind } from '../transport/protocol.js';
2
+ import { CELL_SELECT, COLUMN_SELECT, ROW_SELECT } from './cursors.js';
3
+ import { modsOf } from './keys.js';
4
+ const PointerKind = { down: 0, move: 1, up: 2, cancel: 3, leave: 4 };
5
+ /** DOM `PointerEvent.button` to the engine's: primary 0, secondary 1, middle 2. */
6
+ const buttons = { 0: 0, 2: 1, 1: 2 };
7
+ /** A `.cur` cursor, over the browser's own `fallback` where it can't show one. */
8
+ const cur = (c, fallback) => `url("${c.url}") ${c.x} ${c.y}, ${fallback}`;
9
+ /** The browser's cursors, and the selection arrows it has none of (`cursors.ts`, the WinForms host's `.cur` files). */
10
+ const cursors = {
11
+ default: 'default',
12
+ text: 'text',
13
+ pointer: 'pointer',
14
+ move: 'move',
15
+ resizeHorizontal: 'col-resize',
16
+ resizeVertical: 'row-resize',
17
+ resizeNorthWestSouthEast: 'nwse-resize',
18
+ resizeNorthEastSouthWest: 'nesw-resize',
19
+ crosshair: 'crosshair',
20
+ notAllowed: 'not-allowed',
21
+ copy: 'copy',
22
+ cellSelect: cur(CELL_SELECT, 'cell'),
23
+ columnSelect: cur(COLUMN_SELECT, 'default'),
24
+ rowSelect: cur(ROW_SELECT, 'default'),
25
+ };
26
+ /** Presses closer than this in time (ms) and distance (CSS px) count as one multi-click. */
27
+ const CLICK_TIME = 500;
28
+ const CLICK_SLOP = 4;
29
+ export function startPointer(s, dom, ledger, scroll, report, focus, ignore) {
30
+ const { scroller } = dom;
31
+ const point = (e) => {
32
+ const r = dom.canvas.getBoundingClientRect();
33
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
34
+ };
35
+ const send = (kind, e, button, mods) => s.input({ type: 'pointer', kind, ...point(e), button, clicks, mods }).catch(report);
36
+ let clicks = 0;
37
+ let last = { time: -Infinity, x: 0, y: 0, button: -1 };
38
+ /** The pointer of the press being dragged. */
39
+ let pressed;
40
+ /** Where a mouse or pen last moved over the document, until it leaves. */
41
+ let over;
42
+ /** Touches down, for pinch and pan. */
43
+ const touches = new Map();
44
+ let pinch;
45
+ let stopAutoscroll;
46
+ const twoTouches = () => {
47
+ const [a, b] = [...touches.values()];
48
+ return { distance: Math.hypot(a.x - b.x, a.y - b.y), x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
49
+ };
50
+ ledger.listen(scroller, 'pointerdown', (e) => {
51
+ if (ignore(e.target))
52
+ return;
53
+ const at = point(e);
54
+ // A press on a scrollbar scrolls.
55
+ if (at.x >= scroller.clientWidth || at.y >= scroller.clientHeight)
56
+ return;
57
+ const button = buttons[e.button];
58
+ if (button === undefined)
59
+ return;
60
+ if (e.pointerType === 'touch') {
61
+ touches.set(e.pointerId, { x: e.clientX, y: e.clientY });
62
+ if (touches.size === 2) {
63
+ // A second finger: the first one's press is withdrawn, and the two pinch and pan.
64
+ if (pressed)
65
+ send(PointerKind.cancel, pressed.at, 0, 0);
66
+ pressed = undefined;
67
+ pinch = twoTouches();
68
+ return;
69
+ }
70
+ if (touches.size > 2)
71
+ return;
72
+ }
73
+ // A press takes the focus (the browser would give it to no element: the canvas takes none).
74
+ if (button !== 2) {
75
+ e.preventDefault();
76
+ focus();
77
+ }
78
+ const near = Math.hypot(e.clientX - last.x, e.clientY - last.y) <= CLICK_SLOP;
79
+ clicks = button === last.button && near && e.timeStamp - last.time <= CLICK_TIME ? clicks + 1 : 1;
80
+ last = { time: e.timeStamp, x: e.clientX, y: e.clientY, button };
81
+ if (button === 0) {
82
+ pressed = { id: e.pointerId, at: e };
83
+ scroller.setPointerCapture(e.pointerId);
84
+ }
85
+ send(PointerKind.down, e, button, modsOf(e));
86
+ });
87
+ ledger.listen(scroller, 'pointermove', (e) => {
88
+ if (touches.has(e.pointerId)) {
89
+ touches.set(e.pointerId, { x: e.clientX, y: e.clientY });
90
+ if (pinch && touches.size === 2) {
91
+ const now = twoTouches();
92
+ if (pinch.distance > 0 && Math.abs(now.distance / pinch.distance - 1) > 0.02) {
93
+ scroll.zoomBy(now.distance / pinch.distance);
94
+ pinch.distance = now.distance;
95
+ }
96
+ scroller.scrollBy(pinch.x - now.x, pinch.y - now.y);
97
+ pinch = { ...pinch, x: now.x, y: now.y };
98
+ return;
99
+ }
100
+ if (pinch)
101
+ return;
102
+ }
103
+ if (pressed && pressed.id !== e.pointerId)
104
+ return;
105
+ if (!pressed && ignore(e.target))
106
+ return;
107
+ if (pressed) {
108
+ pressed.at = e;
109
+ autoscroll(e);
110
+ }
111
+ send(PointerKind.move, e, 0, modsOf(e));
112
+ });
113
+ const end = (kind) => (e) => {
114
+ touches.delete(e.pointerId);
115
+ if (pinch) {
116
+ if (touches.size === 0)
117
+ pinch = undefined;
118
+ return;
119
+ }
120
+ if (pressed?.id === e.pointerId) {
121
+ pressed = undefined;
122
+ stopAutoscroll?.();
123
+ if (scroller.hasPointerCapture(e.pointerId))
124
+ scroller.releasePointerCapture(e.pointerId);
125
+ }
126
+ else if (ignore(e.target))
127
+ return;
128
+ const button = buttons[e.button] ?? 0;
129
+ send(kind, e, button, modsOf(e));
130
+ };
131
+ ledger.listen(scroller, 'pointerup', end(PointerKind.up));
132
+ ledger.listen(scroller, 'pointercancel', end(PointerKind.cancel));
133
+ ledger.listen(scroller, 'pointerleave', (e) => {
134
+ if (!pressed && e.pointerType !== 'touch')
135
+ send(PointerKind.leave, e, 0, modsOf(e));
136
+ over = undefined;
137
+ });
138
+ // Ctrl, Shift or Meta pressed or released with the pointer still: the same move again with the new modifiers, so
139
+ // the shape follows what a press would now do (the hand over a link with Ctrl, copy while dragging).
140
+ ledger.listen(scroller, 'pointermove', (e) => {
141
+ if (e.pointerType !== 'touch')
142
+ over = { clientX: e.clientX, clientY: e.clientY };
143
+ });
144
+ const modifier = (event) => {
145
+ const e = event;
146
+ if (!over || e.repeat || !['Control', 'Shift', 'Meta'].includes(e.key))
147
+ return;
148
+ send(PointerKind.move, pressed?.at ?? over, 0, modsOf(e));
149
+ };
150
+ const doc = scroller.ownerDocument;
151
+ ledger.listen(doc, 'keydown', modifier, { capture: true });
152
+ ledger.listen(doc, 'keyup', modifier, { capture: true });
153
+ // Autoscroll: a drag past the viewport's edge scrolls each frame, faster further out, and extends the drag.
154
+ const autoscroll = (e) => {
155
+ const step = () => {
156
+ if (!pressed)
157
+ return;
158
+ const at = point(pressed.at);
159
+ const out = (v, size) => (v < 0 ? v : v > size ? v - size : 0);
160
+ const dx = out(at.x, scroller.clientWidth);
161
+ const dy = out(at.y, scroller.clientHeight);
162
+ if (!dx && !dy) {
163
+ stopAutoscroll = undefined;
164
+ return;
165
+ }
166
+ const speed = (d) => Math.sign(d) * Math.min(40, Math.ceil(Math.abs(d) / 4));
167
+ scroller.scrollBy(speed(dx), speed(dy));
168
+ send(PointerKind.move, pressed.at, 0, modsOf(pressed.at));
169
+ stopAutoscroll = ledger.frame(step);
170
+ };
171
+ const at = point(e);
172
+ const inside = at.x >= 0 && at.y >= 0 && at.x <= scroller.clientWidth && at.y <= scroller.clientHeight;
173
+ if (!inside && !stopAutoscroll)
174
+ stopAutoscroll = ledger.frame(step);
175
+ };
176
+ // The engine's requests: the cursor, and capture for its own drags.
177
+ const onMessage = (m) => {
178
+ if (m.kind !== Kind.HostRequest)
179
+ return;
180
+ const r = m.payload;
181
+ if (r.type === 'cursor')
182
+ scroller.style.cursor = cursors[r.cursor] ?? 'default';
183
+ else if (r.type === 'capture' && pressed && !scroller.hasPointerCapture(pressed.id))
184
+ scroller.setPointerCapture(pressed.id);
185
+ else if (r.type === 'release' && pressed && scroller.hasPointerCapture(pressed.id))
186
+ scroller.releasePointerCapture(pressed.id);
187
+ };
188
+ s.hostListeners.add(onMessage);
189
+ s.disposers.push(() => s.hostListeners.delete(onMessage));
190
+ // The engine opens its menu on a secondary press (and the menu keys): the browser's stays shut while the
191
+ // application listens for it.
192
+ ledger.listen(scroller, 'contextmenu', (e) => {
193
+ if (!ignore(e.target) && s.listening('contextMenu'))
194
+ e.preventDefault();
195
+ });
196
+ }
@@ -0,0 +1,14 @@
1
+ import type { Session } from '../editor.js';
2
+ import { type FrameInfo } from '../transport/protocol.js';
3
+ import type { Dom, Ledger } from './dom.js';
4
+ import type { Frames } from './frames.js';
5
+ /** The scrolling of a mounted editor. */
6
+ export interface Scroll {
7
+ /** Where content x 0 is in the scroller's scrolled box: the centring offset. */
8
+ readonly offset: number;
9
+ /** Sizes the content after a frame. */
10
+ layout(info: FrameInfo): void;
11
+ /** Zooms by a factor of the current zoom. */
12
+ zoomBy(factor: number): void;
13
+ }
14
+ export declare function startScroll(s: Session, dom: Dom, ledger: Ledger, frames: Frames, report: (e: unknown) => void): Scroll;
@@ -0,0 +1,95 @@
1
+ import { Kind } from '../transport/protocol.js';
2
+ export function startScroll(s, dom, ledger, frames, report) {
3
+ const { scroller, content } = dom;
4
+ let offset = 0;
5
+ let sent = { x: NaN, y: NaN };
6
+ let size = { width: 0, height: 0 };
7
+ // A `scroll` request (a zoom), applied once the next frame has sized the content for it.
8
+ let pending;
9
+ const send = () => {
10
+ const at = { x: scroller.scrollLeft - offset, y: scroller.scrollTop };
11
+ if (at.x === sent.x && at.y === sent.y)
12
+ return;
13
+ sent = at;
14
+ s.input({ type: 'scroll', ...at }).catch(report);
15
+ };
16
+ const layout = (info) => {
17
+ if (info.contentWidth !== size.width || info.contentHeight !== size.height) {
18
+ size = { width: info.contentWidth, height: info.contentHeight };
19
+ content.style.width = `${size.width}px`;
20
+ content.style.height = `${size.height}px`;
21
+ }
22
+ const centred = Math.max(0, Math.floor((scroller.clientWidth - size.width) / 2));
23
+ if (centred !== offset) {
24
+ offset = centred;
25
+ content.style.marginLeft = `${offset}px`;
26
+ }
27
+ if (pending) {
28
+ // The engine drew this frame from there already: report only where the scroller couldn't go.
29
+ scroller.scrollLeft = pending.x + offset;
30
+ scroller.scrollTop = pending.y;
31
+ sent = pending;
32
+ pending = undefined;
33
+ }
34
+ send();
35
+ };
36
+ ledger.listen(scroller, 'scroll', send, { passive: true });
37
+ // `scroll_to`: the least scrolling that shows the rectangle (content px).
38
+ const scrollTo = (r) => {
39
+ const left = r.x + offset;
40
+ const { clientWidth: w, clientHeight: h } = scroller;
41
+ if (r.y < scroller.scrollTop)
42
+ scroller.scrollTop = r.y;
43
+ else if (r.y + r.height > scroller.scrollTop + h)
44
+ scroller.scrollTop = r.y + r.height - h;
45
+ if (left < scroller.scrollLeft)
46
+ scroller.scrollLeft = left;
47
+ else if (left + r.width > scroller.scrollLeft + w)
48
+ scroller.scrollLeft = left + r.width - w;
49
+ send();
50
+ };
51
+ const onMessage = (m) => {
52
+ const r = m.payload;
53
+ if (m.kind !== Kind.HostRequest)
54
+ return;
55
+ if (r.type === 'scroll_to')
56
+ scrollTo(r);
57
+ else if (r.type === 'scroll')
58
+ pending = { x: r.x, y: r.y };
59
+ };
60
+ s.hostListeners.add(onMessage);
61
+ s.disposers.push(() => s.hostListeners.delete(onMessage));
62
+ // Zoom: factors gather while a zoom is running, then apply at once.
63
+ let factor = 1;
64
+ let zooming = false;
65
+ const zoomBy = (f) => {
66
+ factor *= f;
67
+ if (zooming)
68
+ return;
69
+ zooming = true;
70
+ const zoom = Math.min(500, Math.max(10, Math.round(s.state.view.zoom * factor)));
71
+ factor = 1;
72
+ s.run({ type: 'setZoom', value: zoom })
73
+ .catch(report)
74
+ .finally(() => {
75
+ zooming = false;
76
+ frames.request();
77
+ if (factor !== 1)
78
+ zoomBy(1);
79
+ });
80
+ };
81
+ ledger.listen(scroller, 'wheel', (e) => {
82
+ if (!e.ctrlKey)
83
+ return;
84
+ e.preventDefault();
85
+ // 10% a wheel notch (100 px, or 3 lines); a pinch sends small deltas.
86
+ zoomBy(1.1 ** (-e.deltaY / (e.deltaMode === 0 ? 100 : 3)));
87
+ }, { passive: false });
88
+ return {
89
+ get offset() {
90
+ return offset;
91
+ },
92
+ layout,
93
+ zoomBy,
94
+ };
95
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The Twip DOCX editor for the browser and Node: a managed, framework-independent editor. Mount it in a sized
3
+ * container with {@link Editor.mount}, or create a session with no DOM with {@link HeadlessEditor.create}.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ export { Editor, HeadlessEditor } from './editor.js';
8
+ export type { ExportResult, MountOptions, SessionOptions } from './editor.js';
9
+ export type { CommandEntry, CommandId, CommandInfo, Commands, FieldTargets, NoArgumentCommandId, Op, OpArgs, PlaceholderLook, } from './commands.js';
10
+ export type { CharacterFormatting, CommandState, CommandValues, EditorState, ElementContext, HighlightColor, LineSpacing, MarkupView, Mixed, ObjectSelection, ObjectWrap, PageFormatting, ParagraphFormatting, ReviewState, RowHeight, SelectionSummary, TableSelection, TableStyleOptions, TableWidth, Tri, UnderlineStyle, ViewState, } from './state.js';
11
+ export type { HostCommand, Tooltip, TransactionRecord, TwipEvents } from './events.js';
12
+ export { EditorError } from './errors.js';
13
+ export type { ErrorCode } from './errors.js';
14
+ export { FontLibrary } from './fonts/library.js';
15
+ export type { FaceInfo, FontRequest, SubstitutionRule } from './fonts/library.js';
16
+ export type { BalloonPlacement, Comment, CommentAnchor, CommentThread, MountedReview, Person, Review, RevisionInfo, RichRun, RichText, } from './review.js';
17
+ export type { ContextMenu, ContextMenuEvent, MenuAction, MenuItem, MenuTarget, Menus } from './services/menus.js';
18
+ export type { AccessibilityDialogSession, DialogKind, DialogSession, DialogSessionOf, DialogValues, Dialogs, FieldInfo, } from './services/dialogs.js';
19
+ export type { Find, FindMatch, FindQuery, FindResult } from './services/find.js';
20
+ export type { Dictionary, Spelling } from './services/spelling.js';
21
+ export type { Block, ControlKind, ControlOptions, DataBinding, DataParts, ElementDetail, ElementInfo, Elements, MountedElements, PlaceholderInfo, } from './services/elements.js';
22
+ export type { Accessibility, AccessibleNode, Shortcuts } from './services/accessibility.js';
23
+ export type { Heading, Navigation, StyleInfo, Styles } from './services/navigation.js';
24
+ export type { Ruler, RulerModel, RulerModelSource, RulerUnit, RulerView, TabAlignment, TabLeader, TabSelector, } from './services/ruler.js';
25
+ export type { GestureAction, GestureBinding, GestureEvent, GestureId, Gestures, View } from './services/view.js';
26
+ export { cm, inches, mm, pt } from './types.js';
27
+ export type { Author, ClientPoint, ClientRect, Color, CommentId, ContentPoint, ElementId, FrameStats, HeadingId, OverlayColors, MatchId, Points, RevisionId, StyleId, Warning, WordCount, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The Twip DOCX editor for the browser and Node: a managed, framework-independent editor. Mount it in a sized
3
+ * container with {@link Editor.mount}, or create a session with no DOM with {@link HeadlessEditor.create}.
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ export { Editor, HeadlessEditor } from './editor.js';
8
+ export { EditorError } from './errors.js';
9
+ export { FontLibrary } from './fonts/library.js';
10
+ export { cm, inches, mm, pt } from './types.js';
@@ -0,0 +1,156 @@
1
+ import type { Session } from './editor.js';
2
+ import type { SelectionSummary } from './state.js';
3
+ import type { Color, CommentId, ContentPoint, RevisionId } from './types.js';
4
+ /** Reading tracked changes and comments. Changing them is done with the review commands. */
5
+ export interface Review {
6
+ /** Every tracked revision, in document order. */
7
+ revisions(): Promise<RevisionInfo[]>;
8
+ /** Every comment thread, in document order. */
9
+ comments(): Promise<CommentThread[]>;
10
+ /** Everyone who made a revision or a comment. */
11
+ people(): Promise<Person[]>;
12
+ /** Card tops that don't overlap: `heights[i]` is the card height for `anchors[i]`. Pure and synchronous. */
13
+ stackBalloons(anchors: readonly CommentAnchor[], heights: readonly number[], options?: {
14
+ gap?: number;
15
+ }): number[];
16
+ }
17
+ /** {@link Review} on a mounted editor: adds the markup pane and balloons. */
18
+ export interface MountedReview extends Review {
19
+ /** Room beside the pages for balloons, CSS px. 0 (the default) removes it. */
20
+ setMarkupPaneWidth(width: number): Promise<void>;
21
+ /**
22
+ * An SDK-owned element inside the scrolling content, in ContentPoint coordinates. Put cards and leader lines here.
23
+ * It has `pointer-events: none`: a card placed here needs `pointer-events: auto` to be clickable.
24
+ */
25
+ readonly balloonLayer: HTMLElement;
26
+ /** Places `card` beside its thread, stacked with the others, and keeps it there through scroll, zoom and edits. */
27
+ attachBalloon(thread: CommentId, card: HTMLElement): () => void;
28
+ /** The current anchors, also delivered by the `commentAnchors` event. */
29
+ anchors(): CommentAnchor[];
30
+ }
31
+ /** A tracked revision. */
32
+ export interface RevisionInfo {
33
+ /** The revision. */
34
+ id: RevisionId;
35
+ /** What changed. */
36
+ kind: 'insertion' | 'deletion' | 'moveFrom' | 'moveTo' | 'formatting' | 'paragraphProperties' | 'sectionProperties' | 'tableProperties' | 'rowInsertion' | 'rowDeletion' | 'cellInsertion' | 'cellDeletion' | 'cellMerge';
37
+ /** Who made it. */
38
+ author: Person;
39
+ /** When, ISO 8601 UTC, if the document says. */
40
+ time: string | null;
41
+ /** The inserted, deleted or moved text, when there is some. */
42
+ text: string;
43
+ /** For example `'Formatted: Bold, Italic'` for formatting changes. */
44
+ description: string;
45
+ /** The page it starts on. */
46
+ page: number;
47
+ /** The story it is in. */
48
+ story: SelectionSummary['story'];
49
+ /** A move's other half. */
50
+ pair?: RevisionId;
51
+ }
52
+ /** A comment and its replies. */
53
+ export interface CommentThread {
54
+ /** The first comment's id. */
55
+ id: CommentId;
56
+ /** Resolved. */
57
+ done: boolean;
58
+ /** The commented text. */
59
+ anchorText: string;
60
+ /** The page it starts on. */
61
+ page: number;
62
+ /** The first comment, then its replies. */
63
+ comments: Comment[];
64
+ }
65
+ /** One comment. */
66
+ export interface Comment {
67
+ /** The comment. */
68
+ id: CommentId;
69
+ /** Who wrote it. */
70
+ author: Person;
71
+ /** When, ISO 8601 UTC, if the document says. */
72
+ time: string | null;
73
+ /** The body. */
74
+ body: RichText;
75
+ /** The body as plain text. */
76
+ plainText: string;
77
+ }
78
+ /** Someone who made a revision or a comment. */
79
+ export interface Person {
80
+ /** The display name. */
81
+ name: string;
82
+ /** The initials. */
83
+ initials?: string;
84
+ /** The identity in a directory. */
85
+ identity?: {
86
+ provider: string;
87
+ userId: string;
88
+ };
89
+ /** The colour the editor marks their changes with. */
90
+ color: Color;
91
+ }
92
+ /** Formatted text: paragraphs of runs. */
93
+ export interface RichText {
94
+ /** The paragraphs. */
95
+ paragraphs: {
96
+ runs: RichRun[];
97
+ }[];
98
+ }
99
+ /** A run of {@link RichText}. */
100
+ export interface RichRun {
101
+ /** The text. */
102
+ text: string;
103
+ /** Bold. */
104
+ bold?: boolean;
105
+ /** Italic. */
106
+ italic?: boolean;
107
+ /** Underlined. */
108
+ underline?: boolean;
109
+ /** Struck through. */
110
+ strikethrough?: boolean;
111
+ /** A link address. */
112
+ link?: string;
113
+ }
114
+ /** Where a visible comment's range is, for placing its card. */
115
+ export interface CommentAnchor {
116
+ /** The thread. */
117
+ thread: CommentId;
118
+ /** The page. */
119
+ page: number;
120
+ /** The active thread. */
121
+ active: boolean;
122
+ /** Resolved. */
123
+ done: boolean;
124
+ /** The range's first character, at its baseline. */
125
+ start: ContentPoint;
126
+ /** The range's last character, at its baseline. */
127
+ end: ContentPoint;
128
+ /** The first line's top, for a card level with it. */
129
+ lineTop: number;
130
+ /** The first line's bottom. */
131
+ lineBottom: number;
132
+ /** The x where a leader line leaves the page. */
133
+ pageEdge: number;
134
+ }
135
+ /** Where the SDK placed an attached balloon. */
136
+ export interface BalloonPlacement {
137
+ /** The thread. */
138
+ thread: CommentId;
139
+ /** The card's top. */
140
+ top: number;
141
+ /** The card's left. */
142
+ left: number;
143
+ /** The leader line, a polyline. */
144
+ leader: ContentPoint[];
145
+ }
146
+ /**
147
+ * Card tops that don't overlap, `gap` apart: card `i` (`heights[i]` tall) is for `anchors[i]`. The first active card
148
+ * sits level with its line; the cards before it rise above it only as far as they must, and the cards after it fall
149
+ * below it. No card goes above 0. With no active card, each card is level with its line or just below the card before
150
+ * it. The engine's `stack_balloons`, the same algorithm.
151
+ */
152
+ export declare function stackBalloons(anchors: readonly CommentAnchor[], heights: readonly number[], gap?: number): number[];
153
+ /** Review over a session. */
154
+ export declare function createReview(s: Session): Review;
155
+ /** The mounted editor's review members this layer provides: the markup pane, and the anchors last sent. */
156
+ export declare function createMountedReview(s: Session): Pick<MountedReview, 'setMarkupPaneWidth' | 'anchors'>;
package/dist/review.js ADDED
@@ -0,0 +1,38 @@
1
+ import { Method } from './transport/protocol.js';
2
+ /**
3
+ * Card tops that don't overlap, `gap` apart: card `i` (`heights[i]` tall) is for `anchors[i]`. The first active card
4
+ * sits level with its line; the cards before it rise above it only as far as they must, and the cards after it fall
5
+ * below it. No card goes above 0. With no active card, each card is level with its line or just below the card before
6
+ * it. The engine's `stack_balloons`, the same algorithm.
7
+ */
8
+ export function stackBalloons(anchors, heights, gap = 8) {
9
+ const n = Math.min(anchors.length, heights.length);
10
+ const tops = anchors.slice(0, n).map((a) => a.lineTop);
11
+ const active = Math.max(0, anchors.slice(0, n).findIndex((a) => a.active));
12
+ for (let i = active - 1; i >= 0; i--)
13
+ tops[i] = Math.min(tops[i], tops[i + 1] - gap - heights[i]);
14
+ let bottom = -Infinity;
15
+ for (let i = 0; i < n; i++) {
16
+ tops[i] = Math.max(tops[i], 0, bottom + gap);
17
+ bottom = tops[i] + heights[i];
18
+ }
19
+ return tops;
20
+ }
21
+ /** Review over a session. */
22
+ export function createReview(s) {
23
+ return {
24
+ revisions: () => s.call(Method.Revisions),
25
+ comments: () => s.call(Method.Comments),
26
+ people: () => s.call(Method.People),
27
+ stackBalloons: (anchors, heights, options) => stackBalloons(anchors, heights, options?.gap),
28
+ };
29
+ }
30
+ /** The mounted editor's review members this layer provides: the markup pane, and the anchors last sent. */
31
+ export function createMountedReview(s) {
32
+ let anchors = [];
33
+ s.on('commentAnchors', (a) => (anchors = a));
34
+ return {
35
+ setMarkupPaneWidth: (width) => s.call(Method.HostSetMarkupPaneWidth, width),
36
+ anchors: () => anchors,
37
+ };
38
+ }
@@ -0,0 +1,52 @@
1
+ import type { CommandId } from '../commands.js';
2
+ import type { Session } from '../editor.js';
3
+ import type { HostCommand } from '../events.js';
4
+ /** The document as an accessibility tree. The mounted editor manages its own accessibility DOM. */
5
+ export interface Accessibility {
6
+ /** The tree. */
7
+ tree(): Promise<AccessibleNode>;
8
+ }
9
+ /** A node of the accessibility tree. */
10
+ export interface AccessibleNode {
11
+ /** Its role. */
12
+ role: 'document' | 'page' | 'paragraph' | 'heading' | 'list' | 'listItem' | 'table' | 'row' | 'cell' | 'image' | 'link' | 'textBox' | 'note' | 'header' | 'footer';
13
+ /** Its accessible name. */
14
+ name?: string;
15
+ /** Its text. */
16
+ text?: string;
17
+ /** A heading's or list item's level. */
18
+ level?: number;
19
+ /** Review and spelling marks on it. */
20
+ marks?: ('insertion' | 'deletion' | 'comment' | 'misspelling')[];
21
+ /** Its children. */
22
+ children: AccessibleNode[];
23
+ }
24
+ /** Keyboard shortcuts: the application's own, and what a key does now. */
25
+ export interface Shortcuts {
26
+ /** Claims a key for the application: pressing it emits `shortcut`. Throws 'unavailable' on a collision, naming it. */
27
+ register(shortcut: {
28
+ id: string;
29
+ keys: string | {
30
+ windows: string;
31
+ mac: string;
32
+ };
33
+ }): () => void;
34
+ /** What a key does now: a command, an application shortcut, a host command, or nothing. */
35
+ lookup(keys: string): {
36
+ kind: 'command';
37
+ id: CommandId;
38
+ } | {
39
+ kind: 'shortcut';
40
+ id: string;
41
+ } | {
42
+ kind: 'host';
43
+ command: HostCommand;
44
+ } | null;
45
+ }
46
+ /** The accessibility tree over a session. */
47
+ export declare function createAccessibility(s: Session): Accessibility;
48
+ /**
49
+ * Shortcuts over a session. Lookups are answered here, from the catalog, the host keys and the registrations, so
50
+ * they can be synchronous; registrations also go to the engine, which presses them.
51
+ */
52
+ export declare function createShortcuts(s: Session): Shortcuts;
@@ -0,0 +1,71 @@
1
+ import { EditorError } from '../errors.js';
2
+ import { Method } from '../transport/protocol.js';
3
+ /** The accessibility tree over a session. */
4
+ export function createAccessibility(s) {
5
+ return { tree: () => s.call(Method.AccessibleTree) };
6
+ }
7
+ /** The keys the application owns (the engine's host keys), by their Windows shortcut. */
8
+ const hostKeys = {
9
+ 'Ctrl+S': 'save',
10
+ F12: 'saveAs',
11
+ 'Ctrl+P': 'print',
12
+ 'Ctrl+O': 'open',
13
+ 'Ctrl+N': 'new',
14
+ 'Ctrl+F': 'find',
15
+ 'Ctrl+H': 'replace',
16
+ 'Ctrl+G': 'goTo',
17
+ 'Ctrl+K': 'insertHyperlink',
18
+ F7: 'nextMisspelling',
19
+ 'Ctrl+Shift+F': 'focusFont',
20
+ 'Ctrl+Shift+P': 'focusFontSize',
21
+ F1: 'help',
22
+ };
23
+ /** A shortcut in one spelling: modifiers sorted, case ignored. `'Ctrl+Alt+S'`, `'⌥⌘S'`. */
24
+ function normalize(keys) {
25
+ const glyphs = /^[⌘⌥⇧⌃]*/u.exec(keys)[0];
26
+ const parts = glyphs ? [...glyphs, keys.slice(glyphs.length)] : keys.split(/\+(?=.)/);
27
+ const key = parts.pop().toLowerCase();
28
+ return [...parts.map((p) => p.toLowerCase()).sort(), key].join('+');
29
+ }
30
+ /**
31
+ * Shortcuts over a session. Lookups are answered here, from the catalog, the host keys and the registrations, so
32
+ * they can be synchronous; registrations also go to the engine, which presses them.
33
+ */
34
+ export function createShortcuts(s) {
35
+ const registered = new Map();
36
+ const lookup = (keys) => {
37
+ const key = normalize(keys);
38
+ const matches = (k) => normalize(k) === key;
39
+ for (const [id, k] of registered)
40
+ if (matches(k.windows) || matches(k.mac))
41
+ return { kind: 'shortcut', id };
42
+ for (const c of s.catalog)
43
+ if ([...c.shortcuts.windows, ...c.shortcuts.mac].some(matches))
44
+ return { kind: 'command', id: c.id };
45
+ const host = Object.entries(hostKeys).find(([k]) => matches(k));
46
+ return host ? { kind: 'host', command: host[1] } : null;
47
+ };
48
+ const report = (call) => call.catch((e) => s.emit('error', e instanceof EditorError ? e : new EditorError('internal', String(e), { cause: e })));
49
+ return {
50
+ register: ({ id, keys }) => {
51
+ const both = typeof keys === 'string' ? { windows: keys, mac: keys } : keys;
52
+ if (s.destroyed)
53
+ throw new EditorError('destroyed', 'the editor was destroyed');
54
+ for (const k of new Set([both.windows, both.mac])) {
55
+ const taken = lookup(k);
56
+ if (taken)
57
+ throw new EditorError('unavailable', `${k} is already ${taken.kind === 'host' ? `the ${taken.command} key` : `bound to ${taken.id}`}`);
58
+ }
59
+ registered.set(id, both);
60
+ report(s.call(Method.ShortcutsRegister, { id, keys }));
61
+ return () => {
62
+ if (registered.get(id) !== both)
63
+ return;
64
+ registered.delete(id);
65
+ if (!s.destroyed)
66
+ report(s.call(Method.ShortcutsUnregister, id));
67
+ };
68
+ },
69
+ lookup,
70
+ };
71
+ }