Use a single place to embed iframe, Youtube, and Tweets

This commit is contained in:
Austin Chen 2022-08-11 10:31:17 -07:00
parent 6423833ea4
commit f688d3c5d3
2 changed files with 48 additions and 107 deletions

View File

@ -23,12 +23,7 @@ import { DisplayMention } from './editor/mention'
import Iframe from 'common/util/tiptap-iframe' import Iframe from 'common/util/tiptap-iframe'
import TiptapTweet from 'common/util/tiptap-tweet' import TiptapTweet from 'common/util/tiptap-tweet'
import { CodeIcon, PhotographIcon } from '@heroicons/react/solid' import { CodeIcon, PhotographIcon } from '@heroicons/react/solid'
import { Modal } from './layout/modal' import { EmbedModal } from './editor/embed-modal'
import { Col } from './layout/col'
import { Button } from './button'
import { Row } from './layout/row'
import { Spacer } from './layout/spacer'
import { TweetModal } from './editor/tweetModal'
const DisplayImage = Image.configure({ const DisplayImage = Image.configure({
HTMLAttributes: { HTMLAttributes: {
@ -128,13 +123,6 @@ function isValidIframe(text: string) {
return /^<iframe.*<\/iframe>$/.test(text) return /^<iframe.*<\/iframe>$/.test(text)
} }
function isValidUrl(text: string) {
// Conjured by Codex, not sure if it's actually good
return /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/.test(
text
)
}
export function TextEditor(props: { export function TextEditor(props: {
editor: Editor | null editor: Editor | null
upload: ReturnType<typeof useUploadMutation> upload: ReturnType<typeof useUploadMutation>
@ -142,7 +130,6 @@ export function TextEditor(props: {
}) { }) {
const { editor, upload, children } = props const { editor, upload, children } = props
const [iframeOpen, setIframeOpen] = useState(false) const [iframeOpen, setIframeOpen] = useState(false)
const [tweetOpen, setTweetOpen] = useState(false)
return ( return (
<> <>
@ -167,7 +154,7 @@ export function TextEditor(props: {
onClick={() => setIframeOpen(true)} onClick={() => setIframeOpen(true)}
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500" className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
> >
<IframeModal <EmbedModal
editor={editor} editor={editor}
open={iframeOpen} open={iframeOpen}
setOpen={setIframeOpen} setOpen={setIframeOpen}
@ -176,26 +163,6 @@ export function TextEditor(props: {
<span className="sr-only">Embed an iframe</span> <span className="sr-only">Embed an iframe</span>
</button> </button>
</div> </div>
<div className="flex items-center">
<button
type="button"
onClick={() => setTweetOpen(true)}
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
>
<TweetModal
editor={editor}
open={tweetOpen}
setOpen={setTweetOpen}
/>
<img
className="mr-2"
src={'/twitter-logo.svg'}
width={15}
height={15}
/>
<span className="sr-only">Embed a tweet</span>
</button>
</div>
{/* Spacer that also focuses editor on click */} {/* Spacer that also focuses editor on click */}
<div <div
className="grow cursor-text self-stretch" className="grow cursor-text self-stretch"
@ -216,66 +183,6 @@ export function TextEditor(props: {
) )
} }
function IframeModal(props: {
editor: Editor | null
open: boolean
setOpen: (open: boolean) => void
}) {
const { editor, open, setOpen } = props
const [input, setInput] = useState('')
const valid = isValidIframe(input) || isValidUrl(input)
const embedCode = isValidIframe(input) ? input : `<iframe src="${input}" />`
return (
<Modal open={open} setOpen={setOpen}>
<Col className="gap-2 rounded bg-white p-6">
<label
htmlFor="embed"
className="block text-sm font-medium text-gray-700"
>
Embed a market, Youtube video, etc.
</label>
<input
type="text"
name="embed"
id="embed"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder='e.g. <iframe src="..."></iframe>'
value={input}
onChange={(e) => setInput(e.target.value)}
/>
{/* Preview the embed if it's valid */}
{valid ? <RichContent content={embedCode} /> : <Spacer h={2} />}
<Row className="gap-2">
<Button
disabled={!valid}
onClick={() => {
if (editor && valid) {
editor.chain().insertContent(embedCode).run()
setInput('')
setOpen(false)
}
}}
>
Embed
</Button>
<Button
color="gray"
onClick={() => {
setInput('')
setOpen(false)
}}
>
Cancel
</Button>
</Row>
</Col>
</Modal>
)
}
const useUploadMutation = (editor: Editor | null) => const useUploadMutation = (editor: Editor | null) =>
useMutation( useMutation(
(files: File[]) => (files: File[]) =>
@ -294,7 +201,7 @@ const useUploadMutation = (editor: Editor | null) =>
} }
) )
function RichContent(props: { export function RichContent(props: {
content: JSONContent | string content: JSONContent | string
smallImage?: boolean smallImage?: boolean
}) { }) {
@ -307,6 +214,7 @@ function RichContent(props: {
DisplayLink, DisplayLink,
DisplayMention, DisplayMention,
Iframe, Iframe,
TiptapTweet,
], ],
content, content,
editable: false, editable: false,

View File

@ -2,29 +2,62 @@ import { Editor } from '@tiptap/react'
import { useState } from 'react' import { useState } from 'react'
import { TwitterTweetEmbed } from 'react-twitter-embed' import { TwitterTweetEmbed } from 'react-twitter-embed'
import { Button } from '../button' import { Button } from '../button'
import { RichContent } from '../editor'
import { Col } from '../layout/col' import { Col } from '../layout/col'
import { Modal } from '../layout/modal' import { Modal } from '../layout/modal'
import { Row } from '../layout/row' import { Row } from '../layout/row'
import { Spacer } from '../layout/spacer' import { Spacer } from '../layout/spacer'
function isValidIframe(text: string) {
return /^<iframe.*<\/iframe>$/.test(text)
}
// A valid tweet URL looks like 'https://twitter.com/username/status/123456789' // A valid tweet URL looks like 'https://twitter.com/username/status/123456789'
// Return the tweetId if the URL is valid, otherwise return null. // Return the tweetId if the URL is valid, otherwise return null.
function getTweetId(text: string) { function getTweetId(text: string) {
const match = text.match(/^https?:\/\/twitter\.com\/.*\/status\/(\d+)$/) const match = text.match(/^https?:\/\/twitter\.com\/.*\/status\/(\d+)/)
return match ? match[1] : null return match ? match[1] : null
} }
export function TweetModal(props: { // A valid YouTube URL looks like 'https://www.youtube.com/watch?v=ziq7FUKpCS8'
function getYoutubeId(text: string) {
const match = text.match(/^https?:\/\/www\.youtube\.com\/watch\?v=([^&]+)/)
return match ? match[1] : null
}
function isValidUrl(text: string) {
// Conjured by Codex
return /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/.test(
text
)
}
function embedCode(text: string) {
if (isValidIframe(text)) {
return text
} else if (getTweetId(text)) {
// Append a leading 't', to prevent tweetId from being interpreted as a number.
// If it's a number, there may be numeric precision issues.
return `<tiptap-tweet tweetid="t${getTweetId(text)}"></tiptap-tweet>`
} else if (getYoutubeId(text)) {
return `<iframe src="https://www.youtube.com/embed/${getYoutubeId(
text
)}"></iframe>`
} else if (isValidUrl(text)) {
return `<iframe src="${text}"></iframe>`
}
// Return null if the text is not embeddable.
return null
}
export function EmbedModal(props: {
editor: Editor | null editor: Editor | null
open: boolean open: boolean
setOpen: (open: boolean) => void setOpen: (open: boolean) => void
}) { }) {
const { editor, open, setOpen } = props const { editor, open, setOpen } = props
const [input, setInput] = useState('') const [input, setInput] = useState('')
const tweetId = getTweetId(input) const embed = embedCode(input)
// Append a leading 't', to prevent tweetId from being interpreted as a number.
// If it's a number, there may be numeric precision issues.
const tweetCode = `<tiptap-tweet tweetid="t${tweetId}"></tiptap-tweet>`
return ( return (
<Modal open={open} setOpen={setOpen}> <Modal open={open} setOpen={setOpen}>
@ -33,7 +66,7 @@ export function TweetModal(props: {
htmlFor="embed" htmlFor="embed"
className="block text-sm font-medium text-gray-700" className="block text-sm font-medium text-gray-700"
> >
Paste a tweet link Embed a Youtube video, Tweet, or other link
</label> </label>
<input <input
type="text" type="text"
@ -46,14 +79,14 @@ export function TweetModal(props: {
/> />
{/* Preview the embed if it's valid */} {/* Preview the embed if it's valid */}
{tweetId ? <TwitterTweetEmbed tweetId={tweetId} /> : <Spacer h={2} />} {embed ? <RichContent content={embed} /> : <Spacer h={2} />}
<Row className="gap-2"> <Row className="gap-2">
<Button <Button
disabled={!tweetId} disabled={!embed}
onClick={() => { onClick={() => {
if (editor && tweetId) { if (editor && embed) {
editor.chain().insertContent(tweetCode).run() editor.chain().insertContent(embed).run()
console.log('editorjson', editor.getJSON()) console.log('editorjson', editor.getJSON())
setInput('') setInput('')
setOpen(false) setOpen(false)