From cc81c788be8dd6f996d7fc54d6f15ba595521ecc Mon Sep 17 00:00:00 2001 From: rhenck <richard.henck@iqb.hu-berlin.de> Date: Fri, 7 Oct 2022 16:43:49 +0200 Subject: [PATCH] Add buttons as allowed cloze children - Make position props on buttons optional - Move UIElement creation back to Factory instead of within Section --- .../components/button/button.component.ts | 4 +- .../cloze/cloze.component.ts | 2 +- .../cloze/compound-child-overlay.component.ts | 6 ++ .../common/models/elements/button/button.ts | 8 +-- .../elements/compound-elements/cloze/cloze.ts | 14 +++-- .../cloze/tiptap-editor-extensions/button.ts | 26 +++++++++ projects/common/models/section.ts | 35 ++---------- projects/common/util/element.factory.ts | 56 ++++++++++++++++++- projects/editor/src/app/app.module.ts | 2 + .../editor/src/app/services/unit.service.ts | 9 +-- .../button-component-extension.ts | 33 +++++++++++ .../button-nodeview.component.ts | 13 +++++ .../rich-text-editor.component.html | 4 ++ .../text-editor/rich-text-editor.component.ts | 8 +++ 14 files changed, 173 insertions(+), 47 deletions(-) create mode 100644 projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/button.ts create mode 100644 projects/editor/src/app/text-editor/angular-node-views/button-component-extension.ts create mode 100644 projects/editor/src/app/text-editor/angular-node-views/button-nodeview.component.ts diff --git a/projects/common/components/button/button.component.ts b/projects/common/components/button/button.component.ts index efb0e597e..e17e671e6 100644 --- a/projects/common/components/button/button.component.ts +++ b/projects/common/components/button/button.component.ts @@ -50,8 +50,8 @@ import { ButtonElement } from 'common/models/elements/button/button'; <input *ngIf="elementModel.imageSrc" type="image" [src]="elementModel.imageSrc | safeResourceUrl" - [class]="elementModel.position.dynamicPositioning && - !elementModel.position.fixedSize ? 'dynamic-image' : 'static-image'" + [class]="elementModel.position?.dynamicPositioning && + !elementModel.position?.fixedSize ? 'dynamic-image' : 'static-image'" [alt]="'imageNotFound' | translate" (click)="elementModel.action && elementModel.actionParam !== null? navigateTo.emit({ diff --git a/projects/common/components/compound-elements/cloze/cloze.component.ts b/projects/common/components/compound-elements/cloze/cloze.component.ts index 9d7ebbde1..10bd922d0 100644 --- a/projects/common/components/compound-elements/cloze/cloze.component.ts +++ b/projects/common/components/compound-elements/cloze/cloze.component.ts @@ -155,7 +155,7 @@ import { ClozeElement } from 'common/models/elements/compound-elements/cloze/clo [style.height]="'1em'" [style.vertical-align]="'middle'"> </ng-container> - <span *ngIf="['ToggleButton', 'DropList', 'TextField'].includes(subPart.type)"> + <span *ngIf="['ToggleButton', 'DropList', 'TextField', 'Button'].includes(subPart.type)"> <aspect-compound-child-overlay [style.display]="'inline-block'" [parentForm]="parentForm" [element]="$any(subPart).attrs.model" 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 7f595f33d..f41139468 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 @@ -35,6 +35,12 @@ import { ValueChangeElement } from 'common/models/elements/element'; [style.width]="element.dynamicWidth ? 'unset' : element.width+'px'" [style.height.px]="element.height"> </aspect-toggle-button> + <aspect-button *ngIf="element.type === 'button'" #childComponent + [style.pointer-events]="editorMode ? 'none' : 'auto'" + [elementModel]="$any(element)" + [style.width.px]="element.width" + [style.height.px]="element.height"> + </aspect-button> </div> `, styles: [ diff --git a/projects/common/models/elements/button/button.ts b/projects/common/models/elements/button/button.ts index 0b92465a1..b5bdcc645 100644 --- a/projects/common/models/elements/button/button.ts +++ b/projects/common/models/elements/button/button.ts @@ -1,18 +1,18 @@ import { Type } from '@angular/core'; import { ElementFactory } from 'common/util/element.factory'; import { - BasicStyles, PositionedUIElement, PositionProperties, UIElement + BasicStyles, PositionProperties, UIElement } from 'common/models/elements/element'; import { ButtonComponent } from 'common/components/button/button.component'; import { ElementComponent } from 'common/directives/element-component.directive'; -export class ButtonElement extends UIElement implements PositionedUIElement { +export class ButtonElement extends UIElement { label: string = 'Navigationsknopf'; imageSrc: string | null = null; asLink: boolean = false; action: null | 'unitNav' | 'pageNav' = null; actionParam: null | 'previous' | 'next' | 'first' | 'last' | 'end' | number = null; - position: PositionProperties; + position: PositionProperties | undefined; styling: BasicStyles & { borderRadius: number; }; @@ -24,7 +24,7 @@ export class ButtonElement extends UIElement implements PositionedUIElement { if (element.asLink) this.asLink = element.asLink; if (element.action) this.action = element.action; if (element.actionParam) this.actionParam = element.actionParam; - this.position = ElementFactory.initPositionProps(element.position); + this.position = element.position ? ElementFactory.initPositionProps(element.position) : undefined; this.styling = { ...ElementFactory.initStylingProps<{ borderRadius: number; }>({ borderRadius: 0, ...element.styling }) }; diff --git a/projects/common/models/elements/compound-elements/cloze/cloze.ts b/projects/common/models/elements/compound-elements/cloze/cloze.ts index 3c363a6eb..e55f6ebc2 100644 --- a/projects/common/models/elements/compound-elements/cloze/cloze.ts +++ b/projects/common/models/elements/compound-elements/cloze/cloze.ts @@ -17,6 +17,7 @@ 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'; export class ClozeElement extends CompoundElement implements PositionedUIElement { document: ClozeDocument = { type: 'doc', content: [] }; @@ -61,7 +62,7 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement private static createSubNodeElements(node: any) { node.content?.forEach((subNode: any) => { - if (['ToggleButton', 'DropList', 'TextField'].includes(subNode.type) && + if (['ToggleButton', 'DropList', 'TextField', 'Button'].includes(subNode.type) && subNode.attrs.model.id === 'cloze-child-id-placeholder') { subNode.attrs.model = ClozeElement.createChildElement({ ...subNode.attrs.model }); @@ -78,7 +79,7 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement ...paragraph, content: paragraph.content ? paragraph.content .map((paraPart: ClozeDocumentParagraphPart) => ( - ['TextField', 'DropList', 'ToggleButton'].includes(paraPart.type) ? + ['TextField', 'DropList', 'ToggleButton', 'Button'].includes(paraPart.type) ? { ...paraPart, attrs: { @@ -139,8 +140,8 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement return elementList; } - private static createChildElement(elementModel: Partial<UIElement>): InputElement { - let newElement: InputElement; + private static createChildElement(elementModel: Partial<UIElement>): InputElement | ButtonElement { + let newElement: InputElement | ButtonElement; switch (elementModel.type) { case 'text-field-simple': newElement = new TextFieldSimpleElement(elementModel as TextFieldSimpleElement); @@ -151,6 +152,9 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement case 'toggle-button': newElement = new ToggleButtonElement(elementModel as ToggleButtonElement); break; + case 'button': + newElement = new ButtonElement(elementModel as ButtonElement); + break; default: throw new Error(`ElementType ${elementModel.type} not found!`); } @@ -162,7 +166,7 @@ export class ClozeElement extends CompoundElement implements PositionedUIElement return []; } return documentPart.content - .filter((word: ClozeDocumentParagraphPart) => ['TextField', 'DropList', 'ToggleButton'].includes(word.type)) + .filter((word: ClozeDocumentParagraphPart) => ['TextField', 'DropList', 'ToggleButton', 'Button'].includes(word.type)) .reduce((accumulator: any[], currentValue: any) => accumulator.concat(currentValue.attrs.model), []); } } diff --git a/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/button.ts b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/button.ts new file mode 100644 index 000000000..807c445b3 --- /dev/null +++ b/projects/common/models/elements/compound-elements/cloze/tiptap-editor-extensions/button.ts @@ -0,0 +1,26 @@ +import { Node, mergeAttributes } from '@tiptap/core'; +import { ButtonElement } from 'common/models/elements/button/button'; + +const ButtonExtension = + Node.create({ + group: 'inline', + inline: true, + name: 'Button', + + addAttributes() { + return { + model: { + default: new ButtonElement({ type: 'button' }) + } + }; + }, + + parseHTML() { + return [{ tag: 'aspect-nodeview-button' }]; + }, + renderHTML({ HTMLAttributes }) { + return ['aspect-nodeview-button', mergeAttributes(HTMLAttributes)]; + } + }); + +export default ButtonExtension; diff --git a/projects/common/models/section.ts b/projects/common/models/section.ts index 96ef87fc0..371ff70a5 100644 --- a/projects/common/models/section.ts +++ b/projects/common/models/section.ts @@ -27,6 +27,7 @@ import { SpellCorrectElement } from 'common/models/elements/input-elements/spell import { FrameElement } from 'common/models/elements/frame/frame'; import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; import { GeometryElement } from 'common/models/elements/geometry/geometry'; +import { ElementFactory } from 'common/util/element.factory'; export class Section { [index: string]: unknown; @@ -40,30 +41,6 @@ export class Section { gridRowSizes: string = '1fr'; activeAfterID: string | null = null; - static ELEMENT_CLASSES: Record<string, Type<UIElement>> = { - text: TextElement, - button: ButtonElement, - 'text-field': TextFieldElement, - 'text-field-simple': TextFieldSimpleElement, - 'text-area': TextAreaElement, - checkbox: CheckboxElement, - dropdown: DropdownElement, - radio: RadioButtonGroupElement, - image: ImageElement, - audio: AudioElement, - video: VideoElement, - likert: LikertElement, - 'radio-group-images': RadioButtonGroupComplexElement, - 'drop-list': DropListElement, - 'drop-list-simple': DropListSimpleElement, - cloze: ClozeElement, - slider: SliderElement, - 'spell-correct': SpellCorrectElement, - frame: FrameElement, - 'toggle-button': ToggleButtonElement, - geometry: GeometryElement - }; - constructor(section?: Partial<Section>) { if (section?.height) this.height = section.height; if (section?.backgroundColor) this.backgroundColor = section.backgroundColor; @@ -74,12 +51,10 @@ export class Section { if (section?.gridRowSizes !== undefined) this.gridRowSizes = section.gridRowSizes; if (section?.activeAfterID) this.activeAfterID = section.activeAfterID; this.elements = - section?.elements?.map(element => Section.createElement(element)) || - []; - } - - static createElement(element: { type: string } & Partial<UIElement>): PositionedUIElement { - return new Section.ELEMENT_CLASSES[element.type](element) as PositionedUIElement; + section?.elements?.map(element => ({ + ...ElementFactory.createElement(element), + position: ElementFactory.initPositionProps(element.position) + } as PositionedUIElement)) || []; } setProperty(property: string, value: UIElementValue): void { diff --git a/projects/common/util/element.factory.ts b/projects/common/util/element.factory.ts index f0a15675a..8fd89b80e 100644 --- a/projects/common/util/element.factory.ts +++ b/projects/common/util/element.factory.ts @@ -1,8 +1,62 @@ import { - BasicStyles, PlayerProperties, PositionProperties, TextImageLabel + BasicStyles, PlayerProperties, PositionedUIElement, PositionProperties, TextImageLabel, UIElement } from 'common/models/elements/element'; +import { Type } from '@angular/core'; +import { TextElement } from 'common/models/elements/text/text'; +import { ButtonElement } from 'common/models/elements/button/button'; +import { TextFieldElement } from 'common/models/elements/input-elements/text-field'; +import { + TextFieldSimpleElement +} from 'common/models/elements/compound-elements/cloze/cloze-child-elements/text-field-simple'; +import { TextAreaElement } from 'common/models/elements/input-elements/text-area'; +import { CheckboxElement } from 'common/models/elements/input-elements/checkbox'; +import { DropdownElement } from 'common/models/elements/input-elements/dropdown'; +import { RadioButtonGroupElement } from 'common/models/elements/input-elements/radio-button-group'; +import { ImageElement } from 'common/models/elements/media-elements/image'; +import { AudioElement } from 'common/models/elements/media-elements/audio'; +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'; +import { FrameElement } from 'common/models/elements/frame/frame'; +import { ToggleButtonElement } from 'common/models/elements/compound-elements/cloze/cloze-child-elements/toggle-button'; +import { GeometryElement } from 'common/models/elements/geometry/geometry'; export abstract class ElementFactory { + static ELEMENT_CLASSES: Record<string, Type<UIElement>> = { + text: TextElement, + button: ButtonElement, + 'text-field': TextFieldElement, + 'text-field-simple': TextFieldSimpleElement, + 'text-area': TextAreaElement, + checkbox: CheckboxElement, + dropdown: DropdownElement, + radio: RadioButtonGroupElement, + image: ImageElement, + audio: AudioElement, + video: VideoElement, + likert: LikertElement, + 'radio-group-images': RadioButtonGroupComplexElement, + 'drop-list': DropListElement, + 'drop-list-simple': DropListSimpleElement, + cloze: ClozeElement, + slider: SliderElement, + 'spell-correct': SpellCorrectElement, + frame: FrameElement, + 'toggle-button': ToggleButtonElement, + geometry: GeometryElement + }; + + static createElement(element: { type: string } & Partial<UIElement>): UIElement { + return new ElementFactory.ELEMENT_CLASSES[element.type](element); + } + static initPositionProps(defaults: Partial<PositionProperties> = {}): PositionProperties { return { fixedSize: defaults.fixedSize !== undefined ? defaults.fixedSize as boolean : false, diff --git a/projects/editor/src/app/app.module.ts b/projects/editor/src/app/app.module.ts index a42abb5f0..df8cdcf8c 100644 --- a/projects/editor/src/app/app.module.ts +++ b/projects/editor/src/app/app.module.ts @@ -47,6 +47,7 @@ import { DropListOptionEditDialogComponent } from './components/dialogs/drop-lis import { ToggleButtonNodeviewComponent } from './text-editor/angular-node-views/toggle-button-nodeview.component'; import { TextFieldNodeviewComponent } from './text-editor/angular-node-views/text-field-nodeview.component'; import { DropListNodeviewComponent } from './text-editor/angular-node-views/drop-list-nodeview.component'; +import { ButtonNodeviewComponent } from './text-editor/angular-node-views/button-nodeview.component'; import { PositionFieldSetComponent } from './components/properties-panel/position-properties-tab/input-groups/position-field-set.component'; import { DimensionFieldSetComponent } from @@ -107,6 +108,7 @@ import { VeronaAPIService } from 'editor/src/app/services/verona-api.service'; ToggleButtonNodeviewComponent, TextFieldNodeviewComponent, DropListNodeviewComponent, + ButtonNodeviewComponent, ElementStylePropertiesComponent, ElementPositionPropertiesComponent, ConfirmationDialogComponent, diff --git a/projects/editor/src/app/services/unit.service.ts b/projects/editor/src/app/services/unit.service.ts index e6c5a6cb9..64f7562fa 100644 --- a/projects/editor/src/app/services/unit.service.ts +++ b/projects/editor/src/app/services/unit.service.ts @@ -134,10 +134,11 @@ export class UnitService { ...(!section.dynamicPositioning && { yPosition: coordinates.y }) }); } - section.addElement(Section.createElement({ + section.addElement(ElementFactory.createElement({ ...newElement, - id: this.idService.getAndRegisterNewID(newElement.type) - })); + id: this.idService.getAndRegisterNewID(newElement.type), + position: ElementFactory.initPositionProps(newElement.position) + }) as PositionedUIElement); this.veronaApiService.sendVoeDefinitionChangedNotification(this.unit); } @@ -184,7 +185,7 @@ export class UnitService { } private duplicateElement(element: UIElement): UIElement { - const newElement = Section.createElement(element); + const newElement = ElementFactory.createElement(element); newElement.id = this.idService.getAndRegisterNewID(newElement.type); if (newElement.position) { diff --git a/projects/editor/src/app/text-editor/angular-node-views/button-component-extension.ts b/projects/editor/src/app/text-editor/angular-node-views/button-component-extension.ts new file mode 100644 index 000000000..952371675 --- /dev/null +++ b/projects/editor/src/app/text-editor/angular-node-views/button-component-extension.ts @@ -0,0 +1,33 @@ +import { Injector } from '@angular/core'; +import { Node, mergeAttributes } from '@tiptap/core'; +import { AngularNodeViewRenderer } from 'ngx-tiptap'; +import { ButtonElement } from 'common/models/elements/button/button'; +import { ButtonNodeviewComponent } from 'editor/src/app/text-editor/angular-node-views/button-nodeview.component'; + +const ButtonComponentExtension = (injector: Injector): Node => { + return Node.create({ + group: 'inline', + inline: true, + name: 'Button', + + addAttributes() { + return { + model: { + default: new ButtonElement({ type: 'button', id: 'cloze-child-id-placeholder', height: 34 }) + } + }; + }, + + parseHTML() { + return [{ tag: 'aspect-nodeview-button' }]; + }, + renderHTML({ HTMLAttributes }) { + return ['aspect-nodeview-button', mergeAttributes(HTMLAttributes)]; + }, + addNodeView() { + return AngularNodeViewRenderer(ButtonNodeviewComponent, { injector }); + } + }); +}; + +export default ButtonComponentExtension; diff --git a/projects/editor/src/app/text-editor/angular-node-views/button-nodeview.component.ts b/projects/editor/src/app/text-editor/angular-node-views/button-nodeview.component.ts new file mode 100644 index 000000000..ac7091924 --- /dev/null +++ b/projects/editor/src/app/text-editor/angular-node-views/button-nodeview.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { AngularNodeViewComponent } from 'ngx-tiptap'; + +@Component({ + selector: 'aspect-nodeview-button', + template: ` + <aspect-button [style.display]="'inline-block'" + [elementModel]="node.attrs.model" + [matTooltip]="'ID: ' + node.attrs.model.id"> + </aspect-button> + ` +}) +export class ButtonNodeviewComponent extends AngularNodeViewComponent { } diff --git a/projects/editor/src/app/text-editor/rich-text-editor.component.html b/projects/editor/src/app/text-editor/rich-text-editor.component.html index 07efd514e..8a7ff1232 100644 --- a/projects/editor/src/app/text-editor/rich-text-editor.component.html +++ b/projects/editor/src/app/text-editor/rich-text-editor.component.html @@ -277,6 +277,10 @@ (click)="insertToggleButton()"> <mat-icon>radio_button_checked</mat-icon> </button> + <button mat-icon-button matTooltip="Navigationsknopf" [matTooltipShowDelay]="300" + (click)="insertButton()"> + <mat-icon>navigation</mat-icon> + </button> </div> </div> <tiptap-editor [editor]="editor" [ngModel]="content" mat-dialog-content diff --git a/projects/editor/src/app/text-editor/rich-text-editor.component.ts b/projects/editor/src/app/text-editor/rich-text-editor.component.ts index 26eda925d..59181bf10 100644 --- a/projects/editor/src/app/text-editor/rich-text-editor.component.ts +++ b/projects/editor/src/app/text-editor/rich-text-editor.component.ts @@ -29,6 +29,7 @@ import { OrderedListExtension } from './extensions/ordered-list'; import ToggleButtonComponentExtension from './angular-node-views/toggle-button-component-extension'; import DropListComponentExtension from './angular-node-views/drop-list-component-extension'; import TextFieldComponentExtension from './angular-node-views/text-field-component-extension'; +import ButtonComponentExtension from 'editor/src/app/text-editor/angular-node-views/button-component-extension'; @Component({ selector: 'aspect-rich-text-editor', @@ -96,6 +97,7 @@ export class RichTextEditorComponent implements OnInit, AfterViewInit { activeExtensions.push(ToggleButtonComponentExtension(this.injector)); activeExtensions.push(DropListComponentExtension(this.injector)); activeExtensions.push(TextFieldComponentExtension(this.injector)); + activeExtensions.push(ButtonComponentExtension(this.injector)); } this.editor = new Editor({ extensions: activeExtensions, @@ -234,4 +236,10 @@ export class RichTextEditorComponent implements OnInit, AfterViewInit { this.editor.commands.insertContent('<aspect-nodeview-text-field></aspect-nodeview-text-field>'); this.editor.commands.focus(); } + + insertButton() { + console.log('inserting button'); + this.editor.commands.insertContent('<aspect-nodeview-button></aspect-nodeview-button>'); + this.editor.commands.focus(); + } } -- GitLab