Flutter Html Editor – 增强版
Flutter HTML Editor Enhanced 是一个适用于 Android、iOS 和 Web 的文本编辑器,通过 Summernote JavaScript 包装器帮助编写所见即所得的 HTML 代码。
请注意,此 README.md 文件中显示的 API 仅为文档的一部分,并且仅符合 GitHub 主分支!因此,这里可能存在尚未发布/发布的方法、选项和事件!如果您需要特定版本,请将此存储库的 GitHub 分支更改为您的版本,或使用在线 API 参考(推荐)。
| 视频示例 | 浅色模式和
ToolbarType.nativeGrid |
深色模式和
ToolbarPosition.belowEditor |
![]() |
![]() |
![]() |
| Flutter Web |
![]() |
目录
此软件包“增强”体现在哪些方面?
-
它对 Flutter Web 有官方支持,几乎支持所有移动功能。键盘快捷键(如 Ctrl+B 加粗)也可用!
-
它具有完全原生的基于 Flutter 的小部件控件
-
它使用高度优化的 WebView,在使用编辑器时提供最佳体验
-
它不使用本地服务器加载包含编辑器的 HTML 代码。相反,此软件包只是加载 HTML 文件,从而提高了性能和编辑器的启动时间。
-
它使用基于控制器的 API。您无需使用
GlobalKey来访问方法,而是可以随时随地调用。. -
它支持 Summernote 的许多方法
-
它支持 Summernote 的所有回调
-
它公开了
InAppWebViewController,因此您可以根据自己的喜好自定义 WebView——您甚至可以加载自己的 HTML 代码并注入自己的 JavaScript 以满足您的用例。 -
它支持深色模式
-
它支持极其精细的工具栏自定义
更多功能正在开发中!如果您希望添加其他功能,请提交功能请求或为项目做出贡献。
设置
将 html_editor_enhanced: ^2.4.0 添加到您的 pubspec.yaml 作为依赖项。
iOS 上需要额外设置以允许用户从存储中选择文件。有关更多详细信息,请参阅此处。
对于图片,该包使用 FileType.image;对于视频,使用 FileType.video;对于音频,使用 FileType.audio;对于任何其他文件,使用 FileType.any。您可以只完成您计划在编辑器中启用的特定按钮的设置。
v2.0.0 迁移指南
基本用法
import 'package:html_editor/html_editor.dart';
HtmlEditorController controller = HtmlEditorController();
@override Widget build(BuildContext context) {
return HtmlEditor(
controller: controller, //required
htmlEditorOptions: HtmlEditorOptions(
hint: "Your text here...",
//initalText: "text content initial, if any",
),
otherOptions: OtherOptions(
height: 400,
),
);
}
Web 版重要注意事项
目前,当有许多 UI 元素绘制在 IframeElement 上时,会出现相当多的闪烁和重绘。有关更多详细信息,请参阅 https://github.com/flutter/flutter/issues/71888。
当前的解决方案是使用 flutter run --web-renderer html 和 flutter build web --web-renderer html 构建和/或运行您的 Web 应用。
关注 https://github.com/flutter/flutter/issues/80524 以获取潜在修复的更新,同时上述解决方案应该能解决大部分闪烁问题。
API 参考
完整的 API 参考,请参阅此处。
完整示例,请参阅此处。
下面,您将找到 HtmlEditor 小部件接受的参数的简要说明以及一些代码片段,以帮助您使用此包。
参数 – HtmlEditor
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| 控制器 | HtmlEditorController |
空 | 必填参数。创建控制器实例并将其传递给小部件。这确保任何调用的方法仅在其 HtmlEditor 实例上工作,允许您在一个页面上使用多个 HTML 小部件。 |
| 回调 | Callbacks |
空 | 自定义各种事件的回调 |
| 选项 | HtmlEditorOptions |
HtmlEditorOptions() |
用于设置各种选项的类。有关更多详细信息,请参阅下文。 |
| 插件 | List<Plugins> |
空 | 自定义激活哪些插件。有关更多详细信息,请参阅下文。 |
| 工具栏 | List<Toolbar> |
参见小部件的构造函数 | 自定义工具栏上显示的按钮及其顺序。有关更多详细信息,请参阅下文。 |
参数 – HtmlEditorController
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| processInputHtml | 布尔值 |
真 |
确定是否对任何输入 HTML 进行处理(例如转义引号、撇号和删除 /n) |
| processNewLineAsBr | 布尔值 |
假 |
确定在任何*输入*HTML中,换行符(\n)是否变为 |
| processOutputHtml | 布尔值 |
真 |
确定是否对任何输出 HTML 进行处理(例如 变为 "") |
参数 – HtmlEditorOptions
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| 自动调整高度 | 布尔值 |
真 |
加载编辑器后,通过分析 HTML 高度自动调整文本编辑器的高度。推荐值:true。有关更多详细信息,请参阅下文。 |
| 为键盘调整高度 | 布尔值 |
真 |
如果键盘处于活动状态且与编辑器重叠,则调整编辑器的高度以防止重叠。推荐值:true,仅在移动设备上有效。有关更多详细信息,请参阅下文。 |
| 字符限制 | 整数 |
空 |
设置编辑器的字符限制。达到限制后,用户将不允许再输入。 |
| 自定义选项 | 字符串 |
空 |
使用 Summernote 语法(参见此处)提供 Summernote 初始化自定义选项 |
| 黑暗模式 | 布尔值 |
空 |
设置深色模式的状态 – false:始终浅色,null:跟随系统,true:始终深色 |
| 文件路径 | 字符串 |
空 |
允许您指定自己的 HTML 加载到 Webview 中。您可以创建一个带有 Summernote 的自定义页面,或者理论上加载任何其他编辑器/HTML。 |
| 提示 | 字符串 |
空 | 占位符提示文本 |
| 初始文本 | 字符串 |
空 | 文本编辑器的初始文本内容 |
| 输入类型 | HtmlInputType |
HtmlInputType.text |
允许您设置移动设备上编辑器的虚拟键盘显示方式 |
| 移动设备上下文菜单 | 上下文菜单 |
空 |
当用户在编辑器中选择文本时,自定义上下文菜单。有关 ContextMenu 的文档,请参阅此处 |
| 移动长按持续时间 | 持续时间 |
持续时间(毫秒: 500) |
设置识别长按的持续时间 |
| 移动端初始脚本 | UnmodifiableListView<UserScript> |
空 |
轻松注入脚本以执行更改编辑器背景颜色等操作。有关 UserScript 的文档,请参阅此处 |
| Web初始脚本 | UnmodifiableListView<WebScript> |
空 |
轻松注入脚本以执行更改编辑器背景颜色等操作。有关更多详细信息,请参阅下文。 |
| 应确保可见 | 布尔值 |
假 |
当 webview 获得焦点时,将父 Scrollable 滚动到编辑器小部件的顶部。如果 HtmlEditor 不在 Scrollable 中,则**不要**使用此参数。有关更多详细信息,请参阅下文。 |
| 拼写检查 | 布尔值 |
假 |
指定是否在编辑器中使用拼写检查并为错误的拼写加下划线。 |
参数 – HtmlToolbarOptions
工具栏选项
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| 音频扩展 | List<String> |
空 |
插入音频文件时允许的扩展名 |
| 自定义工具栏按钮 | List<Widget> |
空 | 向工具栏添加自定义按钮 |
| 自定义工具栏插入索引 | List<int> |
空 | 允许您设置每个自定义工具栏按钮应插入到工具栏小部件列表中的位置 |
| 默认工具栏按钮 | List<Toolbar> |
(所有构造函数都处于活动状态) | 允许您隐藏/显示某些按钮或某些按钮组 |
| 其他文件扩展名 | List<String> |
空 |
插入图像/音频/视频以外的文件时允许的扩展名 |
| 图片扩展名 | List<String> |
空 |
插入图像时允许的扩展名 |
| 初始展开 | 布尔值 |
假 |
在使用 ToolbarType.nativeExpandable 时,设置工具栏是否初始展开。 |
| linkInsertInterceptor | FutureOr<bool> Function(String, String, bool) |
空 |
拦截插入到编辑器中的任何链接。该函数传递显示文本、URL 以及它是否在新选项卡中打开。 |
| 媒体链接插入拦截器 | FutureOr<bool> Function(String, InsertFileType) |
空 |
拦截插入到编辑器中的任何媒体链接。该函数传递 URL 和 InsertFileType,它指示插入了哪种文件类型 |
| 媒体上传拦截器 | FutureOr<bool> Function(PlatformFile, InsertFileType) |
空 |
拦截插入到编辑器中的任何媒体文件。该函数传递包含所有相关文件数据的 PlatformFile,以及指示插入文件类型的 InsertFileType。 |
| onButtonPressed | FutureOr<bool> Function(ButtonType, bool?, void Function()?) |
空 |
拦截任何按钮按下。该函数传递被按下按钮的枚举,按钮的当前选中状态(如果适用)和更新状态的函数(如果适用)。 |
| onDropdownChanged | FutureOr<bool> Function(DropdownType, dynamic, void Function(dynamic)?) |
空 |
拦截任何下拉菜单更改。该函数传递更改的下拉菜单的枚举、更改的值以及用于更新更改值的函数(如果适用)。 |
| onOtherFileLinkInsert | Function(String) |
空 |
拦截除图像/音频/视频以外的文件链接插入。使用其他文件按钮时需要此处理程序,因为该软件包没有内置处理程序 |
| onOtherFileUpload | Function(PlatformFile) |
空 |
拦截除图像/音频/视频以外的文件上传。使用其他文件按钮时需要此处理程序,因为该软件包没有内置处理程序 |
| 其他文件扩展名 | List<String> |
空 |
插入图像/音频/视频以外的文件时允许的扩展名 |
| 工具栏类型 | ToolbarType |
ToolbarType.nativeScrollable |
自定义工具栏显示方式(网格视图、可滚动或可展开) |
| 工具栏位置 | ToolbarPosition |
ToolbarPosition.aboveEditor |
设置工具栏显示位置(编辑器上方或下方) |
| 视频扩展名 | List<String> |
空 |
插入视频时允许的扩展名 |
样式选项
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| 渲染边框 | 布尔值 |
假 |
在下拉菜单和按钮周围渲染边框 |
| 文本样式 | TextStyle |
空 |
显示下拉菜单和按钮时使用的 TextStyle |
| 分隔器小部件 | Widget |
VerticalDivider(indent: 2, endIndent: 2, color: Colors.grey) |
设置分隔每个按钮/下拉菜单组的小部件 |
| 渲染分隔器小部件 | 布尔值 |
真 |
是否应该渲染分隔器小部件 |
| 工具栏项目高度 | 双精度 |
36 |
设置下拉菜单和按钮的高度。按钮将保持正方形宽高比。 |
| 网格视图水平间距 | 双精度 |
5 |
当以 ToolbarType.nativeGrid 显示工具栏时,按钮组之间使用的水平间距 |
| 网格视图垂直间距 | 双精度 |
5 |
当以 ToolbarType.nativeGrid 显示工具栏时,按钮组之间使用的垂直间距 |
样式选项 - 仅适用于下拉菜单
| 参数 | 类型 | 默认值 |
|---|---|---|
| 下拉菜单高度 | 整数 |
8 |
| 下拉图标 | Widget |
空 |
| 下拉图标颜色 | 颜色 |
空 |
| 下拉图标大小 | 双精度 |
24 |
| 下拉项目高度 | 双精度 |
kMinInteractiveDimension (48) |
| 下拉焦点颜色 | 颜色 |
空 |
| 下拉背景颜色 | 颜色 |
空 |
| 下拉菜单方向 | DropdownMenuDirection |
空 |
| 下拉菜单最大高度 | 双精度 |
空 |
| 下拉框装饰 | BoxDecoration |
空 |
样式选项 - 仅适用于按钮
| 参数 | 类型 | 默认值 |
|---|---|---|
| 按钮颜色 | 颜色 |
空 |
| 按钮选中颜色 | 颜色 |
空 |
| 按钮填充颜色 | 颜色 |
空 |
| 按钮焦点颜色 | 颜色 |
空 |
| 按钮高亮颜色 | 颜色 |
空 |
| 按钮悬停颜色 | 颜色 |
空 |
| 按钮飞溅颜色 | 颜色 |
空 |
| 按钮边框颜色 | 颜色 |
空 |
| 按钮选中边框颜色 | 颜色 |
空 |
| 按钮边框半径 | BorderRadius |
空 |
| 按钮边框宽度 | 双精度 |
空 |
参数 – 其他选项
| 参数 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| 装饰 | BoxDecoration |
空 |
包围小部件的 BoxDecoration |
| 高度 | 双精度 |
空 |
小部件的高度(包括工具栏和编辑区) |
方法
按此方式访问这些方法:<控制器名称>.<方法名称>
| 方法 | 参数 | 返回值 | 描述 |
|---|---|---|---|
| addNotification() | String html, NotificationType notificationType |
不适用 | 使用提供的 HTML 内容向编辑器底部添加通知。NotificationType 决定其样式。 |
| 清除() | 不适用 | 不适用 | 将 HTML 编辑器重置为默认状态 |
| clearFocus() | 不适用 | 不适用 | 清除 WebView 的焦点并将移动设备上的高度重置为原始高度。**请勿**在 Flutter Web 中使用此方法。 |
| 禁用() | 不适用 | 不适用 | 禁用编辑器(应用灰色蒙版并吸收所有触摸) |
| 启用() | 不适用 | 不适用 | 启用编辑器 |
| execCommand() | String command, String argument (可选) |
不适用 | 允许您轻松运行任何 execCommand 命令。有关用法,请参阅 MDN 文档。 |
| getText() | 不适用 | Future<String> |
返回编辑器中当前的 HTML |
| getSelectedTextWeb() | bool (可选) |
Future<String> |
获取编辑器中当前选定的文本,带或不带 HTML 标签。**请勿**在 Flutter Mobile 中使用此方法。 |
| insertHtml() | 字符串 |
不适用 | 在当前光标位置将提供的 HTML 字符串插入编辑器。**请勿**将此方法用于纯文本字符串。 |
| insertLink() | String text, String url, bool isNewWindow |
不适用 | 在当前光标位置使用提供的文本和 URL 将超链接插入编辑器。isNewWindow 定义如果点击链接是否启动新的浏览器窗口。 |
| insertNetworkImage() | String url, String filename (可选) |
不适用 | 在当前光标位置插入使用提供的 URL 和可选文件名的图像到编辑器中。该图像必须可以通过 URL 访问。 |
| insertText() | 字符串 |
不适用 | 在当前光标位置将提供的文本插入编辑器。**请勿**将此方法用于 HTML 字符串。 |
| recalculateHeight() | 不适用 | 不适用 | 通过重新评估 document.body.scrollHeight 重新计算编辑器的高度 |
| 重做() | 不适用 | 不适用 | 重做编辑器中的上一个命令 |
| reloadWeb() | 不适用 | 不适用 | 在 Flutter Web 中重新加载网页。这主要用于在主题更改时刷新文本编辑器主题。**请勿**在 Flutter Mobile 中使用此方法。 |
| removeNotification() | 不适用 | 不适用 | 从编辑器底部移除当前通知 |
| resetHeight() | 不适用 | 不适用 | 将 Webview 的高度重置为原始高度。**请勿**在 Flutter Web 中使用此方法。 |
| setHint() | 字符串 |
不适用 | 设置编辑器当前的提示文本 |
| setFocus() | 不适用 | 不适用 | 如果指针在 webview 中,焦点将设置为编辑器框 |
| setFullScreen() | 不适用 | 不适用 | 将编辑器设置为占用整个 WebView 区域 |
| setText() | 字符串 |
不适用 | 将 HTML 中的当前文本设置为输入 HTML 字符串 |
| toggleCodeview() | 不适用 | 不适用 | 在代码视图和富文本视图之间切换 |
| 撤消() | 不适用 | 不适用 | 撤消编辑器中的上一个命令 |
回调
每个回调都定义为 Function(<在某些情况下有参数>)。有关每个回调的更具体详细信息,请参阅文档。
| 回调 | 参数 | 描述 |
|---|---|---|
| onBeforeCommand | 字符串 |
在调用某些命令(如撤消和重做)之前调用,传递命令调用前编辑器中的 HTML |
| onChangeContent | 字符串 |
当编辑器内容改变时调用,传递编辑器中当前的 HTML |
| onChangeCodeview | 字符串 |
当代码视图内容发生变化时调用,传递代码视图中当前的代码 |
| onChangeSelection | EditorSettings |
当编辑器当前选择发生变化时调用,传递所有编辑器设置(例如粗体/斜体/下划线、颜色、文本方向等)。 |
| onDialogShown | 不适用 | 当显示图片、链接、视频或帮助对话框时调用 |
| onEnter | 不适用 | 当按下 Enter/Return 键时调用 |
| onFocus | 不适用 | 当富文本字段获得焦点时调用 |
| onBlur | 不适用 | 当富文本字段或代码视图失去焦点时调用 |
| onBlurCodeview | 不适用 | 当代码视图获得或失去焦点时调用 |
| onImageLinkInsert | 字符串 |
通过 URL 插入图片时调用,传递图片 URL |
| onImageUpload | FileUpload |
通过上传插入图片时调用,传递包含文件名、修改日期、大小和 MIME 类型的 FileUpload |
| onImageUploadError | FileUpload, String, UploadError |
当图片上传失败时调用,传递可能包含文件名、修改日期、大小和 MIME 类型(或为空)的 FileUpload,传递 base64 字符串(或为空),以及描述错误类型的 UploadError |
| onInit | 不适用 | 当富文本字段初始化并可以调用 JavaScript 方法时调用 |
| onKeyDown | 整数 |
按下键时调用,传递按下键的键码 |
| onKeyUp | 整数 |
松开键时调用,传递松开键的键码 |
| onMouseDown | 不适用 | 当鼠标/手指按下时调用 |
| onMouseUp | 不适用 | 当鼠标/手指松开时调用 |
| onNavigationRequestMobile | 字符串 |
仅在移动设备上当 webview 的 URL 即将更改时调用 |
| onPaste | 不适用 | 当内容粘贴到编辑器中时调用 |
| onScroll | 不适用 | 当编辑器框滚动时调用 |
获取器
<控制器名称>.editorController。这返回InAppWebViewController,它管理显示编辑器的 webview。
这极其强大,因为它允许您直接在应用程序中创建自己的自定义方法和实现。有关控制器的文档,请参阅 flutter_inappwebview。
此获取器**不应**在 Flutter Web 中使用。如果您正在进行跨平台实现,请使用 kIsWeb 在您的代码中检查当前平台。
。这返回编辑器中的文本字符数。.characterCount
工具栏
此 API 允许您以简洁、可读的格式自定义工具栏。
默认情况下,工具栏将启用所有按钮,除了“其他文件”按钮,因为该插件无法直接处理这些文件。
自定义实现可能如下所示
HtmlEditorController controller = HtmlEditorController();
Widget htmlEditor = HtmlEditor(
controller: controller, //required
//other options
toolbarOptions: HtmlToolbarOptions(
defaultToolbarButtons: [
StyleButtons(),
ParagraphButtons(lineHeight: false, caseConverter: false)
]
)
);
如果您将 Toolbar 构造函数留空(如上面的 Style()),则软件包会解释为您希望 Style 组的所有按钮都可见。
如果想从组中移除某些按钮,可以将其按钮名称设置为 false,如上面的示例所示。
顺序很重要!您首先设置的组将是首先显示的按钮组。
如果您不想显示整个按钮组,只需不要在 Toolbar 列表中包含其构造函数即可!这意味着如果您只想禁用一个按钮,您仍然必须提供所有其他构造函数。
您也可以创建自己的工具栏按钮!有关更多详细信息,请参阅下文。
插件
此 API 允许您从 Summernote Awesome 库中添加某些 Summernote 插件。
目前支持以下插件
-
Summernote 大小写转换器 –
将选定的文本转换为全部小写、全部大写、句首字母大写或标题大小写。通过工具栏中的ParagraphButtons下拉菜单支持。 -
Summernote 列表样式 –
自定义 ul 和 ol 列表样式。通过工具栏中的ListButtons下拉菜单支持。 -
Summernote RTL –
在 LTR 和 RTL 格式之间切换当前选定的文本。通过工具栏中ParagraphButtons中的两个按钮支持。 -
Summernote At Mention –
当在编辑器中键入“@”字符时,显示可用提及的下拉列表。此实现要求您传递一个可用提及列表,并且您还可以提供一个函数,以便在将提及插入编辑器时调用。 -
Summernote 文件 –
支持 base64 格式的图片文件(jpg, png, gif, wvg, webp)、音频文件(mp3, ogg, oga)和视频文件(mp4, ogv, webm)。通过工具栏中InsertButtons中的图片/音频/视频/其他文件按钮支持。
此列表并非最终版本,可能会添加更多内容。如果您希望看到对某个特定插件的支持,请提交功能请求!
默认情况下不激活任何插件。可以通过修改工具栏项目来激活它们,详情请参阅上文。
激活 Summernote At Mention
HtmlEditorController controller = HtmlEditorController();
Widget htmlEditor = HtmlEditor(
controller: controller, //required
//other options
plugins: [
SummernoteAtMention(
//returns the dropdown items on mobile
getSuggestionsMobile: (String value) {
List<String> mentions = ['test1', 'test2', 'test3'];
return mentions
.where((element) => element.contains(value))
.toList();
},
//returns the dropdown items on web
mentionsWeb: ['test1', 'test2', 'test3'],
onSelect: (String value) {
print(value);
}
),
]
);
HtmlEditorOptions 参数
本节包含 HtmlEditorOptions 中选定参数的较长描述。对于此处未提及的参数,请参阅上方的参数表以获取简短描述。如果您有进一步的问题,请提交问题。
autoAdjustHeight
默认值:true
此选项参数通过获取 JS document.body.scrollHeight 返回的值和工具栏 GlobalKey (toolbarKey.currentContext?.size?.height) 来自动设置编辑器的高度。
这很有用,因为工具栏可能有 1 到 5 行,具体取决于小部件的配置、屏幕尺寸、方向等。在 build() 执行之前,无法可靠地判断工具栏会有多大,因此简单地硬编码 webview 的高度可能会导致底部出现空白或可滚动的 webview。通过在工具栏小部件上使用 JS 和 GlobalKey,编辑器可以获得确切的高度并更新小部件以反映该高度。
有一个缺点:页面加载后,Webview 的大小会明显变化。根据变化的程度,可能会令人不适。有时,Webview 需要一秒钟才能调整到新大小,您可能会在 Webview 容器调整自身后一两秒看到编辑器页面上下跳动。
如果这无助于您的用例,请随时禁用它,但推荐值为 true。
adjustHeightForKeyboard
默认值:true,仅在移动设备上考虑
此选项参数在键盘活动并与编辑器重叠时更改编辑器的高度。
这很有用,因为目前在 Flutter 上,当键盘处于活动状态时,webview 不会改变其视图。这意味着如果您的编辑器占据了页面的高度,如果用户输入了很长的文本,他们可能无法看到他们正在输入的内容,因为它被键盘遮挡了。
当此参数启用时,webview 将调整到完美高度,以确保所有输入内容都可见,并且一旦键盘隐藏,编辑器将恢复到其原始高度。
键盘弹出/消失后,webview 需要一段时间才能来回切换,但延迟并不是很糟糕。如果页面上有其他小部件,强烈建议将 webview 放在 Scrollable 中并启用 shouldEnsureVisible——如果编辑器位于页面下半部分,它将滚动到顶部,然后相应地设置高度,而不是插件试图为被键盘完全遮挡的 webview 设置高度。
请参阅下文的示例用例。
如果这无助于您的用例,请随时禁用它,但推荐值为 true。
filePath
此选项参数允许您通过提供资产中自定义 HTML 文件的文件路径,完全自定义加载到 Webview 中的 HTML。
在为 Web 提供文件路径时,需要/推荐一种特定的格式,因为 Web 实现会将 HTML 作为 String 加载,并使用 replaceAll(). 直接对其进行更改,而不是使用像 evaluateJavascript() 这样的方法——因为这在 Web 上不存在。
在 Web 上,您应该包含以下内容
-
位于内部 – 这可启用深色模式支持 -
位于内部,且在您的 summernote下方 – 这允许加载任何已启用插件的 JS 和 CSS 文件位于内部,且在您的 summernote下方 – **必需** – 这允许 Dart 和 JS 相互通信。如果您不包含此内容,则方法/回调将不起作用。注意事项
-
请勿在您的自定义 HTML 文件中初始化 Summernote 编辑器!软件包会处理此事。
-
确保将 Summernote 的
id设置为summernote-2! –。 -
请务必在文件中包含 jquery 和 Summernote JS/CSS!该软件包不会为您处理此问题。
您可以使用软件包中的这些文件,以避免添加更多资产文件
<script src="assets/packages/html_editor_enhanced/assets/jquery.min.js"></script> <link href="assets/packages/html_editor_enhanced/assets/summernote-lite.min.css" rel="stylesheet"> <script src="assets/packages/html_editor_enhanced/assets/summernote-lite.min.js"></script>
请参阅下文的示例 HTML 文件以获取实际示例。
shouldEnsureVisible默认值:false
当 Webview 获得焦点或文本输入到编辑器中时,此选项参数会将编辑器容器滚动到视图中。
您只能在
HtmlEditor位于Scrollview内部时使用此参数,否则它不起作用。这在页面是
SingleChildScrollView或具有多个小部件(例如表单)的类似情况时很有用。当用户浏览不同的字段时,它会将 webview 弹出到视图中,就像TextField在输入文本时会滚动到视图中一样。请参阅下文的示例,其中有一个很好的使用方式。
webInitialScripts此参数允许您为 Web 上的编辑器指定自定义 JavaScript。可以使用
controller.evaluateJavascriptWeb在任何时候调用这些脚本。您必须使用
WebScript类添加这些脚本,该类接受name和script参数。name**必须**是唯一的标识符,否则您想要的脚本可能无法执行。在script参数中传递您的 JavaScript 代码。该软件包也支持从 JavaScript 返回值。您应该运行
var result = await controller.evaluateJavascriptWeb(。, hasReturnValue: true); 要获取返回值,您必须在 JavaScript 代码末尾添加以下内容
window.parent.postMessage(JSON.stringify({"type": "toDart: <WebScript name goes here>", <add any other params you wish to return here>}), "*");
您可以查看下方的完整示例
HtmlToolbarOptions参数本节包含
HtmlToolbarOptions中选定参数的较长描述。对于此处未提及的参数,请参阅上方的参数表以获取简短描述。如果您有进一步的问题,请提交问题。customToolbarButtons和customToolbarButtonsInsertionIndices这两个参数允许您插入自定义按钮并设置它们插入到工具栏小部件列表中的位置。
这看起来像这样
HtmlEditorController controller = HtmlEditorController(); Widget htmlEditor = HtmlEditor( controller: controller, //required //other options toolbarOptions: HtmlToolbarOptions( defaultToolbarButtons: [ StyleButtons(), FontSettingButtons(), FontButtons(), ColorButtons(), ListButtons(), ParagraphButtons(), InsertButtons(), OtherButtons(), ], customToolbarButtons: [ //your widgets here Button1(), Button2(), ], customToolbarInsertionIndices: [2, 5] ) );
在上面的示例中,我们定义了两个按钮,将它们插入到索引 2 和 5。这些按钮不会分别插入到
FontSettingButtons之前和ListButtons之前!每个默认按钮组可能有几个不同的子组按钮组 子组数量 StyleButtons1 FontSettingButtons3 FontButtons2 颜色按钮1 ListButtons2 ParagraphButtons5 InsertButtons1 OtherButtons2 如果您的某些按钮被停用,则子组的数量可能会减少。插入索引取决于这些子组而不是整个按钮组。计算插入索引的一个简单方法是构建应用程序并计算在您想要插入按钮的位置之前每个按钮组/下拉菜单之间的分隔符空间的数量。
因此,考虑到这一点,
Button1将插入到FontSettingButtons的前两个子组之间,而Button2将插入到FontButtons的两个子组之间。在为小部件创建
onPressed/onTap/onChanged方法时,您可以使用controller.execCommand或控制器上的任何其他方法在编辑器中执行操作。注意事项
-
在 Web 上使用
controller.editorController.将不起作用! -
如果您不提供
customToolbarButtonsInsertionIndices,插件会将您的按钮插入到默认工具栏列表的末尾 -
如果您提供
customToolbarButtonsInsertionIndices,它的长度**必须**与您的customToolbarButtons小部件列表相同。
linkInsertInterceptor、mediaLinkInsertInterceptor、otherFileLinkInsert、mediaUploadInterceptor和onOtherFileUpload这些回调帮助您拦截任何插入到编辑器中的链接或文件。
参数 类型 描述 linkInsertInterceptor FutureOr<bool> Function(String, String, bool)拦截插入到编辑器中的任何链接。该函数传递显示文本 ( String)、URL (String) 以及它是否在新选项卡中打开 (bool)。媒体链接插入拦截器 FutureOr<bool> Function(String, InsertFileType)拦截插入到编辑器中的任何媒体链接。该函数传递 URL ( String)。媒体上传拦截器 FutureOr<bool> Function(PlatformFile, InsertFileType)拦截插入到编辑器中的任何媒体文件。该函数传递 PlatformFile,其中包含所有相关文件数据。您可以使用它上传到您的服务器,提取 base64 数据,执行文件验证等。它还传递文件类型(图像/音频/视频)。onOtherFileLinkInsert Function(String)拦截除图像/音频/视频以外的文件链接插入。使用其他文件按钮时需要此处理程序,因为该软件包没有内置处理程序。该函数传递 URL ( String)。它还传递文件类型(图像/音频/视频)。onOtherFileUpload Function(PlatformFile)拦截除图像/音频/视频以外的文件上传。使用其他文件按钮时需要此处理程序,因为该软件包没有内置处理程序。该函数传递 PlatformFile,其中包含所有相关文件数据。您可以使用它上传到您的服务器,提取 base64 数据,执行文件验证等。对于
linkInsertInterceptor、mediaLinkInsertInterceptor和mediaUploadInterceptor,您必须返回一个bool来告诉插件它应该做什么。当您返回 false 时,它假定您已处理用户请求并采取了行动。当您返回 true 时,插件将使用默认处理程序来处理用户请求。使用“其他文件”按钮时,
onOtherFileLinkInsert和onOtherFileUpload是必需的。此按钮默认不活动,因此如果将其激活,则必须提供这些函数,否则当用户插入图像/音频/视频以外的文件时,什么都不会发生。请参阅下文的示例。
onButtonPressed和onDropdownChanged这些回调帮助您拦截任何按钮按下或下拉菜单更改。
参数 类型 描述 onButtonPressed FutureOr<bool> Function(ButtonType, bool?, void Function()?)拦截任何按钮按下。该函数传递被按下按钮的枚举,按钮的当前选中状态(如果适用)和更新状态的函数(如果适用)。 onDropdownChanged FutureOr<bool> Function(DropdownType, dynamic, void Function(dynamic)?)拦截任何下拉菜单更改。该函数传递更改的下拉菜单的枚举、更改的值以及用于更新更改值的函数(如果适用)。 您必须返回一个
bool来告诉插件它应该做什么。当您返回 false 时,它假定您已处理用户请求并采取了行动。当您返回 true 时,插件将使用默认处理程序来处理用户请求。某些按钮和下拉菜单(例如复制/粘贴和大小写转换器)无需更新其更改值,因此对于这些按钮,将不提供在处理用户请求后更新值的函数。
请参阅下文的示例。
使用
ToolbarPosition.custom的自定义工具栏位置您可以使用
toolbarPosition: ToolbarPosition.custom和ToolbarWidget()小部件来完全自定义工具栏的放置位置。可能性是无限的——您可以使用 Slivers 将工具栏放置在粘性标题中,您可以决定随时显示/隐藏工具栏,或者您可以将工具栏制作成一个浮动的、可拖动的小部件!ToolbarWidget()需要您为编辑器本身创建的HtmlEditorController,以及您提供给Html构造函数的HtmlToolbarOptions。这些可以简单地复制粘贴,无需更改。一个将工具栏放置在不同于正常位置的基本示例
HtmlEditorController controller = HtmlEditorController(); Widget column = Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ HtmlEditor( controller: controller, htmlEditorOptions: HtmlEditorOptions( hint: 'Your text here...', shouldEnsureVisible: true, //initialText: "<p>text content initial, if any</p>", ), htmlToolbarOptions: HtmlToolbarOptions( toolbarPosition: ToolbarPosition.custom, //required to place toolbar anywhere! //other options ), otherOptions: OtherOptions(height: 550), ), //other widgets here Widget1(), Widget2(), ToolbarWidget( controller: controller, htmlToolbarOptions: HtmlToolbarOptions( toolbarPosition: ToolbarPosition.custom, //required to place toolbar anywhere! //other options ), ) ] );
HtmlEditorController参数processInputHtml、processOutputHtml和processNewLineAsBr默认值:分别为 true、true、false
processInputHtml将所有出现的"替换为\\",将'替换为\\',并将\r、\r\n、\n和\n\n替换为空字符串。这是必要的,以防止在将 HTML 插入编辑器时出现语法异常,因为引号和其他特殊字符不会被转义。如果您已经对 HTML 输入中的所有相关字符进行了净化和转义,建议将此参数设置为false。您可能还希望在 Web 上将此参数设置为false,因为在测试中这些字符似乎默认已正确处理,但您的 HTML 可能并非如此。processOutputHtml将输出 HTML 替换为"",如果-
它是空的
-
它是
-
它是
-
它是
这些可能看起来有些随意,但它们是 Summernote 编辑器可能具有的三种可能的默认/初始 HTML 代码。如果您仍然希望收到这些输出,请将参数设置为
false。processNewLineAsBr将\n和\n\n替换为。这仅建议在将纯文本作为初始值插入时使用。在典型的 HTML 中,任何换行符都将被忽略,因此此参数默认为false。示例
请参阅示例应用,了解如何使用大多数方法和回调。您还可以试用参数以查看其功能。
本节稍后将随着此库的增长和更多功能的实现而更新,提供更专业和具体的示例。
linkInsertInterceptor、mediaLinkInsertInterceptor、otherFileLinkInsert、mediaUploadInterceptor和onOtherFileUpload的示例示例代码
注意:此示例使用 http 包。
import 'package:file_picker/file_picker.dart'; import 'package:http/http.dart' as http; Widget editor = HtmlEditor( controller: controller, toolbarOptions: ToolbarOptions( mediaLinkInsertInterceptor: (String url, InsertFileType type) { if (url.contains(website_url)) { controller.insertNetworkImage(url); } else { controller.insertText("This file is invalid!"); } return false; }, mediaUploadInterceptor: (PlatformFile file, InsertFileType type) async { print(file.name); //filename print(file.size); //size in bytes print(file.extension); //MIME type (e.g. image/jpg) //either upload to server: if (file.bytes != null && file.name != null) { final request = http.MultipartRequest('POST', Uri.parse("your_server_url")); request.files.add(http.MultipartFile.fromBytes("file", file.bytes, filename: file.name)); //your server may require a different key than "file" final response = await request.send(); //try to insert as network image, but if it fails, then try to insert as base64: if (response.statusCode == 200) { controller.insertNetworkImage(response.body["url"], filename: file.name!); //where "url" is the url of the uploaded image returned in the body JSON } else { if (type == InsertFileType.image) { String base64Data = base64.encode(file.bytes!); String base64Image = """<img src="data:image/${file.extension};base64,$base64Data" data-filename="${file.name}"/>"""; controller.insertHtml(base64Image); } else if (type == InsertFileType.video) { String base64Data = base64.encode(file.bytes!); String base64Image = """<video src="data:video/${file.extension};base64,$base64Data" data-filename="${file.name}"/>"""; controller.insertHtml(base64Image); } else if (type == InsertFileType.audio) { String base64Data = base64.encode(file.bytes!); String base64Image = """<audio src="data:audio/${file.extension};base64,$base64Data" data-filename="${file.name}"/>"""; controller.insertHtml(base64Image); } } } //or insert as base64: if (file.bytes != null) { if (type == InsertFileType.image) { String base64Data = base64.encode(file.bytes!); String base64Image = """<img src="data:image/${file.extension};base64,$base64Data" data-filename="${file.name}"/>"""; controller.insertHtml(base64Image); } else if (type == InsertFileType.video) { String base64Data = base64.encode(file.bytes!); String base64Image = """<video src="data:video/${file.extension};base64,$base64Data" data-filename="${file.name}"/>"""; controller.insertHtml(base64Image); } else if (type == InsertFileType.audio) { String base64Data = base64.encode(file.bytes!); String base64Image = """<audio src="data:audio/${file.extension};base64,$base64Data" data-filename="${file.name}"/>"""; controller.insertHtml(base64Image); } } return false; }, ), );
linkInsertInterceptor、onOtherFileLinkInsert和onOtherFileUpload可以以非常相似的方式实现,只是它们在函数中不使用InsertFileType枚举。onOtherFileLinkInsert和onOtherFileUpload也无需返回bool。onButtonPressed和onDropdownChanged示例示例代码
Widget editor = HtmlEditor( controller: controller, toolbarOptions: ToolbarOptions( onButtonPressed: (ButtonType type, bool? status, Function()? updateStatus) { print("button '${describeEnum(type)}' pressed, the current selected status is $status"); //run a callback and return false and update the status, otherwise return true; }, onDropdownChanged: (DropdownType type, dynamic changed, Function(dynamic)? updateSelectedItem) { print("dropdown '${describeEnum(type)}' changed to $changed"); //run a callback and return false and update the changed value, otherwise return true; }, ), );
adjustHeightForKeyboard示例示例代码
class _HtmlEditorExampleState extends State<HtmlEditorExample> { final HtmlEditorController controller = HtmlEditorController(); @override Widget build(BuildContext context) { return GestureDetector( onTap: () { if (!kIsWeb) { // this is extremely important to the example, as it allows the user to tap any blank space outside the webview, // and the webview will lose focus and reset to the original height as expected. controller.clearFocus(); } }, child: Scaffold( appBar: AppBar( title: Text(widget.title), elevation: 0, ), body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ //other widgets HtmlEditor( controller: controller, htmlEditorOptions: HtmlEditorOptions( shouldEnsureVisible: true, //adjustHeightForKeyboard is true by default hint: "Your text here...", //initialText: "<p>text content initial, if any</p>", ), otherOptions: OtherOptions( height: 550,c ), ), //other widgets ], ), ), ), ); } }
shouldEnsureVisible示例示例代码
import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:html_editor_enhanced/html_editor.dart'; class _ExampleState extends State<Example> { final HtmlEditorController controller = HtmlEditorController(); @override Widget build(BuildContext context) { return GestureDetector( onTap: () { if (!kIsWeb) { //these lines of code hide the keyboard and clear focus from the webview when any empty //space is clicked. These are very important for the shouldEnsureVisible to work as intended. SystemChannels.textInput.invokeMethod('TextInput.hide'); controller.editorController!.clearFocus(); } }, child: Scaffold( appBar: AppBar( title: Text(widget.title), elevation: 0, actions: [ IconButton( icon: Icon(Icons.check), tooltip: "Save", onPressed: () { //save profile details } ), ] ), body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Padding( padding: EdgeInsets.only(left: 18, right: 18), child: TextField( controller: titleController, textInputAction: TextInputAction.next, focusNode: titleFocusNode, decoration: InputDecoration( hintText: "Name", border: InputBorder.none ), ), ), SizedBox(height: 16), HtmlEditor( controller: controller, htmlEditorOptions: HtmlEditorOptions( shouldEnsureVisible: true, hint: "Description", ), otherOptions: OtherOptions( height: 450, ), ), SizedBox(height: 16), Padding( padding: EdgeInsets.only(left: 18, right: 18), child: TextField( controller: bioController, textInputAction: TextInputAction.next, focusNode: bioFocusNode, decoration: InputDecoration( hintText: "Bio", border: InputBorder.none ), ), ), Image.network("path_to_profile_picture"), IconButton( icon: Icon(Icons.edit, size: 35), tooltip: "Edit profile picture", onPressed: () async { //open gallery and make api call to update profile picture } ), //etc... just a basic form. ], ), ), ), ); } }
filePath的 HTML 示例HTML 示例
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="description" content="Flutter Summernote HTML Editor"> <meta name="author" content="tneotia"> <title>Summernote Text Editor HTML</title> <script src="assets/packages/html_editor_enhanced/assets/jquery.min.js"></script> <link href="assets/packages/html_editor_enhanced/assets/summernote-lite.min.css" rel="stylesheet"> <script src="assets/packages/html_editor_enhanced/assets/summernote-lite.min.js"></script> <!--darkCSS--> </head> <body> <div id="summernote-2"></div> <!--headString--> <!--summernoteScripts--> <style> body { display: block; margin: 0px; } .note-editor.note-airframe, .note-editor.note-frame { border: 0px solid #a9a9a9; } .note-frame { border-radius: 0px; } </style> </body> </html>
webInitialScripts示例查看代码
String result = ''; final HtmlEditorController controller = HtmlEditorController(); final FocusNode node = FocusNode(); @override Widget build(BuildContext context) { return GestureDetector( onTap: () { if (!kIsWeb) { controller.clearFocus(); } }, child: Scaffold( appBar: AppBar( title: Text(widget.title), elevation: 0, ), body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ HtmlEditor( controller: controller, htmlEditorOptions: HtmlEditorOptions( darkMode: false, webInitialScripts: UnmodifiableListView([ WebScript(name: "editorBG", script: "document.getElementsByClassName('note-editable')[0].style.backgroundColor='blue';"), WebScript(name: "height", script: """ var height = document.body.scrollHeight; window.parent.postMessage(JSON.stringify({"type": "toDart: height", "height": height}), "*"); """), ]) ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ TextButton( style: TextButton.styleFrom( backgroundColor: Colors.blueGrey), onPressed: () { controller.evaluateJavascriptWeb("editorBG"); }, child: Text('Change Background', style: TextStyle(color: Colors.white)), ), SizedBox( width: 16, ), TextButton( style: TextButton.styleFrom( backgroundColor: Colors.blueGrey), onPressed: () async { var result = await controller.evaluateJavascriptWeb("height", hasReturnValue: true); print(result); // prints "{type: toDart: height, height: 561}" }, child: Text('Get Height', style: TextStyle(color: Colors.white)), ), ] ), ), ], ), ), ), ); }
注意事项
由于此包依赖于 webview 来渲染 HTML 编辑器,因此编辑器在行为上会存在一些普遍的怪异之处。不幸的是,这些问题我无法解决,它们是 webview 在 Flutter 上运行方式固有的问题。
如果您发现任何问题,请在“问题”选项卡中报告,我将查看是否可能修复,但如果我关闭问题,很可能是由于上述事实。
-
在浅色和深色模式之间切换时,HTML 编辑器需要重新加载才能切换到正确的配色方案。您可以在 Flutter Mobile 中通过编程实现:
,或者在 Flutter Web 中实现:.editorController.reload() 。这将重置编辑器!如果您想保持状态,可以保存当前文本,重新加载,然后设置文本。.reloadWeb() -
如果您正在进行跨平台实现并且正在使用
editorControllergetter 或reloadWeb()方法,请在您的应用程序中使用kIsWeb以确保您在正确的平台上调用这些方法。
常见问题
查看已回答的问题
-
如何显示编辑器的最终输出? – 提供了原始 HTML 输出和渲染 HTML 输出的示例 – https://github.com/tneotia/html-editor-enhanced/issues/2
-
如何将编辑器默认设置为“全屏”? – https://github.com/tneotia/html-editor-enhanced/issues/4
-
使用 iOS 模拟器时,编辑器不接受任何按键输入。如何解决这个问题? – https://github.com/tneotia/html-editor-enhanced/issues/7
-
点击编辑器时,光标会出现在提示文本的第二行。有没有解决办法? – https://github.com/tneotia/html-editor-enhanced/issues/24
-
如何在移动设备上为编辑器框设置自定义背景颜色? – https://github.com/tneotia/html-editor-enhanced/issues/27
-
我的 Web 应用程序左上角有一个文件上传按钮。如何删除它? – https://github.com/tneotia/html-editor-enhanced/issues/28
-
我无法点击 Web 文本编辑器上方的抽屉项目。如何解决这个问题? – https://github.com/tneotia/html-editor-enhanced/issues/30
-
如何移除编辑器底部的“拖动条”? – https://github.com/tneotia/html-editor-enhanced/issues/42
-
如何检测图片是否已从编辑器中删除? – https://github.com/tneotia/html-editor-enhanced/issues/43
-
如何处理编辑器焦点? – https://github.com/tneotia/html-editor-enhanced/issues/47
-
如何为编辑器内容设置默认文本方向? – https://github.com/tneotia/html-editor-enhanced/issues/49
-
如何在初始文本中处理图像的相对 URL? – https://github.com/tneotia/html-editor-enhanced/issues/50
-
如何在 Web 上为编辑器框设置自定义背景颜色? – https://github.com/tneotia/html-editor-enhanced/issues/57
-
如何为自定义字体创建工具栏下拉菜单? – https://github.com/tneotia/html-editor-enhanced/issues/59
-
如何翻译对话框文本等内容? – https://github.com/tneotia/html-editor-enhanced/issues/69
-
如何禁用工具栏中的复制/粘贴按钮? – https://github.com/tneotia/html-editor-enhanced/issues/71
-
如何从编辑器 HTML 中提取图像标签? – https://github.com/tneotia/html-editor-enhanced/issues/72
-
如何在编辑器中使用 LaTeX 或数学公式? – https://github.com/tneotia/html-editor-enhanced/issues/74
-
如何创建工具栏外部的自定义按钮来加粗文本? – https://github.com/tneotia/html-editor-enhanced/issues/81
-
如何以不同方式设置
元素的样式? – https://github.com/tneotia/html-editor-enhanced/issues/83 -
如何将图片宽度默认设置为 100%? – https://github.com/tneotia/html-editor-enhanced/issues/86
-
如何覆盖移动设备上的链接打开方式? – https://github.com/tneotia/html-editor-enhanced/issues/88
-
如何在编辑器中设置初始字体系列? – https://github.com/tneotia/html-editor-enhanced/issues/125
-
如何只更改工具栏的背景颜色? – https://github.com/tneotia/html-editor-enhanced/issues/94
-
如何直接从图库中选择图片而无需显示对话框? – https://github.com/tneotia/html-editor-enhanced/issues/97
许可证
本项目采用 MIT 许可证 – 有关详细信息,请参阅 LICENSE 文件。
贡献指南
即将推出!
同时,欢迎随时提交 PR
原始 html_editor 由 xrb21 提供 – 仓库链接。感谢他提供原始构思和原始基础代码。此库是其仓库的一个分支。
GitHub



