@octane-xplat/richtext 0.0.1 → 0.9.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 ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Alec Larson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # @octane-xplat/richtext
1
+ # `@octane-xplat/richtext`
2
2
 
3
- Version 0.0.1 is a reservation stub so the automated release can publish this package name. It has no supported API. Use a later release for the implementation.
3
+ The native rich-text editing leaf: WordPress Aztec's `AztecText` on Android —
4
+ a `Spannable`-backed `EditText`, not a WebView. iOS mounts an unsupported
5
+ placeholder until the Aztec-iOS Swift facade lands; web, macOS, and Windows
6
+ return `supported: false` (the `@octane-xplat/tiptap` facade covers web).
7
+
8
+ ```ts
9
+ import { RichTextEditor, supported } from '@octane-xplat/richtext'
10
+ ```
11
+
12
+ Content in/out is document HTML via Aztec `fromHtml`/`toPlainHtml`. The
13
+ `bind` callback returns an imperative handle (`getHTML`/`setHTML`,
14
+ `apply(format)`, `linkTo`/`removeLink`, `isActive`, `undo`/`redo`,
15
+ `focus`/`blur`, `native`) once the editor exists, and `onSelectionChange`
16
+ reports the active `RichTextFormat` set at the caret.
17
+
18
+ The Android Aztec dependency (`org.wordpress:aztec:v2.1.7`) arrives through
19
+ the leaf's `platforms/android/include.gradle` — no app-level gradle work.
20
+ See the framework guide for the full contract and the demo:
21
+ [`docs/rich-text.md`](../../docs/rich-text.md) and
22
+ [`packages/demos/src/RichTextEditorDemo.tsrx`](../demos/src/RichTextEditorDemo.tsrx).
@@ -0,0 +1,234 @@
1
+ import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalValue, useCallback, useEffect, useRef } from "@nativescript-community/octane";
2
+ import { View } from "@nativescript/core";
3
+ //#region src/RichTextEditor.android.tsrx
4
+ var _hs$ = /* @__PURE__ */ hookSlots(10);
5
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
6
+ var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
7
+ var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
8
+ var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
9
+ var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
10
+ var _h$5 = /* @__PURE__ */ Symbol(_hs$ + 5);
11
+ var _h$6 = /* @__PURE__ */ Symbol(_hs$ + 6);
12
+ var _h$7 = /* @__PURE__ */ Symbol(_hs$ + 7);
13
+ var _h$8 = /* @__PURE__ */ Symbol(_hs$ + 8);
14
+ var _h$9 = /* @__PURE__ */ Symbol(_hs$ + 9);
15
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
16
+ "kind": "host",
17
+ "type": "xplatrichtext",
18
+ "bindings": [
19
+ ["ref", 0],
20
+ ["id", 1],
21
+ ["className", 2],
22
+ ["style", 3],
23
+ ["row", 4],
24
+ ["col", 5],
25
+ ["rowSpan", 6],
26
+ ["colSpan", 7],
27
+ ["dock", 8],
28
+ ["left", 9],
29
+ ["top", 10],
30
+ ["flexGrow", 11],
31
+ ["flexShrink", 12],
32
+ ["alignSelf", 13],
33
+ ["order", 14]
34
+ ]
35
+ });
36
+ var aztec = globalThis.org?.wordpress?.aztec;
37
+ var androidText = globalThis.android?.text;
38
+ var androidView = globalThis.android?.view;
39
+ var FORMATS = {
40
+ bold: () => aztec.AztecTextFormat.FORMAT_BOLD,
41
+ italic: () => aztec.AztecTextFormat.FORMAT_ITALIC,
42
+ underline: () => aztec.AztecTextFormat.FORMAT_UNDERLINE,
43
+ strikethrough: () => aztec.AztecTextFormat.FORMAT_STRIKETHROUGH,
44
+ code: () => aztec.AztecTextFormat.FORMAT_CODE,
45
+ blockquote: () => aztec.AztecTextFormat.FORMAT_QUOTE,
46
+ bulletList: () => aztec.AztecTextFormat.FORMAT_UNORDERED_LIST,
47
+ orderedList: () => aztec.AztecTextFormat.FORMAT_ORDERED_LIST,
48
+ taskList: () => aztec.AztecTextFormat.FORMAT_TASK_LIST,
49
+ codeBlock: () => aztec.AztecTextFormat.FORMAT_PREFORMAT,
50
+ horizontalRule: () => aztec.AztecTextFormat.FORMAT_HORIZONTAL_RULE,
51
+ highlight: () => aztec.AztecTextFormat.FORMAT_MARK,
52
+ subscript: () => aztec.AztecTextFormat.FORMAT_SUBSCRIPT,
53
+ superscript: () => aztec.AztecTextFormat.FORMAT_SUPERSCRIPT,
54
+ paragraph: () => aztec.AztecTextFormat.FORMAT_PARAGRAPH,
55
+ heading1: () => aztec.AztecTextFormat.FORMAT_HEADING_1,
56
+ heading2: () => aztec.AztecTextFormat.FORMAT_HEADING_2,
57
+ heading3: () => aztec.AztecTextFormat.FORMAT_HEADING_3,
58
+ heading4: () => aztec.AztecTextFormat.FORMAT_HEADING_4,
59
+ heading5: () => aztec.AztecTextFormat.FORMAT_HEADING_5,
60
+ heading6: () => aztec.AztecTextFormat.FORMAT_HEADING_6,
61
+ alignLeft: () => aztec.AztecTextFormat.FORMAT_ALIGN_LEFT,
62
+ alignCenter: () => aztec.AztecTextFormat.FORMAT_ALIGN_CENTER,
63
+ alignRight: () => aztec.AztecTextFormat.FORMAT_ALIGN_RIGHT
64
+ };
65
+ var AZTEC_OF = Object.entries(FORMATS);
66
+ var formatOf = (format) => format === "link" ? aztec.AztecTextFormat.FORMAT_LINK : FORMATS[format]();
67
+ var AztecEditor = class extends View {
68
+ createNativeView() {
69
+ return new aztec.AztecText(this._context);
70
+ }
71
+ };
72
+ registerElement("xplatrichtext", AztecEditor);
73
+ var supported = true;
74
+ var activeFormats = (editor) => {
75
+ const styles = editor.getAppliedStyles(editor.getSelectionStart(), editor.getSelectionEnd());
76
+ const count = styles?.size?.() ?? 0;
77
+ const active = /* @__PURE__ */ new Set();
78
+ for (let i = 0; i < count; i++) active.add(styles.get(i));
79
+ const out = [];
80
+ for (const [name, get] of AZTEC_OF) if (active.has(get())) out.push(name);
81
+ if (active.has(aztec.AztecTextFormat.FORMAT_LINK)) out.push("link");
82
+ return out;
83
+ };
84
+ var RichTextEditor = defineUniversalComponent("nativescript", function RichTextEditor(props) {
85
+ const host = useRef(null, _h$0);
86
+ const lastEmitted = useRef(void 0, _h$1);
87
+ const applyingValue = useRef(false, _h$2);
88
+ const keyListener = useRef(null, _h$3);
89
+ const emitTimer = useRef(null, _h$4);
90
+ const bindHost = useCallback((v) => {
91
+ host.current = v;
92
+ }, [], _h$5);
93
+ const editor = () => host.current?.android;
94
+ const setEditable = (next) => {
95
+ const e = editor();
96
+ if (!e) return;
97
+ if (keyListener.current == null) keyListener.current = e.getKeyListener();
98
+ if (next) {
99
+ e.setKeyListener(keyListener.current);
100
+ e.setFocusable(true);
101
+ e.setCursorVisible(true);
102
+ } else {
103
+ e.setKeyListener(null);
104
+ e.setFocusable(false);
105
+ e.setCursorVisible(false);
106
+ }
107
+ };
108
+ const showKeyboard = (e) => {
109
+ try {
110
+ e.getContext().getSystemService(globalThis.android.content.Context.INPUT_METHOD_SERVICE)?.showSoftInput(e, androidView.inputmethod.InputMethodManager.SHOW_IMPLICIT);
111
+ } catch {}
112
+ };
113
+ const emitChange = () => {
114
+ if (!editor() || applyingValue.current) return;
115
+ clearTimeout(emitTimer.current);
116
+ emitTimer.current = setTimeout(() => {
117
+ const current = editor();
118
+ if (!current || applyingValue.current) return;
119
+ const html = current.toPlainHtml(false);
120
+ lastEmitted.current = html;
121
+ props.onChange?.(html);
122
+ }, 150);
123
+ };
124
+ const handle = (e) => ({
125
+ getHTML: () => e.toPlainHtml(false),
126
+ setHTML: (html) => {
127
+ applyingValue.current = true;
128
+ try {
129
+ e.fromHtml(html, true);
130
+ lastEmitted.current = html;
131
+ } finally {
132
+ applyingValue.current = false;
133
+ }
134
+ },
135
+ apply: (format) => {
136
+ if (format === "link") return;
137
+ e.toggleFormatting(formatOf(format));
138
+ emitChange();
139
+ },
140
+ linkTo: (url, anchor) => {
141
+ e.link(url, anchor ?? url, false);
142
+ emitChange();
143
+ },
144
+ removeLink: () => {
145
+ e.removeLink();
146
+ emitChange();
147
+ },
148
+ isActive: (format) => {
149
+ const applied = e.getAppliedStyles(e.getSelectionStart(), e.getSelectionEnd());
150
+ const count = applied?.size?.() ?? 0;
151
+ const target = formatOf(format);
152
+ for (let i = 0; i < count; i++) if (applied.get(i) === target) return true;
153
+ return false;
154
+ },
155
+ undo: () => e.undo(),
156
+ redo: () => e.redo(),
157
+ focus: () => {
158
+ e.requestFocus();
159
+ showKeyboard(e);
160
+ },
161
+ blur: () => e.clearFocus(),
162
+ isFocused: () => e.isFocused(),
163
+ native: e
164
+ });
165
+ useEffect(() => {
166
+ const e = editor();
167
+ if (!e || !aztec) return;
168
+ e.setOnSelectionChangedListener(new aztec.AztecText.OnSelectionChangedListener({ onSelectionChanged: (start, end) => props.onSelectionChange?.({
169
+ start,
170
+ end,
171
+ active: activeFormats(e)
172
+ }) }));
173
+ e.addTextChangedListener(new androidText.TextWatcher({
174
+ beforeTextChanged: () => {},
175
+ onTextChanged: () => {},
176
+ afterTextChanged: () => emitChange()
177
+ }));
178
+ e.setOnFocusChangeListener(new androidView.View.OnFocusChangeListener({ onFocusChange: (_v, focused) => focused ? props.onFocus?.() : props.onBlur?.() }));
179
+ if (props.placeholder !== void 0) e.setHint(props.placeholder);
180
+ setEditable(props.editable !== false);
181
+ if (props.value !== void 0) {
182
+ applyingValue.current = true;
183
+ try {
184
+ e.fromHtml(props.value, true);
185
+ } finally {
186
+ applyingValue.current = false;
187
+ }
188
+ }
189
+ if (props.autofocus) {
190
+ e.requestFocus();
191
+ showKeyboard(e);
192
+ }
193
+ if (props.android) Object.assign(e, props.android);
194
+ props.bind?.(handle(e));
195
+ props.onReady?.();
196
+ }, [], _h$6);
197
+ useEffect(() => {
198
+ const e = editor();
199
+ if (!e || props.value === void 0) return;
200
+ if (props.value === lastEmitted.current) return;
201
+ applyingValue.current = true;
202
+ try {
203
+ e.fromHtml(props.value, true);
204
+ } finally {
205
+ applyingValue.current = false;
206
+ }
207
+ }, [props.value], _h$7);
208
+ useEffect(() => {
209
+ setEditable(props.editable !== false);
210
+ }, [props.editable], _h$8);
211
+ useEffect(() => {
212
+ const e = editor();
213
+ if (e && props.placeholder !== void 0) e.setHint(props.placeholder);
214
+ }, [props.placeholder], _h$9);
215
+ return universalValue(__octaneUniversalPlan0, [
216
+ bindHost,
217
+ props.id,
218
+ props.className,
219
+ props.style,
220
+ props.row,
221
+ props.col,
222
+ props.rowSpan,
223
+ props.colSpan,
224
+ props.dock,
225
+ props.left,
226
+ props.top,
227
+ props.flexGrow,
228
+ props.flexShrink,
229
+ props.alignSelf,
230
+ props.order
231
+ ]);
232
+ }, { "module": "@nativescript-community/octane" });
233
+ //#endregion
234
+ export { RichTextEditor, supported };
@@ -0,0 +1,22 @@
1
+ import { defineUniversalComponent, universalPlan, universalValue } from "@nativescript-community/octane";
2
+ //#region src/RichTextEditor.ios.tsrx
3
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
4
+ "kind": "host",
5
+ "type": "label",
6
+ "props": { "text": "Rich text editing is not supported on iOS yet." },
7
+ "bindings": [
8
+ ["id", 0],
9
+ ["className", 1],
10
+ ["style", 2]
11
+ ]
12
+ });
13
+ var supported = false;
14
+ var RichTextEditor = defineUniversalComponent("nativescript", function RichTextEditor(props) {
15
+ return universalValue(__octaneUniversalPlan0, [
16
+ props.id,
17
+ props.className,
18
+ props.style
19
+ ]);
20
+ }, { "module": "@nativescript-community/octane" });
21
+ //#endregion
22
+ export { RichTextEditor, supported };
@@ -0,0 +1,2 @@
1
+ import { RichTextEditor, supported } from "./RichTextEditor";
2
+ export { RichTextEditor, supported };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "f6769804-c57f-408a-9066-2cfebe36da4e",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,9 @@
1
+ //#region src/index.web.ts
2
+ var supported = false;
3
+ /** Web routes through @octane-xplat/tiptap — this leaf only exists for the
4
+ * native (Aztec) surface. */
5
+ function RichTextEditor(_props) {
6
+ return null;
7
+ }
8
+ //#endregion
9
+ export { RichTextEditor, supported };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "bd95f265-682a-446a-a284-1f3419b2fb06",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,19 +1,83 @@
1
1
  {
2
2
  "name": "@octane-xplat/richtext",
3
- "version": "0.0.1",
3
+ "version": "0.9.0",
4
4
  "description": "Native rich-text editing leaf for Octane xplat — WordPress Aztec on Android (iOS stubbed, deferred)",
5
+ "keywords": [
6
+ "android",
7
+ "aztec",
8
+ "cross-platform",
9
+ "editor",
10
+ "octane",
11
+ "rich-text",
12
+ "richtext",
13
+ "xplat"
14
+ ],
15
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/richtext",
5
16
  "license": "MIT",
6
17
  "repository": {
7
18
  "type": "git",
8
- "url": "https://github.com/octane-xplat/octane-xplat.git",
19
+ "url": "https://github.com/octane-xplat/octane-xplat",
9
20
  "directory": "packages/richtext"
10
21
  },
11
- "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/richtext",
12
- "main": "./index.js",
13
- "types": "./index.d.ts",
14
22
  "files": [
15
- "README.md",
16
- "index.js",
17
- "index.d.ts"
18
- ]
19
- }
23
+ "dist",
24
+ "types",
25
+ "src",
26
+ "platforms"
27
+ ],
28
+ "type": "module",
29
+ "exports": {
30
+ ".": {
31
+ "macos": {
32
+ "types": "./types/index.d.ts",
33
+ "default": "./src/index.macos.ts"
34
+ },
35
+ "web": {
36
+ "types": "./types/index.d.ts",
37
+ "default": "./dist/web/index.js"
38
+ },
39
+ "native": {
40
+ "types": "./types/index.d.ts",
41
+ "default": "./dist/native/index.js"
42
+ },
43
+ "types": "./types/index.d.ts",
44
+ "default": "./dist/native/index.js"
45
+ }
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "devDependencies": {
51
+ "@nativescript-community/octane": "0.2.4",
52
+ "@nativescript/core": "9.1.2",
53
+ "@octanejs/vite-plugin": "0.1.61",
54
+ "octane": "0.6.3",
55
+ "typescript": "5.9.3",
56
+ "vite": "8.3.0"
57
+ },
58
+ "peerDependencies": {
59
+ "@nativescript-community/octane": ">=0.2.1 <1",
60
+ "@nativescript/core": ">=9.1.0 <10",
61
+ "octane": ">=0.6.3 <1"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "@nativescript-community/octane": {
65
+ "optional": true
66
+ },
67
+ "@nativescript/core": {
68
+ "optional": true
69
+ },
70
+ "octane": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "nativescript": {
75
+ "platforms": {
76
+ "android": "9.1.0",
77
+ "ios": "9.1.0"
78
+ }
79
+ },
80
+ "scripts": {
81
+ "build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs"
82
+ }
83
+ }
@@ -0,0 +1,7 @@
1
+ repositories {
2
+ maven { url "https://a8c-libs.s3.amazonaws.com/android" }
3
+ }
4
+
5
+ dependencies {
6
+ implementation "org.wordpress:aztec:v2.1.7"
7
+ }
@@ -0,0 +1,281 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { useCallback, useEffect, useRef } from 'octane'
3
+ import { View } from '@nativescript/core'
4
+ import { registerElement } from '@nativescript-community/octane'
5
+ import type { Attributes } from '@nativescript-community/octane/intrinsics'
6
+ import type {
7
+ RichTextEditorHandle,
8
+ RichTextEditorProps,
9
+ RichTextFormat,
10
+ } from './types'
11
+
12
+ /** `org.wordpress.aztec` arrives through the leaf's aar — there are no
13
+ * generated typings for it, so every touch goes through `as any` at one
14
+ * boundary instead of `declare const org` per call site. */
15
+ const aztec = (globalThis as any).org?.wordpress?.aztec
16
+ const androidText = (globalThis as any).android?.text
17
+ const androidView = (globalThis as any).android?.view
18
+
19
+ const FORMATS: Record<Exclude<RichTextFormat, 'link'>, () => any> = {
20
+ bold: () => aztec.AztecTextFormat.FORMAT_BOLD,
21
+ italic: () => aztec.AztecTextFormat.FORMAT_ITALIC,
22
+ underline: () => aztec.AztecTextFormat.FORMAT_UNDERLINE,
23
+ strikethrough: () => aztec.AztecTextFormat.FORMAT_STRIKETHROUGH,
24
+ code: () => aztec.AztecTextFormat.FORMAT_CODE,
25
+ blockquote: () => aztec.AztecTextFormat.FORMAT_QUOTE,
26
+ bulletList: () => aztec.AztecTextFormat.FORMAT_UNORDERED_LIST,
27
+ orderedList: () => aztec.AztecTextFormat.FORMAT_ORDERED_LIST,
28
+ taskList: () => aztec.AztecTextFormat.FORMAT_TASK_LIST,
29
+ codeBlock: () => aztec.AztecTextFormat.FORMAT_PREFORMAT,
30
+ horizontalRule: () => aztec.AztecTextFormat.FORMAT_HORIZONTAL_RULE,
31
+ highlight: () => aztec.AztecTextFormat.FORMAT_MARK,
32
+ subscript: () => aztec.AztecTextFormat.FORMAT_SUBSCRIPT,
33
+ superscript: () => aztec.AztecTextFormat.FORMAT_SUPERSCRIPT,
34
+ paragraph: () => aztec.AztecTextFormat.FORMAT_PARAGRAPH,
35
+ heading1: () => aztec.AztecTextFormat.FORMAT_HEADING_1,
36
+ heading2: () => aztec.AztecTextFormat.FORMAT_HEADING_2,
37
+ heading3: () => aztec.AztecTextFormat.FORMAT_HEADING_3,
38
+ heading4: () => aztec.AztecTextFormat.FORMAT_HEADING_4,
39
+ heading5: () => aztec.AztecTextFormat.FORMAT_HEADING_5,
40
+ heading6: () => aztec.AztecTextFormat.FORMAT_HEADING_6,
41
+ alignLeft: () => aztec.AztecTextFormat.FORMAT_ALIGN_LEFT,
42
+ alignCenter: () => aztec.AztecTextFormat.FORMAT_ALIGN_CENTER,
43
+ alignRight: () => aztec.AztecTextFormat.FORMAT_ALIGN_RIGHT,
44
+ }
45
+
46
+ const AZTEC_OF: [RichTextFormat, () => any][] = Object.entries(FORMATS) as any
47
+
48
+ const formatOf = (format: RichTextFormat) =>
49
+ format === 'link' ? aztec.AztecTextFormat.FORMAT_LINK : FORMATS[format]()
50
+
51
+ class AztecEditor extends View {
52
+ createNativeView() {
53
+ return new aztec.AztecText(this._context)
54
+ }
55
+ }
56
+
57
+ declare module '@nativescript-community/octane/intrinsics' {
58
+ interface NativeScriptElements {
59
+ xplatrichtext: Attributes<typeof AztecEditor>
60
+ }
61
+ }
62
+
63
+ registerElement('xplatrichtext', AztecEditor)
64
+
65
+ export const supported = true
66
+
67
+ /** Active formats at the selection, mapped back to facade spellings. */
68
+ const activeFormats = (editor: any): RichTextFormat[] => {
69
+ const styles = editor.getAppliedStyles(
70
+ editor.getSelectionStart(),
71
+ editor.getSelectionEnd(),
72
+ )
73
+
74
+ const count = styles?.size?.() ?? 0
75
+ const active = new Set<any>()
76
+ for (let i = 0; i < count; i++) active.add(styles.get(i))
77
+
78
+ const out: RichTextFormat[] = []
79
+ for (const [name, get] of AZTEC_OF) {
80
+ if (active.has(get())) out.push(name)
81
+ }
82
+
83
+ if (active.has(aztec.AztecTextFormat.FORMAT_LINK)) out.push('link')
84
+ return out
85
+ }
86
+
87
+ /** WordPress Aztec surface on Android — a true Spannable-backed editor, not
88
+ * a WebView. Content in/out is HTML (`fromHtml`/`toPlainHtml`); JSON
89
+ * interchange lives one layer up in @octane-xplat/tiptap. */
90
+ export function RichTextEditor(props: RichTextEditorProps) @{
91
+ const host = useRef<any>(null)
92
+ const lastEmitted = useRef<string | undefined>(undefined)
93
+ const applyingValue = useRef(false)
94
+ const keyListener = useRef<any>(null)
95
+ const emitTimer = useRef<any>(null)
96
+
97
+ // Stable identity — an inline closure re-fires ref(null)/ref(v) per
98
+ // render, which would bounce host.current around the effects below.
99
+ const bindHost = useCallback((v: any) => {
100
+ host.current = v
101
+ }, [])
102
+
103
+ const editor = () => host.current?.android
104
+
105
+ const setEditable = (next: boolean) => {
106
+ const e = editor()
107
+ if (!e) return
108
+ if (keyListener.current == null) keyListener.current = e.getKeyListener()
109
+ if (next) {
110
+ e.setKeyListener(keyListener.current)
111
+ e.setFocusable(true)
112
+ e.setCursorVisible(true)
113
+ } else {
114
+ e.setKeyListener(null)
115
+ e.setFocusable(false)
116
+ e.setCursorVisible(false)
117
+ }
118
+ }
119
+
120
+ const showKeyboard = (e: any) => {
121
+ try {
122
+ const imm = e.getContext().getSystemService(
123
+ (globalThis as any).android.content.Context.INPUT_METHOD_SERVICE,
124
+ )
125
+
126
+ imm?.showSoftInput(e, androidView.inputmethod.InputMethodManager.SHOW_IMPLICIT)
127
+ } catch {}
128
+ }
129
+
130
+ const emitChange = () => {
131
+ const e = editor()
132
+ if (!e || applyingValue.current) return
133
+ clearTimeout(emitTimer.current)
134
+ emitTimer.current = setTimeout(() => {
135
+ const current = editor()
136
+ if (!current || applyingValue.current) return
137
+ const html = current.toPlainHtml(false)
138
+ lastEmitted.current = html
139
+ props.onChange?.(html)
140
+ }, 150)
141
+ }
142
+
143
+ const handle = (e: any): RichTextEditorHandle => ({
144
+ getHTML: () => e.toPlainHtml(false),
145
+ setHTML: (html: string) => {
146
+ applyingValue.current = true
147
+ try {
148
+ e.fromHtml(html, true)
149
+ lastEmitted.current = html
150
+ } finally {
151
+ applyingValue.current = false
152
+ }
153
+ },
154
+ apply: (format: RichTextFormat) => {
155
+ if (format === 'link') return
156
+ e.toggleFormatting(formatOf(format))
157
+ emitChange()
158
+ },
159
+ linkTo: (url: string, anchor?: string) => {
160
+ e.link(url, anchor ?? url, false)
161
+ emitChange()
162
+ },
163
+ removeLink: () => {
164
+ e.removeLink()
165
+ emitChange()
166
+ },
167
+ isActive: (format: RichTextFormat) => {
168
+ const applied = e.getAppliedStyles(e.getSelectionStart(), e.getSelectionEnd())
169
+ const count = applied?.size?.() ?? 0
170
+ const target = formatOf(format)
171
+ for (let i = 0; i < count; i++) {
172
+ if (applied.get(i) === target) return true
173
+ }
174
+
175
+ return false
176
+ },
177
+ undo: () => e.undo(),
178
+ redo: () => e.redo(),
179
+ focus: () => {
180
+ e.requestFocus()
181
+ showKeyboard(e)
182
+ },
183
+ blur: () => e.clearFocus(),
184
+ isFocused: () => e.isFocused(),
185
+ native: e,
186
+ })
187
+
188
+ useEffect(() => {
189
+ const e = editor()
190
+ if (!e || !aztec) return
191
+
192
+ e.setOnSelectionChangedListener(
193
+ new aztec.AztecText.OnSelectionChangedListener({
194
+ onSelectionChanged: (start: number, end: number) =>
195
+ props.onSelectionChange?.({
196
+ start,
197
+ end,
198
+ active: activeFormats(e),
199
+ }),
200
+ }),
201
+ )
202
+
203
+ e.addTextChangedListener(
204
+ new androidText.TextWatcher({
205
+ beforeTextChanged: () => {},
206
+ onTextChanged: () => {},
207
+ afterTextChanged: () => emitChange(),
208
+ }),
209
+ )
210
+
211
+ e.setOnFocusChangeListener(
212
+ new androidView.View.OnFocusChangeListener({
213
+ onFocusChange: (_v: any, focused: boolean) =>
214
+ focused ? props.onFocus?.() : props.onBlur?.(),
215
+ }),
216
+ )
217
+
218
+ if (props.placeholder !== undefined) e.setHint(props.placeholder)
219
+ setEditable(props.editable !== false)
220
+
221
+ if (props.value !== undefined) {
222
+ applyingValue.current = true
223
+ try {
224
+ e.fromHtml(props.value, true)
225
+ } finally {
226
+ applyingValue.current = false
227
+ }
228
+ }
229
+
230
+ if (props.autofocus) {
231
+ e.requestFocus()
232
+ showKeyboard(e)
233
+ }
234
+
235
+ if (props.android) Object.assign(e, props.android)
236
+ props.bind?.(handle(e))
237
+ props.onReady?.()
238
+ }, [])
239
+
240
+ // `value` lands like a controlled prop, minus the loop: text the editor
241
+ // emitted through onChange is never pushed back; any other change is an
242
+ // external reset (and resets Aztec's undo stack, like PM setContent).
243
+ useEffect(() => {
244
+ const e = editor()
245
+ if (!e || props.value === undefined) return
246
+ if (props.value === lastEmitted.current) return
247
+ applyingValue.current = true
248
+ try {
249
+ e.fromHtml(props.value, true)
250
+ } finally {
251
+ applyingValue.current = false
252
+ }
253
+ }, [props.value])
254
+
255
+ useEffect(() => {
256
+ setEditable(props.editable !== false)
257
+ }, [props.editable])
258
+
259
+ useEffect(() => {
260
+ const e = editor()
261
+ if (e && props.placeholder !== undefined) e.setHint(props.placeholder)
262
+ }, [props.placeholder])
263
+
264
+ <xplatrichtext
265
+ ref={bindHost}
266
+ id={props.id}
267
+ className={props.className}
268
+ style={props.style}
269
+ row={props.row}
270
+ col={props.col}
271
+ rowSpan={props.rowSpan}
272
+ colSpan={props.colSpan}
273
+ dock={props.dock}
274
+ left={props.left}
275
+ top={props.top}
276
+ flexGrow={props.flexGrow}
277
+ flexShrink={props.flexShrink}
278
+ alignSelf={props.alignSelf}
279
+ order={props.order}
280
+ />;
281
+ }
@@ -0,0 +1,15 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import type { RichTextEditorProps } from './types'
3
+
4
+ export const supported = false
5
+
6
+ /** iOS bring-up is deferred — the Swift facade over Aztec-iOS lands in a
7
+ * follow-up. Renders the placeholder so a shared screen still mounts. */
8
+ export function RichTextEditor(props: RichTextEditorProps) @{
9
+ <label
10
+ id={props.id}
11
+ className={props.className}
12
+ style={props.style}
13
+ text="Rich text editing is not supported on iOS yet."
14
+ />;
15
+ }
@@ -0,0 +1,10 @@
1
+ import type { RichTextEditorProps } from './types'
2
+
3
+ export const supported = false
4
+
5
+ /** The macOS editor is deferred; the tiptap facade covers desktop needs. */
6
+ export function RichTextEditor(_props: RichTextEditorProps): null {
7
+ return null
8
+ }
9
+
10
+ export type * from './types'
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // Keep this relative import extensionless so the NativeScript resolver picks
2
+ // the Android (Aztec) or iOS (stub until the Swift facade lands) component
3
+ // for the current build target.
4
+ export { RichTextEditor, supported } from './RichTextEditor'
5
+ export type * from './types'
@@ -0,0 +1,11 @@
1
+ import type { RichTextEditorProps } from './types'
2
+
3
+ export const supported = false
4
+
5
+ /** Web routes through @octane-xplat/tiptap — this leaf only exists for the
6
+ * native (Aztec) surface. */
7
+ export function RichTextEditor(_props: RichTextEditorProps): null {
8
+ return null
9
+ }
10
+
11
+ export type * from './types'
package/src/types.ts ADDED
@@ -0,0 +1,97 @@
1
+ /** Supported on Android today (WordPress Aztec); iOS is stubbed until the
2
+ * Swift-facade bring-up. Web/macOS/Windows route through the tiptap facade
3
+ * instead of this leaf. */
4
+ export type RichTextFormat =
5
+ | 'bold'
6
+ | 'italic'
7
+ | 'underline'
8
+ | 'strikethrough'
9
+ | 'code'
10
+ | 'blockquote'
11
+ | 'bulletList'
12
+ | 'orderedList'
13
+ | 'taskList'
14
+ | 'codeBlock'
15
+ | 'horizontalRule'
16
+ | 'highlight'
17
+ | 'subscript'
18
+ | 'superscript'
19
+ | 'paragraph'
20
+ | 'heading1'
21
+ | 'heading2'
22
+ | 'heading3'
23
+ | 'heading4'
24
+ | 'heading5'
25
+ | 'heading6'
26
+ | 'alignLeft'
27
+ | 'alignCenter'
28
+ | 'alignRight'
29
+ | 'link'
30
+
31
+ export interface RichTextEditorHandle {
32
+ /** Serialized document HTML (Aztec's "plain" HTML — no contenteditable
33
+ * markup). */
34
+ getHTML(): string
35
+ /** Replace the document; resets Aztec's undo history. */
36
+ setHTML(html: string): void
37
+ /** Toggle a format at the current selection. `link` needs
38
+ * `linkTo(url, anchor)` instead. */
39
+ apply(format: RichTextFormat): void
40
+ /** Insert or re-target a link over the current selection. */
41
+ linkTo(url: string, anchor?: string): void
42
+ removeLink(): void
43
+ /** Formats active at the selection — powers toolbar highlight state. */
44
+ isActive(format: RichTextFormat): boolean
45
+ /** Android: Aztec's history batches keyboard input only — format
46
+ * toggles and programmatic edits do not register. */
47
+ undo(): void
48
+ redo(): void
49
+ focus(): void
50
+ blur(): void
51
+ isFocused(): boolean
52
+ /** The platform surface — `AztecText` on Android. */
53
+ native: any
54
+ }
55
+
56
+ export interface RichTextSelectionChange {
57
+ start: number
58
+ end: number
59
+ /** Formats active at the caret/range, in facade spellings. */
60
+ active: RichTextFormat[]
61
+ }
62
+
63
+ export interface RichTextEditorProps {
64
+ id?: string
65
+ className?: string
66
+ style?: any
67
+ /** Initial document HTML. Re-applied only when the string changes
68
+ * externally — text the editor emitted back through `onChange` is never
69
+ * re-pushed, so typing doesn't reparse the document. */
70
+ value?: string
71
+ placeholder?: string
72
+ editable?: boolean
73
+ autofocus?: boolean
74
+ onReady?: () => void
75
+ onChange?: (html: string) => void
76
+ onSelectionChange?: (event: RichTextSelectionChange) => void
77
+ onFocus?: () => void
78
+ onBlur?: () => void
79
+ /** Imperative handle — fires once when the native editor exists. */
80
+ bind?: (handle: RichTextEditorHandle) => void
81
+ /** Per-platform escape bag — properties are assigned onto the native
82
+ * `AztecText` after the leaf's own props. */
83
+ android?: Record<string, any>
84
+ ios?: Record<string, any>
85
+ // NativeScript layout-child attributes the driver reads on this element.
86
+ row?: number | string
87
+ col?: number | string
88
+ rowSpan?: number
89
+ colSpan?: number
90
+ dock?: 'left' | 'top' | 'right' | 'bottom'
91
+ left?: number
92
+ top?: number
93
+ flexGrow?: number
94
+ flexShrink?: number
95
+ alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'
96
+ order?: number
97
+ }
@@ -0,0 +1,6 @@
1
+ import type { UniversalComponent } from 'octane/universal'
2
+ import type { RichTextEditorProps } from './types'
3
+
4
+ export declare const RichTextEditor: UniversalComponent<RichTextEditorProps>
5
+ export declare const supported: boolean
6
+ export type * from './types'
@@ -0,0 +1,68 @@
1
+ /** Supported on Android today (WordPress Aztec); iOS is stubbed until the
2
+ * Swift-facade bring-up. Web/macOS/Windows route through the tiptap facade
3
+ * instead of this leaf. */
4
+ export type RichTextFormat = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code' | 'blockquote' | 'bulletList' | 'orderedList' | 'taskList' | 'codeBlock' | 'horizontalRule' | 'highlight' | 'subscript' | 'superscript' | 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'heading4' | 'heading5' | 'heading6' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'link';
5
+ export interface RichTextEditorHandle {
6
+ /** Serialized document HTML (Aztec's "plain" HTML — no contenteditable
7
+ * markup). */
8
+ getHTML(): string;
9
+ /** Replace the document; resets Aztec's undo history. */
10
+ setHTML(html: string): void;
11
+ /** Toggle a format at the current selection. `link` needs
12
+ * `linkTo(url, anchor)` instead. */
13
+ apply(format: RichTextFormat): void;
14
+ /** Insert or re-target a link over the current selection. */
15
+ linkTo(url: string, anchor?: string): void;
16
+ removeLink(): void;
17
+ /** Formats active at the selection — powers toolbar highlight state. */
18
+ isActive(format: RichTextFormat): boolean;
19
+ /** Android: Aztec's history batches keyboard input only — format
20
+ * toggles and programmatic edits do not register. */
21
+ undo(): void;
22
+ redo(): void;
23
+ focus(): void;
24
+ blur(): void;
25
+ isFocused(): boolean;
26
+ /** The platform surface — `AztecText` on Android. */
27
+ native: any;
28
+ }
29
+ export interface RichTextSelectionChange {
30
+ start: number;
31
+ end: number;
32
+ /** Formats active at the caret/range, in facade spellings. */
33
+ active: RichTextFormat[];
34
+ }
35
+ export interface RichTextEditorProps {
36
+ id?: string;
37
+ className?: string;
38
+ style?: any;
39
+ /** Initial document HTML. Re-applied only when the string changes
40
+ * externally — text the editor emitted back through `onChange` is never
41
+ * re-pushed, so typing doesn't reparse the document. */
42
+ value?: string;
43
+ placeholder?: string;
44
+ editable?: boolean;
45
+ autofocus?: boolean;
46
+ onReady?: () => void;
47
+ onChange?: (html: string) => void;
48
+ onSelectionChange?: (event: RichTextSelectionChange) => void;
49
+ onFocus?: () => void;
50
+ onBlur?: () => void;
51
+ /** Imperative handle — fires once when the native editor exists. */
52
+ bind?: (handle: RichTextEditorHandle) => void;
53
+ /** Per-platform escape bag — properties are assigned onto the native
54
+ * `AztecText` after the leaf's own props. */
55
+ android?: Record<string, any>;
56
+ ios?: Record<string, any>;
57
+ row?: number | string;
58
+ col?: number | string;
59
+ rowSpan?: number;
60
+ colSpan?: number;
61
+ dock?: 'left' | 'top' | 'right' | 'bottom';
62
+ left?: number;
63
+ top?: number;
64
+ flexGrow?: number;
65
+ flexShrink?: number;
66
+ alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
67
+ order?: number;
68
+ }
package/index.d.ts DELETED
@@ -1 +0,0 @@
1
- export {}
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- // Reservation stub; implementation will arrive in a later release.
2
- module.exports = {}