From e384bae9458bab365637ec000774f4b2a413dbc4 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 24 Dec 2024 14:00:04 +0100 Subject: [PATCH] refactor(editor): override image and link dialog --- src/components/Markdown/Dialog.module.scss | 10 + src/components/Markdown/Editor.tsx | 10 +- src/components/Markdown/ImageDialog.tsx | 166 +++++++++++ src/components/Markdown/LinkDialog.tsx | 306 +++++++++++++++++++++ src/styles/mdxEditor.scss | 3 + 5 files changed, 493 insertions(+), 2 deletions(-) create mode 100644 src/components/Markdown/Dialog.module.scss create mode 100644 src/components/Markdown/ImageDialog.tsx create mode 100644 src/components/Markdown/LinkDialog.tsx diff --git a/src/components/Markdown/Dialog.module.scss b/src/components/Markdown/Dialog.module.scss new file mode 100644 index 0000000..7212245 --- /dev/null +++ b/src/components/Markdown/Dialog.module.scss @@ -0,0 +1,10 @@ +.formAction { + display: flex; + width: 100%; + justify-content: flex-end; + gap: var(--spacing-2); +} + +.wrapper { + border-radius: 0; +} diff --git a/src/components/Markdown/Editor.tsx b/src/components/Markdown/Editor.tsx index 727e008..45bea77 100644 --- a/src/components/Markdown/Editor.tsx +++ b/src/components/Markdown/Editor.tsx @@ -39,6 +39,8 @@ import React, { useMemo, useRef } from 'react' +import { ImageDialog } from './ImageDialog' +import { LinkDialog } from './LinkDialog' export interface EditorRef { setMarkdown: (md: string) => void @@ -95,10 +97,14 @@ export const Editor = React.memo( }), headingsPlugin(), quotePlugin(), - imagePlugin(), + imagePlugin({ + ImageDialog: ImageDialog + }), tablePlugin(), linkPlugin(), - linkDialogPlugin(), + linkDialogPlugin({ + LinkDialog: LinkDialog + }), listsPlugin(), thematicBreakPlugin(), directivesPlugin({ diff --git a/src/components/Markdown/ImageDialog.tsx b/src/components/Markdown/ImageDialog.tsx new file mode 100644 index 0000000..0482283 --- /dev/null +++ b/src/components/Markdown/ImageDialog.tsx @@ -0,0 +1,166 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { useForm } from 'react-hook-form' +import { useCellValues, usePublisher } from '@mdxeditor/gurx' +import { + closeImageDialog$, + editorRootElementRef$, + imageDialogState$, + imageUploadHandler$, + saveImage$ +} from '@mdxeditor/editor' +import styles from './Dialog.module.scss' +import { createPortal } from 'react-dom' + +interface ImageFormFields { + src: string + title: string + altText: string + file: FileList +} + +export const ImageDialog: React.FC = () => { + const [state, editorRootElementRef, imageUploadHandler] = useCellValues( + imageDialogState$, + editorRootElementRef$, + imageUploadHandler$ + ) + const saveImage = usePublisher(saveImage$) + const closeImageDialog = usePublisher(closeImageDialog$) + const { register, handleSubmit, setValue, reset } = useForm({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + values: state.type === 'editing' ? (state.initialValues as any) : {} + }) + const [open, setOpen] = useState(state.type !== 'inactive') + + useEffect(() => { + setOpen(state.type !== 'inactive') + }, [state.type]) + + useEffect(() => { + if (!open) { + closeImageDialog() + reset({ src: '', title: '', altText: '' }) + } + }, [closeImageDialog, open, reset]) + + const handleClose = useCallback(() => { + setOpen(false) + }, []) + + if (!open) return null + if (!editorRootElementRef?.current) return null + + return createPortal( +
+
+
+
+
+
+

Add an image

+
+
+ + + +
+
+
+
{ + void handleSubmit(saveImage)(e) + reset({ src: '', title: '', altText: '' }) + e.preventDefault() + e.stopPropagation() + }} + > + {imageUploadHandler === null ? ( + + ) : ( +
+ + +
+ )} + +
+ + setValue('src', e.currentTarget.value)} + placeholder={'Paste an image src'} + /> +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+
+
, + editorRootElementRef?.current + ) +} diff --git a/src/components/Markdown/LinkDialog.tsx b/src/components/Markdown/LinkDialog.tsx new file mode 100644 index 0000000..7b2ef11 --- /dev/null +++ b/src/components/Markdown/LinkDialog.tsx @@ -0,0 +1,306 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import * as Popover from '@radix-ui/react-popover' +import * as Tooltip from '@radix-ui/react-tooltip' +import React from 'react' + +import { + activeEditor$, + editorRootElementRef$, + iconComponentFor$, + cancelLinkEdit$, + linkDialogState$, + onWindowChange$, + removeLink$, + switchFromPreviewToLinkEdit$, + updateLink$, + ClickLinkCallback +} from '@mdxeditor/editor' +import { useForm } from 'react-hook-form' +import { Cell, useCellValues, usePublisher } from '@mdxeditor/gurx' +import styles from './Dialog.module.scss' + +interface LinkEditFormProps { + url: string + title: string + onSubmit: (link: { url: string; title: string }) => void + onCancel: () => void +} + +interface LinkFormFields { + url: string + title: string +} + +export function LinkEditForm({ + url, + title, + onSubmit, + onCancel +}: LinkEditFormProps) { + const { register, handleSubmit, setValue } = useForm({ + values: { + url, + title + } + }) + + return ( +
+
{ + void handleSubmit(onSubmit)(e) + e.stopPropagation() + e.preventDefault() + }} + onReset={(e) => { + e.stopPropagation() + onCancel() + }} + > +
+ + setValue('url', e.currentTarget.value)} + placeholder={'Paste an URL'} + /> +
+ +
+ + +
+ +
+ + +
+
+
+ ) +} + +export const onClickLinkCallback$ = Cell(null) + +/** @internal */ +export const LinkDialog = () => { + const [ + editorRootElementRef, + activeEditor, + iconComponentFor, + linkDialogState, + onClickLinkCallback + ] = useCellValues( + editorRootElementRef$, + activeEditor$, + iconComponentFor$, + linkDialogState$, + onClickLinkCallback$ + ) + const publishWindowChange = usePublisher(onWindowChange$) + const updateLink = usePublisher(updateLink$) + const cancelLinkEdit = usePublisher(cancelLinkEdit$) + const switchFromPreviewToLinkEdit = usePublisher(switchFromPreviewToLinkEdit$) + const removeLink = usePublisher(removeLink$) + + React.useEffect(() => { + const update = () => { + activeEditor?.getEditorState().read(() => { + publishWindowChange(true) + }) + } + + window.addEventListener('resize', update) + window.addEventListener('scroll', update) + + return () => { + window.removeEventListener('resize', update) + window.removeEventListener('scroll', update) + } + }, [activeEditor, publishWindowChange]) + + const [copyUrlTooltipOpen, setCopyUrlTooltipOpen] = React.useState(false) + + const theRect = linkDialogState.rectangle + + const urlIsExternal = + linkDialogState.type === 'preview' && linkDialogState.url.startsWith('http') + + return ( + + + + + { + e.preventDefault() + }} + key={linkDialogState.linkNodeKey} + className={[ + 'popUpMainCard', + ...(linkDialogState.type === 'edit' ? [styles.wrapper] : []) + ].join(' ')} + > + {linkDialogState.type === 'edit' && ( + + )} + + {linkDialogState.type === 'preview' && ( + <> +
+ +
+
{ + switchFromPreviewToLinkEdit() + }} + > + + + +
+ + + + +
{ + void window.navigator.clipboard + .writeText(linkDialogState.url) + .then(() => { + setCopyUrlTooltipOpen(true) + setTimeout(() => { + setCopyUrlTooltipOpen(false) + }, 1000) + }) + }} + > + + + +
+
+ + + {'Copied!'} + + + +
+
+ +
{ + removeLink() + }} + > + + + +
+
+
+ + )} + +
+
+
+ ) +} diff --git a/src/styles/mdxEditor.scss b/src/styles/mdxEditor.scss index 4cc4a2a..2bfc808 100644 --- a/src/styles/mdxEditor.scss +++ b/src/styles/mdxEditor.scss @@ -101,6 +101,9 @@ padding-top: 10px; min-height: 75px; } +.mdxeditor { + --baseBg: rgba(255, 255, 255, 0.05); +} .mdxeditor, .mdxeditor-popup-container { --basePageBg: var(--slate-3);