# @libs-ui/components-popover

> Component/Directive hiển thị nội dung nổi (popover/tooltip) gắn liền với phần tử gốc, tự động định vị thông minh, hỗ trợ nhiều chế độ kích hoạt và nội dung tùy chỉnh.

## Giới thiệu

`@libs-ui/components-popover` cung cấp giải pháp hiển thị thông tin ngữ cảnh linh hoạt trong ứng dụng Angular. Lib hỗ trợ cả hai cách dùng: dạng **Directive** gắn trực tiếp vào phần tử HTML bất kỳ, hoặc dạng **Component Selector** để bọc nội dung phức tạp. Popover tự động tính toán vị trí tối ưu dựa trên viewport, fallback sang hướng khác nếu không đủ chỗ, và tự dọn dẹp khi component bị destroy.

## Tính năng

- ✅ **Hai cách dùng**: Directive (`[LibsUiComponentsPopoverDirective]`) và Component Selector (`libs_ui-components-popover`)
- ✅ **Tự động định vị thông minh**: Tính toán hướng tốt nhất dựa trên viewport, tự fallback nếu bị tràn
- ✅ **Nhiều chế độ kích hoạt**: `hover`, `click`, `click-toggle`, `click_open_and_click_panel_content_hidden`
- ✅ **Hỗ trợ `ng-template`**: Truyền nội dung phức tạp qua `TemplateRef`
- ✅ **Chế độ text-ellipsis**: Chỉ hiển thị khi text bị cắt ngắn (`type="text"`)
- ✅ **FunctionControl API**: Điều khiển popover từ bên ngoài qua `outFunctionsControl`
- ✅ **Hỗ trợ iframe**: Render popover vào parent document khi chạy trong iframe
- ✅ **Tự cleanup**: Tự dọn dẹp overlay khi component bị destroy, không memory leak
- ✅ **OnPush + Angular Signals**

## Khi nào sử dụng

- Hiển thị tooltip thông tin bổ sung khi di chuột qua icon hoặc văn bản bị truncate
- Hiển thị dropdown menu hoặc panel tùy chọn khi nhấn vào nút
- Hiển thị card chi tiết (user profile, product info) khi hover
- Xây dựng các component dropdown tùy chỉnh cần định vị thông minh
- Thông báo hướng dẫn, hint tại chỗ cho người dùng

## Cài đặt

```bash
npm install @libs-ui/components-popover
```

## Import

```typescript
import { LibsUiComponentsPopoverComponent } from '@libs-ui/components-popover';

@Component({
  standalone: true,
  imports: [LibsUiComponentsPopoverComponent],
  // ...
})
export class YourComponent {}
```

> **Lưu ý**: `LibsUiComponentsPopoverComponent` bao gồm cả selector dạng Directive (`[LibsUiComponentsPopoverDirective]`). Chỉ cần import một lần cho cả hai cách dùng.

## Ví dụ sử dụng

### Ví dụ 1 — Dạng Directive (Tooltip đơn giản)

```typescript
import { LibsUiComponentsPopoverComponent } from '@libs-ui/components-popover';

@Component({
  standalone: true,
  imports: [LibsUiComponentsPopoverComponent],
  template: `
    <span
      LibsUiComponentsPopoverDirective
      [config]="{ content: 'Tên đầy đủ: Nguyễn Văn A', direction: 'bottom' }">
      Nguyễn V. A
    </span>
  `,
})
export class BasicTooltipComponent {}
```

### Ví dụ 2 — Dạng Component Selector với nội dung phức tạp

```typescript
import { LibsUiComponentsPopoverComponent } from '@libs-ui/components-popover';
import { IPopoverFunctionControlEvent } from '@libs-ui/components-popover';

@Component({
  standalone: true,
  imports: [LibsUiComponentsPopoverComponent],
  template: `
    <libs_ui-components-popover
      [config]="popoverConfig"
      [mode]="'click-toggle'"
      (outEvent)="handlerPopoverEvent($event)"
      (outFunctionsControl)="handlerFunctionsControl($event)">
      <button class="px-4 py-2 bg-blue-600 text-white rounded">
        Mở menu
      </button>
    </libs_ui-components-popover>
  `,
})
export class ClickPopoverComponent {
  protected popoverConfig = {
    content: 'Nội dung menu',
    direction: 'bottom' as const,
    maxWidth: 200,
  };

  private popoverControl?: IPopoverFunctionControlEvent;

  handlerFunctionsControl(control: IPopoverFunctionControlEvent): void {
    event?.stopPropagation();
    this.popoverControl = control;
  }

  handlerPopoverEvent(event: string): void {
    event?.stopPropagation();
    console.log('Popover event:', event);
  }

  closePopover(): void {
    this.popoverControl?.removePopoverOverlay();
  }
}
```

### Ví dụ 3 — Dùng ng-template cho nội dung tùy chỉnh

```typescript
import { LibsUiComponentsPopoverComponent } from '@libs-ui/components-popover';

@Component({
  standalone: true,
  imports: [LibsUiComponentsPopoverComponent],
  template: `
    <div
      LibsUiComponentsPopoverDirective
      [config]="{ template: profileTpl, maxWidth: 280, direction: 'right' }">
      <img src="avatar.png" alt="Avatar" class="w-10 h-10 rounded-full cursor-pointer" />
    </div>

    <ng-template #profileTpl>
      <div class="p-4">
        <div class="font-bold text-gray-800">Nguyễn Văn A</div>
        <div class="text-sm text-gray-500">nguyenvana@example.com</div>
        <hr class="my-3" />
        <button class="text-sm text-red-500">Đăng xuất</button>
      </div>
    </ng-template>
  `,
})
export class ProfilePopoverComponent {}
```

### Ví dụ 4 — Chế độ text-ellipsis (chỉ hiện khi text bị cắt)

```typescript
import { LibsUiComponentsPopoverComponent } from '@libs-ui/components-popover';

@Component({
  standalone: true,
  imports: [LibsUiComponentsPopoverComponent],
  template: `
    <div
      LibsUiComponentsPopoverDirective
      [type]="'text'"
      [config]="{ content: 'Đây là đoạn văn bản dài sẽ bị cắt ngắn khi container quá nhỏ', direction: 'top' }"
      style="width: 120px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
      Đây là đoạn văn bản dài sẽ bị cắt ngắn khi container quá nhỏ
    </div>
  `,
})
export class TextEllipsisPopoverComponent {}
```

### Ví dụ 5 — Điều khiển từ bên ngoài (FunctionControl)

```typescript
import { LibsUiComponentsPopoverComponent } from '@libs-ui/components-popover';
import { IPopoverFunctionControlEvent } from '@libs-ui/components-popover';

@Component({
  standalone: true,
  imports: [LibsUiComponentsPopoverComponent],
  template: `
    <libs_ui-components-popover
      [config]="{ content: 'Popover được mở từ bên ngoài', direction: 'bottom' }"
      [mode]="'click'"
      [ignoreShowPopover]="true"
      (outFunctionsControl)="handlerFunctionsControl($event)">
      <span>Phần tử target</span>
    </libs_ui-components-popover>

    <button (click)="handlerOpenPopover($event)">Mở Popover từ nút khác</button>
    <button (click)="handlerClosePopover($event)">Đóng Popover</button>
  `,
})
export class ControlledPopoverComponent {
  private popoverControl?: IPopoverFunctionControlEvent;

  handlerFunctionsControl(control: IPopoverFunctionControlEvent): void {
    event?.stopPropagation();
    this.popoverControl = control;
  }

  handlerOpenPopover(event: MouseEvent): void {
    event.stopPropagation();
    this.popoverControl?.showPopover();
  }

  handlerClosePopover(event: MouseEvent): void {
    event.stopPropagation();
    this.popoverControl?.removePopoverOverlay();
  }
}
```

## @Input()

| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| `[config]` | `IPopoverOverlay` | `{}` | Cấu hình nội dung, hướng, kích thước và style của popover. Bắt buộc truyền để popover hiển thị. | `[config]="{ content: 'Hello', direction: 'top' }"` |
| `[mode]` | `TYPE_POPOVER_MODE` | `'hover'` | Chế độ kích hoạt hiển thị popover. Xem bảng `TYPE_POPOVER_MODE` bên dưới. | `[mode]="'click-toggle'"` |
| `[type]` | `'text' \| 'other'` | `'other'` | Khi là `'text'`, popover chỉ hiển thị khi nội dung bị truncate (clientWidth >= scrollWidth). | `[type]="'text'"` |
| `[ignoreShowPopover]` | `boolean` | `undefined` | Khi `true`, vô hiệu hóa hoàn toàn việc hiển thị popover. Hữu ích khi muốn kiểm soát thủ công qua FunctionControl. | `[ignoreShowPopover]="isDisabled()"` |
| `[elementRefCustom]` | `HTMLElement` | `undefined` | Element tùy chỉnh để gắn popover vào thay vì host element mặc định. | `[elementRefCustom]="myElementRef"` |
| `[initEventInElementRefCustom]` | `boolean` | `false` | Khi `true`, gắn event listener vào `elementRefCustom` thay vì host element. Phải dùng kèm `elementRefCustom`. | `[initEventInElementRefCustom]="true"` |
| `[classInclude]` | `string` | `''` | Các CSS class bổ sung sẽ được thêm vào host element. | `[classInclude]="'cursor-pointer text-blue-500'"` |
| `[flagMouse]` | `IFlagMouse` | `{ isMouseEnter: false, isMouseEnterContent: false, isContainerHasScroll: false }` | Trạng thái chuột từ component cha, dùng để ngăn đóng popover khi chuột còn trong vùng liên quan. | `[flagMouse]="parentFlagMouse()"` |
| `[ignoreHiddenPopoverContentWhenMouseLeave]` | `boolean` | `undefined` | Khi `true`, không ẩn popover khi chuột rời khỏi content overlay. | `[ignoreHiddenPopoverContentWhenMouseLeave]="true"` |
| `[ignoreClickOutside]` | `boolean` | `undefined` | Khi `true`, không đóng popover khi click bên ngoài vùng popover. | `[ignoreClickOutside]="true"` |
| `[ignoreStopPropagationEvent]` | `boolean` | `undefined` | Khi `true`, không gọi `stopPropagation` trên các event — cho phép event lan truyền lên component cha. | `[ignoreStopPropagationEvent]="true"` |
| `[ignoreCursorPointerModeLikeClick]` | `boolean` | `undefined` | Khi `true`, không thêm class `cursor-pointer` vào host element khi `mode` là click. | `[ignoreCursorPointerModeLikeClick]="true"` |
| `[isAddContentToParentDocument]` | `boolean` | `undefined` | Khi `true`, render overlay vào parent document. Dùng khi popover nằm trong iframe. | `[isAddContentToParentDocument]="true"` |
| `[debugId]` | `string` | `undefined` | ID dùng cho mục đích debug. | `[debugId]="'user-avatar-popover'"` |

## @Output()

| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| `(outEvent)` | `TYPE_POPOVER_EVENT` | Phát ra khi popover thay đổi trạng thái: `'show'` khi mở, `'remove'` khi đóng, `'click'` khi click vào host, `'mouseenter-element'`/`'mouseleave-element'` khi chuột vào/ra khỏi host, `'mouseenter-content'`/`'mouseleave-content'` khi chuột vào/ra khỏi overlay. | `handlerEvent(event: TYPE_POPOVER_EVENT): void { event?.stopPropagation(); ... }` | `(outEvent)="handlerEvent($event)"` |
| `(outChangStageFlagMouse)` | `IFlagMouse` | Phát ra mỗi khi trạng thái chuột thay đổi. Truyền kết quả vào `[flagMouse]` của popover khác để đồng bộ nhiều popover liên quan. | `handlerFlagMouseChange(flag: IFlagMouse): void { event?.stopPropagation(); this.flagMouse.set(flag); }` | `(outChangStageFlagMouse)="handlerFlagMouseChange($event)"` |
| `(outEventPopoverContent)` | `TYPE_POPOVER_DIRECTION` | Phát ra hướng thực tế mà popover đang hiển thị sau khi tính toán. Hữu ích để điều chỉnh UI dựa trên vị trí overlay. | `handlerDirectionChange(dir: TYPE_POPOVER_DIRECTION): void { event?.stopPropagation(); this.currentDirection.set(dir); }` | `(outEventPopoverContent)="handlerDirectionChange($event)"` |
| `(outFunctionsControl)` | `IPopoverFunctionControlEvent` | Phát ra object chứa các hàm điều khiển popover từ bên ngoài. Nhận về ngay khi component khởi tạo. | `handlerFunctionsControl(ctrl: IPopoverFunctionControlEvent): void { event?.stopPropagation(); this.popoverCtrl = ctrl; }` | `(outFunctionsControl)="handlerFunctionsControl($event)"` |

## Types & Interfaces

```typescript
import {
  IPopoverOverlay,
  IPopoverFunctionControlEvent,
  IFlagMouse,
  IPopover,
  TYPE_POPOVER_TYPE,
  TYPE_POPOVER_MODE,
  TYPE_POPOVER_DIRECTION,
  TYPE_POPOVER_POSITION_MODE,
  TYPE_POPOVER_EVENT,
} from '@libs-ui/components-popover';
```

### `IPopoverOverlay` — Cấu hình overlay

```typescript
import { IPopoverOverlay } from '@libs-ui/components-popover';

export interface IPopoverOverlay {
  // Nội dung
  content?: string;               // Nội dung dạng string HTML. Không dùng kèm template.
  template?: TemplateRef<any>;    // Nội dung dạng ng-template. Không dùng kèm content.
  itemContext?: any;              // Context truyền vào template khi dùng ng-template.

  // Kích thước
  width?: number;                 // Chiều rộng cố định (px)
  minWidth?: number;              // Chiều rộng tối thiểu (px)
  maxWidth?: number;              // Chiều rộng tối đa (px). Default: 250
  maxHeight?: number | null;      // Chiều cao tối đa (px). null = tự tính theo viewport. Default: 140
  widthByParent?: boolean;        // Nếu true, overlay rộng bằng phần tử cha
  parentBorderWidth?: number;     // Bù border của phần tử cha khi dùng widthByParent

  // Vị trí & hướng
  direction?: TYPE_POPOVER_DIRECTION;  // Hướng ưu tiên. Default: 'bottom'
  directionDistance?: number;          // Khoảng cách bổ sung theo hướng (px)
  position?: {
    mode: TYPE_POPOVER_POSITION_MODE;  // Căn chỉnh theo trục phụ: 'start' | 'center' | 'end'
    distance: number;                  // Khoảng offset so với vị trí căn chỉnh (px)
    autoUpdatePosition?: {
      startDistance: number;           // Offset khi mode tự chuyển sang 'start'
      endDistance: number;             // Offset khi mode tự chuyển sang 'end'
    };
  };

  // Padding nội dung overlay
  paddingTop?: number;
  paddingRight?: number;
  paddingBottom?: number;
  paddingLeft?: number;

  // Styling
  classInclude?: string;           // CSS class bổ sung cho wrapper overlay
  classIncludeOverlayBody?: string; // CSS class bổ sung cho body overlay
  whiteTheme?: boolean;            // Dùng theme màu trắng
  ngStyles?: Record<string, any>;  // Inline styles bổ sung

  // Mũi tên
  ignoreArrow?: boolean;           // Ẩn mũi tên chỉ hướng
  arrowPositionCustom?: number;    // Vị trí tùy chỉnh của mũi tên (px)

  // Animation
  animationConfig?: {
    time?: number;       // Thời gian animation (giây). Default: 0.4
    distance?: number;   // Khoảng cách trượt khi animate (px). Default: 12
  };

  // Hành vi
  timerDestroy?: number;              // Delay (ms) trước khi ẩn khi chuột rời. Default: 0
  zIndex?: number;                    // z-index của overlay
  isAddContentToParentDocument?: boolean; // Render vào parent document (dùng trong iframe)
  scrollOverlayOptions?: IScrollOverlayOptions; // Tùy chọn scroll cho overlay
}
```

### `TYPE_POPOVER_MODE` — Chế độ kích hoạt

```typescript
import { TYPE_POPOVER_MODE } from '@libs-ui/components-popover';

type TYPE_POPOVER_MODE =
  | 'hover'                                    // Hiển thị khi di chuột vào (mặc định)
  | 'click'                                    // Hiển thị khi click vào, không tự đóng
  | 'click-toggle'                             // Click để mở, click lần nữa để đóng
  | 'click_open_and_click_panel_content_hidden'; // Click để mở, click vào nội dung overlay để đóng
```

### `TYPE_POPOVER_DIRECTION` — Hướng hiển thị

```typescript
import { TYPE_POPOVER_DIRECTION } from '@libs-ui/components-popover';

type TYPE_POPOVER_DIRECTION = 'top' | 'right' | 'bottom' | 'left';
```

Khi hướng ưu tiên không đủ chỗ trong viewport, popover tự động thử theo thứ tự fallback:
- `bottom` → `bottom`, `top`, `right`, `left`
- `top` → `top`, `bottom`, `right`, `left`
- `right` → `right`, `left`, `bottom`, `top`
- `left` → `left`, `right`, `bottom`, `top`

### `TYPE_POPOVER_POSITION_MODE` — Căn chỉnh trục phụ

```typescript
import { TYPE_POPOVER_POSITION_MODE } from '@libs-ui/components-popover';

type TYPE_POPOVER_POSITION_MODE = 'start' | 'center' | 'end';
// 'start'  → Căn về phía bắt đầu của phần tử gốc (trái khi direction là top/bottom)
// 'center' → Căn giữa so với phần tử gốc (mặc định khi không chỉ định)
// 'end'    → Căn về phía cuối của phần tử gốc (phải khi direction là top/bottom)
```

### `TYPE_POPOVER_EVENT` — Sự kiện

```typescript
import { TYPE_POPOVER_EVENT } from '@libs-ui/components-popover';

type TYPE_POPOVER_EVENT =
  | 'show'               // Overlay vừa được hiển thị
  | 'remove'             // Overlay vừa bị xóa
  | 'click'              // Click vào host element
  | 'mouseenter-element' // Chuột vào host element
  | 'mouseleave-element' // Chuột rời khỏi host element
  | 'mouseenter-content' // Chuột vào overlay content
  | 'mouseleave-content'; // Chuột rời khỏi overlay content
```

### `IPopoverFunctionControlEvent` — API điều khiển

```typescript
import { IPopoverFunctionControlEvent } from '@libs-ui/components-popover';

interface IPopoverFunctionControlEvent {
  showPopover: (elementRef?: HTMLElement) => Promise<void>;         // Hiển thị popover (tùy chọn gắn vào element khác)
  removePopoverOverlay: () => Promise<void>;                        // Đóng và xóa overlay
  updatePopoverOverlayPosition: () => Promise<void>;               // Tính toán lại vị trí overlay
  updatePopoverOverlay: () => Promise<void>;                       // Cập nhật config và vị trí overlay
  getRectContainer: () => Promise<IBoundingClientRect>;            // Lấy vị trí phần tử gốc
  getRectContent: () => Promise<IBoundingClientRect>;              // Lấy vị trí overlay content
}
```

### `IFlagMouse` — Trạng thái chuột

```typescript
import { IFlagMouse } from '@libs-ui/components-popover';

interface IFlagMouse {
  isMouseEnter: boolean;           // Chuột đang trong host element
  isMouseEnterContent: boolean;    // Chuột đang trong overlay content
  isContainerHasScroll?: boolean;  // Host element có scroll
}
```

### `IPopover` — Interface tổng hợp

```typescript
import { IPopover } from '@libs-ui/components-popover';

interface IPopover {
  type?: TYPE_POPOVER_TYPE;
  mode?: TYPE_POPOVER_MODE;
  dataView?: string;
  config?: IPopoverOverlay;
  classInclude?: string;
  ignoreShowPopover?: boolean;
  ignoreHiddenPopoverContentWhenMouseLeave?: boolean;
  elementRefCustom?: HTMLElement;
  isAddContentToParentDocument?: boolean;
  templateCustomContent?: TemplateRef<any>;
}
```

## Lưu ý quan trọng

⚠️ **Một import cho cả hai cách dùng**: `LibsUiComponentsPopoverComponent` đăng ký cả selector dạng element (`libs_ui-components-popover`) lẫn attribute directive (`[LibsUiComponentsPopoverDirective]`). Chỉ cần import một lần vào `imports[]`.

⚠️ **Không truyền cả `content` và `template` cùng lúc**: Nếu truyền cả hai, `content` sẽ bị bỏ qua. Chọn một trong hai cách truyền nội dung.

⚠️ **`maxHeight: null` để tự động tính**: Khi đặt `maxHeight: null`, chiều cao overlay sẽ tự điều chỉnh dựa trên khoảng trống còn lại trong viewport (tính từ top của host element xuống đáy màn hình trừ 44px).

⚠️ **Chế độ `hover` với `timerDestroy`**: Khi dùng nhiều popover lồng nhau (popover trong popover), sử dụng `[flagMouse]` và `(outChangStageFlagMouse)` để truyền trạng thái chuột giữa các popover, tránh đóng popover cha khi chuột đang trong popover con.

⚠️ **Debug via URL query**: Có thể đặt thời gian delay trước khi ẩn popover qua URL query `?popover-timer-destroy=500` (đơn vị ms) — hữu ích khi debug bằng DevTools.

⚠️ **Iframe support**: Khi component chạy trong iframe, dùng `[isAddContentToParentDocument]="true"` hoặc `config.isAddContentToParentDocument = true` để render overlay vào parent document và tránh bị cắt bởi `overflow: hidden`.
