react-native-richify 1.0.4 → 1.0.5
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/lib/commonjs/components/RenderedOutput.js +168 -0
- package/lib/commonjs/components/RenderedOutput.js.map +1 -0
- package/lib/commonjs/components/RichTextInput.js +134 -15
- package/lib/commonjs/components/RichTextInput.js.map +1 -1
- package/lib/commonjs/components/Toolbar.js +41 -2
- package/lib/commonjs/components/Toolbar.js.map +1 -1
- package/lib/commonjs/constants/defaultStyles.js +55 -1
- package/lib/commonjs/constants/defaultStyles.js.map +1 -1
- package/lib/commonjs/hooks/useFormatting.js +40 -2
- package/lib/commonjs/hooks/useFormatting.js.map +1 -1
- package/lib/commonjs/hooks/useRichText.js +75 -6
- package/lib/commonjs/hooks/useRichText.js.map +1 -1
- package/lib/commonjs/utils/formatter.js +48 -9
- package/lib/commonjs/utils/formatter.js.map +1 -1
- package/lib/commonjs/utils/parser.js +1 -1
- package/lib/commonjs/utils/parser.js.map +1 -1
- package/lib/commonjs/utils/serializer.js +102 -6
- package/lib/commonjs/utils/serializer.js.map +1 -1
- package/lib/commonjs/utils/styleMapper.js +11 -0
- package/lib/commonjs/utils/styleMapper.js.map +1 -1
- package/lib/module/components/RenderedOutput.js +163 -0
- package/lib/module/components/RenderedOutput.js.map +1 -0
- package/lib/module/components/RichTextInput.js +135 -16
- package/lib/module/components/RichTextInput.js.map +1 -1
- package/lib/module/components/Toolbar.js +41 -2
- package/lib/module/components/Toolbar.js.map +1 -1
- package/lib/module/constants/defaultStyles.js +55 -1
- package/lib/module/constants/defaultStyles.js.map +1 -1
- package/lib/module/hooks/useFormatting.js +41 -3
- package/lib/module/hooks/useFormatting.js.map +1 -1
- package/lib/module/hooks/useRichText.js +75 -6
- package/lib/module/hooks/useRichText.js.map +1 -1
- package/lib/module/utils/formatter.js +46 -9
- package/lib/module/utils/formatter.js.map +1 -1
- package/lib/module/utils/parser.js +1 -1
- package/lib/module/utils/parser.js.map +1 -1
- package/lib/module/utils/serializer.js +102 -6
- package/lib/module/utils/serializer.js.map +1 -1
- package/lib/module/utils/styleMapper.js +11 -0
- package/lib/module/utils/styleMapper.js.map +1 -1
- package/lib/typescript/src/components/RenderedOutput.d.ts +9 -0
- package/lib/typescript/src/components/RenderedOutput.d.ts.map +1 -0
- package/lib/typescript/src/components/RichTextInput.d.ts.map +1 -1
- package/lib/typescript/src/components/Toolbar.d.ts.map +1 -1
- package/lib/typescript/src/constants/defaultStyles.d.ts +1 -0
- package/lib/typescript/src/constants/defaultStyles.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useFormatting.d.ts +4 -1
- package/lib/typescript/src/hooks/useFormatting.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useRichText.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/index.d.ts +94 -1
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/formatter.d.ts +9 -1
- package/lib/typescript/src/utils/formatter.d.ts.map +1 -1
- package/lib/typescript/src/utils/parser.d.ts.map +1 -1
- package/lib/typescript/src/utils/serializer.d.ts.map +1 -1
- package/lib/typescript/src/utils/styleMapper.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/RenderedOutput.tsx +231 -0
- package/src/components/RichTextInput.tsx +197 -19
- package/src/components/Toolbar.tsx +54 -2
- package/src/constants/defaultStyles.d.ts +2 -1
- package/src/constants/defaultStyles.ts +21 -0
- package/src/hooks/useFormatting.ts +76 -2
- package/src/hooks/useRichText.ts +101 -5
- package/src/index.d.ts +1 -1
- package/src/index.ts +4 -0
- package/src/types/index.d.ts +94 -1
- package/src/types/index.ts +104 -1
- package/src/utils/formatter.ts +60 -6
- package/src/utils/parser.ts +6 -1
- package/src/utils/serializer.ts +150 -8
- package/src/utils/styleMapper.ts +21 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useCallback,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import {
|
|
3
9
|
Animated,
|
|
4
10
|
Easing,
|
|
@@ -8,6 +14,7 @@ import {
|
|
|
8
14
|
TextInput,
|
|
9
15
|
View,
|
|
10
16
|
type NativeSyntheticEvent,
|
|
17
|
+
type TextInputContentSizeChangeEventData,
|
|
11
18
|
type TextInputSelectionChangeEventData,
|
|
12
19
|
} from 'react-native';
|
|
13
20
|
import type { RichTextInputProps } from '../types';
|
|
@@ -15,9 +22,10 @@ import { DEFAULT_THEME } from '../constants/defaultStyles';
|
|
|
15
22
|
import { useRichText } from '../hooks/useRichText';
|
|
16
23
|
import { segmentsToPlainText } from '../utils/parser';
|
|
17
24
|
import { serializeSegments } from '../utils/serializer';
|
|
25
|
+
import { RenderedOutput } from './RenderedOutput';
|
|
18
26
|
import { Toolbar } from './Toolbar';
|
|
19
27
|
|
|
20
|
-
const
|
|
28
|
+
const DEFAULT_OUTPUT_PANEL_MAX_HEIGHT = 180;
|
|
21
29
|
const isJestRuntime =
|
|
22
30
|
typeof (
|
|
23
31
|
globalThis as {
|
|
@@ -43,8 +51,16 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
43
51
|
toolbarItems,
|
|
44
52
|
theme,
|
|
45
53
|
showOutputPreview = true,
|
|
46
|
-
outputFormat
|
|
54
|
+
outputFormat,
|
|
55
|
+
defaultOutputFormat = 'markdown',
|
|
56
|
+
outputPreviewMode,
|
|
57
|
+
defaultOutputPreviewMode = 'literal',
|
|
58
|
+
maxOutputHeight = DEFAULT_OUTPUT_PANEL_MAX_HEIGHT,
|
|
47
59
|
onChangeOutput,
|
|
60
|
+
onChangeOutputFormat,
|
|
61
|
+
onChangeOutputPreviewMode,
|
|
62
|
+
onRequestLink,
|
|
63
|
+
onRequestImage,
|
|
48
64
|
multiline = true,
|
|
49
65
|
minHeight = 120,
|
|
50
66
|
maxHeight,
|
|
@@ -55,6 +71,12 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
55
71
|
}) => {
|
|
56
72
|
const resolvedTheme = theme ?? DEFAULT_THEME;
|
|
57
73
|
const previewProgress = useRef(new Animated.Value(0)).current;
|
|
74
|
+
const [internalOutputFormat, setInternalOutputFormat] =
|
|
75
|
+
useState(defaultOutputFormat);
|
|
76
|
+
const [internalOutputPreviewMode, setInternalOutputPreviewMode] = useState(
|
|
77
|
+
defaultOutputPreviewMode,
|
|
78
|
+
);
|
|
79
|
+
const [contentHeight, setContentHeight] = useState(minHeight);
|
|
58
80
|
|
|
59
81
|
const { state, actions } = useRichText({
|
|
60
82
|
initialSegments,
|
|
@@ -67,15 +89,41 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
67
89
|
}, [actions, onReady]);
|
|
68
90
|
|
|
69
91
|
const plainText = segmentsToPlainText(state.segments);
|
|
92
|
+
const resolvedOutputFormat = outputFormat ?? internalOutputFormat;
|
|
93
|
+
const resolvedOutputPreviewMode =
|
|
94
|
+
outputPreviewMode ?? internalOutputPreviewMode;
|
|
95
|
+
const inputHeight = Math.max(
|
|
96
|
+
minHeight,
|
|
97
|
+
typeof maxHeight === 'number'
|
|
98
|
+
? Math.min(contentHeight, maxHeight)
|
|
99
|
+
: contentHeight,
|
|
100
|
+
);
|
|
101
|
+
const shouldScrollInput =
|
|
102
|
+
typeof maxHeight === 'number' && contentHeight > maxHeight;
|
|
70
103
|
const serializedOutput = useMemo(
|
|
71
|
-
() => serializeSegments(state.segments,
|
|
72
|
-
[
|
|
104
|
+
() => serializeSegments(state.segments, resolvedOutputFormat),
|
|
105
|
+
[resolvedOutputFormat, state.segments],
|
|
73
106
|
);
|
|
74
107
|
const shouldShowOutputPreview = showOutputPreview && plainText.length > 0;
|
|
108
|
+
const normalizedSelection = useMemo(
|
|
109
|
+
() => ({
|
|
110
|
+
start: Math.min(state.selection.start, state.selection.end),
|
|
111
|
+
end: Math.max(state.selection.start, state.selection.end),
|
|
112
|
+
}),
|
|
113
|
+
[state.selection.end, state.selection.start],
|
|
114
|
+
);
|
|
115
|
+
const selectedText = useMemo(
|
|
116
|
+
() => plainText.slice(normalizedSelection.start, normalizedSelection.end),
|
|
117
|
+
[normalizedSelection.end, normalizedSelection.start, plainText],
|
|
118
|
+
);
|
|
119
|
+
const selectionStyle = useMemo(
|
|
120
|
+
() => actions.getSelectionStyle(),
|
|
121
|
+
[actions, state.activeStyles, state.segments, state.selection],
|
|
122
|
+
);
|
|
75
123
|
|
|
76
124
|
useEffect(() => {
|
|
77
|
-
onChangeOutput?.(serializedOutput,
|
|
78
|
-
}, [onChangeOutput,
|
|
125
|
+
onChangeOutput?.(serializedOutput, resolvedOutputFormat);
|
|
126
|
+
}, [onChangeOutput, resolvedOutputFormat, serializedOutput]);
|
|
79
127
|
|
|
80
128
|
useEffect(() => {
|
|
81
129
|
if (isJestRuntime) {
|
|
@@ -99,30 +147,108 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
99
147
|
[actions],
|
|
100
148
|
);
|
|
101
149
|
|
|
150
|
+
const onContentSizeChange = useCallback(
|
|
151
|
+
(e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
|
|
152
|
+
setContentHeight(Math.ceil(e.nativeEvent.contentSize.height));
|
|
153
|
+
textInputProps?.onContentSizeChange?.(e);
|
|
154
|
+
},
|
|
155
|
+
[textInputProps],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const handleOutputFormatChange = useCallback(
|
|
159
|
+
(format: 'markdown' | 'html') => {
|
|
160
|
+
if (outputFormat === undefined) {
|
|
161
|
+
setInternalOutputFormat(format);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onChangeOutputFormat?.(format);
|
|
165
|
+
},
|
|
166
|
+
[onChangeOutputFormat, outputFormat],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const handleOutputPreviewModeChange = useCallback(
|
|
170
|
+
(mode: 'literal' | 'rendered') => {
|
|
171
|
+
if (outputPreviewMode === undefined) {
|
|
172
|
+
setInternalOutputPreviewMode(mode);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
onChangeOutputPreviewMode?.(mode);
|
|
176
|
+
},
|
|
177
|
+
[onChangeOutputPreviewMode, outputPreviewMode],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const handleRequestLink = useCallback(() => {
|
|
181
|
+
if (onRequestLink) {
|
|
182
|
+
onRequestLink({
|
|
183
|
+
selectedText,
|
|
184
|
+
currentUrl: selectionStyle.link,
|
|
185
|
+
applyLink: actions.setLink,
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (selectionStyle.link) {
|
|
191
|
+
actions.setLink(undefined);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const detectedUrl = detectLinkTarget(selectedText);
|
|
196
|
+
if (detectedUrl) {
|
|
197
|
+
actions.setLink(detectedUrl);
|
|
198
|
+
}
|
|
199
|
+
}, [actions, onRequestLink, selectedText, selectionStyle.link]);
|
|
200
|
+
|
|
201
|
+
const handleRequestImage = useCallback(() => {
|
|
202
|
+
if (onRequestImage) {
|
|
203
|
+
onRequestImage({
|
|
204
|
+
insertImage: actions.insertImage,
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const detectedSource = detectImageSource(selectedText);
|
|
210
|
+
if (detectedSource) {
|
|
211
|
+
actions.insertImage(detectedSource, {
|
|
212
|
+
alt: selectedText.trim() || undefined,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}, [actions, onRequestImage, selectedText]);
|
|
216
|
+
|
|
217
|
+
const outputLabel = useMemo(() => {
|
|
218
|
+
const formatLabel = resolvedOutputFormat === 'html' ? 'HTML' : 'Markdown';
|
|
219
|
+
|
|
220
|
+
if (resolvedOutputPreviewMode === 'rendered') {
|
|
221
|
+
return `${formatLabel} preview`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return `${formatLabel} output`;
|
|
225
|
+
}, [resolvedOutputFormat, resolvedOutputPreviewMode]);
|
|
226
|
+
|
|
102
227
|
const containerStyle = [
|
|
103
228
|
resolvedTheme.containerStyle ?? DEFAULT_THEME.containerStyle,
|
|
104
229
|
];
|
|
105
|
-
const inputAreaStyle = [
|
|
106
|
-
styles.inputArea,
|
|
107
|
-
{ minHeight },
|
|
108
|
-
maxHeight ? { maxHeight } : undefined,
|
|
109
|
-
];
|
|
230
|
+
const inputAreaStyle = [styles.inputArea];
|
|
110
231
|
const inputStyle = [
|
|
111
232
|
styles.textInput,
|
|
112
233
|
resolvedTheme.baseTextStyle ?? DEFAULT_THEME.baseTextStyle,
|
|
113
234
|
resolvedTheme.inputStyle ?? DEFAULT_THEME.inputStyle,
|
|
235
|
+
{ height: inputHeight },
|
|
114
236
|
textInputProps?.style,
|
|
115
237
|
];
|
|
116
238
|
const outputAnimatedStyle = {
|
|
117
239
|
maxHeight: previewProgress.interpolate({
|
|
118
240
|
inputRange: [0, 1],
|
|
119
|
-
outputRange: [0,
|
|
241
|
+
outputRange: [0, maxOutputHeight + 72],
|
|
120
242
|
}),
|
|
121
243
|
opacity: previewProgress,
|
|
122
244
|
marginTop: previewProgress.interpolate({
|
|
123
245
|
inputRange: [0, 1],
|
|
124
246
|
outputRange: [0, 12],
|
|
125
247
|
}),
|
|
248
|
+
marginBottom: previewProgress.interpolate({
|
|
249
|
+
inputRange: [0, 1],
|
|
250
|
+
outputRange: [0, 16],
|
|
251
|
+
}),
|
|
126
252
|
transform: [
|
|
127
253
|
{
|
|
128
254
|
translateY: previewProgress.interpolate({
|
|
@@ -148,6 +274,12 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
148
274
|
state={state}
|
|
149
275
|
items={toolbarItems}
|
|
150
276
|
theme={resolvedTheme}
|
|
277
|
+
outputFormat={resolvedOutputFormat}
|
|
278
|
+
outputPreviewMode={resolvedOutputPreviewMode}
|
|
279
|
+
onOutputFormatChange={handleOutputFormatChange}
|
|
280
|
+
onOutputPreviewModeChange={handleOutputPreviewModeChange}
|
|
281
|
+
onRequestLink={handleRequestLink}
|
|
282
|
+
onRequestImage={handleRequestImage}
|
|
151
283
|
renderToolbar={renderToolbar}
|
|
152
284
|
/>
|
|
153
285
|
) : null;
|
|
@@ -180,6 +312,7 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
180
312
|
value={plainText}
|
|
181
313
|
onChangeText={actions.handleTextChange}
|
|
182
314
|
onSelectionChange={onSelectionChange}
|
|
315
|
+
onContentSizeChange={onContentSizeChange}
|
|
183
316
|
multiline={multiline}
|
|
184
317
|
placeholder={placeholder}
|
|
185
318
|
placeholderTextColor={
|
|
@@ -193,7 +326,7 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
193
326
|
resolvedTheme.colors?.cursor ?? DEFAULT_THEME.colors?.cursor
|
|
194
327
|
}
|
|
195
328
|
textAlignVertical="top"
|
|
196
|
-
scrollEnabled={
|
|
329
|
+
scrollEnabled={shouldScrollInput}
|
|
197
330
|
/>
|
|
198
331
|
|
|
199
332
|
{showOutputPreview && (
|
|
@@ -203,12 +336,22 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
203
336
|
>
|
|
204
337
|
<View style={outputContainerStyle}>
|
|
205
338
|
<Text style={outputLabelStyle}>
|
|
206
|
-
{
|
|
339
|
+
{outputLabel}
|
|
207
340
|
</Text>
|
|
208
|
-
<ScrollView
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
341
|
+
<ScrollView
|
|
342
|
+
style={{ maxHeight: maxOutputHeight }}
|
|
343
|
+
showsVerticalScrollIndicator={false}
|
|
344
|
+
>
|
|
345
|
+
{resolvedOutputPreviewMode === 'rendered' ? (
|
|
346
|
+
<RenderedOutput
|
|
347
|
+
segments={state.segments}
|
|
348
|
+
theme={resolvedTheme}
|
|
349
|
+
/>
|
|
350
|
+
) : (
|
|
351
|
+
<Text selectable style={outputTextStyle}>
|
|
352
|
+
{serializedOutput}
|
|
353
|
+
</Text>
|
|
354
|
+
)}
|
|
212
355
|
</ScrollView>
|
|
213
356
|
</View>
|
|
214
357
|
</Animated.View>
|
|
@@ -235,3 +378,38 @@ const styles = StyleSheet.create({
|
|
|
235
378
|
overflow: 'hidden',
|
|
236
379
|
},
|
|
237
380
|
});
|
|
381
|
+
|
|
382
|
+
function detectLinkTarget(value: string): string | undefined {
|
|
383
|
+
const trimmed = value.trim();
|
|
384
|
+
if (trimmed.length === 0) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (/^https?:\/\//i.test(trimmed) || /^mailto:/i.test(trimmed)) {
|
|
389
|
+
return trimmed;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
|
|
393
|
+
return `mailto:${trimmed}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (/^[^\s]+\.[^\s]+$/.test(trimmed)) {
|
|
397
|
+
return `https://${trimmed}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function detectImageSource(value: string): string | undefined {
|
|
404
|
+
const normalized = detectLinkTarget(value);
|
|
405
|
+
if (!normalized) {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const candidate = normalized.replace(/^mailto:/i, '');
|
|
410
|
+
if (/\.(png|jpe?g|gif|webp|svg)(\?.*)?$/i.test(candidate)) {
|
|
411
|
+
return normalized;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
@@ -14,7 +14,20 @@ import { ToolbarButton } from './ToolbarButton';
|
|
|
14
14
|
* - Horizontal scrolling for overflow
|
|
15
15
|
*/
|
|
16
16
|
export const Toolbar: React.FC<ToolbarProps> = React.memo(
|
|
17
|
-
({
|
|
17
|
+
({
|
|
18
|
+
actions,
|
|
19
|
+
state,
|
|
20
|
+
items,
|
|
21
|
+
theme,
|
|
22
|
+
visible = true,
|
|
23
|
+
outputFormat = 'markdown',
|
|
24
|
+
outputPreviewMode = 'literal',
|
|
25
|
+
onOutputFormatChange,
|
|
26
|
+
onOutputPreviewModeChange,
|
|
27
|
+
onRequestLink,
|
|
28
|
+
onRequestImage,
|
|
29
|
+
renderToolbar,
|
|
30
|
+
}) => {
|
|
18
31
|
const resolvedTheme = theme ?? DEFAULT_THEME;
|
|
19
32
|
const toolbarItems = items ?? DEFAULT_TOOLBAR_ITEMS;
|
|
20
33
|
|
|
@@ -33,12 +46,32 @@ export const Toolbar: React.FC<ToolbarProps> = React.memo(
|
|
|
33
46
|
isActive = selectionStyle.heading === item.heading;
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
if (item.listType) {
|
|
50
|
+
isActive = selectionStyle.listType === item.listType;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (item.textAlign) {
|
|
54
|
+
isActive = selectionStyle.textAlign === item.textAlign;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (item.outputFormat) {
|
|
58
|
+
isActive = outputFormat === item.outputFormat;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (item.outputPreviewMode) {
|
|
62
|
+
isActive = outputPreviewMode === item.outputPreviewMode;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (item.actionType === 'link') {
|
|
66
|
+
isActive = !!selectionStyle.link;
|
|
67
|
+
}
|
|
68
|
+
|
|
36
69
|
return {
|
|
37
70
|
...item,
|
|
38
71
|
active: item.active ?? isActive,
|
|
39
72
|
};
|
|
40
73
|
});
|
|
41
|
-
}, [actions, toolbarItems]);
|
|
74
|
+
}, [actions, outputFormat, outputPreviewMode, toolbarItems]);
|
|
42
75
|
|
|
43
76
|
// Custom render
|
|
44
77
|
if (renderToolbar) {
|
|
@@ -46,6 +79,13 @@ export const Toolbar: React.FC<ToolbarProps> = React.memo(
|
|
|
46
79
|
items: enrichedItems,
|
|
47
80
|
state,
|
|
48
81
|
actions,
|
|
82
|
+
outputFormat,
|
|
83
|
+
outputPreviewMode,
|
|
84
|
+
onOutputFormatChange: onOutputFormatChange ?? (() => undefined),
|
|
85
|
+
onOutputPreviewModeChange:
|
|
86
|
+
onOutputPreviewModeChange ?? (() => undefined),
|
|
87
|
+
onRequestLink,
|
|
88
|
+
onRequestImage,
|
|
49
89
|
});
|
|
50
90
|
}
|
|
51
91
|
|
|
@@ -79,6 +119,18 @@ export const Toolbar: React.FC<ToolbarProps> = React.memo(
|
|
|
79
119
|
actions.toggleFormat(item.format);
|
|
80
120
|
} else if (item.heading) {
|
|
81
121
|
actions.setHeading(item.heading);
|
|
122
|
+
} else if (item.listType) {
|
|
123
|
+
actions.setListType(item.listType);
|
|
124
|
+
} else if (item.textAlign) {
|
|
125
|
+
actions.setTextAlign(item.textAlign);
|
|
126
|
+
} else if (item.outputFormat) {
|
|
127
|
+
onOutputFormatChange?.(item.outputFormat);
|
|
128
|
+
} else if (item.outputPreviewMode) {
|
|
129
|
+
onOutputPreviewModeChange?.(item.outputPreviewMode);
|
|
130
|
+
} else if (item.actionType === 'link') {
|
|
131
|
+
onRequestLink?.();
|
|
132
|
+
} else if (item.actionType === 'image') {
|
|
133
|
+
onRequestImage?.();
|
|
82
134
|
}
|
|
83
135
|
}}
|
|
84
136
|
/>
|
|
@@ -11,6 +11,7 @@ export declare const DEFAULT_COLORS: {
|
|
|
11
11
|
readonly toolbarBorder: "#E5E7EB";
|
|
12
12
|
readonly outputBackground: "#F8FAFC";
|
|
13
13
|
readonly outputLabel: "#475569";
|
|
14
|
+
readonly link: "#2563EB";
|
|
14
15
|
readonly cursor: "#6366F1";
|
|
15
16
|
readonly activeButtonBg: "#EEF2FF";
|
|
16
17
|
readonly codeBackground: "#F3F4F6";
|
|
@@ -34,7 +35,7 @@ export declare const DEFAULT_BASE_TEXT_STYLE: {
|
|
|
34
35
|
readonly fontFamily: undefined;
|
|
35
36
|
};
|
|
36
37
|
/**
|
|
37
|
-
* Empty format style
|
|
38
|
+
* Empty format style — no formatting applied.
|
|
38
39
|
*/
|
|
39
40
|
export declare const EMPTY_FORMAT_STYLE: FormatStyle;
|
|
40
41
|
/**
|
|
@@ -12,6 +12,7 @@ export const DEFAULT_COLORS = {
|
|
|
12
12
|
toolbarBorder: '#E5E7EB',
|
|
13
13
|
outputBackground: '#F8FAFC',
|
|
14
14
|
outputLabel: '#475569',
|
|
15
|
+
link: '#2563EB',
|
|
15
16
|
cursor: '#6366F1',
|
|
16
17
|
activeButtonBg: '#EEF2FF',
|
|
17
18
|
codeBackground: '#F3F4F6',
|
|
@@ -50,6 +51,11 @@ export const EMPTY_FORMAT_STYLE: FormatStyle = {
|
|
|
50
51
|
backgroundColor: undefined,
|
|
51
52
|
fontSize: undefined,
|
|
52
53
|
heading: undefined,
|
|
54
|
+
listType: undefined,
|
|
55
|
+
textAlign: undefined,
|
|
56
|
+
link: undefined,
|
|
57
|
+
imageSrc: undefined,
|
|
58
|
+
imageAlt: undefined,
|
|
53
59
|
};
|
|
54
60
|
|
|
55
61
|
/**
|
|
@@ -108,6 +114,9 @@ export const DEFAULT_THEME: RichTextTheme = {
|
|
|
108
114
|
color: DEFAULT_COLORS.text,
|
|
109
115
|
fontFamily: 'monospace',
|
|
110
116
|
},
|
|
117
|
+
renderedOutputStyle: {
|
|
118
|
+
gap: 10,
|
|
119
|
+
},
|
|
111
120
|
toolbarStyle: {
|
|
112
121
|
flexDirection: 'row',
|
|
113
122
|
alignItems: 'center',
|
|
@@ -150,6 +159,7 @@ export const DEFAULT_THEME: RichTextTheme = {
|
|
|
150
159
|
placeholder: DEFAULT_COLORS.placeholder,
|
|
151
160
|
toolbarBackground: DEFAULT_COLORS.toolbarBackground,
|
|
152
161
|
toolbarBorder: DEFAULT_COLORS.toolbarBorder,
|
|
162
|
+
link: DEFAULT_COLORS.link,
|
|
153
163
|
cursor: DEFAULT_COLORS.cursor,
|
|
154
164
|
},
|
|
155
165
|
};
|
|
@@ -166,4 +176,15 @@ export const DEFAULT_TOOLBAR_ITEMS: ToolbarItem[] = [
|
|
|
166
176
|
{ id: 'h1', label: 'H1', heading: 'h1' },
|
|
167
177
|
{ id: 'h2', label: 'H2', heading: 'h2' },
|
|
168
178
|
{ id: 'h3', label: 'H3', heading: 'h3' },
|
|
179
|
+
{ id: 'bullet', label: '\u2022', listType: 'bullet' },
|
|
180
|
+
{ id: 'ordered', label: '1.', listType: 'ordered' },
|
|
181
|
+
{ id: 'link', label: 'Link', actionType: 'link' },
|
|
182
|
+
{ id: 'image', label: 'Img', actionType: 'image' },
|
|
183
|
+
{ id: 'align-left', label: 'L', textAlign: 'left' },
|
|
184
|
+
{ id: 'align-center', label: 'C', textAlign: 'center' },
|
|
185
|
+
{ id: 'align-right', label: 'R', textAlign: 'right' },
|
|
186
|
+
{ id: 'format-markdown', label: 'MD', outputFormat: 'markdown' },
|
|
187
|
+
{ id: 'format-html', label: 'HTML', outputFormat: 'html' },
|
|
188
|
+
{ id: 'preview-literal', label: 'Raw', outputPreviewMode: 'literal' },
|
|
189
|
+
{ id: 'preview-rendered', label: 'View', outputPreviewMode: 'rendered' },
|
|
169
190
|
];
|
|
@@ -4,12 +4,16 @@ import type {
|
|
|
4
4
|
FormatType,
|
|
5
5
|
FormatStyle,
|
|
6
6
|
HeadingLevel,
|
|
7
|
+
ListType,
|
|
7
8
|
SelectionRange,
|
|
9
|
+
TextAlign,
|
|
8
10
|
} from '../types';
|
|
9
11
|
import {
|
|
10
12
|
toggleFormatOnSelection,
|
|
11
13
|
setStyleOnSelection,
|
|
12
14
|
setHeadingOnLine,
|
|
15
|
+
setListTypeOnLine,
|
|
16
|
+
setTextAlignOnLine,
|
|
13
17
|
isFormatActiveInSelection,
|
|
14
18
|
getSelectionStyle,
|
|
15
19
|
} from '../utils/formatter';
|
|
@@ -78,14 +82,66 @@ export function useFormatting({
|
|
|
78
82
|
|
|
79
83
|
const setHeading = useCallback(
|
|
80
84
|
(level: HeadingLevel) => {
|
|
85
|
+
const currentHeading =
|
|
86
|
+
selection.start === selection.end
|
|
87
|
+
? getSelectionStyle(segments, selection).heading ?? activeStyles.heading
|
|
88
|
+
: getSelectionStyle(segments, selection).heading;
|
|
89
|
+
const nextHeading: HeadingLevel =
|
|
90
|
+
currentHeading === level ? 'none' : level;
|
|
91
|
+
|
|
92
|
+
if (selection.start === selection.end) {
|
|
93
|
+
onActiveStylesChange({
|
|
94
|
+
...activeStyles,
|
|
95
|
+
heading: nextHeading === 'none' ? undefined : nextHeading,
|
|
96
|
+
listType:
|
|
97
|
+
nextHeading === 'none' ? activeStyles.listType : undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const newSegments = setHeadingOnLine(segments, selection, nextHeading);
|
|
102
|
+
onSegmentsChange(newSegments);
|
|
103
|
+
},
|
|
104
|
+
[
|
|
105
|
+
activeStyles,
|
|
106
|
+
onActiveStylesChange,
|
|
107
|
+
onSegmentsChange,
|
|
108
|
+
segments,
|
|
109
|
+
selection,
|
|
110
|
+
],
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const setListType = useCallback(
|
|
114
|
+
(listType: ListType) => {
|
|
115
|
+
if (selection.start === selection.end) {
|
|
116
|
+
onActiveStylesChange({
|
|
117
|
+
...activeStyles,
|
|
118
|
+
listType: listType === 'none' ? undefined : listType,
|
|
119
|
+
heading: listType === 'none' ? activeStyles.heading : undefined,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const newSegments = setListTypeOnLine(segments, selection, listType);
|
|
124
|
+
onSegmentsChange(newSegments);
|
|
125
|
+
},
|
|
126
|
+
[
|
|
127
|
+
activeStyles,
|
|
128
|
+
onActiveStylesChange,
|
|
129
|
+
onSegmentsChange,
|
|
130
|
+
segments,
|
|
131
|
+
selection,
|
|
132
|
+
],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const setTextAlign = useCallback(
|
|
136
|
+
(textAlign: TextAlign) => {
|
|
81
137
|
if (selection.start === selection.end) {
|
|
82
138
|
onActiveStylesChange({
|
|
83
139
|
...activeStyles,
|
|
84
|
-
|
|
140
|
+
textAlign,
|
|
85
141
|
});
|
|
86
142
|
}
|
|
87
143
|
|
|
88
|
-
const newSegments =
|
|
144
|
+
const newSegments = setTextAlignOnLine(segments, selection, textAlign);
|
|
89
145
|
onSegmentsChange(newSegments);
|
|
90
146
|
},
|
|
91
147
|
[
|
|
@@ -97,6 +153,21 @@ export function useFormatting({
|
|
|
97
153
|
],
|
|
98
154
|
);
|
|
99
155
|
|
|
156
|
+
const setLink = useCallback(
|
|
157
|
+
(url?: string) => {
|
|
158
|
+
if (selection.start === selection.end) {
|
|
159
|
+
onActiveStylesChange({
|
|
160
|
+
...activeStyles,
|
|
161
|
+
link: url,
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
const newSegments = setStyleOnSelection(segments, selection, 'link', url);
|
|
165
|
+
onSegmentsChange(newSegments);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
[segments, selection, activeStyles, onSegmentsChange, onActiveStylesChange],
|
|
169
|
+
);
|
|
170
|
+
|
|
100
171
|
const setColor = useCallback(
|
|
101
172
|
(color: string) => {
|
|
102
173
|
setStyleProperty('color', color);
|
|
@@ -139,6 +210,9 @@ export function useFormatting({
|
|
|
139
210
|
toggleFormat,
|
|
140
211
|
setStyleProperty,
|
|
141
212
|
setHeading,
|
|
213
|
+
setListType,
|
|
214
|
+
setTextAlign,
|
|
215
|
+
setLink,
|
|
142
216
|
setColor,
|
|
143
217
|
setBackgroundColor,
|
|
144
218
|
setFontSize,
|