diff --git a/.changeset/fix-image-resize-cached-image-hidden.md b/.changeset/fix-image-resize-cached-image-hidden.md new file mode 100644 index 0000000000..64561689a3 --- /dev/null +++ b/.changeset/fix-image-resize-cached-image-hidden.md @@ -0,0 +1,5 @@ +--- +'@tiptap/extension-image': patch +--- + +Fix resizable images staying hidden when the image is cached or fails to load. diff --git a/package.json b/package.json index 878d7d4e89..d5ff8d7352 100644 --- a/package.json +++ b/package.json @@ -109,28 +109,5 @@ "engines": { "node": ">=24" }, - "packageManager": "pnpm@11.2.2", - "pnpm": { - "patchedDependencies": { - "@changesets/assemble-release-plan@6.0.5": "patches/@changesets__assemble-release-plan.patch", - "@changesets/assemble-release-plan@6.0.9": "patches/@changesets__assemble-release-plan@6.0.9.patch" - }, - "overrides": { - "@rollup/pluginutils>picomatch": "2.3.2", - "prosemirror-model": "^1.25.11", - "@octokit/action>undici": "6.24.1", - "anymatch>picomatch": "2.3.2", - "glob": "^10.5.0", - "micromatch>picomatch": "2.3.2", - "minimatch@^3.0.5": "3.1.4", - "minimatch@^3.1.2": "3.1.4", - "qs": "^6.14.1", - "rollup": "4.60.1", - "readdirp>picomatch": "2.3.2", - "sass>immutable": "5.1.5", - "terser-webpack-plugin>serialize-javascript": "7.0.5", - "tinyglobby>picomatch": "4.0.4", - "vitest>picomatch": "4.0.4" - } - } + "packageManager": "pnpm@11.2.2" } diff --git a/packages/extension-image/__tests__/image.spec.ts b/packages/extension-image/__tests__/image.spec.ts index 78c25f376c..f2875452ea 100644 --- a/packages/extension-image/__tests__/image.spec.ts +++ b/packages/extension-image/__tests__/image.spec.ts @@ -3,7 +3,7 @@ import Document from '@tiptap/extension-document' import Paragraph from '@tiptap/extension-paragraph' import Text from '@tiptap/extension-text' import { Markdown } from '@tiptap/markdown' -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import Image from '../src/index.js' @@ -499,4 +499,58 @@ describe('extension-image', () => { expect(img?.hasAttribute('data-custom-id')).toBe(false) }) }) + + describe('resizable image visibility', () => { + const setImageLoaded = (loaded: boolean) => { + vi.spyOn(HTMLImageElement.prototype, 'complete', 'get').mockReturnValue(loaded) + vi.spyOn(HTMLImageElement.prototype, 'naturalWidth', 'get').mockReturnValue(loaded ? 100 : 0) + } + + const mountResizableImage = () => { + editor = new Editor({ + element: createEditorEl(), + extensions: [Document, Paragraph, Text, Image.configure({ resize: { enabled: true } })], + content: { + type: 'doc', + content: [{ type: 'image', attrs: { src: imgSrc } }], + }, + }) + + return editor.view.dom.querySelector('[data-resize-container]') as HTMLElement + } + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('should show the node view when the image is already loaded on mount', () => { + // A cached image is already complete before the node view mounts, so + // `load` never fires again and the image would stay hidden forever. + setImageLoaded(true) + + expect(mountResizableImage().style.visibility).toBe('') + }) + + it('should hide the node view until a not-yet-loaded image fires load', () => { + setImageLoaded(false) + + const container = mountResizableImage() + + expect(container.style.visibility).toBe('hidden') + + container.querySelector('img')?.dispatchEvent(new Event('load')) + + expect(container.style.visibility).toBe('') + }) + + it('should show the node view when the image fails to load', () => { + setImageLoaded(false) + + const container = mountResizableImage() + + container.querySelector('img')?.dispatchEvent(new Event('error')) + + expect(container.style.visibility).toBe('') + }) + }) }) diff --git a/packages/extension-image/src/image.ts b/packages/extension-image/src/image.ts index ba2048a8e7..3894d3cb9b 100644 --- a/packages/extension-image/src/image.ts +++ b/packages/extension-image/src/image.ts @@ -278,13 +278,25 @@ export const Image = Node.create({ const dom = nodeView.dom as HTMLElement // when image is loaded, show the node view to get the correct dimensions - dom.style.visibility = 'hidden' - dom.style.pointerEvents = 'none' - el.onload = () => { + const showNodeView = () => { dom.style.visibility = '' dom.style.pointerEvents = '' } + dom.style.visibility = 'hidden' + dom.style.pointerEvents = 'none' + + if (el.complete && el.naturalWidth > 0) { + // A cached image is already complete before this node view mounts, so + // `load` never fires again and the image would stay hidden forever. + showNodeView() + } else { + el.onload = showNodeView + // Reveal on error too, so a broken src renders as an inspectable broken + // image instead of an invisible node. + el.onerror = showNodeView + } + return nodeView } },