Переглянути джерело

Merge branch 'release/0.13.1'

Warafear 6 місяців тому
батько
коміт
84b1981780

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "dndtools",
-  "version": "0.13.0",
+  "version": "0.13.1",
   "scripts": {
     "ng": "ng",
     "start": "nx serve",

+ 4 - 11
src/app/journal/journal-maps/journal-maps.component.html

@@ -1,11 +1,4 @@
-<div
-  style="
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    height: 100%;
-    width: 100%;
-  "
->
-  <img style="height: 100%" src="assets/images/maps_coming_soon.jpeg" alt="" />
-</div>
+<ng-template #tooltip><div [innerHTML]="currentText"></div></ng-template>
+
+<button [ngbTooltip]="tooltip" (mouseover)="setNewText('hans')">Hans</button>
+<button [ngbTooltip]="tooltip" (mouseover)="setNewText('peter')">Peter</button>

+ 12 - 1
src/app/journal/journal-maps/journal-maps.component.ts

@@ -5,4 +5,15 @@ import { Component } from '@angular/core';
   templateUrl: './journal-maps.component.html',
   styleUrl: './journal-maps.component.scss',
 })
-export class JournalMapsComponent {}
+export class JournalMapsComponent {
+  currentText = 'Initial';
+
+  texts: any = {
+    hans: '<b>hans</b>',
+    peter: '<b>peter</b>',
+  };
+
+  setNewText(key: string) {
+    this.currentText = this.texts[key];
+  }
+}

+ 4 - 2
src/app/journal/journal-notes/journal-notes.component.html

@@ -93,13 +93,13 @@
     } @else {
       <div class="read-container">
         <div class="title-row">
-          <div class="title-read">{{ entries[currentEntryIndex].title }}</div>
+          <div class="title-read">{{ currentEntry.title }}</div>
           <div class="date-read">
             {{ currentEntry.created | date: "longDate" : "" : "de" }}
           </div>
         </div>
         <divider appearance="gold-2"></divider>
-        <div class="content-read" [innerHTML]="currentEntry.content"></div>
+        <div class="content-read" [innerHTML]="tooltipifiedEntry.content"></div>
       </div>
     }
     <div class="button-row">
@@ -126,3 +126,5 @@
     {{ "notes.empty" | translate }}
   </div>
 }
+<ng-template #tooltip><div [innerHTML]="tooltipText"></div></ng-template>
+<div #creationAnchor></div>

+ 131 - 29
src/app/journal/journal-notes/journal-notes.component.ts

@@ -1,10 +1,22 @@
-import { Component, OnInit, OnDestroy } from '@angular/core';
+import {
+  Component,
+  OnInit,
+  OnDestroy,
+  inject,
+  TemplateRef,
+  ViewChild,
+  ViewContainerRef,
+  Renderer2,
+  ElementRef,
+} from '@angular/core';
 import { Editor } from 'ngx-editor';
 import { DateAdapter } from '@angular/material/core';
 import { JournalEntry } from 'src/interfaces/interfaces';
 import localeDe from '@angular/common/locales/de';
 import { registerLocaleData } from '@angular/common';
 import { DataService } from 'src/services/data/data.service';
+import { TooltipService } from 'src/services/tooltip/tooltip.service';
+import { HighlightComponent } from 'src/app/shared-components/highlight/highlight.component';
 
 @Component({
   selector: 'app-journal-notes',
@@ -12,6 +24,13 @@ import { DataService } from 'src/services/data/data.service';
   styleUrl: './journal-notes.component.scss',
 })
 export class JournalNotesComponent implements OnInit, OnDestroy {
+  /** Reference to the tooltip */
+  @ViewChild('tooltip', { read: TemplateRef }) tooltip!: TemplateRef<any>;
+
+  /** Reference to the creation anchor */
+  @ViewChild('creationAnchor', { read: ViewContainerRef })
+  creationAnchor!: ViewContainerRef;
+
   editor: Editor = new Editor();
   toolbar: any = [
     // default value
@@ -20,60 +39,71 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
     [{ heading: ['h3', 'h4', 'h5', 'h6'] }],
   ];
 
-  /** Used to show the interactale form or the display version of an entry. */
-  public isInEditMode = false;
+  public entries: JournalEntry[] = [];
+  public currentEntry: JournalEntry = {
+    title: '',
+    content: '',
+    created: new Date(),
+  };
 
-  /** The index of the currently active entry */
+  public isInEditMode = false;
   public currentEntryIndex: number = 0;
-
   private backupIndex: number = -1;
-  /** Indicates, if the currentEntry is a newly generated one that is still not saved. */
   public isNewEntry = false;
 
-  /**The array of JournalEntries, synched to the   */
-  public entries: JournalEntry[] = [];
+  public tooltipText: string = '';
 
-  /** Holds the data for the current entry */
-  public currentEntry: JournalEntry = {
-    title: '',
-    content: '',
+  public npcDescriptions: any = {};
+
+  tooltipifiedEntry: JournalEntry = {
+    title: 'Title',
+    content: 'Content',
     created: new Date(),
   };
 
-  constructor(
-    private _adapter: DateAdapter<any>,
-    private dataService: DataService,
-  ) {
-    registerLocaleData(localeDe);
-    this.entries = this.dataService.notesData;
-  }
+  private _adapter: DateAdapter<any> = inject(DateAdapter);
+  private dataService: DataService = inject(DataService);
+  private tooltipService: TooltipService = inject(TooltipService);
+  private renderer: Renderer2 = inject(Renderer2);
+  private el: ElementRef = inject(ElementRef);
 
   ngOnInit(): void {
+    registerLocaleData(localeDe);
     this._adapter.setLocale('de');
     this.entries = this.dataService.notesData;
-    console.log('JournalNotesComponent: ', this.entries);
 
     // if the list is empty, set the currentEntryIndex to -1 to hide the entry-container
     if (this.entries.length === 0) {
       this.currentEntryIndex = -1;
     } else {
       this.currentEntry = this.entries[0];
+      this.tooltipifiedEntry = JSON.parse(JSON.stringify(this.currentEntry));
     }
+    this.tooltipify();
   }
 
+  // ACTIONS FROM THE TEMPLATE
+
   /**
    * Sets the currentEntry variable when being clicked on in the entries list on the left.
+   * It also modifies the content of the entry by highlighting names and adding tooltips.
+   * Is only called when the entry is not in edit mode or a different entry is selected.
    * @param index The index of the selected entry.
    */
   public selectEntry(index: number): void {
-    this.currentEntryIndex = index;
-    this.currentEntry = this.getEntryAt(index);
-    this.isNewEntry = false;
-    this.isInEditMode = false;
+    if (this.isInEditMode || index !== this.currentEntryIndex) {
+      this.currentEntryIndex = index;
+      this.currentEntry = this.getEntryAt(index);
+      this.isNewEntry = false;
+      this.isInEditMode = false;
+      this.tooltipify();
+    }
   }
 
-  // DONE
-  addEntry(): void {
+  /**
+   * Adds an new empty entry, switches to edit mode and removes the highlighting of the selected entry.
+   */
+  public addEntry(): void {
     this.currentEntry = {
       title: '',
       content: '',
@@ -86,11 +116,18 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
     this.currentEntryIndex = -1;
   }
 
+  /**
+   * Switches to edit mode.
+   */
   public editEntry(): void {
     this.isInEditMode = true;
   }
 
-  // DONE
+  /**
+   * If the current entry is a new entry, it will be prepended to the entries list.
+   * Else it is saved at the current index.
+   * The content is the tooltipified and saved in the database.
+   */
   public saveEntry(): void {
     if (this.isNewEntry) {
       // Prepend the new JournalEntry
@@ -100,9 +137,15 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
     }
     this.isInEditMode = false;
     this.entries[this.currentEntryIndex] = this.currentEntry;
+    this.tooltipify();
     this.uploadNotes();
   }
 
+  /**
+   * Discards the current entry and resets the currentEntry to the last saved version.
+   * If the entry was a new entry, the currentEntryIndex is reset to the last selected entry.
+   * The content is also tooltipified again.
+   */
   public discardEntry(): void {
     if (this.isNewEntry) {
       this.currentEntryIndex = this.backupIndex;
@@ -113,28 +156,87 @@ export class JournalNotesComponent implements OnInit, OnDestroy {
       this.currentEntry = this.getEntryAt(this.currentEntryIndex);
     }
     this.isInEditMode = false;
+    this.tooltipify();
   }
 
-  deleteEntry(): void {
+  /**
+   * Deletes the current entry and removes it from the entries list.
+   * If the last entry was deleted, the currentEntryIndex is reset to -1.
+   * The content is saved in the database.
+   */
+  public deleteEntry(): void {
     this.entries.splice(this.currentEntryIndex, 1);
     if (this.entries.length === 0) {
       this.currentEntryIndex = -1;
     } else {
       this.currentEntryIndex = Math.max(this.currentEntryIndex - 1, 0);
       this.currentEntry = this.getEntryAt(this.currentEntryIndex);
+      // Update the tooltipified entry
+      this.tooltipify();
     }
     this.uploadNotes();
   }
 
+  /**
+   * Returns a deep copy of the entry at the given index.
+   * @param index Defines the index of the entry that should be returned.
+   * @returns A deep copy of the entry at the given index.
+   */
   private getEntryAt(index: number): JournalEntry {
     return JSON.parse(JSON.stringify(this.entries[index]));
   }
 
+  /**
+   * Saves the current entries in the data service.
+   */
   private uploadNotes(): void {
-    console.log('Uploading notes to the server: ', this.entries);
     this.dataService.notesData = this.entries;
   }
 
+  /**
+   * Converts the content of the current entry to a tooltipified version.
+   * It then adds the highlights to the names and sets the tooltip text.
+   * Is called on every refresh of an entry (selecting, saving, discarding, deleting, adding, etc.)
+   */
+  public tooltipify(): void {
+    let result: any = this.tooltipService.tooltipifyEntry(
+      this.getEntryAt(this.currentEntryIndex).content,
+    );
+
+    this.tooltipifiedEntry.content = result.content;
+    this.npcDescriptions = result.npcDescriptions;
+    setTimeout(() => {
+      result.npcs.forEach((name: string) => {
+        this.addHighlightsToText(name);
+      });
+    });
+  }
+
+  /**
+   * Adds a highlight component to the names that are mentioned in the current entry.
+   * Highlights and adds tooltips to the names that are mentioned in the current entry.
+   * @param name The name of the person which is currently highlighted.
+   */
+  private addHighlightsToText(name: string) {
+    // get all elements where a highlight component should be added to
+    const parent = this.el.nativeElement.querySelectorAll('.' + name);
+    parent.forEach((element: any) => {
+      const componentRef =
+        this.creationAnchor.createComponent(HighlightComponent);
+      componentRef.instance.text = name;
+      componentRef.instance.tooltip = this.tooltip;
+      this.renderer.appendChild(element, componentRef.location.nativeElement);
+      // add a mouseover event listener to the highlight component, when it is hovered over, the tooltip-text is set
+      this.renderer.listen(
+        componentRef.location.nativeElement,
+        'mouseover',
+        () => {
+          this.tooltipText = this.npcDescriptions[name];
+        },
+      );
+    });
+  }
+
   ngOnDestroy(): void {
     this.editor.destroy();
   }

+ 4 - 0
src/app/journal/journal.module.ts

@@ -8,6 +8,7 @@ import { ReactiveFormsModule } from '@angular/forms';
 import { MarkdownModule } from 'ngx-markdown';
 import { NgxEditorModule } from 'ngx-editor';
 import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
 import { TranslateModule, TranslatePipe } from '@ngx-translate/core';
 
 // Material Design
@@ -100,6 +101,7 @@ import { CustomSpellsModalComponent } from './journal-spellbook/custom-spells-mo
 import { DurationPipe } from '../../pipes/duration/duration.pipe';
 import { CombinedComponent } from './journal-character/combined/combined.component';
 import { DividerComponent } from '../shared-components/divider/divider.component';
+import { TooltipDirective } from 'src/directives/tooltip/tooltip.directive';
 
 @NgModule({
   declarations: [
@@ -205,6 +207,8 @@ import { DividerComponent } from '../shared-components/divider/divider.component
     NgbCollapseModule,
     MatButtonToggleModule,
     MatIconModule,
+    TooltipDirective,
+    NgbTooltipModule,
   ],
 })
 export class JournalModule {}

+ 1 - 0
src/app/shared-components/highlight/highlight.component.html

@@ -0,0 +1 @@
+<span [ngbTooltip]="tooltip!">{{ text }}</span>

+ 3 - 0
src/app/shared-components/highlight/highlight.component.scss

@@ -0,0 +1,3 @@
+span {
+  font-weight: 600;
+}

+ 23 - 0
src/app/shared-components/highlight/highlight.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HighlightComponent } from './highlight.component';
+
+describe('HighlightComponent', () => {
+  let component: HighlightComponent;
+  let fixture: ComponentFixture<HighlightComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [HighlightComponent]
+    })
+    .compileComponents();
+    
+    fixture = TestBed.createComponent(HighlightComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 15 - 0
src/app/shared-components/highlight/highlight.component.ts

@@ -0,0 +1,15 @@
+import { Component, Input, TemplateRef } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+  selector: 'app-highlight',
+  standalone: true,
+  imports: [CommonModule, NgbTooltipModule],
+  templateUrl: './highlight.component.html',
+  styleUrl: './highlight.component.scss',
+})
+export class HighlightComponent {
+  @Input() text: string = 'Initial';
+  @Input() tooltip!: TemplateRef<any>;
+}

+ 1 - 1
src/assets/i18n/de.json

@@ -860,6 +860,6 @@
     "hint": "Die App befindet sich immer noch in einem Entwicklungsstadium und es können Fehler auftreten",
     "issues": "<p>Fehler und Anmerkungen bitte auf dem <a href='https://gogs.koljastrohm-games.com/Warafear/DNDTools/issues'>Git-Server in Issues</a> vermerken.<p>",
     "okay": "Verstanden",
-    "version": "0.13.0"
+    "version": "0.13.1"
   }
 }

+ 1 - 1
src/assets/i18n/en.json

@@ -855,7 +855,7 @@
     "hint": "The app is still in a development stage and errors may occur",
     "issues": "<p>Please note errors and comments on the <a href='https://gogs.koljastrohm-games.com/Warafear/DNDTools/issues'>Git server in Issues</a>.<p>",
     "okay": "Understood",
-    "version": "0.13.0",
+    "version": "0.13.1",
     "test": "Test"
   }
 }

+ 8 - 0
src/directives/tooltip/tooltip.directive.spec.ts

@@ -0,0 +1,8 @@
+import { TooltipDirective } from './tooltip.directive';
+
+describe('TooltipDirective', () => {
+  it('should create an instance', () => {
+    const directive = new TooltipDirective();
+    expect(directive).toBeTruthy();
+  });
+});

+ 35 - 0
src/directives/tooltip/tooltip.directive.ts

@@ -0,0 +1,35 @@
+import {
+  Directive,
+  ViewContainerRef,
+  ComponentFactoryResolver,
+  Renderer2,
+  ElementRef,
+} from '@angular/core';
+// import { TooltipComponent } from 'src/app/shared-components/tooltip/tooltip/tooltip.component';
+
+@Directive({
+  selector: '[tooltip]',
+  standalone: true,
+})
+export class TooltipDirective {
+  constructor(
+    private viewContainerRef: ViewContainerRef,
+    private componentFactoryResolver: ComponentFactoryResolver,
+    private renderer: Renderer2,
+    private el: ElementRef,
+  ) {
+    // this.showTooltip();
+  }
+
+  // showTooltip() {
+  //   const factory =
+  //     this.componentFactoryResolver.resolveComponentFactory(TooltipComponent);
+  //   const componentRef = this.viewContainerRef.createComponent(factory);
+  //   componentRef.instance.content = 'This is a tooltip';
+
+  //   this.renderer.appendChild(
+  //     this.el.nativeElement,
+  //     componentRef.location.nativeElement,
+  //   );
+  // }
+}

+ 3 - 0
src/interfaces/interfaces.ts

@@ -1,3 +1,5 @@
+import { SafeHtml } from '@angular/platform-browser';
+
 // #region CHARACTER DATA
 export interface Ability {
   name: string;
@@ -186,6 +188,7 @@ export interface Npcs {
 
 export interface Npc {
   name: string;
+  identifier?: string;
   longDescription: string;
   shortDescription: string;
   organization?: string;

+ 16 - 0
src/services/tooltip/tooltip.service.spec.ts

@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { TooltipService } from './tooltip.service';
+
+describe('TooltipService', () => {
+  let service: TooltipService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(TooltipService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});

+ 45 - 0
src/services/tooltip/tooltip.service.ts

@@ -0,0 +1,45 @@
+import { Injectable, inject } from '@angular/core';
+import { JournalEntry, Npcs, Npc } from 'src/interfaces/interfaces';
+import { DataService } from 'src/services/data/data.service';
+
+@Injectable({
+  providedIn: 'root',
+})
+export class TooltipService {
+  private dataService: DataService = inject(DataService);
+
+  public tooltipifyEntry(entry: string): any {
+    const content = this.replaceNpcs(entry);
+    const matches = entry.match(/(?<=@)\w+/g);
+    const uniqueMatches = [...new Set(matches)];
+    // get the descriptions from the data service
+    const npcs = this.dataService.npcs;
+    const npcArray = Object.keys(npcs)
+      .map((key) => npcs[key])
+      .flat();
+    let npcsDescription: any = {};
+
+    uniqueMatches.forEach((name) => {
+      const npc = npcArray.find(
+        (npc: Npc) => npc.name === name || npc.identifier === name,
+      );
+      if (npc) {
+        // Add it to the npcDescription object
+        npcsDescription[name] = npc.shortDescription;
+      } else {
+        // Add a placeholder to the npcDescription object
+        npcsDescription[name] = 'Keine Person mit diesem Name gefunden.';
+      }
+    });
+
+    return {
+      content: content,
+      npcs: uniqueMatches,
+      npcDescriptions: npcsDescription,
+    };
+  }
+
+  private replaceNpcs(content: string): string {
+    return content.replace(/@(\w+)/g, "<span class='$1'></span>");
+  }
+}

+ 4 - 0
src/styles.scss

@@ -270,6 +270,10 @@ input[type="checkbox"]:checked::after {
   padding-left: 0.25rem;
 }
 
+.highlighted {
+  font-weight: 800;
+}
+
 // RICH TEXT EDITOR
 .ProseMirror {
   padding-left: 1rem !important;