forked from github/plane
initialized loom embedding support
This commit is contained in:
parent
ce18387600
commit
a4171552e1
76
packages/editor/rich-text-editor/src/ui/extensions/embed.ts
Normal file
76
packages/editor/rich-text-editor/src/ui/extensions/embed.ts
Normal 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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
@ -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,
|
||||||
|
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user