diff --git a/projects/common/models/elements/element.ts b/projects/common/models/elements/element.ts index 7bae25164a759dd12deb5b917080c6b62d509bc1..7a8b659dca0de1e71a126a7b853fce254be7941a 100644 --- a/projects/common/models/elements/element.ts +++ b/projects/common/models/elements/element.ts @@ -21,6 +21,7 @@ import { InstantiationEror } from 'common/util/errors'; import { MathTableRow } from 'common/models/elements/input-elements/math-table'; import { VariableInfo } from '@iqb/responses'; +import { Markable } from 'player/src/app/models/markable.interface'; export type UIElementType = 'text' | 'button' | 'text-field' | 'text-field-simple' | 'text-area' | 'checkbox' | 'dropdown' | 'radio' | 'image' | 'audio' | 'video' | 'likert' | 'likert-row' | 'radio-group-images' | 'hotspot-image' @@ -141,7 +142,7 @@ export abstract class UIElement implements UIElementProperties { abstract getDuplicate(): UIElement; } -export type InputElementValue = TextLabel[] | Hotspot[] | MathTableRow[] | GeometryValue | string[] | string | +export type InputElementValue = Markable[] | TextLabel[] | Hotspot[] | MathTableRow[] | GeometryValue | string[] | string | number[] | number | boolean[] | boolean | null; export interface InputElementProperties extends UIElementProperties { diff --git a/projects/player/src/app/components/elements/markable-word/markable-word.component.html b/projects/player/src/app/components/elements/markable-word/markable-word.component.html index 53b25142f915d5f7749a5caecac566639cb0ff8b..673090844330dc52fdf593463fd96de71f0a0d36 100644 --- a/projects/player/src/app/components/elements/markable-word/markable-word.component.html +++ b/projects/player/src/app/components/elements/markable-word/markable-word.component.html @@ -1 +1,3 @@ -<ng-container>{{text}}</ng-container> +<span class="prevent-select" + [style.background-color]="marked ? selectionColors.yellow : 'transparent'" + (click)="toggleMarked()">{{text}}</span> diff --git a/projects/player/src/app/components/elements/markable-word/markable-word.component.scss b/projects/player/src/app/components/elements/markable-word/markable-word.component.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..29acef0c1d7e06cf0a6efed161b655b625b388ae 100644 --- a/projects/player/src/app/components/elements/markable-word/markable-word.component.scss +++ b/projects/player/src/app/components/elements/markable-word/markable-word.component.scss @@ -0,0 +1,9 @@ +.marked { + background-color: red; +} + +.prevent-select { + -webkit-user-select: none; /* Safari */ + -ms-user-select: none; /* IE 10 and IE 11 */ + user-select: none; /* Standard syntax */ +} diff --git a/projects/player/src/app/components/elements/markable-word/markable-word.component.ts b/projects/player/src/app/components/elements/markable-word/markable-word.component.ts index 712b408248d09b098640e515c198896a25c72bcf..79235e474b7d998ff3b8909d5d6751319d48b233 100644 --- a/projects/player/src/app/components/elements/markable-word/markable-word.component.ts +++ b/projects/player/src/app/components/elements/markable-word/markable-word.component.ts @@ -1,4 +1,7 @@ -import { Component, Input } from '@angular/core'; +import { + Component, EventEmitter, Input, Output +} from '@angular/core'; +import { TextElement } from 'common/models/elements/text/text'; @Component({ selector: 'aspect-markable-word', @@ -9,4 +12,13 @@ import { Component, Input } from '@angular/core'; }) export class MarkableWordComponent { @Input() text = ''; + @Input() marked!: boolean; + @Output() markedChange = new EventEmitter<boolean>(); + + selectionColors: Record<string, string> = TextElement.selectionColors; + + toggleMarked(): void { + this.marked = !this.marked; + this.markedChange.emit(this.marked); + } } diff --git a/projects/player/src/app/components/elements/markables-container/markables-container.component.html b/projects/player/src/app/components/elements/markables-container/markables-container.component.html index b81043b5cb147b9022ea7d29b1611314d5e010cf..21c1f8aa8a117ebb6096948c4c9d990dc99a43c6 100644 --- a/projects/player/src/app/components/elements/markables-container/markables-container.component.html +++ b/projects/player/src/app/components/elements/markables-container/markables-container.component.html @@ -4,7 +4,8 @@ } @if (markable.word) { <aspect-markable-word [text]="markable.word" - (click)="onClick(markable.id)"> + [(marked)]="markable.marked" + (markedChange)="onMarkedChange()"> </aspect-markable-word> } @if (markable.suffix) { diff --git a/projects/player/src/app/components/elements/markables-container/markables-container.component.ts b/projects/player/src/app/components/elements/markables-container/markables-container.component.ts index 4389492d493309cd64c50da18296afd0b94143e3..f048d5e2e96ed034d293e8a8988737784ceae13f 100644 --- a/projects/player/src/app/components/elements/markables-container/markables-container.component.ts +++ b/projects/player/src/app/components/elements/markables-container/markables-container.component.ts @@ -1,5 +1,5 @@ import { - Component, EventEmitter, Input, Output + Component, EventEmitter, Input } from '@angular/core'; import { MarkableWordComponent } from 'player/src/app/components/elements/markable-word/markable-word.component'; import { Markable } from 'player/src/app/models/markable.interface'; @@ -15,8 +15,9 @@ import { Markable } from 'player/src/app/models/markable.interface'; }) export class MarkablesContainerComponent { @Input() markables!: Markable[]; + @Input() markablesChange: EventEmitter<void> = new EventEmitter<void>(); - onClick(id: number): void { - console.log('ClickableContainerComponent clicked', id); + onMarkedChange() { + this.markablesChange.emit(); } } diff --git a/projects/player/src/app/components/elements/text-group-element/text-group-element.component.ts b/projects/player/src/app/components/elements/text-group-element/text-group-element.component.ts index c0ff45bd091f7cf69566c74a1131342d8c651843..a6e413b5c63f3d22d32204394654faa85dda5d55 100644 --- a/projects/player/src/app/components/elements/text-group-element/text-group-element.component.ts +++ b/projects/player/src/app/components/elements/text-group-element/text-group-element.component.ts @@ -38,9 +38,8 @@ export class TextGroupElementComponent extends ElementGroupDirective implements ngOnInit(): void { this.savedText = this.elementModelElementCodeMappingService - .mapToElementModelValue( - this.unitStateService.getElementCodeById(this.elementModel.id)?.value, this.elementModel - ) as string; + .mapToElementModelValue(this.unitStateService + .getElementCodeById(this.elementModel.id)?.value, this.elementModel) as string; } ngAfterViewInit(): void { diff --git a/projects/player/src/app/directives/markables.directive.ts b/projects/player/src/app/directives/markables.directive.ts index 170498e7cc7ab1a9b2a3d766feb8787fcc8f0c03..a4eec2e7159c38c3bdae1558d9ce100e282f20cd 100644 --- a/projects/player/src/app/directives/markables.directive.ts +++ b/projects/player/src/app/directives/markables.directive.ts @@ -22,7 +22,7 @@ export class MarkablesDirective implements AfterViewInit { ngAfterViewInit(): void { const nodes = MarkablesDirective.findNodes(this.elementComponent.textContainerRef.nativeElement.childNodes); - const markablesContainers = MarkablesDirective.getMarkablesContainers(nodes); + const markablesContainers = this.getMarkablesContainers(nodes); this.markableService.markables = markablesContainers .flatMap((markablesContainer: MarkablesContainer) => markablesContainer.markables); this.createComponents(markablesContainers); @@ -39,42 +39,52 @@ export class MarkablesDirective implements AfterViewInit { hostElement: markableContainerElement }); componentRef.instance.markables = markablesContainer.markables; + componentRef.instance.markablesChange.subscribe(() => { + this.elementComponent.elementValueChanged.emit( + { + id: this.elementComponent.elementModel.id, + value: this.markableService.markables + } + ); + }); this.applicationRef.attachView(componentRef.hostView); }); } - private static getMarkablesContainers(nodes: Node[]): MarkablesContainer[] { + private getMarkablesContainers(nodes: Node[]): MarkablesContainer[] { const markablesContainers: MarkablesContainer[] = []; let wordsCount = 0; nodes.forEach((node: Node) => { - const currentNodes = MarkablesDirective.getMarkablesContainer(node, wordsCount); + const currentNodes = this.getMarkablesContainer(node, wordsCount); wordsCount += currentNodes.markables.length; markablesContainers.push(currentNodes); }); return markablesContainers; } - private static getMarkablesContainer(node: Node, wordsCount: number): MarkablesContainer { + private getMarkablesContainer(node: Node, wordsCount: number): MarkablesContainer { return { node: node, - markables: MarkablesDirective.getMarkables(node.textContent || '', wordsCount) + markables: this.getMarkables(node.textContent || '', wordsCount) }; } - private static getMarkables(text: string, startIndex: number): Markable[] { + private getMarkables(text: string, startIndex: number): Markable[] { const markables: Markable[] = []; const wordsWithWhitespace = text?.match(/\s*\S+\s*/g); wordsWithWhitespace?.forEach((wordWithWhitespace: string, index: number) => { const prefix = wordWithWhitespace.match(/\s(?=[^,]*\S*)/g); const word = wordWithWhitespace.match(/\S+/g); - const after = wordWithWhitespace.match(/[^\S]\s*$/g); + const suffix = wordWithWhitespace.match(/[^\S]\s*$/g); + const id = startIndex + index; + const markedWord = this.markableService.getWordById(id); markables.push( { - id: startIndex + index, + id: id, prefix: prefix ? prefix[0] : '', word: word ? word[0] : '', - suffix: after ? after[0] : '', - marked: false + suffix: suffix ? suffix[0] : '', + marked: markedWord ? markedWord.marked : false } ); }); diff --git a/projects/player/src/app/directives/unit-state.directive.ts b/projects/player/src/app/directives/unit-state.directive.ts index 4c357abb81bfecd5c328be6f3a30edc907987e61..d181c75a7d57a00902f28fbb1462b0cded536147 100644 --- a/projects/player/src/app/directives/unit-state.directive.ts +++ b/projects/player/src/app/directives/unit-state.directive.ts @@ -9,6 +9,7 @@ import { StateVariableStateService } from 'player/src/app/services/state-variabl import { UnitStateService } from '../services/unit-state.service'; import { MediaPlayerService } from '../services/media-player.service'; import { ValidationService } from '../services/validation.service'; +import { MarkableService } from 'player/src/app/services/markable.service'; @Directive({ selector: '[aspectUnitState]' @@ -22,7 +23,8 @@ export class UnitStateDirective implements OnInit, OnDestroy { private mediaPlayerService: MediaPlayerService, private veronaSubscriptionService: VeronaSubscriptionService, private veronaPostService: VeronaPostService, - private validatorService: ValidationService + private validatorService: ValidationService, + private markableService: MarkableService ) {} ngOnInit(): void { @@ -70,6 +72,7 @@ export class UnitStateDirective implements OnInit, OnDestroy { this.stateVariableStateService.reset(); this.mediaPlayerService.reset(); this.validatorService.reset(); + this.markableService.reset(); this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); } diff --git a/projects/player/src/app/services/element-model-element-code-mapping.service.ts b/projects/player/src/app/services/element-model-element-code-mapping.service.ts index d66198a58a051b3d9d2418c0593b6ad3940f31da..abd328acd0c7a0b6d818c9e8006738a992da593c 100644 --- a/projects/player/src/app/services/element-model-element-code-mapping.service.ts +++ b/projects/player/src/app/services/element-model-element-code-mapping.service.ts @@ -26,7 +26,7 @@ export class ElementModelElementCodeMappingService { constructor(private markableService: MarkableService) {} - private static modifyAnchors(text: string): string { + static modifyAnchors(text: string): string { const regEx = /<aspect-anchor /g; return text.replace(regEx, '<aspect-anchor class="" '); } @@ -47,20 +47,12 @@ export class ElementModelElementCodeMappingService { .map((v, i) => ({ ...(elementModel as HotspotImageElement).value[i], value: v })) : (elementModel as HotspotImageElement).value; case 'text': - if ((elementModel as TextElement).markingMode === 'default') { - return (elementCodeValue !== undefined) ? - TextMarkingUtils - .restoreMarkedTextIndices( - elementCodeValue as string[], - ElementModelElementCodeMappingService.modifyAnchors((elementModel as TextElement).text)) : - ElementModelElementCodeMappingService.modifyAnchors((elementModel as TextElement).text); - } - if ((elementModel as TextElement).markingMode === 'word' || - (elementModel as TextElement).markingMode === 'range') { - return ElementModelElementCodeMappingService - .modifyAnchors((elementModel as TextElement).text); - } - return ElementModelElementCodeMappingService.modifyAnchors((elementModel as TextElement).text); + return (elementCodeValue !== undefined && (elementModel as TextElement).markingMode === 'default') ? + TextMarkingUtils + .restoreMarkedTextIndices( + elementCodeValue as string[], + ElementModelElementCodeMappingService.modifyAnchors((elementModel as TextElement).text)) : + ElementModelElementCodeMappingService.modifyAnchors((elementModel as TextElement).text); case 'audio': return elementCodeValue !== undefined ? elementCodeValue as number : @@ -111,7 +103,7 @@ export class ElementModelElementCodeMappingService { if ((elementModel as TextElement).markingMode === 'default') { return TextMarkingUtils.getMarkedTextIndices(elementModelValue as string); } - return this.markableService.getMarkables(elementModelValue as string); + return this.markableService.getMarkedMarkables(); case 'radio': case 'radio-group-images': case 'dropdown': diff --git a/projects/player/src/app/services/markable.service.ts b/projects/player/src/app/services/markable.service.ts index 977bb58ed487bf777dba93e13baf1bc28e2a9382..773f6b5430a34d209f3d91357a0d58435374c686 100644 --- a/projects/player/src/app/services/markable.service.ts +++ b/projects/player/src/app/services/markable.service.ts @@ -2,14 +2,31 @@ import { Injectable } from '@angular/core'; import { Markable } from 'player/src/app/models/markable.interface'; +import { TextElement } from 'common/models/elements/text/text'; @Injectable({ providedIn: 'root' }) export class MarkableService { markables: Markable[] = []; + selectedColor = 'yellow'; // TODO - getMarkables(text: string): string[] { - return []; + getMarkedMarkables(): string[] { + return this.markables + .filter((markable: Markable) => markable.marked) + .map((markable: Markable) => this.mapToTextSelectionFormat(markable)); + } + + private mapToTextSelectionFormat(markable: Markable): string { + const color = TextElement.selectionColors[this.selectedColor]; + return `${markable.id}-${markable.id}-${color}`; + } + + getWordById(id: number): Markable | undefined { + return this.markables.find((markable: Markable) => markable.id === id); + } + + reset(): void { + this.markables = []; } }