From 0b5f0b430a24fcc0c078a3b6ae3598bce700c4a2 Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sat, 3 Aug 2024 17:56:59 +0700 Subject: [PATCH 1/2] feat(caption): add figcaptionKit extension --- package.json | 2 +- src/editors/extensions/figcaptionKit.ts | 138 ++++++++++++++++++++++++ src/editors/extensions/figureAudio.ts | 54 +--------- src/editors/extensions/figureEmbed.ts | 52 +-------- src/editors/extensions/figureImage.ts | 53 +-------- src/editors/extensions/index.ts | 3 + src/editors/extensions/pasteDropFile.ts | 2 +- 7 files changed, 151 insertions(+), 153 deletions(-) create mode 100644 src/editors/extensions/figcaptionKit.ts diff --git a/package.json b/package.json index 3fbd522..e426c05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@matters/matters-editor", - "version": "0.3.0-alpha.0", + "version": "0.3.0-alpha.1", "description": "Editor for matters.news", "author": "https://github.com/thematters", "homepage": "https://github.com/thematters/matters-editor", diff --git a/src/editors/extensions/figcaptionKit.ts b/src/editors/extensions/figcaptionKit.ts new file mode 100644 index 0000000..78d8ad5 --- /dev/null +++ b/src/editors/extensions/figcaptionKit.ts @@ -0,0 +1,138 @@ +import { type Editor } from '@tiptap/core' +import { Node } from '@tiptap/core' +import { Plugin, PluginKey } from '@tiptap/pm/state' + +/** + * FigcaptionKit extension works with FigureAudio, + * FigureEmbed and FigureImage extensions to: + * - limit figcaption length + * - handle enter key event to insert a new paragraph + * - handle backspace key event to remove the figcaption if it's empty + * - handle click event to select the figcaption + * + * @see {https://github.com/ueberdosis/tiptap/issues/629} + */ + +type FigcaptionKitOptions = { + maxCaptionLength?: number +} + +const pluginName = 'figcaptionKit' + +export const makeFigcaptionEventHandlerPlugin = ({ + editor, +}: { + editor: Editor +}) => { + return new Plugin({ + key: new PluginKey('figcaptionEventHandler'), + props: { + handleClickOn(view, pos, node, nodePos, event) { + const isFigcaption = + event.target instanceof HTMLElement + ? event.target.tagName.toUpperCase() === 'FIGCAPTION' + : false + + if (!isFigcaption) return + + // set the selection to the figcaption node + editor.commands.setTextSelection(pos) + + // to prevent the default behavior which is to select the whole node + // @see {@url https://discuss.prosemirror.net/t/prevent-nodeview-selection-on-click/3193} + return true + }, + handleKeyDown(view, event) { + const isBackSpace = event.key.toLowerCase() === 'backspace' + const isEnter = event.key.toLowerCase() === 'enter' + + if (!isBackSpace && !isEnter) { + return + } + + const anchorParent = view.state.selection.$anchor.parent + const isCurrentPlugin = anchorParent.type.name === pluginName + const isEmptyFigcaption = anchorParent.content.size <= 0 + + if (!isCurrentPlugin) { + return + } + + // backSpace to remove if the figcaption is empty + if (isBackSpace && isEmptyFigcaption) { + // FIXME: setTimeOut to avoid repetitive deletion + setTimeout(() => { + editor.commands.deleteNode(pluginName) + }) + return + } + + // insert a new paragraph + if (isEnter) { + const { $from, $to } = editor.state.selection + const isTextAfter = $to.nodeAfter?.type?.name === 'text' + + // skip if figcaption text is selected + // or has text after current selection + if ($from !== $to || isTextAfter) { + return + } + + // FIXME: setTimeOut to avoid repetitive paragraph insertion + setTimeout(() => { + editor.commands.insertContentAt($to.pos + 1, { + type: 'paragraph', + }) + }) + } + }, + }, + }) +} + +export const FigcaptionKit = Node.create({ + name: pluginName, + + addOptions() { + return { + maxCaptionLength: undefined, + } + }, + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey('figcaptionLimit'), + filterTransaction: (transaction) => { + // Nothing has changed, ignore it. + if (!transaction.docChanged || !this.options.maxCaptionLength) { + return true + } + + try { + // skip if not in a figure + const anchorParent = transaction.selection.$anchor.parent + const isFigure = anchorParent.type.name.includes('figure') + if (!isFigure) { + return true + } + + // limit figcaption length + if (anchorParent.content.size <= 0) return true + + const figcaptionText = anchorParent.content.child(0).text || '' + if (figcaptionText.length > this.options.maxCaptionLength) { + return false + } + } catch (e) { + console.error(e) + } + + return true + }, + }), + + makeFigcaptionEventHandlerPlugin({ editor: this.editor }), + ] + }, +}) diff --git a/src/editors/extensions/figureAudio.ts b/src/editors/extensions/figureAudio.ts index ae37263..deb73b9 100644 --- a/src/editors/extensions/figureAudio.ts +++ b/src/editors/extensions/figureAudio.ts @@ -1,4 +1,4 @@ -import { type Editor, Node } from '@tiptap/core' +import { Node } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' /** @@ -52,6 +52,7 @@ export const FigureAudio = Node.create({ content: 'text*', draggable: true, isolating: true, + atom: true, // disallows all marks for figcaption marks: '', @@ -153,58 +154,9 @@ export const FigureAudio = Node.create({ addProseMirrorPlugins() { return [ new Plugin({ - key: new PluginKey('removePastedFigureAudio'), + key: new PluginKey('removePastedFigureImage'), props: { - handleKeyDown(view, event) { - const isBackSpace = event.key.toLowerCase() === 'backspace' - const isEnter = event.key.toLowerCase() === 'enter' - - if (!isBackSpace && !isEnter) { - return - } - - const anchorParent = view.state.selection.$anchor.parent - const isCurrentPlugin = anchorParent.type.name === pluginName - const isEmptyFigcaption = anchorParent.content.size <= 0 - - if (!isCurrentPlugin) { - return - } - - // @ts-expect-error - const editor = view.dom.editor as Editor - - // backSpace to remove if the figcaption is empty - if (isBackSpace && isEmptyFigcaption) { - // FIXME: setTimeOut to avoid repetitive deletion - setTimeout(() => { - editor.commands.deleteNode(pluginName) - }) - return - } - - // insert a new paragraph - if (isEnter) { - const { $from, $to } = editor.state.selection - const isTextAfter = $to.nodeAfter?.type?.name === 'text' - - // skip if figcaption text is selected - // or has text after current selection - if ($from !== $to || isTextAfter) { - return - } - - // FIXME: setTimeOut to avoid repetitive paragraph insertion - setTimeout(() => { - editor.commands.insertContentAt($to.pos + 1, { - type: 'paragraph', - }) - }) - } - }, - transformPastedHTML(html) { - // remove html = html .replace(/\n/g, '') .replace(//g, '') diff --git a/src/editors/extensions/figureEmbed.ts b/src/editors/extensions/figureEmbed.ts index 89809ed..166729d 100644 --- a/src/editors/extensions/figureEmbed.ts +++ b/src/editors/extensions/figureEmbed.ts @@ -1,4 +1,4 @@ -import { type Editor, Node } from '@tiptap/core' +import { Node } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' /** @@ -255,6 +255,7 @@ export const FigureEmbed = Node.create({ content: 'text*', draggable: true, isolating: true, + atom: true, // disallows all marks for figcaption marks: '', @@ -359,56 +360,7 @@ export const FigureEmbed = Node.create({ new Plugin({ key: new PluginKey('removePastedFigureEmbed'), props: { - handleKeyDown(view, event) { - const isBackSpace = event.key.toLowerCase() === 'backspace' - const isEnter = event.key.toLowerCase() === 'enter' - - if (!isBackSpace && !isEnter) { - return - } - - const anchorParent = view.state.selection.$anchor.parent - const isCurrentPlugin = anchorParent.type.name === pluginName - const isEmptyFigcaption = anchorParent.content.size <= 0 - - if (!isCurrentPlugin) { - return - } - - // @ts-expect-error - const editor = view.dom.editor as Editor - - // backSpace to remove if the figcaption is empty - if (isBackSpace && isEmptyFigcaption) { - // FIXME: setTimeOut to avoid repetitive deletion - setTimeout(() => { - editor.commands.deleteNode(pluginName) - }) - return - } - - // insert a new paragraph - if (isEnter) { - const { $from, $to } = editor.state.selection - const isTextAfter = $to.nodeAfter?.type?.name === 'text' - - // skip if figcaption text is selected - // or has text after current selection - if ($from !== $to || isTextAfter) { - return - } - - // FIXME: setTimeOut to avoid repetitive paragraph insertion - setTimeout(() => { - editor.commands.insertContentAt($to.pos + 1, { - type: 'paragraph', - }) - }) - } - }, - transformPastedHTML(html) { - // remove html = html .replace(/\n/g, '') .replace(//g, '') diff --git a/src/editors/extensions/figureImage.ts b/src/editors/extensions/figureImage.ts index 8b32f00..0e3f164 100644 --- a/src/editors/extensions/figureImage.ts +++ b/src/editors/extensions/figureImage.ts @@ -1,4 +1,4 @@ -import { type Editor, Node } from '@tiptap/core' +import { Node } from '@tiptap/core' import { Plugin, PluginKey } from '@tiptap/pm/state' /** @@ -34,6 +34,8 @@ export const FigureImage = Node.create({ content: 'text*', draggable: true, isolating: true, + selectable: true, + atom: true, // disallows all marks for figcaption marks: '', @@ -107,56 +109,7 @@ export const FigureImage = Node.create({ new Plugin({ key: new PluginKey('removePastedFigureImage'), props: { - handleKeyDown(view, event) { - const isBackSpace = event.key.toLowerCase() === 'backspace' - const isEnter = event.key.toLowerCase() === 'enter' - - if (!isBackSpace && !isEnter) { - return - } - - const anchorParent = view.state.selection.$anchor.parent - const isCurrentPlugin = anchorParent.type.name === pluginName - const isEmptyFigcaption = anchorParent.content.size <= 0 - - if (!isCurrentPlugin) { - return - } - - // @ts-expect-error - const editor = view.dom.editor as Editor - - // backSpace to remove if the figcaption is empty - if (isBackSpace && isEmptyFigcaption) { - // FIXME: setTimeOut to avoid repetitive deletion - setTimeout(() => { - editor.commands.deleteNode(pluginName) - }) - return - } - - // insert a new paragraph - if (isEnter) { - const { $from, $to } = editor.state.selection - const isTextAfter = $to.nodeAfter?.type?.name === 'text' - - // skip if figcaption text is selected - // or has text after current selection - if ($from !== $to || isTextAfter) { - return - } - - // FIXME: setTimeOut to avoid repetitive paragraph insertion - setTimeout(() => { - editor.commands.insertContentAt($to.pos + 1, { - type: 'paragraph', - }) - }) - } - }, - transformPastedHTML(html) { - // remove html = html .replace(/\n/g, '') .replace(//g, '') diff --git a/src/editors/extensions/index.ts b/src/editors/extensions/index.ts index 853f8b0..af025d5 100644 --- a/src/editors/extensions/index.ts +++ b/src/editors/extensions/index.ts @@ -1,5 +1,6 @@ export * from './blockquote' export * from './bold' +export * from './figcaptionKit' export * from './figureAudio' export * from './figureEmbed' export * from './figureImage' @@ -26,6 +27,7 @@ import Text from '@tiptap/extension-text' import { Blockquote } from './blockquote' import { Bold } from './bold' +import { FigcaptionKit } from './figcaptionKit' import { FigureAudio } from './figureAudio' import { FigureEmbed } from './figureEmbed' import { FigureImage } from './figureImage' @@ -65,6 +67,7 @@ export const articleEditorExtensions = [ FigureImage, FigureAudio, FigureEmbed, + FigcaptionKit, ] export const commentEditorExtensions = [...baseEditorExtensions] diff --git a/src/editors/extensions/pasteDropFile.ts b/src/editors/extensions/pasteDropFile.ts index 19819df..9a9a8ef 100644 --- a/src/editors/extensions/pasteDropFile.ts +++ b/src/editors/extensions/pasteDropFile.ts @@ -63,6 +63,6 @@ export const PasteDropFile = Node.create({ name: pluginName, addProseMirrorPlugins() { - return [makePlugin({ ...this.options, editor: this.editor as Editor })] + return [makePlugin({ ...this.options, editor: this.editor })] }, }) From 245bd067a89d62ea67fbd5e3cbc289820e7d05bf Mon Sep 17 00:00:00 2001 From: robertu <4065233+robertu7@users.noreply.github.com> Date: Sun, 4 Aug 2024 09:45:30 +0700 Subject: [PATCH 2/2] fix: typo --- src/editors/extensions/figureAudio.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editors/extensions/figureAudio.ts b/src/editors/extensions/figureAudio.ts index deb73b9..813b05c 100644 --- a/src/editors/extensions/figureAudio.ts +++ b/src/editors/extensions/figureAudio.ts @@ -154,7 +154,7 @@ export const FigureAudio = Node.create({ addProseMirrorPlugins() { return [ new Plugin({ - key: new PluginKey('removePastedFigureImage'), + key: new PluginKey('removePastedFigureAudio'), props: { transformPastedHTML(html) { html = html