diff --git a/package-lock.json b/package-lock.json index fd1ab7cc89b..32b62759f45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,13 +23,17 @@ "@igniteui/material-icons-extended": "^3.1.0", "@lit-labs/ssr-dom-shim": "^1.3.0", "@types/source-map": "0.5.2", + "dompurify": "^3.3.0", "express": "^5.1.0", "fflate": "^0.8.1", "igniteui-theming": "^24.0.0", "igniteui-trial-watermark": "^3.1.0", "jspdf": "^3.0.4", "lodash-es": "^4.17.21", + "marked": "^16.4.0", + "marked-shiki": "^1.2.1", "rxjs": "^7.8.2", + "shiki": "^3.13.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, @@ -72,7 +76,7 @@ "ig-typedoc-theme": "^7.0.0", "igniteui-dockmanager": "^1.17.0", "igniteui-sassdoc-theme": "^2.1.0", - "igniteui-webcomponents": "6.2.1", + "igniteui-webcomponents": "^6.3.1", "jasmine": "^5.6.0", "jasmine-core": "^5.6.0", "karma": "^6.4.4", @@ -6068,7 +6072,6 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.15.0.tgz", "integrity": "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==", - "dev": true, "license": "MIT", "dependencies": { "@shikijs/types": "3.15.0", @@ -6081,7 +6084,6 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.15.0.tgz", "integrity": "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==", - "dev": true, "license": "MIT", "dependencies": { "@shikijs/types": "3.15.0", @@ -6093,7 +6095,6 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.15.0.tgz", "integrity": "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==", - "dev": true, "license": "MIT", "dependencies": { "@shikijs/types": "3.15.0", @@ -6104,7 +6105,6 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.15.0.tgz", "integrity": "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==", - "dev": true, "license": "MIT", "dependencies": { "@shikijs/types": "3.15.0" @@ -6114,7 +6114,6 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.15.0.tgz", "integrity": "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==", - "dev": true, "license": "MIT", "dependencies": { "@shikijs/types": "3.15.0" @@ -6124,7 +6123,6 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.15.0.tgz", "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", - "dev": true, "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -6135,7 +6133,6 @@ "version": "10.0.2", "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "dev": true, "license": "MIT" }, "node_modules/@sigstore/bundle": { @@ -6436,7 +6433,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "*" @@ -6477,7 +6473,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "*" @@ -6604,7 +6599,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "dev": true, "license": "MIT" }, "node_modules/@types/webpack-env": { @@ -6863,7 +6857,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, "license": "ISC" }, "node_modules/@vitejs/plugin-basic-ssl": { @@ -8199,22 +8192,6 @@ "regenerator-runtime": "^0.11.0" } }, - "node_modules/babel-runtime/node_modules/core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "dev": true, - "hasInstallScript": true, - "license": "MIT" - }, - "node_modules/babel-runtime/node_modules/regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true, - "license": "MIT" - }, "node_modules/bach": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", @@ -9033,11 +9010,29 @@ "node": ">=10.0.0" } }, + "node_modules/canvg/node_modules/core-js": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/canvg/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -9114,7 +9109,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -9125,7 +9119,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -9551,7 +9544,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -9856,16 +9848,13 @@ } }, "node_modules/core-js": { - "version": "3.47.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", - "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, "hasInstallScript": true, - "license": "MIT", - "optional": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } + "license": "MIT" }, "node_modules/core-util-is": { "version": "1.0.3", @@ -10412,7 +10401,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10481,7 +10469,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dev": true, "license": "MIT", "dependencies": { "dequal": "^2.0.0" @@ -10637,7 +10624,6 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", "license": "(MPL-2.0 OR Apache-2.0)", - "optional": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -11883,6 +11869,12 @@ "pako": "^2.1.0" } }, + "node_modules/fast-png/node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -13871,7 +13863,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -13957,7 +13948,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -14138,7 +14128,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -14364,11 +14353,11 @@ } }, "node_modules/igniteui-webcomponents": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/igniteui-webcomponents/-/igniteui-webcomponents-6.2.1.tgz", - "integrity": "sha512-nsErVEF/2nuU76w8pkDzdu+0Xwv25OYWVDdXP5dFoQwvLMusNFju273e8c+DV9LoPtD0nWx6+RzyNaS+ylWXjw==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/igniteui-webcomponents/-/igniteui-webcomponents-6.3.6.tgz", + "integrity": "sha512-MRCCD204AE/0H2WRWiZHdRnpuldn/pjzk+3VGAtOvJ03+22HlmE3/7sMhnfNqsxqn/SHFTTFzBV56f3FEFu1+w==", "dev": true, - "license": "SEE LICENSE IN LICENSE", + "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.0", "@lit-labs/virtualizer": "^2.1.0", @@ -14377,6 +14366,26 @@ }, "engines": { "node": ">=20" + }, + "peerDependencies": { + "dompurify": "^3.2.0", + "marked": "^16.3.0", + "marked-shiki": "^1.2.0", + "shiki": "^3.12.0" + }, + "peerDependenciesMeta": { + "dompurify": { + "optional": true + }, + "marked": { + "optional": true + }, + "marked-shiki": { + "optional": true + }, + "shiki": { + "optional": true + } } }, "node_modules/ignore": { @@ -15549,6 +15558,18 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jspdf/node_modules/core-js": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.47.0.tgz", + "integrity": "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -17068,16 +17089,25 @@ } }, "node_modules/marked": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.3.tgz", - "integrity": "sha512-Fqa7eq+UaxfMriqzYLayfqAE40WN03jf+zHjT18/uXNuzjq3TY0XTbrAoPeqSJrAmPz11VuUA+kBPYOhHt9oOQ==", - "dev": true, + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", "license": "MIT", "bin": { - "marked": "bin/marked" + "marked": "bin/marked.js" }, "engines": { - "node": ">=0.10.0" + "node": ">= 20" + } + }, + "node_modules/marked-shiki": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/marked-shiki/-/marked-shiki-1.2.1.tgz", + "integrity": "sha512-yHxYQhPY5oYaIRnROn98foKhuClark7M373/VpLxiy5TrDu9Jd/LsMwo8w+U91Up4oDb9IXFrP0N1MFRz8W/DQ==", + "license": "MIT", + "peerDependencies": { + "marked": ">=7.0.0", + "shiki": ">=1.0.0" } }, "node_modules/math-intrinsics": { @@ -17297,7 +17327,6 @@ "version": "13.2.1", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "dev": true, "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -17731,7 +17760,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -17858,7 +17886,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -17932,7 +17959,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -17977,7 +18003,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -17994,7 +18019,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "dev": true, "funding": [ { "type": "GitHub Sponsors", @@ -19585,14 +19609,12 @@ "version": "0.12.1", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", - "dev": true, "license": "MIT" }, "node_modules/oniguruma-to-es": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz", "integrity": "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==", - "dev": true, "license": "MIT", "dependencies": { "oniguruma-parser": "^0.12.1", @@ -19972,10 +19994,11 @@ } }, "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "license": "(MIT AND Zlib)" + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true, + "license": "MIT" }, "node_modules/param-case": { "version": "2.1.1", @@ -20717,7 +20740,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -21141,17 +21163,16 @@ "license": "Apache-2.0" }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "license": "MIT", - "optional": true + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true, + "license": "MIT" }, "node_modules/regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", - "dev": true, "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" @@ -21161,7 +21182,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", - "dev": true, "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" @@ -21171,7 +21191,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", - "dev": true, "license": "MIT" }, "node_modules/registry-auth-token": { @@ -22592,6 +22611,19 @@ "marked": "^0.6.2" } }, + "node_modules/sassdoc-extras/node_modules/marked": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.3.tgz", + "integrity": "sha512-Fqa7eq+UaxfMriqzYLayfqAE40WN03jf+zHjT18/uXNuzjq3TY0XTbrAoPeqSJrAmPz11VuUA+kBPYOhHt9oOQ==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sassdoc-plugin-localization": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sassdoc-plugin-localization/-/sassdoc-plugin-localization-2.0.0.tgz", @@ -23589,7 +23621,6 @@ "version": "3.15.0", "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.15.0.tgz", "integrity": "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==", - "dev": true, "license": "MIT", "dependencies": { "@shikijs/core": "3.15.0", @@ -24033,7 +24064,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -24335,7 +24365,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "dev": true, "license": "MIT", "dependencies": { "character-entities-html4": "^2.0.0", @@ -25406,7 +25435,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "dev": true, "license": "MIT", "funding": { "type": "github", @@ -25798,13 +25826,6 @@ "tiny-inflate": "^1.0.0" } }, - "node_modules/unicode-trie/node_modules/pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", - "dev": true, - "license": "MIT" - }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -25906,7 +25927,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -25935,7 +25955,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -25964,7 +25983,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0" @@ -25978,7 +25996,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -26008,7 +26025,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -26430,7 +26446,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -26460,7 +26475,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "dev": true, "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -27937,7 +27951,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "dev": true, "license": "MIT", "funding": { "type": "github", diff --git a/package.json b/package.json index 83ebf9e7733..2b0be48844a 100644 --- a/package.json +++ b/package.json @@ -73,13 +73,17 @@ "@igniteui/material-icons-extended": "^3.1.0", "@lit-labs/ssr-dom-shim": "^1.3.0", "@types/source-map": "0.5.2", + "dompurify": "^3.3.0", "express": "^5.1.0", "fflate": "^0.8.1", "igniteui-theming": "^24.0.0", "igniteui-trial-watermark": "^3.1.0", "jspdf": "^3.0.4", "lodash-es": "^4.17.21", + "marked": "^16.4.0", + "marked-shiki": "^1.2.1", "rxjs": "^7.8.2", + "shiki": "^3.13.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, @@ -122,7 +126,7 @@ "ig-typedoc-theme": "^7.0.0", "igniteui-dockmanager": "^1.17.0", "igniteui-sassdoc-theme": "^2.1.0", - "igniteui-webcomponents": "6.2.1", + "igniteui-webcomponents": "^6.3.1", "jasmine": "^5.6.0", "jasmine-core": "^5.6.0", "karma": "^6.4.4", diff --git a/projects/igniteui-angular/chat-extras/index.ts b/projects/igniteui-angular/chat-extras/index.ts new file mode 100644 index 00000000000..decc72d85bc --- /dev/null +++ b/projects/igniteui-angular/chat-extras/index.ts @@ -0,0 +1 @@ +export * from './src/public_api'; diff --git a/projects/igniteui-angular/chat-extras/ng-package.json b/projects/igniteui-angular/chat-extras/ng-package.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/projects/igniteui-angular/chat-extras/ng-package.json @@ -0,0 +1 @@ +{} diff --git a/projects/igniteui-angular/chat-extras/src/markdown-pipe.spec.ts b/projects/igniteui-angular/chat-extras/src/markdown-pipe.spec.ts new file mode 100644 index 00000000000..cffbb6d5c2b --- /dev/null +++ b/projects/igniteui-angular/chat-extras/src/markdown-pipe.spec.ts @@ -0,0 +1,57 @@ +import { DomSanitizer } from '@angular/platform-browser'; +import { TestBed } from '@angular/core/testing'; +import { IgxChatMarkdownService } from './markdown-service'; +import { MarkdownPipe } from './markdown-pipe'; +import Spy = jasmine.Spy; + +// Mock the Service: We only care that the pipe calls the service and gets an HTML string. +// We provide a *known* unsafe HTML string to ensure sanitization is working. +const mockUnsafeHtml = ` +
unsafe
+ +`; + +class MockChatMarkdownService { + public async parse(_: string): Promise { + return mockUnsafeHtml; + } +} + +describe('MarkdownPipe', () => { + let pipe: MarkdownPipe; + let sanitizer: DomSanitizer; + let bypassSpy: Spy; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + MarkdownPipe, + { provide: IgxChatMarkdownService, useClass: MockChatMarkdownService }, + ], + }); + + pipe = TestBed.inject(MarkdownPipe); + sanitizer = TestBed.inject(DomSanitizer); + bypassSpy = spyOn(sanitizer, 'bypassSecurityTrustHtml').and.callThrough(); + }); + + it('should be created', () => { + expect(pipe).toBeTruthy(); + }); + + it('should call the service, sanitize content, and return SafeHtml', async () => { + await pipe.transform('some markdown'); + + expect(bypassSpy).toHaveBeenCalledTimes(1); + + const sanitizedString = bypassSpy.calls.mostRecent().args[0]; + + expect(sanitizedString).not.toContain('onerror'); + expect(sanitizedString).toContain('style="color: var(--shiki-fg);"'); + }); + + it('should handle undefined input text', async () => { + await pipe.transform(undefined); + expect(sanitizer.bypassSecurityTrustHtml).toHaveBeenCalled(); + }); +}); diff --git a/projects/igniteui-angular/chat-extras/src/markdown-pipe.ts b/projects/igniteui-angular/chat-extras/src/markdown-pipe.ts new file mode 100644 index 00000000000..9b7bb9bcb96 --- /dev/null +++ b/projects/igniteui-angular/chat-extras/src/markdown-pipe.ts @@ -0,0 +1,18 @@ +import DOMPurify from 'dompurify'; +import { inject, Pipe, type PipeTransform } from '@angular/core'; +import { IgxChatMarkdownService } from './markdown-service'; +import { DomSanitizer, type SafeHtml } from '@angular/platform-browser'; + + +@Pipe({ name: 'fromMarkdown' }) +export class MarkdownPipe implements PipeTransform { + private _service = inject(IgxChatMarkdownService); + private _sanitizer = inject(DomSanitizer); + + + public async transform(text?: string): Promise { + return this._sanitizer.bypassSecurityTrustHtml(DOMPurify.sanitize( + await this._service.parse(text ?? '') + )); + } +} diff --git a/projects/igniteui-angular/chat-extras/src/markdown-service.spec.ts b/projects/igniteui-angular/chat-extras/src/markdown-service.spec.ts new file mode 100644 index 00000000000..4d25633f394 --- /dev/null +++ b/projects/igniteui-angular/chat-extras/src/markdown-service.spec.ts @@ -0,0 +1,41 @@ +import { TestBed } from '@angular/core/testing'; +import { IgxChatMarkdownService } from './markdown-service'; + +describe('IgxChatMarkdownService', () => { + let service: IgxChatMarkdownService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(IgxChatMarkdownService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should parse basic markdown to HTML', async () => { + const markdown = '**Hello** *World*'; + const expectedHtml = '

Hello World

\n'; + + const result = await service.parse(markdown); + expect(result).toBe(expectedHtml); + }); + + it('should parse a code block with shiki highlighting', async () => { + const markdown = '```typescript\nconst x = 5;\n```'; + const result = await service.parse(markdown); + + expect(result).toContain('
 {
+        const markdown = '[Infragistics](https://www.infragistics.com)';
+        const expectedLink = '

Infragistics

'; + + const result = await service.parse(markdown); + expect(result).toContain(expectedLink); + }); +}); diff --git a/projects/igniteui-angular/chat-extras/src/markdown-service.ts b/projects/igniteui-angular/chat-extras/src/markdown-service.ts new file mode 100644 index 00000000000..4f2edf2a508 --- /dev/null +++ b/projects/igniteui-angular/chat-extras/src/markdown-service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@angular/core'; +import { Marked } from 'marked'; +import markedShiki from 'marked-shiki'; +import { bundledThemes, createHighlighter } from 'shiki/bundle/web'; + + +const DEFAULT_LANGUAGES = ['javascript', 'typescript', 'html', 'css']; +const DEFAULT_THEMES = { + light: 'github-light', + dark: 'github-dark' +}; + +@Injectable({ providedIn: 'root' }) +export class IgxChatMarkdownService { + + private _instance: Marked; + private _isInitialized: Promise; + + private _initializeMarked(): void { + this._instance = new Marked({ + breaks: true, + gfm: true, + extensions: [ + { + name: 'link', + renderer({ href, title, text }) { + return `${text}`; + } + } + ] + }); + } + + private async _initializeShiki(): Promise { + const highlighter = await createHighlighter({ + langs: DEFAULT_LANGUAGES, + themes: Object.keys(bundledThemes) + }); + + this._instance.use( + markedShiki({ + highlight(code, lang, _) { + try { + return highlighter.codeToHtml(code, { + lang, + themes: DEFAULT_THEMES, + }); + + } catch { + return `
${code}
`; + } + } + }) + ); + } + + + constructor() { + this._initializeMarked(); + this._isInitialized = this._initializeShiki(); + } + + public async parse(text: string): Promise { + await this._isInitialized; + return await this._instance.parse(text); + } +} diff --git a/projects/igniteui-angular/chat-extras/src/public_api.ts b/projects/igniteui-angular/chat-extras/src/public_api.ts new file mode 100644 index 00000000000..de599f08302 --- /dev/null +++ b/projects/igniteui-angular/chat-extras/src/public_api.ts @@ -0,0 +1 @@ +export { MarkdownPipe } from './markdown-pipe'; diff --git a/projects/igniteui-angular/chat/index.ts b/projects/igniteui-angular/chat/index.ts new file mode 100644 index 00000000000..decc72d85bc --- /dev/null +++ b/projects/igniteui-angular/chat/index.ts @@ -0,0 +1 @@ +export * from './src/public_api'; diff --git a/projects/igniteui-angular/chat/ng-package.json b/projects/igniteui-angular/chat/ng-package.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/projects/igniteui-angular/chat/ng-package.json @@ -0,0 +1 @@ +{} diff --git a/projects/igniteui-angular/chat/src/chat.component.html b/projects/igniteui-angular/chat/src/chat.component.html new file mode 100644 index 00000000000..896e36340b4 --- /dev/null +++ b/projects/igniteui-angular/chat/src/chat.component.html @@ -0,0 +1,16 @@ + + + diff --git a/projects/igniteui-angular/chat/src/chat.component.ts b/projects/igniteui-angular/chat/src/chat.component.ts new file mode 100644 index 00000000000..97aa851aeff --- /dev/null +++ b/projects/igniteui-angular/chat/src/chat.component.ts @@ -0,0 +1,329 @@ +import { + ChangeDetectionStrategy, + Component, + CUSTOM_ELEMENTS_SCHEMA, + Directive, + effect, + inject, + input, + OnInit, + output, + signal, + TemplateRef, + ViewContainerRef, + OnDestroy, + ViewRef, + computed, +} from '@angular/core'; +import { + IgcChatComponent, + type IgcChatMessageAttachment, + type IgcChatMessage, + type IgcChatOptions, + type ChatRenderContext, + type ChatRenderers, + type ChatAttachmentRenderContext, + type ChatInputRenderContext, + type ChatMessageRenderContext, + type IgcChatMessageReaction, +} from 'igniteui-webcomponents'; + +type ChatContextUnion = + | ChatAttachmentRenderContext + | ChatMessageRenderContext + | ChatInputRenderContext + | ChatRenderContext; + +type ChatContextType = + T extends ChatAttachmentRenderContext + ? IgcChatMessageAttachment + : T extends ChatMessageRenderContext + ? IgcChatMessage + : T extends ChatInputRenderContext + ? string + : T extends ChatRenderContext + ? { instance: IgcChatComponent } + : never; + +type ExtractChatContext = T extends (ctx: infer R) => any ? R : never; + +type ChatTemplatesContextMap = { + [K in keyof ChatRenderers]: { + $implicit: ChatContextType< + ExtractChatContext> & ChatContextUnion + >; + }; +}; + +/** + * Template references for customizing chat component rendering. + * Each property corresponds to a specific part of the chat UI that can be customized. + * + * @example + * ```typescript + * templates = { + * messageContent: this.customMessageTemplate, + * attachment: this.customAttachmentTemplate + * } + * ``` + */ +export type IgxChatTemplates = { + [K in keyof Omit]?: TemplateRef; +}; + +/** + * Configuration options for the chat component. + */ +export type IgxChatOptions = Omit; + + +/** + * Angular wrapper component for the Ignite UI Web Components Chat component. + * + * This component provides an Angular-friendly interface to the igc-chat web component, + * including support for Angular templates, signals, and change detection. + * + * Uses OnPush change detection strategy for optimal performance. All inputs are signals, + * so changes are automatically tracked and propagated to the underlying web component. + * + * @example + * ```typescript + * + * ``` + */ +@Component({ + selector: 'igx-chat', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + templateUrl: './chat.component.html' +}) +export class IgxChatComponent implements OnInit, OnDestroy { + //#region Internal state + + private readonly _view = inject(ViewContainerRef); + private readonly _templateViewRefs = new Map, Set>(); + private _oldTemplates: IgxChatTemplates = {}; + + protected readonly _transformedTemplates = signal({}); + + protected readonly _mergedOptions = computed(() => { + const options = this.options(); + const transformedTemplates = this._transformedTemplates(); + return { + ...options, + renderers: transformedTemplates + }; + }); + + //#endregion + + //#region Inputs + + /** Array of chat messages to display */ + public readonly messages = input([]); + + /** Draft message with text and optional attachments */ + public readonly draftMessage = input< + { text: string; attachments?: IgcChatMessageAttachment[] } | undefined + >({ text: '' }); + + /** Configuration options for the chat component */ + public readonly options = input({}); + + /** Custom templates for rendering chat elements */ + public readonly templates = input({}); + + //#endregion + + //#region Outputs + + /** Emitted when a new message is created */ + public readonly messageCreated = output(); + + /** Emitted when a user reacts to a message */ + public readonly messageReact = output(); + + /** Emitted when an attachment is clicked */ + public readonly attachmentClick = output(); + + /** Emitted when attachment drag starts */ + public readonly attachmentDrag = output(); + + /** Emitted when attachment is dropped */ + public readonly attachmentDrop = output(); + + /** Emitted when typing indicator state changes */ + public readonly typingChange = output(); + + /** Emitted when the input receives focus */ + public readonly inputFocus = output(); + + /** Emitted when the input loses focus */ + public readonly inputBlur = output(); + + /** Emitted when the input value changes */ + public readonly inputChange = output(); + + //#endregion + + /** @internal */ + public ngOnInit(): void { + IgcChatComponent.register(); + } + + /** @internal */ + public ngOnDestroy(): void { + for (const viewSet of this._templateViewRefs.values()) { + viewSet.forEach(viewRef => viewRef.destroy()); + } + this._templateViewRefs.clear(); + } + + constructor() { + // Templates changed - update transformed templates and viewRefs + effect(() => { + const templates = this.templates(); + this._setTemplates(templates ?? {}); + }); + } + + private _setTemplates(newTemplates: IgxChatTemplates): void { + const templateCopies: ChatRenderers = {}; + const newTemplateKeys = Object.keys(newTemplates) as Array; + + const oldTemplates = this._oldTemplates; + const oldTemplateKeys = Object.keys(oldTemplates) as Array; + + for (const key of oldTemplateKeys) { + const oldRef = oldTemplates[key]; + const newRef = newTemplates[key]; + + if (oldRef && oldRef !== newRef) { + const obsolete = this._templateViewRefs.get(oldRef); + if (obsolete) { + obsolete.forEach(viewRef => viewRef.destroy()); + this._templateViewRefs.delete(oldRef); + } + } + } + + this._oldTemplates = {}; + + for (const key of newTemplateKeys) { + const ref = newTemplates[key]; + if (ref) { + (this._oldTemplates as Record>)[key] = ref; + templateCopies[key] = this._createTemplateRenderer(ref); + } + } + + this._transformedTemplates.set(templateCopies); + } + + private _createTemplateRenderer(ref: NonNullable) { + type ChatContext = ExtractChatContext>; + + if (!this._templateViewRefs.has(ref)) { + this._templateViewRefs.set(ref, new Set()); + } + + const viewSet = this._templateViewRefs.get(ref)!; + + return (ctx: ChatContext) => { + const context = ctx as ChatContextUnion; + let angularContext: any; + + if ('message' in context && 'attachment' in context) { + angularContext = { $implicit: context.attachment }; + } else if ('message' in context) { + angularContext = { $implicit: context.message }; + } else if ('value' in context) { + angularContext = { + $implicit: context.value, + attachments: context.attachments + }; + } else { + angularContext = { $implicit: { instance: context.instance } }; + } + + const viewRef = this._view.createEmbeddedView(ref, angularContext); + viewSet.add(viewRef); + + return viewRef.rootNodes; + } + } +} + +/** + * Context provided to the chat input template. + */ +export interface ChatInputContext { + /** The current input value */ + $implicit: string; + /** Array of attachments associated with the input */ + attachments: IgcChatMessageAttachment[]; +} + +/** + * Directive providing type information for chat message template contexts. + * Use this directive on ng-template elements that render chat messages. + * + * @example + * ```html + * + *
{{ message.text }}
+ *
+ * ``` + */ +@Directive({ selector: '[igxChatMessageContext]', standalone: true }) +export class IgxChatMessageContextDirective { + + public static ngTemplateContextGuard(_: IgxChatMessageContextDirective, ctx: unknown): ctx is { $implicit: IgcChatMessage } { + return true; + } +} + +/** + * Directive providing type information for chat attachment template contexts. + * Use this directive on ng-template elements that render message attachments. + * + * @example + * ```html + * + * + * + * ``` + */ +@Directive({ selector: '[igxChatAttachmentContext]', standalone: true }) +export class IgxChatAttachmentContextDirective { + + public static ngTemplateContextGuard(_: IgxChatAttachmentContextDirective, ctx: unknown): ctx is { $implicit: IgcChatMessageAttachment } { + return true; + } +} + +/** + * Directive providing type information for chat input template contexts. + * Use this directive on ng-template elements that render the chat input. + * + * @example + * ```html + * + * + * + * ``` + */ +@Directive({ selector: '[igxChatInputContext]', standalone: true }) +export class IgxChatInputContextDirective { + + public static ngTemplateContextGuard(_: IgxChatInputContextDirective, ctx: unknown): ctx is ChatInputContext { + return true; + } +} diff --git a/projects/igniteui-angular/chat/src/chat.spec.ts b/projects/igniteui-angular/chat/src/chat.spec.ts new file mode 100644 index 00000000000..2977d06f194 --- /dev/null +++ b/projects/igniteui-angular/chat/src/chat.spec.ts @@ -0,0 +1,177 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing' +import { IgxChatComponent, IgxChatMessageContextDirective, type IgxChatTemplates } from './chat.component' +import { Component, signal, TemplateRef, viewChild } from '@angular/core'; +import type { IgcChatComponent, IgcChatMessage, IgcTextareaComponent } from 'igniteui-webcomponents'; + +describe('Chat wrapper', () => { + + let chatComponent: IgxChatComponent; + let chatElement: IgcChatComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [IgxChatComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(IgxChatComponent); + chatComponent = fixture.componentInstance; + chatElement = getChatElement(fixture); + fixture.detectChanges(); + }) + + it('is created', () => { + expect(chatComponent).toBeDefined(); + }); + + it('has correct initial empty state', () => { + const draft = chatComponent.draftMessage(); + + expect(chatComponent.messages().length).toEqual(0); + expect(draft.text).toEqual(''); + expect(draft.attachments).toBeUndefined(); + }); + + it('correct bindings for messages', async () => { + fixture.componentRef.setInput('messages', [{ id: '1', sender: 'user', text: 'Hello' }]); + + fixture.detectChanges(); + await fixture.whenStable(); + + + const messageElement = getChatMessages(chatElement)[0]; + expect(messageElement).toBeDefined(); + expect(getChatMessageDOM(messageElement).textContent.trim()).toEqual(chatComponent.messages()[0].text); + }); + + it('correct bindings for draft message', async () => { + fixture.componentRef.setInput('draftMessage', { text: 'Hello world' }); + + fixture.detectChanges(); + await fixture.whenStable(); + + const textarea = getChatInput(chatElement); + expect(textarea.value).toEqual(chatComponent.draftMessage().text); + }); +}); + +describe('Chat templates', () => { + let fixture: ComponentFixture; + let chatElement: IgcChatComponent; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [IgxChatComponent, IgxChatMessageContextDirective, ChatTemplatesBed] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChatTemplatesBed); + fixture.detectChanges(); + chatElement = getChatElement(fixture); + }); + + it('has correct initially bound template', async () => { + await fixture.whenStable(); + + // NOTE: This is invoked since in the test bed there is no app ref so fresh embedded view + // has no change detection ran on it. In an application scenario this is not the case. + // This is so we don't explicitly invoke `viewRef.detectChanges()` inside the returned closure + // from the wrapper's `_createTemplateRenderer` call. + fixture.detectChanges(); + expect(getChatMessageDOM(getChatMessages(chatElement)[0]).textContent.trim()) + .toEqual(`Your message: ${fixture.componentInstance.messages()[0].text}`); + }); +}); + +describe('Chat dynamic templates binding', () => { + let fixture: ComponentFixture; + let chatElement: IgcChatComponent; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [IgxChatComponent, IgxChatMessageContextDirective, ChatDynamicTemplatesBed] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ChatDynamicTemplatesBed); + fixture.detectChanges(); + chatElement = getChatElement(fixture); + }); + + it('supports late binding', async () => { + fixture.componentInstance.bindTemplates(); + fixture.detectChanges(); + + await fixture.whenStable(); + fixture.detectChanges(); + + expect(getChatMessageDOM(getChatMessages(chatElement)[0]).textContent.trim()) + .toEqual(`Your message: ${fixture.componentInstance.messages()[0].text}`); + }); + +}); + + +@Component({ + template: ` + + +

Your message: {{ message.text }}

+
+ `, + imports: [IgxChatComponent, IgxChatMessageContextDirective] +}) +class ChatTemplatesBed { + public messages = signal([{ + id: '1', + sender: 'user', + text: 'Hello world' + }]); + public messageTemplate = viewChild.required>('message'); +} + +@Component({ + template: ` + + +

Your message: {{ message.text }}

+
+ `, + imports: [IgxChatComponent, IgxChatMessageContextDirective] +}) +class ChatDynamicTemplatesBed { + public templates = signal(null); + public messages = signal([{ + id: '1', + sender: 'user', + text: 'Hello world' + }]); + public messageTemplate = viewChild.required>('message'); + + public bindTemplates(): void { + this.templates.set({ + messageContent: this.messageTemplate() + }); + } +} + +function getChatElement(fixture: ComponentFixture): IgcChatComponent { + const nativeElement = fixture.nativeElement as HTMLElement; + return nativeElement.querySelector('igc-chat'); +} + +function getChatInput(chat: IgcChatComponent): IgcTextareaComponent { + return chat.renderRoot.querySelector('igc-chat-input').shadowRoot.querySelector('igc-textarea'); +} + +function getChatMessages(chat: IgcChatComponent): HTMLElement[] { + return Array.from(chat.renderRoot.querySelectorAll('igc-chat-message')); +} + +function getChatMessageDOM(message: HTMLElement) { + return message.shadowRoot; +} diff --git a/projects/igniteui-angular/chat/src/public_api.ts b/projects/igniteui-angular/chat/src/public_api.ts new file mode 100644 index 00000000000..eca793fd7b9 --- /dev/null +++ b/projects/igniteui-angular/chat/src/public_api.ts @@ -0,0 +1 @@ +export * from './chat.component'; diff --git a/projects/igniteui-angular/ng-package.json b/projects/igniteui-angular/ng-package.json index 183215b2851..503e963ec27 100644 --- a/projects/igniteui-angular/ng-package.json +++ b/projects/igniteui-angular/ng-package.json @@ -13,6 +13,11 @@ "igniteui-trial-watermark", "lodash-es", "@igniteui/material-icons-extended", - "igniteui-theming" + "igniteui-theming", + "igniteui-webcomponents", + "dompurify", + "marked", + "marked-shiki", + "shiki" ] } diff --git a/projects/igniteui-angular/ng-package.prod.json b/projects/igniteui-angular/ng-package.prod.json index 7af1254752a..10a2234414b 100644 --- a/projects/igniteui-angular/ng-package.prod.json +++ b/projects/igniteui-angular/ng-package.prod.json @@ -12,6 +12,11 @@ "igniteui-trial-watermark", "lodash-es", "@igniteui/material-icons-extended", - "igniteui-theming" + "igniteui-theming", + "igniteui-webcomponents", + "dompurify", + "marked", + "marked-shiki", + "shiki" ] } diff --git a/projects/igniteui-angular/package.json b/projects/igniteui-angular/package.json index 264200c1cb9..03225f78e67 100644 --- a/projects/igniteui-angular/package.json +++ b/projects/igniteui-angular/package.json @@ -84,7 +84,12 @@ "@angular/animations": "21", "@angular/forms": "21", "hammerjs": "^2.0.8", - "@types/hammerjs": "^2.0.46" + "@types/hammerjs": "^2.0.46", + "igniteui-webcomponents": "^6.3.0", + "dompurify": "^3.2.0", + "marked": "^16.3.0", + "marked-shiki": "^1.2.0", + "shiki": "^3.12.0" }, "peerDependenciesMeta": { "hammerjs": { @@ -92,6 +97,21 @@ }, "@types/hammerjs": { "optional": true + }, + "igniteui-webcomponents": { + "optional": true + }, + "dompurify": { + "optional": true + }, + "marked": { + "optional": true + }, + "marked-shiki": { + "optional": true + }, + "shiki": { + "optional": true } }, "igxDevDependencies": { diff --git a/projects/igniteui-angular/schematics/utils/dependency-handler.ts b/projects/igniteui-angular/schematics/utils/dependency-handler.ts index a7fdb148e62..71e3e0f46df 100644 --- a/projects/igniteui-angular/schematics/utils/dependency-handler.ts +++ b/projects/igniteui-angular/schematics/utils/dependency-handler.ts @@ -27,6 +27,11 @@ export const DEPENDENCIES_MAP: PackageEntry[] = [ { name: 'lodash-es', target: PackageTarget.NONE }, { name: '@igniteui/material-icons-extended', target: PackageTarget.REGULAR }, { name: 'igniteui-theming', target: PackageTarget.NONE }, + { name: 'igniteui-webcomponents', target: PackageTarget.NONE }, + { name: 'dompurify', target: PackageTarget.NONE }, + { name: 'marked', target: PackageTarget.NONE }, + { name: 'marked-shiki', target: PackageTarget.NONE }, + { name: 'shiki', target: PackageTarget.NONE }, // peerDependencies { name: '@angular/forms', target: PackageTarget.NONE }, { name: '@angular/common', target: PackageTarget.NONE }, diff --git a/projects/igniteui-angular/src/public_api.ts b/projects/igniteui-angular/src/public_api.ts index 1360707be6f..5352b064fef 100644 --- a/projects/igniteui-angular/src/public_api.ts +++ b/projects/igniteui-angular/src/public_api.ts @@ -34,6 +34,7 @@ export * from 'igniteui-angular/button-group'; export * from 'igniteui-angular/calendar'; export * from 'igniteui-angular/card'; export * from 'igniteui-angular/carousel'; +export * from 'igniteui-angular/chat'; export * from 'igniteui-angular/checkbox'; export * from 'igniteui-angular/chips'; export * from 'igniteui-angular/combo'; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c6cc68f4294..5785f5a989c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -128,6 +128,11 @@ export class AppComponent implements OnInit { icon: 'view_carousel', name: 'Carousel' }, + { + link: '/chat', + icon: 'chat', + name: 'Chat' + }, { link: '/chip', icon: 'android', diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 1b4ee86a042..62c98189e5c 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -6,6 +6,7 @@ import { ButtonSampleComponent } from './button/button.sample'; import { CalendarSampleComponent } from './calendar/calendar.sample'; import { CardSampleComponent } from './card/card.sample'; import { CarouselSampleComponent } from './carousel/carousel.sample'; +import { ChatSampleComponent } from './chat/chat.sample'; import { InputControlsSampleComponent } from './input-controls/input-controls.sample'; import { ChipsSampleComponent } from './chips/chips.sample'; import { CircularProgressSampleComponent } from './circular-progress-showcase/circular-progress-showcase.sample' @@ -200,6 +201,10 @@ export const appRoutes: Routes = [ path: 'carousel', component: CarouselSampleComponent }, + { + path: 'chat', + component: ChatSampleComponent + }, { path: 'input-controls', component: InputControlsSampleComponent diff --git a/src/app/chat/chat.sample.html b/src/app/chat/chat.sample.html new file mode 100644 index 00000000000..2e278ad1b91 --- /dev/null +++ b/src/app/chat/chat.sample.html @@ -0,0 +1,8 @@ + + Prefix + Actions + + + +
+
diff --git a/src/app/chat/chat.sample.scss b/src/app/chat/chat.sample.scss new file mode 100644 index 00000000000..87a84a0fd2f --- /dev/null +++ b/src/app/chat/chat.sample.scss @@ -0,0 +1,4 @@ +#igniteui-demo-app .sample-wrapper { + padding: 0; +} + diff --git a/src/app/chat/chat.sample.ts b/src/app/chat/chat.sample.ts new file mode 100644 index 00000000000..d4d9058fa34 --- /dev/null +++ b/src/app/chat/chat.sample.ts @@ -0,0 +1,115 @@ + +import { AsyncPipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + CUSTOM_ELEMENTS_SCHEMA, + effect, + signal, + viewChild, +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { + IgxChatComponent, + IgxChatMessageContextDirective, + type IgxChatOptions, +} from 'igniteui-angular/chat'; +import { MarkdownPipe } from 'igniteui-angular/chat-extras'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'app-chat-sample', + styleUrls: ['chat.sample.scss'], + templateUrl: 'chat.sample.html', + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [ + FormsModule, + AsyncPipe, + IgxChatComponent, + MarkdownPipe, + IgxChatMessageContextDirective, + ] +}) +export class ChatSampleComponent { + protected _template = viewChild.required('renderer'); + + public messages = signal([ + { + id: '1', + text: `Hello. How can we assist you today?`, + sender: 'support', + }, + { + id: '2', + text: `Hello. I have problem with styling IgcAvatarComponent. Can you take a look at the attached file and help me?`, + sender: 'user', + attachments: [ + { + id: 'AvatarStyles.css', + name: 'AvatarStyles.css', + url: './styles/AvatarStyles.css', + type: 'text/css' + }, + ], + }, + { + id: '3', + text: `Sure, give me a moment to check the file.`, + sender: 'support', + }, + { + id: '4', + text: ` +Thank you for your patience. It seems that the issue is the name of the **CSS part**. Here is the fixed code: + + +\`\`\`css +igc-avatar::part(base) { + --size: 60px; + color: var(--ig-success-500-contrast); + background: var(--ig-success-500); + border-radius: 20px; +} +\`\`\``, + sender: 'support', + }, + { + id: '123213123', + sender: 'support', + text: ` +Here is some typescript: + + +\`\`\`ts + +class User { + constructor(public name: string, public age: number) {} +} +\`\`\`` + } + ]); + + public options = signal({ + disableAutoScroll: false, + disableInputAttachments: false, + suggestions: [`It works. Thanks.`, `It doesn't work.`], + inputPlaceholder: 'Type your message here...', + headerText: 'Customer Support', + }); + + + public templates = signal({}); + + constructor() { + effect(() => { + const template = this._template(); + if (template) { + this.templates.set({ messageContent: template }); + } + }); + } + + public onMessageReact(event: any) { + console.log(event); + } +}