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..5bb183b2 --- /dev/null +++ b/Oqtane.Client/Modules/Controls/QuillTextEditor.razor @@ -0,0 +1,210 @@ +@namespace Oqtane.Modules.Controls +@inherits ModuleControlBase +@implements ITextEditor +@inject IStringLocalizer Localizer + +
+ @if (_richfilemanager) + { + + +
+ } +
+ @if (AllowFileManagement) + { + + } + @if (_richfilemanager) + { + @((MarkupString)"  ") + + } +
+
+
+
+ @if (ToolbarContent != null) + { + @ToolbarContent + } + else + { + + + + + + + + + + + + + + + + + + + } +
+
+
+
+
+ +@code { + private bool _richfilemanager = false; + private FileManager _fileManager; + private string _message = string.Empty; + private ElementReference _editorElement; + private ElementReference _toolBar; + private QuillEditorInterop interop; + private int _editorIndex; + private string _richhtml = string.Empty; + private string _originalrichhtml = string.Empty; + private bool _initialized = false; + private bool _contentchanged = false; + + [Parameter] + public bool AllowFileManagement{ get; set; } + + [Parameter] + public bool ReadOnly { get; set; } + + [Parameter] + public string Placeholder { get; set; } + + [Parameter] + public string Theme { get; set; } + + [Parameter] + public string DebugLevel { get; set; } + + [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); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); + + 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, bool updated) + { + _richhtml = content; + _contentchanged = updated; + } + + 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 void CloseRichFileManager() + { + _richfilemanager = false; + _message = string.Empty; + StateHasChanged(); + } + + public async Task GetContent() + { + var 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 null; + } + } +} diff --git a/Oqtane.Client/Modules/Controls/RichTextEditor.razor b/Oqtane.Client/Modules/Controls/RichTextEditor.razor index 83986c35..907fff94 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditor.razor +++ b/Oqtane.Client/Modules/Controls/RichTextEditor.razor @@ -1,6 +1,9 @@ @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 @@ -10,62 +13,7 @@ @if (AllowRichText) { - @if (_richfilemanager) - { - - -
- } -
- @if (AllowFileManagement) - { - - } - @if (_richfilemanager) - { - @((MarkupString)"  ") - - } -
-
-
-
- @if (ToolbarContent != null) - { - @ToolbarContent - } - else - { - - - - - - - - - - - - - - - - - - - } -
-
-
-
+ @_textEditorComponent
} @if (AllowRawHtml) @@ -103,26 +51,18 @@ @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 FileManager _fileManager; + private string _message = string.Empty; 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; } @@ -152,96 +92,47 @@ [Parameter] public string DebugLevel { get; set; } = "info"; - 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"]; } + + if(AllowRichText) + { + _textEditorProvider = await GetTextEditorProvider(); + } } protected override void OnParametersSet() { - _richhtml = Content; _rawhtml = Content; _originalrawhtml = _rawhtml; // preserve for comparison later - _originalrichhtml = ""; - - if (Content != _originalrawhtml) - { - _contentchanged = true; // identifies when Content parameter has changed - } if (!AllowRichText) { _activetab = "Raw"; } + + _textEditorComponent = (builder) => + { + 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, Content != _originalrawhtml); } + + await base.OnAfterRenderAsync(firstRender); } - public void CloseRichFileManager() - { - _richfilemanager = false; - _message = string.Empty; - StateHasChanged(); - } public void CloseRawFileManager() { @@ -259,78 +150,101 @@ } else { - var richhtml = ""; - - if (AllowRichText) + var richhtml = string.Empty; + if (AllowRichText && _textEditor != null) { - richhtml = await interop.GetHtml(_editorElement); + richhtml = await _textEditor.GetContent(); } - if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml)) + return richhtml != null ? richhtml : _originalrawhtml; + } + } + + + public async Task InsertRawImage() + { + _message = string.Empty; + if (_rawfilemanager) + { + var file = _fileManager.GetFile(); + if (file != null) { - // convert Quill's empty content to empty string - if (richhtml == "


") - { - richhtml = string.Empty; - } - return richhtml; + 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 { - // return original raw html content - return _originalrawhtml; + _message = Localizer["Message.Require.Image"]; + } + } + else + { + _rawfilemanager = true; + } + StateHasChanged(); + } + + private void CreateTextEditor(RenderTreeBuilder builder) + { + if(_textEditorProvider != null) + { + var editorType = Type.GetType(_textEditorProvider.EditorType); + if (editorType != null) + { + builder.OpenComponent(0, editorType); + + //set editor parameters if available. + var attributes = new Dictionary + { + { "AllowFileManagement", AllowFileManagement }, + { "ReadOnly", ReadOnly }, + { "Placeholder", Placeholder }, + { "Theme", Theme }, + { "DebugLevel", DebugLevel }, + { "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/QuillTextEditor.resx b/Oqtane.Client/Resources/Modules/Controls/QuillTextEditor.resx new file mode 100644 index 00000000..5ac2f720 --- /dev/null +++ b/Oqtane.Client/Resources/Modules/Controls/QuillTextEditor.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Insert Image + + + Close + + + You Must Select An Image To Insert + + \ No newline at end of file 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..8ebc32c4 --- /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, bool updated); + + /// + /// 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; } + } +}