Merge branch 'main' of github.com:ueberdosis/tiptap-next into main
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
import { VueRenderer } from '@tiptap/vue'
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue'
|
||||
import Component from './Component.vue'
|
||||
|
||||
export default Node.create({
|
||||
@@ -30,6 +30,6 @@ export default Node.create({
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return VueRenderer(Component)
|
||||
return VueNodeViewRenderer(Component)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
import { VueRenderer } from '@tiptap/vue'
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue'
|
||||
import Component from './Component.vue'
|
||||
|
||||
export default Node.create({
|
||||
@@ -24,6 +24,6 @@ export default Node.create({
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return VueRenderer(Component)
|
||||
return VueNodeViewRenderer(Component)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Node, mergeAttributes } from '@tiptap/core'
|
||||
import { VueRenderer } from '@tiptap/vue'
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue'
|
||||
import Component from './Component.vue'
|
||||
|
||||
export default Node.create({
|
||||
@@ -22,6 +22,6 @@ export default Node.create({
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return VueRenderer(Component)
|
||||
return VueNodeViewRenderer(Component)
|
||||
},
|
||||
})
|
||||
|
||||
112
docs/src/demos/Nodes/Mention/MentionList.vue
Normal file
112
docs/src/demos/Nodes/Mention/MentionList.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="items">
|
||||
<button
|
||||
class="item"
|
||||
:class="{ 'is-selected': index === selectedIndex }"
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
@click="selectItem(index)"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
command: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: 0,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
items() {
|
||||
this.selectedIndex = 0
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDown({ event }) {
|
||||
if (event.key === 'ArrowUp') {
|
||||
this.upHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
this.downHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
this.enterHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
upHandler() {
|
||||
this.selectedIndex = ((this.selectedIndex + this.items.length) - 1) % this.items.length
|
||||
},
|
||||
|
||||
downHandler() {
|
||||
this.selectedIndex = (this.selectedIndex + 1) % this.items.length
|
||||
},
|
||||
|
||||
enterHandler() {
|
||||
this.selectItem(this.selectedIndex)
|
||||
},
|
||||
|
||||
selectItem(index) {
|
||||
const item = this.items[index]
|
||||
|
||||
if (item) {
|
||||
this.command({ id: item })
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.items {
|
||||
position: relative;
|
||||
border-radius: 0.25rem;
|
||||
background: white;
|
||||
color: rgba(black, 0.8);
|
||||
overflow: hidden;
|
||||
font-size: 0.9rem;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.1),
|
||||
0px 10px 20px rgba(0, 0, 0, 0.1),
|
||||
;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0.2rem 0.5rem;
|
||||
|
||||
&.is-selected,
|
||||
&:hover {
|
||||
color: #A975FF;
|
||||
background: rgba(#A975FF, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
docs/src/demos/Nodes/Mention/index.spec.js
Normal file
5
docs/src/demos/Nodes/Mention/index.spec.js
Normal file
@@ -0,0 +1,5 @@
|
||||
context('/api/nodes/mention', () => {
|
||||
before(() => {
|
||||
cy.visit('/api/nodes/mention')
|
||||
})
|
||||
})
|
||||
100
docs/src/demos/Nodes/Mention/index.vue
Normal file
100
docs/src/demos/Nodes/Mention/index.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div v-if="editor">
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tippy from 'tippy.js'
|
||||
import { Editor, EditorContent, VueRenderer } from '@tiptap/vue'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
import MentionList from './MentionList'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestionOptions: {
|
||||
items: query => {
|
||||
return ['Hans', 'Philipp', 'Kris'].filter(item => item.startsWith(query))
|
||||
},
|
||||
render: () => {
|
||||
let component
|
||||
let popup
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
component = new VueRenderer(MentionList, {
|
||||
parent: this,
|
||||
propsData: props,
|
||||
})
|
||||
|
||||
popup = tippy('body', {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.body,
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: 'manual',
|
||||
placement: 'top-start',
|
||||
})
|
||||
},
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
})
|
||||
},
|
||||
onKeyDown(props) {
|
||||
return component.vm.onKeyDown(props)
|
||||
},
|
||||
onExit() {
|
||||
popup[0].destroy()
|
||||
component.destroy()
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>Hello <span data-mention="Hans"></span> and <span data-mention="Philipp"></span> and <span data-mention="Kris"></span>!</p>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.mention {
|
||||
color: #A975FF;
|
||||
background-color: rgba(#A975FF, 0.1);
|
||||
border-radius: 0.3rem;
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
</style>
|
||||
@@ -147,6 +147,7 @@ Have a look at all of the core commands listed below. They should give you a goo
|
||||
| .lift() | Removes an existing wrap. |
|
||||
| .liftEmptyBlock() | Lift block if empty. |
|
||||
| .newlineInCode() | Add a newline character in code. |
|
||||
| .replace() | Replaces text with a node within a range. |
|
||||
| .resetNodeAttributes() | Resets all node attributes to the default value. |
|
||||
| .selectParentNode() | Select the parent node. |
|
||||
| .setMark() | Add a mark with new attributes. |
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Suggestion
|
||||
|
||||
:::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).
|
||||
:::
|
||||
|
||||
TODO
|
||||
|
||||
- mentions (as text or as/with user ID)
|
||||
- hashtags (as text)
|
||||
- emojis (input rule with an autocomplete popup)
|
||||
- commands (can only be triggered at the beginning of a line, should trigger custom commands)
|
||||
- snippets (should be able to insert multiple paragraphs)
|
||||
- variables (should replaced when copied to external sources)
|
||||
@@ -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
|
||||
<demo name="Nodes/Mention" />
|
||||
|
||||
@@ -7,7 +7,7 @@ Node views are the best thing since sliced bread, at least if you’re a fan of
|
||||
|
||||
<!-- ```js
|
||||
import { Node } from '@tiptap/core'
|
||||
import { VueRenderer } from '@tiptap/vue'
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue'
|
||||
import Component from './Component.vue'
|
||||
|
||||
export default Node.create({
|
||||
@@ -83,12 +83,12 @@ https://github.com/ueberdosis/tiptap-next/blob/main/packages/extension-task-item
|
||||
|
||||
```js
|
||||
import { Node } from '@tiptap/core'
|
||||
import { VueRenderer } from '@tiptap/vue'
|
||||
import { VueNodeViewRenderer } from '@tiptap/vue'
|
||||
import Component from './Component.vue'
|
||||
|
||||
export default Node.create({
|
||||
addNodeView() {
|
||||
return VueRenderer(Component)
|
||||
return VueNodeViewRenderer(Component)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
@@ -174,9 +174,6 @@
|
||||
link: /api/extensions/gapcursor
|
||||
- title: History
|
||||
link: /api/extensions/history
|
||||
- title: Suggestion
|
||||
link: /api/extensions/suggestion
|
||||
type: draft
|
||||
- title: TextAlign
|
||||
link: /api/extensions/text-align
|
||||
- title: Typography
|
||||
|
||||
Reference in New Issue
Block a user