editor improvements

* add callout, youtube embed, image, video, table, detail, math
* fix attachments module
* other fixes
This commit is contained in:
Philipinho
2024-06-20 14:57:00 +01:00
parent c7925739cb
commit 1f4bd129a8
74 changed files with 5205 additions and 381 deletions

View File

@ -1,155 +1,277 @@
import {
IconBlockquote,
IconCheckbox, IconCode,
IconCaretRightFilled,
IconCheckbox,
IconCode,
IconH1,
IconH2,
IconH3,
IconInfoCircle,
IconList,
IconListNumbers, IconPhoto,
IconListNumbers,
IconMath,
IconMathFunction,
IconMovie,
IconPhoto,
IconTable,
IconTypography,
} from '@tabler/icons-react';
import { CommandProps, SlashMenuGroupedItemsType } from '@/features/editor/components/slash-menu/types';
} from "@tabler/icons-react";
import {
CommandProps,
SlashMenuGroupedItemsType,
} from "@/features/editor/components/slash-menu/types";
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
const CommandGroups: SlashMenuGroupedItemsType = {
basic: [
{
title: 'Text',
description: 'Just start typing with plain text.',
searchTerms: ['p', 'paragraph'],
title: "Text",
description: "Just start typing with plain text.",
searchTerms: ["p", "paragraph"],
icon: IconTypography,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleNode('paragraph', 'paragraph')
.toggleNode("paragraph", "paragraph")
.run();
},
},
{
title: 'To-do List',
description: 'Track tasks with a to-do list.',
searchTerms: ['todo', 'task', 'list', 'check', 'checkbox'],
title: "To-do list",
description: "Track tasks with a to-do list.",
searchTerms: ["todo", "task", "list", "check", "checkbox"],
icon: IconCheckbox,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleTaskList().run();
},
},
{
title: 'Heading 1',
description: 'Big section heading.',
searchTerms: ['title', 'big', 'large'],
title: "Heading 1",
description: "Big section heading.",
searchTerms: ["title", "big", "large"],
icon: IconH1,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 1 })
.setNode("heading", { level: 1 })
.run();
},
},
{
title: 'Heading 2',
description: 'Medium section heading.',
searchTerms: ['subtitle', 'medium'],
title: "Heading 2",
description: "Medium section heading.",
searchTerms: ["subtitle", "medium"],
icon: IconH2,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 2 })
.setNode("heading", { level: 2 })
.run();
},
},
{
title: 'Heading 3',
description: 'Small section heading.',
searchTerms: ['subtitle', 'small'],
title: "Heading 3",
description: "Small section heading.",
searchTerms: ["subtitle", "small"],
icon: IconH3,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode('heading', { level: 3 })
.setNode("heading", { level: 3 })
.run();
},
},
{
title: 'Bullet List',
description: 'Create a simple bullet list.',
searchTerms: ['unordered', 'point'],
title: "Bullet list",
description: "Create a simple bullet list.",
searchTerms: ["unordered", "point", "list"],
icon: IconList,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleBulletList().run();
},
},
{
title: 'Numbered List',
description: 'Create a list with numbering.',
searchTerms: ['ordered'],
title: "Numbered list",
description: "Create a list with numbering.",
searchTerms: ["numbered", "ordered", "list"],
icon: IconListNumbers,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
},
},
{
title: 'Quote',
description: 'Capture a quote.',
searchTerms: ['blockquote', 'quotes'],
title: "Quote",
description: "Create block quote.",
searchTerms: ["blockquote", "quotes"],
icon: IconBlockquote,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.toggleNode('paragraph', 'paragraph')
.toggleBlockquote()
.run(),
editor.chain().focus().deleteRange(range).toggleBlockquote().run(),
},
{
title: 'Code',
description: 'Capture a code snippet.',
searchTerms: ['codeblock'],
title: "Code",
description: "Capture a code snippet.",
searchTerms: ["codeblock"],
icon: IconCode,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
title: 'Image',
description: 'Upload an image from your computer.',
searchTerms: ['photo', 'picture', 'media'],
title: "Image",
description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"],
icon: IconPhoto,
command: ({ editor, range }: CommandProps) => {
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
const pageId = editor.storage?.pageId;
if (!pageId) return;
// upload image
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
//startImageUpload(file, editor.view, pos);
uploadImageAction(file, editor.view, pos, pageId);
}
};
input.click();
},
},
{
title: "Video",
description: "Upload an video from your computer.",
searchTerms: ["video", "mp4", "media"],
icon: IconMovie,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
const pageId = editor.storage?.pageId;
if (!pageId) return;
// upload video
const input = document.createElement("input");
input.type = "file";
input.accept = "video/*";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
uploadVideoAction(file, editor.view, pos, pageId);
}
};
input.click();
},
},
{
title: "Table",
description: "Insert a table.",
searchTerms: ["table", "rows", "columns"],
icon: IconTable,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.insertTable({ rows: 3, cols: 3, withHeaderRow: false })
.run(),
},
{
title: "Toggle block",
description: "Insert collapsible block.",
searchTerms: ["collapsible", "block", "toggle", "details", "expand"],
icon: IconCaretRightFilled,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleDetails().run(),
},
{
title: "Callout",
description: "Insert callout notice.",
searchTerms: [
"callout",
"notice",
"panel",
"info",
"warning",
"success",
"error",
"danger",
],
icon: IconInfoCircle,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleCallout().run(),
},
{
title: "Math inline",
description: "Insert inline math equation.",
searchTerms: [
"math",
"inline",
"mathinline",
"inlinemath",
"inline math",
"equation",
"katex",
"latex",
"tex",
],
icon: IconMathFunction,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.setMathInline()
.setNodeSelection(range.from)
.run(),
},
{
title: "Math block",
description: "Insert math equation",
searchTerms: [
"math",
"block",
"mathblock",
"block math",
"equation",
"katex",
"latex",
"tex",
],
icon: IconMath,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).setMathBlock().run(),
},
],
};
export const getSuggestionItems = ({ query }: { query: string }): SlashMenuGroupedItemsType => {
export const getSuggestionItems = ({
query,
}: {
query: string;
}): SlashMenuGroupedItemsType => {
const search = query.toLowerCase();
const filteredGroups: SlashMenuGroupedItemsType = {};
for (const [group, items] of Object.entries(CommandGroups)) {
const filteredItems = items.filter((item) => {
return item.title.toLowerCase().includes(search)
|| item.description.toLowerCase().includes(search)
|| (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)));
return (
item.title.toLowerCase().includes(search) ||
item.description.toLowerCase().includes(search) ||
(item.searchTerms &&
item.searchTerms.some((term: string) => term.includes(search)))
);
});
if (filteredItems.length) {