├── .commitlintrc.yml ├── .github ├── CONTRIBUTING.md └── workflows │ ├── lock.yml │ ├── npmpublish.yml │ ├── pages.yml │ └── tests.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .lintstagedrc.yml ├── .npmrc ├── .prettierignore ├── .prettierrc.yml ├── .vscode ├── extensions.json ├── settings.json └── tailwind.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cypress.config.ts ├── cypress ├── e2e │ └── 1-editor │ │ ├── 1-core.spec.cy.ts │ │ ├── 2-extension.spec.cy.ts │ │ └── 3-component.spec.cy.ts ├── fixtures │ └── example.json ├── support │ ├── commands.ts │ └── e2e.ts └── tsconfig.json ├── eslint.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── scripts └── version-check.js ├── src ├── app.css ├── app.d.ts ├── app.html ├── lib │ ├── BubbleMenu.svelte │ ├── Editor.ts │ ├── EditorContent.svelte │ ├── FloatingMenu.svelte │ ├── NodeViewContent.svelte │ ├── NodeViewWrapper.svelte │ ├── SvelteNodeViewRenderer.svelte.ts │ ├── SvelteRenderer.ts │ ├── context.ts │ ├── createEditor.ts │ ├── index.ts │ ├── types.ts │ └── utils.ts └── routes │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.svelte │ ├── +page.ts │ ├── _components │ ├── Counter.svelte │ ├── Editable.svelte │ └── SvelteExtension.ts │ ├── bubble-menu │ ├── +page.svelte │ └── +page.ts │ └── floating-menu │ ├── +page.svelte │ └── +page.ts ├── static └── favicon.png ├── svelte.config.js ├── tests ├── bubble-menu.test.ts ├── editor.test.ts ├── extension.test.ts ├── floating-menu.test.ts └── setup.ts ├── tsconfig.eslint.json ├── tsconfig.json └── vite.config.ts /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: non-conventional 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All type of contributions are welcome, but when contributing to this repository, please first discuss the change you wish to make. 4 | 5 | If it is a bug - file an issue here https://github.com/sibiraj-s/svelte-tiptap/issues 6 | 7 | For all others - Open a discussion https://github.com/sibiraj-s/svelte-tiptap/discussions here first. 8 | 9 | ## Setting up dev environment 10 | 11 | - clone the repository 12 | 13 | ```bash 14 | git clone https://github.com/sibiraj-s/svelte-tiptap 15 | ``` 16 | 17 | - run npm install to install the deps 18 | 19 | ```bash 20 | npm i 21 | ``` 22 | 23 | - Run dev server 24 | 25 | ```bash 26 | npm run dev 27 | 28 | # or start the server and open the app in a new browser tab 29 | npm run dev -- --open 30 | ``` 31 | 32 | - Run the tests 33 | 34 | ```bash 35 | npm run test 36 | ``` 37 | 38 | Read more about packaging a library [in the docs](https://kit.svelte.dev/docs/packaging). 39 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: Lock closed issues/pull-requests 2 | 3 | on: 4 | # 00:00 hours UTC, i.e. 16:00 hours PST or 17:00 hours PDT 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | # allow manual trigger 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: lock 13 | 14 | permissions: 15 | issues: write 16 | pull-requests: write 17 | discussions: write 18 | 19 | jobs: 20 | lock: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: dessant/lock-threads@v5 24 | with: 25 | process-only: 'issues, prs, discussions' 26 | issue-inactive-days: 30 27 | pr-inactive-days: 30 28 | discussion-inactive-days: 730 29 | issue-comment: > 30 | This issue has been automatically locked since there 31 | has not been any recent activity after it was closed. 32 | Please open a new issue for related bugs and link to 33 | relevant comments in the thread. 34 | pr-comment: > 35 | This pull request has been automatically locked since there 36 | has not been any recent activity after it was closed. 37 | Please open a new issue for related bugs and link to 38 | relevant comments in the thread. 39 | discussion-comment: > 40 | This discussion has been automatically locked since there 41 | has not been any recent activity after it was closed. 42 | Please open a new discussion for related items and link to 43 | relevant comments in the thread. 44 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: NPM Publish 2 | 3 | env: 4 | NODE_VERSION: 22 5 | 6 | on: 7 | push: 8 | tags: v* 9 | 10 | jobs: 11 | build-publish: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | contents: read 16 | id-token: write 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{env.NODE_VERSION}} 23 | registry-url: https://registry.npmjs.org/ 24 | cache: npm 25 | 26 | - run: npm ci 27 | - run: npm run package 28 | env: 29 | NODE_ENV: production 30 | 31 | - name: Publish to NPM 32 | run: npm publish --provenance 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 35 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Github Pages 2 | 3 | env: 4 | NODE_VERSION: 22 5 | 6 | on: 7 | push: 8 | branches: master 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Using node v${{ env.NODE_VERSION }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{env.NODE_VERSION}} 28 | cache: npm 29 | 30 | - name: NPM Install 31 | run: npm ci 32 | env: 33 | CYPRESS_INSTALL_BINARY: 0 34 | 35 | - name: Build Site 36 | run: npm run build:demo 37 | 38 | - name: Upload artifact 39 | uses: actions/upload-pages-artifact@v3 40 | with: 41 | path: ./build 42 | 43 | deploy: 44 | needs: build 45 | runs-on: ubuntu-latest 46 | 47 | permissions: 48 | pages: write 49 | id-token: write 50 | 51 | environment: 52 | name: github-pages 53 | url: ${{ steps.deployment.outputs.page_url }} 54 | 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | env: 4 | NODE_VERSION: 22 5 | 6 | on: 7 | push: 8 | branches: master 9 | pull_request: 10 | branches: master 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | test-unit: 17 | name: Unit Tests 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Using node v${{ env.NODE_VERSION }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{env.NODE_VERSION}} 28 | cache: npm 29 | 30 | - name: NPM Install 31 | run: npm ci 32 | env: 33 | CYPRESS_INSTALL_BINARY: 0 34 | 35 | - name: Test 36 | run: npm run test 37 | 38 | test-e2e: 39 | name: E2E Tests 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | 46 | - name: Using node v${{ env.NODE_VERSION }} 47 | uses: actions/setup-node@v4 48 | with: 49 | node-version: ${{env.NODE_VERSION}} 50 | cache: npm 51 | 52 | - name: NPM Install 53 | run: npm ci 54 | 55 | - name: Run Cypress tests 56 | uses: cypress-io/github-action@v6 57 | with: 58 | start: npm run dev -- --host=127.0.0.1 59 | wait-on: 'http://127.0.0.1:3009' 60 | browser: chrome 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Bower ### 2 | bower_components 3 | .bower-cache 4 | .bower-registry 5 | .bower-tmp 6 | 7 | ### Homebrew ### 8 | Brewfile.lock.json 9 | 10 | ### Linux ### 11 | *~ 12 | 13 | # temporary files which can be created if a process still has a handle open of a deleted file 14 | .fuse_hidden* 15 | 16 | # KDE directory preferences 17 | .directory 18 | 19 | # Linux trash folder which might appear on any partition or disk 20 | .Trash-* 21 | 22 | # .nfs files are created when an open file is removed but is still being accessed 23 | .nfs* 24 | 25 | ### macOS ### 26 | # General 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Icon must end with two \r 32 | Icon 33 | 34 | 35 | # Thumbnails 36 | ._* 37 | 38 | # Files that might appear in the root of a volume 39 | .DocumentRevisions-V100 40 | .fseventsd 41 | .Spotlight-V100 42 | .TemporaryItems 43 | .Trashes 44 | .VolumeIcon.icns 45 | .com.apple.timemachine.donotpresent 46 | 47 | # Directories potentially created on remote AFP share 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | 54 | ### Node ### 55 | # Logs 56 | logs 57 | *.log 58 | npm-debug.log* 59 | yarn-debug.log* 60 | yarn-error.log* 61 | lerna-debug.log* 62 | .pnpm-debug.log* 63 | 64 | # Diagnostic reports (https://nodejs.org/api/report.html) 65 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 66 | 67 | # Runtime data 68 | pids 69 | *.pid 70 | *.seed 71 | *.pid.lock 72 | 73 | # Directory for instrumented libs generated by jscoverage/JSCover 74 | lib-cov 75 | 76 | # Coverage directory used by tools like istanbul 77 | coverage 78 | *.lcov 79 | 80 | # nyc test coverage 81 | .nyc_output 82 | 83 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 84 | .grunt 85 | 86 | # Bower dependency directory (https://bower.io/) 87 | 88 | # node-waf configuration 89 | .lock-wscript 90 | 91 | # Compiled binary addons (https://nodejs.org/api/addons.html) 92 | build/Release 93 | 94 | # Dependency directories 95 | node_modules/ 96 | jspm_packages/ 97 | 98 | # Snowpack dependency directory (https://snowpack.dev/) 99 | web_modules/ 100 | 101 | # TypeScript cache 102 | *.tsbuildinfo 103 | 104 | # Optional npm cache directory 105 | .npm 106 | 107 | # Optional eslint cache 108 | .eslintcache 109 | 110 | # Microbundle cache 111 | .rpt2_cache/ 112 | .rts2_cache_cjs/ 113 | .rts2_cache_es/ 114 | .rts2_cache_umd/ 115 | 116 | # Optional REPL history 117 | .node_repl_history 118 | 119 | # Output of 'npm pack' 120 | *.tgz 121 | 122 | # Yarn Integrity file 123 | .yarn-integrity 124 | 125 | # dotenv environment variables file 126 | .env 127 | .env.test 128 | .env.production 129 | 130 | # parcel-bundler cache (https://parceljs.org/) 131 | .cache 132 | .parcel-cache 133 | 134 | # Next.js build output 135 | .next 136 | out 137 | 138 | # Nuxt.js build / generate output 139 | .nuxt 140 | dist 141 | 142 | # Gatsby files 143 | .cache/ 144 | # Comment in the public line in if your project uses Gatsby and not Next.js 145 | # https://nextjs.org/blog/next-9-1#public-directory-support 146 | # public 147 | 148 | # vuepress build output 149 | .vuepress/dist 150 | 151 | # Serverless directories 152 | .serverless/ 153 | 154 | # FuseBox cache 155 | .fusebox/ 156 | 157 | # DynamoDB Local files 158 | .dynamodb/ 159 | 160 | # TernJS port file 161 | .tern-port 162 | 163 | # Stores VSCode versions used for testing VSCode extensions 164 | .vscode-test 165 | 166 | # yarn v2 167 | .yarn/cache 168 | .yarn/unplugged 169 | .yarn/build-state.yml 170 | .yarn/install-state.gz 171 | .pnp.* 172 | 173 | ### Sass ### 174 | .sass-cache/ 175 | *.css.map 176 | *.sass.map 177 | *.scss.map 178 | 179 | ### Windows ### 180 | # Windows thumbnail cache files 181 | Thumbs.db 182 | Thumbs.db:encryptable 183 | ehthumbs.db 184 | ehthumbs_vista.db 185 | 186 | # Dump file 187 | *.stackdump 188 | 189 | # Folder config file 190 | [Dd]esktop.ini 191 | 192 | # Recycle Bin used on file shares 193 | $RECYCLE.BIN/ 194 | 195 | # Windows Installer files 196 | *.cab 197 | *.msi 198 | *.msix 199 | *.msm 200 | *.msp 201 | 202 | # Windows shortcuts 203 | *.lnk 204 | 205 | ### yarn ### 206 | # https://yarnpkg.com/advanced/qa#which-files-should-be-gitignored 207 | 208 | .yarn/* 209 | !.yarn/releases 210 | !.yarn/plugins 211 | !.yarn/sdks 212 | !.yarn/versions 213 | 214 | # if you are NOT using Zero-installs, then: 215 | # comment the following lines 216 | !.yarn/cache 217 | 218 | # and uncomment the following lines 219 | # .pnp.* 220 | 221 | ### Svelte Kit ### 222 | /build 223 | /dist 224 | /.svelte-kit 225 | /package 226 | .env 227 | .env.* 228 | !.env.example 229 | vite.config.js.timestamp-* 230 | vite.config.ts.timestamp-* 231 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | node scripts/version-check.js 2 | npx --no lint-staged 3 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | npm run build 2 | npm run test -- --run 3 | 4 | echo "\npublint:" 5 | npx --no publint 6 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | '*.{js,cjs,ts,svelte}': 2 | - eslint --fix 3 | - prettier --write 4 | '*.{md,yml,json,html}': prettier --write 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | singleQuote: true 3 | plugins: 4 | - prettier-plugin-svelte 5 | overrides: 6 | - files: '*.svelte' 7 | options: 8 | parser: 'svelte' 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[svelte]": { 3 | "editor.defaultFormatter": "svelte.svelte-vscode" 4 | }, 5 | "svelte.enable-ts-plugin": true, 6 | "css.customData": [".vscode/tailwind.json"] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tailwind.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1.1, 3 | "atDirectives": [ 4 | { 5 | "name": "@apply", 6 | "description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that you’d like to extract to a new component.", 7 | "references": [ 8 | { 9 | "name": "Tailwind Documentation", 10 | "url": "https://tailwindcss.com/docs/functions-and-directives#apply" 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "@responsive", 16 | "description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n", 17 | "references": [ 18 | { 19 | "name": "Tailwind Documentation", 20 | "url": "https://tailwindcss.com/docs/functions-and-directives#responsive" 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "@screen", 26 | "description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n", 27 | "references": [ 28 | { 29 | "name": "Tailwind Documentation", 30 | "url": "https://tailwindcss.com/docs/functions-and-directives#screen" 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "@variants", 36 | "description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n", 37 | "references": [ 38 | { 39 | "name": "Tailwind Documentation", 40 | "url": "https://tailwindcss.com/docs/functions-and-directives#variants" 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | > **Tags** 6 | > 7 | > - Features 8 | > - Bug Fixes 9 | > - Performance Improvements 10 | > - Dependency Updates 11 | > - Breaking Changes 12 | > - Enhancements 13 | > - Documentation 14 | > - Internal 15 | 16 | ## v2.1.0 (2024-12-01) 17 | 18 | #### Features 19 | 20 | - support forwarding `class` prop to editor container ([902ed66](https://github.com/sibiraj-s/svelte-tiptap/commit/902ed66)) 21 | 22 | ## v2.0.3 (2024-11-05) 23 | 24 | #### Bug Fixes 25 | 26 | - fix types mismatch with tiptap ([3cc6cfb](https://github.com/sibiraj-s/svelte-tiptap/commit/3cc6cfb)) 27 | 28 | ## v2.0.2 (2024-10-30) 29 | 30 | #### Bug Fixes 31 | 32 | - update README ([d5aebdd](https://github.com/sibiraj-s/svelte-tiptap/commit/d5aebdd)) 33 | 34 | ## v2.0.1 (2024-10-30) 35 | 36 | #### Bug Fixes 37 | 38 | - update README ([aee74fe](https://github.com/sibiraj-s/svelte-tiptap/commit/aee74fe)) 39 | 40 | ## v2.0.0 (2024-10-30) 41 | 42 | #### Breaking Changes 43 | 44 | - requires svelte 5 ([6490536](https://github.com/sibiraj-s/svelte-tiptap/commit/6490536)) 45 | 46 | #### Bug Fixes 47 | 48 | - check element exists before mount ([a203650](https://github.com/sibiraj-s/svelte-tiptap/commit/a203650)) 49 | 50 | ## v1.1.3 (2024-05-11) 51 | 52 | #### Bug Fixes 53 | 54 | - fixed NodeViewRenderer compatibility with typescript target > 2021 ([426a002](https://github.com/sibiraj-s/svelte-tiptap/commit/426a002), [af9fa49](https://github.com/sibiraj-s/svelte-tiptap/commit/af9fa49)) 55 | 56 | ## v1.1.2 (2023-07-24) 57 | 58 | #### Bug Fixes 59 | 60 | - update nodeview selected prop with text selection ([fe9eefb](https://github.com/sibiraj-s/svelte-tiptap/commit/fe9eefb)) 61 | 62 | ## v1.1.1 (2023-07-12) 63 | 64 | #### Bug Fixes 65 | 66 | - fix invalid prop, rename `delay` to `updateDelay` ([646f655](https://github.com/sibiraj-s/svelte-tiptap/commit/646f655)) 67 | - update SvelteComponent types ([94d54ce](https://github.com/sibiraj-s/svelte-tiptap/commit/94d54ce)) 68 | 69 | ## v1.1.0 (2023-07-02) 70 | 71 | #### Features 72 | 73 | - allow contents to be rendered inside EditorContent via slot ([e954389](https://github.com/sibiraj-s/svelte-tiptap/commit/e954389)) 74 | 75 | ## v1.0.0 (2023-07-02) 76 | 77 | #### Breaking Changes 78 | 79 | - requires svelte 4 ([09730bf](https://github.com/sibiraj-s/svelte-tiptap/commit/09730bf)) 80 | - requires tiptap v2 stable ([09730bf](https://github.com/sibiraj-s/svelte-tiptap/commit/09730bf)) 81 | 82 | #### Enhancements 83 | 84 | - cleanup SvelteNodeViewRenderer implementation ([844d07a](https://github.com/sibiraj-s/svelte-tiptap/commit/844d07a)) 85 | 86 | #### Features 87 | 88 | - Expose BubbleMenu delay ([2185c94](https://github.com/sibiraj-s/svelte-tiptap/commit/2185c94)) 89 | 90 | #### Internal 91 | 92 | - add npm package provenance ([4065bde](https://github.com/sibiraj-s/svelte-tiptap/commit/4065bde)) 93 | 94 | ## v0.7.0 (2022-12-15) 95 | 96 | #### Dependency Updates 97 | 98 | - update svelte-kit to v1 stable ([0c204ff](https://github.com/sibiraj-s/svelte-tiptap/commit/0c204ff)) 99 | - update tiptap dependencies ([2700dae](https://github.com/sibiraj-s/svelte-tiptap/commit/2700dae)) 100 | - update prosemirror dependencies ([52811f2](https://github.com/sibiraj-s/svelte-tiptap/commit/52811f2)) 101 | - update svelte to v3.55.0 ([9124b03](https://github.com/sibiraj-s/svelte-tiptap/commit/9124b03)) 102 | 103 | ## v0.6.0 (2022-06-27) 104 | 105 | #### Dependency Updates 106 | 107 | - update tiptap dependencies ([de57588](https://github.com/sibiraj-s/svelte-tiptap/commit/de57588)) 108 | 109 | ## v0.5.2 (2022-05-30) 110 | 111 | #### Bug Fixes 112 | 113 | - downgrade package target to ES2021 ([9459818](https://github.com/sibiraj-s/svelte-tiptap/commit/9459818)) 114 | 115 | ## v0.5.1 (2022-05-30) 116 | 117 | #### Dependency Updates 118 | 119 | - update svelte-kit to v1.0.0-next.345 ([aedbc9d](https://github.com/sibiraj-s/svelte-tiptap/commit/aedbc9d)) 120 | 121 | ## v0.5.0 (2022-05-25) 122 | 123 | #### Features 124 | 125 | - support dynamic elements in NodeViewWrapper ([df009fe](https://github.com/sibiraj-s/svelte-tiptap/commit/df009fe)) 126 | - support custom tag types for NodeViewRenderer ([d54dfe7](https://github.com/sibiraj-s/svelte-tiptap/commit/d54dfe7)) 127 | 128 | #### Bug Fixes 129 | 130 | - fix rendering inline elements ([219de17](https://github.com/sibiraj-s/svelte-tiptap/commit/219de17)) 131 | 132 | #### Enhancements 133 | 134 | - throw if NodeViewWrapper is not used for NodeViews ([decbecf](https://github.com/sibiraj-s/svelte-tiptap/commit/decbecf)) 135 | 136 | #### Breaking Changes 137 | 138 | - require svelte v3.48.0 or greater ([8edfa0c](https://github.com/sibiraj-s/svelte-tiptap/commit/8edfa0c)) 139 | 140 | ## v0.4.7 (2021-12-31) 141 | 142 | #### Bug Fixes 143 | 144 | - add workaround to fix errors while unmounting floating-menu and bubble-menu ([45feb0c](https://github.com/sibiraj-s/svelte-tiptap/commit/45feb0c)) 145 | 146 | #### Dependency Updates 147 | 148 | - update tiptap peerDependencies ([19db754](https://github.com/sibiraj-s/svelte-tiptap/commit/19db754)) 149 | - update devDependencies ([dccf621](https://github.com/sibiraj-s/svelte-tiptap/commit/dccf621), [413e034](https://github.com/sibiraj-s/svelte-tiptap/commit/413e034), [3232324](https://github.com/sibiraj-s/svelte-tiptap/commit/3232324)) 150 | 151 | ### Internal 152 | 153 | - update demo site ([a506f91](https://github.com/sibiraj-s/svelte-tiptap/commit/a506f91)) 154 | 155 | ## v0.4.6 (2021-12-31) 156 | 157 | #### Dependency Updates 158 | 159 | - update tiptap dependencies ([65d461f](https://github.com/sibiraj-s/svelte-tiptap/commit/65d461f)) 160 | - update devDependencies ([65d461f](https://github.com/sibiraj-s/svelte-tiptap/commit/65d461f)) 161 | 162 | ## v0.4.5 (2021-12-17) 163 | 164 | #### Dependency Updates 165 | 166 | - update tiptap dependencies ([53000b1](https://github.com/sibiraj-s/svelte-tiptap/commit/53000b1)) 167 | - update devDependencies ([53000b1](https://github.com/sibiraj-s/svelte-tiptap/commit/53000b1)) 168 | - update devDependencies ([540d597](https://github.com/sibiraj-s/svelte-tiptap/commit/540d597)) 169 | 170 | ## v0.4.4 (2021-09-28) 171 | 172 | #### Dependency Updates 173 | 174 | - update tiptap dependencies ([e23e024](https://github.com/sibiraj-s/svelte-tiptap/commit/e23e024)) 175 | 176 | #### Internal 177 | 178 | - enable typescript strict mode ([c023eeb](https://github.com/sibiraj-s/svelte-tiptap/commit/c023eeb)) 179 | - setup e2e testing with cypress ([0f138b4](https://github.com/sibiraj-s/svelte-tiptap/commit/0f138b4)) 180 | 181 | ## v0.4.3 (2021-08-13) 182 | 183 | #### Dependency Updates 184 | 185 | - update tiptap dependencies ([8f68062](https://github.com/sibiraj-s/svelte-tiptap/commit/8f68062)) 186 | 187 | ## v0.4.2 (2021-08-13) 188 | 189 | #### Dependency Updates 190 | 191 | - update tiptap dependencies ([977d9a5](https://github.com/sibiraj-s/svelte-tiptap/commit/977d9a5)) 192 | 193 | ## v0.4.1 (2021-08-06) 194 | 195 | #### Bug Fixes 196 | 197 | - publish all build files to npm ([31002f8](https://github.com/sibiraj-s/svelte-tiptap/commit/31002f8)) 198 | 199 | ## v0.4.0 (2021-08-06) 200 | 201 | #### Features 202 | 203 | - generate type definitions ([8a7e84b](https://github.com/sibiraj-s/svelte-tiptap/commit/8a7e84b)) 204 | 205 | #### Internal 206 | 207 | - migrate to svelte-kit ([8a7e84b](https://github.com/sibiraj-s/svelte-tiptap/commit/8a7e84b)) 208 | 209 | ## v0.3.0 (2021-07-31) 210 | 211 | #### Features 212 | 213 | - update tiptap dependencies ([f7c3143](https://github.com/sibiraj-s/svelte-tiptap/commit/f7c3143)) 214 | 215 | ## v0.2.0 (2021-07-17) 216 | 217 | #### Features 218 | 219 | - add `createEditor` method ([f590d5c](https://github.com/sibiraj-s/svelte-tiptap/commit/f590d5c)) 220 | 221 | ## v0.1.1 (2021-07-14) 222 | 223 | #### Documentation 224 | 225 | - update README ([eec8dc2](https://github.com/sibiraj-s/svelte-tiptap/commit/eec8dc2)) 226 | 227 | ## v0.1.0 (2021-07-14) 228 | 229 | Initial Release: Svelte components for Tiptap v2 230 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sibiraj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # svelte-tiptap 2 | 3 | > Svelte components for tiptap v2 4 | 5 | [![Tests](https://github.com/sibiraj-s/svelte-tiptap/actions/workflows/tests.yml/badge.svg)](https://github.com/sibiraj-s/svelte-tiptap/actions/workflows/tests.yml) 6 | [![NPM Version](https://badgen.net/npm/v/svelte-tiptap)](https://www.npmjs.com/package/svelte-tiptap) 7 | [![Total Downloads](https://badgen.net/npm/dt/svelte-tiptap)](https://www.npmjs.com/package/svelte-tiptap) 8 | [![Monthly Downloads](https://badgen.net/npm/dm/svelte-tiptap)](https://www.npmjs.com/package/svelte-tiptap) 9 | [![License](https://badgen.net/npm/license/svelte-tiptap)](https://github.com/sibiraj-s/svelte-tiptap/blob/master/LICENSE) 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm i svelte-tiptap 15 | # or 16 | yarn add svelte-tiptap 17 | ``` 18 | 19 | > [!NOTE] 20 | > This package just provides components for svelte. For configuring/customizing the editor, refer [tiptap's official documentation](https://www.tiptap.dev/). 21 | 22 | For any issues with the editor. You may need to open the issue on [tiptap's repository](https://github.com/ueberdosis/tiptap/issues) 23 | 24 | You can find some [examples for the editor here](./src/routes/) 25 | 26 | ## Usage 27 | 28 | A Simple editor. 29 | 30 | ```svelte 31 | 46 | 47 | 48 | ``` 49 | 50 | Refer https://www.tiptap.dev/api/commands/ for available commands 51 | 52 | ## Extensions 53 | 54 | Refer: https://www.tiptap.dev/api/extensions 55 | 56 | ### Floating menu 57 | 58 | This will make a contextual menu appear near a selection of text. 59 | 60 | The markup and styling are totally up to you. 61 | 62 | ```svelte 63 | 68 | 69 | 70 | 71 | ``` 72 | 73 | Refer: https://www.tiptap.dev/api/extensions/floating-menu 74 | 75 | ### Bubble Menu 76 | 77 | This will make a contextual menu appear near a selection of text. Use it to let users apply marks to their text selection. 78 | 79 | The markup and styling are totally up to you. 80 | 81 | ```svelte 82 | 87 | 88 | 89 | 90 | ``` 91 | 92 | Refer: https://www.tiptap.dev/api/extensions/bubble-menu 93 | 94 | ## SvelteNodeViewRenderer 95 | 96 | SvelteNodeViewRenderer enables rendering Svelte Components as NodeViews. The following is an example for creating a counter component 97 | 98 | ### Create a Node Extension 99 | 100 | ```ts 101 | import { Node, mergeAttributes } from '@tiptap/core'; 102 | import { SvelteNodeViewRenderer } from 'svelte-tiptap'; 103 | 104 | import CounterComponent from './Counter.svelte'; 105 | 106 | export const SvelteCounterExtension = Node.create({ 107 | name: 'svelteCounterComponent', 108 | group: 'block', 109 | atom: true, 110 | draggable: true, // Optional: to make the node draggable 111 | inline: false, 112 | 113 | addAttributes() { 114 | return { 115 | count: { 116 | default: 0, 117 | }, 118 | }; 119 | }, 120 | 121 | parseHTML() { 122 | return [{ tag: 'svelte-counter-component' }]; 123 | }, 124 | 125 | renderHTML({ HTMLAttributes }) { 126 | return ['svelte-counter-component', mergeAttributes(HTMLAttributes)]; 127 | }, 128 | 129 | addNodeView() { 130 | return SvelteNodeViewRenderer(CounterComponent); 131 | }, 132 | }); 133 | ``` 134 | 135 | ### Create a Component 136 | 137 | ```svelte 138 | 149 | 150 | 151 | Svelte Component 152 | 153 |
154 | 157 |
158 |
159 | ``` 160 | 161 | ### Use the extension 162 | 163 | ```ts 164 | import { onMount, onDestroy } from 'svelte'; 165 | import type { Readable } from 'svelte/store'; 166 | import { Editor, EditorContent } from 'svelte-tiptap'; 167 | import StarterKit from '@tiptap/starter-kit'; 168 | 169 | import { SvelteCounterExtension } from './SvelteExtension'; 170 | 171 | let editor = $state() as Readable; 172 | 173 | onMount(() => { 174 | editor = createEditor({ 175 | extensions: [StarterKit, SvelteCounterExtension], 176 | content: ` 177 |

This is still the text editor you’re used to, but enriched with node views.

178 | 179 |

Did you see that? That’s a Svelte component. We are really living in the future.

180 | `, 181 | }); 182 | }); 183 | ``` 184 | 185 | ### Access/Update Attributes 186 | 187 | Refer https://www.tiptap.dev/guide/node-views/react/#all-available-props for the list of all available attributes. You can access them like 188 | 189 | ```ts 190 | import type { NodeViewProps } from '@tiptap/core'; 191 | 192 | let { node, updateAttributes }: NodeViewProps = $props(); 193 | 194 | // update attributes 195 | const handleClick = () => { 196 | updateAttributes({ count: node.attrs.count + 1 }); 197 | }; 198 | ``` 199 | 200 | ### Dragging 201 | 202 | To make your node views draggable, set `draggable: true` in the extension and add `data-drag-handle` to the DOM element that should function as the drag handle. 203 | 204 | ### Adding a content editable 205 | 206 | There is another action called `editable` which helps you adding editable content to your node view. Here is an example. 207 | 208 | ```svelte 209 | 212 | 213 | 214 | Svelte Editable Component 215 | 216 | 217 | 218 | 219 | ``` 220 | 221 | The NodeViewWrapper and NodeViewContent components render a `
` HTML tag (`` for inline nodes), 222 | but you can change that. For example `` should render a paragraph. 223 | One limitation though: That tag must not change during runtime. 224 | 225 | Refer: https://www.tiptap.dev/guide/node-views/react/#adding-a-content-editable 226 | 227 | ## Contributing 228 | 229 | All types of contributions are welcome. See [CONTRIBUTING.md](./.github/CONTRIBUTING.md) to get started. 230 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:3009', 6 | video: false, 7 | screenshotOnRunFailure: false, 8 | setupNodeEvents(_on, _config) { 9 | // implement node event listeners here 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /cypress/e2e/1-editor/1-core.spec.cy.ts: -------------------------------------------------------------------------------- 1 | describe('core', () => { 2 | beforeEach(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | it('should render the editor', () => { 7 | cy.get('.ProseMirror').should('have.length', 1); 8 | cy.get('.ProseMirror').should('have.attr', 'contenteditable'); 9 | cy.get('.ProseMirror').should('contain.text', 'text editor'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /cypress/e2e/1-editor/2-extension.spec.cy.ts: -------------------------------------------------------------------------------- 1 | describe('extension', () => { 2 | it('should render bubble menu on selection', () => { 3 | cy.visit('/bubble-menu'); 4 | 5 | cy.get('.ProseMirror').focus().type('{selectall}'); 6 | cy.get('[data-test-id=bubble-menu]').should('have.length', 1); 7 | }); 8 | 9 | it('should render floating menu', () => { 10 | cy.visit('/floating-menu'); 11 | 12 | cy.get('.ProseMirror').focus().clear(); 13 | cy.get('[data-test-id=floating-menu]').should('have.length', 1); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /cypress/e2e/1-editor/3-component.spec.cy.ts: -------------------------------------------------------------------------------- 1 | describe('components', () => { 2 | beforeEach(() => { 3 | cy.visit('/'); 4 | }); 5 | 6 | it('should render the svelte components', () => { 7 | cy.get('.ProseMirror #svelte-component').should('have.length', 2); 8 | }); 9 | 10 | it('should render the counter component', () => { 11 | cy.get('.ProseMirror #svelte-component #counter-button').should('have.length', 1); 12 | cy.get('#svelte-component button').contains('This button has been clicked 0 times'); 13 | cy.get('#svelte-component button').click(); 14 | cy.get('#svelte-component button').contains('This button has been clicked 1 times'); 15 | cy.get('#svelte-component button').click(); 16 | cy.get('#svelte-component button').contains('This button has been clicked 2 times'); 17 | }); 18 | 19 | it('should render the editable component', () => { 20 | cy.get('.ProseMirror #svelte-component #editable-component').should('have.length', 1); 21 | 22 | cy.get('.ProseMirror #editable-component div').contains('This is editable'); 23 | cy.get('.ProseMirror #editable-component div').type('. Hooray!'); 24 | cy.get('.ProseMirror #editable-component div').contains('This is editable. Hooray!'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "node"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import pegasus from 'eslint-config-pegasus'; 2 | import svelte from 'eslint-plugin-svelte'; 3 | import prettier from 'eslint-config-prettier'; 4 | import tsParser from '@typescript-eslint/parser'; 5 | import pluginCypress from 'eslint-plugin-cypress'; 6 | 7 | import svelteConfig from './svelte.config.js'; 8 | 9 | /** @type {import("eslint").Linter.Config[]} */ 10 | const config = pegasus.tsConfig( 11 | { 12 | ignores: ['build/', '.svelte-kit/', 'dist/'], 13 | }, 14 | pegasus.configs.default, 15 | pegasus.configs.node, 16 | pegasus.configs.browser, 17 | prettier, 18 | { 19 | files: ['*.ts'], 20 | extends: pegasus.configs.typescriptRecommended, 21 | languageOptions: { 22 | parserOptions: { 23 | projectService: false, 24 | project: './tsconfig.eslint.json', 25 | tsconfigRootDir: import.meta.dirname, 26 | }, 27 | }, 28 | }, 29 | { 30 | name: 'overrides', 31 | rules: { 32 | 'no-duplicate-imports': 'off', 33 | }, 34 | }, 35 | { 36 | files: ['**/*.svelte', '**/*.svelte.ts'], 37 | extends: [ 38 | ...pegasus.configs.typescript, 39 | ...svelte.configs['flat/recommended'], 40 | prettier, 41 | ...svelte.configs['flat/prettier'], 42 | ], 43 | languageOptions: { 44 | parserOptions: { 45 | parser: tsParser, 46 | extraFileExtensions: ['.svelte'], 47 | ...svelteConfig, 48 | }, 49 | }, 50 | }, 51 | { 52 | files: ['e2e/**/*'], 53 | extends: [pluginCypress.configs.recommended], 54 | }, 55 | ); 56 | 57 | export default config; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-tiptap", 3 | "description": "Svelte components for tiptap v2", 4 | "version": "2.1.0", 5 | "author": "sibiraj-s", 6 | "license": "MIT", 7 | "repository": "github:sibiraj-s/svelte-tiptap", 8 | "bugs": "https://github.com/sibiraj-s/svelte-tiptap/issues", 9 | "homepage": "https://github.com/sibiraj-s/svelte-tiptap#readme", 10 | "funding": [ 11 | { 12 | "type": "github", 13 | "url": "https://github.com/sponsors/sibiraj-s" 14 | } 15 | ], 16 | "keywords": [ 17 | "svelte", 18 | "tiptap", 19 | "svelte-tiptap", 20 | "tiptap-v2", 21 | "prosemirror", 22 | "rich-text-editor" 23 | ], 24 | "type": "module", 25 | "exports": { 26 | ".": { 27 | "types": "./dist/index.d.ts", 28 | "svelte": "./dist/index.js" 29 | } 30 | }, 31 | "files": [ 32 | "dist", 33 | "!dist/**/*.test.*", 34 | "!dist/**/*.spec.*" 35 | ], 36 | "svelte": "./dist/index.js", 37 | "types": "./dist/index.d.ts", 38 | "scripts": { 39 | "dev": "vite dev --port 3009", 40 | "build:demo": "vite build", 41 | "build": "npm run build:demo && npm run package", 42 | "preview": "vite preview", 43 | "package": "svelte-kit sync && svelte-package && publint", 44 | "prepublishOnly": "is-ci || npm run build", 45 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 46 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 47 | "test": "vitest", 48 | "e2e": "cypress open --e2e", 49 | "lint": "eslint .", 50 | "prepare": "is-ci || husky", 51 | "publish:npm": "npm publish" 52 | }, 53 | "peerDependencies": { 54 | "@tiptap/core": "^2.11.5", 55 | "@tiptap/extension-bubble-menu": "^2.11.5", 56 | "@tiptap/extension-floating-menu": "^2.11.5", 57 | "@tiptap/pm": "^2.11.5", 58 | "svelte": "^5.0.0" 59 | }, 60 | "devDependencies": { 61 | "@commitlint/cli": "^19.7.1", 62 | "@sveltejs/adapter-static": "^3.0.8", 63 | "@sveltejs/kit": "^2.17.3", 64 | "@sveltejs/package": "^2.3.10", 65 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 66 | "@tailwindcss/postcss": "^4.0.9", 67 | "@testing-library/svelte": "^5.2.7", 68 | "@tiptap/core": "^2.11.5", 69 | "@tiptap/extension-bubble-menu": "^2.11.5", 70 | "@tiptap/extension-floating-menu": "^2.11.5", 71 | "@tiptap/extension-placeholder": "^2.11.5", 72 | "@tiptap/pm": "^2.11.5", 73 | "@tiptap/starter-kit": "^2.11.5", 74 | "@types/node": "^22.13.5", 75 | "clsx": "^2.1.1", 76 | "commitlint-config-non-conventional": "^1.0.1", 77 | "cypress": "^14.1.0", 78 | "eslint": "^9.21.0", 79 | "eslint-config-pegasus": "^6.0.2", 80 | "eslint-config-prettier": "^10.0.1", 81 | "eslint-plugin-cypress": "^4.1.0", 82 | "eslint-plugin-svelte": "^3.0.0", 83 | "happy-dom": "^17.1.8", 84 | "husky": "^9.1.7", 85 | "is-ci": "^4.1.0", 86 | "lint-staged": "^15.4.3", 87 | "postcss": "^8.5.3", 88 | "prettier": "^3.5.2", 89 | "prettier-plugin-svelte": "^3.3.3", 90 | "publint": "^0.3.6", 91 | "sass": "^1.85.1", 92 | "svelte": "^5.20.4", 93 | "svelte-check": "^4.1.4", 94 | "tailwindcss": "^4.0.9", 95 | "tslib": "^2.8.1", 96 | "typescript": "^5.7.3", 97 | "typescript-transform-extensions": "^1.0.1", 98 | "vite": "^6.2.0", 99 | "vitest": "^3.0.7" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /scripts/version-check.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | 4 | const readJson = async (fileName) => { 5 | const filePath = path.join(import.meta.dirname, fileName); 6 | const file = await fs.readFile(filePath, 'utf8'); 7 | return JSON.parse(file); 8 | }; 9 | 10 | const pkg = await readJson('../package.json'); 11 | const pkgLock = await readJson('../package-lock.json'); 12 | 13 | if (pkg.version !== pkgLock.version) { 14 | throw new Error('Manifest version mismatch with lock file'); 15 | } 16 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @layer base { 4 | body { 5 | @apply antialiased; 6 | } 7 | } 8 | 9 | .ProseMirror p { 10 | @apply mb-2; 11 | } 12 | 13 | .ProseMirror h1, 14 | .ProseMirror h2 { 15 | @apply font-bold; 16 | } 17 | 18 | .ProseMirror h1 { 19 | @apply text-3xl; 20 | } 21 | 22 | .ProseMirror h2 { 23 | @apply text-2xl; 24 | } 25 | 26 | .tiptap p.is-editor-empty:first-child::before { 27 | color: #adb5bd; 28 | content: attr(data-placeholder); 29 | float: left; 30 | height: 0; 31 | pointer-events: none; 32 | } 33 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Tiptap Svelte 9 | %sveltekit.head% 10 | 11 | 12 |
%sveltekit.body%
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/lib/BubbleMenu.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /src/lib/Editor.ts: -------------------------------------------------------------------------------- 1 | import { Editor as CoreEditor } from '@tiptap/core'; 2 | 3 | export class Editor extends CoreEditor { 4 | public contentElement: HTMLElement | null = null; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/EditorContent.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 |
53 | 54 | {#if children} 55 | {@render children()} 56 | {/if} 57 | -------------------------------------------------------------------------------- /src/lib/FloatingMenu.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /src/lib/NodeViewContent.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | {#if children} 21 | {@render children()} 22 | {/if} 23 | 24 | -------------------------------------------------------------------------------- /src/lib/NodeViewWrapper.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | {#if children} 25 | {@render children()} 26 | {/if} 27 | 28 | -------------------------------------------------------------------------------- /src/lib/SvelteNodeViewRenderer.svelte.ts: -------------------------------------------------------------------------------- 1 | import { NodeView, Editor, getRenderedAttributes } from '@tiptap/core'; 2 | import type { NodeViewRenderer, NodeViewProps, NodeViewRendererOptions, DecorationWithType } from '@tiptap/core'; 3 | import type { Decoration, DecorationSource } from '@tiptap/pm/view'; 4 | import type { Node as ProseMirrorNode } from '@tiptap/pm/model'; 5 | import { type Component, mount } from 'svelte'; 6 | 7 | import SvelteRenderer from './SvelteRenderer'; 8 | import { TIPTAP_NODE_VIEW } from './context'; 9 | import { invariant } from './utils'; 10 | 11 | interface RendererUpdateProps { 12 | oldNode: ProseMirrorNode; 13 | oldDecorations: readonly Decoration[]; 14 | oldInnerDecorations: DecorationSource; 15 | newNode: ProseMirrorNode; 16 | newDecorations: readonly Decoration[]; 17 | newInnerDecorations: DecorationSource; 18 | updateProps: () => void; 19 | } 20 | 21 | type AttrProps = 22 | | Record 23 | | ((props: { node: ProseMirrorNode; HTMLAttributes: Record }) => Record); 24 | 25 | export interface SvelteNodeViewRendererOptions extends NodeViewRendererOptions { 26 | update: ((props: RendererUpdateProps) => boolean) | null; 27 | as?: string; 28 | attrs?: AttrProps; 29 | } 30 | 31 | class SvelteNodeView extends NodeView, Editor, SvelteNodeViewRendererOptions> { 32 | declare renderer: SvelteRenderer; 33 | declare contentDOMElement: HTMLElement | null; 34 | 35 | override mount(): void { 36 | const Component = this.component; 37 | 38 | const props = $state({ 39 | editor: this.editor, 40 | node: this.node, 41 | decorations: this.decorations as DecorationWithType[], 42 | innerDecorations: this.innerDecorations, 43 | view: this.view, 44 | selected: false, 45 | extension: this.extension, 46 | HTMLAttributes: this.HTMLAttributes, 47 | getPos: () => this.getPos(), 48 | updateAttributes: (attributes = {}) => this.updateAttributes(attributes), 49 | deleteNode: () => this.deleteNode(), 50 | }); 51 | 52 | this.contentDOMElement = this.node.isLeaf ? null : document.createElement(this.node.isInline ? 'span' : 'div'); 53 | 54 | if (this.contentDOMElement) { 55 | // For some reason the whiteSpace prop is not inherited properly in Chrome and Safari 56 | // With this fix it seems to work fine 57 | // See: https://github.com/ueberdosis/tiptap/issues/1197 58 | this.contentDOMElement.style.whiteSpace = 'inherit'; 59 | } 60 | 61 | const context = new Map(); 62 | context.set(TIPTAP_NODE_VIEW, { 63 | onDragStart: this.onDragStart.bind(this), 64 | }); 65 | 66 | const as = this.options.as ?? (this.node.isInline ? 'span' : 'div'); 67 | const target = document.createElement(as); 68 | target.classList.add(`node-${this.node.type.name}`); 69 | 70 | this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this); 71 | this.editor.on('selectionUpdate', this.handleSelectionUpdate); 72 | 73 | const svelteComponent = mount(Component, { target, props, context }); 74 | 75 | this.renderer = new SvelteRenderer(svelteComponent, { 76 | element: target, 77 | props, 78 | }); 79 | 80 | this.appendContendDom(); 81 | this.updateElementAttributes(); 82 | } 83 | 84 | private appendContendDom() { 85 | const contentElement = this.dom.querySelector('[data-node-view-content]'); 86 | 87 | if (this.contentDOMElement && contentElement && !contentElement.contains(this.contentDOMElement)) { 88 | contentElement.appendChild(this.contentDOMElement); 89 | } 90 | } 91 | 92 | override get dom() { 93 | invariant( 94 | this.renderer.dom.firstElementChild?.hasAttribute('data-node-view-wrapper'), 95 | 'Please use the NodeViewWrapper component for your node view.', 96 | ); 97 | 98 | return this.renderer.dom; 99 | } 100 | 101 | override get contentDOM() { 102 | if (this.node.isLeaf) { 103 | return null; 104 | } 105 | 106 | return this.contentDOMElement; 107 | } 108 | 109 | handleSelectionUpdate() { 110 | const { from, to } = this.editor.state.selection; 111 | const pos = this.getPos(); 112 | 113 | if (typeof pos !== 'number') { 114 | return; 115 | } 116 | 117 | if (from <= pos && to >= pos + this.node.nodeSize) { 118 | if (this.renderer.props.selected) { 119 | return; 120 | } 121 | 122 | this.selectNode(); 123 | } else { 124 | if (!this.renderer.props.selected) { 125 | return; 126 | } 127 | 128 | this.deselectNode(); 129 | } 130 | } 131 | 132 | update(node: ProseMirrorNode, decorations: readonly Decoration[], innerDecorations: DecorationSource): boolean { 133 | const updateProps = (props: Partial) => { 134 | this.renderer.updateProps(props); 135 | 136 | if (typeof this.options.attrs === 'function') { 137 | this.updateElementAttributes(); 138 | } 139 | }; 140 | 141 | if (typeof this.options.update === 'function') { 142 | const oldNode = this.node; 143 | const oldDecorations = this.decorations; 144 | const oldInnerDecorations = this.innerDecorations; 145 | 146 | this.node = node; 147 | this.decorations = decorations; 148 | this.innerDecorations = innerDecorations; 149 | 150 | return this.options.update({ 151 | oldNode, 152 | oldDecorations, 153 | oldInnerDecorations, 154 | newNode: node, 155 | newDecorations: decorations, 156 | newInnerDecorations: innerDecorations, 157 | updateProps: () => 158 | updateProps({ 159 | node, 160 | decorations: decorations as DecorationWithType[], 161 | innerDecorations, 162 | }), 163 | }); 164 | } 165 | 166 | if (node.type !== this.node.type) { 167 | return false; 168 | } 169 | 170 | if (node === this.node && this.decorations === decorations && this.innerDecorations === innerDecorations) { 171 | return true; 172 | } 173 | 174 | this.node = node; 175 | this.decorations = decorations; 176 | this.innerDecorations = innerDecorations; 177 | 178 | updateProps({ 179 | node, 180 | decorations: decorations as DecorationWithType[], 181 | innerDecorations, 182 | }); 183 | 184 | return true; 185 | } 186 | 187 | selectNode(): void { 188 | this.renderer.updateProps({ selected: true }); 189 | this.renderer.dom.classList.add('ProseMirror-selectednode'); 190 | } 191 | 192 | deselectNode(): void { 193 | this.renderer.updateProps({ selected: false }); 194 | this.renderer.dom.classList.remove('ProseMirror-selectednode'); 195 | } 196 | 197 | destroy(): void { 198 | this.renderer.destroy(); 199 | this.editor.off('selectionUpdate', this.handleSelectionUpdate); 200 | this.contentDOMElement = null; 201 | } 202 | 203 | /** 204 | * Update the attributes of the top-level element that holds the React component. 205 | * Applying the attributes defined in the `attrs` option. 206 | */ 207 | updateElementAttributes() { 208 | if (this.options.attrs) { 209 | let attrsObj: Record = {}; 210 | if (typeof this.options.attrs === 'function') { 211 | const extensionAttributes = this.editor.extensionManager.attributes; 212 | const HTMLAttributes = getRenderedAttributes(this.node, extensionAttributes); 213 | attrsObj = this.options.attrs({ node: this.node, HTMLAttributes }); 214 | } else { 215 | attrsObj = this.options.attrs; 216 | } 217 | this.renderer.updateAttributes(attrsObj); 218 | } 219 | } 220 | } 221 | 222 | const SvelteNodeViewRenderer = ( 223 | component: Component, 224 | options?: Partial, 225 | ): NodeViewRenderer => { 226 | return (props): SvelteNodeView => new SvelteNodeView(component, props, options); 227 | }; 228 | 229 | export default SvelteNodeViewRenderer; 230 | -------------------------------------------------------------------------------- /src/lib/SvelteRenderer.ts: -------------------------------------------------------------------------------- 1 | import { mount, unmount } from 'svelte'; 2 | import type { NodeViewProps } from '@tiptap/core'; 3 | 4 | interface RendererOptions { 5 | element: HTMLElement; 6 | props: NodeViewProps; 7 | } 8 | 9 | type App = ReturnType; 10 | 11 | class SvelteRenderer { 12 | component: App; 13 | props: NodeViewProps; 14 | dom: HTMLElement; 15 | 16 | constructor(component: App, { element, props }: RendererOptions) { 17 | this.component = component; 18 | this.props = props; 19 | this.dom = element; 20 | 21 | this.dom.classList.add('svelte-renderer'); 22 | } 23 | 24 | updateProps(props: Partial): void { 25 | Object.assign(this.props, props); 26 | } 27 | 28 | updateAttributes(attributes: Record): void { 29 | Object.keys(attributes).forEach((key) => { 30 | this.dom.setAttribute(key, attributes[key]); 31 | }); 32 | } 33 | 34 | destroy(): void { 35 | unmount(this.component); 36 | } 37 | } 38 | 39 | export default SvelteRenderer; 40 | -------------------------------------------------------------------------------- /src/lib/context.ts: -------------------------------------------------------------------------------- 1 | export const TIPTAP_NODE_VIEW = 'TipTapNodeView'; 2 | -------------------------------------------------------------------------------- /src/lib/createEditor.ts: -------------------------------------------------------------------------------- 1 | import type { EditorOptions } from '@tiptap/core'; 2 | import { readable, type Readable } from 'svelte/store'; 3 | 4 | import { Editor } from './Editor'; 5 | 6 | const createEditor = (options: Partial): Readable => { 7 | const editor = new Editor(options); 8 | 9 | return readable(editor, (set) => { 10 | editor.on('transaction', () => { 11 | set(editor); 12 | }); 13 | 14 | return () => { 15 | editor.destroy(); 16 | }; 17 | }); 18 | }; 19 | 20 | export default createEditor; 21 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Editor'; 2 | 3 | export { default as createEditor } from './createEditor'; 4 | export { default as SvelteRenderer } from './SvelteRenderer'; 5 | 6 | export { default as SvelteNodeViewRenderer } from './SvelteNodeViewRenderer.svelte'; 7 | export { default as EditorContent } from './EditorContent.svelte'; 8 | export { default as NodeViewWrapper } from './NodeViewWrapper.svelte'; 9 | export { default as NodeViewContent } from './NodeViewContent.svelte'; 10 | export { default as BubbleMenu } from './BubbleMenu.svelte'; 11 | export { default as FloatingMenu } from './FloatingMenu.svelte'; 12 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { Snippet } from 'svelte'; 2 | 3 | import type { Editor } from './Editor'; 4 | 5 | export interface TiptapNodeViewContext { 6 | onDragStart: (event: DragEvent) => void; 7 | } 8 | 9 | export type ComponentInputProps = Partial & { 10 | editor: Editor; 11 | class?: string; 12 | children?: Snippet; 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | type Invariant = (condition: unknown, message?: string) => asserts condition; 2 | 3 | export const invariant: Invariant = (condition: unknown, msg?: string) => { 4 | if (!condition) { 5 | throw new Error(msg); 6 | } 7 | }; 8 | 9 | export const runIfFn = (value: T | ((...args: unknown[]) => T), ...args: unknown[]): T => { 10 | return typeof value === 'function' ? (value as (...args: unknown[]) => T)(...args) : value; 11 | }; 12 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 | 20 | {#if children} 21 | {@render children()} 22 | {/if} 23 |
24 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 91 | 92 | 93 | Tiptap Svelte 94 | 95 | 96 |

Editor with Nodeview Renderer

97 | 98 | {#if editor} 99 |
100 | {#each menuItems as item (item.name)} 101 | 110 | {/each} 111 |
112 | {/if} 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/routes/+page.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /src/routes/_components/Counter.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | Svelte Component 24 | 25 |
26 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /src/routes/_components/Editable.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | Svelte Editable Component 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/routes/_components/SvelteExtension.ts: -------------------------------------------------------------------------------- 1 | import { Node, mergeAttributes } from '@tiptap/core'; 2 | import { SvelteNodeViewRenderer } from '$lib'; 3 | 4 | import CounterComponent from './Counter.svelte'; 5 | import EditableComponent from './Editable.svelte'; 6 | 7 | export const SvelteCounterExtension = Node.create({ 8 | name: 'SvelteCounterComponent', 9 | group: 'block', 10 | atom: true, 11 | draggable: true, 12 | inline: false, 13 | 14 | addAttributes() { 15 | return { 16 | count: { 17 | default: 0, 18 | }, 19 | }; 20 | }, 21 | 22 | parseHTML() { 23 | return [{ tag: 'svelte-counter-component' }]; 24 | }, 25 | 26 | renderHTML({ HTMLAttributes }) { 27 | return ['svelte-counter-component', mergeAttributes(HTMLAttributes)]; 28 | }, 29 | 30 | addNodeView() { 31 | return SvelteNodeViewRenderer(CounterComponent); 32 | }, 33 | }); 34 | 35 | export const SvelteEditableExtension = Node.create({ 36 | name: 'SvelteEditableComponent', 37 | group: 'block', 38 | content: 'inline*', 39 | draggable: true, 40 | 41 | parseHTML() { 42 | return [{ tag: 'svelte-editable-component' }]; 43 | }, 44 | 45 | renderHTML({ HTMLAttributes }) { 46 | return ['svelte-editable-component', mergeAttributes(HTMLAttributes), 0]; 47 | }, 48 | 49 | addNodeView() { 50 | return SvelteNodeViewRenderer(EditableComponent); 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /src/routes/bubble-menu/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | 39 | Bubble Menu | Tiptap Svelte 40 | 41 | 42 |

Editor with Bubble Menu

43 | 44 | {#if editor} 45 | 46 |
47 | 56 | 65 |
66 |
67 | {/if} 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/routes/bubble-menu/+page.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /src/routes/floating-menu/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 43 | 44 | 45 | Floating Menu | Tiptap Svelte 46 | 47 | 48 |

Editor with Floating Menu

49 | 50 | {#if editor} 51 | 52 |
53 | 62 | 71 | 80 |
81 |
82 | {/if} 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/routes/floating-menu/+page.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sibiraj-s/svelte-tiptap/3a947feb73ae7be149da6ecf9578c9c71c45b70b/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | const dev = process.env.NODE_ENV === 'development'; 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | const config = { 8 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 9 | // for more information about preprocessors 10 | preprocess: [vitePreprocess()], 11 | 12 | kit: { 13 | // generate static site 14 | adapter: adapter({ 15 | pages: 'build', 16 | assets: 'build', 17 | fallback: undefined, 18 | precompress: false, 19 | }), 20 | 21 | paths: { 22 | base: dev ? '' : '/svelte-tiptap', 23 | }, 24 | 25 | appDir: 'core', 26 | }, 27 | }; 28 | 29 | export default config; 30 | -------------------------------------------------------------------------------- /tests/bubble-menu.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from 'vitest'; 2 | import { render } from '@testing-library/svelte'; 3 | 4 | import { BubbleMenu } from '$lib'; 5 | 6 | it('should throw error if editor instance is not provided', () => { 7 | expect(() => { 8 | render(BubbleMenu); 9 | }).toThrow(); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/editor.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from 'vitest'; 2 | import { render, act } from '@testing-library/svelte'; 3 | import StarterKit from '@tiptap/starter-kit'; 4 | 5 | import { Editor, EditorContent } from '$lib'; 6 | 7 | it('should render the content correctly', async () => { 8 | const editor = new Editor({ 9 | content: 'Hello world!', 10 | extensions: [StarterKit], 11 | }); 12 | 13 | const { getByText } = render(EditorContent, { editor }); 14 | 15 | await act(); 16 | expect(getByText('Hello world!')); 17 | 18 | editor.destroy(); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/extension.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from 'vitest'; 2 | import { render, act, fireEvent } from '@testing-library/svelte'; 3 | import { mergeAttributes, Node } from '@tiptap/core'; 4 | import StarterKit from '@tiptap/starter-kit'; 5 | 6 | import { Editor, EditorContent, SvelteNodeViewRenderer } from '$lib'; 7 | import CounterComponent from '../src/routes/_components/Counter.svelte'; 8 | import EditableComponent from '../src/routes/_components/Editable.svelte'; 9 | 10 | it('should render the counter component', async () => { 11 | const SvelteCounterExtension = Node.create({ 12 | name: 'svelteCounterComponent', 13 | group: 'block', 14 | atom: true, 15 | draggable: true, 16 | inline: false, 17 | 18 | addAttributes() { 19 | return { 20 | count: { 21 | default: 0, 22 | }, 23 | }; 24 | }, 25 | 26 | parseHTML() { 27 | return [{ tag: 'svelte-counter-component' }]; 28 | }, 29 | 30 | renderHTML({ HTMLAttributes }) { 31 | return ['svelte-counter-component', mergeAttributes(HTMLAttributes)]; 32 | }, 33 | 34 | addNodeView() { 35 | return SvelteNodeViewRenderer(CounterComponent); 36 | }, 37 | }); 38 | 39 | const editor = new Editor({ 40 | content: '', 41 | extensions: [StarterKit, SvelteCounterExtension], 42 | }); 43 | 44 | const { getByText, getByTestId } = render(EditorContent, { editor }); 45 | await act(); 46 | 47 | const wrapper = getByTestId('svelte-component'); 48 | await fireEvent.click(wrapper); 49 | await act(); 50 | 51 | expect(wrapper.dataset.selected).toBe('true'); 52 | 53 | expect(getByText('Svelte Component')).toBeTruthy(); 54 | expect(getByText('This button has been clicked 1 times.')).toBeTruthy(); 55 | 56 | const button = getByTestId('counter-button'); 57 | 58 | // Using await when firing events is unique to the svelte testing library because 59 | // we have to wait for the next `tick` so that Svelte flushes all pending state changes. 60 | await fireEvent.click(button); 61 | await act(); 62 | 63 | expect(getByText('This button has been clicked 2 times.')).toBeTruthy(); 64 | 65 | expect(editor.getHTML()).toContain(''); 66 | }); 67 | 68 | it('should render the editable component', async () => { 69 | const SvelteEditableExtension = Node.create({ 70 | name: 'svelteEditableComponent', 71 | group: 'block', 72 | content: 'inline*', 73 | 74 | parseHTML() { 75 | return [{ tag: 'svelte-editable-component' }]; 76 | }, 77 | 78 | renderHTML({ HTMLAttributes }) { 79 | return ['svelte-editable-component', mergeAttributes(HTMLAttributes), 0]; 80 | }, 81 | 82 | addNodeView() { 83 | return SvelteNodeViewRenderer(EditableComponent); 84 | }, 85 | }); 86 | 87 | const editor = new Editor({ 88 | content: 'This text is editable', 89 | extensions: [StarterKit, SvelteEditableExtension], 90 | }); 91 | 92 | const { getByText, getByTestId } = render(EditorContent, { editor }); 93 | 94 | await act(); 95 | expect(getByText('Svelte Editable Component')).toBeTruthy(); 96 | expect(getByTestId('editable-component')).toBeTruthy(); 97 | }); 98 | -------------------------------------------------------------------------------- /tests/floating-menu.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from 'vitest'; 2 | import { render } from '@testing-library/svelte'; 3 | 4 | import { FloatingMenu } from '$lib'; 5 | 6 | it('should throw error if editor instance is not provided', () => { 7 | expect(() => { 8 | render(FloatingMenu); 9 | }).toThrow(); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/setup.ts: -------------------------------------------------------------------------------- 1 | import { afterEach } from 'vitest'; 2 | import { cleanup, configure } from '@testing-library/svelte'; 3 | 4 | afterEach(cleanup); 5 | 6 | configure({ 7 | testIdAttribute: 'id', 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["**/*.ts", "**/*.svelte"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | import { svelteTesting } from '@testing-library/svelte/vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit(), svelteTesting()], 7 | test: { 8 | environment: 'happy-dom', 9 | include: ['tests/**/*.{test,spec}.{js,ts}'], 10 | setupFiles: ['tests/setup.ts'], 11 | }, 12 | }); 13 | --------------------------------------------------------------------------------