visual-comments 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +477 -0
- package/dist/core/aim.d.ts +21 -0
- package/dist/core/aim.js +21 -0
- package/dist/core/api.d.ts +5 -0
- package/dist/core/api.js +11 -0
- package/dist/core/metaball.d.ts +15 -0
- package/dist/core/metaball.js +42 -0
- package/dist/core/presence.d.ts +13 -0
- package/dist/core/presence.js +25 -0
- package/dist/core/surfaces.d.ts +95 -0
- package/dist/core/surfaces.js +197 -0
- package/dist/core/target.d.ts +59 -0
- package/dist/core/target.js +382 -0
- package/dist/core/threads.svelte.d.ts +80 -0
- package/dist/core/threads.svelte.js +336 -0
- package/dist/core/time.d.ts +3 -0
- package/dist/core/time.js +25 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/server/env.d.ts +78 -0
- package/dist/server/env.js +94 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.js +74 -0
- package/dist/server/variables.d.ts +28 -0
- package/dist/server/variables.js +73 -0
- package/dist/shared/limits.d.ts +9 -0
- package/dist/shared/limits.js +10 -0
- package/dist/shared/protocol.d.ts +6 -0
- package/dist/shared/protocol.js +7 -0
- package/dist/shared/types.d.ts +157 -0
- package/dist/shared/types.js +2 -0
- package/dist/sveltekit/CommentTools.svelte +98 -0
- package/dist/sveltekit/CommentTools.svelte.d.ts +6 -0
- package/dist/sveltekit/Comments.svelte +104 -0
- package/dist/sveltekit/Comments.svelte.d.ts +25 -0
- package/dist/sveltekit/Toolbar.svelte +64 -0
- package/dist/sveltekit/Toolbar.svelte.d.ts +13 -0
- package/dist/sveltekit/ToolbarButton.svelte +66 -0
- package/dist/sveltekit/ToolbarButton.svelte.d.ts +12 -0
- package/dist/sveltekit/context.d.ts +16 -0
- package/dist/sveltekit/context.js +9 -0
- package/dist/sveltekit/index.d.ts +6 -0
- package/dist/sveltekit/index.js +6 -0
- package/dist/ui/Account.svelte +106 -0
- package/dist/ui/Account.svelte.d.ts +8 -0
- package/dist/ui/App.svelte +77 -0
- package/dist/ui/App.svelte.d.ts +7 -0
- package/dist/ui/Avatar.svelte +64 -0
- package/dist/ui/Avatar.svelte.d.ts +11 -0
- package/dist/ui/Composer.svelte +267 -0
- package/dist/ui/Composer.svelte.d.ts +20 -0
- package/dist/ui/Connect.svelte +128 -0
- package/dist/ui/Connect.svelte.d.ts +6 -0
- package/dist/ui/Icon.svelte +44 -0
- package/dist/ui/Icon.svelte.d.ts +29 -0
- package/dist/ui/Menu.svelte +69 -0
- package/dist/ui/Menu.svelte.d.ts +15 -0
- package/dist/ui/MenuItem.svelte +38 -0
- package/dist/ui/MenuItem.svelte.d.ts +12 -0
- package/dist/ui/Overlay.svelte +502 -0
- package/dist/ui/Overlay.svelte.d.ts +3 -0
- package/dist/ui/Panel.svelte +220 -0
- package/dist/ui/Panel.svelte.d.ts +10 -0
- package/dist/ui/Pin.svelte +156 -0
- package/dist/ui/Pin.svelte.d.ts +15 -0
- package/dist/ui/Presence.svelte +43 -0
- package/dist/ui/Presence.svelte.d.ts +8 -0
- package/dist/ui/ThreadCard.svelte +201 -0
- package/dist/ui/ThreadCard.svelte.d.ts +13 -0
- package/dist/ui/Toolbar.svelte +35 -0
- package/dist/ui/Toolbar.svelte.d.ts +18 -0
- package/dist/ui/context.d.ts +3 -0
- package/dist/ui/context.js +8 -0
- package/dist/ui/generated/styles.css +2 -0
- package/dist/ui/generated/styles.d.ts +2 -0
- package/dist/ui/generated/styles.js +3 -0
- package/dist/ui/styles.css +113 -0
- package/dist/ui/text.d.ts +5 -0
- package/dist/ui/text.js +11 -0
- package/dist/visual-comments.svelte.d.ts +230 -0
- package/dist/visual-comments.svelte.js +693 -0
- package/dist/vite/index.d.ts +20 -0
- package/dist/vite/index.js +78 -0
- package/dist-widget/visual-comments.js +58 -0
- package/package.json +86 -3
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge from a dot of radius `r` at `x`, `y` to the nearest edge of a
|
|
3
|
+
* box, as between two metaballs about to part: wide where it meets each,
|
|
4
|
+
* pinched between, and thinner the farther it stretches. Undefined when the
|
|
5
|
+
* dot touches or is inside the box, or is beyond `reach` from it.
|
|
6
|
+
*/
|
|
7
|
+
export function neck(x, y, r, box, reach) {
|
|
8
|
+
const corner = Math.min(box.radius, box.width / 2, box.height / 2);
|
|
9
|
+
// The nearest point on a rounded box: clamp to the box shrunk by its corner, then step out by the corner.
|
|
10
|
+
const cx = clamp(x, box.left + corner, box.left + box.width - corner);
|
|
11
|
+
const cy = clamp(y, box.top + corner, box.top + box.height - corner);
|
|
12
|
+
const away = Math.hypot(x - cx, y - cy);
|
|
13
|
+
const gap = away - corner;
|
|
14
|
+
if (gap <= r || gap > reach)
|
|
15
|
+
return undefined;
|
|
16
|
+
const ux = (cx - x) / away;
|
|
17
|
+
const uy = (cy - y) / away;
|
|
18
|
+
const nx = -uy;
|
|
19
|
+
const ny = ux;
|
|
20
|
+
// Sink the foot into the outline's stroke so the two read as one shape.
|
|
21
|
+
const qx = cx - ux * (corner - 1);
|
|
22
|
+
const qy = cy - uy * (corner - 1);
|
|
23
|
+
const stretch = Math.min(1, (gap - r) / Math.max(reach - r, 1));
|
|
24
|
+
const start = r * 0.85;
|
|
25
|
+
const foot = r * (1.8 - 0.8 * stretch);
|
|
26
|
+
const waist = Math.max(0.6, r * (0.55 - 0.35 * stretch));
|
|
27
|
+
// A quadratic's middle sits at (start + foot) / 4 + control / 2; solve for the waist.
|
|
28
|
+
const control = 2 * waist - (start + foot) / 2;
|
|
29
|
+
const ax = x + ux * r * 0.3;
|
|
30
|
+
const ay = y + uy * r * 0.3;
|
|
31
|
+
const mx = (ax + qx) / 2;
|
|
32
|
+
const my = (ay + qy) / 2;
|
|
33
|
+
const point = (px, py, side, width) => `${(px + nx * side * width).toFixed(2)} ${(py + ny * side * width).toFixed(2)}`;
|
|
34
|
+
return [
|
|
35
|
+
`M ${point(ax, ay, 1, start)}`,
|
|
36
|
+
`Q ${point(mx, my, 1, control)} ${point(qx, qy, 1, foot)}`,
|
|
37
|
+
`L ${point(qx, qy, -1, foot)}`,
|
|
38
|
+
`Q ${point(mx, my, -1, control)} ${point(ax, ay, -1, start)}`,
|
|
39
|
+
'Z'
|
|
40
|
+
].join(' ');
|
|
41
|
+
}
|
|
42
|
+
const clamp = (value, min, max) => Math.min(Math.max(value, min), Math.max(min, max));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Author, Participant, PresenceState } from '../shared/types.js';
|
|
2
|
+
/** Someone present, however many tabs or tasks they have. */
|
|
3
|
+
export interface Present {
|
|
4
|
+
author: Author;
|
|
5
|
+
/** Their busiest participant's state. */
|
|
6
|
+
state: PresenceState;
|
|
7
|
+
/** Busiest first. */
|
|
8
|
+
participants: Participant[];
|
|
9
|
+
}
|
|
10
|
+
/** Participants grouped by who they are, in the order they arrived. */
|
|
11
|
+
export declare function present(participants: Participant[]): Present[];
|
|
12
|
+
/** What a participant is doing, as a sentence, or undefined while they only look. */
|
|
13
|
+
export declare function activity(participant: Participant): string | undefined;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const RANK = { viewing: 0, typing: 1, working: 2 };
|
|
2
|
+
/** Participants grouped by who they are, in the order they arrived. */
|
|
3
|
+
export function present(participants) {
|
|
4
|
+
const groups = new Map();
|
|
5
|
+
for (const participant of participants) {
|
|
6
|
+
const group = groups.get(participant.author.sub);
|
|
7
|
+
if (group)
|
|
8
|
+
group.push(participant);
|
|
9
|
+
else
|
|
10
|
+
groups.set(participant.author.sub, [participant]);
|
|
11
|
+
}
|
|
12
|
+
return [...groups.values()].map((group) => {
|
|
13
|
+
group.sort((a, b) => RANK[b.state] - RANK[a.state]);
|
|
14
|
+
return { author: group[0].author, state: group[0].state, participants: group };
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** What a participant is doing, as a sentence, or undefined while they only look. */
|
|
18
|
+
export function activity(participant) {
|
|
19
|
+
const { name } = participant.author;
|
|
20
|
+
if (participant.state === 'typing')
|
|
21
|
+
return `${name} is typing…`;
|
|
22
|
+
if (participant.state === 'working')
|
|
23
|
+
return participant.note ? `${name} is working: ${participant.note}` : `${name} is working…`;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type Box, type Search } from './target.js';
|
|
2
|
+
import type { CommentThread } from '../shared/types.js';
|
|
3
|
+
export interface Rect {
|
|
4
|
+
left: number;
|
|
5
|
+
top: number;
|
|
6
|
+
right: number;
|
|
7
|
+
bottom: number;
|
|
8
|
+
}
|
|
9
|
+
export interface Surface {
|
|
10
|
+
/** '' for the page. */
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
/** Null for the page. */
|
|
14
|
+
element: Element | null;
|
|
15
|
+
/** Where its elements are found, if that document can be read. */
|
|
16
|
+
doc?: Document;
|
|
17
|
+
/** Surface pixels to client pixels: client = origin + surface × scale. */
|
|
18
|
+
origin: {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
};
|
|
22
|
+
scale: number;
|
|
23
|
+
/** In surface pixels. */
|
|
24
|
+
size: {
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
};
|
|
28
|
+
/** The visible part of the surface, in client pixels. */
|
|
29
|
+
clip: Rect;
|
|
30
|
+
/** The visible part of what holds it, in client pixels: where its cards can go. */
|
|
31
|
+
stage: Rect;
|
|
32
|
+
/** The document's client pixels to the host's: client = docOrigin + doc × docScale. */
|
|
33
|
+
docOrigin: {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
};
|
|
37
|
+
docScale: number;
|
|
38
|
+
/** Where targets are looked for in the document. */
|
|
39
|
+
search: Search;
|
|
40
|
+
/** A frame that takes pointer events itself, so comment mode needs a layer over it to catch them. */
|
|
41
|
+
catches: boolean;
|
|
42
|
+
/** The host keeps its own drag and pan gestures over this surface in comment mode. */
|
|
43
|
+
passGestures: boolean;
|
|
44
|
+
/** From `data-vc-context`. */
|
|
45
|
+
context: Record<string, string>;
|
|
46
|
+
}
|
|
47
|
+
export interface DiscoverOptions {
|
|
48
|
+
/** Whether an element belongs to the comment UI. */
|
|
49
|
+
ours: (element: Element) => boolean;
|
|
50
|
+
documentOf?: (surface: Element, frame: HTMLIFrameElement) => Document | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** The surfaces on the page now, outermost first. */
|
|
53
|
+
export declare function discover(doc: Document, options: DiscoverOptions): Surface[];
|
|
54
|
+
/** A same-origin frame's document once it has loaded, or undefined. */
|
|
55
|
+
export declare function frameDocument(_surface: Element, frame: HTMLIFrameElement): Document | undefined;
|
|
56
|
+
/** The surface under a client point, if a comment can go there. */
|
|
57
|
+
export declare function surfaceAt(surfaces: Surface[], doc: Document, x: number, y: number, ours: (element: Element) => boolean): Surface | undefined;
|
|
58
|
+
export declare const toClient: (s: Surface, x: number, y: number) => {
|
|
59
|
+
x: number;
|
|
60
|
+
y: number;
|
|
61
|
+
};
|
|
62
|
+
export declare const fromClient: (s: Surface, x: number, y: number) => {
|
|
63
|
+
x: number;
|
|
64
|
+
y: number;
|
|
65
|
+
};
|
|
66
|
+
export declare const docToClient: (s: Surface, x: number, y: number) => {
|
|
67
|
+
x: number;
|
|
68
|
+
y: number;
|
|
69
|
+
};
|
|
70
|
+
export declare const clientToDoc: (s: Surface, x: number, y: number) => {
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
};
|
|
74
|
+
/** A box in the surface's document, in client pixels. */
|
|
75
|
+
export declare function docBoxToClient(s: Surface, b: Box): Rect;
|
|
76
|
+
/** A box in the surface's document, in surface pixels. */
|
|
77
|
+
export declare function docBoxToSurface(s: Surface, b: Box): Box;
|
|
78
|
+
export interface Spot {
|
|
79
|
+
/** The pin, in client pixels. */
|
|
80
|
+
x: number;
|
|
81
|
+
y: number;
|
|
82
|
+
/** The element under it, if found, and its box in client pixels. */
|
|
83
|
+
element?: Element;
|
|
84
|
+
box?: Rect;
|
|
85
|
+
/** Out of view, or its element is gone. */
|
|
86
|
+
hidden: boolean;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Where a thread's pin is now. A pin on an element moves with it, at the same
|
|
90
|
+
* share of its width and height, and hides while it is out of view or gone; a
|
|
91
|
+
* free pin, or one whose document cannot be read, stays where it was placed.
|
|
92
|
+
*/
|
|
93
|
+
export declare function spotOf(s: Surface, thread: Pick<CommentThread, 'x' | 'y' | 'anchor'>): Spot;
|
|
94
|
+
export declare function inside(r: Rect, x: number, y: number): boolean;
|
|
95
|
+
export declare function intersect(a: Rect, b: Rect): Rect;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Surfaces: the parts of a page that take comments. Without any declared
|
|
2
|
+
// surface, the page itself is one. A host declares surfaces in markup:
|
|
3
|
+
//
|
|
4
|
+
// <div data-vc-surface="inbox" data-vc-title="Inbox"> part of the page
|
|
5
|
+
// <iframe data-vc-surface="inbox" data-vc-title="Inbox"> a frame
|
|
6
|
+
// <div data-vc-surface="inbox"> … <iframe data-vc-frame> … a box holding a frame
|
|
7
|
+
//
|
|
8
|
+
// Each surface has its own pixels, from its top-left corner, so pins stay put
|
|
9
|
+
// however the host zooms, pans or scrolls it: the scale and position come from
|
|
10
|
+
// measuring the element on screen against its layout size. The page's pixels
|
|
11
|
+
// are the document's, so pins scroll with it. Elements are found in the
|
|
12
|
+
// surface's document: the host page, or a same-origin frame's own document.
|
|
13
|
+
import { elementsAt, findAnchor, shows, clamp } from './target.js';
|
|
14
|
+
/** The surfaces on the page now, outermost first. */
|
|
15
|
+
export function discover(doc, options) {
|
|
16
|
+
const view = doc.defaultView;
|
|
17
|
+
const elements = [...doc.querySelectorAll('[data-vc-surface]')];
|
|
18
|
+
const viewport = { left: 0, top: 0, right: doc.documentElement.clientWidth, bottom: doc.documentElement.clientHeight };
|
|
19
|
+
const skip = (element) => options.ours(element) || Boolean(element.closest('[data-vc-ignore]'));
|
|
20
|
+
if (!elements.length) {
|
|
21
|
+
const root = doc.scrollingElement ?? doc.documentElement;
|
|
22
|
+
return [{
|
|
23
|
+
id: '',
|
|
24
|
+
title: doc.title,
|
|
25
|
+
element: null,
|
|
26
|
+
doc,
|
|
27
|
+
origin: { x: -view.scrollX, y: -view.scrollY },
|
|
28
|
+
scale: 1,
|
|
29
|
+
size: { width: root.scrollWidth, height: root.scrollHeight },
|
|
30
|
+
clip: viewport,
|
|
31
|
+
stage: viewport,
|
|
32
|
+
docOrigin: { x: 0, y: 0 },
|
|
33
|
+
docScale: 1,
|
|
34
|
+
search: { region: box(viewport), skip },
|
|
35
|
+
catches: false,
|
|
36
|
+
passGestures: false,
|
|
37
|
+
context: {}
|
|
38
|
+
}];
|
|
39
|
+
}
|
|
40
|
+
const styles = new Map();
|
|
41
|
+
const style = (element) => {
|
|
42
|
+
let value = styles.get(element);
|
|
43
|
+
if (!value)
|
|
44
|
+
styles.set(element, (value = view.getComputedStyle(element)));
|
|
45
|
+
return value;
|
|
46
|
+
};
|
|
47
|
+
const surfaces = [];
|
|
48
|
+
for (const element of elements) {
|
|
49
|
+
const rect = element.getBoundingClientRect();
|
|
50
|
+
if (rect.width < 1 || rect.height < 1)
|
|
51
|
+
continue;
|
|
52
|
+
const html = element;
|
|
53
|
+
const layoutWidth = html.offsetWidth || rect.width;
|
|
54
|
+
const scale = rect.width / layoutWidth;
|
|
55
|
+
const stage = intersect(viewport, clipOf(element, style));
|
|
56
|
+
const frame = (element.localName === 'iframe' ? element : element.querySelector('iframe[data-vc-frame]'));
|
|
57
|
+
const surface = {
|
|
58
|
+
id: element.getAttribute('data-vc-surface') ?? '',
|
|
59
|
+
title: element.getAttribute('data-vc-title') ?? frame?.title ?? element.getAttribute('aria-label') ?? '',
|
|
60
|
+
element,
|
|
61
|
+
origin: { x: rect.left + html.clientLeft * scale, y: rect.top + html.clientTop * scale },
|
|
62
|
+
scale,
|
|
63
|
+
size: { width: html.clientWidth || layoutWidth, height: html.clientHeight || html.offsetHeight || rect.height / scale },
|
|
64
|
+
clip: intersect(stage, { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }),
|
|
65
|
+
stage,
|
|
66
|
+
docOrigin: { x: 0, y: 0 },
|
|
67
|
+
docScale: 1,
|
|
68
|
+
search: { region: { x: 0, y: 0, width: 0, height: 0 }, skip },
|
|
69
|
+
catches: false,
|
|
70
|
+
passGestures: Boolean(element.closest('[data-vc-gestures="host"]')),
|
|
71
|
+
context: contextOf(element)
|
|
72
|
+
};
|
|
73
|
+
if (frame) {
|
|
74
|
+
const doc = (options.documentOf ?? frameDocument)(element, frame);
|
|
75
|
+
const r = frame.getBoundingClientRect();
|
|
76
|
+
const docScale = frame.offsetWidth ? r.width / frame.offsetWidth : scale;
|
|
77
|
+
surface.doc = doc;
|
|
78
|
+
surface.docOrigin = { x: r.left + frame.clientLeft * docScale, y: r.top + frame.clientTop * docScale };
|
|
79
|
+
surface.docScale = docScale;
|
|
80
|
+
surface.search = { region: { x: 0, y: 0, width: frame.clientWidth, height: frame.clientHeight }, skip };
|
|
81
|
+
surface.catches = style(frame).pointerEvents !== 'none';
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
surface.doc = doc;
|
|
85
|
+
surface.search = { region: box(surface.clip), within: element, skip };
|
|
86
|
+
}
|
|
87
|
+
surfaces.push(surface);
|
|
88
|
+
}
|
|
89
|
+
return surfaces;
|
|
90
|
+
}
|
|
91
|
+
/** A same-origin frame's document once it has loaded, or undefined. */
|
|
92
|
+
export function frameDocument(_surface, frame) {
|
|
93
|
+
try {
|
|
94
|
+
const doc = frame.contentDocument;
|
|
95
|
+
if (!doc || doc.readyState === 'loading' || doc.URL === 'about:blank')
|
|
96
|
+
return undefined;
|
|
97
|
+
return doc;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The surface under a client point, if a comment can go there. */
|
|
104
|
+
export function surfaceAt(surfaces, doc, x, y, ours) {
|
|
105
|
+
const top = elementsAt(doc, x, y, ours)[0];
|
|
106
|
+
if (!top || top.closest('[data-vc-ignore]'))
|
|
107
|
+
return undefined;
|
|
108
|
+
let found;
|
|
109
|
+
for (const surface of surfaces) {
|
|
110
|
+
if (!inside(surface.clip, x, y))
|
|
111
|
+
continue;
|
|
112
|
+
if (surface.element && !(surface.element === top || surface.element.contains(top)))
|
|
113
|
+
continue;
|
|
114
|
+
// The innermost of nested surfaces.
|
|
115
|
+
if (!found || (found.element && surface.element && found.element.contains(surface.element)))
|
|
116
|
+
found = surface;
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
export const toClient = (s, x, y) => ({ x: s.origin.x + x * s.scale, y: s.origin.y + y * s.scale });
|
|
121
|
+
export const fromClient = (s, x, y) => ({ x: (x - s.origin.x) / s.scale, y: (y - s.origin.y) / s.scale });
|
|
122
|
+
export const docToClient = (s, x, y) => ({ x: s.docOrigin.x + x * s.docScale, y: s.docOrigin.y + y * s.docScale });
|
|
123
|
+
export const clientToDoc = (s, x, y) => ({ x: (x - s.docOrigin.x) / s.docScale, y: (y - s.docOrigin.y) / s.docScale });
|
|
124
|
+
/** A box in the surface's document, in client pixels. */
|
|
125
|
+
export function docBoxToClient(s, b) {
|
|
126
|
+
const a = docToClient(s, b.x, b.y);
|
|
127
|
+
return { left: a.x, top: a.y, right: a.x + b.width * s.docScale, bottom: a.y + b.height * s.docScale };
|
|
128
|
+
}
|
|
129
|
+
/** A box in the surface's document, in surface pixels. */
|
|
130
|
+
export function docBoxToSurface(s, b) {
|
|
131
|
+
const a = docToClient(s, b.x, b.y);
|
|
132
|
+
const p = fromClient(s, a.x, a.y);
|
|
133
|
+
const k = s.docScale / s.scale;
|
|
134
|
+
return { x: p.x, y: p.y, width: b.width * k, height: b.height * k };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Where a thread's pin is now. A pin on an element moves with it, at the same
|
|
138
|
+
* share of its width and height, and hides while it is out of view or gone; a
|
|
139
|
+
* free pin, or one whose document cannot be read, stays where it was placed.
|
|
140
|
+
*/
|
|
141
|
+
export function spotOf(s, thread) {
|
|
142
|
+
const free = () => {
|
|
143
|
+
const c = toClient(s, Math.min(thread.x, s.size.width), Math.min(thread.y, s.size.height));
|
|
144
|
+
return { ...c, hidden: !inside(s.clip, c.x, c.y) };
|
|
145
|
+
};
|
|
146
|
+
const { anchor } = thread;
|
|
147
|
+
if (!anchor || !s.doc)
|
|
148
|
+
return free();
|
|
149
|
+
const element = findAnchor(s.doc, anchor);
|
|
150
|
+
if (!element)
|
|
151
|
+
return { ...free(), hidden: true };
|
|
152
|
+
const r = element.getBoundingClientRect();
|
|
153
|
+
const b = docBoxToSurface(s, { x: r.left, y: r.top, width: r.width, height: r.height });
|
|
154
|
+
const share = (offset, size) => (size > 0 ? offset / size : 0);
|
|
155
|
+
const x = b.x + clamp(share(thread.x - anchor.x, anchor.width) * b.width, 1, b.width - 1);
|
|
156
|
+
const y = b.y + clamp(share(thread.y - anchor.y, anchor.height) * b.height, 1, b.height - 1);
|
|
157
|
+
const c = toClient(s, x, y);
|
|
158
|
+
const d = clientToDoc(s, c.x, c.y);
|
|
159
|
+
const box = docBoxToClient(s, { x: r.left, y: r.top, width: r.width, height: r.height });
|
|
160
|
+
const visible = r.width > 0 && r.height > 0 && inside(s.clip, c.x, c.y) && shows(element, d.x, d.y, s.search);
|
|
161
|
+
return { x: c.x, y: c.y, element, box, hidden: !visible };
|
|
162
|
+
}
|
|
163
|
+
export function inside(r, x, y) {
|
|
164
|
+
return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
|
|
165
|
+
}
|
|
166
|
+
export function intersect(a, b) {
|
|
167
|
+
return { left: Math.max(a.left, b.left), top: Math.max(a.top, b.top), right: Math.min(a.right, b.right), bottom: Math.min(a.bottom, b.bottom) };
|
|
168
|
+
}
|
|
169
|
+
const box = (r) => ({ x: r.left, y: r.top, width: Math.max(0, r.right - r.left), height: Math.max(0, r.bottom - r.top) });
|
|
170
|
+
/** What clips an element: every ancestor that does not let its overflow show. */
|
|
171
|
+
function clipOf(element, style) {
|
|
172
|
+
let clip = { left: -Infinity, top: -Infinity, right: Infinity, bottom: Infinity };
|
|
173
|
+
const body = element.ownerDocument.body;
|
|
174
|
+
for (let node = element.parentElement; node && node !== body; node = node.parentElement) {
|
|
175
|
+
const s = style(node);
|
|
176
|
+
if (s.overflowX === 'visible' && s.overflowY === 'visible')
|
|
177
|
+
continue;
|
|
178
|
+
const r = node.getBoundingClientRect();
|
|
179
|
+
clip = intersect(clip, { left: r.left, top: r.top, right: r.right, bottom: r.bottom });
|
|
180
|
+
}
|
|
181
|
+
return clip;
|
|
182
|
+
}
|
|
183
|
+
function contextOf(element) {
|
|
184
|
+
const raw = element.getAttribute('data-vc-context');
|
|
185
|
+
if (!raw)
|
|
186
|
+
return {};
|
|
187
|
+
try {
|
|
188
|
+
const value = JSON.parse(raw);
|
|
189
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
190
|
+
return {};
|
|
191
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => typeof v === 'string' || typeof v === 'number')
|
|
192
|
+
.map(([k, v]) => [k, String(v)]));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return {};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** A box in a document's client pixels. */
|
|
2
|
+
export interface Box {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
}
|
|
8
|
+
/** An element a pin can attach to. */
|
|
9
|
+
export interface CommentTarget {
|
|
10
|
+
element: Element;
|
|
11
|
+
box: Box;
|
|
12
|
+
/** Corner radius in document pixels, so the highlight takes the element's shape. */
|
|
13
|
+
radius: number;
|
|
14
|
+
/** What it is and what it says, e.g. `button “Add prompts”`. */
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
/** Where to look, and what to pass over. */
|
|
18
|
+
export interface Search {
|
|
19
|
+
/** The part of the document that takes comments: a frame's viewport, or a surface's box. */
|
|
20
|
+
region: Box;
|
|
21
|
+
/** Only elements inside this one, for a surface that is part of a page. */
|
|
22
|
+
within?: Element;
|
|
23
|
+
/** Elements that are never targets, with what they contain: the comment UI, host chrome. */
|
|
24
|
+
skip: (element: Element) => boolean;
|
|
25
|
+
}
|
|
26
|
+
/** How far the pull reaches, in screen pixels. */
|
|
27
|
+
export declare const MAGNET = 14;
|
|
28
|
+
/** The elements at a point, top first, without skipped ones. */
|
|
29
|
+
export declare function elementsAt(doc: Document, x: number, y: number, skip: Search['skip']): Element[];
|
|
30
|
+
/**
|
|
31
|
+
* The element nearest the point, or undefined for open space. Under the
|
|
32
|
+
* pointer, the innermost meaningful element wins. A smaller one within
|
|
33
|
+
* `radius` pulls the pointer onto it, as long as it sits inside that element.
|
|
34
|
+
*/
|
|
35
|
+
export declare function targetAt(doc: Document, x: number, y: number, radius: number, search: Search): CommentTarget | undefined;
|
|
36
|
+
/** The meaningful element around this one, for when the pin meant more than it caught. */
|
|
37
|
+
export declare function widen(target: CommentTarget, search: Search): CommentTarget | undefined;
|
|
38
|
+
/** The element an anchor names in a document, if it is still there and still says the same. */
|
|
39
|
+
export declare function findAnchor(doc: Document, anchor: {
|
|
40
|
+
selector: string;
|
|
41
|
+
text: string;
|
|
42
|
+
}): Element | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Whether the point of the element shows: inside the region, not clipped by a
|
|
45
|
+
* scrolling container, and not covered (a sticky header).
|
|
46
|
+
*/
|
|
47
|
+
export declare function shows(element: Element, x: number, y: number, search: Search): boolean;
|
|
48
|
+
/** Scrolls the document's containers until the element is in the middle of them. */
|
|
49
|
+
export declare function reveal(element: Element): void;
|
|
50
|
+
/** The scrolling container from this element up that can still scroll, to pass wheel turns to. */
|
|
51
|
+
export declare function scrollerFrom(start: Element | null, dx: number, dy: number): Element | undefined;
|
|
52
|
+
/** What a thread stores about its target: a selector, its label and text. */
|
|
53
|
+
export declare function describeTarget(target: CommentTarget): {
|
|
54
|
+
selector: string;
|
|
55
|
+
label: string;
|
|
56
|
+
text: string;
|
|
57
|
+
};
|
|
58
|
+
export declare const clamp: (value: number, min: number, max: number) => number;
|
|
59
|
+
export declare const shorten: (text: string, max: number) => string;
|