mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
Collaboration editor diff (#2356)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
bfd120b887
commit
b756b4a035
@ -197,8 +197,11 @@ specifiers:
|
|||||||
'@rushstack/heft': ^0.47.9
|
'@rushstack/heft': ^0.47.9
|
||||||
'@rushstack/heft-jest-plugin': ^0.3.16
|
'@rushstack/heft-jest-plugin': ^0.3.16
|
||||||
'@tiptap/core': ~2.0.0-beta.199
|
'@tiptap/core': ~2.0.0-beta.199
|
||||||
|
'@tiptap/extension-code-block': ~2.0.0-beta.200
|
||||||
'@tiptap/extension-collaboration': ~2.0.0-beta.199
|
'@tiptap/extension-collaboration': ~2.0.0-beta.199
|
||||||
'@tiptap/extension-collaboration-cursor': ~2.0.0-beta.199
|
'@tiptap/extension-collaboration-cursor': ~2.0.0-beta.199
|
||||||
|
'@tiptap/extension-gapcursor': ~2.0.0-beta.200
|
||||||
|
'@tiptap/extension-heading': ~2.0.0-beta.200
|
||||||
'@tiptap/extension-highlight': ~2.0.0-beta.199
|
'@tiptap/extension-highlight': ~2.0.0-beta.199
|
||||||
'@tiptap/extension-link': ~2.0.0-beta.199
|
'@tiptap/extension-link': ~2.0.0-beta.199
|
||||||
'@tiptap/extension-mention': ~2.0.0-beta.199
|
'@tiptap/extension-mention': ~2.0.0-beta.199
|
||||||
@ -213,6 +216,7 @@ specifiers:
|
|||||||
'@types/cors': ^2.8.12
|
'@types/cors': ^2.8.12
|
||||||
'@types/crypto-js': ^4.1.1
|
'@types/crypto-js': ^4.1.1
|
||||||
'@types/deep-equal': ^1.0.1
|
'@types/deep-equal': ^1.0.1
|
||||||
|
'@types/diff': ~5.0.2
|
||||||
'@types/express': ^4.17.13
|
'@types/express': ^4.17.13
|
||||||
'@types/express-fileupload': ^1.1.7
|
'@types/express-fileupload': ^1.1.7
|
||||||
'@types/faker': ~5.5.9
|
'@types/faker': ~5.5.9
|
||||||
@ -248,6 +252,7 @@ specifiers:
|
|||||||
css-loader: ^5.2.1
|
css-loader: ^5.2.1
|
||||||
csv-parse: ~5.1.0
|
csv-parse: ~5.1.0
|
||||||
deep-equal: ^2.0.5
|
deep-equal: ^2.0.5
|
||||||
|
diff: ~5.1.0
|
||||||
dotenv: ~16.0.0
|
dotenv: ~16.0.0
|
||||||
dotenv-webpack: ^7.0.2
|
dotenv-webpack: ^7.0.2
|
||||||
elastic-apm-node: ~3.26.0
|
elastic-apm-node: ~3.26.0
|
||||||
@ -288,9 +293,13 @@ specifiers:
|
|||||||
postcss-loader: ^6.1.0
|
postcss-loader: ^6.1.0
|
||||||
prettier: ^2.7.1
|
prettier: ^2.7.1
|
||||||
prettier-plugin-svelte: ^2.8.0
|
prettier-plugin-svelte: ^2.8.0
|
||||||
|
prosemirror-changeset: ~2.2.0
|
||||||
prosemirror-collab: ~1.3.0
|
prosemirror-collab: ~1.3.0
|
||||||
|
prosemirror-model: ~1.18.1
|
||||||
prosemirror-state: ~1.4.1
|
prosemirror-state: ~1.4.1
|
||||||
prosemirror-transform: ~1.7.0
|
prosemirror-transform: ~1.7.0
|
||||||
|
prosemirror-view: ~1.29.0
|
||||||
|
rfc6902: ~5.0.1
|
||||||
sass: ^1.53.0
|
sass: ^1.53.0
|
||||||
sass-loader: ^12.1.0
|
sass-loader: ^12.1.0
|
||||||
sharp: ~0.30.7
|
sharp: ~0.30.7
|
||||||
@ -515,13 +524,16 @@ dependencies:
|
|||||||
'@rushstack/heft': 0.47.9
|
'@rushstack/heft': 0.47.9
|
||||||
'@rushstack/heft-jest-plugin': 0.3.16_e810491d602256cb9138da3f42d797a2
|
'@rushstack/heft-jest-plugin': 0.3.16_e810491d602256cb9138da3f42d797a2
|
||||||
'@tiptap/core': 2.0.0-beta.199
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
'@tiptap/extension-collaboration': 2.0.0-beta.199_8e74c563ac0499ca22e9a6d9d9b7c0ed
|
'@tiptap/extension-code-block': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-collaboration-cursor': 2.0.0-beta.199_04364fb270bfa949258a810b562e2e26
|
'@tiptap/extension-collaboration': 2.0.0-beta.199_a496b31b65360f0ca30cf7e3de4ce96e
|
||||||
|
'@tiptap/extension-collaboration-cursor': 2.0.0-beta.199_17c6db99c9b1488ff14e755dd0559594
|
||||||
|
'@tiptap/extension-gapcursor': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
|
||||||
|
'@tiptap/extension-heading': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-highlight': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-highlight': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-link': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-link': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-mention': 2.0.0-beta.199_c8f353cb3abc70247a8f6c56ebb87d62
|
'@tiptap/extension-mention': 2.0.0-beta.199_c8f353cb3abc70247a8f6c56ebb87d62
|
||||||
'@tiptap/extension-placeholder': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-placeholder': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-task-item': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-task-item': 2.0.0-beta.199_ec97b388f910dbe754a14ff2f0072b88
|
||||||
'@tiptap/extension-task-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-task-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-typography': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-typography': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/starter-kit': 2.0.0-beta.199
|
'@tiptap/starter-kit': 2.0.0-beta.199
|
||||||
@ -531,6 +543,7 @@ dependencies:
|
|||||||
'@types/cors': 2.8.12
|
'@types/cors': 2.8.12
|
||||||
'@types/crypto-js': 4.1.1
|
'@types/crypto-js': 4.1.1
|
||||||
'@types/deep-equal': 1.0.1
|
'@types/deep-equal': 1.0.1
|
||||||
|
'@types/diff': 5.0.2
|
||||||
'@types/express': 4.17.13
|
'@types/express': 4.17.13
|
||||||
'@types/express-fileupload': 1.2.2
|
'@types/express-fileupload': 1.2.2
|
||||||
'@types/faker': 5.5.9
|
'@types/faker': 5.5.9
|
||||||
@ -566,6 +579,7 @@ dependencies:
|
|||||||
css-loader: 5.2.7_webpack@5.73.0
|
css-loader: 5.2.7_webpack@5.73.0
|
||||||
csv-parse: 5.1.0
|
csv-parse: 5.1.0
|
||||||
deep-equal: 2.0.5
|
deep-equal: 2.0.5
|
||||||
|
diff: 5.1.0
|
||||||
dotenv: 16.0.1
|
dotenv: 16.0.1
|
||||||
dotenv-webpack: 7.1.1_webpack@5.73.0
|
dotenv-webpack: 7.1.1_webpack@5.73.0
|
||||||
elastic-apm-node: 3.26.0
|
elastic-apm-node: 3.26.0
|
||||||
@ -606,9 +620,13 @@ dependencies:
|
|||||||
postcss-loader: 6.2.1_postcss@8.4.14+webpack@5.73.0
|
postcss-loader: 6.2.1_postcss@8.4.14+webpack@5.73.0
|
||||||
prettier: 2.7.1
|
prettier: 2.7.1
|
||||||
prettier-plugin-svelte: 2.8.0_prettier@2.7.1+svelte@3.48.0
|
prettier-plugin-svelte: 2.8.0_prettier@2.7.1+svelte@3.48.0
|
||||||
|
prosemirror-changeset: 2.2.0
|
||||||
prosemirror-collab: 1.3.0
|
prosemirror-collab: 1.3.0
|
||||||
|
prosemirror-model: 1.18.1
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
prosemirror-transform: 1.7.0
|
prosemirror-transform: 1.7.0
|
||||||
|
prosemirror-view: 1.29.0
|
||||||
|
rfc6902: 5.0.1
|
||||||
sass: 1.53.0
|
sass: 1.53.0
|
||||||
sass-loader: 12.6.0_sass@1.53.0+webpack@5.73.0
|
sass-loader: 12.6.0_sass@1.53.0+webpack@5.73.0
|
||||||
sharp: 0.30.7
|
sharp: 0.30.7
|
||||||
@ -1612,7 +1630,7 @@ packages:
|
|||||||
prosemirror-schema-list: 1.2.2
|
prosemirror-schema-list: 1.2.2
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
prosemirror-transform: 1.7.0
|
prosemirror-transform: 1.7.0
|
||||||
prosemirror-view: 1.28.3
|
prosemirror-view: 1.29.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-blockquote/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
/@tiptap/extension-blockquote/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
||||||
@ -1639,8 +1657,8 @@ packages:
|
|||||||
'@tiptap/core': 2.0.0-beta.199
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-code-block/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
/@tiptap/extension-code-block/2.0.0-beta.202_@tiptap+core@2.0.0-beta.199:
|
||||||
resolution: {integrity: sha512-ZfftYE1kHA2pD46hXDkeYd1vuxp3bJLS854B2yHfw1cp3JVDjMXzm4Mzg7zLfr+YV1dT/N/fUfdCg38fqEUCyA==}
|
resolution: {integrity: sha512-tfK9khIroGjsXQvk2K/9z1/UyQrB4+zghkjyK1xikzRmhgfOeqQzA0TDrFrz7ywFXmSFQ7GnnYAp+RW6r6wyUg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1656,13 +1674,13 @@ packages:
|
|||||||
'@tiptap/core': 2.0.0-beta.199
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-collaboration-cursor/2.0.0-beta.199_04364fb270bfa949258a810b562e2e26:
|
/@tiptap/extension-collaboration-cursor/2.0.0-beta.199_17c6db99c9b1488ff14e755dd0559594:
|
||||||
resolution: {integrity: sha512-3LOfuSFYZz5CQOm+3rCKSm/hKXfP+KgW6aDYDfDTI3IKtMkC9b8FYa2ZoW5Qj/H1H+2m0+8aT6p9RIybtVi9jQ==}
|
resolution: {integrity: sha512-3LOfuSFYZz5CQOm+3rCKSm/hKXfP+KgW6aDYDfDTI3IKtMkC9b8FYa2ZoW5Qj/H1H+2m0+8aT6p9RIybtVi9jQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.199
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
y-prosemirror: 1.0.20_aeff9fb8f06ec077e7a357cd76186471
|
y-prosemirror: 1.0.20_0101a562c8137253afa9fb877870d27d
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- prosemirror-model
|
- prosemirror-model
|
||||||
- prosemirror-state
|
- prosemirror-state
|
||||||
@ -1671,21 +1689,6 @@ packages:
|
|||||||
- yjs
|
- yjs
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-collaboration/2.0.0-beta.199_8e74c563ac0499ca22e9a6d9d9b7c0ed:
|
|
||||||
resolution: {integrity: sha512-ub3doQvy7o7YLwLDz8B/LK7zZ6aEy19C36FJbSXfr93Ws8FhVy2PYvgwBnRYPIifXrS8FVkNL5ULjsH+QlEd5Q==}
|
|
||||||
peerDependencies:
|
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
|
||||||
dependencies:
|
|
||||||
'@tiptap/core': 2.0.0-beta.199
|
|
||||||
prosemirror-state: 1.4.1
|
|
||||||
y-prosemirror: 1.0.20_aeff9fb8f06ec077e7a357cd76186471
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- prosemirror-model
|
|
||||||
- prosemirror-view
|
|
||||||
- y-protocols
|
|
||||||
- yjs
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@tiptap/extension-collaboration/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
/@tiptap/extension-collaboration/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
||||||
resolution: {integrity: sha512-ub3doQvy7o7YLwLDz8B/LK7zZ6aEy19C36FJbSXfr93Ws8FhVy2PYvgwBnRYPIifXrS8FVkNL5ULjsH+QlEd5Q==}
|
resolution: {integrity: sha512-ub3doQvy7o7YLwLDz8B/LK7zZ6aEy19C36FJbSXfr93Ws8FhVy2PYvgwBnRYPIifXrS8FVkNL5ULjsH+QlEd5Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1701,6 +1704,21 @@ packages:
|
|||||||
- yjs
|
- yjs
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tiptap/extension-collaboration/2.0.0-beta.199_a496b31b65360f0ca30cf7e3de4ce96e:
|
||||||
|
resolution: {integrity: sha512-ub3doQvy7o7YLwLDz8B/LK7zZ6aEy19C36FJbSXfr93Ws8FhVy2PYvgwBnRYPIifXrS8FVkNL5ULjsH+QlEd5Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
|
dependencies:
|
||||||
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
|
prosemirror-state: 1.4.1
|
||||||
|
y-prosemirror: 1.0.20_0101a562c8137253afa9fb877870d27d
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- prosemirror-model
|
||||||
|
- prosemirror-view
|
||||||
|
- y-protocols
|
||||||
|
- yjs
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-document/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
/@tiptap/extension-document/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
||||||
resolution: {integrity: sha512-l/3k9N2O4wIMQoN/SM3aIBwOhZ2KRxQoqGJfsbAUUwBURBDiT4N2VZaNiJC/w3xCVQXIxHSIlqtm9ZBcZeiH/Q==}
|
resolution: {integrity: sha512-l/3k9N2O4wIMQoN/SM3aIBwOhZ2KRxQoqGJfsbAUUwBURBDiT4N2VZaNiJC/w3xCVQXIxHSIlqtm9ZBcZeiH/Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1718,8 +1736,8 @@ packages:
|
|||||||
prosemirror-dropcursor: 1.5.0
|
prosemirror-dropcursor: 1.5.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-gapcursor/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
/@tiptap/extension-gapcursor/2.0.0-beta.202_@tiptap+core@2.0.0-beta.199:
|
||||||
resolution: {integrity: sha512-0TDpDfDyay+IbD+wJMsBJ2c0Cq0NtllUOxbi0NPjjWW94Jrvs1yqUSzX4Qp9m5MW8qP24IV6krgZBM1JyQc6ng==}
|
resolution: {integrity: sha512-jOPMPPnTfVuc5YpFTcQM42/cg1J3+OeHitYb1/vBMpaNinVijuafsK14xDoVP8+sydKVgtBzYkfP/faN82I9iA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1735,8 +1753,8 @@ packages:
|
|||||||
'@tiptap/core': 2.0.0-beta.199
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-heading/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
/@tiptap/extension-heading/2.0.0-beta.202_@tiptap+core@2.0.0-beta.199:
|
||||||
resolution: {integrity: sha512-WGQ7ET2TBpldrD8JX37OXHXq05LU3OWItIVBs9nKGh4otZTUwPtwfOyMlFfA+IMfQif+ilwLGvUC6EHOw/LwxQ==}
|
resolution: {integrity: sha512-sF271jSWHgtoJLDNFLS7eyUcUStl7mBDQNJIENWVI+lFu2Ax8GmO7AoB74Q6L5Zaw4h73L6TAvaafHIXurz7tA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -1832,7 +1850,7 @@ packages:
|
|||||||
'@tiptap/core': 2.0.0-beta.199
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
prosemirror-model: 1.18.1
|
prosemirror-model: 1.18.1
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
prosemirror-view: 1.28.3
|
prosemirror-view: 1.29.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-strike/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
/@tiptap/extension-strike/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
||||||
@ -1852,6 +1870,16 @@ packages:
|
|||||||
'@tiptap/core': 2.0.0-beta.199
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@tiptap/extension-task-item/2.0.0-beta.199_ec97b388f910dbe754a14ff2f0072b88:
|
||||||
|
resolution: {integrity: sha512-dvMgXr4B/V8dYvksLtbby3R2wM9zk3xdkOBuohTLQuRq73dK12Bh/h5xrl4cey8i/2tQBWgUfFiGVPsEUJjQCQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
|
prosemirror-model: ^1.18.1
|
||||||
|
dependencies:
|
||||||
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
|
prosemirror-model: 1.18.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-task-list/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
/@tiptap/extension-task-list/2.0.0-beta.199_@tiptap+core@2.0.0-beta.199:
|
||||||
resolution: {integrity: sha512-//1bw2Wd4IYKxYLw3iaxBcd0/iFw1Jwc/Q1j41oBc5QTZDuRxhEO/5Gjy1UmEZsWhsH39bS2za4uMBX4DbHBUQ==}
|
resolution: {integrity: sha512-//1bw2Wd4IYKxYLw3iaxBcd0/iFw1Jwc/Q1j41oBc5QTZDuRxhEO/5Gjy1UmEZsWhsH39bS2za4uMBX4DbHBUQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1884,12 +1912,12 @@ packages:
|
|||||||
'@tiptap/extension-bold': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-bold': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-bullet-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-bullet-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-code': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-code': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-code-block': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-code-block': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-document': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-document': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-dropcursor': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-dropcursor': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-gapcursor': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-gapcursor': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-hard-break': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-hard-break': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-heading': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-heading': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-history': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-history': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-horizontal-rule': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-horizontal-rule': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-italic': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-italic': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
@ -1908,7 +1936,7 @@ packages:
|
|||||||
'@tiptap/core': 2.0.0-beta.199
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
prosemirror-model: 1.18.1
|
prosemirror-model: 1.18.1
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
prosemirror-view: 1.28.3
|
prosemirror-view: 1.29.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tootallnate/once/1.1.2:
|
/@tootallnate/once/1.1.2:
|
||||||
@ -2052,6 +2080,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==}
|
resolution: {integrity: sha512-mMUu4nWHLBlHtxXY17Fg6+ucS/MnndyOWyOe7MmwkoMYxvfQU2ajtRaEvqSUv+aVkMqH/C0NCI8UoVfRNQ10yg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/diff/5.0.2:
|
||||||
|
resolution: {integrity: sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/email-addresses/3.0.0:
|
/@types/email-addresses/3.0.0:
|
||||||
resolution: {integrity: sha512-jGUOSgpOEWhTH4tMCj56NZenkzER259nJ5NGRvxXld3X7Lai/lxC3QNfDM0rVGMkj+WhANMpvIf195tgwnE7wQ==}
|
resolution: {integrity: sha512-jGUOSgpOEWhTH4tMCj56NZenkzER259nJ5NGRvxXld3X7Lai/lxC3QNfDM0rVGMkj+WhANMpvIf195tgwnE7wQ==}
|
||||||
deprecated: This is a stub types definition for email-addresses (https://github.com/jackbowman/email-addresses). email-addresses provides its own type definitions, so you don't need @types/email-addresses installed!
|
deprecated: This is a stub types definition for email-addresses (https://github.com/jackbowman/email-addresses). email-addresses provides its own type definitions, so you don't need @types/email-addresses installed!
|
||||||
@ -4098,6 +4130,11 @@ packages:
|
|||||||
engines: {node: '>=0.3.1'}
|
engines: {node: '>=0.3.1'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/diff/5.1.0:
|
||||||
|
resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
|
||||||
|
engines: {node: '>=0.3.1'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/diffie-hellman/5.0.3:
|
/diffie-hellman/5.0.3:
|
||||||
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
|
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7949,6 +7986,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==}
|
resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/prosemirror-changeset/2.2.0:
|
||||||
|
resolution: {integrity: sha512-QM7ohGtkpVpwVGmFb8wqVhaz9+6IUXcIQBGZ81YNAKYuHiFJ1ShvSzab4pKqTinJhwciZbrtBEk/2WsqSt2PYg==}
|
||||||
|
dependencies:
|
||||||
|
prosemirror-transform: 1.7.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/prosemirror-collab/1.3.0:
|
/prosemirror-collab/1.3.0:
|
||||||
resolution: {integrity: sha512-+S/IJ69G2cUu2IM5b3PBekuxs94HO1CxJIWOFrLQXUaUDKL/JfBx+QcH31ldBlBXyDEUl+k3Vltfi1E1MKp2mA==}
|
resolution: {integrity: sha512-+S/IJ69G2cUu2IM5b3PBekuxs94HO1CxJIWOFrLQXUaUDKL/JfBx+QcH31ldBlBXyDEUl+k3Vltfi1E1MKp2mA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -7968,7 +8011,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
prosemirror-transform: 1.7.0
|
prosemirror-transform: 1.7.0
|
||||||
prosemirror-view: 1.28.3
|
prosemirror-view: 1.29.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/prosemirror-gapcursor/1.3.1:
|
/prosemirror-gapcursor/1.3.1:
|
||||||
@ -7977,7 +8020,7 @@ packages:
|
|||||||
prosemirror-keymap: 1.2.0
|
prosemirror-keymap: 1.2.0
|
||||||
prosemirror-model: 1.18.1
|
prosemirror-model: 1.18.1
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
prosemirror-view: 1.28.3
|
prosemirror-view: 1.29.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/prosemirror-history/1.3.0:
|
/prosemirror-history/1.3.0:
|
||||||
@ -8022,8 +8065,8 @@ packages:
|
|||||||
prosemirror-model: 1.18.1
|
prosemirror-model: 1.18.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/prosemirror-view/1.28.3:
|
/prosemirror-view/1.29.0:
|
||||||
resolution: {integrity: sha512-YnJxLRzIaCNEt3VKiy+PBxtpwsCbjrfiBKIgHJeqbKhdeP8bU2qL4ngdGmxp9K4+06cZG5bE9vipuhP+KUl+BQ==}
|
resolution: {integrity: sha512-bifVd5aD9uCNtpLL1AyhquG/cVbNZSv+ALBxTEGYv51a6OHDhq+aOuzqq4MermNdeBdT+5uyURXCALgzk0EN5g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-model: 1.18.1
|
prosemirror-model: 1.18.1
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
@ -8353,6 +8396,10 @@ packages:
|
|||||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/rfc6902/5.0.1:
|
||||||
|
resolution: {integrity: sha512-tYGfLpKIq9X7lrt4o3IkD9w9bpeAtsejfAqWNR98AoxfTsZqCepKa8eDlRiX8QMiCOD9vMx0/YbKLx0G1nPi5w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/rfdc/1.3.0:
|
/rfdc/1.3.0:
|
||||||
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
|
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -10000,7 +10047,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
/y-prosemirror/1.0.20_aeff9fb8f06ec077e7a357cd76186471:
|
/y-prosemirror/1.0.20_0101a562c8137253afa9fb877870d27d:
|
||||||
resolution: {integrity: sha512-LVMtu3qWo0emeYiP+0jgNcvZkqhzE/otOoro+87q0iVKxy/sMKuiJZnokfJdR4cn9qKx0Un5fIxXqbAlR2bFkA==}
|
resolution: {integrity: sha512-LVMtu3qWo0emeYiP+0jgNcvZkqhzE/otOoro+87q0iVKxy/sMKuiJZnokfJdR4cn9qKx0Un5fIxXqbAlR2bFkA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
prosemirror-model: ^1.7.1
|
prosemirror-model: ^1.7.1
|
||||||
@ -10010,7 +10057,9 @@ packages:
|
|||||||
yjs: ^13.3.2
|
yjs: ^13.3.2
|
||||||
dependencies:
|
dependencies:
|
||||||
lib0: 0.2.52
|
lib0: 0.2.52
|
||||||
|
prosemirror-model: 1.18.1
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
|
prosemirror-view: 1.29.0
|
||||||
y-protocols: 1.0.5
|
y-protocols: 1.0.5
|
||||||
yjs: 13.5.42
|
yjs: 13.5.42
|
||||||
dev: false
|
dev: false
|
||||||
@ -14491,25 +14540,30 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/text-editor.tgz_13653a9d42656433759444fbd2afc848:
|
file:projects/text-editor.tgz_13653a9d42656433759444fbd2afc848:
|
||||||
resolution: {integrity: sha512-nR7pmL9ssos3gvV+tv6lfjcG6IldNayfbzZFp5678fazvi5PDxm4FPboUlKFhwBIVFacbmc2GiP+H7PQASn8jg==, tarball: file:projects/text-editor.tgz}
|
resolution: {integrity: sha512-fTQmayD5wtd7FqRklhl13kDS5Y9tyBXfC5FAoJP+xUHbNk61+FzgKnmHFOjbsJVvgFVkag98AJA9xb9nHfjFzw==, tarball: file:projects/text-editor.tgz}
|
||||||
id: file:projects/text-editor.tgz
|
id: file:projects/text-editor.tgz
|
||||||
name: '@rush-temp/text-editor'
|
name: '@rush-temp/text-editor'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.199
|
'@tiptap/core': 2.0.0-beta.199
|
||||||
'@tiptap/extension-collaboration': 2.0.0-beta.199_8e74c563ac0499ca22e9a6d9d9b7c0ed
|
'@tiptap/extension-code-block': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-collaboration-cursor': 2.0.0-beta.199_04364fb270bfa949258a810b562e2e26
|
'@tiptap/extension-collaboration': 2.0.0-beta.199_a496b31b65360f0ca30cf7e3de4ce96e
|
||||||
|
'@tiptap/extension-collaboration-cursor': 2.0.0-beta.199_17c6db99c9b1488ff14e755dd0559594
|
||||||
|
'@tiptap/extension-gapcursor': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
|
||||||
|
'@tiptap/extension-heading': 2.0.0-beta.202_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-highlight': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-highlight': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-link': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-link': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-mention': 2.0.0-beta.199_c8f353cb3abc70247a8f6c56ebb87d62
|
'@tiptap/extension-mention': 2.0.0-beta.199_c8f353cb3abc70247a8f6c56ebb87d62
|
||||||
'@tiptap/extension-placeholder': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-placeholder': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-task-item': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-task-item': 2.0.0-beta.199_ec97b388f910dbe754a14ff2f0072b88
|
||||||
'@tiptap/extension-task-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-task-list': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/extension-typography': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/extension-typography': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
'@tiptap/starter-kit': 2.0.0-beta.199
|
'@tiptap/starter-kit': 2.0.0-beta.199
|
||||||
'@tiptap/suggestion': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
'@tiptap/suggestion': 2.0.0-beta.199_@tiptap+core@2.0.0-beta.199
|
||||||
|
'@types/diff': 5.0.2
|
||||||
'@typescript-eslint/eslint-plugin': 5.42.0_8b6083565a963e7484743e25607ce89c
|
'@typescript-eslint/eslint-plugin': 5.42.0_8b6083565a963e7484743e25607ce89c
|
||||||
'@typescript-eslint/parser': 5.42.0_eslint@8.26.0+typescript@4.7.4
|
'@typescript-eslint/parser': 5.42.0_eslint@8.26.0+typescript@4.7.4
|
||||||
|
diff: 5.1.0
|
||||||
emoji-regex: 10.1.0
|
emoji-regex: 10.1.0
|
||||||
eslint: 8.26.0
|
eslint: 8.26.0
|
||||||
eslint-config-standard-with-typescript: 23.0.0_35db0d754f34ccffcc0e5a361183072e
|
eslint-config-standard-with-typescript: 23.0.0_35db0d754f34ccffcc0e5a361183072e
|
||||||
@ -14520,9 +14574,13 @@ packages:
|
|||||||
eslint-plugin-svelte3: 4.0.0_eslint@8.26.0+svelte@3.48.0
|
eslint-plugin-svelte3: 4.0.0_eslint@8.26.0+svelte@3.48.0
|
||||||
prettier: 2.7.1
|
prettier: 2.7.1
|
||||||
prettier-plugin-svelte: 2.8.0_prettier@2.7.1+svelte@3.48.0
|
prettier-plugin-svelte: 2.8.0_prettier@2.7.1+svelte@3.48.0
|
||||||
|
prosemirror-changeset: 2.2.0
|
||||||
prosemirror-collab: 1.3.0
|
prosemirror-collab: 1.3.0
|
||||||
|
prosemirror-model: 1.18.1
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
prosemirror-transform: 1.7.0
|
prosemirror-transform: 1.7.0
|
||||||
|
prosemirror-view: 1.29.0
|
||||||
|
rfc6902: 5.0.1
|
||||||
sass: 1.53.0
|
sass: 1.53.0
|
||||||
svelte: 3.48.0
|
svelte: 3.48.0
|
||||||
svelte-check: 2.8.0_818cdd0cbd32f329a17bf389fa6ed6e6
|
svelte-check: 2.8.0_818cdd0cbd32f329a17bf389fa6ed6e6
|
||||||
@ -14538,8 +14596,6 @@ packages:
|
|||||||
- node-sass
|
- node-sass
|
||||||
- postcss
|
- postcss
|
||||||
- postcss-load-config
|
- postcss-load-config
|
||||||
- prosemirror-model
|
|
||||||
- prosemirror-view
|
|
||||||
- pug
|
- pug
|
||||||
- stylus
|
- stylus
|
||||||
- sugarss
|
- sugarss
|
||||||
@ -14855,7 +14911,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
file:projects/workbench-resources.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c:
|
file:projects/workbench-resources.tgz_1e3963ebf0ceeb25b2fa6a1cc87e253c:
|
||||||
resolution: {integrity: sha512-4m7K7jULDi99Dty4n+D3lmGmbi+r/NKlp39QbY1b+CJyKgm2VMXciabLOpOpZsCDx42W3fSmRXaVpgRmkuwDew==, tarball: file:projects/workbench-resources.tgz}
|
resolution: {integrity: sha512-ff56NyjSj5PgQmkJbToL25SlAvhBjTKIJ1Zu6cTQuAwoji1/c+K5VC5xNj5lgE+ZD0hVkGqKUHZwmZNK5ti8RA==, tarball: file:projects/workbench-resources.tgz}
|
||||||
id: file:projects/workbench-resources.tgz
|
id: file:projects/workbench-resources.tgz
|
||||||
name: '@rush-temp/workbench-resources'
|
name: '@rush-temp/workbench-resources'
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
@ -34,9 +34,18 @@
|
|||||||
export let isSub: boolean = true
|
export let isSub: boolean = true
|
||||||
export let isAside: boolean = true
|
export let isAside: boolean = true
|
||||||
export let isCustomAttr: boolean = true
|
export let isCustomAttr: boolean = true
|
||||||
|
export let floatAside = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel bind:isAside isHeader={$$slots.header || isHeader} bind:panelWidth bind:innerWidth bind:withoutTitle on:close>
|
<Panel
|
||||||
|
bind:isAside
|
||||||
|
isHeader={$$slots.header || isHeader}
|
||||||
|
bind:panelWidth
|
||||||
|
bind:innerWidth
|
||||||
|
bind:withoutTitle
|
||||||
|
on:close
|
||||||
|
{floatAside}
|
||||||
|
>
|
||||||
<svelte:fragment slot="navigator">
|
<svelte:fragment slot="navigator">
|
||||||
{#if $$slots.navigator}
|
{#if $$slots.navigator}
|
||||||
<div class="buttons-group xsmall-gap mx-2">
|
<div class="buttons-group xsmall-gap mx-2">
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"Objects": "Objects",
|
"Objects": "Objects",
|
||||||
"Food": "Food",
|
"Food": "Food",
|
||||||
"FullDescription": "Full description",
|
"FullDescription": "Full description",
|
||||||
"NoFullDescription": "There are no detailed description"
|
"NoFullDescription": "There are no detailed description",
|
||||||
|
"EnableDiffMode": "Diff mode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"Objects": "Объекты",
|
"Objects": "Объекты",
|
||||||
"Food": "Еда",
|
"Food": "Еда",
|
||||||
"FullDescription": "Детальное описание",
|
"FullDescription": "Детальное описание",
|
||||||
"NoFullDescription": "Нет детального описания"
|
"NoFullDescription": "Нет детального описания",
|
||||||
|
"EnableDiffMode": "Режим сравнения"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,8 @@
|
|||||||
"eslint": "^8.26.0",
|
"eslint": "^8.26.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"svelte-check": "^2.8.0",
|
"svelte-check": "^2.8.0",
|
||||||
"typescript": "^4.3.5"
|
"typescript": "^4.3.5",
|
||||||
|
"@types/diff": "~5.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hcengineering/presentation": "~0.6.2",
|
"@hcengineering/presentation": "~0.6.2",
|
||||||
@ -53,6 +54,14 @@
|
|||||||
"prosemirror-state": "~1.4.1",
|
"prosemirror-state": "~1.4.1",
|
||||||
"prosemirror-transform": "~1.7.0",
|
"prosemirror-transform": "~1.7.0",
|
||||||
"yjs": "^13.5.42",
|
"yjs": "^13.5.42",
|
||||||
"y-websocket": "^1.4.5"
|
"y-websocket": "^1.4.5",
|
||||||
|
"prosemirror-changeset": "~2.2.0",
|
||||||
|
"prosemirror-model": "~1.18.1",
|
||||||
|
"prosemirror-view": "~1.29.0",
|
||||||
|
"rfc6902": "~5.0.1",
|
||||||
|
"diff": "~5.1.0",
|
||||||
|
"@tiptap/extension-code-block": "~2.0.0-beta.200",
|
||||||
|
"@tiptap/extension-gapcursor": "~2.0.0-beta.200",
|
||||||
|
"@tiptap/extension-heading": "~2.0.0-beta.200"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
168
packages/text-editor/src/changeSet.ts
Normal file
168
packages/text-editor/src/changeSet.ts
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import { Extension, Mark, mergeAttributes } from '@tiptap/core'
|
||||||
|
import { ChangeSet } from 'prosemirror-changeset'
|
||||||
|
import { Plugin } from 'prosemirror-state'
|
||||||
|
|
||||||
|
export interface ChangeHighlightOptions {
|
||||||
|
multicolor: boolean
|
||||||
|
HTMLAttributes: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
changeHighlight: {
|
||||||
|
/**
|
||||||
|
* Set a highlight mark
|
||||||
|
*/
|
||||||
|
setChangeHighlight: (attributes?: { color: string }) => ReturnType
|
||||||
|
/**
|
||||||
|
* Toggle a highlight mark
|
||||||
|
*/
|
||||||
|
toggleChangeHighlight: (attributes?: { color: string }) => ReturnType
|
||||||
|
/**
|
||||||
|
* Unset a highlight mark
|
||||||
|
*/
|
||||||
|
unsetChangeHighlight: () => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangeHighlight = Mark.create<ChangeHighlightOptions>({
|
||||||
|
name: 'changeHighlight',
|
||||||
|
|
||||||
|
addOptions () {
|
||||||
|
return {
|
||||||
|
multicolor: true,
|
||||||
|
HTMLAttributes: {
|
||||||
|
changeColor: 'yellow'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addAttributes () {
|
||||||
|
if (!this.options.multicolor) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
color: {
|
||||||
|
default: null,
|
||||||
|
parseHTML: (element) => element.getAttribute('color'),
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
||||||
|
if (!attributes.color) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = attributes.color as string
|
||||||
|
|
||||||
|
return {
|
||||||
|
color,
|
||||||
|
style: `border-top: 1px solid ${color}; border-bottom: 1px solid ${color}; border-radius: 2px;`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHTML () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
tag: 'cmark'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHTML ({ HTMLAttributes }) {
|
||||||
|
return ['cmark', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||||
|
},
|
||||||
|
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
setChangeHighlight:
|
||||||
|
(attributes) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.setMark(this.name, attributes)
|
||||||
|
},
|
||||||
|
toggleChangeHighlight:
|
||||||
|
(attributes) =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.toggleMark(this.name, attributes)
|
||||||
|
},
|
||||||
|
unsetChangeHighlight:
|
||||||
|
() =>
|
||||||
|
({ commands }) => {
|
||||||
|
return commands.unsetMark(this.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export interface ChangesetExtensionOptions {
|
||||||
|
isSuggestMode: () => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangesetExtension = Extension.create<ChangesetExtensionOptions>({
|
||||||
|
// addInputRules () {
|
||||||
|
// return [changeSetRule]
|
||||||
|
// },
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
appendTransaction: (_transactions, oldState, newState) => {
|
||||||
|
// no changes
|
||||||
|
if (newState.doc === oldState.doc) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const tr = newState.tr
|
||||||
|
if (this.options.isSuggestMode()) {
|
||||||
|
let changes = ChangeSet.create(oldState.doc)
|
||||||
|
|
||||||
|
for (const tr of _transactions) {
|
||||||
|
changes = changes.addSteps(tr.doc, tr.mapping.maps, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const r of changes.changes) {
|
||||||
|
const from = r.fromB
|
||||||
|
const to = r.toB
|
||||||
|
if (r.inserted.length > 0 && from !== to) {
|
||||||
|
tr.addMark(from, to, newState.schema.marks.changeHighlight.create({ color: 'lightblue' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.deleted.length > 0) {
|
||||||
|
const deletedText = oldState.doc.textBetween(r.fromA, r.toA)
|
||||||
|
tr.insertText(deletedText, from)
|
||||||
|
tr.addMark(
|
||||||
|
from,
|
||||||
|
from + deletedText.length,
|
||||||
|
newState.schema.marks.changeHighlight.create({ color: 'orange' })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
onCreate () {
|
||||||
|
// The editor is ready.
|
||||||
|
},
|
||||||
|
onUpdate () {
|
||||||
|
// The content has changed.
|
||||||
|
},
|
||||||
|
// onSelectionUpdate ({ editor }) {
|
||||||
|
// // The selection has changed.
|
||||||
|
// },
|
||||||
|
onTransaction ({ transaction }) {
|
||||||
|
// The editor state has changed.
|
||||||
|
},
|
||||||
|
onFocus ({ event }) {
|
||||||
|
// The editor is focused.
|
||||||
|
},
|
||||||
|
onBlur ({ event }) {
|
||||||
|
// The editor isn’t focused anymore.
|
||||||
|
},
|
||||||
|
onDestroy () {
|
||||||
|
// The editor is being destroyed.
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,278 @@
|
|||||||
|
<!--
|
||||||
|
//
|
||||||
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import { Editor, Extension } from '@tiptap/core'
|
||||||
|
import Highlight from '@tiptap/extension-highlight'
|
||||||
|
import Link from '@tiptap/extension-link'
|
||||||
|
import Heading, { Level } from '@tiptap/extension-heading'
|
||||||
|
import TaskItem from '@tiptap/extension-task-item'
|
||||||
|
import TaskList from '@tiptap/extension-task-list'
|
||||||
|
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||||
|
import { onDestroy, onMount } from 'svelte'
|
||||||
|
|
||||||
|
import { Markup } from '@hcengineering/core'
|
||||||
|
import { IconSize } from '@hcengineering/ui'
|
||||||
|
import StyleButton from './StyleButton.svelte'
|
||||||
|
|
||||||
|
import TipTapCodeBlock from '@tiptap/extension-code-block'
|
||||||
|
import Gapcursor from '@tiptap/extension-gapcursor'
|
||||||
|
import { DecorationSet } from 'prosemirror-view'
|
||||||
|
import textEditorPlugin from '../plugin'
|
||||||
|
|
||||||
|
import { calculateDecorations } from './diff/decorations'
|
||||||
|
import Objects from './icons/Objects.svelte'
|
||||||
|
|
||||||
|
export let content: Markup
|
||||||
|
export let buttonSize: IconSize = 'small'
|
||||||
|
export let comparedVersion: Markup | undefined = undefined
|
||||||
|
export let headingLevels: Level[] = [1, 2, 3, 4]
|
||||||
|
|
||||||
|
let element: HTMLElement
|
||||||
|
let editor: Editor
|
||||||
|
|
||||||
|
let _decoration = DecorationSet.empty
|
||||||
|
let oldContent = ''
|
||||||
|
|
||||||
|
function updateEditor (editor?: Editor, comparedVersion?: Markup): void {
|
||||||
|
const r = calculateDecorations(editor, oldContent, comparedVersion)
|
||||||
|
if (r !== undefined) {
|
||||||
|
oldContent = r.oldContent
|
||||||
|
_decoration = r.decorations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDecorations = () => {
|
||||||
|
if (editor && editor.schema) {
|
||||||
|
updateEditor(editor, comparedVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DecorationExtension = Extension.create({
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('diffs'),
|
||||||
|
props: {
|
||||||
|
decorations (state) {
|
||||||
|
updateDecorations()
|
||||||
|
if (showDiff) {
|
||||||
|
return _decoration
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$: updateEditor(editor, comparedVersion)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
editor = new Editor({
|
||||||
|
element,
|
||||||
|
content,
|
||||||
|
editable: true,
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
Highlight.configure({
|
||||||
|
multicolor: false
|
||||||
|
}),
|
||||||
|
TipTapCodeBlock.configure({
|
||||||
|
languageClassPrefix: 'language-',
|
||||||
|
exitOnArrowDown: true,
|
||||||
|
exitOnTripleEnter: true,
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'code-block'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Gapcursor,
|
||||||
|
Heading.configure({
|
||||||
|
levels: headingLevels
|
||||||
|
}),
|
||||||
|
Link.configure({ openOnClick: false }),
|
||||||
|
TaskList,
|
||||||
|
TaskItem.configure({
|
||||||
|
nested: true,
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'flex flex-grow gap-1 checkbox_style'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
DecorationExtension
|
||||||
|
// ...extensions
|
||||||
|
],
|
||||||
|
onTransaction: () => {
|
||||||
|
// force re-render so `editor.isActive` works as expected
|
||||||
|
editor = editor
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (editor) {
|
||||||
|
editor.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
let showDiff = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="ref-container">
|
||||||
|
{#if comparedVersion !== undefined}
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<div class="formatPanel buttons-group xsmall-gap mb-4">
|
||||||
|
<StyleButton
|
||||||
|
icon={Objects}
|
||||||
|
size={buttonSize}
|
||||||
|
selected={showDiff}
|
||||||
|
showTooltip={{ label: textEditorPlugin.string.EnableDiffMode }}
|
||||||
|
on:click={() => {
|
||||||
|
showDiff = !showDiff
|
||||||
|
editor.chain().focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="textInput">
|
||||||
|
<div class="select-text" style="width: 100%;" bind:this={element} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss" global>
|
||||||
|
.ProseMirror {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 60vh;
|
||||||
|
outline: none;
|
||||||
|
line-height: 150%;
|
||||||
|
color: var(--accent-color);
|
||||||
|
|
||||||
|
p:not(:last-child) {
|
||||||
|
margin-block-end: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * + * {
|
||||||
|
margin-top: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder (at the top) */
|
||||||
|
p.is-editor-empty:first-child::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
float: left;
|
||||||
|
color: var(--dark-color);
|
||||||
|
pointer-events: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--theme-bg-accent-hover);
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
background-color: var(--theme-bg-accent-hover);
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Placeholder (at the top) */
|
||||||
|
.ProseMirror p.is-editor-empty:first-child::before {
|
||||||
|
color: #adb5bd;
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
float: left;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lint-icon {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 100px;
|
||||||
|
// background: #f22;
|
||||||
|
color: white;
|
||||||
|
font-family: times, georgia, serif;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 0.7em;
|
||||||
|
height: 0.7em;
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 0.5px;
|
||||||
|
line-height: 1.1em;
|
||||||
|
&.add {
|
||||||
|
background: lightblue;
|
||||||
|
}
|
||||||
|
&.delete {
|
||||||
|
background: orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Give a remote user a caret */
|
||||||
|
.collaboration-cursor__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-cursor__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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmark {
|
||||||
|
border-top: 1px solid lightblue;
|
||||||
|
border-bottom: 1px solid lightblue;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.insertion {
|
||||||
|
border-top: 1px solid lightblue;
|
||||||
|
border-bottom: 1px solid lightblue;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
span.deletion {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
</style>
|
@ -15,38 +15,45 @@
|
|||||||
//
|
//
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { IntlString, translate } from '@hcengineering/platform'
|
import { getEmbeddedLabel, IntlString, translate } from '@hcengineering/platform'
|
||||||
|
|
||||||
import { Editor, HTMLContent } from '@tiptap/core'
|
import { Editor, Extension, HTMLContent } from '@tiptap/core'
|
||||||
import Highlight from '@tiptap/extension-highlight'
|
import Highlight from '@tiptap/extension-highlight'
|
||||||
import Link from '@tiptap/extension-link'
|
import Link from '@tiptap/extension-link'
|
||||||
// import Typography from '@tiptap/extension-typography'
|
|
||||||
import Placeholder from '@tiptap/extension-placeholder'
|
import Placeholder from '@tiptap/extension-placeholder'
|
||||||
// import Collab from '@tiptap/extension-collaboration'
|
|
||||||
import Collaboration from '@tiptap/extension-collaboration'
|
import Collaboration from '@tiptap/extension-collaboration'
|
||||||
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
|
||||||
|
import Heading, { Level } from '@tiptap/extension-heading'
|
||||||
import TaskItem from '@tiptap/extension-task-item'
|
import TaskItem from '@tiptap/extension-task-item'
|
||||||
import TaskList from '@tiptap/extension-task-list'
|
import TaskList from '@tiptap/extension-task-list'
|
||||||
|
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import { Transaction } from 'prosemirror-state'
|
import { Plugin, PluginKey, Transaction } from 'prosemirror-state'
|
||||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||||
|
|
||||||
import { EmployeeAccount } from '@hcengineering/contact'
|
import { EmployeeAccount } from '@hcengineering/contact'
|
||||||
import { getCurrentAccount } from '@hcengineering/core'
|
import { getCurrentAccount, Markup } from '@hcengineering/core'
|
||||||
import { getPlatformColorForText, IconSize, showPopup } from '@hcengineering/ui'
|
import { getEventPositionElement, getPlatformColorForText, IconSize, SelectPopup, showPopup } from '@hcengineering/ui'
|
||||||
import { WebsocketProvider } from 'y-websocket'
|
import { WebsocketProvider } from 'y-websocket'
|
||||||
import * as Y from 'yjs'
|
import * as Y from 'yjs'
|
||||||
import StyleButton from './StyleButton.svelte'
|
import StyleButton from './StyleButton.svelte'
|
||||||
|
|
||||||
|
import TipTapCodeBlock from '@tiptap/extension-code-block'
|
||||||
|
import Gapcursor from '@tiptap/extension-gapcursor'
|
||||||
|
import { DecorationSet } from 'prosemirror-view'
|
||||||
import textEditorPlugin from '../plugin'
|
import textEditorPlugin from '../plugin'
|
||||||
import { FormatMode, FORMAT_MODES } from '../types'
|
import { FormatMode, FORMAT_MODES } from '../types'
|
||||||
import Bold from './icons/Bold.svelte'
|
import Bold from './icons/Bold.svelte'
|
||||||
import Code from './icons/Code.svelte'
|
import Code from './icons/Code.svelte'
|
||||||
import CodeBlock from './icons/CodeBlock.svelte'
|
import CodeBlock from './icons/CodeBlock.svelte'
|
||||||
|
|
||||||
|
import { calculateDecorations } from './diff/decorations'
|
||||||
|
import Header from './icons/Header.svelte'
|
||||||
import Italic from './icons/Italic.svelte'
|
import Italic from './icons/Italic.svelte'
|
||||||
import LinkEl from './icons/Link.svelte'
|
import LinkEl from './icons/Link.svelte'
|
||||||
import ListBullet from './icons/ListBullet.svelte'
|
import ListBullet from './icons/ListBullet.svelte'
|
||||||
import ListNumber from './icons/ListNumber.svelte'
|
import ListNumber from './icons/ListNumber.svelte'
|
||||||
|
import Objects from './icons/Objects.svelte'
|
||||||
import Quote from './icons/Quote.svelte'
|
import Quote from './icons/Quote.svelte'
|
||||||
import Strikethrough from './icons/Strikethrough.svelte'
|
import Strikethrough from './icons/Strikethrough.svelte'
|
||||||
import LinkPopup from './LinkPopup.svelte'
|
import LinkPopup from './LinkPopup.svelte'
|
||||||
@ -61,6 +68,10 @@
|
|||||||
export let focusable: boolean = false
|
export let focusable: boolean = false
|
||||||
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
|
||||||
export let initialContentId: string | undefined = undefined
|
export let initialContentId: string | undefined = undefined
|
||||||
|
export let suggestMode = false
|
||||||
|
export let comparedVersion: Markup | undefined = undefined
|
||||||
|
|
||||||
|
export let headingLevels: Level[] = [1, 2, 3, 4]
|
||||||
|
|
||||||
const ydoc = new Y.Doc()
|
const ydoc = new Y.Doc()
|
||||||
const wsProvider = new WebsocketProvider(collaboratorURL, documentId, ydoc, {
|
const wsProvider = new WebsocketProvider(collaboratorURL, documentId, ydoc, {
|
||||||
@ -104,6 +115,11 @@
|
|||||||
export function toggleStrike () {
|
export function toggleStrike () {
|
||||||
editor.commands.toggleStrike()
|
editor.commands.toggleStrike()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getHTML (): string {
|
||||||
|
return editor.getHTML()
|
||||||
|
}
|
||||||
|
|
||||||
export function getLink () {
|
export function getLink () {
|
||||||
return editor.getAttributes('link').href
|
return editor.getAttributes('link').href
|
||||||
}
|
}
|
||||||
@ -150,14 +166,73 @@
|
|||||||
editor.setEditable(!readonly)
|
editor.setEditable(!readonly)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const isSuggestMode = () => suggestMode
|
||||||
|
|
||||||
|
let _decoration = DecorationSet.empty
|
||||||
|
let oldContent = ''
|
||||||
|
|
||||||
|
function updateEditor (editor?: Editor, comparedVersion?: Markup): void {
|
||||||
|
const r = calculateDecorations(editor, oldContent, comparedVersion)
|
||||||
|
if (r !== undefined) {
|
||||||
|
oldContent = r.oldContent
|
||||||
|
_decoration = r.decorations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDecorations = () => {
|
||||||
|
if (editor && editor.schema) {
|
||||||
|
updateEditor(editor, comparedVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DecorationExtension = Extension.create({
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: new PluginKey('diffs'),
|
||||||
|
props: {
|
||||||
|
decorations (state) {
|
||||||
|
updateDecorations()
|
||||||
|
if (showDiff) {
|
||||||
|
return _decoration
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$: updateEditor(editor, comparedVersion)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
ph.then(() => {
|
ph.then(() => {
|
||||||
editor = new Editor({
|
editor = new Editor({
|
||||||
element,
|
element,
|
||||||
editable: !readonly,
|
// content: 'Hello world<br/> This is simple text<br/>Some more text<br/>Yahoo <br/>Cool <br/><br/> Done',
|
||||||
|
editable: true,
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit,
|
||||||
Highlight,
|
Highlight.configure({
|
||||||
|
multicolor: false
|
||||||
|
}),
|
||||||
|
TipTapCodeBlock.configure({
|
||||||
|
languageClassPrefix: 'language-',
|
||||||
|
exitOnArrowDown: true,
|
||||||
|
exitOnTripleEnter: true,
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: 'code-block'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Gapcursor,
|
||||||
|
Heading.configure({
|
||||||
|
levels: headingLevels
|
||||||
|
}),
|
||||||
|
// ChangeHighlight,
|
||||||
|
// ChangesetExtension.configure({
|
||||||
|
// isSuggestMode
|
||||||
|
// }),
|
||||||
Link.configure({ openOnClick: false }),
|
Link.configure({ openOnClick: false }),
|
||||||
// ...(supportSubmit ? [Handle] : []), // order important
|
// ...(supportSubmit ? [Handle] : []), // order important
|
||||||
// Typography, // we need to disable 1/2 -> ½ rule (https://github.com/hcengineering/anticrm/issues/345)
|
// Typography, // we need to disable 1/2 -> ½ rule (https://github.com/hcengineering/anticrm/issues/345)
|
||||||
@ -169,7 +244,6 @@
|
|||||||
class: 'flex flex-grow gap-1 checkbox_style'
|
class: 'flex flex-grow gap-1 checkbox_style'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// UniqId,
|
|
||||||
Collaboration.configure({
|
Collaboration.configure({
|
||||||
document: ydoc
|
document: ydoc
|
||||||
}),
|
}),
|
||||||
@ -179,7 +253,8 @@
|
|||||||
name: currentUser.name,
|
name: currentUser.name,
|
||||||
color: getPlatformColorForText(currentUser.email)
|
color: getPlatformColorForText(currentUser.email)
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
DecorationExtension
|
||||||
// ...extensions
|
// ...extensions
|
||||||
],
|
],
|
||||||
onTransaction: () => {
|
onTransaction: () => {
|
||||||
@ -192,10 +267,11 @@
|
|||||||
},
|
},
|
||||||
onFocus: () => {
|
onFocus: () => {
|
||||||
focused = true
|
focused = true
|
||||||
dispatch('focus', editor.getHTML())
|
|
||||||
},
|
},
|
||||||
onUpdate: (op: { editor: Editor; transaction: Transaction }) => {
|
onUpdate: (op: { editor: Editor; transaction: Transaction }) => {
|
||||||
|
// _decoration = DecorationSet.empty
|
||||||
dispatch('content', editor.getHTML())
|
dispatch('content', editor.getHTML())
|
||||||
|
updateFormattingState()
|
||||||
},
|
},
|
||||||
onSelectionUpdate: () => {
|
onSelectionUpdate: () => {
|
||||||
dispatch('selection-update')
|
dispatch('selection-update')
|
||||||
@ -219,8 +295,19 @@
|
|||||||
return editor.isActive(formatMode)
|
return editor.isActive(formatMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let headingLevel = 0
|
||||||
|
|
||||||
function updateFormattingState () {
|
function updateFormattingState () {
|
||||||
activeModes = new Set(FORMAT_MODES.filter(checkIsActive))
|
activeModes = new Set(FORMAT_MODES.filter(checkIsActive))
|
||||||
|
for (const l of headingLevels) {
|
||||||
|
if (editor.isActive('heading', { level: l })) {
|
||||||
|
headingLevel = l
|
||||||
|
activeModes.add('heading')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!activeModes.has('heading')) {
|
||||||
|
headingLevel = 0
|
||||||
|
}
|
||||||
isSelectionEmpty = editor.view.state.selection.empty
|
isSelectionEmpty = editor.view.state.selection.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,6 +319,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleHeader (event: MouseEvent) {
|
||||||
|
if (activeModes.has('heading')) {
|
||||||
|
editor.commands.toggleHeading({ level: headingLevel as Level })
|
||||||
|
needFocus = true
|
||||||
|
updateFormattingState()
|
||||||
|
} else {
|
||||||
|
showPopup(
|
||||||
|
SelectPopup,
|
||||||
|
{
|
||||||
|
value: Array.from(headingLevels).map((it) => ({ id: it.toString(), label: it.toString() }))
|
||||||
|
},
|
||||||
|
getEventPositionElement(event),
|
||||||
|
(val) => {
|
||||||
|
if (val !== undefined) {
|
||||||
|
editor.commands.toggleHeading({ level: parseInt(val) as Level })
|
||||||
|
needFocus = true
|
||||||
|
updateFormattingState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function formatLink (): Promise<void> {
|
async function formatLink (): Promise<void> {
|
||||||
const link = editor.getAttributes('link').href
|
const link = editor.getAttributes('link').href
|
||||||
|
|
||||||
@ -243,80 +353,106 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
let showDiff = true
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="ref-container">
|
<div class="ref-container">
|
||||||
{#if isFormatting && !readonly}
|
<div class="flex">
|
||||||
<div class="formatPanel buttons-group xsmall-gap mb-4">
|
{#if isFormatting && !readonly}
|
||||||
<StyleButton
|
<div class="formatPanel buttons-group xsmall-gap mb-4">
|
||||||
icon={Bold}
|
<StyleButton
|
||||||
size={buttonSize}
|
icon={Header}
|
||||||
selected={activeModes.has('bold')}
|
size={buttonSize}
|
||||||
showTooltip={{ label: textEditorPlugin.string.Bold }}
|
selected={activeModes.has('heading')}
|
||||||
on:click={getToggler(toggleBold)}
|
showTooltip={{ label: getEmbeddedLabel(`H${headingLevel}`) }}
|
||||||
/>
|
on:click={toggleHeader}
|
||||||
<StyleButton
|
/>
|
||||||
icon={Italic}
|
|
||||||
size={buttonSize}
|
<StyleButton
|
||||||
selected={activeModes.has('italic')}
|
icon={Bold}
|
||||||
showTooltip={{ label: textEditorPlugin.string.Italic }}
|
size={buttonSize}
|
||||||
on:click={getToggler(toggleItalic)}
|
selected={activeModes.has('bold')}
|
||||||
/>
|
showTooltip={{ label: textEditorPlugin.string.Bold }}
|
||||||
<StyleButton
|
on:click={getToggler(toggleBold)}
|
||||||
icon={Strikethrough}
|
/>
|
||||||
size={buttonSize}
|
<StyleButton
|
||||||
selected={activeModes.has('strike')}
|
icon={Italic}
|
||||||
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
|
size={buttonSize}
|
||||||
on:click={getToggler(toggleStrike)}
|
selected={activeModes.has('italic')}
|
||||||
/>
|
showTooltip={{ label: textEditorPlugin.string.Italic }}
|
||||||
<StyleButton
|
on:click={getToggler(toggleItalic)}
|
||||||
icon={LinkEl}
|
/>
|
||||||
size={buttonSize}
|
<StyleButton
|
||||||
selected={activeModes.has('link')}
|
icon={Strikethrough}
|
||||||
disabled={isSelectionEmpty && !activeModes.has('link')}
|
size={buttonSize}
|
||||||
showTooltip={{ label: textEditorPlugin.string.Link }}
|
selected={activeModes.has('strike')}
|
||||||
on:click={formatLink}
|
showTooltip={{ label: textEditorPlugin.string.Strikethrough }}
|
||||||
/>
|
on:click={getToggler(toggleStrike)}
|
||||||
<div class="buttons-divider" />
|
/>
|
||||||
<StyleButton
|
<StyleButton
|
||||||
icon={ListNumber}
|
icon={LinkEl}
|
||||||
size={buttonSize}
|
size={buttonSize}
|
||||||
selected={activeModes.has('orderedList')}
|
selected={activeModes.has('link')}
|
||||||
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
|
disabled={isSelectionEmpty && !activeModes.has('link')}
|
||||||
on:click={getToggler(toggleOrderedList)}
|
showTooltip={{ label: textEditorPlugin.string.Link }}
|
||||||
/>
|
on:click={formatLink}
|
||||||
<StyleButton
|
/>
|
||||||
icon={ListBullet}
|
<div class="buttons-divider" />
|
||||||
size={buttonSize}
|
<StyleButton
|
||||||
selected={activeModes.has('bulletList')}
|
icon={ListNumber}
|
||||||
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
|
size={buttonSize}
|
||||||
on:click={getToggler(toggleBulletList)}
|
selected={activeModes.has('orderedList')}
|
||||||
/>
|
showTooltip={{ label: textEditorPlugin.string.OrderedList }}
|
||||||
<div class="buttons-divider" />
|
on:click={getToggler(toggleOrderedList)}
|
||||||
<StyleButton
|
/>
|
||||||
icon={Quote}
|
<StyleButton
|
||||||
size={buttonSize}
|
icon={ListBullet}
|
||||||
selected={activeModes.has('blockquote')}
|
size={buttonSize}
|
||||||
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
|
selected={activeModes.has('bulletList')}
|
||||||
on:click={getToggler(toggleBlockquote)}
|
showTooltip={{ label: textEditorPlugin.string.BulletedList }}
|
||||||
/>
|
on:click={getToggler(toggleBulletList)}
|
||||||
<div class="buttons-divider" />
|
/>
|
||||||
<StyleButton
|
<div class="buttons-divider" />
|
||||||
icon={Code}
|
<StyleButton
|
||||||
size={buttonSize}
|
icon={Quote}
|
||||||
selected={activeModes.has('code')}
|
size={buttonSize}
|
||||||
showTooltip={{ label: textEditorPlugin.string.Code }}
|
selected={activeModes.has('blockquote')}
|
||||||
on:click={getToggler(toggleCode)}
|
showTooltip={{ label: textEditorPlugin.string.Blockquote }}
|
||||||
/>
|
on:click={getToggler(toggleBlockquote)}
|
||||||
<StyleButton
|
/>
|
||||||
icon={CodeBlock}
|
<div class="buttons-divider" />
|
||||||
size={buttonSize}
|
<StyleButton
|
||||||
selected={activeModes.has('codeBlock')}
|
icon={Code}
|
||||||
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
|
size={buttonSize}
|
||||||
on:click={getToggler(toggleCodeBlock)}
|
selected={activeModes.has('code')}
|
||||||
/>
|
showTooltip={{ label: textEditorPlugin.string.Code }}
|
||||||
</div>
|
on:click={getToggler(toggleCode)}
|
||||||
{/if}
|
/>
|
||||||
|
<StyleButton
|
||||||
|
icon={CodeBlock}
|
||||||
|
size={buttonSize}
|
||||||
|
selected={activeModes.has('codeBlock')}
|
||||||
|
showTooltip={{ label: textEditorPlugin.string.CodeBlock }}
|
||||||
|
on:click={getToggler(toggleCodeBlock)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-grow" />
|
||||||
|
{#if comparedVersion !== undefined}
|
||||||
|
<div class="formatPanel buttons-group xsmall-gap mb-4">
|
||||||
|
<StyleButton
|
||||||
|
icon={Objects}
|
||||||
|
size={buttonSize}
|
||||||
|
selected={showDiff}
|
||||||
|
showTooltip={{ label: textEditorPlugin.string.EnableDiffMode }}
|
||||||
|
on:click={() => {
|
||||||
|
showDiff = !showDiff
|
||||||
|
editor.chain().focus()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="textInput" class:focusable>
|
<div class="textInput" class:focusable>
|
||||||
<div class="select-text" style="width: 100%;" bind:this={element} />
|
<div class="select-text" style="width: 100%;" bind:this={element} />
|
||||||
</div>
|
</div>
|
||||||
@ -325,7 +461,7 @@
|
|||||||
<style lang="scss" global>
|
<style lang="scss" global>
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-y: auto;
|
overflow: auto;
|
||||||
max-height: 60vh;
|
max-height: 60vh;
|
||||||
outline: none;
|
outline: none;
|
||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
@ -335,6 +471,10 @@
|
|||||||
margin-block-end: 1em;
|
margin-block-end: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre !important;
|
||||||
|
}
|
||||||
|
|
||||||
> * + * {
|
> * + * {
|
||||||
margin-top: 0.75em;
|
margin-top: 0.75em;
|
||||||
}
|
}
|
||||||
@ -367,6 +507,30 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lint-icon {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
right: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 100px;
|
||||||
|
// background: #f22;
|
||||||
|
color: white;
|
||||||
|
font-family: times, georgia, serif;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 0.7em;
|
||||||
|
height: 0.7em;
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 0.5px;
|
||||||
|
line-height: 1.1em;
|
||||||
|
&.add {
|
||||||
|
background: lightblue;
|
||||||
|
}
|
||||||
|
&.delete {
|
||||||
|
background: orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Give a remote user a caret */
|
/* Give a remote user a caret */
|
||||||
.collaboration-cursor__caret {
|
.collaboration-cursor__caret {
|
||||||
border-left: 1px solid #0d0d0d;
|
border-left: 1px solid #0d0d0d;
|
||||||
@ -393,4 +557,25 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmark {
|
||||||
|
border-top: 1px solid lightblue;
|
||||||
|
border-bottom: 1px solid lightblue;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.insertion {
|
||||||
|
border-top: 1px solid lightblue;
|
||||||
|
border-bottom: 1px solid lightblue;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
span.deletion {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -15,15 +15,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Asset } from '@hcengineering/platform'
|
import type { Asset } from '@hcengineering/platform'
|
||||||
import { AnySvelteComponent, Icon, IconSize, LabelAndProps, tooltip } from '@hcengineering/ui'
|
import { AnySvelteComponent, Icon, IconSize, LabelAndProps, tooltip } from '@hcengineering/ui'
|
||||||
import { createEventDispatcher } from 'svelte'
|
|
||||||
|
|
||||||
export let icon: Asset | AnySvelteComponent
|
export let icon: Asset | AnySvelteComponent
|
||||||
export let size: IconSize
|
export let size: IconSize
|
||||||
export let selected: boolean = false
|
export let selected: boolean = false
|
||||||
export let showTooltip: LabelAndProps | undefined = undefined
|
export let showTooltip: LabelAndProps | undefined = undefined
|
||||||
export let disabled: boolean = false
|
export let disabled: boolean = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@ -32,11 +29,9 @@
|
|||||||
{disabled}
|
{disabled}
|
||||||
use:tooltip={showTooltip}
|
use:tooltip={showTooltip}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
on:mousedown|preventDefault|stopPropagation={() => {
|
on:click|preventDefault|stopPropagation
|
||||||
dispatch('click')
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div class="icon {size}">
|
<div class="icon {size} flex">
|
||||||
<Icon {icon} {size} />
|
<Icon {icon} {size} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
99
packages/text-editor/src/components/diff/decorations.ts
Normal file
99
packages/text-editor/src/components/diff/decorations.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { Markup } from '@hcengineering/core'
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import { ChangeSet } from 'prosemirror-changeset'
|
||||||
|
import { DOMParser, Node, Schema } from 'prosemirror-model'
|
||||||
|
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||||
|
import { recreateTransform } from './recreate'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function createDocument (schema: Schema, content: Markup): Node {
|
||||||
|
const wrappedValue = `<body>${content}</body>`
|
||||||
|
|
||||||
|
const body = new window.DOMParser().parseFromString(wrappedValue, 'text/html').body
|
||||||
|
|
||||||
|
return DOMParser.fromSchema(schema).parse(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function calculateDecorations (
|
||||||
|
editor?: Editor,
|
||||||
|
oldContent?: string,
|
||||||
|
comparedVersion?: Markup
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
decorations: DecorationSet
|
||||||
|
oldContent: string
|
||||||
|
}
|
||||||
|
| undefined {
|
||||||
|
try {
|
||||||
|
if (editor === undefined || editor.schema === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (comparedVersion === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const schema = editor.schema
|
||||||
|
const docOld = createDocument(schema, comparedVersion)
|
||||||
|
const docNew = editor.state.doc
|
||||||
|
|
||||||
|
const c = editor.getHTML()
|
||||||
|
if (c === oldContent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tr = recreateTransform(docOld, docNew)
|
||||||
|
const changeSet = ChangeSet.create(docOld).addSteps(tr.doc, tr.mapping.maps, undefined)
|
||||||
|
const changes = changeSet.changes
|
||||||
|
|
||||||
|
const decorations: Decoration[] = []
|
||||||
|
|
||||||
|
function lintIcon (color: string): any {
|
||||||
|
const icon = document.createElement('div')
|
||||||
|
icon.className = `lint-icon ${color}`
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleted (prob: any): any {
|
||||||
|
const icon = document.createElement('span')
|
||||||
|
icon.className = 'deletion'
|
||||||
|
icon.innerText = prob
|
||||||
|
return icon
|
||||||
|
}
|
||||||
|
changes.forEach((change) => {
|
||||||
|
if (change.inserted.length > 0) {
|
||||||
|
decorations.push(Decoration.inline(change.fromB, change.toB, { class: 'diff insertion' }, {}))
|
||||||
|
decorations.push(Decoration.widget(change.fromB, lintIcon('add')))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (change.deleted.length > 0) {
|
||||||
|
const cont = docOld.textBetween(change.fromA, change.toA)
|
||||||
|
decorations.push(Decoration.widget(change.fromB, deleted(cont)))
|
||||||
|
decorations.push(Decoration.widget(change.fromB, lintIcon('delete')))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (decorations.length > 0) {
|
||||||
|
return { decorations: DecorationSet.empty.add(docNew, decorations), oldContent: c }
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
163
packages/text-editor/src/components/diff/diff.ts
Normal file
163
packages/text-editor/src/components/diff/diff.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { Diff, diffAny, Operation } from 'rfc6902/diff'
|
||||||
|
import { Pointer } from 'rfc6902/pointer'
|
||||||
|
|
||||||
|
interface ArrayOperation {
|
||||||
|
op: 'add' | 'remove' | 'replace'
|
||||||
|
index: number
|
||||||
|
value?: any
|
||||||
|
original?: number
|
||||||
|
}
|
||||||
|
interface MemoValue {
|
||||||
|
operations: ArrayOperation[]
|
||||||
|
cost: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
* Modification of {diffArray} from rfc6902/diff, with respect to prosemirror model.
|
||||||
|
*/
|
||||||
|
export function diffArraysPM (input: any, output: any, ptr: Pointer, diff: Diff = diffAny): any {
|
||||||
|
// set up cost matrix (very simple initialization: just a map)
|
||||||
|
const memo: Record<string, MemoValue> = {
|
||||||
|
'0,0': { operations: [], cost: 0 }
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
Calculate the cheapest sequence of operations required to get from
|
||||||
|
input.slice(0, i) to output.slice(0, j).
|
||||||
|
There may be other valid sequences with the same cost, but none cheaper.
|
||||||
|
|
||||||
|
@param i The row in the layout above
|
||||||
|
@param j The column in the layout above
|
||||||
|
@returns An object containing a list of operations, along with the total cost
|
||||||
|
of applying them (+1 for each add/remove/replace operation)
|
||||||
|
*/
|
||||||
|
function dist (i: number, j: number): MemoValue {
|
||||||
|
// memoized
|
||||||
|
const memoKey = `${i},${j}`
|
||||||
|
let memoized = memo[memoKey]
|
||||||
|
if (memoized === undefined) {
|
||||||
|
// TODO: this !diff(...).length usage could/should be lazy
|
||||||
|
if (i > 0 && j > 0 && diff(input[i - 1], output[j - 1], ptr.add(String(i - 1))).length === 0) {
|
||||||
|
// equal (no operations => no cost)
|
||||||
|
memoized = dist(i - 1, j - 1)
|
||||||
|
} else {
|
||||||
|
const alternatives: MemoValue[] = []
|
||||||
|
if (i > 0) {
|
||||||
|
// NOT topmost row
|
||||||
|
const removeBase = dist(i - 1, j)
|
||||||
|
const removeOperation: ArrayOperation = {
|
||||||
|
op: 'remove',
|
||||||
|
index: i - 1
|
||||||
|
}
|
||||||
|
alternatives.push(appendArrayOperation(removeBase, removeOperation))
|
||||||
|
}
|
||||||
|
if (j > 0) {
|
||||||
|
// NOT leftmost column
|
||||||
|
const addBase: MemoValue = dist(i, j - 1)
|
||||||
|
const addOperation: ArrayOperation = {
|
||||||
|
op: 'add',
|
||||||
|
index: i - 1,
|
||||||
|
value: output[j - 1]
|
||||||
|
}
|
||||||
|
alternatives.push(appendArrayOperation(addBase, addOperation))
|
||||||
|
}
|
||||||
|
if (i > 0 && j > 0) {
|
||||||
|
// TABLE MIDDLE
|
||||||
|
// supposing we replaced it, compute the rest of the costs:
|
||||||
|
const replaceBase = dist(i - 1, j - 1)
|
||||||
|
// okay, the general plan is to replace it, but we can be smarter,
|
||||||
|
// recursing into the structure and replacing only part of it if
|
||||||
|
// possible, but to do so we'll need the original value
|
||||||
|
const replaceOperation: ArrayOperation = {
|
||||||
|
op: 'replace',
|
||||||
|
index: i - 1,
|
||||||
|
original: input[i - 1],
|
||||||
|
value: output[j - 1]
|
||||||
|
}
|
||||||
|
// Replace only if simple or object's with type equal
|
||||||
|
const io = input[i - 1]
|
||||||
|
const jo = output[j - 1]
|
||||||
|
if (
|
||||||
|
(typeof io !== 'object' && typeof jo !== 'object') ||
|
||||||
|
(typeof io === 'object' &&
|
||||||
|
typeof jo === 'object' &&
|
||||||
|
io.type === jo.type &&
|
||||||
|
Array.isArray(io.content) &&
|
||||||
|
Array.isArray(jo.content))
|
||||||
|
) {
|
||||||
|
alternatives.push(appendArrayOperation(replaceBase, replaceOperation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the only other case, i === 0 && j === 0, has already been memoized
|
||||||
|
// the meat of the algorithm:
|
||||||
|
// sort by cost to find the lowest one (might be several ties for lowest)
|
||||||
|
// [4, 6, 7, 1, 2].sort((a, b) => a - b) -> [ 1, 2, 4, 6, 7 ]
|
||||||
|
const best = alternatives.sort(function (a, b) {
|
||||||
|
return a.cost - b.cost
|
||||||
|
})[0]
|
||||||
|
memoized = best
|
||||||
|
}
|
||||||
|
memo[memoKey] = memoized
|
||||||
|
}
|
||||||
|
return memoized
|
||||||
|
}
|
||||||
|
// handle weird objects masquerading as Arrays that don't have proper length
|
||||||
|
// properties by using 0 for everything but positive numbers
|
||||||
|
const inputLength: number = isNaN(input.length) || input.length <= 0 ? 0 : input.length
|
||||||
|
const outputLength: number = isNaN(output.length) || output.length <= 0 ? 0 : output.length
|
||||||
|
const arrayOperations = dist(inputLength, outputLength).operations
|
||||||
|
const paddedOperations = arrayOperations.reduce(
|
||||||
|
function (_a: any, arrayOperation: ArrayOperation) {
|
||||||
|
const operations: Operation[] = _a[0]
|
||||||
|
const padding: number = _a[1]
|
||||||
|
if (arrayOperation.op === 'add') {
|
||||||
|
const paddedIndex = arrayOperation.index + 1 + padding
|
||||||
|
const indexToken = paddedIndex < inputLength + padding ? String(paddedIndex) : '-'
|
||||||
|
const operation: Operation = {
|
||||||
|
op: arrayOperation.op,
|
||||||
|
path: ptr.add(indexToken).toString(),
|
||||||
|
value: arrayOperation.value
|
||||||
|
}
|
||||||
|
// padding++ // maybe only if array_operation.index > -1 ?
|
||||||
|
return [operations.concat(operation), padding + 1]
|
||||||
|
} else if (arrayOperation.op === 'remove') {
|
||||||
|
const operation: Operation = {
|
||||||
|
op: arrayOperation.op,
|
||||||
|
path: ptr.add(String(arrayOperation.index + padding)).toString()
|
||||||
|
}
|
||||||
|
// padding--
|
||||||
|
return [operations.concat(operation), padding - 1]
|
||||||
|
} else {
|
||||||
|
// replace
|
||||||
|
const replacePtr = ptr.add(String(arrayOperation.index + padding))
|
||||||
|
const replaceOperations = diff(arrayOperation.original, arrayOperation.value, replacePtr)
|
||||||
|
return [operations.concat.apply(operations, replaceOperations), padding]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[[], 0]
|
||||||
|
)[0]
|
||||||
|
return paddedOperations
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendArrayOperation (base: MemoValue, operation: ArrayOperation): MemoValue {
|
||||||
|
return {
|
||||||
|
// the new operation must be pushed on the end
|
||||||
|
operations: base.operations.concat(operation),
|
||||||
|
cost: base.cost + 1
|
||||||
|
}
|
||||||
|
}
|
331
packages/text-editor/src/components/diff/recreate.ts
Normal file
331
packages/text-editor/src/components/diff/recreate.ts
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2022 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Parts of source code is taken from https://github.com/sueddeutsche/prosemirror-recreate-transform
|
||||||
|
// based on Apache License 2.0
|
||||||
|
//
|
||||||
|
|
||||||
|
import { Change, diffWordsWithSpace } from 'diff'
|
||||||
|
import { Node, Schema } from 'prosemirror-model'
|
||||||
|
import { ReplaceStep, Step, Transform } from 'prosemirror-transform'
|
||||||
|
import { applyPatch, createPatch, Operation } from 'rfc6902'
|
||||||
|
import { Pointer } from 'rfc6902/pointer'
|
||||||
|
import { diffArraysPM } from './diff'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export function getReplaceStep (fromDoc: Node, toDoc: Node): ReplaceStep | undefined {
|
||||||
|
const start = toDoc.content.findDiffStart(fromDoc.content)
|
||||||
|
if (start !== null) {
|
||||||
|
const pos = toDoc.content.findDiffEnd(fromDoc.content)
|
||||||
|
if (pos != null) {
|
||||||
|
return getReplaceStepOverlap(pos, start, fromDoc, toDoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getReplaceStepOverlap (pos: { a: number, b: number }, start: number, fromDoc: Node, toDoc: Node): ReplaceStep {
|
||||||
|
let { a: endA, b: endB } = pos
|
||||||
|
const overlap = start - Math.min(endA, endB)
|
||||||
|
if (overlap > 0) {
|
||||||
|
if (fromDoc.resolve(start - overlap).depth < toDoc.resolve(endA + overlap).depth) {
|
||||||
|
start -= overlap
|
||||||
|
} else {
|
||||||
|
endA += overlap
|
||||||
|
endB += overlap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ReplaceStep(start, endB, toDoc.slice(start, endA))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function simplifyTransform (tr: Transform): Transform | undefined {
|
||||||
|
if (tr.steps.length === 0) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTr = new Transform(tr.docs[0])
|
||||||
|
const oldSteps = tr.steps.slice()
|
||||||
|
|
||||||
|
while (oldSteps.length > 0) {
|
||||||
|
let step = oldSteps.shift()
|
||||||
|
while (oldSteps.length > 0 && step?.merge(oldSteps[0]) != null) {
|
||||||
|
step = simplifyStep(step, oldSteps.shift(), newTr)
|
||||||
|
}
|
||||||
|
if (step === undefined) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
newTr.step(step)
|
||||||
|
}
|
||||||
|
return newTr
|
||||||
|
}
|
||||||
|
|
||||||
|
function simplifyStep (step: Step | undefined, addedStep: Step | undefined, newTr: Transform): Step | undefined {
|
||||||
|
if (step != null && step instanceof ReplaceStep && addedStep != null && addedStep instanceof ReplaceStep) {
|
||||||
|
const stepA = step.apply(newTr.doc)
|
||||||
|
if (stepA.doc != null) {
|
||||||
|
const stepB = addedStep.apply(stepA.doc)
|
||||||
|
if (stepB.doc !== null) {
|
||||||
|
step = getReplaceStep(newTr.doc, stepB.doc)
|
||||||
|
} else {
|
||||||
|
step = undefined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
step = undefined
|
||||||
|
}
|
||||||
|
} else if (addedStep !== undefined) {
|
||||||
|
step = step?.merge(addedStep) ?? undefined
|
||||||
|
}
|
||||||
|
return step
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFromPath (obj: any, path: string): any {
|
||||||
|
const pathParts = path.split('/')
|
||||||
|
pathParts.shift() // remove root
|
||||||
|
while (pathParts.length > 0) {
|
||||||
|
const property = pathParts.shift()
|
||||||
|
obj = obj[property ?? '']
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeMarks (doc: Node): Node {
|
||||||
|
const tr = new Transform(doc)
|
||||||
|
tr.removeMark(0, doc.nodeSize - 2)
|
||||||
|
return tr.doc
|
||||||
|
}
|
||||||
|
|
||||||
|
function clone (obj: any): any {
|
||||||
|
if (typeof obj === 'function') {
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
const result: any = Array.isArray(obj) ? [] : {}
|
||||||
|
for (const key in obj) {
|
||||||
|
// include prototype properties
|
||||||
|
const value = obj[key]
|
||||||
|
const type = {}.toString.call(value).slice(8, -1)
|
||||||
|
if (type === 'Array') {
|
||||||
|
result[key] = clone(value)
|
||||||
|
} else if (type === 'Object') {
|
||||||
|
result[key] = clone(value)
|
||||||
|
} else if (type === 'Date') {
|
||||||
|
result[key] = new Date(value.getTime())
|
||||||
|
} else {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StepTransform {
|
||||||
|
schema: Schema
|
||||||
|
tr: Transform
|
||||||
|
currentDoc: any
|
||||||
|
finalDoc: any
|
||||||
|
ops: Operation[] = []
|
||||||
|
|
||||||
|
constructor (readonly fromDoc: Node, readonly toDoc: Node) {
|
||||||
|
this.schema = fromDoc.type.schema
|
||||||
|
this.tr = new Transform(fromDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
init (): Transform {
|
||||||
|
this.currentDoc = removeMarks(this.fromDoc).toJSON()
|
||||||
|
this.finalDoc = removeMarks(this.toDoc).toJSON()
|
||||||
|
this.ops = createPatch(this.currentDoc, this.finalDoc, (input: any, output: any, ptr: Pointer) => {
|
||||||
|
if (Array.isArray(input) && Array.isArray(output)) {
|
||||||
|
return diffArraysPM(input, output, ptr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.recreateChangeContentSteps()
|
||||||
|
this.recreateChangeMarkSteps()
|
||||||
|
this.tr = simplifyTransform(this.tr) ?? this.tr
|
||||||
|
|
||||||
|
return this.tr
|
||||||
|
}
|
||||||
|
|
||||||
|
recreateChangeContentSteps (): void {
|
||||||
|
// First step: find content changing steps.
|
||||||
|
let ops = []
|
||||||
|
while (this.ops.length > 0) {
|
||||||
|
// get next
|
||||||
|
let op = this.ops.shift() as Operation
|
||||||
|
ops.push(op)
|
||||||
|
|
||||||
|
let toDoc
|
||||||
|
const afterStepJSON = clone(this.currentDoc) // working document receiving patches
|
||||||
|
const pathParts = op.path.split('/')
|
||||||
|
|
||||||
|
// collect operations until we receive a valid document:
|
||||||
|
// apply ops-patches until a valid prosemirror document is retrieved,
|
||||||
|
// then try to create a transformation step or retry with next operation
|
||||||
|
while (toDoc == null) {
|
||||||
|
applyPatch(afterStepJSON, [op])
|
||||||
|
|
||||||
|
try {
|
||||||
|
toDoc = this.schema.nodeFromJSON(afterStepJSON)
|
||||||
|
toDoc.check()
|
||||||
|
} catch (error: any) {
|
||||||
|
toDoc = null
|
||||||
|
if (this.ops.length > 0) {
|
||||||
|
op = this.ops.shift() as Operation
|
||||||
|
ops.push(op)
|
||||||
|
} else {
|
||||||
|
throw new Error(`No valid diff possible applying ${op.path} ${JSON.stringify(error, undefined, 2)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply operation (ignoring afterStepJSON)
|
||||||
|
if (ops.length === 1 && (pathParts.includes('attrs') || pathParts.includes('type'))) {
|
||||||
|
// Node markup is changing
|
||||||
|
this.addSetNodeMarkup() // a lost update is ignored
|
||||||
|
ops = []
|
||||||
|
} else if (ops.length === 1 && op.op === 'replace' && pathParts[pathParts.length - 1] === 'text') {
|
||||||
|
// Text is being replaced, we apply text diffing to find the smallest possible diffs.
|
||||||
|
this.addReplaceTextSteps(op, afterStepJSON)
|
||||||
|
ops = []
|
||||||
|
} else if (this.addReplaceStep(toDoc, afterStepJSON)) {
|
||||||
|
// operations have been applied
|
||||||
|
ops = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addSetNodeMarkup (): boolean {
|
||||||
|
const fromDoc = this.schema.nodeFromJSON(this.currentDoc)
|
||||||
|
const toDoc = this.schema.nodeFromJSON(this.finalDoc)
|
||||||
|
const start = toDoc.content.findDiffStart(fromDoc.content)
|
||||||
|
// @note start is the same (first) position for current and target document
|
||||||
|
const fromNode = fromDoc.nodeAt(start ?? 0)
|
||||||
|
const toNode = toDoc.nodeAt(start ?? 0)
|
||||||
|
|
||||||
|
if (start != null) {
|
||||||
|
// @note this completly updates all attributes in one step, by completely replacing node
|
||||||
|
const nodeType = fromNode?.type === toNode?.type ? null : toNode?.type
|
||||||
|
try {
|
||||||
|
this.tr.setNodeMarkup(start, nodeType, toNode?.attrs, toNode?.marks)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (nodeType != null && (e.message as string).includes('Invalid content')) {
|
||||||
|
// @todo add test-case for this scenario
|
||||||
|
if (fromNode != null && toNode != null) {
|
||||||
|
this.tr.replaceWith(start, start + fromNode.nodeSize, toNode)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.currentDoc = removeMarks(this.tr.doc).toJSON()
|
||||||
|
// setting the node markup may have invalidated the following ops, so we calculate them again.
|
||||||
|
this.ops = createPatch(this.currentDoc, this.finalDoc)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
recreateChangeMarkSteps (): void {
|
||||||
|
// Now the documents should be the same, except their marks, so everything should map 1:1.
|
||||||
|
// Second step: Iterate through the toDoc and make sure all marks are the same in tr.doc
|
||||||
|
this.toDoc.descendants((tNode, tPos) => {
|
||||||
|
if (!tNode.isInline) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tr.doc.nodesBetween(tPos, tPos + tNode.nodeSize, (fNode, fPos) => {
|
||||||
|
if (!fNode.isInline) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const from = Math.max(tPos, fPos)
|
||||||
|
const to = Math.min(tPos + tNode.nodeSize, fPos + fNode.nodeSize)
|
||||||
|
fNode.marks.forEach((nodeMark) => {
|
||||||
|
if (!nodeMark.isInSet(tNode.marks)) {
|
||||||
|
this.tr.removeMark(from, to, nodeMark)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tNode.marks.forEach((nodeMark) => {
|
||||||
|
if (!nodeMark.isInSet(fNode.marks)) {
|
||||||
|
this.tr.addMark(from, to, nodeMark)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addReplaceStep (toDoc: Node, afterStepJSON: any): boolean {
|
||||||
|
const fromDoc = this.schema.nodeFromJSON(this.currentDoc)
|
||||||
|
const step = getReplaceStep(fromDoc, toDoc)
|
||||||
|
|
||||||
|
if (step == null) {
|
||||||
|
return false
|
||||||
|
} else if (this.tr.maybeStep(step).failed === null) {
|
||||||
|
this.currentDoc = afterStepJSON
|
||||||
|
return true // @change previously null
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('No valid step found.')
|
||||||
|
}
|
||||||
|
|
||||||
|
addReplaceTextSteps (op: any, afterStepJSON: any): void {
|
||||||
|
// We find the position number of the first character in the string
|
||||||
|
const op1 = { ...op, value: 'xx' }
|
||||||
|
const op2 = { ...op, value: 'yy' }
|
||||||
|
const afterOP1JSON = clone(this.currentDoc)
|
||||||
|
const afterOP2JSON = clone(this.currentDoc)
|
||||||
|
applyPatch(afterOP1JSON, [op1])
|
||||||
|
applyPatch(afterOP2JSON, [op2])
|
||||||
|
const op1Doc = this.schema.nodeFromJSON(afterOP1JSON)
|
||||||
|
const op2Doc = this.schema.nodeFromJSON(afterOP2JSON)
|
||||||
|
|
||||||
|
// get text diffs
|
||||||
|
const finalText = op.value
|
||||||
|
const currentText = getFromPath(this.currentDoc, op.path)
|
||||||
|
const textDiffs = diffWordsWithSpace(currentText, finalText)
|
||||||
|
|
||||||
|
let offset = op1Doc.content.findDiffStart(op2Doc.content) as number
|
||||||
|
const marks = op1Doc.resolve(offset + 1).marks()
|
||||||
|
|
||||||
|
while (textDiffs.length > 0) {
|
||||||
|
const diff = textDiffs.shift() as Change
|
||||||
|
|
||||||
|
if (diff.added === true) {
|
||||||
|
const textNode = this.schema.nodeFromJSON({ type: 'text', text: diff.value }).mark(marks)
|
||||||
|
|
||||||
|
if (textDiffs.length > 0 && textDiffs[0].removed === true) {
|
||||||
|
const nextDiff = textDiffs.shift() as Change
|
||||||
|
this.tr.replaceWith(offset, offset + nextDiff.value.length, textNode)
|
||||||
|
} else {
|
||||||
|
this.tr.insert(offset, textNode)
|
||||||
|
}
|
||||||
|
offset += diff.value.length
|
||||||
|
} else if (diff.removed === true) {
|
||||||
|
if (textDiffs.length > 0 && textDiffs[0].added === true) {
|
||||||
|
const nextDiff = textDiffs.shift() as Change
|
||||||
|
const textNode = this.schema.nodeFromJSON({ type: 'text', text: nextDiff.value }).mark(marks)
|
||||||
|
this.tr.replaceWith(offset, offset + diff.value.length, textNode)
|
||||||
|
offset += nextDiff.value.length
|
||||||
|
} else {
|
||||||
|
this.tr.delete(offset, offset + diff.value.length)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
offset += diff.value.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentDoc = afterStepJSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recreateTransform (fromDoc: Node, toDoc: Node): Transform {
|
||||||
|
const recreator = new StepTransform(fromDoc, toDoc)
|
||||||
|
return recreator.init()
|
||||||
|
}
|
8
packages/text-editor/src/components/icons/Header.svelte
Normal file
8
packages/text-editor/src/components/icons/Header.svelte
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let size: 'small' | 'medium' | 'large'
|
||||||
|
const fill: string = 'currentColor'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg class="svg-{size}" {fill} viewBox="0 0 19 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.818182 24V0.727272H3.63636V11.0909H16.0455V0.727272H18.8636V24H16.0455V13.5909H3.63636V24H0.818182Z" />
|
||||||
|
</svg>
|
@ -26,6 +26,7 @@ export { default as TextEditor } from './components/TextEditor.svelte'
|
|||||||
export { default as EmojiPopup } from './components/EmojiPopup.svelte'
|
export { default as EmojiPopup } from './components/EmojiPopup.svelte'
|
||||||
export { default as FullDescriptionBox } from './components/FullDescriptionBox.svelte'
|
export { default as FullDescriptionBox } from './components/FullDescriptionBox.svelte'
|
||||||
export { default as CollaboratorEditor } from './components/CollaboratorEditor.svelte'
|
export { default as CollaboratorEditor } from './components/CollaboratorEditor.svelte'
|
||||||
|
export { default as CollaborationDiffViewer } from './components/CollaborationDiffViewer.svelte'
|
||||||
export { default } from './plugin'
|
export { default } from './plugin'
|
||||||
export * from './types'
|
export * from './types'
|
||||||
|
|
||||||
|
@ -54,6 +54,7 @@ export default plugin(textEditorId, {
|
|||||||
Food: '' as IntlString,
|
Food: '' as IntlString,
|
||||||
Objects: '' as IntlString,
|
Objects: '' as IntlString,
|
||||||
FullDescription: '' as IntlString,
|
FullDescription: '' as IntlString,
|
||||||
NoFullDescription: '' as IntlString
|
NoFullDescription: '' as IntlString,
|
||||||
|
EnableDiffMode: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -34,7 +34,8 @@ export const FORMAT_MODES = [
|
|||||||
'bulletList',
|
'bulletList',
|
||||||
'blockquote',
|
'blockquote',
|
||||||
'code',
|
'code',
|
||||||
'codeBlock'
|
'codeBlock',
|
||||||
|
'heading'
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export type FormatMode = typeof FORMAT_MODES[number]
|
export type FormatMode = typeof FORMAT_MODES[number]
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright © 2022 Hardcore Engineering Inc.
|
|
||||||
//
|
|
||||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License. You may
|
|
||||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
//
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
//
|
|
||||||
import { generateId } from '@hcengineering/core'
|
|
||||||
import { Node } from '@tiptap/core'
|
|
||||||
import { Plugin } from 'prosemirror-state'
|
|
||||||
|
|
||||||
export const UniqId = Node.create({
|
|
||||||
name: 'blockId',
|
|
||||||
|
|
||||||
addGlobalAttributes () {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
types: ['heading', 'paragraph'],
|
|
||||||
attributes: {
|
|
||||||
uid: {
|
|
||||||
default: undefined,
|
|
||||||
rendered: false,
|
|
||||||
keepOnSplit: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
addProseMirrorPlugins () {
|
|
||||||
return [
|
|
||||||
new Plugin({
|
|
||||||
appendTransaction: (_transactions, oldState, newState) => {
|
|
||||||
// no changes
|
|
||||||
if (newState.doc === oldState.doc) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const tr = newState.tr
|
|
||||||
|
|
||||||
newState.doc.descendants((node, pos, parent) => {
|
|
||||||
if (node.isBlock && parent === newState.doc && node.attrs?.uid === undefined) {
|
|
||||||
tr.setNodeMarkup(pos, undefined, {
|
|
||||||
...node.attrs,
|
|
||||||
uid: generateId()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return tr
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
@ -29,6 +29,7 @@
|
|||||||
export let isAside: boolean = true
|
export let isAside: boolean = true
|
||||||
export let isFullSize: boolean = false
|
export let isFullSize: boolean = false
|
||||||
export let withoutTitle: boolean = false
|
export let withoutTitle: boolean = false
|
||||||
|
export let floatAside = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
@ -39,7 +40,9 @@
|
|||||||
$: twoRows = $deviceInfo.minWidth
|
$: twoRows = $deviceInfo.minWidth
|
||||||
|
|
||||||
const checkPanel = (): void => {
|
const checkPanel = (): void => {
|
||||||
if (panelWidth <= 900 && !asideFloat) asideFloat = true
|
if (floatAside) {
|
||||||
|
asideFloat = true
|
||||||
|
} else if (panelWidth <= 900 && !asideFloat) asideFloat = true
|
||||||
else if (panelWidth > 900 && asideFloat) {
|
else if (panelWidth > 900 && asideFloat) {
|
||||||
asideFloat = false
|
asideFloat = false
|
||||||
asideShown = false
|
asideShown = false
|
||||||
|
@ -46,6 +46,8 @@
|
|||||||
"Approve": "Approve",
|
"Approve": "Approve",
|
||||||
"Reject": "Reject",
|
"Reject": "Reject",
|
||||||
"Approved": "Approved",
|
"Approved": "Approved",
|
||||||
"Rejected": "Rejected"
|
"Rejected": "Rejected",
|
||||||
|
|
||||||
|
"CompareTo": "Compare to..."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -46,6 +46,8 @@
|
|||||||
"Approve": "Утвердить",
|
"Approve": "Утвердить",
|
||||||
"Reject": "Отказать",
|
"Reject": "Отказать",
|
||||||
"Approved": "Утверждено",
|
"Approved": "Утверждено",
|
||||||
"Rejected": "Отказано"
|
"Rejected": "Отказано",
|
||||||
|
|
||||||
|
"CompareTo": "Сравнить с..."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,20 +22,32 @@
|
|||||||
import document from '../plugin'
|
import document from '../plugin'
|
||||||
|
|
||||||
import { CollaboratorEditor } from '@hcengineering/text-editor'
|
import { CollaboratorEditor } from '@hcengineering/text-editor'
|
||||||
|
import { Markup } from '@hcengineering/core'
|
||||||
|
|
||||||
export let object: DocumentVersion
|
export let object: DocumentVersion
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
export let initialContentId: string | undefined = undefined
|
export let initialContentId: string | undefined = undefined
|
||||||
|
export let suggestMode = false
|
||||||
|
export let comparedVersion: Markup | undefined = undefined
|
||||||
|
|
||||||
const token = getMetadata(login.metadata.LoginToken) ?? ''
|
const token = getMetadata(login.metadata.LoginToken) ?? ''
|
||||||
const collaboratorURL = getMetadata(document.metadata.CollaboratorUrl) ?? ''
|
const collaboratorURL = getMetadata(document.metadata.CollaboratorUrl) ?? ''
|
||||||
|
let editor: CollaboratorEditor
|
||||||
|
export function getHTML (): string {
|
||||||
|
return editor.getHTML()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CollaboratorEditor
|
{#key comparedVersion}
|
||||||
documentId={object.contentAttachmentId}
|
<CollaboratorEditor
|
||||||
{token}
|
documentId={object.contentAttachmentId}
|
||||||
{collaboratorURL}
|
{token}
|
||||||
{readonly}
|
{suggestMode}
|
||||||
on:content
|
{collaboratorURL}
|
||||||
{initialContentId}
|
{readonly}
|
||||||
/>
|
on:content
|
||||||
|
{initialContentId}
|
||||||
|
{comparedVersion}
|
||||||
|
bind:this={editor}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
@ -29,8 +29,9 @@
|
|||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import { Panel } from '@hcengineering/panel'
|
import { Panel } from '@hcengineering/panel'
|
||||||
import { getResource, translate } from '@hcengineering/platform'
|
import { getResource, translate } from '@hcengineering/platform'
|
||||||
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import tags from '@hcengineering/tags'
|
import tags from '@hcengineering/tags'
|
||||||
|
import { CollaborationDiffViewer } from '@hcengineering/text-editor'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -161,6 +162,7 @@
|
|||||||
{ sort: { version: 1 } }
|
{ sort: { version: 1 } }
|
||||||
)
|
)
|
||||||
let version: DocumentVersion | undefined
|
let version: DocumentVersion | undefined
|
||||||
|
let compareTo: DocumentVersion | undefined
|
||||||
|
|
||||||
let info: any
|
let info: any
|
||||||
|
|
||||||
@ -205,6 +207,25 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectCompareToVersion (event: MouseEvent): void {
|
||||||
|
showPopup(
|
||||||
|
SelectPopup,
|
||||||
|
{
|
||||||
|
value: [{ id: null, text: '-' }, ...info.slice(0, info.length - 1)],
|
||||||
|
placeholder: document.string.Version,
|
||||||
|
searchable: true
|
||||||
|
},
|
||||||
|
eventToHTMLElement(event),
|
||||||
|
(res) => {
|
||||||
|
if (res != null) {
|
||||||
|
compareTo = versions.find((it) => it._id === res)
|
||||||
|
} else if (res === null) {
|
||||||
|
compareTo = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
$: readonly = !documentObject?.authors.includes(currentUser.employee)
|
$: readonly = !documentObject?.authors.includes(currentUser.employee)
|
||||||
|
|
||||||
let autoSelect = true
|
let autoSelect = true
|
||||||
@ -213,8 +234,8 @@
|
|||||||
let mode: ModelType = 'view'
|
let mode: ModelType = 'view'
|
||||||
const modeLabels = {
|
const modeLabels = {
|
||||||
view: document.string.ViewMode,
|
view: document.string.ViewMode,
|
||||||
edit: document.string.EditMode
|
edit: document.string.EditMode,
|
||||||
// ,suggest: document.string.SuggestMode
|
suggest: document.string.SuggestMode
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectMode (event: MouseEvent): void {
|
function selectMode (event: MouseEvent): void {
|
||||||
@ -304,8 +325,6 @@
|
|||||||
|
|
||||||
let processing = false
|
let processing = false
|
||||||
|
|
||||||
let content = ''
|
|
||||||
|
|
||||||
const updateRequests = async (kind: DocumentRequestKind): Promise<void> => {
|
const updateRequests = async (kind: DocumentRequestKind): Promise<void> => {
|
||||||
processing = true
|
processing = true
|
||||||
if (documentObject === undefined) {
|
if (documentObject === undefined) {
|
||||||
@ -335,7 +354,7 @@
|
|||||||
|
|
||||||
if (version) {
|
if (version) {
|
||||||
await client.update(version, {
|
await client.update(version, {
|
||||||
content
|
content: editor.getHTML()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +362,7 @@
|
|||||||
|
|
||||||
processing = false
|
processing = false
|
||||||
}
|
}
|
||||||
|
let editor: DocumentEditor
|
||||||
const updateState = async (state: DocumentVersionState): Promise<void> => {
|
const updateState = async (state: DocumentVersionState): Promise<void> => {
|
||||||
processing = true
|
processing = true
|
||||||
if (documentObject === undefined) {
|
if (documentObject === undefined) {
|
||||||
@ -377,6 +396,12 @@
|
|||||||
|
|
||||||
processing = false
|
processing = false
|
||||||
}
|
}
|
||||||
|
async function switchToDraft (): Promise<void> {
|
||||||
|
const requests = await client.findAll(document.class.DocumentRequest, { attachedTo: documentObject?._id })
|
||||||
|
for (const r of requests) {
|
||||||
|
client.remove(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if documentObject !== undefined}
|
{#if documentObject !== undefined}
|
||||||
@ -386,6 +411,7 @@
|
|||||||
isAside={true}
|
isAside={true}
|
||||||
isSub={false}
|
isSub={false}
|
||||||
bind:innerWidth
|
bind:innerWidth
|
||||||
|
floatAside={true}
|
||||||
on:close={() => dispatch('close')}
|
on:close={() => dispatch('close')}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="navigator">
|
<svelte:fragment slot="navigator">
|
||||||
@ -410,6 +436,21 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
loading={processing}
|
||||||
|
kind={'link-bordered'}
|
||||||
|
on:click={selectCompareToVersion}
|
||||||
|
disabled={info.length < 2}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="content">
|
||||||
|
{#if compareTo}
|
||||||
|
{compareTo.version} - {labels[compareTo.state]}
|
||||||
|
{:else}
|
||||||
|
<Label label={document.string.CompareTo} />
|
||||||
|
{/if}
|
||||||
|
</svelte:fragment>
|
||||||
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="tools">
|
<svelte:fragment slot="tools">
|
||||||
@ -465,6 +506,16 @@
|
|||||||
icon={IconClose}
|
icon={IconClose}
|
||||||
size={'medium'}
|
size={'medium'}
|
||||||
/>
|
/>
|
||||||
|
{#if !readonly}
|
||||||
|
<Button
|
||||||
|
loading={processing}
|
||||||
|
kind={'link-bordered'}
|
||||||
|
label={document.string.Draft}
|
||||||
|
on:click={() => switchToDraft()}
|
||||||
|
icon={IconEdit}
|
||||||
|
size={'medium'}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{#if !readonly && version?.state === DocumentVersionState.Draft && approveRequest === undefined}
|
{#if !readonly && version?.state === DocumentVersionState.Draft && approveRequest === undefined}
|
||||||
<Button loading={processing} kind={'link-bordered'} on:click={selectMode} icon={IconEdit} size={'medium'}>
|
<Button loading={processing} kind={'link-bordered'} on:click={selectMode} icon={IconEdit} size={'medium'}>
|
||||||
@ -479,17 +530,19 @@
|
|||||||
<div class="description-preview select-text mt-2 emphasized">
|
<div class="description-preview select-text mt-2 emphasized">
|
||||||
{#if version && version.state === DocumentVersionState.Draft && approveRequest === undefined}
|
{#if version && version.state === DocumentVersionState.Draft && approveRequest === undefined}
|
||||||
{#key version?._id}
|
{#key version?._id}
|
||||||
|
<!-- suggestMode={mode === 'suggest'} -->
|
||||||
<DocumentEditor
|
<DocumentEditor
|
||||||
object={version}
|
object={version}
|
||||||
initialContentId={version.initialContentId}
|
initialContentId={version.initialContentId}
|
||||||
|
comparedVersion={compareTo?.content ?? versions[versions.length - 1].content}
|
||||||
readonly={mode === 'view'}
|
readonly={mode === 'view'}
|
||||||
on:content={(evt) => {
|
bind:this={editor}
|
||||||
content = evt.detail
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
{:else if version}
|
{:else if version}
|
||||||
<MessageViewer message={version.content} />
|
{#key [compareTo?.content, version.content]}
|
||||||
|
<CollaborationDiffViewer content={version.content} comparedVersion={compareTo?.content} />
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -533,6 +586,7 @@
|
|||||||
.description-preview {
|
.description-preview {
|
||||||
color: var(--theme-content-color);
|
color: var(--theme-content-color);
|
||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
|
@ -76,6 +76,8 @@ export default mergeIds(documentId, document, {
|
|||||||
Requests: '' as IntlString,
|
Requests: '' as IntlString,
|
||||||
|
|
||||||
Approve: '' as IntlString,
|
Approve: '' as IntlString,
|
||||||
Reject: '' as IntlString
|
Reject: '' as IntlString,
|
||||||
|
|
||||||
|
CompareTo: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user