diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index d4714bac..31222f03 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -105,6 +105,7 @@ "Email": "Mail", "Enter": "Enter", "Enter URL address": "Enter URL address", + "Enter footnote text": "Enter footnote text", "Enter image description": "Enter image description", "Enter image title": "Enter image title", "Enter text": "Enter text", diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index da916c9b..47d0ff9a 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -420,5 +420,6 @@ "user already exist": "пользователь уже существует", "video": "видео", "view": "просмотр", - "zine": "журнал" + "zine": "журнал", + "Enter footnote text": "Введите текст сноски" } diff --git a/src/components/Article/Article.module.scss b/src/components/Article/Article.module.scss index 650ede1a..d4d3440a 100644 --- a/src/components/Article/Article.module.scss +++ b/src/components/Article/Article.module.scss @@ -564,6 +564,10 @@ a[data-toggle='tooltip'] { background: var(--black-500); color: var(--default-color-invert); + p:last-child { + margin: 0; + } + &::after { content: ''; position: absolute; diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 93578908..473d2efa 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -2,18 +2,13 @@ import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } import { Title } from '@solidjs/meta' import { clsx } from 'clsx' import { getPagePath } from '@nanostores/router' - import MD from './MD' - import type { Author, Shout } from '../../graphql/types.gen' import { useSession } from '../../context/session' import { useLocalize } from '../../context/localize' import { useReactions } from '../../context/reactions' - import { MediaItem } from '../../pages/types' - import { router, useRouter } from '../../stores/router' - import { formatDate } from '../../utils' import { getDescription } from '../../utils/meta' import { imageProxy } from '../../utils/imageProxy' @@ -26,7 +21,6 @@ import { ShoutRatingControl } from './ShoutRatingControl' import { CommentsTree } from './CommentsTree' import stylesHeader from '../Nav/Header/Header.module.scss' import { AudioHeader } from './AudioHeader' - import { Popover } from '../_shared/Popover' import { VideoPlayer } from '../_shared/VideoPlayer' import { Icon } from '../_shared/Icon' @@ -47,6 +41,7 @@ export const FullArticle = (props: Props) => { isAuthenticated, actions: { requireAuthentication } } = useSession() + const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt))) @@ -131,17 +126,23 @@ export const FullArticle = (props: Props) => { const clickHandlers = [] const documentClickHandlers = [] - onMount(() => { - const tooltipElements: NodeListOf = - document.querySelectorAll('[data-toggle="tooltip"]') - if (!tooltipElements) return + createEffect(() => { + if (!body()) { + return + } + const tooltipElements: NodeListOf = document.querySelectorAll( + '[data-toggle="tooltip"], footnote' + ) + if (!tooltipElements) return tooltipElements.forEach((element) => { const tooltip = document.createElement('div') tooltip.classList.add(styles.tooltip) - tooltip.textContent = element.dataset.originalTitle + tooltip.innerHTML = element.dataset.originalTitle || element.dataset.value document.body.appendChild(tooltip) - element.setAttribute('href', 'javascript: void(0);') + if (element.tagName === 'a') { + element.setAttribute('href', 'javascript: void(0);') + } createPopper(element, tooltip, { placement: 'top', modifiers: [ diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index d6ccd409..7aa96413 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -47,6 +47,7 @@ import { isDesktop } from '../../utils/media-query' import './Prosemirror.scss' import { Image } from '@tiptap/extension-image' +import { Footnote } from './extensions/Footnote' type Props = { shoutId: number @@ -173,6 +174,7 @@ export const Editor = (props: Props) => { ImageFigure, Image, Figcaption, + Footnote, Embed, CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689 BubbleMenu.configure({ diff --git a/src/components/Editor/Prosemirror.scss b/src/components/Editor/Prosemirror.scss index 3c133e87..e9ab8c8e 100644 --- a/src/components/Editor/Prosemirror.scss +++ b/src/components/Editor/Prosemirror.scss @@ -258,3 +258,24 @@ mark.highlight { figure[data-type='capturedImage'] { flex-direction: column-reverse; } + +footnote { + display: inline-flex; + position: relative; + cursor: pointer; + &: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; + } +} diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index ec6b0ac9..f8fd3939 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -1,4 +1,5 @@ import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js' +import { Portal } from 'solid-js/web' import { createEditorTransaction, createTiptapEditor, @@ -33,14 +34,14 @@ import { Figcaption } from './extensions/Figcaption' import { TextBubbleMenu } from './TextBubbleMenu' import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { CharacterCount } from '@tiptap/extension-character-count' -import { createStore } from 'solid-js/store' type Props = { + placeholder: string initialContent?: string label?: string onSubmit?: (text: string) => void + onCancel?: () => void onChange?: (text: string) => void - placeholder: string variant?: 'minimal' | 'bordered' maxLength?: number submitButtonText?: string @@ -179,6 +180,9 @@ const SimplifiedEditor = (props: Props) => { } const handleClear = () => { + if (props.onCancel) { + props.onCancel() + } editor().commands.clearContent(true) } @@ -204,7 +208,7 @@ const SimplifiedEditor = (props: Props) => { if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !editor().state.selection.empty) { event.preventDefault() - showModal('editorInsertLink') + showModal('simplifiedEditorInsertLink') } } @@ -222,7 +226,7 @@ const SimplifiedEditor = (props: Props) => { }) } - const handleInsertLink = () => !editor().state.selection.empty && showModal('editorInsertLink') + const handleInsertLink = () => !editor().state.selection.empty && showModal('simplifiedEditorInsertLink') createEffect(() => { if (html()) { @@ -306,7 +310,7 @@ const SimplifiedEditor = (props: Props) => { )} diff --git a/src/components/Editor/extensions/Footnote.ts b/src/components/Editor/extensions/Footnote.ts new file mode 100644 index 00000000..327df0af --- /dev/null +++ b/src/components/Editor/extensions/Footnote.ts @@ -0,0 +1,104 @@ +import { mergeAttributes, Node } from '@tiptap/core' + +declare module '@tiptap/core' { + interface Commands { + Footnote: { + setFootnote: (options: { value: string }) => ReturnType + updateFootnote: (options: { value: string }) => ReturnType + deleteFootnote: () => ReturnType + } + } +} + +export const Footnote = Node.create({ + name: 'footnote', + addOptions() { + return { + HTMLAttributes: {} + } + }, + group: 'inline', + content: 'text*', + inline: true, + isolating: true, + + addAttributes() { + return { + value: { + default: null, + parseHTML: (element) => { + return { + value: element.dataset.value + } + }, + renderHTML: (attributes) => { + return { + 'data-value': attributes.value + } + } + } + } + }, + + parseHTML() { + return [ + { + tag: 'footnote' + } + ] + }, + + renderHTML({ HTMLAttributes }) { + return ['footnote', mergeAttributes(HTMLAttributes), 0] + }, + + addCommands() { + return { + setFootnote: + (attributes) => + ({ tr, state }) => { + const { selection } = state + const position = selection.$to.pos + + console.log('!!! attributes:', attributes) + const node = this.type.create(attributes) + tr.insert(position, node) + tr.insertText('\u00A0', position + 1) // it's make selection visible + return true + }, + updateFootnote: + (newValue) => + ({ tr, state }) => { + const { selection } = state + const { $from, $to } = selection + + if ($from.parent.type.name === 'footnote' || $to.parent.type.name === 'footnote') { + const node = $from.parent.type.name === 'footnote' ? $from.parent : $to.parent + const pos = $from.parent.type.name === 'footnote' ? $from.pos - 1 : $to.pos - 1 + + const newNode = node.type.create({ value: newValue }) + tr.setNodeMarkup(pos, null, newNode.attrs) + + return true + } + + return false + }, + deleteFootnote: + () => + ({ tr, state }) => { + const { selection } = state + const { $from, $to } = selection + + if ($from.parent.type.name === 'footnote' || $to.parent.type.name === 'footnote') { + const startPos = $from.start($from.depth) + const endPos = $to.end($to.depth) + tr.delete(startPos, endPos) + return true + } + + return false + } + } + } +}) diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index 6363eeb6..6b38e428 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -23,6 +23,10 @@ type FormFields = { type ValidationErrors = Partial> +const handleEmailInput = (newEmail: string) => { + setEmail(newEmail) +} + export const RegisterForm = () => { const { changeSearchParam } = useRouter() const { t } = useLocalize() @@ -38,10 +42,6 @@ export const RegisterForm = () => { const authFormRef: { current: HTMLFormElement } = { current: null } - const handleEmailInput = (newEmail: string) => { - setEmail(newEmail) - } - const handleEmailBlur = () => { if (validateEmail(email())) { checkEmail(email()) diff --git a/src/components/Nav/Modal/Modal.module.scss b/src/components/Nav/Modal/Modal.module.scss index f0b55f3e..6a9b655b 100644 --- a/src/components/Nav/Modal/Modal.module.scss +++ b/src/components/Nav/Modal/Modal.module.scss @@ -10,7 +10,7 @@ position: fixed; top: 0; width: 100%; - z-index: 11000; + z-index: 10002; } .modal { diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index baa4e5ce..b9f62582 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -38,7 +38,8 @@ export const PublishSettings = (props: Props) => { const composeDescription = () => { if (!props.form.description) { - const leadText = props.form.body.replaceAll(/<\/?[^>]+(>|$)/gi, ' ') + const cleanFootnotes = props.form.body.replaceAll(/.*?<\/footnote>/g, '') + const leadText = cleanFootnotes.replaceAll(/<\/?[^>]+(>|$)/gi, ' ') return shorten(leadText, MAX_DESCRIPTION_LIMIT).trim() } return props.form.description diff --git a/src/context/editor.tsx b/src/context/editor.tsx index d65080c2..75812c53 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -92,15 +92,12 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const [form, setForm] = createStore(null) const [formErrors, setFormErrors] = createStore>(null) - const [wordCounter, setWordCounter] = createSignal({ characters: 0, words: 0 }) - const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value) const countWords = (value) => setWordCounter(value) - const validate = () => { if (!form.title) { setFormErrors('title', t('Required')) diff --git a/src/stores/ui.ts b/src/stores/ui.ts index ed376d7a..b01bd52d 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -16,8 +16,10 @@ export type ModalType = | 'donate' | 'inviteToChat' | 'uploadImage' + | 'simplifiedEditorUploadImage' | 'uploadCoverImage' | 'editorInsertLink' + | 'simplifiedEditorInsertLink' type WarnKind = 'error' | 'warn' | 'info' @@ -36,8 +38,10 @@ export const MODALS: Record = { donate: 'donate', inviteToChat: 'inviteToChat', uploadImage: 'uploadImage', + simplifiedEditorUploadImage: 'simplifiedEditorUploadImage', uploadCoverImage: 'uploadCoverImage', - editorInsertLink: 'editorInsertLink' + editorInsertLink: 'editorInsertLink', + simplifiedEditorInsertLink: 'simplifiedEditorInsertLink' } const [modal, setModal] = createSignal(null)