diff --git a/docs/release-notes-player.txt b/docs/release-notes-player.txt index 234711331a1446fe400294ddf5dd60e58847c33a..da9894ba26067ea2fcfa546e1e0c97c97cc5c5e7 100644 --- a/docs/release-notes-player.txt +++ b/docs/release-notes-player.txt @@ -2,6 +2,8 @@ Player ====== next - [Bug] Fix position of error warn message for radio buttons + - Allow texts to be underlined + It is recommended not to combine this with highlighting texts although it works reasonably well. 1.2.2 diff --git a/projects/common/element-components/text.component.ts b/projects/common/element-components/text.component.ts index 2fedccef8f01cda6ec844a8fbbec817eb20f1d6e..5f2c503dfdcc3dce86707a8142f730ba074a0a9a 100644 --- a/projects/common/element-components/text.component.ts +++ b/projects/common/element-components/text.component.ts @@ -9,26 +9,35 @@ import { TextElement } from '../models/text-element'; template: ` <div [style.width.%]="100" [style.height]="'auto'"> - <div *ngIf="elementModel.highlightable" + <div *ngIf="elementModel.highlightable || elementModel.underlinable" class="marking-bar"> - <button class="marking-button" mat-mini-fab [style.background-color]="'yellow'" - (click)="onClick($event, {color:'yellow', element: container, clear: false})"> - <mat-icon>border_color</mat-icon> - </button> - <button class="marking-button" mat-mini-fab [style.background-color]="'turquoise'" - (click)="onClick($event, {color: 'turquoise', element: container, clear: false})"> - <mat-icon>border_color</mat-icon> - </button> - <button class="marking-button" mat-mini-fab [style.background-color]="'orange'" - (click)="onClick($event, {color: 'orange', element: container, clear: false})"> - <mat-icon>border_color</mat-icon> - </button> + <ng-container *ngIf="elementModel.highlightable"> + <button class="marking-button" mat-mini-fab [style.background-color]="'yellow'" + (click)="onMarkingButtonClick($event, { mode: 'mark', color:'yellow', element: container })"> + <mat-icon>border_color</mat-icon> + </button> + <button class="marking-button" mat-mini-fab [style.background-color]="'turquoise'" + (click)="onMarkingButtonClick($event, { mode: 'mark', color: 'turquoise', element: container })"> + <mat-icon>border_color</mat-icon> + </button> + <button class="marking-button" mat-mini-fab [style.background-color]="'orange'" + (click)="onMarkingButtonClick($event, { mode: 'mark', color: 'orange', element: container })"> + <mat-icon>border_color</mat-icon> + </button> + </ng-container> + <ng-container *ngIf="elementModel.underlinable"> + <button class="marking-button" mat-mini-fab [style.background-color]="'white'" + (click)="onMarkingButtonClick($event, { mode: 'underline', color: 'black', element: container })"> + <mat-icon>format_underlined</mat-icon> + </button> + </ng-container> <button class="marking-button" [style.background-color]="'lightgrey'" mat-mini-fab - (click)="onClick($event, {color: 'none', element: container, clear: true})"> + (click)="onMarkingButtonClick($event, { mode: 'delete', color: 'none', element: container })"> <mat-icon>clear</mat-icon> </button> </div> <div #container class="text-container" + (mousedown)="startSelection.emit($event)" [style.background-color]="elementModel.backgroundColor" [style.color]="elementModel.fontColor" [style.font-family]="elementModel.font" @@ -53,10 +62,22 @@ import { TextElement } from '../models/text-element'; }) export class TextComponent extends ElementComponent { elementModel!: TextElement; - @Output() applySelection = new EventEmitter<{ color: string, element: HTMLElement, clear: boolean }>(); + @Output() startSelection = new EventEmitter<MouseEvent>(); + @Output() applySelection = new EventEmitter <{ + mode: 'mark' | 'underline' | 'delete', + color: string, + element: HTMLElement + }>(); + @ViewChild('container') containerDiv!: ElementRef; - onClick(event: MouseEvent, markingValues: { color: string; element: HTMLElement; clear: boolean }) : void { + onMarkingButtonClick( + event: MouseEvent, markingValues: { + mode: 'mark' | 'underline' | 'delete', + color: string; + element: HTMLElement; + } + ) : void { this.applySelection.emit(markingValues); event.preventDefault(); event.stopPropagation(); diff --git a/projects/player/src/app/components/element-container/element-container.component.ts b/projects/player/src/app/components/element-container/element-container.component.ts index be1e6acef88635799cf2b479a7b69cac07ba99b7..ef013bd0fbba690b2494aabb3b8e6b7751024c2e 100644 --- a/projects/player/src/app/components/element-container/element-container.component.ts +++ b/projects/player/src/app/components/element-container/element-container.component.ts @@ -77,11 +77,23 @@ export class ElementContainerComponent implements OnInit { }); } + if (elementComponent.startSelection) { + elementComponent.startSelection + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe((mouseEvent: MouseEvent) => { + const selection = window.getSelection(); + if (mouseEvent.ctrlKey && selection?.rangeCount) { + selection.removeAllRanges(); + } + }); + } + if (elementComponent.applySelection) { elementComponent.applySelection .pipe(takeUntil(this.ngUnsubscribe)) - .subscribe((selection: { color: string; element: HTMLElement; clear: boolean }) => { - this.applySelection(selection.color, selection.element, selection.clear); + .subscribe((selection: + { mode: 'mark' | 'underline' | 'delete', color: string; element: HTMLElement; clear: boolean }) => { + this.applySelection(selection.mode, selection.color, selection.element); }); } @@ -203,20 +215,19 @@ export class ElementContainerComponent implements OnInit { }); } - private applySelection(color: string, element: HTMLElement, clear: boolean): void { + private applySelection(mode: 'mark' | 'underline' | 'delete', color: string, element: HTMLElement): void { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { - for (let i = 0; i < selection.rangeCount; i++) { - const range = selection.getRangeAt(i); - if (this.isDescendantOf(range.startContainer, element) && - this.isDescendantOf(range.endContainer, element)) { - this.markingService.applySelection(range, selection, clear, color); - this.unitStateService.changeElementValue({ - id: this.elementModel.id, - values: [this.elementModel.text as string, element.innerHTML] - }); - this.elementModel.text = element.innerHTML; - } + const range = selection.getRangeAt(0); + if (this.isDescendantOf(range.startContainer, element) && + this.isDescendantOf(range.endContainer, element)) { + const markMode = mode === 'mark' ? 'marked' : 'underlined'; + this.markingService.applySelection(range, selection, mode === 'delete', color, markMode); + this.unitStateService.changeElementValue({ + id: this.elementModel.id, + values: [this.elementModel.text as string, element.innerHTML] + }); + this.elementModel.text = element.innerHTML; } selection.removeAllRanges(); } // nothing to do! diff --git a/projects/player/src/app/services/marking.service.ts b/projects/player/src/app/services/marking.service.ts index bfbacd161099548b1af25f7368598c0fc1864bf3..bd75193e6a402dfbb7ad5e02401421d7d12c3fd4 100644 --- a/projects/player/src/app/services/marking.service.ts +++ b/projects/player/src/app/services/marking.service.ts @@ -5,13 +5,16 @@ import { Injectable } from '@angular/core'; }) export class MarkingService { private static readonly MARKING_TAG = 'MARKED'; + private static readonly UNDERLINE_TAG = 'UNDERLINED'; - applySelection(range: Range, selection: Selection, clear: boolean, color: string):void { + applySelection( + range: Range, selection: Selection, clear: boolean, color: string, markMode: 'marked' | 'underlined' + ): void { if (range.startContainer === range.endContainer) { if (clear) { this.clearMarkingFromNode(range); } else { - this.markNode(range, color); + this.markNode(range, color, markMode); } } else { const nodes: Node[] = []; @@ -19,55 +22,81 @@ export class MarkingService { if (clear) { this.clearMarkingFromNodes(nodes, range); } else { - this.markNodes(nodes, range, color); + this.markNodes(nodes, range, color, markMode); } } } private clearMarkingFromNode(range: Range): void { - if (range.startContainer.parentElement?.tagName?.toUpperCase() === MarkingService.MARKING_TAG) { + if (range.startContainer.parentElement?.tagName?.toUpperCase() === MarkingService.MARKING_TAG || + range.startContainer.parentElement?.tagName?.toUpperCase() === MarkingService.UNDERLINE_TAG) { const previousText = range.startContainer.nodeValue?.substring(0, range.startOffset) || ''; const text = range.startContainer.nodeValue?.substring(range.startOffset, range.endOffset) || ''; const nextText = range.startContainer.nodeValue?.substring(range.endOffset) || ''; if (text) { - this.clearMarking(range.startContainer, text, previousText, nextText, range); + this.clearMarking(range.startContainer, text, previousText, nextText); } } } private clearMarkingFromNodes(nodes: Node[], range: Range): void { - nodes.forEach((node, index) => { - if (node.parentElement?.tagName.toUpperCase() === MarkingService.MARKING_TAG) { - const nodeValues = this.getNodeValues(node, nodes, index, range); - if (nodeValues.text) { - this.clearMarking(node, nodeValues.text, nodeValues.previousText, nodeValues.nextText, range); - } - } - }); + const nestedMarkedNodes = nodes + .filter(node => node.parentElement?.parentElement?.tagName.toUpperCase() === MarkingService.MARKING_TAG); + const nestedUnderLinedNodes = nodes + .filter(node => node.parentElement?.parentElement?.tagName.toUpperCase() === MarkingService.UNDERLINE_TAG); + if (nestedUnderLinedNodes.length) { + this.clearNodes(nestedUnderLinedNodes, range, nodes); + } else if (nestedMarkedNodes.length) { + this.clearNodes(nestedMarkedNodes, range, nodes); + } else { + this.clearNodes(nodes, range, nodes); + } } - private clearMarking(node: Node, text: string, previousText: string, nextText: string, range: Range) { + private clearMarking(node: Node, text: string, previousText: string, nextText: string) { const textElement = document.createTextNode(text as string); if (node.parentNode) { - const color = node.parentElement?.style.backgroundColor || 'none'; - node.parentNode.parentNode?.replaceChild(textElement, node.parentNode); + const { parentNode } = node.parentNode; + const markMode = + node.parentElement?.tagName.toUpperCase() === MarkingService.MARKING_TAG ? 'marked' : 'underlined'; + const color = + markMode === 'underlined' ? 'black' : (node.parentNode as HTMLElement).style.backgroundColor || 'none'; + parentNode?.replaceChild(textElement, node.parentNode); if (previousText) { - const prev = this.createMarkedElement(color); + const prev = this.createMarkedElement(color, markMode); prev.append(document.createTextNode(previousText)); - range.startContainer.insertBefore(prev, textElement); + prev.style.textDecoration = `solid underline ${color}`; + parentNode?.insertBefore(prev, textElement); } if (nextText) { - const end = this.createMarkedElement(color); + const end = this.createMarkedElement(color, markMode); end.append(document.createTextNode(nextText)); - range.endContainer.insertBefore(end, textElement.nextSibling); + end.style.textDecoration = `solid underline ${color}`; + parentNode?.insertBefore(end, textElement.nextSibling); } } } + private clearNodes(nodes: Node[], range: Range, allNodes: Node[]): void { + nodes.forEach((node: Node) => { + const index = allNodes.findIndex(rangeNode => rangeNode === node); + if (node.parentElement?.tagName.toUpperCase() === MarkingService.MARKING_TAG || + node.parentElement?.tagName.toUpperCase() === MarkingService.UNDERLINE_TAG) { + const nodeValues = this.getNodeValues(node, nodes, index, range, allNodes.length); + if (nodeValues.text) { + this.clearMarking(node, nodeValues.text, nodeValues.previousText, nodeValues.nextText); + } else { + // eslint-disable-next-line no-console + console.warn('Cannot recreate node for text', nodeValues); + } + } + }); + } + private mark( - node: Node, text: string, previousText: string, nextText: string, color: string + node: Node, text: string, previousText: string, nextText: string, color: string, markMode: 'marked' | 'underlined' ): void { - const markedElement: HTMLElement = this.createMarkedElement(color); + const markedElement: HTMLElement = this.createMarkedElement(color, markMode); markedElement.append(document.createTextNode(text)); // important! const { parentNode } = node; @@ -82,11 +111,16 @@ export class MarkingService { } } - private getNodeValues = (node: Node, nodes: Node[], index: number, range: Range): { + private getNodeValues = (node: Node, nodes: Node[], index: number, range: Range, nodesCount: number): { text: string, previousText: string, nextText: string } => { - const start = Math.min(range.startOffset, range.endOffset); - const end = Math.max(range.startOffset, range.endOffset); + let start = range.startOffset; + let end = range.endOffset; + // Firefox double click hack + if (nodesCount === 1) { + start = Math.min(range.startOffset, range.endOffset); + end = Math.max(range.startOffset, range.endOffset); + } let text: string; let previousText = ''; let nextText = ''; if (index === 0) { previousText = node.nodeValue?.substring(0, start) || ''; @@ -100,25 +134,34 @@ export class MarkingService { return { text, previousText, nextText }; }; - private markNode(range: Range, color: string): void { - if (range.startContainer.parentElement?.tagName.toUpperCase() !== MarkingService.MARKING_TAG) { - const markedElement: HTMLElement = this.createMarkedElement(color); + private markNode(range: Range, color: string, markMode: 'marked' | 'underlined'): void { + if (range.startContainer.parentElement?.tagName.toUpperCase() !== MarkingService.MARKING_TAG || + range.startContainer.parentElement?.tagName.toUpperCase() !== MarkingService.UNDERLINE_TAG) { + const markedElement: HTMLElement = this.createMarkedElement(color, markMode); range.surroundContents(markedElement); } } - private markNodes(nodes: Node[], range: Range, color: string): void { + private markNodes(nodes: Node[], range: Range, color: string, markMode: 'marked' | 'underlined'): void { nodes.forEach((node, index) => { - const nodeValues = this.getNodeValues(node, nodes, index, range); - if (nodeValues.text && node.parentElement?.tagName.toUpperCase() !== MarkingService.MARKING_TAG) { - this.mark(node, nodeValues.text, nodeValues.previousText, nodeValues.nextText, color); + const nodeValues = this.getNodeValues(node, nodes, index, range, nodes.length); + if (nodeValues.text && node.parentElement?.tagName.toUpperCase() !== MarkingService.MARKING_TAG && + (nodeValues.text && node.parentElement?.tagName.toUpperCase() !== MarkingService.UNDERLINE_TAG) + ) { + this.mark(node, nodeValues.text, nodeValues.previousText, nodeValues.nextText, color, markMode); } }); } - private createMarkedElement = (color: string): HTMLElement => { - const markedElement = document.createElement(MarkingService.MARKING_TAG); - markedElement.style.backgroundColor = color; + private createMarkedElement = (color: string, markMode: 'marked' | 'underlined'): HTMLElement => { + let markedElement; + if (markMode === 'marked') { + markedElement = document.createElement(MarkingService.MARKING_TAG); + markedElement.style.backgroundColor = color; + } else { + markedElement = document.createElement(MarkingService.UNDERLINE_TAG); + markedElement.style.textDecoration = `underline solid ${color}`; + } return markedElement; }; diff --git a/projects/player/src/app/services/unit-state.service.ts b/projects/player/src/app/services/unit-state.service.ts index a21464775b0c21dadb74f4ab6c336fc915f52c3b..1aa2971cec76af54d56800236e7254e223588427 100644 --- a/projects/player/src/app/services/unit-state.service.ts +++ b/projects/player/src/app/services/unit-state.service.ts @@ -71,8 +71,8 @@ export class UnitStateService { changeElementValue(elementValues: ValueChangeElement): void { // eslint-disable-next-line no-console - console.log(`player: changeElementValue ${elementValues.id}: - old: ${elementValues.values[0]}, new: ${elementValues.values[1]}`); + // console.log(`player: changeElementValue ${elementValues.id}: + // old: ${elementValues.values[0]}, new: ${elementValues.values[1]}`); this.setUnitStateElementCodeStatus(elementValues.id, 'VALUE_CHANGED'); this.setUnitStateElementCodeValue(elementValues.id, elementValues.values[1]); }