From 5506e774bbce27c93f34661578ece8de67c1d1fd Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Wed, 30 Oct 2024 13:39:51 +0200 Subject: [PATCH 01/58] component reorganization --- app/(main)/(auth)/_components/SignInForm.tsx | 8 +- app/(main)/(auth)/_components/SignUpForm.tsx | 8 +- .../_components/CreateStudyForm.tsx | 16 +- .../(dashboard)/_components/SignOutBtn.tsx | 5 +- .../_components/StudySwitcherClient.tsx | 6 +- app/(main)/(dashboard)/layout.tsx | 10 +- app/(main)/(dashboard)/page.tsx | 14 +- app/_components/LocaleSwitcherSelect.tsx | 4 +- app/_components/Providers.tsx | 14 +- components/{ui => }/Button.stories.tsx | 2 +- components/{ui => }/Button.tsx | 0 components/{ui => }/Card.stories.tsx | 3 +- components/{ui => }/Card.tsx | 8 +- components/{ui => }/CloseButton.tsx | 0 components/{ui => }/Popover.tsx | 6 +- components/{ui => }/PopoverBackdrop.tsx | 0 components/{ui => }/ProgressBar.stories.tsx | 0 components/{ui => }/ProgressBar.tsx | 0 components/Spotlight.tsx | 6 +- components/{ui => }/Tooltip.stories.tsx | 0 components/{ui => }/Tooltip.tsx | 0 .../block-editor/BlockEditor.stories.tsx | 27 ++ components/block-editor/BlockEditor.tsx | 12 + components/{ui => }/form/Form.stories.tsx | 0 components/{ui => }/form/Form.tsx | 0 components/{ui => }/form/Input.tsx | 0 components/{ui => }/form/Label.tsx | 4 +- components/{ui => }/form/RadioGroup.tsx | 0 components/{ui => }/form/Select.tsx | 8 +- components/{ui => }/form/SubmitButton.tsx | 2 +- components/{ui => }/form/Switch.tsx | 0 .../name-generator/NameGenerator.tsx | 14 +- components/interview/ui/HelpButton.tsx | 12 +- .../interview/ui/InterviewLocaleSwitcher.tsx | 4 +- components/interview/ui/Navigation.tsx | 17 +- components/interview/ui/NavigationButton.tsx | 4 +- components/layout/Section.tsx | 4 +- components/layout/Surface.stories.tsx | 4 +- components/onboard-wizard/WizardStep.tsx | 17 +- components/{ui => }/select.tsx | 0 .../BlockquoteFigure/BlockquoteFigure.ts | 80 ++++ .../BlockquoteFigure/Quote/Quote.ts | 31 ++ .../BlockquoteFigure/Quote/index.ts | 1 + .../QuoteCaption/QuoteCaption.ts | 54 +++ .../BlockquoteFigure/QuoteCaption/index.ts | 1 + .../extensions/BlockquoteFigure/index.ts | 1 + .../extensions/CodeBlock/CodeBlock.ts | 9 + .../extensions/CodeBlock/index.ts | 1 + .../extensions/Document/Document.ts | 7 + lib/block-editor/extensions/Document/index.ts | 1 + .../EmojiSuggestion/components/EmojiList.tsx | 106 ++++++ .../extensions/EmojiSuggestion/index.ts | 1 + .../extensions/EmojiSuggestion/suggestion.ts | 74 ++++ .../extensions/EmojiSuggestion/types.ts | 10 + .../extensions/Figcaption/Figcaption.ts | 90 +++++ .../extensions/Figcaption/index.ts | 1 + lib/block-editor/extensions/Figure/Figure.ts | 62 +++ lib/block-editor/extensions/Figure/index.ts | 1 + .../extensions/FontSize/FontSize.ts | 64 ++++ lib/block-editor/extensions/FontSize/index.ts | 1 + .../extensions/Heading/Heading.ts | 15 + lib/block-editor/extensions/Heading/index.ts | 1 + .../HorizontalRule/HorizontalRule.ts | 10 + .../extensions/HorizontalRule/index.ts | 1 + lib/block-editor/extensions/Image/Image.ts | 7 + lib/block-editor/extensions/Image/index.ts | 1 + .../extensions/ImageBlock/ImageBlock.ts | 103 +++++ .../ImageBlock/components/ImageBlockMenu.tsx | 98 +++++ .../ImageBlock/components/ImageBlockView.tsx | 45 +++ .../ImageBlock/components/ImageBlockWidth.tsx | 40 ++ .../extensions/ImageBlock/index.ts | 1 + lib/block-editor/extensions/Link/Link.ts | 39 ++ lib/block-editor/extensions/Link/index.ts | 1 + .../extensions/MultiColumn/Column.ts | 33 ++ .../extensions/MultiColumn/Columns.ts | 65 ++++ .../extensions/MultiColumn/index.ts | 2 + .../MultiColumn/menus/ColumnsMenu.tsx | 79 ++++ .../extensions/MultiColumn/menus/index.ts | 1 + .../extensions/Selection/Selection.ts | 36 ++ .../extensions/Selection/index.ts | 1 + .../extensions/SlashCommand/CommandButton.tsx | 33 ++ .../extensions/SlashCommand/MenuList.tsx | 148 ++++++++ .../extensions/SlashCommand/SlashCommand.ts | 259 +++++++++++++ .../extensions/SlashCommand/groups.ts | 165 ++++++++ .../extensions/SlashCommand/index.ts | 1 + .../extensions/SlashCommand/types.ts | 25 ++ lib/block-editor/extensions/Table/Cell.ts | 125 ++++++ lib/block-editor/extensions/Table/Header.ts | 89 +++++ lib/block-editor/extensions/Table/Row.ts | 8 + lib/block-editor/extensions/Table/Table.ts | 5 + lib/block-editor/extensions/Table/index.ts | 4 + .../Table/menus/TableColumn/index.tsx | 71 ++++ .../Table/menus/TableColumn/utils.ts | 38 ++ .../extensions/Table/menus/TableRow/index.tsx | 72 ++++ .../extensions/Table/menus/TableRow/utils.ts | 38 ++ .../extensions/Table/menus/index.tsx | 2 + lib/block-editor/extensions/Table/utils.ts | 251 +++++++++++++ .../extensions/TrailingNode/index.ts | 1 + .../extensions/TrailingNode/trailing-node.ts | 71 ++++ lib/block-editor/extensions/extension-kit.ts | 113 ++++++ lib/block-editor/extensions/index.ts | 48 +++ lib/block-editor/useBlockEditor.tsx | 29 ++ lib/dialogs/Dialog.stories.tsx | 6 +- lib/dialogs/Dialog.tsx | 8 +- lib/dialogs/DialogProvider.tsx | 8 +- lib/dialogs/useDialog.stories.tsx | 6 +- lib/localisation/config.ts | 2 +- lib/localisation/utils.ts | 6 +- package.json | 3 + pnpm-lock.yaml | 355 +++++++++++++++++- 110 files changed, 3282 insertions(+), 122 deletions(-) rename components/{ui => }/Button.stories.tsx (98%) rename components/{ui => }/Button.tsx (100%) rename components/{ui => }/Card.stories.tsx (94%) rename components/{ui => }/Card.tsx (86%) rename components/{ui => }/CloseButton.tsx (100%) rename components/{ui => }/Popover.tsx (95%) rename components/{ui => }/PopoverBackdrop.tsx (100%) rename components/{ui => }/ProgressBar.stories.tsx (100%) rename components/{ui => }/ProgressBar.tsx (100%) rename components/{ui => }/Tooltip.stories.tsx (100%) rename components/{ui => }/Tooltip.tsx (100%) create mode 100644 components/block-editor/BlockEditor.stories.tsx create mode 100644 components/block-editor/BlockEditor.tsx rename components/{ui => }/form/Form.stories.tsx (100%) rename components/{ui => }/form/Form.tsx (100%) rename components/{ui => }/form/Input.tsx (100%) rename components/{ui => }/form/Label.tsx (92%) rename components/{ui => }/form/RadioGroup.tsx (100%) rename components/{ui => }/form/Select.tsx (96%) rename components/{ui => }/form/SubmitButton.tsx (89%) rename components/{ui => }/form/Switch.tsx (100%) rename components/{ui => }/select.tsx (100%) create mode 100644 lib/block-editor/extensions/BlockquoteFigure/BlockquoteFigure.ts create mode 100644 lib/block-editor/extensions/BlockquoteFigure/Quote/Quote.ts create mode 100644 lib/block-editor/extensions/BlockquoteFigure/Quote/index.ts create mode 100644 lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/QuoteCaption.ts create mode 100644 lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/index.ts create mode 100644 lib/block-editor/extensions/BlockquoteFigure/index.ts create mode 100644 lib/block-editor/extensions/CodeBlock/CodeBlock.ts create mode 100644 lib/block-editor/extensions/CodeBlock/index.ts create mode 100644 lib/block-editor/extensions/Document/Document.ts create mode 100644 lib/block-editor/extensions/Document/index.ts create mode 100644 lib/block-editor/extensions/EmojiSuggestion/components/EmojiList.tsx create mode 100644 lib/block-editor/extensions/EmojiSuggestion/index.ts create mode 100644 lib/block-editor/extensions/EmojiSuggestion/suggestion.ts create mode 100644 lib/block-editor/extensions/EmojiSuggestion/types.ts create mode 100644 lib/block-editor/extensions/Figcaption/Figcaption.ts create mode 100644 lib/block-editor/extensions/Figcaption/index.ts create mode 100644 lib/block-editor/extensions/Figure/Figure.ts create mode 100644 lib/block-editor/extensions/Figure/index.ts create mode 100644 lib/block-editor/extensions/FontSize/FontSize.ts create mode 100644 lib/block-editor/extensions/FontSize/index.ts create mode 100644 lib/block-editor/extensions/Heading/Heading.ts create mode 100644 lib/block-editor/extensions/Heading/index.ts create mode 100644 lib/block-editor/extensions/HorizontalRule/HorizontalRule.ts create mode 100644 lib/block-editor/extensions/HorizontalRule/index.ts create mode 100644 lib/block-editor/extensions/Image/Image.ts create mode 100644 lib/block-editor/extensions/Image/index.ts create mode 100644 lib/block-editor/extensions/ImageBlock/ImageBlock.ts create mode 100644 lib/block-editor/extensions/ImageBlock/components/ImageBlockMenu.tsx create mode 100644 lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx create mode 100644 lib/block-editor/extensions/ImageBlock/components/ImageBlockWidth.tsx create mode 100644 lib/block-editor/extensions/ImageBlock/index.ts create mode 100644 lib/block-editor/extensions/Link/Link.ts create mode 100644 lib/block-editor/extensions/Link/index.ts create mode 100644 lib/block-editor/extensions/MultiColumn/Column.ts create mode 100644 lib/block-editor/extensions/MultiColumn/Columns.ts create mode 100644 lib/block-editor/extensions/MultiColumn/index.ts create mode 100644 lib/block-editor/extensions/MultiColumn/menus/ColumnsMenu.tsx create mode 100644 lib/block-editor/extensions/MultiColumn/menus/index.ts create mode 100644 lib/block-editor/extensions/Selection/Selection.ts create mode 100644 lib/block-editor/extensions/Selection/index.ts create mode 100644 lib/block-editor/extensions/SlashCommand/CommandButton.tsx create mode 100644 lib/block-editor/extensions/SlashCommand/MenuList.tsx create mode 100644 lib/block-editor/extensions/SlashCommand/SlashCommand.ts create mode 100644 lib/block-editor/extensions/SlashCommand/groups.ts create mode 100644 lib/block-editor/extensions/SlashCommand/index.ts create mode 100644 lib/block-editor/extensions/SlashCommand/types.ts create mode 100644 lib/block-editor/extensions/Table/Cell.ts create mode 100644 lib/block-editor/extensions/Table/Header.ts create mode 100644 lib/block-editor/extensions/Table/Row.ts create mode 100644 lib/block-editor/extensions/Table/Table.ts create mode 100644 lib/block-editor/extensions/Table/index.ts create mode 100644 lib/block-editor/extensions/Table/menus/TableColumn/index.tsx create mode 100644 lib/block-editor/extensions/Table/menus/TableColumn/utils.ts create mode 100644 lib/block-editor/extensions/Table/menus/TableRow/index.tsx create mode 100644 lib/block-editor/extensions/Table/menus/TableRow/utils.ts create mode 100644 lib/block-editor/extensions/Table/menus/index.tsx create mode 100644 lib/block-editor/extensions/Table/utils.ts create mode 100644 lib/block-editor/extensions/TrailingNode/index.ts create mode 100644 lib/block-editor/extensions/TrailingNode/trailing-node.ts create mode 100644 lib/block-editor/extensions/extension-kit.ts create mode 100644 lib/block-editor/extensions/index.ts create mode 100644 lib/block-editor/useBlockEditor.tsx diff --git a/app/(main)/(auth)/_components/SignInForm.tsx b/app/(main)/(auth)/_components/SignInForm.tsx index ebb80434..b5cfee93 100644 --- a/app/(main)/(auth)/_components/SignInForm.tsx +++ b/app/(main)/(auth)/_components/SignInForm.tsx @@ -1,9 +1,9 @@ -import { login } from '~/server/actions/auth'; -import { Input } from '~/components/ui/form/Input'; -import { SubmitButton } from '~/components/ui/form/SubmitButton'; -import Form from '~/components/ui/form/Form'; import { getTranslations } from 'next-intl/server'; +import Form from '~/components/form/Form'; +import { Input } from '~/components/form/Input'; +import { SubmitButton } from '~/components/form/SubmitButton'; import Link from '~/components/Link'; +import { login } from '~/server/actions/auth'; export default async function SignInForm() { const t = await getTranslations('Auth'); diff --git a/app/(main)/(auth)/_components/SignUpForm.tsx b/app/(main)/(auth)/_components/SignUpForm.tsx index 93c74678..fad7d1f2 100644 --- a/app/(main)/(auth)/_components/SignUpForm.tsx +++ b/app/(main)/(auth)/_components/SignUpForm.tsx @@ -1,11 +1,11 @@ 'use client'; +import { useTranslations } from 'next-intl'; import { useFormState } from 'react-dom'; +import Form from '~/components/form/Form'; +import { Input } from '~/components/form/Input'; +import { SubmitButton } from '~/components/form/SubmitButton'; import { signup } from '~/server/actions/auth'; -import { Input } from '~/components/ui/form/Input'; -import { useTranslations } from 'next-intl'; -import { SubmitButton } from '~/components/ui/form/SubmitButton'; -import Form from '~/components/ui/form/Form'; export default function SignUpForm() { const [formState, formAction] = useFormState(signup, { diff --git a/app/(main)/(dashboard)/_components/CreateStudyForm.tsx b/app/(main)/(dashboard)/_components/CreateStudyForm.tsx index e54d79a7..c73fbbf4 100644 --- a/app/(main)/(dashboard)/_components/CreateStudyForm.tsx +++ b/app/(main)/(dashboard)/_components/CreateStudyForm.tsx @@ -1,17 +1,17 @@ -import { createStudy } from '~/server/actions/study'; +import { Role } from '@prisma/client'; import { getTranslations } from 'next-intl/server'; -import { SubmitButton } from '~/components/ui/form/SubmitButton'; -import { Input } from '~/components/ui/form/Input'; +import Form from '~/components/form/Form'; +import { Input } from '~/components/form/Input'; +import { SubmitButton } from '~/components/form/SubmitButton'; import Section from '~/components/layout/Section'; import { Select, SelectContent, - SelectValue, - SelectTrigger, SelectItem, -} from '~/components/ui/select'; -import { Role } from '@prisma/client'; -import Form from '~/components/ui/form/Form'; + SelectTrigger, + SelectValue, +} from '~/components/select'; +import { createStudy } from '~/server/actions/study'; export default async function CreateStudyForm() { const t = await getTranslations('Components.CreateStudyForm'); diff --git a/app/(main)/(dashboard)/_components/SignOutBtn.tsx b/app/(main)/(dashboard)/_components/SignOutBtn.tsx index 49f7c985..4396b774 100644 --- a/app/(main)/(dashboard)/_components/SignOutBtn.tsx +++ b/app/(main)/(dashboard)/_components/SignOutBtn.tsx @@ -1,9 +1,8 @@ 'use client'; -import React from 'react'; -import { logout } from '~/server/actions/auth'; import { useTranslations } from 'next-intl'; -import { SubmitButton } from '~/components/ui/form/SubmitButton'; +import { SubmitButton } from '~/components/form/SubmitButton'; +import { logout } from '~/server/actions/auth'; export default function SignOutBtn() { const t = useTranslations('Auth.SignOut'); diff --git a/app/(main)/(dashboard)/_components/StudySwitcherClient.tsx b/app/(main)/(dashboard)/_components/StudySwitcherClient.tsx index c3d73afd..d2cfc092 100644 --- a/app/(main)/(dashboard)/_components/StudySwitcherClient.tsx +++ b/app/(main)/(dashboard)/_components/StudySwitcherClient.tsx @@ -1,8 +1,9 @@ 'use client'; import { type Study } from '@prisma/client'; -import { useParams, useRouter } from 'next/navigation'; import { useTranslations } from 'next-intl'; +import { useParams, useRouter } from 'next/navigation'; +import { route } from 'nextjs-routes'; import { useEffect, useState } from 'react'; import { Select, @@ -10,8 +11,7 @@ import { SelectItem, SelectTrigger, SelectValue, -} from '~/components/ui/select'; -import { route } from 'nextjs-routes'; +} from '~/components/select'; export default function StudySwitcherClient({ studies }: { studies: Study[] }) { const t = useTranslations('Components.StudySwitcher'); diff --git a/app/(main)/(dashboard)/layout.tsx b/app/(main)/(dashboard)/layout.tsx index 20fba013..2628fbba 100644 --- a/app/(main)/(dashboard)/layout.tsx +++ b/app/(main)/(dashboard)/layout.tsx @@ -1,12 +1,12 @@ import { SearchIcon } from 'lucide-react'; -import { Input } from '~/components/ui/form/Input'; import Image from 'next/image'; -import StudySwitcher from './_components/StudySwitcher'; import LanguageSwitcher from '~/app/_components/LocaleSwitcher'; -import SignOutBtn from './_components/SignOutBtn'; -import { requirePageAuth } from '~/lib/auth'; -import ResponsiveContainer from '~/components/layout/ResponsiveContainer'; import ThemeSwitcher from '~/app/_components/ThemeSwitcher'; +import { Input } from '~/components/form/Input'; +import ResponsiveContainer from '~/components/layout/ResponsiveContainer'; +import { requirePageAuth } from '~/lib/auth'; +import SignOutBtn from './_components/SignOutBtn'; +import StudySwitcher from './_components/StudySwitcher'; export default async function DashboardLayout({ children, diff --git a/app/(main)/(dashboard)/page.tsx b/app/(main)/(dashboard)/page.tsx index 7200f860..90af0e10 100644 --- a/app/(main)/(dashboard)/page.tsx +++ b/app/(main)/(dashboard)/page.tsx @@ -1,15 +1,15 @@ -import { getUserStudies } from '~/server/queries/studies'; import { getTranslations } from 'next-intl/server'; -import Link from '~/components/Link'; -import UnorderedList from '~/components/typography/UnorderedList'; +import { route } from 'nextjs-routes'; +import { Button } from '~/components/Button'; import Section from '~/components/layout/Section'; +import Link from '~/components/Link'; import PageHeader from '~/components/typography/PageHeader'; +import Paragraph from '~/components/typography/Paragraph'; +import UnorderedList from '~/components/typography/UnorderedList'; +import { requirePageAuth } from '~/lib/auth'; import { getInterviews } from '~/server/queries/interviews'; +import { getUserStudies } from '~/server/queries/studies'; import CreateStudyForm from './_components/CreateStudyForm'; -import { requirePageAuth } from '~/lib/auth'; -import { Button } from '~/components/ui/Button'; -import Paragraph from '~/components/typography/Paragraph'; -import { route } from 'nextjs-routes'; export default async function Dashboard() { await requirePageAuth(); diff --git a/app/_components/LocaleSwitcherSelect.tsx b/app/_components/LocaleSwitcherSelect.tsx index 019a6bb2..72694e04 100644 --- a/app/_components/LocaleSwitcherSelect.tsx +++ b/app/_components/LocaleSwitcherSelect.tsx @@ -1,14 +1,14 @@ 'use client'; +import { useTransition } from 'react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '~/components/ui/select'; +} from '~/components/select'; import { type Locale } from '~/lib/localisation/config'; -import { useTransition } from 'react'; import { setUserLocale } from '~/lib/localisation/locale'; import { getLocaleRecordsFromCodes } from '~/lib/localisation/utils'; diff --git a/app/_components/Providers.tsx b/app/_components/Providers.tsx index b3414e3b..c5968979 100644 --- a/app/_components/Providers.tsx +++ b/app/_components/Providers.tsx @@ -1,13 +1,13 @@ +import { MotionConfig } from 'framer-motion'; import { type AbstractIntlMessages } from 'next-intl'; -import { type ReactNode } from 'react'; -import RadixDirectionProvider from './RadixDirectionProvider'; -import { TooltipProvider } from '~/components/ui/Tooltip'; -import { type Locale } from '~/lib/localisation/config'; -import IntlProvider from './IntlProvider'; import { ThemeProvider } from 'next-themes'; -import { WizardProvider } from '~/lib/onboarding-wizard/Provider'; -import { MotionConfig } from 'framer-motion'; +import { type ReactNode } from 'react'; +import { TooltipProvider } from '~/components/Tooltip'; import DialogProvider from '~/lib/dialogs/DialogProvider'; +import { WizardProvider } from '~/lib/onboarding-wizard/Provider'; +import { type Locale } from '~/schemas/protocol/i18n'; +import IntlProvider from './IntlProvider'; +import RadixDirectionProvider from './RadixDirectionProvider'; export default function Providers({ intlParams: { dir, messages, locale, now, timeZone }, diff --git a/components/ui/Button.stories.tsx b/components/Button.stories.tsx similarity index 98% rename from components/ui/Button.stories.tsx rename to components/Button.stories.tsx index 064aad2a..527a6621 100644 --- a/components/ui/Button.stories.tsx +++ b/components/Button.stories.tsx @@ -1,12 +1,12 @@ import type { Meta } from '@storybook/react'; import { fn } from '@storybook/test'; +import Heading from '~/components/typography/Heading'; import { Button, type ButtonProps, type ButtonVariants, buttonVariants, } from './Button'; -import Heading from '../typography/Heading'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { diff --git a/components/ui/Button.tsx b/components/Button.tsx similarity index 100% rename from components/ui/Button.tsx rename to components/Button.tsx diff --git a/components/ui/Card.stories.tsx b/components/Card.stories.tsx similarity index 94% rename from components/ui/Card.stories.tsx rename to components/Card.stories.tsx index 8a034d8c..ce2d2e7f 100644 --- a/components/ui/Card.stories.tsx +++ b/components/Card.stories.tsx @@ -1,9 +1,8 @@ // Card.stories.tsx -import React from 'react'; import type { Meta, StoryFn } from '@storybook/react'; +import Paragraph from '~/components/typography/Paragraph'; import { Card, type CardProps } from './Card'; -import Paragraph from '../typography/Paragraph'; // Meta configuration for Storybook const meta: Meta = { diff --git a/components/ui/Card.tsx b/components/Card.tsx similarity index 86% rename from components/ui/Card.tsx rename to components/Card.tsx index 866f8fc4..86b64a70 100644 --- a/components/ui/Card.tsx +++ b/components/Card.tsx @@ -1,11 +1,11 @@ 'use client'; import * as React from 'react'; -import { cn } from '~/lib/utils'; +import Divider from '~/components/layout/Divider'; +import Surface from '~/components/layout/Surface'; import Heading from '~/components/typography/Heading'; -import Paragraph from '../typography/Paragraph'; -import Surface from '../layout/Surface'; -import Divider from '../layout/Divider'; +import Paragraph from '~/components/typography/Paragraph'; +import { cn } from '~/lib/utils'; export type CardProps = { title: string; diff --git a/components/ui/CloseButton.tsx b/components/CloseButton.tsx similarity index 100% rename from components/ui/CloseButton.tsx rename to components/CloseButton.tsx diff --git a/components/ui/Popover.tsx b/components/Popover.tsx similarity index 95% rename from components/ui/Popover.tsx rename to components/Popover.tsx index 37ecd38a..e2dbd969 100644 --- a/components/ui/Popover.tsx +++ b/components/Popover.tsx @@ -1,9 +1,9 @@ -import { type PropsWithChildren } from 'react'; import * as PopoverPrimitive from '@radix-ui/react-popover'; +import { type PropsWithChildren } from 'react'; +import { MotionSurface } from '~/components/layout/Surface'; +import Heading from '~/components/typography/Heading'; import { cn } from '~/lib/utils'; import CloseButton from './CloseButton'; -import Heading from '../typography/Heading'; -import { MotionSurface } from '../layout/Surface'; const Popover = ({ children, diff --git a/components/ui/PopoverBackdrop.tsx b/components/PopoverBackdrop.tsx similarity index 100% rename from components/ui/PopoverBackdrop.tsx rename to components/PopoverBackdrop.tsx diff --git a/components/ui/ProgressBar.stories.tsx b/components/ProgressBar.stories.tsx similarity index 100% rename from components/ui/ProgressBar.stories.tsx rename to components/ProgressBar.stories.tsx diff --git a/components/ui/ProgressBar.tsx b/components/ProgressBar.tsx similarity index 100% rename from components/ui/ProgressBar.tsx rename to components/ProgressBar.tsx diff --git a/components/Spotlight.tsx b/components/Spotlight.tsx index 61971e9c..4a3fa8fa 100644 --- a/components/Spotlight.tsx +++ b/components/Spotlight.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useState } from 'react'; -import { createPortal } from 'react-dom'; import * as motion from 'framer-motion/client'; -import PopoverBackdrop from './ui/PopoverBackdrop'; +import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import PopoverBackdrop from '~/components/PopoverBackdrop'; /** * Overlay that darkens the background and highlights a specific element. diff --git a/components/ui/Tooltip.stories.tsx b/components/Tooltip.stories.tsx similarity index 100% rename from components/ui/Tooltip.stories.tsx rename to components/Tooltip.stories.tsx diff --git a/components/ui/Tooltip.tsx b/components/Tooltip.tsx similarity index 100% rename from components/ui/Tooltip.tsx rename to components/Tooltip.tsx diff --git a/components/block-editor/BlockEditor.stories.tsx b/components/block-editor/BlockEditor.stories.tsx new file mode 100644 index 00000000..d720f573 --- /dev/null +++ b/components/block-editor/BlockEditor.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import BlockEditor from './BlockEditor'; + +const meta: Meta = { + title: 'Systems/BlockEditor', + component: BlockEditor, + parameters: { + nextjs: { + appDirectory: 'true', + }, + layout: 'fullscreen', + }, + decorators: [ + (Story, _context) => { + return ( +
+ +
+ ); + }, + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/components/block-editor/BlockEditor.tsx b/components/block-editor/BlockEditor.tsx new file mode 100644 index 00000000..b89d5f87 --- /dev/null +++ b/components/block-editor/BlockEditor.tsx @@ -0,0 +1,12 @@ +'use client'; + +import { EditorContent } from '@tiptap/react'; +import { useBlockEditor } from '~/lib/block-editor/useBlockEditor'; + +const BlockEditor = () => { + const { editor } = useBlockEditor(); + + return ; +}; + +export default BlockEditor; diff --git a/components/ui/form/Form.stories.tsx b/components/form/Form.stories.tsx similarity index 100% rename from components/ui/form/Form.stories.tsx rename to components/form/Form.stories.tsx diff --git a/components/ui/form/Form.tsx b/components/form/Form.tsx similarity index 100% rename from components/ui/form/Form.tsx rename to components/form/Form.tsx diff --git a/components/ui/form/Input.tsx b/components/form/Input.tsx similarity index 100% rename from components/ui/form/Input.tsx rename to components/form/Input.tsx diff --git a/components/ui/form/Label.tsx b/components/form/Label.tsx similarity index 92% rename from components/ui/form/Label.tsx rename to components/form/Label.tsx index 9bfcf60e..1ad5d3a9 100644 --- a/components/ui/form/Label.tsx +++ b/components/form/Label.tsx @@ -1,11 +1,11 @@ 'use client'; -import * as React from 'react'; import * as LabelPrimitive from '@radix-ui/react-label'; +import * as React from 'react'; import { type VariantProps } from 'tailwind-variants'; +import { headingVariants } from '~/components/typography/Heading'; import { cn } from '~/lib/utils'; -import { headingVariants } from '../../typography/Heading'; const Label = React.forwardRef< React.ElementRef, diff --git a/components/ui/form/RadioGroup.tsx b/components/form/RadioGroup.tsx similarity index 100% rename from components/ui/form/RadioGroup.tsx rename to components/form/RadioGroup.tsx diff --git a/components/ui/form/Select.tsx b/components/form/Select.tsx similarity index 96% rename from components/ui/form/Select.tsx rename to components/form/Select.tsx index bd617167..83c57731 100644 --- a/components/ui/form/Select.tsx +++ b/components/form/Select.tsx @@ -1,11 +1,11 @@ +import type * as SelectPrimitive from '@radix-ui/react-select'; import { Select, SelectContent, - SelectValue, - SelectTrigger, SelectItem, -} from '~/components/ui/select'; -import type * as SelectPrimitive from '@radix-ui/react-select'; + SelectTrigger, + SelectValue, +} from '~/components/select'; type SimpleSelectProps = { options: { label: string; value: string }[]; diff --git a/components/ui/form/SubmitButton.tsx b/components/form/SubmitButton.tsx similarity index 89% rename from components/ui/form/SubmitButton.tsx rename to components/form/SubmitButton.tsx index 8e3d5abc..5549f780 100644 --- a/components/ui/form/SubmitButton.tsx +++ b/components/form/SubmitButton.tsx @@ -2,7 +2,7 @@ import { Loader2 } from 'lucide-react'; import { useFormStatus } from 'react-dom'; -import { Button } from '~/components/ui/Button'; +import { Button } from '~/components/Button'; export function SubmitButton({ children }: React.PropsWithChildren) { const { pending } = useFormStatus(); diff --git a/components/ui/form/Switch.tsx b/components/form/Switch.tsx similarity index 100% rename from components/ui/form/Switch.tsx rename to components/form/Switch.tsx diff --git a/components/interview/interfaces/name-generator/NameGenerator.tsx b/components/interview/interfaces/name-generator/NameGenerator.tsx index 1075facd..b7f4a6e3 100644 --- a/components/interview/interfaces/name-generator/NameGenerator.tsx +++ b/components/interview/interfaces/name-generator/NameGenerator.tsx @@ -2,17 +2,15 @@ * Building blocks for NameGenerator interface */ -import Prompts from '~/components/interview/Prompts/Prompts'; -import NodePanels from './NodePanels'; import NodeList from '~/components/interview/NodeList'; -import QuickNodeForm from './QuickNodeForm'; -import { cn } from '~/lib/utils'; -import { interfaceWrapperClasses } from '../../ui/SimpleShell'; -import { withOnboardingWizard } from '~/components/onboard-wizard/withOnboardingWizard'; -import { type InterviewStage } from '../../ui/InterviewShell'; -import { useTranslations } from 'next-intl'; +import Prompts from '~/components/interview/Prompts/Prompts'; +import { type InterviewStage } from '~/components/interview/ui/InterviewShell'; +import { interfaceWrapperClasses } from '~/components/interview/ui/SimpleShell'; import devProtocol from '~/lib/db/sample-data/dev-protocol'; +import { cn } from '~/lib/utils'; import { type NameGeneratorInterface } from '~/schemas/protocol/interfaces/name-generator'; +import NodePanels from './NodePanels'; +import QuickNodeForm from './QuickNodeForm'; const demoNodes = [ { diff --git a/components/interview/ui/HelpButton.tsx b/components/interview/ui/HelpButton.tsx index 2b8e0244..a7408e9d 100644 --- a/components/interview/ui/HelpButton.tsx +++ b/components/interview/ui/HelpButton.tsx @@ -1,14 +1,14 @@ import { HelpCircle } from 'lucide-react'; -import { NavButtonWithTooltip } from './NavigationButton'; import { useTranslations } from 'next-intl'; +import { Button } from '~/components/Button'; +import { Card } from '~/components/Card'; +import Form from '~/components/form/Form'; import { useWizardController } from '~/components/onboard-wizard/useWizardController'; -import { env } from '~/env'; -import { WIZARD_LOCAL_STORAGE_KEY } from '~/lib/onboarding-wizard/Provider'; import { renderLocalisedValue } from '~/components/RenderRichText'; +import { env } from '~/env'; import { useDialog } from '~/lib/dialogs/DialogProvider'; -import { Button } from '~/components/ui/Button'; -import { Card } from '~/components/ui/Card'; -import Form from '~/components/ui/form/Form'; +import { WIZARD_LOCAL_STORAGE_KEY } from '~/lib/onboarding-wizard/Provider'; +import { NavButtonWithTooltip } from './NavigationButton'; /** * Button to be added to the main navigation, which triggers the help popover. * This popover allows the participant to trigger help wizards. diff --git a/components/interview/ui/InterviewLocaleSwitcher.tsx b/components/interview/ui/InterviewLocaleSwitcher.tsx index b23652ea..1d8b3c7b 100644 --- a/components/interview/ui/InterviewLocaleSwitcher.tsx +++ b/components/interview/ui/InterviewLocaleSwitcher.tsx @@ -1,12 +1,12 @@ import { SelectTrigger } from '@radix-ui/react-select'; +import { Globe } from 'lucide-react'; import { useLocale, useTranslations } from 'next-intl'; import { useTransition } from 'react'; -import { Select, SelectContent, SelectItem } from '~/components/ui/select'; +import { Select, SelectContent, SelectItem } from '~/components/select'; import { type Locale } from '~/lib/localisation/config'; import { setUserLocale } from '~/lib/localisation/locale'; import { getLocaleRecordsFromCodes } from '~/lib/localisation/utils'; import { NavButtonWithTooltip } from './NavigationButton'; -import { Globe } from 'lucide-react'; export default function InterviewLocaleSwitcher({ codes, diff --git a/components/interview/ui/Navigation.tsx b/components/interview/ui/Navigation.tsx index 6a8bc38e..17ec54e7 100644 --- a/components/interview/ui/Navigation.tsx +++ b/components/interview/ui/Navigation.tsx @@ -1,24 +1,23 @@ 'use client'; import { - ChevronUp, ChevronDown, ChevronLeft, ChevronRight, + ChevronUp, } from 'lucide-react'; import { useTranslations } from 'next-intl'; -import { useSearchParams } from 'next/navigation'; -import { usePathname, useRouter } from 'next/navigation'; -import { cn } from '~/lib/utils'; -import { ProgressBarWithTooltip } from '../../ui/ProgressBar'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import type { IntRange } from 'type-fest'; -import HelpButton from './HelpButton'; -import { NavButtonWithTooltip } from './NavigationButton'; +import { ProgressBarWithTooltip } from '~/components/ProgressBar'; import Surface from '~/components/layout/Surface'; -import { type Locale } from '~/lib/localisation/config'; +import { withOnboardingWizard } from '~/components/onboard-wizard/withOnboardingWizard'; import { useMediaQuery } from '~/hooks/useMediaQuery'; +import { cn } from '~/lib/utils'; +import { type Locale } from '~/schemas/protocol/i18n'; +import HelpButton from './HelpButton'; import InterviewLocaleSwitcher from './InterviewLocaleSwitcher'; -import { withOnboardingWizard } from '~/components/onboard-wizard/withOnboardingWizard'; +import { NavButtonWithTooltip } from './NavigationButton'; type NavigationProps = { pulseNext: boolean; diff --git a/components/interview/ui/NavigationButton.tsx b/components/interview/ui/NavigationButton.tsx index 9553a230..988c88c1 100644 --- a/components/interview/ui/NavigationButton.tsx +++ b/components/interview/ui/NavigationButton.tsx @@ -1,5 +1,5 @@ -import { Button, type ButtonProps } from '~/components/ui/Button'; -import { withTooltip } from '~/components/ui/Tooltip'; +import { Button, type ButtonProps } from '~/components/Button'; +import { withTooltip } from '~/components/Tooltip'; import { cn } from '~/lib/utils'; const NavigationButton = (props: ButtonProps) => { diff --git a/components/layout/Section.tsx b/components/layout/Section.tsx index f086fd89..c6964254 100644 --- a/components/layout/Section.tsx +++ b/components/layout/Section.tsx @@ -1,8 +1,8 @@ 'use client'; -import Heading from '../typography/Heading'; -import { cn } from '~/lib/utils'; import { useId } from 'react'; +import Heading from '~/components/typography/Heading'; +import { cn } from '~/lib/utils'; import Surface, { type SurfaceVariants } from './Surface'; const sectionClasses = 'rounded mb-10'; diff --git a/components/layout/Surface.stories.tsx b/components/layout/Surface.stories.tsx index 148be014..d666932c 100644 --- a/components/layout/Surface.stories.tsx +++ b/components/layout/Surface.stories.tsx @@ -1,9 +1,9 @@ // src/components/Surface.stories.tsx -import React, { type ElementType } from 'react'; import type { Meta, StoryFn } from '@storybook/react'; +import { type ElementType } from 'react'; +import { Button } from '~/components/Button'; import Surface, { MotionSurface, type SurfaceVariants } from './Surface'; -import { Button } from '../ui/Button'; // Define the metadata for the Storybook const meta: Meta = { diff --git a/components/onboard-wizard/WizardStep.tsx b/components/onboard-wizard/WizardStep.tsx index 0758c5ce..fa45d464 100644 --- a/components/onboard-wizard/WizardStep.tsx +++ b/components/onboard-wizard/WizardStep.tsx @@ -1,15 +1,14 @@ -import Popover from '~/components/ui/Popover'; -import { Button } from '../ui/Button'; -import { useWizardController } from './useWizardController'; -import RenderRichText from '../RenderRichText'; import { useTranslations } from 'next-intl'; -import { useElementPosition } from '~/lib/onboarding-wizard/utils'; -import { type Step } from '~/lib/onboarding-wizard/store'; -import Form from '../ui/form/Form'; -import { generatePublicId } from '~/lib/generatePublicId'; +import { Button } from '~/components/Button'; +import Form from '~/components/form/Form'; +import Popover from '~/components/Popover'; import { ControlledDialog } from '~/lib/dialogs/ControlledDialog'; +import { generatePublicId } from '~/lib/generatePublicId'; +import { useElementPosition } from '~/lib/onboarding-wizard/utils'; +import RenderRichText from '../RenderRichText'; +import { useWizardController } from './useWizardController'; -export default function WizardStep({ step }: { step: Step }) { +export default function WizardStep({ step }: { step }) { const { title, content, targetElementId } = step; const { diff --git a/components/ui/select.tsx b/components/select.tsx similarity index 100% rename from components/ui/select.tsx rename to components/select.tsx diff --git a/lib/block-editor/extensions/BlockquoteFigure/BlockquoteFigure.ts b/lib/block-editor/extensions/BlockquoteFigure/BlockquoteFigure.ts new file mode 100644 index 00000000..a0316437 --- /dev/null +++ b/lib/block-editor/extensions/BlockquoteFigure/BlockquoteFigure.ts @@ -0,0 +1,80 @@ +import { mergeAttributes } from '@tiptap/core' +import { Figure } from '../Figure' +import { Quote } from './Quote' +import { QuoteCaption } from './QuoteCaption' + +declare module '@tiptap/core' { + // eslint-disable-next-line no-unused-vars + interface Commands { + blockquoteFigure: { + setBlockquote: () => ReturnType + } + } +} + +export const BlockquoteFigure = Figure.extend({ + name: 'blockquoteFigure', + + group: 'block', + + content: 'quote quoteCaption', + + isolating: true, + + addExtensions() { + return [Quote, QuoteCaption] + }, + + renderHTML({ HTMLAttributes }) { + return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), ['div', {}, 0]] + }, + + addKeyboardShortcuts() { + return { + Enter: () => false, + } + }, + + addAttributes() { + return { + ...this.parent?.(), + } + }, + + addCommands() { + return { + setBlockquote: + () => + ({ state, chain }) => { + const position = state.selection.$from.start() + const selectionContent = state.selection.content() + + return chain() + .focus() + .insertContent({ + type: this.name, + content: [ + { + type: 'quote', + content: selectionContent.content.toJSON() || [ + { + type: 'paragraph', + attrs: { + textAlign: 'left', + }, + }, + ], + }, + { + type: 'quoteCaption', + }, + ], + }) + .focus(position + 1) + .run() + }, + } + }, +}) + +export default BlockquoteFigure diff --git a/lib/block-editor/extensions/BlockquoteFigure/Quote/Quote.ts b/lib/block-editor/extensions/BlockquoteFigure/Quote/Quote.ts new file mode 100644 index 00000000..cc2023ee --- /dev/null +++ b/lib/block-editor/extensions/BlockquoteFigure/Quote/Quote.ts @@ -0,0 +1,31 @@ +import { Node } from '@tiptap/core' + +export const Quote = Node.create({ + name: 'quote', + + content: 'paragraph+', + + defining: true, + + marks: '', + + parseHTML() { + return [ + { + tag: 'blockquote', + }, + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['blockquote', HTMLAttributes, 0] + }, + + addKeyboardShortcuts() { + return { + Backspace: () => false, + } + }, +}) + +export default Quote diff --git a/lib/block-editor/extensions/BlockquoteFigure/Quote/index.ts b/lib/block-editor/extensions/BlockquoteFigure/Quote/index.ts new file mode 100644 index 00000000..2cfc86d0 --- /dev/null +++ b/lib/block-editor/extensions/BlockquoteFigure/Quote/index.ts @@ -0,0 +1 @@ +export * from './Quote' diff --git a/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/QuoteCaption.ts b/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/QuoteCaption.ts new file mode 100644 index 00000000..f625b157 --- /dev/null +++ b/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/QuoteCaption.ts @@ -0,0 +1,54 @@ +import { Node } from '@tiptap/core' + +export const QuoteCaption = Node.create({ + name: 'quoteCaption', + + group: 'block', + + content: 'text*', + + defining: true, + + isolating: true, + + parseHTML() { + return [ + { + tag: 'figcaption', + }, + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['figcaption', HTMLAttributes, 0] + }, + + addKeyboardShortcuts() { + return { + // On Enter at the end of line, create new paragraph and focus + Enter: ({ editor }) => { + const { + state: { + selection: { $from, empty }, + }, + } = editor + + if (!empty || $from.parent.type !== this.type) { + return false + } + + const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2 + + if (!isAtEnd) { + return false + } + + const pos = editor.state.selection.$from.end() + + return editor.chain().focus(pos).insertContentAt(pos, { type: 'paragraph' }).run() + }, + } + }, +}) + +export default QuoteCaption diff --git a/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/index.ts b/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/index.ts new file mode 100644 index 00000000..9a5f4f98 --- /dev/null +++ b/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/index.ts @@ -0,0 +1 @@ +export * from './QuoteCaption' diff --git a/lib/block-editor/extensions/BlockquoteFigure/index.ts b/lib/block-editor/extensions/BlockquoteFigure/index.ts new file mode 100644 index 00000000..9c78a829 --- /dev/null +++ b/lib/block-editor/extensions/BlockquoteFigure/index.ts @@ -0,0 +1 @@ +export * from './BlockquoteFigure' diff --git a/lib/block-editor/extensions/CodeBlock/CodeBlock.ts b/lib/block-editor/extensions/CodeBlock/CodeBlock.ts new file mode 100644 index 00000000..7b915be0 --- /dev/null +++ b/lib/block-editor/extensions/CodeBlock/CodeBlock.ts @@ -0,0 +1,9 @@ +import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight' +import { all, createLowlight } from 'lowlight' + +const lowlight = createLowlight(all) + +export const CodeBlock = CodeBlockLowlight.configure({ + lowlight, + defaultLanguage: 'javascript', +}) diff --git a/lib/block-editor/extensions/CodeBlock/index.ts b/lib/block-editor/extensions/CodeBlock/index.ts new file mode 100644 index 00000000..412da358 --- /dev/null +++ b/lib/block-editor/extensions/CodeBlock/index.ts @@ -0,0 +1 @@ +export * from './CodeBlock' diff --git a/lib/block-editor/extensions/Document/Document.ts b/lib/block-editor/extensions/Document/Document.ts new file mode 100644 index 00000000..239abf5e --- /dev/null +++ b/lib/block-editor/extensions/Document/Document.ts @@ -0,0 +1,7 @@ +import { Document as TiptapDocument } from '@tiptap/extension-document' + +export const Document = TiptapDocument.extend({ + content: '(block|columns)+', +}) + +export default Document diff --git a/lib/block-editor/extensions/Document/index.ts b/lib/block-editor/extensions/Document/index.ts new file mode 100644 index 00000000..6b7b839d --- /dev/null +++ b/lib/block-editor/extensions/Document/index.ts @@ -0,0 +1 @@ +export * from './Document' diff --git a/lib/block-editor/extensions/EmojiSuggestion/components/EmojiList.tsx b/lib/block-editor/extensions/EmojiSuggestion/components/EmojiList.tsx new file mode 100644 index 00000000..209776f3 --- /dev/null +++ b/lib/block-editor/extensions/EmojiSuggestion/components/EmojiList.tsx @@ -0,0 +1,106 @@ +import { EmojiItem } from '@tiptap-pro/extension-emoji' +import React, { ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react' + +import { Button } from '@/components/ui/Button' +import { Panel } from '@/components/ui/Panel' +import { EmojiListProps } from '../types' +import { SuggestionKeyDownProps } from '@tiptap/suggestion' + +const EmojiList = forwardRef( + (props: EmojiListProps, ref: ForwardedRef<{ onKeyDown: (evt: SuggestionKeyDownProps) => boolean }>) => { + const [selectedIndex, setSelectedIndex] = useState(0) + + useEffect(() => setSelectedIndex(0), [props.items]) + + const selectItem = useCallback( + (index: number) => { + const item = props.items[index] + + if (item) { + props.command({ name: item.name }) + } + }, + [props], + ) + + useImperativeHandle(ref, () => { + const scrollIntoView = (index: number) => { + const item = props.items[index] + + if (item) { + const node = document.querySelector(`[data-emoji-name="${item.name}"]`) + + if (node) { + node.scrollIntoView({ block: 'nearest' }) + } + } + } + + const upHandler = () => { + const newIndex = (selectedIndex + props.items.length - 1) % props.items.length + setSelectedIndex(newIndex) + scrollIntoView(newIndex) + } + + const downHandler = () => { + const newIndex = (selectedIndex + 1) % props.items.length + setSelectedIndex(newIndex) + scrollIntoView(newIndex) + } + + const enterHandler = () => { + selectItem(selectedIndex) + } + + return { + onKeyDown: ({ event }) => { + if (event.key === 'ArrowUp') { + upHandler() + return true + } + + if (event.key === 'ArrowDown') { + downHandler() + return true + } + + if (event.key === 'Enter') { + enterHandler() + return true + } + + return false + }, + } + }, [props, selectedIndex, selectItem]) + + const createClickHandler = useCallback((index: number) => () => selectItem(index), [selectItem]) + + if (!props.items || !props.items.length) { + return null + } + + return ( + + {props.items.map((item: EmojiItem, index: number) => ( + + ))} + + ) + }, +) + +EmojiList.displayName = 'EmojiList' + +export default EmojiList diff --git a/lib/block-editor/extensions/EmojiSuggestion/index.ts b/lib/block-editor/extensions/EmojiSuggestion/index.ts new file mode 100644 index 00000000..a7051661 --- /dev/null +++ b/lib/block-editor/extensions/EmojiSuggestion/index.ts @@ -0,0 +1 @@ +export * from './suggestion' diff --git a/lib/block-editor/extensions/EmojiSuggestion/suggestion.ts b/lib/block-editor/extensions/EmojiSuggestion/suggestion.ts new file mode 100644 index 00000000..0b9196c7 --- /dev/null +++ b/lib/block-editor/extensions/EmojiSuggestion/suggestion.ts @@ -0,0 +1,74 @@ +import { ReactRenderer } from '@tiptap/react' +import { Editor } from '@tiptap/core' +import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion' +import tippy, { Instance } from 'tippy.js' + +import EmojiList from './components/EmojiList' +import { KeyboardEvent, RefAttributes } from 'react' +import { EmojiListProps } from './types' + +export const emojiSuggestion = { + items: ({ editor, query }: { editor: Editor; query: string }) => + editor.storage.emoji.emojis + .filter( + ({ shortcodes, tags }: { shortcodes: string[]; tags: string[] }) => + shortcodes.find(shortcode => shortcode.startsWith(query.toLowerCase())) || + tags.find(tag => tag.startsWith(query.toLowerCase())), + ) + .slice(0, 250), + + allowSpaces: false, + + render: () => { + let component: ReactRenderer< + { onKeyDown: (evt: SuggestionKeyDownProps) => boolean }, + EmojiListProps & RefAttributes<{ onKeyDown: (evt: SuggestionKeyDownProps) => boolean }> + > + let popup: ReturnType + + return { + onStart: (props: SuggestionProps) => { + component = new ReactRenderer(EmojiList, { + props, + editor: props.editor, + }) + + popup = tippy('body', { + getReferenceClientRect: props.clientRect as () => DOMRect, + appendTo: () => document.body, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: 'manual', + placement: 'bottom-start', + }) + }, + + onUpdate(props: SuggestionProps) { + component.updateProps(props) + + popup[0].setProps({ + getReferenceClientRect: props.clientRect as () => DOMRect, + }) + }, + + onKeyDown(props: SuggestionKeyDownProps) { + if (props.event.key === 'Escape') { + popup[0].hide() + component.destroy() + + return true + } + + return component.ref?.onKeyDown(props) ?? false + }, + + onExit() { + popup[0].destroy() + component.destroy() + }, + } + }, +} + +export default emojiSuggestion diff --git a/lib/block-editor/extensions/EmojiSuggestion/types.ts b/lib/block-editor/extensions/EmojiSuggestion/types.ts new file mode 100644 index 00000000..da1b7a57 --- /dev/null +++ b/lib/block-editor/extensions/EmojiSuggestion/types.ts @@ -0,0 +1,10 @@ +import { EmojiItem } from '@tiptap-pro/extension-emoji' + +export interface Command { + name: string +} + +export interface EmojiListProps { + command: (command: Command) => void + items: EmojiItem[] +} diff --git a/lib/block-editor/extensions/Figcaption/Figcaption.ts b/lib/block-editor/extensions/Figcaption/Figcaption.ts new file mode 100644 index 00000000..c05bfc5a --- /dev/null +++ b/lib/block-editor/extensions/Figcaption/Figcaption.ts @@ -0,0 +1,90 @@ +import { mergeAttributes, Node } from '@tiptap/core' + +import { Image } from '../Image' + +export const Figcaption = Node.create({ + name: 'figcaption', + + addOptions() { + return { + HTMLAttributes: {}, + } + }, + + content: 'inline*', + + selectable: false, + + draggable: false, + + marks: 'link', + + parseHTML() { + return [ + { + tag: 'figcaption', + }, + ] + }, + + addKeyboardShortcuts() { + return { + // On Enter at the end of line, create new paragraph and focus + Enter: ({ editor }) => { + const { + state: { + selection: { $from, empty }, + }, + } = editor + + if (!empty || $from.parent.type !== this.type) { + return false + } + + const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2 + + if (!isAtEnd) { + return false + } + + const pos = editor.state.selection.$from.end() + + return editor.chain().focus(pos).insertContentAt(pos, { type: 'paragraph' }).run() + }, + + // On Backspace at the beginning of line, + // dont delete content of image before + Backspace: ({ editor }) => { + const { + state: { + selection: { $from, empty }, + }, + } = editor + + if (!empty || $from.parent.type !== this.type) { + return false + } + + const isAtStart = $from.parentOffset === 0 + + if (!isAtStart) { + return false + } + + // if the node before is of type image, don't do anything + const nodeBefore = editor.state.doc.nodeAt($from.pos - 2) + if (nodeBefore?.type.name === Image.name) { + return true + } + + return false + }, + } + }, + + renderHTML({ HTMLAttributes }) { + return ['figcaption', mergeAttributes(HTMLAttributes), 0] + }, +}) + +export default Figcaption diff --git a/lib/block-editor/extensions/Figcaption/index.ts b/lib/block-editor/extensions/Figcaption/index.ts new file mode 100644 index 00000000..da786d04 --- /dev/null +++ b/lib/block-editor/extensions/Figcaption/index.ts @@ -0,0 +1 @@ +export * from './Figcaption' diff --git a/lib/block-editor/extensions/Figure/Figure.ts b/lib/block-editor/extensions/Figure/Figure.ts new file mode 100644 index 00000000..afa98ce8 --- /dev/null +++ b/lib/block-editor/extensions/Figure/Figure.ts @@ -0,0 +1,62 @@ +import { mergeAttributes, Node } from '@tiptap/core' +import { Plugin } from '@tiptap/pm/state' + +export const Figure = Node.create({ + name: 'figure', + + addOptions() { + return { + HTMLAttributes: {}, + } + }, + + group: 'block', + + content: 'block figcaption', + + draggable: true, + + defining: true, + + selectable: true, + + parseHTML() { + return [ + { + tag: `figure[data-type="${this.name}"]`, + }, + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), 0] + }, + + addProseMirrorPlugins() { + return [ + new Plugin({ + props: { + handleDOMEvents: { + // Prevent dragging child nodes from figure + dragstart: (view, event) => { + if (!event.target) { + return false + } + + const pos = view.posAtDOM(event.target as HTMLElement, 0) + const $pos = view.state.doc.resolve(pos) + + if ($pos.parent.type.name === this.type.name) { + event.preventDefault() + } + + return false + }, + }, + }, + }), + ] + }, +}) + +export default Figure diff --git a/lib/block-editor/extensions/Figure/index.ts b/lib/block-editor/extensions/Figure/index.ts new file mode 100644 index 00000000..7ddab7db --- /dev/null +++ b/lib/block-editor/extensions/Figure/index.ts @@ -0,0 +1 @@ +export * from './Figure' diff --git a/lib/block-editor/extensions/FontSize/FontSize.ts b/lib/block-editor/extensions/FontSize/FontSize.ts new file mode 100644 index 00000000..77f117c1 --- /dev/null +++ b/lib/block-editor/extensions/FontSize/FontSize.ts @@ -0,0 +1,64 @@ +import { Attributes, Extension } from '@tiptap/core' +import '@tiptap/extension-text-style' + +declare module '@tiptap/core' { + interface Commands { + fontSize: { + setFontSize: (size: string) => ReturnType + unsetFontSize: () => ReturnType + } + } +} + +export const FontSize = Extension.create({ + name: 'fontSize', + + addOptions() { + return { + types: ['textStyle'], + } + }, + + addGlobalAttributes() { + return [ + { + types: ['paragraph'], + attributes: { + class: {}, + }, + }, + { + types: this.options.types, + attributes: { + fontSize: { + parseHTML: element => element.style.fontSize.replace(/['"]+/g, ''), + renderHTML: attributes => { + if (!attributes.fontSize) { + return {} + } + + return { + style: `font-size: ${attributes.fontSize}`, + } + }, + }, + } as Attributes, + }, + ] + }, + + addCommands() { + return { + setFontSize: + (fontSize: string) => + ({ chain }) => + chain().setMark('textStyle', { fontSize }).run(), + unsetFontSize: + () => + ({ chain }) => + chain().setMark('textStyle', { fontSize: null }).removeEmptyTextStyle().run(), + } + }, +}) + +export default FontSize diff --git a/lib/block-editor/extensions/FontSize/index.ts b/lib/block-editor/extensions/FontSize/index.ts new file mode 100644 index 00000000..818343ef --- /dev/null +++ b/lib/block-editor/extensions/FontSize/index.ts @@ -0,0 +1 @@ +export * from './FontSize' diff --git a/lib/block-editor/extensions/Heading/Heading.ts b/lib/block-editor/extensions/Heading/Heading.ts new file mode 100644 index 00000000..6a68b5fa --- /dev/null +++ b/lib/block-editor/extensions/Heading/Heading.ts @@ -0,0 +1,15 @@ +import { mergeAttributes } from '@tiptap/core' +import TiptapHeading from '@tiptap/extension-heading' +import type { Level } from '@tiptap/extension-heading' + +export const Heading = TiptapHeading.extend({ + renderHTML({ node, HTMLAttributes }) { + const nodeLevel = parseInt(node.attrs.level, 10) as Level + const hasLevel = this.options.levels.includes(nodeLevel) + const level = hasLevel ? nodeLevel : this.options.levels[0] + + return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + }, +}) + +export default Heading diff --git a/lib/block-editor/extensions/Heading/index.ts b/lib/block-editor/extensions/Heading/index.ts new file mode 100644 index 00000000..dd0dbbaa --- /dev/null +++ b/lib/block-editor/extensions/Heading/index.ts @@ -0,0 +1 @@ +export * from './Heading' diff --git a/lib/block-editor/extensions/HorizontalRule/HorizontalRule.ts b/lib/block-editor/extensions/HorizontalRule/HorizontalRule.ts new file mode 100644 index 00000000..650e6e3d --- /dev/null +++ b/lib/block-editor/extensions/HorizontalRule/HorizontalRule.ts @@ -0,0 +1,10 @@ +import { mergeAttributes } from '@tiptap/core' +import TiptapHorizontalRule from '@tiptap/extension-horizontal-rule' + +export const HorizontalRule = TiptapHorizontalRule.extend({ + renderHTML() { + return ['div', mergeAttributes(this.options.HTMLAttributes, { 'data-type': this.name }), ['hr']] + }, +}) + +export default HorizontalRule diff --git a/lib/block-editor/extensions/HorizontalRule/index.ts b/lib/block-editor/extensions/HorizontalRule/index.ts new file mode 100644 index 00000000..65fc6ace --- /dev/null +++ b/lib/block-editor/extensions/HorizontalRule/index.ts @@ -0,0 +1 @@ +export * from './HorizontalRule' diff --git a/lib/block-editor/extensions/Image/Image.ts b/lib/block-editor/extensions/Image/Image.ts new file mode 100644 index 00000000..cd33a575 --- /dev/null +++ b/lib/block-editor/extensions/Image/Image.ts @@ -0,0 +1,7 @@ +import { Image as BaseImage } from '@tiptap/extension-image' + +export const Image = BaseImage.extend({ + group: 'block', +}) + +export default Image diff --git a/lib/block-editor/extensions/Image/index.ts b/lib/block-editor/extensions/Image/index.ts new file mode 100644 index 00000000..072b1619 --- /dev/null +++ b/lib/block-editor/extensions/Image/index.ts @@ -0,0 +1 @@ +export * from './Image' diff --git a/lib/block-editor/extensions/ImageBlock/ImageBlock.ts b/lib/block-editor/extensions/ImageBlock/ImageBlock.ts new file mode 100644 index 00000000..7185a750 --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/ImageBlock.ts @@ -0,0 +1,103 @@ +import { ReactNodeViewRenderer } from '@tiptap/react' +import { mergeAttributes, Range } from '@tiptap/core' + +import { ImageBlockView } from './components/ImageBlockView' +import { Image } from '../Image' + +declare module '@tiptap/core' { + interface Commands { + imageBlock: { + setImageBlock: (attributes: { src: string }) => ReturnType + setImageBlockAt: (attributes: { src: string; pos: number | Range }) => ReturnType + setImageBlockAlign: (align: 'left' | 'center' | 'right') => ReturnType + setImageBlockWidth: (width: number) => ReturnType + } + } +} + +export const ImageBlock = Image.extend({ + name: 'imageBlock', + + group: 'block', + + defining: true, + + isolating: true, + + addAttributes() { + return { + src: { + default: '', + parseHTML: element => element.getAttribute('src'), + renderHTML: attributes => ({ + src: attributes.src, + }), + }, + width: { + default: '100%', + parseHTML: element => element.getAttribute('data-width'), + renderHTML: attributes => ({ + 'data-width': attributes.width, + }), + }, + align: { + default: 'center', + parseHTML: element => element.getAttribute('data-align'), + renderHTML: attributes => ({ + 'data-align': attributes.align, + }), + }, + alt: { + default: undefined, + parseHTML: element => element.getAttribute('alt'), + renderHTML: attributes => ({ + alt: attributes.alt, + }), + }, + } + }, + + parseHTML() { + return [ + { + tag: 'img[src*="tiptap.dev"]:not([src^="data:"]), img[src*="windows.net"]:not([src^="data:"])', + }, + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)] + }, + + addCommands() { + return { + setImageBlock: + attrs => + ({ commands }) => { + return commands.insertContent({ type: 'imageBlock', attrs: { src: attrs.src } }) + }, + + setImageBlockAt: + attrs => + ({ commands }) => { + return commands.insertContentAt(attrs.pos, { type: 'imageBlock', attrs: { src: attrs.src } }) + }, + + setImageBlockAlign: + align => + ({ commands }) => + commands.updateAttributes('imageBlock', { align }), + + setImageBlockWidth: + width => + ({ commands }) => + commands.updateAttributes('imageBlock', { width: `${Math.max(0, Math.min(100, width))}%` }), + } + }, + + addNodeView() { + return ReactNodeViewRenderer(ImageBlockView) + }, +}) + +export default ImageBlock diff --git a/lib/block-editor/extensions/ImageBlock/components/ImageBlockMenu.tsx b/lib/block-editor/extensions/ImageBlock/components/ImageBlockMenu.tsx new file mode 100644 index 00000000..719a3bbf --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/components/ImageBlockMenu.tsx @@ -0,0 +1,98 @@ +import { BubbleMenu as BaseBubbleMenu, useEditorState } from '@tiptap/react' +import React, { useCallback, useRef } from 'react' +import { Instance, sticky } from 'tippy.js' +import { v4 as uuid } from 'uuid' + +import { Toolbar } from '@/components/ui/Toolbar' +import { Icon } from '@/components/ui/Icon' +import { ImageBlockWidth } from './ImageBlockWidth' +import { MenuProps } from '@/components/menus/types' +import { getRenderContainer } from '@/lib/utils' + +export const ImageBlockMenu = ({ editor, appendTo }: MenuProps): JSX.Element => { + const menuRef = useRef(null) + const tippyInstance = useRef(null) + + const getReferenceClientRect = useCallback(() => { + const renderContainer = getRenderContainer(editor, 'node-imageBlock') + const rect = renderContainer?.getBoundingClientRect() || new DOMRect(-1000, -1000, 0, 0) + + return rect + }, [editor]) + + const shouldShow = useCallback(() => { + const isActive = editor.isActive('imageBlock') + + return isActive + }, [editor]) + + const onAlignImageLeft = useCallback(() => { + editor.chain().focus(undefined, { scrollIntoView: false }).setImageBlockAlign('left').run() + }, [editor]) + + const onAlignImageCenter = useCallback(() => { + editor.chain().focus(undefined, { scrollIntoView: false }).setImageBlockAlign('center').run() + }, [editor]) + + const onAlignImageRight = useCallback(() => { + editor.chain().focus(undefined, { scrollIntoView: false }).setImageBlockAlign('right').run() + }, [editor]) + + const onWidthChange = useCallback( + (value: number) => { + editor.chain().focus(undefined, { scrollIntoView: false }).setImageBlockWidth(value).run() + }, + [editor], + ) + const { isImageCenter, isImageLeft, isImageRight, width } = useEditorState({ + editor, + selector: ctx => { + return { + isImageLeft: ctx.editor.isActive('imageBlock', { align: 'left' }), + isImageCenter: ctx.editor.isActive('imageBlock', { align: 'center' }), + isImageRight: ctx.editor.isActive('imageBlock', { align: 'right' }), + width: parseInt(ctx.editor.getAttributes('imageBlock')?.width || 0), + } + }, + }) + + return ( + { + tippyInstance.current = instance + }, + appendTo: () => { + return appendTo?.current + }, + plugins: [sticky], + sticky: 'popper', + }} + > + + + + + + + + + + + + + + + ) +} + +export default ImageBlockMenu diff --git a/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx b/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx new file mode 100644 index 00000000..08fc6ac2 --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx @@ -0,0 +1,45 @@ +import { cn } from '@/lib/utils' +import { Node } from '@tiptap/pm/model' +import { Editor, NodeViewWrapper } from '@tiptap/react' +import { useCallback, useRef } from 'react' + +interface ImageBlockViewProps { + editor: Editor + getPos: () => number + node: Node + updateAttributes: (attrs: Record) => void +} + +export const ImageBlockView = (props: ImageBlockViewProps) => { + const { editor, getPos, node } = props as ImageBlockViewProps & { + node: Node & { + attrs: { + src: string + } + } + } + const imageWrapperRef = useRef(null) + const { src } = node.attrs + + const wrapperClassName = cn( + node.attrs.align === 'left' ? 'ml-0' : 'ml-auto', + node.attrs.align === 'right' ? 'mr-0' : 'mr-auto', + node.attrs.align === 'center' && 'mx-auto', + ) + + const onClick = useCallback(() => { + editor.commands.setNodeSelection(getPos()) + }, [getPos, editor.commands]) + + return ( + +
+
+ +
+
+
+ ) +} + +export default ImageBlockView diff --git a/lib/block-editor/extensions/ImageBlock/components/ImageBlockWidth.tsx b/lib/block-editor/extensions/ImageBlock/components/ImageBlockWidth.tsx new file mode 100644 index 00000000..5edea3a9 --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/components/ImageBlockWidth.tsx @@ -0,0 +1,40 @@ +import { memo, useCallback, useEffect, useState } from 'react' + +export type ImageBlockWidthProps = { + onChange: (value: number) => void + value: number +} + +export const ImageBlockWidth = memo(({ onChange, value }: ImageBlockWidthProps) => { + const [currentValue, setCurrentValue] = useState(value) + + useEffect(() => { + setCurrentValue(value) + }, [value]) + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const nextValue = parseInt(e.target.value) + onChange(nextValue) + setCurrentValue(nextValue) + }, + [onChange], + ) + + return ( +
+ + {value}% +
+ ) +}) + +ImageBlockWidth.displayName = 'ImageBlockWidth' diff --git a/lib/block-editor/extensions/ImageBlock/index.ts b/lib/block-editor/extensions/ImageBlock/index.ts new file mode 100644 index 00000000..e870ec86 --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/index.ts @@ -0,0 +1 @@ +export * from './ImageBlock' diff --git a/lib/block-editor/extensions/Link/Link.ts b/lib/block-editor/extensions/Link/Link.ts new file mode 100644 index 00000000..6dafcd90 --- /dev/null +++ b/lib/block-editor/extensions/Link/Link.ts @@ -0,0 +1,39 @@ +import { mergeAttributes } from '@tiptap/core' +import TiptapLink from '@tiptap/extension-link' +import { Plugin } from '@tiptap/pm/state' +import { EditorView } from '@tiptap/pm/view' + +export const Link = TiptapLink.extend({ + inclusive: false, + + parseHTML() { + return [{ tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])' }] + }, + + renderHTML({ HTMLAttributes }) { + return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { class: 'link' }), 0] + }, + + addProseMirrorPlugins() { + const { editor } = this + + return [ + ...(this.parent?.() || []), + new Plugin({ + props: { + handleKeyDown: (view: EditorView, event: KeyboardEvent) => { + const { selection } = editor.state + + if (event.key === 'Escape' && selection.empty !== true) { + editor.commands.focus(selection.to, { scrollIntoView: false }) + } + + return false + }, + }, + }), + ] + }, +}) + +export default Link diff --git a/lib/block-editor/extensions/Link/index.ts b/lib/block-editor/extensions/Link/index.ts new file mode 100644 index 00000000..9378deb1 --- /dev/null +++ b/lib/block-editor/extensions/Link/index.ts @@ -0,0 +1 @@ +export * from './Link' diff --git a/lib/block-editor/extensions/MultiColumn/Column.ts b/lib/block-editor/extensions/MultiColumn/Column.ts new file mode 100644 index 00000000..3fb08c69 --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/Column.ts @@ -0,0 +1,33 @@ +import { Node, mergeAttributes } from '@tiptap/core' + +export const Column = Node.create({ + name: 'column', + + content: 'block+', + + isolating: true, + + addAttributes() { + return { + position: { + default: '', + parseHTML: element => element.getAttribute('data-position'), + renderHTML: attributes => ({ 'data-position': attributes.position }), + }, + } + }, + + renderHTML({ HTMLAttributes }) { + return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'column' }), 0] + }, + + parseHTML() { + return [ + { + tag: 'div[data-type="column"]', + }, + ] + }, +}) + +export default Column diff --git a/lib/block-editor/extensions/MultiColumn/Columns.ts b/lib/block-editor/extensions/MultiColumn/Columns.ts new file mode 100644 index 00000000..817596dd --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/Columns.ts @@ -0,0 +1,65 @@ +import { Node } from '@tiptap/core' + +export enum ColumnLayout { + SidebarLeft = 'sidebar-left', + SidebarRight = 'sidebar-right', + TwoColumn = 'two-column', +} + +declare module '@tiptap/core' { + interface Commands { + columns: { + setColumns: () => ReturnType + setLayout: (layout: ColumnLayout) => ReturnType + } + } +} + +export const Columns = Node.create({ + name: 'columns', + + group: 'columns', + + content: 'column column', + + defining: true, + + isolating: true, + + addAttributes() { + return { + layout: { + default: ColumnLayout.TwoColumn, + }, + } + }, + + addCommands() { + return { + setColumns: + () => + ({ commands }) => + commands.insertContent( + `

`, + ), + setLayout: + (layout: ColumnLayout) => + ({ commands }) => + commands.updateAttributes('columns', { layout }), + } + }, + + renderHTML({ HTMLAttributes }) { + return ['div', { 'data-type': 'columns', class: `layout-${HTMLAttributes.layout}` }, 0] + }, + + parseHTML() { + return [ + { + tag: 'div[data-type="columns"]', + }, + ] + }, +}) + +export default Columns diff --git a/lib/block-editor/extensions/MultiColumn/index.ts b/lib/block-editor/extensions/MultiColumn/index.ts new file mode 100644 index 00000000..8537f09a --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/index.ts @@ -0,0 +1,2 @@ +export * from './Columns' +export * from './Column' diff --git a/lib/block-editor/extensions/MultiColumn/menus/ColumnsMenu.tsx b/lib/block-editor/extensions/MultiColumn/menus/ColumnsMenu.tsx new file mode 100644 index 00000000..ca6d390f --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/menus/ColumnsMenu.tsx @@ -0,0 +1,79 @@ +import { BubbleMenu as BaseBubbleMenu, useEditorState } from '@tiptap/react' +import { useCallback } from 'react' +import { sticky } from 'tippy.js' +import { v4 as uuid } from 'uuid' + +import { MenuProps } from '@/components/menus/types' +import { getRenderContainer } from '@/lib/utils/getRenderContainer' +import { Toolbar } from '@/components/ui/Toolbar' +import { ColumnLayout } from '../Columns' +import { Icon } from '@/components/ui/Icon' + +export const ColumnsMenu = ({ editor, appendTo }: MenuProps) => { + const getReferenceClientRect = useCallback(() => { + const renderContainer = getRenderContainer(editor, 'columns') + const rect = renderContainer?.getBoundingClientRect() || new DOMRect(-1000, -1000, 0, 0) + + return rect + }, [editor]) + + const shouldShow = useCallback(() => { + const isColumns = editor.isActive('columns') + return isColumns + }, [editor]) + + const onColumnLeft = useCallback(() => { + editor.chain().focus().setLayout(ColumnLayout.SidebarLeft).run() + }, [editor]) + + const onColumnRight = useCallback(() => { + editor.chain().focus().setLayout(ColumnLayout.SidebarRight).run() + }, [editor]) + + const onColumnTwo = useCallback(() => { + editor.chain().focus().setLayout(ColumnLayout.TwoColumn).run() + }, [editor]) + const { isColumnLeft, isColumnRight, isColumnTwo } = useEditorState({ + editor, + selector: ctx => { + return { + isColumnLeft: ctx.editor.isActive('columns', { layout: ColumnLayout.SidebarLeft }), + isColumnRight: ctx.editor.isActive('columns', { layout: ColumnLayout.SidebarRight }), + isColumnTwo: ctx.editor.isActive('columns', { layout: ColumnLayout.TwoColumn }), + } + }, + }) + + return ( + appendTo?.current, + plugins: [sticky], + sticky: 'popper', + }} + > + + + + + + + + + + + + + ) +} + +export default ColumnsMenu diff --git a/lib/block-editor/extensions/MultiColumn/menus/index.ts b/lib/block-editor/extensions/MultiColumn/menus/index.ts new file mode 100644 index 00000000..5a7b3232 --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/menus/index.ts @@ -0,0 +1 @@ +export * from './ColumnsMenu' diff --git a/lib/block-editor/extensions/Selection/Selection.ts b/lib/block-editor/extensions/Selection/Selection.ts new file mode 100644 index 00000000..19c7b529 --- /dev/null +++ b/lib/block-editor/extensions/Selection/Selection.ts @@ -0,0 +1,36 @@ +import { Extension } from '@tiptap/core' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' + +export const Selection = Extension.create({ + name: 'selection', + + addProseMirrorPlugins() { + const { editor } = this + + return [ + new Plugin({ + key: new PluginKey('selection'), + props: { + decorations(state) { + if (state.selection.empty) { + return null + } + + if (editor.isFocused === true) { + return null + } + + return DecorationSet.create(state.doc, [ + Decoration.inline(state.selection.from, state.selection.to, { + class: 'selection', + }), + ]) + }, + }, + }), + ] + }, +}) + +export default Selection diff --git a/lib/block-editor/extensions/Selection/index.ts b/lib/block-editor/extensions/Selection/index.ts new file mode 100644 index 00000000..9279d55f --- /dev/null +++ b/lib/block-editor/extensions/Selection/index.ts @@ -0,0 +1 @@ +export * from './Selection' diff --git a/lib/block-editor/extensions/SlashCommand/CommandButton.tsx b/lib/block-editor/extensions/SlashCommand/CommandButton.tsx new file mode 100644 index 00000000..5a969b01 --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/CommandButton.tsx @@ -0,0 +1,33 @@ +import { forwardRef } from 'react' +import { cn } from '@/lib/utils' +import { icons } from 'lucide-react' +import { Icon } from '@/components/ui/Icon' + +export type CommandButtonProps = { + active?: boolean + description: string + icon: keyof typeof icons + onClick: () => void + title: string +} + +export const CommandButton = forwardRef( + ({ active, icon, onClick, title }, ref) => { + const wrapperClass = cn( + 'flex text-neutral-500 items-center text-xs font-semibold justify-start p-1.5 gap-2 rounded', + !active && 'bg-transparent hover:bg-neutral-50 hover:text-black', + active && 'bg-neutral-100 text-black hover:bg-neutral-100', + ) + + return ( + + ) + }, +) + +CommandButton.displayName = 'CommandButton' diff --git a/lib/block-editor/extensions/SlashCommand/MenuList.tsx b/lib/block-editor/extensions/SlashCommand/MenuList.tsx new file mode 100644 index 00000000..e5cdeb8d --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/MenuList.tsx @@ -0,0 +1,148 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react' + +import { Command, MenuListProps } from './types' +import { CommandButton } from './CommandButton' +import { Surface } from '@/components/ui/Surface' +import { DropdownButton } from '@/components/ui/Dropdown' +import { Icon } from '@/components/ui/Icon' + +export const MenuList = React.forwardRef((props: MenuListProps, ref) => { + const scrollContainer = useRef(null) + const activeItem = useRef(null) + const [selectedGroupIndex, setSelectedGroupIndex] = useState(0) + const [selectedCommandIndex, setSelectedCommandIndex] = useState(0) + + // Anytime the groups change, i.e. the user types to narrow it down, we want to + // reset the current selection to the first menu item + useEffect(() => { + setSelectedGroupIndex(0) + setSelectedCommandIndex(0) + }, [props.items]) + + const selectItem = useCallback( + (groupIndex: number, commandIndex: number) => { + const command = props.items[groupIndex].commands[commandIndex] + props.command(command) + }, + [props], + ) + + React.useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }: { event: React.KeyboardEvent }) => { + if (event.key === 'ArrowDown') { + if (!props.items.length) { + return false + } + + const commands = props.items[selectedGroupIndex].commands + + let newCommandIndex = selectedCommandIndex + 1 + let newGroupIndex = selectedGroupIndex + + if (commands.length - 1 < newCommandIndex) { + newCommandIndex = 0 + newGroupIndex = selectedGroupIndex + 1 + } + + if (props.items.length - 1 < newGroupIndex) { + newGroupIndex = 0 + } + + setSelectedCommandIndex(newCommandIndex) + setSelectedGroupIndex(newGroupIndex) + + return true + } + + if (event.key === 'ArrowUp') { + if (!props.items.length) { + return false + } + + let newCommandIndex = selectedCommandIndex - 1 + let newGroupIndex = selectedGroupIndex + + if (newCommandIndex < 0) { + newGroupIndex = selectedGroupIndex - 1 + newCommandIndex = props.items[newGroupIndex]?.commands.length - 1 || 0 + } + + if (newGroupIndex < 0) { + newGroupIndex = props.items.length - 1 + newCommandIndex = props.items[newGroupIndex].commands.length - 1 + } + + setSelectedCommandIndex(newCommandIndex) + setSelectedGroupIndex(newGroupIndex) + + return true + } + + if (event.key === 'Enter') { + if (!props.items.length || selectedGroupIndex === -1 || selectedCommandIndex === -1) { + return false + } + + selectItem(selectedGroupIndex, selectedCommandIndex) + + return true + } + + return false + }, + })) + + useEffect(() => { + if (activeItem.current && scrollContainer.current) { + const offsetTop = activeItem.current.offsetTop + const offsetHeight = activeItem.current.offsetHeight + + scrollContainer.current.scrollTop = offsetTop - offsetHeight + } + }, [selectedCommandIndex, selectedGroupIndex]) + + const createCommandClickHandler = useCallback( + (groupIndex: number, commandIndex: number) => { + return () => { + selectItem(groupIndex, commandIndex) + } + }, + [selectItem], + ) + + if (!props.items.length) { + return null + } + + return ( + +
+ {props.items.map((group, groupIndex: number) => ( + +
+ {group.title} +
+ {group.commands.map((command: Command, commandIndex: number) => ( + + + {command.label} + + ))} +
+ ))} +
+
+ ) +}) + +MenuList.displayName = 'MenuList' + +export default MenuList diff --git a/lib/block-editor/extensions/SlashCommand/SlashCommand.ts b/lib/block-editor/extensions/SlashCommand/SlashCommand.ts new file mode 100644 index 00000000..db8f7fa5 --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/SlashCommand.ts @@ -0,0 +1,259 @@ +import { Editor, Extension } from '@tiptap/core' +import { ReactRenderer } from '@tiptap/react' +import Suggestion, { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion' +import { PluginKey } from '@tiptap/pm/state' +import tippy from 'tippy.js' + +import { GROUPS } from './groups' +import { MenuList } from './MenuList' + +const extensionName = 'slashCommand' + +let popup: any + +export const SlashCommand = Extension.create({ + name: extensionName, + + priority: 200, + + onCreate() { + popup = tippy('body', { + interactive: true, + trigger: 'manual', + placement: 'bottom-start', + theme: 'slash-command', + maxWidth: '16rem', + offset: [16, 8], + popperOptions: { + strategy: 'fixed', + modifiers: [ + { + name: 'flip', + enabled: false, + }, + ], + }, + }) + }, + + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + char: '/', + allowSpaces: true, + startOfLine: true, + pluginKey: new PluginKey(extensionName), + allow: ({ state, range }) => { + const $from = state.doc.resolve(range.from) + const isRootDepth = $from.depth === 1 + const isParagraph = $from.parent.type.name === 'paragraph' + const isStartOfNode = $from.parent.textContent?.charAt(0) === '/' + // TODO + const isInColumn = this.editor.isActive('column') + + const afterContent = $from.parent.textContent?.substring($from.parent.textContent?.indexOf('/')) + const isValidAfterContent = !afterContent?.endsWith(' ') + + return ( + ((isRootDepth && isParagraph && isStartOfNode) || (isInColumn && isParagraph && isStartOfNode)) && + isValidAfterContent + ) + }, + command: ({ editor, props }: { editor: Editor; props: any }) => { + const { view, state } = editor + const { $head, $from } = view.state.selection + + const end = $from.pos + const from = $head?.nodeBefore + ? end - ($head.nodeBefore.text?.substring($head.nodeBefore.text?.indexOf('/')).length ?? 0) + : $from.start() + + const tr = state.tr.deleteRange(from, end) + view.dispatch(tr) + + props.action(editor) + view.focus() + }, + items: ({ query }: { query: string }) => { + const withFilteredCommands = GROUPS.map(group => ({ + ...group, + commands: group.commands + .filter(item => { + const labelNormalized = item.label.toLowerCase().trim() + const queryNormalized = query.toLowerCase().trim() + + if (item.aliases) { + const aliases = item.aliases.map(alias => alias.toLowerCase().trim()) + + return labelNormalized.includes(queryNormalized) || aliases.includes(queryNormalized) + } + + return labelNormalized.includes(queryNormalized) + }) + .filter(command => (command.shouldBeHidden ? !command.shouldBeHidden(this.editor) : true)), + })) + + const withoutEmptyGroups = withFilteredCommands.filter(group => { + if (group.commands.length > 0) { + return true + } + + return false + }) + + const withEnabledSettings = withoutEmptyGroups.map(group => ({ + ...group, + commands: group.commands.map(command => ({ + ...command, + isEnabled: true, + })), + })) + + return withEnabledSettings + }, + render: () => { + let component: any + + let scrollHandler: (() => void) | null = null + + return { + onStart: (props: SuggestionProps) => { + component = new ReactRenderer(MenuList, { + props, + editor: props.editor, + }) + + const { view } = props.editor + + const editorNode = view.dom as HTMLElement + + const getReferenceClientRect = () => { + if (!props.clientRect) { + return props.editor.storage[extensionName].rect + } + + const rect = props.clientRect() + + if (!rect) { + return props.editor.storage[extensionName].rect + } + + let yPos = rect.y + + if (rect.top + component.element.offsetHeight + 40 > window.innerHeight) { + const diff = rect.top + component.element.offsetHeight - window.innerHeight + 40 + yPos = rect.y - diff + } + + // Account for when the editor is bound inside a container that doesn't go all the way to the edge of the screen + const editorXOffset = editorNode.getBoundingClientRect().x + return new DOMRect(rect.x, yPos, rect.width, rect.height) + } + + scrollHandler = () => { + popup?.[0].setProps({ + getReferenceClientRect, + }) + } + + view.dom.parentElement?.addEventListener('scroll', scrollHandler) + + popup?.[0].setProps({ + getReferenceClientRect, + appendTo: () => document.body, + content: component.element, + }) + + popup?.[0].show() + }, + + onUpdate(props: SuggestionProps) { + component.updateProps(props) + + const { view } = props.editor + + const editorNode = view.dom as HTMLElement + + const getReferenceClientRect = () => { + if (!props.clientRect) { + return props.editor.storage[extensionName].rect + } + + const rect = props.clientRect() + + if (!rect) { + return props.editor.storage[extensionName].rect + } + + // Account for when the editor is bound inside a container that doesn't go all the way to the edge of the screen + return new DOMRect(rect.x, rect.y, rect.width, rect.height) + } + + let scrollHandler = () => { + popup?.[0].setProps({ + getReferenceClientRect, + }) + } + + view.dom.parentElement?.addEventListener('scroll', scrollHandler) + + // eslint-disable-next-line no-param-reassign + props.editor.storage[extensionName].rect = props.clientRect + ? getReferenceClientRect() + : { + width: 0, + height: 0, + left: 0, + top: 0, + right: 0, + bottom: 0, + } + popup?.[0].setProps({ + getReferenceClientRect, + }) + }, + + onKeyDown(props: SuggestionKeyDownProps) { + if (props.event.key === 'Escape') { + popup?.[0].hide() + + return true + } + + if (!popup?.[0].state.isShown) { + popup?.[0].show() + } + + return component.ref?.onKeyDown(props) + }, + + onExit(props) { + popup?.[0].hide() + if (scrollHandler) { + const { view } = props.editor + view.dom.parentElement?.removeEventListener('scroll', scrollHandler) + } + component.destroy() + }, + } + }, + }), + ] + }, + + addStorage() { + return { + rect: { + width: 0, + height: 0, + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + } + }, +}) + +export default SlashCommand diff --git a/lib/block-editor/extensions/SlashCommand/groups.ts b/lib/block-editor/extensions/SlashCommand/groups.ts new file mode 100644 index 00000000..ccdf7406 --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/groups.ts @@ -0,0 +1,165 @@ +import { Group } from './types' + +export const GROUPS: Group[] = [ + { + name: 'ai', + title: 'AI', + commands: [ + { + name: 'aiWriter', + label: 'AI Writer', + iconName: 'Sparkles', + description: 'Let AI finish your thoughts', + shouldBeHidden: editor => editor.isActive('columns'), + action: editor => editor.chain().focus().setAiWriter().run(), + }, + { + name: 'aiImage', + label: 'AI Image', + iconName: 'Sparkles', + description: 'Generate an image from text', + shouldBeHidden: editor => editor.isActive('columns'), + action: editor => editor.chain().focus().setAiImage().run(), + }, + ], + }, + { + name: 'format', + title: 'Format', + commands: [ + { + name: 'heading1', + label: 'Heading 1', + iconName: 'Heading1', + description: 'High priority section title', + aliases: ['h1'], + action: editor => { + editor.chain().focus().setHeading({ level: 1 }).run() + }, + }, + { + name: 'heading2', + label: 'Heading 2', + iconName: 'Heading2', + description: 'Medium priority section title', + aliases: ['h2'], + action: editor => { + editor.chain().focus().setHeading({ level: 2 }).run() + }, + }, + { + name: 'heading3', + label: 'Heading 3', + iconName: 'Heading3', + description: 'Low priority section title', + aliases: ['h3'], + action: editor => { + editor.chain().focus().setHeading({ level: 3 }).run() + }, + }, + { + name: 'bulletList', + label: 'Bullet List', + iconName: 'List', + description: 'Unordered list of items', + aliases: ['ul'], + action: editor => { + editor.chain().focus().toggleBulletList().run() + }, + }, + { + name: 'numberedList', + label: 'Numbered List', + iconName: 'ListOrdered', + description: 'Ordered list of items', + aliases: ['ol'], + action: editor => { + editor.chain().focus().toggleOrderedList().run() + }, + }, + { + name: 'taskList', + label: 'Task List', + iconName: 'ListTodo', + description: 'Task list with todo items', + aliases: ['todo'], + action: editor => { + editor.chain().focus().toggleTaskList().run() + }, + }, + { + name: 'toggleList', + label: 'Toggle List', + iconName: 'ListCollapse', + description: 'Toggles can show and hide content', + aliases: ['toggle'], + action: editor => { + editor.chain().focus().setDetails().run() + }, + }, + { + name: 'blockquote', + label: 'Blockquote', + iconName: 'Quote', + description: 'Element for quoting', + action: editor => { + editor.chain().focus().setBlockquote().run() + }, + }, + { + name: 'codeBlock', + label: 'Code Block', + iconName: 'SquareCode', + description: 'Code block with syntax highlighting', + shouldBeHidden: editor => editor.isActive('columns'), + action: editor => { + editor.chain().focus().setCodeBlock().run() + }, + }, + ], + }, + { + name: 'insert', + title: 'Insert', + commands: [ + { + name: 'table', + label: 'Table', + iconName: 'Table', + description: 'Insert a table', + shouldBeHidden: editor => editor.isActive('columns'), + action: editor => { + editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: false }).run() + }, + }, + { + name: 'columns', + label: 'Columns', + iconName: 'Columns2', + description: 'Add two column content', + aliases: ['cols'], + shouldBeHidden: editor => editor.isActive('columns'), + action: editor => { + editor + .chain() + .focus() + .setColumns() + .focus(editor.state.selection.head - 1) + .run() + }, + }, + { + name: 'horizontalRule', + label: 'Horizontal Rule', + iconName: 'Minus', + description: 'Insert a horizontal divider', + aliases: ['hr'], + action: editor => { + editor.chain().focus().setHorizontalRule().run() + }, + }, + ], + }, +] + +export default GROUPS diff --git a/lib/block-editor/extensions/SlashCommand/index.ts b/lib/block-editor/extensions/SlashCommand/index.ts new file mode 100644 index 00000000..6e4e154b --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/index.ts @@ -0,0 +1 @@ +export * from './SlashCommand' diff --git a/lib/block-editor/extensions/SlashCommand/types.ts b/lib/block-editor/extensions/SlashCommand/types.ts new file mode 100644 index 00000000..da42d9b4 --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/types.ts @@ -0,0 +1,25 @@ +import { Editor } from '@tiptap/core' + +import { icons } from 'lucide-react' + +export interface Group { + name: string + title: string + commands: Command[] +} + +export interface Command { + name: string + label: string + description: string + aliases?: string[] + iconName: keyof typeof icons + action: (editor: Editor) => void + shouldBeHidden?: (editor: Editor) => boolean +} + +export interface MenuListProps { + editor: Editor + items: Group[] + command: (command: Command) => void +} diff --git a/lib/block-editor/extensions/Table/Cell.ts b/lib/block-editor/extensions/Table/Cell.ts new file mode 100644 index 00000000..05952f99 --- /dev/null +++ b/lib/block-editor/extensions/Table/Cell.ts @@ -0,0 +1,125 @@ +import { mergeAttributes, Node } from '@tiptap/core' +import { Plugin } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' + +import { getCellsInColumn, isRowSelected, selectRow } from './utils' + +export interface TableCellOptions { + HTMLAttributes: Record +} + +export const TableCell = Node.create({ + name: 'tableCell', + + content: 'block+', // TODO: Do not allow table in table + + tableRole: 'cell', + + isolating: true, + + addOptions() { + return { + HTMLAttributes: {}, + } + }, + + parseHTML() { + return [{ tag: 'td' }] + }, + + renderHTML({ HTMLAttributes }) { + return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + }, + + addAttributes() { + return { + colspan: { + default: 1, + parseHTML: element => { + const colspan = element.getAttribute('colspan') + const value = colspan ? parseInt(colspan, 10) : 1 + + return value + }, + }, + rowspan: { + default: 1, + parseHTML: element => { + const rowspan = element.getAttribute('rowspan') + const value = rowspan ? parseInt(rowspan, 10) : 1 + + return value + }, + }, + colwidth: { + default: null, + parseHTML: element => { + const colwidth = element.getAttribute('colwidth') + const value = colwidth ? [parseInt(colwidth, 10)] : null + + return value + }, + }, + style: { + default: null, + }, + } + }, + + addProseMirrorPlugins() { + const { isEditable } = this.editor + + return [ + new Plugin({ + props: { + decorations: state => { + if (!isEditable) { + return DecorationSet.empty + } + + const { doc, selection } = state + const decorations: Decoration[] = [] + const cells = getCellsInColumn(0)(selection) + + if (cells) { + cells.forEach(({ pos }: { pos: number }, index: number) => { + decorations.push( + Decoration.widget(pos + 1, () => { + const rowSelected = isRowSelected(index)(selection) + let className = 'grip-row' + + if (rowSelected) { + className += ' selected' + } + + if (index === 0) { + className += ' first' + } + + if (index === cells.length - 1) { + className += ' last' + } + + const grip = document.createElement('a') + + grip.className = className + grip.addEventListener('mousedown', event => { + event.preventDefault() + event.stopImmediatePropagation() + + this.editor.view.dispatch(selectRow(index)(this.editor.state.tr)) + }) + + return grip + }), + ) + }) + } + + return DecorationSet.create(doc, decorations) + }, + }, + }), + ] + }, +}) diff --git a/lib/block-editor/extensions/Table/Header.ts b/lib/block-editor/extensions/Table/Header.ts new file mode 100644 index 00000000..d4fa7cce --- /dev/null +++ b/lib/block-editor/extensions/Table/Header.ts @@ -0,0 +1,89 @@ +import TiptapTableHeader from '@tiptap/extension-table-header' +import { Plugin } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' + +import { getCellsInRow, isColumnSelected, selectColumn } from './utils' + +export const TableHeader = TiptapTableHeader.extend({ + addAttributes() { + return { + colspan: { + default: 1, + }, + rowspan: { + default: 1, + }, + colwidth: { + default: null, + parseHTML: element => { + const colwidth = element.getAttribute('colwidth') + const value = colwidth ? colwidth.split(',').map(item => parseInt(item, 10)) : null + + return value + }, + }, + style: { + default: null, + }, + } + }, + + addProseMirrorPlugins() { + const { isEditable } = this.editor + + return [ + new Plugin({ + props: { + decorations: state => { + if (!isEditable) { + return DecorationSet.empty + } + + const { doc, selection } = state + const decorations: Decoration[] = [] + const cells = getCellsInRow(0)(selection) + + if (cells) { + cells.forEach(({ pos }: { pos: number }, index: number) => { + decorations.push( + Decoration.widget(pos + 1, () => { + const colSelected = isColumnSelected(index)(selection) + let className = 'grip-column' + + if (colSelected) { + className += ' selected' + } + + if (index === 0) { + className += ' first' + } + + if (index === cells.length - 1) { + className += ' last' + } + + const grip = document.createElement('a') + + grip.className = className + grip.addEventListener('mousedown', event => { + event.preventDefault() + event.stopImmediatePropagation() + + this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr)) + }) + + return grip + }), + ) + }) + } + + return DecorationSet.create(doc, decorations) + }, + }, + }), + ] + }, +}) + +export default TableHeader diff --git a/lib/block-editor/extensions/Table/Row.ts b/lib/block-editor/extensions/Table/Row.ts new file mode 100644 index 00000000..fa902a5c --- /dev/null +++ b/lib/block-editor/extensions/Table/Row.ts @@ -0,0 +1,8 @@ +import TiptapTableRow from '@tiptap/extension-table-row' + +export const TableRow = TiptapTableRow.extend({ + allowGapCursor: false, + content: 'tableCell*', +}) + +export default TableRow diff --git a/lib/block-editor/extensions/Table/Table.ts b/lib/block-editor/extensions/Table/Table.ts new file mode 100644 index 00000000..5ac2eb9f --- /dev/null +++ b/lib/block-editor/extensions/Table/Table.ts @@ -0,0 +1,5 @@ +import TiptapTable from '@tiptap/extension-table' + +export const Table = TiptapTable.configure({ resizable: true, lastColumnResizable: false }) + +export default Table diff --git a/lib/block-editor/extensions/Table/index.ts b/lib/block-editor/extensions/Table/index.ts new file mode 100644 index 00000000..78877815 --- /dev/null +++ b/lib/block-editor/extensions/Table/index.ts @@ -0,0 +1,4 @@ +export { Table } from './Table' +export { TableCell } from './Cell' +export { TableRow } from './Row' +export { TableHeader } from './Header' diff --git a/lib/block-editor/extensions/Table/menus/TableColumn/index.tsx b/lib/block-editor/extensions/Table/menus/TableColumn/index.tsx new file mode 100644 index 00000000..9e72799a --- /dev/null +++ b/lib/block-editor/extensions/Table/menus/TableColumn/index.tsx @@ -0,0 +1,71 @@ +import { BubbleMenu as BaseBubbleMenu } from '@tiptap/react' +import React, { useCallback } from 'react' +import * as PopoverMenu from '@/components/ui/PopoverMenu' + +import { Toolbar } from '@/components/ui/Toolbar' +import { isColumnGripSelected } from './utils' +import { Icon } from '@/components/ui/Icon' +import { MenuProps, ShouldShowProps } from '@/components/menus/types' + +export const TableColumnMenu = React.memo(({ editor, appendTo }: MenuProps): JSX.Element => { + const shouldShow = useCallback( + ({ view, state, from }: ShouldShowProps) => { + if (!state) { + return false + } + + return isColumnGripSelected({ editor, view, state, from: from || 0 }) + }, + [editor], + ) + + const onAddColumnBefore = useCallback(() => { + editor.chain().focus().addColumnBefore().run() + }, [editor]) + + const onAddColumnAfter = useCallback(() => { + editor.chain().focus().addColumnAfter().run() + }, [editor]) + + const onDeleteColumn = useCallback(() => { + editor.chain().focus().deleteColumn().run() + }, [editor]) + + return ( + { + return appendTo?.current + }, + offset: [0, 15], + popperOptions: { + modifiers: [{ name: 'flip', enabled: false }], + }, + }} + shouldShow={shouldShow} + > + + } + close={false} + label="Add column before" + onClick={onAddColumnBefore} + /> + } + close={false} + label="Add column after" + onClick={onAddColumnAfter} + /> + + + + ) +}) + +TableColumnMenu.displayName = 'TableColumnMenu' + +export default TableColumnMenu diff --git a/lib/block-editor/extensions/Table/menus/TableColumn/utils.ts b/lib/block-editor/extensions/Table/menus/TableColumn/utils.ts new file mode 100644 index 00000000..d713158e --- /dev/null +++ b/lib/block-editor/extensions/Table/menus/TableColumn/utils.ts @@ -0,0 +1,38 @@ +import { Editor } from '@tiptap/react' +import { EditorState } from '@tiptap/pm/state' +import { EditorView } from '@tiptap/pm/view' + +import { isTableSelected } from '../../utils' +import { Table } from '../..' + +export const isColumnGripSelected = ({ + editor, + view, + state, + from, +}: { + editor: Editor + view: EditorView + state: EditorState + from: number +}) => { + const domAtPos = view.domAtPos(from).node as HTMLElement + const nodeDOM = view.nodeDOM(from) as HTMLElement + const node = nodeDOM || domAtPos + + if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) { + return false + } + + let container = node + + while (container && !['TD', 'TH'].includes(container.tagName)) { + container = container.parentElement! + } + + const gripColumn = container && container.querySelector && container.querySelector('a.grip-column.selected') + + return !!gripColumn +} + +export default isColumnGripSelected diff --git a/lib/block-editor/extensions/Table/menus/TableRow/index.tsx b/lib/block-editor/extensions/Table/menus/TableRow/index.tsx new file mode 100644 index 00000000..5f9aebf7 --- /dev/null +++ b/lib/block-editor/extensions/Table/menus/TableRow/index.tsx @@ -0,0 +1,72 @@ +import { BubbleMenu as BaseBubbleMenu } from '@tiptap/react' +import React, { useCallback } from 'react' +import * as PopoverMenu from '@/components/ui/PopoverMenu' + +import { Toolbar } from '@/components/ui/Toolbar' +import { isRowGripSelected } from './utils' +import { Icon } from '@/components/ui/Icon' +import { MenuProps, ShouldShowProps } from '@/components/menus/types' + +export const TableRowMenu = React.memo(({ editor, appendTo }: MenuProps): JSX.Element => { + const shouldShow = useCallback( + ({ view, state, from }: ShouldShowProps) => { + if (!state || !from) { + return false + } + + return isRowGripSelected({ editor, view, state, from }) + }, + [editor], + ) + + const onAddRowBefore = useCallback(() => { + editor.chain().focus().addRowBefore().run() + }, [editor]) + + const onAddRowAfter = useCallback(() => { + editor.chain().focus().addRowAfter().run() + }, [editor]) + + const onDeleteRow = useCallback(() => { + editor.chain().focus().deleteRow().run() + }, [editor]) + + return ( + { + return appendTo?.current + }, + placement: 'left', + offset: [0, 15], + popperOptions: { + modifiers: [{ name: 'flip', enabled: false }], + }, + }} + shouldShow={shouldShow} + > + + } + close={false} + label="Add row before" + onClick={onAddRowBefore} + /> + } + close={false} + label="Add row after" + onClick={onAddRowAfter} + /> + + + + ) +}) + +TableRowMenu.displayName = 'TableRowMenu' + +export default TableRowMenu diff --git a/lib/block-editor/extensions/Table/menus/TableRow/utils.ts b/lib/block-editor/extensions/Table/menus/TableRow/utils.ts new file mode 100644 index 00000000..57646b3f --- /dev/null +++ b/lib/block-editor/extensions/Table/menus/TableRow/utils.ts @@ -0,0 +1,38 @@ +import { Editor } from '@tiptap/react' +import { EditorState } from '@tiptap/pm/state' +import { EditorView } from '@tiptap/pm/view' + +import { isTableSelected } from '../../utils' +import { Table } from '../..' + +export const isRowGripSelected = ({ + editor, + view, + state, + from, +}: { + editor: Editor + view: EditorView + state: EditorState + from: number +}) => { + const domAtPos = view.domAtPos(from).node as HTMLElement + const nodeDOM = view.nodeDOM(from) as HTMLElement + const node = nodeDOM || domAtPos + + if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) { + return false + } + + let container = node + + while (container && !['TD', 'TH'].includes(container.tagName)) { + container = container.parentElement! + } + + const gripRow = container && container.querySelector && container.querySelector('a.grip-row.selected') + + return !!gripRow +} + +export default isRowGripSelected diff --git a/lib/block-editor/extensions/Table/menus/index.tsx b/lib/block-editor/extensions/Table/menus/index.tsx new file mode 100644 index 00000000..2f8e1fe8 --- /dev/null +++ b/lib/block-editor/extensions/Table/menus/index.tsx @@ -0,0 +1,2 @@ +export * from './TableColumn' +export * from './TableRow' diff --git a/lib/block-editor/extensions/Table/utils.ts b/lib/block-editor/extensions/Table/utils.ts new file mode 100644 index 00000000..4f321e42 --- /dev/null +++ b/lib/block-editor/extensions/Table/utils.ts @@ -0,0 +1,251 @@ +import { findParentNode } from '@tiptap/core' +import { Selection, Transaction } from '@tiptap/pm/state' +import { CellSelection, Rect, TableMap } from '@tiptap/pm/tables' +import { Node, ResolvedPos } from '@tiptap/pm/model' + +export const isRectSelected = (rect: Rect) => (selection: CellSelection) => { + const map = TableMap.get(selection.$anchorCell.node(-1)) + const start = selection.$anchorCell.start(-1) + const cells = map.cellsInRect(rect) + const selectedCells = map.cellsInRect( + map.rectBetween(selection.$anchorCell.pos - start, selection.$headCell.pos - start), + ) + + for (let i = 0, count = cells.length; i < count; i += 1) { + if (selectedCells.indexOf(cells[i]) === -1) { + return false + } + } + + return true +} + +export const findTable = (selection: Selection) => + findParentNode(node => node.type.spec.tableRole && node.type.spec.tableRole === 'table')(selection) + +export const isCellSelection = (selection: Selection): selection is CellSelection => selection instanceof CellSelection + +export const isColumnSelected = (columnIndex: number) => (selection: Selection) => { + if (isCellSelection(selection)) { + const map = TableMap.get(selection.$anchorCell.node(-1)) + + return isRectSelected({ + left: columnIndex, + right: columnIndex + 1, + top: 0, + bottom: map.height, + })(selection) + } + + return false +} + +export const isRowSelected = (rowIndex: number) => (selection: Selection) => { + if (isCellSelection(selection)) { + const map = TableMap.get(selection.$anchorCell.node(-1)) + + return isRectSelected({ + left: 0, + right: map.width, + top: rowIndex, + bottom: rowIndex + 1, + })(selection) + } + + return false +} + +export const isTableSelected = (selection: Selection) => { + if (isCellSelection(selection)) { + const map = TableMap.get(selection.$anchorCell.node(-1)) + + return isRectSelected({ + left: 0, + right: map.width, + top: 0, + bottom: map.height, + })(selection) + } + + return false +} + +export const getCellsInColumn = (columnIndex: number | number[]) => (selection: Selection) => { + const table = findTable(selection) + if (table) { + const map = TableMap.get(table.node) + const indexes = Array.isArray(columnIndex) ? columnIndex : Array.from([columnIndex]) + + return indexes.reduce( + (acc, index) => { + if (index >= 0 && index <= map.width - 1) { + const cells = map.cellsInRect({ + left: index, + right: index + 1, + top: 0, + bottom: map.height, + }) + + return acc.concat( + cells.map(nodePos => { + const node = table.node.nodeAt(nodePos) + const pos = nodePos + table.start + + return { pos, start: pos + 1, node } + }), + ) + } + + return acc + }, + [] as { pos: number; start: number; node: Node | null | undefined }[], + ) + } + return null +} + +export const getCellsInRow = (rowIndex: number | number[]) => (selection: Selection) => { + const table = findTable(selection) + + if (table) { + const map = TableMap.get(table.node) + const indexes = Array.isArray(rowIndex) ? rowIndex : Array.from([rowIndex]) + + return indexes.reduce( + (acc, index) => { + if (index >= 0 && index <= map.height - 1) { + const cells = map.cellsInRect({ + left: 0, + right: map.width, + top: index, + bottom: index + 1, + }) + + return acc.concat( + cells.map(nodePos => { + const node = table.node.nodeAt(nodePos) + const pos = nodePos + table.start + return { pos, start: pos + 1, node } + }), + ) + } + + return acc + }, + [] as { pos: number; start: number; node: Node | null | undefined }[], + ) + } + + return null +} + +export const getCellsInTable = (selection: Selection) => { + const table = findTable(selection) + + if (table) { + const map = TableMap.get(table.node) + const cells = map.cellsInRect({ + left: 0, + right: map.width, + top: 0, + bottom: map.height, + }) + + return cells.map(nodePos => { + const node = table.node.nodeAt(nodePos) + const pos = nodePos + table.start + + return { pos, start: pos + 1, node } + }) + } + + return null +} + +export const findParentNodeClosestToPos = ($pos: ResolvedPos, predicate: (node: Node) => boolean) => { + for (let i = $pos.depth; i > 0; i -= 1) { + const node = $pos.node(i) + + if (predicate(node)) { + return { + pos: i > 0 ? $pos.before(i) : 0, + start: $pos.start(i), + depth: i, + node, + } + } + } + + return null +} + +export const findCellClosestToPos = ($pos: ResolvedPos) => { + const predicate = (node: Node) => node.type.spec.tableRole && /cell/i.test(node.type.spec.tableRole) + + return findParentNodeClosestToPos($pos, predicate) +} + +const select = (type: 'row' | 'column') => (index: number) => (tr: Transaction) => { + const table = findTable(tr.selection) + const isRowSelection = type === 'row' + + if (table) { + const map = TableMap.get(table.node) + + // Check if the index is valid + if (index >= 0 && index < (isRowSelection ? map.height : map.width)) { + const left = isRowSelection ? 0 : index + const top = isRowSelection ? index : 0 + const right = isRowSelection ? map.width : index + 1 + const bottom = isRowSelection ? index + 1 : map.height + + const cellsInFirstRow = map.cellsInRect({ + left, + top, + right: isRowSelection ? right : left + 1, + bottom: isRowSelection ? top + 1 : bottom, + }) + + const cellsInLastRow = + bottom - top === 1 + ? cellsInFirstRow + : map.cellsInRect({ + left: isRowSelection ? left : right - 1, + top: isRowSelection ? bottom - 1 : top, + right, + bottom, + }) + + const head = table.start + cellsInFirstRow[0] + const anchor = table.start + cellsInLastRow[cellsInLastRow.length - 1] + const $head = tr.doc.resolve(head) + const $anchor = tr.doc.resolve(anchor) + + return tr.setSelection(new CellSelection($anchor, $head)) + } + } + return tr +} + +export const selectColumn = select('column') + +export const selectRow = select('row') + +export const selectTable = (tr: Transaction) => { + const table = findTable(tr.selection) + + if (table) { + const { map } = TableMap.get(table.node) + + if (map && map.length) { + const head = table.start + map[0] + const anchor = table.start + map[map.length - 1] + const $head = tr.doc.resolve(head) + const $anchor = tr.doc.resolve(anchor) + + return tr.setSelection(new CellSelection($anchor, $head)) + } + } + + return tr +} diff --git a/lib/block-editor/extensions/TrailingNode/index.ts b/lib/block-editor/extensions/TrailingNode/index.ts new file mode 100644 index 00000000..977a62f0 --- /dev/null +++ b/lib/block-editor/extensions/TrailingNode/index.ts @@ -0,0 +1 @@ +export * from './trailing-node' diff --git a/lib/block-editor/extensions/TrailingNode/trailing-node.ts b/lib/block-editor/extensions/TrailingNode/trailing-node.ts new file mode 100644 index 00000000..4f0d9937 --- /dev/null +++ b/lib/block-editor/extensions/TrailingNode/trailing-node.ts @@ -0,0 +1,71 @@ +import { Extension } from '@tiptap/core' +import { Plugin, PluginKey } from '@tiptap/pm/state' + +// @ts-ignore +function nodeEqualsType({ types, node }) { + return (Array.isArray(types) && types.includes(node.type)) || node.type === types +} + +/** + * Extension based on: + * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js + * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts + */ + +export interface TrailingNodeOptions { + node: string + notAfter: string[] +} + +export const TrailingNode = Extension.create({ + name: 'trailingNode', + + addOptions() { + return { + node: 'paragraph', + notAfter: ['paragraph'], + } + }, + + addProseMirrorPlugins() { + const plugin = new PluginKey(this.name) + const disabledNodes = Object.entries(this.editor.schema.nodes) + .map(([, value]) => value) + .filter(node => this.options.notAfter.includes(node.name)) + + return [ + new Plugin({ + key: plugin, + appendTransaction: (_, __, state) => { + const { doc, tr, schema } = state + const shouldInsertNodeAtEnd = plugin.getState(state) + const endPosition = doc.content.size + const type = schema.nodes[this.options.node] + + if (!shouldInsertNodeAtEnd) { + return + } + + // eslint-disable-next-line consistent-return + return tr.insert(endPosition, type.create()) + }, + state: { + init: (_, state) => { + const lastNode = state.tr.doc.lastChild + + return !nodeEqualsType({ node: lastNode, types: disabledNodes }) + }, + apply: (tr, value) => { + if (!tr.docChanged) { + return value + } + + const lastNode = tr.doc.lastChild + + return !nodeEqualsType({ node: lastNode, types: disabledNodes }) + }, + }, + }), + ] + }, +}) diff --git a/lib/block-editor/extensions/extension-kit.ts b/lib/block-editor/extensions/extension-kit.ts new file mode 100644 index 00000000..3543a7c2 --- /dev/null +++ b/lib/block-editor/extensions/extension-kit.ts @@ -0,0 +1,113 @@ +'use client' + +import { HocuspocusProvider } from '@hocuspocus/provider' + +import { + BlockquoteFigure, + CharacterCount, + CodeBlock, + Color, + Document, + Dropcursor, + Figcaption, + Focus, + FontFamily, + FontSize, + Heading, + Highlight, + HorizontalRule, + ImageBlock, + Link, + Placeholder, + Selection, + SlashCommand, + StarterKit, + Subscript, + Superscript, + Table, + TableCell, + TableHeader, + TableRow, + TextAlign, + TextStyle, + TrailingNode, + Typography, + Underline, + Columns, + Column, + TaskItem, + TaskList, +} from '.' + +import GlobalDragHandle from 'tiptap-extension-global-drag-handle' + +interface ExtensionKitProps { + provider?: HocuspocusProvider | null +} + +export const ExtensionKit = ({ provider }: ExtensionKitProps) => [ + GlobalDragHandle, + Document, + Columns, + TaskList, + TaskItem.configure({ + nested: true, + }), + Column, + Selection, + Heading.configure({ + levels: [1, 2, 3, 4, 5, 6], + }), + HorizontalRule, + StarterKit.configure({ + document: false, + dropcursor: false, + heading: false, + horizontalRule: false, + blockquote: false, + history: false, + codeBlock: false, + }), + CodeBlock, + TextStyle, + FontSize, + FontFamily, + Color, + TrailingNode, + Link.configure({ + openOnClick: false, + }), + Highlight.configure({ multicolor: true }), + Underline, + CharacterCount.configure({ limit: 50000 }), + ImageBlock, + TextAlign.extend({ + addKeyboardShortcuts() { + return {} + }, + }).configure({ + types: ['heading', 'paragraph'], + }), + Subscript, + Superscript, + Table, + TableCell, + TableHeader, + TableRow, + Typography, + Placeholder.configure({ + includeChildren: true, + showOnlyCurrent: false, + placeholder: () => '', + }), + SlashCommand, + Focus, + Figcaption, + BlockquoteFigure, + Dropcursor.configure({ + width: 2, + class: 'ProseMirror-dropcursor border-black', + }), +] + +export default ExtensionKit diff --git a/lib/block-editor/extensions/index.ts b/lib/block-editor/extensions/index.ts new file mode 100644 index 00000000..7972c91f --- /dev/null +++ b/lib/block-editor/extensions/index.ts @@ -0,0 +1,48 @@ +'use client'; + +export { CharacterCount } from '@tiptap/extension-character-count'; +export { Highlight } from '@tiptap/extension-highlight'; +export { Placeholder } from '@tiptap/extension-placeholder'; +export { Underline } from '@tiptap/extension-underline'; +// export { Emoji, gitHubEmojis } from '@tiptap-pro/extension-emoji' +export { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'; +export { Color } from '@tiptap/extension-color'; +export { Dropcursor } from '@tiptap/extension-dropcursor'; +export { FocusClasses as Focus } from '@tiptap/extension-focus'; +export { FontFamily } from '@tiptap/extension-font-family'; +export { Subscript } from '@tiptap/extension-subscript'; +export { TextAlign } from '@tiptap/extension-text-align'; +export { TextStyle } from '@tiptap/extension-text-style'; +export { Typography } from '@tiptap/extension-typography'; +// export { TableOfContents } from '@tiptap-pro/extension-table-of-contents' +export { BulletList } from '@tiptap/extension-bullet-list'; +export { Collaboration } from '@tiptap/extension-collaboration'; +export { OrderedList } from '@tiptap/extension-ordered-list'; +export { Paragraph } from '@tiptap/extension-paragraph'; +export { Superscript } from '@tiptap/extension-superscript'; +export { TaskItem } from '@tiptap/extension-task-item'; +export { TaskList } from '@tiptap/extension-task-list'; +// export { FileHandler } from '@tiptap-pro/extension-file-handler' +// export { Details } from '@tiptap-pro/extension-details' +// export { DetailsContent } from '@tiptap-pro/extension-details-content' +// export { DetailsSummary } from '@tiptap-pro/extension-details-summary' +// export { UniqueID } from '@tiptap-pro/extension-unique-id' + +export { BlockquoteFigure } from './BlockquoteFigure'; +export { Quote } from './BlockquoteFigure/Quote'; +export { QuoteCaption } from './BlockquoteFigure/QuoteCaption'; +export { CodeBlock } from './CodeBlock'; +export { Document } from './Document'; +export { emojiSuggestion } from './EmojiSuggestion'; +export { Figcaption } from './Figcaption'; +export { Figure } from './Figure'; +export { FontSize } from './FontSize'; +export { Heading } from './Heading'; +export { HorizontalRule } from './HorizontalRule'; +export { ImageBlock } from './ImageBlock'; +export { Link } from './Link'; +export { Column, Columns } from './MultiColumn'; +export { Selection } from './Selection'; +export { SlashCommand } from './SlashCommand'; +export { Table, TableCell, TableHeader, TableRow } from './Table'; +export { TrailingNode } from './TrailingNode'; diff --git a/lib/block-editor/useBlockEditor.tsx b/lib/block-editor/useBlockEditor.tsx new file mode 100644 index 00000000..c6b98d1e --- /dev/null +++ b/lib/block-editor/useBlockEditor.tsx @@ -0,0 +1,29 @@ +import { useEditor } from '@tiptap/react'; + +export const useBlockEditor = () => { + const editor = useEditor( + { + immediatelyRender: true, + shouldRerenderOnTransaction: false, + autofocus: true, + // onCreate: (ctx) => {}, + extensions: [], + editorProps: { + attributes: { + autocomplete: 'off', + autocorrect: 'off', + autocapitalize: 'off', + class: 'min-h-full', + }, + }, + content: ` +

+ Hello, world! +

+ `, + }, + [], // Dependency array + ); + + return { editor }; +}; diff --git a/lib/dialogs/Dialog.stories.tsx b/lib/dialogs/Dialog.stories.tsx index 816b77d2..354079db 100644 --- a/lib/dialogs/Dialog.stories.tsx +++ b/lib/dialogs/Dialog.stories.tsx @@ -1,8 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { Dialog, type DialogProps } from './Dialog'; import { fn } from '@storybook/test'; -import Form from '~/components/ui/form/Form'; -import { Button } from '~/components/ui/Button'; +import { Button } from '~/components/Button'; +import Form from '~/components/form/Form'; +import { Dialog, type DialogProps } from './Dialog'; const meta: Meta = { title: 'Systems/Dialogs/Dialog', diff --git a/lib/dialogs/Dialog.tsx b/lib/dialogs/Dialog.tsx index 57f0984d..238f0de6 100644 --- a/lib/dialogs/Dialog.tsx +++ b/lib/dialogs/Dialog.tsx @@ -1,9 +1,9 @@ +import React, { useId } from 'react'; +import CloseButton from '~/components/CloseButton'; import Surface from '~/components/layout/Surface'; -import { cn } from '../utils'; -import Paragraph from '~/components/typography/Paragraph'; -import CloseButton from '~/components/ui/CloseButton'; import Heading from '~/components/typography/Heading'; -import React, { useId } from 'react'; +import Paragraph from '~/components/typography/Paragraph'; +import { cn } from '../utils'; export type DialogProps = { title: string; diff --git a/lib/dialogs/DialogProvider.tsx b/lib/dialogs/DialogProvider.tsx index 57910bc5..a2e52f07 100644 --- a/lib/dialogs/DialogProvider.tsx +++ b/lib/dialogs/DialogProvider.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useTranslations } from 'next-intl'; import React, { createContext, useCallback, @@ -7,12 +8,11 @@ import React, { useState, type RefObject, } from 'react'; -import { generatePublicId } from '../generatePublicId'; import { flushSync } from 'react-dom'; +import { Button } from '~/components/Button'; +import Form from '~/components/form/Form'; +import { generatePublicId } from '../generatePublicId'; import { Dialog } from './Dialog'; -import { useTranslations } from 'next-intl'; -import Form from '~/components/ui/form/Form'; -import { Button } from '~/components/ui/Button'; type ConfirmDialog = { type: 'confirm'; diff --git a/lib/dialogs/useDialog.stories.tsx b/lib/dialogs/useDialog.stories.tsx index d8710120..b4416327 100644 --- a/lib/dialogs/useDialog.stories.tsx +++ b/lib/dialogs/useDialog.stories.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-console */ import type { StoryObj } from '@storybook/react'; -import { Button } from '~/components/ui/Button'; -import { useDialog } from './DialogProvider'; -import Form from '~/components/ui/form/Form'; import { fn } from '@storybook/test'; +import { Button } from '~/components/Button'; +import Form from '~/components/form/Form'; +import { useDialog } from './DialogProvider'; const meta = { title: 'Systems/Dialogs/useDialog', diff --git a/lib/localisation/config.ts b/lib/localisation/config.ts index d7aacee6..dad3c404 100644 --- a/lib/localisation/config.ts +++ b/lib/localisation/config.ts @@ -1,6 +1,6 @@ import { type Locale } from '~/schemas/protocol/i18n'; -export const FALLBACK_LOCALE = 'en' as const; +export const FALLBACK_LOCALE = 'en'; // Locales we provide for our backend. For now, english, spanish, and arabic // for testing RTL support. diff --git a/lib/localisation/utils.ts b/lib/localisation/utils.ts index 4e83fd96..94ef966c 100644 --- a/lib/localisation/utils.ts +++ b/lib/localisation/utils.ts @@ -3,13 +3,13 @@ import { type IntlError, IntlErrorCode, } from 'next-intl'; -import type { LocalisedRecord, LocalisedString } from '../../schemas/shared'; import { type Locale, - type LocaleCookieName, type LocaleObject, SUPPORTED_LOCALE_OBJECTS, -} from './config'; +} from '~/schemas/protocol/i18n'; +import type { LocalisedRecord, LocalisedString } from '../../schemas/shared'; +import { type LocaleCookieName } from './config'; export const customErrorLogger = (error: IntlError) => { if (error.code === IntlErrorCode.MISSING_MESSAGE) { diff --git a/package.json b/package.json index c2d6e44b..07c058fb 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "@t3-oss/env-nextjs": "^0.11.1", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/container-queries": "^0.1.1", + "@tiptap/pm": "^2.9.1", + "@tiptap/react": "^2.9.1", "@types/rtl-detect": "^1.0.3", "@types/validator": "^13.12.1", "@typescript-eslint/eslint-plugin": "^7.18.0", @@ -64,6 +66,7 @@ "sharp": "^0.33.5", "tailwind-merge": "^2.5.2", "tailwind-variants": "^0.2.1", + "tiptap-extension-global-drag-handle": "^0.1.15", "type-fest": "^4.26.1", "validator": "^13.12.0", "zod": "^3.23.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f49a7b4..f6a1a3e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,12 @@ importers: '@tailwindcss/container-queries': specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.12) + '@tiptap/pm': + specifier: ^2.9.1 + version: 2.9.1 + '@tiptap/react': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) '@types/rtl-detect': specifier: ^1.0.3 version: 1.0.3 @@ -151,6 +157,9 @@ importers: tailwind-variants: specifier: ^0.2.1 version: 0.2.1(tailwindcss@3.4.12) + tiptap-extension-global-drag-handle: + specifier: ^0.1.15 + version: 0.1.15 type-fest: specifier: ^4.26.1 version: 4.26.1 @@ -1778,6 +1787,9 @@ packages: webpack-plugin-serve: optional: true + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@prisma/client@5.19.1': resolution: {integrity: sha512-x30GFguInsgt+4z5I4WbkZP2CGpotJMUXy+Gl/aaUjHn2o1DnLYNTA+q9XdYmAQZM8fIIkvUiA2NpgosM3fneg==} engines: {node: '>=16.13'} @@ -2197,6 +2209,9 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rollup/rollup-android-arm-eabi@4.24.0': resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} cpu: [arm] @@ -2561,6 +2576,34 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tiptap/core@2.9.1': + resolution: {integrity: sha512-tifnLL/ARzQ6/FGEJjVwj9UT3v+pENdWHdk9x6F3X0mB1y0SeCjV21wpFLYESzwNdBPAj8NMp8Behv7dBnhIfw==} + peerDependencies: + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-bubble-menu@2.9.1': + resolution: {integrity: sha512-DWUF6NG08/bZDWw0jCeotSTvpkyqZTi4meJPomG9Wzs/Ol7mEwlNCsCViD999g0+IjyXFatBk4DfUq1YDDu++Q==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-floating-menu@2.9.1': + resolution: {integrity: sha512-MxZ7acNNsoNaKpetxfwi3Z11Bgrh0T2EJlCV77v9N1vWK38+st3H1WJanmLbPNtc2ocvhHJrz+DjDz3CWxQ9rQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/pm@2.9.1': + resolution: {integrity: sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==} + + '@tiptap/react@2.9.1': + resolution: {integrity: sha512-LQJ34ZPfXtJF36SZdcn4Fiwsl2WxZ9YRJI87OLnsjJ45O+gV/PfBzz/4ap+LF8LOS0AbbGhTTjBOelPoNm+aYA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + '@total-typescript/ts-reset@0.5.1': resolution: {integrity: sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==} @@ -2627,9 +2670,18 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash@4.17.7': resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -2681,6 +2733,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -3394,6 +3449,9 @@ packages: create-hmac@1.1.7: resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -3622,6 +3680,10 @@ packages: entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -4466,6 +4528,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -4537,6 +4602,10 @@ packages: map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + markdown-to-jsx@7.5.0: resolution: {integrity: sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw==} engines: {node: '>= 10'} @@ -4546,6 +4615,9 @@ packages: md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4774,6 +4846,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + os-browserify@0.3.0: resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} @@ -5103,6 +5178,64 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + prosemirror-changeset@2.2.1: + resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.6.2: + resolution: {integrity: sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==} + + prosemirror-dropcursor@1.8.1: + resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==} + + prosemirror-gapcursor@1.3.2: + resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + + prosemirror-history@1.4.1: + resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} + + prosemirror-inputrules@1.4.0: + resolution: {integrity: sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==} + + prosemirror-keymap@1.2.2: + resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} + + prosemirror-markdown@1.13.1: + resolution: {integrity: sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==} + + prosemirror-menu@1.2.4: + resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} + + prosemirror-model@1.23.0: + resolution: {integrity: sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==} + + prosemirror-schema-basic@1.2.3: + resolution: {integrity: sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==} + + prosemirror-schema-list@1.4.1: + resolution: {integrity: sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==} + + prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + + prosemirror-tables@1.6.1: + resolution: {integrity: sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.10.2: + resolution: {integrity: sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==} + + prosemirror-view@1.35.0: + resolution: {integrity: sha512-Umtbh22fmUlpZpRTiOVXA0PpdRZeYEeXQsLp51VfnMhjkJrqJ0n8APinIZrRAD5Jr3UxH8FnOaUqRylSuMsqHA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5110,6 +5243,10 @@ packages: public-encrypt@4.0.3: resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -5350,6 +5487,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + rtl-detect@1.1.2: resolution: {integrity: sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==} @@ -5722,6 +5862,12 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + + tiptap-extension-global-drag-handle@0.1.15: + resolution: {integrity: sha512-gpKXzeB4xtg3klhADRqkvoU9F0TCdlDmNtAO5J4SZgxWEfZ8/KNVdPTWlwiKPmOYYrgPnyFd53f6g+mAGoofng==} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -5828,6 +5974,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -6008,6 +6157,9 @@ packages: vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + watchpack@2.4.2: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} @@ -7637,6 +7789,8 @@ snapshots: type-fest: 4.26.1 webpack-hot-middleware: 2.26.1 + '@popperjs/core@2.11.8': {} + '@prisma/client@5.19.1(prisma@5.19.1)': optionalDependencies: prisma: 5.19.1 @@ -8055,6 +8209,8 @@ snapshots: '@radix-ui/rect@1.1.0': {} + '@remirror/core-constants@3.0.0': {} + '@rollup/rollup-android-arm-eabi@4.24.0': optional: true @@ -8610,6 +8766,55 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@tiptap/core@2.9.1(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-bubble-menu@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + tippy.js: 6.3.7 + + '@tiptap/extension-floating-menu@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + tippy.js: 6.3.7 + + '@tiptap/pm@2.9.1': + dependencies: + prosemirror-changeset: 2.2.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.6.2 + prosemirror-dropcursor: 1.8.1 + prosemirror-gapcursor: 1.3.2 + prosemirror-history: 1.4.1 + prosemirror-inputrules: 1.4.0 + prosemirror-keymap: 1.2.2 + prosemirror-markdown: 1.13.1 + prosemirror-menu: 1.2.4 + prosemirror-model: 1.23.0 + prosemirror-schema-basic: 1.2.3 + prosemirror-schema-list: 1.4.1 + prosemirror-state: 1.4.3 + prosemirror-tables: 1.6.1 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.35.0) + prosemirror-transform: 1.10.2 + prosemirror-view: 1.35.0 + + '@tiptap/react@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-bubble-menu': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-floating-menu': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + '@types/use-sync-external-store': 0.0.6 + fast-deep-equal: 3.1.3 + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + use-sync-external-store: 1.2.2(react@19.0.0-rc-e740d4b1-20240919) + '@total-typescript/ts-reset@0.5.1': {} '@tybys/wasm-util@0.8.3': @@ -8690,8 +8895,17 @@ snapshots: '@types/json5@0.0.29': {} + '@types/linkify-it@5.0.0': {} + '@types/lodash@4.17.7': {} + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + '@types/mdx@2.0.13': {} '@types/mime@1.3.5': {} @@ -8742,6 +8956,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/uuid@9.0.8': {} '@types/validator@13.12.1': {} @@ -9602,6 +9818,8 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 + crelt@1.0.6: {} + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -9848,6 +10066,8 @@ snapshots: entities@2.2.0: {} + entities@4.5.0: {} + env-paths@2.2.1: {} error-ex@1.3.2: @@ -10946,6 +11166,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + loader-runner@4.3.0: {} loader-utils@2.0.4: @@ -11012,6 +11236,15 @@ snapshots: map-or-similar@1.5.0: {} + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-to-jsx@7.5.0(react@18.3.1): dependencies: react: 18.3.1 @@ -11026,6 +11259,8 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + mdurl@2.0.0: {} + media-typer@0.3.0: {} memfs-browser@3.5.10302: @@ -11268,6 +11503,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + orderedmap@2.1.1: {} + os-browserify@0.3.0: {} oslo@1.2.0: @@ -11526,6 +11763,109 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + prosemirror-changeset@2.2.1: + dependencies: + prosemirror-transform: 1.10.2 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.3 + + prosemirror-commands@1.6.2: + dependencies: + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-dropcursor@1.8.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.35.0 + + prosemirror-gapcursor@1.3.2: + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-view: 1.35.0 + + prosemirror-history@1.4.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.35.0 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.4.0: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-keymap@1.2.2: + dependencies: + prosemirror-state: 1.4.3 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.1: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.23.0 + + prosemirror-menu@1.2.4: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.6.2 + prosemirror-history: 1.4.1 + prosemirror-state: 1.4.3 + + prosemirror-model@1.23.0: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.3: + dependencies: + prosemirror-model: 1.23.0 + + prosemirror-schema-list@1.4.1: + dependencies: + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-state@1.4.3: + dependencies: + prosemirror-model: 1.23.0 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.35.0 + + prosemirror-tables@1.6.1: + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.35.0 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.35.0): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-view: 1.35.0 + + prosemirror-transform@1.10.2: + dependencies: + prosemirror-model: 1.23.0 + + prosemirror-view@1.35.0: + dependencies: + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -11540,6 +11880,8 @@ snapshots: randombytes: 2.1.0 safe-buffer: 5.2.1 + punycode.js@2.3.1: {} + punycode@1.4.1: {} punycode@2.3.1: {} @@ -11847,6 +12189,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.24.0 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + rtl-detect@1.1.2: {} run-parallel@1.2.0: @@ -12290,6 +12634,12 @@ snapshots: tinyspy@3.0.2: {} + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + + tiptap-extension-global-drag-handle@0.1.15: {} + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -12396,6 +12746,8 @@ snapshots: typescript@5.6.2: {} + uc.micro@2.1.0: {} + unbox-primitive@1.0.2: dependencies: call-bind: 1.0.7 @@ -12481,7 +12833,6 @@ snapshots: use-sync-external-store@1.2.2(react@19.0.0-rc-e740d4b1-20240919): dependencies: react: 19.0.0-rc-e740d4b1-20240919 - optional: true util-deprecate@1.0.2: {} @@ -12567,6 +12918,8 @@ snapshots: vm-browserify@1.1.2: {} + w3c-keyname@2.2.8: {} + watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1 From 9212b1878ae00d8d314cf653a2a4d949ebf917e1 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Wed, 30 Oct 2024 15:30:38 +0200 Subject: [PATCH 02/58] heading and paragraph --- components/block-editor/BlockEditor.tsx | 6 +- components/typography/Heading.tsx | 18 +- .../extensions/Document/Document.ts | 8 +- .../extensions/Heading/Heading.ts | 15 -- .../extensions/Heading/Heading.tsx | 29 +++ .../extensions/Paragraph/Paragraph.tsx | 26 +++ .../extensions/Paragraph/index.ts | 1 + lib/block-editor/extensions/extension-kit.ts | 203 +++++++++--------- lib/block-editor/useBlockEditor.tsx | 12 +- package.json | 5 + pnpm-lock.yaml | 51 +++++ styles/global.css | 37 ++++ 12 files changed, 276 insertions(+), 135 deletions(-) delete mode 100644 lib/block-editor/extensions/Heading/Heading.ts create mode 100644 lib/block-editor/extensions/Heading/Heading.tsx create mode 100644 lib/block-editor/extensions/Paragraph/Paragraph.tsx create mode 100644 lib/block-editor/extensions/Paragraph/index.ts diff --git a/components/block-editor/BlockEditor.tsx b/components/block-editor/BlockEditor.tsx index b89d5f87..96fd52dc 100644 --- a/components/block-editor/BlockEditor.tsx +++ b/components/block-editor/BlockEditor.tsx @@ -6,7 +6,11 @@ import { useBlockEditor } from '~/lib/block-editor/useBlockEditor'; const BlockEditor = () => { const { editor } = useBlockEditor(); - return ; + return ( +
+ +
+ ); }; export default BlockEditor; diff --git a/components/typography/Heading.tsx b/components/typography/Heading.tsx index 56ca1c48..8ae9beef 100644 --- a/components/typography/Heading.tsx +++ b/components/typography/Heading.tsx @@ -1,21 +1,21 @@ 'use client'; -import { tv, type VariantProps } from 'tailwind-variants'; +import { Slot } from '@radix-ui/react-slot'; import React from 'react'; +import { tv, type VariantProps } from 'tailwind-variants'; import { cn } from '~/lib/utils'; -import { Slot } from '@radix-ui/react-slot'; export const headingVariants = tv({ - base: 'text-balance font-heading font-bold [&:has(+.lead)]:mb-2 max-w-[55ch]', + base: 'font-heading max-w-[55ch] text-balance font-bold [&:has(+.lead)]:mb-2', variants: { variant: { - 'h1': 'scroll-m-20 text-2xl tracking-tight mb-6', - 'h2': 'scroll-m-20 text-xl tracking-tight mb-4', - 'h3': 'scroll-m-20 text-lg mb-3', - 'h4': 'scroll-m-20 text-base mb-2 leading-6 font-[460]', - 'h4-all-caps': 'scroll-m-20 text-base tracking-widest uppercase', + 'h1': 'mb-6 scroll-m-20 text-2xl tracking-tight', + 'h2': 'mb-4 scroll-m-20 text-xl tracking-tight', + 'h3': 'mb-3 scroll-m-20 text-lg', + 'h4': 'mb-2 scroll-m-20 text-base font-[460] leading-6', + 'h4-all-caps': 'scroll-m-20 text-base uppercase tracking-widest', 'label': - 'scroll-m-20 text-sm tracking-normal peer-disabled:opacity-70 peer-disabled:cursor-not-allowed font-extrabold', + 'scroll-m-20 text-sm font-extrabold tracking-normal peer-disabled:cursor-not-allowed peer-disabled:opacity-70', }, }, defaultVariants: { diff --git a/lib/block-editor/extensions/Document/Document.ts b/lib/block-editor/extensions/Document/Document.ts index 239abf5e..f5950434 100644 --- a/lib/block-editor/extensions/Document/Document.ts +++ b/lib/block-editor/extensions/Document/Document.ts @@ -1,7 +1,7 @@ -import { Document as TiptapDocument } from '@tiptap/extension-document' +import { Document as TiptapDocument } from '@tiptap/extension-document'; export const Document = TiptapDocument.extend({ - content: '(block|columns)+', -}) + // content: '(block|columns)+', +}); -export default Document +export default Document; diff --git a/lib/block-editor/extensions/Heading/Heading.ts b/lib/block-editor/extensions/Heading/Heading.ts deleted file mode 100644 index 6a68b5fa..00000000 --- a/lib/block-editor/extensions/Heading/Heading.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { mergeAttributes } from '@tiptap/core' -import TiptapHeading from '@tiptap/extension-heading' -import type { Level } from '@tiptap/extension-heading' - -export const Heading = TiptapHeading.extend({ - renderHTML({ node, HTMLAttributes }) { - const nodeLevel = parseInt(node.attrs.level, 10) as Level - const hasLevel = this.options.levels.includes(nodeLevel) - const level = hasLevel ? nodeLevel : this.options.levels[0] - - return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] - }, -}) - -export default Heading diff --git a/lib/block-editor/extensions/Heading/Heading.tsx b/lib/block-editor/extensions/Heading/Heading.tsx new file mode 100644 index 00000000..668d1f80 --- /dev/null +++ b/lib/block-editor/extensions/Heading/Heading.tsx @@ -0,0 +1,29 @@ +import TiptapHeading from '@tiptap/extension-heading'; +import { mergeAttributes } from '@tiptap/react'; +import { headingVariants } from '~/components/typography/Heading'; + +type HeadingAttrs = { + level: string; +}; + +export const Heading = TiptapHeading.extend({ + renderHTML({ node, HTMLAttributes }) { + const { level } = node.attrs as HeadingAttrs; + const nodeLevel = parseInt(level, 10); + + const variant = this.options.levels.includes(nodeLevel) + ? (`h${nodeLevel}` as 'h1' | 'h2' | 'h3' | 'h4') + : 'h1'; + const classes = headingVariants({ variant }); + + return [ + `h${level}`, + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + class: classes, + }), + 0, + ]; + }, +}); + +export default Heading; diff --git a/lib/block-editor/extensions/Paragraph/Paragraph.tsx b/lib/block-editor/extensions/Paragraph/Paragraph.tsx new file mode 100644 index 00000000..cb4e8e6c --- /dev/null +++ b/lib/block-editor/extensions/Paragraph/Paragraph.tsx @@ -0,0 +1,26 @@ +import { type NodeViewProps } from '@tiptap/core'; +import TipTapParagraph from '@tiptap/extension-paragraph'; +import { + NodeViewContent, + NodeViewWrapper, + ReactNodeViewRenderer, +} from '@tiptap/react'; +import TypographyParagraph from '~/components/typography/Paragraph'; + +const WrappedParagraph = (props: NodeViewProps) => { + return ( + + + + + + ); +}; + +export const Paragraph = TipTapParagraph.extend({ + addNodeView() { + return ReactNodeViewRenderer(WrappedParagraph); + }, +}); + +export default Paragraph; diff --git a/lib/block-editor/extensions/Paragraph/index.ts b/lib/block-editor/extensions/Paragraph/index.ts new file mode 100644 index 00000000..01752c91 --- /dev/null +++ b/lib/block-editor/extensions/Paragraph/index.ts @@ -0,0 +1 @@ +export * from './Paragraph'; diff --git a/lib/block-editor/extensions/extension-kit.ts b/lib/block-editor/extensions/extension-kit.ts index 3543a7c2..8547d7d7 100644 --- a/lib/block-editor/extensions/extension-kit.ts +++ b/lib/block-editor/extensions/extension-kit.ts @@ -1,113 +1,106 @@ -'use client' +import { mergeAttributes } from '@tiptap/core'; +import Heading from '@tiptap/extension-heading'; +import Paragraph from '@tiptap/extension-paragraph'; +import Text from '@tiptap/extension-text'; +import GlobalDragHandle from 'tiptap-extension-global-drag-handle'; +import { headingVariants } from '~/components/typography/Heading'; +import { paragraphVariants } from '~/components/typography/Paragraph'; +import { Document } from './Document'; -import { HocuspocusProvider } from '@hocuspocus/provider' - -import { - BlockquoteFigure, - CharacterCount, - CodeBlock, - Color, +export const ExtensionKit = () => [ Document, - Dropcursor, - Figcaption, - Focus, - FontFamily, - FontSize, - Heading, - Highlight, - HorizontalRule, - ImageBlock, - Link, - Placeholder, - Selection, - SlashCommand, - StarterKit, - Subscript, - Superscript, - Table, - TableCell, - TableHeader, - TableRow, - TextAlign, - TextStyle, - TrailingNode, - Typography, - Underline, - Columns, - Column, - TaskItem, - TaskList, -} from '.' + Heading.extend({ + levels: [1, 2, 3, 4], + renderHTML({ node, HTMLAttributes }) { + type HeadingAttrs = { + level: string; + }; -import GlobalDragHandle from 'tiptap-extension-global-drag-handle' + const { level } = node.attrs as HeadingAttrs; + const nodeLevel = parseInt(level, 10); -interface ExtensionKitProps { - provider?: HocuspocusProvider | null -} + const variant = this.options.levels.includes(nodeLevel) + ? (`h${nodeLevel}` as 'h1' | 'h2' | 'h3' | 'h4') + : 'h1'; -export const ExtensionKit = ({ provider }: ExtensionKitProps) => [ - GlobalDragHandle, - Document, - Columns, - TaskList, - TaskItem.configure({ - nested: true, - }), - Column, - Selection, - Heading.configure({ - levels: [1, 2, 3, 4, 5, 6], - }), - HorizontalRule, - StarterKit.configure({ - document: false, - dropcursor: false, - heading: false, - horizontalRule: false, - blockquote: false, - history: false, - codeBlock: false, - }), - CodeBlock, - TextStyle, - FontSize, - FontFamily, - Color, - TrailingNode, - Link.configure({ - openOnClick: false, - }), - Highlight.configure({ multicolor: true }), - Underline, - CharacterCount.configure({ limit: 50000 }), - ImageBlock, - TextAlign.extend({ - addKeyboardShortcuts() { - return {} + const classes = headingVariants({ variant }); + + return [ + `h${level}`, + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + class: classes, + }), + 0, + ]; + }, + }).configure({ levels: [1, 2, 3, 4] }), + Paragraph.configure({ + HTMLAttributes: { + class: paragraphVariants(), }, - }).configure({ - types: ['heading', 'paragraph'], - }), - Subscript, - Superscript, - Table, - TableCell, - TableHeader, - TableRow, - Typography, - Placeholder.configure({ - includeChildren: true, - showOnlyCurrent: false, - placeholder: () => '', - }), - SlashCommand, - Focus, - Figcaption, - BlockquoteFigure, - Dropcursor.configure({ - width: 2, - class: 'ProseMirror-dropcursor border-black', }), -] + Text, + GlobalDragHandle, + // Columns, + // TaskList, + // TaskItem.configure({ + // nested: true, + // }), + // Column, + // Selection, + // Heading.configure({ + // levels: [1, 2, 3, 4, 5, 6], + // }), + // HorizontalRule, + // StarterKit.configure({ + // document: false, + // dropcursor: false, + // heading: false, + // horizontalRule: false, + // blockquote: false, + // history: false, + // codeBlock: false, + // }), + // CodeBlock, + // TextStyle, + // FontSize, + // FontFamily, + // Color, + // TrailingNode, + // Link.configure({ + // openOnClick: false, + // }), + // Highlight.configure({ multicolor: true }), + // Underline, + // CharacterCount.configure({ limit: 50000 }), + // ImageBlock, + // TextAlign.extend({ + // addKeyboardShortcuts() { + // return {}; + // }, + // }).configure({ + // types: ['heading', 'paragraph'], + // }), + // Subscript, + // Superscript, + // Table, + // TableCell, + // TableHeader, + // TableRow, + // Typography, + // Placeholder.configure({ + // includeChildren: true, + // showOnlyCurrent: false, + // placeholder: () => '', + // }), + // SlashCommand, + // Focus, + // Figcaption, + // BlockquoteFigure, + // Dropcursor.configure({ + // width: 2, + // class: 'ProseMirror-dropcursor border-black', + // }), +]; -export default ExtensionKit +export default ExtensionKit; diff --git a/lib/block-editor/useBlockEditor.tsx b/lib/block-editor/useBlockEditor.tsx index c6b98d1e..11549ac9 100644 --- a/lib/block-editor/useBlockEditor.tsx +++ b/lib/block-editor/useBlockEditor.tsx @@ -1,4 +1,5 @@ import { useEditor } from '@tiptap/react'; +import ExtensionKit from './extensions/extension-kit'; export const useBlockEditor = () => { const editor = useEditor( @@ -7,7 +8,7 @@ export const useBlockEditor = () => { shouldRerenderOnTransaction: false, autofocus: true, // onCreate: (ctx) => {}, - extensions: [], + extensions: [...ExtensionKit()], editorProps: { attributes: { autocomplete: 'off', @@ -17,6 +18,15 @@ export const useBlockEditor = () => { }, }, content: ` +

+ Welcome to the Block Editor! +

+

+ This is a paragraph block. +

+

+ Another heading +

Hello, world!

diff --git a/package.json b/package.json index 07c058fb..7811cc1d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,11 @@ "@t3-oss/env-nextjs": "^0.11.1", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/container-queries": "^0.1.1", + "@tiptap/core": "^2.9.1", + "@tiptap/extension-document": "^2.9.1", + "@tiptap/extension-heading": "^2.9.1", + "@tiptap/extension-paragraph": "^2.9.1", + "@tiptap/extension-text": "^2.9.1", "@tiptap/pm": "^2.9.1", "@tiptap/react": "^2.9.1", "@types/rtl-detect": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6a1a3e9..0c5956cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,21 @@ importers: '@tailwindcss/container-queries': specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.12) + '@tiptap/core': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-document': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-heading': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-paragraph': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-text': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) '@tiptap/pm': specifier: ^2.9.1 version: 2.9.1 @@ -2587,12 +2602,32 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-document@2.9.1': + resolution: {integrity: sha512-1a+HCoDPnBttjqExfYLwfABq8MYdiowhy/wp8eCxVb6KGFEENO53KapstISvPzqH7eOi+qRjBB1KtVYb/ZXicg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-floating-menu@2.9.1': resolution: {integrity: sha512-MxZ7acNNsoNaKpetxfwi3Z11Bgrh0T2EJlCV77v9N1vWK38+st3H1WJanmLbPNtc2ocvhHJrz+DjDz3CWxQ9rQ==} peerDependencies: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-heading@2.9.1': + resolution: {integrity: sha512-SjZowzLixOFaCrV2cMaWi1mp8REK0zK1b3OcVx7bCZfVSmsOETJyrAIUpCKA8o60NwF7pwhBg0MN8oXlNKMeFw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-paragraph@2.9.1': + resolution: {integrity: sha512-JOmT0xd4gd3lIhLwrsjw8lV+ZFROKZdIxLi0Ia05XSu4RLrrvWj0zdKMSB+V87xOWfSB3Epo95zAvnPox5Q16A==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text@2.9.1': + resolution: {integrity: sha512-3wo9uCrkLVLQFgbw2eFU37QAa1jq1/7oExa+FF/DVxdtHRS9E2rnUZ8s2hat/IWzvPUHXMwo3Zg2XfhoamQpCA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm@2.9.1': resolution: {integrity: sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==} @@ -8776,12 +8811,28 @@ snapshots: '@tiptap/pm': 2.9.1 tippy.js: 6.3.7 + '@tiptap/extension-document@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-floating-menu@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': dependencies: '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) '@tiptap/pm': 2.9.1 tippy.js: 6.3.7 + '@tiptap/extension-heading@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-paragraph@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-text@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm@2.9.1': dependencies: prosemirror-changeset: 2.2.1 diff --git a/styles/global.css b/styles/global.css index edfa4b56..a461eb9a 100644 --- a/styles/global.css +++ b/styles/global.css @@ -40,4 +40,41 @@ .focusable { @apply ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2; } + + .drag-handle { + position: fixed; + opacity: 1; + transition: opacity ease-in 0.2s; + border-radius: 0.25rem; + + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(0, 0, 0, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E"); + background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem); + background-repeat: no-repeat; + background-position: center; + width: 1.2rem; + height: 1.5rem; + z-index: 50; + cursor: grab; + + &:hover { + background-color: red; + transition: background-color 0.2s; + } + + &:active { + background-color: green; + transition: background-color 0.2s; + cursor: grabbing; + } + + &.hide { + opacity: 0; + pointer-events: none; + } + + @media screen and (max-width: 600px) { + display: none; + pointer-events: none; + } + } } From 9efe94d3c1fe3273a8f824bd1c80b4d43d87e925 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Wed, 30 Oct 2024 16:15:52 +0200 Subject: [PATCH 03/58] wip additional extensions --- components/typography/UnorderedList.tsx | 8 +- .../BlockquoteFigure/BlockquoteFigure.ts | 80 ------ .../BlockquoteFigure/Quote/Quote.ts | 31 -- .../BlockquoteFigure/Quote/index.ts | 1 - .../QuoteCaption/QuoteCaption.ts | 54 ---- .../BlockquoteFigure/QuoteCaption/index.ts | 1 - .../extensions/BlockquoteFigure/index.ts | 1 - .../extensions/CodeBlock/CodeBlock.ts | 9 - .../extensions/CodeBlock/index.ts | 1 - .../extensions/Document/Document.ts | 7 - lib/block-editor/extensions/Document/index.ts | 1 - .../EmojiSuggestion/components/EmojiList.tsx | 106 ------- .../extensions/EmojiSuggestion/index.ts | 1 - .../extensions/EmojiSuggestion/suggestion.ts | 74 ----- .../extensions/EmojiSuggestion/types.ts | 10 - .../extensions/Figcaption/Figcaption.ts | 90 ------ .../extensions/Figcaption/index.ts | 1 - .../extensions/FontSize/FontSize.ts | 64 ----- lib/block-editor/extensions/FontSize/index.ts | 1 - .../extensions/Heading/Heading.tsx | 29 -- lib/block-editor/extensions/Heading/index.ts | 1 - .../extensions/ImageBlock/ImageBlock.ts | 80 +++--- .../ImageBlock/components/ImageBlockView.tsx | 44 +-- .../extensions/Paragraph/Paragraph.tsx | 26 -- .../extensions/Paragraph/index.ts | 1 - .../extensions/SlashCommand/MenuList.tsx | 138 +++++---- .../extensions/SlashCommand/SlashCommand.ts | 218 +++++++------- .../extensions/SlashCommand/groups.ts | 95 ++----- lib/block-editor/extensions/Table/Cell.ts | 125 -------- lib/block-editor/extensions/Table/Header.ts | 89 ------ lib/block-editor/extensions/Table/Row.ts | 8 - lib/block-editor/extensions/Table/Table.ts | 5 - lib/block-editor/extensions/Table/index.ts | 4 - .../Table/menus/TableColumn/index.tsx | 71 ----- .../Table/menus/TableColumn/utils.ts | 38 --- .../extensions/Table/menus/TableRow/index.tsx | 72 ----- .../extensions/Table/menus/TableRow/utils.ts | 38 --- .../extensions/Table/menus/index.tsx | 2 - lib/block-editor/extensions/Table/utils.ts | 251 ---------------- .../extensions/TrailingNode/index.ts | 1 - .../extensions/TrailingNode/trailing-node.ts | 71 ----- lib/block-editor/extensions/extension-kit.ts | 108 +++---- lib/block-editor/extensions/index.ts | 48 ---- lib/block-editor/useBlockEditor.tsx | 6 +- package.json | 12 + pnpm-lock.yaml | 268 ++++++++++++++++++ styles/global.css | 57 ++-- 47 files changed, 657 insertions(+), 1790 deletions(-) delete mode 100644 lib/block-editor/extensions/BlockquoteFigure/BlockquoteFigure.ts delete mode 100644 lib/block-editor/extensions/BlockquoteFigure/Quote/Quote.ts delete mode 100644 lib/block-editor/extensions/BlockquoteFigure/Quote/index.ts delete mode 100644 lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/QuoteCaption.ts delete mode 100644 lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/index.ts delete mode 100644 lib/block-editor/extensions/BlockquoteFigure/index.ts delete mode 100644 lib/block-editor/extensions/CodeBlock/CodeBlock.ts delete mode 100644 lib/block-editor/extensions/CodeBlock/index.ts delete mode 100644 lib/block-editor/extensions/Document/Document.ts delete mode 100644 lib/block-editor/extensions/Document/index.ts delete mode 100644 lib/block-editor/extensions/EmojiSuggestion/components/EmojiList.tsx delete mode 100644 lib/block-editor/extensions/EmojiSuggestion/index.ts delete mode 100644 lib/block-editor/extensions/EmojiSuggestion/suggestion.ts delete mode 100644 lib/block-editor/extensions/EmojiSuggestion/types.ts delete mode 100644 lib/block-editor/extensions/Figcaption/Figcaption.ts delete mode 100644 lib/block-editor/extensions/Figcaption/index.ts delete mode 100644 lib/block-editor/extensions/FontSize/FontSize.ts delete mode 100644 lib/block-editor/extensions/FontSize/index.ts delete mode 100644 lib/block-editor/extensions/Heading/Heading.tsx delete mode 100644 lib/block-editor/extensions/Heading/index.ts delete mode 100644 lib/block-editor/extensions/Paragraph/Paragraph.tsx delete mode 100644 lib/block-editor/extensions/Paragraph/index.ts delete mode 100644 lib/block-editor/extensions/Table/Cell.ts delete mode 100644 lib/block-editor/extensions/Table/Header.ts delete mode 100644 lib/block-editor/extensions/Table/Row.ts delete mode 100644 lib/block-editor/extensions/Table/Table.ts delete mode 100644 lib/block-editor/extensions/Table/index.ts delete mode 100644 lib/block-editor/extensions/Table/menus/TableColumn/index.tsx delete mode 100644 lib/block-editor/extensions/Table/menus/TableColumn/utils.ts delete mode 100644 lib/block-editor/extensions/Table/menus/TableRow/index.tsx delete mode 100644 lib/block-editor/extensions/Table/menus/TableRow/utils.ts delete mode 100644 lib/block-editor/extensions/Table/menus/index.tsx delete mode 100644 lib/block-editor/extensions/Table/utils.ts delete mode 100644 lib/block-editor/extensions/TrailingNode/index.ts delete mode 100644 lib/block-editor/extensions/TrailingNode/trailing-node.ts delete mode 100644 lib/block-editor/extensions/index.ts diff --git a/components/typography/UnorderedList.tsx b/components/typography/UnorderedList.tsx index 5cc51e24..9a2db876 100644 --- a/components/typography/UnorderedList.tsx +++ b/components/typography/UnorderedList.tsx @@ -1,5 +1,7 @@ import { cn } from '~/lib/utils'; +export const unorderedListClasses = 'my-3 ml-8 list-disc [&>li]:mt-1'; + export default function UnorderedList({ children, className, @@ -7,9 +9,5 @@ export default function UnorderedList({ children: React.ReactNode; className?: string; }) { - return ( -
    li]:mt-1', className)}> - {children} -
- ); + return
    {children}
; } diff --git a/lib/block-editor/extensions/BlockquoteFigure/BlockquoteFigure.ts b/lib/block-editor/extensions/BlockquoteFigure/BlockquoteFigure.ts deleted file mode 100644 index a0316437..00000000 --- a/lib/block-editor/extensions/BlockquoteFigure/BlockquoteFigure.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { mergeAttributes } from '@tiptap/core' -import { Figure } from '../Figure' -import { Quote } from './Quote' -import { QuoteCaption } from './QuoteCaption' - -declare module '@tiptap/core' { - // eslint-disable-next-line no-unused-vars - interface Commands { - blockquoteFigure: { - setBlockquote: () => ReturnType - } - } -} - -export const BlockquoteFigure = Figure.extend({ - name: 'blockquoteFigure', - - group: 'block', - - content: 'quote quoteCaption', - - isolating: true, - - addExtensions() { - return [Quote, QuoteCaption] - }, - - renderHTML({ HTMLAttributes }) { - return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), ['div', {}, 0]] - }, - - addKeyboardShortcuts() { - return { - Enter: () => false, - } - }, - - addAttributes() { - return { - ...this.parent?.(), - } - }, - - addCommands() { - return { - setBlockquote: - () => - ({ state, chain }) => { - const position = state.selection.$from.start() - const selectionContent = state.selection.content() - - return chain() - .focus() - .insertContent({ - type: this.name, - content: [ - { - type: 'quote', - content: selectionContent.content.toJSON() || [ - { - type: 'paragraph', - attrs: { - textAlign: 'left', - }, - }, - ], - }, - { - type: 'quoteCaption', - }, - ], - }) - .focus(position + 1) - .run() - }, - } - }, -}) - -export default BlockquoteFigure diff --git a/lib/block-editor/extensions/BlockquoteFigure/Quote/Quote.ts b/lib/block-editor/extensions/BlockquoteFigure/Quote/Quote.ts deleted file mode 100644 index cc2023ee..00000000 --- a/lib/block-editor/extensions/BlockquoteFigure/Quote/Quote.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Node } from '@tiptap/core' - -export const Quote = Node.create({ - name: 'quote', - - content: 'paragraph+', - - defining: true, - - marks: '', - - parseHTML() { - return [ - { - tag: 'blockquote', - }, - ] - }, - - renderHTML({ HTMLAttributes }) { - return ['blockquote', HTMLAttributes, 0] - }, - - addKeyboardShortcuts() { - return { - Backspace: () => false, - } - }, -}) - -export default Quote diff --git a/lib/block-editor/extensions/BlockquoteFigure/Quote/index.ts b/lib/block-editor/extensions/BlockquoteFigure/Quote/index.ts deleted file mode 100644 index 2cfc86d0..00000000 --- a/lib/block-editor/extensions/BlockquoteFigure/Quote/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Quote' diff --git a/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/QuoteCaption.ts b/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/QuoteCaption.ts deleted file mode 100644 index f625b157..00000000 --- a/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/QuoteCaption.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Node } from '@tiptap/core' - -export const QuoteCaption = Node.create({ - name: 'quoteCaption', - - group: 'block', - - content: 'text*', - - defining: true, - - isolating: true, - - parseHTML() { - return [ - { - tag: 'figcaption', - }, - ] - }, - - renderHTML({ HTMLAttributes }) { - return ['figcaption', HTMLAttributes, 0] - }, - - addKeyboardShortcuts() { - return { - // On Enter at the end of line, create new paragraph and focus - Enter: ({ editor }) => { - const { - state: { - selection: { $from, empty }, - }, - } = editor - - if (!empty || $from.parent.type !== this.type) { - return false - } - - const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2 - - if (!isAtEnd) { - return false - } - - const pos = editor.state.selection.$from.end() - - return editor.chain().focus(pos).insertContentAt(pos, { type: 'paragraph' }).run() - }, - } - }, -}) - -export default QuoteCaption diff --git a/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/index.ts b/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/index.ts deleted file mode 100644 index 9a5f4f98..00000000 --- a/lib/block-editor/extensions/BlockquoteFigure/QuoteCaption/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './QuoteCaption' diff --git a/lib/block-editor/extensions/BlockquoteFigure/index.ts b/lib/block-editor/extensions/BlockquoteFigure/index.ts deleted file mode 100644 index 9c78a829..00000000 --- a/lib/block-editor/extensions/BlockquoteFigure/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BlockquoteFigure' diff --git a/lib/block-editor/extensions/CodeBlock/CodeBlock.ts b/lib/block-editor/extensions/CodeBlock/CodeBlock.ts deleted file mode 100644 index 7b915be0..00000000 --- a/lib/block-editor/extensions/CodeBlock/CodeBlock.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight' -import { all, createLowlight } from 'lowlight' - -const lowlight = createLowlight(all) - -export const CodeBlock = CodeBlockLowlight.configure({ - lowlight, - defaultLanguage: 'javascript', -}) diff --git a/lib/block-editor/extensions/CodeBlock/index.ts b/lib/block-editor/extensions/CodeBlock/index.ts deleted file mode 100644 index 412da358..00000000 --- a/lib/block-editor/extensions/CodeBlock/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CodeBlock' diff --git a/lib/block-editor/extensions/Document/Document.ts b/lib/block-editor/extensions/Document/Document.ts deleted file mode 100644 index f5950434..00000000 --- a/lib/block-editor/extensions/Document/Document.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Document as TiptapDocument } from '@tiptap/extension-document'; - -export const Document = TiptapDocument.extend({ - // content: '(block|columns)+', -}); - -export default Document; diff --git a/lib/block-editor/extensions/Document/index.ts b/lib/block-editor/extensions/Document/index.ts deleted file mode 100644 index 6b7b839d..00000000 --- a/lib/block-editor/extensions/Document/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Document' diff --git a/lib/block-editor/extensions/EmojiSuggestion/components/EmojiList.tsx b/lib/block-editor/extensions/EmojiSuggestion/components/EmojiList.tsx deleted file mode 100644 index 209776f3..00000000 --- a/lib/block-editor/extensions/EmojiSuggestion/components/EmojiList.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { EmojiItem } from '@tiptap-pro/extension-emoji' -import React, { ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useState } from 'react' - -import { Button } from '@/components/ui/Button' -import { Panel } from '@/components/ui/Panel' -import { EmojiListProps } from '../types' -import { SuggestionKeyDownProps } from '@tiptap/suggestion' - -const EmojiList = forwardRef( - (props: EmojiListProps, ref: ForwardedRef<{ onKeyDown: (evt: SuggestionKeyDownProps) => boolean }>) => { - const [selectedIndex, setSelectedIndex] = useState(0) - - useEffect(() => setSelectedIndex(0), [props.items]) - - const selectItem = useCallback( - (index: number) => { - const item = props.items[index] - - if (item) { - props.command({ name: item.name }) - } - }, - [props], - ) - - useImperativeHandle(ref, () => { - const scrollIntoView = (index: number) => { - const item = props.items[index] - - if (item) { - const node = document.querySelector(`[data-emoji-name="${item.name}"]`) - - if (node) { - node.scrollIntoView({ block: 'nearest' }) - } - } - } - - const upHandler = () => { - const newIndex = (selectedIndex + props.items.length - 1) % props.items.length - setSelectedIndex(newIndex) - scrollIntoView(newIndex) - } - - const downHandler = () => { - const newIndex = (selectedIndex + 1) % props.items.length - setSelectedIndex(newIndex) - scrollIntoView(newIndex) - } - - const enterHandler = () => { - selectItem(selectedIndex) - } - - return { - onKeyDown: ({ event }) => { - if (event.key === 'ArrowUp') { - upHandler() - return true - } - - if (event.key === 'ArrowDown') { - downHandler() - return true - } - - if (event.key === 'Enter') { - enterHandler() - return true - } - - return false - }, - } - }, [props, selectedIndex, selectItem]) - - const createClickHandler = useCallback((index: number) => () => selectItem(index), [selectItem]) - - if (!props.items || !props.items.length) { - return null - } - - return ( - - {props.items.map((item: EmojiItem, index: number) => ( - - ))} - - ) - }, -) - -EmojiList.displayName = 'EmojiList' - -export default EmojiList diff --git a/lib/block-editor/extensions/EmojiSuggestion/index.ts b/lib/block-editor/extensions/EmojiSuggestion/index.ts deleted file mode 100644 index a7051661..00000000 --- a/lib/block-editor/extensions/EmojiSuggestion/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './suggestion' diff --git a/lib/block-editor/extensions/EmojiSuggestion/suggestion.ts b/lib/block-editor/extensions/EmojiSuggestion/suggestion.ts deleted file mode 100644 index 0b9196c7..00000000 --- a/lib/block-editor/extensions/EmojiSuggestion/suggestion.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ReactRenderer } from '@tiptap/react' -import { Editor } from '@tiptap/core' -import { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion' -import tippy, { Instance } from 'tippy.js' - -import EmojiList from './components/EmojiList' -import { KeyboardEvent, RefAttributes } from 'react' -import { EmojiListProps } from './types' - -export const emojiSuggestion = { - items: ({ editor, query }: { editor: Editor; query: string }) => - editor.storage.emoji.emojis - .filter( - ({ shortcodes, tags }: { shortcodes: string[]; tags: string[] }) => - shortcodes.find(shortcode => shortcode.startsWith(query.toLowerCase())) || - tags.find(tag => tag.startsWith(query.toLowerCase())), - ) - .slice(0, 250), - - allowSpaces: false, - - render: () => { - let component: ReactRenderer< - { onKeyDown: (evt: SuggestionKeyDownProps) => boolean }, - EmojiListProps & RefAttributes<{ onKeyDown: (evt: SuggestionKeyDownProps) => boolean }> - > - let popup: ReturnType - - return { - onStart: (props: SuggestionProps) => { - component = new ReactRenderer(EmojiList, { - props, - editor: props.editor, - }) - - popup = tippy('body', { - getReferenceClientRect: props.clientRect as () => DOMRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: 'manual', - placement: 'bottom-start', - }) - }, - - onUpdate(props: SuggestionProps) { - component.updateProps(props) - - popup[0].setProps({ - getReferenceClientRect: props.clientRect as () => DOMRect, - }) - }, - - onKeyDown(props: SuggestionKeyDownProps) { - if (props.event.key === 'Escape') { - popup[0].hide() - component.destroy() - - return true - } - - return component.ref?.onKeyDown(props) ?? false - }, - - onExit() { - popup[0].destroy() - component.destroy() - }, - } - }, -} - -export default emojiSuggestion diff --git a/lib/block-editor/extensions/EmojiSuggestion/types.ts b/lib/block-editor/extensions/EmojiSuggestion/types.ts deleted file mode 100644 index da1b7a57..00000000 --- a/lib/block-editor/extensions/EmojiSuggestion/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { EmojiItem } from '@tiptap-pro/extension-emoji' - -export interface Command { - name: string -} - -export interface EmojiListProps { - command: (command: Command) => void - items: EmojiItem[] -} diff --git a/lib/block-editor/extensions/Figcaption/Figcaption.ts b/lib/block-editor/extensions/Figcaption/Figcaption.ts deleted file mode 100644 index c05bfc5a..00000000 --- a/lib/block-editor/extensions/Figcaption/Figcaption.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { mergeAttributes, Node } from '@tiptap/core' - -import { Image } from '../Image' - -export const Figcaption = Node.create({ - name: 'figcaption', - - addOptions() { - return { - HTMLAttributes: {}, - } - }, - - content: 'inline*', - - selectable: false, - - draggable: false, - - marks: 'link', - - parseHTML() { - return [ - { - tag: 'figcaption', - }, - ] - }, - - addKeyboardShortcuts() { - return { - // On Enter at the end of line, create new paragraph and focus - Enter: ({ editor }) => { - const { - state: { - selection: { $from, empty }, - }, - } = editor - - if (!empty || $from.parent.type !== this.type) { - return false - } - - const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2 - - if (!isAtEnd) { - return false - } - - const pos = editor.state.selection.$from.end() - - return editor.chain().focus(pos).insertContentAt(pos, { type: 'paragraph' }).run() - }, - - // On Backspace at the beginning of line, - // dont delete content of image before - Backspace: ({ editor }) => { - const { - state: { - selection: { $from, empty }, - }, - } = editor - - if (!empty || $from.parent.type !== this.type) { - return false - } - - const isAtStart = $from.parentOffset === 0 - - if (!isAtStart) { - return false - } - - // if the node before is of type image, don't do anything - const nodeBefore = editor.state.doc.nodeAt($from.pos - 2) - if (nodeBefore?.type.name === Image.name) { - return true - } - - return false - }, - } - }, - - renderHTML({ HTMLAttributes }) { - return ['figcaption', mergeAttributes(HTMLAttributes), 0] - }, -}) - -export default Figcaption diff --git a/lib/block-editor/extensions/Figcaption/index.ts b/lib/block-editor/extensions/Figcaption/index.ts deleted file mode 100644 index da786d04..00000000 --- a/lib/block-editor/extensions/Figcaption/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Figcaption' diff --git a/lib/block-editor/extensions/FontSize/FontSize.ts b/lib/block-editor/extensions/FontSize/FontSize.ts deleted file mode 100644 index 77f117c1..00000000 --- a/lib/block-editor/extensions/FontSize/FontSize.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Attributes, Extension } from '@tiptap/core' -import '@tiptap/extension-text-style' - -declare module '@tiptap/core' { - interface Commands { - fontSize: { - setFontSize: (size: string) => ReturnType - unsetFontSize: () => ReturnType - } - } -} - -export const FontSize = Extension.create({ - name: 'fontSize', - - addOptions() { - return { - types: ['textStyle'], - } - }, - - addGlobalAttributes() { - return [ - { - types: ['paragraph'], - attributes: { - class: {}, - }, - }, - { - types: this.options.types, - attributes: { - fontSize: { - parseHTML: element => element.style.fontSize.replace(/['"]+/g, ''), - renderHTML: attributes => { - if (!attributes.fontSize) { - return {} - } - - return { - style: `font-size: ${attributes.fontSize}`, - } - }, - }, - } as Attributes, - }, - ] - }, - - addCommands() { - return { - setFontSize: - (fontSize: string) => - ({ chain }) => - chain().setMark('textStyle', { fontSize }).run(), - unsetFontSize: - () => - ({ chain }) => - chain().setMark('textStyle', { fontSize: null }).removeEmptyTextStyle().run(), - } - }, -}) - -export default FontSize diff --git a/lib/block-editor/extensions/FontSize/index.ts b/lib/block-editor/extensions/FontSize/index.ts deleted file mode 100644 index 818343ef..00000000 --- a/lib/block-editor/extensions/FontSize/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './FontSize' diff --git a/lib/block-editor/extensions/Heading/Heading.tsx b/lib/block-editor/extensions/Heading/Heading.tsx deleted file mode 100644 index 668d1f80..00000000 --- a/lib/block-editor/extensions/Heading/Heading.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import TiptapHeading from '@tiptap/extension-heading'; -import { mergeAttributes } from '@tiptap/react'; -import { headingVariants } from '~/components/typography/Heading'; - -type HeadingAttrs = { - level: string; -}; - -export const Heading = TiptapHeading.extend({ - renderHTML({ node, HTMLAttributes }) { - const { level } = node.attrs as HeadingAttrs; - const nodeLevel = parseInt(level, 10); - - const variant = this.options.levels.includes(nodeLevel) - ? (`h${nodeLevel}` as 'h1' | 'h2' | 'h3' | 'h4') - : 'h1'; - const classes = headingVariants({ variant }); - - return [ - `h${level}`, - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - class: classes, - }), - 0, - ]; - }, -}); - -export default Heading; diff --git a/lib/block-editor/extensions/Heading/index.ts b/lib/block-editor/extensions/Heading/index.ts deleted file mode 100644 index dd0dbbaa..00000000 --- a/lib/block-editor/extensions/Heading/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Heading' diff --git a/lib/block-editor/extensions/ImageBlock/ImageBlock.ts b/lib/block-editor/extensions/ImageBlock/ImageBlock.ts index 7185a750..ffd88e61 100644 --- a/lib/block-editor/extensions/ImageBlock/ImageBlock.ts +++ b/lib/block-editor/extensions/ImageBlock/ImageBlock.ts @@ -1,18 +1,21 @@ -import { ReactNodeViewRenderer } from '@tiptap/react' -import { mergeAttributes, Range } from '@tiptap/core' +import { mergeAttributes, type Range } from '@tiptap/core'; +import { ReactNodeViewRenderer } from '@tiptap/react'; -import { ImageBlockView } from './components/ImageBlockView' -import { Image } from '../Image' +import { Image } from '../Image'; +import { ImageBlockView } from './components/ImageBlockView'; declare module '@tiptap/core' { - interface Commands { + type Commands = { imageBlock: { - setImageBlock: (attributes: { src: string }) => ReturnType - setImageBlockAt: (attributes: { src: string; pos: number | Range }) => ReturnType - setImageBlockAlign: (align: 'left' | 'center' | 'right') => ReturnType - setImageBlockWidth: (width: number) => ReturnType - } - } + setImageBlock: (attributes: { src: string }) => ReturnType; + setImageBlockAt: (attributes: { + src: string; + pos: number | Range; + }) => ReturnType; + setImageBlockAlign: (align: 'left' | 'center' | 'right') => ReturnType; + setImageBlockWidth: (width: number) => ReturnType; + }; + }; } export const ImageBlock = Image.extend({ @@ -28,33 +31,33 @@ export const ImageBlock = Image.extend({ return { src: { default: '', - parseHTML: element => element.getAttribute('src'), - renderHTML: attributes => ({ + parseHTML: (element) => element.getAttribute('src'), + renderHTML: (attributes) => ({ src: attributes.src, }), }, width: { default: '100%', - parseHTML: element => element.getAttribute('data-width'), - renderHTML: attributes => ({ + parseHTML: (element) => element.getAttribute('data-width'), + renderHTML: (attributes) => ({ 'data-width': attributes.width, }), }, align: { default: 'center', - parseHTML: element => element.getAttribute('data-align'), - renderHTML: attributes => ({ + parseHTML: (element) => element.getAttribute('data-align'), + renderHTML: (attributes) => ({ 'data-align': attributes.align, }), }, alt: { default: undefined, - parseHTML: element => element.getAttribute('alt'), - renderHTML: attributes => ({ + parseHTML: (element) => element.getAttribute('alt'), + renderHTML: (attributes) => ({ alt: attributes.alt, }), }, - } + }; }, parseHTML() { @@ -62,42 +65,53 @@ export const ImageBlock = Image.extend({ { tag: 'img[src*="tiptap.dev"]:not([src^="data:"]), img[src*="windows.net"]:not([src^="data:"])', }, - ] + ]; }, renderHTML({ HTMLAttributes }) { - return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)] + return [ + 'img', + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + ]; }, addCommands() { return { setImageBlock: - attrs => + (attrs) => ({ commands }) => { - return commands.insertContent({ type: 'imageBlock', attrs: { src: attrs.src } }) + return commands.insertContent({ + type: 'imageBlock', + attrs: { src: attrs.src }, + }); }, setImageBlockAt: - attrs => + (attrs) => ({ commands }) => { - return commands.insertContentAt(attrs.pos, { type: 'imageBlock', attrs: { src: attrs.src } }) + return commands.insertContentAt(attrs.pos, { + type: 'imageBlock', + attrs: { src: attrs.src }, + }); }, setImageBlockAlign: - align => + (align) => ({ commands }) => commands.updateAttributes('imageBlock', { align }), setImageBlockWidth: - width => + (width) => ({ commands }) => - commands.updateAttributes('imageBlock', { width: `${Math.max(0, Math.min(100, width))}%` }), - } + commands.updateAttributes('imageBlock', { + width: `${Math.max(0, Math.min(100, width))}%`, + }), + }; }, addNodeView() { - return ReactNodeViewRenderer(ImageBlockView) + return ReactNodeViewRenderer(ImageBlockView); }, -}) +}); -export default ImageBlock +export default ImageBlock; diff --git a/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx b/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx index 08fc6ac2..5c762cb6 100644 --- a/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx +++ b/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx @@ -1,35 +1,35 @@ -import { cn } from '@/lib/utils' -import { Node } from '@tiptap/pm/model' -import { Editor, NodeViewWrapper } from '@tiptap/react' -import { useCallback, useRef } from 'react' +import { type Node } from '@tiptap/pm/model'; +import { type Editor, NodeViewWrapper } from '@tiptap/react'; +import { useCallback, useRef } from 'react'; +import { cn } from '~/lib/utils'; -interface ImageBlockViewProps { - editor: Editor - getPos: () => number - node: Node - updateAttributes: (attrs: Record) => void -} +type ImageBlockViewProps = { + editor: Editor; + getPos: () => number; + node: Node; + updateAttributes: (attrs: Record) => void; +}; export const ImageBlockView = (props: ImageBlockViewProps) => { const { editor, getPos, node } = props as ImageBlockViewProps & { node: Node & { attrs: { - src: string - } - } - } - const imageWrapperRef = useRef(null) - const { src } = node.attrs + src: string; + }; + }; + }; + const imageWrapperRef = useRef(null); + const { src } = node.attrs; const wrapperClassName = cn( node.attrs.align === 'left' ? 'ml-0' : 'ml-auto', node.attrs.align === 'right' ? 'mr-0' : 'mr-auto', node.attrs.align === 'center' && 'mx-auto', - ) + ); const onClick = useCallback(() => { - editor.commands.setNodeSelection(getPos()) - }, [getPos, editor.commands]) + editor.commands.setNodeSelection(getPos()); + }, [getPos, editor.commands]); return ( @@ -39,7 +39,7 @@ export const ImageBlockView = (props: ImageBlockViewProps) => { - ) -} + ); +}; -export default ImageBlockView +export default ImageBlockView; diff --git a/lib/block-editor/extensions/Paragraph/Paragraph.tsx b/lib/block-editor/extensions/Paragraph/Paragraph.tsx deleted file mode 100644 index cb4e8e6c..00000000 --- a/lib/block-editor/extensions/Paragraph/Paragraph.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { type NodeViewProps } from '@tiptap/core'; -import TipTapParagraph from '@tiptap/extension-paragraph'; -import { - NodeViewContent, - NodeViewWrapper, - ReactNodeViewRenderer, -} from '@tiptap/react'; -import TypographyParagraph from '~/components/typography/Paragraph'; - -const WrappedParagraph = (props: NodeViewProps) => { - return ( - - - - - - ); -}; - -export const Paragraph = TipTapParagraph.extend({ - addNodeView() { - return ReactNodeViewRenderer(WrappedParagraph); - }, -}); - -export default Paragraph; diff --git a/lib/block-editor/extensions/Paragraph/index.ts b/lib/block-editor/extensions/Paragraph/index.ts deleted file mode 100644 index 01752c91..00000000 --- a/lib/block-editor/extensions/Paragraph/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Paragraph'; diff --git a/lib/block-editor/extensions/SlashCommand/MenuList.tsx b/lib/block-editor/extensions/SlashCommand/MenuList.tsx index e5cdeb8d..14c7b35f 100644 --- a/lib/block-editor/extensions/SlashCommand/MenuList.tsx +++ b/lib/block-editor/extensions/SlashCommand/MenuList.tsx @@ -1,148 +1,162 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' - -import { Command, MenuListProps } from './types' -import { CommandButton } from './CommandButton' -import { Surface } from '@/components/ui/Surface' -import { DropdownButton } from '@/components/ui/Dropdown' -import { Icon } from '@/components/ui/Icon' +import { CircleIcon } from 'lucide-react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Button } from '~/components/Button'; +import Surface from '~/components/layout/Surface'; +import { type Command, type MenuListProps } from './types'; export const MenuList = React.forwardRef((props: MenuListProps, ref) => { - const scrollContainer = useRef(null) - const activeItem = useRef(null) - const [selectedGroupIndex, setSelectedGroupIndex] = useState(0) - const [selectedCommandIndex, setSelectedCommandIndex] = useState(0) + const scrollContainer = useRef(null); + const activeItem = useRef(null); + const [selectedGroupIndex, setSelectedGroupIndex] = useState(0); + const [selectedCommandIndex, setSelectedCommandIndex] = useState(0); // Anytime the groups change, i.e. the user types to narrow it down, we want to // reset the current selection to the first menu item useEffect(() => { - setSelectedGroupIndex(0) - setSelectedCommandIndex(0) - }, [props.items]) + setSelectedGroupIndex(0); + setSelectedCommandIndex(0); + }, [props.items]); const selectItem = useCallback( (groupIndex: number, commandIndex: number) => { - const command = props.items[groupIndex].commands[commandIndex] - props.command(command) + const command = props.items[groupIndex].commands[commandIndex]; + props.command(command); }, [props], - ) + ); React.useImperativeHandle(ref, () => ({ onKeyDown: ({ event }: { event: React.KeyboardEvent }) => { if (event.key === 'ArrowDown') { if (!props.items.length) { - return false + return false; } - const commands = props.items[selectedGroupIndex].commands + const commands = props.items[selectedGroupIndex].commands; - let newCommandIndex = selectedCommandIndex + 1 - let newGroupIndex = selectedGroupIndex + let newCommandIndex = selectedCommandIndex + 1; + let newGroupIndex = selectedGroupIndex; if (commands.length - 1 < newCommandIndex) { - newCommandIndex = 0 - newGroupIndex = selectedGroupIndex + 1 + newCommandIndex = 0; + newGroupIndex = selectedGroupIndex + 1; } if (props.items.length - 1 < newGroupIndex) { - newGroupIndex = 0 + newGroupIndex = 0; } - setSelectedCommandIndex(newCommandIndex) - setSelectedGroupIndex(newGroupIndex) + setSelectedCommandIndex(newCommandIndex); + setSelectedGroupIndex(newGroupIndex); - return true + return true; } if (event.key === 'ArrowUp') { if (!props.items.length) { - return false + return false; } - let newCommandIndex = selectedCommandIndex - 1 - let newGroupIndex = selectedGroupIndex + let newCommandIndex = selectedCommandIndex - 1; + let newGroupIndex = selectedGroupIndex; if (newCommandIndex < 0) { - newGroupIndex = selectedGroupIndex - 1 - newCommandIndex = props.items[newGroupIndex]?.commands.length - 1 || 0 + newGroupIndex = selectedGroupIndex - 1; + newCommandIndex = + props.items[newGroupIndex]?.commands.length - 1 || 0; } if (newGroupIndex < 0) { - newGroupIndex = props.items.length - 1 - newCommandIndex = props.items[newGroupIndex].commands.length - 1 + newGroupIndex = props.items.length - 1; + newCommandIndex = props.items[newGroupIndex].commands.length - 1; } - setSelectedCommandIndex(newCommandIndex) - setSelectedGroupIndex(newGroupIndex) + setSelectedCommandIndex(newCommandIndex); + setSelectedGroupIndex(newGroupIndex); - return true + return true; } if (event.key === 'Enter') { - if (!props.items.length || selectedGroupIndex === -1 || selectedCommandIndex === -1) { - return false + if ( + !props.items.length || + selectedGroupIndex === -1 || + selectedCommandIndex === -1 + ) { + return false; } - selectItem(selectedGroupIndex, selectedCommandIndex) + selectItem(selectedGroupIndex, selectedCommandIndex); - return true + return true; } - return false + return false; }, - })) + })); useEffect(() => { if (activeItem.current && scrollContainer.current) { - const offsetTop = activeItem.current.offsetTop - const offsetHeight = activeItem.current.offsetHeight + const offsetTop = activeItem.current.offsetTop; + const offsetHeight = activeItem.current.offsetHeight; - scrollContainer.current.scrollTop = offsetTop - offsetHeight + scrollContainer.current.scrollTop = offsetTop - offsetHeight; } - }, [selectedCommandIndex, selectedGroupIndex]) + }, [selectedCommandIndex, selectedGroupIndex]); const createCommandClickHandler = useCallback( (groupIndex: number, commandIndex: number) => { return () => { - selectItem(groupIndex, commandIndex) - } + selectItem(groupIndex, commandIndex); + }; }, [selectItem], - ) + ); if (!props.items.length) { - return null + return null; } return ( - +
{props.items.map((group, groupIndex: number) => (
{group.title}
{group.commands.map((command: Command, commandIndex: number) => ( - - + {command.label} - + ))}
))}
- ) -}) + ); +}); -MenuList.displayName = 'MenuList' +MenuList.displayName = 'MenuList'; -export default MenuList +export default MenuList; diff --git a/lib/block-editor/extensions/SlashCommand/SlashCommand.ts b/lib/block-editor/extensions/SlashCommand/SlashCommand.ts index db8f7fa5..1bed4000 100644 --- a/lib/block-editor/extensions/SlashCommand/SlashCommand.ts +++ b/lib/block-editor/extensions/SlashCommand/SlashCommand.ts @@ -1,15 +1,18 @@ -import { Editor, Extension } from '@tiptap/core' -import { ReactRenderer } from '@tiptap/react' -import Suggestion, { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion' -import { PluginKey } from '@tiptap/pm/state' -import tippy from 'tippy.js' +import { type Editor, Extension } from '@tiptap/core'; +import { PluginKey } from '@tiptap/pm/state'; +import { ReactRenderer } from '@tiptap/react'; +import Suggestion, { + type SuggestionKeyDownProps, + type SuggestionProps, +} from '@tiptap/suggestion'; +import tippy from 'tippy.js'; -import { GROUPS } from './groups' -import { MenuList } from './MenuList' +import { GROUPS } from './groups'; +import { MenuList } from './MenuList'; -const extensionName = 'slashCommand' +const extensionName = 'slashCommand'; -let popup: any +let popup: any; export const SlashCommand = Extension.create({ name: extensionName, @@ -33,7 +36,7 @@ export const SlashCommand = Extension.create({ }, ], }, - }) + }); }, addProseMirrorPlugins() { @@ -45,158 +48,180 @@ export const SlashCommand = Extension.create({ startOfLine: true, pluginKey: new PluginKey(extensionName), allow: ({ state, range }) => { - const $from = state.doc.resolve(range.from) - const isRootDepth = $from.depth === 1 - const isParagraph = $from.parent.type.name === 'paragraph' - const isStartOfNode = $from.parent.textContent?.charAt(0) === '/' + const $from = state.doc.resolve(range.from); + const isRootDepth = $from.depth === 1; + const isParagraph = $from.parent.type.name === 'paragraph'; + const isStartOfNode = $from.parent.textContent?.startsWith('/'); // TODO - const isInColumn = this.editor.isActive('column') + const isInColumn = this.editor.isActive('column'); - const afterContent = $from.parent.textContent?.substring($from.parent.textContent?.indexOf('/')) - const isValidAfterContent = !afterContent?.endsWith(' ') + const afterContent = $from.parent.textContent?.substring( + $from.parent.textContent?.indexOf('/'), + ); + const isValidAfterContent = !afterContent?.endsWith(' '); return ( - ((isRootDepth && isParagraph && isStartOfNode) || (isInColumn && isParagraph && isStartOfNode)) && + ((isRootDepth && isParagraph && isStartOfNode) || + (isInColumn && isParagraph && isStartOfNode)) && isValidAfterContent - ) + ); }, command: ({ editor, props }: { editor: Editor; props: any }) => { - const { view, state } = editor - const { $head, $from } = view.state.selection + const { view, state } = editor; + const { $head, $from } = view.state.selection; - const end = $from.pos + const end = $from.pos; const from = $head?.nodeBefore - ? end - ($head.nodeBefore.text?.substring($head.nodeBefore.text?.indexOf('/')).length ?? 0) - : $from.start() + ? end - + ($head.nodeBefore.text?.substring( + $head.nodeBefore.text?.indexOf('/'), + ).length ?? 0) + : $from.start(); - const tr = state.tr.deleteRange(from, end) - view.dispatch(tr) + const tr = state.tr.deleteRange(from, end); + view.dispatch(tr); - props.action(editor) - view.focus() + props.action(editor); + view.focus(); }, items: ({ query }: { query: string }) => { - const withFilteredCommands = GROUPS.map(group => ({ + const withFilteredCommands = GROUPS.map((group) => ({ ...group, commands: group.commands - .filter(item => { - const labelNormalized = item.label.toLowerCase().trim() - const queryNormalized = query.toLowerCase().trim() + .filter((item) => { + const labelNormalized = item.label.toLowerCase().trim(); + const queryNormalized = query.toLowerCase().trim(); if (item.aliases) { - const aliases = item.aliases.map(alias => alias.toLowerCase().trim()) - - return labelNormalized.includes(queryNormalized) || aliases.includes(queryNormalized) + const aliases = item.aliases.map((alias) => + alias.toLowerCase().trim(), + ); + + return ( + labelNormalized.includes(queryNormalized) || + aliases.includes(queryNormalized) + ); } - return labelNormalized.includes(queryNormalized) + return labelNormalized.includes(queryNormalized); }) - .filter(command => (command.shouldBeHidden ? !command.shouldBeHidden(this.editor) : true)), - })) - - const withoutEmptyGroups = withFilteredCommands.filter(group => { + .filter((command) => + command.shouldBeHidden + ? !command.shouldBeHidden(this.editor) + : true, + ), + })); + + const withoutEmptyGroups = withFilteredCommands.filter((group) => { if (group.commands.length > 0) { - return true + return true; } - return false - }) + return false; + }); - const withEnabledSettings = withoutEmptyGroups.map(group => ({ + const withEnabledSettings = withoutEmptyGroups.map((group) => ({ ...group, - commands: group.commands.map(command => ({ + commands: group.commands.map((command) => ({ ...command, isEnabled: true, })), - })) + })); - return withEnabledSettings + return withEnabledSettings; }, render: () => { - let component: any + let component: any; - let scrollHandler: (() => void) | null = null + let scrollHandler: (() => void) | null = null; return { onStart: (props: SuggestionProps) => { component = new ReactRenderer(MenuList, { props, editor: props.editor, - }) + }); - const { view } = props.editor + const { view } = props.editor; - const editorNode = view.dom as HTMLElement + const editorNode = view.dom; const getReferenceClientRect = () => { if (!props.clientRect) { - return props.editor.storage[extensionName].rect + return props.editor.storage[extensionName].rect; } - const rect = props.clientRect() + const rect = props.clientRect(); if (!rect) { - return props.editor.storage[extensionName].rect + return props.editor.storage[extensionName].rect; } - let yPos = rect.y - - if (rect.top + component.element.offsetHeight + 40 > window.innerHeight) { - const diff = rect.top + component.element.offsetHeight - window.innerHeight + 40 - yPos = rect.y - diff + let yPos = rect.y; + + if ( + rect.top + component.element.offsetHeight + 40 > + window.innerHeight + ) { + const diff = + rect.top + + component.element.offsetHeight - + window.innerHeight + + 40; + yPos = rect.y - diff; } // Account for when the editor is bound inside a container that doesn't go all the way to the edge of the screen - const editorXOffset = editorNode.getBoundingClientRect().x - return new DOMRect(rect.x, yPos, rect.width, rect.height) - } + const editorXOffset = editorNode.getBoundingClientRect().x; + return new DOMRect(rect.x, yPos, rect.width, rect.height); + }; scrollHandler = () => { popup?.[0].setProps({ getReferenceClientRect, - }) - } + }); + }; - view.dom.parentElement?.addEventListener('scroll', scrollHandler) + view.dom.parentElement?.addEventListener('scroll', scrollHandler); popup?.[0].setProps({ getReferenceClientRect, appendTo: () => document.body, content: component.element, - }) + }); - popup?.[0].show() + popup?.[0].show(); }, onUpdate(props: SuggestionProps) { - component.updateProps(props) + component.updateProps(props); - const { view } = props.editor + const { view } = props.editor; - const editorNode = view.dom as HTMLElement + const editorNode = view.dom; const getReferenceClientRect = () => { if (!props.clientRect) { - return props.editor.storage[extensionName].rect + return props.editor.storage[extensionName].rect; } - const rect = props.clientRect() + const rect = props.clientRect(); if (!rect) { - return props.editor.storage[extensionName].rect + return props.editor.storage[extensionName].rect; } // Account for when the editor is bound inside a container that doesn't go all the way to the edge of the screen - return new DOMRect(rect.x, rect.y, rect.width, rect.height) - } + return new DOMRect(rect.x, rect.y, rect.width, rect.height); + }; - let scrollHandler = () => { + const scrollHandler = () => { popup?.[0].setProps({ getReferenceClientRect, - }) - } + }); + }; - view.dom.parentElement?.addEventListener('scroll', scrollHandler) + view.dom.parentElement?.addEventListener('scroll', scrollHandler); // eslint-disable-next-line no-param-reassign props.editor.storage[extensionName].rect = props.clientRect @@ -208,38 +233,41 @@ export const SlashCommand = Extension.create({ top: 0, right: 0, bottom: 0, - } + }; popup?.[0].setProps({ getReferenceClientRect, - }) + }); }, onKeyDown(props: SuggestionKeyDownProps) { if (props.event.key === 'Escape') { - popup?.[0].hide() + popup?.[0].hide(); - return true + return true; } if (!popup?.[0].state.isShown) { - popup?.[0].show() + popup?.[0].show(); } - return component.ref?.onKeyDown(props) + return component.ref?.onKeyDown(props); }, onExit(props) { - popup?.[0].hide() + popup?.[0].hide(); if (scrollHandler) { - const { view } = props.editor - view.dom.parentElement?.removeEventListener('scroll', scrollHandler) + const { view } = props.editor; + view.dom.parentElement?.removeEventListener( + 'scroll', + scrollHandler, + ); } - component.destroy() + component.destroy(); }, - } + }; }, }), - ] + ]; }, addStorage() { @@ -252,8 +280,8 @@ export const SlashCommand = Extension.create({ right: 0, bottom: 0, }, - } + }; }, -}) +}); -export default SlashCommand +export default SlashCommand; diff --git a/lib/block-editor/extensions/SlashCommand/groups.ts b/lib/block-editor/extensions/SlashCommand/groups.ts index ccdf7406..3ca335c5 100644 --- a/lib/block-editor/extensions/SlashCommand/groups.ts +++ b/lib/block-editor/extensions/SlashCommand/groups.ts @@ -1,25 +1,17 @@ -import { Group } from './types' +import { type Group } from './types'; export const GROUPS: Group[] = [ { - name: 'ai', - title: 'AI', + name: 'variables', + title: 'Variables', commands: [ { - name: 'aiWriter', - label: 'AI Writer', + name: 'addVariable', + label: 'Add Variable', iconName: 'Sparkles', - description: 'Let AI finish your thoughts', - shouldBeHidden: editor => editor.isActive('columns'), - action: editor => editor.chain().focus().setAiWriter().run(), - }, - { - name: 'aiImage', - label: 'AI Image', - iconName: 'Sparkles', - description: 'Generate an image from text', - shouldBeHidden: editor => editor.isActive('columns'), - action: editor => editor.chain().focus().setAiImage().run(), + description: 'Insert a variable', + shouldBeHidden: (editor) => editor.isActive('columns'), + action: (editor) => () => {}, }, ], }, @@ -33,8 +25,8 @@ export const GROUPS: Group[] = [ iconName: 'Heading1', description: 'High priority section title', aliases: ['h1'], - action: editor => { - editor.chain().focus().setHeading({ level: 1 }).run() + action: (editor) => { + editor.chain().focus().setHeading({ level: 1 }).run(); }, }, { @@ -43,8 +35,8 @@ export const GROUPS: Group[] = [ iconName: 'Heading2', description: 'Medium priority section title', aliases: ['h2'], - action: editor => { - editor.chain().focus().setHeading({ level: 2 }).run() + action: (editor) => { + editor.chain().focus().setHeading({ level: 2 }).run(); }, }, { @@ -53,8 +45,8 @@ export const GROUPS: Group[] = [ iconName: 'Heading3', description: 'Low priority section title', aliases: ['h3'], - action: editor => { - editor.chain().focus().setHeading({ level: 3 }).run() + action: (editor) => { + editor.chain().focus().setHeading({ level: 3 }).run(); }, }, { @@ -63,8 +55,8 @@ export const GROUPS: Group[] = [ iconName: 'List', description: 'Unordered list of items', aliases: ['ul'], - action: editor => { - editor.chain().focus().toggleBulletList().run() + action: (editor) => { + editor.chain().focus().toggleBulletList().run(); }, }, { @@ -73,8 +65,8 @@ export const GROUPS: Group[] = [ iconName: 'ListOrdered', description: 'Ordered list of items', aliases: ['ol'], - action: editor => { - editor.chain().focus().toggleOrderedList().run() + action: (editor) => { + editor.chain().focus().toggleOrderedList().run(); }, }, { @@ -83,8 +75,8 @@ export const GROUPS: Group[] = [ iconName: 'ListTodo', description: 'Task list with todo items', aliases: ['todo'], - action: editor => { - editor.chain().focus().toggleTaskList().run() + action: (editor) => { + editor.chain().focus().toggleTaskList().run(); }, }, { @@ -93,27 +85,8 @@ export const GROUPS: Group[] = [ iconName: 'ListCollapse', description: 'Toggles can show and hide content', aliases: ['toggle'], - action: editor => { - editor.chain().focus().setDetails().run() - }, - }, - { - name: 'blockquote', - label: 'Blockquote', - iconName: 'Quote', - description: 'Element for quoting', - action: editor => { - editor.chain().focus().setBlockquote().run() - }, - }, - { - name: 'codeBlock', - label: 'Code Block', - iconName: 'SquareCode', - description: 'Code block with syntax highlighting', - shouldBeHidden: editor => editor.isActive('columns'), - action: editor => { - editor.chain().focus().setCodeBlock().run() + action: (editor) => { + editor.chain().focus().setDetails().run(); }, }, ], @@ -122,30 +95,20 @@ export const GROUPS: Group[] = [ name: 'insert', title: 'Insert', commands: [ - { - name: 'table', - label: 'Table', - iconName: 'Table', - description: 'Insert a table', - shouldBeHidden: editor => editor.isActive('columns'), - action: editor => { - editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: false }).run() - }, - }, { name: 'columns', label: 'Columns', iconName: 'Columns2', description: 'Add two column content', aliases: ['cols'], - shouldBeHidden: editor => editor.isActive('columns'), - action: editor => { + shouldBeHidden: (editor) => editor.isActive('columns'), + action: (editor) => { editor .chain() .focus() .setColumns() .focus(editor.state.selection.head - 1) - .run() + .run(); }, }, { @@ -154,12 +117,12 @@ export const GROUPS: Group[] = [ iconName: 'Minus', description: 'Insert a horizontal divider', aliases: ['hr'], - action: editor => { - editor.chain().focus().setHorizontalRule().run() + action: (editor) => { + editor.chain().focus().setHorizontalRule().run(); }, }, ], }, -] +]; -export default GROUPS +export default GROUPS; diff --git a/lib/block-editor/extensions/Table/Cell.ts b/lib/block-editor/extensions/Table/Cell.ts deleted file mode 100644 index 05952f99..00000000 --- a/lib/block-editor/extensions/Table/Cell.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { mergeAttributes, Node } from '@tiptap/core' -import { Plugin } from '@tiptap/pm/state' -import { Decoration, DecorationSet } from '@tiptap/pm/view' - -import { getCellsInColumn, isRowSelected, selectRow } from './utils' - -export interface TableCellOptions { - HTMLAttributes: Record -} - -export const TableCell = Node.create({ - name: 'tableCell', - - content: 'block+', // TODO: Do not allow table in table - - tableRole: 'cell', - - isolating: true, - - addOptions() { - return { - HTMLAttributes: {}, - } - }, - - parseHTML() { - return [{ tag: 'td' }] - }, - - renderHTML({ HTMLAttributes }) { - return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] - }, - - addAttributes() { - return { - colspan: { - default: 1, - parseHTML: element => { - const colspan = element.getAttribute('colspan') - const value = colspan ? parseInt(colspan, 10) : 1 - - return value - }, - }, - rowspan: { - default: 1, - parseHTML: element => { - const rowspan = element.getAttribute('rowspan') - const value = rowspan ? parseInt(rowspan, 10) : 1 - - return value - }, - }, - colwidth: { - default: null, - parseHTML: element => { - const colwidth = element.getAttribute('colwidth') - const value = colwidth ? [parseInt(colwidth, 10)] : null - - return value - }, - }, - style: { - default: null, - }, - } - }, - - addProseMirrorPlugins() { - const { isEditable } = this.editor - - return [ - new Plugin({ - props: { - decorations: state => { - if (!isEditable) { - return DecorationSet.empty - } - - const { doc, selection } = state - const decorations: Decoration[] = [] - const cells = getCellsInColumn(0)(selection) - - if (cells) { - cells.forEach(({ pos }: { pos: number }, index: number) => { - decorations.push( - Decoration.widget(pos + 1, () => { - const rowSelected = isRowSelected(index)(selection) - let className = 'grip-row' - - if (rowSelected) { - className += ' selected' - } - - if (index === 0) { - className += ' first' - } - - if (index === cells.length - 1) { - className += ' last' - } - - const grip = document.createElement('a') - - grip.className = className - grip.addEventListener('mousedown', event => { - event.preventDefault() - event.stopImmediatePropagation() - - this.editor.view.dispatch(selectRow(index)(this.editor.state.tr)) - }) - - return grip - }), - ) - }) - } - - return DecorationSet.create(doc, decorations) - }, - }, - }), - ] - }, -}) diff --git a/lib/block-editor/extensions/Table/Header.ts b/lib/block-editor/extensions/Table/Header.ts deleted file mode 100644 index d4fa7cce..00000000 --- a/lib/block-editor/extensions/Table/Header.ts +++ /dev/null @@ -1,89 +0,0 @@ -import TiptapTableHeader from '@tiptap/extension-table-header' -import { Plugin } from '@tiptap/pm/state' -import { Decoration, DecorationSet } from '@tiptap/pm/view' - -import { getCellsInRow, isColumnSelected, selectColumn } from './utils' - -export const TableHeader = TiptapTableHeader.extend({ - addAttributes() { - return { - colspan: { - default: 1, - }, - rowspan: { - default: 1, - }, - colwidth: { - default: null, - parseHTML: element => { - const colwidth = element.getAttribute('colwidth') - const value = colwidth ? colwidth.split(',').map(item => parseInt(item, 10)) : null - - return value - }, - }, - style: { - default: null, - }, - } - }, - - addProseMirrorPlugins() { - const { isEditable } = this.editor - - return [ - new Plugin({ - props: { - decorations: state => { - if (!isEditable) { - return DecorationSet.empty - } - - const { doc, selection } = state - const decorations: Decoration[] = [] - const cells = getCellsInRow(0)(selection) - - if (cells) { - cells.forEach(({ pos }: { pos: number }, index: number) => { - decorations.push( - Decoration.widget(pos + 1, () => { - const colSelected = isColumnSelected(index)(selection) - let className = 'grip-column' - - if (colSelected) { - className += ' selected' - } - - if (index === 0) { - className += ' first' - } - - if (index === cells.length - 1) { - className += ' last' - } - - const grip = document.createElement('a') - - grip.className = className - grip.addEventListener('mousedown', event => { - event.preventDefault() - event.stopImmediatePropagation() - - this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr)) - }) - - return grip - }), - ) - }) - } - - return DecorationSet.create(doc, decorations) - }, - }, - }), - ] - }, -}) - -export default TableHeader diff --git a/lib/block-editor/extensions/Table/Row.ts b/lib/block-editor/extensions/Table/Row.ts deleted file mode 100644 index fa902a5c..00000000 --- a/lib/block-editor/extensions/Table/Row.ts +++ /dev/null @@ -1,8 +0,0 @@ -import TiptapTableRow from '@tiptap/extension-table-row' - -export const TableRow = TiptapTableRow.extend({ - allowGapCursor: false, - content: 'tableCell*', -}) - -export default TableRow diff --git a/lib/block-editor/extensions/Table/Table.ts b/lib/block-editor/extensions/Table/Table.ts deleted file mode 100644 index 5ac2eb9f..00000000 --- a/lib/block-editor/extensions/Table/Table.ts +++ /dev/null @@ -1,5 +0,0 @@ -import TiptapTable from '@tiptap/extension-table' - -export const Table = TiptapTable.configure({ resizable: true, lastColumnResizable: false }) - -export default Table diff --git a/lib/block-editor/extensions/Table/index.ts b/lib/block-editor/extensions/Table/index.ts deleted file mode 100644 index 78877815..00000000 --- a/lib/block-editor/extensions/Table/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { Table } from './Table' -export { TableCell } from './Cell' -export { TableRow } from './Row' -export { TableHeader } from './Header' diff --git a/lib/block-editor/extensions/Table/menus/TableColumn/index.tsx b/lib/block-editor/extensions/Table/menus/TableColumn/index.tsx deleted file mode 100644 index 9e72799a..00000000 --- a/lib/block-editor/extensions/Table/menus/TableColumn/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { BubbleMenu as BaseBubbleMenu } from '@tiptap/react' -import React, { useCallback } from 'react' -import * as PopoverMenu from '@/components/ui/PopoverMenu' - -import { Toolbar } from '@/components/ui/Toolbar' -import { isColumnGripSelected } from './utils' -import { Icon } from '@/components/ui/Icon' -import { MenuProps, ShouldShowProps } from '@/components/menus/types' - -export const TableColumnMenu = React.memo(({ editor, appendTo }: MenuProps): JSX.Element => { - const shouldShow = useCallback( - ({ view, state, from }: ShouldShowProps) => { - if (!state) { - return false - } - - return isColumnGripSelected({ editor, view, state, from: from || 0 }) - }, - [editor], - ) - - const onAddColumnBefore = useCallback(() => { - editor.chain().focus().addColumnBefore().run() - }, [editor]) - - const onAddColumnAfter = useCallback(() => { - editor.chain().focus().addColumnAfter().run() - }, [editor]) - - const onDeleteColumn = useCallback(() => { - editor.chain().focus().deleteColumn().run() - }, [editor]) - - return ( - { - return appendTo?.current - }, - offset: [0, 15], - popperOptions: { - modifiers: [{ name: 'flip', enabled: false }], - }, - }} - shouldShow={shouldShow} - > - - } - close={false} - label="Add column before" - onClick={onAddColumnBefore} - /> - } - close={false} - label="Add column after" - onClick={onAddColumnAfter} - /> - - - - ) -}) - -TableColumnMenu.displayName = 'TableColumnMenu' - -export default TableColumnMenu diff --git a/lib/block-editor/extensions/Table/menus/TableColumn/utils.ts b/lib/block-editor/extensions/Table/menus/TableColumn/utils.ts deleted file mode 100644 index d713158e..00000000 --- a/lib/block-editor/extensions/Table/menus/TableColumn/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Editor } from '@tiptap/react' -import { EditorState } from '@tiptap/pm/state' -import { EditorView } from '@tiptap/pm/view' - -import { isTableSelected } from '../../utils' -import { Table } from '../..' - -export const isColumnGripSelected = ({ - editor, - view, - state, - from, -}: { - editor: Editor - view: EditorView - state: EditorState - from: number -}) => { - const domAtPos = view.domAtPos(from).node as HTMLElement - const nodeDOM = view.nodeDOM(from) as HTMLElement - const node = nodeDOM || domAtPos - - if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) { - return false - } - - let container = node - - while (container && !['TD', 'TH'].includes(container.tagName)) { - container = container.parentElement! - } - - const gripColumn = container && container.querySelector && container.querySelector('a.grip-column.selected') - - return !!gripColumn -} - -export default isColumnGripSelected diff --git a/lib/block-editor/extensions/Table/menus/TableRow/index.tsx b/lib/block-editor/extensions/Table/menus/TableRow/index.tsx deleted file mode 100644 index 5f9aebf7..00000000 --- a/lib/block-editor/extensions/Table/menus/TableRow/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { BubbleMenu as BaseBubbleMenu } from '@tiptap/react' -import React, { useCallback } from 'react' -import * as PopoverMenu from '@/components/ui/PopoverMenu' - -import { Toolbar } from '@/components/ui/Toolbar' -import { isRowGripSelected } from './utils' -import { Icon } from '@/components/ui/Icon' -import { MenuProps, ShouldShowProps } from '@/components/menus/types' - -export const TableRowMenu = React.memo(({ editor, appendTo }: MenuProps): JSX.Element => { - const shouldShow = useCallback( - ({ view, state, from }: ShouldShowProps) => { - if (!state || !from) { - return false - } - - return isRowGripSelected({ editor, view, state, from }) - }, - [editor], - ) - - const onAddRowBefore = useCallback(() => { - editor.chain().focus().addRowBefore().run() - }, [editor]) - - const onAddRowAfter = useCallback(() => { - editor.chain().focus().addRowAfter().run() - }, [editor]) - - const onDeleteRow = useCallback(() => { - editor.chain().focus().deleteRow().run() - }, [editor]) - - return ( - { - return appendTo?.current - }, - placement: 'left', - offset: [0, 15], - popperOptions: { - modifiers: [{ name: 'flip', enabled: false }], - }, - }} - shouldShow={shouldShow} - > - - } - close={false} - label="Add row before" - onClick={onAddRowBefore} - /> - } - close={false} - label="Add row after" - onClick={onAddRowAfter} - /> - - - - ) -}) - -TableRowMenu.displayName = 'TableRowMenu' - -export default TableRowMenu diff --git a/lib/block-editor/extensions/Table/menus/TableRow/utils.ts b/lib/block-editor/extensions/Table/menus/TableRow/utils.ts deleted file mode 100644 index 57646b3f..00000000 --- a/lib/block-editor/extensions/Table/menus/TableRow/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Editor } from '@tiptap/react' -import { EditorState } from '@tiptap/pm/state' -import { EditorView } from '@tiptap/pm/view' - -import { isTableSelected } from '../../utils' -import { Table } from '../..' - -export const isRowGripSelected = ({ - editor, - view, - state, - from, -}: { - editor: Editor - view: EditorView - state: EditorState - from: number -}) => { - const domAtPos = view.domAtPos(from).node as HTMLElement - const nodeDOM = view.nodeDOM(from) as HTMLElement - const node = nodeDOM || domAtPos - - if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) { - return false - } - - let container = node - - while (container && !['TD', 'TH'].includes(container.tagName)) { - container = container.parentElement! - } - - const gripRow = container && container.querySelector && container.querySelector('a.grip-row.selected') - - return !!gripRow -} - -export default isRowGripSelected diff --git a/lib/block-editor/extensions/Table/menus/index.tsx b/lib/block-editor/extensions/Table/menus/index.tsx deleted file mode 100644 index 2f8e1fe8..00000000 --- a/lib/block-editor/extensions/Table/menus/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from './TableColumn' -export * from './TableRow' diff --git a/lib/block-editor/extensions/Table/utils.ts b/lib/block-editor/extensions/Table/utils.ts deleted file mode 100644 index 4f321e42..00000000 --- a/lib/block-editor/extensions/Table/utils.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { findParentNode } from '@tiptap/core' -import { Selection, Transaction } from '@tiptap/pm/state' -import { CellSelection, Rect, TableMap } from '@tiptap/pm/tables' -import { Node, ResolvedPos } from '@tiptap/pm/model' - -export const isRectSelected = (rect: Rect) => (selection: CellSelection) => { - const map = TableMap.get(selection.$anchorCell.node(-1)) - const start = selection.$anchorCell.start(-1) - const cells = map.cellsInRect(rect) - const selectedCells = map.cellsInRect( - map.rectBetween(selection.$anchorCell.pos - start, selection.$headCell.pos - start), - ) - - for (let i = 0, count = cells.length; i < count; i += 1) { - if (selectedCells.indexOf(cells[i]) === -1) { - return false - } - } - - return true -} - -export const findTable = (selection: Selection) => - findParentNode(node => node.type.spec.tableRole && node.type.spec.tableRole === 'table')(selection) - -export const isCellSelection = (selection: Selection): selection is CellSelection => selection instanceof CellSelection - -export const isColumnSelected = (columnIndex: number) => (selection: Selection) => { - if (isCellSelection(selection)) { - const map = TableMap.get(selection.$anchorCell.node(-1)) - - return isRectSelected({ - left: columnIndex, - right: columnIndex + 1, - top: 0, - bottom: map.height, - })(selection) - } - - return false -} - -export const isRowSelected = (rowIndex: number) => (selection: Selection) => { - if (isCellSelection(selection)) { - const map = TableMap.get(selection.$anchorCell.node(-1)) - - return isRectSelected({ - left: 0, - right: map.width, - top: rowIndex, - bottom: rowIndex + 1, - })(selection) - } - - return false -} - -export const isTableSelected = (selection: Selection) => { - if (isCellSelection(selection)) { - const map = TableMap.get(selection.$anchorCell.node(-1)) - - return isRectSelected({ - left: 0, - right: map.width, - top: 0, - bottom: map.height, - })(selection) - } - - return false -} - -export const getCellsInColumn = (columnIndex: number | number[]) => (selection: Selection) => { - const table = findTable(selection) - if (table) { - const map = TableMap.get(table.node) - const indexes = Array.isArray(columnIndex) ? columnIndex : Array.from([columnIndex]) - - return indexes.reduce( - (acc, index) => { - if (index >= 0 && index <= map.width - 1) { - const cells = map.cellsInRect({ - left: index, - right: index + 1, - top: 0, - bottom: map.height, - }) - - return acc.concat( - cells.map(nodePos => { - const node = table.node.nodeAt(nodePos) - const pos = nodePos + table.start - - return { pos, start: pos + 1, node } - }), - ) - } - - return acc - }, - [] as { pos: number; start: number; node: Node | null | undefined }[], - ) - } - return null -} - -export const getCellsInRow = (rowIndex: number | number[]) => (selection: Selection) => { - const table = findTable(selection) - - if (table) { - const map = TableMap.get(table.node) - const indexes = Array.isArray(rowIndex) ? rowIndex : Array.from([rowIndex]) - - return indexes.reduce( - (acc, index) => { - if (index >= 0 && index <= map.height - 1) { - const cells = map.cellsInRect({ - left: 0, - right: map.width, - top: index, - bottom: index + 1, - }) - - return acc.concat( - cells.map(nodePos => { - const node = table.node.nodeAt(nodePos) - const pos = nodePos + table.start - return { pos, start: pos + 1, node } - }), - ) - } - - return acc - }, - [] as { pos: number; start: number; node: Node | null | undefined }[], - ) - } - - return null -} - -export const getCellsInTable = (selection: Selection) => { - const table = findTable(selection) - - if (table) { - const map = TableMap.get(table.node) - const cells = map.cellsInRect({ - left: 0, - right: map.width, - top: 0, - bottom: map.height, - }) - - return cells.map(nodePos => { - const node = table.node.nodeAt(nodePos) - const pos = nodePos + table.start - - return { pos, start: pos + 1, node } - }) - } - - return null -} - -export const findParentNodeClosestToPos = ($pos: ResolvedPos, predicate: (node: Node) => boolean) => { - for (let i = $pos.depth; i > 0; i -= 1) { - const node = $pos.node(i) - - if (predicate(node)) { - return { - pos: i > 0 ? $pos.before(i) : 0, - start: $pos.start(i), - depth: i, - node, - } - } - } - - return null -} - -export const findCellClosestToPos = ($pos: ResolvedPos) => { - const predicate = (node: Node) => node.type.spec.tableRole && /cell/i.test(node.type.spec.tableRole) - - return findParentNodeClosestToPos($pos, predicate) -} - -const select = (type: 'row' | 'column') => (index: number) => (tr: Transaction) => { - const table = findTable(tr.selection) - const isRowSelection = type === 'row' - - if (table) { - const map = TableMap.get(table.node) - - // Check if the index is valid - if (index >= 0 && index < (isRowSelection ? map.height : map.width)) { - const left = isRowSelection ? 0 : index - const top = isRowSelection ? index : 0 - const right = isRowSelection ? map.width : index + 1 - const bottom = isRowSelection ? index + 1 : map.height - - const cellsInFirstRow = map.cellsInRect({ - left, - top, - right: isRowSelection ? right : left + 1, - bottom: isRowSelection ? top + 1 : bottom, - }) - - const cellsInLastRow = - bottom - top === 1 - ? cellsInFirstRow - : map.cellsInRect({ - left: isRowSelection ? left : right - 1, - top: isRowSelection ? bottom - 1 : top, - right, - bottom, - }) - - const head = table.start + cellsInFirstRow[0] - const anchor = table.start + cellsInLastRow[cellsInLastRow.length - 1] - const $head = tr.doc.resolve(head) - const $anchor = tr.doc.resolve(anchor) - - return tr.setSelection(new CellSelection($anchor, $head)) - } - } - return tr -} - -export const selectColumn = select('column') - -export const selectRow = select('row') - -export const selectTable = (tr: Transaction) => { - const table = findTable(tr.selection) - - if (table) { - const { map } = TableMap.get(table.node) - - if (map && map.length) { - const head = table.start + map[0] - const anchor = table.start + map[map.length - 1] - const $head = tr.doc.resolve(head) - const $anchor = tr.doc.resolve(anchor) - - return tr.setSelection(new CellSelection($anchor, $head)) - } - } - - return tr -} diff --git a/lib/block-editor/extensions/TrailingNode/index.ts b/lib/block-editor/extensions/TrailingNode/index.ts deleted file mode 100644 index 977a62f0..00000000 --- a/lib/block-editor/extensions/TrailingNode/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './trailing-node' diff --git a/lib/block-editor/extensions/TrailingNode/trailing-node.ts b/lib/block-editor/extensions/TrailingNode/trailing-node.ts deleted file mode 100644 index 4f0d9937..00000000 --- a/lib/block-editor/extensions/TrailingNode/trailing-node.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Extension } from '@tiptap/core' -import { Plugin, PluginKey } from '@tiptap/pm/state' - -// @ts-ignore -function nodeEqualsType({ types, node }) { - return (Array.isArray(types) && types.includes(node.type)) || node.type === types -} - -/** - * Extension based on: - * - https://github.com/ueberdosis/tiptap/blob/v1/packages/tiptap-extensions/src/extensions/TrailingNode.js - * - https://github.com/remirror/remirror/blob/e0f1bec4a1e8073ce8f5500d62193e52321155b9/packages/prosemirror-trailing-node/src/trailing-node-plugin.ts - */ - -export interface TrailingNodeOptions { - node: string - notAfter: string[] -} - -export const TrailingNode = Extension.create({ - name: 'trailingNode', - - addOptions() { - return { - node: 'paragraph', - notAfter: ['paragraph'], - } - }, - - addProseMirrorPlugins() { - const plugin = new PluginKey(this.name) - const disabledNodes = Object.entries(this.editor.schema.nodes) - .map(([, value]) => value) - .filter(node => this.options.notAfter.includes(node.name)) - - return [ - new Plugin({ - key: plugin, - appendTransaction: (_, __, state) => { - const { doc, tr, schema } = state - const shouldInsertNodeAtEnd = plugin.getState(state) - const endPosition = doc.content.size - const type = schema.nodes[this.options.node] - - if (!shouldInsertNodeAtEnd) { - return - } - - // eslint-disable-next-line consistent-return - return tr.insert(endPosition, type.create()) - }, - state: { - init: (_, state) => { - const lastNode = state.tr.doc.lastChild - - return !nodeEqualsType({ node: lastNode, types: disabledNodes }) - }, - apply: (tr, value) => { - if (!tr.docChanged) { - return value - } - - const lastNode = tr.doc.lastChild - - return !nodeEqualsType({ node: lastNode, types: disabledNodes }) - }, - }, - }), - ] - }, -}) diff --git a/lib/block-editor/extensions/extension-kit.ts b/lib/block-editor/extensions/extension-kit.ts index 8547d7d7..9452ddbf 100644 --- a/lib/block-editor/extensions/extension-kit.ts +++ b/lib/block-editor/extensions/extension-kit.ts @@ -1,13 +1,43 @@ import { mergeAttributes } from '@tiptap/core'; +import BulletList from '@tiptap/extension-bullet-list'; +import Document from '@tiptap/extension-document'; +import Dropcursor from '@tiptap/extension-dropcursor'; import Heading from '@tiptap/extension-heading'; import Paragraph from '@tiptap/extension-paragraph'; import Text from '@tiptap/extension-text'; +import Typography from '@tiptap/extension-typography'; +import StarterKit from '@tiptap/starter-kit'; +import { Underline } from 'lucide-react'; +import AutoJoiner from 'tiptap-extension-auto-joiner'; import GlobalDragHandle from 'tiptap-extension-global-drag-handle'; import { headingVariants } from '~/components/typography/Heading'; import { paragraphVariants } from '~/components/typography/Paragraph'; -import { Document } from './Document'; +import { unorderedListClasses } from '~/components/typography/UnorderedList'; +import { HorizontalRule } from './HorizontalRule'; +import { ImageBlock } from './ImageBlock'; +import { Link } from './Link'; +import { Column, Columns } from './MultiColumn'; +import { Selection } from './Selection'; +import { SlashCommand } from './SlashCommand'; export const ExtensionKit = () => [ + StarterKit.configure({ + document: false, + dropcursor: false, + heading: false, + paragraph: false, + text: false, + bulletList: false, + horizontalRule: false, + blockquote: false, + history: false, + codeBlock: false, + }), + BulletList.configure({ + HTMLAttributes: { + class: unorderedListClasses, + }, + }), Document, Heading.extend({ levels: [1, 2, 3, 4], @@ -41,66 +71,22 @@ export const ExtensionKit = () => [ }), Text, GlobalDragHandle, - // Columns, - // TaskList, - // TaskItem.configure({ - // nested: true, - // }), - // Column, - // Selection, - // Heading.configure({ - // levels: [1, 2, 3, 4, 5, 6], - // }), - // HorizontalRule, - // StarterKit.configure({ - // document: false, - // dropcursor: false, - // heading: false, - // horizontalRule: false, - // blockquote: false, - // history: false, - // codeBlock: false, - // }), - // CodeBlock, - // TextStyle, - // FontSize, - // FontFamily, - // Color, - // TrailingNode, - // Link.configure({ - // openOnClick: false, - // }), - // Highlight.configure({ multicolor: true }), - // Underline, - // CharacterCount.configure({ limit: 50000 }), - // ImageBlock, - // TextAlign.extend({ - // addKeyboardShortcuts() { - // return {}; - // }, - // }).configure({ - // types: ['heading', 'paragraph'], - // }), - // Subscript, - // Superscript, - // Table, - // TableCell, - // TableHeader, - // TableRow, - // Typography, - // Placeholder.configure({ - // includeChildren: true, - // showOnlyCurrent: false, - // placeholder: () => '', - // }), - // SlashCommand, - // Focus, - // Figcaption, - // BlockquoteFigure, - // Dropcursor.configure({ - // width: 2, - // class: 'ProseMirror-dropcursor border-black', - // }), + AutoJoiner, + Columns, + Column, + Selection, + HorizontalRule, + Link.configure({ + openOnClick: false, + }), + Underline, + ImageBlock, + Typography, + SlashCommand, + Dropcursor.configure({ + width: 2, + class: '', + }), ]; export default ExtensionKit; diff --git a/lib/block-editor/extensions/index.ts b/lib/block-editor/extensions/index.ts deleted file mode 100644 index 7972c91f..00000000 --- a/lib/block-editor/extensions/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -'use client'; - -export { CharacterCount } from '@tiptap/extension-character-count'; -export { Highlight } from '@tiptap/extension-highlight'; -export { Placeholder } from '@tiptap/extension-placeholder'; -export { Underline } from '@tiptap/extension-underline'; -// export { Emoji, gitHubEmojis } from '@tiptap-pro/extension-emoji' -export { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'; -export { Color } from '@tiptap/extension-color'; -export { Dropcursor } from '@tiptap/extension-dropcursor'; -export { FocusClasses as Focus } from '@tiptap/extension-focus'; -export { FontFamily } from '@tiptap/extension-font-family'; -export { Subscript } from '@tiptap/extension-subscript'; -export { TextAlign } from '@tiptap/extension-text-align'; -export { TextStyle } from '@tiptap/extension-text-style'; -export { Typography } from '@tiptap/extension-typography'; -// export { TableOfContents } from '@tiptap-pro/extension-table-of-contents' -export { BulletList } from '@tiptap/extension-bullet-list'; -export { Collaboration } from '@tiptap/extension-collaboration'; -export { OrderedList } from '@tiptap/extension-ordered-list'; -export { Paragraph } from '@tiptap/extension-paragraph'; -export { Superscript } from '@tiptap/extension-superscript'; -export { TaskItem } from '@tiptap/extension-task-item'; -export { TaskList } from '@tiptap/extension-task-list'; -// export { FileHandler } from '@tiptap-pro/extension-file-handler' -// export { Details } from '@tiptap-pro/extension-details' -// export { DetailsContent } from '@tiptap-pro/extension-details-content' -// export { DetailsSummary } from '@tiptap-pro/extension-details-summary' -// export { UniqueID } from '@tiptap-pro/extension-unique-id' - -export { BlockquoteFigure } from './BlockquoteFigure'; -export { Quote } from './BlockquoteFigure/Quote'; -export { QuoteCaption } from './BlockquoteFigure/QuoteCaption'; -export { CodeBlock } from './CodeBlock'; -export { Document } from './Document'; -export { emojiSuggestion } from './EmojiSuggestion'; -export { Figcaption } from './Figcaption'; -export { Figure } from './Figure'; -export { FontSize } from './FontSize'; -export { Heading } from './Heading'; -export { HorizontalRule } from './HorizontalRule'; -export { ImageBlock } from './ImageBlock'; -export { Link } from './Link'; -export { Column, Columns } from './MultiColumn'; -export { Selection } from './Selection'; -export { SlashCommand } from './SlashCommand'; -export { Table, TableCell, TableHeader, TableRow } from './Table'; -export { TrailingNode } from './TrailingNode'; diff --git a/lib/block-editor/useBlockEditor.tsx b/lib/block-editor/useBlockEditor.tsx index 11549ac9..815f6329 100644 --- a/lib/block-editor/useBlockEditor.tsx +++ b/lib/block-editor/useBlockEditor.tsx @@ -14,7 +14,7 @@ export const useBlockEditor = () => { autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', - class: 'min-h-full', + class: 'min-h-full focus:outline-none', }, }, content: ` @@ -28,8 +28,10 @@ export const useBlockEditor = () => { Another heading

- Hello, world! + Text following h2.

+
  • Unordered list item
  • another item
+ `, }, [], // Dependency array diff --git a/package.json b/package.json index 7811cc1d..097d0cb5 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,22 @@ "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/container-queries": "^0.1.1", "@tiptap/core": "^2.9.1", + "@tiptap/extension-bullet-list": "^2.9.1", "@tiptap/extension-document": "^2.9.1", + "@tiptap/extension-dropcursor": "^2.9.1", "@tiptap/extension-heading": "^2.9.1", + "@tiptap/extension-horizontal-rule": "^2.9.1", + "@tiptap/extension-image": "^2.9.1", + "@tiptap/extension-link": "^2.9.1", + "@tiptap/extension-list-item": "^2.9.1", "@tiptap/extension-paragraph": "^2.9.1", "@tiptap/extension-text": "^2.9.1", + "@tiptap/extension-typography": "^2.9.1", + "@tiptap/extension-underline": "^2.9.1", "@tiptap/pm": "^2.9.1", "@tiptap/react": "^2.9.1", + "@tiptap/starter-kit": "^2.9.1", + "@tiptap/suggestion": "^2.9.1", "@types/rtl-detect": "^1.0.3", "@types/validator": "^13.12.1", "@typescript-eslint/eslint-plugin": "^7.18.0", @@ -71,6 +81,8 @@ "sharp": "^0.33.5", "tailwind-merge": "^2.5.2", "tailwind-variants": "^0.2.1", + "tippy.js": "^6.3.7", + "tiptap-extension-auto-joiner": "^0.1.3", "tiptap-extension-global-drag-handle": "^0.1.15", "type-fest": "^4.26.1", "validator": "^13.12.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c5956cd..bd383288 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,24 +67,54 @@ importers: '@tiptap/core': specifier: ^2.9.1 version: 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-bullet-list': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) '@tiptap/extension-document': specifier: ^2.9.1 version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-dropcursor': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) '@tiptap/extension-heading': specifier: ^2.9.1 version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-horizontal-rule': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-image': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-link': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-list-item': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) '@tiptap/extension-paragraph': specifier: ^2.9.1 version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) '@tiptap/extension-text': specifier: ^2.9.1 version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-typography': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-underline': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) '@tiptap/pm': specifier: ^2.9.1 version: 2.9.1 '@tiptap/react': specifier: ^2.9.1 version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@tiptap/starter-kit': + specifier: ^2.9.1 + version: 2.9.1 + '@tiptap/suggestion': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) '@types/rtl-detect': specifier: ^1.0.3 version: 1.0.3 @@ -172,6 +202,12 @@ importers: tailwind-variants: specifier: ^0.2.1 version: 0.2.1(tailwindcss@3.4.12) + tippy.js: + specifier: ^6.3.7 + version: 6.3.7 + tiptap-extension-auto-joiner: + specifier: ^0.1.3 + version: 0.1.3 tiptap-extension-global-drag-handle: specifier: ^0.1.15 version: 0.1.15 @@ -2596,38 +2632,139 @@ packages: peerDependencies: '@tiptap/pm': ^2.7.0 + '@tiptap/extension-blockquote@2.9.1': + resolution: {integrity: sha512-Y0jZxc/pdkvcsftmEZFyG+73um8xrx6/DMfgUcNg3JAM63CISedNcr+OEI11L0oFk1KFT7/aQ9996GM6Kubdqg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bold@2.9.1': + resolution: {integrity: sha512-e2P1zGpnnt4+TyxTC5pX/lPxPasZcuHCYXY0iwQ3bf8qRQQEjDfj3X7EI+cXqILtnhOiviEOcYmeu5op2WhQDg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-bubble-menu@2.9.1': resolution: {integrity: sha512-DWUF6NG08/bZDWw0jCeotSTvpkyqZTi4meJPomG9Wzs/Ol7mEwlNCsCViD999g0+IjyXFatBk4DfUq1YDDu++Q==} peerDependencies: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-bullet-list@2.9.1': + resolution: {integrity: sha512-0hizL/0j9PragJObjAWUVSuGhN1jKjCFnhLQVRxtx4HutcvS/lhoWMvFg6ZF8xqWgIa06n6A7MaknQkqhTdhKA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-code-block@2.9.1': + resolution: {integrity: sha512-A/50wPWDqEUUUPhrwRKILP5gXMO5UlQ0F6uBRGYB9CEVOREam9yIgvONOnZVJtszHqOayjIVMXbH/JMBeq11/g==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-code@2.9.1': + resolution: {integrity: sha512-WQqcVGe7i/E+yO3wz5XQteU1ETNZ00euUEl4ylVVmH2NM4Dh0KDjEhbhHlCM0iCfLUo7jhjC7dmS+hMdPUb+Tg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-document@2.9.1': resolution: {integrity: sha512-1a+HCoDPnBttjqExfYLwfABq8MYdiowhy/wp8eCxVb6KGFEENO53KapstISvPzqH7eOi+qRjBB1KtVYb/ZXicg==} peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-dropcursor@2.9.1': + resolution: {integrity: sha512-wJZspSmJRkDBtPkzFz1g7gvZOEOayk8s93UHsgbJxcV4VWHYleZ5XhT74sZunSjefNDm3qC6v2BSgLp3vNHVKQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@tiptap/extension-floating-menu@2.9.1': resolution: {integrity: sha512-MxZ7acNNsoNaKpetxfwi3Z11Bgrh0T2EJlCV77v9N1vWK38+st3H1WJanmLbPNtc2ocvhHJrz+DjDz3CWxQ9rQ==} peerDependencies: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-gapcursor@2.9.1': + resolution: {integrity: sha512-jsRBmX01vr+5H02GljiHMo0n5H1vzoMLmFarxe0Yq2d2l9G/WV2VWX2XnGliqZAYWd1bI0phs7uLQIN3mxGQTw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-hard-break@2.9.1': + resolution: {integrity: sha512-fCuaOD/b7nDjm47PZ58oanq7y4ccS2wjPh42Qm0B0yipu/1fmC8eS1SmaXmk28F89BLtuL6uOCtR1spe+lZtlQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-heading@2.9.1': resolution: {integrity: sha512-SjZowzLixOFaCrV2cMaWi1mp8REK0zK1b3OcVx7bCZfVSmsOETJyrAIUpCKA8o60NwF7pwhBg0MN8oXlNKMeFw==} peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-history@2.9.1': + resolution: {integrity: sha512-wp9qR1NM+LpvyLZFmdNaAkDq0d4jDJ7z7Fz7icFQPu31NVxfQYO3IXNmvJDCNu8hFAbImpA5aG8MBuwzRo0H9w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-horizontal-rule@2.9.1': + resolution: {integrity: sha512-ydUhABeaBI1CoJp+/BBqPhXINfesp1qMNL/jiDcMsB66fsD4nOyphpAJT7FaRFZFtQVF06+nttBtFZVkITQVqg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-image@2.9.1': + resolution: {integrity: sha512-aGqJnsuS8oagIhsx7wetm8jw4NEDsOV0OSx4FQ4VPlUqWlnzK0N+erFKKJmXTdAxL8PGzoPSlITFH63MV3eV3Q==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-italic@2.9.1': + resolution: {integrity: sha512-VkNA6Vz96+/+7uBlsgM7bDXXx4b62T1fDam/3UKifA72aD/fZckeWrbT7KrtdUbzuIniJSbA0lpTs5FY29+86Q==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-link@2.9.1': + resolution: {integrity: sha512-yG+e3e8cCCN9dZjX4ttEe3e2xhh58ryi3REJV4MdiEkOT9QF75Bl5pUbMIS4tQ8HkOr04QBFMHKM12kbSxg1BA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-list-item@2.9.1': + resolution: {integrity: sha512-6O4NtYNR5N2Txi4AC0/4xMRJq9xd4+7ShxCZCDVL0WDVX37IhaqMO7LGQtA6MVlYyNaX4W1swfdJaqrJJ5HIUw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-ordered-list@2.9.1': + resolution: {integrity: sha512-6J9jtv1XP8dW7/JNSH/K4yiOABc92tBJtgCsgP8Ep4+fjfjdj4HbjS1oSPWpgItucF2Fp/VF8qg55HXhjxHjTw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-paragraph@2.9.1': resolution: {integrity: sha512-JOmT0xd4gd3lIhLwrsjw8lV+ZFROKZdIxLi0Ia05XSu4RLrrvWj0zdKMSB+V87xOWfSB3Epo95zAvnPox5Q16A==} peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-strike@2.9.1': + resolution: {integrity: sha512-V5aEXdML+YojlPhastcu7w4biDPwmzy/fWq0T2qjfu5Te/THcqDmGYVBKESBm5x6nBy5OLkanw2O+KHu2quDdg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text-style@2.9.1': + resolution: {integrity: sha512-LAxc0SeeiPiAVBwksczeA7BJSZb6WtVpYhy5Esvy9K0mK5kttB4KxtnXWeQzMIJZQbza65yftGKfQlexf/Y7yg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-text@2.9.1': resolution: {integrity: sha512-3wo9uCrkLVLQFgbw2eFU37QAa1jq1/7oExa+FF/DVxdtHRS9E2rnUZ8s2hat/IWzvPUHXMwo3Zg2XfhoamQpCA==} peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-typography@2.9.1': + resolution: {integrity: sha512-HX0kghh+Gmlp5FsVVGmQNRxxA+aErLBgmKVspycJ3UHzAkyzsdx4qM19KCZ3pMOI+kxcXF9cMh3QxJYJ+OQ7wg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-underline@2.9.1': + resolution: {integrity: sha512-IrUsIqKPgD7GcAjr4D+RC0WvLHUDBTMkD8uPNEoeD1uH9t9zFyDfMRPnx/z3/6Gf6fTh3HzLcHGibiW2HiMi2A==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm@2.9.1': resolution: {integrity: sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==} @@ -2639,6 +2776,15 @@ packages: react: ^17.0.0 || ^18.0.0 react-dom: ^17.0.0 || ^18.0.0 + '@tiptap/starter-kit@2.9.1': + resolution: {integrity: sha512-nsw6UF/7wDpPfHRhtGOwkj1ipIEiWZS1VGw+c14K61vM1CNj0uQ4jogbHwHZqN1dlL5Hh+FCqUHDPxG6ECbijg==} + + '@tiptap/suggestion@2.9.1': + resolution: {integrity: sha512-MMxwpbtocxUsbmc8qtFY1AQYNTW5i/M4aNSv9zsKKRISaS5hMD7XVrw2eod0x0yEqZU3izLiPDZPmgr8glF+jQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@total-typescript/ts-reset@0.5.1': resolution: {integrity: sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==} @@ -4566,6 +4712,9 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + linkifyjs@4.1.3: + resolution: {integrity: sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==} + loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -5900,6 +6049,9 @@ packages: tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + tiptap-extension-auto-joiner@0.1.3: + resolution: {integrity: sha512-nY3aKeCpVb2WjjVEZkLtEqxsK3KU1zGioyglMhK1sUFNjKDccOfRyz/YDKrHRAVsKJPGnk2A8VA1827iGEAXWQ==} + tiptap-extension-global-drag-handle@0.1.15: resolution: {integrity: sha512-gpKXzeB4xtg3klhADRqkvoU9F0TCdlDmNtAO5J4SZgxWEfZ8/KNVdPTWlwiKPmOYYrgPnyFd53f6g+mAGoofng==} @@ -8805,34 +8957,117 @@ snapshots: dependencies: '@tiptap/pm': 2.9.1 + '@tiptap/extension-blockquote@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-bold@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-bubble-menu@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': dependencies: '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) '@tiptap/pm': 2.9.1 tippy.js: 6.3.7 + '@tiptap/extension-bullet-list@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-code-block@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-code@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-document@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': dependencies: '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-dropcursor@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + '@tiptap/extension-floating-menu@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': dependencies: '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) '@tiptap/pm': 2.9.1 tippy.js: 6.3.7 + '@tiptap/extension-gapcursor@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-hard-break@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-heading@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': dependencies: '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-history@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-horizontal-rule@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-image@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-italic@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-link@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + linkifyjs: 4.1.3 + + '@tiptap/extension-list-item@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-ordered-list@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-paragraph@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': dependencies: '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-strike@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-text-style@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-text@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': dependencies: '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-typography@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-underline@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm@2.9.1': dependencies: prosemirror-changeset: 2.2.1 @@ -8866,6 +9101,35 @@ snapshots: react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) use-sync-external-store: 1.2.2(react@19.0.0-rc-e740d4b1-20240919) + '@tiptap/starter-kit@2.9.1': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-blockquote': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-bold': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-bullet-list': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-code': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-code-block': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-document': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-dropcursor': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-gapcursor': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-hard-break': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-heading': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-history': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-horizontal-rule': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-italic': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-list-item': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-ordered-list': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-paragraph': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-strike': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-text': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-text-style': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/pm': 2.9.1 + + '@tiptap/suggestion@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + '@total-typescript/ts-reset@0.5.1': {} '@tybys/wasm-util@0.8.3': @@ -11221,6 +11485,8 @@ snapshots: dependencies: uc.micro: 2.1.0 + linkifyjs@4.1.3: {} + loader-runner@4.3.0: {} loader-utils@2.0.4: @@ -12689,6 +12955,8 @@ snapshots: dependencies: '@popperjs/core': 2.11.8 + tiptap-extension-auto-joiner@0.1.3: {} + tiptap-extension-global-drag-handle@0.1.15: {} to-fast-properties@2.0.0: {} diff --git a/styles/global.css b/styles/global.css index a461eb9a..b3470995 100644 --- a/styles/global.css +++ b/styles/global.css @@ -40,41 +40,36 @@ .focusable { @apply ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2; } +} - .drag-handle { - position: fixed; - opacity: 1; - transition: opacity ease-in 0.2s; - border-radius: 0.25rem; - - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(0, 0, 0, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E"); - background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem); - background-repeat: no-repeat; - background-position: center; - width: 1.2rem; - height: 1.5rem; - z-index: 50; - cursor: grab; +.drag-handle { + position: fixed; + opacity: 1; + transition: opacity ease-in 0.2s; + border-radius: 0.25rem; - &:hover { - background-color: red; - transition: background-color 0.2s; - } + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(0, 0, 0, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E"); + background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem); + background-repeat: no-repeat; + background-position: center; + width: 1.2rem; + height: 1.5rem; + z-index: 50; + cursor: grab; - &:active { - background-color: green; - transition: background-color 0.2s; - cursor: grabbing; - } + &:hover { + background-color: red; + transition: background-color 0.2s; + } - &.hide { - opacity: 0; - pointer-events: none; - } + &:active { + background-color: green; + transition: background-color 0.2s; + cursor: grabbing; + } - @media screen and (max-width: 600px) { - display: none; - pointer-events: none; - } + &.hide { + opacity: 0; + pointer-events: none; } } From ee60603f880740e09ee6801b0ae5135ec3d44757 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Wed, 30 Oct 2024 16:20:54 +0200 Subject: [PATCH 04/58] remove figure --- lib/block-editor/extensions/Figure/Figure.ts | 62 ------------------- lib/block-editor/extensions/Figure/index.ts | 1 - .../HorizontalRule/HorizontalRule.ts | 10 --- .../HorizontalRule/HorizontalRule.tsx | 17 +++++ 4 files changed, 17 insertions(+), 73 deletions(-) delete mode 100644 lib/block-editor/extensions/Figure/Figure.ts delete mode 100644 lib/block-editor/extensions/Figure/index.ts delete mode 100644 lib/block-editor/extensions/HorizontalRule/HorizontalRule.ts create mode 100644 lib/block-editor/extensions/HorizontalRule/HorizontalRule.tsx diff --git a/lib/block-editor/extensions/Figure/Figure.ts b/lib/block-editor/extensions/Figure/Figure.ts deleted file mode 100644 index afa98ce8..00000000 --- a/lib/block-editor/extensions/Figure/Figure.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { mergeAttributes, Node } from '@tiptap/core' -import { Plugin } from '@tiptap/pm/state' - -export const Figure = Node.create({ - name: 'figure', - - addOptions() { - return { - HTMLAttributes: {}, - } - }, - - group: 'block', - - content: 'block figcaption', - - draggable: true, - - defining: true, - - selectable: true, - - parseHTML() { - return [ - { - tag: `figure[data-type="${this.name}"]`, - }, - ] - }, - - renderHTML({ HTMLAttributes }) { - return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), 0] - }, - - addProseMirrorPlugins() { - return [ - new Plugin({ - props: { - handleDOMEvents: { - // Prevent dragging child nodes from figure - dragstart: (view, event) => { - if (!event.target) { - return false - } - - const pos = view.posAtDOM(event.target as HTMLElement, 0) - const $pos = view.state.doc.resolve(pos) - - if ($pos.parent.type.name === this.type.name) { - event.preventDefault() - } - - return false - }, - }, - }, - }), - ] - }, -}) - -export default Figure diff --git a/lib/block-editor/extensions/Figure/index.ts b/lib/block-editor/extensions/Figure/index.ts deleted file mode 100644 index 7ddab7db..00000000 --- a/lib/block-editor/extensions/Figure/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Figure' diff --git a/lib/block-editor/extensions/HorizontalRule/HorizontalRule.ts b/lib/block-editor/extensions/HorizontalRule/HorizontalRule.ts deleted file mode 100644 index 650e6e3d..00000000 --- a/lib/block-editor/extensions/HorizontalRule/HorizontalRule.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { mergeAttributes } from '@tiptap/core' -import TiptapHorizontalRule from '@tiptap/extension-horizontal-rule' - -export const HorizontalRule = TiptapHorizontalRule.extend({ - renderHTML() { - return ['div', mergeAttributes(this.options.HTMLAttributes, { 'data-type': this.name }), ['hr']] - }, -}) - -export default HorizontalRule diff --git a/lib/block-editor/extensions/HorizontalRule/HorizontalRule.tsx b/lib/block-editor/extensions/HorizontalRule/HorizontalRule.tsx new file mode 100644 index 00000000..5e7b685a --- /dev/null +++ b/lib/block-editor/extensions/HorizontalRule/HorizontalRule.tsx @@ -0,0 +1,17 @@ +import TiptapHorizontalRule from '@tiptap/extension-horizontal-rule'; +import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'; +import Divider from '~/components/layout/Divider'; + +const WrappedDivider = () => ( + + + +); + +export const HorizontalRule = TiptapHorizontalRule.extend({ + addNodeView() { + return ReactNodeViewRenderer(WrappedDivider); + }, +}); + +export default HorizontalRule; From cd298219c696a71368c8f23f8528a6d3b7989cbb Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Wed, 30 Oct 2024 16:54:52 +0200 Subject: [PATCH 05/58] add nodes --- lib/block-editor/extensions/extension-kit.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/block-editor/extensions/extension-kit.ts b/lib/block-editor/extensions/extension-kit.ts index 9452ddbf..e1e6fef3 100644 --- a/lib/block-editor/extensions/extension-kit.ts +++ b/lib/block-editor/extensions/extension-kit.ts @@ -33,11 +33,6 @@ export const ExtensionKit = () => [ history: false, codeBlock: false, }), - BulletList.configure({ - HTMLAttributes: { - class: unorderedListClasses, - }, - }), Document, Heading.extend({ levels: [1, 2, 3, 4], @@ -70,8 +65,13 @@ export const ExtensionKit = () => [ }, }), Text, + BulletList.configure({ + HTMLAttributes: { + class: unorderedListClasses, + }, + }), GlobalDragHandle, - AutoJoiner, + AutoJoiner, // Recommended by GlobalDragHandle author. Allows merging nodes when dragging. Columns, Column, Selection, @@ -84,6 +84,7 @@ export const ExtensionKit = () => [ Typography, SlashCommand, Dropcursor.configure({ + // Shows a placeholder for where dragged content will be inserted. width: 2, class: '', }), From 460933dcf83f864a78551c19b79800e98b0380e7 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Wed, 30 Oct 2024 13:00:53 -0700 Subject: [PATCH 06/58] features: bubble menu, link --- components/block-editor/BlockEditor.tsx | 2 + .../extensions/BubbleMenu/BubbleMenu.tsx | 76 +++++++++++++++++++ .../extensions/BubbleMenu/index.ts | 1 + lib/block-editor/extensions/Link/Link.ts | 38 ++++++---- lib/block-editor/extensions/extension-kit.ts | 2 + package.json | 1 + pnpm-lock.yaml | 3 + 7 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx create mode 100644 lib/block-editor/extensions/BubbleMenu/index.ts diff --git a/components/block-editor/BlockEditor.tsx b/components/block-editor/BlockEditor.tsx index 96fd52dc..cb53a68f 100644 --- a/components/block-editor/BlockEditor.tsx +++ b/components/block-editor/BlockEditor.tsx @@ -1,6 +1,7 @@ 'use client'; import { EditorContent } from '@tiptap/react'; +import { BubbleMenu } from '~/lib/block-editor/extensions/BubbleMenu/BubbleMenu'; import { useBlockEditor } from '~/lib/block-editor/useBlockEditor'; const BlockEditor = () => { @@ -9,6 +10,7 @@ const BlockEditor = () => { return (
+
); }; diff --git a/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx b/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx new file mode 100644 index 00000000..376ce95b --- /dev/null +++ b/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx @@ -0,0 +1,76 @@ +import { BubbleMenu as BaseBubbleMenu, type Editor } from '@tiptap/react'; +import { Bold, Italic, Link } from 'lucide-react'; +import { Button } from '~/components/Button'; +import Popover from '~/components/Popover'; + +type BubbleMenuProps = { + editor: Editor | null; +}; + +export const BubbleMenu = ({ editor }: BubbleMenuProps) => { + if (!editor) { + return null; + } + + const handleLinkSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const url = formData.get('url') as string; + + if (url) { + editor + .chain() + .focus() + .setLink({ + href: url, + target: '_blank', + }) + .run(); + } + }; + + return ( + <> + +
+ + + + + + + } + > + + +
+
+ + ); +}; diff --git a/lib/block-editor/extensions/BubbleMenu/index.ts b/lib/block-editor/extensions/BubbleMenu/index.ts new file mode 100644 index 00000000..1325bc40 --- /dev/null +++ b/lib/block-editor/extensions/BubbleMenu/index.ts @@ -0,0 +1 @@ +export * from './BubbleMenu'; diff --git a/lib/block-editor/extensions/Link/Link.ts b/lib/block-editor/extensions/Link/Link.ts index 6dafcd90..defaae76 100644 --- a/lib/block-editor/extensions/Link/Link.ts +++ b/lib/block-editor/extensions/Link/Link.ts @@ -1,39 +1,49 @@ -import { mergeAttributes } from '@tiptap/core' -import TiptapLink from '@tiptap/extension-link' -import { Plugin } from '@tiptap/pm/state' -import { EditorView } from '@tiptap/pm/view' +import { mergeAttributes } from '@tiptap/core'; +import TiptapLink from '@tiptap/extension-link'; +import { Plugin } from '@tiptap/pm/state'; +import { type EditorView } from '@tiptap/pm/view'; export const Link = TiptapLink.extend({ inclusive: false, parseHTML() { - return [{ tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])' }] + return [ + { + tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])', + }, + ]; }, renderHTML({ HTMLAttributes }) { - return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { class: 'link' }), 0] + return [ + 'a', + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + class: 'text-link hover:underline', + }), + 0, + ]; }, addProseMirrorPlugins() { - const { editor } = this + const { editor } = this; return [ - ...(this.parent?.() || []), + ...(this.parent?.() ?? []), new Plugin({ props: { handleKeyDown: (view: EditorView, event: KeyboardEvent) => { - const { selection } = editor.state + const { selection } = editor.state; if (event.key === 'Escape' && selection.empty !== true) { - editor.commands.focus(selection.to, { scrollIntoView: false }) + editor.commands.focus(selection.to, { scrollIntoView: false }); } - return false + return false; }, }, }), - ] + ]; }, -}) +}); -export default Link +export default Link; diff --git a/lib/block-editor/extensions/extension-kit.ts b/lib/block-editor/extensions/extension-kit.ts index e1e6fef3..7f93723b 100644 --- a/lib/block-editor/extensions/extension-kit.ts +++ b/lib/block-editor/extensions/extension-kit.ts @@ -78,6 +78,8 @@ export const ExtensionKit = () => [ HorizontalRule, Link.configure({ openOnClick: false, + autolink: true, + defaultProtocol: 'https', }), Underline, ImageBlock, diff --git a/package.json b/package.json index 097d0cb5..155794cb 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/container-queries": "^0.1.1", "@tiptap/core": "^2.9.1", + "@tiptap/extension-bubble-menu": "^2.9.1", "@tiptap/extension-bullet-list": "^2.9.1", "@tiptap/extension-document": "^2.9.1", "@tiptap/extension-dropcursor": "^2.9.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd383288..25c81662 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,9 @@ importers: '@tiptap/core': specifier: ^2.9.1 version: 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-bubble-menu': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) '@tiptap/extension-bullet-list': specifier: ^2.9.1 version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) From a9056c86365dda4e9a0e4c98fb3c78a753c1f38f Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Wed, 30 Oct 2024 14:56:42 -0700 Subject: [PATCH 07/58] menu button component, ability to delete link --- .../extensions/BubbleMenu/BubbleMenu.tsx | 100 ++++++++++++------ 1 file changed, 66 insertions(+), 34 deletions(-) diff --git a/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx b/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx index 376ce95b..80548e7c 100644 --- a/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx +++ b/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx @@ -1,13 +1,10 @@ import { BubbleMenu as BaseBubbleMenu, type Editor } from '@tiptap/react'; -import { Bold, Italic, Link } from 'lucide-react'; -import { Button } from '~/components/Button'; +import { Bold, Italic, Link, Trash } from 'lucide-react'; +import { Button, type ButtonProps } from '~/components/Button'; import Popover from '~/components/Popover'; +import { withTooltip } from '~/components/Tooltip'; -type BubbleMenuProps = { - editor: Editor | null; -}; - -export const BubbleMenu = ({ editor }: BubbleMenuProps) => { +export const BubbleMenu = ({ editor }: { editor: Editor | null }) => { if (!editor) { return null; } @@ -29,48 +26,83 @@ export const BubbleMenu = ({ editor }: BubbleMenuProps) => { } }; + const handleLinkRemove = () => { + editor?.chain().focus().unsetLink().run(); + }; + return ( <>
- - + + - - - + editor.getAttributes('link').href ? ( + + ) : ( +
+ + +
+ ) } > - + + +
); }; + +const MenuButton = (props: ButtonProps) => { + const { children, variant, ...rest } = props; + + return ( + + ); +}; + +const MenuButtonWithTooltip = withTooltip(MenuButton); From 4096acebef6f1a06a0c6996061cfacd88122cdba Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Thu, 31 Oct 2024 07:45:43 -0700 Subject: [PATCH 08/58] use useEditorState hook for states --- .../extensions/BubbleMenu/BubbleMenu.tsx | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx b/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx index 80548e7c..454e02af 100644 --- a/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx +++ b/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx @@ -1,10 +1,26 @@ -import { BubbleMenu as BaseBubbleMenu, type Editor } from '@tiptap/react'; +import { + BubbleMenu as BaseBubbleMenu, + useEditorState, + type Editor, +} from '@tiptap/react'; import { Bold, Italic, Link, Trash } from 'lucide-react'; import { Button, type ButtonProps } from '~/components/Button'; import Popover from '~/components/Popover'; import { withTooltip } from '~/components/Tooltip'; export const BubbleMenu = ({ editor }: { editor: Editor | null }) => { + const editorState = useEditorState({ + editor, + selector: ({ editor }) => { + return { + isBold: !!editor?.isActive('bold'), + isItalic: !!editor?.isActive('italic'), + isLink: !!editor?.isActive('link'), + activeLink: editor?.getAttributes('link').href, + }; + }, + }); + if (!editor) { return null; } @@ -37,23 +53,23 @@ export const BubbleMenu = ({ editor }: { editor: Editor | null }) => { editor.chain().focus().toggleBold().run()} tooltipContent="Bold (Ctrl+B)" - variant={editor.isActive('bold') ? 'default' : 'text'} + variant={editorState?.isBold ? 'default' : 'text'} > editor.chain().focus().toggleItalic().run()} tooltipContent="Italic (Ctrl+I)" - variant={editor.isActive('italic') ? 'default' : 'text'} // maybe should be passed as a 'selected' prop so that the variant can be standardized + variant={editorState?.isItalic ? 'default' : 'text'} // maybe should be passed as a 'selected' prop so that the variant can be standardized > - - {editor.getAttributes('link').href} + + {editorState.activeLink} + + + } + > + + Edit Variable + + + + ); +}; diff --git a/lib/block-editor/extensions/Variable/VariableNodeView.tsx b/lib/block-editor/extensions/Variable/VariableNodeView.tsx index 7f8a32b4..915a61ea 100644 --- a/lib/block-editor/extensions/Variable/VariableNodeView.tsx +++ b/lib/block-editor/extensions/Variable/VariableNodeView.tsx @@ -1,7 +1,7 @@ import { NodeViewWrapper, type NodeViewProps } from '@tiptap/react'; export const VariableNodeView: React.FC = ({ node }) => { - const { type, name, hint, label, id } = node.attrs; + const { type, name, hint, label, id, control } = node.attrs; return ( @@ -12,6 +12,7 @@ export const VariableNodeView: React.FC = ({ node }) => { Label: {label} Hint: {hint} Type: {type} + Control: {control} From 7fad3484e7d4dd3b2542deb52a4dbbf4127289ad Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Fri, 1 Nov 2024 10:29:49 -0700 Subject: [PATCH 12/58] fix: allow drag of variables already in content area must add data-type value to customNodes array in config for any added custom nodes --- lib/block-editor/extensions/Variable/Variable.ts | 12 +++++------- lib/block-editor/extensions/extension-kit.ts | 4 +++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/block-editor/extensions/Variable/Variable.ts b/lib/block-editor/extensions/Variable/Variable.ts index 092a166c..beca50c8 100644 --- a/lib/block-editor/extensions/Variable/Variable.ts +++ b/lib/block-editor/extensions/Variable/Variable.ts @@ -5,10 +5,8 @@ import { VariableNodeView } from './VariableNodeView'; export const VariableNode = Node.create({ name: 'variable', - group: 'inline', - inline: true, - selectable: true, - draggable: true, + content: 'block+', + group: 'block', addAttributes() { return { @@ -36,15 +34,15 @@ export const VariableNode = Node.create({ parseHTML() { return [ { - tag: 'span[data-type="form-variable"]', + tag: 'div[data-type="variable"]', }, ]; }, renderHTML({ HTMLAttributes }) { return [ - 'span', - mergeAttributes({ 'data-type': 'form-variable' }, HTMLAttributes), + 'div', + mergeAttributes({ 'data-type': 'variable' }, HTMLAttributes), 0, ]; }, diff --git a/lib/block-editor/extensions/extension-kit.ts b/lib/block-editor/extensions/extension-kit.ts index 97e53c52..e469eee5 100644 --- a/lib/block-editor/extensions/extension-kit.ts +++ b/lib/block-editor/extensions/extension-kit.ts @@ -71,7 +71,9 @@ export const ExtensionKit = () => [ class: unorderedListClasses, }, }), - GlobalDragHandle, + GlobalDragHandle.configure({ + customNodes: ['variable'], // customNodes is required for dragging custom nodes + }), AutoJoiner, // Recommended by GlobalDragHandle author. Allows merging nodes when dragging. Columns, Column, From b3b870957f5d54600fec89dd15b1b67658e13531 Mon Sep 17 00:00:00 2001 From: Caden Buckhalt Date: Fri, 1 Nov 2024 14:18:02 -0700 Subject: [PATCH 13/58] render variable components as they will appear on the form allows editing label --- components/block-editor/SidePanel.tsx | 36 +++++- .../extensions/BubbleMenu/BubbleMenu.tsx | 108 +++++++++--------- .../extensions/Variable/EditVariable.tsx | 38 +++--- .../extensions/Variable/Variable.ts | 25 ++-- .../extensions/Variable/VariableNodeView.tsx | 86 ++++++++++++-- lib/block-editor/useBlockEditor.tsx | 3 + 6 files changed, 195 insertions(+), 101 deletions(-) diff --git a/components/block-editor/SidePanel.tsx b/components/block-editor/SidePanel.tsx index 732543cf..53d7294e 100644 --- a/components/block-editor/SidePanel.tsx +++ b/components/block-editor/SidePanel.tsx @@ -1,3 +1,4 @@ +import { CaseSensitive, Hash, SquareStack } from 'lucide-react'; import { Card } from '../Card'; // simplified version of the variable type @@ -5,15 +6,34 @@ type Variable = { id: string; type: string; name: string; - // control: ReactNode; + control: string; + options?: string[]; }; export default function SidePanel() { // dummy data, will get from the protocol const availableVariables = [ - { id: 'var1', type: 'text', name: 'Name' }, - { id: 'var2', type: 'number', name: 'Age' }, - { id: 'var3', type: 'categorical', name: 'School' }, + { + id: 'var1', + type: 'text', + name: 'Name', + control: 'text', + hint: 'Add some text...', + }, + { + id: 'var2', + type: 'number', + name: 'Age', + control: 'number', + hint: 'Add a number...', + }, + { + id: 'var3', + type: 'categorical', + name: 'School', + control: 'checkboxGroup', + options: ['Northwestern', 'U Chicago'], + }, ] as Variable[]; const handleDragStart = ( @@ -32,9 +52,13 @@ export default function SidePanel() { key={index} draggable onDragStart={(e) => handleDragStart(e, variable)} - className="w-48 border p-2" + className="flex w-48 flex-row items-center justify-between border p-2" > - {variable.name} ({variable.type}) + {variable.name} + + {variable.type === 'text' && } + {variable.type === 'number' && } + {variable.type === 'categorical' && } ))} diff --git a/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx b/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx index fbba9e88..707e1d6d 100644 --- a/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx +++ b/lib/block-editor/extensions/BubbleMenu/BubbleMenu.tsx @@ -50,7 +50,11 @@ export const BubbleMenu = ({ editor }: { editor: Editor | null }) => { if (editorState?.isVariable) { return ( - + ); @@ -58,59 +62,61 @@ export const BubbleMenu = ({ editor }: { editor: Editor | null }) => { return ( <> - -
+ + editor.chain().focus().toggleBold().run()} + tooltipContent="Bold (Ctrl+B)" + variant={editorState?.isBold ? 'default' : 'text'} + > + + + editor.chain().focus().toggleItalic().run()} + tooltipContent="Italic (Ctrl+I)" + variant={editorState?.isItalic ? 'default' : 'text'} // maybe should be passed as a 'selected' prop so that the variant can be standardized + > + + + + + {editorState.activeLink} + + +
+ ) : ( +
+ + +
+ ) + } + > editor.chain().focus().toggleBold().run()} - tooltipContent="Bold (Ctrl+B)" - variant={editorState?.isBold ? 'default' : 'text'} + tooltipContent="Set Link" + variant={editorState?.isLink ? 'default' : 'text'} > - + - editor.chain().focus().toggleItalic().run()} - tooltipContent="Italic (Ctrl+I)" - variant={editorState?.isItalic ? 'default' : 'text'} // maybe should be passed as a 'selected' prop so that the variant can be standardized - > - - - - - {editorState.activeLink} - - - - ) : ( -
- - -
- ) - } - > - - - -
- +
); diff --git a/lib/block-editor/extensions/Variable/EditVariable.tsx b/lib/block-editor/extensions/Variable/EditVariable.tsx index 3e61371e..6d5b9477 100644 --- a/lib/block-editor/extensions/Variable/EditVariable.tsx +++ b/lib/block-editor/extensions/Variable/EditVariable.tsx @@ -9,7 +9,7 @@ export const VariableMenu = ({ editor }: { editor: Editor | null }) => { selector: ({ editor }) => { return { isVariable: !!editor?.isActive('variable'), - variableType: editor?.getAttributes('variable').type, + variable: editor?.getAttributes('variable'), }; }, }); @@ -21,15 +21,12 @@ export const VariableMenu = ({ editor }: { editor: Editor | null }) => { const handleLinkSubmit = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); - const label = formData.get('label') as string; - const hint = formData.get('hint') as string; const control = formData.get('control') as string; editor.commands.updateAttributes('variable', { - control, - label, - hint, + control: control, }); + editor.commands.focus(); // Focus the editor after the update }; return ( @@ -38,22 +35,19 @@ export const VariableMenu = ({ editor }: { editor: Editor | null }) => { content={
- - - {editorState.variableType === 'categorical' && ( + {editorState.variable.type === 'categorical' && ( + )} + {editorState.variable.type === 'text' && ( + )} @@ -64,7 +58,7 @@ export const VariableMenu = ({ editor }: { editor: Editor | null }) => { } > - + Edit Variable diff --git a/lib/block-editor/extensions/Variable/Variable.ts b/lib/block-editor/extensions/Variable/Variable.ts index beca50c8..3ae65055 100644 --- a/lib/block-editor/extensions/Variable/Variable.ts +++ b/lib/block-editor/extensions/Variable/Variable.ts @@ -5,8 +5,10 @@ import { VariableNodeView } from './VariableNodeView'; export const VariableNode = Node.create({ name: 'variable', - content: 'block+', group: 'block', + content: 'inline', // todo: this should be block+ but it needs to be inline* for the content to be editable + selectable: true, + draggable: true, addAttributes() { return { @@ -22,27 +24,30 @@ export const VariableNode = Node.create({ control: { default: null, }, + options: { + default: [], + }, label: { default: '', }, hint: { default: '', }, - }; - }, - - parseHTML() { - return [ - { - tag: 'div[data-type="variable"]', + value: { + default: '', }, - ]; + }; }, renderHTML({ HTMLAttributes }) { return [ 'div', - mergeAttributes({ 'data-type': 'variable' }, HTMLAttributes), + mergeAttributes( + { + 'data-type': 'variable', + }, + HTMLAttributes, + ), 0, ]; }, diff --git a/lib/block-editor/extensions/Variable/VariableNodeView.tsx b/lib/block-editor/extensions/Variable/VariableNodeView.tsx index 915a61ea..2d70d29b 100644 --- a/lib/block-editor/extensions/Variable/VariableNodeView.tsx +++ b/lib/block-editor/extensions/Variable/VariableNodeView.tsx @@ -1,19 +1,81 @@ -import { NodeViewWrapper, type NodeViewProps } from '@tiptap/react'; +import { type NodeViewProps, NodeViewWrapper } from '@tiptap/react'; +import { Input } from '~/components/form/Input'; +import { Label } from '~/components/form/Label'; -export const VariableNodeView: React.FC = ({ node }) => { - const { type, name, hint, label, id, control } = node.attrs; +export const VariableNodeView: React.FC = ({ + node, + updateAttributes, +}) => { + const { type, control, options = [], value } = node.attrs; + + const renderControl = () => { + switch (type) { + case 'text': + return control === 'textArea' ? ( +