支持 Flutter 平台的富文本编辑器

[![MIT License][license-badge]][license-link] [![PRs Welcome][prs-badge]][prs-link] [![Watch on GitHub][github-watch-badge]][github-watch-link] [![Star on GitHub][github-star-badge]][github-star-link] [![Watch on GitHub][github-forks-badge]][github-forks-link] [license-badge]: https://img.shields.io/github/license/singerdmx/flutter-quill.svg?style=for-the-badge [license-link]: https://github.com/singerdmx/flutter-quill/blob/master/LICENSE [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge [prs-link]: https://github.com/singerdmx/flutter-quill/issues [github-watch-badge]: https://img.shields.io/github/watchers/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff [github-watch-link]: https://github.com/singerdmx/flutter-quill/watchers [github-star-badge]: https://img.shields.io/github/stars/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff [github-star-link]: https://github.com/singerdmx/flutter-quill/stargazers [github-forks-badge]: https://img.shields.io/github/forks/singerdmx/flutter-quill.svg?style=for-the-badge&logo=github&logoColor=ffffff [github-forks-link]: https://github.com/singerdmx/flutter-quill/network/members FlutterQuill 是一个富文本编辑器,同样也是 [Quill] 在 [Flutter] 的版本。 该库是为移动平台构建的 “ 所见即所得 ” 的富文本编辑器,同时我们还正在对 Web 平台进行兼容。查看我们的 [Youtube 播放列表] 或 [代码介绍] 以了解代码的详细内容。你可以加入我们的 [Slack Group] 来进行讨论。 Demo App: https://bulletjournal.us/home/index.html Pub: https://pub.dev/packages/flutter_quill ## 用法 查看 `示例` 目录来学习 FlutterQuill 最简单的使用方法,你通常只需要实例化一个控制器: ``` QuillController _controller = QuillController.basic(); ``` 然后在你的 App 中嵌入工具栏和编辑器,例如: ```dart Column( children: [ QuillToolbar.basic(controller: _controller), Expanded( child: Container( child: QuillEditor.basic( controller: _controller, readOnly: false, // true for view only mode ), ), ) ], ) ``` 查看 [示例页面] 以了解高级用户。 ## 输入 / 输出 该库使用 [Quill] 作为内部数据格式。 * 使用 `_controller.document.toDelta()` 获取增量。 * 使用 `_controller.document.toPlainText()` 获取纯文本。 FlutterQuill 提供了一些 JSON 序列化支持,以便您可以保存和打开文档。 要将文档保存为 JSON 类型,请执行以下操作: ``` var json = jsonEncode(_controller.document.toDelta().toJson()); ``` 然后你就可以将其存储。 想要 FlutterQuill 编辑器使用你之前存储的 JSON 数据,请执行以下操作: ``` var myJSON = jsonDecode(incomingJSONText); _controller = QuillController( document: Document.fromJson(myJSON), selection: TextSelection.collapsed(offset: 0)); ``` ## Web 对于 web 开发,请执行 `flutter config --enable-web` 来获取对 flutter 的支持或使用 [ReactQuill] 获取对 React 的支持。 进行 Web 开发需要提供 `EmbedBuilder`, 参考:[defaultEmbedBuilderWeb](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/universal_ui/universal_ui.dart#L29). 进行 Web 开发还需要提供 `webImagePickImpl`, 参考: [示例页面](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L225). ## Desktop 在桌面端进行工具栏按钮开发,需要提供 `filePickImpl`。参考: [示例页面](https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart#L205). ## 配置 `QuillToolbar` 类允许您自定义可用的格式选项。[示例页面] 提供了高级使用和配置的示例代码。 ### 字号 在编辑器工具栏中,提供了具有字号功能的下拉菜单。 这可以通过 `showFontSize` 启用或禁用。 启用后,可以通过*可选的* `fontSizeValues` 属性修改默认字号。 `fontSizeValues` 接受一个 `Map`,其中包含一个 `String` 类型的标题和一个 `String` 类型的字号。 例子: ``` fontSizeValues: const {'Small': '8', 'Medium': '24.5', 'Large': '46'} ``` 字体大小可以使用 `0` 值清除,例如: ``` fontSizeValues: const {'Small': '8', 'Medium': '24.5', 'Large': '46', 'Clear': '0'} ``` ### 字体 想要使用你自己的字体,请更新你的 [assets folder](https://github.com/singerdmx/flutter-quill/tree/master/example/assets/fonts) 并且传入 `fontFamilyValues`。详情内容请查看 [this change](https://github.com/singerdmx/flutter-quill/commit/71d06f6b7be1b7b6dba2ea48e09fed0d7ff8bbaa), [this article](https://stackoverflow.com/questions/55075834/fontfamily-property-not-working-properly-in-flutter) 和 [this](https://www.flutterbeads.com/change-font-family-flutter/)。 ### 自定义按钮 您可以通过 `customButtons` 可选参数将自定义按钮添加到工具栏的*末尾*,该参数接收的了行是 `QuillCustomButton` 的 `List`。 要添加一个 Icon,我们应该实例化一个新的新的 `QuillCustomButton` ``` QuillCustomButton( icon:Icons.ac_unit, onTap: () { debugPrint('snowflake'); } ), ``` 每个 `QuillCustomButton` 都是 `customButtons` 可选参数的一部分,如下所示: ``` QuillToolbar.basic( (...), customButtons: [ QuillCustomButton( icon:Icons.ac_unit, onTap: () { debugPrint('snowflake1'); } ), QuillCustomButton( icon:Icons.ac_unit, onTap: () { debugPrint('snowflake2'); } ), QuillCustomButton( icon:Icons.ac_unit, onTap: () { debugPrint('snowflake3'); } ), ] ``` ### 移动端上自定义图片尺寸 定义`mobileWidth`、`mobileHeight`、`mobileMargin`、`mobileAlignment`如下: ``` { "insert": { "image": "https://user-images.githubusercontent.com/122956/72955931-ccc07900-3d52-11ea-89b1-d468a6e2aa2b.png" }, "attributes":{ "style":"mobileWidth: 50; mobileHeight: 50; mobileMargin: 10; mobileAlignment: topLeft" } } ``` ### 自定义嵌入块 有时您想在文本中添加一些自定义内容或者是自定义小部件。 比如向文本添加注释,或者在文本编辑器中添加的任何自定义内容。 您唯一需要做的就是添加一个 `CustomBlockEmbed` 并将其映射到 `customElementsEmbedBuilder` 中,以将自定义块内的数据转换为一个 widget! 例子: 从 `CustomBlockEmbed` 开始,我们在这里扩展它并添加对 'Note' widget 的方法,这就是 `Document`,`flutter_quill` 使用它来呈现富文本。 ```dart class NotesBlockEmbed extends CustomBlockEmbed { const NotesBlockEmbed(String value) : super(noteType, value); static const String noteType = 'notes'; static NotesBlockEmbed fromDocument(Document document) => NotesBlockEmbed(jsonEncode(document.toDelta().toJson())); Document get document => Document.fromJson(jsonDecode(data)); } ``` 然后,我们需要将这个 “notes” 类型映射到 widget 中。在例子中,我使用 `ListTile` 来显示它,使用 `onTap` 方法俩编辑内容,另外不要忘记将此方法添加到 `QuillEditor` 中。 ```dart Widget customElementsEmbedBuilder( BuildContext context, QuillController controller, CustomBlockEmbed block, bool readOnly, void Function(GlobalKey videoContainerKey)? onVideoInit, ) { switch (block.type) { case 'notes': final notes = NotesBlockEmbed(block.data).document; return Material( color: Colors.transparent, child: ListTile( title: Text( notes.toPlainText().replaceAll('\n', ' '), maxLines: 3, overflow: TextOverflow.ellipsis, ), leading: const Icon(Icons.notes), onTap: () => _addEditNote(context, document: notes), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), side: const BorderSide(color: Colors.grey), ), ), ); default: return const SizedBox(); } } ``` 然后,编写一个方法来添加/编辑内容,`showDialog` 方法显示 Quill 编辑器以编辑内容,用户编辑完成后,需要检查文档是否有内容,如果有内容,在 `CustomBlockEmbed` 中添加/编辑 `NotesBlockEmbed`(注意,如果没有在 `NotesBlockEmbed` 中传递 `CustomBlockEmbed` ,编辑将不会生效)。 ```dart Future _addEditNote(BuildContext context, {Document? document}) async { final isEditing = document != null; final quillEditorController = QuillController( document: document ?? Document(), selection: const TextSelection.collapsed(offset: 0), ); await showDialog( context: context, builder: (context) => AlertDialog( titlePadding: const EdgeInsets.only(left: 16, top: 8), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('${isEditing ? 'Edit' : 'Add'} note'), IconButton( onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close), ) ], ), content: QuillEditor.basic( controller: quillEditorController, readOnly: false, ), ), ); if (quillEditorController.document.isEmpty()) return; final block = BlockEmbed.custom( NotesBlockEmbed.fromDocument(quillEditorController.document), ); final controller = _controller!; final index = controller.selection.baseOffset; final length = controller.selection.extentOffset - index; if (isEditing) { final offset = getEmbedNode(controller, controller.selection.start).item1; controller.replaceText( offset, 1, block, TextSelection.collapsed(offset: offset)); } else { controller.replaceText(index, length, block, null); } } ``` 这样我们就成功的在富文本编辑器中添加了一个自定义小组件。

1

> 更多信息和视频示例,请参阅 [PR of this feature](https://github.com/singerdmx/flutter-quill/pull/877) > 有关更多详细信息,请查看 [this YouTube video](https://youtu.be/pI5p5j7cfHc) ### 翻译 该库为 quill 工具栏和编辑器提供翻译,除非您设置自己的语言环境,否则它将遵循系统语言环境: ```dart QuillToolbar(locale: Locale('fr'), ...) QuillEditor(locale: Locale('fr'), ...) ``` 目前,可提供以下 22 种语言环境的翻译: * `Locale('en')` * `Locale('ar')` * `Locale('de')` * `Locale('da')` * `Locale('fr')` * `Locale('zh', 'CN')` * `Locale('zh', 'HK')` * `Locale('ko')` * `Locale('ru')` * `Locale('es')` * `Locale('tr')` * `Locale('uk')` * `Locale('ur')` * `Locale('pt')` * `Locale('pl')` * `Locale('vi')` * `Locale('id')` * `Locale('nl')` * `Locale('no')` * `Locale('fa')` * `Locale('hi')` * `Locale('sr')` #### 贡献翻译 翻译文件位于 [toolbar.i18n.dart](lib/src/translations/toolbar.i18n.dart)。 随意贡献您自己的翻译,只需复制英文翻译映射并将值替换为您的翻译。 然后打开一个拉取请求,这样每个人都可以从您的翻译中受益! ---

1 1

1 1

## 帮助 [Quill]: https://quilljs.com/docs/formats [Flutter]: https://github.com/flutter/flutter [FlutterQuill]: https://pub.dev/packages/flutter_quill [ReactQuill]: https://github.com/zenoamaro/react-quill [Youtube 播放列表]: https://youtube.com/playlist?list=PLbhaS_83B97vONkOAWGJrSXWX58et9zZ2 [Slack Group]: https://join.slack.com/t/bulletjournal1024/shared_invite/zt-fys7t9hi-ITVU5PGDen1rNRyCjdcQ2g [示例页面]: https://github.com/singerdmx/flutter-quill/blob/master/example/lib/pages/home_page.dart [代码介绍]: https://github.com/singerdmx/flutter-quill/blob/master/CodeIntroduction.md