Flutter HTML 编辑器 – 增强版

pub package

Flutter HTML Editor Enhanced 是一个适用于 Android、iOS 和 Web 的文本编辑器,通过 Summernote JavaScript 包装器帮助编写所见即所得的 HTML 代码。

请注意,此 README.md 文件中显示的 API 仅是文档的一部分,并且仅符合 GitHub master 分支!因此,这里可能会有尚未发布/发布的其他方法、选项和事件!如果您需要特定版本,请将此存储库的 GitHub 分支更改为您需要的版本,或使用在线 API 参考(推荐)。

视频示例 浅色模式和

ToolbarType.nativeGrid
深色模式和

ToolbarPosition.belowEditor
GIF example Light Dark
Flutter Web
Flutter Web

目录

这个包在哪些方面得到了“增强”?

  1. 它官方支持 Flutter Web,几乎所有移动端功能都得到了支持。像 Ctrl+B 加粗这样的键盘快捷键也同样有效!

  2. 它拥有完全原生的 Flutter 小部件控件

  3. 它使用高度优化的 WebView,为使用编辑器提供最佳体验

  4. 它不使用本地服务器来加载包含编辑器的 HTML 代码。相反,这个包直接加载 HTML 文件,这提高了性能和编辑器的启动时间。

  5. 它使用基于控制器的 API。您无需再通过 `GlobalKey` 来访问方法,只需随时随地调用 `.` 即可。

  6. 它支持 Summernote 的许多方法

  7. 它支持 Summernote 的所有回调

  8. 它暴露了 `InAppWebViewController`,以便您可以根据需要自定义 WebView – 您甚至可以加载自己的 HTML 代码并注入自己的 JavaScript 以满足您的用例。

  9. 它支持暗模式

  10. 它支持极其精细化的工具栏自定义

更多功能正在开发中!如果您希望添加其他功能,请提交功能请求或为项目做出贡献。

设置

将 `html_editor_enhanced: ^2.5.0` 作为依赖添加到您的 pubspec.yaml 文件中。

请确保在 `AndroidManifest.xml` 中声明互联网支持:``

在 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` 之上时,会出现相当多的闪烁和重绘。有关更多详细信息,请参见 flutter/flutter#71888

当前的解决方案是使用 flutter run --web-renderer htmlflutter build web --web-renderer html 构建和/或运行您的 Web 应用。

请关注 flutter/flutter#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,仅在移动设备上有效。有关更多详细信息,请参阅下文
characterLimit 整数 设置编辑器的字符限制。达到限制时,用户将无法再输入。
customOptions 字符串 使用 Summernote 语法提供 Summernote 初始化自定义选项(参见 此处
黑暗模式 布尔值 设置暗模式状态 – `false`:始终浅色,`null`:跟随系统,`true`:始终深色
文件路径 字符串 允许您指定自己的 HTML 加载到 Webview 中。您可以创建一个带有 Summernote 的自定义页面,或者理论上加载任何其他编辑器/HTML。
提示 字符串 占位符提示文本
初始文本 字符串 文本编辑器的初始文本内容
输入类型 HtmlInputType HtmlInputType.text 允许您设置编辑器在移动设备上虚拟键盘的显示方式
移动设备上下文菜单 上下文菜单 自定义用户在编辑器中选择文本时的上下文菜单。有关 `ContextMenu` 的文档,请参见 此处
移动长按持续时间 持续时间 持续时间(毫秒: 500) 设置识别长按的持续时间
移动端初始脚本 UnmodifiableListView<UserScript> 轻松注入脚本以执行诸如更改编辑器背景颜色之类的操作。有关 `UserScript` 的文档,请参见 此处
webInitialScripts UnmodifiableListView<WebScript> 轻松注入脚本以执行诸如更改编辑器背景颜色之类的操作。有关更多详细信息,请参见 下方
应确保可见 布尔值 当 webview 获得焦点时,将父级 `Scrollable` 滚动到编辑器小部件的顶部。如果 `HtmlEditor` 不在 `Scrollable` 中,请 *不要* 使用此参数。有关更多详细信息,请参见 下方
spellCheck 布尔值 指定编辑器是否使用拼写检查并下划线错误拼写。

参数 – `HtmlToolbarOptions`

工具栏选项

参数 类型 默认值 描述
音频扩展 List<String> 插入音频文件时允许的扩展名
自定义工具栏按钮 List<Widget> 向工具栏添加自定义按钮
自定义工具栏插入索引 List<int> 允许您设置每个自定义工具栏按钮应插入到工具栏小部件列表中的位置
默认工具栏按钮 List<Toolbar> (所有构造函数都处于活动状态) 允许您隐藏/显示某些按钮或某些按钮组
其他文件扩展名 List<String> 插入图像/音频/视频以外的文件时允许的扩展名
图片扩展名 List<String> 插入图像时允许的扩展名
initiallyExpanded 布尔值 设置在使用 `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 移动端请 *不要* 使用此方法。
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() 不适用 不适用 在代码视图和富文本视图之间切换
撤消() 不适用 不适用 撤消编辑器中的上一个命令

Callbacks

每个回调都定义为 `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 当通过上传插入图片失败时调用,传递 `FileUpload`(可能包含文件名、修改日期、大小和 MIME 类型,或者为 null)、`String`(base64 或 null)以及 `UploadError`(描述错误类型)。
onInit 不适用 当富文本字段初始化并可以调用 JavaScript 方法时调用
onKeyDown 整数 按下键时调用,传递按下键的键码
onKeyUp 整数 松开键时调用,传递松开键的键码
onMouseDown 不适用 当鼠标/手指按下时调用
onMouseUp 不适用 当鼠标/手指松开时调用
onNavigationRequestMobile 字符串 仅在移动端,当 webview 的 URL 即将更改时调用。
onPaste 不适用 当内容粘贴到编辑器中时调用
onScroll 不适用 当编辑器框滚动时调用

获取器

  1. `<controller name>.editorController`。这会返回 `InAppWebViewController`,它管理显示编辑器的 webview。

这非常强大,因为它允许您直接在应用程序中创建自己的自定义方法和实现。有关控制器的文档,请参见 `flutter_inappwebview`

此获取器**不应**在 Flutter Web 中使用。如果您正在进行跨平台实现,请使用 kIsWeb 在您的代码中检查当前平台。

  1. `<controller name>.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 插件。

目前支持以下插件

  1. Summernote Case Converter – 将选定文本转换为全小写、全大写、句子大小写或标题大小写。在 `ParagraphButtons` 的工具栏中通过下拉菜单支持。

  2. Summernote List Styles – 自定义 ul 和 ol 列表样式。在 `ListButtons` 的工具栏中通过下拉菜单支持。

  3. Summernote RTL – 在 LTR 和 RTL 格式之间切换当前选定的文本。在 `ParagraphButtons` 的工具栏中通过两个按钮支持。

  4. Summernote At Mention – 当在编辑器中键入“@”字符时,显示可用提及的下拉列表。实现需要您传递可用提及的列表,并且您还可以提供一个函数,在提及插入到编辑器中时调用。

  5. Summernote File – 支持 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` 中部分参数的详细说明。对于此处未提及的参数,请参见 上方 的参数表以获取简短说明。如果您有进一步的问题,请提交一个 issue。

自动调整高度

默认值:true

此选项参数通过获取 JS document.body.scrollHeight 返回的值和工具栏 GlobalKey (toolbarKey.currentContext?.size?.height) 来自动设置编辑器的高度。

这很有用,因为工具栏可能根据小部件的配置、屏幕尺寸、方向等有 1 到 5 行。在 `build()` 执行后,没有可靠的方法可以确定工具栏的大小,因此简单地硬编码 webview 的高度可能会导致底部出现空白区域或 webview 可滚动。通过使用 JS 和工具栏小部件上的 `GlobalKey`,编辑器可以获得确切的高度并更新小部件以反映该高度。

有一个缺点:webview 的大小会在页面加载后明显移动。根据变化的幅度,这可能会令人感到刺眼。有时,webview 需要一秒钟才能调整到新尺寸,您可能会在 webview 容器调整大小后一两秒钟看到编辑器页面跳动。

如果这无助于您的用例,请随时禁用它,但推荐值为 true

为键盘调整高度

默认值:true,仅在移动设备上考虑

此选项参数在键盘活动并与编辑器重叠时更改编辑器的高度。

这很有用,因为目前在 Flutter 中,键盘激活时 webview 不会调整其视图。这意味着,如果您的编辑器占满页面高度,如果用户输入长文本,他们可能无法看到他们正在输入的内容,因为它被键盘遮挡了。

当此参数启用时,webview 将调整到完美高度,以确保所有输入内容都可见,并且一旦键盘隐藏,编辑器将恢复到其原始高度。

webview 在键盘弹出/消失后需要一点时间来移动,但延迟并不算太坏。强烈建议将 webview 放置在 `Scrollable` 中并启用 `shouldEnsureVisible`,如果页面上有其他小部件 – 如果编辑器位于页面的下半部分,它将被滚动到顶部,然后相应地调整高度,而不是让插件尝试为被键盘完全遮挡的 webview 设置高度。

请参阅下文的示例用例。

如果这无助于您的用例,请随时禁用它,但推荐值为 true

文件路径

此选项参数允许您通过提供资产中自定义 HTML 文件的文件路径,完全自定义加载到 Webview 中的 HTML。

对于 Web 端,在提供文件路径时需要/建议使用特定的格式,因为 Web 实现会将 HTML 加载为 `String` 并直接使用 `replaceAll().` 进行修改,而不是使用 `evaluateJavascript()` 之类的方法 – 因为 Web 上不存在该方法。

在 Web 上,您应该包含以下内容

  1. `<!--darkCSS-->` 在 `<head>` 中 – 这启用了暗模式支持

  2. `<!--headString-->` 在 `<body>` 中,位于您的 summernote `<div>` 之下 – 这允许加载任何已启用插件的 JS 和 CSS 文件

  3. `<!--summernoteScripts-->` 在 `<body>` 中,位于您的 summernote `<div>` 之下 – 必需 – 这允许 Dart 和 JS 相互通信。如果您不包含此项,则方法/回调将不起作用。

注意事项

  1. 请勿在您的自定义 HTML 文件中初始化 Summernote 编辑器!软件包会处理此事。

  2. 确保将 Summernote 的 `id` 设置为 `summernote-2`!– `<div id="summernote-2"></div>`。

  3. 请确保在您的文件中包含 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 文件以获取实际示例。

应确保可见

默认值:false

当 Webview 获得焦点或文本输入到编辑器中时,此选项参数会将编辑器容器滚动到视图中。

您只能在 HtmlEditor 位于 Scrollview 内部时使用此参数,否则它不起作用。

这在页面是 `SingleChildScrollView` 或包含多个小部件(如表单)的类似容器时很有用。当用户在不同字段之间导航时,它会将 webview 滚动到视图中,就像输入文本时的 `TextField` 会滚动到视图中一样。

请参阅下文的示例,其中有一个很好的使用方式。

webInitialScripts

此参数允许您为 Web 上的编辑器指定自定义 JavaScript。可以随时使用 `controller.evaluateJavascriptWeb` 调用它们。

您必须使用 `WebScript` 类添加这些脚本,该类接受 `name` 和 `script` 参数。`name` *必须* 是一个唯一的标识符,否则您期望的脚本可能不会执行。将您的 JavaScript 代码传递给 `script` 参数。

该包支持从 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` 中部分参数的详细说明。对于此处未提及的参数,请参见 上方 的参数表以获取简短说明。如果您有进一步的问题,请提交一个 issue。

customToolbarButtonscustomToolbarButtonsInsertionIndices

这两个参数允许您插入自定义按钮并设置它们插入到工具栏小部件列表中的位置。

这看起来像这样

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` 之前!每个默认按钮组可能包含几个不同的子组。

按钮组 子组数量
StyleButtons 1
FontSettingButtons 3
FontButtons 2
颜色按钮 1
ListButtons 2
ParagraphButtons 5
InsertButtons 1
OtherButtons 2

如果您的某些按钮被禁用,子组的数量可能会减少。插入索引取决于这些子组而不是整个按钮组。计算插入索引的一个简单方法是构建应用程序,并计算您想要插入按钮的位置之前每个按钮组/下拉菜单之间的分隔符空格数。

因此,考虑到这一点,Button1 将插入到 FontSettingButtons 的前两个子组之间,而 Button2 将插入到 FontButtons 的两个子组之间。

在为小部件创建 onPressed/onTap/onChanged 方法时,您可以使用 controller.execCommand 或控制器上的任何其他方法在编辑器中执行操作。

注意事项

  1. 在 Web 上使用 controller.editorController. 将不起作用!

  2. 如果您没有提供 `customToolbarButtonsInsertionIndices`,插件将把您的按钮插入到默认工具栏列表的末尾。

  3. 如果您提供 customToolbarButtonsInsertionIndices,它的长度**必须**与您的 customToolbarButtons 小部件列表相同。

linkInsertInterceptormediaLinkInsertInterceptorotherFileLinkInsertmediaUploadInterceptoronOtherFileUpload

这些回调帮助您拦截任何插入到编辑器中的链接或文件。

参数 类型 描述
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` 在使用“其他文件”按钮时是必需的。此按钮默认不激活,因此如果您激活它,则必须提供这些函数,否则当用户插入图像/音频/视频以外的文件时,将不会发生任何事情。

请参阅下文的示例。

onButtonPressedonDropdownChanged

这些回调帮助您拦截任何按钮按下或下拉菜单更改。

参数 类型 描述
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 参数

processInputHtmlprocessOutputHtmlprocessNewLineAsBr

默认值:分别为 true、true、false

`processInputHtml` 会将 `"` 替换为 `\"`,将 `'` 替换为 `\'`,并将 `\r`、`\r\n`、`\n` 和 `\n\n` 替换为空字符串。这是为了防止在编辑器中插入 HTML 时出现语法异常,因为引号和其他特殊字符不会被转义。如果您已经对 HTML 输入中的所有相关字符进行了清理和转义,则建议将此参数设置为 `false`。您可能还想在 Web 端将此参数设置为 `false`,因为在测试中,这些字符似乎默认得到了正确处理,但您的 HTML 可能并非如此。

processOutputHtml 将输出 HTML 替换为 "",如果

  1. 它是空的

  2. 它是

  3. 它是


  4. 它是


这些参数可能看起来有点随机,但它们是 Summernote 编辑器将拥有的三个可能的默认/初始 HTML 代码。如果您仍然想接收这些输出,请将参数设置为 `false`。

`processNewLineAsBr` 会将 `\n` 和 `\n\n` 替换为 `<br/>`。这仅在将纯文本作为初始值插入时推荐。在典型的 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;
      },
    ),
  );

linkInsertInterceptoronOtherFileLinkInsertonOtherFileUpload 可以以非常相似的方式实现,只是它们在函数中不使用 InsertFileType 枚举。

onOtherFileLinkInsertonOtherFileUpload 也无需返回 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 上运行方式的固有问题。

如果您确实发现了任何问题,请在 Issues 选项卡中报告,我会看看是否可以修复,但如果我关闭了 issue,很可能是由于上述原因。

  1. 在暗模式和浅色模式之间切换时,需要重新加载才能使 HTML 编辑器切换到正确的颜色方案。您可以在 Flutter 移动端以编程方式实现此功能:`.editorController.reload()`,或在 Flutter Web 中:`.reloadWeb()`。这将重置编辑器!如果您想保持状态,可以先保存当前文本,然后重新加载,再设置文本。

  2. 如果您正在进行跨平台实现,并且正在使用 `editorController` getter 或 `reloadWeb()` 方法,请在您的应用程序中使用 `kIsWeb` 来确保您在正确的平台上调用这些方法。

常见问题

查看已回答的问题

许可证

本项目根据 MIT 许可证授权 – 有关详细信息,请参见 LICENSE 文件。

贡献指南

即将推出!

同时,欢迎随时提交 PR

原始 `html_editor` 由 xrb21 创建 – 仓库链接。原始创意和基础代码归功于他。本库是他的仓库的 fork。

GitHub

查看 Github