diff --git a/public/error.svg b/public/error.svg index a520a262..71d58e79 100644 --- a/public/error.svg +++ b/public/error.svg @@ -34,4 +34,4 @@ - \ No newline at end of file + diff --git a/public/icons/editor-image-half-align-left.svg b/public/icons/editor-image-half-align-left.svg new file mode 100644 index 00000000..61b9d858 --- /dev/null +++ b/public/icons/editor-image-half-align-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/editor-image-half-align-right.svg b/public/icons/editor-image-half-align-right.svg new file mode 100644 index 00000000..fe129bb6 --- /dev/null +++ b/public/icons/editor-image-half-align-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/icons/editor-squib.svg b/public/icons/editor-squib.svg new file mode 100644 index 00000000..519a3975 --- /dev/null +++ b/public/icons/editor-squib.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 711e6c01..5175fa79 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -295,5 +295,7 @@ "number list": "number list", "delimiter": "delimiter", "cancel_low_caps": "cancel", - "repeat": "repeat" + "repeat": "repeat", + "Add signature": "Add signature", + "Substrate": "Substrate" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index 72a75569..862c6e7f 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -300,6 +300,7 @@ "sign up": "зарегистрироваться", "sign up or sign in": "зарегистрироваться или войти", "slug is used by another user": "Имя уже занято другим пользователем", + "squib": "Подверстка", "terms of use": "правилами пользования сайтом", "topics": "темы", "user already exist": "пользователь уже существует", @@ -316,5 +317,7 @@ "number list": "нумер. список", "delimiter": "разделитель", "cancel_low_caps": "отменить", - "repeat": "повторить" + "repeat": "повторить", + "Add signature": "Добавить подпись", + "Substrate": "Подложка" } diff --git a/src/components/Article/Article.module.scss b/src/components/Article/Article.module.scss index e7a90172..7ee1567c 100644 --- a/src/components/Article/Article.module.scss +++ b/src/components/Article/Article.module.scss @@ -372,15 +372,3 @@ img { } } } - -[data-float] { - max-width: 50%; -} - -[data-float='left'] { - float: left; -} - -[data-float='right'] { - float: right; -} diff --git a/src/components/Editor/BubbleMenu/BlockquoteBubbleMenu.tsx b/src/components/Editor/BubbleMenu/BlockquoteBubbleMenu.tsx new file mode 100644 index 00000000..796a8d97 --- /dev/null +++ b/src/components/Editor/BubbleMenu/BlockquoteBubbleMenu.tsx @@ -0,0 +1,40 @@ +import type { Editor } from '@tiptap/core' +import styles from './FigureBubbleMenu.module.scss' +import { clsx } from 'clsx' +import { Icon } from '../../_shared/Icon' +import { useLocalize } from '../../../context/localize' + +type Props = { + editor: Editor + ref: (el: HTMLElement) => void +} + +export const BlockquoteBubbleMenu = (props: Props) => { + return ( +
+ + + +
+ ) +} diff --git a/src/components/Editor/BubbleMenu/FigureBubbleMenu.module.scss b/src/components/Editor/BubbleMenu/FigureBubbleMenu.module.scss new file mode 100644 index 00000000..3cad51d7 --- /dev/null +++ b/src/components/Editor/BubbleMenu/FigureBubbleMenu.module.scss @@ -0,0 +1,107 @@ +.FigureBubbleMenu { + background: #000; + display: flex; + flex-direction: row; + align-items: center; + + .bubbleMenuButton { + display: inline-flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + opacity: 0.5; + padding: 1rem; + + .triangle { + margin-left: 4px; + } + + img { + display: block; + } + } + + .bubbleMenuButtonActive { + opacity: 1; + } + + .delimiter { + background: #fff; + opacity: 0.5; + 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: #000; + color: #898c94; + + & > header { + font-size: 10px; + border-bottom: 1px solid #898c94; + } + + .actions { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 10px; + margin-bottom: 8px; + + .color { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #3c3c3c; + background: #ccc; + + &.yellow { + background: #f6e3a1; + } + &.white { + background: #fff; + box-shadow: inset 0 0 0 1px #000; + border-color: #fff; + } + &.yellow { + background: #f6e3a1; + } + &.pink { + background: #f1b5bc; + } + &.green { + background: #bfe9cb; + box-shadow: inset 0 0 0 1px #000; + border-color: #fff; + } + &.black { + background: #000; + } + } + + &:last-child { + margin-bottom: 0; + } + + .bubbleMenuButton { + min-width: 40px; + } + } + } + } +} diff --git a/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx b/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx similarity index 62% rename from src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx rename to src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx index c6156c68..4ae20add 100644 --- a/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx +++ b/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx @@ -1,43 +1,40 @@ import type { Editor } from '@tiptap/core' -import styles from './ImageBubbleMenu.module.scss' +import styles from './FigureBubbleMenu.module.scss' import { clsx } from 'clsx' import { Icon } from '../../_shared/Icon' +import { useLocalize } from '../../../context/localize' -type BubbleMenuProps = { +type Props = { editor: Editor - ref: (el: HTMLDivElement) => void + ref: (el: HTMLElement) => void } -export const ImageBubbleMenu = (props: BubbleMenuProps) => { +export const FigureBubbleMenu = (props: Props) => { + const { t } = useLocalize() return ( -
+
+
+ + + + + + + +
+
+ + +
+
+ + {(bg) => ( +
props.editor.chain().focus().setArticleBg(bg).run()} + class={clsx(styles.color, styles[bg])} + /> + )} + +
+
+ +
+
+ ) +} diff --git a/src/components/Editor/BubbleMenu/index.ts b/src/components/Editor/BubbleMenu/index.ts new file mode 100644 index 00000000..950f3ba3 --- /dev/null +++ b/src/components/Editor/BubbleMenu/index.ts @@ -0,0 +1,3 @@ +export { FigureBubbleMenu } from './FigureBubbleMenu' +export { BlockquoteBubbleMenu } from './BlockquoteBubbleMenu' +export { IncutBubbleMenu } from './IncutBubbleMenu' diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 98630a27..a7365d48 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -1,7 +1,6 @@ import { createEffect, createSignal } from 'solid-js' import { createTiptapEditor, useEditorHTML } from 'solid-tiptap' import { useLocalize } from '../../context/localize' -import { Blockquote } from '@tiptap/extension-blockquote' import { Bold } from '@tiptap/extension-bold' import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { Dropcursor } from '@tiptap/extension-dropcursor' @@ -23,26 +22,27 @@ import { Link } from '@tiptap/extension-link' import { Document } from '@tiptap/extension-document' import { Text } from '@tiptap/extension-text' import { CustomImage } from './extensions/CustomImage' +import { CustomBlockquote } from './extensions/CustomBlockquote' import { Figure } from './extensions/Figure' import { Paragraph } from '@tiptap/extension-paragraph' import Focus from '@tiptap/extension-focus' import * as Y from 'yjs' import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor' import { Collaboration } from '@tiptap/extension-collaboration' - import { IndexeddbPersistence } from 'y-indexeddb' import { useSession } from '../../context/session' import uniqolor from 'uniqolor' import { HocuspocusProvider } from '@hocuspocus/provider' import { Embed } from './extensions/Embed' import { TextBubbleMenu } from './TextBubbleMenu' -import { ImageBubbleMenu } from './ImageBubbleMenu' +import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu' import { EditorFloatingMenu } from './EditorFloatingMenu' import { useEditorContext } from '../../context/editor' import { isTextSelection } from '@tiptap/core' import type { Doc } from 'yjs/dist/src/utils/Doc' import './Prosemirror.scss' import { TrailingNode } from './extensions/TrailingNode' +import Article from './extensions/Article' type EditorProps = { shoutId: number @@ -58,6 +58,7 @@ export const Editor = (props: EditorProps) => { const { t } = useLocalize() const { user } = useSession() const [isCommonMarkup, setIsCommonMarkup] = createSignal(false) + const [floatMenuRef, setFloatMenuRef] = createSignal<'blockquote' | 'image' | 'incut'>() const docName = `shout-${props.shoutId}` @@ -89,8 +90,18 @@ export const Editor = (props: EditorProps) => { current: null } - const imageBubbleMenuRef: { - current: HTMLDivElement + const incutBubbleMenuRef: { + current: HTMLElement + } = { + current: null + } + const figureBubbleMenuRef: { + current: HTMLElement + } = { + current: null + } + const blockquoteBubbleMenuRef: { + current: HTMLElement } = { current: null } @@ -108,7 +119,7 @@ export const Editor = (props: EditorProps) => { Text, Paragraph, Dropcursor, - Blockquote, + CustomBlockquote, Bold, Italic, Strike, @@ -163,16 +174,36 @@ export const Editor = (props: EditorProps) => { shouldShow: ({ editor: e, view, state, from, to }) => { const { doc, selection } = state const { empty } = selection - const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection) setIsCommonMarkup(e.isActive('figure')) - return view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image') + return ( + view.hasFocus() && + !empty && + !isEmptyTextBlock && + !e.isActive('image') && + !e.isActive('blockquote') && + !e.isActive('article') + ) + } + }), + BubbleMenu.configure({ + pluginKey: 'blockquoteBubbleMenu', + element: blockquoteBubbleMenuRef.current, + shouldShow: ({ editor: e, view }) => { + return view.hasFocus() && e.isActive('blockquote') + } + }), + BubbleMenu.configure({ + pluginKey: 'incutBubbleMenu', + element: incutBubbleMenuRef.current, + shouldShow: ({ editor: e, view }) => { + return view.hasFocus() && e.isActive('article') } }), BubbleMenu.configure({ pluginKey: 'imageBubbleMenu', - element: imageBubbleMenuRef.current, + element: figureBubbleMenuRef.current, shouldShow: ({ editor: e, view }) => { return view.hasFocus() && e.isActive('image') } @@ -183,7 +214,8 @@ export const Editor = (props: EditorProps) => { }, element: floatingMenuRef.current }), - TrailingNode + TrailingNode, + Article ] })) @@ -213,7 +245,24 @@ export const Editor = (props: EditorProps) => { editor={editor()} ref={(el) => (textBubbleMenuRef.current = el)} /> - (imageBubbleMenuRef.current = el)} /> + { + blockquoteBubbleMenuRef.current = el + }} + editor={editor()} + /> + { + figureBubbleMenuRef.current = el + }} + /> + { + incutBubbleMenuRef.current = el + }} + /> (floatingMenuRef.current = el)} /> ) diff --git a/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.module.scss b/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.module.scss deleted file mode 100644 index 1f32a873..00000000 --- a/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.module.scss +++ /dev/null @@ -1,33 +0,0 @@ -.ImageBubbleMenu { - background: #000; - display: flex; - flex-direction: row; - align-items: center; - - .bubbleMenuButton { - display: inline-flex; - align-items: center; - justify-content: center; - flex-wrap: nowrap; - opacity: 0.5; - padding: 1rem; - - img { - display: block; - } - } - - .bubbleMenuButtonActive { - opacity: 1; - } - - .delimiter { - background: #fff; - opacity: 0.5; - display: inline-block; - height: 1.4em; - margin: 0 0.2em; - vertical-align: text-bottom; - width: 1px; - } -} diff --git a/src/components/Editor/ImageBubbleMenu/index.ts b/src/components/Editor/ImageBubbleMenu/index.ts deleted file mode 100644 index 8eb0eb1a..00000000 --- a/src/components/Editor/ImageBubbleMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ImageBubbleMenu } from './ImageBubbleMenu' diff --git a/src/components/Editor/Prosemirror.scss b/src/components/Editor/Prosemirror.scss index 64cb38b5..fc7e0dd6 100644 --- a/src/components/Editor/Prosemirror.scss +++ b/src/components/Editor/Prosemirror.scss @@ -1,14 +1,6 @@ .ProseMirror { outline: none; min-height: 300px; - - blockquote { - @include font-size(1.6rem); - - border-left: 2px solid; - margin: 1.5em 0; - padding-left: 1.6em; - } } .ProseMirror p.is-editor-empty:first-child::before { @@ -78,3 +70,110 @@ mark.highlight { box-decoration-break: clone; padding: 0.125em 0; } + +// custom atibutes fro TipTap Nodes + +[data-float] { + max-width: 50%; +} +[data-float='left'] { + max-width: 30%; + float: left; + margin: 1rem 1rem 0 0; +} +[data-float='right'] { + max-width: 30%; + float: right; + margin: 1rem 0 1rem 1rem; +} + +[data-float='half-left'] { + max-width: 50%; + min-width: 30%; + float: left; + margin: 1rem 1rem 0; +} +[data-float='half-right'] { + max-width: 50%; + min-width: 30%; + float: right; + margin: 1rem 0 1rem; +} + +.ProseMirror blockquote { + p:last-child { + margin-bottom: 0; + } + + &[data-type='quote'] { + @include font-size(1.6rem); + + border: solid #000; + border-width: 0 0 0 2px; + margin: 1.6rem 0; + padding: 1rem; + + &[data-float='left'] { + padding-left: 0; + padding-right: 0; + margin-right: 1.6rem; + border-width: 0 2px 0 0; + } + + &[data-float='right'] { + margin-left: 1.6rem; + border-width: 0 0 0 2px; + } + } + + &[data-type='punchline'] { + padding: 1.6rem 0 0; + border: solid #000; + border-width: 2px 0; + font-size: 3.2rem; + font-weight: 700; + margin: 0; + + &[data-float='left'], + &[data-float='right'] { + font-size: 2.2rem; + } + } +} + +.ProseMirror article[data-type='incut'] { + background: #f1f2f3; + padding: 1.6rem; + font-size: 1.4rem; + transition: background 0.3s ease-in-out; + + &[data-float] img { + float: none; + max-width: unset; + width: 100% !important; + margin: 0; + } + + *: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: #bfe9cb; + box-shadow: 0 0 0 1px #000; + } + &[data-bg='white'] { + background: #fff; + box-shadow: 0 0 0 1px #000; + } +} diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx index 19e3b40b..0e000843 100644 --- a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx +++ b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx @@ -54,7 +54,6 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { if (textSizeBubbleOpen()) { setTextSizeBubbleOpen(false) } - setListBubbleOpen((prev) => !prev) } @@ -153,7 +152,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { [styles.bubbleMenuButtonActive]: isBlockQuote() })} onClick={() => { - props.editor.chain().focus().toggleBlockquote().run() + props.editor.chain().focus().toggleBlockquote('quote').run() toggleTextSizePopup() }} > @@ -165,13 +164,28 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { [styles.bubbleMenuButtonActive]: isBlockQuote() })} onClick={() => { - props.editor.chain().focus().toggleBlockquote().run() + props.editor.chain().focus().toggleBlockquote('punchline').run() toggleTextSizePopup() }} >
+
{t('squib')}
+
+ +
diff --git a/src/components/Editor/extensions/Article.ts b/src/components/Editor/extensions/Article.ts new file mode 100644 index 00000000..15a91511 --- /dev/null +++ b/src/components/Editor/extensions/Article.ts @@ -0,0 +1,66 @@ +import { Node, mergeAttributes } from '@tiptap/core' + +declare module '@tiptap/core' { + interface Commands { + Article: { + toggleArticle: () => ReturnType + setArticleFloat: (float: null | 'left' | 'half-left' | 'right' | 'half-right') => ReturnType + setArticleBg: (bg: null | string) => ReturnType + } + } +} + +export default Node.create({ + name: 'article', + + defaultOptions: { + HTMLAttributes: { + 'data-type': 'incut' + } + }, + group: 'block', + content: 'block+', + + parseHTML() { + return [ + { + tag: 'article' + } + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['article', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] + }, + + addAttributes() { + return { + 'data-float': { + default: null + }, + 'data-bg': { + default: null + } + } + }, + + addCommands() { + return { + toggleArticle: + () => + ({ commands }) => { + return commands.toggleWrap('article') + }, + setArticleFloat: + (value) => + ({ commands }) => { + return commands.updateAttributes(this.name, { 'data-float': value }) + }, + setArticleBg: + (value) => + ({ commands }) => { + return commands.updateAttributes(this.name, { 'data-bg': value }) + } + } + } +}) diff --git a/src/components/Editor/extensions/CustomBlockquote.ts b/src/components/Editor/extensions/CustomBlockquote.ts new file mode 100644 index 00000000..681b9a29 --- /dev/null +++ b/src/components/Editor/extensions/CustomBlockquote.ts @@ -0,0 +1,48 @@ +import { Blockquote } from '@tiptap/extension-blockquote' +import { Command } from '@tiptap/core' + +export type QuoteTypes = 'quote' | 'punchline' + +declare module '@tiptap/core' { + interface Commands { + CustomBlockquote: { + toggleBlockquote: (type: QuoteTypes) => ReturnType + setBlockQuoteFloat: (float: null | 'left' | 'right') => ReturnType + } + } +} + +export const CustomBlockquote = Blockquote.extend({ + name: 'blockquote', + defaultOptions: { + HTMLAttributes: {}, + group: 'block', + content: 'block+' + }, + addAttributes() { + return { + 'data-float': { + default: null + }, + 'data-type': { + default: null + } + } + }, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + addCommands() { + return { + toggleBlockquote: + (type) => + ({ commands }) => { + return commands.toggleWrap(this.name, { 'data-type': type }) + }, + setBlockQuoteFloat: + (value) => + ({ commands }) => { + return commands.updateAttributes(this.name, { 'data-float': value }) + } + } + } +}) diff --git a/src/components/Editor/extensions/CustomImage.ts b/src/components/Editor/extensions/CustomImage.ts index f5d68447..7829e94a 100644 --- a/src/components/Editor/extensions/CustomImage.ts +++ b/src/components/Editor/extensions/CustomImage.ts @@ -7,7 +7,7 @@ declare module '@tiptap/core' { * Add an image */ setImage: (options: { src: string; alt?: string; title?: string }) => ReturnType - setFloat: (float: null | 'left' | 'right') => ReturnType + setImageFloat: (float: null | 'left' | 'right') => ReturnType } } } @@ -42,7 +42,7 @@ export const CustomImage = Image.extend({ attrs: options }) }, - setFloat: + setImageFloat: (value) => ({ commands }) => { return commands.updateAttributes(this.name, { 'data-float': value })