From fac94543ff481e730641ceea7a8641650f01e5d6 Mon Sep 17 00:00:00 2001 From: Ricardo Amaral Date: Fri, 5 Apr 2024 19:24:22 +0100 Subject: [PATCH] feat: Add `RichTextVideo` extension for video playback --- src/extensions/rich-text/rich-text-kit.ts | 11 + src/extensions/rich-text/rich-text-video.ts | 318 ++++++++++++++++++ src/helpers/schema.test.ts | 2 +- src/helpers/unified.test.ts | 17 +- src/helpers/unified.ts | 17 +- src/index.ts | 4 + src/serializers/html/html.test.ts | 62 ++++ src/serializers/html/html.ts | 6 + src/serializers/html/plugins/remark-video.ts | 76 +++++ src/serializers/markdown/markdown.test.ts | 42 +++ src/serializers/markdown/markdown.ts | 6 + src/serializers/markdown/plugins/video.ts | 25 ++ .../typist-editor-decorator.module.css | 30 +- stories/typist-editor/rich-text.stories.tsx | 16 +- .../rich-text-video-wrapper.module.css | 26 ++ .../wrappers/rich-text-video-wrapper.tsx | 55 +++ 16 files changed, 692 insertions(+), 21 deletions(-) create mode 100644 src/extensions/rich-text/rich-text-video.ts create mode 100644 src/serializers/html/plugins/remark-video.ts create mode 100644 src/serializers/markdown/plugins/video.ts create mode 100644 stories/typist-editor/wrappers/rich-text-video-wrapper.module.css create mode 100644 stories/typist-editor/wrappers/rich-text-video-wrapper.tsx 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 `