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.
Files changed (304) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +227 -0
  3. package/RNMarcus.podspec +78 -0
  4. package/android/build.gradle +220 -0
  5. package/android/proguard-rules.pro +1 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/AndroidManifestNew.xml +2 -0
  8. package/android/src/main/cpp/CMakeLists.txt +41 -0
  9. package/android/src/main/cpp/MarkdownParser.cpp +49 -0
  10. package/android/src/main/cpp/MarkdownParser.h +56 -0
  11. package/android/src/main/cpp/OnLoad.cpp +17 -0
  12. package/android/src/main/java/app/getbullet/marcus/MarcusModule.kt +28 -0
  13. package/android/src/main/java/app/getbullet/marcus/MarcusPackage.kt +31 -0
  14. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorView.kt +63 -0
  15. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManager.kt +32 -0
  16. package/android/src/main/java/app/getbullet/marcus/MarcusTextInputDecoratorViewManagerSpec.kt +21 -0
  17. package/android/src/main/java/app/getbullet/marcus/MarkdownFormatter.kt +333 -0
  18. package/android/src/main/java/app/getbullet/marcus/MarkdownParseResult.kt +12 -0
  19. package/android/src/main/java/app/getbullet/marcus/MarkdownParser.kt +89 -0
  20. package/android/src/main/java/app/getbullet/marcus/MarkdownRange.kt +11 -0
  21. package/android/src/main/java/app/getbullet/marcus/MarkdownStyle.kt +81 -0
  22. package/android/src/main/java/app/getbullet/marcus/MarkdownTextWatcher.kt +17 -0
  23. package/android/src/main/java/app/getbullet/marcus/MarkdownUtils.kt +84 -0
  24. package/android/src/main/java/app/getbullet/marcus/ReactLineHeight.java +29 -0
  25. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundColorSpan.kt +6 -0
  26. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBackgroundSpan.kt +105 -0
  27. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBlockIndentSpan.kt +67 -0
  28. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownBoldSpan.kt +6 -0
  29. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontFamilySpan.kt +28 -0
  30. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownFontSizeSpan.kt +7 -0
  31. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownForegroundColorSpan.kt +6 -0
  32. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownGapSpan.kt +57 -0
  33. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownItalicSpan.kt +6 -0
  34. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownLineHeightSpan.kt +21 -0
  35. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownSpan.kt +7 -0
  36. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownStrikethroughSpan.kt +5 -0
  37. package/android/src/main/java/app/getbullet/marcus/spans/MarkdownUnderlineSpan.kt +5 -0
  38. package/android/src/main/new_arch/CMakeLists.txt +70 -0
  39. package/android/src/main/new_arch/RNMarcusSpec.cpp +43 -0
  40. package/android/src/main/new_arch/RNMarcusSpec.h +36 -0
  41. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.cpp +218 -0
  42. package/android/src/main/new_arch/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorShadowNode.h +54 -0
  43. package/android/src/reactNativeVersionPatch/CustomMountingManager/81/app/getbullet/marcus/CustomFabricUIManager.java +156 -0
  44. package/android/src/reactNativeVersionPatch/CustomMountingManager/latest/app/getbullet/marcus/CustomFabricUIManager.java +130 -0
  45. package/android/src/test/java/app/getbullet/marcus/MarkdownFormatterTest.kt +205 -0
  46. package/android/src/test/java/app/getbullet/marcus/RenderModel.kt +78 -0
  47. package/android/src/test/resources/render-model.txt +583 -0
  48. package/apple/MarcusModule.h +8 -0
  49. package/apple/MarcusModule.mm +26 -0
  50. package/apple/MarcusRange.h +15 -0
  51. package/apple/MarcusRange.mm +15 -0
  52. package/apple/MarcusTextInputDecoratorComponentView.h +12 -0
  53. package/apple/MarcusTextInputDecoratorComponentView.mm +121 -0
  54. package/apple/MarcusTextInputDecoratorShadowNode.cpp +251 -0
  55. package/apple/MarcusTextInputDecoratorShadowNode.h +65 -0
  56. package/apple/MarcusTextInputDecoratorShadowNode.mm +204 -0
  57. package/apple/MarcusTextInputDecoratorViewManager.h +8 -0
  58. package/apple/MarcusTextInputDecoratorViewManager.mm +11 -0
  59. package/apple/MarkdownAdaptiveImageGlyph.swift +44 -0
  60. package/apple/MarkdownAttributes.h +31 -0
  61. package/apple/MarkdownAttributes.m +9 -0
  62. package/apple/MarkdownBackedTextInputDelegate.swift +117 -0
  63. package/apple/MarkdownDecorator.swift +316 -0
  64. package/apple/MarkdownFonts.swift +21 -0
  65. package/apple/MarkdownFormatter+React.swift +51 -0
  66. package/apple/MarkdownFormatter.swift +509 -0
  67. package/apple/MarkdownLog.h +10 -0
  68. package/apple/MarkdownLog.mm +6 -0
  69. package/apple/MarkdownParser.swift +200 -0
  70. package/apple/MarkdownSwiftInterop.h +23 -0
  71. package/apple/MarkdownTextBackground.swift +17 -0
  72. package/apple/MarkdownTextFieldObserver.swift +90 -0
  73. package/apple/MarkdownTextLayoutFragment.swift +205 -0
  74. package/apple/MarkdownTextLayoutManagerDelegate.swift +84 -0
  75. package/apple/MarkdownTextStorageDelegate.swift +32 -0
  76. package/apple/MarkdownTextViewObserver.swift +30 -0
  77. package/apple/RCTMarcusStyle+Codegen.h +18 -0
  78. package/apple/RCTMarcusStyle+Codegen.mm +64 -0
  79. package/apple/RCTMarcusStyle.h +41 -0
  80. package/apple/RCTMarcusStyle.m +5 -0
  81. package/apple/RCTMarcusUtils.h +26 -0
  82. package/apple/RCTMarcusUtils.mm +26 -0
  83. package/apple/RCTTextInput+AdaptiveImageGlyph.h +16 -0
  84. package/apple/RCTTextInput+AdaptiveImageGlyph.mm +79 -0
  85. package/apple/RCTTextInputComponentView+Marcus.h +11 -0
  86. package/apple/RCTTextInputComponentView+Marcus.mm +79 -0
  87. package/cpp/MarkdownCxx.modulemap +8 -0
  88. package/cpp/MarkdownGlobal.cpp +48 -0
  89. package/cpp/MarkdownGlobal.h +33 -0
  90. package/cpp/MarkdownWorkletParser.cpp +93 -0
  91. package/cpp/MarkdownWorkletParser.h +45 -0
  92. package/cpp/RuntimeDecorator.cpp +39 -0
  93. package/cpp/RuntimeDecorator.h +14 -0
  94. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorState.h +33 -0
  95. package/cpp/react/renderer/components/RNMarcusSpec/MarcusTextInputDecoratorViewComponentDescriptor.h +17 -0
  96. package/lib/commonjs/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
  97. package/lib/commonjs/MarkdownTextInput.js +99 -0
  98. package/lib/commonjs/MarkdownTextInput.js.map +1 -0
  99. package/lib/commonjs/MarkdownTextInput.web.js +618 -0
  100. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -0
  101. package/lib/commonjs/NativeMarcusModule.js +9 -0
  102. package/lib/commonjs/NativeMarcusModule.js.map +1 -0
  103. package/lib/commonjs/commonTypes.js +2 -0
  104. package/lib/commonjs/commonTypes.js.map +1 -0
  105. package/lib/commonjs/global.d.js +2 -0
  106. package/lib/commonjs/global.d.js.map +1 -0
  107. package/lib/commonjs/index.js +27 -0
  108. package/lib/commonjs/index.js.map +1 -0
  109. package/lib/commonjs/package.json +1 -0
  110. package/lib/commonjs/parser/index.js +7030 -0
  111. package/lib/commonjs/parser/index.js.map +7 -0
  112. package/lib/commonjs/rangeUtils.js +71 -0
  113. package/lib/commonjs/rangeUtils.js.map +1 -0
  114. package/lib/commonjs/react-native-web.d.js +2 -0
  115. package/lib/commonjs/react-native-web.d.js.map +1 -0
  116. package/lib/commonjs/styleUtils.js +91 -0
  117. package/lib/commonjs/styleUtils.js.map +1 -0
  118. package/lib/commonjs/web/InputHistory.js +111 -0
  119. package/lib/commonjs/web/InputHistory.js.map +1 -0
  120. package/lib/commonjs/web/MarkdownTextInput.css +72 -0
  121. package/lib/commonjs/web/utils/blockLayout.js +132 -0
  122. package/lib/commonjs/web/utils/blockLayout.js.map +1 -0
  123. package/lib/commonjs/web/utils/blockUtils.js +219 -0
  124. package/lib/commonjs/web/utils/blockUtils.js.map +1 -0
  125. package/lib/commonjs/web/utils/browserUtils.js +40 -0
  126. package/lib/commonjs/web/utils/browserUtils.js.map +1 -0
  127. package/lib/commonjs/web/utils/cursorUtils.js +157 -0
  128. package/lib/commonjs/web/utils/cursorUtils.js.map +1 -0
  129. package/lib/commonjs/web/utils/inputUtils.js +152 -0
  130. package/lib/commonjs/web/utils/inputUtils.js.map +1 -0
  131. package/lib/commonjs/web/utils/measureUtils.js +0 -0
  132. package/lib/commonjs/web/utils/measureUtils.js.map +1 -0
  133. package/lib/commonjs/web/utils/parserUtils.js +552 -0
  134. package/lib/commonjs/web/utils/parserUtils.js.map +1 -0
  135. package/lib/commonjs/web/utils/treeUtils.js +109 -0
  136. package/lib/commonjs/web/utils/treeUtils.js.map +1 -0
  137. package/lib/commonjs/web/utils/webStyleUtils.js +59 -0
  138. package/lib/commonjs/web/utils/webStyleUtils.js.map +1 -0
  139. package/lib/module/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
  140. package/lib/module/MarkdownTextInput.js +94 -0
  141. package/lib/module/MarkdownTextInput.js.map +1 -0
  142. package/lib/module/MarkdownTextInput.web.js +612 -0
  143. package/lib/module/MarkdownTextInput.web.js.map +1 -0
  144. package/lib/module/NativeMarcusModule.js +5 -0
  145. package/lib/module/NativeMarcusModule.js.map +1 -0
  146. package/lib/module/commonTypes.js +2 -0
  147. package/lib/module/commonTypes.js.map +1 -0
  148. package/lib/module/global.d.js +2 -0
  149. package/lib/module/global.d.js.map +1 -0
  150. package/lib/module/index.js +5 -0
  151. package/lib/module/index.js.map +1 -0
  152. package/lib/module/package.json +1 -0
  153. package/lib/module/parser/index.js +7029 -0
  154. package/lib/module/parser/index.js.map +7 -0
  155. package/lib/module/rangeUtils.js +66 -0
  156. package/lib/module/rangeUtils.js.map +1 -0
  157. package/lib/module/react-native-web.d.js +2 -0
  158. package/lib/module/react-native-web.d.js.map +1 -0
  159. package/lib/module/styleUtils.js +88 -0
  160. package/lib/module/styleUtils.js.map +1 -0
  161. package/lib/module/web/InputHistory.js +106 -0
  162. package/lib/module/web/InputHistory.js.map +1 -0
  163. package/lib/module/web/MarkdownTextInput.css +72 -0
  164. package/lib/module/web/utils/blockLayout.js +128 -0
  165. package/lib/module/web/utils/blockLayout.js.map +1 -0
  166. package/lib/module/web/utils/blockUtils.js +210 -0
  167. package/lib/module/web/utils/blockUtils.js.map +1 -0
  168. package/lib/module/web/utils/browserUtils.js +36 -0
  169. package/lib/module/web/utils/browserUtils.js.map +1 -0
  170. package/lib/module/web/utils/cursorUtils.js +150 -0
  171. package/lib/module/web/utils/cursorUtils.js.map +1 -0
  172. package/lib/module/web/utils/inputUtils.js +144 -0
  173. package/lib/module/web/utils/inputUtils.js.map +1 -0
  174. package/lib/module/web/utils/measureUtils.js +0 -0
  175. package/lib/module/web/utils/measureUtils.js.map +1 -0
  176. package/lib/module/web/utils/parserUtils.js +547 -0
  177. package/lib/module/web/utils/parserUtils.js.map +1 -0
  178. package/lib/module/web/utils/treeUtils.js +103 -0
  179. package/lib/module/web/utils/treeUtils.js.map +1 -0
  180. package/lib/module/web/utils/webStyleUtils.js +54 -0
  181. package/lib/module/web/utils/webStyleUtils.js.map +1 -0
  182. package/lib/typescript/commonjs/e2e/checks.test.d.ts +2 -0
  183. package/lib/typescript/commonjs/e2e/checks.test.d.ts.map +1 -0
  184. package/lib/typescript/commonjs/e2e/editing.test.d.ts +2 -0
  185. package/lib/typescript/commonjs/e2e/editing.test.d.ts.map +1 -0
  186. package/lib/typescript/commonjs/e2e/helpers.d.ts +23 -0
  187. package/lib/typescript/commonjs/e2e/helpers.d.ts.map +1 -0
  188. package/lib/typescript/commonjs/e2e/smoke.test.d.ts +2 -0
  189. package/lib/typescript/commonjs/e2e/smoke.test.d.ts.map +1 -0
  190. package/lib/typescript/commonjs/package.json +1 -0
  191. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
  192. package/lib/typescript/commonjs/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
  193. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts +22 -0
  194. package/lib/typescript/commonjs/src/MarkdownTextInput.d.ts.map +1 -0
  195. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts +41 -0
  196. package/lib/typescript/commonjs/src/MarkdownTextInput.web.d.ts.map +1 -0
  197. package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts +7 -0
  198. package/lib/typescript/commonjs/src/NativeMarcusModule.d.ts.map +1 -0
  199. package/lib/typescript/commonjs/src/commonTypes.d.ts +10 -0
  200. package/lib/typescript/commonjs/src/commonTypes.d.ts.map +1 -0
  201. package/lib/typescript/commonjs/src/index.d.ts +5 -0
  202. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  203. package/lib/typescript/commonjs/src/parser/index.d.ts +8 -0
  204. package/lib/typescript/commonjs/src/parser/index.d.ts.map +1 -0
  205. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts +8 -0
  206. package/lib/typescript/commonjs/src/parser/micromark-extension-mentions.d.ts.map +1 -0
  207. package/lib/typescript/commonjs/src/rangeUtils.d.ts +6 -0
  208. package/lib/typescript/commonjs/src/rangeUtils.d.ts.map +1 -0
  209. package/lib/typescript/commonjs/src/styleUtils.d.ts +8 -0
  210. package/lib/typescript/commonjs/src/styleUtils.d.ts.map +1 -0
  211. package/lib/typescript/commonjs/src/web/InputHistory.d.ts +24 -0
  212. package/lib/typescript/commonjs/src/web/InputHistory.d.ts.map +1 -0
  213. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts +41 -0
  214. package/lib/typescript/commonjs/src/web/utils/blockLayout.d.ts.map +1 -0
  215. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts +33 -0
  216. package/lib/typescript/commonjs/src/web/utils/blockUtils.d.ts.map +1 -0
  217. package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts +7 -0
  218. package/lib/typescript/commonjs/src/web/utils/browserUtils.d.ts.map +1 -0
  219. package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts +12 -0
  220. package/lib/typescript/commonjs/src/web/utils/cursorUtils.d.ts.map +1 -0
  221. package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts +12 -0
  222. package/lib/typescript/commonjs/src/web/utils/inputUtils.d.ts.map +1 -0
  223. package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts +22 -0
  224. package/lib/typescript/commonjs/src/web/utils/measureUtils.d.ts.map +1 -0
  225. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts +44 -0
  226. package/lib/typescript/commonjs/src/web/utils/parserUtils.d.ts.map +1 -0
  227. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts +25 -0
  228. package/lib/typescript/commonjs/src/web/utils/treeUtils.d.ts.map +1 -0
  229. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts +7 -0
  230. package/lib/typescript/commonjs/src/web/utils/webStyleUtils.d.ts.map +1 -0
  231. package/lib/typescript/module/e2e/checks.test.d.ts +2 -0
  232. package/lib/typescript/module/e2e/checks.test.d.ts.map +1 -0
  233. package/lib/typescript/module/e2e/editing.test.d.ts +2 -0
  234. package/lib/typescript/module/e2e/editing.test.d.ts.map +1 -0
  235. package/lib/typescript/module/e2e/helpers.d.ts +23 -0
  236. package/lib/typescript/module/e2e/helpers.d.ts.map +1 -0
  237. package/lib/typescript/module/e2e/smoke.test.d.ts +2 -0
  238. package/lib/typescript/module/e2e/smoke.test.d.ts.map +1 -0
  239. package/lib/typescript/module/package.json +1 -0
  240. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts +63 -0
  241. package/lib/typescript/module/src/MarcusTextInputDecoratorViewNativeComponent.d.ts.map +1 -0
  242. package/lib/typescript/module/src/MarkdownTextInput.d.ts +22 -0
  243. package/lib/typescript/module/src/MarkdownTextInput.d.ts.map +1 -0
  244. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts +41 -0
  245. package/lib/typescript/module/src/MarkdownTextInput.web.d.ts.map +1 -0
  246. package/lib/typescript/module/src/NativeMarcusModule.d.ts +7 -0
  247. package/lib/typescript/module/src/NativeMarcusModule.d.ts.map +1 -0
  248. package/lib/typescript/module/src/commonTypes.d.ts +10 -0
  249. package/lib/typescript/module/src/commonTypes.d.ts.map +1 -0
  250. package/lib/typescript/module/src/index.d.ts +5 -0
  251. package/lib/typescript/module/src/index.d.ts.map +1 -0
  252. package/lib/typescript/module/src/parser/index.d.ts +8 -0
  253. package/lib/typescript/module/src/parser/index.d.ts.map +1 -0
  254. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts +8 -0
  255. package/lib/typescript/module/src/parser/micromark-extension-mentions.d.ts.map +1 -0
  256. package/lib/typescript/module/src/rangeUtils.d.ts +6 -0
  257. package/lib/typescript/module/src/rangeUtils.d.ts.map +1 -0
  258. package/lib/typescript/module/src/styleUtils.d.ts +8 -0
  259. package/lib/typescript/module/src/styleUtils.d.ts.map +1 -0
  260. package/lib/typescript/module/src/web/InputHistory.d.ts +24 -0
  261. package/lib/typescript/module/src/web/InputHistory.d.ts.map +1 -0
  262. package/lib/typescript/module/src/web/utils/blockLayout.d.ts +41 -0
  263. package/lib/typescript/module/src/web/utils/blockLayout.d.ts.map +1 -0
  264. package/lib/typescript/module/src/web/utils/blockUtils.d.ts +33 -0
  265. package/lib/typescript/module/src/web/utils/blockUtils.d.ts.map +1 -0
  266. package/lib/typescript/module/src/web/utils/browserUtils.d.ts +7 -0
  267. package/lib/typescript/module/src/web/utils/browserUtils.d.ts.map +1 -0
  268. package/lib/typescript/module/src/web/utils/cursorUtils.d.ts +12 -0
  269. package/lib/typescript/module/src/web/utils/cursorUtils.d.ts.map +1 -0
  270. package/lib/typescript/module/src/web/utils/inputUtils.d.ts +12 -0
  271. package/lib/typescript/module/src/web/utils/inputUtils.d.ts.map +1 -0
  272. package/lib/typescript/module/src/web/utils/measureUtils.d.ts +22 -0
  273. package/lib/typescript/module/src/web/utils/measureUtils.d.ts.map +1 -0
  274. package/lib/typescript/module/src/web/utils/parserUtils.d.ts +44 -0
  275. package/lib/typescript/module/src/web/utils/parserUtils.d.ts.map +1 -0
  276. package/lib/typescript/module/src/web/utils/treeUtils.d.ts +25 -0
  277. package/lib/typescript/module/src/web/utils/treeUtils.d.ts.map +1 -0
  278. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts +7 -0
  279. package/lib/typescript/module/src/web/utils/webStyleUtils.d.ts.map +1 -0
  280. package/package.json +167 -0
  281. package/react-native.config.js +10 -0
  282. package/src/MarcusTextInputDecoratorViewNativeComponent.ts +77 -0
  283. package/src/MarkdownTextInput.tsx +139 -0
  284. package/src/MarkdownTextInput.web.tsx +958 -0
  285. package/src/NativeMarcusModule.ts +8 -0
  286. package/src/commonTypes.ts +30 -0
  287. package/src/global.d.ts +1 -0
  288. package/src/index.tsx +4 -0
  289. package/src/parser/index.ts +556 -0
  290. package/src/parser/micromark-extension-mentions.ts +82 -0
  291. package/src/rangeUtils.ts +68 -0
  292. package/src/react-native-web.d.ts +1 -0
  293. package/src/styleUtils.ts +100 -0
  294. package/src/web/InputHistory.ts +137 -0
  295. package/src/web/MarkdownTextInput.css +72 -0
  296. package/src/web/utils/blockLayout.ts +163 -0
  297. package/src/web/utils/blockUtils.ts +245 -0
  298. package/src/web/utils/browserUtils.ts +41 -0
  299. package/src/web/utils/cursorUtils.ts +204 -0
  300. package/src/web/utils/inputUtils.ts +206 -0
  301. package/src/web/utils/measureUtils.ts +0 -0
  302. package/src/web/utils/parserUtils.ts +813 -0
  303. package/src/web/utils/treeUtils.ts +149 -0
  304. 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 }