parent
0ec7e5ceed
commit
ead332184d
|
@ -105,6 +105,7 @@
|
||||||
"Email": "Mail",
|
"Email": "Mail",
|
||||||
"Enter": "Enter",
|
"Enter": "Enter",
|
||||||
"Enter URL address": "Enter URL address",
|
"Enter URL address": "Enter URL address",
|
||||||
|
"Enter footnote text": "Enter footnote text",
|
||||||
"Enter image description": "Enter image description",
|
"Enter image description": "Enter image description",
|
||||||
"Enter image title": "Enter image title",
|
"Enter image title": "Enter image title",
|
||||||
"Enter text": "Enter text",
|
"Enter text": "Enter text",
|
||||||
|
|
|
@ -420,5 +420,6 @@
|
||||||
"user already exist": "пользователь уже существует",
|
"user already exist": "пользователь уже существует",
|
||||||
"video": "видео",
|
"video": "видео",
|
||||||
"view": "просмотр",
|
"view": "просмотр",
|
||||||
"zine": "журнал"
|
"zine": "журнал",
|
||||||
|
"Enter footnote text": "Введите текст сноски"
|
||||||
}
|
}
|
||||||
|
|
|
@ -564,6 +564,10 @@ a[data-toggle='tooltip'] {
|
||||||
background: var(--black-500);
|
background: var(--black-500);
|
||||||
color: var(--default-color-invert);
|
color: var(--default-color-invert);
|
||||||
|
|
||||||
|
p:last-child {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -2,18 +2,13 @@ import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup }
|
||||||
import { Title } from '@solidjs/meta'
|
import { Title } from '@solidjs/meta'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
|
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
|
|
||||||
import type { Author, Shout } from '../../graphql/types.gen'
|
import type { Author, Shout } from '../../graphql/types.gen'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useReactions } from '../../context/reactions'
|
import { useReactions } from '../../context/reactions'
|
||||||
|
|
||||||
import { MediaItem } from '../../pages/types'
|
import { MediaItem } from '../../pages/types'
|
||||||
|
|
||||||
import { router, useRouter } from '../../stores/router'
|
import { router, useRouter } from '../../stores/router'
|
||||||
|
|
||||||
import { formatDate } from '../../utils'
|
import { formatDate } from '../../utils'
|
||||||
import { getDescription } from '../../utils/meta'
|
import { getDescription } from '../../utils/meta'
|
||||||
import { imageProxy } from '../../utils/imageProxy'
|
import { imageProxy } from '../../utils/imageProxy'
|
||||||
|
@ -26,7 +21,6 @@ import { ShoutRatingControl } from './ShoutRatingControl'
|
||||||
import { CommentsTree } from './CommentsTree'
|
import { CommentsTree } from './CommentsTree'
|
||||||
import stylesHeader from '../Nav/Header/Header.module.scss'
|
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||||
import { AudioHeader } from './AudioHeader'
|
import { AudioHeader } from './AudioHeader'
|
||||||
|
|
||||||
import { Popover } from '../_shared/Popover'
|
import { Popover } from '../_shared/Popover'
|
||||||
import { VideoPlayer } from '../_shared/VideoPlayer'
|
import { VideoPlayer } from '../_shared/VideoPlayer'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
|
@ -47,6 +41,7 @@ export const FullArticle = (props: Props) => {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
actions: { requireAuthentication }
|
actions: { requireAuthentication }
|
||||||
} = useSession()
|
} = useSession()
|
||||||
|
|
||||||
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
||||||
|
|
||||||
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
|
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
|
||||||
|
@ -131,17 +126,23 @@ export const FullArticle = (props: Props) => {
|
||||||
const clickHandlers = []
|
const clickHandlers = []
|
||||||
const documentClickHandlers = []
|
const documentClickHandlers = []
|
||||||
|
|
||||||
onMount(() => {
|
createEffect(() => {
|
||||||
const tooltipElements: NodeListOf<HTMLLinkElement> =
|
if (!body()) {
|
||||||
document.querySelectorAll('[data-toggle="tooltip"]')
|
return
|
||||||
if (!tooltipElements) return
|
}
|
||||||
|
|
||||||
|
const tooltipElements: NodeListOf<HTMLElement> = document.querySelectorAll(
|
||||||
|
'[data-toggle="tooltip"], footnote'
|
||||||
|
)
|
||||||
|
if (!tooltipElements) return
|
||||||
tooltipElements.forEach((element) => {
|
tooltipElements.forEach((element) => {
|
||||||
const tooltip = document.createElement('div')
|
const tooltip = document.createElement('div')
|
||||||
tooltip.classList.add(styles.tooltip)
|
tooltip.classList.add(styles.tooltip)
|
||||||
tooltip.textContent = element.dataset.originalTitle
|
tooltip.innerHTML = element.dataset.originalTitle || element.dataset.value
|
||||||
document.body.appendChild(tooltip)
|
document.body.appendChild(tooltip)
|
||||||
element.setAttribute('href', 'javascript: void(0);')
|
if (element.tagName === 'a') {
|
||||||
|
element.setAttribute('href', 'javascript: void(0);')
|
||||||
|
}
|
||||||
createPopper(element, tooltip, {
|
createPopper(element, tooltip, {
|
||||||
placement: 'top',
|
placement: 'top',
|
||||||
modifiers: [
|
modifiers: [
|
||||||
|
|
|
@ -47,6 +47,7 @@ import { isDesktop } from '../../utils/media-query'
|
||||||
|
|
||||||
import './Prosemirror.scss'
|
import './Prosemirror.scss'
|
||||||
import { Image } from '@tiptap/extension-image'
|
import { Image } from '@tiptap/extension-image'
|
||||||
|
import { Footnote } from './extensions/Footnote'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -173,6 +174,7 @@ export const Editor = (props: Props) => {
|
||||||
ImageFigure,
|
ImageFigure,
|
||||||
Image,
|
Image,
|
||||||
Figcaption,
|
Figcaption,
|
||||||
|
Footnote,
|
||||||
Embed,
|
Embed,
|
||||||
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
|
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
|
||||||
BubbleMenu.configure({
|
BubbleMenu.configure({
|
||||||
|
|
|
@ -258,3 +258,24 @@ mark.highlight {
|
||||||
figure[data-type='capturedImage'] {
|
figure[data-type='capturedImage'] {
|
||||||
flex-direction: column-reverse;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js'
|
||||||
|
import { Portal } from 'solid-js/web'
|
||||||
import {
|
import {
|
||||||
createEditorTransaction,
|
createEditorTransaction,
|
||||||
createTiptapEditor,
|
createTiptapEditor,
|
||||||
|
@ -33,14 +34,14 @@ import { Figcaption } from './extensions/Figcaption'
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||||
import { CharacterCount } from '@tiptap/extension-character-count'
|
import { CharacterCount } from '@tiptap/extension-character-count'
|
||||||
import { createStore } from 'solid-js/store'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
placeholder: string
|
||||||
initialContent?: string
|
initialContent?: string
|
||||||
label?: string
|
label?: string
|
||||||
onSubmit?: (text: string) => void
|
onSubmit?: (text: string) => void
|
||||||
|
onCancel?: () => void
|
||||||
onChange?: (text: string) => void
|
onChange?: (text: string) => void
|
||||||
placeholder: string
|
|
||||||
variant?: 'minimal' | 'bordered'
|
variant?: 'minimal' | 'bordered'
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
submitButtonText?: string
|
submitButtonText?: string
|
||||||
|
@ -179,6 +180,9 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
|
if (props.onCancel) {
|
||||||
|
props.onCancel()
|
||||||
|
}
|
||||||
editor().commands.clearContent(true)
|
editor().commands.clearContent(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +208,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
|
|
||||||
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !editor().state.selection.empty) {
|
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !editor().state.selection.empty) {
|
||||||
event.preventDefault()
|
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(() => {
|
createEffect(() => {
|
||||||
if (html()) {
|
if (html()) {
|
||||||
|
@ -306,7 +310,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
<button
|
<button
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => showModal('uploadImage')}
|
onClick={() => showModal('simplifiedEditorUploadImage')}
|
||||||
class={clsx(styles.actionButton, { [styles.active]: isBlockquote() })}
|
class={clsx(styles.actionButton, { [styles.active]: isBlockquote() })}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-dd-full" />
|
<Icon name="editor-image-dd-full" />
|
||||||
|
@ -317,7 +321,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<Show when={!props.onChange}>
|
<Show when={!props.onChange}>
|
||||||
<div class={styles.buttons}>
|
<div class={styles.buttons}>
|
||||||
<Button value={t('Cancel')} variant="secondary" disabled={isEmpty()} onClick={handleClear} />
|
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
||||||
<Button
|
<Button
|
||||||
value={props.submitButtonText ?? t('Send')}
|
value={props.submitButtonText ?? t('Send')}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
@ -328,17 +332,21 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Modal variant="narrow" name="editorInsertLink">
|
<Portal>
|
||||||
<InsertLinkForm editor={editor()} onClose={() => hideModal()} />
|
<Modal variant="narrow" name="simplifiedEditorInsertLink">
|
||||||
</Modal>
|
<InsertLinkForm editor={editor()} onClose={() => hideModal()} />
|
||||||
<Show when={props.imageEnabled}>
|
|
||||||
<Modal variant="narrow" name="uploadImage">
|
|
||||||
<UploadModalContent
|
|
||||||
onClose={(value) => {
|
|
||||||
renderImage(value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</Portal>
|
||||||
|
<Show when={props.imageEnabled}>
|
||||||
|
<Portal>
|
||||||
|
<Modal variant="narrow" name="simplifiedEditorUploadImage">
|
||||||
|
<UploadModalContent
|
||||||
|
onClose={(value) => {
|
||||||
|
renderImage(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</Portal>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.onlyBubbleControls}>
|
<Show when={props.onlyBubbleControls}>
|
||||||
<TextBubbleMenu
|
<TextBubbleMenu
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
background: var(--editor-bubble-menu-background);
|
background: var(--editor-bubble-menu-background);
|
||||||
box-shadow: 0 4px 10px rgba(#000, 0.25);
|
box-shadow: 0 4px 10px rgba(#000, 0.25);
|
||||||
|
|
||||||
|
&.growWidth {
|
||||||
|
min-width: 460px;
|
||||||
|
}
|
||||||
|
|
||||||
.bubbleMenuButton {
|
.bubbleMenuButton {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -7,6 +7,9 @@ import { createEditorTransaction } from 'solid-tiptap'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Popover } from '../../_shared/Popover'
|
import { Popover } from '../../_shared/Popover'
|
||||||
import { InsertLinkForm } from '../InsertLinkForm'
|
import { InsertLinkForm } from '../InsertLinkForm'
|
||||||
|
import SimplifiedEditor from '../SimplifiedEditor'
|
||||||
|
import { Button } from '../../_shared/Button'
|
||||||
|
import { showModal } from '../../../stores/ui'
|
||||||
|
|
||||||
type BubbleMenuProps = {
|
type BubbleMenuProps = {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
|
@ -27,6 +30,8 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
|
||||||
const [listBubbleOpen, setListBubbleOpen] = createSignal(false)
|
const [listBubbleOpen, setListBubbleOpen] = createSignal(false)
|
||||||
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false)
|
const [linkEditorOpen, setLinkEditorOpen] = createSignal(false)
|
||||||
|
const [footnoteEditorOpen, setFootnoteEditorOpen] = createSignal(false)
|
||||||
|
const [footNote, setFootNote] = createSignal<string>()
|
||||||
|
|
||||||
const isBold = isActive('bold')
|
const isBold = isActive('bold')
|
||||||
const isItalic = isActive('italic')
|
const isItalic = isActive('italic')
|
||||||
|
@ -38,6 +43,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
const isBulletList = isActive('isBulletList')
|
const isBulletList = isActive('isBulletList')
|
||||||
const isLink = isActive('link')
|
const isLink = isActive('link')
|
||||||
const isHighlight = isActive('highlight')
|
const isHighlight = isActive('highlight')
|
||||||
|
const isFootnote = isActive('footnote')
|
||||||
|
|
||||||
const toggleTextSizePopup = () => {
|
const toggleTextSizePopup = () => {
|
||||||
if (listBubbleOpen()) {
|
if (listBubbleOpen()) {
|
||||||
|
@ -58,6 +64,28 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentFootnoteValue = createEditorTransaction(
|
||||||
|
() => props.editor,
|
||||||
|
(ed) => {
|
||||||
|
return (ed && ed.getAttributes('footnote').value) || ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleAddFootnote = (footnote) => {
|
||||||
|
if (footNote()) {
|
||||||
|
props.editor.chain().focus().updateFootnote(footnote).run()
|
||||||
|
} else {
|
||||||
|
props.editor.chain().focus().setFootnote({ value: footnote }).run()
|
||||||
|
}
|
||||||
|
setFootNote()
|
||||||
|
setFootnoteEditorOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenFootnoteEditor = () => {
|
||||||
|
setFootNote(currentFootnoteValue())
|
||||||
|
setFootnoteEditorOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
})
|
})
|
||||||
|
@ -67,12 +95,26 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={props.ref} class={styles.TextBubbleMenu}>
|
<div ref={props.ref} class={clsx(styles.TextBubbleMenu, { [styles.growWidth]: footnoteEditorOpen() })}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={linkEditorOpen()}>
|
<Match when={linkEditorOpen()}>
|
||||||
<InsertLinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} />
|
<InsertLinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!linkEditorOpen()}>
|
<Match when={footnoteEditorOpen()}>
|
||||||
|
<Button size={'S'} onClick={() => showModal('uploadImage')} value={'img'} />
|
||||||
|
<SimplifiedEditor
|
||||||
|
imageEnabled={true}
|
||||||
|
placeholder={t('Enter footnote text')}
|
||||||
|
onSubmit={(value) => handleAddFootnote(value)}
|
||||||
|
variant={'bordered'}
|
||||||
|
initialContent={currentFootnoteValue().value ?? null}
|
||||||
|
onCancel={() => {
|
||||||
|
setFootnoteEditorOpen(false)
|
||||||
|
}}
|
||||||
|
submitButtonText={t('Send')}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={!linkEditorOpen() || !footnoteEditorOpen()}>
|
||||||
<>
|
<>
|
||||||
<Show when={!props.isCommonMarkup}>
|
<Show when={!props.isCommonMarkup}>
|
||||||
<>
|
<>
|
||||||
|
@ -270,7 +312,14 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
|
||||||
<>
|
<>
|
||||||
<Popover content={t('Insert footnote')}>
|
<Popover content={t('Insert footnote')}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
<button ref={triggerRef} type="button" class={styles.bubbleMenuButton}>
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
type="button"
|
||||||
|
class={clsx(styles.bubbleMenuButton, {
|
||||||
|
[styles.bubbleMenuButtonActive]: isFootnote()
|
||||||
|
})}
|
||||||
|
onClick={handleOpenFootnoteEditor}
|
||||||
|
>
|
||||||
<Icon name="editor-footnote" />
|
<Icon name="editor-footnote" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
104
src/components/Editor/extensions/Footnote.ts
Normal file
104
src/components/Editor/extensions/Footnote.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -23,6 +23,10 @@ type FormFields = {
|
||||||
|
|
||||||
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
||||||
|
|
||||||
|
const handleEmailInput = (newEmail: string) => {
|
||||||
|
setEmail(newEmail)
|
||||||
|
}
|
||||||
|
|
||||||
export const RegisterForm = () => {
|
export const RegisterForm = () => {
|
||||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
@ -38,10 +42,6 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||||
|
|
||||||
const handleEmailInput = (newEmail: string) => {
|
|
||||||
setEmail(newEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEmailBlur = () => {
|
const handleEmailBlur = () => {
|
||||||
if (validateEmail(email())) {
|
if (validateEmail(email())) {
|
||||||
checkEmail(email())
|
checkEmail(email())
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 11000;
|
z-index: 10002;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
|
|
|
@ -38,7 +38,8 @@ export const PublishSettings = (props: Props) => {
|
||||||
|
|
||||||
const composeDescription = () => {
|
const composeDescription = () => {
|
||||||
if (!props.form.description) {
|
if (!props.form.description) {
|
||||||
const leadText = props.form.body.replaceAll(/<\/?[^>]+(>|$)/gi, ' ')
|
const cleanFootnotes = props.form.body.replaceAll(/<footnote data-value=".*?">.*?<\/footnote>/g, '')
|
||||||
|
const leadText = cleanFootnotes.replaceAll(/<\/?[^>]+(>|$)/gi, ' ')
|
||||||
return shorten(leadText, MAX_DESCRIPTION_LIMIT).trim()
|
return shorten(leadText, MAX_DESCRIPTION_LIMIT).trim()
|
||||||
}
|
}
|
||||||
return props.form.description
|
return props.form.description
|
||||||
|
|
|
@ -92,15 +92,12 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
|
||||||
|
|
||||||
const [form, setForm] = createStore<ShoutForm>(null)
|
const [form, setForm] = createStore<ShoutForm>(null)
|
||||||
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
|
const [formErrors, setFormErrors] = createStore<Record<keyof ShoutForm, string>>(null)
|
||||||
|
|
||||||
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
|
||||||
characters: 0,
|
characters: 0,
|
||||||
words: 0
|
words: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
|
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
|
||||||
const countWords = (value) => setWordCounter(value)
|
const countWords = (value) => setWordCounter(value)
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
if (!form.title) {
|
if (!form.title) {
|
||||||
setFormErrors('title', t('Required'))
|
setFormErrors('title', t('Required'))
|
||||||
|
|
|
@ -16,8 +16,10 @@ export type ModalType =
|
||||||
| 'donate'
|
| 'donate'
|
||||||
| 'inviteToChat'
|
| 'inviteToChat'
|
||||||
| 'uploadImage'
|
| 'uploadImage'
|
||||||
|
| 'simplifiedEditorUploadImage'
|
||||||
| 'uploadCoverImage'
|
| 'uploadCoverImage'
|
||||||
| 'editorInsertLink'
|
| 'editorInsertLink'
|
||||||
|
| 'simplifiedEditorInsertLink'
|
||||||
|
|
||||||
type WarnKind = 'error' | 'warn' | 'info'
|
type WarnKind = 'error' | 'warn' | 'info'
|
||||||
|
|
||||||
|
@ -36,8 +38,10 @@ export const MODALS: Record<ModalType, ModalType> = {
|
||||||
donate: 'donate',
|
donate: 'donate',
|
||||||
inviteToChat: 'inviteToChat',
|
inviteToChat: 'inviteToChat',
|
||||||
uploadImage: 'uploadImage',
|
uploadImage: 'uploadImage',
|
||||||
|
simplifiedEditorUploadImage: 'simplifiedEditorUploadImage',
|
||||||
uploadCoverImage: 'uploadCoverImage',
|
uploadCoverImage: 'uploadCoverImage',
|
||||||
editorInsertLink: 'editorInsertLink'
|
editorInsertLink: 'editorInsertLink',
|
||||||
|
simplifiedEditorInsertLink: 'simplifiedEditorInsertLink'
|
||||||
}
|
}
|
||||||
|
|
||||||
const [modal, setModal] = createSignal<ModalType | null>(null)
|
const [modal, setModal] = createSignal<ModalType | null>(null)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user