@fendent/react-native-enriched 0.5.2-fork.1
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 +20 -0
- package/README.md +343 -0
- package/ReactNativeEnriched.podspec +31 -0
- package/android/build.gradle +106 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +197 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +72 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ComponentDescriptors.cpp +22 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ComponentDescriptors.h +24 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.cpp +434 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/EventEmitters.h +391 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.cpp +173 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/Props.h +833 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ShadowNodes.cpp +17 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/ShadowNodes.h +23 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/ReactNativeEnrichedSpec/States.h +20 -0
- package/android/gradle.properties +5 -0
- package/android/lint.gradle +70 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/swmansion/enriched/ReactNativeEnrichedPackage.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/AsyncDrawable.kt +126 -0
- package/android/src/main/java/com/swmansion/enriched/common/CheckboxDrawable.kt +81 -0
- package/android/src/main/java/com/swmansion/enriched/common/EnrichedConstants.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/common/EnrichedStyle.kt +57 -0
- package/android/src/main/java/com/swmansion/enriched/common/ForceRedrawSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/common/GumboNormalizer.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/common/MentionStyle.kt +7 -0
- package/android/src/main/java/com/swmansion/enriched/common/ResourceManager.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java +956 -0
- package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt +79 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedBlockQuoteSpan.kt +53 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedBoldSpan.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCheckboxListSpan.kt +92 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCodeBlockSpan.kt +81 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH1Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH2Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH3Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH4Span.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH5Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedH6Span.kt +20 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedImageSpan.kt +184 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedInlineCodeSpan.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedItalicSpan.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedLinkSpan.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedMentionSpan.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedOrderedListSpan.kt +79 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedStrikeThroughSpan.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnderlineSpan.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnorderedListSpan.kt +62 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedBlockSpan.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedHeadingSpan.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedInlineSpan.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedParagraphSpan.kt +5 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedSpan.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/common/spans/interfaces/EnrichedZeroWidthSpaceSpan.kt +4 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputConnectionWrapper.kt +140 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt +83 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +1120 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewLayoutManager.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt +478 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt +225 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/MentionHandler.kt +55 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeHtmlEvent.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeSelectionEvent.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeStateEvent.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnChangeTextEvent.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnContextMenuItemPressEvent.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputBlurEvent.kt +25 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputFocusEvent.kt +25 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnInputKeyPressEvent.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnLinkDetectedEvent.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionDetectedEvent.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionEvent.kt +34 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnPasteImagesEvent.kt +47 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnRequestHtmlResultEvent.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnSubmitEditingEvent.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputBlockQuoteSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputBoldSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputCheckboxListSpan.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputCodeBlockSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH1Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH2Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH3Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH4Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH5Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputH6Span.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputImageSpan.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputInlineCodeSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputItalicSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputLinkSpan.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputMentionSpan.kt +18 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputOrderedListSpan.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputStrikeThroughSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputUnderlineSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedInputUnorderedListSpan.kt +14 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedLineHeightSpan.kt +44 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/EnrichedSpans.kt +241 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/spans/interfaces/EnrichedInputSpan.kt +10 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/HtmlStyle.kt +372 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +164 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt +263 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt +434 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +394 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedEditableFactory.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt +320 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpanState.kt +310 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt +106 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannableStringBuilder.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/RichContentReceiver.kt +127 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt +106 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedSpanWatcher.kt +107 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/watchers/EnrichedTextWatcher.kt +74 -0
- package/android/src/main/new_arch/CMakeLists.txt +62 -0
- package/android/src/main/new_arch/GumboNormalizerJni.cpp +14 -0
- package/android/src/main/new_arch/ReactNativeEnrichedSpec.cpp +11 -0
- package/android/src/main/new_arch/ReactNativeEnrichedSpec.h +15 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputComponentDescriptor.h +35 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputMeasurementManager.cpp +53 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputMeasurementManager.h +25 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputShadowNode.cpp +35 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputShadowNode.h +53 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputState.cpp +9 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/EnrichedTextInputState.h +24 -0
- package/android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h +27 -0
- package/android/src/main/res/drawable/broken_image.xml +10 -0
- package/cpp/CMakeLists.txt +50 -0
- package/cpp/GumboParser/GumboParser.h +34043 -0
- package/cpp/README.md +59 -0
- package/cpp/parser/GumboNormalizer.c +915 -0
- package/cpp/parser/GumboParser.cpp +16 -0
- package/cpp/parser/GumboParser.hpp +23 -0
- package/cpp/tests/GumboParserTest.cpp +457 -0
- package/ios/EnrichedTextInputView.h +53 -0
- package/ios/EnrichedTextInputView.mm +2360 -0
- package/ios/EnrichedTextInputViewManager.mm +13 -0
- package/ios/attributesManager/AttributesManager.h +17 -0
- package/ios/attributesManager/AttributesManager.mm +195 -0
- package/ios/config/InputConfig.h +104 -0
- package/ios/config/InputConfig.mm +664 -0
- package/ios/extensions/ColorExtension.h +7 -0
- package/ios/extensions/ColorExtension.mm +38 -0
- package/ios/extensions/FontExtension.h +11 -0
- package/ios/extensions/FontExtension.mm +72 -0
- package/ios/extensions/ImageExtension.h +34 -0
- package/ios/extensions/ImageExtension.mm +165 -0
- package/ios/extensions/LayoutManagerExtension.h +6 -0
- package/ios/extensions/LayoutManagerExtension.mm +443 -0
- package/ios/extensions/StringExtension.h +15 -0
- package/ios/extensions/StringExtension.mm +69 -0
- package/ios/generated/ReactNativeEnrichedSpec/ComponentDescriptors.cpp +22 -0
- package/ios/generated/ReactNativeEnrichedSpec/ComponentDescriptors.h +24 -0
- package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.cpp +434 -0
- package/ios/generated/ReactNativeEnrichedSpec/EventEmitters.h +391 -0
- package/ios/generated/ReactNativeEnrichedSpec/Props.cpp +173 -0
- package/ios/generated/ReactNativeEnrichedSpec/Props.h +833 -0
- package/ios/generated/ReactNativeEnrichedSpec/RCTComponentViewHelpers.h +582 -0
- package/ios/generated/ReactNativeEnrichedSpec/ShadowNodes.cpp +17 -0
- package/ios/generated/ReactNativeEnrichedSpec/ShadowNodes.h +23 -0
- package/ios/generated/ReactNativeEnrichedSpec/States.cpp +16 -0
- package/ios/generated/ReactNativeEnrichedSpec/States.h +20 -0
- package/ios/inputParser/InputParser.h +11 -0
- package/ios/inputParser/InputParser.mm +1463 -0
- package/ios/inputTextView/InputTextView.h +6 -0
- package/ios/inputTextView/InputTextView.mm +285 -0
- package/ios/interfaces/AttributeEntry.h +9 -0
- package/ios/interfaces/AttributeEntry.mm +4 -0
- package/ios/interfaces/BaseStyleProtocol.h +17 -0
- package/ios/interfaces/ImageAttachment.h +11 -0
- package/ios/interfaces/ImageAttachment.mm +107 -0
- package/ios/interfaces/ImageData.h +10 -0
- package/ios/interfaces/ImageData.mm +4 -0
- package/ios/interfaces/LinkData.h +11 -0
- package/ios/interfaces/LinkData.mm +29 -0
- package/ios/interfaces/LinkRegexConfig.h +19 -0
- package/ios/interfaces/LinkRegexConfig.mm +37 -0
- package/ios/interfaces/MediaAttachment.h +23 -0
- package/ios/interfaces/MediaAttachment.mm +31 -0
- package/ios/interfaces/MentionParams.h +8 -0
- package/ios/interfaces/MentionParams.mm +4 -0
- package/ios/interfaces/MentionStyleProps.h +13 -0
- package/ios/interfaces/MentionStyleProps.mm +63 -0
- package/ios/interfaces/StyleBase.h +36 -0
- package/ios/interfaces/StyleBase.mm +256 -0
- package/ios/interfaces/StyleHeaders.h +102 -0
- package/ios/interfaces/StylePair.h +9 -0
- package/ios/interfaces/StylePair.mm +4 -0
- package/ios/interfaces/StyleTypeEnum.h +26 -0
- package/ios/interfaces/TextDecorationLineEnum.h +6 -0
- package/ios/interfaces/TextDecorationLineEnum.mm +4 -0
- package/ios/internals/EnrichedTextInputViewComponentDescriptor.h +19 -0
- package/ios/internals/EnrichedTextInputViewShadowNode.h +44 -0
- package/ios/internals/EnrichedTextInputViewShadowNode.mm +103 -0
- package/ios/internals/EnrichedTextInputViewState.cpp +10 -0
- package/ios/internals/EnrichedTextInputViewState.h +22 -0
- package/ios/styles/BlockQuoteStyle.mm +55 -0
- package/ios/styles/BoldStyle.mm +37 -0
- package/ios/styles/CheckboxListStyle.mm +153 -0
- package/ios/styles/CodeBlockStyle.mm +49 -0
- package/ios/styles/H1Style.mm +20 -0
- package/ios/styles/H2Style.mm +20 -0
- package/ios/styles/H3Style.mm +20 -0
- package/ios/styles/H4Style.mm +20 -0
- package/ios/styles/H5Style.mm +20 -0
- package/ios/styles/H6Style.mm +20 -0
- package/ios/styles/HeadingStyleBase.mm +65 -0
- package/ios/styles/ImageStyle.mm +146 -0
- package/ios/styles/InlineCodeStyle.mm +65 -0
- package/ios/styles/ItalicStyle.mm +37 -0
- package/ios/styles/LinkStyle.mm +532 -0
- package/ios/styles/MentionStyle.mm +538 -0
- package/ios/styles/OrderedListStyle.mm +86 -0
- package/ios/styles/StrikethroughStyle.mm +25 -0
- package/ios/styles/UnderlineStyle.mm +24 -0
- package/ios/styles/UnorderedListStyle.mm +86 -0
- package/ios/utils/CheckboxHitTestUtils.h +10 -0
- package/ios/utils/CheckboxHitTestUtils.mm +122 -0
- package/ios/utils/DotReplacementUtils.h +10 -0
- package/ios/utils/DotReplacementUtils.mm +68 -0
- package/ios/utils/KeyboardUtils.h +7 -0
- package/ios/utils/KeyboardUtils.mm +31 -0
- package/ios/utils/OccurenceUtils.h +44 -0
- package/ios/utils/OccurenceUtils.mm +179 -0
- package/ios/utils/ParagraphAttributesUtils.h +15 -0
- package/ios/utils/ParagraphAttributesUtils.mm +257 -0
- package/ios/utils/RangeUtils.h +12 -0
- package/ios/utils/RangeUtils.mm +183 -0
- package/ios/utils/TextBlockTapGestureRecognizer.h +17 -0
- package/ios/utils/TextBlockTapGestureRecognizer.mm +56 -0
- package/ios/utils/TextInsertionUtils.h +17 -0
- package/ios/utils/TextInsertionUtils.mm +64 -0
- package/ios/utils/WordsUtils.h +7 -0
- package/ios/utils/WordsUtils.mm +98 -0
- package/ios/utils/ZeroWidthSpaceUtils.h +9 -0
- package/ios/utils/ZeroWidthSpaceUtils.mm +270 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/EnrichedTextInput.js +304 -0
- package/lib/module/native/EnrichedTextInput.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/spec/EnrichedTextInputNativeComponent.ts +517 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/EnrichedTextInputDefaultProps.js +12 -0
- package/lib/module/utils/EnrichedTextInputDefaultProps.js.map +1 -0
- package/lib/module/utils/normalizeHtmlStyle.js +155 -0
- package/lib/module/utils/normalizeHtmlStyle.js.map +1 -0
- package/lib/module/utils/nullthrows.js +9 -0
- package/lib/module/utils/nullthrows.js.map +1 -0
- package/lib/module/utils/regexParser.js +46 -0
- package/lib/module/utils/regexParser.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/native/EnrichedTextInput.d.ts +3 -0
- package/lib/typescript/src/native/EnrichedTextInput.d.ts.map +1 -0
- package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts +397 -0
- package/lib/typescript/src/spec/EnrichedTextInputNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +447 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/EnrichedTextInputDefaultProps.d.ts +10 -0
- package/lib/typescript/src/utils/EnrichedTextInputDefaultProps.d.ts.map +1 -0
- package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts +4 -0
- package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts.map +1 -0
- package/lib/typescript/src/utils/nullthrows.d.ts +2 -0
- package/lib/typescript/src/utils/nullthrows.d.ts.map +1 -0
- package/lib/typescript/src/utils/regexParser.d.ts +3 -0
- package/lib/typescript/src/utils/regexParser.d.ts.map +1 -0
- package/package.json +226 -0
- package/react-native.config.js +13 -0
- package/src/index.tsx +20 -0
- package/src/native/EnrichedTextInput.tsx +370 -0
- package/src/spec/EnrichedTextInputNativeComponent.ts +517 -0
- package/src/types.ts +499 -0
- package/src/utils/EnrichedTextInputDefaultProps.ts +9 -0
- package/src/utils/normalizeHtmlStyle.ts +199 -0
- package/src/utils/nullthrows.ts +7 -0
- package/src/utils/regexParser.ts +56 -0
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
package com.swmansion.enriched.textinput
|
|
2
|
+
|
|
3
|
+
import android.content.ClipData
|
|
4
|
+
import android.content.ClipboardManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.graphics.BlendMode
|
|
7
|
+
import android.graphics.BlendModeColorFilter
|
|
8
|
+
import android.graphics.Color
|
|
9
|
+
import android.graphics.Rect
|
|
10
|
+
import android.graphics.text.LineBreaker
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.text.Editable
|
|
13
|
+
import android.text.InputType
|
|
14
|
+
import android.text.Spannable
|
|
15
|
+
import android.text.SpannableString
|
|
16
|
+
import android.util.AttributeSet
|
|
17
|
+
import android.util.Log
|
|
18
|
+
import android.util.Patterns
|
|
19
|
+
import android.util.TypedValue
|
|
20
|
+
import android.view.ActionMode
|
|
21
|
+
import android.view.Gravity
|
|
22
|
+
import android.view.KeyEvent
|
|
23
|
+
import android.view.Menu
|
|
24
|
+
import android.view.MenuItem
|
|
25
|
+
import android.view.MotionEvent
|
|
26
|
+
import android.view.inputmethod.EditorInfo
|
|
27
|
+
import android.view.inputmethod.InputConnection
|
|
28
|
+
import android.view.inputmethod.InputMethodManager
|
|
29
|
+
import android.widget.TextView
|
|
30
|
+
import androidx.appcompat.widget.AppCompatEditText
|
|
31
|
+
import androidx.core.view.ViewCompat
|
|
32
|
+
import com.facebook.react.bridge.ReactContext
|
|
33
|
+
import com.facebook.react.bridge.ReadableArray
|
|
34
|
+
import com.facebook.react.bridge.ReadableMap
|
|
35
|
+
import com.facebook.react.common.ReactConstants
|
|
36
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
37
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
38
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
39
|
+
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
|
|
40
|
+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
|
|
41
|
+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
|
|
42
|
+
import com.swmansion.enriched.common.EnrichedConstants
|
|
43
|
+
import com.swmansion.enriched.common.GumboNormalizer
|
|
44
|
+
import com.swmansion.enriched.common.parser.EnrichedParser
|
|
45
|
+
import com.swmansion.enriched.textinput.events.MentionHandler
|
|
46
|
+
import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
|
|
47
|
+
import com.swmansion.enriched.textinput.events.OnInputBlurEvent
|
|
48
|
+
import com.swmansion.enriched.textinput.events.OnInputFocusEvent
|
|
49
|
+
import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
|
|
50
|
+
import com.swmansion.enriched.textinput.events.OnSubmitEditingEvent
|
|
51
|
+
import com.swmansion.enriched.textinput.spans.EnrichedInputH1Span
|
|
52
|
+
import com.swmansion.enriched.textinput.spans.EnrichedInputH2Span
|
|
53
|
+
import com.swmansion.enriched.textinput.spans.EnrichedInputH3Span
|
|
54
|
+
import com.swmansion.enriched.textinput.spans.EnrichedInputH4Span
|
|
55
|
+
import com.swmansion.enriched.textinput.spans.EnrichedInputH5Span
|
|
56
|
+
import com.swmansion.enriched.textinput.spans.EnrichedInputH6Span
|
|
57
|
+
import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan
|
|
58
|
+
import com.swmansion.enriched.textinput.spans.EnrichedInputLinkSpan
|
|
59
|
+
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
|
|
60
|
+
import com.swmansion.enriched.textinput.spans.EnrichedSpans
|
|
61
|
+
import com.swmansion.enriched.textinput.spans.interfaces.EnrichedInputSpan
|
|
62
|
+
import com.swmansion.enriched.textinput.styles.HtmlStyle
|
|
63
|
+
import com.swmansion.enriched.textinput.styles.InlineStyles
|
|
64
|
+
import com.swmansion.enriched.textinput.styles.ListStyles
|
|
65
|
+
import com.swmansion.enriched.textinput.styles.ParagraphStyles
|
|
66
|
+
import com.swmansion.enriched.textinput.styles.ParametrizedStyles
|
|
67
|
+
import com.swmansion.enriched.textinput.utils.EnrichedEditableFactory
|
|
68
|
+
import com.swmansion.enriched.textinput.utils.EnrichedSelection
|
|
69
|
+
import com.swmansion.enriched.textinput.utils.EnrichedSpanState
|
|
70
|
+
import com.swmansion.enriched.textinput.utils.RichContentReceiver
|
|
71
|
+
import com.swmansion.enriched.textinput.utils.mergeSpannables
|
|
72
|
+
import com.swmansion.enriched.textinput.utils.setCheckboxClickListener
|
|
73
|
+
import com.swmansion.enriched.textinput.utils.zwsCountBefore
|
|
74
|
+
import com.swmansion.enriched.textinput.watchers.EnrichedSpanWatcher
|
|
75
|
+
import com.swmansion.enriched.textinput.watchers.EnrichedTextWatcher
|
|
76
|
+
import java.util.regex.Pattern
|
|
77
|
+
import java.util.regex.PatternSyntaxException
|
|
78
|
+
import kotlin.math.ceil
|
|
79
|
+
|
|
80
|
+
class EnrichedTextInputView :
|
|
81
|
+
AppCompatEditText,
|
|
82
|
+
TextView.OnEditorActionListener {
|
|
83
|
+
var stateWrapper: StateWrapper? = null
|
|
84
|
+
val selection: EnrichedSelection? = EnrichedSelection(this)
|
|
85
|
+
val spanState: EnrichedSpanState? = EnrichedSpanState(this)
|
|
86
|
+
val inlineStyles: InlineStyles? = InlineStyles(this)
|
|
87
|
+
val paragraphStyles: ParagraphStyles? = ParagraphStyles(this)
|
|
88
|
+
val listStyles: ListStyles? = ListStyles(this)
|
|
89
|
+
val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
|
|
90
|
+
var isDuringTransaction: Boolean = false
|
|
91
|
+
var isRemovingMany: Boolean = false
|
|
92
|
+
var scrollEnabled: Boolean = true
|
|
93
|
+
|
|
94
|
+
val mentionHandler: MentionHandler? = MentionHandler(this)
|
|
95
|
+
var htmlStyle: HtmlStyle = HtmlStyle(this, null)
|
|
96
|
+
set(value) {
|
|
97
|
+
if (field != value) {
|
|
98
|
+
val prev = field
|
|
99
|
+
field = value
|
|
100
|
+
reApplyHtmlStyleForSpans(prev, value)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var linkRegex: Pattern? = Patterns.WEB_URL
|
|
105
|
+
var spanWatcher: EnrichedSpanWatcher? = null
|
|
106
|
+
var layoutManager: EnrichedTextInputViewLayoutManager = EnrichedTextInputViewLayoutManager(this)
|
|
107
|
+
|
|
108
|
+
var shouldEmitHtml: Boolean = false
|
|
109
|
+
var shouldEmitOnChangeText: Boolean = false
|
|
110
|
+
var experimentalSynchronousEvents: Boolean = false
|
|
111
|
+
var useHtmlNormalizer: Boolean = false
|
|
112
|
+
|
|
113
|
+
var fontSize: Float? = null
|
|
114
|
+
private var lineHeight: Float? = null
|
|
115
|
+
var submitBehavior: String? = null
|
|
116
|
+
private var autoFocus = false
|
|
117
|
+
private var typefaceDirty = false
|
|
118
|
+
private var didAttachToWindow = false
|
|
119
|
+
private var detectScrollMovement = false
|
|
120
|
+
private var fontFamily: String? = null
|
|
121
|
+
private var fontStyle: Int = ReactConstants.UNSET
|
|
122
|
+
private var fontWeight: Int = ReactConstants.UNSET
|
|
123
|
+
private var defaultValue: CharSequence? = null
|
|
124
|
+
private var defaultValueDirty: Boolean = false
|
|
125
|
+
|
|
126
|
+
private var inputMethodManager: InputMethodManager? = null
|
|
127
|
+
private val spannableFactory = EnrichedTextInputSpannableFactory()
|
|
128
|
+
private var contextMenuItems: List<Pair<Int, String>> = emptyList()
|
|
129
|
+
|
|
130
|
+
constructor(context: Context) : super(context) {
|
|
131
|
+
prepareComponent()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
|
135
|
+
prepareComponent()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
|
|
139
|
+
context,
|
|
140
|
+
attrs,
|
|
141
|
+
defStyleAttr,
|
|
142
|
+
) {
|
|
143
|
+
prepareComponent()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
|
147
|
+
var inputConnection = super.onCreateInputConnection(outAttrs)
|
|
148
|
+
|
|
149
|
+
if (shouldSubmitOnReturn()) {
|
|
150
|
+
// Remove the "No Enter Action" flag if it exists
|
|
151
|
+
outAttrs.imeOptions = outAttrs.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION.inv()
|
|
152
|
+
|
|
153
|
+
// Force the key to be "Done" (or whatever label you set) instead of "Return"
|
|
154
|
+
// This ensures onEditorAction gets called instead of just inserting \n
|
|
155
|
+
if (outAttrs.imeOptions and EditorInfo.IME_MASK_ACTION == EditorInfo.IME_ACTION_UNSPECIFIED) {
|
|
156
|
+
outAttrs.imeOptions = outAttrs.imeOptions or EditorInfo.IME_ACTION_DONE
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (inputConnection != null) {
|
|
161
|
+
inputConnection =
|
|
162
|
+
EnrichedTextInputConnectionWrapper(
|
|
163
|
+
inputConnection,
|
|
164
|
+
context as ReactContext,
|
|
165
|
+
this,
|
|
166
|
+
experimentalSynchronousEvents,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return inputConnection
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
init {
|
|
174
|
+
inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
175
|
+
ViewCompat.setOnReceiveContentListener(
|
|
176
|
+
this,
|
|
177
|
+
RichContentReceiver.MIME_TYPES,
|
|
178
|
+
RichContentReceiver(this, context as ReactContext),
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private fun prepareComponent() {
|
|
183
|
+
isSingleLine = false
|
|
184
|
+
isHorizontalScrollBarEnabled = false
|
|
185
|
+
isVerticalScrollBarEnabled = true
|
|
186
|
+
gravity = Gravity.TOP or Gravity.START
|
|
187
|
+
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
188
|
+
|
|
189
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
190
|
+
breakStrategy = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
setPadding(0, 0, 0, 0)
|
|
194
|
+
setBackgroundColor(Color.TRANSPARENT)
|
|
195
|
+
|
|
196
|
+
// Ensure that every time new editable is created, it has EnrichedSpanWatcher attached
|
|
197
|
+
val spanWatcher = EnrichedSpanWatcher(this)
|
|
198
|
+
this.spanWatcher = spanWatcher
|
|
199
|
+
|
|
200
|
+
setEditableFactory(EnrichedEditableFactory(spanWatcher))
|
|
201
|
+
addTextChangedListener(EnrichedTextWatcher(this))
|
|
202
|
+
|
|
203
|
+
// Handle checkbox list item clicks
|
|
204
|
+
this.setCheckboxClickListener()
|
|
205
|
+
|
|
206
|
+
setOnEditorActionListener(this)
|
|
207
|
+
setReturnKeyLabel(DEFAULT_IME_ACTION_LABEL)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Similar implementation to: https://github.com/facebook/react-native/blob/c1f5445f4a59d0035389725e47da58eb3d2c267c/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt#L940
|
|
211
|
+
override fun onEditorAction(
|
|
212
|
+
v: TextView?,
|
|
213
|
+
actionId: Int,
|
|
214
|
+
event: KeyEvent?,
|
|
215
|
+
): Boolean {
|
|
216
|
+
// Check if it's a valid keyboard action (Done, Next, etc.) or the Enter key (IME_NULL)
|
|
217
|
+
val isAction = (actionId and EditorInfo.IME_MASK_ACTION) != 0 || actionId == EditorInfo.IME_NULL
|
|
218
|
+
|
|
219
|
+
if (isAction) {
|
|
220
|
+
val shouldSubmit = shouldSubmitOnReturn()
|
|
221
|
+
val shouldBlur = shouldBlurOnReturn()
|
|
222
|
+
|
|
223
|
+
if (shouldSubmit) {
|
|
224
|
+
emitSubmitEditing()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (shouldBlur) {
|
|
228
|
+
clearFocus()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (shouldSubmit || shouldBlur) {
|
|
232
|
+
return true
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Return false to let the system handle default behavior (like inserting \n)
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private fun emitSubmitEditing() {
|
|
241
|
+
val context = context as ReactContext
|
|
242
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
243
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id)
|
|
244
|
+
dispatcher?.dispatchEvent(
|
|
245
|
+
OnSubmitEditingEvent(
|
|
246
|
+
surfaceId,
|
|
247
|
+
id,
|
|
248
|
+
text,
|
|
249
|
+
experimentalSynchronousEvents,
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L295C1-L296C1
|
|
255
|
+
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
|
256
|
+
when (ev.action) {
|
|
257
|
+
MotionEvent.ACTION_DOWN -> {
|
|
258
|
+
detectScrollMovement = true
|
|
259
|
+
// Disallow parent views to intercept touch events, until we can detect if we should be
|
|
260
|
+
// capturing these touches or not.
|
|
261
|
+
this.parent.requestDisallowInterceptTouchEvent(true)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
MotionEvent.ACTION_MOVE -> {
|
|
265
|
+
if (detectScrollMovement) {
|
|
266
|
+
if (!canScrollVertically(-1) &&
|
|
267
|
+
!canScrollVertically(1) &&
|
|
268
|
+
!canScrollHorizontally(-1) &&
|
|
269
|
+
!canScrollHorizontally(1)
|
|
270
|
+
) {
|
|
271
|
+
// We cannot scroll, let parent views take care of these touches.
|
|
272
|
+
this.parent.requestDisallowInterceptTouchEvent(false)
|
|
273
|
+
}
|
|
274
|
+
detectScrollMovement = false
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return super.onTouchEvent(ev)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
override fun canScrollVertically(direction: Int): Boolean = scrollEnabled
|
|
283
|
+
|
|
284
|
+
override fun canScrollHorizontally(direction: Int): Boolean = scrollEnabled
|
|
285
|
+
|
|
286
|
+
override fun onSelectionChanged(
|
|
287
|
+
selStart: Int,
|
|
288
|
+
selEnd: Int,
|
|
289
|
+
) {
|
|
290
|
+
super.onSelectionChanged(selStart, selEnd)
|
|
291
|
+
selection?.onSelection(selStart, selEnd)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
override fun clearFocus() {
|
|
295
|
+
super.clearFocus()
|
|
296
|
+
inputMethodManager?.hideSoftInputFromWindow(windowToken, 0)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
override fun onFocusChanged(
|
|
300
|
+
focused: Boolean,
|
|
301
|
+
direction: Int,
|
|
302
|
+
previouslyFocusedRect: Rect?,
|
|
303
|
+
) {
|
|
304
|
+
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
|
305
|
+
val context = context as ReactContext
|
|
306
|
+
val surfaceId = UIManagerHelper.getSurfaceId(context)
|
|
307
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id)
|
|
308
|
+
|
|
309
|
+
if (focused) {
|
|
310
|
+
dispatcher?.dispatchEvent(OnInputFocusEvent(surfaceId, id, experimentalSynchronousEvents))
|
|
311
|
+
} else {
|
|
312
|
+
dispatcher?.dispatchEvent(OnInputBlurEvent(surfaceId, id, experimentalSynchronousEvents))
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
317
|
+
when (id) {
|
|
318
|
+
android.R.id.copy -> {
|
|
319
|
+
handleCustomCopy()
|
|
320
|
+
return true
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return super.onTextContextMenuItem(id)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private fun handleCustomCopy() {
|
|
327
|
+
val start = selectionStart
|
|
328
|
+
val end = selectionEnd
|
|
329
|
+
val spannable = text as Spannable
|
|
330
|
+
|
|
331
|
+
if (start < end) {
|
|
332
|
+
val selectedText = spannable.subSequence(start, end) as Spannable
|
|
333
|
+
val selectedHtml = EnrichedParser.toHtml(selectedText)
|
|
334
|
+
|
|
335
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
336
|
+
val clip = ClipData.newHtmlText(CLIPBOARD_TAG, selectedText, selectedHtml)
|
|
337
|
+
clipboard.setPrimaryClip(clip)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fun handleTextPaste(item: ClipData.Item) {
|
|
342
|
+
val currentText = text as Spannable
|
|
343
|
+
val start = selectionStart.coerceAtLeast(0)
|
|
344
|
+
val end = selectionEnd.coerceAtLeast(0)
|
|
345
|
+
val lengthBefore = currentText.length
|
|
346
|
+
|
|
347
|
+
val pastedSpannable: Spannable =
|
|
348
|
+
when {
|
|
349
|
+
item.htmlText != null -> {
|
|
350
|
+
val parsed = parseText(item.htmlText)
|
|
351
|
+
(parsed as? Spannable) ?: return
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
item.text != null -> {
|
|
355
|
+
SpannableString(item.text.toString())
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
else -> {
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
val finalText = currentText.mergeSpannables(start, end, pastedSpannable)
|
|
364
|
+
setValue(finalText, false)
|
|
365
|
+
|
|
366
|
+
// replacement-safe: oldLength - removed + inserted
|
|
367
|
+
val insertedLength = finalText.length - (lengthBefore - (end - start))
|
|
368
|
+
val pasteEnd = (start + insertedLength).coerceIn(0, finalText.length)
|
|
369
|
+
setSelection(pasteEnd)
|
|
370
|
+
|
|
371
|
+
// Detect links in the newly pasted range
|
|
372
|
+
parametrizedStyles?.detectLinksInRange(finalText, start.coerceAtMost(pasteEnd), pasteEnd)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fun requestFocusProgrammatically() {
|
|
376
|
+
requestFocus()
|
|
377
|
+
inputMethodManager?.showSoftInput(this, 0)
|
|
378
|
+
setSelection(selection?.start ?: text?.length ?: 0)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private fun normalizeHtmlIfNeeded(text: CharSequence): CharSequence {
|
|
382
|
+
if (!useHtmlNormalizer) return text
|
|
383
|
+
val normalized = GumboNormalizer.normalizeHtml(text.toString()) ?: return text
|
|
384
|
+
|
|
385
|
+
return try {
|
|
386
|
+
val parsed = EnrichedParser.fromHtml(normalized, htmlStyle, spannableFactory)
|
|
387
|
+
parsed.trimEnd('\n')
|
|
388
|
+
} catch (e: Exception) {
|
|
389
|
+
Log.e(TAG, "Error parsing normalized HTML: ${e.message}")
|
|
390
|
+
text
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private fun parseText(text: CharSequence): CharSequence {
|
|
395
|
+
val isInternalHtml = text.startsWith("<html>") && text.endsWith("</html>")
|
|
396
|
+
|
|
397
|
+
if (isInternalHtml) {
|
|
398
|
+
try {
|
|
399
|
+
val parsed = EnrichedParser.fromHtml(text.toString(), htmlStyle, spannableFactory)
|
|
400
|
+
return parsed.trimEnd('\n')
|
|
401
|
+
} catch (e: Exception) {
|
|
402
|
+
Log.e(TAG, "Error parsing HTML: ${e.message}")
|
|
403
|
+
return normalizeHtmlIfNeeded(text)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return normalizeHtmlIfNeeded(text)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
fun setValue(
|
|
411
|
+
value: CharSequence?,
|
|
412
|
+
shouldParseHtml: Boolean = true,
|
|
413
|
+
) {
|
|
414
|
+
if (value == null) return
|
|
415
|
+
|
|
416
|
+
runAsATransaction {
|
|
417
|
+
val newText = if (shouldParseHtml) parseText(value) else value
|
|
418
|
+
setText(newText)
|
|
419
|
+
applyLineSpacing()
|
|
420
|
+
|
|
421
|
+
observeAsyncImages()
|
|
422
|
+
|
|
423
|
+
// Scroll to the last line of text
|
|
424
|
+
setSelection(text?.length ?: 0)
|
|
425
|
+
}
|
|
426
|
+
layoutManager.invalidateLayout()
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
fun insertText(
|
|
430
|
+
text: String,
|
|
431
|
+
start: Int,
|
|
432
|
+
end: Int,
|
|
433
|
+
) {
|
|
434
|
+
val editable = this.text ?: return
|
|
435
|
+
val replaceStart: Int
|
|
436
|
+
val replaceEnd: Int
|
|
437
|
+
|
|
438
|
+
if (start < 0) {
|
|
439
|
+
// Use current selection
|
|
440
|
+
replaceStart = selectionStart
|
|
441
|
+
replaceEnd = selectionEnd
|
|
442
|
+
} else {
|
|
443
|
+
replaceStart = getActualIndex(start)
|
|
444
|
+
replaceEnd = getActualIndex(end)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
editable.replace(replaceStart, replaceEnd, text)
|
|
448
|
+
setSelection(replaceStart + text.length)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fun setCustomSelection(
|
|
452
|
+
visibleStart: Int,
|
|
453
|
+
visibleEnd: Int,
|
|
454
|
+
) {
|
|
455
|
+
val actualStart = getActualIndex(visibleStart)
|
|
456
|
+
val actualEnd = getActualIndex(visibleEnd)
|
|
457
|
+
|
|
458
|
+
setSelection(actualStart, actualEnd)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Helper: Walks through the string skipping ZWSPs to find the Nth visible character
|
|
462
|
+
private fun getActualIndex(visibleIndex: Int): Int {
|
|
463
|
+
val currentText = text as Spannable
|
|
464
|
+
var currentVisibleCount = 0
|
|
465
|
+
var actualIndex = 0
|
|
466
|
+
|
|
467
|
+
while (actualIndex < currentText.length) {
|
|
468
|
+
if (currentVisibleCount == visibleIndex) {
|
|
469
|
+
return actualIndex
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// If the current char is not a hidden space, it counts towards our visible index
|
|
473
|
+
if (currentText[actualIndex] != EnrichedConstants.ZWS) {
|
|
474
|
+
currentVisibleCount++
|
|
475
|
+
}
|
|
476
|
+
actualIndex++
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return actualIndex
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Finds all async images in the current text and sets up listeners
|
|
484
|
+
* to redraw the text layout when they finish downloading.
|
|
485
|
+
*/
|
|
486
|
+
private fun observeAsyncImages() {
|
|
487
|
+
val liveText = text ?: return
|
|
488
|
+
|
|
489
|
+
val spans = liveText.getSpans(0, liveText.length, EnrichedInputImageSpan::class.java)
|
|
490
|
+
|
|
491
|
+
for (span in spans) {
|
|
492
|
+
span.observeAsyncDrawableLoaded(liveText)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
fun setAutoFocus(autoFocus: Boolean) {
|
|
497
|
+
this.autoFocus = autoFocus
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
fun setPlaceholder(placeholder: String?) {
|
|
501
|
+
if (placeholder == null) return
|
|
502
|
+
|
|
503
|
+
hint = placeholder
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
fun setPlaceholderTextColor(colorInt: Int?) {
|
|
507
|
+
if (colorInt == null) return
|
|
508
|
+
|
|
509
|
+
setHintTextColor(colorInt)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
fun setSelectionColor(colorInt: Int?) {
|
|
513
|
+
if (colorInt == null) return
|
|
514
|
+
|
|
515
|
+
highlightColor = colorInt
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
fun setCursorColor(colorInt: Int?) {
|
|
519
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
520
|
+
val cursorDrawable = textCursorDrawable ?: return
|
|
521
|
+
|
|
522
|
+
if (colorInt != null) {
|
|
523
|
+
cursorDrawable.colorFilter = BlendModeColorFilter(colorInt, BlendMode.SRC_IN)
|
|
524
|
+
} else {
|
|
525
|
+
cursorDrawable.clearColorFilter()
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
textCursorDrawable = cursorDrawable
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
fun setReturnKeyLabel(returnKeyLabel: String?) {
|
|
533
|
+
setImeActionLabel(returnKeyLabel, EditorInfo.IME_ACTION_UNSPECIFIED)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
fun setColor(colorInt: Int?) {
|
|
537
|
+
if (colorInt == null) {
|
|
538
|
+
setTextColor(Color.BLACK)
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
setTextColor(colorInt)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
fun setFontSize(size: Float) {
|
|
546
|
+
if (size == 0f) return
|
|
547
|
+
|
|
548
|
+
val sizeInt = ceil(PixelUtil.toPixelFromSP(size))
|
|
549
|
+
fontSize = sizeInt
|
|
550
|
+
setTextSize(TypedValue.COMPLEX_UNIT_PX, sizeInt)
|
|
551
|
+
|
|
552
|
+
// This ensured that newly created spans will take the new font size into account
|
|
553
|
+
htmlStyle.invalidateStyles()
|
|
554
|
+
layoutManager.invalidateLayout()
|
|
555
|
+
forceScrollToSelection()
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
fun setLineHeight(height: Float) {
|
|
559
|
+
lineHeight = if (height == 0f) null else height
|
|
560
|
+
applyLineSpacing()
|
|
561
|
+
layoutManager.invalidateLayout()
|
|
562
|
+
forceScrollToSelection()
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private fun applyLineSpacing() {
|
|
566
|
+
val spannable = text as? Spannable ?: return
|
|
567
|
+
spannable
|
|
568
|
+
.getSpans(0, spannable.length, EnrichedLineHeightSpan::class.java)
|
|
569
|
+
.forEach { spannable.removeSpan(it) }
|
|
570
|
+
|
|
571
|
+
val lh = lineHeight ?: return
|
|
572
|
+
spannable.setSpan(
|
|
573
|
+
EnrichedLineHeightSpan(lh),
|
|
574
|
+
0,
|
|
575
|
+
spannable.length,
|
|
576
|
+
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
fun setFontFamily(family: String?) {
|
|
581
|
+
if (family != fontFamily) {
|
|
582
|
+
fontFamily = family
|
|
583
|
+
typefaceDirty = true
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
fun setFontWeight(weight: String?) {
|
|
588
|
+
val fontWeight = parseFontWeight(weight)
|
|
589
|
+
|
|
590
|
+
if (fontWeight != fontStyle) {
|
|
591
|
+
this.fontWeight = fontWeight
|
|
592
|
+
typefaceDirty = true
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
fun setFontStyle(style: String?) {
|
|
597
|
+
val fontStyle = parseFontStyle(style)
|
|
598
|
+
|
|
599
|
+
if (fontStyle != this.fontStyle) {
|
|
600
|
+
this.fontStyle = fontStyle
|
|
601
|
+
typefaceDirty = true
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
fun setAutoCapitalize(flagName: String?) {
|
|
606
|
+
val flag =
|
|
607
|
+
when (flagName) {
|
|
608
|
+
"none" -> InputType.TYPE_NULL
|
|
609
|
+
"sentences" -> InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
610
|
+
"words" -> InputType.TYPE_TEXT_FLAG_CAP_WORDS
|
|
611
|
+
"characters" -> InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
|
612
|
+
else -> InputType.TYPE_NULL
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
inputType = (
|
|
616
|
+
inputType and
|
|
617
|
+
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS.inv() and
|
|
618
|
+
InputType.TYPE_TEXT_FLAG_CAP_WORDS.inv() and
|
|
619
|
+
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES.inv()
|
|
620
|
+
) or if (flag == InputType.TYPE_NULL) 0 else flag
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
fun setLinkRegex(config: ReadableMap?) {
|
|
624
|
+
val patternStr = config?.getString("pattern")
|
|
625
|
+
if (patternStr == null) {
|
|
626
|
+
linkRegex = Patterns.WEB_URL
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (config.getBoolean("isDefault")) {
|
|
631
|
+
linkRegex = Patterns.WEB_URL
|
|
632
|
+
return
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (config.getBoolean("isDisabled")) {
|
|
636
|
+
linkRegex = null
|
|
637
|
+
return
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
var flags = 0
|
|
641
|
+
if (config.getBoolean("caseInsensitive")) flags = flags or Pattern.CASE_INSENSITIVE
|
|
642
|
+
if (config.getBoolean("dotAll")) flags = flags or Pattern.DOTALL
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
linkRegex = Pattern.compile("(?s).*?($patternStr).*", flags)
|
|
646
|
+
} catch (_: PatternSyntaxException) {
|
|
647
|
+
Log.w(TAG, "Invalid link regex pattern: $patternStr")
|
|
648
|
+
linkRegex = Patterns.WEB_URL
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
fun setContextMenuItems(items: ReadableArray?) {
|
|
653
|
+
if (items == null) {
|
|
654
|
+
contextMenuItems = emptyList()
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
val result = mutableListOf<Pair<Int, String>>()
|
|
659
|
+
for (i in 0 until items.size()) {
|
|
660
|
+
val item = items.getMap(i) ?: continue
|
|
661
|
+
val text = item.getString("text") ?: continue
|
|
662
|
+
result.add(Pair(i, text))
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
contextMenuItems = result
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
override fun startActionMode(
|
|
669
|
+
callback: ActionMode.Callback?,
|
|
670
|
+
type: Int,
|
|
671
|
+
): ActionMode? {
|
|
672
|
+
if (contextMenuItems.isEmpty()) {
|
|
673
|
+
return super.startActionMode(callback, type)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
val wrappedCallback =
|
|
677
|
+
object : ActionMode.Callback2() {
|
|
678
|
+
override fun onCreateActionMode(
|
|
679
|
+
mode: ActionMode,
|
|
680
|
+
menu: Menu,
|
|
681
|
+
): Boolean {
|
|
682
|
+
val result = callback?.onCreateActionMode(mode, menu) ?: false
|
|
683
|
+
for ((index, text) in contextMenuItems) {
|
|
684
|
+
menu.add(Menu.NONE, CONTEXT_MENU_ITEM_ID + index, Menu.NONE, text)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return result
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
override fun onPrepareActionMode(
|
|
691
|
+
mode: ActionMode,
|
|
692
|
+
menu: Menu,
|
|
693
|
+
) = callback?.onPrepareActionMode(mode, menu) ?: false
|
|
694
|
+
|
|
695
|
+
override fun onActionItemClicked(
|
|
696
|
+
mode: ActionMode,
|
|
697
|
+
menuItem: MenuItem,
|
|
698
|
+
): Boolean {
|
|
699
|
+
val itemId = menuItem.itemId
|
|
700
|
+
if (itemId < CONTEXT_MENU_ITEM_ID) {
|
|
701
|
+
return callback?.onActionItemClicked(mode, menuItem) ?: false
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
val selStart = selection?.start ?: 0
|
|
705
|
+
val selEnd = selection?.end ?: 0
|
|
706
|
+
val itemText = contextMenuItems.getOrNull(itemId - CONTEXT_MENU_ITEM_ID)?.second ?: return false
|
|
707
|
+
emitContextMenuItemPressEvent(itemText)
|
|
708
|
+
mode.finish()
|
|
709
|
+
post {
|
|
710
|
+
// Ensures selection is not lost after the action mode is finished
|
|
711
|
+
if (selStart in 0..selEnd) {
|
|
712
|
+
setSelection(selStart, selEnd)
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return true
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
override fun onDestroyActionMode(mode: ActionMode) {
|
|
719
|
+
callback?.onDestroyActionMode(mode)
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return super.startActionMode(wrappedCallback, type)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private fun emitContextMenuItemPressEvent(itemText: String) {
|
|
727
|
+
val start = selection?.start ?: return
|
|
728
|
+
val end = selection.end
|
|
729
|
+
val styleState = spanState?.getStyleStatePayload() ?: return
|
|
730
|
+
val currentText = text ?: return
|
|
731
|
+
val selectedText = currentText.subSequence(start, end).toString().replace(EnrichedConstants.ZWS_STRING, "")
|
|
732
|
+
|
|
733
|
+
val visibleStart = start - currentText.zwsCountBefore(start)
|
|
734
|
+
val visibleEnd = end - currentText.zwsCountBefore(end)
|
|
735
|
+
|
|
736
|
+
val reactContext = context as ReactContext
|
|
737
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
738
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
|
|
739
|
+
dispatcher?.dispatchEvent(
|
|
740
|
+
OnContextMenuItemPressEvent(
|
|
741
|
+
surfaceId,
|
|
742
|
+
id,
|
|
743
|
+
itemText,
|
|
744
|
+
selectedText,
|
|
745
|
+
visibleStart,
|
|
746
|
+
visibleEnd,
|
|
747
|
+
styleState,
|
|
748
|
+
experimentalSynchronousEvents,
|
|
749
|
+
),
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L283C2-L284C1
|
|
754
|
+
// After the text changes inside an EditText, TextView checks if a layout() has been requested.
|
|
755
|
+
// If it has, it will not scroll the text to the end of the new text inserted, but wait for the
|
|
756
|
+
// next layout() to be called. However, we do not perform a layout() after a requestLayout(), so
|
|
757
|
+
// we need to override isLayoutRequested to force EditText to scroll to the end of the new text
|
|
758
|
+
// immediately.
|
|
759
|
+
override fun isLayoutRequested(): Boolean = false
|
|
760
|
+
|
|
761
|
+
fun afterUpdateTransaction() {
|
|
762
|
+
updateTypeface()
|
|
763
|
+
updateDefaultValue()
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
fun setDefaultValue(value: CharSequence?) {
|
|
767
|
+
defaultValue = value
|
|
768
|
+
defaultValueDirty = true
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
fun shouldBlurOnReturn(): Boolean = submitBehavior == "blurAndSubmit"
|
|
772
|
+
|
|
773
|
+
fun shouldSubmitOnReturn(): Boolean = submitBehavior == "submit" || submitBehavior == "blurAndSubmit"
|
|
774
|
+
|
|
775
|
+
private fun updateDefaultValue() {
|
|
776
|
+
if (!defaultValueDirty) return
|
|
777
|
+
|
|
778
|
+
defaultValueDirty = false
|
|
779
|
+
setValue(defaultValue ?: "")
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
private fun updateTypeface() {
|
|
783
|
+
if (!typefaceDirty) return
|
|
784
|
+
typefaceDirty = false
|
|
785
|
+
|
|
786
|
+
val newTypeface = applyStyles(typeface, fontStyle, fontWeight, fontFamily, context.assets)
|
|
787
|
+
typeface = newTypeface
|
|
788
|
+
paint.typeface = newTypeface
|
|
789
|
+
|
|
790
|
+
layoutManager.invalidateLayout()
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private fun toggleStyle(name: String) {
|
|
794
|
+
when (name) {
|
|
795
|
+
EnrichedSpans.BOLD -> inlineStyles?.toggleStyle(EnrichedSpans.BOLD)
|
|
796
|
+
EnrichedSpans.ITALIC -> inlineStyles?.toggleStyle(EnrichedSpans.ITALIC)
|
|
797
|
+
EnrichedSpans.UNDERLINE -> inlineStyles?.toggleStyle(EnrichedSpans.UNDERLINE)
|
|
798
|
+
EnrichedSpans.STRIKETHROUGH -> inlineStyles?.toggleStyle(EnrichedSpans.STRIKETHROUGH)
|
|
799
|
+
EnrichedSpans.INLINE_CODE -> inlineStyles?.toggleStyle(EnrichedSpans.INLINE_CODE)
|
|
800
|
+
EnrichedSpans.H1 -> paragraphStyles?.toggleStyle(EnrichedSpans.H1)
|
|
801
|
+
EnrichedSpans.H2 -> paragraphStyles?.toggleStyle(EnrichedSpans.H2)
|
|
802
|
+
EnrichedSpans.H3 -> paragraphStyles?.toggleStyle(EnrichedSpans.H3)
|
|
803
|
+
EnrichedSpans.H4 -> paragraphStyles?.toggleStyle(EnrichedSpans.H4)
|
|
804
|
+
EnrichedSpans.H5 -> paragraphStyles?.toggleStyle(EnrichedSpans.H5)
|
|
805
|
+
EnrichedSpans.H6 -> paragraphStyles?.toggleStyle(EnrichedSpans.H6)
|
|
806
|
+
EnrichedSpans.CODE_BLOCK -> paragraphStyles?.toggleStyle(EnrichedSpans.CODE_BLOCK)
|
|
807
|
+
EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.toggleStyle(EnrichedSpans.BLOCK_QUOTE)
|
|
808
|
+
EnrichedSpans.ORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.ORDERED_LIST)
|
|
809
|
+
EnrichedSpans.UNORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.UNORDERED_LIST)
|
|
810
|
+
EnrichedSpans.CHECKBOX_LIST -> listStyles?.toggleStyle(EnrichedSpans.CHECKBOX_LIST)
|
|
811
|
+
else -> Log.w(TAG, "Unknown style: $name")
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
layoutManager.invalidateLayout()
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private fun removeStyle(
|
|
818
|
+
name: String,
|
|
819
|
+
start: Int,
|
|
820
|
+
end: Int,
|
|
821
|
+
): Boolean {
|
|
822
|
+
val removed =
|
|
823
|
+
when (name) {
|
|
824
|
+
EnrichedSpans.BOLD -> inlineStyles?.removeStyle(EnrichedSpans.BOLD, start, end)
|
|
825
|
+
EnrichedSpans.ITALIC -> inlineStyles?.removeStyle(EnrichedSpans.ITALIC, start, end)
|
|
826
|
+
EnrichedSpans.UNDERLINE -> inlineStyles?.removeStyle(EnrichedSpans.UNDERLINE, start, end)
|
|
827
|
+
EnrichedSpans.STRIKETHROUGH -> inlineStyles?.removeStyle(EnrichedSpans.STRIKETHROUGH, start, end)
|
|
828
|
+
EnrichedSpans.INLINE_CODE -> inlineStyles?.removeStyle(EnrichedSpans.INLINE_CODE, start, end)
|
|
829
|
+
EnrichedSpans.H1 -> paragraphStyles?.removeStyle(EnrichedSpans.H1, start, end)
|
|
830
|
+
EnrichedSpans.H2 -> paragraphStyles?.removeStyle(EnrichedSpans.H2, start, end)
|
|
831
|
+
EnrichedSpans.H3 -> paragraphStyles?.removeStyle(EnrichedSpans.H3, start, end)
|
|
832
|
+
EnrichedSpans.H4 -> paragraphStyles?.removeStyle(EnrichedSpans.H4, start, end)
|
|
833
|
+
EnrichedSpans.H5 -> paragraphStyles?.removeStyle(EnrichedSpans.H5, start, end)
|
|
834
|
+
EnrichedSpans.H6 -> paragraphStyles?.removeStyle(EnrichedSpans.H6, start, end)
|
|
835
|
+
EnrichedSpans.CODE_BLOCK -> paragraphStyles?.removeStyle(EnrichedSpans.CODE_BLOCK, start, end)
|
|
836
|
+
EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.removeStyle(EnrichedSpans.BLOCK_QUOTE, start, end)
|
|
837
|
+
EnrichedSpans.ORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.ORDERED_LIST, start, end)
|
|
838
|
+
EnrichedSpans.UNORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.UNORDERED_LIST, start, end)
|
|
839
|
+
EnrichedSpans.CHECKBOX_LIST -> listStyles?.removeStyle(EnrichedSpans.CHECKBOX_LIST, start, end)
|
|
840
|
+
EnrichedSpans.LINK -> parametrizedStyles?.removeStyle(EnrichedSpans.LINK, start, end)
|
|
841
|
+
EnrichedSpans.IMAGE -> parametrizedStyles?.removeStyle(EnrichedSpans.IMAGE, start, end)
|
|
842
|
+
EnrichedSpans.MENTION -> parametrizedStyles?.removeStyle(EnrichedSpans.MENTION, start, end)
|
|
843
|
+
else -> false
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
return removed == true
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private fun getTargetRange(name: String): Pair<Int, Int> {
|
|
850
|
+
val result =
|
|
851
|
+
when (name) {
|
|
852
|
+
EnrichedSpans.BOLD -> inlineStyles?.getStyleRange()
|
|
853
|
+
EnrichedSpans.ITALIC -> inlineStyles?.getStyleRange()
|
|
854
|
+
EnrichedSpans.UNDERLINE -> inlineStyles?.getStyleRange()
|
|
855
|
+
EnrichedSpans.STRIKETHROUGH -> inlineStyles?.getStyleRange()
|
|
856
|
+
EnrichedSpans.INLINE_CODE -> inlineStyles?.getStyleRange()
|
|
857
|
+
EnrichedSpans.H1 -> paragraphStyles?.getStyleRange()
|
|
858
|
+
EnrichedSpans.H2 -> paragraphStyles?.getStyleRange()
|
|
859
|
+
EnrichedSpans.H3 -> paragraphStyles?.getStyleRange()
|
|
860
|
+
EnrichedSpans.H4 -> paragraphStyles?.getStyleRange()
|
|
861
|
+
EnrichedSpans.H5 -> paragraphStyles?.getStyleRange()
|
|
862
|
+
EnrichedSpans.H6 -> paragraphStyles?.getStyleRange()
|
|
863
|
+
EnrichedSpans.CODE_BLOCK -> paragraphStyles?.getStyleRange()
|
|
864
|
+
EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.getStyleRange()
|
|
865
|
+
EnrichedSpans.ORDERED_LIST -> listStyles?.getStyleRange()
|
|
866
|
+
EnrichedSpans.UNORDERED_LIST -> listStyles?.getStyleRange()
|
|
867
|
+
EnrichedSpans.CHECKBOX_LIST -> listStyles?.getStyleRange()
|
|
868
|
+
EnrichedSpans.LINK -> parametrizedStyles?.getStyleRange()
|
|
869
|
+
EnrichedSpans.IMAGE -> parametrizedStyles?.getStyleRange()
|
|
870
|
+
EnrichedSpans.MENTION -> parametrizedStyles?.getStyleRange()
|
|
871
|
+
else -> Pair(0, 0)
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return result ?: Pair(0, 0)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private fun verifyStyle(name: String): Boolean {
|
|
878
|
+
val mergingConfig = EnrichedSpans.getMergingConfigForStyle(name, htmlStyle) ?: return true
|
|
879
|
+
val conflictingStyles = mergingConfig.conflictingStyles
|
|
880
|
+
val blockingStyles = mergingConfig.blockingStyles
|
|
881
|
+
val isEnabling = spanState?.getStart(name) == null
|
|
882
|
+
if (!isEnabling) return true
|
|
883
|
+
|
|
884
|
+
for (style in blockingStyles) {
|
|
885
|
+
if (spanState?.getStart(style) != null) {
|
|
886
|
+
spanState.setStart(name, null)
|
|
887
|
+
return false
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
for (style in conflictingStyles) {
|
|
892
|
+
val start = selection?.start ?: 0
|
|
893
|
+
val end = selection?.end ?: 0
|
|
894
|
+
val lengthBefore = text?.length ?: 0
|
|
895
|
+
|
|
896
|
+
runAsATransaction {
|
|
897
|
+
val targetRange = getTargetRange(name)
|
|
898
|
+
val removed = removeStyle(style, targetRange.first, targetRange.second)
|
|
899
|
+
if (removed) {
|
|
900
|
+
spanState?.setStart(style, null)
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
val lengthAfter = text?.length ?: 0
|
|
905
|
+
val charactersRemoved = lengthBefore - lengthAfter
|
|
906
|
+
val finalEnd =
|
|
907
|
+
if (charactersRemoved > 0) {
|
|
908
|
+
(end - charactersRemoved).coerceAtLeast(0)
|
|
909
|
+
} else {
|
|
910
|
+
end
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
val finalStart = start.coerceAtLeast(0).coerceAtMost(finalEnd)
|
|
914
|
+
selection?.onSelection(finalStart, finalEnd)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
return true
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
fun verifyAndToggleStyle(name: String) {
|
|
921
|
+
val isValid = verifyStyle(name)
|
|
922
|
+
if (!isValid) return
|
|
923
|
+
|
|
924
|
+
toggleStyle(name)
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
fun toggleCheckboxListItem(checked: Boolean) {
|
|
928
|
+
val isValid = verifyStyle(EnrichedSpans.CHECKBOX_LIST)
|
|
929
|
+
if (!isValid) return
|
|
930
|
+
|
|
931
|
+
listStyles?.toggleCheckboxListStyle(checked)
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
fun addLink(
|
|
935
|
+
start: Int,
|
|
936
|
+
end: Int,
|
|
937
|
+
text: String,
|
|
938
|
+
url: String,
|
|
939
|
+
) {
|
|
940
|
+
val isValid = verifyStyle(EnrichedSpans.LINK)
|
|
941
|
+
if (!isValid) return
|
|
942
|
+
|
|
943
|
+
parametrizedStyles?.setLinkSpan(getActualIndex(start), getActualIndex(end), text, url)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
fun removeLink(
|
|
947
|
+
start: Int,
|
|
948
|
+
end: Int,
|
|
949
|
+
) {
|
|
950
|
+
parametrizedStyles?.removeLinkSpans(getActualIndex(start), getActualIndex(end))
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
fun addImage(
|
|
954
|
+
src: String,
|
|
955
|
+
width: Float,
|
|
956
|
+
height: Float,
|
|
957
|
+
) {
|
|
958
|
+
val isValid = verifyStyle(EnrichedSpans.IMAGE)
|
|
959
|
+
if (!isValid) return
|
|
960
|
+
|
|
961
|
+
parametrizedStyles?.setImageSpan(src, width, height)
|
|
962
|
+
layoutManager.invalidateLayout()
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
fun startMention(indicator: String) {
|
|
966
|
+
val isValid = verifyStyle(EnrichedSpans.MENTION)
|
|
967
|
+
if (!isValid) return
|
|
968
|
+
|
|
969
|
+
parametrizedStyles?.startMention(indicator)
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
fun addMention(
|
|
973
|
+
indicator: String,
|
|
974
|
+
text: String,
|
|
975
|
+
attributes: Map<String, String>,
|
|
976
|
+
) {
|
|
977
|
+
val isValid = verifyStyle(EnrichedSpans.MENTION)
|
|
978
|
+
if (!isValid) return
|
|
979
|
+
|
|
980
|
+
parametrizedStyles?.setMentionSpan(text, indicator, attributes)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
fun requestHTML(requestId: Int) {
|
|
984
|
+
val html =
|
|
985
|
+
try {
|
|
986
|
+
EnrichedParser.toHtmlWithDefault(text)
|
|
987
|
+
} catch (_: Exception) {
|
|
988
|
+
null
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
val reactContext = context as ReactContext
|
|
992
|
+
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
|
|
993
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
|
|
994
|
+
dispatcher?.dispatchEvent(OnRequestHtmlResultEvent(surfaceId, id, requestId, html, experimentalSynchronousEvents))
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Sometimes setting up style triggers many changes in sequence
|
|
998
|
+
// Eg. removing conflicting styles -> changing text -> applying spans
|
|
999
|
+
// In such scenario we want to prevent from handling side effects (eg. onTextChanged)
|
|
1000
|
+
fun runAsATransaction(block: () -> Unit) {
|
|
1001
|
+
try {
|
|
1002
|
+
isDuringTransaction = true
|
|
1003
|
+
block()
|
|
1004
|
+
} finally {
|
|
1005
|
+
isDuringTransaction = false
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
private fun forceScrollToSelection() {
|
|
1010
|
+
val textLayout = layout ?: return
|
|
1011
|
+
val cursorOffset = selectionStart
|
|
1012
|
+
if (cursorOffset <= 0) return
|
|
1013
|
+
|
|
1014
|
+
val selectedLineIndex = textLayout.getLineForOffset(cursorOffset)
|
|
1015
|
+
val selectedLineTop = textLayout.getLineTop(selectedLineIndex)
|
|
1016
|
+
val selectedLineBottom = textLayout.getLineBottom(selectedLineIndex)
|
|
1017
|
+
val visibleTextHeight = height - paddingTop - paddingBottom
|
|
1018
|
+
|
|
1019
|
+
if (visibleTextHeight <= 0) return
|
|
1020
|
+
|
|
1021
|
+
val visibleTop = scrollY
|
|
1022
|
+
val visibleBottom = scrollY + visibleTextHeight
|
|
1023
|
+
var targetScrollY = scrollY
|
|
1024
|
+
|
|
1025
|
+
if (selectedLineTop < visibleTop) {
|
|
1026
|
+
targetScrollY = selectedLineTop
|
|
1027
|
+
} else if (selectedLineBottom > visibleBottom) {
|
|
1028
|
+
targetScrollY = selectedLineBottom - visibleTextHeight
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
val maxScrollY = (textLayout.height - visibleTextHeight).coerceAtLeast(0)
|
|
1032
|
+
targetScrollY = targetScrollY.coerceIn(0, maxScrollY)
|
|
1033
|
+
scrollTo(scrollX, targetScrollY)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private fun isHeadingBold(
|
|
1037
|
+
style: HtmlStyle,
|
|
1038
|
+
span: EnrichedInputSpan,
|
|
1039
|
+
): Boolean =
|
|
1040
|
+
when (span) {
|
|
1041
|
+
is EnrichedInputH1Span -> style.h1Bold
|
|
1042
|
+
is EnrichedInputH2Span -> style.h2Bold
|
|
1043
|
+
is EnrichedInputH3Span -> style.h3Bold
|
|
1044
|
+
is EnrichedInputH4Span -> style.h4Bold
|
|
1045
|
+
is EnrichedInputH5Span -> style.h5Bold
|
|
1046
|
+
is EnrichedInputH6Span -> style.h6Bold
|
|
1047
|
+
else -> false
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private fun shouldRemoveBoldFromHeading(
|
|
1051
|
+
span: EnrichedInputSpan,
|
|
1052
|
+
prevStyle: HtmlStyle,
|
|
1053
|
+
nextStyle: HtmlStyle,
|
|
1054
|
+
): Boolean {
|
|
1055
|
+
val wasBold = isHeadingBold(prevStyle, span)
|
|
1056
|
+
val isNowBold = isHeadingBold(nextStyle, span)
|
|
1057
|
+
|
|
1058
|
+
return !wasBold && isNowBold
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
private fun reApplyHtmlStyleForSpans(
|
|
1062
|
+
previousHtmlStyle: HtmlStyle,
|
|
1063
|
+
nextHtmlStyle: HtmlStyle,
|
|
1064
|
+
) {
|
|
1065
|
+
val spannable = text as? Spannable ?: return
|
|
1066
|
+
if (spannable.isEmpty()) return
|
|
1067
|
+
|
|
1068
|
+
var shouldEmitStateChange = false
|
|
1069
|
+
|
|
1070
|
+
runAsATransaction {
|
|
1071
|
+
val spans = spannable.getSpans(0, spannable.length, EnrichedInputSpan::class.java)
|
|
1072
|
+
for (span in spans) {
|
|
1073
|
+
if (!span.dependsOnHtmlStyle) continue
|
|
1074
|
+
|
|
1075
|
+
val start = spannable.getSpanStart(span)
|
|
1076
|
+
val end = spannable.getSpanEnd(span)
|
|
1077
|
+
val flags = spannable.getSpanFlags(span)
|
|
1078
|
+
|
|
1079
|
+
if (start == -1 || end == -1) continue
|
|
1080
|
+
|
|
1081
|
+
// Check if we need to remove explicit bold spans
|
|
1082
|
+
if (shouldRemoveBoldFromHeading(span, previousHtmlStyle, nextHtmlStyle)) {
|
|
1083
|
+
val isRemoved = removeStyle(EnrichedSpans.BOLD, start, end)
|
|
1084
|
+
if (isRemoved) shouldEmitStateChange = true
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
spannable.removeSpan(span)
|
|
1088
|
+
val newSpan = span.rebuildWithStyle(htmlStyle)
|
|
1089
|
+
spannable.setSpan(newSpan, start, end, flags)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (shouldEmitStateChange) {
|
|
1093
|
+
selection?.validateStyles()
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
layoutManager.invalidateLayout()
|
|
1097
|
+
forceScrollToSelection()
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
override fun onAttachedToWindow() {
|
|
1101
|
+
super.onAttachedToWindow()
|
|
1102
|
+
|
|
1103
|
+
// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L946
|
|
1104
|
+
// setTextIsSelectable internally calls setText(), which fires afterTextChanged that should be marked as a transaction to avoid unwanted side effects
|
|
1105
|
+
runAsATransaction { super.setTextIsSelectable(true) }
|
|
1106
|
+
|
|
1107
|
+
if (autoFocus && !didAttachToWindow) {
|
|
1108
|
+
requestFocusProgrammatically()
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
didAttachToWindow = true
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
companion object {
|
|
1115
|
+
const val TAG = "EnrichedTextInputView"
|
|
1116
|
+
const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
|
|
1117
|
+
private const val CONTEXT_MENU_ITEM_ID = 10000
|
|
1118
|
+
const val DEFAULT_IME_ACTION_LABEL = "DONE"
|
|
1119
|
+
}
|
|
1120
|
+
}
|