-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[4.x] Add mentions to rich editor #17483
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
Open
bmspereira-07
wants to merge
24
commits into
filamentphp:4.x
Choose a base branch
from
bmspereira-07:add-mentions-to-rich-editor
base: 4.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
1089f35
added mentions to rich editor
303de5f
Updated documentation
ed33db0
updated doc
4029925
Update rich-editor.blade.php
960b2fa
Update RichEditor.php
6b7933c
added to gitignore
d4ed299
updated git ignore
a1a65d4
updated gitignore
487327a
updated rich editor to allow multiple chars
031c02f
added multiple chars to mentions and limit results shown
5db9558
Merge branch 'add-mentions-to-rich-editor' of github.com:bmspereira-0…
0b323fc
revert gitignore changes
ff37d73
added the dist folder :/
119a6a7
Merge branch '4.x' into add-mentions-to-rich-editor
bmspereira-07 ef35200
Merge branch '4.x' into add-mentions-to-rich-editor
bmspereira-07 9b60c12
added possibilite to fetch data async from DB.
808bc73
Merge branch '4.x' into add-mentions-to-rich-editor
bmspereira-07 dbd1e9d
updated docs
f613098
Merge branch 'add-mentions-to-rich-editor' of github.com:bmspereira-0…
29f1323
implementation with MentionProvider and async fetch. Labels are fetch…
f0ad500
fix phpstan errors
9e2788f
fixed extrainputs render
a509c15
Merge branch '4.x' into add-mentions-to-rich-editor
bmspereira-07 4ef0806
Merge branch '4.x' into add-mentions-to-rich-editor
bmspereira-07 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
249 changes: 249 additions & 0 deletions
249
packages/forms/resources/js/components/rich-editor/extension-mention.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,249 @@ | ||
| import { mergeAttributes, Node } from '@tiptap/core' | ||
| import { Node as ProseMirrorNode } from '@tiptap/pm/model' | ||
| import { Plugin, PluginKey } from '@tiptap/pm/state' | ||
| import Suggestion from '@tiptap/suggestion' | ||
| import getMentionSuggestion from './mention-suggestion.js' | ||
|
|
||
| const getSuggestionOptions = function ({ | ||
| editor: tiptapEditor, | ||
| overrideSuggestionOptions, | ||
| extensionName, | ||
| }) { | ||
| const pluginKey = new PluginKey() | ||
|
|
||
| return { | ||
| editor: tiptapEditor, | ||
| char: '@', | ||
| pluginKey, | ||
| command: ({ editor, range, props }) => { | ||
| const nodeAfter = editor.view.state.selection.$to.nodeAfter | ||
| const overrideSpace = nodeAfter?.text?.startsWith(' ') | ||
|
|
||
| if (overrideSpace) { | ||
| range.to += 1 | ||
| } | ||
|
|
||
| editor | ||
| .chain() | ||
| .focus() | ||
| .insertContentAt(range, [ | ||
| { | ||
| type: extensionName, | ||
| attrs: { ...props }, | ||
| }, | ||
| { | ||
| type: 'text', | ||
| text: ' ', | ||
| }, | ||
| ]) | ||
| .run() | ||
|
|
||
| editor.view.dom.ownerDocument.defaultView | ||
| ?.getSelection() | ||
| ?.collapseToEnd() | ||
| }, | ||
| allow: ({ state, range }) => { | ||
| const $from = state.doc.resolve(range.from) | ||
| const type = state.schema.nodes[extensionName] | ||
| const allow = !!$from.parent.type.contentMatch.matchType(type) | ||
|
|
||
| return allow | ||
| }, | ||
| ...overrideSuggestionOptions, | ||
| } | ||
| } | ||
|
|
||
| export default Node.create({ | ||
| name: 'mention', | ||
|
|
||
| priority: 101, | ||
|
|
||
| addStorage() { | ||
| return { | ||
| suggestions: [], | ||
| getSuggestionFromChar: () => null, | ||
| } | ||
| }, | ||
|
|
||
| addOptions() { | ||
| return { | ||
| HTMLAttributes: {}, | ||
| renderText({ node }) { | ||
| return `@${node.attrs.label ?? node.attrs.id}` | ||
| }, | ||
| deleteTriggerWithBackspace: false, | ||
| renderHTML({ options, node }) { | ||
| return [ | ||
| 'span', | ||
| mergeAttributes(this.HTMLAttributes, options.HTMLAttributes), | ||
| `@${node.attrs.label ?? node.attrs.id}`, | ||
| ] | ||
| }, | ||
| suggestions: [], | ||
| suggestion: {}, | ||
| } | ||
| }, | ||
|
|
||
| group: 'inline', | ||
|
|
||
| inline: true, | ||
|
|
||
| selectable: false, | ||
|
|
||
| atom: true, | ||
|
|
||
| addAttributes() { | ||
| return { | ||
| id: { | ||
| default: null, | ||
| parseHTML: (element) => element.getAttribute('data-id'), | ||
| renderHTML: (attributes) => { | ||
| if (!attributes.id) { | ||
| return {} | ||
| } | ||
|
|
||
| return { | ||
| 'data-id': attributes.id, | ||
| } | ||
| }, | ||
| }, | ||
|
|
||
| label: { | ||
| default: null, | ||
| parseHTML: (element) => element.getAttribute('data-label'), | ||
| renderHTML: (attributes) => { | ||
| if (!attributes.label) { | ||
| return {} | ||
| } | ||
|
|
||
| return { | ||
| 'data-label': attributes.label, | ||
| } | ||
| }, | ||
| }, | ||
| } | ||
| }, | ||
|
|
||
| parseHTML() { | ||
| return [ | ||
| { | ||
| tag: `span[data-type="${this.name}"]`, | ||
| }, | ||
| ] | ||
| }, | ||
|
|
||
| renderHTML({ node, HTMLAttributes }) { | ||
| const suggestion = this.editor?.extensionStorage?.[this.name]?.getSuggestionFromChar('@') | ||
|
|
||
| const mergedOptions = { ...this.options } | ||
|
|
||
| mergedOptions.HTMLAttributes = mergeAttributes( | ||
| { 'data-type': this.name }, | ||
| this.options.HTMLAttributes, | ||
| HTMLAttributes, | ||
| ) | ||
|
|
||
| const html = this.options.renderHTML({ | ||
| options: mergedOptions, | ||
| node, | ||
| suggestion, | ||
| }) | ||
|
|
||
| if (typeof html === 'string') { | ||
| return [ | ||
| 'span', | ||
| mergeAttributes( | ||
| { 'data-type': this.name }, | ||
| this.options.HTMLAttributes, | ||
| HTMLAttributes, | ||
| ), | ||
| html, | ||
| ] | ||
| } | ||
| return html | ||
| }, | ||
|
|
||
| renderText({ node }) { | ||
| const args = { | ||
| options: this.options, | ||
| node, | ||
| suggestion: this.editor?.extensionStorage?.[this.name]?.getSuggestionFromChar('@'), | ||
| } | ||
|
|
||
| return this.options.renderText(args) | ||
| }, | ||
|
|
||
| addKeyboardShortcuts() { | ||
| return { | ||
| Backspace: () => | ||
| this.editor.commands.command(({ tr, state }) => { | ||
| let isMention = false | ||
| const { selection } = state | ||
| const { empty, anchor } = selection | ||
|
|
||
| if (!empty) { | ||
| return false | ||
| } | ||
|
|
||
| let mentionNode = new ProseMirrorNode() | ||
| let mentionPos = 0 | ||
|
|
||
| state.doc.nodesBetween(anchor - 1, anchor, (node, pos) => { | ||
| if (node.type.name === this.name) { | ||
| isMention = true | ||
| mentionNode = node | ||
| mentionPos = pos | ||
| return false | ||
| } | ||
| }) | ||
|
|
||
| if (isMention) { | ||
| tr.insertText( | ||
| this.options.deleteTriggerWithBackspace ? '' : '@', | ||
| mentionPos, | ||
| mentionPos + mentionNode.nodeSize, | ||
| ) | ||
| } | ||
|
|
||
| return isMention | ||
| }), | ||
| } | ||
| }, | ||
|
|
||
| addProseMirrorPlugins() { | ||
| return [ | ||
| ...this.storage.suggestions.map(Suggestion), | ||
| new Plugin({}), | ||
| ] | ||
| }, | ||
|
|
||
| onBeforeCreate() { | ||
| this.storage.suggestions = ( | ||
| this.options.suggestions.length ? this.options.suggestions : [this.options.suggestion] | ||
| ).map((suggestion) => { | ||
| const normalized = typeof suggestion.items === 'function' || typeof suggestion.render === 'function' | ||
| ? suggestion | ||
| : getMentionSuggestion({ items: suggestion.items ?? [] }) | ||
|
|
||
| return getSuggestionOptions({ | ||
| editor: this.editor, | ||
| overrideSuggestionOptions: normalized, | ||
| extensionName: this.name, | ||
| }) | ||
| }) | ||
|
|
||
| this.storage.getSuggestionFromChar = (char) => { | ||
| const suggestion = this.storage.suggestions.find((s) => s.char === char) | ||
| if (suggestion) { | ||
| return suggestion | ||
| } | ||
| if (this.storage.suggestions.length) { | ||
| return this.storage.suggestions[0] | ||
| } | ||
|
|
||
| return null | ||
| } | ||
| }, | ||
| }) | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It could be cool to be able to customize the character.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@is quite common. What other char do you have in mind?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like this maybe https://tiptap.dev/docs/editor/extensions/nodes/mention#suggestions.