diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index d59d0a9d..db5b4bbd 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -659,14 +659,14 @@ const MarkdownTextInput = React.forwardRef( 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 diff --git a/src/web/inputElements/inlineImage.ts b/src/web/inputElements/inlineImage.ts index 3369c26f..e5ae4ccb 100644 --- a/src/web/inputElements/inlineImage.ts +++ b/src/web/inputElements/inlineImage.ts @@ -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 @@ -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); @@ -105,69 +172,12 @@ 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) { @@ -175,8 +185,10 @@ function forceRefreshAllImages(currentInput: MarkdownTextInputElement) { } 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}#`; }); }