initialized loom embedding support

This commit is contained in:
Palanikannan1437 2023-10-30 10:00:10 +05:30
parent ce18387600
commit a4171552e1
3 changed files with 250 additions and 115 deletions

View File

@ -0,0 +1,76 @@
import { Node } from '@tiptap/core'
export interface IframeOptions {
allowFullscreen: boolean,
HTMLAttributes: {
[key: string]: any
},
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
iframe: {
/**
* Add an iframe
*/
setIframe: (options: { src: string }) => ReturnType,
}
}
}
export const EmbedExtension = Node.create<IframeOptions>({
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
},
}
},
})

View File

@ -8,6 +8,7 @@ import ts from "highlight.js/lib/languages/typescript";
import SlashCommand from "./slash-command"; import SlashCommand from "./slash-command";
import { UploadImage } from "../"; import { UploadImage } from "../";
import { EmbedExtension } from "./embed";
const lowlight = createLowlight(common) const lowlight = createLowlight(common)
lowlight.register("ts", ts); lowlight.register("ts", ts);
@ -39,6 +40,7 @@ export const RichTextEditorExtensions = (
class: "mb-6 border-t border-custom-border-300", class: "mb-6 border-t border-custom-border-300",
}, },
}), }),
EmbedExtension,
SlashCommand(uploadFile, setIsSubmitting), SlashCommand(uploadFile, setIsSubmitting),
CodeBlockLowlight.configure({ CodeBlockLowlight.configure({
lowlight, lowlight,

View File

@ -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 { Editor, Range, Extension } from "@tiptap/core";
import Suggestion from "@tiptap/suggestion"; import Suggestion from "@tiptap/suggestion";
import { ReactRenderer } from "@tiptap/react"; import { ReactRenderer } from "@tiptap/react";
@ -18,7 +25,18 @@ import {
Table, Table,
} from "lucide-react"; } from "lucide-react";
import { UploadImage } from "../"; 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 { interface CommandItemProps {
title: string; title: string;
@ -37,7 +55,15 @@ const Command = Extension.create({
return { return {
suggestion: { suggestion: {
char: "/", 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 }); props.command({ editor, range });
}, },
}, },
@ -59,127 +85,153 @@ const Command = Extension.create({
const getSuggestionItems = const getSuggestionItems =
( (
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void,
) => ) =>
({ query }: { query: string }) => ({ query }: { query: string }) =>
[ [
{ {
title: "Text", title: "Text",
description: "Just start typing with plain text.", description: "Just start typing with plain text.",
searchTerms: ["p", "paragraph"], searchTerms: ["p", "paragraph"],
icon: <Text size={18} />, icon: <Text size={18} />,
command: ({ editor, range }: CommandProps) => { command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run(); editor
}, .chain()
.focus()
.deleteRange(range)
.toggleNode("paragraph", "paragraph")
.run();
}, },
{ },
title: "Heading 1", {
description: "Big section heading.", title: "Heading 1",
searchTerms: ["title", "big", "large"], description: "Big section heading.",
icon: <Heading1 size={18} />, searchTerms: ["title", "big", "large"],
command: ({ editor, range }: CommandProps) => { icon: <Heading1 size={18} />,
toggleHeadingOne(editor, range); command: ({ editor, range }: CommandProps) => {
}, toggleHeadingOne(editor, range);
}, },
{ },
title: "Heading 2", {
description: "Medium section heading.", title: "Heading 2",
searchTerms: ["subtitle", "medium"], description: "Medium section heading.",
icon: <Heading2 size={18} />, searchTerms: ["subtitle", "medium"],
command: ({ editor, range }: CommandProps) => { icon: <Heading2 size={18} />,
toggleHeadingTwo(editor, range); command: ({ editor, range }: CommandProps) => {
}, toggleHeadingTwo(editor, range);
}, },
{ },
title: "Heading 3", {
description: "Small section heading.", title: "Heading 3",
searchTerms: ["subtitle", "small"], description: "Small section heading.",
icon: <Heading3 size={18} />, searchTerms: ["subtitle", "small"],
command: ({ editor, range }: CommandProps) => { icon: <Heading3 size={18} />,
toggleHeadingThree(editor, range); command: ({ editor, range }: CommandProps) => {
}, toggleHeadingThree(editor, range);
}, },
{ },
title: "To-do List", {
description: "Track tasks with a to-do list.", title: "To-do List",
searchTerms: ["todo", "task", "list", "check", "checkbox"], description: "Track tasks with a to-do list.",
icon: <CheckSquare size={18} />, searchTerms: ["todo", "task", "list", "check", "checkbox"],
command: ({ editor, range }: CommandProps) => { icon: <CheckSquare size={18} />,
toggleTaskList(editor, range) command: ({ editor, range }: CommandProps) => {
}, toggleTaskList(editor, range);
}, },
{ },
title: "Bullet List", {
description: "Create a simple bullet list.", title: "Bullet List",
searchTerms: ["unordered", "point"], description: "Create a simple bullet list.",
icon: <List size={18} />, searchTerms: ["unordered", "point"],
command: ({ editor, range }: CommandProps) => { icon: <List size={18} />,
toggleBulletList(editor, range); command: ({ editor, range }: CommandProps) => {
}, toggleBulletList(editor, range);
}, },
{ },
title: "Divider", {
description: "Visually divide blocks", title: "Divider",
searchTerms: ["line", "divider", "horizontal", "rule", "separate"], description: "Visually divide blocks",
icon: <MinusSquare size={18} />, searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
command: ({ editor, range }: CommandProps) => { icon: <MinusSquare size={18} />,
editor.chain().focus().deleteRange(range).setHorizontalRule().run(); command: ({ editor, range }: CommandProps) => {
}, editor.chain().focus().deleteRange(range).setHorizontalRule().run();
}, },
{ },
title: "Table", {
description: "Create a Table", title: "Table",
searchTerms: ["table", "cell", "db", "data", "tabular"], description: "Create a Table",
icon: <Table size={18} />, searchTerms: ["table", "cell", "db", "data", "tabular"],
command: ({ editor, range }: CommandProps) => { icon: <Table size={18} />,
insertTableCommand(editor, range); command: ({ editor, range }: CommandProps) => {
}, insertTableCommand(editor, range);
}, },
{ },
title: "Numbered List", {
description: "Create a list with numbering.", title: "Numbered List",
searchTerms: ["ordered"], description: "Create a list with numbering.",
icon: <ListOrdered size={18} />, searchTerms: ["ordered"],
command: ({ editor, range }: CommandProps) => { icon: <ListOrdered size={18} />,
toggleOrderedList(editor, range) command: ({ editor, range }: CommandProps) => {
}, toggleOrderedList(editor, range);
}, },
{ },
title: "Quote", {
description: "Capture a quote.", title: "Quote",
searchTerms: ["blockquote"], description: "Capture a quote.",
icon: <TextQuote size={18} />, searchTerms: ["blockquote"],
command: ({ editor, range }: CommandProps) => icon: <TextQuote size={18} />,
toggleBlockquote(editor, range) command: ({ editor, range }: CommandProps) =>
toggleBlockquote(editor, range),
},
{
title: "Code",
description: "Capture a code snippet.",
searchTerms: ["codeblock"],
icon: <Code size={18} />,
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: <ImageIcon size={18} />,
command: ({ editor, range }: CommandProps) => {
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
}, },
{ },
title: "Code", {
description: "Capture a code snippet.", title: "Iframe",
searchTerms: ["codeblock"], description: "Embed an iframe.",
icon: <Code size={18} />, searchTerms: ["youtube", "loom", "embed"],
command: ({ editor, range }: CommandProps) => icon: <ImageIcon size={18} />,
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), 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", ].filter((item) => {
description: "Upload an image from your computer.", if (typeof query === "string" && query.length > 0) {
searchTerms: ["photo", "picture", "media"], const search = query.toLowerCase();
icon: <ImageIcon size={18} />, return (
command: ({ editor, range }: CommandProps) => { item.title.toLowerCase().includes(search) ||
insertImageCommand(editor, uploadFile, setIsSubmitting, range); item.description.toLowerCase().includes(search) ||
}, (item.searchTerms &&
}, item.searchTerms.some((term: string) => term.includes(search)))
].filter((item) => { );
if (typeof query === "string" && query.length > 0) { }
const search = query.toLowerCase(); return true;
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) => { export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
const containerHeight = container.offsetHeight; const containerHeight = container.offsetHeight;
@ -213,7 +265,7 @@ const CommandList = ({
command(item); command(item);
} }
}, },
[command, items] [command, items],
); );
useEffect(() => { useEffect(() => {
@ -266,7 +318,10 @@ const CommandList = ({
<button <button
className={cn( className={cn(
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`, `flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
{ "bg-custom-primary-100/5 text-custom-text-100": index === selectedIndex } {
"bg-custom-primary-100/5 text-custom-text-100":
index === selectedIndex,
},
)} )}
key={index} key={index}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
@ -331,7 +386,9 @@ const renderItems = () => {
export const SlashCommand = ( export const SlashCommand = (
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void setIsSubmitting?: (
isSubmitting: "submitting" | "submitted" | "saved",
) => void,
) => ) =>
Command.configure({ Command.configure({
suggestion: { suggestion: {