From 91361d2ef96189670907b509eb61ec3e68b92ef6 Mon Sep 17 00:00:00 2001 From: iamsivin Date: Fri, 30 Aug 2024 22:41:33 +0530 Subject: [PATCH] feat: Update `triggerCharacters` plugin to support min characters --- package.json | 2 +- src/mentions/plugin.js | 164 ++++++++++++++++++++--------------------- 2 files changed, 82 insertions(+), 84 deletions(-) diff --git a/package.json b/package.json index 57df957..44fc68d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chatwoot/prosemirror-schema", - "version": "1.0.11", + "version": "1.0.12", "description": "Schema setup for using prosemirror in chatwoot. Based on 👉 https://github.com/ProseMirror/prosemirror-example-setup/", "main": "dist/index.js", "scripts": { diff --git a/src/mentions/plugin.js b/src/mentions/plugin.js index ab72630..dd4e276 100644 --- a/src/mentions/plugin.js +++ b/src/mentions/plugin.js @@ -6,8 +6,8 @@ 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'); +export const triggerCharacters = (char, minChars = 0) => $position => { + const regexp = new RegExp(`(^|\\s)(${char}[^\\s${char}]{${minChars},})`, 'g'); const textFrom = $position.before(); const textTo = $position.end(); @@ -18,17 +18,17 @@ export const triggerCharacters = char => $position => { // eslint-disable-next-line while ((match = regexp.exec(text))) { - const prefix = match.input.slice(Math.max(0, match.index - 1), match.index); - if (!/^[\s\0]?$/.test(prefix)) { - // eslint-disable-next-line - continue; - } + const beforeChar = match[1]; // Will be empty at start of text, or a space in the middle + const fullMatch = match[2]; // Includes the trigger character and following text - const from = match.index + $position.start(); - let to = from + match[0].length; + const from = match.index + $position.start() + beforeChar.length; + const to = from + fullMatch.length; if (from < $position.pos && to >= $position.pos) { - return { range: { from, to }, text: match[0] }; + const trimmedText = fullMatch + ? fullMatch.slice(char.length).trim() + : ""; // Remove trigger char and trim + return { range: { from, to }, text: trimmedText }; } } return null; @@ -41,96 +41,94 @@ export const suggestionsPlugin = ({ onChange = () => false, onExit = () => false, onKeyDown = () => false, -}) => { - return new Plugin({ - key: new PluginKey('mentions'), +}) => new Plugin({ + key: new PluginKey('mentions'), - view() { - return { - update: (view, prevState) => { - const prev = this.key.getState(prevState); - const next = this.key.getState(view.state); + view() { + return { + update: (view, prevState) => { + const prev = this.key.getState(prevState); + const next = this.key.getState(view.state); - const moved = + const moved = prev.active && next.active && prev.range.from !== next.range.from; - const started = !prev.active && next.active; - const stopped = prev.active && !next.active; - const changed = !started && !stopped && prev.text !== next.text; - - if (stopped || moved) - onExit({ view, range: prev.range, text: prev.text }); - if (changed && !moved) - onChange({ view, range: next.range, text: next.text }); - if (started || moved) - onEnter({ view, range: next.range, text: next.text }); - }, + const started = !prev.active && next.active; + const stopped = prev.active && !next.active; + const changed = !started && !stopped && prev.text !== next.text; + + if (stopped || moved) + onExit({ view, range: prev.range, text: prev.text }); + if (changed && !moved) + onChange({ view, range: next.range, text: next.text }); + if (started || moved) + onEnter({ view, range: next.range, text: next.text }); + }, + }; + }, + + state: { + init() { + return { + active: false, + range: {}, + text: null, }; }, - state: { - init() { - return { - active: false, - range: {}, - text: null, - }; - }, - - apply(tr, prev) { - const { selection } = tr; - const next = { ...prev }; + apply(tr, prev) { + const { selection } = tr; + const next = { ...prev }; - if (selection.from === selection.to) { - if ( - selection.from < prev.range.from || + if (selection.from === selection.to) { + if ( + selection.from < prev.range.from || selection.from > prev.range.to - ) { - next.active = false; - } - - const $position = selection.$from; - const match = matcher($position); - - if (match) { - next.active = true; - next.range = match.range; - next.text = match.text; - } else { - next.active = false; - } - } else { + ) { next.active = false; } - if (!next.active) { - next.range = {}; - next.text = null; + const $position = selection.$from; + const match = matcher($position); + + if (match) { + next.active = true; + next.range = match.range; + next.text = match.text; + } else { + next.active = false; } + } else { + next.active = false; + } - return next; - }, + if (!next.active) { + next.range = {}; + next.text = null; + } + + return next; }, + }, - props: { - handleKeyDown(view, event) { - const { active } = this.getState(view.state); + props: { + handleKeyDown(view, event) { + const { active } = this.getState(view.state); - if (!active) return false; + if (!active) return false; - return onKeyDown({ view, event }); - }, - decorations(editorState) { - const { active, range } = this.getState(editorState); + return onKeyDown({ view, event }); + }, + decorations(editorState) { + const { active, range } = this.getState(editorState); - if (!active) return null; + if (!active) return null; - return DecorationSet.create(editorState.doc, [ - Decoration.inline(range.from, range.to, { - nodeName: 'span', - class: suggestionClass, - }), - ]); - }, + return DecorationSet.create(editorState.doc, [ + Decoration.inline(range.from, range.to, { + nodeName: 'span', + class: suggestionClass, + }), + ]); }, - }); -}; + }, +});