FluentEditor

Fluent Editor

Flutter Version License Version Live Demo

A powerful and feature-rich rich word processor for Flutter applications, inspired by nature 🍃.

Features

  • Rich Text Editing: Support for bold, italic, underline, strikethrough, and more
  • Paragraph Styles: Headings, normal text, and paragraph formatting
  • Lists: Ordered and unordered lists with nested sublists
  • Tables: Create and edit tables with cell spanning
  • Images: Insert and resize images with inline and block positioning
  • Links: Insert and manage hyperlinks
  • Colors: Text color and highlight color support
  • Alignment: Left, center, right, and justify text alignment
  • Export: Export to DOCX, ODT, and PDF formats
  • Undo/Redo: Full undo/redo history with intelligent action grouping
  • Selection: Mouse and keyboard selection support
  • Word Count: Real-time word and character count
  • Clipboard: Cut, copy, and paste with formatting support

Tested On

  • Web: Chrome
  • Windows: Windows 11
  • Linux: Ubuntu, Debian, Fedora
  • macOS: macOS 13+
  • iOS: iOS 15+
  • Android: Android 12+

Plugins

  • fluent_editor_spellcheck — Hunspell-based spell-check plugin with isolate-backed checking, and multi-language support. (WIP)
  • fluent_editor_comments - Comments and annotations plugin. (WIP)
  • fluent_editor_review - Review plugin for track changes and comments. (WIP)

Getting Started

Add Fluent Editor to your pubspec.yaml:

dependencies:
  fluent_editor:
    git:
      url: https://github.com/exusr/fluent-editor.git

Usage

import 'package:fluent_editor/fluent_editor.dart';

class MyEditor extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FluentEditor(
      document: FluentDocument(),
      labels: FluentEditorLabels( //translate labels
        file: 'File',
        edit: 'Edit',
        insert: 'Insert',
        format: 'Format',
      ),
    );
  }
}

Creating a Document

final document = FluentDocument();

// Add a paragraph
final paragraph = Paragraph()
  ..fragments = [Fragment('Hello, World!')];
document.content.nodes.add(paragraph);

// Add a heading
final heading = Paragraph()
  ..styleName = 'heading1'
  ..fragments = [Fragment('Title')];
document.content.nodes.add(heading);

// Add a list
final listItem1 = ListItem(bulletType: 'ordered', indexList: [1])
  ..children = [Paragraph()..fragments = [Fragment('First item')]];
final listItem2 = ListItem(bulletType: 'ordered', indexList: [2])
  ..children = [Paragraph()..fragments = [Fragment('Second item')]];
final list = FluentList(listType: 'ordered')
  ..items = [listItem1, listItem2];
document.content.nodes.add(list);

// Add an image
final image = FluentImage(src: 'https://example.com/image.png')
  ..width = 300
  ..height = 200;
document.content.nodes.add(image);

// Add a table
final cell1 = FluentCell()..fragments = [Fragment('Cell 1')];
final cell2 = FluentCell()..fragments = [Fragment('Cell 2')];
final row = FluentRow()..cells = [cell1, cell2];
final table = FluentTable()..rows = [row];
document.content.nodes.add(table);

// Add a link
final link = Link(url: 'https://example.com')
  ..fragments = [Fragment('Click here')];
final linkParagraph = Paragraph()
  ..fragments = [Fragment('Visit '), link, Fragment(' for more info')];
document.content.nodes.add(linkParagraph);

// Add a horizontal line
final hr = HorizontalRule();
document.content.nodes.add(hr);

Working with Selection

// Get the current cursor position
final cursor = document.cursor;
final fragmentId = cursor.anchorId;
final offset = cursor.anchorOffset;

// Check if there's a selection
if (cursor.isCollapsed) {
  // Cursor is collapsed (no selection)
  print('Cursor at $fragmentId:$offset');
} else {
  // There's a selection
  print('Selection from ${cursor.anchorId}:${cursor.anchorOffset} to ${cursor.focusId}:${cursor.focusOffset}');
}

// Move the cursor (collapses selection)
cursor.moveTo(fragmentId, offset);

// Extend selection (creates or updates selection)
cursor.focusTo(targetFragmentId, targetOffset);

// Get selection offsets for a specific fragment
final paragraph = document.content.nodes.first as Paragraph;
final fragment = paragraph.fragments.first;
final selectionOffsets = cursor.getOffsets(paragraph, fragment);
if (selectionOffsets.$1 != -1) {
  print('Selection in fragment: ${selectionOffsets.$1} to ${selectionOffsets.$2}');
}

Additional Information

Documentation

For more detailed documentation and examples, see the /example folder.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Issues

If you find any bugs or have feature requests, please open an issue on GitHub.

Libraries

comments/comment_provider
controllers/document_language_controller
core/constants
core/markers
core/paragraph_registry
core/types
cursor
factories
fluent_document
fluent_editor
handlers/arrow_key_repeater
handlers/event_handler
handlers/handle_arrow_key
handlers/handle_backspace
handlers/handle_clear_formatting
handlers/handle_clipboard
handlers/handle_delete
handlers/handle_enter
handlers/handle_font_family
handlers/handle_font_size
handlers/handle_formats
handlers/handle_highlight_color
handlers/handle_insert_character
handlers/handle_insert_node
handlers/handle_paragraph_spacing
handlers/handle_paragraph_style
handlers/handle_replace_selection
handlers/handle_select_all
handlers/handle_tab
handlers/handle_text_align
handlers/handle_text_color
localization/fluent_editor_labels
models/document_language
renderers/render_fluent_node
renderers/render_fragment
renderers/render_paragraph
selection_manager
services/docx_exporter
services/export_service
services/export_service_web_html
services/export_service_web_stub
services/import_docx_service
services/import_html_service
services/import_markdown_service
services/import_odt_service
services/import_service
services/odt_exporter
services/pdf_font_provider
spell_check/spell_annotation
spell_check/spell_check_provider
styles
undo_redo/document_delta
undo_redo/undo_redo_manager
utils/color_utils
utils/cursor_navigation
utils/cursor_utils
utils/editor_utils
utils/fragment_operations
utils/list_marker_types
utils/node_operations
utils/perf_profiler
Lightweight profiler for identifying hot paths in the editor. All methods are no-ops in release builds; profiling is active only in debug/profile mode.
utils/resolve_selection
utils/string_utils
utils/tree_utils
widgets/dialogs/author_info_dialog
widgets/dialogs/image_drop_stub
widgets/dialogs/image_drop_web
widgets/dialogs/image_insert_dialog
widgets/dialogs/list_marker_dialog
widgets/editor/fluent_context_menu
widgets/editor/fluent_font_selector_widget
widgets/editor/fluent_font_size_selector_widget
widgets/editor/fluent_highlight_color_button
widgets/editor/fluent_paragraph_spacing_button
widgets/editor/fluent_paragraph_spacing_dialog
widgets/editor/fluent_paragraph_style_selector
widgets/editor/fluent_text_color_button
widgets/editor/fluent_toolbar_widget
widgets/nodes/fluent_cell_widget
widgets/nodes/fluent_fragment_widget
widgets/nodes/fluent_hr_widget
widgets/nodes/fluent_image_widget
widgets/nodes/fluent_list_item_widget
widgets/nodes/fluent_list_widget
widgets/nodes/fluent_paragraph_widget
widgets/nodes/fluent_row_widget
widgets/nodes/fluent_table_widget
widgets/nodes/virtualized_selectable_area
widgets/shared/color_picker_widgets
widgets/toolbar/language_selector_widget