From 71880dfc989d406060f113b7991e43569f83de2c Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Sat, 23 Jul 2022 09:19:49 -0700 Subject: [PATCH] Add a toolbar for images and iframes (#688) * Add a toolbar for images and iframes * Insert embed code via modal --- web/components/editor.tsx | 122 +++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 7 deletions(-) diff --git a/web/components/editor.tsx b/web/components/editor.tsx index d64dcc78..4dfddac9 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -12,7 +12,7 @@ import StarterKit from '@tiptap/starter-kit' import { Image } from '@tiptap/extension-image' import { Link } from '@tiptap/extension-link' import clsx from 'clsx' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { Linkify } from './linkify' import { uploadImage } from 'web/lib/firebase/storage' import { useMutation } from 'react-query' @@ -20,6 +20,12 @@ import { exhibitExts } from 'common/util/parse' import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' import Iframe from 'common/util/tiptap-iframe' +import { CodeIcon, PhotographIcon } from '@heroicons/react/solid' +import { Modal } from './layout/modal' +import { Col } from './layout/col' +import { Button } from './button' +import { Row } from './layout/row' +import { Spacer } from './layout/spacer' const proseClass = clsx( 'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed', @@ -36,7 +42,7 @@ export function useTextEditor(props: { const editorClass = clsx( proseClass, - 'box-content min-h-[6em] textarea textarea-bordered text-base' + 'min-h-[6em] resize-none outline-none border-none pt-3 px-4 focus:ring-0' ) const editor = useEditor({ @@ -78,8 +84,7 @@ export function useTextEditor(props: { // If the pasted content is iframe code, directly inject it const text = event.clipboardData?.getData('text/plain').trim() ?? '' - const isValidIframe = /^$/.test(text) - if (isValidIframe) { + if (isValidIframe(text)) { editor.chain().insertContent(text).run() return true // Prevent the code from getting pasted as text } @@ -96,16 +101,21 @@ export function useTextEditor(props: { return { editor, upload } } +function isValidIframe(text: string) { + return /^$/.test(text) +} + export function TextEditor(props: { editor: Editor | null upload: ReturnType }) { const { editor, upload } = props + const [iframeOpen, setIframeOpen] = useState(false) return ( <> {/* hide placeholder when focused */} -
+
{editor && ( )} - +
+ + {/* Spacer element to match the height of the toolbar */} + + + {/* Toolbar, with buttons for image and embeds */} +
+
+
+ + +
+
+ +
+
+
{upload.isLoading && Uploading image...} {upload.isError && ( @@ -131,6 +180,65 @@ export function TextEditor(props: { ) } +function IframeModal(props: { + editor: Editor | null + open: boolean + setOpen: (open: boolean) => void +}) { + const { editor, open, setOpen } = props + const [embedCode, setEmbedCode] = useState('') + const valid = isValidIframe(embedCode) + + return ( + + + + setEmbedCode(e.target.value)} + /> + + {/* Preview the embed if it's valid */} + {valid ? : } + + + + + + + + ) +} + const useUploadMutation = (editor: Editor | null) => useMutation( (files: File[]) => @@ -149,7 +257,7 @@ const useUploadMutation = (editor: Editor | null) => } ) -function RichContent(props: { content: JSONContent }) { +function RichContent(props: { content: JSONContent | string }) { const { content } = props const editor = useEditor({ editorProps: { attributes: { class: proseClass } },