Skip to content

Commit

Permalink
Inline images preview follow ups (#500)
Browse files Browse the repository at this point in the history
  • Loading branch information
Skalakid authored Sep 20, 2024
1 parent 2ae0aaf commit 8a1459e
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 48 deletions.
12 changes: 12 additions & 0 deletions src/MarkdownTextInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import './web/MarkdownTextInput.css';
import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent';
import {getElementHeight, getPlaceholderValue, isEventComposing, normalizeValue, parseInnerHTMLToText} from './web/utils/inputUtils';
import {parseToReactDOMStyle, processMarkdownStyle} from './web/utils/webStyleUtils';
import {forceRefreshAllImages} from './web/inputElements/inlineImage';

require('../parser/react-native-live-markdown-parser.js');

Expand Down Expand Up @@ -656,6 +657,17 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(
setCursorPosition(divRef.current, newSelection.start, newSelection.end);
}, [selection, updateRefSelectionVariables]);

useEffect(() => {
const handleReconnect = () => {
forceRefreshAllImages(divRef.current as MarkdownTextInputElement, processedMarkdownStyle);
};

window.addEventListener('online', handleReconnect);
return () => {
window.removeEventListener('online', handleReconnect);
};
}, [processedMarkdownStyle]);

return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
Expand Down
155 changes: 108 additions & 47 deletions src/web/inputElements/inlineImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,94 @@ const inlineImageDefaultStyles = {
left: 0,
};

const timeoutMap = new Map<string, NodeJS.Timeout>();
type DebouncePreviewItem = {
timeout: NodeJS.Timeout;
url: string;
};

const timeoutMap = new Map<string, DebouncePreviewItem>();

function getImagePreviewElement(targetElement: HTMLMarkdownElement) {
return Array.from(targetElement?.childNodes || []).find((el) => (el as HTMLElement)?.contentEditable === 'false') as HTMLMarkdownElement | undefined;
}

function handleOnLoad(
currentInput: MarkdownTextInputElement,
target: HTMLMarkdownElement,
imageHref: string,
markdownStyle: PartialMarkdownStyle,
imageContainer: HTMLSpanElement,
err?: string | Event,
) {
let targetElement = target;

// Update the target element if the input structure was changed while the image was loading and its content hasn't changed
if (!targetElement.isConnected) {
const currentElement = currentInput.querySelector(`[data-type="block"][data-id="${target.getAttribute('data-id')}"]`) as HTMLMarkdownElement;

const currentElementURL = getImagePreviewElement(currentElement)?.getAttribute('data-url');
const targetElementURL = getImagePreviewElement(targetElement)?.getAttribute('data-url');
if (currentElementURL && targetElementURL && currentElementURL === targetElementURL) {
targetElement = currentElement;
} else {
return; // Prevent adding expired image previews to the input structure
}
}

// 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"][data-url="${imageHref}"]`);

// 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,
...(err && {
...imgStyle,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}),
});

Object.assign(img.style, !err && imgStyle);

targetElement.appendChild(imageContainer);

function createImageElement(targetNode: TreeNode, url: string, callback: (img: HTMLElement, err?: string | Event) => void) {
const imageClientHeight = Math.max(img.clientHeight, imageContainer.clientHeight);
Object.assign(imageContainer.style, {
height: `${imageClientHeight}px`,
});
// Set paddingBottom to the height of the image so it's displayed under the block
const imageMarginTop = parseStringWithUnitToNumber(`${markdownStyle.inlineImage?.marginTop}`);
Object.assign(targetElement.style, {
paddingBottom: `${imageClientHeight + imageMarginTop}px`,
});
}

function createImageElement(currentInput: MarkdownTextInputElement, targetNode: TreeNode, url: string, markdownStyle: PartialMarkdownStyle) {
if (timeoutMap.has(targetNode.orderIndex)) {
clearTimeout(timeoutMap.get(targetNode.orderIndex));
const mapItem = timeoutMap.get(targetNode.orderIndex);
// Check if the image URL has been changed, if not, early return so the image can be loaded asynchronously
const currentElement = document.querySelector(`[data-type="block"][data-id="${targetNode.orderIndex}"]`) as HTMLMarkdownElement;
if (mapItem?.url === url && currentElement && getImagePreviewElement(currentElement)) {
return;
}

clearTimeout(mapItem?.timeout);
timeoutMap.delete(targetNode.orderIndex);
}

Expand All @@ -30,12 +113,15 @@ function createImageElement(targetNode: TreeNode, url: string, callback: (img: H
imageContainer.appendChild(img);

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

/** Adds already loaded image element from current input content to the tree node */
Expand Down Expand Up @@ -86,50 +172,25 @@ function addInlineImagePreview(currentInput: MarkdownTextInputElement, targetNod
paddingBottom: markdownStyle.loadingIndicatorContainer?.height || markdownStyle.loadingIndicator?.height || (!!markdownStyle.loadingIndicator && '30px') || undefined,
});

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();
createImageElement(currentInput, targetNode, imageHref, markdownStyle);

return targetNode;
}

function forceRefreshAllImages(currentInput: MarkdownTextInputElement, markdownStyle: PartialMarkdownStyle) {
currentInput.querySelectorAll('img').forEach((img) => {
// force image reload only if broken image icon is displayed
if (img.naturalWidth > 0) {
return;
}

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,
...(err && {
...imgStyle,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}),
});

Object.assign(img.style, !err && imgStyle);

targetNode.element.appendChild(imageContainer);

Object.assign(imageContainer.style, {
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: `${imageContainer.clientHeight + imageMarginTop}px`,
});
const url = img.src;
const imgElement = img;
imgElement.src = '';
imgElement.onload = () => handleOnLoad(currentInput, img.parentElement?.parentElement as HTMLMarkdownElement, url, markdownStyle, img.parentElement as HTMLMarkdownElement);
imgElement.src = `${url}#`;
});

return targetNode;
}

// eslint-disable-next-line import/prefer-default-export
export {addInlineImagePreview};
export {addInlineImagePreview, forceRefreshAllImages};
2 changes: 1 addition & 1 deletion src/web/utils/blockUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function addStyleToBlock(targetElement: HTMLElement, type: NodeType, markdownSty
}

const BLOCK_MARKDOWN_TYPES = ['inline-image'];
const FULL_LINE_MARKDOWN_TYPES = ['blockquote', 'h1'];
const FULL_LINE_MARKDOWN_TYPES = ['blockquote'];

function isBlockMarkdownType(type: NodeType) {
return BLOCK_MARKDOWN_TYPES.includes(type);
Expand Down

0 comments on commit 8a1459e

Please sign in to comment.