From 373cc2ebd406038cacd97a440b17753245fab12d Mon Sep 17 00:00:00 2001 From: Hans Pagel Date: Tue, 19 Jan 2021 19:43:23 +0100 Subject: [PATCH 01/28] fix minor styling issue with the navigation --- docs/src/layouts/App/style.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/src/layouts/App/style.scss b/docs/src/layouts/App/style.scss index fd6c98d3..765e174e 100644 --- a/docs/src/layouts/App/style.scss +++ b/docs/src/layouts/App/style.scss @@ -256,8 +256,6 @@ $menuBreakPoint: 800px; } &--sponsor { - color: $colorWhite; - &::after { content: '💖'; font-family: 'JetBrainsMono', monospace; From 230a0a13ac22c3f27b526ccdb8dcded0537356ce Mon Sep 17 00:00:00 2001 From: Hans Pagel Date: Wed, 20 Jan 2021 09:18:33 +0100 Subject: [PATCH 02/28] clean up page navigation --- docs/src/links.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/src/links.yaml b/docs/src/links.yaml index 6fbf3c01..4ce8a96b 100644 --- a/docs/src/links.yaml +++ b/docs/src/links.yaml @@ -35,9 +35,6 @@ link: /examples/drawing - title: Multiple editors link: /examples/multiple-editors - - title: Comments - link: /examples/comments - draft: true - title: Guide From 28422177814a3df3b160376d1968c13f83abf5f5 Mon Sep 17 00:00:00 2001 From: Hans Pagel Date: Wed, 20 Jan 2021 09:20:03 +0100 Subject: [PATCH 03/28] fix navigation link type --- docs/src/links.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/links.yaml b/docs/src/links.yaml index 4ce8a96b..80e4f834 100644 --- a/docs/src/links.yaml +++ b/docs/src/links.yaml @@ -157,7 +157,7 @@ items: - title: Annotation link: /api/extensions/annotation - draft: true + type: draft - title: Collaboration link: /api/extensions/collaboration type: pro From 72b4eb17bdb7216d64b0f4924653f6d0a5161ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Wed, 16 Dec 2020 17:47:17 +0100 Subject: [PATCH 04/28] add basic suggestions package --- packages/suggestions/README.md | 14 ++ packages/suggestions/package.json | 30 +++ packages/suggestions/src/index.ts | 5 + packages/suggestions/src/suggestions.ts | 254 ++++++++++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 packages/suggestions/README.md create mode 100644 packages/suggestions/package.json create mode 100644 packages/suggestions/src/index.ts create mode 100644 packages/suggestions/src/suggestions.ts diff --git a/packages/suggestions/README.md b/packages/suggestions/README.md new file mode 100644 index 00000000..22c4be68 --- /dev/null +++ b/packages/suggestions/README.md @@ -0,0 +1,14 @@ +# @tiptap/suggestions +[![Version](https://img.shields.io/npm/v/@tiptap/suggestions.svg?label=version)](https://www.npmjs.com/package/@tiptap/suggestions) +[![Downloads](https://img.shields.io/npm/dm/@tiptap/suggestions.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@tiptap/suggestions.svg)](https://www.npmjs.com/package/@tiptap/suggestions) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +## Introduction +tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*. + +## Offical Documentation +Documentation can be found on the [tiptap website](https://tiptap.dev). + +## License +tiptap is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap-next/blob/main/LICENSE.md). diff --git a/packages/suggestions/package.json b/packages/suggestions/package.json new file mode 100644 index 00000000..2f7b0f62 --- /dev/null +++ b/packages/suggestions/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tiptap/suggestions", + "description": "suggestions", + "version": "2.0.0-alpha.0", + "homepage": "https://tiptap.dev", + "keywords": [ + "tiptap", + "tiptap utility" + ], + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "main": "dist/tiptap-suggestions.cjs.js", + "umd": "dist/tiptap-suggestions.umd.js", + "module": "dist/tiptap-suggestions.esm.js", + "unpkg": "dist/tiptap-suggestions.bundle.umd.min.js", + "types": "dist/packages/suggestions/src/index.d.ts", + "files": [ + "src", + "dist" + ], + "dependencies": { + "@tiptap/core": "^2.0.0-alpha.7", + "prosemirror-state": "^1.3.3", + "prosemirror-view": "^1.16.3", + "prosemirror-model": "^1.12.0" + } +} diff --git a/packages/suggestions/src/index.ts b/packages/suggestions/src/index.ts new file mode 100644 index 00000000..69d382d8 --- /dev/null +++ b/packages/suggestions/src/index.ts @@ -0,0 +1,5 @@ +import { Suggestions } from './suggestions' + +export * from './suggestions' + +export default Suggestions diff --git a/packages/suggestions/src/suggestions.ts b/packages/suggestions/src/suggestions.ts new file mode 100644 index 00000000..7577673c --- /dev/null +++ b/packages/suggestions/src/suggestions.ts @@ -0,0 +1,254 @@ +import { Plugin, PluginKey } from 'prosemirror-state' +import { ResolvedPos } from 'prosemirror-model' +import { Decoration, DecorationSet } from 'prosemirror-view' + +// Create a matcher that matches when a specific character is typed. Useful for @mentions and #tags. +function triggerCharacter({ + char = '@', + allowSpaces = false, + startOfLine = false, +}) { + + return ($position: ResolvedPos) => { + // cancel if top level node + if ($position.depth <= 0) { + return false + } + + // Matching expressions used for later + const escapedChar = `\\${char}` + const suffix = new RegExp(`\\s${escapedChar}$`) + const prefix = startOfLine ? '^' : '' + const regexp = allowSpaces + ? new RegExp(`${prefix}${escapedChar}.*?(?=\\s${escapedChar}|$)`, 'gm') + : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\s${escapedChar}]*`, 'gm') + + // Lookup the boundaries of the current node + const textFrom = $position.before() + const textTo = $position.end() + const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0') + + let match = regexp.exec(text) + let position + + while (match !== null) { + // JavaScript doesn't have lookbehinds; this hacks a check that first character is " " + // or the line beginning + const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index) + + if (/^[\s\0]?$/.test(matchPrefix)) { + // The absolute position of the match in the document + const from = match.index + $position.start() + let to = from + match[0].length + + // Edge case handling; if spaces are allowed and we're directly in between + // two triggers + if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) { + match[0] += ' ' + to += 1 + } + + // If the $position is located within the matched substring, return that range + if (from < $position.pos && to >= $position.pos) { + position = { + range: { + from, + to, + }, + query: match[0].slice(char.length), + text: match[0], + } + } + } + + match = regexp.exec(text) + } + + return position + } +} + +export function Suggestions({ + matcher = { + char: '@', + allowSpaces: false, + startOfLine: false, + }, + appendText = null, + suggestionClass = 'suggestion', + command = () => false, + items = [], + onEnter = (props: any) => false, + onChange = (props: any) => false, + onExit = (props: any) => false, + onKeyDown = (props: any) => false, + onFilter = (searchItems: any[], query: string) => { + if (!query) { + return searchItems + } + + return searchItems + .filter(item => JSON.stringify(item).toLowerCase().includes(query.toLowerCase())) + }, +}) { + return new Plugin({ + key: new PluginKey('suggestions'), + + view() { + return { + update: async (view, prevState) => { + const prev = this.key?.getState(prevState) + const next = this.key?.getState(view.state) + + // See how the state changed + 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.query !== next.query + const handleStart = started || moved + const handleChange = changed && !moved + const handleExit = stopped || moved + + // Cancel when suggestion isn't active + if (!handleStart && !handleChange && !handleExit) { + return + } + + const state = handleExit ? prev : next + const decorationNode = document.querySelector(`[data-decoration-id="${state.decorationId}"]`) + + // build a virtual node for popper.js or tippy.js + // this can be used for building popups without a DOM node + const virtualNode = decorationNode ? { + getBoundingClientRect() { + return decorationNode.getBoundingClientRect() + }, + clientWidth: decorationNode.clientWidth, + clientHeight: decorationNode.clientHeight, + } : null + + const props = { + view, + range: state.range, + query: state.query, + text: state.text, + decorationNode, + virtualNode, + items: (handleChange || handleStart) + // @ts-ignore + ? await onFilter(Array.isArray(items) ? items : await items(), state.query) + : [], + command: () => { + console.log('command') + }, + // command: ({ range, attrs }) => { + // command({ + // range, + // attrs, + // schema: view.state.schema, + // })(view.state, view.dispatch, view) + + // if (appendText) { + // insertText(appendText)(view.state, view.dispatch, view) + // } + // }, + } + + // Trigger the hooks when necessary + if (handleExit) { + onExit(props) + } + + if (handleChange) { + onChange(props) + } + + if (handleStart) { + onEnter(props) + } + }, + } + }, + + state: { + + // Initialize the plugin's internal state. + init() { + return { + active: false, + range: {}, + query: null, + text: null, + } + }, + + // Apply changes to the plugin state from a view transaction. + apply(tr, prev) { + const { selection } = tr + const next = { ...prev } + + // We can only be suggesting if there is no selection + if (selection.from === selection.to) { + // Reset active state if we just left the previous suggestion range + if (selection.from < prev.range.from || selection.from > prev.range.to) { + next.active = false + } + + // Try to match against where our cursor currently is + const $position = selection.$from + const match = triggerCharacter(matcher)($position) + const decorationId = (Math.random() + 1).toString(36).substr(2, 5) + + // If we found a match, update the current state to show it + if (match) { + next.active = true + next.decorationId = prev.decorationId ? prev.decorationId : decorationId + next.range = match.range + next.query = match.query + next.text = match.text + } else { + next.active = false + } + } else { + next.active = false + } + + // Make sure to empty the range if suggestion is inactive + if (!next.active) { + next.decorationId = null + next.range = {} + next.query = null + next.text = null + } + + return next + }, + }, + + props: { + // Call the keydown hook if suggestion is active. + handleKeyDown(view, event) { + const { active, range } = this.getState(view.state) + + if (!active) return false + + return onKeyDown({ view, event, range }) + }, + + // Setup decorator on the currently active suggestion. + decorations(editorState) { + const { active, range, decorationId } = this.getState(editorState) + + if (!active) return null + + return DecorationSet.create(editorState.doc, [ + Decoration.inline(range.from, range.to, { + nodeName: 'span', + class: suggestionClass, + 'data-decoration-id': decorationId, + }), + ]) + }, + }, + }) +} From eb695878a0a2b706298c3311f10b2fa2efb9177c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Thu, 17 Dec 2020 17:13:35 +0100 Subject: [PATCH 05/28] add basic mention extension --- docs/src/demos/Nodes/Mention/index.spec.js | 5 ++ docs/src/demos/Nodes/Mention/index.vue | 44 +++++++++++++++++ docs/src/docPages/api/nodes/mention.md | 17 +++++-- packages/extension-mention/README.md | 14 ++++++ packages/extension-mention/package.json | 30 ++++++++++++ packages/extension-mention/src/index.ts | 5 ++ packages/extension-mention/src/mention.ts | 49 +++++++++++++++++++ .../{suggestions => suggestion}/README.md | 0 .../{suggestions => suggestion}/package.json | 14 +++--- packages/suggestion/src/index.ts | 5 ++ .../src/suggestion.ts} | 2 +- packages/suggestions/src/index.ts | 5 -- 12 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 docs/src/demos/Nodes/Mention/index.spec.js create mode 100644 docs/src/demos/Nodes/Mention/index.vue create mode 100644 packages/extension-mention/README.md create mode 100644 packages/extension-mention/package.json create mode 100644 packages/extension-mention/src/index.ts create mode 100644 packages/extension-mention/src/mention.ts rename packages/{suggestions => suggestion}/README.md (100%) rename packages/{suggestions => suggestion}/package.json (58%) create mode 100644 packages/suggestion/src/index.ts rename packages/{suggestions/src/suggestions.ts => suggestion/src/suggestion.ts} (99%) delete mode 100644 packages/suggestions/src/index.ts diff --git a/docs/src/demos/Nodes/Mention/index.spec.js b/docs/src/demos/Nodes/Mention/index.spec.js new file mode 100644 index 00000000..773694f9 --- /dev/null +++ b/docs/src/demos/Nodes/Mention/index.spec.js @@ -0,0 +1,5 @@ +context('/api/nodes/mention', () => { + before(() => { + cy.visit('/api/nodes/mention') + }) +}) diff --git a/docs/src/demos/Nodes/Mention/index.vue b/docs/src/demos/Nodes/Mention/index.vue new file mode 100644 index 00000000..a13fd656 --- /dev/null +++ b/docs/src/demos/Nodes/Mention/index.vue @@ -0,0 +1,44 @@ + + + diff --git a/docs/src/docPages/api/nodes/mention.md b/docs/src/docPages/api/nodes/mention.md index e74457ea..3d15ae56 100644 --- a/docs/src/docPages/api/nodes/mention.md +++ b/docs/src/docPages/api/nodes/mention.md @@ -1,7 +1,16 @@ # Mention -:::pro Fund the development 💖 -We need your support to maintain, update, support and develop tiptap 2. If you’re waiting for this extension, [become a sponsor and fund open source](/sponsor). -::: +## Installation +```bash +# with npm +npm install @tiptap/extension-mention -TODO +# with Yarn +yarn add @tiptap/extension-mention +``` + +## Source code +[packages/extension-mention/](https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-mention/) + +## Usage + diff --git a/packages/extension-mention/README.md b/packages/extension-mention/README.md new file mode 100644 index 00000000..e4452298 --- /dev/null +++ b/packages/extension-mention/README.md @@ -0,0 +1,14 @@ +# @tiptap/extension-mention +[![Version](https://img.shields.io/npm/v/@tiptap/extension-mention.svg?label=version)](https://www.npmjs.com/package/@tiptap/extension-mention) +[![Downloads](https://img.shields.io/npm/dm/@tiptap/extension-mention.svg)](https://npmcharts.com/compare/tiptap?minimal=true) +[![License](https://img.shields.io/npm/l/@tiptap/extension-mention.svg)](https://www.npmjs.com/package/@tiptap/extension-mention) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/ueberdosis) + +## Introduction +tiptap is a headless wrapper around [ProseMirror](https://ProseMirror.net) – a toolkit for building rich text WYSIWYG editors, which is already in use at many well-known companies such as *New York Times*, *The Guardian* or *Atlassian*. + +## Offical Documentation +Documentation can be found on the [tiptap website](https://tiptap.dev). + +## License +tiptap is open-sourced software licensed under the [MIT license](https://github.com/ueberdosis/tiptap-next/blob/main/LICENSE.md). diff --git a/packages/extension-mention/package.json b/packages/extension-mention/package.json new file mode 100644 index 00000000..355fb52f --- /dev/null +++ b/packages/extension-mention/package.json @@ -0,0 +1,30 @@ +{ + "name": "@tiptap/extension-mention", + "description": "mention extension for tiptap", + "version": "2.0.0-alpha.0", + "homepage": "https://tiptap.dev", + "keywords": [ + "tiptap", + "tiptap extension" + ], + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "main": "dist/tiptap-extension-mention.cjs.js", + "umd": "dist/tiptap-extension-mention.umd.js", + "module": "dist/tiptap-extension-mention.esm.js", + "unpkg": "dist/tiptap-extension-mention.bundle.umd.min.js", + "types": "dist/packages/extension-mention/src/index.d.ts", + "files": [ + "src", + "dist" + ], + "peerDependencies": { + "@tiptap/core": "^2.0.0-alpha.6" + }, + "dependencies": { + "@tiptap/suggestion": "^2.0.0-alpha.0" + } +} diff --git a/packages/extension-mention/src/index.ts b/packages/extension-mention/src/index.ts new file mode 100644 index 00000000..0c1654ac --- /dev/null +++ b/packages/extension-mention/src/index.ts @@ -0,0 +1,5 @@ +import { Mention } from './mention' + +export * from './mention' + +export default Mention diff --git a/packages/extension-mention/src/mention.ts b/packages/extension-mention/src/mention.ts new file mode 100644 index 00000000..ae4add0b --- /dev/null +++ b/packages/extension-mention/src/mention.ts @@ -0,0 +1,49 @@ +import { Node } from '@tiptap/core' +import Suggestion from '@tiptap/suggestion' + +export const Mention = Node.create({ + name: 'mention', + + group: 'inline', + + inline: true, + + selectable: false, + + atom: true, + + addAttributes() { + return { + id: { + default: null, + }, + label: { + default: null, + }, + } + }, + + parseHTML() { + return [ + { + tag: 'span[data-mention]', + }, + ] + }, + + renderHTML({ node, HTMLAttributes }) { + return ['span', HTMLAttributes, `@${node.attrs.label}`] + }, + + addProseMirrorPlugins() { + return [ + Suggestion({}), + ] + }, +}) + +declare module '@tiptap/core' { + interface AllExtensions { + Mention: typeof Mention, + } +} diff --git a/packages/suggestions/README.md b/packages/suggestion/README.md similarity index 100% rename from packages/suggestions/README.md rename to packages/suggestion/README.md diff --git a/packages/suggestions/package.json b/packages/suggestion/package.json similarity index 58% rename from packages/suggestions/package.json rename to packages/suggestion/package.json index 2f7b0f62..a47cccf6 100644 --- a/packages/suggestions/package.json +++ b/packages/suggestion/package.json @@ -1,6 +1,6 @@ { - "name": "@tiptap/suggestions", - "description": "suggestions", + "name": "@tiptap/suggestion", + "description": "suggestion plugin for tiptap", "version": "2.0.0-alpha.0", "homepage": "https://tiptap.dev", "keywords": [ @@ -12,11 +12,11 @@ "type": "github", "url": "https://github.com/sponsors/ueberdosis" }, - "main": "dist/tiptap-suggestions.cjs.js", - "umd": "dist/tiptap-suggestions.umd.js", - "module": "dist/tiptap-suggestions.esm.js", - "unpkg": "dist/tiptap-suggestions.bundle.umd.min.js", - "types": "dist/packages/suggestions/src/index.d.ts", + "main": "dist/tiptap-suggestion.cjs.js", + "umd": "dist/tiptap-suggestion.umd.js", + "module": "dist/tiptap-suggestion.esm.js", + "unpkg": "dist/tiptap-suggestion.bundle.umd.min.js", + "types": "dist/packages/suggestion/src/index.d.ts", "files": [ "src", "dist" diff --git a/packages/suggestion/src/index.ts b/packages/suggestion/src/index.ts new file mode 100644 index 00000000..43cdefd6 --- /dev/null +++ b/packages/suggestion/src/index.ts @@ -0,0 +1,5 @@ +import { Suggestion } from './suggestion' + +export * from './suggestion' + +export default Suggestion diff --git a/packages/suggestions/src/suggestions.ts b/packages/suggestion/src/suggestion.ts similarity index 99% rename from packages/suggestions/src/suggestions.ts rename to packages/suggestion/src/suggestion.ts index 7577673c..142aa97d 100644 --- a/packages/suggestions/src/suggestions.ts +++ b/packages/suggestion/src/suggestion.ts @@ -68,7 +68,7 @@ function triggerCharacter({ } } -export function Suggestions({ +export function Suggestion({ matcher = { char: '@', allowSpaces: false, diff --git a/packages/suggestions/src/index.ts b/packages/suggestions/src/index.ts deleted file mode 100644 index 69d382d8..00000000 --- a/packages/suggestions/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Suggestions } from './suggestions' - -export * from './suggestions' - -export default Suggestions From 44a026416b2ac72bfdace37a8592983e8bc85706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 15 Jan 2021 09:25:50 +0100 Subject: [PATCH 06/28] refactoring --- packages/suggestion/src/suggestion.ts | 124 ++++++++++++++------------ 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/packages/suggestion/src/suggestion.ts b/packages/suggestion/src/suggestion.ts index 142aa97d..9300d7c5 100644 --- a/packages/suggestion/src/suggestion.ts +++ b/packages/suggestion/src/suggestion.ts @@ -2,78 +2,80 @@ import { Plugin, PluginKey } from 'prosemirror-state' import { ResolvedPos } from 'prosemirror-model' import { Decoration, DecorationSet } from 'prosemirror-view' +export interface Trigger { + char: string, + allowSpaces: boolean, + startOfLine: boolean, + $position: ResolvedPos, +} + // Create a matcher that matches when a specific character is typed. Useful for @mentions and #tags. -function triggerCharacter({ - char = '@', - allowSpaces = false, - startOfLine = false, -}) { +function triggerCharacter(config: Trigger) { + const { + char, allowSpaces, startOfLine, $position, + } = config - return ($position: ResolvedPos) => { - // cancel if top level node - if ($position.depth <= 0) { - return false - } + // cancel if top level node + if ($position.depth <= 0) { + return false + } - // Matching expressions used for later - const escapedChar = `\\${char}` - const suffix = new RegExp(`\\s${escapedChar}$`) - const prefix = startOfLine ? '^' : '' - const regexp = allowSpaces - ? new RegExp(`${prefix}${escapedChar}.*?(?=\\s${escapedChar}|$)`, 'gm') - : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\s${escapedChar}]*`, 'gm') + // Matching expressions used for later + const escapedChar = `\\${char}` + const suffix = new RegExp(`\\s${escapedChar}$`) + const prefix = startOfLine ? '^' : '' + const regexp = allowSpaces + ? new RegExp(`${prefix}${escapedChar}.*?(?=\\s${escapedChar}|$)`, 'gm') + : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\s${escapedChar}]*`, 'gm') - // Lookup the boundaries of the current node - const textFrom = $position.before() - const textTo = $position.end() - const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0') + // Lookup the boundaries of the current node + const textFrom = $position.before() + const textTo = $position.end() + const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0') - let match = regexp.exec(text) - let position + let match = regexp.exec(text) + let position - while (match !== null) { - // JavaScript doesn't have lookbehinds; this hacks a check that first character is " " - // or the line beginning - const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index) + while (match !== null) { + // JavaScript doesn't have lookbehinds; this hacks a check that first character is " " + // or the line beginning + const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index) - if (/^[\s\0]?$/.test(matchPrefix)) { - // The absolute position of the match in the document - const from = match.index + $position.start() - let to = from + match[0].length + if (/^[\s\0]?$/.test(matchPrefix)) { + // The absolute position of the match in the document + const from = match.index + $position.start() + let to = from + match[0].length - // Edge case handling; if spaces are allowed and we're directly in between - // two triggers - if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) { - match[0] += ' ' - to += 1 - } - - // If the $position is located within the matched substring, return that range - if (from < $position.pos && to >= $position.pos) { - position = { - range: { - from, - to, - }, - query: match[0].slice(char.length), - text: match[0], - } - } + // Edge case handling; if spaces are allowed and we're directly in between + // two triggers + if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) { + match[0] += ' ' + to += 1 } - match = regexp.exec(text) + // If the $position is located within the matched substring, return that range + if (from < $position.pos && to >= $position.pos) { + position = { + range: { + from, + to, + }, + query: match[0].slice(char.length), + text: match[0], + } + } } - return position + match = regexp.exec(text) } + + return position } export function Suggestion({ - matcher = { - char: '@', - allowSpaces: false, - startOfLine: false, - }, + char = '@', + allowSpaces = false, + startOfLine = false, appendText = null, suggestionClass = 'suggestion', command = () => false, @@ -171,7 +173,6 @@ export function Suggestion({ }, state: { - // Initialize the plugin's internal state. init() { return { @@ -196,9 +197,16 @@ export function Suggestion({ // Try to match against where our cursor currently is const $position = selection.$from - const match = triggerCharacter(matcher)($position) + const match = triggerCharacter({ + char, + allowSpaces, + startOfLine, + $position, + }) const decorationId = (Math.random() + 1).toString(36).substr(2, 5) + console.log({ match }) + // If we found a match, update the current state to show it if (match) { next.active = true From 8e23627f61c80298613d9918f3b23d3a5effe53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 15 Jan 2021 09:55:15 +0100 Subject: [PATCH 07/28] maybe improve text match --- packages/suggestion/src/suggestion.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/suggestion/src/suggestion.ts b/packages/suggestion/src/suggestion.ts index 9300d7c5..4aa765b5 100644 --- a/packages/suggestion/src/suggestion.ts +++ b/packages/suggestion/src/suggestion.ts @@ -30,7 +30,10 @@ function triggerCharacter(config: Trigger) { // Lookup the boundaries of the current node const textFrom = $position.before() - const textTo = $position.end() + + // Only look up to the cursor, old behavior: textTo = $position.end() + const textTo = $position.pos + const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0') let match = regexp.exec(text) @@ -196,12 +199,11 @@ export function Suggestion({ } // Try to match against where our cursor currently is - const $position = selection.$from const match = triggerCharacter({ char, allowSpaces, startOfLine, - $position, + $position: selection.$from, }) const decorationId = (Math.random() + 1).toString(36).substr(2, 5) From be9167589eb08b40da13f43c8ef61ebf0328df54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 15 Jan 2021 14:49:28 +0100 Subject: [PATCH 08/28] refactoring --- .../suggestion/src/findSuggestionMatch.ts | 82 ++++++++++++ packages/suggestion/src/getVirtualNode.ts | 9 ++ packages/suggestion/src/suggestion.ts | 117 +++--------------- 3 files changed, 111 insertions(+), 97 deletions(-) create mode 100644 packages/suggestion/src/findSuggestionMatch.ts create mode 100644 packages/suggestion/src/getVirtualNode.ts diff --git a/packages/suggestion/src/findSuggestionMatch.ts b/packages/suggestion/src/findSuggestionMatch.ts new file mode 100644 index 00000000..23a1c088 --- /dev/null +++ b/packages/suggestion/src/findSuggestionMatch.ts @@ -0,0 +1,82 @@ +import { ResolvedPos } from 'prosemirror-model' + +export interface Trigger { + char: string, + allowSpaces: boolean, + startOfLine: boolean, + $position: ResolvedPos, +} + +export type SuggestionMatch = { + range: { + from: number, + to: number, + }, + query: string, + text: string, +} | null + +export function findSuggestionMatch(config: Trigger): SuggestionMatch { + const { + char, allowSpaces, startOfLine, $position, + } = config + + // cancel if top level node + if ($position.depth <= 0) { + return null + } + + // Matching expressions used for later + const escapedChar = `\\${char}` + const suffix = new RegExp(`\\s${escapedChar}$`) + const prefix = startOfLine ? '^' : '' + const regexp = allowSpaces + ? new RegExp(`${prefix}${escapedChar}.*?(?=\\s${escapedChar}|$)`, 'gm') + : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\s${escapedChar}]*`, 'gm') + + // Lookup the boundaries of the current node + const textFrom = $position.before() + + // Only look up to the cursor, old behavior: textTo = $position.end() + const textTo = $position.pos + + const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0') + + let match = regexp.exec(text) + let position = null + + while (match !== null) { + // JavaScript doesn't have lookbehinds; this hacks a check that first character is " " + // or the line beginning + const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index) + + if (/^[\s\0]?$/.test(matchPrefix)) { + // The absolute position of the match in the document + const from = match.index + $position.start() + let to = from + match[0].length + + // Edge case handling; if spaces are allowed and we're directly in between + // two triggers + if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) { + match[0] += ' ' + to += 1 + } + + // If the $position is located within the matched substring, return that range + if (from < $position.pos && to >= $position.pos) { + position = { + range: { + from, + to, + }, + query: match[0].slice(char.length), + text: match[0], + } + } + } + + match = regexp.exec(text) + } + + return position +} diff --git a/packages/suggestion/src/getVirtualNode.ts b/packages/suggestion/src/getVirtualNode.ts new file mode 100644 index 00000000..7f2ca9ab --- /dev/null +++ b/packages/suggestion/src/getVirtualNode.ts @@ -0,0 +1,9 @@ +export function getVirtualNode(node: Element) { + return { + getBoundingClientRect() { + return node.getBoundingClientRect() + }, + clientWidth: node.clientWidth, + clientHeight: node.clientHeight, + } +} diff --git a/packages/suggestion/src/suggestion.ts b/packages/suggestion/src/suggestion.ts index 4aa765b5..01b9a2cd 100644 --- a/packages/suggestion/src/suggestion.ts +++ b/packages/suggestion/src/suggestion.ts @@ -1,79 +1,7 @@ import { Plugin, PluginKey } from 'prosemirror-state' -import { ResolvedPos } from 'prosemirror-model' import { Decoration, DecorationSet } from 'prosemirror-view' - -export interface Trigger { - char: string, - allowSpaces: boolean, - startOfLine: boolean, - $position: ResolvedPos, -} - -// Create a matcher that matches when a specific character is typed. Useful for @mentions and #tags. -function triggerCharacter(config: Trigger) { - const { - char, allowSpaces, startOfLine, $position, - } = config - - // cancel if top level node - if ($position.depth <= 0) { - return false - } - - // Matching expressions used for later - const escapedChar = `\\${char}` - const suffix = new RegExp(`\\s${escapedChar}$`) - const prefix = startOfLine ? '^' : '' - const regexp = allowSpaces - ? new RegExp(`${prefix}${escapedChar}.*?(?=\\s${escapedChar}|$)`, 'gm') - : new RegExp(`${prefix}(?:^)?${escapedChar}[^\\s${escapedChar}]*`, 'gm') - - // Lookup the boundaries of the current node - const textFrom = $position.before() - - // Only look up to the cursor, old behavior: textTo = $position.end() - const textTo = $position.pos - - const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0') - - let match = regexp.exec(text) - let position - - while (match !== null) { - // JavaScript doesn't have lookbehinds; this hacks a check that first character is " " - // or the line beginning - const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index) - - if (/^[\s\0]?$/.test(matchPrefix)) { - // The absolute position of the match in the document - const from = match.index + $position.start() - let to = from + match[0].length - - // Edge case handling; if spaces are allowed and we're directly in between - // two triggers - if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) { - match[0] += ' ' - to += 1 - } - - // If the $position is located within the matched substring, return that range - if (from < $position.pos && to >= $position.pos) { - position = { - range: { - from, - to, - }, - query: match[0].slice(char.length), - text: match[0], - } - } - } - - match = regexp.exec(text) - } - - return position -} +import { findSuggestionMatch } from './findSuggestionMatch' +import { getVirtualNode } from './getVirtualNode' export function Suggestion({ char = '@', @@ -84,7 +12,7 @@ export function Suggestion({ command = () => false, items = [], onEnter = (props: any) => false, - onChange = (props: any) => false, + onUpdate = (props: any) => false, onExit = (props: any) => false, onKeyDown = (props: any) => false, onFilter = (searchItems: any[], query: string) => { @@ -121,24 +49,17 @@ export function Suggestion({ const state = handleExit ? prev : next const decorationNode = document.querySelector(`[data-decoration-id="${state.decorationId}"]`) - - // build a virtual node for popper.js or tippy.js - // this can be used for building popups without a DOM node - const virtualNode = decorationNode ? { - getBoundingClientRect() { - return decorationNode.getBoundingClientRect() - }, - clientWidth: decorationNode.clientWidth, - clientHeight: decorationNode.clientHeight, - } : null - const props = { view, range: state.range, query: state.query, text: state.text, decorationNode, - virtualNode, + // build a virtual node for popper.js or tippy.js + // this can be used for building popups without a DOM node + virtualNode: decorationNode + ? getVirtualNode(decorationNode) + : null, items: (handleChange || handleStart) // @ts-ignore ? await onFilter(Array.isArray(items) ? items : await items(), state.query) @@ -165,7 +86,7 @@ export function Suggestion({ } if (handleChange) { - onChange(props) + onUpdate(props) } if (handleStart) { @@ -199,15 +120,13 @@ export function Suggestion({ } // Try to match against where our cursor currently is - const match = triggerCharacter({ + const match = findSuggestionMatch({ char, allowSpaces, startOfLine, $position: selection.$from, }) - const decorationId = (Math.random() + 1).toString(36).substr(2, 5) - - console.log({ match }) + const decorationId = `id_${Math.floor(Math.random() * 0xFFFFFFFF)}` // If we found a match, update the current state to show it if (match) { @@ -240,18 +159,22 @@ export function Suggestion({ handleKeyDown(view, event) { const { active, range } = this.getState(view.state) - if (!active) return false + if (!active) { + return false + } return onKeyDown({ view, event, range }) }, // Setup decorator on the currently active suggestion. - decorations(editorState) { - const { active, range, decorationId } = this.getState(editorState) + decorations(state) { + const { active, range, decorationId } = this.getState(state) - if (!active) return null + if (!active) { + return null + } - return DecorationSet.create(editorState.doc, [ + return DecorationSet.create(state.doc, [ Decoration.inline(range.from, range.to, { nodeName: 'span', class: suggestionClass, From 58b7aa7baaa449aef5752d9c34c0d35c1fa2b42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Fri, 15 Jan 2021 15:58:39 +0100 Subject: [PATCH 09/28] refactoring --- packages/extension-mention/src/mention.ts | 14 ++++++- packages/suggestion/src/suggestion.ts | 46 ++++++++++++++--------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/packages/extension-mention/src/mention.ts b/packages/extension-mention/src/mention.ts index ae4add0b..5fb7f557 100644 --- a/packages/extension-mention/src/mention.ts +++ b/packages/extension-mention/src/mention.ts @@ -1,9 +1,17 @@ import { Node } from '@tiptap/core' import Suggestion from '@tiptap/suggestion' +export interface MentionOptions { + renderer: any, +} + export const Mention = Node.create({ name: 'mention', + defaultOptions: { + renderer: null, + }, + group: 'inline', inline: true, @@ -37,7 +45,11 @@ export const Mention = Node.create({ addProseMirrorPlugins() { return [ - Suggestion({}), + Suggestion({ + editor: this.editor, + char: '@', + renderer: this.options.renderer, + }), ] }, }) diff --git a/packages/suggestion/src/suggestion.ts b/packages/suggestion/src/suggestion.ts index 01b9a2cd..19213137 100644 --- a/packages/suggestion/src/suggestion.ts +++ b/packages/suggestion/src/suggestion.ts @@ -1,29 +1,40 @@ +import { Editor } from '@tiptap/core' import { Plugin, PluginKey } from 'prosemirror-state' import { Decoration, DecorationSet } from 'prosemirror-view' import { findSuggestionMatch } from './findSuggestionMatch' import { getVirtualNode } from './getVirtualNode' +export interface SuggestionOptions { + editor: Editor, + char?: string, + allowSpaces?: boolean, + startOfLine?: boolean, + suggestionClass?: string, + command?: () => any, + items?: (query: string) => any[], + onStart?: (props: any) => any, + onUpdate?: (props: any) => any, + onExit?: (props: any) => any, + onKeyDown?: (props: any) => any, + renderer?: any, +} + export function Suggestion({ + editor, char = '@', allowSpaces = false, startOfLine = false, - appendText = null, suggestionClass = 'suggestion', - command = () => false, - items = [], - onEnter = (props: any) => false, - onUpdate = (props: any) => false, - onExit = (props: any) => false, - onKeyDown = (props: any) => false, - onFilter = (searchItems: any[], query: string) => { - if (!query) { - return searchItems - } + command = () => null, + items = () => [], + onStart = () => null, + onUpdate = () => null, + onExit = () => null, + onKeyDown = () => null, + renderer = () => ({}), +}: SuggestionOptions) { + // const testRenderer = renderer() - return searchItems - .filter(item => JSON.stringify(item).toLowerCase().includes(query.toLowerCase())) - }, -}) { return new Plugin({ key: new PluginKey('suggestions'), @@ -61,8 +72,7 @@ export function Suggestion({ ? getVirtualNode(decorationNode) : null, items: (handleChange || handleStart) - // @ts-ignore - ? await onFilter(Array.isArray(items) ? items : await items(), state.query) + ? await items(state.query) : [], command: () => { console.log('command') @@ -90,7 +100,7 @@ export function Suggestion({ } if (handleStart) { - onEnter(props) + onStart(props) } }, } From 34f6a0d9cec44dcf0fccf814c6828b3ae2f6384f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Mon, 18 Jan 2021 12:40:05 +0100 Subject: [PATCH 10/28] rename VueRenderer to VueNodeViewRenderer --- .../vue/src/{VueRenderer.ts => VueNodeViewRenderer.ts} | 8 ++++---- packages/vue/src/index.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename packages/vue/src/{VueRenderer.ts => VueNodeViewRenderer.ts} (96%) diff --git a/packages/vue/src/VueRenderer.ts b/packages/vue/src/VueNodeViewRenderer.ts similarity index 96% rename from packages/vue/src/VueRenderer.ts rename to packages/vue/src/VueNodeViewRenderer.ts index 4215b9c2..58f70772 100644 --- a/packages/vue/src/VueRenderer.ts +++ b/packages/vue/src/VueNodeViewRenderer.ts @@ -16,7 +16,7 @@ function getComponentFromElement(element: HTMLElement): Vue { return element.__vue__ } -interface VueRendererOptions { +interface VueNodeViewRendererOptions { stopEvent: ((event: Event) => boolean) | null, update: ((node: ProseMirrorNode, decorations: Decoration[]) => boolean) | null, } @@ -39,12 +39,12 @@ class VueNodeView implements NodeView { isDragging = false - options: VueRendererOptions = { + options: VueNodeViewRendererOptions = { stopEvent: null, update: null, } - constructor(component: Vue | VueConstructor, props: NodeViewRendererProps, options?: Partial) { + constructor(component: Vue | VueConstructor, props: NodeViewRendererProps, options?: Partial) { this.options = { ...this.options, ...options } this.editor = props.editor this.extension = props.extension @@ -314,7 +314,7 @@ class VueNodeView implements NodeView { } -export default function VueRenderer(component: Vue | VueConstructor, options?: Partial): NodeViewRenderer { +export default function VueNodeViewRenderer(component: Vue | VueConstructor, options?: Partial): NodeViewRenderer { return (props: NodeViewRendererProps) => { // try to get the parent component // this is important for vue devtools to show the component hierarchy correctly diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index d668e82c..9640f8c9 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -1,3 +1,3 @@ export * from '@tiptap/core' -export { default as VueRenderer } from './VueRenderer' +export { default as VueNodeViewRenderer } from './VueNodeViewRenderer' export { default as EditorContent } from './components/EditorContent' From 18d3cbdef8d2b602318a17a628637c104e72a8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Mon, 18 Jan 2021 12:40:13 +0100 Subject: [PATCH 11/28] add tippy --- docs/package.json | 1 + docs/src/demos/Nodes/Mention/index.vue | 50 ++++++++++++++++++++++- packages/extension-mention/src/mention.ts | 12 +++--- packages/suggestion/src/suggestion.ts | 34 ++++++++------- yarn.lock | 12 ++++++ 5 files changed, 87 insertions(+), 22 deletions(-) diff --git a/docs/package.json b/docs/package.json index d456261e..3c850ae1 100644 --- a/docs/package.json +++ b/docs/package.json @@ -25,6 +25,7 @@ "remark-toc": "^7.0.0", "remixicon": "^2.5.0", "simplify-js": "^1.2.4", + "tippy.js": "^6.2.7", "vue-github-button": "^1.1.2", "vue-live": "^1.16.0", "y-indexeddb": "^9.0.6", diff --git a/docs/src/demos/Nodes/Mention/index.vue b/docs/src/demos/Nodes/Mention/index.vue index a13fd656..5ab67006 100644 --- a/docs/src/demos/Nodes/Mention/index.vue +++ b/docs/src/demos/Nodes/Mention/index.vue @@ -5,6 +5,8 @@ + + diff --git a/docs/src/demos/Nodes/Mention/index.vue b/docs/src/demos/Nodes/Mention/index.vue index 5ab67006..aa9fc9a6 100644 --- a/docs/src/demos/Nodes/Mention/index.vue +++ b/docs/src/demos/Nodes/Mention/index.vue @@ -5,14 +5,13 @@ - diff --git a/docs/src/demos/Nodes/Mention/index.vue b/docs/src/demos/Nodes/Mention/index.vue index dd671d14..de380edc 100644 --- a/docs/src/demos/Nodes/Mention/index.vue +++ b/docs/src/demos/Nodes/Mention/index.vue @@ -58,6 +58,9 @@ export default { onUpdate(props) { component.updateProps(props) }, + onKeyDown(props) { + return component.vm.onKeyDown(props) + }, onExit() { popup[0].destroy() component.destroy() diff --git a/packages/suggestion/src/suggestion.ts b/packages/suggestion/src/suggestion.ts index 809547af..baa0898c 100644 --- a/packages/suggestion/src/suggestion.ts +++ b/packages/suggestion/src/suggestion.ts @@ -13,10 +13,10 @@ export interface SuggestionOptions { command?: () => any, items?: (query: string) => any[], renderer?: () => { - onStart?: (props: any) => any, - onUpdate?: (props: any) => any, - onExit?: (props: any) => any, - onKeyDown?: (props: any) => any, + onStart?: (props: any) => void, + onUpdate?: (props: any) => void, + onExit?: (props: any) => void, + onKeyDown?: (props: any) => boolean, }, } From a7ac9a7ef439fbc2a753eab90cbf898807b47b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Mon, 18 Jan 2021 16:54:02 +0100 Subject: [PATCH 16/28] improve styling --- docs/src/demos/Nodes/Mention/MentionList.vue | 19 ++++++++++++++----- docs/src/demos/Nodes/Mention/index.vue | 2 +- packages/suggestion/src/suggestion.ts | 3 +-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/src/demos/Nodes/Mention/MentionList.vue b/docs/src/demos/Nodes/Mention/MentionList.vue index 09ae201e..6812eef2 100644 --- a/docs/src/demos/Nodes/Mention/MentionList.vue +++ b/docs/src/demos/Nodes/Mention/MentionList.vue @@ -74,14 +74,23 @@ export default { diff --git a/docs/src/demos/Nodes/Mention/index.vue b/docs/src/demos/Nodes/Mention/index.vue index de380edc..4587ba05 100644 --- a/docs/src/demos/Nodes/Mention/index.vue +++ b/docs/src/demos/Nodes/Mention/index.vue @@ -32,7 +32,7 @@ export default { Text, Mention.configure({ items: query => { - return ['foo', 'bar'].filter(item => item.startsWith(query)) + return ['Hans', 'Philipp', 'Kris'].filter(item => item.startsWith(query)) }, renderer: () => { let component diff --git a/packages/suggestion/src/suggestion.ts b/packages/suggestion/src/suggestion.ts index baa0898c..9f553606 100644 --- a/packages/suggestion/src/suggestion.ts +++ b/packages/suggestion/src/suggestion.ts @@ -178,8 +178,7 @@ export function Suggestion({ return false } - // return onKeyDown({ view, event, range }) - return testRenderer?.onKeyDown?.({ view, event, range }) + return testRenderer?.onKeyDown?.({ view, event, range }) || false }, // Setup decorator on the currently active suggestion. From b4032a17cb924ab5726967542897306d88daf942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Mon, 18 Jan 2021 17:02:03 +0100 Subject: [PATCH 17/28] refactoring --- docs/src/demos/Nodes/Mention/MentionList.vue | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/src/demos/Nodes/Mention/MentionList.vue b/docs/src/demos/Nodes/Mention/MentionList.vue index 6812eef2..5a625751 100644 --- a/docs/src/demos/Nodes/Mention/MentionList.vue +++ b/docs/src/demos/Nodes/Mention/MentionList.vue @@ -1,13 +1,14 @@ @@ -18,6 +19,11 @@ export default { type: Array, default: () => [], }, + + command: { + type: Function, + default: () => true, + }, }, data() { @@ -61,10 +67,14 @@ export default { }, enterHandler() { - const item = this.items[this.selectedIndex] + this.selectItem(this.selectedIndex) + }, + + selectItem(index) { + const item = this.items[index] if (item) { - console.log('select', item) + this.command(item) } }, }, @@ -85,6 +95,11 @@ export default { ; } .item { + display: block; + width: 100%; + text-align: left; + background: transparent; + border: none; padding: 0.2rem 0.5rem; &.is-selected, From ba5d38a621616877dc1d65168eff83410875f074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Mon, 18 Jan 2021 23:41:38 +0100 Subject: [PATCH 18/28] refactoring --- docs/src/demos/Nodes/Mention/index.vue | 4 +- docs/src/docPages/api/commands.md | 1 + packages/core/src/commands/replace.ts | 29 ++++++++++ packages/core/src/extensions/commands.ts | 2 + packages/extension-mention/src/mention.ts | 9 +++- packages/suggestion/src/suggestion.ts | 64 ++++++++--------------- 6 files changed, 64 insertions(+), 45 deletions(-) create mode 100644 packages/core/src/commands/replace.ts diff --git a/docs/src/demos/Nodes/Mention/index.vue b/docs/src/demos/Nodes/Mention/index.vue index 4587ba05..3c163d17 100644 --- a/docs/src/demos/Nodes/Mention/index.vue +++ b/docs/src/demos/Nodes/Mention/index.vue @@ -5,7 +5,7 @@ + + diff --git a/packages/extension-mention/src/mention.ts b/packages/extension-mention/src/mention.ts index 921b4a94..a7412c2c 100644 --- a/packages/extension-mention/src/mention.ts +++ b/packages/extension-mention/src/mention.ts @@ -1,4 +1,4 @@ -import { Node } from '@tiptap/core' +import { Node, mergeAttributes } from '@tiptap/core' import Suggestion, { SuggestionOptions } from '@tiptap/suggestion' export type MentionOptions = { @@ -57,7 +57,7 @@ export const Mention = Node.create({ }, renderHTML({ node, HTMLAttributes }) { - return ['span', HTMLAttributes, `@${node.attrs.id}`] + return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), `@${node.attrs.id}`] }, renderText({ node }) { From 3109f119803cb09a11f7ef65a81ffe8ab430ab15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Tue, 19 Jan 2021 20:45:18 +0100 Subject: [PATCH 27/28] refactoring --- packages/suggestion/src/suggestion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/suggestion/src/suggestion.ts b/packages/suggestion/src/suggestion.ts index 17c30152..685bf898 100644 --- a/packages/suggestion/src/suggestion.ts +++ b/packages/suggestion/src/suggestion.ts @@ -50,7 +50,7 @@ export function Suggestion({ const renderer = render?.() return new Plugin({ - key: new PluginKey('suggestions'), + key: new PluginKey('suggestion'), view() { return { From ab2d7d56c05e031791540ef91f53ddb12d1b4203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Ku=CC=88hn?= Date: Tue, 19 Jan 2021 22:29:46 +0100 Subject: [PATCH 28/28] refactoring --- packages/core/src/Extension.ts | 7 ++----- packages/core/src/Mark.ts | 7 ++----- packages/core/src/Node.ts | 7 ++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/packages/core/src/Extension.ts b/packages/core/src/Extension.ts index 3d74cc91..41e29459 100644 --- a/packages/core/src/Extension.ts +++ b/packages/core/src/Extension.ts @@ -171,13 +171,10 @@ export class Extension { return new Extension(config) } - configure(options?: Partial) { + configure(options: Partial = {}) { return Extension .create(this.config as ExtensionConfig) - .#configure({ - ...this.config.defaultOptions, - ...options, - }) + .#configure(options) } #configure = (options: Partial) => { diff --git a/packages/core/src/Mark.ts b/packages/core/src/Mark.ts index 857c02fe..230fc643 100644 --- a/packages/core/src/Mark.ts +++ b/packages/core/src/Mark.ts @@ -231,13 +231,10 @@ export class Mark { return new Mark(config) } - configure(options?: Partial) { + configure(options: Partial = {}) { return Mark .create(this.config as MarkConfig) - .#configure({ - ...this.config.defaultOptions, - ...options, - }) + .#configure(options) } #configure = (options: Partial) => { diff --git a/packages/core/src/Node.ts b/packages/core/src/Node.ts index 44accd6b..b831dfc3 100644 --- a/packages/core/src/Node.ts +++ b/packages/core/src/Node.ts @@ -298,13 +298,10 @@ export class Node { return new Node(config) } - configure(options?: Partial) { + configure(options: Partial = {}) { return Node .create(this.config as NodeConfig) - .#configure({ - ...this.config.defaultOptions, - ...options, - }) + .#configure(options) } #configure = (options: Partial) => {