Skip to content

Commit

Permalink
Fix image reloading when multiple images are inside the input
Browse files Browse the repository at this point in the history
  • Loading branch information
Skalakid committed Sep 19, 2024
1 parent 84676c9 commit 931fe90
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 66 deletions.
4 changes: 2 additions & 2 deletions src/MarkdownTextInput.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -659,14 +659,14 @@ const MarkdownTextInput = React.forwardRef<TextInput, MarkdownTextInputProps>(

useEffect(() => {
const handleReconnect = () => {
forceRefreshAllImages(divRef.current as MarkdownTextInputElement);
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
Expand Down
140 changes: 76 additions & 64 deletions src/web/inputElements/inlineImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,74 @@ function getImagePreviewElement(targetElement: HTMLMarkdownElement) {
return Array.from(targetElement?.childNodes || []).find((el) => (el as HTMLElement)?.contentEditable === 'false') as HTMLMarkdownElement | undefined;
}

function createImageElement(targetNode: TreeNode, url: string, callback: (img: HTMLElement, err?: string | Event) => void) {
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);

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)) {
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
Expand All @@ -46,8 +113,8 @@ 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);
Expand Down Expand Up @@ -105,78 +172,23 @@ function addInlineImagePreview(currentInput: MarkdownTextInputElement, targetNod
paddingBottom: markdownStyle.loadingIndicatorContainer?.height || markdownStyle.loadingIndicator?.height || (!!markdownStyle.loadingIndicator && '30px') || undefined,
});

createImageElement(targetNode, imageHref, (imageContainer, err) => {
let targetElement = targetNode.element;

// 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="${targetNode.orderIndex}"]`) as HTMLMarkdownElement;

const currentElementURL = getImagePreviewElement(currentElement)?.getAttribute('data-url');
const targetElementURL = getImagePreviewElement(targetNode.element)?.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);

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
Object.assign(targetElement.style, {
paddingBottom: `${imageClientHeight + imageMarginTop}px`,
});
});
createImageElement(currentInput, targetNode, imageHref, markdownStyle);

return targetNode;
}

function forceRefreshAllImages(currentInput: MarkdownTextInputElement) {
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 url = img.src;
// eslint-disable-next-line no-param-reassign
img.src = `${url}#`;
const imgElement = img;
imgElement.src = '';
imgElement.onload = () => handleOnLoad(currentInput, img.parentElement?.parentElement as HTMLMarkdownElement, url, markdownStyle, img.parentElement as HTMLMarkdownElement);
imgElement.src = `${url}#`;
});
}

Expand Down

0 comments on commit 931fe90

Please sign in to comment.