diff --git a/package.json b/package.json index 1bf21a56..a04af18f 100644 --- a/package.json +++ b/package.json @@ -104,9 +104,9 @@ "prosemirror-view": "^1.34.3", "sass": "^1.79.4", "solid-js": "^1.9.2", + "solid-popper": "^0.3.0", "solid-tiptap": "0.7.0", "solid-transition-group": "^0.2.3", - "solid-popper": "^0.3.0", "storybook": "^8.3.5", "storybook-addon-sass-postcss": "^0.3.2", "storybook-solidjs": "^1.0.0-beta.2", diff --git a/src/components/Editor/Editor.module.scss b/src/components/Editor/Editor.module.scss new file mode 100644 index 00000000..83f5ad10 --- /dev/null +++ b/src/components/Editor/Editor.module.scss @@ -0,0 +1,313 @@ +.articleEditor { + font-size: 1.6rem; + outline: none; + min-height: 300px; + + p.is-editor-empty:first-child::before { + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + opacity: 0.3; + } + + /* Give a remote user a caret */ + .collaboration-cursor__caret { + border-left: 1px solid #0d0d0d; + border-right: 1px solid #0d0d0d; + margin-left: -1px; + margin-right: -1px; + pointer-events: none; + position: relative; + word-break: normal; + } + + + /* Render the username above the caret */ + .collaboration-cursor__label { + border-radius: 3px 3px 3px 0; + color: #0d0d0d; + font-size: 12px; + font-style: normal; + font-weight: 600; + left: -1px; + line-height: normal; + padding: 0.1rem 0.3rem; + position: absolute; + top: -1.4em; + user-select: none; + white-space: nowrap; + } + + .embed-wrapper { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: #f1f1f1; + margin: 4rem 0; + + iframe { + border: none; + overflow: hidden; + } + } + + .horizontalRule { + border-top: 2px solid #000; + } + + mark.highlight { + box-decoration-break: clone; + padding: 0.2em 0; + } + + // custom atibutes fro TipTap Nodes + + @include media-breakpoint-up(sm) { + [data-float] { + max-width: 50%; + } + + [data-float='left'] { + float: left; + max-width: 35%; + margin: 1rem 2.2em 0 0; + clear: left; + } + + [data-float='right'] { + float: right; + margin: 1rem 0 1rem 2.2em; + max-width: 35%; + clear: right; + } + + [data-float='half-left'] { + float: left; + margin: 1rem 1rem 0; + clear: left; + + @include media-breakpoint-up(md) { + max-width: 50%; + min-width: 30%; + } + } + + [data-float='half-right'] { + float: right; + margin: 1rem 0; + clear: right; + + @include media-breakpoint-up(md) { + max-width: 50%; + min-width: 30%; + } + } + } + + blockquote, .blockquote { + p:last-child { + margin-bottom: 0; + } + + &[data-type='quote'] { + font-size: 1.4rem; + border: solid #000; + border-width: 0 0 0 2px; + margin: 1.6rem 0; + padding: 0 0 0 1.5em; + + &[data-float='left'] { + padding-left: 0; + padding-right: 0; + margin-right: 1.6rem; + border-width: 0 2px 0 0; + clear: left; + } + + &[data-float='right'] { + margin-left: 1.6rem; + border-width: 0 0 0 2px; + clear: right; + } + } + + &[data-type='punchline'] { + border: solid #000; + border-width: 2px 0; + font-size: 3.2rem; + font-weight: 700; + line-height: 1.2; + margin: 1em 0; + padding: 2.4rem 0; + + &[data-float='left'], + &[data-float='right'] { + font-size: 2.2rem; + line-height: 1.4; + } + + @include media-breakpoint-up(sm) { + &[data-float='left'] { + margin-right: 1.5em; + clear: left; + } + + &[data-float='right'] { + margin-left: 1.5em; + clear: right; + } + } + } + } + + article[data-type='incut'] { + background: #f1f2f3; + font-size: 1.4rem; + margin: 1em -1rem; + padding: 2em 2rem; + transition: background 0.3s ease-in-out; + + @include media-breakpoint-up(sm) { + margin-left: -2rem; + margin-right: -2rem; + } + + @include media-breakpoint-up(lg) { + margin-right: -6%; + padding-left: 3em; + padding-right: 3em; + } + + @include media-breakpoint-up(lg) { + margin-left: -3em; + margin-right: -3em; + } + + &[data-float] img { + float: none; + max-width: unset; + width: 100% !important; + margin: 0; + } + + &[data-float='left'], + &[data-float='half-left'] { + margin-left: -1rem; + clear: left; + + @include media-breakpoint-up(sm) { + margin-left: -2rem; + margin-right: 2rem; + } + + @include media-breakpoint-up(lg) { + margin-left: -6%; + } + + @include media-breakpoint-up(xl) { + margin-left: -12.5%; + } + } + + &[data-float='right'], + &[data-float='half-right'] { + margin-right: -1rem; + clear: right; + + @include media-breakpoint-up(sm) { + margin-left: 2rem; + margin-right: -2rem; + } + + @include media-breakpoint-up(lg) { + margin-right: -6%; + } + + @include media-breakpoint-up(xl) { + margin-right: -12.5%; + } + } + + *:last-child { + margin-bottom: 0; + } + + &[data-bg='black'] { + background: #000; + color: #fff; + } + + &[data-bg='yellow'] { + background: #f6e3a1; + } + + &[data-bg='pink'] { + background: #f1b5bc; + } + + &[data-bg='green'] { + background: #eafff2; + } + + &[data-bg='white'] { + background: #fff; + box-shadow: 0 0 0 1px #000; + } + } + + figure[data-type='figure'] { + width: 100% !important; + + .iframe-wrapper { + position: relative; + overflow: hidden; + width: 100%; + height: auto; + + iframe { + display: block; + width: 100%; + } + } + } + + /* stylelint-disable-next-line selector-type-no-unknown */ + footnote, .footnote { + display: inline-flex; + position: relative; + cursor: pointer; + width: 0.8rem; + height: 1em; + + &::before { + content: ''; + position: absolute; + width: 10px; + height: 10px; + border-radius: 50%; + top: -2px; + border: unset; + background-size: 10px; + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgY2xhc3M9ImJpIGJpLWluZm8tY2lyY2xlIiBmaWxsPSJjdXJyZW50Q29sb3IiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgd2lkdGg9IjE2IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik04IDE1QTcgNyAwIDEgMSA4IDFhNyA3IDAgMCAxIDAgMTR6bTAgMUE4IDggMCAxIDAgOCAwYTggOCAwIDAgMCAwIDE2eiIvPjxwYXRoIGQ9Im04LjkzIDYuNTg4LTIuMjkuMjg3LS4wODIuMzguNDUuMDgzYy4yOTQuMDcuMzUyLjE3Ni4yODguNDY5bC0uNzM4IDMuNDY4Yy0uMTk0Ljg5Ny4xMDUgMS4zMTkuODA4IDEuMzE5LjU0NSAwIDEuMTc4LS4yNTIgMS40NjUtLjU5OGwuMDg4LS40MTZjLS4yLjE3Ni0uNDkyLjI0Ni0uNjg2LjI0Ni0uMjc1IDAtLjM3NS0uMTkzLS4zMDQtLjUzM0w4LjkzIDYuNTg4ek05IDQuNWExIDEgMCAxIDEtMiAwIDEgMSAwIDAgMSAyIDB6Ii8+PC9zdmc+'); + } + + &:hover { + background-color: unset; + } + } + + .highlight-fake-selection { + background: var(--selection-background); + color: var(--selection-color); + border: solid var(--selection-background); + border-width: 0; + } + + &.ProseMirror-hideselection figure[data-type='figure'] { + &>figcaption { + --selection-color: rgb(0 0 0 / 60%); + } + } +} \ No newline at end of file diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 4b1f8ba3..9f12de96 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -7,9 +7,9 @@ import { Collaboration } from '@tiptap/extension-collaboration' import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor' import { FloatingMenu } from '@tiptap/extension-floating-menu' import { Placeholder } from '@tiptap/extension-placeholder' -import { createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' +import { Accessor, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' import { isServer } from 'solid-js/web' -import { createTiptapEditor } from 'solid-tiptap' +import { createEditorTransaction, createTiptapEditor } from 'solid-tiptap' import uniqolor from 'uniqolor' import { Doc } from 'yjs' import { useEditorContext } from '~/context/editor' @@ -23,10 +23,10 @@ import { allowedImageTypes, renderUploadedImage } from '../Upload/renderUploaded import { BlockquoteBubbleMenu } from './Toolbar/BlockquoteBubbleMenu' import { EditorFloatingMenu } from './Toolbar/EditorFloatingMenu' import { FigureBubbleMenu } from './Toolbar/FigureBubbleMenu' +import { FullBubbleMenu } from './Toolbar/FullBubbleMenu' import { IncutBubbleMenu } from './Toolbar/IncutBubbleMenu' -import { TextBubbleMenu } from './Toolbar/TextBubbleMenu' -import './Prosemirror.scss' +import styles from './Editor.module.scss' export type EditorComponentProps = { shoutId: number @@ -126,11 +126,10 @@ export const EditorComponent = (props: EditorComponentProps) => { const options: Partial = { element: editorElRef()!, editorProps: { - attributes: { class: 'articleEditor' }, + attributes: { class: styles.articleEditor }, transformPastedHTML: (c: string) => c.replaceAll(//g, ''), - handlePaste: (_view, _event, _slice) => { - handleClipboardPaste().then((result) => result) - return false + handlePaste: () => { + handleClipboardPaste().then((_) => 0) } }, extensions: [ @@ -180,6 +179,11 @@ export const EditorComponent = (props: EditorComponentProps) => { }, 'edit') }) + const isFigcaptionActive = createEditorTransaction(editor as Accessor, (e) => + e?.isActive('figcaption') + ) + createEffect(() => setIsCommonMarkup(!!isFigcaptionActive())) + const initializeMenus = () => { if (menusInitialized() || !editor()) return if (blockquoteBubbleMenuRef() && figureBubbleMenuRef() && incutBubbleMenuRef() && floatingMenuRef()) { @@ -188,7 +192,7 @@ export const EditorComponent = (props: EditorComponentProps) => { BubbleMenu.configure({ pluginKey: 'textBubbleMenu', element: textBubbleMenuRef()!, - shouldShow: ({ editor: e, view, state: { doc, selection }, from, to }) => { + shouldShow: ({ editor: e, state: { doc, selection }, from, to }) => { const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection) if (isEmptyTextBlock) { e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run() @@ -197,10 +201,8 @@ export const EditorComponent = (props: EditorComponentProps) => { const isFootnoteOrFigcaption = e.isActive('footnote') || (e.isActive('figcaption') && hasSelection) - setIsCommonMarkup(e?.isActive('figcaption')) - const result = - view.hasFocus() && + e.isFocused && hasSelection && !e.isActive('image') && !e.isActive('figure') && @@ -355,10 +357,10 @@ export const EditorComponent = (props: EditorComponentProps) => { - } ref={setTextBubbleMenuRef} /> diff --git a/src/components/Editor/MicroEditor.tsx b/src/components/Editor/MicroEditor.tsx index 87416823..5a27bb59 100644 --- a/src/components/Editor/MicroEditor.tsx +++ b/src/components/Editor/MicroEditor.tsx @@ -49,11 +49,7 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { [styles.bordered]: props.bordered })} > -