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 1845e51093..5e6d907fb7 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,142 @@ -

Insert/edit link

-
-
- - URL - - +
+
+

Insert/edit external link

+
- -
- @if (data.needsText) { +
+@if (toggleExternalReference) { +
+
+ + URL + + +
+
+ @if (data.needsText) { + + Text to display + + + } +
+
- Text to display - + Open link in ... + - } +
- -
- - Open link in ... - - +} +
+
+

Link a topic/motion/assignment

+
-
- +@if (toggleInternalReference) { +
+
+ + + Topics + + + Motions + + + Assignments + + + + + {{ searchLists[selectedRepoValue].label }} + + @for (searchRepo of searchRepos; track $index) { + @if (selectedRepoValue === $index) { + + } + } + +
+
+ +
+
+} + +
+ @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 bc76a27e25..45cb5bd063 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,43 @@ 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); + } +} + +// How the focus is highlighted must be discussed further. Actual style is provisional. + +.title { + padding: 0; +} + +.title:focus { + padding: 0; + outline: none; + box-shadow: none; + & h1 { + background-color: #eee; + 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 dd1dcde6f6..47371b273d 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,5 +1,18 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, inject, Input, OnInit } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, UntypedFormGroup } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { AgendaItemRepositoryService } from 'src/app/gateways/repositories/agenda'; +import { AssignmentRepositoryService } from 'src/app/gateways/repositories/assignments/assignment-repository.service'; +import { MotionRepositoryService } from 'src/app/gateways/repositories/motions'; +import { getAgendaListMinimalSubscriptionConfig } from 'src/app/site/pages/meetings/pages/agenda/agenda.subscription'; +import { ViewAgendaItem } from 'src/app/site/pages/meetings/pages/agenda/view-models'; +import { ViewAssignment } from 'src/app/site/pages/meetings/pages/assignments/view-models/view-assignment'; +import { ViewMotion } from 'src/app/site/pages/meetings/pages/motions/view-models/view-motion'; +import { ActiveMeetingIdService } from 'src/app/site/pages/meetings/services/active-meeting-id.service'; +import { SubscribeToConfig } from 'src/app/site/services/model-request.service'; interface EditorLinkDialogInput { link?: { href: string; target?: string }; @@ -18,22 +31,130 @@ export interface EditorLinkDialogOutput { styleUrls: [`editor-link-dialog.component.scss`], standalone: false }) -export class EditorLinkDialogComponent { +export class EditorLinkDialogComponent implements OnInit { public isUpdate: boolean; public link: { href: string; target?: string }; public text = ``; + public internalLink: { href: string; target?: string }; + + public internalText = ``; + + public toggleInternalReference: boolean; + public toggleExternalReference: boolean; + + private activeMeetingIdService = inject(ActiveMeetingIdService); + public subscriptionConfig: SubscribeToConfig = getAgendaListMinimalSubscriptionConfig( + this.activeMeetingIdService.meetingId + ); + + /** + * Initial value of the input-field. + */ + @Input() + public searchFieldInput!: string; + + /** + * Boolean to decide, whether to open the extension-input and search-list. + */ + public editMode = false; + + /** + * Model for the input-field. + */ + public inputControl; + + /** + * Init Repos + */ + public agendaItemRepo = inject(AgendaItemRepositoryService); + public motionItemRepo = inject(MotionRepositoryService); + public assignmentItemRepo = inject(AssignmentRepositoryService); + /** + * Define lists + */ + protected agendaItemList: Observable[]>; + protected motionItemList: Observable; + protected assignmentItemList: Observable; + + public searchLists; + public searchRepos; + /** + * FormGroup for the search-list. + */ + public internalReferenceForm: UntypedFormGroup; + + /** + * The selected internal item + */ + public item; + + /** + * Values selected by radio buttons + */ + public internalRadioOptions: FormGroup; + public selectedRepoValue = 0; + + /** + * Values for external link + */ + public externalLink: FormGroup; + public externalUrl: string; + public externalText: string; + public externalDisplayMode: string; + public constructor( @Inject(MAT_DIALOG_DATA) public data: EditorLinkDialogInput, - private dialogRef: MatDialogRef + private dialogRef: MatDialogRef, + private router: Router, + private fb: FormBuilder, + private translate: TranslateService ) { - this.link = data.link; + // External reference + this.link = { ...data.link }; this.isUpdate = !!data.link && !!data.link.href; if (!this.link.target) { this.link.target = `_self`; } + this.externalLink = this.fb.group({ + extUrl: new FormControl(), + extText: new FormControl(), + extDisplayMode: new FormControl() + }); + this.externalLink.valueChanges.subscribe(() => { + this.externalUrl = this.externalLink.get('extUrl').value; + this.externalText = this.externalLink.get('extText').value; + this.externalDisplayMode = this.externalLink.get('extDisplayMode').value; + }); + + // Internal reference + this.internalLink = { ...data.link }; + if (!this.internalLink.target) { + this.internalLink.target = `_blank`; + } + this.internalRadioOptions = this.fb.group({ + options: [0] + }); + this.internalRadioOptions.valueChanges.subscribe(() => { + this.selectedRepoValue = this.internalRadioOptions.get('options').value; + }); + } + + public ngOnInit(): void { + this.agendaItemList = this.agendaItemRepo.getSortedViewModelListObservable(); + this.motionItemList = this.motionItemRepo.getSortedViewModelListObservable(); + this.assignmentItemList = this.assignmentItemRepo.getSortedViewModelListObservable(); + + this.searchLists = [ + { observable: this.agendaItemList, label: this.translate.instant('Topic') }, + { observable: this.motionItemList, label: this.translate.instant('Motion') }, + { observable: this.assignmentItemList, label: this.translate.instant('Assignment') } + ]; + this.searchRepos = [this.agendaItemRepo, this.motionItemRepo, this.assignmentItemRepo]; + this.initInput(); + this.initForm(); } public removeLink(): void { @@ -45,14 +166,98 @@ export class EditorLinkDialogComponent { } public save(): void { - if (this.link.href && !/^[a-zA-Z]+:\/\//.test(this.link.href)) { - this.link.href = `http://` + this.link.href; + if (this.externalUrl) { + if (!/^[a-zA-Z]+:\/\//.test(this.link.href)) { + this.link.href = this.externalUrl.includes(`http`) ? this.externalUrl : `http://` + this.externalUrl; + } + if (this.data.needsText) { + this.dialogRef.close({ action: `set-link`, link: this.link, text: this.externalText || this.link }); + } else { + this.dialogRef.close({ action: `set-link`, link: this.link }); + } + } else { + this.changeEditMode(true); + if (!/^[a-zA-Z]+:\/\//.test(this.internalLink.href)) { + this.dialogRef.close({ + action: `set-link`, + text: this.internalText, + link: this.internalLink + }); + } } + } - if (this.data.needsText) { - this.dialogRef.close({ action: `set-link`, link: this.link, text: this.text || this.link }); + public toggleArrow(prop: 'toggleInternalReference' | 'toggleExternalReference'): void { + this[prop] = !this[prop]; + } + + /** + * 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.initForm(); + this.initInput(); + this.internalText = ``; } + this.editMode = !this.editMode; + } + + /** + * Initialize the value of the input. + */ + public initInput(): void { + this.inputControl = this.searchFieldInput; + } + + /** + * Initializes the form. + */ + public initForm(): void { + this.internalReferenceForm = new FormGroup({ + TopicFormControl: new FormControl(this.agendaItemRepo), + MotionFormControl: new FormControl(this.motionItemRepo), + AssignmentFormControl: new FormControl(this.assignmentItemRepo) + }); + this.internalReferenceForm.valueChanges.subscribe(() => { + const controlName = `${this.searchLists[this.selectedRepoValue].label}FormControl`; + const selectedId = this.internalReferenceForm.get(controlName)?.value; + const repo = this.searchRepos[this.selectedRepoValue]; + this.item = repo.getViewModel(selectedId); + const action = this.item ? 'disable' : 'enable'; + ['extUrl', 'extText', 'extDisplayMode'].forEach(name => this.externalLink.get(name)?.[action]()); + + this.addReference(); + }); + } + + /** + * Function to add the values. + */ + public addReference(): void { + this.internalText = this.item ? `${this.item.getTitle()}` : ''; + this.internalLink.href = this.item ? this.urlBuilder(this.item) : ''; + } + + public urlBuilder(item): string { + const parts = item.content_object_id?.split('/'); + const isAgendaItem = item.collection === 'agenda_item' && item.content_object_id?.split('/')[0] === 'topic'; + const setCollection: string = isAgendaItem + ? 'agenda/topic' + : item.collection === 'agenda_item' + ? parts?.[0] + : item.collection; + const setId: number = isAgendaItem + ? item.content_object.sequential_number + : item.content_object_id + ? parts?.[1] + : item.sequential_number; + const builtUrl = `${this.activeMeetingIdService.meetingId}/${setCollection}s/${setId}`; + const url = this.router.url.replace(/^\/.*$/, `/${builtUrl}`); + return url; } } 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 c7a24320ec..f98058285b 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 @@ -379,6 +379,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 ee73d107ca..5dafd45ab8 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 'src/app/site/modules/translations'; import { MotionEditorComponent } from 'src/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.forChild() + OpenSlidesTranslationModule.forChild(), + SearchSelectorModule, + MatRadioButton, + MatRadioGroup ], exports: DECLARATIONS })