Use a single place to embed iframe, Youtube, and Tweets
This commit is contained in:
parent
6423833ea4
commit
f688d3c5d3
|
@ -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,
|
||||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue
Block a user