Fix #4358: RichTextEditor Provider Abstraction.

This commit is contained in:
Ben 2024-07-01 17:11:26 +08:00
parent 1eafed755d
commit e00c261777
21 changed files with 726 additions and 342 deletions

View File

@ -1,6 +1,7 @@
@namespace Oqtane.Modules.Admin.Site @namespace Oqtane.Modules.Admin.Site
@inherits ModuleBase @inherits ModuleBase
@using System.Text.RegularExpressions @using System.Text.RegularExpressions
@using Microsoft.Extensions.DependencyInjection
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ISiteService SiteService @inject ISiteService SiteService
@inject ITenantService TenantService @inject ITenantService TenantService
@ -8,6 +9,7 @@
@inject IAliasService AliasService @inject IAliasService AliasService
@inject IThemeService ThemeService @inject IThemeService ThemeService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject INotificationService NotificationService @inject INotificationService NotificationService
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -123,6 +125,28 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="textEditorProvider" HelpText="Select the text editor provider for the site" ResourceKey="TextEditorProvider">Text Editor Provider: </Label>
<div class="col-sm-9">
<select id="textEditorProvider" class="form-select" value="@_textEditorProvider" required @onchange="TextEditorProviderChanged">
@if (_textEditorProviders != null)
{
@foreach (var provider in _textEditorProviders)
{
<option value="@provider.EditorType">@provider.Name</option>
}
}
</select>
</div>
</div>
@if (_textEditorProviderSettings != null)
{
<div class="row mb-1 align-items-center">
<div class="col-sm-9 offset-sm-3">
@_textEditorProviderSettings
</div>
</div>
}
</div> </div>
</Section> </Section>
<Section Name="FileExtensions" Heading="File Extensions" ResourceKey="FileExtensions"> <Section Name="FileExtensions" Heading="File Extensions" ResourceKey="FileExtensions">
@ -438,6 +462,10 @@
private string _tenant = string.Empty; private string _tenant = string.Empty;
private string _database = string.Empty; private string _database = string.Empty;
private string _connectionstring = string.Empty; private string _connectionstring = string.Empty;
private string _textEditorProvider = "";
private IEnumerable<ITextEditorProvider> _textEditorProviders;
private RenderFragment _textEditorProviderSettings;
private ISettingsControl _textEditorProviderSettingsControl;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
private string _modifiedby; private string _modifiedby;
@ -479,6 +507,7 @@
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype); _containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer; _containertype = (!string.IsNullOrEmpty(site.DefaultContainerType)) ? site.DefaultContainerType : Constants.DefaultContainer;
_admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer; _admincontainertype = (!string.IsNullOrEmpty(site.AdminContainerType)) ? site.AdminContainerType : Constants.DefaultAdminContainer;
_textEditorProviders = ServiceProvider.GetServices<ITextEditorProvider>();
// page content // page content
_headcontent = site.HeadContent; _headcontent = site.HeadContent;
@ -517,6 +546,10 @@
// aliases // aliases
await GetAliases(); await GetAliases();
//text editor
_textEditorProvider = SettingService.GetSetting(settings, "TextEditorProvider", Constants.DefaultTextEditorProvider);
LoadTextEditorProviderSettingsControl();
// hosting model // hosting model
_rendermode = site.RenderMode; _rendermode = site.RenderMode;
_runtime = site.Runtime; _runtime = site.Runtime;
@ -692,10 +725,18 @@
settings = SettingService.SetSetting(settings, "ImageFiles", (_ImageFiles != Constants.ImageFiles) ? _ImageFiles.Replace(" ", "") : "", false); settings = SettingService.SetSetting(settings, "ImageFiles", (_ImageFiles != Constants.ImageFiles) ? _ImageFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_UploadableFiles != Constants.UploadableFiles) ? _UploadableFiles.Replace(" ", "") : "", false); settings = SettingService.SetSetting(settings, "UploadableFiles", (_UploadableFiles != Constants.UploadableFiles) ? _UploadableFiles.Replace(" ", "") : "", false);
//text editor
settings = SettingService.SetSetting(settings, "TextEditorProvider", _textEditorProvider);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await logger.LogInformation("Site Settings Saved {Site}", site); await logger.LogInformation("Site Settings Saved {Site}", site);
if(_textEditorProviderSettingsControl != null)
{
await _textEditorProviderSettingsControl.UpdateSettings();
}
NavigationManager.NavigateTo(NavigateUrl(), true); // reload NavigationManager.NavigateTo(NavigateUrl(), true); // reload
} }
} }
@ -885,4 +926,35 @@
_aliasname = ""; _aliasname = "";
StateHasChanged(); StateHasChanged();
} }
private void TextEditorProviderChanged(ChangeEventArgs e)
{
_textEditorProvider = e.Value.ToString();
LoadTextEditorProviderSettingsControl();
StateHasChanged();
}
private void LoadTextEditorProviderSettingsControl()
{
var provider = _textEditorProviders.FirstOrDefault(i => i.EditorType == _textEditorProvider);
var settingsType = provider != null && !string.IsNullOrEmpty(provider.SettingsType) ? Type.GetType(provider.SettingsType) : null;
if (settingsType != null)
{
_textEditorProviderSettings = builder =>
{
builder.OpenComponent(0, settingsType);
builder.AddComponentReferenceCapture(1, (c) =>
{
_textEditorProviderSettingsControl = (ISettingsControl)c;
});
builder.CloseComponent();
};
}
else
{
_textEditorProviderSettings = null;
_textEditorProviderSettingsControl = null;
}
}
} }

View File

@ -1,11 +1,12 @@
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@inherits ModuleControlBase @inherits ModuleControlBase
@implements ITextEditor @implements ITextEditor
@inject IStringLocalizer<QuillTextEditor> Localizer @inject ISettingService SettingService
@inject IStringLocalizer<QuillJSTextEditor> Localizer
<div class="quill-text-editor"> <div class="quill-text-editor">
<TabStrip ActiveTab="@_activetab"> <TabStrip ActiveTab="@_activetab">
@if (AllowRichText) @if (_allowRichText)
{ {
<TabPanel Name="Rich" Heading="Rich Text Editor" ResourceKey="RichTextEditor"> <TabPanel Name="Rich" Heading="Rich Text Editor" ResourceKey="RichTextEditor">
@if (_richfilemanager) @if (_richfilemanager)
@ -15,7 +16,7 @@
<br /> <br />
} }
<div class="d-flex justify-content-center mb-2"> <div class="d-flex justify-content-center mb-2">
@if (AllowFileManagement) @if (_allowFileManagement)
{ {
<button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button> <button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button>
} }
@ -28,9 +29,9 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div @ref="@_toolBar"> <div @ref="@_toolBar">
@if (ToolbarContent != null) @if (!string.IsNullOrEmpty(_toolbarContent))
{ {
@ToolbarContent @((MarkupString)_toolbarContent)
} }
else else
{ {
@ -66,7 +67,7 @@
</div> </div>
</TabPanel> </TabPanel>
} }
@if (AllowRawHtml) @if (_allowRawHtml)
{ {
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor"> <TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
@if (_rawfilemanager) @if (_rawfilemanager)
@ -76,7 +77,7 @@
<br /> <br />
} }
<div class="d-flex justify-content-center mb-2"> <div class="d-flex justify-content-center mb-2">
@if (AllowFileManagement) @if (_allowFileManagement)
{ {
<button type="button" class="btn btn-primary" @onclick="InsertRawImage">@Localizer["InsertImage"]</button> <button type="button" class="btn btn-primary" @onclick="InsertRawImage">@Localizer["InsertImage"]</button>
} }
@ -106,6 +107,14 @@
private FileManager _fileManager; private FileManager _fileManager;
private string _activetab = "Rich"; private string _activetab = "Rich";
private bool _allowFileManagement = false;
private bool _allowRawHtml = false;
private bool _allowRichText = false;
private string _theme = "snow";
private string _debugLevel = "info";
private string _toolbarContent = string.Empty;
private bool _settingsLoaded;
private ElementReference _editorElement; private ElementReference _editorElement;
private ElementReference _toolBar; private ElementReference _toolBar;
private bool _richfilemanager = false; private bool _richfilemanager = false;
@ -121,38 +130,22 @@
private bool _contentchanged = false; private bool _contentchanged = false;
private int _editorIndex; private int _editorIndex;
[Parameter]
public bool AllowFileManagement{ get; set; }
[Parameter]
public bool AllowRichText { get; set; } = true;
[Parameter]
public bool AllowRawHtml { get; set; } = true;
[Parameter] [Parameter]
public bool ReadOnly { get; set; } public bool ReadOnly { get; set; }
[Parameter] [Parameter]
public string Placeholder { get; set; } 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<Resource> Resources { get; set; } = new List<Resource>() public override List<Resource> Resources { get; set; } = new List<Resource>()
{ {
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.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-blot-formatter.min.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js", Location = ResourceLocation.Body } new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js", Location = ResourceLocation.Body },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.bubble.css" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.snow.css" }
}; };
protected override void OnInitialized() protected override async Task OnInitializedAsync()
{ {
interop = new QuillEditorInterop(JSRuntime); interop = new QuillEditorInterop(JSRuntime);
@ -160,11 +153,13 @@
{ {
Placeholder = Localizer["Placeholder"]; Placeholder = Localizer["Placeholder"];
} }
await LoadSettings();
} }
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
if (!AllowRichText) if (!_allowRichText)
{ {
_activetab = "Raw"; _activetab = "Raw";
} }
@ -174,17 +169,17 @@
{ {
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
if (AllowRichText) if (_allowRichText)
{ {
if (firstRender) if (_settingsLoaded && !_initialized)
{ {
await interop.CreateEditor( await interop.CreateEditor(
_editorElement, _editorElement,
_toolBar, _toolBar,
ReadOnly, ReadOnly,
Placeholder, Placeholder,
Theme, _theme,
DebugLevel); _debugLevel);
await interop.LoadEditorContent(_editorElement, _richhtml); await interop.LoadEditorContent(_editorElement, _richhtml);
@ -202,6 +197,8 @@
// reload editor if Content passed to component has changed // reload editor if Content passed to component has changed
await interop.LoadEditorContent(_editorElement, _richhtml); await interop.LoadEditorContent(_editorElement, _richhtml);
_originalrichhtml = await interop.GetHtml(_editorElement); _originalrichhtml = await interop.GetHtml(_editorElement);
_contentchanged = false;
} }
else else
{ {
@ -215,8 +212,6 @@
} }
} }
} }
_contentchanged = false;
} }
} }
@ -224,10 +219,14 @@
{ {
_richhtml = content; _richhtml = content;
_rawhtml = content; _rawhtml = content;
_originalrawhtml = _rawhtml; // preserve for comparison later
_originalrichhtml = ""; _originalrichhtml = "";
_richhtml = content; _richhtml = content;
if (!_contentchanged)
{
_contentchanged = content != _originalrawhtml; _contentchanged = content != _originalrawhtml;
}
_originalrawhtml = _rawhtml; // preserve for comparison later
StateHasChanged(); StateHasChanged();
} }
@ -243,7 +242,7 @@
{ {
var richhtml = ""; var richhtml = "";
if (AllowRichText) if (_allowRichText)
{ {
richhtml = await interop.GetHtml(_editorElement); richhtml = await interop.GetHtml(_editorElement);
} }
@ -290,7 +289,7 @@
{ {
var richhtml = ""; var richhtml = "";
if (AllowRichText) if (_allowRichText)
{ {
richhtml = await interop.GetHtml(_editorElement); richhtml = await interop.GetHtml(_editorElement);
} }
@ -362,4 +361,24 @@
} }
StateHasChanged(); StateHasChanged();
} }
private async Task LoadSettings()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowFileManagement = SettingService.GetSetting(settings, "QuillTextEditor_AllowFileManagement", "true") == "true";
_allowRawHtml = SettingService.GetSetting(settings, "QuillTextEditor_AllowRawHtml", "true") == "true";
_allowRichText = SettingService.GetSetting(settings, "QuillTextEditor_AllowRichText", "true") == "true";
_theme = SettingService.GetSetting(settings, "QuillTextEditor_Theme", "snow");
_debugLevel = SettingService.GetSetting(settings, "QuillTextEditor_DebugLevel", "info");
_toolbarContent = SettingService.GetSetting(settings, "QuillTextEditor_ToolbarContent", string.Empty);
_settingsLoaded = true;
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
} }

View File

@ -0,0 +1,99 @@
@namespace Oqtane.Modules.Controls
@inherits ModuleBase
@inject ISettingService SettingService
@implements Oqtane.Interfaces.ISettingsControl
@inject IStringLocalizer<QuillJSTextEditorSettings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="AllowFileManagement" ResourceKey="AllowFileManagement" ResourceType="@resourceType" HelpText="Specify If Editors Can Upload and Select Files">Allow File Management: </Label>
<div class="col-sm-9">
<input type="checkbox" id="AllowFileManagement" class="form-check-input" @bind="_allowFileManagement" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="AllowRawHtml" ResourceKey="AllowRawHtml" ResourceType="@resourceType" HelpText="Specify If Editors Can Enter Raw HTML">Allow Raw HTML: </Label>
<div class="col-sm-9">
<input type="checkbox" id="AllowRawHtml" class="form-check-input" @bind="_allowRawHtml" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="AllowRichText" ResourceKey="AllowRichText" ResourceType="@resourceType" HelpText="Specify If Editors Can Use Rich Text Editor">Allow Rich Text: </Label>
<div class="col-sm-9">
<input type="checkbox" id="AllowRichText" class="form-check-input" @bind="_allowRichText" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="Theme" ResourceKey="Theme" ResourceType="@resourceType" HelpText="Specify the Rich Text Editor's Theme">Theme: </Label>
<div class="col-sm-9">
<input type="text" id="Theme" class="form-control" @bind="_theme" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="DebugLevel" ResourceKey="DebugLevel" ResourceType="@resourceType" HelpText="Specify the Debug Level">Debug Level: </Label>
<div class="col-sm-9">
<select id="DebugLevel" class="form-select" @bind="_debugLevel">
@foreach (var level in _debugLevels)
{
<option value="@level">@level</option>
}
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="ToolbarContent" ResourceKey="ToolbarContent" ResourceType="@resourceType" HelpText="Specify the Toolbar Content">Toolbar Content: </Label>
<div class="col-sm-9">
<textarea id="ToolbarContent" class="form-control" @bind="_toolbarContent" rows="5" />
</div>
</div>
</div>
@code {
private string resourceType = "Oqtane.Modules.Controls.QuillJSTextEditorSettings, Oqtane.Client";
private bool _allowFileManagement;
private bool _allowRawHtml;
private bool _allowRichText;
private string _theme;
private string _debugLevel;
private string _toolbarContent;
private List<string> _debugLevels = new List<string> { "info", "log", "warn", "error" };
protected override async Task OnInitializedAsync()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_allowFileManagement = SettingService.GetSetting(settings, "QuillTextEditor_AllowFileManagement", "true") == "true";
_allowRawHtml = SettingService.GetSetting(settings, "QuillTextEditor_AllowRawHtml", "true") == "true";
_allowRichText = SettingService.GetSetting(settings, "QuillTextEditor_AllowRichText", "true") == "true";
_theme = SettingService.GetSetting(settings, "QuillTextEditor_Theme", "snow");
_debugLevel = SettingService.GetSetting(settings, "QuillTextEditor_DebugLevel", "info");
_toolbarContent = SettingService.GetSetting(settings, "QuillTextEditor_ToolbarContent", string.Empty);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "QuillTextEditor_AllowFileManagement", _allowFileManagement.ToString().ToLower());
settings = SettingService.SetSetting(settings, "QuillTextEditor_AllowRawHtml", _allowRawHtml.ToString().ToLower());
settings = SettingService.SetSetting(settings, "QuillTextEditor_AllowRichText", _allowRichText.ToString().ToLower());
settings = SettingService.SetSetting(settings, "QuillTextEditor_Theme", _theme);
settings = SettingService.SetSetting(settings, "QuillTextEditor_DebugLevel", _debugLevel);
settings = SettingService.SetSetting(settings, "QuillTextEditor_ToolbarContent", _toolbarContent);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -4,7 +4,6 @@
@namespace Oqtane.Modules.Controls @namespace Oqtane.Modules.Controls
@inherits ModuleControlBase @inherits ModuleControlBase
@inject IServiceProvider ServiceProvider @inject IServiceProvider ServiceProvider
@inject ISettingService SettingService
@inject IStringLocalizer<RichTextEditor> Localizer @inject IStringLocalizer<RichTextEditor> Localizer
<div class="row" style="margin-bottom: 50px;"> <div class="row" style="margin-bottom: 50px;">
@ -14,7 +13,7 @@
</div> </div>
@code { @code {
private ITextEditorProvider _textEditorProvider; private string _textEditorProvider;
private RenderFragment _textEditorComponent; private RenderFragment _textEditorComponent;
private ITextEditor _textEditor; private ITextEditor _textEditor;
@ -28,29 +27,14 @@
public string Placeholder { get; set; } public string Placeholder { get; set; }
[Parameter] [Parameter]
public bool AllowFileManagement { get; set; } = true; public string Provider { get; set; }
[Parameter] [Parameter(CaptureUnmatchedValues = true)]
public bool AllowRichText { get; set; } = true; public Dictionary<string, object> AdditionalAttributes { get; set; } = new Dictionary<string, object>();
[Parameter]
public bool AllowRawHtml { get; set; } = true;
// parameters only applicable to rich text editor
[Parameter]
public RenderFragment ToolbarContent { get; set; }
[Parameter]
public string Theme { get; set; }
[Parameter]
public string DebugLevel { get; set; }
public override List<Resource> Resources { get; set; } = new List<Resource>();
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
_textEditorProvider = await GetTextEditorProvider(); _textEditorProvider = await GetTextEditorType(ServiceProvider, PageState.Site.SiteId);
} }
protected override void OnParametersSet() protected override void OnParametersSet()
@ -71,6 +55,22 @@
await base.OnAfterRenderAsync(firstRender); await base.OnAfterRenderAsync(firstRender);
} }
public override async Task<List<Resource>> GetResources(IServiceProvider serviceProvider, Page page)
{
var type = await GetTextEditorType(serviceProvider, page.SiteId);
if (!string.IsNullOrEmpty(type))
{
var editorType = Type.GetType(type);
if (editorType != null && editorType.GetInterfaces().Contains(typeof(IModuleControl)))
{
var control = Activator.CreateInstance(editorType) as IModuleControl;
return await control?.GetResources(serviceProvider, page);
}
}
return await base.GetResources(serviceProvider, page);
}
public async Task<string> GetHtml() public async Task<string> GetHtml()
{ {
return await _textEditor.GetContent(); return await _textEditor.GetContent();
@ -78,37 +78,32 @@
private void CreateTextEditor(RenderTreeBuilder builder) private void CreateTextEditor(RenderTreeBuilder builder)
{ {
if(_textEditorProvider != null) if(!string.IsNullOrEmpty(_textEditorProvider))
{ {
var editorType = Type.GetType(_textEditorProvider.EditorType); var editorType = Type.GetType(_textEditorProvider);
if (editorType != null) if (editorType != null)
{ {
builder.OpenComponent(0, editorType); builder.OpenComponent(0, editorType);
//set editor parameters if available.
var attributes = new Dictionary<string, object> var attributes = new Dictionary<string, object>
{ {
{ "AllowFileManagement", AllowFileManagement }, { "Placeholder", Placeholder },
{ "AllowRichText", AllowRichText },
{ "AllowRawHtml", AllowRawHtml },
{ "ReadOnly", ReadOnly } { "ReadOnly", ReadOnly }
}; };
if(!string.IsNullOrEmpty(Theme)) if (AdditionalAttributes != null)
{ {
attributes.Add("Theme", Theme); foreach(var key in AdditionalAttributes.Keys)
{
if(!attributes.ContainsKey(key))
{
attributes.Add(key, AdditionalAttributes[key]);
} }
if (!string.IsNullOrEmpty(DebugLevel)) else
{ {
attributes.Add("DebugLevel", DebugLevel); attributes[key] = AdditionalAttributes[key];
} }
if (!string.IsNullOrEmpty(Placeholder))
{
attributes.Add("Placeholder", Placeholder);
} }
if(ToolbarContent != null)
{
attributes.Add("ToolbarContent", ToolbarContent);
} }
var index = 1; var index = 1;
@ -129,26 +124,21 @@
} }
} }
private async Task<ITextEditorProvider> GetTextEditorProvider() private async Task<string> GetTextEditorType(IServiceProvider serviceProvider, int siteId)
{ {
const string DefaultEditorName = "Quill"; const string EditorSettingName = "TextEditorProvider";
var editorName = await GetTextEditorName(DefaultEditorName); if(!string.IsNullOrEmpty(Provider))
var editorProviders = ServiceProvider.GetServices<ITextEditorProvider>();
var editorProvider = editorProviders.FirstOrDefault(i => i.Name == editorName);
if(editorProvider == null)
{ {
editorProvider = editorProviders.FirstOrDefault(i => i.Name == DefaultEditorName); var provider = serviceProvider.GetServices<ITextEditorProvider>().FirstOrDefault(i => i.Name.Equals(Provider, StringComparison.OrdinalIgnoreCase));
if(provider != null)
{
return provider.EditorType;
}
} }
return editorProvider; var settingService = serviceProvider.GetService<ISettingService>();
} var settings = await settingService.GetSiteSettingsAsync(siteId);
return settingService.GetSetting(settings, EditorSettingName, Constants.DefaultTextEditorProvider);
private async Task<string> GetTextEditorName(string defaultName)
{
const string EditorSettingName = "TextEditor";
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
return SettingService.GetSetting(settings, EditorSettingName, defaultName);
} }
} }

View File

@ -0,0 +1,31 @@
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@implements ITextEditor
<div class="text-area-editor">
<textarea @bind="_content" @ref="_editor" placeholder="@Placeholder" readonly="@ReadOnly" />
</div>
@code {
private ElementReference _editor;
private string _content;
[Parameter]
public bool ReadOnly { get; set; }
[Parameter]
public string Placeholder { get; set; }
public void Initialize(string content)
{
_content = content;
StateHasChanged();
}
public async Task<string> GetContent()
{
await Task.CompletedTask;
return _content;
}
}

View File

@ -13,7 +13,7 @@
<TabPanel Name="Edit" Heading="Edit" ResourceKey="Edit"> <TabPanel Name="Edit" Heading="Edit" ResourceKey="Edit">
@if (_content != null) @if (_content != null)
{ {
<RichTextEditor Content="@_content" AllowFileManagement="@_allowfilemanagement" AllowRawHtml="@_allowrawhtml" @ref="@RichTextEditorHtml"></RichTextEditor> <RichTextEditor Content="@_content" @ref="@RichTextEditorHtml"></RichTextEditor>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveContent">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveContent">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
@ -51,15 +51,7 @@
public override string Title => "Edit Html/Text"; public override string Title => "Edit Html/Text";
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.bubble.css" },
new Resource { ResourceType = ResourceType.Stylesheet, Url = "css/quill/quill.snow.css" }
};
private RichTextEditor RichTextEditorHtml; private RichTextEditor RichTextEditorHtml;
private bool _allowfilemanagement;
private bool _allowrawhtml;
private string _content = null; private string _content = null;
private string _createdby; private string _createdby;
private DateTime _createdon; private DateTime _createdon;
@ -68,12 +60,12 @@
private List<Models.HtmlText> _htmltexts; private List<Models.HtmlText> _htmltexts;
private string _view = ""; private string _view = "";
public override List<string> ResourcesRegistrationTypes => new List<string> { typeof(RichTextEditor).FullName };
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
try try
{ {
_allowfilemanagement = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true"));
_allowrawhtml = bool.Parse(SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "true"));
await LoadContent(); await LoadContent();
} }
catch (Exception ex) catch (Exception ex)

View File

@ -15,7 +15,7 @@ namespace Oqtane.Modules.HtmlText
Version = "1.0.1", Version = "1.0.1",
ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server", ServerManagerType = "Oqtane.Modules.HtmlText.Manager.HtmlTextManager, Oqtane.Server",
ReleaseVersions = "1.0.0,1.0.1", ReleaseVersions = "1.0.0,1.0.1",
SettingsType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client", SettingsType = string.Empty,
Resources = new List<Resource>() Resources = new List<Resource>()
{ {
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" } new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Module.css" }

View File

@ -1,61 +0,0 @@
@namespace Oqtane.Modules.HtmlText
@inherits ModuleBase
@inject ISettingService SettingService
@implements Oqtane.Interfaces.ISettingsControl
@inject IStringLocalizer<Settings> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="files" ResourceKey="AllowFileManagement" ResourceType="@resourceType" HelpText="Specify If Editors Can Upload and Select Files">Allow File Management: </Label>
<div class="col-sm-9">
<select id="files" class="form-select" @bind="@_allowfilemanagement">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="files" ResourceKey="AllowRawHtml" ResourceType="@resourceType" HelpText="Specify If Editors Can Enter Raw HTML">Allow Raw HTML: </Label>
<div class="col-sm-9">
<select id="files" class="form-select" @bind="@_allowrawhtml">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
@code {
private string resourceType = "Oqtane.Modules.HtmlText.Settings, Oqtane.Client"; // for localization
private string _allowfilemanagement;
private string _allowrawhtml;
protected override void OnInitialized()
{
try
{
_allowfilemanagement = SettingService.GetSetting(ModuleState.Settings, "AllowFileManagement", "true");
_allowrawhtml = SettingService.GetSetting(ModuleState.Settings, "AllowRawHtml", "true");
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
public async Task UpdateSettings()
{
try
{
var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
settings = SettingService.SetSetting(settings, "AllowFileManagement", _allowfilemanagement);
settings = SettingService.SetSetting(settings, "AllowRawHtml", _allowrawhtml);
await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
}
catch (Exception ex)
{
AddModuleMessage(ex.Message, MessageType.Error);
}
}
}

View File

@ -50,6 +50,8 @@ namespace Oqtane.Modules
public virtual List<Resource> Resources { get; set; } public virtual List<Resource> Resources { get; set; }
public virtual List<string> ResourcesRegistrationTypes { get; }
public virtual string RenderMode { get { return RenderModes.Interactive; } } // interactive by default public virtual string RenderMode { get { return RenderModes.Interactive; } } // interactive by default
public virtual bool? Prerender { get { return null; } } // allows the Site Prerender property to be overridden public virtual bool? Prerender { get { return null; } } // allows the Site Prerender property to be overridden
@ -71,6 +73,21 @@ namespace Oqtane.Modules
// base lifecycle method for handling JSInterop script registration // base lifecycle method for handling JSInterop script registration
public virtual async Task<List<Resource>> GetResources(IServiceProvider serviceProvider, Page page)
{
var resources = Resources ?? new List<Resource>();
if(ResourcesRegistrationTypes != null)
{
foreach(var type in ResourcesRegistrationTypes)
{
resources.AddRange(await LoadResourcesFromType(serviceProvider, page, type));
}
}
return resources;
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
@ -482,6 +499,25 @@ namespace Oqtane.Modules
} }
} }
private async Task<List<Resource>> LoadResourcesFromType(IServiceProvider serviceProvider, Page page, string typeName)
{
try
{
var type = Type.GetType(typeName, false, true);
if (type != null && type.GetInterfaces().Contains(typeof(IModuleControl)))
{
var control = Activator.CreateInstance(type) as IModuleControl;
return await control?.GetResources(serviceProvider, page);
}
}
catch(Exception ex)
{
//await Log(null, LogLevel.Error, string.Empty, ex, "Load Resources From Type {Type} Failed. {Message}", typeName, ex.Message);
}
return null;
}
[Obsolete("ContentUrl(int fileId) is deprecated. Use FileUrl(int fileId) instead.", false)] [Obsolete("ContentUrl(int fileId) is deprecated. Use FileUrl(int fileId) instead.", false)]
public string ContentUrl(int fileid) public string ContentUrl(int fileid)
{ {

View File

@ -0,0 +1,13 @@
using Oqtane.Interfaces;
namespace Oqtane.Providers
{
public class QuillJSTextEditorProvider : ITextEditorProvider
{
public string Name => "QuillJS";
public string EditorType => "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client";
public string SettingsType => "Oqtane.Modules.Controls.QuillJSTextEditorSettings, Oqtane.Client";
}
}

View File

@ -1,11 +0,0 @@
using Oqtane.Interfaces;
namespace Oqtane.Providers
{
public class QuillTextEditorProvider : ITextEditorProvider
{
public string Name => "Quill";
public string EditorType => "Oqtane.Modules.Controls.QuillTextEditor, Oqtane.Client";
}
}

View File

@ -0,0 +1,13 @@
using Oqtane.Interfaces;
namespace Oqtane.Providers
{
public class TextAreaTextEditorProvider : ITextEditorProvider
{
public string Name => "TextArea";
public string EditorType => "Oqtane.Modules.Controls.TextAreaTextEditor, Oqtane.Client";
public string SettingsType => string.Empty;
}
}

View File

@ -429,4 +429,10 @@
<data name="Runtime.Text" xml:space="preserve"> <data name="Runtime.Text" xml:space="preserve">
<value>Interactivity:</value> <value>Interactivity:</value>
</data> </data>
<data name="TextEditorProvider.HelpText" xml:space="preserve">
<value>Select the text editor provider for the site</value>
</data>
<data name="TextEditorProvider.Text" xml:space="preserve">
<value>Text Editor Provider:</value>
</data>
</root> </root>

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AllowFileManagement.HelpText" xml:space="preserve">
<value>Specify If Editors Can Upload and Select Files</value>
</data>
<data name="AllowFileManagement.Text" xml:space="preserve">
<value>Allow File Management:</value>
</data>
<data name="AllowRawHtml.HelpText" xml:space="preserve">
<value>Specify If Editors Can Enter Raw HTML</value>
</data>
<data name="AllowRawHtml.Text" xml:space="preserve">
<value>Allow Raw HTML:</value>
</data>
<data name="AllowRichText.HelpText" xml:space="preserve">
<value>Specify If Editors Can Use Rich Text Editor</value>
</data>
<data name="AllowRichText.Text" xml:space="preserve">
<value>Allow Rich Text: </value>
</data>
<data name="DebugLevel.HelpText" xml:space="preserve">
<value>Specify the Debug Level</value>
</data>
<data name="DebugLevel.Text" xml:space="preserve">
<value>Debug Level:</value>
</data>
<data name="Theme.HelpText" xml:space="preserve">
<value>Specify the Rich Text Editor's Theme</value>
</data>
<data name="Theme.Text" xml:space="preserve">
<value>Theme:</value>
</data>
<data name="ToolbarContent.HelpText" xml:space="preserve">
<value>Specify the Toolbar Content</value>
</data>
<data name="ToolbarContent.Text" xml:space="preserve">
<value>Toolbar Content:</value>
</data>
</root>

View File

@ -5,6 +5,7 @@
@using Microsoft.AspNetCore.Http.Extensions @using Microsoft.AspNetCore.Http.Extensions
@using Microsoft.AspNetCore.Antiforgery @using Microsoft.AspNetCore.Antiforgery
@using Microsoft.AspNetCore.Localization @using Microsoft.AspNetCore.Localization
@using Microsoft.Extensions.DependencyInjection
@using Microsoft.Net.Http.Headers @using Microsoft.Net.Http.Headers
@using Microsoft.Extensions.Primitives @using Microsoft.Extensions.Primitives
@using Oqtane.Client @using Oqtane.Client
@ -30,6 +31,7 @@
@inject IUrlMappingRepository UrlMappingRepository @inject IUrlMappingRepository UrlMappingRepository
@inject IVisitorRepository VisitorRepository @inject IVisitorRepository VisitorRepository
@inject IJwtManager JwtManager @inject IJwtManager JwtManager
@inject IServiceProvider ServiceProvider
@if (_initialized) @if (_initialized)
{ {
@ -674,7 +676,7 @@
var obj = Activator.CreateInstance(moduletype) as IModuleControl; var obj = Activator.CreateInstance(moduletype) as IModuleControl;
if (obj != null) if (obj != null)
{ {
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); resources = AddResources(resources, await obj.GetResources(ServiceProvider, page), ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
{ {
// settings components are embedded within a framework settings module // settings components are embedded within a framework settings module
@ -682,7 +684,7 @@
if (moduletype != null) if (moduletype != null)
{ {
obj = Activator.CreateInstance(moduletype) as IModuleControl; obj = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); resources = AddResources(resources, await obj.GetResources(ServiceProvider, page), ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
} }
} }
} }

View File

@ -151,7 +151,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient<IUserManager, UserManager>(); services.AddTransient<IUserManager, UserManager>();
// providers // providers
services.AddTransient<ITextEditorProvider, QuillTextEditorProvider>(); services.AddTransient<ITextEditorProvider, QuillJSTextEditorProvider>();
services.AddTransient<ITextEditorProvider, TextAreaTextEditorProvider>();
// obsolete - replaced by ITenantManager // obsolete - replaced by ITenantManager
services.AddTransient<ITenantResolver, TenantResolver>(); services.AddTransient<ITenantResolver, TenantResolver>();

View File

@ -249,3 +249,9 @@ app {
.app-search input + button .oi{ .app-search input + button .oi{
top: 0; top: 0;
} }
/* Text Editor */
.text-area-editor > textarea {
width: 100%;
min-height: 250px;
}

View File

@ -1,6 +1,8 @@
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Shared; using Oqtane.Shared;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace Oqtane.Modules namespace Oqtane.Modules
{ {
@ -31,6 +33,17 @@ namespace Oqtane.Modules
/// </summary> /// </summary>
List<Resource> Resources { get; } List<Resource> Resources { get; }
/// <summary>
/// The component types which need to register it's own resources.
/// </summary>
List<string> ResourcesRegistrationTypes { get; }
/// <summary>
/// Identifies all resources in a module including resources from resources registration types.
/// </summary>
/// <returns></returns>
Task<List<Resource>> GetResources(IServiceProvider serviceProvider, Page page);
/// <summary> /// <summary>
/// Specifies the required render mode for the module control ie. Static,Interactive /// Specifies the required render mode for the module control ie. Static,Interactive
/// </summary> /// </summary>

View File

@ -14,5 +14,10 @@ namespace Oqtane.Interfaces
/// The text editor type full name. /// The text editor type full name.
/// </summary> /// </summary>
string EditorType { get; } string EditorType { get; }
/// <summary>
/// The text editor settings type full name.
/// </summary>
string SettingsType { get; }
} }
} }

View File

@ -82,6 +82,8 @@ namespace Oqtane.Shared
public const string SearchPageIdPropertyName = "PageId"; public const string SearchPageIdPropertyName = "PageId";
public const string SearchModuleIdPropertyName = "ModuleId"; public const string SearchModuleIdPropertyName = "ModuleId";
public const string DefaultTextEditorProvider = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client";
// Obsolete constants // Obsolete constants
const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames"; const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames";