今天用到富文本框了,ant-design提供的是vditor的控件。但是感觉功能有点少,而且扩展的不太好。先后对比了下vditor、quill、ckeditor
vditor 主要优势在markdown的编写上,我这项目用户群体不适合用markdown,quill是轻量级的,利于扩展,ckeditor重量级,功能很多。但是就是太重了。最终选择了这个quill。
网上走到一个结合quill 富文本框做的一个开源的组件,但是这个组件有几个缺点
1、没有实现双向绑定
2、图片上传采用的base64存储
于是对这个组件做了一次大手术,修改了不少地方。代码改动量几乎相当于重新做了遍了。
先看下效果
接下来就给看下每个细节地方的代码
QuillEditor.razor
@inject IJSRuntime JSRuntime
<div @ref="@ToolBarContainer">
@if (ToolBarMode == "basic")
{
<span class="ql-formats">
<select class="ql-font"></select>
<select class="ql-size"></select>
</span>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-header" value="1"></button>
<button class="ql-header" value="2"></button>
<button class="ql-blockquote"></button>
<button class="ql-code-block"></button>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
<button class="ql-indent" value="-1"></button>
<button class="ql-indent" value="+1"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
<button class="ql-image"></button>
</span>
<span class="ql-formats">
<button class="ql-clean"></button>
</span>
}
else if (ToolBarMode == "full")
{
<span class="ql-formats">
<select class="ql-font"></select>
<select class="ql-size"></select>
</span>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-script" value="sub"></button>
<button class="ql-script" value="super"></button>
</span>
<span class="ql-formats">
<button class="ql-header" value="1"></button>
<button class="ql-header" value="2"></button>
<button class="ql-blockquote"></button>
<button class="ql-code-block"></button>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
<button class="ql-indent" value="-1"></button>
<button class="ql-indent" value="+1"></button>
</span>
<span class="ql-formats">
<button class="ql-direction" value="rtl"></button>
<select class="ql-align"></select>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
<button class="ql-image"></button>
<button class="ql-video"></button>
<button class="ql-formula"></button>
</span>
<span class="ql-formats">
<button class="ql-clean"></button>
</span>
}
else if (ToolBarMode == "custom")
{
@ToolBarContent
}
</div>
<div @ref="@ElementContentContainer" style="height:@ContainerHeight">
@EditorContent
</div>
QuillEditor.razor.cs
public partial class QuillEditor : ComponentBase
{
[Parameter]
public string Value
{
get => _value??"";
set
{
if (_value != value)
{
_value = value;
_wattingUpdate = true;
}
}
}
private bool _wattingUpdate = false;
private string _value;
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Inject] private QuillEditorService EditorService { get; set; }
[Parameter]
public RenderFragment EditorContent { get; set; }
[Parameter]
public RenderFragment ToolBarContent { get; set; }
[Parameter]
public bool ReadOnly { get; set; }= false;
[Parameter]
public string Placeholder { get; set; }= "";
[Parameter]
public string Theme { get; set; }= "snow";
[Parameter]
public string DebugLevel { get; set; }= "info";
[Parameter]
public string ToolBarMode { get; set; } = "basic";//full
[Parameter]
public string ContainerHeight { get; set; } = "600px";
[Parameter]
public string UploadImageUrl { get; set; }
private ElementReference ElementContentContainer;
private ElementReference ToolBarContainer;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await EditorService.CreateQuill(
ElementContentContainer,
ToolBarContainer,
ReadOnly,
Placeholder,
Theme,
DebugLevel,
UploadImageUrl,
this);
}
}
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
}
[JSInvokable]
public void OnInput(string value)
{
_value = value;
_wattingUpdate = false;
if (ValueChanged.HasDelegate)
{
ValueChanged.InvokeAsync(value);
}
}
public async Task<string> GetText()
{
return await EditorService.GetText(ElementContentContainer);
}
public async Task<string> GetHTML()
{
return await EditorService.GetHTML(ElementContentContainer);
}
public async Task<string> GetContent()
{
return await EditorService.GetContent(ElementContentContainer);
}
public async Task LoadContent(string Content)
{
var QuillDelta =
await EditorService.LoadQuillContent(ElementContentContainer, Content);
}
public async Task LoadHTMLContent(string quillHTMLContent)
{
var QuillDelta =
await EditorService.LoadQuillHTMLContent(ElementContentContainer, quillHTMLContent);
}
public async Task InsertImage(string ImageURL)
{
var QuillDelta =
await EditorService.InsertQuillImage(ElementContentContainer, ImageURL);
}
public async Task EnableEditor(bool mode)
{
var QuillDelta =
await EditorService.EnableQuillEditor(ElementContentContainer, mode);
}
}
QuillEditorService.cs
public class QuillEditorOption
{
public string UploadBaseUrl { get; set; }
}
public class QuillEditorService
{
private readonly IJSRuntime jsRuntime;
private readonly QuillEditorOption _option;
public QuillEditorService(IJSRuntime js, IOptions<QuillEditorOption> option)
{
jsRuntime = js;
_option = option.Value;
}
public async ValueTask<object> CreateQuill(
ElementReference quillElement,
ElementReference toolbar,
bool readOnly,
string placeholder,
string theme,
string debugLevel,
string uploadImageUrl,
QuillEditor editor)
{
return await jsRuntime.InvokeAsync<object>(
"QuillFunctions.createQuill",
quillElement, toolbar, readOnly,
placeholder, theme, debugLevel, DotNetObjectReference.Create(editor),editor.Value, _option.UploadBaseUrl+uploadImageUrl);
}
public async ValueTask<string> GetText(
ElementReference quillElement)
{
return await jsRuntime.InvokeAsync<string>(
"QuillFunctions.getQuillText",
quillElement);
}
public async ValueTask<string> GetHTML(
ElementReference quillElement)
{
return await jsRuntime.InvokeAsync<string>(
"QuillFunctions.getQuillHTML",
quillElement);
}
public async ValueTask<string> GetContent(
ElementReference quillElement)
{
return await jsRuntime.InvokeAsync<string>(
"QuillFunctions.getQuillContent",
quillElement);
}
public async ValueTask<object> LoadQuillContent(
ElementReference quillElement,
string Content)
{
return await jsRuntime.InvokeAsync<object>(
"QuillFunctions.loadQuillContent",
quillElement, Content);
}
public async ValueTask<object> LoadQuillHTMLContent(
ElementReference quillElement,
string quillHTMLContent)
{
return await jsRuntime.InvokeAsync<object>(
"QuillFunctions.loadQuillHTMLContent",
quillElement, quillHTMLContent);
}
public async ValueTask<object> EnableQuillEditor(
ElementReference quillElement,
bool mode)
{
return await jsRuntime.InvokeAsync<object>(
"QuillFunctions.enableQuillEditor",
quillElement, mode);
}
public async ValueTask<object> InsertQuillImage(
ElementReference quillElement,
string imageURL)
{
return await jsRuntime.InvokeAsync<object>(
"QuillFunctions.insertQuillImage",
quillElement, imageURL);
}
}
blazorquill.js
(function () {
window.QuillFunctions = {
createQuill: function (
quillElement, toolBar, readOnly,
placeholder, theme, debugLevel,editor,value,uploadurl) {
Quill.register('modules/blotFormatter', QuillBlotFormatter.default);
var options = {
debug: debugLevel,
modules: {
toolbar: toolBar,
blotFormatter: {}
},
placeholder: placeholder,
readOnly: readOnly,
theme: theme
};
quillElement.__quill = new Quill(quillElement, options);
quillElement.__quill.root.innerHTML = value;//初始化时设置初始值 HTML
quillElement.__quill.on('text-change', function () {
var cotent = quillElement.__quill.root.innerHTML;
editor.invokeMethodAsync('OnInput', cotent);
});
let toolbar = quillElement.__quill.getModule('toolbar');
toolbar.addHandler('image', () => {
var fileInput = toolbar.container.querySelector('input.ql-image[type=file]');
if (fileInput == null) {
fileInput = document.createElement('input');
fileInput.setAttribute('type', 'file');
fileInput.setAttribute('accept', 'image/*');
fileInput.classList.add('ql-image');
fileInput.addEventListener('change', function () {
if (fileInput.files != null && fileInput.files[0] != null) {
var formData = new FormData();
formData.append('file', fileInput.files[0]);
var xhr = new XMLHttpRequest();
// 调用xhr.open()函数
xhr.open("POST", uploadurl);
//调用xhr.send()函数,发送请求,这一步是异步操作
xhr.send(formData);
//监听 xhr.onreadystatechange 事件
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var filejson = eval("(" + xhr.responseText + ")");
var range = quillElement.__quill.getSelection();
//插入图片
quillElement.__quill.insertEmbed(range.index, 'image', filejson.fileUrl);
}
}
}
});
toolbar.container.appendChild(fileInput);
}
fileInput.click();
});
},
getQuillContent: function (quillElement) {
return JSON.stringify(quillElement.__quill.getContents());
},
getQuillText: function (quillElement) {
return quillElement.__quill.getText();
},
getQuillHTML: function (quillElement) {
return quillElement.__quill.root.innerHTML;
},
loadQuillContent: function (quillElement, quillContent) {
content = JSON.parse(quillContent);
return quillElement.__quill.setContents(content, 'api');
},
loadQuillHTMLContent: function (quillElement, quillHTMLContent) {
return quillElement.__quill.root.innerHTML = quillHTMLContent;
},
enableQuillEditor: function (quillElement, mode) {
quillElement.__quill.enable(mode);
},
insertQuillImage: function (quillElement, imageURL) {
var Delta = Quill.import('delta');
editorIndex = 0;
if (quillElement.__quill.getSelection() !== null) {
editorIndex = quillElement.__quill.getSelection().index;
}
return quillElement.__quill.updateContents(
new Delta()
.retain(editorIndex)
.insert({ image: imageURL },
{ alt: imageURL }));
}
};
})();