Moved the ability to spawn the card into it's own component.
I think it's time to start on the proper implementation.
This commit is contained in:
parent
7c08a8c301
commit
fc54f8571f
4 changed files with 160 additions and 81 deletions
64
UI/Web/src/app/_service/annotation-card.service.ts
Normal file
64
UI/Web/src/app/_service/annotation-card.service.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import {ApplicationRef, ComponentRef, createComponent, EmbeddedViewRef, inject, Injectable} from '@angular/core';
|
||||||
|
import {AnnotationCardComponent} from '../book-reader/_components/annotation-card/annotation-card.component';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AnnotationCardService {
|
||||||
|
|
||||||
|
private readonly applicationRef = inject(ApplicationRef);
|
||||||
|
|
||||||
|
private componentRef?: ComponentRef<AnnotationCardComponent>;
|
||||||
|
|
||||||
|
show(config: {
|
||||||
|
position: any;
|
||||||
|
annotationText?: string;
|
||||||
|
createdDate?: Date;
|
||||||
|
onMouseEnter?: () => void;
|
||||||
|
onMouseLeave?: () => void;
|
||||||
|
}): ComponentRef<AnnotationCardComponent> {
|
||||||
|
// Remove existing card if present
|
||||||
|
this.hide();
|
||||||
|
|
||||||
|
// Create component using createComponent (Angular 13+ approach)
|
||||||
|
this.componentRef = createComponent(AnnotationCardComponent, {
|
||||||
|
environmentInjector: this.applicationRef.injector
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set inputs using signals
|
||||||
|
this.componentRef.setInput('position', config.position);
|
||||||
|
this.componentRef.setInput('annotationText', config.annotationText || 'This is test text');
|
||||||
|
this.componentRef.setInput('createdDate', config.createdDate || new Date());
|
||||||
|
|
||||||
|
// Set up event handlers
|
||||||
|
if (config.onMouseEnter) {
|
||||||
|
this.componentRef.instance.mouseEnter.subscribe(config.onMouseEnter);
|
||||||
|
}
|
||||||
|
if (config.onMouseLeave) {
|
||||||
|
this.componentRef.instance.mouseLeave.subscribe(config.onMouseLeave);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach to application
|
||||||
|
this.applicationRef.attachView(this.componentRef.hostView);
|
||||||
|
|
||||||
|
// Append to body
|
||||||
|
const domElem = (this.componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement;
|
||||||
|
document.body.appendChild(domElem);
|
||||||
|
|
||||||
|
return this.componentRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
hide(): void {
|
||||||
|
if (this.componentRef) {
|
||||||
|
this.applicationRef.detachView(this.componentRef.hostView);
|
||||||
|
this.componentRef.destroy();
|
||||||
|
this.componentRef = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHoverState(isHovered: boolean): void {
|
||||||
|
if (this.componentRef) {
|
||||||
|
this.componentRef.instance.isHovered.set(isHovered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
background: white;
|
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {Component} from '@angular/core';
|
import {Component, input, model, output} from '@angular/core';
|
||||||
import {UtcToLocaleDatePipe} from "../../../_pipes/utc-to-locale-date.pipe";
|
import {UtcToLocaleDatePipe} from "../../../_pipes/utc-to-locale-date.pipe";
|
||||||
import {DatePipe} from "@angular/common";
|
import {DatePipe} from "@angular/common";
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import {DatePipe} from "@angular/common";
|
||||||
export class AnnotationCardComponent {
|
export class AnnotationCardComponent {
|
||||||
position = input.required<any>();
|
position = input.required<any>();
|
||||||
annotationText = input<string>('This is test text');
|
annotationText = input<string>('This is test text');
|
||||||
createdDate = input<Date>(new Date());
|
createdDate = input<string>('01-01-0001');
|
||||||
isHovered = model<boolean>(false);
|
isHovered = model<boolean>(false);
|
||||||
|
|
||||||
mouseEnter = output<void>();
|
mouseEnter = output<void>();
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import {
|
import {
|
||||||
AfterViewChecked,
|
AfterViewChecked,
|
||||||
Component,
|
Component,
|
||||||
|
ComponentRef,
|
||||||
computed,
|
computed,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
|
inject,
|
||||||
input,
|
input,
|
||||||
model,
|
model,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
|
@ -11,6 +13,8 @@ import {
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {Annotation} from "../../_models/annotation";
|
import {Annotation} from "../../_models/annotation";
|
||||||
|
import {AnnotationCardComponent} from "../annotation-card/annotation-card.component";
|
||||||
|
import {AnnotationCardService} from 'src/app/_service/annotation-card.service';
|
||||||
|
|
||||||
export type HighlightColor = 'blue' | 'green';
|
export type HighlightColor = 'blue' | 'green';
|
||||||
|
|
||||||
|
@ -30,6 +34,9 @@ export class EpubHighlightComponent implements OnInit, AfterViewChecked, OnDestr
|
||||||
|
|
||||||
private resizeObserver?: ResizeObserver;
|
private resizeObserver?: ResizeObserver;
|
||||||
private annotationCardElement?: HTMLElement;
|
private annotationCardElement?: HTMLElement;
|
||||||
|
private annotationCardRef?: ComponentRef<AnnotationCardComponent>;
|
||||||
|
|
||||||
|
private annotationCardService = inject(AnnotationCardService);
|
||||||
|
|
||||||
showAnnotationCard = computed(() => {
|
showAnnotationCard = computed(() => {
|
||||||
const annotation = this.annotation();
|
const annotation = this.annotation();
|
||||||
|
@ -147,90 +154,99 @@ export class EpubHighlightComponent implements OnInit, AfterViewChecked, OnDestr
|
||||||
}
|
}
|
||||||
|
|
||||||
private createOrUpdateAnnotationCard() {
|
private createOrUpdateAnnotationCard() {
|
||||||
|
// const pos = this.cardPosition();
|
||||||
|
// if (!pos) return;
|
||||||
|
//
|
||||||
|
// // Remove existing card if it exists
|
||||||
|
// this.removeAnnotationCard();
|
||||||
|
//
|
||||||
|
// // Create new card element
|
||||||
|
// this.annotationCardElement = document.createElement('div');
|
||||||
|
// this.annotationCardElement.className = `annotation-card ${this.isHovered() ? 'hovered' : ''}`;
|
||||||
|
// this.annotationCardElement.setAttribute('data-position', pos.isRight ? 'right' : 'left');
|
||||||
|
// this.annotationCardElement.style.position = 'absolute';
|
||||||
|
// this.annotationCardElement.style.top = `${pos.top}px`;
|
||||||
|
// this.annotationCardElement.style.left = `${pos.left}px`;
|
||||||
|
// this.annotationCardElement.style.zIndex = '1000';
|
||||||
|
//
|
||||||
|
// // Add event listeners for hover
|
||||||
|
// this.annotationCardElement.addEventListener('mouseenter', () => this.onMouseEnter());
|
||||||
|
// this.annotationCardElement.addEventListener('mouseleave', () => this.onMouseLeave());
|
||||||
|
//
|
||||||
|
// // Create card content
|
||||||
|
// this.annotationCardElement.innerHTML = `
|
||||||
|
// <div class="annotation-content">
|
||||||
|
// <div class="annotation-text">This is test text</div>
|
||||||
|
// <div class="annotation-meta">
|
||||||
|
// <small>10/20/2025</small>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// `;
|
||||||
|
//
|
||||||
|
// // Create connection line
|
||||||
|
// const lineElement = document.createElement('div');
|
||||||
|
// lineElement.className = `connection-line ${this.isHovered() ? 'hovered' : ''}`;
|
||||||
|
// lineElement.style.position = 'absolute';
|
||||||
|
// lineElement.style.left = `${pos.connection.startX}px`;
|
||||||
|
// lineElement.style.top = `${pos.connection.startY}px`;
|
||||||
|
// lineElement.style.width = `${pos.connection.distance}px`;
|
||||||
|
// lineElement.style.height = '2px';
|
||||||
|
// lineElement.style.backgroundColor = '#9ca3af';
|
||||||
|
// lineElement.style.transformOrigin = '0 50%';
|
||||||
|
// lineElement.style.transform = `rotate(${pos.connection.angle}deg)`;
|
||||||
|
// lineElement.style.opacity = this.isHovered() ? '1' : '0.3';
|
||||||
|
// lineElement.style.transition = 'opacity 0.2s ease';
|
||||||
|
// lineElement.style.zIndex = '999';
|
||||||
|
//
|
||||||
|
// // Add dot at the end
|
||||||
|
// const dotElement = document.createElement('div');
|
||||||
|
// dotElement.style.position = 'absolute';
|
||||||
|
// dotElement.style.right = '-3px';
|
||||||
|
// dotElement.style.top = '50%';
|
||||||
|
// dotElement.style.width = '6px';
|
||||||
|
// dotElement.style.height = '6px';
|
||||||
|
// dotElement.style.backgroundColor = '#9ca3af';
|
||||||
|
// dotElement.style.borderRadius = '50%';
|
||||||
|
// dotElement.style.transform = 'translateY(-50%)';
|
||||||
|
//
|
||||||
|
// lineElement.appendChild(dotElement);
|
||||||
|
//
|
||||||
|
// // Append to body
|
||||||
|
// document.body.appendChild(this.annotationCardElement);
|
||||||
|
// document.body.appendChild(lineElement);
|
||||||
|
//
|
||||||
|
// // Store reference to line for updates
|
||||||
|
// (this.annotationCardElement as any).lineElement = lineElement;
|
||||||
|
|
||||||
const pos = this.cardPosition();
|
const pos = this.cardPosition();
|
||||||
if (!pos) return;
|
if (!pos) return;
|
||||||
|
|
||||||
// Remove existing card if it exists
|
// Only create if not already created
|
||||||
this.removeAnnotationCard();
|
if (!this.annotationCardRef) {
|
||||||
|
this.annotationCardRef = this.annotationCardService.show({
|
||||||
// Create new card element
|
position: pos,
|
||||||
this.annotationCardElement = document.createElement('div');
|
annotationText: 'This is test text',
|
||||||
this.annotationCardElement.className = `annotation-card ${this.isHovered() ? 'hovered' : ''}`;
|
createdDate: new Date('10/20/2025'),
|
||||||
this.annotationCardElement.setAttribute('data-position', pos.isRight ? 'right' : 'left');
|
onMouseEnter: () => this.onMouseEnter(),
|
||||||
this.annotationCardElement.style.position = 'absolute';
|
onMouseLeave: () => this.onMouseLeave()
|
||||||
this.annotationCardElement.style.top = `${pos.top}px`;
|
});
|
||||||
this.annotationCardElement.style.left = `${pos.left}px`;
|
}
|
||||||
this.annotationCardElement.style.zIndex = '1000';
|
|
||||||
|
|
||||||
// Add event listeners for hover
|
|
||||||
this.annotationCardElement.addEventListener('mouseenter', () => this.onMouseEnter());
|
|
||||||
this.annotationCardElement.addEventListener('mouseleave', () => this.onMouseLeave());
|
|
||||||
|
|
||||||
// Create card content
|
|
||||||
this.annotationCardElement.innerHTML = `
|
|
||||||
<div class="annotation-content">
|
|
||||||
<div class="annotation-text">This is test text</div>
|
|
||||||
<div class="annotation-meta">
|
|
||||||
<small>10/20/2025</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Create connection line
|
|
||||||
const lineElement = document.createElement('div');
|
|
||||||
lineElement.className = `connection-line ${this.isHovered() ? 'hovered' : ''}`;
|
|
||||||
lineElement.style.position = 'absolute';
|
|
||||||
lineElement.style.left = `${pos.connection.startX}px`;
|
|
||||||
lineElement.style.top = `${pos.connection.startY}px`;
|
|
||||||
lineElement.style.width = `${pos.connection.distance}px`;
|
|
||||||
lineElement.style.height = '2px';
|
|
||||||
lineElement.style.backgroundColor = '#9ca3af';
|
|
||||||
lineElement.style.transformOrigin = '0 50%';
|
|
||||||
lineElement.style.transform = `rotate(${pos.connection.angle}deg)`;
|
|
||||||
lineElement.style.opacity = this.isHovered() ? '1' : '0.3';
|
|
||||||
lineElement.style.transition = 'opacity 0.2s ease';
|
|
||||||
lineElement.style.zIndex = '999';
|
|
||||||
|
|
||||||
// Add dot at the end
|
|
||||||
const dotElement = document.createElement('div');
|
|
||||||
dotElement.style.position = 'absolute';
|
|
||||||
dotElement.style.right = '-3px';
|
|
||||||
dotElement.style.top = '50%';
|
|
||||||
dotElement.style.width = '6px';
|
|
||||||
dotElement.style.height = '6px';
|
|
||||||
dotElement.style.backgroundColor = '#9ca3af';
|
|
||||||
dotElement.style.borderRadius = '50%';
|
|
||||||
dotElement.style.transform = 'translateY(-50%)';
|
|
||||||
|
|
||||||
lineElement.appendChild(dotElement);
|
|
||||||
|
|
||||||
// Append to body
|
|
||||||
document.body.appendChild(this.annotationCardElement);
|
|
||||||
document.body.appendChild(lineElement);
|
|
||||||
|
|
||||||
// Store reference to line for updates
|
|
||||||
(this.annotationCardElement as any).lineElement = lineElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeAnnotationCard() {
|
private removeAnnotationCard() {
|
||||||
if (this.annotationCardElement) {
|
// if (this.annotationCardElement) {
|
||||||
// Remove associated line element
|
// // Remove associated line element
|
||||||
const lineElement = (this.annotationCardElement as any).lineElement;
|
// const lineElement = (this.annotationCardElement as any).lineElement;
|
||||||
if (lineElement) {
|
// if (lineElement) {
|
||||||
lineElement.remove();
|
// lineElement.remove();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
this.annotationCardElement.remove();
|
// this.annotationCardElement.remove();
|
||||||
this.annotationCardElement = undefined;
|
// this.annotationCardElement = undefined;
|
||||||
}
|
// }
|
||||||
}
|
if (this.annotationCardRef) {
|
||||||
|
this.annotationCardService.hide();
|
||||||
private updateLineOpacity() {
|
this.annotationCardRef = undefined;
|
||||||
if (this.annotationCardElement) {
|
|
||||||
const lineElement = (this.annotationCardElement as any).lineElement;
|
|
||||||
if (lineElement) {
|
|
||||||
lineElement.style.opacity = this.isHovered() ? '1' : '0.3';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue