npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@libs-ui/components-inputs-quill2x

v0.2.357-7

Published

> Rich Text Editor mạnh mẽ dựa trên Quill 2.x, tích hợp đầy đủ toolbar, mention, emoji, bảng, upload ảnh và validation cho Angular.

Readme

@libs-ui/components-inputs-quill2x

Rich Text Editor mạnh mẽ dựa trên Quill 2.x, tích hợp đầy đủ toolbar, mention, emoji, bảng, upload ảnh và validation cho Angular.

Giới thiệu

@libs-ui/components-inputs-quill2x cung cấp một trình soạn thảo văn bản giàu tính năng (Rich Text Editor) được xây dựng trên nền tảng Quill 2.x. Component hỗ trợ binding hai chiều với object model, toolbar thông minh tự co giãn theo chiều rộng container, cùng nhiều tính năng nâng cao như mention, emoji, bảng, upload ảnh và validation tích hợp. Nội dung luôn được lọc qua bộ lọc XSS nội bộ trước khi ghi vào model.

Tính năng

  • Toolbar thông minh tự tính toán độ rộng, tự động ẩn nút vào menu "Xem thêm" khi không đủ chỗ
  • Hỗ trợ 4 chế độ toolbar: default, basic, all, custom
  • Toolbar floating (position fixed) hiển thị theo sự kiện click/mouseenter trên element bất kỳ
  • Tích hợp mention (@user, #tag) với danh sách gợi ý có thể lọc động
  • Bộ chọn emoji tích hợp sẵn
  • Hỗ trợ tạo và quản lý bảng (table) với context menu dòng/cột
  • Upload ảnh từ file hoặc paste từ clipboard, hỗ trợ custom upload function
  • Plugin resize ảnh (tùy chọn)
  • Validation: bắt buộc nhập, độ dài tối thiểu/tối đa
  • Lọc màu gần trắng (near white) khi paste từ Word/nguồn bên ngoài
  • Hỗ trợ nhập tiếng Việt Telex/VNI qua composition event
  • Auto-detect và format URL thành hyperlink khi gõ
  • Lịch sử undo/redo với debounce 2 giây
  • Expose FunctionsControl để component cha tương tác trực tiếp với editor
  • XSS filter tự động khi set content

Khi nào sử dụng

  • Khi cần trình soạn thảo văn bản hỗ trợ định dạng (Bold, Italic, danh sách, tiêu đề)
  • Khi xây dựng hệ thống chat hoặc comment cần tính năng mention người dùng
  • Khi cần tích hợp bảng, hình ảnh vào nội dung (CMS, báo cáo, ghi chú)
  • Khi cần trải nghiệm soạn thảo chuyên nghiệp tương đương Slack, Notion
  • Khi cần toolbar floating gắn với một element bên ngoài editor

Cài đặt

npm install @libs-ui/components-inputs-quill2x

Các package phụ thuộc (quill2x = npm:[email protected], @ssumo/quill-resize-module, quill-delta...) đã khai báo peerDependencies trong package.json của lib → trình quản lý gói tự cài kèm, không cần cài thủ công.

⚠️ Nạp CSS của Quill (BẮT BUỘC — nếu thiếu, editor/toolbar sẽ vỡ giao diện)

Quill ship style riêng, KHÔNG đi kèm trong bundle của component. Consumer PHẢI tự nạp CSS theme từ package quill2x. Khai báo trong angular.json / project.json:

// architect.build.options.styles  (Angular CLI)  HOẶC  targets.build.options.styles (Nx)
"styles": [
  "src/styles.scss",
  "./node_modules/quill2x/dist/quill.snow.css",
  "./node_modules/quill2x/dist/quill.bubble.css"
]

🔴 quill.snow.css là theme mặc định (BẮT BUỘC). Chỉ thêm quill.bubble.css nếu dùng theme bubble. Lấy CSS từ package quill2x (KHÔNG dùng quill) để khớp đúng version Quill 2.x. Nếu gặp cảnh báo CommonJS khi build, thêm allowedCommonJsDependencies: ["quill2x"] vào cấu hình build.

Import

import { LibsUiComponentsInputsQuill2xComponent } from '@libs-ui/components-inputs-quill2x';

// Interfaces và types
import {
  IQuill2xCustomConfig,
  IQuill2xFunctionControlEvent,
  IQuill2xUploadImageConfig,
  IQuill2xBlotRegister,
  IQuill2xSelectionChange,
  IQuill2xTextChange,
  IQuill2xLink,
  IQuill2xToolbarConfig,
  QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION,
} from '@libs-ui/components-inputs-quill2x';

// Utility functions
import {
  getHTMLFromDeltaOfQuill2x,
  getDeltaOfQuill2xFromHTML,
  isEmptyQuill2x,
  convertStandardList,
  convertStandardListToQuill2x,
} from '@libs-ui/components-inputs-quill2x';

Ví dụ sử dụng

1. Cơ bản — binding hai chiều với object model

// component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsQuill2xComponent } from '@libs-ui/components-inputs-quill2x';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsInputsQuill2xComponent],
  templateUrl: './my.component.html',
})
export class MyComponent {
  protected formData = signal<Record<string, string>>({
    content: '<p>Xin chào! Đây là nội dung ban đầu.</p>',
  });

  protected handlerChange(html: string): void {
    // html là nội dung HTML đã qua XSS filter
    console.log('Nội dung mới:', html);
  }
}
<!-- my.component.html -->
<libs_ui-components-inputs-quill2x
  [(item)]="formData"
  [fieldBind]="'content'"
  [placeholder]="'Nhập nội dung...'"
  (outChange)="handlerChange($event)">
</libs_ui-components-inputs-quill2x>

2. Toàn bộ tính năng — toolbar all, resize editor, giới hạn chiều cao

// component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsQuill2xComponent, IQuill2xCustomConfig } from '@libs-ui/components-inputs-quill2x';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsInputsQuill2xComponent],
  templateUrl: './my.component.html',
})
export class MyComponent {
  protected articleData = signal<Record<string, string>>({
    body: '<h1>Tiêu đề bài viết</h1><p>Nội dung bài viết tại đây.</p>',
  });

  protected fullConfig: IQuill2xCustomConfig = {
    toolbar: signal({
      type: signal<'all'>('all'),
    }),
  };

  protected handlerFunctionsControl(ctrl: IQuill2xFunctionControlEvent): void {
    // Lưu lại để sử dụng về sau
    this.editorControl = ctrl;
  }

  private editorControl?: IQuill2xFunctionControlEvent;
}
<!-- my.component.html -->
<libs_ui-components-inputs-quill2x
  [(item)]="articleData"
  [fieldBind]="'body'"
  [quillCustomConfig]="fullConfig"
  [resize]="'vertical'"
  [minHeightEditorContentDefault]="150"
  [maxHeightEditorContentDefault]="600"
  [resizeImagePlugin]="true"
  [validRequired]="{ isRequired: true, message: 'Vui lòng nhập nội dung' }"
  (outFunctionsControl)="handlerFunctionsControl($event)">
</libs_ui-components-inputs-quill2x>

3. Mention — gợi ý @user khi gõ ký tự trigger

// component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsQuill2xComponent } from '@libs-ui/components-inputs-quill2x';
import { IMentionConfig } from '@libs-ui/components-inputs-mention';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsInputsQuill2xComponent],
  templateUrl: './my.component.html',
})
export class MyComponent {
  protected commentData = signal<Record<string, string>>({ comment: '' });

  protected mentionConfig: IMentionConfig = {
    items: [
      { id: '1', name: 'Anh Tuấn', username: '[email protected]' },
      { id: '2', name: 'Bảo Ngọc', username: '[email protected]' },
      { id: '3', name: 'Công Thành', username: '[email protected]' },
    ],
    triggerChar: '@',
    labelKey: 'name',
    mentionFilter: (search: string, items?: Array<Record<string, string>>) => {
      const keyword = search.toLowerCase();
      return items?.filter((item) => item['name'].toLowerCase().includes(keyword));
    },
    mentionSelect: (item: Record<string, string>, triggerChar?: string) => {
      return (triggerChar || '@') + item['name'];
    },
  };
}
<!-- my.component.html -->
<libs_ui-components-inputs-quill2x
  [(item)]="commentData"
  [fieldBind]="'comment'"
  [dataConfigMention]="mentionConfig"
  [placeholder]="'Nhập bình luận, gõ @ để mention...'">
</libs_ui-components-inputs-quill2x>

4. Upload ảnh — custom upload function

// component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsQuill2xComponent, IQuill2xUploadImageConfig } from '@libs-ui/components-inputs-quill2x';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsInputsQuill2xComponent],
  templateUrl: './my.component.html',
})
export class MyComponent {
  protected postData = signal<Record<string, string>>({ content: '' });

  protected uploadConfig: IQuill2xUploadImageConfig = {
    modeCustom: true,
    showIcon: true,
    maxImageSize: 5 * 1024 * 1024, // 5MB
    onlyAcceptImageHttpsLink: true,
    functionUploadImage: async (files: Array<File>): Promise<Array<string | ArrayBuffer>> => {
      // Gọi API upload ảnh thực tế
      const uploadedUrls: string[] = [];
      for (const file of files) {
        const formData = new FormData();
        formData.append('file', file);
        const response = await fetch('/api/upload', { method: 'POST', body: formData });
        const result = await response.json();
        uploadedUrls.push(result.url);
      }
      return uploadedUrls;
    },
  };
}
<!-- my.component.html -->
<libs_ui-components-inputs-quill2x
  [(item)]="postData"
  [fieldBind]="'content'"
  [uploadImageConfig]="uploadConfig">
</libs_ui-components-inputs-quill2x>

5. Toolbar floating (position fixed) — toolbar hiển thị theo element trigger

// component.ts
import { AfterViewInit, Component, ElementRef, signal, ViewChild } from '@angular/core';
import { LibsUiComponentsInputsQuill2xComponent, IQuill2xCustomConfig } from '@libs-ui/components-inputs-quill2x';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsInputsQuill2xComponent],
  templateUrl: './my.component.html',
})
export class MyComponent implements AfterViewInit {
  @ViewChild('triggerBtn') triggerBtn?: ElementRef<HTMLButtonElement>;

  protected floatingData = signal<Record<string, string>>({ content: '' });
  protected floatingConfig = signal<IQuill2xCustomConfig | undefined>(undefined);

  ngAfterViewInit(): void {
    if (!this.triggerBtn) return;
    this.floatingConfig.set({
      toolbar: signal({
        type: signal<'default'>('default'),
        positionFixed: signal({
          event: signal<'click'>('click'),
          elementShow: signal(this.triggerBtn.nativeElement),
          position: signal<'bottom'>('bottom'),
          align: signal<'left'>('left'),
          zIndex: signal(9999),
          width: signal(600),
          gapVertical: signal(5),
          gapHorizontal: signal(0),
        }),
      }),
    });
  }
}
<!-- my.component.html -->
<button #triggerBtn class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
  Mở toolbar
</button>
<libs_ui-components-inputs-quill2x
  [(item)]="floatingData"
  [fieldBind]="'content'"
  [quillCustomConfig]="floatingConfig()"
  [minHeightEditorContentDefault]="150">
</libs_ui-components-inputs-quill2x>

6. Lập trình điều khiển editor qua FunctionsControl

// component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsInputsQuill2xComponent, IQuill2xFunctionControlEvent } from '@libs-ui/components-inputs-quill2x';

@Component({
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [LibsUiComponentsInputsQuill2xComponent],
  templateUrl: './my.component.html',
})
export class MyComponent {
  protected editorData = signal<Record<string, string>>({ html: '' });
  private editorCtrl?: IQuill2xFunctionControlEvent;

  protected handlerFunctionsControl(ctrl: IQuill2xFunctionControlEvent): void {
    this.editorCtrl = ctrl;
  }

  protected async handlerInsertTemplate(): Promise<void> {
    await this.editorCtrl?.setContent('<p>Nội dung template mới được set từ bên ngoài.</p>');
  }

  protected async handlerValidate(): Promise<void> {
    const isValid = await this.editorCtrl?.checkIsValid();
    console.log('Valid:', isValid);
  }
}
<!-- my.component.html -->
<libs_ui-components-inputs-quill2x
  [(item)]="editorData"
  [fieldBind]="'html'"
  [validRequired]="{ isRequired: true, message: 'Vui lòng nhập nội dung' }"
  (outFunctionsControl)="handlerFunctionsControl($event)">
</libs_ui-components-inputs-quill2x>
<button (click)="handlerInsertTemplate()">Chèn template</button>
<button (click)="handlerValidate()">Kiểm tra hợp lệ</button>

@Input()

| Input | Type | Default | Mô tả | Ví dụ | |---|---|---|---|---| | item (model, required) | TYPE_OBJECT | — | Object chứa dữ liệu binding. Binding hai chiều qua [(item)]. | [(item)]="formData" | | fieldBind (required) | string | — | Tên trường trong item để đọc/ghi nội dung HTML. | [fieldBind]="'content'" | | placeholder | string | 'i18n_import_content' | Văn bản placeholder khi editor trống. | [placeholder]="'Nhập nội dung...'" | | readonly | boolean | false | Bật chế độ chỉ đọc, vô hiệu hóa toolbar. | [readonly]="true" | | displayToolbar | boolean | true | Ẩn/hiện toàn bộ thanh công cụ. | [displayToolbar]="false" | | quillCustomConfig | IQuill2xCustomConfig | undefined | Cấu hình nâng cao: loại toolbar, position fixed, class tùy chỉnh. | [quillCustomConfig]="myConfig" | | dataConfigMention | IMentionConfig | undefined | Cấu hình tính năng mention (@user, #tag). | [dataConfigMention]="mentionConfig" | | uploadImageConfig | IQuill2xUploadImageConfig | mặc định | Cấu hình upload ảnh: custom mode, kích thước tối đa, function upload. | [uploadImageConfig]="uploadCfg" | | label | ILabel | undefined | Label hiển thị phía trên editor. | [label]="{ text: 'Mô tả' }" | | resize | 'vertical' \| 'none' | 'none' | Cho phép kéo giãn editor theo chiều dọc. | [resize]="'vertical'" | | zIndex | number | 1250 | z-index áp dụng cho các popup/modal của editor. | [zIndex]="2000" | | autoFocus | boolean | false | Tự động focus vào editor khi khởi tạo. | [autoFocus]="true" | | focusTimerOnInit | number | 750 | Delay (ms) trước khi thực hiện auto focus. | [focusTimerOnInit]="300" | | focusBottom | boolean | false | Khi auto focus, đặt con trỏ ở cuối nội dung thay vì đầu. | [focusBottom]="true" | | fontSizeDefault | number | 14 | Cỡ chữ mặc định (px). | [fontSizeDefault]="16" | | heightEditorContentDefault | number | undefined | Chiều cao cố định của vùng soạn thảo (px). | [heightEditorContentDefault]="300" | | minHeightEditorContentDefault | number | undefined | Chiều cao tối thiểu của vùng soạn thảo (px). | [minHeightEditorContentDefault]="150" | | maxHeightEditorContentDefault | number | undefined | Chiều cao tối đa của vùng soạn thảo (px). | [maxHeightEditorContentDefault]="500" | | validRequired | IValidRequired | undefined | Quy tắc bắt buộc nhập. { isRequired: true, message: '...' } | [validRequired]="{ isRequired: true }" | | validMinLength | IValidLength | undefined | Quy tắc độ dài tối thiểu. { length: 10, message: '...' } | [validMinLength]="{ length: 20 }" | | validMaxLength | IValidLength | undefined | Quy tắc độ dài tối đa. { length: 1000, message: '...' } | [validMaxLength]="{ length: 500 }" | | showErrorLabel | boolean | true | Hiển thị label thông báo lỗi validation bên dưới editor. | [showErrorLabel]="false" | | showErrorBorder | boolean | true | Hiển thị viền đỏ khi validation thất bại. | [showErrorBorder]="false" | | ignoreShowBorderErrorToolbar | boolean | false | Không áp dụng viền đỏ lên toolbar khi có lỗi. | [ignoreShowBorderErrorToolbar]="true" | | blotsRegister | Array<IQuill2xBlotRegister> | undefined | Đăng ký custom Quill blots/formats. | [blotsRegister]="customBlots" | | templateToolBarPersonalize | TemplateRef | undefined | TemplateRef để thêm nút tùy chỉnh vào toolbar. | [templateToolBarPersonalize]="myTmpl" | | handlersExpand | Array<{title: string; action: () => void}> | undefined | Mở rộng handlers toolbar. Key title khớp với tên nút trong toolbar. | [handlersExpand]="[{title:'myBtn', action: fn}]" | | resizeImagePlugin | boolean | false | Bật plugin cho phép resize ảnh trực tiếp trong editor. | [resizeImagePlugin]="true" | | removeNearWhiteColorsOnPaste | boolean | true | Tự động xóa màu gần trắng khi paste từ nguồn bên ngoài. | [removeNearWhiteColorsOnPaste]="false" | | ignoreShowPopupEditLink | boolean | false | Khi true, emit outShowPopupEditLink thay vì dùng popup mặc định để edit link. | [ignoreShowPopupEditLink]="true" | | ignoreCommunicateMicroEventPopup | boolean | false | Bỏ qua giao tiếp micro-frontend event cho các popup nội bộ. | [ignoreCommunicateMicroEventPopup]="true" |

@Output()

| Output | Type | Mô tả | Handler TS | Binding HTML | |---|---|---|---|---| | (outChange) | string | Emit nội dung HTML (đã qua XSS filter) mỗi khi nội dung thay đổi. | handlerChange(html: string): void { event.stopPropagation?.(); this.savedHtml = html; } | (outChange)="handlerChange($event)" | | (outFocus) | void | Emit khi editor nhận focus. | handlerFocus(): void { this.isEditing.set(true); } | (outFocus)="handlerFocus()" | | (outBlur) | void | Emit khi editor mất focus. | handlerBlur(): void { this.isEditing.set(false); } | (outBlur)="handlerBlur()" | | (outFunctionsControl) | IQuill2xFunctionControlEvent | Emit ngay khi editor khởi tạo xong, trả về object chứa các hàm điều khiển editor. | handlerFunctionsControl(ctrl: IQuill2xFunctionControlEvent): void { this.editorCtrl = ctrl; } | (outFunctionsControl)="handlerFunctionsControl($event)" | | (outSelectionChange) | IQuill2xSelectionChange | Emit khi vị trí con trỏ hoặc vùng chọn thay đổi. | handlerSelectionChange(e: IQuill2xSelectionChange): void { console.log(e.range); } | (outSelectionChange)="handlerSelectionChange($event)" | | (outTextChange) | IQuill2xTextChange | Emit khi nội dung thay đổi, bao gồm Quill Delta. | handlerTextChange(e: IQuill2xTextChange): void { console.log(e.delta); } | (outTextChange)="handlerTextChange($event)" | | (outMessageError) | string | Emit thông báo lỗi validation. Chuỗi rỗng khi không có lỗi. | handlerMessageError(msg: string): void { this.errorMsg.set(msg); } | (outMessageError)="handlerMessageError($event)" | | (outContextMenu) | MouseEvent | Emit khi người dùng click chuột phải vào vùng soạn thảo. | handlerContextMenu(event: MouseEvent): void { event.stopPropagation(); event.preventDefault(); } | (outContextMenu)="handlerContextMenu($event)" | | (outShowPopupEditLink) | { dataLink: IQuill2xLink; callback: (linkEdit: { title: string; link: string }) => Promise<void> } | Emit khi cần hiển thị popup edit link tùy chỉnh. Chỉ hoạt động khi ignoreShowPopupEditLink="true". | handlerShowPopupEditLink(e: { dataLink: IQuill2xLink; callback: Function }): void { this.openCustomLinkDialog(e); } | (outShowPopupEditLink)="handlerShowPopupEditLink($event)" |

FunctionsControl — Điều khiển editor từ component cha

FunctionsControl được emit qua (outFunctionsControl). Lưu lại để gọi các hàm bên dưới:

private editorCtrl?: IQuill2xFunctionControlEvent;

protected handlerFunctionsControl(ctrl: IQuill2xFunctionControlEvent): void {
  this.editorCtrl = ctrl;
}

| Method | Signature | Mô tả | |---|---|---| | setContent | (content: string) => Promise<void> | Set toàn bộ nội dung HTML của editor (qua XSS filter và convert list). | | insertText | (value: string, index?: number, focusLt?: boolean) => Promise<void> | Chèn plain text tại vị trí con trỏ hoặc index chỉ định. | | insertLink | (value: string, url: string, index?: number) => Promise<void> | Chèn hyperlink vào editor. | | insertImage | (content: string, index?: number) => Promise<void> | Chèn ảnh theo URL vào editor. | | setFontSize | (size: number) => Promise<Delta \| undefined> | Đặt cỡ chữ (px) tại vị trí con trỏ hiện tại. | | setColor | (color: string) => Promise<Delta \| undefined> | Đặt màu chữ tại vị trí con trỏ. | | setBackground | (color: string) => Promise<Delta \| undefined> | Đặt màu nền tại vị trí con trỏ. | | checkIsValid | () => Promise<boolean> | Chạy validation theo các input valid* đã cấu hình. Trả về true nếu hợp lệ. | | refreshItemValue | () => void | Buộc đọc lại nội dung HTML từ DOM và cập nhật vào item[fieldBind]. | | setMessageError | (message: string) => Promise<void> | Hiển thị thông báo lỗi tùy chỉnh bên dưới editor. | | reCalculatorToolbar | () => Promise<void> | Tính toán lại kích thước toolbar (dùng khi container thay đổi kích thước). | | updatePositionToolbar | () => Promise<void> | Cập nhật lại vị trí toolbar floating (dùng khi trigger element di chuyển). | | quill | () => Quill2x \| undefined | Trả về instance Quill gốc để truy cập API Quill trực tiếp. |

Types & Interfaces

import {
  IQuill2xCustomConfig,
  IQuill2xToolbarConfig,
  IQuill2xUploadImageConfig,
  IQuill2xFunctionControlEvent,
  IQuill2xLink,
  IQuill2xBlotRegister,
  IQuill2xSelectionChange,
  IQuill2xTextChange,
  QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION,
} from '@libs-ui/components-inputs-quill2x';
import { WritableSignal } from '@angular/core';

// Loại toolbar
type QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION = 'all' | 'default' | 'basic' | 'custom';

// Cấu hình tổng thể editor
interface IQuill2xCustomConfig {
  classContainer?: WritableSignal<string>;
  toolbar?: WritableSignal<{
    type: WritableSignal<QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION>;
    positionFixed?: WritableSignal<{
      event: WritableSignal<'click' | 'mouseenter'>;
      elementShow: WritableSignal<HTMLElement>;
      position?: WritableSignal<'top' | 'bottom'>;
      align?: WritableSignal<'left' | 'center' | 'right'>;
      zIndex: WritableSignal<number>;
      width: WritableSignal<number>;
      gapVertical?: WritableSignal<number>;
      gapHorizontal?: WritableSignal<number>;
    }>;
    styles?: WritableSignal<Record<string, unknown>>;
    options?: WritableSignal<Array<IQuill2xToolbarConfig>>;
    classCustomContainerToolbar?: WritableSignal<string>;
    lessWidthToolbarRecallCalculator?: WritableSignal<number>;
  }>;
  editor?: WritableSignal<{
    classCustomContainerEditor?: WritableSignal<string>;
  }>;
}

// Cấu hình từng nút trên toolbar
interface IQuill2xToolbarConfig {
  type: string;
  mode?: Array<QUILL2X_TYPE_MODE_BAR_CONFIG_OPTION>;
  width: number;
  display?: boolean;
  classInclude?: string;
}

// Cấu hình upload ảnh
interface IQuill2xUploadImageConfig {
  modeCustom: boolean;          // true = mở popup tùy chỉnh thay vì input file
  showIcon?: boolean;            // Hiển thị nút ảnh trên toolbar
  zIndex?: number;
  maxImageSize?: number;         // Giới hạn kích thước file (bytes)
  onlyAcceptImageHttpsLink?: boolean; // Chỉ chấp nhận link https
  functionUploadImage?: (files: Array<File>) => Promise<Array<string | ArrayBuffer>>;
}

// API điều khiển editor từ bên ngoài
interface IQuill2xFunctionControlEvent {
  checkIsValid: () => Promise<boolean>;
  refreshItemValue: () => void;
  setContent: (content: string) => Promise<void>;
  insertText: (value: string, index?: number, focusLt?: boolean) => Promise<void>;
  insertLink: (value: string, url: string, index?: number) => Promise<void>;
  insertImage: (content: string, index?: number) => Promise<void>;
  setFontSize: (size: number) => Promise<Delta | undefined>;
  setColor: (color: string) => Promise<Delta | undefined>;
  setBackground: (color: string) => Promise<Delta | undefined>;
  setMessageError: (message: string) => Promise<void>;
  quill: () => Quill2x | undefined;
  reCalculatorToolbar: () => Promise<void>;
  updatePositionToolbar: () => Promise<void>;
}

// Thông tin link khi click vào hyperlink trong editor
interface IQuill2xLink {
  title: string;
  url: string;
  range: { index: number; length: number };
}

// Đăng ký custom Quill blot
interface IQuill2xBlotRegister {
  component: any;       // Class Quill blot
  className: string;    // CSS class của blot
  style: string;        // Inline styles dạng 'key:value;key2:value2'
  ignoreDelete?: boolean; // true = không xóa blot khi nhấn Backspace/Delete
}

// Dữ liệu emit từ outSelectionChange
interface IQuill2xSelectionChange {
  quill: Quill2x;
  range: Range;
  oldRange: Range;
  source: EmitterSource;
}

// Dữ liệu emit từ outTextChange
interface IQuill2xTextChange {
  quill: Quill2x;
  delta: Delta;
}

Utility Functions

import {
  isEmptyQuill2x,
  convertStandardList,
  convertStandardListToQuill2x,
  getHTMLFromDeltaOfQuill2x,
  getDeltaOfQuill2xFromHTML,
} from '@libs-ui/components-inputs-quill2x';

// Kiểm tra editor có nội dung hay không
const isEmpty = isEmptyQuill2x(quillInstance);

// Chuẩn hóa HTML list từ định dạng nội bộ sang HTML chuẩn (để lưu)
const standardHtml = convertStandardList(rawHtml);

// Chuẩn hóa HTML list từ HTML chuẩn sang định dạng Quill (khi load vào editor)
const quillHtml = convertStandardListToQuill2x(savedHtml);

// Lấy HTML từ Quill Delta
const html = getHTMLFromDeltaOfQuill2x(delta);

// Lấy Quill Delta từ HTML string
const delta = getDeltaOfQuill2xFromHTML(html);

Lưu ý quan trọng

⚠️ item là model bắt buộc: Phải dùng [(item)] (two-way binding). Không truyền object thuần bất biến vì component sẽ ghi trực tiếp vào item[fieldBind] khi nội dung thay đổi.

⚠️ IQuill2xCustomConfig dùng WritableSignal: Toàn bộ thuộc tính trong IQuill2xCustomConfigWritableSignal. Phải khởi tạo với signal(...) thay vì truyền giá trị thường. Xem ví dụ mục 2 và 5 ở trên.

⚠️ position fixed cần ngAfterViewInit: Khi dùng positionFixed.elementShow, element DOM phải tồn tại trước. Khởi tạo config trong ngAfterViewInit sau khi @ViewChild đã sẵn sàng.

⚠️ XSS Security: Nội dung được tự động lọc qua xssFilter khi set content qua setContent(). Không set content trực tiếp qua Quill API (quill().clipboard.dangerouslyPasteHTML) mà không sanitize.

⚠️ Peer dependency quill2x: Package quill2x phải được cài đặt. Component không hoạt động với quill phiên bản khác.

⚠️ Tính toán toolbar: Toolbar tự tính toán độ rộng sau khi render. Nếu component cha có animation hoặc thay đổi kích thước, gọi editorCtrl?.reCalculatorToolbar() sau khi animation kết thúc để toolbar hiển thị đúng.

⚠️ convertStandardList khi lưu: HTML được lưu vào item[fieldBind] đã qua convertStandardList() để chuẩn hóa định dạng danh sách. Khi hiển thị lại nội dung, dùng component @libs-ui/components-inputs-quill2x-preview hoặc apply CSS Quill Snow theme.

Demo

npx nx serve core-ui

Truy cập: http://localhost:4500/inputs/quill2x