Merge pull request #5577 from zyhfish/task/radzen-text-editor

implement radzen text editor.
This commit is contained in:
Shaun Walker
2025-09-05 11:13:52 -04:00
committed by GitHub
13 changed files with 1069 additions and 4 deletions

View File

@ -1,8 +1,10 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Localization;
using Oqtane.Interfaces;
using Oqtane.Providers;
using Oqtane.Services;
using Oqtane.Shared;
using Radzen;
namespace Microsoft.Extensions.DependencyInjection
{
@ -23,7 +25,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<SiteState>();
services.AddScoped<IInstallationService, InstallationService>();
services.AddScoped<IModuleDefinitionService, ModuleDefinitionService>();
services.AddScoped<IThemeService, ThemeService>();
services.AddScoped<IThemeService, Oqtane.Services.ThemeService>();
services.AddScoped<IAliasService, AliasService>();
services.AddScoped<ITenantService, TenantService>();
services.AddScoped<ISiteService, SiteService>();
@ -39,7 +41,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ILogService, LogService>();
services.AddScoped<IJobService, JobService>();
services.AddScoped<IJobLogService, JobLogService>();
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<INotificationService, Oqtane.Services.NotificationService>();
services.AddScoped<IFolderService, FolderService>();
services.AddScoped<IFileService, FileService>();
services.AddScoped<ISiteTemplateService, SiteTemplateService>();
@ -59,6 +61,12 @@ namespace Microsoft.Extensions.DependencyInjection
// providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.TextAreaTextEditor>();
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.RadzenTextEditor>();
services.AddRadzenComponents();
var localizer = services.BuildServiceProvider().GetService<IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor>>();
Oqtane.Modules.Controls.RadzenEditorDefinitions.Localizer = localizer;
return services;
}

View File

@ -0,0 +1,45 @@
@namespace Oqtane.Modules.Controls
@using System.IO
@using Radzen
@using Radzen.Blazor
@inject DialogService DialogService
@inject IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor> Localizer
<div class="d-flex">
<FileManager @ref="_fileManager" Filter="@Filters" />
</div>
<div class="d-flex">
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
</div>
<div class="mt-1 text-end">
<RadzenButton Text="OK" Click=@OnOkClick />
<RadzenButton Text="Cancel" Click=@OnCancelClick ButtonStyle="ButtonStyle.Secondary" />
</div>
@code {
private FileManager _fileManager;
private string _message = string.Empty;
[Parameter]
public string Filters { get; set; }
private void OnCancelClick()
{
DialogService.Close(null);
}
private void OnOkClick()
{
_message = string.Empty;
var file = _fileManager.GetFile();
if (file != null)
{
var result = $"<img src=\"{file.Url}\" style=\"max-width: 100%\" alt=\"{file.Name}\" />";
DialogService.Close(result);
}
else
{
_message = Localizer["Message.Require.Image"];
StateHasChanged();
}
}
}

View File

@ -0,0 +1,85 @@
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Localization;
using System;
using System.Collections.Generic;
namespace Oqtane.Modules.Controls
{
public sealed class RadzenEditorDefinitions
{
public static IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor> Localizer { get; internal set; }
public const string TransparentBackgroundColor = "rgba(0, 0, 0, 0)";
public const string LightBackgroundColor = "rgba(255, 255, 255, 1)";
public const string DarkBackgroundColor = "rgba(0, 0, 0, 1)";
public const string DefaultTheme = "default";
public const string DefaultBackground = "Default";
public static readonly IDictionary<string, Func<RenderTreeBuilder, int, int>> ToolbarItems = new Dictionary<string, Func<RenderTreeBuilder, int, int>>()
{
{ "AlignCenter", (builder, sequence) => CreateFragment(builder, sequence, "AlignCenter", "RadzenHtmlEditorAlignCenter") },
{ "AlignLeft", (builder, sequence) => CreateFragment(builder, sequence, "AlignLeft", "RadzenHtmlEditorAlignLeft") },
{ "AlignRight", (builder, sequence) => CreateFragment(builder, sequence, "AlignRight", "RadzenHtmlEditorAlignRight") },
{ "Background", (builder, sequence) => CreateFragment(builder, sequence, "Background", "RadzenHtmlEditorBackground") },
{ "Color", (builder, sequence) => CreateFragment(builder, sequence, "Color", "RadzenHtmlEditorColor") },
{ "FontName", (builder, sequence) => CreateFragment(builder, sequence, "FontName", "RadzenHtmlEditorFontName") },
{ "FontSize", (builder, sequence) => CreateFragment(builder, sequence, "FontSize", "RadzenHtmlEditorFontSize") },
{ "FormatBlock", (builder, sequence) => CreateFragment(builder, sequence, "FormatBlock", "RadzenHtmlEditorFormatBlock") },
{ "Indent", (builder, sequence) => CreateFragment(builder, sequence, "Indent", "RadzenHtmlEditorIndent") },
{ "InsertImage", (builder, sequence) => CreateFragment(builder, sequence, "InsertImage", "RadzenHtmlEditorCustomTool", "InsertImage", "image") },
{ "Italic", (builder, sequence) => CreateFragment(builder, sequence, "Italic", "RadzenHtmlEditorItalic") },
{ "Justify", (builder, sequence) => CreateFragment(builder, sequence, "Justify", "RadzenHtmlEditorJustify") },
{ "Link", (builder, sequence) => CreateFragment(builder, sequence, "Link", "RadzenHtmlEditorLink") },
{ "OrderedList", (builder, sequence) => CreateFragment(builder, sequence, "OrderedList", "RadzenHtmlEditorOrderedList") },
{ "Outdent", (builder, sequence) => CreateFragment(builder, sequence, "Outdent", "RadzenHtmlEditorOutdent") },
{ "Redo", (builder, sequence) => CreateFragment(builder, sequence, "Redo", "RadzenHtmlEditorRedo") },
{ "RemoveFormat", (builder, sequence) => CreateFragment(builder, sequence, "RemoveFormat", "RadzenHtmlEditorRemoveFormat") },
{ "Separator", (builder, sequence) => CreateFragment(builder, sequence, "Separator", "RadzenHtmlEditorSeparator") },
{ "Source", (builder, sequence) => CreateFragment(builder, sequence, "Source", "RadzenHtmlEditorSource") },
{ "StrikeThrough", (builder, sequence) => CreateFragment(builder, sequence, "StrikeThrough", "RadzenHtmlEditorStrikeThrough") },
{ "Subscript", (builder, sequence) => CreateFragment(builder, sequence, "Subscript", "RadzenHtmlEditorSubscript") },
{ "Superscript", (builder, sequence) => CreateFragment(builder, sequence, "Superscript", "RadzenHtmlEditorSuperscript") },
{ "Underline", (builder, sequence) => CreateFragment(builder, sequence, "Underline", "RadzenHtmlEditorUnderline") },
{ "Undo", (builder, sequence) => CreateFragment(builder, sequence, "Undo", "RadzenHtmlEditorUndo") },
{ "Unlink", (builder, sequence) => CreateFragment(builder, sequence, "Unlink", "RadzenHtmlEditorUnlink") },
{ "UnorderedList", (builder, sequence) => CreateFragment(builder, sequence, "UnorderedList", "RadzenHtmlEditorUnorderedList") },
};
public static readonly string DefaultToolbarItems = "Undo,Redo,Separator,FontName,FontSize,FormatBlock,Bold,Italic,Underline,StrikeThrough,Separator,AlignLeft,AlignCenter,AlignRight,Justify,Separator,Indent,Outdent,UnorderedList,OrderedList,Separator,Color,Background,RemoveFormat,Separator,Subscript,Superscript,Separator,Link,Unlink,InsertImage,Separator,Source";
private static int CreateFragment(RenderTreeBuilder builder, int sequence, string name, string typeName, string commaneName = "", string icon = "")
{
var fullTypeName = $"Radzen.Blazor.{typeName}, Radzen.Blazor";
var type = Type.GetType(fullTypeName);
if (type != null)
{
var title = Localizer[$"{name}.Title"];
var placeholder = Localizer[$"{name}.Placeholder"];
builder.OpenComponent(sequence++, type);
if (!string.IsNullOrEmpty(title) && title != $"{name}.Title" && type.GetProperty("Title") != null)
{
builder.AddAttribute(sequence++, "Title", title);
}
if (!string.IsNullOrEmpty(placeholder) && placeholder != $"{name}.Placeholder" && type.GetProperty("Placeholder") != null)
{
builder.AddAttribute(sequence++, "Placeholder", placeholder);
}
if (!string.IsNullOrEmpty(commaneName) && type.GetProperty("CommandName") != null)
{
builder.AddAttribute(sequence++, "CommandName", commaneName);
}
if (!string.IsNullOrEmpty(icon) && type.GetProperty("Icon") != null)
{
builder.AddAttribute(sequence++, "Icon", icon);
}
builder.CloseComponent();
}
return sequence;
}
}
}

View File

@ -0,0 +1,60 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Threading.Tasks;
namespace Oqtane.Modules.Controls
{
public class RadzenEditorInterop
{
private readonly IJSRuntime _jsRuntime;
public RadzenEditorInterop(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public Task Initialize(ElementReference editor)
{
try
{
_jsRuntime.InvokeVoidAsync("Oqtane.RadzenTextEditor.initialize", editor);
}
catch
{
}
return Task.CompletedTask;
}
public Task SetBackgroundColor(ElementReference editor, string color)
{
try
{
_jsRuntime.InvokeVoidAsync(
"Oqtane.RadzenTextEditor.setBackgroundColor",
editor, color);
}
catch
{
}
return Task.CompletedTask;
}
public Task UpdateDialogLayout(ElementReference editor)
{
try
{
_jsRuntime.InvokeVoidAsync("Oqtane.RadzenTextEditor.updateDialogLayout", editor);
}
catch
{
}
return Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,11 @@
namespace Oqtane.Modules.Controls
{
public class RadzenEditorSetting
{
public string Theme { get; set; }
public string Background { get; set; }
public string ToolbarItems { get; set; }
}
}

View File

@ -0,0 +1,193 @@
@using Microsoft.Extensions.Configuration
@using Oqtane.Interfaces
@using System.Text.RegularExpressions
@using Radzen
@using Radzen.Blazor
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@implements ITextEditor
@implements IDisposable
@inject Radzen.ThemeService ThemeService
@inject IRadzenEditorSettingService EditorSettingService
@inject DialogService DialogService
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor> Localizer
<RadzenTheme Theme="@RadzenEditorDefinitions.DefaultTheme" />
<RadzenComponents />
<RadzenHtmlEditor @ref="_editor" Visible="_visible" Placeholder="@Placeholder" style="@($"height: {Height}px;")"
@bind-Value="_value" Execute="OnExecute" class="rz-text-editor">
<ChildContent>
@_toolbar
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
<RadzenHtmlEditorCustomTool CommandName="Settings" Icon="settings" Title="@Localizer["Settings"]" />
}
</ChildContent>
</RadzenHtmlEditor>
@code {
private Oqtane.Modules.Controls.RadzenEditorInterop _interop;
private RadzenHtmlEditor _editor;
private string _value;
private bool _visible = false;
private string _theme;
private string _background;
private IList<string> _toolbarItems;
private RenderFragment _toolbar;
[Parameter]
public string Placeholder { get; set; }
[Parameter]
public bool ReadOnly { get; set; }
[Parameter]
public int Height { get; set; } = 450;
public string Name => "RadzenTextEditor";
public override List<Resource> Resources { get; set; } = new List<Resource>()
{
new Resource { ResourceType = ResourceType.Script, Url = "/_content/Radzen.Blazor/Radzen.Blazor.js", Location = ResourceLocation.Head },
new Resource { ResourceType = ResourceType.Script, Url = "/Modules/Oqtane.RadzenTextEditor/Resources/js/Interops/RadzenTextEditorInterop.js", Location = ResourceLocation.Head },
};
protected override void OnInitialized()
{
_interop = new Oqtane.Modules.Controls.RadzenEditorInterop(JSRuntime);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
var interop = new Interop(JSRuntime);
await interop.IncludeLink("", "stylesheet", $"/Modules/Oqtane.RadzenTextEditor/Resources/css/radzentexteditor.override.css", "text/css", "", "", "");
await LoadSettings();
_visible = true;
StateHasChanged();
await _interop.Initialize(_editor.Element);
if (!string.IsNullOrEmpty(_theme))
{
ThemeService.SetTheme(_theme);
}
if (!string.IsNullOrEmpty(_background))
{
var backgroundColor = RadzenEditorDefinitions.TransparentBackgroundColor;
switch (_background)
{
case "Light":
backgroundColor = RadzenEditorDefinitions.LightBackgroundColor;
break;
case "Dark":
backgroundColor = RadzenEditorDefinitions.DarkBackgroundColor;
break;
}
await _interop.SetBackgroundColor(_editor.Element, backgroundColor);
}
}
}
public void Initialize(string content)
{
_value = !string.IsNullOrEmpty(content) ? content : string.Empty;
DialogService.OnOpen += OnDialogOpened;
}
public void Dispose()
{
if (DialogService != null)
{
DialogService.OnOpen -= OnDialogOpened;
}
}
public async Task<string> GetContent()
{
await Task.CompletedTask;
return _value;
}
private async Task LoadSettings()
{
var scope = await EditorSettingService.GetSettingScopeAsync(ModuleState.ModuleId);
var editorSetting = scope == 1
? await EditorSettingService.LoadSettingsFromModuleAsync(ModuleState.ModuleId)
: await EditorSettingService.LoadSettingsFromSiteAsync(PageState.Site.SiteId);
_theme = editorSetting.Theme;
_background = editorSetting.Background;
_toolbarItems = editorSetting.ToolbarItems.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
_toolbar = SetupToolbarItems();
}
private RenderFragment SetupToolbarItems()
{
return builder =>
{
var sequence = 0;
foreach (var item in _toolbarItems)
{
if (RadzenEditorDefinitions.ToolbarItems.ContainsKey(item))
{
sequence = RadzenEditorDefinitions.ToolbarItems[item](builder, sequence);
}
}
};
}
private async Task OnExecute(HtmlEditorExecuteEventArgs args)
{
if (args.CommandName == "InsertImage")
{
await InsertImage(args.Editor);
}
else if (args.CommandName == "Settings")
{
await UpdateSettings(args.Editor);
}
}
private async Task InsertImage(RadzenHtmlEditor editor)
{
await editor.SaveSelectionAsync();
var result = await DialogService.OpenAsync<FileManagerDialog>(Localizer["DialogTitle.SelectImage"], new Dictionary<string, object>
{
{ "Filters", PageState.Site.ImageFiles }
});
await editor.RestoreSelectionAsync();
if (result != null)
{
await editor.ExecuteCommandAsync(HtmlEditorCommands.InsertHtml, result);
}
}
private async Task UpdateSettings(RadzenHtmlEditor editor)
{
await editor.SaveSelectionAsync();
var result = await DialogService.OpenAsync<SettingsDialog>(Localizer["Settings"], null, new DialogOptions { Width = "650px" });
if (result == true)
{
NavigationManager.NavigateTo(NavigationManager.Uri);
}
await editor.RestoreSelectionAsync();
}
private async void OnDialogOpened(string title, Type componentType, Dictionary<string, object> parameters, DialogOptions options)
{
await _interop.UpdateDialogLayout(_editor.Element);
}
}

View File

@ -0,0 +1,222 @@
@namespace Oqtane.Modules.Controls
@using System.IO
@using Radzen
@using Radzen.Blazor
@inherits ModuleControlBase
@inject DialogService DialogService
@inject Radzen.ThemeService ThemeService
@inject ISettingService SettingService
@inject IRadzenEditorSettingService EditorSettingService
@inject IStringLocalizer<Oqtane.Modules.Controls.RadzenTextEditor> Localizer
<div class="row">
<div class="col-12 col-sm-3">
@Localizer["Scope"]
</div>
<div class="col-12 col-sm-9">
<RadzenRadioButtonList @bind-Value="@_settingScope" TValue="int" Change="OnScopeChanged">
<Items>
<RadzenRadioButtonListItem Text="@Localizer["Site"]" Value="0" />
<RadzenRadioButtonListItem Text="@Localizer["Module"]" Value="1" />
</Items>
</RadzenRadioButtonList>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-sm-3">
@Localizer["Theme"]
</div>
<div class="col-12 col-sm-9">
<RadzenDropDown @bind-Value="_theme" TValue="string" Data="@_themes" Style="width: 100%;">
<Template>
<span>@Localizer[$"theme.{context}"]</span>
</Template>
</RadzenDropDown>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-sm-3">
@Localizer["Background"]
</div>
<div class="col-12 col-sm-9">
<RadzenDropDown @bind-Value="_background" TValue="string" Data="_backgroundColors" Style="width: 100%;">
<Template>
<span>@Localizer[context]</span>
</Template>
</RadzenDropDown>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-sm-3">
@Localizer["Toolbar"]
</div>
<div class="col-12 col-sm-9">
<div class="row">
<div class="col-12 col-sm-7">
<RadzenDropDown TValue="string" @bind-Value="_addToolbarItem" Data="@RadzenEditorDefinitions.ToolbarItems.Keys" Style="width: 100%;">
</RadzenDropDown>
</div>
<div class="col-12 col-sm-5 text-end">
<button type="button" class="btn btn-primary" @onclick="AddToolbarItem">@Localizer["Add"]</button>
<button type="button" class="btn btn-secondary" @onclick="ResetToolbarItem">@Localizer["Reset"]</button>
</div>
</div>
<div class="row mt-2" style="max-height: 500px; overflow-y: scroll;">
<div class="col">
<RadzenDropZoneContainer TItem="ToolbarItem" Data="_toolbarItems"
ItemSelector="@((i, z) => true)"
CanDrop="@((i) => true)"
Drop="OnToolbarItemDrop"
ItemRender="OnToolbarItemRender">
<ChildContent>
<RadzenDropZone TItem="ToolbarItem" class="rounded">
</RadzenDropZone>
</ChildContent>
<Template>
<div>
<strong>@context.Name</strong>
<RadzenButton Icon="delete" Click="@((e) => DeleteToolbarItem(context))" Size="ButtonSize.ExtraSmall" ButtonStyle="ButtonStyle.Light" />
</div>
</Template>
</RadzenDropZoneContainer>
</div>
</div>
</div>
</div>
<div class="mt-2 text-end">
<RadzenButton Text="OK" Click=@OnOkClick />
<RadzenButton Text="Cancel" Click=@OnCancelClick ButtonStyle="ButtonStyle.Secondary" />
</div>
@code {
private readonly IList<string> _themes = new List<string>
{
"default",
"dark",
"material",
"material-dark",
"standard",
"standard-dark",
"humanistic",
"humanistic-dark",
"software",
"software-dark"
};
private readonly IList<string> _backgroundColors = new List<string> { "Default", "Light", "Dark" };
private int _settingScope;
private string _theme;
private string _background;
private IList<ToolbarItem> _toolbarItems = new List<ToolbarItem>();
private string _addToolbarItem;
protected override async Task OnInitializedAsync()
{
_settingScope = await EditorSettingService.GetSettingScopeAsync(ModuleState.ModuleId);
await LoadSettings();
}
private async Task<RadzenEditorSetting> LoadSettingsFromModule()
{
return await EditorSettingService.LoadSettingsFromModuleAsync(ModuleState.ModuleId);
}
private async Task<RadzenEditorSetting> LoadSettingsFromSite()
{
return await EditorSettingService.LoadSettingsFromSiteAsync(PageState.Site.SiteId);
}
private async Task LoadSettings()
{
var editorSetting = _settingScope == 1 ? await LoadSettingsFromModule() : await LoadSettingsFromSite();
_theme = editorSetting.Theme;
_background = editorSetting.Background;
_toolbarItems = editorSetting.ToolbarItems.Split(',').Select((v, i) =>
{
return new ToolbarItem { Key = i, Name = v };
}).ToList();
}
private async Task OnScopeChanged()
{
await LoadSettings();
StateHasChanged();
}
private void AddToolbarItem()
{
if (!string.IsNullOrEmpty(_addToolbarItem))
{
_toolbarItems.Add(new ToolbarItem { Key = _toolbarItems.Count, Name = _addToolbarItem });
_addToolbarItem = string.Empty;
StateHasChanged();
}
}
private void ResetToolbarItem()
{
_toolbarItems = RadzenEditorDefinitions.DefaultToolbarItems.Split(',').Select((v, i) =>
{
return new ToolbarItem { Key = i, Name = v };
}).ToList();
StateHasChanged();
}
private void DeleteToolbarItem(ToolbarItem item)
{
_toolbarItems.Remove(item);
StateHasChanged();
}
private void OnCancelClick()
{
DialogService.Close(false);
}
private async Task OnOkClick()
{
var editorSetting = new RadzenEditorSetting
{
Theme = _theme,
Background = _background,
ToolbarItems = string.Join(",", _toolbarItems.Select(i => i.Name))
};
await EditorSettingService.UpdateSettingScopeAsync(ModuleState.ModuleId, _settingScope);
if (_settingScope == 1)
{
await EditorSettingService.SaveModuleSettingsAsync(ModuleState.ModuleId, editorSetting);
}
else
{
await EditorSettingService.SaveSiteSettingsAsync(PageState.Site.SiteId, editorSetting);
}
DialogService.Close(true);
}
private void OnToolbarItemDrop(RadzenDropZoneItemEventArgs<ToolbarItem> args)
{
if (args.ToItem != null && args.ToItem.Key != args.Item.Key)
{
_toolbarItems.Remove(args.Item);
_toolbarItems.Insert(_toolbarItems.IndexOf(args.ToItem), args.Item);
}
}
private void OnToolbarItemRender(RadzenDropZoneItemRenderEventArgs<ToolbarItem> args)
{
args.Attributes.Add("class", "rz-card rz-variant-flat rz-background-color-primary-lighter rz-color-on-primary-lighter rz-p-2 d-inline-block ms-1 mt-1");
}
public class ToolbarItem
{
public int Key { get; set; }
public string Name { get; set; }
}
}

View File

@ -26,6 +26,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" />
<PackageReference Include="Radzen.Blazor" Version="7.3.3" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,219 @@
<?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="Background" xml:space="preserve">
<value>Editor Background:</value>
</data>
<data name="Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="Dark" xml:space="preserve">
<value>Dark</value>
</data>
<data name="Default" xml:space="preserve">
<value>Default</value>
</data>
<data name="DialogTitle.SelectImage" xml:space="preserve">
<value>Select Image</value>
</data>
<data name="FontName.Placeholder" xml:space="preserve">
<value>Font</value>
</data>
<data name="FontName.Title" xml:space="preserve">
<value>Font Name</value>
</data>
<data name="FontSize.Placeholder" xml:space="preserve">
<value>Size</value>
</data>
<data name="FontSize.Title" xml:space="preserve">
<value>Font Size</value>
</data>
<data name="FormatBlock.Placeholder" xml:space="preserve">
<value>Format</value>
</data>
<data name="FormatBlock.Title" xml:space="preserve">
<value>Format Block</value>
</data>
<data name="theme.humanistic" xml:space="preserve">
<value>Humanistic</value>
</data>
<data name="theme.humanistic-dark" xml:space="preserve">
<value>Humanistic Dark</value>
</data>
<data name="InsertImage" xml:space="preserve">
<value>Insert Image</value>
</data>
<data name="Light" xml:space="preserve">
<value>Light</value>
</data>
<data name="theme.material" xml:space="preserve">
<value>Material</value>
</data>
<data name="theme.material-dark" xml:space="preserve">
<value>Material Dark</value>
</data>
<data name="Message.Require.Image" xml:space="preserve">
<value>You Must Select An Image To Insert</value>
</data>
<data name="Module" xml:space="preserve">
<value>Module</value>
</data>
<data name="Placeholder" xml:space="preserve">
<value>Enter Your Content...</value>
</data>
<data name="Settings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Scope" xml:space="preserve">
<value>Scope:</value>
</data>
<data name="Site" xml:space="preserve">
<value>Site</value>
</data>
<data name="theme.software" xml:space="preserve">
<value>Software</value>
</data>
<data name="theme.software-dark" xml:space="preserve">
<value>Software Dark</value>
</data>
<data name="theme.standard" xml:space="preserve">
<value>Standard</value>
</data>
<data name="theme.standard-dark" xml:space="preserve">
<value>Standard Dark</value>
</data>
<data name="Theme" xml:space="preserve">
<value>Theme:</value>
</data>
<data name="theme.dark" xml:space="preserve">
<value>Dark</value>
</data>
<data name="theme.default" xml:space="preserve">
<value>Default</value>
</data>
<data name="Toolbar" xml:space="preserve">
<value>Toolbar Items:</value>
</data>
<data name="Add" xml:space="preserve">
<value>Add</value>
</data>
<data name="Reset" xml:space="preserve">
<value>Reset</value>
</data>
</root>

View File

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Modules.Controls;
using Oqtane.Services;
namespace Oqtane.Services
{
public interface IRadzenEditorSettingService
{
Task<int> GetSettingScopeAsync(int moduleId);
Task UpdateSettingScopeAsync(int moduleId, int scope);
Task<RadzenEditorSetting> LoadSettingsFromModuleAsync(int moduleId);
Task<RadzenEditorSetting> LoadSettingsFromSiteAsync(int siteId);
Task SaveSiteSettingsAsync(int siteId, RadzenEditorSetting editorSetting);
Task SaveModuleSettingsAsync(int moduleId, RadzenEditorSetting editorSetting);
}
public class RadzenEditorSettingService : IRadzenEditorSettingService, IService
{
private const string SettingPrefix = "rzeditor:";
private readonly ISettingService _settingService;
public RadzenEditorSettingService(ISettingService settingService)
{
_settingService = settingService;
}
public async Task<int> GetSettingScopeAsync(int moduleId)
{
var key = $"{SettingPrefix}Scope";
var settings = await _settingService.GetModuleSettingsAsync(moduleId);
if (settings.ContainsKey(key) && int.TryParse(settings[key], out int value))
{
return value;
}
return 0; // site as default
}
public async Task UpdateSettingScopeAsync(int moduleId, int scope)
{
var settings = new Dictionary<string, string>
{
{ $"{SettingPrefix}Scope", scope.ToString() }
};
await _settingService.UpdateModuleSettingsAsync(settings, moduleId);
}
public async Task<RadzenEditorSetting> LoadSettingsFromModuleAsync(int moduleId)
{
var settings = await _settingService.GetModuleSettingsAsync(moduleId);
return ReadSettings(settings);
}
public async Task<RadzenEditorSetting> LoadSettingsFromSiteAsync(int siteId)
{
var settings = await _settingService.GetSiteSettingsAsync(siteId);
return ReadSettings(settings);
}
public async Task SaveSiteSettingsAsync(int siteId, RadzenEditorSetting editorSetting)
{
var settings = CreateSettingsDictionary(editorSetting);
if (settings.Any())
{
await _settingService.UpdateSiteSettingsAsync(settings, siteId);
}
}
public async Task SaveModuleSettingsAsync(int moduleId, RadzenEditorSetting editorSetting)
{
var settings = CreateSettingsDictionary(editorSetting);
if (settings.Any())
{
await _settingService.UpdateModuleSettingsAsync(settings, moduleId);
}
}
private RadzenEditorSetting ReadSettings(IDictionary<string, string> settings)
{
var setting = new RadzenEditorSetting
{
Theme = RadzenEditorDefinitions.DefaultTheme,
Background = RadzenEditorDefinitions.DefaultBackground,
ToolbarItems = RadzenEditorDefinitions.DefaultToolbarItems
};
if (settings != null)
{
var themeKey = $"{SettingPrefix}Theme";
var backgroundKey = $"{SettingPrefix}Background";
var toolbarItemsKey = $"{SettingPrefix}ToolbarItems";
if (settings.ContainsKey(themeKey) && !string.IsNullOrEmpty(settings[themeKey]))
{
setting.Theme = settings[themeKey];
}
if (settings.ContainsKey(backgroundKey) && !string.IsNullOrEmpty(settings[backgroundKey]))
{
setting.Background = settings[backgroundKey];
}
if (settings.ContainsKey(toolbarItemsKey) && !string.IsNullOrEmpty(settings[toolbarItemsKey]))
{
setting.ToolbarItems = settings[toolbarItemsKey];
}
}
return setting;
}
private Dictionary<string, string> CreateSettingsDictionary(RadzenEditorSetting editorSetting)
{
var settings = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(editorSetting.Theme))
{
settings.Add($"{SettingPrefix}Theme", editorSetting.Theme);
}
if (!string.IsNullOrEmpty(editorSetting.Background))
{
settings.Add($"{SettingPrefix}Background", editorSetting.Background);
}
if (!string.IsNullOrEmpty(editorSetting.ToolbarItems))
{
settings.Add($"{SettingPrefix}ToolbarItems", editorSetting.ToolbarItems);
}
return settings;
}
}
}