From 30de1ddb3e44e575405d49a148657ad8b4ce76ff Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 21:09:50 +0300 Subject: [PATCH] editor-refactored-2 --- .../TextBubbleMenu.module.scss | 0 .../TextBubbleMenu.tsx | 4 +- .../{Prosemirror.scss => Editor.module.scss} | 0 src/components/Editor/Editor.tsx | 4 +- .../Editor/EditorToolbar/EditorToolbar.tsx | 2 +- .../Editor/EditorToolbar/InsertLinkForm.tsx | 1 - .../EditorToolbar/SimplifiedToolbar.tsx | 133 ---------- .../Editor/EditorToolbar/ToolbarControl.tsx | 2 +- .../LinkBubbleMenu/LinkBubbleMenu.module.scss | 4 - .../LinkBubbleMenu/LinkBubbleMenu.module.tsx | 19 -- src/components/Editor/LinkBubbleMenu/index.ts | 1 - .../Editor/MicroEditor/MicroEditor.tsx | 4 +- .../MiniEditor.module.scss} | 2 +- .../Editor/MiniEditor/MiniEditor.tsx | 8 +- src/components/Editor/SimplifiedEditor.tsx | 244 ------------------ src/components/Editor/TextBubbleMenu/index.ts | 1 - src/components/Editor/index.ts | 4 - .../Views/EditView/EditSettingsView.tsx | 2 +- src/components/Views/EditView/EditView.tsx | 3 +- .../Views/PublishSettings/PublishSettings.tsx | 3 +- 20 files changed, 18 insertions(+), 423 deletions(-) rename src/components/Editor/{TextBubbleMenu => BubbleMenu}/TextBubbleMenu.module.scss (100%) rename src/components/Editor/{TextBubbleMenu => BubbleMenu}/TextBubbleMenu.tsx (99%) rename src/components/Editor/{Prosemirror.scss => Editor.module.scss} (100%) delete mode 100644 src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx delete mode 100644 src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.scss delete mode 100644 src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.tsx delete mode 100644 src/components/Editor/LinkBubbleMenu/index.ts rename src/components/Editor/{SimplifiedEditor.module.scss => MiniEditor/MiniEditor.module.scss} (99%) delete mode 100644 src/components/Editor/SimplifiedEditor.tsx delete mode 100644 src/components/Editor/TextBubbleMenu/index.ts delete mode 100644 src/components/Editor/index.ts diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss b/src/components/Editor/BubbleMenu/TextBubbleMenu.module.scss similarity index 100% rename from src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss rename to src/components/Editor/BubbleMenu/TextBubbleMenu.module.scss diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx b/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx similarity index 99% rename from src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx rename to src/components/Editor/BubbleMenu/TextBubbleMenu.tsx index 981523dc..73d86c0b 100644 --- a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx +++ b/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx @@ -7,9 +7,9 @@ import { Popover } from '~/components/_shared/Popover' import { useLocalize } from '~/context/localize' import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm' -import styles from './TextBubbleMenu.module.scss' +import styles from '../TextBubbleMenu/TextBubbleMenu.module.scss' -const MiniEditor = lazy(() => import('../../Editor/MiniEditor/MiniEditor')) +const MiniEditor = lazy(() => import('../MiniEditor/MiniEditor')) type BubbleMenuProps = { editor: Editor diff --git a/src/components/Editor/Prosemirror.scss b/src/components/Editor/Editor.module.scss similarity index 100% rename from src/components/Editor/Prosemirror.scss rename to src/components/Editor/Editor.module.scss diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 4a05a420..b49b2799 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -18,10 +18,10 @@ import { base, custom, extended } from '~/lib/editorExtensions' import { handleImageUpload } from '~/lib/handleImageUpload' import { renderUploadedImage } from '../Upload/renderUploadedImage' import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu' +import { TextBubbleMenu } from './BubbleMenu/TextBubbleMenu' import { EditorFloatingMenu } from './EditorFloatingMenu' -import { TextBubbleMenu } from './TextBubbleMenu' -import './Prosemirror.scss' +import './Editor.module.scss' export type EditorComponentProps = { shoutId: number diff --git a/src/components/Editor/EditorToolbar/EditorToolbar.tsx b/src/components/Editor/EditorToolbar/EditorToolbar.tsx index 06cef005..3632bc29 100644 --- a/src/components/Editor/EditorToolbar/EditorToolbar.tsx +++ b/src/components/Editor/EditorToolbar/EditorToolbar.tsx @@ -12,7 +12,7 @@ import { UploadedFile } from '~/types/upload' import { InsertLinkForm } from './InsertLinkForm' import { ToolbarControl as Control } from './ToolbarControl' -import styles from '../SimplifiedEditor.module.scss' +import styles from '../MiniEditor/MiniEditor.module.scss' interface EditorToolbarProps { editor: Accessor diff --git a/src/components/Editor/EditorToolbar/InsertLinkForm.tsx b/src/components/Editor/EditorToolbar/InsertLinkForm.tsx index 9d7c479f..4ce396cf 100644 --- a/src/components/Editor/EditorToolbar/InsertLinkForm.tsx +++ b/src/components/Editor/EditorToolbar/InsertLinkForm.tsx @@ -1,6 +1,5 @@ import { Editor } from '@tiptap/core' import { createEffect, createSignal, onCleanup } from 'solid-js' - import { useLocalize } from '~/context/localize' import { validateUrl } from '~/utils/validate' import { InlineForm } from '../../_shared/InlineForm' diff --git a/src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx b/src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx deleted file mode 100644 index 1ea1d449..00000000 --- a/src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import clsx from 'clsx' -import { Show } from 'solid-js' -import { createEditorTransaction, useEditorHTML, useEditorIsEmpty } from 'solid-tiptap' -import { useEditorContext } from '~/context/editor' -import { useLocalize } from '~/context/localize' -import { useUI } from '~/context/ui' -import { Button } from '../../_shared/Button' -import { Icon } from '../../_shared/Icon' -import { Loading } from '../../_shared/Loading' -import { Popover } from '../../_shared/Popover' -import { SimplifiedEditorProps } from '../SimplifiedEditor' - -import styles from '../SimplifiedEditor.module.scss' - -export const ToolbarControls = ( - props: SimplifiedEditorProps & { setShouldShowLinkBubbleMenu: (x: boolean) => void } -) => { - const { t } = useLocalize() - const { showModal } = useUI() - const { editor } = useEditorContext() - const isActive = (name: string) => createEditorTransaction(editor, (ed) => ed?.isActive(name)) - const isBold = isActive('bold') - const isItalic = isActive('italic') - const isLink = isActive('link') - const isBlockquote = isActive('blockquote') - const isEmpty = useEditorIsEmpty(editor) - const html = useEditorHTML(editor) - - const handleClear = () => { - props.onCancel?.() - editor()?.commands.clearContent(true) - } - - const handleShowLinkBubble = () => { - editor()?.chain().focus().run() - props.setShouldShowLinkBubbleMenu(true) - } - - return ( - - {/* Only show controls if 'hideToolbar' is false */} -
-
- {/* Bold button */} - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - - {/* Italic button */} - - {(triggerRef) => ( - - )} - - {/* Link button */} - - {(triggerRef) => ( - - )} - - {/* Blockquote button (optional) */} - - - {(triggerRef) => ( - - )} - - - {/* Image button (optional) */} - - - {(triggerRef) => ( - - )} - - -
- {/* Cancel and submit buttons */} - -
- -
-
-
-
- ) -} diff --git a/src/components/Editor/EditorToolbar/ToolbarControl.tsx b/src/components/Editor/EditorToolbar/ToolbarControl.tsx index b4046e67..2abc7d09 100644 --- a/src/components/Editor/EditorToolbar/ToolbarControl.tsx +++ b/src/components/Editor/EditorToolbar/ToolbarControl.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx' import { JSX } from 'solid-js' import { Popover } from '~/components/_shared/Popover' -import styles from '../SimplifiedEditor.module.scss' +import styles from '../MiniEditor/MiniEditor.module.scss' interface ControlProps { editor: Editor diff --git a/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.scss b/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.scss deleted file mode 100644 index 27f3ca77..00000000 --- a/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.scss +++ /dev/null @@ -1,4 +0,0 @@ -.LinkBubbleMenu { - background: var(--editor-bubble-menu-background); - box-shadow: 0 4px 10px rgba(#000, 0.25); -} diff --git a/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.tsx b/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.tsx deleted file mode 100644 index 2b87ab50..00000000 --- a/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { Editor } from '@tiptap/core' - -import { InsertLinkForm } from '../InsertLinkForm' - -import styles from './LinkBubbleMenu.module.scss' - -type Props = { - editor: Editor - ref: (el: HTMLDivElement) => void - onClose: () => void -} - -export const LinkBubbleMenuModule = (props: Props) => { - return ( -
- -
- ) -} diff --git a/src/components/Editor/LinkBubbleMenu/index.ts b/src/components/Editor/LinkBubbleMenu/index.ts deleted file mode 100644 index 7d3e3ace..00000000 --- a/src/components/Editor/LinkBubbleMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LinkBubbleMenuModule } from './LinkBubbleMenu.module' diff --git a/src/components/Editor/MicroEditor/MicroEditor.tsx b/src/components/Editor/MicroEditor/MicroEditor.tsx index 549dd54a..907456e4 100644 --- a/src/components/Editor/MicroEditor/MicroEditor.tsx +++ b/src/components/Editor/MicroEditor/MicroEditor.tsx @@ -5,7 +5,7 @@ import { createTiptapEditor, useEditorHTML, useEditorIsFocused } from 'solid-tip import { minimal } from '~/lib/editorExtensions' import { EditorToolbar } from '../EditorToolbar/EditorToolbar' -import styles from '../SimplifiedEditor.module.scss' +import styles from '../MiniEditor/MiniEditor.module.scss' interface MicroEditorProps { content?: string @@ -36,7 +36,7 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { createEffect(on(html, (c?: string) => c && props.onChange?.(c))) return ( -
+
diff --git a/src/components/Editor/SimplifiedEditor.module.scss b/src/components/Editor/MiniEditor/MiniEditor.module.scss similarity index 99% rename from src/components/Editor/SimplifiedEditor.module.scss rename to src/components/Editor/MiniEditor/MiniEditor.module.scss index cc9ba21d..f2f7ec2d 100644 --- a/src/components/Editor/SimplifiedEditor.module.scss +++ b/src/components/Editor/MiniEditor/MiniEditor.module.scss @@ -1,4 +1,4 @@ -.SimplifiedEditor { +.MiniEditor { width: 100%; display: flex; flex-direction: column; diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index 1b86aa82..e93f2289 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -3,12 +3,12 @@ import Placeholder from '@tiptap/extension-placeholder' import clsx from 'clsx' import { type JSX, Show, createEffect, createSignal, on } from 'solid-js' import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap' +import { Button } from '~/components/_shared/Button' +import { useLocalize } from '~/context/localize' import { base } from '~/lib/editorExtensions' import { EditorToolbar } from '../EditorToolbar/EditorToolbar' -import { Button } from '~/components/_shared/Button' -import { useLocalize } from '~/context/localize' -import styles from '../SimplifiedEditor.module.scss' +import styles from './MiniEditor.module.scss' interface MiniEditorProps { content?: string @@ -58,7 +58,7 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { } return ( -
+
diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx deleted file mode 100644 index f09c55c6..00000000 --- a/src/components/Editor/SimplifiedEditor.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { Editor, FocusPosition } from '@tiptap/core' -import { BubbleMenu } from '@tiptap/extension-bubble-menu' -import { CharacterCount } from '@tiptap/extension-character-count' -import { Placeholder } from '@tiptap/extension-placeholder' -import { clsx } from 'clsx' -import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js' -import { Portal } from 'solid-js/web' -import { createEditorTransaction, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap' -import { useEditorContext } from '~/context/editor' -import { useUI } from '~/context/ui' -import { base, custom } from '~/lib/editorExtensions' -import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler' -import { UploadedFile } from '~/types/upload' -import { UploadModalContent } from '../Upload/UploadModalContent' -import { renderUploadedImage } from '../Upload/renderUploadedImage' -import { Modal } from '../_shared/Modal/Modal' -import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' -import { ToolbarControls } from './EditorToolbar/SimplifiedToolbar' -import { LinkBubbleMenuModule } from './LinkBubbleMenu' -import { TextBubbleMenu } from './TextBubbleMenu' - -import styles from './Editor.module.scss' - -export type SimplifiedEditorProps = { - placeholder: string - initialContent?: string - label?: string - onSubmit?: (text: string) => void - onCancel?: () => void - onChange?: (text: string) => void - variant?: 'minimal' | 'bordered' - maxLength?: number - noLimits?: boolean - maxHeight?: number - submitButtonText?: string - quoteEnabled?: boolean - imageEnabled?: boolean - setClear?: boolean - resetToInitial?: boolean - smallHeight?: boolean - submitByCtrlEnter?: boolean - hideToolbar?: boolean - controlsAlwaysVisible?: boolean - autoFocus?: boolean - isCancelButtonVisible?: boolean - isPosting?: boolean -} - -const DEFAULT_MAX_LENGTH = 400 - -const SimplifiedEditor = (props: SimplifiedEditorProps) => { - // local signals - const [counter, setCounter] = createSignal(0) - const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false) - const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false) - const [editorElement, setEditorElement] = createSignal() - const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal() - const [linkBubbleMenuRef, setLinkBubbleMenuRef] = createSignal() - - // contexts - const { hideModal } = useUI() - const { editor, createEditor } = useEditorContext() - - const initEditor = (element?: HTMLElement) => { - if (element instanceof HTMLElement && editor()?.options.element !== element) { - const opts = { - element, - extensions: [ - // common extensions - ...base, - ...custom, - - // setup from component props - Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder }), - CharacterCount.configure({ limit: props.noLimits ? undefined : props.maxLength }), - - // bubble menu 1 - BubbleMenu.configure({ - pluginKey: 'bubble-menu', - element: textBubbleMenuRef(), - shouldShow: ({ view }) => view.hasFocus() && shouldShowTextBubbleMenu() - }), - - // bubble menu 2 - BubbleMenu.configure({ - pluginKey: 'bubble-link-input', - element: linkBubbleMenuRef(), - shouldShow: ({ state }) => !state.selection.empty && shouldShowLinkBubbleMenu(), - tippyOptions: { placement: 'bottom' } - }) - ], - editorProps: { - attributes: { class: styles.simplifiedEditorField } - }, - content: props.initialContent || '', - onCreate: () => console.info('[SimplifiedEditor] created'), - onContentError: console.error, - autofocus: (props.autoFocus && 'end') as FocusPosition | undefined, - editable: true, - enableCoreExtensions: true, - enableContentCheck: true, - injectNonce: undefined, // TODO: can be useful copyright/copyleft mark - parseOptions: undefined // see: https://prosemirror.net/docs/ref/#model.ParseOptions - } - - createEditor(opts) - } - } - - // editor observers - const isEmpty = useEditorIsEmpty(editor) - const isFocused = useEditorIsFocused(editor) - const selection = createEditorTransaction(editor, (ed) => ed?.state.selection) - const html = useEditorHTML(editor) - - /// EFFECTS /// - - // Mount event listeners for handling key events and clean up on component unmount - onMount(() => { - window.addEventListener('keydown', handleKeyDown) - onCleanup(() => { - window.removeEventListener('keydown', handleKeyDown) - editor()?.destroy() - }) - }) - - // watch changes - createEffect(on(editorElement, initEditor, { defer: true })) // element -> editorOptions -> set editor - createEffect( - on(selection, (s?: Editor['state']['selection']) => s && setShouldShowTextBubbleMenu(!s?.empty)) - ) - createEffect( - on( - () => props.setClear, - (x?: boolean) => x && editor()?.commands.clearContent(true) - ) - ) - createEffect( - on( - () => props.resetToInitial, - (x?: boolean) => x && editor()?.commands.setContent(props.initialContent || '') - ) - ) - createEffect(on([html, () => props.onChange], ([c, handler]) => c && handler && handler(c))) // onChange - createEffect(on(html, (c?: string) => c && setCounter(editor()?.storage.characterCount.characters()))) //counter - - /// HANDLERS /// - - const handleImageRender = (image?: UploadedFile) => { - image && renderUploadedImage(editor() as Editor, image) - hideModal() - } - - const handleKeyDown = (event: KeyboardEvent) => { - if ( - isFocused() && - !isEmpty() && - event.code === 'Enter' && - props.submitByCtrlEnter && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault() - props.onSubmit?.(html() || '') - } - } - - const handleHideLinkBubble = () => { - editor()?.commands.focus() - setShouldShowLinkBubbleMenu(false) - } - - useEscKeyDownHandler(handleHideLinkBubble) - - return ( - -
0 - })} - > - {/* Display label when applicable */} - 0}> -
{props.label}
-
- - - } - > - - - {/* Link bubble menu */} - - - - - - {/* editor element */} -
- - {/* Display character limit if maxLength is provided */} - -
{(props.maxLength || DEFAULT_MAX_LENGTH) - counter()}
-
- - {/* Image upload modal (show/hide) */} - - - - - - - -
- - ) -} - -export default SimplifiedEditor // Export component for lazy loading diff --git a/src/components/Editor/TextBubbleMenu/index.ts b/src/components/Editor/TextBubbleMenu/index.ts deleted file mode 100644 index 5dd90ad9..00000000 --- a/src/components/Editor/TextBubbleMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TextBubbleMenu } from './TextBubbleMenu' diff --git a/src/components/Editor/index.ts b/src/components/Editor/index.ts deleted file mode 100644 index 4f9a58ab..00000000 --- a/src/components/Editor/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { EditorComponent as Editor } from './Editor' -export { Panel } from './Panel' -export { TopicSelect } from '../TopicSelect' -export { UploadModalContent } from '../Upload/UploadModalContent' diff --git a/src/components/Views/EditView/EditSettingsView.tsx b/src/components/Views/EditView/EditSettingsView.tsx index 351c42c7..72f5802d 100644 --- a/src/components/Views/EditView/EditSettingsView.tsx +++ b/src/components/Views/EditView/EditSettingsView.tsx @@ -3,6 +3,7 @@ import deepEqual from 'fast-deep-equal' import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' +import { Panel } from '~/components/Editor/Panel/Panel' import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' import { ShoutForm, useEditorContext } from '~/context/editor' @@ -12,7 +13,6 @@ import getMyShoutQuery from '~/graphql/query/core/article-my' import type { Shout, Topic } from '~/graphql/schema/core.gen' import { isDesktop } from '~/lib/mediaQuery' import { clone } from '~/utils/clone' -import { Panel } from '../../Editor' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' import { Modal } from '../../_shared/Modal' import { TableOfContents } from '../../_shared/TableOfContents' diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 25dd9d7c..ba153c6e 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -3,6 +3,8 @@ import deepEqual from 'fast-deep-equal' import { Show, createEffect, createSignal, lazy, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' +import { EditorComponent } from '~/components/Editor/Editor' +import { Panel } from '~/components/Editor/Panel/Panel' import { DropArea } from '~/components/_shared/DropArea' import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' @@ -20,7 +22,6 @@ import { isDesktop } from '~/lib/mediaQuery' import { LayoutType } from '~/types/common' import { MediaItem } from '~/types/mediaitem' import { clone } from '~/utils/clone' -import { Editor as EditorComponent, Panel } from '../../Editor' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' import { AudioUploader } from '../../Upload/AudioUploader' import { VideoUploader } from '../../Upload/VideoUploader' diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 9c848a52..552535b5 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -12,9 +12,10 @@ import { useTopics } from '~/context/topics' import { useSnackbar, useUI } from '~/context/ui' import { Topic } from '~/graphql/schema/core.gen' import { UploadedFile } from '~/types/upload' -import { TopicSelect, UploadModalContent } from '../../Editor' import { Modal } from '../../_shared/Modal' +import { TopicSelect } from '~/components/TopicSelect/TopicSelect' +import { UploadModalContent } from '~/components/Upload/UploadModalContent/UploadModalContent' import stylesBeside from '../../Feed/Beside.module.scss' import styles from './PublishSettings.module.scss'