diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7276e67ef..be2e7cdea 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -38,6 +38,7 @@ "ngx-cookie-service": "^18.0.0", "ngx-cookieconsent": "^6.0.0", "ngx-dropzone": "^3.0.0", + "ngx-editor": "^17.5.4", "ngx-guided-tour": "^2.0.1", "ngx-matomo-client": "^6.2.0", "rxjs": "^7.4.0", @@ -3112,6 +3113,28 @@ "node": ">=12" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", + "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==", + "dependencies": { + "@floating-ui/utils": "^0.2.4" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz", + "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.4" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", + "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==" + }, "node_modules/@inquirer/figures": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.3.tgz", @@ -5218,6 +5241,11 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -9813,6 +9841,32 @@ "tslib": "^2.0.0" } }, + "node_modules/ngx-editor": { + "version": "17.5.4", + "resolved": "https://registry.npmjs.org/ngx-editor/-/ngx-editor-17.5.4.tgz", + "integrity": "sha512-1uezbJnMFyELY3h+bNovZCIjrgqXOuIiEA1ZcBb9KIaCoRf6RtBqeRxz/nZVE4lvadHyCH/F4iLqhC8bVDEf2w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/dom": "^1.6.0", + "@types/trusted-types": "~2.0.7", + "prosemirror-commands": "^1.5.2", + "prosemirror-history": "^1.4.0", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-model": "^1.21.0", + "prosemirror-schema-list": "^1.3.0", + "prosemirror-state": "^1.4.3", + "prosemirror-view": "^1.33.6", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=17.1.0", + "@angular/core": ">=17.1.0", + "@angular/forms": ">=17.1.0", + "@angular/platform-browser": ">=17.1.0", + "rxjs": ">=7.8.0" + } + }, "node_modules/ngx-guided-tour": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ngx-guided-tour/-/ngx-guided-tour-2.0.1.tgz", @@ -10328,6 +10382,11 @@ "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==", "dev": true }, + "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", @@ -10914,6 +10973,91 @@ "node": ">=10" } }, + "node_modules/prosemirror-commands": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz", + "integrity": "sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^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-model": { + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.21.3.tgz", + "integrity": "sha512-nt2Xs/RNGepD9hrrkzXvtCm1mpGJoQfFSPktGa0BF/aav6XsnmVGZ9sTXNWRLupAz5SCLa3EyKlFeK7zJWROKg==", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.4.0.tgz", + "integrity": "sha512-nZOIq/AkBSzCENxUyLm5ltWE53e2PLk65ghMN8qLQptOmDVixZlPqtMeQdiNw0odL9vNpalEjl3upgRkuJ/Jyw==", + "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-transform": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz", + "integrity": "sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg==", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.33.8", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.33.8.tgz", + "integrity": "sha512-4PhMr/ufz2cdvFgpUAnZfs+0xij3RsFysreeG9V/utpwX7AJtYCDVyuRxzWoMJIEf4C7wVihuBNMPpFLPCiLQw==", + "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", @@ -11358,6 +11502,11 @@ "fsevents": "~2.3.2" } }, + "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-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -13280,6 +13429,11 @@ "@esbuild/win32-x64": "0.20.2" } }, + "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/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 2fb2dbc92..496823c5f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,6 +42,7 @@ "ngx-cookie-service": "^18.0.0", "ngx-cookieconsent": "^6.0.0", "ngx-dropzone": "^3.0.0", + "ngx-editor": "^17.5.4", "ngx-guided-tour": "^2.0.1", "ngx-matomo-client": "^6.2.0", "rxjs": "^7.4.0", diff --git a/frontend/src/app/library/rich-text-editor/ngx-rich-text-editor.component.html b/frontend/src/app/library/rich-text-editor/ngx-rich-text-editor.component.html new file mode 100644 index 000000000..1f7263c4e --- /dev/null +++ b/frontend/src/app/library/rich-text-editor/ngx-rich-text-editor.component.html @@ -0,0 +1,22 @@ + +
+ + +
+ +has error + diff --git a/frontend/src/app/library/rich-text-editor/ngx-rich-text-editor.component.scss b/frontend/src/app/library/rich-text-editor/ngx-rich-text-editor.component.scss new file mode 100644 index 000000000..fc98e81c4 --- /dev/null +++ b/frontend/src/app/library/rich-text-editor/ngx-rich-text-editor.component.scss @@ -0,0 +1,7 @@ +.NgxEditor__Wrapper { + min-height: 150px; + + .error { + border: 1px solid #f44336 !important; + } +} diff --git a/frontend/src/app/library/rich-text-editor/ngx-rich-text-editor.component.ts b/frontend/src/app/library/rich-text-editor/ngx-rich-text-editor.component.ts new file mode 100644 index 000000000..0853ab8b2 --- /dev/null +++ b/frontend/src/app/library/rich-text-editor/ngx-rich-text-editor.component.ts @@ -0,0 +1,86 @@ +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from "@angular/core"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; +import { Editor, Toolbar, toHTML, toDoc } from "ngx-editor"; + +@Component({ + selector: 'rich-text-editor-component', + // selector: 'app-rich-text-editor', + templateUrl: './ngx-rich-text-editor.component.html', + styleUrls: ['./ngx-rich-text-editor.component.scss'], +}) +export class NgxRichTextEditorComponent implements OnInit, OnChanges, OnDestroy { + + @Input() form: FormControl; + @Input() id: string = "editor1"; + @Input() placeholder: string = "Enter text"; + @Input() required: boolean = false; + @Input() wrapperClasses: string = ""; + @Input() editable: boolean = true; + + editor: Editor; + internalControl!: FormControl; + + toolbar: Toolbar = [ + ['undo', 'redo'], + ['bold', 'italic', 'underline', 'strike', 'superscript', 'subscript'], + ['align_left', 'align_center', 'align_right', 'align_justify'], + ['bullet_list', 'ordered_list'], + ['text_color'], + [{ heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] }], + ['link', 'horizontal_rule'], + [ 'format_clear'], + ]; + + constructor(private formBuilder: FormBuilder) {} + + ngOnInit(): void { + if (this.required) this.placeholder = this.placeholder + '*'; + + const isEmpty = this.form?.value == null || this._isEmpty(toDoc(this.form?.value)); + + if (isEmpty) { + this.form?.setValue(null); + this.internalControl = this.formBuilder.control(null, Validators.required); + } else { + this.internalControl = this.formBuilder.control(toDoc(this.form?.value), Validators.required); + } + + this.editor = new Editor({ content: isEmpty ? null : toDoc(this.form?.value) }); + + if (!this.editable) this.internalControl?.disable(); + } + + ngOnChanges(changes: SimpleChanges): void { + } + + ngOnDestroy(): void { + this.editor.destroy(); + } + + onChange(event: any): void { + // console.log(event); + + if (this._isEmpty(event)) this.form.setValue(null); + else this.form.setValue(toHTML(event)); + } + + private _isEmpty(richTextContent: any): boolean { + const content = richTextContent.content; + + if (!Array.isArray(content)) return true; + + const isEmpty = content?.every(c => { + + if (c.content == null) return true; + + let text = c.content; + if (Array.isArray(text)) { + if (text?.some(t => t.text != null && t.text?.trim() != '')) return false; + } + + return true; + }); + + return isEmpty; + } +} \ No newline at end of file diff --git a/frontend/src/app/library/rich-text-editor/rich-text-editor.component.ts b/frontend/src/app/library/rich-text-editor/rich-text-editor.component.ts index b9c73c95f..0b98c1a5e 100644 --- a/frontend/src/app/library/rich-text-editor/rich-text-editor.component.ts +++ b/frontend/src/app/library/rich-text-editor/rich-text-editor.component.ts @@ -4,7 +4,8 @@ import { AngularEditorConfig } from "@kolkov/angular-editor"; import { Subscription } from "rxjs"; @Component({ - selector: 'rich-text-editor-component', + selector: 'old-rich-text-editor-component', + // selector: 'rich-text-editor-component', template: `