tiptap_flutter 0.3.0 copy "tiptap_flutter: ^0.3.0" to clipboard
tiptap_flutter: ^0.3.0 copied to clipboard

A Flutter rich-text editor; the Tiptap JS engine runs the document, Flutter natively renders every pixel. Composable widgets.

example/lib/main.dart

// Tiptap Flutter — Example application.
//
// Demonstrates how to compose the tiptap_flutter package widgets into a
// complete editor experience with a toolbar, content area, status bar,
// and performance overlay.
//
// It also demonstrates the two ways to theme the editor:
//   - Globally, by registering a [TiptapEditorTheme] in ThemeData.extensions
//     for the light and dark themes. Everything left unset there derives from
//     the Material ColorScheme, so the editor's text, caret, and links follow
//     the app automatically when the theme mode switches.
//   - Per editor, by passing a [TiptapEditorTheme] to [TiptapEditor.theme],
//     which layers over the global one field by field.
//
// The app bar has a light/dark toggle so the automatic dark-mode behavior
// can be seen without changing system settings.
//
// This is the same functionality as the original PoC app, now built on
// top of the package's composable widget API.

import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:tiptap_flutter/tiptap_flutter.dart';

void main() {
  runApp(const TiptapEditorApp());
}

class TiptapEditorApp extends StatefulWidget {
  const TiptapEditorApp({super.key});

  @override
  State<TiptapEditorApp> createState() => _TiptapEditorAppState();
}

class _TiptapEditorAppState extends State<TiptapEditorApp> {
  /// Starts on the system setting; the app bar toggle flips between light
  /// and dark explicitly from there.
  ThemeMode _themeMode = ThemeMode.system;

  void _toggleThemeMode() {
    setState(() {
      final isDark =
          _themeMode == ThemeMode.dark ||
          (_themeMode == ThemeMode.system &&
              WidgetsBinding.instance.platformDispatcher.platformBrightness ==
                  Brightness.dark);
      _themeMode = isDark ? ThemeMode.light : ThemeMode.dark;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Tiptap Editor Example',
      themeMode: _themeMode,

      /// Only the link color is customized globally; every other editor
      /// color (text, caret, selection, code backgrounds) is derived from
      /// the ColorScheme, which is what makes the dark theme below work
      /// without listing each color again.
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
        extensions: const [TiptapEditorTheme(linkColor: Colors.indigo)],
      ),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.dark,
        useMaterial3: true,
        extensions: const [TiptapEditorTheme(linkColor: Colors.lightBlue)],
      ),
      home: EditorScreen(onToggleThemeMode: _toggleThemeMode),
    );
  }
}

class EditorScreen extends StatefulWidget {
  /// Flips the app between light and dark mode.
  final VoidCallback onToggleThemeMode;

  const EditorScreen({super.key, required this.onToggleThemeMode});

  @override
  State<EditorScreen> createState() => _EditorScreenState();
}

class _EditorScreenState extends State<EditorScreen> {
  final EditorController _controller = EditorController();
  final ImagePicker _imagePicker = ImagePicker();

  /// Sample HTML content to initialize the editor with.
  static const _sampleContent = '''
<h1>Hello from Tiptap</h1>
<p>This is a <strong>proof of concept</strong> demonstrating the 
<em>headless engine bridge</em> between Flutter and Tiptap.</p>
<p>The engine runs inside a hidden WebView. Every pixel you see 
is rendered by Flutter.</p>
<ul>
  <li>Item one</li>
  <li>Item two</li>
  <li>Item three</li>
</ul>
<blockquote>This is a blockquote to test more node types.</blockquote>
<p>Inline <code>code</code> and a <a href="https://tiptap.dev">link</a> pick up
theme colors too.</p>
<pre><code class="language-dart">void main() => print('theme-aware');</code></pre>
''';

  /// Subscriptions to controller streams for the status bar.
  final List<StreamSubscription> _subscriptions = [];

  /// Current engine state for the status bar indicator.
  EngineState _engineState = EngineState.uninitialized;

  /// Schema metadata for the status bar summary.
  SchemaMetadata? _schema;

  /// Whether the performance overlay is currently visible.
  bool _showPerformance = false;

  /// Whether the per-editor override is applied on top of the global theme.
  /// Toggled from the app bar to show the two layers composing.
  bool _usePerEditorOverride = false;

  /// A deliberately conspicuous per-editor override so its effect is
  /// unmistakable when toggled: serif body text and an amber caret. Fields
  /// left null here (links, code backgrounds, etc.) continue to come from
  /// the global theme.
  static const _perEditorTheme = TiptapEditorTheme(
    baseTextStyle: TextStyle(fontFamily: 'serif'),
    cursorColor: Colors.amber,
    selectionColor: Color(0x40FFC107),
  );

  @override
  void initState() {
    super.initState();

    /// Subscribe to controller streams for the status bar.
    _subscriptions.add(
      _controller.engineStateStream.listen((state) {
        setState(() {
          _engineState = state;
        });
      }),
    );

    _subscriptions.add(
      _controller.schemaStream.listen((schema) {
        setState(() {
          _schema = schema;
        });
      }),
    );

    _initEditor();
  }

  Future<void> _initEditor() async {
    try {
      await _controller.initialize(content: _sampleContent);
    } catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Editor initialization failed: $e')),
        );
      }
    }
  }

  /// Pick an image from the device gallery, convert it to a base64 data URI,
  /// and return an [ImageInsertResult] for the toolbar to insert into the
  /// editor. Returns null if the user cancels the picker.
  ///
  /// This demonstrates the simplest possible image insertion flow using
  /// base64 encoding. In a production app, you would typically upload the
  /// image to a server or CDN and return the hosted URL instead — base64
  /// data URIs work but bloat the document size.
  Future<ImageInsertResult?> _pickImage() async {
    final XFile? pickedFile = await _imagePicker.pickImage(
      source: ImageSource.gallery,
      maxWidth: 1920,
      maxHeight: 1080,
      imageQuality: 85,
    );

    if (pickedFile == null) return null;

    final bytes = await pickedFile.readAsBytes();
    final base64Data = base64Encode(bytes);

    /// Determine the MIME type from the file extension. Falls back to
    /// a generic image type if the extension isn't recognized.
    final mimeType = _mimeTypeFromPath(pickedFile.path);

    return ImageInsertResult(
      src: 'data:$mimeType;base64,$base64Data',
      alt: pickedFile.name,
    );
  }

  /// Map common image file extensions to their MIME types.
  String _mimeTypeFromPath(String path) {
    final extension = path.split('.').last.toLowerCase();
    const mimeTypes = {
      'jpg': 'image/jpeg',
      'jpeg': 'image/jpeg',
      'png': 'image/png',
      'gif': 'image/gif',
      'webp': 'image/webp',
      'bmp': 'image/bmp',
      'heic': 'image/heic',
      'heif': 'image/heif',
    };
    return mimeTypes[extension] ?? 'image/png';
  }

  @override
  void dispose() {
    for (final sub in _subscriptions) {
      sub.cancel();
    }
    _subscriptions.clear();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final isDark = Theme.of(context).brightness == Brightness.dark;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Tiptap Editor'),
        actions: [
          IconButton(
            icon: Icon(isDark ? Icons.light_mode : Icons.dark_mode),
            tooltip: isDark ? 'Switch to light mode' : 'Switch to dark mode',
            onPressed: widget.onToggleThemeMode,
          ),
          IconButton(
            icon: Icon(
              _usePerEditorOverride
                  ? Icons.format_paint
                  : Icons.format_paint_outlined,
            ),
            tooltip: 'Toggle per-editor theme override',
            onPressed: () {
              setState(() {
                _usePerEditorOverride = !_usePerEditorOverride;
              });
            },
          ),
          IconButton(
            icon: Icon(_showPerformance ? Icons.speed : Icons.speed_outlined),
            tooltip: 'Toggle performance overlay',
            onPressed: () {
              setState(() {
                _showPerformance = !_showPerformance;
              });
            },
          ),
        ],
        bottom: PreferredSize(
          preferredSize: const Size.fromHeight(32),
          child: _buildStatusBar(),
        ),
      ),
      body: Stack(
        children: [
          /// Main editor content: toolbar + document.
          Column(
            children: [
              /// The formatting toolbar with image picker wired up.
              TiptapToolbar(controller: _controller, onPickImage: _pickImage),

              /// The rendered document with input and selection. With
              /// theme: null the editor follows the global theme alone.
              Expanded(
                child: TiptapEditor(
                  controller: _controller,
                  theme: _usePerEditorOverride ? _perEditorTheme : null,
                ),
              ),
            ],
          ),

          /// Performance overlay, shown when toggled.
          if (_showPerformance)
            TiptapPerformanceOverlay(
              controller: _controller,
              onClose: () {
                setState(() {
                  _showPerformance = false;
                });
              },
            ),
        ],
      ),
    );
  }

  /// Status bar showing the current engine state with a color indicator.
  Widget _buildStatusBar() {
    final Color statusColor;
    final String statusText;

    switch (_engineState) {
      case EngineState.uninitialized:
        statusColor = Colors.grey;
        statusText = 'Uninitialized';
      case EngineState.loading:
        statusColor = Colors.orange;
        statusText = 'Loading Engine...';
      case EngineState.pageLoaded:
        statusColor = Colors.amber;
        statusText = 'Page Loaded, Waiting for Engine JS...';
      case EngineState.engineGlobalReady:
        statusColor = Colors.lime;
        statusText = 'Engine Global Found, Sending Init...';
      case EngineState.schemaReady:
        statusColor = Colors.lightBlue;
        statusText = 'Schema Ready';
      case EngineState.ready:
        statusColor = Colors.green;
        statusText = 'Engine Ready';
      case EngineState.error:
        statusColor = Colors.red;
        statusText = 'Error: ${_controller.errorMessage ?? "Unknown"}';
      case EngineState.destroyed:
        statusColor = Colors.grey;
        statusText = 'Destroyed';
    }

    return Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
      child: Row(
        children: [
          Container(
            width: 12,
            height: 12,
            decoration: BoxDecoration(
              color: statusColor,
              shape: BoxShape.circle,
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              statusText,
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                color: Theme.of(context).colorScheme.onSurfaceVariant,
              ),
              overflow: TextOverflow.ellipsis,
            ),
          ),
          if (_schema != null)
            Text(
              '${_schema!.nodes.length} nodes, '
              '${_schema!.marks.length} marks, '
              '${_schema!.commands.length} commands',
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                color: Theme.of(context).colorScheme.onSurfaceVariant,
              ),
            ),
        ],
      ),
    );
  }
}
10
likes
150
points
180
downloads

Documentation

API reference

Publisher

verified publisherfeziks.com

Weekly Downloads

A Flutter rich-text editor; the Tiptap JS engine runs the document, Flutter natively renders every pixel. Composable widgets.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, path_provider, webview_flutter

More

Packages that depend on tiptap_flutter