Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add popup plugin #3128

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NodeSpec } from 'prosemirror-model';

export const mention: NodeSpec = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should call this thing "mention" already? Or if we should be more generic and design the feature in a way that the consumer can provide limel-text-editor with a list of items, which will be then shown if @ is entered as an input?

Having "mentions" implemented in a text-editor is not an unusual feature. And probably approaching this feature as "mention" is an easy and straightforward way for going forward. But I'm just thinking about other forms of hotkeys (or short command triggers) such as / or # or perhaps custom characters defined by the consumer like $.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many aspects of this will be agnostic and generic. However for NodeSpec's they will have to be unique and specific to what they hold.

If we're traversing the Editor State the current Node or group of Nodes needs to tell us exactly what it or they are. So for tags we would create a separate NodeSpec to specify tags.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I entirely understand the technical aspects that you mentioned. But what I was trying to say is, should this @-ing or #-ing be called "mentioning" and "tagging"? Or can we find more generic names and functionality-agnostic designs for them, such as at-command, hashtag-command etc…

If we call it "mention" and create an entire set of user interactions and components for handling mentions (picking a person from a searchable list while typing a piece of text), our work will be easy and straightforward. The consumer's expectations will also be straightforward. They will only use the @ trigger to implement mentions, where they use the text editor component.

However, in another context for other consumers, the @-ing may be used to link things together maybe… Or to add locations, or dates or whatever the 3rd-party consumers' use-case may be.

Maybe I'm over complicating… but this was just a question that poped in my head.

inline: true,
group: 'inline',
selectable: true,
atom: true,

attrs: {
type: {},
id: {},
name: {},
},

toDOM: (node) => [
'span',
{
class: 'mention',
style: 'color: blue',
'data-mention-name': node.attrs.name,
'data-mention-type': node.attrs.type,
'data-mention-id': node.attrs.id,
},
'@' + node.attrs.name,
],
parseDOM: [
{
tag: 'span[data-mention-type][data-mention-id][data-mention-name]',
getAttrs: (dom) => ({
name: dom.getAttribute('data-mention-name'),
type: dom.getAttribute('data-mention-type'),
id: dom.getAttribute('data-mention-id'),
}),
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Schema, Fragment, Slice } from 'prosemirror-model';
import { EditorState } from 'prosemirror-state';

/**
* Processes text nodes to find mentions and creates a transaction to replace text with mention nodes.
* @param state - The current editor state.
* @param schema - The ProseMirror schema used to create mention nodes.
* @returns - A transaction if changes are made, or null if no changes are needed.
*/
export const createMentionParseTransaction = (
state: EditorState,
schema: Schema,
) => {
const transaction = state.tr;
const regex = /@([^:]+):(\w+):(\w+)/g;
let madeChanges = false;

state.doc.descendants((node, pos) => {
if (!node.isText) {
return;
}

const text = node.text;
let lastMatchEnd = 0;
const fragments = [];
let match;

while ((match = regex.exec(text)) !== null) {
const [fullMatch, name, type, id] = match;
const offset = match.index;

// Add text before the match
if (offset > lastMatchEnd) {
fragments.push(node.cut(lastMatchEnd, offset));
}

// Create and add the mention node
const mentionNode = schema.nodes.mention.create({
name: name,
type: type,
id: id,
});
fragments.push(mentionNode);

lastMatchEnd = offset + fullMatch.length;
}

// Add any remaining text after the last match
if (lastMatchEnd < text.length) {
fragments.push(node.cut(lastMatchEnd));
}

if (fragments.length) {
const newFragments = Fragment.fromArray(fragments);
transaction.replaceRange(
pos,
pos + node.nodeSize,
new Slice(newFragments, 0, 0),
);
madeChanges = true;
}
});

return madeChanges ? transaction : null;
};
Loading
Loading