diff --git a/client/src/app/site/pages/meetings/modules/list-of-speakers-content/components/moderation-note/moderation-note.component.html b/client/src/app/site/pages/meetings/modules/list-of-speakers-content/components/moderation-note/moderation-note.component.html index 77d027fa9bc..741d0dadff5 100644 --- a/client/src/app/site/pages/meetings/modules/list-of-speakers-content/components/moderation-note/moderation-note.component.html +++ b/client/src/app/site/pages/meetings/modules/list-of-speakers-content/components/moderation-note/moderation-note.component.html @@ -53,7 +53,7 @@

{{ 'Moderation not } @if (isEditing) {
- +
} diff --git a/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.html b/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.html index 6761d4e9fd3..5230ed43a9e 100644 --- a/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.html +++ b/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.html @@ -1,35 +1,128 @@ -

{{ 'Insert/edit link' | translate }}

-
-
- - {{ 'URL' | translate }} - - +
+
+

{{ 'Insert/edit external link' | translate }}

+ @if (canEmbed) { + + }
- -
- @if (data.needsText) { +
+@if (toggleExternalReference || !canEmbed) { +
+
- {{ 'Text to display' | translate }} - + {{ 'URL' | translate }} + - } +
+
+ @if (data.needsText) { + + {{ 'Text to display' | translate }} + + + } +
+
+ + {{ 'Open link in ...' | translate }} + + +
+
+ +
- -
- - {{ 'Open link in ...' | translate }} - - +} +@if (canEmbed) { +
+
+

+ {{ 'Link a topic/motion/assignment' | translate }} +

+ +
-
-
- + @if (toggleInternalReference) { +
+
+ + + {{ this.translate.instant('Topics') }} + + + {{ this.translate.instant('Motions') }} + + + {{ this.translate.instant('Assignments') }} + + + + + {{ this.translate.instant(selectedRepoName) }} + + @let selectedFormControl = selectedRepoName + 'FormControl'; + @for (currentRepo of [repo ?? agendaItemRepo]; track currentRepo) { + + } + +
+
+ +
+
+ } +} + +
+ @if (isUpdate) { - + } - +
diff --git a/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.scss b/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.scss index bc76a27e252..e56c2ad1056 100644 --- a/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.scss +++ b/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.scss @@ -1,3 +1,41 @@ mat-form-field { width: 100%; } + +.radio-group { + display: flex; + gap: 15px; + margin-bottom: 5px; +} + +.references { + display: flex; + + .arrows { + display: flex; + margin-top: 7px; + margin-left: auto; + transform: translateX(-20px); + } +} + +.title { + padding: 0; +} + +.title:focus { + padding: 0; + outline: none; + box-shadow: none; + & h1 { + text-decoration: underline 2px #ccc; + border-radius: 25px; + } +} + +.clear-button { + display: flex; + justify-content: flex-end; + margin-top: -15px; + margin-bottom: -20px; +} diff --git a/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.ts b/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.ts index f21d2810468..ae56236cbc7 100644 --- a/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.ts +++ b/client/src/app/ui/modules/editor/components/editor-link-dialog/editor-link-dialog.component.ts @@ -1,9 +1,25 @@ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, DestroyRef, inject, OnInit } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormBuilder, FormControl, FormGroup, UntypedFormGroup } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { AssignmentRepositoryService } from '@app/gateways/repositories/assignments/assignment-repository.service'; +import { MotionRepositoryService } from '@app/gateways/repositories/motions/motion-repository.service'; +import { TopicRepositoryService } from '@app/gateways/repositories/topics/topic-repository.service'; +import { ViewTopic } from '@app/site/pages/meetings/pages/agenda'; +import { getAgendaListMinimalSubscriptionConfig } from '@app/site/pages/meetings/pages/agenda/agenda.subscription'; +import { ViewAssignment } from '@app/site/pages/meetings/pages/assignments'; +import { getAssignmentListMinimalSubscriptionConfig } from '@app/site/pages/meetings/pages/assignments/assignments.subscription'; +import { ViewMotion } from '@app/site/pages/meetings/pages/motions'; +import { getMotionListMinimalSubscriptionConfig } from '@app/site/pages/meetings/pages/motions/motions.subscription'; +import { ActiveMeetingIdService } from '@app/site/pages/meetings/services/active-meeting-id.service'; +import { SubscribeToConfig } from '@app/site/services/model-request.service'; +import { TranslateService } from '@ngx-translate/core'; interface EditorLinkDialogInput { link?: { href: string; target?: string }; needsText?: boolean; + isModerationNote?: boolean; } export interface EditorLinkDialogOutput { @@ -19,22 +35,129 @@ export interface EditorLinkDialogOutput { changeDetection: ChangeDetectionStrategy.Eager, standalone: false }) -export class EditorLinkDialogComponent { - public isUpdate: boolean; +export class EditorLinkDialogComponent implements OnInit { + protected translate = inject(TranslateService); + protected data = inject(MAT_DIALOG_DATA); + private fb = inject(FormBuilder); + private router = inject(Router); + private destroyRef = inject(DestroyRef); + private agendaItemRepo = inject(TopicRepositoryService); + private motionItemRepo = inject(MotionRepositoryService); + private assignmentItemRepo = inject(AssignmentRepositoryService); + private activeMeetingIdService = inject(ActiveMeetingIdService); + private dialogRef = inject(MatDialogRef); + public isUpdate: boolean; public link: { href: string; target?: string }; - public text = ``; - public constructor( - @Inject(MAT_DIALOG_DATA) public data: EditorLinkDialogInput, - private dialogRef: MatDialogRef - ) { - this.link = data.link; - this.isUpdate = !!data.link && !!data.link.href; + /** + * FormGroup for the search-list. + */ + protected internalReferenceSelector: UntypedFormGroup; + + /** + * The selected internal item + */ + protected item: ViewTopic | ViewMotion | ViewAssignment; + + /** + * Values selected by radio buttons + */ + protected internalRadioOptions: FormGroup; + protected selectedRepoName = 'Topics'; + + /** + * Values for external link + */ + protected externalLink = this.fb.group({ + extUrl: this.fb.control(''), + extText: this.fb.control(''), + extDisplayMode: this.fb.control<'_self' | '_blank'>('_self') + }); + + protected externalUrl: string; + protected externalText: string; + protected canEmbed: boolean; + protected toggleInternalReference: boolean; + protected toggleExternalReference: boolean; + protected searchSubscriptionConfig: Record; + protected searchRepos: Record; + protected repo; + + /** + * Boolean to decide, whether to open the extension-input and search-list. + */ + private editMode = false; + + private readonly externalControls = ['extUrl', 'extText', 'extDisplayMode']; + private readonly meetingId = this.activeMeetingIdService.meetingId; + private internalLink: { href: string; target?: string }; + private internalText = ``; + private subscriptionTopicConfig: SubscribeToConfig = getAgendaListMinimalSubscriptionConfig(this.meetingId); + private subscriptionMotionConfig: SubscribeToConfig = getMotionListMinimalSubscriptionConfig(this.meetingId); + private subscriptionAssignmentConfig: SubscribeToConfig = getAssignmentListMinimalSubscriptionConfig( + this.meetingId + ); + + public constructor() { + // External reference + this.link = { ...this.data.link }; + this.isUpdate = !!this.data.link && !!this.data.link.href; if (!this.link.target) { this.link.target = `_self`; } + this.externalLink.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + const { extUrl, extText, extDisplayMode } = this.externalLink.getRawValue(); + this.externalUrl = extUrl; + this.externalText = extText; + this.link.target = extDisplayMode; + this.updateDisabledState(); + }); + // Internal reference + if (!(this.canEmbed = this.embedAllowed())) return; + this.internalLink = { ...this.data.link }; + this.internalRadioOptions = this.fb.group({ + options: this.fb.control('Topics') + }); + this.internalRadioOptions + .get('options') + ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + this.selectedRepoName = value; + this.repo = this.searchRepos[this.selectedRepoName]; + }); + } + + private updateDisabledState(): void { + const disabled = !!this.externalUrl; + const optionsControl = this.internalRadioOptions.get('options'); + const referenceControl = this.internalReferenceSelector.get(this.selectedRepoName + 'FormControl'); + + optionsControl?.[disabled ? 'disable' : 'enable']({ emitEvent: false }); + referenceControl?.[disabled ? 'disable' : 'enable']({ emitEvent: false }); + } + + public ngOnInit(): void { + if (!this.canEmbed) return; + this.searchRepos = { + Topics: this.agendaItemRepo, + Motions: this.motionItemRepo, + Assignments: this.assignmentItemRepo + }; + this.searchSubscriptionConfig = { + Topics: this.subscriptionTopicConfig, + Motions: this.subscriptionMotionConfig, + Assignments: this.subscriptionAssignmentConfig + }; + this.initForm(); + } + + private embedAllowed(): boolean { + return ( + (this.router.url === `/${this.meetingId}` || this.router.url.includes('agenda/topics')) && + this.data.isModerationNote + ); } public removeLink(): void { @@ -46,14 +169,110 @@ export class EditorLinkDialogComponent { } public save(): void { - if (this.link.href && !/^[a-zA-Z]+:\/\//.test(this.link.href)) { - this.link.href = `http://` + this.link.href; + const { extUrl, extText } = this.externalLink.getRawValue(); + if (extUrl?.trim()) { + this.link.href = this.normalizeExternalUrl(extUrl); + this.internalLink.target = `_blank`; + if (this.data.needsText) { + this.dialogRef.close({ + action: `set-link`, + link: this.link, + text: extText || this.link.href + }); + } else { + this.dialogRef.close({ action: `set-link`, link: this.link }); + } + return; + } else { + this.changeEditMode(true); + if (this.internalLink.href) { + this.dialogRef.close({ + action: `set-link`, + text: this.internalText, + link: this.internalLink + }); + } } + } + + private normalizeExternalUrl(externalUrl): string { + return !/^[a-zA-Z]+:\/\//.test(externalUrl) ? `http://` + externalUrl : externalUrl; + } + + public toggleArrow(prop: 'toggleInternalReference' | 'toggleExternalReference'): void { + this[prop] = !this[prop]; + } - if (this.data.needsText) { - this.dialogRef.close({ action: `set-link`, link: this.link, text: this.text || this.link }); + /** + * Function to switch to or from editing-mode. + * + * @param save Boolean, whether the changes should be saved or resetted. + */ + public changeEditMode(save = false): void { + if (save) { + this.addReference(); } else { - this.dialogRef.close({ action: `set-link`, link: this.link }); + this.internalReferenceSelector.reset(); + this.internalText = ``; + } + this.editMode = !this.editMode; + } + + /** + * Initializes the form. + */ + protected initForm(externalLink?: FormGroup): void { + if (externalLink) { + this.externalControls.forEach(name => this.externalLink.get(name)?.reset()); + return; + } + if (!this.internalReferenceSelector) { + this.initInternalReferenceForm(); } } + + private disableExternalLinkFields(): void { + const selectedId = this.internalReferenceSelector.get(this.selectedRepoName + 'FormControl')?.value; + const repo = this.searchRepos[this.selectedRepoName]; + this.item = repo.getViewModel(selectedId); + const action = this.item ? 'disable' : 'enable'; + this.externalControls.forEach(name => { + this.externalLink.get(name)?.[action](); + }); + } + + private initInternalReferenceForm(): void { + this.internalReferenceSelector = new FormGroup({ + TopicsFormControl: new FormControl({ + value: this.agendaItemRepo, + disabled: !!this.externalUrl + }), + MotionsFormControl: new FormControl({ + value: this.motionItemRepo, + disabled: !!this.externalUrl + }), + AssignmentsFormControl: new FormControl({ + value: this.assignmentItemRepo, + disabled: !!this.externalUrl + }) + }); + this.internalReferenceSelector.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.disableExternalLinkFields(); + this.addReference(); + }); + } + + /** + * Function to add the values. + */ + private addReference(): void { + this.internalText = this.item ? `${this.item.getTitle()}` : ''; + this.internalLink.href = this.item ? this.urlBuilder(this.item) : ''; + } + + private urlBuilder(item): string { + const collection = item.collection === 'topic' ? 'agenda/topics' : `${item.collection}s`; + const id = item.collection === 'assignment' ? item.id : item.sequential_number; + return `/${this.meetingId}/${collection}/${id}`; + } } diff --git a/client/src/app/ui/modules/editor/components/editor/editor.component.html b/client/src/app/ui/modules/editor/components/editor/editor.component.html index bfafc6a604e..0edf953e3ae 100644 --- a/client/src/app/ui/modules/editor/components/editor/editor.component.html +++ b/client/src/app/ui/modules/editor/components/editor/editor.component.html @@ -10,7 +10,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [ngClass]="{ active: isGodButtonActive }" (click)="godButtonAction()" (focus)="setTab.setTab()" @@ -24,7 +24,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matMenuTriggerFor]="formatMenu" > arrow_drop_down @@ -145,7 +145,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Bold' | translate" [ngClass]="{ active: editor.isActive('bold') }" (click)="editor.chain().focus().toggleBold().run()" @@ -159,7 +159,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Italic' | translate" [ngClass]="{ active: editor.isActive('italic') }" (click)="editor.chain().focus().toggleItalic().run()" @@ -174,7 +174,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Underline' | translate" [ngClass]="{ active: editor.isActive('underline') }" (click)="editor.chain().focus().toggleUnderline().run()" @@ -189,7 +189,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Strikethrough' | translate" [ngClass]="{ active: editor.isActive('strike') }" (click)="editor.chain().focus().toggleStrike().run()" @@ -207,7 +207,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Text color' | translate" [ngClass]="{ active: editor.isActive('textStyle') }" [ngStyle]="{ @@ -231,7 +231,7 @@ list="text-colors" osArrowNavigation type="color" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Text color' | translate" [value]="editor.getAttributes('textStyle')['color']" (change)="updateFontColor($event)" @@ -249,7 +249,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Background color' | translate" [ngClass]="{ active: editor.isActive('highlight') }" [ngStyle]="{ color: editor.getAttributes('highlight')['color'] }" @@ -271,7 +271,7 @@ list="background-colors" osArrowNavigation type="color" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Background color' | translate" [value]="editor.getAttributes('highlight')['color']" (change)="updateHighlightColor($event)" @@ -286,7 +286,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Clear formatting' | translate" (click)="clearSelectedFormat()" > @@ -301,7 +301,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Bullet list' | translate" [ngClass]="{ active: editor.isActive('bulletList') }" (click)="editor.chain().focus().toggleBulletList().run()" @@ -316,7 +316,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Numbered list' | translate" [ngClass]="{ active: editor.isActive('orderedList') }" (click)="editor.chain().focus().toggleOrderedList().run()" @@ -335,7 +335,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Insert/edit link' | translate" [ngClass]="{ active: editor.isActive('link') }" (click)="setLinkDialog()" @@ -350,7 +350,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Insert/edit image' | translate" [ngClass]="{ active: editor.isActive('image') }" (click)="setImageDialog()" @@ -369,7 +369,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [disabled]="!editor.can().undo()" [matTooltip]="'Undo' | translate" (click)="editor.chain().focus().undo().run() && isDisabled.setDisabled()" @@ -383,7 +383,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [disabled]="!editor.can().redo()" [matTooltip]="'Redo' | translate" (click)="editor.chain().focus().redo().run() && isDisabled.setDisabled()" @@ -400,7 +400,7 @@ mat-ripple osArrowNavigation type="button" - [buttonList]="buttonElements" + [buttonList]="buttonElements()" [matTooltip]="'Source code' | translate" (click)="editCode()" (focus)="setTab.setTab(true)" diff --git a/client/src/app/ui/modules/editor/components/editor/editor.component.ts b/client/src/app/ui/modules/editor/components/editor/editor.component.ts index 10356313696..2ebb089e752 100644 --- a/client/src/app/ui/modules/editor/components/editor/editor.component.ts +++ b/client/src/app/ui/modules/editor/components/editor/editor.component.ts @@ -4,15 +4,13 @@ import { ChangeDetectorRef, Component, ElementRef, - EventEmitter, forwardRef, inject, - Input, + input, OnDestroy, - Output, + output, QueryList, - ViewChild, - ViewChildren + viewChild } from '@angular/core'; import { NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; @@ -96,23 +94,15 @@ const DEFAULT_COLOR_PALETE = [ standalone: false }) export class EditorComponent extends BaseFormControlComponent implements AfterViewInit, OnDestroy { - @ViewChild(`editorEl`) private editorEl: ElementRef; + public buttonElements = viewChild>(`btn`); + public isDisabled = viewChild(`isDisabled`); + public setTab = viewChild(`setTab`); + private editorEl = viewChild(`editorEl`); - @ViewChildren(`btn`) - public buttonElements!: QueryList; - - @ViewChild(`isDisabled`) public isDisabled: ArrowNavigationDirective; - - @ViewChild(`setTab`) public setTab: ArrowNavigationDirective; - - @Input() - public customSettings: object = {}; - - @Input() - public allowEmbeds = false; - - @Output() - public leaveFocus = new EventEmitter(); + public customSettings = input({}); + public allowEmbeds = input(false); + public isModerationNote = input(); + public leaveFocus = output(); public focusText = false; public focusBackground = false; @@ -169,7 +159,7 @@ export class EditorComponent extends BaseFormControlComponent implements public ngAfterViewInit(): void { const editorConfig = { - element: this.editorEl.nativeElement, + element: this.editorEl().nativeElement, extensions: this.getExtensions(), content: this.value }; @@ -352,7 +342,8 @@ export class EditorComponent extends BaseFormControlComponent implements width: `400px`, data: { link: this.editor.getAttributes(`link`), - needsText: this.editor.state.selection.empty && !this.editor.isActive(`link`) + needsText: this.editor.state.selection.empty && !this.editor.isActive(`link`), + isModerationNote: !this.isModerationNote() } }) .afterClosed() @@ -378,6 +369,7 @@ export class EditorComponent extends BaseFormControlComponent implements } ] }) + .insertContent({ type: `text`, text: ` ` }) .run(); } else { chain.setLink(result.link).run(); diff --git a/client/src/app/ui/modules/editor/editor.module.ts b/client/src/app/ui/modules/editor/editor.module.ts index 95ed9c7fda0..bbd161ea7c2 100644 --- a/client/src/app/ui/modules/editor/editor.module.ts +++ b/client/src/app/ui/modules/editor/editor.module.ts @@ -8,10 +8,12 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; +import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; import { MatTooltipModule } from '@angular/material/tooltip'; import { OpenSlidesTranslationModule } from '@app/site/modules/translations'; import { MotionEditorComponent } from '@app/site/pages/meetings/pages/motions/components/motion-editor/motion-editor.component'; +import { SearchSelectorModule } from '../search-selector'; import { EditorComponent } from './components/editor/editor.component'; import { EditorEmbedDialogComponent } from './components/editor-embed-dialog/editor-embed-dialog.component'; import { EditorHtmlDialogComponent } from './components/editor-html-dialog/editor-html-dialog.component'; @@ -43,7 +45,10 @@ const DECLARATIONS = [ MatTooltipModule, FormsModule, ArrowNavigationDirective, - OpenSlidesTranslationModule + OpenSlidesTranslationModule, + SearchSelectorModule, + MatRadioButton, + MatRadioGroup ], exports: DECLARATIONS })