Angular結(jié)合quill實(shí)現(xiàn)富文本編輯器

1. 前言

由于需要一個(gè)富文本編輯器來編輯一些網(wǎng)頁內(nèi)容, 手動(dòng)編輯后存儲(chǔ)到數(shù)據(jù)庫比較麻煩, 所以著手實(shí)現(xiàn)一個(gè)自己的富文本編輯器, 來編輯和存儲(chǔ)一些html文件.
這里使用Angular框架, 加Quill庫實(shí)現(xiàn).

ngx-quill: https://github.com/KillerCodeMonkey/ngx-quill
quill官網(wǎng): https://quilljs.com/

2. 創(chuàng)建Angular工程

2.1. 創(chuàng)建工程

首先創(chuàng)建一個(gè)angular工程. 工程的名字就叫angular-editor.


ng new angular-editor

2.2. 添加依賴

這里需要添加ngx-quill依賴包, 以下是 ngx-quill 與Angular之間的兼容關(guān)系.

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, 我選擇了一個(gè)穩(wěn)定版本ngx-quill@16.2.1
查看ngx-quill@16.2.1的配置文件package.json, 其對(duì)應(yīng)的quill版本為quill@1.3.7, 所以這里quill使用quill@1.3.7. 為了讓typescript能識(shí)別類型信息, 這里還需要導(dǎo)入一個(gè)開發(fā)依賴包@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

當(dāng)前最新版本為 ngx-quill@20.0.1 quill@1.3.7

3. 創(chuàng)建編輯器

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. 可以在預(yù)編譯的時(shí)候?qū)uill的樣式編譯進(jìn)來.

找到styles.scss, 添加如下代碼引入quill樣式

quill提供兩種主題, 一種是bubble, 另一種是snow, 默認(rèn)是snow, 可以任選一種導(dǎo)入, 也可以同時(shí)導(dǎo)入兩種主題, 方便動(dòng)態(tài)切換樣式.

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富文本編輯器添加到頁面

做好以上準(zhǔn)備工作, 我們就可以將quill富文本編輯器. 以app.component.html為例, 只要在頁面添加這樣一行, 然后啟動(dòng)angular應(yīng)用, 就可以看到編輯器了.

<quill-editor></quill-editor>

啟動(dòng)應(yīng)用看效果

ng serve --open

4. 加載與獲取富文本內(nèi)容

當(dāng)我們使用quill編輯文檔的時(shí)候, 往往不是從空白文檔開始, 大多數(shù)情況下我們是在已有文檔的基礎(chǔ)上進(jìn)行修改.

當(dāng)我們拿到一個(gè)文檔時(shí), 如何將其內(nèi)容加載到quill編輯器中呢? 網(wǎng)上很多的教程或博客講解得不夠深入.

首先要將quill-editor與一個(gè)control控件連接起來, 如下:


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

當(dāng)然連接之前, 我們需要?jiǎng)?chuàng)建該控件.

  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>'),
  })

這樣我們通過控制該控件, 可以在構(gòu)建FormControl時(shí)傳入, 也可以在創(chuàng)建完FormControl后通過setValue方法加載html內(nèi)容, 就能加載html內(nèi)容到quill編輯器中. 而在quill中編輯文檔內(nèi)容時(shí), control控件中的內(nèi)容會(huì)自動(dòng)更新.

當(dāng)我們準(zhǔn)備存盤時(shí), 獲取到該控件, 通過value熟悉即可獲取到修改后的html內(nèi)容 form.get('html').value, 而不必去操作quill-editor組件.

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

5. 如何處理插入圖片

當(dāng)插入圖片時(shí), quill默認(rèn)會(huì)將圖片轉(zhuǎn)換成base64編碼嵌入到文本中, 因?yàn)槲倚枰獙⒏晃谋敬鎯?chǔ)到數(shù)據(jù)庫中, 這種默認(rèn)方式會(huì)導(dǎo)致數(shù)據(jù)庫字段內(nèi)容龐大.
影響查詢性能. 所以我想將這種默認(rèn)行為改為, 將圖片保存到圖片服務(wù)器, 在富文本中僅僅插入圖片鏈接.

首先需要捕獲onEditorCreated事件, 捕獲該事件后我們才有機(jī)會(huì)替換quill編輯器的默認(rèn)行為. 獲取該事件的方法十分簡(jiǎn)單.
只需要給qull-editor綁定一個(gè)定制的方法editorCreated($event), 通過$event即可獲取到創(chuàng)建好的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. 例如, 一下是我實(shí)現(xiàn)的一個(gè)image.
實(shí)現(xiàn)方式比較容易理解, 即將圖片上傳到文件服務(wù)器, 然后獲取到圖片的url, 將url嵌入到圖片插入位置.

這里imageHandler做的事情很簡(jiǎn)單, 只是出發(fā)一個(gè)open dialog事件.
為什么這樣設(shè)計(jì)? 因?yàn)樵趇mageHandler內(nèi)部調(diào)用this.dialog.open創(chuàng)建的DialogOverviewExampleDialog脫離了NgZone, 后續(xù)無論是渲染, 還是關(guān)閉對(duì)話框都會(huì)出現(xiàn)很奇怪的行為.
所以在imageHandler內(nèi)部只是觸發(fā)一個(gè)事件.外部的component接收到這個(gè)事件再打開對(duì)話框.

   /**
   * 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接收到事件再彈出對(duì)話框

  @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)
      }
    });
  }

這里要自己設(shè)計(jì)對(duì)話框組件DialogOverviewExampleDialog, 關(guān)閉時(shí)傳出圖片的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. 實(shí)現(xiàn)后的效果

實(shí)現(xiàn)后的效果如下:

angular quill editor

7. Angular 系列文章

最新更新以及更多Angular相關(guān)文章請(qǐng)?jiān)L問 鵬叔的技術(shù)博客空間 - Angular

8. 參考文檔

Angular:ngx-quill富文本編輯器的使用

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

element ui富文本編輯器的使用

Dialog

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末掀宋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖衣厘,帶你破解...
    沈念sama閱讀 212,080評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窖梁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡髓考,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門汞贸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绳军,“玉大人,你說我怎么就攤上這事矢腻∶偶荩” “怎么了?”我有些...
    開封第一講書人閱讀 157,630評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵多柑,是天一觀的道長(zhǎng)奶是。 經(jīng)常有香客問我,道長(zhǎng)竣灌,這世上最難降的妖魔是什么聂沙? 我笑而不...
    開封第一講書人閱讀 56,554評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮初嘹,結(jié)果婚禮上及汉,老公的妹妹穿的比我還像新娘。我一直安慰自己屯烦,他們只是感情好坷随,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,662評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著驻龟,像睡著了一般温眉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上翁狐,一...
    開封第一講書人閱讀 49,856評(píng)論 1 290
  • 那天类溢,我揣著相機(jī)與錄音,去河邊找鬼露懒。 笑死闯冷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的懈词。 我是一名探鬼主播窃躲,決...
    沈念sama閱讀 39,014評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼钦睡!你這毒婦竟也來了蒂窒?” 一聲冷哼從身側(cè)響起躁倒,我...
    開封第一講書人閱讀 37,752評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體空盼,經(jīng)...
    沈念sama閱讀 44,212評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,541評(píng)論 2 327
  • 正文 我和宋清朗相戀三年象迎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片呛踊。...
    茶點(diǎn)故事閱讀 38,687評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡砾淌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谭网,到底是詐尸還是另有隱情汪厨,我是刑警寧澤,帶...
    沈念sama閱讀 34,347評(píng)論 4 331
  • 正文 年R本政府宣布愉择,位于F島的核電站劫乱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏锥涕。R本人自食惡果不足惜衷戈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,973評(píng)論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望层坠。 院中可真熱鬧殖妇,春花似錦、人聲如沸破花。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,777評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽旧乞。三九已至,卻和暖如春磅氨,著一層夾襖步出監(jiān)牢的瞬間尺栖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,006評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工烦租, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留延赌,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,406評(píng)論 2 360
  • 正文 我出身青樓叉橱,卻偏偏與公主長(zhǎng)得像挫以,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子窃祝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,576評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容