diff --git a/package.json b/package.json index 9b9aa61..44b344e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/prosemirror-schema", - "version": "1.1.0-next", + "version": "1.1.1-next", "description": "Schema setup for using prosemirror in chatwoot. Based on 👉 https://github.com/ProseMirror/prosemirror-example-setup/", "main": "dist/index.es.js", "scripts": { diff --git a/src/mentions/plugin.js b/src/mentions/plugin.js index ab72630..443aeaa 100644 --- a/src/mentions/plugin.js +++ b/src/mentions/plugin.js @@ -6,29 +6,46 @@ import { Plugin, PluginKey } from 'prosemirror-state'; import { Decoration, DecorationSet } from 'prosemirror-view'; -export const triggerCharacters = char => $position => { - const regexp = new RegExp(`(?:^)?${char}[^\\s${char}]*`, 'g'); +/** + * Creates a function to detect if the trigger character followed by a specified number of characters + * has been typed, starting from a new word, after a space, or after a newline. + * @param {string} char - The trigger character to detect. + * @param {number} [minChars=0] - The minimum number of characters that should follow the trigger character. + * @returns {Function} A function that takes a position object and returns the match if the condition is met. + */ +export const triggerCharacters = (char, minChars = 0) => $position => { + // Regular expression to find occurrences of 'char' followed by at least 'minChars' non-space characters. + // It matches these sequences starting from the beginning of the text or after a space. + const regexp = new RegExp(`(?:^)?${char}[^\\s${char}]{${minChars},}`, 'g'); + // Get the position before the current cursor position in the document. const textFrom = $position.before(); + // Get the position at the end of the current node. const textTo = $position.end(); + // Get the text between the start of the node and the cursor position. const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0'); let match; // eslint-disable-next-line while ((match = regexp.exec(text))) { + // Check if the character before the match is a space, start of string, or null character const prefix = match.input.slice(Math.max(0, match.index - 1), match.index); if (!/^[\s\0]?$/.test(prefix)) { + // If the prefix is not empty, space, or null, skip this match // eslint-disable-next-line continue; } const from = match.index + $position.start(); - let to = from + match[0].length; + const to = from + match[0].length; if (from < $position.pos && to >= $position.pos) { - return { range: { from, to }, text: match[0] }; + const fullMatch = match[0]; + // Remove trigger char and trim + const trimmedText = fullMatch ? fullMatch.slice(char.length) : ''; + return { range: { from, to }, text: trimmedText }; } } return null; diff --git a/src/plugins/image.js b/src/plugins/image.js new file mode 100644 index 0000000..ea93254 --- /dev/null +++ b/src/plugins/image.js @@ -0,0 +1,58 @@ +import { Plugin } from "prosemirror-state"; + +/** + * Replaces an image node in the editor with a new image URL. + * + * @param {string} currentUrl - The current URL of the image to be replaced. + * @param {string} newUrl - The new URL to replace the current image with. + * @param {EditorView} view - The ProseMirror editor view. + */ +function replaceImage(currentUrl, newUrl, view) { + view.state.doc.descendants((node, pos) => { + if (node.type.name === "image" && node.attrs.src === currentUrl) { + const tr = view.state.tr.setNodeMarkup(pos, null, { + ...node.attrs, + src: newUrl, + }); + view.dispatch(tr); + } + }); +} + +/** + * Creates a ProseMirror plugin that handles image pasting and uploading. + * + * @param {Function} uploadImage - A function that takes an image URL and returns a Promise + * that resolves to the new URL after uploading. + * @returns {Plugin} A ProseMirror plugin that handles image pasting. + */ +const imagePastePlugin = (uploadImage) => + new Plugin({ + props: { + /** + * Handles the paste event in the editor. + * + * @param {EditorView} view - The ProseMirror editor view. + * @param {Event} event - The paste event. + * @param {Slice} slice - The ProseMirror Slice object representing the pasted content. + */ + handlePaste(view, event, slice) { + const imageUrls = []; + slice.content.descendants((node) => { + if (node.type.name === "image") { + imageUrls.push(node.attrs.src); + } + }); + Promise.all(imageUrls.map(async (url) => { + try { + const newUrl = await uploadImage(url); + replaceImage(url, newUrl, view); + } catch (error) { + console.error("Error uploading image:", error); + } + })); + }, + }, + }); + +export default imagePastePlugin;