@expensify/react-native-live-markdown 0.1.143 → 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.
- package/lib/commonjs/MarkdownTextInput.web.js +3 -3
- package/lib/commonjs/MarkdownTextInput.web.js.map +1 -1
- package/lib/commonjs/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
- package/lib/commonjs/styleUtils.js +18 -1
- package/lib/commonjs/styleUtils.js.map +1 -1
- package/lib/commonjs/web/MarkdownTextInput.css +17 -0
- package/lib/commonjs/web/inputElements/inlineImage.js +104 -0
- package/lib/commonjs/web/inputElements/inlineImage.js.map +1 -0
- package/lib/commonjs/web/inputElements/loadingIndicator.js +52 -0
- package/lib/commonjs/web/inputElements/loadingIndicator.js.map +1 -0
- package/lib/commonjs/web/utils/animationUtils.js +55 -0
- package/lib/commonjs/web/utils/animationUtils.js.map +1 -0
- package/lib/commonjs/web/utils/blockUtils.js +32 -3
- package/lib/commonjs/web/utils/blockUtils.js.map +1 -1
- package/lib/commonjs/web/utils/inputUtils.js +12 -3
- package/lib/commonjs/web/utils/inputUtils.js.map +1 -1
- package/lib/commonjs/web/utils/parserUtils.js +25 -3
- package/lib/commonjs/web/utils/parserUtils.js.map +1 -1
- package/lib/commonjs/web/utils/treeUtils.js +1 -1
- package/lib/commonjs/web/utils/treeUtils.js.map +1 -1
- package/lib/module/MarkdownTextInput.web.js +3 -3
- package/lib/module/MarkdownTextInput.web.js.map +1 -1
- package/lib/module/MarkdownTextInputDecoratorViewNativeComponent.js.map +1 -1
- package/lib/module/styleUtils.js +18 -2
- package/lib/module/styleUtils.js.map +1 -1
- package/lib/module/web/MarkdownTextInput.css +17 -0
- package/lib/module/web/inputElements/inlineImage.js +99 -0
- package/lib/module/web/inputElements/inlineImage.js.map +1 -0
- package/lib/module/web/inputElements/loadingIndicator.js +47 -0
- package/lib/module/web/inputElements/loadingIndicator.js.map +1 -0
- package/lib/module/web/utils/animationUtils.js +49 -0
- package/lib/module/web/utils/animationUtils.js.map +1 -0
- package/lib/module/web/utils/blockUtils.js +30 -4
- package/lib/module/web/utils/blockUtils.js.map +1 -1
- package/lib/module/web/utils/inputUtils.js +12 -3
- package/lib/module/web/utils/inputUtils.js.map +1 -1
- package/lib/module/web/utils/parserUtils.js +26 -4
- package/lib/module/web/utils/parserUtils.js.map +1 -1
- package/lib/module/web/utils/treeUtils.js +1 -1
- package/lib/module/web/utils/treeUtils.js.map +1 -1
- package/lib/parser/react-native-live-markdown-parser.js +15 -15
- package/lib/typescript/src/MarkdownTextInput.web.d.ts.map +1 -1
- package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts +21 -0
- package/lib/typescript/src/MarkdownTextInputDecoratorViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/commonTypes.d.ts +1 -1
- package/lib/typescript/src/commonTypes.d.ts.map +1 -1
- package/lib/typescript/src/styleUtils.d.ts +2 -1
- package/lib/typescript/src/styleUtils.d.ts.map +1 -1
- package/lib/typescript/src/web/inputElements/inlineImage.d.ts +8 -0
- package/lib/typescript/src/web/inputElements/inlineImage.d.ts.map +1 -0
- package/lib/typescript/src/web/inputElements/loadingIndicator.d.ts +5 -0
- package/lib/typescript/src/web/inputElements/loadingIndicator.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/animationUtils.d.ts +12 -0
- package/lib/typescript/src/web/utils/animationUtils.d.ts.map +1 -0
- package/lib/typescript/src/web/utils/blockUtils.d.ts +7 -2
- package/lib/typescript/src/web/utils/blockUtils.d.ts.map +1 -1
- package/lib/typescript/src/web/utils/inputUtils.d.ts +1 -1
- package/lib/typescript/src/web/utils/inputUtils.d.ts.map +1 -1
- package/lib/typescript/src/web/utils/parserUtils.d.ts +2 -2
- package/lib/typescript/src/web/utils/parserUtils.d.ts.map +1 -1
- package/lib/typescript/src/web/utils/treeUtils.d.ts +1 -1
- package/lib/typescript/src/web/utils/treeUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/parser/react-native-live-markdown-parser.js +15 -15
- package/src/MarkdownTextInput.web.tsx +5 -3
- package/src/MarkdownTextInputDecoratorViewNativeComponent.ts +21 -0
- package/src/commonTypes.ts +15 -1
- package/src/styleUtils.ts +20 -2
- package/src/web/MarkdownTextInput.css +17 -0
- package/src/web/inputElements/inlineImage.ts +114 -0
- package/src/web/inputElements/loadingIndicator.ts +52 -0
- package/src/web/utils/animationUtils.ts +59 -0
- package/src/web/utils/blockUtils.ts +44 -3
- package/src/web/utils/inputUtils.ts +10 -3
- package/src/web/utils/parserUtils.ts +37 -3
- 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(
|
|
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 {
|
package/src/commonTypes.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
type MarkdownType =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|