@blocksdiy/react-common 1.70.2 → 1.71.1
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/dist/comments/anchor.d.ts +96 -0
- package/dist/comments/anchor.d.ts.map +1 -0
- package/dist/comments/anchor.js +346 -0
- package/dist/comments/api.d.ts +38 -0
- package/dist/comments/api.d.ts.map +1 -0
- package/dist/comments/api.js +62 -0
- package/dist/comments/comment-agents.d.ts +28 -0
- package/dist/comments/comment-agents.d.ts.map +1 -0
- package/dist/comments/comment-agents.js +32 -0
- package/dist/comments/comment-mode.d.ts +115 -0
- package/dist/comments/comment-mode.d.ts.map +1 -0
- package/dist/comments/comment-mode.js +176 -0
- package/dist/comments/composer.d.ts +133 -0
- package/dist/comments/composer.d.ts.map +1 -0
- package/dist/comments/composer.js +273 -0
- package/dist/comments/index.d.ts +19 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +23 -0
- package/dist/comments/layout.d.ts +25 -0
- package/dist/comments/layout.d.ts.map +1 -0
- package/dist/comments/layout.js +83 -0
- package/dist/comments/mentions.d.ts +32 -0
- package/dist/comments/mentions.d.ts.map +1 -0
- package/dist/comments/mentions.js +30 -0
- package/dist/comments/menu.d.ts +33 -0
- package/dist/comments/menu.d.ts.map +1 -0
- package/dist/comments/menu.js +47 -0
- package/dist/comments/open-comment.d.ts +28 -0
- package/dist/comments/open-comment.d.ts.map +1 -0
- package/dist/comments/open-comment.js +9 -0
- package/dist/comments/page-pick.d.ts +76 -0
- package/dist/comments/page-pick.d.ts.map +1 -0
- package/dist/comments/page-pick.js +283 -0
- package/dist/comments/pick-message.d.ts +11 -0
- package/dist/comments/pick-message.d.ts.map +1 -0
- package/dist/comments/pick-message.js +33 -0
- package/dist/comments/pin-view.d.ts +107 -0
- package/dist/comments/pin-view.d.ts.map +1 -0
- package/dist/comments/pin-view.js +248 -0
- package/dist/comments/pins.d.ts +126 -0
- package/dist/comments/pins.d.ts.map +1 -0
- package/dist/comments/pins.js +218 -0
- package/dist/comments/text-pick.d.ts +69 -0
- package/dist/comments/text-pick.d.ts.map +1 -0
- package/dist/comments/text-pick.js +307 -0
- package/dist/comments/threads-store.d.ts +119 -0
- package/dist/comments/threads-store.d.ts.map +1 -0
- package/dist/comments/threads-store.js +286 -0
- package/dist/comments/types.d.ts +235 -0
- package/dist/comments/types.d.ts.map +1 -0
- package/dist/comments/types.js +109 -0
- package/dist/comments/use-comment-menu.d.ts +69 -0
- package/dist/comments/use-comment-menu.d.ts.map +1 -0
- package/dist/comments/use-comment-menu.js +140 -0
- package/dist/comments/use-held.d.ts +3 -0
- package/dist/comments/use-held.d.ts.map +1 -0
- package/dist/comments/use-held.js +16 -0
- package/dist/comments/use-thread-scroll.d.ts +23 -0
- package/dist/comments/use-thread-scroll.d.ts.map +1 -0
- package/dist/comments/use-thread-scroll.js +89 -0
- package/dist/hooks/agent-conversation-context.d.ts +12 -1
- package/dist/hooks/agent-conversation-context.d.ts.map +1 -1
- package/dist/hooks/agent-conversation-context.js +12 -5
- package/dist/hooks/agent-thread-live-update.d.ts +5 -2
- package/dist/hooks/agent-thread-live-update.d.ts.map +1 -1
- package/dist/hooks/agent-thread-live-update.js +14 -0
- package/dist/hooks/use-agent-conversation.d.ts +8 -0
- package/dist/hooks/use-agent-conversation.d.ts.map +1 -1
- package/dist/hooks/use-agent-conversation.js +10 -0
- package/dist/hooks/use-agent-threads.d.ts +1 -0
- package/dist/hooks/use-agent-threads.d.ts.map +1 -1
- package/dist/hooks/use-agent-threads.js +4 -1
- package/dist/iframe-communication/types.d.ts +25 -0
- package/dist/iframe-communication/types.d.ts.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { intersectRects } from "./layout.js";
|
|
2
|
+
const PIN_SIZE = 28;
|
|
3
|
+
/** What each face after the first adds to a pin's width: a face, less the overlap. */
|
|
4
|
+
const PIN_FACE_STEP = 14;
|
|
5
|
+
/** How far a pin keeps inside the window when its point is near an edge. */
|
|
6
|
+
const PIN_EDGE_MARGIN = 4;
|
|
7
|
+
/**
|
|
8
|
+
* The gap between a pin (or a new comment's point) and its panel: room for the open pin's glow (7px)
|
|
9
|
+
* and its growth (110%, from its pointed corner), with air past them.
|
|
10
|
+
*/
|
|
11
|
+
const PANEL_GAP = 20;
|
|
12
|
+
/**
|
|
13
|
+
* The height a panel is given room for while its own height isn't known (`panelHeight`), the window
|
|
14
|
+
* allowing: its header, its comments' minimum and a composer at its tallest. In a short window the
|
|
15
|
+
* panel slides along the pin's side for it.
|
|
16
|
+
*/
|
|
17
|
+
const PANEL_MIN_ROOM = 384;
|
|
18
|
+
/** Faces a pin shows before "+N". */
|
|
19
|
+
const PIN_FACES_MAX = 3;
|
|
20
|
+
/** A pin's width: one face fills it, and each more face (or "+N") adds what shows past the overlap. */
|
|
21
|
+
export const pinWidth = (faceCount) => PIN_SIZE + Math.max(0, faceCount - 1) * PIN_FACE_STEP;
|
|
22
|
+
/** The gap between what a comment is on (or its pin) and a panel above or below it. */
|
|
23
|
+
const TARGET_GAP = 8;
|
|
24
|
+
/**
|
|
25
|
+
* Where the panel goes by a pin `width` wide whose pointed corner is on `point` (a new comment's:
|
|
26
|
+
* the one-face pin it becomes), keeping clear of `target` where it can. The pin's box is worked out
|
|
27
|
+
* as `Pin` places it: above the point and toward the line's end, slid back inside the window at its
|
|
28
|
+
* edges. Where it stands is `side` (else as `panelSide` opens one): past the pin's end side or
|
|
29
|
+
* before its start, level with the pin's top (`down`) or with its bottom (`up`), or further along
|
|
30
|
+
* when that leaves it less than `PANEL_MIN_ROOM`; or below or above the target, its end edge level
|
|
31
|
+
* with the pin's. It is never taller than the room it has, so its replies scroll and its composer
|
|
32
|
+
* stays on screen. With a `shift` it then moves by it, keeping its height: it goes out of the
|
|
33
|
+
* window with its pin.
|
|
34
|
+
*/
|
|
35
|
+
export const panelPosition = (point, width, panelWidth, { viewport: frame, shift, side, target, panelHeight }) => {
|
|
36
|
+
const geometry = besidePin(point, width, panelWidth, frame, target, panelHeight);
|
|
37
|
+
const taken = side ?? panelSide(point, width, panelWidth, undefined, frame, target, panelHeight);
|
|
38
|
+
const box = panelBox(taken, geometry);
|
|
39
|
+
const dx = shift?.x ?? 0;
|
|
40
|
+
const dy = shift?.y ?? 0;
|
|
41
|
+
const hangsUp = taken.place === "above" || (taken.place !== "below" && taken.block === "up");
|
|
42
|
+
if (hangsUp) {
|
|
43
|
+
return {
|
|
44
|
+
left: box.left + dx,
|
|
45
|
+
bottom: frame.height - box.bottom - dy,
|
|
46
|
+
width: panelWidth,
|
|
47
|
+
maxHeight: box.bottom - geometry.margin,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
left: box.left + dx,
|
|
52
|
+
top: box.top + dy,
|
|
53
|
+
width: panelWidth,
|
|
54
|
+
maxHeight: frame.height - box.top - geometry.margin,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* How far a panel's place may run short of the room it wants (its height down the window, its
|
|
59
|
+
* width across), or cover what the comment is on, before it moves: the two switch points are this
|
|
60
|
+
* far apart at least, so a page scrolled back and forth by a few pixels never makes it flap.
|
|
61
|
+
*/
|
|
62
|
+
const PANEL_SIDE_SLACK = 48;
|
|
63
|
+
/**
|
|
64
|
+
* Where a panel stands by its pin (`panelPosition`'s `side`), sticky. As it opens: level with the
|
|
65
|
+
* pin's top, hanging down, when it fits between there and the window's bottom margin; hanging up
|
|
66
|
+
* from the pin's bottom only when it doesn't and does fit above (`block`); and the first place
|
|
67
|
+
* (`PanelSide`) that fits inside the window's margins (a panel below or above the target with the
|
|
68
|
+
* room it wants) without covering `target` (`ThreadLayout.target`), else before the pin's start.
|
|
69
|
+
* The room it wants is `panelHeight` when known, else `PANEL_MIN_ROOM`. Then, given where it
|
|
70
|
+
* stood (`previous`): hanging down and standing past the pin's end side are where it rests, kept
|
|
71
|
+
* while they fit within `PANEL_SIDE_SLACK` (a panel kept within the slack slides along for room,
|
|
72
|
+
* still beside the pin); any other side or place is held only until the resting one fits again
|
|
73
|
+
* with no slack, then kept while it fits within the slack. The two switch points are the slack
|
|
74
|
+
* apart, so a page scrolled back and forth by a few pixels never makes it flap. A `previous`
|
|
75
|
+
* chosen before the height was known is not kept once it is (`isMeasured`). Without a target only
|
|
76
|
+
* the places beside the pin are taken.
|
|
77
|
+
*/
|
|
78
|
+
export const panelSide = (point, width, panelWidth, previous, viewport, target, panelHeight) => {
|
|
79
|
+
const geometry = besidePin(point, width, panelWidth, viewport, target, panelHeight);
|
|
80
|
+
const { margin, pin, room } = geometry;
|
|
81
|
+
const isMeasured = panelHeight !== undefined;
|
|
82
|
+
const kept = previous && (previous.isMeasured || !isMeasured) ? previous : undefined;
|
|
83
|
+
const roomDown = viewport.height - margin - pin.top;
|
|
84
|
+
const roomUp = pin.bottom - margin;
|
|
85
|
+
// Down is where it rests: it hangs up only while down is short by more than the slack, and comes
|
|
86
|
+
// back down as soon as down fits.
|
|
87
|
+
const block = kept?.block === "down"
|
|
88
|
+
? roomDown < room - PANEL_SIDE_SLACK && roomUp >= room
|
|
89
|
+
? "up"
|
|
90
|
+
: "down"
|
|
91
|
+
: kept?.block === "up"
|
|
92
|
+
? roomDown >= room
|
|
93
|
+
? "down"
|
|
94
|
+
: "up"
|
|
95
|
+
: roomDown < room && roomUp >= room
|
|
96
|
+
? "up"
|
|
97
|
+
: "down";
|
|
98
|
+
const otherBlock = block === "down" ? "up" : "down";
|
|
99
|
+
const order = [
|
|
100
|
+
{ block, place: "end" },
|
|
101
|
+
{ block: otherBlock, place: "end" },
|
|
102
|
+
{ block, place: "below" },
|
|
103
|
+
{ block, place: "above" },
|
|
104
|
+
{ block, place: "start" },
|
|
105
|
+
];
|
|
106
|
+
if (kept) {
|
|
107
|
+
// Beside the pin is where it rests too: a panel below, above or over the target goes back
|
|
108
|
+
// there as soon as it fits; one beside it stays while it fits within the slack.
|
|
109
|
+
const backBeside = kept.place === "end" ? undefined : order.slice(0, 2).find((candidate) => fits(candidate, geometry, 0));
|
|
110
|
+
if (backBeside) {
|
|
111
|
+
return { ...backBeside, isMeasured };
|
|
112
|
+
}
|
|
113
|
+
if (fits({ block, place: kept.place }, geometry, PANEL_SIDE_SLACK)) {
|
|
114
|
+
return { block, place: kept.place, isMeasured };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const side = order.find((candidate) => fits(candidate, geometry, 0)) ?? { block, place: kept?.place ?? "start" };
|
|
118
|
+
return { ...side, isMeasured };
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* The pin's box as `Pin` places it, where a panel would go on each side of it, and what the panel
|
|
122
|
+
* keeps clear of: the target and the pin together.
|
|
123
|
+
*/
|
|
124
|
+
const besidePin = (point, width, panelWidth, viewport, target, panelHeight) => {
|
|
125
|
+
const margin = 8;
|
|
126
|
+
const pinLeft = viewport.isRtl
|
|
127
|
+
? Math.max(PIN_EDGE_MARGIN, point.x - width)
|
|
128
|
+
: Math.min(point.x, viewport.width - PIN_EDGE_MARGIN - width);
|
|
129
|
+
const pinRight = pinLeft + width;
|
|
130
|
+
const pinTop = Math.max(PIN_EDGE_MARGIN, point.y - PIN_SIZE);
|
|
131
|
+
const pin = { left: pinLeft, right: pinRight, top: pinTop, bottom: pinTop + PIN_SIZE };
|
|
132
|
+
return {
|
|
133
|
+
margin,
|
|
134
|
+
panelWidth,
|
|
135
|
+
viewport,
|
|
136
|
+
pin,
|
|
137
|
+
target,
|
|
138
|
+
/** The target and the pin together: what a panel below or above them stands clear of. */
|
|
139
|
+
avoid: target
|
|
140
|
+
? {
|
|
141
|
+
top: Math.min(target.top, pin.top),
|
|
142
|
+
left: Math.min(target.left, pin.left),
|
|
143
|
+
right: Math.max(target.right, pin.right),
|
|
144
|
+
bottom: Math.max(target.bottom, pin.bottom),
|
|
145
|
+
}
|
|
146
|
+
: null,
|
|
147
|
+
pastEnd: viewport.isRtl ? pinLeft - PANEL_GAP - panelWidth : pinRight + PANEL_GAP,
|
|
148
|
+
beforeStart: viewport.isRtl ? pinRight + PANEL_GAP : pinLeft - PANEL_GAP - panelWidth,
|
|
149
|
+
/** A panel's left edge with its end edge level with the pin's. */
|
|
150
|
+
endAligned: viewport.isRtl ? pinLeft : pinRight - panelWidth,
|
|
151
|
+
room: Math.min(panelHeight ?? PANEL_MIN_ROOM, viewport.height - 2 * margin),
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
/** The panel's box at `side`, as tall as the room it wants, kept inside the window's margins. */
|
|
155
|
+
const panelBox = (side, geometry) => {
|
|
156
|
+
const { margin, panelWidth, viewport, pin, room } = geometry;
|
|
157
|
+
const avoid = geometry.avoid ?? pin;
|
|
158
|
+
// The lowest top that still leaves the panel its room.
|
|
159
|
+
const farthest = viewport.height - margin - room;
|
|
160
|
+
const inMargins = (left) => Math.min(Math.max(margin, left), viewport.width - margin - panelWidth);
|
|
161
|
+
const box = (left, top) => ({
|
|
162
|
+
left,
|
|
163
|
+
right: left + panelWidth,
|
|
164
|
+
top,
|
|
165
|
+
bottom: top + room,
|
|
166
|
+
});
|
|
167
|
+
switch (side.place) {
|
|
168
|
+
case "below":
|
|
169
|
+
return box(inMargins(geometry.endAligned), Math.max(margin, Math.min(avoid.bottom + TARGET_GAP, farthest)));
|
|
170
|
+
case "above":
|
|
171
|
+
return box(inMargins(geometry.endAligned), Math.min(viewport.height - margin, Math.max(avoid.top - TARGET_GAP, margin + room)) - room);
|
|
172
|
+
default: {
|
|
173
|
+
const left = inMargins(side.place === "end" ? geometry.pastEnd : geometry.beforeStart);
|
|
174
|
+
if (side.block === "up") {
|
|
175
|
+
const bottom = Math.max(margin, Math.min(viewport.height - pin.bottom, farthest));
|
|
176
|
+
return box(left, viewport.height - bottom - room);
|
|
177
|
+
}
|
|
178
|
+
return box(left, Math.max(margin, Math.min(pin.top, farthest)));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
/** How far two boxes overlap: the lesser of their overlaps across and down, zero when apart. */
|
|
183
|
+
const overlapDepth = (a, b) => {
|
|
184
|
+
const overlap = intersectRects(a, b);
|
|
185
|
+
return overlap ? Math.min(overlap.right - overlap.left, overlap.bottom - overlap.top) : 0;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Whether a panel at `side` fits within `slack`: beside the pin, across the window's margins;
|
|
189
|
+
* below or above a target, with the room it wants (a panel short of room slides over the target);
|
|
190
|
+
* and, but for the last resort (`start`), covering the target and its pin (each on its own, not
|
|
191
|
+
* the empty corner between them) by no more than `slack`.
|
|
192
|
+
*/
|
|
193
|
+
const fits = (side, geometry, slack) => {
|
|
194
|
+
const { target, pin } = geometry;
|
|
195
|
+
if (side.place === "start") {
|
|
196
|
+
return fitsAt(geometry.beforeStart, geometry, slack);
|
|
197
|
+
}
|
|
198
|
+
if (side.place === "end" ? !fitsAt(geometry.pastEnd, geometry, slack) : !target) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
if (!target) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
const box = panelBox(side, geometry);
|
|
205
|
+
return overlapDepth(box, target) <= slack && overlapDepth(box, pin) <= slack;
|
|
206
|
+
};
|
|
207
|
+
/** Whether a panel whose left edge is at `left` fits across the window, within `slack` past its margins. */
|
|
208
|
+
const fitsAt = (left, geometry, slack) => left >= geometry.margin - slack && left + geometry.panelWidth <= geometry.viewport.width - geometry.margin + slack;
|
|
209
|
+
export const participantKey = (participant) => participant.type === "user" ? `u${participant.userId}` : `a${participant.agentId}`;
|
|
210
|
+
/**
|
|
211
|
+
* Everyone in a pin's threads, by first comment, each once: up to three as faces, the working
|
|
212
|
+
* agent's last (on top, so its spinner badge shows), and how many more there are, as "+N".
|
|
213
|
+
*/
|
|
214
|
+
export const pinParticipants = (threads, workingAgentId) => {
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
const others = threads
|
|
217
|
+
// Participants count only comments still there: a thread whose comments were all deleted
|
|
218
|
+
// shows its author.
|
|
219
|
+
.flatMap((thread) => thread.participants.length > 0 ? thread.participants : [{ type: "user", userId: thread.createdByUserId }])
|
|
220
|
+
.filter((participant) => {
|
|
221
|
+
const key = participantKey(participant);
|
|
222
|
+
const isNew = !seen.has(key) && !(participant.type === "agent" && participant.agentId === workingAgentId);
|
|
223
|
+
seen.add(key);
|
|
224
|
+
return isNew;
|
|
225
|
+
});
|
|
226
|
+
const shown = workingAgentId
|
|
227
|
+
? [...others.slice(0, PIN_FACES_MAX - 1), { type: "agent", agentId: workingAgentId }]
|
|
228
|
+
: others.slice(0, PIN_FACES_MAX);
|
|
229
|
+
return {
|
|
230
|
+
shown,
|
|
231
|
+
moreCount: others.length + (workingAgentId ? 1 : 0) - shown.length,
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
/** How far a panel with nothing to stand beside keeps from the window's bottom and end edges. */
|
|
235
|
+
const CORNER_INSET = 16;
|
|
236
|
+
/**
|
|
237
|
+
* Where a pin `width` wide goes for a panel with nothing on the page to stand beside (a thread
|
|
238
|
+
* opened from its link whose element isn't there): so that `panelPosition` from it puts the panel
|
|
239
|
+
* in the window's bottom end corner, 16px in, with the pin just past the panel's start side, its
|
|
240
|
+
* bottom level with the panel's.
|
|
241
|
+
*/
|
|
242
|
+
export const cornerPoint = (viewport, width, panelWidth) => {
|
|
243
|
+
const fromEnd = CORNER_INSET + panelWidth + PANEL_GAP + width;
|
|
244
|
+
return {
|
|
245
|
+
x: viewport.isRtl ? fromEnd : viewport.width - fromEnd,
|
|
246
|
+
y: viewport.height - CORNER_INSET,
|
|
247
|
+
};
|
|
248
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type AnchorState, resolveAnchor } from "./anchor.js";
|
|
2
|
+
import { type Rect } from "./layout.js";
|
|
3
|
+
import type { CommentAnchor } from "./types.js";
|
|
4
|
+
/** How far a pin keeps from the viewport's edges. */
|
|
5
|
+
export declare const PIN_EDGE_MARGIN = 12;
|
|
6
|
+
export interface PinPoint {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The pin's point, in viewport coordinates: the top-end corner of what the comment is on (top-right
|
|
12
|
+
* on a left-to-right page, top-left with `isRtl`; without it, as the page reads), so the pin,
|
|
13
|
+
* standing above its point and past the end side (`Pin`), stays outside it.
|
|
14
|
+
* - A quote: its first line's corner (`firstLine`), the pin above the words, never over them;
|
|
15
|
+
* given only `lastLine` (a copy from before `firstLine`), that line's corner.
|
|
16
|
+
* - An element: its box's corner. Where it was clicked (`point`, stored on the anchor) doesn't
|
|
17
|
+
* place the pin; it is taken so a copy that passes it keeps working.
|
|
18
|
+
*/
|
|
19
|
+
export declare const pinPoint: (target: {
|
|
20
|
+
kind: "quote";
|
|
21
|
+
lastLine: Rect;
|
|
22
|
+
firstLine?: Rect;
|
|
23
|
+
isRtl?: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
kind: "element";
|
|
26
|
+
rect: Rect;
|
|
27
|
+
point?: {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
};
|
|
31
|
+
isRtl?: boolean;
|
|
32
|
+
}) => PinPoint;
|
|
33
|
+
/** `point` kept `margin` inside the viewport. */
|
|
34
|
+
export declare const keepInViewport: (point: PinPoint, viewport: {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
}, margin?: number) => {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
};
|
|
41
|
+
export interface PinGroup<T> {
|
|
42
|
+
key: string;
|
|
43
|
+
point: PinPoint;
|
|
44
|
+
items: T[];
|
|
45
|
+
}
|
|
46
|
+
/** Pins on the same element become one group, at the first one's point (items in input order). */
|
|
47
|
+
export declare const groupPins: <T>(pins: readonly {
|
|
48
|
+
key: string;
|
|
49
|
+
point: PinPoint;
|
|
50
|
+
item: T;
|
|
51
|
+
}[]) => PinGroup<T>[];
|
|
52
|
+
export interface ThreadLayout {
|
|
53
|
+
state: AnchorState;
|
|
54
|
+
element: HTMLElement | null;
|
|
55
|
+
/** Where the pin goes; null when there is nothing on screen to pin to (scrolled away, covered, missing). */
|
|
56
|
+
point: PinPoint | null;
|
|
57
|
+
/** Threads with the same key point at the same element. */
|
|
58
|
+
groupKey: string | null;
|
|
59
|
+
/** What the anchored text says now, when it changed. */
|
|
60
|
+
currentText?: string;
|
|
61
|
+
/**
|
|
62
|
+
* What the comment is on, in window coordinates, for its panel to keep clear of: the quote's
|
|
63
|
+
* lines' bounding box, else the element's box, cut to the part of it in view (to the window's
|
|
64
|
+
* edge, a line with no height or width, while it is out of view). Absent when the element is
|
|
65
|
+
* missing, and on a platform from before it.
|
|
66
|
+
*/
|
|
67
|
+
target?: Rect;
|
|
68
|
+
/**
|
|
69
|
+
* Why `point` is null though the element is on the page, and where its pin would go: what an
|
|
70
|
+
* open thread needs to keep its pin and panel with the element. Absent while the pin shows, when
|
|
71
|
+
* the element is missing, and on a platform from before it.
|
|
72
|
+
* - `offscreen`: the element is outside the window. `point` is where the pin stood as the element
|
|
73
|
+
* left (kept inside the window, as a shown pin is), and `shift` is how far the element has gone
|
|
74
|
+
* past the window's edge since: the pin and its panel move by it, so they slide out with the
|
|
75
|
+
* page instead of jumping or staying at the edge.
|
|
76
|
+
* - `clipped`: the element's box is in the window but a scroll container hides it (or it has no
|
|
77
|
+
* box): nothing of it can be seen, and nothing should float over what is there instead.
|
|
78
|
+
* - `covered`: it is in view under other UI (a dialog, a menu). `shift` is zero then and for
|
|
79
|
+
* `clipped`.
|
|
80
|
+
*/
|
|
81
|
+
hidden?: {
|
|
82
|
+
by: "offscreen" | "clipped" | "covered";
|
|
83
|
+
point: PinPoint;
|
|
84
|
+
shift: PinPoint;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The comments' own layer (pins, threads) marks its root with this attribute: what it draws over an
|
|
89
|
+
* element doesn't hide the element. Other ignored UI (the agent chat's panel) does.
|
|
90
|
+
*/
|
|
91
|
+
export declare const COMMENTS_LAYER_ATTRIBUTE = "data-blocks-comments-layer";
|
|
92
|
+
/**
|
|
93
|
+
* How far `box` lies past the window's edges: negative past the start (left, top), positive past
|
|
94
|
+
* the end, zero along an axis where it overlaps the window.
|
|
95
|
+
*/
|
|
96
|
+
export declare const pastWindow: (box: Rect, viewport: {
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
}) => PinPoint;
|
|
100
|
+
/** Lays out one anchor whose element is known, on a page that reads right to left with `isRtl`. */
|
|
101
|
+
export declare const layoutAnchor: (resolved: ReturnType<typeof resolveAnchor>, isRtl: boolean) => ThreadLayout;
|
|
102
|
+
/**
|
|
103
|
+
* Where a new comment's pin goes and what its panel keeps clear of, as it is picked (`usePagePick`'s
|
|
104
|
+
* `onPick`): the same corner and target as a thread on it gets (`layoutAnchor`), its quote found in
|
|
105
|
+
* `element`. Reads the page; call it from the pick, not while rendering.
|
|
106
|
+
*/
|
|
107
|
+
export declare const pickPlacement: (anchor: CommentAnchor, element: HTMLElement, isRtl?: boolean) => {
|
|
108
|
+
point: PinPoint;
|
|
109
|
+
target: Rect;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Finds an anchor again, re-checking only its text while its element is still in the page and
|
|
113
|
+
* still inside the same row. One found on the row itself is found anew each time: what the row
|
|
114
|
+
* must say depends on how the thread came to be on it (`resolveAnchor`).
|
|
115
|
+
*/
|
|
116
|
+
export declare const resolveAnchorCached: (anchor: CommentAnchor) => import("./anchor.js").ResolvedAnchor;
|
|
117
|
+
/**
|
|
118
|
+
* Where each thread is on the page now, re-measured at most once per frame while `isActive`, on a
|
|
119
|
+
* page that reads right to left with `isRtl` (the caller's window as it read it; read here on each
|
|
120
|
+
* render when absent).
|
|
121
|
+
*/
|
|
122
|
+
export declare const useCommentPins: (anchors: readonly {
|
|
123
|
+
id: string | number;
|
|
124
|
+
anchor: CommentAnchor;
|
|
125
|
+
}[], isActive: boolean, isRtl?: boolean) => ReadonlyMap<string | number, ThreadLayout>;
|
|
126
|
+
//# sourceMappingURL=pins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pins.d.ts","sourceRoot":"","sources":["../../src/comments/pins.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { checkText, findAnchorItemElement, resolveAnchor } from "./anchor.js";
|
|
3
|
+
import { clipRectsOf, toRect, usePageLayoutTick, visiblePart } from "./layout.js";
|
|
4
|
+
/**
|
|
5
|
+
* Where each thread's pin goes on the page as it is now. Never stored: worked out from the anchor
|
|
6
|
+
* every time the page moves, so a pin follows its element across screen sizes, re-sorted lists and
|
|
7
|
+
* scrolling.
|
|
8
|
+
*/
|
|
9
|
+
/** The page's direction, as the comments read it (left to right with no page). */
|
|
10
|
+
const isPageRtl = () => typeof document !== "undefined" && getComputedStyle(document.documentElement).direction === "rtl";
|
|
11
|
+
/** How far a pin keeps from the viewport's edges. */
|
|
12
|
+
export const PIN_EDGE_MARGIN = 12;
|
|
13
|
+
/**
|
|
14
|
+
* The pin's point, in viewport coordinates: the top-end corner of what the comment is on (top-right
|
|
15
|
+
* on a left-to-right page, top-left with `isRtl`; without it, as the page reads), so the pin,
|
|
16
|
+
* standing above its point and past the end side (`Pin`), stays outside it.
|
|
17
|
+
* - A quote: its first line's corner (`firstLine`), the pin above the words, never over them;
|
|
18
|
+
* given only `lastLine` (a copy from before `firstLine`), that line's corner.
|
|
19
|
+
* - An element: its box's corner. Where it was clicked (`point`, stored on the anchor) doesn't
|
|
20
|
+
* place the pin; it is taken so a copy that passes it keeps working.
|
|
21
|
+
*/
|
|
22
|
+
export const pinPoint = (target) => {
|
|
23
|
+
const box = target.kind === "quote" ? (target.firstLine ?? target.lastLine) : target.rect;
|
|
24
|
+
const isRtl = target.isRtl ?? isPageRtl();
|
|
25
|
+
return { x: isRtl ? box.left : box.right, y: box.top };
|
|
26
|
+
};
|
|
27
|
+
/** `point` kept `margin` inside the viewport. */
|
|
28
|
+
export const keepInViewport = (point, viewport, margin = PIN_EDGE_MARGIN) => ({
|
|
29
|
+
x: Math.min(Math.max(point.x, margin), Math.max(margin, viewport.width - margin)),
|
|
30
|
+
y: Math.min(Math.max(point.y, margin), Math.max(margin, viewport.height - margin)),
|
|
31
|
+
});
|
|
32
|
+
/** Pins on the same element become one group, at the first one's point (items in input order). */
|
|
33
|
+
export const groupPins = (pins) => {
|
|
34
|
+
const groups = new Map();
|
|
35
|
+
for (const pin of pins) {
|
|
36
|
+
const group = groups.get(pin.key);
|
|
37
|
+
if (group) {
|
|
38
|
+
group.items.push(pin.item);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
groups.set(pin.key, { key: pin.key, point: pin.point, items: [pin.item] });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [...groups.values()];
|
|
45
|
+
};
|
|
46
|
+
const HIDDEN = { point: null, groupKey: null };
|
|
47
|
+
/**
|
|
48
|
+
* The comments' own layer (pins, threads) marks its root with this attribute: what it draws over an
|
|
49
|
+
* element doesn't hide the element. Other ignored UI (the agent chat's panel) does.
|
|
50
|
+
*/
|
|
51
|
+
export const COMMENTS_LAYER_ATTRIBUTE = "data-blocks-comments-layer";
|
|
52
|
+
/** Whether the element can be seen at `visible`'s centre, or only the comments' own layer is over it. */
|
|
53
|
+
const isUncovered = (element, visible) => {
|
|
54
|
+
const hit = document.elementFromPoint((visible.left + visible.right) / 2, (visible.top + visible.bottom) / 2);
|
|
55
|
+
return !hit || element.contains(hit) || hit.closest(`[${COMMENTS_LAYER_ATTRIBUTE}]`) !== null;
|
|
56
|
+
};
|
|
57
|
+
const elementKeys = new WeakMap();
|
|
58
|
+
let nextElementKey = 0;
|
|
59
|
+
const keyOf = (element) => {
|
|
60
|
+
let key = elementKeys.get(element);
|
|
61
|
+
if (!key) {
|
|
62
|
+
key = `el-${++nextElementKey}`;
|
|
63
|
+
elementKeys.set(element, key);
|
|
64
|
+
}
|
|
65
|
+
return key;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* How far `box` lies past the window's edges: negative past the start (left, top), positive past
|
|
69
|
+
* the end, zero along an axis where it overlaps the window.
|
|
70
|
+
*/
|
|
71
|
+
export const pastWindow = (box, viewport) => ({
|
|
72
|
+
x: Math.min(0, box.right) + Math.max(0, box.left - viewport.width),
|
|
73
|
+
y: Math.min(0, box.bottom) + Math.max(0, box.top - viewport.height),
|
|
74
|
+
});
|
|
75
|
+
const NO_SHIFT = { x: 0, y: 0 };
|
|
76
|
+
/** A scroll container's box, each edge at or past the window's taken on without end. */
|
|
77
|
+
const reachingWindowEdges = (clip, viewport) => ({
|
|
78
|
+
top: clip.top <= 0 ? -Infinity : clip.top,
|
|
79
|
+
left: clip.left <= 0 ? -Infinity : clip.left,
|
|
80
|
+
right: clip.right >= viewport.width ? Infinity : clip.right,
|
|
81
|
+
bottom: clip.bottom >= viewport.height ? Infinity : clip.bottom,
|
|
82
|
+
});
|
|
83
|
+
/** `rect` squeezed into `bounds`: each edge kept inside it (a line or a point at its edge when outside). */
|
|
84
|
+
const clampRect = (rect, bounds) => {
|
|
85
|
+
const clampX = (x) => Math.min(Math.max(x, bounds.left), bounds.right);
|
|
86
|
+
const clampY = (y) => Math.min(Math.max(y, bounds.top), bounds.bottom);
|
|
87
|
+
return { top: clampY(rect.top), left: clampX(rect.left), right: clampX(rect.right), bottom: clampY(rect.bottom) };
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* A quote's line boxes (several to a line where it crosses inline elements): its first line (the
|
|
91
|
+
* boxes level with the first one, as one) and the box around them all.
|
|
92
|
+
*/
|
|
93
|
+
const quoteLines = (lines) => {
|
|
94
|
+
const first = lines[0];
|
|
95
|
+
const onFirstLine = lines.filter((line) => {
|
|
96
|
+
const middle = (line.top + line.bottom) / 2;
|
|
97
|
+
return middle >= first.top && middle <= first.bottom;
|
|
98
|
+
});
|
|
99
|
+
const around = (rects) => ({
|
|
100
|
+
top: Math.min(...rects.map((rect) => rect.top)),
|
|
101
|
+
left: Math.min(...rects.map((rect) => rect.left)),
|
|
102
|
+
right: Math.max(...rects.map((rect) => rect.right)),
|
|
103
|
+
bottom: Math.max(...rects.map((rect) => rect.bottom)),
|
|
104
|
+
});
|
|
105
|
+
return { firstLine: around(onFirstLine), bounds: around(lines) };
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Where a comment's pin goes and what its panel keeps clear of, measured now: the top-end corner of
|
|
109
|
+
* its quote's first line, else of its element's box (`pinPoint`), kept on the part of the element in
|
|
110
|
+
* view and inside the window. A thread's pin (`layoutAnchor`) and a new comment's (`pickPlacement`)
|
|
111
|
+
* both come from here.
|
|
112
|
+
*/
|
|
113
|
+
const measure = (element, range, isRtl) => {
|
|
114
|
+
const box = toRect(element.getBoundingClientRect());
|
|
115
|
+
const clips = clipRectsOf(element);
|
|
116
|
+
const visible = visiblePart(box, clips);
|
|
117
|
+
const boxes = range
|
|
118
|
+
? Array.from(range.getClientRects())
|
|
119
|
+
.filter((rect) => rect.width > 0 || rect.height > 0)
|
|
120
|
+
.map(toRect)
|
|
121
|
+
: [];
|
|
122
|
+
// A drag from just past a line's last letter leaves an empty box at that line's end: not where
|
|
123
|
+
// the words start, unless nothing else is there.
|
|
124
|
+
const wide = boxes.filter((rect) => rect.right > rect.left);
|
|
125
|
+
const lines = wide.length > 0 ? wide : boxes;
|
|
126
|
+
const quote = lines.length > 0 ? quoteLines(lines) : null;
|
|
127
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
128
|
+
const corner = quote
|
|
129
|
+
? pinPoint({ kind: "quote", firstLine: quote.firstLine, lastLine: lines.at(-1), isRtl })
|
|
130
|
+
: pinPoint({ kind: "element", rect: box, isRtl });
|
|
131
|
+
const shown = visible ?? clips[0];
|
|
132
|
+
const point = keepInViewport({
|
|
133
|
+
x: Math.min(Math.max(corner.x, shown.left), shown.right),
|
|
134
|
+
y: Math.min(Math.max(corner.y, shown.top), shown.bottom),
|
|
135
|
+
}, viewport);
|
|
136
|
+
return { box, clips, visible, viewport, point, target: clampRect(quote?.bounds ?? box, shown) };
|
|
137
|
+
};
|
|
138
|
+
/** Lays out one anchor whose element is known, on a page that reads right to left with `isRtl`. */
|
|
139
|
+
export const layoutAnchor = (resolved, isRtl) => {
|
|
140
|
+
const { element, state, range, currentText } = resolved;
|
|
141
|
+
if (!element) {
|
|
142
|
+
return { state, element: null, currentText, ...HIDDEN };
|
|
143
|
+
}
|
|
144
|
+
const { box, clips, visible, viewport, point, target } = measure(element, range, isRtl);
|
|
145
|
+
if (!visible) {
|
|
146
|
+
// Off screen: only the window hides it. A scroll container's edge counts as the window's only
|
|
147
|
+
// where it reaches the window's edge (a page that scrolls in a full-window container); outside
|
|
148
|
+
// a container through an edge inside the window (below a fixed header, a list mid-page), it is
|
|
149
|
+
// clipped, and stays so once its box leaves the window too: its pin never pops back at the
|
|
150
|
+
// window's edge. So is an element with no box at all.
|
|
151
|
+
const [windowClip, ...containers] = clips;
|
|
152
|
+
const isBox = box.right > box.left && box.bottom > box.top;
|
|
153
|
+
const isOffscreen = isBox &&
|
|
154
|
+
!visiblePart(box, [windowClip]) &&
|
|
155
|
+
!!visiblePart(box, containers.map((clip) => reachingWindowEdges(clip, viewport)));
|
|
156
|
+
const hidden = isOffscreen
|
|
157
|
+
? { by: "offscreen", point, shift: pastWindow(box, viewport) }
|
|
158
|
+
: { by: "clipped", point, shift: NO_SHIFT };
|
|
159
|
+
return { state, element, currentText, target, ...HIDDEN, hidden };
|
|
160
|
+
}
|
|
161
|
+
if (!isUncovered(element, visible)) {
|
|
162
|
+
return { state, element, currentText, target, ...HIDDEN, hidden: { by: "covered", point, shift: NO_SHIFT } };
|
|
163
|
+
}
|
|
164
|
+
return { state, element, currentText, target, point, groupKey: keyOf(element) };
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Where a new comment's pin goes and what its panel keeps clear of, as it is picked (`usePagePick`'s
|
|
168
|
+
* `onPick`): the same corner and target as a thread on it gets (`layoutAnchor`), its quote found in
|
|
169
|
+
* `element`. Reads the page; call it from the pick, not while rendering.
|
|
170
|
+
*/
|
|
171
|
+
export const pickPlacement = (anchor, element, isRtl = isPageRtl()) => {
|
|
172
|
+
const { point, target } = measure(element, checkText(anchor, element).range, isRtl);
|
|
173
|
+
return { point, target };
|
|
174
|
+
};
|
|
175
|
+
/** The element each anchor was last found at: an anchor's object stays the same while its thread is unchanged. */
|
|
176
|
+
const foundByAnchor = new WeakMap();
|
|
177
|
+
/**
|
|
178
|
+
* Finds an anchor again, re-checking only its text while its element is still in the page and
|
|
179
|
+
* still inside the same row. One found on the row itself is found anew each time: what the row
|
|
180
|
+
* must say depends on how the thread came to be on it (`resolveAnchor`).
|
|
181
|
+
*/
|
|
182
|
+
export const resolveAnchorCached = (anchor) => {
|
|
183
|
+
const known = foundByAnchor.get(anchor);
|
|
184
|
+
const row = anchor.item ? findAnchorItemElement(anchor) : null;
|
|
185
|
+
const stillThere = known?.isConnected && (!anchor.item || (row !== known && row?.contains(known) === true));
|
|
186
|
+
const resolved = stillThere ? checkText(anchor, known) : resolveAnchor(anchor);
|
|
187
|
+
if (resolved.element) {
|
|
188
|
+
foundByAnchor.set(anchor, resolved.element);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
foundByAnchor.delete(anchor);
|
|
192
|
+
}
|
|
193
|
+
return resolved;
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Where each thread is on the page now, re-measured at most once per frame while `isActive`, on a
|
|
197
|
+
* page that reads right to left with `isRtl` (the caller's window as it read it; read here on each
|
|
198
|
+
* render when absent).
|
|
199
|
+
*/
|
|
200
|
+
export const useCommentPins = (anchors, isActive, isRtl) => {
|
|
201
|
+
// Not for the React Compiler (app-render compiles this module): the memo's input is the page's
|
|
202
|
+
// layout, read through `tick`, which the compiler would drop from the deps as unused.
|
|
203
|
+
"use no memo";
|
|
204
|
+
const tick = usePageLayoutTick(isActive);
|
|
205
|
+
// A change of direction moves no tick: it is a dependency of its own.
|
|
206
|
+
const rtl = isRtl ?? (isActive && isPageRtl());
|
|
207
|
+
return useMemo(() => {
|
|
208
|
+
void tick;
|
|
209
|
+
const layouts = new Map();
|
|
210
|
+
if (!isActive || typeof document === "undefined") {
|
|
211
|
+
return layouts;
|
|
212
|
+
}
|
|
213
|
+
for (const { id, anchor } of anchors) {
|
|
214
|
+
layouts.set(id, layoutAnchor(resolveAnchorCached(anchor), rtl));
|
|
215
|
+
}
|
|
216
|
+
return layouts;
|
|
217
|
+
}, [anchors, isActive, tick, rtl]);
|
|
218
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Rect } from "./layout.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pointing at part of a text, as the picker does over text: the caret position under the pointer,
|
|
4
|
+
* the word around it, and the range a drag covers — clamped to the row it started in, so a quote
|
|
5
|
+
* never spans two records. The geometry is the browser's; what can be computed without it is pure
|
|
6
|
+
* (word bounds, hit tests, ordering) and tested on its own.
|
|
7
|
+
*/
|
|
8
|
+
/** A position in a text node: before the character at `offset`. */
|
|
9
|
+
export interface TextPoint {
|
|
10
|
+
node: Text;
|
|
11
|
+
offset: number;
|
|
12
|
+
}
|
|
13
|
+
/** A position between nodes or characters, as a Range's boundary takes it. */
|
|
14
|
+
export interface Boundary {
|
|
15
|
+
node: Node;
|
|
16
|
+
offset: number;
|
|
17
|
+
}
|
|
18
|
+
/** How far the pointer moves from a press on text before it is a drag, not a click. */
|
|
19
|
+
export declare const DRAG_THRESHOLD_PX = 4;
|
|
20
|
+
/**
|
|
21
|
+
* The word around `offset` in `text` — the one it is in, else the one ending right before it —
|
|
22
|
+
* as `[start, end)`; null over spaces and punctuation. Words in any script (`Intl.Segmenter`), a
|
|
23
|
+
* whitespace split where it is missing.
|
|
24
|
+
*/
|
|
25
|
+
export declare const wordBoundsAt: (text: string, offset: number, locale?: string) => {
|
|
26
|
+
start: number;
|
|
27
|
+
end: number;
|
|
28
|
+
} | null;
|
|
29
|
+
/** Whether `(x, y)` is on one of `rects`, with `slop` px around each. */
|
|
30
|
+
export declare const isPointInRects: (x: number, y: number, rects: readonly Rect[], slop?: number) => boolean;
|
|
31
|
+
/** Whether `a` comes after `b` in the document. */
|
|
32
|
+
export declare const isAfter: (a: Boundary, b: Boundary) => boolean;
|
|
33
|
+
/** The range between two positions, in document order. */
|
|
34
|
+
export declare const rangeBetween: (doc: Document, a: Boundary, b: Boundary) => Range;
|
|
35
|
+
/**
|
|
36
|
+
* What a drag that starts at `node` may cover: the record it is in (the nearest `itemAnchorProps`
|
|
37
|
+
* element), else the element a click there would pick. A quote never leaves it.
|
|
38
|
+
*/
|
|
39
|
+
export declare const dragRootOf: (node: Node) => Element | null;
|
|
40
|
+
/** `point` kept inside `root`: a position past its end is its end, one before its start is its start. */
|
|
41
|
+
export declare const clampToRoot: (root: Element, point: Boundary) => Boundary;
|
|
42
|
+
/** The most characters a lookup measures to correct the browser's answer before keeping it. */
|
|
43
|
+
export declare const CARET_SCAN_MAX = 256;
|
|
44
|
+
/**
|
|
45
|
+
* The caret position the browser puts under `(x, y)`, when it is in a text node. Where a run reads
|
|
46
|
+
* the other way than its line (Latin words on a right-to-left page), the browser can answer at a
|
|
47
|
+
* run's edge with a position beside other characters, far from the pointer (Chrome: the line's
|
|
48
|
+
* end for the left half of the run's first letter, the run's start for the right half of its
|
|
49
|
+
* last): when neither character beside its answer is under the pointer, the one that is decides,
|
|
50
|
+
* found near the answer (`CARET_SCAN_MAX`), else the answer stands. Characters are what a reader
|
|
51
|
+
* sees as one (an emoji, a letter with its marks): a corrected position is never inside one.
|
|
52
|
+
*/
|
|
53
|
+
export declare const caretFromPoint: (doc: Document, x: number, y: number) => TextPoint | null;
|
|
54
|
+
/**
|
|
55
|
+
* The text position under the pointer, only when the pointer is ON the text: the browser answers
|
|
56
|
+
* with the nearest text from anywhere in an element, so the characters beside the caret decide
|
|
57
|
+
* (a space between words counts, so the cursor doesn't flicker along a line). Null over empty
|
|
58
|
+
* space, in ignored UI or an editable field.
|
|
59
|
+
*/
|
|
60
|
+
export declare const textPointAt: (doc: Document, x: number, y: number) => TextPoint | null;
|
|
61
|
+
/** The word under a text position, as a range: what the hover tints. */
|
|
62
|
+
export declare const wordRangeAt: (doc: Document, point: TextPoint) => Range | null;
|
|
63
|
+
/** Where the caret bar goes for a text position: the edge of the character beside it. */
|
|
64
|
+
export declare const caretRectAt: (doc: Document, point: TextPoint) => {
|
|
65
|
+
x: number;
|
|
66
|
+
top: number;
|
|
67
|
+
bottom: number;
|
|
68
|
+
} | null;
|
|
69
|
+
//# sourceMappingURL=text-pick.d.ts.map
|