@ai-matrx/rich-content 0.2.10 → 0.2.16
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/copy/copy-commands.d.ts +42 -0
- package/dist/copy/copy-commands.js +119 -0
- package/dist/copy/markdown-copy-html.d.ts +3 -0
- package/dist/copy/markdown-copy-html.js +8 -0
- package/dist/copy/markdown-readable-text.d.ts +4 -0
- package/dist/copy/markdown-readable-text.js +62 -0
- package/dist/display/chat-markdown/block-registry/BlockComponentRegistry.js +2 -0
- package/dist/display/chat-markdown/block-registry/block-dispatch.js +2 -0
- package/dist/host/app-bindings.d.ts +30 -0
- package/dist/host/app-bindings.js +38 -0
- package/dist/host/feedback-subject.d.ts +16 -0
- package/dist/host/feedback-subject.js +0 -0
- package/dist/host/manual-copy.d.ts +7 -0
- package/dist/host/manual-copy.js +11 -0
- package/dist/host/registry.d.ts +2 -0
- package/dist/host/registry.js +10 -0
- package/dist/host/session-knob.d.ts +5 -0
- package/dist/host/session-knob.js +7 -0
- package/dist/host/types.d.ts +24 -0
- package/dist/rich-document/RegistryContextMenu.d.ts +29 -0
- package/dist/rich-document/RegistryContextMenu.js +40 -0
- package/dist/rich-document/RichDocument.d.ts +78 -0
- package/dist/rich-document/RichDocument.js +218 -0
- package/dist/rich-document/RichDocumentActionProvider.d.ts +18 -0
- package/dist/rich-document/RichDocumentActionProvider.js +18 -0
- package/dist/rich-document/RichDocumentActionSurface.d.ts +11 -0
- package/dist/rich-document/RichDocumentActionSurface.js +59 -0
- package/dist/rich-document/RichDocumentActions.d.ts +12 -0
- package/dist/rich-document/RichDocumentActions.js +29 -0
- package/dist/rich-document/actions/hostActions.d.ts +5 -0
- package/dist/rich-document/actions/hostActions.js +31 -0
- package/dist/rich-document/actions/labels.d.ts +9 -0
- package/dist/rich-document/actions/labels.js +8 -0
- package/dist/rich-document/actions/markdownTable.d.ts +9 -0
- package/dist/rich-document/actions/markdownTable.js +39 -0
- package/dist/rich-document/actions/provider.d.ts +58 -0
- package/dist/rich-document/actions/provider.js +245 -0
- package/dist/rich-document/actions/resumePendingAuthAction.d.ts +3 -0
- package/dist/rich-document/actions/resumePendingAuthAction.js +31 -0
- package/dist/rich-document/actions/sources/index.d.ts +9 -0
- package/dist/rich-document/actions/sources/index.js +22 -0
- package/dist/rich-document/actions/sources/raw.d.ts +4 -0
- package/dist/rich-document/actions/sources/raw.js +35 -0
- package/dist/rich-document/actions/useRichDocumentProvider.d.ts +1 -0
- package/dist/rich-document/actions/useRichDocumentProvider.js +13 -0
- package/dist/rich-document/host-types.d.ts +51 -0
- package/dist/rich-document/host-types.js +0 -0
- package/dist/rich-document/registrations.d.ts +1 -0
- package/dist/rich-document/registrations.js +13 -0
- package/dist/rich-document/review/applyTargets.d.ts +16 -0
- package/dist/rich-document/review/applyTargets.js +32 -0
- package/dist/rich-document/review/proposedEdit.d.ts +22 -0
- package/dist/rich-document/review/proposedEdit.js +48 -0
- package/dist/rich-document/review/protectedSpans.d.ts +14 -0
- package/dist/rich-document/review/protectedSpans.js +61 -0
- package/dist/rich-document/runtime/actionSurfacesStore.d.ts +30 -0
- package/dist/rich-document/runtime/actionSurfacesStore.js +79 -0
- package/dist/rich-document/runtime/contextMenuFor.d.ts +6 -0
- package/dist/rich-document/runtime/contextMenuFor.js +7 -0
- package/dist/rich-document/runtime/providerBridge.d.ts +20 -0
- package/dist/rich-document/runtime/providerBridge.js +23 -0
- package/dist/rich-document/runtime/useActionSurfaceProvider.d.ts +33 -0
- package/dist/rich-document/runtime/useActionSurfaceProvider.js +214 -0
- package/dist/rich-document/search/ContentFindControl.d.ts +8 -0
- package/dist/rich-document/search/ContentFindControl.js +46 -0
- package/dist/rich-document/search/RenderedFindBar.d.ts +8 -0
- package/dist/rich-document/search/RenderedFindBar.js +110 -0
- package/dist/rich-document/search/renderedFind.d.ts +1 -0
- package/dist/rich-document/search/renderedFind.js +1 -0
- package/dist/rich-document/types.d.ts +384 -0
- package/dist/rich-document/types.js +0 -0
- package/dist/rich-document/variants/ActionBar.d.ts +17 -0
- package/dist/rich-document/variants/ActionBar.js +48 -0
- package/dist/rich-document/variants/MenuVariant.d.ts +15 -0
- package/dist/rich-document/variants/MenuVariant.js +17 -0
- package/dist/rich-document/variants/MiniActionBar.d.ts +5 -0
- package/dist/rich-document/variants/MiniActionBar.js +11 -0
- package/dist/rich-document/variants/RegistryActionList.d.ts +17 -0
- package/dist/rich-document/variants/RegistryActionList.js +94 -0
- package/dist/rich-document/variants/RegistryActionMenu.d.ts +24 -0
- package/dist/rich-document/variants/RegistryActionMenu.js +74 -0
- package/dist/rich-document/variants/shared/AlchemyDocumentMenu.d.ts +7 -0
- package/dist/rich-document/variants/shared/AlchemyDocumentMenu.js +83 -0
- package/dist/rich-document/variants/shared/OpenOneMenuButton.d.ts +9 -0
- package/dist/rich-document/variants/shared/OpenOneMenuButton.js +70 -0
- package/dist/rich-document/variants/shared/StandaloneOneMenu.d.ts +8 -0
- package/dist/rich-document/variants/shared/StandaloneOneMenu.js +32 -0
- package/dist/rich-document/variants/shared/categories.d.ts +4 -0
- package/dist/rich-document/variants/shared/categories.js +31 -0
- package/dist/rich-document/variants/shared/iconMap.d.ts +2 -0
- package/dist/rich-document/variants/shared/iconMap.js +73 -0
- package/dist/rich-document/variants/shared/menuStructure.d.ts +81 -0
- package/dist/rich-document/variants/shared/menuStructure.js +271 -0
- package/dist/rich-document/variants/shared/runAction.d.ts +17 -0
- package/dist/rich-document/variants/shared/runAction.js +24 -0
- package/dist/selection-toolbar/SelectionToolbarFrame.d.ts +64 -0
- package/dist/selection-toolbar/SelectionToolbarFrame.js +191 -0
- package/dist/selection-toolbar/common-actions.d.ts +2 -0
- package/dist/selection-toolbar/common-actions.js +103 -0
- package/dist/selection-toolbar/selection-actions.d.ts +126 -0
- package/dist/selection-toolbar/selection-actions.js +240 -0
- package/dist/selection-toolbar/selection-copy.d.ts +19 -0
- package/dist/selection-toolbar/selection-copy.js +98 -0
- package/dist/selection-toolbar/selection-shape.d.ts +1 -0
- package/dist/selection-toolbar/selection-shape.js +15 -0
- package/dist/selection-toolbar/selection-zones.d.ts +90 -0
- package/dist/selection-toolbar/selection-zones.js +85 -0
- package/dist/selection-toolbar/textarea-caret.d.ts +8 -0
- package/dist/selection-toolbar/textarea-caret.js +69 -0
- package/dist/utils/alchemy-icon-keys.d.ts +18 -0
- package/dist/utils/alchemy-icon-keys.js +29 -0
- package/dist/utils/rendered-find.d.ts +9 -0
- package/dist/utils/rendered-find.js +68 -0
- package/package.json +23 -7
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Action, ActionProvider, ClickTarget } from "@ai-matrx/alchemy/actions";
|
|
2
|
+
import type { SelectionMode, SelectionToolbarUi } from "./selection-zones.js";
|
|
3
|
+
import type { FeedbackSubject } from "../host/feedback-subject.js";
|
|
4
|
+
/** The toolbar's knobs (platform.feature_knob, feature `selection_toolbar`). */
|
|
5
|
+
export declare const SELECTION_TOOLBAR_KNOB_FEATURE = "selection_toolbar";
|
|
6
|
+
export declare const HIGHLIGHT_WHILE_EDITING_KNOB: {
|
|
7
|
+
readonly feature: "selection_toolbar";
|
|
8
|
+
readonly key: "highlight_while_editing";
|
|
9
|
+
};
|
|
10
|
+
/** The code default while the knob loads (and the seeded default): off. */
|
|
11
|
+
export declare const HIGHLIGHT_WHILE_EDITING_DEFAULT = false;
|
|
12
|
+
/**
|
|
13
|
+
* The comment composer's "Send with my next message" switch on a chat answer
|
|
14
|
+
* (Turn References, ruling 5): its starting position. Default ON — a comment on
|
|
15
|
+
* an answer is usually meant for the agent, and the person can switch it off
|
|
16
|
+
* per comment.
|
|
17
|
+
*/
|
|
18
|
+
export declare const COMMENT_SENDS_WITH_NEXT_MESSAGE_KNOB: {
|
|
19
|
+
readonly feature: "selection_toolbar";
|
|
20
|
+
readonly key: "comment_sends_with_next_message";
|
|
21
|
+
};
|
|
22
|
+
export declare const COMMENT_SENDS_WITH_NEXT_MESSAGE_DEFAULT = true;
|
|
23
|
+
export interface SelectionToolbarKnobs {
|
|
24
|
+
highlightWhileEditing: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** The toolbar's own half of the composite host. */
|
|
27
|
+
export interface SelectionToolbarHost {
|
|
28
|
+
kind: "selection-toolbar";
|
|
29
|
+
mode: SelectionMode;
|
|
30
|
+
knobs: SelectionToolbarKnobs;
|
|
31
|
+
ui: SelectionToolbarUi;
|
|
32
|
+
/**
|
|
33
|
+
* How many buttons fit in one row (the docked phone bar), or null when the
|
|
34
|
+
* strip is unbounded (desktop). Past it, the lowest-priority actions move
|
|
35
|
+
* into the registry's overflow (More) so the bar never scrolls.
|
|
36
|
+
*/
|
|
37
|
+
slots: number | null;
|
|
38
|
+
}
|
|
39
|
+
export declare function selectionToolbarHostOf(target: ClickTarget): SelectionToolbarHost | null;
|
|
40
|
+
/** Read another provider's half of the composite host. */
|
|
41
|
+
export declare function hostHalf<T>(target: ClickTarget, key: string): T | null;
|
|
42
|
+
type ModeRule = readonly SelectionMode[] | ((knobs: SelectionToolbarKnobs) => readonly SelectionMode[]);
|
|
43
|
+
/**
|
|
44
|
+
* THE MODE TABLE. Editing: formatting + AI + comment. Reading: highlight,
|
|
45
|
+
* comment, suggest, AI, link and report. AI shows in both.
|
|
46
|
+
*/
|
|
47
|
+
export declare const SELECTION_ACTION_MODES: Readonly<Record<string, ModeRule>>;
|
|
48
|
+
/**
|
|
49
|
+
* THE HOST KINDS — what a person gets, by where the text is. Documentation that
|
|
50
|
+
* the census test checks against the providers (selection-host-kinds.test.ts):
|
|
51
|
+
* a host kind is the set of host halves present on the target.
|
|
52
|
+
*
|
|
53
|
+
* annotated reading (annotation + context menu) highlight ×5, comment, suggest, link, report, AI and more —
|
|
54
|
+
* the study guide, document Annotate, and EVERY saved record
|
|
55
|
+
* (annotations/RecordAnnotations): a note's preview, a chat
|
|
56
|
+
* answer, the studio previewing an unedited saved document.
|
|
57
|
+
* Highlight/Link are absent on a kind with no association pair
|
|
58
|
+
* (a chat answer until its pairs are applied).
|
|
59
|
+
* rich editor (richEditor + context menu) formatting, copy, AI and more; COMMENT only when the buffer
|
|
60
|
+
* is a saved record (a saved document or note) — absent for an
|
|
61
|
+
* unsaved buffer; table actions while the caret/selection is in
|
|
62
|
+
* a table. Notes' own editor modes: pending the notes migration
|
|
63
|
+
* onto the one editor (RC-A4).
|
|
64
|
+
* plain reading (context menu only) copy, save to notes, AI and more — unsaved content: a stream
|
|
65
|
+
* in flight, raw text, an edited studio copy, window panels
|
|
66
|
+
* text field (context menu, editable) copy, save to notes, AI and more
|
|
67
|
+
*/
|
|
68
|
+
export declare const SELECTION_HOST_KINDS: {
|
|
69
|
+
readonly annotatedReading: readonly ["annotation", "contextMenuSelection"];
|
|
70
|
+
readonly richEditor: readonly ["richEditor", "contextMenuSelection"];
|
|
71
|
+
readonly plainReading: readonly ["contextMenuSelection"];
|
|
72
|
+
};
|
|
73
|
+
/** Does the toolbar show `actionId` for this target's mode? */
|
|
74
|
+
export declare function shownInSelectionMode(actionId: string, target: ClickTarget): boolean;
|
|
75
|
+
export declare const SELECTION_COMMON_HOST_KEY = "selectionCommon";
|
|
76
|
+
/** Set by the root on every target: the selected text and the app's doors. */
|
|
77
|
+
export interface SelectionCommonHost {
|
|
78
|
+
kind: "selection-common";
|
|
79
|
+
text: string;
|
|
80
|
+
saveToNotes(content: string): void;
|
|
81
|
+
/** The one "Save to a table" (SAVE-AS-TABLE-EVERYWHERE) for a selection that reads as rows. */
|
|
82
|
+
saveToTable(content: string): void;
|
|
83
|
+
/**
|
|
84
|
+
* The selection written back as the shapes it was drawn from (`shapeTextOfNode` over the
|
|
85
|
+
* selected DOM): a rendered table's rows as a markdown table, a list's bullets as bullets. The
|
|
86
|
+
* flattened `text` loses both. Null where the selection is not over rendered content.
|
|
87
|
+
*/
|
|
88
|
+
shapeText: string | null;
|
|
89
|
+
/** The app's feedback window (a passage report). */
|
|
90
|
+
openFeedback(report: {
|
|
91
|
+
title: string;
|
|
92
|
+
subject: FeedbackSubject;
|
|
93
|
+
}): void;
|
|
94
|
+
/** Open a new chat with this passage of a chat answer staged as a comment (empty body). */
|
|
95
|
+
newChatAbout(passage: {
|
|
96
|
+
quote: string;
|
|
97
|
+
conversationId: string;
|
|
98
|
+
messageId: string;
|
|
99
|
+
}): void;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Which actions keep a button when the bar cannot show them all (a phone).
|
|
103
|
+
* Everything else — including "AI and more" when space is tight — moves into
|
|
104
|
+
* the registry's overflow (the layout's More), so the bar never scrolls.
|
|
105
|
+
*/
|
|
106
|
+
export declare const SELECTION_PRIORITY: Readonly<Record<SelectionMode, readonly string[]>>;
|
|
107
|
+
/** "primary" (a button) or "overflow" (a More row) for this action at this target. */
|
|
108
|
+
export declare function selectionPlacement(id: string, target: ClickTarget): "primary" | "overflow";
|
|
109
|
+
/** A provider's actions, placed for this target (every selection provider returns through this). */
|
|
110
|
+
export declare function placeSelectionActions(actions: readonly Action[], target: ClickTarget): Action[];
|
|
111
|
+
/**
|
|
112
|
+
* A surface adds passage actions (the study guide's "I don't get this") by
|
|
113
|
+
* putting ordinary Alchemy actions on its zone's `passageActions` host half;
|
|
114
|
+
* this provider yields them, gated by the mode table like every other
|
|
115
|
+
* selection action. One provider, so a surface never hand-renders buttons.
|
|
116
|
+
*/
|
|
117
|
+
export declare const PASSAGE_ACTIONS_HOST_KEY = "passageActions";
|
|
118
|
+
export declare const passageActionsProvider: ActionProvider;
|
|
119
|
+
export declare function declareSelectionProvider(provider: ActionProvider): void;
|
|
120
|
+
export declare function declaredSelectionProviders(): readonly ActionProvider[];
|
|
121
|
+
export declare function subscribeDeclaredSelectionProviders(listener: () => void): () => void;
|
|
122
|
+
/** Register a provider into a registry, once per registry (the root calls this). */
|
|
123
|
+
export declare function ensureProvider(registry: {
|
|
124
|
+
register(p: ActionProvider): () => void;
|
|
125
|
+
}, provider: ActionProvider): void;
|
|
126
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const SELECTION_TOOLBAR_KNOB_FEATURE = "selection_toolbar";
|
|
2
|
+
const HIGHLIGHT_WHILE_EDITING_KNOB = {
|
|
3
|
+
feature: SELECTION_TOOLBAR_KNOB_FEATURE,
|
|
4
|
+
key: "highlight_while_editing"
|
|
5
|
+
};
|
|
6
|
+
const HIGHLIGHT_WHILE_EDITING_DEFAULT = false;
|
|
7
|
+
const COMMENT_SENDS_WITH_NEXT_MESSAGE_KNOB = {
|
|
8
|
+
feature: SELECTION_TOOLBAR_KNOB_FEATURE,
|
|
9
|
+
key: "comment_sends_with_next_message"
|
|
10
|
+
};
|
|
11
|
+
const COMMENT_SENDS_WITH_NEXT_MESSAGE_DEFAULT = true;
|
|
12
|
+
function selectionToolbarHostOf(target) {
|
|
13
|
+
const half = target.host?.selectionToolbar;
|
|
14
|
+
return half?.kind === "selection-toolbar" ? half : null;
|
|
15
|
+
}
|
|
16
|
+
function hostHalf(target, key) {
|
|
17
|
+
const host = target.host;
|
|
18
|
+
return host?.[key] ?? null;
|
|
19
|
+
}
|
|
20
|
+
const HIGHLIGHT = (knobs) => knobs.highlightWhileEditing ? ["read", "edit"] : ["read"];
|
|
21
|
+
const SELECTION_ACTION_MODES = {
|
|
22
|
+
// Formatting (rich editor)
|
|
23
|
+
"selection:format-bold": ["edit"],
|
|
24
|
+
"selection:format-italic": ["edit"],
|
|
25
|
+
"selection:format-strike": ["edit"],
|
|
26
|
+
"selection:format-code": ["edit"],
|
|
27
|
+
"selection:format-link": ["edit"],
|
|
28
|
+
"selection:format-h1": ["edit"],
|
|
29
|
+
"selection:format-h2": ["edit"],
|
|
30
|
+
"selection:format-h3": ["edit"],
|
|
31
|
+
"selection:format-quote": ["edit"],
|
|
32
|
+
"selection:format-list": ["edit"],
|
|
33
|
+
"selection:format-numbered": ["edit"],
|
|
34
|
+
"selection:format-tasks": ["edit"],
|
|
35
|
+
"selection:format-codeblock": ["edit"],
|
|
36
|
+
"selection:format-variable": ["edit"],
|
|
37
|
+
// Annotations (the annotation sidecar)
|
|
38
|
+
"selection:highlight-yellow": HIGHLIGHT,
|
|
39
|
+
"selection:highlight-green": HIGHLIGHT,
|
|
40
|
+
"selection:highlight-blue": HIGHLIGHT,
|
|
41
|
+
"selection:highlight-pink": HIGHLIGHT,
|
|
42
|
+
"selection:highlight-purple": HIGHLIGHT,
|
|
43
|
+
"selection:comment": ["read", "edit"],
|
|
44
|
+
"selection:suggest": ["read"],
|
|
45
|
+
"selection:link-record": ["read"],
|
|
46
|
+
// AI — every mode
|
|
47
|
+
"selection:ai": ["read", "edit"],
|
|
48
|
+
"selection:tutor-explain": ["read"],
|
|
49
|
+
"selection:tutor-ask": ["read"],
|
|
50
|
+
// Reporting (every annotated passage: study guides, documents in Annotate)
|
|
51
|
+
"selection:report": ["read"],
|
|
52
|
+
// A chat answer's passage, taken to a new conversation as a comment
|
|
53
|
+
"selection:new-chat": ["read"],
|
|
54
|
+
// Rich-editor tables (the caret or a selection in a table cell)
|
|
55
|
+
"selection:table-row-above": ["edit"],
|
|
56
|
+
"selection:table-row-below": ["edit"],
|
|
57
|
+
"selection:table-col-left": ["edit"],
|
|
58
|
+
"selection:table-col-right": ["edit"],
|
|
59
|
+
"selection:table-align-left": ["edit"],
|
|
60
|
+
"selection:table-align-center": ["edit"],
|
|
61
|
+
"selection:table-align-right": ["edit"],
|
|
62
|
+
"selection:table-del-row": ["edit"],
|
|
63
|
+
"selection:table-del-col": ["edit"],
|
|
64
|
+
"selection:table-del-table": ["edit"],
|
|
65
|
+
// Every selection that has no richer home: copy it, keep it
|
|
66
|
+
"selection:copy": ["read", "edit"],
|
|
67
|
+
"selection:copy-markdown": ["read", "edit"],
|
|
68
|
+
"selection:copy-text": ["read", "edit"],
|
|
69
|
+
"selection:save-to-notes": ["read", "edit"],
|
|
70
|
+
// A selection that reads as rows (a table, a list, Key: value lines, CSV) — SAVE-AS-TABLE-EVERYWHERE
|
|
71
|
+
"selection:save-to-table": ["read", "edit"]
|
|
72
|
+
};
|
|
73
|
+
const SELECTION_HOST_KINDS = {
|
|
74
|
+
annotatedReading: ["annotation", "contextMenuSelection"],
|
|
75
|
+
richEditor: ["richEditor", "contextMenuSelection"],
|
|
76
|
+
plainReading: ["contextMenuSelection"]
|
|
77
|
+
};
|
|
78
|
+
function shownInSelectionMode(actionId, target) {
|
|
79
|
+
const toolbar = selectionToolbarHostOf(target);
|
|
80
|
+
if (!toolbar) return false;
|
|
81
|
+
const rule = SELECTION_ACTION_MODES[actionId];
|
|
82
|
+
if (!rule) return false;
|
|
83
|
+
const modes = typeof rule === "function" ? rule(toolbar.knobs) : rule;
|
|
84
|
+
return modes.includes(toolbar.mode);
|
|
85
|
+
}
|
|
86
|
+
const SELECTION_COMMON_HOST_KEY = "selectionCommon";
|
|
87
|
+
const SELECTION_PRIORITY = {
|
|
88
|
+
edit: [
|
|
89
|
+
"selection:format-bold",
|
|
90
|
+
"selection:format-italic",
|
|
91
|
+
"selection:format-link",
|
|
92
|
+
"selection:ai",
|
|
93
|
+
"selection:comment",
|
|
94
|
+
"selection:table-row-below",
|
|
95
|
+
"selection:table-col-right",
|
|
96
|
+
"selection:copy",
|
|
97
|
+
"selection:format-strike",
|
|
98
|
+
"selection:format-code",
|
|
99
|
+
"selection:format-h1",
|
|
100
|
+
"selection:format-h2",
|
|
101
|
+
"selection:format-quote",
|
|
102
|
+
"selection:format-list",
|
|
103
|
+
"selection:format-variable",
|
|
104
|
+
"selection:highlight-yellow",
|
|
105
|
+
"selection:highlight-green",
|
|
106
|
+
"selection:highlight-blue",
|
|
107
|
+
"selection:highlight-pink",
|
|
108
|
+
"selection:highlight-purple",
|
|
109
|
+
"selection:table-row-above",
|
|
110
|
+
"selection:table-col-left",
|
|
111
|
+
"selection:table-align-left",
|
|
112
|
+
"selection:table-align-center",
|
|
113
|
+
"selection:table-align-right",
|
|
114
|
+
"selection:table-del-row",
|
|
115
|
+
"selection:table-del-col",
|
|
116
|
+
"selection:table-del-table",
|
|
117
|
+
"selection:save-to-notes",
|
|
118
|
+
"selection:save-to-table"
|
|
119
|
+
],
|
|
120
|
+
read: [
|
|
121
|
+
"selection:highlight-yellow",
|
|
122
|
+
"selection:comment",
|
|
123
|
+
"selection:ai",
|
|
124
|
+
"selection:suggest",
|
|
125
|
+
"selection:tutor-explain",
|
|
126
|
+
"selection:highlight-green",
|
|
127
|
+
"selection:highlight-blue",
|
|
128
|
+
"selection:highlight-pink",
|
|
129
|
+
"selection:highlight-purple",
|
|
130
|
+
"selection:tutor-ask",
|
|
131
|
+
"selection:link-record",
|
|
132
|
+
"selection:report",
|
|
133
|
+
"selection:new-chat",
|
|
134
|
+
"selection:copy",
|
|
135
|
+
"selection:save-to-notes",
|
|
136
|
+
"selection:save-to-table"
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
function hostKeyOf(id) {
|
|
140
|
+
if (id.startsWith("selection:format-") || id.startsWith("selection:table-")) return "richEditor";
|
|
141
|
+
if (id === "selection:copy" || id === "selection:save-to-notes" || id === "selection:save-to-table") return SELECTION_COMMON_HOST_KEY;
|
|
142
|
+
if (id === "selection:ai") return "contextMenuSelection";
|
|
143
|
+
if (id.startsWith("selection:tutor-")) return PASSAGE_ACTIONS_HOST_KEY;
|
|
144
|
+
return "annotation";
|
|
145
|
+
}
|
|
146
|
+
function presentAt(id, target) {
|
|
147
|
+
const key = hostKeyOf(id);
|
|
148
|
+
if (key === SELECTION_COMMON_HOST_KEY && id !== "selection:save-to-table") {
|
|
149
|
+
const annotation = hostHalf(target, "annotation");
|
|
150
|
+
if (annotation && (!annotation.capture || annotation.capture({ silent: true }))) return false;
|
|
151
|
+
}
|
|
152
|
+
if (id === "selection:new-chat") {
|
|
153
|
+
const annotation = hostHalf(target, "annotation");
|
|
154
|
+
if (annotation?.source?.token !== "message" || !annotation.source.conversationId) return false;
|
|
155
|
+
}
|
|
156
|
+
if (id.startsWith("selection:table-")) {
|
|
157
|
+
const editor = hostHalf(target, "richEditor");
|
|
158
|
+
if (!editor?.inTable?.()) return false;
|
|
159
|
+
}
|
|
160
|
+
const half = hostHalf(target, key);
|
|
161
|
+
if (!half) return false;
|
|
162
|
+
if (key === PASSAGE_ACTIONS_HOST_KEY) return half.some((a) => a.id === id);
|
|
163
|
+
return shownInSelectionMode(id, target);
|
|
164
|
+
}
|
|
165
|
+
function selectionPlacement(id, target) {
|
|
166
|
+
const toolbar = selectionToolbarHostOf(target);
|
|
167
|
+
if (!toolbar || toolbar.slots === null) return "primary";
|
|
168
|
+
const shown = SELECTION_PRIORITY[toolbar.mode].filter((x) => presentAt(x, target));
|
|
169
|
+
if (shown.length <= toolbar.slots) return "primary";
|
|
170
|
+
const rank = shown.indexOf(id);
|
|
171
|
+
return rank >= 0 && rank < Math.max(1, toolbar.slots - 1) ? "primary" : "overflow";
|
|
172
|
+
}
|
|
173
|
+
function placeSelectionActions(actions, target) {
|
|
174
|
+
return actions.map((a) => a.placement === "overflow" ? a : { ...a, placement: selectionPlacement(a.id, target) });
|
|
175
|
+
}
|
|
176
|
+
const PASSAGE_ACTIONS_HOST_KEY = "passageActions";
|
|
177
|
+
const passageActionsProvider = {
|
|
178
|
+
id: "selection-passage",
|
|
179
|
+
tier: "T0",
|
|
180
|
+
actions: (target) => {
|
|
181
|
+
const list = hostHalf(target, PASSAGE_ACTIONS_HOST_KEY) ?? [];
|
|
182
|
+
return placeSelectionActions(list, target).map((a) => ({
|
|
183
|
+
...a,
|
|
184
|
+
eligible: (t) => shownInSelectionMode(a.id, t) ? a.eligible(t) : { status: "absent" }
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const declared = /* @__PURE__ */ new Map();
|
|
189
|
+
const declaredListeners = /* @__PURE__ */ new Set();
|
|
190
|
+
function declareSelectionProvider(provider) {
|
|
191
|
+
const existing = declared.get(provider.id);
|
|
192
|
+
if (existing === provider) return;
|
|
193
|
+
if (existing && false) {
|
|
194
|
+
throw new Error(`Selection provider "${provider.id}" was declared twice by different modules.`);
|
|
195
|
+
}
|
|
196
|
+
declared.set(provider.id, provider);
|
|
197
|
+
for (const l of [...declaredListeners]) l();
|
|
198
|
+
}
|
|
199
|
+
function declaredSelectionProviders() {
|
|
200
|
+
return [...declared.values()];
|
|
201
|
+
}
|
|
202
|
+
function subscribeDeclaredSelectionProviders(listener) {
|
|
203
|
+
declaredListeners.add(listener);
|
|
204
|
+
return () => {
|
|
205
|
+
declaredListeners.delete(listener);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
declareSelectionProvider(passageActionsProvider);
|
|
209
|
+
const REGISTERED = /* @__PURE__ */ new WeakMap();
|
|
210
|
+
function ensureProvider(registry, provider) {
|
|
211
|
+
let seen = REGISTERED.get(registry);
|
|
212
|
+
if (!seen) {
|
|
213
|
+
seen = /* @__PURE__ */ new Map();
|
|
214
|
+
REGISTERED.set(registry, seen);
|
|
215
|
+
}
|
|
216
|
+
if (seen.has(provider.id)) return;
|
|
217
|
+
seen.set(provider.id, registry.register(provider));
|
|
218
|
+
}
|
|
219
|
+
export {
|
|
220
|
+
COMMENT_SENDS_WITH_NEXT_MESSAGE_DEFAULT,
|
|
221
|
+
COMMENT_SENDS_WITH_NEXT_MESSAGE_KNOB,
|
|
222
|
+
HIGHLIGHT_WHILE_EDITING_DEFAULT,
|
|
223
|
+
HIGHLIGHT_WHILE_EDITING_KNOB,
|
|
224
|
+
PASSAGE_ACTIONS_HOST_KEY,
|
|
225
|
+
SELECTION_ACTION_MODES,
|
|
226
|
+
SELECTION_COMMON_HOST_KEY,
|
|
227
|
+
SELECTION_HOST_KINDS,
|
|
228
|
+
SELECTION_PRIORITY,
|
|
229
|
+
SELECTION_TOOLBAR_KNOB_FEATURE,
|
|
230
|
+
declareSelectionProvider,
|
|
231
|
+
declaredSelectionProviders,
|
|
232
|
+
ensureProvider,
|
|
233
|
+
hostHalf,
|
|
234
|
+
passageActionsProvider,
|
|
235
|
+
placeSelectionActions,
|
|
236
|
+
selectionPlacement,
|
|
237
|
+
selectionToolbarHostOf,
|
|
238
|
+
shownInSelectionMode,
|
|
239
|
+
subscribeDeclaredSelectionProviders
|
|
240
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type CopyFlavor } from "../copy/copy-commands.js";
|
|
2
|
+
/** The live selection's range when it lies over rendered (non-editable) content. */
|
|
3
|
+
export declare function renderedSelectionRange(): Range | null;
|
|
4
|
+
/** The selected DOM as HTML (what the person sees, formatted), links as their author wrote them. */
|
|
5
|
+
export declare function rangeHtml(range: Range): string;
|
|
6
|
+
/**
|
|
7
|
+
* The selected DOM as markdown, through THE editor's HTML → markdown converter
|
|
8
|
+
* (the same one every paste uses), loaded at copy time. Falls back to the
|
|
9
|
+
* shape reader (tables and lists kept) when the converter cannot run.
|
|
10
|
+
*/
|
|
11
|
+
export declare function rangeMarkdown(html: string): Promise<string | null>;
|
|
12
|
+
/** Copy the live rendered selection in one flavor. False when there is no rendered selection. */
|
|
13
|
+
export declare function copyRenderedSelection(flavor: CopyFlavor, fallbackText: string): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* The keyboard half, installed once at the app root (SelectionToolbarRoot):
|
|
16
|
+
* ⌘C on rendered content writes formatted HTML + a markdown text flavor;
|
|
17
|
+
* ⌘⇧C writes readable plain text.
|
|
18
|
+
*/
|
|
19
|
+
export declare function installRichCopyKeys(): () => void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { copyRich } from "@ai-matrx/kit/clipboard";
|
|
2
|
+
import {
|
|
3
|
+
copyRichContent,
|
|
4
|
+
defaultCopyFlavor,
|
|
5
|
+
markdownToReadableText,
|
|
6
|
+
richCopyPlainText,
|
|
7
|
+
writeClipboardFlavors
|
|
8
|
+
} from "../copy/copy-commands.js";
|
|
9
|
+
import { liveSelectionShapeText } from "./selection-shape.js";
|
|
10
|
+
import { announceBareHost, hostCapability } from "../host/registry.js";
|
|
11
|
+
import { stripOwnUtmSource } from "../utils/lifted/url-utm.js";
|
|
12
|
+
function renderedSelectionRange() {
|
|
13
|
+
if (typeof window === "undefined") return null;
|
|
14
|
+
const sel = window.getSelection();
|
|
15
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
|
|
16
|
+
const range = sel.getRangeAt(0);
|
|
17
|
+
const host = range.commonAncestorContainer;
|
|
18
|
+
const el = host.nodeType === 1 ? host : host.parentElement;
|
|
19
|
+
if (!el || el.closest("textarea, input, [contenteditable='true'], [contenteditable=''], .cm-editor")) return null;
|
|
20
|
+
const active = document.activeElement;
|
|
21
|
+
if (active && active.matches("textarea, input, [contenteditable='true']")) return null;
|
|
22
|
+
return range;
|
|
23
|
+
}
|
|
24
|
+
function rangeHtml(range) {
|
|
25
|
+
const box = document.createElement("div");
|
|
26
|
+
box.appendChild(range.cloneContents());
|
|
27
|
+
for (const a of Array.from(box.querySelectorAll("a[href]"))) {
|
|
28
|
+
const href = a.getAttribute("href") ?? "";
|
|
29
|
+
const clean = stripOwnUtmSource(href);
|
|
30
|
+
if (clean !== href) a.setAttribute("href", clean);
|
|
31
|
+
}
|
|
32
|
+
return box.innerHTML;
|
|
33
|
+
}
|
|
34
|
+
async function rangeMarkdown(html) {
|
|
35
|
+
try {
|
|
36
|
+
const load = hostCapability("loadHtmlToMarkdown");
|
|
37
|
+
if (!load) {
|
|
38
|
+
announceBareHost("loadHtmlToMarkdown", "a copied selection keeps its shape text");
|
|
39
|
+
return liveSelectionShapeText();
|
|
40
|
+
}
|
|
41
|
+
const md = (await load())(html);
|
|
42
|
+
return md.trim() ? md : null;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.warn("[copy] the selection could not be read as markdown; copying its shape text instead:", error);
|
|
45
|
+
return liveSelectionShapeText();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function copyRenderedSelection(flavor, fallbackText) {
|
|
49
|
+
const range = renderedSelectionRange();
|
|
50
|
+
const html = range ? rangeHtml(range) : null;
|
|
51
|
+
const markdown = (html ? await rangeMarkdown(html) : null) ?? fallbackText;
|
|
52
|
+
if (flavor === "default" && html) {
|
|
53
|
+
return writeClipboardFlavors(richCopyPlainText(markdown, "default", defaultCopyFlavor()), html);
|
|
54
|
+
}
|
|
55
|
+
return copyRichContent(markdown, flavor, { toast: false });
|
|
56
|
+
}
|
|
57
|
+
function installRichCopyKeys() {
|
|
58
|
+
const onCopy = (event) => {
|
|
59
|
+
if (event.defaultPrevented || !event.clipboardData) return;
|
|
60
|
+
const range = renderedSelectionRange();
|
|
61
|
+
if (!range) return;
|
|
62
|
+
const html = rangeHtml(range);
|
|
63
|
+
const quick = liveSelectionShapeText() ?? range.toString();
|
|
64
|
+
event.clipboardData.setData("text/html", html);
|
|
65
|
+
event.clipboardData.setData("text/plain", quick);
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
void rangeMarkdown(html).then(async (markdown) => {
|
|
68
|
+
if (!markdown || markdown === quick) return;
|
|
69
|
+
const plain = richCopyPlainText(markdown, "default", defaultCopyFlavor());
|
|
70
|
+
const upgraded = await copyRich({ text: plain, html }, { plainFallback: false });
|
|
71
|
+
if (!upgraded) console.warn("[copy] the markdown flavor could not be added to this copy; the shape text stands");
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const onKeyDown = (event) => {
|
|
75
|
+
const mod = event.metaKey || event.ctrlKey;
|
|
76
|
+
if (!mod || !event.shiftKey || event.altKey || event.code !== "KeyC" && event.key.toLowerCase() !== "c") return;
|
|
77
|
+
const range = renderedSelectionRange();
|
|
78
|
+
if (!range) return;
|
|
79
|
+
event.preventDefault();
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
const html = rangeHtml(range);
|
|
82
|
+
const fallback = liveSelectionShapeText() ?? range.toString();
|
|
83
|
+
void rangeMarkdown(html).then((markdown) => writeClipboardFlavors(markdownToReadableText(markdown ?? fallback)));
|
|
84
|
+
};
|
|
85
|
+
document.addEventListener("copy", onCopy);
|
|
86
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
87
|
+
return () => {
|
|
88
|
+
document.removeEventListener("copy", onCopy);
|
|
89
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
copyRenderedSelection,
|
|
94
|
+
installRichCopyKeys,
|
|
95
|
+
rangeHtml,
|
|
96
|
+
rangeMarkdown,
|
|
97
|
+
renderedSelectionRange
|
|
98
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function liveSelectionShapeText(): string | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { shapeTextOfNode } from "@ai-matrx/records-ui/table-shape";
|
|
2
|
+
function liveSelectionShapeText() {
|
|
3
|
+
if (typeof window === "undefined") return null;
|
|
4
|
+
const sel = window.getSelection();
|
|
5
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
|
|
6
|
+
const range = sel.getRangeAt(0);
|
|
7
|
+
const host = range.commonAncestorContainer;
|
|
8
|
+
const el = host.nodeType === 1 ? host : host.parentElement;
|
|
9
|
+
if (el?.closest("textarea, input, [contenteditable='true']")) return null;
|
|
10
|
+
const text = shapeTextOfNode(range.cloneContents());
|
|
11
|
+
return text.trim() ? text : null;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
liveSelectionShapeText
|
|
15
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Editing shows formatting; reading shows highlight / suggest / link. */
|
|
3
|
+
export type SelectionMode = "edit" | "read";
|
|
4
|
+
/** What a running action (or a panel) may do to the toolbar. */
|
|
5
|
+
export interface SelectionToolbarUi {
|
|
6
|
+
/** Swap the strip for one of the zone's panels (a comment composer), handing it `payload`. */
|
|
7
|
+
openPanel(panel: string, payload?: unknown): void;
|
|
8
|
+
/** Back to the strip. */
|
|
9
|
+
closePanel(): void;
|
|
10
|
+
/** Close the toolbar. `clearSelection` also drops the text selection. */
|
|
11
|
+
close(options?: {
|
|
12
|
+
clearSelection?: boolean;
|
|
13
|
+
}): void;
|
|
14
|
+
}
|
|
15
|
+
export interface SelectionZoneContribution {
|
|
16
|
+
/**
|
|
17
|
+
* This zone's halves of the composite ClickTarget host, keyed by provider
|
|
18
|
+
* (`{ annotation: … }`, `{ richEditor: … }`). Providers narrow their own key.
|
|
19
|
+
*/
|
|
20
|
+
host?: Record<string, unknown>;
|
|
21
|
+
/** The text here is being edited (a rich editor, an editable field). */
|
|
22
|
+
editable?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Stop here: zones OUTSIDE this one contribute nothing for a selection in
|
|
25
|
+
* it, and when nothing deeper contributes the toolbar stays closed (a
|
|
26
|
+
* surface that owns its own passage UI, e.g. a chat answer's action bar).
|
|
27
|
+
*/
|
|
28
|
+
suppress?: boolean;
|
|
29
|
+
/** Panels this zone draws inside the toolbar frame. Null = not this zone's. */
|
|
30
|
+
renderPanel?(panel: string, ui: SelectionToolbarUi, payload: unknown): ReactNode | null;
|
|
31
|
+
/**
|
|
32
|
+
* CARET MODE: where to anchor the toolbar when the selection is only a caret
|
|
33
|
+
* here, or null to stay closed. The rich editor answers with its table's box
|
|
34
|
+
* while the caret is in a table (the table actions are toolbar actions).
|
|
35
|
+
*/
|
|
36
|
+
caretAnchor?(): {
|
|
37
|
+
left: number;
|
|
38
|
+
top: number;
|
|
39
|
+
bottom: number;
|
|
40
|
+
width: number;
|
|
41
|
+
} | null;
|
|
42
|
+
/** The text a caret-mode toolbar acts on (a selected code block's source), if any. */
|
|
43
|
+
caretText?(): string;
|
|
44
|
+
/** A panel (and its payload) to show immediately when the toolbar opens here (a pending reattach). */
|
|
45
|
+
initialPanel?(): {
|
|
46
|
+
panel: string;
|
|
47
|
+
payload?: unknown;
|
|
48
|
+
} | null;
|
|
49
|
+
/**
|
|
50
|
+
* A zone whose own state decides what its actions can do (an editor's model selection, which
|
|
51
|
+
* the editor syncs a beat AFTER the DOM selection the toolbar reads) calls `onChange` when that
|
|
52
|
+
* state moves; the open toolbar then re-resolves its actions. Returns the unsubscribe.
|
|
53
|
+
*/
|
|
54
|
+
subscribe?(onChange: () => void): () => void;
|
|
55
|
+
}
|
|
56
|
+
export declare function subscribeSelectionZones(listener: () => void): () => void;
|
|
57
|
+
export declare function selectionZonesVersion(): number;
|
|
58
|
+
/** Re-render when zones mount/unmount (the root re-evaluates an open toolbar). */
|
|
59
|
+
export declare function useSelectionZonesVersion(): number;
|
|
60
|
+
/** Tell the root a zone's contribution changed (a pending reattach started). */
|
|
61
|
+
export declare function notifySelectionZonesChanged(): void;
|
|
62
|
+
export interface ResolvedZone {
|
|
63
|
+
id: string;
|
|
64
|
+
element: HTMLElement;
|
|
65
|
+
contribution: SelectionZoneContribution;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Every zone holding `node`, deepest first, cut at the first `suppress` zone
|
|
69
|
+
* (which itself contributes nothing).
|
|
70
|
+
*/
|
|
71
|
+
export declare function zonesContaining(node: Node | null): ResolvedZone[];
|
|
72
|
+
/**
|
|
73
|
+
* Register `element` as a selection zone for as long as the caller is mounted.
|
|
74
|
+
* The contribution is read fresh at every selection (a ref), so passing a new
|
|
75
|
+
* object each render never re-registers.
|
|
76
|
+
*/
|
|
77
|
+
export declare function useSelectionZone(element: HTMLElement | null, contribution: SelectionZoneContribution | null): void;
|
|
78
|
+
/** A collapsed caret inside a contenteditable, measured on demand. */
|
|
79
|
+
export interface EditableCaret {
|
|
80
|
+
/** The node the caret sits in. */
|
|
81
|
+
node: Node;
|
|
82
|
+
/** The contenteditable element (or its descendant) holding it. */
|
|
83
|
+
host: Element;
|
|
84
|
+
/** Where the caret is on screen now. */
|
|
85
|
+
rect: () => DOMRect | null;
|
|
86
|
+
}
|
|
87
|
+
/** Hear every caret move inside an editable surface. Returns the unsubscribe. */
|
|
88
|
+
export declare function subscribeEditableCaret(listener: (caret: EditableCaret) => void): () => void;
|
|
89
|
+
/** The root calls this on every `selectionchange`. */
|
|
90
|
+
export declare function publishEditableCaret(): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useId, useLayoutEffect, useRef, useSyncExternalStore } from "react";
|
|
3
|
+
const zones = /* @__PURE__ */ new Map();
|
|
4
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
5
|
+
let version = 0;
|
|
6
|
+
function notify() {
|
|
7
|
+
version += 1;
|
|
8
|
+
for (const l of [...listeners]) l();
|
|
9
|
+
}
|
|
10
|
+
function subscribeSelectionZones(listener) {
|
|
11
|
+
listeners.add(listener);
|
|
12
|
+
return () => {
|
|
13
|
+
listeners.delete(listener);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function selectionZonesVersion() {
|
|
17
|
+
return version;
|
|
18
|
+
}
|
|
19
|
+
function useSelectionZonesVersion() {
|
|
20
|
+
return useSyncExternalStore(subscribeSelectionZones, selectionZonesVersion, () => 0);
|
|
21
|
+
}
|
|
22
|
+
function notifySelectionZonesChanged() {
|
|
23
|
+
notify();
|
|
24
|
+
}
|
|
25
|
+
function zonesContaining(node) {
|
|
26
|
+
if (!node) return [];
|
|
27
|
+
const hits = [];
|
|
28
|
+
for (const z of zones.values()) {
|
|
29
|
+
if (z.element.isConnected && z.element.contains(node)) {
|
|
30
|
+
hits.push({ id: z.id, element: z.element, contribution: z.read() });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
hits.sort((a, b) => a.element === b.element ? 0 : a.element.contains(b.element) ? 1 : -1);
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const z of hits) {
|
|
36
|
+
if (z.contribution.suppress) break;
|
|
37
|
+
out.push(z);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function useSelectionZone(element, contribution) {
|
|
42
|
+
const id = useId();
|
|
43
|
+
const latest = useRef(contribution);
|
|
44
|
+
useLayoutEffect(() => {
|
|
45
|
+
latest.current = contribution;
|
|
46
|
+
});
|
|
47
|
+
const enabled = contribution !== null;
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!element || !enabled) return;
|
|
50
|
+
zones.set(id, { id, element, read: () => latest.current ?? {} });
|
|
51
|
+
notify();
|
|
52
|
+
return () => {
|
|
53
|
+
zones.delete(id);
|
|
54
|
+
notify();
|
|
55
|
+
};
|
|
56
|
+
}, [id, element, enabled]);
|
|
57
|
+
}
|
|
58
|
+
const caretListeners = /* @__PURE__ */ new Set();
|
|
59
|
+
function subscribeEditableCaret(listener) {
|
|
60
|
+
caretListeners.add(listener);
|
|
61
|
+
return () => {
|
|
62
|
+
caretListeners.delete(listener);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function publishEditableCaret() {
|
|
66
|
+
if (caretListeners.size === 0) return;
|
|
67
|
+
const sel = document.getSelection();
|
|
68
|
+
if (!sel || sel.rangeCount === 0 || !sel.isCollapsed) return;
|
|
69
|
+
const node = sel.anchorNode;
|
|
70
|
+
const host = node instanceof Element ? node : node?.parentElement;
|
|
71
|
+
if (!node || !host?.closest("[contenteditable='true'], [contenteditable='']")) return;
|
|
72
|
+
const range = sel.getRangeAt(0);
|
|
73
|
+
const caret = { node, host, rect: () => range.getClientRects()[0] ?? range.getBoundingClientRect() };
|
|
74
|
+
for (const l of [...caretListeners]) l(caret);
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
notifySelectionZonesChanged,
|
|
78
|
+
publishEditableCaret,
|
|
79
|
+
selectionZonesVersion,
|
|
80
|
+
subscribeEditableCaret,
|
|
81
|
+
subscribeSelectionZones,
|
|
82
|
+
useSelectionZone,
|
|
83
|
+
useSelectionZonesVersion,
|
|
84
|
+
zonesContaining
|
|
85
|
+
};
|