Merge pull request #4159 from CatoTH/Openslides3-Line-Highlighting
Line highlighting
This commit is contained in:
commit
48b79ae24d
@ -358,7 +358,8 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
to: change.getLineFrom()
|
||||
},
|
||||
true,
|
||||
lineLength
|
||||
lineLength,
|
||||
highlightLine
|
||||
);
|
||||
} else if (changes[idx - 1].getLineTo() < change.getLineFrom()) {
|
||||
text += this.extractMotionLineRange(
|
||||
@ -368,7 +369,8 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
to: change.getLineFrom()
|
||||
},
|
||||
true,
|
||||
lineLength
|
||||
lineLength,
|
||||
highlightLine
|
||||
);
|
||||
}
|
||||
text += this.getChangeDiff(targetMotion, change, lineLength, highlightLine);
|
||||
@ -418,8 +420,15 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
* @param {LineRange} lineRange
|
||||
* @param {boolean} lineNumbers - weather to add line numbers to the returned HTML string
|
||||
* @param {number} lineLength
|
||||
* @param {number|null} highlightedLine
|
||||
*/
|
||||
public extractMotionLineRange(id: number, lineRange: LineRange, lineNumbers: boolean, lineLength: number): string {
|
||||
public extractMotionLineRange(
|
||||
id: number,
|
||||
lineRange: LineRange,
|
||||
lineNumbers: boolean,
|
||||
lineLength: number,
|
||||
highlightedLine: number
|
||||
): string {
|
||||
const origHtml = this.formatMotion(id, ChangeRecoMode.Original, [], lineLength);
|
||||
const extracted = this.diff.extractRangeByLineNumbers(origHtml, lineRange.from, lineRange.to);
|
||||
let html =
|
||||
@ -429,7 +438,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
extracted.innerContextEnd +
|
||||
extracted.outerContextEnd;
|
||||
if (lineNumbers) {
|
||||
html = this.lineNumbering.insertLineNumbers(html, lineLength, null, null, lineRange.from);
|
||||
html = this.lineNumbering.insertLineNumbers(html, lineLength, highlightedLine, null, lineRange.from);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
@ -491,6 +500,19 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last line number of a motion
|
||||
*
|
||||
* @param {ViewMotion} motion
|
||||
* @param {number} lineLength
|
||||
* @return {number}
|
||||
*/
|
||||
public getLastLineNumber(motion: ViewMotion, lineLength: number): number {
|
||||
const numberedHtml = this.lineNumbering.insertLineNumbers(motion.text, lineLength);
|
||||
const range = this.lineNumbering.getLineNumberRange(numberedHtml);
|
||||
return range.to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ViewChangeReco} object based on the motion ID and the given lange range.
|
||||
* This object is not saved yet and does not yet have any changed HTML. It's meant to populate the UI form.
|
||||
@ -508,7 +530,7 @@ export class MotionRepositoryService extends BaseRepository<ViewMotion, Motion>
|
||||
changeReco.line_from = lineRange.from;
|
||||
changeReco.line_to = lineRange.to;
|
||||
changeReco.type = ModificationType.TYPE_REPLACEMENT;
|
||||
changeReco.text = this.extractMotionLineRange(motionId, lineRange, false, lineLength);
|
||||
changeReco.text = this.extractMotionLineRange(motionId, lineRange, false, lineLength, null);
|
||||
changeReco.rejected = false;
|
||||
changeReco.motion_id = motionId;
|
||||
|
||||
|
@ -6,13 +6,15 @@ import { ViewMotion } from '../../models/view-motion';
|
||||
import { ViewChangeReco } from '../../models/view-change-reco';
|
||||
import { MotionDetailDiffComponent } from './motion-detail-diff.component';
|
||||
import { MotionDetailOriginalChangeRecommendationsComponent } from '../motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component';
|
||||
import { ViewUnifiedChange } from '../../models/view-unified-change';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<os-motion-detail-diff
|
||||
[motion]="motion"
|
||||
[changes]="changes"
|
||||
(scrollToChange)="scrollToChange($event)"
|
||||
[highlightedLine]="highlightedLine"
|
||||
[scrollToChange]="scrollToChange"
|
||||
(createChangeRecommendation)="createChangeRecommendation($event)"
|
||||
>
|
||||
</os-motion-detail-diff>
|
||||
@ -21,14 +23,13 @@ import { MotionDetailOriginalChangeRecommendationsComponent } from '../motion-de
|
||||
class TestHostComponent {
|
||||
public motion: ViewMotion;
|
||||
public changes: ViewChangeReco[];
|
||||
public scrollToChange: ViewUnifiedChange = null;
|
||||
|
||||
public constructor() {
|
||||
this.motion = new ViewMotion();
|
||||
this.changes = [];
|
||||
}
|
||||
|
||||
public scrollToChange($event: Event): void {}
|
||||
|
||||
public createChangeRecommendation($event: Event): void {}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
* [motion]="motion"
|
||||
* [changes]="changes"
|
||||
* [scrollToChange]="change"
|
||||
* [highlightedLine]="highlightedLine"
|
||||
* (createChangeRecommendation)="createChangeRecommendation($event)"
|
||||
* ></os-motion-detail-diff>
|
||||
* ```
|
||||
@ -48,6 +49,8 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
public changes: ViewUnifiedChange[];
|
||||
@Input()
|
||||
public scrollToChange: ViewUnifiedChange;
|
||||
@Input()
|
||||
public highlightedLine: number;
|
||||
|
||||
@Output()
|
||||
public createChangeRecommendation: EventEmitter<LineRange> = new EventEmitter<LineRange>();
|
||||
@ -109,7 +112,13 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
return '';
|
||||
}
|
||||
|
||||
return this.motionRepo.extractMotionLineRange(this.motion.id, lineRange, true, this.lineLength);
|
||||
return this.motionRepo.extractMotionLineRange(
|
||||
this.motion.id,
|
||||
lineRange,
|
||||
true,
|
||||
this.lineLength,
|
||||
this.highlightedLine
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -138,7 +147,7 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
* @param {ViewUnifiedChange} change
|
||||
*/
|
||||
public getDiff(change: ViewUnifiedChange): SafeHtml {
|
||||
const html = this.motionRepo.getChangeDiff(this.motion, change, this.lineLength);
|
||||
const html = this.motionRepo.getChangeDiff(this.motion, change, this.lineLength, this.highlightedLine);
|
||||
return this.sanitizer.bypassSecurityTrustHtml(html);
|
||||
}
|
||||
|
||||
@ -149,7 +158,12 @@ export class MotionDetailDiffComponent extends BaseViewComponent implements Afte
|
||||
if (!this.lineLength) {
|
||||
return ''; // @TODO This happens in the test case when the lineLength-variable is not set
|
||||
}
|
||||
return this.motionRepo.getTextRemainderAfterLastChange(this.motion, this.changes, this.lineLength);
|
||||
return this.motionRepo.getTextRemainderAfterLastChange(
|
||||
this.motion,
|
||||
this.changes,
|
||||
this.lineLength,
|
||||
this.highlightedLine
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -118,8 +118,7 @@
|
||||
<span *ngIf="motion.parent_id">
|
||||
·
|
||||
<span>
|
||||
<span translate>Amendment to</span> <a
|
||||
[routerLink]="motion.parent.getDetailStateURL()">{{
|
||||
<span translate>Amendment to</span> <a [routerLink]="motion.parent.getDetailStateURL()">{{
|
||||
motion.parent.identifier || motion.parent.title
|
||||
}}</a>
|
||||
</span>
|
||||
@ -336,9 +335,7 @@
|
||||
<!-- Make the whole container a trigger to prevent unexpected menu behavior -->
|
||||
<div [matMenuTriggerFor]="tagMenu">
|
||||
<!-- No selected tags -->
|
||||
<mat-basic-chip *ngIf="!motion.hasTags()" class="grey" disabled>
|
||||
{{ '–' }}
|
||||
</mat-basic-chip>
|
||||
<mat-basic-chip *ngIf="!motion.hasTags()" class="grey" disabled> {{ '–' }} </mat-basic-chip>
|
||||
|
||||
<!-- Display a chip list of tags -->
|
||||
<mat-chip-list class="mat-chip-list-stacked">
|
||||
@ -352,9 +349,7 @@
|
||||
<!-- For non privileged users -->
|
||||
<div *ngIf="!perms.isAllowed('change_metadata', motion)">
|
||||
<mat-chip-list class="mat-chip-list-stacked">
|
||||
<mat-basic-chip *ngFor="let tag of motion.tags" class="grey">
|
||||
{{ tag }}
|
||||
</mat-basic-chip>
|
||||
<mat-basic-chip *ngFor="let tag of motion.tags" class="grey"> {{ tag }} </mat-basic-chip>
|
||||
</mat-chip-list>
|
||||
</div>
|
||||
</div>
|
||||
@ -418,6 +413,42 @@
|
||||
>
|
||||
<!-- Line Number and Diff buttons -->
|
||||
<div *ngIf="!editMotion && !motion.isStatuteAmendment()" class="motion-text-controls">
|
||||
<mat-form-field class="motion-goto-line" *ngIf="highlightedLineOpened">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
matInput
|
||||
placeholder="{{ 'Go to line' | translate }}"
|
||||
osAutofocus
|
||||
[(ngModel)]="highlightedLineTyping"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[errorStateMatcher]="highlightedLineMatcher"
|
||||
/>
|
||||
<mat-error *ngIf="highlightedLineTyping > 10" translate>Invalid line number</mat-error>
|
||||
<button
|
||||
type="submit"
|
||||
mat-button
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
aria-label="Go to line"
|
||||
*ngIf="highlightedLineTyping"
|
||||
(click)="gotoHighlightedLine(highlightedLineTyping); highlightedLineTyping = ''"
|
||||
>
|
||||
<mat-icon>redo</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-icon-button
|
||||
matTooltip="{{ 'Go to line' | translate }}"
|
||||
*ngIf="!highlightedLineOpened"
|
||||
(click)="highlightedLineOpened = true"
|
||||
>
|
||||
<mat-icon>redo</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="highlightedLineOpened = false" *ngIf="highlightedLineOpened">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
@ -547,6 +578,7 @@
|
||||
[motion]="motion"
|
||||
[changes]="allChangingObjects"
|
||||
[scrollToChange]="scrollToChange"
|
||||
[highlightedLine]="highlightedLine"
|
||||
(createChangeRecommendation)="createChangeRecommendation($event)"
|
||||
></os-motion-detail-diff>
|
||||
</ng-container>
|
||||
|
@ -23,9 +23,29 @@ span {
|
||||
|
||||
.motion-text-controls {
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
margin-top: -15px;
|
||||
height: 65px;
|
||||
|
||||
button {
|
||||
font-size: 115%;
|
||||
}
|
||||
|
||||
> button {
|
||||
// Prevent moving the buttons when the "go to line"-input is shown
|
||||
margin-top: 7px;
|
||||
}
|
||||
.motion-goto-line {
|
||||
width: 150px;
|
||||
}
|
||||
input[type='number']::-webkit-inner-spin-button,
|
||||
input[type='number']::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-info-panel {
|
||||
@ -71,6 +91,7 @@ span {
|
||||
}
|
||||
|
||||
.motion-text {
|
||||
clear: both;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml, Title } from '@angular/platform-browser';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MatDialog, MatExpansionPanel, MatSnackBar, MatCheckboxChange } from '@angular/material';
|
||||
import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms';
|
||||
import { MatDialog, MatExpansionPanel, MatSnackBar, MatCheckboxChange, ErrorStateMatcher } from '@angular/material';
|
||||
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { takeWhile } from 'rxjs/operators';
|
||||
@ -292,6 +292,21 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
*/
|
||||
public highlightedLine: number;
|
||||
|
||||
/**
|
||||
* Validator for checking the go to line number input field
|
||||
*/
|
||||
public highlightedLineMatcher: ErrorStateMatcher;
|
||||
|
||||
/**
|
||||
* Indicates if the highlight line form was opened
|
||||
*/
|
||||
public highlightedLineOpened: boolean;
|
||||
|
||||
/**
|
||||
* Holds the model for the typed line number
|
||||
*/
|
||||
public highlightedLineTyping: number;
|
||||
|
||||
/**
|
||||
* The personal notes' content for this motion
|
||||
*/
|
||||
@ -319,6 +334,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
* @param route determine if this is a new or an existing motion
|
||||
* @param formBuilder For reactive forms. Form Group and Form Control
|
||||
* @param dialogService For opening dialogs
|
||||
* @param el The native element
|
||||
* @param repo Motion Repository
|
||||
* @param agendaRepo Read out agenda variables
|
||||
* @param changeRecoRepo Change Recommendation Repository
|
||||
@ -340,7 +356,8 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
private route: ActivatedRoute,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogService: MatDialog,
|
||||
private repo: MotionRepositoryService,
|
||||
private el: ElementRef,
|
||||
public repo: MotionRepositoryService,
|
||||
private agendaRepo: AgendaRepositoryService,
|
||||
private changeRecoRepo: ChangeRecommendationRepositoryService,
|
||||
private statuteRepo: StatuteParagraphRepositoryService,
|
||||
@ -569,6 +586,15 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
statute_paragraph_id: ['']
|
||||
});
|
||||
this.updateWorkflowIdForCreateForm();
|
||||
|
||||
const component = this;
|
||||
this.highlightedLineMatcher = new class implements ErrorStateMatcher {
|
||||
public isErrorState(control: FormControl): boolean {
|
||||
const value: string = control && control.value ? control.value + '' : '';
|
||||
const maxLineNumber = component.repo.getLastLineNumber(component.motion, component.lineLength);
|
||||
return value.match(/[^\d]/) !== null || parseInt(value, 10) >= maxLineNumber;
|
||||
}
|
||||
}();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -694,7 +720,13 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
* @returns safe html strings
|
||||
*/
|
||||
public getParentMotionRange(from: number, to: number): SafeHtml {
|
||||
const str = this.repo.extractMotionLineRange(this.motion.parent_id, { from, to }, true, this.lineLength);
|
||||
const str = this.repo.extractMotionLineRange(
|
||||
this.motion.parent_id,
|
||||
{ from, to },
|
||||
true,
|
||||
this.lineLength,
|
||||
this.highlightedLine
|
||||
);
|
||||
return this.sanitizer.bypassSecurityTrustHtml(str);
|
||||
}
|
||||
|
||||
@ -773,6 +805,25 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
return this.crMode === mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights the line and scrolls to it
|
||||
* @param {number} line
|
||||
*/
|
||||
public gotoHighlightedLine(line: number): void {
|
||||
const maxLineNumber = this.repo.getLastLineNumber(this.motion, this.lineLength);
|
||||
if (line >= maxLineNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.highlightedLine = line;
|
||||
// setTimeout necessary for DOM-operations to work
|
||||
window.setTimeout(() => {
|
||||
const element = <HTMLElement>this.el.nativeElement;
|
||||
const target = element.querySelector('.os-line-number.line-number-' + line.toString(10));
|
||||
target.scrollIntoView({ behavior: 'smooth' });
|
||||
}, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* In the original version, a line number range has been selected in order to create a new change recommendation
|
||||
*
|
||||
@ -886,7 +937,7 @@ export class MotionDetailComponent extends BaseViewComponent implements OnInit {
|
||||
const configKey = isStatuteAmendment ? 'motions_statute_amendments_workflow' : 'motions_workflow';
|
||||
this.configService
|
||||
.get<string>(configKey)
|
||||
.pipe(takeWhile(id => !id, true)) // Wait for the id to be present.
|
||||
.pipe(takeWhile(id => !id)) // Wait for the id to be present.
|
||||
.subscribe(id => {
|
||||
this.contentForm.patchValue({
|
||||
workflow_id: parseInt(id as string, 10)
|
||||
|
Loading…
Reference in New Issue
Block a user