From 7cace82b83a6ebb2bae29f4aab643502fef497ca Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Fri, 22 Jul 2022 09:12:23 -0700 Subject: [PATCH] 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 --- common/util/parse.ts | 2 + common/util/tiptap-iframe.ts | 92 ++++++++++++++++++++++++++++++++++++ web/components/editor.tsx | 18 +++++-- 3 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 common/util/tiptap-iframe.ts diff --git a/common/util/parse.ts b/common/util/parse.ts index 30dcb952..cdaa6a6c 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -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] diff --git a/common/util/tiptap-iframe.ts b/common/util/tiptap-iframe.ts new file mode 100644 index 00000000..5af63d2f --- /dev/null +++ b/common/util/tiptap-iframe.ts @@ -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 { + 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({ + 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 + }, + } + }, +}) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 7063fa42..d64dcc78 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -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 = /^$/.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 }, }, })