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)后的效果如下:
7. Angular 系列文章
最新更新以及更多Angular相關(guān)文章請(qǐng)?jiān)L問 鵬叔的技術(shù)博客空間 - Angular