-
+ @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