From 86357eaf2ac822e0ae84acb092fb167cbab6f47c Mon Sep 17 00:00:00 2001 From: junaid Date: Fri, 8 Jul 2022 16:33:16 +0530 Subject: [PATCH 1/3] mentions cut 1 --- src/components/CommandMenu.tsx | 55 +++++++++++++- src/components/MentionMenu.tsx | 114 +++++++++++++++++++++++++++++ src/components/MentionMenuItem.tsx | 34 +++++++++ src/index.tsx | 26 +++++++ src/nodes/Mention.ts | 97 ++++++++++++++++++++++++ src/plugins/Mentions.tsx | 93 +++++++++++++++++++++++ src/server.ts | 2 + 7 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 src/components/MentionMenu.tsx create mode 100644 src/components/MentionMenuItem.tsx create mode 100644 src/nodes/Mention.ts create mode 100644 src/plugins/Mentions.tsx diff --git a/src/components/CommandMenu.tsx b/src/components/CommandMenu.tsx index 390512d90..7f53d1640 100644 --- a/src/components/CommandMenu.tsx +++ b/src/components/CommandMenu.tsx @@ -11,6 +11,7 @@ import getDataTransferFiles from "../lib/getDataTransferFiles"; import filterExcessSeparators from "../lib/filterExcessSeparators"; import insertFiles from "../commands/insertFiles"; import baseDictionary from "../dictionary"; +import createAndInsertLink from "../commands/createAndInsertLink"; const SSR = typeof window === "undefined"; @@ -29,6 +30,7 @@ export type Props = { view: EditorView; search: string; uploadImage?: (file: File) => Promise; + onCreateLink?:(title: string) => Promise; onImageUploadStart?: () => void; onImageUploadStop?: () => void; onShowToast?: (message: string, id: string) => void; @@ -173,7 +175,46 @@ class CommandMenu extends React.Component, State> { } }; + handleOnCreateLink = async (title: string) => { + const { dictionary, onCreateLink, view, onClose, onShowToast } = this.props; + + onClose(); + this.props.view.focus(); + + if (!onCreateLink) { + return; + } + + const { dispatch, state } = view; + const { from, to } = state.selection; + if (from !== to) { + // selection must be collapsed + return; + } + + const href = `creating#${title}…`; + + // Insert a placeholder link + dispatch( + view.state.tr + .insertText(title, from, to) + .addMark( + from, + to + title.length, + state.schema.marks.link.create({ href }) + ) + ); + + createAndInsertLink(view, title, href, { + onCreateLink, + onShowToast, + dictionary, + }); + }; + insertItem = item => { + console.log(item.name); + switch (item.name) { case "image": return this.triggerImagePick(); @@ -185,6 +226,8 @@ class CommandMenu extends React.Component, State> { this.props.onLinkToolbarOpen?.(); return; } + // case 'mention' : + // return this.handleOnCreateLink(item.title) default: this.insertBlock(item); } @@ -302,7 +345,17 @@ class CommandMenu extends React.Component, State> { this.clearSearch(); const command = this.props.commands[item.name]; - + // if(item.name == 'mention') { + // const { state, dispatch } = this.props.view; + // dispatch( + // state.tr.insertText( + // item.title, + // state.selection.$from.pos - (this.props.search ?? "").length - 1, + // state.selection.to + // ) + // ); + // } + if (command) { command(item.attrs); } else { diff --git a/src/components/MentionMenu.tsx b/src/components/MentionMenu.tsx new file mode 100644 index 000000000..14fc6a856 --- /dev/null +++ b/src/components/MentionMenu.tsx @@ -0,0 +1,114 @@ +import React from "react"; +import gemojies from "gemoji"; +import FuzzySearch from "fuzzy-search"; +import CommandMenu, { Props } from "./CommandMenu"; +import EmojiMenuItem from "./EmojiMenuItem"; +import MentionMenuItem from "./MentionMenuItem"; + +type Emoji = { + name: string; + title: string; +// emoji: string; + email: string; + attrs: { markup: string; "data-name": string }; +}; + +const people = [{ + name: { + firstName: 'Jesse', + lastName: 'Bowen', + }, + state: 'Seattle', + }, + { + name: { + firstName: 'juniad', + lastName: 'Bowen', + }, + state: 'junaid', + + }, + { + name: { + firstName: 'juniad', + lastName: 'adam', + }, + state: 'adam', + + } +]; + +const searcher = new FuzzySearch(people, ['name.firstName', 'state'], { + caseSensitive: true, + sort: true, +}); +type onCreateLink = (title: string) => Promise +class MentionMenu extends React.Component< + Omit< + Props, + | "renderMenuItem" + | "items" + | "onLinkToolbarOpen" + | "embeds" + | "onClearSearch" + > +> { + get items(): Emoji[] { + const { search = "" } = this.props; + + const n = search.toLowerCase(); + const result = searcher.search(n).map(item => { + const email = item.name.firstName; + const name = item.state; + return { + ...item, + name: "mention", + title: name, + email, + attrs: { markup: name, "data-name": name }, + }; + }); + + return result.slice(0, 10); + } + + clearSearch = () => { + const { state, dispatch } = this.props.view; + + // clear search input + dispatch( + state.tr.insertText( + "", + state.selection.$from.pos - (this.props.search ?? "").length - 1, + state.selection.to + ) + ); + }; + + + render() { + return ( + { + return ( + + ); + }} + items={this.items} + /> + ); + } +} + +export default MentionMenu; diff --git a/src/components/MentionMenuItem.tsx b/src/components/MentionMenuItem.tsx new file mode 100644 index 000000000..271463a0d --- /dev/null +++ b/src/components/MentionMenuItem.tsx @@ -0,0 +1,34 @@ +import * as React from "react"; +import BlockMenuItem, { Props as BlockMenuItemProps } from "./BlockMenuItem"; +import styled from "styled-components"; + +const Emoji = styled.span` + font-size: 16px; +`; + +const MentionName = ({ + title, +}: { + title: React.ReactNode; +}) => { + return ( +

+ {/* {emoji} */} +    + {title} +

+ ); +}; + +type EmojiMenuItemProps = Omit & { + // emoji: string; +}; + +export default function MentionMenuItem(props: EmojiMenuItemProps) { + return ( + } + /> + ); +} diff --git a/src/index.tsx b/src/index.tsx index 428a1c273..e2d3dda0a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -40,6 +40,7 @@ import CodeBlock from "./nodes/CodeBlock"; import CodeFence from "./nodes/CodeFence"; import CheckboxList from "./nodes/CheckboxList"; import Emoji from "./nodes/Emoji"; +import Mention from "./nodes/Mention"; import CheckboxItem from "./nodes/CheckboxItem"; import Embed from "./nodes/Embed"; import HardBreak from "./nodes/HardBreak"; @@ -68,6 +69,7 @@ import Underline from "./marks/Underline"; // plugins import BlockMenuTrigger from "./plugins/BlockMenuTrigger"; import EmojiTrigger from "./plugins/EmojiTrigger"; +import Mentions from "./plugins/Mentions"; import Folding from "./plugins/Folding"; import History from "./plugins/History"; import Keys from "./plugins/Keys"; @@ -77,6 +79,7 @@ import SmartText from "./plugins/SmartText"; import TrailingNode from "./plugins/TrailingNode"; import PasteHandler from "./plugins/PasteHandler"; import { PluginSimple } from "markdown-it"; +import MentionMenu from "./components/MentionMenu"; export { schema, parser, serializer, renderToHtml } from "./server"; @@ -164,6 +167,7 @@ type State = { linkMenuOpen: boolean; blockMenuSearch: string; emojiMenuOpen: boolean; + mentionsOpen: boolean }; type Step = { @@ -197,6 +201,7 @@ class RichMarkdownEditor extends React.PureComponent { linkMenuOpen: false, blockMenuSearch: "", emojiMenuOpen: false, + mentionsOpen: false }; isBlurred: boolean; @@ -219,6 +224,8 @@ class RichMarkdownEditor extends React.PureComponent { rulePlugins: PluginSimple[]; componentDidMount() { + console.log('hellooo'); + this.init(); if (this.props.scrollTo) { @@ -327,6 +334,7 @@ class RichMarkdownEditor extends React.PureComponent { onShowToast: this.props.onShowToast, }), new Emoji(), + new Mention(), new Text(), new CheckboxList(), new CheckboxItem(), @@ -397,6 +405,14 @@ class RichMarkdownEditor extends React.PureComponent { this.setState({ emojiMenuOpen: false }); }, }), + new Mentions({ + onOpen: (search: string) => { + this.setState({ mentionsOpen: true, blockMenuSearch: search }); + }, + onClose: () => { + this.setState({ mentionsOpen: false }); + }, + }), new Placeholder({ placeholder: this.props.placeholder, }), @@ -814,6 +830,16 @@ class RichMarkdownEditor extends React.PureComponent { search={this.state.blockMenuSearch} onClose={() => this.setState({ emojiMenuOpen: false })} /> + this.setState({ mentionsOpen: false })} + /> ({ + "data-name": dom.dataset.name, + }), + }, + ], + toDOM: node => { + const text = document.createTextNode(`@${node.attrs["data-name"]}`); + return ["span", { class: "emoji" }, text]; + }, + }; + } + +// get rulePlugins() { +// return [emojiRule]; +// } + + commands({ type }) { + return attrs => (state, dispatch) => { + const { selection } = state; + const position = selection.$cursor + ? selection.$cursor.pos + : selection.$to.pos; + const node = type.create(attrs); + const transaction = state.tr.insert(position, node); + dispatch(transaction); + return true; + }; + } + + inputRules({ type }) { + return [ + new InputRule(/^\@([a-zA-Z0-9_+-]+)\@$/, (state, match, start, end) => { + const [okay, markup] = match; + const { tr } = state; + if (okay) { + tr.replaceWith( + start - 1, + end, + type.create({ + "data-name": markup, + markup, + }) + ); + } + + return tr; + }), + ]; + } + + toMarkdown(state, node) { + const name = node.attrs["data-name"]; + if (name) { + state.write(`@${name}`); + } + } + + parseMarkdown() { + return { + node: "mention", + getAttrs: tok => { + return { "data-name": tok.markup.trim() }; + }, + }; + } +} + + diff --git a/src/plugins/Mentions.tsx b/src/plugins/Mentions.tsx new file mode 100644 index 000000000..9d8c98ed2 --- /dev/null +++ b/src/plugins/Mentions.tsx @@ -0,0 +1,93 @@ +import { InputRule } from "prosemirror-inputrules"; +import { Plugin } from "prosemirror-state"; +import Extension from "../lib/Extension"; +import isInCode from "../queries/isInCode"; +import { run } from "./BlockMenuTrigger"; + +const OPEN_REGEX = /(?:^|\s)@([0-9a-zA-Z_+-]+)?$/; +const CLOSE_REGEX = /(?:^|\s)@(([0-9a-zA-Z_+-]*\s+)|(\s+[0-9a-zA-Z_+-]+)|[^0-9a-zA-Z_+-]+)$/; + +export default class Mentions extends Extension { + get name() { + return "mentions"; + } + + get plugins() { + return [ + new Plugin({ + props: { + handleClick: () => { + this.options.onClose(); + return false; + }, + handleKeyDown: (view, event) => { + // Prosemirror input rules are not triggered on backspace, however + // we need them to be evaluted for the filter trigger to work + // correctly. This additional handler adds inputrules-like handling. + if (event.key === "Backspace") { + // timeout ensures that the delete has been handled by prosemirror + // and any characters removed, before we evaluate the rule. + setTimeout(() => { + const { pos } = view.state.selection.$from; + return run(view, pos, pos, OPEN_REGEX, (state, match) => { + if (match) { + this.options.onOpen(match[1]); + } else { + this.options.onClose(); + } + return null; + }); + }); + } + + // If the query is active and we're navigating the block menu then + // just ignore the key events in the editor itself until we're done + if ( + event.key === "Enter" || + event.key === "ArrowUp" || + event.key === "ArrowDown" || + event.key === "Tab" + ) { + const { pos } = view.state.selection.$from; + + return run(view, pos, pos, OPEN_REGEX, (state, match) => { + // just tell Prosemirror we handled it and not to do anything + return match ? true : null; + }); + } + + return false; + }, + }, + }), + ]; + } + + inputRules() { + return [ + // main regex should match only: + // :word + new InputRule(OPEN_REGEX, (state, match) => { + if ( + match && + state.selection.$from.parent.type.name === "paragraph" && + !isInCode(state) + ) { + this.options.onOpen(match[1]); + } + return null; + }), + // invert regex should match some of these scenarios: + // :word + // : + // :word + // :) + new InputRule(CLOSE_REGEX, (state, match) => { + if (match) { + this.options.onClose(); + } + return null; + }), + ]; + } +} diff --git a/src/server.ts b/src/server.ts index fe7a16155..342c19458 100644 --- a/src/server.ts +++ b/src/server.ts @@ -7,6 +7,7 @@ import Doc from "./nodes/Doc"; import Text from "./nodes/Text"; import Blockquote from "./nodes/Blockquote"; import Emoji from "./nodes/Emoji"; +import Mention from "./nodes/Mention"; import BulletList from "./nodes/BulletList"; import CodeBlock from "./nodes/CodeBlock"; import CodeFence from "./nodes/CodeFence"; @@ -43,6 +44,7 @@ const extensions = new ExtensionManager([ new Paragraph(), new Blockquote(), new Emoji(), + new Mention(), new BulletList(), new CodeBlock(), new CodeFence(), From 4fc774c51949b2b12a8d2b2e67be499d376a3863 Mon Sep 17 00:00:00 2001 From: junaid Date: Fri, 4 Nov 2022 10:24:51 +0530 Subject: [PATCH 2/3] mentions first cut --- package.json | 12 +++- src/components/CommandMenu.tsx | 2 + src/components/MentionMenu.tsx | 54 +++++++++-------- src/components/MentionMenuItem.tsx | 7 ++- src/index.tsx | 4 +- src/lib/renderToHtml.ts | 2 + src/nodes/Mention.ts | 97 ------------------------------ src/plugins/Mentions.tsx | 4 +- src/rules/mentions.ts | 68 +++++++++++++++++++++ src/styles/editor.ts | 10 ++- 10 files changed, 130 insertions(+), 130 deletions(-) delete mode 100644 src/nodes/Mention.ts create mode 100644 src/rules/mentions.ts diff --git a/package.json b/package.json index aaa08c515..72acd8bbc 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ ] }, "dependencies": { + "@quartzy/markdown-it-mentions": "^0.2.0", "copy-to-clipboard": "^3.0.8", "fuzzy-search": "^3.2.1", "gemoji": "6.x", @@ -52,7 +53,8 @@ "refractor": "^3.3.1", "resize-observer-polyfill": "^1.5.1", "slugify": "^1.4.0", - "smooth-scroll-into-view-if-needed": "^1.1.29" + "smooth-scroll-into-view-if-needed": "^1.1.29", + "url-parse": "^1.5.10" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0", @@ -112,7 +114,8 @@ "resolutions": { "markdown-it": "^12.2.0", "prosemirror-transform": "1.2.5", - "yargs-parser": "^15.0.1" + "yargs-parser": "^15.0.1", + "prosemirror-model": "1.9.1" }, "repository": { "type": "git", @@ -128,5 +131,8 @@ "bugs": { "url": "https://github.com/outline/rich-markdown-editor/issues" }, - "homepage": "https://github.com/outline/rich-markdown-editor#readme" + "homepage": "https://github.com/outline/rich-markdown-editor#readme", + "volta": { + "node": "16.18.0" + } } diff --git a/src/components/CommandMenu.tsx b/src/components/CommandMenu.tsx index 7f53d1640..e95478b19 100644 --- a/src/components/CommandMenu.tsx +++ b/src/components/CommandMenu.tsx @@ -345,6 +345,8 @@ class CommandMenu extends React.Component, State> { this.clearSearch(); const command = this.props.commands[item.name]; + console.log(item,'insertBlock'); + // if(item.name == 'mention') { // const { state, dispatch } = this.props.view; // dispatch( diff --git a/src/components/MentionMenu.tsx b/src/components/MentionMenu.tsx index 14fc6a856..949fab6ef 100644 --- a/src/components/MentionMenu.tsx +++ b/src/components/MentionMenu.tsx @@ -8,34 +8,40 @@ import MentionMenuItem from "./MentionMenuItem"; type Emoji = { name: string; title: string; -// emoji: string; + // emoji: string; email: string; attrs: { markup: string; "data-name": string }; }; const people = [{ - name: { - firstName: 'Jesse', - lastName: 'Bowen', - }, - state: 'Seattle', + name: { + firstName: 'Jesse', + lastName: 'Bowen', }, - { - name: { - firstName: 'juniad', - lastName: 'Bowen', - }, - state: 'junaid', - + state: 'Seattle', +}, +{ + name: { + firstName: 'juniad', + lastName: 'Bowen', }, - { - name: { - firstName: 'juniad', - lastName: 'adam', - }, - state: 'adam', - - } + state: 'junaid', + +}, +{ + name: { + firstName: 'juniad', + lastName: 'adam', + }, + state: 'adam', +}, +{ + name: { + firstName: 'no-name', + lastName: 'yes-name', + }, + state: 'name', +} ]; const searcher = new FuzzySearch(people, ['name.firstName', 'state'], { @@ -51,7 +57,7 @@ class MentionMenu extends React.Component< | "onLinkToolbarOpen" | "embeds" | "onClearSearch" - > + > > { get items(): Emoji[] { const { search = "" } = this.props; @@ -85,7 +91,7 @@ class MentionMenu extends React.Component< ); }; - + render() { return ( ); diff --git a/src/components/MentionMenuItem.tsx b/src/components/MentionMenuItem.tsx index 271463a0d..ebfffbe2b 100644 --- a/src/components/MentionMenuItem.tsx +++ b/src/components/MentionMenuItem.tsx @@ -6,6 +6,7 @@ const Emoji = styled.span` font-size: 16px; `; + const MentionName = ({ title, }: { @@ -24,11 +25,15 @@ type EmojiMenuItemProps = Omit & { // emoji: string; }; +const Mentions = styled(MentionName)({ + cursor: 'pointer' +}) + export default function MentionMenuItem(props: EmojiMenuItemProps) { return ( } + title={} /> ); } diff --git a/src/index.tsx b/src/index.tsx index e2d3dda0a..f92de42b1 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -69,7 +69,7 @@ import Underline from "./marks/Underline"; // plugins import BlockMenuTrigger from "./plugins/BlockMenuTrigger"; import EmojiTrigger from "./plugins/EmojiTrigger"; -import Mentions from "./plugins/Mentions"; +import MentionsTrigger from "./plugins/Mentions"; import Folding from "./plugins/Folding"; import History from "./plugins/History"; import Keys from "./plugins/Keys"; @@ -405,7 +405,7 @@ class RichMarkdownEditor extends React.PureComponent { this.setState({ emojiMenuOpen: false }); }, }), - new Mentions({ + new MentionsTrigger({ onOpen: (search: string) => { this.setState({ mentionsOpen: true, blockMenuSearch: search }); }, diff --git a/src/lib/renderToHtml.ts b/src/lib/renderToHtml.ts index 76c4a3593..c9cf175e5 100644 --- a/src/lib/renderToHtml.ts +++ b/src/lib/renderToHtml.ts @@ -8,6 +8,7 @@ import tablesRule from "../rules/tables"; import noticesRule from "../rules/notices"; import underlinesRule from "../rules/underlines"; import emojiRule from "../rules/emoji"; +import mention from "../rules/mentions"; const defaultRules = [ embedsRule, @@ -15,6 +16,7 @@ const defaultRules = [ checkboxRule, markRule({ delim: "==", mark: "highlight" }), markRule({ delim: "!!", mark: "placeholder" }), + mention, underlinesRule, tablesRule, noticesRule, diff --git a/src/nodes/Mention.ts b/src/nodes/Mention.ts deleted file mode 100644 index b160306be..000000000 --- a/src/nodes/Mention.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { InputRule } from "prosemirror-inputrules"; -import nameToEmoji from "gemoji/name-to-emoji.json"; -import Node from "./Node"; -import emojiRule from "../rules/emoji"; - -export default class Mention extends Node { - get name() { - return "mention"; - } - - get schema() { - return { - attrs: { - style: { - default: "", - }, - "data-name": { - default: undefined, - }, - }, - inline: true, - content: "text*", - marks: "", - group: "inline", - selectable: false, - parseDOM: [ - { - tag: "span.mention", - preserveWhitespace: "full", - getAttrs: (dom: HTMLDivElement) => ({ - "data-name": dom.dataset.name, - }), - }, - ], - toDOM: node => { - const text = document.createTextNode(`@${node.attrs["data-name"]}`); - return ["span", { class: "emoji" }, text]; - }, - }; - } - -// get rulePlugins() { -// return [emojiRule]; -// } - - commands({ type }) { - return attrs => (state, dispatch) => { - const { selection } = state; - const position = selection.$cursor - ? selection.$cursor.pos - : selection.$to.pos; - const node = type.create(attrs); - const transaction = state.tr.insert(position, node); - dispatch(transaction); - return true; - }; - } - - inputRules({ type }) { - return [ - new InputRule(/^\@([a-zA-Z0-9_+-]+)\@$/, (state, match, start, end) => { - const [okay, markup] = match; - const { tr } = state; - if (okay) { - tr.replaceWith( - start - 1, - end, - type.create({ - "data-name": markup, - markup, - }) - ); - } - - return tr; - }), - ]; - } - - toMarkdown(state, node) { - const name = node.attrs["data-name"]; - if (name) { - state.write(`@${name}`); - } - } - - parseMarkdown() { - return { - node: "mention", - getAttrs: tok => { - return { "data-name": tok.markup.trim() }; - }, - }; - } -} - - diff --git a/src/plugins/Mentions.tsx b/src/plugins/Mentions.tsx index 9d8c98ed2..c1c8fdea8 100644 --- a/src/plugins/Mentions.tsx +++ b/src/plugins/Mentions.tsx @@ -7,9 +7,9 @@ import { run } from "./BlockMenuTrigger"; const OPEN_REGEX = /(?:^|\s)@([0-9a-zA-Z_+-]+)?$/; const CLOSE_REGEX = /(?:^|\s)@(([0-9a-zA-Z_+-]*\s+)|(\s+[0-9a-zA-Z_+-]+)|[^0-9a-zA-Z_+-]+)$/; -export default class Mentions extends Extension { +export default class MentionsTrigger extends Extension { get name() { - return "mentions"; + return "mentionsMenu"; } get plugins() { diff --git a/src/rules/mentions.ts b/src/rules/mentions.ts new file mode 100644 index 000000000..78f86b98c --- /dev/null +++ b/src/rules/mentions.ts @@ -0,0 +1,68 @@ +import parseUrl from 'url-parse'; + +export default function mention(md): void { + + function renderMention(tokens, idx) { + return `${tokens[idx].mention.label}`; + } + + function parseUri(uri) { + const pieces = parseUrl(uri); + + return { + type: pieces.host, + id: pieces.pathname.slice(1), + }; + } + + function parseMentions(state) { + const matcher = /@$/; + + state.tokens.forEach(blockToken => { + if (blockToken.type !== 'inline') return; + + const { children } = blockToken; + + children.forEach((token, idx) => { + // Back out if we're near the end of the token array + if (idx + 3 > children.length) return; + + // Grab the next four tokens that could potentially construct a mention + let [matchToken, openToken, textToken, closeToken = {}] = children.slice(idx, idx + 4); + + // Compensate for when the link has no label + if (textToken.type === 'link_close') { + closeToken = textToken; + textToken = null; + } + + // Back out if we're not dealing with a mention + if (matchToken.type !== 'text') return; + if (!matcher.test(matchToken.content)) return; + if (openToken.type !== 'link_open') return; + if (closeToken.type !== 'link_close') return; + + // Lookup the mention type and ID from the link's href + const href = openToken.attrs.reduce((href, attr) => attr[0] === 'href' ? attr[1] : href, ''); + + // Remove the @ character from the previous text node + matchToken.content = matchToken.content.slice(0, -1); + + // Replace the "link_open" with a single "mention" token + openToken.type = 'mention'; + openToken.mention = parseUri(href); + openToken.mention.label = textToken && textToken.content || ''; + + // Remove the "text" and "link_close" tokens + children.splice(idx + 2, textToken ? 2 : 1); + }); + + blockToken.children = children; + }); + } + + + md.core.ruler.after('inline', 'mention', parseMentions); + md.renderer.rules.mention = renderMention; + +} diff --git a/src/styles/editor.ts b/src/styles/editor.ts index 668fd9afb..6af1d335c 100644 --- a/src/styles/editor.ts +++ b/src/styles/editor.ts @@ -237,7 +237,15 @@ export const StyledEditor = styled("div")<{ opacity: 1; } } - + + .mention { + background: #e8f5fa; + color: #1264a3; + border-radius: 4px; + cursor: pointer; + padding: 0px 2px; + } + .heading-actions { opacity: 0; background: ${props => props.theme.background}; From 54f33de5336cb781c96aedbd344e80003c6731f5 Mon Sep 17 00:00:00 2001 From: junaid Date: Fri, 4 Nov 2022 10:29:02 +0530 Subject: [PATCH 3/3] remove unused code --- src/nodes/Mention.tsx | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/nodes/Mention.tsx diff --git a/src/nodes/Mention.tsx b/src/nodes/Mention.tsx new file mode 100644 index 000000000..01841efdf --- /dev/null +++ b/src/nodes/Mention.tsx @@ -0,0 +1,110 @@ +import { InputRule, wrappingInputRule } from "prosemirror-inputrules"; +import nameToEmoji from "gemoji/name-to-emoji.json"; +import Node from "./Node"; +import emojiRule from "../rules/emoji"; +import markRule from "../rules/mark"; +import mentionsRule from "../rules/mentions"; + +export default class Mention extends Node { + get name() { + return "mention"; + } + + get schema() { + return { + attrs: { + style: { + default: "", + }, + "data-name": { + default: undefined, + }, + }, + inline: true, + content: "text*", + marks: "", + group: "inline", + selectable: false, + parseDOM: [ + { + tag: "span.mention", + preserveWhitespace: "full", + getAttrs: (dom: HTMLDivElement) => ({ + "data-name": dom.dataset.name, + }), + contentElement: ".mention" + }, + ], + toDOM: node => { + let mention = document.createElement("span") + mention.innerText = `@${node.attrs["data-name"]}` + mention.addEventListener('click', () => { + console.log('clik',node.attrs["data-name"]); + }) + return [ + "span", + { + class: "mention", + "data-name": node.attrs["data-name"], + }, + mention, + ]; + }, + }; + } + + get rulePlugins() { + return [mentionsRule]; + } + + commands({ type }) { + return attrs => (state, dispatch) => { + const { selection } = state; + const position = selection.$cursor + ? selection.$cursor.pos + : selection.$to.pos; + const node = type.create(attrs); + const transaction = state.tr.insert(position, node); + dispatch(transaction); + return true; + }; + } + + inputRules({ type }) { + return [ + new InputRule(/^\@([a-zA-Z0-9_+-]+)\@$/, (state, match, start, end) => { + const [okay, markup] = match; + const { tr } = state; + if (okay) { + tr.replaceWith( + start - 1, + end, + type.create({ + "data-name": markup, + markup, + }) + ); + } + return tr; + }), + ]; + } + + toMarkdown(state, node) { + const name = node.attrs["data-name"]; + if (name) { + const label = state.esc(name || ''); + const uri = state.esc(`mention://${name}/${name}`); + state.write(`@[${label}](${uri})`); + } + } + + parseMarkdown() { + return { + node: "mention", + getAttrs: tok => { + return { "data-name": tok.mention.type.trim() }; + }, + }; + } +} \ No newline at end of file