@sigx/lynx-richtext 0.4.7
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 +95 -0
- package/android/com/sigx/richtext/DocumentMapper.kt +148 -0
- package/android/com/sigx/richtext/RichEditText.kt +41 -0
- package/android/com/sigx/richtext/SigxRichTextBehavior.kt +18 -0
- package/android/com/sigx/richtext/SigxRichTextUI.kt +537 -0
- package/android/com/sigx/richtext/SigxSpans.kt +87 -0
- package/dist/RichTextInput.d.ts +20 -0
- package/dist/RichTextInput.js +41 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- package/dist/jsx-augment.d.ts +48 -0
- package/dist/jsx-augment.js +9 -0
- package/dist/methods.d.ts +40 -0
- package/dist/methods.js +57 -0
- package/dist/model/codec.d.ts +23 -0
- package/dist/model/codec.js +123 -0
- package/dist/model/types.d.ts +113 -0
- package/dist/model/types.js +23 -0
- package/ios/DocumentMapper.swift +252 -0
- package/ios/RichTextView.swift +132 -0
- package/ios/SigxRichTextUI.swift +566 -0
- package/package.json +63 -0
- package/signalx-module.json +18 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import './jsx-augment.js';
|
|
2
|
+
export { RichTextInput } from './RichTextInput.js';
|
|
3
|
+
export type { RichTextInputProps } from './RichTextInput.js';
|
|
4
|
+
export { RichTextMethods } from './methods.js';
|
|
5
|
+
export type { RichTextHandle } from './methods.js';
|
|
6
|
+
export { encodeDoc, decodeDoc, docEquals, normalizeDoc, emptyDoc } from './model/codec.js';
|
|
7
|
+
export type { RichDoc, InlineSpan, InlineSpanType, BlockAttr, BlockAttrType, SelectionState, RichTextChangeEvent, RichTextSelectionEvent, RichTextHeightChangeEvent, RichTextFocusEvent, } from './model/types.js';
|
|
8
|
+
export type { SigxRichTextAttributes } from './jsx-augment.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSX intrinsic type augmentation for the native `<sigx-richtext>` element.
|
|
3
|
+
*
|
|
4
|
+
* Registered natively via the autolinker (`signalx-module.json` →
|
|
5
|
+
* `ios.uiComponents` / `android.behaviors`). Importing this module (pulled in
|
|
6
|
+
* by the package entry point) declares the tag and its typed attribute/event
|
|
7
|
+
* surface.
|
|
8
|
+
*/
|
|
9
|
+
import type { LynxCommonAttributes, LynxEventHandler } from '@sigx/lynx-runtime';
|
|
10
|
+
import type { RichTextChangeEvent, RichTextFocusEvent, RichTextHeightChangeEvent, RichTextSelectionEvent } from './model/types.js';
|
|
11
|
+
export interface SigxRichTextAttributes extends LynxCommonAttributes {
|
|
12
|
+
/**
|
|
13
|
+
* Initial document as a JSON-encoded `RichDoc` (`encodeDoc`). Initial-only
|
|
14
|
+
* once the user has edited — programmatic replacements must go through the
|
|
15
|
+
* `setDocument` UI method (see `RichTextMethods`).
|
|
16
|
+
*/
|
|
17
|
+
value?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
editable?: boolean;
|
|
20
|
+
/** Auto-grow floor, px. */
|
|
21
|
+
'min-height'?: number;
|
|
22
|
+
/** Auto-grow ceiling, px — content beyond this scrolls internally. */
|
|
23
|
+
'max-height'?: number;
|
|
24
|
+
/** Base font size, px (headings scale from this). */
|
|
25
|
+
'font-size'?: number;
|
|
26
|
+
/** Base text color (hex). */
|
|
27
|
+
'text-color'?: string;
|
|
28
|
+
/** Caret tint + link color (hex). */
|
|
29
|
+
'accent-color'?: string;
|
|
30
|
+
/** Placeholder text color (hex). */
|
|
31
|
+
'placeholder-color'?: string;
|
|
32
|
+
/** Native confirm/return key type. */
|
|
33
|
+
'confirm-type'?: 'send' | 'search' | 'next' | 'go' | 'done';
|
|
34
|
+
/** Focus + raise the keyboard on mount. */
|
|
35
|
+
'auto-focus'?: boolean;
|
|
36
|
+
bindchange?: LynxEventHandler<RichTextChangeEvent>;
|
|
37
|
+
bindselection?: LynxEventHandler<RichTextSelectionEvent>;
|
|
38
|
+
bindheightchange?: LynxEventHandler<RichTextHeightChangeEvent>;
|
|
39
|
+
bindfocus?: LynxEventHandler<RichTextFocusEvent>;
|
|
40
|
+
bindblur?: LynxEventHandler<RichTextFocusEvent>;
|
|
41
|
+
}
|
|
42
|
+
declare global {
|
|
43
|
+
namespace JSX {
|
|
44
|
+
interface IntrinsicElements {
|
|
45
|
+
'sigx-richtext': SigxRichTextAttributes;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSX intrinsic type augmentation for the native `<sigx-richtext>` element.
|
|
3
|
+
*
|
|
4
|
+
* Registered natively via the autolinker (`signalx-module.json` →
|
|
5
|
+
* `ios.uiComponents` / `android.behaviors`). Importing this module (pulled in
|
|
6
|
+
* by the package entry point) declares the tag and its typed attribute/event
|
|
7
|
+
* surface.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-thread command surface for `<sigx-richtext>`.
|
|
3
|
+
*
|
|
4
|
+
* Commands ride the `INVOKE_UI_METHOD` op (BG→MT, fire-and-forget — landed in
|
|
5
|
+
* lynx-runtime#145): no `main-thread:ref` worklet plumbing is needed. Return
|
|
6
|
+
* values are deliberately not part of this surface — state reconciles through
|
|
7
|
+
* the element's `bindchange`/`bindselection` events, which is the editor's
|
|
8
|
+
* single source of truth.
|
|
9
|
+
*
|
|
10
|
+
* The element handle is the ShadowElement delivered by a callback `ref`:
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* let el: RichTextHandle = null;
|
|
14
|
+
* <sigx-richtext ref={(e) => { el = e; }} … />
|
|
15
|
+
* RichTextMethods.toggleFormat(el, 'bold');
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import type { BlockAttrType, InlineSpanType, RichDoc } from './model/types.js';
|
|
19
|
+
/** Minimal structural handle — the BG ShadowElement from a callback `ref`. */
|
|
20
|
+
export type RichTextHandle = {
|
|
21
|
+
id: number;
|
|
22
|
+
} | null | undefined;
|
|
23
|
+
export declare const RichTextMethods: {
|
|
24
|
+
/**
|
|
25
|
+
* Replace the document. Carries the version the write was based on —
|
|
26
|
+
* native drops stale writes (`doc.v < localVersion`) and re-emits current
|
|
27
|
+
* state, and rejects writes during an active IME composition.
|
|
28
|
+
*/
|
|
29
|
+
readonly setDocument: (el: RichTextHandle, doc: RichDoc) => void;
|
|
30
|
+
/** Toggle an inline format over the current selection (collapsed → flips typing attributes). */
|
|
31
|
+
readonly toggleFormat: (el: RichTextHandle, type: InlineSpanType) => void;
|
|
32
|
+
/** Set the block type of the paragraph(s) covering the current selection. */
|
|
33
|
+
readonly setBlockType: (el: RichTextHandle, type: BlockAttrType, level?: number) => void;
|
|
34
|
+
/** Insert text at the caret (inherits typing attributes). */
|
|
35
|
+
readonly insertText: (el: RichTextHandle, text: string) => void;
|
|
36
|
+
/** Move/extend the caret. */
|
|
37
|
+
readonly setSelectionRange: (el: RichTextHandle, start: number, end: number) => void;
|
|
38
|
+
readonly focus: (el: RichTextHandle) => void;
|
|
39
|
+
readonly blur: (el: RichTextHandle) => void;
|
|
40
|
+
};
|
package/dist/methods.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background-thread command surface for `<sigx-richtext>`.
|
|
3
|
+
*
|
|
4
|
+
* Commands ride the `INVOKE_UI_METHOD` op (BG→MT, fire-and-forget — landed in
|
|
5
|
+
* lynx-runtime#145): no `main-thread:ref` worklet plumbing is needed. Return
|
|
6
|
+
* values are deliberately not part of this surface — state reconciles through
|
|
7
|
+
* the element's `bindchange`/`bindselection` events, which is the editor's
|
|
8
|
+
* single source of truth.
|
|
9
|
+
*
|
|
10
|
+
* The element handle is the ShadowElement delivered by a callback `ref`:
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* let el: RichTextHandle = null;
|
|
14
|
+
* <sigx-richtext ref={(e) => { el = e; }} … />
|
|
15
|
+
* RichTextMethods.toggleFormat(el, 'bold');
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { OP, pushOp, scheduleFlush } from '@sigx/lynx';
|
|
19
|
+
import { encodeDoc } from './model/codec.js';
|
|
20
|
+
function invoke(el, method, params) {
|
|
21
|
+
if (!el)
|
|
22
|
+
return;
|
|
23
|
+
pushOp(OP.INVOKE_UI_METHOD, el.id, method, params);
|
|
24
|
+
scheduleFlush();
|
|
25
|
+
}
|
|
26
|
+
export const RichTextMethods = {
|
|
27
|
+
/**
|
|
28
|
+
* Replace the document. Carries the version the write was based on —
|
|
29
|
+
* native drops stale writes (`doc.v < localVersion`) and re-emits current
|
|
30
|
+
* state, and rejects writes during an active IME composition.
|
|
31
|
+
*/
|
|
32
|
+
setDocument(el, doc) {
|
|
33
|
+
invoke(el, 'setDocument', { doc: encodeDoc(doc) });
|
|
34
|
+
},
|
|
35
|
+
/** Toggle an inline format over the current selection (collapsed → flips typing attributes). */
|
|
36
|
+
toggleFormat(el, type) {
|
|
37
|
+
invoke(el, 'toggleFormat', { type });
|
|
38
|
+
},
|
|
39
|
+
/** Set the block type of the paragraph(s) covering the current selection. */
|
|
40
|
+
setBlockType(el, type, level) {
|
|
41
|
+
invoke(el, 'setBlockType', { type, ...(level !== undefined ? { level } : {}) });
|
|
42
|
+
},
|
|
43
|
+
/** Insert text at the caret (inherits typing attributes). */
|
|
44
|
+
insertText(el, text) {
|
|
45
|
+
invoke(el, 'insertText', { text });
|
|
46
|
+
},
|
|
47
|
+
/** Move/extend the caret. */
|
|
48
|
+
setSelectionRange(el, start, end) {
|
|
49
|
+
invoke(el, 'setSelectionRange', { start, end });
|
|
50
|
+
},
|
|
51
|
+
focus(el) {
|
|
52
|
+
invoke(el, 'focus', {});
|
|
53
|
+
},
|
|
54
|
+
blur(el) {
|
|
55
|
+
invoke(el, 'blur', {});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single JSON codec for {@link RichDoc} — used by the `value` prop,
|
|
3
|
+
* `setDocument`/`getDocument`, and `bindchange` payloads, so there is exactly
|
|
4
|
+
* one wire schema and one validation point.
|
|
5
|
+
*
|
|
6
|
+
* `decodeDoc` is defensive: malformed input degrades to an empty document
|
|
7
|
+
* rather than throwing (events from native should never crash the BG thread).
|
|
8
|
+
*/
|
|
9
|
+
import type { RichDoc } from './types.js';
|
|
10
|
+
export declare function emptyDoc(v?: number): RichDoc;
|
|
11
|
+
export declare function encodeDoc(doc: RichDoc): string;
|
|
12
|
+
export declare function decodeDoc(raw: string | null | undefined): RichDoc;
|
|
13
|
+
/**
|
|
14
|
+
* Structural equality, ignoring `v` — the echo-suppression comparison.
|
|
15
|
+
* (Two docs with identical content but different versions are "equal": the
|
|
16
|
+
* version exists to order writes, not to distinguish content.)
|
|
17
|
+
*/
|
|
18
|
+
export declare function docEquals(a: RichDoc, b: RichDoc): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a doc so structurally-identical content compares equal regardless
|
|
21
|
+
* of producer ordering: spans sorted by (start, end, type), blocks by start.
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeDoc(doc: RichDoc): RichDoc;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single JSON codec for {@link RichDoc} — used by the `value` prop,
|
|
3
|
+
* `setDocument`/`getDocument`, and `bindchange` payloads, so there is exactly
|
|
4
|
+
* one wire schema and one validation point.
|
|
5
|
+
*
|
|
6
|
+
* `decodeDoc` is defensive: malformed input degrades to an empty document
|
|
7
|
+
* rather than throwing (events from native should never crash the BG thread).
|
|
8
|
+
*/
|
|
9
|
+
export function emptyDoc(v = 0) {
|
|
10
|
+
return { text: '', spans: [], blocks: [], v };
|
|
11
|
+
}
|
|
12
|
+
export function encodeDoc(doc) {
|
|
13
|
+
return JSON.stringify(doc);
|
|
14
|
+
}
|
|
15
|
+
export function decodeDoc(raw) {
|
|
16
|
+
if (!raw)
|
|
17
|
+
return emptyDoc();
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
const text = typeof parsed.text === 'string' ? parsed.text : '';
|
|
21
|
+
return {
|
|
22
|
+
text,
|
|
23
|
+
spans: sanitizeSpans(parsed.spans, text.length),
|
|
24
|
+
blocks: sanitizeBlocks(parsed.blocks, text.length),
|
|
25
|
+
v: typeof parsed.v === 'number' && Number.isFinite(parsed.v) ? parsed.v : 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return emptyDoc();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function sanitizeSpans(spans, max) {
|
|
33
|
+
if (!Array.isArray(spans))
|
|
34
|
+
return [];
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const s of spans) {
|
|
37
|
+
if (!s || typeof s.start !== 'number' || typeof s.end !== 'number' || !s.type)
|
|
38
|
+
continue;
|
|
39
|
+
const start = clamp(s.start, 0, max);
|
|
40
|
+
const end = clamp(s.end, 0, max);
|
|
41
|
+
if (end <= start)
|
|
42
|
+
continue;
|
|
43
|
+
out.push({ start, end, type: s.type, ...(s.attrs ? { attrs: s.attrs } : {}) });
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function sanitizeBlocks(blocks, max) {
|
|
48
|
+
if (!Array.isArray(blocks))
|
|
49
|
+
return [];
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const b of blocks) {
|
|
52
|
+
if (!b || typeof b.start !== 'number' || typeof b.end !== 'number' || !b.type)
|
|
53
|
+
continue;
|
|
54
|
+
const start = clamp(b.start, 0, max);
|
|
55
|
+
const end = clamp(b.end, 0, max);
|
|
56
|
+
if (end < start)
|
|
57
|
+
continue;
|
|
58
|
+
out.push({
|
|
59
|
+
start,
|
|
60
|
+
end,
|
|
61
|
+
type: b.type,
|
|
62
|
+
...(b.level !== undefined ? { level: b.level } : {}),
|
|
63
|
+
...(b.checked !== undefined ? { checked: b.checked } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function clamp(n, lo, hi) {
|
|
69
|
+
return Math.min(hi, Math.max(lo, Math.floor(n)));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Structural equality, ignoring `v` — the echo-suppression comparison.
|
|
73
|
+
* (Two docs with identical content but different versions are "equal": the
|
|
74
|
+
* version exists to order writes, not to distinguish content.)
|
|
75
|
+
*/
|
|
76
|
+
export function docEquals(a, b) {
|
|
77
|
+
if (a === b)
|
|
78
|
+
return true;
|
|
79
|
+
if (a.text !== b.text)
|
|
80
|
+
return false;
|
|
81
|
+
if (a.spans.length !== b.spans.length || a.blocks.length !== b.blocks.length)
|
|
82
|
+
return false;
|
|
83
|
+
for (let i = 0; i < a.spans.length; i++) {
|
|
84
|
+
const x = a.spans[i];
|
|
85
|
+
const y = b.spans[i];
|
|
86
|
+
if (x.start !== y.start || x.end !== y.end || x.type !== y.type)
|
|
87
|
+
return false;
|
|
88
|
+
if (!attrsEqual(x.attrs, y.attrs))
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
for (let i = 0; i < a.blocks.length; i++) {
|
|
92
|
+
const x = a.blocks[i];
|
|
93
|
+
const y = b.blocks[i];
|
|
94
|
+
if (x.start !== y.start || x.end !== y.end || x.type !== y.type ||
|
|
95
|
+
x.level !== y.level || x.checked !== y.checked)
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
function attrsEqual(a, b) {
|
|
101
|
+
if (a === b)
|
|
102
|
+
return true;
|
|
103
|
+
const ka = a ? Object.keys(a) : [];
|
|
104
|
+
const kb = b ? Object.keys(b) : [];
|
|
105
|
+
if (ka.length !== kb.length)
|
|
106
|
+
return false;
|
|
107
|
+
for (const k of ka)
|
|
108
|
+
if (a[k] !== b?.[k])
|
|
109
|
+
return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Normalize a doc so structurally-identical content compares equal regardless
|
|
114
|
+
* of producer ordering: spans sorted by (start, end, type), blocks by start.
|
|
115
|
+
*/
|
|
116
|
+
export function normalizeDoc(doc) {
|
|
117
|
+
return {
|
|
118
|
+
text: doc.text,
|
|
119
|
+
spans: [...doc.spans].sort((a, b) => a.start - b.start || a.end - b.end || a.type.localeCompare(b.type)),
|
|
120
|
+
blocks: [...doc.blocks].sort((a, b) => a.start - b.start),
|
|
121
|
+
v: doc.v,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rich document model — the single data shape that crosses the JS↔native
|
|
3
|
+
* bridge for `<sigx-richtext>`.
|
|
4
|
+
*
|
|
5
|
+
* Design invariants (load-bearing — native code on both platforms relies on
|
|
6
|
+
* them):
|
|
7
|
+
*
|
|
8
|
+
* - **Flat text + ranges, not a tree.** `NSAttributedString` (iOS) and
|
|
9
|
+
* `Spannable` (Android) are natively a flat string with attribute ranges, so
|
|
10
|
+
* this model maps 1:1 onto the platform primitives with no tree walking.
|
|
11
|
+
* - **UTF-16 code-unit offsets everywhere.** JS strings, `NSRange`, and
|
|
12
|
+
* Android `CharSequence` all index by UTF-16 code units, so no index
|
|
13
|
+
* translation happens at any boundary. (Surrogate pairs — emoji — count
|
|
14
|
+
* as 2; producers must never split one.)
|
|
15
|
+
* - **`blocks` ranges align to line boundaries** (`\n`-separated). Producers
|
|
16
|
+
* normalize; native re-snaps defensively.
|
|
17
|
+
* - **Native never parses markdown.** Markdown ↔ RichDoc conversion lives in
|
|
18
|
+
* `@sigx/lynx-markdown`; this package is markdown-agnostic.
|
|
19
|
+
* - **`v` is a monotonic version.** Every native-side user edit bumps it;
|
|
20
|
+
* `setDocument` carries the version the write was based on so native can
|
|
21
|
+
* drop stale writes (see the IME/echo rules in the package README).
|
|
22
|
+
*/
|
|
23
|
+
/** Inline character-range formats. `link` carries `attrs.href`; `mention` is an atomic chip with `attrs.id`/`attrs.label`. */
|
|
24
|
+
export type InlineSpanType = 'bold' | 'italic' | 'strike' | 'code' | 'link' | 'mention';
|
|
25
|
+
export interface InlineSpan {
|
|
26
|
+
/** Inclusive start, UTF-16 code units. */
|
|
27
|
+
start: number;
|
|
28
|
+
/** Exclusive end, UTF-16 code units. */
|
|
29
|
+
end: number;
|
|
30
|
+
type: InlineSpanType;
|
|
31
|
+
/** Type-specific payload (`href` for links, `id`/`label` for mentions). */
|
|
32
|
+
attrs?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Paragraph-level block types. MVP renders `paragraph` + `heading`; the rest
|
|
36
|
+
* are reserved (P2/P3). `raw` is a consumer escape hatch (e.g. lynx-markdown
|
|
37
|
+
* keeps unmodeled markdown source verbatim in a raw block) — native renders it
|
|
38
|
+
* as a plain paragraph and round-trips the attr untouched.
|
|
39
|
+
*/
|
|
40
|
+
export type BlockAttrType = 'paragraph' | 'heading' | 'bullet' | 'ordered' | 'task' | 'blockquote' | 'codeBlock' | 'raw';
|
|
41
|
+
export interface BlockAttr {
|
|
42
|
+
/** Inclusive start of the paragraph's char range (line boundary). */
|
|
43
|
+
start: number;
|
|
44
|
+
/** Exclusive end (line boundary / end of text). */
|
|
45
|
+
end: number;
|
|
46
|
+
type: BlockAttrType;
|
|
47
|
+
/** Heading level 1–6 (heading only). */
|
|
48
|
+
level?: number;
|
|
49
|
+
/** Task checkbox state (task only). */
|
|
50
|
+
checked?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface RichDoc {
|
|
53
|
+
text: string;
|
|
54
|
+
spans: InlineSpan[];
|
|
55
|
+
blocks: BlockAttr[];
|
|
56
|
+
/** Monotonic document version (see module docs). */
|
|
57
|
+
v: number;
|
|
58
|
+
}
|
|
59
|
+
/** `bindchange` — fired after every user edit (and after applied programmatic mutations). */
|
|
60
|
+
export interface RichTextChangeEvent {
|
|
61
|
+
type: 'change';
|
|
62
|
+
detail: {
|
|
63
|
+
/** JSON-encoded {@link RichDoc} (decode with `decodeDoc`). */
|
|
64
|
+
doc: string;
|
|
65
|
+
/** True while an IME composition session is active — do NOT echo writes back. */
|
|
66
|
+
isComposing: boolean;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** `bindselection` — caret/selection moved. Drives toolbar active state + popup anchoring. */
|
|
70
|
+
export interface RichTextSelectionEvent {
|
|
71
|
+
type: 'selection';
|
|
72
|
+
detail: {
|
|
73
|
+
start: number;
|
|
74
|
+
end: number;
|
|
75
|
+
/** Comma-separated inline formats covering the selection (or the typing attributes when collapsed), e.g. `"bold,italic"`. */
|
|
76
|
+
activeFormats: string;
|
|
77
|
+
/** Block type of the caret's paragraph. */
|
|
78
|
+
activeBlock: string;
|
|
79
|
+
/** Heading level when `activeBlock === 'heading'`. */
|
|
80
|
+
headingLevel?: number;
|
|
81
|
+
/** Caret rectangle in the element's own coordinate space (popup anchoring). */
|
|
82
|
+
caretX: number;
|
|
83
|
+
caretY: number;
|
|
84
|
+
caretHeight: number;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** `bindheightchange` — intrinsic content height changed (auto-grow). */
|
|
88
|
+
export interface RichTextHeightChangeEvent {
|
|
89
|
+
type: 'heightchange';
|
|
90
|
+
detail: {
|
|
91
|
+
/** Content height in px (may exceed the clamped frame height). */
|
|
92
|
+
height: number;
|
|
93
|
+
/** Line count. */
|
|
94
|
+
lines: number;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export interface RichTextFocusEvent {
|
|
98
|
+
type: 'focus' | 'blur';
|
|
99
|
+
detail: Record<string, never>;
|
|
100
|
+
}
|
|
101
|
+
/** Parsed form of `bindselection`'s detail (after `activeFormats` is split). */
|
|
102
|
+
export interface SelectionState {
|
|
103
|
+
start: number;
|
|
104
|
+
end: number;
|
|
105
|
+
activeFormats: InlineSpanType[];
|
|
106
|
+
activeBlock: BlockAttrType;
|
|
107
|
+
headingLevel?: number;
|
|
108
|
+
caretRect: {
|
|
109
|
+
x: number;
|
|
110
|
+
y: number;
|
|
111
|
+
height: number;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rich document model — the single data shape that crosses the JS↔native
|
|
3
|
+
* bridge for `<sigx-richtext>`.
|
|
4
|
+
*
|
|
5
|
+
* Design invariants (load-bearing — native code on both platforms relies on
|
|
6
|
+
* them):
|
|
7
|
+
*
|
|
8
|
+
* - **Flat text + ranges, not a tree.** `NSAttributedString` (iOS) and
|
|
9
|
+
* `Spannable` (Android) are natively a flat string with attribute ranges, so
|
|
10
|
+
* this model maps 1:1 onto the platform primitives with no tree walking.
|
|
11
|
+
* - **UTF-16 code-unit offsets everywhere.** JS strings, `NSRange`, and
|
|
12
|
+
* Android `CharSequence` all index by UTF-16 code units, so no index
|
|
13
|
+
* translation happens at any boundary. (Surrogate pairs — emoji — count
|
|
14
|
+
* as 2; producers must never split one.)
|
|
15
|
+
* - **`blocks` ranges align to line boundaries** (`\n`-separated). Producers
|
|
16
|
+
* normalize; native re-snaps defensively.
|
|
17
|
+
* - **Native never parses markdown.** Markdown ↔ RichDoc conversion lives in
|
|
18
|
+
* `@sigx/lynx-markdown`; this package is markdown-agnostic.
|
|
19
|
+
* - **`v` is a monotonic version.** Every native-side user edit bumps it;
|
|
20
|
+
* `setDocument` carries the version the write was based on so native can
|
|
21
|
+
* drop stale writes (see the IME/echo rules in the package README).
|
|
22
|
+
*/
|
|
23
|
+
export {};
|