Example: tweet
Embed and display Twitter tweets.
Install this example with
shadcn: npx shadcn@latest add @prosekit/react-example-tweetnpx shadcn@latest add @prosekit/vue-example-tweetimport 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type Extension, type NodeJSON } from 'prosekit/core'
import { defineReactNodeView, ProseKit, useExtension } from 'prosekit/react'
import { useMemo, useState } from 'react'
import { sampleContent } from '../../sample/sample-doc-tweet'
import { defineExtension } from './extension'
import { MethodSelect } from './method-select'
import { TweetView } from './tweet-view'
interface EditorProps {
initialContent?: NodeJSON
}
export default function Editor(props: EditorProps) {
const defaultContent = props.initialContent ?? sampleContent
const editor = useMemo(() => {
return createEditor({ extension: defineExtension(), defaultContent })
}, [defaultContent])
const [method, setMethod] = useState<'basic' | 'advanced'>('basic')
const reactTweetView: Extension | null = useMemo(() => {
if (method === 'basic') {
return null
}
return defineReactNodeView({
name: 'tweet',
component: TweetView,
})
}, [method])
useExtension(reactTweetView, { editor })
return (
<ProseKit editor={editor}>
<MethodSelect value={method} onChange={setMethod} />
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</div>
</ProseKit>
)
}import { defineBasicExtension } from 'prosekit/basic'
import { defineNodeSpec, union } from 'prosekit/core'
function defineTweetSpec() {
return defineNodeSpec({
name: 'tweet',
group: 'block',
attrs: {
tweetId: { default: null },
},
parseDOM: [{
tag: 'iframe[src^="https://platform.twitter.com/embed/Tweet.html"]',
getAttrs: (node) => {
const src = node.getAttribute('src')
const match = src?.match(/id=([^&]+)/)
return {
tweetId: match?.[1] ?? null,
}
},
}],
toDOM: (node) => {
return [
'iframe',
{
src: `https://platform.twitter.com/embed/Tweet.html?id=${node.attrs.tweetId}&theme=dark`,
style: 'height: 300px',
},
]
},
})
}
function defineTweet() {
return union(
defineTweetSpec(),
)
}
export function defineExtension() {
return union(
defineBasicExtension(),
defineTweet(),
)
}
export type EditorExtension = ReturnType<typeof defineExtension>'use client'
export { default as ExampleEditor } from './editor'import { useId } from 'react'
export function MethodSelect(props: {
value: 'basic' | 'advanced'
onChange: (value: 'basic' | 'advanced') => void
}) {
const id = 'id-' + useId()
const basicId = `${id}-basic`
const advancedId = `${id}-advanced`
return (
<fieldset className="not-content">
<legend>Select a render method:</legend>
<div>
<input
type="radio"
id={basicId}
name={id}
value="basic"
checked={props.value === 'basic'}
onChange={() => props.onChange('basic')}
/>
<label htmlFor={basicId}>basic</label>
</div>
<div>
<input
type="radio"
id={advancedId}
name={id}
value="advanced"
checked={props.value === 'advanced'}
onChange={() => props.onChange('advanced')}
/>
<label htmlFor={advancedId}>advanced</label>
</div>
</fieldset>
)
}import type { ReactNodeViewProps } from 'prosekit/react'
import { Tweet } from 'react-tweet'
export function TweetView({ node }: ReactNodeViewProps) {
const tweetId = node.attrs.tweetId as string
return (
<div className="[&_img]:m-0!">
<div>
<strong>
Rendered in React using library{' '}
<span>
<a href="https://github.com/vercel/react-tweet" target="_blank" rel="noopener noreferrer">react-tweet</a>
</span>
</strong>
</div>
<Tweet id={tweetId} />
</div>
)
}import type { NodeJSON } from 'prosekit/core'
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Render a tweet in your document',
},
],
},
{
type: 'tweet',
attrs: {
tweetId: '20',
},
},
],
}<script setup lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { createEditor, type NodeJSON } from 'prosekit/core'
import { defineVueNodeView, ProseKit, useExtension, type VueNodeViewComponent } from 'prosekit/vue'
import { computed, ref } from 'vue'
import { sampleContent } from '../../sample/sample-doc-tweet'
import { defineExtension } from './extension'
import MethodSelect from './method-select.vue'
import TweetView from './tweet-view.vue'
const props = defineProps<{
initialContent?: NodeJSON
}>()
const defaultContent = props.initialContent ?? sampleContent
const editor = createEditor({ extension: defineExtension(), defaultContent })
const method = ref<'basic' | 'advanced'>('basic')
const vueTweetView = computed(() => {
if (method.value === 'basic') {
return null
}
return defineVueNodeView({
name: 'tweet',
component: TweetView as VueNodeViewComponent,
})
})
useExtension(vueTweetView, { editor })
</script>
<template>
<ProseKit :editor="editor">
<MethodSelect :value="method" :on-change="(newMethod) => (method = newMethod)" />
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div :ref="(el) => editor.mount(el as HTMLElement | null)" class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500" />
</div>
</div>
</ProseKit>
</template>import { defineBasicExtension } from 'prosekit/basic'
import { defineNodeSpec, union } from 'prosekit/core'
function defineTweetSpec() {
return defineNodeSpec({
name: 'tweet',
group: 'block',
attrs: {
tweetId: { default: null },
},
parseDOM: [{
tag: 'iframe[src^="https://platform.twitter.com/embed/Tweet.html"]',
getAttrs: (node) => {
const src = node.getAttribute('src')
const match = src?.match(/id=([^&]+)/)
return {
tweetId: match?.[1] ?? null,
}
},
}],
toDOM: (node) => {
return [
'iframe',
{
src: `https://platform.twitter.com/embed/Tweet.html?id=${node.attrs.tweetId}&theme=dark`,
style: 'height: 300px',
},
]
},
})
}
function defineTweet() {
return union(
defineTweetSpec(),
)
}
export function defineExtension() {
return union(
defineBasicExtension(),
defineTweet(),
)
}
export type EditorExtension = ReturnType<typeof defineExtension>export { default as ExampleEditor } from './editor.vue'<script setup lang="ts">
import { useId } from 'vue'
const props = defineProps<{
value: 'basic' | 'advanced'
onChange: (value: 'basic' | 'advanced') => void
}>()
const id = 'id-' + useId()
const basicId = `${id}-basic`
const advancedId = `${id}-advanced`
</script>
<template>
<fieldset class="not-content">
<legend>Select a render method:</legend>
<div>
<input
:id="basicId"
type="radio"
:name="id"
value="basic"
:checked="props.value === 'basic'"
@change="props.onChange('basic')"
/>
<label :for="basicId">basic</label>
</div>
<div>
<input
:id="advancedId"
type="radio"
:name="id"
value="advanced"
:checked="props.value === 'advanced'"
@change="props.onChange('advanced')"
/>
<label :for="advancedId">advanced</label>
</div>
</fieldset>
</template><script setup lang="ts">
import type { VueNodeViewProps } from 'prosekit/vue'
import Tweet from 'vue-tweet'
const props = defineProps<VueNodeViewProps>()
const tweetId = props.node.value.attrs.tweetId as string
</script>
<template>
<div>
<div>
<strong>Rendered in Vue using library <span><a
href="https://github.com/DannyFeliz/vue-tweet"
target="_blank"
rel="noopener noreferrer"
>vue-tweet</a></span></strong>
</div>
<Tweet :tweet-id="tweetId" />
</div>
</template>import type { NodeJSON } from 'prosekit/core'
export const sampleContent: NodeJSON = {
type: 'doc',
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: 'Render a tweet in your document',
},
],
},
{
type: 'tweet',
attrs: {
tweetId: '20',
},
},
],
}