react-native-enriched-html 1.0.0 → 1.0.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/android/src/main/java/com/swmansion/enriched/common/EnrichedSpanFlags.kt +39 -0
- package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedParser.java +43 -20
- package/android/src/main/java/com/swmansion/enriched/common/parser/EnrichedSpanFactory.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/text/EnrichedTextSpanFactory.kt +1 -0
- package/android/src/main/java/com/swmansion/enriched/text/EnrichedTextView.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputSpannableFactory.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt +8 -5
- package/android/src/main/java/com/swmansion/enriched/textinput/events/MentionHandler.kt +12 -2
- package/android/src/main/java/com/swmansion/enriched/textinput/events/OnMentionEvent.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/AlignmentStyles.kt +6 -7
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/InlineStyles.kt +13 -1
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ListStyles.kt +4 -3
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParagraphStyles.kt +4 -3
- package/android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt +38 -11
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSelection.kt +2 -0
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/textinput/utils/Utils.kt +2 -1
- package/cpp/parser/GumboNormalizer.c +61 -3
- package/cpp/tests/GumboParserTest.cpp +54 -0
- package/ios/EnrichedTextInputView.mm +37 -25
- package/ios/extensions/LayoutManagerExtension.mm +73 -62
- package/ios/extensions/StringExtension.mm +75 -0
- package/ios/htmlParser/HtmlParser.h +4 -1
- package/ios/htmlParser/HtmlParser.mm +23 -5
- package/ios/inputHtmlParser/InputHtmlParser.mm +6 -3
- package/ios/interfaces/StyleHeaders.h +4 -0
- package/ios/styles/LinkStyle.mm +20 -34
- package/ios/styles/MentionStyle.mm +55 -4
- package/ios/textHtmlParser/TextHtmlParser.mm +2 -1
- package/ios/utils/ParagraphAttributesUtils.mm +7 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/expandMentionStylesForIndicators.js +2 -3
- package/lib/module/utils/expandMentionStylesForIndicators.js.map +1 -1
- package/lib/module/utils/normalizeHtmlStyle.js +1 -1
- package/lib/module/utils/normalizeHtmlStyle.js.map +1 -1
- package/lib/module/web/EnrichedText.css +343 -0
- package/lib/module/web/EnrichedText.js +55 -0
- package/lib/module/web/EnrichedText.js.map +1 -0
- package/lib/module/web/EnrichedTextInput.js +54 -52
- package/lib/module/web/EnrichedTextInput.js.map +1 -1
- package/lib/module/web/constants/brokenImageGlyph.js +9 -0
- package/lib/module/web/constants/brokenImageGlyph.js.map +1 -0
- package/lib/module/web/constants/classNames.js +5 -0
- package/lib/module/web/constants/classNames.js.map +1 -0
- package/lib/module/web/emitLinkDetected.js +12 -0
- package/lib/module/web/emitLinkDetected.js.map +1 -0
- package/lib/module/web/formats/EnrichedBlockquote.js +1 -1
- package/lib/module/web/formats/EnrichedBlockquote.js.map +1 -1
- package/lib/module/web/formats/EnrichedCheckboxList.js +2 -2
- package/lib/module/web/formats/EnrichedCheckboxList.js.map +1 -1
- package/lib/module/web/formats/EnrichedCodeBlock.js +1 -1
- package/lib/module/web/formats/EnrichedCodeBlock.js.map +1 -1
- package/lib/module/web/formats/EnrichedHeading.js +1 -1
- package/lib/module/web/formats/EnrichedHeading.js.map +1 -1
- package/lib/module/web/formats/EnrichedImageNodeView.js +1 -1
- package/lib/module/web/formats/EnrichedImageNodeView.js.map +1 -1
- package/lib/module/web/formats/EnrichedLink.js +43 -3
- package/lib/module/web/formats/EnrichedLink.js.map +1 -1
- package/lib/module/web/formats/EnrichedOrderedList.js +2 -2
- package/lib/module/web/formats/EnrichedOrderedList.js.map +1 -1
- package/lib/module/web/formats/EnrichedTextAlign.js +82 -0
- package/lib/module/web/formats/EnrichedTextAlign.js.map +1 -0
- package/lib/module/web/formats/EnrichedUnorderedList.js +2 -2
- package/lib/module/web/formats/EnrichedUnorderedList.js.map +1 -1
- package/lib/module/web/formats/applyWrappingListToSelection.js +3 -2
- package/lib/module/web/formats/applyWrappingListToSelection.js.map +1 -1
- package/lib/module/web/formats/formatRules.js +39 -3
- package/lib/module/web/formats/formatRules.js.map +1 -1
- package/lib/module/web/formats/listKeyboard.js +4 -1
- package/lib/module/web/formats/listKeyboard.js.map +1 -1
- package/lib/module/web/normalization/checkboxHtmlNormalizer.js.map +1 -0
- package/lib/module/web/normalization/htmlNormalizer.js +514 -0
- package/lib/module/web/normalization/htmlNormalizer.js.map +1 -0
- package/lib/module/web/normalization/prepareHtmlForWeb.js +78 -0
- package/lib/module/web/normalization/prepareHtmlForWeb.js.map +1 -0
- package/lib/module/web/{tiptapHtmlNormalizer.js → normalization/tiptapHtmlNormalizer.js} +6 -2
- package/lib/module/web/normalization/tiptapHtmlNormalizer.js.map +1 -0
- package/lib/module/web/pmPlugins/AutolinkPlugin/autolinkRegex.js +27 -0
- package/lib/module/web/pmPlugins/AutolinkPlugin/autolinkRegex.js.map +1 -0
- package/lib/module/web/pmPlugins/AutolinkPlugin/index.js +178 -0
- package/lib/module/web/pmPlugins/AutolinkPlugin/index.js.map +1 -0
- package/lib/module/web/pmPlugins/MentionPlugin/index.js +1 -1
- package/lib/module/web/pmPlugins/MentionPlugin/index.js.map +1 -1
- package/lib/module/web/pmPlugins/MentionPlugin/makeMentionPluginState.js +23 -4
- package/lib/module/web/pmPlugins/MentionPlugin/makeMentionPluginState.js.map +1 -1
- package/lib/module/web/pmPlugins/MentionPlugin/setMention.js +17 -18
- package/lib/module/web/pmPlugins/MentionPlugin/setMention.js.map +1 -1
- package/lib/module/web/pmPlugins/MentionPlugin/useMentionEvents.js +105 -0
- package/lib/module/web/pmPlugins/MentionPlugin/useMentionEvents.js.map +1 -0
- package/lib/module/web/pmPlugins/ShortcutPlugin.js +11 -0
- package/lib/module/web/pmPlugins/ShortcutPlugin.js.map +1 -1
- package/lib/module/web/pmPlugins/StrictMarksPlugin.js +2 -0
- package/lib/module/web/pmPlugins/StrictMarksPlugin.js.map +1 -1
- package/lib/module/web/pmPlugins/StripBoldInStyledHeadingsPlugin.js.map +1 -1
- package/lib/module/web/sanitization/htmlSanitizer.js +10 -0
- package/lib/module/web/sanitization/htmlSanitizer.js.map +1 -0
- package/lib/module/web/styleConversion/buildMentionRulesCSS.js +12 -6
- package/lib/module/web/styleConversion/buildMentionRulesCSS.js.map +1 -1
- package/lib/module/web/styleConversion/enrichedBaseStyleToCSSProperties.js +159 -0
- package/lib/module/web/styleConversion/enrichedBaseStyleToCSSProperties.js.map +1 -0
- package/lib/module/web/styleConversion/enrichedInputStyleToCSSProperties.js +3 -155
- package/lib/module/web/styleConversion/enrichedInputStyleToCSSProperties.js.map +1 -1
- package/lib/module/web/styleConversion/enrichedTextStyleToCSSProperties.js +44 -0
- package/lib/module/web/styleConversion/enrichedTextStyleToCSSProperties.js.map +1 -0
- package/lib/module/web/styleConversion/{enrichedInputThemingToCSSProperties.js → enrichedThemingToCSSProperties.js} +3 -3
- package/lib/module/web/styleConversion/enrichedThemingToCSSProperties.js.map +1 -0
- package/lib/module/web/styleConversion/htmlStyleToCSSVariables.js +87 -64
- package/lib/module/web/styleConversion/htmlStyleToCSSVariables.js.map +1 -1
- package/lib/module/web/styleConversion/inlineImageCSSVariables.js +7 -0
- package/lib/module/web/styleConversion/inlineImageCSSVariables.js.map +1 -0
- package/lib/module/web/useImageErrorFallback.js +33 -0
- package/lib/module/web/useImageErrorFallback.js.map +1 -0
- package/lib/module/web/useOnChangeHtml.js +1 -1
- package/lib/module/web/useOnChangeHtml.js.map +1 -1
- package/lib/module/web/useOnChangeState.js +6 -3
- package/lib/module/web/useOnChangeState.js.map +1 -1
- package/lib/module/web/useOnLinkDetected.js +32 -45
- package/lib/module/web/useOnLinkDetected.js.map +1 -1
- package/lib/module/web/usePressInteractions.js +19 -0
- package/lib/module/web/usePressInteractions.js.map +1 -0
- package/lib/module/web/useStableRef.js +13 -0
- package/lib/module/web/useStableRef.js.map +1 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/EnrichedText.d.ts +1 -1
- package/lib/typescript/src/native/EnrichedText.d.ts.map +1 -1
- package/lib/typescript/src/native/EnrichedTextInput.d.ts +1 -1
- package/lib/typescript/src/native/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +1 -1
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/expandMentionStylesForIndicators.d.ts +1 -1
- package/lib/typescript/src/utils/expandMentionStylesForIndicators.d.ts.map +1 -1
- package/lib/typescript/src/utils/normalizeHtmlStyle.d.ts.map +1 -1
- package/lib/typescript/src/web/EnrichedText.d.ts +4 -0
- package/lib/typescript/src/web/EnrichedText.d.ts.map +1 -0
- package/lib/typescript/src/web/EnrichedTextInput.d.ts +2 -2
- package/lib/typescript/src/web/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/web/constants/brokenImageGlyph.d.ts +3 -0
- package/lib/typescript/src/web/constants/brokenImageGlyph.d.ts.map +1 -0
- package/lib/typescript/src/web/constants/classNames.d.ts +3 -0
- package/lib/typescript/src/web/constants/classNames.d.ts.map +1 -0
- package/lib/typescript/src/web/emitLinkDetected.d.ts +10 -0
- package/lib/typescript/src/web/emitLinkDetected.d.ts.map +1 -0
- package/lib/typescript/src/web/formats/EnrichedBlockquote.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/EnrichedCheckboxList.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/EnrichedCodeBlock.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/EnrichedHeading.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/EnrichedImageNodeView.d.ts +1 -1
- package/lib/typescript/src/web/formats/EnrichedImageNodeView.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/EnrichedLink.d.ts +4 -1
- package/lib/typescript/src/web/formats/EnrichedLink.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/EnrichedOrderedList.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/EnrichedTextAlign.d.ts +2 -0
- package/lib/typescript/src/web/formats/EnrichedTextAlign.d.ts.map +1 -0
- package/lib/typescript/src/web/formats/EnrichedUnorderedList.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/applyWrappingListToSelection.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/formatRules.d.ts +3 -1
- package/lib/typescript/src/web/formats/formatRules.d.ts.map +1 -1
- package/lib/typescript/src/web/formats/listKeyboard.d.ts.map +1 -1
- package/lib/typescript/src/web/normalization/checkboxHtmlNormalizer.d.ts.map +1 -0
- package/lib/typescript/src/web/normalization/htmlNormalizer.d.ts +2 -0
- package/lib/typescript/src/web/normalization/htmlNormalizer.d.ts.map +1 -0
- package/lib/typescript/src/web/normalization/prepareHtmlForWeb.d.ts +2 -0
- package/lib/typescript/src/web/normalization/prepareHtmlForWeb.d.ts.map +1 -0
- package/lib/typescript/src/web/normalization/tiptapHtmlNormalizer.d.ts +3 -0
- package/lib/typescript/src/web/normalization/tiptapHtmlNormalizer.d.ts.map +1 -0
- package/lib/typescript/src/web/pmPlugins/AutolinkPlugin/autolinkRegex.d.ts +7 -0
- package/lib/typescript/src/web/pmPlugins/AutolinkPlugin/autolinkRegex.d.ts.map +1 -0
- package/lib/typescript/src/web/pmPlugins/AutolinkPlugin/index.d.ts +8 -0
- package/lib/typescript/src/web/pmPlugins/AutolinkPlugin/index.d.ts.map +1 -0
- package/lib/typescript/src/web/pmPlugins/MentionPlugin/index.d.ts +1 -1
- package/lib/typescript/src/web/pmPlugins/MentionPlugin/index.d.ts.map +1 -1
- package/lib/typescript/src/web/pmPlugins/MentionPlugin/makeMentionPluginState.d.ts.map +1 -1
- package/lib/typescript/src/web/pmPlugins/MentionPlugin/setMention.d.ts.map +1 -1
- package/lib/typescript/src/web/pmPlugins/MentionPlugin/useMentionEvents.d.ts +4 -0
- package/lib/typescript/src/web/pmPlugins/MentionPlugin/useMentionEvents.d.ts.map +1 -0
- package/lib/typescript/src/web/pmPlugins/ShortcutPlugin.d.ts.map +1 -1
- package/lib/typescript/src/web/pmPlugins/StrictMarksPlugin.d.ts.map +1 -1
- package/lib/typescript/src/web/pmPlugins/StripBoldInStyledHeadingsPlugin.d.ts +6 -1
- package/lib/typescript/src/web/pmPlugins/StripBoldInStyledHeadingsPlugin.d.ts.map +1 -1
- package/lib/typescript/src/web/sanitization/htmlSanitizer.d.ts +2 -0
- package/lib/typescript/src/web/sanitization/htmlSanitizer.d.ts.map +1 -0
- package/lib/typescript/src/web/styleConversion/buildMentionRulesCSS.d.ts +2 -2
- package/lib/typescript/src/web/styleConversion/buildMentionRulesCSS.d.ts.map +1 -1
- package/lib/typescript/src/web/styleConversion/enrichedBaseStyleToCSSProperties.d.ts +8 -0
- package/lib/typescript/src/web/styleConversion/enrichedBaseStyleToCSSProperties.d.ts.map +1 -0
- package/lib/typescript/src/web/styleConversion/enrichedInputStyleToCSSProperties.d.ts +2 -5
- package/lib/typescript/src/web/styleConversion/enrichedInputStyleToCSSProperties.d.ts.map +1 -1
- package/lib/typescript/src/web/styleConversion/enrichedTextStyleToCSSProperties.d.ts +4 -0
- package/lib/typescript/src/web/styleConversion/enrichedTextStyleToCSSProperties.d.ts.map +1 -0
- package/lib/typescript/src/web/styleConversion/{enrichedInputThemingToCSSProperties.d.ts → enrichedThemingToCSSProperties.d.ts} +1 -1
- package/lib/typescript/src/web/styleConversion/enrichedThemingToCSSProperties.d.ts.map +1 -0
- package/lib/typescript/src/web/styleConversion/htmlStyleToCSSVariables.d.ts +5 -4
- package/lib/typescript/src/web/styleConversion/htmlStyleToCSSVariables.d.ts.map +1 -1
- package/lib/typescript/src/web/styleConversion/inlineImageCSSVariables.d.ts +3 -0
- package/lib/typescript/src/web/styleConversion/inlineImageCSSVariables.d.ts.map +1 -0
- package/lib/typescript/src/web/useImageErrorFallback.d.ts +3 -0
- package/lib/typescript/src/web/useImageErrorFallback.d.ts.map +1 -0
- package/lib/typescript/src/web/useOnChangeState.d.ts.map +1 -1
- package/lib/typescript/src/web/useOnLinkDetected.d.ts +2 -2
- package/lib/typescript/src/web/useOnLinkDetected.d.ts.map +1 -1
- package/lib/typescript/src/web/usePressInteractions.d.ts +2 -0
- package/lib/typescript/src/web/usePressInteractions.d.ts.map +1 -0
- package/lib/typescript/src/web/useStableRef.d.ts +3 -0
- package/lib/typescript/src/web/useStableRef.d.ts.map +1 -0
- package/package.json +54 -28
- package/src/index.tsx +1 -0
- package/src/types.ts +5 -3
- package/src/utils/expandMentionStylesForIndicators.ts +3 -3
- package/src/utils/normalizeHtmlStyle.ts +2 -1
- package/src/web/EnrichedText.css +343 -0
- package/src/web/EnrichedText.tsx +82 -0
- package/src/web/EnrichedTextInput.tsx +69 -55
- package/src/web/constants/brokenImageGlyph.ts +7 -0
- package/src/web/constants/classNames.ts +2 -0
- package/src/web/emitLinkDetected.ts +29 -0
- package/src/web/formats/EnrichedBlockquote.ts +2 -1
- package/src/web/formats/EnrichedCheckboxList.ts +5 -2
- package/src/web/formats/EnrichedCodeBlock.ts +2 -1
- package/src/web/formats/EnrichedHeading.ts +2 -1
- package/src/web/formats/EnrichedImageNodeView.tsx +1 -3
- package/src/web/formats/EnrichedLink.ts +59 -4
- package/src/web/formats/EnrichedOrderedList.ts +5 -2
- package/src/web/formats/EnrichedTextAlign.ts +88 -0
- package/src/web/formats/EnrichedUnorderedList.ts +5 -2
- package/src/web/formats/applyWrappingListToSelection.ts +4 -4
- package/src/web/formats/formatRules.ts +48 -4
- package/src/web/formats/listKeyboard.ts +6 -1
- package/src/web/normalization/htmlNormalizer.ts +656 -0
- package/src/web/normalization/prepareHtmlForWeb.ts +93 -0
- package/src/web/{tiptapHtmlNormalizer.ts → normalization/tiptapHtmlNormalizer.ts} +12 -2
- package/src/web/pmPlugins/AutolinkPlugin/autolinkRegex.ts +43 -0
- package/src/web/pmPlugins/AutolinkPlugin/index.ts +274 -0
- package/src/web/pmPlugins/MentionPlugin/index.ts +1 -1
- package/src/web/pmPlugins/MentionPlugin/makeMentionPluginState.ts +32 -5
- package/src/web/pmPlugins/MentionPlugin/setMention.ts +19 -29
- package/src/web/pmPlugins/MentionPlugin/useMentionEvents.ts +114 -0
- package/src/web/pmPlugins/ShortcutPlugin.ts +7 -0
- package/src/web/pmPlugins/StrictMarksPlugin.ts +3 -0
- package/src/web/pmPlugins/StripBoldInStyledHeadingsPlugin.ts +41 -36
- package/src/web/sanitization/htmlSanitizer.ts +8 -0
- package/src/web/styleConversion/buildMentionRulesCSS.ts +30 -11
- package/src/web/styleConversion/enrichedBaseStyleToCSSProperties.ts +228 -0
- package/src/web/styleConversion/enrichedInputStyleToCSSProperties.ts +6 -217
- package/src/web/styleConversion/enrichedTextStyleToCSSProperties.ts +53 -0
- package/src/web/styleConversion/{enrichedInputThemingToCSSProperties.ts → enrichedThemingToCSSProperties.ts} +4 -4
- package/src/web/styleConversion/htmlStyleToCSSVariables.ts +121 -70
- package/src/web/styleConversion/inlineImageCSSVariables.ts +6 -0
- package/src/web/useImageErrorFallback.ts +37 -0
- package/src/web/useOnChangeHtml.ts +1 -1
- package/src/web/useOnChangeState.ts +7 -5
- package/src/web/useOnLinkDetected.ts +38 -50
- package/src/web/usePressInteractions.tsx +22 -0
- package/src/web/useStableRef.ts +12 -0
- package/lib/module/web/EnrichedTextInput.css +0 -201
- package/lib/module/web/checkboxHtmlNormalizer.js.map +0 -1
- package/lib/module/web/pmPlugins/MentionPlugin/subscribeMentionEvents.js +0 -100
- package/lib/module/web/pmPlugins/MentionPlugin/subscribeMentionEvents.js.map +0 -1
- package/lib/module/web/styleConversion/enrichedInputThemingToCSSProperties.js.map +0 -1
- package/lib/module/web/tiptapHtmlNormalizer.js.map +0 -1
- package/lib/typescript/src/web/checkboxHtmlNormalizer.d.ts.map +0 -1
- package/lib/typescript/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.d.ts +0 -4
- package/lib/typescript/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.d.ts.map +0 -1
- package/lib/typescript/src/web/styleConversion/enrichedInputThemingToCSSProperties.d.ts.map +0 -1
- package/lib/typescript/src/web/tiptapHtmlNormalizer.d.ts +0 -3
- package/lib/typescript/src/web/tiptapHtmlNormalizer.d.ts.map +0 -1
- package/src/web/EnrichedTextInput.css +0 -201
- package/src/web/pmPlugins/MentionPlugin/subscribeMentionEvents.ts +0 -105
- /package/lib/module/web/{checkboxHtmlNormalizer.js → normalization/checkboxHtmlNormalizer.js} +0 -0
- /package/lib/typescript/src/web/{checkboxHtmlNormalizer.d.ts → normalization/checkboxHtmlNormalizer.d.ts} +0 -0
- /package/src/web/{checkboxHtmlNormalizer.ts → normalization/checkboxHtmlNormalizer.ts} +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export function prepareHtmlForWeb(html: string): string {
|
|
2
|
+
if (typeof DOMParser === 'undefined') return html;
|
|
3
|
+
|
|
4
|
+
const parser = new DOMParser();
|
|
5
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
6
|
+
|
|
7
|
+
wrapBareLiContentInParagraph(doc);
|
|
8
|
+
checkboxHtmlToWeb(doc);
|
|
9
|
+
|
|
10
|
+
return doc.body.innerHTML;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Native list format:
|
|
15
|
+
* <ul>
|
|
16
|
+
* <li>foo</li>
|
|
17
|
+
* <li></li>
|
|
18
|
+
* </ul>
|
|
19
|
+
*
|
|
20
|
+
* Web-native, with <p> wrappers to display the content correctly:
|
|
21
|
+
* <ul>
|
|
22
|
+
* <li>
|
|
23
|
+
* <p>foo</p>
|
|
24
|
+
* </li>
|
|
25
|
+
* <li>
|
|
26
|
+
* <p></p>
|
|
27
|
+
* </li>
|
|
28
|
+
* </ul>
|
|
29
|
+
*/
|
|
30
|
+
function wrapBareLiContentInParagraph(doc: Document) {
|
|
31
|
+
// Target only standard lists (ignore checkbox lists, as they get wrapped in <label> later)
|
|
32
|
+
const listItems = doc.querySelectorAll(
|
|
33
|
+
'ul:not([data-type="checkbox"]) > li, ol > li'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
listItems.forEach((li) => {
|
|
37
|
+
if (li.firstElementChild?.tagName.toUpperCase() === 'P') return;
|
|
38
|
+
|
|
39
|
+
const nodesToWrap: Node[] = [];
|
|
40
|
+
const childNodes = Array.from(li.childNodes);
|
|
41
|
+
|
|
42
|
+
for (const node of childNodes) {
|
|
43
|
+
if (
|
|
44
|
+
node.nodeType === Node.ELEMENT_NODE &&
|
|
45
|
+
['UL', 'OL'].includes((node as Element).tagName.toUpperCase())
|
|
46
|
+
) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
nodesToWrap.push(node);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (nodesToWrap.length === 0 && childNodes.length > 0) return;
|
|
53
|
+
|
|
54
|
+
const p = doc.createElement('p');
|
|
55
|
+
|
|
56
|
+
li.insertBefore(p, childNodes[0] || null);
|
|
57
|
+
|
|
58
|
+
nodesToWrap.forEach((node) => p.appendChild(node));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* Native checkbox format (as produced by the editor):
|
|
64
|
+
* <ul data-type="checkbox">
|
|
65
|
+
* <li checked>foo</li>
|
|
66
|
+
* <li>bar</li>
|
|
67
|
+
* </ul>
|
|
68
|
+
*
|
|
69
|
+
* Web-native, display-only format:
|
|
70
|
+
* <ul data-type="checkbox">
|
|
71
|
+
* <li>
|
|
72
|
+
* <input type="checkbox" checked>
|
|
73
|
+
* <label>foo</label>
|
|
74
|
+
* </li>
|
|
75
|
+
* <li>
|
|
76
|
+
* <input type="checkbox">
|
|
77
|
+
* <label>bar</label>
|
|
78
|
+
* </li>
|
|
79
|
+
* </ul>
|
|
80
|
+
*/
|
|
81
|
+
function checkboxHtmlToWeb(doc: Document) {
|
|
82
|
+
doc.querySelectorAll('ul[data-type="checkbox"]').forEach((ul) => {
|
|
83
|
+
ul.querySelectorAll('li').forEach((li) => {
|
|
84
|
+
const checked = li.hasAttribute('checked');
|
|
85
|
+
const labelContent = li.innerHTML;
|
|
86
|
+
|
|
87
|
+
li.removeAttribute('checked');
|
|
88
|
+
li.innerHTML =
|
|
89
|
+
`<input type="checkbox"${checked ? ' checked' : ''}>` +
|
|
90
|
+
`<label>${labelContent}</label>`;
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -2,8 +2,15 @@ import {
|
|
|
2
2
|
checkboxHtmlForTiptap,
|
|
3
3
|
checkboxHtmlFromTiptap,
|
|
4
4
|
} from './checkboxHtmlNormalizer';
|
|
5
|
+
import { normalizeHtml } from './htmlNormalizer';
|
|
5
6
|
|
|
6
|
-
export function prepareHtmlForTiptap(
|
|
7
|
+
export function prepareHtmlForTiptap(
|
|
8
|
+
html: string,
|
|
9
|
+
useHtmlNormalizer: boolean | undefined
|
|
10
|
+
): string {
|
|
11
|
+
if (useHtmlNormalizer) {
|
|
12
|
+
html = normalizeHtml(html);
|
|
13
|
+
}
|
|
7
14
|
html = checkboxHtmlForTiptap(html);
|
|
8
15
|
html = html.replace(/<br\s*\/?>/gi, '<p></p>');
|
|
9
16
|
return html;
|
|
@@ -16,7 +23,10 @@ export function normalizeHtmlFromTiptap(html: string): string {
|
|
|
16
23
|
// TipTap renders <li><p>text</p></li> but native expects <li>text</li>.
|
|
17
24
|
// This regex is safe because EnrichedListItem.content is 'paragraph', which
|
|
18
25
|
// prevents TipTap from ever emitting nested lists
|
|
19
|
-
html = html.replace(
|
|
26
|
+
html = html.replace(
|
|
27
|
+
/<li([^>]*)>\s*<p[^>]*>(.*?)<\/p>\s*<\/li>/gs,
|
|
28
|
+
'<li$1>$2</li>'
|
|
29
|
+
);
|
|
20
30
|
|
|
21
31
|
// Convert remaining empty <p></p> to <br> (outside of lists)
|
|
22
32
|
html = html.replace(/<p><\/p>/g, '<br>');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const FULL_URL_REGEX =
|
|
2
|
+
/https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/;
|
|
3
|
+
const WWW_REGEX =
|
|
4
|
+
/www\.[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/;
|
|
5
|
+
const BARE_REGEX =
|
|
6
|
+
/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-z]{2,63}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)/;
|
|
7
|
+
|
|
8
|
+
const DEFAULT_AUTOLINK_SUBSTRING_PATTERN = `(?:${FULL_URL_REGEX.source})|(?:${WWW_REGEX.source})|(?:${BARE_REGEX.source})`;
|
|
9
|
+
|
|
10
|
+
export type AutolinkRangeInWord = {
|
|
11
|
+
start: number;
|
|
12
|
+
endExclusive: number;
|
|
13
|
+
text: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function asGlobalRegex(re: RegExp): RegExp {
|
|
17
|
+
return re.global
|
|
18
|
+
? re
|
|
19
|
+
: new RegExp(re.source, `${re.flags.replace(/g/g, '')}g`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// URL-like substrings inside a single whitespace-delimited token (`\S+`),
|
|
23
|
+
export function findAutolinkRangesInWord(
|
|
24
|
+
word: string,
|
|
25
|
+
linkRegex: RegExp | undefined
|
|
26
|
+
): readonly AutolinkRangeInWord[] {
|
|
27
|
+
const re = linkRegex
|
|
28
|
+
? asGlobalRegex(linkRegex)
|
|
29
|
+
: new RegExp(DEFAULT_AUTOLINK_SUBSTRING_PATTERN, 'gi');
|
|
30
|
+
|
|
31
|
+
const out: AutolinkRangeInWord[] = [];
|
|
32
|
+
for (const m of word.matchAll(re)) {
|
|
33
|
+
const text = m[0] ?? '';
|
|
34
|
+
if (text.length === 0 || m.index === undefined) continue;
|
|
35
|
+
const start = m.index;
|
|
36
|
+
out.push({
|
|
37
|
+
start,
|
|
38
|
+
endExclusive: start + text.length,
|
|
39
|
+
text,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import type { MarkType, Node, Schema } from '@tiptap/pm/model';
|
|
3
|
+
import { Plugin, PluginKey, type Transaction } from '@tiptap/pm/state';
|
|
4
|
+
import { Mapping } from '@tiptap/pm/transform';
|
|
5
|
+
import type { OnLinkDetected } from '../../../types';
|
|
6
|
+
import {
|
|
7
|
+
emitLinkDetected,
|
|
8
|
+
type LinkEmitterState,
|
|
9
|
+
} from '../../emitLinkDetected';
|
|
10
|
+
import { tiptapPosToNativePos } from '../../positionMapping';
|
|
11
|
+
import { findAutolinkRangesInWord } from './autolinkRegex';
|
|
12
|
+
|
|
13
|
+
interface Run {
|
|
14
|
+
text: string;
|
|
15
|
+
startPos: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface DirtyBlock {
|
|
19
|
+
node: Node;
|
|
20
|
+
pos: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface AutolinkPluginOptions {
|
|
24
|
+
getLinkEmitter: () => LinkEmitterState;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const WHITESPACE_RE = /\S+/g;
|
|
28
|
+
|
|
29
|
+
function removeAutoLinksInRange(
|
|
30
|
+
doc: Node,
|
|
31
|
+
tr: Transaction,
|
|
32
|
+
linkType: MarkType,
|
|
33
|
+
from: number,
|
|
34
|
+
to: number
|
|
35
|
+
): void {
|
|
36
|
+
if (from >= to) return;
|
|
37
|
+
|
|
38
|
+
doc.nodesBetween(from, to, (node, pos) => {
|
|
39
|
+
if (!node.isText) return true;
|
|
40
|
+
|
|
41
|
+
const link = linkType.isInSet(node.marks);
|
|
42
|
+
if (link?.attrs.auto === true) {
|
|
43
|
+
tr.removeMark(
|
|
44
|
+
Math.max(from, pos),
|
|
45
|
+
Math.min(to, pos + node.nodeSize),
|
|
46
|
+
linkType
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rangeHasManualLink(
|
|
55
|
+
doc: Node,
|
|
56
|
+
linkType: MarkType,
|
|
57
|
+
from: number,
|
|
58
|
+
to: number
|
|
59
|
+
): boolean {
|
|
60
|
+
let found = false;
|
|
61
|
+
|
|
62
|
+
doc.nodesBetween(from, to, (node) => {
|
|
63
|
+
if (found) return false;
|
|
64
|
+
if (!node.isText) return true;
|
|
65
|
+
|
|
66
|
+
const link = linkType.isInSet(node.marks);
|
|
67
|
+
if (link && link.attrs.auto !== true) found = true;
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function rangeHasExactAutoLink(
|
|
76
|
+
doc: Node,
|
|
77
|
+
linkType: MarkType,
|
|
78
|
+
from: number,
|
|
79
|
+
to: number,
|
|
80
|
+
href: string
|
|
81
|
+
): boolean {
|
|
82
|
+
let hasExact = true;
|
|
83
|
+
let hasText = false;
|
|
84
|
+
|
|
85
|
+
doc.nodesBetween(from, to, (node) => {
|
|
86
|
+
if (!hasExact) return false;
|
|
87
|
+
if (!node.isText) return true;
|
|
88
|
+
|
|
89
|
+
hasText = true;
|
|
90
|
+
const link = linkType.isInSet(node.marks);
|
|
91
|
+
if (!link || link.attrs.auto !== true || link.attrs.href !== href) {
|
|
92
|
+
hasExact = false;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return hasText && hasExact;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractRuns(
|
|
101
|
+
block: Node,
|
|
102
|
+
blockStartPos: number,
|
|
103
|
+
schema: Schema
|
|
104
|
+
): Run[] {
|
|
105
|
+
const runs: Run[] = [];
|
|
106
|
+
let current: Run | null = null;
|
|
107
|
+
|
|
108
|
+
block.forEach((child, offset) => {
|
|
109
|
+
const eligibleText =
|
|
110
|
+
child.isText &&
|
|
111
|
+
child.text &&
|
|
112
|
+
!schema.marks.code?.isInSet(child.marks) &&
|
|
113
|
+
!schema.marks.mention?.isInSet(child.marks);
|
|
114
|
+
|
|
115
|
+
if (eligibleText) {
|
|
116
|
+
if (!current) {
|
|
117
|
+
current = { text: '', startPos: blockStartPos + 1 + offset };
|
|
118
|
+
}
|
|
119
|
+
current.text += child.text;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (current) {
|
|
124
|
+
runs.push(current);
|
|
125
|
+
current = null;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (current) runs.push(current);
|
|
130
|
+
return runs;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getDirtyBlocks(
|
|
134
|
+
doc: Node,
|
|
135
|
+
transactions: readonly Transaction[]
|
|
136
|
+
): DirtyBlock[] {
|
|
137
|
+
const mapping = new Mapping();
|
|
138
|
+
for (const tr of transactions) mapping.appendMapping(tr.mapping);
|
|
139
|
+
|
|
140
|
+
const blocks = new Map<number, Node>();
|
|
141
|
+
const docSize = doc.content.size;
|
|
142
|
+
|
|
143
|
+
mapping.maps.forEach((stepMap, i) => {
|
|
144
|
+
const rest = mapping.slice(i + 1);
|
|
145
|
+
|
|
146
|
+
stepMap.forEach((_oldStart, _oldEnd, newStart, newEnd) => {
|
|
147
|
+
const from = Math.max(0, rest.map(newStart, -1) - 1);
|
|
148
|
+
const to = Math.min(docSize, rest.map(newEnd, 1) + 1);
|
|
149
|
+
|
|
150
|
+
doc.nodesBetween(from, to, (node, pos) => {
|
|
151
|
+
if (node.type.name === 'codeBlock') return false;
|
|
152
|
+
|
|
153
|
+
if (node.inlineContent && !blocks.has(pos)) {
|
|
154
|
+
blocks.set(pos, node);
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return true;
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return Array.from(blocks, ([pos, node]) => ({ pos, node }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const AutolinkPlugin = Extension.create<AutolinkPluginOptions>({
|
|
167
|
+
name: 'autolinkDetector',
|
|
168
|
+
addOptions() {
|
|
169
|
+
return {
|
|
170
|
+
getLinkEmitter: () => {
|
|
171
|
+
throw new Error(
|
|
172
|
+
'AutolinkPlugin.configure({ getLinkEmitter }) is required'
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
addProseMirrorPlugins() {
|
|
178
|
+
return [
|
|
179
|
+
new Plugin({
|
|
180
|
+
key: new PluginKey('autolinkDetector'),
|
|
181
|
+
appendTransaction: (transactions, _oldState, newState) => {
|
|
182
|
+
if (!transactions.some((tr) => tr.docChanged)) return null;
|
|
183
|
+
|
|
184
|
+
const state = this.options.getLinkEmitter();
|
|
185
|
+
if (!state || state.linkRegex === null) return null;
|
|
186
|
+
const { schema, doc, tr } = newState;
|
|
187
|
+
const linkType = schema.marks.link;
|
|
188
|
+
if (!linkType) return null;
|
|
189
|
+
|
|
190
|
+
const dirtyBlocks = getDirtyBlocks(doc, transactions);
|
|
191
|
+
if (dirtyBlocks.length === 0) return null;
|
|
192
|
+
|
|
193
|
+
const detected: OnLinkDetected[] = [];
|
|
194
|
+
|
|
195
|
+
for (const { node, pos } of dirtyBlocks) {
|
|
196
|
+
const blockFrom = pos + 1;
|
|
197
|
+
const blockTo = pos + node.nodeSize - 1;
|
|
198
|
+
|
|
199
|
+
// find all valid links that should exist in this block
|
|
200
|
+
const desiredLinks: Array<{
|
|
201
|
+
start: number;
|
|
202
|
+
end: number;
|
|
203
|
+
href: string;
|
|
204
|
+
}> = [];
|
|
205
|
+
|
|
206
|
+
for (const run of extractRuns(node, pos, schema)) {
|
|
207
|
+
for (const match of run.text.matchAll(WHITESPACE_RE)) {
|
|
208
|
+
const word = match[0];
|
|
209
|
+
const wordStart = run.startPos + match.index!;
|
|
210
|
+
const wordEnd = wordStart + word.length;
|
|
211
|
+
|
|
212
|
+
const ranges = findAutolinkRangesInWord(word, state.linkRegex);
|
|
213
|
+
const fullMatch = ranges.some(
|
|
214
|
+
(r) => r.start === 0 && r.endExclusive === word.length
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (!fullMatch) continue;
|
|
218
|
+
if (rangeHasManualLink(doc, linkType, wordStart, wordEnd))
|
|
219
|
+
continue;
|
|
220
|
+
|
|
221
|
+
desiredLinks.push({
|
|
222
|
+
start: wordStart,
|
|
223
|
+
end: wordEnd,
|
|
224
|
+
href: word,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let lastPos = blockFrom;
|
|
230
|
+
|
|
231
|
+
for (const link of desiredLinks) {
|
|
232
|
+
// strip auto links in the "gap" before this desired link
|
|
233
|
+
removeAutoLinksInRange(doc, tr, linkType, lastPos, link.start);
|
|
234
|
+
|
|
235
|
+
tr.addMark(
|
|
236
|
+
link.start,
|
|
237
|
+
link.end,
|
|
238
|
+
linkType.create({ href: link.href, auto: true })
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const alreadyExisted = rangeHasExactAutoLink(
|
|
242
|
+
doc,
|
|
243
|
+
linkType,
|
|
244
|
+
link.start,
|
|
245
|
+
link.end,
|
|
246
|
+
link.href
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// don't emit if the link was not changed
|
|
250
|
+
if (!alreadyExisted) {
|
|
251
|
+
detected.push({
|
|
252
|
+
text: link.href,
|
|
253
|
+
url: link.href,
|
|
254
|
+
start: tiptapPosToNativePos(doc, link.start),
|
|
255
|
+
end: tiptapPosToNativePos(doc, link.end),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
lastPos = link.end;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// strip rest of the auto links that are now not desired
|
|
263
|
+
removeAutoLinksInRange(doc, tr, linkType, lastPos, blockTo);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (tr.steps.length === 0) return null;
|
|
267
|
+
|
|
268
|
+
for (const event of detected) emitLinkDetected(state, event);
|
|
269
|
+
return tr;
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
];
|
|
273
|
+
},
|
|
274
|
+
});
|
|
@@ -11,7 +11,7 @@ export type { MentionPluginOptions, TriggerState } from './types';
|
|
|
11
11
|
export { mentionPluginKey } from './mentionPluginKey';
|
|
12
12
|
export { setMention } from './setMention';
|
|
13
13
|
export { startMention } from './startMention';
|
|
14
|
-
export {
|
|
14
|
+
export { useMentionEvents } from './useMentionEvents';
|
|
15
15
|
|
|
16
16
|
export const MentionPlugin = Extension.create<MentionPluginOptions>({
|
|
17
17
|
name: 'mentionTrigger',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { StateField } from '@tiptap/pm/state';
|
|
1
|
+
import type { EditorState, StateField } from '@tiptap/pm/state';
|
|
2
2
|
import { isCaretInBlockedContext } from './isCaretInBlockedContext';
|
|
3
3
|
import type { MentionPluginOptions, TriggerState } from './types';
|
|
4
|
+
import type { ResolvedPos } from '@tiptap/pm/model';
|
|
4
5
|
|
|
5
6
|
export function makeMentionPluginState(
|
|
6
7
|
getIndicators: MentionPluginOptions['getIndicators']
|
|
@@ -19,9 +20,11 @@ export function makeMentionPluginState(
|
|
|
19
20
|
return { active: false };
|
|
20
21
|
|
|
21
22
|
const blockStart = $from.start();
|
|
23
|
+
const wordEndPos = getCurrentWordEndPosition(newEditorState, $from);
|
|
24
|
+
|
|
22
25
|
const text = newEditorState.doc.textBetween(
|
|
23
26
|
blockStart,
|
|
24
|
-
|
|
27
|
+
wordEndPos,
|
|
25
28
|
'\n',
|
|
26
29
|
'\n'
|
|
27
30
|
);
|
|
@@ -45,7 +48,7 @@ export function makeMentionPluginState(
|
|
|
45
48
|
active: true,
|
|
46
49
|
indicator: found.indicator,
|
|
47
50
|
from: blockStart + found.indexInText,
|
|
48
|
-
to:
|
|
51
|
+
to: wordEndPos,
|
|
49
52
|
query,
|
|
50
53
|
};
|
|
51
54
|
},
|
|
@@ -65,11 +68,35 @@ function findLastValidMentionIndicator(
|
|
|
65
68
|
const isAfterSpace = idx > 0 && text[idx - 1] === ' ';
|
|
66
69
|
if (!isAtStart && !isAfterSpace) continue;
|
|
67
70
|
|
|
68
|
-
//
|
|
69
|
-
if (isIndicatorInsideFinalizedMention(idx))
|
|
71
|
+
// Stops inside a finalized mention
|
|
72
|
+
if (isIndicatorInsideFinalizedMention(idx)) return null;
|
|
70
73
|
|
|
71
74
|
return { indexInText: idx, indicator: ch };
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
return null;
|
|
75
78
|
}
|
|
79
|
+
|
|
80
|
+
function getCurrentWordEndPosition(state: EditorState, $from: ResolvedPos) {
|
|
81
|
+
const blockEnd = $from.end();
|
|
82
|
+
|
|
83
|
+
let wordEndPos = $from.pos;
|
|
84
|
+
|
|
85
|
+
while (wordEndPos < blockEnd) {
|
|
86
|
+
const char = state.doc.textBetween(wordEndPos, wordEndPos + 1);
|
|
87
|
+
|
|
88
|
+
if (/\s/.test(char)) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Break if advancing enters a blocked context
|
|
93
|
+
const $nextPos = state.doc.resolve(wordEndPos + 1);
|
|
94
|
+
if (isCaretInBlockedContext($nextPos, state.schema)) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
wordEndPos++;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return wordEndPos;
|
|
102
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment } from '@tiptap/pm/model';
|
|
2
|
-
import
|
|
2
|
+
import { TextSelection } from '@tiptap/pm/state';
|
|
3
3
|
import type { Editor } from '@tiptap/react';
|
|
4
4
|
import { isCaretInBlockedContext } from './isCaretInBlockedContext';
|
|
5
5
|
import { mentionPluginKey } from './mentionPluginKey';
|
|
@@ -34,10 +34,13 @@ export function setMention(
|
|
|
34
34
|
|
|
35
35
|
const { from, to } = triggerState;
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
37
|
+
// avoid inserting a space if there already is one
|
|
38
|
+
const parentEnd = state.doc.resolve(to).end();
|
|
39
|
+
let hasSpaceAfter = false;
|
|
40
|
+
if (to < parentEnd) {
|
|
41
|
+
const charAfter = state.doc.textBetween(to, to + 1, '');
|
|
42
|
+
hasSpaceAfter = /\s/.test(charAfter);
|
|
43
|
+
}
|
|
41
44
|
|
|
42
45
|
const mentionMark = mentionType.create({
|
|
43
46
|
indicator,
|
|
@@ -48,36 +51,23 @@ export function setMention(
|
|
|
48
51
|
.resolve(from)
|
|
49
52
|
.marks()
|
|
50
53
|
.filter((m) => m.type.name !== 'mention');
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
const nodes = [state.schema.text(text, mentionMark.addToSet(baseMarks))];
|
|
56
|
+
if (!hasSpaceAfter) {
|
|
57
|
+
nodes.push(state.schema.text(' ', baseMarks));
|
|
58
|
+
}
|
|
59
|
+
const fragment = Fragment.fromArray(nodes);
|
|
55
60
|
|
|
56
61
|
editor
|
|
57
62
|
.chain()
|
|
58
63
|
.focus()
|
|
59
64
|
.command(({ tr }) => {
|
|
60
|
-
tr.replaceWith(from,
|
|
65
|
+
tr.replaceWith(from, to, fragment);
|
|
66
|
+
|
|
67
|
+
const targetPos = from + fragment.size + (hasSpaceAfter ? 1 : 0);
|
|
68
|
+
tr.setSelection(TextSelection.create(tr.doc, targetPos));
|
|
69
|
+
|
|
61
70
|
return true;
|
|
62
71
|
})
|
|
63
72
|
.run();
|
|
64
73
|
}
|
|
65
|
-
|
|
66
|
-
// If the user moved the caret back into a partial match, extend `to` over the
|
|
67
|
-
// matching tail to avoid leftover characters after the inserted mention.
|
|
68
|
-
function exclusiveEndThroughMatchingMentionTail(
|
|
69
|
-
state: EditorState,
|
|
70
|
-
from: number,
|
|
71
|
-
text: string
|
|
72
|
-
): number {
|
|
73
|
-
const parentEnd = state.doc.resolve(from).end();
|
|
74
|
-
let scanPos = from;
|
|
75
|
-
for (const ch of text) {
|
|
76
|
-
const end = scanPos + ch.length;
|
|
77
|
-
if (end > parentEnd || state.doc.textBetween(scanPos, end, '') !== ch) {
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
scanPos = end;
|
|
81
|
-
}
|
|
82
|
-
return scanPos;
|
|
83
|
-
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { getMarkRange } from '@tiptap/core';
|
|
2
|
+
import type { Editor } from '@tiptap/react';
|
|
3
|
+
import type { OnMentionDetected } from '../../../types';
|
|
4
|
+
import { mentionPluginKey } from './mentionPluginKey';
|
|
5
|
+
import type { MentionCallbacks, TriggerState } from './types';
|
|
6
|
+
import { useEffect } from 'react';
|
|
7
|
+
import type { EditorState } from '@tiptap/pm/state';
|
|
8
|
+
|
|
9
|
+
export function useMentionEvents(
|
|
10
|
+
editor: Editor,
|
|
11
|
+
getCallbacks: () => MentionCallbacks
|
|
12
|
+
) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
let prevTriggerState: TriggerState = { active: false };
|
|
15
|
+
let prevMentionKey: string | null = null;
|
|
16
|
+
let wasInMention = false;
|
|
17
|
+
|
|
18
|
+
const handleUpdate = () => {
|
|
19
|
+
const cb = getCallbacks();
|
|
20
|
+
const curr = mentionPluginKey.getState(editor.state);
|
|
21
|
+
if (!curr) return;
|
|
22
|
+
|
|
23
|
+
if (!prevTriggerState.active && curr.active) {
|
|
24
|
+
cb.onStartMention?.(curr.indicator);
|
|
25
|
+
if (curr.query !== '')
|
|
26
|
+
cb.onChangeMention?.({ indicator: curr.indicator, text: curr.query });
|
|
27
|
+
} else if (
|
|
28
|
+
prevTriggerState.active &&
|
|
29
|
+
curr.active &&
|
|
30
|
+
prevTriggerState.indicator !== curr.indicator
|
|
31
|
+
) {
|
|
32
|
+
cb.onEndMention?.(prevTriggerState.indicator);
|
|
33
|
+
cb.onStartMention?.(curr.indicator);
|
|
34
|
+
if (curr.query !== '')
|
|
35
|
+
cb.onChangeMention?.({ indicator: curr.indicator, text: curr.query });
|
|
36
|
+
} else if (
|
|
37
|
+
prevTriggerState.active &&
|
|
38
|
+
curr.active &&
|
|
39
|
+
curr.query !== prevTriggerState.query
|
|
40
|
+
) {
|
|
41
|
+
cb.onChangeMention?.({ indicator: curr.indicator, text: curr.query });
|
|
42
|
+
} else if (prevTriggerState.active && !curr.active) {
|
|
43
|
+
cb.onEndMention?.(prevTriggerState.indicator);
|
|
44
|
+
}
|
|
45
|
+
prevTriggerState = curr;
|
|
46
|
+
|
|
47
|
+
if (!cb.onMentionDetected) return;
|
|
48
|
+
|
|
49
|
+
const mention = getMentionInCurrentSelection(editor.state);
|
|
50
|
+
if (!mention) {
|
|
51
|
+
if (wasInMention) {
|
|
52
|
+
cb.onMentionDetected({
|
|
53
|
+
text: '',
|
|
54
|
+
indicator: '',
|
|
55
|
+
attributes: {},
|
|
56
|
+
});
|
|
57
|
+
wasInMention = false;
|
|
58
|
+
prevMentionKey = null;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
wasInMention = true;
|
|
62
|
+
if (mention.key === prevMentionKey) return;
|
|
63
|
+
prevMentionKey = mention.key;
|
|
64
|
+
cb.onMentionDetected({
|
|
65
|
+
text: mention.text,
|
|
66
|
+
indicator: mention.indicator,
|
|
67
|
+
attributes: mention.attributes,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleBlur = () => {
|
|
73
|
+
const cb = getCallbacks();
|
|
74
|
+
if (prevTriggerState.active) {
|
|
75
|
+
cb.onEndMention?.(prevTriggerState.indicator);
|
|
76
|
+
prevTriggerState = { active: false };
|
|
77
|
+
}
|
|
78
|
+
prevMentionKey = null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
editor.on('transaction', handleUpdate);
|
|
82
|
+
editor.on('blur', handleBlur);
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
editor.off('transaction', handleUpdate);
|
|
86
|
+
editor.off('blur', handleBlur);
|
|
87
|
+
};
|
|
88
|
+
}, [editor, getCallbacks]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getMentionInCurrentSelection(
|
|
92
|
+
state: EditorState
|
|
93
|
+
): (OnMentionDetected & { key: string }) | null {
|
|
94
|
+
const mentionType = state.schema.marks.mention;
|
|
95
|
+
if (!mentionType) return null;
|
|
96
|
+
|
|
97
|
+
const { from: selFrom, to: selTo } = state.selection;
|
|
98
|
+
const $from = state.doc.resolve(selFrom);
|
|
99
|
+
const mark = mentionType.isInSet($from.marks());
|
|
100
|
+
if (!mark) return null;
|
|
101
|
+
|
|
102
|
+
const range = getMarkRange($from, mentionType);
|
|
103
|
+
if (!range) return null;
|
|
104
|
+
|
|
105
|
+
if (selFrom < range.from || selTo > range.to) return null;
|
|
106
|
+
|
|
107
|
+
const { text, indicator, attributes } = mark.attrs;
|
|
108
|
+
return {
|
|
109
|
+
key: `${range.from}:${range.to}:${text}:${indicator}`,
|
|
110
|
+
text: text as string,
|
|
111
|
+
indicator: indicator as string,
|
|
112
|
+
attributes: (attributes ?? {}) as Record<string, string>,
|
|
113
|
+
};
|
|
114
|
+
}
|