react-native-marcus 0.1.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 +201 -0
- package/README.md +227 -0
- package/RNMarcus.podspec +78 -0
- package/android/build.gradle +220 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/cpp/CMakeLists.txt +41 -0
- package/android/src/main/cpp/MarkdownParser.cpp +49 -0
- package/android/src/main/cpp/MarkdownParser.h +56 -0
- package/android/src/main/cpp/OnLoad.cpp +17 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusModule.kt +28 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +31 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +63 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManager.kt +32 -0
- package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManagerSpec.kt +21 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +333 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownParseResult.kt +12 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownParser.kt +89 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownRange.kt +11 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +81 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownTextWatcher.kt +17 -0
- package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +84 -0
- package/android/src/main/java/app/getbullet/marcus/ReactLineHeight.java +29 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +6 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +105 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +67 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +6 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontFamilySpan.kt +28 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontSizeSpan.kt +7 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownForegroundColorSpan.kt +6 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +57 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +6 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLineHeightSpan.kt +21 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownSpan.kt +7 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownStrikethroughSpan.kt +5 -0
- package/android/src/main/java/app/getbullet/marcus/spans/MarkdownUnderlineSpan.kt +5 -0
- package/android/src/main/new_arch/CMakeLists.txt +70 -0
- package/android/src/main/new_arch/RNMarcusSpec.cpp +43 -0
- package/android/src/main/new_arch/RNMarcusSpec.h +36 -0
- package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.cpp +218 -0
- package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.h +54 -0
- package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +156 -0
- package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +130 -0
- package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +205 -0
- package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +78 -0
- package/android/src/test/resources/render-model.txt +583 -0
- package/apple/MarcusModule.h +8 -0
- package/apple/MarcusModule.mm +26 -0
- package/apple/MarcusRange.h +15 -0
- package/apple/MarcusRange.mm +15 -0
- package/apple/MarcusTextInputDecoratorComponentView.h +12 -0
- package/apple/MarcusTextInputDecoratorComponentView.mm +121 -0
- package/apple/MarcusTextInputDecoratorShadowNode.cpp +251 -0
- package/apple/MarcusTextInputDecoratorShadowNode.h +65 -0
- package/apple/MarcusTextInputDecoratorShadowNode.mm +204 -0
- package/apple/MarcusTextInputDecoratorViewManager.h +8 -0
- package/apple/MarcusTextInputDecoratorViewManager.mm +11 -0
- package/apple/MarkdownAdaptiveImageGlyph.swift +44 -0
- package/apple/MarkdownAttributes.h +31 -0
- package/apple/MarkdownAttributes.m +9 -0
- package/apple/MarkdownBackedTextInputDelegate.swift +117 -0
- package/apple/MarkdownDecorator.swift +316 -0
- package/apple/MarkdownFonts.swift +21 -0
- package/apple/MarkdownFormatter+React.swift +51 -0
- package/apple/MarkdownFormatter.swift +509 -0
- package/apple/MarkdownLog.h +10 -0
- package/apple/MarkdownLog.mm +6 -0
- package/apple/MarkdownParser.swift +200 -0
- package/apple/MarkdownSwiftInterop.h +23 -0
- package/apple/MarkdownTextBackground.swift +17 -0
- package/apple/MarkdownTextFieldObserver.swift +90 -0
- package/apple/MarkdownTextLayoutFragment.swift +205 -0
- package/apple/MarkdownTextLayoutManagerDelegate.swift +84 -0
- package/apple/MarkdownTextStorageDelegate.swift +32 -0
- package/apple/MarkdownTextViewObserver.swift +30 -0
- package/apple/RCTMarcusStyle+Codegen.h +18 -0
- package/apple/RCTMarcusStyle+Codegen.mm +64 -0
- package/apple/RCTMarcusStyle.h +41 -0
- package/apple/RCTMarcusStyle.m +5 -0
- package/apple/RCTMarcusUtils.h +26 -0
- package/apple/RCTMarcusUtils.mm +26 -0
- package/apple/RCTTextInput+AdaptiveImageGlyph.h +16 -0
- package/apple/RCTTextInput+AdaptiveImageGlyph.mm +79 -0
- package/apple/RCTTextInputComponentView+Marcus.h +11 -0
- package/apple/RCTTextInputComponentView+Marcus.mm +79 -0
- package/cpp/MarkdownCxx.modulemap +8 -0
- package/cpp/MarkdownGlobal.cpp +48 -0
- package/cpp/MarkdownGlobal.h +33 -0
- package/cpp/MarkdownWorkletParser.cpp +93 -0
- package/cpp/MarkdownWorkletParser.h +45 -0
- package/cpp/RuntimeDecorator.cpp +39 -0
- package/cpp/RuntimeDecorator.h +14 -0
- package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorState.h +33 -0
- package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorViewComponentDescriptor.h +17 -0
- package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
- package/lib/commonjs/MarkdownTextInput.js +99 -0
- package/lib/commonjs/MarkdownTextInput.js.map +1 -0
- package/lib/commonjs/MarkdownTextInput.web.js +618 -0
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -0
- package/lib/commonjs/NativeMarcusModule.js +9 -0
- package/lib/commonjs/NativeMarcusModule.js.map +1 -0
- package/lib/commonjs/commonTypes.js +2 -0
- package/lib/commonjs/commonTypes.js.map +1 -0
- package/lib/commonjs/global.d.js +2 -0
- package/lib/commonjs/global.d.js.map +1 -0
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/parser/index.js +7030 -0
- package/lib/commonjs/parser/index.js.map +7 -0
- package/lib/commonjs/rangeUtils.js +71 -0
- package/lib/commonjs/rangeUtils.js.map +1 -0
- package/lib/commonjs/react-native-web.d.js +2 -0
- package/lib/commonjs/react-native-web.d.js.map +1 -0
- package/lib/commonjs/styleUtils.js +91 -0
- package/lib/commonjs/styleUtils.js.map +1 -0
- package/lib/commonjs/web/InputHistory.js +111 -0
- package/lib/commonjs/web/InputHistory.js.map +1 -0
- package/lib/commonjs/web/MarkdownTextInput.css +72 -0
- package/lib/commonjs/web/utils/blockLayout.js +132 -0
- package/lib/commonjs/web/utils/blockLayout.js.map +1 -0
- package/lib/commonjs/web/utils/blockUtils.js +219 -0
- package/lib/commonjs/web/utils/blockUtils.js.map +1 -0
- package/lib/commonjs/web/utils/browserUtils.js +40 -0
- package/lib/commonjs/web/utils/browserUtils.js.map +1 -0
- package/lib/commonjs/web/utils/cursorUtils.js +157 -0
- package/lib/commonjs/web/utils/cursorUtils.js.map +1 -0
- package/lib/commonjs/web/utils/inputUtils.js +152 -0
- package/lib/commonjs/web/utils/inputUtils.js.map +1 -0
- package/lib/commonjs/web/utils/measureUtils.js +0 -0
- package/lib/commonjs/web/utils/measureUtils.js.map +1 -0
- package/lib/commonjs/web/utils/parserUtils.js +552 -0
- package/lib/commonjs/web/utils/parserUtils.js.map +1 -0
- package/lib/commonjs/web/utils/treeUtils.js +109 -0
- package/lib/commonjs/web/utils/treeUtils.js.map +1 -0
- package/lib/commonjs/web/utils/webStyleUtils.js +59 -0
- package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -0
- package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
- package/lib/module/MarkdownTextInput.js +94 -0
- package/lib/module/MarkdownTextInput.js.map +1 -0
- package/lib/module/MarkdownTextInput.web.js +612 -0
- package/lib/module/MarkdownTextInput.web.js.map +1 -0
- package/lib/module/NativeMarcusModule.js +5 -0
- package/lib/module/NativeMarcusModule.js.map +1 -0
- package/lib/module/commonTypes.js +2 -0
- package/lib/module/commonTypes.js.map +1 -0
- package/lib/module/global.d.js +2 -0
- package/lib/module/global.d.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/parser/index.js +7029 -0
- package/lib/module/parser/index.js.map +7 -0
- package/lib/module/rangeUtils.js +66 -0
- package/lib/module/rangeUtils.js.map +1 -0
- package/lib/module/react-native-web.d.js +2 -0
- package/lib/module/react-native-web.d.js.map +1 -0
- package/lib/module/styleUtils.js +88 -0
- package/lib/module/styleUtils.js.map +1 -0
- package/lib/module/web/InputHistory.js +106 -0
- package/lib/module/web/InputHistory.js.map +1 -0
- package/lib/module/web/MarkdownTextInput.css +72 -0
- package/lib/module/web/utils/blockLayout.js +128 -0
- package/lib/module/web/utils/blockLayout.js.map +1 -0
- package/lib/module/web/utils/blockUtils.js +210 -0
- package/lib/module/web/utils/blockUtils.js.map +1 -0
- package/lib/module/web/utils/browserUtils.js +36 -0
- package/lib/module/web/utils/browserUtils.js.map +1 -0
- package/lib/module/web/utils/cursorUtils.js +150 -0
- package/lib/module/web/utils/cursorUtils.js.map +1 -0
- package/lib/module/web/utils/inputUtils.js +144 -0
- package/lib/module/web/utils/inputUtils.js.map +1 -0
- package/lib/module/web/utils/measureUtils.js +0 -0
- package/lib/module/web/utils/measureUtils.js.map +1 -0
- package/lib/module/web/utils/parserUtils.js +547 -0
- package/lib/module/web/utils/parserUtils.js.map +1 -0
- package/lib/module/web/utils/treeUtils.js +103 -0
- package/lib/module/web/utils/treeUtils.js.map +1 -0
- package/lib/module/web/utils/webStyleUtils.js +54 -0
- package/lib/module/web/utils/webStyleUtils.js.map +1 -0
- package/lib/typescript/commonjs/e2e/checks.test.d.ts +2 -0
- package/lib/typescript/commonjs/e2e/checks.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/e2e/editing.test.d.ts +2 -0
- package/lib/typescript/commonjs/e2e/editing.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/e2e/helpers.d.ts +23 -0
- package/lib/typescript/commonjs/e2e/helpers.d.ts.map +1 -0
- package/lib/typescript/commonjs/e2e/smoke.test.d.ts +2 -0
- package/lib/typescript/commonjs/e2e/smoke.test.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
- package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +22 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +41 -0
- package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts +7 -0
- package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/commonTypes.d.ts +10 -0
- package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +5 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/parser/index.d.ts +8 -0
- package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts +8 -0
- package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/rangeUtils.d.ts +6 -0
- package/lib/typescript/commonjs/src/rangeUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/styleUtils.d.ts +8 -0
- package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/InputHistory.d.ts +24 -0
- package/lib/typescript/commonjs/src/web/InputHistory.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +41 -0
- package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +33 -0
- package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts +7 -0
- package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts +12 -0
- package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts +12 -0
- package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts +22 -0
- package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +44 -0
- package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +25 -0
- package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +7 -0
- package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -0
- package/lib/typescript/module/e2e/checks.test.d.ts +2 -0
- package/lib/typescript/module/e2e/checks.test.d.ts.map +1 -0
- package/lib/typescript/module/e2e/editing.test.d.ts +2 -0
- package/lib/typescript/module/e2e/editing.test.d.ts.map +1 -0
- package/lib/typescript/module/e2e/helpers.d.ts +23 -0
- package/lib/typescript/module/e2e/helpers.d.ts.map +1 -0
- package/lib/typescript/module/e2e/smoke.test.d.ts +2 -0
- package/lib/typescript/module/e2e/smoke.test.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
- package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/MarkdownTextInput.d.ts +22 -0
- package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -0
- package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +41 -0
- package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -0
- package/lib/typescript/module/src/NativeMarcusModule.d.ts +7 -0
- package/lib/typescript/module/src/NativeMarcusModule.d.ts.map +1 -0
- package/lib/typescript/module/src/commonTypes.d.ts +10 -0
- package/lib/typescript/module/src/commonTypes.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +5 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/parser/index.d.ts +8 -0
- package/lib/typescript/module/src/parser/index.d.ts.map +1 -0
- package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts +8 -0
- package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -0
- package/lib/typescript/module/src/rangeUtils.d.ts +6 -0
- package/lib/typescript/module/src/rangeUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/styleUtils.d.ts +8 -0
- package/lib/typescript/module/src/styleUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/InputHistory.d.ts +24 -0
- package/lib/typescript/module/src/web/InputHistory.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/blockLayout.d.ts +41 -0
- package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/blockUtils.d.ts +33 -0
- package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/browserUtils.d.ts +7 -0
- package/lib/typescript/module/src/web/utils/browserUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/cursorUtils.d.ts +12 -0
- package/lib/typescript/module/src/web/utils/cursorUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/inputUtils.d.ts +12 -0
- package/lib/typescript/module/src/web/utils/inputUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/measureUtils.d.ts +22 -0
- package/lib/typescript/module/src/web/utils/measureUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/parserUtils.d.ts +44 -0
- package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/treeUtils.d.ts +25 -0
- package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -0
- package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +7 -0
- package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -0
- package/package.json +167 -0
- package/react-native.config.js +10 -0
- package/src/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
- package/src/MarkdownTextInput.tsx +139 -0
- package/src/MarkdownTextInput.web.tsx +958 -0
- package/src/NativeMarcusModule.ts +8 -0
- package/src/commonTypes.ts +30 -0
- package/src/global.d.ts +1 -0
- package/src/index.tsx +4 -0
- package/src/parser/index.ts +556 -0
- package/src/parser/micromark-extension-mentions.ts +82 -0
- package/src/rangeUtils.ts +68 -0
- package/src/react-native-web.d.ts +1 -0
- package/src/styleUtils.ts +100 -0
- package/src/web/InputHistory.ts +137 -0
- package/src/web/MarkdownTextInput.css +72 -0
- package/src/web/utils/blockLayout.ts +163 -0
- package/src/web/utils/blockUtils.ts +245 -0
- package/src/web/utils/browserUtils.ts +41 -0
- package/src/web/utils/cursorUtils.ts +204 -0
- package/src/web/utils/inputUtils.ts +206 -0
- package/src/web/utils/measureUtils.ts +0 -0
- package/src/web/utils/parserUtils.ts +813 -0
- package/src/web/utils/treeUtils.ts +149 -0
- package/src/web/utils/webStyleUtils.ts +69 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type {
|
|
3
|
+
TextInput,
|
|
4
|
+
TextInputSubmitEditingEventData,
|
|
5
|
+
TextStyle,
|
|
6
|
+
NativeSyntheticEvent,
|
|
7
|
+
TextInputSelectionChangeEventData,
|
|
8
|
+
TextInputProps,
|
|
9
|
+
TextInputKeyPressEventData,
|
|
10
|
+
TextInputFocusEventData,
|
|
11
|
+
TextInputContentSizeChangeEventData,
|
|
12
|
+
GestureResponderEvent,
|
|
13
|
+
} from "react-native"
|
|
14
|
+
import React, { useEffect, useRef, useCallback, useMemo, useLayoutEffect } from "react"
|
|
15
|
+
import type {
|
|
16
|
+
CSSProperties,
|
|
17
|
+
MutableRefObject,
|
|
18
|
+
ReactEventHandler,
|
|
19
|
+
FocusEventHandler,
|
|
20
|
+
MouseEvent,
|
|
21
|
+
KeyboardEvent,
|
|
22
|
+
SyntheticEvent,
|
|
23
|
+
ClipboardEventHandler,
|
|
24
|
+
TouchEvent,
|
|
25
|
+
} from "react"
|
|
26
|
+
import { StyleSheet, TextInput as RNTextInput } from "react-native"
|
|
27
|
+
import { updateInputStructure } from "./web/utils/parserUtils"
|
|
28
|
+
import type { LineCache } from "./web/utils/parserUtils"
|
|
29
|
+
import InputHistory from "./web/InputHistory"
|
|
30
|
+
import type { TreeNode } from "./web/utils/treeUtils"
|
|
31
|
+
import {
|
|
32
|
+
getCurrentCursorPosition,
|
|
33
|
+
removeSelection,
|
|
34
|
+
setCursorPosition,
|
|
35
|
+
} from "./web/utils/cursorUtils"
|
|
36
|
+
import "./web/MarkdownTextInput.css"
|
|
37
|
+
import type { MarkdownStyle } from "./MarcusTextInputDecoratorViewNativeComponent"
|
|
38
|
+
import {
|
|
39
|
+
getElementHeight,
|
|
40
|
+
getPlaceholderValue,
|
|
41
|
+
isEventComposing,
|
|
42
|
+
normalizeValue,
|
|
43
|
+
parseInnerHTMLToText,
|
|
44
|
+
} from "./web/utils/inputUtils"
|
|
45
|
+
import {
|
|
46
|
+
idGenerator,
|
|
47
|
+
parseToReactDOMStyle,
|
|
48
|
+
processMarkdownStyle,
|
|
49
|
+
} from "./web/utils/webStyleUtils"
|
|
50
|
+
import type { MarkdownRange } from "./commonTypes"
|
|
51
|
+
|
|
52
|
+
const useClientEffect = typeof window === "undefined" ? useEffect : useLayoutEffect
|
|
53
|
+
|
|
54
|
+
interface MarkdownTextInputProps extends TextInputProps {
|
|
55
|
+
markdownStyle?: MarkdownStyle
|
|
56
|
+
parser: (text: string) => MarkdownRange[]
|
|
57
|
+
onClick?: (e: MouseEvent<HTMLDivElement>) => void
|
|
58
|
+
dir?: string
|
|
59
|
+
disabled?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface MarkdownNativeEvent extends Event {
|
|
63
|
+
inputType?: string
|
|
64
|
+
isComposing?: boolean
|
|
65
|
+
keyCode?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
type MarkdownTextInput = TextInput & React.Component<MarkdownTextInputProps>
|
|
69
|
+
|
|
70
|
+
type Selection = {
|
|
71
|
+
start: number
|
|
72
|
+
end: number
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type Dimensions = {
|
|
76
|
+
width: number
|
|
77
|
+
height: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ParseTextResult = {
|
|
81
|
+
text: string
|
|
82
|
+
cursorPosition: number | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let focusTimeout: NodeJS.Timeout | null = null
|
|
86
|
+
|
|
87
|
+
type MarkdownTextInputElement = HTMLDivElement &
|
|
88
|
+
HTMLInputElement & {
|
|
89
|
+
tree: TreeNode
|
|
90
|
+
uniqueId: string
|
|
91
|
+
selection: Selection
|
|
92
|
+
/** What each line was last rendered from, so an update can skip rebuilding the unchanged ones. */
|
|
93
|
+
lineCache?: LineCache
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type HTMLMarkdownElement = HTMLElement & {
|
|
97
|
+
value: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const MarkdownTextInput = React.forwardRef<MarkdownTextInput, MarkdownTextInputProps>(
|
|
101
|
+
(
|
|
102
|
+
{
|
|
103
|
+
accessibilityLabel,
|
|
104
|
+
accessibilityLabelledBy,
|
|
105
|
+
accessibilityRole,
|
|
106
|
+
autoCapitalize = "sentences",
|
|
107
|
+
autoCorrect = true,
|
|
108
|
+
blurOnSubmit = false,
|
|
109
|
+
submitBehavior,
|
|
110
|
+
caretHidden,
|
|
111
|
+
clearTextOnFocus,
|
|
112
|
+
dir = "auto",
|
|
113
|
+
disabled = false,
|
|
114
|
+
numberOfLines,
|
|
115
|
+
multiline = false,
|
|
116
|
+
markdownStyle,
|
|
117
|
+
parser,
|
|
118
|
+
onBlur,
|
|
119
|
+
onChange,
|
|
120
|
+
onChangeText,
|
|
121
|
+
onClick,
|
|
122
|
+
onFocus,
|
|
123
|
+
onKeyPress,
|
|
124
|
+
onSelectionChange,
|
|
125
|
+
onSubmitEditing,
|
|
126
|
+
placeholder,
|
|
127
|
+
placeholderTextColor = `rgba(0,0,0,0.2)`,
|
|
128
|
+
selectTextOnFocus,
|
|
129
|
+
spellCheck,
|
|
130
|
+
selection,
|
|
131
|
+
style = {},
|
|
132
|
+
value,
|
|
133
|
+
autoFocus = false,
|
|
134
|
+
onContentSizeChange,
|
|
135
|
+
id,
|
|
136
|
+
inputMode,
|
|
137
|
+
onTouchStart,
|
|
138
|
+
maxLength,
|
|
139
|
+
},
|
|
140
|
+
ref,
|
|
141
|
+
) => {
|
|
142
|
+
if (parser === undefined) {
|
|
143
|
+
throw new Error("[react-native-marcus] `parser` is undefined")
|
|
144
|
+
}
|
|
145
|
+
if (typeof parser !== "function") {
|
|
146
|
+
throw new Error("[react-native-marcus] `parser` is not a function")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const divRef = useRef<MarkdownTextInputElement | null>(null)
|
|
150
|
+
const currentlyFocusedField = useRef<HTMLDivElement | null>(null)
|
|
151
|
+
const contentSelection = useRef<Selection | null>(null)
|
|
152
|
+
const className = `react-native-marcus-input-${multiline ? "multiline" : "singleline"}`
|
|
153
|
+
const history = useRef<InputHistory | null>(null)
|
|
154
|
+
const dimensions = useRef<Dimensions | null>(null)
|
|
155
|
+
const pasteContent = useRef<string | null>(null)
|
|
156
|
+
const hasJustBeenFocused = useRef<boolean>(false)
|
|
157
|
+
|
|
158
|
+
if (!history.current) {
|
|
159
|
+
history.current = new InputHistory(100, 150, value || "")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const flattenedStyle = useMemo(() => StyleSheet.flatten(style), [style])
|
|
163
|
+
// Using JSON.stringify(flattenedMarkdownStyle) as a simple styles object hash to avoid rerenders when not memoized markdownStyle is passed
|
|
164
|
+
const hashedMarkdownStyle = useMemo(
|
|
165
|
+
() => JSON.stringify(StyleSheet.flatten(markdownStyle)),
|
|
166
|
+
[markdownStyle],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Empty placeholder would collapse the div, so we need to use zero-width space to prevent it
|
|
170
|
+
const heightSafePlaceholder = useMemo(() => getPlaceholderValue(placeholder), [placeholder])
|
|
171
|
+
|
|
172
|
+
const setEventProps = useCallback((e: NativeSyntheticEvent<any>) => {
|
|
173
|
+
if (divRef.current) {
|
|
174
|
+
const text = divRef.current.value
|
|
175
|
+
if (e.target) {
|
|
176
|
+
// TODO: change the logic here so every event have value property
|
|
177
|
+
;(e.target as unknown as HTMLInputElement).value = text
|
|
178
|
+
}
|
|
179
|
+
if (e.nativeEvent && e.nativeEvent.text) {
|
|
180
|
+
e.nativeEvent.text = text
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return e
|
|
184
|
+
}, [])
|
|
185
|
+
|
|
186
|
+
const parseText = useCallback(
|
|
187
|
+
(
|
|
188
|
+
parserFunction: (input: string) => MarkdownRange[],
|
|
189
|
+
target: MarkdownTextInputElement,
|
|
190
|
+
text: string | null,
|
|
191
|
+
customMarkdownStyles: MarkdownStyle,
|
|
192
|
+
cursorPosition: number | null = null,
|
|
193
|
+
shouldAddToHistory = true,
|
|
194
|
+
shouldForceDOMUpdate = false,
|
|
195
|
+
shouldScrollIntoView = false,
|
|
196
|
+
): ParseTextResult => {
|
|
197
|
+
if (!divRef.current) {
|
|
198
|
+
return { text: text || "", cursorPosition: null }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (text === null) {
|
|
202
|
+
return { text: divRef.current.value, cursorPosition: null }
|
|
203
|
+
}
|
|
204
|
+
const parsedText = updateInputStructure(
|
|
205
|
+
parserFunction,
|
|
206
|
+
target,
|
|
207
|
+
text,
|
|
208
|
+
cursorPosition,
|
|
209
|
+
multiline,
|
|
210
|
+
customMarkdownStyles,
|
|
211
|
+
false,
|
|
212
|
+
shouldForceDOMUpdate,
|
|
213
|
+
shouldScrollIntoView,
|
|
214
|
+
)
|
|
215
|
+
divRef.current.value = parsedText.text
|
|
216
|
+
|
|
217
|
+
if (history.current && shouldAddToHistory) {
|
|
218
|
+
history.current.throttledAdd(parsedText.text, parsedText.cursorPosition)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return parsedText
|
|
222
|
+
},
|
|
223
|
+
[multiline],
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const processedMarkdownStyle = useMemo(() => {
|
|
227
|
+
const newMarkdownStyle = processMarkdownStyle(markdownStyle)
|
|
228
|
+
if (divRef.current) {
|
|
229
|
+
parseText(
|
|
230
|
+
parser,
|
|
231
|
+
divRef.current,
|
|
232
|
+
divRef.current.value,
|
|
233
|
+
newMarkdownStyle,
|
|
234
|
+
null,
|
|
235
|
+
false,
|
|
236
|
+
false,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
return newMarkdownStyle
|
|
240
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
241
|
+
}, [hashedMarkdownStyle, parser, parseText])
|
|
242
|
+
|
|
243
|
+
const inputStyles = useMemo(
|
|
244
|
+
() =>
|
|
245
|
+
StyleSheet.flatten([
|
|
246
|
+
styles.defaultInputStyles,
|
|
247
|
+
flattenedStyle && {
|
|
248
|
+
caretColor: (flattenedStyle as TextStyle).color || "black",
|
|
249
|
+
},
|
|
250
|
+
{ whiteSpace: multiline ? "pre-wrap" : "pre" },
|
|
251
|
+
disabled && styles.disabledInputStyles,
|
|
252
|
+
parseToReactDOMStyle(flattenedStyle),
|
|
253
|
+
caretHidden && styles.caretHidden,
|
|
254
|
+
]) as CSSProperties,
|
|
255
|
+
[flattenedStyle, multiline, disabled, caretHidden],
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const undo = useCallback(
|
|
259
|
+
(target: MarkdownTextInputElement): ParseTextResult => {
|
|
260
|
+
if (!history.current) {
|
|
261
|
+
return {
|
|
262
|
+
text: "",
|
|
263
|
+
cursorPosition: 0,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const item = history.current.undo()
|
|
267
|
+
const undoValue = item ? item.text : null
|
|
268
|
+
return parseText(
|
|
269
|
+
parser,
|
|
270
|
+
target,
|
|
271
|
+
undoValue,
|
|
272
|
+
processedMarkdownStyle,
|
|
273
|
+
item ? item.cursorPosition : null,
|
|
274
|
+
false,
|
|
275
|
+
)
|
|
276
|
+
},
|
|
277
|
+
[parser, parseText, processedMarkdownStyle],
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const redo = useCallback(
|
|
281
|
+
(target: MarkdownTextInputElement): ParseTextResult => {
|
|
282
|
+
if (!history.current) {
|
|
283
|
+
return {
|
|
284
|
+
text: "",
|
|
285
|
+
cursorPosition: 0,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const item = history.current.redo()
|
|
289
|
+
const redoValue = item ? item.text : null
|
|
290
|
+
return parseText(
|
|
291
|
+
parser,
|
|
292
|
+
target,
|
|
293
|
+
redoValue,
|
|
294
|
+
processedMarkdownStyle,
|
|
295
|
+
item ? item.cursorPosition : null,
|
|
296
|
+
false,
|
|
297
|
+
)
|
|
298
|
+
},
|
|
299
|
+
[parser, parseText, processedMarkdownStyle],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
// Placeholder text color logic
|
|
303
|
+
const updateTextColor = useCallback(
|
|
304
|
+
(node: HTMLDivElement, text: string) => {
|
|
305
|
+
// eslint-disable-next-line no-param-reassign -- we need to change the style of the node, so we need to modify it
|
|
306
|
+
node.style.color = String(
|
|
307
|
+
placeholder && (text === "" || text === "\n")
|
|
308
|
+
? placeholderTextColor
|
|
309
|
+
: flattenedStyle.color || "black",
|
|
310
|
+
)
|
|
311
|
+
},
|
|
312
|
+
[flattenedStyle.color, placeholder, placeholderTextColor],
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
const handleSelectionChange: ReactEventHandler<HTMLDivElement> = useCallback(
|
|
316
|
+
(event) => {
|
|
317
|
+
const e = event as unknown as NativeSyntheticEvent<TextInputSelectionChangeEventData>
|
|
318
|
+
setEventProps(e)
|
|
319
|
+
if (onSelectionChange && contentSelection.current) {
|
|
320
|
+
e.nativeEvent.selection = contentSelection.current
|
|
321
|
+
onSelectionChange(e)
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
[onSelectionChange, setEventProps],
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
const updateRefSelectionVariables = useCallback((newSelection: Selection) => {
|
|
328
|
+
if (!divRef.current) {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
const { start, end } = newSelection
|
|
332
|
+
divRef.current.selection = { start, end }
|
|
333
|
+
}, [])
|
|
334
|
+
|
|
335
|
+
const updateSelection = useCallback(
|
|
336
|
+
(e: SyntheticEvent<HTMLDivElement>, predefinedSelection: Selection | null = null) => {
|
|
337
|
+
if (!divRef.current) {
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
const newSelection = predefinedSelection || getCurrentCursorPosition(divRef.current)
|
|
341
|
+
|
|
342
|
+
if (
|
|
343
|
+
newSelection &&
|
|
344
|
+
(!contentSelection.current ||
|
|
345
|
+
contentSelection.current.start !== newSelection.start ||
|
|
346
|
+
contentSelection.current.end !== newSelection.end)
|
|
347
|
+
) {
|
|
348
|
+
updateRefSelectionVariables(newSelection)
|
|
349
|
+
contentSelection.current = newSelection
|
|
350
|
+
|
|
351
|
+
handleSelectionChange(e)
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
[handleSelectionChange, updateRefSelectionVariables],
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
const handleOnSelect = useCallback(
|
|
358
|
+
(e: React.SyntheticEvent<HTMLDivElement>) => {
|
|
359
|
+
updateSelection(e)
|
|
360
|
+
|
|
361
|
+
// If the input has just been focused, we need to scroll the cursor into view
|
|
362
|
+
if (divRef.current && contentSelection.current && hasJustBeenFocused.current) {
|
|
363
|
+
setCursorPosition(
|
|
364
|
+
divRef.current,
|
|
365
|
+
contentSelection.current?.start,
|
|
366
|
+
contentSelection.current?.end,
|
|
367
|
+
true,
|
|
368
|
+
)
|
|
369
|
+
hasJustBeenFocused.current = false
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
[updateSelection],
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
const handleContentSizeChange = useCallback(() => {
|
|
376
|
+
if (!divRef.current || !multiline || !onContentSizeChange) {
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const { offsetWidth: newWidth, offsetHeight: newHeight } = divRef.current
|
|
381
|
+
|
|
382
|
+
if (newHeight !== dimensions.current?.height || newWidth !== dimensions.current.width) {
|
|
383
|
+
dimensions.current = { height: newHeight, width: newWidth }
|
|
384
|
+
|
|
385
|
+
onContentSizeChange({
|
|
386
|
+
nativeEvent: {
|
|
387
|
+
contentSize: dimensions.current,
|
|
388
|
+
},
|
|
389
|
+
} as NativeSyntheticEvent<TextInputContentSizeChangeEventData>)
|
|
390
|
+
}
|
|
391
|
+
}, [multiline, onContentSizeChange])
|
|
392
|
+
|
|
393
|
+
const handleOnChangeText = useCallback(
|
|
394
|
+
(e: SyntheticEvent<HTMLDivElement>) => {
|
|
395
|
+
if (
|
|
396
|
+
!divRef.current ||
|
|
397
|
+
!(e.target instanceof HTMLElement) ||
|
|
398
|
+
!contentSelection.current
|
|
399
|
+
) {
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
const nativeEvent = e.nativeEvent as MarkdownNativeEvent
|
|
403
|
+
const inputType = nativeEvent.inputType
|
|
404
|
+
const isComposing = isEventComposing(nativeEvent)
|
|
405
|
+
|
|
406
|
+
updateTextColor(divRef.current, e.target.textContent ?? "")
|
|
407
|
+
const previousText = divRef.current.value
|
|
408
|
+
let parsedText = normalizeValue(
|
|
409
|
+
inputType === "pasteText"
|
|
410
|
+
? pasteContent.current || ""
|
|
411
|
+
: parseInnerHTMLToText(
|
|
412
|
+
e.target as MarkdownTextInputElement,
|
|
413
|
+
contentSelection.current.start,
|
|
414
|
+
inputType,
|
|
415
|
+
multiline,
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if (pasteContent.current) {
|
|
420
|
+
pasteContent.current = null
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (maxLength !== undefined && parsedText.length > maxLength) {
|
|
424
|
+
parsedText = previousText
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const prevSelection = contentSelection.current ?? { start: 0, end: 0 }
|
|
428
|
+
const newCursorPosition =
|
|
429
|
+
inputType === "deleteContentForward" &&
|
|
430
|
+
contentSelection.current.start === contentSelection.current.end
|
|
431
|
+
? Math.max(contentSelection.current.start, 0) // Don't move the caret when deleting forward with no characters selected
|
|
432
|
+
: Math.max(
|
|
433
|
+
Math.max(contentSelection.current.end, 0) +
|
|
434
|
+
(parsedText.length - previousText.length),
|
|
435
|
+
0,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if (isComposing) {
|
|
439
|
+
updateTextColor(divRef.current, parsedText)
|
|
440
|
+
updateSelection(e, {
|
|
441
|
+
start: newCursorPosition,
|
|
442
|
+
end: newCursorPosition,
|
|
443
|
+
})
|
|
444
|
+
divRef.current.value = parsedText
|
|
445
|
+
if (onChangeText) {
|
|
446
|
+
onChangeText(parsedText)
|
|
447
|
+
}
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
let newInputUpdate: ParseTextResult
|
|
451
|
+
switch (inputType) {
|
|
452
|
+
case "historyUndo":
|
|
453
|
+
newInputUpdate = undo(divRef.current)
|
|
454
|
+
break
|
|
455
|
+
case "historyRedo":
|
|
456
|
+
newInputUpdate = redo(divRef.current)
|
|
457
|
+
break
|
|
458
|
+
default:
|
|
459
|
+
newInputUpdate = parseText(
|
|
460
|
+
parser,
|
|
461
|
+
divRef.current,
|
|
462
|
+
parsedText,
|
|
463
|
+
processedMarkdownStyle,
|
|
464
|
+
newCursorPosition,
|
|
465
|
+
true,
|
|
466
|
+
!inputType,
|
|
467
|
+
inputType === "pasteText",
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
const { text, cursorPosition } = newInputUpdate
|
|
471
|
+
updateTextColor(divRef.current, text)
|
|
472
|
+
updateSelection(e, {
|
|
473
|
+
start: cursorPosition ?? 0,
|
|
474
|
+
end: cursorPosition ?? 0,
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
if (onChange) {
|
|
478
|
+
const event = e as unknown as NativeSyntheticEvent<{
|
|
479
|
+
count: number
|
|
480
|
+
before: number
|
|
481
|
+
start: number
|
|
482
|
+
}>
|
|
483
|
+
setEventProps(event)
|
|
484
|
+
|
|
485
|
+
// The new text is between the prev start selection and the new end selection, can be empty
|
|
486
|
+
const addedText = text.slice(prevSelection.start, cursorPosition ?? 0)
|
|
487
|
+
// The length of the text that replaced the before text
|
|
488
|
+
const count = addedText.length
|
|
489
|
+
// The start index of the replacement operation
|
|
490
|
+
let start = prevSelection.start
|
|
491
|
+
|
|
492
|
+
const prevSelectionRange = prevSelection.end - prevSelection.start
|
|
493
|
+
// The length the deleted text had before
|
|
494
|
+
let before = prevSelectionRange
|
|
495
|
+
if (
|
|
496
|
+
prevSelectionRange === 0 &&
|
|
497
|
+
(inputType === "deleteContentBackward" || inputType === "deleteContentForward")
|
|
498
|
+
) {
|
|
499
|
+
// its possible the user pressed a delete key without a selection range, so we need to adjust the before value to have the length of the deleted text
|
|
500
|
+
before = previousText.length - text.length
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (inputType === "deleteContentBackward") {
|
|
504
|
+
// When the user does a backspace delete he expects the content before the cursor to be removed.
|
|
505
|
+
// For this the start value needs to be adjusted (its as if the selection was before the text that we want to delete)
|
|
506
|
+
start = Math.max(start - before, 0)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
event.nativeEvent.count = count
|
|
510
|
+
event.nativeEvent.before = before
|
|
511
|
+
event.nativeEvent.start = start
|
|
512
|
+
|
|
513
|
+
// @ts-expect-error TODO: Remove once react native PR merged https://github.com/facebook/react-native/pull/45248
|
|
514
|
+
onChange(event)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (onChangeText) {
|
|
518
|
+
onChangeText(text)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
handleContentSizeChange()
|
|
522
|
+
},
|
|
523
|
+
[
|
|
524
|
+
updateTextColor,
|
|
525
|
+
multiline,
|
|
526
|
+
maxLength,
|
|
527
|
+
updateSelection,
|
|
528
|
+
onChange,
|
|
529
|
+
onChangeText,
|
|
530
|
+
handleContentSizeChange,
|
|
531
|
+
undo,
|
|
532
|
+
redo,
|
|
533
|
+
parseText,
|
|
534
|
+
parser,
|
|
535
|
+
processedMarkdownStyle,
|
|
536
|
+
setEventProps,
|
|
537
|
+
],
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
const insertText = useCallback(
|
|
541
|
+
(e: SyntheticEvent<HTMLDivElement, Event>, text: string) => {
|
|
542
|
+
if (!contentSelection.current || !divRef.current) {
|
|
543
|
+
return
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const previousText = divRef.current.value
|
|
547
|
+
let insertedText = text
|
|
548
|
+
let availableLength = text.length
|
|
549
|
+
const prefix = divRef.current.value.substring(0, contentSelection.current.start)
|
|
550
|
+
const suffix = divRef.current.value.substring(contentSelection.current.end)
|
|
551
|
+
if (maxLength !== undefined) {
|
|
552
|
+
availableLength = maxLength - prefix.length - suffix.length
|
|
553
|
+
insertedText = text.slice(0, Math.max(availableLength, 0))
|
|
554
|
+
}
|
|
555
|
+
const newText = `${prefix}${insertedText}${suffix}`
|
|
556
|
+
if (previousText === newText) {
|
|
557
|
+
document.execCommand("delete")
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
pasteContent.current = availableLength > 0 ? newText : previousText
|
|
561
|
+
;(e.nativeEvent as MarkdownNativeEvent).inputType = "pasteText"
|
|
562
|
+
|
|
563
|
+
handleOnChangeText(e)
|
|
564
|
+
},
|
|
565
|
+
[handleOnChangeText, maxLength],
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
const handleKeyPress = useCallback(
|
|
569
|
+
(e: KeyboardEvent<HTMLDivElement>) => {
|
|
570
|
+
if (!divRef.current) {
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const hostNode = e.target
|
|
575
|
+
e.stopPropagation()
|
|
576
|
+
|
|
577
|
+
if (e.key === "z" && (e.ctrlKey || e.metaKey)) {
|
|
578
|
+
e.preventDefault()
|
|
579
|
+
const nativeEvent = e.nativeEvent as unknown as MarkdownNativeEvent
|
|
580
|
+
if (e.shiftKey) {
|
|
581
|
+
nativeEvent.inputType = "historyRedo"
|
|
582
|
+
} else {
|
|
583
|
+
nativeEvent.inputType = "historyUndo"
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
handleOnChangeText(e)
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Support submitBehavior prop (React Native 0.73+), fallback to blurOnSubmit for backwards compatibility
|
|
591
|
+
let shouldBlurOnSubmit
|
|
592
|
+
let shouldSubmit
|
|
593
|
+
if (submitBehavior != null) {
|
|
594
|
+
// submitBehavior takes precedence over blurOnSubmit
|
|
595
|
+
shouldSubmit = submitBehavior === "submit" || submitBehavior === "blurAndSubmit"
|
|
596
|
+
shouldBlurOnSubmit = submitBehavior === "blurAndSubmit"
|
|
597
|
+
} else {
|
|
598
|
+
// Fallback to blurOnSubmit logic for backwards compatibility
|
|
599
|
+
const blurOnSubmitDefault = !multiline
|
|
600
|
+
shouldBlurOnSubmit = blurOnSubmit === null ? blurOnSubmitDefault : blurOnSubmit
|
|
601
|
+
shouldSubmit = blurOnSubmit || !multiline
|
|
602
|
+
}
|
|
603
|
+
const nativeEvent = e.nativeEvent
|
|
604
|
+
const isComposing = isEventComposing(nativeEvent)
|
|
605
|
+
|
|
606
|
+
const event = e as unknown as NativeSyntheticEvent<TextInputKeyPressEventData>
|
|
607
|
+
setEventProps(event)
|
|
608
|
+
if (onKeyPress) {
|
|
609
|
+
onKeyPress(event)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (
|
|
613
|
+
e.key === "Enter" &&
|
|
614
|
+
// Do not call submit if composition is occuring.
|
|
615
|
+
!isComposing &&
|
|
616
|
+
!e.isDefaultPrevented()
|
|
617
|
+
) {
|
|
618
|
+
// prevent "Enter" from inserting a newline or submitting a form
|
|
619
|
+
e.preventDefault()
|
|
620
|
+
// submitBehavior === 'newline' means don't submit, just insert newline (default behavior)
|
|
621
|
+
if (!e.shiftKey && shouldSubmit && onSubmitEditing) {
|
|
622
|
+
onSubmitEditing(
|
|
623
|
+
event as unknown as NativeSyntheticEvent<TextInputSubmitEditingEventData>,
|
|
624
|
+
)
|
|
625
|
+
} else if (multiline && (!shouldSubmit || e.shiftKey)) {
|
|
626
|
+
// We need to change normal behavior of "Enter" key to insert a line breaks, to prevent wrapping contentEditable text in <div> tags.
|
|
627
|
+
// Thanks to that in every situation we have proper amount of new lines in our parsed text. Without it pressing enter in empty lines will add 2 more new lines.
|
|
628
|
+
insertText(e, "\n")
|
|
629
|
+
}
|
|
630
|
+
if (!e.shiftKey && shouldBlurOnSubmit && hostNode !== null) {
|
|
631
|
+
setTimeout(() => divRef.current && divRef.current.blur(), 0)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
[
|
|
636
|
+
multiline,
|
|
637
|
+
blurOnSubmit,
|
|
638
|
+
submitBehavior,
|
|
639
|
+
setEventProps,
|
|
640
|
+
onKeyPress,
|
|
641
|
+
handleOnChangeText,
|
|
642
|
+
onSubmitEditing,
|
|
643
|
+
insertText,
|
|
644
|
+
],
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
const handleFocus: FocusEventHandler<HTMLDivElement> = useCallback(
|
|
648
|
+
(event) => {
|
|
649
|
+
hasJustBeenFocused.current = true
|
|
650
|
+
const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>
|
|
651
|
+
RNTextInput.State.focusTextInput?.(e.target)
|
|
652
|
+
const hostNode = e.target as unknown as HTMLDivElement
|
|
653
|
+
currentlyFocusedField.current = hostNode
|
|
654
|
+
setEventProps(e)
|
|
655
|
+
if (divRef.current) {
|
|
656
|
+
if (contentSelection.current) {
|
|
657
|
+
setCursorPosition(
|
|
658
|
+
divRef.current,
|
|
659
|
+
contentSelection.current.start,
|
|
660
|
+
contentSelection.current.end,
|
|
661
|
+
)
|
|
662
|
+
} else {
|
|
663
|
+
const valueLength = value ? value.length : (divRef.current.value || "").length
|
|
664
|
+
setCursorPosition(divRef.current, valueLength, null)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (divRef.current) {
|
|
669
|
+
divRef.current.scrollIntoView({
|
|
670
|
+
block: "nearest",
|
|
671
|
+
})
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (onFocus) {
|
|
675
|
+
setEventProps(e)
|
|
676
|
+
onFocus(e)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (hostNode !== null) {
|
|
680
|
+
if (clearTextOnFocus && divRef.current) {
|
|
681
|
+
divRef.current.textContent = ""
|
|
682
|
+
}
|
|
683
|
+
if (selectTextOnFocus) {
|
|
684
|
+
// Safari requires selection to occur in a setTimeout
|
|
685
|
+
if (focusTimeout !== null) {
|
|
686
|
+
clearTimeout(focusTimeout)
|
|
687
|
+
}
|
|
688
|
+
focusTimeout = setTimeout(() => {
|
|
689
|
+
if (hostNode === null) {
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
document.execCommand("selectAll", false, "")
|
|
693
|
+
}, 0)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
[clearTextOnFocus, onFocus, selectTextOnFocus, setEventProps, value],
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
const handleBlur: FocusEventHandler<HTMLDivElement> = useCallback(
|
|
701
|
+
(event) => {
|
|
702
|
+
const e = event as unknown as NativeSyntheticEvent<TextInputFocusEventData>
|
|
703
|
+
if (event.target !== document.activeElement) {
|
|
704
|
+
RNTextInput.State.blurTextInput?.(e.target)
|
|
705
|
+
}
|
|
706
|
+
removeSelection()
|
|
707
|
+
currentlyFocusedField.current = null
|
|
708
|
+
if (onBlur) {
|
|
709
|
+
setEventProps(e)
|
|
710
|
+
onBlur(e)
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
[onBlur, setEventProps],
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
const handleClick = useCallback(
|
|
717
|
+
(e: MouseEvent<HTMLDivElement, globalThis.MouseEvent>) => {
|
|
718
|
+
if (!onClick || !divRef.current) {
|
|
719
|
+
return
|
|
720
|
+
}
|
|
721
|
+
;(e.target as HTMLInputElement).value = divRef.current.value
|
|
722
|
+
onClick(e)
|
|
723
|
+
},
|
|
724
|
+
[onClick],
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
const handleCopy: ClipboardEventHandler<HTMLDivElement> = useCallback((e) => {
|
|
728
|
+
if (!divRef.current || !contentSelection.current) {
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
e.preventDefault()
|
|
732
|
+
const text = divRef.current?.value.substring(
|
|
733
|
+
contentSelection.current.start,
|
|
734
|
+
contentSelection.current.end,
|
|
735
|
+
)
|
|
736
|
+
e.clipboardData.setData("text/plain", text ?? "")
|
|
737
|
+
}, [])
|
|
738
|
+
|
|
739
|
+
const handleCut = useCallback(
|
|
740
|
+
(e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
741
|
+
if (!divRef.current || !contentSelection.current) {
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
handleCopy(e)
|
|
745
|
+
if (contentSelection.current.start !== contentSelection.current.end) {
|
|
746
|
+
document.execCommand("delete")
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
[handleCopy],
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
const handlePaste = useCallback(
|
|
753
|
+
(e: React.ClipboardEvent<HTMLDivElement>) => {
|
|
754
|
+
if (e.isDefaultPrevented() || !divRef.current || !contentSelection.current) {
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
e.preventDefault()
|
|
758
|
+
const clipboardData = e.clipboardData
|
|
759
|
+
const text =
|
|
760
|
+
clipboardData.getData("text/plain").trim() ||
|
|
761
|
+
clipboardData.getData("text/uri-list").trim()
|
|
762
|
+
insertText(e, text)
|
|
763
|
+
},
|
|
764
|
+
[insertText],
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
const endComposition = useCallback(
|
|
768
|
+
(e: React.CompositionEvent<HTMLDivElement>) => {
|
|
769
|
+
handleOnChangeText(e)
|
|
770
|
+
},
|
|
771
|
+
[handleOnChangeText],
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
const setRef = (currentRef: HTMLDivElement | null) => {
|
|
775
|
+
const r = currentRef
|
|
776
|
+
if (r) {
|
|
777
|
+
;(r as unknown as TextInput).isFocused = () => document.activeElement === r
|
|
778
|
+
;(r as unknown as TextInput).clear = () => {
|
|
779
|
+
r.textContent = ""
|
|
780
|
+
updateTextColor(r, "")
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (value === "" || value === undefined) {
|
|
784
|
+
// update to placeholder color when value is empty
|
|
785
|
+
updateTextColor(r, r.textContent ?? "")
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (ref) {
|
|
790
|
+
if (typeof ref === "object") {
|
|
791
|
+
// eslint-disable-next-line no-param-reassign
|
|
792
|
+
;(ref as MutableRefObject<HTMLDivElement | null>).current = r
|
|
793
|
+
} else if (typeof ref === "function") {
|
|
794
|
+
;(ref as (elementRef: HTMLDivElement | null) => void)(r)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
divRef.current = r as MarkdownTextInputElement
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const handleTouchStart = (event: TouchEvent<HTMLDivElement>) => {
|
|
801
|
+
if (!onTouchStart) {
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
const e = event as unknown as GestureResponderEvent
|
|
805
|
+
onTouchStart(e)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
useClientEffect(
|
|
809
|
+
function parseAndStyleValue() {
|
|
810
|
+
if (!divRef.current || value === divRef.current.value) {
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (value === undefined) {
|
|
815
|
+
parseText(parser, divRef.current, divRef.current.value, processedMarkdownStyle)
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
const normalizedValue = normalizeValue(value)
|
|
819
|
+
|
|
820
|
+
divRef.current.value = normalizedValue
|
|
821
|
+
parseText(
|
|
822
|
+
parser,
|
|
823
|
+
divRef.current,
|
|
824
|
+
normalizedValue,
|
|
825
|
+
processedMarkdownStyle,
|
|
826
|
+
null,
|
|
827
|
+
true,
|
|
828
|
+
false,
|
|
829
|
+
true,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
updateTextColor(divRef.current, value)
|
|
833
|
+
},
|
|
834
|
+
[parser, multiline, processedMarkdownStyle, value, maxLength],
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
useClientEffect(
|
|
838
|
+
function adjustHeight() {
|
|
839
|
+
if (!divRef.current || !multiline) {
|
|
840
|
+
return
|
|
841
|
+
}
|
|
842
|
+
const elementHeight = getElementHeight(divRef.current, inputStyles, numberOfLines)
|
|
843
|
+
divRef.current.style.height = elementHeight
|
|
844
|
+
divRef.current.style.maxHeight = elementHeight
|
|
845
|
+
},
|
|
846
|
+
[numberOfLines],
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
useEffect(() => {
|
|
850
|
+
if (!divRef.current) {
|
|
851
|
+
return
|
|
852
|
+
}
|
|
853
|
+
// focus the input on mount if autoFocus is set
|
|
854
|
+
if (autoFocus) {
|
|
855
|
+
divRef.current.focus()
|
|
856
|
+
}
|
|
857
|
+
divRef.current.uniqueId = idGenerator.next().value as string
|
|
858
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
859
|
+
}, [])
|
|
860
|
+
|
|
861
|
+
useEffect(() => {
|
|
862
|
+
// update content size when the input styles change
|
|
863
|
+
handleContentSizeChange()
|
|
864
|
+
}, [handleContentSizeChange, inputStyles])
|
|
865
|
+
|
|
866
|
+
useEffect(() => {
|
|
867
|
+
if (
|
|
868
|
+
!divRef.current ||
|
|
869
|
+
!selection ||
|
|
870
|
+
(contentSelection.current &&
|
|
871
|
+
selection.start === contentSelection.current.start &&
|
|
872
|
+
selection.end === contentSelection.current.end)
|
|
873
|
+
) {
|
|
874
|
+
return
|
|
875
|
+
}
|
|
876
|
+
const newSelection: Selection = {
|
|
877
|
+
start: selection.start,
|
|
878
|
+
end: selection.end ?? selection.start,
|
|
879
|
+
}
|
|
880
|
+
contentSelection.current = newSelection
|
|
881
|
+
updateRefSelectionVariables(newSelection)
|
|
882
|
+
setCursorPosition(divRef.current, newSelection.start, newSelection.end)
|
|
883
|
+
}, [selection, updateRefSelectionVariables])
|
|
884
|
+
|
|
885
|
+
return (
|
|
886
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
887
|
+
<div
|
|
888
|
+
id={id}
|
|
889
|
+
ref={setRef}
|
|
890
|
+
contentEditable={!disabled}
|
|
891
|
+
style={inputStyles}
|
|
892
|
+
role={accessibilityRole || "textbox"}
|
|
893
|
+
aria-label={accessibilityLabel}
|
|
894
|
+
aria-labelledby={`${accessibilityLabelledBy}`}
|
|
895
|
+
aria-placeholder={heightSafePlaceholder}
|
|
896
|
+
aria-multiline={multiline}
|
|
897
|
+
autoCorrect={autoCorrect ? "on" : "off"}
|
|
898
|
+
autoCapitalize={autoCapitalize}
|
|
899
|
+
className={className}
|
|
900
|
+
onKeyDown={handleKeyPress}
|
|
901
|
+
onCompositionEnd={endComposition}
|
|
902
|
+
onInput={handleOnChangeText}
|
|
903
|
+
onClick={handleClick}
|
|
904
|
+
onFocus={handleFocus}
|
|
905
|
+
onBlur={handleBlur}
|
|
906
|
+
onCopy={handleCopy}
|
|
907
|
+
onCut={handleCut}
|
|
908
|
+
onPaste={handlePaste}
|
|
909
|
+
// @ts-expect-error: we use placeholder prop to style it in CSS even though its not handled internally
|
|
910
|
+
placeholder={heightSafePlaceholder}
|
|
911
|
+
spellCheck={spellCheck}
|
|
912
|
+
dir={dir}
|
|
913
|
+
inputMode={inputMode}
|
|
914
|
+
onSelect={handleOnSelect}
|
|
915
|
+
onTouchStart={handleTouchStart}
|
|
916
|
+
/>
|
|
917
|
+
)
|
|
918
|
+
},
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
const styles = StyleSheet.create({
|
|
922
|
+
defaultInputStyles: {
|
|
923
|
+
borderColor: "black",
|
|
924
|
+
borderWidth: 1,
|
|
925
|
+
borderStyle: "solid",
|
|
926
|
+
fontFamily: "sans-serif",
|
|
927
|
+
boxSizing: "border-box",
|
|
928
|
+
// @ts-expect-error it works on web
|
|
929
|
+
overflowY: "auto",
|
|
930
|
+
overflowX: "auto",
|
|
931
|
+
overflowWrap: "break-word",
|
|
932
|
+
},
|
|
933
|
+
disabledInputStyles: {
|
|
934
|
+
opacity: 0.75,
|
|
935
|
+
cursor: "auto",
|
|
936
|
+
},
|
|
937
|
+
caretHidden: {
|
|
938
|
+
// @ts-expect-error it works on web
|
|
939
|
+
caretColor: "transparent",
|
|
940
|
+
},
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
export default MarkdownTextInput
|
|
944
|
+
|
|
945
|
+
export type {
|
|
946
|
+
MarkdownNativeEvent,
|
|
947
|
+
MarkdownTextInputProps,
|
|
948
|
+
MarkdownTextInputElement,
|
|
949
|
+
HTMLMarkdownElement,
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function getWorkletRuntime() {
|
|
953
|
+
throw new Error(
|
|
954
|
+
"[react-native-marcus] `getWorkletRuntime` is not available on web. Please make sure to use it only on native Android or iOS.",
|
|
955
|
+
)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export { getWorkletRuntime }
|