@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 +21 -3
- package/dist/native/RichTextEditor.android.js +39 -0
- package/dist/native/RichTextEditor.ios.js +194 -8
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/octane-client-build.json +1 -1
- package/nativescript.config.ts +22 -0
- package/package.json +4 -3
- package/platforms/ios/LICENSE-Aztec-MPL-2.0.txt +355 -0
- package/platforms/ios/UPSTREAM.md +14 -0
- package/platforms/ios/src/XplatAztecEditor.swift +357 -0
- package/src/RichTextEditor.android.tsrx +39 -0
- package/src/RichTextEditor.ios.tsrx +221 -6
- package/src/host-session.macos.ts +9 -0
- package/src/index.ts +2 -2
- package/src/types.ts +49 -7
- package/types/types.d.ts +49 -7
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
|
|
5
|
-
|
|
6
|
-
return `supported: false` (the
|
|
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": "
|
|
6
|
-
"props": { "text": "Rich text editing is not supported on iOS yet." },
|
|
18
|
+
"type": "xplatrichtext",
|
|
7
19
|
"bindings": [
|
|
8
|
-
["
|
|
9
|
-
["
|
|
10
|
-
["
|
|
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
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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",
|