Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add support for markviews #5759

Merged
merged 8 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/gold-ads-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tiptap/react': minor
'@tiptap/vue-3': minor
'@tiptap/core': minor
---

Add support for [markviews](https://prosemirror.net/docs/ref/#view.MarkView), with support for React & Vue-3 MarkViewRenderers
23 changes: 23 additions & 0 deletions demos/src/GuideMarkViews/ReactComponent/React/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { MarkViewContent, MarkViewRendererProps } from '@tiptap/react'
import React from 'react'

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default (props: MarkViewRendererProps) => {
const [count, setCount] = React.useState(0)

return (
<span className="content" data-test-id="mark-view">
<MarkViewContent />
<label contentEditable={false}>
React component:
<button
onClick={() => {
setCount(count + 1)
}}
>
This button has been clicked {count} times.
</button>
</label>
</span>
)
}
24 changes: 24 additions & 0 deletions demos/src/GuideMarkViews/ReactComponent/React/Extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Mark } from '@tiptap/core'
import { ReactMarkViewRenderer } from '@tiptap/react'

import Component from './Component.js'

export default Mark.create({
name: 'reactComponent',

parseHTML() {
return [
{
tag: 'react-component',
},
]
},

renderHTML({ HTMLAttributes }) {
return ['react-component', HTMLAttributes]
},

addMarkView() {
return ReactMarkViewRenderer(Component)
},
})
Empty file.
32 changes: 32 additions & 0 deletions demos/src/GuideMarkViews/ReactComponent/React/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/// <reference types="cypress" />

context('/src/GuideMarkViews/ReactComponent/React/', () => {
before(() => {
cy.visit('/src/GuideMarkViews/ReactComponent/React/')
})

beforeEach(() => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p><react-component>Mark View Text</react-component>')
})
cy.get('.tiptap').type('{selectall}')
})

it('should show the markview', () => {
cy.get('.tiptap').find('[data-test-id="mark-view"]').should('exist')
})

it('should allow clicking the button', () => {
cy.get('.tiptap')
.find('[data-test-id="mark-view"] button')
.should('contain', 'This button has been clicked 0 times.')
cy.get('.tiptap')
.find('[data-test-id="mark-view"] button')
.click()
.then(() => {
cy.get('.tiptap')
.find('[data-test-id="mark-view"] button')
.should('contain', 'This button has been clicked 1 times.')
})
})
})
24 changes: 24 additions & 0 deletions demos/src/GuideMarkViews/ReactComponent/React/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import './styles.scss'

import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'

import ReactComponent from './Extension.js'

export default () => {
const editor = useEditor({
extensions: [StarterKit, ReactComponent],
content: `
<p>
This is still the text editor you’re used to, but enriched with node views.
</p>
<react-component>Sub-text</react-component>
<p>
Did you see that? That’s a React component. We are really living in the future.
</p>
`,
})

return <EditorContent editor={editor} />
}
116 changes: 116 additions & 0 deletions demos/src/GuideMarkViews/ReactComponent/React/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/* 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;
}

/* React component */
.react-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;
}
}
}
58 changes: 58 additions & 0 deletions demos/src/GuideMarkViews/VueComponent/Vue/Component.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<span className="content" data-test-id="mark-view">
<mark-view-content />
<label contenteditable="false"
>Vue Component::
<button @click="increase" class="primary">This button has been clicked {{ count }} times.</button>
</label>
</span>
</template>

<script>
import { MarkViewContent, markViewProps } from '@tiptap/vue-3'

export default {
components: {
MarkViewContent,
},

data() {
return {
count: 0,
}
},

props: markViewProps,

methods: {
increase() {
this.count += 1
},
},
}
</script>

<style lang="scss">
.tiptap {
/* Vue component */
.vue-component {
background-color: var(--purple-light);
border: 2px solid var(--purple);
border-radius: 0.5rem;

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;
}

.content {
margin-top: 1.5rem;
padding: 1rem;
}
}
}
</style>
24 changes: 24 additions & 0 deletions demos/src/GuideMarkViews/VueComponent/Vue/Extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Mark } from '@tiptap/core'
import { VueMarkViewRenderer } from '@tiptap/vue-3'

import Component from './Component.vue'

export default Mark.create({
name: 'vueComponent',

parseHTML() {
return [
{
tag: 'vue-component',
},
]
},

renderHTML({ HTMLAttributes }) {
return ['vue-component', HTMLAttributes]
},

addMarkView() {
return VueMarkViewRenderer(Component)
},
})
Empty file.
32 changes: 32 additions & 0 deletions demos/src/GuideMarkViews/VueComponent/Vue/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/// <reference types="cypress" />

context('/src/GuideMarkViews/VueComponent/Vue/', () => {
before(() => {
cy.visit('/src/GuideMarkViews/VueComponent/Vue/')
})

beforeEach(() => {
cy.get('.tiptap').then(([{ editor }]) => {
editor.commands.setContent('<p>Example Text</p><vue-component>Mark View Text</vue-component>')
})
cy.get('.tiptap').type('{selectall}')
})

it('should show the markview', () => {
cy.get('.tiptap').find('[data-test-id="mark-view"]').should('exist')
})

it('should allow clicking the button', () => {
cy.get('.tiptap')
.find('[data-test-id="mark-view"] button')
.should('contain', 'This button has been clicked 0 times.')
cy.get('.tiptap')
.find('[data-test-id="mark-view"] button')
.click()
.then(() => {
cy.get('.tiptap')
.find('[data-test-id="mark-view"] button')
.should('contain', 'This button has been clicked 1 times.')
})
})
})
Loading
Loading