diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 000000000..e5b6d8d6a --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/chatty-monkeys-hear.md b/.changeset/chatty-monkeys-hear.md new file mode 100644 index 000000000..8f8d01daf --- /dev/null +++ b/.changeset/chatty-monkeys-hear.md @@ -0,0 +1,5 @@ +--- +"@tiptap/extension-list-keymap": patch +--- + +Fix backspace behavior when selection is not collapsed diff --git a/.changeset/chatty-pianos-learn.md b/.changeset/chatty-pianos-learn.md new file mode 100644 index 000000000..201b5076c --- /dev/null +++ b/.changeset/chatty-pianos-learn.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": patch +--- + +preserve existing node attributes when running setNode diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..7828f76af --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [["@tiptap/*"]], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [], + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + } +} diff --git a/.changeset/five-flowers-eat.md b/.changeset/five-flowers-eat.md new file mode 100644 index 000000000..a44e7c58d --- /dev/null +++ b/.changeset/five-flowers-eat.md @@ -0,0 +1,5 @@ +--- +"@tiptap/vue-3": patch +--- + +Fix editor destruction before transition end if editor is nested diff --git a/.changeset/five-mice-turn.md b/.changeset/five-mice-turn.md new file mode 100644 index 000000000..a7f0aa1aa --- /dev/null +++ b/.changeset/five-mice-turn.md @@ -0,0 +1,5 @@ +--- +"@tiptap/extension-bubble-menu": patch +--- + +Add `element: HTMLElement` to `shouldShow` options within the BubbleMenu options. diff --git a/.changeset/fresh-coats-relate.md b/.changeset/fresh-coats-relate.md new file mode 100644 index 000000000..e187ea460 --- /dev/null +++ b/.changeset/fresh-coats-relate.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": minor +--- + +Previously, only a json representation of the node could be inserted into the editor. This change allows for the insertion of Prosemirror `Node`s and `Fragment`s directly into the editor through the `insertContentAt`, `setContent` and `insertContent` commands. diff --git a/.changeset/funny-otters-protect.md b/.changeset/funny-otters-protect.md new file mode 100644 index 000000000..63cb6a9e3 --- /dev/null +++ b/.changeset/funny-otters-protect.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": patch +--- + +Addresses a bug with `insertContentAt`'s `simulatedPasteRules` option where it could only accept text and not Prosemirror `Node` and `Content` diff --git a/.changeset/happy-vans-smash.md b/.changeset/happy-vans-smash.md new file mode 100644 index 000000000..71e89f0b5 --- /dev/null +++ b/.changeset/happy-vans-smash.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": patch +--- + +Updates the types of `addOptions` and `addStorage` to have the parent be possibly undefined which is the most accurate typing diff --git a/.changeset/mean-moose-bow.md b/.changeset/mean-moose-bow.md new file mode 100644 index 000000000..6598c1e0e --- /dev/null +++ b/.changeset/mean-moose-bow.md @@ -0,0 +1,5 @@ +--- +"@tiptap/vue-2": patch +--- + +Pin vue-ts-types to a working version for vue-2 diff --git a/.changeset/polite-buttons-wash.md b/.changeset/polite-buttons-wash.md new file mode 100644 index 000000000..69aa677ea --- /dev/null +++ b/.changeset/polite-buttons-wash.md @@ -0,0 +1,5 @@ +--- +"@tiptap/react": patch +--- + +React 19 is now allowed as a peer dep, we did not have to make any changes for React 19 diff --git a/.changeset/serious-coins-fail.md b/.changeset/serious-coins-fail.md new file mode 100644 index 000000000..a44662cbf --- /dev/null +++ b/.changeset/serious-coins-fail.md @@ -0,0 +1,5 @@ +--- +"@tiptap/extension-mention": patch +--- + +add zero-width space to resolve cursor selection issue diff --git a/.changeset/swift-keys-collect.md b/.changeset/swift-keys-collect.md new file mode 100644 index 000000000..d9a6ebf45 --- /dev/null +++ b/.changeset/swift-keys-collect.md @@ -0,0 +1,5 @@ +--- +"@tiptap/core": patch +--- + +Improve handling of selections with `updateAttributes`. Should no longer modify parent nodes of the same type. diff --git a/.changeset/two-rats-watch.md b/.changeset/two-rats-watch.md new file mode 100644 index 000000000..8d772ff8c --- /dev/null +++ b/.changeset/two-rats-watch.md @@ -0,0 +1,5 @@ +--- +"@tiptap/extension-table": patch +--- + +enforce cellMinWidth even on column not resized by the user, fixes #5435 diff --git a/.changeset/witty-olives-protect.md b/.changeset/witty-olives-protect.md new file mode 100644 index 000000000..0893a2fde --- /dev/null +++ b/.changeset/witty-olives-protect.md @@ -0,0 +1,6 @@ +--- +"@tiptap/extension-link": patch +"tiptap-demos": patch +--- + +The link extension's `validate` option now applies to both auto-linking and XSS mitigation. While, the new `shouldAutoLink` option is used to disable auto linking on an otherwise valid url. diff --git a/.eslintrc.js b/.eslintrc.js index ebea8e454..18cd55d3a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,15 @@ module.exports = { node: true, }, overrides: [ + { + files: [ + './**/*.ts', + './**/*.tsx', + './**/*.js', + './**/*.jsx', + ], + extends: ['plugin:react-hooks/recommended'], + }, { files: [ './**/*.ts', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 80cb17335..8f24e4a8e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,9 +1,6 @@ # Global * @bdbch @svenadlung -# docs -/docs/ @svenadlung - # demos /demos/ @bdbch diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index be0a4564a..f3eea948d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,7 +14,7 @@ ## Checklist -- [ ] I have renamed my PR according to the naming conventions. (e.g. `feat: Implement new feature` or `chore(deps): Update dependencies`) +- [ ] I have created a [changeset](https://github.com/changesets/changesets) for this PR if necessary. - [ ] My changes do not break the library. - [ ] I have added tests where applicable. - [ ] I have followed the project guidelines. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ef791381..fdd0413ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,16 +3,22 @@ name: build +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + on: push: branches: - main - develop + - next - release/* pull_request: branches: - main - develop + - next jobs: lint: @@ -20,7 +26,7 @@ jobs: strategy: matrix: - node-version: [16] + node-version: [20] steps: - uses: actions/checkout@v4.1.4 @@ -31,11 +37,12 @@ jobs: node-version: ${{ matrix.node-version }} - name: Load cached dependencies - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 id: cache with: path: | **/node_modules + **/.turbo /home/runner/.cache/Cypress key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} @@ -64,7 +71,7 @@ jobs: strategy: matrix: - node-version: [16] + node-version: [20] test-spec: - { name: "Integration", spec: "./tests/cypress/integration/**/*.spec.{js,ts}" } #- { name: "Demos/Commands", spec: "./demos/src/Commands/**/*.spec.{js,ts}" } @@ -96,10 +103,10 @@ jobs: - name: Test ${{ matrix.test-spec.name }} id: cypress - uses: cypress-io/github-action@v6.6.0 + uses: cypress-io/github-action@v6.7.6 with: cache-key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} - start: npm run start + start: npm run serve wait-on: 'http://localhost:3000' spec: ${{ matrix.test-spec.spec }} project: ./tests @@ -107,7 +114,7 @@ jobs: quiet: true - name: Export screenshots (on failure only) - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.4.3 if: failure() with: name: cypress-screenshots @@ -115,7 +122,7 @@ jobs: retention-days: 7 - name: Export screen recordings (on failure only) - uses: actions/upload-artifact@v4.3.3 + uses: actions/upload-artifact@v4.4.3 if: failure() with: name: cypress-videos @@ -129,7 +136,7 @@ jobs: strategy: matrix: - node-version: [16] + node-version: [20] steps: - uses: actions/checkout@v4.1.4 @@ -140,7 +147,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Load cached dependencies - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 id: cache with: path: | diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 84fe214a7..000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. -# Documentation: https://docs.github.com/en/actions - -name: deploy - -on: - push: - branches: - - main - -jobs: - - deploy: - runs-on: ubuntu-latest - - if: github.ref == 'refs/heads/main' - - steps: - - - name: Update the documentation - run: curl ${{ secrets.TRIGGER_DEPLOYMENT }} diff --git a/.github/workflows/docsearch.yml b/.github/workflows/docsearch.yml deleted file mode 100644 index a2965c97d..000000000 --- a/.github/workflows/docsearch.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. -# Documentation: https://docs.github.com/en/actions - -name: docsearch - -on: - workflow_dispatch: - schedule: - - cron: '5 0 * * *' - -jobs: - - docsearch: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.1.4 - - - name: Run DocSearch Scraper - shell: bash - run: | - docker run \ - -e TYPESENSE_API_KEY=${{ secrets.TYPESENSE_API_KEY }} \ - -e TYPESENSE_HOST="${{ secrets.TYPESENSE_HOST }}" \ - -e TYPESENSE_PORT="${{ secrets.TYPESENSE_PORT }}" \ - -e TYPESENSE_PROTOCOL="${{ secrets.TYPESENSE_PROTOCOL }}" \ - -e CONFIG="$(cat docsearch.config.json | jq -r tostring)" \ - typesense/docsearch-scraper diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..9b2fe4a62 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,88 @@ +name: Publish + +on: + push: + branches: + - main + - develop + # manual trigger for other branches + workflow_dispatch: + +permissions: + id-token: write + contents: write + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + release: + name: Release + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20] + + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + + - name: Setup Node ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://registry.npmjs.org/' + + - name: Load cached dependencies + uses: actions/cache@v4.1.2 + id: cache + with: + path: | + **/node_modules + **/.turbo + /home/runner/.cache/Cypress + key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }} + + - name: Install Dependencies + run: npm ci + + - name: Create Release PR or publish stable version to npm + id: changesets + uses: changesets/action@v1 + with: + createGithubReleases: false + publish: npm run publish + version: npm run version + title: ${{ github.ref_name == 'main' && 'Publish a new stable version' || 'Publish a new pre-release version' }} + commit: >- + ${{ github.ref_name == 'main' && 'chore(release): publish a new release version' || 'chore(release): publish a new pre-release version' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + + - name: Send release notification + if: steps.changesets.outputs.published == 'true' + id: slack + uses: slackapi/slack-github-action@v1.27.0 + with: + payload: | + { + "message": "[Tiptap Editor Release]: New Tiptap Editor version has been released to NPM." + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + - name: Send failure notification + if: failure() + id: slack_failure + uses: slackapi/slack-github-action@v1.27.0 + with: + payload: | + { + "message": "[Tiptap Editor Release]: There was an issue publishing a new version. You can find the logs here: https://github.com/ueberdosis/tiptap/actions/runs/${{ github.run_id }}" + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index aa66f6621..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,31 +0,0 @@ -# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created -# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages - -name: Release new version - -# on github release published or workflow_dispatch -on: - workflow_dispatch: - release: - types: [published] - -jobs: - publish-npm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4.1.4 - - uses: actions/setup-node@v4.0.0 - with: - node-version: 16 - registry-url: https://registry.npmjs.org/ - - run: npm ci - - run: npm run publish - name: "Publish release (current) to NPM" - if: "!github.event.release.prerelease" - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - - run: npm run publish:pre - name: "Publish release (next) to NPM" - if: "github.event.release.prerelease" - env: - NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.gitignore b/.gitignore index bccf33db3..bd129fe8f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ yarn-error.log* # parcel-bundler cache (https://parceljs.org/) .cache +# Turbo cache +.turbo + .rpt2_cache .rts2_cache .rts2_cache_cjs diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100755 index a11011270..000000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env sh -npx --no -- commitlint --edit "$1" diff --git a/CHANGELOG.md b/CHANGELOG.md index 5794f195b..ab8025947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +> **Important information** +> +> As of version 2.4.1 Tiptap uses **Changesets** which don't allow the generation of one generic CHANGELOG file. +> If you want to check changes of a specific package version, check the **CHANGELOG.md** file in the specific package +> directory or out [Github Releases](https://github.com/ueberdosis/tiptap/releases) + All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dcf45d760..e59b86f40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,10 +36,10 @@ Before submitting a pull request: - Check the codebase to ensure that your feature doesn't already exist. - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. -Before commiting: +Before committing: - Make sure to run the tests and linter before committing your changes. -- Write [conventional commit messages](https://www.conventionalcommits.org/en). You can use `npm run cz` for that. +- If you are making changes to one of the packages, make sure to **always** include a [changeset](https://github.com/changesets/changesets) in your PR describing **what changed** with a **description** of the change. Those are responsible for changelog creation ## Requirements diff --git a/LICENSE.md b/LICENSE.md index 8cb208498..13a77e8e7 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023, Tiptap GmbH +Copyright (c) 2024, Tiptap GmbH Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7f8f2214a..739fe587f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,12 @@ For help, discussion about best practices, or any other conversation that would Basewell +
- this is a basic example of tiptap. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists: + this is a basic example of Tiptap. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
Hello World
This is a paragraph
with a break.
And this is some additional string content.
') + cy.get('.tiptap').should('contain.html', 'Hello World
This is a paragraph
with a break.
And this is some additional string content.
') }) it('should keep spaces inbetween tags in html content', () => { @@ -41,4 +41,78 @@ context('/src/Commands/InsertContent/React/', () => { cy.get('.tiptap').should('contain.html', 'foo\nbar
')
})
})
+
+ it('should keep newlines and tabs', () => {
+ cy.get('.tiptap').then(([{ editor }]) => {
+ editor.commands.insertContent('Hello\n\tworld\n\t\thow\n\t\t\tnice.\ntest\tOK
') + cy.get('.tiptap').should('contain.html', 'Hello\n\tworld\n\t\thow\n\t\t\tnice.\ntest\tOK
') + }) + }) + + it('should keep newlines and tabs', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.insertContent('Hello World
') + cy.get('.tiptap').should('contain.html', 'Hello World
') + }) + }) + + it('should allow inserting nothing', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.insertContent('') + cy.get('.tiptap').should('contain.html', '') + }) + }) + + it('should allow inserting a partial HTML tag', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.insertContent('foo') + cy.get('.tiptap').should('contain.html', '
foo
') + }) + }) + + it('should allow inserting an incomplete HTML tag', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.insertContent('foofoo<p
') + }) + }) + + it('should allow inserting a list', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.insertContent('ABC
123
Hello\n World\n
\n', { parseOptions: { preserveWhitespace: false } }) + cy.get('.tiptap').should('contain.html', 'Hello World
') + }) + }) + + it('should respect editor.options.parseOptions if defined to be `false`', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.options.parseOptions = { preserveWhitespace: false } + editor.commands.insertContent('\nHello\n World\n
\n') + cy.get('.tiptap').should('contain.html', 'Hello World
') + }) + }) + + it('should respect editor.options.parseOptions if defined to be `full`', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.options.parseOptions = { preserveWhitespace: 'full' } + editor.commands.insertContent('\nHello\n World\n
\n') + cy.get('.tiptap').should('contain.html', 'Hello\n World
') + }) + }) + + it('should respect editor.options.parseOptions if defined to be `true`', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.options.parseOptions = { preserveWhitespace: true } + editor.commands.insertContent('Hello\n World\n
') + cy.get('.tiptap').should('contain.html', 'Hello World
') + }) + }) + }) diff --git a/demos/src/Commands/InsertContent/React/styles.scss b/demos/src/Commands/InsertContent/React/styles.scss index 4d2b2c81e..7694d7322 100644 --- a/demos/src/Commands/InsertContent/React/styles.scss +++ b/demos/src/Commands/InsertContent/React/styles.scss @@ -1,56 +1,91 @@ /* Basic editor styles */ .tiptap { - > * + * { - margin-top: 0.75em; + :first-child { + margin-top: 0; } - ul, + /* List styles */ + 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; + margin: 1.25rem 1rem 1.25rem 0.4rem; + + li p { + margin-top: 0.25em; + margin-bottom: 0.25em; } } - img { - max-width: 100%; - height: auto; + /* Heading styles */ + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + margin-top: 2.5rem; + text-wrap: pretty; + } + + h1, + h2 { + margin-top: 3.5rem; + margin-bottom: 1.5rem; + } + + h1 { + font-size: 1.4rem; + } + + h2 { + font-size: 1.2rem; + } + + h3 { + font-size: 1.1rem; + } + + h4, + h5, + h6 { + font-size: 1rem; + } + + /* Code and preformatted text styles */ + code { + background-color: var(--purple-light); + border-radius: 0.4rem; + color: var(--black); + font-size: 0.85rem; + padding: 0.25em 0.3em; + } + + pre { + background: var(--black); + border-radius: 0.5rem; + color: var(--white); + font-family: 'JetBrainsMono', monospace; + margin: 1.5rem 0; + padding: 0.75rem 1rem; + + code { + background: none; + color: inherit; + font-size: 0.8rem; + padding: 0; + } } blockquote { + border-left: 3px solid var(--gray-3); + margin: 1.5rem 0; padding-left: 1rem; - border-left: 2px solid rgba(#0D0D0D, 0.1); } hr { border: none; - border-top: 2px solid rgba(#0D0D0D, 0.1); + border-top: 1px solid var(--gray-2); margin: 2rem 0; } } diff --git a/demos/src/Commands/InsertContentApplyingRules/React/index.jsx b/demos/src/Commands/InsertContentApplyingRules/React/index.jsx index 1f8036053..c41d3ade9 100644 --- a/demos/src/Commands/InsertContentApplyingRules/React/index.jsx +++ b/demos/src/Commands/InsertContentApplyingRules/React/index.jsx @@ -14,154 +14,152 @@ const MenuBar = () => { } return ( - <> +Hello World.
') + }) + }) + + it('should insert raw JSON content', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent({ type: 'paragraph', content: [{ type: 'text', text: 'Hello World.' }] }) + cy.get('.tiptap').should('contain.html', 'Hello World.
') + }) + }) + + it('should insert a Prosemirror Node as content', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent(editor.schema.node('paragraph', null, editor.schema.text('Hello World.'))) + cy.get('.tiptap').should('contain.html', 'Hello World.
') + }) + }) + + it('should insert a Prosemirror Fragment as content', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent(editor.schema.node('doc', null, editor.schema.node('paragraph', null, editor.schema.text('Hello World.'))).content) + cy.get('.tiptap').should('contain.html', 'Hello World.
') + }) + }) + + it('should emit updates', () => { + cy.get('.tiptap').then(([{ editor }]) => { + let updateCount = 0 + const callback = () => { + updateCount += 1 + } + + editor.on('update', callback) + // emit an update + editor.commands.setContent('Hello World.', true) + expect(updateCount).to.equal(1) + + updateCount = 0 + // do not emit an update + editor.commands.setContent('Hello World again.', false) + expect(updateCount).to.equal(0) + editor.off('update', callback) + }) + }) + + it('should insert more complex html content', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('This is a paragraph.
List Item A
List Item B
Subchild
This is a paragraph.
List Item A
List Item B
Subchild
Hello\n\tworld\n\t\thow\n\t\t\tnice.
') + cy.get('.tiptap').should('contain.html', 'Hello world how nice.
') + }) + }) + + it('should keep newlines and tabs when preserveWhitespace = full', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('Hello\n\tworld\n\t\thow\n\t\t\tnice.
', false, { preserveWhitespace: 'full' }) + cy.get('.tiptap').should('contain.html', 'Hello\n\tworld\n\t\thow\n\t\t\tnice.
') + }) + }) + + it('should overwrite existing content', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('Initial Content
') + cy.get('.tiptap').should('contain.html', 'Initial Content
') + }) + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('Overwritten Content
') + cy.get('.tiptap').should('contain.html', 'Overwritten Content
') + }) + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('Content without tags') + cy.get('.tiptap').should('contain.html', 'Content without tags
') + }) + }) + + it('should insert mentions', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('@John Doe
') + cy.get('.tiptap').should('contain.html', '@John Doe') + }) + }) + + it('should remove newlines and tabs between html fragments', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('Hello World
') + cy.get('.tiptap').should('contain.html', 'Hello World
') + }) + }) + + // TODO I'm not certain about this behavior and what it should do... + // This exists in insertContentAt as well + it('should keep newlines and tabs between html fragments when preserveWhitespace = full', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('Hello World
', false, { preserveWhitespace: 'full' }) + cy.get('.tiptap').should('contain.html', '\n\t
Hello World
') + }) + }) + + it('should allow inserting nothing', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('') + cy.get('.tiptap').should('contain.html', '') + }) + }) + + it('should allow inserting nothing when preserveWhitespace = full', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('', false, { preserveWhitespace: 'full' }) + cy.get('.tiptap').should('contain.html', '') + }) + }) + + it('should allow inserting a partial HTML tag', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('foo') + cy.get('.tiptap').should('contain.html', '
foo
') + }) + }) + + it('should allow inserting a partial HTML tag when preserveWhitespace = full', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('foo', false, { preserveWhitespace: 'full' }) + cy.get('.tiptap').should('contain.html', '
foo
') + }) + }) + + it('will remove an incomplete HTML tag', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('foofoo
') + }) + }) + + // TODO I'm not certain about this behavior and what it should do... + // This exists in insertContentAt as well + it('should allow inserting an incomplete HTML tag when preserveWhitespace = full', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('foofoo<p
') + }) + }) + + it('should allow inserting a list', () => { + cy.get('.tiptap').then(([{ editor }]) => { + editor.commands.setContent('ABC
123
ABC
123
Hello\n World\n
\n', false, { preserveWhitespace: false }) + cy.get('.tiptap').should('contain.html', 'Hello World
') + }) + }) +}) diff --git a/demos/src/Commands/SetContent/React/styles.scss b/demos/src/Commands/SetContent/React/styles.scss new file mode 100644 index 000000000..4d2b2c81e --- /dev/null +++ b/demos/src/Commands/SetContent/React/styles.scss @@ -0,0 +1,56 @@ +/* Basic editor styles */ +.tiptap { + > * + * { + 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; + } +} diff --git a/demos/src/Demos/CollaborationSplitPane/React/Editor.jsx b/demos/src/Demos/CollaborationSplitPane/React/Editor.jsx new file mode 100644 index 000000000..dbf486379 --- /dev/null +++ b/demos/src/Demos/CollaborationSplitPane/React/Editor.jsx @@ -0,0 +1,209 @@ +import CharacterCount from '@tiptap/extension-character-count' +import Collaboration from '@tiptap/extension-collaboration' +import CollaborationCursor from '@tiptap/extension-collaboration-cursor' +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' + +const colors = [ + '#958DF1', + '#F98181', + '#FBBC88', + '#FAF594', + '#70CFF8', + '#94FADB', + '#B9F18D', + '#C3E2C2', + '#EAECCC', + '#AFC8AD', + '#EEC759', + '#9BB8CD', + '#FF90BC', + '#FFC0D9', + '#DC8686', + '#7ED7C1', + '#F3EEEA', + '#89B9AD', + '#D0BFFF', + '#FFF8C9', + '#CBFFA9', + '#9BABB8', + '#E3F4F4', +] +const names = [ + 'Lea Thompson', + 'Cyndi Lauper', + 'Tom Cruise', + 'Madonna', + 'Jerry Hall', + 'Joan Collins', + 'Winona Ryder', + 'Christina Applegate', + 'Alyssa Milano', + 'Molly Ringwald', + 'Ally Sheedy', + 'Debbie Harry', + 'Olivia Newton-John', + 'Elton John', + 'Michael J. Fox', + 'Axl Rose', + 'Emilio Estevez', + 'Ralph Macchio', + 'Rob Lowe', + 'Jennifer Grey', + 'Mickey Rourke', + 'John Cusack', + 'Matthew Broderick', + 'Justine Bateman', + 'Lisa Bonet', +] + +const defaultContent = ` +Hi 👋, this is a collaborative document.
+Feel free to edit and collaborate in real-time!
+` + +const getRandomElement = list => list[Math.floor(Math.random() * list.length)] + +const getRandomColor = () => getRandomElement(colors) +const getRandomName = () => getRandomElement(names) + +const getInitialUser = () => { + return { + name: getRandomName(), + color: getRandomColor(), + } +} + +const Editor = ({ + ydoc, provider, room, +}) => { + const [status, setStatus] = useState('connecting') + const [currentUser, setCurrentUser] = useState(getInitialUser) + + const editor = useEditor({ + enableContentCheck: true, + onContentError: ({ disableCollaboration }) => { + disableCollaboration() + }, + onCreate: ({ editor: currentEditor }) => { + provider.on('synced', () => { + if (currentEditor.isEmpty) { + currentEditor.commands.setContent(defaultContent) + } + }) + }, + extensions: [ + StarterKit.configure({ + history: false, + }), + Highlight, + TaskList, + TaskItem, + CharacterCount.extend().configure({ + limit: 10000, + }), + Collaboration.extend().configure({ + document: ydoc, + }), + CollaborationCursor.extend().configure({ + provider, + }), + ], + }) + + useEffect(() => { + // Update status changes + const statusHandler = event => { + setStatus(event.status) + } + + provider.on('status', statusHandler) + + return () => { + provider.off('status', statusHandler) + } + }, [provider]) + + // Save current user to localStorage and emit to editor + useEffect(() => { + if (editor && currentUser) { + localStorage.setItem('currentUser', JSON.stringify(currentUser)) + editor.chain().focus().updateUser(currentUser).run() + } + }, [editor, currentUser]) + + const setName = useCallback(() => { + const name = (window.prompt('Name', currentUser.name) || '').trim().substring(0, 32) + + if (name) { + return setCurrentUser({ ...currentUser, name }) + } + }, [currentUser]) + + if (!editor) { + return null + } + + return ( +- That’s a boring paragraph followed by a fenced code block: + That's a boring paragraph followed by a fenced code block:
for (var i=1; i <= 20; i++)
{
@@ -88,24 +91,26 @@ export default {
diff --git a/demos/src/Examples/Community/Vue/index.spec.js b/demos/src/Examples/Community/Vue/index.spec.js
index ea6a1aa63..e403e10b0 100644
--- a/demos/src/Examples/Community/Vue/index.spec.js
+++ b/demos/src/Examples/Community/Vue/index.spec.js
@@ -4,33 +4,33 @@ context('/src/Examples/Community/Vue/', () => {
})
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')
+ // check if count text is "44 / 280 characters"
+ cy.get('.character-count').should('contain', '44 / 280 characters')
// type in .tiptap
cy.get('.tiptap').type(' Hello World')
- cy.get('.character-count__text').should('have.text', '56/280 characters')
+ cy.get('.character-count').should('contain', '56 / 280 characters')
// remove content from .tiptap and enter text
cy.get('.tiptap').type('{selectall}{backspace}Hello World')
- cy.get('.character-count__text').should('have.text', '11/280 characters')
+ cy.get('.character-count').should('contain', '11 / 280 characters')
})
it('should mention a user', () => {
cy.get('.tiptap').type('{selectall}{backspace}@')
// check if the mention autocomplete is visible
- cy.get('.tippy-content .items').should('be.visible')
+ cy.get('.tippy-content .dropdown-menu').should('be.visible')
// select the first user
- cy.get('.tippy-content .items .item').first().then($el => {
+ cy.get('.tippy-content .dropdown-menu button').first().then($el => {
const name = $el.text()
$el.click()
// check if the user is mentioned
cy.get('.tiptap').should('have.text', `@${name} `)
- cy.get('.character-count__text').should('have.text', '2/280 characters')
+ cy.get('.character-count').should('contain', '2 / 280 characters')
})
})
diff --git a/demos/src/Examples/Community/Vue/index.vue b/demos/src/Examples/Community/Vue/index.vue
index 33c9bea14..88dfcf50f 100644
--- a/demos/src/Examples/Community/Vue/index.vue
+++ b/demos/src/Examples/Community/Vue/index.vue
@@ -6,7 +6,6 @@
height="20"
width="20"
viewBox="0 0 20 20"
- class="character-count__graph"
>
- {{ editor.storage.characterCount.characters() }}/{{ limit }} characters
+ {{ editor.storage.characterCount.characters() }} / {{ limit }} characters
- this is a basic example of tiptap. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists: + this is a basic example of Tiptap. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
body {
-display: none;
+ display: none;
}
I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too. diff --git a/demos/src/Examples/Default/React/index.spec.js b/demos/src/Examples/Default/React/index.spec.js index b23e72c17..005560dee 100644 --- a/demos/src/Examples/Default/React/index.spec.js +++ b/demos/src/Examples/Default/React/index.spec.js @@ -21,41 +21,41 @@ context('/src/Examples/Default/React/', () => { }) const buttonMarks = [ - { label: 'bold', tag: 'strong' }, - { label: 'italic', tag: 'em' }, - { label: 'strike', tag: 's' }, + { label: 'Bold', tag: 'strong' }, + { label: 'Italic', tag: 'em' }, + { label: 'Strike', tag: 's' }, ] buttonMarks.forEach(m => { it(`should disable ${m.label} when the code tag is enabled for cursor`, () => { cy.get('.tiptap').type('{selectall}Hello world') - cy.get('button').contains('code').click() + cy.get('button').contains('Code').click() cy.get('button').contains(m.label).should('be.disabled') }) it(`should enable ${m.label} when the code tag is disabled for cursor`, () => { cy.get('.tiptap').type('{selectall}Hello world') - cy.get('button').contains('code').click() - cy.get('button').contains('code').click() + cy.get('button').contains('Code').click() + cy.get('button').contains('Code').click() cy.get('button').contains(m.label).should('not.be.disabled') }) it(`should disable ${m.label} when the code tag is enabled for selection`, () => { cy.get('.tiptap').type('{selectall}Hello world{selectall}') - cy.get('button').contains('code').click() + cy.get('button').contains('Code').click() cy.get('button').contains(m.label).should('be.disabled') }) it(`should enable ${m.label} when the code tag is disabled for selection`, () => { cy.get('.tiptap').type('{selectall}Hello world{selectall}') - cy.get('button').contains('code').click() - cy.get('button').contains('code').click() + cy.get('button').contains('Code').click() + cy.get('button').contains('Code').click() cy.get('button').contains(m.label).should('not.be.disabled') }) it(`should apply ${m.label} when the button is pressed`, () => { cy.get('.tiptap').type('{selectall}Hello world') - cy.get('button').contains('paragraph').click() + cy.get('button').contains('Paragraph').click() cy.get('.tiptap').type('{selectall}') cy.get('button').contains(m.label).click() cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world') @@ -64,40 +64,40 @@ context('/src/Examples/Default/React/', () => { it('should clear marks when the button is pressed', () => { cy.get('.tiptap').type('{selectall}Hello world') - cy.get('button').contains('paragraph').click() + cy.get('button').contains('Paragraph').click() cy.get('.tiptap').type('{selectall}') - cy.get('button').contains('bold').click() + cy.get('button').contains('Bold').click() cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world') - cy.get('button').contains('clear marks').click() + cy.get('button').contains('Clear marks').click() cy.get('.tiptap strong').should('not.exist') }) it('should clear nodes when the button is pressed', () => { cy.get('.tiptap').type('{selectall}Hello world') - cy.get('button').contains('bullet list').click() + cy.get('button').contains('Bullet list').click() cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world') cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}') - cy.get('button').contains('clear nodes').click() + cy.get('button').contains('Clear nodes').click() cy.get('.tiptap ul').should('not.exist') cy.get('.tiptap 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' }, + { 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('button').contains('Paragraph').click() cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('button').contains(n.label).click() @@ -109,35 +109,35 @@ context('/src/Examples/Default/React/', () => { it('should add a hr when on the same line as a node', () => { cy.get('.tiptap').type('{rightArrow}') - cy.get('button').contains('horizontal rule').click() + cy.get('button').contains('Horizontal rule').click() cy.get('.tiptap hr').should('exist') cy.get('.tiptap h1').should('exist') }) it('should add a hr when on a new line', () => { cy.get('.tiptap').type('{rightArrow}{enter}') - cy.get('button').contains('horizontal rule').click() + cy.get('button').contains('Horizontal rule').click() cy.get('.tiptap hr').should('exist') cy.get('.tiptap h1').should('exist') }) it('should add a br', () => { cy.get('.tiptap').type('{rightArrow}') - cy.get('button').contains('hard break').click() + cy.get('button').contains('Hard break').click() cy.get('.tiptap h1 br').should('exist') }) it('should undo', () => { cy.get('.tiptap').type('{selectall}{backspace}') - cy.get('button').contains('undo').click() + cy.get('button').contains('Undo').click() cy.get('.tiptap').should('contain', 'Hello world') }) it('should redo', () => { cy.get('.tiptap').type('{selectall}{backspace}') - cy.get('button').contains('undo').click() + cy.get('button').contains('Undo').click() cy.get('.tiptap').should('contain', 'Hello world') - cy.get('button').contains('redo').click() + cy.get('button').contains('Redo').click() cy.get('.tiptap').should('not.contain', 'Hello world') }) }) diff --git a/demos/src/Examples/Default/React/styles.scss b/demos/src/Examples/Default/React/styles.scss index 4d2b2c81e..7694d7322 100644 --- a/demos/src/Examples/Default/React/styles.scss +++ b/demos/src/Examples/Default/React/styles.scss @@ -1,56 +1,91 @@ /* Basic editor styles */ .tiptap { - > * + * { - margin-top: 0.75em; + :first-child { + margin-top: 0; } - ul, + /* List styles */ + 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; + margin: 1.25rem 1rem 1.25rem 0.4rem; + + li p { + margin-top: 0.25em; + margin-bottom: 0.25em; } } - img { - max-width: 100%; - height: auto; + /* Heading styles */ + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + margin-top: 2.5rem; + text-wrap: pretty; + } + + h1, + h2 { + margin-top: 3.5rem; + margin-bottom: 1.5rem; + } + + h1 { + font-size: 1.4rem; + } + + h2 { + font-size: 1.2rem; + } + + h3 { + font-size: 1.1rem; + } + + h4, + h5, + h6 { + font-size: 1rem; + } + + /* Code and preformatted text styles */ + code { + background-color: var(--purple-light); + border-radius: 0.4rem; + color: var(--black); + font-size: 0.85rem; + padding: 0.25em 0.3em; + } + + pre { + background: var(--black); + border-radius: 0.5rem; + color: var(--white); + font-family: 'JetBrainsMono', monospace; + margin: 1.5rem 0; + padding: 0.75rem 1rem; + + code { + background: none; + color: inherit; + font-size: 0.8rem; + padding: 0; + } } blockquote { + border-left: 3px solid var(--gray-3); + margin: 1.5rem 0; padding-left: 1rem; - border-left: 2px solid rgba(#0D0D0D, 0.1); } hr { border: none; - border-top: 2px solid rgba(#0D0D0D, 0.1); + border-top: 1px solid var(--gray-2); margin: 2rem 0; } } diff --git a/demos/src/Examples/Default/Svelte/index.spec.js b/demos/src/Examples/Default/Svelte/index.spec.js index 4e2c71b79..dc0e1c0a5 100644 --- a/demos/src/Examples/Default/Svelte/index.spec.js +++ b/demos/src/Examples/Default/Svelte/index.spec.js @@ -21,15 +21,15 @@ context('/src/Examples/Default/React/', () => { }) const buttonMarks = [ - { label: 'bold', tag: 'strong' }, - { label: 'italic', tag: 'em' }, - { label: 'strike', tag: 's' }, + { 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('.tiptap').type('{selectall}Hello world') - cy.get('button').contains('paragraph').click() + cy.get('button').contains('Paragraph').click() cy.get('.tiptap').type('{selectall}') cy.get('button').contains(m.label).click() cy.get(`.tiptap ${m.tag}`).should('exist').should('have.text', 'Hello world') @@ -38,40 +38,40 @@ context('/src/Examples/Default/React/', () => { it('should clear marks when the button is pressed', () => { cy.get('.tiptap').type('{selectall}Hello world') - cy.get('button').contains('paragraph').click() + cy.get('button').contains('Paragraph').click() cy.get('.tiptap').type('{selectall}') - cy.get('button').contains('bold').click() + cy.get('button').contains('Bold').click() cy.get('.tiptap strong').should('exist').should('have.text', 'Hello world') - cy.get('button').contains('clear marks').click() + cy.get('button').contains('Clear marks').click() cy.get('.tiptap strong').should('not.exist') }) it('should clear nodes when the button is pressed', () => { cy.get('.tiptap').type('{selectall}Hello world') - cy.get('button').contains('bullet list').click() + cy.get('button').contains('Bullet list').click() cy.get('.tiptap ul').should('exist').should('have.text', 'Hello world') cy.get('.tiptap').type('{enter}A second item{enter}A third item{selectall}') - cy.get('button').contains('clear nodes').click() + cy.get('button').contains('Clear nodes').click() cy.get('.tiptap ul').should('not.exist') cy.get('.tiptap 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' }, + { 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('button').contains('Paragraph').click() cy.get('.tiptap').type('{selectall}Hello world{selectall}') cy.get('button').contains(n.label).click() @@ -83,35 +83,35 @@ context('/src/Examples/Default/React/', () => { it('should add a hr when on the same line as a node', () => { cy.get('.tiptap').type('{rightArrow}') - cy.get('button').contains('horizontal rule').click() + cy.get('button').contains('Horizontal rule').click() cy.get('.tiptap hr').should('exist') cy.get('.tiptap h1').should('exist') }) it('should add a hr when on a new line', () => { cy.get('.tiptap').type('{rightArrow}{enter}') - cy.get('button').contains('horizontal rule').click() + cy.get('button').contains('Horizontal rule').click() cy.get('.tiptap hr').should('exist') cy.get('.tiptap h1').should('exist') }) it('should add a br', () => { cy.get('.tiptap').type('{rightArrow}') - cy.get('button').contains('hard break').click() + cy.get('button').contains('Hard break').click() cy.get('.tiptap h1 br').should('exist') }) it('should undo', () => { cy.get('.tiptap').type('{selectall}{backspace}') - cy.get('button').contains('undo').click() + cy.get('button').contains('Undo').click() cy.get('.tiptap').should('contain', 'Hello world') }) it('should redo', () => { cy.get('.tiptap').type('{selectall}{backspace}') - cy.get('button').contains('undo').click() + cy.get('button').contains('Undo').click() cy.get('.tiptap').should('contain', 'Hello world') - cy.get('button').contains('redo').click() + cy.get('button').contains('Redo').click() cy.get('.tiptap').should('not.contain', 'Hello world') }) }) diff --git a/demos/src/Examples/Default/Svelte/index.svelte b/demos/src/Examples/Default/Svelte/index.svelte index d43c0f0e3..6e915f09d 100644 --- a/demos/src/Examples/Default/Svelte/index.svelte +++ b/demos/src/Examples/Default/Svelte/index.svelte @@ -1,6 +1,9 @@ {#if editor} -
{{ globalValue }}
+{{ appValue }}
+{{ indexValue }}
+{{ editorValue }}
+#
followed by a space to get a heading. Try #
, ##
, ###
, ####
, #####
, ######
for different levels.
- Those conventions are called input rules in tiptap. Some of them are enabled by default. Try >
for blockquotes, *
, -
or +
for bullet lists, or \`foobar\`
to highlight code, ~~tildes~~
to strike text, or ==equal signs==
to highlight text.
+ Those conventions are called input rules in Tiptap. Some of them are enabled by default. Try >
for blockquotes, *
, -
or +
for bullet lists, or \`foobar\`
to highlight code, ~~tildes~~
to strike text, or ==equal signs==
to highlight text.
You can overwrite existing input rules or add your own to nodes, marks and extensions.
diff --git a/demos/src/Examples/MarkdownShortcuts/React/styles.scss b/demos/src/Examples/MarkdownShortcuts/React/styles.scss
index 6932a405b..d23c43fc9 100644
--- a/demos/src/Examples/MarkdownShortcuts/React/styles.scss
+++ b/demos/src/Examples/MarkdownShortcuts/React/styles.scss
@@ -1,53 +1,98 @@
+/* Basic editor styles */
.tiptap {
- > * + * {
- margin-top: 0.75em;
+ :first-child {
+ margin-top: 0;
}
- ul,
+ /* List styles */
+ 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;
+ margin: 1.25rem 1rem 1.25rem 0.4rem;
+
+ li p {
+ margin-top: 0.25em;
+ margin-bottom: 0.25em;
}
}
- img {
- max-width: 100%;
- height: auto;
+ /* Heading styles */
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ line-height: 1.1;
+ margin-top: 2.5rem;
+ text-wrap: pretty;
}
- hr {
- margin: 1rem 0;
+ h1,
+ h2 {
+ margin-top: 3.5rem;
+ margin-bottom: 1.5rem;
+ }
+
+ h1 {
+ font-size: 1.4rem;
+ }
+
+ h2 {
+ font-size: 1.2rem;
+ }
+
+ h3 {
+ font-size: 1.1rem;
+ }
+
+ h4,
+ h5,
+ h6 {
+ font-size: 1rem;
+ }
+
+ /* Code and preformatted text styles */
+ code {
+ background-color: var(--purple-light);
+ border-radius: 0.4rem;
+ color: var(--black);
+ font-size: 0.85rem;
+ padding: 0.25em 0.3em;
+ }
+
+ pre {
+ background: var(--black);
+ border-radius: 0.5rem;
+ color: var(--white);
+ font-family: 'JetBrainsMono', monospace;
+ margin: 1.5rem 0;
+ padding: 0.75rem 1rem;
+
+ code {
+ background: none;
+ color: inherit;
+ font-size: 0.8rem;
+ padding: 0;
+ }
+ }
+
+ mark {
+ background-color: #FAF594;
+ border-radius: 0.4rem;
+ box-decoration-break: clone;
+ padding: 0.1rem 0.3rem;
}
blockquote {
+ border-left: 3px solid var(--gray-3);
+ margin: 1.5rem 0;
padding-left: 1rem;
- border-left: 2px solid rgba(#0D0D0D, 0.1);
+ }
+
+ hr {
+ border: none;
+ border-top: 1px solid var(--gray-2);
+ margin: 2rem 0;
}
}
diff --git a/demos/src/Examples/MarkdownShortcuts/Vue/index.vue b/demos/src/Examples/MarkdownShortcuts/Vue/index.vue
index c2705ac76..034550d4a 100644
--- a/demos/src/Examples/MarkdownShortcuts/Vue/index.vue
+++ b/demos/src/Examples/MarkdownShortcuts/Vue/index.vue
@@ -34,7 +34,7 @@ export default {
To test that, start a new line and type #
followed by a space to get a heading. Try #
, ##
, ###
, ####
, #####
, ######
for different levels.
- Those conventions are called input rules in tiptap. Some of them are enabled by default. Try >
for blockquotes, *
, -
or +
for bullet lists, or \`foobar\`
to highlight code, ~~tildes~~
to strike text, or ==equal signs==
to highlight text.
+ Those conventions are called input rules in Tiptap. Some of them are enabled by default. Try >
for blockquotes, *
, -
or +
for bullet lists, or \`foobar\`
to highlight code, ~~tildes~~
to strike text, or ==equal signs==
to highlight text.
You can overwrite existing input rules or add your own to nodes, marks and extensions. @@ -55,15 +55,23 @@ export default { diff --git a/demos/src/Examples/Menus/React/index.jsx b/demos/src/Examples/Menus/React/index.jsx index c6a86209b..b05c7e1f7 100644 --- a/demos/src/Examples/Menus/React/index.jsx +++ b/demos/src/Examples/Menus/React/index.jsx @@ -64,7 +64,7 @@ export default () => { onClick={() => editor.chain().focus().toggleBulletList().run()} className={editor.isActive('bulletList') ? 'is-active' : ''} > - Bullet List + Bullet list } diff --git a/demos/src/Examples/Menus/React/styles.scss b/demos/src/Examples/Menus/React/styles.scss index d9820d58d..cf6f3bc01 100644 --- a/demos/src/Examples/Menus/React/styles.scss +++ b/demos/src/Examples/Menus/React/styles.scss @@ -1,53 +1,144 @@ +/* Basic editor styles */ .tiptap { - > * + * { - margin-top: 0.75em; + :first-child { + margin-top: 0; } - ul, + /* List styles */ + ul, ol { padding: 0 1rem; + margin: 1.25rem 1rem 1.25rem 0.4rem; + + li p { + margin-top: 0.25em; + margin-bottom: 0.25em; + } + } + + /* Heading styles */ + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + margin-top: 2.5rem; + text-wrap: pretty; + } + + h1, + h2 { + margin-top: 3.5rem; + margin-bottom: 1.5rem; + } + + h1 { + font-size: 1.4rem; + } + + h2 { + font-size: 1.2rem; + } + + h3 { + font-size: 1.1rem; + } + + h4, + h5, + h6 { + font-size: 1rem; + } + + /* Code and preformatted text styles */ + code { + background-color: var(--purple-light); + border-radius: 0.4rem; + color: var(--black); + font-size: 0.85rem; + padding: 0.25em 0.3em; + } + + pre { + background: var(--black); + border-radius: 0.5rem; + color: var(--white); + font-family: 'JetBrainsMono', monospace; + margin: 1.5rem 0; + padding: 0.75rem 1rem; + + code { + background: none; + color: inherit; + font-size: 0.8rem; + padding: 0; + } + } + + blockquote { + border-left: 3px solid var(--gray-3); + margin: 1.5rem 0; + padding-left: 1rem; + } + + hr { + border: none; + border-top: 1px solid var(--gray-2); + margin: 2rem 0; } } +/* Bubble menu */ .bubble-menu { + background-color: var(--white); + border: 1px solid var(--gray-1); + border-radius: 0.7rem; + box-shadow: var(--shadow); display: flex; - background-color: #0D0D0D; padding: 0.2rem; - border-radius: 0.5rem; button { - border: none; - background: none; - color: #FFF; - font-size: 0.85rem; - font-weight: 500; - padding: 0 0.2rem; - opacity: 0.6; + background-color: unset; + + &:hover { + background-color: var(--gray-3); + } - &:hover, &.is-active { - opacity: 1; + background-color: var(--purple); + + &:hover { + background-color: var(--purple-contrast); + } } } } +/* Floating menu */ .floating-menu { display: flex; - background-color: #0D0D0D10; - padding: 0.2rem; + background-color: var(--gray-3); + padding: 0.1rem; border-radius: 0.5rem; button { - border: none; - background: none; - font-size: 0.85rem; - font-weight: 500; - padding: 0 0.2rem; - opacity: 0.6; + background-color: unset; + padding: 0.275rem 0.425rem; + border-radius: 0.3rem; + + &:hover { + background-color: var(--gray-3); + } - &:hover, &.is-active { - opacity: 1; + background-color: var(--white); + color: var(--purple); + + &:hover { + color: var(--purple-contrast); + } } } } diff --git a/demos/src/Examples/Menus/Vue/index.vue b/demos/src/Examples/Menus/Vue/index.vue index b52941c32..4f4cf9f78 100644 --- a/demos/src/Examples/Menus/Vue/index.vue +++ b/demos/src/Examples/Menus/Vue/index.vue @@ -28,7 +28,7 @@ H2
This is a simple paragraph.
- +Here is another paragraph inside this document.
@@ -72,11 +72,11 @@ export default () => {- + `, }) @@ -160,7 +160,7 @@ export default () => { }, [editor]) const findSquaredImage = useCallback(() => { - const nodePosition = editor.$doc.querySelector('image', { src: 'https://unsplash.it/200/200' }) + const nodePosition = editor.$doc.querySelector('image', { src: 'https://placehold.co/200x200' }) if (!nodePosition) { setFoundNodes(null) @@ -171,7 +171,7 @@ export default () => { }, [editor]) const findLandscapeImage = useCallback(() => { - const nodePosition = editor.$doc.querySelector('image', { src: 'https://unsplash.it/260/200' }) + const nodePosition = editor.$doc.querySelector('image', { src: 'https://placehold.co/260x200' }) if (!nodePosition) { setFoundNodes(null) @@ -182,7 +182,7 @@ export default () => { }, [editor]) const findAllLandscapeImages = useCallback(() => { - const nodePosition = editor.$doc.querySelectorAll('image', { src: 'https://unsplash.it/260/200' }) + const nodePosition = editor.$doc.querySelectorAll('image', { src: 'https://placehold.co/260x200' }) if (!nodePosition) { setFoundNodes(null) @@ -193,7 +193,7 @@ export default () => { }, [editor]) const findFirstLandscapeImageWithAllQuery = useCallback(() => { - const nodePosition = editor.$doc.querySelectorAll('image', { src: 'https://unsplash.it/260/200' }, true) + const nodePosition = editor.$doc.querySelectorAll('image', { src: 'https://placehold.co/260x200' }, true) if (!nodePosition) { setFoundNodes(null) @@ -204,7 +204,7 @@ export default () => { }, [editor]) const findPortraitImageInBlockquote = useCallback(() => { - const nodePosition = editor.$doc.querySelector('image', { src: 'https://unsplash.it/100/200' }) + const nodePosition = editor.$doc.querySelector('image', { src: 'https://placehold.co/100x200' }) if (!nodePosition) { setFoundNodes(null) @@ -259,33 +259,35 @@ export default () => { }, [editor]) return ( -Here we have another paragraph inside a blockquote.
- - + +
+ A highly optimized editor that only re-renders when it’s necessary. +
+ `, + }) + /** + * This hook allows us to select the editor state we want to use in our component. + */ + const currentEditorState = useEditorState({ + /** + * The editor instance we want to use. + */ + editor, + /** + * This selector allows us to select the data we want to use in our component. + * It is evaluated on every editor transaction and compared to it's previously returned value. + */ + selector: ctx => ({ + isBold: ctx.editor.isActive('bold'), + isItalic: ctx.editor.isActive('italic'), + isStrike: ctx.editor.isActive('strike'), + }), + /** + * This function allows us to customize the equality check for the selector. + * By default it is a `===` check. + */ + equalityFn: (prev, next) => { + // A deep-equal function would probably be more maintainable here, but, we use a shallow one to show that it can be customized. + if (!next) { + return false + } + return ( + prev.isBold === next.isBold + && prev.isItalic === next.isItalic + && prev.isStrike === next.isStrike + ) + }, + }) + + return ( +- → With the Typography extension, tiptap understands »what you mean« and adds correct characters to your text — it’s like a “typography nerd” on your side. + → With the Typography extension, Tiptap understands »what you mean« and adds correct characters to your text — it’s like a “typography nerd” on your side.
Try it out and type (c)
, ->
, >>
, 1/2
, !=
, --
or 1x1
here:
diff --git a/demos/src/Examples/Savvy/React/styles.scss b/demos/src/Examples/Savvy/React/styles.scss
index 377369437..feee91627 100644
--- a/demos/src/Examples/Savvy/React/styles.scss
+++ b/demos/src/Examples/Savvy/React/styles.scss
@@ -1,38 +1,33 @@
/* Basic editor styles */
.tiptap {
- > * + * {
- margin-top: 0.75em;
- }
-
- h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- line-height: 1.1;
+ :first-child {
+ margin-top: 0;
}
+ /* Code and preformatted text styles */
code {
- background-color: rgba(#616161, 0.1);
- color: #616161;
- }
-}
-
-/* Color swatches */
-.color {
- white-space: nowrap;
-
- &::before {
- background-color: var(--color);
- border: 1px solid rgba(128, 128, 128, 0.3);
- border-radius: 2px;
- content: " ";
- display: inline-block;
- height: 1em;
- margin-bottom: 0.15em;
- margin-right: 0.1em;
- vertical-align: middle;
- width: 1em;
+ background-color: var(--purple-light);
+ border-radius: 0.4rem;
+ color: var(--black);
+ font-size: 0.85rem;
+ padding: 0.25em 0.3em;
+ }
+
+ /* Color swatches */
+ .color {
+ white-space: nowrap;
+
+ &::before {
+ background-color: var(--color);
+ border: 1px solid rgba(128, 128, 128, 0.3);
+ border-radius: 2px;
+ content: " ";
+ display: inline-block;
+ height: 1em;
+ margin-bottom: 0.15em;
+ margin-right: 0.1em;
+ vertical-align: middle;
+ width: 1em;
+ }
}
}
diff --git a/demos/src/Examples/Savvy/Vue/index.vue b/demos/src/Examples/Savvy/Vue/index.vue
index b296747bd..a824b6097 100644
--- a/demos/src/Examples/Savvy/Vue/index.vue
+++ b/demos/src/Examples/Savvy/Vue/index.vue
@@ -37,7 +37,7 @@ export default {
],
content: `
- → With the Typography extension, tiptap understands »what you mean« and adds correct characters to your text — it’s like a “typography nerd” on your side. + → With the Typography extension, Tiptap understands »what you mean« and adds correct characters to your text — it’s like a “typography nerd” on your side.
Try it out and type (c)
, ->
, >>
, 1/2
, !=
, --
or 1x1
here:
@@ -64,40 +64,35 @@ export default {
diff --git a/demos/src/Examples/Tables/React/index.jsx b/demos/src/Examples/Tables/React/index.jsx
index 7fab59158..347bf6d08 100644
--- a/demos/src/Examples/Tables/React/index.jsx
+++ b/demos/src/Examples/Tables/React/index.jsx
@@ -60,69 +60,71 @@ const MenuBar = ({ editor }) => {
}
return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
+
colgroup
and rowspan
colgroup
and rowspan
Here is an example: @@ -155,25 +157,25 @@ export default () => {
Name | -Description | +Name | +Description | ||||
---|---|---|---|---|---|---|---|
Cyndi Lauper | -singer | -songwriter | -actress | +Singer | +Songwriter | +Actress | |
Marie Curie | -scientist | -chemist | -physicist | +Scientist | +Chemist | +Physicist | |
Indira Gandhi | -prime minister | -politician | +Prime minister | +Politician |
- Another Editor -
- +Another Editor
+