@

Editor.js 是一个基于 Web 的所见即所得富文本编辑器,它由CodeX团队开发。之前写过一篇博文专门介绍过这个编辑器,可以回看:开源好用的所见即所得(WYSIWYG)编辑器:Editor.js

.NET MAUI Blazor允许使用 Web UI 生成跨平台本机应用。 组件在 .NET 进程中以本机方式运行,并使用本地互操作通道将 Web UI 呈现到嵌入式 Web 视图控件(BlazorWebView)。

这次我们将Editor.js集成到.NET MAUI应用中。并实现只读切换,明/暗主题切换等功能。

使用.NET MAUI实现跨平台支持,本项目可运行于Android、iOS平台。

获取资源

我们先要获取web应用的资源文件(js,css等),以便MAUI的视图呈现标准的Web UI。有两种方式可以获取:

  1. 从源码构建
  2. 从CDN获取

从源码构建

此方法需要首先安装nodejs

克隆Editorjs项目到本地

git clone https://github.com/codex-team/editor.js.git

运行

npm i

以及

npm run build

等待nodejs构建完成,在项目根目录找到dist/editorjs.umd.js这个就是我们需要的js文件

从CDN获取

从官方CDN获取:

https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest

获取扩展插件

Editor.js中的每个块都由插件提供。有简单的外部脚本,有自己的逻辑。默认Editor.js项目中已包含唯一的 Paragraph 块。其它的工具插件可以单独获取。

同样我们可以找到这些插件的源码编译,或通过CDN获取:

  1. Header
  2. 链接
  3. HTML块
  4. 简单图片(无后端要求)
  5. 图片
  6. 清单
  7. 列表
  8. 嵌入
  9. 引用

创建项目

新建.NET MAUI Blazor项目,命名Editorjs

将editorjs.umd.js和各插件js文件拷贝至项目根目录下wwwroot文件夹,文件结构如下:

在wwwroot创建editorjs_index.html文件,并在body中引入editorjs.umd.js和各插件js文件

<body>
...
<script src="lib/editorjs/editorjs.umd.js"></script>
<script src="lib/editorjs/tools/checklist@latest.js"></script>
<script src="lib/editorjs/tools/code@latest.js"></script>
<script src="lib/editorjs/tools/delimiter@latest.js"></script>
<script src="lib/editorjs/tools/embed@latest.js"></script>
<script src="lib/editorjs/tools/header@latest.js"></script>
<script src="lib/editorjs/tools/image@latest.js"></script>
<script src="lib/editorjs/tools/inline-code@latest.js"></script>
<script src="lib/editorjs/tools/link@latest.js"></script>
<script src="lib/editorjs/tools/nested-list@latest.js"></script>
<script src="lib/editorjs/tools/marker@latest.js"></script>
<script src="lib/editorjs/tools/quote@latest.js"></script>
<script src="lib/editorjs/tools/table@latest.js"></script>
</body>

创建控件

创建 EditNotePage.xaml ,EditNotePage类作为视图控件,继承于ContentView,EditNotePage.xaml的完整代码如下:

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:mato="clr-namespace:Editorjs;assembly=Editorjs"
xmlns:service="clr-namespace:Editorjs.ViewModels;assembly=Editorjs"
xmlns:xct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Name="MainPage"
x:Class="Editorjs.Controls.EditNotePage">
<Grid BackgroundColor="{AppThemeBinding Light={StaticResource LightPageBackgroundColor}, Dark={StaticResource DarkPageBackgroundColor}}"
RowDefinitions="Auto, *, Auto"
Padding="20, 10, 20, 0">
<Grid Grid.Row="0"
Margin="0, 0, 0, 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions> <Entry Grid.Column="1"
Placeholder="请输入标题"
Margin="10, 0, 0, 0"
VerticalOptions="Center"
Text="{Binding Title}"
>
</Entry> <HorizontalStackLayout Grid.Column="2"
HeightRequest="60"
VerticalOptions="Center"
HorizontalOptions="End"
Margin="0, 0, 10, 0">
<StackLayout RadioButtonGroup.GroupName="State"
RadioButtonGroup.SelectedValue="{Binding NoteSegmentState,Mode=TwoWay}"
Orientation="Horizontal">
<RadioButton Value="{x:Static service:NoteSegmentState.Edit}"
Content="编辑"> </RadioButton>
<RadioButton Value="{x:Static service:NoteSegmentState.PreView}"
Content="预览"> </RadioButton> </StackLayout> </HorizontalStackLayout> </Grid> <BlazorWebView Grid.Row="1"
Margin="-10, 0"
x:Name="mainMapBlazorWebView"
HostPage="wwwroot/editorjs_index.html">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app"
x:Name="rootComponent"
ComponentType="{x:Type mato:EditorjsPage}" />
</BlazorWebView.RootComponents>
</BlazorWebView> <ActivityIndicator Grid.RowSpan="4"
IsRunning="{Binding Loading}"></ActivityIndicator>
</Grid>
</ContentView>

创建一个EditNotePageViewModel的ViewModel类,用于处理页面逻辑。代码如下:

public class EditNotePageViewModel : ObservableObject, IEditorViewModel
{
public Func<Task<string>> OnSubmitting { get; set; }
public Action<string> OnInited { get; set; }
public Action OnFocus { get; set; } public EditNotePageViewModel()
{
Submit = new Command(SubmitAction); NoteSegmentState=NoteSegmentState.Edit;
var content = "";
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Editorjs.Assets.sample1.json"))
{
if (stream != null)
{
using (StreamReader reader = new StreamReader(stream))
{
content = reader.ReadToEnd();
}
}
}
Init(new Note()
{
Title = "sample",
Content=content });
} private void Init(Note note)
{
if (note != null)
{
Title = note.Title;
Content = note.Content;
}
OnInited?.Invoke(this.Content);
} private string _title; public string Title
{
get { return _title; }
set
{
_title = value;
OnPropertyChanged();
}
} private string _content; public string Content
{
get { return _content; }
set
{
_content = value;
OnPropertyChanged();
}
} private async void SubmitAction(object obj)
{
var savedContent = await OnSubmitting?.Invoke();
if (string.IsNullOrEmpty(savedContent))
{
return;
}
this.Content=savedContent; var note = new Note();
note.Title = this.Title;
note.Content = this.Content;
}
public Command Submit { get; set; } }

注意这里的Init方法,用于初始化内容。这里我们读取Editorjs.Assets.sample1.json资源文件作为初始内容。

创建Blazor组件

创建Blazor页面EditorjsPage.razor

EditorjsPage.razor页面中,我们放置一个div,用于放置编辑器,

razor页面的 @Code 代码段中,放置EditNotePageViewModel属性,以及一个DotNetObjectReference对象,用于在JS中调用C#方法。

@code {
[Parameter]
public IEditorViewModel EditNotePageViewModel { get; set; }
private DotNetObjectReference<EditorjsPage> objRef; protected override void OnInitialized()
{
objRef = DotNetObjectReference.Create(this);
}

初始化

在script代码段中,创建LoadContent函数,用于加载EditorJs的初始内容。

<div class="ce-main">
<div id="editorjs"></div>
</div>

LoadContent中,调用函数window.editor = new window.EditorJS(config)创建一个EditorJS对象,其中config对象包括holder,tools,data等属性,关于EditorJs配置的更多说明请参考官方文档

<script type="text/javascript">
window.editor = null;
window.viewService = {
LoadContent: function (content) {
var obj = JSON.parse(content);
var createEdtor = () => {
window.editor = new window.EditorJS({
holder: 'editorjs', /**
* Tools list
*/
tools: {
paragraph: {
config: {
placeholder: "Enter something"
}
}, header: {
class: Header,
inlineToolbar: ['link'],
config: {
placeholder: 'Header'
},
shortcut: 'CMD+SHIFT+H'
}, /**
* Or pass class directly without any configuration
*/
image: {
class: ImageTool
}, list: {
class: NestedList,
inlineToolbar: true,
shortcut: 'CMD+SHIFT+L'
}, checklist: {
class: Checklist,
inlineToolbar: true,
}, quote: {
class: Quote,
inlineToolbar: true,
config: {
quotePlaceholder: '输入引用内容',
captionPlaceholder: '引用标题',
},
shortcut: 'CMD+SHIFT+O'
}, marker: {
class: Marker,
shortcut: 'CMD+SHIFT+M'
}, code: {
class: CodeTool,
shortcut: 'CMD+SHIFT+C'
}, delimiter: Delimiter, inlineCode: {
class: InlineCode,
shortcut: 'CMD+SHIFT+C'
}, linkTool: LinkTool, embed: Embed, table: {
class: Table,
inlineToolbar: true,
shortcut: 'CMD+ALT+T'
}, }, i18n: {
messages: {
"ui": {
"blockTunes": {
"toggler": {
"Click to tune": "点击转换",
"or drag to move": "拖动调整"
},
},
"inlineToolbar": {
"converter": {
"Convert to": "转换成"
}
},
"toolbar": {
"toolbox": {
"Add": "添加",
"Filter": "过滤",
"Nothing found": "无内容"
},
"popover": {
"Filter": "过滤",
"Nothing found": "无内容"
}
}
},
"toolNames": {
"Text": "段落",
"Heading": "标题",
"List": "列表",
"Warning": "警告",
"Checklist": "清单",
"Quote": "引用",
"Code": "代码",
"Delimiter": "分割线",
"Raw HTML": "HTML片段",
"Table": "表格",
"Link": "链接",
"Marker": "突出显示",
"Bold": "加粗",
"Italic": "倾斜",
"InlineCode": "代码片段",
"Image": "图片"
},
"tools": {
"link": {
"Add a link": "添加链接"
},
"stub": {
'The block can not be displayed correctly.': '该模块不能放置在这里'
},
"image": {
"Caption": "图片说明",
"Select an Image": "选择图片",
"With border": "添加边框",
"Stretch image": "拉伸图像",
"With background": "添加背景",
},
"code": {
"Enter a code": "输入代码",
},
"linkTool": {
"Link": "请输入链接地址",
"Couldn't fetch the link data": "获取链接数据失败",
"Couldn't get this link data, try the other one": "该链接不能访问,请修改",
"Wrong response format from the server": "错误响应",
},
"header": {
"Header": "标题",
"Heading 1": "一级标题",
"Heading 2": "二级标题",
"Heading 3": "三级标题",
"Heading 4": "四级标题",
"Heading 5": "五级标题",
"Heading 6": "六级标题",
},
"paragraph": {
"Enter something": "请输入笔记内容",
},
"list": {
"Ordered": "有序列表",
"Unordered": "无序列表",
},
"table": {
"Heading": "标题",
"Add column to left": "在左侧插入列",
"Add column to right": "在右侧插入列",
"Delete column": "删除列",
"Add row above": "在上方插入行",
"Add row below": "在下方插入行",
"Delete row": "删除行",
"With headings": "有标题",
"Without headings": "无标题",
},
"quote": {
"Align Left": "左对齐",
"Align Center": "居中对齐",
}
},
"blockTunes": {
"delete": {
"Delete": "删除",
'Click to delete': "点击删除"
},
"moveUp": {
"Move up": "向上移"
},
"moveDown": {
"Move down": "向下移"
},
"filter": {
"Filter": "过滤"
}
},
}
}, /**
* Initial Editor data
*/
data: obj
}); }
if (window.editor) {
editor.isReady.then(() => {
editor.destroy();
createEdtor();
});
}
else {
createEdtor();
} },
DumpContent: async function () {
outputData = null;
if (window.editor) {
if (window.editor.readOnly.isEnabled) {
await window.editor.readOnly.toggle();
}
var outputObj = await window.editor.save();
outputData = JSON.stringify(outputObj);
}
return outputData;
},
SwitchTheme: function () {
document.body.classList.toggle("dark-mode");
}, SwitchState: async function () {
state = null;
if (window.editor && window.editor.readOnly) {
var readOnlyState = await window.editor.readOnly.toggle();
state = readOnlyState;
}
return state;
}, Focus: async function (atEnd) {
if (window.editor) {
await window.editor.focus(atEnd);
}
}, GetState() {
if (window.editor && window.editor.readOnly) {
return window.editor.readOnly.isEnabled;
}
}, Destroy: function () {
if (window.editor) {
window.editor.destroy();
}
}, } window.initObjRef = function (objRef) {
window.objRef = objRef;
} </script>

保存

创建转存函数DumpContent

DumpContent: async function () {
outputData = null;
if (window.editor) {
if (window.editor.readOnly.isEnabled) {
await window.editor.readOnly.toggle();
}
var outputObj = await window.editor.save();
outputData = JSON.stringify(outputObj);
}
return outputData;
},

销毁

创建销毁函数Destroy


Destroy: function () {
if (window.editor) {
window.editor.destroy();
}
},

编写渲染逻辑

在OnAfterRenderAsync中调用初始化函数,并订阅OnSubmitting和OnInited事件,以便在提交事件触发时保存,以及文本状态变更时重新渲染。

 protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
return;
if (EditNotePageViewModel != null)
{
EditNotePageViewModel.PropertyChanged += EditNotePageViewModel_PropertyChanged;
this.EditNotePageViewModel.OnSubmitting += OnSubmitting;
this.EditNotePageViewModel.OnInited += OnInited;
var currentContent = EditNotePageViewModel.Content; await JSRuntime.InvokeVoidAsync("viewService.LoadContent", currentContent);
} await JSRuntime.InvokeVoidAsync("window.initObjRef", this.objRef); }
private async Task<string> OnSubmitting()
{
var savedContent = await JSRuntime.InvokeAsync<string>("viewService.DumpContent");
return savedContent;
} private async void OnInited(string content)
{
await JSRuntime.InvokeVoidAsync("viewService.LoadContent", content);
}

实现只读/编辑功能

在.NET本机中,我们使用枚举来表示编辑状态。 并在控件上设置一个按钮来切换编辑状态。

public enum NoteSegmentState
{
Edit,
PreView
}

EditNotePageViewModel.cs:

...
private NoteSegmentState _noteSegmentState; public NoteSegmentState NoteSegmentState
{
get { return _noteSegmentState; }
set
{
_noteSegmentState = value;
OnPropertyChanged(); }
}

EditNotePage.xaml:

...
<StackLayout RadioButtonGroup.GroupName="State"
RadioButtonGroup.SelectedValue="{Binding NoteSegmentState,Mode=TwoWay}"
Orientation="Horizontal">
<RadioButton Value="{x:Static service:NoteSegmentState.Edit}"
Content="编辑"> </RadioButton>
<RadioButton Value="{x:Static service:NoteSegmentState.PreView}"
Content="预览"> </RadioButton> </StackLayout>

Editorjs官方提供了readOnly对象,通过toggle()方法,可以切换编辑模式和只读模式。

在创建Editorjs实例时,也可以通过设置readOnly属性为true即可实现只读模式。

切换模式

在razor页面中创建SwitchState函数,用来切换编辑模式和只读模式。

SwitchState: async function () {
state = null;
if (window.editor && window.editor.readOnly) {
var readOnlyState = await window.editor.readOnly.toggle();
state = readOnlyState;
}
return state;
},

获取只读模式状态

在razor页面中创建GetState函数,用来获取编辑模式和只读模式的状态。


GetState() {
if (window.editor && window.editor.readOnly) {
return window.editor.readOnly.isEnabled;
}
},

响应切换事件

我们监听EditNotePageViewModel 的NoteSegmentState属性变更事件,当状态改变时,调用对应的js方法

private async void EditNotePageViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(EditNotePageViewModel.NoteSegmentState))
{
if (EditNotePageViewModel.NoteSegmentState==NoteSegmentState.PreView)
{
var state = await JSRuntime.InvokeAsync<bool>("viewService.GetState");
if (!state)
{
await JSRuntime.InvokeAsync<bool>("viewService.SwitchState"); } }
else if (EditNotePageViewModel.NoteSegmentState==NoteSegmentState.Edit)
{
var state = await JSRuntime.InvokeAsync<bool>("viewService.GetState");
if (state)
{
await JSRuntime.InvokeAsync<bool>("viewService.SwitchState");
}
}
}
}

实现明/暗主题切换

lib/editorjs/css/main.css中,定义了.dark-mode类的样式表

.dark-mode {
--color-border-light: rgba(255, 255, 255,.08);
--color-bg-main: #212121;
--color-text-main: #F5F5F5;
} .dark-mode .ce-popover {
--color-background: #424242;
--color-text-primary: #F5F5F5;
--color-text-secondary: #707684;
--color-border: #424242;
} .dark-mode .ce-toolbar__settings-btn {
background: #2A2A2A;
border: 1px solid #424242;
} .dark-mode .ce-toolbar__plus {
background: #2A2A2A;
border: 1px solid #424242;
} .dark-mode .ce-popover-item__icon {
background: #2A2A2A;
} .dark-mode .ce-code__textarea {
color: #212121;
background: #2A2A2A;
} .dark-mode .tc-popover {
--color-border: #424242;
--color-background: #424242;
}
.dark-mode .tc-wrap {
--color-background: #424242;
}

在razor页面中添加SwitchTheme函数,用于用于切换dark-mode"的`类名,从而实现暗黑模式和正常模式之间的切换。

SwitchTheme: function () {
document.body.classList.toggle("dark-mode");
},

OnInitializedAsync中,订阅Application.Current.RequestedThemeChanged事件,用于监听主题切换事件,并调用SwitchTheme函数。

protected override async Task OnInitializedAsync()
{
objRef = DotNetObjectReference.Create(this); Application.Current.RequestedThemeChanged += OnRequestedThemeChanged; }
private async void OnRequestedThemeChanged(object sender, AppThemeChangedEventArgs args)
{
await JSRuntime.InvokeVoidAsync("viewService.SwitchTheme");
}

在渲染页面时,也判断是否需要切换主题

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
return;
···
if (Application.Current.UserAppTheme==AppTheme.Dark)
{
await JSRuntime.InvokeVoidAsync("viewService.SwitchTheme"); } }

项目地址

Github:maui-samples

[MAUI]集成富文本编辑器Editor.js至.NET MAUI Blazor项目的更多相关文章

  1. AngularJS集成富文本编辑器

    最近在Angular中需要集成富文本编辑器,本来已经集成好百度的UEditor,后台觉得配置太多,让我弄个别的,然后就找到了wangEditor,这个配置和上手都要简单一些,下面来看看具体操作步骤吧: ...

  2. yii2集成富文本编辑器redactor

    作者:白狼 出处:http://www.manks.top/article/yii2_redactor本文版权归作者,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保 ...

  3. nodejs后台集成富文本编辑器(ueditor)

    1 下载ueditor nodejs版本 2 复制public目录下面的文件 到项目静态资源public文件夹下 3 在项目根目录创建ueditor文件夹 要复制进来的内容为 4 在根目录的 uedi ...

  4. Xadmin集成富文本编辑器ueditor

    在xadmin中通过自定义插件,实现富文本编辑器,效果如下: 1.首先,pip安装ueditor的Django版本: pip install DjangoUeditor 2.之后需要添加到项目的set ...

  5. django—xadmin中集成富文本编辑器ueditor

    一.安装 pip命令安装,由于ueditor为百度开发的一款富文本编辑框,现已停止维护,如果解释器为python2,则直接pip install djangoueditor 解压包安装,python3 ...

  6. uniapp - 富文本编辑器editor(仅支持App和微信小程序)

    uniapp - editor富文本编辑器用法示例 丢几个图,用心看下去(-.-) 这里使用了https://ext.dcloud.net.cn/plugin?id=412 插件,用于选择字体颜色.其 ...

  7. django后台集成富文本编辑器Tinymce的使用

    富文本编辑器Tinymce是使用步骤: 1.首先去python的模块包的网站下载一个django-tinymce的包 2.下载上图的安装包,然后解压,进入文件夹,执行: (pychrm直接运行命令pi ...

  8. 富文本编辑器 summernote.js

    1.引用js  可在 https://summernote.org/ 官网下载 ,并查看详细的API  引入:summernote.js 和 summernote-zh-CN.js 以及样式文件:su ...

  9. Django使用xadmin集成富文本编辑器Ueditor(方法二)

    一.xadmin的安装与配置1.安装xadmin,其中第一种在python3中安装不成功,推荐第二种或者第三种 方式一:pip install xadmin 方式二:pip install git+g ...

  10. 富文本编辑器 wangEditor.js

    1.引用 wangEditor 相关js  和 css 下载地址:https://files.cnblogs.com/files/kitty-blog/WangEditor.zip 3.页面: < ...

随机推荐

  1. 图数据库认证考试 NGCP 错题解析 vol.02:这 10 道题竟无一人全部答对

    如果你读过「NebulaGraph 错题解析第一期」,大概知道在错题解析未出来之前,NebulaGraph 专业技能认证 NGCP(全称 NebulaGraph Certified Professio ...

  2. kotlin协程异常处理之-try catch

    kotlin协程小记 协程的async使用 kotlin协程异常处理之-try catch kotlin协程异常处理之-CoroutineExceptionHandler 一.try catch tr ...

  3. JVM内存模式

    Java内存模型即Java Memory Model,简称JMM. JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式.JVM 是整个计算机虚拟模型,所以 JMM 是隶属于 JV ...

  4. [linux 爬坑] 几个linux发行版尝试和令人崩溃的ssr安装体验

    最近电脑上的manjaro好像出了问题,长时间不用就会死机.也懒得追究原因了,正好决定尝试几个发行版.首先尝试安装银河麒麟    这个发行版实际上就是ubuntu,甚至源什么的都是ubuntu的,也不 ...

  5. $event - vue中默认参数的显示 - @on-change="func($event, code)" - 基础知识

    @on-change="checkAllOnChangeHandle($event,scItem.code)"

  6. shell脚本中常用的自定义函数

    在Shell脚本中,你可以定义各种函数来执行不同的任务.以下是20个常用的自定义函数示例,涵盖了从文件操作.文本处理到系统监控等多个方面: 检查文件是否存在 file_exists() { [ -f ...

  7. 基于Apollo3 Blue MCU芯片的可穿戴产品解决方案开发之六轴加速度传感器适配

    一 前记 MPU-60X0 是全球首例9 轴运动处理传感器.它集成了3 轴MEMS 陀螺仪,3 轴MEMS加速度计,以及一个可扩展的数字运动处理器DMP(Digital Motion Processo ...

  8. Web服务器通信原理

    Web服务器通信原理 1.区分系统 2.DOS系统 3.IP地址 4.域名.DNS 5.端口 6.HTTP协议 7.Web容器 8.整个流程 一.区分系统 主要三个系统Windows.Linux.Ma ...

  9. Git进阶命令-reset

    之前有关Git,写过一片文章: Git五个常见问题及解决方法 一.reset命令使用场景 有时候我们提交了一些错误的或者不完善的代码,需要回退到之前的某个稳定的版本,面对这种情况有两种解决方法: 解决 ...

  10. 在Ubuntu14.04上安装qt5和qtcreator的 两种方式(源代码和xxxxx.run) 和我的感悟-------超级详细版

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文发布于 2014-07-25 12:21:13 ...