Skip to content

Commit

Permalink
feat(form/inputs): add support for image drop+paste in PTE input (#6534)
Browse files Browse the repository at this point in the history
* feat(form/inputs): add support for image drop+paste in PTE input

Signed-off-by: Fred Carlsen <fred@sjelfull.no>

* fixup! feat(form/inputs): add support for image drop+paste in PTE input

Signed-off-by: Fred Carlsen <fred@sjelfull.no>

* fixup! feat(form/inputs): add support for image drop+paste in PTE input

* fixup! feat(form/inputs): add support for image drop+paste in PTE input

Signed-off-by: Fred Carlsen <fred@sjelfull.no>

---------

Signed-off-by: Fred Carlsen <fred@sjelfull.no>
  • Loading branch information
sjelfull committed May 6, 2024
1 parent 48321e6 commit e964b1e
Show file tree
Hide file tree
Showing 12 changed files with 425 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {BellIcon, ColorWheelIcon, ImageIcon, InfoOutlineIcon} from '@sanity/icons'
import {BellIcon, ColorWheelIcon, DocumentPdfIcon, ImageIcon, InfoOutlineIcon} from '@sanity/icons'
import {type Rule} from '@sanity/types'
import {defineArrayMember, defineField, defineType} from 'sanity'

Expand Down Expand Up @@ -36,21 +36,21 @@ export const ptAllTheBellsAndWhistlesType = defineType({
marks: {
// decorators: [{title: 'Strong', value: 'strong'}],
annotations: [
{
defineField({
type: 'object',
name: 'link',
title: 'Link',
// options: {
// modal: {type: 'dialog'},
// },
fields: [
{
defineField({
type: 'url',
name: 'href',
title: 'URL',
validation: (rule: Rule) =>
validation: (rule) =>
rule
.custom((url: string, context: any) => {
.custom((url: string | undefined, context: any) => {
if (!url && !context.parent.reference) {
return 'Inline Link: Requires a reference or URL'
}
Expand All @@ -61,7 +61,7 @@ export const ptAllTheBellsAndWhistlesType = defineType({
scheme: ['http', 'https', 'mailto', 'tel'],
allowRelative: true,
}),
},
}),
defineField({
title: 'Linked Book',
name: 'reference',
Expand All @@ -77,8 +77,8 @@ export const ptAllTheBellsAndWhistlesType = defineType({
initialValue: false,
},
],
},
{
}),
defineField({
type: 'object',
name: 'color',
title: 'Color',
Expand All @@ -91,7 +91,7 @@ export const ptAllTheBellsAndWhistlesType = defineType({
validation: (rule: Rule) => rule.required(),
},
],
},
}),
],
},
of: [
Expand Down Expand Up @@ -120,6 +120,21 @@ export const ptAllTheBellsAndWhistlesType = defineType({
],
}),

defineField({
type: 'file',
icon: DocumentPdfIcon,
name: 'pdfFile',
title: 'PDF file',
options: {
accept: 'application/pdf',
},
preview: {
select: {
title: 'asset.originalFilename',
},
},
}),

defineField({
type: 'image',
icon: ImageIcon,
Expand Down Expand Up @@ -173,6 +188,29 @@ export const ptAllTheBellsAndWhistlesType = defineType({
],
}),

defineField({
name: 'imageObject',
title: 'Image object',
type: 'object',
icon: ImageIcon,
fields: [
defineField({
type: 'image',
icon: ImageIcon,
name: 'image',
title: 'Image',
options: {
hotspot: true,
},
preview: {
select: {
media: 'asset',
},
},
}),
],
}),

defineField({
name: 'infoBox',
icon: InfoOutlineIcon,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,16 @@ export function createWithInsertData(
change$.next({type: 'loading', isLoading: true}) // This could potentially take some time
const html = data.getData('text/html')
const text = data.getData('text/plain')
if (html || text) {

const {files} = data
const hasFiles = files && files.length > 0

if (hasFiles) {
const plural = files.length === 1 ? 'file' : 'files'
debug(`Inserting ${plural}`, data)
}

if (!hasFiles && (html || text)) {
debug('Inserting data', data)
let portableText: PortableTextBlock[]
let fragment: Node[]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable max-nested-callbacks */
import path from 'node:path'

import {expect, test} from '@playwright/experimental-ct-react'
import {type Path, type SanityDocument} from '@sanity/types'

Expand Down Expand Up @@ -101,4 +103,54 @@ test.describe('Portable Text Input', () => {
await expect(bodyLength).toEqual(snapshotLength)
})
})

test.describe('Should be able to paste files into the PTE', () => {
test(`Added pasted image as a block`, async ({browserName, mount, page}) => {
test.skip(browserName === 'firefox', 'Currently not working in Firefox')
const {getFocusedPortableTextEditor, pasteFileOverPortableTextEditor} = testHelpers({page})

await mount(<CopyPasteStory document={document} />)

const imagePath = path.resolve(__dirname, 'static', 'dummy-image-1.jpg')
const $pte = await getFocusedPortableTextEditor('field-body')

await pasteFileOverPortableTextEditor(imagePath, 'image/jpeg', $pte)

await expect($pte.getByTestId('block-preview')).toBeVisible()
})
test(`Added dropped image as a block`, async ({mount, page}) => {
const {
getFocusedPortableTextEditor,
dropFileOverPortableTextEditor,
hoverFileOverPortableTextEditor,
} = testHelpers({page})

await mount(<CopyPasteStory document={document} />)

const imagePath = path.resolve(__dirname, 'static', 'dummy-image-1.jpg')
const $pte = await getFocusedPortableTextEditor('field-body')

await hoverFileOverPortableTextEditor(imagePath, 'image/jpeg', $pte)

await expect(page.getByText('Drop to upload 1 file')).toBeVisible()

await dropFileOverPortableTextEditor(imagePath, 'image/jpeg', $pte)

await expect(page.getByText('Drop to upload 1 file')).not.toBeVisible()

await expect($pte.getByTestId('block-preview')).toBeVisible()
})
test(`Display error message on drag over if file is not accepted`, async ({mount, page}) => {
const {getFocusedPortableTextEditor, hoverFileOverPortableTextEditor} = testHelpers({page})

await mount(<CopyPasteStory document={document} />)

const zipPath = path.resolve(__dirname, 'static', 'dummy.zip')
const $pte = await getFocusedPortableTextEditor('field-body')

await hoverFileOverPortableTextEditor(zipPath, 'application/zip', $pte)

await expect(page.getByText(`upload this file here`)).toBeVisible()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,36 @@ const SCHEMA_TYPES = [
unstable_whitespaceOnPasteMode: 'remove',
},
}),
defineArrayMember({
type: 'image',
name: 'image',
title: 'Image block',
preview: {
select: {
fileName: 'asset.originalFilename',
image: 'asset',
},
prepare({fileName, image}) {
return {
media: image,
title: fileName,
}
},
},
}),
defineArrayMember({
type: 'file',
name: 'filePDF',
title: 'PDF file block',
options: {
accept: 'application/pdf',
},
preview: {
select: {
tile: 'asset.originalFilename',
},
},
}),
],
}),
defineField({
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
117 changes: 117 additions & 0 deletions packages/sanity/playwright-ct/tests/utils/testHelpers.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import {readFileSync} from 'node:fs'
import path from 'node:path'

import {type Locator, type PlaywrightTestArgs} from '@playwright/test'

export const DEFAULT_TYPE_DELAY = 20
Expand Down Expand Up @@ -118,6 +121,120 @@ export function testHelpers({page}: {page: PlaywrightTestArgs['page']}) {

await locator.getByText(firstTextContent).waitFor()
},
/**
* Emulate dragging a file over an focused Portable Text Editor's editable element
* @param text - The string to be pasted.
* @param locator - editable element of a Portable Text Editor (as returned by getFocusedPortableTextEditorElement)
*/
hoverFileOverPortableTextEditor: async (
filePath: string,
fileType: string,
locator: Locator,
) => {
const fileName = path.basename(filePath)
const buffer = readFileSync(filePath).toString('base64')

await locator.focus()
await locator.evaluate(
async (el, {bufferData, localFileName, localFileType}) => {
const response = await fetch(bufferData)
const blob = await response.blob()

const image = new File([blob], localFileName, {type: localFileType})

const dataTransfer = new DataTransfer()
dataTransfer.items.add(image)

el.dispatchEvent(
new DragEvent('dragenter', {
dataTransfer,
bubbles: true,
}),
)
},
{
bufferData: `data:application/octet-stream;base64,${buffer}`,
localFileName: fileName,
localFileType: fileType,
},
)
},
/**
* Emulate dropping a file over an focused Portable Text Editor's editable element
* @param filePath - Absolute path to the file to be dropped.
* @param fileType - Mime type of the file to be dropped.
* @param locator - editable element of a Portable Text Editor (as returned by getFocusedPortableTextEditorElement)
*/
dropFileOverPortableTextEditor: async (
imagePath: string,
fileType: string,
locator: Locator,
) => {
const fileName = path.basename(imagePath)
const buffer = readFileSync(imagePath).toString('base64')

await locator.focus()
await locator.evaluate(
async (el, {bufferData, localFileName, localFileType}) => {
const response = await fetch(bufferData)
const blob = await response.blob()
const image = new File([blob], localFileName, {type: localFileType})

const dataTransfer = new DataTransfer()
dataTransfer.items.add(image)

el.dispatchEvent(
new DragEvent('drop', {
dataTransfer,
bubbles: true,
}),
)
},
{
bufferData: `data:application/octet-stream;base64,${buffer}`,
localFileName: fileName,
localFileType: fileType,
},
)
},
/**
* Emulate pasting a file over an focused Portable Text Editor's editable element
* @param filePath - Absolute path to the file to be pasted.
* @param fileType - Mime type of the file to be pasted.
* @param locator - editable element of a Portable Text Editor (as returned by getFocusedPortableTextEditorElement)
*/
pasteFileOverPortableTextEditor: async (
filePath: string,
fileType: string,
locator: Locator,
) => {
const fileName = path.basename(filePath)
const buffer = readFileSync(filePath).toString('base64')

await locator.focus()
await locator.evaluate(
async (el, {bufferData, localFileName, localFileType}) => {
const response = await fetch(bufferData)
const blob = await response.blob()
const image = new File([blob], localFileName, {type: localFileType})

const dataTransfer = new DataTransfer()
dataTransfer.items.add(image)

el.dispatchEvent(
new ClipboardEvent('paste', {
clipboardData: dataTransfer,
bubbles: true,
}),
)
},
{
bufferData: `data:application/octet-stream;base64,${buffer}`,
localFileName: fileName,
localFileType: fileType,
},
)
},
/**
* Will create a keyboard event of a given hotkey combination that can be activated with a modifier key
* @param hotkey - the hotkey
Expand Down
Loading

0 comments on commit e964b1e

Please sign in to comment.