diff --git a/client/kb-frontend/package-lock.json b/client/kb-frontend/package-lock.json index 52eb584..651f507 100644 --- a/client/kb-frontend/package-lock.json +++ b/client/kb-frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@azure/msal-browser": "^4.26.2", + "quill-blot-formatter": "^1.0.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-quill": "^2.0.0" @@ -26,21 +27,21 @@ } }, "node_modules/@azure/msal-browser": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.26.2.tgz", - "integrity": "sha512-F2U1mEAFsYGC5xzo1KuWc/Sy3CRglU9Ql46cDUx8x/Y3KnAIr1QAq96cIKCk/ZfnVxlvprXWRjNKoEpgLJXLhg==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.27.0.tgz", + "integrity": "sha512-bZ8Pta6YAbdd0o0PEaL1/geBsPrLEnyY/RDWqvF1PP9RUH8EMLvUMGoZFYS6jSlUan6KZ9IMTLCnwpWWpQRK/w==", "license": "MIT", "dependencies": { - "@azure/msal-common": "15.13.2" + "@azure/msal-common": "15.13.3" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "15.13.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.2.tgz", - "integrity": "sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==", + "version": "15.13.3", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.3.tgz", + "integrity": "sha512-shSDU7Ioecya+Aob5xliW9IGq1Ui8y4EVSdWGyI1Gbm4Vg61WpP95LuzcY214/wEjSn6w4PZYD4/iVldErHayQ==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -854,9 +855,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -866,7 +867,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -1524,9 +1525,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", - "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.3.tgz", + "integrity": "sha512-8QdH6czo+G7uBsNo0GiUfouPN1lRzKdJTGnKXwe12gkFbnnOUaUKGN55dMkfy+mnxmvjwl9zcI4VncczcVXDhA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1545,9 +1546,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -1565,11 +1566,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -1636,9 +1637,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001757", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", - "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "dev": true, "funding": [ { @@ -1783,6 +1784,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1832,9 +1842,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.260", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz", - "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==", + "version": "1.5.266", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", + "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", "dev": true, "license": "ISC" }, @@ -2938,6 +2948,18 @@ "quill-delta": "^3.6.2" } }, + "node_modules/quill-blot-formatter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/quill-blot-formatter/-/quill-blot-formatter-1.0.5.tgz", + "integrity": "sha512-iVmuEdmMIpvERBnnDfosWul6VAVN6tqQRruUzAEwA9ZbQ/Ef7DTHGZDUR4KklXpxM+z50opFp6m1NhNdN6HJhw==", + "license": "Apache-2.0", + "dependencies": { + "deepmerge": "^2.0.0" + }, + "peerDependencies": { + "quill": "^1.3.4" + } + }, "node_modules/quill-delta": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", @@ -3215,9 +3237,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", "dev": true, "funding": [ { @@ -3256,9 +3278,9 @@ } }, "node_modules/vite": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz", - "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/client/kb-frontend/package.json b/client/kb-frontend/package.json index b239c5e..bf9a9cd 100644 --- a/client/kb-frontend/package.json +++ b/client/kb-frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@azure/msal-browser": "^4.26.2", + "quill-blot-formatter": "^1.0.5", "react": "^18.3.1", "react-dom": "^18.3.1", "react-quill": "^2.0.0" diff --git a/client/kb-frontend/src/components/ArticleEdit.jsx b/client/kb-frontend/src/components/ArticleEdit.jsx index e7994a3..2af9cc7 100644 --- a/client/kb-frontend/src/components/ArticleEdit.jsx +++ b/client/kb-frontend/src/components/ArticleEdit.jsx @@ -1,10 +1,61 @@ import { useState, useRef } from 'react'; +import Quill from 'quill'; import ReactQuill from 'react-quill'; import 'react-quill/dist/quill.snow.css'; import './ArticleEdit.css'; import MediaGallery from './MediaGallery'; +import BlotFormatter from 'quill-blot-formatter'; -function ArticleEditor ({ article, onSave, onCancel, token }) { +Quill.register('modules/blotFormatter', BlotFormatter); +const BlockEmbed = Quill.import('blots/block/embed'); + +class VideoBlot extends BlockEmbed { + static create(value) { + const node = super.create(); + node.setAttribute('controls', ''); + node.setAttribute('style', 'max-width: 100%; height: auto;'); + node.setAttribute('width', '100%'); + + const source = document.createElement('source'); + source.setAttribute('src', value); + source.setAttribute('type', 'video/mp4'); + node.appendChild(source); + + return node; + } + + static value(node) { + const source = node.querySelector('source'); + return source ? source.getAttribute('src') : ''; + } + + static formats(node) { + return { + width: node.getAttribute('width'), + height: node.getAttribute('height') + }; + } + + format(name, value) { + if (name === 'width' || name === 'height') { + if (value) { + this.domNode.setAttribute(name, value); + } else { + this.domNode.removeAttribute(name); + } + } else { + super.format(name, value); + } + } +} + +VideoBlot.blotName = 'video'; +VideoBlot.tagName = 'video'; + +Quill.register(VideoBlot); + + +function ArticleEditor({ article, onSave, onCancel, token }) { const [title, setTitle] = useState(article.title); const [content, setContent] = useState(article.content); const [isGalleryOpen, setIsGalleryOpen] = useState(false); @@ -43,7 +94,7 @@ function ArticleEditor ({ article, onSave, onCancel, token }) { if (mediaItem.type === 'image') { quill.insertEmbed(range.index, 'image', mediaUrl); } else { - const videoHtml = ``; + quill.insertEmbed(range.index, 'video', mediaUrl); quill.clipboard.dangerouslyPasteHTML(range.index, videoHtml); } @@ -54,10 +105,42 @@ function ArticleEditor ({ article, onSave, onCancel, token }) { setIsGalleryOpen(!isGalleryOpen); }; + const modules = { + toolbar: [ + [{ 'header': [1, 2, 3, false] }], + ['bold', 'italic', 'underline', 'strike'], + [{ 'list': 'ordered' }, { 'list': 'bullet' }], + [{ 'indent': '-1' }, { 'indent': '+1' }], + ['link'], + ['clean'] + ], + blotFormatter: { + specs: [ + BlotFormatter.DefaultSpec + ], + overlay: { + style: { + border: '2px solid #3498db' + } + } + }, + clipboard: { + matchVisual: false + } + }; + + const formats = [ + 'header', + 'bold', 'italic', 'underline', 'strike', + 'list', 'bullet', 'indent', + 'link', 'image', 'video', + 'width', 'height' + ]; + return (