diff --git a/packages/editor/rich-text-editor/src/ui/extensions/embed.ts b/packages/editor/rich-text-editor/src/ui/extensions/embed.ts new file mode 100644 index 000000000..4672b75b7 --- /dev/null +++ b/packages/editor/rich-text-editor/src/ui/extensions/embed.ts @@ -0,0 +1,76 @@ +import { Node } from '@tiptap/core' + +export interface IframeOptions { + allowFullscreen: boolean, + HTMLAttributes: { + [key: string]: any + }, +} + +declare module '@tiptap/core' { + interface Commands { + iframe: { + /** + * Add an iframe + */ + setIframe: (options: { src: string }) => ReturnType, + } + } +} + +export const EmbedExtension = Node.create({ + name: 'iframe', + + group: 'block', + + atom: true, + + addOptions() { + return { + allowFullscreen: true, + HTMLAttributes: { + class: 'iframe-wrapper', + }, + } + }, + + addAttributes() { + return { + src: { + default: null, + }, + frameborder: { + default: 0, + }, + allowfullscreen: { + default: this.options.allowFullscreen, + parseHTML: () => this.options.allowFullscreen, + }, + } + }, + + parseHTML() { + return [{ + tag: 'iframe', + }] + }, + + renderHTML({ HTMLAttributes }) { + return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]] + }, + + addCommands() { + return { + setIframe: (options: { src: string }) => ({ tr, dispatch }) => { + const { selection } = tr + const node = this.type.create(options) + + if (dispatch) { + tr.replaceRangeWith(selection.from, selection.to, node) + } + + return true + }, + } + }, +}) diff --git a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx index f0f3bed34..192607d68 100644 --- a/packages/editor/rich-text-editor/src/ui/extensions/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/extensions/index.tsx @@ -8,6 +8,7 @@ import ts from "highlight.js/lib/languages/typescript"; import SlashCommand from "./slash-command"; import { UploadImage } from "../"; +import { EmbedExtension } from "./embed"; const lowlight = createLowlight(common) lowlight.register("ts", ts); @@ -39,6 +40,7 @@ export const RichTextEditorExtensions = ( class: "mb-6 border-t border-custom-border-300", }, }), + EmbedExtension, SlashCommand(uploadFile, setIsSubmitting), CodeBlockLowlight.configure({ lowlight, diff --git a/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx b/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx index e00585dd8..78457191e 100644 --- a/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx +++ b/packages/editor/rich-text-editor/src/ui/extensions/slash-command.tsx @@ -1,4 +1,11 @@ -import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react"; +import { + useState, + useEffect, + useCallback, + ReactNode, + useRef, + useLayoutEffect, +} from "react"; import { Editor, Range, Extension } from "@tiptap/core"; import Suggestion from "@tiptap/suggestion"; import { ReactRenderer } from "@tiptap/react"; @@ -18,7 +25,18 @@ import { Table, } from "lucide-react"; import { UploadImage } from "../"; -import { cn, insertTableCommand, toggleBlockquote, toggleBulletList, toggleOrderedList, toggleTaskList, insertImageCommand, toggleHeadingOne, toggleHeadingTwo, toggleHeadingThree } from "@plane/editor-core"; +import { + cn, + insertTableCommand, + toggleBlockquote, + toggleBulletList, + toggleOrderedList, + toggleTaskList, + insertImageCommand, + toggleHeadingOne, + toggleHeadingTwo, + toggleHeadingThree, +} from "@plane/editor-core"; interface CommandItemProps { title: string; @@ -37,7 +55,15 @@ const Command = Extension.create({ return { suggestion: { char: "/", - command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { + command: ({ + editor, + range, + props, + }: { + editor: Editor; + range: Range; + props: any; + }) => { props.command({ editor, range }); }, }, @@ -59,127 +85,153 @@ const Command = Extension.create({ const getSuggestionItems = ( uploadFile: UploadImage, - setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void + setIsSubmitting?: ( + isSubmitting: "submitting" | "submitted" | "saved", + ) => void, ) => - ({ query }: { query: string }) => - [ - { - title: "Text", - description: "Just start typing with plain text.", - searchTerms: ["p", "paragraph"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); - }, + ({ query }: { query: string }) => + [ + { + title: "Text", + description: "Just start typing with plain text.", + searchTerms: ["p", "paragraph"], + icon: , + command: ({ editor, range }: CommandProps) => { + editor + .chain() + .focus() + .deleteRange(range) + .toggleNode("paragraph", "paragraph") + .run(); }, - { - title: "Heading 1", - description: "Big section heading.", - searchTerms: ["title", "big", "large"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleHeadingOne(editor, range); - }, + }, + { + title: "Heading 1", + description: "Big section heading.", + searchTerms: ["title", "big", "large"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingOne(editor, range); }, - { - title: "Heading 2", - description: "Medium section heading.", - searchTerms: ["subtitle", "medium"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleHeadingTwo(editor, range); - }, + }, + { + title: "Heading 2", + description: "Medium section heading.", + searchTerms: ["subtitle", "medium"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingTwo(editor, range); }, - { - title: "Heading 3", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleHeadingThree(editor, range); - }, + }, + { + title: "Heading 3", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingThree(editor, range); }, - { - title: "To-do List", - description: "Track tasks with a to-do list.", - searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleTaskList(editor, range) - }, + }, + { + title: "To-do List", + description: "Track tasks with a to-do list.", + searchTerms: ["todo", "task", "list", "check", "checkbox"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleTaskList(editor, range); }, - { - title: "Bullet List", - description: "Create a simple bullet list.", - searchTerms: ["unordered", "point"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleBulletList(editor, range); - }, + }, + { + title: "Bullet List", + description: "Create a simple bullet list.", + searchTerms: ["unordered", "point"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleBulletList(editor, range); }, - { - title: "Divider", - description: "Visually divide blocks", - searchTerms: ["line", "divider", "horizontal", "rule", "separate"], - icon: , - command: ({ editor, range }: CommandProps) => { - editor.chain().focus().deleteRange(range).setHorizontalRule().run(); - }, + }, + { + title: "Divider", + description: "Visually divide blocks", + searchTerms: ["line", "divider", "horizontal", "rule", "separate"], + icon: , + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setHorizontalRule().run(); }, - { - title: "Table", - description: "Create a Table", - searchTerms: ["table", "cell", "db", "data", "tabular"], - icon: , - command: ({ editor, range }: CommandProps) => { - insertTableCommand(editor, range); - }, + }, + { + title: "Table", + description: "Create a Table", + searchTerms: ["table", "cell", "db", "data", "tabular"], + icon:
, + command: ({ editor, range }: CommandProps) => { + insertTableCommand(editor, range); }, - { - title: "Numbered List", - description: "Create a list with numbering.", - searchTerms: ["ordered"], - icon: , - command: ({ editor, range }: CommandProps) => { - toggleOrderedList(editor, range) - }, + }, + { + title: "Numbered List", + description: "Create a list with numbering.", + searchTerms: ["ordered"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleOrderedList(editor, range); }, - { - title: "Quote", - description: "Capture a quote.", - searchTerms: ["blockquote"], - icon: , - command: ({ editor, range }: CommandProps) => - toggleBlockquote(editor, range) + }, + { + title: "Quote", + description: "Capture a quote.", + searchTerms: ["blockquote"], + icon: , + command: ({ editor, range }: CommandProps) => + toggleBlockquote(editor, range), + }, + { + title: "Code", + description: "Capture a code snippet.", + searchTerms: ["codeblock"], + icon: , + command: ({ editor, range }: CommandProps) => + editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), + }, + { + title: "Image", + description: "Upload an image from your computer.", + searchTerms: ["photo", "picture", "media"], + icon: , + command: ({ editor, range }: CommandProps) => { + insertImageCommand(editor, uploadFile, setIsSubmitting, range); }, - { - title: "Code", - description: "Capture a code snippet.", - searchTerms: ["codeblock"], - icon: , - command: ({ editor, range }: CommandProps) => - editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), + }, + { + title: "Iframe", + description: "Embed an iframe.", + searchTerms: ["youtube", "loom", "embed"], + icon: , + command: ({ editor, range }: CommandProps) => { + const url = window.prompt("URL"); + if (url) { + console.log(url); + editor + .chain() + .focus() + .deleteRange(range) + .setIframe({ src: url }) + .run(); + } }, - { - title: "Image", - description: "Upload an image from your computer.", - searchTerms: ["photo", "picture", "media"], - icon: , - command: ({ editor, range }: CommandProps) => { - insertImageCommand(editor, uploadFile, setIsSubmitting, range); - }, - }, - ].filter((item) => { - if (typeof query === "string" && query.length > 0) { - const search = query.toLowerCase(); - return ( - item.title.toLowerCase().includes(search) || - item.description.toLowerCase().includes(search) || - (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search))) - ); - } - return true; - }); + }, + ].filter((item) => { + if (typeof query === "string" && query.length > 0) { + const search = query.toLowerCase(); + return ( + item.title.toLowerCase().includes(search) || + item.description.toLowerCase().includes(search) || + (item.searchTerms && + item.searchTerms.some((term: string) => term.includes(search))) + ); + } + return true; + }); export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { const containerHeight = container.offsetHeight; @@ -213,7 +265,7 @@ const CommandList = ({ command(item); } }, - [command, items] + [command, items], ); useEffect(() => { @@ -266,7 +318,10 @@ const CommandList = ({