Skip to content

Commit

Permalink
Merge branch 'main' into @tomekzaw/worklets
Browse files Browse the repository at this point in the history
  • Loading branch information
tomekzaw committed Sep 18, 2024
2 parents 769e7a3 + bd20d84 commit 41af79a
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 33 deletions.
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,16 @@ When you're sending a pull request:
- Review the documentation to make sure it looks good.
- Follow the pull request template when opening a pull request.
- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.
### Testing with Expensify/App (or other projects)
It's possible to locally develop this repo such with live-reload in another React Native project. These instructions are for Expensify/App, but they can be adapted to other repos as well.

1. Clone this repo
2. Run `yarn install`
3. Run `yarn build:watch`
4. In Expensify/App, run `npm install`.
- _Note:_ There is a patch for the `link` dev dependency in this repo. If you want these steps to work reliably, you'll likely need to copy that patch over.
5. In Expensify/App, run `npx link publish --watch ~/react-native-live-markdown --litmus .build_complete`
6. In E/App, run the app with `npm run web`/`npm run ios`/etc...
The end result should be that you can make a change directly in this repo, and your changes will live-reload in E/App.
2 changes: 1 addition & 1 deletion example/src/testConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const EXAMPLE_CONTENT = [
'@here',
'@[email protected]',
'#mention-report',
'![demo image](https://picsum.photos/id/1069/200/300)',
'![demo image](https://picsum.photos/id/1067/200/300)',
].join('\n');

const INPUT_ID = 'MarkdownInput_Example';
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@expensify/react-native-live-markdown",
"version": "0.1.150",
"version": "0.1.155",
"description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down
5 changes: 4 additions & 1 deletion src/MarkdownTextInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,10 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
}

const prevSelection = contentSelection.current ?? {start: 0, end: 0};
const newCursorPosition = Math.max(Math.max(contentSelection.current.end, 0) + (parsedText.length - previousText.length), 0);
const newCursorPosition =
inputType === 'deleteContentForward' && contentSelection.current.start === contentSelection.current.end
? Math.max(contentSelection.current.start, 0) // Don't move the caret when deleting forward with no characters selected
: Math.max(Math.max(contentSelection.current.end, 0) + (parsedText.length - previousText.length), 0);

if (compositionRef.current) {
updateTextColor(divRef.current, parsedText);
Expand Down
3 changes: 3 additions & 0 deletions src/MarkdownTextInputDecoratorViewNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,13 @@ interface MarkdownStyle {
backgroundColor: ColorValue;
};
inlineImage: {
minWidth: Float;
minHeight: Float;
maxWidth: Float;
maxHeight: Float;
marginTop: Float;
marginBottom: Float;
borderRadius: Float;
};
loadingIndicatorContainer?: {
backgroundColor?: ColorValue;
Expand Down
3 changes: 3 additions & 0 deletions src/styleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ function makeDefaultMarkdownStyle(): MarkdownStyle {
backgroundColor: 'pink',
},
inlineImage: {
minWidth: 50,
minHeight: 50,
maxWidth: 150,
maxHeight: 150,
marginTop: 5,
marginBottom: 0,
borderRadius: 5,
},
loadingIndicator: {
primaryColor: 'gray',
Expand Down
81 changes: 51 additions & 30 deletions src/web/inputElements/inlineImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,43 @@ import type {PartialMarkdownStyle} from '../../styleUtils';
import type {TreeNode} from '../utils/treeUtils';
import {createLoadingIndicator} from './loadingIndicator';

const INLINE_IMAGE_PREVIEW_DEBOUNCE_TIME_MS = 300;

const inlineImageDefaultStyles = {
position: 'absolute',
bottom: 0,
left: 0,
};

function createImageElement(url: string, callback: (img: HTMLElement) => void) {
const imageContainer = document.createElement('span');
imageContainer.contentEditable = 'false';
imageContainer.setAttribute('data-type', 'inline-container');
const timeoutMap = new Map<string, NodeJS.Timeout>();

const img = new Image();
imageContainer.appendChild(img);
function createImageElement(targetNode: TreeNode, url: string, callback: (img: HTMLElement, err?: string | Event) => void) {
if (timeoutMap.has(targetNode.orderIndex)) {
clearTimeout(timeoutMap.get(targetNode.orderIndex));
timeoutMap.delete(targetNode.orderIndex);
}

img.contentEditable = 'false';
img.onload = () => callback(imageContainer);
img.onerror = () => callback(imageContainer);
img.src = url;
const timeout = setTimeout(() => {
const imageContainer = document.createElement('span');
imageContainer.contentEditable = 'false';
imageContainer.setAttribute('data-type', 'inline-container');

const img = new Image();
imageContainer.appendChild(img);

img.contentEditable = 'false';
img.onload = () => callback(imageContainer);
img.onerror = (err) => callback(imageContainer, err);
img.src = url;
timeoutMap.delete(targetNode.orderIndex);
}, INLINE_IMAGE_PREVIEW_DEBOUNCE_TIME_MS);
timeoutMap.set(targetNode.orderIndex, timeout);
}

/** Adds already loaded image element from current input content to the tree node */
function updateImageTreeNode(targetNode: TreeNode, newElement: HTMLMarkdownElement) {
const paddingBottom = `${newElement.style.height}`;
targetNode.element.appendChild(newElement);
function updateImageTreeNode(targetNode: TreeNode, newElement: HTMLMarkdownElement, imageMarginTop = 0) {
const paddingBottom = `${parseStringWithUnitToNumber(newElement.style.height) + imageMarginTop}px`;
targetNode.element.appendChild(newElement.cloneNode(true));

let currentParent = targetNode.element;
while (currentParent.parentElement && !['line', 'block'].includes(currentParent.getAttribute('data-type') || '')) {
Expand All @@ -49,12 +62,16 @@ function addInlineImagePreview(currentInput: MarkdownTextInputElement, targetNod
imageHref = text.substring(linkRange.start, linkRange.start + linkRange.length);
}

const imageMarginTop = parseStringWithUnitToNumber(`${markdownStyle.inlineImage?.marginTop}`);
const imageMarginBottom = parseStringWithUnitToNumber(`${markdownStyle.inlineImage?.marginBottom}`);

// If the inline image markdown with the same href exists in the current input, use it instead of creating new one.
// Prevents from image flickering and layout jumps
const alreadyLoadedPreview = currentInput.querySelector(`img[src="${imageHref}"]`);
const loadedImageContainer = alreadyLoadedPreview?.parentElement;

if (loadedImageContainer && loadedImageContainer.getAttribute('data-type') === 'inline-container') {
return updateImageTreeNode(targetNode, loadedImageContainer as HTMLMarkdownElement);
return updateImageTreeNode(targetNode, loadedImageContainer as HTMLMarkdownElement, imageMarginTop);
}

// Add a loading spinner
Expand All @@ -63,47 +80,51 @@ function addInlineImagePreview(currentInput: MarkdownTextInputElement, targetNod
targetNode.element.appendChild(spinner);
}

const maxWidth = markdownStyle.inlineImage?.maxWidth;
const maxHeight = markdownStyle.inlineImage?.maxHeight;
const imageMarginTop = parseStringWithUnitToNumber(`${markdownStyle.inlineImage?.marginTop}`);
const imageMarginBottom = parseStringWithUnitToNumber(`${markdownStyle.inlineImage?.marginBottom}`);

Object.assign(targetNode.element.style, {
display: 'block',
marginBottom: `${imageMarginBottom}px`,
paddingBottom: markdownStyle.loadingIndicatorContainer?.height || markdownStyle.loadingIndicator?.height || (!!markdownStyle.loadingIndicator && '30px') || undefined,
});

createImageElement(imageHref, (imageContainer) => {
createImageElement(targetNode, imageHref, (imageContainer, err) => {
// 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
const currentSpinner = currentInput.querySelector('[data-type="spinner"]');
// Remove the spinner
if (currentSpinner) {
currentSpinner.remove();
}

const img = imageContainer.firstChild as HTMLImageElement;
const {minHeight, minWidth, maxHeight, maxWidth, borderRadius} = markdownStyle.inlineImage || {};
const imgStyle = {
minHeight,
minWidth,
maxHeight,
maxWidth,
borderRadius,
};

// Set the image styles
Object.assign(imageContainer.style, {
...inlineImageDefaultStyles,
maxHeight,
maxWidth,
...(err && {
...imgStyle,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}),
});

const img = imageContainer.firstChild as HTMLImageElement;
Object.assign(img.style, {
maxHeight,
maxWidth,
});
Object.assign(img.style, !err && imgStyle);

targetNode.element.appendChild(imageContainer);

const imageHeight = `${imageContainer.clientHeight + imageMarginTop}px`;
Object.assign(imageContainer.style, {
height: imageHeight,
height: `${imageContainer.clientHeight}px`,
});
// Set paddingBottom to the height of the image so it's displayed under the block
Object.assign(targetNode.element.style, {
paddingBottom: imageHeight,
paddingBottom: `${imageContainer.clientHeight + imageMarginTop}px`,
});
});

Expand Down

0 comments on commit 41af79a

Please sign in to comment.