diff --git a/projects/common/components/button/button.component.ts b/projects/common/components/button/button.component.ts index efb0e597e8cde230dd8a841d56a7ffed530f5d9d..e17e671e6ea3508617a40f0e819f8719379f0e20 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 9d7ebbde1917de31d61c48fc007737d2ef3ef277..10bd922d0db25d4d5cdaf1f8bd6b27116170fc7a 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 7f595f33dc76b05146e5d1d9cc4f9b22ea945246..f411394687589f7e80eccd4bcd5ca69631c4a688 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 0b92465a1eda304de04a68f592c0d1bf380a5687..b5bdcc6456540614e5d1f1a71ba7849125d5f9ff 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 3c363a6eb17f856f4e6f0f3fba2035dafd190272..e55f6ebc26f012d8bfd1eee406d01db111eb56f9 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 0000000000000000000000000000000000000000..807c445b3a04a51848e44fc7c4b14a57b08ac18d --- /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 96ef87fc0b86d0f21190e451fde2bca7aeade9c6..371ff70a550351df6ee7c75c0d1e6323d9f04862 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 f0a15675a39b1a82a7d744da472f7be69a95a536..8fd89b80e3f600c59ede018145954f86347a85cd 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 a42abb5f032bd0ffef2251aecc16e7cd61adf413..df8cdcf8c9b5fa694532e7e04b1d53b13bb177de 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 e6c5a6cb954b7788bc0c1012f0d1e2d8c184ce87..64f7562fa0d7ebf0a858e657dada1200cc014ebb 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 0000000000000000000000000000000000000000..952371675d5bc576607cdf931f208e4bcccdeeae --- /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 0000000000000000000000000000000000000000..ac7091924e6fac0d6fe0f779e4fcf9cf80287fb9 --- /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 07efd514eb517757bedf3ed4abbc48b4e4a56aa8..8a7ff12324330c4393451a57bb6f1f1090de6e85 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 26eda925dee280bd9efc0b9a07b7f9fabc724142..59181bf1009751ad17d9c19390ff77d11b9b11c9 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(); + } }