snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
This commit is contained in:
571
demos/CHANGELOG.md
Normal file
571
demos/CHANGELOG.md
Normal file
@@ -0,0 +1,571 @@
|
||||
# Change Log
|
||||
|
||||
## 3.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a1c6243: Allow KaTeX 0.17
|
||||
|
||||
## 3.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 75e8404: Fix the `Selection` extension highlighting beyond the selected text on multi-line selections: the native browser selection is now hidden while the editor is blurred, so only the styled `.selection` decoration is shown.
|
||||
|
||||
## 3.0.1
|
||||
|
||||
### Major Changes
|
||||
|
||||
- a92f4a6: We are now building packages with tsup which does not support UMD builds, please repackage if you require UMD builds
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 89bd9c7: Enforce type imports so that the bundler ignores TypeScript type imports when generating the index.js file of the dist directory
|
||||
- 8c69002: Synced beta with stable features
|
||||
|
||||
## 3.0.0-beta.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 8c69002: Synced beta with stable features
|
||||
|
||||
## 3.0.0-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 89bd9c7: Enforce type imports so that the bundler ignores TypeScript type imports when generating the index.js file of the dist directory
|
||||
|
||||
## 3.0.0-next.1
|
||||
|
||||
### Major Changes
|
||||
|
||||
- a92f4a6: We are now building packages with tsup which does not support UMD builds, please repackage if you require UMD builds
|
||||
|
||||
## 3.0.0-next.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- a92f4a6: We are now building packages with tsup which does not support UMD builds, please repackage if you require UMD builds
|
||||
|
||||
## 2.5.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 98fffbb: Upgraded prosemirror-tables to 1.6.3 to fix cells being resizable while the editor is uneditable
|
||||
|
||||
## 2.5.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7619215: 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.
|
||||
|
||||
## 2.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 6834a7f: Bundling of packages no longer includes tiptap dependency type definitions
|
||||
|
||||
## 2.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d6e56c4: declare lowlight to be a peer dep of extension-code-block-lowlight, update usage to v3
|
||||
|
||||
## 2.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 85d21ca: Updated demos and reverted vue specific performance enhancements until we know they work
|
||||
|
||||
> in commit ff04353b3ee0e6fc63733a673e2b27d2272a3355 revert: "fix(vue-3): faster component rendering (#5206)"
|
||||
> This reverts commit 31f37464912b7b21f3a565ca63222b9f5b6cce00.
|
||||
|
||||
and
|
||||
|
||||
> in commit dbab8e42eac893a0237566fb30c14b4ed0f3674a revert: "fix(vue-3): fix editor.state updating too late during a transaction due to reactiveState fixes #4870 (#5252)"
|
||||
> This reverts commit 509676ed4a63b84b904a98c1e34d18449d25c2a7.
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.4.0](https://github.com/ueberdosis/tiptap/compare/v2.3.2...v2.4.0) (2024-05-14)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.3.2](https://github.com/ueberdosis/tiptap/compare/v2.3.1...v2.3.2) (2024-05-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- NodePos querySelectorAll function ([#5094](https://github.com/ueberdosis/tiptap/issues/5094)) ([4900a27](https://github.com/ueberdosis/tiptap/commit/4900a27c5389d9a2d0d69f407ca3db0155304315))
|
||||
|
||||
## [2.3.1](https://github.com/ueberdosis/tiptap/compare/v2.3.0...v2.3.1) (2024-04-30)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.3.0](https://github.com/ueberdosis/tiptap/compare/v2.2.6...v2.3.0) (2024-04-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** fix nodepos child lookup ([#5038](https://github.com/ueberdosis/tiptap/issues/5038)) ([22ced31](https://github.com/ueberdosis/tiptap/commit/22ced318723003365fbfd8f59b8dac79c7563017))
|
||||
|
||||
### Features
|
||||
|
||||
- **core:** apply input and paste rules when using insertContent methods ([#5046](https://github.com/ueberdosis/tiptap/issues/5046)) ([96b6abc](https://github.com/ueberdosis/tiptap/commit/96b6abcf6edbc6cac03a391130d9feebb6de3a04))
|
||||
|
||||
## [2.2.6](https://github.com/ueberdosis/tiptap/compare/v2.2.5...v2.2.6) (2024-04-06)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.2.5](https://github.com/ueberdosis/tiptap/compare/v2.2.4...v2.2.5) (2024-04-05)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.2.4](https://github.com/ueberdosis/tiptap/compare/v2.2.3...v2.2.4) (2024-02-23)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.2.3](https://github.com/ueberdosis/tiptap/compare/v2.2.2...v2.2.3) (2024-02-15)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix test path ([21aa96d](https://github.com/ueberdosis/tiptap/commit/21aa96dee8deab1f439b7f655b8ed266a516a4cd))
|
||||
|
||||
## [2.2.2](https://github.com/ueberdosis/tiptap/compare/v2.2.1...v2.2.2) (2024-02-07)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.2.1](https://github.com/ueberdosis/tiptap/compare/v2.2.0...v2.2.1) (2024-01-31)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.2.0](https://github.com/ueberdosis/tiptap/compare/v2.1.16...v2.2.0) (2024-01-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** fix new lines being added via elementFromString ([#4767](https://github.com/ueberdosis/tiptap/issues/4767)) ([b7a2504](https://github.com/ueberdosis/tiptap/commit/b7a2504f16f46563537c890930cb2c332c256175))
|
||||
- fix imports, fix demos, unpin y-prosemirror ([681aa57](https://github.com/ueberdosis/tiptap/commit/681aa577bff500015c3f925e300c55a71c73efaf))
|
||||
- fix newline stripping via insertContent ([8954007](https://github.com/ueberdosis/tiptap/commit/8954007b2b92b040d69b26a0866ae58fabf5e512))
|
||||
|
||||
# [2.2.0-rc.8](https://github.com/ueberdosis/tiptap/compare/v2.1.14...v2.2.0-rc.8) (2024-01-08)
|
||||
|
||||
# [2.2.0-rc.7](https://github.com/ueberdosis/tiptap/compare/v2.2.0-rc.6...v2.2.0-rc.7) (2023-11-27)
|
||||
|
||||
# [2.2.0-rc.6](https://github.com/ueberdosis/tiptap/compare/v2.2.0-rc.5...v2.2.0-rc.6) (2023-11-23)
|
||||
|
||||
# [2.2.0-rc.4](https://github.com/ueberdosis/tiptap/compare/v2.1.11...v2.2.0-rc.4) (2023-10-10)
|
||||
|
||||
# [2.2.0-rc.3](https://github.com/ueberdosis/tiptap/compare/v2.2.0-rc.2...v2.2.0-rc.3) (2023-08-18)
|
||||
|
||||
# [2.2.0-rc.1](https://github.com/ueberdosis/tiptap/compare/v2.2.0-rc.0...v2.2.0-rc.1) (2023-08-18)
|
||||
|
||||
# [2.2.0-rc.0](https://github.com/ueberdosis/tiptap/compare/v2.1.5...v2.2.0-rc.0) (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
- **placeholder:** allow editor-is-empty class on any node ([#4335](https://github.com/ueberdosis/tiptap/issues/4335)) ([ff929b1](https://github.com/ueberdosis/tiptap/commit/ff929b179de930619005a773bb4186ae2aa2ec58))
|
||||
|
||||
## [2.1.16](https://github.com/ueberdosis/tiptap/compare/v2.1.15...v2.1.16) (2024-01-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** fix new lines being added via elementFromString ([#4767](https://github.com/ueberdosis/tiptap/issues/4767)) ([2235908](https://github.com/ueberdosis/tiptap/commit/2235908c28f388eda041d1d5d017554d513fe909))
|
||||
|
||||
## [2.1.15](https://github.com/ueberdosis/tiptap/compare/v2.1.14...v2.1.15) (2024-01-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** fix insertContentAt keeping new lines in html content ([#4465](https://github.com/ueberdosis/tiptap/issues/4465)) ([135a12f](https://github.com/ueberdosis/tiptap/commit/135a12f7aa2df839a0b619704110a360b980c738))
|
||||
- **link:** fix tests ([d495d92](https://github.com/ueberdosis/tiptap/commit/d495d92a1f7b1c51e09ac8f4934e15a2d1cf070d))
|
||||
|
||||
## [2.1.14](https://github.com/ueberdosis/tiptap/compare/v2.1.13...v2.1.14) (2024-01-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **typography:** require spaces after divisions to not break date formats ([#4696](https://github.com/ueberdosis/tiptap/issues/4696)) ([f6d7e00](https://github.com/ueberdosis/tiptap/commit/f6d7e00a746a67fa440a3fa0f5362295959873d2))
|
||||
|
||||
## [2.1.13](https://github.com/ueberdosis/tiptap/compare/v2.1.12...v2.1.13) (2023-11-30)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.12](https://github.com/ueberdosis/tiptap/compare/v2.1.11...v2.1.12) (2023-10-11)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.11](https://github.com/ueberdosis/tiptap/compare/v2.1.10...v2.1.11) (2023-09-20)
|
||||
|
||||
### Reverts
|
||||
|
||||
- Revert "v2.2.11" ([6aa755a](https://github.com/ueberdosis/tiptap/commit/6aa755a04b9955fc175c7ab33dee527d0d5deef0))
|
||||
|
||||
## [2.1.10](https://github.com/ueberdosis/tiptap/compare/v2.1.9...v2.1.10) (2023-09-15)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.9](https://github.com/ueberdosis/tiptap/compare/v2.1.8...v2.1.9) (2023-09-14)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.8](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.7...v2.1.8) (2023-09-04)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.7](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.6...v2.1.7) (2023-09-04)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.6](https://github.com/ueberdosis/tiptap/compare/v2.1.5...v2.1.6) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.2.0-rc.2](https://github.com/ueberdosis/tiptap/compare/v2.1.6...v2.2.0-rc.2) (2023-08-18)
|
||||
|
||||
# [2.2.0-rc.1](https://github.com/ueberdosis/tiptap/compare/v2.2.0-rc.0...v2.2.0-rc.1) (2023-08-18)
|
||||
|
||||
# [2.2.0-rc.0](https://github.com/ueberdosis/tiptap/compare/v2.1.5...v2.2.0-rc.0) (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
- **placeholder:** allow editor-is-empty class on any node ([#4335](https://github.com/ueberdosis/tiptap/issues/4335)) ([ff929b1](https://github.com/ueberdosis/tiptap/commit/ff929b179de930619005a773bb4186ae2aa2ec58))
|
||||
|
||||
# [2.2.0-rc.1](https://github.com/ueberdosis/tiptap/compare/v2.2.0-rc.0...v2.2.0-rc.1) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.2.0-rc.0](https://github.com/ueberdosis/tiptap/compare/v2.1.5...v2.2.0-rc.0) (2023-08-18)
|
||||
|
||||
### Features
|
||||
|
||||
- **placeholder:** allow editor-is-empty class on any node ([#4335](https://github.com/ueberdosis/tiptap/issues/4335)) ([ff929b1](https://github.com/ueberdosis/tiptap/commit/ff929b179de930619005a773bb4186ae2aa2ec58))
|
||||
|
||||
## [2.1.6](https://github.com/ueberdosis/tiptap/compare/v2.1.5...v2.1.6) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.5](https://github.com/ueberdosis/tiptap/compare/v2.1.4...v2.1.5) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.4](https://github.com/ueberdosis/tiptap/compare/v2.1.3...v2.1.4) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.3](https://github.com/ueberdosis/tiptap/compare/v2.1.2...v2.1.3) (2023-08-18)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
## [2.1.2](https://github.com/ueberdosis/tiptap/compare/v2.1.1...v2.1.2) (2023-08-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** fix error when merging class attributes ([#4340](https://github.com/ueberdosis/tiptap/issues/4340)) ([a251946](https://github.com/ueberdosis/tiptap/commit/a2519468589e2baa44901a66a3a06b24dc8626d6))
|
||||
|
||||
## [2.1.1](https://github.com/ueberdosis/tiptap/compare/v2.1.0...v2.1.1) (2023-08-16)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.14...v2.1.0) (2023-08-16)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0-rc.14](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.13...v2.1.0-rc.14) (2023-08-11)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0-rc.13](https://github.com/ueberdosis/tiptap-workspace/compare/v2.0.4...v2.1.0-rc.13) (2023-08-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **demos:** add missing extensions ([6383fd5](https://github.com/ueberdosis/tiptap-workspace/commit/6383fd54080b2ad555286cd0e7c4ad880200200f))
|
||||
- **demos:** update deps ([05a2edf](https://github.com/ueberdosis/tiptap-workspace/commit/05a2edfc16e297effa86d1583fb1680be0320f25))
|
||||
- **strikethrough:** update strikethrough shortcut ([#4288](https://github.com/ueberdosis/tiptap-workspace/issues/4288)) ([fd35db4](https://github.com/ueberdosis/tiptap-workspace/commit/fd35db4d090d9fdfef1196fb1f6f858f13cf53d1))
|
||||
|
||||
# [2.1.0-rc.12](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.11...v2.1.0-rc.12) (2023-07-14)
|
||||
|
||||
# [2.1.0-rc.11](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.10...v2.1.0-rc.11) (2023-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **tests:** fix link rel tests ([c1d1854](https://github.com/ueberdosis/tiptap-workspace/commit/c1d18543b03b1fb6b99a2f3546aa5da10c919920))
|
||||
|
||||
# [2.1.0-rc.10](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.9...v2.1.0-rc.10) (2023-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **react:** check props.clientRect before creating ReactRenderer ([#4138](https://github.com/ueberdosis/tiptap-workspace/issues/4138)) ([d710846](https://github.com/ueberdosis/tiptap-workspace/commit/d710846ecb6a3059dfbc21300b9a4b887a8defa3))
|
||||
|
||||
# [2.1.0-rc.9](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.8...v2.1.0-rc.9) (2023-06-15)
|
||||
|
||||
# [2.1.0-rc.8](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.7...v2.1.0-rc.8) (2023-05-25)
|
||||
|
||||
# [2.1.0-rc.5](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.4...v2.1.0-rc.5) (2023-05-25)
|
||||
|
||||
### Features
|
||||
|
||||
- add tiptap class ([614fc80](https://github.com/ueberdosis/tiptap-workspace/commit/614fc8082c376bf3c40a05c23ceda6b4a6fbf8d0))
|
||||
|
||||
# [2.1.0-rc.4](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.3...v2.1.0-rc.4) (2023-04-27)
|
||||
|
||||
# [2.1.0-rc.3](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.2...v2.1.0-rc.3) (2023-04-26)
|
||||
|
||||
# [2.1.0-rc.2](https://github.com/ueberdosis/tiptap-workspace/compare/v2.0.3...v2.1.0-rc.2) (2023-04-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **extension-link:** fix link not being kept when pasting url with link ([#3975](https://github.com/ueberdosis/tiptap-workspace/issues/3975)) ([e7d7d49](https://github.com/ueberdosis/tiptap-workspace/commit/e7d7d496376c8c11e24c342e20bd179a6ea7dcee))
|
||||
|
||||
# [2.1.0-rc.1](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.0...v2.1.0-rc.1) (2023-04-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **bubble-menu:** fix debounce not working with collab/collaboration cursor ([#3956](https://github.com/ueberdosis/tiptap-workspace/issues/3956)) ([e8cef04](https://github.com/ueberdosis/tiptap-workspace/commit/e8cef0404b5039ec2657536976b8b31931afd337))
|
||||
|
||||
# [2.1.0-rc.0](https://github.com/ueberdosis/tiptap-workspace/compare/v2.0.2...v2.1.0-rc.0) (2023-04-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- clear nodes when cursor at start of empty isolating parent ([#3943](https://github.com/ueberdosis/tiptap-workspace/issues/3943)) ([7278ee2](https://github.com/ueberdosis/tiptap-workspace/commit/7278ee2b05de2f96efddf3b1dc3bfd3d52262cbb))
|
||||
- Update peerDependencies to fix lerna version tasks ([#3914](https://github.com/ueberdosis/tiptap-workspace/issues/3914)) ([0c1bba3](https://github.com/ueberdosis/tiptap-workspace/commit/0c1bba3137b535776bcef95ff3c55e13f5a2db46))
|
||||
|
||||
# [2.1.0-rc.12](https://github.com/ueberdosis/tiptap-workspace/compare/v2.1.0-rc.11...v2.1.0-rc.12) (2023-07-14)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0-rc.11](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.10...v2.1.0-rc.11) (2023-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **tests:** fix link rel tests ([c1d1854](https://github.com/ueberdosis/tiptap/commit/c1d18543b03b1fb6b99a2f3546aa5da10c919920))
|
||||
|
||||
# [2.1.0-rc.10](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.9...v2.1.0-rc.10) (2023-07-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **react:** check props.clientRect before creating ReactRenderer ([#4138](https://github.com/ueberdosis/tiptap/issues/4138)) ([d710846](https://github.com/ueberdosis/tiptap/commit/d710846ecb6a3059dfbc21300b9a4b887a8defa3))
|
||||
|
||||
# [2.1.0-rc.9](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.8...v2.1.0-rc.9) (2023-06-15)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0-rc.8](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.7...v2.1.0-rc.8) (2023-05-25)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0-rc.7](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.6...v2.1.0-rc.7) (2023-05-25)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0-rc.6](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.5...v2.1.0-rc.6) (2023-05-25)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0-rc.5](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.4...v2.1.0-rc.5) (2023-05-25)
|
||||
|
||||
### Features
|
||||
|
||||
- add tiptap class ([614fc80](https://github.com/ueberdosis/tiptap/commit/614fc8082c376bf3c40a05c23ceda6b4a6fbf8d0))
|
||||
|
||||
# [2.1.0-rc.4](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.3...v2.1.0-rc.4) (2023-04-27)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0-rc.3](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.2...v2.1.0-rc.3) (2023-04-26)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.1.0-rc.2](https://github.com/ueberdosis/tiptap/compare/v2.0.3...v2.1.0-rc.2) (2023-04-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **extension-link:** fix link not being kept when pasting url with link ([#3975](https://github.com/ueberdosis/tiptap/issues/3975)) ([e7d7d49](https://github.com/ueberdosis/tiptap/commit/e7d7d496376c8c11e24c342e20bd179a6ea7dcee))
|
||||
|
||||
# [2.1.0-rc.1](https://github.com/ueberdosis/tiptap/compare/v2.1.0-rc.0...v2.1.0-rc.1) (2023-04-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **bubble-menu:** fix debounce not working with collab/collaboration cursor ([#3956](https://github.com/ueberdosis/tiptap/issues/3956)) ([e8cef04](https://github.com/ueberdosis/tiptap/commit/e8cef0404b5039ec2657536976b8b31931afd337))
|
||||
|
||||
# [2.1.0-rc.0](https://github.com/ueberdosis/tiptap/compare/v2.0.2...v2.1.0-rc.0) (2023-04-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- clear nodes when cursor at start of empty isolating parent ([#3943](https://github.com/ueberdosis/tiptap/issues/3943)) ([7278ee2](https://github.com/ueberdosis/tiptap/commit/7278ee2b05de2f96efddf3b1dc3bfd3d52262cbb))
|
||||
- Update peerDependencies to fix lerna version tasks ([#3914](https://github.com/ueberdosis/tiptap/issues/3914)) ([0c1bba3](https://github.com/ueberdosis/tiptap/commit/0c1bba3137b535776bcef95ff3c55e13f5a2db46))
|
||||
|
||||
# [2.1.0-rc.0](https://github.com/ueberdosis/tiptap/compare/v2.0.2...v2.1.0-rc.0) (2023-04-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- clear nodes when cursor at start of empty isolating parent ([#3943](https://github.com/ueberdosis/tiptap/issues/3943)) ([7278ee2](https://github.com/ueberdosis/tiptap/commit/7278ee2b05de2f96efddf3b1dc3bfd3d52262cbb))
|
||||
- **bubble-menu:** fix debounce not working with collab/collaboration cursor ([#3956](https://github.com/ueberdosis/tiptap/issues/3956)) ([a78f8cd](https://github.com/ueberdosis/tiptap/commit/a78f8cd9646008e4db938fa3c22b0714c8bb5849))
|
||||
|
||||
## [2.0.3](https://github.com/ueberdosis/tiptap/compare/v2.0.2...v2.0.3) (2023-04-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **bubble-menu:** fix debounce not working with collab/collaboration cursor ([#3956](https://github.com/ueberdosis/tiptap/issues/3956)) ([e8cef04](https://github.com/ueberdosis/tiptap/commit/e8cef0404b5039ec2657536976b8b31931afd337))
|
||||
|
||||
## [2.0.2](https://github.com/ueberdosis/tiptap/compare/v2.0.1...v2.0.2) (2023-04-03)
|
||||
|
||||
### Features
|
||||
|
||||
- add box-shadow to collab demo ([c5496c1](https://github.com/ueberdosis/tiptap/commit/c5496c1b27783150dafb5ebdf6bda43648a46316))
|
||||
- landingpage demo ([#3925](https://github.com/ueberdosis/tiptap/issues/3925)) ([958925f](https://github.com/ueberdosis/tiptap/commit/958925f2560ca786cd0cf52b83b7ae51deb7dd77))
|
||||
- Tiptap collab demo styling ([87840b0](https://github.com/ueberdosis/tiptap/commit/87840b0f0821ca65d9f104d9c90512021aa70113))
|
||||
|
||||
## [2.0.1](https://github.com/ueberdosis/tiptap/compare/v2.0.0...v2.0.1) (2023-03-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Update peerDependencies to fix lerna version tasks ([#3914](https://github.com/ueberdosis/tiptap/issues/3914)) ([0534f76](https://github.com/ueberdosis/tiptap/commit/0534f76401bf5399c01ca7f39d87f7221d91b4f7))
|
||||
|
||||
# [2.0.0-beta.220](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.219...v2.0.0-beta.220) (2023-02-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** fix destroyed view causing errors on dispatchTransaction ([#3799](https://github.com/ueberdosis/tiptap/issues/3799)) ([3c07ca0](https://github.com/ueberdosis/tiptap/commit/3c07ca0b9c48cef60d56acdd44812e20e05fc928))
|
||||
- **tests:** fix tests for lists ([02eec8a](https://github.com/ueberdosis/tiptap/commit/02eec8aaefc2709dc20f91c3c8f9eca84cddc12d))
|
||||
|
||||
# [2.0.0-beta.219](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.218...v2.0.0-beta.219) (2023-02-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** allow insertContentAt and insertContent text node arrays ([#3790](https://github.com/ueberdosis/tiptap/issues/3790)) ([0300630](https://github.com/ueberdosis/tiptap/commit/0300630a5b04b61d4eef8155f24ca0ef2d683966))
|
||||
|
||||
### Features
|
||||
|
||||
- [#3540](https://github.com/ueberdosis/tiptap/issues/3540) Ability to preserve marks on lists ([#3541](https://github.com/ueberdosis/tiptap/issues/3541)) ([36bb1e1](https://github.com/ueberdosis/tiptap/commit/36bb1e1041f91da6437272e7196702df868eae0f))
|
||||
|
||||
# [2.0.0-beta.218](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.217...v2.0.0-beta.218) (2023-02-18)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.217](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.216...v2.0.0-beta.217) (2023-02-09)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.216](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.215...v2.0.0-beta.216) (2023-02-08)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.215](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.214...v2.0.0-beta.215) (2023-02-08)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.214](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.213...v2.0.0-beta.214) (2023-02-08)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.213](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.212...v2.0.0-beta.213) (2023-02-07)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.212](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.211...v2.0.0-beta.212) (2023-02-03)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.211](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.210...v2.0.0-beta.211) (2023-02-02)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.210](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.209...v2.0.0-beta.210) (2023-02-02)
|
||||
|
||||
### Features
|
||||
|
||||
- **pm:** new prosemirror package for dependency resolving ([f387ad3](https://github.com/ueberdosis/tiptap/commit/f387ad3dd4c2b30eaea33fb0ba0b42e0cd39263b))
|
||||
|
||||
# [2.0.0-beta.209](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.208...v2.0.0-beta.209) (2022-12-16)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.208](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.207...v2.0.0-beta.208) (2022-12-16)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.207](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.206...v2.0.0-beta.207) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.206](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.205...v2.0.0-beta.206) (2022-12-08)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.205](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.204...v2.0.0-beta.205) (2022-12-05)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.204](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.203...v2.0.0-beta.204) (2022-11-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **tests:** fix autolink validation test ([5150095](https://github.com/ueberdosis/tiptap/commit/5150095c6b510c080f4aa35f54d2387543f86da8))
|
||||
|
||||
# [2.0.0-beta.203](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.202...v2.0.0-beta.203) (2022-11-24)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **extension/table:** move dependency from @\_ueberdosis to [@tiptap](https://github.com/tiptap) ([#3448](https://github.com/ueberdosis/tiptap/issues/3448)) ([31c3a9a](https://github.com/ueberdosis/tiptap/commit/31c3a9aad9eb37f445eadcd27135611291178ca6))
|
||||
|
||||
# [2.0.0-beta.202](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.201...v2.0.0-beta.202) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.201](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.200...v2.0.0-beta.201) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.200](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.199...v2.0.0-beta.200) (2022-11-04)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.199](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.198...v2.0.0-beta.199) (2022-09-30)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.198](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.197...v2.0.0-beta.198) (2022-09-29)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.197](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.196...v2.0.0-beta.197) (2022-09-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** Can() does not work for setting marks ([#3223](https://github.com/ueberdosis/tiptap/issues/3223)) ([17a41da](https://github.com/ueberdosis/tiptap/commit/17a41da5a7a14879cf490c81914084791c4c494c))
|
||||
|
||||
# [2.0.0-beta.196](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.195...v2.0.0-beta.196) (2022-09-20)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.195](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.194...v2.0.0-beta.195) (2022-09-14)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **extension/bubble-menu:** :bug: fix bubble menu and floating menu being available when editor not editable ([#3195](https://github.com/ueberdosis/tiptap/issues/3195)) ([fa96749](https://github.com/ueberdosis/tiptap/commit/fa96749ce22ec67125da491cfeeb38623b9f0d6e))
|
||||
|
||||
# [2.0.0-beta.194](https://github.com/ueberdosis/tiptap/compare/v2.0.0-beta.193...v2.0.0-beta.194) (2022-09-11)
|
||||
|
||||
**Note:** Version bump only for package tiptap-demos
|
||||
|
||||
# [2.0.0-beta.193](https://github.com/ueberdosis/tiptap/compare/v0.1.2...v2.0.0-beta.193) (2022-09-10)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- bump documents ([43611ea](https://github.com/ueberdosis/tiptap/commit/43611ea2e70d3dc66ff907ba7ca377bf74814543))
|
||||
- disable broken tests for experiements with further todo message ([b8ae9e2](https://github.com/ueberdosis/tiptap/commit/b8ae9e27622857093c6ca539901956da5cc291e5))
|
||||
- don’t check for active node in wrapIn command, fix [#1059](https://github.com/ueberdosis/tiptap/issues/1059) ([170ec4b](https://github.com/ueberdosis/tiptap/commit/170ec4be5b3c8362890ca3100a223b505f788381))
|
||||
- **extension/collaboration:** :ambulance: pin y-prosemirror version to 1.0.20 to fix broken functionality with vue ([5989f3b](https://github.com/ueberdosis/tiptap/commit/5989f3b780bb64b2884d81dcd41a95d98a0714b2))
|
||||
- fix RangeError bug when selecting all text, fix [#2490](https://github.com/ueberdosis/tiptap/issues/2490) ([70422dd](https://github.com/ueberdosis/tiptap/commit/70422dd107ed1ecdd8dfe41a8a93297124d2f1e0))
|
||||
- **maintainment:** fix cjs issues with prosemirror-tables ([eb92597](https://github.com/ueberdosis/tiptap/commit/eb925976038fbf59f6ba333ccc57ea84113da00e))
|
||||
- remove some magic strings ([6c34dec](https://github.com/ueberdosis/tiptap/commit/6c34dec33ac39c9f037a0a72e4525f3fc6d422bf))
|
||||
- **suggestion:** :bug: make clientrect prop optional as it can potentially be undefined ([#2813](https://github.com/ueberdosis/tiptap/issues/2813)) ([f019f70](https://github.com/ueberdosis/tiptap/commit/f019f70a19c34715e2d5c3921d348e11c7ac51a3)), closes [#2795](https://github.com/ueberdosis/tiptap/issues/2795)
|
||||
- temp fix collaboration demo ([4528756](https://github.com/ueberdosis/tiptap/commit/45287563f3cfb389095a2794cb2001d65e56d633))
|
||||
|
||||
### Features
|
||||
|
||||
- Add extension storage ([#2069](https://github.com/ueberdosis/tiptap/issues/2069)) ([7ffabf2](https://github.com/ueberdosis/tiptap/commit/7ffabf251c408a652eec1931cc78a8bd43cccb67))
|
||||
- add getText() and generateText() methods (fix [#1428](https://github.com/ueberdosis/tiptap/issues/1428)) ([#1875](https://github.com/ueberdosis/tiptap/issues/1875)) ([fe6a3e7](https://github.com/ueberdosis/tiptap/commit/fe6a3e7491f6a42123d3d8a92ab588f2a40d7799))
|
||||
- add some improvements to `CharacterCount` extension ([#2256](https://github.com/ueberdosis/tiptap/issues/2256)), fix [#1049](https://github.com/ueberdosis/tiptap/issues/1049), fix [#1550](https://github.com/ueberdosis/tiptap/issues/1550), fix [#1839](https://github.com/ueberdosis/tiptap/issues/1839), fix [#2245](https://github.com/ueberdosis/tiptap/issues/2245) ([5daa870](https://github.com/ueberdosis/tiptap/commit/5daa870b0906f0387fe07041681bc6f5b3774617))
|
||||
- Add support for autolink ([#2226](https://github.com/ueberdosis/tiptap/issues/2226)) ([3d68981](https://github.com/ueberdosis/tiptap/commit/3d68981b47d087fff40549d2143eb952fc9e0a50))
|
||||
- **extension-link:** :sparkles: add validate option to link extension ([23e67ad](https://github.com/ueberdosis/tiptap/commit/23e67adfa730df7364bc31220d0ed0e8ea522593)), closes [#2779](https://github.com/ueberdosis/tiptap/issues/2779)
|
||||
- **extension/youtube:** :sparkles: new youtube embed extension ([#2814](https://github.com/ueberdosis/tiptap/issues/2814)) ([1c0554b](https://github.com/ueberdosis/tiptap/commit/1c0554b7c06d80145274353e58d56608b097fbe4))
|
||||
- Integrate input rules and paste rules into the core ([#1997](https://github.com/ueberdosis/tiptap/issues/1997)) ([723b955](https://github.com/ueberdosis/tiptap/commit/723b955cecc5c92c8aad897ce16c60fb62976571))
|
||||
- parseHTML for attributes should return the value instead of an object now, fix [#1863](https://github.com/ueberdosis/tiptap/issues/1863) ([8a3b47a](https://github.com/ueberdosis/tiptap/commit/8a3b47a529d28b28b50d634c6ff69b8e5aad3080))
|
||||
39
demos/dedupeDeps.txt
Normal file
39
demos/dedupeDeps.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
d3
|
||||
highlight.js/lib/languages/css
|
||||
highlight.js/lib/languages/javascript
|
||||
highlight.js/lib/languages/typescript
|
||||
highlight.js/lib/languages/xml
|
||||
highlight.js/lib/core
|
||||
linkifyjs
|
||||
lowlight
|
||||
react
|
||||
react-dom
|
||||
react-dom/client
|
||||
use-sync-external-store/shim
|
||||
use-sync-external-store/shim/with-selector
|
||||
shiki
|
||||
@floating-ui/dom
|
||||
uuid
|
||||
y-webrtc
|
||||
yjs
|
||||
prosemirror-changeset
|
||||
prosemirror-collab
|
||||
prosemirror-commands
|
||||
prosemirror-dropcursor
|
||||
prosemirror-gapcursor
|
||||
prosemirror-history
|
||||
prosemirror-inputrules
|
||||
prosemirror-keymap
|
||||
prosemirror-markdown
|
||||
prosemirror-menu
|
||||
prosemirror-model
|
||||
prosemirror-schema-basic
|
||||
prosemirror-schema-list
|
||||
prosemirror-state
|
||||
prosemirror-tables
|
||||
prosemirror-trailing-node
|
||||
prosemirror-transform
|
||||
prosemirror-view
|
||||
@hocuspocus/provider
|
||||
@lifeomic/attempt
|
||||
@tiptap/y-tiptap
|
||||
9
demos/index.html
Normal file
9
demos/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="refresh" content="0; url=/preview/" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
71
demos/package.json
Normal file
71
demos/package.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "tiptap-demos",
|
||||
"version": "3.0.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vp dev --host",
|
||||
"start:e2e": "vp dev --host --port 4080",
|
||||
"build:demos": "vp build",
|
||||
"preview": "vp preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@hocuspocus/provider": "^2.15.0",
|
||||
"@hocuspocus/transformer": "^2.15.0",
|
||||
"@lexical/react": "^0.36.2",
|
||||
"@lifeomic/attempt": "3.1.0",
|
||||
"@shikijs/core": "1.10.3",
|
||||
"@tiptap/y-tiptap": "^3.0.7",
|
||||
"d3": "^7.9.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"highlight.js": "^11.11.1",
|
||||
"katex": "^0.18.0",
|
||||
"lexical": "^0.36.2",
|
||||
"linkifyjs": "^4.3.2",
|
||||
"lowlight": "^3.3.0",
|
||||
"prosemirror-changeset": "^2.4.1",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
"prosemirror-commands": "^1.7.1",
|
||||
"prosemirror-dropcursor": "^1.8.2",
|
||||
"prosemirror-gapcursor": "^1.4.1",
|
||||
"prosemirror-history": "^1.5.0",
|
||||
"prosemirror-inputrules": "^1.5.1",
|
||||
"prosemirror-keymap": "^1.2.3",
|
||||
"prosemirror-markdown": "^1.13.4",
|
||||
"prosemirror-menu": "^1.3.2",
|
||||
"prosemirror-model": "^1.25.9",
|
||||
"prosemirror-schema-basic": "^1.2.4",
|
||||
"prosemirror-schema-list": "^1.5.1",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-tables": "^1.8.5",
|
||||
"prosemirror-trailing-node": "^3.0.0",
|
||||
"prosemirror-transform": "^1.12.0",
|
||||
"prosemirror-view": "^1.42.3",
|
||||
"remixicon": "^2.5.0",
|
||||
"shiki": "^1.25.1",
|
||||
"y-webrtc": "10.3.0",
|
||||
"yjs": "13.6.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "7.2.0",
|
||||
"@types/node": "^22.12.0",
|
||||
"@vitejs/plugin-react": "6.0.5",
|
||||
"@vitejs/plugin-vue": "6.0.8",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"iframe-resizer": "^4.4.5",
|
||||
"postcss": "^8.5.23",
|
||||
"postcss-import": "^15.1.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"sass-embedded": "^1.99.0",
|
||||
"svelte": "^5.56.8",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"uuid": "^14.0.0",
|
||||
"vite": "catalog:",
|
||||
"vite-plus": "catalog:",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
}
|
||||
}
|
||||
8
demos/postcss.config.cjs
Normal file
8
demos/postcss.config.cjs
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'tailwindcss/nesting': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
233
demos/preview/Demo.vue
Normal file
233
demos/preview/Demo.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div class="overflow-hidden antialiased rounded-lg">
|
||||
<div class="px-3 py-1 bg-black flex items-center gap-0.5">
|
||||
<button
|
||||
v-for="(language, index) in sortedTabs"
|
||||
:key="index"
|
||||
@click="setTab(language.name)"
|
||||
class="px-3 py-2 text-sm text-white leading-[125%] font-semibold rounded-[0.625rem] transition-all"
|
||||
:class="[
|
||||
currentTab === language.name
|
||||
? 'opacity-100 bg-[#1C1917]'
|
||||
: 'opacity-50 bg-transparent hover:opacity-100 hover:bg-[#1C1917]',
|
||||
]"
|
||||
>
|
||||
{{ language.name }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<div class="bg-white" :class="[hidePreview ? 'hidden' : '']">
|
||||
<demo-frame :src="currentIframeUrl" :key="currentIframeUrl" />
|
||||
</div>
|
||||
|
||||
<div class="text-white bg-black" v-if="!hideSource && currentFile">
|
||||
<div class="flex overflow-x-auto">
|
||||
<div class="flex flex-auto px-4 border-b-2 border-gray-800">
|
||||
<button
|
||||
class="inline-flex relative mr-4 py-2 pb-[calc(0.3rem + 2px)] mb-[-2px] border-b-2 border-transparent font-mono text-sm whitespace-nowrap"
|
||||
:class="[
|
||||
!showDebug && currentFile.content === file.content
|
||||
? 'text-white border-white font-bold'
|
||||
: 'text-gray-400',
|
||||
]"
|
||||
v-for="(file, index) in source"
|
||||
:key="index"
|
||||
@click="setFile(file.name)"
|
||||
>
|
||||
{{ file.name }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="debugJSON"
|
||||
class="inline-flex relative py-2 pb-[calc(0.3rem + 2px)] mb-[-2px] border-b-2 border-transparent font-mono text-sm ml-auto"
|
||||
:class="[showDebug ? 'text-white border-white font-bold' : 'text-gray-400']"
|
||||
@click="showDebug = !showDebug"
|
||||
>
|
||||
Inspect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-dark overflow-auto max-h-[500px] relative text-white">
|
||||
<shiki
|
||||
class="p-4 overflow-visible"
|
||||
:language="debugJSON && showDebug ? 'js' : getFileExtension(currentFile.name)"
|
||||
:code="debugJSON && showDebug ? debugJSON : currentFile.content"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between px-4 py-2 text-gray-400 border-t border-gray-800 text-md">
|
||||
<a
|
||||
class="flex-shrink min-w-0 overflow-hidden overflow-ellipsis whitespace-nowrap"
|
||||
:href="currentIframeUrl"
|
||||
>
|
||||
{{ name }}/{{ currentTab }}
|
||||
</a>
|
||||
<a class="pl-4 whitespace-nowrap" :href="githubUrl" target="_blank"> Edit on GitHub → </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getDebugJSON } from '@tiptap/core'
|
||||
|
||||
import DemoFrame from './DemoFrame.vue'
|
||||
import Shiki from './Shiki.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DemoFrame,
|
||||
Shiki,
|
||||
},
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
tabs: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
data: [],
|
||||
sources: {},
|
||||
currentTab: null,
|
||||
currentFile: null,
|
||||
tabOrder: ['React', 'Vue', 'Svelte', 'JS'],
|
||||
debugJSON: null,
|
||||
showDebug: false,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
currentIframeUrl() {
|
||||
return `/src/${this.name}/${this.currentTab}/`
|
||||
},
|
||||
|
||||
firstTabSelected() {
|
||||
return this.sortedTabs[0].name === this.currentTab
|
||||
},
|
||||
|
||||
sortedTabs() {
|
||||
return [...this.tabs].sort((a, b) => {
|
||||
return this.tabOrder.indexOf(a.name) - this.tabOrder.indexOf(b.name)
|
||||
})
|
||||
},
|
||||
|
||||
query() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(this.$route.query).map(([key, value]) => [key, this.fromString(value)]),
|
||||
)
|
||||
},
|
||||
|
||||
inline() {
|
||||
return this.query.inline || false
|
||||
},
|
||||
|
||||
hideSource() {
|
||||
return this.query.hideSource || false
|
||||
},
|
||||
|
||||
hidePreview() {
|
||||
return this.query.hidePreview || false
|
||||
},
|
||||
|
||||
githubUrl() {
|
||||
return `https://github.com/ueberdosis/tiptap/tree/main/demos/src/${this.name}`
|
||||
},
|
||||
|
||||
source() {
|
||||
return this.sources[this.currentTab]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
getFileExtension(name) {
|
||||
return name.split('.').pop()
|
||||
},
|
||||
|
||||
setTab(name, persist = true) {
|
||||
this.currentTab = name
|
||||
|
||||
if (persist) {
|
||||
localStorage.tab = name
|
||||
}
|
||||
},
|
||||
|
||||
setFile(name) {
|
||||
this.showDebug = false
|
||||
this.currentFile = this.source.find(item => item.name === name)
|
||||
},
|
||||
|
||||
onSource(event) {
|
||||
this.sources[this.currentTab] = event.detail
|
||||
this.setFile(this.source[0].name)
|
||||
},
|
||||
|
||||
onEditor(event) {
|
||||
const editor = event.detail
|
||||
|
||||
if (!editor) {
|
||||
this.debugJSON = null
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.debugJSON = JSON.stringify(getDebugJSON(editor.state.doc), null, ' ')
|
||||
|
||||
editor.on('update', () => {
|
||||
this.debugJSON = JSON.stringify(getDebugJSON(editor.state.doc), null, ' ')
|
||||
})
|
||||
},
|
||||
|
||||
fromString(value) {
|
||||
if (value === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value.match(/^\d*(\.\d+)?$/)) {
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
if (value === 'true') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (value === 'null') {
|
||||
return null
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// TODO: load language from url params
|
||||
const intitialTab =
|
||||
localStorage.tab && this.tabs.some(tab => tab.name === localStorage.tab)
|
||||
? localStorage.tab
|
||||
: this.sortedTabs[0]?.name
|
||||
|
||||
this.setTab(intitialTab, false)
|
||||
|
||||
window.document.addEventListener('editor', this.onEditor, false)
|
||||
window.document.addEventListener('source', this.onSource, false)
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
window.document.removeEventListener('editor', this.onEditor)
|
||||
window.document.removeEventListener('source', this.onSource)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
56
demos/preview/DemoFrame.vue
Normal file
56
demos/preview/DemoFrame.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="flex flex-col relative min-h-[5rem]">
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full flex justify-center items-center pointer-events-none"
|
||||
v-if="isLoading"
|
||||
>
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-3 h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<iframe
|
||||
class="bg-transparent max-h-[28rem]"
|
||||
v-resize.quiet="{ scrolling: 'omit' }"
|
||||
:src="src"
|
||||
width="100%"
|
||||
height="0"
|
||||
frameborder="0"
|
||||
@load="onLoad"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
src: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isLoading: true,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onLoad() {
|
||||
this.isLoading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
73
demos/preview/Shiki.vue
Normal file
73
demos/preview/Shiki.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div v-if="html" v-html="html" />
|
||||
<pre v-else><code>{{ code }}</code></pre>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// this import is a bugfix
|
||||
// otherwise the `onig.wasm` file is missing in the dist folder
|
||||
import 'shiki/dist/onig.wasm?url'
|
||||
|
||||
import Worker from './shiki.worker.js?worker'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
code: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
|
||||
language: {
|
||||
default: 'js',
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
worker: new Worker(),
|
||||
html: null,
|
||||
highlighter: window?.highlighter,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
code: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.render()
|
||||
},
|
||||
},
|
||||
|
||||
highlighter: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.render()
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
render() {
|
||||
this.worker.postMessage({
|
||||
code: this.code,
|
||||
language: this.language,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.worker.addEventListener('message', event => {
|
||||
const { html } = event.data
|
||||
|
||||
this.html = html
|
||||
})
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.shiki {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
12
demos/preview/index.html
Normal file
12
demos/preview/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Preview</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/preview/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
61
demos/preview/index.js
Normal file
61
demos/preview/index.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'iframe-resizer/js/iframeResizer.contentWindow'
|
||||
import './style.css'
|
||||
|
||||
import { demos } from '@demos'
|
||||
import iframeResize from 'iframe-resizer/js/iframeResizer'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import Demo from './Demo.vue'
|
||||
import App from './index.vue'
|
||||
|
||||
const routes = demos.map(({ name, tabs }) => {
|
||||
return {
|
||||
path: `/${name}`,
|
||||
component: Demo,
|
||||
props: {
|
||||
name,
|
||||
tabs,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory('preview'),
|
||||
routes,
|
||||
})
|
||||
|
||||
createApp(App)
|
||||
.directive('resize', {
|
||||
beforeMount: (el, { value = {} }) => {
|
||||
el.addEventListener('load', () => {
|
||||
iframeResize(
|
||||
{
|
||||
...value,
|
||||
// messageCallback(messageData) {
|
||||
// if (messageData.message.type !== 'resize') {
|
||||
// return
|
||||
// }
|
||||
|
||||
// const style = window.getComputedStyle(el.parentElement)
|
||||
// const maxHeight = parseInt(style.getPropertyValue('max-height'), 10)
|
||||
|
||||
// if (messageData.message.height > maxHeight) {
|
||||
// el.setAttribute('scrolling', 'auto')
|
||||
// } else {
|
||||
// el.setAttribute('scrolling', 'no')
|
||||
// }
|
||||
|
||||
// el?.iFrameResizer?.resize?.()
|
||||
// },
|
||||
},
|
||||
el,
|
||||
)
|
||||
})
|
||||
},
|
||||
unmounted(el) {
|
||||
el?.iFrameResizer?.removeListeners?.()
|
||||
},
|
||||
})
|
||||
.use(router)
|
||||
.mount('#app')
|
||||
95
demos/preview/index.vue
Normal file
95
demos/preview/index.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const searchQuery = new URLSearchParams(window.location.search)
|
||||
|
||||
const searchValue = ref(searchQuery.get('search') || '')
|
||||
|
||||
// Update the window search param when the searchValue changes
|
||||
// this allows the user to bookmark / return to a specific search
|
||||
// and also allows the browser to handle back/forward navigation
|
||||
// without losing the search state
|
||||
watch(searchValue, (newValue, oldValue) => {
|
||||
if (newValue !== oldValue) {
|
||||
searchQuery.set('search', newValue)
|
||||
window.history.replaceState({}, '', `?${searchQuery.toString()}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="$route.path === '/'">
|
||||
<input
|
||||
class="w-full p-3 my-3 focus:outline-none border-b"
|
||||
type="search"
|
||||
placeholder="Search for a demo..."
|
||||
autofocus
|
||||
v-model="searchValue"
|
||||
/>
|
||||
<ul>
|
||||
<li
|
||||
class="p-5 border-b-2 border-black"
|
||||
v-for="route in $router.options.routes.filter(route =>
|
||||
searchValue === ''
|
||||
? true
|
||||
: route.props.name.toLowerCase().includes(searchValue.toLowerCase()),
|
||||
)"
|
||||
:key="route.path"
|
||||
>
|
||||
<router-link class="block mb-2 font-medium" :to="route.path">
|
||||
{{ route.props.name }}
|
||||
</router-link>
|
||||
|
||||
<div class="flex">
|
||||
<a
|
||||
class="mr-4 text-sm text-gray-300 font-medium"
|
||||
v-for="(tab, index) in route.props.tabs"
|
||||
:key="index"
|
||||
:href="`/src/${route.props.name}/${tab.name}/`"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<router-view v-else />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
fromString(value) {
|
||||
if (value === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value.match(/^\d*(\.\d+)?$/)) {
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
if (value === 'true') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (value === 'null') {
|
||||
return null
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
query() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(this.$route.query).map(([key, value]) => [key, this.fromString(value)]),
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
30
demos/preview/shiki.worker.js
Normal file
30
demos/preview/shiki.worker.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as shiki from 'shiki'
|
||||
|
||||
let highlighter = null
|
||||
|
||||
async function init() {
|
||||
if (highlighter) {
|
||||
return highlighter
|
||||
}
|
||||
|
||||
highlighter = await shiki.createHighlighter({
|
||||
themes: ['material-theme-darker'],
|
||||
langs: ['html', 'js', 'jsx', 'ts', 'tsx', 'css', 'vue-html', 'vue', 'scss'],
|
||||
})
|
||||
|
||||
return highlighter
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line
|
||||
self.addEventListener('message', async event => {
|
||||
init().then(async () => {
|
||||
const { code, language } = event.data
|
||||
|
||||
await highlighter.loadLanguage(language)
|
||||
|
||||
const html = highlighter.codeToHtml(code, { lang: language, theme: 'material-theme-darker' })
|
||||
|
||||
// oxlint-disable-next-line
|
||||
self.postMessage({ code, language, html })
|
||||
})
|
||||
})
|
||||
148
demos/preview/style.css
Normal file
148
demos/preview/style.css
Normal file
@@ -0,0 +1,148 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://rsms.me/inter/font-files/Inter-Regular.woff2?v=3.19') format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-Regular.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://rsms.me/inter/font-files/Inter-Italic.woff2?v=3.19') format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-Italic.woff?v=3.19') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://rsms.me/inter/font-files/Inter-Medium.woff2?v=3.19') format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-Medium.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://rsms.me/inter/font-files/Inter-MediumItalic.woff2?v=3.19') format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-MediumItalic.woff?v=3.19') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://rsms.me/inter/font-files/Inter-SemiBold.woff2?v=3.19') format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-SemiBold.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://rsms.me/inter/font-files/Inter-SemiBoldItalic.woff2?v=3.19') format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-SemiBoldItalic.woff?v=3.19') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://rsms.me/inter/font-files/Inter-Bold.woff2?v=3.19') format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-Bold.woff?v=3.19') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://rsms.me/inter/font-files/Inter-BoldItalic.woff2?v=3.19') format('woff2'),
|
||||
url('https://rsms.me/inter/font-files/Inter-BoldItalic.woff?v=3.19') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2')
|
||||
format('woff2'),
|
||||
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff')
|
||||
format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff2')
|
||||
format('woff2'),
|
||||
url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff')
|
||||
format('woff');
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
border: 4px solid transparent;
|
||||
background-clip: padding-box;
|
||||
border-radius: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 4px solid rgba(0, 0, 0, 0);
|
||||
background-clip: padding-box;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.overflow-dark:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.overflow-dark::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
1
demos/public/_redirects
Normal file
1
demos/public/_redirects
Normal file
@@ -0,0 +1 @@
|
||||
/preview/* /preview/index.html 200
|
||||
40
demos/setup/helper.ts
Normal file
40
demos/setup/helper.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
const waitUntilElementExists = (selector: any, callback: (element: Element) => void) => {
|
||||
const element = document.querySelector(selector)
|
||||
|
||||
if (element) {
|
||||
return callback(element)
|
||||
}
|
||||
|
||||
setTimeout(() => waitUntilElementExists(selector, callback), 500)
|
||||
}
|
||||
|
||||
const sendData = (eventName: string, data: any) => {
|
||||
const event = new CustomEvent(eventName, { detail: data })
|
||||
|
||||
window.parent.document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
export function splitName(name: string) {
|
||||
const parts = name.split('/')
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw Error(
|
||||
'Demos must always be within exactly one category. Nested categories are not supported.',
|
||||
)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
export function debug() {
|
||||
sendData('editor', null)
|
||||
// @ts-ignore
|
||||
sendData('source', window.source)
|
||||
|
||||
waitUntilElementExists('.tiptap', element => {
|
||||
// @ts-ignore
|
||||
const editor = element.editor
|
||||
|
||||
sendData('editor', editor)
|
||||
})
|
||||
}
|
||||
12
demos/setup/js.ts
Normal file
12
demos/setup/js.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'iframe-resizer/js/iframeResizer.contentWindow'
|
||||
import './style.scss'
|
||||
|
||||
import { debug } from './helper.js'
|
||||
|
||||
export default function init(name: string, source: any) {
|
||||
// @ts-ignore
|
||||
window.source = source
|
||||
document.title = name
|
||||
|
||||
debug()
|
||||
}
|
||||
35
demos/setup/react.ts
Normal file
35
demos/setup/react.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'iframe-resizer/js/iframeResizer.contentWindow'
|
||||
import './style.scss'
|
||||
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import { debug, splitName } from './helper.js'
|
||||
|
||||
export default function init(name: string, source: any) {
|
||||
// @ts-ignore
|
||||
window.source = source
|
||||
document.title = name
|
||||
|
||||
const [demoCategory, demoName, frameworkName] = splitName(name)
|
||||
|
||||
import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.tsx`)
|
||||
.then(module => {
|
||||
const root = document.getElementById('app')
|
||||
|
||||
if (root) {
|
||||
createRoot(root).render(React.createElement(module.default))
|
||||
}
|
||||
debug()
|
||||
})
|
||||
.catch(() => {
|
||||
import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.jsx`).then(module => {
|
||||
const root = document.getElementById('app')
|
||||
|
||||
if (root) {
|
||||
createRoot(root).render(React.createElement(module.default))
|
||||
}
|
||||
debug()
|
||||
})
|
||||
})
|
||||
}
|
||||
500
demos/setup/style.scss
Normal file
500
demos/setup/style.scss
Normal file
@@ -0,0 +1,500 @@
|
||||
/* Base HTML and global element styles*/
|
||||
:root {
|
||||
--white: #fff;
|
||||
--black: #2e2b29;
|
||||
--black-contrast: #110f0e;
|
||||
--gray-1: rgba(61, 37, 20, 0.05);
|
||||
--gray-2: rgba(61, 37, 20, 0.08);
|
||||
--gray-3: rgba(61, 37, 20, 0.12);
|
||||
--gray-4: rgba(53, 38, 28, 0.3);
|
||||
--gray-5: rgba(28, 25, 23, 0.6);
|
||||
--green: #22c55e;
|
||||
--purple: #6a00f5;
|
||||
--purple-contrast: #5800cc;
|
||||
--purple-light: rgba(88, 5, 255, 0.05);
|
||||
--yellow-contrast: #facc15;
|
||||
--yellow: rgba(250, 204, 21, 0.4);
|
||||
--yellow-light: #fffae5;
|
||||
--red: #ff5c33;
|
||||
--red-light: #ffebe5;
|
||||
--shadow: 0px 12px 33px 0px rgba(0, 0, 0, 0.06), 0px 3.618px 9.949px 0px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
'Helvetica Neue',
|
||||
Arial,
|
||||
'Noto Sans',
|
||||
sans-serif,
|
||||
'Apple Color Emoji',
|
||||
'Segoe UI Emoji',
|
||||
'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
line-height: 1.5;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.tiptap {
|
||||
caret-color: var(--purple);
|
||||
margin: 1.5rem;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar styles */
|
||||
::-webkit-scrollbar {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-clip: padding-box;
|
||||
background-color: transparent;
|
||||
border: 4px solid transparent;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border: 4px solid rgba(0, 0, 0, 0);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Specific element and component styles */
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
background: var(--gray-2);
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
color: var(--black);
|
||||
font-family: inherit;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.15;
|
||||
margin: none;
|
||||
padding: 0.375rem 0.625rem;
|
||||
transition: all 0.2s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-3);
|
||||
color: var(--black-contrast);
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background: var(--gray-1);
|
||||
color: var(--gray-4);
|
||||
}
|
||||
|
||||
&:checked {
|
||||
accent-color: var(--purple);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: var(--black);
|
||||
color: var(--white);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--black-contrast);
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background: var(--gray-1);
|
||||
color: var(--gray-4);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--purple);
|
||||
color: var(--white);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--purple-contrast);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[popover] {
|
||||
background-color: var(--white);
|
||||
box-shadow: var(--shadow);
|
||||
max-height: 18rem;
|
||||
overflow-y: auto;
|
||||
scrollbar-color: var(--gray-4) transparent;
|
||||
}
|
||||
|
||||
[popover]::-webkit-scrollbar-thumb {
|
||||
background-color: var(--gray-4);
|
||||
}
|
||||
|
||||
/* Details */
|
||||
[data-type='details'] {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin: 1.5rem 0;
|
||||
border: 1px solid var(--gray-3);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
summary {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
> button {
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
font-size: 0.625rem;
|
||||
height: 1.25rem;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
margin-top: 0.1rem;
|
||||
padding: 0;
|
||||
width: 1.25rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-3);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '\25B6';
|
||||
}
|
||||
}
|
||||
|
||||
&.is-open > button::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
|
||||
> [data-type='detailsContent'] > :last-child {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-type='taskList'] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
[data-type='taskList'] {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
> li > div > p:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> li {
|
||||
&:not(:first-child) {
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-checked='true'] > div > p:first-child {
|
||||
text-decoration: line-through;
|
||||
color: var(--gray-4);
|
||||
}
|
||||
}
|
||||
|
||||
button:not([disabled]),
|
||||
select:not([disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
textarea {
|
||||
background-color: unset;
|
||||
border: 1px solid var(--gray-3);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--black);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--gray-4);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
border-color: var(--gray-4);
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
&:focus {
|
||||
border-color: var(--purple);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
/* reset */
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="Gray" d="M7 10l5 5 5-5z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.1rem center;
|
||||
background-size: 1.25rem 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.hint {
|
||||
align-items: center;
|
||||
background-color: var(--yellow-light);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--gray-2);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 0.75rem;
|
||||
gap: 0.25rem;
|
||||
line-height: 1.15;
|
||||
padding: 0.3rem 0.5rem;
|
||||
|
||||
&.purple-spinner,
|
||||
&.error {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: var(--gray-1);
|
||||
border: 1px solid var(--gray-3);
|
||||
border-radius: 2rem;
|
||||
color: var(--gray-5);
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
&.purple-spinner {
|
||||
background-color: var(--purple-light);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-image: url("data:image/svg+xml;utf8,<svg version='1.1' id='Layer_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='28px' height='30px' viewBox='0 0 24 30' style='enable-background:new 0 0 50 50;' xml:space='preserve'><rect x='0' y='10' width='6' height='10' fill='%236A00F5' rx='3' ry='3'><animateTransform attributeType='xml' attributeName='transform' type='translate' values='0 0; 0 5; 0 -5; 0 0' begin='0' dur='0.6s' repeatCount='indefinite'/></rect><rect x='10' y='10' width='6' height='10' fill='%236A00F5' rx='3' ry='3'><animateTransform attributeType='xml' attributeName='transform' type='translate' values='0 0; 0 5; 0 -5; 0 0' begin='0.2s' dur='0.6s' repeatCount='indefinite'/></rect><rect x='20' y='10' width='6' height='10' fill='%236A00F5' rx='3' ry='3'><animateTransform attributeType='xml' attributeName='transform' type='translate' values='0 0; 0 5; 0 -5; 0 0' begin='0.4s' dur='0.6s' repeatCount='indefinite'/></rect></svg>");
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: var(--red-light);
|
||||
}
|
||||
}
|
||||
|
||||
.label,
|
||||
.label-small,
|
||||
.label-large {
|
||||
color: var(--black);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.label-small {
|
||||
color: var(--gray-5);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.label-large {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-3);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: var(--gray-2);
|
||||
border: 1px solid var(--gray-2);
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.6rem;
|
||||
line-height: 1.15;
|
||||
padding: 0.1rem 0.25rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Layout and structure */
|
||||
#app,
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
align-items: flex-start;
|
||||
background-color: var(--white);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-node-view-wrapper] > .control-group {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.switch-group {
|
||||
align-items: center;
|
||||
background: var(--gray-2);
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
flex: 0 1 auto;
|
||||
justify-content: flex-start;
|
||||
padding: 0.125rem;
|
||||
|
||||
label {
|
||||
align-items: center;
|
||||
border-radius: 0.375rem;
|
||||
color: var(--gray-5);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
gap: 0.25rem;
|
||||
line-height: 1.15;
|
||||
min-height: 1.5rem;
|
||||
padding: 0 0.375rem;
|
||||
transition: all 0.2s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
|
||||
&:has(input:checked) {
|
||||
background-color: var(--white);
|
||||
color: var(--black-contrast);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
margin: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.output-group {
|
||||
background-color: var(--gray-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
font-size: 0.75rem;
|
||||
gap: 1rem;
|
||||
margin-top: 2.5rem;
|
||||
padding: 1.5rem;
|
||||
|
||||
label {
|
||||
color: var(--black);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
}
|
||||
}
|
||||
21
demos/setup/svelte.ts
Normal file
21
demos/setup/svelte.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'iframe-resizer/js/iframeResizer.contentWindow'
|
||||
import './style.scss'
|
||||
|
||||
import { mount } from 'svelte'
|
||||
import { debug, splitName } from './helper.js'
|
||||
|
||||
export default function init(name: string, source: any) {
|
||||
// @ts-ignore
|
||||
window.source = source
|
||||
document.title = name
|
||||
|
||||
const [demoCategory, demoName, frameworkName] = splitName(name)
|
||||
|
||||
import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.svelte`).then(Module => {
|
||||
const Component = Module.default
|
||||
|
||||
mount(Component, { target: document.querySelector('#app')! })
|
||||
|
||||
debug()
|
||||
})
|
||||
}
|
||||
24
demos/setup/vue.ts
Normal file
24
demos/setup/vue.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'iframe-resizer/js/iframeResizer.contentWindow'
|
||||
import './style.scss'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import { debug, splitName } from './helper.js'
|
||||
|
||||
export default function init(name: string, source: any) {
|
||||
// @ts-ignore
|
||||
window.source = source
|
||||
document.title = name
|
||||
|
||||
const [demoCategory, demoName, frameworkName] = splitName(name)
|
||||
|
||||
import(`../src/${demoCategory}/${demoName}/${frameworkName}/index.vue`).then(module => {
|
||||
const app = createApp(module.default)
|
||||
|
||||
if (typeof module.configureApp === 'function') {
|
||||
module.configureApp(app)
|
||||
}
|
||||
app.mount('#app')
|
||||
debug()
|
||||
})
|
||||
}
|
||||
0
demos/src/Commands/Cut/React/index.html
Normal file
0
demos/src/Commands/Cut/React/index.html
Normal file
81
demos/src/Commands/Cut/React/index.jsx
Normal file
81
demos/src/Commands/Cut/React/index.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { Color, TextStyle } from '@tiptap/extension-text-style'
|
||||
import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
const MenuBar = ({ editor }) => {
|
||||
const onCutToStart = useCallback(() => {
|
||||
editor
|
||||
.chain()
|
||||
.cut({ from: editor.state.selection.$from.pos, to: editor.state.selection.$to.pos }, 1)
|
||||
.run()
|
||||
}, [editor])
|
||||
|
||||
const onCutToEnd = useCallback(() => {
|
||||
editor
|
||||
.chain()
|
||||
.cut(
|
||||
{ from: editor.state.selection.$from.pos, to: editor.state.selection.$to.pos },
|
||||
editor.state.doc.nodeSize - 2,
|
||||
)
|
||||
.run()
|
||||
}, [editor])
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="control-group">
|
||||
<div className="button-group">
|
||||
<button onClick={onCutToStart}>Cut content to start of document</button>
|
||||
<button onClick={onCutToEnd}>Cut content to end of document</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const editor = useEditor({
|
||||
extensions: [Color, TextStyle, StarterKit],
|
||||
content: `
|
||||
<h2>
|
||||
Hi there,
|
||||
</h2>
|
||||
<p>
|
||||
this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
That’s a bullet list with one …
|
||||
</li>
|
||||
<li>
|
||||
… or two list items.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
|
||||
</p>
|
||||
<pre><code class="language-css">body {
|
||||
display: none;
|
||||
}</code></pre>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<blockquote>
|
||||
Wow, that’s amazing. Good work, boy! 👏
|
||||
<br />
|
||||
— Mom
|
||||
</blockquote>
|
||||
`,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuBar editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
91
demos/src/Commands/Cut/React/styles.scss
Normal file
91
demos/src/Commands/Cut/React/styles.scss
Normal file
@@ -0,0 +1,91 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
0
demos/src/Commands/InsertContent/React/index.html
Normal file
0
demos/src/Commands/InsertContent/React/index.html
Normal file
80
demos/src/Commands/InsertContent/React/index.jsx
Normal file
80
demos/src/Commands/InsertContent/React/index.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import { Color, TextStyle } from '@tiptap/extension-text-style'
|
||||
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
const htmlContent = `
|
||||
<h1><a href="https://tiptap.dev/">Tiptap</a></h1>
|
||||
<p><strong>Hello World</strong></p>
|
||||
<p>This is a paragraph<br />with a break.</p>
|
||||
<p>And this is some additional string content.</p>
|
||||
`
|
||||
|
||||
const textContent = `Hello World
|
||||
This is content with a new line. Is this working?
|
||||
|
||||
Lets see if multiple new lines are inserted correctly
|
||||
|
||||
And more lines`
|
||||
|
||||
const MenuBar = () => {
|
||||
const { editor } = useCurrentEditor()
|
||||
|
||||
const insertImage = useCallback(() => {
|
||||
const url = prompt('Enter an image URL')
|
||||
|
||||
editor.chain().insertContent(`<img src="${url}" alt="Example image" />`).focus().run()
|
||||
}, [editor])
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="control-group">
|
||||
<div className="button-group">
|
||||
<button
|
||||
data-test-id="html-content"
|
||||
onClick={() => editor.chain().insertContent(htmlContent).focus().run()}
|
||||
>
|
||||
Insert HTML content
|
||||
</button>
|
||||
<button
|
||||
data-test-id="html-content-spans"
|
||||
onClick={() =>
|
||||
editor.chain().insertContent('<p><b>Hello</b> <i>World</i></p>').focus().run()
|
||||
}
|
||||
>
|
||||
Insert HTML with span tags content
|
||||
</button>
|
||||
<button
|
||||
data-test-id="text-content"
|
||||
onClick={() => editor.chain().insertContent(textContent).focus().run()}
|
||||
>
|
||||
Insert text content
|
||||
</button>
|
||||
<button data-test-id="image-content" onClick={insertImage}>
|
||||
Insert image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const extensions = [Image, Color, TextStyle, Link, StarterKit]
|
||||
|
||||
const content = ''
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<EditorProvider
|
||||
slotBefore={<MenuBar />}
|
||||
extensions={extensions}
|
||||
content={content}
|
||||
></EditorProvider>
|
||||
)
|
||||
}
|
||||
91
demos/src/Commands/InsertContent/React/styles.scss
Normal file
91
demos/src/Commands/InsertContent/React/styles.scss
Normal file
@@ -0,0 +1,91 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
178
demos/src/Commands/InsertContentApplyingRules/React/index.jsx
Normal file
178
demos/src/Commands/InsertContentApplyingRules/React/index.jsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const MenuBar = () => {
|
||||
const { editor } = useCurrentEditor()
|
||||
const [useInputRules, setUseInputRules] = useState(true)
|
||||
const [usePasteRules, setUsePasteRules] = useState(false)
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="control-group">
|
||||
<div>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useInputRules}
|
||||
onChange={() => setUseInputRules(prev => !prev)}
|
||||
/>
|
||||
Apply input rules
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={usePasteRules}
|
||||
onChange={() => setUsePasteRules(prev => !prev)}
|
||||
/>
|
||||
Apply paste rules
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="button-group">
|
||||
<button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.insertContent('-', {
|
||||
applyInputRules: useInputRules,
|
||||
applyPasteRules: usePasteRules,
|
||||
})
|
||||
.focus()
|
||||
.run()
|
||||
}}
|
||||
data-test-1
|
||||
>
|
||||
Insert "-"
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.insertContent(' ', {
|
||||
applyInputRules: useInputRules,
|
||||
applyPasteRules: usePasteRules,
|
||||
})
|
||||
.focus()
|
||||
.run()
|
||||
}}
|
||||
data-test-2
|
||||
>
|
||||
Insert " "
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.insertContent('A', {
|
||||
applyInputRules: useInputRules,
|
||||
applyPasteRules: usePasteRules,
|
||||
})
|
||||
.focus()
|
||||
.run()
|
||||
}}
|
||||
data-test-3
|
||||
>
|
||||
Insert "A"
|
||||
</button>
|
||||
|
||||
<br />
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.insertContent('*this is', {
|
||||
applyInputRules: useInputRules,
|
||||
applyPasteRules: usePasteRules,
|
||||
})
|
||||
.focus()
|
||||
.run()
|
||||
}}
|
||||
data-test-4
|
||||
>
|
||||
Insert "*this is"
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.insertContent(' a test*', {
|
||||
applyInputRules: useInputRules,
|
||||
applyPasteRules: usePasteRules,
|
||||
})
|
||||
.focus()
|
||||
.run()
|
||||
}}
|
||||
data-test-5
|
||||
>
|
||||
Insert " a test*"
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.insertContent(' a test*, whooho', {
|
||||
applyInputRules: useInputRules,
|
||||
applyPasteRules: usePasteRules,
|
||||
})
|
||||
.focus()
|
||||
.run()
|
||||
}}
|
||||
data-test-6
|
||||
>
|
||||
Insert " a test*, whooho."
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.insertContent('*this is a test*, whooho.', {
|
||||
applyInputRules: useInputRules,
|
||||
applyPasteRules: usePasteRules,
|
||||
})
|
||||
.focus()
|
||||
.run()
|
||||
}}
|
||||
data-test-7
|
||||
>
|
||||
Insert "*this is a test*, whooho."
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor
|
||||
.chain()
|
||||
.insertContent('*this is a test*', {
|
||||
applyInputRules: useInputRules,
|
||||
applyPasteRules: usePasteRules,
|
||||
})
|
||||
.focus()
|
||||
.run()
|
||||
}}
|
||||
data-test-8
|
||||
>
|
||||
Insert "*this is a test*"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const extensions = [StarterKit]
|
||||
|
||||
const content = ''
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<EditorProvider
|
||||
slotBefore={<MenuBar />}
|
||||
extensions={extensions}
|
||||
content={content}
|
||||
></EditorProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
0
demos/src/Commands/SetContent/React/index.html
Normal file
0
demos/src/Commands/SetContent/React/index.html
Normal file
15
demos/src/Commands/SetContent/React/index.jsx
Normal file
15
demos/src/Commands/SetContent/React/index.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import './styles.scss'
|
||||
|
||||
import Mentions from '@tiptap/extension-mention'
|
||||
import { Color, TextStyle } from '@tiptap/extension-text-style'
|
||||
import { EditorProvider } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React from 'react'
|
||||
|
||||
const extensions = [Color, TextStyle, StarterKit, Mentions]
|
||||
|
||||
const content = ''
|
||||
|
||||
export default () => {
|
||||
return <EditorProvider extensions={extensions} content={content}></EditorProvider>
|
||||
}
|
||||
56
demos/src/Commands/SetContent/React/styles.scss
Normal file
56
demos/src/Commands/SetContent/React/styles.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
233
demos/src/Demos/CollaborationSplitPane/React/Editor.jsx
Normal file
233
demos/src/Demos/CollaborationSplitPane/React/Editor.jsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import { TaskItem, TaskList } from '@tiptap/extension-list'
|
||||
import { CharacterCount } from '@tiptap/extensions'
|
||||
import { EditorContent, useEditor, useEditorState } 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 = `
|
||||
<p>Hi 👋, this is a collaborative document.</p>
|
||||
<p>Feel free to edit and collaborate in real-time!</p>
|
||||
`
|
||||
|
||||
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 [userCount, setUserCount] = useState(0)
|
||||
|
||||
const editor = useEditor({
|
||||
enableContentCheck: true,
|
||||
onContentError: ({ disableCollaboration }) => {
|
||||
disableCollaboration()
|
||||
},
|
||||
onCreate: ({ editor: currentEditor }) => {
|
||||
provider.on('synced', () => {
|
||||
if (currentEditor.isEmpty) {
|
||||
currentEditor.commands.setContent(defaultContent)
|
||||
}
|
||||
})
|
||||
},
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
undoRedo: false,
|
||||
}),
|
||||
Highlight,
|
||||
TaskList,
|
||||
TaskItem,
|
||||
CharacterCount.extend().configure({
|
||||
limit: 10000,
|
||||
}),
|
||||
Collaboration.extend().configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCaret.extend().configure({
|
||||
provider,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
const { isBold, isItalic, isStrike, isBulletList, isCode } = useEditorState({
|
||||
editor,
|
||||
selector: ctx => ({
|
||||
isBold: ctx.editor.isActive('bold') ?? false,
|
||||
isItalic: ctx.editor.isActive('italic') ?? false,
|
||||
isStrike: ctx.editor.isActive('strike') ?? false,
|
||||
isBulletList: ctx.editor.isActive('bulletList') ?? false,
|
||||
isCode: ctx.editor.isActive('code') ?? false,
|
||||
}),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Update status changes
|
||||
const statusHandler = event => {
|
||||
setStatus(event.status)
|
||||
}
|
||||
|
||||
provider.on('status', statusHandler)
|
||||
|
||||
return () => {
|
||||
provider.off('status', statusHandler)
|
||||
}
|
||||
}, [provider])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const updateUserCount = () => {
|
||||
setUserCount(editor.storage.collaborationCaret.users.length)
|
||||
}
|
||||
|
||||
updateUserCount()
|
||||
provider.awareness.on('update', updateUserCount)
|
||||
|
||||
return () => {
|
||||
provider.awareness.off('update', updateUserCount)
|
||||
}
|
||||
}, [editor, 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 (
|
||||
<div className="column-half">
|
||||
<div className="control-group">
|
||||
<div className="button-group">
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
className={isBold ? 'is-active' : ''}
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
className={isItalic ? 'is-active' : ''}
|
||||
>
|
||||
Italic
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
className={isStrike ? 'is-active' : ''}
|
||||
>
|
||||
Strike
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
className={isBulletList ? 'is-active' : ''}
|
||||
>
|
||||
Bullet list
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
className={isCode ? 'is-active' : ''}
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditorContent editor={editor} className="main-group" />
|
||||
|
||||
<div
|
||||
className="collab-status-group"
|
||||
data-state={status === 'connected' ? 'online' : 'offline'}
|
||||
>
|
||||
<label>
|
||||
{status === 'connected'
|
||||
? `${userCount} user${userCount === 1 ? '' : 's'} online in ${room}`
|
||||
: 'offline'}
|
||||
</label>
|
||||
<button style={{ '--color': currentUser.color }} onClick={setName}>
|
||||
✎ {currentUser.name}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Editor
|
||||
39
demos/src/Demos/CollaborationSplitPane/React/index.jsx
Normal file
39
demos/src/Demos/CollaborationSplitPane/React/index.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { TiptapCollabProvider } from '@hocuspocus/provider'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
import Editor from './Editor.jsx'
|
||||
|
||||
const appId = '7j9y6m10'
|
||||
const room = `room.${new Date()
|
||||
.getFullYear()
|
||||
.toString()
|
||||
.slice(-2)}${new Date().getMonth() + 1}${new Date().getDate()}-ok`
|
||||
|
||||
// ydoc and provider for Editor A
|
||||
const ydocA = new Y.Doc()
|
||||
const providerA = new TiptapCollabProvider({
|
||||
appId,
|
||||
name: room,
|
||||
document: ydocA,
|
||||
})
|
||||
|
||||
// ydoc and provider for Editor B
|
||||
const ydocB = new Y.Doc()
|
||||
const providerB = new TiptapCollabProvider({
|
||||
appId,
|
||||
name: room,
|
||||
document: ydocB,
|
||||
})
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<div className="col-group">
|
||||
<Editor provider={providerA} ydoc={ydocA} room={room} />
|
||||
<Editor provider={providerB} ydoc={ydocB} room={room} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
280
demos/src/Demos/CollaborationSplitPane/React/styles.scss
Normal file
280
demos/src/Demos/CollaborationSplitPane/React/styles.scss
Normal file
@@ -0,0 +1,280 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Highlight specific styles */
|
||||
mark {
|
||||
background-color: #faf594;
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
|
||||
/* Task list specific styles */
|
||||
ul[data-type='taskList'] {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Give a remote user a caret */
|
||||
.collaboration-carets__caret {
|
||||
border-left: 1px solid #0d0d0d;
|
||||
border-right: 1px solid #0d0d0d;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
/* Render the username above the caret */
|
||||
.collaboration-carets__label {
|
||||
border-radius: 3px 3px 3px 0;
|
||||
color: #0d0d0d;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
left: -1px;
|
||||
line-height: normal;
|
||||
padding: 0.1rem 0.3rem;
|
||||
position: absolute;
|
||||
top: -1.4em;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.col-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100vh;
|
||||
|
||||
@media (max-width: 540px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* Column-half */
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.column-half {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
&:last-child {
|
||||
border-left: 1px solid var(--gray-3);
|
||||
|
||||
@media (max-width: 540px) {
|
||||
border-left: none;
|
||||
border-top: 1px solid var(--gray-3);
|
||||
}
|
||||
}
|
||||
|
||||
& > .main-group {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collaboration status */
|
||||
.collab-status-group {
|
||||
align-items: center;
|
||||
background-color: var(--white);
|
||||
border-top: 1px solid var(--gray-3);
|
||||
bottom: 0;
|
||||
color: var(--gray-5);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
padding: 0.375rem 0.5rem 0.375rem 1rem;
|
||||
position: sticky;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
|
||||
button {
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
align-self: stretch;
|
||||
background: none;
|
||||
display: -webkit-box;
|
||||
flex-shrink: 1;
|
||||
font-size: 0.75rem;
|
||||
max-width: 100%;
|
||||
padding: 0.25rem 0.375rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
background-color: var(--color);
|
||||
border-radius: 0.375rem;
|
||||
content: '';
|
||||
height: 100%;
|
||||
left: 0;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: all 0.2s cubic-bezier(0.65, 0.05, 0.36, 1);
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
gap: 0.375rem;
|
||||
line-height: 1.1;
|
||||
|
||||
&::before {
|
||||
border-radius: 50%;
|
||||
content: ' ';
|
||||
height: 0.35rem;
|
||||
width: 0.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-state='online'] {
|
||||
label {
|
||||
&::before {
|
||||
background-color: var(--green);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-state='offline'] {
|
||||
label {
|
||||
&::before {
|
||||
background-color: var(--red);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
demos/src/Demos/CollaborationSplitPane/index.spec.ts
Normal file
41
demos/src/Demos/CollaborationSplitPane/index.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
const demoName = 'CollaborationSplitPane'
|
||||
const frameworkPaths = ['React']
|
||||
const demoPath = '/src/Demos'
|
||||
|
||||
test.describe(`${demoPath}/${demoName}`, () => {
|
||||
frameworkPaths.forEach(frameworkPath => {
|
||||
const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/`
|
||||
|
||||
test.describe(`${frameworkPath}`, () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(fullDemoPath)
|
||||
})
|
||||
|
||||
test('should have a working tiptap instance', async ({ page }) => {
|
||||
const editor = page.locator('.tiptap').first()
|
||||
await editor.waitFor()
|
||||
|
||||
const hasEditor = await editor.evaluate((el: any) => !!el.editor)
|
||||
|
||||
expect(hasEditor).toBe(true)
|
||||
})
|
||||
|
||||
test('should have a ydoc', async ({ page }) => {
|
||||
const editor = page.locator('.tiptap').first()
|
||||
await editor.waitFor()
|
||||
|
||||
const hasYDoc = await editor.evaluate((el: any) => {
|
||||
const extension = el.editor.extensionManager.extensions.find(
|
||||
(a: any) => a.name === 'collaboration',
|
||||
)
|
||||
|
||||
return !!extension?.options.document
|
||||
})
|
||||
|
||||
expect(hasYDoc).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
0
demos/src/Demos/SingleRoomCollab/React/index.html
Normal file
0
demos/src/Demos/SingleRoomCollab/React/index.html
Normal file
147
demos/src/Demos/SingleRoomCollab/React/index.jsx
Normal file
147
demos/src/Demos/SingleRoomCollab/React/index.jsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { TiptapCollabProvider } from '@hocuspocus/provider'
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
|
||||
import { CharacterCount } from '@tiptap/extensions'
|
||||
import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
const room = 'room-1'
|
||||
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']
|
||||
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 getRandomElement = list => list[Math.floor(Math.random() * list.length)]
|
||||
|
||||
const getRandomColor = () => getRandomElement(colors)
|
||||
const getRandomName = () => getRandomElement(names)
|
||||
|
||||
const ydoc = new Y.Doc()
|
||||
const websocketProvider = new TiptapCollabProvider({
|
||||
appId: '7j9y6m10',
|
||||
name: room,
|
||||
document: ydoc,
|
||||
})
|
||||
|
||||
const getInitialUser = () => {
|
||||
return (
|
||||
JSON.parse(localStorage.getItem('currentUser')) || {
|
||||
name: getRandomName(),
|
||||
color: getRandomColor(),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [status, setStatus] = useState('connecting')
|
||||
const [currentUser, setCurrentUser] = useState(getInitialUser)
|
||||
const [userCount, setUserCount] = useState(0)
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
undoRedo: false,
|
||||
}),
|
||||
CharacterCount.configure({
|
||||
limit: 10000,
|
||||
}),
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCaret.configure({
|
||||
provider: websocketProvider,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Update status changes
|
||||
websocketProvider.on('status', event => {
|
||||
setStatus(event.status)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const updateUserCount = () => {
|
||||
setUserCount(editor.storage.collaborationCaret.users.length)
|
||||
}
|
||||
|
||||
updateUserCount()
|
||||
websocketProvider.awareness.on('update', updateUserCount)
|
||||
|
||||
return () => {
|
||||
websocketProvider.awareness.off('update', updateUserCount)
|
||||
}
|
||||
}, [editor])
|
||||
|
||||
// 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') || '').trim().substring(0, 32)
|
||||
|
||||
if (name) {
|
||||
return setCurrentUser({ ...currentUser, name })
|
||||
}
|
||||
}, [currentUser])
|
||||
|
||||
return (
|
||||
<div className="editor">
|
||||
<div className="editor__header">
|
||||
<div className="dots">
|
||||
<span className="dot"></span>
|
||||
<span className="dot"></span>
|
||||
<span className="dot"></span>
|
||||
</div>
|
||||
<div className="editor__users">
|
||||
<div className={`editor__status editor__status--${status}`}>
|
||||
{status === 'connected'
|
||||
? `${userCount} user${userCount === 1 ? '' : 's'} online in ${room}`
|
||||
: 'offline'}
|
||||
</div>
|
||||
<div className="editor__name">
|
||||
<button onClick={setName}>{currentUser.name}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EditorContent className="editor__content" editor={editor} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
199
demos/src/Demos/SingleRoomCollab/React/styles.scss
Normal file
199
demos/src/Demos/SingleRoomCollab/React/styles.scss
Normal file
@@ -0,0 +1,199 @@
|
||||
/* 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;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #faf594;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid rgba(#0d0d0d, 0.1);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid rgba(#0d0d0d, 0.1);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
background-color: #fff;
|
||||
border: 2px solid #0d0d0d;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 5px 5px #000;
|
||||
color: #0d0d0d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 26rem;
|
||||
|
||||
&__header {
|
||||
align-items: center;
|
||||
border-bottom: 2px solid #0d0d0d;
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-top-right-radius: 0.25rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
&__users {
|
||||
color: rgba(#000, 0.8);
|
||||
display: flex;
|
||||
font-size: 0.85rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem 1rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Some information about the status */
|
||||
&__status {
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
|
||||
&::before {
|
||||
background: rgba(#0d0d0d, 0.5);
|
||||
border-radius: 50%;
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
height: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
&--connecting::before {
|
||||
background: #616161;
|
||||
}
|
||||
|
||||
&--connected::before {
|
||||
background: #b9f18d;
|
||||
}
|
||||
}
|
||||
|
||||
&__name button {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Give a remote user a caret */
|
||||
.collaboration-carets__caret {
|
||||
border-left: 1px solid #0d0d0d;
|
||||
border-right: 1px solid #0d0d0d;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
/* Render the username above the caret */
|
||||
.collaboration-carets__label {
|
||||
border-radius: 3px 3px 3px 0;
|
||||
color: #0d0d0d;
|
||||
font-size: 0.75rem;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
left: -1px;
|
||||
line-height: normal;
|
||||
padding: 0.1rem 0.3rem;
|
||||
position: absolute;
|
||||
top: -1.4em;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
background: #000;
|
||||
border-radius: 100%;
|
||||
height: 0.625rem;
|
||||
width: 0.625rem;
|
||||
}
|
||||
26
demos/src/Demos/SingleRoomCollab/index.spec.ts
Normal file
26
demos/src/Demos/SingleRoomCollab/index.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { getEditor } from '../../../test/helpers.js'
|
||||
|
||||
const demoName = 'SingleRoomCollab'
|
||||
const frameworkPaths = ['React']
|
||||
const demoPath = '/src/Demos'
|
||||
|
||||
test.describe(`${demoPath}/${demoName}`, () => {
|
||||
frameworkPaths.forEach(frameworkPath => {
|
||||
const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/`
|
||||
|
||||
test.describe(`${frameworkPath}`, () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(fullDemoPath)
|
||||
})
|
||||
|
||||
test('should have a working tiptap instance', async ({ page }) => {
|
||||
const editor = await getEditor(page)
|
||||
const hasEditor = await editor.evaluate((el: any) => !!el.editor)
|
||||
|
||||
expect(hasEditor).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
0
demos/src/Dev/.gitkeep
Normal file
0
demos/src/Dev/.gitkeep
Normal file
89
demos/src/Examples/Accessibility/React/InsertMenu.tsx
Normal file
89
demos/src/Examples/Accessibility/React/InsertMenu.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import type { Editor } from '@tiptap/react'
|
||||
import { useEditorState } from '@tiptap/react'
|
||||
import { FloatingMenu } from '@tiptap/react/menus'
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
import { useMenubarNav } from './useMenubarNav.js'
|
||||
|
||||
/**
|
||||
* A floating menu for inserting new elements like lists, horizontal rules, etc.
|
||||
*/
|
||||
export function InsertMenu({ editor }: { editor: Editor }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { activeNodeType } = useEditorState({
|
||||
editor,
|
||||
selector: ctx => {
|
||||
const activeNode = ctx.editor.state.selection.$from.node(1)
|
||||
|
||||
return {
|
||||
activeNodeType: activeNode?.type.name ?? 'paragraph',
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Handle arrow navigation within a menu bar container, and allow to escape to the editor
|
||||
const { getFocusableElements } = useMenubarNav({
|
||||
editor,
|
||||
ref: containerRef,
|
||||
onEscape: e => {
|
||||
e.preventDefault()
|
||||
// On escape, focus the editor
|
||||
editor.chain().focus().run()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<FloatingMenu
|
||||
editor={editor}
|
||||
shouldShow={null}
|
||||
aria-orientation="horizontal"
|
||||
role="menubar"
|
||||
aria-label="Insert Element menu"
|
||||
className="floating-menu"
|
||||
// Types are broken here, since we import jsx from vue-2
|
||||
ref={containerRef as any}
|
||||
onFocus={e => {
|
||||
// The ref we have is to the container, not the menu itself
|
||||
if (containerRef.current === e.target?.parentNode) {
|
||||
// Focus the first enabled button-like when the menu bar is focused
|
||||
getFocusableElements()?.[0]?.focus()
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
className={activeNodeType === 'bulletList' ? 'is-active' : ''}
|
||||
aria-pressed={activeNodeType === 'bulletList'}
|
||||
aria-label="Bullet List"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Bullet list
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
className={activeNodeType === 'orderedList' ? 'is-active' : ''}
|
||||
aria-pressed={activeNodeType === 'orderedList'}
|
||||
aria-label="Ordered List"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Ordered List
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||
aria-label="Horizontal rule"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Horizontal rule
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().setHardBreak().run()}
|
||||
aria-label="Hard break"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Hard break
|
||||
</button>
|
||||
</FloatingMenu>
|
||||
)
|
||||
}
|
||||
317
demos/src/Examples/Accessibility/React/MenuBar.tsx
Normal file
317
demos/src/Examples/Accessibility/React/MenuBar.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import { useEditorState } from '@tiptap/react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { useMenubarNav } from './useMenubarNav.js'
|
||||
|
||||
/**
|
||||
* Handles the heading dropdown
|
||||
*/
|
||||
function NodeTypeDropdown({ editor }: { editor: Editor }) {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const menuRef = useRef<HTMLDivElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: ctx => {
|
||||
const activeNode = ctx.editor.state.selection.$from.node(1)
|
||||
|
||||
return {
|
||||
activeNodeType: activeNode?.type.name ?? 'paragraph',
|
||||
isParagraph: ctx.editor.isActive('paragraph'),
|
||||
isHeading1: ctx.editor.isActive('heading', { level: 1 }),
|
||||
isHeading2: ctx.editor.isActive('heading', { level: 2 }),
|
||||
isHeading3: ctx.editor.isActive('heading', { level: 3 }),
|
||||
isHeading4: ctx.editor.isActive('heading', { level: 4 }),
|
||||
isHeading5: ctx.editor.isActive('heading', { level: 5 }),
|
||||
isHeading6: ctx.editor.isActive('heading', { level: 6 }),
|
||||
isBulletList: ctx.editor.isActive('bulletList'),
|
||||
isOrderedList: ctx.editor.isActive('orderedList'),
|
||||
isCodeBlock: ctx.editor.isActive('codeBlock'),
|
||||
isBlockquote: ctx.editor.isActive('blockquote'),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="node-type-dropdown__container"
|
||||
ref={menuRef}
|
||||
onKeyDown={e => {
|
||||
// Escape or tab should close the dropdown if it's open
|
||||
if (isOpen && (e.key === 'Escape' || e.key === 'Tab')) {
|
||||
setIsOpen(false)
|
||||
if (e.key === 'Escape') {
|
||||
// Prevent the editor from handling the escape key
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={e => {
|
||||
setIsOpen(open => !open)
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
setIsOpen(open => !open)
|
||||
}
|
||||
}}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={isOpen}
|
||||
aria-controls="heading-dropdown"
|
||||
className={`node-type-dropdown__trigger${isOpen ? ' is-active' : ''}`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Node Type:{' '}
|
||||
{editorState.activeNodeType.slice(0, 1).toUpperCase() + editorState.activeNodeType.slice(1)}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
id="node-type-dropdown"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
className="node-type-dropdown__items"
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().setParagraph().run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isParagraph ? 'is-active' : ''}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="Paragraph"
|
||||
aria-pressed={editorState.isParagraph}
|
||||
// This is the first element in the dropdown, so if it loses focus, close the dropdown
|
||||
onBlur={e => {
|
||||
// Is it not within the dropdown?
|
||||
if (!dropdownRef.current?.contains(e.relatedTarget as Node)) {
|
||||
e.preventDefault()
|
||||
setIsOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Paragraph
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isHeading1 ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isHeading1}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="H1"
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isHeading2 ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isHeading2}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="H2"
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isHeading3 ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isHeading3}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="H3"
|
||||
>
|
||||
H3
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleHeading({ level: 4 }).run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isHeading4 ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isHeading4}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="H4"
|
||||
>
|
||||
H4
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleHeading({ level: 5 }).run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isHeading5 ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isHeading5}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="H5"
|
||||
>
|
||||
H5
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleHeading({ level: 6 }).run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isHeading6 ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isHeading6}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="H6"
|
||||
>
|
||||
H6
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleBulletList().run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isBulletList ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isBulletList}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="Bullet list"
|
||||
>
|
||||
Bullet list
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleOrderedList().run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isOrderedList ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isOrderedList}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="Ordered List"
|
||||
>
|
||||
Ordered list
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleCodeBlock().run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isCodeBlock ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isCodeBlock}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="Code block"
|
||||
>
|
||||
Code block
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().toggleBlockquote().run()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={editorState.isBlockquote ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isBlockquote}
|
||||
tabIndex={-1}
|
||||
role="menuitem"
|
||||
aria-label="Blockquote"
|
||||
onBlur={e => {
|
||||
if (!dropdownRef.current?.contains(e.relatedTarget as Node)) {
|
||||
e.preventDefault()
|
||||
setIsOpen(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
Blockquote
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* An accessible static top menu bar for the editor
|
||||
*/
|
||||
export function MenuBar({ editor }: { editor: Editor }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: ctx => {
|
||||
return {
|
||||
canUndo: ctx.editor.can().chain().focus().undo().run(),
|
||||
canRedo: ctx.editor.can().chain().focus().redo().run(),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
useMenubarNav({
|
||||
ref: containerRef,
|
||||
editor,
|
||||
onKeydown: event => {
|
||||
// Handle focus on alt + f10
|
||||
if (event.altKey && event.key === 'F10') {
|
||||
event.preventDefault()
|
||||
containerRef.current?.querySelector('button')?.focus()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="control-group"
|
||||
role="toolbar"
|
||||
aria-orientation="horizontal"
|
||||
aria-keyshortcuts="Alt+F10"
|
||||
ref={containerRef}
|
||||
>
|
||||
<div className="button-group">
|
||||
<NodeTypeDropdown editor={editor} />
|
||||
<button
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
disabled={!editorState.canUndo}
|
||||
tabIndex={-1}
|
||||
aria-label="Undo"
|
||||
>
|
||||
Undo
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
disabled={!editorState.canRedo}
|
||||
tabIndex={-1}
|
||||
aria-label="Redo"
|
||||
>
|
||||
Redo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
118
demos/src/Examples/Accessibility/React/TextMenu.tsx
Normal file
118
demos/src/Examples/Accessibility/React/TextMenu.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Selection } from '@tiptap/pm/state'
|
||||
import type { Editor } from '@tiptap/react'
|
||||
import { useEditorState } from '@tiptap/react'
|
||||
import { BubbleMenu } from '@tiptap/react/menus'
|
||||
import React, { useRef } from 'react'
|
||||
|
||||
import { useMenubarNav } from './useMenubarNav.js'
|
||||
|
||||
/**
|
||||
* Handles formatting text with marks like bold, italic, etc.
|
||||
*/
|
||||
export function TextMenu({ editor }: { editor: Editor }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: ctx => {
|
||||
return {
|
||||
isBold: ctx.editor.isActive('bold'),
|
||||
canBold: ctx.editor.can().chain().focus().toggleBold().run(),
|
||||
isItalic: ctx.editor.isActive('italic'),
|
||||
canItalic: ctx.editor.can().chain().focus().toggleItalic().run(),
|
||||
isStrike: ctx.editor.isActive('strike'),
|
||||
canStrike: ctx.editor.can().chain().focus().toggleStrike().run(),
|
||||
isCode: ctx.editor.isActive('code'),
|
||||
canCode: ctx.editor.can().chain().focus().toggleCode().run(),
|
||||
canClearMarks: ctx.editor.can().chain().focus().unsetAllMarks().run(),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Handle arrow navigation within a menu bar container, and allow to escape to the editor
|
||||
const { getFocusableElements } = useMenubarNav({
|
||||
editor,
|
||||
ref: containerRef,
|
||||
onEscape: e => {
|
||||
e.preventDefault()
|
||||
// On escape, focus the editor & dismiss the menu by moving the selection to the end of the selection
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.command(({ tr }) => {
|
||||
tr.setSelection(Selection.near(tr.selection.$to))
|
||||
return true
|
||||
})
|
||||
.run()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<BubbleMenu
|
||||
editor={editor}
|
||||
shouldShow={null}
|
||||
aria-label="Text formatting menu"
|
||||
aria-orientation="horizontal"
|
||||
role="menubar"
|
||||
className="bubble-menu"
|
||||
// Types are broken here, since we import jsx from vue-2
|
||||
ref={containerRef as any}
|
||||
onFocus={e => {
|
||||
// The ref we have is to the container, not the menu itself
|
||||
if (containerRef.current === e.target?.parentNode) {
|
||||
// Focus the first button when the menu bar is focused
|
||||
getFocusableElements()?.[0]?.focus()
|
||||
}
|
||||
}}
|
||||
tabIndex={0}
|
||||
>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
disabled={!editorState.canBold}
|
||||
className={editorState.isBold ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isBold}
|
||||
aria-label="Bold"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
disabled={!editorState.canItalic}
|
||||
className={editorState.isItalic ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isItalic}
|
||||
aria-label="Italic"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Italic
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
disabled={!editorState.canStrike}
|
||||
className={editorState.isStrike ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isStrike}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Strike
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
disabled={!editorState.canCode}
|
||||
className={editorState.isCode ? 'is-active' : ''}
|
||||
aria-pressed={editorState.isCode}
|
||||
aria-label="Code"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button
|
||||
disabled={!editorState.canClearMarks}
|
||||
onClick={() => editor.chain().focus().unsetAllMarks().run()}
|
||||
aria-label="Clear Marks"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Clear marks
|
||||
</button>
|
||||
</BubbleMenu>
|
||||
)
|
||||
}
|
||||
0
demos/src/Examples/Accessibility/React/index.html
Normal file
0
demos/src/Examples/Accessibility/React/index.html
Normal file
42
demos/src/Examples/Accessibility/React/index.tsx
Normal file
42
demos/src/Examples/Accessibility/React/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React from 'react'
|
||||
|
||||
import { InsertMenu } from './InsertMenu.jsx'
|
||||
import { MenuBar } from './MenuBar.jsx'
|
||||
import { TextMenu } from './TextMenu.jsx'
|
||||
|
||||
export default () => {
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
content: `
|
||||
<h2>Accessibility Demo</h2>
|
||||
<p>Tab into the demo & navigate around with only your keyboard</p>
|
||||
<p>Use <code>Alt + F10</code> to focus the menu bar</p>
|
||||
`,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
// Make sure the editor is announced as a rich text editor
|
||||
'aria-label': 'Rich Text Editor',
|
||||
// editor accepts multiline input
|
||||
'aria-multiline': 'true',
|
||||
'aria-readonly': 'false',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div role="application" className="editor-application">
|
||||
{/* The static menu bar */}
|
||||
<MenuBar editor={editor} />
|
||||
{/* The editor content */}
|
||||
<EditorContent editor={editor} />
|
||||
{/* Handles marks: bold, italic, etc. using a bubble menu */}
|
||||
<TextMenu editor={editor} />
|
||||
{/* Handles nodes: headings, lists, etc. using a floating menu */}
|
||||
<InsertMenu editor={editor} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
219
demos/src/Examples/Accessibility/React/styles.scss
Normal file
219
demos/src/Examples/Accessibility/React/styles.scss
Normal file
@@ -0,0 +1,219 @@
|
||||
.tiptap {
|
||||
/**
|
||||
* This fixes a bug with voiceover where it speaks text that runs from one element to another as one word like so:
|
||||
*
|
||||
* <h1>Heading</h1>
|
||||
* <p>Paragraph</p>
|
||||
*
|
||||
* Voiceover would read this as "HeadingParagraph" instead of "Heading Paragraph"
|
||||
* So, we add a zero-width space after each block element to fix this.
|
||||
*/
|
||||
h1::after,
|
||||
h2::after,
|
||||
h3::after,
|
||||
h4::after,
|
||||
h5::after,
|
||||
h6::after,
|
||||
p::after {
|
||||
content: '\200B';
|
||||
}
|
||||
}
|
||||
|
||||
.node-type-dropdown__container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.node-type-dropdown__items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0;
|
||||
background-color: var(--white);
|
||||
z-index: 1000;
|
||||
|
||||
button {
|
||||
background-color: unset;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-3);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--purple);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--purple-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Bubble menu styles */
|
||||
.bubble-menu {
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--gray-1);
|
||||
border-radius: 0.7rem;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
padding: 0.2rem;
|
||||
|
||||
button {
|
||||
background-color: unset;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-3);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--purple);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--purple-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Floating menu styles */
|
||||
.floating-menu {
|
||||
display: flex;
|
||||
background-color: var(--gray-3);
|
||||
padding: 0.1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
button {
|
||||
background-color: unset;
|
||||
padding: 0.275rem 0.425rem;
|
||||
border-radius: 0.3rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-3);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: var(--white);
|
||||
color: var(--purple);
|
||||
|
||||
&:hover {
|
||||
color: var(--purple-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
&:focus {
|
||||
outline: 1px solid var(--purple);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Link styles */
|
||||
a {
|
||||
color: var(--purple);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--purple-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-empty::before {
|
||||
color: var(--gray-4);
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
127
demos/src/Examples/Accessibility/React/useMenubarNav.ts
Normal file
127
demos/src/Examples/Accessibility/React/useMenubarNav.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import type React from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Handle arrow navigation within a menu bar container, and allow to escape to the editor
|
||||
*/
|
||||
export function useMenubarNav({
|
||||
ref: containerRef,
|
||||
editor,
|
||||
onEscape = (e, ctx) => {
|
||||
e.preventDefault()
|
||||
ctx.editor.commands.focus()
|
||||
},
|
||||
onKeydown = () => {
|
||||
// Do nothing
|
||||
},
|
||||
}: {
|
||||
/**
|
||||
* Ref to the menu bar container
|
||||
*/
|
||||
ref: React.RefObject<HTMLElement>
|
||||
/**
|
||||
* The editor instance
|
||||
*/
|
||||
editor: Editor
|
||||
/**
|
||||
* Callback when the user presses the escape key
|
||||
*/
|
||||
onEscape?: (
|
||||
event: KeyboardEvent,
|
||||
ctx: { editor: Editor; ref: React.RefObject<HTMLElement> },
|
||||
) => void
|
||||
/**
|
||||
* Callback when a keyboard event occurs
|
||||
* @note Call `event.preventDefault()` to prevent the default behavior
|
||||
*/
|
||||
onKeydown?: (
|
||||
event: KeyboardEvent,
|
||||
ctx: { editor: Editor; ref: React.RefObject<HTMLElement> },
|
||||
) => void
|
||||
}) {
|
||||
const callbacks = useRef({
|
||||
onEscape,
|
||||
onKeydown,
|
||||
})
|
||||
|
||||
// Always take the latest callback
|
||||
callbacks.current = {
|
||||
onEscape,
|
||||
onKeydown,
|
||||
}
|
||||
|
||||
const getFocusableElements = useCallback(() => {
|
||||
if (!containerRef.current) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Array.from(
|
||||
containerRef.current.querySelectorAll<HTMLElement>('button, [role="button"], [tabindex="0"]'),
|
||||
).filter(el => !el.hasAttribute('disabled'))
|
||||
}, [containerRef])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (!containerRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
callbacks.current.onKeydown(event, { editor, ref: containerRef })
|
||||
if (event.defaultPrevented) {
|
||||
return
|
||||
}
|
||||
|
||||
const elements = getFocusableElements()
|
||||
const isWithinContainer = containerRef.current.contains(event.target as HTMLElement)
|
||||
|
||||
if (isWithinContainer) {
|
||||
// Allow to escape to the editor
|
||||
if (event.key === 'Escape') {
|
||||
callbacks.current.onEscape(event, { editor, ref: containerRef })
|
||||
if (event.defaultPrevented) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the first element in the menu bar
|
||||
if (event.key === 'Home') {
|
||||
event.preventDefault()
|
||||
elements[0].focus()
|
||||
return true
|
||||
}
|
||||
|
||||
// Move to the last element in the menu bar
|
||||
if (event.key === 'End') {
|
||||
event.preventDefault()
|
||||
elements[elements.length - 1].focus()
|
||||
return true
|
||||
}
|
||||
|
||||
// Move forward in the menu bar
|
||||
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
||||
const element = elements.indexOf(event.target as HTMLElement)
|
||||
|
||||
elements[(element + 1) % elements.length].focus()
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// Move backward in the menu bar
|
||||
if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
|
||||
const element = elements.indexOf(event.target as HTMLElement)
|
||||
|
||||
elements[(element - 1 + elements.length) % elements.length].focus()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [containerRef, editor, getFocusableElements])
|
||||
|
||||
return { getFocusableElements }
|
||||
}
|
||||
83
demos/src/Examples/AutolinkValidation/React/index.jsx
Normal file
83
demos/src/Examples/AutolinkValidation/React/index.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import './styles.scss'
|
||||
|
||||
import Link from '@tiptap/extension-link'
|
||||
import { EditorContent, useEditor, useEditorState } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React from 'react'
|
||||
|
||||
export default () => {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Link.configure({
|
||||
validate: link => /^https?:\/\//.test(link),
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>Hey! Try to type in url with and without a http/s protocol. - Links without a protocol should not get auto linked</p>
|
||||
`,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
spellcheck: 'false',
|
||||
},
|
||||
},
|
||||
shouldRerenderOnTransaction: true,
|
||||
})
|
||||
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: ctx => {
|
||||
return {
|
||||
isLink: ctx.editor.isActive('link'),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const setLink = React.useCallback(() => {
|
||||
const previousUrl = editor.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
|
||||
// cancelled
|
||||
if (url === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// update link
|
||||
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}, [editor])
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="control-group">
|
||||
<div className="button-group">
|
||||
<button
|
||||
onClick={setLink}
|
||||
className={editorState.isLink ? 'is-active' : ''}
|
||||
data-testid="setLink"
|
||||
>
|
||||
Set link
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().unsetLink().run()}
|
||||
disabled={!editorState.isLink}
|
||||
data-testid="unsetLink"
|
||||
>
|
||||
Unset link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<EditorContent editor={editor} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
101
demos/src/Examples/AutolinkValidation/React/styles.scss
Normal file
101
demos/src/Examples/AutolinkValidation/React/styles.scss
Normal file
@@ -0,0 +1,101 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Link styles */
|
||||
a {
|
||||
color: var(--purple);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--purple-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
demos/src/Examples/AutolinkValidation/Vue/index.vue
Normal file
101
demos/src/Examples/AutolinkValidation/Vue/index.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<editor-content :editor="editor" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Link from '@tiptap/extension-link'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Editor, EditorContent } from '@tiptap/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Link.configure({
|
||||
validate: link => /^https?:\/\//.test(link),
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>Hey! Try to type in url with and without a http/s protocol. - Links without a protocol should not get auto linked</p>
|
||||
`,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
spellcheck: 'false',
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.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;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid rgba(#0d0d0d, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
151
demos/src/Examples/AutolinkValidation/index.spec.ts
Normal file
151
demos/src/Examples/AutolinkValidation/index.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { getEditor } from '../../../test/helpers.js'
|
||||
|
||||
const demoName = 'AutolinkValidation'
|
||||
const frameworkPaths = ['React', 'Vue']
|
||||
const demoPath = '/src/Examples'
|
||||
|
||||
const validLinks: [string, string][] = [
|
||||
['https://tiptap.dev ', 'https://tiptap.dev'],
|
||||
['http://tiptap.dev ', 'http://tiptap.dev'],
|
||||
['https://www.tiptap.dev/ ', 'https://www.tiptap.dev/'],
|
||||
['http://www.tiptap.dev/ ', 'http://www.tiptap.dev/'],
|
||||
['[http://www.example.com/] ', 'http://www.example.com/'],
|
||||
['(http://www.example.com/) ', 'http://www.example.com/'],
|
||||
]
|
||||
|
||||
const invalidLinks = ['tiptap.dev', 'www.tiptap.dev', 'https://tiptap.dev']
|
||||
|
||||
test.describe(`${demoPath}/${demoName}`, () => {
|
||||
frameworkPaths.forEach(frameworkPath => {
|
||||
const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/`
|
||||
|
||||
test.describe(`${frameworkPath}`, () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(fullDemoPath)
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.evaluate((el: any) => {
|
||||
el.editor.commands.clearContent()
|
||||
})
|
||||
})
|
||||
|
||||
validLinks.forEach(([rawTextInput, textThatShouldBeLinked]) => {
|
||||
test(`should autolink ${rawTextInput}`, async ({ page }) => {
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.click()
|
||||
await editor.type(rawTextInput)
|
||||
|
||||
await expect(page.locator('.tiptap a').first()).toContainText(textThatShouldBeLinked)
|
||||
})
|
||||
})
|
||||
|
||||
invalidLinks.forEach(rawTextInput => {
|
||||
test(`should not autolink ${rawTextInput}`, async ({ page }) => {
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.click()
|
||||
await editor.type(rawTextInput)
|
||||
|
||||
await expect(page.locator('.tiptap a')).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
|
||||
if (frameworkPath === 'React') {
|
||||
test('should not relink unset links after entering second link', async ({ page }) => {
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.click()
|
||||
await editor.type('https://tiptap.dev ')
|
||||
await editor.press('Home')
|
||||
|
||||
await expect(page.locator('.tiptap')).toHaveText('https://tiptap.dev ')
|
||||
|
||||
await page.locator('[data-testid=unsetLink]').click()
|
||||
await expect(page.locator('.tiptap a')).toHaveCount(0)
|
||||
|
||||
await editor.press('End')
|
||||
await editor.type('http://www.example.com/ ')
|
||||
|
||||
await expect(page.locator('.tiptap a')).toHaveCount(1)
|
||||
await expect(page.locator('.tiptap a').first()).toHaveAttribute(
|
||||
'href',
|
||||
'http://www.example.com/',
|
||||
)
|
||||
})
|
||||
|
||||
test('should not relink unset links after hitting next paragraph', async ({ page }) => {
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.click()
|
||||
await editor.type('https://tiptap.dev ')
|
||||
await editor.press('Home')
|
||||
await expect(page.locator('.tiptap')).toHaveText('https://tiptap.dev ')
|
||||
|
||||
await page.locator('[data-testid=unsetLink]').click()
|
||||
await expect(page.locator('.tiptap a')).toHaveCount(0)
|
||||
|
||||
await editor.press('End')
|
||||
await editor.type(
|
||||
'typing other text should prevent the link from relinking when hitting enter',
|
||||
)
|
||||
await editor.press('Enter')
|
||||
|
||||
await expect(page.locator('.tiptap a')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('should not relink unset links after modifying', async ({ page }) => {
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.click()
|
||||
await editor.type('https://tiptap.dev ')
|
||||
await editor.press('Home')
|
||||
await expect(page.locator('.tiptap')).toHaveText('https://tiptap.dev ')
|
||||
|
||||
await page.locator('[data-testid=unsetLink]').click()
|
||||
await expect(page.locator('.tiptap a')).toHaveCount(0)
|
||||
|
||||
await editor.press('Home')
|
||||
await Array.from({ length: 'https://'.length }).reduce<Promise<void>>(
|
||||
previousPromise => previousPromise.then(() => editor.press('ArrowRight')),
|
||||
Promise.resolve(),
|
||||
)
|
||||
await editor.type('blah')
|
||||
|
||||
await expect(page.locator('.tiptap')).toHaveText('https://blahtiptap.dev ')
|
||||
await expect(page.locator('.tiptap a')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('should autolink after hitting enter (new paragraph)', async ({ page }) => {
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.click()
|
||||
await editor.type('https://tiptap.dev')
|
||||
await editor.press('Enter')
|
||||
|
||||
await expect(page.locator('.tiptap a')).toHaveCount(1)
|
||||
await expect(page.locator('.tiptap a').first()).toHaveAttribute(
|
||||
'href',
|
||||
'https://tiptap.dev',
|
||||
)
|
||||
})
|
||||
|
||||
test('should autolink after hitting shift-enter (hardbreak)', async ({ page }) => {
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.click()
|
||||
await editor.type('https://tiptap.dev')
|
||||
await editor.press('Shift+Enter')
|
||||
|
||||
await expect(page.locator('.tiptap a')).toHaveCount(1)
|
||||
await expect(page.locator('.tiptap a').first()).toHaveAttribute(
|
||||
'href',
|
||||
'https://tiptap.dev',
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
0
demos/src/Examples/Book/React/index.html
Normal file
0
demos/src/Examples/Book/React/index.html
Normal file
157
demos/src/Examples/Book/React/index.jsx
Normal file
157
demos/src/Examples/Book/React/index.jsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { EditorContent, useEditor, useEditorState } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React from 'react'
|
||||
|
||||
import { content } from '../content.js'
|
||||
|
||||
const menuBarStateSelector = ctx => ({
|
||||
isBold: ctx.editor.isActive('bold') ?? false,
|
||||
isItalic: ctx.editor.isActive('italic') ?? false,
|
||||
isStrike: ctx.editor.isActive('strike') ?? false,
|
||||
isCode: ctx.editor.isActive('code') ?? false,
|
||||
isParagraph: ctx.editor.isActive('paragraph') ?? false,
|
||||
isHeading1: ctx.editor.isActive('heading', { level: 1 }) ?? false,
|
||||
isHeading2: ctx.editor.isActive('heading', { level: 2 }) ?? false,
|
||||
isHeading3: ctx.editor.isActive('heading', { level: 3 }) ?? false,
|
||||
isHeading4: ctx.editor.isActive('heading', { level: 4 }) ?? false,
|
||||
isHeading5: ctx.editor.isActive('heading', { level: 5 }) ?? false,
|
||||
isHeading6: ctx.editor.isActive('heading', { level: 6 }) ?? false,
|
||||
isBulletList: ctx.editor.isActive('bulletList') ?? false,
|
||||
isOrderedList: ctx.editor.isActive('orderedList') ?? false,
|
||||
isCodeBlock: ctx.editor.isActive('codeBlock') ?? false,
|
||||
isBlockquote: ctx.editor.isActive('blockquote') ?? false,
|
||||
})
|
||||
|
||||
const MenuBar = ({ editor }) => {
|
||||
const editorState = useEditorState({ editor, selector: menuBarStateSelector })
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="control-group">
|
||||
<div className="button-group">
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
className={editorState.isBold ? 'is-active' : ''}
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
className={editorState.isItalic ? 'is-active' : ''}
|
||||
>
|
||||
Italic
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
className={editorState.isStrike ? 'is-active' : ''}
|
||||
>
|
||||
Strike
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
className={editorState.isCode ? 'is-active' : ''}
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}>Clear marks</button>
|
||||
<button onClick={() => editor.chain().focus().clearNodes().run()}>Clear nodes</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||
className={editorState.isParagraph ? 'is-active' : ''}
|
||||
>
|
||||
Paragraph
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
className={editorState.isHeading1 ? 'is-active' : ''}
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
className={editorState.isHeading2 ? 'is-active' : ''}
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
className={editorState.isHeading3 ? 'is-active' : ''}
|
||||
>
|
||||
H3
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
|
||||
className={editorState.isHeading4 ? 'is-active' : ''}
|
||||
>
|
||||
H4
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
|
||||
className={editorState.isHeading5 ? 'is-active' : ''}
|
||||
>
|
||||
H5
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
|
||||
className={editorState.isHeading6 ? 'is-active' : ''}
|
||||
>
|
||||
H6
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
className={editorState.isBulletList ? 'is-active' : ''}
|
||||
>
|
||||
Bullet list
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
className={editorState.isOrderedList ? 'is-active' : ''}
|
||||
>
|
||||
Ordered list
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
className={editorState.isCodeBlock ? 'is-active' : ''}
|
||||
>
|
||||
Code block
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
className={editorState.isBlockquote ? 'is-active' : ''}
|
||||
>
|
||||
Blockquote
|
||||
</button>
|
||||
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
|
||||
Horizontal rule
|
||||
</button>
|
||||
<button onClick={() => editor.chain().focus().setHardBreak().run()}>Hard break</button>
|
||||
<button onClick={() => editor.chain().focus().undo().run()}>Undo</button>
|
||||
<button onClick={() => editor.chain().focus().redo().run()}>Redo</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
content,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
spellcheck: 'false',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuBar editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
91
demos/src/Examples/Book/React/styles.scss
Normal file
91
demos/src/Examples/Book/React/styles.scss
Normal file
@@ -0,0 +1,91 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
0
demos/src/Examples/Book/Vue/index.html
Normal file
0
demos/src/Examples/Book/Vue/index.html
Normal file
234
demos/src/Examples/Book/Vue/index.vue
Normal file
234
demos/src/Examples/Book/Vue/index.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div v-if="editor" class="container">
|
||||
<div class="control-group">
|
||||
<div class="button-group">
|
||||
<button
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
>
|
||||
Italic
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
>
|
||||
Strike
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleCode().run()"
|
||||
:class="{ 'is-active': editor.isActive('code') }"
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button @click="editor.chain().focus().unsetAllMarks().run()">Clear marks</button>
|
||||
<button @click="editor.chain().focus().clearNodes().run()">Clear nodes</button>
|
||||
<button
|
||||
@click="editor.chain().focus().setParagraph().run()"
|
||||
:class="{ 'is-active': editor.isActive('paragraph') }"
|
||||
>
|
||||
Paragraph
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
|
||||
>
|
||||
H3
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
|
||||
>
|
||||
H4
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"
|
||||
>
|
||||
H5
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"
|
||||
>
|
||||
H6
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||||
>
|
||||
Bullet list
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
||||
>
|
||||
Ordered list
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleCodeBlock().run()"
|
||||
:class="{ 'is-active': editor.isActive('codeBlock') }"
|
||||
>
|
||||
Code block
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
||||
>
|
||||
Blockquote
|
||||
</button>
|
||||
<button @click="editor.chain().focus().setHorizontalRule().run()">Horizontal rule</button>
|
||||
<button @click="editor.chain().focus().setHardBreak().run()">Hard break</button>
|
||||
<button @click="editor.chain().focus().undo().run()">Undo</button>
|
||||
<button @click="editor.chain().focus().redo().run()">Redo</button>
|
||||
</div>
|
||||
</div>
|
||||
<editor-content :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Editor, EditorContent } from '@tiptap/vue'
|
||||
|
||||
import { content } from '../content.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [StarterKit],
|
||||
content,
|
||||
editorProps: {
|
||||
attributes: {
|
||||
spellcheck: 'false',
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
0
demos/src/Examples/CSSModules/React/index.html
Normal file
0
demos/src/Examples/CSSModules/React/index.html
Normal file
179
demos/src/Examples/CSSModules/React/index.jsx
Normal file
179
demos/src/Examples/CSSModules/React/index.jsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { EditorContent, useEditor, useEditorState } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React from 'react'
|
||||
|
||||
import styles from './index.module.css'
|
||||
|
||||
const menuBarStateSelector = ctx => ({
|
||||
isBold: ctx.editor.isActive('bold') ?? false,
|
||||
isItalic: ctx.editor.isActive('italic') ?? false,
|
||||
isStrike: ctx.editor.isActive('strike') ?? false,
|
||||
isCode: ctx.editor.isActive('code') ?? false,
|
||||
isParagraph: ctx.editor.isActive('paragraph') ?? false,
|
||||
isHeading1: ctx.editor.isActive('heading', { level: 1 }) ?? false,
|
||||
isHeading2: ctx.editor.isActive('heading', { level: 2 }) ?? false,
|
||||
isHeading3: ctx.editor.isActive('heading', { level: 3 }) ?? false,
|
||||
isHeading4: ctx.editor.isActive('heading', { level: 4 }) ?? false,
|
||||
isHeading5: ctx.editor.isActive('heading', { level: 5 }) ?? false,
|
||||
isHeading6: ctx.editor.isActive('heading', { level: 6 }) ?? false,
|
||||
isBulletList: ctx.editor.isActive('bulletList') ?? false,
|
||||
isOrderedList: ctx.editor.isActive('orderedList') ?? false,
|
||||
isCodeBlock: ctx.editor.isActive('codeBlock') ?? false,
|
||||
isBlockquote: ctx.editor.isActive('blockquote') ?? false,
|
||||
})
|
||||
|
||||
const MenuBar = ({ editor }) => {
|
||||
const editorState = useEditorState({ editor, selector: menuBarStateSelector })
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`toolbar ${styles.toolbar}`}>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
className={editorState.isBold ? 'is-active' : ''}
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
className={editorState.isItalic ? 'is-active' : ''}
|
||||
>
|
||||
Italic
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
className={editorState.isStrike ? 'is-active' : ''}
|
||||
>
|
||||
Strike
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCode().run()}
|
||||
className={editorState.isCode ? 'is-active' : ''}
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button onClick={() => editor.chain().focus().unsetAllMarks().run()}>Clear marks</button>
|
||||
<button onClick={() => editor.chain().focus().clearNodes().run()}>Clear nodes</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||
className={editorState.isParagraph ? 'is-active' : ''}
|
||||
>
|
||||
Paragraph
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
className={editorState.isHeading1 ? 'is-active' : ''}
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
className={editorState.isHeading2 ? 'is-active' : ''}
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
className={editorState.isHeading3 ? 'is-active' : ''}
|
||||
>
|
||||
H3
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
|
||||
className={editorState.isHeading4 ? 'is-active' : ''}
|
||||
>
|
||||
H4
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
|
||||
className={editorState.isHeading5 ? 'is-active' : ''}
|
||||
>
|
||||
H5
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
|
||||
className={editorState.isHeading6 ? 'is-active' : ''}
|
||||
>
|
||||
H6
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
className={editorState.isBulletList ? 'is-active' : ''}
|
||||
>
|
||||
Bullet list
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
className={editorState.isOrderedList ? 'is-active' : ''}
|
||||
>
|
||||
Ordered list
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
className={editorState.isCodeBlock ? 'is-active' : ''}
|
||||
>
|
||||
Code block
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
className={editorState.isBlockquote ? 'is-active' : ''}
|
||||
>
|
||||
Blockquote
|
||||
</button>
|
||||
<button onClick={() => editor.chain().focus().setHorizontalRule().run()}>
|
||||
Horizontal rule
|
||||
</button>
|
||||
<button onClick={() => editor.chain().focus().setHardBreak().run()}>Hard break</button>
|
||||
<button onClick={() => editor.chain().focus().undo().run()}>Undo</button>
|
||||
<button onClick={() => editor.chain().focus().redo().run()}>Redo</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
content: `
|
||||
<h2>
|
||||
Hi there,
|
||||
</h2>
|
||||
<p>
|
||||
this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
That’s a bullet list with one …
|
||||
</li>
|
||||
<li>
|
||||
… or two list items.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
|
||||
</p>
|
||||
<pre><code class="language-css">body {
|
||||
display: none;
|
||||
}</code></pre>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<blockquote>
|
||||
Wow, that’s amazing. Good work, boy! 👏
|
||||
<br />
|
||||
— Mom
|
||||
</blockquote>
|
||||
`,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuBar editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
4
demos/src/Examples/CSSModules/React/index.module.css
Normal file
4
demos/src/Examples/CSSModules/React/index.module.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.toolbar {
|
||||
background-color: red;
|
||||
padding: 16px;
|
||||
}
|
||||
91
demos/src/Examples/CSSModules/React/styles.scss
Normal file
91
demos/src/Examples/CSSModules/React/styles.scss
Normal file
@@ -0,0 +1,91 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
0
demos/src/Examples/CSSModules/Vue/index.html
Normal file
0
demos/src/Examples/CSSModules/Vue/index.html
Normal file
4
demos/src/Examples/CSSModules/Vue/index.module.css
Normal file
4
demos/src/Examples/CSSModules/Vue/index.module.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.toolbar {
|
||||
background-color: red;
|
||||
padding: 16px;
|
||||
}
|
||||
255
demos/src/Examples/CSSModules/Vue/index.vue
Normal file
255
demos/src/Examples/CSSModules/Vue/index.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div v-if="editor" class="toolbar" :class="styles.toolbar">
|
||||
<button
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
>
|
||||
Bold
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
>
|
||||
Italic
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
>
|
||||
Strike
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleCode().run()"
|
||||
:class="{ 'is-active': editor.isActive('code') }"
|
||||
>
|
||||
Code
|
||||
</button>
|
||||
<button @click="editor.chain().focus().unsetAllMarks().run()">Clear marks</button>
|
||||
<button @click="editor.chain().focus().clearNodes().run()">Clear nodes</button>
|
||||
<button
|
||||
@click="editor.chain().focus().setParagraph().run()"
|
||||
:class="{ 'is-active': editor.isActive('paragraph') }"
|
||||
>
|
||||
Paragraph
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
|
||||
>
|
||||
H1
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
|
||||
>
|
||||
H2
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 3 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 3 }) }"
|
||||
>
|
||||
H3
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 4 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 4 }) }"
|
||||
>
|
||||
H4
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 5 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 5 }) }"
|
||||
>
|
||||
H5
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleHeading({ level: 6 }).run()"
|
||||
:class="{ 'is-active': editor.isActive('heading', { level: 6 }) }"
|
||||
>
|
||||
H6
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleBulletList().run()"
|
||||
:class="{ 'is-active': editor.isActive('bulletList') }"
|
||||
>
|
||||
Bullet list
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleOrderedList().run()"
|
||||
:class="{ 'is-active': editor.isActive('orderedList') }"
|
||||
>
|
||||
Ordered list
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleCodeBlock().run()"
|
||||
:class="{ 'is-active': editor.isActive('codeBlock') }"
|
||||
>
|
||||
Code block
|
||||
</button>
|
||||
<button
|
||||
@click="editor.chain().focus().toggleBlockquote().run()"
|
||||
:class="{ 'is-active': editor.isActive('blockquote') }"
|
||||
>
|
||||
Blockquote
|
||||
</button>
|
||||
<button @click="editor.chain().focus().setHorizontalRule().run()">Horizontal rule</button>
|
||||
<button @click="editor.chain().focus().setHardBreak().run()">Hard break</button>
|
||||
<button @click="editor.chain().focus().undo().run()">Undo</button>
|
||||
<button @click="editor.chain().focus().redo().run()">Redo</button>
|
||||
</div>
|
||||
<editor-content :editor="editor" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Editor, EditorContent } from '@tiptap/vue'
|
||||
|
||||
import styles from './index.module.css'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
styles,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [StarterKit],
|
||||
content: `
|
||||
<h1 class="test">
|
||||
This is a red headline
|
||||
</h1>
|
||||
<p>
|
||||
this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
That’s a bullet list with one …
|
||||
</li>
|
||||
<li>
|
||||
… or two list items.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
|
||||
</p>
|
||||
<pre><code class="language-css">body {
|
||||
display: none;
|
||||
}</code></pre>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<blockquote>
|
||||
Wow, that’s amazing. Good work, boy! 👏
|
||||
<br />
|
||||
— Mom
|
||||
</blockquote>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* List styles */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
|
||||
li p {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
margin-top: 2.5rem;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 3.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Code and preformatted text styles */
|
||||
code {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
color: var(--black);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.25em 0.3em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--black);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--white);
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
margin: 1.5rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 3px solid var(--gray-3);
|
||||
margin: 1.5rem 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--gray-2);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
demos/src/Examples/CSSModules/index.spec.ts
Normal file
25
demos/src/Examples/CSSModules/index.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
const demoName = 'CSSModules'
|
||||
const frameworkPaths = ['React', 'Vue']
|
||||
const demoPath = '/src/Examples'
|
||||
|
||||
test.describe(`${demoPath}/${demoName}`, () => {
|
||||
frameworkPaths.forEach(frameworkPath => {
|
||||
const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/`
|
||||
|
||||
test.describe(`${frameworkPath}`, () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(fullDemoPath)
|
||||
})
|
||||
|
||||
test('should apply the toolbar styles from the CSS module', async ({ page }) => {
|
||||
const toolbar = page.locator('.toolbar')
|
||||
|
||||
await expect(toolbar).toBeVisible()
|
||||
await expect(toolbar).toHaveCSS('background-color', 'rgb(255, 0, 0)')
|
||||
await expect(toolbar).toHaveCSS('padding', '16px')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import './CodeBlockComponent.scss'
|
||||
|
||||
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
|
||||
import React from 'react'
|
||||
|
||||
export default ({
|
||||
node: {
|
||||
attrs: { language: defaultLanguage },
|
||||
},
|
||||
updateAttributes,
|
||||
extension,
|
||||
}) => (
|
||||
<NodeViewWrapper className="code-block">
|
||||
<select
|
||||
contentEditable={false}
|
||||
defaultValue={defaultLanguage}
|
||||
onChange={event => updateAttributes({ language: event.target.value })}
|
||||
>
|
||||
<option value="null">auto</option>
|
||||
<option disabled>—</option>
|
||||
{extension.options.lowlight.listLanguages().map((lang, index) => (
|
||||
<option key={index} value={lang}>
|
||||
{lang}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<pre>
|
||||
<NodeViewContent as="code" />
|
||||
</pre>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
.tiptap {
|
||||
.code-block {
|
||||
position: relative;
|
||||
|
||||
select {
|
||||
position: absolute;
|
||||
background-color: var(--white);
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="Black" d="M7 10l5 5 5-5z"/></svg>');
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
demos/src/Examples/CodeBlockLanguage/React/index.jsx
Normal file
93
demos/src/Examples/CodeBlockLanguage/React/index.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import './styles.scss'
|
||||
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { EditorContent, ReactNodeViewRenderer, useEditor, useEditorState } from '@tiptap/react'
|
||||
import css from 'highlight.js/lib/languages/css'
|
||||
import js from 'highlight.js/lib/languages/javascript'
|
||||
import ts from 'highlight.js/lib/languages/typescript'
|
||||
import html from 'highlight.js/lib/languages/xml'
|
||||
// load all languages with "all" or common languages with "common"
|
||||
import { all, createLowlight } from 'lowlight'
|
||||
import React from 'react'
|
||||
|
||||
// oxlint-disable-next-line
|
||||
import CodeBlockComponent from './CodeBlockComponent'
|
||||
|
||||
// create a lowlight instance
|
||||
const lowlight = createLowlight(all)
|
||||
|
||||
// you can also register individual languages
|
||||
lowlight.register('html', html)
|
||||
lowlight.register('css', css)
|
||||
lowlight.register('js', js)
|
||||
lowlight.register('ts', ts)
|
||||
|
||||
const MenuBar = ({ editor }) => {
|
||||
const { isCodeBlock } = useEditorState({
|
||||
editor,
|
||||
selector: ctx => ({
|
||||
isCodeBlock: ctx.editor.isActive('codeBlock') ?? false,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!editor) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="control-group">
|
||||
<div className="button-group">
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
className={isCodeBlock ? 'is-active' : ''}
|
||||
>
|
||||
Toggle code block
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
CodeBlockLowlight.extend({
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CodeBlockComponent)
|
||||
},
|
||||
}).configure({ lowlight }),
|
||||
],
|
||||
content: `
|
||||
<p>
|
||||
That's a boring paragraph followed by a fenced code block:
|
||||
</p>
|
||||
<pre><code class="language-javascript">for (var i=1; i <= 20; i++)
|
||||
{
|
||||
if (i % 15 == 0)
|
||||
console.log("FizzBuzz");
|
||||
else if (i % 3 == 0)
|
||||
console.log("Fizz");
|
||||
else if (i % 5 == 0)
|
||||
console.log("Buzz");
|
||||
else
|
||||
console.log(i);
|
||||
}</code></pre>
|
||||
<p>
|
||||
Press Command/Ctrl + Enter to leave the fenced code block and continue typing in boring paragraphs.
|
||||
</p>
|
||||
`,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuBar editor={editor} />
|
||||
<EditorContent editor={editor} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
75
demos/src/Examples/CodeBlockLanguage/React/styles.scss
Normal file
75
demos/src/Examples/CodeBlockLanguage/React/styles.scss
Normal file
@@ -0,0 +1,75 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* Code styling */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #f98181;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #fbbc88;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #b9f18d;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #faf594;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #70cff8;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<node-view-wrapper class="code-block">
|
||||
<select contenteditable="false" v-model="selectedLanguage">
|
||||
<option :value="null">auto</option>
|
||||
<option disabled>—</option>
|
||||
<option v-for="(language, index) in languages" :value="language" :key="index">
|
||||
{{ language }}
|
||||
</option>
|
||||
</select>
|
||||
<pre><code><node-view-content /></code></pre>
|
||||
</node-view-wrapper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { NodeViewContent, nodeViewProps, NodeViewWrapper } from '@tiptap/vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NodeViewWrapper,
|
||||
NodeViewContent,
|
||||
},
|
||||
|
||||
props: nodeViewProps,
|
||||
|
||||
data() {
|
||||
return {
|
||||
languages: this.extension.options.lowlight.listLanguages(),
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
selectedLanguage: {
|
||||
get() {
|
||||
return this.node.attrs.language
|
||||
},
|
||||
set(language) {
|
||||
this.updateAttributes({ language })
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.tiptap {
|
||||
.code-block {
|
||||
position: relative;
|
||||
|
||||
select {
|
||||
position: absolute;
|
||||
background-color: var(--white);
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="Black" d="M7 10l5 5 5-5z"/></svg>');
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
0
demos/src/Examples/CodeBlockLanguage/Vue/index.html
Normal file
0
demos/src/Examples/CodeBlockLanguage/Vue/index.html
Normal file
168
demos/src/Examples/CodeBlockLanguage/Vue/index.vue
Normal file
168
demos/src/Examples/CodeBlockLanguage/Vue/index.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div v-if="editor" class="container">
|
||||
<div class="control-group">
|
||||
<div class="button-">
|
||||
<button
|
||||
@click="editor.chain().focus().toggleCodeBlock().run()"
|
||||
:class="{ 'is-active': editor.isActive('codeBlock') }"
|
||||
>
|
||||
Toggle code block
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<editor-content :editor="editor" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { Editor, EditorContent, VueNodeViewRenderer } from '@tiptap/vue'
|
||||
import css from 'highlight.js/lib/languages/css'
|
||||
import js from 'highlight.js/lib/languages/javascript'
|
||||
import ts from 'highlight.js/lib/languages/typescript'
|
||||
import html from 'highlight.js/lib/languages/xml'
|
||||
// load all languages with "all" or common languages with "common"
|
||||
import { all, createLowlight } from 'lowlight'
|
||||
|
||||
import CodeBlockComponent from './CodeBlockComponent.vue'
|
||||
|
||||
// create a lowlight instance
|
||||
const lowlight = createLowlight(all)
|
||||
|
||||
// you can also register languages
|
||||
lowlight.register('html', html)
|
||||
lowlight.register('css', css)
|
||||
lowlight.register('js', js)
|
||||
lowlight.register('ts', ts)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
CodeBlockLowlight.extend({
|
||||
addNodeView() {
|
||||
return VueNodeViewRenderer(CodeBlockComponent)
|
||||
},
|
||||
}).configure({ lowlight }),
|
||||
],
|
||||
content: `
|
||||
<p>
|
||||
That's a boring paragraph followed by a fenced code block:
|
||||
</p>
|
||||
<pre><code class="language-javascript">for (var i=1; i <= 20; i++)
|
||||
{
|
||||
if (i % 15 == 0)
|
||||
console.log("FizzBuzz");
|
||||
else if (i % 3 == 0)
|
||||
console.log("Fizz");
|
||||
else if (i % 5 == 0)
|
||||
console.log("Buzz");
|
||||
else
|
||||
console.log(i);
|
||||
}</code></pre>
|
||||
<p>
|
||||
Press Command/Ctrl + Enter to leave the fenced code block and continue typing in boring paragraphs.
|
||||
</p>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* Code styling */
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-name,
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #f98181;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #fbbc88;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #b9f18d;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #faf594;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag {
|
||||
color: #70cff8;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
155
demos/src/Examples/CollaborativeEditing/React/MenuBar.jsx
Normal file
155
demos/src/Examples/CollaborativeEditing/React/MenuBar.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import './MenuBar.scss'
|
||||
|
||||
import { useEditorState } from '@tiptap/react'
|
||||
import React, { Fragment } from 'react'
|
||||
|
||||
import MenuItem from './MenuItem.jsx'
|
||||
|
||||
export default ({ editor }) => {
|
||||
const editorState = useEditorState({
|
||||
editor,
|
||||
selector: ctx => ({
|
||||
isBold: ctx.editor.isActive('bold') ?? false,
|
||||
isItalic: ctx.editor.isActive('italic') ?? false,
|
||||
isStrike: ctx.editor.isActive('strike') ?? false,
|
||||
isCode: ctx.editor.isActive('code') ?? false,
|
||||
isHighlight: ctx.editor.isActive('highlight') ?? false,
|
||||
isHeading1: ctx.editor.isActive('heading', { level: 1 }) ?? false,
|
||||
isHeading2: ctx.editor.isActive('heading', { level: 2 }) ?? false,
|
||||
isParagraph: ctx.editor.isActive('paragraph') ?? false,
|
||||
isBulletList: ctx.editor.isActive('bulletList') ?? false,
|
||||
isOrderedList: ctx.editor.isActive('orderedList') ?? false,
|
||||
isTaskList: ctx.editor.isActive('taskList') ?? false,
|
||||
isCodeBlock: ctx.editor.isActive('codeBlock') ?? false,
|
||||
isBlockquote: ctx.editor.isActive('blockquote') ?? false,
|
||||
}),
|
||||
})
|
||||
|
||||
const items = [
|
||||
{
|
||||
icon: 'bold',
|
||||
title: 'Bold',
|
||||
action: () => editor.chain().focus().toggleBold().run(),
|
||||
isActive: () => editorState.isBold,
|
||||
},
|
||||
{
|
||||
icon: 'italic',
|
||||
title: 'Italic',
|
||||
action: () => editor.chain().focus().toggleItalic().run(),
|
||||
isActive: () => editorState.isItalic,
|
||||
},
|
||||
{
|
||||
icon: 'strikethrough',
|
||||
title: 'Strike',
|
||||
action: () => editor.chain().focus().toggleStrike().run(),
|
||||
isActive: () => editorState.isStrike,
|
||||
},
|
||||
{
|
||||
icon: 'code-view',
|
||||
title: 'Code',
|
||||
action: () => editor.chain().focus().toggleCode().run(),
|
||||
isActive: () => editorState.isCode,
|
||||
},
|
||||
{
|
||||
icon: 'mark-pen-line',
|
||||
title: 'Highlight',
|
||||
action: () => editor.chain().focus().toggleHighlight().run(),
|
||||
isActive: () => editorState.isHighlight,
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'h-1',
|
||||
title: 'Heading 1',
|
||||
action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
isActive: () => editorState.isHeading1,
|
||||
},
|
||||
{
|
||||
icon: 'h-2',
|
||||
title: 'Heading 2',
|
||||
action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
isActive: () => editorState.isHeading2,
|
||||
},
|
||||
{
|
||||
icon: 'paragraph',
|
||||
title: 'Paragraph',
|
||||
action: () => editor.chain().focus().setParagraph().run(),
|
||||
isActive: () => editorState.isParagraph,
|
||||
},
|
||||
{
|
||||
icon: 'list-unordered',
|
||||
title: 'Bullet list',
|
||||
action: () => editor.chain().focus().toggleBulletList().run(),
|
||||
isActive: () => editorState.isBulletList,
|
||||
},
|
||||
{
|
||||
icon: 'list-ordered',
|
||||
title: 'Ordered list',
|
||||
action: () => editor.chain().focus().toggleOrderedList().run(),
|
||||
isActive: () => editorState.isOrderedList,
|
||||
},
|
||||
{
|
||||
icon: 'list-check-2',
|
||||
title: 'Task list',
|
||||
action: () => editor.chain().focus().toggleTaskList().run(),
|
||||
isActive: () => editorState.isTaskList,
|
||||
},
|
||||
{
|
||||
icon: 'code-box-line',
|
||||
title: 'Code block',
|
||||
action: () => editor.chain().focus().toggleCodeBlock().run(),
|
||||
isActive: () => editorState.isCodeBlock,
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'double-quotes-l',
|
||||
title: 'Blockquote',
|
||||
action: () => editor.chain().focus().toggleBlockquote().run(),
|
||||
isActive: () => editorState.isBlockquote,
|
||||
},
|
||||
{
|
||||
icon: 'separator',
|
||||
title: 'Horizontal rule',
|
||||
action: () => editor.chain().focus().setHorizontalRule().run(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'text-wrap',
|
||||
title: 'Hard break',
|
||||
action: () => editor.chain().focus().setHardBreak().run(),
|
||||
},
|
||||
{
|
||||
icon: 'format-clear',
|
||||
title: 'Clear format',
|
||||
action: () => editor.chain().focus().clearNodes().unsetAllMarks().run(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'arrow-go-back-line',
|
||||
title: 'Undo',
|
||||
action: () => editor.chain().focus().undo().run(),
|
||||
},
|
||||
{
|
||||
icon: 'arrow-go-forward-line',
|
||||
title: 'Redo',
|
||||
action: () => editor.chain().focus().redo().run(),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="editor__header">
|
||||
{items.map((item, index) => (
|
||||
<Fragment key={index}>
|
||||
{item.type === 'divider' ? <div className="divider" /> : <MenuItem {...item} />}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.divider {
|
||||
background-color: rgba(#fff, 0.25);
|
||||
height: 1.25rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.75rem;
|
||||
width: 1px;
|
||||
}
|
||||
16
demos/src/Examples/CollaborativeEditing/React/MenuItem.jsx
Normal file
16
demos/src/Examples/CollaborativeEditing/React/MenuItem.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import './MenuItem.scss'
|
||||
|
||||
import React from 'react'
|
||||
import remixiconUrl from 'remixicon/fonts/remixicon.symbol.svg'
|
||||
|
||||
export default ({ icon, title, action, isActive = null }) => (
|
||||
<button
|
||||
className={`menu-item${isActive && isActive() ? ' is-active' : ''}`}
|
||||
onClick={action}
|
||||
title={title}
|
||||
>
|
||||
<svg className="remix">
|
||||
<use xlinkHref={`${remixiconUrl}#ri-${icon}`} />
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
22
demos/src/Examples/CollaborativeEditing/React/MenuItem.scss
Normal file
22
demos/src/Examples/CollaborativeEditing/React/MenuItem.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.menu-item {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0.4rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
height: 1.75rem;
|
||||
margin-right: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
width: 1.75rem;
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.is-active {
|
||||
background-color: #303030;
|
||||
}
|
||||
}
|
||||
155
demos/src/Examples/CollaborativeEditing/React/index.jsx
Normal file
155
demos/src/Examples/CollaborativeEditing/React/index.jsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import './styles.scss'
|
||||
|
||||
import { TiptapCollabProvider } from '@hocuspocus/provider'
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import { TaskItem, TaskList } from '@tiptap/extension-list'
|
||||
import { CharacterCount } from '@tiptap/extensions'
|
||||
import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
import { variables } from '../../../variables.js'
|
||||
import MenuBar from './MenuBar.jsx'
|
||||
|
||||
const colors = ['#958DF1', '#F98181', '#FBBC88', '#FAF594', '#70CFF8', '#94FADB', '#B9F18D']
|
||||
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 getRandomElement = list => list[Math.floor(Math.random() * list.length)]
|
||||
|
||||
const getRandomRoom = () => {
|
||||
const roomNumbers = variables.collabRooms?.trim()?.split(',') ?? [10, 11, 12]
|
||||
|
||||
return getRandomElement(roomNumbers.map(number => `rooms.${number}`))
|
||||
}
|
||||
const getRandomColor = () => getRandomElement(colors)
|
||||
const getRandomName = () => getRandomElement(names)
|
||||
|
||||
const room = getRandomRoom()
|
||||
|
||||
const ydoc = new Y.Doc()
|
||||
const websocketProvider = new TiptapCollabProvider({
|
||||
appId: '7j9y6m10',
|
||||
name: room,
|
||||
document: ydoc,
|
||||
})
|
||||
|
||||
const getInitialUser = () => {
|
||||
return (
|
||||
JSON.parse(localStorage.getItem('currentUser')) || {
|
||||
name: getRandomName(),
|
||||
color: getRandomColor(),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const [status, setStatus] = useState('connecting')
|
||||
const [currentUser, setCurrentUser] = useState(getInitialUser)
|
||||
const [userCount, setUserCount] = useState(0)
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
undoRedo: false,
|
||||
}),
|
||||
Highlight,
|
||||
TaskList,
|
||||
TaskItem,
|
||||
CharacterCount.configure({
|
||||
limit: 10000,
|
||||
}),
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCaret.configure({
|
||||
provider: websocketProvider,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Update status changes
|
||||
websocketProvider.on('status', event => {
|
||||
setStatus(event.status)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const updateUserCount = () => {
|
||||
setUserCount(editor.storage.collaborationCaret.users.length)
|
||||
}
|
||||
|
||||
updateUserCount()
|
||||
websocketProvider.awareness.on('update', updateUserCount)
|
||||
|
||||
return () => {
|
||||
websocketProvider.awareness.off('update', updateUserCount)
|
||||
}
|
||||
}, [editor])
|
||||
|
||||
// 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') || '').trim().substring(0, 32)
|
||||
|
||||
if (name) {
|
||||
return setCurrentUser({ ...currentUser, name })
|
||||
}
|
||||
}, [currentUser])
|
||||
|
||||
return (
|
||||
<div className="editor">
|
||||
{editor && <MenuBar editor={editor} />}
|
||||
<EditorContent className="editor__content" editor={editor} />
|
||||
<div className="editor__footer">
|
||||
<div className={`editor__status editor__status--${status}`}>
|
||||
{status === 'connected'
|
||||
? `${userCount} user${userCount === 1 ? '' : 's'} online in ${room}`
|
||||
: 'offline'}
|
||||
</div>
|
||||
<div className="editor__name">
|
||||
<button onClick={setName}>{currentUser.name}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
199
demos/src/Examples/CollaborativeEditing/React/styles.scss
Normal file
199
demos/src/Examples/CollaborativeEditing/React/styles.scss
Normal file
@@ -0,0 +1,199 @@
|
||||
/* 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;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #faf594;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid rgba(#0d0d0d, 0.1);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid rgba(#0d0d0d, 0.1);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.editor {
|
||||
background-color: #fff;
|
||||
border: 3px solid #0d0d0d;
|
||||
border-radius: 0.75rem;
|
||||
color: #0d0d0d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 26rem;
|
||||
|
||||
&__header {
|
||||
align-items: center;
|
||||
background: #0d0d0d;
|
||||
border-bottom: 3px solid #0d0d0d;
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-top-right-radius: 0.25rem;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem 1rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
align-items: center;
|
||||
border-top: 3px solid #0d0d0d;
|
||||
color: #0d0d0d;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 12px;
|
||||
flex-wrap: wrap;
|
||||
font-weight: 600;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Some information about the status */
|
||||
&__status {
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
|
||||
&::before {
|
||||
background: rgba(#0d0d0d, 0.5);
|
||||
border-radius: 50%;
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
height: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
&--connecting::before {
|
||||
background: #616161;
|
||||
}
|
||||
|
||||
&--connected::before {
|
||||
background: #b9f18d;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.4rem;
|
||||
color: #0d0d0d;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: #0d0d0d;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Give a remote user a caret */
|
||||
.collaboration-carets__caret {
|
||||
border-left: 1px solid #0d0d0d;
|
||||
border-right: 1px solid #0d0d0d;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
/* Render the username above the caret */
|
||||
.collaboration-carets__label {
|
||||
border-radius: 3px 3px 3px 0;
|
||||
color: #0d0d0d;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
left: -1px;
|
||||
line-height: normal;
|
||||
padding: 0.1rem 0.3rem;
|
||||
position: absolute;
|
||||
top: -1.4em;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
157
demos/src/Examples/CollaborativeEditing/Vue/MenuBar.vue
Normal file
157
demos/src/Examples/CollaborativeEditing/Vue/MenuBar.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-for="(item, index) in items">
|
||||
<div class="divider" v-if="item.type === 'divider'" :key="`divider${index}`" />
|
||||
<menu-item v-else :key="index" v-bind="item" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuItem from './MenuItem.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuItem,
|
||||
},
|
||||
|
||||
props: {
|
||||
editor: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
items: [
|
||||
{
|
||||
icon: 'bold',
|
||||
title: 'Bold',
|
||||
action: () => this.editor.chain().focus().toggleBold().run(),
|
||||
isActive: () => this.editor.isActive('bold'),
|
||||
},
|
||||
{
|
||||
icon: 'italic',
|
||||
title: 'Italic',
|
||||
action: () => this.editor.chain().focus().toggleItalic().run(),
|
||||
isActive: () => this.editor.isActive('italic'),
|
||||
},
|
||||
{
|
||||
icon: 'strikethrough',
|
||||
title: 'Strike',
|
||||
action: () => this.editor.chain().focus().toggleStrike().run(),
|
||||
isActive: () => this.editor.isActive('strike'),
|
||||
},
|
||||
{
|
||||
icon: 'code-view',
|
||||
title: 'Code',
|
||||
action: () => this.editor.chain().focus().toggleCode().run(),
|
||||
isActive: () => this.editor.isActive('code'),
|
||||
},
|
||||
{
|
||||
icon: 'mark-pen-line',
|
||||
title: 'Highlight',
|
||||
action: () => this.editor.chain().focus().toggleHighlight().run(),
|
||||
isActive: () => this.editor.isActive('highlight'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'h-1',
|
||||
title: 'Heading 1',
|
||||
action: () => this.editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
||||
isActive: () => this.editor.isActive('heading', { level: 1 }),
|
||||
},
|
||||
{
|
||||
icon: 'h-2',
|
||||
title: 'Heading 2',
|
||||
action: () => this.editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||
isActive: () => this.editor.isActive('heading', { level: 2 }),
|
||||
},
|
||||
{
|
||||
icon: 'paragraph',
|
||||
title: 'Paragraph',
|
||||
action: () => this.editor.chain().focus().setParagraph().run(),
|
||||
isActive: () => this.editor.isActive('paragraph'),
|
||||
},
|
||||
{
|
||||
icon: 'list-unordered',
|
||||
title: 'Bullet list',
|
||||
action: () => this.editor.chain().focus().toggleBulletList().run(),
|
||||
isActive: () => this.editor.isActive('bulletList'),
|
||||
},
|
||||
{
|
||||
icon: 'list-ordered',
|
||||
title: 'Ordered list',
|
||||
action: () => this.editor.chain().focus().toggleOrderedList().run(),
|
||||
isActive: () => this.editor.isActive('orderedList'),
|
||||
},
|
||||
{
|
||||
icon: 'list-check-2',
|
||||
title: 'Task list',
|
||||
action: () => this.editor.chain().focus().toggleTaskList().run(),
|
||||
isActive: () => this.editor.isActive('taskList'),
|
||||
},
|
||||
{
|
||||
icon: 'code-box-line',
|
||||
title: 'Code block',
|
||||
action: () => this.editor.chain().focus().toggleCodeBlock().run(),
|
||||
isActive: () => this.editor.isActive('codeBlock'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'double-quotes-l',
|
||||
title: 'Blockquote',
|
||||
action: () => this.editor.chain().focus().toggleBlockquote().run(),
|
||||
isActive: () => this.editor.isActive('blockquote'),
|
||||
},
|
||||
{
|
||||
icon: 'separator',
|
||||
title: 'Horizontal rule',
|
||||
action: () => this.editor.chain().focus().setHorizontalRule().run(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'text-wrap',
|
||||
title: 'Hard break',
|
||||
action: () => this.editor.chain().focus().setHardBreak().run(),
|
||||
},
|
||||
{
|
||||
icon: 'format-clear',
|
||||
title: 'Clear format',
|
||||
action: () => this.editor.chain().focus().clearNodes().unsetAllMarks().run(),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: 'arrow-go-back-line',
|
||||
title: 'Undo',
|
||||
action: () => this.editor.chain().focus().undo().run(),
|
||||
},
|
||||
{
|
||||
icon: 'arrow-go-forward-line',
|
||||
title: 'Redo',
|
||||
action: () => this.editor.chain().focus().redo().run(),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.divider {
|
||||
background-color: rgba(#fff, 0.25);
|
||||
height: 1.25rem;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.75rem;
|
||||
width: 1px;
|
||||
}
|
||||
</style>
|
||||
71
demos/src/Examples/CollaborativeEditing/Vue/MenuItem.vue
Normal file
71
demos/src/Examples/CollaborativeEditing/Vue/MenuItem.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<button
|
||||
class="menu-item"
|
||||
:class="{ 'is-active': isActive ? isActive() : null }"
|
||||
@click="action"
|
||||
:title="title"
|
||||
>
|
||||
<svg class="remix">
|
||||
<use :xlink:href="`${remixiconUrl}#ri-${icon}`" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import remixiconUrl from 'remixicon/fonts/remixicon.symbol.svg'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
action: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
|
||||
isActive: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
remixiconUrl,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-item {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0.4rem;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
height: 1.75rem;
|
||||
padding: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
width: 1.75rem;
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.is-active,
|
||||
&:hover {
|
||||
background-color: #303030;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
372
demos/src/Examples/CollaborativeEditing/Vue/index.vue
Normal file
372
demos/src/Examples/CollaborativeEditing/Vue/index.vue
Normal file
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<div class="editor" v-if="editor">
|
||||
<menu-bar class="editor__header" :editor="editor" />
|
||||
<editor-content class="editor__content" :editor="editor" />
|
||||
<div class="editor__footer">
|
||||
<div :class="`editor__status editor__status--${status}`">
|
||||
<template v-if="status === 'connected'">
|
||||
{{ editor.storage.collaborationCaret.users.length }} user{{
|
||||
editor.storage.collaborationCaret.users.length === 1 ? '' : 's'
|
||||
}}
|
||||
online in {{ room }}
|
||||
</template>
|
||||
<template v-else> offline </template>
|
||||
</div>
|
||||
<div class="editor__name">
|
||||
<button @click="setName">
|
||||
{{ currentUser.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TiptapCollabProvider } from '@hocuspocus/provider'
|
||||
import Collaboration from '@tiptap/extension-collaboration'
|
||||
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
|
||||
import Highlight from '@tiptap/extension-highlight'
|
||||
import { TaskItem, TaskList } from '@tiptap/extension-list'
|
||||
import { CharacterCount } from '@tiptap/extensions'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import { Editor, EditorContent } from '@tiptap/vue'
|
||||
import * as Y from 'yjs'
|
||||
|
||||
import { variables } from '../../../variables.js'
|
||||
import MenuBar from './MenuBar.vue'
|
||||
|
||||
const getRandomElement = list => {
|
||||
return list[Math.floor(Math.random() * list.length)]
|
||||
}
|
||||
|
||||
const getRandomRoom = () => {
|
||||
const roomNumbers = variables.collabRooms?.trim()?.split(',') ?? [10, 11, 12]
|
||||
|
||||
return getRandomElement(roomNumbers.map(number => `rooms.${number}`))
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
MenuBar,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentUser: JSON.parse(localStorage.getItem('currentUser')) || {
|
||||
name: this.getRandomName(),
|
||||
color: this.getRandomColor(),
|
||||
},
|
||||
provider: null,
|
||||
editor: null,
|
||||
status: 'connecting',
|
||||
room: getRandomRoom(),
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const ydoc = new Y.Doc()
|
||||
|
||||
this.provider = new TiptapCollabProvider({
|
||||
appId: '7j9y6m10',
|
||||
name: this.room,
|
||||
document: ydoc,
|
||||
})
|
||||
|
||||
this.provider.on('status', event => {
|
||||
this.status = event.status
|
||||
})
|
||||
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
StarterKit.configure({
|
||||
history: false,
|
||||
}),
|
||||
Highlight,
|
||||
TaskList,
|
||||
TaskItem,
|
||||
Collaboration.configure({
|
||||
document: ydoc,
|
||||
}),
|
||||
CollaborationCaret.configure({
|
||||
provider: this.provider,
|
||||
user: this.currentUser,
|
||||
}),
|
||||
CharacterCount.configure({
|
||||
limit: 10000,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
localStorage.setItem('currentUser', JSON.stringify(this.currentUser))
|
||||
},
|
||||
|
||||
methods: {
|
||||
setName() {
|
||||
const name = (window.prompt('Name') || '').trim().substring(0, 32)
|
||||
|
||||
if (name) {
|
||||
return this.updateCurrentUser({
|
||||
name,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
updateCurrentUser(attributes) {
|
||||
this.currentUser = { ...this.currentUser, ...attributes }
|
||||
this.editor.chain().focus().updateUser(this.currentUser).run()
|
||||
|
||||
localStorage.setItem('currentUser', JSON.stringify(this.currentUser))
|
||||
},
|
||||
|
||||
getRandomColor() {
|
||||
return getRandomElement([
|
||||
'#958DF1',
|
||||
'#F98181',
|
||||
'#FBBC88',
|
||||
'#FAF594',
|
||||
'#70CFF8',
|
||||
'#94FADB',
|
||||
'#B9F18D',
|
||||
])
|
||||
},
|
||||
|
||||
getRandomName() {
|
||||
return getRandomElement([
|
||||
'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',
|
||||
])
|
||||
},
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.editor.destroy()
|
||||
this.provider.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.editor {
|
||||
background-color: #fff;
|
||||
border: 3px solid #0d0d0d;
|
||||
border-radius: 0.75rem;
|
||||
color: #0d0d0d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 26rem;
|
||||
|
||||
&__header {
|
||||
align-items: center;
|
||||
background: #0d0d0d;
|
||||
border-bottom: 3px solid #0d0d0d;
|
||||
border-top-left-radius: 0.25rem;
|
||||
border-top-right-radius: 0.25rem;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1 1 auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 1.25rem 1rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
align-items: center;
|
||||
border-top: 3px solid #0d0d0d;
|
||||
color: #0d0d0d;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
justify-content: space-between;
|
||||
padding: 0.25rem 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Some information about the status */
|
||||
&__status {
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
|
||||
&::before {
|
||||
background: rgba(#0d0d0d, 0.5);
|
||||
border-radius: 50%;
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
height: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
&--connecting::before {
|
||||
background: #616161;
|
||||
}
|
||||
|
||||
&--connected::before {
|
||||
background: #b9f18d;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0.4rem;
|
||||
color: #0d0d0d;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: #0d0d0d;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Give a remote user a caret */
|
||||
.collaboration-carets__caret {
|
||||
border-left: 1px solid #0d0d0d;
|
||||
border-right: 1px solid #0d0d0d;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
/* Render the username above the caret */
|
||||
.collaboration-carets__label {
|
||||
border-radius: 3px 3px 3px 0;
|
||||
color: #0d0d0d;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
left: -1px;
|
||||
line-height: normal;
|
||||
padding: 0.1rem 0.3rem;
|
||||
position: absolute;
|
||||
top: -1.4em;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
border-radius: 0.5rem;
|
||||
color: #fff;
|
||||
font-family: 'JetBrainsMono', monospace;
|
||||
padding: 0.75rem 1rem;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: #faf594;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid rgba(#0d0d0d, 0.1);
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid rgba(#0d0d0d, 0.1);
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
ul[data-type='taskList'] {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
> label {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
68
demos/src/Examples/Community/React/MentionList.jsx
Normal file
68
demos/src/Examples/Community/React/MentionList.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import './MentionList.scss'
|
||||
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
|
||||
export const MentionList = forwardRef((props, ref) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
const selectItem = index => {
|
||||
const item = props.items[index]
|
||||
|
||||
if (item) {
|
||||
props.command({ id: item })
|
||||
}
|
||||
}
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length)
|
||||
}
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length)
|
||||
}
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex)
|
||||
}
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items])
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === 'ArrowUp') {
|
||||
upHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
downHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
enterHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className="dropdown-menu">
|
||||
{props.items.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<button
|
||||
className={index === selectedIndex ? 'is-selected' : ''}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="item">No result</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
31
demos/src/Examples/Community/React/MentionList.scss
Normal file
31
demos/src/Examples/Community/React/MentionList.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
/* Dropdown menu */
|
||||
.dropdown-menu {
|
||||
background: var(--white);
|
||||
border: 1px solid var(--gray-1);
|
||||
border-radius: 0.7rem;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
overflow: auto;
|
||||
padding: 0.4rem;
|
||||
position: relative;
|
||||
|
||||
button {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&:hover,
|
||||
&:hover.is-selected {
|
||||
background-color: var(--gray-3);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: var(--gray-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
0
demos/src/Examples/Community/React/index.html
Normal file
0
demos/src/Examples/Community/React/index.html
Normal file
80
demos/src/Examples/Community/React/index.jsx
Normal file
80
demos/src/Examples/Community/React/index.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import './styles.scss'
|
||||
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { CharacterCount } from '@tiptap/extensions'
|
||||
import { EditorContent, useEditor, useEditorState } from '@tiptap/react'
|
||||
import React from 'react'
|
||||
|
||||
import suggestion from './suggestion.js'
|
||||
|
||||
export default () => {
|
||||
const limit = 280
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
CharacterCount.configure({
|
||||
limit,
|
||||
}),
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestion,
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>
|
||||
What do you all think about the new <span data-type="mention" data-id="Winona Ryder"></span> movie?
|
||||
</p>
|
||||
`,
|
||||
})
|
||||
|
||||
const { characterCount } = useEditorState({
|
||||
editor,
|
||||
selector: ctx => {
|
||||
if (!ctx.editor) {
|
||||
return {
|
||||
characterCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
characterCount: ctx.editor.storage.characterCount.characters(),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const percentage = editor ? Math.round((100 / limit) * characterCount) : 0
|
||||
const isAtLimit = characterCount === limit
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorContent editor={editor} />
|
||||
{editor && (
|
||||
<div className={`character-count ${isAtLimit ? 'character-count--warning' : ''}`}>
|
||||
<svg height="20" width="20" viewBox="0 0 20 20">
|
||||
<circle r="10" cx="10" cy="10" fill="#e9ecef" />
|
||||
<circle
|
||||
r="5"
|
||||
cx="10"
|
||||
cy="10"
|
||||
fill="transparent"
|
||||
stroke="currentColor"
|
||||
strokeWidth="10"
|
||||
strokeDasharray={`calc(${percentage} * 31.4 / 100) 31.4`}
|
||||
transform="rotate(-90) translate(-20)"
|
||||
/>
|
||||
<circle r="6" cx="10" cy="10" fill="white" />
|
||||
</svg>
|
||||
{characterCount} / {limit} characters
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
33
demos/src/Examples/Community/React/styles.scss
Normal file
33
demos/src/Examples/Community/React/styles.scss
Normal file
@@ -0,0 +1,33 @@
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mention {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
color: var(--purple);
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Character count */
|
||||
.character-count {
|
||||
align-items: center;
|
||||
color: var(--gray-5);
|
||||
display: flex;
|
||||
font-size: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
margin: 1.5rem;
|
||||
|
||||
svg {
|
||||
color: var(--purple);
|
||||
}
|
||||
|
||||
&--warning,
|
||||
&--warning svg {
|
||||
color: var(--red);
|
||||
}
|
||||
}
|
||||
104
demos/src/Examples/Community/React/suggestion.js
Normal file
104
demos/src/Examples/Community/React/suggestion.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import { computePosition, flip, shift } from '@floating-ui/dom'
|
||||
import { posToDOMRect, ReactRenderer } from '@tiptap/react'
|
||||
|
||||
import { MentionList } from './MentionList.jsx'
|
||||
|
||||
const updatePosition = (editor, element) => {
|
||||
const virtualElement = {
|
||||
getBoundingClientRect: () =>
|
||||
posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
|
||||
}
|
||||
|
||||
computePosition(virtualElement, element, {
|
||||
placement: 'bottom-start',
|
||||
strategy: 'absolute',
|
||||
middleware: [shift(), flip()],
|
||||
}).then(({ x, y, strategy }) => {
|
||||
element.style.width = 'max-content'
|
||||
element.style.position = strategy
|
||||
element.style.left = `${x}px`
|
||||
element.style.top = `${y}px`
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
items: ({ query }) => {
|
||||
return [
|
||||
'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',
|
||||
]
|
||||
.filter(item => item.toLowerCase().startsWith(query.toLowerCase()))
|
||||
.slice(0, 5)
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let reactRenderer
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
reactRenderer = new ReactRenderer(MentionList, {
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
reactRenderer.element.style.position = 'absolute'
|
||||
|
||||
document.body.appendChild(reactRenderer.element)
|
||||
|
||||
updatePosition(props.editor, reactRenderer.element)
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
reactRenderer.updateProps(props)
|
||||
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
updatePosition(props.editor, reactRenderer.element)
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
reactRenderer.destroy()
|
||||
reactRenderer.element.remove()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return reactRenderer.ref?.onKeyDown(props)
|
||||
},
|
||||
|
||||
onExit() {
|
||||
reactRenderer.destroy()
|
||||
reactRenderer.element.remove()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
118
demos/src/Examples/Community/Vue/MentionList.vue
Normal file
118
demos/src/Examples/Community/Vue/MentionList.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="dropdown-menu">
|
||||
<template v-if="items.length">
|
||||
<button
|
||||
:class="{ 'is-selected': index === selectedIndex }"
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
@click="selectItem(index)"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</template>
|
||||
<div class="item" v-else>No result</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
|
||||
command: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: 0,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
items() {
|
||||
this.selectedIndex = 0
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onKeyDown({ event }) {
|
||||
if (event.key === 'ArrowUp') {
|
||||
this.upHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
this.downHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
this.enterHandler()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
upHandler() {
|
||||
this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length
|
||||
},
|
||||
|
||||
downHandler() {
|
||||
this.selectedIndex = (this.selectedIndex + 1) % this.items.length
|
||||
},
|
||||
|
||||
enterHandler() {
|
||||
this.selectItem(this.selectedIndex)
|
||||
},
|
||||
|
||||
selectItem(index) {
|
||||
const item = this.items[index]
|
||||
|
||||
if (item) {
|
||||
this.command({ id: item })
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Dropdown menu */
|
||||
.dropdown-menu {
|
||||
background: var(--white);
|
||||
border: 1px solid var(--gray-1);
|
||||
border-radius: 0.7rem;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
overflow: auto;
|
||||
padding: 0.4rem;
|
||||
position: relative;
|
||||
|
||||
button {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&:hover,
|
||||
&:hover.is-selected {
|
||||
background-color: var(--gray-3);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: var(--gray-2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
0
demos/src/Examples/Community/Vue/index.html
Normal file
0
demos/src/Examples/Community/Vue/index.html
Normal file
122
demos/src/Examples/Community/Vue/index.vue
Normal file
122
demos/src/Examples/Community/Vue/index.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<editor-content :editor="editor" />
|
||||
|
||||
<div
|
||||
v-if="editor"
|
||||
:class="{
|
||||
'character-count': true,
|
||||
'character-count--warning': editor.storage.characterCount.characters() === limit,
|
||||
}"
|
||||
>
|
||||
<svg height="20" width="20" viewBox="0 0 20 20">
|
||||
<circle r="10" cx="10" cy="10" fill="#e9ecef" />
|
||||
<circle
|
||||
r="5"
|
||||
cx="10"
|
||||
cy="10"
|
||||
fill="transparent"
|
||||
stroke="currentColor"
|
||||
stroke-width="10"
|
||||
:stroke-dasharray="`calc(${percentage} * 31.4 / 100) 31.4`"
|
||||
transform="rotate(-90) translate(-20)"
|
||||
/>
|
||||
<circle r="6" cx="10" cy="10" fill="white" />
|
||||
</svg>
|
||||
|
||||
{{ editor.storage.characterCount.characters() }} / {{ limit }} characters
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Document from '@tiptap/extension-document'
|
||||
import Mention from '@tiptap/extension-mention'
|
||||
import Paragraph from '@tiptap/extension-paragraph'
|
||||
import Text from '@tiptap/extension-text'
|
||||
import { CharacterCount } from '@tiptap/extensions'
|
||||
import { Editor, EditorContent } from '@tiptap/vue'
|
||||
|
||||
import suggestion from './suggestion.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EditorContent,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
editor: null,
|
||||
limit: 280,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.editor = new Editor({
|
||||
extensions: [
|
||||
Document,
|
||||
Paragraph,
|
||||
Text,
|
||||
CharacterCount.configure({
|
||||
limit: this.limit,
|
||||
}),
|
||||
Mention.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'mention',
|
||||
},
|
||||
suggestion,
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<p>
|
||||
What do you all think about the new <span data-type="mention" data-id="Winona Ryder"></span> movie?
|
||||
</p>
|
||||
`,
|
||||
})
|
||||
},
|
||||
|
||||
computed: {
|
||||
percentage() {
|
||||
return Math.round((100 / this.limit) * this.editor.storage.characterCount.characters())
|
||||
},
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.editor.destroy()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* Basic editor styles */
|
||||
.tiptap {
|
||||
:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mention {
|
||||
background-color: var(--purple-light);
|
||||
border-radius: 0.4rem;
|
||||
box-decoration-break: clone;
|
||||
color: var(--purple);
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Character count */
|
||||
.character-count {
|
||||
align-items: center;
|
||||
color: var(--gray-5);
|
||||
display: flex;
|
||||
font-size: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
margin: 1.5rem;
|
||||
|
||||
svg {
|
||||
color: var(--purple);
|
||||
}
|
||||
|
||||
&--warning,
|
||||
&--warning svg {
|
||||
color: var(--red);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
demos/src/Examples/Community/Vue/suggestion.js
Normal file
108
demos/src/Examples/Community/Vue/suggestion.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import { computePosition, flip, shift } from '@floating-ui/dom'
|
||||
import { posToDOMRect, VueRenderer } from '@tiptap/vue'
|
||||
|
||||
import MentionList from './MentionList.vue'
|
||||
|
||||
const updatePosition = (editor, element) => {
|
||||
const virtualElement = {
|
||||
getBoundingClientRect: () =>
|
||||
posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
|
||||
}
|
||||
|
||||
computePosition(virtualElement, element, {
|
||||
placement: 'bottom-start',
|
||||
strategy: 'absolute',
|
||||
middleware: [shift(), flip()],
|
||||
}).then(({ x, y, strategy }) => {
|
||||
element.style.width = 'max-content'
|
||||
element.style.position = strategy
|
||||
element.style.left = `${x}px`
|
||||
element.style.top = `${y}px`
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
items: ({ query }) => {
|
||||
return [
|
||||
'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',
|
||||
]
|
||||
.filter(item => item.toLowerCase().startsWith(query.toLowerCase()))
|
||||
.slice(0, 5)
|
||||
},
|
||||
|
||||
render: () => {
|
||||
let component
|
||||
|
||||
return {
|
||||
onStart: props => {
|
||||
component = new VueRenderer(MentionList, {
|
||||
// using vue 2:
|
||||
// parent: this,
|
||||
// propsData: props,
|
||||
props,
|
||||
editor: props.editor,
|
||||
})
|
||||
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
component.element.style.position = 'absolute'
|
||||
|
||||
document.body.appendChild(component.element)
|
||||
|
||||
updatePosition(props.editor, component.element)
|
||||
},
|
||||
|
||||
onUpdate(props) {
|
||||
component.updateProps(props)
|
||||
|
||||
if (!props.clientRect) {
|
||||
return
|
||||
}
|
||||
|
||||
updatePosition(props.editor, component.element)
|
||||
},
|
||||
|
||||
onKeyDown(props) {
|
||||
if (props.event.key === 'Escape') {
|
||||
component.destroy()
|
||||
component.element.remove()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return component.ref?.onKeyDown(props)
|
||||
},
|
||||
|
||||
onExit() {
|
||||
component.destroy()
|
||||
component.element.remove()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
60
demos/src/Examples/Community/index.spec.ts
Normal file
60
demos/src/Examples/Community/index.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
import { getEditor } from '../../../test/helpers.js'
|
||||
|
||||
const demoName = 'Community'
|
||||
const frameworkPaths = ['React', 'Vue']
|
||||
const demoPath = '/src/Examples'
|
||||
|
||||
test.describe(`${demoPath}/${demoName}`, () => {
|
||||
frameworkPaths.forEach(frameworkPath => {
|
||||
const fullDemoPath = `${demoPath}/${demoName}/${frameworkPath}/`
|
||||
|
||||
test.describe(`${frameworkPath}`, () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(fullDemoPath)
|
||||
await getEditor(page)
|
||||
})
|
||||
|
||||
test('should count the characters correctly', async ({ page }) => {
|
||||
await expect(page.locator('.character-count')).toContainText('44 / 280 characters')
|
||||
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.click()
|
||||
await editor.evaluate((el: any) => el.editor.commands.focus('end'))
|
||||
await page.keyboard.type(' Hello World')
|
||||
|
||||
await expect(page.locator('.character-count')).toContainText('56 / 280 characters')
|
||||
|
||||
await editor.evaluate((el: any) => {
|
||||
el.editor.chain().focus().clearContent().run()
|
||||
})
|
||||
await page.keyboard.type('Hello World')
|
||||
|
||||
await expect(page.locator('.character-count')).toContainText('11 / 280 characters')
|
||||
})
|
||||
|
||||
test('should mention a user', async ({ page }) => {
|
||||
const editor = await getEditor(page)
|
||||
|
||||
await editor.evaluate((el: any) => {
|
||||
el.editor.chain().focus().clearContent().run()
|
||||
})
|
||||
await editor.click()
|
||||
await editor.type('@')
|
||||
|
||||
const dropdown = page.locator('.dropdown-menu')
|
||||
await expect(dropdown).toBeVisible()
|
||||
|
||||
const firstButton = dropdown.locator('button').first()
|
||||
const name = await firstButton.innerText()
|
||||
|
||||
await firstButton.click()
|
||||
|
||||
await expect(page.locator('.tiptap')).toHaveText(`@${name} `)
|
||||
await expect(page.locator('.character-count')).toContainText('2 / 280 characters')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
0
demos/src/Examples/CustomDocument/React/index.html
Normal file
0
demos/src/Examples/CustomDocument/React/index.html
Normal file
44
demos/src/Examples/CustomDocument/React/index.jsx
Normal file
44
demos/src/Examples/CustomDocument/React/index.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import './styles.scss'
|
||||
|
||||
import Document from '@tiptap/extension-document'
|
||||
import { Placeholder } from '@tiptap/extensions'
|
||||
import { EditorContent, useEditor } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import React from 'react'
|
||||
|
||||
const CustomDocument = Document.extend({
|
||||
content: 'heading block*',
|
||||
})
|
||||
|
||||
export default () => {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
CustomDocument,
|
||||
StarterKit.configure({
|
||||
document: false,
|
||||
trailingNode: {
|
||||
node: 'paragraph',
|
||||
},
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === 'heading') {
|
||||
return 'What’s the title?'
|
||||
}
|
||||
|
||||
return 'Can you add some further context?'
|
||||
},
|
||||
}),
|
||||
],
|
||||
content: `
|
||||
<h1>
|
||||
It’ll always have a heading …
|
||||
</h1>
|
||||
<p>
|
||||
… if you pass a custom document. That’s the beauty of having full control over the schema.
|
||||
</p>
|
||||
`,
|
||||
})
|
||||
|
||||
return <EditorContent editor={editor} />
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user