Flutter HTML编辑器 - 增强版

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

设置

html_editor_enhanced: ^2.1.1 添加到您的 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 以获取潜在修复程序的更新,在此期间,上述解决方案应能解决大部分闪烁问题。

关于工具栏按钮的重要提示

强烈建议 选择要显示的按钮 - 实际上所有按钮默认都显示,这可能会让用户感到不知所措。您可以这样做:

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",
        ),   
        htmlToolbarOptions: HtmlToolbarOptions(
          defaultToolbarButtons: [
            //add constructors here and set buttons to false, e.g.
            ParagraphButtons(lineHeight: false, caseConverter: false)
          ]
        ),   
        otherOptions: OtherOptions(
          height: 400,
        ),
    );
}

请注意:您不能仅添加要删除按钮的按钮组的构造函数。如果您这样做,则插件将仅显示该特定按钮组中的按钮。您必须包含所有其他构造函数,并可以将其留空:例如 [ListButtons(), ParagraphButtons()] 等。

更多详情请参见 下方

当您想从编辑器中获取文本时

final txt = await controller.getText();

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,仅在移动设备上有效。有关更多详细信息,请参阅下文
黑暗模式 布尔值 设置暗模式的状态 - false:始终浅色,null:跟随系统,true:始终暗色
文件路径 字符串 允许您指定自己的 HTML 加载到 Webview 中。您可以创建一个带有 Summernote 的自定义页面,或者理论上加载任何其他编辑器/HTML。
提示 字符串 占位符提示文本
初始文本 字符串 文本编辑器的初始文本内容
移动设备上下文菜单 上下文菜单 自定义用户选择编辑器中文本时的上下文菜单。有关 ContextMenu 的文档,请参见 此处
移动长按持续时间 持续时间 持续时间(毫秒: 500) 设置识别长按的持续时间
移动端初始脚本 UnmodifiableListView<UserScript> 轻松注入脚本以执行诸如更改编辑器背景颜色之类的操作。有关 UserScript 的文档,请参见 此处
应确保可见 布尔值 当 Webview 获得焦点时,滚动父级 Scrollable 到编辑器小部件的顶部。如果 HtmlEditor 不在 Scrollable 中,请不要使用此参数。有关更多详细信息,请参见 下方

参数 - HtmlToolbarOptions

工具栏选项

参数 类型 默认值 描述
音频扩展 List<String> 插入音频文件时允许的扩展名
自定义工具栏按钮 List<Widget> 向工具栏添加自定义按钮
自定义工具栏插入索引 List<int> 允许您设置每个自定义工具栏按钮应插入到工具栏小部件列表中的位置
默认工具栏按钮 List<Toolbar> (所有构造函数都处于活动状态) 允许您隐藏/显示某些按钮或某些按钮组
其他文件扩展名 List<String> 插入图像/音频/视频以外的文件时允许的扩展名
图片扩展名 List<String> 插入图像时允许的扩展名
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
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 不适用 当鼠标/手指松开时调用
onPaste 不适用 当内容粘贴到编辑器中时调用
onScroll 不适用 当编辑器框滚动时调用

获取器

目前,该包有一个 getter:<controller name>.editorController。它返回 InAppWebViewController,该控制器管理显示编辑器的 webview。

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

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

工具栏

此 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 @提及 -
    当在编辑器中输入“@”字符时,显示可用提及的下拉列表。实现需要您传递可用提及列表,并且您还可以提供一个函数,在提及插入编辑器时调用。

  5. Summernote 文件 -
    支持 base64 格式的图片文件(jpg、png、gif、wvg、webp)、音频文件(mp3、ogg、oga)和视频文件(mp4、ogv、webm)。通过 InsertButtons 中的图片/音频/视频/其他文件按钮支持。

此列表并非最终列表,更多内容可能会添加。如果您希望看到特定插件的支持,请提交功能请求!

除 Summernote @提及外的所有插件均默认激活。可以通过修改工具栏项来禁用它们,有关详情请参见 上方

激活 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 中部分参数的详细说明。对于此处未提及的参数,请参见 上方 的参数表以获取简短说明。如果您有其他疑问,请提交问题。

自动调整高度

默认值: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 设置 idsummernote-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 会滚动到视图中一样。

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

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

对于 linkInsertInterceptormediaLinkInsertInterceptormediaUploadInterceptoronOtherFileLinkInsert,您必须返回一个 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 替换为 <br/>。这仅推荐在插入纯文本作为初始值时使用。在典型的 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>

<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>

GitHub

https://github.com/tneotia/html-editor-enhanced