Flutter Html Editor – 增强版

pub package

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

请注意,此 README.md 文件中显示的 API 仅为文档的一部分,并且仅符合 GitHub 主分支!因此,这里可能存在尚未发布/发布的方​​法、选项和事件!如果您需要特定版本,请将此存储库的 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.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 htmlflutter 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 不适用 当编辑器框滚动时调用

获取器

  1. <控制器名称>.editorController。这返回 InAppWebViewController,它管理显示编辑器的 webview。

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

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

  1. .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 大小写转换器
    将选定的文本转换为全部小写、全部大写、句首字母大写或标题大小写。通过工具栏中的 ParagraphButtons 下拉菜单支持。

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

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

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

  5. 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 上,您应该包含以下内容

  1. 位于 内部 – 这可启用深色模式支持

  2. 位于 内部,且在您的 summernote

    下方 – 这允许加载任何已启用插件的 JS 和 CSS 文件

  3. 位于 内部,且在您的 summernote

    下方 – **必需** – 这允许 Dart 和 JS 相互通信。如果您不包含此内容,则方法/回调将不起作用。

注意事项

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

  2. 确保将 Summernote 的 id 设置为 summernote-2! –

  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 文件以获取实际示例。

shouldEnsureVisible

默认值:false

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

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

这在页面是 SingleChildScrollView 或具有多个小部件(例如表单)的类似情况时很有用。当用户浏览不同的字段时,它会将 webview 弹出到视图中,就像 TextField 在输入文本时会滚动到视图中一样。

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

webInitialScripts

此参数允许您为 Web 上的编辑器指定自定义 JavaScript。可以使用 controller.evaluateJavascriptWeb 在任何时候调用这些脚本。

您必须使用 WebScript 类添加这些脚本,该类接受 namescript 参数。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 中选定参数的较长描述。对于此处未提及的参数,请参阅上方的参数表以获取简短描述。如果您有进一步的问题,请提交问题。

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 数据,执行文件验证等。

对于 linkInsertInterceptormediaLinkInsertInterceptormediaUploadInterceptor,您必须返回一个 bool 来告诉插件它应该做什么。当您返回 false 时,它假定您已处理用户请求并采取了行动。当您返回 true 时,插件将使用默认处理程序来处理用户请求。

使用“其他文件”按钮时,onOtherFileLinkInsertonOtherFileUpload 是必需的。此按钮默认不活动,因此如果将其激活,则必须提供这些函数,否则当用户插入图像/音频/视频以外的文件时,什么都不会发生。

请参阅下文的示例。

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.customToolbarWidget() 小部件来完全自定义工具栏的放置位置。可能性是无限的——您可以使用 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 替换为
。这仅建议在将纯文本作为初始值插入时使用。在典型的 HTML 中,任何换行符都将被忽略,因此此参数默认为 false

示例

请参阅示例应用,了解如何使用大多数方法和回调。您还可以试用参数以查看其功能。

本节稍后将随着此库的增长和更多功能的实现而更新,提供更专业和具体的示例。

linkInsertInterceptormediaLinkInsertInterceptorotherFileLinkInsertmediaUploadInterceptoronOtherFileUpload 的示例

示例代码

注意:此示例使用 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

onButtonPressedonDropdownChanged 示例

示例代码

  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 上运行方式固有的问题。

如果您发现任何问题,请在“问题”选项卡中报告,我将查看是否可能修复,但如果我关闭问题,很可能是由于上述事实。

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

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

常见问题

查看已回答的问题

许可证

本项目采用 MIT 许可证 – 有关详细信息,请参阅 LICENSE 文件。

贡献指南

即将推出!

同时,欢迎随时提交 PR

原始 html_editor 由 xrb21 提供 – 仓库链接。感谢他提供原始构思和原始基础代码。此库是其仓库的一个分支。

GitHub

查看 Github