This commit is contained in:
Chris Chen 2025-11-04 12:42:10 -08:00
parent b8b35645ac
commit 46ec236ed5
11 changed files with 335 additions and 92 deletions

View File

@ -43,10 +43,12 @@ import { GridModule } from '@progress/kendo-angular-grid';
import { DialogModule } from '@progress/kendo-angular-dialog';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { LayoutModule } from '@progress/kendo-angular-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MD2MobInfoMaintenanceComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-info-maintenance.component';
import { MD2MobInfoEditorComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-info-editor/md2-mob-info-editor.component';
import { MD2MobInfoDetailComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-info-detail/md2-mob-info-detail.component';
import { MD2MobSkillEditorComponent } from './massive-darkness2/md2-mob-info-maintenance/md2-mob-skill-editor/md2-mob-skill-editor.component';
@NgModule({
@ -79,7 +81,8 @@ import { MD2MobInfoDetailComponent } from './massive-darkness2/md2-mob-info-main
MD2IconPickerDlgComponent,
MD2MobInfoMaintenanceComponent,
MD2MobInfoEditorComponent,
MD2MobInfoDetailComponent
MD2MobInfoDetailComponent,
MD2MobSkillEditorComponent
],
imports: [
CommonModule,
@ -119,7 +122,8 @@ import { MD2MobInfoDetailComponent } from './massive-darkness2/md2-mob-info-main
GridModule,
DialogModule,
InputsModule,
DropDownsModule
DropDownsModule,
LayoutModule
]
})
export class GamesModule { }

View File

@ -1,5 +1,5 @@
<kendo-editor [value]="value" (valueChange)="onChange($event)" (blur)="onTouched()" [disabled]="disabled"
[schema]="messageTemplateSchema" [iframe]="false">
[schema]="messageTemplateSchema" [iframe]="false" class="h-100">
<kendo-toolbar>
<!-- Standard editing tools -->
<kendo-toolbar-buttongroup>

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, EventEmitter, Inject, Input, NgZone, Output, PLATFORM_ID, Renderer2, ViewChild, AfterViewInit } from '@angular/core';
import { ControlValueAccessor, Validator, AbstractControl, ValidationErrors } from '@angular/forms';
import { Component, ElementRef, EventEmitter, Inject, Input, NgZone, Output, PLATFORM_ID, Renderer2, ViewChild, AfterViewInit, forwardRef, ChangeDetectorRef } from '@angular/core';
import { ControlValueAccessor, Validator, AbstractControl, ValidationErrors, NG_VALUE_ACCESSOR } from '@angular/forms';
import { EditorComponent, NodeSpec, schema, Schema, FontSizeItem } from '@progress/kendo-angular-editor';
import { MsgBoxService } from '../../../services/msg-box.service';
@ -10,10 +10,18 @@ import { MD2StateService } from '../../../services/MD2/md2-state.service';
import { MD2IconPickerDlgComponent } from './md2-icon-picker-dlg.component';
import { NbDialogService } from '@nebular/theme';
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
import { DialogService } from '@progress/kendo-angular-dialog';
@Component({
selector: 'md2-html-editor',
templateUrl: './md2-html-editor.component.html',
styleUrl: './md2-html-editor.component.scss'
styleUrl: './md2-html-editor.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MD2HtmlEditorComponent),
multi: true
}
]
})
export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewInit {
@ViewChild(EditorComponent) editor: EditorComponent;
@ -52,7 +60,8 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewIn
constructor(
private msgBoxService: MsgBoxService,
private md2StateService: MD2StateService,
private dialogService: NbDialogService,
private dialogService: DialogService,
private cdr: ChangeDetectorRef,
elementRef: ElementRef, ngZone: NgZone, @Inject(PLATFORM_ID) platformId: Object) {
}
@ -71,12 +80,38 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewIn
this.fontSizeDropdown.defaultItem = this.defaultFontSize;
}
}
// Ensure the editor value is set if writeValue was called before view init
if (this.editor && this.value && this.editor.value !== this.value) {
this.editor.value = this.value;
}
}, 0);
}
// ControlValueAccessor implementation
writeValue(value: string): void {
this.value = value || '';
writeValue(value: string | null | undefined): void {
const newValue = value || '';
// Only update if the value actually changed to avoid unnecessary updates
if (this.value !== newValue) {
this.value = newValue;
// Angular's [value] binding will handle updating the editor
// But if editor is already initialized and value is out of sync, ensure sync
if (this.editor && this.editor.value !== this.value) {
// Use setTimeout to avoid ExpressionChangedAfterItHasBeenCheckedError
setTimeout(() => {
if (this.editor && this.editor.value !== this.value) {
this.editor.value = this.value;
}
}, 0);
}
// Trigger change detection to ensure the binding updates
if (!this.cdr['destroyed']) {
this.cdr.markForCheck();
}
}
}
registerOnChange(fn: (value: string) => void): void {
@ -92,8 +127,11 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewIn
}
onChange(value: string): void {
this.value = value;
this.onChangeFn(value);
// Only update if value actually changed to prevent infinite loops
if (this.value !== value) {
this.value = value || '';
this.onChangeFn(this.value);
}
}
onTouched(): void {
@ -101,10 +139,12 @@ export class MD2HtmlEditorComponent implements ControlValueAccessor, AfterViewIn
}
showInsertMD2Icon() {
this.dialogService.open(MD2IconPickerDlgComponent, {
closeOnBackdropClick: true,
closeOnEsc: true
}).onClose.pipe(first()).subscribe((html: string) => {
this.dialogService.open({
title: 'Insert MD2 Icon',
content: MD2IconPickerDlgComponent,
width: '800px',
height: 600
}).result.subscribe((html: string) => {
if (html && this.editor) {
this.insertAfterSelection(html, true);
return;

View File

@ -2,16 +2,11 @@ import { Component, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { MD2Icon } from '../massive-darkness2.model';
import { MD2StateService } from '../../../services/MD2/md2-state.service';
import { DialogRef } from '@progress/kendo-angular-dialog';
@Component({
selector: 'md2-icon-picker-dlg',
template: `
<nb-card style="max-width: 800px;">
<nb-card-header>
<h5>Insert MD2 Icon</h5>
</nb-card-header>
<nb-card-body>
<div class="md2-icon-grid">
template: ` <div class="md2-icon-grid">
<div
*ngFor="let iconData of iconList"
(click)="selectIcon(iconData)"
@ -19,13 +14,23 @@ import { MD2StateService } from '../../../services/MD2/md2-state.service';
[innerHTML]="iconData.html">
</div>
</div>
<!-- <nb-card>
<nb-card-header>
<h5>Insert MD2 Icon</h5>
</nb-card-header>
<nb-card-body>
</nb-card-body>
<nb-card-footer>
<button nbButton status="primary" (click)="cancel()">Cancel</button>
</nb-card-footer>
</nb-card>
</nb-card> -->
`,
styles: [`
nb-card{
max-width: 800px;
z-index: 1050;
}
.md2-icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
@ -57,7 +62,7 @@ export class MD2IconPickerDlgComponent implements OnInit {
iconList: Array<{ icon: MD2Icon, name: string, html: string }> = [];
constructor(
private dlgRef: NbDialogRef<MD2IconPickerDlgComponent>,
private dlgRef: DialogRef,
private md2StateService: MD2StateService
) { }

View File

@ -1 +1 @@
<span class="MD2Icon {{iconName}} {{iconClass}} {{sizeClass}}"></span>
<span [innerHtml]="iconHtml" class="g-font-size-20 g-line-height-0 mx-2"></span>

View File

@ -1,5 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { MD2Icon } from '../massive-darkness2.model';
import { MD2StateService } from '../../../services/MD2/md2-state.service';
@Component({
selector: 'md2-icon',
@ -10,13 +11,17 @@ export class MD2IconComponent implements OnInit {
@Input() iconClass: string = 'mr-1';
iconHtml: string;
private _icon: string | MD2Icon;
@Input() public set icon(v: string | MD2Icon) {
if (this._icon != v) {
this._icon = v;
//if it's string, convert it to MD2Icon
if (typeof v === 'string') {
v = MD2Icon[v];
}
this.iconHtml = this.md2StateService.iconHtml(v as MD2Icon);
}
if (this.isMD2Icon(v)) {
this.iconName = MD2Icon[v].toLowerCase();
@ -30,7 +35,7 @@ export class MD2IconComponent implements OnInit {
}
@Input() size: string = 'sm';
iconName: string;
constructor() { }
constructor(private md2StateService: MD2StateService) { }
ngOnInit(): void {
}

View File

@ -158,12 +158,11 @@
<kendo-grid #skillsGrid [data]="skillsData" [loading]="isLoading" [pageSize]="skillsState.take"
[skip]="skillsState.skip" [sortable]="true" [filterable]="true" [pageable]="true" [height]="400"
kendoGridTemplateEditing (save)="saveSkillHandler($event)" (remove)="removeSkillHandler($event)"
(dataStateChange)="skillsState = $event">
(remove)="removeSkillHandler($event)" (dataStateChange)="skillsState = $event">
<kendo-grid-column field="name" title="Name" [width]="150">
<ng-template kendoGridEditTemplate let-dataItem="dataItem">
<input kendoTextBox [(ngModel)]="dataItem.name" name="name" placeholder="Enter skill name" />
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.name }}
</ng-template>
</kendo-grid-column>
@ -171,55 +170,37 @@
<ng-template kendoGridCellTemplate let-dataItem>
{{ getSkillTypeName(dataItem.type) }}
</ng-template>
<ng-template kendoGridEditTemplate let-dataItem="dataItem">
<kendo-dropdownlist [(ngModel)]="dataItem.type" name="type" [data]="skillTypes" [valueField]="'value'"
[textField]="'text'">
</kendo-dropdownlist>
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="skillTarget" title="Target" [width]="150">
<ng-template kendoGridCellTemplate let-dataItem>
{{ getSkillTargetName(dataItem.skillTarget) }}
</ng-template>
<ng-template kendoGridEditTemplate let-dataItem="dataItem">
<kendo-dropdownlist [(ngModel)]="dataItem.skillTarget" name="skillTarget" [data]="skillTargets"
[valueField]="'value'" [textField]="'text'">
</kendo-dropdownlist>
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="clawRoll" title="Claw Roll" [width]="100">
<ng-template kendoGridEditTemplate let-dataItem="dataItem">
<kendo-numerictextbox [(ngModel)]="dataItem.clawRoll" name="clawRoll" [min]="0">
</kendo-numerictextbox>
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.clawRoll }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="skillRoll" title="Skill Roll" [width]="100">
<ng-template kendoGridEditTemplate let-dataItem="dataItem">
<kendo-numerictextbox [(ngModel)]="dataItem.skillRoll" name="skillRoll" [min]="0">
</kendo-numerictextbox>
<ng-template kendoGridCellTemplate let-dataItem>
{{ dataItem.skillRoll }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="description" title="Description" [width]="300">
<ng-template kendoGridEditTemplate let-dataItem="dataItem">
<textarea kendoTextArea [(ngModel)]="dataItem.description" name="description"
placeholder="Enter skill description" rows="3">
</textarea>
<ng-template kendoGridCellTemplate let-dataItem>
<div [innerHTML]="dataItem.description"></div>
</ng-template>
</kendo-grid-column>
<kendo-grid-command-column title="Actions" [width]="200">
<ng-template kendoGridCellTemplate let-isNew="isNew" let-dataItem="dataItem" let-rowIndex="rowIndex">
<button kendoGridEditCommand [primary]="true">Edit</button>
<button kendoButton [primary]="true" (click)="editSkillHandler(dataItem)">Edit</button>
<button kendoGridRemoveCommand>Remove</button>
</ng-template>
<ng-template kendoGridEditCommandTemplate let-isNew="isNew" let-rowIndex="rowIndex">
<button kendoGridSaveCommand>Save</button>
<button kendoGridCancelCommand>Cancel</button>
</ng-template>
</kendo-grid-command-column>
</kendo-grid>
</div>

View File

@ -1,5 +1,5 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { DialogRef, DialogContentBase } from '@progress/kendo-angular-dialog';
import { DialogRef, DialogContentBase, DialogService } from '@progress/kendo-angular-dialog';
import { GridComponent, GridDataResult } from '@progress/kendo-angular-grid';
import { State } from '@progress/kendo-data-query';
import { first } from 'rxjs/operators';
@ -8,6 +8,7 @@ import { MobType } from '../../massive-darkness2.model';
import { MobSkillType } from '../../massive-darkness2.model.boss';
import { MD2MobLevelInfoService, MD2MobSkillService } from '../../service/massive-darkness2.service';
import { MsgBoxService } from '../../../../services/msg-box.service';
import { MD2MobSkillEditorComponent } from '../md2-mob-skill-editor/md2-mob-skill-editor.component';
@Component({
selector: 'ngx-md2-mob-info-detail',
@ -48,6 +49,7 @@ export class MD2MobInfoDetailComponent extends DialogContentBase implements OnIn
constructor(
public dialog: DialogRef,
private dialogService: DialogService,
private mobLevelInfoService: MD2MobLevelInfoService,
private mobSkillService: MD2MobSkillService,
private msgBoxService: MsgBoxService
@ -224,45 +226,64 @@ export class MD2MobInfoDetailComponent extends DialogContentBase implements OnIn
description: ''
};
this.openSkillEditor(newSkill, true);
}
public editSkillHandler(dataItem: MD2MobSkill): void {
if (!this.selectedLevelInfo) return;
// Create a copy of the skill for editing
const skillCopy: MD2MobSkill = JSON.parse(JSON.stringify(dataItem));
this.openSkillEditor(skillCopy, false);
}
private openSkillEditor(skill: MD2MobSkill, isNew: boolean): void {
if (!this.selectedLevelInfo) return;
const dialogRef = this.dialogService.open({
title: isNew ? 'Add New Skill' : 'Edit Skill',
content: MD2MobSkillEditorComponent,
width: '80vw',
height: 700
});
const editor = dialogRef.content.instance;
editor.isAdding = isNew;
editor.data = skill;
editor.mobLevelInfoId = this.selectedLevelInfo.id;
// Force model re-initialization after data is set
setTimeout(() => {
editor.initializeModel();
}, 0);
dialogRef.result.subscribe(result => {
if (result && typeof result === 'object' && 'id' in result) {
this.handleSkillSaved(result as MD2MobSkill, isNew);
}
});
}
private handleSkillSaved(result: MD2MobSkill, isNew: boolean): void {
if (!this.selectedLevelInfo) return;
if (isNew) {
if (!this.selectedLevelInfo.skills) {
this.selectedLevelInfo.skills = [];
}
this.selectedLevelInfo.skills.push(newSkill);
this.selectedLevelInfo.skills.push(result);
} else {
const index = this.selectedLevelInfo.skills?.findIndex(s => s.id === result.id);
if (index !== undefined && index !== -1 && this.selectedLevelInfo.skills) {
this.selectedLevelInfo.skills[index] = result;
}
}
this.loadSkills(this.selectedLevelInfo);
}
public saveSkillHandler({ dataItem, isNew }: any): void {
if (isNew) {
dataItem.id = this.generateId();
dataItem.mobLevelInfoId = this.selectedLevelInfo?.id;
}
this.isLoading = true;
this.mobSkillService.createOrUpdate(dataItem).pipe(first()).subscribe(result => {
this.isLoading = false;
if (this.selectedLevelInfo) {
if (isNew) {
if (!this.selectedLevelInfo.skills) {
this.selectedLevelInfo.skills = [];
}
const index = this.selectedLevelInfo.skills.findIndex(s => s.id === dataItem.id);
if (index !== -1) {
this.selectedLevelInfo.skills[index] = result;
} else {
this.selectedLevelInfo.skills.push(result);
}
} else {
const index = this.selectedLevelInfo.skills.findIndex(s => s.id === result.id);
if (index !== -1) {
this.selectedLevelInfo.skills[index] = result;
}
}
this.loadSkills(this.selectedLevelInfo);
}
}, error => {
this.isLoading = false;
console.error('Error saving skill:', error);
});
// This method is no longer used but kept for backward compatibility
// Skills are now edited via dialog
}
public removeSkillHandler({ dataItem }: { dataItem: MD2MobSkill }): void {

View File

@ -0,0 +1,64 @@
<div class="k-dialog-content">
<form #form="ngForm" class="k-form">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label class="k-label">Name</label>
<input kendoTextBox [(ngModel)]="model.name" name="name" class="k-input"
placeholder="Enter skill name" />
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Type *</label>
<kendo-dropdownlist [(ngModel)]="selectedSkillType" name="type" [data]="skillTypes"
[valueField]="'value'" [textField]="'text'"
[defaultItem]="{ value: null, text: 'Select type...' }">
</kendo-dropdownlist>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label">Target</label>
<kendo-dropdownlist [(ngModel)]="selectedSkillTarget" name="skillTarget" [data]="skillTargets"
[valueField]="'value'" [textField]="'text'">
</kendo-dropdownlist>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label"><md2-icon icon="EnemyClaw"></md2-icon>Claw Roll</label>
<kendo-numerictextbox [(ngModel)]="model.clawRoll" name="clawRoll" [min]="0" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="k-label"><md2-icon icon="EnemySkill"></md2-icon>Skill Roll</label>
<kendo-numerictextbox [(ngModel)]="model.skillRoll" name="skillRoll" [min]="0" [decimals]="0"
[format]="'n0'">
</kendo-numerictextbox>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label class="k-label">Description</label>
<md2-html-editor [(ngModel)]="model.description" name="description"
class="htmlEditor"></md2-html-editor>
</div>
</div>
</div>
</form>
</div>
<kendo-dialog-actions>
<button kendoButton (click)="close()">Cancel</button>
<button kendoButton [primary]="true" (click)="save()" [disabled]="!isValid || processing">
{{ processing ? 'Saving...' : 'Save' }}
</button>
</kendo-dialog-actions>

View File

@ -0,0 +1,8 @@
.k-label {
font-weight: 500;
}
md2-html-editor {
width: 100%;
min-height: 300px;
}

View File

@ -0,0 +1,115 @@
import { Component, Input, OnInit, ViewChild, ChangeDetectorRef } from '@angular/core';
import { DialogRef, DialogContentBase } from '@progress/kendo-angular-dialog';
import { NgForm } from '@angular/forms';
import { first } from 'rxjs/operators';
import { MD2MobSkill, MobSkillTarget } from '../../massive-darkness2.db.model';
import { MobSkillType } from '../../massive-darkness2.model.boss';
import { MD2MobSkillService } from '../../service/massive-darkness2.service';
@Component({
selector: 'ngx-md2-mob-skill-editor',
templateUrl: './md2-mob-skill-editor.component.html',
styleUrls: ['./md2-mob-skill-editor.component.scss']
})
export class MD2MobSkillEditorComponent extends DialogContentBase implements OnInit {
@Input() public data: MD2MobSkill;
@Input() public mobLevelInfoId: string;
@Input() public isAdding: boolean = false;
@ViewChild('form') form: NgForm;
public model: MD2MobSkill;
public processing: boolean = false;
public skillTypes: Array<{ value: MobSkillType; text: string }> = [];
public skillTargets: Array<{ value: MobSkillTarget | null; text: string }> = [];
public selectedSkillType: { value: MobSkillType; text: string } | null = null;
public selectedSkillTarget: { value: MobSkillTarget | null; text: string } | null = null;
constructor(
public dialog: DialogRef,
private mobSkillService: MD2MobSkillService,
private cdr: ChangeDetectorRef
) {
super(dialog);
this.initializeEnums();
}
ngOnInit(): void {
this.initializeModel();
}
public initializeModel(): void {
const typeValue = this.data?.type !== undefined && this.data?.type !== null ? this.data.type : MobSkillType.Combat;
const targetValue = this.data?.skillTarget !== undefined ? this.data.skillTarget : null;
this.model = {
id: this.data?.id || '',
mobLevelInfoId: this.mobLevelInfoId || this.data?.mobLevelInfoId || '',
type: typeValue,
skillTarget: targetValue,
clawRoll: this.data?.clawRoll ?? 0,
skillRoll: this.data?.skillRoll ?? 1,
name: this.data?.name || '',
description: this.data?.description || ''
};
// Set selected objects for dropdowns
this.selectedSkillType = this.skillTypes.find(t => t.value === typeValue) || this.skillTypes[0] || null;
this.selectedSkillTarget = this.skillTargets.find(t => t.value === targetValue) || this.skillTargets[0] || null;
this.cdr.detectChanges();
}
private initializeEnums(): void {
// Initialize MobSkillType options
Object.keys(MobSkillType).filter(key => isNaN(Number(key))).forEach(key => {
this.skillTypes.push({
value: MobSkillType[key] as MobSkillType,
text: key
});
});
// Initialize MobSkillTarget options
this.skillTargets.push({ value: null, text: 'None' });
Object.keys(MobSkillTarget).filter(key => isNaN(Number(key))).forEach(key => {
this.skillTargets.push({
value: MobSkillTarget[key] as MobSkillTarget,
text: key
});
});
}
public close(): void {
this.dialog.close();
}
public save(): void {
if (!this.processing) {
this.processing = true;
// Extract enum values from selected objects
const mobSkill: MD2MobSkill = {
...this.model,
type: this.selectedSkillType?.value ?? MobSkillType.Combat,
skillTarget: this.selectedSkillTarget?.value ?? null,
mobLevelInfoId: this.mobLevelInfoId || this.model.mobLevelInfoId
};
this.mobSkillService.createOrUpdate(mobSkill).pipe(first()).subscribe(result => {
this.processing = false;
this.dialog.close(result);
}, error => {
this.processing = false;
console.error('Error saving mob skill:', error);
});
}
}
public get isValid(): boolean {
if (!this.model) {
return false;
}
const typeValid = this.selectedSkillType !== null && this.selectedSkillType !== undefined;
return typeValid;
}
}