From 2792279f9a9ad97ac18cd91aab1a489531cfc8bc Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 21 Dec 2025 15:12:51 -0800 Subject: [PATCH] add edra tiptap editor component copied from edra library with svelte 5 fix for onTransaction callback --- package.json | 31 +- pnpm-lock.yaml | 645 +++++++++++++----- src/lib/components/edra/commands/index.ts | 1 + .../edra/commands/toolbar-commands.ts | 503 ++++++++++++++ src/lib/components/edra/commands/types.ts | 13 + .../edra/components/BubbleMenu.svelte | 67 ++ .../edra/components/DragHandle.svelte | 31 + .../edra/components/MediaPlaceHolder.svelte | 30 + src/lib/components/edra/editor.css | 493 +++++++++++++ src/lib/components/edra/editor.ts | 130 ++++ .../edra/extensions/ColorHighlighter.ts | 28 + .../edra/extensions/FindAndReplace.ts | 408 +++++++++++ .../edra/extensions/InlineMathReplacer.ts | 21 + .../edra/extensions/audio/AudiExtended.ts | 34 + .../edra/extensions/audio/AudioExtension.ts | 147 ++++ .../edra/extensions/audio/AudioPlaceholder.ts | 64 ++ .../drag-handle/ClipboardSerializer.ts | 28 + .../edra/extensions/drag-handle/index.ts | 381 +++++++++++ .../edra/extensions/iframe/IFrame.ts | 85 +++ .../edra/extensions/iframe/IFrameExtended.ts | 35 + .../extensions/iframe/IFramePlaceholder.ts | 56 ++ .../edra/extensions/image/ImageExtended.ts | 36 + .../edra/extensions/image/ImagePlaceholder.ts | 64 ++ .../edra/extensions/slash-command/groups.ts | 59 ++ .../extensions/slash-command/slashcommand.ts | 259 +++++++ .../components/edra/extensions/table/index.ts | 4 + .../edra/extensions/table/table-cell.ts | 124 ++++ .../edra/extensions/table/table-header.ts | 89 +++ .../edra/extensions/table/table-row.ts | 8 + .../components/edra/extensions/table/table.ts | 9 + .../components/edra/extensions/table/utils.ts | 322 +++++++++ .../edra/extensions/video/VideoExtended.ts | 34 + .../edra/extensions/video/VideoExtension.ts | 147 ++++ .../edra/extensions/video/VideoPlaceholder.ts | 62 ++ .../headless/components/AudioExtended.svelte | 13 + .../components/AudioPlaceHolder.svelte | 21 + .../edra/headless/components/CodeBlock.svelte | 47 ++ .../headless/components/IFrameExtended.svelte | 13 + .../components/IFramePlaceHolder.svelte | 21 + .../headless/components/ImageExtended.svelte | 13 + .../components/ImagePlaceholder.svelte | 21 + .../headless/components/MediaExtended.svelte | 243 +++++++ .../components/SlashCommandList.svelte | 114 ++++ .../headless/components/ToolBarIcon.svelte | 26 + .../headless/components/VideoExtended.svelte | 15 + .../components/VideoPlaceholder.svelte | 21 + .../components/toolbar/FontSize.svelte | 52 ++ .../components/toolbar/QuickColors.svelte | 70 ++ .../toolbar/SearchAndReplace.svelte | 120 ++++ .../components/edra/headless/editor.svelte | 109 +++ src/lib/components/edra/headless/index.ts | 3 + .../edra/headless/menus/Link.svelte | 44 ++ .../edra/headless/menus/Menu.svelte | 96 +++ .../edra/headless/menus/TableCol.svelte | 53 ++ .../edra/headless/menus/TableRow.svelte | 53 ++ src/lib/components/edra/headless/style.css | 340 +++++++++ .../components/edra/headless/toolbar.svelte | 32 + src/lib/components/edra/onedark.css | 176 +++++ src/lib/components/edra/svelte-renderer.ts | 75 ++ src/lib/components/edra/types.ts | 30 + src/lib/components/edra/utils.ts | 119 ++++ 61 files changed, 6201 insertions(+), 187 deletions(-) create mode 100644 src/lib/components/edra/commands/index.ts create mode 100644 src/lib/components/edra/commands/toolbar-commands.ts create mode 100644 src/lib/components/edra/commands/types.ts create mode 100644 src/lib/components/edra/components/BubbleMenu.svelte create mode 100644 src/lib/components/edra/components/DragHandle.svelte create mode 100644 src/lib/components/edra/components/MediaPlaceHolder.svelte create mode 100644 src/lib/components/edra/editor.css create mode 100644 src/lib/components/edra/editor.ts create mode 100644 src/lib/components/edra/extensions/ColorHighlighter.ts create mode 100644 src/lib/components/edra/extensions/FindAndReplace.ts create mode 100644 src/lib/components/edra/extensions/InlineMathReplacer.ts create mode 100644 src/lib/components/edra/extensions/audio/AudiExtended.ts create mode 100644 src/lib/components/edra/extensions/audio/AudioExtension.ts create mode 100644 src/lib/components/edra/extensions/audio/AudioPlaceholder.ts create mode 100644 src/lib/components/edra/extensions/drag-handle/ClipboardSerializer.ts create mode 100644 src/lib/components/edra/extensions/drag-handle/index.ts create mode 100644 src/lib/components/edra/extensions/iframe/IFrame.ts create mode 100644 src/lib/components/edra/extensions/iframe/IFrameExtended.ts create mode 100644 src/lib/components/edra/extensions/iframe/IFramePlaceholder.ts create mode 100644 src/lib/components/edra/extensions/image/ImageExtended.ts create mode 100644 src/lib/components/edra/extensions/image/ImagePlaceholder.ts create mode 100644 src/lib/components/edra/extensions/slash-command/groups.ts create mode 100644 src/lib/components/edra/extensions/slash-command/slashcommand.ts create mode 100644 src/lib/components/edra/extensions/table/index.ts create mode 100644 src/lib/components/edra/extensions/table/table-cell.ts create mode 100644 src/lib/components/edra/extensions/table/table-header.ts create mode 100644 src/lib/components/edra/extensions/table/table-row.ts create mode 100644 src/lib/components/edra/extensions/table/table.ts create mode 100644 src/lib/components/edra/extensions/table/utils.ts create mode 100644 src/lib/components/edra/extensions/video/VideoExtended.ts create mode 100644 src/lib/components/edra/extensions/video/VideoExtension.ts create mode 100644 src/lib/components/edra/extensions/video/VideoPlaceholder.ts create mode 100644 src/lib/components/edra/headless/components/AudioExtended.svelte create mode 100644 src/lib/components/edra/headless/components/AudioPlaceHolder.svelte create mode 100644 src/lib/components/edra/headless/components/CodeBlock.svelte create mode 100644 src/lib/components/edra/headless/components/IFrameExtended.svelte create mode 100644 src/lib/components/edra/headless/components/IFramePlaceHolder.svelte create mode 100644 src/lib/components/edra/headless/components/ImageExtended.svelte create mode 100644 src/lib/components/edra/headless/components/ImagePlaceholder.svelte create mode 100644 src/lib/components/edra/headless/components/MediaExtended.svelte create mode 100644 src/lib/components/edra/headless/components/SlashCommandList.svelte create mode 100644 src/lib/components/edra/headless/components/ToolBarIcon.svelte create mode 100644 src/lib/components/edra/headless/components/VideoExtended.svelte create mode 100644 src/lib/components/edra/headless/components/VideoPlaceholder.svelte create mode 100644 src/lib/components/edra/headless/components/toolbar/FontSize.svelte create mode 100644 src/lib/components/edra/headless/components/toolbar/QuickColors.svelte create mode 100644 src/lib/components/edra/headless/components/toolbar/SearchAndReplace.svelte create mode 100644 src/lib/components/edra/headless/editor.svelte create mode 100644 src/lib/components/edra/headless/index.ts create mode 100644 src/lib/components/edra/headless/menus/Link.svelte create mode 100644 src/lib/components/edra/headless/menus/Menu.svelte create mode 100644 src/lib/components/edra/headless/menus/TableCol.svelte create mode 100644 src/lib/components/edra/headless/menus/TableRow.svelte create mode 100644 src/lib/components/edra/headless/style.css create mode 100644 src/lib/components/edra/headless/toolbar.svelte create mode 100644 src/lib/components/edra/onedark.css create mode 100644 src/lib/components/edra/svelte-renderer.ts create mode 100644 src/lib/components/edra/types.ts create mode 100644 src/lib/components/edra/utils.ts diff --git a/package.json b/package.json index 3efb8ae1..30012588 100644 --- a/package.json +++ b/package.json @@ -68,20 +68,41 @@ }, "packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67", "dependencies": { + "@floating-ui/dom": "^1.7.4", "@friendofsvelte/tipex": "^0.0.9", "@internationalized/date": "^3.10.0", + "@lucide/svelte": "^0.562.0", "@tanstack/svelte-query": "^6.0.9", - "@tiptap/core": "^3.5.1", - "@tiptap/extension-highlight": "^3.5.1", - "@tiptap/extension-link": "^3.5.1", - "@tiptap/pm": "^3.5.1", - "@tiptap/starter-kit": "^3.5.1", + "@tiptap/core": "^3.14.0", + "@tiptap/extension-bubble-menu": "^3.14.0", + "@tiptap/extension-code-block-lowlight": "^3.14.0", + "@tiptap/extension-floating-menu": "3.14.0", + "@tiptap/extension-highlight": "^3.14.0", + "@tiptap/extension-image": "^3.14.0", + "@tiptap/extension-link": "^3.14.0", + "@tiptap/extension-list": "^3.14.0", + "@tiptap/extension-mathematics": "^3.14.0", + "@tiptap/extension-subscript": "^3.14.0", + "@tiptap/extension-superscript": "^3.14.0", + "@tiptap/extension-table": "^3.14.0", + "@tiptap/extension-text-align": "^3.14.0", + "@tiptap/extension-text-style": "^3.14.0", + "@tiptap/extension-typography": "^3.14.0", + "@tiptap/extensions": "^3.14.0", + "@tiptap/markdown": "^3.14.0", + "@tiptap/pm": "^3.14.0", + "@tiptap/starter-kit": "^3.14.0", + "@tiptap/suggestion": "^3.14.0", "bits-ui": "^2.9.6", "fluid-dnd": "^2.6.2", + "katex": "^0.16.27", + "lowlight": "^3.3.0", "modern-normalize": "^3.0.1", "runed": "^0.31.1", "svelecte": "^5.3.0", "svelte-sonner": "^1.0.7", + "svelte-tiptap": "^3.0.1", + "tiptap-extension-auto-joiner": "^0.1.3", "wx-grid-data-provider": "^2.2.0", "wx-svelte-grid": "^2.0.0", "zod": "^4.1.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7abb837..c8998620 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,36 +8,93 @@ importers: .: dependencies: + '@floating-ui/dom': + specifier: ^1.7.4 + version: 1.7.4 '@friendofsvelte/tipex': specifier: ^0.0.9 - version: 0.0.9(highlight.js@11.8.0)(svelte@5.38.7) + version: 0.0.9(highlight.js@11.11.1)(svelte@5.38.7) '@internationalized/date': specifier: ^3.10.0 version: 3.10.0 + '@lucide/svelte': + specifier: ^0.562.0 + version: 0.562.0(svelte@5.38.7) '@tanstack/svelte-query': specifier: ^6.0.9 version: 6.0.9(svelte@5.38.7) '@tiptap/core': - specifier: ^3.5.1 - version: 3.5.1(@tiptap/pm@3.5.1) + specifier: ^3.14.0 + version: 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/extension-bubble-menu': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-code-block-lowlight': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/extension-code-block@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)(highlight.js@11.11.1)(lowlight@3.3.0) + '@tiptap/extension-floating-menu': + specifier: 3.14.0 + version: 3.14.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) '@tiptap/extension-highlight': - specifier: ^3.5.1 - version: 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-image': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) '@tiptap/extension-link': - specifier: ^3.5.1 - version: 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-list': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-mathematics': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)(katex@0.16.27) + '@tiptap/extension-subscript': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-superscript': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-table': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-text-align': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-text-style': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-typography': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extensions': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/markdown': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) '@tiptap/pm': - specifier: ^3.5.1 - version: 3.5.1 + specifier: ^3.14.0 + version: 3.14.0 '@tiptap/starter-kit': - specifier: ^3.5.1 - version: 3.5.1 + specifier: ^3.14.0 + version: 3.14.0 + '@tiptap/suggestion': + specifier: ^3.14.0 + version: 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) bits-ui: specifier: ^2.9.6 version: 2.9.6(@internationalized/date@3.10.0)(svelte@5.38.7) fluid-dnd: specifier: ^2.6.2 version: 2.6.2 + katex: + specifier: ^0.16.27 + version: 0.16.27 + lowlight: + specifier: ^3.3.0 + version: 3.3.0 modern-normalize: specifier: ^3.0.1 version: 3.0.1 @@ -50,6 +107,12 @@ importers: svelte-sonner: specifier: ^1.0.7 version: 1.0.7(svelte@5.38.7) + svelte-tiptap: + specifier: ^3.0.1 + version: 3.0.1(@floating-ui/dom@1.7.4)(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/extension-bubble-menu@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))(@tiptap/extension-floating-menu@3.14.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)(svelte@5.38.7) + tiptap-extension-auto-joiner: + specifier: ^0.1.3 + version: 0.1.3 wx-grid-data-provider: specifier: ^2.2.0 version: 2.2.0 @@ -468,6 +531,11 @@ packages: '@lix-js/server-protocol-schema@0.1.1': resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==} + '@lucide/svelte@0.562.0': + resolution: {integrity: sha512-wDMULwtTFN2Sc/TFBm6gfuVCNb4Y5P9LDrwxNnUbV52+IEU7NXZmvxwXoz+vrrpad6Xupq+Hw5eUlqIHEGhouw==} + peerDependencies: + svelte: ^5 + '@mdx-js/react@3.1.1': resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} peerDependencies: @@ -928,40 +996,46 @@ packages: peerDependencies: '@tiptap/pm': ^2.7.0 - '@tiptap/core@3.5.1': - resolution: {integrity: sha512-OmnL68v+DAHEGEO9C48oV1/F6VUOzb6ozDySrer0cxtbasVlt4QjndoXg99Db3lSgoL0JgRSHR+dBge5RbILAQ==} + '@tiptap/core@3.14.0': + resolution: {integrity: sha512-nm0VWVA1Vq/jaKY3wyRXViL/kf78yMdH7qETpv4qZXDQLU+pdWV3IGoRTQTKESc7d8L1wL/2uCeByLNUJfrSIw==} peerDependencies: - '@tiptap/pm': ^3.5.1 + '@tiptap/pm': ^3.14.0 '@tiptap/extension-blockquote@2.26.2': resolution: {integrity: sha512-SQNMX2rkWdAOYT6pM9KZ4bZK07YiCqX6wkHiKbLSZ8GMLi35dhkiSBxvY2I72q5ucIjgC9asGf8knA/2fbVypA==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-blockquote@3.5.1': - resolution: {integrity: sha512-J08E8BPuH0em+2GAd9NsN0kP7yOX8R4FXnXj18dQf0ihVshb0Wb1F2ZnAXJsl+GYxt5TWejwHJmCtsW1oxIy9Q==} + '@tiptap/extension-blockquote@3.14.0': + resolution: {integrity: sha512-I7aOqcVLHBgCeRtMaMHA+ILSS8Sli46fjFq8477stOpQ79TPiBd6e4SDuFCAu58M94mVLMvlPKF2Eh5IvbIMyQ==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 '@tiptap/extension-bold@2.26.2': resolution: {integrity: sha512-kNjbHZhLyDu2ZBZmJINzXg3MAW7+05KqGkcwxudC1X/DQM5V5FpW7u6TOlC+nf1I9wABgayxURyU8FsIaXDxqA==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-bold@3.5.1': - resolution: {integrity: sha512-Pe8mZ/frTCVcaXcpOVCvWdCP000GfG5bF1dDFnkEkFyeCeEEfCnC5FxcQyP86gXg8goLCau54TghJ3HPYaZn9g==} + '@tiptap/extension-bold@3.14.0': + resolution: {integrity: sha512-T4ma6VLoHm9JupglidD3CfZXm89A3HMv99gLplXNizvy1mlr4R3uC3aBqKw6lAP+NoqCqbIgjwc4YYsqZClNwA==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 + + '@tiptap/extension-bubble-menu@3.14.0': + resolution: {integrity: sha512-nraHy+5jumT67J7hWrCuVwVTS2vNj4FpV5kO8epVySBmgEBr/7Pyi4w7mQA1VRVOMdjeN9iypbgQ2rKhpfaoTw==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 '@tiptap/extension-bullet-list@2.26.2': resolution: {integrity: sha512-L0qxUa5vzUciLEVtr1nY6HG8gH8432GtuX807MM/5wKiYYdbSii3I22456ZnboiozoqXrjjvYUHeB++HhOSPgQ==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-bullet-list@3.5.1': - resolution: {integrity: sha512-Fixu3DyDovOACP9tC04u3hZgo/vFcZdSrbz5MMCfqNVQF8xb/dVuTDoiwgP6zjFBZJb/uf2Rrum5qWdhYOrGig==} + '@tiptap/extension-bullet-list@3.14.0': + resolution: {integrity: sha512-luqPX4u52hiOAHJ95mYsNE+x+9dZxsM461Xny9d/eTXLjAcnwS7MghjrnpljvyYsSXNiwQtxUyEr4uEZZJ5gIQ==} peerDependencies: - '@tiptap/extension-list': ^3.5.1 + '@tiptap/extension-list': ^3.14.0 '@tiptap/extension-code-block-lowlight@2.26.2': resolution: {integrity: sha512-FfNvM8P/WsqatTpKY3V1lu2wuHLttjMQ11nh+F5s4MBoXScSTlsb5V0eUJ4Wmg+ZX0FpAvOtBBOTcJZ2fxtqdA==} @@ -972,37 +1046,46 @@ packages: highlight.js: ^11 lowlight: ^2 || ^3 + '@tiptap/extension-code-block-lowlight@3.14.0': + resolution: {integrity: sha512-vkiDvPZUadrjAGNzvJYYXl5R+U1XmGALSbm+VlrGCR7iXHgYaMHdkqxHwGZMSqtsF2szPEqcAzLZShlAKl+AkA==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/extension-code-block': ^3.14.0 + '@tiptap/pm': ^3.14.0 + highlight.js: ^11 + lowlight: ^2 || ^3 + '@tiptap/extension-code-block@2.26.2': resolution: {integrity: sha512-MJZ4QtziIWJ1zuSW2ogAHv+UHGk3DvGbVi+Dfmo0ybonXX7vRVHE+3qT7OcdTRBF+pC2oCnsjzqwFcGBP3BbZw==} peerDependencies: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 - '@tiptap/extension-code-block@3.5.1': - resolution: {integrity: sha512-R8xy3Y1ws+mq5nU42hEAT3BMHS9ZEyd3n4Xbo8KPCiNuULDeAHdcQkZ7nbiTPbK/XsrvLUs0nl8iauMd469K5w==} + '@tiptap/extension-code-block@3.14.0': + resolution: {integrity: sha512-hRSdIhhm3Q9JBMQdKaifRVFnAa4sG+M7l1QcTKR3VSYVy2/oR0U+aiOifi5OvMRBUwhaR71Ro+cMT9FH9s26Kg==} peerDependencies: - '@tiptap/core': ^3.5.1 - '@tiptap/pm': ^3.5.1 + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 '@tiptap/extension-code@2.26.2': resolution: {integrity: sha512-xnKJvzlAp75dheyaK5tLKAksHf9PtSr8a7OuPjf2IXS5K+QMtnwxx7KAHHijmecfWjLR0wyu9AvT/FWFfKi5LQ==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-code@3.5.1': - resolution: {integrity: sha512-cBHbWTn+cre1TgCnnNiQRQEU76iYIMxVAhn6maqDv/7KCvCUMB19b8y+NTU1rlg9DWVXy2Bd9Il52pXQr01/aA==} + '@tiptap/extension-code@3.14.0': + resolution: {integrity: sha512-Sx9yLorzS+oqNmXID4jt0G5tDnsEgU0HtEXPLD3KNt/ltVxWJU0AXwCsp1/Dg0HIDL868vWpJ2jC1t/4oaf9kA==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 '@tiptap/extension-document@2.26.2': resolution: {integrity: sha512-s0/P3A8zxWL/h3e20xWMTT/rcwD0+57I6mT9JgNBPtvhPePy8d698G6/qFK+x+GdIyjJylfsq2BrSE9H+QhIBg==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-document@3.5.1': - resolution: {integrity: sha512-Ej2dHjHChEA4kam8SpSIwGveJfHglER4cfMBGHFoSxJhmBC8pVfnxHtsqVclgDBB6yTjBhpmG033lj6+QlQCrQ==} + '@tiptap/extension-document@3.14.0': + resolution: {integrity: sha512-O3D7/GPB3XrWGy0y/b4LMHiY0eTd+dyIbSdiFtmUnbC/E9lqQLw43GiqvD9Gm6AyKhBA+Z45dKMbaOe1c6eTwQ==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 '@tiptap/extension-dropcursor@2.26.2': resolution: {integrity: sha512-o5j4Gkurb/WBu1wP2tihYnZ8dENzmlxFWWMx++g6abEpn9xdud7VxHT5Ny7mBSBptI8uMwKT53weYC0on38n3g==} @@ -1010,10 +1093,10 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 - '@tiptap/extension-dropcursor@3.5.1': - resolution: {integrity: sha512-Ywpor6OD8Kr1pgJILCrZUI7gfwvcTPo3iMpm1tbL043+0Fq7TmDzdSaQRn071vZLuMehwYckZQJI/Rw/4OuvLA==} + '@tiptap/extension-dropcursor@3.14.0': + resolution: {integrity: sha512-IwHyiZKLjV9WSBlQFS+afMjucIML8wFAKkG8UKCu+CVOe/Qd1ImDGyv6rzPlCmefJkDHIUWS+c2STapJlUD1VQ==} peerDependencies: - '@tiptap/extensions': ^3.5.1 + '@tiptap/extensions': ^3.14.0 '@tiptap/extension-floating-menu@2.26.2': resolution: {integrity: sha512-AILrhwKAGU4Z6GcjNXJAsN0LHlL26bE7VRrYIqUwDv44ImiQf5vu9jEnncBOeHWzMe8SgjrrJWGNNu+dceACpw==} @@ -1021,41 +1104,48 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 + '@tiptap/extension-floating-menu@3.14.0': + resolution: {integrity: sha512-+ErwDF74NzX4JV0nXMSIUT9V8FDdo85r0SaBZ8lb2NLmElaA3LDklcNV7SsoKlRcwsAXtFkqQbDwXLNGQLYSPQ==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 + '@tiptap/extension-gapcursor@2.26.2': resolution: {integrity: sha512-a68mi8V0mh058UrBIk23f50K5JGVeRZnF6ViptIleAD/Ny1K6VLjGCz6k190de+Tb9tnQLPEwwwDcy+ZnvCmYQ==} peerDependencies: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 - '@tiptap/extension-gapcursor@3.5.1': - resolution: {integrity: sha512-LKz0tYGYf0GwkoVKVUTaL9453cSqKmkPDOkAiheJUJbbPfssyFpqhOk9GQedDhyvzsxaMx1R6BmqxKtnyfA1Og==} + '@tiptap/extension-gapcursor@3.14.0': + resolution: {integrity: sha512-hMg2U59+c9FreYtTvzxx5GWKejdZLRITMLEu4OTfrgQok6uF4qkzGEEqmYqPiHk08TBqAg18Y5bbpyqTsuit9A==} peerDependencies: - '@tiptap/extensions': ^3.5.1 + '@tiptap/extensions': ^3.14.0 '@tiptap/extension-hard-break@2.26.2': resolution: {integrity: sha512-OLpeTey7p3ChyEsABLPvNv7rD/8E4k1JTt+H+MUjyL0dnrZuIWluckUJCJKnV8PhR9Mifngk1MTFUilpooiv1g==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-hard-break@3.5.1': - resolution: {integrity: sha512-BF5c1m/iFpgCdNV8I5EsUmRJFqJoYBBBvjzBHGYuOX7mUTUpjNt1ahDZl2QU51Rx6u28F8RZ2l4pmqyNzGBLbA==} + '@tiptap/extension-hard-break@3.14.0': + resolution: {integrity: sha512-XKxr8usQp+kFevhDK6Ccmnq1CIkLmPClhKwbt7AClGLKLBtEVAS1qUgcmKudkw8cD8Q2/69twI37LXa23sfuLA==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 '@tiptap/extension-heading@2.26.2': resolution: {integrity: sha512-0VAr1l1QKFJ0B2l4D4wV0LRlyFYeJt0S09mz+HPF2TqKF4twmPjaGD6o5zzXWl8c4cQj1CmM8P+9an3SKRjOaA==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-heading@3.5.1': - resolution: {integrity: sha512-rleioqbuoCgs9fEmUYKKbqwdvDHEUQ3O680alpGi4rc6dCi8wncPrPf6AyzEiP+L7M2jHLOu948Pm8U1a1Fxgg==} + '@tiptap/extension-heading@3.14.0': + resolution: {integrity: sha512-4xpahSo3b1dN2nwA0XKXLQVz9nZ/vE443a/Y5QLWeXiu3v9wkcMs/5kQ5ysFeDZRBTfVUWBqhngI7zhvDUx2zQ==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 - '@tiptap/extension-highlight@3.5.1': - resolution: {integrity: sha512-TV+Wo/FfS4HOpge+0EyMjDC3652g74n+05XPB2cAgK7ld/cH/LJgeQ6prSvrD1JHZCoFHWyjMOyOg9S0RgcNAQ==} + '@tiptap/extension-highlight@3.14.0': + resolution: {integrity: sha512-D4SsD4karzlRIIHarZ9C0+gCQ0CPtp2LFyG93RE8GQzi+RgV+B2AaAiWPizXfRs9AN93neLBQ33EkuR9CbDzUw==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 '@tiptap/extension-history@2.26.2': resolution: {integrity: sha512-X/cu79AV5D2Z1QtuvKo/4/Rgl/Uti/n5V3QgCxFLQRCKTxHOCis+RlBCjBfOPztJX4T9QUE6lq20KqB47rsNwQ==} @@ -1069,26 +1159,31 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 - '@tiptap/extension-horizontal-rule@3.5.1': - resolution: {integrity: sha512-zob/1y49yiIUqrMssoGefR55+NSObN8iJ3SGDYPMW3o13S9sUrSLKX8ssH2R9BScUsCovBd5PyExC17tLt7IQA==} + '@tiptap/extension-horizontal-rule@3.14.0': + resolution: {integrity: sha512-65O4T9vPKLUKO1fLowh5jqtfQlH5eaIL7qb/uj5sXMMg8O7TCvBIRkwNuYsFTkJmTk4vBy+fjZ0uwSY3DFkO1g==} peerDependencies: - '@tiptap/core': ^3.5.1 - '@tiptap/pm': ^3.5.1 + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 '@tiptap/extension-image@2.26.2': resolution: {integrity: sha512-3gK+ETLiWGAUdyPDXDheNJ38OgQabSzZJ+1nQo9KWjI7P3LQ7/ctxLtT+hAFpxX0qMK4bnu5vZaItSXxE3ZtpQ==} peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-image@3.14.0': + resolution: {integrity: sha512-lmRU2bhKMDPo+00AiGXZu15jBA9Gmw6QixBWzRrUtsYuFrVAYYCUNIA6mDH7b80935ISqYI+YH1ZlJEmsMptJw==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/extension-italic@2.26.2': resolution: {integrity: sha512-/4AiE2JWtjY9yW+MifMP8EOOwOSDKDUxCyqtGT6e4xqqFUNLZJA0o4P/MYjcKVwsa1/IsDRsOaFRlAiMmAXVXw==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-italic@3.5.1': - resolution: {integrity: sha512-axIwDNlAfhOMUIInJiLm9sZMgCyroUJC6je3CdUsNfM0gQANx1JYI3lF6LgEAWoaJRdMxE+Wb72BGHmRq1loUg==} + '@tiptap/extension-italic@3.14.0': + resolution: {integrity: sha512-Arl5EaG4wdyipwvKjsI7Krlk3OkmqvLfF0YfGwsd5AVDxTiYuiDGgz7RF8J2kttbBeiUTqwME5xpkryQK3F+fg==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 '@tiptap/extension-link@2.26.2': resolution: {integrity: sha512-rzYxx5wI1551ubPfW2pJ3V957cX/WAmbUI3q8Un+LlOsSmbddl+5BjlF5t/vl/pwaOv7FJAz9e29n877zkGOVQ==} @@ -1096,52 +1191,59 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/pm': ^2.7.0 - '@tiptap/extension-link@3.5.1': - resolution: {integrity: sha512-WvpsUqULArJfPQKGAwzmF32/M1xp78ewjg+s29tkKxYLzy+TvotB97AOjK2KqKBvZnWZCrA6GITp4GCmXqsv4Q==} + '@tiptap/extension-link@3.14.0': + resolution: {integrity: sha512-xaeJIktD42rJ4t9fbQpKe+yYNZ+YFIK96cp1Kdm0hZHv/8MPMNRiF85TRY+9U1aoyh5uRcspgCj7EKQb2Hs7qg==} peerDependencies: - '@tiptap/core': ^3.5.1 - '@tiptap/pm': ^3.5.1 + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 '@tiptap/extension-list-item@2.26.2': resolution: {integrity: sha512-T1dFfx1JjRRX0iyStSTwMNajMyT+OE7XEggn+DON1g+zbgA+4cJ11WQpfrfA9VM2H5QWYyKGfHFigoFcJ8rjog==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-list-item@3.5.1': - resolution: {integrity: sha512-kLSpS48jnmxRL7yMqzRhYA9FVqftPwh1BvIiubPvQtgtURag3qSdGLLVVwMIojopBpklDXcMQ9qyRIC6cgd6oA==} + '@tiptap/extension-list-item@3.14.0': + resolution: {integrity: sha512-19Dcp8HCFdhINmRy0KQLFfz9ZEuVwFWGAAjYG7BvMvkd9k4sJ5vCv5fej59G99rhsc+tCmik77w+SLksOcxwKQ==} peerDependencies: - '@tiptap/extension-list': ^3.5.1 + '@tiptap/extension-list': ^3.14.0 - '@tiptap/extension-list-keymap@3.5.1': - resolution: {integrity: sha512-qRJXa4L1V+L6JQTRyGzWlirvxi/ff3zhcUJynShRr9oBY3ETtrMU0/YUQmNEkKbyvCrmSZfPdmaGsCFnsFxIMQ==} + '@tiptap/extension-list-keymap@3.14.0': + resolution: {integrity: sha512-1oPbvNnQjeOxkHZcUbWPx/IY9o4fT3QGk/9A9cIjFrJRD2AHzbYfPDHNHINtg7Bj0jWz74cHvAHcaxP+M27jkA==} peerDependencies: - '@tiptap/extension-list': ^3.5.1 + '@tiptap/extension-list': ^3.14.0 - '@tiptap/extension-list@3.5.1': - resolution: {integrity: sha512-C9caCC6hKe2KwTTyFxvimigaZolDqFyVFB2iUDHCBpUbIyNw0PubmYfjGiIHcv3ltblo896TCV2nkVILrfwHdA==} + '@tiptap/extension-list@3.14.0': + resolution: {integrity: sha512-rsjFH0Vd/4UbDsjwMLay7oz72VVu1r35t8ofAzy5587jn5JAjflaZs05XbRRMD2imUTK41dyajVSh8CqSnDEJw==} peerDependencies: - '@tiptap/core': ^3.5.1 - '@tiptap/pm': ^3.5.1 + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 + + '@tiptap/extension-mathematics@3.14.0': + resolution: {integrity: sha512-czy1Yi6ObEsKy9xuPceEyRvVsg5nvwW31mQQTBMxEBHjHUq5gsQqaxIqQVu4kwcTjW2GgSIWfULKWsO11hRYhA==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 + katex: ^0.16.4 '@tiptap/extension-ordered-list@2.26.2': resolution: {integrity: sha512-UVGYyWKB5wWWvrvdN/WrPXPHJoP/UD1TNyeoE75M6nq4oD4l+Nc9Y5MIPsngrv/TimbomLNilR4ZRHibEriG9w==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-ordered-list@3.5.1': - resolution: {integrity: sha512-grEvsK7Tpu1Jf1dzRGPeRxa6XZfozanhdSjCWKnu1dQJ+2J4llaOALtQrAudoJJSpr0sKbS2F5noGESPCbmz/w==} + '@tiptap/extension-ordered-list@3.14.0': + resolution: {integrity: sha512-/fXjVL4JajkJQoc213iiput0bCXC4ztUPUpvNuI62VcgFKHcTvX4eYxED1VflotCx0OdkyY9yYD8PtvyO5lkmA==} peerDependencies: - '@tiptap/extension-list': ^3.5.1 + '@tiptap/extension-list': ^3.14.0 '@tiptap/extension-paragraph@2.26.2': resolution: {integrity: sha512-dccyffm95nNT9arjxGOyv4/cOPF4XS5Osylccp9KYLrmiSTXEuzVIYtMXhXbc0UUdABGvbFQWi88tRxgeDTkgA==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-paragraph@3.5.1': - resolution: {integrity: sha512-iAV9pa52V4AEZh16o8MJ7GamLS9hiEGlvIVHUUxy4SGVuq/HAQcLpsjTkc4wv4XPPQImgcTO639XxHnViZvVUQ==} + '@tiptap/extension-paragraph@3.14.0': + resolution: {integrity: sha512-NFxk2yNo3Cvh9g8evea+yTLNV48se7MbMcVizTnVhobqtBKv793qsb5FM5Hu30Y72FQPNfH+LRoap4XZyBPfVw==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 '@tiptap/extension-placeholder@2.26.2': resolution: {integrity: sha512-XBTDcpEo7Zo/1+RhGnRxA2TF0elQW7EayUcV+lJ3f7HQ5lrb5NTnakYc1ydeZ8Ih6vUqbK2CQUsESe3UWHHgHg==} @@ -1154,10 +1256,28 @@ packages: peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-strike@3.5.1': - resolution: {integrity: sha512-8dwMoQm53r/5DWp2e12zCpoRsfWZrManspSpPkpvbhhXRs6prhPmofplyNPqQq2Qjsep0c8Q8w2xnR/iBu0YZQ==} + '@tiptap/extension-strike@3.14.0': + resolution: {integrity: sha512-R8BbAhnWpisBml6okMKl98hY4tJjedTTgyTkx8tPabIJ92nS9IURKEk3foWB9uHxdTOBUqTvVT+2ScDf9r6QHg==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 + + '@tiptap/extension-subscript@3.14.0': + resolution: {integrity: sha512-PUO698roA3mB3mFBf6Ig4MwEpP7sVX1QPAi7F7xM9ZacvVMJEAPLFl6KDjJljYP2h5vE2wNOFk4aGl3vRquEwA==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 + + '@tiptap/extension-superscript@3.14.0': + resolution: {integrity: sha512-SPkwHz6yVMIzlPJS8ellQIQUB5MKiw36kMi+PO904IuKKbR9wONg8ud0wG5cgg5ni5d2jkQGG+mnI0kwDDts2Q==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 + + '@tiptap/extension-table@3.14.0': + resolution: {integrity: sha512-tt1OlzqlsRsTazARGpvwPJmKEu+jiMFuaueNM28V+AADtaIPyE160V6w2bkdeWQeaBWqZjOUGZaMjpsKqWXR+g==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 '@tiptap/extension-task-item@2.26.2': resolution: {integrity: sha512-KFfl9QraNldIR9ayP7t+b69BvjmdGeE9Mr+u/6h6jOmUVFpnES45geuEBSYuWWpK4EeD3ZUKXo7WGEWnTLSyfQ==} @@ -1170,48 +1290,75 @@ packages: peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-text-align@3.14.0': + resolution: {integrity: sha512-CaxxlbAvfofZZ7KPL28Kg8xuMv8t4rvt5GPwZAqE+jd3rwrucpovpX/SdgclYDc75xs0t8qeoxDFe9HQmG5XZA==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/extension-text-style@2.26.2': resolution: {integrity: sha512-rNkV3dgT3nTISEf3Ax/DdqQsSy9p46n2fOBkD8FCtdrwsWNH5N4uUh4jI/q0exYKJUyZGvl60uXwCkZiQ3pVBA==} peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-text-style@3.14.0': + resolution: {integrity: sha512-YZID7tvcMr5XLdq1PBxpIQi3dZFS2/LRAqIZHJ85FcG0EJkeRdM+y32+DXAWGvbwgAZaZOOvrgOfthQ/oqCdZg==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/extension-text@2.26.2': resolution: {integrity: sha512-Rb8Le/Li+EixQNc/pGkEJpLjozTjWYP9glaYfnjPtRVw4tHcd7khVm5mer0TQjonbBUjVC1zSuXv9gifXOv6DQ==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-text@3.5.1': - resolution: {integrity: sha512-+OS4qQnYZ6NNyDvYasx0wlBf717nttyU0h7hT3VCijm3ERIvwdiJKwZtsCVEF0yHqu+zUqJP84svg5ymEdtFSw==} + '@tiptap/extension-text@3.14.0': + resolution: {integrity: sha512-XlpnD87LQ7lLcDcBenHgzxv3uivQzPdVHM16CY4lXR4aKDIp2mxjPZr4twHT+cOnRQHc8VYpRgkEo6LLX6VylA==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 + + '@tiptap/extension-typography@3.14.0': + resolution: {integrity: sha512-vqRP6muQI/o95bchhmdGDGFRnJkOVs8zAkzuL5FCc0GmwJAPKy+GHoRP9hfgYlawCqzi8exVDXiY6WkknN87Yg==} + peerDependencies: + '@tiptap/core': ^3.14.0 '@tiptap/extension-underline@2.26.2': resolution: {integrity: sha512-kpKJSfsn1+b8l96YPg2GRn3aaN78pLqSeyzfA5FYVbN0lyptbqRVOrNM8p3n6l0LbAkiEjc/TgOMwNNEL93kyA==} peerDependencies: '@tiptap/core': ^2.7.0 - '@tiptap/extension-underline@3.5.1': - resolution: {integrity: sha512-VW5flDMV8sjtg61gEBpoqm7y92rlfz7OfPX/9U/qAXvZS1Z9CQ49mcIiKc8mMJvHKZWS61HcMMoRFSsdOvQnGg==} + '@tiptap/extension-underline@3.14.0': + resolution: {integrity: sha512-zmnWlsi2g/tMlThHby0Je9O+v24j4d+qcXF3nuzLUUaDsGCEtOyC9RzwITft59ViK+Nc2PD2W/J14rsB0j+qoQ==} peerDependencies: - '@tiptap/core': ^3.5.1 + '@tiptap/core': ^3.14.0 - '@tiptap/extensions@3.5.1': - resolution: {integrity: sha512-kxmhfMjMRTW7ZnYPPxyIh6vSWMCtIhl5KrJTL3Gnk6wMZYZArkekjLD6cab7p42VRjbH30n4phq1kIlKW9xzKQ==} + '@tiptap/extensions@3.14.0': + resolution: {integrity: sha512-qQBVKqzU4ZVjRn8W0UbdfE4LaaIgcIWHOMrNnJ+PutrRzQ6ZzhmD/kRONvRWBfG9z3DU7pSKGwVYSR2hztsGuQ==} peerDependencies: - '@tiptap/core': ^3.5.1 - '@tiptap/pm': ^3.5.1 + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 + + '@tiptap/markdown@3.14.0': + resolution: {integrity: sha512-huZr0AWgLOVRSIFVVaNVmmZKKAZ0vmKtwMvmHTrQPG7OL/EnNPvYGkjeI3cSBabRqVo02WQEs5fTms32rdIwQg==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 '@tiptap/pm@2.26.2': resolution: {integrity: sha512-H2kJHckC9idlYPu/PNdu5XR3Rdu7gbNb+Qrdt2gBnaDyHgAcs+14wak6x19vy27GV9FFzg9722Eb7LErooo28w==} - '@tiptap/pm@3.5.1': - resolution: {integrity: sha512-4WwRiAwyUHc2GTR4jaP/nzZkSc7lXv2jyP735HsnHyD+wgxS/SHVhyfDYHVTxOPt68USKcgYv75tVsuntELgKg==} + '@tiptap/pm@3.14.0': + resolution: {integrity: sha512-xrZmqI5jl4yMeAsu8p8gVP9S3An5h2MBi8BQHNnZmpyzkUrlpd40vlT6u13SWIqVi5ZWhBZ6U3rL7mkVLZuRKg==} '@tiptap/starter-kit@2.26.1': resolution: {integrity: sha512-oziMGCds8SVQ3s5dRpBxVdEKZAmO/O//BjZ69mhA3q4vJdR0rnfLb5fTxSeQvHiqB878HBNn76kNaJrHrV35GA==} - '@tiptap/starter-kit@3.5.1': - resolution: {integrity: sha512-J6Ko3d9DiKqsOR20VktbryOJg27Tsr+lMD60mwcqHzOEm99sfKCVjs2SHG09l9/sCl9AEjN0f1Row3CoBg9PpA==} + '@tiptap/starter-kit@3.14.0': + resolution: {integrity: sha512-fHsC4oDVzvMU9btg+IUmu/eqPquapjJ341qaNI7cCeSCKjjE6XJEN6WcONLAVId2OZUwML0IX1Jgl+6gJxU9Jw==} + + '@tiptap/suggestion@3.14.0': + resolution: {integrity: sha512-B9BQ9Tck8HsISDc6jZmtaSpl8KK69JbKHfU0ntILxgj8aBASElewO+W8WE49JSTxuyJGRgnhGSgaGpM4LhbLAg==} + peerDependencies: + '@tiptap/core': ^3.14.0 + '@tiptap/pm': ^3.14.0 '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -1234,6 +1381,9 @@ packages: '@types/hast@2.3.10': resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1511,6 +1661,10 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + comment-json@4.2.5: resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} engines: {node: '>= 6'} @@ -1838,6 +1992,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + highlight.js@11.8.0: resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==} engines: {node: '>=12.0.0'} @@ -1937,6 +2095,10 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + katex@0.16.27: + resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1984,6 +2146,9 @@ packages: lowlight@2.9.0: resolution: {integrity: sha512-OpcaUTCLmHuVuBcyNckKfH5B0oA4JUavb/M/8n9iAvanJYNQkrVm4pvyX0SUaqkBG4dnWHKt7p50B3ngAG2Rfw==} + lowlight@3.3.0: + resolution: {integrity: sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -1998,6 +2163,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -2585,6 +2755,16 @@ packages: peerDependencies: svelte: ^5.0.0 + svelte-tiptap@3.0.1: + resolution: {integrity: sha512-Vi3kVGOd01f7mslOxGbJB7z2QavdvH+6WffhB+Y5fleTiZaW0YWqIboyO2u/uh4BQeosiINmmuRJ+Qwb7mYP+A==} + peerDependencies: + '@floating-ui/dom': ^1.0.0 + '@tiptap/core': ^3.0.0 + '@tiptap/extension-bubble-menu': ^3.0.0 + '@tiptap/extension-floating-menu': ^3.0.0 + '@tiptap/pm': ^3.0.0 + svelte: ^5.0.0 + svelte-toolbelt@0.9.3: resolution: {integrity: sha512-HCSWxCtVmv+c6g1ACb8LTwHVbDqLKJvHpo6J8TaqwUme2hj9ATJCpjCPNISR1OCq2Q4U1KT41if9ON0isINQZw==} engines: {node: '>=18', pnpm: '>=8.7.0'} @@ -2632,6 +2812,9 @@ packages: tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + tiptap-extension-auto-joiner@0.1.3: + resolution: {integrity: sha512-nY3aKeCpVb2WjjVEZkLtEqxsK3KU1zGioyglMhK1sUFNjKDccOfRyz/YDKrHRAVsKJPGnk2A8VA1827iGEAXWQ==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3072,11 +3255,11 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@friendofsvelte/tipex@0.0.9(highlight.js@11.8.0)(svelte@5.38.7)': + '@friendofsvelte/tipex@0.0.9(highlight.js@11.11.1)(svelte@5.38.7)': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-code-block': 2.26.2(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) - '@tiptap/extension-code-block-lowlight': 2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/extension-code-block@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)(highlight.js@11.8.0)(lowlight@2.9.0) + '@tiptap/extension-code-block': 2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2) + '@tiptap/extension-code-block-lowlight': 2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/extension-code-block@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)(highlight.js@11.11.1)(lowlight@2.9.0) '@tiptap/extension-floating-menu': 2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2) '@tiptap/extension-image': 2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2)) '@tiptap/extension-link': 2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2) @@ -3168,6 +3351,10 @@ snapshots: '@lix-js/server-protocol-schema@0.1.1': {} + '@lucide/svelte@0.562.0(svelte@5.38.7)': + dependencies: + svelte: 5.38.7 + '@mdx-js/react@3.1.1(@types/react@19.1.12)(react@19.1.1)': dependencies: '@types/mdx': 2.0.13 @@ -3605,81 +3792,90 @@ snapshots: dependencies: '@tiptap/pm': 2.26.2 - '@tiptap/core@3.5.1(@tiptap/pm@3.5.1)': + '@tiptap/core@3.14.0(@tiptap/pm@3.14.0)': dependencies: - '@tiptap/pm': 3.5.1 + '@tiptap/pm': 3.14.0 '@tiptap/extension-blockquote@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-blockquote@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-blockquote@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) '@tiptap/extension-bold@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-bold@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-bold@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + + '@tiptap/extension-bubble-menu@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 '@tiptap/extension-bullet-list@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-bullet-list@3.5.1(@tiptap/extension-list@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1))': + '@tiptap/extension-bullet-list@3.14.0(@tiptap/extension-list@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/extension-list': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) + '@tiptap/extension-list': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) - '@tiptap/extension-code-block-lowlight@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/extension-code-block@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)(highlight.js@11.8.0)(lowlight@2.9.0)': + '@tiptap/extension-code-block-lowlight@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/extension-code-block@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)(highlight.js@11.11.1)(lowlight@2.9.0)': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-code-block': 2.26.2(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) + '@tiptap/extension-code-block': 2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2) '@tiptap/pm': 2.26.2 - highlight.js: 11.8.0 + highlight.js: 11.11.1 lowlight: 2.9.0 + '@tiptap/extension-code-block-lowlight@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/extension-code-block@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)(highlight.js@11.11.1)(lowlight@3.3.0)': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/extension-code-block': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + highlight.js: 11.11.1 + lowlight: 3.3.0 + '@tiptap/extension-code-block@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) '@tiptap/pm': 2.26.2 - '@tiptap/extension-code-block@2.26.2(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)': + '@tiptap/extension-code-block@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) - '@tiptap/pm': 3.5.1 - - '@tiptap/extension-code-block@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)': - dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) - '@tiptap/pm': 3.5.1 + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 '@tiptap/extension-code@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-code@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-code@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) '@tiptap/extension-document@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-document@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-document@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) '@tiptap/extension-dropcursor@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) '@tiptap/pm': 2.26.2 - '@tiptap/extension-dropcursor@3.5.1(@tiptap/extensions@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1))': + '@tiptap/extension-dropcursor@3.14.0(@tiptap/extensions@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/extensions': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) + '@tiptap/extensions': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) '@tiptap/extension-floating-menu@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)': dependencies: @@ -3687,34 +3883,40 @@ snapshots: '@tiptap/pm': 2.26.2 tippy.js: 6.3.7 + '@tiptap/extension-floating-menu@3.14.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + '@tiptap/extension-gapcursor@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) '@tiptap/pm': 2.26.2 - '@tiptap/extension-gapcursor@3.5.1(@tiptap/extensions@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1))': + '@tiptap/extension-gapcursor@3.14.0(@tiptap/extensions@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/extensions': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) + '@tiptap/extensions': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) '@tiptap/extension-hard-break@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-hard-break@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-hard-break@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) '@tiptap/extension-heading@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-heading@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-heading@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) - '@tiptap/extension-highlight@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-highlight@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) '@tiptap/extension-history@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)': dependencies: @@ -3726,22 +3928,26 @@ snapshots: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) '@tiptap/pm': 2.26.2 - '@tiptap/extension-horizontal-rule@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)': + '@tiptap/extension-horizontal-rule@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) - '@tiptap/pm': 3.5.1 + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 '@tiptap/extension-image@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) + '@tiptap/extension-image@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/extension-italic@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-italic@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-italic@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) '@tiptap/extension-link@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)': dependencies: @@ -3749,44 +3955,50 @@ snapshots: '@tiptap/pm': 2.26.2 linkifyjs: 4.3.2 - '@tiptap/extension-link@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)': + '@tiptap/extension-link@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) - '@tiptap/pm': 3.5.1 + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 linkifyjs: 4.3.2 '@tiptap/extension-list-item@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-list-item@3.5.1(@tiptap/extension-list@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1))': + '@tiptap/extension-list-item@3.14.0(@tiptap/extension-list@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/extension-list': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) + '@tiptap/extension-list': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) - '@tiptap/extension-list-keymap@3.5.1(@tiptap/extension-list@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1))': + '@tiptap/extension-list-keymap@3.14.0(@tiptap/extension-list@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/extension-list': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) + '@tiptap/extension-list': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) - '@tiptap/extension-list@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)': + '@tiptap/extension-list@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) - '@tiptap/pm': 3.5.1 + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + + '@tiptap/extension-mathematics@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)(katex@0.16.27)': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + katex: 0.16.27 '@tiptap/extension-ordered-list@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-ordered-list@3.5.1(@tiptap/extension-list@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1))': + '@tiptap/extension-ordered-list@3.14.0(@tiptap/extension-list@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/extension-list': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) + '@tiptap/extension-list': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) '@tiptap/extension-paragraph@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-paragraph@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-paragraph@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) '@tiptap/extension-placeholder@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)': dependencies: @@ -3797,9 +4009,24 @@ snapshots: dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-strike@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-strike@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + + '@tiptap/extension-subscript@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + + '@tiptap/extension-superscript@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + + '@tiptap/extension-table@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 '@tiptap/extension-task-item@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))(@tiptap/pm@2.26.2)': dependencies: @@ -3810,30 +4037,48 @@ snapshots: dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) + '@tiptap/extension-text-align@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/extension-text-style@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) + '@tiptap/extension-text-style@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/extension-text@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-text@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-text@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + + '@tiptap/extension-typography@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) '@tiptap/extension-underline@2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2))': dependencies: '@tiptap/core': 2.26.2(@tiptap/pm@2.26.2) - '@tiptap/extension-underline@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))': + '@tiptap/extension-underline@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) - '@tiptap/extensions@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)': + '@tiptap/extensions@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) - '@tiptap/pm': 3.5.1 + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + + '@tiptap/markdown@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + marked: 15.0.12 '@tiptap/pm@2.26.2': dependencies: @@ -3856,7 +4101,7 @@ snapshots: prosemirror-transform: 1.10.4 prosemirror-view: 1.41.1 - '@tiptap/pm@3.5.1': + '@tiptap/pm@3.14.0': dependencies: prosemirror-changeset: 2.3.1 prosemirror-collab: 1.3.1 @@ -3901,32 +4146,37 @@ snapshots: '@tiptap/extension-text-style': 2.26.2(@tiptap/core@2.26.2(@tiptap/pm@2.26.2)) '@tiptap/pm': 2.26.2 - '@tiptap/starter-kit@3.5.1': + '@tiptap/starter-kit@3.14.0': dependencies: - '@tiptap/core': 3.5.1(@tiptap/pm@3.5.1) - '@tiptap/extension-blockquote': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-bold': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-bullet-list': 3.5.1(@tiptap/extension-list@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)) - '@tiptap/extension-code': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-code-block': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) - '@tiptap/extension-document': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-dropcursor': 3.5.1(@tiptap/extensions@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)) - '@tiptap/extension-gapcursor': 3.5.1(@tiptap/extensions@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)) - '@tiptap/extension-hard-break': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-heading': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-horizontal-rule': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) - '@tiptap/extension-italic': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-link': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) - '@tiptap/extension-list': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) - '@tiptap/extension-list-item': 3.5.1(@tiptap/extension-list@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)) - '@tiptap/extension-list-keymap': 3.5.1(@tiptap/extension-list@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)) - '@tiptap/extension-ordered-list': 3.5.1(@tiptap/extension-list@3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1)) - '@tiptap/extension-paragraph': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-strike': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-text': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extension-underline': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1)) - '@tiptap/extensions': 3.5.1(@tiptap/core@3.5.1(@tiptap/pm@3.5.1))(@tiptap/pm@3.5.1) - '@tiptap/pm': 3.5.1 + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/extension-blockquote': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-bold': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-bullet-list': 3.14.0(@tiptap/extension-list@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)) + '@tiptap/extension-code': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-code-block': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-document': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-dropcursor': 3.14.0(@tiptap/extensions@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)) + '@tiptap/extension-gapcursor': 3.14.0(@tiptap/extensions@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)) + '@tiptap/extension-hard-break': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-heading': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-horizontal-rule': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-italic': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-link': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-list': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-list-item': 3.14.0(@tiptap/extension-list@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)) + '@tiptap/extension-list-keymap': 3.14.0(@tiptap/extension-list@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)) + '@tiptap/extension-ordered-list': 3.14.0(@tiptap/extension-list@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)) + '@tiptap/extension-paragraph': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-strike': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-text': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extension-underline': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0)) + '@tiptap/extensions': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + + '@tiptap/suggestion@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)': + dependencies: + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 '@types/aria-query@5.0.4': {} @@ -3948,6 +4198,10 @@ snapshots: dependencies: '@types/unist': 2.0.11 + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} '@types/linkify-it@5.0.0': {} @@ -4245,6 +4499,8 @@ snapshots: commander@11.1.0: {} + commander@8.3.0: {} + comment-json@4.2.5: dependencies: array-timsort: 1.0.3 @@ -4566,6 +4822,8 @@ snapshots: dependencies: function-bind: 1.1.2 + highlight.js@11.11.1: {} + highlight.js@11.8.0: {} human-id@4.1.1: {} @@ -4641,6 +4899,10 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + katex@0.16.27: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -4682,6 +4944,12 @@ snapshots: fault: 2.0.1 highlight.js: 11.8.0 + lowlight@3.3.0: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + highlight.js: 11.11.1 + lz-string@1.5.0: {} magic-string@0.30.19: @@ -4699,6 +4967,8 @@ snapshots: markdown-table@3.0.4: {} + marked@15.0.12: {} + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -5478,6 +5748,15 @@ snapshots: runed: 0.28.0(svelte@5.38.7) svelte: 5.38.7 + svelte-tiptap@3.0.1(@floating-ui/dom@1.7.4)(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/extension-bubble-menu@3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))(@tiptap/extension-floating-menu@3.14.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0)(svelte@5.38.7): + dependencies: + '@floating-ui/dom': 1.7.4 + '@tiptap/core': 3.14.0(@tiptap/pm@3.14.0) + '@tiptap/extension-bubble-menu': 3.14.0(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/extension-floating-menu': 3.14.0(@floating-ui/dom@1.7.4)(@tiptap/core@3.14.0(@tiptap/pm@3.14.0))(@tiptap/pm@3.14.0) + '@tiptap/pm': 3.14.0 + svelte: 5.38.7 + svelte-toolbelt@0.9.3(svelte@5.38.7): dependencies: clsx: 2.1.1 @@ -5532,6 +5811,8 @@ snapshots: dependencies: '@popperjs/core': 2.11.8 + tiptap-extension-auto-joiner@0.1.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 diff --git a/src/lib/components/edra/commands/index.ts b/src/lib/components/edra/commands/index.ts new file mode 100644 index 00000000..a9428a58 --- /dev/null +++ b/src/lib/components/edra/commands/index.ts @@ -0,0 +1 @@ +export { default as ToolBarCommands } from './toolbar-commands.js'; diff --git a/src/lib/components/edra/commands/toolbar-commands.ts b/src/lib/components/edra/commands/toolbar-commands.ts new file mode 100644 index 00000000..a598e6e6 --- /dev/null +++ b/src/lib/components/edra/commands/toolbar-commands.ts @@ -0,0 +1,503 @@ +import type { EdraToolBarCommands } from './types.js'; +import { isMac } from '../utils.js'; +import Undo from '@lucide/svelte/icons/undo-2'; +import Redo from '@lucide/svelte/icons/redo-2'; +import Heading1 from '@lucide/svelte/icons/heading-1'; +import Heading2 from '@lucide/svelte/icons/heading-2'; +import Heading3 from '@lucide/svelte/icons/heading-3'; +import Heading4 from '@lucide/svelte/icons/heading-4'; +import Link from '@lucide/svelte/icons/link-2'; +import Bold from '@lucide/svelte/icons/bold'; +import Italic from '@lucide/svelte/icons/italic'; +import Underline from '@lucide/svelte/icons/underline'; +import StrikeThrough from '@lucide/svelte/icons/strikethrough'; +import Quote from '@lucide/svelte/icons/quote'; +import Code from '@lucide/svelte/icons/code'; +import Superscript from '@lucide/svelte/icons/superscript'; +import Subscript from '@lucide/svelte/icons/subscript'; +import AlignLeft from '@lucide/svelte/icons/align-left'; +import AlignCenter from '@lucide/svelte/icons/align-center'; +import AlignRight from '@lucide/svelte/icons/align-right'; +import AlighJustify from '@lucide/svelte/icons/align-justify'; +import List from '@lucide/svelte/icons/list'; +import ListOrdered from '@lucide/svelte/icons/list-ordered'; +import ListChecks from '@lucide/svelte/icons/list-checks'; +import Image from '@lucide/svelte/icons/image'; +import Video from '@lucide/svelte/icons/video'; +import Audio from '@lucide/svelte/icons/audio-lines'; +import IFrame from '@lucide/svelte/icons/code-xml'; +import Table from '@lucide/svelte/icons/table'; +import Radical from '@lucide/svelte/icons/radical'; +import SquareRadical from '@lucide/svelte/icons/square-radical'; +import { isTextSelection } from '@tiptap/core'; +import Pilcrow from '@lucide/svelte/icons/pilcrow'; + +const commands: Record = { + 'undo-redo': [ + { + icon: Undo, + name: 'undo', + tooltip: 'Undo', + shortCut: `${isMac ? '⌘' : 'Ctrl+'}Z`, + onClick: (editor) => { + editor.chain().focus().undo().run(); + }, + clickable: (editor) => { + return editor.can().undo(); + } + }, + { + icon: Redo, + name: 'redo', + tooltip: 'Redo', + shortCut: `${isMac ? '⌘' : 'Ctrl+'}Y`, + onClick: (editor) => { + editor.chain().focus().redo().run(); + }, + clickable: (editor) => { + return editor.can().redo(); + } + } + ], + headings: [ + { + icon: Heading1, + name: 'h1', + tooltip: 'Heading 1', + shortCut: `${isMac ? '⌘⌥' : 'Ctrl+Alt+'}1`, + onClick: (editor) => { + editor.chain().focus().toggleHeading({ level: 1 }).run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setHeading({ level: 1 }).run(); + }, + clickable: (editor) => { + return editor.can().toggleHeading({ level: 1 }); + }, + isActive: (editor) => { + return editor.isActive('heading', { level: 1 }); + } + }, + { + icon: Heading2, + name: 'h2', + tooltip: 'Heading 2', + shortCut: `${isMac ? '⌘⌥' : 'Ctrl+Alt+'}2`, + onClick: (editor) => { + editor.chain().focus().toggleHeading({ level: 2 }).run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setHeading({ level: 2 }).run(); + }, + clickable: (editor) => { + return editor.can().toggleHeading({ level: 2 }); + }, + isActive: (editor) => { + return editor.isActive('heading', { level: 2 }); + } + }, + { + icon: Heading3, + name: 'h3', + tooltip: 'Heading 3', + shortCut: `${isMac ? '⌘⌥' : 'Ctrl+Alt+'}3`, + onClick: (editor) => { + editor.chain().focus().toggleHeading({ level: 3 }).run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setHeading({ level: 3 }).run(); + }, + clickable: (editor) => { + return editor.can().toggleHeading({ level: 3 }); + }, + isActive: (editor) => { + return editor.isActive('heading', { level: 3 }); + } + }, + { + icon: Heading4, + name: 'h4', + tooltip: 'Heading 4', + shortCut: `${isMac ? '⌘⌥' : 'Ctrl+Alt+'}4`, + onClick: (editor) => { + editor.chain().focus().toggleHeading({ level: 4 }).run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setHeading({ level: 4 }).run(); + }, + clickable: (editor) => { + return editor.can().toggleHeading({ level: 4 }); + }, + isActive: (editor) => { + return editor.isActive('heading', { level: 4 }); + } + } + ], + 'text-formatting': [ + { + icon: Link, + name: 'link', + tooltip: 'Link', + onClick: (editor) => { + if (editor.isActive('link')) { + editor.chain().focus().unsetLink().run(); + } else { + const url = window.prompt('Enter the URL of the link:'); + if (url) { + editor.chain().focus().toggleLink({ href: url }).run(); + } + } + }, + isActive: (editor) => { + return editor.isActive('link'); + } + }, + { + icon: Pilcrow, + name: 'paragraph', + tooltip: 'Paragraph', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}0`, + onClick: (editor) => { + editor.chain().focus().setParagraph().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setParagraph().run(); + }, + clickable: (editor) => { + return editor.can().setParagraph(); + }, + isActive: (editor) => { + return editor.isActive('paragraph'); + } + }, + { + icon: Bold, + name: 'bold', + tooltip: 'Bold', + shortCut: `${isMac ? '⌘' : 'Ctrl+'}B`, + onClick: (editor) => { + editor.chain().focus().toggleBold().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setMark('bold').run(); + }, + clickable: (editor) => { + return editor.can().toggleBold(); + }, + isActive: (editor) => { + return editor.isActive('bold'); + } + }, + { + icon: Italic, + name: 'italic', + tooltip: 'Italic', + shortCut: `${isMac ? '⌘' : 'Ctrl+'}I`, + onClick: (editor) => { + editor.chain().focus().toggleItalic().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setMark('italic').run(); + }, + clickable: (editor) => { + return editor.can().toggleItalic(); + }, + isActive: (editor) => { + return editor.isActive('italic'); + } + }, + { + icon: Underline, + name: 'underline', + tooltip: 'Underline', + shortCut: `${isMac ? '⌘' : 'Ctrl+'}U`, + onClick: (editor) => { + editor.chain().focus().toggleUnderline().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setMark('underline').run(); + }, + clickable: (editor) => { + return editor.can().toggleUnderline(); + }, + isActive: (editor) => { + return editor.isActive('underline'); + } + }, + { + icon: StrikeThrough, + name: 'strikethrough', + tooltip: 'Strikethrough', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}S`, + onClick: (editor) => { + editor.chain().focus().toggleStrike().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).setMark('strike').run(); + }, + clickable: (editor) => { + return editor.can().toggleStrike(); + }, + isActive: (editor) => { + return editor.isActive('strike'); + } + }, + { + icon: Quote, + name: 'blockQuote', + tooltip: 'BlockQuote', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}B`, + onClick: (editor) => { + editor.chain().focus().toggleBlockquote().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleBlockquote().run(); + }, + clickable: (editor) => { + return editor.can().toggleBlockquote(); + }, + isActive: (editor) => { + return editor.isActive('blockquote'); + } + }, + { + icon: Code, + name: 'code', + tooltip: 'Inline Code', + shortCut: `${isMac ? '⌘' : 'Ctrl+'}E`, + onClick: (editor) => { + editor.chain().focus().toggleCode().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleCodeBlock().run(); + }, + clickable: (editor) => { + return editor.can().toggleCode(); + }, + isActive: (editor) => { + return editor.isActive('code'); + } + }, + { + icon: Superscript, + name: 'superscript', + tooltip: 'Superscript', + shortCut: `${isMac ? '⌘' : 'Ctrl+'}.`, + onClick: (editor) => { + editor.chain().focus().toggleSuperscript().run(); + }, + clickable: (editor) => { + return editor.can().toggleSuperscript(); + }, + isActive: (editor) => { + return editor.isActive('superscript'); + } + }, + { + icon: Subscript, + name: 'subscript', + tooltip: 'Subscript', + shortCut: `${isMac ? '⌘' : 'Ctrl+'},`, + onClick: (editor) => { + editor.chain().focus().toggleSubscript().run(); + }, + clickable: (editor) => { + return editor.can().toggleSubscript(); + }, + isActive: (editor) => { + return editor.isActive('subscript'); + } + } + ], + alignment: [ + { + icon: AlignLeft, + name: 'align-left', + tooltip: 'Align Left', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}L`, + onClick: (editor) => { + editor.chain().focus().toggleTextAlign('left').run(); + }, + clickable: (editor) => { + return editor.can().toggleTextAlign('left'); + }, + isActive: (editor) => editor.isActive({ textAlign: 'left' }) + }, + { + icon: AlignCenter, + name: 'align-center', + tooltip: 'Align Center', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}E`, + onClick: (editor) => { + editor.chain().focus().toggleTextAlign('center').run(); + }, + clickable: (editor) => { + return editor.can().toggleTextAlign('center'); + }, + isActive: (editor) => editor.isActive({ textAlign: 'center' }) + }, + { + icon: AlignRight, + name: 'align-right', + tooltip: 'Align Right', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}R`, + onClick: (editor) => { + editor.chain().focus().toggleTextAlign('right').run(); + }, + clickable: (editor) => { + return editor.can().toggleTextAlign('right'); + }, + isActive: (editor) => editor.isActive({ textAlign: 'right' }) + }, + { + icon: AlighJustify, + name: 'align-justify', + tooltip: 'Align Justify', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}J`, + onClick: (editor) => { + editor.chain().focus().toggleTextAlign('justify').run(); + }, + clickable: (editor) => { + return editor.can().toggleTextAlign('justify'); + }, + isActive: (editor) => editor.isActive({ textAlign: 'justify' }) + } + ], + lists: [ + { + icon: List, + name: 'bulletList', + tooltip: 'Bullet List', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}8`, + onClick: (editor) => { + editor.chain().focus().toggleBulletList().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleBulletList().run(); + }, + isActive: (editor) => editor.isActive('bulletList') + }, + { + icon: ListOrdered, + name: 'orderedList', + tooltip: 'Ordered List', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}7`, + onClick: (editor) => { + editor.chain().focus().toggleOrderedList().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleOrderedList().run(); + }, + clickable: (editor) => { + return editor.can().toggleOrderedList(); + }, + isActive: (editor) => { + return editor.isActive('orderedList'); + } + }, + { + icon: ListChecks, + name: 'taskList', + tooltip: 'Task List', + shortCut: `${isMac ? '⌘⇧' : 'Ctrl+Shift+'}9`, + onClick: (editor) => { + editor.chain().focus().toggleTaskList().run(); + }, + turnInto: (editor, pos) => { + editor.chain().setNodeSelection(pos).toggleTaskList().run(); + }, + clickable: (editor) => { + return editor.can().toggleTaskList(); + }, + isActive: (editor) => { + return editor.isActive('taskList'); + } + } + ], + media: [ + { + icon: Image, + name: 'image-placeholder', + tooltip: 'Image Placeholder', + onClick: (editor) => { + editor.chain().focus().insertImagePlaceholder().run(); + }, + isActive: (editor) => editor.isActive('image-placeholder') + }, + { + icon: Video, + name: 'video-placeholder', + tooltip: 'Video Placeholder', + onClick: (editor) => { + editor.chain().focus().insertVideoPlaceholder().run(); + }, + isActive: (editor) => editor.isActive('video-placeholder') + }, + { + icon: Audio, + name: 'audio-placeholder', + tooltip: 'Audio Placeholder', + onClick: (editor) => { + editor.chain().focus().insertAudioPlaceholder().run(); + }, + isActive: (editor) => editor.isActive('audio-placeholder') + }, + { + icon: IFrame, + name: 'iframe-placeholder', + tooltip: 'IFrame Placeholder', + onClick: (editor) => { + editor.chain().focus().insertIFramePlaceholder().run(); + }, + isActive: (editor) => editor.isActive('iframe-placeholder') + } + ], + table: [ + { + icon: Table, + name: 'table', + tooltip: 'Table', + onClick: (editor) => { + if (editor.isActive('table')) { + const del = confirm('Do you really want to delete this table??'); + if (del) { + editor.chain().focus().deleteTable().run(); + return; + } + } + editor.chain().focus().insertTable({ cols: 3, rows: 3, withHeaderRow: false }).run(); + }, + isActive: (editor) => editor.isActive('table') + } + ], + math: [ + { + icon: Radical, + name: 'mathematics', + tooltip: 'Inline Expression', + onClick: (editor) => { + let latex = 'a^2 + b^2 = c^2'; + const chain = editor.chain().focus(); + if (isTextSelection(editor.view.state.selection)) { + const { from, to } = editor.view.state.selection; + latex = editor.view.state.doc.textBetween(from, to); + chain.deleteRange({ from, to }); + } + chain.insertInlineMath({ latex }).run(); + }, + isActive: (editor) => editor.isActive('inlineMath') + }, + { + icon: SquareRadical, + name: 'mathematics', + tooltip: 'Block Expression', + onClick: (editor) => { + const latex = 'a^2 + b^2 = c^2'; + editor.chain().focus().insertBlockMath({ latex }).run(); + }, + isActive: (editor) => editor.isActive('blockMath') + } + ] +}; + +export default commands; diff --git a/src/lib/components/edra/commands/types.ts b/src/lib/components/edra/commands/types.ts new file mode 100644 index 00000000..2f33432e --- /dev/null +++ b/src/lib/components/edra/commands/types.ts @@ -0,0 +1,13 @@ +import type { Editor } from '@tiptap/core'; +import { Icon } from '@lucide/svelte'; + +export interface EdraToolBarCommands { + name: string; + icon: typeof Icon; + tooltip?: string; + shortCut?: string; + onClick?: (editor: Editor) => void; + turnInto?: (editor: Editor, pos: number) => void; + isActive?: (editor: Editor) => boolean; + clickable?: (editor: Editor) => boolean; +} diff --git a/src/lib/components/edra/components/BubbleMenu.svelte b/src/lib/components/edra/components/BubbleMenu.svelte new file mode 100644 index 00000000..09e96c9f --- /dev/null +++ b/src/lib/components/edra/components/BubbleMenu.svelte @@ -0,0 +1,67 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/edra/components/DragHandle.svelte b/src/lib/components/edra/components/DragHandle.svelte new file mode 100644 index 00000000..b56b249c --- /dev/null +++ b/src/lib/components/edra/components/DragHandle.svelte @@ -0,0 +1,31 @@ + + +
+ +
diff --git a/src/lib/components/edra/components/MediaPlaceHolder.svelte b/src/lib/components/edra/components/MediaPlaceHolder.svelte new file mode 100644 index 00000000..f830d27e --- /dev/null +++ b/src/lib/components/edra/components/MediaPlaceHolder.svelte @@ -0,0 +1,30 @@ + + + + {#if !children && icon && title} + {@const Icon = icon} + +
{title}
+ {/if} + {@render children?.()} +
diff --git a/src/lib/components/edra/editor.css b/src/lib/components/edra/editor.css new file mode 100644 index 00000000..33bfc0c5 --- /dev/null +++ b/src/lib/components/edra/editor.css @@ -0,0 +1,493 @@ +/* Base TipTap Editor Styles with Light/Dark Theme Support */ + +.tiptap :first-child { + margin-top: 0; +} + +/* For Placeholder */ + +.tiptap .is-empty:not(blockquote.is-empty)::before { + pointer-events: none; + float: left; + height: 0; + color: var(--color-muted-foreground); + content: attr(data-placeholder); +} + +/* Heading Styles */ +.tiptap h1, +.tiptap h2, +.tiptap h3, +.tiptap h4, +.tiptap h5, +.tiptap h6 { + line-height: 1.2; + margin-top: 1rem; + text-wrap: pretty; +} + +.tiptap h1, +.tiptap h2 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.tiptap h1 { + scroll-margin: 5rem; + font-size: 2.25rem; + line-height: 2.5rem; + font-weight: 800; + letter-spacing: -0.015em; +} + +.tiptap h2 { + font-size: 1.5rem; + font-weight: 700; +} + +.tiptap h3 { + font-size: 1.25rem; + font-weight: 600; +} + +.tiptap h4, +.tiptap h5, +.tiptap h6 { + font-size: 1rem; + font-weight: 600; +} + +.tiptap blockquote { + margin: 1rem 0rem; + padding: 0.5rem 0; + padding-left: 0.75rem; + font-style: italic; + color: var(--blockquote-color); + position: relative; +} + +.tiptap blockquote::before { + content: ' '; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 0.4rem; + background-color: var(--blockquote-border); + border-radius: 0.5rem; +} + +.tiptap blockquote p { + margin: 0; +} + +/* Horizontal Rule */ +.tiptap hr { + border: none; + border-top: 1px solid var(--border-color); + margin: 1rem 0; +} + +/* Inline Code */ +.tiptap code:not(pre code) { + background-color: var(--code-bg); + border-radius: 0.25rem; + padding: 0.2rem 0.3rem; + font-family: monospace; + font-size: 0.875rem; + font-weight: bold; +} + +/* List Styling */ + +.tiptap ul, +.tiptap ol { + padding: 0 1rem; + margin: 0.5rem 1rem 0.5rem 0.4rem; +} + +.tiptap ul li p, +.tiptap ol li p { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + +/* Task List Styling */ +.tiptap ul[data-type='taskList'] { + list-style: none; + margin: 0; + padding: 0; +} + +.tiptap ul[data-type='taskList'] li { + align-items: flex-start; + display: flex; +} + +.tiptap ul[data-type='taskList'] li > label { + flex: 0 0 auto; + margin-right: 0.5rem; + user-select: none; +} + +.tiptap ul[data-type='taskList'] li > div { + flex: 1 1 auto; +} + +.tiptap ul[data-type='taskList'] input[type='checkbox'] { + cursor: pointer; +} + +.tiptap ul[data-type='taskList'] ul[data-type='taskList'] { + margin: 0; +} + +ul[data-type='taskList'] li[data-checked='true'] div { + color: var(--task-completed-color); + text-decoration: line-through; +} + +input[type='checkbox'] { + position: relative; + top: 0.25rem; + margin: 0; + display: grid; + place-content: center; + cursor: pointer; + width: 1rem; + height: 1rem; + border-radius: 0.25rem; +} + +/* Color Swatches */ +.color { + white-space: nowrap; +} + +.color::before { + margin-bottom: 0.15rem; + margin-right: 0.1rem; + display: inline-block; + width: 1rem; + height: 1rem; + border-radius: 0.25rem; + border: 1px solid var(--border-color); + vertical-align: middle; + background-color: var(--color); + content: ' '; +} + +/* Code Block Styling */ +.tiptap pre { + margin: 0; + display: flex; + height: fit-content; + overflow: auto; + background-color: transparent; + padding: 0; +} + +.tiptap pre code { + flex: 1; + border-radius: 0 !important; + background-color: transparent; + padding: 0; + color: inherit; + font-family: 'JetBrains Mono', monospace, Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono'; +} + +/* Drag Handle Styling */ +.drag-handle { + position: fixed; + z-index: 50; + width: 1.5rem; + height: 1.5rem; + display: flex; + padding-right: 0.5rem; + flex-direction: column; + justify-content: center; + align-items: center; + cursor: grab; + opacity: 100; + transition-property: all; + transition-duration: 300ms; + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); + color: var(--border-color-hover); +} + +.drag-handle:active { + cursor: grabbing; +} + +.drag-handle.hide { + opacity: 0; + pointer-events: none; +} + +@media screen and (max-width: 600px) { + .drag-handle { + display: none; + pointer-events: none; + } +} + +.drag-handle svg { + height: 1rem; + width: 1rem; +} + +/* Math Equations (KaTeX) */ +.katex:hover { + background-color: var(--code-bg); +} + +.katex.result { + border-bottom: 1px dashed var(--highlight-border); + background-color: var(--highlight-color); +} + +/* Table Styling */ +.ProseMirror .tableWrapper { + margin: 0; + overflow: auto; + padding: 1rem; +} + +.ProseMirror table { + margin-top: 1rem; + margin-bottom: 1rem; + box-sizing: border-box; + width: 100%; + border-collapse: collapse; + border-radius: 0.25rem; + border: 1px solid var(--table-border); +} + +.ProseMirror table td, +.ProseMirror table th { + position: relative; + min-width: 100px; + border: 1px solid var(--table-border); + padding: 0.5rem; + text-align: left; + vertical-align: top; +} + +.ProseMirror table td:first-of-type:not(a), +.ProseMirror table th:first-of-type:not(a) { + margin-top: 0; +} + +.ProseMirror table td p, +.ProseMirror table th p { + margin: 0; +} + +.ProseMirror table td p + p, +.ProseMirror table th p + p { + margin-top: 0.75rem; +} + +.ProseMirror table th { + font-weight: bold; +} + +.ProseMirror table .column-resize-handle { + pointer-events: none; + position: absolute; + top: 0; + right: -0.25rem; + bottom: -2px; + display: flex; + width: 0.5rem; + background-color: var(--table-border); +} + +.ProseMirror table .column-resize-handle::before { + content: ''; + margin-left: 0.5rem; + height: 100%; + width: 1px; + background-color: var(--table-border); +} + +.ProseMirror table .selectedCell { + border-style: double; + border-color: var(--table-border); + background-color: var(--table-bg-selected); +} + +.ProseMirror table .grip-column, +.ProseMirror table .grip-row { + position: absolute; + z-index: 10; + display: flex; + cursor: pointer; + align-items: center; + justify-content: center; + background-color: var(--table-bg-selected); +} + +.ProseMirror table .grip-column { + top: -0.75rem; + left: 0; + margin-left: -1px; + height: 0.75rem; + width: calc(100% + 1px); + border-left: 1px solid var(--table-border); +} + +.ProseMirror table .grip-column:hover::before, +.ProseMirror table .grip-column.selected::before { + content: ''; + width: 0.625rem; +} + +.ProseMirror table .grip-column:hover { + background-color: var(--table-bg-hover); +} + +.ProseMirror table .grip-column:hover::before { + border-bottom: 2px dotted var(--border-color-hover); +} + +.ProseMirror table .grip-column.first { + border-top-left-radius: 0.125rem; + border-color: transparent; +} + +.ProseMirror table .grip-column.last { + border-top-right-radius: 0.125rem; +} + +.ProseMirror table .grip-column.selected { + border-color: var(--table-border); + background-color: var(--table-bg-hover); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.ProseMirror table .grip-column.selected::before { + border-bottom: 2px dotted var(--border-color-hover); +} + +.ProseMirror table .grip-row { + left: -0.75rem; + top: 0; + margin-top: -1px; + height: calc(100% + 1px); + width: 0.75rem; + border-top: 1px solid var(--table-border); +} + +.ProseMirror table .grip-row:hover::before, +.ProseMirror table .grip-row.selected::before { + content: ''; + height: 0.625rem; +} + +.ProseMirror table .grip-row:hover { + background-color: var(--table-bg-hover); +} + +.ProseMirror table .grip-row:hover::before { + border-left: 2px dotted var(--border-color-hover); +} + +.ProseMirror table .grip-row.first { + border-top-left-radius: 0.125rem; + border-color: transparent; +} + +.ProseMirror table .grip-row.last { + border-bottom-left-radius: 0.125rem; +} + +.ProseMirror table .grip-row.selected { + border-color: var(--table-border); + background-color: var(--table-bg-hover); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); +} + +.ProseMirror table .grip-row.selected::before { + border-left: 2px dotted var(--border-color-hover); +} + +.tiptap .search-result { + background-color: var(--search-result-bg); + color: black; +} + +.tiptap .search-result-current { + background-color: var(--search-result-current-bg); + color: black; +} + +.code-wrapper { + background-color: var(--codeblock-bg); + border-radius: 0.5rem; + padding: 1rem; + position: relative; + height: fit-content; + width: 100%; + margin: 0.5rem 0; +} + +.code-wrapper-tile { + opacity: 0; + width: 100%; + display: flex; + position: absolute; + top: 0; + padding: 0.25rem; + right: 0; + align-items: center; + justify-content: space-between; + gap: 1rem; + transition: opacity 0.2s ease-in-out; +} + +.code-wrapper:hover .code-wrapper-tile { + opacity: 1; +} + +.tiptap iframe { + aspect-ratio: 16 / 9; +} + +/* Mathematics extension styles */ +.tiptap .tiptap-mathematics-render { + padding: 0 0.25rem; + border-radius: 0.25rem; +} + +/* Editable math block */ +.tiptap .tiptap-mathematics-render--editable { + cursor: pointer; + transition: background 0.2s; +} + +.tiptap .tiptap-mathematics-render--editable:hover { + background: var(--code-bg); +} + +/* Inline math */ +.tiptap .tiptap-mathematics-render[data-type='inline-math'] { + display: inline-block; +} + +/* Block math */ +.tiptap .tiptap-mathematics-render[data-type='block-math'] { + display: block; + margin: 1rem 0; + padding: 0.5rem; + text-align: center; + font-size: 1.25rem; +} + +/* Error styles */ +.tiptap .tiptap-mathematics-render.inline-math-error, +.tiptap .tiptap-mathematics-render.block-math-error { + background: var(--code-bg); + color: var(--color-destructive); + border: 1px solid darkred; +} diff --git a/src/lib/components/edra/editor.ts b/src/lib/components/edra/editor.ts new file mode 100644 index 00000000..b2a881ed --- /dev/null +++ b/src/lib/components/edra/editor.ts @@ -0,0 +1,130 @@ +import { Editor, type Extensions, type EditorOptions, type Content } from '@tiptap/core'; +import StarterKit from '@tiptap/starter-kit'; +import { getHandlePaste } from './utils.js'; +import Subscript from '@tiptap/extension-subscript'; +import Superscript from '@tiptap/extension-superscript'; +import Typography from '@tiptap/extension-typography'; +import { ColorHighlighter } from './extensions/ColorHighlighter.js'; +import { FontSize, TextStyle, Color } from '@tiptap/extension-text-style'; +import TextAlign from '@tiptap/extension-text-align'; +import Highlight from '@tiptap/extension-highlight'; +import SearchAndReplace from './extensions/FindAndReplace.js'; +import { TaskItem, TaskList } from '@tiptap/extension-list'; +import { Table, TableCell, TableRow, TableHeader } from './extensions/table/index.js'; +import { Placeholder } from '@tiptap/extensions'; +import { Markdown } from '@tiptap/markdown'; +import MathMatics from '@tiptap/extension-mathematics'; + +import AutoJoiner from 'tiptap-extension-auto-joiner'; +import 'katex/dist/katex.min.css'; +import { InlineMathReplacer } from './extensions/InlineMathReplacer.js'; + +export default ( + element?: HTMLElement, + content?: Content, + extensions?: Extensions, + options?: Partial +) => { + const editor = new Editor({ + element, + content, + extensions: [ + StarterKit.configure({ + orderedList: { + HTMLAttributes: { + class: 'list-decimal' + } + }, + bulletList: { + HTMLAttributes: { + class: 'list-disc' + } + }, + heading: { + levels: [1, 2, 3, 4] + }, + link: { + openOnClick: false, + autolink: true, + linkOnPaste: true + }, + codeBlock: false + }), + Highlight.configure({ + multicolor: true + }), + Placeholder.configure({ + emptyEditorClass: 'is-empty', + // Use a placeholder: + // Use different placeholders depending on the node type: + placeholder: ({ node }) => { + if (node.type.name === 'heading') { + return 'What’s the title?'; + } else if (node.type.name === 'paragraph') { + return 'Press / or write something ...'; + } + return ''; + } + }), + Color, + Subscript, + Superscript, + Typography, + ColorHighlighter, + TextStyle, + FontSize, + TextAlign.configure({ + types: ['heading', 'paragraph'] + }), + TaskList, + TaskItem.configure({ + nested: true + }), + SearchAndReplace, + InlineMathReplacer, + MathMatics.configure({ + blockOptions: { + onClick: (node, pos) => { + const newCalculation = prompt('Enter new calculation:', node.attrs.latex); + if (newCalculation) { + editor + .chain() + .setNodeSelection(pos) + .updateBlockMath({ latex: newCalculation }) + .focus() + .run(); + } + } + }, + inlineOptions: { + onClick: (node, pos) => { + const newCalculation = prompt('Enter new calculation:', node.attrs.latex); + if (newCalculation) { + editor + .chain() + .setNodeSelection(pos) + .updateInlineMath({ latex: newCalculation }) + .focus() + .run(); + } + } + } + }), + AutoJoiner, + Table, + TableHeader, + TableRow, + TableCell, + Markdown, + ...(extensions ?? []) + ], + ...options + }); + + editor.setOptions({ + editorProps: { + handlePaste: getHandlePaste(editor) + } + }); + return editor; +}; diff --git a/src/lib/components/edra/extensions/ColorHighlighter.ts b/src/lib/components/edra/extensions/ColorHighlighter.ts new file mode 100644 index 00000000..dff91741 --- /dev/null +++ b/src/lib/components/edra/extensions/ColorHighlighter.ts @@ -0,0 +1,28 @@ +import { Extension } from '@tiptap/core'; +import { Plugin } from '@tiptap/pm/state'; + +import { findColors } from '../utils.js'; + +export const ColorHighlighter = Extension.create({ + name: 'colorHighlighter', + + addProseMirrorPlugins() { + return [ + new Plugin({ + state: { + init(_, { doc }) { + return findColors(doc); + }, + apply(transaction, oldState) { + return transaction.docChanged ? findColors(transaction.doc) : oldState; + } + }, + props: { + decorations(state) { + return this.getState(state); + } + } + }) + ]; + } +}); diff --git a/src/lib/components/edra/extensions/FindAndReplace.ts b/src/lib/components/edra/extensions/FindAndReplace.ts new file mode 100644 index 00000000..a64e8f19 --- /dev/null +++ b/src/lib/components/edra/extensions/FindAndReplace.ts @@ -0,0 +1,408 @@ +// MIT License + +// Copyright (c) 2023 - 2024 Jeet Mandaliya (Github Username: sereneinserenade) + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { Extension, type Range, type Dispatch } from '@tiptap/core'; +import { Decoration, DecorationSet } from '@tiptap/pm/view'; +import { Plugin, PluginKey, type EditorState, type Transaction } from '@tiptap/pm/state'; +import { Node as PMNode } from '@tiptap/pm/model'; + +declare module '@tiptap/core' { + interface Commands { + search: { + /** + * @description Set search term in extension. + */ + setSearchTerm: (searchTerm: string) => ReturnType; + /** + * @description Set replace term in extension. + */ + setReplaceTerm: (replaceTerm: string) => ReturnType; + /** + * @description Set case sensitivity in extension. + */ + setCaseSensitive: (caseSensitive: boolean) => ReturnType; + /** + * @description Reset current search result to first instance. + */ + resetIndex: () => ReturnType; + /** + * @description Find next instance of search result. + */ + nextSearchResult: () => ReturnType; + /** + * @description Find previous instance of search result. + */ + previousSearchResult: () => ReturnType; + /** + * @description Replace first instance of search result with given replace term. + */ + replace: () => ReturnType; + /** + * @description Replace all instances of search result with given replace term. + */ + replaceAll: () => ReturnType; + }; + } +} + +interface TextNodesWithPosition { + text: string; + pos: number; +} + +const getRegex = (s: string, disableRegex: boolean, caseSensitive: boolean): RegExp => { + return RegExp( + disableRegex ? s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : s, + caseSensitive ? 'gu' : 'gui' + ); +}; + +interface ProcessedSearches { + decorationsToReturn: DecorationSet; + results: Range[]; +} + +function processSearches( + doc: PMNode, + searchTerm: RegExp, + searchResultClass: string, + resultIndex: number +): ProcessedSearches { + const decorations: Decoration[] = []; + const results: Range[] = []; + + let textNodesWithPosition: TextNodesWithPosition[] = []; + let index = 0; + + if (!searchTerm) { + return { + decorationsToReturn: DecorationSet.empty, + results: [] + }; + } + + doc?.descendants((node, pos) => { + if (node.isText) { + if (textNodesWithPosition[index]) { + textNodesWithPosition[index] = { + text: textNodesWithPosition[index].text + node.text, + pos: textNodesWithPosition[index].pos + }; + } else { + textNodesWithPosition[index] = { + text: `${node.text}`, + pos + }; + } + } else { + index += 1; + } + }); + + textNodesWithPosition = textNodesWithPosition.filter(Boolean); + + for (const element of textNodesWithPosition) { + const { text, pos } = element; + const matches = Array.from(text.matchAll(searchTerm)).filter(([matchText]) => matchText.trim()); + + for (const m of matches) { + if (m[0] === '') break; + + if (m.index !== undefined) { + results.push({ + from: pos + m.index, + to: pos + m.index + m[0].length + }); + } + } + } + + for (let i = 0; i < results.length; i += 1) { + const r = results[i]; + const className = + i === resultIndex ? `${searchResultClass} ${searchResultClass}-current` : searchResultClass; + const decoration: Decoration = Decoration.inline(r.from, r.to, { + class: className + }); + + decorations.push(decoration); + } + + return { + decorationsToReturn: DecorationSet.create(doc, decorations), + results + }; +} + +const replace = ( + replaceTerm: string, + results: Range[], + { state, dispatch }: { state: EditorState; dispatch: Dispatch } +) => { + const firstResult = results[0]; + + if (!firstResult) return; + + const { from, to } = results[0]; + + if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to)); +}; + +const rebaseNextResult = ( + replaceTerm: string, + index: number, + lastOffset: number, + results: Range[] +): [number, Range[]] | null => { + const nextIndex = index + 1; + + if (!results[nextIndex]) return null; + + const { from: currentFrom, to: currentTo } = results[index]; + + const offset = currentTo - currentFrom - replaceTerm.length + lastOffset; + + const { from, to } = results[nextIndex]; + + results[nextIndex] = { + to: to - offset, + from: from - offset + }; + + return [offset, results]; +}; + +const replaceAll = ( + replaceTerm: string, + results: Range[], + { tr, dispatch }: { tr: Transaction; dispatch: Dispatch } +) => { + let offset = 0; + + let resultsCopy = results.slice(); + + if (!resultsCopy.length) return; + + for (let i = 0; i < resultsCopy.length; i += 1) { + const { from, to } = resultsCopy[i]; + + tr.insertText(replaceTerm, from, to); + + const rebaseNextResultResponse = rebaseNextResult(replaceTerm, i, offset, resultsCopy); + + if (!rebaseNextResultResponse) continue; + + offset = rebaseNextResultResponse[0]; + resultsCopy = rebaseNextResultResponse[1]; + } + + if (dispatch) { + dispatch(tr); + } +}; + +export const searchAndReplacePluginKey = new PluginKey('searchAndReplacePlugin'); + +export interface SearchAndReplaceOptions { + searchResultClass: string; + disableRegex: boolean; +} + +export interface SearchAndReplaceStorage { + searchTerm: string; + replaceTerm: string; + results: Range[]; + lastSearchTerm: string; + caseSensitive: boolean; + lastCaseSensitive: boolean; + resultIndex: number; + lastResultIndex: number; +} + +export const SearchAndReplace = Extension.create({ + name: 'searchAndReplace', + + addOptions() { + return { + searchResultClass: 'search-result', + disableRegex: true + }; + }, + + addStorage() { + return { + searchTerm: '', + replaceTerm: '', + results: [], + lastSearchTerm: '', + caseSensitive: false, + lastCaseSensitive: false, + resultIndex: 0, + lastResultIndex: 0 + }; + }, + + addCommands() { + return { + setSearchTerm: + (searchTerm: string) => + ({ editor }) => { + editor.storage.searchAndReplace.searchTerm = searchTerm; + + return false; + }, + setReplaceTerm: + (replaceTerm: string) => + ({ editor }) => { + editor.storage.searchAndReplace.replaceTerm = replaceTerm; + + return false; + }, + setCaseSensitive: + (caseSensitive: boolean) => + ({ editor }) => { + editor.storage.searchAndReplace.caseSensitive = caseSensitive; + + return false; + }, + resetIndex: + () => + ({ editor }) => { + editor.storage.searchAndReplace.resultIndex = 0; + + return false; + }, + nextSearchResult: + () => + ({ editor }) => { + const { results, resultIndex } = editor.storage.searchAndReplace; + + const nextIndex = resultIndex + 1; + + if (results[nextIndex]) { + editor.storage.searchAndReplace.resultIndex = nextIndex; + } else { + editor.storage.searchAndReplace.resultIndex = 0; + } + + return false; + }, + previousSearchResult: + () => + ({ editor }) => { + const { results, resultIndex } = editor.storage.searchAndReplace; + + const prevIndex = resultIndex - 1; + + if (results[prevIndex]) { + editor.storage.searchAndReplace.resultIndex = prevIndex; + } else { + editor.storage.searchAndReplace.resultIndex = results.length - 1; + } + + return false; + }, + replace: + () => + ({ editor, state, dispatch }) => { + const { replaceTerm, results } = editor.storage.searchAndReplace; + + replace(replaceTerm, results, { state, dispatch }); + + return false; + }, + replaceAll: + () => + ({ editor, tr, dispatch }) => { + const { replaceTerm, results } = editor.storage.searchAndReplace; + + replaceAll(replaceTerm, results, { tr, dispatch }); + + return false; + } + }; + }, + + addProseMirrorPlugins() { + const editor = this.editor; + const { searchResultClass, disableRegex } = this.options; + + const setLastSearchTerm = (t: string) => (editor.storage.searchAndReplace.lastSearchTerm = t); + const setLastCaseSensitive = (t: boolean) => + (editor.storage.searchAndReplace.lastCaseSensitive = t); + const setLastResultIndex = (t: number) => (editor.storage.searchAndReplace.lastResultIndex = t); + + return [ + new Plugin({ + key: searchAndReplacePluginKey, + state: { + init: () => DecorationSet.empty, + apply({ doc, docChanged }, oldState) { + const { + searchTerm, + lastSearchTerm, + caseSensitive, + lastCaseSensitive, + resultIndex, + lastResultIndex + } = editor.storage.searchAndReplace; + + if ( + !docChanged && + lastSearchTerm === searchTerm && + lastCaseSensitive === caseSensitive && + lastResultIndex === resultIndex + ) + return oldState; + + setLastSearchTerm(searchTerm); + setLastCaseSensitive(caseSensitive); + setLastResultIndex(resultIndex); + + if (!searchTerm) { + editor.storage.searchAndReplace.results = []; + return DecorationSet.empty; + } + + const { decorationsToReturn, results } = processSearches( + doc, + getRegex(searchTerm, disableRegex, caseSensitive), + searchResultClass, + resultIndex + ); + + editor.storage.searchAndReplace.results = results; + + return decorationsToReturn; + } + }, + props: { + decorations(state) { + return this.getState(state); + } + } + }) + ]; + } +}); + +export default SearchAndReplace; diff --git a/src/lib/components/edra/extensions/InlineMathReplacer.ts b/src/lib/components/edra/extensions/InlineMathReplacer.ts new file mode 100644 index 00000000..f4a08d06 --- /dev/null +++ b/src/lib/components/edra/extensions/InlineMathReplacer.ts @@ -0,0 +1,21 @@ +import { textInputRule } from '@tiptap/core'; +import { InlineMath } from '@tiptap/extension-mathematics'; + +export const InlineMathReplacer = InlineMath.extend({ + name: 'inlineMathReplacer', + addInputRules() { + return [ + textInputRule({ + find: /\$\$([^$]+)\$\$/, + replace: ({ match, commands }) => { + const latex = match[1]; + // Insert the inline math node with the LaTeX content + commands.insertInlineMath({ + latex + }); + return ''; + } + }) + ]; + } +}); diff --git a/src/lib/components/edra/extensions/audio/AudiExtended.ts b/src/lib/components/edra/extensions/audio/AudiExtended.ts new file mode 100644 index 00000000..4edd2400 --- /dev/null +++ b/src/lib/components/edra/extensions/audio/AudiExtended.ts @@ -0,0 +1,34 @@ +import { SvelteNodeViewRenderer } from 'svelte-tiptap'; +import { Audio } from './AudioExtension.js'; +import type { NodeViewProps } from '@tiptap/core'; +import type { Component } from 'svelte'; + +export const AudioExtended = (content: Component) => + Audio.extend({ + addAttributes() { + return { + src: { + default: null + }, + alt: { + default: null + }, + title: { + default: null + }, + width: { + default: '100%' + }, + height: { + default: null + }, + align: { + default: 'left' + } + }; + }, + + addNodeView: () => { + return SvelteNodeViewRenderer(content); + } + }); diff --git a/src/lib/components/edra/extensions/audio/AudioExtension.ts b/src/lib/components/edra/extensions/audio/AudioExtension.ts new file mode 100644 index 00000000..5e2da234 --- /dev/null +++ b/src/lib/components/edra/extensions/audio/AudioExtension.ts @@ -0,0 +1,147 @@ +import { Node, nodeInputRule } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +export interface AudioOptions { + HTMLAttributes: Record; +} + +declare module '@tiptap/core' { + interface Commands { + audio: { + /** + * Set a audio node + */ + setAudio: (src: string) => ReturnType; + /** + * Toggle a audio + */ + toggleAudio: (src: string) => ReturnType; + /** + * Remove a audio + */ + removeAudio: () => ReturnType; + }; + } +} + +const AUDIO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/; + +export const Audio = Node.create({ + name: 'audio', + group: 'block', + draggable: true, + isolating: true, + atom: true, + addOptions() { + return { + HTMLAttributes: {} + }; + }, + addAttributes() { + return { + src: { + default: null, + parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'), + renderHTML: (attrs) => ({ src: attrs.src }) + } + }; + }, + parseHTML() { + return [ + { + tag: 'audio', + getAttrs: (el) => ({ src: (el as HTMLAudioElement).getAttribute('src') }) + } + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'audio', + { controls: 'true', style: 'width: 100%;', ...HTMLAttributes }, + ['source', HTMLAttributes] + ]; + }, + addCommands() { + return { + setAudio: + (src: string) => + ({ commands }) => + commands.insertContent( + `` + ), + + toggleAudio: + () => + ({ commands }) => + commands.toggleNode(this.name, 'paragraph'), + removeAudio: + () => + ({ commands }) => + commands.deleteNode(this.name) + }; + }, + addInputRules() { + return [ + nodeInputRule({ + find: AUDIO_INPUT_REGEX, + type: this.type, + getAttributes: (match) => { + const [, , src] = match; + + return { src }; + } + }) + ]; + }, + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey('audioDropPlugin'), + + props: { + handleDOMEvents: { + drop(view, event) { + const { + state: { schema, tr }, + dispatch + } = view; + const hasFiles = + event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length; + + if (!hasFiles) return false; + + const audios = Array.from(event.dataTransfer.files).filter((file) => + /audio/i.test(file.type) + ); + + if (audios.length === 0) return false; + + event.preventDefault(); + + const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY }); + + audios.forEach((audio) => { + const reader = new FileReader(); + + reader.onload = (readerEvent) => { + const node = schema.nodes.audio.create({ src: readerEvent.target?.result }); + + if (coordinates && typeof coordinates.pos === 'number') { + const transaction = tr.insert(coordinates?.pos, node); + + dispatch(transaction); + } + }; + + reader.readAsDataURL(audio); + }); + + return true; + } + } + } + }) + ]; + } +}); diff --git a/src/lib/components/edra/extensions/audio/AudioPlaceholder.ts b/src/lib/components/edra/extensions/audio/AudioPlaceholder.ts new file mode 100644 index 00000000..ca7b2b3d --- /dev/null +++ b/src/lib/components/edra/extensions/audio/AudioPlaceholder.ts @@ -0,0 +1,64 @@ +import { Editor, Node, mergeAttributes, type CommandProps, type NodeViewProps } from '@tiptap/core'; +import type { Component } from 'svelte'; +import { SvelteNodeViewRenderer } from 'svelte-tiptap'; + +export interface AudioPlaceholderOptions { + HTMLAttributes: Record; + onDrop: (files: File[], editor: Editor) => void; + onDropRejected?: (files: File[], editor: Editor) => void; + onEmbed: (url: string, editor: Editor) => void; + allowedMimeTypes?: Record; + maxFiles?: number; + maxSize?: number; +} + +declare module '@tiptap/core' { + interface Commands { + audioPlaceholder: { + /** + * Inserts an audio placeholder + */ + insertAudioPlaceholder: () => ReturnType; + }; + } +} + +export const AudioPlaceholder = ( + component: Component +): Node => + Node.create({ + name: 'audio-placeholder', + addOptions() { + return { + HTMLAttributes: {}, + onDrop: () => {}, + onDropRejected: () => {}, + onEmbed: () => {} + }; + }, + parseHTML() { + return [{ tag: `div[data-type="${this.name}"]` }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['div', mergeAttributes(HTMLAttributes)]; + }, + group: 'block', + draggable: true, + atom: true, + content: 'inline*', + isolating: true, + + addNodeView() { + return SvelteNodeViewRenderer(component); + }, + addCommands() { + return { + insertAudioPlaceholder: () => (props: CommandProps) => { + return props.commands.insertContent({ + type: 'audio-placeholder' + }); + } + }; + } + }); diff --git a/src/lib/components/edra/extensions/drag-handle/ClipboardSerializer.ts b/src/lib/components/edra/extensions/drag-handle/ClipboardSerializer.ts new file mode 100644 index 00000000..e7c8648e --- /dev/null +++ b/src/lib/components/edra/extensions/drag-handle/ClipboardSerializer.ts @@ -0,0 +1,28 @@ +import { Slice } from '@tiptap/pm/model'; +import { EditorView } from '@tiptap/pm/view'; +import * as pmView from '@tiptap/pm/view'; + +function getPmView() { + try { + return pmView; + } catch (error) { + console.error(error); + return null; + } +} + +export function serializeForClipboard(view: EditorView, slice: Slice) { + // Newer Tiptap/ProseMirror + if (view && typeof view.serializeForClipboard === 'function') { + return view.serializeForClipboard(slice); + } + + // Older version fallback + const proseMirrorView = getPmView(); + + if (proseMirrorView && typeof proseMirrorView?.__serializeForClipboard === 'function') { + return proseMirrorView.__serializeForClipboard(view, slice); + } + + throw new Error('No supported clipboard serialization method found.'); +} diff --git a/src/lib/components/edra/extensions/drag-handle/index.ts b/src/lib/components/edra/extensions/drag-handle/index.ts new file mode 100644 index 00000000..0da5ff1e --- /dev/null +++ b/src/lib/components/edra/extensions/drag-handle/index.ts @@ -0,0 +1,381 @@ +import { Extension } from '@tiptap/core'; +import { NodeSelection, Plugin, PluginKey, TextSelection } from '@tiptap/pm/state'; +import { Fragment, Slice, Node } from '@tiptap/pm/model'; +import { EditorView } from '@tiptap/pm/view'; +import { serializeForClipboard } from './ClipboardSerializer.js'; + +export interface GlobalDragHandleOptions { + /** + * The width of the drag handle + */ + dragHandleWidth: number; + + /** + * The treshold for scrolling + */ + scrollTreshold: number; + + /* + * The css selector to query for the drag handle. (eg: '.custom-handle'). + * If handle element is found, that element will be used as drag handle. If not, a default handle will be created + */ + dragHandleSelector?: string; + + /** + * Tags to be excluded for drag handle + */ + excludedTags: string[]; + + /** + * Custom nodes to be included for drag handle + */ + customNodes: string[]; + + /** + * onNodeChange callback for drag handle + * @param data + * @returns + */ + onMouseMove?: (data: { node: Node; pos: number }) => void; +} +function absoluteRect(node: Element) { + const data = node.getBoundingClientRect(); + const modal = node.closest('[role="dialog"]'); + + if (modal && window.getComputedStyle(modal).transform !== 'none') { + const modalRect = modal.getBoundingClientRect(); + + return { + top: data.top - modalRect.top, + left: data.left - modalRect.left, + width: data.width + }; + } + return { + top: data.top, + left: data.left, + width: data.width + }; +} + +function nodeDOMAtCoords(coords: { x: number; y: number }, options: GlobalDragHandleOptions) { + const selectors = [ + 'li', + 'p:not(:first-child)', + 'pre', + 'blockquote', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + ...options.customNodes.map((node) => `[data-type=${node}]`) + ].join(', '); + return document + .elementsFromPoint(coords.x, coords.y) + .find( + (elem: Element) => elem.parentElement?.matches?.('.ProseMirror') || elem.matches(selectors) + ); +} +function nodePosAtDOM(node: Element, view: EditorView, options: GlobalDragHandleOptions) { + const boundingRect = node.getBoundingClientRect(); + + return view.posAtCoords({ + left: boundingRect.left + 50 + options.dragHandleWidth, + top: boundingRect.top + 1 + })?.inside; +} + +function calcNodePos(pos: number, view: EditorView) { + const $pos = view.state.doc.resolve(pos); + if ($pos.depth > 1) return $pos.before($pos.depth); + return pos; +} + +export function DragHandlePlugin(options: GlobalDragHandleOptions & { pluginKey: string }) { + let listType = ''; + function handleDragStart(event: DragEvent, view: EditorView) { + view.focus(); + + if (!event.dataTransfer) return; + + const node = nodeDOMAtCoords( + { + x: event.clientX + 50 + options.dragHandleWidth, + y: event.clientY + }, + options + ); + + if (!(node instanceof Element)) return; + + let draggedNodePos = nodePosAtDOM(node, view, options); + if (draggedNodePos == null || draggedNodePos < 0) return; + draggedNodePos = calcNodePos(draggedNodePos, view); + + const { from, to } = view.state.selection; + const diff = from - to; + + const fromSelectionPos = calcNodePos(from, view); + let differentNodeSelected = false; + + const nodePos = view.state.doc.resolve(fromSelectionPos); + + // Check if nodePos points to the top level node + if (nodePos.node().type.name === 'doc') differentNodeSelected = true; + else { + const nodeSelection = NodeSelection.create(view.state.doc, nodePos.before()); + + // Check if the node where the drag event started is part of the current selection + differentNodeSelected = !( + draggedNodePos + 1 >= nodeSelection.$from.pos && draggedNodePos <= nodeSelection.$to.pos + ); + } + let selection = view.state.selection; + if (!differentNodeSelected && diff !== 0 && !(view.state.selection instanceof NodeSelection)) { + const endSelection = NodeSelection.create(view.state.doc, to - 1); + selection = TextSelection.create(view.state.doc, draggedNodePos, endSelection.$to.pos); + } else { + selection = NodeSelection.create(view.state.doc, draggedNodePos); + + // if inline node is selected, e.g mention -> go to the parent node to select the whole node + // if table row is selected, go to the parent node to select the whole node + if ( + (selection as NodeSelection).node.type.isInline || + (selection as NodeSelection).node.type.name === 'tableRow' + ) { + const $pos = view.state.doc.resolve(selection.from); + selection = NodeSelection.create(view.state.doc, $pos.before()); + } + } + view.dispatch(view.state.tr.setSelection(selection)); + + // If the selected node is a list item, we need to save the type of the wrapping list e.g. OL or UL + if ( + view.state.selection instanceof NodeSelection && + view.state.selection.node.type.name === 'listItem' + ) { + listType = node.parentElement!.tagName; + } + + const slice = view.state.selection.content(); + const { dom, text } = serializeForClipboard(view, slice); + + event.dataTransfer.clearData(); + event.dataTransfer.setData('text/html', dom.innerHTML); + event.dataTransfer.setData('text/plain', text); + event.dataTransfer.effectAllowed = 'copyMove'; + + event.dataTransfer.setDragImage(node, 0, 0); + + view.dragging = { slice, move: event.ctrlKey }; + } + + let dragHandleElement: HTMLElement | null = null; + + function hideDragHandle() { + if (dragHandleElement) { + dragHandleElement.classList.add('hide'); + } + } + + function showDragHandle() { + if (dragHandleElement) { + dragHandleElement.classList.remove('hide'); + } + } + + function hideHandleOnEditorOut(event: MouseEvent) { + if (event.target instanceof Element) { + // Check if the relatedTarget class is still inside the editor + const relatedTarget = event.relatedTarget as HTMLElement; + const isInsideEditor = + relatedTarget?.classList.contains('tiptap') || + relatedTarget?.classList.contains('drag-handle'); + + if (isInsideEditor) return; + } + hideDragHandle(); + } + + return new Plugin({ + key: new PluginKey(options.pluginKey), + view: (view) => { + const handleBySelector = options.dragHandleSelector + ? document.querySelector(options.dragHandleSelector) + : null; + dragHandleElement = handleBySelector ?? document.createElement('div'); + dragHandleElement.draggable = true; + dragHandleElement.dataset.dragHandle = ''; + dragHandleElement.classList.add('drag-handle'); + + function onDragHandleDragStart(e: DragEvent) { + handleDragStart(e, view); + } + + dragHandleElement.addEventListener('dragstart', onDragHandleDragStart); + + function onDragHandleDrag(e: DragEvent) { + hideDragHandle(); + const scrollY = window.scrollY; + if (e.clientY < options.scrollTreshold) { + window.scrollTo({ top: scrollY - 30, behavior: 'smooth' }); + } else if (window.innerHeight - e.clientY < options.scrollTreshold) { + window.scrollTo({ top: scrollY + 30, behavior: 'smooth' }); + } + } + + dragHandleElement.addEventListener('drag', onDragHandleDrag); + + hideDragHandle(); + + if (!handleBySelector) { + view?.dom?.parentElement?.appendChild(dragHandleElement); + } + view?.dom?.parentElement?.addEventListener('mouseout', hideHandleOnEditorOut); + + return { + destroy: () => { + if (!handleBySelector) { + dragHandleElement?.remove?.(); + } + dragHandleElement?.removeEventListener('drag', onDragHandleDrag); + dragHandleElement?.removeEventListener('dragstart', onDragHandleDragStart); + dragHandleElement = null; + view?.dom?.parentElement?.removeEventListener('mouseout', hideHandleOnEditorOut); + } + }; + }, + props: { + handleDOMEvents: { + mousemove: (view, event) => { + if (!view.editable) { + return; + } + + const node = nodeDOMAtCoords( + { + x: event.clientX + 50 + options.dragHandleWidth, + y: event.clientY + }, + options + ); + + const notDragging = node?.closest('.not-draggable'); + const excludedTagList = options.excludedTags.concat(['ol', 'ul']).join(', '); + + if (!(node instanceof Element) || node.matches(excludedTagList) || notDragging) { + hideDragHandle(); + return; + } + + const nodePos = nodePosAtDOM(node, view, options); + if (nodePos !== undefined) { + const currentNode = view.state.doc.nodeAt(nodePos); + if (currentNode !== null) { + options.onMouseMove?.({ node: currentNode, pos: nodePos }); + } + } + + const compStyle = window.getComputedStyle(node); + const parsedLineHeight = parseInt(compStyle.lineHeight, 10); + const lineHeight = isNaN(parsedLineHeight) + ? parseInt(compStyle.fontSize) * 1.2 + : parsedLineHeight; + const paddingTop = parseInt(compStyle.paddingTop, 10); + + const rect = absoluteRect(node); + + rect.top += (lineHeight - 24) / 2; + rect.top += paddingTop; + // Li markers + if (node.matches('ul:not([data-type=taskList]) li, ol li')) { + rect.left -= options.dragHandleWidth; + } + rect.width = options.dragHandleWidth; + + if (!dragHandleElement) return; + + dragHandleElement.style.left = `${rect.left - rect.width}px`; + dragHandleElement.style.top = `${rect.top}px`; + showDragHandle(); + }, + keydown: () => { + hideDragHandle(); + }, + mousewheel: () => { + hideDragHandle(); + }, + // dragging class is used for CSS + dragstart: (view) => { + view.dom.classList.add('dragging'); + }, + drop: (view, event) => { + view.dom.classList.remove('dragging'); + hideDragHandle(); + let droppedNode: Node | null = null; + const dropPos = view.posAtCoords({ + left: event.clientX, + top: event.clientY + }); + + if (!dropPos) return; + + if (view.state.selection instanceof NodeSelection) { + droppedNode = view.state.selection.node; + } + if (!droppedNode) return; + + const resolvedPos = view.state.doc.resolve(dropPos.pos); + + const isDroppedInsideList = resolvedPos.parent.type.name === 'listItem'; + + // If the selected node is a list item and is not dropped inside a list, we need to wrap it inside
    tag otherwise ol list items will be transformed into ul list item when dropped + if ( + view.state.selection instanceof NodeSelection && + view.state.selection.node.type.name === 'listItem' && + !isDroppedInsideList && + listType == 'OL' + ) { + const newList = view.state.schema.nodes.orderedList?.createAndFill(null, droppedNode); + const slice = new Slice(Fragment.from(newList), 0, 0); + view.dragging = { slice, move: event.ctrlKey }; + } + }, + dragend: (view) => { + view.dom.classList.remove('dragging'); + } + } + } + }); +} + +const GlobalDragHandle = Extension.create({ + name: 'globalDragHandle', + + addOptions() { + return { + dragHandleWidth: 20, + scrollTreshold: 100, + excludedTags: [], + customNodes: [] + }; + }, + + addProseMirrorPlugins() { + return [ + DragHandlePlugin({ + pluginKey: 'globalDragHandle', + dragHandleWidth: this.options.dragHandleWidth, + scrollTreshold: this.options.scrollTreshold, + dragHandleSelector: this.options.dragHandleSelector, + excludedTags: this.options.excludedTags, + customNodes: this.options.customNodes, + onMouseMove: this.options.onMouseMove + }) + ]; + } +}); + +export default GlobalDragHandle; diff --git a/src/lib/components/edra/extensions/iframe/IFrame.ts b/src/lib/components/edra/extensions/iframe/IFrame.ts new file mode 100644 index 00000000..bf91ece9 --- /dev/null +++ b/src/lib/components/edra/extensions/iframe/IFrame.ts @@ -0,0 +1,85 @@ +import { Node } from '@tiptap/core'; + +export interface IframeOptions { + allowFullscreen: boolean; + HTMLAttributes: { + [key: string]: unknown; + }; +} + +declare module '@tiptap/core' { + interface Commands { + iframe: { + /** + * Add an iframe with src + */ + setIframe: (options: { src: string }) => ReturnType; + removeIframe: () => ReturnType; + }; + } +} + +export default Node.create({ + name: 'iframe', + + group: 'block', + + atom: true, + + addOptions() { + return { + allowFullscreen: true, + HTMLAttributes: { + class: 'iframe-wrapper' + } + }; + }, + + addAttributes() { + return { + src: { + default: null + }, + frameborder: { + default: 0 + }, + allowfullscreen: { + default: this.options.allowFullscreen, + parseHTML: () => this.options.allowFullscreen + } + }; + }, + + parseHTML() { + return [ + { + tag: 'iframe' + } + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ['div', this.options.HTMLAttributes, ['iframe', HTMLAttributes]]; + }, + + addCommands() { + return { + setIframe: + (options: { src: string }) => + ({ tr, dispatch }) => { + const { selection } = tr; + const node = this.type.create(options); + + if (dispatch) { + tr.replaceRangeWith(selection.from, selection.to, node); + } + + return true; + }, + removeIframe: + () => + ({ commands }) => + commands.deleteNode(this.name) + }; + } +}); diff --git a/src/lib/components/edra/extensions/iframe/IFrameExtended.ts b/src/lib/components/edra/extensions/iframe/IFrameExtended.ts new file mode 100644 index 00000000..95c37ecb --- /dev/null +++ b/src/lib/components/edra/extensions/iframe/IFrameExtended.ts @@ -0,0 +1,35 @@ +import { SvelteNodeViewRenderer } from 'svelte-tiptap'; + +import type { NodeViewProps } from '@tiptap/core'; +import type { Component } from 'svelte'; +import IFrame from './IFrame.js'; + +export const IFrameExtended = (content: Component) => + IFrame.extend({ + addAttributes() { + return { + src: { + default: null + }, + alt: { + default: null + }, + title: { + default: null + }, + width: { + default: '100%' + }, + height: { + default: null + }, + align: { + default: 'left' + } + }; + }, + + addNodeView: () => { + return SvelteNodeViewRenderer(content); + } + }); diff --git a/src/lib/components/edra/extensions/iframe/IFramePlaceholder.ts b/src/lib/components/edra/extensions/iframe/IFramePlaceholder.ts new file mode 100644 index 00000000..96ba8bfd --- /dev/null +++ b/src/lib/components/edra/extensions/iframe/IFramePlaceholder.ts @@ -0,0 +1,56 @@ +import { Node, mergeAttributes, type CommandProps, type NodeViewProps } from '@tiptap/core'; +import type { Component } from 'svelte'; +import { SvelteNodeViewRenderer } from 'svelte-tiptap'; + +export interface IFramePlaceholderOptions { + HTMLAttributes: Record; +} + +declare module '@tiptap/core' { + interface Commands { + iframePlaceholder: { + /** + * Inserts a IFrame placeholder + */ + insertIFramePlaceholder: () => ReturnType; + }; + } +} + +export const IFramePlaceholder = (content: Component) => + Node.create({ + name: 'iframe-placeholder', + addOptions() { + return { + HTMLAttributes: {}, + onDrop: () => {}, + onDropRejected: () => {}, + onEmbed: () => {} + }; + }, + parseHTML() { + return [{ tag: `div[data-type="${this.name}"]` }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['div', mergeAttributes(HTMLAttributes)]; + }, + group: 'block', + draggable: true, + atom: true, + content: 'inline*', + isolating: true, + + addNodeView() { + return SvelteNodeViewRenderer(content); + }, + addCommands() { + return { + insertIFramePlaceholder: () => (props: CommandProps) => { + return props.commands.insertContent({ + type: 'iframe-placeholder' + }); + } + }; + } + }); diff --git a/src/lib/components/edra/extensions/image/ImageExtended.ts b/src/lib/components/edra/extensions/image/ImageExtended.ts new file mode 100644 index 00000000..e5430956 --- /dev/null +++ b/src/lib/components/edra/extensions/image/ImageExtended.ts @@ -0,0 +1,36 @@ +import { SvelteNodeViewRenderer } from 'svelte-tiptap'; +import Image, { type ImageOptions } from '@tiptap/extension-image'; +import type { Component } from 'svelte'; +import type { NodeViewProps, Node } from '@tiptap/core'; + +export const ImageExtended = (component: Component): Node => { + return Image.extend({ + addAttributes() { + return { + src: { + default: null + }, + alt: { + default: null + }, + title: { + default: null + }, + width: { + default: '100%' + }, + height: { + default: null + }, + align: { + default: 'left' + } + }; + }, + addNodeView: () => { + return SvelteNodeViewRenderer(component); + } + }).configure({ + allowBase64: true + }); +}; diff --git a/src/lib/components/edra/extensions/image/ImagePlaceholder.ts b/src/lib/components/edra/extensions/image/ImagePlaceholder.ts new file mode 100644 index 00000000..b031abf0 --- /dev/null +++ b/src/lib/components/edra/extensions/image/ImagePlaceholder.ts @@ -0,0 +1,64 @@ +import { Editor, Node, mergeAttributes, type CommandProps, type NodeViewProps } from '@tiptap/core'; +import type { Component } from 'svelte'; +import { SvelteNodeViewRenderer } from 'svelte-tiptap'; + +export interface ImagePlaceholderOptions { + HTMLAttributes: Record; + onDrop: (files: File[], editor: Editor) => void; + onDropRejected?: (files: File[], editor: Editor) => void; + onEmbed: (url: string, editor: Editor) => void; + allowedMimeTypes?: Record; + maxFiles?: number; + maxSize?: number; +} + +declare module '@tiptap/core' { + interface Commands { + imagePlaceholder: { + /** + * Inserts an image placeholder + */ + insertImagePlaceholder: () => ReturnType; + }; + } +} + +export const ImagePlaceholder = ( + component: Component +): Node => + Node.create({ + name: 'image-placeholder', + addOptions() { + return { + HTMLAttributes: {}, + onDrop: () => {}, + onDropRejected: () => {}, + onEmbed: () => {} + }; + }, + parseHTML() { + return [{ tag: `div[data-type="${this.name}"]` }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['div', mergeAttributes(HTMLAttributes)]; + }, + group: 'block', + draggable: true, + atom: true, + content: 'inline*', + isolating: true, + + addNodeView() { + return SvelteNodeViewRenderer(component); + }, + addCommands() { + return { + insertImagePlaceholder: () => (props: CommandProps) => { + return props.commands.insertContent({ + type: 'image-placeholder' + }); + } + }; + } + }); diff --git a/src/lib/components/edra/extensions/slash-command/groups.ts b/src/lib/components/edra/extensions/slash-command/groups.ts new file mode 100644 index 00000000..6ada0ba8 --- /dev/null +++ b/src/lib/components/edra/extensions/slash-command/groups.ts @@ -0,0 +1,59 @@ +import commands from '../../commands/toolbar-commands.js'; + +import type { EdraToolBarCommands } from '../../commands/types.js'; +import type { Editor } from '@tiptap/core'; +import Quote from '@lucide/svelte/icons/quote'; +import SquareCode from '@lucide/svelte/icons/square-code'; +import Minus from '@lucide/svelte/icons/minus'; + +export interface Group { + name: string; + title: string; + actions: EdraToolBarCommands[]; +} + +export const GROUPS: Group[] = [ + { + name: 'format', + title: 'Format', + actions: [ + ...commands.headings, + { + icon: Quote, + name: 'blockquote', + tooltip: 'Blockquote', + onClick: (editor: Editor) => { + editor.chain().focus().setBlockquote().run(); + } + }, + { + icon: SquareCode, + name: 'codeBlock', + tooltip: 'Code Block', + onClick: (editor: Editor) => { + editor.chain().focus().setCodeBlock().run(); + } + }, + ...commands.lists + ] + }, + { + name: 'insert', + title: 'Insert', + actions: [ + ...commands.media, + ...commands.table, + ...commands.math, + { + icon: Minus, + name: 'horizontalRule', + tooltip: 'Horizontal Rule', + onClick: (editor: Editor) => { + editor.chain().focus().setHorizontalRule().run(); + } + } + ] + } +]; + +export default GROUPS; diff --git a/src/lib/components/edra/extensions/slash-command/slashcommand.ts b/src/lib/components/edra/extensions/slash-command/slashcommand.ts new file mode 100644 index 00000000..fbf85f37 --- /dev/null +++ b/src/lib/components/edra/extensions/slash-command/slashcommand.ts @@ -0,0 +1,259 @@ +import { Editor, Extension } from '@tiptap/core'; +import Suggestion, { type SuggestionProps, type SuggestionKeyDownProps } from '@tiptap/suggestion'; +import { PluginKey } from '@tiptap/pm/state'; +import { computePosition, flip, offset, autoUpdate, type Placement } from '@floating-ui/dom'; + +import { GROUPS } from './groups.js'; +import SvelteRenderer from '../../svelte-renderer.js'; + +import type { Component } from 'svelte'; + +const extensionName = 'slashCommand'; + +interface PopupState { + element: HTMLElement | null; + cleanup: (() => void) | null; + isVisible: boolean; +} + +const popup: PopupState = { + element: null, + cleanup: null, + isVisible: false +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export default (menuList: Component): Extension => + Extension.create({ + name: extensionName, + + priority: 200, + + onCreate() { + // Create popup container + popup.element = document.createElement('div'); + popup.element.style.position = 'fixed'; + popup.element.style.zIndex = '9999'; + popup.element.style.maxWidth = '16rem'; + popup.element.style.visibility = 'hidden'; + popup.element.style.pointerEvents = 'none'; + popup.element.className = 'slash-command-popup'; + document.body.appendChild(popup.element); + }, + + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + char: '/', + allowSpaces: true, + pluginKey: new PluginKey(extensionName), + allow: ({ state, range }) => { + const $from = state.doc.resolve(range.from); + const afterContent = $from.parent.textContent?.substring( + $from.parent.textContent?.indexOf('/') + ); + const isValidAfterContent = !afterContent?.endsWith(' '); + + return isValidAfterContent; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + command: ({ editor, props }: { editor: Editor; props: any }) => { + const { view, state } = editor; + const { $head, $from } = view.state.selection; + + try { + const end = $from.pos; + const from = $head?.nodeBefore + ? end - + ($head.nodeBefore.text?.substring($head.nodeBefore.text?.indexOf('/')).length ?? + 0) + : $from.start(); + + const tr = state.tr.deleteRange(from, end); + view.dispatch(tr); + } catch (error) { + console.error(error); + } + + props.onClick(editor); + view.focus(); + }, + items: ({ query }: { query: string }) => { + const withFilteredCommands = GROUPS.map((group) => ({ + ...group, + commands: group.actions.filter((item) => { + const labelNormalized = item.tooltip!.toLowerCase().trim(); + const queryNormalized = query.toLowerCase().trim(); + return labelNormalized.includes(queryNormalized); + }) + })); + + const withoutEmptyGroups = withFilteredCommands.filter((group) => { + if (group.commands.length > 0) { + return true; + } + + return false; + }); + + const withEnabledSettings = withoutEmptyGroups.map((group) => ({ + ...group, + commands: group.commands.map((command) => ({ + ...command, + isEnabled: true + })) + })); + + return withEnabledSettings; + }, + render: () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let component: any; + + let scrollHandler: (() => void) | null = null; + + return { + onStart: (props: SuggestionProps) => { + component = new SvelteRenderer(menuList, { + props, + editor: props.editor + }); + + const { view } = props.editor; + + if (popup.element) { + popup.element.appendChild(component.element); + popup.element.style.visibility = 'visible'; + popup.element.style.pointerEvents = 'auto'; + popup.isVisible = true; + + const updatePosition = () => { + if (!popup.element || !props.clientRect) return; + + const rect = props.clientRect(); + if (!rect) return; + + const referenceElement = { + getBoundingClientRect: () => rect + }; + + computePosition(referenceElement, popup.element, { + placement: 'bottom-start' as Placement, + middleware: [ + offset({ mainAxis: 8, crossAxis: 16 }), + flip({ fallbackPlacements: ['top-start', 'bottom-start'] }) + ] + }).then(({ x, y }) => { + if (popup.element) { + popup.element.style.left = `${x}px`; + popup.element.style.top = `${y}px`; + } + }); + }; + + updatePosition(); + + // Set up auto-update for scroll events + if (props.clientRect) { + const referenceElement = { + getBoundingClientRect: () => props.clientRect?.() || new DOMRect() + }; + popup.cleanup = autoUpdate(referenceElement, popup.element, updatePosition); + } + + scrollHandler = updatePosition; + view.dom.parentElement?.addEventListener('scroll', scrollHandler); + } + }, + + onUpdate(props: SuggestionProps) { + component.updateProps(props); + + if (popup.element && popup.isVisible && props.clientRect) { + const rect = props.clientRect(); + if (rect) { + const referenceElement = { + getBoundingClientRect: () => rect + }; + + computePosition(referenceElement, popup.element, { + placement: 'bottom-start' as Placement, + middleware: [ + offset({ mainAxis: 8, crossAxis: 16 }), + flip({ fallbackPlacements: ['top-start', 'bottom-start'] }) + ] + }).then(({ x, y }) => { + if (popup.element) { + popup.element.style.left = `${x}px`; + popup.element.style.top = `${y}px`; + } + }); + + props.editor.storage[extensionName].rect = rect; + } + } + }, + + onKeyDown(props: SuggestionKeyDownProps) { + if (props.event.key === 'Escape') { + if (popup.element) { + popup.element.style.visibility = 'hidden'; + popup.element.style.pointerEvents = 'none'; + popup.isVisible = false; + } + return true; + } + + if (!popup.isVisible && popup.element) { + popup.element.style.visibility = 'visible'; + popup.element.style.pointerEvents = 'auto'; + popup.isVisible = true; + } + + if (props.event.key === 'Enter') return true; + + // return component.ref?.onKeyDown(props); + return false; + }, + + onExit(props) { + if (popup.element) { + popup.element.style.visibility = 'hidden'; + popup.element.style.pointerEvents = 'none'; + popup.element.innerHTML = ''; + popup.isVisible = false; + } + + if (popup.cleanup) { + popup.cleanup(); + popup.cleanup = null; + } + + if (scrollHandler) { + const { view } = props.editor; + view.dom.parentElement?.removeEventListener('scroll', scrollHandler); + scrollHandler = null; + } + + component.destroy(); + } + }; + } + }) + ]; + }, + + addStorage() { + return { + rect: { + width: 0, + height: 0, + left: 0, + top: 0, + right: 0, + bottom: 0 + } + }; + } + }); diff --git a/src/lib/components/edra/extensions/table/index.ts b/src/lib/components/edra/extensions/table/index.ts new file mode 100644 index 00000000..9ad12d62 --- /dev/null +++ b/src/lib/components/edra/extensions/table/index.ts @@ -0,0 +1,4 @@ +export { Table } from './table.js'; +export { TableCell } from './table-cell.js'; +export { TableRow } from './table-row.js'; +export { TableHeader } from './table-header.js'; diff --git a/src/lib/components/edra/extensions/table/table-cell.ts b/src/lib/components/edra/extensions/table/table-cell.ts new file mode 100644 index 00000000..6ff3fc9a --- /dev/null +++ b/src/lib/components/edra/extensions/table/table-cell.ts @@ -0,0 +1,124 @@ +import { mergeAttributes, Node } from '@tiptap/core'; +import { Plugin } from '@tiptap/pm/state'; +import { Decoration, DecorationSet } from '@tiptap/pm/view'; + +import { getCellsInColumn, isRowSelected, selectRow } from './utils.js'; + +export interface TableCellOptions { + HTMLAttributes: Record; +} + +export const TableCell = Node.create({ + name: 'tableCell', + + content: 'block+', + tableRole: 'cell', + + isolating: true, + + addOptions() { + return { + HTMLAttributes: {} + }; + }, + + parseHTML() { + return [{ tag: 'td' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['td', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + }, + + addAttributes() { + return { + colspan: { + default: 1, + parseHTML: (element) => { + const colspan = element.getAttribute('colspan'); + const value = colspan ? parseInt(colspan, 10) : 1; + + return value; + } + }, + rowspan: { + default: 1, + parseHTML: (element) => { + const rowspan = element.getAttribute('rowspan'); + const value = rowspan ? parseInt(rowspan, 10) : 1; + + return value; + } + }, + colwidth: { + default: null, + parseHTML: (element) => { + const colwidth = element.getAttribute('colwidth'); + const value = colwidth ? [parseInt(colwidth, 10)] : null; + + return value; + } + }, + style: { + default: null + } + }; + }, + + addProseMirrorPlugins() { + const { isEditable } = this.editor; + + return [ + new Plugin({ + props: { + decorations: (state) => { + if (!isEditable) { + return DecorationSet.empty; + } + + const { doc, selection } = state; + const decorations: Decoration[] = []; + const cells = getCellsInColumn(0)(selection); + + if (cells) { + cells.forEach(({ pos }: { pos: number }, index: number) => { + decorations.push( + Decoration.widget(pos + 1, () => { + const rowSelected = isRowSelected(index)(selection); + let className = 'grip-row'; + + if (rowSelected) { + className += ' selected'; + } + + if (index === 0) { + className += ' first'; + } + + if (index === cells.length - 1) { + className += ' last'; + } + + const grip = document.createElement('a'); + + grip.className = className; + grip.addEventListener('mousedown', (event) => { + event.preventDefault(); + event.stopImmediatePropagation(); + + this.editor.view.dispatch(selectRow(index)(this.editor.state.tr)); + }); + + return grip; + }) + ); + }); + } + + return DecorationSet.create(doc, decorations); + } + } + }) + ]; + } +}); diff --git a/src/lib/components/edra/extensions/table/table-header.ts b/src/lib/components/edra/extensions/table/table-header.ts new file mode 100644 index 00000000..fe35a1b6 --- /dev/null +++ b/src/lib/components/edra/extensions/table/table-header.ts @@ -0,0 +1,89 @@ +import { TableHeader as TiptapTableHeader } from '@tiptap/extension-table'; +import { Plugin } from '@tiptap/pm/state'; +import { Decoration, DecorationSet } from '@tiptap/pm/view'; + +import { getCellsInRow, isColumnSelected, selectColumn } from './utils.js'; + +export const TableHeader = TiptapTableHeader.extend({ + addAttributes() { + return { + colspan: { + default: 1 + }, + rowspan: { + default: 1 + }, + colwidth: { + default: null, + parseHTML: (element) => { + const colwidth = element.getAttribute('colwidth'); + const value = colwidth ? colwidth.split(',').map((item) => parseInt(item, 10)) : null; + + return value; + } + }, + style: { + default: null + } + }; + }, + + addProseMirrorPlugins() { + const { isEditable } = this.editor; + + return [ + new Plugin({ + props: { + decorations: (state) => { + if (!isEditable) { + return DecorationSet.empty; + } + + const { doc, selection } = state; + const decorations: Decoration[] = []; + const cells = getCellsInRow(0)(selection); + + if (cells) { + cells.forEach(({ pos }: { pos: number }, index: number) => { + decorations.push( + Decoration.widget(pos + 1, () => { + const colSelected = isColumnSelected(index)(selection); + let className = 'grip-column'; + + if (colSelected) { + className += ' selected'; + } + + if (index === 0) { + className += ' first'; + } + + if (index === cells.length - 1) { + className += ' last'; + } + + const grip = document.createElement('a'); + + grip.className = className; + grip.addEventListener('mousedown', (event) => { + event.preventDefault(); + event.stopImmediatePropagation(); + + this.editor.view.dispatch(selectColumn(index)(this.editor.state.tr)); + }); + + return grip; + }) + ); + }); + } + + return DecorationSet.create(doc, decorations); + } + } + }) + ]; + } +}); + +export default TableHeader; diff --git a/src/lib/components/edra/extensions/table/table-row.ts b/src/lib/components/edra/extensions/table/table-row.ts new file mode 100644 index 00000000..63cc1e1d --- /dev/null +++ b/src/lib/components/edra/extensions/table/table-row.ts @@ -0,0 +1,8 @@ +import { TableRow as TiptapTableRow } from '@tiptap/extension-table'; + +export const TableRow = TiptapTableRow.extend({ + allowGapCursor: false, + content: '(tableCell | tableHeader)*' +}); + +export default TableRow; diff --git a/src/lib/components/edra/extensions/table/table.ts b/src/lib/components/edra/extensions/table/table.ts new file mode 100644 index 00000000..e5f825df --- /dev/null +++ b/src/lib/components/edra/extensions/table/table.ts @@ -0,0 +1,9 @@ +import { Table as TiptapTable } from '@tiptap/extension-table'; + +export const Table = TiptapTable.configure({ + resizable: true, + lastColumnResizable: true, + allowTableNodeSelection: true +}); + +export default Table; diff --git a/src/lib/components/edra/extensions/table/utils.ts b/src/lib/components/edra/extensions/table/utils.ts new file mode 100644 index 00000000..d20a0e0f --- /dev/null +++ b/src/lib/components/edra/extensions/table/utils.ts @@ -0,0 +1,322 @@ +import { Editor, findParentNode } from '@tiptap/core'; +import { EditorState, Selection, Transaction } from '@tiptap/pm/state'; +import { CellSelection, type Rect, TableMap } from '@tiptap/pm/tables'; +import { Node, ResolvedPos } from '@tiptap/pm/model'; +import type { EditorView } from '@tiptap/pm/view'; +import Table from './table.js'; + +export const isRectSelected = (rect: Rect) => (selection: CellSelection) => { + const map = TableMap.get(selection.$anchorCell.node(-1)); + const start = selection.$anchorCell.start(-1); + const cells = map.cellsInRect(rect); + const selectedCells = map.cellsInRect( + map.rectBetween(selection.$anchorCell.pos - start, selection.$headCell.pos - start) + ); + + for (let i = 0, count = cells.length; i < count; i += 1) { + if (selectedCells.indexOf(cells[i]) === -1) { + return false; + } + } + + return true; +}; + +export const findTable = (selection: Selection) => + findParentNode((node) => node.type.spec.tableRole && node.type.spec.tableRole === 'table')( + selection + ); + +export const isCellSelection = (selection: Selection): selection is CellSelection => + selection instanceof CellSelection; + +export const isColumnSelected = (columnIndex: number) => (selection: Selection) => { + if (isCellSelection(selection)) { + const map = TableMap.get(selection.$anchorCell.node(-1)); + + return isRectSelected({ + left: columnIndex, + right: columnIndex + 1, + top: 0, + bottom: map.height + })(selection); + } + + return false; +}; + +export const isRowSelected = (rowIndex: number) => (selection: Selection) => { + if (isCellSelection(selection)) { + const map = TableMap.get(selection.$anchorCell.node(-1)); + + return isRectSelected({ + left: 0, + right: map.width, + top: rowIndex, + bottom: rowIndex + 1 + })(selection); + } + + return false; +}; + +export const isTableSelected = (selection: Selection) => { + if (isCellSelection(selection)) { + const map = TableMap.get(selection.$anchorCell.node(-1)); + + return isRectSelected({ + left: 0, + right: map.width, + top: 0, + bottom: map.height + })(selection); + } + + return false; +}; + +export const getCellsInColumn = (columnIndex: number | number[]) => (selection: Selection) => { + const table = findTable(selection); + if (table) { + const map = TableMap.get(table.node); + const indexes = Array.isArray(columnIndex) ? columnIndex : Array.from([columnIndex]); + + return indexes.reduce( + (acc, index) => { + if (index >= 0 && index <= map.width - 1) { + const cells = map.cellsInRect({ + left: index, + right: index + 1, + top: 0, + bottom: map.height + }); + + return acc.concat( + cells.map((nodePos) => { + const node = table.node.nodeAt(nodePos); + const pos = nodePos + table.start; + + return { pos, start: pos + 1, node }; + }) + ); + } + + return acc; + }, + [] as { pos: number; start: number; node: Node | null | undefined }[] + ); + } + return null; +}; + +export const getCellsInRow = (rowIndex: number | number[]) => (selection: Selection) => { + const table = findTable(selection); + + if (table) { + const map = TableMap.get(table.node); + const indexes = Array.isArray(rowIndex) ? rowIndex : Array.from([rowIndex]); + + return indexes.reduce( + (acc, index) => { + if (index >= 0 && index <= map.height - 1) { + const cells = map.cellsInRect({ + left: 0, + right: map.width, + top: index, + bottom: index + 1 + }); + + return acc.concat( + cells.map((nodePos) => { + const node = table.node.nodeAt(nodePos); + const pos = nodePos + table.start; + return { pos, start: pos + 1, node }; + }) + ); + } + + return acc; + }, + [] as { pos: number; start: number; node: Node | null | undefined }[] + ); + } + + return null; +}; + +export const getCellsInTable = (selection: Selection) => { + const table = findTable(selection); + + if (table) { + const map = TableMap.get(table.node); + const cells = map.cellsInRect({ + left: 0, + right: map.width, + top: 0, + bottom: map.height + }); + + return cells.map((nodePos) => { + const node = table.node.nodeAt(nodePos); + const pos = nodePos + table.start; + + return { pos, start: pos + 1, node }; + }); + } + + return null; +}; + +export const findParentNodeClosestToPos = ( + $pos: ResolvedPos, + predicate: (node: Node) => boolean +) => { + for (let i = $pos.depth; i > 0; i -= 1) { + const node = $pos.node(i); + + if (predicate(node)) { + return { + pos: i > 0 ? $pos.before(i) : 0, + start: $pos.start(i), + depth: i, + node + }; + } + } + + return null; +}; + +export const findCellClosestToPos = ($pos: ResolvedPos) => { + const predicate = (node: Node) => + node.type.spec.tableRole && /cell/i.test(node.type.spec.tableRole); + + return findParentNodeClosestToPos($pos, predicate); +}; + +const select = (type: 'row' | 'column') => (index: number) => (tr: Transaction) => { + const table = findTable(tr.selection); + const isRowSelection = type === 'row'; + + if (table) { + const map = TableMap.get(table.node); + + // Check if the index is valid + if (index >= 0 && index < (isRowSelection ? map.height : map.width)) { + const left = isRowSelection ? 0 : index; + const top = isRowSelection ? index : 0; + const right = isRowSelection ? map.width : index + 1; + const bottom = isRowSelection ? index + 1 : map.height; + + const cellsInFirstRow = map.cellsInRect({ + left, + top, + right: isRowSelection ? right : left + 1, + bottom: isRowSelection ? top + 1 : bottom + }); + + const cellsInLastRow = + bottom - top === 1 + ? cellsInFirstRow + : map.cellsInRect({ + left: isRowSelection ? left : right - 1, + top: isRowSelection ? bottom - 1 : top, + right, + bottom + }); + + const head = table.start + cellsInFirstRow[0]; + const anchor = table.start + cellsInLastRow[cellsInLastRow.length - 1]; + const $head = tr.doc.resolve(head); + const $anchor = tr.doc.resolve(anchor); + + return tr.setSelection(new CellSelection($anchor, $head)); + } + } + return tr; +}; + +export const selectColumn = select('column'); + +export const selectRow = select('row'); + +export const selectTable = (tr: Transaction) => { + const table = findTable(tr.selection); + + if (table) { + const { map } = TableMap.get(table.node); + + if (map && map.length) { + const head = table.start + map[0]; + const anchor = table.start + map[map.length - 1]; + const $head = tr.doc.resolve(head); + const $anchor = tr.doc.resolve(anchor); + + return tr.setSelection(new CellSelection($anchor, $head)); + } + } + + return tr; +}; + +export const isColumnGripSelected = ({ + editor, + view, + state, + from +}: { + editor: Editor; + view: EditorView; + state: EditorState; + from: number; +}) => { + const domAtPos = view.domAtPos(from).node as HTMLElement; + const nodeDOM = view.nodeDOM(from) as HTMLElement; + const node = nodeDOM || domAtPos; + + if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) { + return false; + } + + let container = node; + + while (container && !['TD', 'TH'].includes(container.tagName)) { + container = container.parentElement!; + } + + const gripColumn = + container && container.querySelector && container.querySelector('a.grip-column.selected'); + + return !!gripColumn; +}; + +export const isRowGripSelected = ({ + editor, + view, + state, + from +}: { + editor: Editor; + view: EditorView; + state: EditorState; + from: number; +}) => { + const domAtPos = view.domAtPos(from).node as HTMLElement; + const nodeDOM = view.nodeDOM(from) as HTMLElement; + const node = nodeDOM || domAtPos; + + if (!editor.isActive(Table.name) || !node || isTableSelected(state.selection)) { + return false; + } + + let container = node; + + while (container && !['TD', 'TH'].includes(container.tagName)) { + container = container.parentElement!; + } + + const gripRow = + container && container.querySelector && container.querySelector('a.grip-row.selected'); + + return !!gripRow; +}; diff --git a/src/lib/components/edra/extensions/video/VideoExtended.ts b/src/lib/components/edra/extensions/video/VideoExtended.ts new file mode 100644 index 00000000..6a220c7e --- /dev/null +++ b/src/lib/components/edra/extensions/video/VideoExtended.ts @@ -0,0 +1,34 @@ +import { SvelteNodeViewRenderer } from 'svelte-tiptap'; +import { Video } from './VideoExtension.js'; +import type { NodeViewProps } from '@tiptap/core'; +import type { Component } from 'svelte'; + +export const VideoExtended = (content: Component) => + Video.extend({ + addAttributes() { + return { + src: { + default: null + }, + alt: { + default: null + }, + title: { + default: null + }, + width: { + default: '100%' + }, + height: { + default: null + }, + align: { + default: 'left' + } + }; + }, + + addNodeView: () => { + return SvelteNodeViewRenderer(content); + } + }); diff --git a/src/lib/components/edra/extensions/video/VideoExtension.ts b/src/lib/components/edra/extensions/video/VideoExtension.ts new file mode 100644 index 00000000..f3b9b2b7 --- /dev/null +++ b/src/lib/components/edra/extensions/video/VideoExtension.ts @@ -0,0 +1,147 @@ +import { Node, nodeInputRule } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +export interface VideoOptions { + HTMLAttributes: Record; +} + +declare module '@tiptap/core' { + interface Commands { + video: { + /** + * Set a video node + */ + setVideo: (src: string) => ReturnType; + /** + * Toggle a video + */ + toggleVideo: (src: string) => ReturnType; + /** + * Remove a video + */ + removeVideo: () => ReturnType; + }; + } +} + +const VIDEO_INPUT_REGEX = /!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\)/; + +export const Video = Node.create({ + name: 'video', + group: 'block', + content: 'inline*', + draggable: true, + isolating: true, + addOptions() { + return { + HTMLAttributes: {} + }; + }, + addAttributes() { + return { + src: { + default: null, + parseHTML: (el) => (el as HTMLSpanElement).getAttribute('src'), + renderHTML: (attrs) => ({ src: attrs.src }) + } + }; + }, + parseHTML() { + return [ + { + tag: 'video', + getAttrs: (el) => ({ src: (el as HTMLVideoElement).getAttribute('src') }) + } + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'video', + { controls: 'true', style: 'width: fit-content;', ...HTMLAttributes }, + ['source', HTMLAttributes] + ]; + }, + addCommands() { + return { + setVideo: + (src: string) => + ({ commands }) => + commands.insertContent( + `