diff --git a/client/src/app/domain/models/meetings/meeting.ts b/client/src/app/domain/models/meetings/meeting.ts index 41527dafed..790697e5eb 100644 --- a/client/src/app/domain/models/meetings/meeting.ts +++ b/client/src/app/domain/models/meetings/meeting.ts @@ -141,6 +141,8 @@ export class Settings { public motions_export_follow_recommendation!: boolean; public motions_hide_metadata_background: boolean; public motions_create_enable_additional_submitter_text: boolean; + public motions_enable_restricted_editor_for_manager: boolean; + public motions_enable_restricted_editor_for_non_manager: boolean; public motion_poll_ballot_paper_selection!: BallotPaperSelection; public motion_poll_ballot_paper_number!: number; diff --git a/client/src/app/site/pages/meetings/pages/motions/components/motion-editor/motion-editor.component.spec.ts b/client/src/app/site/pages/meetings/pages/motions/components/motion-editor/motion-editor.component.spec.ts new file mode 100644 index 0000000000..bdc5530384 --- /dev/null +++ b/client/src/app/site/pages/meetings/pages/motions/components/motion-editor/motion-editor.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MotionEditorComponent } from './motion-editor.component'; + +xdescribe(`MotionEditor`, () => { + let component: MotionEditorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MotionEditorComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MotionEditorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it(`should create`, () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/pages/meetings/pages/motions/components/motion-editor/motion-editor.component.ts b/client/src/app/site/pages/meetings/pages/motions/components/motion-editor/motion-editor.component.ts new file mode 100644 index 0000000000..c5efb7c603 --- /dev/null +++ b/client/src/app/site/pages/meetings/pages/motions/components/motion-editor/motion-editor.component.ts @@ -0,0 +1,78 @@ +import { AfterViewInit, Component, forwardRef, inject } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import OfficePaste from '@intevation/tiptap-extension-office-paste'; +import { Extension } from '@tiptap/core'; +import { Bold } from '@tiptap/extension-bold'; +import { Document } from '@tiptap/extension-document'; +import { HardBreak } from '@tiptap/extension-hard-break'; +import { Heading } from '@tiptap/extension-heading'; +import { Paragraph } from '@tiptap/extension-paragraph'; +import { Text } from '@tiptap/extension-text'; +import { UndoRedo } from '@tiptap/extensions'; +import { Permission } from 'src/app/domain/definitions/permission'; +import { MeetingSettingsService } from 'src/app/site/pages/meetings/services/meeting-settings.service'; +import { OperatorService } from 'src/app/site/services/operator.service'; + +import { EditorComponent } from '../../../../../../../ui/modules/editor/components/editor/editor.component'; +import { + OsSplit, + OsSplitBulletList, + OsSplitListItem, + OsSplitOrderedList +} from '../../../../../../../ui/modules/editor/components/editor/extensions/os-split'; +import { TextStyle } from '../../../../../../../ui/modules/editor/components/editor/extensions/text-style'; + +@Component({ + selector: `os-motion-editor`, + + templateUrl: `../../../../../../../ui/modules/editor/components/editor/editor.component.html`, + styleUrls: [`../../../../../../../ui/modules/editor/components/editor/editor.component.scss`], + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MotionEditorComponent), multi: true }], + standalone: false +}) +export class MotionEditorComponent extends EditorComponent implements AfterViewInit { + private nonManagerSetting = false; + private managerSetting = false; + + private canManage = false; + + protected operator: OperatorService = inject(OperatorService); + + public constructor(private meetingSettingsService: MeetingSettingsService) { + super(); + + this.nonManagerSetting = this.meetingSettingsService.instant( + `motions_enable_restricted_editor_for_non_manager` + ); + this.managerSetting = this.meetingSettingsService.instant(`motions_enable_restricted_editor_for_manager`); + + this.canManage = this.operator.hasPerms(Permission.motionCanManage); + } + + public override getExtensions(): Extension[] { + if ((this.canManage && this.managerSetting) || (!this.canManage && this.nonManagerSetting)) { + return [ + OfficePaste, + // Nodes + Document, + HardBreak, + Heading, + OsSplitBulletList, + OsSplitOrderedList, + OsSplitListItem, + Paragraph, + Text, + + // Marks + Bold, + TextStyle, + + // Extensions + UndoRedo, + OsSplit, + this.ngExtension() + ]; + } + return super.getExtensions(); + } +} diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/modules/motion-change-recommendation-dialog/components/motion-content-change-recommendation-dialog/motion-content-change-recommendation-dialog.component.html b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/modules/motion-change-recommendation-dialog/components/motion-content-change-recommendation-dialog/motion-content-change-recommendation-dialog.component.html index ee7fcb5c02..c741191e2f 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/modules/motion-change-recommendation-dialog/components/motion-content-change-recommendation-dialog/motion-content-change-recommendation-dialog.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/modules/motion-change-recommendation-dialog/components/motion-content-change-recommendation-dialog/motion-content-change-recommendation-dialog.component.html @@ -25,7 +25,7 @@

}
- +
{{ 'Public' | translate }} diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/amendment-create-wizard/amendment-create-wizard.component.html b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/amendment-create-wizard/amendment-create-wizard.component.html index 6465d43ae5..31f888fad8 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/amendment-create-wizard/amendment-create-wizard.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/amendment-create-wizard/amendment-create-wizard.component.html @@ -110,7 +110,7 @@

}

- + } @@ -131,7 +131,7 @@

}

- + @if ( reasonRequired && contentForm.get('reason')?.invalid && diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/motion-form/motion-form.component.html b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/motion-form/motion-form.component.html index 5786e3365e..93b8f53536 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/motion-form/motion-form.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/motion-form/motion-form.component.html @@ -116,10 +116,10 @@

{{ preamble }}

} - + > @if ( contentForm.get('text')?.invalid && (contentForm.get('text')?.dirty || contentForm.get('text')?.touched) @@ -154,7 +154,10 @@

}

- + @if ( reasonRequired && contentForm.get('reason')?.invalid && diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/paragraph-based-amendment-editor/paragraph-based-amendment-editor.component.html b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/paragraph-based-amendment-editor/paragraph-based-amendment-editor.component.html index 7a54375382..871eed5b64 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/paragraph-based-amendment-editor/paragraph-based-amendment-editor.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-form/components/paragraph-based-amendment-editor/paragraph-based-amendment-editor.component.html @@ -11,7 +11,7 @@

}

- + @if (isControlInvalid(paragraph.paragraphNo)) {
{{ 'This field is required.' | translate }} diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-comment/motion-comment.component.html b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-comment/motion-comment.component.html index d164521ef8..7b61723fdf 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-comment/motion-comment.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-comment/motion-comment.component.html @@ -19,7 +19,7 @@ } @else {
- +
} @if (saveHint) { diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-final-version/motion-final-version.component.html b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-final-version/motion-final-version.component.html index de60e731f9..774bb1d4db 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-final-version/motion-final-version.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-final-version/motion-final-version.component.html @@ -1,6 +1,6 @@ @if (isEditMode) {
- + @if ( contentForm.get('modified_final_version')?.invalid && (contentForm.get('modified_final_version')?.dirty || contentForm.get('modified_final_version')?.touched) diff --git a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-personal-note/motion-personal-note.component.html b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-personal-note/motion-personal-note.component.html index aa15c14735..74b5936817 100644 --- a/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-personal-note/motion-personal-note.component.html +++ b/client/src/app/site/pages/meetings/pages/motions/pages/motion-detail/pages/motion-view/components/motion-personal-note/motion-personal-note.component.html @@ -42,7 +42,7 @@ } @else { - +
} @if (saveHint) { diff --git a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts index 7b97181b48..11eb57a5c6 100644 --- a/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts +++ b/client/src/app/site/pages/meetings/services/meeting-settings-definition.service/meeting-settings-definitions.ts @@ -636,6 +636,16 @@ export const meetingSettings: SettingsGroup[] = fillInSettingsDefaults([ label: _(`Show the sequential number for a motion`), helpText: _(`In motion list, motion detail and PDF.`), type: `boolean` + }, + { + key: `motions_enable_restricted_editor_for_non_manager`, + label: _(`Limit the editor for all users without motion manage permissions`), + type: `boolean` + }, + { + key: `motions_enable_restricted_editor_for_manager`, + label: _(`Limit the editor for all users with motion manage permissions`), + type: `boolean` } ] }, 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 83623e814a..c6de1a91b0 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 @@ -1,43 +1,53 @@
@if (editorReady) {
-
- + @if (['heading', 'textAlign', 'subscript', 'superscript'].some(isExtensionActive)) { +
+ - -
+ +
+ } - - - + @if (isExtensionActive('heading')) { + + } + + @if (isExtensionActive('textAlign')) { + + } + + @if (['subscript', 'superscript'].some(isExtensionActive)) { + + } @@ -102,154 +112,175 @@ - + @if (isExtensionActive('subscript')) { + + } - + @if (isExtensionActive('superscript')) { + + } -
- - - - -
+ @if (['italic', 'bold', 'underline', 'strike'].some(isExtensionActive)) { +
+ @if (isExtensionActive('bold')) { + + } + @if (isExtensionActive('italic')) { + + } -
- - + } + + @if (isExtensionActive('strike')) { + + } +
+ } + + @if (isExtensionActive('color')) { +
+ -
-
- - + +
+ } + + @if (isExtensionActive('highlight')) { +
+ -
+ [ngClass]="{ active: editor.isActive('highlight') }" + [ngStyle]="{ color: editor.getAttributes('highlight')['color'] }" + (click)="updateColorSets(); editor.chain().focus().toggleHighlight().run()" + > + format_color_fill + + +
+ } -
- - -
+ @if (['bulletList', 'orderedList'].some(isExtensionActive)) { +
+ @if (isExtensionActive('bulletList')) { + + } -
- + @if (isExtensionActive('orderedList')) { + + } +
+ } - -
+ @if (['link', 'iframe'].some(isExtensionActive)) { +
+ @if (isExtensionActive('link')) { + + } -
- + @if (isExtensionActive('image')) { + + } +
+ } + @if (isExtensionActive('undoRedo')) { +
+ + + +
+ } + + @if (isExtensionActive('clipboardTextSerializer')) { -
- - + }
} 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 833c98f099..fd7828ae71 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 @@ -144,7 +144,9 @@ export class EditorComponent extends BaseFormControlComponent implements } public get godButtonText(): string { - if (this.editor.isActive(`subscript`)) { + if (!['textAlign', 'subscript', 'superscript'].some(this.isExtensionActive)) { + return this.translate.instant(`Heading`); + } else if (this.editor.isActive(`subscript`)) { return this.translate.instant(`Subscript`); } else if (this.editor.isActive(`superscript`)) { return this.translate.instant(`Superscript`); @@ -157,87 +159,20 @@ export class EditorComponent extends BaseFormControlComponent implements return this.translate.instant(`Paragraph`); } - private cd: ChangeDetectorRef = inject(ChangeDetectorRef); + protected cd: ChangeDetectorRef = inject(ChangeDetectorRef); + private dialog: MatDialog = inject(MatDialog); + private translate: TranslateService = inject(TranslateService); private domParser = new DOMParser(); - public constructor( - private dialog: MatDialog, - private translate: TranslateService - ) { + public constructor() { super(); } public ngAfterViewInit(): void { const editorConfig = { element: this.editorEl.nativeElement, - extensions: [ - OfficePaste, - ClearTextcolorPaste, - // Nodes - Document, - Blockquote, - HardBreak, - Heading, - ImageResize.configure({ - inline: true - }), - OsSplitBulletList, - OsSplitOrderedList, - OsSplitListItem, - Paragraph, - Text, - Table, - TableRow, - TableHeader, - TableCell, - - // Marks - Bold, - Highlight.configure({ - multicolor: true - }), - Italic, - Link.extend({ - inclusive: false - }), - Strike, - Subscript, - Superscript, - TextStyle, - Underline, - - // Extensions - Color, - UndoRedo, - TextAlign.configure({ - types: [`heading`, `paragraph`] - }), - OsSplit, - Extension.create({ - name: `angular-component-ext`, - onCreate: () => { - this.editorReady = true; - this.cd.detectChanges(); - }, - onDestroy: () => { - this.editorReady = false; - this.leaveFocus.emit(); - }, - onBlur: () => { - this.leaveFocus.emit(); - }, - onSelectionUpdate: () => { - this.cd.detectChanges(); - }, - onUpdate: () => { - const content = this.cleanupOutput(this.editor.getHTML()); - if (this.value != content) { - this.updateForm(content); - } - } - }) - ], + extensions: this.getExtensions(), content: this.value }; @@ -261,6 +196,83 @@ export class EditorComponent extends BaseFormControlComponent implements } } + public isExtensionActive = (extension: string): boolean => + !!this.editor.extensionManager.extensions.find(ext => ext.name === extension); + + public getExtensions(): Extension[] { + return [ + OfficePaste, + ClearTextcolorPaste, + // Nodes + Document, + Blockquote, + HardBreak, + Heading, + ImageResize.configure({ + inline: true + }), + OsSplitBulletList, + OsSplitOrderedList, + OsSplitListItem, + Paragraph, + Text, + Table, + TableRow, + TableHeader, + TableCell, + + // Marks + Bold, + Highlight.configure({ + multicolor: true + }), + Italic, + Link.extend({ + inclusive: false + }), + Strike, + Subscript, + Superscript, + TextStyle, + Underline, + + // Extensions + Color, + UndoRedo, + TextAlign.configure({ + types: [`heading`, `paragraph`] + }), + OsSplit, + this.ngExtension() + ]; + } + + public ngExtension(): Extension { + return Extension.create({ + name: `angular-component-ext`, + onCreate: () => { + this.editorReady = true; + this.cd.detectChanges(); + }, + onDestroy: () => { + this.editorReady = false; + this.leaveFocus.emit(); + }, + onBlur: () => { + this.leaveFocus.emit(); + }, + onSelectionUpdate: () => { + this.cd.detectChanges(); + }, + onUpdate: () => { + const content = this.cleanupOutput(this.editor.getHTML()); + if (this.value != content) { + this.updateForm(content); + } + } + }); + } + public updateColorSets(): void { // Safari and Firefox have their own color paletes so no presets necessary if (navigator.userAgent.search(`Firefox`) > -1 || /^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { @@ -443,8 +455,20 @@ export class EditorComponent extends BaseFormControlComponent implements // Remove paragraphs inside list elements const listParagraphs = dom.querySelectorAll(`li > p`); - for (let i = 0; i < listParagraphs.length; i++) { - unwrapNode(listParagraphs.item(i)); + for (const item of listParagraphs) { + unwrapNode(item); + } + + // if editor is limited remove empty span + // color is the only element which we support which produce spans + if (!this.isExtensionActive(`color`)) { + const spanElements = dom.querySelectorAll(`span`); + for (const item of spanElements) { + item.style.removeProperty(`color`); + if (item.getAttribute(`style`) === ``) { + unwrapNode(item); + } + } } if (!this.editor.getText() && !dom.images.length) { diff --git a/client/src/app/ui/modules/editor/editor.module.ts b/client/src/app/ui/modules/editor/editor.module.ts index 61f63f2244..2df5e2b0dd 100644 --- a/client/src/app/ui/modules/editor/editor.module.ts +++ b/client/src/app/ui/modules/editor/editor.module.ts @@ -10,6 +10,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; 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 { EditorComponent } from './components/editor/editor.component'; import { EditorEmbedDialogComponent } from './components/editor-embed-dialog/editor-embed-dialog.component'; @@ -24,7 +25,8 @@ const DECLARATIONS = [ EditorEmbedDialogComponent, EditorLinkDialogComponent, EditorHtmlDialogComponent, - EditorTabNavigationDirective + EditorTabNavigationDirective, + MotionEditorComponent ]; @NgModule({