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
25 changes: 25 additions & 0 deletions docs/api/component-image-block.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ You can configure the component by updating the `imageBlockConfig` ctx in `edito
| `captionPlaceholderText` | `string` | `'Image caption'` | Placeholder text for the caption input |
| `onUpload` | `(file: File) => Promise<string>` | `(file) => Promise.resolve(URL.createObjectURL(file))` | Function called when an image is uploaded; must return a Promise with the image URL |
| `proxyDomURL` | `(url: string) => Promise<string> \| string` | `undefined` | Optional function to proxy the image URL |
| `onImageLoadError` | `(event: Event) => void \| Promise<void>` | `undefined` | Optional callback when an image fails to load (e.g. invalid URL or network error) |

---

Expand Down Expand Up @@ -112,3 +113,27 @@ ctx.update(imageBlockConfig.key, (defaultConfig) => ({
},
}))
```

## `onImageLoadError`

Optional callback invoked when an image fails to load (invalid URL, CORS, 404, etc.). Use it to show a message, fallback UI, or report errors. May be sync or async (`Promise<void>`).

```typescript
import { imageBlockConfig } from '@milkdown/components/image-block'

ctx.update(imageBlockConfig.key, (defaultConfig) => ({
...defaultConfig,
onImageLoadError: (event: Event) => {
console.error('Image failed to load', event)
// e.g. show toast or replace with placeholder
},
}))

// Async is also supported
ctx.update(imageBlockConfig.key, (defaultConfig) => ({
...defaultConfig,
onImageLoadError: async (event: Event) => {
await reportToAnalytics('image_load_error', event)
},
}))
```
8 changes: 6 additions & 2 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@
"src"
],
"scripts": {
"build": "rollup -c"
"build": "rollup -c",
"test": "vitest run"
},
"peerDependencies": {
"@codemirror/language": "^6",
Expand Down Expand Up @@ -127,6 +128,9 @@
"devDependencies": {
"@codemirror/language": "^6.10.1",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.26.0"
"@codemirror/view": "^6.26.0",
"@testing-library/vue": "^8.1.0",
"jsdom": "^28.0.0",
"vitest": "^4.0.0"
}
}
7 changes: 6 additions & 1 deletion packages/components/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ const dirs = fs.readdirSync(path.resolve(dirname, './src'))

export default () =>
dirs
.filter((x) => x !== '__internal__' && !x.includes('index'))
.filter(
(x) =>
x !== '__internal__' &&
x !== '__tests__' &&
!x.includes('index')
)
.flatMap(componentModule)
.concat(main)
23 changes: 20 additions & 3 deletions packages/components/src/__internal__/components/image-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { Icon } from './icon'

h

Check warning on line 7 in packages/components/src/__internal__/components/image-input.tsx

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unused-expressions)

Expected expression to be used

const nanoid = customAlphabet('abcdefg', 8)

Expand All @@ -22,6 +22,7 @@
className?: string

onUpload: (file: File) => Promise<string>
onImageLoadError?: (event: Event) => void | Promise<void>
}

export const ImageInput = defineComponent<ImageInputProps>({
Expand Down Expand Up @@ -62,6 +63,10 @@
type: Function,
required: true,
},
onImageLoadError: {
type: Function,
required: false,
},
},
setup({
readonly,
Expand All @@ -73,6 +78,7 @@
confirmButton,
uploadPlaceholderText,
className,
onImageLoadError,
}) {
const focusLinkInput = ref(false)
const linkInputRef = ref<HTMLInputElement>()
Expand Down Expand Up @@ -153,9 +159,20 @@
)}
</div>
{currentLink.value && (
<div class="confirm" onClick={() => onConfirmLinkInput()}>
<Icon icon={confirmButton} />
</div>
<>
<div class="image-preview">
<img
src={currentLink.value}
alt=""
onError={(e) =>
Promise.resolve(onImageLoadError?.(e)).catch(() => {})
}
/>
</div>
<div class="confirm" onClick={() => onConfirmLinkInput()}>
<Icon icon={confirmButton} />
</div>
</>
)}
</div>
)
Expand Down
6 changes: 6 additions & 0 deletions packages/components/src/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { cleanup } from '@testing-library/vue'
import { afterEach } from 'vitest'

afterEach(() => {
cleanup()
})
1 change: 1 addition & 0 deletions packages/components/src/image-block/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ImageBlockConfig {
captionPlaceholderText: string
onUpload: (file: File) => Promise<string>
proxyDomURL?: (url: string) => Promise<string> | string
onImageLoadError?: (event: Event) => void | Promise<void>
}

export const defaultImageBlockConfig: ImageBlockConfig = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { render } from '@testing-library/vue'
import { expect, test, vi } from 'vitest'
import { ref } from 'vue'

import { ImageViewer } from '../image-viewer'

test('calls onImageLoadError when img fires error', async () => {
const onImageLoadError = vi.fn()
const config = {
onImageLoadError,
captionIcon: '💬',
captionPlaceholderText: 'Image caption',
imageIcon: '🖼️',
uploadButton: 'Upload',
confirmButton: 'Confirm',
uploadPlaceholderText: 'or paste an image URL',
onUpload: async () => 'https://example.com/photo.png',
}

// render the ImageViewer component
const { container } = render(ImageViewer, {
props: {
src: ref('https://example.com/photo.png'),
caption: ref(''),
ratio: ref(1),
selected: ref(false),
readonly: ref(false),
setAttr: () => {},
config,
},
})

const img = container.querySelector('img[data-type="image-block"]')
expect(img).toBeTruthy()

// simulate the image load error
img!.dispatchEvent(new Event('error'))

// expect the onImageLoadError function to have been called
expect(onImageLoadError).toHaveBeenCalledTimes(1)
expect(onImageLoadError).toHaveBeenCalledWith(expect.any(Event))
})
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import { ImageInput } from '../../../__internal__/components/image-input'
import { ImageViewer } from './image-viewer'

h

Check warning on line 8 in packages/components/src/image-block/view/components/image-block.tsx

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unused-expressions)

Expected expression to be used

Check warning on line 8 in packages/components/src/image-block/view/components/image-block.tsx

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unused-expressions)

Expected expression to be used
Fragment

Check warning on line 9 in packages/components/src/image-block/view/components/image-block.tsx

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unused-expressions)

Expected expression to be used

Check warning on line 9 in packages/components/src/image-block/view/components/image-block.tsx

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unused-expressions)

Expected expression to be used

type Attrs = {
src: string
Expand Down Expand Up @@ -70,6 +70,7 @@
confirmButton={props.config.confirmButton}
uploadPlaceholderText={props.config.uploadPlaceholderText}
onUpload={props.config.onUpload}
onImageLoadError={props.config.onImageLoadError}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import { Icon } from '../../../__internal__/components/icon'
import { IMAGE_DATA_TYPE } from '../../schema'

h

Check warning on line 8 in packages/components/src/image-block/view/components/image-viewer.tsx

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unused-expressions)

Expected expression to be used

Check warning on line 8 in packages/components/src/image-block/view/components/image-viewer.tsx

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unused-expressions)

Expected expression to be used
Fragment

Check warning on line 9 in packages/components/src/image-block/view/components/image-viewer.tsx

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unused-expressions)

Expected expression to be used

Check warning on line 9 in packages/components/src/image-block/view/components/image-viewer.tsx

View workflow job for this annotation

GitHub Actions / lint

eslint(no-unused-expressions)

Expected expression to be used

export const ImageViewer = defineComponent<MilkdownImageBlockProps>({
props: {
Expand Down Expand Up @@ -143,6 +143,9 @@
onLoad={onImageLoad}
src={src.value}
alt={caption.value}
onError={(e) =>
Promise.resolve(config.onImageLoadError?.(e)).catch(() => {})
}
/>
<div
ref={resizeHandle}
Expand Down
9 changes: 9 additions & 0 deletions packages/components/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/__tests__/setup.ts'],
},
})
2 changes: 2 additions & 0 deletions packages/crepe/src/feature/image-block/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface ImageBlockConfig {
blockCaptionPlaceholderText: string
blockUploadPlaceholderText: string
blockOnUpload: (file: File) => Promise<string>
onImageLoadError: (event: Event) => void | Promise<void>
}

export type ImageBlockFeatureConfig = Partial<ImageBlockConfig>
Expand Down Expand Up @@ -61,6 +62,7 @@ export const imageBlock: DefineFeature<ImageBlockFeatureConfig> = (
config?.blockUploadPlaceholderText ?? 'or paste link',
onUpload: config?.blockOnUpload ?? config?.onUpload ?? value.onUpload,
proxyDomURL: config?.proxyDomURL,
onImageLoadError: config?.onImageLoadError ?? value.onImageLoadError,
}))
})
.use(imageBlockComponent)
Expand Down
Loading
Loading