snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
This commit is contained in:
5
packages/extension-youtube/src/index.ts
Normal file
5
packages/extension-youtube/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Youtube } from './youtube.js'
|
||||
|
||||
export * from './youtube.js'
|
||||
|
||||
export default Youtube
|
||||
163
packages/extension-youtube/src/utils.test.ts
Normal file
163
packages/extension-youtube/src/utils.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { describe, expect, it } from 'vite-plus/test'
|
||||
|
||||
import {
|
||||
getAttributesFromYoutubeEmbedUrl,
|
||||
getEmbedUrlFromYoutubeUrl,
|
||||
isValidYoutubeUrl,
|
||||
} from './utils.js'
|
||||
|
||||
describe('YouTube Live URL handling', () => {
|
||||
it('generates correct embed URL for a YouTube Live URL', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://www.youtube.com/live/EkRHhOCdZjw',
|
||||
controls: true,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube.com/embed/EkRHhOCdZjw')
|
||||
})
|
||||
|
||||
it('generates correct embed URL for a YouTube Live URL without www prefix', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://youtube.com/live/EkRHhOCdZjw',
|
||||
controls: true,
|
||||
autoplay: true,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube.com/embed/EkRHhOCdZjw?autoplay=1')
|
||||
})
|
||||
|
||||
it('generates correct embed URL for a YouTube Live URL with multiple parameters', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://www.youtube.com/live/EkRHhOCdZjw',
|
||||
autoplay: true,
|
||||
controls: false,
|
||||
rel: 0,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube.com/embed/EkRHhOCdZjw?autoplay=1&controls=0&rel=0')
|
||||
})
|
||||
|
||||
it('generates correct embed URL for a YouTube Live URL with nocookie option', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://www.youtube.com/live/EkRHhOCdZjw',
|
||||
nocookie: true,
|
||||
controls: true,
|
||||
rel: 1,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube-nocookie.com/embed/EkRHhOCdZjw?rel=1')
|
||||
})
|
||||
|
||||
it('keeps a live URL with a query string on the video id', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://www.youtube.com/live/EkRHhOCdZjw?feature=share',
|
||||
controls: true,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube.com/embed/EkRHhOCdZjw')
|
||||
})
|
||||
})
|
||||
|
||||
describe('YouTube path-like near misses', () => {
|
||||
const nearMisses = [
|
||||
'https://www.youtube.com/notlive/EkRHhOCdZjw',
|
||||
'https://www.youtube.com/notshorts/EkRHhOCdZjw',
|
||||
'https://www.youtube.com/watch?redirect=live/EkRHhOCdZjw',
|
||||
]
|
||||
|
||||
nearMisses.forEach(url => {
|
||||
it(`does not embed a video id for ${url}`, () => {
|
||||
expect(getEmbedUrlFromYoutubeUrl({ url, controls: true })).toBe(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('YouTube Shorts URL handling', () => {
|
||||
it('generates correct embed URL for YouTube Shorts with rel parameter', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://www.youtube.com/shorts/EkRHhOCdZjw',
|
||||
controls: true,
|
||||
rel: 1,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube.com/embed/EkRHhOCdZjw?rel=1')
|
||||
})
|
||||
|
||||
it('generates correct embed URL for YouTube Shorts without www prefix', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://youtube.com/shorts/EkRHhOCdZjw',
|
||||
controls: true,
|
||||
autoplay: true,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube.com/embed/EkRHhOCdZjw?autoplay=1')
|
||||
})
|
||||
|
||||
it('generates correct embed URL for YouTube Shorts with multiple parameters', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://www.youtube.com/shorts/EkRHhOCdZjw',
|
||||
autoplay: true,
|
||||
controls: false,
|
||||
rel: 0,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube.com/embed/EkRHhOCdZjw?autoplay=1&controls=0&rel=0')
|
||||
})
|
||||
|
||||
it('generates correct embed URL for YouTube Shorts with nocookie option', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://www.youtube.com/shorts/EkRHhOCdZjw',
|
||||
nocookie: true,
|
||||
controls: true,
|
||||
rel: 1,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube-nocookie.com/embed/EkRHhOCdZjw?rel=1')
|
||||
})
|
||||
|
||||
it('generates correct embed URL for YouTube Shorts without extra parameters', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://www.youtube.com/shorts/EkRHhOCdZjw',
|
||||
controls: true,
|
||||
})
|
||||
|
||||
expect(result).toBe('https://www.youtube.com/embed/EkRHhOCdZjw')
|
||||
})
|
||||
})
|
||||
|
||||
describe('YouTube Playlist URL handling', () => {
|
||||
it('generates correct embed URL for playlist with additional parameters using & separator', () => {
|
||||
const result = getEmbedUrlFromYoutubeUrl({
|
||||
url: 'https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf',
|
||||
controls: true,
|
||||
autoplay: true,
|
||||
})
|
||||
|
||||
// Playlist URLs use ?list= in base URL, so additional params should use &
|
||||
expect(result).toBe(
|
||||
'https://www.youtube-nocookie.com/embed/videoseries?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf&autoplay=1',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
'https://www.youtube.com/embed/',
|
||||
'https://www.youtube.com/embed/videoseries',
|
||||
'https://example.com/embed/dQw4w9WgXcQ',
|
||||
])('returns null for unsupported youtube embed urls: %s', embedUrl => {
|
||||
expect(getAttributesFromYoutubeEmbedUrl(embedUrl)).toBeNull()
|
||||
})
|
||||
|
||||
describe('missing URLs', () => {
|
||||
it('accepts a well-formed youtube url', () => {
|
||||
expect(isValidYoutubeUrl('https://www.youtube.com/watch?v=EkRHhOCdZjw')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('returns null for a missing url instead of throwing', () => {
|
||||
expect(isValidYoutubeUrl(null)).toBe(null)
|
||||
})
|
||||
|
||||
it('returns null from getEmbedUrlFromYoutubeUrl when the url is missing', () => {
|
||||
expect(getEmbedUrlFromYoutubeUrl({ url: null as unknown as string })).toBe(null)
|
||||
})
|
||||
})
|
||||
234
packages/extension-youtube/src/utils.ts
Normal file
234
packages/extension-youtube/src/utils.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
export const YOUTUBE_REGEX =
|
||||
/^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu\.be|youtube-nocookie\.com))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/
|
||||
export const YOUTUBE_REGEX_GLOBAL =
|
||||
/^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu\.be|youtube-nocookie\.com))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/g
|
||||
|
||||
/**
|
||||
* Checks whether a url is a youtube url that the extension can embed.
|
||||
*
|
||||
* The `src` attribute of a youtube node is declared with `default: null`, so a node
|
||||
* parsed from an iframe without a `src` attribute legitimately carries no url. Guard
|
||||
* against that here instead of letting the caller dereference it.
|
||||
*/
|
||||
export const isValidYoutubeUrl = (url: string | null | undefined) => {
|
||||
if (!url) {
|
||||
return null
|
||||
}
|
||||
|
||||
return url.match(YOUTUBE_REGEX)
|
||||
}
|
||||
|
||||
export interface GetEmbedUrlOptions {
|
||||
url: string
|
||||
allowFullscreen?: boolean
|
||||
autoplay?: boolean
|
||||
ccLanguage?: string
|
||||
ccLoadPolicy?: boolean
|
||||
controls?: boolean
|
||||
disableKBcontrols?: boolean
|
||||
enableIFrameApi?: boolean
|
||||
endTime?: number
|
||||
interfaceLanguage?: string
|
||||
ivLoadPolicy?: number
|
||||
loop?: boolean
|
||||
modestBranding?: boolean
|
||||
nocookie?: boolean
|
||||
origin?: string
|
||||
playlist?: string
|
||||
progressBarColor?: string
|
||||
startAt?: number
|
||||
rel?: number
|
||||
}
|
||||
|
||||
export interface YoutubeEmbedAttributes {
|
||||
src: string
|
||||
start?: number
|
||||
}
|
||||
|
||||
export const getYoutubeEmbedUrl = (nocookie?: boolean, isPlaylist?: boolean) => {
|
||||
if (isPlaylist) {
|
||||
return 'https://www.youtube-nocookie.com/embed/videoseries?list='
|
||||
}
|
||||
return nocookie ? 'https://www.youtube-nocookie.com/embed/' : 'https://www.youtube.com/embed/'
|
||||
}
|
||||
|
||||
export const getEmbedUrlFromYoutubeUrl = (options: GetEmbedUrlOptions) => {
|
||||
const {
|
||||
url,
|
||||
allowFullscreen,
|
||||
autoplay,
|
||||
ccLanguage,
|
||||
ccLoadPolicy,
|
||||
controls,
|
||||
disableKBcontrols,
|
||||
enableIFrameApi,
|
||||
endTime,
|
||||
interfaceLanguage,
|
||||
ivLoadPolicy,
|
||||
loop,
|
||||
modestBranding,
|
||||
nocookie,
|
||||
origin,
|
||||
playlist,
|
||||
progressBarColor,
|
||||
startAt,
|
||||
rel,
|
||||
} = options
|
||||
|
||||
if (!isValidYoutubeUrl(url)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// if is already an embed url, return it
|
||||
if (url.includes('/embed/')) {
|
||||
return url
|
||||
}
|
||||
|
||||
// if is a youtu.be url, get the id after the /
|
||||
if (url.includes('youtu.be')) {
|
||||
const id = url.split('/').pop()
|
||||
|
||||
if (!id) {
|
||||
return null
|
||||
}
|
||||
return `${getYoutubeEmbedUrl(nocookie)}${id}`
|
||||
}
|
||||
|
||||
const videoIdRegex = /(?:(v|list)=|\/(?:shorts|live)\/)([-\w]+)/gm
|
||||
const matches = videoIdRegex.exec(url)
|
||||
|
||||
if (!matches || !matches[2]) {
|
||||
return null
|
||||
}
|
||||
|
||||
let outputUrl = `${getYoutubeEmbedUrl(nocookie, matches[1] === 'list')}${matches[2]}`
|
||||
|
||||
const params = []
|
||||
|
||||
if (allowFullscreen === false) {
|
||||
params.push('fs=0')
|
||||
}
|
||||
|
||||
if (autoplay) {
|
||||
params.push('autoplay=1')
|
||||
}
|
||||
|
||||
if (ccLanguage) {
|
||||
params.push(`cc_lang_pref=${ccLanguage}`)
|
||||
}
|
||||
|
||||
if (ccLoadPolicy) {
|
||||
params.push('cc_load_policy=1')
|
||||
}
|
||||
|
||||
if (!controls) {
|
||||
params.push('controls=0')
|
||||
}
|
||||
|
||||
if (disableKBcontrols) {
|
||||
params.push('disablekb=1')
|
||||
}
|
||||
|
||||
if (enableIFrameApi) {
|
||||
params.push('enablejsapi=1')
|
||||
}
|
||||
|
||||
if (endTime) {
|
||||
params.push(`end=${endTime}`)
|
||||
}
|
||||
|
||||
if (interfaceLanguage) {
|
||||
params.push(`hl=${interfaceLanguage}`)
|
||||
}
|
||||
|
||||
if (ivLoadPolicy) {
|
||||
params.push(`iv_load_policy=${ivLoadPolicy}`)
|
||||
}
|
||||
|
||||
if (loop) {
|
||||
params.push('loop=1')
|
||||
}
|
||||
|
||||
if (modestBranding) {
|
||||
params.push('modestbranding=1')
|
||||
}
|
||||
|
||||
if (origin) {
|
||||
params.push(`origin=${origin}`)
|
||||
}
|
||||
|
||||
if (playlist) {
|
||||
params.push(`playlist=${playlist}`)
|
||||
}
|
||||
|
||||
if (startAt) {
|
||||
params.push(`start=${startAt}`)
|
||||
}
|
||||
|
||||
if (progressBarColor) {
|
||||
params.push(`color=${progressBarColor}`)
|
||||
}
|
||||
|
||||
if (rel !== undefined) {
|
||||
params.push(`rel=${rel}`)
|
||||
}
|
||||
|
||||
if (params.length) {
|
||||
outputUrl += `${matches[1] === 'list' ? '&' : '?'}${params.join('&')}`
|
||||
}
|
||||
|
||||
return outputUrl
|
||||
}
|
||||
|
||||
export const getAttributesFromYoutubeEmbedUrl = (url: string): YoutubeEmbedAttributes | null => {
|
||||
let parsedUrl: URL
|
||||
|
||||
try {
|
||||
parsedUrl = new URL(url)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
const hostname = parsedUrl.hostname.replace(/^www\./, '')
|
||||
|
||||
if (hostname !== 'youtube.com' && hostname !== 'youtube-nocookie.com') {
|
||||
return null
|
||||
}
|
||||
|
||||
let src: string | null = null
|
||||
|
||||
if (parsedUrl.pathname === '/embed/videoseries') {
|
||||
const list = parsedUrl.searchParams.get('list')
|
||||
|
||||
if (!list) {
|
||||
return null
|
||||
}
|
||||
|
||||
src = `https://www.youtube.com/playlist?list=${list}`
|
||||
} else {
|
||||
const matches = parsedUrl.pathname.match(/^\/embed\/([\w-]+)$/)
|
||||
|
||||
if (!matches?.[1]) {
|
||||
return null
|
||||
}
|
||||
|
||||
src = `https://www.youtube.com/watch?v=${matches[1]}`
|
||||
}
|
||||
|
||||
if (!isValidYoutubeUrl(src)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const attributes: YoutubeEmbedAttributes = { src }
|
||||
const start = parsedUrl.searchParams.get('start')
|
||||
|
||||
if (start) {
|
||||
const parsedStart = Number.parseInt(start, 10)
|
||||
|
||||
if (!Number.isNaN(parsedStart)) {
|
||||
attributes.start = parsedStart
|
||||
}
|
||||
}
|
||||
|
||||
return attributes
|
||||
}
|
||||
399
packages/extension-youtube/src/youtube.ts
Normal file
399
packages/extension-youtube/src/youtube.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
import { createAtomBlockMarkdownSpec, mergeAttributes, Node, nodePasteRule } from '@tiptap/core'
|
||||
|
||||
import {
|
||||
getAttributesFromYoutubeEmbedUrl,
|
||||
getEmbedUrlFromYoutubeUrl,
|
||||
isValidYoutubeUrl,
|
||||
YOUTUBE_REGEX_GLOBAL,
|
||||
} from './utils.js'
|
||||
|
||||
const getParsedDimension = (value: string | null) => {
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
|
||||
const trimmedValue = value.trim()
|
||||
|
||||
if (trimmedValue === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
const parsedValue = Number(trimmedValue)
|
||||
|
||||
return Number.isNaN(parsedValue) ? trimmedValue : parsedValue
|
||||
}
|
||||
|
||||
const getParsedYoutubeAttributes = (element: HTMLElement) => {
|
||||
const src = element.getAttribute('src')
|
||||
|
||||
if (!src) {
|
||||
return null
|
||||
}
|
||||
|
||||
const embedAttributes = getAttributesFromYoutubeEmbedUrl(src)
|
||||
|
||||
if (embedAttributes) {
|
||||
return embedAttributes
|
||||
}
|
||||
|
||||
return {
|
||||
src,
|
||||
}
|
||||
}
|
||||
|
||||
export type { GetEmbedUrlOptions } from './utils.js'
|
||||
export {
|
||||
getAttributesFromYoutubeEmbedUrl,
|
||||
getEmbedUrlFromYoutubeUrl,
|
||||
isValidYoutubeUrl,
|
||||
} from './utils.js'
|
||||
|
||||
export interface YoutubeOptions {
|
||||
/**
|
||||
* Controls if the paste handler for youtube videos should be added.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
addPasteHandler: boolean
|
||||
|
||||
/**
|
||||
* Controls if the youtube video should be allowed to go fullscreen.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
allowFullscreen: boolean
|
||||
|
||||
/**
|
||||
* Controls if the youtube video should autoplay.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
autoplay: boolean
|
||||
|
||||
/**
|
||||
* The language of the captions shown in the youtube video.
|
||||
* @default undefined
|
||||
* @example 'en'
|
||||
*/
|
||||
ccLanguage?: string
|
||||
|
||||
/**
|
||||
* Controls if the captions should be shown in the youtube video.
|
||||
* @default undefined
|
||||
* @example true
|
||||
*/
|
||||
ccLoadPolicy?: boolean
|
||||
|
||||
/**
|
||||
* Controls if the controls should be shown in the youtube video.
|
||||
* @default true
|
||||
* @example false
|
||||
*/
|
||||
controls: boolean
|
||||
|
||||
/**
|
||||
* Controls if the keyboard controls should be disabled in the youtube video.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
disableKBcontrols: boolean
|
||||
|
||||
/**
|
||||
* Controls if the iframe api should be enabled in the youtube video.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
enableIFrameApi: boolean
|
||||
|
||||
/**
|
||||
* The end time of the youtube video.
|
||||
* @default 0
|
||||
* @example 120
|
||||
*/
|
||||
endTime: number
|
||||
|
||||
/**
|
||||
* The height of the youtube video.
|
||||
* @default 480
|
||||
* @example 720
|
||||
* @example '100%'
|
||||
*/
|
||||
height: number | string
|
||||
|
||||
/**
|
||||
* The language of the youtube video.
|
||||
* @default undefined
|
||||
* @example 'en'
|
||||
*/
|
||||
interfaceLanguage?: string
|
||||
|
||||
/**
|
||||
* Controls if the video annotations should be shown in the youtube video.
|
||||
* @default 0
|
||||
* @example 1
|
||||
*/
|
||||
ivLoadPolicy: number
|
||||
|
||||
/**
|
||||
* Controls if the youtube video should loop.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
loop: boolean
|
||||
|
||||
/**
|
||||
* Controls if the youtube video should show a small youtube logo.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
modestBranding: boolean
|
||||
|
||||
/**
|
||||
* The HTML attributes for a youtube video node.
|
||||
* @default {}
|
||||
* @example { class: 'foo' }
|
||||
*/
|
||||
HTMLAttributes: Record<string, any>
|
||||
|
||||
/**
|
||||
* Controls if the youtube node should be inline or not.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
inline: boolean
|
||||
|
||||
/**
|
||||
* Controls if the youtube video should be loaded from youtube-nocookie.com.
|
||||
* @default false
|
||||
* @example true
|
||||
*/
|
||||
nocookie: boolean
|
||||
|
||||
/**
|
||||
* The origin of the youtube video.
|
||||
* @default ''
|
||||
* @example 'https://tiptap.dev'
|
||||
*/
|
||||
origin: string
|
||||
|
||||
/**
|
||||
* The playlist of the youtube video.
|
||||
* @default ''
|
||||
* @example 'PLQg6GaokU5CwiVmsZ0dZm6VeIg0V5z1tK'
|
||||
*/
|
||||
playlist: string
|
||||
|
||||
/**
|
||||
* The color of the youtube video progress bar.
|
||||
* @default undefined
|
||||
* @example 'red'
|
||||
*/
|
||||
progressBarColor?: string
|
||||
|
||||
/**
|
||||
* The width of the youtube video.
|
||||
* @default 640
|
||||
* @example 1280
|
||||
* @example '100%'
|
||||
*/
|
||||
width: number | string
|
||||
|
||||
/**
|
||||
* Controls if the related youtube videos at the end are from the same channel.
|
||||
* @default 1
|
||||
* @example 0
|
||||
*/
|
||||
rel: number
|
||||
}
|
||||
|
||||
/**
|
||||
* The options for setting a youtube video.
|
||||
*/
|
||||
type SetYoutubeVideoOptions = {
|
||||
src: string
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
start?: number
|
||||
}
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
youtube: {
|
||||
/**
|
||||
* Insert a youtube video
|
||||
* @param options The youtube video attributes
|
||||
* @example editor.commands.setYoutubeVideo({ src: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' })
|
||||
*/
|
||||
setYoutubeVideo: (options: SetYoutubeVideoOptions) => ReturnType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension adds support for youtube videos.
|
||||
* @see https://www.tiptap.dev/api/nodes/youtube
|
||||
*/
|
||||
export const Youtube = Node.create<YoutubeOptions>({
|
||||
name: 'youtube',
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
addPasteHandler: true,
|
||||
allowFullscreen: true,
|
||||
autoplay: false,
|
||||
ccLanguage: undefined,
|
||||
ccLoadPolicy: undefined,
|
||||
controls: true,
|
||||
disableKBcontrols: false,
|
||||
enableIFrameApi: false,
|
||||
endTime: 0,
|
||||
height: 480,
|
||||
interfaceLanguage: undefined,
|
||||
ivLoadPolicy: 0,
|
||||
loop: false,
|
||||
modestBranding: false,
|
||||
HTMLAttributes: {},
|
||||
inline: false,
|
||||
nocookie: false,
|
||||
origin: '',
|
||||
playlist: '',
|
||||
progressBarColor: undefined,
|
||||
width: 640,
|
||||
rel: 1,
|
||||
}
|
||||
},
|
||||
|
||||
inline() {
|
||||
return this.options.inline
|
||||
},
|
||||
|
||||
group() {
|
||||
return this.options.inline ? 'inline' : 'block'
|
||||
},
|
||||
|
||||
draggable: true,
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
src: {
|
||||
default: null,
|
||||
parseHTML: element => getParsedYoutubeAttributes(element)?.src,
|
||||
},
|
||||
start: {
|
||||
default: 0,
|
||||
parseHTML: element => getParsedYoutubeAttributes(element)?.start,
|
||||
},
|
||||
width: {
|
||||
default: this.options.width,
|
||||
parseHTML: element => getParsedDimension(element.getAttribute('width')),
|
||||
},
|
||||
height: {
|
||||
default: this.options.height,
|
||||
parseHTML: element => getParsedDimension(element.getAttribute('height')),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'div[data-youtube-video] iframe',
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
return {
|
||||
setYoutubeVideo:
|
||||
(options: SetYoutubeVideoOptions) =>
|
||||
({ commands }) => {
|
||||
if (!isValidYoutubeUrl(options.src)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return commands.insertContent({
|
||||
type: this.name,
|
||||
attrs: options,
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
addPasteRules() {
|
||||
if (!this.options.addPasteHandler) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
nodePasteRule({
|
||||
find: YOUTUBE_REGEX_GLOBAL,
|
||||
type: this.type,
|
||||
getAttributes: match => {
|
||||
return { src: match.input }
|
||||
},
|
||||
}),
|
||||
]
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
const embedUrl = getEmbedUrlFromYoutubeUrl({
|
||||
url: HTMLAttributes.src,
|
||||
allowFullscreen: this.options.allowFullscreen,
|
||||
autoplay: this.options.autoplay,
|
||||
ccLanguage: this.options.ccLanguage,
|
||||
ccLoadPolicy: this.options.ccLoadPolicy,
|
||||
controls: this.options.controls,
|
||||
disableKBcontrols: this.options.disableKBcontrols,
|
||||
enableIFrameApi: this.options.enableIFrameApi,
|
||||
endTime: this.options.endTime,
|
||||
interfaceLanguage: this.options.interfaceLanguage,
|
||||
ivLoadPolicy: this.options.ivLoadPolicy,
|
||||
loop: this.options.loop,
|
||||
modestBranding: this.options.modestBranding,
|
||||
nocookie: this.options.nocookie,
|
||||
origin: this.options.origin,
|
||||
playlist: this.options.playlist,
|
||||
progressBarColor: this.options.progressBarColor,
|
||||
startAt: HTMLAttributes.start || 0,
|
||||
rel: this.options.rel,
|
||||
})
|
||||
|
||||
HTMLAttributes.src = embedUrl
|
||||
|
||||
return [
|
||||
'div',
|
||||
{ 'data-youtube-video': '' },
|
||||
[
|
||||
'iframe',
|
||||
mergeAttributes(
|
||||
this.options.HTMLAttributes,
|
||||
{
|
||||
width: this.options.width,
|
||||
height: this.options.height,
|
||||
allowfullscreen: this.options.allowFullscreen,
|
||||
autoplay: this.options.autoplay,
|
||||
ccLanguage: this.options.ccLanguage,
|
||||
ccLoadPolicy: this.options.ccLoadPolicy,
|
||||
disableKBcontrols: this.options.disableKBcontrols,
|
||||
enableIFrameApi: this.options.enableIFrameApi,
|
||||
endTime: this.options.endTime,
|
||||
interfaceLanguage: this.options.interfaceLanguage,
|
||||
ivLoadPolicy: this.options.ivLoadPolicy,
|
||||
loop: this.options.loop,
|
||||
modestBranding: this.options.modestBranding,
|
||||
origin: this.options.origin,
|
||||
playlist: this.options.playlist,
|
||||
progressBarColor: this.options.progressBarColor,
|
||||
rel: this.options.rel,
|
||||
},
|
||||
HTMLAttributes,
|
||||
),
|
||||
],
|
||||
]
|
||||
},
|
||||
|
||||
...createAtomBlockMarkdownSpec({
|
||||
nodeName: 'youtube',
|
||||
allowedAttributes: ['src', 'width', 'height', 'start'],
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user