Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions samples/BlazorServer/BlazorServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@
<ProjectReference Include="..\..\src\Tizzani.MudBlazor.HtmlEditor\Tizzani.MudBlazor.HtmlEditor.csproj" />
</ItemGroup>

<ItemGroup>
<Folder Include="wwwroot\images\" />
</ItemGroup>

</Project>
27 changes: 27 additions & 0 deletions samples/BlazorServer/Controllers/FileController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.AspNetCore.Mvc;

namespace BlazorServer.Controllers
{
[Route("[Controller]/[Action]")]
public class FileController : Controller
{
private readonly IConfiguration _config;
public FileController(IConfiguration config)
{
_config = config;
}

[HttpPost]
public async Task<IActionResult> Upload(List<IFormFile> files)
{
var file = files[0];
var root = _config.GetValue<string>(WebHostDefaults.ContentRootKey) ?? "";
var path = Path.Combine(root, $"wwwroot{Path.DirectorySeparatorChar}images", file.FileName);
FileStream filestream = new FileStream(path, FileMode.Create, FileAccess.Write);
await file.CopyToAsync(filestream);
filestream.Close();

return Content($"images/{file.FileName}");
}
}
}
16 changes: 15 additions & 1 deletion samples/BlazorServer/Pages/EditorView.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<MudStack Justify="Justify.FlexEnd" Row="true">
@inject IConfiguration _config;

<MudStack Justify="Justify.FlexEnd" Row="true">
<MudButton OnClick="OnCancel" Size="Size.Small">Cancel</MudButton>
<MudButton OnClick="Reset" Size="Size.Small">Reset</MudButton>
<MudButton Color="Color.Primary" OnClick="SaveChanges" Size="Size.Small" StartIcon="@Icons.Material.Filled.Save" Variant="Variant.Filled">Save Changes</MudButton>
Expand Down Expand Up @@ -33,4 +35,16 @@
{
await OnSave.InvokeAsync(_html);
}

// Upload the image into the wwwroot/images folder for demonstration
private async Task<string> ImageUploadHandler(string imageName, string imageContentType, Stream imageStream)
{
var root = _config.GetValue<string>(WebHostDefaults.ContentRootKey) ?? "";
var path = Path.Combine(root, $"wwwroot{Path.DirectorySeparatorChar}images", imageName);
FileStream filestream = new FileStream(path, FileMode.Create, FileAccess.Write);
await imageStream.CopyToAsync(filestream);
filestream.Close();

return $"images/{imageName}";
}
}
1 change: 1 addition & 0 deletions samples/BlazorServer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

app.UseRouting();

app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

Expand Down
36 changes: 35 additions & 1 deletion src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ public sealed partial class MudHtmlEditor : IAsyncDisposable
[Parameter]
public EventCallback<string> TextChanged { get; set; }

[Parameter]
public Func<string, string, Stream, Task<string>>? ImageUploadHandler { get; set; }

[Parameter]
public string? ImageUploadUrl { get; set; }

[Parameter]
public IEnumerable<string> AllowedImageMimeTypes { get; set; } = new List<string>() { "image/png", "image/jpeg" };

[Parameter]
public bool Resizable { get; set; } = true;

Expand Down Expand Up @@ -75,7 +84,16 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
_dotNetRef = DotNetObjectReference.Create(this);

await using var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.js");
_quill = await module.InvokeAsync<IJSObjectReference>("createQuillInterop", _dotNetRef, _editor, _toolbar, Placeholder);

var settings = new
{
Placeholder = Placeholder,
BlazorImageUpload = ImageUploadHandler != null,
ImageUploadUrl = ImageUploadUrl ?? "",
AllowedImageMimeTypes = AllowedImageMimeTypes
};

_quill = await module.InvokeAsync<IJSObjectReference>("createQuillInterop", _dotNetRef, _editor, _toolbar, settings);

await SetHtml(Html);

Expand All @@ -101,6 +119,22 @@ public async void HandleTextContentChanged(string text)
await TextChanged.InvokeAsync(text);
}

[JSInvokable]
public async Task<string> SaveImage(string imageName, string fileType, long size)
{
if(_quill is not null)
{
var imageJsStream = await _quill.InvokeAsync<IJSStreamReference>("getImageBytes", imageName);

await using var imageStream = await imageJsStream.OpenReadStreamAsync(size);

if (ImageUploadHandler is not null)
return await ImageUploadHandler(imageName, fileType, imageStream);
}

return "";
}

async ValueTask IAsyncDisposable.DisposeAsync()
{
if (_quill is not null)
Expand Down
108 changes: 97 additions & 11 deletions src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var Embed = Quill.import('blots/block/embed');
const Delta = Quill.import('delta');

class Divider extends Embed {
static create(value) {
Expand All @@ -16,33 +17,57 @@ try {
Quill.register('modules/blotFormatter', QuillBlotFormatter.default);
} catch { }

export function createQuillInterop(dotNetRef, editorRef, toolbarRef, placeholder) {
var quill = new Quill(editorRef, {

export function createQuillInterop(dotNetRef, editorRef, toolbarRef, settings) {
var interop = new MudQuillInterop(dotNetRef, editorRef, toolbarRef, settings);

var properties = {
modules: {
toolbar: {
container: toolbarRef
},
blotFormatter: {}
blotFormatter: {},
uploader: {
mimetypes: settings.allowedImageMimeTypes
}
},
placeholder: placeholder,
placeholder: settings.placeholder,
theme: 'snow'
});
return new MudQuillInterop(dotNetRef, quill, editorRef, toolbarRef);
};

// Use custom handler if specified
if (settings.blazorImageUpload || settings.imageUploadUrl) {
properties.modules.uploader.handler = interop.uploadImageHandler;
}

var quill = new Quill(editorRef, properties);

interop.addQuill(quill);

return interop;
}

export class MudQuillInterop {
/**
* @param {Quill} quill
* @param {Element} editorRef
* @param {Element} toolbarRef
* @param {object} toolbarRef
*/
constructor(dotNetRef, quill, editorRef, toolbarRef) {
quill.getModule('toolbar').addHandler('hr', this.insertDividerHandler);
quill.on('text-change', this.textChangedHandler);
constructor(dotNetRef, editorRef, toolbarRef, settings) {
this.dotNetRef = dotNetRef;
this.quill = quill;
this.quill;
this.editorRef = editorRef;
this.toolbarRef = toolbarRef;
this.blazorImageUpload = settings.blazorImageUpload;
this.imageBytes = {};
this.imageUploadUrl = settings.imageUploadUrl;
}

addQuill = (quill) => {
quill.getModule('toolbar').addHandler('hr', this.insertDividerHandler);
quill.on('text-change', this.textChangedHandler);
this.quill = quill;
}

getText = () => {
Expand Down Expand Up @@ -75,4 +100,65 @@ export class MudQuillInterop {
this.dotNetRef.invokeMethodAsync('HandleHtmlContentChanged', this.getHtml());
this.dotNetRef.invokeMethodAsync('HandleTextContentChanged', this.getText());
};
}

// Get imageBytes by filename
getImageBytes = (key) => {
return this.imageBytes[key];
}

// Read file and upload it via the blazor or controller method
upload(file) {
const fileReader = new FileReader();
return new Promise((resolve, reject) => {
fileReader.addEventListener("load", () => {
// Pass file via DotNetInterop
if (this.blazorImageUpload) {
this.imageBytes[file.name] = new Uint8Array(fileReader.result);
this.dotNetRef.invokeMethodAsync("SaveImage", file.name, file.type, file.size).then(url => resolve({name: file.name, url: url }));
}
// Upload to API endpoint
else if (this.imageUploadUrl) {
const formData = new FormData();
formData.append("files", file);

fetch(this.imageUploadUrl, {
method: "POST",
headers: {},
body: formData
})
.then(response => {
if (response.status === 200) {
response.text().then(url =>
resolve({ name: file.name, url: url })
);
}
else {
reject("Error uploading image to " + this.imageUploadUrl);
}
});
}
});

if (file) {
fileReader.readAsArrayBuffer(file);
} else {
reject("No file selected");
}
});
}

// Handle when images are copy/pasted or dragged/dropped into the editor
uploadImageHandler = (range, files) => {
for (let file of files) {
this.upload(file).then((data) => {
let delta = new Delta().retain(range.index).delete(range.length).insert({ image: data.url });
delete this.imageBytes[data.name]
this.quill.updateContents(delta, Quill.sources.USER);
this.quill.setSelection(range.index, Quill.sources.SILENT);
},
(error) => {
console.warn(error);
});
}
}
}