From 0f72f6656d8c0ac4485c35dd87b4d595edfd8932 Mon Sep 17 00:00:00 2001 From: Habiba Date: Thu, 5 Jun 2025 14:12:43 +0300 Subject: [PATCH 01/26] config: add tiptap dependency --- package.json | 3 + pnpm-lock.yaml | 556 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 559 insertions(+) diff --git a/package.json b/package.json index 7d9483a47c..93260864df 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,10 @@ "homepage": "https://github.com/learningequality/studio#readme", "dependencies": { "@sentry/vue": "^7.112.2", + "@tiptap/starter-kit": "^2.13.0", + "@tiptap/vue-2": "^2.13.0", "@toast-ui/editor": "^2.3.1", + "add": "^2.0.6", "ajv": "^8.12.0", "axios": "^1.8.2", "broadcast-channel": "^5.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77275ed0a2..8069d31ff8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,18 @@ importers: '@sentry/vue': specifier: ^7.112.2 version: 7.120.3(vue@2.7.16) + '@tiptap/starter-kit': + specifier: ^2.13.0 + version: 2.13.0 + '@tiptap/vue-2': + specifier: ^2.13.0 + version: 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0)(vue@2.7.16) '@toast-ui/editor': specifier: ^2.3.1 version: 2.5.4 + add: + specifier: ^2.0.6 + version: 2.0.6 ajv: specifier: ^8.12.0 version: 8.17.1 @@ -1261,6 +1270,12 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rollup/plugin-babel@5.3.1': resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==} engines: {node: '>= 10.0.0'} @@ -1391,6 +1406,136 @@ packages: vue: ^2.6.10 vue-template-compiler: ^2.6.10 + '@tiptap/core@2.13.0': + resolution: {integrity: sha512-VDwlf5+DznkrAT+vlggl9t/mPVeo3ayi4irXgLPkDfHAY8adVIx+RCek6GBChclJ8q2Iy0HZwpIYs/8L7tadqA==} + peerDependencies: + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-blockquote@2.13.0': + resolution: {integrity: sha512-DQjd8AUG9TE+ReFp05T9N5Z+o4dw88j+O6xgPMjs1T1u9yHUMxwjvidPRi5QqCqPiwDJcqm3bjYxebPPMHbi9w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bold@2.13.0': + resolution: {integrity: sha512-q/Kqo1HXas+dUevP/Qice+nbxXue8ZpmYBniw9zt/JHbgwH1b6Rw7lIjLxYerdaPWj305h9ZHxLqmzDOEcQRPw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bubble-menu@2.13.0': + resolution: {integrity: sha512-y2PRg7YT8Km1e4+xEvXcKTPfEu/i44eKNjbsKojgs70kuONdhFmhWIXCeGEVAwPH8ZPH+JPam5kcW2vsihoayg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-bullet-list@2.13.0': + resolution: {integrity: sha512-7XMNtRFCC+Co7FrwyU5gUC+4i3k83kQb1KJcYjq6Deud/2DoXNJeRcacqXr+mNePm+W71xZJ7W7ePWLlHrfHPw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-code-block@2.13.0': + resolution: {integrity: sha512-HGcWmwKx4D53HY4XO0ve6lNHirpWdd91AVGVPuIkyG0wSvAzBgXy4TIgyUrHxGRE4qvNAe685RCYIA/VMBJAyg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-code@2.13.0': + resolution: {integrity: sha512-TrMhcRKsxmpZyHEstMdXAjwe+3bTqsSdiLsotzV7LwO8eCyeN42xfm2yzz9h/SJGUsO1X70YhoLH8liBNif97A==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-document@2.13.0': + resolution: {integrity: sha512-jZ9NRUtJ2g67XTFgfn/6hMz+N4XJ+kG0/4GeLU2B7ZMF3UdcMWTvgyLxUNXvuLgMbCATVFTDTZiYTXgB/nPAVw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-dropcursor@2.13.0': + resolution: {integrity: sha512-nx2PjBiuOb3vBUDKGSzHtfKcWJstaHGr4dx3C+TsJ3Z8qHl4tB9Ud+c7Uaz6/DptK52Q3nn2WAK2mp/njLvXuA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-floating-menu@2.13.0': + resolution: {integrity: sha512-gF14Nu61QUWWJDxOxzB679uK0W/rWcU7FTn1ll2zGt3NW2P2HheLo6qL1U5Wwxo3YwXloM8KLofdWi6vMN5RQQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-gapcursor@2.13.0': + resolution: {integrity: sha512-rOYVPa+mBksgGn+o9KGzKXhPNabSUVxvOB5BBwFhc0pTD+icoNk5awcUNfKx1VcBqXE3ghUvR/EEro9PvwTtdg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-hard-break@2.13.0': + resolution: {integrity: sha512-1n83Uq7r/x/vJecj46ZO/a+MrAVDYFofz2J1V+N42EindNXaf/c2I9dIR8yxIXH8NT211gd1MckRM43eSSXU0w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-heading@2.13.0': + resolution: {integrity: sha512-85G/DrrywpMQtZYwZJFwR+2ocRdvUM3FMLO4JVIZA7DyFsMD59lAixSSdBC2yZyr2Y8uzMW/UYyJdE+gpO6k7A==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-history@2.13.0': + resolution: {integrity: sha512-0gaCTHYrsECs8ZeG3oX3fyTyvKJHuGUuCnbEBfvGVxlYdhDVyuloe+G3s3UFeVVQJKO/P5OcxvUuAQ06s29tXw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-horizontal-rule@2.13.0': + resolution: {integrity: sha512-8gqYh2f7OAgEckIEoqsj98omJSPUOOWUvJRL50q70AUceMvPtVngWGKfITlDy4eYvRj5WXrNRSYrQrCU/8S8sg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-italic@2.13.0': + resolution: {integrity: sha512-eI5TtUgfvwIkj576pWWgHQjRItEe7IWG+t/ZyRDMo5eTxz5b6lqsJydexmJbdL/u25kMgtwd39v7QEu0OiPcVg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-list-item@2.13.0': + resolution: {integrity: sha512-EDq4Xm/dPjvEhdnaVJlsjV/qjdNtCCFAgO7eBzHvXP14b8feVmqJdDGs6sEZznAGxDe9gjRNsDj9EABEkQK8Dg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-ordered-list@2.13.0': + resolution: {integrity: sha512-krHS6lRf3R+m2eNpgHIfqI/A4CcbNoAWyQFH7HXkSuh0bgO7Aq1zi39clVcYfnI7eFGy07Lx8dQZ2kTLQ/n4Lg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-paragraph@2.13.0': + resolution: {integrity: sha512-icWUqD6j3VCIW8RTFoknBDSOwWruraJwSwfUKadEoplRSIz81J75cmoExscO8rHMD8juhIrKGbs5WD5WJajrpQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-strike@2.13.0': + resolution: {integrity: sha512-1j7ho8vuldtLuSHl4oQUNbVtub3P/mR8b+DbK+Fmp9UZrd5moHP9sD3yeIP4Ms/7iJhdDgLJPKAKNqmA2fsOtg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text-style@2.13.0': + resolution: {integrity: sha512-gUXzgBCYcY3lk5AJ9DvWolphNBw5b2TLbKyJ0ZBWDjdNL7nF87iKKcNBGAXh2bYugvSi7dlPeSv7OQELyBm8Ug==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text@2.13.0': + resolution: {integrity: sha512-4VhKCxfOlBXbaRAM4/OtZnOeq+WcjJoaduo4Jrb7w1I18yqknVjOwFH3Cv+2VqIjPI8bILMWzgpxvFPKdrOllA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/pm@2.13.0': + resolution: {integrity: sha512-CU2DEMQbYXwlnISFD0+7nQSBaqGKJHOiTnWpnAi5pOVphroEIbLPlT6AO4638MWtIR9QsOmGR8KXkQmNGA6tjQ==} + + '@tiptap/starter-kit@2.13.0': + resolution: {integrity: sha512-hik7WvHXIWWNfkSez+Lp50GNL1pWboWm6aiYNCF6B7AEaN6mB+unuI2F+6YpSQ2LwXEajmyF+xLUKA+hBg5ZxA==} + + '@tiptap/vue-2@2.13.0': + resolution: {integrity: sha512-g/qJXHClUVaKxKQhPUKkww/CcRl6zyxYRz64RBxJz78ErPku/tpfvyb/bn+xpoQzmYfDVh/zKv0r7qzk9MLseQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + vue: ^2.6.0 + '@toast-ui/editor@2.5.4': resolution: {integrity: sha512-XsuYlPQxhec9dHQREFAigjE4enHSuGMF7D0YQ6wW7phmusvAu0FnJfZUPjJBoU/GKz7WP5U6fKU9/P+8j65D8A==} @@ -1487,6 +1632,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/localforage@0.0.34': resolution: {integrity: sha512-tJxahnjm9dEI1X+hQSC5f2BSd/coZaqbIl1m3TCl0q9SVuC52XcXfV0XmoCU1+PmjyucuVITwoTnN8OlTbEXXA==} deprecated: This is a stub types definition for localforage (https://github.com/localForage/localForage). localforage provides its own type definitions, so you don't need @types/localforage installed! @@ -1494,6 +1642,12 @@ packages: '@types/lodash@4.17.15': resolution: {integrity: sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==} + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -1756,6 +1910,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + add@2.0.6: + resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==} + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -2646,6 +2803,9 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@6.0.6: resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} engines: {node: '>=4.8'} @@ -4622,6 +4782,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + load-bmfont@1.4.2: resolution: {integrity: sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==} @@ -4759,6 +4922,10 @@ packages: map-values@1.0.1: resolution: {integrity: sha512-BbShUnr5OartXJe1GeccAWtfro11hhgNJg6G9/UtWKjVGvV5U4C09cg5nk8JUevhXODaXY+hQ3xxMUKSs62ONQ==} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + marks-pane@1.0.9: resolution: {integrity: sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg==} @@ -4784,6 +4951,9 @@ packages: mdn-data@2.16.0: resolution: {integrity: sha512-FYRGKJUm9JD7JMUXwIi0dPkIozBqiSlz7Cx458FpEhTT3rQieI4sPL1LhY80FS3MIC6AJ/FWNJ2039GnIBJySg==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -5155,6 +5325,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -5707,6 +5880,64 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + prosemirror-changeset@2.3.1: + resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.7.1: + resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==} + + prosemirror-dropcursor@1.8.2: + resolution: {integrity: sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==} + + prosemirror-gapcursor@1.3.2: + resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + + prosemirror-history@1.4.1: + resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} + + prosemirror-inputrules@1.5.0: + resolution: {integrity: sha512-K0xJRCmt+uSw7xesnHmcn72yBGTbY45vm8gXI4LZXbx2Z0jwh5aF9xrGQgrVPu0WbyFVFF3E/o9VhJYz6SQWnA==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-markdown@1.13.2: + resolution: {integrity: sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==} + + prosemirror-menu@1.2.5: + resolution: {integrity: sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==} + + prosemirror-model@1.25.1: + resolution: {integrity: sha512-AUvbm7qqmpZa5d9fPKMvH1Q5bqYQvAZWOGRvxsB6iFLyycvC9MwNemNVjHVrWgjaoxAfY8XVg7DbvQ/qxvI9Eg==} + + prosemirror-schema-basic@1.2.4: + resolution: {integrity: sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + + prosemirror-tables@1.7.1: + resolution: {integrity: sha512-eRQ97Bf+i9Eby99QbyAiyov43iOKgWa7QCGly+lrDt7efZ1v8NWolhXiB43hSDGIXT1UXgbs4KJN3a06FGpr1Q==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.10.4: + resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} + + prosemirror-view@1.40.0: + resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -5723,6 +5954,10 @@ packages: psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -5982,6 +6217,9 @@ packages: engines: {node: '>=10.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -6684,6 +6922,9 @@ packages: tippy.js@4.3.5: resolution: {integrity: sha512-NDq3efte8nGK6BOJ1dDN1/WelAwfmh3UtIYXXck6+SxLzbIQNZE/cmRSnwScZ/FyiKdIcvFHvYUgqmoGx8CcyA==} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + tmp@0.2.3: resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} engines: {node: '>=14.14'} @@ -6842,6 +7083,9 @@ packages: resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -7056,6 +7300,11 @@ packages: vue-template-es2015-compiler@1.9.1: resolution: {integrity: sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==} + vue-ts-types@1.6.2: + resolution: {integrity: sha512-iOBe4oMoTeISs+Wxlt5E6q3W6lEX6UtkwWCPu6727IoweBE3O3Vp/2/BtO+Kf5pISQU7WpX71hXTeMGqHsmecA==} + peerDependencies: + vue: ^2.6 || ^3.2 + vue2-teleport@1.1.4: resolution: {integrity: sha512-mGTszyQP6k3sSSk7MBq+PZdVojHYLwg5772hl3UVpu5uaLBqWIZ5eNP6/TjkDrf1XUTTxybvpXC6inpjwO+i/Q==} @@ -7074,6 +7323,9 @@ packages: peerDependencies: vue: ^2.0.0 + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} @@ -8610,6 +8862,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@popperjs/core@2.11.8': {} + + '@remirror/core-constants@3.0.0': {} + '@rollup/plugin-babel@5.3.1(@babel/core@7.26.10)(@types/babel__core@7.20.5)(rollup@2.79.2)': dependencies: '@babel/core': 7.26.10 @@ -8777,6 +9033,157 @@ snapshots: vue: 2.7.16 vue-template-compiler: 2.7.16 + '@tiptap/core@2.13.0(@tiptap/pm@2.13.0)': + dependencies: + '@tiptap/pm': 2.13.0 + + '@tiptap/extension-blockquote@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-bold@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-bubble-menu@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0)': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/pm': 2.13.0 + tippy.js: 6.3.7 + + '@tiptap/extension-bullet-list@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-code-block@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0)': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/pm': 2.13.0 + + '@tiptap/extension-code@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-document@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-dropcursor@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0)': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/pm': 2.13.0 + + '@tiptap/extension-floating-menu@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0)': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/pm': 2.13.0 + tippy.js: 6.3.7 + + '@tiptap/extension-gapcursor@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0)': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/pm': 2.13.0 + + '@tiptap/extension-hard-break@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-heading@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-history@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0)': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/pm': 2.13.0 + + '@tiptap/extension-horizontal-rule@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0)': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/pm': 2.13.0 + + '@tiptap/extension-italic@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-list-item@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-ordered-list@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-paragraph@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-strike@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-text-style@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/extension-text@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + + '@tiptap/pm@2.13.0': + dependencies: + prosemirror-changeset: 2.3.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.7.1 + prosemirror-dropcursor: 1.8.2 + prosemirror-gapcursor: 1.3.2 + prosemirror-history: 1.4.1 + prosemirror-inputrules: 1.5.0 + prosemirror-keymap: 1.2.3 + prosemirror-markdown: 1.13.2 + prosemirror-menu: 1.2.5 + prosemirror-model: 1.25.1 + prosemirror-schema-basic: 1.2.4 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.3 + prosemirror-tables: 1.7.1 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0) + prosemirror-transform: 1.10.4 + prosemirror-view: 1.40.0 + + '@tiptap/starter-kit@2.13.0': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/extension-blockquote': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-bold': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-bullet-list': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-code': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-code-block': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0) + '@tiptap/extension-document': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-dropcursor': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0) + '@tiptap/extension-gapcursor': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0) + '@tiptap/extension-hard-break': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-heading': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-history': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0) + '@tiptap/extension-horizontal-rule': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0) + '@tiptap/extension-italic': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-list-item': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-ordered-list': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-paragraph': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-strike': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-text': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/extension-text-style': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) + '@tiptap/pm': 2.13.0 + + '@tiptap/vue-2@2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0)(vue@2.7.16)': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/extension-bubble-menu': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0) + '@tiptap/extension-floating-menu': 2.13.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))(@tiptap/pm@2.13.0) + '@tiptap/pm': 2.13.0 + vue: 2.7.16 + vue-ts-types: 1.6.2(vue@2.7.16) + '@toast-ui/editor@2.5.4': dependencies: '@types/codemirror': 0.0.71 @@ -8900,12 +9307,21 @@ snapshots: '@types/json5@0.0.29': {} + '@types/linkify-it@5.0.0': {} + '@types/localforage@0.0.34': dependencies: localforage: 1.10.0 '@types/lodash@4.17.15': {} + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + '@types/mime@1.3.5': {} '@types/minimist@1.2.5': {} @@ -9266,6 +9682,8 @@ snapshots: acorn@8.14.1: {} + add@2.0.6: {} + agent-base@6.0.2: dependencies: debug: 4.4.0 @@ -10126,6 +10544,8 @@ snapshots: - supports-color - ts-node + crelt@1.0.6: {} + cross-spawn@6.0.6: dependencies: nice-try: 1.0.5 @@ -12778,6 +13198,10 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + load-bmfont@1.4.2: dependencies: buffer-equal: 0.0.1 @@ -12949,6 +13373,15 @@ snapshots: map-values@1.0.1: {} + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + marks-pane@1.0.9: {} material-icons@0.3.1: {} @@ -12965,6 +13398,8 @@ snapshots: mdn-data@2.16.0: {} + mdurl@2.0.0: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -13381,6 +13816,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + orderedmap@2.1.1: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -13854,6 +14291,109 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 + prosemirror-changeset@2.3.1: + dependencies: + prosemirror-transform: 1.10.4 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.3 + + prosemirror-commands@1.7.1: + dependencies: + prosemirror-model: 1.25.1 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + prosemirror-dropcursor@1.8.2: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.40.0 + + prosemirror-gapcursor@1.3.2: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.1 + prosemirror-state: 1.4.3 + prosemirror-view: 1.40.0 + + prosemirror-history@1.4.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.40.0 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.0: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.3 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.2: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.25.1 + + prosemirror-menu@1.2.5: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.7.1 + prosemirror-history: 1.4.1 + prosemirror-state: 1.4.3 + + prosemirror-model@1.25.1: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.4: + dependencies: + prosemirror-model: 1.25.1 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.1 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + + prosemirror-state@1.4.3: + dependencies: + prosemirror-model: 1.25.1 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.40.0 + + prosemirror-tables@1.7.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.1 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + prosemirror-view: 1.40.0 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.25.1 + prosemirror-state: 1.4.3 + prosemirror-view: 1.40.0 + + prosemirror-transform@1.10.4: + dependencies: + prosemirror-model: 1.25.1 + + prosemirror-view@1.40.0: + dependencies: + prosemirror-model: 1.25.1 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.4 + proto-list@1.2.4: {} proxy-addr@2.0.7: @@ -13869,6 +14409,8 @@ snapshots: dependencies: punycode: 2.3.1 + punycode.js@2.3.1: {} + punycode@1.4.1: {} punycode@2.3.1: {} @@ -14168,6 +14710,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + rope-sequence@1.3.4: {} + router@2.2.0: dependencies: debug: 4.4.0 @@ -14998,6 +15542,10 @@ snapshots: dependencies: popper.js: 1.16.1 + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + tmp@0.2.3: {} tmpl@1.0.5: {} @@ -15149,6 +15697,8 @@ snapshots: ua-parser-js@1.0.40: {} + uc.micro@2.1.0: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.3 @@ -15412,6 +15962,10 @@ snapshots: vue-template-es2015-compiler@1.9.1: {} + vue-ts-types@1.6.2(vue@2.7.16): + dependencies: + vue: 2.7.16 + vue2-teleport@1.1.4: {} vue@2.7.16: @@ -15427,6 +15981,8 @@ snapshots: dependencies: vue: 2.7.16 + w3c-keyname@2.2.8: {} + w3c-xmlserializer@4.0.0: dependencies: xml-name-validator: 4.0.0 From 34257da60aa94af99d9f7a8be75b7ca11c9133a0 Mon Sep 17 00:00:00 2001 From: Habiba Date: Sat, 7 Jun 2025 01:32:20 +0300 Subject: [PATCH 02/26] feat(texteditor): implement static pre tiptap logic editor component --- .../TipTapEditor/TipTapEditor.vue | 573 ++++++++++++++++++ .../views/TipTapEditor/assets/icon-bold.svg | 1 + .../TipTapEditor/assets/icon-bulletList.svg | 3 + .../TipTapEditor/assets/icon-chevron-down.svg | 1 + .../TipTapEditor/assets/icon-codeblock.svg | 3 + .../views/TipTapEditor/assets/icon-copy.svg | 1 + .../TipTapEditor/assets/icon-formula.svg | 3 + .../TipTapEditor/assets/icon-insertImage.svg | 3 + .../views/TipTapEditor/assets/icon-italic.svg | 1 + .../views/TipTapEditor/assets/icon-link.svg | 3 + .../TipTapEditor/assets/icon-numberList.svg | 3 + .../views/TipTapEditor/assets/icon-paste.svg | 1 + .../assets/icon-pasteNoFormat.svg | 3 + .../views/TipTapEditor/assets/icon-redo.svg | 3 + .../assets/icon-strikethrough.svg | 1 + .../TipTapEditor/assets/icon-subscript.svg | 3 + .../TipTapEditor/assets/icon-superscript.svg | 3 + .../TipTapEditor/assets/icon-underline.svg | 1 + .../views/TipTapEditor/assets/icon-undo.svg | 3 + 19 files changed, 613 insertions(+) create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bold.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bulletList.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-chevron-down.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-codeblock.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-copy.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-formula.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-insertImage.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-italic.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-link.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-numberList.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-paste.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-pasteNoFormat.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-redo.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-strikethrough.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-subscript.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-superscript.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-underline.svg create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-undo.svg diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue new file mode 100644 index 0000000000..f11f58d38f --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue @@ -0,0 +1,573 @@ + + + + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bold.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bold.svg new file mode 100644 index 0000000000..974567707d --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bulletList.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bulletList.svg new file mode 100644 index 0000000000..15e17847a7 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bulletList.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-chevron-down.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-chevron-down.svg new file mode 100644 index 0000000000..85b191bfb3 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-codeblock.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-codeblock.svg new file mode 100644 index 0000000000..f9d48830b8 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-codeblock.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-copy.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-copy.svg new file mode 100644 index 0000000000..d5784eefc7 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-formula.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-formula.svg new file mode 100644 index 0000000000..ca6c8f2f15 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-formula.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-insertImage.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-insertImage.svg new file mode 100644 index 0000000000..02ba5b9f53 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-insertImage.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-italic.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-italic.svg new file mode 100644 index 0000000000..95b95c189e --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-italic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-link.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-link.svg new file mode 100644 index 0000000000..3601f03dd7 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-link.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-numberList.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-numberList.svg new file mode 100644 index 0000000000..7a284893bc --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-numberList.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-paste.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-paste.svg new file mode 100644 index 0000000000..963d72c8b4 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-paste.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-pasteNoFormat.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-pasteNoFormat.svg new file mode 100644 index 0000000000..817a5d55d2 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-pasteNoFormat.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-redo.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-redo.svg new file mode 100644 index 0000000000..ab910de246 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-redo.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-strikethrough.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-strikethrough.svg new file mode 100644 index 0000000000..dfdfef72f6 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-strikethrough.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-subscript.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-subscript.svg new file mode 100644 index 0000000000..c615beaefa --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-subscript.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-superscript.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-superscript.svg new file mode 100644 index 0000000000..733c653c0a --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-superscript.svg @@ -0,0 +1,3 @@ + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-underline.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-underline.svg new file mode 100644 index 0000000000..c50954e85d --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-underline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-undo.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-undo.svg new file mode 100644 index 0000000000..08539a0467 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-undo.svg @@ -0,0 +1,3 @@ + + + From d9a6d37fe07dea733c4c20a6c3f5f1b7453204d3 Mon Sep 17 00:00:00 2001 From: Habiba Date: Sun, 8 Jun 2025 06:53:55 +0300 Subject: [PATCH 03/26] refactor(texteditor): separated tiptap editor to multiple components --- .../TipTapEditor/TipTapEditor.vue | 528 +----------------- .../TipTapEditor/components/EditorContent.vue | 27 + .../TipTapEditor/components/EditorToolbar.vue | 142 +++++ .../components/toolbar/FormatDropdown.vue | 119 ++++ .../components/toolbar/PasteDropdown.vue | 186 ++++++ .../components/toolbar/ToolbarButton.vue | 72 +++ .../components/toolbar/ToolbarDivider.vue | 20 + .../components/toolbar/ToolbarGroup.vue | 22 + .../TipTapEditor/composables/useDropdowns.js | 63 +++ .../TipTapEditor/composables/useEditor.js | 36 ++ .../composables/useToolbarActions.js | 201 +++++++ 11 files changed, 896 insertions(+), 520 deletions(-) create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContent.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarDivider.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarGroup.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue index f11f58d38f..c9f90f9b08 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue @@ -1,360 +1,25 @@ \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue new file mode 100644 index 0000000000..45211ba339 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue @@ -0,0 +1,142 @@ + + + + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue new file mode 100644 index 0000000000..33b2818ad4 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue @@ -0,0 +1,119 @@ + + + + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue new file mode 100644 index 0000000000..d70b44ad89 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue @@ -0,0 +1,186 @@ + + + + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue new file mode 100644 index 0000000000..712c9107da --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue @@ -0,0 +1,72 @@ + + + + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarDivider.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarDivider.vue new file mode 100644 index 0000000000..2ec0056317 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarDivider.vue @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarGroup.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarGroup.vue new file mode 100644 index 0000000000..9acd0f9622 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarGroup.vue @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js new file mode 100644 index 0000000000..bc27809e03 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js @@ -0,0 +1,63 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +export function useDropdowns() { + const selectedFormat = ref('Normal') + const showHeadersDropdown = ref(false) + const showPasteDropdown = ref(false) + + // Dropdown management + const toggleHeadersDropdown = () => { + showHeadersDropdown.value = !showHeadersDropdown.value + showPasteDropdown.value = false + } + + const togglePasteDropdown = () => { + showPasteDropdown.value = !showPasteDropdown.value + showHeadersDropdown.value = false + } + + const closeAllDropdowns = () => { + showHeadersDropdown.value = false + showPasteDropdown.value = false + } + + const selectFormat = (format) => { + selectedFormat.value = format.label + closeAllDropdowns() + // TipTap formatting logic will be added here + console.log('Format selected:', format) + } + + const handleClickOutside = (event) => { + const dropdownContainers = document.querySelectorAll('.dropdown-container') + let isOutside = true + + dropdownContainers.forEach(container => { + if (container.contains(event.target)) { + isOutside = false + } + }) + + if (isOutside) { + closeAllDropdowns() + } + } + + onMounted(() => { + document.addEventListener('click', handleClickOutside) + }) + + onUnmounted(() => { + document.removeEventListener('click', handleClickOutside) + }) + + return { + selectedFormat, + showHeadersDropdown, + showPasteDropdown, + toggleHeadersDropdown, + togglePasteDropdown, + closeAllDropdowns, + selectFormat + } +} \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js new file mode 100644 index 0000000000..ff91d04bce --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js @@ -0,0 +1,36 @@ +import { ref, onMounted, onUnmounted } from 'vue' + +export function useEditor() { + const editor = ref(null) + const isReady = ref(false) + + const initializeEditor = () => { + // TipTap editor initialization will be added here + console.log('Editor initialization placeholder') + isReady.value = true + } + + const destroyEditor = () => { + if (editor.value) { + // TipTap editor cleanup will be added here + console.log('Editor cleanup placeholder') + editor.value = null + isReady.value = false + } + } + + onMounted(() => { + initializeEditor() + }) + + onUnmounted(() => { + destroyEditor() + }) + + return { + editor, + isReady, + initializeEditor, + destroyEditor + } +} \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js new file mode 100644 index 0000000000..84ff17f5e7 --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js @@ -0,0 +1,201 @@ +import { computed } from 'vue' + +export function useToolbarActions() { + // Action handlers + const handleUndo = () => { + // TipTap undo logic will be added here + console.log('Undo action') + } + + const handleRedo = () => { + // TipTap redo logic will be added here + console.log('Redo action') + } + + const handleBold = () => { + // TipTap bold logic will be added here + console.log('Bold action') + } + + const handleItalic = () => { + // TipTap italic logic will be added here + console.log('Italic action') + } + + const handleUnderline = () => { + // TipTap underline logic will be added here + console.log('Underline action') + } + + const handleStrikethrough = () => { + // TipTap strikethrough logic will be added here + console.log('Strikethrough action') + } + + const handleCopy = () => { + // TipTap copy logic will be added here + console.log('Copy action') + } + + const handlePaste = () => { + // TipTap paste logic will be added here + console.log('Paste action') + } + + const handlePasteNoFormat = () => { + // TipTap paste without formatting logic will be added here + console.log('Paste without formatting action') + } + + const handleBulletList = () => { + // TipTap bullet list logic will be added here + console.log('Bullet list action') + } + + const handleNumberList = () => { + // TipTap numbered list logic will be added here + console.log('Number list action') + } + + const handleSubscript = () => { + // TipTap subscript logic will be added here + console.log('Subscript action') + } + + const handleSuperscript = () => { + // TipTap superscript logic will be added here + console.log('Superscript action') + } + + const handleInsertImage = () => { + // TipTap insert image logic will be added here + console.log('Insert image action') + } + + const handleInsertLink = () => { + // TipTap insert link logic will be added here + console.log('Insert link action') + } + + const handleMath = () => { + // TipTap math formula logic may be added here + console.log('Math action') + } + + const handleCodeBlock = () => { + // TipTap code block logic may be added here + console.log('Code block action') + } + + // Computed arrays for toolbar actions + const textActions = computed(() => [ + { + name: 'bold', + title: 'Bold', + icon: require('../../assets/icon-bold.svg'), + handler: handleBold + }, + { + name: 'italic', + title: 'Italic', + icon: require('../../assets/icon-italic.svg'), + handler: handleItalic + }, + { + name: 'underline', + title: 'Underline', + icon: require('../../assets/icon-underline.svg'), + handler: handleUnderline + }, + { + name: 'strikethrough', + title: 'Strikethrough', + icon: require('../../assets/icon-strikethrough.svg'), + handler: handleStrikethrough + } + ]) + + const listActions = computed(() => [ + { + name: 'bulletList', + title: 'Bullet List', + icon: require('../../assets/icon-bulletList.svg'), + handler: handleBulletList + }, + { + name: 'numberList', + title: 'Numbered List', + icon: require('../../assets/icon-numberList.svg'), + handler: handleNumberList + } + ]) + + const scriptActions = computed(() => [ + { + name: 'subscript', + title: 'Subscript', + icon: require('../../assets/icon-subscript.svg'), + handler: handleSubscript + }, + { + name: 'superscript', + title: 'Superscript', + icon: require('../../assets/icon-superscript.svg'), + handler: handleSuperscript + } + ]) + + const insertTools = computed(() => [ + { + name: 'image', + title: 'Insert Image', + icon: require('../../assets/icon-insertImage.svg'), + handler: handleInsertImage + }, + { + name: 'link', + title: 'Insert Link', + icon: require('../../assets/icon-link.svg'), + handler: handleInsertLink + }, + { + name: 'math', + title: 'Math', + icon: require('../../assets/icon-formula.svg'), + handler: handleMath + }, + { + name: 'code', + title: 'Code Block', + icon: require('../../assets/icon-codeblock.svg'), + handler: handleCodeBlock + } + ]) + + return { + // Individual handlers + handleUndo, + handleRedo, + handleBold, + handleItalic, + handleUnderline, + handleStrikethrough, + handleCopy, + handlePaste, + handlePasteNoFormat, + handleBulletList, + handleNumberList, + handleSubscript, + handleSuperscript, + handleInsertImage, + handleInsertLink, + handleMath, + handleCodeBlock, + + // Action arrays + textActions, + listActions, + scriptActions, + insertTools + } +} \ No newline at end of file From 4048536a8d562a4621a5f1e2b611eb436df7ace1 Mon Sep 17 00:00:00 2001 From: Habiba Date: Sun, 8 Jun 2025 16:42:20 +0300 Subject: [PATCH 04/26] feat(texteditor): implement text styling actions --- .../TipTapEditor/TipTapEditor.vue | 21 ++- .../TipTapEditor/components/EditorContent.vue | 27 ---- .../components/EditorContentWrapper.vue | 128 ++++++++++++++++++ .../TipTapEditor/components/EditorToolbar.vue | 1 + .../components/toolbar/FormatDropdown.vue | 6 + .../components/toolbar/ToolbarButton.vue | 13 +- .../TipTapEditor/composables/useEditor.js | 21 ++- .../composables/useToolbarActions.js | 52 ++++--- package.json | 1 + pnpm-lock.yaml | 12 ++ 10 files changed, 229 insertions(+), 53 deletions(-) delete mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContent.vue create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue index c9f90f9b08..e582d42f23 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue @@ -1,20 +1,33 @@ diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContent.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContent.vue deleted file mode 100644 index 606d071344..0000000000 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContent.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - - - \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue new file mode 100644 index 0000000000..5f3e8a58ab --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue @@ -0,0 +1,128 @@ + + + + + \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue index 45211ba339..b211530f5b 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue @@ -30,6 +30,7 @@ :key="action.name" :title="action.title" :icon="action.icon" + :is-active="action.isActive" @click="action.handler" /> diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue index 33b2818ad4..5cda824f1b 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue @@ -81,6 +81,12 @@ export default defineComponent({ background: #e6e6e6; } +.format-dropdown:focus-visible { + outline: 2px solid #0097F2; + border-radius: 4px; + background: #e6e6e6; +} + .dropdown-icon { width: 12px; height: 12px; diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue index 712c9107da..5a6014e35c 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue @@ -61,7 +61,18 @@ export default defineComponent({ .toolbar-btn:active, .toolbar-btn.active { - background: #dee2e6; + background: #D9E1FD; +} + +.toolbar-btn.active .toolbar-icon { + filter: brightness(0) saturate(100%) invert(32%) sepia(97%) saturate(2640%) hue-rotate(230deg) brightness(103%) contrast(94%); + opacity: 1; +} + +.toolbar-btn:focus-visible { + outline: 2px solid #0097F2; + border-radius: 4px; + background: #e6e6e6; } .toolbar-icon { diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js index ff91d04bce..05ec8746ad 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js @@ -1,19 +1,32 @@ import { ref, onMounted, onUnmounted } from 'vue' +import { Editor } from '@tiptap/vue-2' +import StarterKit from '@tiptap/starter-kit' +import Underline from '@tiptap/extension-underline' export function useEditor() { const editor = ref(null) const isReady = ref(false) const initializeEditor = () => { - // TipTap editor initialization will be added here - console.log('Editor initialization placeholder') + editor.value = new Editor({ + extensions: [ + StarterKit, + Underline + ], + content: '

Start typing here...

', + editorProps: { + attributes: { + class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl focus:outline-none', + }, + }, + }) + isReady.value = true } const destroyEditor = () => { if (editor.value) { - // TipTap editor cleanup will be added here - console.log('Editor cleanup placeholder') + editor.value.destroy() editor.value = null isReady.value = false } diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js index 84ff17f5e7..348b265d36 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js @@ -1,35 +1,44 @@ -import { computed } from 'vue' +import { computed, inject } from 'vue' export function useToolbarActions() { + // Get editor instance from provide/inject + const editor = inject('editor', null) + // Action handlers const handleUndo = () => { - // TipTap undo logic will be added here - console.log('Undo action') + if (editor?.value) { + editor.value.chain().focus().undo().run() + } } const handleRedo = () => { - // TipTap redo logic will be added here - console.log('Redo action') + if (editor?.value) { + editor.value.chain().focus().redo().run() + } } const handleBold = () => { - // TipTap bold logic will be added here - console.log('Bold action') + if (editor?.value) { + editor.value.chain().focus().toggleBold().run() + } } const handleItalic = () => { - // TipTap italic logic will be added here - console.log('Italic action') + if (editor?.value) { + editor.value.chain().focus().toggleItalic().run() + } } const handleUnderline = () => { - // TipTap underline logic will be added here - console.log('Underline action') + if (editor?.value) { + editor.value.chain().focus().toggleUnderline().run() + } } const handleStrikethrough = () => { - // TipTap strikethrough logic will be added here - console.log('Strikethrough action') + if (editor?.value) { + editor.value.chain().focus().toggleStrike().run() + } } const handleCopy = () => { @@ -87,31 +96,40 @@ export function useToolbarActions() { console.log('Code block action') } + // Helper function to check if a mark is active + const isMarkActive = (markName) => { + return editor?.value?.isActive(markName) || false + } + // Computed arrays for toolbar actions const textActions = computed(() => [ { name: 'bold', title: 'Bold', icon: require('../../assets/icon-bold.svg'), - handler: handleBold + handler: handleBold, + isActive: isMarkActive('bold') }, { name: 'italic', title: 'Italic', icon: require('../../assets/icon-italic.svg'), - handler: handleItalic + handler: handleItalic, + isActive: isMarkActive('italic') }, { name: 'underline', title: 'Underline', icon: require('../../assets/icon-underline.svg'), - handler: handleUnderline + handler: handleUnderline, + isActive: isMarkActive('underline') }, { name: 'strikethrough', title: 'Strikethrough', icon: require('../../assets/icon-strikethrough.svg'), - handler: handleStrikethrough + handler: handleStrikethrough, + isActive: isMarkActive('strike') } ]) diff --git a/package.json b/package.json index 93260864df..4955057cf1 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "homepage": "https://github.com/learningequality/studio#readme", "dependencies": { "@sentry/vue": "^7.112.2", + "@tiptap/extension-underline": "^2.14.0", "@tiptap/starter-kit": "^2.13.0", "@tiptap/vue-2": "^2.13.0", "@toast-ui/editor": "^2.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8069d31ff8..09b0cfc7e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@sentry/vue': specifier: ^7.112.2 version: 7.120.3(vue@2.7.16) + '@tiptap/extension-underline': + specifier: ^2.14.0 + version: 2.14.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0)) '@tiptap/starter-kit': specifier: ^2.13.0 version: 2.13.0 @@ -1523,6 +1526,11 @@ packages: peerDependencies: '@tiptap/core': ^2.7.0 + '@tiptap/extension-underline@2.14.0': + resolution: {integrity: sha512-rlBasbwElFikaL5qPyp3OeoEBH2p9Dve0K6liqIWF4i9cECH2Bm53y2S0enVEe01hmgQEWmoYK+fq67rxr3XsQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm@2.13.0': resolution: {integrity: sha512-CU2DEMQbYXwlnISFD0+7nQSBaqGKJHOiTnWpnAi5pOVphroEIbLPlT6AO4638MWtIR9QsOmGR8KXkQmNGA6tjQ==} @@ -9130,6 +9138,10 @@ snapshots: dependencies: '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/extension-underline@2.14.0(@tiptap/core@2.13.0(@tiptap/pm@2.13.0))': + dependencies: + '@tiptap/core': 2.13.0(@tiptap/pm@2.13.0) + '@tiptap/pm@2.13.0': dependencies: prosemirror-changeset: 2.3.1 From 1c613c242b1a3621ec1e85233afafa8bebc9bcc9 Mon Sep 17 00:00:00 2001 From: Habiba Date: Sun, 8 Jun 2025 18:02:41 +0300 Subject: [PATCH 05/26] feat(texteditor): implement copy/paste actions --- .../composables/useToolbarActions.js | 56 ++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js index 348b265d36..ccdfd5ca23 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js @@ -1,3 +1,4 @@ +import { ContentKindLearningActivityDefaults } from 'shared/leUtils/ContentKinds' import { computed, inject } from 'vue' export function useToolbarActions() { @@ -41,19 +42,58 @@ export function useToolbarActions() { } } + // Copy with formatting const handleCopy = () => { - // TipTap copy logic will be added here - console.log('Copy action') + if (editor.value) { + const { from, to } = editor.value.state.selection; + const selectedContent = editor.value.state.doc.slice(from, to); + + // Get HTML + const html = editor.value.getHTML(); + const text = editor.value.getText(); + + // Copy both HTML and plain text + navigator.clipboard.write([ + new ClipboardItem({ + 'text/html': new Blob([html], { type: 'text/html' }), + 'text/plain': new Blob([text], { type: 'text/plain' }) + }) + ]); + } } - const handlePaste = () => { - // TipTap paste logic will be added here - console.log('Paste action') + const handlePaste = async () => { + if (editor.value) { + try { + // Try HTML first + const clipboardData = await navigator.clipboard.read(); + const htmlType = clipboardData[0].types.find(type => type === 'text/html'); + + if (htmlType) { + const htmlBlob = await clipboardData[0].getType('text/html'); + const html = await htmlBlob.text(); + editor.value.chain().focus().insertContent(html).run(); + } else { + // Fall back to plain text + const text = await navigator.clipboard.readText(); + editor.value.chain().focus().insertContent(text).run(); + } + } catch (err) { + console.error('Paste failed:', err); + } } +} - const handlePasteNoFormat = () => { - // TipTap paste without formatting logic will be added here - console.log('Paste without formatting action') + const handlePasteNoFormat = async () => { + if (editor.value) { + try { + // Read plain text from clipboard + const text = await navigator.clipboard.readText(); + editor.value.chain().focus().insertContent(text).run(); + } catch (err) { + console.error('Paste without format failed:', err); + } + } } const handleBulletList = () => { From 25bcc53a220739ba1cd9dc4149738b7ef1e37531 Mon Sep 17 00:00:00 2001 From: Habiba Date: Mon, 9 Jun 2025 11:59:35 +0300 Subject: [PATCH 06/26] feat(texteditor): implement lists actions --- .../TipTapEditor/components/EditorToolbar.vue | 1 + .../components/toolbar/FormatDropdown.vue | 4 ++++ .../components/toolbar/PasteDropdown.vue | 22 ++++++++++++++----- .../composables/useToolbarActions.js | 22 ++++++++++--------- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue index b211530f5b..3a6da4a3de 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue @@ -56,6 +56,7 @@ :key="list.name" :title="list.title" :icon="list.icon" + :is-active="list.isActive" @click="list.handler" /> diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue index 5cda824f1b..05c63146b2 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue @@ -81,6 +81,10 @@ export default defineComponent({ background: #e6e6e6; } +.format-dropdown:active { + background: #d1d5da; +} + .format-dropdown:focus-visible { outline: 2px solid #0097F2; border-radius: 4px; diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue index d70b44ad89..9bd8d0fb58 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue @@ -11,6 +11,7 @@ class="paste-dropdown-btn" @click="toggleDropdown" title="Paste Options" + :class="{ active: isOpen }" > @@ -133,14 +134,25 @@ export default defineComponent({ background: #e6e6e6; } -.paste-dropdown-btn:active { - background: #dee2e6; +.paste-dropdown-btn.active { + background: #D9E1FD; +} + +.paste-dropdown-btn.active .dropdown-arrow { + filter: brightness(0) saturate(100%) invert(32%) sepia(97%) saturate(2640%) hue-rotate(230deg) brightness(103%) contrast(94%); + opacity: 1; +} + +.paste-dropdown-btn:focus-visible, .paste-main-btn:focus-visible { + outline: 2px solid #0097F2; + border-radius: 4px; + background: #e6e6e6; } .dropdown-arrow { - width: 8px; - height: 8px; - opacity: 0.5; + width: 12px; + height: 12px; + opacity: 0.9; } .dropdown-menu { diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js index ccdfd5ca23..23a0a0da22 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js @@ -1,5 +1,4 @@ -import { ContentKindLearningActivityDefaults } from 'shared/leUtils/ContentKinds' -import { computed, inject } from 'vue' +import { computed, h, inject } from 'vue' export function useToolbarActions() { // Get editor instance from provide/inject @@ -75,8 +74,7 @@ export function useToolbarActions() { editor.value.chain().focus().insertContent(html).run(); } else { // Fall back to plain text - const text = await navigator.clipboard.readText(); - editor.value.chain().focus().insertContent(text).run(); + handlePasteNoFormat(); } } catch (err) { console.error('Paste failed:', err); @@ -97,13 +95,15 @@ export function useToolbarActions() { } const handleBulletList = () => { - // TipTap bullet list logic will be added here - console.log('Bullet list action') + if (editor?.value) { + editor.value.chain().focus().toggleBulletList().run() + } } const handleNumberList = () => { - // TipTap numbered list logic will be added here - console.log('Number list action') + if (editor?.value) { + editor.value.chain().focus().toggleOrderedList().run() + } } const handleSubscript = () => { @@ -178,13 +178,15 @@ export function useToolbarActions() { name: 'bulletList', title: 'Bullet List', icon: require('../../assets/icon-bulletList.svg'), - handler: handleBulletList + handler: handleBulletList , + isActive: isMarkActive('bulletList') }, { name: 'numberList', title: 'Numbered List', icon: require('../../assets/icon-numberList.svg'), - handler: handleNumberList + handler: handleNumberList, + isActive: isMarkActive('orderedList') } ]) From 83fa4073c79c707258230ff5fc8fbc9ecaf4b22c Mon Sep 17 00:00:00 2001 From: Habiba Date: Mon, 9 Jun 2025 13:52:22 +0300 Subject: [PATCH 07/26] feat(texteditor): implement text size formatting actions --- .../components/EditorContentWrapper.vue | 5 + .../components/toolbar/FormatDropdown.vue | 12 +- .../TipTapEditor/composables/useDropdowns.js | 2 + .../TipTapEditor/composables/useEditor.js | 6 +- .../composables/useToolbarActions.js | 31 +++- .../extensions/SmallTextExtension.js | 75 +++++++++ package.json | 1 + pnpm-lock.yaml | 149 +++++++++--------- 8 files changed, 202 insertions(+), 79 deletions(-) create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/extensions/SmallTextExtension.js diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue index 5f3e8a58ab..bafb21ac6d 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue @@ -125,4 +125,9 @@ export default defineComponent({ font-weight: 600; margin: 8px 0; } + +:deep(.ProseMirror small) { + font-size: 12px; + margin: 4px 0 4px; +} \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue index 05c63146b2..e717a2f6bd 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue @@ -15,7 +15,7 @@ v-for="format in formatOptions" :key="format.value" class="dropdown-item" - @click="selectFormat(format)" + @click="applyFormat(format)" > @@ -26,6 +26,7 @@ - - \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js index e57ee94c11..d6c524be2a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js @@ -1,9 +1,48 @@ -import { ref, onMounted, onUnmounted } from 'vue' +import { ref, onMounted, onUnmounted, inject, watch } from 'vue' export function useDropdowns() { const selectedFormat = ref('Normal') const showHeadersDropdown = ref(false) const showPasteDropdown = ref(false) + const editor = inject('editor', null) + + // Format detection function + const updateSelectedFormat = () => { + if (!editor?.value) return + + // Check current active format at cursor position + if (editor.value.isActive('heading', { level: 1 })) { + selectedFormat.value = 'Header 1' + } else if (editor.value.isActive('heading', { level: 2 })) { + selectedFormat.value = 'Header 2' + } else if (editor.value.isActive('heading', { level: 3 })) { + selectedFormat.value = 'Header 3' + } else if (editor.value.isActive('small')) { + selectedFormat.value = 'small' + } else { + selectedFormat.value = 'Normal' + } + } + + watch( + () => editor?.value?.state.selection, + () => { + if (editor?.value) { + updateSelectedFormat() + } + }, + { deep: true } + ) + + let offTransaction = null + + const setupEditorListener = () => { + if (editor?.value) { + const handler = () => updateSelectedFormat() + editor.value.on('transaction', handler) + offTransaction = () => editor.value.off('transaction', handler) + } + } // Dropdown management const toggleHeadersDropdown = () => { @@ -24,10 +63,7 @@ export function useDropdowns() { const selectFormat = (format) => { selectedFormat.value = format.label closeAllDropdowns() - // TipTap formatting logic will be added here - console.log('Format selected:', format) - // console.log (useToolbarActions().handleFormat(format.value)) - + console.log('Format selected:', format) } const handleClickOutside = (event) => { @@ -47,10 +83,15 @@ export function useDropdowns() { onMounted(() => { document.addEventListener('click', handleClickOutside) + // Setup editor listener when component mounts + setupEditorListener() + // Initial format detection + updateSelectedFormat() }) onUnmounted(() => { document.removeEventListener('click', handleClickOutside) + if (offTransaction) offTransaction() }) return { @@ -60,6 +101,7 @@ export function useDropdowns() { toggleHeadersDropdown, togglePasteDropdown, closeAllDropdowns, - selectFormat + selectFormat, + updateSelectedFormat } } \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js index a2fb517756..510c683a6d 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js @@ -19,6 +19,7 @@ export function useEditor() { editorProps: { attributes: { class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl focus:outline-none', + dir: 'auto' }, }, }) diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js index a3862d5750..73f605271a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js @@ -43,9 +43,6 @@ export function useToolbarActions() { // Copy with formatting const handleCopy = () => { if (editor.value) { - const { from, to } = editor.value.state.selection; - const selectedContent = editor.value.state.doc.slice(from, to); - // Get HTML const html = editor.value.getHTML(); const text = editor.value.getText(); diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/constants.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/constants.js new file mode 100644 index 0000000000..4f51768e2b --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/constants.js @@ -0,0 +1 @@ +export const LOAD_EDITOR_PLACEHOLDER = 'Loading editor...'; \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/extensions/SmallTextExtension.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/extensions/SmallTextExtension.js index 8a292a0bee..2f9c6464c2 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/extensions/SmallTextExtension.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/extensions/SmallTextExtension.js @@ -1,9 +1,9 @@ import { Node, mergeAttributes } from '@tiptap/core'; export const Small = Node.create({ - name: 'small', // Consistent name for the node + name: 'small', - priority: 1000, // Higher priority to override other block nodes when toggling + priority: 1000, addOptions() { return { @@ -11,23 +11,23 @@ export const Small = Node.create({ }; }, - group: 'block', // Block-level node, like headings + group: 'block', - content: 'inline*', // Can contain text and inline marks (bold, italic, etc.) + content: 'inline*', parseHTML() { return [ { - tag: 'small', // Parse tags for this node + tag: 'small', }, ]; }, renderHTML({ HTMLAttributes }) { return [ - 'small', // Render as for simplicity + 'small', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - class: 'small-text', // Class for styling + class: 'small-text', }), 0, ]; @@ -36,7 +36,7 @@ export const Small = Node.create({ addAttributes() { return { class: { - default: 'small-text', // Default class for styling + default: 'small-text', }, }; }, @@ -44,10 +44,10 @@ export const Small = Node.create({ addCommands() { return { setSmall: () => ({ commands }) => { - return commands.setNode(this.name); // Set current block to 'small' + return commands.setNode(this.name); }, toggleSmall: () => ({ commands }) => { - return commands.toggleNode(this.name, 'paragraph'); // Toggle between 'small' and 'paragraph' + return commands.toggleNode(this.name, 'paragraph'); }, unsetSmall: () => ({ commands }) => { return commands.setNode('paragraph'); // Convert back to paragraph @@ -57,19 +57,7 @@ export const Small = Node.create({ addKeyboardShortcuts() { return { - 'Mod-Shift-S': () => this.editor.commands.toggleSmall(), // Consistent with command name + 'Mod-Shift-S': () => this.editor.commands.toggleSmall(), }; }, - - // Optional: Input rules (commented out to avoid errors; enable if needed) - /* - addInputRules() { - return [ - textblockTypeInputRule({ - find: /^\s*small\s+/, - type: this.type, - }), - ]; - }, - */ }); \ No newline at end of file From 36f24faa24e83057b3685dda22565d869ba38672 Mon Sep 17 00:00:00 2001 From: Habiba Date: Tue, 10 Jun 2025 00:30:10 +0300 Subject: [PATCH 10/26] chore: create dev route view --- contentcuration/contentcuration/dev_urls.py | 5 +++++ .../contentcuration/frontend/channelEdit/router.js | 6 ++++++ .../contentcuration/frontend/editorDev/index.js | 10 ++++++++++ .../contentcuration/frontend/editorDev/router.js | 12 ++++++++++++ .../templates/contentcuration/editor_dev.html | 7 +++++++ docs/rich_text_editor.md | 0 webpack.config.js | 1 + 7 files changed, 41 insertions(+) create mode 100644 contentcuration/contentcuration/frontend/editorDev/index.js create mode 100644 contentcuration/contentcuration/frontend/editorDev/router.js create mode 100644 contentcuration/contentcuration/templates/contentcuration/editor_dev.html create mode 100644 docs/rich_text_editor.md diff --git a/contentcuration/contentcuration/dev_urls.py b/contentcuration/contentcuration/dev_urls.py index 003cac87a3..38653c020d 100644 --- a/contentcuration/contentcuration/dev_urls.py +++ b/contentcuration/contentcuration/dev_urls.py @@ -13,6 +13,7 @@ from rest_framework import permissions from .urls import urlpatterns +from django.views.generic import TemplateView def webpack_redirect_view(request): @@ -75,3 +76,7 @@ def file_server(request, storage_path=None): re_path(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework")), re_path(r"^content/(?P.+)$", file_server), ] + +urlpatterns += [ + re_path(r'^editor-dev(?:/.*)?$', TemplateView.as_view(template_name='contentcuration/editor_dev.html')), +] \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/channelEdit/router.js b/contentcuration/contentcuration/frontend/channelEdit/router.js index 6ef3473fd4..a416c00f40 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/router.js +++ b/contentcuration/contentcuration/frontend/channelEdit/router.js @@ -11,10 +11,16 @@ import ReviewSelectionsPage from './views/ImportFromChannels/ReviewSelectionsPag import EditModal from './components/edit/EditModal'; import ChannelDetailsModal from 'shared/views/channel/ChannelDetailsModal'; import ChannelModal from 'shared/views/channel/ChannelModal'; +import TipTapEditor from 'shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue'; import { RouteNames as ChannelRouteNames } from 'frontend/channelList/constants'; const router = new VueRouter({ routes: [ + { + path: '/editor-dev', + name: 'TipTapEditorDev', + component: TipTapEditor, + }, { name: RouteNames.TREE_ROOT_VIEW, path: '/', diff --git a/contentcuration/contentcuration/frontend/editorDev/index.js b/contentcuration/contentcuration/frontend/editorDev/index.js new file mode 100644 index 0000000000..73b2c3815d --- /dev/null +++ b/contentcuration/contentcuration/frontend/editorDev/index.js @@ -0,0 +1,10 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import TipTapEditor from '../shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue'; +Vue.use(VueRouter); + +console.log('TipTapEditor entry loaded'); + +new Vue({ + render: h => h(TipTapEditor), +}).$mount('#app'); \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/editorDev/router.js b/contentcuration/contentcuration/frontend/editorDev/router.js new file mode 100644 index 0000000000..d6a1059559 --- /dev/null +++ b/contentcuration/contentcuration/frontend/editorDev/router.js @@ -0,0 +1,12 @@ +import Vue from 'vue'; +import Router from 'vue-router'; +import TipTapEditor from '../shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue'; + +Vue.use(Router); +export default new Router({ + mode: 'history', + base: '/editor-dev/', + routes: [ + { path: '/', component: TipTapEditor }, + ], +}); diff --git a/contentcuration/contentcuration/templates/contentcuration/editor_dev.html b/contentcuration/contentcuration/templates/contentcuration/editor_dev.html new file mode 100644 index 0000000000..62e1fe2a77 --- /dev/null +++ b/contentcuration/contentcuration/templates/contentcuration/editor_dev.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} +
+ + +{% endblock %} diff --git a/docs/rich_text_editor.md b/docs/rich_text_editor.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webpack.config.js b/webpack.config.js index d07dfcf483..f3ff72aafb 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -103,6 +103,7 @@ module.exports = (env = {}) => { pdfJSWorker: ['pdfjs-dist/build/pdf.worker.entry.js'], // Utility for taking screenshots inside an iframe sandbox htmlScreenshot: ['./shared/utils/htmlScreenshot.js'], + editorDev: './editorDev/index.js', }, output: { filename: dev ? '[name].js' : '[name]-[fullhash].js', From 96407d0d5f1d1768a1fb57d7e9ae1d3970ef0499 Mon Sep 17 00:00:00 2001 From: habibayman Date: Fri, 13 Jun 2025 17:19:28 +0300 Subject: [PATCH 11/26] feat(texteditor): wrap strings to utilize i18n --- .../TipTapEditor/TipTapEditor.vue | 9 ++++ .../TipTapEditor/TipTapEditorStrings.js | 54 +++++++++++++++++++ .../components/EditorContentWrapper.vue | 5 -- .../TipTapEditor/components/EditorToolbar.vue | 22 ++++---- .../components/toolbar/FormatDropdown.vue | 18 +++---- .../components/toolbar/PasteDropdown.vue | 31 ++++------- .../TipTapEditor/composables/useDropdowns.js | 38 ++++++++++++- .../composables/useToolbarActions.js | 44 +++++++++------ .../TipTapEditor/TipTapEditor/constants.js | 1 - 9 files changed, 153 insertions(+), 69 deletions(-) create mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js delete mode 100644 contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/constants.js diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue index e582d42f23..65885e17e0 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue @@ -71,4 +71,13 @@ export default defineComponent({ font-size: 12px; margin: 4px 0; } + +:deep(ul), +:deep(ol) { + margin: 8px 0; + padding-left: 20px; +} +:deep(li) { + margin: 4px 0; +} \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js new file mode 100644 index 0000000000..aac9afa8ff --- /dev/null +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js @@ -0,0 +1,54 @@ +import { createTranslator } from "shared/i18n"; + +const NAMESPACE = "TipTapEditorStrings"; + +const MESSAGES = { + // Toolbar button labels & alt text + undo: "Undo", + redo: "Redo", + bold: "Strong", + italic: "Italic", + underline: "Underline", + strikethrough: "Strikethrough", + copy: "Copy", + paste: "Paste", + pasteWithoutFormatting: "Paste without formatting", + bulletList: "Bullet list", + numberedList: "Numbered list", + subscript: "Subscript", + superscript: "Superscript", + insertImage: "Insert image", + insertLink: "Insert link", + mathFormula: "Math formula", + codeBlock: "Code block", + + // Format dropdown options + formatNormal: "Normal", + formatSmall: "Small", + formatHeader1: "Header 1", + formatHeader2: "Header 2", + formatHeader3: "Header 3", + + // Accessibility labels + textFormattingToolbar: "Text formatting toolbar", + historyActions: "History actions", + textFormattingOptions: "Text formatting options", + textStyleFormatting: "Text style formatting", + copyAndPasteActions: "Copy and paste actions", + listFormatting: "List formatting", + scriptFormatting: "Script formatting", + insertTools: "Insert tools", + textFormatOptions: "Text format options", + formatOptions: "Format options", + pasteOptions: "Paste options", + pasteOptionsMenu: "Paste options menu" +}; + +let translator = null; + +export function getTranslator() { + if (!translator) { + translator = createTranslator(NAMESPACE, MESSAGES); + } + return translator; +} \ No newline at end of file diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue index 24a1356153..11660ad83e 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue @@ -5,16 +5,12 @@ :editor="editor" class="tiptap-editor" /> -
- {{ LOAD_EDITOR_PLACEHOLDER }} -
+ \ No newline at end of file + .editor-container { + width: 1000px; + margin: auto; + font-family: + 'Noto Sans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'Helvetica Neue', + Arial, + sans-serif; + background: white; + border: 1px solid #e1e5e9; + border-radius: 8px; + } + + + + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js index aac9afa8ff..d050f477c7 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js @@ -1,47 +1,47 @@ -import { createTranslator } from "shared/i18n"; +import { createTranslator } from 'shared/i18n'; -const NAMESPACE = "TipTapEditorStrings"; +const NAMESPACE = 'TipTapEditorStrings'; const MESSAGES = { // Toolbar button labels & alt text - undo: "Undo", - redo: "Redo", - bold: "Strong", - italic: "Italic", - underline: "Underline", - strikethrough: "Strikethrough", - copy: "Copy", - paste: "Paste", - pasteWithoutFormatting: "Paste without formatting", - bulletList: "Bullet list", - numberedList: "Numbered list", - subscript: "Subscript", - superscript: "Superscript", - insertImage: "Insert image", - insertLink: "Insert link", - mathFormula: "Math formula", - codeBlock: "Code block", + undo: 'Undo', + redo: 'Redo', + bold: 'Strong', + italic: 'Italic', + underline: 'Underline', + strikethrough: 'Strikethrough', + copy: 'Copy', + paste: 'Paste', + pasteWithoutFormatting: 'Paste without formatting', + bulletList: 'Bullet list', + numberedList: 'Numbered list', + subscript: 'Subscript', + superscript: 'Superscript', + insertImage: 'Insert image', + insertLink: 'Insert link', + mathFormula: 'Math formula', + codeBlock: 'Code block', // Format dropdown options - formatNormal: "Normal", - formatSmall: "Small", - formatHeader1: "Header 1", - formatHeader2: "Header 2", - formatHeader3: "Header 3", + formatNormal: 'Normal', + formatSmall: 'Small', + formatHeader1: 'Header 1', + formatHeader2: 'Header 2', + formatHeader3: 'Header 3', // Accessibility labels - textFormattingToolbar: "Text formatting toolbar", - historyActions: "History actions", - textFormattingOptions: "Text formatting options", - textStyleFormatting: "Text style formatting", - copyAndPasteActions: "Copy and paste actions", - listFormatting: "List formatting", - scriptFormatting: "Script formatting", - insertTools: "Insert tools", - textFormatOptions: "Text format options", - formatOptions: "Format options", - pasteOptions: "Paste options", - pasteOptionsMenu: "Paste options menu" + textFormattingToolbar: 'Text formatting toolbar', + historyActions: 'History actions', + textFormattingOptions: 'Text formatting options', + textStyleFormatting: 'Text style formatting', + copyAndPasteActions: 'Copy and paste actions', + listFormatting: 'List formatting', + scriptFormatting: 'Script formatting', + insertTools: 'Insert tools', + textFormatOptions: 'Text format options', + formatOptions: 'Format options', + pasteOptions: 'Paste options', + pasteOptionsMenu: 'Paste options menu', }; let translator = null; @@ -51,4 +51,4 @@ export function getTranslator() { translator = createTranslator(NAMESPACE, MESSAGES); } return translator; -} \ No newline at end of file +} diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue index 11660ad83e..5a6ac235eb 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue @@ -1,120 +1,91 @@ + + + \ No newline at end of file + + .editor-content { + min-height: 200px; + padding: 16px; + padding-inline: 16px; + margin-inline: 0 auto; + } + + .editor-placeholder { + font-size: 14px; + color: #6c757d; + } + + .tiptap-editor { + outline: none; + } + + + + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue index 0085762069..5a630bf223 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue @@ -1,7 +1,15 @@ + + \ No newline at end of file + + .toolbar { + display: flex; + gap: 8px; + align-items: center; + padding: 8px 12px; + background: #f8f9fa; + border-bottom: 1px solid #e1e5e9; + border-radius: 8px 8px 0 0; + } + + [role='group'] { + display: flex; + gap: 2px; + align-items: center; + } + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue index a05f62d569..9ae8b3a51b 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/FormatDropdown.vue @@ -1,8 +1,12 @@ + + \ No newline at end of file + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue index e1c7ff78d5..d8f9d1ad09 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue @@ -1,337 +1,366 @@ + + \ No newline at end of file + + .paste-button-container { + position: relative; + display: flex; + overflow: visible; + border-radius: 4px; + } + + .dropdown-container { + position: relative; + } + + .toolbar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + cursor: pointer; + background: transparent; + border: 0; + border-radius: 4px; + transition: background-color 0.2s ease; + } + + .toolbar-btn:hover { + background: #e6e6e6; + } + + .toolbar-btn:active { + background: #dee2e6; + } + + .toolbar-btn:focus-visible { + background: #e6e6e6; + border-radius: 4px; + outline: 2px solid #0097f2; + outline-offset: 2px; + } + + .toolbar-icon { + width: 17px; + height: 17px; + opacity: 0.7; + } + + .paste-main-btn { + width: 28px !important; + border-radius: 4px 0 0 4px !important; + } + + .paste-dropdown-btn { + display: flex; + align-items: center; + justify-content: center; + width: 12px; + height: 32px; + cursor: pointer; + background: transparent; + border: 0; + border-left: 1px solid #dee2e6; + border-radius: 0 4px 4px 0; + transition: background-color 0.2s ease; + } + + .paste-dropdown-btn:hover { + background: #e6e6e6; + } + + .paste-dropdown-btn.active { + background: #d9e1fd; + } + + .paste-dropdown-btn.active .dropdown-arrow { + filter: brightness(0) saturate(100%) invert(32%) sepia(97%) saturate(2640%) hue-rotate(230deg) + brightness(103%) contrast(94%); + opacity: 1; + } + + .paste-dropdown-btn:focus-visible { + background: #e6e6e6; + border-radius: 4px; + outline: 2px solid #0097f2; + outline-offset: 2px; + } + + .dropdown-arrow { + width: 12px; + height: 12px; + opacity: 0.9; + } + + .dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + min-width: 200px; + margin-top: 4px; + background: white; + border: 1px solid #e1e5e9; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + + .paste-dropdown { + right: -12px; + min-width: 250px; + } + + .dropdown-item { + display: flex; + gap: 8px; + align-items: center; + padding: 8px 12px; + font-family: + 'Noto Sans', + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 140%; + color: #000000; + cursor: pointer; + } + + .dropdown-item:hover, + .dropdown-item:focus { + background: #f8f9fa; + outline: none; + } + + .dropdown-item:focus-visible { + background: #e6f3ff; + outline: 2px solid #0097f2; + outline-offset: -2px; + } + + .dropdown-item-icon { + width: 16px; + height: 16px; + opacity: 0.7; + } + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue index c75141c617..97af38d3ec 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarButton.vue @@ -1,6 +1,7 @@ + + \ No newline at end of file + + .toolbar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + cursor: pointer; + background: transparent; + border: 0; + border-radius: 4px; + transition: background-color 0.2s ease; + } + + .toolbar-btn:hover:not(.disabled) { + background: #e6e6e6; + } + + .toolbar-btn:active:not(.disabled), + .toolbar-btn.active { + background: #d9e1fd; + } + + .toolbar-btn.active .toolbar-icon { + filter: brightness(0) saturate(100%) invert(32%) sepia(97%) saturate(2640%) hue-rotate(230deg) + brightness(103%) contrast(94%); + opacity: 1; + } + + .toolbar-btn:focus-visible { + background: #e6e6e6; + border-radius: 4px; + outline: 2px solid #0097f2; + outline-offset: 2px; + } + + .toolbar-icon { + width: 19px; + height: 19px; + opacity: 0.7; + } + + .toolbar-btn.disabled { + cursor: not-allowed; + opacity: 0.3; + } + + .toolbar-btn:disabled { + pointer-events: none; + cursor: not-allowed; + opacity: 0.3; + } + + .toolbar-icon.rtl-flip { + transform: scaleX(-1); + } + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarDivider.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarDivider.vue index 2ec0056317..57917ffa58 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarDivider.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/ToolbarDivider.vue @@ -1,20 +1,28 @@ + + \ No newline at end of file + + .toolbar-divider { + width: 1px; + height: 20px; + margin: 0 4px; + background: #dee2e6; + } + + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js index c195bacbbe..01b474ff36 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useDropdowns.js @@ -1,12 +1,12 @@ -import { ref, onMounted, onUnmounted, inject, watch, computed } from 'vue' -import {useToolbarActions} from './useToolbarActions' -import { getTranslator } from '../TipTapEditorStrings' +import { ref, onMounted, onUnmounted, inject, watch, computed } from 'vue'; +import { getTranslator } from '../TipTapEditorStrings'; +import { useToolbarActions } from './useToolbarActions'; export function useDropdowns() { - const selectedFormat = ref('Normal') - const showHeadersDropdown = ref(false) - const showPasteDropdown = ref(false) - const editor = inject('editor', null) + const selectedFormat = ref('Normal'); + const showHeadersDropdown = ref(false); + const showPasteDropdown = ref(false); + const editor = inject('editor', null); // This function retrieves the translator instance for localization const t = (key, args = {}) => { @@ -16,119 +16,117 @@ export function useDropdowns() { // Format detection function const updateSelectedFormat = () => { - if (!editor?.value) return + if (!editor?.value) return; // Check current active format at cursor position if (editor.value.isActive('heading', { level: 1 })) { - selectedFormat.value = 'Header 1' + selectedFormat.value = 'Header 1'; } else if (editor.value.isActive('heading', { level: 2 })) { - selectedFormat.value = 'Header 2' + selectedFormat.value = 'Header 2'; } else if (editor.value.isActive('heading', { level: 3 })) { - selectedFormat.value = 'Header 3' + selectedFormat.value = 'Header 3'; } else if (editor.value.isActive('small')) { - selectedFormat.value = 'small' + selectedFormat.value = 'small'; } else { - selectedFormat.value = 'Normal' + selectedFormat.value = 'Normal'; } - } + }; watch( () => editor?.value?.state.selection, () => { if (editor?.value) { - updateSelectedFormat() + updateSelectedFormat(); } }, - { deep: true } - ) + { deep: true }, + ); - let offTransaction = null + let offTransaction = null; const setupEditorListener = () => { if (editor?.value) { - const handler = () => updateSelectedFormat() - editor.value.on('transaction', handler) - offTransaction = () => editor.value.off('transaction', handler) + const handler = () => updateSelectedFormat(); + editor.value.on('transaction', handler); + offTransaction = () => editor.value.off('transaction', handler); } - } + }; // Dropdown management const toggleHeadersDropdown = () => { - showHeadersDropdown.value = !showHeadersDropdown.value - showPasteDropdown.value = false - } + showHeadersDropdown.value = !showHeadersDropdown.value; + showPasteDropdown.value = false; + }; const togglePasteDropdown = () => { - showPasteDropdown.value = !showPasteDropdown.value - showHeadersDropdown.value = false - } + showPasteDropdown.value = !showPasteDropdown.value; + showHeadersDropdown.value = false; + }; const closeAllDropdowns = () => { - showHeadersDropdown.value = false - showPasteDropdown.value = false - } - - const selectFormat = (format) => { - selectedFormat.value = format.label - closeAllDropdowns() - console.log('Format selected:', format) - } - - const handleClickOutside = (event) => { - const dropdownContainers = document.querySelectorAll('.dropdown-container') - let isOutside = true - + showHeadersDropdown.value = false; + showPasteDropdown.value = false; + }; + + const selectFormat = format => { + selectedFormat.value = format.label; + closeAllDropdowns(); + }; + + const handleClickOutside = event => { + const dropdownContainers = document.querySelectorAll('.dropdown-container'); + let isOutside = true; + dropdownContainers.forEach(container => { if (container.contains(event.target)) { - isOutside = false + isOutside = false; } - }) - + }); + if (isOutside) { - closeAllDropdowns() + closeAllDropdowns(); } - } + }; onMounted(() => { - document.addEventListener('click', handleClickOutside) + document.addEventListener('click', handleClickOutside); // Setup editor listener when component mounts - setupEditorListener() + setupEditorListener(); // Initial format detection - updateSelectedFormat() - }) + updateSelectedFormat(); + }); onUnmounted(() => { - document.removeEventListener('click', handleClickOutside) - if (offTransaction) offTransaction() - }) + document.removeEventListener('click', handleClickOutside); + if (offTransaction) offTransaction(); + }); const formatOptions = computed(() => [ - { value: 'small', label: t('formatSmall'), tag: 'small' }, - { value: 'normal', label: t('formatNormal'), tag: 'p' }, - { value: 'h3', label: t('formatHeader3'), tag: 'h3' }, - { value: 'h2', label: t('formatHeader2'), tag: 'h2' }, - { value: 'h1', label: t('formatHeader1'), tag: 'h1' } -]) - + { value: 'small', label: t('formatSmall'), tag: 'small' }, + { value: 'normal', label: t('formatNormal'), tag: 'p' }, + { value: 'h3', label: t('formatHeader3'), tag: 'h3' }, + { value: 'h2', label: t('formatHeader2'), tag: 'h2' }, + { value: 'h1', label: t('formatHeader1'), tag: 'h1' }, + ]); const pasteOptions = computed(() => [ - { - name: 'paste', - title: t('paste'), - icon: require('../../assets/icon-paste.svg'), - handler: useToolbarActions().handlePaste + { + name: 'paste', + title: t('paste'), + icon: require('../../assets/icon-paste.svg'), + handler: useToolbarActions().handlePaste, }, - { - name: 'pasteNoFormat', - title: t('pasteWithoutFormatting'), - icon: require('../../assets/icon-pasteNoFormat.svg'), - handler: useToolbarActions().handlePasteNoFormat - } - ]) + { + name: 'pasteNoFormat', + title: t('pasteWithoutFormatting'), + icon: require('../../assets/icon-pasteNoFormat.svg'), + handler: useToolbarActions().handlePasteNoFormat, + }, + ]); return { selectedFormat, - showHeadersDropdown, + showHeadersDropdown, showPasteDropdown, formatOptions, pasteOptions, @@ -136,6 +134,6 @@ export function useDropdowns() { togglePasteDropdown, closeAllDropdowns, selectFormat, - updateSelectedFormat - } -} \ No newline at end of file + updateSelectedFormat, + }; +} diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js index 510c683a6d..95375cbda0 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useEditor.js @@ -1,52 +1,48 @@ -import { ref, onMounted, onUnmounted } from 'vue' -import { Editor } from '@tiptap/vue-2' -import StarterKit from '@tiptap/starter-kit' -import Underline from '@tiptap/extension-underline' -import { Small } from '../extensions/SmallTextExtension' +import { ref, onMounted, onUnmounted } from 'vue'; +import { Editor } from '@tiptap/vue-2'; +import StarterKitExtension from '@tiptap/starter-kit'; +import UnderlineExtension from '@tiptap/extension-underline'; +import { Small } from '../extensions/SmallTextExtension'; export function useEditor() { - const editor = ref(null) - const isReady = ref(false) + const editor = ref(null); + const isReady = ref(false); const initializeEditor = () => { editor.value = new Editor({ - extensions: [ - StarterKit, - Underline, - Small, - ], + extensions: [StarterKitExtension, UnderlineExtension, Small], content: '

', editorProps: { attributes: { class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl focus:outline-none', - dir: 'auto' + dir: 'auto', }, }, - }) - - isReady.value = true - } + }); + + isReady.value = true; + }; const destroyEditor = () => { if (editor.value) { - editor.value.destroy() - editor.value = null - isReady.value = false + editor.value.destroy(); + editor.value = null; + isReady.value = false; } - } + }; onMounted(() => { - initializeEditor() - }) + initializeEditor(); + }); onUnmounted(() => { - destroyEditor() - }) + destroyEditor(); + }); return { editor, isReady, initializeEditor, - destroyEditor - } -} \ No newline at end of file + destroyEditor, + }; +} diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js index 93c4853cd1..7f74400f0e 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js @@ -1,51 +1,51 @@ -import { computed, inject } from 'vue' +import { computed, inject } from 'vue'; import { getTranslator } from '../TipTapEditorStrings'; export function useToolbarActions() { - const editor = inject('editor', null) + const editor = inject('editor', null); // Create the translator object const t = (key, args = {}) => { const translator = getTranslator(); return translator.$tr(key, args); }; - + // Action handlers const handleUndo = () => { if (editor?.value) { - editor.value.chain().focus().undo().run() + editor.value.chain().focus().undo().run(); } - } + }; const handleRedo = () => { if (editor?.value) { - editor.value.chain().focus().redo().run() + editor.value.chain().focus().redo().run(); } - } + }; const handleBold = () => { if (editor?.value) { - editor.value.chain().focus().toggleBold().run() + editor.value.chain().focus().toggleBold().run(); } - } + }; const handleItalic = () => { if (editor?.value) { - editor.value.chain().focus().toggleItalic().run() + editor.value.chain().focus().toggleItalic().run(); } - } + }; const handleUnderline = () => { if (editor?.value) { - editor.value.chain().focus().toggleUnderline().run() + editor.value.chain().focus().toggleUnderline().run(); } - } + }; const handleStrikethrough = () => { if (editor?.value) { - editor.value.chain().focus().toggleStrike().run() + editor.value.chain().focus().toggleStrike().run(); } - } + }; // Copy with formatting const handleCopy = () => { @@ -53,37 +53,41 @@ export function useToolbarActions() { // Get HTML const html = editor.value.getHTML(); const text = editor.value.getText(); - + // Copy both HTML and plain text navigator.clipboard.write([ new ClipboardItem({ 'text/html': new Blob([html], { type: 'text/html' }), - 'text/plain': new Blob([text], { type: 'text/plain' }) - }) + 'text/plain': new Blob([text], { type: 'text/plain' }), + }), ]); } - } + }; const handlePaste = async () => { - if (editor.value) { - try { - // Try HTML first - const clipboardData = await navigator.clipboard.read(); - const htmlType = clipboardData[0].types.find(type => type === 'text/html'); - - if (htmlType) { - const htmlBlob = await clipboardData[0].getType('text/html'); - const html = await htmlBlob.text(); - editor.value.chain().focus().insertContent(html).run(); - } else { - // Fall back to plain text - handlePasteNoFormat(); + if (editor.value) { + try { + // Try HTML first + const clipboardData = await navigator.clipboard.read(); + const htmlType = clipboardData[0].types.find(type => type === 'text/html'); + + if (htmlType) { + const htmlBlob = await clipboardData[0].getType('text/html'); + const html = await htmlBlob.text(); + editor.value.chain().focus().insertContent(html).run(); + } else { + // Fall back to plain text + handlePasteNoFormat(); + } + } catch (err) { + editor.value + .chain() + .focus() + .insertContent('Clipboard access failed. Try copying again.') + .run(); } - } catch (err) { - console.error('Paste failed:', err); } - } -} + }; const handlePasteNoFormat = async () => { if (editor.value) { @@ -92,87 +96,84 @@ export function useToolbarActions() { const text = await navigator.clipboard.readText(); editor.value.chain().focus().insertContent(text).run(); } catch (err) { - console.error('Paste without format failed:', err); + editor.value + .chain() + .focus() + .insertContent('Clipboard access failed. Try copying again.') + .run(); } } - } + }; const handleBulletList = () => { if (editor?.value) { - editor.value.chain().focus().toggleBulletList().run() + editor.value.chain().focus().toggleBulletList().run(); } - } + }; const handleNumberList = () => { if (editor?.value) { - editor.value.chain().focus().toggleOrderedList().run() + editor.value.chain().focus().toggleOrderedList().run(); } - } + }; const handleSubscript = () => { // TipTap subscript logic will be added here - console.log('Subscript action') - } + }; const handleSuperscript = () => { // TipTap superscript logic will be added here - console.log('Superscript action') - } + }; const handleInsertImage = () => { // TipTap insert image logic will be added here - console.log('Insert image action') - } + }; const handleInsertLink = () => { // TipTap insert link logic will be added here - console.log('Insert link action') - } + }; const handleMath = () => { // TipTap math formula logic may be added here - console.log('Math action') - } + }; const handleCodeBlock = () => { // TipTap code block logic may be added here - console.log('Code block action') - } + }; - const handleFormatChange = (format) =>{ + const handleFormatChange = format => { if (!editor?.value) return; - + switch (format) { case 'normal': - // Clear any existing formatting and set to paragraph - editor.value.chain().focus().clearNodes().setParagraph().run() - break - case 'h1': - editor.value.chain().focus().toggleHeading({ level: 1 }).run() - break - case 'h2': - editor.value.chain().focus().toggleHeading({ level: 2 }).run() - break - case 'h3': - editor.value.chain().focus().toggleHeading({ level: 3 }).run() - break - case 'small': - // Convert to paragraph first, then apply small mark - editor.value.chain().focus().setSmall().run() - break - default: - console.warn('Unknown format:', format) - break + // Clear any existing formatting and set to paragraph + editor.value.chain().focus().clearNodes().setParagraph().run(); + break; + case 'h1': + editor.value.chain().focus().toggleHeading({ level: 1 }).run(); + break; + case 'h2': + editor.value.chain().focus().toggleHeading({ level: 2 }).run(); + break; + case 'h3': + editor.value.chain().focus().toggleHeading({ level: 3 }).run(); + break; + case 'small': + // Convert to paragraph first, then apply small mark + editor.value.chain().focus().setSmall().run(); + break; + default: + break; } - } + }; // Helper function to check if a mark is active - const isMarkActive = (markName) => { - return editor?.value?.isActive(markName) || false - } + const isMarkActive = markName => { + return editor?.value?.isActive(markName) || false; + }; // Helper function to check if a button is clickable - const isButtonAvailable = (action) => { + const isButtonAvailable = action => { if (!editor?.value) return false; switch (action) { @@ -183,116 +184,116 @@ export function useToolbarActions() { default: return true; // Default to true for other actions } - } + }; // Computed arrays for toolbar actions const historyActions = computed(() => [ - { - name: 'undo', - title: t('undo'), + { + name: 'undo', + title: t('undo'), icon: require('../../assets/icon-undo.svg'), handler: handleUndo, - isAvailable: isButtonAvailable('undo') + isAvailable: isButtonAvailable('undo'), }, - { - name: 'redo', - title: t('redo'), + { + name: 'redo', + title: t('redo'), icon: require('../../assets/icon-redo.svg'), handler: handleRedo, - isAvailable: isButtonAvailable('redo') - } - ]) + isAvailable: isButtonAvailable('redo'), + }, + ]); const textActions = computed(() => [ - { - name: 'bold', - title: t('bold'), - icon: require('../../assets/icon-bold.svg'), + { + name: 'bold', + title: t('bold'), + icon: require('../../assets/icon-bold.svg'), handler: handleBold, - isActive: isMarkActive('bold') + isActive: isMarkActive('bold'), }, - { - name: 'italic', - title: t('italic'), - icon: require('../../assets/icon-italic.svg'), + { + name: 'italic', + title: t('italic'), + icon: require('../../assets/icon-italic.svg'), handler: handleItalic, - isActive: isMarkActive('italic') + isActive: isMarkActive('italic'), }, - { - name: 'underline', - title: t('underline'), - icon: require('../../assets/icon-underline.svg'), + { + name: 'underline', + title: t('underline'), + icon: require('../../assets/icon-underline.svg'), handler: handleUnderline, - isActive: isMarkActive('underline') + isActive: isMarkActive('underline'), }, - { - name: 'strikethrough', - title: t('strikethrough'), - icon: require('../../assets/icon-strikethrough.svg'), + { + name: 'strikethrough', + title: t('strikethrough'), + icon: require('../../assets/icon-strikethrough.svg'), handler: handleStrikethrough, - isActive: isMarkActive('strike') - } - ]) + isActive: isMarkActive('strike'), + }, + ]); const listActions = computed(() => [ - { - name: 'bulletList', - title: t('bulletList'), - icon: require('../../assets/icon-bulletList.svg'), - handler: handleBulletList , - isActive: isMarkActive('bulletList') + { + name: 'bulletList', + title: t('bulletList'), + icon: require('../../assets/icon-bulletList.svg'), + handler: handleBulletList, + isActive: isMarkActive('bulletList'), }, - { - name: 'numberList', - title: t('numberedList'), - icon: require('../../assets/icon-numberList.svg'), + { + name: 'numberList', + title: t('numberedList'), + icon: require('../../assets/icon-numberList.svg'), rtlIcon: require('../../assets/icon-numberListRTL.svg'), - handler: handleNumberList, - isActive: isMarkActive('orderedList') - } - ]) + handler: handleNumberList, + isActive: isMarkActive('orderedList'), + }, + ]); const scriptActions = computed(() => [ - { - name: 'subscript', - title: t('subscript'), - icon: require('../../assets/icon-subscript.svg'), - handler: handleSubscript + { + name: 'subscript', + title: t('subscript'), + icon: require('../../assets/icon-subscript.svg'), + handler: handleSubscript, }, - { - name: 'superscript', - title: t('superscript'), - icon: require('../../assets/icon-superscript.svg'), - handler: handleSuperscript - } - ]) + { + name: 'superscript', + title: t('superscript'), + icon: require('../../assets/icon-superscript.svg'), + handler: handleSuperscript, + }, + ]); const insertTools = computed(() => [ - { - name: 'image', - title: t('insertImage'), - icon: require('../../assets/icon-insertImage.svg'), - handler: handleInsertImage + { + name: 'image', + title: t('insertImage'), + icon: require('../../assets/icon-insertImage.svg'), + handler: handleInsertImage, }, - { - name: 'link', - title: t('insertLink'), - icon: require('../../assets/icon-link.svg'), - handler: handleInsertLink + { + name: 'link', + title: t('insertLink'), + icon: require('../../assets/icon-link.svg'), + handler: handleInsertLink, }, - { - name: 'math', - title: t('mathFormula'), - icon: require('../../assets/icon-formula.svg'), - handler: handleMath + { + name: 'math', + title: t('mathFormula'), + icon: require('../../assets/icon-formula.svg'), + handler: handleMath, }, - { - name: 'code', - title: t('codeBlock'), - icon: require('../../assets/icon-codeblock.svg'), - handler: handleCodeBlock - } - ]) + { + name: 'code', + title: t('codeBlock'), + icon: require('../../assets/icon-codeblock.svg'), + handler: handleCodeBlock, + }, + ]); return { // Individual handlers @@ -323,6 +324,6 @@ export function useToolbarActions() { insertTools, // translator function - t - } -} \ No newline at end of file + t, + }; +} diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/extensions/SmallTextExtension.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/extensions/SmallTextExtension.js index 2f9c6464c2..b52a27e53e 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/extensions/SmallTextExtension.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/extensions/SmallTextExtension.js @@ -1,9 +1,9 @@ import { Node, mergeAttributes } from '@tiptap/core'; export const Small = Node.create({ - name: 'small', + name: 'small', - priority: 1000, + priority: 1000, addOptions() { return { @@ -11,23 +11,23 @@ export const Small = Node.create({ }; }, - group: 'block', + group: 'block', - content: 'inline*', + content: 'inline*', parseHTML() { return [ { - tag: 'small', + tag: 'small', }, ]; }, renderHTML({ HTMLAttributes }) { return [ - 'small', + 'small', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - class: 'small-text', + class: 'small-text', }), 0, ]; @@ -43,21 +43,27 @@ export const Small = Node.create({ addCommands() { return { - setSmall: () => ({ commands }) => { - return commands.setNode(this.name); - }, - toggleSmall: () => ({ commands }) => { - return commands.toggleNode(this.name, 'paragraph'); - }, - unsetSmall: () => ({ commands }) => { - return commands.setNode('paragraph'); // Convert back to paragraph - }, + setSmall: + () => + ({ commands }) => { + return commands.setNode(this.name); + }, + toggleSmall: + () => + ({ commands }) => { + return commands.toggleNode(this.name, 'paragraph'); + }, + unsetSmall: + () => + ({ commands }) => { + return commands.setNode('paragraph'); // Convert back to paragraph + }, }; }, addKeyboardShortcuts() { return { - 'Mod-Shift-S': () => this.editor.commands.toggleSmall(), + 'Mod-Shift-S': () => this.editor.commands.toggleSmall(), }; }, -}); \ No newline at end of file +}); From 276815cc87f14efc6e6417dac0273c1beaabae12 Mon Sep 17 00:00:00 2001 From: habibayman Date: Fri, 13 Jun 2025 20:39:23 +0300 Subject: [PATCH 15/26] fix(texteditor): remove white spaces for precommit action --- contentcuration/contentcuration/dev_urls.py | 4 ++-- .../frontend/shared/views/TipTapEditor/assets/icon-bold.svg | 2 +- .../shared/views/TipTapEditor/assets/icon-chevron-down.svg | 2 +- .../frontend/shared/views/TipTapEditor/assets/icon-copy.svg | 2 +- .../shared/views/TipTapEditor/assets/icon-italic.svg | 2 +- .../shared/views/TipTapEditor/assets/icon-numberListRTL.svg | 2 +- .../shared/views/TipTapEditor/assets/icon-paste.svg | 2 +- .../shared/views/TipTapEditor/assets/icon-strikethrough.svg | 2 +- .../shared/views/TipTapEditor/assets/icon-underline.svg | 2 +- docs/rich_text_editor.md | 6 +++--- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contentcuration/contentcuration/dev_urls.py b/contentcuration/contentcuration/dev_urls.py index 38653c020d..51952aa931 100644 --- a/contentcuration/contentcuration/dev_urls.py +++ b/contentcuration/contentcuration/dev_urls.py @@ -8,12 +8,12 @@ from django.urls import include from django.urls import path from django.urls import re_path +from django.views.generic import TemplateView from drf_yasg import openapi from drf_yasg.views import get_schema_view from rest_framework import permissions from .urls import urlpatterns -from django.views.generic import TemplateView def webpack_redirect_view(request): @@ -79,4 +79,4 @@ def file_server(request, storage_path=None): urlpatterns += [ re_path(r'^editor-dev(?:/.*)?$', TemplateView.as_view(template_name='contentcuration/editor_dev.html')), -] \ No newline at end of file +] diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bold.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bold.svg index 974567707d..1f98e5d62c 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bold.svg +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-bold.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-chevron-down.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-chevron-down.svg index 85b191bfb3..3a861a2508 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-chevron-down.svg +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-chevron-down.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-copy.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-copy.svg index d5784eefc7..ce9b8becb8 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-copy.svg +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-copy.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-italic.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-italic.svg index 95b95c189e..5f3b172475 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-italic.svg +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-italic.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-numberListRTL.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-numberListRTL.svg index 21c2a80b73..7d44f4c45f 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-numberListRTL.svg +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-numberListRTL.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-paste.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-paste.svg index 963d72c8b4..bfd0ae5e6f 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-paste.svg +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-paste.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-strikethrough.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-strikethrough.svg index dfdfef72f6..157cbd361a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-strikethrough.svg +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-strikethrough.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-underline.svg b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-underline.svg index c50954e85d..f3e99f25f2 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-underline.svg +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/assets/icon-underline.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/docs/rich_text_editor.md b/docs/rich_text_editor.md index f73e9fd93b..a83a14fb07 100644 --- a/docs/rich_text_editor.md +++ b/docs/rich_text_editor.md @@ -2,7 +2,7 @@ This Component replaces Studio’s text editor with a future-ready implementation, deliberately scoped to immediate needs: -​ Swap Toast UI while preserving Markdown storage --​ Support for core formatting +-​ Support for core formatting -​ Support for more advanced formats (code and math blocks, image uploading) It uses TipTap which is a headless, framework-agnostic rich-text editor built on top of ProseMirror. @@ -29,7 +29,7 @@ TipTapEditor/ | │ └── useToolbarActions.js | ├── extensions/ | │ ├── SmallTextExtension.js -| └── TipTapEditor.vue # Main container +| └── TipTapEditor.vue # Main container └── TipTapEditorStrings.js ``` ## Current Key Features @@ -268,4 +268,4 @@ asheres to [the suggested figma design](https://www.figma.com/design/uw8lx88ZKZU - Modern browsers with Clipboard API support - RTL text direction support -- Keyboard navigation compatibility \ No newline at end of file +- Keyboard navigation compatibility From dab44fba2ba7f98836094f0730761e9843fbf560 Mon Sep 17 00:00:00 2001 From: habibayman Date: Fri, 13 Jun 2025 21:15:35 +0300 Subject: [PATCH 16/26] fix(texteditor): adjust the editor-dev route to start correctly --- contentcuration/contentcuration/dev_urls.py | 5 ++++- .../frontend/editorDev/index.js | 21 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/contentcuration/contentcuration/dev_urls.py b/contentcuration/contentcuration/dev_urls.py index 51952aa931..77cbddfbac 100644 --- a/contentcuration/contentcuration/dev_urls.py +++ b/contentcuration/contentcuration/dev_urls.py @@ -78,5 +78,8 @@ def file_server(request, storage_path=None): ] urlpatterns += [ - re_path(r'^editor-dev(?:/.*)?$', TemplateView.as_view(template_name='contentcuration/editor_dev.html')), + re_path( + r"^editor-dev(?:/.*)?$", + TemplateView.as_view(template_name="contentcuration/editor_dev.html"), + ), ] diff --git a/contentcuration/contentcuration/frontend/editorDev/index.js b/contentcuration/contentcuration/frontend/editorDev/index.js index 7773e9bd50..ef9a10c8db 100644 --- a/contentcuration/contentcuration/frontend/editorDev/index.js +++ b/contentcuration/contentcuration/frontend/editorDev/index.js @@ -1,9 +1,24 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import VueRouter from 'vue-router'; import TipTapEditor from '../shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue'; +import startApp from 'shared/app'; +import storeFactory from 'shared/vuex/baseStore'; Vue.use(VueRouter); +Vue.use(Vuex); -new Vue({ - render: h => h(TipTapEditor), -}).$mount('#app'); +// Create a minimal store that has the required methods +const store = storeFactory(); + +startApp({ + store, // Provide the store - this is required Althought not needed + router: new VueRouter({ + routes: [ + { + path: '/', + component: TipTapEditor, + }, + ], + }), +}); From 03a362676fe091eb3bebc074f73471bb97f11beb Mon Sep 17 00:00:00 2001 From: habibayman Date: Tue, 24 Jun 2025 06:31:11 +0300 Subject: [PATCH 17/26] fix(texteditor)[copy]: unexpected extra copying behavior --- .../TipTapEditor/TipTapEditor/TipTapEditor.vue | 2 +- .../components/toolbar/PasteDropdown.vue | 1 - .../TipTapEditor/composables/useToolbarActions.js | 13 ++----------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue index 67c407dae3..405f7654de 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditor.vue @@ -37,7 +37,7 @@ .editor-container { width: 1000px; - margin: auto; + margin: 40px auto; font-family: 'Noto Sans', -apple-system, diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue index d8f9d1ad09..6a5e00ed0a 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/toolbar/PasteDropdown.vue @@ -277,7 +277,6 @@ cursor: pointer; background: transparent; border: 0; - border-left: 1px solid #dee2e6; border-radius: 0 4px 4px 0; transition: background-color 0.2s ease; } diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js index 7f74400f0e..a4d53a06a5 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/composables/useToolbarActions.js @@ -50,17 +50,8 @@ export function useToolbarActions() { // Copy with formatting const handleCopy = () => { if (editor.value) { - // Get HTML - const html = editor.value.getHTML(); - const text = editor.value.getText(); - - // Copy both HTML and plain text - navigator.clipboard.write([ - new ClipboardItem({ - 'text/html': new Blob([html], { type: 'text/html' }), - 'text/plain': new Blob([text], { type: 'text/plain' }), - }), - ]); + // Just use the browser's built-in copy command + document.execCommand('copy'); } }; From 042b71957f4c15671bc00dddfa27e7f4a10591cf Mon Sep 17 00:00:00 2001 From: habibayman Date: Tue, 24 Jun 2025 11:47:08 +0300 Subject: [PATCH 18/26] refactor(texteditor)[i18n]: translated strings syntax --- .../TipTapEditor/TipTapEditorStrings.js | 180 ++++++++++++++---- .../components/EditorContentWrapper.vue | 7 +- .../TipTapEditor/components/EditorToolbar.vue | 42 ++-- .../components/toolbar/FormatDropdown.vue | 15 +- .../components/toolbar/PasteDropdown.vue | 23 ++- .../TipTapEditor/composables/useDropdowns.js | 40 ++-- .../composables/useToolbarActions.js | 57 +++--- 7 files changed, 257 insertions(+), 107 deletions(-) diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js index d050f477c7..f340b8101b 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/TipTapEditorStrings.js @@ -4,51 +4,153 @@ const NAMESPACE = 'TipTapEditorStrings'; const MESSAGES = { // Toolbar button labels & alt text - undo: 'Undo', - redo: 'Redo', - bold: 'Strong', - italic: 'Italic', - underline: 'Underline', - strikethrough: 'Strikethrough', - copy: 'Copy', - paste: 'Paste', - pasteWithoutFormatting: 'Paste without formatting', - bulletList: 'Bullet list', - numberedList: 'Numbered list', - subscript: 'Subscript', - superscript: 'Superscript', - insertImage: 'Insert image', - insertLink: 'Insert link', - mathFormula: 'Math formula', - codeBlock: 'Code block', + undo: { + message: 'Undo', + context: 'Button to undo the last action in the text editor' + }, + redo: { + message: 'Redo', + context: 'Button to redo the last undone action in the text editor' + }, + bold: { + message: 'Strong', + context: 'Button to make text bold/strong' + }, + italic: { + message: 'Italic', + context: 'Button to italicize the selected text' + }, + underline: { + message: 'Underline', + context: 'Button to underline the selected text' + }, + strikethrough: { + message: 'Strikethrough', + context: 'Button to strikethrough the selected text' + }, + copy: { + message: 'Copy', + context: 'Button to copy the selected text' + }, + paste: { + message: 'Paste', + context: 'Button to paste text from clipboard' + }, + pasteWithoutFormatting: { + message: 'Paste without formatting', + context: 'Button to paste text without any formatting' + }, + bulletList: { + message: 'Bullet list', + context: 'Button to create a bullet list' + }, + numberedList: { + message: 'Numbered list', + context: 'Button to create a numbered list' + }, + subscript: { + message: 'Subscript', + context: 'Button to make text subscript' + }, + superscript: { + message: 'Superscript', + context: 'Button to make text superscript' + }, + insertImage: { + message: 'Insert image', + context: 'Button to insert an image' + }, + insertLink: { + message: 'Insert link', + context: 'Button to insert a hyperlink' + }, + mathFormula: { + message: 'Math formula', + context: 'Button to insert a mathematical formula' + }, + codeBlock: { + message: 'Code block', + context: 'Button to insert a block of code' + }, // Format dropdown options - formatNormal: 'Normal', - formatSmall: 'Small', - formatHeader1: 'Header 1', - formatHeader2: 'Header 2', - formatHeader3: 'Header 3', + formatNormal: { + message: 'Normal', + context: 'Option to set text format to normal' + }, + formatSmall: { + message: 'Small', + context: 'Option to set text format to small' + }, + formatHeader1: { + message: 'Header 1', + context: 'Option to set text format to header 1' + }, + formatHeader2: { + message: 'Header 2', + context: 'Option to set text format to header 2' + }, + formatHeader3: { + message: 'Header 3', + context: 'Option to set text format to header 3' + }, // Accessibility labels - textFormattingToolbar: 'Text formatting toolbar', - historyActions: 'History actions', - textFormattingOptions: 'Text formatting options', - textStyleFormatting: 'Text style formatting', - copyAndPasteActions: 'Copy and paste actions', - listFormatting: 'List formatting', - scriptFormatting: 'Script formatting', - insertTools: 'Insert tools', - textFormatOptions: 'Text format options', - formatOptions: 'Format options', - pasteOptions: 'Paste options', - pasteOptionsMenu: 'Paste options menu', + textFormattingToolbar: { + message: 'Text formatting toolbar', + context: 'Toolbar for text formatting options' + }, + historyActions: { + message: 'History actions', + context: 'Actions for managing history of changes' + }, + textFormattingOptions: { + message: 'Text formatting options', + context: 'Options for formatting text' + }, + textStyleFormatting: { + message: 'Text style formatting', + context: 'Formatting options for text styles' + }, + copyAndPasteActions: { + message: 'Copy and paste actions', + context: 'Actions for copying and pasting text' + }, + listFormatting: { + message: 'List formatting', + context: 'Options for formatting lists' + }, + scriptFormatting: { + message: 'Script formatting', + context: 'Options for formatting scripts' + }, + insertTools: { + message: 'Insert tools', + context: 'Tools for inserting elements into the text' + }, + textFormatOptions: { + message: 'Text format options', + context: 'Options for text formatting' + }, + formatOptions: { + message: 'Format options', + context: 'General options for formatting' + }, + pasteOptions: { + message: 'Paste options', + context: 'Options for pasting text' + }, + pasteOptionsMenu: { + message: 'Paste options menu', + context: 'Menu for selecting paste options' + }, }; -let translator = null; +let TipTapEditorStrings = null; -export function getTranslator() { - if (!translator) { - translator = createTranslator(NAMESPACE, MESSAGES); +export function getTipTapEditorStrings() { + if (!TipTapEditorStrings) { + TipTapEditorStrings = createTranslator(NAMESPACE, MESSAGES); } - return translator; + return TipTapEditorStrings; } diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue index 5a6ac235eb..75ca8bc98e 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorContentWrapper.vue @@ -41,15 +41,10 @@ .editor-content { min-height: 200px; padding: 16px; - padding-inline: 16px; + padding-inline: 24px; margin-inline: 0 auto; } - .editor-placeholder { - font-size: 14px; - color: #6c757d; - } - .tiptap-editor { outline: none; } diff --git a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue index 5a630bf223..cc82a1b8a0 100644 --- a/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue +++ b/contentcuration/contentcuration/frontend/shared/views/TipTapEditor/TipTapEditor/components/EditorToolbar.vue @@ -3,12 +3,12 @@