diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 6a29f00b..4e240d91 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -41,6 +41,7 @@ import { ImageBubbleMenu } from './ImageBubbleMenu' import { EditorFloatingMenu } from './EditorFloatingMenu' import { useEditorContext } from '../../context/editor' import { isTextSelection } from '@tiptap/core' +import { Figure } from './extensions/Figure' type EditorProps = { shoutId: number @@ -143,9 +144,9 @@ export const Editor = (props: EditorProps) => { class: 'uploadedImage' } }), + Figure, TrailingNode, Embed, - TrailingNode, CharacterCount, BubbleMenu.configure({ pluginKey: 'textBubbleMenu', @@ -156,7 +157,9 @@ export const Editor = (props: EditorProps) => { const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection) - return !(!view.hasFocus() || empty || isEmptyTextBlock || e.isActive('image')) + return ( + view.hasFocus() && !empty && !isEmptyTextBlock && !e.isActive('image') && !e.isActive('figure') + ) } }), BubbleMenu.configure({ diff --git a/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx b/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx index 07865745..c6156c68 100644 --- a/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx +++ b/src/components/Editor/ImageBubbleMenu/ImageBubbleMenu.tsx @@ -20,7 +20,13 @@ export const ImageBubbleMenu = (props: BubbleMenuProps) => { > -
+ +
diff --git a/src/components/Editor/UploadModal/UploadModalContent.tsx b/src/components/Editor/UploadModal/UploadModalContent.tsx index 52b64753..34c7cc74 100644 --- a/src/components/Editor/UploadModal/UploadModalContent.tsx +++ b/src/components/Editor/UploadModal/UploadModalContent.tsx @@ -29,7 +29,6 @@ export const UploadModalContent = (props: Props) => { props.editor .chain() .focus() - .extendMarkRange('link') .setImage({ src: imageProxy(src) }) .run() hideModal() diff --git a/src/components/Editor/extensions/CustomImage.ts b/src/components/Editor/extensions/CustomImage.ts index b1160d76..fc4486de 100644 --- a/src/components/Editor/extensions/CustomImage.ts +++ b/src/components/Editor/extensions/CustomImage.ts @@ -1,24 +1,17 @@ import Image from '@tiptap/extension-image' -import { mergeAttributes } from '@tiptap/core' declare module '@tiptap/core' { interface Commands { - resizableMedia: { - setFloat: (float: 'none' | 'left' | 'right') => ReturnType + customImage: { + /** + * Add an image + */ + setImage: (options: { src: string; alt?: string; title?: string }) => ReturnType + setFloat: (float: null | 'left' | 'right') => ReturnType } } } -export const updateAttrs = (attrs, editor, node) => { - const { view } = editor - if (!view.editable) return - const { state } = view - const newAttrs = { ...node.attrs, ...attrs } - const { from } = state.selection - const transaction = state.tr.setNodeMarkup(from, null, newAttrs) - view.dispatch(transaction) -} - export default Image.extend({ addAttributes() { return { @@ -39,11 +32,16 @@ export default Image.extend({ } } }, - renderHTML({ HTMLAttributes }) { - return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)] - }, addCommands() { return { + setImage: + (options) => + ({ commands }) => { + return commands.insertContent({ + type: this.name, + attrs: options + }) + }, setFloat: (value) => ({ commands }) => { diff --git a/src/components/Editor/extensions/Figure.ts b/src/components/Editor/extensions/Figure.ts new file mode 100644 index 00000000..78b38de4 --- /dev/null +++ b/src/components/Editor/extensions/Figure.ts @@ -0,0 +1,190 @@ +import { findChildrenInRange, mergeAttributes, Node, nodeInputRule, Tracker } from '@tiptap/core' + +export interface FigureOptions { + HTMLAttributes: Record +} + +declare module '@tiptap/core' { + interface Commands { + figure: { + /** + * 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 inputRegex = /!\[(.+|:?)]\((\S+)(?:\s+["'](\S+)["'])?\)/ + +export const Figure = Node.create({ + name: 'figure', + + addOptions() { + return { + HTMLAttributes: {} + } + }, + + group: 'block', + + content: 'inline*', + + draggable: true, + + isolating: true, + + addAttributes() { + return { + src: { + 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() { + return [ + { + tag: 'figure', + contentElement: 'figcaption' + } + ] + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'figure', + this.options.HTMLAttributes, + ['img', mergeAttributes(HTMLAttributes, { draggable: false, contenteditable: false })], + ['figcaption', 0] + ] + }, + + addCommands() { + return { + setFigure: + ({ caption, ...attrs }) => + ({ chain }) => { + return ( + 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: + () => + ({ tr, commands }) => { + const { doc, selection } = tr + const { from, to } = selection + const images = findChildrenInRange(doc, { from, to }, (node) => node.type.name === 'image') + + if (!images.length) { + return false + } + + const tracker = new Tracker(tr) + + return commands.forEach(images, ({ node, pos }) => { + 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 + } + }) + }) + }, + + figureToImage: + () => + ({ 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) { + return false + } + + const tracker = new Tracker(tr) + + return commands.forEach(figures, ({ node, pos }) => { + 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 } + } + }) + ] + } +}) diff --git a/src/components/Editor/nodes/ImageView/ImageView.module.scss b/src/components/Editor/nodes/ImageView/ImageView.module.scss deleted file mode 100644 index 803df20e..00000000 --- a/src/components/Editor/nodes/ImageView/ImageView.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.ImageView { - border: 1px solid royalblue; -} diff --git a/src/components/Editor/nodes/ImageView/ImageView.tsx b/src/components/Editor/nodes/ImageView/ImageView.tsx deleted file mode 100644 index 4d13d1fc..00000000 --- a/src/components/Editor/nodes/ImageView/ImageView.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import styles from './ImageView.module.scss' -import { ImageDisplay, updateAttrs } from '../../extensions/CustomImage' -import { Editor } from '@tiptap/core' -import { onMount } from 'solid-js' - -type Props = { - editor: Editor -} - -export const ImageView = (props: Props) => { - return
asdads
-} diff --git a/src/components/Editor/nodes/ImageView/index.ts b/src/components/Editor/nodes/ImageView/index.ts deleted file mode 100644 index f856591a..00000000 --- a/src/components/Editor/nodes/ImageView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ImageView } from './ImageView'