react-native-richify 1.0.4 → 1.0.6
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 +383 -120
- package/lib/commonjs/components/RenderedOutput.js +168 -0
- package/lib/commonjs/components/RenderedOutput.js.map +1 -0
- package/lib/commonjs/components/RichTextInput.js +130 -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 +51 -1
- package/lib/commonjs/constants/defaultStyles.js.map +1 -1
- package/lib/commonjs/hooks/useFormatting.js +44 -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 +131 -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 +51 -1
- package/lib/module/constants/defaultStyles.js.map +1 -1
- package/lib/module/hooks/useFormatting.js +45 -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 +193 -19
- package/src/components/Toolbar.tsx +54 -2
- package/src/constants/defaultStyles.d.ts +2 -1
- package/src/constants/defaultStyles.ts +20 -0
- package/src/hooks/useFormatting.ts +101 -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,24 +147,98 @@ 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({
|
|
@@ -148,6 +270,12 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
148
270
|
state={state}
|
|
149
271
|
items={toolbarItems}
|
|
150
272
|
theme={resolvedTheme}
|
|
273
|
+
outputFormat={resolvedOutputFormat}
|
|
274
|
+
outputPreviewMode={resolvedOutputPreviewMode}
|
|
275
|
+
onOutputFormatChange={handleOutputFormatChange}
|
|
276
|
+
onOutputPreviewModeChange={handleOutputPreviewModeChange}
|
|
277
|
+
onRequestLink={handleRequestLink}
|
|
278
|
+
onRequestImage={handleRequestImage}
|
|
151
279
|
renderToolbar={renderToolbar}
|
|
152
280
|
/>
|
|
153
281
|
) : null;
|
|
@@ -180,6 +308,7 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
180
308
|
value={plainText}
|
|
181
309
|
onChangeText={actions.handleTextChange}
|
|
182
310
|
onSelectionChange={onSelectionChange}
|
|
311
|
+
onContentSizeChange={onContentSizeChange}
|
|
183
312
|
multiline={multiline}
|
|
184
313
|
placeholder={placeholder}
|
|
185
314
|
placeholderTextColor={
|
|
@@ -193,7 +322,7 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
193
322
|
resolvedTheme.colors?.cursor ?? DEFAULT_THEME.colors?.cursor
|
|
194
323
|
}
|
|
195
324
|
textAlignVertical="top"
|
|
196
|
-
scrollEnabled={
|
|
325
|
+
scrollEnabled={shouldScrollInput}
|
|
197
326
|
/>
|
|
198
327
|
|
|
199
328
|
{showOutputPreview && (
|
|
@@ -203,12 +332,22 @@ export const RichTextInput: React.FC<RichTextInputProps> = ({
|
|
|
203
332
|
>
|
|
204
333
|
<View style={outputContainerStyle}>
|
|
205
334
|
<Text style={outputLabelStyle}>
|
|
206
|
-
{
|
|
335
|
+
{outputLabel}
|
|
207
336
|
</Text>
|
|
208
|
-
<ScrollView
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
337
|
+
<ScrollView
|
|
338
|
+
style={{ maxHeight: maxOutputHeight }}
|
|
339
|
+
showsVerticalScrollIndicator={false}
|
|
340
|
+
>
|
|
341
|
+
{resolvedOutputPreviewMode === 'rendered' ? (
|
|
342
|
+
<RenderedOutput
|
|
343
|
+
segments={state.segments}
|
|
344
|
+
theme={resolvedTheme}
|
|
345
|
+
/>
|
|
346
|
+
) : (
|
|
347
|
+
<Text selectable style={outputTextStyle}>
|
|
348
|
+
{serializedOutput}
|
|
349
|
+
</Text>
|
|
350
|
+
)}
|
|
212
351
|
</ScrollView>
|
|
213
352
|
</View>
|
|
214
353
|
</Animated.View>
|
|
@@ -235,3 +374,38 @@ const styles = StyleSheet.create({
|
|
|
235
374
|
overflow: 'hidden',
|
|
236
375
|
},
|
|
237
376
|
});
|
|
377
|
+
|
|
378
|
+
function detectLinkTarget(value: string): string | undefined {
|
|
379
|
+
const trimmed = value.trim();
|
|
380
|
+
if (trimmed.length === 0) {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (/^https?:\/\//i.test(trimmed) || /^mailto:/i.test(trimmed)) {
|
|
385
|
+
return trimmed;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
|
|
389
|
+
return `mailto:${trimmed}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (/^[^\s]+\.[^\s]+$/.test(trimmed)) {
|
|
393
|
+
return `https://${trimmed}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function detectImageSource(value: string): string | undefined {
|
|
400
|
+
const normalized = detectLinkTarget(value);
|
|
401
|
+
if (!normalized) {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const candidate = normalized.replace(/^mailto:/i, '');
|
|
406
|
+
if (/\.(png|jpe?g|gif|webp|svg)(\?.*)?$/i.test(candidate)) {
|
|
407
|
+
return normalized;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
@@ -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,14 @@ 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\u2261', listType: 'bullet' },
|
|
180
|
+
{ id: 'ordered', label: '1\u2261', listType: 'ordered' },
|
|
181
|
+
{ id: 'link', label: '\ud83d\udd17', actionType: 'link' },
|
|
182
|
+
{ id: 'align-left', label: '\u21e4', textAlign: 'left' },
|
|
183
|
+
{ id: 'align-center', label: '\u2194', textAlign: 'center' },
|
|
184
|
+
{ id: 'align-right', label: '\u21e5', textAlign: 'right' },
|
|
185
|
+
{ id: 'format-markdown', label: 'MD', outputFormat: 'markdown' },
|
|
186
|
+
{ id: 'format-html', label: 'HTML', outputFormat: 'html' },
|
|
187
|
+
{ id: 'preview-literal', label: 'Raw', outputPreviewMode: 'literal' },
|
|
188
|
+
{ id: 'preview-rendered', label: 'View', outputPreviewMode: 'rendered' },
|
|
169
189
|
];
|
|
@@ -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,91 @@ 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
|
+
const currentListType =
|
|
116
|
+
selection.start === selection.end
|
|
117
|
+
? getSelectionStyle(segments, selection).listType ??
|
|
118
|
+
activeStyles.listType
|
|
119
|
+
: getSelectionStyle(segments, selection).listType;
|
|
120
|
+
const nextListType: ListType =
|
|
121
|
+
currentListType === listType ? 'none' : listType;
|
|
122
|
+
|
|
123
|
+
if (selection.start === selection.end) {
|
|
124
|
+
onActiveStylesChange({
|
|
125
|
+
...activeStyles,
|
|
126
|
+
listType: nextListType === 'none' ? undefined : nextListType,
|
|
127
|
+
heading:
|
|
128
|
+
nextListType === 'none' ? activeStyles.heading : undefined,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const newSegments = setListTypeOnLine(
|
|
133
|
+
segments,
|
|
134
|
+
selection,
|
|
135
|
+
nextListType,
|
|
136
|
+
);
|
|
137
|
+
onSegmentsChange(newSegments);
|
|
138
|
+
},
|
|
139
|
+
[
|
|
140
|
+
activeStyles,
|
|
141
|
+
onActiveStylesChange,
|
|
142
|
+
onSegmentsChange,
|
|
143
|
+
segments,
|
|
144
|
+
selection,
|
|
145
|
+
],
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const setTextAlign = useCallback(
|
|
149
|
+
(textAlign: TextAlign) => {
|
|
150
|
+
const currentTextAlign =
|
|
151
|
+
selection.start === selection.end
|
|
152
|
+
? getSelectionStyle(segments, selection).textAlign ??
|
|
153
|
+
activeStyles.textAlign
|
|
154
|
+
: getSelectionStyle(segments, selection).textAlign;
|
|
155
|
+
const nextTextAlign =
|
|
156
|
+
currentTextAlign === textAlign ? undefined : textAlign;
|
|
157
|
+
|
|
81
158
|
if (selection.start === selection.end) {
|
|
82
159
|
onActiveStylesChange({
|
|
83
160
|
...activeStyles,
|
|
84
|
-
|
|
161
|
+
textAlign: nextTextAlign,
|
|
85
162
|
});
|
|
86
163
|
}
|
|
87
164
|
|
|
88
|
-
const newSegments =
|
|
165
|
+
const newSegments = setTextAlignOnLine(
|
|
166
|
+
segments,
|
|
167
|
+
selection,
|
|
168
|
+
nextTextAlign,
|
|
169
|
+
);
|
|
89
170
|
onSegmentsChange(newSegments);
|
|
90
171
|
},
|
|
91
172
|
[
|
|
@@ -97,6 +178,21 @@ export function useFormatting({
|
|
|
97
178
|
],
|
|
98
179
|
);
|
|
99
180
|
|
|
181
|
+
const setLink = useCallback(
|
|
182
|
+
(url?: string) => {
|
|
183
|
+
if (selection.start === selection.end) {
|
|
184
|
+
onActiveStylesChange({
|
|
185
|
+
...activeStyles,
|
|
186
|
+
link: url,
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
const newSegments = setStyleOnSelection(segments, selection, 'link', url);
|
|
190
|
+
onSegmentsChange(newSegments);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
[segments, selection, activeStyles, onSegmentsChange, onActiveStylesChange],
|
|
194
|
+
);
|
|
195
|
+
|
|
100
196
|
const setColor = useCallback(
|
|
101
197
|
(color: string) => {
|
|
102
198
|
setStyleProperty('color', color);
|
|
@@ -139,6 +235,9 @@ export function useFormatting({
|
|
|
139
235
|
toggleFormat,
|
|
140
236
|
setStyleProperty,
|
|
141
237
|
setHeading,
|
|
238
|
+
setListType,
|
|
239
|
+
setTextAlign,
|
|
240
|
+
setLink,
|
|
142
241
|
setColor,
|
|
143
242
|
setBackgroundColor,
|
|
144
243
|
setFontSize,
|