@octane-xplat/richtext 0.10.0 → 0.11.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/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # `@octane-xplat/richtext`
2
2
 
3
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 and Windows
6
- return `supported: false` (the `@octane-xplat/tiptap` facade covers web).
4
+ a `Spannable`-backed `EditText`, not a WebView. iOS mounts AztecEditor-iOS
5
+ (`Aztec.TextView`) through the `XplatAztecEditorView` Swift facade in
6
+ `platforms/ios`; web and Windows return `supported: false` (the
7
+ `@octane-xplat/tiptap` facade covers web).
7
8
 
8
9
  ```tsx
9
10
  import { useState } from 'octane'
@@ -26,6 +27,16 @@ Content in/out is document HTML via Aztec `fromHtml`/`toPlainHtml`. The
26
27
  `focus`/`blur`, `native`) once the editor exists, and `onSelectionChange`
27
28
  reports the active `RichTextFormat` set at the caret.
28
29
 
30
+ Structural commands exist on the Aztec surface: `split()` breaks the block
31
+ at the caret, `join()` merges a block into the previous one from a
32
+ collapsed caret at its start, and `indent()`/`outdent()` demote or promote
33
+ the selected blocks — a real nested-list restructure on `<ul>`/`<ol>`, a
34
+ `\t` indent on plain blocks. `canIndent()`/`canOutdent()` report whether
35
+ the current selection can take the command. All six return `false` when
36
+ the command cannot apply; markup Aztec does not understand (for example
37
+ `<div data-*>` wrappers) is opaque to them. On AppKit the commands return
38
+ `false` — the engine transport cannot report a synchronous result.
39
+
29
40
  ```tsx
30
41
  import { useRef } from 'octane'
31
42
  import { RichTextEditor, type RichTextEditorHandle } from '@octane-xplat/richtext'
@@ -102,6 +113,13 @@ export function Formatting() {
102
113
 
103
114
  The Android Aztec dependency (`org.wordpress:aztec:v2.1.7`) arrives through
104
115
  the leaf's `platforms/android/include.gradle` — no app-level gradle work.
116
+ On iOS the leaf declares AztecEditor-iOS as a pinned-revision Swift Package
117
+ in its plugin-level `nativescript.config.ts` and compiles
118
+ `platforms/ios/src/XplatAztecEditor.swift` into the app — NativeScript
119
+ plugin discovery applies both when the app depends on
120
+ `@octane-xplat/richtext`. iOS has no task-list or text-alignment surface:
121
+ `apply('taskList')` and the `align*` formats report a warning and leave the
122
+ document unchanged, and `isActive` returns `false` for them.
105
123
  See the framework guide for the full contract and the demo:
106
124
  [`docs/app/rich-text.md`](../../docs/app/rich-text.md) and
107
125
  [`packages/demos/src/RichTextEditorDemo.tsrx`](../demos/src/RichTextEditorDemo.tsrx).
@@ -156,6 +156,45 @@ var RichTextEditor = defineUniversalComponent("nativescript", function RichTextE
156
156
  },
157
157
  undo: () => e.undo(),
158
158
  redo: () => e.redo(),
159
+ split: () => {
160
+ const selStart = e.getSelectionStart();
161
+ const selEnd = e.getSelectionEnd();
162
+ if (selStart < 0 || selEnd < 0) return false;
163
+ const start = Math.min(selStart, selEnd);
164
+ const end = Math.max(selStart, selEnd);
165
+ const text = e.getText();
166
+ if (end > String(text).length) return false;
167
+ if (end > start) text.delete(start, end);
168
+ text.insert(start, "\n");
169
+ e.setSelection(start + 1);
170
+ emitChange();
171
+ return true;
172
+ },
173
+ join: () => {
174
+ const start = e.getSelectionStart();
175
+ if (start <= 0 || e.getSelectionEnd() !== start) return false;
176
+ const text = e.getText();
177
+ const s = String(text);
178
+ if (start > s.length || s.charAt(start - 1) !== "\n") return false;
179
+ text.delete(start - 1, start);
180
+ e.setSelection(start - 1);
181
+ emitChange();
182
+ return true;
183
+ },
184
+ indent: () => {
185
+ if (!e.isIndentAvailable()) return false;
186
+ e.indent();
187
+ emitChange();
188
+ return true;
189
+ },
190
+ outdent: () => {
191
+ if (!e.isOutdentAvailable()) return false;
192
+ e.outdent();
193
+ emitChange();
194
+ return true;
195
+ },
196
+ canIndent: () => Boolean(e.isIndentAvailable()),
197
+ canOutdent: () => Boolean(e.isOutdentAvailable()),
159
198
  focus: () => {
160
199
  e.requestFocus();
161
200
  showKeyboard(e);
@@ -1,21 +1,207 @@
1
- import { defineUniversalComponent, universalPlan, universalValue } from "@nativescript-community/octane";
1
+ import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalValue, useCallback, useEffect, useImperativeHandle, useRef } from "@nativescript-community/octane";
2
+ import { View } from "@nativescript/core";
2
3
  //#region src/RichTextEditor.ios.tsrx
4
+ var _hs$ = /* @__PURE__ */ hookSlots(11);
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 _h$10 = /* @__PURE__ */ Symbol(_hs$ + 10);
3
16
  var __octaneUniversalPlan0 = universalPlan("nativescript", {
4
17
  "kind": "host",
5
- "type": "label",
6
- "props": { "text": "Rich text editing is not supported on iOS yet." },
18
+ "type": "xplatrichtext",
7
19
  "bindings": [
8
- ["id", 0],
9
- ["className", 1],
10
- ["style", 2]
20
+ ["ref", 0],
21
+ ["id", 1],
22
+ ["className", 2],
23
+ ["style", 3],
24
+ ["row", 4],
25
+ ["col", 5],
26
+ ["rowSpan", 6],
27
+ ["colSpan", 7],
28
+ ["dock", 8],
29
+ ["left", 9],
30
+ ["top", 10],
31
+ ["flexGrow", 11],
32
+ ["flexShrink", 12],
33
+ ["alignSelf", 13],
34
+ ["order", 14]
11
35
  ]
12
36
  });
13
- var supported = false;
37
+ var UNSUPPORTED = /* @__PURE__ */ new Set([
38
+ "taskList",
39
+ "alignLeft",
40
+ "alignCenter",
41
+ "alignRight"
42
+ ]);
43
+ var AztecEditor = class extends View {
44
+ createNativeView() {
45
+ return new XplatAztecEditorView();
46
+ }
47
+ };
48
+ registerElement("xplatrichtext", AztecEditor);
49
+ var supported = true;
14
50
  var RichTextEditor = defineUniversalComponent("nativescript", function RichTextEditor(props) {
51
+ const host = useRef(null, _h$0);
52
+ const lastEmitted = useRef(void 0, _h$1);
53
+ const applyingValue = useRef(false, _h$2);
54
+ const emitTimer = useRef(null, _h$3);
55
+ const warned = useRef(/* @__PURE__ */ new Set(), _h$4);
56
+ useImperativeHandle(props.ref, () => handle(editor()), [], _h$5);
57
+ const bindHost = useCallback((v) => {
58
+ host.current = v;
59
+ }, [], _h$6);
60
+ const editor = () => host.current?.ios;
61
+ const emitChange = () => {
62
+ if (!editor() || applyingValue.current) return;
63
+ clearTimeout(emitTimer.current);
64
+ emitTimer.current = setTimeout(() => {
65
+ const current = editor();
66
+ if (!current || applyingValue.current) return;
67
+ const html = current.getHTML();
68
+ lastEmitted.current = html;
69
+ props.onChange?.(html);
70
+ }, 150);
71
+ };
72
+ const reportUnsupported = (format) => {
73
+ if (warned.current.has(format)) return;
74
+ warned.current.add(format);
75
+ console.warn(`[richtext] apply('${format}') is not supported by the iOS Aztec surface`);
76
+ };
77
+ const handle = (e) => ({
78
+ getHTML: () => e.getHTML(),
79
+ setHTML: (html) => {
80
+ applyingValue.current = true;
81
+ try {
82
+ e.setHTML(html);
83
+ lastEmitted.current = html;
84
+ } finally {
85
+ applyingValue.current = false;
86
+ }
87
+ },
88
+ apply: (format) => {
89
+ if (format === "link") return;
90
+ if (UNSUPPORTED.has(format) || !e.applyFormat(format)) {
91
+ reportUnsupported(format);
92
+ return;
93
+ }
94
+ emitChange();
95
+ },
96
+ linkTo: (url, anchor) => {
97
+ e.linkTo(url, anchor ?? null);
98
+ emitChange();
99
+ },
100
+ removeLink: () => {
101
+ e.removeLink();
102
+ emitChange();
103
+ },
104
+ isActive: (format) => !UNSUPPORTED.has(format) && Boolean(e.isFormatActive(format)),
105
+ undo: () => e.undo(),
106
+ redo: () => e.redo(),
107
+ split: () => {
108
+ const ok = Boolean(e.split());
109
+ if (ok) emitChange();
110
+ return ok;
111
+ },
112
+ join: () => {
113
+ const ok = Boolean(e.join());
114
+ if (ok) emitChange();
115
+ return ok;
116
+ },
117
+ indent: () => {
118
+ const ok = Boolean(e.indent());
119
+ if (ok) emitChange();
120
+ return ok;
121
+ },
122
+ outdent: () => {
123
+ const ok = Boolean(e.outdent());
124
+ if (ok) emitChange();
125
+ return ok;
126
+ },
127
+ canIndent: () => Boolean(e.canIndent()),
128
+ canOutdent: () => Boolean(e.canOutdent()),
129
+ focus: () => e.focus(),
130
+ blur: () => e.blur(),
131
+ isFocused: () => e.focused(),
132
+ native: e
133
+ });
134
+ useEffect(() => {
135
+ const e = editor();
136
+ if (!e) return;
137
+ e.onTextChange = () => emitChange();
138
+ e.onSelectionChange = (start, end, active) => {
139
+ const formats = [];
140
+ const count = active?.count ?? 0;
141
+ for (let i = 0; i < count; i++) formats.push(active.objectAtIndex(i));
142
+ props.onSelectionChange?.({
143
+ start,
144
+ end,
145
+ active: formats
146
+ });
147
+ };
148
+ e.onFocus = () => props.onFocus?.();
149
+ e.onBlur = () => props.onBlur?.();
150
+ if (props.placeholder !== void 0) e.placeholder = props.placeholder;
151
+ e.editable = props.editable !== false;
152
+ if (props.value !== void 0) {
153
+ applyingValue.current = true;
154
+ try {
155
+ e.setHTML(props.value);
156
+ } finally {
157
+ applyingValue.current = false;
158
+ }
159
+ }
160
+ if (props.autofocus) e.focus();
161
+ if (props.ios) Object.assign(e, props.ios);
162
+ props.onReady?.();
163
+ return () => {
164
+ e.onTextChange = null;
165
+ e.onSelectionChange = null;
166
+ e.onFocus = null;
167
+ e.onBlur = null;
168
+ };
169
+ }, [], _h$7);
170
+ useEffect(() => {
171
+ const e = editor();
172
+ if (!e || props.value === void 0) return;
173
+ if (props.value === lastEmitted.current) return;
174
+ applyingValue.current = true;
175
+ try {
176
+ e.setHTML(props.value);
177
+ } finally {
178
+ applyingValue.current = false;
179
+ }
180
+ }, [props.value], _h$8);
181
+ useEffect(() => {
182
+ const e = editor();
183
+ if (e) e.editable = props.editable !== false;
184
+ }, [props.editable], _h$9);
185
+ useEffect(() => {
186
+ const e = editor();
187
+ if (e && props.placeholder !== void 0) e.placeholder = props.placeholder;
188
+ }, [props.placeholder], _h$10);
15
189
  return universalValue(__octaneUniversalPlan0, [
190
+ bindHost,
16
191
  props.id,
17
192
  props.className,
18
- props.style
193
+ props.style,
194
+ props.row,
195
+ props.col,
196
+ props.rowSpan,
197
+ props.colSpan,
198
+ props.dock,
199
+ props.left,
200
+ props.top,
201
+ props.flexGrow,
202
+ props.flexShrink,
203
+ props.alignSelf,
204
+ props.order
19
205
  ]);
20
206
  }, { "module": "@nativescript-community/octane" });
21
207
  //#endregion
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "ca3e4cfe-2095-45b7-8420-77a24704b7b4",
3
+ "buildId": "933ede86-144c-4cb6-9763-762ae7aea462",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "87e27032-9a72-48e8-857d-c8a8216a9331",
3
+ "buildId": "10dfc210-9d43-431b-bdf5-f06486fddbf1",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
@@ -0,0 +1,22 @@
1
+ import type { NativeScriptConfig } from '@nativescript/core'
2
+
3
+ // Plugin-level NativeScript config — read by the CLI's plugin discovery for
4
+ // every app that depends on @octane-xplat/richtext directly. Declares the
5
+ // iOS Swift facade's engine dependency so consuming apps don't wire it
6
+ // themselves.
7
+ export default {
8
+ ios: {
9
+ SPMPackages: [
10
+ {
11
+ name: 'AztecEditor-iOS',
12
+ libs: ['Aztec'],
13
+ repositoryURL:
14
+ 'https://github.com/wordpress-mobile/AztecEditor-iOS',
15
+ // '#' prefix selects a revision requirement in the generated
16
+ // pbxproj — develop @ 44aeac6 (podspec version 1.20.0, first
17
+ // with SPM support). MPL-2.0; see platforms/ios/UPSTREAM.md.
18
+ version: '#44aeac606a3cff6d6ecb1d49ea3f9068082567e1',
19
+ },
20
+ ],
21
+ },
22
+ } as NativeScriptConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octane-xplat/richtext",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Native rich-text editing leaf for Octane xplat — Android Aztec and AppKit WKWebView StarterKit",
5
5
  "keywords": [
6
6
  "android",
@@ -23,7 +23,8 @@
23
23
  "dist",
24
24
  "types",
25
25
  "src",
26
- "platforms"
26
+ "platforms",
27
+ "nativescript.config.ts"
27
28
  ],
28
29
  "type": "module",
29
30
  "exports": {
@@ -58,7 +59,7 @@
58
59
  "devDependencies": {
59
60
  "@nativescript-community/octane": "0.2.4",
60
61
  "@nativescript/core": "9.1.2",
61
- "@octane-xplat/macos-renderer": "0.10.0",
62
+ "@octane-xplat/macos-renderer": "0.11.0",
62
63
  "@octanejs/vite-plugin": "0.1.61",
63
64
  "@tiptap/core": "3.28.0",
64
65
  "@tiptap/starter-kit": "3.28.0",