From c242d7c4f7e2a5076c1759a2e7dd4b369263ad2d Mon Sep 17 00:00:00 2001 From: rhenck <richard.henck@iqb.hu-berlin.de> Date: Thu, 7 Oct 2021 12:59:57 +0200 Subject: [PATCH] [editor] Add new text editor module Based on TipTap which in turn is based on ProseMirror. No further assets are needed. --- package-lock.json | 489 +++++++++++++++++- package.json | 22 +- projects/editor/src/app/app.module.ts | 10 +- .../components/unit-view/page-view/indent.ts | 109 ++++ .../page-view/rich-text-editor.component.css | 29 ++ .../page-view/rich-text-editor.component.html | 188 +++++++ .../page-view/rich-text-editor.component.ts | 144 ++++++ projects/editor/src/app/dialog.service.ts | 29 +- 8 files changed, 981 insertions(+), 39 deletions(-) create mode 100644 projects/editor/src/app/components/unit-view/page-view/indent.ts create mode 100644 projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.css create mode 100644 projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.html create mode 100644 projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.ts diff --git a/package-lock.json b/package-lock.json index f97f4eb24..d30ff4c2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1788,6 +1788,11 @@ "schema-utils": "^2.7.0" } }, + "@luciusa/extension-font-size": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@luciusa/extension-font-size/-/extension-font-size-1.0.1.tgz", + "integrity": "sha512-5pQe4hAiH3KQRDzfKYAxwkwb+hgOBF9aPrPcFBn1rv0VfXV/h5yyWwPHAoFov4Pjo8zJOpGO3ZUFMvXHzuMLmQ==" + }, "@ngx-translate/core": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-13.0.0.tgz", @@ -1893,6 +1898,11 @@ "read-package-json-fast": "^2.0.1" } }, + "@popperjs/core": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", + "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==" + }, "@schematics/angular": { "version": "12.2.7", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-12.2.7.tgz", @@ -1904,19 +1914,233 @@ "jsonc-parser": "3.0.0" } }, - "@tinymce/tinymce-angular": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tinymce/tinymce-angular/-/tinymce-angular-4.2.4.tgz", - "integrity": "sha512-A+FIvhTahHaqDuvcXovuhzvpCR2yjUbPuF+Z/t/MnNaCv/9Y0w7W5Qgmo5wARIW+NM7o8msWcMSxOMK4xkv10Q==", + "@tiptap/core": { + "version": "2.0.0-beta.116", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.0-beta.116.tgz", + "integrity": "sha512-5x3HkT71IxF56lPEHPSTqOqkm1fVfHgVfyBbiNA3Qsz47npgJCDelDr6PHY8qlzxLCaN4dMDQUXEsvDDAiRouw==", "requires": { - "tslib": "^1.10.0" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } + "@types/prosemirror-commands": "^1.0.4", + "@types/prosemirror-inputrules": "^1.0.4", + "@types/prosemirror-keymap": "^1.0.4", + "@types/prosemirror-model": "^1.13.2", + "@types/prosemirror-schema-list": "^1.0.3", + "@types/prosemirror-state": "^1.2.7", + "@types/prosemirror-transform": "^1.1.4", + "@types/prosemirror-view": "^1.19.1", + "prosemirror-commands": "^1.1.10", + "prosemirror-inputrules": "^1.1.3", + "prosemirror-keymap": "^1.1.3", + "prosemirror-model": "^1.14.3", + "prosemirror-schema-list": "^1.1.6", + "prosemirror-state": "^1.3.4", + "prosemirror-transform": "^1.3.3", + "prosemirror-view": "^1.20.1" + } + }, + "@tiptap/extension-blockquote": { + "version": "2.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.0.0-beta.15.tgz", + "integrity": "sha512-Cso44KsYsqKqaNveQmx5KVaLy9krq5AzE9WhGVDBSFqWhvuIJkQYrTRBbOTfUDs/st9VuwJrbjTDD65ow50wEw==", + "requires": { + "prosemirror-inputrules": "^1.1.3" + } + }, + "@tiptap/extension-bold": { + "version": "2.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.0.0-beta.15.tgz", + "integrity": "sha512-jKyV6iiwhxwa0+7uuKD74jNDVNLNOS1GmU14MgaA95pY5e1fyaRBPPX8Gtt89niz2CLOY711AV17RPZTe/e60w==" + }, + "@tiptap/extension-bubble-menu": { + "version": "2.0.0-beta.38", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.38.tgz", + "integrity": "sha512-XBQemM+0w2VBe72e9AH8fql0ZcrhoDThxbldgq0Cx6Nr49ZmzuYYcFKQvTGTkxEcWSfXIsisSRbkaqUB9QM+ZQ==", + "requires": { + "prosemirror-state": "^1.3.4", + "prosemirror-view": "^1.20.1", + "tippy.js": "^6.3.1" + } + }, + "@tiptap/extension-bullet-list": { + "version": "2.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.0-beta.15.tgz", + "integrity": "sha512-5i44JzsZOh8Ci6CuYRQy6W3jCpYgX0+VuJKeHvZ6Aomy4Qqrtc9Jk43PBmCj91lNUUtH6Io9l+kDrLCumEFnEg==", + "requires": { + "prosemirror-inputrules": "^1.1.3" + } + }, + "@tiptap/extension-code": { + "version": "2.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.0.0-beta.16.tgz", + "integrity": "sha512-Kakg/RMiVrxjzIkLVDXtbCzRh/9W8dgSG04IhMZNOI8N9vWn8Z78jdUyxEEDTcL/JyWWcMxn9AsJw2U5ajO3pA==" + }, + "@tiptap/extension-code-block": { + "version": "2.0.0-beta.18", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.0.0-beta.18.tgz", + "integrity": "sha512-E2gz7ovl9nXLZzheqLyN3hi7A10fCaodDn4DvIl4wiEbKZpF7WFBNeb+FQetWNay9UWNeDO94SCX9+rT9H+yHA==", + "requires": { + "prosemirror-inputrules": "^1.1.3" + } + }, + "@tiptap/extension-color": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.0.0-beta.3.tgz", + "integrity": "sha512-xy079EyE/5X5BIyOjBrhUzx5yYmdixErEB/eF4Q7rrF/4yLxmt+P5q29nEejjaYEA0x5IPIX3iVeNZEFv6XO4A==" + }, + "@tiptap/extension-document": { + "version": "2.0.0-beta.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.0.0-beta.13.tgz", + "integrity": "sha512-nrufdKziA/wovaY4DjGkc8OGuIZi8CH8CW3+yYfeWbruwFKkyZHlZy9nplFWSEqBHPAeqD+px9r91yGMW3ontA==" + }, + "@tiptap/extension-dropcursor": { + "version": "2.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.0-beta.19.tgz", + "integrity": "sha512-rslIcVvD42NNh5sEbkCkG03DWMFBrS5KoK+lDOdIcC1DjmTtpVgcLvvE01btzaB3ljx+UVqI2Zaxa6VOiTeEMw==", + "requires": { + "@types/prosemirror-dropcursor": "^1.0.3", + "prosemirror-dropcursor": "^1.3.5" + } + }, + "@tiptap/extension-floating-menu": { + "version": "2.0.0-beta.32", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.32.tgz", + "integrity": "sha512-frroKOpE99fOdfJoWeM1ByWHCda+7Fe1n/Li6rdrmP9NdOS9FluG5lzzYf1qZ8wANfZhrXM8DOjXEXow/L3wkw==", + "requires": { + "prosemirror-state": "^1.3.4", + "prosemirror-view": "^1.20.1", + "tippy.js": "^6.3.1" + } + }, + "@tiptap/extension-font-family": { + "version": "2.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-2.0.0-beta.15.tgz", + "integrity": "sha512-ZNmrGVpKRXnGaMysRn7E8vkxNBNxdu1m3KXjDxARuGhwn17pBsdzDhRM3zD/4DBb45SZ1hy1EDtvXgurfRwiVQ==" + }, + "@tiptap/extension-gapcursor": { + "version": "2.0.0-beta.23", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.23.tgz", + "integrity": "sha512-yav1pAZ6YTZ4GzRaY3x0KutghLfC0Z6g/KXFzdE5KD5nxBhLUhid9rZ/kQiVPmZx2R0M/O5C+mdScYS2UzMmqA==", + "requires": { + "@types/prosemirror-gapcursor": "^1.0.4", + "prosemirror-gapcursor": "^1.2.0" + } + }, + "@tiptap/extension-hard-break": { + "version": "2.0.0-beta.20", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.0.0-beta.20.tgz", + "integrity": "sha512-tLZ53VMse2C1skj23tPFlq0wmdOCQ9vRsukz/KaR8VAFQBany0GOFmazu6QEFpC9+TI2gAckIGijEGFyP9QkMA==" + }, + "@tiptap/extension-heading": { + "version": "2.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.0.0-beta.15.tgz", + "integrity": "sha512-UoXDwEdCV9KiPh0wj0jj2Jt6VDqkoTaSU3d9bmEBLwg1Gjgbuv39JDst7oxSqbf9rgbl3txbeOy35wVBKe9CqA==", + "requires": { + "prosemirror-inputrules": "^1.1.3" + } + }, + "@tiptap/extension-highlight": { + "version": "2.0.0-beta.20", + "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.0.0-beta.20.tgz", + "integrity": "sha512-ClVlWkHwygoihsA+5i3nmmR2xyV50POOX4XZNHGLVG/E4pjst/Qcl5aH2Nu9jBfigg/TqwqF93m8NJ+Su0pvyg==" + }, + "@tiptap/extension-history": { + "version": "2.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.0.0-beta.16.tgz", + "integrity": "sha512-nrNwV8a7zUt1t2I/kPX5Y6N9vZ8mrugimJIQmPGIp/4mmw1SEUzkaPpIsv6+ELmqMHSDktQ0ofb3pXeWDXWZvw==", + "requires": { + "@types/prosemirror-history": "^1.0.3", + "prosemirror-history": "^1.2.0" + } + }, + "@tiptap/extension-horizontal-rule": { + "version": "2.0.0-beta.20", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.0-beta.20.tgz", + "integrity": "sha512-phvZy1ckl4FJ8k6cXce8wOXxv0c50HjU5sA7r6b8u7+Mj0Dc5DZE7enjiDGxoBLP6Yf2lzOgq1phH/r9EihDUQ==", + "requires": { + "prosemirror-state": "^1.3.4" + } + }, + "@tiptap/extension-italic": { + "version": "2.0.0-beta.15", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.0.0-beta.15.tgz", + "integrity": "sha512-ZCz1vCysLdvOUrwODuyBP0BDaemCLh6ib7qTYoSDKdive9kfn0Vc5Fg3o8xgHrtrUfwKIJz/sWOknjDEGIc9cw==" + }, + "@tiptap/extension-list-item": { + "version": "2.0.0-beta.14", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.0.0-beta.14.tgz", + "integrity": "sha512-t6xwEqP+d5443Ul2Jvqz9kXb3ro7bA7yY9HA0vskm3120WxxHW9jxgxZN+82Ot5Tm7nXOAlsN6vuqnt4idnxZQ==" + }, + "@tiptap/extension-ordered-list": { + "version": "2.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.0-beta.16.tgz", + "integrity": "sha512-3n0h5FBfQqBrN/zqF/Ngoyd1bZxeIRLwWI7ak4KulpvOg5V/yw3sw5CSxr2f13ZI9AgGaTq8yOsTYs9dkCCnsQ==", + "requires": { + "prosemirror-inputrules": "^1.1.3" + } + }, + "@tiptap/extension-paragraph": { + "version": "2.0.0-beta.17", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.0.0-beta.17.tgz", + "integrity": "sha512-qCQVCf9c2hgaeIdfy22PaoZyW5Vare/1aGkOEAaZma5RjrUbV9hrRKwoW9LsDjnh1EN1fIeKdg02yEhnHWtG8A==" + }, + "@tiptap/extension-strike": { + "version": "2.0.0-beta.17", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.0.0-beta.17.tgz", + "integrity": "sha512-+WRd0RuCK4+jFKNVN+4rHTa5VMqqGDO2uc+TknkqhFqWp/z96OAGlpHJOwPrnW1fLbpjEBBQIr1vVYSw6KgcZg==" + }, + "@tiptap/extension-subscript": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.0.0-beta.4.tgz", + "integrity": "sha512-eEjUXkgfeIBIgzdg3/GQGdta9Ww0Wwfiovn7ZvalRofRT4dnoiS0/83t1pQL81JT+ENow5jtx8RZHlaw/fMP4g==" + }, + "@tiptap/extension-superscript": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.0.0-beta.4.tgz", + "integrity": "sha512-rTQCnSnloSf6UN1y3zhu6j41MxrcCVWm5JIPX8VEt60WsOXJLAc/YJHLYi2FWhh/Psq8k78sPrmZbjYUrj3Dkw==" + }, + "@tiptap/extension-text": { + "version": "2.0.0-beta.13", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.0-beta.13.tgz", + "integrity": "sha512-0EtAwuRldCAoFaL/iXgkRepEeOd55rPg5N4FQUN1xTwZT7PDofukP0DG/2jff/Uj17x4uTaJAa9qlFWuNnDvjw==" + }, + "@tiptap/extension-text-align": { + "version": "2.0.0-beta.23", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.0.0-beta.23.tgz", + "integrity": "sha512-MB0BCjPvQGab4VYxJpuhuWHh897T1p/9qhLnYxRnAKGx7dC1c7jzCH6Ojq30oPgMndaAOibxGYQiB+dkCNzuyA==" + }, + "@tiptap/extension-text-style": { + "version": "2.0.0-beta.17", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.0.0-beta.17.tgz", + "integrity": "sha512-ZHdPihhKqwosVo6cydyo1L8OVHUXxVda3fojrMtJTkW6m+84cBUYe6lnqtmG1MyyNvSEfh45Ypcut/W2JGptHw==" + }, + "@tiptap/extension-underline": { + "version": "2.0.0-beta.16", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.0.0-beta.16.tgz", + "integrity": "sha512-N3wMfSJkn+A5HbuLSpbn1Zp9CWWLXfMexpVdg4eUwTMOgF/kpVRaPGCkBonhHH3NfWtT7QbSipdWc7DKpBb8Jw==" + }, + "@tiptap/starter-kit": { + "version": "2.0.0-beta.115", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.0.0-beta.115.tgz", + "integrity": "sha512-GDkjNkp1zRl8K2U9VaClCvmOAASOqCiJiKMccdWKer8uyEEmjpZVhkdMqIQBflX3z9cDxdbh9Ywkqoysg4NFkw==", + "requires": { + "@tiptap/core": "^2.0.0-beta.116", + "@tiptap/extension-blockquote": "^2.0.0-beta.15", + "@tiptap/extension-bold": "^2.0.0-beta.15", + "@tiptap/extension-bullet-list": "^2.0.0-beta.15", + "@tiptap/extension-code": "^2.0.0-beta.16", + "@tiptap/extension-code-block": "^2.0.0-beta.18", + "@tiptap/extension-document": "^2.0.0-beta.13", + "@tiptap/extension-dropcursor": "^2.0.0-beta.19", + "@tiptap/extension-gapcursor": "^2.0.0-beta.23", + "@tiptap/extension-hard-break": "^2.0.0-beta.20", + "@tiptap/extension-heading": "^2.0.0-beta.15", + "@tiptap/extension-history": "^2.0.0-beta.16", + "@tiptap/extension-horizontal-rule": "^2.0.0-beta.20", + "@tiptap/extension-italic": "^2.0.0-beta.15", + "@tiptap/extension-list-item": "^2.0.0-beta.14", + "@tiptap/extension-ordered-list": "^2.0.0-beta.16", + "@tiptap/extension-paragraph": "^2.0.0-beta.17", + "@tiptap/extension-strike": "^2.0.0-beta.17", + "@tiptap/extension-text": "^2.0.0-beta.13" } }, "@tootallnate/once": { @@ -2009,12 +2233,119 @@ "integrity": "sha512-F6S4Chv4JicJmyrwlDkxUdGNSplsQdGwp1A0AJloEVDirWdZOAiRHhovDlsFkKUrquUXhz1imJhXHsf59auyAg==", "dev": true }, + "@types/orderedmap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/orderedmap/-/orderedmap-1.0.0.tgz", + "integrity": "sha512-dxKo80TqYx3YtBipHwA/SdFmMMyLCnP+5mkEqN0eMjcTBzHkiiX0ES118DsjDBjvD+zeSsSU9jULTZ+frog+Gw==" + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/prosemirror-commands": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/prosemirror-commands/-/prosemirror-commands-1.0.4.tgz", + "integrity": "sha512-utDNYB3EXLjAfYIcRWJe6pn3kcQ5kG4RijbT/0Y/TFOm6yhvYS/D9eJVnijdg9LDjykapcezchxGRqFD5LcyaQ==", + "requires": { + "@types/prosemirror-model": "*", + "@types/prosemirror-state": "*", + "@types/prosemirror-view": "*" + } + }, + "@types/prosemirror-dropcursor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/prosemirror-dropcursor/-/prosemirror-dropcursor-1.0.3.tgz", + "integrity": "sha512-b0/8njnJ4lwyHKcGuCMf3x7r1KjxyugB1R/c2iMCjplsJHSC7UY9+OysqgJR5uUXRekUSGniiLgBtac/lvH6wg==", + "requires": { + "@types/prosemirror-state": "*" + } + }, + "@types/prosemirror-gapcursor": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.4.tgz", + "integrity": "sha512-9xKjFIG5947dzerFvkLWp6F53JwrUYoYwh3SgcTFEp8SbSfNNrez/PFYVZKPnoqPoaK5WtTdQTaMwpCV9rXQIg==", + "requires": { + "@types/prosemirror-model": "*", + "@types/prosemirror-state": "*" + } + }, + "@types/prosemirror-history": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/prosemirror-history/-/prosemirror-history-1.0.3.tgz", + "integrity": "sha512-5TloMDRavgLjOAKXp1Li8u0xcsspzbT1Cm9F2pwHOkgvQOz1jWQb2VIXO7RVNsFjLBZdIXlyfSLivro3DuMWXg==", + "requires": { + "@types/prosemirror-model": "*", + "@types/prosemirror-state": "*" + } + }, + "@types/prosemirror-inputrules": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/prosemirror-inputrules/-/prosemirror-inputrules-1.0.4.tgz", + "integrity": "sha512-lJIMpOjO47SYozQybUkpV6QmfuQt7GZKHtVrvS+mR5UekA8NMC5HRIVMyaIauJLWhKU6oaNjpVaXdw41kh165g==", + "requires": { + "@types/prosemirror-model": "*", + "@types/prosemirror-state": "*" + } + }, + "@types/prosemirror-keymap": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/prosemirror-keymap/-/prosemirror-keymap-1.0.4.tgz", + "integrity": "sha512-ycevwkqUh+jEQtPwqO7sWGcm+Sybmhu8MpBsM8DlO3+YTKnXbKA6SDz/+q14q1wK3UA8lHJyfR+v+GPxfUSemg==", + "requires": { + "@types/prosemirror-commands": "*", + "@types/prosemirror-model": "*", + "@types/prosemirror-state": "*", + "@types/prosemirror-view": "*" + } + }, + "@types/prosemirror-model": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/prosemirror-model/-/prosemirror-model-1.13.2.tgz", + "integrity": "sha512-a2rDB0aZ+7aIP7uBqQq1wLb4Hg4qqEvpkCqvhsgT/gG8IWC0peCAZfQ24sgTco0qSJLeDgIbtPeU6mgr869/kg==", + "requires": { + "@types/orderedmap": "*" + } + }, + "@types/prosemirror-schema-list": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/prosemirror-schema-list/-/prosemirror-schema-list-1.0.3.tgz", + "integrity": "sha512-uWybOf+M2Ea7rlbs0yLsS4YJYNGXYtn4N+w8HCw3Vvfl6wBAROzlMt0gV/D/VW/7J/LlAjwMezuGe8xi24HzXA==", + "requires": { + "@types/orderedmap": "*", + "@types/prosemirror-model": "*", + "@types/prosemirror-state": "*" + } + }, + "@types/prosemirror-state": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/prosemirror-state/-/prosemirror-state-1.2.7.tgz", + "integrity": "sha512-clJf5uw3/XQnBJtl2RqYXoLMGBySnLYl43xtDvFfQZKkLnnYcM1SDU8dcz7lWjl2Dm+H98RpLOl44pp7DYT+wA==", + "requires": { + "@types/prosemirror-model": "*", + "@types/prosemirror-transform": "*", + "@types/prosemirror-view": "*" + } + }, + "@types/prosemirror-transform": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/prosemirror-transform/-/prosemirror-transform-1.1.4.tgz", + "integrity": "sha512-HP1PauvkqSgDquZut8HaLOTUDQ6jja/LAy4OA7tTS1XG7wqRnX3gLUyEj0mD6vFd4y8BPkNddNdOh/BeGHlUjg==", + "requires": { + "@types/prosemirror-model": "*" + } + }, + "@types/prosemirror-view": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/prosemirror-view/-/prosemirror-view-1.19.1.tgz", + "integrity": "sha512-fyQ4NVxAdfISWrE2qT8cpZdosXoH/1JuVYMBs9CdaXPbvi/8R2L2tkkcMRM314piKrO8nfYH5OBZKzP2Ax3jtA==", + "requires": { + "@types/prosemirror-model": "*", + "@types/prosemirror-state": "*", + "@types/prosemirror-transform": "*" + } + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -8072,6 +8403,14 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "ngx-tiptap": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/ngx-tiptap/-/ngx-tiptap-3.0.4.tgz", + "integrity": "sha512-iU69pGqYoan5v9N8l92tpFSncB7x5mbXcJVwJwUDtEI922N+59cd4pw5y+NrN3er9E/HVAgOLfRB40TDHliwqQ==", + "requires": { + "tslib": "^2.2.0" + } + }, "nice-napi": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", @@ -8553,6 +8892,11 @@ "wcwidth": "^1.0.1" } }, + "orderedmap": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.1.tgz", + "integrity": "sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ==" + }, "original": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", @@ -12096,6 +12440,109 @@ "retry": "^0.12.0" } }, + "prosemirror-commands": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.1.10.tgz", + "integrity": "sha512-IWyBBXNAd44RM6NnBPljwq+/CM2oYCQJkF+YhKEAZNwzW0uFdGf4qComhjbKZzqFdu6Iub2ZhNsXgwPibA0lCQ==", + "requires": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "prosemirror-dropcursor": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.3.5.tgz", + "integrity": "sha512-tNUwcF2lPAkwKBZPZRtbxpwljnODRNZ3eiYloN1DSUqDjMT1nBZm0nejaEMS1TvNQ+3amibUSAiV4hX+jpASFA==", + "requires": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "prosemirror-gapcursor": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.2.0.tgz", + "integrity": "sha512-yCLy5+0rVqLir/KcHFathQj4Rf8aRHi80FmEfKtM0JmyzvwdomslLzDZ/pX4oFhFKDgjl/WBBBFNqDyNifWg7g==", + "requires": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "prosemirror-history": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.2.0.tgz", + "integrity": "sha512-B9v9xtf4fYbKxQwIr+3wtTDNLDZcmMMmGiI3TAPShnUzvo+Rmv1GiUrsQChY1meetHl7rhML2cppF3FTs7f7UQ==", + "requires": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "rope-sequence": "^1.3.0" + } + }, + "prosemirror-inputrules": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz", + "integrity": "sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==", + "requires": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "prosemirror-keymap": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.1.4.tgz", + "integrity": "sha512-Al8cVUOnDFL4gcI5IDlG6xbZ0aOD/i3B17VT+1JbHWDguCgt/lBHVTHUBcKvvbSg6+q/W4Nj1Fu6bwZSca3xjg==", + "requires": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "prosemirror-model": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.14.3.tgz", + "integrity": "sha512-yzZlBaSxfUPIIP6U5Edh5zKxJPZ5f7bwZRhiCuH3UYkWhj+P3d8swHsbuAMOu/iDatDc5J/Qs5Mb3++mZf+CvQ==", + "requires": { + "orderedmap": "^1.1.0" + } + }, + "prosemirror-schema-list": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.1.6.tgz", + "integrity": "sha512-aFGEdaCWmJzouZ8DwedmvSsL50JpRkqhQ6tcpThwJONVVmCgI36LJHtoQ4VGZbusMavaBhXXr33zyD2IVsTlkw==", + "requires": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "prosemirror-state": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.4.tgz", + "integrity": "sha512-Xkkrpd1y/TQ6HKzN3agsQIGRcLckUMA9u3j207L04mt8ToRgpGeyhbVv0HI7omDORIBHjR29b7AwlATFFf2GLA==", + "requires": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "prosemirror-transform": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.3.3.tgz", + "integrity": "sha512-9NLVXy1Sfa2G6qPqhWMkEvwQQMTw7OyTqOZbJaGQWsCeH3hH5Cw+c5eNaLM1Uu75EyKLsEZhJ93XpHJBa6RX8A==", + "requires": { + "prosemirror-model": "^1.0.0" + } + }, + "prosemirror-view": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.20.1.tgz", + "integrity": "sha512-djWORhy3a706mUH4A2dgEEV0IPZqQd1tFyz/ZVHJNoqhSgq82FwG6dq7uqHeUB2KdVSNfI2yc3rwfqlC/ll2pA==", + "requires": { + "prosemirror-model": "^1.14.3", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -12677,6 +13124,11 @@ "glob": "^7.1.3" } }, + "rope-sequence": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.2.tgz", + "integrity": "sha512-ku6MFrwEVSVmXLvy3dYph3LAMNS0890K7fabn+0YIRQ2T96T9F4gkFf0vf0WW0JUraNWwGRtInEpH7yO4tbQZg==" + }, "run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -13894,6 +14346,14 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "tippy.js": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.2.tgz", + "integrity": "sha512-35XVQI7Zl/jHZ51+8eHu/vVRXBjWYGobPm5G9FxOchj4r5dWhghKGS0nm0ARUKZTF96V7pPn7EbXS191NTwldw==", + "requires": { + "@popperjs/core": "^2.9.0" + } + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -14326,6 +14786,11 @@ "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", "dev": true }, + "w3c-keyname": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz", + "integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==" + }, "watchpack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.2.0.tgz", diff --git a/package.json b/package.json index 313f2a534..e5a313f60 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,27 @@ "@angular/router": "~12.2.7", "@ngx-translate/core": "^13.0.0", "@ngx-translate/http-loader": "^6.0.0", - "@tinymce/tinymce-angular": "^4.2.4", + "ngx-tiptap": "^3.0.4", + "@tiptap/core": "^2.0.0-beta.116", + "@tiptap/starter-kit": "^2.0.0-beta.115", + "@tiptap/extension-bubble-menu": "^2.0.0-beta.38", + "@tiptap/extension-bullet-list": "^2.0.0-beta.15", + "@tiptap/extension-color": "^2.0.0-beta.3", + "@tiptap/extension-floating-menu": "^2.0.0-beta.32", + "@tiptap/extension-font-family": "^2.0.0-beta.15", + "@tiptap/extension-heading": "^2.0.0-beta.15", + "@tiptap/extension-highlight": "^2.0.0-beta.20", + "@tiptap/extension-list-item": "^2.0.0-beta.14", + "@tiptap/extension-ordered-list": "^2.0.0-beta.16", + "@tiptap/extension-paragraph": "^2.0.0-beta.17", + "@tiptap/extension-strike": "^2.0.0-beta.17", + "@tiptap/extension-subscript": "^2.0.0-beta.4", + "@tiptap/extension-superscript": "^2.0.0-beta.4", + "@tiptap/extension-text-align": "^2.0.0-beta.23", + "@tiptap/extension-text-style": "^2.0.0-beta.17", + "@tiptap/extension-underline": "^2.0.0-beta.16", + "@luciusa/extension-font-size": "^1.0.1", + "prosemirror-state": "^1.3.4", "rxjs": "~6.6.0", "tslib": "^2.1.0", "zone.js": "~0.11.4" diff --git a/projects/editor/src/app/app.module.ts b/projects/editor/src/app/app.module.ts index 41f51f2ee..9a49daf71 100644 --- a/projects/editor/src/app/app.module.ts +++ b/projects/editor/src/app/app.module.ts @@ -5,6 +5,7 @@ import { CommonModule } from '@angular/common'; import { createCustomElement } from '@angular/elements'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { NgxTiptapModule } from 'ngx-tiptap'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; @@ -20,13 +21,14 @@ import { StaticCanvasOverlayComponent } from './components/unit-view/page-view/c import { DynamicCanvasOverlayComponent } from './components/unit-view/page-view/canvas/dynamic-canvas-overlay.component'; import { SharedModule } from '../../../common/app.module'; import { - ConfirmationDialog, TextEditDialog, MultilineTextEditDialog, RichTextEditDialogTinyMCE + ConfirmationDialog, TextEditDialog, MultilineTextEditDialog, RichTextEditDialog } from './dialog.service'; import { EditorTranslateLoader } from './editor-translate-loader'; import { ElementPropertiesComponent } from './components/unit-view/page-view/properties/element-properties.component'; import { SectionMenuComponent } from './components/unit-view/page-view/canvas/section-menu.component'; import { SectionStaticComponent } from './components/unit-view/page-view/canvas/section-static.component'; import { SectionDynamicComponent } from './components/unit-view/page-view/canvas/section-dynamic.component'; +import { RichTextEditorComponent } from './components/unit-view/page-view/rich-text-editor.component'; @NgModule({ declarations: [ @@ -41,11 +43,12 @@ import { SectionDynamicComponent } from './components/unit-view/page-view/canvas ConfirmationDialog, TextEditDialog, MultilineTextEditDialog, - RichTextEditDialogTinyMCE, + RichTextEditDialog, ElementPropertiesComponent, SectionMenuComponent, SectionStaticComponent, - SectionDynamicComponent + SectionDynamicComponent, + RichTextEditorComponent ], imports: [ BrowserModule, @@ -55,6 +58,7 @@ import { SectionDynamicComponent } from './components/unit-view/page-view/canvas MatButtonToggleModule, MatSlideToggleModule, MatMenuModule, + NgxTiptapModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, diff --git a/projects/editor/src/app/components/unit-view/page-view/indent.ts b/projects/editor/src/app/components/unit-view/page-view/indent.ts new file mode 100644 index 000000000..8bf89adec --- /dev/null +++ b/projects/editor/src/app/components/unit-view/page-view/indent.ts @@ -0,0 +1,109 @@ +import { Command, Extension } from '@tiptap/core'; +import { TextSelection, AllSelection, Transaction } from 'prosemirror-state'; + +export interface IndentOptions { + types: string[]; + minLevel: number; + maxLevel: number; + paddingMultiplier: number; +} + +declare module '@tiptap/core' { + interface Commands<ReturnType> { + indent: { + indent: () => ReturnType; + outdent: () => ReturnType; + }; + } +} + +export const Indent = Extension.create<IndentOptions>({ + name: 'indent', + + defaultOptions: { + types: ['listItem', 'paragraph'], + minLevel: 0, + maxLevel: 8, + paddingMultiplier: 10 + }, + + addGlobalAttributes() { + return [ + { + types: this.options.types, + attributes: { + indent: { + renderHTML: attributes => ( + { style: `padding-left: ${attributes.indent * this.options.paddingMultiplier}px` } + ), + parseHTML: element => { + const level = Number(element.getAttribute('style')); + return level && level > this.options.minLevel ? level : null; + } + } + } + } + ]; + }, + + addCommands() { + const setNodeIndentMarkup = (tr: Transaction, pos: number, delta: number): Transaction => { + const node = tr?.doc?.nodeAt(pos); + + if (node) { + const nextLevel = (node.attrs.indent || 0) + delta; + const { minLevel, maxLevel } = this.options; + const indent = nextLevel < minLevel ? minLevel : nextLevel > maxLevel ? maxLevel : nextLevel; + + if (indent !== node.attrs.indent) { + const nodeAttrs = { ...node.attrs, indent }; + return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks); + } + } + return tr; + }; + + const updateIndentLevel = (tr: Transaction, delta: number): Transaction => { + const { doc, selection } = tr; + + if (doc && selection && (selection instanceof TextSelection || selection instanceof AllSelection)) { + const { from, to } = selection; + doc.nodesBetween(from, to, (node, pos) => { + if (this.options.types.includes(node.type.name)) { + tr = setNodeIndentMarkup(tr, pos, delta); + return false; + } + + return true; + }); + } + + return tr; + }; + const applyIndent: (direction: number) => () => Command = + direction => () => ({ tr, state, dispatch }) => { + const { selection } = state; + tr = tr.setSelection(selection); + tr = updateIndentLevel(tr, direction); + + if (tr.docChanged) { + dispatch?.(tr); + return true; + } + + return false; + }; + + return { + indent: applyIndent(1), + outdent: applyIndent(-1) + }; + }, + + addKeyboardShortcuts() { + return { + Tab: () => this.editor.commands.indent(), + 'Shift-Tab': () => this.editor.commands.outdent() + }; + } +}); diff --git a/projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.css b/projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.css new file mode 100644 index 000000000..7f45b364d --- /dev/null +++ b/projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.css @@ -0,0 +1,29 @@ +div.ProseMirror { + height: 450px; + border: 1px solid; + overflow: auto; +} + +app-rich-text-editor button { + min-width: unset !important; + width: 50px; +} + +app-rich-text-editor .editor-button-formfield-align button { + vertical-align: top !important; + margin-top: 10px !important; +} + +app-rich-text-editor button.active { + background-color: lightgrey; +} + +app-rich-text-editor mat-form-field { + width: 120px; + margin: 0 10px; +} + +app-rich-text-editor .editor-control-line { + height: 50px; + margin-bottom: 5px; +} diff --git a/projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.html b/projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.html new file mode 100644 index 000000000..cd50bd84c --- /dev/null +++ b/projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.html @@ -0,0 +1,188 @@ +<div class="editor-control-line" fxLayout="row"> + <div class="editor-button-formfield-align"> + <button mat-icon-button matTooltip="Fett" [matTooltipPosition]="'above'" + [class.active]="editor.isActive('bold')" + (click)="toggleBold()"> + <mat-icon>format_bold</mat-icon> + </button> + <button mat-icon-button matTooltip="Kursiv" [matTooltipPosition]="'above'" + [class.active]="editor.isActive('italic')" + (click)="toggleItalic()"> + <mat-icon>format_italic</mat-icon> + </button> + <button mat-icon-button matTooltip="Unterstrichen" [matTooltipPosition]="'above'" + [class.active]="editor.isActive('underline')" + (click)="toggleUnderline()" style="text-decoration: underline"> + <mat-icon>format_underlined</mat-icon> + </button> + <button mat-icon-button matTooltip="Hochgestellt" [matTooltipPosition]="'above'" + [class.active]="editor.isActive('superscript')" + (click)="toggleSuperscript()"> + <mat-icon>superscript</mat-icon> + </button> + <button mat-icon-button matTooltip="Tiefgestellt" [matTooltipPosition]="'above'" + [class.active]="editor.isActive('subscript')" + (click)="toggleSubscript()"> + <mat-icon>subscript</mat-icon> + </button> + <button mat-icon-button matTooltip="Durchgestrichen" [matTooltipPosition]="'above'" + [class.active]="editor.isActive('strike')" + (click)="toggleStrike()"> + <mat-icon>strikethrough_s</mat-icon> + </button> + </div> + <div> + <mat-form-field> + <mat-label>Schriftgröße</mat-label> + <mat-select [value]="'16px'" (valueChange)="applyFontSize($event)"> + <mat-option value="8px">8px</mat-option> + <mat-option value="14px">14px</mat-option> + <mat-option value="16px">16px</mat-option> + <mat-option value="18px">18px</mat-option> + <mat-option value="24px">24px</mat-option> + <mat-option value="36px">36px</mat-option> + </mat-select> + </mat-form-field> + <mat-form-field> + <mat-label>Textfarbe</mat-label> + <input matInput type="color" + [value]="editor.getAttributes('textStyle').color" + (input)="applyColor($any($event.target).value)"> + </mat-form-field> + <mat-form-field> + <mat-label>Hintergrundfarbe</mat-label> + <input matInput type="color" + [value]="editor.getAttributes('highlight').color" + (input)="applyHighlight($any($event.target).value)"> + </mat-form-field> + </div> +</div> +<div class="editor-button-formfield-align" fxLayout="row" fxLayoutAlign="space-between"> + <div> + <button mat-icon-button matTooltip="Linksbündig" + [class.active]="editor.isActive({ textAlign: 'left' })" + (click)="alignText('left')"> + <mat-icon>format_align_left</mat-icon> + </button> + <button mat-icon-button matTooltip="Rechtsbündig" + [class.active]="editor.isActive({ textAlign: 'right' })" + (click)="alignText('right')"> + <mat-icon>format_align_right</mat-icon> + </button> + <button mat-icon-button matTooltip="Zentriert" + [class.active]="editor.isActive({ textAlign: 'center' })" + (click)="alignText('center')"> + <mat-icon>format_align_center</mat-icon> + </button> + <button mat-icon-button matTooltip="Blocksatz" + [class.active]="editor.isActive({ textAlign: 'justify' })" + (click)="alignText('justify')"> + <mat-icon>format_align_justify</mat-icon> + </button> + </div> + <div> + <button mat-icon-button matTooltip="Einrücken" (click)="indent()"> + <mat-icon>format_indent_increase</mat-icon> + </button> + <button mat-icon-button matTooltip="Einrückung verringern" (click)="outdent()"> + <mat-icon>format_indent_decrease</mat-icon> + </button> + </div> + <div> + <button mat-icon-button matTooltip="Aufzählung" + [class.active]="editor.isActive('bulletList')" + (click)="toggleBulletList()"> + <mat-icon>format_list_bulleted</mat-icon> + </button> + <button mat-icon-button matTooltip="Aufzählung (nummeriert)" + [class.active]="editor.isActive('orderedList')" + (click)="togleOrderedList()"> + <mat-icon>format_list_numbered</mat-icon> + </button> + </div> + <button mat-icon-button matTooltip="Sonderzeichen" [matMenuTriggerFor]="specialCharsMenu"> + <mat-icon>emoji_symbols</mat-icon> + </button> + <mat-menu #specialCharsMenu="matMenu" yPosition="above"> + <button mat-button (click)="insertSpecialChar('♀')">♀</button> + <button mat-button (click)="insertSpecialChar('♂')">♂</button> + <button mat-button (click)="insertSpecialChar('µ')">µ</button> + <br> + <button mat-button (click)="insertSpecialChar('ç')">ç</button> + <button mat-button (click)="insertSpecialChar('Ç')">Ç</button> + <button mat-button (click)="insertSpecialChar('Æ')">Æ</button> + <br> + <button mat-button (click)="insertSpecialChar('‹')">‹</button> + <button mat-button (click)="insertSpecialChar('›')">›</button> + <button mat-button (click)="insertSpecialChar('«')">«</button> + <button mat-button (click)="insertSpecialChar('»')">»</button> + <br> + <button mat-button (click)="insertSpecialChar('¢')">¢</button> + <button mat-button (click)="insertSpecialChar('£')">£</button> + <button mat-button (click)="insertSpecialChar('¥')">¥</button> + <br> + <button mat-button (click)="insertSpecialChar('©')">©</button> + <button mat-button (click)="insertSpecialChar('®')">®</button> + <button mat-button (click)="insertSpecialChar('™')">™</button> + <br> + <button mat-button (click)="insertSpecialChar('≤')">≤</button> + <button mat-button (click)="insertSpecialChar('≥')">≥</button> + <button mat-button (click)="insertSpecialChar('⨍')">⨍</button> + <button mat-button (click)="insertSpecialChar('∑')">∑</button> + <button mat-button (click)="insertSpecialChar('∈')">∈</button> + <button mat-button (click)="insertSpecialChar('∉')">∉</button> + <button mat-button (click)="insertSpecialChar('√')">√</button> + <button mat-button (click)="insertSpecialChar('∞')">∞</button> + <br> + <button mat-button (click)="insertSpecialChar('♠')">♠</button> + <button mat-button (click)="insertSpecialChar('♣')">♣</button> + <button mat-button (click)="insertSpecialChar('♥')">♥</button> + <button mat-button (click)="insertSpecialChar('♦')">♦</button> + </mat-menu> + <mat-form-field> + <mat-label>Ãœberschrift</mat-label> + <mat-select [value]="editor.getAttributes('heading').level?.toString() || ''" + (valueChange)="toggleHeading($event)"> + <mat-option value="1">H1</mat-option> + <mat-option value="2">H2</mat-option> + <mat-option value="3">H3</mat-option> + <mat-option value="4">H4</mat-option> + <mat-option value="">Paragraph</mat-option> + </mat-select> + </mat-form-field> + <button mat-icon-button [matMenuTriggerFor]="optionsMenu"> + <mat-icon>more_vert</mat-icon> + </button> + <mat-menu #optionsMenu="matMenu"> + <mat-form-field> + <mat-label>Aufzählungszeichen</mat-label> + <mat-select [value]="'disc'" (click)="$any($event).stopPropagation()" + (valueChange)="applyListStyle('bulletList', $event)"> + <mat-option value="disc">disc</mat-option> + <mat-option value="circle">circle</mat-option> + <mat-option value="square">square</mat-option> + </mat-select> + </mat-form-field> + <mat-form-field> + <mat-label>Aufzählungsnummerierung</mat-label> + <mat-select [value]="'decimal'" (click)="$any($event).stopPropagation()" + (valueChange)="applyListStyle('orderedList', $event)"> + <mat-option value="decimal">decimal</mat-option> + <mat-option value="lower-latin">lower-latin</mat-option> + <mat-option value="upper-latin">upper-latin</mat-option> + <mat-option value="lower-roman">lower-roman</mat-option> + <mat-option value="upper-roman">upper-roman</mat-option> + <mat-option value="lower-greek">lower-greek</mat-option> + </mat-select> + </mat-form-field> + <mat-form-field> + <mat-label>Absatzabstand</mat-label> + <mat-select [value]="'10'" (valueChange)="applyParagraphStyle($event)"> + <mat-option value="10">10px</mat-option> + <mat-option value="20">20px</mat-option> + </mat-select> + </mat-form-field> + </mat-menu> +</div> +<tiptap-editor [editor]="editor" [ngModel]="text" (ngModelChange)="textChange.emit($event)"> +</tiptap-editor> diff --git a/projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.ts b/projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.ts new file mode 100644 index 000000000..fa1c49480 --- /dev/null +++ b/projects/editor/src/app/components/unit-view/page-view/rich-text-editor.component.ts @@ -0,0 +1,144 @@ +import { + Component, EventEmitter, Input, Output, ViewEncapsulation, + AfterViewInit, OnInit +} from '@angular/core'; +import { ChainedCommands, Editor, Extension } from '@tiptap/core'; +import StarterKit from '@tiptap/starter-kit'; +import { Underline } from '@tiptap/extension-underline'; +import { Superscript } from '@tiptap/extension-superscript'; +import { Subscript } from '@tiptap/extension-subscript'; +import { TextStyle } from '@tiptap/extension-text-style'; +import { Color } from '@tiptap/extension-color'; +import { Highlight } from '@tiptap/extension-highlight'; +import { FontSize } from '@luciusa/extension-font-size'; +import { TextAlign } from '@tiptap/extension-text-align'; +import { Heading } from '@tiptap/extension-heading'; +import { Indent } from './indent'; + +@Component({ + selector: 'app-rich-text-editor', + templateUrl: './rich-text-editor.component.html', + styleUrls: ['./rich-text-editor.component.css'], + encapsulation: ViewEncapsulation.None +}) +export class RichTextEditorComponent implements AfterViewInit { + @Input() text!: string; + @Output() textChange = new EventEmitter<string>(); + + editor = new Editor({ + extensions: [StarterKit, Underline, Superscript, Subscript, + TextStyle, Color, FontSize, + Highlight.configure({ + multicolor: true + }), + TextAlign.configure({ + types: ['paragraph', 'heading'] + }), + Indent.configure({ + types: ['listItem', 'paragraph'], + minLevel: 0, + maxLevel: 4, + paddingMultiplier: 20 + }), + Heading.configure({ + levels: [1, 2, 3, 4] + }) + ] + }); + + ngAfterViewInit(): void { + this.editor.commands.focus(); + (this.editor.extensionManager.extensions + .filter(ext => ext.name === 'paragraph')[0] as Extension).options.HTMLAttributes = + { + style: 'margin: 10px 0' + }; + // Hack to apply style on first p-Element. All following paragraphs have this automatically + this.editor.commands.toggleNode('paragraph', 'heading'); + this.editor.commands.toggleNode('heading', 'paragraph'); + } + + toggleBold(): void { + this.editor.chain().toggleBold().focus().run(); + } + + toggleItalic(): void { + this.editor.chain().toggleItalic().focus().run(); + } + + toggleUnderline(): void { + this.editor.chain().toggleUnderline().focus().run(); + } + + toggleStrike(): void { + this.editor.commands.toggleStrike(); + } + + toggleSuperscript(): void { + this.editor.chain().toggleSuperscript().focus().run(); + } + + toggleSubscript(): void { + this.editor.chain().toggleSubscript().focus().run(); + } + + applyFontSize(size: string): void { + (this.editor.chain().setFontSize(size) as unknown as ChainedCommands).focus().run(); + } + + applyColor(color: string): void { + this.editor.chain().focus().setColor(color).run(); + } + + applyHighlight(color: string): void { + this.editor.chain().focus().toggleHighlight({ color: color }).run(); + } + + alignText(direction: string): void { + this.editor.chain().focus().setTextAlign(direction).run(); + } + + indent(): void { + this.editor.commands.indent(); + } + + outdent(): void { + this.editor.commands.outdent(); + } + + toggleBulletList(): void { + this.editor.chain().toggleBulletList().focus().run(); + } + + togleOrderedList(): void { + this.editor.chain().toggleOrderedList().focus().run(); + } + + applyListStyle(listType: string, style: string): void { + (this.editor.extensionManager.extensions + .filter(ext => ext.name === listType)[0] as Extension).options.HTMLAttributes = + { + style: `list-style-type:${style}` + }; + } + + toggleHeading(level?: string): void { + if (!level) { + this.editor.commands.toggleNode('heading', 'paragraph'); + } else { + this.editor.commands.toggleHeading({ level: Number(level) as 1 | 2 | 3 | 4 }); + } + } + + applyParagraphStyle(margin: number): void { + (this.editor.extensionManager.extensions + .filter(ext => ext.name === 'paragraph')[0] as Extension).options.HTMLAttributes = + { + style: `margin: ${margin}px 0` + }; + } + + insertSpecialChar(char: string): void { + this.editor.chain().insertContent(char).focus().run(); + } +} diff --git a/projects/editor/src/app/dialog.service.ts b/projects/editor/src/app/dialog.service.ts index 6759af289..bd3dbb5ed 100644 --- a/projects/editor/src/app/dialog.service.ts +++ b/projects/editor/src/app/dialog.service.ts @@ -38,12 +38,13 @@ export class DialogService { } showRichTextEditDialog(text: string): Observable<string> { - const dialogRef = this.dialog.open(RichTextEditDialogTinyMCE, { + const dialogRef = this.dialog.open(RichTextEditDialog, { width: '800px', - height: '600px', + height: '700px', data: { text: text - } + }, + autoFocus: false }); return dialogRef.afterClosed(); } @@ -108,25 +109,7 @@ export class MultilineTextEditDialog { selector: 'app-rich-text-edit-dialog-tinymce', template: ` <mat-dialog-content> - <editor - [(ngModel)]="data.text" - [init]="{ - height: 500, - menubar: false, - statusbar: false, - paste_as_text: true, - fontsize_formats: '8pt 10pt 12pt 14pt 16pt 18pt 24pt 36pt', - plugins: [ - 'charmap paste lists' - ], - toolbar: [ - 'newdocument | undo redo | selectall | removeformat | charmap', - 'bold italic underline strikethrough forecolor backcolor | formatselect | fontselect fontsizeselect |' + - 'alignleft aligncenter alignright alignjustify bullist numlist outdent indent', - ] - }" - ></editor> - + <app-rich-text-editor [(text)]="data.text"></app-rich-text-editor> </mat-dialog-content> <mat-dialog-actions> <button mat-button [mat-dialog-close]="data.text">Speichern</button> @@ -134,6 +117,6 @@ export class MultilineTextEditDialog { </mat-dialog-actions> ` }) -export class RichTextEditDialogTinyMCE { +export class RichTextEditDialog { constructor(@Inject(MAT_DIALOG_DATA) public data: { text: string }) { } } -- GitLab