diff --git a/Oqtane.Client/Modules/Controls/RichTextEditorInterop.cs b/Oqtane.Client/Modules/Controls/QuillEditorInterop.cs similarity index 97% rename from Oqtane.Client/Modules/Controls/RichTextEditorInterop.cs rename to Oqtane.Client/Modules/Controls/QuillEditorInterop.cs index 338f240a..ed53c4e7 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditorInterop.cs +++ b/Oqtane.Client/Modules/Controls/QuillEditorInterop.cs @@ -4,11 +4,11 @@ using System.Threading.Tasks; namespace Oqtane.Modules.Controls { - public class RichTextEditorInterop + public class QuillEditorInterop { private readonly IJSRuntime _jsRuntime; - public RichTextEditorInterop(IJSRuntime jsRuntime) + public QuillEditorInterop(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } diff --git a/Oqtane.Client/Modules/Controls/QuillTextEditor.razor b/Oqtane.Client/Modules/Controls/QuillTextEditor.razor new file mode 100644 index 00000000..c64a5c6f --- /dev/null +++ b/Oqtane.Client/Modules/Controls/QuillTextEditor.razor @@ -0,0 +1,365 @@ +@namespace Oqtane.Modules.Controls +@inherits ModuleControlBase +@implements ITextEditor +@inject IStringLocalizer Localizer + +
+ + @if (AllowRichText) + { + + @if (_richfilemanager) + { + + +
+ } +
+ @if (AllowFileManagement) + { + + } + @if (_richfilemanager) + { + @((MarkupString)"  ") + + } +
+
+
+
+ @if (ToolbarContent != null) + { + @ToolbarContent + } + else + { + + + + + + + + + + + + + + + + + + + } +
+
+
+
+
+ } + @if (AllowRawHtml) + { + + @if (_rawfilemanager) + { + + +
+ } +
+ @if (AllowFileManagement) + { + + } + @if (_rawfilemanager) + { + @((MarkupString)"  ") + + } +
+ @if (ReadOnly) + { + + } + else + { + + } +
+ } +
+
+ +@code { + private bool _initialized = false; + + private QuillEditorInterop interop; + private FileManager _fileManager; + private string _activetab = "Rich"; + + private ElementReference _editorElement; + private ElementReference _toolBar; + private bool _richfilemanager = false; + private string _richhtml = string.Empty; + private string _originalrichhtml = string.Empty; + + private bool _rawfilemanager = false; + private string _rawhtmlid = "RawHtmlEditor_" + Guid.NewGuid().ToString("N"); + private string _rawhtml = string.Empty; + private string _originalrawhtml = string.Empty; + + private string _message = string.Empty; + private bool _contentchanged = false; + private int _editorIndex; + + [Parameter] + public bool AllowFileManagement{ get; set; } + + [Parameter] + public bool AllowRichText { get; set; } = true; + + [Parameter] + public bool AllowRawHtml { get; set; } = true; + + [Parameter] + public bool ReadOnly { get; set; } + + [Parameter] + public string Placeholder { get; set; } + + [Parameter] + public string Theme { get; set; } = "snow"; + + [Parameter] + public string DebugLevel { get; set; } = "info"; + + [Parameter] + public RenderFragment ToolbarContent { get; set; } + + public override List Resources { get; set; } = new List() + { + new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js", Location = ResourceLocation.Body }, + new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js", Location = ResourceLocation.Body }, + new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js", Location = ResourceLocation.Body } + }; + + protected override void OnInitialized() + { + interop = new QuillEditorInterop(JSRuntime); + + if (string.IsNullOrEmpty(Placeholder)) + { + Placeholder = Localizer["Placeholder"]; + } + } + + protected override void OnParametersSet() + { + if (!AllowRichText) + { + _activetab = "Raw"; + } + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + if (AllowRichText) + { + if (firstRender) + { + await interop.CreateEditor( + _editorElement, + _toolBar, + ReadOnly, + Placeholder, + Theme, + DebugLevel); + + await interop.LoadEditorContent(_editorElement, _richhtml); + + // preserve a copy of the content (Quill sanitizes content so we need to retrieve it from the editor as it may have been modified) + _originalrichhtml = await interop.GetHtml(_editorElement); + + _initialized = true; + } + else + { + if (_initialized) + { + if (_contentchanged) + { + // reload editor if Content passed to component has changed + await interop.LoadEditorContent(_editorElement, _richhtml); + _originalrichhtml = await interop.GetHtml(_editorElement); + } + else + { + // preserve changed content on re-render event + var richhtml = await interop.GetHtml(_editorElement); + if (richhtml != _richhtml) + { + _richhtml = richhtml; + await interop.LoadEditorContent(_editorElement, _richhtml); + } + } + } + } + + _contentchanged = false; + } + } + + public void Initialize(string content) + { + _richhtml = content; + _rawhtml = content; + _originalrawhtml = _rawhtml; // preserve for comparison later + _originalrichhtml = ""; + _richhtml = content; + _contentchanged = content != _originalrawhtml; + + StateHasChanged(); + } + + public async Task GetContent() + { + // evaluate raw html content as first priority + if (_rawhtml != _originalrawhtml) + { + return _rawhtml; + } + else + { + var richhtml = ""; + + if (AllowRichText) + { + richhtml = await interop.GetHtml(_editorElement); + } + + if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml)) + { + // convert Quill's empty content to empty string + if (richhtml == "


") + { + richhtml = string.Empty; + } + return richhtml; + } + else + { + // return original raw html content + return _originalrawhtml; + } + } + } + + public void CloseRichFileManager() + { + _richfilemanager = false; + _message = string.Empty; + StateHasChanged(); + } + + public void CloseRawFileManager() + { + _rawfilemanager = false; + _message = string.Empty; + StateHasChanged(); + } + + public async Task GetHtml() + { + // evaluate raw html content as first priority + if (_rawhtml != _originalrawhtml) + { + return _rawhtml; + } + else + { + var richhtml = ""; + + if (AllowRichText) + { + richhtml = await interop.GetHtml(_editorElement); + } + + if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml)) + { + // convert Quill's empty content to empty string + if (richhtml == "


") + { + richhtml = string.Empty; + } + return richhtml; + } + else + { + // return original raw html content + return _originalrawhtml; + } + } + } + + public async Task InsertRichImage() + { + _message = string.Empty; + if (_richfilemanager) + { + var file = _fileManager.GetFile(); + if (file != null) + { + await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name), _editorIndex); + _richhtml = await interop.GetHtml(_editorElement); + _richfilemanager = false; + } + else + { + _message = Localizer["Message.Require.Image"]; + } + } + else + { + _editorIndex = await interop.GetCurrentCursor(_editorElement); + _richfilemanager = true; + } + StateHasChanged(); + } + + public async Task InsertRawImage() + { + _message = string.Empty; + if (_rawfilemanager) + { + var file = _fileManager.GetFile(); + if (file != null) + { + var interop = new Interop(JSRuntime); + int pos = await interop.GetCaretPosition(_rawhtmlid); + var image = "\"""; + _rawhtml = _rawhtml.Substring(0, pos) + image + _rawhtml.Substring(pos); + _rawfilemanager = false; + } + else + { + _message = Localizer["Message.Require.Image"]; + } + } + else + { + _rawfilemanager = true; + } + StateHasChanged(); + } +} diff --git a/Oqtane.Client/Modules/Controls/RichTextEditor.razor b/Oqtane.Client/Modules/Controls/RichTextEditor.razor index 83986c35..52d03be8 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditor.razor +++ b/Oqtane.Client/Modules/Controls/RichTextEditor.razor @@ -1,128 +1,22 @@ @using System.Text.RegularExpressions +@using Microsoft.AspNetCore.Components.Rendering +@using Microsoft.Extensions.DependencyInjection @namespace Oqtane.Modules.Controls @inherits ModuleControlBase +@inject IServiceProvider ServiceProvider @inject ISettingService SettingService @inject IStringLocalizer Localizer
- - @if (AllowRichText) - { - - @if (_richfilemanager) - { - - -
- } -
- @if (AllowFileManagement) - { - - } - @if (_richfilemanager) - { - @((MarkupString)"  ") - - } -
-
-
-
- @if (ToolbarContent != null) - { - @ToolbarContent - } - else - { - - - - - - - - - - - - - - - - - - - } -
-
-
-
-
- } - @if (AllowRawHtml) - { - - @if (_rawfilemanager) - { - - -
- } -
- @if (AllowFileManagement) - { - - } - @if (_rawfilemanager) - { - @((MarkupString)"  ") - - } -
- @if (ReadOnly) - { - - } - else - { - - } -
- } -
+ @_textEditorComponent
@code { - private bool _initialized = false; - - private RichTextEditorInterop interop; - private FileManager _fileManager; - private string _activetab = "Rich"; - - private ElementReference _editorElement; - private ElementReference _toolBar; - private bool _richfilemanager = false; - private string _richhtml = string.Empty; - private string _originalrichhtml = string.Empty; - - private bool _rawfilemanager = false; - private string _rawhtmlid = "RawHtmlEditor_" + Guid.NewGuid().ToString("N"); - private string _rawhtml = string.Empty; - private string _originalrawhtml = string.Empty; - - private string _message = string.Empty; - private bool _contentchanged = false; - private int _editorIndex; + private ITextEditorProvider _textEditorProvider; + private RenderFragment _textEditorComponent; + private ITextEditor _textEditor; [Parameter] public string Content { get; set; } @@ -147,190 +41,114 @@ public RenderFragment ToolbarContent { get; set; } [Parameter] - public string Theme { get; set; } = "snow"; + public string Theme { get; set; } [Parameter] - public string DebugLevel { get; set; } = "info"; + public string DebugLevel { get; set; } - public override List Resources => new List() - { - new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js", Location = ResourceLocation.Body }, - new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js", Location = ResourceLocation.Body }, - new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js", Location = ResourceLocation.Body } - }; + public override List Resources { get; set; } = new List(); - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - interop = new RichTextEditorInterop(JSRuntime); - if (string.IsNullOrEmpty(Placeholder)) - { - Placeholder = Localizer["Placeholder"]; - } + _textEditorProvider = await GetTextEditorProvider(); } protected override void OnParametersSet() { - _richhtml = Content; - _rawhtml = Content; - _originalrawhtml = _rawhtml; // preserve for comparison later - _originalrichhtml = ""; - - if (Content != _originalrawhtml) + _textEditorComponent = (builder) => { - _contentchanged = true; // identifies when Content parameter has changed - } - - if (!AllowRichText) - { - _activetab = "Raw"; - } + CreateTextEditor(builder); + }; } protected override async Task OnAfterRenderAsync(bool firstRender) { - await base.OnAfterRenderAsync(firstRender); - - if (AllowRichText) + if(_textEditor != null) { - if (firstRender) - { - await interop.CreateEditor( - _editorElement, - _toolBar, - ReadOnly, - Placeholder, - Theme, - DebugLevel); - - await interop.LoadEditorContent(_editorElement, _richhtml); - - // preserve a copy of the content (Quill sanitizes content so we need to retrieve it from the editor as it may have been modified) - _originalrichhtml = await interop.GetHtml(_editorElement); - - _initialized = true; - } - else - { - if (_initialized) - { - if (_contentchanged) - { - // reload editor if Content passed to component has changed - await interop.LoadEditorContent(_editorElement, _richhtml); - _originalrichhtml = await interop.GetHtml(_editorElement); - } - else - { - // preserve changed content on re-render event - var richhtml = await interop.GetHtml(_editorElement); - if (richhtml != _richhtml) - { - _richhtml = richhtml; - await interop.LoadEditorContent(_editorElement, _richhtml); - } - } - } - } - - _contentchanged = false; + _textEditor.Initialize(Content); } - } - public void CloseRichFileManager() - { - _richfilemanager = false; - _message = string.Empty; - StateHasChanged(); - } - - public void CloseRawFileManager() - { - _rawfilemanager = false; - _message = string.Empty; - StateHasChanged(); + await base.OnAfterRenderAsync(firstRender); } public async Task GetHtml() { - // evaluate raw html content as first priority - if (_rawhtml != _originalrawhtml) - { - return _rawhtml; - } - else - { - var richhtml = ""; + return await _textEditor.GetContent(); + } - if (AllowRichText) + private void CreateTextEditor(RenderTreeBuilder builder) + { + if(_textEditorProvider != null) + { + var editorType = Type.GetType(_textEditorProvider.EditorType); + if (editorType != null) { - richhtml = await interop.GetHtml(_editorElement); - } + builder.OpenComponent(0, editorType); - if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml)) - { - // convert Quill's empty content to empty string - if (richhtml == "


") + //set editor parameters if available. + var attributes = new Dictionary { - richhtml = string.Empty; + { "AllowFileManagement", AllowFileManagement }, + { "AllowRichText", AllowRichText }, + { "AllowRawHtml", AllowRawHtml }, + { "ReadOnly", ReadOnly } + }; + + if(!string.IsNullOrEmpty(Theme)) + { + attributes.Add("Theme", Theme); } - return richhtml; - } - else - { - // return original raw html content - return _originalrawhtml; + if (!string.IsNullOrEmpty(DebugLevel)) + { + attributes.Add("DebugLevel", DebugLevel); + } + if (!string.IsNullOrEmpty(Placeholder)) + { + attributes.Add("Placeholder", Placeholder); + } + if(ToolbarContent != null) + { + attributes.Add("ToolbarContent", ToolbarContent); + } + + var index = 1; + foreach(var name in attributes.Keys) + { + if (editorType.GetProperty(name) != null) + { + builder.AddAttribute(index++, name, attributes[name]); + } + } + + builder.AddComponentReferenceCapture(index, (c) => + { + _textEditor = (ITextEditor)c; + }); + builder.CloseComponent(); } } } - public async Task InsertRichImage() + private async Task GetTextEditorProvider() { - _message = string.Empty; - if (_richfilemanager) - { - var file = _fileManager.GetFile(); - if (file != null) - { - await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name), _editorIndex); - _richhtml = await interop.GetHtml(_editorElement); - _richfilemanager = false; - } - else - { - _message = Localizer["Message.Require.Image"]; - } - } - else - { - _editorIndex = await interop.GetCurrentCursor(_editorElement); - _richfilemanager = true; - } - StateHasChanged(); - } + const string DefaultEditorName = "Quill"; - public async Task InsertRawImage() - { - _message = string.Empty; - if (_rawfilemanager) - { - var file = _fileManager.GetFile(); - if (file != null) - { - var interop = new Interop(JSRuntime); - int pos = await interop.GetCaretPosition(_rawhtmlid); - var image = "\"""; - _rawhtml = _rawhtml.Substring(0, pos) + image + _rawhtml.Substring(pos); - _rawfilemanager = false; - } - else - { - _message = Localizer["Message.Require.Image"]; - } - } - else - { - _rawfilemanager = true; - } - StateHasChanged(); - } + var editorName = await GetTextEditorName(DefaultEditorName); + var editorProviders = ServiceProvider.GetServices(); + var editorProvider = editorProviders.FirstOrDefault(i => i.Name == editorName); + if(editorProvider == null) + { + editorProvider = editorProviders.FirstOrDefault(i => i.Name == DefaultEditorName); + } + + return editorProvider; + } + + private async Task GetTextEditorName(string defaultName) + { + const string EditorSettingName = "TextEditor"; + + var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); + return SettingService.GetSetting(settings, EditorSettingName, defaultName); + } } diff --git a/Oqtane.Client/Providers/QuillTextEditorProvider.cs b/Oqtane.Client/Providers/QuillTextEditorProvider.cs new file mode 100644 index 00000000..d591a590 --- /dev/null +++ b/Oqtane.Client/Providers/QuillTextEditorProvider.cs @@ -0,0 +1,11 @@ +using Oqtane.Interfaces; + +namespace Oqtane.Providers +{ + public class QuillTextEditorProvider : ITextEditorProvider + { + public string Name => "Quill"; + + public string EditorType => "Oqtane.Modules.Controls.QuillTextEditor, Oqtane.Client"; + } +} diff --git a/Oqtane.Client/Resources/Modules/Controls/RichTextEditor.resx b/Oqtane.Client/Resources/Modules/Controls/QuillTextEditor.resx similarity index 100% rename from Oqtane.Client/Resources/Modules/Controls/RichTextEditor.resx rename to Oqtane.Client/Resources/Modules/Controls/QuillTextEditor.resx index 4e2b64c5..e8180682 100644 --- a/Oqtane.Client/Resources/Modules/Controls/RichTextEditor.resx +++ b/Oqtane.Client/Resources/Modules/Controls/QuillTextEditor.resx @@ -117,12 +117,12 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Insert Image - Close + + Insert Image + You Must Select An Image To Insert diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index d018a4bc..0eaf05dd 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using Oqtane.Infrastructure; using Oqtane.Infrastructure.Interfaces; +using Oqtane.Interfaces; using Oqtane.Managers; using Oqtane.Modules; using Oqtane.Providers; @@ -101,6 +102,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + return services; } @@ -148,6 +150,9 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); + // providers + services.AddTransient(); + // obsolete - replaced by ITenantManager services.AddTransient(); diff --git a/Oqtane.Shared/Interfaces/ITextEditor.cs b/Oqtane.Shared/Interfaces/ITextEditor.cs new file mode 100644 index 00000000..67f4efab --- /dev/null +++ b/Oqtane.Shared/Interfaces/ITextEditor.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; + +namespace Oqtane.Interfaces +{ + /// + /// Text editor interface. + /// + public interface ITextEditor + { + /// + /// initializes the editor with the initialize content. + /// + /// the initialize content. + void Initialize(string content); + + /// + /// get content from the editor. + /// + /// + Task GetContent(); + } +} diff --git a/Oqtane.Shared/Interfaces/ITextEditorProvider.cs b/Oqtane.Shared/Interfaces/ITextEditorProvider.cs new file mode 100644 index 00000000..a00c6817 --- /dev/null +++ b/Oqtane.Shared/Interfaces/ITextEditorProvider.cs @@ -0,0 +1,18 @@ +namespace Oqtane.Interfaces +{ + /// + /// Rich text editor provider interface. + /// + public interface ITextEditorProvider + { + /// + /// The text editor provider name. + /// + string Name { get; } + + /// + /// The text editor type full name. + /// + string EditorType { get; } + } +}