Angular结合quill实现富文本编辑器

1. 前言

由于需要一个富文本编辑器来编辑一些网页内容, 手动编辑后存储到数据库比较麻烦, 所以着手实现一个自己的富文本编辑器, 来编辑和存储一些html文件.
这里使用Angular框架, 加Quill库实现.

ngx-quill: https://github.com/KillerCodeMonkey/ngx-quill
quill官网: https://quilljs.com/

2. 创建Angular工程

2.1. 创建工程

首先创建一个angular工程. 工程的名字就叫angular-editor.


ng new angular-editor

2.2. 添加依赖

这里需要添加ngx-quill依赖包, 以下是 ngx-quill 与Angular之间的兼容关系.

Angular ngx-quill supported
v15 >= 20.0.0 until May, 2024
v14 >= 17.0.0 until Dec 02, 2023
v13 >= 15.0.0, < 17.0.0 until May 04, 2023

由于我目前使用的angular版本13.3.11, 我选择了一个稳定版本ngx-quill@16.2.1
查看ngx-quill@16.2.1的配置文件package.json, 其对应的quill版本为quill@1.3.7, 所以这里quill使用quill@1.3.7. 为了让typescript能识别类型信息, 这里还需要导入一个开发依赖包@types/quill@1.3.10, 版本也可以从ngx-quill的package.json中找到.


npm install ngx-quill@16.2.1 --save
npm install quill@1.3.7 --save
npm install @types/quill@1.3.10 --save-dev

当前最新版本为 ngx-quill@20.0.1 quill@1.3.7

3. 创建编辑器

3.1. 引入Quill模块

添加依赖包之后还不能直接使用Quill, 还需要再使用Quill的Module声明文件引入它.

以下以根模块为例讲解如何引入模块,引入ngx-quill的QuillModule

app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { QuillModule } from 'ngx-quill';  // 引入富文本编辑器模块
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    QuillModule.forRoot()                  // 富文本编辑器模块
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

3.2. 引入quill css样式

由于我使用的scss, 所以可以很方便的引入quill css. 可以在预编译的时候将quill的样式编译进来.

找到styles.scss, 添加如下代码引入quill样式

quill提供两种主题, 一种是bubble, 另一种是snow, 默认是snow, 可以任选一种导入, 也可以同时导入两种主题, 方便动态切换样式.

styles.scss

@import url('https://cdn.quilljs.com/1.3.7/quill.snow.css');
@import url('https://cdn.quilljs.com/1.3.7/quill.bubble.css');

3.3. 将quill富文本编辑器添加到页面

做好以上准备工作, 我们就可以将quill富文本编辑器. 以app.component.html为例, 只要在页面添加这样一行, 然后启动angular应用, 就可以看到编辑器了.

<quill-editor></quill-editor>

启动应用看效果

ng serve --open

4. 加载与获取富文本内容

当我们使用quill编辑文档的时候, 往往不是从空白文档开始, 大多数情况下我们是在已有文档的基础上进行修改.

当我们拿到一个文档时, 如何将其内容加载到quill编辑器中呢? 网上很多的教程或博客讲解得不够深入.

首先要将quill-editor与一个control控件连接起来, 如下:


<form [formGroup]="form">
  <quill-editor format="html" formControlName="html"></quill-editor>
</form>

当然连接之前, 我们需要创建该控件.

  form: FormGroup = this.fb.group({
    html: new FormControl('<div>test</div><ul><li>1</li><li class="ql-indent-1">1-1</li><li>2</li><ol><li>numbered</li><li class="ql-indent-1">numbered-1</li></ol></ul><div><br></div>'),
  })

这样我们通过控制该控件, 可以在构建FormControl时传入, 也可以在创建完FormControl后通过setValue方法加载html内容, 就能加载html内容到quill编辑器中. 而在quill中编辑文档内容时, control控件中的内容会自动更新.

当我们准备存盘时, 获取到该控件, 通过value熟悉即可获取到修改后的html内容 form.get('html').value, 而不必去操作quill-editor组件.

更多用法可以参考ngx-quill示例

5. 如何处理插入图片

当插入图片时, quill默认会将图片转换成base64编码嵌入到文本中, 因为我需要将富文本存储到数据库中, 这种默认方式会导致数据库字段内容庞大.
影响查询性能. 所以我想将这种默认行为改为, 将图片保存到图片服务器, 在富文本中仅仅插入图片链接.

首先需要捕获onEditorCreated事件, 捕获该事件后我们才有机会替换quill编辑器的默认行为. 获取该事件的方法十分简单.
只需要给qull-editor绑定一个定制的方法editorCreated($event), 通过$event即可获取到创建好的editor本身.

    <quill-editor format="html" formControlName="body" (onEditorCreated)="editorCreated($event)"></quill-editor>

捕获到onEditorCreated事件以及获取到editor后我们就可以客制化插入图片的行为了.

  /**
   * ngx-quill上传图片需要的方法
   */
   editorCreated(quill:any) {
    const toolbar = quill.getModule('toolbar');
    toolbar.addHandler('image', this.imageHandler); // 将image handler替换为自己的imageHander
    this.editor = quill;
  }
 

将image handler替换为自己的imageHander. 例如, 一下是我实现的一个image.
实现方式比较容易理解, 即将图片上传到文件服务器, 然后获取到图片的url, 将url嵌入到图片插入位置.

这里imageHandler做的事情很简单, 只是出发一个open dialog事件.
为什么这样设计? 因为在imageHandler内部调用this.dialog.open创建的DialogOverviewExampleDialog脱离了NgZone, 后续无论是渲染, 还是关闭对话框都会出现很奇怪的行为.
所以在imageHandler内部只是触发一个事件.外部的component接收到这个事件再打开对话框.

   /**
   * Note: why not dirrectly call open dialog that's because
   * the mothod need to bind this which will cause 
   * the problem that the component created by this.dialog.open
   * will be out of box (ngzone)
   * please refer to the page for the details
   * https://github.com/angular/components/issues/9676
   */
  imageHandler(){
    const event = new Event("open dialog");
    window.dispatchEvent(event);
  }

外部的组件也就是AppComponent接收到事件再弹出对话框

  @HostListener("window:open dialog")
  openDialog() {
    
    let dialogRef = this.dialog.open(DialogOverviewExampleDialog, {width:'400px'});

    dialogRef.afterClosed().subscribe(result => {
      console.log(result);
      if(result) {
        const range = this.editor.getSelection(true);
        const index = range.index + range.length;
        this.editor.insertEmbed(index, 'image', result, 'user');
        this.editor.setSelection(1+index)
      }
    });
  }

这里要自己设计对话框组件DialogOverviewExampleDialog, 关闭时传出图片的URL;

可以参照如下代码

example-dialog.component.ts

import { Component, NgZone, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';

@Component({
    selector: 'dialog-overview-example-dialog',
    templateUrl: 'example-dialog.component.html',
    styleUrls: ['./example-dialog.component.scss']
})
export class DialogOverviewExampleDialog implements OnInit {

    value = ""
    constructor(
        public dialogRef: MatDialogRef<DialogOverviewExampleDialog>,
        public ngZone: NgZone
    ) {
    }

    ngOnInit(): void {
        
    }

    close(): void {
        console.log("close clicked")
        this.dialogRef.close();
    }

}

example-dialog.component.html


<div cdkDrag cdkDragRootElement=".cdk-overlay-pane" class="w-100">
  <h1 mat-dialog-title>Insert image</h1>
  <div mat-dialog-content>
    <mat-form-field class="w-100">
      <mat-label>url</mat-label>
      <input type="text" placeholder="input image url" matInput [(ngModel)]="value">
    </mat-form-field>
  </div>
  <div mat-dialog-actions>
    <button mat-button (click)="close()">Cancel</button>
    <button mat-button [mat-dialog-close]="value" cdkFocusInitial>Ok</button>
  </div>
</div>

6. 实现后的效果

实现后的效果如下:

angular quill editor

7. Angular 系列文章

最新更新以及更多Angular相关文章请访问 鹏叔的技术博客空间 - Angular

8. 参考文档

Angular:ngx-quill富文本编辑器的使用

如何在Angular 11/12版本中整合ngx-quill教程

element ui富文本编辑器的使用

Dialog

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容