Skip to content
Snippets Groups Projects
Commit 781ded38 authored by jojohoch's avatar jojohoch
Browse files

[editor] Make text elements markable

parent 9f95aa2c
No related branches found
No related tags found
No related merge requests found
Player Player
====== ======
- new
- make texts markable
1.0.11 1.0.11
- Disable autocomplete for text field and text area - Disable autocomplete for text field and text area
......
...@@ -45,6 +45,7 @@ import { ErrorTransformPipe } from './element-components/pipes/error-transform.p ...@@ -45,6 +45,7 @@ import { ErrorTransformPipe } from './element-components/pipes/error-transform.p
MatCheckboxModule, MatCheckboxModule,
ReactiveFormsModule, ReactiveFormsModule,
FormsModule, FormsModule,
MatIconModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatDialogModule, MatDialogModule,
......
import { Component, ElementRef, ViewChild } from '@angular/core'; import {
Component, ElementRef, EventEmitter, Output, ViewChild
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { TextElement } from '../unit'; import { TextElement } from '../unit';
import { ElementComponent } from '../element-component.directive'; import { ElementComponent } from '../element-component.directive';
...@@ -10,10 +12,21 @@ import { ElementComponent } from '../element-component.directive'; ...@@ -10,10 +12,21 @@ import { ElementComponent } from '../element-component.directive';
[style.height]="'auto'"> [style.height]="'auto'">
<div *ngIf="elementModel.highlightable"> <div *ngIf="elementModel.highlightable">
<button mat-button [style.background-color]="'yellow'" <button mat-button [style.background-color]="'yellow'"
(click)="highlightSelection('yellow')">Gelb</button> (click)="applySelection.emit({color:'yellow', element: container, clear: false})">
<mat-icon>border_color</mat-icon>
</button>
<button mat-button [style.background-color]="'turquoise'" <button mat-button [style.background-color]="'turquoise'"
(click)="highlightSelection('turquoise')">Türkis</button> (click)="applySelection.emit({color: 'turquoise', element: container, clear: false})">
<button mat-button (click)="clearHighlight()">X</button> <mat-icon>border_color</mat-icon>
</button>
<button mat-button [style.background-color]="'orange'"
(click)="applySelection.emit({color: 'orange', element: container, clear: false})">
<mat-icon>border_color</mat-icon>
</button>
<button mat-button
(click)="applySelection.emit({color: 'none', element: container, clear: true})">
<mat-icon>clear</mat-icon>
</button>
</div> </div>
<div [style.background-color]="elementModel.backgroundColor" <div [style.background-color]="elementModel.backgroundColor"
[style.color]="elementModel.fontColor" [style.color]="elementModel.fontColor"
...@@ -29,36 +42,10 @@ import { ElementComponent } from '../element-component.directive'; ...@@ -29,36 +42,10 @@ import { ElementComponent } from '../element-component.directive';
}) })
export class TextComponent extends ElementComponent { export class TextComponent extends ElementComponent {
elementModel!: TextElement; elementModel!: TextElement;
@Output()applySelection = new EventEmitter<{ color: string, element: HTMLElement, clear: boolean }>();
@ViewChild('container') containerDiv!: ElementRef; @ViewChild('container') containerDiv!: ElementRef;
highlightedNodes: Node[] = [];
constructor(public sanitizer: DomSanitizer) { constructor(public sanitizer: DomSanitizer) {
super(); super();
} }
// TODO double click selection does not work and adds more and more nested spans
highlightSelection(color: string): void {
const selection = window.getSelection();
if (selection) {
this.clearHighlight(selection.anchorNode?.parentElement as HTMLElement);
const newNode = document.createElement('SPAN');
newNode.classList.add('markedText');
newNode.style.backgroundColor = color;
this.highlightedNodes.push(newNode as Node);
const range = selection.getRangeAt(0);
range.surroundContents(newNode);
} else {
console.warn('No selection to highlight');
}
}
clearHighlight(container: HTMLElement = this.containerDiv.nativeElement): void {
(Array.from(container.children) as HTMLElement[]).forEach((child: HTMLElement) => {
if (child.classList.contains('markedText')) {
container.replaceChild(document.createTextNode(child.innerHTML), child);
}
});
}
} }
...@@ -13,6 +13,7 @@ import { TextAreaComponent } from '../../../../../common/element-components/text ...@@ -13,6 +13,7 @@ import { TextAreaComponent } from '../../../../../common/element-components/text
import { FormService } from '../../../../../common/form.service'; import { FormService } from '../../../../../common/form.service';
import { ValueChangeElement } from '../../../../../common/form'; import { ValueChangeElement } from '../../../../../common/form';
import { UnitStateService } from '../../services/unit-state.service'; import { UnitStateService } from '../../services/unit-state.service';
import { MarkingService } from '../../services/marking.service';
@Component({ @Component({
selector: 'app-element', selector: 'app-element',
...@@ -35,7 +36,8 @@ export class ElementComponent implements OnInit { ...@@ -35,7 +36,8 @@ export class ElementComponent implements OnInit {
private componentFactoryResolver: ComponentFactoryResolver, private componentFactoryResolver: ComponentFactoryResolver,
private formService: FormService, private formService: FormService,
private unitStateService: UnitStateService, private unitStateService: UnitStateService,
private formBuilder: FormBuilder) { private formBuilder: FormBuilder,
private markingService: MarkingService) {
} }
ngOnInit(): void { ngOnInit(): void {
...@@ -46,7 +48,11 @@ export class ElementComponent implements OnInit { ...@@ -46,7 +48,11 @@ export class ElementComponent implements OnInit {
const unitStateElementCode = this.unitStateService.getUnitStateElement(this.elementModel.id); const unitStateElementCode = this.unitStateService.getUnitStateElement(this.elementModel.id);
if (unitStateElementCode) { if (unitStateElementCode) {
elementComponent.elementModel.value = unitStateElementCode.value; if (this.elementModel.type === 'text') {
elementComponent.elementModel.text = unitStateElementCode.value;
} else {
elementComponent.elementModel.value = unitStateElementCode.value;
}
} }
this.unitStateService.registerElement(elementComponent.elementModel); this.unitStateService.registerElement(elementComponent.elementModel);
...@@ -59,6 +65,15 @@ export class ElementComponent implements OnInit { ...@@ -59,6 +65,15 @@ export class ElementComponent implements OnInit {
}); });
} }
if (elementComponent.applySelection) {
elementComponent.applySelection
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((selection: { color: string; element: HTMLElement; clear: boolean }) => {
this.unitStateService.changeElementStatus({ id: this.elementModel.id, status: 'TOUCHED' });
this.applySelection(selection.color, selection.element, selection.clear);
});
}
if (Object.prototype.hasOwnProperty.call(this.elementModel, 'required')) { if (Object.prototype.hasOwnProperty.call(this.elementModel, 'required')) {
const elementForm = this.formBuilder.group({}); const elementForm = this.formBuilder.group({});
elementComponent.parentForm = elementForm; elementComponent.parentForm = elementForm;
...@@ -103,6 +118,33 @@ export class ElementComponent implements OnInit { ...@@ -103,6 +118,33 @@ export class ElementComponent implements OnInit {
}); });
} }
private applySelection(color: string, element: HTMLElement, clear: boolean): void {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
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;
}
selection.removeRange(range);
} // nothing to do!
}
private isDescendantOf(node: Node | null, element: HTMLElement): boolean {
if (!node || node === document) {
return false;
}
if (node.parentElement === element) {
return true;
}
return this.isDescendantOf(node.parentNode, element);
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.ngUnsubscribe.next(); this.ngUnsubscribe.next();
this.ngUnsubscribe.complete(); this.ngUnsubscribe.complete();
......
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MarkingService {
applySelection(range: Range, selection: Selection, clear: boolean, color: string):void {
if (range.startContainer === range.endContainer) {
if (clear) {
this.clearMarkingFromNode(range);
} else {
const markedElement: HTMLElement = this.createMarkedElement(color);
range.surroundContents(markedElement);
}
} else {
const nodes: Node[] = [];
this.findNodes(range.commonAncestorContainer.childNodes, nodes, selection);
if (clear) {
this.clearMarkingFromNodes(nodes, range);
} else {
this.markNodes(nodes, range, color);
}
}
}
private clearMarkingFromNode(range: Range): void {
if (range.startContainer.parentElement?.tagName?.toUpperCase() === 'MARKED') {
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);
}
}
}
private clearMarkingFromNodes(nodes: Node[], range: Range): void {
nodes.forEach((node, index) => {
if (node.parentElement?.tagName === 'MARKED') {
const nodeValues = this.getNodeValues(node, nodes, index, range);
if (nodeValues.text) {
this.clearMarking(node, nodeValues.text, nodeValues.previousText, nodeValues.nextText, range);
}
}
});
}
private clearMarking(node: Node, text: string, previousText: string, nextText: string, range: Range) {
const textElement = document.createTextNode(text as string);
if (node.parentNode) {
const color = node.parentElement?.style.backgroundColor || 'none';
node.parentNode.parentNode?.replaceChild(textElement, node.parentNode);
if (previousText) {
const prev = this.createMarkedElement(color);
prev.append(document.createTextNode(previousText));
range.startContainer.insertBefore(prev, textElement);
}
if (nextText) {
const end = this.createMarkedElement(color);
end.append(document.createTextNode(nextText));
range.endContainer.insertBefore(end, textElement.nextSibling);
}
}
}
private mark(
node: Node, text: string, previousText: string, nextText: string, color: string
): void {
const markedElement: HTMLElement = this.createMarkedElement(color);
markedElement.append(document.createTextNode(text));
// important!
const { parentNode } = node;
parentNode?.replaceChild(markedElement, node);
if (previousText) {
const prevDOM = document.createTextNode(previousText);
parentNode?.insertBefore(prevDOM, markedElement);
}
if (nextText) {
const nextDOM = document.createTextNode(nextText);
parentNode?.insertBefore(nextDOM, markedElement.nextSibling);
}
}
private getNodeValues = (node: Node, nodes: Node[], index: number, range: Range): {
text: string, previousText: string, nextText: string
} => {
let text: string; let previousText = ''; let nextText = '';
if (index === 0) {
previousText = node.nodeValue?.substring(0, range.startOffset) || '';
text = node.nodeValue?.substring(range.startOffset) || '';
} else if (index === nodes.length - 1) {
text = node.nodeValue?.substring(0, range.endOffset) || '';
nextText = node.nodeValue?.substring(range.endOffset) || '';
} else {
text = node.nodeValue || '';
}
return { text, previousText, nextText };
};
private markNodes(nodes: Node[], range: Range, color: string): void {
nodes.forEach((node, index) => {
const nodeValues = this.getNodeValues(node, nodes, index, range);
if (nodeValues.text && node.parentElement?.tagName.toUpperCase() !== 'MARKED') {
this.mark(node, nodeValues.text, nodeValues.previousText, nodeValues.nextText, color);
}
});
}
private createMarkedElement = (color: string): HTMLElement => {
const markedElement = document.createElement('MARKED');
markedElement.style.backgroundColor = color;
return markedElement;
};
private findNodes(childList: Node[] | NodeListOf<ChildNode>, nodes: Node[], selection: Selection): void {
childList.forEach((node: Node) => {
if (selection.containsNode(node, true)) {
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
nodes.push(node);
}
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.childNodes) {
this.findNodes(node.childNodes, nodes, selection);
}
}
}
});
}
}
...@@ -22,7 +22,7 @@ export class UnitStateService { ...@@ -22,7 +22,7 @@ export class UnitStateService {
.find((elementCode: UnitStateElementCode): boolean => elementCode.id === id); .find((elementCode: UnitStateElementCode): boolean => elementCode.id === id);
} }
setUnitStateElementCodeValue(id: string, value: string | number | boolean | undefined): void { private setUnitStateElementCodeValue(id: string, value: string | number | boolean | undefined): void {
const unitStateElementCode = this.getUnitStateElement(id); const unitStateElementCode = this.getUnitStateElement(id);
if (unitStateElementCode) { if (unitStateElementCode) {
unitStateElementCode.value = value; unitStateElementCode.value = value;
...@@ -30,7 +30,7 @@ export class UnitStateService { ...@@ -30,7 +30,7 @@ export class UnitStateService {
} }
} }
setUnitStateElementCodeStatus(id: string, status: UnitStateElementCodeStatus): void { private setUnitStateElementCodeStatus(id: string, status: UnitStateElementCodeStatus): void {
const unitStateElementCode = this.getUnitStateElement(id); const unitStateElementCode = this.getUnitStateElement(id);
if (unitStateElementCode) { if (unitStateElementCode) {
// Set status only if it is higher than the old status // Set status only if it is higher than the old status
......
File added
/* fallback */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-moz-font-feature-settings: 'liga';
-moz-osx-font-smoothing: grayscale;
}
...@@ -45,6 +45,7 @@ ...@@ -45,6 +45,7 @@
</script> </script>
<link rel="stylesheet" href="player.css"> <link rel="stylesheet" href="player.css">
<link href="player/assets/roboto.css" rel="stylesheet"> <link href="player/assets/roboto.css" rel="stylesheet">
<link href="player/assets/material-icons.css" rel="stylesheet">>
</head> </head>
<body> <body>
<script type="text/javascript" src="player.js"></script> <script type="text/javascript" src="player.js"></script>
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="assets/roboto.css" rel="stylesheet"> <link href="assets/roboto.css" rel="stylesheet">
<link href="assets/material-icons.css" rel="stylesheet">
</head> </head>
<body> <body>
<player-aspect></player-aspect> <player-aspect></player-aspect>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment