Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-image-resize-cached-image-hidden.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tiptap/extension-image': patch
---

Fix resizable images staying hidden when the image is cached or fails to load.
25 changes: 1 addition & 24 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
56 changes: 55 additions & 1 deletion packages/extension-image/__tests__/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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('')
})
})
})
18 changes: 15 additions & 3 deletions packages/extension-image/src/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,13 +278,25 @@ export const Image = Node.create<ImageOptions>({
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
}
},
Expand Down
Loading