diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 926a9955..4722a1ff 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -35,7 +35,8 @@ import { useSession } from '../../context/session' import uniqolor from 'uniqolor' import { HocuspocusProvider } from '@hocuspocus/provider' import { Embed } from './extensions/embed' -import { EditorBubbleMenu } from './EditorBubbleMenu' +import { TextBubbleMenu } from './TextBubbleMenu' +import { ImageBubbleMenu } from './ImageBubbleMenu' import { EditorFloatingMenu } from './EditorFloatingMenu' import { useEditorContext } from '../../context/editor' @@ -74,7 +75,13 @@ export const Editor = (props: EditorProps) => { current: null } - const bubbleMenuRef: { + const textBubbleMenuRef: { + current: HTMLDivElement + } = { + current: null + } + + const imageBubbleMenuRef: { current: HTMLDivElement } = { current: null @@ -135,7 +142,19 @@ export const Editor = (props: EditorProps) => { TrailingNode, CharacterCount, BubbleMenu.configure({ - element: bubbleMenuRef.current + pluginKey: 'textBubbleMenu', + element: textBubbleMenuRef.current, + shouldShow: ({ editor: e, view, state, oldState, from, to }) => { + console.log(view) + return e.isFocused && !e.isActive('image') + } + }), + BubbleMenu.configure({ + pluginKey: 'imageBubbleMenu', + element: imageBubbleMenuRef.current, + shouldShow: ({ editor: e, view, state, oldState, from, to }) => { + return e.isFocused && e.isActive('image') + } }), FloatingMenu.configure({ tippyOptions: { @@ -165,7 +184,8 @@ export const Editor = (props: EditorProps) => { return ( <>
(editorElRef.current = el)} /> - (bubbleMenuRef.current = el)} /> + (textBubbleMenuRef.current = el)} /> + (imageBubbleMenuRef.current = el)} /> (floatingMenuRef.current = el)} /> ) diff --git a/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx b/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx deleted file mode 100644 index 5a8aad2e..00000000 --- a/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import { Switch, Match, createSignal, Show } from 'solid-js' -import type { Editor } from '@tiptap/core' -import styles from './EditorBubbleMenu.module.scss' -import { Icon } from '../../_shared/Icon' -import { clsx } from 'clsx' -import { createEditorTransaction } from 'solid-tiptap' -import { useLocalize } from '../../../context/localize' -import { InlineForm } from '../InlineForm' -import validateImage from '../../../utils/validateUrl' - -type BubbleMenuProps = { - editor: Editor - ref: (el: HTMLDivElement) => void -} - -export const EditorBubbleMenu = (props: BubbleMenuProps) => { - const { t } = useLocalize() - const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false) - const [listBubbleOpen, setListBubbleOpen] = createSignal(false) - const [linkEditorOpen, setLinkEditorOpen] = createSignal(false) - - const isActive = (name: string, attributes?: unknown) => - createEditorTransaction( - () => props.editor, - (editor) => { - return editor && editor.isActive(name, attributes) - } - ) - - const isBold = isActive('bold') - const isItalic = isActive('italic') - const isH1 = isActive('heading', { level: 1 }) - const isH2 = isActive('heading', { level: 2 }) - const isH3 = isActive('heading', { level: 3 }) - const isBlockQuote = isActive('blockquote') - const isOrderedList = isActive('isOrderedList') - const isBulletList = isActive('isBulletList') - const isLink = isActive('link') - - const toggleLinkForm = () => { - setLinkEditorOpen(true) - } - - const toggleTextSizePopup = () => { - if (listBubbleOpen()) { - setListBubbleOpen(false) - } - - setTextSizeBubbleOpen((prev) => !prev) - } - const toggleListPopup = () => { - if (textSizeBubbleOpen()) { - setTextSizeBubbleOpen(false) - } - - setListBubbleOpen((prev) => !prev) - } - - const handleLinkFormSubmit = (value: string) => { - props.editor.chain().focus().setLink({ href: value }).run() - } - - const currentUrl = createEditorTransaction( - () => props.editor, - (editor) => { - return (editor && editor.getAttributes('link').href) || '' - } - ) - - const handleClearLinkForm = () => { - if (currentUrl()) { - props.editor.chain().focus().unsetLink().run() - } - setLinkEditorOpen(false) - } - - return ( - <> -
- - - (validateImage(value) ? '' : t('Invalid url format'))} - onSubmit={handleLinkFormSubmit} - onClose={() => setLinkEditorOpen(false)} - errorMessage={t('Error')} - /> - - - <> -
- - -
-
{t('Headers')}
-
- - - -
-
{t('Quotes')}
-
- - -
-
-
-
-
- - -
- - -
-
- - -
-
{t('Lists')}
-
- - -
-
-
-
- - - -
- - ) -} diff --git a/src/components/Editor/EditorBubbleMenu/index.ts b/src/components/Editor/EditorBubbleMenu/index.ts deleted file mode 100644 index c4d82a89..00000000 --- a/src/components/Editor/EditorBubbleMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { EditorBubbleMenu } from './EditorBubbleMenu' diff --git a/src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.module.scss b/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.module.scss similarity index 100% rename from src/components/Editor/EditorBubbleMenu/EditorBubbleMenu.module.scss rename to src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.module.scss diff --git a/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx b/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx new file mode 100644 index 00000000..dc29029f --- /dev/null +++ b/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx @@ -0,0 +1,15 @@ +import type { Editor } from '@tiptap/core' +import styles from './ImageBubbleMenu.module.scss' + +type BubbleMenuProps = { + editor: Editor + ref: (el: HTMLDivElement) => void +} + +export const ImageBubbleMenu = (props: BubbleMenuProps) => { + return ( +
+ test +
+ ) +} diff --git a/src/components/Editor/ImageBubbleMenu/index.ts b/src/components/Editor/ImageBubbleMenu/index.ts new file mode 100644 index 00000000..8eb0eb1a --- /dev/null +++ b/src/components/Editor/ImageBubbleMenu/index.ts @@ -0,0 +1 @@ +export { ImageBubbleMenu } from './ImageBubbleMenu' diff --git a/src/components/Editor/EditorBubbleMenu/LinkForm/LinkForm.module.scss b/src/components/Editor/TextBubbleMenu/LinkForm/LinkForm.module.scss similarity index 100% rename from src/components/Editor/EditorBubbleMenu/LinkForm/LinkForm.module.scss rename to src/components/Editor/TextBubbleMenu/LinkForm/LinkForm.module.scss diff --git a/src/components/Editor/EditorBubbleMenu/LinkForm/LinkForm.tsx b/src/components/Editor/TextBubbleMenu/LinkForm/LinkForm.tsx similarity index 100% rename from src/components/Editor/EditorBubbleMenu/LinkForm/LinkForm.tsx rename to src/components/Editor/TextBubbleMenu/LinkForm/LinkForm.tsx diff --git a/src/components/Editor/EditorBubbleMenu/LinkForm/index.ts b/src/components/Editor/TextBubbleMenu/LinkForm/index.ts similarity index 100% rename from src/components/Editor/EditorBubbleMenu/LinkForm/index.ts rename to src/components/Editor/TextBubbleMenu/LinkForm/index.ts diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss new file mode 100644 index 00000000..12eb9215 --- /dev/null +++ b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss @@ -0,0 +1,85 @@ +.bubbleMenu { + background: #fff; + box-shadow: 0 4px 10px rgba(#000, 0.25); + + .bubbleMenuButton { + display: inline-flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + opacity: 0.5; + padding: 1rem; + + .triangle { + margin-left: 4px; + } + + .colorWheel { + display: inline-block; + width: 20px; + height: 20px; + border-radius: 50%; + background: #f6e3a1; + } + } + + .bubbleMenuButtonActive { + opacity: 1; + } + + .delimiter { + background: #999; + display: inline-block; + height: 1.4em; + margin: 0 0.2em; + vertical-align: text-bottom; + width: 1px; + } + + .dropDownHolder { + position: relative; + cursor: pointer; + display: inline-flex; + flex-flow: row nowrap; + align-items: center; + + .dropDown { + position: absolute; + padding: 6px; + top: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + box-shadow: 0 4px 10px rgb(0 0 0 / 25%); + background: #fff; + color: #898c94; + + & > header { + font-size: 10px; + border-bottom: 1px solid #898c94; + } + + .actions { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 12px; + flex-wrap: nowrap; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } + + .bubbleMenuButton { + min-width: 40px; + } + } + } + } + + .dropDownEnter, + .dropDownExit { + height: 0; + color: transparent; + } +} diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx new file mode 100644 index 00000000..3ffa2d08 --- /dev/null +++ b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx @@ -0,0 +1,256 @@ +import { Switch, Match, createSignal, Show } from 'solid-js' +import type { Editor } from '@tiptap/core' +import styles from './TextBubbleMenu.module.scss' +import { Icon } from '../../_shared/Icon' +import { clsx } from 'clsx' +import { createEditorTransaction } from 'solid-tiptap' +import { useLocalize } from '../../../context/localize' +import { InlineForm } from '../InlineForm' +import validateImage from '../../../utils/validateUrl' + +type BubbleMenuProps = { + editor: Editor + ref: (el: HTMLDivElement) => void +} + +export const TextBubbleMenu = (props: BubbleMenuProps) => { + const { t } = useLocalize() + const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false) + const [listBubbleOpen, setListBubbleOpen] = createSignal(false) + const [linkEditorOpen, setLinkEditorOpen] = createSignal(false) + + const isActive = (name: string, attributes?: unknown) => + createEditorTransaction( + () => props.editor, + (editor) => { + return editor && editor.isActive(name, attributes) + } + ) + + const isBold = isActive('bold') + const isItalic = isActive('italic') + const isH1 = isActive('heading', { level: 1 }) + const isH2 = isActive('heading', { level: 2 }) + const isH3 = isActive('heading', { level: 3 }) + const isBlockQuote = isActive('blockquote') + const isOrderedList = isActive('isOrderedList') + const isBulletList = isActive('isBulletList') + const isLink = isActive('link') + + const toggleLinkForm = () => { + setLinkEditorOpen(true) + } + + const toggleTextSizePopup = () => { + if (listBubbleOpen()) { + setListBubbleOpen(false) + } + + setTextSizeBubbleOpen((prev) => !prev) + } + const toggleListPopup = () => { + if (textSizeBubbleOpen()) { + setTextSizeBubbleOpen(false) + } + + setListBubbleOpen((prev) => !prev) + } + + const handleLinkFormSubmit = (value: string) => { + props.editor.chain().focus().setLink({ href: value }).run() + } + + const currentUrl = createEditorTransaction( + () => props.editor, + (editor) => { + return (editor && editor.getAttributes('link').href) || '' + } + ) + + const handleClearLinkForm = () => { + if (currentUrl()) { + props.editor.chain().focus().unsetLink().run() + } + setLinkEditorOpen(false) + } + + return ( +
+ + + (validateImage(value) ? '' : t('Invalid url format'))} + onSubmit={handleLinkFormSubmit} + onClose={() => setLinkEditorOpen(false)} + errorMessage={t('Error')} + /> + + + <> +
+ + +
+
{t('Headers')}
+
+ + + +
+
{t('Quotes')}
+
+ + +
+
+
+
+
+ + +
+ + +
+
+ + +
+
{t('Lists')}
+
+ + +
+
+
+
+ + + +
+ ) +} diff --git a/src/components/Editor/TextBubbleMenu/index.ts b/src/components/Editor/TextBubbleMenu/index.ts new file mode 100644 index 00000000..5dd90ad9 --- /dev/null +++ b/src/components/Editor/TextBubbleMenu/index.ts @@ -0,0 +1 @@ +export { TextBubbleMenu } from './TextBubbleMenu' diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx index ba568081..b5a49ac4 100644 --- a/src/components/Nav/HeaderAuth.tsx +++ b/src/components/Nav/HeaderAuth.tsx @@ -50,6 +50,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => { const showNotifications = createMemo(() => isAuthenticated() && !isEditorPage()) const showSaveButton = createMemo(() => isAuthenticated() && isEditorPage()) const showCreatePostButton = createMemo(() => isAuthenticated() && !isEditorPage()) + const showAuthenticatedControls = createMemo(() => isAuthenticated() && isEditorPage()) const handleBurgerButtonClick = () => { toggleEditorPanel() @@ -120,7 +121,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => { diff --git a/src/graphql/query/article-load.ts b/src/graphql/query/article-load.ts index 6bec8004..5073a1b9 100644 --- a/src/graphql/query/article-load.ts +++ b/src/graphql/query/article-load.ts @@ -3,7 +3,6 @@ import { gql } from '@urql/core' export default gql` query LoadShoutQuery($slug: String!) { loadShout(slug: $slug) { - _id: slug id title subtitle @@ -15,12 +14,11 @@ export default gql` # community mainTopic topics { - # id + id title body slug stat { - _id: shouts shouts authors followers @@ -35,7 +33,6 @@ export default gql` createdAt publishedAt stat { - _id: viewed viewed reacted rating