Merge pull request #4159 from CatoTH/Openslides3-Line-Highlighting

Line highlighting
This commit is contained in:
Sean 2019-02-04 14:37:41 +01:00 committed by GitHub
commit 48b79ae24d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 166 additions and 25 deletions

View File

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

View File

@ -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 {}
}

View File

@ -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
);
}
/**

View File

@ -118,8 +118,7 @@
<span *ngIf="motion.parent_id">
&#xb7;
<span>
<span translate>Amendment to</span>&nbsp;<a
[routerLink]="motion.parent.getDetailStateURL()">{{
<span translate>Amendment to</span>&nbsp;<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>

View File

@ -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;
}

View File

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