经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » ASP.net » 查看文章
[MAUI]集成富文本编辑器Editor.js至.NET MAUI Blazor项目
来源:cnblogs  作者:林晓lx  时间:2024/4/15 9:49:34  对本文有异议

@


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项目到本地

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

运行

  1. npm i

以及

  1. npm run build

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

在这里插入图片描述

从CDN获取

从官方CDN获取:

  1. 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文件

  1. <body>
  2. ...
  3. <script src="lib/editorjs/editorjs.umd.js"></script>
  4. <script src="lib/editorjs/tools/checklist@latest.js"></script>
  5. <script src="lib/editorjs/tools/code@latest.js"></script>
  6. <script src="lib/editorjs/tools/delimiter@latest.js"></script>
  7. <script src="lib/editorjs/tools/embed@latest.js"></script>
  8. <script src="lib/editorjs/tools/header@latest.js"></script>
  9. <script src="lib/editorjs/tools/image@latest.js"></script>
  10. <script src="lib/editorjs/tools/inline-code@latest.js"></script>
  11. <script src="lib/editorjs/tools/link@latest.js"></script>
  12. <script src="lib/editorjs/tools/nested-list@latest.js"></script>
  13. <script src="lib/editorjs/tools/marker@latest.js"></script>
  14. <script src="lib/editorjs/tools/quote@latest.js"></script>
  15. <script src="lib/editorjs/tools/table@latest.js"></script>
  16. </body>

创建控件

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

  1. <ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
  2. xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  3. xmlns:mato="clr-namespace:Editorjs;assembly=Editorjs"
  4. xmlns:service="clr-namespace:Editorjs.ViewModels;assembly=Editorjs"
  5. xmlns:xct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
  6. x:Name="MainPage"
  7. x:Class="Editorjs.Controls.EditNotePage">
  8. <Grid BackgroundColor="{AppThemeBinding Light={StaticResource LightPageBackgroundColor}, Dark={StaticResource DarkPageBackgroundColor}}"
  9. RowDefinitions="Auto, *, Auto"
  10. Padding="20, 10, 20, 0">
  11. <Grid Grid.Row="0"
  12. Margin="0, 0, 0, 10">
  13. <Grid.ColumnDefinitions>
  14. <ColumnDefinition Width="auto"></ColumnDefinition>
  15. <ColumnDefinition></ColumnDefinition>
  16. <ColumnDefinition></ColumnDefinition>
  17. </Grid.ColumnDefinitions>
  18. <Entry Grid.Column="1"
  19. Placeholder="请输入标题"
  20. Margin="10, 0, 0, 0"
  21. VerticalOptions="Center"
  22. Text="{Binding Title}"
  23. >
  24. </Entry>
  25. <HorizontalStackLayout Grid.Column="2"
  26. HeightRequest="60"
  27. VerticalOptions="Center"
  28. HorizontalOptions="End"
  29. Margin="0, 0, 10, 0">
  30. <StackLayout RadioButtonGroup.GroupName="State"
  31. RadioButtonGroup.SelectedValue="{Binding NoteSegmentState,Mode=TwoWay}"
  32. Orientation="Horizontal">
  33. <RadioButton Value="{x:Static service:NoteSegmentState.Edit}"
  34. Content="编辑">
  35. </RadioButton>
  36. <RadioButton Value="{x:Static service:NoteSegmentState.PreView}"
  37. Content="预览">
  38. </RadioButton>
  39. </StackLayout>
  40. </HorizontalStackLayout>
  41. </Grid>
  42. <BlazorWebView Grid.Row="1"
  43. Margin="-10, 0"
  44. x:Name="mainMapBlazorWebView"
  45. HostPage="wwwroot/editorjs_index.html">
  46. <BlazorWebView.RootComponents>
  47. <RootComponent Selector="#app"
  48. x:Name="rootComponent"
  49. ComponentType="{x:Type mato:EditorjsPage}" />
  50. </BlazorWebView.RootComponents>
  51. </BlazorWebView>
  52. <ActivityIndicator Grid.RowSpan="4"
  53. IsRunning="{Binding Loading}"></ActivityIndicator>
  54. </Grid>
  55. </ContentView>

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

  1. public class EditNotePageViewModel : ObservableObject, IEditorViewModel
  2. {
  3. public Func<Task<string>> OnSubmitting { get; set; }
  4. public Action<string> OnInited { get; set; }
  5. public Action OnFocus { get; set; }
  6. public EditNotePageViewModel()
  7. {
  8. Submit = new Command(SubmitAction);
  9. NoteSegmentState=NoteSegmentState.Edit;
  10. var content = "";
  11. using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Editorjs.Assets.sample1.json"))
  12. {
  13. if (stream != null)
  14. {
  15. using (StreamReader reader = new StreamReader(stream))
  16. {
  17. content = reader.ReadToEnd();
  18. }
  19. }
  20. }
  21. Init(new Note()
  22. {
  23. Title = "sample",
  24. Content=content
  25. });
  26. }
  27. private void Init(Note note)
  28. {
  29. if (note != null)
  30. {
  31. Title = note.Title;
  32. Content = note.Content;
  33. }
  34. OnInited?.Invoke(this.Content);
  35. }
  36. private string _title;
  37. public string Title
  38. {
  39. get { return _title; }
  40. set
  41. {
  42. _title = value;
  43. OnPropertyChanged();
  44. }
  45. }
  46. private string _content;
  47. public string Content
  48. {
  49. get { return _content; }
  50. set
  51. {
  52. _content = value;
  53. OnPropertyChanged();
  54. }
  55. }
  56. private async void SubmitAction(object obj)
  57. {
  58. var savedContent = await OnSubmitting?.Invoke();
  59. if (string.IsNullOrEmpty(savedContent))
  60. {
  61. return;
  62. }
  63. this.Content=savedContent;
  64. var note = new Note();
  65. note.Title = this.Title;
  66. note.Content = this.Content;
  67. }
  68. public Command Submit { get; set; }
  69. }

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

在这里插入图片描述

创建Blazor组件

创建Blazor页面EditorjsPage.razor

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

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

  1. @code {
  2. [Parameter]
  3. public IEditorViewModel EditNotePageViewModel { get; set; }
  4. private DotNetObjectReference<EditorjsPage> objRef;
  5. protected override void OnInitialized()
  6. {
  7. objRef = DotNetObjectReference.Create(this);
  8. }

初始化

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

  1. <div class="ce-main">
  2. <div id="editorjs"></div>
  3. </div>

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

  1. <script type="text/javascript">
  2. window.editor = null;
  3. window.viewService = {
  4. LoadContent: function (content) {
  5. var obj = JSON.parse(content);
  6. var createEdtor = () => {
  7. window.editor = new window.EditorJS({
  8. holder: 'editorjs',
  9. /**
  10. * Tools list
  11. */
  12. tools: {
  13. paragraph: {
  14. config: {
  15. placeholder: "Enter something"
  16. }
  17. },
  18. header: {
  19. class: Header,
  20. inlineToolbar: ['link'],
  21. config: {
  22. placeholder: 'Header'
  23. },
  24. shortcut: 'CMD+SHIFT+H'
  25. },
  26. /**
  27. * Or pass class directly without any configuration
  28. */
  29. image: {
  30. class: ImageTool
  31. },
  32. list: {
  33. class: NestedList,
  34. inlineToolbar: true,
  35. shortcut: 'CMD+SHIFT+L'
  36. },
  37. checklist: {
  38. class: Checklist,
  39. inlineToolbar: true,
  40. },
  41. quote: {
  42. class: Quote,
  43. inlineToolbar: true,
  44. config: {
  45. quotePlaceholder: '输入引用内容',
  46. captionPlaceholder: '引用标题',
  47. },
  48. shortcut: 'CMD+SHIFT+O'
  49. },
  50. marker: {
  51. class: Marker,
  52. shortcut: 'CMD+SHIFT+M'
  53. },
  54. code: {
  55. class: CodeTool,
  56. shortcut: 'CMD+SHIFT+C'
  57. },
  58. delimiter: Delimiter,
  59. inlineCode: {
  60. class: InlineCode,
  61. shortcut: 'CMD+SHIFT+C'
  62. },
  63. linkTool: LinkTool,
  64. embed: Embed,
  65. table: {
  66. class: Table,
  67. inlineToolbar: true,
  68. shortcut: 'CMD+ALT+T'
  69. },
  70. },
  71. i18n: {
  72. messages: {
  73. "ui": {
  74. "blockTunes": {
  75. "toggler": {
  76. "Click to tune": "点击转换",
  77. "or drag to move": "拖动调整"
  78. },
  79. },
  80. "inlineToolbar": {
  81. "converter": {
  82. "Convert to": "转换成"
  83. }
  84. },
  85. "toolbar": {
  86. "toolbox": {
  87. "Add": "添加",
  88. "Filter": "过滤",
  89. "Nothing found": "无内容"
  90. },
  91. "popover": {
  92. "Filter": "过滤",
  93. "Nothing found": "无内容"
  94. }
  95. }
  96. },
  97. "toolNames": {
  98. "Text": "段落",
  99. "Heading": "标题",
  100. "List": "列表",
  101. "Warning": "警告",
  102. "Checklist": "清单",
  103. "Quote": "引用",
  104. "Code": "代码",
  105. "Delimiter": "分割线",
  106. "Raw HTML": "HTML片段",
  107. "Table": "表格",
  108. "Link": "链接",
  109. "Marker": "突出显示",
  110. "Bold": "加粗",
  111. "Italic": "倾斜",
  112. "InlineCode": "代码片段",
  113. "Image": "图片"
  114. },
  115. "tools": {
  116. "link": {
  117. "Add a link": "添加链接"
  118. },
  119. "stub": {
  120. 'The block can not be displayed correctly.': '该模块不能放置在这里'
  121. },
  122. "image": {
  123. "Caption": "图片说明",
  124. "Select an Image": "选择图片",
  125. "With border": "添加边框",
  126. "Stretch image": "拉伸图像",
  127. "With background": "添加背景",
  128. },
  129. "code": {
  130. "Enter a code": "输入代码",
  131. },
  132. "linkTool": {
  133. "Link": "请输入链接地址",
  134. "Couldn't fetch the link data": "获取链接数据失败",
  135. "Couldn't get this link data, try the other one": "该链接不能访问,请修改",
  136. "Wrong response format from the server": "错误响应",
  137. },
  138. "header": {
  139. "Header": "标题",
  140. "Heading 1": "一级标题",
  141. "Heading 2": "二级标题",
  142. "Heading 3": "三级标题",
  143. "Heading 4": "四级标题",
  144. "Heading 5": "五级标题",
  145. "Heading 6": "六级标题",
  146. },
  147. "paragraph": {
  148. "Enter something": "请输入笔记内容",
  149. },
  150. "list": {
  151. "Ordered": "有序列表",
  152. "Unordered": "无序列表",
  153. },
  154. "table": {
  155. "Heading": "标题",
  156. "Add column to left": "在左侧插入列",
  157. "Add column to right": "在右侧插入列",
  158. "Delete column": "删除列",
  159. "Add row above": "在上方插入行",
  160. "Add row below": "在下方插入行",
  161. "Delete row": "删除行",
  162. "With headings": "有标题",
  163. "Without headings": "无标题",
  164. },
  165. "quote": {
  166. "Align Left": "左对齐",
  167. "Align Center": "居中对齐",
  168. }
  169. },
  170. "blockTunes": {
  171. "delete": {
  172. "Delete": "删除",
  173. 'Click to delete': "点击删除"
  174. },
  175. "moveUp": {
  176. "Move up": "向上移"
  177. },
  178. "moveDown": {
  179. "Move down": "向下移"
  180. },
  181. "filter": {
  182. "Filter": "过滤"
  183. }
  184. },
  185. }
  186. },
  187. /**
  188. * Initial Editor data
  189. */
  190. data: obj
  191. });
  192. }
  193. if (window.editor) {
  194. editor.isReady.then(() => {
  195. editor.destroy();
  196. createEdtor();
  197. });
  198. }
  199. else {
  200. createEdtor();
  201. }
  202. },
  203. DumpContent: async function () {
  204. outputData = null;
  205. if (window.editor) {
  206. if (window.editor.readOnly.isEnabled) {
  207. await window.editor.readOnly.toggle();
  208. }
  209. var outputObj = await window.editor.save();
  210. outputData = JSON.stringify(outputObj);
  211. }
  212. return outputData;
  213. },
  214. SwitchTheme: function () {
  215. document.body.classList.toggle("dark-mode");
  216. },
  217. SwitchState: async function () {
  218. state = null;
  219. if (window.editor && window.editor.readOnly) {
  220. var readOnlyState = await window.editor.readOnly.toggle();
  221. state = readOnlyState;
  222. }
  223. return state;
  224. },
  225. Focus: async function (atEnd) {
  226. if (window.editor) {
  227. await window.editor.focus(atEnd);
  228. }
  229. },
  230. GetState() {
  231. if (window.editor && window.editor.readOnly) {
  232. return window.editor.readOnly.isEnabled;
  233. }
  234. },
  235. Destroy: function () {
  236. if (window.editor) {
  237. window.editor.destroy();
  238. }
  239. },
  240. }
  241. window.initObjRef = function (objRef) {
  242. window.objRef = objRef;
  243. }
  244. </script>

在这里插入图片描述

保存

创建转存函数DumpContent

  1. DumpContent: async function () {
  2. outputData = null;
  3. if (window.editor) {
  4. if (window.editor.readOnly.isEnabled) {
  5. await window.editor.readOnly.toggle();
  6. }
  7. var outputObj = await window.editor.save();
  8. outputData = JSON.stringify(outputObj);
  9. }
  10. return outputData;
  11. },

销毁

创建销毁函数Destroy

  1. Destroy: function () {
  2. if (window.editor) {
  3. window.editor.destroy();
  4. }
  5. },

编写渲染逻辑

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

  1. protected override async Task OnAfterRenderAsync(bool firstRender)
  2. {
  3. if (!firstRender)
  4. return;
  5. if (EditNotePageViewModel != null)
  6. {
  7. EditNotePageViewModel.PropertyChanged += EditNotePageViewModel_PropertyChanged;
  8. this.EditNotePageViewModel.OnSubmitting += OnSubmitting;
  9. this.EditNotePageViewModel.OnInited += OnInited;
  10. var currentContent = EditNotePageViewModel.Content;
  11. await JSRuntime.InvokeVoidAsync("viewService.LoadContent", currentContent);
  12. }
  13. await JSRuntime.InvokeVoidAsync("window.initObjRef", this.objRef);
  14. }
  1. private async Task<string> OnSubmitting()
  2. {
  3. var savedContent = await JSRuntime.InvokeAsync<string>("viewService.DumpContent");
  4. return savedContent;
  5. }
  6. private async void OnInited(string content)
  7. {
  8. await JSRuntime.InvokeVoidAsync("viewService.LoadContent", content);
  9. }

在这里插入图片描述

实现只读/编辑功能

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

  1. public enum NoteSegmentState
  2. {
  3. Edit,
  4. PreView
  5. }

EditNotePageViewModel.cs:

  1. ...
  2. private NoteSegmentState _noteSegmentState;
  3. public NoteSegmentState NoteSegmentState
  4. {
  5. get { return _noteSegmentState; }
  6. set
  7. {
  8. _noteSegmentState = value;
  9. OnPropertyChanged();
  10. }
  11. }

EditNotePage.xaml:

  1. ...
  2. <StackLayout RadioButtonGroup.GroupName="State"
  3. RadioButtonGroup.SelectedValue="{Binding NoteSegmentState,Mode=TwoWay}"
  4. Orientation="Horizontal">
  5. <RadioButton Value="{x:Static service:NoteSegmentState.Edit}"
  6. Content="编辑">
  7. </RadioButton>
  8. <RadioButton Value="{x:Static service:NoteSegmentState.PreView}"
  9. Content="预览">
  10. </RadioButton>
  11. </StackLayout>

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

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

切换模式

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

  1. SwitchState: async function () {
  2. state = null;
  3. if (window.editor && window.editor.readOnly) {
  4. var readOnlyState = await window.editor.readOnly.toggle();
  5. state = readOnlyState;
  6. }
  7. return state;
  8. },

获取只读模式状态

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

  1. GetState() {
  2. if (window.editor && window.editor.readOnly) {
  3. return window.editor.readOnly.isEnabled;
  4. }
  5. },

响应切换事件

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

  1. private async void EditNotePageViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
  2. {
  3. if (e.PropertyName == nameof(EditNotePageViewModel.NoteSegmentState))
  4. {
  5. if (EditNotePageViewModel.NoteSegmentState==NoteSegmentState.PreView)
  6. {
  7. var state = await JSRuntime.InvokeAsync<bool>("viewService.GetState");
  8. if (!state)
  9. {
  10. await JSRuntime.InvokeAsync<bool>("viewService.SwitchState");
  11. }
  12. }
  13. else if (EditNotePageViewModel.NoteSegmentState==NoteSegmentState.Edit)
  14. {
  15. var state = await JSRuntime.InvokeAsync<bool>("viewService.GetState");
  16. if (state)
  17. {
  18. await JSRuntime.InvokeAsync<bool>("viewService.SwitchState");
  19. }
  20. }
  21. }
  22. }

在这里插入图片描述

实现明/暗主题切换

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

  1. .dark-mode {
  2. --color-border-light: rgba(255, 255, 255,.08);
  3. --color-bg-main: #212121;
  4. --color-text-main: #F5F5F5;
  5. }
  6. .dark-mode .ce-popover {
  7. --color-background: #424242;
  8. --color-text-primary: #F5F5F5;
  9. --color-text-secondary: #707684;
  10. --color-border: #424242;
  11. }
  12. .dark-mode .ce-toolbar__settings-btn {
  13. background: #2A2A2A;
  14. border: 1px solid #424242;
  15. }
  16. .dark-mode .ce-toolbar__plus {
  17. background: #2A2A2A;
  18. border: 1px solid #424242;
  19. }
  20. .dark-mode .ce-popover-item__icon {
  21. background: #2A2A2A;
  22. }
  23. .dark-mode .ce-code__textarea {
  24. color: #212121;
  25. background: #2A2A2A;
  26. }
  27. .dark-mode .tc-popover {
  28. --color-border: #424242;
  29. --color-background: #424242;
  30. }
  31. .dark-mode .tc-wrap {
  32. --color-background: #424242;
  33. }

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

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

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

  1. protected override async Task OnInitializedAsync()
  2. {
  3. objRef = DotNetObjectReference.Create(this);
  4. Application.Current.RequestedThemeChanged += OnRequestedThemeChanged;
  5. }
  6. private async void OnRequestedThemeChanged(object sender, AppThemeChangedEventArgs args)
  7. {
  8. await JSRuntime.InvokeVoidAsync("viewService.SwitchTheme");
  9. }

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

  1. protected override async Task OnAfterRenderAsync(bool firstRender)
  2. {
  3. if (!firstRender)
  4. return;
  5. ···
  6. if (Application.Current.UserAppTheme==AppTheme.Dark)
  7. {
  8. await JSRuntime.InvokeVoidAsync("viewService.SwitchTheme");
  9. }
  10. }

在这里插入图片描述

项目地址

Github:maui-samples

原文链接:https://www.cnblogs.com/jevonsflash/p/18133608

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号