巧家县住房和城乡建设局网站,揭阳网站建设工作,惠州房地产网站开发,一个网站做多有几种颜色文章目录 获取资源从源码构建从CDN获取获取扩展插件 创建项目创建控件创建Blazor组件初始化保存销毁编写渲染逻辑 实现只读/编辑功能切换模式获取只读模式状态响应切换事件 实现明/暗主题切换项目地址 Editor.js 是一个基于 Web 的所见即所得富文本编辑器#xff0c;它由CodeX… 文章目录 获取资源从源码构建从CDN获取获取扩展插件 创建项目创建控件创建Blazor组件初始化保存销毁编写渲染逻辑 实现只读/编辑功能切换模式获取只读模式状态响应切换事件 实现明/暗主题切换项目地址 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应用的资源文件jscss等以便MAUI的视图呈现标准的Web UI。有两种方式可以获取
从源码构建从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/editorjslatest获取扩展插件
Editor.js中的每个块都由插件提供。有简单的外部脚本有自己的逻辑。默认Editor.js项目中已包含唯一的 Paragraph 块。其它的工具插件可以单独获取。
同样我们可以找到这些插件的源码编译或通过CDN获取
Header链接HTML块简单图片无后端要求图片清单列表嵌入引用
创建项目
新建.NET MAUI Blazor项目命名Editorjs
将editorjs.umd.js和各插件js文件拷贝至项目根目录下wwwroot文件夹文件结构如下 在wwwroot创建editorjs_index.html文件并在body中引入editorjs.umd.js和各插件js文件
body...script srclib/editorjs/editorjs.umd.js/scriptscript srclib/editorjs/tools/checklistlatest.js/scriptscript srclib/editorjs/tools/codelatest.js/scriptscript srclib/editorjs/tools/delimiterlatest.js/scriptscript srclib/editorjs/tools/embedlatest.js/scriptscript srclib/editorjs/tools/headerlatest.js/scriptscript srclib/editorjs/tools/imagelatest.js/scriptscript srclib/editorjs/tools/inline-codelatest.js/scriptscript srclib/editorjs/tools/linklatest.js/scriptscript srclib/editorjs/tools/nested-listlatest.js/scriptscript srclib/editorjs/tools/markerlatest.js/scriptscript srclib/editorjs/tools/quotelatest.js/scriptscript srclib/editorjs/tools/tablelatest.js/script
/body创建控件
创建 EditNotePage.xaml EditNotePage类作为视图控件继承于ContentViewEditNotePage.xaml的完整代码如下
ContentView xmlnshttp://schemas.microsoft.com/dotnet/2021/mauixmlns:xhttp://schemas.microsoft.com/winfx/2009/xamlxmlns:matoclr-namespace:Editorjs;assemblyEditorjsxmlns:serviceclr-namespace:Editorjs.ViewModels;assemblyEditorjsxmlns:xcthttp://schemas.microsoft.com/dotnet/2022/maui/toolkitx:NameMainPagex:ClassEditorjs.Controls.EditNotePageGrid BackgroundColor{AppThemeBinding Light{StaticResource LightPageBackgroundColor}, Dark{StaticResource DarkPageBackgroundColor}}RowDefinitionsAuto, *, AutoPadding20, 10, 20, 0Grid Grid.Row0Margin0, 0, 0, 10Grid.ColumnDefinitionsColumnDefinition Widthauto/ColumnDefinitionColumnDefinition/ColumnDefinitionColumnDefinition/ColumnDefinition/Grid.ColumnDefinitionsEntry Grid.Column1Placeholder请输入标题Margin10, 0, 0, 0VerticalOptionsCenterText{Binding Title}
/EntryHorizontalStackLayout Grid.Column2HeightRequest60VerticalOptionsCenterHorizontalOptionsEndMargin0, 0, 10, 0StackLayout RadioButtonGroup.GroupNameStateRadioButtonGroup.SelectedValue{Binding NoteSegmentState,ModeTwoWay}OrientationHorizontalRadioButton Value{x:Static service:NoteSegmentState.Edit}Content编辑/RadioButtonRadioButton Value{x:Static service:NoteSegmentState.PreView}Content预览/RadioButton/StackLayout/HorizontalStackLayout/GridBlazorWebView Grid.Row1Margin-10, 0x:NamemainMapBlazorWebViewHostPagewwwroot/editorjs_index.htmlBlazorWebView.RootComponentsRootComponent Selector#appx:NamerootComponentComponentType{x:Type mato:EditorjsPage} //BlazorWebView.RootComponents/BlazorWebViewActivityIndicator Grid.RowSpan4IsRunning{Binding Loading}/ActivityIndicator/Grid
/ContentView
创建一个EditNotePageViewModel的ViewModel类用于处理页面逻辑。代码如下
public class EditNotePageViewModel : ObservableObject, IEditorViewModel
{public FuncTaskstring OnSubmitting { get; set; }public Actionstring OnInited { get; set; }public Action OnFocus { get; set; }public EditNotePageViewModel(){Submit new Command(SubmitAction);NoteSegmentStateNoteSegmentState.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,Contentcontent});}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.ContentsavedContent;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 DotNetObjectReferenceEditorjsPage objRef;protected override void OnInitialized(){objRef DotNetObjectReference.Create(this);}
初始化
在script代码段中创建LoadContent函数用于加载EditorJs的初始内容。
div classce-maindiv ideditorjs/div
/div
LoadContent中调用函数window.editor new window.EditorJS(config)创建一个EditorJS对象其中config对象包括holdertoolsdata等属性关于EditorJs配置的更多说明请参考官方文档
script typetext/javascriptwindow.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: CMDSHIFTH},/*** Or pass class directly without any configuration*/image: {class: ImageTool},list: {class: NestedList,inlineToolbar: true,shortcut: CMDSHIFTL},checklist: {class: Checklist,inlineToolbar: true,},quote: {class: Quote,inlineToolbar: true,config: {quotePlaceholder: 输入引用内容,captionPlaceholder: 引用标题,},shortcut: CMDSHIFTO},marker: {class: Marker,shortcut: CMDSHIFTM},code: {class: CodeTool,shortcut: CMDSHIFTC},delimiter: Delimiter,inlineCode: {class: InlineCode,shortcut: CMDSHIFTC},linkTool: LinkTool,embed: Embed,table: {class: Table,inlineToolbar: true,shortcut: CMDALTT},},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: 请输入链接地址,Couldnt fetch the link data: 获取链接数据失败,Couldnt 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 Taskstring OnSubmitting()
{var savedContent await JSRuntime.InvokeAsyncstring(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.GroupNameStateRadioButtonGroup.SelectedValue{Binding NoteSegmentState,ModeTwoWay}OrientationHorizontalRadioButton Value{x:Static service:NoteSegmentState.Edit}Content编辑/RadioButtonRadioButton Value{x:Static service:NoteSegmentState.PreView}Content预览/RadioButton/StackLayoutEditorjs官方提供了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.NoteSegmentStateNoteSegmentState.PreView){var state await JSRuntime.InvokeAsyncbool(viewService.GetState);if (!state){await JSRuntime.InvokeAsyncbool(viewService.SwitchState);}}else if (EditNotePageViewModel.NoteSegmentStateNoteSegmentState.Edit){var state await JSRuntime.InvokeAsyncbool(viewService.GetState);if (state){await JSRuntime.InvokeAsyncbool(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.UserAppThemeAppTheme.Dark){await JSRuntime.InvokeVoidAsync(viewService.SwitchTheme);}} 项目地址
Github:maui-samples