Merge branch 'main' of github.com:ueberdosis/tiptap into add-empty-editor-class-to-root-div

This commit is contained in:
Dominik Biedebach
2022-09-10 15:31:04 +02:00
647 changed files with 40003 additions and 11691 deletions

View File

@@ -20,6 +20,7 @@ module.exports = {
'html', 'html',
'cypress', 'cypress',
'@typescript-eslint', '@typescript-eslint',
'simple-import-sort',
], ],
env: { env: {
'cypress/globals': true, 'cypress/globals': true,
@@ -90,6 +91,8 @@ module.exports = {
'@typescript-eslint/ban-types': 'off', '@typescript-eslint/ban-types': 'off',
'@typescript-eslint/comma-dangle': ['error', 'always-multiline'], '@typescript-eslint/comma-dangle': ['error', 'always-multiline'],
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
}, },
}, },
], ],

View File

@@ -11,6 +11,13 @@ body:
placeholder: "Im always frustrated when …" placeholder: "Im always frustrated when …"
validations: validations:
required: true required: true
- type: textarea
id: environment
attributes:
label: Which browser was this experienced in? Are any special extensions installed?
description: Please give us more information about your browser environment so we can reproduce the bug faster.
validations:
required: true
- type: textarea - type: textarea
id: reproduction id: reproduction
attributes: attributes:
@@ -46,7 +53,7 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Did you update your dependencies? label: Did you update your dependencies?
description: "Use `yarn upgrade-interactive` to update your dependencies." description: "Use `npm update` to update your dependencies."
options: options:
- label: Yes, Ive updated my dependencies to use the latest version of all packages. - label: Yes, Ive updated my dependencies to use the latest version of all packages.
required: true required: true

View File

@@ -3,7 +3,7 @@ description: Share what we need to explain better
labels: labels:
- documentation - documentation
assignees: assignees:
- hanspagel - bdbch
body: body:
- type: input - type: input
id: url id: url

View File

@@ -11,5 +11,5 @@ updates:
interval: 'weekly' interval: 'weekly'
day: 'monday' day: 'monday'
reviewers: reviewers:
- 'hanspagel' - 'bdbch'

View File

@@ -24,30 +24,30 @@ jobs:
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v3.0.2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.5.1 uses: actions/setup-node@v3.4.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Load cached dependencies - name: Load cached dependencies
uses: actions/cache@v2.1.7 uses: actions/cache@v3.0.8
id: cache id: cache
with: with:
path: | path: |
**/node_modules **/node_modules
/home/runner/.cache/Cypress /home/runner/.cache/Cypress
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies - name: Install dependencies
id: install-dependencies id: install-dependencies
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
run: yarn install run: npm install
# - name: Fix code style linting errors # - name: Fix code style linting errors
# id: lint-fix # id: lint-fix
# run: yarn lint:fix # run: npm run lint:fix
# continue-on-error: true # continue-on-error: true
# #
# - name: Commit fixed linting errors # - name: Commit fixed linting errors
@@ -58,7 +58,7 @@ jobs:
- name: Lint code - name: Lint code
id: lint id: lint
run: yarn lint run: npm run lint
- name: Send Slack notifications - name: Send Slack notifications
uses: act10ns/slack@v1 uses: act10ns/slack@v1
@@ -80,10 +80,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v3.0.2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.5.1 uses: actions/setup-node@v3.4.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@@ -91,15 +91,15 @@ jobs:
id: cypress id: cypress
uses: cypress-io/github-action@v2 uses: cypress-io/github-action@v2
with: with:
cache-key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} cache-key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
start: yarn start start: npm run start
wait-on: 'http://localhost:3000' wait-on: 'http://localhost:3000'
project: ./tests project: ./tests
browser: chrome browser: chrome
quiet: true quiet: true
- name: Export screenshots (on failure only) - name: Export screenshots (on failure only)
uses: actions/upload-artifact@v2.3.1 uses: actions/upload-artifact@v3.1.0
if: failure() if: failure()
with: with:
name: cypress-screenshots name: cypress-screenshots
@@ -107,7 +107,7 @@ jobs:
retention-days: 7 retention-days: 7
- name: Export screen recordings (on failure only) - name: Export screen recordings (on failure only)
uses: actions/upload-artifact@v2.3.1 uses: actions/upload-artifact@v3.1.0
if: failure() if: failure()
with: with:
name: cypress-videos name: cypress-videos
@@ -136,30 +136,30 @@ jobs:
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v3.0.2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.5.1 uses: actions/setup-node@v3.4.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Load cached dependencies - name: Load cached dependencies
uses: actions/cache@v2.1.7 uses: actions/cache@v3.0.8
id: cache id: cache
with: with:
path: | path: |
**/node_modules **/node_modules
/home/runner/.cache/Cypress /home/runner/.cache/Cypress
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies - name: Install dependencies
id: install-dependencies id: install-dependencies
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
run: yarn install run: npm install
- name: Try to build the packages - name: Try to build the packages
id: build-packages id: build-packages
run: yarn build:ci run: npm run build:ci
- name: Send Slack notifications - name: Send Slack notifications
uses: act10ns/slack@v1 uses: act10ns/slack@v1

19
.github/workflows/issues.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Add issues to Tiptap project
on:
issues:
types:
- opened
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
with:
project-url: ${{ secrets.ADD_TO_PROJECT_URL }}
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
- uses: actions-ecosystem/action-add-labels@v1
with:
labels: needs-triage

16
.github/workflows/prs.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Add pull requests to Tiptap project
on:
pull_request:
types:
- opened
jobs:
add-to-project:
name: Add pull request to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
with:
project-url: ${{ secrets.ADD_TO_PROJECT_URL }}
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}

18
.github/workflows/stale.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: 'Close stale issues and PRs'
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@main
with:
stale-issue-message: 'This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 7 days'
days-before-stale: 90
days-before-close: 7
days-before-pr-stale: 180
stale-issue-label: stale
stale-pr-label: stale

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint:staged

3
.lintstagedrc.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
"./**/*.{ts,tsx,js,jsx,vue}": ["eslint --fix --quiet --no-error-on-unmatched-pattern"],
};

11
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"configurations": [
{
"name": "Launch Tiptap demos in Google Chrome",
"request": "launch",
"type": "chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

59
.vscode/settings.json vendored
View File

@@ -1,3 +1,60 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
"conventionalCommits.scopes": [
"ci",
"docs",
"maintainment",
"tests",
"core",
"extension/blockquote",
"extension/bold",
"extension/bubble-menu",
"extension/bullet-list",
"extension/character-count",
"extension/code",
"extension/code-block",
"extension/code-block-lowlight",
"extension/collaboration",
"extension/collaboration-cursor",
"extension/color",
"extension/document",
"extension/dropcursor",
"extension/floating-menu",
"extension/focus",
"extension/font-family",
"extension/gapcursor",
"extension/hard-break",
"extension/heading",
"extension/highlight",
"extension/history",
"extension/horizontal-rule",
"extension/image",
"extension/italic",
"extension/link",
"extension/list-item",
"extension/mention",
"extension/ordered-list",
"extension/paragraph",
"extension/placeholder",
"extension/strike",
"extension/subscript",
"extension/table",
"extension/table-cell",
"extension/table-header",
"extension/table-row",
"extension/task-item",
"extension/task-list",
"extension/text",
"extension/text-align",
"extension/text-style",
"extension/typography",
"extension/underline",
"extension/youtube",
"html",
"react",
"starter-kit",
"suggestion",
"vue-2",
"vue-3"
]
} }

View File

@@ -10,13 +10,15 @@ prosemirror-keymap
prosemirror-model prosemirror-model
prosemirror-schema-list prosemirror-schema-list
prosemirror-state prosemirror-state
prosemirror-tables @_ueberdosis/prosemirror-tables
prosemirror-transform prosemirror-transform
prosemirror-view prosemirror-view
react
react-dom
react-dom/client
shiki shiki
simplify-js simplify-js
tippy.js tippy.js
uuid uuid
y-prosemirror
y-webrtc y-webrtc
yjs yjs

2118
demos/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,33 +4,38 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "vite --host", "start": "vite --host",
"build": "yarn ts && vite build", "build": "npm run ts && vite build",
"ts": "tsc --project tsconfig.base.json --noEmit && tsc --project tsconfig.react.json --noEmit && tsc --project tsconfig.vue-2.json --noEmit && tsc --project tsconfig.vue-3.json --noEmit" "ts": "tsc --project tsconfig.base.json --noEmit && tsc --project tsconfig.react.json --noEmit && tsc --project tsconfig.vue-2.json --noEmit && tsc --project tsconfig.vue-3.json --noEmit"
}, },
"dependencies": { "dependencies": {
"@hocuspocus/provider": "^1.0.0-alpha.29", "@hocuspocus/provider": "^1.0.0-alpha.29",
"d3": "^7.3.0", "d3": "^7.3.0",
"fast-glob": "^3.2.11", "fast-glob": "^3.2.11",
"highlight.js": "^11.6.0",
"lowlight": "^2.7.0",
"remixicon": "^2.5.0", "remixicon": "^2.5.0",
"shiki": "^0.10.0", "shiki": "^0.10.0",
"simplify-js": "^1.2.4", "simplify-js": "^1.2.4",
"y-webrtc": "^10.2.2", "y-prosemirror": "1.0.20",
"yjs": "^13.5.26" "y-webrtc": "^10.2.3",
"yjs": "^13.5.39"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.49",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@vitejs/plugin-react-refresh": "^1.3.6", "@vitejs/plugin-react": "^1.3.1",
"@vitejs/plugin-vue": "^1.10.2", "@vitejs/plugin-vue": "^1.10.2",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"iframe-resizer": "^4.3.2", "iframe-resizer": "^4.3.2",
"postcss": "^8.4.6", "postcss": "^8.4.6",
"react": "^17.0.2", "react": "^18.0.0",
"react-dom": "^17.0.2", "react-dom": "^18.0.0",
"sass": "^1.49.7", "sass": "^1.49.7",
"svelte": "^3.49.0",
"tailwindcss": "^2.2.19", "tailwindcss": "^2.2.19",
"typescript": "^4.5.5", "typescript": "^4.5.5",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"vite": "^2.7.13", "vite": "^2.9.13",
"vite-plugin-checker": "^0.3.4", "vite-plugin-checker": "^0.3.4",
"vue": "^3.0.5", "vue": "^3.0.5",
"vue-router": "^4.0.11" "vue-router": "^4.0.11"

View File

@@ -10,7 +10,7 @@
v-for="(language, index) in sortedTabs" v-for="(language, index) in sortedTabs"
:key="index" :key="index"
@click="setTab(language.name)" @click="setTab(language.name)"
class="px-4 py-2 rounded-t-lg text-xs uppercase font-bold tracking-wide" class="px-4 py-2 text-xs font-bold tracking-wide uppercase rounded-t-lg"
:class="[currentTab === language.name :class="[currentTab === language.name
? 'bg-black text-white' ? 'bg-black text-white'
: 'text-black' : 'text-black'
@@ -21,7 +21,7 @@
</div> </div>
<div class="overflow-hidden rounded-b-xl"> <div class="overflow-hidden rounded-b-xl">
<div <div
class="bg-white border-3 border-black last:rounded-b-xl" class="bg-white border-black border-3 last:rounded-b-xl"
:class="[ :class="[
showTabs && firstTabSelected showTabs && firstTabSelected
? 'rounded-tr-xl' ? 'rounded-tr-xl'
@@ -34,7 +34,7 @@
/> />
</div> </div>
<div class="bg-black text-white" v-if="!hideSource && currentFile"> <div class="text-white bg-black" v-if="!hideSource && currentFile">
<div class="flex overflow-x-auto"> <div class="flex overflow-x-auto">
<div class="flex flex-auto px-4 border-b-2 border-gray-800"> <div class="flex flex-auto px-4 border-b-2 border-gray-800">
<button <button
@@ -66,17 +66,17 @@
<div class="overflow-dark overflow-auto max-h-[500px] relative text-white"> <div class="overflow-dark overflow-auto max-h-[500px] relative text-white">
<shiki <shiki
class="overflow-visible p-4" class="p-4 overflow-visible"
:language="debugJSON && showDebug ? 'js' : getFileExtension(currentFile.name)" :language="debugJSON && showDebug ? 'js' : getFileExtension(currentFile.name)"
:code="debugJSON && showDebug ? debugJSON : currentFile.content" :code="debugJSON && showDebug ? debugJSON : currentFile.content"
/> />
</div> </div>
<div class="flex justify-between px-4 py-2 text-md text-gray-400 border-t border-gray-800"> <div class="flex justify-between px-4 py-2 text-gray-400 border-t border-gray-800 text-md">
<a class="flex-shrink min-w-0 overflow-ellipsis overflow-hidden whitespace-nowrap" :href="currentIframeUrl"> <a class="flex-shrink min-w-0 overflow-hidden overflow-ellipsis whitespace-nowrap" :href="currentIframeUrl">
{{ name }}/{{ currentTab }} {{ name }}/{{ currentTab }}
</a> </a>
<a class="whitespace-nowrap pl-4" :href="githubUrl" target="_blank"> <a class="pl-4 whitespace-nowrap" :href="githubUrl" target="_blank">
Edit on GitHub Edit on GitHub
</a> </a>
</div> </div>
@@ -87,6 +87,7 @@
<script> <script>
import { getDebugJSON } from '@tiptap/core' import { getDebugJSON } from '@tiptap/core'
import DemoFrame from './DemoFrame.vue' import DemoFrame from './DemoFrame.vue'
import Shiki from './Shiki.vue' import Shiki from './Shiki.vue'
@@ -114,7 +115,7 @@ export default {
sources: {}, sources: {},
currentTab: null, currentTab: null,
currentFile: null, currentFile: null,
tabOrder: ['React', 'Vue', 'JS'], tabOrder: ['React', 'Vue', 'Svelte', 'JS'],
debugJSON: null, debugJSON: null,
showDebug: false, showDebug: false,
} }

View File

@@ -4,11 +4,12 @@
</template> </template>
<script> <script>
import Worker from './shiki.worker?worker'
// this import is a bugfix // this import is a bugfix
// otherwise the `onig.wasm` file is missing in the dist folder // otherwise the `onig.wasm` file is missing in the dist folder
import 'shiki/dist/onig.wasm?url' import 'shiki/dist/onig.wasm?url'
import Worker from './shiki.worker?worker'
export default { export default {
props: { props: {
code: { code: {

View File

@@ -1,11 +1,13 @@
import 'iframe-resizer/js/iframeResizer.contentWindow'
import './style.css'
import { demos } from '@demos'
import iframeResize from 'iframe-resizer/js/iframeResizer'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import App from './index.vue'
import Demo from './Demo.vue' import Demo from './Demo.vue'
import { demos } from '@demos' import App from './index.vue'
import 'iframe-resizer/js/iframeResizer.contentWindow'
import iframeResize from 'iframe-resizer/js/iframeResizer'
import './style.css'
const routes = demos const routes = demos
.map(({ name, tabs }) => { .map(({ name, tabs }) => {

View File

@@ -1,15 +1,15 @@
import * as shiki from 'shiki' import * as shiki from 'shiki'
import onigasm from 'shiki/dist/onig.wasm?url' import onigasm from 'shiki/dist/onig.wasm?url'
import theme from 'shiki/themes/material-darker.json' import langCSS from 'shiki/languages/css.tmLanguage.json'
import langHTML from 'shiki/languages/html.tmLanguage.json' import langHTML from 'shiki/languages/html.tmLanguage.json'
import langJS from 'shiki/languages/javascript.tmLanguage.json' import langJS from 'shiki/languages/javascript.tmLanguage.json'
import langJSX from 'shiki/languages/jsx.tmLanguage.json' import langJSX from 'shiki/languages/jsx.tmLanguage.json'
import langTS from 'shiki/languages/typescript.tmLanguage.json'
import langTSX from 'shiki/languages/tsx.tmLanguage.json'
import langVueHTML from 'shiki/languages/vue-html.tmLanguage.json'
import langVue from 'shiki/languages/vue.tmLanguage.json'
import langCSS from 'shiki/languages/css.tmLanguage.json'
import langSCSS from 'shiki/languages/scss.tmLanguage.json' import langSCSS from 'shiki/languages/scss.tmLanguage.json'
import langTSX from 'shiki/languages/tsx.tmLanguage.json'
import langTS from 'shiki/languages/typescript.tmLanguage.json'
import langVue from 'shiki/languages/vue.tmLanguage.json'
import langVueHTML from 'shiki/languages/vue-html.tmLanguage.json'
import theme from 'shiki/themes/material-darker.json'
let highlighter = null let highlighter = null

View File

@@ -1,7 +1,8 @@
import 'iframe-resizer/js/iframeResizer.contentWindow' import 'iframe-resizer/js/iframeResizer.contentWindow'
import { debug } from './helper'
import './style.scss' import './style.scss'
import { debug } from './helper'
export default function init(name: string, source: any) { export default function init(name: string, source: any) {
// @ts-ignore // @ts-ignore
window.source = source window.source = source

View File

@@ -1,9 +1,11 @@
import React from 'react'
import ReactDOM from 'react-dom'
import 'iframe-resizer/js/iframeResizer.contentWindow' import 'iframe-resizer/js/iframeResizer.contentWindow'
import { debug, splitName } from './helper'
import './style.scss' import './style.scss'
import React from 'react'
import { createRoot } from 'react-dom/client'
import { debug, splitName } from './helper'
export default function init(name: string, source: any) { export default function init(name: string, source: any) {
// @ts-ignore // @ts-ignore
window.source = source window.source = source
@@ -13,7 +15,11 @@ export default function init(name: string, source: any) {
import(`../src/${demoCategory}/${demoName}/React/index.jsx`) import(`../src/${demoCategory}/${demoName}/React/index.jsx`)
.then(module => { .then(module => {
ReactDOM.render(React.createElement(module.default), document.getElementById('app')) const root = document.getElementById('app')
if (root) {
createRoot(root).render(React.createElement(module.default))
}
debug() debug()
}) })
} }

21
demos/setup/svelte.ts Normal file
View File

@@ -0,0 +1,21 @@
import 'iframe-resizer/js/iframeResizer.contentWindow'
import './style.scss'
import { debug, splitName } from './helper'
export default function init(name: string, source: any) {
// @ts-ignore
window.source = source
document.title = name
const [demoCategory, demoName] = splitName(name)
import(`../src/${demoCategory}/${demoName}/Svelte/index.svelte`)
.then(Module => {
const Component = Module.default
new Component({ target: document.querySelector('#app') }) // eslint-disable-line
debug()
})
}

View File

@@ -1,8 +1,10 @@
import { createApp } from 'vue'
import 'iframe-resizer/js/iframeResizer.contentWindow' import 'iframe-resizer/js/iframeResizer.contentWindow'
import { debug, splitName } from './helper'
import './style.scss' import './style.scss'
import { createApp } from 'vue'
import { debug, splitName } from './helper'
export default function init(name: string, source: any) { export default function init(name: string, source: any) {
// @ts-ignore // @ts-ignore
window.source = source window.source = source

View File

@@ -0,0 +1,31 @@
import './styles.scss'
import Link from '@tiptap/extension-link'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'
export default () => {
const editor = useEditor({
extensions: [
StarterKit,
Link.configure({
validate: link => /^https?:\/\//.test(link),
}),
],
content: `
<p>Hey! Try to type in url with and without a http/s protocol. - Links without a protocol should not get auto linked</p>
`,
editorProps: {
attributes: {
spellcheck: 'false',
},
},
})
return (
<div>
<EditorContent editor={editor} />
</div>
)
}

View File

@@ -0,0 +1,42 @@
context('/src/Examples/AutolinkValidation/React/', () => {
before(() => {
cy.visit('/src/Examples/AutolinkValidation/React/')
})
beforeEach(() => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
})
const validLinks = [
'https://tiptap.dev',
'http://tiptap.dev',
'https://www.tiptap.dev/',
'http://www.tiptap.dev/',
]
const invalidLinks = [
'tiptap.dev',
'www.tiptap.dev',
]
validLinks.forEach(link => {
it(`${link} should get autolinked`, () => {
cy.get('.ProseMirror').type(link)
cy.get('.ProseMirror').should('have.text', link)
cy.get('.ProseMirror')
.find('a')
.should('have.length', 1)
.should('have.attr', 'href', link)
})
})
invalidLinks.forEach(link => {
it(`${link} should NOT get autolinked`, () => {
cy.get('.ProseMirror').type(link)
cy.get('.ProseMirror').should('have.text', link)
cy.get('.ProseMirror')
.find('a')
.should('have.length', 0)
})
})
})

View File

@@ -0,0 +1,54 @@
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
hr {
margin: 1rem 0;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
}

View File

@@ -0,0 +1,42 @@
context('/src/Examples/AutolinkValidation/Vue/', () => {
before(() => {
cy.visit('/src/Examples/AutolinkValidation/Vue/')
})
beforeEach(() => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
})
const validLinks = [
'https://tiptap.dev',
'http://tiptap.dev',
'https://www.tiptap.dev/',
'http://www.tiptap.dev/',
]
const invalidLinks = [
'tiptap.dev',
'www.tiptap.dev',
]
validLinks.forEach(link => {
it(`${link} should get autolinked`, () => {
cy.get('.ProseMirror').type(link)
cy.get('.ProseMirror').should('have.text', link)
cy.get('.ProseMirror')
.find('a')
.should('have.length', 1)
.should('have.attr', 'href', link)
})
})
invalidLinks.forEach(link => {
it(`${link} should NOT get autolinked`, () => {
cy.get('.ProseMirror').type(link)
cy.get('.ProseMirror').should('have.text', link)
cy.get('.ProseMirror')
.find('a')
.should('have.length', 0)
})
})
})

View File

@@ -0,0 +1,101 @@
<template>
<editor-content :editor="editor" />
</template>
<script>
import Link from '@tiptap/extension-link'
import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
}
},
mounted() {
this.editor = new Editor({
extensions: [
StarterKit,
Link.configure({
validate: link => /^https?:\/\//.test(link),
}),
],
content: `
<p>Hey! Try to type in url with and without a http/s protocol. - Links without a protocol should not get auto linked</p>
`,
editorProps: {
attributes: {
spellcheck: 'false',
},
},
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
hr {
margin: 1rem 0;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
}
</style>

View File

@@ -1,9 +1,11 @@
import React from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { content } from '../content.js'
import './styles.scss' import './styles.scss'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'
import { content } from '../content.js'
const MenuBar = ({ editor }) => { const MenuBar = ({ editor }) => {
if (!editor) { if (!editor) {
return null return null

View File

@@ -0,0 +1,12 @@
context('/src/Examples/Book/React/', () => {
before(() => {
cy.visit('/src/Examples/Book/React/')
})
it('should have a working tiptap instance', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
// eslint-disable-next-line
expect(editor).to.not.be.null
})
})
})

View File

@@ -3,5 +3,10 @@ context('/src/Examples/Book/Vue/', () => {
cy.visit('/src/Examples/Book/Vue/') cy.visit('/src/Examples/Book/Vue/')
}) })
// TODO: Write tests it('should have a working tiptap instance', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
// eslint-disable-next-line
expect(editor).to.not.be.null
})
})
}) })

View File

@@ -68,8 +68,9 @@
</template> </template>
<script> <script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
import { content } from '../content.js' import { content } from '../content.js'
export default { export default {

View File

@@ -0,0 +1,171 @@
import './styles.scss'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'
import styles from './index.module.css'
const MenuBar = ({ editor }) => {
if (!editor) {
return null
}
return (
<div className={`toolbar ${styles.toolbar}`}>
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'is-active' : ''}
>
bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'is-active' : ''}
>
italic
</button>
<button
onClick={() => editor.chain().focus().toggleStrike().run()}
className={editor.isActive('strike') ? 'is-active' : ''}
>
strike
</button>
<button
onClick={() => editor.chain().focus().toggleCode().run()}
className={editor.isActive('code') ? 'is-active' : ''}
>
code
</button>
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}>
clear marks
</button>
<button onClick={() => editor.chain().focus().clearNodes().run()}>
clear nodes
</button>
<button
onClick={() => editor.chain().focus().setParagraph().run()}
className={editor.isActive('paragraph') ? 'is-active' : ''}
>
paragraph
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
>
h1
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
>
h2
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
className={editor.isActive('heading', { level: 3 }) ? 'is-active' : ''}
>
h3
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
className={editor.isActive('heading', { level: 4 }) ? 'is-active' : ''}
>
h4
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
className={editor.isActive('heading', { level: 5 }) ? 'is-active' : ''}
>
h5
</button>
<button
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
className={editor.isActive('heading', { level: 6 }) ? 'is-active' : ''}
>
h6
</button>
<button
onClick={() => editor.chain().focus().toggleBulletList().run()}
className={editor.isActive('bulletList') ? 'is-active' : ''}
>
bullet list
</button>
<button
onClick={() => editor.chain().focus().toggleOrderedList().run()}
className={editor.isActive('orderedList') ? 'is-active' : ''}
>
ordered list
</button>
<button
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
className={editor.isActive('codeBlock') ? 'is-active' : ''}
>
code block
</button>
<button
onClick={() => editor.chain().focus().toggleBlockquote().run()}
className={editor.isActive('blockquote') ? 'is-active' : ''}
>
blockquote
</button>
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
horizontal rule
</button>
<button onClick={() => editor.chain().focus().setHardBreak().run()}>
hard break
</button>
<button onClick={() => editor.chain().focus().undo().run()}>
undo
</button>
<button onClick={() => editor.chain().focus().redo().run()}>
redo
</button>
</div>
)
}
export default () => {
const editor = useEditor({
extensions: [
StarterKit,
],
content: `
<h2>
Hi there,
</h2>
<p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p>
<ul>
<li>
Thats a bullet list with one …
</li>
<li>
… or two list items.
</li>
</ul>
<p>
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
</p>
<pre><code class="language-css">body {
display: none;
}</code></pre>
<p>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
</p>
<blockquote>
Wow, thats amazing. Good work, boy! 👏
<br />
— Mom
</blockquote>
`,
})
return (
<div>
<MenuBar editor={editor} />
<EditorContent editor={editor} />
</div>
)
}

View File

@@ -0,0 +1,4 @@
.toolbar {
background-color: red;
padding: 16px;
}

View File

@@ -0,0 +1,11 @@
context('/src/Examples/CSSModules/React/', () => {
before(() => {
cy.visit('/src/Examples/CSSModules/React/')
})
it('should apply a randomly generated class that adds padding and background color to the toolbar', () => {
cy.get('.toolbar').should('exist')
cy.get('.toolbar').should('have.css', 'background-color', 'rgb(255, 0, 0)')
cy.get('.toolbar').should('have.css', 'padding', '16px')
})
})

View File

@@ -0,0 +1,56 @@
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
}

View File

@@ -0,0 +1,4 @@
.toolbar {
background-color: red;
padding: 16px;
}

View File

@@ -0,0 +1,11 @@
context('/src/Examples/CSSModules/Vue/', () => {
before(() => {
cy.visit('/src/Examples/CSSModules/Vue/')
})
it('should apply a randomly generated class that adds padding and background color to the toolbar', () => {
cy.get('.toolbar').should('exist')
cy.get('.toolbar').should('have.css', 'background-color', 'rgb(255, 0, 0)')
cy.get('.toolbar').should('have.css', 'padding', '16px')
})
})

View File

@@ -0,0 +1,189 @@
<template>
<div v-if="editor" class="toolbar" :class="styles.toolbar">
<button @click="editor.chain().focus().toggleBold().run()" :class="{ 'is-active': editor.isActive('bold') }">
bold
</button>
<button @click="editor.chain().focus().toggleItalic().run()" :class="{ 'is-active': editor.isActive('italic') }">
italic
</button>
<button @click="editor.chain().focus().toggleStrike().run()" :class="{ 'is-active': editor.isActive('strike') }">
strike
</button>
<button @click="editor.chain().focus().toggleCode().run()" :class="{ 'is-active': editor.isActive('code') }">
code
</button>
<button @click="editor.chain().focus().unsetAllMarks().run()">
clear marks
</button>
<button @click="editor.chain().focus().clearNodes().run()">
clear nodes
</button>
<button @click="editor.chain().focus().setParagraph().run()" :class="{ 'is-active': editor.isActive('paragraph') }">
paragraph
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }">
h1
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }">
h2
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 3 }) }">
h3
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 4 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 4 }) }">
h4
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 5 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 5 }) }">
h5
</button>
<button @click="editor.chain().focus().toggleHeading({ level: 6 }).run()" :class="{ 'is-active': editor.isActive('heading', { level: 6 }) }">
h6
</button>
<button @click="editor.chain().focus().toggleBulletList().run()" :class="{ 'is-active': editor.isActive('bulletList') }">
bullet list
</button>
<button @click="editor.chain().focus().toggleOrderedList().run()" :class="{ 'is-active': editor.isActive('orderedList') }">
ordered list
</button>
<button @click="editor.chain().focus().toggleCodeBlock().run()" :class="{ 'is-active': editor.isActive('codeBlock') }">
code block
</button>
<button @click="editor.chain().focus().toggleBlockquote().run()" :class="{ 'is-active': editor.isActive('blockquote') }">
blockquote
</button>
<button @click="editor.chain().focus().setHorizontalRule().run()">
horizontal rule
</button>
<button @click="editor.chain().focus().setHardBreak().run()">
hard break
</button>
<button @click="editor.chain().focus().undo().run()">
undo
</button>
<button @click="editor.chain().focus().redo().run()">
redo
</button>
</div>
<editor-content :editor="editor" />
</template>
<script>
import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
import styles from './index.module.css'
export default {
components: {
EditorContent,
},
data() {
return {
editor: null,
styles,
}
},
mounted() {
this.editor = new Editor({
extensions: [
StarterKit,
],
content: `
<h1 class="test">
This is a red headline
</h1>
<p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p>
<ul>
<li>
Thats a bullet list with one …
</li>
<li>
… or two list items.
</li>
</ul>
<p>
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
</p>
<pre><code class="language-css">body {
display: none;
}</code></pre>
<p>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
</p>
<blockquote>
Wow, thats amazing. Good work, boy! 👏
<br />
— Mom
</blockquote>
`,
})
},
beforeUnmount() {
this.editor.destroy()
},
}
</script>
<style lang="scss">
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
}
</style>

View File

@@ -1,7 +1,8 @@
import React from 'react'
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'
import './CodeBlockComponent.scss' import './CodeBlockComponent.scss'
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import React from 'react'
export default ({ node: { attrs: { language: defaultLanguage } }, updateAttributes, extension }) => ( export default ({ node: { attrs: { language: defaultLanguage } }, updateAttributes, extension }) => (
<NodeViewWrapper className="code-block"> <NodeViewWrapper className="code-block">
<select contentEditable={false} defaultValue={defaultLanguage} onChange={event => updateAttributes({ language: event.target.value })}> <select contentEditable={false} defaultValue={defaultLanguage} onChange={event => updateAttributes({ language: event.target.value })}>

View File

@@ -1,20 +1,29 @@
import React from 'react'
import { useEditor, EditorContent, ReactNodeViewRenderer } from '@tiptap/react'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import CodeBlockComponent from './CodeBlockComponent'
// load all highlight.js languages
import lowlight from 'lowlight'
// load specific languages only // load specific languages only
// import lowlight from 'lowlight/lib/core' // import { lowlight } from 'lowlight/lib/core'
// import javascript from 'highlight.js/lib/languages/javascript' // import javascript from 'highlight.js/lib/languages/javascript'
// lowlight.registerLanguage('javascript', javascript) // lowlight.registerLanguage('javascript', javascript)
import './styles.scss' import './styles.scss'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text'
import { EditorContent, ReactNodeViewRenderer, useEditor } from '@tiptap/react'
import css from 'highlight.js/lib/languages/css'
import js from 'highlight.js/lib/languages/javascript'
import ts from 'highlight.js/lib/languages/typescript'
import html from 'highlight.js/lib/languages/xml'
// load all highlight.js languages
import { lowlight } from 'lowlight'
import React from 'react'
import CodeBlockComponent from './CodeBlockComponent'
lowlight.registerLanguage('html', html)
lowlight.registerLanguage('css', css)
lowlight.registerLanguage('js', js)
lowlight.registerLanguage('ts', ts)
const MenuBar = ({ editor }) => { const MenuBar = ({ editor }) => {
if (!editor) { if (!editor) {
return null return null

View File

@@ -0,0 +1,28 @@
context('/src/Examples/CodeBlockLanguage/React/', () => {
before(() => {
cy.visit('/src/Examples/CodeBlockLanguage/React/')
})
it('should have hljs classes for syntax highlighting', () => {
cy.get('[class^=hljs]').then(elements => {
expect(elements.length).to.be.greaterThan(0)
})
})
it('should have different count of hljs classes after switching language', () => {
cy.get('[class^=hljs]').then(elements => {
const initialCount = elements.length
expect(initialCount).to.be.greaterThan(0)
cy.get('.ProseMirror select').select('java')
cy.wait(500)
cy.get('[class^=hljs]').then(newElements => {
const newCount = newElements.length
expect(newCount).to.not.equal(initialCount)
})
})
})
})

View File

@@ -16,7 +16,7 @@
</template> </template>
<script> <script>
import { NodeViewWrapper, NodeViewContent, nodeViewProps } from '@tiptap/vue-3' import { NodeViewContent, nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
export default { export default {
components: { components: {

View File

@@ -0,0 +1,28 @@
context('/src/Examples/CodeBlockLanguage/Vue/', () => {
before(() => {
cy.visit('/src/Examples/CodeBlockLanguage/Vue/')
})
it('should have hljs classes for syntax highlighting', () => {
cy.get('[class^=hljs]').then(elements => {
expect(elements.length).to.be.greaterThan(0)
})
})
it('should have different count of hljs classes after switching language', () => {
cy.get('[class^=hljs]').then(elements => {
const initialCount = elements.length
expect(initialCount).to.be.greaterThan(0)
cy.get('.ProseMirror select').select('java')
cy.wait(500)
cy.get('[class^=hljs]').then(newElements => {
const newCount = newElements.length
expect(newCount).to.not.equal(initialCount)
})
})
})
})

View File

@@ -8,18 +8,27 @@
</template> </template>
<script> <script>
import { Editor, EditorContent, VueNodeViewRenderer } from '@tiptap/vue-3' import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Paragraph from '@tiptap/extension-paragraph' import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' import { Editor, EditorContent, VueNodeViewRenderer } from '@tiptap/vue-3'
import css from 'highlight.js/lib/languages/css'
import js from 'highlight.js/lib/languages/javascript'
import ts from 'highlight.js/lib/languages/typescript'
import html from 'highlight.js/lib/languages/xml'
// load all highlight.js languages
import { lowlight } from 'lowlight'
import CodeBlockComponent from './CodeBlockComponent.vue' import CodeBlockComponent from './CodeBlockComponent.vue'
// load all highlight.js languages lowlight.registerLanguage('html', html)
import lowlight from 'lowlight' lowlight.registerLanguage('css', css)
lowlight.registerLanguage('js', js)
lowlight.registerLanguage('ts', ts)
// load specific languages only // load specific languages only
// import lowlight from 'lowlight/lib/core' // import { lowlight } from 'lowlight/lib/core'
// import javascript from 'highlight.js/lib/languages/javascript' // import javascript from 'highlight.js/lib/languages/javascript'
// lowlight.registerLanguage('javascript', javascript) // lowlight.registerLanguage('javascript', javascript)

View File

@@ -1,7 +1,9 @@
import React, { Fragment } from 'react'
import MenuItem from './MenuItem'
import './MenuBar.scss' import './MenuBar.scss'
import React, { Fragment } from 'react'
import MenuItem from './MenuItem'
export default ({ editor }) => { export default ({ editor }) => {
const items = [ const items = [
{ {

View File

@@ -1,5 +1,6 @@
import React from 'react'
import './MenuItem.scss' import './MenuItem.scss'
import React from 'react'
import remixiconUrl from 'remixicon/fonts/remixicon.symbol.svg' import remixiconUrl from 'remixicon/fonts/remixicon.symbol.svg'
export default ({ export default ({

View File

@@ -1,18 +1,21 @@
import React, { import './styles.scss'
useState, useCallback, useEffect,
} from 'react' import { HocuspocusProvider } from '@hocuspocus/provider'
import * as Y from 'yjs'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
import Highlight from '@tiptap/extension-highlight'
import CharacterCount from '@tiptap/extension-character-count' import CharacterCount from '@tiptap/extension-character-count'
import Collaboration from '@tiptap/extension-collaboration' import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor' import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import { HocuspocusProvider } from '@hocuspocus/provider' import Highlight from '@tiptap/extension-highlight'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React, {
useCallback, useEffect,
useState,
} from 'react'
import * as Y from 'yjs'
import MenuBar from './MenuBar' import MenuBar from './MenuBar'
import './styles.scss'
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D'] const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']
const rooms = ['rooms.10', 'rooms.11', 'rooms.12'] const rooms = ['rooms.10', 'rooms.11', 'rooms.12']
@@ -56,7 +59,7 @@ const ydoc = new Y.Doc()
const websocketProvider = new HocuspocusProvider({ const websocketProvider = new HocuspocusProvider({
url: 'wss://connect.hocuspocus.cloud', url: 'wss://connect.hocuspocus.cloud',
parameters: { parameters: {
key: 'write_B0sHbuV5xwYl6WzGjoqL', key: 'write_bqgvQ3Zwl34V4Nxt43zR',
}, },
name: room, name: room,
document: ydoc, document: ydoc,

View File

@@ -1,7 +1,21 @@
context('/src/Examples/CollaborativeEditing/React/', () => { context('/src/Examples/CollaborativeEditing/React/', () => {
before(() => { beforeEach(() => {
cy.visit('/src/Examples/CollaborativeEditing/React/') cy.visit('/src/Examples/CollaborativeEditing/React/')
}) })
// TODO: Write tests /* it('should show the current room with participants', () => {
cy.wait(6000)
cy.get('.editor__status')
.should('contain', 'rooms.')
.should('contain', 'users online')
})
it('should allow user to change name', () => {
cy.window().then(win => {
cy.stub(win, 'prompt').returns('John Doe')
cy.get('.editor__name > button').click()
cy.wait(6000)
cy.get('.editor__name').should('contain', 'John Doe')
})
}) */
}) })

View File

@@ -1,7 +1,21 @@
context('/src/Examples/CollaborativeEditing/Vue/', () => { context('/src/Examples/CollaborativeEditing/Vue/', () => {
before(() => { beforeEach(() => {
cy.visit('/src/Examples/CollaborativeEditing/Vue/') cy.visit('/src/Examples/CollaborativeEditing/Vue/')
}) })
// TODO: Write tests /* it('should show the current room with participants', () => {
cy.wait(6000)
cy.get('.editor__status')
.should('contain', 'rooms.')
.should('contain', 'users online')
})
it('should allow user to change name', () => {
cy.window().then(win => {
cy.stub(win, 'prompt').returns('John Doe')
cy.get('.editor__name > button').click()
cy.wait(6000)
cy.get('.editor__name').should('contain', 'John Doe')
})
}) */
}) })

View File

@@ -21,16 +21,17 @@
</template> </template>
<script> <script>
import { Editor, EditorContent } from '@tiptap/vue-3' import { HocuspocusProvider } from '@hocuspocus/provider'
import StarterKit from '@tiptap/starter-kit' import CharacterCount from '@tiptap/extension-character-count'
import Collaboration from '@tiptap/extension-collaboration' import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor' import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import TaskList from '@tiptap/extension-task-list'
import TaskItem from '@tiptap/extension-task-item'
import Highlight from '@tiptap/extension-highlight' import Highlight from '@tiptap/extension-highlight'
import CharacterCount from '@tiptap/extension-character-count' import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
import * as Y from 'yjs' import * as Y from 'yjs'
import { HocuspocusProvider } from '@hocuspocus/provider'
import MenuBar from './MenuBar.vue' import MenuBar from './MenuBar.vue'
const getRandomElement = list => { const getRandomElement = list => {
@@ -70,7 +71,7 @@ export default {
this.provider = new HocuspocusProvider({ this.provider = new HocuspocusProvider({
url: 'wss://connect.hocuspocus.cloud', url: 'wss://connect.hocuspocus.cloud',
parameters: { parameters: {
key: 'write_B0sHbuV5xwYl6WzGjoqL', key: 'write_bqgvQ3Zwl34V4Nxt43zR',
}, },
name: this.room, name: this.room,
document: ydoc, document: ydoc,

View File

@@ -1,11 +1,12 @@
import React, {
useState,
useEffect,
forwardRef,
useImperativeHandle,
} from 'react'
import './MentionList.scss' import './MentionList.scss'
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react'
export const MentionList = forwardRef((props, ref) => { export const MentionList = forwardRef((props, ref) => {
const [selectedIndex, setSelectedIndex] = useState(0) const [selectedIndex, setSelectedIndex] = useState(0)

View File

@@ -1,12 +1,14 @@
import React from 'react' import './styles.scss'
import { useEditor, EditorContent } from '@tiptap/react'
import CharacterCount from '@tiptap/extension-character-count'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Mention from '@tiptap/extension-mention'
import Paragraph from '@tiptap/extension-paragraph' import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
import CharacterCount from '@tiptap/extension-character-count' import { EditorContent, useEditor } from '@tiptap/react'
import Mention from '@tiptap/extension-mention' import React from 'react'
import suggestion from './suggestion' import suggestion from './suggestion'
import './styles.scss'
export default () => { export default () => {
const limit = 280 const limit = 280

View File

@@ -0,0 +1,37 @@
context('/src/Examples/Community/React/', () => {
beforeEach(() => {
cy.visit('/src/Examples/Community/React/')
})
it('should count the characters correctly', () => {
// check if count text is "44/280 characters"
cy.get('.character-count__text').should('have.text', '44/280 characters')
// type in .ProseMirror
cy.get('.ProseMirror').type(' Hello World')
cy.get('.character-count__text').should('have.text', '56/280 characters')
// remove content from .ProseMirror and enter text
cy.get('.ProseMirror').type('{selectall}{backspace}Hello World')
cy.get('.character-count__text').should('have.text', '11/280 characters')
})
it('should mention a user', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}@')
// check if the mention autocomplete is visible
cy.get('.tippy-content .items').should('be.visible')
// select the first user
cy.get('.tippy-content .items .item').first().then($el => {
const name = $el.text()
$el.click()
// check if the user is mentioned
cy.get('.ProseMirror').should('have.text', `@${name} `)
cy.get('.character-count__text').should('have.text', '2/280 characters')
})
})
})

View File

@@ -1,5 +1,6 @@
import { ReactRenderer } from '@tiptap/react' import { ReactRenderer } from '@tiptap/react'
import tippy from 'tippy.js' import tippy from 'tippy.js'
import { MentionList } from './MentionList' import { MentionList } from './MentionList'
export default { export default {
@@ -20,6 +21,10 @@ export default {
editor: props.editor, editor: props.editor,
}) })
if (!props.clientRect) {
return
}
popup = tippy('body', { popup = tippy('body', {
getReferenceClientRect: props.clientRect, getReferenceClientRect: props.clientRect,
appendTo: () => document.body, appendTo: () => document.body,
@@ -34,6 +39,10 @@ export default {
onUpdate(props) { onUpdate(props) {
reactRenderer.updateProps(props) reactRenderer.updateProps(props)
if (!props.clientRect) {
return
}
popup[0].setProps({ popup[0].setProps({
getReferenceClientRect: props.clientRect, getReferenceClientRect: props.clientRect,
}) })

View File

@@ -1,7 +1,37 @@
context('/src/Examples/Community/Vue/', () => { context('/src/Examples/Community/Vue/', () => {
before(() => { beforeEach(() => {
cy.visit('/src/Examples/Community/Vue/') cy.visit('/src/Examples/Community/Vue/')
}) })
// TODO: Write tests it('should count the characters correctly', () => {
// check if count text is "44/280 characters"
cy.get('.character-count__text').should('have.text', '44/280 characters')
// type in .ProseMirror
cy.get('.ProseMirror').type(' Hello World')
cy.get('.character-count__text').should('have.text', '56/280 characters')
// remove content from .ProseMirror and enter text
cy.get('.ProseMirror').type('{selectall}{backspace}Hello World')
cy.get('.character-count__text').should('have.text', '11/280 characters')
})
it('should mention a user', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}@')
// check if the mention autocomplete is visible
cy.get('.tippy-content .items').should('be.visible')
// select the first user
cy.get('.tippy-content .items .item').first().then($el => {
const name = $el.text()
$el.click()
// check if the user is mentioned
cy.get('.ProseMirror').should('have.text', `@${name} `)
cy.get('.character-count__text').should('have.text', '2/280 characters')
})
})
}) })

View File

@@ -32,19 +32,18 @@
/> />
</svg> </svg>
<div class="character-count__text"> <div class="character-count__text">{{ editor.storage.characterCount.characters() }}/{{ limit }} characters</div>
{{ editor.storage.characterCount.characters() }}/{{ limit }} characters
</div>
</div> </div>
</template> </template>
<script> <script>
import { Editor, EditorContent } from '@tiptap/vue-3' import CharacterCount from '@tiptap/extension-character-count'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Mention from '@tiptap/extension-mention'
import Paragraph from '@tiptap/extension-paragraph' import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
import CharacterCount from '@tiptap/extension-character-count' import { Editor, EditorContent } from '@tiptap/vue-3'
import Mention from '@tiptap/extension-mention'
import suggestion from './suggestion' import suggestion from './suggestion'
export default { export default {

View File

@@ -1,5 +1,6 @@
import { VueRenderer } from '@tiptap/vue-3' import { VueRenderer } from '@tiptap/vue-3'
import tippy from 'tippy.js' import tippy from 'tippy.js'
import MentionList from './MentionList.vue' import MentionList from './MentionList.vue'
export default { export default {
@@ -23,6 +24,10 @@ export default {
editor: props.editor, editor: props.editor,
}) })
if (!props.clientRect) {
return
}
popup = tippy('body', { popup = tippy('body', {
getReferenceClientRect: props.clientRect, getReferenceClientRect: props.clientRect,
appendTo: () => document.body, appendTo: () => document.body,
@@ -37,6 +42,10 @@ export default {
onUpdate(props) { onUpdate(props) {
component.updateProps(props) component.updateProps(props)
if (!props.clientRect) {
return
}
popup[0].setProps({ popup[0].setProps({
getReferenceClientRect: props.clientRect, getReferenceClientRect: props.clientRect,
}) })

View File

@@ -1,9 +1,10 @@
import React from 'react' import './styles.scss'
import { useEditor, EditorContent } from '@tiptap/react'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import './styles.scss' import React from 'react'
const CustomDocument = Document.extend({ const CustomDocument = Document.extend({
content: 'heading block*', content: 'heading block*',

View File

@@ -0,0 +1,46 @@
context('/src/Examples/CustomDocument/React/', () => {
beforeEach(() => {
cy.visit('/src/Examples/CustomDocument/React/')
})
it('should have a working tiptap instance', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
// eslint-disable-next-line
expect(editor).to.not.be.null
})
})
it('should have a headline and a paragraph', () => {
cy.get('.ProseMirror h1').should('exist').should('have.text', 'Itll always have a heading …')
cy.get('.ProseMirror p').should('exist').should('have.text', '… if you pass a custom document. Thats the beauty of having full control over the schema.')
})
it('should have a tooltip for a paragraph on a new line', () => {
cy.get('.ProseMirror').type('{enter}')
cy.get('.ProseMirror p[data-placeholder]').should('exist').should('have.attr', 'data-placeholder', 'Can you add some further context?')
})
it('should have a headline after clearing the document', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.get('.ProseMirror').focus()
cy.get('.ProseMirror h1[data-placeholder]')
.should('exist')
.should('have.attr', 'class', 'is-empty is-editor-empty')
.should('have.attr', 'data-placeholder', 'Whats the title?')
})
it('should have a headline after clearing the document & enter paragraph automatically after adding a headline', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}Hello world{enter}')
cy.get('.ProseMirror h1')
.should('exist')
.should('have.text', 'Hello world')
cy.get('.ProseMirror p[data-placeholder]')
.should('exist')
.should('have.attr', 'data-placeholder', 'Can you add some further context?')
cy.get('.ProseMirror').type('This is a paragraph for this test document')
cy.get('.ProseMirror p')
.should('exist')
.should('have.text', 'This is a paragraph for this test document')
})
})

View File

@@ -1,7 +1,48 @@
context('/src/Examples/CustomDocument/Vue/', () => { context('/src/Examples/CustomDocument/Vue/', () => {
before(() => { beforeEach(() => {
cy.visit('/src/Examples/CustomDocument/Vue/') cy.visit('/src/Examples/CustomDocument/Vue/')
}) })
// TODO: Write tests it('should have a working tiptap instance', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
// eslint-disable-next-line
expect(editor).to.not.be.null
})
})
it('should have a headline and a paragraph', () => {
cy.get('.ProseMirror h1').should('exist').should('have.text', 'Itll always have a heading …')
cy.get('.ProseMirror p').should('exist').should('have.text', '… if you pass a custom document. Thats the beauty of having full control over the schema.')
})
it('should have a tooltip for a paragraph on a new line', () => {
cy.get('.ProseMirror').type('{enter}')
cy.get('.ProseMirror p[data-placeholder]').should('exist').should('have.attr', 'data-placeholder', 'Can you add some further context?')
})
it('should have a headline after clearing the document', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.wait(100)
cy.get('.ProseMirror').focus()
cy.get('.ProseMirror h1[data-placeholder]')
.should('exist')
.should('have.attr', 'class', 'is-empty is-editor-empty')
.should('have.attr', 'data-placeholder', 'Whats the title?')
})
it('should have a headline after clearing the document & enter paragraph automatically after adding a headline', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}Hello world{enter}')
cy.wait(100)
cy.get('.ProseMirror h1')
.should('exist')
.should('have.text', 'Hello world')
cy.get('.ProseMirror p[data-placeholder]')
.should('exist')
.should('have.attr', 'data-placeholder', 'Can you add some further context?')
cy.get('.ProseMirror').type('This is a paragraph for this test document')
cy.get('.ProseMirror p')
.should('exist')
.should('have.text', 'This is a paragraph for this test document')
})
}) })

View File

@@ -3,10 +3,10 @@
</template> </template>
<script> <script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
const CustomDocument = Document.extend({ const CustomDocument = Document.extend({
content: 'heading block*', content: 'heading block*',

View File

@@ -1,8 +1,9 @@
import React from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import './styles.scss' import './styles.scss'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'
const MenuBar = ({ editor }) => { const MenuBar = ({ editor }) => {
if (!editor) { if (!editor) {
return null return null

View File

@@ -19,4 +19,99 @@ context('/src/Examples/Default/React/', () => {
.find('p') .find('p')
.should('contain', 'Example Text') .should('contain', 'Example Text')
}) })
const buttonMarks = [
{ label: 'bold', tag: 'strong' },
{ label: 'italic', tag: 'em' },
{ label: 'strike', tag: 's' },
]
buttonMarks.forEach(m => {
it(`should apply ${m.label} when the button is pressed`, () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}')
cy.get('button').contains(m.label).click()
cy.get(`.ProseMirror ${m.tag}`).should('exist').should('have.text', 'Hello world')
})
})
it('should clear marks when the button is pressed', () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}')
cy.get('button').contains('bold').click()
cy.get('.ProseMirror strong').should('exist').should('have.text', 'Hello world')
cy.get('button').contains('clear marks').click()
cy.get('.ProseMirror strong').should('not.exist')
})
it('should clear nodes when the button is pressed', () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('bullet list').click()
cy.get('.ProseMirror ul').should('exist').should('have.text', 'Hello world')
cy.get('.ProseMirror').type('{enter}A second item{enter}A third item{selectall}')
cy.get('button').contains('clear nodes').click()
cy.get('.ProseMirror ul').should('not.exist')
cy.get('.ProseMirror p').should('have.length', 3)
})
const buttonNodes = [
{ label: 'h1', tag: 'h1' },
{ label: 'h2', tag: 'h2' },
{ label: 'h3', tag: 'h3' },
{ label: 'h4', tag: 'h4' },
{ label: 'h5', tag: 'h5' },
{ label: 'h6', tag: 'h6' },
{ label: 'bullet list', tag: 'ul' },
{ label: 'ordered list', tag: 'ol' },
{ label: 'code block', tag: 'pre code' },
{ label: 'blockquote', tag: 'blockquote' },
]
buttonNodes.forEach(n => {
it(`should set ${n.label} when the button is pressed`, () => {
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}Hello world{selectall}')
cy.get('button').contains(n.label).click()
cy.get(`.ProseMirror ${n.tag}`).should('exist').should('have.text', 'Hello world')
cy.get('button').contains(n.label).click()
cy.get(`.ProseMirror ${n.tag}`).should('not.exist')
})
})
it('should add a hr when on the same line as a node', () => {
cy.get('.ProseMirror').type('{rightArrow}')
cy.get('button').contains('horizontal rule').click()
cy.get('.ProseMirror hr').should('exist')
cy.get('.ProseMirror h1').should('exist')
})
it('should add a hr when on a new line', () => {
cy.get('.ProseMirror').type('{rightArrow}{enter}')
cy.get('button').contains('horizontal rule').click()
cy.get('.ProseMirror hr').should('exist')
cy.get('.ProseMirror h1').should('exist')
})
it('should add a br', () => {
cy.get('.ProseMirror').type('{rightArrow}')
cy.get('button').contains('hard break').click()
cy.get('.ProseMirror h1 br').should('exist')
})
it('should undo', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.get('button').contains('undo').click()
cy.get('.ProseMirror').should('contain', 'Hello world')
})
it('should redo', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.get('button').contains('undo').click()
cy.get('.ProseMirror').should('contain', 'Hello world')
cy.get('button').contains('redo').click()
cy.get('.ProseMirror').should('not.contain', 'Hello world')
})
}) })

View File

@@ -0,0 +1,117 @@
context('/src/Examples/Default/React/', () => {
before(() => {
cy.visit('/src/Examples/Default/React/')
})
beforeEach(() => {
cy.get('.ProseMirror').then(([{ editor }]) => {
editor.commands.setContent('<h1>Example Text</h1>')
cy.get('.ProseMirror').type('{selectall}')
})
})
it('should apply the paragraph style when the keyboard shortcut is pressed', () => {
cy.get('.ProseMirror h1').should('exist')
cy.get('.ProseMirror p').should('not.exist')
cy.get('.ProseMirror')
.trigger('keydown', { modKey: true, altKey: true, key: '0' })
.find('p')
.should('contain', 'Example Text')
})
const buttonMarks = [
{ label: 'bold', tag: 'strong' },
{ label: 'italic', tag: 'em' },
{ label: 'strike', tag: 's' },
]
buttonMarks.forEach(m => {
it(`should apply ${m.label} when the button is pressed`, () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}')
cy.get('button').contains(m.label).click()
cy.get(`.ProseMirror ${m.tag}`).should('exist').should('have.text', 'Hello world')
})
})
it('should clear marks when the button is pressed', () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}')
cy.get('button').contains('bold').click()
cy.get('.ProseMirror strong').should('exist').should('have.text', 'Hello world')
cy.get('button').contains('clear marks').click()
cy.get('.ProseMirror strong').should('not.exist')
})
it('should clear nodes when the button is pressed', () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('bullet list').click()
cy.get('.ProseMirror ul').should('exist').should('have.text', 'Hello world')
cy.get('.ProseMirror').type('{enter}A second item{enter}A third item{selectall}')
cy.get('button').contains('clear nodes').click()
cy.get('.ProseMirror ul').should('not.exist')
cy.get('.ProseMirror p').should('have.length', 3)
})
const buttonNodes = [
{ label: 'h1', tag: 'h1' },
{ label: 'h2', tag: 'h2' },
{ label: 'h3', tag: 'h3' },
{ label: 'h4', tag: 'h4' },
{ label: 'h5', tag: 'h5' },
{ label: 'h6', tag: 'h6' },
{ label: 'bullet list', tag: 'ul' },
{ label: 'ordered list', tag: 'ol' },
{ label: 'code block', tag: 'pre code' },
{ label: 'blockquote', tag: 'blockquote' },
]
buttonNodes.forEach(n => {
it(`should set ${n.label} when the button is pressed`, () => {
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}Hello world{selectall}')
cy.get('button').contains(n.label).click()
cy.get(`.ProseMirror ${n.tag}`).should('exist').should('have.text', 'Hello world')
cy.get('button').contains(n.label).click()
cy.get(`.ProseMirror ${n.tag}`).should('not.exist')
})
})
it('should add a hr when on the same line as a node', () => {
cy.get('.ProseMirror').type('{rightArrow}')
cy.get('button').contains('horizontal rule').click()
cy.get('.ProseMirror hr').should('exist')
cy.get('.ProseMirror h1').should('exist')
})
it('should add a hr when on a new line', () => {
cy.get('.ProseMirror').type('{rightArrow}{enter}')
cy.get('button').contains('horizontal rule').click()
cy.get('.ProseMirror hr').should('exist')
cy.get('.ProseMirror h1').should('exist')
})
it('should add a br', () => {
cy.get('.ProseMirror').type('{rightArrow}')
cy.get('button').contains('hard break').click()
cy.get('.ProseMirror h1 br').should('exist')
})
it('should undo', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.get('button').contains('undo').click()
cy.get('.ProseMirror').should('contain', 'Hello world')
})
it('should redo', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.get('button').contains('undo').click()
cy.get('.ProseMirror').should('contain', 'Hello world')
cy.get('button').contains('redo').click()
cy.get('.ProseMirror').should('not.contain', 'Hello world')
})
})

View File

@@ -0,0 +1,157 @@
<script>
import "./styles.scss";
import StarterKit from "@tiptap/starter-kit";
import { Editor } from "@tiptap/core";
import { onMount } from "svelte";
let element;
let editor;
onMount(() => {
editor = new Editor({
element: element,
extensions: [StarterKit],
content: `
<h2>
Hi there,
</h2>
<p>
this is a <em>basic</em> example of <strong>tiptap</strong>. Sure, there are all kind of basic text styles youd probably expect from a text editor. But wait until you see the lists:
</p>
<ul>
<li>
Thats a bullet list with one …
</li>
<li>
… or two list items.
</li>
</ul>
<p>
Isnt that great? And all of that is editable. But wait, theres more. Lets try a code block:
</p>
<pre><code class="language-css">body {
display: none;
}</code></pre>
<p>
I know, I know, this is impressive. Its only the tip of the iceberg though. Give it a try and click a little bit around. Dont forget to check the other examples too.
</p>
<blockquote>
Wow, thats amazing. Good work, boy! 👏
<br />
— Mom
</blockquote>
`,
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor;
},
});
});
</script>
{#if editor}
<div>
<div>
<button
on:click={() => console.log && editor.chain().focus().toggleBold().run()}
class={editor.isActive("bold") ? "is-active" : ""}
>
bold
</button>
<button
on:click={() => editor.chain().focus().toggleItalic().run()}
class={editor.isActive("italic") ? "is-active" : ""}
>
italic
</button>
<button
on:click={() => editor.chain().focus().toggleStrike().run()}
class={editor.isActive("strike") ? "is-active" : ""}
>
strike
</button>
<button
on:click={() => editor.chain().focus().toggleCode().run()}
class={editor.isActive("code") ? "is-active" : ""}
>
code
</button>
<button on:click={() => editor.chain().focus().unsetAllMarks().run()}> clear marks </button>
<button on:click={() => editor.chain().focus().clearNodes().run()}> clear nodes </button>
<button
on:click={() => editor.chain().focus().setParagraph().run()}
class={editor.isActive("paragraph") ? "is-active" : ""}
>
paragraph
</button>
<button
on:click={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
class={editor.isActive("heading", { level: 1 }) ? "is-active" : ""}
>
h1
</button>
<button
on:click={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
class={editor.isActive("heading", { level: 2 }) ? "is-active" : ""}
>
h2
</button>
<button
on:click={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
class={editor.isActive("heading", { level: 3 }) ? "is-active" : ""}
>
h3
</button>
<button
on:click={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
class={editor.isActive("heading", { level: 4 }) ? "is-active" : ""}
>
h4
</button>
<button
on:click={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
class={editor.isActive("heading", { level: 5 }) ? "is-active" : ""}
>
h5
</button>
<button
on:click={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
class={editor.isActive("heading", { level: 6 }) ? "is-active" : ""}
>
h6
</button>
<button
on:click={() => editor.chain().focus().toggleBulletList().run()}
class={editor.isActive("bulletList") ? "is-active" : ""}
>
bullet list
</button>
<button
on:click={() => editor.chain().focus().toggleOrderedList().run()}
class={editor.isActive("orderedList") ? "is-active" : ""}
>
ordered list
</button>
<button
on:click={() => editor.chain().focus().toggleCodeBlock().run()}
class={editor.isActive("codeBlock") ? "is-active" : ""}
>
code block
</button>
<button
on:click={() => editor.chain().focus().toggleBlockquote().run()}
class={editor.isActive("blockquote") ? "is-active" : ""}
>
blockquote
</button>
<button on:click={() => editor.chain().focus().setHorizontalRule().run()}>
horizontal rule
</button>
<button on:click={() => editor.chain().focus().setHardBreak().run()}> hard break </button>
<button on:click={() => editor.chain().focus().undo().run()}> undo </button>
<button on:click={() => editor.chain().focus().redo().run()}> redo </button>
</div>
</div>
{/if}
<div bind:this={element} />

View File

@@ -0,0 +1,56 @@
/* Basic editor styles */
.ProseMirror {
> * + * {
margin-top: 0.75em;
}
ul,
ol {
padding: 0 1rem;
}
h1,
h2,
h3,
h4,
h5,
h6 {
line-height: 1.1;
}
code {
background-color: rgba(#616161, 0.1);
color: #616161;
}
pre {
background: #0D0D0D;
color: #FFF;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
code {
color: inherit;
padding: 0;
background: none;
font-size: 0.8rem;
}
}
img {
max-width: 100%;
height: auto;
}
blockquote {
padding-left: 1rem;
border-left: 2px solid rgba(#0D0D0D, 0.1);
}
hr {
border: none;
border-top: 2px solid rgba(#0D0D0D, 0.1);
margin: 2rem 0;
}
}

View File

@@ -19,4 +19,99 @@ context('/src/Examples/Default/Vue/', () => {
.find('p') .find('p')
.should('contain', 'Example Text') .should('contain', 'Example Text')
}) })
const buttonMarks = [
{ label: 'bold', tag: 'strong' },
{ label: 'italic', tag: 'em' },
{ label: 'strike', tag: 's' },
]
buttonMarks.forEach(m => {
it(`should apply ${m.label} when the button is pressed`, () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}')
cy.get('button').contains(m.label).click()
cy.get(`.ProseMirror ${m.tag}`).should('exist').should('have.text', 'Hello world')
})
})
it('should clear marks when the button is pressed', () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}')
cy.get('button').contains('bold').click()
cy.get('.ProseMirror strong').should('exist').should('have.text', 'Hello world')
cy.get('button').contains('clear marks').click()
cy.get('.ProseMirror strong').should('not.exist')
})
it('should clear nodes when the button is pressed', () => {
cy.get('.ProseMirror').type('{selectall}Hello world')
cy.get('button').contains('bullet list').click()
cy.get('.ProseMirror ul').should('exist').should('have.text', 'Hello world')
cy.get('.ProseMirror').type('{enter}A second item{enter}A third item{selectall}')
cy.get('button').contains('clear nodes').click()
cy.get('.ProseMirror ul').should('not.exist')
cy.get('.ProseMirror p').should('have.length', 3)
})
const buttonNodes = [
{ label: 'h1', tag: 'h1' },
{ label: 'h2', tag: 'h2' },
{ label: 'h3', tag: 'h3' },
{ label: 'h4', tag: 'h4' },
{ label: 'h5', tag: 'h5' },
{ label: 'h6', tag: 'h6' },
{ label: 'bullet list', tag: 'ul' },
{ label: 'ordered list', tag: 'ol' },
{ label: 'code block', tag: 'pre code' },
{ label: 'blockquote', tag: 'blockquote' },
]
buttonNodes.forEach(n => {
it(`should set ${n.label} when the button is pressed`, () => {
cy.get('button').contains('paragraph').click()
cy.get('.ProseMirror').type('{selectall}Hello world{selectall}')
cy.get('button').contains(n.label).click()
cy.get(`.ProseMirror ${n.tag}`).should('exist').should('have.text', 'Hello world')
cy.get('button').contains(n.label).click()
cy.get(`.ProseMirror ${n.tag}`).should('not.exist')
})
})
it('should add a hr when on the same line as a node', () => {
cy.get('.ProseMirror').type('{rightArrow}')
cy.get('button').contains('horizontal rule').click()
cy.get('.ProseMirror hr').should('exist')
cy.get('.ProseMirror h1').should('exist')
})
it('should add a hr when on a new line', () => {
cy.get('.ProseMirror').type('{rightArrow}{enter}')
cy.get('button').contains('horizontal rule').click()
cy.get('.ProseMirror hr').should('exist')
cy.get('.ProseMirror h1').should('exist')
})
it('should add a br', () => {
cy.get('.ProseMirror').type('{rightArrow}')
cy.get('button').contains('hard break').click()
cy.get('.ProseMirror h1 br').should('exist')
})
it('should undo', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.get('button').contains('undo').click()
cy.get('.ProseMirror').should('contain', 'Hello world')
})
it('should redo', () => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
cy.get('button').contains('undo').click()
cy.get('.ProseMirror').should('contain', 'Hello world')
cy.get('button').contains('redo').click()
cy.get('.ProseMirror').should('not.contain', 'Hello world')
})
}) })

View File

@@ -68,8 +68,8 @@
</template> </template>
<script> <script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
export default { export default {
components: { components: {

View File

@@ -26,9 +26,9 @@
</template> </template>
<script> <script>
import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3' import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
import { v4 as uuid } from 'uuid'
import * as d3 from 'd3' import * as d3 from 'd3'
import { v4 as uuid } from 'uuid'
const getRandomElement = list => { const getRandomElement = list => {
return list[Math.floor(Math.random() * list.length)] return list[Math.floor(Math.random() * list.length)]

View File

@@ -1,4 +1,5 @@
import { VueNodeViewRenderer, Node, mergeAttributes } from '@tiptap/vue-3' import { mergeAttributes, Node, VueNodeViewRenderer } from '@tiptap/vue-3'
import Component from './Component.vue' import Component from './Component.vue'
export default Node.create({ export default Node.create({

View File

@@ -0,0 +1,38 @@
context('/src/Examples/Drawing/Vue/', () => {
beforeEach(() => {
cy.visit('/src/Examples/Drawing/Vue/')
})
it('should have a working tiptap instance', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
// eslint-disable-next-line
expect(editor).to.not.be.null
})
})
it('should have a svg canvas', () => {
cy.get('.ProseMirror svg').should('exist')
})
it('should draw on the svg canvas', () => {
cy.get('.ProseMirror svg').should('exist')
cy.wait(500)
cy.get('input').then(inputs => {
const color = inputs[0].value
const size = inputs[1].value
cy.get('.ProseMirror svg')
.click()
.trigger('mousedown', { pageX: 100, pageY: 100, which: 1 })
.trigger('mousemove', { pageX: 200, pageY: 200, which: 1 })
.trigger('mouseup')
cy.get('.ProseMirror svg path')
.should('exist')
.should('have.attr', 'stroke-width', size)
.should('have.attr', 'stroke', color.toUpperCase())
})
})
})

View File

@@ -3,9 +3,10 @@
</template> </template>
<script> <script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
import { Editor, EditorContent } from '@tiptap/vue-3'
import Paper from './Paper' import Paper from './Paper'
export default { export default {

View File

@@ -1,10 +1,11 @@
import React from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import TextAlign from '@tiptap/extension-text-align'
import Highlight from '@tiptap/extension-highlight'
import './styles.scss' import './styles.scss'
import Highlight from '@tiptap/extension-highlight'
import TextAlign from '@tiptap/extension-text-align'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import React from 'react'
const MenuBar = ({ editor }) => { const MenuBar = ({ editor }) => {
if (!editor) { if (!editor) {
return null return null

View File

@@ -0,0 +1,38 @@
context('/src/Examples/Formatting/React/', () => {
before(() => {
cy.visit('/src/Examples/Formatting/React/')
})
beforeEach(() => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
})
const marks = [
{ label: 'highlight', mark: 'mark' },
]
marks.forEach(m => {
it(`sets ${m.label}`, () => {
cy.get('.ProseMirror').type('Hello world.{selectall}')
cy.get('button').contains(m.label).click()
cy.get('.ProseMirror mark').should('exist')
})
})
const alignments = [
{ label: 'left', alignment: 'left' },
{ label: 'center', alignment: 'center' },
{ label: 'right', alignment: 'right' },
{ label: 'justify', alignment: 'justify' },
]
alignments.forEach(a => {
it(`sets ${a.label}`, () => {
cy.get('.ProseMirror').type('Hello world.{selectall}')
cy.get('button').contains(a.label).click()
if (a.alignment !== 'left') {
cy.get('.ProseMirror p').should('have.css', 'text-align', a.alignment)
}
})
})
})

View File

@@ -3,5 +3,36 @@ context('/src/Examples/Formatting/Vue/', () => {
cy.visit('/src/Examples/Formatting/Vue/') cy.visit('/src/Examples/Formatting/Vue/')
}) })
// TODO: Write tests beforeEach(() => {
cy.get('.ProseMirror').type('{selectall}{backspace}')
})
const marks = [
{ label: 'highlight', mark: 'mark' },
]
marks.forEach(m => {
it(`sets ${m.label}`, () => {
cy.get('.ProseMirror').type('Hello world.{selectall}')
cy.get('button').contains(m.label).click()
cy.get('.ProseMirror mark').should('exist')
})
})
const alignments = [
{ label: 'left', alignment: 'left' },
{ label: 'center', alignment: 'center' },
{ label: 'right', alignment: 'right' },
{ label: 'justify', alignment: 'justify' },
]
alignments.forEach(a => {
it(`sets ${a.label}`, () => {
cy.get('.ProseMirror').type('Hello world.{selectall}')
cy.get('button').contains(a.label).click()
if (a.alignment !== 'left') {
cy.get('.ProseMirror p').should('have.css', 'text-align', a.alignment)
}
})
})
}) })

View File

@@ -41,10 +41,10 @@
</template> </template>
<script> <script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import TextAlign from '@tiptap/extension-text-align'
import Highlight from '@tiptap/extension-highlight' import Highlight from '@tiptap/extension-highlight'
import TextAlign from '@tiptap/extension-text-align'
import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
export default { export default {
components: { components: {

View File

@@ -1,11 +1,12 @@
import React from 'react' import './styles.scss'
import { useEditor, EditorContent } from '@tiptap/react'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Dropcursor from '@tiptap/extension-dropcursor'
import Image from '@tiptap/extension-image'
import Paragraph from '@tiptap/extension-paragraph' import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
import Image from '@tiptap/extension-image' import { EditorContent, useEditor } from '@tiptap/react'
import Dropcursor from '@tiptap/extension-dropcursor' import React from 'react'
import './styles.scss'
export default () => { export default () => {
const editor = useEditor({ const editor = useEditor({
@@ -33,9 +34,7 @@ export default () => {
return ( return (
<div> <div>
<button onClick={addImage}> <button onClick={addImage}>add image from URL</button>
add image from URL
</button>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
</div> </div>
) )

View File

@@ -0,0 +1,24 @@
context('/src/Examples/Images/React/', () => {
beforeEach(() => {
cy.visit('/src/Examples/Images/React/')
})
it('finds image elements inside editor', () => {
cy.get('.ProseMirror img').should('have.length', 2)
})
it('allows removing images', () => {
cy.get('.ProseMirror img').should('have.length', 2)
cy.get('.ProseMirror img').first().trigger('mousedown', { which: 1 })
cy.get('.ProseMirror').type('{backspace}')
cy.get('.ProseMirror img').should('have.length', 1)
})
it('allows images to be added via URL', () => {
cy.window().then(win => {
cy.stub(win, 'prompt').returns('https://unsplash.it/250/250')
cy.get('button').contains('add image from URL').click({ force: false })
cy.get('.ProseMirror img').should('have.length', 3)
})
})
})

View File

@@ -1,7 +1,28 @@
context('/src/Examples/Images/Vue/', () => { context('/src/Examples/Images/Vue/', () => {
before(() => { beforeEach(() => {
cy.visit('/src/Examples/Images/Vue/') cy.visit('/src/Examples/Images/Vue/')
}) })
// TODO: Write tests // TODO: Write tests
it('finds image elements inside editor', () => {
cy.get('.ProseMirror img').should('have.length', 2)
})
it('allows removing images', () => {
cy.get('.ProseMirror img').should('have.length', 2)
cy.get('.ProseMirror img').first().trigger('mousedown', { which: 1 })
cy.get('.ProseMirror').type('{backspace}')
cy.get('.ProseMirror img').should('have.length', 1)
})
it('allows images to be added via URL', () => {
cy.window().then(win => {
cy.stub(win, 'prompt').returns('https://unsplash.it/250/250')
cy.wait(1000)
cy.get('button').contains('add image from URL').click({ force: false })
cy.wait(1000)
cy.get('.ProseMirror img').should('have.length', 3)
})
})
}) })

View File

@@ -1,19 +1,17 @@
<template> <template>
<div v-if="editor"> <div v-if="editor">
<button @click="addImage"> <button @click="addImage">add image from URL</button>
add image from URL
</button>
</div> </div>
<editor-content :editor="editor" /> <editor-content :editor="editor" />
</template> </template>
<script> <script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import Document from '@tiptap/extension-document' import Document from '@tiptap/extension-document'
import Dropcursor from '@tiptap/extension-dropcursor'
import Image from '@tiptap/extension-image'
import Paragraph from '@tiptap/extension-paragraph' import Paragraph from '@tiptap/extension-paragraph'
import Text from '@tiptap/extension-text' import Text from '@tiptap/extension-text'
import Image from '@tiptap/extension-image' import { Editor, EditorContent } from '@tiptap/vue-3'
import Dropcursor from '@tiptap/extension-dropcursor'
export default { export default {
components: { components: {

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { NodeViewWrapper } from '@tiptap/react' import { NodeViewWrapper } from '@tiptap/react'
import React from 'react'
export default props => { export default props => {
const increase = () => { const increase = () => {

View File

@@ -1,5 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core' import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react' import { ReactNodeViewRenderer } from '@tiptap/react'
import Component from './Component.jsx' import Component from './Component.jsx'
export default Node.create({ export default Node.create({

View File

@@ -1,9 +1,11 @@
import React from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import ReactComponent from './Extension.js'
import './styles.scss' 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 () => { export default () => {
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [

View File

@@ -0,0 +1,27 @@
context('/src/Examples/InteractivityComponent/React/', () => {
beforeEach(() => {
cy.visit('/src/Examples/InteractivityComponent/React/')
})
it('should have a working tiptap instance', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
// eslint-disable-next-line
expect(editor).to.not.be.null
})
})
it('should render a custom node', () => {
cy.get('.ProseMirror .react-component').should('have.length', 1)
})
it('should handle count click inside custom node', () => {
cy.get('.ProseMirror .react-component button')
.should('have.text', 'This button has been clicked 0 times.')
.click()
.should('have.text', 'This button has been clicked 1 times.')
.click()
.should('have.text', 'This button has been clicked 2 times.')
.click()
.should('have.text', 'This button has been clicked 3 times.')
})
})

View File

@@ -3,15 +3,13 @@
<span class="label">Vue Component</span> <span class="label">Vue Component</span>
<div class="content"> <div class="content">
<button @click="increase"> <button @click="increase">This button has been clicked {{ node.attrs.count }} times.</button>
This button has been clicked {{ node.attrs.count }} times.
</button>
</div> </div>
</node-view-wrapper> </node-view-wrapper>
</template> </template>
<script> <script>
import { NodeViewWrapper, nodeViewProps } from '@tiptap/vue-3' import { nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
export default { export default {
components: { components: {

View File

@@ -1,5 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core' import { mergeAttributes, Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3' import { VueNodeViewRenderer } from '@tiptap/vue-3'
import Component from './Component.vue' import Component from './Component.vue'
export default Node.create({ export default Node.create({

View File

@@ -0,0 +1,27 @@
context('/src/Examples/InteractivityComponent/Vue/', () => {
beforeEach(() => {
cy.visit('/src/Examples/InteractivityComponent/Vue/')
})
it('should have a working tiptap instance', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
// eslint-disable-next-line
expect(editor).to.not.be.null
})
})
it('should render a custom node', () => {
cy.get('.ProseMirror .vue-component').should('have.length', 1)
})
it('should handle count click inside custom node', () => {
cy.get('.ProseMirror .vue-component button')
.should('have.text', 'This button has been clicked 0 times.')
.click()
.should('have.text', 'This button has been clicked 1 times.')
.click()
.should('have.text', 'This button has been clicked 2 times.')
.click()
.should('have.text', 'This button has been clicked 3 times.')
})
})

View File

@@ -3,8 +3,9 @@
</template> </template>
<script> <script>
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit' import StarterKit from '@tiptap/starter-kit'
import { Editor, EditorContent } from '@tiptap/vue-3'
import VueComponent from './Extension.js' import VueComponent from './Extension.js'
export default { export default {

View File

@@ -1,5 +1,5 @@
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import React from 'react' import React from 'react'
import { NodeViewWrapper, NodeViewContent } from '@tiptap/react'
export default () => { export default () => {
return ( return (

View File

@@ -1,5 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core' import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react' import { ReactNodeViewRenderer } from '@tiptap/react'
import Component from './Component.jsx' import Component from './Component.jsx'
export default Node.create({ export default Node.create({

View File

@@ -1,9 +1,11 @@
import React from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import ReactComponent from './Extension.js'
import './styles.scss' 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 () => { export default () => {
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [

View File

@@ -0,0 +1,47 @@
context('/src/Examples/InteractivityComponentContent/React/', () => {
beforeEach(() => {
cy.visit('/src/Examples/InteractivityComponentContent/React/')
})
it('should have a working tiptap instance', () => {
cy.get('.ProseMirror').then(([{ editor }]) => {
// eslint-disable-next-line
expect(editor).to.not.be.null
})
})
it('should render a custom node', () => {
cy.get('.ProseMirror .react-component-with-content')
.should('have.length', 1)
})
it('should allow text editing inside component', () => {
cy.get('.ProseMirror .react-component-with-content .content div')
.invoke('attr', 'contentEditable', true)
.invoke('text', '')
.type('Hello World!')
.should('have.text', 'Hello World!')
})
it('should allow text editing inside component with markdown text', () => {
cy.get('.ProseMirror .react-component-with-content .content div')
.invoke('attr', 'contentEditable', true)
.invoke('text', '')
.type('Hello World! This is **bold**.')
.should('have.text', 'Hello World! This is bold.')
cy.get('.ProseMirror .react-component-with-content .content strong')
.should('exist')
})
it('should remove node via selectall', () => {
cy.get('.ProseMirror .react-component-with-content')
.should('have.length', 1)
cy.get('.ProseMirror')
.type('{selectall}{backspace}')
cy.get('.ProseMirror .react-component-with-content')
.should('have.length', 0)
})
})

View File

@@ -6,7 +6,7 @@
</template> </template>
<script> <script>
import { NodeViewWrapper, NodeViewContent, nodeViewProps } from '@tiptap/vue-3' import { NodeViewContent, nodeViewProps, NodeViewWrapper } from '@tiptap/vue-3'
export default { export default {
components: { components: {

View File

@@ -1,5 +1,6 @@
import { Node, mergeAttributes } from '@tiptap/core' import { mergeAttributes, Node } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3' import { VueNodeViewRenderer } from '@tiptap/vue-3'
import Component from './Component.vue' import Component from './Component.vue'
export default Node.create({ export default Node.create({

Some files were not shown because too many files have changed in this diff Show More