diff --git a/src/Umbraco.Web.UI.Client/.vscode/settings.json b/src/Umbraco.Web.UI.Client/.vscode/settings.json index f4ca096129..bc10a8c132 100644 --- a/src/Umbraco.Web.UI.Client/.vscode/settings.json +++ b/src/Umbraco.Web.UI.Client/.vscode/settings.json @@ -6,7 +6,9 @@ "combobox", "ctrls", "devs", + "Dropcursor", "Elementable", + "Gapcursor", "iframes", "invariantable", "lucide", diff --git a/src/Umbraco.Web.UI.Client/index.html b/src/Umbraco.Web.UI.Client/index.html index b92c54f0d6..5b7473a4d2 100644 --- a/src/Umbraco.Web.UI.Client/index.html +++ b/src/Umbraco.Web.UI.Client/index.html @@ -6,6 +6,13 @@ Umbraco + diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index e58352be24..a11cae44d9 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -12,6 +12,21 @@ "./src/packages/*" ], "dependencies": { + "@tiptap/core": "^2.7.4", + "@tiptap/extension-image": "^2.7.4", + "@tiptap/extension-link": "^2.7.4", + "@tiptap/extension-placeholder": "^2.7.4", + "@tiptap/extension-subscript": "^2.7.4", + "@tiptap/extension-superscript": "^2.7.4", + "@tiptap/extension-table": "^2.7.4", + "@tiptap/extension-table-cell": "^2.7.4", + "@tiptap/extension-table-header": "^2.7.4", + "@tiptap/extension-table-row": "^2.7.4", + "@tiptap/extension-text-align": "^2.7.4", + "@tiptap/extension-text-style": "^2.8.0", + "@tiptap/extension-underline": "^2.7.4", + "@tiptap/pm": "^2.7.4", + "@tiptap/starter-kit": "^2.8.0", "@types/diff": "^5.2.1", "@types/dompurify": "^3.0.5", "@types/uuid": "^10.0.0", @@ -85,7 +100,7 @@ "typedoc": "^0.26.5", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "typescript-json-schema": "^0.64.0", + "typescript-json-schema": "^0.65.1", "vite": "^5.4.6", "vite-plugin-static-copy": "^1.0.6", "vite-tsconfig-paths": "^4.3.2", @@ -4612,6 +4627,11 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==" + }, "node_modules/@rollup/plugin-commonjs": { "version": "26.0.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz", @@ -6453,6 +6473,453 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@tiptap/core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.8.0.tgz", + "integrity": "sha512-xsqDI4BNzYRWRtBq7+/38ThhqEr7uG9Njip1x+9/wgR3vWPBFnBkYJTz6jSxS35NRE6BSnERm4/B/vrLuY1Hdw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.8.0.tgz", + "integrity": "sha512-m3CKrOIvV7fY1Ak2gYf5LkKiz6AHxHpg6wxfVaJvdBqXgLyVtHo552N+A4oSHOSRbB4AG9EBQ2NeBM8cdEQ4MA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.8.0.tgz", + "integrity": "sha512-U1YkZBxDkSLNvPNiqxB5g42IeJHr27C7zDb/yGQN2xL4UBeg4O9xVhCFfe32f6tLwivSL0dar4ScElpaCJuqow==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.8.0.tgz", + "integrity": "sha512-H4O2X0ozbc/ce9/XF1H98sqWVUdtt7jzy7hMBunwmY8ZxI4dHtcRkeg81CZbpKTqOqRrMCLWjE3M2tgiDXrDkA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/extension-list-item": "^2.7.0", + "@tiptap/extension-text-style": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.8.0.tgz", + "integrity": "sha512-VSFn3sFF6qPpOGkXFhik8oYRH5iByVJpFEFd/duIEftmS0MdPzkbSItOpN3mc9xsJ5dCX80LYaResSj5hr5zkA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.8.0.tgz", + "integrity": "sha512-POuA5Igx+Dto0DTazoBFAQTj/M/FCdkqRVD9Uhsxhv49swPyANTJRr05vgbgtHB+NDDsZfCawVh7pI0IAD/O0w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.8.0.tgz", + "integrity": "sha512-mp7Isx1sVc/ifeW4uW/PexGQ9exN3NRUOebSpnLfqXeWYk4y1RS1PA/3+IHkOPVetbnapgPjFx/DswlCP3XLjA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.8.0.tgz", + "integrity": "sha512-rAFvx44YuT6dtS1c+ALw0ROAGI16l5L1HxquL4hR1gtxDcTieST5xhw5bkshXlmrlfotZXPrhokzqA7qjhZtJw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.8.0.tgz", + "integrity": "sha512-Be1LWCmvteQInOnNVN+HTqc1XWsj1bCl+Q7et8qqNjtGtTaCbdCp8ppcH1SKJxNTM/RLUtPyJ8FDgOTj51ixCA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.8.0.tgz", + "integrity": "sha512-vqiIfviNiCmy/pJTHuDSCAGL2O4QDEdDmAvGJu8oRmElUrnlg8DbJUfKvn6DWQHNSQwRb+LDrwWlzAYj1K9u6A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.8.0.tgz", + "integrity": "sha512-4inWgrTPiqlivPmEHFOM5ck2UsmOsbKKPtqga6bALvWPmCv24S6/EBwFp8Jz4YABabXDnkviihmGu0LpP9D69w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-history": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.8.0.tgz", + "integrity": "sha512-u5YS0J5Egsxt8TUWMMAC3QhPZaak+IzQeyHch4gtqxftx96tprItY7AD/A3pGDF2uCSnN+SZrk6yVexm6EncDw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.8.0.tgz", + "integrity": "sha512-Sn/MI8WVFBoIYSIHA9NJryJIyCEzZdRysau8pC5TFnfifre0QV1ksPz2bgF+DyCD69ozQiRdBBHDEwKe47ZbfQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.7.4.tgz", + "integrity": "sha512-hUBN8q42pxrKR0erLTl5N0mq4HYP0aKIbZaxBui9DdlMaE1qkrm4bJ+Ori+OabUvhEbnky1HYhmrYaUDEPTfdQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.8.0.tgz", + "integrity": "sha512-PwwSE2LTYiHI47NJnsfhBmPiLE8IXZYqaSoNPU6flPrk1KxEzqvRI1joKZBmD9wuqzmHJ93VFIeZcC+kfwi8ZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.7.4.tgz", + "integrity": "sha512-nVzCEkK85JuNJH7oHW922V7LSjnZseihDsSCHCWjVNVgc+21s2ncGz16ZNOgiCOcnvxv7PtIB0EefXSuFZVPAQ==", + "dependencies": { + "linkifyjs": "^4.1.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.8.0.tgz", + "integrity": "sha512-o7OGymGxB0B9x3x2prp3KBDYFuBYGc5sW69O672jk8G52DqhzzndgPnkk0qUn8nXAUKuDGbJmpmHVA2kagqnRg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.8.0.tgz", + "integrity": "sha512-sCvNbcTS1+5QTTXwUPFa10vf5I1pr8sGcOTIh0G+a5ZkS5+6FxT12k7VLzPt39QyNbOi+77U2o4Xr4XyaEkfSg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/extension-list-item": "^2.7.0", + "@tiptap/extension-text-style": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.8.0.tgz", + "integrity": "sha512-XgxxNNbuBF48rAGwv7/s6as92/xjm/lTZIGTq9aG13ClUKFtgdel7C33SpUCcxg3cO2WkEyllXVyKUiauFZw/A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-placeholder": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.7.4.tgz", + "integrity": "sha512-7MOA4z8M7tUu8G9eiMvnitLcrhZJb4Hak3VCWgU2Cl9SXPizgKuF5VHd5ESOaEhNRk5pktFDDvCX9PHD7ZayGg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.8.0.tgz", + "integrity": "sha512-ezkDiXxQ3ME/dDMMM7tAMkKRi6UWw7tIu+Mx7Os0z8HCGpVBk1gFhLlhEd8I5rJaPZr4tK1wtSehMA9bscFGQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-subscript": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.7.4.tgz", + "integrity": "sha512-EZLwt/u1PQcIVuXRA+Lq8zVuzLxajNiJi5C2XqwvyLhhNGySvYqWCy2Nr80dTiwOe+yZVr9gwVQOvOE53EHW2A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-superscript": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.7.4.tgz", + "integrity": "sha512-AFYvbVCkOsix+2QVTl036LJeMpNNJT/XOCnxcCaWUeVwNKxrLxlGLzwrNqCC7hW6eYd73/Ht4+mzGIAGM78PPA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.7.4.tgz", + "integrity": "sha512-zL9BKQFJDGkwKnr1MYzCfpBllhlL8pDR3Sf5WscbN66I+rXrAdpFl75AbWf7gE0Tk2YaJldshFkakgWN1tr+2A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table-cell": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.7.4.tgz", + "integrity": "sha512-8/mM0lv8k6dRBjGHNh9HIGNaRuq+A/7h699GC7A9xuE7R1/xjDMKRZpPTmvogqfAq2U6mH16oxr/KpBuixji1w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table-header": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.7.4.tgz", + "integrity": "sha512-ZChahHwx0WlPynbMc4zsIgAAOar695A1AYTkWes7Y454xJy1vkGw607w+DVEHCWxU5h943H2UF7DUgzQS9XbAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-table-row": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.7.4.tgz", + "integrity": "sha512-JxR6PdLiXUjW8VC7YdVSIvd8D7RKVOPPPK7cFrawxS4tKz+1temsK8hNZ3RKhajwS5ya4IRT+iKhSRetVnjuww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.8.0.tgz", + "integrity": "sha512-EDAdFFzWOvQfVy7j3qkKhBpOeE5thkJaBemSWfXI93/gMVc0ZCdLi24mDvNNgUHlT+RjlIoQq908jZaaxLKN2A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text-align": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.7.4.tgz", + "integrity": "sha512-/zJFhFko6yztjVlXL+Rpb4cpfSHydtFXkj+eto3Mjs0r+xzAsgP7WmQU2oTq482X1uvbfHD9u9SGSKH4jPcmig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.8.0.tgz", + "integrity": "sha512-jJp0vcZ2Ty7RvIL0VU6dm1y+fTfXq1lN2GwtYzYM0ueFuESa+Qo8ticYOImyWZ3wGJGVrjn7OV9r0ReW0/NYkQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.7.4.tgz", + "integrity": "sha512-1WT2ZHjBoyW6MzKrLC1v2KJszuozh6jzIbcabslRRNaEJFfsjIFgfU3TBpaXF+JKEBCi3h1JpWMgmtnr0puFVA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.8.0.tgz", + "integrity": "sha512-eMGpRooUMvKz/vOpnKKppApMSoNM325HxTdAJvTlVAmuHp5bOY5kyY1kfUlePRiVx1t1UlFcXs3kecFwkkBD3Q==", + "dependencies": { + "prosemirror-changeset": "^2.2.1", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.0", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.0", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.22.3", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.4.1", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.4.0", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.0", + "prosemirror-view": "^1.33.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.8.0.tgz", + "integrity": "sha512-r7UwaTrECkQoheWVZKFDqtL5tBx07x7IFT+prfgnsVlYFutGWskVVqzCDvD3BDmrg5PzeCWYZrQGlPaLib7tjg==", + "dependencies": { + "@tiptap/core": "^2.8.0", + "@tiptap/extension-blockquote": "^2.8.0", + "@tiptap/extension-bold": "^2.8.0", + "@tiptap/extension-bullet-list": "^2.8.0", + "@tiptap/extension-code": "^2.8.0", + "@tiptap/extension-code-block": "^2.8.0", + "@tiptap/extension-document": "^2.8.0", + "@tiptap/extension-dropcursor": "^2.8.0", + "@tiptap/extension-gapcursor": "^2.8.0", + "@tiptap/extension-hard-break": "^2.8.0", + "@tiptap/extension-heading": "^2.8.0", + "@tiptap/extension-history": "^2.8.0", + "@tiptap/extension-horizontal-rule": "^2.8.0", + "@tiptap/extension-italic": "^2.8.0", + "@tiptap/extension-list-item": "^2.8.0", + "@tiptap/extension-ordered-list": "^2.8.0", + "@tiptap/extension-paragraph": "^2.8.0", + "@tiptap/extension-strike": "^2.8.0", + "@tiptap/extension-text": "^2.8.0", + "@tiptap/pm": "^2.8.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -7871,6 +8338,10 @@ "resolved": "src/packages/relations", "link": true }, + "node_modules/@umbraco-backoffice/rte": { + "resolved": "src/packages/rte", + "link": true + }, "node_modules/@umbraco-backoffice/search": { "resolved": "src/packages/search", "link": true @@ -7895,10 +8366,6 @@ "resolved": "src/packages/templating", "link": true }, - "node_modules/@umbraco-backoffice/tiny-mce": { - "resolved": "src/packages/tiny-mce", - "link": true - }, "node_modules/@umbraco-backoffice/ufm": { "resolved": "src/packages/ufm", "link": true @@ -9851,8 +10318,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-hidden": { "version": "1.2.4", @@ -11412,6 +11878,11 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -12174,7 +12645,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "engines": { "node": ">=0.12" }, @@ -15985,11 +16455,15 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "dev": true, "dependencies": { "uc.micro": "^2.0.0" } }, + "node_modules/linkifyjs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz", + "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==" + }, "node_modules/lit": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.0.tgz", @@ -16363,7 +16837,6 @@ "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "dev": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -16639,8 +17112,7 @@ "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "dev": true + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" }, "node_modules/media-typer": { "version": "0.3.0", @@ -18228,6 +18700,11 @@ "node": ">=8" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==" + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -18959,6 +19436,193 @@ "node": ">= 6" } }, + "node_modules/prosemirror-changeset": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz", + "integrity": "sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz", + "integrity": "sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.1.tgz", + "integrity": "sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.3.2.tgz", + "integrity": "sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.4.1.tgz", + "integrity": "sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.4.0.tgz", + "integrity": "sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.2.tgz", + "integrity": "sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.0.tgz", + "integrity": "sha512-UziddX3ZYSYibgx8042hfGKmukq5Aljp2qoBiJRejD/8MH70siQNz5RB1TrdTPheqLMy4aCe4GYNF10/3lQS5g==", + "dependencies": { + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.20.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.4.tgz", + "integrity": "sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.22.3.tgz", + "integrity": "sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.3.tgz", + "integrity": "sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==", + "dependencies": { + "prosemirror-model": "^1.19.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.4.1.tgz", + "integrity": "sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz", + "integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.5.0.tgz", + "integrity": "sha512-VMx4zlYWm7aBlZ5xtfJHpqa3Xgu3b7srV54fXYnXgsAcIGRqKSrhiK3f89omzzgaAgAtDOV4ImXnLKhVfheVNQ==", + "dependencies": { + "prosemirror-keymap": "^1.1.2", + "prosemirror-model": "^1.8.1", + "prosemirror-state": "^1.3.1", + "prosemirror-transform": "^1.2.1", + "prosemirror-view": "^1.13.3" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-trailing-node/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz", + "integrity": "sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.34.2", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.2.tgz", + "integrity": "sha512-tPX/V2Xd70vrAGQ/V9CppJtPKnQyQMypJGlLylvdI94k6JaG+4P6fVmXPR1zc1eVTW0gq3c6zsfqwJKCRLaG9Q==", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -19050,7 +19714,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "dev": true, "engines": { "node": ">=6" } @@ -19917,6 +20580,11 @@ "rollup": "^1.9.2 || ^2.0.0" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -21516,18 +22184,18 @@ } }, "node_modules/typescript-json-schema": { - "version": "0.64.0", - "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.64.0.tgz", - "integrity": "sha512-Sew8llkYSzpxaMoGjpjD6NMFCr6DoWFHLs7Bz1LU48pzzi8ok8W+GZs9cG87IMBpC0UI7qwBMUI2um0LGxxLOg==", + "version": "0.65.1", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.65.1.tgz", + "integrity": "sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "@types/node": "^16.9.2", + "@types/node": "^18.11.9", "glob": "^7.1.7", "path-equal": "^1.2.5", "safe-stable-stringify": "^2.2.0", "ts-node": "^10.9.1", - "typescript": "~5.1.0", + "typescript": "~5.5.0", "yargs": "^17.1.1" }, "bin": { @@ -21535,10 +22203,13 @@ } }, "node_modules/typescript-json-schema/node_modules/@types/node": { - "version": "16.18.105", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.105.tgz", - "integrity": "sha512-w2d0Z9yMk07uH3+Cx0N8lqFyi3yjXZxlbYappPj+AsOlT02OyxyiuNoNHdGt6EuiSm8Wtgp2YV7vWg+GMFrvFA==", - "dev": true + "version": "18.19.53", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.53.tgz", + "integrity": "sha512-GLxgUgHhDKO1Edw9Q0lvMbiO/IQXJwJlMaqxSGBXMpPy8uhkCs2iiPFaB2Q/gmobnFkckD3rqTBMVjXdwq+nKg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/typescript-json-schema/node_modules/glob": { "version": "7.2.3", @@ -21561,18 +22232,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/typescript-json-schema/node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } + "node_modules/typescript-json-schema/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true }, "node_modules/typical": { "version": "4.0.0", @@ -21609,8 +22273,7 @@ "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" }, "node_modules/ufo": { "version": "1.5.4", @@ -22603,6 +23266,11 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/walkdir": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", @@ -23135,6 +23803,9 @@ "src/packages/relations": { "name": "@umbraco-backoffice/relation" }, + "src/packages/rte": { + "name": "@umbraco-backoffice/rte" + }, "src/packages/search": { "name": "@umbraco-backoffice/search" }, @@ -23154,7 +23825,8 @@ "name": "@umbraco-backoffice/templating" }, "src/packages/tiny-mce": { - "name": "@umbraco-backoffice/tiny-mce" + "name": "@umbraco-backoffice/tiny-mce", + "extraneous": true }, "src/packages/ufm": { "name": "@umbraco-backoffice/ufm" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 0282f30fcb..c236d460c6 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -93,7 +93,8 @@ "./template": "./dist-cms/packages/templating/templates/index.js", "./temporary-file": "./dist-cms/packages/core/temporary-file/index.js", "./themes": "./dist-cms/packages/core/themes/index.js", - "./tiny-mce": "./dist-cms/packages/tiny-mce/index.js", + "./tiny-mce": "./dist-cms/packages/rte/tiny-mce/index.js", + "./tiptap": "./dist-cms/packages/rte/tiptap/index.js", "./translation": "./dist-cms/packages/translation/index.js", "./tree": "./dist-cms/packages/core/tree/index.js", "./ufm": "./dist-cms/packages/ufm/index.js", @@ -117,6 +118,7 @@ "./external/router-slot": "./dist-cms/external/router-slot/index.js", "./external/rxjs": "./dist-cms/external/rxjs/index.js", "./external/tinymce": "./dist-cms/external/tinymce/index.js", + "./external/tiptap": "./dist-cms/external/tiptap/index.js", "./external/uui": "./dist-cms/external/uui/index.js", "./external/uuid": "./dist-cms/external/uuid/index.js" }, @@ -193,6 +195,21 @@ "npm": ">=10.1 < 11" }, "dependencies": { + "@tiptap/core": "^2.7.4", + "@tiptap/extension-image": "^2.7.4", + "@tiptap/extension-link": "^2.7.4", + "@tiptap/extension-placeholder": "^2.7.4", + "@tiptap/extension-subscript": "^2.7.4", + "@tiptap/extension-superscript": "^2.7.4", + "@tiptap/extension-table": "^2.7.4", + "@tiptap/extension-table-cell": "^2.7.4", + "@tiptap/extension-table-header": "^2.7.4", + "@tiptap/extension-table-row": "^2.7.4", + "@tiptap/extension-text-align": "^2.7.4", + "@tiptap/extension-text-style": "^2.8.0", + "@tiptap/extension-underline": "^2.7.4", + "@tiptap/pm": "^2.7.4", + "@tiptap/starter-kit": "^2.8.0", "@types/diff": "^5.2.1", "@types/dompurify": "^3.0.5", "@types/uuid": "^10.0.0", @@ -266,7 +283,7 @@ "typedoc": "^0.26.5", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", - "typescript-json-schema": "^0.64.0", + "typescript-json-schema": "^0.65.1", "vite": "^5.4.6", "vite-plugin-static-copy": "^1.0.6", "vite-tsconfig-paths": "^4.3.2", diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts index 5e6655468a..5a7588ee2f 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts @@ -33,6 +33,7 @@ const CORE_PACKAGES = [ import('../../packages/property-editors/umbraco-package.js'), import('../../packages/publish-cache/umbraco-package.js'), import('../../packages/relations/umbraco-package.js'), + import('../../packages/rte/umbraco-package.js'), import('../../packages/search/umbraco-package.js'), import('../../packages/settings/umbraco-package.js'), import('../../packages/static-file/umbraco-package.js'), @@ -40,7 +41,6 @@ const CORE_PACKAGES = [ import('../../packages/tags/umbraco-package.js'), import('../../packages/telemetry/umbraco-package.js'), import('../../packages/templating/umbraco-package.js'), - import('../../packages/tiny-mce/umbraco-package.js'), import('../../packages/translation/umbraco-package.js'), import('../../packages/ufm/umbraco-package.js'), import('../../packages/umbraco-news/umbraco-package.js'), diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index f35a6596ed..3a8de51a46 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -643,6 +643,8 @@ export default { a11yCreateItem: 'Opret element', a11yEdit: 'Rediger', a11yName: 'Navn', + rteParagraph: 'Udfold din kreativitet...', + rteHeading: 'Hvad skal overskriften være?', }, editcontenttype: { createListView: 'Opret brugerdefineret listevisning', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 042c78dec8..8f9dd5999a 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -664,6 +664,8 @@ export default { a11yCreateItem: 'Create item', a11yEdit: 'Edit', a11yName: 'Name', + rteParagraph: 'Write something amazing...', + rteHeading: "What's the title?", }, editcontenttype: { createListView: 'Create custom list view', @@ -2626,4 +2628,10 @@ export default { wordWrapConfigLabel: 'Word wrap', wordWrapConfigDescription: 'Enable word wrapping in the code editor.', }, + tiptap: { + extGroup_formatting: 'Text formatting', + extGroup_interactive: 'Interactive elements', + extGroup_media: 'Embeds and media', + extGroup_structure: 'Content structure', + } } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-figcaption.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-figcaption.extension.ts new file mode 100644 index 0000000000..23b188b7bf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-figcaption.extension.ts @@ -0,0 +1,40 @@ +import { Node } from '@tiptap/core'; + +export interface FigcaptionOptions { + /** + * HTML attributes to add to the image element. + * @default {} + * @example { class: 'foo' } + */ + HTMLAttributes: Record; +} + +export const Figcaption = Node.create({ + name: 'figcaption', + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + group: 'block', + + content: 'inline*', + + selectable: false, + + draggable: false, + + parseHTML() { + return [ + { + tag: 'figcaption', + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [this.name, HTMLAttributes, 0]; + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-figure.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-figure.extension.ts new file mode 100644 index 0000000000..0cae9f7b47 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-figure.extension.ts @@ -0,0 +1,52 @@ +import { mergeAttributes, Node } from '@tiptap/core'; + +export interface FigureOptions { + /** + * HTML attributes to add to the image element. + * @default {} + * @example { class: 'foo' } + */ + HTMLAttributes: Record; +} + +export const Figure = Node.create({ + name: 'figure', + group: 'block', + content: 'block+', + draggable: true, + selectable: true, + isolating: true, + atom: true, + + addAttributes() { + return { + figcaption: { + default: '', + }, + }; + }, + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + parseHTML() { + return [ + { + tag: 'figure', + getAttrs: (dom) => { + const figcaption = dom.querySelector('figcaption'); + return { + figcaption: figcaption?.textContent || '', + }; + }, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [this.name, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts new file mode 100644 index 0000000000..3a13add2cb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-embedded-media.extension.ts @@ -0,0 +1,66 @@ +import { mergeAttributes, Node } from '@tiptap/core'; + +export const umbEmbeddedMedia = Node.create({ + name: 'umbEmbeddedMedia', + group() { + return this.options.inline ? 'inline' : 'block'; + }, + inline() { + return this.options.inline; + }, + atom: true, + marks: '', + draggable: true, + selectable: true, + + addAttributes() { + return { + 'data-embed-constrain': { default: false }, + 'data-embed-height': { default: 240 }, + 'data-embed-url': { default: null }, + 'data-embed-width': { default: 360 }, + markup: { default: null }, + }; + }, + + parseHTML() { + return [{ tag: 'div', class: 'umb-embed-holder', getAttrs: (node) => ({ markup: node.innerHTML }) }]; + }, + + renderHTML({ HTMLAttributes }) { + const { markup, ...attrs } = HTMLAttributes; + const embed = document.createRange().createContextualFragment(markup); + return ['div', mergeAttributes({ class: 'umb-embed-holder' }, attrs), embed]; + }, + + addCommands() { + return { + setEmbeddedMedia: + (options) => + ({ commands }) => { + const attrs = { + markup: options.markup, + 'data-embed-url': options.url, + 'data-embed-width': options.width, + 'data-embed-height': options.height, + 'data-embed-constrain': options.constrain, + }; + return commands.insertContent({ type: this.name, attrs }); + }, + }; + }, +}); + +declare module '@tiptap/core' { + interface Commands { + umbEmbeddedMedia: { + setEmbeddedMedia: (options: { + markup: string; + url: string; + width?: string; + height?: string; + constrain?: boolean; + }) => ReturnType; + }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-image.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-image.extension.ts new file mode 100644 index 0000000000..81807bc9ad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-image.extension.ts @@ -0,0 +1,55 @@ +import Image from '@tiptap/extension-image'; + +export interface UmbImageAttributes { + src: string; + alt?: string; + title?: string; + width?: string; + height?: string; + loading?: string; + srcset?: string; + sizes?: string; + 'data-tmpimg'?: string; + 'data-udi'?: string; +} + +export const UmbImage = Image.extend({ + addAttributes() { + return { + ...this.parent?.(), + width: { + default: '100%', + }, + height: { + default: null, + }, + loading: { + default: null, + }, + srcset: { + default: null, + }, + sizes: { + default: null, + }, + 'data-tmpimg': { default: null }, + 'data-udi': { default: null }, + }; + }, +}); + +declare module '@tiptap/core' { + interface Commands { + umbImage: { + /** + * Add an image + * @param options The image attributes + * @example + * editor + * .commands + * .setImage({ src: 'https://tiptap.dev/logo.png', alt: 'tiptap', title: 'tiptap logo' }) + */ + setImage: (options: UmbImageAttributes) => ReturnType; + }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-link.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-link.extension.ts new file mode 100644 index 0000000000..9ba9433993 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-umb-link.extension.ts @@ -0,0 +1,55 @@ +import Link from '@tiptap/extension-link'; + +export const UmbLink = Link.extend({ + name: 'umbLink', + + addAttributes() { + return { + ...this.parent?.(), + 'data-anchor': { default: null }, + title: { default: null }, + type: { default: 'external' }, + }; + }, + + addOptions() { + return { + ...this.parent?.(), + HTMLAttributes: { + target: '', + 'data-router-slot': 'disabled', + }, + }; + }, + + addCommands() { + return { + setUmbLink: (attributes) => { + return ({ chain }) => { + return chain().setMark(this.name, attributes).setMeta('preventAutolink', true).run(); + }; + }, + unsetUmbLink: () => { + return ({ chain }) => { + return chain().unsetMark(this.name, { extendEmptyMarkRange: true }).setMeta('preventAutolink', true).run(); + }; + }, + }; + }, +}); + +declare module '@tiptap/core' { + interface Commands { + umbLink: { + setUmbLink: (options: { + type: string; + href: string; + 'data-anchor'?: string | null; + target?: string | null; + title?: string | null; + }) => ReturnType; + + unsetUmbLink: () => ReturnType; + }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts new file mode 100644 index 0000000000..16db5db676 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts @@ -0,0 +1,35 @@ +// REQUIRED EXTENSIONS +export * from '@tiptap/core'; +export { StarterKit } from '@tiptap/starter-kit'; +export { Placeholder } from '@tiptap/extension-placeholder'; +export { TextStyle } from '@tiptap/extension-text-style'; + +// OPTIONAL EXTENSIONS +export { Blockquote } from '@tiptap/extension-blockquote'; +export { Bold } from '@tiptap/extension-bold'; +export { BulletList } from '@tiptap/extension-bullet-list'; +export { Code } from '@tiptap/extension-code'; +export { CodeBlock } from '@tiptap/extension-code-block'; +export { Heading } from '@tiptap/extension-heading'; +export { HorizontalRule } from '@tiptap/extension-horizontal-rule'; +export { Image } from '@tiptap/extension-image'; +export { Italic } from '@tiptap/extension-italic'; +export { Link } from '@tiptap/extension-link'; +export { ListItem } from '@tiptap/extension-list-item'; +export { OrderedList } from '@tiptap/extension-ordered-list'; +export { Strike } from '@tiptap/extension-strike'; +export { Subscript } from '@tiptap/extension-subscript'; +export { Superscript } from '@tiptap/extension-superscript'; +export { Table } from '@tiptap/extension-table'; +export { TableCell } from '@tiptap/extension-table-cell'; +export { TableHeader } from '@tiptap/extension-table-header'; +export { TableRow } from '@tiptap/extension-table-row'; +export { TextAlign } from '@tiptap/extension-text-align'; +export { Underline } from '@tiptap/extension-underline'; + +// CUSTOM EXTENSIONS +export * from './extensions/tiptap-umb-embedded-media.extension.js'; +export * from './extensions/tiptap-figcaption.extension.js'; +export * from './extensions/tiptap-figure.extension.js'; +export * from './extensions/tiptap-umb-image.extension.js'; +export * from './extensions/tiptap-umb-link.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts index c8847d1eef..f3a3749350 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/browser-handlers.ts @@ -35,6 +35,7 @@ import { handlers as userHandlers } from './handlers/user/index.js'; import * as manifestsHandlers from './handlers/manifests.handlers.js'; import * as serverHandlers from './handlers/server.handlers.js'; import { handlers as documentBlueprintHandlers } from './handlers/document-blueprint/index.js'; +import { handlers as temporaryFileHandlers } from './handlers/temporary-file/index.js'; const handlers = [ ...configHandlers, @@ -72,6 +73,7 @@ const handlers = [ ...userGroupsHandlers, ...userHandlers, ...documentBlueprintHandlers, + ...temporaryFileHandlers, ...serverHandlers.serverInformationHandlers, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index f3c0ab8d4d..eec1070db2 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -934,7 +934,23 @@ export const data: Array = [ }, { name: 'Rich Text Editor', - id: 'dt-richTextEditor', + id: 'dt-richTextEditorTiptap', + parent: null, + editorAlias: 'Umbraco.RichText', + editorUiAlias: 'Umb.PropertyEditorUi.Tiptap', + hasChildren: false, + isFolder: false, + isDeletable: true, + canIgnoreStartNodes: false, + values: [ + { alias: 'dimensions', value: { height: 500 } }, + { alias: 'maxImageSize', value: 500 }, + { alias: 'ignoreUserStartNodes', value: false }, + ], + }, + { + name: 'Rich Text Editor (TinyMCE)', + id: 'dt-richTextEditorTinyMce', parent: null, editorAlias: 'Umbraco.RichText', editorUiAlias: 'Umb.PropertyEditorUi.TinyMCE', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts index 1011e3c467..0145765978 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts @@ -87,10 +87,34 @@ export const data: Array = [ id: 'all-properties-group-key', }, alias: 'richTextEditor', - name: 'Rich Text editor', + name: 'Rich Text editor (Tiptap)', description: 'Some description to test with a long description.', dataType: { - id: 'dt-richTextEditor', + id: 'dt-richTextEditorTiptap', + }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: true, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + { + id: '1', + container: { + id: 'all-properties-group-key', + }, + alias: 'richTextEditorTinyMce', + name: 'Rich Text editor (TinyMce)', + description: 'Some description to test with a long description.', + dataType: { + id: 'dt-richTextEditorTinyMce', }, variesByCulture: false, variesBySegment: false, @@ -1721,4 +1745,84 @@ export const data: Array = [ properties: [], containers: [], }, + { + allowedTemplates: [], + defaultTemplate: { id: 'all-rtes-document-type-id' }, + id: 'all-rtes-document-type-id', + alias: 'allRtesDocumentType', + name: 'All RTEs document type', + description: null, + icon: 'icon-document', + allowedAsRoot: true, + variesByCulture: false, + variesBySegment: false, + isElement: false, + hasChildren: false, + parent: null, + isFolder: false, + properties: [ + { + id: '1dd0d4d2-cda8-4ac2-affd-a69fc10382b1', + container: { id: 'the-simplest-document-type-id-container' }, + alias: 'tiptap', + name: 'Tiptap', + description: + 'This is to test the default configuration for the Tiptap editor.\n\nSearch for **dt-richTextEditorTiptap** in the codebase to find the configuration and add configuration values.', + dataType: { id: 'dt-richTextEditorTiptap' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: false, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + { + id: '2dd0d4d2-cda8-4ac2-affd-a69fc10382b1', + container: { id: 'the-simplest-document-type-id-container' }, + alias: 'tinymce', + name: 'TinyMCE', + description: ` +This is to test the default configuration of the TinyMCE editor. + +Search for **dt-richTextEditorTinyMce** in the codebase to find the configuration and add configuration values. + +**NB!** If this throws an error in console, make sure that \`@umbraco-cms/backoffice/block-rte\` is available in the importmap.`, + dataType: { id: 'dt-richTextEditorTinyMce' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: false, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + ], + containers: [ + { + id: 'the-simplest-document-type-id-container', + parent: null, + name: 'Content', + type: 'Group', + sortOrder: 0, + }, + ], + allowedDocumentTypes: [], + compositions: [], + cleanup: { + preventCleanup: false, + keepAllVersionsNewerThanDays: null, + keepLatestVersionPerDayForDays: null, + }, + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index 6c6c669d67..ffbeef1e24 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -860,4 +860,73 @@ export const data: Array = [ }, ], }, + { + urls: [ + { + culture: 'en-US', + url: '/', + }, + ], + template: null, + id: 'all-rtes-id', + parent: null, + documentType: { + id: 'all-rtes-document-type-id', + icon: 'icon-document', + }, + hasChildren: false, + noAccess: false, + isProtected: false, + isTrashed: false, + variants: [ + { + state: DocumentVariantStateModel.PUBLISHED, + publishDate: '2023-02-06T15:32:24.957009', + culture: null, + segment: null, + name: 'All RTEs', + createDate: '2023-02-06T15:32:05.350038', + updateDate: '2023-02-06T15:32:24.957009', + }, + ], + values: [ + { + alias: 'tiptap', + editorAlias: 'Umb.PropertyEditorUi.Tiptap', + culture: null, + segment: null, + value: { + blocks: undefined, + markup: ` +

+ Some value for the RTE with an external link and an internal link foo foo +

+

+ Jason +

+

End of test content

+ `, + }, + }, + { + alias: 'tinymce', + editorAlias: 'Umb.PropertyEditorUi.TinyMCE', + culture: null, + segment: null, + value: { + blocks: undefined, + markup: ` +

+ Some value for the RTE with an external link and an internal link foo foo +

+
Macro alias: TestMacro
+

+ Jason +

+

End of test content

+ `, + }, + }, + ], + }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-tree.manager.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-tree.manager.ts index fc6a0c7334..61ebfea7a5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-tree.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/utils/entity/entity-tree.manager.ts @@ -21,6 +21,18 @@ export class UmbMockEntityTreeManager { + const items = []; + let currentId: string | undefined = descendantId; + while (currentId) { + const item = this.#db.read(currentId); + if (!item) break; + items.push(item); + currentId = item.parent?.id; + } + return items.reverse(); + } + #pagedTreeResult({ items, skip, take }: { items: Array; skip: number; take: number }) { const paged = pagedResult(items, skip, take); const treeItems = paged.items.map((item) => this.#treeItemMapper(item)); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/tree.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/tree.handlers.ts index e148fc83da..df3400b392 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/tree.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/document/tree.handlers.ts @@ -1,5 +1,6 @@ const { rest } = window.MockServiceWorker; import { umbDocumentMockDb } from '../../data/document/document.db.js'; +import type { GetTreeDocumentAncestorsResponse } from '@umbraco-cms/backoffice/external/backend-api'; import { UMB_SLUG } from './slug.js'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; @@ -19,4 +20,11 @@ export const treeHandlers = [ const response = umbDocumentMockDb.tree.getChildrenOf({ parentId, skip, take }); return res(ctx.status(200), ctx.json(response)); }), + + rest.get(umbracoPath(`/tree${UMB_SLUG}/ancestors`), (req, res, ctx) => { + const descendantId = req.url.searchParams.get('descendantId'); + if (!descendantId) return; + const response = umbDocumentMockDb.tree.getAncestorsOf({ descendantId }); + return res(ctx.status(200), ctx.json(response)); + }), ]; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/index.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/index.ts new file mode 100644 index 0000000000..57daf6e209 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/index.ts @@ -0,0 +1 @@ +export { handlers } from './temporary-file.handlers.js'; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts new file mode 100644 index 0000000000..0c1eeb3c05 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/temporary-file/temporary-file.handlers.ts @@ -0,0 +1,12 @@ +import { rest } from 'msw'; +import { umbracoPath } from '@umbraco-cms/backoffice/utils'; +import type { PostTemporaryFileResponse } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbId } from '@umbraco-cms/backoffice/id'; + +const UMB_SLUG = 'temporary-file'; + +export const handlers = [ + rest.post(umbracoPath(`/${UMB_SLUG}`), async (_req, res, ctx) => { + return res(ctx.delay(), ctx.status(201), ctx.text(UmbId.new())); + }), +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-custom-view/block-editor-custom-view.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-custom-view/block-editor-custom-view.extension.ts index d41818eae6..36f849fa13 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-custom-view/block-editor-custom-view.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-custom-view/block-editor-custom-view.extension.ts @@ -14,7 +14,7 @@ export interface ManifestBlockEditorCustomView extends ManifestElement } forBlockEditor - Declare if this Custom View only must appear at specific Block Editors. * @description Optional condition if you like this custom view to only appear at a specific type of Block Editor. * @example 'block-list' - * @example ['block-list', 'block-grid'] + * @example ['block-list', 'block-grid', 'block-rte'] */ forBlockEditor?: string | Array; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts index 7079b9bbdd..40ec86b23d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts @@ -171,7 +171,7 @@ export class UmbBlockGridManagerContext< originData: UmbBlockGridWorkspaceOriginData, ) { this.setOneLayout(layoutEntry, originData); - this.insertBlockData(layoutEntry, content, settings, originData); + this.insertBlockData(layoutEntry, content, settings); return true; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts index f381f7d00b..6d6b573564 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-entries.context.ts @@ -48,11 +48,7 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< }) .onSubmit(async (value, data) => { if (value?.create && data) { - const created = await this.create( - value.create.contentElementTypeKey, - {}, - data.originData as UmbBlockListWorkspaceOriginData, - ); + const created = await this.create(value.create.contentElementTypeKey, {}); if (created) { this.insert( created.layout, @@ -131,13 +127,9 @@ export class UmbBlockListEntriesContext extends UmbBlockEntriesContext< this._manager?.setLayouts(layouts); } - async create( - contentElementTypeKey: string, - partialLayoutEntry?: Omit, - originData?: UmbBlockListWorkspaceOriginData, - ) { + async create(contentElementTypeKey: string, partialLayoutEntry?: Omit) { await this._retrieveManager; - return this._manager?.create(contentElementTypeKey, partialLayoutEntry, originData); + return this._manager?.create(contentElementTypeKey, partialLayoutEntry); } // insert Block? diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts index ae26dd0360..b2b8d3d065 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/context/block-list-manager.context.ts @@ -21,13 +21,7 @@ export class UmbBlockListManagerContext< return this.#inlineEditingMode.getValue(); } - create( - contentElementTypeKey: string, - partialLayoutEntry?: Omit, - // This property is used by some implementations, but not used in this. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - originData?: UmbBlockListWorkspaceOriginData, - ) { + create(contentElementTypeKey: string, partialLayoutEntry?: Omit) { return super._createBlockData(contentElementTypeKey, partialLayoutEntry); } @@ -39,7 +33,7 @@ export class UmbBlockListManagerContext< ) { this._layouts.appendOneAt(layoutEntry, originData.index ?? -1); - this.insertBlockData(layoutEntry, content, settings, originData); + this.insertBlockData(layoutEntry, content, settings); return true; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts index 732f36f4f2..be1531ced1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/block-rte-entry/block-rte-entry.element.ts @@ -1,10 +1,14 @@ -import type { UmbBlockRteLayoutModel } from '../../types.js'; +import { UMB_BLOCK_RTE, type UmbBlockRteLayoutModel } from '../../types.js'; import { UmbBlockRteEntryContext } from '../../context/block-rte-entry.context.js'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { html, css, property, state, customElement } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UmbBlockEditorCustomViewProperties } from '@umbraco-cms/backoffice/block-custom-view'; +import type { + ManifestBlockEditorCustomView, + UmbBlockEditorCustomViewProperties, +} from '@umbraco-cms/backoffice/block-custom-view'; +import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils'; import '../ref-rte-block/index.js'; @@ -29,6 +33,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert @state() _showContentEdit = false; + @state() _hasSettings = false; @@ -47,6 +52,9 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert @state() _workspaceEditSettingsPath?: string; + @state() + _contentElementTypeAlias?: string; + @state() _blockViewProps: UmbBlockEditorCustomViewProperties = { contentKey: undefined!, @@ -72,6 +80,9 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert this._hasSettings = !!key; this.#updateBlockViewProps({ config: { ...this._blockViewProps.config, showSettingsEdit: !!key } }); }); + this.observe(this.#context.contentElementTypeAlias, (alias) => { + this._contentElementTypeAlias = alias; + }); this.observe( this.#context.blockType, (blockType) => { @@ -186,16 +197,26 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert .settings=${this._blockViewProps.settings}>`; } + readonly #filterBlockCustomViews = (manifest: ManifestBlockEditorCustomView) => { + const elementTypeAlias = this._contentElementTypeAlias ?? ''; + const isForBlockEditor = + !manifest.forBlockEditor || stringOrStringArrayContains(manifest.forBlockEditor, UMB_BLOCK_RTE); + const isForContentTypeAlias = + !manifest.forContentTypeAlias || stringOrStringArrayContains(manifest.forContentTypeAlias, elementTypeAlias); + return isForBlockEditor && isForContentTypeAlias; + }; + #renderBlock() { return html`
${this.#renderRefBlock()} + .filter=${this.#filterBlockCustomViews} + single> + ${this.#renderRefBlock()} + ${this._showContentEdit && this._workspaceEditContentPath ? html` @@ -207,9 +228,6 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert ` : ''} - this.#context.requestDelete()}> - -
`; @@ -219,7 +237,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert return this.#renderBlock(); } - static override styles = [ + static override readonly styles = [ UmbTextStyles, css` :host { @@ -227,6 +245,13 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert display: block; user-select: none; user-drag: auto; + white-space: nowrap; + } + :host(.ProseMirror-selectednode) { + umb-ref-rte-block { + cursor: not-allowed; + outline: 3px solid var(--uui-color-focus); + } } uui-action-bar { position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts index 9e32cdecb2..cf93788814 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts @@ -44,8 +44,11 @@ export class UmbRefRteBlockElement extends UmbLitElement { `; } - static override styles = [ + static override readonly styles = [ css` + :host { + display: block; + } uui-ref-node { min-height: var(--uui-size-16); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts index 13caae7e53..441f6c17fb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-entries.context.ts @@ -19,11 +19,11 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext< UmbBlockRteWorkspaceOriginData > { // - #catalogueModal: UmbModalRouteRegistrationController< + readonly #catalogueModal: UmbModalRouteRegistrationController< typeof UMB_BLOCK_CATALOGUE_MODAL.DATA, typeof UMB_BLOCK_CATALOGUE_MODAL.VALUE >; - #workspaceModal; + readonly #workspaceModal; // We will just say its always allowed for RTE for now: [NL] public readonly canCreate = new UmbBooleanState(true).asObservable(); @@ -51,15 +51,9 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext< value.create.contentElementTypeKey, // We can parse an empty object, cause the rest will be filled in by others. {} as any, - data.originData as UmbBlockRteWorkspaceOriginData, ); if (created) { - this.insert( - created.layout, - created.content, - created.settings, - data.originData as UmbBlockRteWorkspaceOriginData, - ); + this.insert(created.layout, created.content, created.settings); } else { throw new Error('Failed to create block'); } @@ -128,13 +122,9 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext< this._manager?.setLayouts(layouts); } - async create( - contentElementTypeKey: string, - partialLayoutEntry?: Omit, - originData?: UmbBlockRteWorkspaceOriginData, - ) { + async create(contentElementTypeKey: string, partialLayoutEntry?: Omit) { await this._retrieveManager; - return this._manager?.create(contentElementTypeKey, partialLayoutEntry, originData); + return this._manager?.create(contentElementTypeKey, partialLayoutEntry); } // insert Block? @@ -143,10 +133,9 @@ export class UmbBlockRteEntriesContext extends UmbBlockEntriesContext< layoutEntry: UmbBlockRteLayoutModel, content: UmbBlockDataModel, settings: UmbBlockDataModel | undefined, - originData: UmbBlockRteWorkspaceOriginData, ) { await this._retrieveManager; - return this._manager?.insert(layoutEntry, content, settings, originData) ?? false; + return this._manager?.insert(layoutEntry, content, settings) ?? false; } // create Block? diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts index 60d685ed92..876dfb06d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/context/block-rte-manager.context.ts @@ -1,7 +1,5 @@ import type { UmbBlockRteLayoutModel, UmbBlockRteTypeModel } from '../types.js'; -import type { UmbBlockRteWorkspaceOriginData } from '../index.js'; import type { UmbBlockDataModel } from '../../block/types.js'; -import type { Editor } from '@umbraco-cms/backoffice/external/tinymce'; import { UmbBlockManagerContext } from '@umbraco-cms/backoffice/block'; import '../components/block-rte-entry/index.js'; @@ -12,32 +10,11 @@ import '../components/block-rte-entry/index.js'; export class UmbBlockRteManagerContext< BlockLayoutType extends UmbBlockRteLayoutModel = UmbBlockRteLayoutModel, > extends UmbBlockManagerContext { - // - #editor?: Editor; - - setTinyMceEditor(editor: Editor) { - this.#editor = editor; - } - - getTinyMceEditor() { - return this.#editor; - } - removeOneLayout(contentKey: string) { this._layouts.removeOne(contentKey); } - getLayouts(): Array { - return this._layouts.getValue(); - } - - create( - contentElementTypeKey: string, - partialLayoutEntry?: Omit, - // This property is used by some implementations, but not used in this. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - originData?: UmbBlockRteWorkspaceOriginData, - ) { + create(contentElementTypeKey: string, partialLayoutEntry?: Omit) { const data = super._createBlockData(contentElementTypeKey, partialLayoutEntry); // Find block type. @@ -53,29 +30,10 @@ export class UmbBlockRteManagerContext< return data; } - insert( - layoutEntry: BlockLayoutType, - content: UmbBlockDataModel, - settings: UmbBlockDataModel | undefined, - originData: UmbBlockRteWorkspaceOriginData, - ) { - if (!this.#editor) return false; - + insert(layoutEntry: BlockLayoutType, content: UmbBlockDataModel, settings: UmbBlockDataModel | undefined) { this._layouts.appendOne(layoutEntry); - this.insertBlockData(layoutEntry, content, settings, originData); - - if (layoutEntry.displayInline) { - this.#editor.selection.setContent( - ``, - ); - } else { - this.#editor.selection.setContent( - ``, - ); - } - - this.#editor.fire('change'); + this.insertBlockData(layoutEntry, content, settings); return true; } @@ -85,13 +43,6 @@ export class UmbBlockRteManagerContext< * @internal */ public deleteLayoutElement(contentKey: string) { - if (!this.#editor) return; - - const blockElementsOfThiskey = this.#editor.dom.select( - `umb-rte-block[data-content-key='${contentKey}'], umb-rte-block-inline[data-content-key='${contentKey}']`, - ); - blockElementsOfThiskey.forEach((blockElement) => { - this.#editor?.dom.remove(blockElement); - }); + this.removeBlockKey(contentKey); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/types.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/types.ts index c97075ec68..f6e44b3beb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/types.ts @@ -2,6 +2,7 @@ import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; import type { UmbBlockLayoutBaseModel, UmbBlockValueType } from '@umbraco-cms/backoffice/block'; export const UMB_BLOCK_RTE_TYPE = 'block-rte-type'; +export const UMB_BLOCK_RTE = 'block-rte'; export interface UmbBlockRteTypeModel extends UmbBlockTypeBaseModel { displayInline: boolean; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts index 724a9cbbd8..e823da7bfd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts @@ -21,7 +21,6 @@ export abstract class UmbBlockManagerContext< BlockLayoutType extends UmbBlockLayoutBaseModel = UmbBlockLayoutBaseModel, BlockOriginDataType extends UmbBlockWorkspaceOriginData = UmbBlockWorkspaceOriginData, > extends UmbContextBase { - // get contentTypesLoaded() { return Promise.all(this.#contentTypeRequests); } @@ -40,7 +39,7 @@ export abstract class UmbBlockManagerContext< this.#variantId.setValue(variantId); } - #structures: Array = []; + readonly #structures: Array = []; #blockTypes = new UmbArrayState(>[], (x) => x.contentElementTypeKey); public readonly blockTypes = this.#blockTypes.asObservable(); @@ -54,13 +53,16 @@ export abstract class UmbBlockManagerContext< protected _layouts = new UmbArrayState(>[], (x) => x.contentKey); public readonly layouts = this._layouts.asObservable(); - #contents = new UmbArrayState(>[], (x) => x.key); + readonly #contents = new UmbArrayState(>[], (x) => x.key); public readonly contents = this.#contents.asObservable(); - #settings = new UmbArrayState(>[], (x) => x.key); + readonly #settings = new UmbArrayState(>[], (x) => x.key); public readonly settings = this.#settings.asObservable(); - #exposes = new UmbArrayState(>[], (x) => x.contentKey + '_' + x.culture + '_' + x.segment); + readonly #exposes = new UmbArrayState( + >[], + (x) => x.contentKey + '_' + x.culture + '_' + x.segment, + ); public readonly exposes = this.#exposes.asObservable(); setEditorConfiguration(configs: UmbPropertyEditorConfigCollection) { @@ -83,9 +85,15 @@ export abstract class UmbBlockManagerContext< setLayouts(layouts: Array) { this._layouts.setValue(layouts); } + getLayouts() { + return this._layouts.getValue(); + } setContents(contents: Array) { this.#contents.setValue(contents); } + getContents() { + return this.#contents.value; + } setSettings(settings: Array) { this.#settings.setValue(settings); } @@ -179,9 +187,7 @@ export abstract class UmbBlockManagerContext< getContentOf(contentKey: string) { return this.#contents.value.find((x) => x.key === contentKey); } - // TODO: [v15]: ignoring unused var here here to prevent a breaking change - // eslint-disable-next-line @typescript-eslint/no-unused-vars - setOneLayout(layoutData: BlockLayoutType, originData?: BlockOriginDataType) { + setOneLayout(layoutData: BlockLayoutType) { this._layouts.appendOne(layoutData); } setOneContent(contentData: UmbBlockDataModel) { @@ -220,7 +226,7 @@ export abstract class UmbBlockManagerContext< ); } settingsProperty(key: string, propertyAlias: string) { - this.#contents.asObservablePart( + this.#settings.asObservablePart( (source) => source.find((x) => x.key === key)?.values?.find((values) => values.alias === propertyAlias)?.value, ); } @@ -290,20 +296,12 @@ export abstract class UmbBlockManagerContext< originData: BlockOriginDataType, ): boolean; - protected insertBlockData( - layoutEntry: BlockLayoutType, - content: UmbBlockDataModel, - settings: UmbBlockDataModel | undefined, - // TODO: [v15]: ignoring unused var here here to prevent a breaking change - // eslint-disable-next-line @typescript-eslint/no-unused-vars - originData: BlockOriginDataType, - ) { + protected insertBlockData(layoutEntry: BlockLayoutType, content: UmbBlockDataModel, settings?: UmbBlockDataModel) { // Create content entry: if (layoutEntry.contentKey) { this.#contents.appendOne(content); } else { throw new Error('Cannot create block, missing contentKey'); - return false; } //Create settings entry: @@ -311,4 +309,8 @@ export abstract class UmbBlockManagerContext< this.#settings.appendOne(settings); } } + + protected removeBlockKey(contentKey: string) { + this.#contents.removeOne(contentKey); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index 78c2030ef9..f43e8bd519 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -256,8 +256,6 @@ export class UmbBlockWorkspaceContext { if (layoutData) { - this.#blockManager?.setOneLayout(layoutData, this.#modalContext?.data as UmbBlockWorkspaceData); + this.#blockManager?.setOneLayout(layoutData); } }, 'observeThisLayout', @@ -447,7 +447,7 @@ export class UmbBlockWorkspaceContext import("./icons/icon-block.js"), },{ +name: "icon-blockquote", + +path: () => import("./icons/icon-blockquote.js"), +},{ name: "icon-bluetooth", path: () => import("./icons/icon-bluetooth.js"), @@ -171,6 +175,10 @@ name: "icon-boat-shipping", path: () => import("./icons/icon-boat-shipping.js"), },{ +name: "icon-bold", + +path: () => import("./icons/icon-bold.js"), +},{ name: "icon-bones", path: () => import("./icons/icon-bones.js"), @@ -379,6 +387,10 @@ name: "icon-code", path: () => import("./icons/icon-code.js"), },{ +name: "icon-code-xml", + +path: () => import("./icons/icon-code-xml.js"), +},{ name: "icon-coffee", path: () => import("./icons/icon-coffee.js"), @@ -631,6 +643,10 @@ name: "icon-edit", path: () => import("./icons/icon-edit.js"), },{ +name: "icon-embed", + +path: () => import("./icons/icon-embed.js"), +},{ name: "icon-employee", legacy: true, path: () => import("./icons/icon-employee.js"), @@ -859,6 +875,18 @@ name: "icon-hard-drive", legacy: true, path: () => import("./icons/icon-hard-drive.js"), },{ +name: "icon-heading-1", + +path: () => import("./icons/icon-heading-1.js"), +},{ +name: "icon-heading-2", + +path: () => import("./icons/icon-heading-2.js"), +},{ +name: "icon-heading-3", + +path: () => import("./icons/icon-heading-3.js"), +},{ name: "icon-headphones", path: () => import("./icons/icon-headphones.js"), @@ -891,6 +919,10 @@ name: "icon-home", path: () => import("./icons/icon-home.js"), },{ +name: "icon-horizontal-rule", + +path: () => import("./icons/icon-horizontal-rule.js"), +},{ name: "icon-hourglass", path: () => import("./icons/icon-hourglass.js"), @@ -899,6 +931,10 @@ name: "icon-imac", legacy: true, path: () => import("./icons/icon-imac.js"), },{ +name: "icon-image-up", + +path: () => import("./icons/icon-image-up.js"), +},{ name: "icon-inbox-full", legacy: true, path: () => import("./icons/icon-inbox-full.js"), @@ -931,6 +967,10 @@ name: "icon-iphone", legacy: true, path: () => import("./icons/icon-iphone.js"), },{ +name: "icon-italic", + +path: () => import("./icons/icon-italic.js"), +},{ name: "icon-item-arrangement", legacy: true, path: () => import("./icons/icon-item-arrangement.js"), @@ -1676,7 +1716,7 @@ name: "icon-shipping", path: () => import("./icons/icon-shipping.js"), },{ name: "icon-shoe", - +legacy: true, path: () => import("./icons/icon-shoe.js"), },{ name: "icon-shopping-basket-alt-2", @@ -1811,6 +1851,18 @@ name: "icon-stream", legacy: true, path: () => import("./icons/icon-stream.js"), },{ +name: "icon-strikethrough", + +path: () => import("./icons/icon-strikethrough.js"), +},{ +name: "icon-subscript", + +path: () => import("./icons/icon-subscript.js"), +},{ +name: "icon-superscript", + +path: () => import("./icons/icon-superscript.js"), +},{ name: "icon-sunny", path: () => import("./icons/icon-sunny.js"), @@ -1831,6 +1883,10 @@ name: "icon-tab-key", path: () => import("./icons/icon-tab-key.js"), },{ +name: "icon-table", + +path: () => import("./icons/icon-table.js"), +},{ name: "icon-tag", path: () => import("./icons/icon-tag.js"), @@ -1859,6 +1915,22 @@ name: "icon-terminal", path: () => import("./icons/icon-terminal.js"), },{ +name: "icon-text-align-center", + +path: () => import("./icons/icon-text-align-center.js"), +},{ +name: "icon-text-align-justify", + +path: () => import("./icons/icon-text-align-justify.js"), +},{ +name: "icon-text-align-left", + +path: () => import("./icons/icon-text-align-left.js"), +},{ +name: "icon-text-align-right", + +path: () => import("./icons/icon-text-align-right.js"), +},{ name: "icon-theater", path: () => import("./icons/icon-theater.js"), @@ -1975,6 +2047,14 @@ name: "icon-undo", path: () => import("./icons/icon-undo.js"), },{ +name: "icon-underline", + +path: () => import("./icons/icon-underline.js"), +},{ +name: "icon-unlink", + +path: () => import("./icons/icon-unlink.js"), +},{ name: "icon-unlocked", path: () => import("./icons/icon-unlocked.js"), diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-blockquote.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-blockquote.ts new file mode 100644 index 0000000000..738040780d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-blockquote.ts @@ -0,0 +1,17 @@ +export default ` + + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-bold.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-bold.ts new file mode 100644 index 0000000000..48c0d84527 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-bold.ts @@ -0,0 +1,14 @@ +export default ` + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-code-xml.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-code-xml.ts new file mode 100644 index 0000000000..22e1bb6911 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-code-xml.ts @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-embed.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-embed.ts new file mode 100644 index 0000000000..93711a4dcd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-embed.ts @@ -0,0 +1,17 @@ +export default ` + + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-heading-1.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-heading-1.ts new file mode 100644 index 0000000000..d28662ea03 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-heading-1.ts @@ -0,0 +1,17 @@ +export default ` + + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-heading-2.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-heading-2.ts new file mode 100644 index 0000000000..ef8abb2e8f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-heading-2.ts @@ -0,0 +1,17 @@ +export default ` + + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-heading-3.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-heading-3.ts new file mode 100644 index 0000000000..7259f77719 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-heading-3.ts @@ -0,0 +1,18 @@ +export default ` + + + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-horizontal-rule.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-horizontal-rule.ts new file mode 100644 index 0000000000..09b103e193 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-horizontal-rule.ts @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-image-up.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-image-up.ts new file mode 100644 index 0000000000..9a1c61f69f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-image-up.ts @@ -0,0 +1,17 @@ +export default ` + + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-italic.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-italic.ts new file mode 100644 index 0000000000..561248265a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-italic.ts @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-science.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-science.ts index b45b7ed1d5..5c6f524a0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-science.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-science.ts @@ -1,4 +1,4 @@ -export default ` +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-subscript.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-subscript.ts new file mode 100644 index 0000000000..1af43054ac --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-subscript.ts @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-superscript.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-superscript.ts new file mode 100644 index 0000000000..e2b7852d44 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-superscript.ts @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-table.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-table.ts new file mode 100644 index 0000000000..283a8a901c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-table.ts @@ -0,0 +1,17 @@ +export default ` + + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-center.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-center.ts new file mode 100644 index 0000000000..086f4b297d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-center.ts @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-justify.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-justify.ts new file mode 100644 index 0000000000..13a803830a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-justify.ts @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-left.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-left.ts new file mode 100644 index 0000000000..37fde997fc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-left.ts @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-right.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-right.ts new file mode 100644 index 0000000000..587b13f99d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-text-align-right.ts @@ -0,0 +1,16 @@ +export default ` + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-underline.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-underline.ts new file mode 100644 index 0000000000..b7d3049c36 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-underline.ts @@ -0,0 +1,15 @@ +export default ` + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-unlink.ts b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-unlink.ts new file mode 100644 index 0000000000..7a308ee40d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/icon-registry/icons/icon-unlink.ts @@ -0,0 +1,19 @@ +export default ` + + + + + + + + +`; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts index 9fcf08824d..e89f8979aa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts @@ -1,6 +1,5 @@ import { UmbTemporaryFileRepository } from './temporary-file.repository.js'; import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; ///export type TemporaryFileStatus = 'success' | 'waiting' | 'error'; @@ -20,16 +19,11 @@ export interface UmbTemporaryFileModel { export class UmbTemporaryFileManager< UploadableItem extends UmbTemporaryFileModel = UmbTemporaryFileModel, > extends UmbControllerBase { - #temporaryFileRepository; + readonly #temporaryFileRepository = new UmbTemporaryFileRepository(this._host); - #queue = new UmbArrayState([], (item) => item.temporaryUnique); + readonly #queue = new UmbArrayState([], (item) => item.temporaryUnique); public readonly queue = this.#queue.asObservable(); - constructor(host: UmbControllerHost) { - super(host); - this.#temporaryFileRepository = new UmbTemporaryFileRepository(host); - } - async uploadOne(uploadableItem: UploadableItem): Promise { this.#queue.setValue([]); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/get-guid-from-udi.function.ts similarity index 59% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/utils.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/utils/get-guid-from-udi.function.ts index d1236320b4..92655aae55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/get-guid-from-udi.function.ts @@ -1,8 +1,10 @@ /** - * - * @param udi + * Get the guid from a UDI. + * @example getGuidFromUdi('umb://document/4f058f8b1f7e4f3e8b4b6b4b4b6b4b6b') // '4f058f8b-1f7e-4f3e-8b4b-6b4b4b6b4b6b' + * @param {string} udi The UDI to get the guid from. + * @returns {string} The guid from the UDI. */ -export function getGuid(udi: string) { +export function getGuidFromUdi(udi: string) { if (!udi.startsWith('umb://')) throw new Error('udi does not start with umb://'); const withoutScheme = udi.replace('umb://', ''); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/get-processed-image-url.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/get-processed-image-url.function.ts index 674e6de91b..51a43983d5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/get-processed-image-url.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/get-processed-image-url.function.ts @@ -1,18 +1,25 @@ -// TODO: This does not feel like a utility, but should instead become a repository/data-source/resource something in that direction [NL] +import type { GetImagingResizeUrlsData } from '@umbraco-cms/backoffice/external/backend-api'; /** - * Returns the URL of the processed image - * @param imagePath - * @param options + * Returns the URL of the processed image. + * @param {string} imagePath The path to the image. + * @param {GetImagingResizeUrlsData} options The options for resizing the image. + * @returns {Promise} The URL of the processed image. */ -export async function getProcessedImageUrl(imagePath: string, options: any) { +export async function getProcessedImageUrl(imagePath: string, options: GetImagingResizeUrlsData): Promise { if (!options) { return imagePath; } - // TODO => use backend cli when available - const result = await fetch('/umbraco/management/api/v1/images/GetProcessedImageUrl'); - const url = (await result.json()) as string; + const searchParams = new URLSearchParams({ + width: options.width?.toString() ?? '', + height: options.height?.toString() ?? '', + mode: options.mode ?? '', + }); + + // This should ideally use the ImagingService.getImagingResizeUrls method, but + // that would require the GUID of the media item, which is not available here. + const url = `${imagePath}?${searchParams.toString()}`; return url; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts index 483767c50a..7bbf20844e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts @@ -1,8 +1,10 @@ export * from './debounce/debounce.function.js'; export * from './direction/index.js'; export * from './download/blob-download.function.js'; +export * from './get-guid-from-udi.function.js'; export * from './get-processed-image-url.function.js'; export * from './math/math.js'; +export * from './media/image-size.function.js'; export * from './object/deep-merge.function.js'; export * from './pagination-manager/pagination.manager.js'; export * from './path/ensure-local-path.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.function.ts new file mode 100644 index 0000000000..8958da1c0b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/media/image-size.function.ts @@ -0,0 +1,43 @@ +/** + * Get the dimensions of an image from a URL. + * @param {string} url The URL of the image. It can be a local file (blob url) or a remote file. + * @param {{maxWidth?: number}} opts Options for the image size. + * @param {number} opts.maxWidth The maximum width of the image. If the image is wider than this, it will be scaled down to this width while keeping the aspect ratio. + * @returns {Promise<{width: number, height: number, naturalWidth: number, naturalHeight: number}>} The width and height of the image as downloaded from the URL. The width and height can differ from the natural numbers if maxImageWidth is given. + */ +export function imageSize( + url: string, + opts?: { maxWidth?: number }, +): Promise<{ width: number; height: number; naturalWidth: number; naturalHeight: number }> { + const img = new Image(); + + const promise = new Promise<{ width: number; height: number; naturalWidth: number; naturalHeight: number }>( + (resolve, reject) => { + img.onload = () => { + // Natural size is the actual image size regardless of rendering. + // The 'normal' `width`/`height` are for the **rendered** size. + const naturalWidth = img.naturalWidth; + const naturalHeight = img.naturalHeight; + let width = naturalWidth; + let height = naturalHeight; + + if (opts?.maxWidth && opts.maxWidth > 0 && width > opts?.maxWidth) { + const ratio = opts.maxWidth / naturalWidth; + width = opts.maxWidth; + height = Math.round(naturalHeight * ratio); + } + + // Resolve promise with the width and height + resolve({ width, height, naturalWidth, naturalHeight }); + }; + + // Reject promise on error + img.onerror = reject; + }, + ); + + // Setting the source makes it start downloading and eventually call `onload` + img.src = url; + + return promise; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/embedded-media/modal/embedded-media-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/embedded-media/modal/embedded-media-modal.element.ts index fcf5ff01a6..e91c24cbcd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/embedded-media/modal/embedded-media-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/embedded-media/modal/embedded-media-modal.element.ts @@ -1,8 +1,9 @@ import { UmbOEmbedRepository } from '../repository/oembed.repository.js'; import type { UmbEmbeddedMediaModalData, UmbEmbeddedMediaModalValue } from './embedded-media-modal.token.js'; import { css, html, unsafeHTML, when, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UUIButtonState, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-embedded-media-modal') @@ -27,6 +28,7 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement< override connectedCallback() { super.connectedCallback(); + if (this.data?.width) this._width = this.data.width; if (this.data?.height) this._height = this.data.height; if (this.data?.constrain) this.value = { ...this.value, constrain: this.data.constrain }; @@ -42,8 +44,8 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement< const { data } = await this.#oEmbedRepository.requestOEmbed({ url: this._url, - maxWidth: this._width, - maxHeight: this._height, + maxWidth: this._width > 0 ? this._width : undefined, + maxHeight: this._height > 0 ? this._height : undefined, }); if (data) { @@ -62,11 +64,13 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement< #onWidthChange(e: UUIInputEvent) { this._width = parseInt(e.target.value as string, 10); + this.value = { ...this.value, width: this._width }; this.#getPreview(); } #onHeightChange(e: UUIInputEvent) { this._height = parseInt(e.target.value as string, 10); + this.value = { ...this.value, height: this._height }; this.#getPreview(); } @@ -81,7 +85,7 @@ export class UmbEmbeddedMediaModalElement extends UmbModalBaseElement<
- + { let element: UmbInputMediaElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts index 8ee054676a..ca9211b392 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts @@ -11,6 +11,11 @@ export { UMB_MEDIA_TREE_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL } from './tree/index. export { UMB_MEDIA_COLLECTION_ALIAS } from './collection/index.js'; export { UMB_MEDIA_MENU_ALIAS } from './menu/index.js'; export { UMB_MEDIA_PICKER_MODAL } from './modals/media-picker/index.js'; +export { + UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, + type UmbMediaCaptionAltTextModalData, + type UmbMediaCaptionAltTextModalValue, +} from './modals/media-caption-alt-text/index.js'; export type { UmbMediaTreeItemModel } from './tree/index.js'; export { UmbMediaAuditLogRepository } from './audit-log/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts index 380266b300..4dc380687c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts @@ -1,2 +1,3 @@ export * from './image-cropper-editor/index.js'; +export * from './media-caption-alt-text/index.js'; export * from './media-picker/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts index 260e1366e4..fb40e44c2f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts @@ -1,4 +1,5 @@ import { manifests as imageCropperEditorManifests } from './image-cropper-editor/manifests.js'; +import { manifests as mediaCaptionAltTextManifests } from './media-caption-alt-text/manifests.js'; import { manifests as mediaPickerManifests } from './media-picker/manifests.js'; -export const manifests = [...imageCropperEditorManifests, ...mediaPickerManifests]; +export const manifests = [...imageCropperEditorManifests, ...mediaCaptionAltTextManifests, ...mediaPickerManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/manifests.ts similarity index 63% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/manifests.ts index cd73c2fa3f..61cb7b6d72 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/manifests.ts @@ -3,6 +3,6 @@ export const manifests: Array = [ type: 'modal', alias: 'Umb.Modal.MediaCaptionAltText', name: 'Media Caption Alt Text', - element: () => import('./media-caption-alt-text/media-caption-alt-text-modal.element.js'), + element: () => import('./media-caption-alt-text-modal.element.js'), }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/media-caption-alt-text-modal.element.ts similarity index 77% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/media-caption-alt-text-modal.element.ts index adb87d2e7d..06ad404ab8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/media-caption-alt-text-modal.element.ts @@ -1,11 +1,10 @@ +import { UmbMediaDetailRepository } from '../../repository/index.js'; import type { UmbMediaCaptionAltTextModalData, UmbMediaCaptionAltTextModalValue, } from './media-caption-alt-text-modal.token.js'; import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import '@umbraco-cms/backoffice/block-type'; -import { UmbMediaDetailRepository } from '@umbraco-cms/backoffice/media'; import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-media-caption-alt-text-modal') @@ -14,7 +13,7 @@ export class UmbMediaCaptionAltTextModalElement extends UmbModalBaseElement< UmbMediaCaptionAltTextModalValue > { #mediaUnique?: string; - #mediaDetailRepository = new UmbMediaDetailRepository(this); + readonly #mediaDetailRepository = new UmbMediaDetailRepository(this); override connectedCallback() { super.connectedCallback(); @@ -27,7 +26,7 @@ export class UmbMediaCaptionAltTextModalElement extends UmbModalBaseElement< const { data } = await this.#mediaDetailRepository.requestByUnique(this.#mediaUnique); if (!data) return; - this.value = { altText: data.variants[0].name, caption: undefined, url: data.urls[0]?.url ?? '' }; + this.value = { ...this.value, altText: this.value?.altText ?? data.variants[0].name, url: data.urls[0]?.url ?? '' }; } override render() { @@ -46,11 +45,14 @@ export class UmbMediaCaptionAltTextModalElement extends UmbModalBaseElement< (this.value = { ...this.value, caption: e.target.value as string })}> - ${this.value?.altText - ${this.value?.caption ?? ''} +
+ ${this.value?.altText +
${this.value?.caption ?? ''}
+
@@ -64,7 +66,7 @@ export class UmbMediaCaptionAltTextModalElement extends UmbModalBaseElement< `; } - static override styles = [ + static override readonly styles = [ css` uui-input { margin-bottom: var(--uui-size-layout-1); @@ -74,6 +76,17 @@ export class UmbMediaCaptionAltTextModalElement extends UmbModalBaseElement< display: flex; flex-direction: column; } + + #mainobject { + display: flex; + flex-direction: column; + max-width: 100%; + + img { + max-width: 100%; + height: auto; + } + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-caption-alt-text/media-caption-alt-text-modal.token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/Umbraco.RichText.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/Umbraco.RichText.ts similarity index 93% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/Umbraco.RichText.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/Umbraco.RichText.ts index 3dbe86e0a0..36fb6b3c94 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/Umbraco.RichText.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/Umbraco.RichText.ts @@ -5,7 +5,7 @@ export const manifest: ManifestPropertyEditorSchema = { name: 'Rich Text', alias: 'Umbraco.RichText', meta: { - defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.TinyMCE', + defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap', settings: { properties: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts similarity index 53% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts index 55361eb8cd..c7e01a7dd5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts @@ -1,4 +1,5 @@ -import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { type UmbPropertyEditorUiValueType, UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../types.js'; +import { property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; import type { @@ -7,31 +8,12 @@ import type { } from '@umbraco-cms/backoffice/property-editor'; import { UmbBlockRteEntriesContext, - type UmbBlockRteLayoutModel, UmbBlockRteManagerContext, type UmbBlockRteTypeModel, } from '@umbraco-cms/backoffice/block-rte'; -import type { UmbBlockValueType } from '@umbraco-cms/backoffice/block'; import { UMB_PROPERTY_CONTEXT, UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; -import '../../components/input-tiny-mce/input-tiny-mce.element.js'; -import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; -import { debounceTime } from '@umbraco-cms/backoffice/external/rxjs'; - -export interface UmbRichTextEditorValueType { - markup: string; - blocks: UmbBlockValueType; -} - -const UMB_BLOCK_RTE_BLOCK_LAYOUT_ALIAS = 'Umbraco.RichText'; // Not rich text, cause this has not been migrated [NL] - -/** - * @element umb-property-editor-ui-tiny-mce - */ -@customElement('umb-property-editor-ui-tiny-mce') -export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements UmbPropertyEditorUiElement { - // - // No need to registerer as a LIT-property, as we are calling it directly and no need for it to be reactive [NL] +export abstract class UmbRteBaseElement extends UmbLitElement implements UmbPropertyEditorUiElement { public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; @@ -43,55 +25,66 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements this.#managerContext.setEditorConfiguration(config); } - @property({ attribute: false }) - public set value(value: UmbRichTextEditorValueType | undefined) { - const buildUpValue: Partial = value ? { ...value } : {}; + @property({ + attribute: false, + type: Object, + hasChanged(value?: UmbPropertyEditorUiValueType, oldValue?: UmbPropertyEditorUiValueType) { + return value?.markup !== oldValue?.markup; + }, + }) + public set value(value: UmbPropertyEditorUiValueType | undefined) { + const buildUpValue: Partial = value ? { ...value } : {}; buildUpValue.markup ??= ''; buildUpValue.blocks ??= { layout: {}, contentData: [], settingsData: [], expose: [] }; buildUpValue.blocks.layout ??= {}; buildUpValue.blocks.contentData ??= []; buildUpValue.blocks.settingsData ??= []; buildUpValue.blocks.expose ??= []; - this._value = buildUpValue as UmbRichTextEditorValueType; + this._value = buildUpValue as UmbPropertyEditorUiValueType; + // Only update the actual editor markup if it is not the same as the value. if (this._latestMarkup !== this._value.markup) { this._markup = this._value.markup; } - this.#managerContext.setLayouts(buildUpValue.blocks.layout[UMB_BLOCK_RTE_BLOCK_LAYOUT_ALIAS] ?? []); + this.#managerContext.setLayouts(buildUpValue.blocks.layout[UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS] ?? []); this.#managerContext.setContents(buildUpValue.blocks.contentData); this.#managerContext.setSettings(buildUpValue.blocks.settingsData); this.#managerContext.setExposes(buildUpValue.blocks.expose); } - public get value(): UmbRichTextEditorValueType { + public get value() { return this._value; } /** * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. - * @type {boolean} - * @attr * @default false */ @property({ type: Boolean, reflect: true }) readonly = false; @state() - _config?: UmbPropertyEditorConfigCollection; + protected _config?: UmbPropertyEditorConfigCollection; @state() - private _value: UmbRichTextEditorValueType = { + protected _value: UmbPropertyEditorUiValueType = { markup: '', blocks: { layout: {}, contentData: [], settingsData: [], expose: [] }, }; - // Separate state for markup, to avoid re-rendering/re-setting the value of the TinyMCE editor when the value does not really change. + /** + * Separate state for markup, to avoid re-rendering/re-setting the value of the Tiptap editor when the value does not really change. + */ @state() - private _markup = ''; - private _latestMarkup = ''; // The latest value gotten from the TinyMCE editor. + protected _markup = ''; - #managerContext = new UmbBlockRteManagerContext(this); - #entriesContext = new UmbBlockRteEntriesContext(this); + /** + * The latest value gotten from the RTE editor. + */ + protected _latestMarkup = ''; + + readonly #managerContext = new UmbBlockRteManagerContext(this); + readonly #entriesContext = new UmbBlockRteEntriesContext(this); constructor() { super(); @@ -125,7 +118,34 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements 'observePropertyAlias', ); - this.observe( + this.observe(this.#entriesContext.layoutEntries, (layouts) => { + // Update manager: + this.#managerContext.setLayouts(layouts); + }); + + // Observe the value of the property and update the editor value. + this.observe(this.#managerContext.layouts, (layouts) => { + this._value = { + ...this._value, + blocks: { ...this._value.blocks, layout: { [UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts } }, + }; + this._fireChangeEvent(); + }); + this.observe(this.#managerContext.contents, (contents) => { + this._value = { ...this._value, blocks: { ...this._value.blocks, contentData: contents } }; + this._fireChangeEvent(); + }); + this.observe(this.#managerContext.settings, (settings) => { + this._value = { ...this._value, blocks: { ...this._value.blocks, settingsData: settings } }; + this._fireChangeEvent(); + }); + this.observe(this.#managerContext.exposes, (exposes) => { + this._value = { ...this._value, blocks: { ...this._value.blocks, expose: exposes } }; + this._fireChangeEvent(); + }); + + // The above could potentially be replaced with a single observeMultiple call, but it is not done for now to avoid potential issues with the order of the updates. + /*this.observe( observeMultiple([ this.#managerContext.layouts, this.#managerContext.contents, @@ -136,16 +156,17 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements this._value = { ...this._value, blocks: { - layout: { [UMB_BLOCK_RTE_BLOCK_LAYOUT_ALIAS]: layouts }, + layout: { [UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts }, contentData: contents, settingsData: settings, expose: exposes, }, }; - context.setValue(this._value); + + this._fireChangeEvent(); }, 'motherObserver', - ); + );*/ }); this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => { this.#managerContext.setVariantId(context.getVariantId()); @@ -157,59 +178,18 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements }); } - #onChange() { - const editor = this.#managerContext.getTinyMceEditor(); - if (!editor) return; - - // Clone the DOM, to remove the classes and attributes on the original: - const div = document.createElement('div'); - div.innerHTML = editor.getContent(); - - // Loop through used, to remove the classes on these. - const blockEls = div.querySelectorAll(`umb-rte-block, umb-rte-block-inline`); - blockEls.forEach((blockEl) => { - blockEl.removeAttribute('contenteditable'); - blockEl.removeAttribute('class'); + protected _filterUnusedBlocks(usedContentKeys: (string | null)[]) { + const unusedBlockContents = this.#managerContext.getContents().filter((x) => usedContentKeys.indexOf(x.key) === -1); + unusedBlockContents.forEach((blockContent) => { + this.#managerContext.removeOneContent(blockContent.key); }); - - const markup = div.innerHTML; - - // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup. - //const blockElements = editor.dom.select(`umb-rte-block, umb-rte-block-inline`); - const usedContentKeys = Array.from(blockEls).map((blockElement) => blockElement.getAttribute('data-content-key')); const unusedBlocks = this.#managerContext.getLayouts().filter((x) => usedContentKeys.indexOf(x.contentKey) === -1); unusedBlocks.forEach((blockLayout) => { this.#managerContext.removeOneLayout(blockLayout.contentKey); }); + } - // Then get the content of the editor and update the value. - // maybe in this way doc.body.innerHTML; - - this._latestMarkup = markup; - - this._value = { - ...this._value, - markup: markup, - }; + protected _fireChangeEvent() { this.dispatchEvent(new UmbPropertyValueChangeEvent()); } - - override render() { - return html` - - - `; - } -} - -export default UmbPropertyEditorUITinyMceElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-property-editor-ui-tiny-mce': UmbPropertyEditorUITinyMceElement; - } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/manifests.ts new file mode 100644 index 0000000000..bfeeb1e04b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/manifests.ts @@ -0,0 +1,13 @@ +// eslint-disable-next-line local-rules/no-relative-import-to-import-map-module +import { manifests as tiptapManifests } from './tiptap/manifests.js'; +import { manifests as tinyMceManifests } from './tiny-mce/manifests.js'; +import { manifest as schemaManifest } from './Umbraco.RichText.js'; +import { manifest as blockRtePropertyValueResolver } from './property-value-resolver/manifest.js'; +import type { ManifestTypes, UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + ...tinyMceManifests, + ...tiptapManifests, + schemaManifest, + blockRtePropertyValueResolver, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/package.json b/src/Umbraco.Web.UI.Client/src/packages/rte/package.json similarity index 66% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/package.json rename to src/Umbraco.Web.UI.Client/src/packages/rte/package.json index 6362dc5347..20394e7e36 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/package.json +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/package.json @@ -1,5 +1,5 @@ { - "name": "@umbraco-backoffice/tiny-mce", + "name": "@umbraco-backoffice/rte", "private": true, "type": "module", "scripts": { diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/manifest.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/manifest.ts new file mode 100644 index 0000000000..d9c9b061f0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/manifest.ts @@ -0,0 +1,13 @@ +import { UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../types.js'; +import { UmbRteBlockValueResolver } from './rte-block-value-resolver.api.js'; +import type { ManifestPropertyValueResolver } from '@umbraco-cms/backoffice/property'; + +export const manifest: ManifestPropertyValueResolver = { + type: 'propertyValueResolver', + alias: 'Umb.PropertyValueResolver.RichTextBlocks', + name: 'Block Value Resolver', + api: UmbRteBlockValueResolver, + meta: { + editorAlias: UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-value-resolver/rte-block-value-resolver.api.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/rte-block-value-resolver.api.ts similarity index 94% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-value-resolver/rte-block-value-resolver.api.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/rte-block-value-resolver.api.ts index edf2726889..fbf8f45d17 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-value-resolver/rte-block-value-resolver.api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/property-value-resolver/rte-block-value-resolver.api.ts @@ -1,4 +1,4 @@ -import type { UmbPropertyEditorUiValueType } from '../../../types.js'; +import type { UmbPropertyEditorUiValueType } from '../types.js'; import { UmbBlockValueResolver, type UmbBlockDataValueModel, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.defaults.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.defaults.ts new file mode 100644 index 0000000000..45705a7ec5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.defaults.ts @@ -0,0 +1,153 @@ +import { UMB_CONTENT_REQUEST_EVENT_TYPE, type UmbContextRequestEvent } from '@umbraco-cms/backoffice/context-api'; +import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce'; +import { UUIIconRequestEvent } from '@umbraco-cms/backoffice/external/uui'; + +//export const UMB_BLOCK_ENTRY_WEB_COMPONENTS_ABSOLUTE_PATH = '/umbraco/backoffice/packages/block/block-rte/index.js'; +export const UMB_BLOCK_ENTRY_WEB_COMPONENTS_ABSOLUTE_PATH = '@umbraco-cms/backoffice/block-rte'; + +//we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce +//so we don't have to specify all the normal elements again +export const defaultFallbackConfig: RawEditorOptions = { + plugins: ['anchor', 'charmap', 'table', 'lists', 'advlist', 'autolink', 'directionality', 'searchreplace'], + valid_elements: + '+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,cite,video[*],audio[*],picture[*],source[*],canvas[*]', + invalid_elements: 'font', + extended_valid_elements: + '@[id|class|style],+umb-rte-block[!data-content-key],+umb-rte-block-inline[!data-content-key],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang],figure,figcaption', + custom_elements: 'umb-rte-block,~umb-rte-block-inline', + toolbar: [ + 'styles', + 'bold', + 'italic', + 'alignleft', + 'aligncenter', + 'alignright', + 'bullist', + 'numlist', + 'outdent', + 'indent', + 'link', + 'umbmediapicker', + 'umbembeddialog', + ], + + init_instance_callback: function (editor) { + // The following code is the context api proxy. [NL] + // It re-dispatches the context api request event to the origin target of this modal, in other words the element that initiated the modal. [NL] + editor.dom.doc.addEventListener(UMB_CONTENT_REQUEST_EVENT_TYPE, ((event: UmbContextRequestEvent) => { + if (!editor.iframeElement) return; + + event.stopImmediatePropagation(); + editor.iframeElement.dispatchEvent(event.clone()); + }) as EventListener); + + // Proxy for retrieving icons from outside the iframe [NL] + editor.dom.doc.addEventListener(UUIIconRequestEvent.ICON_REQUEST, ((event: UUIIconRequestEvent) => { + if (!editor.iframeElement) return; + + const newEvent = new UUIIconRequestEvent(UUIIconRequestEvent.ICON_REQUEST, { + detail: event.detail, + }); + editor.iframeElement.dispatchEvent(newEvent); + if (newEvent.icon !== null) { + event.acceptRequest(newEvent.icon); + } + }) as EventListener); + + // Transfer our import-map to the iframe: [NL] + const importMapTag = document.head.querySelector('script[type="importmap"]'); + if (importMapTag) { + const importMap = document.createElement('script'); + importMap.type = 'importmap'; + importMap.text = importMapTag.innerHTML; + editor.dom.doc.head.appendChild(importMap); + } + + // Transfer our stylesheets to the iframe: [NL] + const stylesheetTags = document.head.querySelectorAll('link[rel="stylesheet"]'); + stylesheetTags.forEach((stylesheetTag) => { + const stylesheet = document.createElement('link'); + stylesheet.rel = 'stylesheet'; + stylesheet.href = stylesheetTag.href; + editor.dom.doc.head.appendChild(stylesheet); + }); + + editor.dom.doc.addEventListener('click', (e: MouseEvent) => { + // If we try to open link in a new tab, then we want to skip skip: + //if ((isWindows && e.ctrlKey) || (!isWindows && e.metaKey)) return; + + // Find the target by using the composed path to get the element through the shadow boundaries. + // Notice the difference here compared to RouterSlots implementation [NL] + const $anchor: HTMLAnchorElement = (('composedPath' in e) as any) + ? (e + .composedPath() + .find(($elem) => $elem instanceof HTMLAnchorElement || ($elem as any).tagName === 'A') as HTMLAnchorElement) + : (e.target as HTMLAnchorElement); + + // Abort if the event is not about the anchor tag + if ($anchor == null || !($anchor instanceof HTMLAnchorElement || ($anchor as any).tagName === 'A')) { + return; + } + + // Get the HREF value from the anchor tag + const href = $anchor.href; + + // Only handle the anchor tag if the follow holds true: + // - The HREF is relative to the origin of the current location. + // - The target is targeting the current frame. + // - The anchor doesn't have the attribute [data-router-slot]="disabled" + if ( + !href.startsWith(location.origin) || + ($anchor.target !== '' && $anchor.target !== '_self') || + $anchor.dataset['routerSlot'] === 'disabled' + ) { + return; + } + + // Remove the origin from the start of the HREF to get the path + const path = $anchor.pathname + $anchor.search + $anchor.hash; + + // Prevent the default behavior + e.preventDefault(); + + // Change the history! + window.history.pushState(null, '', path); + }); + + // Load backoffice JS so we can get the umb-rte-block component registered inside the iframe [NL] + const script = document.createElement('script'); + script.type = 'text/javascript'; + script.setAttribute('type', 'module'); + + script.text = `import "@umbraco-cms/backoffice/extension-registry";`; + script.text = `import "${UMB_BLOCK_ENTRY_WEB_COMPONENTS_ABSOLUTE_PATH}";`; + editor.dom.doc.head.appendChild(script); + }, + + style_formats: [ + { + title: 'Headers', + items: [ + { title: 'Page header', block: 'h2' }, + { title: 'Section header', block: 'h3' }, + { title: 'Paragraph header', block: 'h4' }, + ], + }, + { + title: 'Blocks', + items: [{ title: 'Paragraph', block: 'p' }], + }, + { + title: 'Containers', + items: [ + { title: 'Quote', block: 'blockquote' }, + { title: 'Code', block: 'code' }, + ], + }, + ], + /** + * @description The maximum image size in pixels that can be inserted into the editor. + * @remarks This is registered and used by the UmbMediaPicker plugin + */ + maxImageSize: 500, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts similarity index 93% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts index 87601bc0a2..0a081ea5a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts @@ -18,13 +18,13 @@ import { renderEditor, } from '@umbraco-cms/backoffice/external/tinymce'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { ManifestTinyMcePlugin } from '@umbraco-cms/backoffice/tiny-mce'; /** * Handles the resize event * @param e */ -// TODO: This does somehow not belong as a utility method as it is very specific to this implementation. [NL] async function onResize( e: EditorEvent<{ target: HTMLElement; @@ -43,7 +43,7 @@ async function onResize( const resizedPath = await getProcessedImageUrl(path, { width: e.width, height: e.height, - mode: 'max', + mode: ImageCropModeModel.MAX, }); e.target.setAttribute('data-mce-src', resizedPath); @@ -56,8 +56,8 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, ' #plugins: Array | undefined> = []; #editorRef?: Editor | null = null; - #stylesheetRepository = new UmbStylesheetDetailRepository(this); - #umbStylesheetRuleManager = new UmbStylesheetRuleManager(); + readonly #stylesheetRepository = new UmbStylesheetDetailRepository(this); + readonly #umbStylesheetRuleManager = new UmbStylesheetRuleManager(); protected override getFormElement() { return this._editorElement?.querySelector('iframe') ?? undefined; @@ -65,7 +65,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, ' override set value(newValue: FormDataEntryValue | FormData) { super.value = newValue; - const newContent = newValue?.toString() ?? ''; + const newContent = typeof newValue === 'string' ? newValue : ''; if (this.#editorRef && this.#editorRef.getContent() != newContent) { this.#editorRef.setContent(newContent); @@ -95,14 +95,13 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, ' #readonly = false; @query('.editor', true) - private _editorElement?: HTMLElement; + private readonly _editorElement?: HTMLElement; getEditor() { return this.#editorRef; } - constructor() { - super(); + override firstUpdated() { this.#loadEditor(); } @@ -228,8 +227,8 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, ' // set the configured toolbar if any, otherwise false const toolbar = this.configuration?.getValueByAlias('toolbar'); - if (toolbar && toolbar.length) { - configurationOptions.toolbar = toolbar?.join(' '); + if (toolbar?.length) { + configurationOptions.toolbar = toolbar.join(' '); } else { configurationOptions.toolbar = false; } @@ -337,13 +336,12 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, ' /** * Prevent injecting arbitrary JavaScript execution in on-attributes. * - * TODO: This used to be toggleable through server variables with window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce */ const allNodes = Array.from(editor.dom.doc.getElementsByTagName('*')); allNodes.forEach((node) => { - for (let i = 0; i < node.attributes.length; i++) { - if (node.attributes[i].name.startsWith('on')) { - node.removeAttribute(node.attributes[i].name); + for (const attr of node.attributes) { + if (attr.name.startsWith('on')) { + node.removeAttribute(attr.name); } } }); @@ -365,7 +363,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, ' //enable browser based spell checking editor.getBody().setAttribute('spellcheck', 'true'); uriAttributeSanitizer(editor); - editor.setContent(this.value?.toString() ?? ''); + editor.setContent(typeof this.value === 'string' ? this.value : ''); } #onChange(value: string) { @@ -381,7 +379,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, ' return html`
`; } - static override styles = [ + static override readonly styles = [ css` .tox-tinymce { position: relative; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.handlers.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.handlers.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.handlers.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.handlers.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.languages.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.languages.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.languages.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.languages.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts similarity index 61% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts index fbc5e0ebd9..d50a6ef6e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/input-tiny-mce.sanitizer.ts @@ -1,4 +1,3 @@ -// TODO: clean up this file /* eslint-disable @typescript-eslint/no-unused-vars */ import type { Editor } from '@umbraco-cms/backoffice/external/tinymce'; @@ -28,13 +27,7 @@ export const uriAttributeSanitizer = (editor: Editor) => { return function parseUri(uri: string, tagName: string) { uri = uri.replace(trimRegExp, ''); - try { - // Might throw malformed URI sequence - uri = decodeURIComponent(uri); - } catch (ex) { - // Fallback to non UTF-8 decoder - uri = unescape(uri); - } + uri = decodeURIComponent(uri); if (scriptUriRegExp.test(uri)) { return; @@ -48,20 +41,15 @@ export const uriAttributeSanitizer = (editor: Editor) => { }; })(); - // TODO: sanitizeTinyMce is not defined in the global scope, so this will not work. Instead we need to get this setting from somewhere else: - /* - if (window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce) { - uriAttributesToSanitize.forEach((attribute) => { - editor.serializer.addAttributeFilter(attribute, (nodes: AstNode[]) => { - nodes.forEach((node: AstNode) => { - node.attributes?.forEach((attr) => { - if (uriAttributesToSanitize.includes(attr.name.toLowerCase())) { - attr.value = parseUri(attr.value, node.name) ?? ''; - } - }); - }); - }); + editor.serializer.addAttributeFilter('uriAttributesToSanitize', function (nodes) { + nodes.forEach(function (node) { + if (!node.attributes) return; + for (const attr of node.attributes) { + const attrName = attr.name.toLowerCase(); + if (uriAttributesToSanitize.indexOf(attrName) !== -1) { + attr.value = parseUri(attr.value, node.name) ?? ''; + } + } }); - } - */ + }); }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/tiny-mce-plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/tiny-mce-plugin.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/tiny-mce-plugin.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/components/input-tiny-mce/tiny-mce-plugin.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/index.ts similarity index 68% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/index.ts index d303eda538..fad1d3710a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/index.ts @@ -1,3 +1,2 @@ export * from './components/index.js'; -export * from './modals/index.js'; export * from './plugins/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/manifests.ts similarity index 65% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/manifests.ts index 7c24fabe3e..0c978efd63 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/manifests.ts @@ -1,5 +1,4 @@ import { manifests as propertyEditors } from './property-editors/manifests.js'; import { manifests as plugins } from './plugins/manifests.js'; -import { manifests as modalManifests } from './modals/manifests.js'; -export const manifests: Array = [...propertyEditors, ...plugins, ...modalManifests]; +export const manifests: Array = [...propertyEditors, ...plugins]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts similarity index 56% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts index 43265dca4d..ddfc1e5a01 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-block-picker.plugin.ts @@ -1,18 +1,26 @@ -import { UMB_BLOCK_RTE_MANAGER_CONTEXT } from '../../block/block-rte/context/block-rte-manager.context-token.js'; -import { UMB_BLOCK_RTE_ENTRIES_CONTEXT } from '../../block/block-rte/context/block-rte-entries.context-token.js'; +import { UMB_BLOCK_RTE_DATA_CONTENT_KEY } from '../../types.js'; import { type TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/tiny-mce'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; +import type { Editor } from '@umbraco-cms/backoffice/external/tinymce'; +import type { UmbBlockDataModel } from '@umbraco-cms/backoffice/block'; +import { + UMB_BLOCK_RTE_ENTRIES_CONTEXT, + UMB_BLOCK_RTE_MANAGER_CONTEXT, + type UmbBlockRteLayoutModel, +} from '@umbraco-cms/backoffice/block-rte'; export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase { - #localize = new UmbLocalizationController(this._host); - - private _blocks?: Array; + readonly #localize = new UmbLocalizationController(this._host); + readonly #editor: Editor; + #blocks?: Array; #entriesContext?: typeof UMB_BLOCK_RTE_ENTRIES_CONTEXT.TYPE; constructor(args: TinyMcePluginArguments) { super(args); + this.#editor = args.editor; + args.editor.ui.registry.addToggleButton('umbblockpicker', { icon: 'visualblocks', tooltip: this.#localize.term('blockEditor_insertBlock'), @@ -27,15 +35,21 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase }); this.consumeContext(UMB_BLOCK_RTE_MANAGER_CONTEXT, (context) => { - context.setTinyMceEditor(args.editor); - this.observe( context.blockTypes, (blockTypes) => { - this._blocks = blockTypes; + this.#blocks = blockTypes; }, 'blockType', ); + + this.observe( + context.contents, + (contents) => { + this.#updateBlocks(contents, context.getLayouts()); + }, + 'contents', + ); }); this.consumeContext(UMB_BLOCK_RTE_ENTRIES_CONTEXT, (context) => { this.#entriesContext = context; @@ -64,11 +78,10 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase return; } - // TODO: Missing solution to skip catalogue if only one type available. [NL] let createPath: string | undefined = undefined; - if (this._blocks?.length === 1) { - const elementKey = this._blocks[0].contentElementTypeKey; + if (this.#blocks?.length === 1) { + const elementKey = this.#blocks[0].contentElementTypeKey; createPath = this.#entriesContext.getPathForCreateBlock() + 'modal/umb-modal-workspace/create/' + elementKey; } else { createPath = this.#entriesContext.getPathForCreateBlock(); @@ -78,4 +91,32 @@ export default class UmbTinyMceMultiUrlPickerPlugin extends UmbTinyMcePluginBase window.history.pushState({}, '', createPath); } } + + #updateBlocks(blocks: UmbBlockDataModel[], layouts: Array) { + const editor = this.#editor; + if (!editor?.dom) return; + + const existingBlocks = editor.dom + .select('umb-rte-block, umb-rte-block-inline') + .map((x) => x.getAttribute(UMB_BLOCK_RTE_DATA_CONTENT_KEY)); + const newBlocks = blocks.filter((x) => !existingBlocks.find((contentKey) => contentKey === x.key)); + + newBlocks.forEach((block) => { + // Find layout for block + const layout = layouts.find((x) => x.contentKey === block.key); + const inline = layout?.displayInline ?? false; + + let blockTag = 'umb-rte-block'; + + if (inline) { + blockTag = 'umb-rte-block-inline'; + } + + const blockEl = `<${blockTag} ${UMB_BLOCK_RTE_DATA_CONTENT_KEY}="${block.key}">`; + + editor.selection.setContent(blockEl); + editor.setDirty(true); + editor.dispatch('Change'); + }); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-code-editor.plugin.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts similarity index 84% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts index 91b79639a7..e7e4ca1556 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tiny-mce-mediapicker.plugin.ts @@ -1,11 +1,15 @@ -import { getGuid } from '../utils.js'; -import { UMB_MEDIA_CAPTION_ALT_TEXT_MODAL } from '../modals/media-caption-alt-text/media-caption-alt-text-modal.token.js'; import { type TinyMcePluginArguments, UmbTinyMcePluginBase } from '../components/input-tiny-mce/tiny-mce-plugin.js'; +import { getGuidFromUdi } from '@umbraco-cms/backoffice/utils'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce'; import { UmbTemporaryFileRepository } from '@umbraco-cms/backoffice/temporary-file'; import { UmbId } from '@umbraco-cms/backoffice/id'; -import { sizeImageInEditor, uploadBlobImages, UMB_MEDIA_PICKER_MODAL } from '@umbraco-cms/backoffice/media'; +import { + sizeImageInEditor, + uploadBlobImages, + UMB_MEDIA_PICKER_MODAL, + UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, +} from '@umbraco-cms/backoffice/media'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; interface MediaPickerTargetData { @@ -27,7 +31,7 @@ interface MediaPickerResultData { export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { #modalManager?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; - #temporaryFileRepository; + readonly #temporaryFileRepository; constructor(args: TinyMcePluginArguments) { super(args); @@ -39,13 +43,6 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { this.#modalManager = instance; }); - // TODO => this breaks tests. disabling for now - // will ignore user media start nodes - // this.host.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { - // this.#currentUserContext = instance; - // this.#observeCurrentUser(); - // }); - this.editor.ui.registry.addToggleButton('umbmediapicker', { icon: 'image', tooltip: localize.term('general_mediaPicker'), @@ -120,7 +117,6 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { async #showMediaPicker(currentTarget: MediaPickerTargetData) { /* - // TODO: I dont think we should parse this one... it should be up to the modal to get this information, and then we could parse some configs on to affect this. let startNodeId; let startNodeIsVirtual; @@ -135,31 +131,36 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { } */ - // TODO => startNodeId and startNodeIsVirtual do not exist on ContentTreeItemResponseModel - const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, { data: { multiple: false, //startNodeIsVirtual, }, value: { - selection: currentTarget.udi ? [getGuid(currentTarget.udi)] : [], + selection: currentTarget.udi ? [getGuidFromUdi(currentTarget.udi)] : [], }, }); if (!modalHandler) return; const { selection } = await modalHandler.onSubmit().catch(() => ({ selection: undefined })); - if (!selection || !selection.length) return; + if (!selection?.length) return; - this.#showMediaCaptionAltText(selection[0]); + this.#showMediaCaptionAltText(selection[0], currentTarget); this.editor.dispatch('Change'); } - async #showMediaCaptionAltText(mediaUnique: string | null) { + async #showMediaCaptionAltText(mediaUnique: string | null, currentTarget: MediaPickerTargetData) { if (!mediaUnique) return; - const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, { data: { mediaUnique } }); + const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, { + data: { mediaUnique }, + value: { + url: '', + altText: currentTarget.altText, + caption: currentTarget.caption, + }, + }); const mediaData = await modalHandler?.onSubmit().catch(() => null); if (!mediaData) return; @@ -199,13 +200,11 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { } else { parentElement.innerHTML = combined; } - } else { + } else if (parentElement?.nodeName === 'FIGURE' && parentElement.parentElement) { //if caption is removed, remove the figure element - if (parentElement?.nodeName === 'FIGURE' && parentElement.parentElement) { - parentElement.parentElement.innerHTML = newImage; - } else { - this.editor.selection.setContent(newImage); - } + parentElement.parentElement.innerHTML = newImage; + } else { + this.editor.selection.setContent(newImage); } // Using settimeout to wait for a DoM-render, so we can find the new element by ID. @@ -230,7 +229,7 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase { }); } - #uploadImageHandler: RawEditorOptions['images_upload_handler'] = (blobInfo, progress) => { + readonly #uploadImageHandler: RawEditorOptions['images_upload_handler'] = (blobInfo, progress) => { return new Promise((resolve, reject) => { // Fetch does not support progress, so we need to fake it. progress(0); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tinymce-plugin.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tinymce-plugin.extension.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/plugins/tinymce-plugin.extension.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/plugins/tinymce-plugin.extension.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts similarity index 83% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts index 2c10cd74a5..a830297e36 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.element.ts @@ -1,14 +1,15 @@ -import { type UmbBlockTypeBaseModel, UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type'; -import { UMB_BLOCK_RTE_TYPE } from '@umbraco-cms/backoffice/block-rte'; -import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; -import { html, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { - UmbPropertyValueChangeEvent, - type UmbPropertyEditorConfigCollection, -} from '@umbraco-cms/backoffice/property-editor'; +import { customElement, html, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; +import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import { UMB_BLOCK_RTE_TYPE } from '@umbraco-cms/backoffice/block-rte'; +import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; +import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; +import type { + UmbPropertyEditorUiElement, + UmbPropertyEditorConfigCollection, +} from '@umbraco-cms/backoffice/property-editor'; /** * @element umb-property-editor-ui-block-rte-type-configuration @@ -18,7 +19,7 @@ export class UmbPropertyEditorUIBlockRteBlockConfigurationElement extends UmbLitElement implements UmbPropertyEditorUiElement { - #blockTypeWorkspaceModalRegistration?: UmbModalRouteRegistrationController< + readonly #blockTypeWorkspaceModalRegistration?: UmbModalRouteRegistrationController< typeof UMB_WORKSPACE_MODAL.DATA, typeof UMB_WORKSPACE_MODAL.VALUE >; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.stories.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/block/property-editor-ui-block-rte-type-configuration.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts similarity index 97% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts index b8b9b82d8b..e47c74dce5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.element.ts @@ -38,7 +38,7 @@ export class UmbPropertyEditorUITinyMceDimensionsConfigurationElement extends Um pixels`; } - static override styles = [UmbTextStyles]; + static override readonly styles = [UmbTextStyles]; } export default UmbPropertyEditorUITinyMceDimensionsConfigurationElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts similarity index 91% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts index 70b2d39ad3..51322338cd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.stories.ts @@ -1,6 +1,6 @@ +import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type.db.js'; import type { Meta } from '@storybook/web-components'; import './property-editor-ui-tiny-mce-dimensions-configuration.element.js'; -import { umbDataTypeMockDb } from '../../../../mocks/data/data-type/data-type.db.js'; import { html } from '@umbraco-cms/backoffice/external/lit'; import type { UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/dimensions/property-editor-ui-tiny-mce-dimensions-configuration.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts similarity index 90% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts index 714136611c..652aa6769e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.stories.ts @@ -1,4 +1,4 @@ -import { umbDataTypeMockDb } from '../../../../mocks/data/data-type/data-type.db.js'; +import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type.db.js'; import type { Meta } from '@storybook/web-components'; import { html } from '@umbraco-cms/backoffice/external/lit'; import './property-editor-ui-tiny-mce-maximagesize.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/max-image-size/property-editor-ui-tiny-mce-maximagesize.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts similarity index 87% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts index 43ed4bceb4..a0efa34c22 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts @@ -1,9 +1,11 @@ -import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; -import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; +import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; +import type { + UmbPropertyEditorConfigCollection, + UmbPropertyEditorUiElement, +} from '@umbraco-cms/backoffice/property-editor'; import type { UmbStylesheetInputElement } from '@umbraco-cms/backoffice/stylesheet'; /** @@ -14,7 +16,7 @@ export class UmbPropertyEditorUITinyMceStylesheetsConfigurationElement extends UmbLitElement implements UmbPropertyEditorUiElement { - #serverFilePathUniqueSerializer = new UmbServerFilePathUniqueSerializer(); + readonly #serverFilePathUniqueSerializer = new UmbServerFilePathUniqueSerializer(); @property({ type: Array }) public set value(value: Array) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts similarity index 91% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts index 1341c768a7..0c6292c87b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.stories.ts @@ -1,4 +1,4 @@ -import { umbDataTypeMockDb } from '../../../../mocks/data/data-type/data-type.db.js'; +import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type.db.js'; import type { Meta } from '@storybook/web-components'; import { html } from '@umbraco-cms/backoffice/external/lit'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/manifests.ts similarity index 90% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/manifests.ts index 5577fe4db2..c65992bdca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/manifests.ts @@ -1,7 +1,5 @@ -import { manifest as schemaManifest } from './Umbraco.RichText.js'; -import { UmbRteBlockValueResolver } from './property-value-resolver/rte-block-value-resolver.api.js'; +import { UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../../types.js'; -export const UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS = 'Umbraco.RichText'; export const UMB_BLOCK_RTE_PROPERTY_EDITOR_UI_ALIAS = 'Umb.PropertyEditorUi.TinyMCE'; export const manifests: Array = [ @@ -11,7 +9,7 @@ export const manifests: Array = [ name: 'Rich Text Editor Property Editor UI', element: () => import('./property-editor-ui-tiny-mce.element.js'), meta: { - label: 'Rich Text Editor', + label: 'Rich Text Editor [TinyMCE]', propertyEditorSchemaAlias: UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS, icon: 'icon-browser-window', group: 'richContent', @@ -134,14 +132,4 @@ export const manifests: Array = [ }, }, }, - { - type: 'propertyValueResolver', - alias: 'Umb.PropertyValueResolver.TinyMce', - name: 'Block Value Resolver', - api: UmbRteBlockValueResolver, - meta: { - editorAlias: UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS, - }, - }, - schemaManifest, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts new file mode 100644 index 0000000000..2d1064c461 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts @@ -0,0 +1,68 @@ +import { UmbRteBaseElement } from '../../../components/rte-base.element.js'; +import { UMB_BLOCK_RTE_DATA_CONTENT_KEY } from '../../../types.js'; +import type { UmbInputTinyMceElement } from '../../components/input-tiny-mce/input-tiny-mce.element.js'; +import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; + +import '../../components/input-tiny-mce/input-tiny-mce.element.js'; + +/** + * @element umb-property-editor-ui-tiny-mce + */ +@customElement('umb-property-editor-ui-tiny-mce') +export class UmbPropertyEditorUITinyMceElement extends UmbRteBaseElement { + #onChange(event: CustomEvent & { target: UmbInputTinyMceElement }) { + const value = typeof event.target.value === 'string' ? event.target.value : ''; + + // Clone the DOM, to remove the classes and attributes on the original: + const div = document.createElement('div'); + div.innerHTML = value; + + // Loop through used, to remove the classes on these. + const blockEls = div.querySelectorAll(`umb-rte-block, umb-rte-block-inline`); + blockEls.forEach((blockEl) => { + blockEl.removeAttribute('contenteditable'); + blockEl.removeAttribute('class'); + }); + + const markup = div.innerHTML; + + // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup. + //const blockElements = editor.dom.select(`umb-rte-block, umb-rte-block-inline`); + const usedContentKeys = Array.from(blockEls).map((blockElement) => + blockElement.getAttribute(UMB_BLOCK_RTE_DATA_CONTENT_KEY), + ); + + this._filterUnusedBlocks(usedContentKeys); + + // Then get the content of the editor and update the value. + // maybe in this way doc.body.innerHTML; + + this._latestMarkup = markup; + + this._value = { + ...this._value, + markup: markup, + }; + + this._fireChangeEvent(); + } + + override render() { + return html` + + + `; + } +} + +export default UmbPropertyEditorUITinyMceElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-tiny-mce': UmbPropertyEditorUITinyMceElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.stories.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts similarity index 92% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts index af902002c4..9559126d33 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.element.ts @@ -1,15 +1,15 @@ +import { css, customElement, html, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; +import { tinymce } from '@umbraco-cms/backoffice/external/tinymce'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; -import { customElement, css, html, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; -import { - UmbPropertyValueChangeEvent, - type UmbPropertyEditorConfigCollection, +import type { + UmbPropertyEditorUiElement, + UmbPropertyEditorConfigCollection, } from '@umbraco-cms/backoffice/property-editor'; -import { tinymce } from '@umbraco-cms/backoffice/external/tinymce'; const tinyIconSet = tinymce.IconManager.get('default'); @@ -59,7 +59,7 @@ export class UmbPropertyEditorUITinyMceToolbarConfigurationElement config?: UmbPropertyEditorConfigCollection; @state() - private _toolbarConfig: ToolbarConfig[] = []; + private readonly _toolbarConfig: ToolbarConfig[] = []; #selectedValues: string[] = []; @@ -128,7 +128,7 @@ export class UmbPropertyEditorUITinyMceToolbarConfigurationElement `; } - static override styles = [ + static override readonly styles = [ UmbTextStyles, css` ul { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts similarity index 91% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts index 29f8543717..aa598bf356 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.stories.ts @@ -1,4 +1,4 @@ -import { umbDataTypeMockDb } from '../../../../mocks/data/data-type/data-type.db.js'; +import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type.db.js'; import type { Meta } from '@storybook/web-components'; import { html } from '@umbraco-cms/backoffice/external/lit'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/rte/tiny-mce/property-editors/toolbar/property-editor-ui-tiny-mce-toolbar-configuration.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/index.ts new file mode 100644 index 0000000000..f03903fb07 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/index.ts @@ -0,0 +1,2 @@ +export * from './input-tiptap/index.js'; +export * from './toolbar/tiptap-toolbar-dropdown-base.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/index.ts new file mode 100644 index 0000000000..f11b53c7d9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/index.ts @@ -0,0 +1 @@ +export * from './input-tiptap.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/input-tiptap.element.ts new file mode 100644 index 0000000000..dee7a0609d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -0,0 +1,329 @@ +import type { UmbTiptapExtensionApi, UmbTiptapToolbarValue } from '../../extensions/types.js'; +import { css, customElement, html, property, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { Editor, Placeholder, StarterKit, TextStyle } from '@umbraco-cms/backoffice/external/tiptap'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; + +import './tiptap-fixed-menu.element.js'; +import './tiptap-hover-menu.element.js'; + +const elementName = 'umb-input-tiptap'; + +@customElement(elementName) +export class UmbInputTiptapElement extends UmbFormControlMixin(UmbLitElement) { + readonly #requiredExtensions = [ + StarterKit, + Placeholder.configure({ + placeholder: ({ node }) => { + if (node.type.name === 'heading') { + return this.localize.term('placeholders_rteHeading'); + } + + return this.localize.term('placeholders_rteParagraph'); + }, + }), + TextStyle, + ]; + + @state() + private readonly _extensions: Array = []; + + @property({ type: String }) + override set value(value: string) { + this.#markup = value; + + // Try to set the value to the editor if it is ready. + if (this._editor) { + this._editor.commands.setContent(value); + } + } + override get value() { + return this.#markup; + } + + #markup = ''; + + @property({ attribute: false }) + configuration?: UmbPropertyEditorConfigCollection; + + /** + * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. + */ + @property({ type: Boolean, reflect: true }) + readonly = false; + + @state() + private _editor!: Editor; + + @state() + _toolbar: UmbTiptapToolbarValue = [[[]]]; + + protected override async firstUpdated() { + await Promise.all([await this.#loadExtensions(), await this.#loadEditor()]); + } + + async #loadExtensions() { + await new Promise((resolve) => { + this.observe(umbExtensionsRegistry.byType('tiptapExtension'), async (manifests) => { + const enabledExtensions = this.configuration?.getValueByAlias('extensions') ?? []; + for (const manifest of manifests) { + if (manifest.api) { + const extension = await loadManifestApi(manifest.api); + if (extension) { + // Check if the extension is enabled + if (enabledExtensions.includes(manifest.alias)) { + this._extensions.push(new extension(this)); + } + } + } + } + resolve(); + }); + }); + } + + async #loadEditor() { + const element = this.shadowRoot?.querySelector('#editor'); + if (!element) return; + + const dimensions = this.configuration?.getValueByAlias<{ width?: number; height?: number }>('dimensions'); + if (dimensions?.width) this.setAttribute('style', `max-width: ${dimensions.width}px;`); + if (dimensions?.height) element.setAttribute('style', `max-height: ${dimensions.height}px;`); + + this._toolbar = this.configuration?.getValueByAlias('toolbar') ?? [[[]]]; + + const extensions = this._extensions + .map((ext) => ext.getTiptapExtensions({ configuration: this.configuration })) + .flat(); + + this._editor = new Editor({ + element: element, + editable: !this.readonly, + extensions: [...this.#requiredExtensions, ...extensions], + content: this.#markup, + onBeforeCreate: ({ editor }) => { + this._extensions.forEach((ext) => ext.setEditor(editor)); + }, + onUpdate: ({ editor }) => { + this.#markup = editor.getHTML(); + this.dispatchEvent(new UmbChangeEvent()); + }, + }); + } + + override render() { + return html` + ${when( + !this._editor && !this._extensions?.length, + () => html`
`, + () => html` + + `, + )} +
+ `; + } + + static override readonly styles = [ + css` + :host { + display: block; + position: relative; + z-index: 0; + } + + :host([readonly]) { + pointer-events: none; + + #editor { + background-color: var(--uui-color-surface-alt); + } + } + + #loader { + display: flex; + align-items: center; + justify-content: center; + } + + .tiptap { + height: 100%; + width: 100%; + outline: none; + white-space: pre-wrap; + min-width: 0; + } + + .tiptap .is-editor-empty:first-child::before { + color: var(--uui-color-text); + opacity: 0.55; + content: attr(data-placeholder); + float: left; + height: 0; + pointer-events: none; + } + + #editor { + overflow: auto; + border-radius: var(--uui-border-radius); + border: 1px solid var(--uui-color-border); + padding: 1rem; + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top: 0; + box-sizing: border-box; + height: 100%; + width: 100%; + min-height: 400px; + display: grid; /* Don't ask me why this is needed, but it is. */ + pre { + background-color: var(--uui-color-surface-alt); + padding: var(--uui-size-space-2) var(--uui-size-space-4); + border-radius: calc(var(--uui-border-radius) * 2); + overflow-x: auto; + } + + code:not(pre > code) { + background-color: var(--uui-color-surface-alt); + padding: var(--uui-size-space-1) var(--uui-size-space-2); + border-radius: calc(var(--uui-border-radius) * 2); + } + + code { + font-family: 'Roboto Mono', monospace; + background: none; + color: inherit; + font-size: 0.8rem; + padding: 0; + } + + h1, + h2, + h3 { + margin-top: 0; + margin-bottom: 0.5em; + } + + figure { + > p, + img { + pointer-events: none; + margin: 0; + padding: 0; + } + + &.ProseMirror-selectednode { + outline: 3px solid var(--uui-color-focus); + } + } + + img { + &.ProseMirror-selectednode { + outline: 3px solid var(--uui-color-focus); + } + } + + .umb-embed-holder { + display: inline-block; + position: relative; + } + + .umb-embed-holder > * { + user-select: none; + pointer-events: none; + } + + .umb-embed-holder.ProseMirror-selectednode { + outline: 2px solid var(--uui-palette-spanish-pink-light); + } + + .umb-embed-holder::before { + z-index: 1000; + width: 100%; + height: 100%; + position: absolute; + content: ' '; + } + + .umb-embed-holder.ProseMirror-selectednode::before { + background: rgba(0, 0, 0, 0.025); + } + + /* Table-specific styling */ + .tableWrapper { + margin: 1.5rem 0; + overflow-x: auto; + + table { + border-collapse: collapse; + margin: 0; + overflow: hidden; + table-layout: fixed; + width: 100%; + + td, + th { + border: 1px solid var(--uui-color-border); + box-sizing: border-box; + min-width: 1em; + padding: 6px 8px; + position: relative; + vertical-align: top; + + > * { + margin-bottom: 0; + } + } + + th { + background-color: var(--uui-color-background); + font-weight: bold; + text-align: left; + } + + .selectedCell:after { + background: var(--uui-color-surface-emphasis); + content: ''; + left: 0; + right: 0; + top: 0; + bottom: 0; + pointer-events: none; + position: absolute; + z-index: 2; + } + + .column-resize-handle { + background-color: var(--uui-color-default); + bottom: -2px; + pointer-events: none; + position: absolute; + right: -2px; + top: 0; + width: 3px; + } + } + + .resize-cursor { + cursor: ew-resize; + cursor: col-resize; + } + } + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbInputTiptapElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-fixed-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-fixed-menu.element.ts new file mode 100644 index 0000000000..37c837f0ce --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-fixed-menu.element.ts @@ -0,0 +1,125 @@ +import type { UmbTiptapToolbarValue } from '../../extensions/types.js'; +import { css, customElement, html, map, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbExtensionsElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; + +import '../toolbar/tiptap-toolbar-dropdown-base.element.js'; + +const elementName = 'umb-tiptap-fixed-menu'; + +@customElement(elementName) +export class UmbTiptapFixedMenuElement extends UmbLitElement { + #attached = false; + #extensionsController?: UmbExtensionsElementAndApiInitializer; + + @state() + private _lookup?: Map; + + @property({ type: Boolean, reflect: true }) + readonly = false; + + @property({ attribute: false }) + editor?: Editor; + + @property({ attribute: false }) + configuration?: UmbPropertyEditorConfigCollection; + + @property({ attribute: false }) + toolbar: UmbTiptapToolbarValue = [[[]]]; + + override connectedCallback(): void { + super.connectedCallback(); + this.#attached = true; + this.#observeExtensions(); + } + override disconnectedCallback(): void { + this.#attached = false; + this.#extensionsController?.destroy(); + this.#extensionsController = undefined; + super.disconnectedCallback(); + } + + #observeExtensions(): void { + if (!this.#attached) return; + this.#extensionsController?.destroy(); + + this.#extensionsController = new UmbExtensionsElementAndApiInitializer( + this, + umbExtensionsRegistry, + 'tiptapToolbarExtension', + [], + (manifest) => this.toolbar.flat(2).includes(manifest.alias), + (extensionControllers) => { + this._lookup = new Map(extensionControllers.map((ext) => [ext.alias, ext.component])); + }, + ); + + this.#extensionsController.apiProperties = { configuration: this.configuration }; + this.#extensionsController.elementProperties = { editor: this.editor, configuration: this.configuration }; + } + + override render() { + return html`${map(this.toolbar, (row, rowIndex) => + map( + row, + (group, groupIndex) => + html`${map(group, (alias, aliasIndex) => { + const newRow = rowIndex !== 0 && groupIndex === 0 && aliasIndex === 0; + return html`
+ ${this._lookup?.get(alias)} +
`; + })} +
`, + ), + )} `; + } + + static override readonly styles = css` + :host([readonly]) { + pointer-events: none; + background-color: var(--uui-color-surface-alt); + } + + :host { + border-radius: var(--uui-border-radius); + border: 1px solid var(--uui-color-border); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background-color: var(--uui-color-surface); + color: var(--color-text); + display: grid; + grid-template-columns: repeat(auto-fill, 10px); + grid-auto-flow: row; + position: sticky; + top: -25px; + left: 0px; + right: 0px; + padding: var(--uui-size-space-3); + z-index: 9999999; + } + + .item { + grid-column: span 3; + } + + .separator { + background-color: var(--uui-color-border); + width: 1px; + place-self: center; + height: 22px; + } + .separator:last-child, + .separator:has(+ [data-new-row]) { + display: none; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbTiptapFixedMenuElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-hover-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-hover-menu.element.ts new file mode 100644 index 0000000000..36a75174bd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/input-tiptap/tiptap-hover-menu.element.ts @@ -0,0 +1,53 @@ +import { LitElement, css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +@customElement('umb-tiptap-hover-menu') +export class UmbTiptapHoverMenuElement extends LitElement { + @property({ attribute: false }) + get editor() { + return this.#editor; + } + set editor(value) { + const oldValue = this.#editor; + if (value === oldValue) { + return; + } + this.#editor = value; + this.#editor?.on('selectionUpdate', this.#onUpdate); + this.#editor?.on('update', this.#onUpdate); + } + #editor?: Editor; + + override connectedCallback(): void { + super.connectedCallback(); + this.setAttribute('popover', ''); + } + + readonly #onUpdate = () => { + if (this.editor?.isActive('link')) { + // show the popover + this.showPopover(); + } else { + this.requestUpdate(); + } + }; + + override render() { + return html``; + } + + static override readonly styles = css` + :host { + position: fixed; + background-color: var(--uui-color-surface-alt); + border: 1px solid var(--uui-color-border); + border-radius: var(--uui-size-border-radius); + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-hover-menu': UmbTiptapHoverMenuElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts new file mode 100644 index 0000000000..a2311fa601 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts @@ -0,0 +1,33 @@ +import { UmbTiptapToolbarButtonElement } from './tiptap-toolbar-button.element.js'; +import { customElement, html, ifDefined, when } from '@umbraco-cms/backoffice/external/lit'; + +const elementName = 'umb-tiptap-toolbar-button-disabled'; + +@customElement(elementName) +export class UmbTiptapToolbarButtonDisabledElement extends UmbTiptapToolbarButtonElement { + override render() { + return html` + (this.api && this.editor ? this.api.execute(this.editor) : null)}> + ${when( + this.manifest?.meta.icon, + () => html``, + () => html`${this.manifest?.meta.label}`, + )} + + `; + } +} + +export { UmbTiptapToolbarButtonDisabledElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbTiptapToolbarButtonDisabledElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button.element.ts new file mode 100644 index 0000000000..35212303be --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-button.element.ts @@ -0,0 +1,66 @@ +import type { ManifestTiptapToolbarExtensionButtonKind } from '../../extensions/index.js'; +import type { UmbTiptapToolbarElementApi } from '../../extensions/types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; +import { customElement, html, ifDefined, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +const elementName = 'umb-tiptap-toolbar-button'; + +@customElement(elementName) +export class UmbTiptapToolbarButtonElement extends UmbLitElement { + public api?: UmbTiptapToolbarElementApi; + public editor?: Editor; + public manifest?: ManifestTiptapToolbarExtensionButtonKind; + + @state() + protected isActive = false; + + override connectedCallback() { + super.connectedCallback(); + + if (this.editor) { + this.editor.on('selectionUpdate', this.#onEditorUpdate); + this.editor.on('update', this.#onEditorUpdate); + } + } + + override disconnectedCallback() { + super.disconnectedCallback(); + + if (this.editor) { + this.editor.off('selectionUpdate', this.#onEditorUpdate); + this.editor.off('update', this.#onEditorUpdate); + } + } + + readonly #onEditorUpdate = () => { + if (this.api && this.editor && this.manifest) { + this.isActive = this.api.isActive(this.editor); + } + }; + + override render() { + return html` + (this.api && this.editor ? this.api.execute(this.editor) : null)}> + ${when( + this.manifest?.meta.icon, + () => html``, + () => html`${this.manifest?.meta.label}`, + )} + + `; + } +} + +export { UmbTiptapToolbarButtonElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbTiptapToolbarButtonElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-dropdown-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-dropdown-base.element.ts new file mode 100644 index 0000000000..a5308a8539 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/components/toolbar/tiptap-toolbar-dropdown-base.element.ts @@ -0,0 +1,120 @@ +import { css, html, nothing, repeat, type TemplateResult } from '@umbraco-cms/backoffice/external/lit'; +import type { PopoverContainerPlacement, UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +export type TiptapDropdownItem = { + alias: string; + label: string; + nested?: TiptapDropdownItem[]; + execute?: () => void; + isActive?: () => boolean; +}; + +export abstract class UmbTiptapToolbarDropdownBaseElement extends UmbLitElement { + protected abstract get items(): TiptapDropdownItem[]; + protected abstract get label(): string; + + readonly #onMouseEnter = (popoverId: string) => { + const popover = this.shadowRoot?.querySelector(`#${this.makeAlias(popoverId)}`) as UUIPopoverContainerElement; + if (!popover) return; + popover.showPopover(); + }; + + readonly #onMouseLeave = (popoverId: string) => { + popoverId = popoverId.replace(/\s/g, '-').toLowerCase(); + const popover = this.shadowRoot?.querySelector(`#${this.makeAlias(popoverId)}`) as UUIPopoverContainerElement; + if (!popover) return; + popover.hidePopover(); + }; + + protected makeAlias(label: string) { + return label.replace(/\s/g, '-').toLowerCase(); + } + + protected renderItem(item: TiptapDropdownItem): TemplateResult { + return html` + + `; + } + + protected renderItems( + label: string, + items: Array, + placement: PopoverContainerPlacement = 'right-start', + ): TemplateResult { + return html` +
+ ${repeat( + items, + (item) => item.alias, + (item) => html`${this.renderItem(item)}`, + )} +
+
`; + } + protected override render() { + return html` + + ${this.renderItems(this.label, this.items, 'bottom-start')} + `; + } + + static override readonly styles = [ + UmbTextStyles, + css` + button { + border: unset; + background-color: unset; + font: unset; + text-align: unset; + } + + uui-symbol-expand { + position: absolute; + right: 5px; + top: 5px; + } + + .label { + border-radius: var(--uui-border-radius); + width: 100%; + box-sizing: border-box; + align-content: center; + padding: var(--uui-size-space-1) var(--uui-size-space-3); + padding-right: 21px; + align-items: center; + cursor: pointer; + color: var(--uui-color-text); + position: relative; + } + + .label:hover { + background: var(--uui-color-surface-alt); + color: var(--uui-color-interactive-emphasis); + } + + .selected-value { + background: var(--uui-color-surface-alt); + } + + .popover-content { + background: var(--uui-color-surface); + border-radius: var(--uui-border-radius); + box-shadow: var(--uui-shadow-depth-3); + padding: var(--uui-size-space-1); + } + `, + ]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/embedded-media.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/embedded-media.extension.ts new file mode 100644 index 0000000000..0e64a0153a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/embedded-media.extension.ts @@ -0,0 +1,6 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import { umbEmbeddedMedia } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapEmbeddedMediaExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions = () => [umbEmbeddedMedia.configure({ inline: true })]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/figure.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/figure.extension.ts new file mode 100644 index 0000000000..1da6bdec35 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/figure.extension.ts @@ -0,0 +1,6 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import { Figure, Figcaption } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapFigureExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions = () => [Figcaption, Figure]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/image.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/image.extension.ts new file mode 100644 index 0000000000..7254da3b61 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/image.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import { UmbImage } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapImageExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions() { + return [UmbImage.configure({ inline: true })]; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/link.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/link.extension.ts new file mode 100644 index 0000000000..4800000fda --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/link.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import { UmbLink } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapLinkExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions() { + return [UmbLink.configure({ openOnClick: false })]; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/media-upload.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/media-upload.extension.ts new file mode 100644 index 0000000000..1af085137c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/media-upload.extension.ts @@ -0,0 +1,128 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import type { UmbTiptapExtensionArgs } from '../types.js'; +import { imageSize } from '@umbraco-cms/backoffice/utils'; +import { Extension } from '@umbraco-cms/backoffice/external/tiptap'; +import { TemporaryFileStatus, UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file'; + +export default class UmbTiptapMediaUploadExtensionApi extends UmbTiptapExtensionApiBase { + #configuration?: UmbPropertyEditorConfigCollection; + + /** + * @returns {number} The maximum width of uploaded images + */ + get maxWidth(): number { + const maxImageSize = parseInt(this.#configuration?.getValueByAlias('maxImageSize') ?? '', 10); + return isNaN(maxImageSize) ? 500 : maxImageSize; + } + + /** + * @returns {Array} The allowed mime types for uploads + */ + get allowedFileTypes(): string[] { + return ( + this.#configuration?.getValueByAlias('allowedFileTypes') ?? ['image/jpeg', 'image/png', 'image/gif'] + ); + } + + readonly #manager = new UmbTemporaryFileManager(this); + readonly #localize = new UmbLocalizationController(this); + #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host); + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => { + this.#notificationContext = instance; + }); + } + + getTiptapExtensions(args: UmbTiptapExtensionArgs) { + this.#configuration = args?.configuration; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + return [ + Extension.create({ + name: 'umbMediaUpload', + onCreate() { + this.parent?.(); + const host = this.editor.view.dom; + + host.addEventListener('dragover', (event) => { + // Required to allow drop events + event.preventDefault(); + }); + + host.addEventListener('drop', (event) => { + event.preventDefault(); + + const files = event.dataTransfer?.files; + if (!files) return; + + self.#uploadTemporaryFile(files, this.editor); + }); + }, + }), + ]; + } + + /** + * Uploads the files to the server and inserts them into the editor as data URIs. + * The server will replace the data URI with a proper URL when the content is saved. + * @param {FileList} files The files to upload. + * @param {Editor} editor The editor to insert the images into. + */ + async #uploadTemporaryFile(files: FileList, editor: Editor): Promise { + const filteredFiles = this.#filterFiles(files); + const fileModels = filteredFiles.map((file) => this.#mapFileToTemporaryFile(file)); + + this.dispatchEvent(new CustomEvent('rte.file.uploading', { composed: true, bubbles: true, detail: fileModels })); + + const uploads = await this.#manager.upload(fileModels); + const maxImageSize = this.maxWidth; + + uploads.forEach(async (upload) => { + if (upload.status !== TemporaryFileStatus.SUCCESS) { + this.#notificationContext?.peek('danger', { + data: { + headline: upload.file.name, + message: this.#localize.term('errors_dissallowedMediaType'), + }, + }); + return; + } + + const { width, height } = await imageSize(URL.createObjectURL(upload.file), { maxWidth: maxImageSize }); + + editor + .chain() + .focus() + .setImage({ + src: URL.createObjectURL(upload.file), + width: width.toString(), + height: height.toString(), + 'data-tmpimg': upload.temporaryUnique, + }) + .run(); + }); + + this.dispatchEvent(new CustomEvent('rte.file.uploaded', { composed: true, bubbles: true, detail: uploads })); + } + + #mapFileToTemporaryFile(file: File): UmbTemporaryFileModel { + return { + file, + temporaryUnique: UmbId.new(), + }; + } + + #filterFiles(files: FileList): File[] { + return Array.from(files).filter((file) => this.allowedFileTypes.includes(file.type)); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/subscript.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/subscript.extension.ts new file mode 100644 index 0000000000..57bfb91472 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/subscript.extension.ts @@ -0,0 +1,6 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import { Subscript } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapBoldExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions = () => [Subscript]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/superscript.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/superscript.extension.ts new file mode 100644 index 0000000000..453efb13e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/superscript.extension.ts @@ -0,0 +1,6 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import { Superscript } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapBoldExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions = () => [Superscript]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/table.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/table.extension.ts new file mode 100644 index 0000000000..d8c00b5d39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/table.extension.ts @@ -0,0 +1,6 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import { Table, TableHeader, TableRow, TableCell } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapTableExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions = () => [Table.configure({ resizable: true }), TableHeader, TableRow, TableCell]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/text-align.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/text-align.extension.ts new file mode 100644 index 0000000000..9855c4d150 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/text-align.extension.ts @@ -0,0 +1,10 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import { TextAlign } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapTextAlignExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions = () => [ + TextAlign.configure({ + types: ['heading', 'paragraph', 'blockquote', 'orderedList', 'bulletList', 'codeBlock'], + }), + ]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/underline.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/underline.extension.ts new file mode 100644 index 0000000000..f9663f91c8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/core/underline.extension.ts @@ -0,0 +1,6 @@ +import { UmbTiptapExtensionApiBase } from '../types.js'; +import { Underline } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapUnderlineExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions = () => [Underline]; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/index.ts new file mode 100644 index 0000000000..8f09498433 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/index.ts @@ -0,0 +1,3 @@ +export type * from './tiptap-extension.js'; +export type * from './tiptap-toolbar-extension.js'; +export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts new file mode 100644 index 0000000000..ed8f9f7f56 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/manifests.ts @@ -0,0 +1,479 @@ +import type { ManifestTiptapExtension } from './tiptap-extension.js'; +import type { ManifestTiptapToolbarExtension } from './tiptap-toolbar-extension.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +const kinds: Array = [ + { + type: 'kind', + alias: 'Umb.Kind.Button', + matchKind: 'button', + matchType: 'tiptapToolbarExtension', + manifest: { + element: () => import('../components/toolbar/tiptap-toolbar-button.element.js'), + }, + }, +]; + +const coreExtensions: Array = [ + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.Embed', + name: 'Embed Tiptap Extension', + api: () => import('./core/embedded-media.extension.js'), + meta: { + icon: 'icon-embed', + label: '#general_embed', + group: '#tiptap_extGroup_media', + }, + }, + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.Link', + name: 'Link Tiptap Extension', + api: () => import('./core/link.extension.js'), + meta: { + icon: 'icon-link', + label: '#defaultdialogs_urlLinkPicker', + group: '#tiptap_extGroup_interactive', + }, + }, + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.Figure', + name: 'Figure Tiptap Extension', + api: () => import('./core/figure.extension.js'), + meta: { + icon: 'icon-frame', + label: 'Figure', + group: '#tiptap_extGroup_media', + }, + }, + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.Image', + name: 'Image Tiptap Extension', + api: () => import('./core/image.extension.js'), + meta: { + icon: 'icon-picture', + label: 'Image', + group: '#tiptap_extGroup_media', + }, + }, + { + type: 'tiptapExtension', + kind: 'button', + alias: 'Umb.Tiptap.Subscript', + name: 'Subscript Tiptap Extension', + api: () => import('./core/subscript.extension.js'), + meta: { + icon: 'icon-subscript', + label: 'Subscript', + group: '#tiptap_extGroup_formatting', + }, + }, + { + type: 'tiptapExtension', + kind: 'button', + alias: 'Umb.Tiptap.Superscript', + name: 'Superscript Tiptap Extension', + api: () => import('./core/superscript.extension.js'), + meta: { + icon: 'icon-superscript', + label: 'Superscript', + group: '#tiptap_extGroup_formatting', + }, + }, + { + type: 'tiptapExtension', + kind: 'button', + alias: 'Umb.Tiptap.Table', + name: 'Table Tiptap Extension', + api: () => import('./core/table.extension.js'), + meta: { + icon: 'icon-table', + label: 'Table', + group: '#tiptap_extGroup_interactive', + }, + }, + { + type: 'tiptapExtension', + kind: 'button', + alias: 'Umb.Tiptap.Underline', + name: 'Underline Tiptap Extension', + api: () => import('./core/underline.extension.js'), + meta: { + icon: 'icon-underline', + label: 'Underline', + group: '#tiptap_extGroup_formatting', + }, + }, + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.TextAlign', + name: 'Text Align Tiptap Extension', + api: () => import('./core/text-align.extension.js'), + meta: { + icon: 'icon-text-align-justify', + label: 'Text Align', + group: '#tiptap_extGroup_formatting', + }, + }, + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.MediaUpload', + name: 'Media Upload Tiptap Extension', + api: () => import('./core/media-upload.extension.js'), + meta: { + icon: 'icon-image-up', + label: 'Media Upload', + group: '#tiptap_extGroup_media', + }, + }, +]; + +const toolbarExtensions: Array = [ + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Blockquote', + name: 'Blockquote Tiptap Extension', + api: () => import('./toolbar/blockquote.extension.js'), + weight: 995, + meta: { + alias: 'blockquote', + icon: 'icon-blockquote', + label: 'Blockquote', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Bold', + name: 'Bold Tiptap Extension', + api: () => import('./toolbar/bold.extension.js'), + weight: 999, + meta: { + alias: 'bold', + icon: 'icon-bold', + label: 'Bold', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.CodeBlock', + name: 'Code Block Tiptap Extension', + api: () => import('./toolbar/code-block.extension.js'), + weight: 994, + meta: { + alias: 'codeBlock', + icon: 'icon-code', + label: 'Code Block', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.BulletList', + name: 'Bullet List Tiptap Extension', + api: () => import('./toolbar/bullet-list.extension.js'), + weight: 993, + meta: { + alias: 'bulletList', + icon: 'icon-bulleted-list', + label: 'Bullet List', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.OrderedList', + name: 'Ordered List Tiptap Extension', + api: () => import('./toolbar/ordered-list.extension.js'), + weight: 992, + meta: { + alias: 'orderedList', + icon: 'icon-ordered-list', + label: 'Ordered List', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Redo', + name: 'Redo Tiptap Extension', + api: () => import('./toolbar/redo.extension.js'), + element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'), + weight: 994, + meta: { + alias: 'redo', + icon: 'icon-redo', + label: 'Redo', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Strike', + name: 'Strike Tiptap Extension', + api: () => import('./toolbar/strike.extension.js'), + weight: 996, + meta: { + alias: 'strike', + icon: 'icon-strikethrough', + label: 'Strike', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Subscript', + name: 'Subscript Tiptap Extension', + api: () => import('./toolbar/subscript.extension.js'), + weight: 1010, + meta: { + alias: 'subscript', + icon: 'icon-subscript', + label: 'Subscript', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Superscript', + name: 'Superscript Tiptap Extension', + api: () => import('./toolbar/superscript.extension.js'), + weight: 1011, + meta: { + alias: 'superscript', + icon: 'icon-superscript', + label: 'Superscript', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Table', + name: 'Table Tiptap Extension', + api: () => import('./toolbar/table.extension.js'), + weight: 909, + meta: { + alias: 'table', + icon: 'icon-table', + label: 'Table', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Heading1', + name: 'Heading 1 Tiptap Extension', + api: () => import('./toolbar/heading1.extension.js'), + weight: 949, + meta: { + alias: 'heading1', + icon: 'icon-heading-1', + label: 'Heading 1', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Heading2', + name: 'Heading 2 Tiptap Extension', + api: () => import('./toolbar/heading2.extension.js'), + weight: 948, + meta: { + alias: 'heading2', + icon: 'icon-heading-2', + label: 'Heading 2', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Heading3', + name: 'Heading 3 Tiptap Extension', + api: () => import('./toolbar/heading3.extension.js'), + weight: 947, + meta: { + alias: 'heading3', + icon: 'icon-heading-3', + label: 'Heading 3', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.HorizontalRule', + name: 'Horizontal Rule Tiptap Extension', + api: () => import('./toolbar/horizontal-rule.extension.js'), + weight: 991, + meta: { + alias: 'horizontalRule', + icon: 'icon-horizontal-rule', + label: 'Horizontal Rule', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Italic', + name: 'Italic Tiptap Extension', + api: () => import('./toolbar/italic.extension.js'), + weight: 998, + meta: { + alias: 'italic', + icon: 'icon-italic', + label: 'Italic', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextAlignCenter', + name: 'Text Align Center Tiptap Extension', + api: () => import('./toolbar/text-align-center.extension.js'), + weight: 918, + meta: { + alias: 'text-align-center', + icon: 'icon-text-align-center', + label: 'Text Align Center', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextAlignJustify', + name: 'Text Align Justify Tiptap Extension', + api: () => import('./toolbar/text-align-justify.extension.js'), + weight: 916, + meta: { + alias: 'text-align-justify', + icon: 'icon-text-align-justify', + label: 'Text Align Justify', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextAlignLeft', + name: 'Text Align Left Tiptap Extension', + api: () => import('./toolbar/text-align-left.extension.js'), + weight: 919, + meta: { + alias: 'text-align-left', + icon: 'icon-text-align-left', + label: 'Text Align Left', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.TextAlignRight', + name: 'Text Align Right Tiptap Extension', + api: () => import('./toolbar/text-align-right.extension.js'), + weight: 917, + meta: { + alias: 'text-align-right', + icon: 'icon-text-align-right', + label: 'Text Align Right', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Underline', + name: 'Underline Tiptap Extension', + api: () => import('./toolbar/underline.extension.js'), + weight: 997, + meta: { + alias: 'underline', + icon: 'icon-underline', + label: 'Underline', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Undo', + name: 'Undo Tiptap Extension', + api: () => import('./toolbar/undo.extension.js'), + element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'), + weight: 994, + meta: { + alias: 'undo', + icon: 'icon-undo', + label: 'Undo', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Unlink', + name: 'Unlink Tiptap Extension', + api: () => import('./toolbar/unlink.extension.js'), + element: () => import('../components/toolbar/tiptap-toolbar-button-disabled.element.js'), + weight: 101, + meta: { + alias: 'unlink', + icon: 'icon-unlink', + label: 'Unlink', + }, + }, +]; + +const umbToolbarExtensions: Array = [ + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.SourceEditor', + name: 'Source Editor Tiptap Extension', + api: () => import('./toolbar/source-editor.extension.js'), + meta: { + alias: 'umbSourceEditor', + icon: 'icon-code-xml', + label: '#general_viewSourceCode', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.Link', + name: 'Link Tiptap Extension', + api: () => import('./toolbar/link.extension.js'), + meta: { + alias: 'umbLink', + icon: 'icon-link', + label: '#defaultdialogs_urlLinkPicker', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.MediaPicker', + name: 'Media Picker Tiptap Extension', + api: () => import('./toolbar/media-picker.extension.js'), + meta: { + alias: 'umbMedia', + icon: 'icon-picture', + label: 'Media picker', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.EmbeddedMedia', + name: 'Embedded Media Tiptap Extension', + api: () => import('./toolbar/embedded-media.extension.js'), + meta: { + alias: 'umbEmbeddedMedia', + icon: 'icon-embed', + label: '#general_embed', + }, + }, +]; + +const extensions = [...coreExtensions, ...toolbarExtensions, ...umbToolbarExtensions]; + +export const manifests = [...kinds, ...extensions]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-extension.ts new file mode 100644 index 0000000000..619792c6bb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-extension.ts @@ -0,0 +1,20 @@ +import type { UmbTiptapExtensionApi } from './types.js'; +import type { ManifestApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestTiptapExtension + extends ManifestApi { + type: 'tiptapExtension'; + meta: MetaType; +} + +export interface MetaTiptapExtension { + icon: string; + label: string; + group: string; +} + +declare global { + interface UmbExtensionManifestMap { + tiptapExtension: ManifestTiptapExtension; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-toolbar-extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-toolbar-extension.ts new file mode 100644 index 0000000000..a97d658328 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/tiptap-toolbar-extension.ts @@ -0,0 +1,29 @@ +import type { UmbTiptapToolbarElementApi } from './types.js'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestTiptapToolbarExtension< + MetaType extends MetaTiptapToolbarExtension = MetaTiptapToolbarExtension, +> extends ManifestElementAndApi { + type: 'tiptapToolbarExtension'; + meta: MetaType; +} + +export interface MetaTiptapToolbarExtension { + alias: string; + icon: string; + label: string; +} + +export interface ManifestTiptapToolbarExtensionButtonKind< + MetaType extends MetaTiptapToolbarExtension = MetaTiptapToolbarExtension, +> extends ManifestTiptapToolbarExtension { + type: 'tiptapToolbarExtension'; + kind: 'button'; +} + +declare global { + interface UmbExtensionManifestMap { + tiptapToolbarExtension: ManifestTiptapToolbarExtension | ManifestTiptapToolbarExtensionButtonKind; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/blockquote.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/blockquote.extension.ts new file mode 100644 index 0000000000..f061e7e6b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/blockquote.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarBlockquoteExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().toggleBlockquote().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bold.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bold.extension.ts new file mode 100644 index 0000000000..5ec1124963 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bold.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarBoldExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().toggleBold().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bullet-list.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bullet-list.extension.ts new file mode 100644 index 0000000000..983adb0c3d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/bullet-list.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarBulletListExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().toggleBulletList().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/code-block.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/code-block.extension.ts new file mode 100644 index 0000000000..a3a30b0f5d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/code-block.extension.ts @@ -0,0 +1,9 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarCodeBlockExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + // editor.chain().focus().toggleCode().run(); + editor?.chain().focus().toggleCodeBlock().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/embedded-media.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/embedded-media.extension.ts new file mode 100644 index 0000000000..b50872c1f0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/embedded-media.extension.ts @@ -0,0 +1,42 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import { umbEmbeddedMedia } from '@umbraco-cms/backoffice/external/tiptap'; +import { UMB_EMBEDDED_MEDIA_MODAL } from '@umbraco-cms/backoffice/embedded-media'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarEmbeddedMediaExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive = (editor: Editor) => editor.isActive(umbEmbeddedMedia.name) === true; + + override async execute(editor?: Editor) { + const data = { + constrain: false, + height: 240, + width: 360, + url: '', + }; + + const attrs = editor?.getAttributes(umbEmbeddedMedia.name); + if (attrs) { + data.constrain = attrs['data-embed-constrain']; + data.height = attrs['data-embed-height']; + data.width = attrs['data-embed-width']; + data.url = attrs['data-embed-url']; + } + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modalHandler = modalManager.open(this, UMB_EMBEDDED_MEDIA_MODAL, { data }); + + if (!modalHandler) return; + + const result = await modalHandler.onSubmit().catch(() => undefined); + if (!result) return; + + editor?.commands.setEmbeddedMedia({ + markup: result.markup, + url: result.url, + constrain: result.constrain, + height: result.height?.toString(), + width: result.width?.toString(), + }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading1.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading1.extension.ts new file mode 100644 index 0000000000..b301f438e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading1.extension.ts @@ -0,0 +1,12 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarHeading1ExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive(editor?: Editor) { + return editor?.isActive('heading', { level: 1 }) === true; + } + + override execute(editor?: Editor) { + editor?.chain().focus().toggleHeading({ level: 1 }).run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading2.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading2.extension.ts new file mode 100644 index 0000000000..663bc09d22 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading2.extension.ts @@ -0,0 +1,12 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarHeading2ExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive(editor?: Editor) { + return editor?.isActive('heading', { level: 2 }) === true; + } + + override execute(editor?: Editor) { + editor?.chain().focus().toggleHeading({ level: 2 }).run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading3.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading3.extension.ts new file mode 100644 index 0000000000..3c3aed7cba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/heading3.extension.ts @@ -0,0 +1,12 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarHeading3ExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive(editor?: Editor) { + return editor?.isActive('heading', { level: 3 }) === true; + } + + override execute(editor?: Editor) { + editor?.chain().focus().toggleHeading({ level: 3 }).run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/horizontal-rule.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/horizontal-rule.extension.ts new file mode 100644 index 0000000000..58836d2257 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/horizontal-rule.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarHorizontalRuleExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().setHorizontalRule().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/italic.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/italic.extension.ts new file mode 100644 index 0000000000..2ac2c77649 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/italic.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarItalicExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().toggleItalic().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/link.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/link.extension.ts new file mode 100644 index 0000000000..d78648f6fb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/link.extension.ts @@ -0,0 +1,140 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import { UmbLink } from '@umbraco-cms/backoffice/external/tiptap'; +import { UMB_LINK_PICKER_MODAL } from '@umbraco-cms/backoffice/multi-url-picker'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; +import type { UmbLinkPickerLink } from '@umbraco-cms/backoffice/multi-url-picker'; +import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; + +export default class UmbTiptapToolbarLinkExtensionApi extends UmbTiptapToolbarElementApiBase { + override async execute(editor?: Editor) { + const attrs = editor?.getAttributes(UmbLink.name) ?? {}; + const link = this.#getLinkData(attrs); + const data = { config: {}, index: null }; + const value = { link }; + + const overlaySize = this.configuration?.getValueByAlias('overlaySize') ?? 'small'; + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modalHandler = modalManager.open(this, UMB_LINK_PICKER_MODAL, { data, value, modal: { size: overlaySize } }); + + if (!modalHandler) return; + + const result = await modalHandler.onSubmit().catch(() => undefined); + if (!result?.link) return; + + const linkAttrs = this.#parseLinkData(result.link); + + if (linkAttrs) { + editor?.chain().focus().extendMarkRange(UmbLink.name).setUmbLink(linkAttrs).run(); + } else { + editor?.chain().focus().extendMarkRange(UmbLink.name).unsetLink().run(); + } + } + + #getLinkData(attrs: Record): UmbLinkPickerLink { + const queryString = attrs['data-anchor']; + const url = attrs.href?.substring(0, attrs.href.length - (queryString?.length ?? 0)); + const unique = url?.includes('localLink:') ? url.substring(url.indexOf(':') + 1, url.indexOf('}')) : null; + + return { + name: attrs.title, + queryString, + target: attrs.target, + type: attrs.type, + unique, + url, + }; + } + + #parseLinkData(link: UmbLinkPickerLink) { + const { name, target, type, unique } = link; + let { queryString, url } = link; + + // If an anchor exists, check that it is appropriately prefixed + queryString = this.#queryStringFromUrl(queryString); + + // The href might be an external url, so check the value for an anchor/querystring; + // `href` has the anchor re-appended later, hence the reset here to avoid duplicating the anchor + if (!queryString) { + const extractedInfo = this.#extractUrlAndQueryString(url, queryString); + url = extractedInfo.url; + queryString = extractedInfo.queryString; + } + + // If we have a unique id, it must be a `/{localLink:guid}` + if (unique) { + url = `/{localLink:${unique}}`; + } else { + // If it's an email address and not `//user@domain.com` and protocol (e.g. mailto:, sip:) is not specified; + // then we'll assume it should be a "mailto" link. + url = this.#transformURLToMailto(url); + + url = this.#ensureHttpProtocol(url); + } + + const anchor = this.#getAnchorFromQueryString(queryString); + + if (anchor) url += anchor; + + if (!url) return null; + + return { + type: type ?? 'external', + href: url, + 'data-anchor': anchor, + target, + title: name ?? url, + }; + } + + #extractUrlAndQueryString(url: string | null | undefined, queryString: string | null) { + const urlParts = url?.split(/([#?])/); + if (urlParts?.length === 3) { + url = urlParts[0]; + queryString = urlParts[1] + urlParts[2]; + } + return { url, queryString }; + } + + /** + * If the URL is prefixed "www.", then prepend "http://" protocol scheme. + */ + #ensureHttpProtocol(url: string | null | undefined) { + if (!url) return null; + if (/^\s*www\./i.test(url)) { + url = `http://${url}`; + } + return url; + } + + /** + * If the URL is an email address, then prepend "mailto:" protocol scheme. + */ + #transformURLToMailto(url: string | null | undefined) { + if (!url) return null; + if (url?.includes('@') && !url.includes('//') && !url.includes(':')) { + url = `mailto:${url}`; + } + return url; + } + + /** + * If the URL contains an anchor, then return the anchor. + */ + #getAnchorFromQueryString(queryString: string | null) { + if (!queryString) return null; + return queryString.startsWith('#') || queryString.startsWith('?') ? queryString : null; + } + + /** + * If the query string does not start with "?" or "#", then prepend it. + */ + #queryStringFromUrl(queryString: string | null | undefined) { + if (!queryString) return null; + if (!queryString.startsWith('?') && !queryString.startsWith('#')) { + queryString = (queryString.startsWith('=') ? '#' : '?') + queryString; + } + return queryString; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/media-picker.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/media-picker.extension.ts new file mode 100644 index 0000000000..0ba24357bd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/media-picker.extension.ts @@ -0,0 +1,135 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import { getGuidFromUdi, getProcessedImageUrl, imageSize } from '@umbraco-cms/backoffice/utils'; +import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, UMB_MEDIA_PICKER_MODAL } from '@umbraco-cms/backoffice/media'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbMediaCaptionAltTextModalValue } from '@umbraco-cms/backoffice/media'; + +export default class UmbTiptapToolbarMediaPickerToolbarExtensionApi extends UmbTiptapToolbarElementApiBase { + #modalManager?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; + + /** + * @returns {number} The maximum width of uploaded images + */ + get maxWidth(): number { + const maxImageSize = parseInt(this.configuration?.getValueByAlias('maxImageSize') ?? '', 10); + return isNaN(maxImageSize) ? 500 : maxImageSize; + } + + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => { + this.#modalManager = instance; + }); + } + + override isActive(editor?: Editor) { + return editor?.isActive('image') === true || editor?.isActive('figure') === true; + } + + override async execute(editor: Editor) { + const currentTarget = editor.getAttributes('image'); + const figure = editor.getAttributes('figure'); + + let currentMediaUdi: string | undefined = undefined; + if (currentTarget?.['data-udi']) { + currentMediaUdi = getGuidFromUdi(currentTarget['data-udi']); + } + + let currentAltText: string | undefined = undefined; + if (currentTarget?.alt) { + currentAltText = currentTarget.alt; + } + + let currentCaption: string | undefined = undefined; + if (figure?.figcaption) { + currentCaption = figure.figcaption; + } + + const selection = await this.#openMediaPicker(currentMediaUdi); + if (!selection?.length) return; + + const mediaGuid = selection[0]; + const media = await this.#showMediaCaptionAltText(mediaGuid, currentAltText, currentCaption); + if (!media) return; + + this.#insertInEditor(editor, mediaGuid, media); + } + + async #openMediaPicker(currentMediaUdi?: string) { + const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, { + data: { + multiple: false, + //startNodeIsVirtual, + }, + value: { + selection: currentMediaUdi ? [currentMediaUdi] : [], + }, + }); + + if (!modalHandler) return; + + const { selection } = await modalHandler.onSubmit().catch(() => ({ selection: undefined })); + + return selection; + } + + async #showMediaCaptionAltText(mediaUnique: string, altText?: string, caption?: string) { + const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_CAPTION_ALT_TEXT_MODAL, { + data: { mediaUnique }, + value: { + url: '', + altText, + caption, + }, + }); + const mediaData = await modalHandler?.onSubmit().catch(() => null); + return mediaData; + } + + async #insertInEditor(editor: Editor, mediaUnique: string, media: UmbMediaCaptionAltTextModalValue) { + if (!media?.url) return; + + const { width, height } = await imageSize(media.url, { maxWidth: this.maxWidth }); + const src = await getProcessedImageUrl(media.url, { width, height, mode: ImageCropModeModel.MAX }); + + const img = { + alt: media.altText, + src, + 'data-udi': `umb://media/${mediaUnique.replace(/-/g, '')}`, + width: width.toString(), + height: height.toString(), + }; + + if (media.caption) { + return editor.commands.insertContent({ + type: 'figure', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'image', + attrs: img, + }, + ], + }, + { + type: 'figcaption', + content: [ + { + type: 'text', + text: media.caption, + }, + ], + }, + ], + }); + } + + return editor.commands.setImage(img); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/ordered-list.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/ordered-list.extension.ts new file mode 100644 index 0000000000..f95d7da3d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/ordered-list.extension.ts @@ -0,0 +1,11 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import { OrderedList, ListItem } from '@umbraco-cms/backoffice/external/tiptap'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarOrderedListExtensionApi extends UmbTiptapToolbarElementApiBase { + getTiptapExtensions = () => [OrderedList, ListItem]; + + override execute(editor?: Editor) { + editor?.chain().focus().toggleOrderedList().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/redo.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/redo.extension.ts new file mode 100644 index 0000000000..0aae301bba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/redo.extension.ts @@ -0,0 +1,12 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarRedoExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive(editor: Editor): boolean { + return editor.can().redo(); + } + + override execute(editor?: Editor) { + editor?.chain().focus().redo().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/source-editor.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/source-editor.extension.ts new file mode 100644 index 0000000000..7ff4fcd4ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/source-editor.extension.ts @@ -0,0 +1,26 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import { UMB_CODE_EDITOR_MODAL } from '@umbraco-cms/backoffice/code-editor'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarSourceEditorExtensionApi extends UmbTiptapToolbarElementApiBase { + override async execute(editor?: Editor) { + if (!editor) return; + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modal = modalManager.open(this, UMB_CODE_EDITOR_MODAL, { + data: { + headline: 'Edit source code', + content: editor?.getHTML() ?? '', + language: 'html', + }, + }); + + if (!modal) return; + + const data = await modal.onSubmit().catch(() => undefined); + if (!data) return; + + editor?.commands.setContent(data.content, true); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/strike.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/strike.extension.ts new file mode 100644 index 0000000000..1428e2009b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/strike.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarStrikeExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().toggleStrike().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/style-select.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/style-select.extension.ts new file mode 100644 index 0000000000..96868aa349 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/style-select.extension.ts @@ -0,0 +1,24 @@ +import { UmbTiptapToolbarDropdownBaseElement, type TiptapDropdownItem } from '../../components/index.js'; +import { customElement, state } from '@umbraco-cms/backoffice/external/lit'; + +const elementName = 'umb-tiptap-style-select-toolbar-element'; + +@customElement(elementName) +export class UmbTiptapToolbarStyleSelectToolbarElement extends UmbTiptapToolbarDropdownBaseElement { + protected override label = 'Style select'; + + @state() + protected override get items(): TiptapDropdownItem[] { + throw new Error('Method not implemented.'); + } + + static override readonly styles = UmbTiptapToolbarDropdownBaseElement.styles; +} + +export { UmbTiptapToolbarStyleSelectToolbarElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbTiptapToolbarStyleSelectToolbarElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/subscript.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/subscript.extension.ts new file mode 100644 index 0000000000..121e5210a6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/subscript.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarSubscriptExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().toggleSubscript().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/superscript.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/superscript.extension.ts new file mode 100644 index 0000000000..d46e966d29 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/superscript.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarSuperscriptExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().toggleSuperscript().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/table.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/table.extension.ts new file mode 100644 index 0000000000..3ef38f6f13 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/table.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarTableExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-center.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-center.extension.ts new file mode 100644 index 0000000000..a027bc18c1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-center.extension.ts @@ -0,0 +1,12 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarTextAlignCenterExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive(editor?: Editor) { + return editor?.isActive({ textAlign: 'center' }) === true; + } + + override execute(editor?: Editor) { + editor?.chain().focus().setTextAlign('center').run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-justify.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-justify.extension.ts new file mode 100644 index 0000000000..38a53fc008 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-justify.extension.ts @@ -0,0 +1,12 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarTextAlignJustifyExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive(editor?: Editor) { + return editor?.isActive({ textAlign: 'justify' }) === true; + } + + override execute(editor?: Editor) { + editor?.chain().focus().setTextAlign('justify').run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-left.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-left.extension.ts new file mode 100644 index 0000000000..86b90b7e81 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-left.extension.ts @@ -0,0 +1,12 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarTextAlignLeftExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive(editor?: Editor) { + return editor?.isActive({ textAlign: 'left' }) === true; + } + + override execute(editor?: Editor) { + editor?.chain().focus().setTextAlign('left').run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-right.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-right.extension.ts new file mode 100644 index 0000000000..8f3984bb5c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/text-align-right.extension.ts @@ -0,0 +1,12 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarTextAlignRightExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive(editor?: Editor) { + return editor?.isActive({ textAlign: 'right' }) === true; + } + + override execute(editor?: Editor) { + editor?.chain().focus().setTextAlign('right').run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/underline.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/underline.extension.ts new file mode 100644 index 0000000000..9a2aab8b50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/underline.extension.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarUnderlineExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor) { + editor?.chain().focus().toggleUnderline().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/undo.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/undo.extension.ts new file mode 100644 index 0000000000..1cc600442b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/undo.extension.ts @@ -0,0 +1,12 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarUndoExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive(editor: Editor): boolean { + return editor.can().undo(); + } + + override execute(editor?: Editor) { + editor?.chain().focus().undo().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/unlink.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/unlink.extension.ts new file mode 100644 index 0000000000..a3137f6ec5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/toolbar/unlink.extension.ts @@ -0,0 +1,10 @@ +import { UmbTiptapToolbarElementApiBase } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarUnlinkExtensionApi extends UmbTiptapToolbarElementApiBase { + override isActive = (editor?: Editor) => editor?.isActive('umbLink') ?? false; + + override execute(editor?: Editor) { + editor?.chain().focus().unsetUmbLink().run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/types.ts new file mode 100644 index 0000000000..927d22cf63 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/extensions/types.ts @@ -0,0 +1,102 @@ +import type { ManifestTiptapExtension } from './tiptap-extension.js'; +import type { ManifestTiptapToolbarExtension } from './tiptap-toolbar-extension.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { Editor, Extension, Mark, Node } from '@umbraco-cms/backoffice/external/tiptap'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; + +export interface UmbTiptapExtensionApi extends UmbApi { + /** + * The manifest for the extension. + */ + manifest?: ManifestTiptapExtension; + + /** + * Sets the editor instance to the extension. + */ + setEditor(editor: Editor): void; + + /** + * Gets the Tiptap extensions for the editor. + */ + getTiptapExtensions(args?: UmbTiptapExtensionArgs): Array; +} + +export abstract class UmbTiptapExtensionApiBase extends UmbControllerBase implements UmbTiptapExtensionApi { + /** + * The manifest for the extension. + */ + manifest?: ManifestTiptapExtension; + + /** + * The editor instance. + */ + protected _editor?: Editor; + + /** + * @inheritdoc + */ + setEditor(editor: Editor): void { + this._editor = editor; + } + + /** + * @inheritdoc + */ + abstract getTiptapExtensions(args?: UmbTiptapExtensionArgs): Array; +} + +export interface UmbTiptapExtensionArgs { + /** + * The data type configuration for the property editor that the editor is used for. + * You can populate this manually if you are using the editor outside of a property editor with the {@link UmbPropertyEditorConfigCollection} object. + * @remark This is only available when the editor is used in a property editor or populated manually. + */ + configuration?: UmbPropertyEditorConfigCollection; +} + +export interface UmbTiptapToolbarElementApi extends UmbApi, UmbTiptapExtensionArgs { + /** + * The manifest for the extension. + */ + manifest?: ManifestTiptapToolbarExtension; + + /** + * Executes the toolbar element action. + */ + execute(editor: Editor): void; + + /** + * Checks if the toolbar element is active. + */ + isActive(editor: Editor): boolean; +} + +export abstract class UmbTiptapToolbarElementApiBase extends UmbControllerBase implements UmbTiptapToolbarElementApi { + /** + * The manifest for the extension. + */ + manifest?: ManifestTiptapToolbarExtension; + + /** + * The data type configuration for the property editor that the editor is used for. + */ + configuration?: UmbPropertyEditorConfigCollection; + + /** + * A method to execute the toolbar element action. + */ + public abstract execute(editor: Editor): void; + + /** + * Informs the toolbar element if it is active or not. It uses the manifest meta alias to check if the toolbar element is active. + * @see {ManifestTiptapToolbarExtension} + * @param {Editor} editor The editor instance. + * @returns {boolean} Returns true if the toolbar element is active. + */ + public isActive(editor: Editor) { + return editor && this.manifest?.meta.alias ? editor?.isActive(this.manifest.meta.alias) : false; + } +} + +export type UmbTiptapToolbarValue = Array>>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/index.ts new file mode 100644 index 0000000000..f0f1ade33d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/index.ts @@ -0,0 +1,2 @@ +export * from './components/index.js'; +export * from './extensions/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/manifests.ts new file mode 100644 index 0000000000..aea7de21b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/manifests.ts @@ -0,0 +1,5 @@ +import { manifests as extensions } from './extensions/manifests.js'; +import { manifests as propertyEditors } from './property-editors/manifests.js'; +import { manifests as plugins } from './plugins/manifests.js'; + +export const manifests = [...extensions, ...propertyEditors, ...plugins]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block-picker-toolbar.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block-picker-toolbar.extension.ts new file mode 100644 index 0000000000..1777a677c8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block-picker-toolbar.extension.ts @@ -0,0 +1,55 @@ +import { UMB_BLOCK_RTE_MANAGER_CONTEXT, UMB_BLOCK_RTE_ENTRIES_CONTEXT } from '@umbraco-cms/backoffice/block-rte'; +import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; +import { UmbTiptapToolbarElementApiBase } from '@umbraco-cms/backoffice/tiptap'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export default class UmbTiptapBlockPickerToolbarExtension extends UmbTiptapToolbarElementApiBase { + #blocks?: Array; + #entriesContext?: typeof UMB_BLOCK_RTE_ENTRIES_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_BLOCK_RTE_MANAGER_CONTEXT, (context) => { + this.observe( + context.blockTypes, + (blockTypes) => { + this.#blocks = blockTypes; + }, + 'blockType', + ); + }); + this.consumeContext(UMB_BLOCK_RTE_ENTRIES_CONTEXT, (context) => { + this.#entriesContext = context; + }); + } + + override isActive(editor: Editor) { + return editor.isActive('umbRteBlock') || editor.isActive('umbRteBlockInline'); + } + + override async execute() { + return this.#createBlock(); + } + + #createBlock() { + if (!this.#entriesContext) { + console.error('[Block Picker] No entries context available.'); + return; + } + + let createPath: string | undefined = undefined; + + if (this.#blocks?.length === 1) { + const elementKey = this.#blocks[0].contentElementTypeKey; + createPath = this.#entriesContext.getPathForCreateBlock() + 'modal/umb-modal-workspace/create/' + elementKey; + } else { + createPath = this.#entriesContext.getPathForCreateBlock(); + } + + if (createPath) { + window.history.pushState({}, '', createPath); + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block.extension.ts new file mode 100644 index 0000000000..38e82123f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/block.extension.ts @@ -0,0 +1,130 @@ +import { UMB_BLOCK_RTE_DATA_CONTENT_KEY } from '../../types.js'; +import { UmbTiptapExtensionApiBase } from '@umbraco-cms/backoffice/tiptap'; +import { Node } from '@umbraco-cms/backoffice/external/tiptap'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { distinctUntilChanged } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbBlockDataModel } from '@umbraco-cms/backoffice/block'; +import { UMB_BLOCK_RTE_MANAGER_CONTEXT, type UmbBlockRteLayoutModel } from '@umbraco-cms/backoffice/block-rte'; + +declare module '@tiptap/core' { + interface Commands { + umbRteBlock: { + setBlock: (options: { contentKey: string }) => ReturnType; + }; + umbRteBlockInline: { + setBlockInline: (options: { contentKey: string }) => ReturnType; + }; + } +} + +const umbRteBlock = Node.create({ + name: 'umbRteBlock', + group: 'block', + content: undefined, // The block does not have any content, it is just a wrapper. + atom: true, // The block is an atom, meaning it is a single unit that cannot be split. + marks: '', // We do not allow marks on the block + draggable: true, + selectable: true, + + addAttributes() { + return { + [UMB_BLOCK_RTE_DATA_CONTENT_KEY]: { + isRequired: true, + }, + }; + }, + + parseHTML() { + return [{ tag: 'umb-rte-block' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['umb-rte-block', HTMLAttributes]; + }, + + addCommands() { + return { + setBlock: + (options) => + ({ commands }) => { + const attrs = { [UMB_BLOCK_RTE_DATA_CONTENT_KEY]: options.contentKey }; + return commands.insertContent({ + type: this.name, + attrs, + }); + }, + }; + }, +}); + +const umbRteBlockInline = umbRteBlock.extend({ + name: 'umbRteBlockInline', + group: 'inline', + inline: true, + + parseHTML() { + return [{ tag: 'umb-rte-block-inline' }]; + }, + + renderHTML({ HTMLAttributes }) { + return ['umb-rte-block-inline', HTMLAttributes]; + }, + + addCommands() { + return { + setBlockInline: + (options) => + ({ commands }) => { + const attrs = { [UMB_BLOCK_RTE_DATA_CONTENT_KEY]: options.contentKey }; + return commands.insertContent({ + type: this.name, + attrs, + }); + }, + }; + }, +}); + +export default class UmbTiptapBlockElementApi extends UmbTiptapExtensionApiBase { + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_BLOCK_RTE_MANAGER_CONTEXT, (context) => { + this.observe( + context.contents.pipe( + distinctUntilChanged((prev, curr) => prev.map((y) => y.key).join() === curr.map((y) => y.key).join()), + ), + (contents) => { + this.#updateBlocks(contents, context.getLayouts()); + }, + 'contents', + ); + }); + } + + getTiptapExtensions() { + return [umbRteBlock, umbRteBlockInline]; + } + + #updateBlocks(blocks: UmbBlockDataModel[], layouts: Array) { + const editor = this._editor; + if (!editor) return; + + const existingBlocks = Array.from(editor.view.dom.querySelectorAll('umb-rte-block, umb-rte-block-inline')).map( + (x) => x.getAttribute(UMB_BLOCK_RTE_DATA_CONTENT_KEY), + ); + const newBlocks = blocks.filter((x) => !existingBlocks.find((contentKey) => contentKey === x.key)); + + newBlocks.forEach((block) => { + // Find layout for block + const layout = layouts.find((x) => x.contentKey === block.key); + const inline = layout?.displayInline ?? false; + + if (inline) { + editor.commands.setBlockInline({ contentKey: block.key }); + } else { + editor.commands.setBlock({ contentKey: block.key }); + } + }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/manifests.ts new file mode 100644 index 0000000000..aafd570b89 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/plugins/manifests.ts @@ -0,0 +1,29 @@ +import type { ManifestTiptapExtension } from '../extensions/tiptap-extension.js'; +import type { ManifestTiptapToolbarExtensionButtonKind } from '../extensions/tiptap-toolbar-extension.js'; + +export const manifests: Array = [ + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.Block', + name: 'Block Tiptap Extension', + api: () => import('./block.extension.js'), + meta: { + icon: 'icon-plugin', + label: 'Block', + group: '#tiptap_extGroup_interactive', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'button', + alias: 'Umb.Tiptap.Toolbar.BlockPicker', + name: 'Block Picker Tiptap Extension Button', + api: () => import('./block-picker-toolbar.extension.js'), + weight: 90, + meta: { + alias: 'umbblockpicker', + icon: 'icon-plugin', + label: '#blockEditor_insertBlock', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/manifests.ts new file mode 100644 index 0000000000..734619f0cf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/manifests.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line local-rules/no-relative-import-to-import-map-module +import { manifests as tiptapManifests } from './tiptap/manifests.js'; +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor'; + +export const manifests: Array = [...tiptapManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts new file mode 100644 index 0000000000..d020ec400d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-extensions-configuration.element.ts @@ -0,0 +1,210 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, css, html, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { + UmbPropertyValueChangeEvent, + type UmbPropertyEditorConfigCollection, + type UmbPropertyEditorUiElement, +} from '@umbraco-cms/backoffice/property-editor'; + +type UmbTiptapExtensionConfig = { + alias: string; + label: string; + icon?: string; + group: string; +}; + +type UmbTiptapExtensionGroupItem = { + alias: string; + label: string; + icon?: string; + selected: boolean; +}; + +type UmbTiptapExtensionGroup = { + group: string; + extensions: UmbTiptapExtensionGroupItem[]; +}; + +const elementName = 'umb-property-editor-ui-tiptap-extensions-configuration'; + +@customElement(elementName) +export class UmbPropertyEditorUiTiptapExtensionsConfigurationElement + extends UmbLitElement + implements UmbPropertyEditorUiElement +{ + @property({ attribute: false }) + value?: Array = []; + + @property({ attribute: false }) + config?: UmbPropertyEditorConfigCollection; + + @state() + private _extensionCategories: UmbTiptapExtensionGroup[] = []; + + @state() + private _extensionConfigs: UmbTiptapExtensionConfig[] = []; + + protected override async firstUpdated(_changedProperties: PropertyValueMap) { + super.firstUpdated(_changedProperties); + + this.observe(umbExtensionsRegistry.byType('tiptapExtension'), (extensions) => { + this._extensionConfigs = extensions + .sort((a, b) => a.alias.localeCompare(b.alias)) + .map((ext) => { + return { + alias: ext.alias, + label: ext.meta.label, + icon: ext.meta.icon, + group: ext.meta.group, + }; + }); + + if (!this.value) { + // The default value is all extensions enabled + this.value = this._extensionConfigs.map((ext) => ext.alias); + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + this.#setupExtensionCategories(); + }); + } + + #setupExtensionCategories() { + const useDefault = !this.value; // The default value is all extensions enabled + const withSelectedProperty = this._extensionConfigs.map((extensionConfig) => { + return { + ...extensionConfig, + selected: useDefault ? true : this.value!.includes(extensionConfig.alias), + }; + }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const grouped = Object.groupBy( + withSelectedProperty, + (item: UmbTiptapExtensionConfig) => item.group || 'Uncategorized', + ); + + this._extensionCategories = Object.keys(grouped) + .sort((a, b) => a.localeCompare(b)) + .map((key) => ({ + group: key, + extensions: grouped[key], + })); + } + + #onExtensionClick(item: UmbTiptapExtensionGroupItem) { + item.selected = !item.selected; + + if (!this.value) { + this.value = []; + } + + if (item.selected) { + this.value = [...this.value, item.alias]; + } else { + this.value = this.value.filter((alias) => alias !== item.alias); + } + + this.requestUpdate('_extensionCategories'); + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + override render() { + return html` +
+ ${repeat( + this._extensionCategories, + (group) => html` +
+

${this.localize.string(group.group)}

+ ${repeat( + group.extensions, + (item) => html` +
+ this.#onExtensionClick(item)}> + + + this.#onExtensionClick(item)}> +
+ `, + )} +
+ `, + )} +
+ `; + } + + static override readonly styles = [ + UmbTextStyles, + css` + uui-icon { + width: unset; + height: unset; + display: flex; + vertical-align: unset; + } + + uui-button.selected { + --uui-button-border-color: var(--uui-color-selected); + --uui-button-border-width: 2px; + } + + .extensions { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 16px; + } + + .extension-item { + display: grid; + grid-template-columns: 36px 1fr; + grid-template-rows: 1fr; + align-items: center; + gap: 9px; + } + + .group { + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px; + background-color: var(--uui-color-surface-alt); + border: 1px solid var(--uui-color-border); + border-radius: 6px; + } + + .group-name { + grid-column: 1 / -1; + display: flex; + font-weight: bold; + margin: 0; + } + `, + ]; +} + +export { UmbPropertyEditorUiTiptapExtensionsConfigurationElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPropertyEditorUiTiptapExtensionsConfigurationElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts new file mode 100644 index 0000000000..74d387c06a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts @@ -0,0 +1,329 @@ +import type { UmbTiptapToolbarValue } from '../../../extensions/types.js'; +import { customElement, css, html, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbPropertyValueChangeEvent, type UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; + +type UmbTiptapToolbarExtension = { + alias: string; + label: string; + icon: string; +}; +const elementName = 'umb-property-editor-ui-tiptap-toolbar-configuration'; + +@customElement(elementName) +export class UmbPropertyEditorUiTiptapToolbarConfigurationElement + extends UmbLitElement + implements UmbPropertyEditorUiElement +{ + readonly #inUse: Set = new Set(); + + #currentDragItem?: { + alias: string; + fromPos?: [number, number, number]; + }; + + #lookup?: Map; + + @state() + private _extensions: Array = []; + + @property({ attribute: false }) + set value(value: UmbTiptapToolbarValue | undefined) { + if (!value) { + this.#value = [[[]]]; + } else { + // TODO: This can be optimized with cashing; + this.#value = value ? value.map((rows) => rows.map((groups) => [...groups])) : [[[]]]; + value.forEach((row) => row.forEach((group) => group.forEach((alias) => this.#inUse.add(alias)))); + } + } + get value(): UmbTiptapToolbarValue { + // TODO: This can be optimized with cashing; + return this.#value.map((rows) => rows.map((groups) => [...groups])); + } + #value: UmbTiptapToolbarValue = [[[]]]; + + protected override async firstUpdated(_changedProperties: PropertyValueMap) { + super.firstUpdated(_changedProperties); + + this.observe(umbExtensionsRegistry.byType('tiptapToolbarExtension'), (extensions) => { + this._extensions = extensions.map((ext) => ({ alias: ext.alias, label: ext.meta.label, icon: ext.meta.icon })); + this.#lookup = new Map(this._extensions.map((ext) => [ext.alias, ext])); + }); + } + + #onDragStart(event: DragEvent, alias: string, fromPos?: [number, number, number]) { + event.dataTransfer!.effectAllowed = 'move'; + this.#currentDragItem = { alias, fromPos }; + } + + #onDragOver(event: DragEvent) { + event.preventDefault(); + event.dataTransfer!.dropEffect = 'move'; + } + + #onDragEnd(event: DragEvent) { + event.preventDefault(); + if (event.dataTransfer?.dropEffect === 'none') { + const { fromPos } = this.#currentDragItem ?? {}; + if (!fromPos) return; + + this.#removeItem(fromPos); + } + } + + #onDrop(event: DragEvent, toPos?: [number, number, number]) { + event.preventDefault(); + const { alias, fromPos } = this.#currentDragItem ?? {}; + + // Remove item if no destination position is provided + if (fromPos && !toPos) { + this.#removeItem(fromPos); + return; + } + // Move item if both source and destination positions are available + if (fromPos && toPos) { + this.#moveItem(fromPos, toPos); + return; + } + // Insert item if an alias and a destination position are provided + if (alias && toPos) { + this.#insertItem(alias, toPos); + } + } + + #moveItem(from: [number, number, number], to: [number, number, number]) { + const [rowIndex, groupIndex, itemIndex] = from; + + // Get the item to move from the 'from' position + const itemToMove = this.#value[rowIndex][groupIndex][itemIndex]; + + // Remove the item from the original position + this.#value[rowIndex][groupIndex].splice(itemIndex, 1); + + this.#insertItem(itemToMove, to); + } + + #insertItem(alias: string, toPos: [number, number, number]) { + const [rowIndex, groupIndex, itemIndex] = toPos; + + // Insert the item into the new position + const inserted = this.#value[rowIndex][groupIndex].splice(itemIndex, 0, alias); + inserted.forEach((alias) => this.#inUse.add(alias)); + + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + #removeItem(from: [number, number, number]) { + const [rowIndex, groupIndex, itemIndex] = from; + + const removed = this.#value[rowIndex][groupIndex].splice(itemIndex, 1); + removed.forEach((alias) => this.#inUse.delete(alias)); + + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + #addGroup(rowIndex: number, groupIndex: number) { + this.#value[rowIndex].splice(groupIndex, 0, []); + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + #removeGroup(rowIndex: number, groupIndex: number) { + if (this.#value[rowIndex].length > groupIndex) { + const removed = this.#value[rowIndex].splice(groupIndex, 1); + removed.forEach((group) => group.forEach((alias) => this.#inUse.delete(alias))); + } + + // Prevent leaving an empty group + if (this.#value[rowIndex].length === 0) { + this.#value[rowIndex][groupIndex] = []; + } + + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + #addRow(rowIndex: number) { + this.#value.splice(rowIndex, 0, [[]]); + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + #removeRow(rowIndex: number) { + if (this.#value.length > rowIndex) { + const removed = this.#value.splice(rowIndex, 1); + removed.forEach((row) => row.forEach((group) => group.forEach((alias) => this.#inUse.delete(alias)))); + } + + // Prevent leaving an empty row + if (this.#value.length === 0) { + this.#value[rowIndex] = [[]]; + } + + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + override render() { + return html` + ${repeat(this.#value, (row, rowIndex) => this.#renderRow(row, rowIndex))} + this.#addRow(this.#value.length)}> + + Add row + + ${this.#renderExtensions()} + `; + } + + #renderRow(row: string[][], rowIndex: number) { + return html` +
+ ${repeat(row, (group, groupIndex) => this.#renderGroup(group, rowIndex, groupIndex))} + this.#addGroup(rowIndex, row.length)}> + + Add group + + this.#removeRow(rowIndex)}> + + +
+ `; + } + + #renderGroup(group: string[], rowIndex: number, groupIndex: number) { + return html` +
this.#onDrop(e, [rowIndex, groupIndex, group.length])}> + ${group.map((alias, itemIndex) => this.#renderItem(alias, rowIndex, groupIndex, itemIndex))} + this.#removeGroup(rowIndex, groupIndex)}> + + +
+ `; + } + + #renderItem(alias: string, rowIndex: number, groupIndex: number, itemIndex: number) { + const extension = this.#lookup?.get(alias); + if (!extension) return nothing; + return html` +
this.#onDragStart(e, alias, [rowIndex, groupIndex, itemIndex])}> + +
+ `; + } + + #renderExtensions() { + return html` +
+ ${repeat( + this._extensions.filter((ext) => !this.#inUse.has(ext.alias)), + (extension) => html` +
this.#onDragStart(e, extension.alias)} + @dragend=${this.#onDragEnd}> + +
+ `, + )} +
+ `; + } + + static override readonly styles = [ + UmbTextStyles, + css` + :host { + display: flex; + flex-direction: column; + gap: 6px; + } + .extensions { + display: flex; + flex-wrap: wrap; + gap: 3px; + border-radius: var(--uui-border-radius); + background-color: var(--uui-color-surface-alt); + padding: 6px; + min-height: 30px; + min-width: 30px; + } + .row { + position: relative; + display: flex; + gap: 12px; + } + .group { + position: relative; + display: flex; + gap: 3px; + border-radius: var(--uui-border-radius); + background-color: var(--uui-color-surface-alt); + padding: 6px; + min-height: 32px; + min-width: 32px; + } + .item { + padding: var(--uui-size-space-2); + border: 1px solid var(--uui-color-border); + border-radius: var(--uui-border-radius); + background-color: var(--uui-color-surface); + cursor: move; + display: flex; + box-sizing: border-box; + width: 32px; + height: 32px; + justify-content: center; + } + + .remove-row-button, + .remove-group-button { + display: none; + } + .remove-group-button { + position: absolute; + top: -26px; + left: 50%; + transform: translateX(-50%); + z-index: 1; + } + + .row:hover .remove-row-button:not(.hidden), + .group:hover .remove-group-button:not(.hidden) { + display: flex; + } + umb-icon { + /* Prevents titles from bugging out */ + pointer-events: none; + } + `, + ]; +} + +export { UmbPropertyEditorUiTiptapToolbarConfigurationElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPropertyEditorUiTiptapToolbarConfigurationElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/manifests.ts new file mode 100644 index 0000000000..a2791c4935 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/manifests.ts @@ -0,0 +1,99 @@ +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor'; + +export const manifests: Array = [ + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.Tiptap', + name: 'Rich Text Editor [Tiptap] Property Editor UI', + element: () => import('./property-editor-ui-tiptap.element.js'), + meta: { + label: 'Rich Text Editor [Tiptap]', + propertyEditorSchemaAlias: 'Umbraco.RichText', + icon: 'icon-browser-window', + group: 'richContent', + settings: { + properties: [ + { + alias: 'toolbar', + label: 'Toolbar', + description: 'Pick the toolbar items that should be available when editing', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ToolbarConfiguration', + weight: 5, + }, + { + alias: 'extensions', + label: 'Extensions', + description: 'Extensions to enable', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ExtensionsConfiguration', + weight: 10, + }, + { + alias: 'dimensions', + label: 'Dimensions', + description: 'Set the maximum width and height of the editor', + propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.DimensionsConfiguration', + weight: 20, + }, + { + alias: 'maxImageSize', + label: 'Maximum size for inserted images', + description: 'Maximum width or height - enter 0 to disable resizing', + propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.MaxImageSizeConfiguration', + weight: 40, + }, + { + alias: 'overlaySize', + label: 'Overlay Size', + description: 'Select the width of the overlay (link picker)', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.OverlaySize', + weight: 50, + }, + ], + defaultData: [ + { + alias: 'toolbar', + value: [ + [ + ['Umb.Tiptap.Toolbar.SourceEditor'], + ['Umb.Tiptap.Toolbar.Bold', 'Umb.Tiptap.Toolbar.Italic', 'Umb.Tiptap.Toolbar.Underline'], + [ + 'Umb.Tiptap.Toolbar.TextAlignLeft', + 'Umb.Tiptap.Toolbar.TextAlignCenter', + 'Umb.Tiptap.Toolbar.TextAlignRight', + ], + ['Umb.Tiptap.Toolbar.BulletList', 'Umb.Tiptap.Toolbar.OrderedList'], + ['Umb.Tiptap.Toolbar.Blockquote', 'Umb.Tiptap.Toolbar.HorizontalRule'], + ['Umb.Tiptap.Toolbar.Link', 'Umb.Tiptap.Toolbar.Unlink'], + ['Umb.Tiptap.Toolbar.MediaPicker', 'Umb.Tiptap.Toolbar.EmbeddedMedia'], + ], + ], + }, + { alias: 'maxImageSize', value: 500 }, + { alias: 'overlaySize', value: 'medium' }, + ], + }, + }, + }, + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.Tiptap.ToolbarConfiguration', + name: 'Tiptap Toolbar Property Editor UI', + js: () => import('./components/property-editor-ui-tiptap-toolbar-configuration.element.js'), + meta: { + label: 'Tiptap Toolbar Configuration', + icon: 'icon-autofill', + group: 'common', + }, + }, + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.Tiptap.ExtensionsConfiguration', + name: 'Tiptap Extensions Property Editor UI', + js: () => import('./components/property-editor-ui-tiptap-extensions-configuration.element.js'), + meta: { + label: 'Tiptap Extensions Configuration', + icon: 'icon-autofill', + group: 'common', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts new file mode 100644 index 0000000000..8d56fc1943 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts @@ -0,0 +1,60 @@ +import type { UmbInputTiptapElement } from '../../components/input-tiptap/input-tiptap.element.js'; +import { UmbRteBaseElement } from '../../../components/rte-base.element.js'; +import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; + +import '../../components/input-tiptap/input-tiptap.element.js'; + +const elementName = 'umb-property-editor-ui-tiptap'; + +/** + * @element umb-property-editor-ui-tiptap + */ +@customElement(elementName) +export class UmbPropertyEditorUiTiptapElement extends UmbRteBaseElement { + #onChange(event: CustomEvent & { target: UmbInputTiptapElement }) { + const value = event.target.value; + + // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup. + const usedContentKeys: string[] = []; + + // Regex matching all block elements in the markup, and extracting the content key. It's the same as the one used on the backend. + const regex = new RegExp( + /(?:)?<\/umb-rte-block(?:-inline)?>/gi, + ); + let blockElement: RegExpExecArray | null; + while ((blockElement = regex.exec(value)) !== null) { + if (blockElement.groups?.key) { + usedContentKeys.push(blockElement.groups.key); + } + } + + this._filterUnusedBlocks(usedContentKeys); + + this._latestMarkup = value; + + this._value = { + ...this._value, + markup: this._latestMarkup, + }; + + this._fireChangeEvent(); + } + + override render() { + return html` + + `; + } +} + +export { UmbPropertyEditorUiTiptapElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPropertyEditorUiTiptapElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.stories.ts new file mode 100644 index 0000000000..e104bbe2e9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.stories.ts @@ -0,0 +1,91 @@ +import type { UmbPropertyEditorUiTiptapElement } from './property-editor-ui-tiptap.element.js'; +import type { Meta, StoryObj } from '@storybook/web-components'; +import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; + +import './property-editor-ui-tiptap.element.js'; + +const config = new UmbPropertyEditorConfigCollection([ + { + alias: 'hideLabel', + value: true, + }, + { alias: 'dimensions', value: { height: 500 } }, + { alias: 'maxImageSize', value: 500 }, + { alias: 'ignoreUserStartNodes', value: false }, + { + alias: 'toolbar', + value: [ + [ + [ + ['Umb.Tiptap.Toolbar.Bold', 'Umb.Tiptap.Toolbar.Italic', 'Umb.Tiptap.Toolbar.Underline'], + [ + 'Umb.Tiptap.Toolbar.TextAlignLeft', + 'Umb.Tiptap.Toolbar.TextAlignCenter', + 'Umb.Tiptap.Toolbar.TextAlignRight', + ], + ['Umb.Tiptap.Toolbar.Heading1', 'Umb.Tiptap.Toolbar.Heading2', 'Umb.Tiptap.Toolbar.Heading3'], + ['Umb.Tiptap.Toolbar.Unlink', 'Umb.Tiptap.Toolbar.Link'], + ['Umb.Tiptap.Toolbar.Embed', 'Umb.Tiptap.Toolbar.MediaPicker', 'Umb.Tiptap.Toolbar.BlockPicker'], + ['Umb.Tiptap.Toolbar.Redo', 'Umb.Tiptap.Toolbar.Undo'], + ], + ], + ], + }, + { + alias: 'extensions', + value: [ + 'Umb.Tiptap.Bold', + 'Umb.Tiptap.Italic', + 'Umb.Tiptap.Underline', + 'Umb.Tiptap.Strike', + 'Umb.Tiptap.Blockquote', + 'Umb.Tiptap.CodeBlock', + 'Umb.Tiptap.HorizontalRule', + 'Umb.Tiptap.Figure', + 'Umb.Tiptap.Table', + 'Umb.Tiptap.Link', + 'Umb.Tiptap.Embed', + 'Umb.Tiptap.Image', + 'Umb.Tiptap.Heading', + 'Umb.Tiptap.List', + 'Umb.Tiptap.TextAlign', + 'Umb.Tiptap.MediaUpload', + 'Umb.Tiptap.Block', + ], + }, +]); + +const meta: Meta = { + title: 'Property Editor UIs/Tiptap', + component: 'umb-property-editor-ui-tiptap', + id: 'umb-property-editor-ui-tiptap', + args: { + config: undefined, + value: { + blocks: { + layout: {}, + contentData: [], + settingsData: [], + expose: [], + }, + markup: ` +

Tiptap

+

I am a default value for the Tiptap text editor story.

+

+ Umbraco documentation +

+ `, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const DefaultConfig: Story = { + args: { + config, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.test.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.test.ts new file mode 100644 index 0000000000..214fa7d487 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/tiptap/property-editors/tiptap/property-editor-ui-tiptap.test.ts @@ -0,0 +1,21 @@ +import { UmbPropertyEditorUiTiptapElement } from './property-editor-ui-tiptap.element.js'; +import { expect, fixture, html } from '@open-wc/testing'; +import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; + +describe('UmbPropertyEditorUITiptapElement', () => { + let element: UmbPropertyEditorUiTiptapElement; + + beforeEach(async () => { + element = await fixture(html` `); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbPropertyEditorUiTiptapElement); + }); + + if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) { + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/types.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/types.ts new file mode 100644 index 0000000000..5ce3b8d84d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/types.ts @@ -0,0 +1,14 @@ +import type { UmbBlockValueType } from '@umbraco-cms/backoffice/block'; +import type { UmbBlockRteLayoutModel } from '@umbraco-cms/backoffice/block-rte'; + +export interface UmbPropertyEditorUiValueType { + markup: string; + blocks: UmbBlockValueType; +} + +export const UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS = 'Umbraco.RichText'; + +/** + * The attribute where the block content key is stored. + */ +export const UMB_BLOCK_RTE_DATA_CONTENT_KEY = 'data-content-key'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/umbraco-package.ts new file mode 100644 index 0000000000..68eece9518 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/umbraco-package.ts @@ -0,0 +1,9 @@ +export const name = 'Umbraco.Core.Rte'; +export const extensions = [ + { + name: 'RTE Bundle', + alias: 'Umb.Bundle.Rte', + type: 'bundle', + js: () => import('./manifests.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/vite.config.ts new file mode 100644 index 0000000000..20ec618303 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/vite.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vite'; +import { rmSync } from 'fs'; +import { getDefaultConfig } from '../../vite-config-base'; + +const dist = '../../../dist-cms/packages/rte'; + +// delete the unbundled dist folder +rmSync(dist, { recursive: true, force: true }); + +export default defineConfig({ + ...getDefaultConfig({ + dist, + entry: { + 'tiny-mce/index': 'tiny-mce/index.ts', + 'tiptap/index': 'tiptap/index.ts', + manifests: 'manifests.ts', + 'umbraco-package': 'umbraco-package.ts', + }, + }), +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/index.ts deleted file mode 100644 index 011799ec00..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/modals/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './media-caption-alt-text/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/types.ts deleted file mode 100644 index 35c975e701..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { UmbBlockValueDataPropertiesBaseType } from '@umbraco-cms/backoffice/block'; - -export interface UmbPropertyEditorUiValueType { - markup: string; - blocks: UmbBlockValueDataPropertiesBaseType; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts deleted file mode 100644 index a5de5f04cd..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const name = 'Umbraco.Core.TinyMce'; -export const extensions = [ - { - name: 'TinyMce Bundle', - alias: 'Umb.Bundle.TinyMce', - type: 'bundle', - js: () => import('./manifests.js'), - }, -]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/vite.config.ts deleted file mode 100644 index d0c457d1e9..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/vite.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'vite'; -import { rmSync } from 'fs'; -import { getDefaultConfig } from '../../vite-config-base'; - -const dist = '../../../dist-cms/packages/tiny-mce'; - -// delete the unbundled dist folder -rmSync(dist, { recursive: true, force: true }); - -export default defineConfig({ - ...getDefaultConfig({ dist }), -}); diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 2f92b70db4..08f7cd7734 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -121,7 +121,8 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/template": ["./src/packages/templating/templates/index.ts"], "@umbraco-cms/backoffice/temporary-file": ["./src/packages/core/temporary-file/index.ts"], "@umbraco-cms/backoffice/themes": ["./src/packages/core/themes/index.ts"], - "@umbraco-cms/backoffice/tiny-mce": ["./src/packages/tiny-mce/index.ts"], + "@umbraco-cms/backoffice/tiny-mce": ["./src/packages/rte/tiny-mce/index.ts"], + "@umbraco-cms/backoffice/tiptap": ["./src/packages/rte/tiptap/index.ts"], "@umbraco-cms/backoffice/translation": ["./src/packages/translation/index.ts"], "@umbraco-cms/backoffice/tree": ["./src/packages/core/tree/index.ts"], "@umbraco-cms/backoffice/ufm": ["./src/packages/ufm/index.ts"], @@ -145,6 +146,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/external/router-slot": ["./src/external/router-slot/index.ts"], "@umbraco-cms/backoffice/external/rxjs": ["./src/external/rxjs/index.ts"], "@umbraco-cms/backoffice/external/tinymce": ["./src/external/tinymce/index.ts"], + "@umbraco-cms/backoffice/external/tiptap": ["./src/external/tiptap/index.ts"], "@umbraco-cms/backoffice/external/uui": ["./src/external/uui/index.ts"], "@umbraco-cms/backoffice/external/uuid": ["./src/external/uuid/index.ts"] }