ObsiViewer/src/app/editor/components/block/block-inline-toolbar.component.ts
Bruno Charest 9887be548e ```
feat: add collapsible block type and enhance paragraph menu UX

- Added CollapsibleBlockComponent with registration in block host and columns
- Enhanced paragraph block with inline "+" button for column layouts and improved menu positioning
- Updated toggle block to support creating new toggle blocks below with dedicated event handlers
- Improved menu styling with fixed positioning, keyboard navigation (arrows, Enter, Escape), and focus management
- Simplified inline action handling by deleg
2025-11-14 22:29:11 -05:00

253 lines
10 KiB
TypeScript

import { Component, Output, EventEmitter, Input, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
export interface InlineToolbarAction {
type: 'use-ai' | 'checkbox-list' | 'numbered-list' | 'bullet-list' | 'table' | 'image' | 'file' | 'new-page' | 'heading-2' | 'more' | 'drag' | 'menu';
}
@Component({
selector: 'app-block-inline-toolbar',
standalone: true,
imports: [CommonModule],
template: `
<div class="group/block relative flex items-center justify-between gap-2 bg-[var(--editor-bg)] rounded-2xl px-4 py-2 shadow-sm z-[60]">
<!-- Drag handle (visible on hover) -->
@if (showDragHandle) {
<div
class="absolute -left-8 opacity-0 group-hover/block:opacity-100 transition-opacity cursor-grab active:cursor-grabbing"
title="Drag to move\nClick to open menu"
(mouseenter)="showDragTooltip.set(true)"
(mouseleave)="showDragTooltip.set(false)"
(click)="onAction('menu')"
>
<div class="p-1 rounded hover:bg-neutral-700 text-gray-500 hover:text-gray-300">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
<circle cx="9" cy="5" r="1.5"/>
<circle cx="15" cy="5" r="1.5"/>
<circle cx="9" cy="12" r="1.5"/>
<circle cx="15" cy="12" r="1.5"/>
<circle cx="9" cy="19" r="1.5"/>
<circle cx="15" cy="19" r="1.5"/>
</svg>
</div>
<!-- Tooltip -->
@if (showDragTooltip()) {
<div class="absolute left-0 top-full mt-1 px-2 py-1 bg-neutral-800 text-white text-xs rounded whitespace-nowrap z-50 shadow-lg">
Drag to move<br/>Click to open menu
</div>
}
</div>
}
<!-- Input area and inline icons -->
<div class="flex items-center gap-2 w-full">
<div class="flex-1">
<ng-content />
</div>
<!-- Inline icons are only shown when the line is empty (initial prompt state) -->
<div class="flex items-center gap-1 select-none" *ngIf="isEmpty()">
<!-- Use AI -->
<button *ngIf="!actions || actions.includes('use-ai')"
class="p-1.5 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer"
title="Use AI"
(click)="onAction('use-ai')"
type="button"
>
<svg class="w-6 h-6" viewBox="0 0 24 24" aria-label="Assistant AI de rédaction"
fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<!-- Document -->
<rect x="3" y="2.5" width="12.5" height="17" rx="2.2"/>
<!-- Lignes de texte -->
<path d="M6 7h7.5M6 10h7.5M6 13h6"/>
<!-- Étincelle IA -->
<path d="M15.8 4.2v1.6M15 5h1.6M15.3 4.3l1.1 1.1M15.3 5.4l1.1-1.1"/>
<!-- Plume (nib) en bas à droite -->
<path d="M19 13.8l3.2 3.2-5 5-3.2-3.2z"/>
<circle cx="17.8" cy="18.2" r="0.9"/>
<path d="M13.9 18.9l-1.6.5.5-1.6"/>
</svg>
</button>
<!-- Checkbox list amélioré -->
<button *ngIf="!actions || actions.includes('checkbox-list')"
class="p-1.5 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer"
title="Checkbox list"
(click)="onAction('checkbox-list')"
type="button"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<!-- Ligne 1 -->
<rect x="3" y="3" width="5" height="5" rx="1"/>
<path d="M10 6h10"/>
<!-- Ligne 2 -->
<rect x="3" y="10" width="5" height="5" rx="1"/>
<path d="M10 13h10"/>
<!-- Ligne 3 -->
<rect x="3" y="17" width="5" height="5" rx="1"/>
<path d="M10 20h10"/>
</svg>
</button>
<!-- Bullet list -->
<button *ngIf="!actions || actions.includes('bullet-list')"
class="p-1.5 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer"
title="Bullet list"
(click)="onAction('bullet-list')"
type="button"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<!-- Ligne 1 -->
<circle cx="5.5" cy="5.5" r="1.5"/>
<path d="M10 6h10"/>
<!-- Ligne 2 -->
<circle cx="5.5" cy="12.5" r="1.5"/>
<path d="M10 13h10"/>
<!-- Ligne 3 -->
<circle cx="5.5" cy="19.5" r="1.5"/>
<path d="M10 20h10"/>
</svg>
</button>
<!-- Numbered list -->
<button *ngIf="!actions || actions.includes('numbered-list')"
class="p-1.5 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer"
title="Numbered list"
(click)="onAction('numbered-list')"
type="button"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<text x="4" y="7" font-size="5" text-anchor="middle" dominant-baseline="middle" fill="currentColor" stroke="none">1</text>
<path d="M10 7h10"/>
<text x="4" y="14" font-size="5" text-anchor="middle" dominant-baseline="middle" fill="currentColor" stroke="none">2</text>
<path d="M10 14h10"/>
<text x="4" y="21" font-size="5" text-anchor="middle" dominant-baseline="middle" fill="currentColor" stroke="none">3</text>
<path d="M10 21h10"/>
</svg>
</button>
<!-- Table -->
<button *ngIf="!actions || actions.includes('table')"
class="p-1.5 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer"
title="Table"
(click)="onAction('table')"
type="button"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18M9 3v18"/>
</svg>
</button>
<!-- Image -->
<button *ngIf="!actions || actions.includes('image')"
class="p-1.5 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer"
title="Image"
(click)="onAction('image')"
type="button"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
</button>
<!-- File -->
<button *ngIf="!actions || actions.includes('file')"
class="p-1.5 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer"
title="File"
(click)="onAction('file')"
type="button"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9l-7-7z"/>
<path d="M13 2v7h7"/>
</svg>
</button>
<!-- Link/New Page -->
<button *ngIf="!actions || actions.includes('new-page')"
class="p-1.5 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer"
title="Link to page"
(click)="onAction('new-page')"
type="button"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/>
</svg>
</button>
<!-- Heading M -->
<button *ngIf="!actions || actions.includes('heading-2')"
class="px-1.5 py-1 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer font-bold text-xs"
title="Heading 2"
(click)="onAction('heading-2')"
type="button"
>
H<sub class="text-[8px]">M</sub>
</button>
<div *ngIf="!actions || actions.length > 1" class="border-l border-gray-600 mx-2 h-4"></div>
<!-- More items (opens menu) -->
<button *ngIf="!actions || actions.includes('more')"
class="p-1.5 rounded transition-colors text-gray-400 hover:text-gray-100 cursor-pointer"
title="More items"
(click)="onAction('more')"
type="button"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
</div>
</div>
</div>
`,
styles: [`
:host {
display: block;
}
button {
user-select: none;
-webkit-user-select: none;
}
button:active {
transform: scale(0.95);
}
`]
})
export class BlockInlineToolbarComponent {
@Input() placeholder = "Start writing or type '/', '@'";
@Input() isFocused = signal(false);
@Input() isHovered = signal(false);
// New: whether the current line is empty. When true, icons are shown and placeholder is visible.
@Input() isEmpty = signal(true);
// New: whether to show the drag handle (default true, false in columns)
@Input() showDragHandle = true;
// New: list of actions to render; when undefined, render all
@Input() actions: InlineToolbarAction['type'][] | undefined;
@Output() action = new EventEmitter<InlineToolbarAction['type']>();
showDragTooltip = signal(false);
onAction(type: InlineToolbarAction['type']): void {
this.action.emit(type);
}
}