diff --git a/src/app/editor/components/block/block-context-menu.component.ts b/src/app/editor/components/block/block-context-menu.component.ts index 3ee9e8d..52f36c7 100644 --- a/src/app/editor/components/block/block-context-menu.component.ts +++ b/src/app/editor/components/block/block-context-menu.component.ts @@ -5,7 +5,7 @@ import { DocumentService } from '../../services/document.service'; import { CodeThemeService } from '../../services/code-theme.service'; export interface MenuAction { - type: 'comment' | 'add' | 'convert' | 'background' | 'lineColor' | 'borderColor' | 'codeTheme' | 'codeLanguage' | 'copyCode' | 'toggleWrap' | 'toggleLineNumbers' | 'addCaption' | 'tableLayout' | 'copyTable' | 'filterTable' | 'importCSV' | 'tableHelp' | 'insertColumn' | 'imageAspectRatio' | 'imageAlignment' | 'imageReplace' | 'imageRotate' | 'imageSetPreview' | 'imageOCR' | 'imageDownload' | 'imageViewFull' | 'imageOpenTab' | 'imageInfo' | 'duplicate' | 'copy' | 'lock' | 'copyLink' | 'delete' | 'align' | 'indent'; + type: 'comment' | 'add' | 'convert' | 'background' | 'lineColor' | 'borderColor' | 'codeTheme' | 'codeLanguage' | 'copyCode' | 'toggleWrap' | 'toggleLineNumbers' | 'addCaption' | 'tableLayout' | 'copyTable' | 'filterTable' | 'importCSV' | 'tableHelp' | 'insertColumn' | 'imageAspectRatio' | 'imageAlignment' | 'imageDefaultSize' | 'imageReplace' | 'imageRotate' | 'imageSetPreview' | 'imageOCR' | 'imageDownload' | 'imageViewFull' | 'imageOpenTab' | 'imageInfo' | 'duplicate' | 'copy' | 'lock' | 'copyLink' | 'delete' | 'align' | 'indent'; payload?: any; } @@ -25,43 +25,99 @@ export interface MenuAction { (click)="$event.stopPropagation()" (contextmenu)="$event.preventDefault()" > - +
- -
- - + @if (block.type === 'image') { + + + + +
+ + + + } @else { + +
+ + + }
@@ -1066,17 +1122,27 @@ export class BlockContextMenuComponent implements OnChanges { { name: 'Sky 300', value: '#7dd3fc' } ]; - onAction(type: MenuAction['type']): void { + onAction(type: MenuAction['type'], payload?: any): void { if (type === 'copy') { // Copy block to clipboard this.copyBlockToClipboard(); } else { - // Emit action for parent to handle (including comment) - this.action.emit({ type }); + // Emit action for parent to handle (including ratios/alignment payload) + this.action.emit({ type, payload }); } this.close.emit(); } + onAlignImage(alignment: 'left' | 'center' | 'right'): void { + this.action.emit({ type: 'imageAlignment', payload: { alignment } }); + this.close.emit(); + } + + onImageDefaultSize(): void { + this.action.emit({ type: 'imageDefaultSize' }); + this.close.emit(); + } + private copyBlockToClipboard(): void { // Store in service for paste this.clipboardData = JSON.parse(JSON.stringify(this.block)); diff --git a/src/app/editor/components/block/block-host.component.ts b/src/app/editor/components/block/block-host.component.ts index 20e8001..c8b5ef6 100644 --- a/src/app/editor/components/block/block-host.component.ts +++ b/src/app/editor/components/block/block-host.component.ts @@ -9,6 +9,8 @@ import { CommentStoreService } from '../../services/comment-store.service'; import { Overlay, OverlayModule, OverlayRef } from '@angular/cdk/overlay'; import { ComponentPortal, PortalModule } from '@angular/cdk/portal'; import { BlockCommentComposerComponent } from '../comment/block-comment-composer.component'; +import { ImageInfoModalComponent } from '../image/image-info-modal.component'; +import { ImageCaptionModalComponent } from '../image/image-caption-modal.component'; import { BlockInitialMenuComponent, BlockMenuAction } from './block-initial-menu.component'; // Import block components @@ -243,7 +245,9 @@ export class BlockHostComponent implements OnDestroy { private readonly overlay = inject(Overlay); private readonly host = inject(ElementRef); private commentRef?: OverlayRef; - private commentSub?: { unsubscribe: () => void } | null = null; + private commentSub?: OverlayRef | { unsubscribe: () => void } | null = null; + private imageInfoRef?: OverlayRef; + private imageCaptionRef?: OverlayRef; readonly isActive = signal(false); readonly menuVisible = signal(false); @@ -356,9 +360,14 @@ export class BlockHostComponent implements OnDestroy { return; } else { // Dropping in the gap BETWEEN columns - insert as new column - const columnsContainerEl = columnsBlockEl.querySelector('[class*="columns"]'); + let columnsContainerEl = columnsBlockEl.querySelector('[data-column-id]') as HTMLElement | null; + columnsContainerEl = (columnsContainerEl?.parentElement as HTMLElement) || columnsContainerEl; + if (!columnsContainerEl) { + // Fallback: use the block element itself + columnsContainerEl = columnsBlockEl as HTMLElement; + } if (columnsContainerEl) { - const containerRect = columnsContainerEl.getBoundingClientRect(); + const containerRect = (columnsContainerEl as HTMLElement).getBoundingClientRect(); const props = columnsBlock.props as any; const columns = [...(props.columns || [])]; @@ -403,91 +412,44 @@ export class BlockHostComponent implements OnDestroy { } } } - } - } - } - - const blocks = this.documentService.blocks(); - - // Handle column creation/addition - if (mode === 'column-left' || mode === 'column-right') { - const targetBlock = blocks[to]; - if (!targetBlock) return; - - // Create copy of dragged block - const draggedBlockCopy = JSON.parse(JSON.stringify(this.block)); - - // Find the target block's position - const targetIndex = blocks.findIndex(b => b.id === targetBlock.id); - - // Check if target is already a columns block - if (targetBlock.type === 'columns') { - // Add new column to existing columns block - const columnsProps = targetBlock.props as any; - const currentColumns = columnsProps.columns || []; - const newColumnWidth = 100 / (currentColumns.length + 1); - - // Recalculate existing column widths - const updatedColumns = currentColumns.map((col: any) => ({ - ...col, - width: newColumnWidth - })); - - // Add new column - const newColumn = { - id: this.generateId(), - blocks: [draggedBlockCopy], - width: newColumnWidth - }; - - if (mode === 'column-left') { - updatedColumns.unshift(newColumn); } else { - updatedColumns.push(newColumn); + // Not a columns block: detect lateral drop on a normal block to create a 2-column layout + const targetWrapper = target.closest('.block-wrapper[data-block-id]') as HTMLElement | null; + const targetBlockId = targetWrapper?.getAttribute('data-block-id') || null; + if (targetWrapper && targetBlockId && targetBlockId !== this.block.id) { + const contentEl = targetWrapper.querySelector('.block-content') as HTMLElement | null; + const rect = (contentEl || targetWrapper).getBoundingClientRect(); + const relX = e.clientX - rect.left; + const sideThreshold = Math.min(80, Math.max(40, rect.width * 0.15)); // 15% width, clamped 40-80px + const onLeft = relX <= sideThreshold; + const onRight = relX >= rect.width - sideThreshold; + if (onLeft || onRight) { + const all = this.documentService.blocks(); + const targetIndex = all.findIndex(b => b.id === targetBlockId); + if (targetIndex >= 0) { + // Prepare new columns block with dragged + target in the right order + const draggedCopy = JSON.parse(JSON.stringify(this.block)); + const targetBlock = all[targetIndex]; + const columns = onLeft + ? [ { id: this.generateId(), blocks: [draggedCopy], width: 50 }, { id: this.generateId(), blocks: [JSON.parse(JSON.stringify(targetBlock))], width: 50 } ] + : [ { id: this.generateId(), blocks: [JSON.parse(JSON.stringify(targetBlock))], width: 50 }, { id: this.generateId(), blocks: [draggedCopy], width: 50 } ]; + const newColumnsBlock = this.documentService.createBlock('columns', { columns }); + // Insert new columns block before the target, then delete old dragged + target blocks + const beforeId = targetIndex > 0 ? all[targetIndex - 1].id : null; + this.documentService.insertBlock(beforeId, newColumnsBlock); + this.documentService.deleteBlock(targetBlockId); + this.documentService.deleteBlock(this.block.id); + this.selectionService.setActive(draggedCopy.id); + return; + } + } + } } - - // Update the columns block - this.documentService.updateBlockProps(targetBlock.id, { - columns: updatedColumns - }); - - // Delete dragged block - this.documentService.deleteBlock(this.block.id); - this.selectionService.setActive(targetBlock.id); - return; } - - // Create new columns block with two columns - const targetBlockCopy = JSON.parse(JSON.stringify(targetBlock)); - const newColumnsBlock = this.documentService.createBlock('columns', { - columns: mode === 'column-left' - ? [ - { id: this.generateId(), blocks: [draggedBlockCopy], width: 50 }, - { id: this.generateId(), blocks: [targetBlockCopy], width: 50 } - ] - : [ - { id: this.generateId(), blocks: [targetBlockCopy], width: 50 }, - { id: this.generateId(), blocks: [draggedBlockCopy], width: 50 } - ] - }); - - // Delete both blocks - this.documentService.deleteBlock(this.block.id); - this.documentService.deleteBlock(targetBlock.id); - - // Insert columns block at target position - if (targetIndex > 0) { - const beforeBlockId = this.documentService.blocks()[targetIndex - 1]?.id || null; - this.documentService.insertBlock(beforeBlockId, newColumnsBlock); - } else { - this.documentService.insertBlock(null, newColumnsBlock); - } - - this.selectionService.setActive(newColumnsBlock.id); - return; } // Handle regular line move + const blocks = this.documentService.blocks(); let toIndex = to; if (toIndex > from) toIndex = toIndex - 1; if (toIndex < 0) toIndex = 0; @@ -740,16 +702,8 @@ export class BlockHostComponent implements OnDestroy { } break; case 'addCaption': - // For Table/Image blocks - add or edit caption if (this.block.type === 'table' || this.block.type === 'image') { - const currentCaption = (this.block.props as any)?.caption || ''; - const caption = prompt(`Enter ${this.block.type} caption:`, currentCaption); - if (caption !== null) { - this.documentService.updateBlockProps(this.block.id, { - ...this.block.props, - caption: caption.trim() || undefined - }); - } + this.openCaptionModal(); } break; case 'tableLayout': @@ -843,19 +797,29 @@ export class BlockHostComponent implements OnDestroy { case 'imageAspectRatio': if (this.block.type === 'image') { const { ratio } = action.payload || {}; - this.documentService.updateBlockProps(this.block.id, { - ...this.block.props, - aspectRatio: ratio - }); + const patch: any = { ...this.block.props, aspectRatio: ratio }; + if (ratio && ratio !== 'free') { + patch.height = undefined; // let CSS aspect-ratio compute height from width + } + this.documentService.updateBlockProps(this.block.id, patch); } break; case 'imageAlignment': if (this.block.type === 'image') { const { alignment } = action.payload || {}; + const patch: any = { ...this.block.props, alignment }; + if (alignment === 'full') { patch.width = undefined; patch.height = undefined; } + this.documentService.updateBlockProps(this.block.id, patch); + } + break; + case 'imageDefaultSize': + if (this.block.type === 'image') { this.documentService.updateBlockProps(this.block.id, { ...this.block.props, - alignment - }); + width: undefined, + height: undefined, + aspectRatio: 'free' + } as any); } break; case 'imageReplace': @@ -927,9 +891,7 @@ export class BlockHostComponent implements OnDestroy { break; case 'imageInfo': if (this.block.type === 'image') { - const p: any = this.block.props || {}; - const info = `URL: ${p.src}\nAlt: ${p.alt || ''}\nSize: ${p.width || '-'} x ${p.height || '-'} px\nAspect: ${p.aspectRatio || 'free'}\nAlignment: ${p.alignment || 'center'}\nRotation: ${p.rotation || 0}°`; - alert(info); + this.openImageInfo(); } break; case 'comment': @@ -1000,8 +962,60 @@ export class BlockHostComponent implements OnDestroy { this.commentRef.keydownEvents().subscribe((e) => { if ((e as KeyboardEvent).key === 'Escape') this.closeComments(); }); } closeComments(): void { - if (this.commentSub) { try { this.commentSub.unsubscribe(); } catch {} this.commentSub = null; } + if (this.commentSub) { try { (this.commentSub as any).unsubscribe?.(); } catch {} this.commentSub = null; } if (this.commentRef) { this.commentRef.dispose(); this.commentRef = undefined; } } + private openImageInfo(): void { + this.closeImageInfo(); + const anchor = this.host.nativeElement.querySelector(`[data-block-id="${this.block.id}"]`) as HTMLElement || this.host.nativeElement; + const pos = this.overlay.position().flexibleConnectedTo(anchor).withPositions([ + { originX: 'center', originY: 'center', overlayX: 'center', overlayY: 'center' } + ]); + this.imageInfoRef = this.overlay.create({ hasBackdrop: true, backdropClass: 'cdk-overlay-dark-backdrop', positionStrategy: pos, panelClass: 'nimbus-menu-panel' }); + const portal = new ComponentPortal(ImageInfoModalComponent); + const ref = this.imageInfoRef.attach(portal); + const p: any = this.block.props || {}; + ref.instance.src = p.src || ''; + ref.instance.width = p.width; + ref.instance.height = p.height; + ref.instance.aspect = p.aspectRatio; + ref.instance.alignment = p.alignment; + ref.instance.rotation = p.rotation; + const sub = ref.instance.close.subscribe(() => this.closeImageInfo()); + this.imageInfoRef.backdropClick().subscribe(() => this.closeImageInfo()); + this.imageInfoRef.keydownEvents().subscribe((e) => { if ((e as KeyboardEvent).key === 'Escape') this.closeImageInfo(); }); + this.commentSub = { unsubscribe: () => { try { sub.unsubscribe(); } catch {} } } as any; + } + private closeImageInfo(): void { + if (this.imageInfoRef) { this.imageInfoRef.dispose(); this.imageInfoRef = undefined; } + } + + private openCaptionModal(): void { + this.closeCaptionModal(); + const anchor = this.host.nativeElement.querySelector(`[data-block-id="${this.block.id}"]`) as HTMLElement || this.host.nativeElement; + const pos = this.overlay.position().flexibleConnectedTo(anchor).withPositions([ + { originX: 'center', originY: 'center', overlayX: 'center', overlayY: 'center' } + ]); + this.imageCaptionRef = this.overlay.create({ hasBackdrop: true, backdropClass: 'cdk-overlay-dark-backdrop', positionStrategy: pos, panelClass: 'nimbus-menu-panel' }); + const portal = new ComponentPortal(ImageCaptionModalComponent); + const ref = this.imageCaptionRef.attach(portal); + const currentCaption = (this.block.props as any)?.caption || ''; + ref.instance.caption = currentCaption; + ref.instance.title = this.block.type === 'table' ? 'Table caption' : 'Image caption'; + const onSave = ref.instance.save.subscribe((caption: string) => { + this.documentService.updateBlockProps(this.block.id, { + ...this.block.props, + caption: (caption || '').trim() || undefined + } as any); + this.closeCaptionModal(); + }); + const onCancel = ref.instance.cancel.subscribe(() => this.closeCaptionModal()); + this.imageCaptionRef.backdropClick().subscribe(() => this.closeCaptionModal()); + this.imageCaptionRef.keydownEvents().subscribe((e) => { if ((e as KeyboardEvent).key === 'Escape') this.closeCaptionModal(); }); + this.commentSub = { unsubscribe: () => { try { onSave.unsubscribe(); onCancel.unsubscribe(); } catch {} } } as any; + } + private closeCaptionModal(): void { + if (this.imageCaptionRef) { this.imageCaptionRef.dispose(); this.imageCaptionRef = undefined; } + } ngOnDestroy(): void { this.closeComments(); } } diff --git a/src/app/editor/components/block/blocks/columns-block.component.ts b/src/app/editor/components/block/blocks/columns-block.component.ts index 02e7488..7abca51 100644 --- a/src/app/editor/components/block/blocks/columns-block.component.ts +++ b/src/app/editor/components/block/blocks/columns-block.component.ts @@ -416,6 +416,44 @@ export class ColumnsBlockComponent { this.duplicateBlockInColumns(block.id); } + // Handle add action inside columns + if (action.type === 'add') { + const pos = (action.payload || {}).position as 'above' | 'below' | 'left' | 'right' | undefined; + if (pos) { + if (pos === 'above' || pos === 'below') { + // Insert paragraph in same column before/after the selected block + const updatedColumns = this.props.columns.map((column) => { + const idx = column.blocks.findIndex(b => b.id === block.id); + if (idx === -1) return column; + const newBlock = this.documentService.createBlock('paragraph', { text: '' }); + const newBlocks = [...column.blocks]; + const insertAt = pos === 'above' ? idx : idx + 1; + newBlocks.splice(insertAt, 0, newBlock); + return { ...column, blocks: newBlocks }; + }); + this.update.emit({ columns: updatedColumns }); + this.closeMenu(); + return; + } + if (pos === 'left' || pos === 'right') { + // Add a new column on left/right with a new paragraph + const newParagraph = this.documentService.createBlock('paragraph', { text: '' }); + const cols = [...this.props.columns]; + const newWidth = 100 / (cols.length + 1); + const resized = cols.map(col => ({ ...col, width: newWidth })); + const insertion = { id: this.generateId(), blocks: [newParagraph], width: newWidth } as any; + const targetColIndex = resized.findIndex(c => c.blocks.some(b => b.id === block.id)); + if (targetColIndex >= 0) { + if (pos === 'left') resized.splice(targetColIndex, 0, insertion); + else resized.splice(targetColIndex + 1, 0, insertion); + this.update.emit({ columns: resized }); + this.closeMenu(); + return; + } + } + } + } + this.closeMenu(); } diff --git a/src/app/editor/components/block/blocks/image-block.component.ts b/src/app/editor/components/block/blocks/image-block.component.ts index 1e4e42e..d5f2a79 100644 --- a/src/app/editor/components/block/blocks/image-block.component.ts +++ b/src/app/editor/components/block/blocks/image-block.component.ts @@ -53,6 +53,40 @@ import { ImageUploadService } from '../../../services/image-upload.service';
+
Align
+
+ + + +
+ + +
Aspect
@@ -69,14 +103,24 @@ import { ImageUploadService } from '../../../services/image-upload.service'; } @if (showHandles()) { -
-
-
-
-
-
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
} @if (props.caption) { @@ -127,27 +171,22 @@ import { ImageUploadService } from '../../../services/image-upload.service'; .resize-handle { position: absolute; - background: #ffffff; - border: 2px solid #9ca3af; /* gray-400 */ - border-radius: 50%; + background: rgba(0,0,0,0.78); + color: #e5e7eb; + border: 1px solid rgba(255,255,255,0.25); + border-radius: 9999px; cursor: pointer; z-index: 10; - transition: transform 0.2s; + transition: transform 0.2s, background 0.2s; } .resize-handle:hover { transform: scale(1.2); } - .resize-handle.corner { - width: 12px; - height: 12px; - } - - .resize-handle.edge { - width: 10px; - height: 10px; - } + .resize-handle.corner { width: 20px; height: 20px; display:flex; align-items:center; justify-content:center; } + .resize-handle.edge { width: 20px; height: 20px; display:flex; align-items:center; justify-content:center; } + .rh-ico { width: 12px; height: 12px; opacity: 0.9; } .resize-handle.top-left { top: -6px; @@ -243,6 +282,8 @@ import { ImageUploadService } from '../../../services/image-upload.service'; background: #ffffff; } .qa-btn:hover { background: #f3f4f6; } + .qa-icon-btn { width: 28px; height: 28px; display:flex; align-items:center; justify-content:center; border-radius: 6px; border: 1px solid #e5e7eb; background: #ffffff; } + .qa-icon-btn:hover { background: #f3f4f6; } `] }) export class ImageBlockComponent { @@ -394,11 +435,25 @@ export class ImageBlockComponent { return (this.props.aspectRatio || 'free') === ratio; } onAspect(ratio: string) { - this.update.emit({ ...this.props, aspectRatio: ratio }); + const patch: any = { ...this.props, aspectRatio: ratio }; + if (ratio && ratio !== 'free') { + patch.height = undefined; + } + this.update.emit(patch); } onCrop() { alert('Crop coming soon!'); } + setAlignment(a: 'left' | 'center' | 'right' | 'full') { + const patch: any = { ...this.props, alignment: a }; + if (a === 'full') { patch.width = undefined; patch.height = undefined; } + this.update.emit(patch); + this.showQuick.set(false); + } + defaultSize() { + this.update.emit({ ...this.props, width: undefined, height: undefined, aspectRatio: 'free' } as any); + this.showQuick.set(false); + } openSettings(ev: MouseEvent) { ev.stopPropagation(); const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect(); @@ -432,25 +487,26 @@ export class ImageBlockComponent { let newWidth = this.startWidth; let newHeight = this.startHeight; - - // Calculer les nouvelles dimensions selon la direction - if (this.resizeDirection.includes('e')) newWidth = this.startWidth + deltaX; - if (this.resizeDirection.includes('w')) newWidth = this.startWidth - deltaX; - if (this.resizeDirection.includes('s')) newHeight = this.startHeight + deltaY; - if (this.resizeDirection.includes('n')) newHeight = this.startHeight - deltaY; - - // Limites min/max - newWidth = Math.max(100, Math.min(1200, newWidth)); - newHeight = Math.max(100, Math.min(1200, newHeight)); - - // Si aspect ratio défini, maintenir la proportion - if (this.props.aspectRatio && this.props.aspectRatio !== 'free') { - const ratio = this.getAspectRatioValue(); - if (ratio) { - newHeight = newWidth / ratio; - } + const dir = this.resizeDirection; + const isCorner = dir === 'nw' || dir === 'ne' || dir === 'sw' || dir === 'se'; + const hasFixedRatio = !!(this.props.aspectRatio && this.props.aspectRatio !== 'free'); + + // Compute tentative size per axis + if (dir.includes('e')) newWidth = this.startWidth + deltaX; + if (dir.includes('w')) newWidth = this.startWidth - deltaX; + if (dir.includes('s')) newHeight = this.startHeight + deltaY; + if (dir.includes('n')) newHeight = this.startHeight - deltaY; + + // Clamp + newWidth = Math.max(100, Math.min(1600, newWidth)); + newHeight = Math.max(100, Math.min(1600, newHeight)); + + // Maintain aspect ratio for corner handles: if no fixed ratio, keep initial ratio + if (isCorner) { + const ratio = hasFixedRatio ? (this.getAspectRatioValue() || (this.startWidth / this.startHeight)) : (this.startWidth / this.startHeight); + if (ratio > 0) newHeight = Math.round(newWidth / ratio); } - + this.update.emit({ ...this.props, width: Math.round(newWidth), diff --git a/src/app/editor/components/image/image-caption-modal.component.ts b/src/app/editor/components/image/image-caption-modal.component.ts new file mode 100644 index 0000000..e8f4b28 --- /dev/null +++ b/src/app/editor/components/image/image-caption-modal.component.ts @@ -0,0 +1,38 @@ +import { Component, ChangeDetectionStrategy, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'app-image-caption-modal', + standalone: true, + imports: [CommonModule, FormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+
+

{{ title || 'Image caption' }}

+ + +
+ + +
+
+
+ `, +}) +export class ImageCaptionModalComponent { + @Input() caption: string = ''; + @Input() title: string = ''; + @Output() save = new EventEmitter(); + @Output() cancel = new EventEmitter(); + isDark(): boolean { try { return document.documentElement.classList.contains('dark'); } catch { return false; } } + onBackdrop(e: MouseEvent) { if (e.target === e.currentTarget) this.cancel.emit(); } +} diff --git a/src/app/editor/components/image/image-info-modal.component.ts b/src/app/editor/components/image/image-info-modal.component.ts new file mode 100644 index 0000000..cc7b8ff --- /dev/null +++ b/src/app/editor/components/image/image-info-modal.component.ts @@ -0,0 +1,92 @@ +import { Component, ChangeDetectionStrategy, EventEmitter, Input, Output } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-image-info-modal', + standalone: true, + imports: [CommonModule], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+
+ +
+
🖼️
+
+

Image info

+
{{ src }}
+
+
+ +
+
Displayed size: {{ width || 'auto' }} × {{ height || 'auto' }} px
+
Natural size: {{ natW || '—' }} × {{ natH || '—' }} px
+
Aspect: {{ aspect || 'free' }}
+
Alignment: {{ alignment || 'center' }}
+
Rotation: {{ rotation || 0 }}°
+
Type: {{ typeHint }}
+
+ +
+ + +
+
+
+ `, +}) +export class ImageInfoModalComponent { + @Input() src: string = ''; + @Input() width: number | undefined; + @Input() height: number | undefined; + @Input() aspect: string | undefined; + @Input() alignment: string | undefined; + @Input() rotation: number | undefined; + @Output() close = new EventEmitter(); + + natW: number | null = null; + natH: number | null = null; + + ngOnInit() { + if (this.src) { + const img = new Image(); + img.onload = () => { + this.natW = img.naturalWidth; + this.natH = img.naturalHeight; + }; + img.src = this.src; + } + } + + get typeHint(): string { + const s = (this.src || '').toLowerCase(); + if (s.endsWith('.png')) return 'PNG'; + if (s.endsWith('.jpg') || s.endsWith('.jpeg')) return 'JPEG'; + if (s.endsWith('.gif')) return 'GIF'; + if (s.endsWith('.webp')) return 'WEBP'; + return 'Image'; + } + + isDark(): boolean { try { return document.documentElement.classList.contains('dark'); } catch { return false; } } + onBackdrop(e: MouseEvent) { if (e.target === e.currentTarget) this.close.emit(); } + openInNewTab() { if (this.src) window.open(this.src, '_blank', 'noopener'); } + download() { + if (!this.src) return; + const a = document.createElement('a'); + a.href = this.src; + a.download = this.src.split('/').pop() || 'image'; + document.body.appendChild(a); + a.click(); + a.remove(); + } +} diff --git a/vault/.obsidian/workspace.json b/vault/.obsidian/workspace.json index eefa591..cc7cd3f 100644 --- a/vault/.obsidian/workspace.json +++ b/vault/.obsidian/workspace.json @@ -181,6 +181,8 @@ }, "active": "aaf62e01f34df49b", "lastOpenFiles": [ + "attachments/nimbus/2025/1111/img-pasted-20251111-120357-ke3ao3.png", + "attachments/nimbus/2025/1111", "attachments/nimbus/2025/1110/img-bridger-tower-vejbbvtdgcm-unsplash-jpg-20251110-175132-92f6k8.png", "attachments/nimbus/2025/1110/img-image_1-png-20251110-154537-gaoaou.png", "attachments/nimbus/2025/1110/img-logo_obsiviewer-png-20251110-151755-r8r5qq.png", @@ -218,7 +220,6 @@ "big/note_497.md.bak", "big/note_498.md.bak", "big/note_495.md.bak", - "big/note_496.md.bak", "mixe/Dessin-02.png", "Dessin-02.png", "mixe/Claude_ObsiViewer_V1.png", @@ -226,7 +227,6 @@ "Drawing-20251028-1452.png", "dessin.svg", "dessin.png", - "dessin_05.svg", "Untitled.canvas" ] } \ No newline at end of file diff --git a/vault/attachments/nimbus/2025/1111/img-pasted-20251111-120357-ke3ao3.png b/vault/attachments/nimbus/2025/1111/img-pasted-20251111-120357-ke3ao3.png new file mode 100644 index 0000000..87c8951 Binary files /dev/null and b/vault/attachments/nimbus/2025/1111/img-pasted-20251111-120357-ke3ao3.png differ