mirror of
https://github.com/ueberdosis/tiptap.git
synced 2025-06-07 17:43:49 +08:00
fix(vue-3): set editor's appContext.provide to forward inject chain (#5397)
Vue internally uses prototype chain to preserve injects across the entire component chain. Thus should avoid Object.assign or spread operator as it won't copy the prototype. All correct provides will be already present on `instance.provides`.
This commit is contained in:
parent
a08bf85cf0
commit
f7f644f7b2
5
.changeset/odd-paws-tap.md
Normal file
5
.changeset/odd-paws-tap.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@tiptap/vue-3": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Correctly set editor's appContext.provide to forward full inject chain
|
@ -14,7 +14,12 @@ export default function init(name: string, source: any) {
|
|||||||
|
|
||||||
import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.vue`)
|
import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.vue`)
|
||||||
.then(module => {
|
.then(module => {
|
||||||
createApp(module.default).mount('#app')
|
const app = createApp(module.default)
|
||||||
|
|
||||||
|
if (typeof module.configureApp === 'function') {
|
||||||
|
module.configureApp(app)
|
||||||
|
}
|
||||||
|
app.mount('#app')
|
||||||
debug()
|
debug()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<node-view-wrapper class="vue-component">
|
||||||
|
<label>Vue Component</label>
|
||||||
|
<ValidateInject />
|
||||||
|
</node-view-wrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
|
||||||
|
|
||||||
|
import ValidateInject from './ValidateInject.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
NodeViewWrapper,
|
||||||
|
ValidateInject,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: nodeViewProps,
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tiptap {
|
||||||
|
/* Vue component */
|
||||||
|
.vue-component {
|
||||||
|
background-color: var(--purple-light);
|
||||||
|
border: 2px solid var(--purple);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
label {
|
||||||
|
background-color: var(--purple);
|
||||||
|
border-radius: 0 0 0.5rem 0;
|
||||||
|
color: var(--white);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<editor-content :editor="editor" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { Editor, EditorContent } from '@tiptap/vue-3'
|
||||||
|
|
||||||
|
import VueComponent from './Extension.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
EditorContent,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
editor: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
editorValue: 'editorValue',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.editor = new Editor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
VueComponent,
|
||||||
|
],
|
||||||
|
content: `
|
||||||
|
<p>
|
||||||
|
This is still the text editor you’re used to, but enriched with node views.
|
||||||
|
</p>
|
||||||
|
<vue-component count="0"></vue-component>
|
||||||
|
<p>
|
||||||
|
Did you see that? That’s a Vue component. We are really living in the future.
|
||||||
|
</p>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeUnmount() {
|
||||||
|
this.editor.destroy()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
/* Basic editor styles */
|
||||||
|
.tiptap {
|
||||||
|
:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List styles */
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding: 0 1rem;
|
||||||
|
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||||
|
|
||||||
|
li p {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heading styles */
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
margin-top: 3.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code and preformatted text styles */
|
||||||
|
code {
|
||||||
|
background-color: var(--purple-light);
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
color: var(--black);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.25em 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: var(--black);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--white);
|
||||||
|
font-family: 'JetBrainsMono', monospace;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
|
||||||
|
code {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 3px solid var(--gray-3);
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--gray-2);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,36 @@
|
|||||||
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
|
import { VueNodeViewRenderer } from '@tiptap/vue-3'
|
||||||
|
|
||||||
|
import Component from './Component.vue'
|
||||||
|
|
||||||
|
export default Node.create({
|
||||||
|
name: 'vueComponent',
|
||||||
|
|
||||||
|
group: 'block',
|
||||||
|
|
||||||
|
atom: true,
|
||||||
|
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
count: {
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'vue-component',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ['vue-component', mergeAttributes(HTMLAttributes)]
|
||||||
|
},
|
||||||
|
|
||||||
|
addNodeView() {
|
||||||
|
return VueNodeViewRenderer(Component)
|
||||||
|
},
|
||||||
|
})
|
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="validate-inject">
|
||||||
|
<p>{{ globalValue }}</p>
|
||||||
|
<p>{{ appValue }} </p>
|
||||||
|
<p>{{ indexValue }}</p>
|
||||||
|
<p>{{ editorValue }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
inject: ['appValue', 'indexValue', 'editorValue'],
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.validate-inject {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,29 @@
|
|||||||
|
context('/src/Examples/InteractivityComponentProvideInject/Vue/', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/src/Examples/InteractivityComponentProvideInject/Vue/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have a working tiptap instance', () => {
|
||||||
|
cy.get('.tiptap').then(([{ editor }]) => {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect(editor).to.not.be.null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render a custom node', () => {
|
||||||
|
cy.get('.tiptap .vue-component').should('have.length', 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have global and all injected values', () => {
|
||||||
|
const expectedTexts = [
|
||||||
|
'globalValue',
|
||||||
|
'appValue',
|
||||||
|
'indexValue',
|
||||||
|
'editorValue',
|
||||||
|
]
|
||||||
|
|
||||||
|
cy.get('.tiptap .vue-component p').each((p, index) => {
|
||||||
|
cy.wrap(p).should('have.text', expectedTexts[index])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<Editor />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Editor from './Editor.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Editor,
|
||||||
|
},
|
||||||
|
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
indexValue: 'indexValue',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configureApp(app) {
|
||||||
|
app.config.globalProperties.globalValue = 'globalValue'
|
||||||
|
app.provide('appValue', 'appValue')
|
||||||
|
}
|
||||||
|
</script>
|
@ -46,11 +46,10 @@ export const EditorContent = defineComponent({
|
|||||||
if (instance) {
|
if (instance) {
|
||||||
editor.appContext = {
|
editor.appContext = {
|
||||||
...instance.appContext,
|
...instance.appContext,
|
||||||
provides: Object.assign(
|
// Vue internally uses prototype chain to forward/shadow injects across the entire component chain
|
||||||
instance.appContext.provides,
|
// so don't use object spread operator or 'Object.assign' and just set `provides` as is on editor's appContext
|
||||||
// @ts-ignore
|
// @ts-expect-error forward instance's 'provides' into appContext
|
||||||
instance.provides,
|
provides: instance.provides,
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user