feat: media picker and image removal

This commit is contained in:
fsylvain committed 2026-10-01 05:04:58 +02:00
1 parent 008b0dfbb9
commit b3793d5dbb
54 files changed
+3769 -1412

No files matched your search

@@ -61,6 +61,23 @@ describe('Selecting an inserted image reveals its delete button', () => {
})
})
describe('The remove button is labelled and neutral', () => {
it('reads "Remove image" with a trash icon and is not styled as destructive', async () => {
const { wrapper, getEditor } = mountHarness()
activeWrapper = wrapper
const editor = getEditor()
editor.commands.setArticleImage({ src: 'photo.png', alt: '', imageId: 10 })
editor.commands.setNodeSelection(findArticleImagePos(editor, 10) as number)
await flush()
const button = deleteButton()!
expect(button.textContent?.trim()).toBe('Remove image')
expect(button.className).not.toContain('text-error')
expect(button.className).toContain('border-accented')
})
})
describe('An image with no data-image-id gets no delete affordance', () => {
it('renders no delete button even when the node is selected', async () => {
const { wrapper, getEditor } = mountHarness()
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { bodyImageIds } from '../bodyImageIds'
describe('bodyImageIds', () => {
it('lists the ids of the images that carry data-image-id', () => {
const body =
'<p>a</p><img src="/a.jpg" alt="" data-image-id="1"><p>b</p><img data-image-id="4" src="/b.svg">'
expect(bodyImageIds(body)).toEqual([1, 4])
})
it('ignores images without an id and non numeric ids', () => {
const body =
'<img src="/a.jpg"><img src="/b.jpg" data-image-id="abc"><img data-image-id="">'
expect(bodyImageIds(body)).toEqual([])
})
it('returns nothing for a body without images', () => {
expect(bodyImageIds('<p>Hello</p>')).toEqual([])
})
})
+13
View File
@@ -0,0 +1,13 @@
const IMAGE_ID_SELECTOR = 'img[data-image-id]'
const IMAGE_ID_ATTRIBUTE = 'data-image-id'
export function bodyImageCount(html: string): number {
return new DOMParser().parseFromString(html, 'text/html').querySelectorAll('img').length
}
export function bodyImageIds(html: string): number[] {
const document = new DOMParser().parseFromString(html, 'text/html')
return Array.from(document.querySelectorAll(IMAGE_ID_SELECTOR))
.map((image) => Number.parseInt(image.getAttribute(IMAGE_ID_ATTRIBUTE) ?? '', 10))
.filter((id) => !Number.isNaN(id))
}
+8
View File
@@ -76,3 +76,11 @@ export function joinNames(names: string[]) {
}
return `${names.slice(0, -1).join(', ')} and ${names[names.length - 1]}`
}
export function libraryMatchText(
filename: string,
existing: { created_at: string; uploaded_by: string },
) {
const by = existing.uploaded_by ? ` by ${existing.uploaded_by}` : ''
return `${filename} matches an image uploaded on ${formatMediaDate(existing.created_at)}${by}.`
}
+2
View File
@@ -0,0 +1,2 @@
export const UNDERLINE_TAB_TRIGGER =
'h-9 px-0 py-0 data-[state=inactive]:text-toned in-[[data-slot=list]:not(:has([data-slot=indicator]))]:data-[state=active]:after:bottom-0 in-[[data-slot=list]:not(:has([data-slot=indicator]))]:data-[state=active]:after:h-0.5 max-md:h-11 max-md:text-[0.9375rem]'