diff --git a/samples/BlazorServer/BlazorServer.csproj b/samples/BlazorServer/BlazorServer.csproj index daab3e6..971e014 100644 --- a/samples/BlazorServer/BlazorServer.csproj +++ b/samples/BlazorServer/BlazorServer.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/samples/BlazorServer/Controllers/FileController.cs b/samples/BlazorServer/Controllers/FileController.cs new file mode 100644 index 0000000..43efc20 --- /dev/null +++ b/samples/BlazorServer/Controllers/FileController.cs @@ -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 Upload(List files) + { + var file = files[0]; + var root = _config.GetValue(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}"); + } + } +} diff --git a/samples/BlazorServer/Pages/EditorView.razor b/samples/BlazorServer/Pages/EditorView.razor index b39375d..b064231 100644 --- a/samples/BlazorServer/Pages/EditorView.razor +++ b/samples/BlazorServer/Pages/EditorView.razor @@ -1,4 +1,6 @@ - +@inject IConfiguration _config; + + Cancel Reset Save Changes @@ -33,4 +35,16 @@ { await OnSave.InvokeAsync(_html); } + + // Upload the image into the wwwroot/images folder for demonstration + private async Task ImageUploadHandler(string imageName, string imageContentType, Stream imageStream) + { + var root = _config.GetValue(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}"; + } } \ No newline at end of file diff --git a/samples/BlazorServer/Program.cs b/samples/BlazorServer/Program.cs index 015118e..7478fee 100644 --- a/samples/BlazorServer/Program.cs +++ b/samples/BlazorServer/Program.cs @@ -23,6 +23,7 @@ app.UseRouting(); +app.MapControllers(); app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); diff --git a/src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.cs b/src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.cs index 7b70358..5e1c623 100644 --- a/src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.cs +++ b/src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.cs @@ -34,6 +34,15 @@ public sealed partial class MudHtmlEditor : IAsyncDisposable [Parameter] public EventCallback TextChanged { get; set; } + [Parameter] + public Func>? ImageUploadHandler { get; set; } + + [Parameter] + public string? ImageUploadUrl { get; set; } + + [Parameter] + public IEnumerable AllowedImageMimeTypes { get; set; } = new List() { "image/png", "image/jpeg" }; + [Parameter] public bool Resizable { get; set; } = true; @@ -75,7 +84,16 @@ protected override async Task OnAfterRenderAsync(bool firstRender) _dotNetRef = DotNetObjectReference.Create(this); await using var module = await JS.InvokeAsync("import", "./_content/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.js"); - _quill = await module.InvokeAsync("createQuillInterop", _dotNetRef, _editor, _toolbar, Placeholder); + + var settings = new + { + Placeholder = Placeholder, + BlazorImageUpload = ImageUploadHandler != null, + ImageUploadUrl = ImageUploadUrl ?? "", + AllowedImageMimeTypes = AllowedImageMimeTypes + }; + + _quill = await module.InvokeAsync("createQuillInterop", _dotNetRef, _editor, _toolbar, settings); await SetHtml(Html); @@ -101,6 +119,22 @@ public async void HandleTextContentChanged(string text) await TextChanged.InvokeAsync(text); } + [JSInvokable] + public async Task SaveImage(string imageName, string fileType, long size) + { + if(_quill is not null) + { + var imageJsStream = await _quill.InvokeAsync("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) diff --git a/src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.js b/src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.js index 974d5b6..13b7a46 100644 --- a/src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.js +++ b/src/Tizzani.MudBlazor.HtmlEditor/MudHtmlEditor.razor.js @@ -1,4 +1,5 @@ var Embed = Quill.import('blots/block/embed'); +const Delta = Quill.import('delta'); class Divider extends Embed { static create(value) { @@ -16,18 +17,34 @@ 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 { @@ -35,14 +52,22 @@ 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 = () => { @@ -75,4 +100,65 @@ export class MudQuillInterop { this.dotNetRef.invokeMethodAsync('HandleHtmlContentChanged', this.getHtml()); this.dotNetRef.invokeMethodAsync('HandleTextContentChanged', this.getText()); }; -} \ No newline at end of file + + // 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); + }); + } + } +}