From 6d1ad52c8edfe14a53b40746394a11e7ad64befe Mon Sep 17 00:00:00 2001 From: rhenck <richard.henck@iqb.hu-berlin.de> Date: Mon, 7 Nov 2022 12:20:41 +0100 Subject: [PATCH] Re-implement DropList element - Replace Material Droplist with native HTML events - Remove simple-drop-list element; Cloze elements now use the normal DropList element - Add example units --- docs/unit_definition_changelog.txt | 4 +- example_data/droplist/dnd1.json | 1 + example_data/droplist/dnd2.json | 1 + example_data/droplist/dnd3.json | 1 + example_data/droplist/dnd4.json | 1 + package.json | 2 +- .../drop-list-simple.component.ts | 171 -------- .../cloze/compound-child-overlay.component.ts | 12 +- .../input-elements/drop-list.component.ts | 415 ++++++++++-------- .../cloze-child-elements/drop-list-simple.ts | 67 --- .../elements/compound-elements/cloze/cloze.ts | 11 +- .../tiptap-editor-extensions/drop-list.ts | 6 +- projects/common/models/elements/element.ts | 9 +- .../elements/input-elements/drop-list.ts | 31 +- projects/common/models/section.ts | 13 +- projects/common/models/unit.ts | 3 +- .../common/services/sanitization.service.ts | 1 - projects/common/shared.module.ts | 15 +- projects/common/util/element.factory.ts | 4 - .../canvas/overlays/canvas-element-overlay.ts | 13 +- .../element-properties-panel.component.html | 2 - .../drop-list-properties.component.ts | 9 +- .../editor/src/app/services/id.service.ts | 1 - .../editor/src/app/services/unit.service.ts | 8 +- .../drop-list-component-extension.ts | 11 +- .../drop-list-nodeview.component.ts | 6 +- projects/editor/src/assets/i18n/de.json | 3 +- ...ment-model-element-code-mapping.service.ts | 2 - 28 files changed, 316 insertions(+), 507 deletions(-) create mode 100644 example_data/droplist/dnd1.json create mode 100644 example_data/droplist/dnd2.json create mode 100644 example_data/droplist/dnd3.json create mode 100644 example_data/droplist/dnd4.json delete mode 100644 projects/common/components/compound-elements/cloze/cloze-child-elements/drop-list-simple.component.ts delete mode 100644 projects/common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple.ts diff --git a/docs/unit_definition_changelog.txt b/docs/unit_definition_changelog.txt index c7320369c..f1bcc976a 100644 --- a/docs/unit_definition_changelog.txt +++ b/docs/unit_definition_changelog.txt @@ -53,4 +53,6 @@ iqb-aspect-definition@1.0.0 +hasKeyboardIcon: boolean; - TextFieldElement: +hasKeyboardIcon: boolean; - TextFieldElement/TextFieldSimpleElement/SpellCorrectElement/TextAreaElement: InputAssistancePreset +'custom' - +inputAssistanceCustomKeys: string; + +3.9.0 +- remove drop-list-simple as cloze child; is now a drop-list diff --git a/example_data/droplist/dnd1.json b/example_data/droplist/dnd1.json new file mode 100644 index 000000000..075844082 --- /dev/null +++ b/example_data/droplist/dnd1.json @@ -0,0 +1 @@ +{"type":"aspect-unit-definition","pages":[{"sections":[{"elements":[{"width":180,"height":98,"type":"text","id":"text_1","text":"<p style=\"padding-left: 0px; text-indent: 0px; margin-bottom: 0px; margin-top: 0\" indentsize=\"20\">Normale Liste</p>","highlightableOrange":false,"highlightableTurquoise":false,"highlightableYellow":false,"hasSelectionPopup":true,"columnCount":1,"position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":false,"gridColumn":1,"gridColumnRange":1,"gridRow":1,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"transparent","lineHeight":135,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":180,"height":100,"type":"drop-list","id":"drop-list_1","label":"Beschriftung","value":[{"text":"aaa","imgSrc":null,"imgPosition":"above","id":"value_1"},{"text":"bbb","imgSrc":null,"imgPosition":"above","id":"value_2"},{"text":"ccc","imgSrc":null,"imgPosition":"above","id":"value_3"}],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"isSortList":false,"connectedTo":["drop-list_2","drop-list_3","drop-list_4"],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":true,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":180,"height":98,"type":"text","id":"text_2","text":"<p style=\"padding-left: 0px; text-indent: 0px; margin-bottom: 0px; margin-top: 0\" indentsize=\"20\">Liste 2</p>","highlightableOrange":false,"highlightableTurquoise":false,"highlightableYellow":false,"hasSelectionPopup":true,"columnCount":1,"position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":false,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"transparent","lineHeight":135,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":180,"height":100,"type":"drop-list","id":"drop-list_2","label":"Beschriftung","value":[{"text":"ddd","imgSrc":null,"imgPosition":"above","id":"value_4"},{"text":"eee","imgSrc":null,"imgPosition":"above","id":"value_5"},{"text":"fff","imgSrc":null,"imgPosition":"above","id":"value_6"}],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"sorting":false,"connectedTo":["drop-list_1","drop-list_3"],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":true,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":180,"height":98,"type":"text","id":"text_3","text":"<p style=\"padding-left: 0px; text-indent: 0px; margin-bottom: 0px; margin-top: 0\" indentsize=\"20\">Leere Liste</p>","highlightableOrange":false,"highlightableTurquoise":false,"highlightableYellow":false,"hasSelectionPopup":true,"columnCount":1,"position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":false,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"transparent","lineHeight":135,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":180,"height":100,"type":"drop-list","id":"drop-list_3","label":"Beschriftung","value":[],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"sorting":false,"connectedTo":["drop-list_1","drop-list_2"],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}],"height":400,"backgroundColor":"#ffffff","dynamicPositioning":true,"autoColumnSize":true,"autoRowSize":true,"gridColumnSizes":"1fr 1fr","gridRowSizes":"1fr","activeAfterID":null},{"elements":[{"width":180,"height":200,"type":"cloze","id":"cloze_1","document":{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":"left","indent":null,"indentSize":20,"hangingIndent":false,"margin":0},"content":[{"type":"text","text":"Lorem Ipsum"},{"type":"ToggleButton","attrs":{"model":{"width":100,"height":25,"type":"toggle-button","id":"toggle-button_1","label":"Beschriftung","value":null,"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"options":[],"strikeOtherOptions":false,"strikeSelectedOption":false,"verticalOrientation":false,"dynamicWidth":true,"styling":{"lineHeight":135,"selectionColor":"#c7f3d0","backgroundColor":"transparent","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}}},{"type":"DropList","attrs":{"model":{"width":150,"height":30,"type":"drop-list","id":"drop-list_4","label":"Beschriftung","value":[],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"sorting":false,"connectedTo":["drop-list_1"],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}}}]}]},"columnCount":1,"position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":false,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"lineHeight":150,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false,"backgroundColor":"#d3d3d3"}}],"height":400,"backgroundColor":"#ffffff","dynamicPositioning":true,"autoColumnSize":true,"autoRowSize":true,"gridColumnSizes":"1fr 1fr","gridRowSizes":"1fr","activeAfterID":null}],"hasMaxWidth":false,"maxWidth":900,"margin":30,"backgroundColor":"#ffffff","alwaysVisible":false,"alwaysVisiblePagePosition":"left","alwaysVisibleAspectRatio":50}],"version":"3.8.0"} diff --git a/example_data/droplist/dnd2.json b/example_data/droplist/dnd2.json new file mode 100644 index 000000000..c740cdceb --- /dev/null +++ b/example_data/droplist/dnd2.json @@ -0,0 +1 @@ +{"type":"aspect-unit-definition","pages":[{"sections":[{"elements":[{"width":180,"height":98,"type":"text","id":"text_1","text":"<p style=\"padding-left: 0px; text-indent: 0px; margin-bottom: 0px; margin-top: 0\" indentsize=\"20\">Sortierliste 1</p>","highlightableOrange":false,"highlightableTurquoise":false,"highlightableYellow":false,"hasSelectionPopup":true,"columnCount":1,"position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":false,"gridColumn":1,"gridColumnRange":1,"gridRow":1,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"transparent","lineHeight":135,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":180,"height":100,"type":"drop-list","id":"drop-list_1","label":"Beschriftung","value":[{"text":"aaa","imgSrc":null,"imgPosition":"above","id":"value_5"},{"text":"bbb","imgSrc":null,"imgPosition":"above","id":"value_6"},{"text":"ccc","imgSrc":null,"imgPosition":"above","id":"value_7"},{"text":"ddd","imgSrc":null,"imgPosition":"above","id":"value_8"}],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"isSortList":true,"connectedTo":[],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}],"height":400,"backgroundColor":"#ffffff","dynamicPositioning":true,"autoColumnSize":true,"autoRowSize":true,"gridColumnSizes":"1fr 1fr","gridRowSizes":"1fr","activeAfterID":null},{"elements":[{"width":180,"height":98,"type":"text","id":"text_2","text":"<p style=\"padding-left: 0px; text-indent: 0px; margin-bottom: 0px; margin-top: 0\" indent=\"0\" indentsize=\"20\">Sortierliste 2</p>","highlightableOrange":false,"highlightableTurquoise":false,"highlightableYellow":false,"hasSelectionPopup":true,"columnCount":1,"position":{"fixedSize":false,"dynamicPositioning":false,"xPosition":10,"yPosition":10,"useMinHeight":false,"gridColumn":1,"gridColumnRange":1,"gridRow":1,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"transparent","lineHeight":135,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":865,"height":367,"type":"drop-list","id":"drop-list_2","label":"Beschriftung","value":[{"text":"eee","imgSrc":null,"id":"value_1"},{"text":"fff","imgSrc":null,"id":"value_2"},{"text":"ggg","imgSrc":null,"id":"value_3"},{"text":"hhh","imgSrc":null,"id":"value_4"}],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"sorting":true,"connectedTo":[],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":false,"xPosition":10,"yPosition":10,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}],"height":400,"backgroundColor":"#ffffff","dynamicPositioning":false,"autoColumnSize":true,"autoRowSize":true,"gridColumnSizes":"1fr 1fr","gridRowSizes":"1fr","activeAfterID":null}],"hasMaxWidth":false,"maxWidth":900,"margin":30,"backgroundColor":"#ffffff","alwaysVisible":false,"alwaysVisiblePagePosition":"left","alwaysVisibleAspectRatio":50}],"version":"3.8.0"} diff --git a/example_data/droplist/dnd3.json b/example_data/droplist/dnd3.json new file mode 100644 index 000000000..ce6a8b62e --- /dev/null +++ b/example_data/droplist/dnd3.json @@ -0,0 +1 @@ +{"type":"aspect-unit-definition","pages":[{"sections":[{"elements":[{"width":180,"height":98,"type":"text","id":"text_1","text":"<p style=\"padding-left: 0px; text-indent: 0px; margin-bottom: 0px; margin-top: 0\" indentsize=\"20\">Sortierliste 1</p>","highlightableOrange":false,"highlightableTurquoise":false,"highlightableYellow":false,"hasSelectionPopup":true,"columnCount":1,"position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":false,"gridColumn":1,"gridColumnRange":1,"gridRow":1,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"transparent","lineHeight":135,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":180,"height":100,"type":"drop-list","id":"drop-list_1","label":"Beschriftung","value":[{"text":"aaa","imgSrc":null,"imgPosition":"above","id":"value_5"},{"text":"bbb","imgSrc":null,"imgPosition":"above","id":"value_6"},{"text":"ccc","imgSrc":null,"imgPosition":"above","id":"value_7"},{"text":"ddd","imgSrc":null,"imgPosition":"above","id":"value_8"}],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"isSortList":true,"connectedTo":[],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}],"height":400,"backgroundColor":"#ffffff","dynamicPositioning":true,"autoColumnSize":true,"autoRowSize":true,"gridColumnSizes":"1fr 1fr","gridRowSizes":"1fr","activeAfterID":null},{"elements":[{"width":180,"height":98,"type":"text","id":"text_2","text":"<p style=\"padding-left: 0px; text-indent: 0px; margin-bottom: 0px; margin-top: 0\" indent=\"0\" indentsize=\"20\">Sortierliste 2</p>","highlightableOrange":false,"highlightableTurquoise":false,"highlightableYellow":false,"hasSelectionPopup":true,"columnCount":1,"position":{"fixedSize":false,"dynamicPositioning":false,"xPosition":10,"yPosition":10,"useMinHeight":false,"gridColumn":1,"gridColumnRange":1,"gridRow":1,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"transparent","lineHeight":135,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":865,"height":367,"type":"drop-list","id":"drop-list_2","label":"Beschriftung","value":[],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"sorting":true,"connectedTo":[],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":false,"xPosition":10,"yPosition":10,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}],"height":400,"backgroundColor":"#ffffff","dynamicPositioning":false,"autoColumnSize":true,"autoRowSize":true,"gridColumnSizes":"1fr 1fr","gridRowSizes":"1fr","activeAfterID":null}],"hasMaxWidth":false,"maxWidth":900,"margin":30,"backgroundColor":"#ffffff","alwaysVisible":false,"alwaysVisiblePagePosition":"left","alwaysVisibleAspectRatio":50}],"version":"3.8.0"} diff --git a/example_data/droplist/dnd4.json b/example_data/droplist/dnd4.json new file mode 100644 index 000000000..0c20918be --- /dev/null +++ b/example_data/droplist/dnd4.json @@ -0,0 +1 @@ +{"type":"aspect-unit-definition","pages":[{"sections":[{"elements":[{"width":180,"height":100,"type":"drop-list","id":"drop-list_1","label":"Beschriftung","value":[{"text":"aaa","imgSrc":null,"imgPosition":"above","id":"value_1"},{"text":"bbb","imgSrc":null,"imgPosition":"above","id":"value_2"},{"text":"ccc","imgSrc":null,"imgPosition":"above","id":"value_3"}],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"isSortList":false,"connectedTo":["drop-list_2"],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":true,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}},{"width":180,"height":200,"type":"cloze","id":"cloze_1","document":{"type":"doc","content":[{"type":"paragraph","attrs":{"textAlign":"left","indent":null,"indentSize":20,"hangingIndent":false,"margin":0},"content":[{"type":"text","text":"Lorem Ipsum"},{"type":"DropList","attrs":{"model":{"width":150,"height":30,"type":"drop-list","id":"drop-list_2","label":"Beschriftung","value":[],"required":false,"requiredWarnMessage":"Eingabe erforderlich","readOnly":false,"onlyOneItem":false,"sorting":false,"connectedTo":["drop-list_1"],"copyOnDrop":false,"orientation":"vertical","highlightReceivingDropList":false,"highlightReceivingDropListColor":"#006064","styling":{"backgroundColor":"#f4f4f2","itemBackgroundColor":"#c9e0e0","fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false}}}}]}]},"columnCount":1,"position":{"fixedSize":false,"dynamicPositioning":true,"xPosition":0,"yPosition":0,"useMinHeight":false,"gridColumn":null,"gridColumnRange":1,"gridRow":null,"gridRowRange":1,"marginLeft":0,"marginRight":0,"marginTop":0,"marginBottom":0,"zIndex":0},"styling":{"lineHeight":150,"fontColor":"#000000","font":"Roboto","fontSize":20,"bold":false,"italic":false,"underline":false,"backgroundColor":"#d3d3d3"}}],"height":400,"backgroundColor":"#ffffff","dynamicPositioning":true,"autoColumnSize":true,"autoRowSize":true,"gridColumnSizes":"1fr 1fr","gridRowSizes":"1fr","activeAfterID":null}],"hasMaxWidth":false,"maxWidth":900,"margin":30,"backgroundColor":"#ffffff","alwaysVisible":false,"alwaysVisiblePagePosition":"left","alwaysVisibleAspectRatio":50}],"version":"3.8.0"} diff --git a/package.json b/package.json index 972fa676a..931a04627 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "config": { "player_version": "1.27.0", "editor_version": "1.34.0", - "unit_definition_version": "3.8.0" + "unit_definition_version": "3.9.0" }, "scripts": { "ng": "ng", diff --git a/projects/common/components/compound-elements/cloze/cloze-child-elements/drop-list-simple.component.ts b/projects/common/components/compound-elements/cloze/cloze-child-elements/drop-list-simple.component.ts deleted file mode 100644 index d1f4440ce..000000000 --- a/projects/common/components/compound-elements/cloze/cloze-child-elements/drop-list-simple.component.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { CdkDragDrop, CdkDragEnter, CdkDragStart } from '@angular/cdk/drag-drop/drag-events'; -import { - CdkDrag, CdkDropList, moveItemInArray, transferArrayItem -} from '@angular/cdk/drag-drop'; -import { FormElementComponent } from 'common/directives/form-element-component.directive'; -import { DropListSimpleElement } from - 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple'; -import { DragNDropValueObject } from 'common/models/elements/element'; -import { DropListComponent } from 'common/components/input-elements/drop-list.component'; - -@Component({ - selector: 'aspect-drop-list-simple', - template: ` - <div class="list-container"> - <!-- Border width is a workaround to enable/disable the Material cdk-drop-list-receiving class style.--> - <div class="list" - [class.errors]="elementFormControl.errors && elementFormControl.touched" - [class.dropList-highlight]="elementModel.highlightReceivingDropList" - [style.min-height.px]="elementModel.height" - [style.border-color]="elementModel.highlightReceivingDropListColor" - [style.border-width.px]="elementModel.highlightReceivingDropList ? 2 : 0" - [style.color]="elementModel.styling.fontColor" - [style.font-family]="elementModel.styling.font" - [style.font-size.px]="elementModel.styling.fontSize" - [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" - [style.font-style]="elementModel.styling.italic ? 'italic' : ''" - [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" - [style.backgroundColor]="elementModel.styling.backgroundColor" - [matTooltip]="elementFormControl.errors && elementFormControl.touched ? - (elementFormControl.errors | errorTransform: elementModel) : ''" - [matTooltipClass]="'error-tooltip'" - cdkDropList - [id]="elementModel.id" - [cdkDropListData]="this" - [cdkDropListConnectedTo]="elementModel.connectedTo" - [cdkDropListEnterPredicate]="onlyOneItemPredicate" - tabindex="0" - (focusout)="elementFormControl.markAsTouched()" - (cdkDropListDropped)="drop($event)"> - <ng-container *ngIf="!parentForm"> - <ng-container *ngFor="let dropListValueElement of $any(elementModel.value)"> - <ng-container [ngTemplateOutlet]="dropObject" - [ngTemplateOutletContext]="{ $implicit: dropListValueElement }"> - </ng-container> - </ng-container> - </ng-container> - - <ng-container *ngIf="parentForm"> - <ng-container *ngFor="let value of elementFormControl.value"> - <ng-container [ngTemplateOutlet]="dropObject" [ngTemplateOutletContext]="{ $implicit: value }"> - </ng-container> - </ng-container> - </ng-container> - - <ng-template #dropObject let-value> - <div class="item" - [style.background-color]="elementModel.styling.itemBackgroundColor" - cdkDrag [cdkDragData]="{ element: value }" - (cdkDragStarted)="dragStart($event)" (cdkDragEnded)="dragEnd()" (cdkDragEntered)="dragEnter($event)"> - <div *cdkDragPreview - [style.font-size.px]="elementModel.styling.fontSize" - [style.background-color]="elementModel.styling.itemBackgroundColor"> - {{value.text}} - </div> - <div class="drag-placeholder" *cdkDragPlaceholder - [class.drag-placeholder-border]="placeholderDimensions.width !== '0px'" - [style.height]="placeholderDimensions.height" - [style.width]="placeholderDimensions.width"> - </div> - {{value.text}} - </div> - </ng-template> - </div> - </div> - `, - styles: [ - '.list-container {display: flex; flex-direction: column; width: 100%; height: 100%;}', - '.list {width: 100%; height: 100%; border-radius: 5px; overflow: hidden}', - '.item {border-radius: 5px; padding: 0 5px; height: 100%; text-align: center;}', - '.item:not(:last-child) {margin-bottom: 5px;}', - '.item:active {cursor: grabbing}', - '.errors {box-sizing: border-box; border: 2px solid #f44336 !important;}', - '.error-message {font-size: 75%; margin-top: 10px;}', - '.cdk-drag-preview {padding: 8px 20px; border-radius: 5px; box-shadow: 2px 2px 5px black;}', - '.drag-placeholder {background-color: lightgrey; transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);}', - '.drag-placeholder-border {box-sizing: border-box; border: solid 3px #999; border-radius: 5px}', - '.cdk-drag-animating {transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);}', - - '.dropList-highlight.cdk-drop-list-receiving {border: solid;}', - '.dropList-highlight.cdk-drop-list-dragging {border: solid;}' - ] -}) -export class DropListSimpleComponent extends FormElementComponent { - @Input() elementModel!: DropListSimpleElement; - placeholderDimensions: { width: string, height: string } = { width: '1px', height: '1px' }; - - bodyElement: HTMLElement = document.body; - - dragStart(event: CdkDragStart<DropListSimpleComponent>): void { - this.setPlaceholderDimensions( - event.source.dropContainer.data.elementFormControl.value.length - 1, - event.source.dropContainer.data.elementModel.orientation - ); - this.bodyElement.classList.add('inheritCursors'); - this.bodyElement.style.cursor = 'grabbing'; - } - - dragEnd(): void { - this.bodyElement.classList.remove('inheritCursors'); - this.bodyElement.style.cursor = 'unset'; - } - - drop(event: CdkDragDrop<DropListSimpleComponent>): void { - if (!this.elementModel.readOnly) { - if (event.previousContainer === event.container) { - moveItemInArray(event.container.data.elementFormControl.value as unknown as DragNDropValueObject[], - event.previousIndex, - event.currentIndex); - this.elementFormControl.setValue( - (event.container.data.elementFormControl.value as DragNDropValueObject[]) - ); - } else { - transferArrayItem( - event.previousContainer.data.elementFormControl.value as unknown as DragNDropValueObject[], - event.container.data.elementFormControl.value as unknown as DragNDropValueObject[], - event.previousIndex, - event.currentIndex - ); - event.previousContainer.data.elementFormControl.setValue( - (event.previousContainer.data.elementFormControl.value as DragNDropValueObject[]) - ); - } - this.elementFormControl.setValue( - (event.container.data.elementFormControl.value as DragNDropValueObject[]) - ); - } - } - - dragEnter(event: CdkDragEnter<DropListSimpleComponent | DropListComponent, { element: DragNDropValueObject }>) { - const presentValueIDs = event.container.data.elementFormControl.value - .map((value: DragNDropValueObject) => value.id); - const itemCountOffset = presentValueIDs.includes(event.item.data.element.id) ? 1 : 0; - this.setPlaceholderDimensions( - presentValueIDs.length - itemCountOffset, - event.container.data.elementModel.orientation); - } - - setPlaceholderDimensions(itemsCount: number, orientation: unknown): void { - switch (orientation) { - case 'vertical': { - this.placeholderDimensions.width = '100%'; - this.placeholderDimensions.height = itemsCount ? '1px' : '100%'; - break; - } - case 'horizontal': { - this.placeholderDimensions.width = itemsCount ? '1px' : '100%'; - this.placeholderDimensions.height = '100%'; - break; - } - default: { // 'flex' - this.placeholderDimensions.width = itemsCount ? '0px' : '100%'; - this.placeholderDimensions.height = itemsCount ? '0px' : '100%'; - } - } - } - - onlyOneItemPredicate = (drag: CdkDrag, drop: CdkDropList): boolean => ( - drop.data.elementFormControl.value.length < 1 - ); -} diff --git a/projects/common/components/compound-elements/cloze/compound-child-overlay.component.ts b/projects/common/components/compound-elements/cloze/compound-child-overlay.component.ts index f41139468..4dd7a4f3a 100644 --- a/projects/common/components/compound-elements/cloze/compound-child-overlay.component.ts +++ b/projects/common/components/compound-elements/cloze/compound-child-overlay.component.ts @@ -6,7 +6,6 @@ import { FormGroup } from '@angular/forms'; import { ElementComponent } from '../../../directives/element-component.directive'; import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; import { TextFieldSimpleElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple'; -import { DropListSimpleElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple'; import { ValueChangeElement } from 'common/models/elements/element'; @Component({ @@ -21,13 +20,14 @@ import { ValueChangeElement } from 'common/models/elements/element'; [style.width.px]="element.width" [style.height.px]="element.height"> </aspect-text-field-simple> - <aspect-drop-list-simple *ngIf="element.type === 'drop-list-simple'" #childComponent + <aspect-drop-list *ngIf="element.type === 'drop-list'" #childComponent + [clozeContext]="true" [style.pointer-events]="editorMode ? 'none' : 'auto'" [parentForm]="parentForm" [elementModel]="$any(element)" [style.width.px]="element.width" [style.height.px]="element.height"> - </aspect-drop-list-simple> + </aspect-drop-list> <aspect-toggle-button *ngIf="element.type === 'toggle-button'" #childComponent [style.pointer-events]="editorMode ? 'none' : 'auto'" [parentForm]="parentForm" @@ -47,12 +47,12 @@ import { ValueChangeElement } from 'common/models/elements/element'; ':host {vertical-align: middle;}', ':host > div {border-radius: 3px;}', ':host div > * {display: inline-block; padding-bottom: 2px; box-sizing: border-box;}', - 'aspect-drop-list-simple, aspect-text-field-simple {width: 100%; height: 100%;}', - 'aspect-drop-list-simple {line-height: unset; vertical-align: top;}' + 'aspect-drop-list, aspect-text-field-simple {width: 100%; height: 100%;}', + 'aspect-drop-list {line-height: unset; vertical-align: top;}' ] }) export class CompoundChildOverlayComponent { // TODO rename to ClozeChildOverlay - @Input() element!: ToggleButtonElement | TextFieldSimpleElement | DropListSimpleElement; + @Input() element!: ToggleButtonElement | TextFieldSimpleElement; @Input() parentForm!: FormGroup; @Input() editorMode: boolean = false; @Input() lineHeight!: number; diff --git a/projects/common/components/input-elements/drop-list.component.ts b/projects/common/components/input-elements/drop-list.component.ts index ffb1cbc74..f4b74ec61 100644 --- a/projects/common/components/input-elements/drop-list.component.ts +++ b/projects/common/components/input-elements/drop-list.component.ts @@ -1,225 +1,254 @@ -import { Component, Input } from '@angular/core'; +// eslint-disable-next-line max-classes-per-file import { - CdkDragDrop, CdkDragEnter, CdkDragStart -} from '@angular/cdk/drag-drop/drag-events'; -import { - CdkDrag, CdkDropList, moveItemInArray -} from '@angular/cdk/drag-drop'; + AfterViewInit, + Component, ElementRef, Input, OnDestroy, OnInit, Pipe, PipeTransform, ViewChild +} from '@angular/core'; import { DropListElement } from 'common/models/elements/input-elements/drop-list'; import { DragNDropValueObject } from 'common/models/elements/element'; -import { - DropListSimpleComponent -} from 'common/components/compound-elements/cloze/cloze-child-elements/drop-list-simple.component'; import { FormElementComponent } from '../../directives/form-element-component.directive'; @Component({ selector: 'aspect-drop-list', template: ` - <div class="list-container" fxLayout="column"> - <!-- Border width is a workaround to enable/disable the Material cdk-drop-list-receiving--> - <!-- class style.--> - <!-- min-height for the following div is important for iOS 14! - iOS 14 is not able to determine the height of the flex container--> - <div class="list" - [ngClass]="{ 'align-flex' : elementModel.orientation === 'flex', 'copyOnDrop': elementModel.copyOnDrop }" - [class.errors]="elementFormControl.errors && elementFormControl.touched" - [style.min-height.px]="elementModel.position.useMinHeight ? elementModel.height - 6 : null" - [class.dropList-highlight]="elementModel.highlightReceivingDropList" - [style.outline-color]="elementModel.highlightReceivingDropListColor" - [style.color]="elementModel.styling.fontColor" - [style.font-family]="elementModel.styling.font" - [style.font-size.px]="elementModel.styling.fontSize" - [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" - [style.font-style]="elementModel.styling.italic ? 'italic' : ''" - [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" - [style.backgroundColor]="elementModel.styling.backgroundColor" - [style.display]="elementModel.orientation === 'horizontal' ? 'flex' : ''" - [style.flex-direction]="elementModel.orientation === 'horizontal' ? 'row' : ''" - cdkDropList - [id]="elementModel.id" - [cdkDropListData]="this" - [cdkDropListConnectedTo]="elementModel.connectedTo" - [cdkDropListOrientation]="elementModel.orientation !== 'flex' ? $any(elementModel.orientation) : ''" - [cdkDropListEnterPredicate]="onlyOneItemPredicate" - tabindex="0" - (focusout)="elementFormControl.markAsTouched()" - (cdkDropListDropped)="drop($event)"> - - <ng-container *ngIf="!parentForm"> - <ng-container *ngFor="let dropListValueElement of $any(elementModel.value); let index = index;"> - <ng-container [ngTemplateOutlet]="dropObject" - [ngTemplateOutletContext]="{ $implicit: dropListValueElement, index: index }"> - </ng-container> - </ng-container> - </ng-container> - - <ng-container *ngIf="parentForm"> - <ng-container *ngFor="let dropListValueElement of elementFormControl.value; let index = index;"> - <ng-container [ngTemplateOutlet]="dropObject" - [ngTemplateOutletContext]="{ $implicit: dropListValueElement, index: index }"> - </ng-container> - </ng-container> - </ng-container> - <!--Leave template within the dom to ensure dragNdrop--> - <ng-template #dropObject let-dropListValueElement let-index="index"> - <div class="item text-item" *ngIf="!dropListValueElement.imgSrc" - [ngClass]="{ 'vertical-orientation' : elementModel.orientation === 'vertical', - 'horizontal-orientation' : elementModel.orientation === 'horizontal'}" - [style.background-color]="elementModel.styling.itemBackgroundColor" - cdkDrag - [cdkDragData]="{ element: dropListValueElement, index: index }" - (cdkDragStarted)="dragStart(index, $event)" (cdkDragEnded)="dragEnd()" (cdkDragEntered)="dragEnter($event)"> - <div *cdkDragPreview - [style.font-size.px]="elementModel.styling.fontSize" - [style.background-color]="elementModel.styling.itemBackgroundColor"> - {{dropListValueElement.text}} - </div> - <div class="drag-placeholder" *cdkDragPlaceholder - [class.drag-placeholder-border]="placeholderDimensions.width !== '0px'" - [style.padding]="0" - [style.margin]="0" - [style.height]="placeholderDimensions.height" - [style.width]="placeholderDimensions.width"> - </div> - {{dropListValueElement.text}} - </div> - - <!-- actual placeholder when item is being dragged from copy-list --> - <div *ngIf="elementModel.copyOnDrop && draggedItemIndex === index" class="item text-item" - [style.font-size.px]="elementModel.styling.fontSize" - [ngClass]="{ 'vertical-orientation' : elementModel.orientation === 'vertical', - 'horizontal-orientation' : elementModel.orientation === 'horizontal'}" - [style.background-color]="elementModel.styling.itemBackgroundColor"> - {{dropListValueElement.text}} - </div> - - <img *ngIf="dropListValueElement.imgSrc" - [src]="dropListValueElement.imgSrc | safeResourceUrl" alt="Image Placeholder" - [style.display]="elementModel.orientation === 'flex' ? '' : 'block'" - class="item" - [ngClass]="{ 'vertical-orientation' : elementModel.orientation === 'vertical', - 'horizontal-orientation' : elementModel.orientation === 'horizontal'}" - cdkDrag [cdkDragData]="{ element: dropListValueElement, index: index }" - (cdkDragStarted)="dragStart(index, $event)" (cdkDragEnded)="dragEnd()" - [style.object-fit]="'scale-down'"> - <img *ngIf="elementModel.copyOnDrop && draggedItemIndex === index && dropListValueElement.imgSrc" - [src]="dropListValueElement.imgSrc | safeResourceUrl" alt="Image Placeholder" - [style.display]="elementModel.orientation === 'flex' ? '' : 'block'" - class="item" - [ngClass]="{ 'vertical-orientation' : elementModel.orientation === 'vertical', - 'horizontal-orientation' : elementModel.orientation === 'horizontal'}" - [style.object-fit]="'scale-down'"> - </ng-template> - </div> - <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched" - class="error-message"> - {{elementFormControl.errors | errorTransform: elementModel}} - </mat-error> + <div class="list" [id]="elementModel.id" + [fxLayout]="elementModel.orientation | droplistLayout" + [fxLayoutAlign]="elementModel.orientation | droplistLayoutAlign" + [ngClass]="{ 'vertical-orientation' : elementModel.orientation === 'vertical', + 'horizontal-orientation' : elementModel.orientation === 'horizontal', + 'clozeContext': clozeContext}" + [style.min-height.px]="elementModel.position?.useMinHeight ? elementModel.height : undefined" + [style.color]="elementModel.styling.fontColor" + [style.font-family]="elementModel.styling.font" + [style.font-size.px]="elementModel.styling.fontSize" + [style.font-weight]="elementModel.styling.bold ? 'bold' : ''" + [style.font-style]="elementModel.styling.italic ? 'italic' : ''" + [style.text-decoration]="elementModel.styling.underline ? 'underline' : ''" + [style.backgroundColor]="elementModel.styling.backgroundColor" + [class.errors]="elementFormControl.errors && elementFormControl.touched" + [style.outline-color]="elementModel.highlightReceivingDropListColor" + [class.highlight-valid-drop]="highlightValidDrop" + [class.highlight-as-receiver]="highlightAsReceiver" + (drop)="drop($event)" (dragenter)="dragEnterList($event)" (dragleave)="dragLeaveList($event)" + (dragover)="$event.preventDefault()"> + <ng-container *ngFor="let dropListValueElement of viewModel let index = index;"> + <div class="list-item" + draggable="true" + (dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($event)" + (dragenter)="dragEnterItem($event)" + [class.show-as-placeholder]="showAsPlaceholder && placeHolderIndex === index" + [class.show-as-hidden]="hidePlaceholder && placeHolderIndex === index" + [style.pointer-events]="dragging && elementModel.isSortList === false ? 'none' : ''" + [style.background-color]="elementModel.styling.itemBackgroundColor"> + <span>{{dropListValueElement.text}}</span> + </div> + <img *ngIf="dropListValueElement.imgSrc" + class="list-item" + [src]="dropListValueElement.imgSrc | safeResourceUrl" alt="Image Placeholder" + draggable="true" [id]="dropListValueElement.id" + (dragstart)="dragStart($event, dropListValueElement, index)" (dragend)="dragEnd($event)" + [style.object-fit]="'scale-down'"> + </ng-container> </div> + <mat-error *ngIf="elementFormControl.errors && elementFormControl.touched" + class="error-message"> + {{elementFormControl.errors | errorTransform: elementModel}} + </mat-error> `, styles: [ - '.list-container {width: 100%; height: 100%;}', - '.list {border-radius: 5px; width: calc(100% - 6px); overflow: hidden}', - '.list {height: calc(100% - 6px); margin-top: 3px; margin-left: 3px;}', - '.text-item {border-radius: 5px; padding: 10px;}', - '.item {cursor: grab}', - '.item:active {cursor: grabbing}', - '.copyOnDrop .item {transform: none !important}', - '.vertical-orientation.item:not(:last-child) {margin-bottom: 5px;}', - '.horizontal-orientation.item:not(:last-child) {margin-right: 5px}', + '.list {width: 100%; height: 100%; background-color: rgb(244, 244, 242); padding: 3px;}', + ':not(.clozeContext) .list-item {border-radius: 5px; padding: 10px;}', + '.vertical-orientation .list-item:not(:last-child) {margin-bottom: 5px;}', + '.horizontal-orientation .list-item:not(:last-child) {margin-right: 5px;}', '.errors {outline: 2px solid #f44336 !important;}', - '.error-message {font-size: 75%; margin-top: 10px;}', - '.cdk-drag-preview {padding: 8px 20px; border-radius: 5px; z-index: 5; box-shadow: 2px 2px 5px black;}', - '.drag-placeholder-border {box-sizing: border-box; border: solid 3px #999; border-radius: 5px}', - '.drag-placeholder {background-color: lightgrey; transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);}', - '.cdk-drag-animating {transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);}', - '.dropList-highlight.cdk-drop-list-receiving {outline: solid;}', - '.dropList-highlight.cdk-drop-list-dragging {outline: solid;}', - '.align-flex {flex: 1 1 auto; flex-flow: row wrap; display: flex; place-content: center space-around; gap: 10px}', - ':host .copyOnDrop .cdk-drag-placeholder {position: relative; visibility: hidden;}', - ':host .copyOnDrop .cdk-drag-placeholder {height: 0 !important; min-height: 0 !important;}', - ':host .copyOnDrop .cdk-drag-placeholder {margin: 0 !important; padding: 0 !important; border: 0;}' + '.list-item {cursor: grab;}', + '.list-item:active {cursor: grabbing}', + '.show-as-placeholder {opacity: 0.5 !important; pointer-events: none;}', + '.highlight-valid-drop {background-color: lightblue !important;}', + '.highlight-as-receiver {outline: 2px solid;}', + '.show-as-hidden {visibility: hidden;}' ] }) -export class DropListComponent extends FormElementComponent { +export class DropListComponent extends FormElementComponent implements OnInit, AfterViewInit, OnDestroy { @Input() elementModel!: DropListElement; + @Input() clozeContext: boolean = false; + @ViewChild('placeholder') placeholder!: ElementRef<HTMLElement>; + static dragAndDropComponents: { [id: string]: DropListComponent } = {}; + + viewModel: DragNDropValueObject[] = []; + placeHolderIndex?: number; + highlightAsReceiver = false; + + dragging = false; + + showAsPlaceholder = false; + hidePlaceholder = false; + highlightValidDrop = false; + + static draggedElement?: DragNDropValueObject; + static sourceList?: DropListComponent; - bodyElement: HTMLElement = document.body; - draggedItemIndex: number | null = null; - placeholderDimensions: { width: string, height: string } = { width: '1px', height: '1px' }; - - dragStart(itemIndex: number, event: CdkDragStart<DropListComponent>): void { - this.setPlaceholderDimensions( - event.source.dropContainer.data.elementFormControl.value.length - 1, - event.source.dropContainer.data.elementModel.orientation - ); - this.draggedItemIndex = itemIndex; - this.bodyElement.classList.add('inheritCursors'); - this.bodyElement.style.cursor = 'grabbing'; + ngOnInit() { + super.ngOnInit(); + this.viewModel = [...this.elementFormControl.value]; } - dragEnd(): void { - this.draggedItemIndex = null; - this.bodyElement.classList.remove('inheritCursors'); - this.bodyElement.style.cursor = 'unset'; + ngAfterViewInit() { + DropListComponent.dragAndDropComponents[this.elementModel.id] = this; } - drop(event: CdkDragDrop<DropListComponent>): void { - if (event.previousContainer === event.container && !event.container.data.elementModel.copyOnDrop) { - moveItemInArray( - event.container.data.elementFormControl.value as unknown as DragNDropValueObject[], - event.previousIndex, - event.currentIndex - ); - this.elementFormControl.setValue( - (event.container.data.elementFormControl.value as DragNDropValueObject[]) - ); + dragStart(dragEvent: DragEvent, + dropListValueElement: DragNDropValueObject, + sourceListIndex: number) { + if (dragEvent.dataTransfer) { + dragEvent.dataTransfer.effectAllowed = 'copyMove'; + dragEvent.dataTransfer.setDragImage( + DropListComponent.createDragImage(dragEvent.target as Node, dropListValueElement.id), 0, 0); + } + + DropListComponent.draggedElement = dropListValueElement; + DropListComponent.sourceList = this; + this.placeHolderIndex = sourceListIndex; + if (this.elementModel.isSortList) { + this.showAsPlaceholder = true; } else { - const presentValueIDs = event.container.data.elementFormControl.value - .map((value2: DragNDropValueObject) => value2.id); - if (!presentValueIDs.includes(event.item.data.element.id)) { - event.container.data.elementFormControl.value.splice(event.currentIndex, 0, event.item.data.element); - event.container.data.elementFormControl - .setValue(event.container.data.elementFormControl.value); - } - if (!event.previousContainer.data.elementModel.copyOnDrop) { - event.previousContainer.data.elementFormControl.value.splice(event.item.data.index, 1); - event.previousContainer.data.elementFormControl.setValue( - (event.previousContainer.data.elementFormControl.value as DragNDropValueObject[]) - ); + this.hidePlaceholder = true; + this.highlightValidDrop = true; + } + + Object.entries(DropListComponent.dragAndDropComponents) + .forEach(([, value]) => { + value.dragging = true; + }); + + if (this.elementModel.highlightReceivingDropList) { + this.highlightAsReceiver = true; + this.elementModel.connectedTo.forEach(connectedDropListID => { + DropListComponent.dragAndDropComponents[connectedDropListID].highlightAsReceiver = true; + }); + } + } + + static createDragImage(baseElement: Node, baseID: string): HTMLElement { + const dragImage: HTMLElement = baseElement.cloneNode(true) as HTMLElement; + dragImage.id = `${baseID}-dragimage`; + dragImage.style.display = 'inline-block'; + document.body.appendChild(dragImage); + return dragImage; + } + + dragEnterItem(event: DragEvent) { + event.preventDefault(); + + if (this.elementModel.isSortList && DropListComponent.sourceList === this) { + const sourceIndex: number = this.placeHolderIndex as number; + const targetIndex: number = Array.from((event.target as any).parentNode.children).indexOf(event.target); + const removedElement = this.viewModel.splice(sourceIndex, 1)[0]; + this.viewModel.splice(targetIndex, 0, removedElement); + this.placeHolderIndex = targetIndex; + } + } + + dragEnterList(event: DragEvent) { + event.preventDefault(); + + if (!this.elementModel.isSortList) { + this.highlightValidDrop = true; + } else if (DropListComponent.sourceList !== this) { + this.viewModel.push(DropListComponent.draggedElement as DragNDropValueObject); + const sourceList = DropListComponent.sourceList as DropListComponent; + sourceList.viewModel.splice(sourceList.placeHolderIndex as number, 1); + sourceList.elementFormControl.setValue(sourceList.viewModel); + sourceList.placeHolderIndex = undefined; + DropListComponent.sourceList = this; + this.placeHolderIndex = this.viewModel.length > 0 ? this.viewModel.length - 1 : 0; + } + } + + dragLeaveList(event: DragEvent) { + event.preventDefault(); + this.highlightValidDrop = false; + } + + drop(event: DragEvent) { + event.preventDefault(); + + if (DropListComponent.sourceList === this && this.elementModel.isSortList) { + this.elementFormControl.setValue(this.viewModel); + } else if (this.isDropAllowed((DropListComponent.sourceList as DropListComponent).elementModel.connectedTo)) { + const presentValueIDs = this.elementFormControl.value + .map((valueValue: DragNDropValueObject) => valueValue.id); + if (!presentValueIDs.includes(DropListComponent.draggedElement?.id)) { + this.viewModel.push(DropListComponent.draggedElement as DragNDropValueObject); + this.elementFormControl.setValue(this.viewModel); + if (!DropListComponent.sourceList?.elementModel.copyOnDrop) { + DropListComponent.sourceList?.viewModel.splice(this.placeHolderIndex as number, 1); + DropListComponent.sourceList?.elementFormControl.setValue(DropListComponent.sourceList.viewModel); + } } + } else { + console.log('Not an allowed target list'); } + this.dragEnd(); + } + + isDropAllowed(connectedDropLists: string[]): boolean { + return (connectedDropLists as string[]).includes(this.elementModel.id) && + !(this.elementModel.onlyOneItem && this.elementModel.value.length > 0); + // TODO presentValueIDs? + } + + dragEnd(event?: DragEvent) { + event?.preventDefault(); + + Object.entries(DropListComponent.dragAndDropComponents) + .forEach(([, value]) => { + value.highlightAsReceiver = false; + value.dragging = false; + value.highlightValidDrop = false; + }); + if (DropListComponent.sourceList) DropListComponent.sourceList.placeHolderIndex = undefined; + this.placeHolderIndex = undefined; + + document.getElementById(`${DropListComponent.draggedElement?.id}-dragimage`)?.remove(); } - dragEnter(event: CdkDragEnter<DropListSimpleComponent | DropListComponent, { element: DragNDropValueObject }>) { - const presentValueIDs = event.container.data.elementFormControl.value - .map((value: DragNDropValueObject) => value.id); - const itemCountOffset = presentValueIDs.includes(event.item.data.element.id) ? 1 : 0; - this.setPlaceholderDimensions( - presentValueIDs.length - itemCountOffset, - event.container.data.elementModel.orientation); + ngOnDestroy(): void { + delete DropListComponent.dragAndDropComponents[this.elementModel.id]; } +} - setPlaceholderDimensions(itemsCount: number, orientation: unknown): void { +@Pipe({ + name: 'droplistLayout' +}) +export class DropListLayoutPipe implements PipeTransform { + transform(orientation: string): string { switch (orientation) { - case 'vertical': { - this.placeholderDimensions.width = '100%'; - this.placeholderDimensions.height = itemsCount ? '1px' : '100%'; - break; - } - case 'horizontal': { - this.placeholderDimensions.width = itemsCount ? '1px' : '100%'; - this.placeholderDimensions.height = '100%'; - break; - } - default: { // 'flex' - this.placeholderDimensions.width = itemsCount ? '0px' : '100%'; - this.placeholderDimensions.height = itemsCount ? '0px' : '100%'; - } + case 'horizontal': + return 'row'; + case 'vertical': + return 'column'; + case 'flex': + return 'row wrap'; + default: + throw Error(`droplist orientation invalid: ${orientation}`); } } +} - onlyOneItemPredicate = (drag: CdkDrag, drop: CdkDropList): boolean => ( - !drop.data.elementModel.onlyOneItem || drop.data.elementFormControl.value.length < 1 - ); +@Pipe({ + name: 'droplistLayoutAlign' +}) +export class DropListLayoutAlignPipe implements PipeTransform { + transform(orientation: string): string { + switch (orientation) { + case 'horizontal': + return 'start start'; + case 'vertical': + return 'start stretch'; + case 'flex': + return 'space-around center'; + default: + throw Error(`droplist orientation invalid: ${orientation}`); + } + } } diff --git a/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple.ts b/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple.ts deleted file mode 100644 index 7b7022b7b..000000000 --- a/projects/common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - BasicStyles, DragNDropValueObject, InputElement, AnswerScheme, - AnswerSchemeValue, UIElement -} from 'common/models/elements/element'; -import { Type } from '@angular/core'; -import { ElementComponent } from 'common/directives/element-component.directive'; -import { - DropListSimpleComponent -} from 'common/components/compound-elements/cloze/cloze-child-elements/drop-list-simple.component'; -import { DropListElement } from 'common/models/elements/input-elements/drop-list'; - -export class DropListSimpleElement extends InputElement { - value: DragNDropValueObject[] = []; - connectedTo: string[] = []; - copyOnDrop: boolean = false; - highlightReceivingDropList: boolean = false; - highlightReceivingDropListColor: string = '#006064'; - styling: BasicStyles & { - itemBackgroundColor: string; - }; - - constructor(element: Partial<DropListSimpleElement>) { - super({ width: 150, height: 30, ...element }); - this.value = element.value || []; - if (element.connectedTo) this.connectedTo = element.connectedTo; - if (element.copyOnDrop) this.copyOnDrop = element.copyOnDrop; - if (element.highlightReceivingDropList) this.highlightReceivingDropList = element.highlightReceivingDropList; - if (element.highlightReceivingDropListColor) { - this.highlightReceivingDropListColor = element.highlightReceivingDropListColor; - } - this.styling = { - ...UIElement.initStylingProps({ - backgroundColor: '#f4f4f2', - itemBackgroundColor: '#c9e0e0', - ...element.styling - }) - }; - } - - hasAnswerScheme(): boolean { - return Boolean(this.getAnswerScheme); - } - - getAnswerScheme(dropLists: Array<DropListElement | DropListSimpleElement>): AnswerScheme { - return { - id: this.id, - type: 'string', - format: '', - multiple: true, - nullable: false, - values: this.getAnswerSchemeValues(dropLists), - valuesComplete: true - }; - } - - getAnswerSchemeValues(dropLists: Array<DropListElement | DropListSimpleElement>): AnswerSchemeValue[] { - const valueDropLists = dropLists.filter(dropList => dropList.connectedTo.includes(this.id)); - return [this, ...valueDropLists] - .map(dropList => dropList.value as DragNDropValueObject[]) - .flat() - .map(option => ({ value: option.id, label: option.text as string })); - } - - getElementComponent(): Type<ElementComponent> { - return DropListSimpleComponent; - } -} diff --git a/projects/common/models/elements/compound-elements/cloze/cloze.ts b/projects/common/models/elements/compound-elements/cloze/cloze.ts index fa9bf59ca..eaa8a05bc 100644 --- a/projects/common/models/elements/compound-elements/cloze/cloze.ts +++ b/projects/common/models/elements/compound-elements/cloze/cloze.ts @@ -4,7 +4,7 @@ import { InputElement, PositionedUIElement, PositionProperties, - UIElement, UIElementValue + UIElement, UIElementType, UIElementValue } from 'common/models/elements/element'; import { Type } from '@angular/core'; import { ElementComponent } from 'common/directives/element-component.directive'; @@ -12,11 +12,9 @@ import { ClozeComponent } from 'common/components/compound-elements/cloze/cloze. import { TextFieldSimpleElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple'; -import { - DropListSimpleElement -} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple'; import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; import { ButtonElement } from 'common/models/elements/button/button'; +import { DropListElement } from 'common/models/elements/input-elements/drop-list'; export class ClozeElement extends CompoundElement implements PositionedUIElement { document: ClozeDocument = { type: 'doc', content: [] }; @@ -145,8 +143,9 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement case 'text-field-simple': newElement = new TextFieldSimpleElement(elementModel as TextFieldSimpleElement); break; - case 'drop-list-simple': - newElement = new DropListSimpleElement(elementModel as DropListSimpleElement); + case 'drop-list': + case 'drop-list-simple' as UIElementType: // keep here for compatibility + newElement = new DropListElement({ ...elementModel as DropListElement, type: 'drop-list' }); break; case 'toggle-button': newElement = new ToggleButtonElement(elementModel as ToggleButtonElement); diff --git a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/drop-list.ts b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/drop-list.ts index 2ca23e9bb..f546d6b28 100644 --- a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/drop-list.ts +++ b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/drop-list.ts @@ -1,7 +1,5 @@ import { Node, mergeAttributes } from '@tiptap/core'; -import { - DropListSimpleElement -} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple'; +import { DropListElement } from 'common/models/elements/input-elements/drop-list'; const DropListExtension = Node.create({ @@ -12,7 +10,7 @@ const DropListExtension = addAttributes() { return { model: { - default: new DropListSimpleElement({ type: 'drop-list-simple' }) + default: new DropListElement({ type: 'drop-list' }) } }; }, diff --git a/projects/common/models/elements/element.ts b/projects/common/models/elements/element.ts index 23deb30da..3ce40b183 100644 --- a/projects/common/models/elements/element.ts +++ b/projects/common/models/elements/element.ts @@ -6,7 +6,7 @@ import { LikertRowElement } from 'common/models/elements/compound-elements/liker 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' -| 'drop-list' | 'drop-list-simple' | 'cloze' | 'spell-correct' | 'slider' | 'frame' | 'toggle-button' | 'geometry'; +| 'drop-list' | 'cloze' | 'spell-correct' | 'slider' | 'frame' | 'toggle-button' | 'geometry'; export type UIElementValue = string | number | boolean | undefined | UIElementType | InputElementValue | TextLabel | TextLabel[] | ClozeDocument | LikertRowElement[] | Hotspot[] | @@ -34,7 +34,12 @@ export abstract class UIElement { } setProperty(property: string, value: UIElementValue): void { - this[property] = value; + if (Array.isArray(this[property])) { // keep array reference intact + (this[property] as UIElementValue[]) + .splice(0, (this[property] as UIElementValue[]).length, ...(value as UIElementValue[])); + } else { + this[property] = value; + } } setStyleProperty(property: string, value: UIElementValue): void { diff --git a/projects/common/models/elements/input-elements/drop-list.ts b/projects/common/models/elements/input-elements/drop-list.ts index 0c33e5099..883ecbc67 100644 --- a/projects/common/models/elements/input-elements/drop-list.ts +++ b/projects/common/models/elements/input-elements/drop-list.ts @@ -1,25 +1,23 @@ import { Type } from '@angular/core'; import { - InputElement, PositionedUIElement, + InputElement, DragNDropValueObject, BasicStyles, PositionProperties, AnswerScheme, AnswerSchemeValue, UIElement } from 'common/models/elements/element'; import { ElementComponent } from 'common/directives/element-component.directive'; import { DropListComponent } from 'common/components/input-elements/drop-list.component'; -import { - DropListSimpleElement -} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple'; -export class DropListElement extends InputElement implements PositionedUIElement { +export class DropListElement extends InputElement { value: DragNDropValueObject[]; onlyOneItem: boolean = false; + isSortList: boolean = false; connectedTo: string[] = []; copyOnDrop: boolean = false; orientation: 'vertical' | 'horizontal' | 'flex' = 'vertical'; highlightReceivingDropList: boolean = false; highlightReceivingDropListColor: string = '#006064'; - position: PositionProperties; + position: PositionProperties | undefined; styling: BasicStyles & { itemBackgroundColor: string; }; @@ -28,6 +26,7 @@ export class DropListElement extends InputElement implements PositionedUIElement super({ height: 100, ...element }); this.value = element.value !== undefined ? [...element.value] : []; if (element.onlyOneItem) this.onlyOneItem = element.onlyOneItem; + if (element.isSortList) this.isSortList = element.isSortList; if (element.connectedTo) this.connectedTo = element.connectedTo; if (element.copyOnDrop) this.copyOnDrop = element.copyOnDrop; if (element.orientation) this.orientation = element.orientation; @@ -35,21 +34,21 @@ export class DropListElement extends InputElement implements PositionedUIElement if (element.highlightReceivingDropListColor) { this.highlightReceivingDropListColor = element.highlightReceivingDropListColor; } - this.position = UIElement.initPositionProps({ useMinHeight: true, ...element.position }); - this.styling = { - ...UIElement.initStylingProps({ - backgroundColor: '#f4f4f2', - itemBackgroundColor: '#c9e0e0', - ...element.styling - }) - }; + this.position = element.position ? + UIElement.initPositionProps({ useMinHeight: true, ...element.position as Partial<PositionProperties> }) : + undefined; + this.styling = UIElement.initStylingProps({ + backgroundColor: '#f4f4f2', + itemBackgroundColor: '#c9e0e0', + ...element.styling + }); } hasAnswerScheme(): boolean { return Boolean(this.getAnswerScheme); } - getAnswerScheme(options: Array<DropListElement | DropListSimpleElement>): AnswerScheme { + getAnswerScheme(options: Array<DropListElement>): AnswerScheme { return { id: this.id, type: 'string', @@ -61,7 +60,7 @@ export class DropListElement extends InputElement implements PositionedUIElement }; } - private getAnswerSchemeValues(dropLists: Array<DropListElement | DropListSimpleElement>): AnswerSchemeValue[] { + private getAnswerSchemeValues(dropLists: Array<DropListElement>): AnswerSchemeValue[] { const valueDropLists = dropLists.filter(dropList => dropList.connectedTo.includes(this.id)); if (valueDropLists.length || this.isSortingList()) { return [this, ...valueDropLists] diff --git a/projects/common/models/section.ts b/projects/common/models/section.ts index a50f7f180..fffe89e51 100644 --- a/projects/common/models/section.ts +++ b/projects/common/models/section.ts @@ -27,12 +27,11 @@ export class Section { if (section?.gridRowSizes !== undefined) this.gridRowSizes = section.gridRowSizes; if (section?.activeAfterID) this.activeAfterID = section.activeAfterID; this.elements = - section?.elements?.map(element => ( - ElementFactory.createElement({ - ...element, - position: UIElement.initPositionProps(element.position) - }) as PositionedUIElement) - ) || []; + section?.elements?.map(element => ElementFactory.createElement({ + ...element, + position: UIElement.initPositionProps(element.position) + }) as PositionedUIElement) || + []; } setProperty(property: string, value: UIElementValue): void { @@ -58,7 +57,7 @@ export class Section { getAnswerScheme(dropLists: UIElement[]): AnswerScheme[] { return this.getAllElements() .filter(element => element.hasAnswerScheme()) - .map(element => ((element.type === 'drop-list' || element.type === 'drop-list-simple') ? + .map(element => ((element.type === 'drop-list') ? (element as InputElement).getAnswerScheme(dropLists) : (element as InputElement | PlayerElement | TextElement | ImageElement).getAnswerScheme())); } diff --git a/projects/common/models/unit.ts b/projects/common/models/unit.ts index 0907972d7..de374d1c2 100644 --- a/projects/common/models/unit.ts +++ b/projects/common/models/unit.ts @@ -18,8 +18,7 @@ export class Unit { getAnswerScheme(): AnswerScheme[] { const dropLists = [ - ...this.getAllElements('drop-list'), - ...this.getAllElements('drop-list-simple') + ...this.getAllElements('drop-list') ]; return this.pages.map(page => page.getAnswerScheme(dropLists)).flat(); } diff --git a/projects/common/services/sanitization.service.ts b/projects/common/services/sanitization.service.ts index 617aa16c6..e4af0b49f 100644 --- a/projects/common/services/sanitization.service.ts +++ b/projects/common/services/sanitization.service.ts @@ -311,7 +311,6 @@ export class SanitizationService { // repair child element types childElements.forEach(childElement => { childElement.type = childElement.type === 'text-field' ? 'text-field-simple' : childElement.type; - childElement.type = childElement.type === 'drop-list' ? 'drop-list-simple' : childElement.type; }); return { diff --git a/projects/common/shared.module.ts b/projects/common/shared.module.ts index d17113c90..ecd0d1b83 100644 --- a/projects/common/shared.module.ts +++ b/projects/common/shared.module.ts @@ -50,13 +50,14 @@ import { } from './components/compound-elements/likert/likert-radio-button-group.component'; import { ImageMagnifierComponent } from './components/media-elements/image-magnifier.component'; import { RadioGroupImagesComponent } from './components/input-elements/radio-group-images.component'; -import { DropListComponent } from './components/input-elements/drop-list.component'; +import { + DropListComponent, + DropListLayoutAlignPipe, + DropListLayoutPipe +} from './components/input-elements/drop-list.component'; import { ClozeComponent } from './components/compound-elements/cloze/cloze.component'; import { SliderComponent } from './components/input-elements/slider.component'; import { SpellCorrectComponent } from './components/input-elements/spell-correct.component'; -import { - DropListSimpleComponent -} from './components/compound-elements/cloze/cloze-child-elements/drop-list-simple.component'; import { FrameComponent } from './components/frame/frame.component'; import { ToggleButtonComponent @@ -119,7 +120,6 @@ import { UpdateTextareaRowsPipe } from './pipes/update-textarea-rows.pipe'; DropListComponent, ClozeComponent, HotspotImageComponent, - DropListSimpleComponent, SliderComponent, SpellCorrectComponent, FrameComponent, @@ -134,7 +134,9 @@ import { UpdateTextareaRowsPipe } from './pipes/update-textarea-rows.pipe'; MathAtanPipe, MathDegreesPipe, ArrayIncludesPipe, - UpdateTextareaRowsPipe + UpdateTextareaRowsPipe, + DropListLayoutPipe, + DropListLayoutAlignPipe ], exports: [ CommonModule, @@ -155,7 +157,6 @@ import { UpdateTextareaRowsPipe } from './pipes/update-textarea-rows.pipe'; ToggleButtonComponent, TextFieldComponent, TextFieldSimpleComponent, - DropListSimpleComponent, TextAreaComponent, AudioComponent, VideoComponent, diff --git a/projects/common/util/element.factory.ts b/projects/common/util/element.factory.ts index 73a161461..68a258965 100644 --- a/projects/common/util/element.factory.ts +++ b/projects/common/util/element.factory.ts @@ -16,9 +16,6 @@ import { VideoElement } from 'common/models/elements/media-elements/video'; import { LikertElement } from 'common/models/elements/compound-elements/likert/likert'; import { RadioButtonGroupComplexElement } from 'common/models/elements/input-elements/radio-button-group-complex'; import { DropListElement } from 'common/models/elements/input-elements/drop-list'; -import { - DropListSimpleElement -} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple'; import { ClozeElement } from 'common/models/elements/compound-elements/cloze/cloze'; import { SliderElement } from 'common/models/elements/input-elements/slider'; import { SpellCorrectElement } from 'common/models/elements/input-elements/spell-correct'; @@ -43,7 +40,6 @@ export abstract class ElementFactory { likert: LikertElement, 'radio-group-images': RadioButtonGroupComplexElement, 'drop-list': DropListElement, - 'drop-list-simple': DropListSimpleElement, cloze: ClozeElement, slider: SliderElement, 'spell-correct': SpellCorrectElement, diff --git a/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts b/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts index 9a64e1c46..e7a89a152 100644 --- a/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts +++ b/projects/editor/src/app/components/canvas/overlays/canvas-element-overlay.ts @@ -9,11 +9,14 @@ import { CompoundElementComponent } from 'common/directives/compound-element.dir import { ClozeComponent } from 'common/components/compound-elements/cloze/cloze.component'; import { CompoundChildOverlayComponent } from 'common/components/compound-elements/cloze/compound-child-overlay.component'; -import { UIElement } from 'common/models/elements/element'; +import { DragNDropValueObject, UIElement } from 'common/models/elements/element'; import { GeometryComponent } from 'common/components/geometry/geometry.component'; import { GeometryElement } from 'common/models/elements/geometry/geometry'; import { UnitService } from '../../../services/unit.service'; import { SelectionService } from '../../../services/selection.service'; +import { DropListComponent } from 'common/components/input-elements/drop-list.component'; +import { DropListElement } from 'common/models/elements/input-elements/drop-list'; +import { FormElementComponent } from 'common/directives/form-element-component.directive'; @Directive() export abstract class CanvasElementOverlay implements OnInit, OnDestroy { @@ -34,6 +37,14 @@ export abstract class CanvasElementOverlay implements OnInit, OnDestroy { ngOnInit(): void { this.childComponent = this.elementContainer.createComponent(this.element.getElementComponent()); this.childComponent.instance.elementModel = this.element; + this.childComponent.changeDetectorRef.detectChanges(); // this fires onInit, which initializes the FormControl + if (this.childComponent.instance instanceof FormElementComponent) { + (this.childComponent.instance as FormElementComponent).elementFormControl.setValue(this.element.value); + } + // DropList keeps a special viewModel variable, which needs to be updated + if (this.childComponent.instance instanceof DropListComponent) { + (this.childComponent.instance as DropListComponent).viewModel = this.element.value as DragNDropValueObject[]; + } // Make children not clickable. This way the only relevant events are managed by the overlay. this.childComponent.location.nativeElement.style.pointerEvents = 'none'; diff --git a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.html b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.html index d7a996b46..c5356915f 100644 --- a/projects/editor/src/app/components/properties-panel/element-properties-panel.component.html +++ b/projects/editor/src/app/components/properties-panel/element-properties-panel.component.html @@ -48,13 +48,11 @@ <mat-divider></mat-divider> <button mat-raised-button [disabled]="selectedElements.length > 1 || combinedProperties.type == 'toggle-button' || - combinedProperties.type == 'drop-list-simple' || combinedProperties.type == 'text-field-simple'" (click)="duplicateElement()"> {{'propertiesPanel.duplicateElement' | translate }} </button> <button mat-raised-button color="warn" [disabled]="combinedProperties.type == 'toggle-button' || - combinedProperties.type == 'drop-list-simple' || combinedProperties.type == 'text-field-simple'" (click)="deleteElement()"> {{'propertiesPanel.deleteElement' | translate }} diff --git a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/drop-list-properties.component.ts b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/drop-list-properties.component.ts index eb49caada..6655ad5a0 100644 --- a/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/drop-list-properties.component.ts +++ b/projects/editor/src/app/components/properties-panel/model-properties-tab/input-groups/drop-list-properties.component.ts @@ -14,8 +14,7 @@ import { IDService } from 'editor/src/app/services/id.service'; @Component({ selector: 'aspect-drop-list-properties', template: ` - <div *ngIf="combinedProperties.type === 'drop-list' || - combinedProperties.type === 'drop-list-simple'" + <div *ngIf="combinedProperties.type === 'drop-list'" fxLayout="column"> <aspect-option-list-panel [title]="'preset'" [textFieldLabel]="'Neue Option'" [itemList]="$any(combinedProperties.value)" @@ -52,6 +51,12 @@ import { IDService } from 'editor/src/app/services/id.service'; </mat-select> </mat-form-field> + <mat-checkbox *ngIf="combinedProperties.isSortList !== undefined" + [checked]="$any(combinedProperties.isSortList)" + (change)="updateModel.emit({ property: 'isSortList', value: $event.checked })"> + {{'propertiesPanel.isSortList' | translate }} + </mat-checkbox> + <mat-checkbox *ngIf="combinedProperties.onlyOneItem !== undefined" [checked]="$any(combinedProperties.onlyOneItem)" (change)="updateModel.emit({ property: 'onlyOneItem', value: $event.checked })"> diff --git a/projects/editor/src/app/services/id.service.ts b/projects/editor/src/app/services/id.service.ts index d96646ed7..35ded1c1b 100644 --- a/projects/editor/src/app/services/id.service.ts +++ b/projects/editor/src/app/services/id.service.ts @@ -28,7 +28,6 @@ export class IDService { 'spell-correct': 0, 'radio-group-images': 0, 'drop-list': 0, - 'drop-list-simple': 0, cloze: 0, frame: 0, 'toggle-button': 0, diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts index 10aae508d..8835e0340 100644 --- a/projects/editor/src/app/services/unit.service.ts +++ b/projects/editor/src/app/services/unit.service.ts @@ -11,7 +11,7 @@ import { CompoundElement, DragNDropValueObject, InputElement, InputElementValue, TextLabel, PlayerElement, PlayerProperties, PositionedUIElement, - UIElement, UIElementType, UIElementValue, Hotspot + UIElement, UIElementType, UIElementValue, Hotspot, PositionProperties } from 'common/models/elements/element'; import { ClozeDocument, ClozeElement } from 'common/models/elements/compound-elements/cloze/cloze'; import { LikertRowElement } from 'common/models/elements/compound-elements/likert/likert-row'; @@ -128,17 +128,17 @@ export class UnitService { } if (coordinates) { - newElement.position = UIElement.initPositionProps({ + newElement.position = { ...(section.dynamicPositioning && { gridColumn: coordinates.x }), ...(section.dynamicPositioning && { gridRow: coordinates.y }), ...(!section.dynamicPositioning && { yPosition: coordinates.y }), ...(!section.dynamicPositioning && { yPosition: coordinates.y }) - }); + } as PositionProperties; } section.addElement(ElementFactory.createElement({ ...newElement, id: this.idService.getAndRegisterNewID(newElement.type), - position: UIElement.initPositionProps(newElement.position) + position: { ...newElement.position } as PositionProperties }) as PositionedUIElement); this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); } diff --git a/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts b/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts index cf7df19a0..032c6659e 100644 --- a/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts +++ b/projects/editor/src/app/text-editor/angular-node-views/drop-list-component-extension.ts @@ -2,8 +2,7 @@ import { Injector } from '@angular/core'; import { Node, mergeAttributes } from '@tiptap/core'; import { AngularNodeViewRenderer } from 'ngx-tiptap'; import { DropListNodeviewComponent } from './drop-list-nodeview.component'; -import { DropListSimpleElement } - from 'common/models/elements/compound-elements/cloze/cloze-child-elements/drop-list-simple'; +import { DropListElement } from 'common/models/elements/input-elements/drop-list'; const DropListComponentExtension = (injector: Injector): Node => { return Node.create({ @@ -14,7 +13,13 @@ const DropListComponentExtension = (injector: Injector): Node => { addAttributes() { return { model: { - default: new DropListSimpleElement({ type: 'drop-list-simple', id: 'cloze-child-id-placeholder' }) + default: new DropListElement({ + type: 'drop-list', + id: 'cloze-child-id-placeholder', + width: 150, + height: 30, + onlyOneItem: true + }) } }; }, diff --git a/projects/editor/src/app/text-editor/angular-node-views/drop-list-nodeview.component.ts b/projects/editor/src/app/text-editor/angular-node-views/drop-list-nodeview.component.ts index a2021cf06..ab50bfb7c 100644 --- a/projects/editor/src/app/text-editor/angular-node-views/drop-list-nodeview.component.ts +++ b/projects/editor/src/app/text-editor/angular-node-views/drop-list-nodeview.component.ts @@ -8,9 +8,9 @@ import { AngularNodeViewComponent } from 'ngx-tiptap'; [style.vertical-align]="'middle'" [style.width.px]="node.attrs.model.width" [style.height.px]="node.attrs.model.height"> - <aspect-drop-list-simple [elementModel]="node.attrs.model" - [matTooltip]="'ID: ' + node.attrs.model.id"> - </aspect-drop-list-simple> + <aspect-drop-list [elementModel]="node.attrs.model" + [matTooltip]="'ID: ' + node.attrs.model.id"> + </aspect-drop-list> </div> ` }) diff --git a/projects/editor/src/assets/i18n/de.json b/projects/editor/src/assets/i18n/de.json index 387202ddf..a9daae04c 100644 --- a/projects/editor/src/assets/i18n/de.json +++ b/projects/editor/src/assets/i18n/de.json @@ -184,7 +184,8 @@ "hotspots": "Aktive Bereiche", "newHotspot": "Neuer Bereich", "hasDynamicRowCount": "Dynamische Zeilen", - "expectedCharactersCount": "Erwartete Zeichenanzahl" + "expectedCharactersCount": "Erwartete Zeichenanzahl", + "isSortList": "Sortierliste" }, "hotspot": { "top": "Abstand von oben", 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 8a49d63cf..c08a9f685 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,6 @@ export class ElementModelElementCodeMappingService { : InputElementValue => { switch (elementModel.type) { case 'drop-list': - case 'drop-list-simple': return (elementCodeValue !== undefined) ? (elementCodeValue as string[]).map(id => this.getDragNDropValueObjectById(id)) as DragNDropValueObject[] : (elementModel as InputElement).value; @@ -69,7 +68,6 @@ export class ElementModelElementCodeMappingService { mapToElementCodeValue = (elementModelValue: InputElementValue, elementType: UIElementType): InputElementValue => { switch (elementType) { case 'drop-list': - case 'drop-list-simple': return (elementModelValue as DragNDropValueObject[]).map(object => object.id); case 'hotspot-image': return (elementModelValue as Hotspot[]).map(hotspot => hotspot.value); -- GitLab