Render iframes inside the rich text editor (#682)
* Try embedding iframes in tiptap * When iframe code is pasted, inject it into the editor * Code cleanups and comments * Remove clsx dependency Cuz it doesn't exist in `common` anyways * Rename to tiptap-iframe
This commit is contained in:
parent
87170894e2
commit
7cace82b83
|
@ -20,6 +20,7 @@ import { Text } from '@tiptap/extension-text'
|
|||
// other tiptap extensions
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Link } from '@tiptap/extension-link'
|
||||
import Iframe from './tiptap-iframe'
|
||||
|
||||
export function parseTags(text: string) {
|
||||
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi
|
||||
|
@ -80,6 +81,7 @@ export const exhibitExts = [
|
|||
|
||||
Image,
|
||||
Link,
|
||||
Iframe,
|
||||
]
|
||||
// export const exhibitExts = [StarterKit as unknown as Extension, Image]
|
||||
|
||||
|
|
92
common/util/tiptap-iframe.ts
Normal file
92
common/util/tiptap-iframe.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
// Adopted from https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/iframe.ts
|
||||
|
||||
import { Node } from '@tiptap/core'
|
||||
|
||||
export interface IframeOptions {
|
||||
allowFullscreen: boolean
|
||||
HTMLAttributes: {
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
iframe: {
|
||||
setIframe: (options: { src: string }) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// These classes style the outer wrapper and the inner iframe;
|
||||
// Adopted from css in https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/index.vue
|
||||
const wrapperClasses = 'relative h-auto w-full overflow-hidden'
|
||||
const iframeClasses = 'absolute top-0 left-0 h-full w-full'
|
||||
|
||||
export default Node.create<IframeOptions>({
|
||||
name: 'iframe',
|
||||
|
||||
group: 'block',
|
||||
|
||||
atom: true,
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
allowFullscreen: true,
|
||||
HTMLAttributes: {
|
||||
class: 'iframe-wrapper' + ' ' + wrapperClasses,
|
||||
// Tailwind JIT doesn't seem to pick up `pb-[20rem]`, so we hack this in:
|
||||
style: 'padding-bottom: 20rem;',
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
},
|
||||
frameborder: {
|
||||
default: 0,
|
||||
},
|
||||
allowfullscreen: {
|
||||
default: this.options.allowFullscreen,
|
||||
parseHTML: () => this.options.allowFullscreen,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [{ tag: 'iframe' }]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
'div',
|
||||
this.options.HTMLAttributes,
|
||||
[
|
||||
'iframe',
|
||||
{
|
||||
...HTMLAttributes,
|
||||
class: HTMLAttributes.class + ' ' + iframeClasses,
|
||||
},
|
||||
],
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setIframe:
|
||||
(options: { src: string }) =>
|
||||
({ tr, dispatch }) => {
|
||||
const { selection } = tr
|
||||
const node = this.type.create(options)
|
||||
|
||||
if (dispatch) {
|
||||
tr.replaceRangeWith(selection.from, selection.to, node)
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
|
@ -19,6 +19,7 @@ import { useMutation } from 'react-query'
|
|||
import { exhibitExts } from 'common/util/parse'
|
||||
import { FileUploadButton } from './file-upload-button'
|
||||
import { linkClass } from './site-link'
|
||||
import Iframe from 'common/util/tiptap-iframe'
|
||||
|
||||
const proseClass = clsx(
|
||||
'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed',
|
||||
|
@ -56,6 +57,7 @@ export function useTextEditor(props: {
|
|||
class: clsx('no-underline !text-indigo-700', linkClass),
|
||||
},
|
||||
}),
|
||||
Iframe,
|
||||
],
|
||||
content: defaultValue,
|
||||
})
|
||||
|
@ -69,12 +71,20 @@ export function useTextEditor(props: {
|
|||
(file) => file.type.startsWith('image')
|
||||
)
|
||||
|
||||
if (!imageFiles.length) {
|
||||
return // if no files pasted, use default paste handler
|
||||
if (imageFiles.length) {
|
||||
event.preventDefault()
|
||||
upload.mutate(imageFiles)
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
upload.mutate(imageFiles)
|
||||
// If the pasted content is iframe code, directly inject it
|
||||
const text = event.clipboardData?.getData('text/plain').trim() ?? ''
|
||||
const isValidIframe = /^<iframe.*<\/iframe>$/.test(text)
|
||||
if (isValidIframe) {
|
||||
editor.chain().insertContent(text).run()
|
||||
return true // Prevent the code from getting pasted as text
|
||||
}
|
||||
|
||||
return // Otherwise, use default paste handler
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue
Block a user