@expensify/react-native-live-markdown 0.1.144 → 0.1.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/lib/commonjs/MarkdownTextInput.web.js +3 -3
  2. package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
  3. package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
  4. package/lib/commonjs/styleUtils.js +18 -1
  5. package/lib/commonjs/styleUtils.js.map +1 -1
  6. package/lib/commonjs/web/MarkdownTextInput.css +17 -0
  7. package/lib/commonjs/web/inputElements/inlineImage.js +104 -0
  8. package/lib/commonjs/web/inputElements/inlineImage.js.map +1 -0
  9. package/lib/commonjs/web/inputElements/loadingIndicator.js +52 -0
  10. package/lib/commonjs/web/inputElements/loadingIndicator.js.map +1 -0
  11. package/lib/commonjs/web/utils/animationUtils.js +55 -0
  12. package/lib/commonjs/web/utils/animationUtils.js.map +1 -0
  13. package/lib/commonjs/web/utils/blockUtils.js +32 -3
  14. package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
  15. package/lib/commonjs/web/utils/inputUtils.js +12 -3
  16. package/lib/commonjs/web/utils/inputUtils.js.map +1 -1
  17. package/lib/commonjs/web/utils/parserUtils.js +25 -3
  18. package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
  19. package/lib/commonjs/web/utils/treeUtils.js +1 -1
  20. package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
  21. package/lib/module/MarkdownTextInput.web.js +3 -3
  22. package/lib/module/MarkdownTextInput.web.js.map +1 -1
  23. package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
  24. package/lib/module/styleUtils.js +18 -2
  25. package/lib/module/styleUtils.js.map +1 -1
  26. package/lib/module/web/MarkdownTextInput.css +17 -0
  27. package/lib/module/web/inputElements/inlineImage.js +99 -0
  28. package/lib/module/web/inputElements/inlineImage.js.map +1 -0
  29. package/lib/module/web/inputElements/loadingIndicator.js +47 -0
  30. package/lib/module/web/inputElements/loadingIndicator.js.map +1 -0
  31. package/lib/module/web/utils/animationUtils.js +49 -0
  32. package/lib/module/web/utils/animationUtils.js.map +1 -0
  33. package/lib/module/web/utils/blockUtils.js +30 -4
  34. package/lib/module/web/utils/blockUtils.js.map +1 -1
  35. package/lib/module/web/utils/inputUtils.js +12 -3
  36. package/lib/module/web/utils/inputUtils.js.map +1 -1
  37. package/lib/module/web/utils/parserUtils.js +26 -4
  38. package/lib/module/web/utils/parserUtils.js.map +1 -1
  39. package/lib/module/web/utils/treeUtils.js +1 -1
  40. package/lib/module/web/utils/treeUtils.js.map +1 -1
  41. package/lib/parser/react-native-live-markdown-parser.js +15 -15
  42. package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
  43. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts +21 -0
  44. package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
  45. package/lib/typescript/src/commonTypes.d.ts +1 -1
  46. package/lib/typescript/src/commonTypes.d.ts.map +1 -1
  47. package/lib/typescript/src/styleUtils.d.ts +2 -1
  48. package/lib/typescript/src/styleUtils.d.ts.map +1 -1
  49. package/lib/typescript/src/web/inputElements/inlineImage.d.ts +8 -0
  50. package/lib/typescript/src/web/inputElements/inlineImage.d.ts.map +1 -0
  51. package/lib/typescript/src/web/inputElements/loadingIndicator.d.ts +5 -0
  52. package/lib/typescript/src/web/inputElements/loadingIndicator.d.ts.map +1 -0
  53. package/lib/typescript/src/web/utils/animationUtils.d.ts +12 -0
  54. package/lib/typescript/src/web/utils/animationUtils.d.ts.map +1 -0
  55. package/lib/typescript/src/web/utils/blockUtils.d.ts +7 -2
  56. package/lib/typescript/src/web/utils/blockUtils.d.ts.map +1 -1
  57. package/lib/typescript/src/web/utils/inputUtils.d.ts +1 -1
  58. package/lib/typescript/src/web/utils/inputUtils.d.ts.map +1 -1
  59. package/lib/typescript/src/web/utils/parserUtils.d.ts +2 -2
  60. package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -1
  61. package/lib/typescript/src/web/utils/treeUtils.d.ts +1 -1
  62. package/lib/typescript/src/web/utils/treeUtils.d.ts.map +1 -1
  63. package/package.json +1 -1
  64. package/parser/react-native-live-markdown-parser.js +15 -15
  65. package/src/MarkdownTextInput.web.tsx +5 -3
  66. package/src/MarkdownTextInputDecoratorViewNativeComponent.ts +21 -0
  67. package/src/commonTypes.ts +15 -1
  68. package/src/styleUtils.ts +20 -2
  69. package/src/web/MarkdownTextInput.css +17 -0
  70. package/src/web/inputElements/inlineImage.ts +114 -0
  71. package/src/web/inputElements/loadingIndicator.ts +52 -0
  72. package/src/web/utils/animationUtils.ts +59 -0
  73. package/src/web/utils/blockUtils.ts +44 -3
  74. package/src/web/utils/inputUtils.ts +10 -3
  75. package/src/web/utils/parserUtils.ts +37 -3
  76. package/src/web/utils/treeUtils.ts +2 -2
@@ -149,7 +149,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
149
149
  if (text === null) {
150
150
  return {text: divRef.current.value, cursorPosition: null};
151
151
  }
152
- const parsedText = updateInputStructure(target, text, cursorPosition, customMarkdownStyles, false, shouldForceDOMUpdate);
152
+ const parsedText = updateInputStructure(target, text, cursorPosition, multiline, customMarkdownStyles, false, shouldForceDOMUpdate);
153
153
  divRef.current.value = parsedText.text;
154
154
 
155
155
  if (history.current && shouldAddToHistory) {
@@ -158,7 +158,7 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
158
158
 
159
159
  return parsedText;
160
160
  },
161
- [],
161
+ [multiline],
162
162
  );
163
163
 
164
164
  const processedMarkdownStyle = useMemo(() => {
@@ -287,7 +287,9 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
287
287
 
288
288
  updateTextColor(divRef.current, e.target.textContent ?? '');
289
289
  const previousText = divRef.current.value;
290
- const parsedText = normalizeValue(inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement));
290
+ const parsedText = normalizeValue(
291
+ inputType === 'pasteText' ? pasteContent.current || '' : parseInnerHTMLToText(e.target as MarkdownTextInputElement, inputType, contentSelection.current.start),
292
+ );
291
293
 
292
294
  if (pasteContent.current) {
293
295
  pasteContent.current = null;
@@ -46,6 +46,27 @@ interface MarkdownStyle {
46
46
  color: ColorValue;
47
47
  backgroundColor: ColorValue;
48
48
  };
49
+ inlineImage: {
50
+ maxWidth: Float;
51
+ maxHeight: Float;
52
+ marginTop: Float;
53
+ marginBottom: Float;
54
+ };
55
+ loadingIndicatorContainer?: {
56
+ backgroundColor?: ColorValue;
57
+ borderWidth?: Float;
58
+ borderColor?: ColorValue;
59
+ borderRadius?: Float;
60
+ width?: Float;
61
+ height?: Float;
62
+ };
63
+ loadingIndicator?: {
64
+ primaryColor?: ColorValue;
65
+ secondaryColor?: ColorValue;
66
+ width?: Float;
67
+ height?: Float;
68
+ borderWidth?: Float;
69
+ };
49
70
  }
50
71
 
51
72
  interface NativeProps extends ViewProps {
@@ -1,4 +1,18 @@
1
- type MarkdownType = 'bold' | 'italic' | 'strikethrough' | 'emoji' | 'mention-here' | 'mention-user' | 'mention-report' | 'link' | 'code' | 'pre' | 'blockquote' | 'h1' | 'syntax';
1
+ type MarkdownType =
2
+ | 'bold'
3
+ | 'italic'
4
+ | 'strikethrough'
5
+ | 'emoji'
6
+ | 'mention-here'
7
+ | 'mention-user'
8
+ | 'mention-report'
9
+ | 'link'
10
+ | 'code'
11
+ | 'pre'
12
+ | 'blockquote'
13
+ | 'h1'
14
+ | 'syntax'
15
+ | 'inline-image';
2
16
 
3
17
  interface MarkdownRange {
4
18
  type: MarkdownType;
package/src/styleUtils.ts CHANGED
@@ -54,6 +54,16 @@ function makeDefaultMarkdownStyle(): MarkdownStyle {
54
54
  color: 'red',
55
55
  backgroundColor: 'pink',
56
56
  },
57
+ inlineImage: {
58
+ maxWidth: 150,
59
+ maxHeight: 150,
60
+ marginTop: 5,
61
+ marginBottom: 0,
62
+ },
63
+ loadingIndicator: {
64
+ primaryColor: 'gray',
65
+ secondaryColor: 'lightgray',
66
+ },
57
67
  };
58
68
  }
59
69
 
@@ -65,13 +75,21 @@ function mergeMarkdownStyleWithDefault(input: PartialMarkdownStyle | undefined):
65
75
  if (!(key in output)) {
66
76
  return;
67
77
  }
68
- Object.assign(output[key as keyof MarkdownStyle], input[key as keyof MarkdownStyle]);
78
+
79
+ const outputValue = output[key as keyof MarkdownStyle];
80
+ if (outputValue) {
81
+ Object.assign(outputValue, input[key as keyof MarkdownStyle]);
82
+ }
69
83
  });
70
84
  }
71
85
 
72
86
  return output;
73
87
  }
74
88
 
89
+ function parseStringWithUnitToNumber(value: string | null): number {
90
+ return value ? parseInt(value.replace('px', ''), 10) : 0;
91
+ }
92
+
75
93
  export type {PartialMarkdownStyle};
76
94
 
77
- export {mergeMarkdownStyleWithDefault};
95
+ export {mergeMarkdownStyleWithDefault, parseStringWithUnitToNumber};
@@ -24,3 +24,20 @@
24
24
  display: block; /* For Firefox */
25
25
  content: attr(placeholder);
26
26
  }
27
+
28
+ @keyframes react-native-live-markdown-spin {
29
+ 0% {
30
+ transform: rotate(0deg);
31
+ }
32
+ 100% {
33
+ transform: rotate(360deg);
34
+ }
35
+ }
36
+
37
+ .react-native-live-markdown-input-multiline [contenteditable='false'] {
38
+ -webkit-touch-callout: none;
39
+ -webkit-user-select: none;
40
+ -moz-user-select: none;
41
+ -ms-user-select: none;
42
+ user-select: none;
43
+ }
@@ -0,0 +1,114 @@
1
+ import type {HTMLMarkdownElement, MarkdownTextInputElement} from '../../MarkdownTextInput.web';
2
+ import type {MarkdownRange} from '../../commonTypes';
3
+ import {parseStringWithUnitToNumber} from '../../styleUtils';
4
+ import type {PartialMarkdownStyle} from '../../styleUtils';
5
+ import type {TreeNode} from '../utils/treeUtils';
6
+ import {createLoadingIndicator} from './loadingIndicator';
7
+
8
+ const inlineImageDefaultStyles = {
9
+ position: 'absolute',
10
+ bottom: 0,
11
+ left: 0,
12
+ };
13
+
14
+ function createImageElement(url: string, callback: (img: HTMLElement) => void) {
15
+ const imageContainer = document.createElement('span');
16
+ imageContainer.contentEditable = 'false';
17
+ imageContainer.setAttribute('data-type', 'inline-container');
18
+
19
+ const img = new Image();
20
+ imageContainer.appendChild(img);
21
+
22
+ img.contentEditable = 'false';
23
+ img.onload = () => callback(imageContainer);
24
+ img.onerror = () => callback(imageContainer);
25
+ img.src = url;
26
+ }
27
+
28
+ /** Adds already loaded image element from current input content to the tree node */
29
+ function updateImageTreeNode(targetNode: TreeNode, newElement: HTMLMarkdownElement) {
30
+ const paddingBottom = `${newElement.style.height}`;
31
+ targetNode.element.appendChild(newElement);
32
+
33
+ let currentParent = targetNode.element;
34
+ while (currentParent.parentElement && !['line', 'block'].includes(currentParent.getAttribute('data-type') || '')) {
35
+ currentParent = currentParent.parentElement as HTMLMarkdownElement;
36
+ }
37
+ Object.assign(currentParent.style, {
38
+ paddingBottom,
39
+ });
40
+
41
+ return targetNode;
42
+ }
43
+
44
+ /** The main function that adds inline image preview to the node */
45
+ function addInlineImagePreview(currentInput: MarkdownTextInputElement, targetNode: TreeNode, text: string, ranges: MarkdownRange[], markdownStyle: PartialMarkdownStyle) {
46
+ const linkRange = ranges.find((r) => r.type === 'link');
47
+ let imageHref = '';
48
+ if (linkRange) {
49
+ imageHref = text.substring(linkRange.start, linkRange.start + linkRange.length);
50
+ }
51
+
52
+ // If the inline image markdown with the same href exists in the current input, use it instead of creating new one.
53
+ // Prevents from image flickering and layout jumps
54
+ const alreadyLoadedPreview = currentInput.querySelector(`img[src="${imageHref}"]`);
55
+ const loadedImageContainer = alreadyLoadedPreview?.parentElement;
56
+ if (loadedImageContainer && loadedImageContainer.getAttribute('data-type') === 'inline-container') {
57
+ return updateImageTreeNode(targetNode, loadedImageContainer as HTMLMarkdownElement);
58
+ }
59
+
60
+ // Add a loading spinner
61
+ const spinner = createLoadingIndicator(imageHref, markdownStyle);
62
+ if (spinner) {
63
+ targetNode.element.appendChild(spinner);
64
+ }
65
+
66
+ const maxWidth = markdownStyle.inlineImage?.maxWidth;
67
+ const maxHeight = markdownStyle.inlineImage?.maxHeight;
68
+ const imageMarginTop = parseStringWithUnitToNumber(`${markdownStyle.inlineImage?.marginTop}`);
69
+ const imageMarginBottom = parseStringWithUnitToNumber(`${markdownStyle.inlineImage?.marginBottom}`);
70
+
71
+ Object.assign(targetNode.element.style, {
72
+ display: 'block',
73
+ marginBottom: `${imageMarginBottom}px`,
74
+ paddingBottom: markdownStyle.loadingIndicatorContainer?.height || markdownStyle.loadingIndicator?.height || (!!markdownStyle.loadingIndicator && '30px') || undefined,
75
+ });
76
+
77
+ createImageElement(imageHref, (imageContainer) => {
78
+ // Verify if the current spinner is for the loaded image. If not, it means that the response came after the user changed the image url
79
+ const currentSpinner = currentInput.querySelector('[data-type="spinner"]');
80
+ // Remove the spinner
81
+ if (currentSpinner) {
82
+ currentSpinner.remove();
83
+ }
84
+
85
+ // Set the image styles
86
+ Object.assign(imageContainer.style, {
87
+ ...inlineImageDefaultStyles,
88
+ maxHeight,
89
+ maxWidth,
90
+ });
91
+
92
+ const img = imageContainer.firstChild as HTMLImageElement;
93
+ Object.assign(img.style, {
94
+ maxHeight,
95
+ maxWidth,
96
+ });
97
+
98
+ targetNode.element.appendChild(imageContainer);
99
+
100
+ const imageHeight = `${imageContainer.clientHeight + imageMarginTop}px`;
101
+ Object.assign(imageContainer.style, {
102
+ height: imageHeight,
103
+ });
104
+ // Set paddingBottom to the height of the image so it's displayed under the block
105
+ Object.assign(targetNode.element.style, {
106
+ paddingBottom: imageHeight,
107
+ });
108
+ });
109
+
110
+ return targetNode;
111
+ }
112
+
113
+ // eslint-disable-next-line import/prefer-default-export
114
+ export {addInlineImagePreview};
@@ -0,0 +1,52 @@
1
+ import type {PartialMarkdownStyle} from '../../styleUtils';
2
+
3
+ const spinnerDefaultStyles = {
4
+ borderRadius: '50%',
5
+ display: 'block',
6
+ animationPlayState: 'paused',
7
+ };
8
+ const spinnerContainerDefaultStyles = {
9
+ position: 'absolute',
10
+ bottom: '0',
11
+ left: '0',
12
+ display: 'flex',
13
+ justifyContent: 'center',
14
+ alignItems: 'center',
15
+ };
16
+
17
+ /** Creates animated loading spinner */
18
+ function createLoadingIndicator(url: string, markdownStyle: PartialMarkdownStyle) {
19
+ const container = document.createElement('span');
20
+ container.contentEditable = 'false';
21
+
22
+ const spinner = document.createElement('span');
23
+ const spinnerStyles = markdownStyle.loadingIndicator;
24
+ if (spinnerStyles) {
25
+ const spinnerBorderWidth = spinnerStyles.borderWidth || 3;
26
+ Object.assign(spinner.style, {
27
+ ...spinnerDefaultStyles,
28
+ border: `${spinnerBorderWidth}px solid ${String(spinnerStyles.secondaryColor)}`,
29
+ borderTop: `${spinnerBorderWidth}px solid ${String(spinnerStyles.primaryColor)}`,
30
+ width: spinnerStyles.width || '20px',
31
+ height: spinnerStyles.height || '20px',
32
+ });
33
+ }
34
+
35
+ const containerStyles = markdownStyle.loadingIndicatorContainer;
36
+ Object.assign(container.style, {
37
+ ...markdownStyle.loadingIndicatorContainer,
38
+ ...spinnerContainerDefaultStyles,
39
+ width: containerStyles?.width || 'auto',
40
+ height: containerStyles?.height || 'auto',
41
+ });
42
+
43
+ container.setAttribute('data-type', 'spinner');
44
+ container.setAttribute('data-url', url);
45
+ container.contentEditable = 'false';
46
+ container.appendChild(spinner);
47
+
48
+ return container;
49
+ }
50
+
51
+ // eslint-disable-next-line import/prefer-default-export
52
+ export {createLoadingIndicator};
@@ -0,0 +1,59 @@
1
+ import type {HTMLMarkdownElement, MarkdownTextInputElement} from '../../MarkdownTextInput.web';
2
+
3
+ const ANIMATED_ELEMENT_TYPES = ['spinner'] as const;
4
+
5
+ type AnimatedElementType = (typeof ANIMATED_ELEMENT_TYPES)[number];
6
+
7
+ type AnimationTimes = {
8
+ [key in AnimatedElementType]?: CSSNumberish[];
9
+ };
10
+
11
+ const KEYFRAMES: {
12
+ [key in AnimatedElementType]?: Keyframe[];
13
+ } = {
14
+ spinner: [{transform: 'rotate(0deg)'}, {transform: 'rotate(360deg)'}],
15
+ };
16
+
17
+ const OPTIONS: {[key in AnimatedElementType]?: KeyframeAnimationOptions} = {
18
+ spinner: {
19
+ duration: 1000,
20
+ iterations: Infinity,
21
+ },
22
+ };
23
+
24
+ /** Gets the current times of all animated elements inside the input */
25
+ function getAnimationCurrentTimes(currentInput: MarkdownTextInputElement): AnimationTimes {
26
+ const animationTimes: AnimationTimes = {};
27
+
28
+ ANIMATED_ELEMENT_TYPES.forEach((type) => {
29
+ const elements = currentInput.querySelectorAll(`[data-type="${type}"]`);
30
+ animationTimes[type] = Array.from(elements).map((element) => {
31
+ const animation = (element.firstChild as HTMLMarkdownElement).getAnimations()[0];
32
+ if (animation) {
33
+ return animation.currentTime || 0;
34
+ }
35
+ return 0;
36
+ });
37
+ });
38
+
39
+ return animationTimes;
40
+ }
41
+
42
+ /** Updates the current times of all animated elements inside the input, to preserve their state between input rerenders */
43
+ function updateAnimationsTime(currentInput: MarkdownTextInputElement, animationTimes: AnimationTimes) {
44
+ ANIMATED_ELEMENT_TYPES.forEach((type) => {
45
+ const elements = currentInput.querySelectorAll(`[data-type="${type}"]`);
46
+ if (!KEYFRAMES[type]) {
47
+ return;
48
+ }
49
+
50
+ elements.forEach((element, index) => {
51
+ const animation = (element.firstChild as HTMLMarkdownElement).animate(KEYFRAMES[type] as Keyframe[], OPTIONS[type]);
52
+ if (animationTimes?.[type] && animation) {
53
+ animation.currentTime = animationTimes[type]?.[index] || 0;
54
+ }
55
+ });
56
+ });
57
+ }
58
+
59
+ export {getAnimationCurrentTimes, updateAnimationsTime};
@@ -1,5 +1,8 @@
1
+ import type {MarkdownTextInputElement} from '../../MarkdownTextInput.web';
2
+ import type {MarkdownRange} from '../../commonTypes';
1
3
  import type {PartialMarkdownStyle} from '../../styleUtils';
2
- import type {NodeType} from './treeUtils';
4
+ import {addInlineImagePreview} from '../inputElements/inlineImage';
5
+ import type {NodeType, TreeNode} from './treeUtils';
3
6
 
4
7
  function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownStyle: PartialMarkdownStyle) {
5
8
  const node = targetElement;
@@ -54,6 +57,7 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
54
57
  display: 'inline-block',
55
58
  maxWidth: '100%',
56
59
  boxSizing: 'border-box',
60
+ overflowWrap: 'anywhere',
57
61
  });
58
62
  break;
59
63
  case 'h1':
@@ -62,10 +66,47 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
62
66
  fontWeight: 'bold',
63
67
  });
64
68
  break;
69
+ case 'block':
70
+ Object.assign(node.style, {
71
+ display: 'block',
72
+ margin: '0',
73
+ padding: '0',
74
+ position: 'relative',
75
+ });
76
+ break;
77
+ default:
78
+ break;
79
+ }
80
+ }
81
+
82
+ const BLOCK_MARKDOWN_TYPES = ['inline-image'];
83
+ const FULL_LINE_MARKDOWN_TYPES = ['blockquote', 'h1'];
84
+
85
+ function isBlockMarkdownType(type: NodeType) {
86
+ return BLOCK_MARKDOWN_TYPES.includes(type);
87
+ }
88
+
89
+ function getFirstBlockMarkdownRange(ranges: MarkdownRange[]) {
90
+ const blockMarkdownRange = ranges.find((r) => isBlockMarkdownType(r.type) || FULL_LINE_MARKDOWN_TYPES.includes(r.type));
91
+ return FULL_LINE_MARKDOWN_TYPES.includes(blockMarkdownRange?.type || '') ? undefined : blockMarkdownRange;
92
+ }
93
+
94
+ function extendBlockStructure(
95
+ currentInput: MarkdownTextInputElement,
96
+ targetNode: TreeNode,
97
+ currentRange: MarkdownRange,
98
+ ranges: MarkdownRange[],
99
+ text: string,
100
+ markdownStyle: PartialMarkdownStyle,
101
+ ) {
102
+ switch (currentRange.type) {
103
+ case 'inline-image':
104
+ return addInlineImagePreview(currentInput, targetNode, text, ranges, markdownStyle);
65
105
  default:
66
106
  break;
67
107
  }
108
+
109
+ return targetNode;
68
110
  }
69
111
 
70
- // eslint-disable-next-line import/prefer-default-export
71
- export {addStyleToBlock};
112
+ export {addStyleToBlock, extendBlockStructure, isBlockMarkdownType, getFirstBlockMarkdownRange};
@@ -37,7 +37,7 @@ function normalizeValue(value: string) {
37
37
  }
38
38
 
39
39
  // Parses the HTML structure of a MarkdownTextInputElement to a plain text string. Used for getting the correct value of the input element.
40
- function parseInnerHTMLToText(target: MarkdownTextInputElement): string {
40
+ function parseInnerHTMLToText(target: MarkdownTextInputElement, inputType: string, cursorPosition: number): string {
41
41
  // Returns the parent of a given node that is higher in the hierarchy and is of a different type than 'text', 'br' or 'line'
42
42
  function getTopParentNode(node: ChildNode) {
43
43
  let currentParentNode = node.parentNode;
@@ -69,7 +69,9 @@ function parseInnerHTMLToText(target: MarkdownTextInputElement): string {
69
69
  if (node.nodeType === Node.TEXT_NODE) {
70
70
  shouldAddNewline = false;
71
71
  } else {
72
- if (shouldAddNewline) {
72
+ const firstChild = node.firstChild as HTMLElement;
73
+ const containsEmptyBlockElement = firstChild?.getAttribute?.('data-type') === 'block' && firstChild.textContent === '';
74
+ if (shouldAddNewline && !containsEmptyBlockElement) {
73
75
  text += '\n';
74
76
  shouldAddNewline = false;
75
77
  }
@@ -99,8 +101,13 @@ function parseInnerHTMLToText(target: MarkdownTextInputElement): string {
99
101
  }
100
102
  }
101
103
  }
104
+ text = text.replaceAll('\r\n', '\n');
102
105
 
103
- return text.replaceAll('\r\n', '\n');
106
+ // Force letter removal if the input value haven't changed but input type is 'delete'
107
+ if (text === target.value && inputType.includes('delete')) {
108
+ text = text.slice(0, cursorPosition - 1) + text.slice(cursorPosition);
109
+ }
110
+ return text;
104
111
  }
105
112
 
106
113
  export {isEventComposing, getPlaceholderValue, getElementHeight, parseInnerHTMLToText, normalizeValue};
@@ -3,8 +3,9 @@ import {addNodeToTree, updateTreeElementRefs} from './treeUtils';
3
3
  import type {NodeType, TreeNode} from './treeUtils';
4
4
  import type {PartialMarkdownStyle} from '../../styleUtils';
5
5
  import {getCurrentCursorPosition, moveCursorToEnd, setCursorPosition} from './cursorUtils';
6
- import {addStyleToBlock} from './blockUtils';
6
+ import {addStyleToBlock, extendBlockStructure, getFirstBlockMarkdownRange, isBlockMarkdownType} from './blockUtils';
7
7
  import type {MarkdownRange} from '../../commonTypes';
8
+ import {getAnimationCurrentTimes, updateAnimationsTime} from './animationUtils';
8
9
 
9
10
  type Paragraph = {
10
11
  text: string;
@@ -135,8 +136,22 @@ function addParagraph(node: TreeNode, text: string | null = null, length: number
135
136
  return pNode;
136
137
  }
137
138
 
139
+ function addBlockWrapper(targetNode: TreeNode, length: number, markdownStyle: PartialMarkdownStyle) {
140
+ const span = document.createElement('span') as HTMLMarkdownElement;
141
+ span.setAttribute('data-type', 'block');
142
+ addStyleToBlock(span, 'block', markdownStyle);
143
+ return appendNode(span, targetNode, 'block', length);
144
+ }
145
+
138
146
  /** Builds HTML DOM structure based on passed text and markdown ranges */
139
- function parseRangesToHTMLNodes(text: string, ranges: MarkdownRange[], markdownStyle: PartialMarkdownStyle = {}, disableInlineStyles = false) {
147
+ function parseRangesToHTMLNodes(
148
+ text: string,
149
+ ranges: MarkdownRange[],
150
+ isMultiline = true,
151
+ markdownStyle: PartialMarkdownStyle = {},
152
+ disableInlineStyles = false,
153
+ currentInput: MarkdownTextInputElement | null = null,
154
+ ) {
140
155
  const rootElement: HTMLMarkdownElement = document.createElement('span') as HTMLMarkdownElement;
141
156
  const textLength = text.length;
142
157
  const rootNode: TreeNode = {
@@ -180,6 +195,8 @@ function parseRangesToHTMLNodes(text: string, ranges: MarkdownRange[], markdownS
180
195
  addTextToElement(currentParentNode, line.text);
181
196
  }
182
197
 
198
+ let wasBlockGenerated = false;
199
+
183
200
  lastRangeEndIndex = line.start;
184
201
  const lineMarkdownRanges = line.markdownRanges;
185
202
  // go through all markdown ranges in the line
@@ -192,6 +209,12 @@ function parseRangesToHTMLNodes(text: string, ranges: MarkdownRange[], markdownS
192
209
  const endOfCurrentRange = range.start + range.length;
193
210
  const nextRangeStartIndex = lineMarkdownRanges.length > 0 && !!lineMarkdownRanges[0] ? lineMarkdownRanges[0].start || 0 : textLength;
194
211
 
212
+ // wrap all elements before the first block type markdown range with a span element
213
+ const blockRange = getFirstBlockMarkdownRange([range, ...lineMarkdownRanges]);
214
+ if (!wasBlockGenerated && blockRange) {
215
+ currentParentNode = addBlockWrapper(currentParentNode, line.text.substring(lastRangeEndIndex - line.start, blockRange.start + blockRange.length - line.start).length, markdownStyle);
216
+ wasBlockGenerated = true;
217
+ }
195
218
  // add text before the markdown range
196
219
  const textBeforeRange = line.text.substring(lastRangeEndIndex - line.start, range.start - line.start);
197
220
  if (textBeforeRange) {
@@ -201,12 +224,17 @@ function parseRangesToHTMLNodes(text: string, ranges: MarkdownRange[], markdownS
201
224
  // create markdown span element
202
225
  const span = document.createElement('span') as HTMLMarkdownElement;
203
226
  span.setAttribute('data-type', range.type);
227
+
204
228
  if (!disableInlineStyles) {
205
229
  addStyleToBlock(span, range.type, markdownStyle);
206
230
  }
207
231
 
208
232
  const spanNode = appendNode(span, currentParentNode, range.type, range.length);
209
233
 
234
+ if (isMultiline && !disableInlineStyles && currentInput) {
235
+ currentParentNode = extendBlockStructure(currentInput, currentParentNode, range, lineMarkdownRanges, text, markdownStyle);
236
+ }
237
+
210
238
  if (lineMarkdownRanges.length > 0 && nextRangeStartIndex < endOfCurrentRange && range.type !== 'syntax') {
211
239
  // tag nesting
212
240
  currentParentNode = spanNode;
@@ -226,6 +254,9 @@ function parseRangesToHTMLNodes(text: string, ranges: MarkdownRange[], markdownS
226
254
  if (currentParentNode.parentNode.type !== 'root') {
227
255
  currentParentNode.parentNode.element.value = currentParentNode.element.value || '';
228
256
  }
257
+ if (isBlockMarkdownType(currentParentNode.type)) {
258
+ wasBlockGenerated = false;
259
+ }
229
260
  currentParentNode = currentParentNode.parentNode || rootNode;
230
261
  }
231
262
  }
@@ -251,6 +282,7 @@ function updateInputStructure(
251
282
  target: MarkdownTextInputElement,
252
283
  text: string,
253
284
  cursorPositionIndex: number | null,
285
+ isMultiline = true,
254
286
  markdownStyle: PartialMarkdownStyle = {},
255
287
  alwaysMoveCursorToTheEnd = false,
256
288
  shouldForceDOMUpdate = false,
@@ -272,12 +304,14 @@ function updateInputStructure(
272
304
 
273
305
  // We don't want to parse text with single '\n', because contentEditable represents it as invisible <br />
274
306
  if (text) {
275
- const {dom, tree} = parseRangesToHTMLNodes(text, markdownRanges, markdownStyle);
307
+ const {dom, tree} = parseRangesToHTMLNodes(text, markdownRanges, isMultiline, markdownStyle, false, targetElement);
276
308
 
277
309
  if (shouldForceDOMUpdate || targetElement.innerHTML !== dom.innerHTML) {
310
+ const animationTimes = getAnimationCurrentTimes(targetElement);
278
311
  targetElement.innerHTML = '';
279
312
  targetElement.innerText = '';
280
313
  targetElement.innerHTML = dom.innerHTML;
314
+ updateAnimationsTime(targetElement, animationTimes);
281
315
  }
282
316
 
283
317
  updateTreeElementRefs(tree, targetElement);
@@ -1,7 +1,7 @@
1
1
  import type {HTMLMarkdownElement} from '../../MarkdownTextInput.web';
2
2
  import type {MarkdownRange, MarkdownType} from '../../commonTypes';
3
3
 
4
- type NodeType = MarkdownType | 'line' | 'text' | 'br' | 'root';
4
+ type NodeType = MarkdownType | 'line' | 'text' | 'br' | 'block' | 'root';
5
5
 
6
6
  type TreeNode = Omit<MarkdownRange, 'type'> & {
7
7
  element: HTMLMarkdownElement;
@@ -21,7 +21,7 @@ function addNodeToTree(element: HTMLMarkdownElement, parentTreeNode: TreeNode, t
21
21
  const lastParentChild = parentTreeNode.childNodes[parentChildrenCount - 1];
22
22
  if (lastParentChild) {
23
23
  startIndex = lastParentChild.start + lastParentChild.length;
24
- startIndex += lastParentChild.isGeneratingNewline || element.style.display === 'block' ? 1 : 0;
24
+ startIndex += lastParentChild.isGeneratingNewline || (type !== 'block' && element.style.display === 'block') ? 1 : 0;
25
25
  }
26
26
  }
27
27