parent
a94b8343b4
commit
3b0c3789df
|
@ -3,6 +3,10 @@ import styles from './BubbleMenu.module.scss'
|
||||||
import { Icon } from '../../_shared/Icon'
|
import { Icon } from '../../_shared/Icon'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Popover } from '../../_shared/Popover'
|
import { Popover } from '../../_shared/Popover'
|
||||||
|
import { UploadModalContent } from '../UploadModalContent'
|
||||||
|
import { Modal } from '../../Nav/Modal'
|
||||||
|
import { UploadedFile } from '../../../pages/types'
|
||||||
|
import { renderUploadedImage } from '../../../utils/renderUploadedImage'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
|
@ -11,6 +15,11 @@ type Props = {
|
||||||
|
|
||||||
export const FigureBubbleMenu = (props: Props) => {
|
export const FigureBubbleMenu = (props: Props) => {
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
|
const handleUpload = (image: UploadedFile) => {
|
||||||
|
renderUploadedImage(props.editor, image)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={props.ref} class={styles.BubbleMenu}>
|
<div ref={props.ref} class={styles.BubbleMenu}>
|
||||||
<Popover content={t('Alignment left')}>
|
<Popover content={t('Alignment left')}>
|
||||||
|
@ -19,7 +28,7 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={styles.bubbleMenuButton}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setImageFloat('left').run()}
|
onClick={() => props.editor.chain().focus().setFigcaptionFocus(true).run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-left" />
|
<Icon name="editor-image-align-left" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -31,7 +40,7 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={styles.bubbleMenuButton}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setImageFloat(null).run()}
|
onClick={() => props.editor.chain().focus().setFigureFloat(null).run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-center" />
|
<Icon name="editor-image-align-center" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -43,7 +52,7 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
ref={triggerRef}
|
ref={triggerRef}
|
||||||
type="button"
|
type="button"
|
||||||
class={styles.bubbleMenuButton}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => props.editor.chain().focus().setImageFloat('right').run()}
|
onClick={() => props.editor.chain().focus().setFigureFloat('right').run()}
|
||||||
>
|
>
|
||||||
<Icon name="editor-image-align-right" />
|
<Icon name="editor-image-align-right" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -54,7 +63,7 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
type="button"
|
type="button"
|
||||||
class={styles.bubbleMenuButton}
|
class={styles.bubbleMenuButton}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.editor.chain().focus().imageToFigure().run()
|
props.editor.chain().focus().setFigcaptionFocus(true).run()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ color: 'white' }}>{t('Add signature')}</span>
|
<span style={{ color: 'white' }}>{t('Add signature')}</span>
|
||||||
|
@ -67,6 +76,14 @@ export const FigureBubbleMenu = (props: Props) => {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
<Modal variant="narrow" name="uploadImage">
|
||||||
|
<UploadModalContent
|
||||||
|
onClose={(value) => {
|
||||||
|
handleUpload(value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,18 +29,15 @@ import { Paragraph } from '@tiptap/extension-paragraph'
|
||||||
import Focus from '@tiptap/extension-focus'
|
import Focus from '@tiptap/extension-focus'
|
||||||
import { Collaboration } from '@tiptap/extension-collaboration'
|
import { Collaboration } from '@tiptap/extension-collaboration'
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider'
|
import { HocuspocusProvider } from '@hocuspocus/provider'
|
||||||
|
|
||||||
import { CustomImage } from './extensions/CustomImage'
|
|
||||||
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
||||||
import { Figure } from './extensions/Figure'
|
import { Figure } from './extensions/Figure'
|
||||||
|
import { Figcaption } from './extensions/Figcaption'
|
||||||
import { Embed } from './extensions/Embed'
|
import { Embed } from './extensions/Embed'
|
||||||
|
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { useEditorContext } from '../../context/editor'
|
import { useEditorContext } from '../../context/editor'
|
||||||
import { TrailingNode } from './extensions/TrailingNode'
|
import { TrailingNode } from './extensions/TrailingNode'
|
||||||
import Article from './extensions/Article'
|
import Article from './extensions/Article'
|
||||||
|
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
||||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
|
@ -49,6 +46,7 @@ import { TableOfContents } from '../TableOfContents'
|
||||||
import { isDesktop } from '../../utils/media-query'
|
import { isDesktop } from '../../utils/media-query'
|
||||||
|
|
||||||
import './Prosemirror.scss'
|
import './Prosemirror.scss'
|
||||||
|
import { Image } from '@tiptap/extension-image'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -112,6 +110,12 @@ export const Editor = (props: Props) => {
|
||||||
} = {
|
} = {
|
||||||
current: null
|
current: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ImageFigure = Figure.extend({
|
||||||
|
name: 'capturedImage',
|
||||||
|
content: 'figcaption image'
|
||||||
|
})
|
||||||
|
|
||||||
const { initialContent } = props
|
const { initialContent } = props
|
||||||
const editor = createTiptapEditor(() => ({
|
const editor = createTiptapEditor(() => ({
|
||||||
element: editorElRef.current,
|
element: editorElRef.current,
|
||||||
|
@ -166,12 +170,9 @@ export const Editor = (props: Props) => {
|
||||||
class: 'highlight'
|
class: 'highlight'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
CustomImage.configure({
|
ImageFigure,
|
||||||
HTMLAttributes: {
|
Image,
|
||||||
class: 'uploadedImage'
|
Figcaption,
|
||||||
}
|
|
||||||
}),
|
|
||||||
Figure,
|
|
||||||
Embed,
|
Embed,
|
||||||
CharacterCount,
|
CharacterCount,
|
||||||
BubbleMenu.configure({
|
BubbleMenu.configure({
|
||||||
|
@ -181,8 +182,7 @@ export const Editor = (props: Props) => {
|
||||||
const { doc, selection } = state
|
const { doc, selection } = state
|
||||||
const { empty } = selection
|
const { empty } = selection
|
||||||
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
|
||||||
|
setIsCommonMarkup(e.isActive('figcaption'))
|
||||||
setIsCommonMarkup(e.isActive('figure'))
|
|
||||||
return view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image')
|
return view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image')
|
||||||
},
|
},
|
||||||
tippyOptions: {
|
tippyOptions: {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { hideModal, showModal } from '../../../stores/ui'
|
||||||
import { UploadModalContent } from '../UploadModalContent'
|
import { UploadModalContent } from '../UploadModalContent'
|
||||||
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
|
||||||
import { imageProxy } from '../../../utils/imageProxy'
|
import { imageProxy } from '../../../utils/imageProxy'
|
||||||
|
import { UploadedFile } from '../../../pages/types'
|
||||||
|
import { renderUploadedImage } from '../../../utils/renderUploadedImage'
|
||||||
|
|
||||||
type FloatingMenuProps = {
|
type FloatingMenuProps = {
|
||||||
editor: Editor
|
editor: Editor
|
||||||
|
@ -76,13 +78,8 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const renderImage = (src: string) => {
|
const handleUpload = (image: UploadedFile) => {
|
||||||
props.editor
|
renderUploadedImage(props.editor, image)
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.setImage({ src: imageProxy(src) })
|
|
||||||
.run()
|
|
||||||
hideModal()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -116,7 +113,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
||||||
<Modal variant="narrow" name="uploadImage" onClose={closeUploadModalHandler}>
|
<Modal variant="narrow" name="uploadImage" onClose={closeUploadModalHandler}>
|
||||||
<UploadModalContent
|
<UploadModalContent
|
||||||
onClose={(value) => {
|
onClose={(value) => {
|
||||||
renderImage(value)
|
handleUpload(value)
|
||||||
setSelectedMenuItem()
|
setSelectedMenuItem()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -37,7 +37,6 @@
|
||||||
|
|
||||||
.articleEditor blockquote,
|
.articleEditor blockquote,
|
||||||
.articleEditor figure,
|
.articleEditor figure,
|
||||||
.articleEditor .uploadedImage,
|
|
||||||
.articleEditor article[data-type='incut'] {
|
.articleEditor article[data-type='incut'] {
|
||||||
@media (width >= 768px) {
|
@media (width >= 768px) {
|
||||||
margin-left: calc(21.9% + 3px) !important;
|
margin-left: calc(21.9% + 3px) !important;
|
||||||
|
@ -124,13 +123,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadedImage {
|
|
||||||
max-height: 80vh;
|
|
||||||
margin: auto;
|
|
||||||
display: block;
|
|
||||||
width: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.horizontalRule {
|
.horizontalRule {
|
||||||
border-top: 2px solid #000;
|
border-top: 2px solid #000;
|
||||||
}
|
}
|
||||||
|
@ -330,3 +322,7 @@ mark.highlight {
|
||||||
box-shadow: 0 0 0 1px #000;
|
box-shadow: 0 0 0 1px #000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
figure[data-type='capturedImage'] {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
|
@ -43,12 +43,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadedImage {
|
|
||||||
max-height: 60vh;
|
|
||||||
margin: auto;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { Italic } from '@tiptap/extension-italic'
|
||||||
import { Modal } from '../Nav/Modal'
|
import { Modal } from '../Nav/Modal'
|
||||||
import { hideModal, showModal } from '../../stores/ui'
|
import { hideModal, showModal } from '../../stores/ui'
|
||||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||||
import { CustomImage } from './extensions/CustomImage'
|
|
||||||
import { UploadModalContent } from './UploadModalContent'
|
import { UploadModalContent } from './UploadModalContent'
|
||||||
import { imageProxy } from '../../utils/imageProxy'
|
import { imageProxy } from '../../utils/imageProxy'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -27,6 +26,8 @@ import styles from './SimplifiedEditor.module.scss'
|
||||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||||
import { InsertLinkForm } from './InsertLinkForm'
|
import { InsertLinkForm } from './InsertLinkForm'
|
||||||
import { Link } from '@tiptap/extension-link'
|
import { Link } from '@tiptap/extension-link'
|
||||||
|
import { UploadedFile } from '../../pages/types'
|
||||||
|
import { Figure } from './extensions/Figure'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialContent?: string
|
initialContent?: string
|
||||||
|
@ -54,6 +55,11 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
actions: { setEditor }
|
actions: { setEditor }
|
||||||
} = useEditorContext()
|
} = useEditorContext()
|
||||||
|
|
||||||
|
const ImageFigure = Figure.extend({
|
||||||
|
name: 'capturedImage',
|
||||||
|
content: 'figcaption image'
|
||||||
|
})
|
||||||
|
|
||||||
const editor = createTiptapEditor(() => ({
|
const editor = createTiptapEditor(() => ({
|
||||||
element: editorElRef.current,
|
element: editorElRef.current,
|
||||||
editorProps: {
|
editorProps: {
|
||||||
|
@ -75,11 +81,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
class: styles.blockQuote
|
class: styles.blockQuote
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
CustomImage.configure({
|
ImageFigure,
|
||||||
HTMLAttributes: {
|
|
||||||
class: styles.uploadedImage
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
emptyNodeClass: styles.emptyNode,
|
emptyNodeClass: styles.emptyNode,
|
||||||
placeholder: props.placeholder
|
placeholder: props.placeholder
|
||||||
|
@ -106,11 +108,30 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
const isLink = isActive('link')
|
const isLink = isActive('link')
|
||||||
const isBlockquote = isActive('blockquote')
|
const isBlockquote = isActive('blockquote')
|
||||||
|
|
||||||
const renderImage = (src: string) => {
|
const renderImage = (image: UploadedFile) => {
|
||||||
editor()
|
editor()
|
||||||
.chain()
|
.chain()
|
||||||
.focus()
|
.focus()
|
||||||
.setImage({ src: imageProxy(src) })
|
.insertContent({
|
||||||
|
type: 'capturedImage',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'figcaption',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: image.originalFilename
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
attrs: {
|
||||||
|
src: imageProxy(image.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
.run()
|
.run()
|
||||||
hideModal()
|
hideModal()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,10 @@ import { handleFileUpload } from '../../../utils/handleFileUpload'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { Loading } from '../../_shared/Loading'
|
import { Loading } from '../../_shared/Loading'
|
||||||
import { verifyImg } from '../../../utils/verifyImg'
|
import { verifyImg } from '../../../utils/verifyImg'
|
||||||
|
import { UploadedFile } from '../../../pages/types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: (imgUrl?: string) => void
|
onClose: (image?: UploadedFile) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UploadModalContent = (props: Props) => {
|
export const UploadModalContent = (props: Props) => {
|
||||||
|
@ -27,7 +28,7 @@ export const UploadModalContent = (props: Props) => {
|
||||||
try {
|
try {
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const result = await handleFileUpload(file)
|
const result = await handleFileUpload(file)
|
||||||
props.onClose(result.url)
|
props.onClose(result)
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
|
|
45
src/components/Editor/extensions/Figcaption.ts
Normal file
45
src/components/Editor/extensions/Figcaption.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
Figcaption: {
|
||||||
|
setFigcaptionFocus: (value: boolean) => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const Figcaption = Node.create({
|
||||||
|
name: 'figcaption',
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
HTMLAttributes: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
content: 'inline*',
|
||||||
|
|
||||||
|
selectable: false,
|
||||||
|
|
||||||
|
draggable: false,
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'figcaption'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['figcaption', mergeAttributes(HTMLAttributes), 0]
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setFigcaptionFocus:
|
||||||
|
(value) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.focus(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
|
@ -1,205 +1,73 @@
|
||||||
import { findChildrenInRange, mergeAttributes, Node, nodeInputRule, Tracker } from '@tiptap/core'
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
|
import { Plugin } from '@tiptap/pm/state'
|
||||||
export interface FigureOptions {
|
|
||||||
HTMLAttributes: Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
figure: {
|
Figure: {
|
||||||
/**
|
setFigureFloat: (float: null | 'left' | 'right') => ReturnType
|
||||||
* Add a figure element
|
|
||||||
*/
|
|
||||||
setFigure: (options: { src: string; alt?: string; title?: string; caption?: string }) => ReturnType
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an image to a figure
|
|
||||||
*/
|
|
||||||
imageToFigure: () => ReturnType
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a figure to an image
|
|
||||||
*/
|
|
||||||
figureToImage: () => ReturnType
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export const Figure = Node.create({
|
||||||
export const inputRegex = /!\[(.+|:?)]\((\S+)(?:\s+["'](\S+)["'])?\)/
|
|
||||||
|
|
||||||
export const Figure = Node.create<FigureOptions>({
|
|
||||||
name: 'figure',
|
name: 'figure',
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
HTMLAttributes: {}
|
HTMLAttributes: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
group: 'block',
|
group: 'block',
|
||||||
|
content: 'block figcaption',
|
||||||
content: 'inline*',
|
|
||||||
|
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
|
||||||
isolating: true,
|
isolating: true,
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
src: {
|
'data-float': null
|
||||||
default: null,
|
|
||||||
parseHTML: (element) => element.querySelector('img')?.getAttribute('src')
|
|
||||||
},
|
|
||||||
|
|
||||||
alt: {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (element) => element.querySelector('img')?.getAttribute('alt')
|
|
||||||
},
|
|
||||||
|
|
||||||
title: {
|
|
||||||
default: null,
|
|
||||||
parseHTML: (element) => element.querySelector('img')?.getAttribute('title')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: 'figure',
|
tag: `figure[data-type="${this.name}"]`
|
||||||
contentElement: (dom: HTMLElement) =>
|
|
||||||
dom.querySelector('figcaption') ?? document.createElement('figcaption')
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['figure', mergeAttributes(HTMLAttributes, { 'data-type': this.name }), 0]
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
return [
|
return [
|
||||||
'figure',
|
new Plugin({
|
||||||
this.options.HTMLAttributes,
|
props: {
|
||||||
['img', mergeAttributes(HTMLAttributes, { draggable: false, contenteditable: false })],
|
handleDOMEvents: {
|
||||||
['figcaption', 0]
|
// prevent dragging nodes out of the figure
|
||||||
|
dragstart: (view, event) => {
|
||||||
|
if (!event.target) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const pos = view.posAtDOM(event.target as HTMLElement, 0)
|
||||||
|
const $pos = view.state.doc.resolve(pos)
|
||||||
|
if ($pos.parent.type === this.type) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
addCommands() {
|
addCommands() {
|
||||||
return {
|
return {
|
||||||
setFigure:
|
setFigureFloat:
|
||||||
({ caption, ...attrs }) =>
|
(value) =>
|
||||||
({ chain }) => {
|
({ commands }) => {
|
||||||
return (
|
return commands.updateAttributes(this.name, { 'data-float': value })
|
||||||
chain()
|
|
||||||
.insertContent({
|
|
||||||
type: this.name,
|
|
||||||
attrs,
|
|
||||||
content: caption ? [{ type: 'text', text: caption }] : []
|
|
||||||
})
|
|
||||||
// set cursor at end of caption field
|
|
||||||
.command(({ tr, commands }) => {
|
|
||||||
const { doc, selection } = tr
|
|
||||||
const position = doc.resolve(selection.to - 2).end()
|
|
||||||
|
|
||||||
return commands.setTextSelection(position)
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
imageToFigure:
|
|
||||||
() =>
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
||||||
({ tr, commands }) => {
|
|
||||||
const { doc, selection } = tr
|
|
||||||
const { from, to } = selection
|
|
||||||
const images = findChildrenInRange(doc, { from, to }, (node) => node.type.name === 'image')
|
|
||||||
|
|
||||||
if (images.length === 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const tracker = new Tracker(tr)
|
|
||||||
|
|
||||||
return commands.forEach(
|
|
||||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
||||||
images,
|
|
||||||
// eslint-disable-next-line unicorn/no-array-method-this-argument
|
|
||||||
({ node, pos }) => {
|
|
||||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
||||||
const mapResult = tracker.map(pos)
|
|
||||||
|
|
||||||
if (mapResult.deleted) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = {
|
|
||||||
from: mapResult.position,
|
|
||||||
to: mapResult.position + node.nodeSize
|
|
||||||
}
|
|
||||||
|
|
||||||
return commands.insertContentAt(range, {
|
|
||||||
type: this.name,
|
|
||||||
attrs: {
|
|
||||||
src: node.attrs.src
|
|
||||||
},
|
|
||||||
content: [{ type: 'text', text: node.attrs.src }]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
figureToImage:
|
|
||||||
() =>
|
|
||||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
||||||
({ tr, commands }) => {
|
|
||||||
const { doc, selection } = tr
|
|
||||||
const { from, to } = selection
|
|
||||||
const figures = findChildrenInRange(doc, { from, to }, (node) => node.type.name === this.name)
|
|
||||||
|
|
||||||
if (figures.length === 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const tracker = new Tracker(tr)
|
|
||||||
|
|
||||||
return commands.forEach(
|
|
||||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
||||||
figures,
|
|
||||||
// eslint-disable-next-line unicorn/no-array-method-this-argument
|
|
||||||
({ node, pos }) => {
|
|
||||||
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
||||||
const mapResult = tracker.map(pos)
|
|
||||||
|
|
||||||
if (mapResult.deleted) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = {
|
|
||||||
from: mapResult.position,
|
|
||||||
to: mapResult.position + node.nodeSize
|
|
||||||
}
|
|
||||||
|
|
||||||
return commands.insertContentAt(range, {
|
|
||||||
type: 'image',
|
|
||||||
attrs: {
|
|
||||||
src: node.attrs.src
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
addInputRules() {
|
|
||||||
return [
|
|
||||||
nodeInputRule({
|
|
||||||
find: inputRegex,
|
|
||||||
type: this.type,
|
|
||||||
getAttributes: (match) => {
|
|
||||||
const [, src, alt, title] = match
|
|
||||||
|
|
||||||
return { src, alt, title }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { AudioUploader } from '../Editor/AudioUploader'
|
||||||
import { slugify } from '../../utils/slugify'
|
import { slugify } from '../../utils/slugify'
|
||||||
import { SolidSwiper } from '../_shared/SolidSwiper'
|
import { SolidSwiper } from '../_shared/SolidSwiper'
|
||||||
import { DropArea } from '../_shared/DropArea'
|
import { DropArea } from '../_shared/DropArea'
|
||||||
import { LayoutType, MediaItem } from '../../pages/types'
|
import { LayoutType, MediaItem, UploadedFile } from '../../pages/types'
|
||||||
import { clone } from '../../utils/clone'
|
import { clone } from '../../utils/clone'
|
||||||
import deepEqual from 'fast-deep-equal'
|
import deepEqual from 'fast-deep-equal'
|
||||||
import { AutoSaveNotice } from '../Editor/AutoSaveNotice'
|
import { AutoSaveNotice } from '../Editor/AutoSaveNotice'
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { redirectPage } from '@nanostores/router'
|
||||||
import { router } from '../../../stores/router'
|
import { router } from '../../../stores/router'
|
||||||
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
import { UploadedFile } from '../../../pages/types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -60,9 +61,9 @@ export const PublishSettings = (props: Props) => {
|
||||||
const [settingsForm, setSettingsForm] = createStore(initialData)
|
const [settingsForm, setSettingsForm] = createStore(initialData)
|
||||||
const [topics, setTopics] = createSignal<Topic[]>(null)
|
const [topics, setTopics] = createSignal<Topic[]>(null)
|
||||||
|
|
||||||
const handleUploadModalContentCloseSetCover = (imgUrl: string) => {
|
const handleUploadModalContentCloseSetCover = (image: UploadedFile) => {
|
||||||
hideModal()
|
hideModal()
|
||||||
setSettingsForm('coverImageUrl', imgUrl)
|
setSettingsForm('coverImageUrl', image.url)
|
||||||
}
|
}
|
||||||
const handleDeleteCoverImage = () => {
|
const handleDeleteCoverImage = () => {
|
||||||
setSettingsForm('coverImageUrl', '')
|
setSettingsForm('coverImageUrl', '')
|
||||||
|
|
32
src/utils/renderUploadedImage.ts
Normal file
32
src/utils/renderUploadedImage.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { UploadedFile } from '../pages/types'
|
||||||
|
import { imageProxy } from './imageProxy'
|
||||||
|
import { hideModal } from '../stores/ui'
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
|
||||||
|
export const renderUploadedImage = (editor: Editor, image: UploadedFile) => {
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.insertContent({
|
||||||
|
type: 'capturedImage',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'figcaption',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: image.originalFilename
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image',
|
||||||
|
attrs: {
|
||||||
|
src: imageProxy(image.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
hideModal()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user