Skip to content
Snippets Groups Projects
Commit 7f1184be authored by jojohoch's avatar jojohoch
Browse files

[player] Implement intersection detection for compound element children

* Add IntersectionService
* Add CompoundElementComponent as parent for LikertComponent
* Inject ElementRef to common ElementComponent
* Rename players' ElementComponent to ElementContainerComponent
parent a6027b15
No related branches found
No related tags found
No related merge requests found
Showing
with 207 additions and 105 deletions
import {
Directive
Directive, ElementRef
} from '@angular/core';
import { UIElement } from './models/uI-element';
@Directive()
export abstract class ElementComponent {
abstract elementModel: UIElement;
constructor(private elementRef: ElementRef) {}
get domElement(): Element {
return this.elementRef.nativeElement;
}
}
import {
AfterViewInit,
Directive, EventEmitter, Output, QueryList
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import { ElementComponent } from '../../element-component.directive';
import { InputElement } from '../../models/uI-element';
@Directive({ selector: 'app-compound-element' })
export abstract class CompoundElementComponent implements AfterViewInit {
@Output() childrenAdded = new EventEmitter<QueryList<ElementComponent>>();
compoundChildren!: QueryList<ElementComponent>;
parentForm!: FormGroup;
ngAfterViewInit(): void {
this.childrenAdded.emit(this.compoundChildren);
}
abstract getFormElementModelChildren(): InputElement[];
}
import { Component, EventEmitter, Output } from '@angular/core';
import {
Component, EventEmitter, Output, QueryList, ViewChildren
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import { LikertElement } from '../../models/compound-elements/likert-element';
import { InputElementValue, ValueChangeElement } from '../../models/uI-element';
import { ValueChangeElement } from '../../models/uI-element';
import { LikertElementRow } from '../../models/compound-elements/likert-element-row';
import { LikertRadioButtonGroupComponent } from './likert-radio-button-group.component';
import { CompoundElementComponent } from './compound-element.directive';
@Component({
selector: 'app-likert',
......@@ -65,15 +69,13 @@ import { LikertElementRow } from '../../models/compound-elements/likert-element-
'::ng-deep app-likert mat-radio-button span.mat-radio-container {left: calc(50% - 10px)}'
]
})
export class LikertComponent {
export class LikertComponent extends CompoundElementComponent {
@Output() formValueChanged = new EventEmitter<ValueChangeElement>();
@ViewChildren(LikertRadioButtonGroupComponent) compoundChildren!: QueryList<LikertRadioButtonGroupComponent>;
elementModel!: LikertElement;
parentForm!: FormGroup;
getChildElementValues(): { id: string, value: InputElementValue }[] {
return this.elementModel.questions
.map((question: LikertElementRow): { id: string, value: InputElementValue } => (
{ id: question.id, value: question.value }
));
getFormElementModelChildren(): LikertElementRow[] {
return this.elementModel.questions;
}
}
......@@ -9,7 +9,7 @@ import { AppComponent } from './app.component';
import { PageComponent } from './components/page/page.component';
import { SectionComponent } from './components/section/section.component';
import { SharedModule } from '../../../common/shared.module';
import { ElementComponent } from './components/element/element.component';
import { ElementContainerComponent } from './components/element/element-container.component';
import { UnitStateComponent } from './components/unit-state/unit-state.component';
import { PlayerStateComponent } from './components/player-state/player-state.component';
import { PlayerTranslateLoader } from './classes/player-translate-loader';
......@@ -30,7 +30,7 @@ import { NumbersAndOperatorsKeyboardComponent }
AppComponent,
PageComponent,
SectionComponent,
ElementComponent,
ElementContainerComponent,
UnitStateComponent,
PlayerStateComponent,
LayoutComponent,
......
import {
Component, OnInit, Input, ComponentFactoryResolver,
ViewChild, ViewContainerRef
Component, OnInit, Input, ComponentFactoryResolver, ViewChild, ViewContainerRef, QueryList
} from '@angular/core';
import {
FormBuilder, FormControl, FormGroup, ValidatorFn
......@@ -16,19 +15,21 @@ import { UnitStateService } from '../../services/unit-state.service';
import { MarkingService } from '../../services/marking.service';
import {
InputElement,
InputElementValue,
UIElement,
ValueChangeElement
} from '../../../../../common/models/uI-element';
import { TextFieldElement } from '../../../../../common/models/text-field-element';
import { FormElementComponent } from '../../../../../common/form-element-component.directive';
import { ElementComponent } from '../../../../../common/element-component.directive';
import { CompoundElementComponent }
from '../../../../../common/element-components/compound-elements/compound-element.directive';
@Component({
selector: 'app-element',
templateUrl: './element.component.html',
styleUrls: ['./element.component.css']
selector: 'app-element-container',
templateUrl: './element-container.component.html',
styleUrls: ['./element-container.component.css']
})
export class ElementComponent implements OnInit {
export class ElementContainerComponent implements OnInit {
@Input() elementModel!: UIElement;
@Input() parentForm!: FormGroup;
@Input() parentArrayIndex!: number;
......@@ -53,24 +54,23 @@ export class ElementComponent implements OnInit {
const elementComponentFactory =
ElementFactory.getComponentFactory(this.elementModel.type, this.componentFactoryResolver);
const elementComponent = this.elementComponentContainer.createComponent(elementComponentFactory).instance;
elementComponent.elementModel = this.elementModel;
const unitStateElementCode = this.unitStateService.getUnitStateElement(this.elementModel.id);
if (unitStateElementCode && unitStateElementCode.value !== undefined) {
switch (this.elementModel.type) {
case 'text':
elementComponent.elementModel.text = unitStateElementCode.value;
break;
case 'video':
case 'audio':
elementComponent.elementModel.playbackTime = unitStateElementCode.value;
break;
default:
elementComponent.elementModel.value = unitStateElementCode.value;
}
elementComponent.elementModel = this.unitStateService.restoreUnitStateValue(this.elementModel);
if (elementComponent.domElement) {
this.unitStateService.registerElement(elementComponent.elementModel, elementComponent.domElement);
}
this.unitStateService.registerElement(elementComponent.elementModel.id, elementComponent.elementModel.value);
if (elementComponent.childrenAdded) {
elementComponent.childrenAdded
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((children: QueryList<ElementComponent>) => {
children.forEach(child => {
if (child.domElement) {
this.unitStateService.registerElement(child.elementModel, child.domElement);
}
});
});
}
if (elementComponent.applySelection) {
elementComponent.applySelection
......@@ -88,47 +88,36 @@ export class ElementComponent implements OnInit {
});
}
if (elementComponent instanceof FormElementComponent || this.elementModel.type === 'likert') {
const elementForm = this.formBuilder.group({});
if (elementComponent.formValueChanged) {
elementComponent.formValueChanged
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((changeElement: ValueChangeElement) => {
this.unitStateService.changeElementValue(changeElement);
});
}
if (elementComponent.setValidators) {
elementComponent.setValidators
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((validators: ValidatorFn[]) => {
this.formService.setValidators({
id: this.elementModel.id,
validators: validators,
formGroup: elementForm
});
});
}
const elementForm = this.formBuilder.group({});
if (elementComponent instanceof FormElementComponent) {
elementComponent.parentForm = elementForm;
this.registerFormGroup(elementForm);
this.formService.registerFormControl({
id: this.elementModel.id,
formControl: new FormControl((this.elementModel as InputElement).value),
formGroup: elementForm
});
if (this.elementModel.type === 'likert') {
elementComponent.getChildElementValues()
.forEach((element: { id: string, value: InputElementValue }) => {
this.unitStateService.registerElement(element.id, element.value);
this.formService.registerFormControl({
id: element.id,
formControl: new FormControl((element as InputElement).value),
formGroup: elementForm
});
});
}
if (elementComponent.setValidators) {
elementComponent.setValidators
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((validators: ValidatorFn[]) => {
this.formService.setValidators({
id: this.elementModel.id,
validators: validators,
formGroup: elementForm
});
});
}
elementComponent.formValueChanged
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((changeElement: ValueChangeElement) => {
this.unitStateService.changeElementValue(changeElement);
});
if (this.elementModel.inputAssistancePreset !== 'none' &&
(this.elementModel.type === 'text-field' || this.elementModel.type === 'text-area')) {
this.keyboardLayout = (this.elementModel as TextFieldElement).inputAssistancePreset;
......@@ -138,7 +127,18 @@ export class ElementComponent implements OnInit {
this.initEventsForKeyboard(elementComponent as TextAreaComponent);
}
}
} // no else
} else if (elementComponent instanceof CompoundElementComponent) {
elementComponent.parentForm = elementForm;
elementComponent.getFormElementModelChildren()
.forEach((element: InputElement) => {
this.registerFormGroup(elementForm);
this.formService.registerFormControl({
id: element.id,
formControl: new FormControl(element.value),
formGroup: elementForm
});
});
}
}
private registerFormGroup(elementForm: FormGroup): void {
......
......@@ -40,11 +40,11 @@ export class PageComponent implements OnInit {
});
}
onIntersection(detection: { detectionType: 'top' | 'bottom' | 'full', id: string }): void {
if (detection.detectionType === 'bottom') {
onIntersection(detectionType: 'top' | 'bottom'): void {
if (detectionType === 'bottom') {
this.unitStateService.addPresentedPage(this.index);
}
if (detection.detectionType === 'top' || this.isLastPage) {
if (detectionType === 'top' || this.isLastPage) {
this.selectedIndexChange.emit(this.index);
}
}
......
......@@ -2,12 +2,7 @@
<ng-template #staticElements>
<ng-container *ngFor="let element of section.elements; let i = index">
<app-element
appIntersectionDetection
detectionType="full"
[intersectionContainer]="document"
[id]="element.id"
(intersecting)="onIntersection($event)"
<app-element-container
[style.display]="'block'"
[style.overflow]="'auto'"
[style.width.px]="element.width"
......@@ -18,7 +13,7 @@
[elementModel]="element"
[parentForm]="sectionForm"
[parentArrayIndex]="i">
</app-element>
</app-element-container>
</ng-container>
</ng-template>
......@@ -29,12 +24,7 @@
[style.grid-auto-columns]="section.autoColumnSize ? 'auto' : undefined"
[style.grid-auto-rows]="section.autoRowSize ? 'auto' : undefined">
<ng-container *ngFor="let element of section.elements; let i = index">
<app-element
appIntersectionDetection
detectionType="full"
[intersectionContainer]="document"
[id]="element.id"
(intersecting)="onIntersection($event)"
<app-element-container
[style.min-width.px]="element.width"
[style.min-height.px]="element.height"
[style.margin-left.px]="element.marginLeft"
......@@ -48,7 +38,7 @@
[elementModel]="element"
[parentForm]="sectionForm"
[parentArrayIndex]="i">
</app-element>
</app-element-container>
</ng-container>
</div>
</ng-template>
......@@ -5,7 +5,6 @@ import { FormBuilder, FormGroup } from '@angular/forms';
import { DOCUMENT } from '@angular/common';
import { FormService } from '../../services/form.service';
import { Section } from '../../../../../common/models/section';
import { UnitStateService } from '../../services/unit-state.service';
@Component({
selector: 'app-section',
......@@ -20,7 +19,6 @@ export class SectionComponent implements OnInit {
constructor(private formService: FormService,
private formBuilder: FormBuilder,
private unitStateService: UnitStateService,
@Inject(DOCUMENT) public document: Document) {
}
......@@ -35,8 +33,4 @@ export class SectionComponent implements OnInit {
parentArrayIndex: this.parentArrayIndex
});
}
onIntersection(detection: { detectionType: 'top' | 'bottom' | 'full', id: string }): void {
this.unitStateService.changeElementStatus({ id: detection.id, status: 'DISPLAYED' });
}
}
......@@ -6,10 +6,9 @@ import {
selector: '[appIntersectionDetection]'
})
export class IntersectionDetectionDirective implements OnInit, OnDestroy {
@Input() detectionType!: 'top' | 'bottom' | 'full';
@Input() id!: string;
@Output() intersecting = new EventEmitter<{ detectionType: 'top' | 'bottom' | 'full', id: string }>();
@Input() intersectionContainer!: HTMLElement | Document;
@Input() detectionType!: 'top' | 'bottom';
@Output() intersecting = new EventEmitter<'top' | 'bottom'>();
@Input() intersectionContainer!: HTMLElement;
intersectionObserver!: IntersectionObserver;
......@@ -18,11 +17,7 @@ export class IntersectionDetectionDirective implements OnInit, OnDestroy {
constructor(private elementRef: ElementRef) {}
ngOnInit(): void {
if (this.detectionType === 'top') {
this.constraint = '0px 0px -95% 0px';
} else {
this.constraint = this.detectionType === 'full' ? '0px 0px 0px 0px' : '-95% 0px 0px 0px';
}
this.constraint = this.detectionType === 'top' ? '0px 0px -95% 0px' : '-95% 0px 0px 0px';
this.initIntersectionObserver();
}
......@@ -30,7 +25,7 @@ export class IntersectionDetectionDirective implements OnInit, OnDestroy {
this.intersectionObserver = new IntersectionObserver(
(entries: IntersectionObserverEntry[]): void => entries.forEach(entry => {
if (entry.isIntersecting) {
this.intersecting.emit({ detectionType: this.detectionType, id: this.id });
this.intersecting.emit(this.detectionType);
}
}), {
root: this.intersectionContainer,
......
import {
EventEmitter, Inject, Injectable, Output
} from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Injectable({
providedIn: 'root'
})
export class IntersectionService {
intersectionObserver!: IntersectionObserver;
elements: { id: string, element: Element }[] = [];
@Output() intersecting = new EventEmitter<string>();
constructor(@Inject(DOCUMENT) private document: Document) {
this.initIntersectionObserver();
}
private initIntersectionObserver(): void {
this.intersectionObserver = new IntersectionObserver(
(entries: IntersectionObserverEntry[]): void => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.intersectionDetected(entry.target);
}
});
}, {
root: document,
rootMargin: '0px 0px 0px 0px'
}
);
}
observe(id: string, element: Element): void {
this.elements.push({ id, element });
this.intersectionObserver.observe(element);
}
private intersectionDetected(element: Element):void {
const intersectedElementIndex = this.elements.findIndex(e => e.element === element);
if (intersectedElementIndex > -1) {
const intersectedElement = this.elements[intersectedElementIndex];
this.intersecting.emit(intersectedElement.id);
this.intersectionObserver.unobserve(intersectedElement.element);
this.elements.splice(intersectedElementIndex, 1);
}
}
}
......@@ -6,7 +6,13 @@ import {
UnitStateElementCodeStatus,
UnitStateElementCodeStatusValue
} from '../models/verona';
import { InputElementValue, ValueChangeElement } from '../../../../common/models/uI-element';
import {
InputElement, InputElementValue, UIElement, ValueChangeElement
} from '../../../../common/models/uI-element';
import { TextElement } from '../../../../common/models/text-element';
import { VideoElement } from '../../../../common/models/video-element';
import { AudioElement } from '../../../../common/models/audio-element';
import { IntersectionService } from './intersection.service';
@Injectable({
providedIn: 'root'
......@@ -16,7 +22,9 @@ export class UnitStateService {
private _unitStateElementCodeChanged = new Subject<UnitStateElementCode>();
unitStateElementCodes!: UnitStateElementCode[];
getUnitStateElement(id: string): UnitStateElementCode | undefined {
constructor(private intersectionService: IntersectionService) {}
private getUnitStateElement(id: string): UnitStateElementCode | undefined {
return this.unitStateElementCodes
.find((elementCode: UnitStateElementCode): boolean => elementCode.id === id);
}
......@@ -48,8 +56,31 @@ export class UnitStateService {
return this._presentedPageAdded.asObservable();
}
registerElement(id: string, value: InputElementValue): void {
this.addUnitStateElementCode(id, value);
registerElement(elementModel: UIElement, element: Element): void {
this.initUnitStateValue(elementModel);
this.intersectionService.observe(elementModel.id, element);
this.intersectionService.intersecting
.subscribe((id: string) => {
this.changeElementStatus({ id: id, status: 'DISPLAYED' });
});
}
restoreUnitStateValue(elementModel: UIElement): UIElement {
const unitStateElementCode = this.getUnitStateElement(elementModel.id);
if (unitStateElementCode && unitStateElementCode.value !== undefined) {
switch (elementModel.type) {
case 'text':
elementModel.text = unitStateElementCode.value;
break;
case 'video':
case 'audio':
elementModel.playbackTime = unitStateElementCode.value;
break;
default:
elementModel.value = unitStateElementCode.value;
}
}
return elementModel;
}
addPresentedPage(presentedPage: number): void {
......@@ -70,6 +101,22 @@ export class UnitStateService {
this.setUnitStateElementCodeStatus(elementStatus.id, elementStatus.status);
}
private initUnitStateValue(elementModel: UIElement): void {
switch (elementModel.type) {
case 'text':
this.addUnitStateElementCode(elementModel.id, (elementModel as TextElement).text);
break;
case 'video':
this.addUnitStateElementCode(elementModel.id, (elementModel as VideoElement).playbackTime);
break;
case 'audio':
this.addUnitStateElementCode(elementModel.id, (elementModel as AudioElement).playbackTime);
break;
default:
this.addUnitStateElementCode(elementModel.id, (elementModel as InputElement).value);
}
}
private addUnitStateElementCode(id: string, value: InputElementValue): void {
if (!this.getUnitStateElement(id)) {
const unitStateElementCode: UnitStateElementCode = { id: id, value: value, status: 'NOT_REACHED' };
......
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