diff --git a/src/extensions/rich-text/rich-text-kit.ts b/src/extensions/rich-text/rich-text-kit.ts index 80f6df45..4346a93c 100644 --- a/src/extensions/rich-text/rich-text-kit.ts +++ b/src/extensions/rich-text/rich-text-kit.ts @@ -31,6 +31,7 @@ import { RichTextImage } from './rich-text-image' import { RichTextLink } from './rich-text-link' import { RichTextOrderedList } from './rich-text-ordered-list' import { RichTextStrikethrough } from './rich-text-strikethrough' +import { RichTextVideo } from './rich-text-video' import type { Extensions } from '@tiptap/core' import type { BlockquoteOptions } from '@tiptap/extension-blockquote' @@ -52,6 +53,7 @@ import type { RichTextImageOptions } from './rich-text-image' import type { RichTextLinkOptions } from './rich-text-link' import type { RichTextOrderedListOptions } from './rich-text-ordered-list' import type { RichTextStrikethroughOptions } from './rich-text-strikethrough' +import type { RichTextVideoOptions } from './rich-text-video' /** * The options available to customize the `RichTextKit` extension. @@ -186,6 +188,11 @@ type RichTextKitOptions = { * Set to `false` to disable the `Typography` extension. */ typography: false + + /** + * Set options for the `Video` extension, or `false` to disable. + */ + video: Partial | false } /** @@ -330,6 +337,10 @@ const RichTextKit = Extension.create({ extensions.push(Typography) } + if (this.options.video !== false) { + extensions.push(RichTextVideo.configure(this.options?.video)) + } + return extensions }, }) diff --git a/src/extensions/rich-text/rich-text-video.ts b/src/extensions/rich-text/rich-text-video.ts new file mode 100644 index 00000000..3fdbe9a0 --- /dev/null +++ b/src/extensions/rich-text/rich-text-video.ts @@ -0,0 +1,318 @@ +import { mergeAttributes, Node, nodeInputRule } from '@tiptap/core' +import { Plugin, PluginKey, Selection } from '@tiptap/pm/state' +import { ReactNodeViewRenderer } from '@tiptap/react' + +import { REGEX_WEB_URL } from '../../constants/regular-expressions' + +import type { NodeView } from '@tiptap/pm/view' +import type { NodeViewProps } from '@tiptap/react' + +/** + * The properties that describe `RichTextVideo` node attributes. + */ +type RichTextVideoAttributes = { + /** + * Additional metadata about a video attachment upload. + */ + metadata?: { + /** + * A unique ID for the video attachment. + */ + attachmentId: string + + /** + * Specifies if the video attachment failed to upload. + */ + isUploadFailed: boolean + + /** + * The upload progress for the video attachment. + */ + uploadProgress: number + } +} & Pick + +/** + * Augment the official `@tiptap/core` module with extra commands, relevant for this extension, so + * that the compiler knows about them. + */ +declare module '@tiptap/core' { + interface Commands { + richTextVideo: { + /** + * Inserts an video into the editor with the given attributes. + */ + insertVideo: (attributes: RichTextVideoAttributes) => ReturnType + + /** + * Updates the attributes for an existing image in the editor. + */ + updateVideo: ( + attributes: Partial & + Required>, + ) => ReturnType + } + } +} + +/** + * The options available to customize the `RichTextVideo` extension. + */ +type RichTextVideoOptions = { + /** + * A list of accepted MIME types for videos pasting. + */ + acceptedVideoMimeTypes: string[] + + /** + * Whether to automatically start playback of the video as soon as the player is loaded. Its + * default value is `false`, meaning that the video will not start playing automatically. + */ + autoplay: boolean + + /** + * Whether to browser will offer controls to allow the user to control video playback, including + * volume, seeking, and pause/resume playback. Its default value is `true`, meaning that the + * browser will offer playback controls. + */ + controls: boolean + + /** + * A list of options the browser should consider when determining which controls to show for the video element. + * The value is a space-separated list of tokens, which are case-insensitive. + * + * @example 'nofullscreen nodownload noremoteplayback' + * @see https://wicg.github.io/controls-list/explainer.html + * + * Unfortunatelly, both Firefox and Safari do not support this attribute. + * + * @see https://caniuse.com/mdn-html_elements_video_controlslist + */ + controlsList: string + + /** + * Custom HTML attributes that should be added to the rendered HTML tag. + */ + HTMLAttributes: Record + + /** + * Renders the video node inline (e.g.,

). Its default value is + * `false`, meaning that videos are on the same level as paragraphs. + */ + inline: boolean + + /** + * Whether to automatically seek back to the start upon reaching the end of the video. Its + * default value is `false`, meaning that the video will stop playing when it reaches the end. + */ + loop: boolean + + /** + * Whether the audio will be initially silenced. Its default value is `false`, meaning that the + * audio will be played when the video is played. + */ + muted: boolean + + /** + * A React component to render inside the interactive node view. + */ + NodeViewComponent?: React.ComponentType + + /** + * The event handler that is fired when a video file is pasted. + */ + onVideoFilePaste?: (file: File) => void +} + +/** + * The input regex for Markdown video links (i.e. that end with a supported video file extension). + */ +const inputRegex = new RegExp( + `(?:^|\\s)${REGEX_WEB_URL.source}\\.(?:mov|mp4|webm)$`, + REGEX_WEB_URL.flags, +) + +/** + * The `RichTextVideo` extension adds support to render `