From 3ed2730c646796bb5318ce5b6db473dec3f16e6d Mon Sep 17 00:00:00 2001 From: Ruben Bimberg <ruben.bimberg@mni.thm.de> Date: Sat, 28 Aug 2021 15:22:04 +0200 Subject: [PATCH] Add emojis and image resizing to quill --- angular.json | 6 +- package-lock.json | 234 +++++++++++++++--- package.json | 2 + .../write-comment.component.html | 36 ++- .../write-comment.component.scss | 22 ++ .../write-comment/write-comment.component.ts | 163 +++++++++--- .../write-comment/write-comment.marks.ts | 230 ++++++----------- src/assets/i18n/creator/de.json | 14 ++ src/assets/i18n/creator/en.json | 14 ++ src/assets/i18n/participant/de.json | 14 ++ src/assets/i18n/participant/en.json | 14 ++ 11 files changed, 512 insertions(+), 237 deletions(-) diff --git a/angular.json b/angular.json index f1693a005..31f04944f 100644 --- a/angular.json +++ b/angular.json @@ -29,7 +29,8 @@ "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.css", "node_modules/quill/dist/quill.core.css", "node_modules/quill/dist/quill.bubble.css", - "node_modules/quill/dist/quill.snow.css" + "node_modules/quill/dist/quill.snow.css", + "node_modules/quill-emoji/dist/quill-emoji.css" ], "scripts": [ "node_modules/marked/lib/marked.js", @@ -40,7 +41,8 @@ "node_modules/prismjs/plugins/line-highlight/prism-line-highlight.js", "node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js", "node_modules/katex/dist/katex.min.js", - "node_modules/quill/dist/quill.js" + "node_modules/quill/dist/quill.js", + "node_modules/quill-image-resize-module/image-resize.min.js" ] }, "configurations": { diff --git a/package-lock.json b/package-lock.json index 5ba02ab68..bb315dc06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,8 @@ "ngx-quill": "^14.2.0", "prismjs": "^1.23.0", "quill": "^1.3.7", + "quill-emoji": "^0.2.0", + "quill-image-resize-module": "^3.0.0", "rxjs": "^6.5.4", "tslib": "^2.0.0", "typescript-map": "0.0.7", @@ -3730,6 +3732,14 @@ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "dev": true }, + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "engines": { + "node": ">=0.4.2" + } + }, "node_modules/angular-tag-cloud-module": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/angular-tag-cloud-module/-/angular-tag-cloud-module-5.3.0.tgz", @@ -5253,6 +5263,43 @@ "node": ">=0.10.0" } }, + "node_modules/clean-css": { + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", + "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", + "dependencies": { + "commander": "2.8.x", + "source-map": "0.4.x" + }, + "bin": { + "cleancss": "bin/cleancss" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-css/node_modules/commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "dependencies": { + "graceful-readlink": ">= 1.0.0" + }, + "engines": { + "node": ">= 0.6.x" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dependencies": { + "amdefine": ">=0.0.4" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -6768,6 +6815,20 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, + "node_modules/emoji-data-css": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/emoji-data-css/-/emoji-data-css-1.0.1.tgz", + "integrity": "sha1-T5W0g5S1hXHtMoSs+nCepRGHX48=", + "dependencies": { + "clean-css": "^3.4.20", + "emoji-datasource": "^2.4.4" + } + }, + "node_modules/emoji-datasource": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/emoji-datasource/-/emoji-datasource-2.4.4.tgz", + "integrity": "sha1-uXrBiWvCCOzxgzVkogaHpSFdA4k=" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -8566,6 +8627,14 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "node_modules/fuse.js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", + "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==", + "engines": { + "node": ">=6" + } + }, "node_modules/gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -8762,6 +8831,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, + "node_modules/graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -14847,6 +14921,33 @@ "node": ">=0.10" } }, + "node_modules/quill-emoji": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/quill-emoji/-/quill-emoji-0.2.0.tgz", + "integrity": "sha512-0kqHKTFA9hk1Vf5g32KBm/NYZal6n9N/ATmk13Hka/XYsgrEIaShSR84B5VMB7bg5o9+TMeIzc+wey5OP7hv+A==", + "dependencies": { + "emoji-data-css": "^1.0.1", + "fuse.js": "^3.3.0" + }, + "peerDependencies": { + "quill": "^1.3.5" + } + }, + "node_modules/quill-image-resize-module": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quill-image-resize-module/-/quill-image-resize-module-3.0.0.tgz", + "integrity": "sha1-D9k3Rqg3M22VsvU2FAQWpiPHF3E=", + "dependencies": { + "lodash": "^4.17.4", + "quill": "^1.2.2", + "raw-loader": "^0.5.1" + } + }, + "node_modules/quill-image-resize-module/node_modules/raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=" + }, "node_modules/quill/node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -20718,8 +20819,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-4.2.0.tgz", "integrity": "sha512-qM4hpweuQ14ul8CU6LKpUWFZs6POUE7HZKdTllUrYuoZMrTpNB1XGelR0pweYzbfo4XRnUaO1NVgWhWOWiD5MA==", - "dev": true, - "requires": {} + "dev": true }, "@angular-eslint/eslint-plugin": { "version": "4.2.0", @@ -23243,8 +23343,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", - "dev": true, - "requires": {} + "dev": true }, "adjust-sourcemap-loader": { "version": "4.0.0", @@ -23342,15 +23441,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true, - "requires": {} + "dev": true }, "ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} + "dev": true }, "alphanum-sort": { "version": "1.0.2", @@ -23358,6 +23455,11 @@ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "dev": true }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, "angular-tag-cloud-module": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/angular-tag-cloud-module/-/angular-tag-cloud-module-5.3.0.tgz", @@ -24481,8 +24583,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz", "integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==", - "dev": true, - "requires": {} + "dev": true }, "class-utils": { "version": "0.3.6", @@ -24564,6 +24665,33 @@ } } }, + "clean-css": { + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", + "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", + "requires": { + "commander": "2.8.x", + "source-map": "0.4.x" + }, + "dependencies": { + "commander": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", + "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -25381,8 +25509,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz", "integrity": "sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==", - "dev": true, - "requires": {} + "dev": true }, "csso": { "version": "4.2.0", @@ -25795,6 +25922,20 @@ } } }, + "emoji-data-css": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/emoji-data-css/-/emoji-data-css-1.0.1.tgz", + "integrity": "sha1-T5W0g5S1hXHtMoSs+nCepRGHX48=", + "requires": { + "clean-css": "^3.4.20", + "emoji-datasource": "^2.4.4" + } + }, + "emoji-datasource": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/emoji-datasource/-/emoji-datasource-2.4.4.tgz", + "integrity": "sha1-uXrBiWvCCOzxgzVkogaHpSFdA4k=" + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -26288,8 +26429,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.3.tgz", "integrity": "sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ==", - "dev": true, - "requires": {} + "dev": true }, "eslint-scope": { "version": "5.1.1", @@ -27205,6 +27345,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "fuse.js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", + "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==" + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -27349,6 +27494,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -27855,8 +28005,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "ieee754": { "version": "1.2.1", @@ -28878,8 +29027,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.6.0.tgz", "integrity": "sha512-ELO9yf0cNqpzaNLsfFgXd/wxZVYkE2+ECUwhMHUD4PZ17kcsPsYsVyjquiRqyMn2jkd2sHt0IeMyAyq1MC23Fw==", - "dev": true, - "requires": {} + "dev": true }, "karma-source-map-support": { "version": "1.4.0", @@ -29948,8 +30096,7 @@ "ngx-matomo": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/ngx-matomo/-/ngx-matomo-0.1.4.tgz", - "integrity": "sha512-AKZMnJGyytZqAxuSh+k/AulyQhgqlnnsmtkfvHMJyNuh5g+wVpIbwac36RyeFU3El6INgZVso2CCLElV3bQnBQ==", - "requires": {} + "integrity": "sha512-AKZMnJGyytZqAxuSh+k/AulyQhgqlnnsmtkfvHMJyNuh5g+wVpIbwac36RyeFU3El6INgZVso2CCLElV3bQnBQ==" }, "ngx-matomo-v9": { "version": "0.3.0", @@ -31149,29 +31296,25 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz", "integrity": "sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-duplicates": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz", "integrity": "sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-empty": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz", "integrity": "sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-overridden": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz", "integrity": "sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==", - "dev": true, - "requires": {} + "dev": true }, "postcss-import": { "version": "14.0.0", @@ -31290,8 +31433,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -31326,8 +31468,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz", "integrity": "sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-normalize-display-values": { "version": "5.0.1", @@ -32030,6 +32171,32 @@ "fast-diff": "1.1.2" } }, + "quill-emoji": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/quill-emoji/-/quill-emoji-0.2.0.tgz", + "integrity": "sha512-0kqHKTFA9hk1Vf5g32KBm/NYZal6n9N/ATmk13Hka/XYsgrEIaShSR84B5VMB7bg5o9+TMeIzc+wey5OP7hv+A==", + "requires": { + "emoji-data-css": "^1.0.1", + "fuse.js": "^3.3.0" + } + }, + "quill-image-resize-module": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quill-image-resize-module/-/quill-image-resize-module-3.0.0.tgz", + "integrity": "sha1-D9k3Rqg3M22VsvU2FAQWpiPHF3E=", + "requires": { + "lodash": "^4.17.4", + "quill": "^1.2.2", + "raw-loader": "^0.5.1" + }, + "dependencies": { + "raw-loader": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", + "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=" + } + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -36430,8 +36597,7 @@ "version": "7.4.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", - "dev": true, - "requires": {} + "dev": true }, "xml2js": { "version": "0.4.23", diff --git a/package.json b/package.json index 56a69f779..5e9dd6689 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "ngx-quill": "^14.2.0", "prismjs": "^1.23.0", "quill": "^1.3.7", + "quill-emoji": "^0.2.0", + "quill-image-resize-module": "^3.0.0", "rxjs": "^6.5.4", "tslib": "^2.0.0", "typescript-map": "0.0.7", diff --git a/src/app/components/shared/write-comment/write-comment.component.html b/src/app/components/shared/write-comment/write-comment.component.html index 6b06f2fff..b931a1daa 100644 --- a/src/app/components/shared/write-comment/write-comment.component.html +++ b/src/app/components/shared/write-comment/write-comment.component.html @@ -46,26 +46,22 @@ </ars-row> <ars-row [height]="12"></ars-row> <ars-row *ngIf="enabled"> - <mat-tab-group> - <mat-tab label="Bearbeiten"> - <div #editorErrorLayer id="editorErrorLayer"></div> - <quill-editor #editor [maxLength]="10" placeholder="{{ 'comment-page.enter-comment' | translate }}" - [modules]="quillModules" (document:click)="onDocumentClick($event)"> - </quill-editor> - <div #tooltipContainer></div> - <div fxLayout="row" style="justify-content: space-between; padding: 0 5px"> - <span aria-hidden="true" style="font-size: 75%"> - {{ 'comment-page.Markdown-hint' | translate }} - </span> - <span aria-hidden="true" style="font-size: 75%"> - {{currentHTML.length}} / {{user.role === 3 ? 1000 : 500}} - </span> - </div> - </mat-tab> - <mat-tab label="Vorschau"> - <app-custom-markdown [data]="currentHTML" [isRawHTML]="true"></app-custom-markdown> - </mat-tab> - </mat-tab-group> + <div #editorErrorLayer id="editorErrorLayer"></div> + <quill-editor #editor + [maxLength]="user.role === 3 ? 1000 : 500" + placeholder="{{ 'comment-page.enter-comment' | translate }}" + [modules]="quillModules" + (document:click)="onDocumentClick($event)"> + </quill-editor> + <div #tooltipContainer></div> + <div fxLayout="row" style="justify-content: space-between; padding: 0 5px"> + <span aria-hidden="true" style="font-size: 75%"> + {{ 'comment-page.Markdown-hint' | translate }} + </span> + <span aria-hidden="true" style="font-size: 75%"> + {{currentHTML.length}} / {{user.role === 3 ? 1000 : 500}} + </span> + </div> </ars-row> <ars-row ars-flex-box *ngIf="enabled" class="spellcheck"> <ars-col> diff --git a/src/app/components/shared/write-comment/write-comment.component.scss b/src/app/components/shared/write-comment/write-comment.component.scss index 5b77b630a..2579d849f 100644 --- a/src/app/components/shared/write-comment/write-comment.component.scss +++ b/src/app/components/shared/write-comment/write-comment.component.scss @@ -258,6 +258,22 @@ $borderOffset: 2px; } .ql-tooltip { + &[data-mode=formula]::before { + --quill-tooltip-label: var(--quill-tooltip-label-formula); + } + &[data-mode=video]::before { + --quill-tooltip-label: var(--quill-tooltip-label-video); + } + &[data-mode=image]::before { + --quill-tooltip-label: var(--quill-tooltip-label-image); + } + &[data-mode=link]::before { + --quill-tooltip-label: var(--quill-tooltip-label-link); + } + &::before { + content: var(--quill-tooltip-label) !important; + } + color: var(--on-surface); background-color: var(--surface); @@ -266,9 +282,15 @@ $borderOffset: 2px; background: var(--primary); border-radius: 4px; color: var(--on-primary); + content: var(--quill-tooltip-action) !important; + } + + &.ql-editing .ql-action::after { + --quill-tooltip-action: var(--quill-tooltip-action-save); } .ql-remove::before { + content: var(--quill-tooltip-remove) !important; padding: 7px !important; background: var(--cancel); border-radius: 4px; diff --git a/src/app/components/shared/write-comment/write-comment.component.ts b/src/app/components/shared/write-comment/write-comment.component.ts index e1177d335..1d4d08f4e 100644 --- a/src/app/components/shared/write-comment/write-comment.component.ts +++ b/src/app/components/shared/write-comment/write-comment.component.ts @@ -8,28 +8,31 @@ import { EventService } from '../../../services/util/event.service'; import { QuillEditorComponent, QuillModules } from 'ngx-quill'; import { CreateCommentKeywords } from '../../../utils/create-comment-keywords'; import { Marks } from './write-comment.marks'; +import { LanguageService } from '../../../services/util/language.service'; +import Delta from 'quill-delta'; +import Quill from 'quill'; +import ImageResize from 'quill-image-resize-module'; +import 'quill-emoji/dist/quill-emoji.js'; -const participantOptions: QuillModules = { - toolbar: [ - ['bold', 'strike'], - ['blockquote', 'code-block'], - [{ list: 'ordered' }, { list: 'bullet' }], - ['link'] - ] -}; - -const moderatorOptions: QuillModules = { - toolbar: [ - ['bold', 'strike'], - ['blockquote', 'code-block'], - [{ header: 1 }, { header: 2 }], - [{ list: 'ordered' }, { list: 'bullet' }], - [{ indent: '-1' }, { indent: '+1' }], - [{ color: [] }], - [{ align: [] }], - ['link', 'image', 'video'] - ] -}; +Quill.register('modules/imageResize', ImageResize); + +const participantToolbar = [ + ['bold', 'strike'], + ['blockquote', 'code-block'], + [{ list: 'ordered' }, { list: 'bullet' }], + ['link', 'formula'] +]; + +const moderatorToolbar = [ + ['bold', 'strike'], + ['blockquote', 'code-block'], + [{ header: 1 }, { header: 2 }], + [{ list: 'ordered' }, { list: 'bullet' }], + [{ indent: '-1' }, { indent: '+1' }], + [{ color: [] }], + [{ align: [] }], + ['link', 'image', 'video', 'formula'] +]; @Component({ selector: 'app-write-comment', @@ -63,17 +66,42 @@ export class WriteCommentComponent implements OnInit, AfterViewInit { hasSpellcheckConfidence = true; newLang = 'auto'; marks: Marks; - quillModules = participantOptions; + quillModules: QuillModules = { + toolbar: { + container: participantToolbar, + handlers: { + image: () => this.handle('image'), + video: () => this.handle('video'), + link: () => this.handleLink(), + formula: () => this.handle('formula') + } + }, + 'emoji-toolbar': true, + 'emoji-textarea': true, + 'emoji-shortname': true, + imageResize: { + modules: ['Resize', 'DisplaySize', 'Toolbar'] + } + }; constructor(private notification: NotificationService, + private languageService: LanguageService, private translateService: TranslateService, public eventService: EventService, public languagetoolService: LanguagetoolService) { + this.languageService.langEmitter.subscribe(lang => { + this.translateService.use(lang); + this.updateCSSVariables(); + }); } ngOnInit(): void { this.translateService.use(localStorage.getItem('currentLang')); - this.quillModules = this.user && this.user.role > 0 ? moderatorOptions : participantOptions; + if (this.user && this.user.role > 0) { + this.quillModules.toolbar['container'] = moderatorToolbar; + } + this.translateService.use(localStorage.getItem('currentLang')); + this.updateCSSVariables(); } ngAfterViewInit() { @@ -83,12 +111,24 @@ export class WriteCommentComponent implements OnInit, AfterViewInit { this.currentText = e.text; }); this.editor.onEditorCreated.subscribe(_ => { - this.marks = new Marks(this.editorErrorLayer.nativeElement, this.tooltipContainer.nativeElement, - this.editor.editorElem.firstElementChild as HTMLDivElement); + this.marks = new Marks(this.editorErrorLayer.nativeElement, this.tooltipContainer.nativeElement, this.editor); this.syncErrorLayer(); setTimeout(() => this.syncErrorLayer(), 200); // animations? }); this.editor.onEditorChanged.subscribe(_ => { + const elem: HTMLDivElement = document.querySelector('div.ql-tooltip'); + if (elem) { // fix tooltip + setTimeout(() => { + const left = parseFloat(elem.style.left); + const right = left + elem.getBoundingClientRect().width; + const containerWidth = this.editor.editorElem.getBoundingClientRect().width; + if (left < 0) { + elem.style.left = '0'; + } else if (right > containerWidth) { + elem.style.left = (containerWidth - right + left) + 'px'; + } + }); + } this.syncErrorLayer(); this.marks.sync(); }); @@ -132,10 +172,8 @@ export class WriteCommentComponent implements OnInit, AfterViewInit { if (!this.marks) { return; } - const range = window.getSelection && window.getSelection().rangeCount > 0 ? window.getSelection().getRangeAt(0) : null; - const isClick = range && range.startContainer === range.endContainer && range.startOffset === range.endOffset && - (e.target as HTMLElement).contains(range.commonAncestorContainer); - this.marks.onClick(isClick ? range : null); + const range = this.editor.quillEditor.getSelection(false); + this.marks.onClick(range && range.length === 0 ? range.index : null); } maxLength(commentBody: HTMLDivElement, size: number): void { @@ -206,4 +244,71 @@ export class WriteCommentComponent implements OnInit, AfterViewInit { return true; } + private updateCSSVariables() { + const variables = [ + 'quill.tooltip-remove', 'quill.tooltip-action-save', 'quill.tooltip-action', 'quill.tooltip-label', + 'quill.tooltip-label-link', 'quill.tooltip-label-image', 'quill.tooltip-label-video', + 'quill.tooltip-label-formula' + ]; + for (const variable of variables) { + this.translateService.get(variable).subscribe(translation => { + document.body.style.setProperty('--' + variable.replace('.', '-'), JSON.stringify(translation)); + }); + } + } + + private handleLink(): void { + const quill = this.editor.quillEditor; + const selection = quill.getSelection(false); + if (!selection || !selection.length) { + return; + } + const tooltip = quill.theme.tooltip; + const originalSave = tooltip.save; + const originalHide = tooltip.hide; + tooltip.save = () => { + const value = tooltip.textbox.value; + if (value) { + const delta = new Delta() + .retain(selection.index) + .retain(selection.length, { link: value }); + quill.updateContents(delta); + tooltip.hide(); + } + }; + // Called on hide and save. + tooltip.hide = () => { + tooltip.save = originalSave; + tooltip.hide = originalHide; + tooltip.hide(); + }; + tooltip.edit('link'); + tooltip.textbox.value = quill.getText(selection.index, selection.length); + this.translateService.get('quill.tooltip-placeholder-link') + .subscribe(translation => tooltip.textbox.placeholder = translation); + } + + private handle(type: string): void { + const quill = this.editor.quillEditor; + const tooltip = quill.theme.tooltip; + const originalSave = tooltip.save; + const originalHide = tooltip.hide; + tooltip.save = () => { + const range = quill.getSelection(true); + const value = tooltip.textbox.value; + if (value) { + quill.insertEmbed(range.index, type, value, 'user'); + } + }; + // Called on hide and save. + tooltip.hide = () => { + tooltip.save = originalSave; + tooltip.hide = originalHide; + tooltip.hide(); + }; + tooltip.edit(type); + this.translateService.get('quill.tooltip-placeholder-' + type) + .subscribe(translation => tooltip.textbox.placeholder = translation); + } + } diff --git a/src/app/components/shared/write-comment/write-comment.marks.ts b/src/app/components/shared/write-comment/write-comment.marks.ts index ce937236f..b387e18f9 100644 --- a/src/app/components/shared/write-comment/write-comment.marks.ts +++ b/src/app/components/shared/write-comment/write-comment.marks.ts @@ -1,4 +1,5 @@ import { LanguagetoolResult } from '../../../services/http/languagetool.service'; +import { QuillEditorComponent } from 'ngx-quill'; export class Marks { @@ -6,7 +7,7 @@ export class Marks { constructor(private markContainer: HTMLDivElement, private tooltipContainer: HTMLDivElement, - private editor: HTMLDivElement) { + private editor: QuillEditorComponent) { } clear() { @@ -16,11 +17,12 @@ export class Marks { this.textErrors.length = 0; } - onClick(range: Range) { + onClick(index: number) { + const editorRect = this.editor.editorElem.firstElementChild.getBoundingClientRect(); for (const textError of this.textErrors) { textError.close(); - if (range) { - textError.open(range); + if (index !== null) { + textError.open(index, editorRect.y, editorRect.x); } } } @@ -31,10 +33,10 @@ export class Marks { if (op['insert']) { const len = op['insert'].length; for (const textError of this.textErrors) { - if (index > textError.endIndex) { + if (index > textError.startIndex + textError.markLength) { continue; } - textError.endIndex += len; + textError.markLength += len; if (index >= textError.startIndex) { continue; } @@ -47,26 +49,26 @@ export class Marks { const len = op['delete']; const endDelete = index + len; this.textErrors = this.textErrors.filter((textError) => { - if (index >= textError.endIndex) { + if (index >= textError.startIndex + textError.markLength) { return true; } if (index > textError.startIndex) { - if (endDelete < textError.endIndex) { - textError.endIndex -= len; + if (endDelete < textError.startIndex + textError.markLength) { + textError.markLength -= len; return true; } - textError.endIndex = index; + textError.markLength = index - textError.startIndex; return true; } if (endDelete < textError.startIndex) { textError.startIndex -= len; - textError.endIndex -= len; + textError.markLength -= len; return true; } - if (endDelete < textError.endIndex) { - const errLen = textError.endIndex - endDelete; + if (endDelete < textError.startIndex + textError.markLength) { + const errLen = textError.startIndex + textError.markLength - endDelete; textError.startIndex = index; - textError.endIndex = index + errLen; + textError.markLength = errLen; return true; } textError.remove(); @@ -76,26 +78,17 @@ export class Marks { console.log(op); } } - let currentElement: Node = this.editor; - let currentOffset = 0; - let depth = 0; - for (const error of this.textErrors) { - [currentElement, currentOffset, depth] = error.rebuildMark(depth, currentElement, currentOffset); - } + this.sync(); } buildErrors(initialText: string, wrongWords: string[], res: LanguagetoolResult): void { - let currentElement: Node = this.editor; - let currentOffset = 0; - let depth = 0; for (let i = 0; i < res.matches.length; i++) { const match = res.matches[i]; const foundWord = initialText.slice(match.offset, match.offset + match.length); if (!wrongWords.includes(foundWord)) { continue; } - const mark = new Mark(match.offset, match.offset + match.length, this.markContainer, this.tooltipContainer); - [currentElement, currentOffset, depth] = mark.rebuildMark(depth, currentElement, currentOffset); + const mark = new Mark(match.offset, match.length, this.markContainer, this.tooltipContainer, this.editor.quillEditor); mark.setSuggestions(res, i, () => { const index = this.textErrors.findIndex(elem => elem === mark); if (index >= 0) { @@ -105,121 +98,48 @@ export class Marks { }); this.textErrors.push(mark); } + this.sync(); } sync(): void { const parentRect = this.markContainer.getBoundingClientRect(); + const editorRect = this.editor.editorElem.firstElementChild.getBoundingClientRect(); for (const error of this.textErrors) { - error.syncMark(parentRect); + error.syncMark(parentRect, editorRect.y - parentRect.y); } } } -interface MarkedRange { - mark: HTMLSpanElement; - range: Range; -} - class Mark { - private marks: MarkedRange[] = []; - private textRange: Range; + private marks: HTMLSpanElement[] = []; private dropdown: HTMLDivElement; constructor(public startIndex, - public endIndex, + public markLength, private markContainer: HTMLDivElement, - private tooltipContainer: HTMLDivElement) { - } - - private static calcNodeTextSize(node: Node): number { - if (node instanceof HTMLBRElement) { - return 1; - } - return node.textContent.length; - } - - private static findNode(depth: number, - currentElement: Node, - currentOffset: number, - target: number, - onNodeLeave?: (Node) => void): [Node, number, number] { - while (currentElement.firstChild) { - currentElement = currentElement.firstChild; - depth += 1; - } - let length = Mark.calcNodeTextSize(currentElement); - while (currentOffset + length <= target) { - if (onNodeLeave) { - onNodeLeave(currentElement); - } - currentOffset += length; - const wasAlreadyBreak = currentElement instanceof HTMLBRElement; - let currentBefore = currentElement.parentElement; - currentElement = currentElement.nextSibling; - while (!currentElement) { - if (depth <= 1) { - throw new Error('The requested text position was not inside the container.'); - } - if (depth === 2 && !wasAlreadyBreak) { - currentOffset += 1; - } - currentElement = currentBefore.nextSibling; - currentBefore = currentBefore.parentElement; - depth -= 1; - } - while (currentElement.firstChild) { - currentElement = currentElement.firstChild; - depth += 1; - } - length = Mark.calcNodeTextSize(currentElement); - } - return [currentElement, currentOffset, depth]; - } - - isCollapsed(): boolean { - return this.marks.some(range => range.range.collapsed); + private tooltipContainer: HTMLDivElement, + private quillEditor: any) { } - rebuildMark(depth: number, currentElement: Node, currentOffset: number): [Node, number, number] { - this.textRange = document.createRange(); - for (const mark of this.marks) { - mark.mark.remove(); + syncMark(parentRect: DOMRect, offset: number) { + const boundaries = this.calculateBoundaries(); + for (let i = this.marks.length; i < boundaries.length; i++) { + const elem = document.createElement('span'); + this.markContainer.appendChild(elem); + this.marks.push(elem); } - this.marks.length = 0; - [currentElement, currentOffset, depth] = Mark.findNode(depth, currentElement, currentOffset, this.startIndex); - let rangeStart = this.startIndex - currentOffset; - rangeStart = rangeStart === -1 ? 0 : rangeStart; - this.textRange.setStart(currentElement, rangeStart); - [currentElement, currentOffset, depth] = Mark.findNode(depth, currentElement, currentOffset, this.endIndex - 1, - (node: Node) => { - const currentRange = this.textRange.cloneRange(); - if (currentRange.startContainer !== node) { - currentRange.setStart(node, 0); - } - currentRange.setEnd(node, node.textContent.length); - this.marks.push(this.createMarkedRange(currentRange)); - }); - const rangeEnd = this.endIndex - currentOffset; - this.textRange.setEnd(currentElement, rangeEnd); - if (this.textRange.startContainer !== this.textRange.endContainer) { - const currentRange = this.textRange.cloneRange(); - currentRange.setStart(currentElement, 0); - this.marks.push(this.createMarkedRange(currentRange)); - } else { - this.marks.push(this.createMarkedRange(this.textRange)); + for (let i = this.marks.length - 1; i >= boundaries.length; i--) { + this.marks[i].remove(); } - - return [currentElement, currentOffset, depth]; - } - - syncMark(parentRect: DOMRect) { - for (const mark of this.marks) { - const rect = mark.range.getBoundingClientRect(); - mark.mark.style.setProperty('--width', rect.width + 'px'); - mark.mark.style.setProperty('--height', rect.height + 'px'); - mark.mark.style.setProperty('--left', (rect.x - parentRect.x) + 'px'); - mark.mark.style.setProperty('--top', (rect.y - parentRect.y) + 'px'); + this.marks.length = boundaries.length; + for (let i = 0; i < this.marks.length; i++) { + const mark = this.marks[i]; + const rect = this.quillEditor.getBounds(boundaries[i][0], boundaries[i][1]); + mark.style.setProperty('--width', rect.width + 'px'); + mark.style.setProperty('--height', rect.height + 'px'); + mark.style.setProperty('--left', rect.left + 'px'); + mark.style.setProperty('--top', (rect.top + offset) + 'px'); } } @@ -228,24 +148,29 @@ class Mark { this.dropdown.remove(); } - open(range: Range): void { - for (const markedRange of this.marks) { - if (markedRange.range.compareBoundaryPoints(Range.END_TO_END, range) >= 0 && - markedRange.range.compareBoundaryPoints(Range.START_TO_START, range) < 0) { - this.dropdown.style.display = 'block'; - const rangeRect = markedRange.range.getBoundingClientRect(); - this.dropdown.style.left = (rangeRect.x + rangeRect.width / 2) + 'px'; - this.dropdown.style.top = rangeRect.y + 'px'; - this.tooltipContainer.appendChild(this.dropdown); - this.dropdown.style.transform = 'translateY(-' + this.dropdown.getBoundingClientRect().height + 'px)'; - return; - } + open(index: number, editorOffsetTop: number, editorOffsetLeft: number): void { + if (index < this.startIndex || index >= this.startIndex + this.markLength) { + return; + } + const boundaries = this.calculateBoundaries(); + const i = boundaries.findIndex(value => index >= value[0] && index < value[0] + value[1]); + this.dropdown.style.display = 'block'; + if (i >= 0) { + const rangeRect = this.marks[i].getBoundingClientRect(); + this.dropdown.style.left = (rangeRect.x + rangeRect.width / 2) + 'px'; + this.dropdown.style.top = rangeRect.y + 'px'; + } else { + const bounds = this.quillEditor.getBounds(index); + this.dropdown.style.left = (bounds.left + editorOffsetLeft) + 'px'; + this.dropdown.style.top = (bounds.top + editorOffsetTop) + 'px'; } + this.tooltipContainer.appendChild(this.dropdown); + this.dropdown.style.transform = 'translateY(-' + this.dropdown.getBoundingClientRect().height + 'px)'; } remove() { for (const mark of this.marks) { - mark.mark.remove(); + mark.remove(); } this.marks.length = 0; this.dropdown.remove(); @@ -269,13 +194,11 @@ class Mark { this.dropdown.append(dropdownElem); dropdownElem.addEventListener('click', () => { if (document.queryCommandSupported('insertText')) { - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(this.textRange); + this.quillEditor.setSelection(this.startIndex, this.markLength, 'user'); document.execCommand('insertText', false, suggestions[j].value); } else { - this.textRange.deleteContents(); - this.textRange.insertNode(document.createTextNode(suggestions[j].value)); + this.quillEditor.deleteText(this.startIndex, this.markLength, 'user'); + this.quillEditor.insertText(this.startIndex, suggestions[j].value, 'user'); } removeMark(); }); @@ -283,19 +206,22 @@ class Mark { } } - private createMarkedRange(range: Range): MarkedRange { - const rect = range.getBoundingClientRect(); - const parentRect = this.markContainer.getBoundingClientRect(); - const elem = document.createElement('span'); - elem.style.setProperty('--width', rect.width + 'px'); - elem.style.setProperty('--height', rect.height + 'px'); - elem.style.setProperty('--left', (rect.x - parentRect.x) + 'px'); - elem.style.setProperty('--top', (rect.y - parentRect.y) + 'px'); - this.markContainer.append(elem); - return { - mark: elem, - range - }; + private calculateBoundaries(): [start: number, length: number][] { + const text: string = this.quillEditor.getText(this.startIndex, this.markLength); + const bounds = []; + let i = text.indexOf('\n'); + let currentIndex = 0; + while (i >= 0) { + if (i > currentIndex) { + bounds.push([this.startIndex + currentIndex, i - currentIndex]); + } + currentIndex = i + 1; + i = text.indexOf('\n', currentIndex); + } + if (this.markLength > currentIndex) { + bounds.push([this.startIndex + currentIndex, this.markLength - currentIndex]); + } + return bounds; } } diff --git a/src/assets/i18n/creator/de.json b/src/assets/i18n/creator/de.json index 381960d8c..2a8f15709 100644 --- a/src/assets/i18n/creator/de.json +++ b/src/assets/i18n/creator/de.json @@ -238,6 +238,20 @@ "created-2": "« wurde erstellt.", "no-empty-name": "Gib einen Namen ein. Der Raum-Code wird generiert." }, + "quill": { + "tooltip-remove": "Löschen", + "tooltip-action-save": "Speichern", + "tooltip-action": "Editieren", + "tooltip-label": "URL besuchen:", + "tooltip-label-link": "Link eingeben:", + "tooltip-placeholder-link": "https://quilljs.com", + "tooltip-label-image": "Bild-URL eingeben:", + "tooltip-placeholder-image": "URL Adresse", + "tooltip-label-video": "Video-URL eingeben:", + "tooltip-placeholder-video": "URL Adresse", + "tooltip-label-formula": "Formel eingeben:", + "tooltip-placeholder-formula": "e=mc^2" + }, "room-page": { "a11y-add-moderator": "Fügt den eingegebenen Benutzer als Moderator hinzu.", "a11y-cloud_download": "Direktlink zur Sitzung in die Zwischenablage kopieren", diff --git a/src/assets/i18n/creator/en.json b/src/assets/i18n/creator/en.json index daf3b2b01..bcdbd67ad 100644 --- a/src/assets/i18n/creator/en.json +++ b/src/assets/i18n/creator/en.json @@ -239,6 +239,20 @@ "created-2": "« created.", "no-empty-name": "Please enter a name." }, + "quill": { + "tooltip-remove": "Delete", + "tooltip-action-save": "Save", + "tooltip-action": "Edit", + "tooltip-label": "Visit URL:", + "tooltip-label-link": "Enter link:", + "tooltip-placeholder-link": "https://quilljs.com", + "tooltip-label-image": "Enter image:", + "tooltip-placeholder-image": "Embed URL", + "tooltip-label-video": "Enter video:", + "tooltip-placeholder-video": "Embed URL", + "tooltip-label-formula": "Enter formula:", + "tooltip-placeholder-formula": "e=mc^2" + }, "room-page": { "a11y-add-moderator": "Add the entered User as Moderator", "a11y-cloud_download": "Copy link to session to the clipboard", diff --git a/src/assets/i18n/participant/de.json b/src/assets/i18n/participant/de.json index 7a63a6d40..84b38ce5d 100644 --- a/src/assets/i18n/participant/de.json +++ b/src/assets/i18n/participant/de.json @@ -248,6 +248,20 @@ "overview-questioners-tooltip": "Anzahl Fragensteller*innen", "questions-blocked": "Neue Fragen deaktiviert " }, + "quill": { + "tooltip-remove": "Löschen", + "tooltip-action-save": "Speichern", + "tooltip-action": "Editieren", + "tooltip-label": "URL besuchen:", + "tooltip-label-link": "Link eingeben:", + "tooltip-placeholder-link": "https://quilljs.com", + "tooltip-label-image": "Bild-URL eingeben:", + "tooltip-placeholder-image": "URL Adresse", + "tooltip-label-video": "Video-URL eingeben:", + "tooltip-placeholder-video": "URL Adresse", + "tooltip-label-formula": "Formel eingeben:", + "tooltip-placeholder-formula": "e=mc^2" + }, "tag-cloud": { "demo-data-topic": "Thema %d", "overview-question-topic-tooltip": "Anzahl Fragen mit diesem Thema", diff --git a/src/assets/i18n/participant/en.json b/src/assets/i18n/participant/en.json index 936d67e96..56949fa39 100644 --- a/src/assets/i18n/participant/en.json +++ b/src/assets/i18n/participant/en.json @@ -254,6 +254,20 @@ "overview-questioners-tooltip": "Number of questioners", "questions-blocked": "New questions blocked" }, + "quill": { + "tooltip-remove": "Delete", + "tooltip-action-save": "Save", + "tooltip-action": "Edit", + "tooltip-label": "Visit URL:", + "tooltip-label-link": "Enter link:", + "tooltip-placeholder-link": "https://quilljs.com", + "tooltip-label-image": "Enter image:", + "tooltip-placeholder-image": "Embed URL", + "tooltip-label-video": "Enter video:", + "tooltip-placeholder-video": "Embed URL", + "tooltip-label-formula": "Enter formula:", + "tooltip-placeholder-formula": "e=mc^2" + }, "tag-cloud": { "demo-data-topic": "Topic %d", "overview-question-topic-tooltip": "Number of questions with this topic", -- GitLab