flutter-quill icon indicating copy to clipboard operation
flutter-quill copied to clipboard

How to add a copy button to a code block?

Open yzxh24 opened this issue 1 year ago • 10 comments

Is there an existing issue for this?

The question

Hi all, I would like to ask if I add a copy button to the top right corner of the code block so that I can quickly copy the code inside, similar to the picture below: image

yzxh24 avatar Dec 10 '24 05:12 yzxh24

The code block is not customizable, but it's possible to replace it with another widget, possibly a code highlighter / viewer that already has a copy button.

EchoEllet avatar Dec 10 '24 06:12 EchoEllet

The code block is not customizable, but it's possible to replace it with another widget, possibly a code highlighter / viewer that already has a copy button.

Yes, the screenshot in my question was generated from CustomBlockEmbed in the documentation. However, there are two issues I can't deal with when using CustomBlockEmbed:

  1. in the demo of the document, it uses a popup window for editing and then updating to the content, whereas I want to edit directly in the editor, as if I were editing a block of code
  2. when I use the toPlainText() method, the content customized with CustomBlockEmbed doesn't export the text and displays an empty content, toPlainText() allows to pass an Iterable<EmbedBuilder>? parameter, but it doesn't seem to be working correctly at the moment, I'm still trying to find out why, my solution so far is to extract the custom from the json string before using toPlainText() to generate a new json content.

yzxh24 avatar Dec 10 '24 08:12 yzxh24

Yes, the screenshot in my question was generated from CustomBlockEmbed in the documentation. However, there are two issues I can't deal with when using CustomBlockEmbed:

Please refer to #2146, and consider using the experimental property customLeadingBlockBuilder instead of CustomBlockEmbed to override the built-in code block instead of implementing a new custom embed.

allows to pass an Iterable<EmbedBuilder>? parameter, but it doesn't seem to be working correctly at the moment, I'm still trying to find out why

Could you provide a minimal example code? The toPlainText() extracts plain text without attributes or rich output. What are you using it for?

EchoEllet avatar Dec 10 '24 08:12 EchoEllet

@EchoEllet Hi, I've created a new demo repository on GitHub at https://github.com/yzxh24/quill_to_plain_text_demo.git. This demo shows me using EmbedBuilder to insert a custom Note into the content and copying all the content with the button in the bottom right corner. Simulator Screenshot - iPhone 16 Plus - 2024-12-10 at 17 22 23 When I click the copy button, all I can get is:

content

And the content I expect should be:

content
Notes

This is the full code:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_quill/flutter_quill.dart' as quill;

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  final quill.QuillController quillController = quill.QuillController.basic();

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

    quillController.document = quill.Document.fromJson(
      jsonDecode(r'[{"insert":"content\n"},{"insert":{"custom":"{\"notes\":\"[{\\\"insert\\\":\\\"Notes\\\\n\\\"}]\"}"}},{"insert":"\n"}]')
    );
  }

  @override
  void dispose() {
    quillController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
        actions: [
          IconButton(onPressed: () {
              _addEditNote(context);
            },
            icon: const Icon(Icons.add),
          )
        ],
      ),
      floatingActionButton: FloatingActionButton(onPressed: () async {
         /// Here, I pass the NotesEmbedBuilder to the toPlainText method
          var text = quillController.document.toPlainText([NotesEmbedBuilder(addEditNote: _addEditNote)]);
          await Clipboard.setData(ClipboardData(text: text));
          if (mounted) {
            ScaffoldMessenger.of(context)
              ..removeCurrentSnackBar()
              ..showSnackBar(const SnackBar(
                content: Text('Copy it'),
                duration: Duration(seconds: 1),
              ));
          }
        },
        child: const Icon(Icons.copy)
      ),
      body: Column(
        children: [
          quill.QuillToolbar.simple(controller: quillController),
          Expanded(child: quill.QuillEditor.basic(
            controller: quillController,
            focusNode: FocusNode(),
            configurations: quill.QuillEditorConfigurations(
              padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
              embedBuilders: [NotesEmbedBuilder(addEditNote: _addEditNote)],
            )
          ))
        ],
      )
    );
  }

  Future<void> _addEditNote(BuildContext context,
      {quill.Document? document}) async {
    final isEditing = document != null;
    final quillEditorController = quill.QuillController(
      document: document ?? quill.Document(),
      selection: const TextSelection.collapsed(offset: 0),
    );

    await showDialog(
      context: context,
      builder: (context) => AlertDialog(
        titlePadding: const EdgeInsets.only(left: 16, right: 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: quill.QuillEditor.basic(
          controller: quillEditorController,
          configurations: const quill.QuillEditorConfigurations(),
        ),
      ),
    );

    if (quillEditorController.document.isEmpty()) return;

    final block = quill.BlockEmbed.custom(
      NotesBlockEmbed.fromDocument(quillEditorController.document),
    );
    final controller = quillController;
    final index = controller.selection.baseOffset;
    final length = controller.selection.extentOffset - index;

    if (isEditing) {
      final offset =
          quill.getEmbedNode(controller, controller.selection.start).offset;
      controller.replaceText(
          offset, 1, block, TextSelection.collapsed(offset: offset));
    } else {
      controller.replaceText(index, length, block, null);
    }
  }
}

class NotesBlockEmbed extends quill.CustomBlockEmbed {
  const NotesBlockEmbed(String value) : super(noteType, value);

  static const String noteType = 'notes';

  static NotesBlockEmbed fromDocument(quill.Document document) =>
      NotesBlockEmbed(jsonEncode(document.toDelta().toJson()));

  quill.Document get document => quill.Document.fromJson(jsonDecode(data));
}

class NotesEmbedBuilder extends quill.EmbedBuilder {
  NotesEmbedBuilder({required this.addEditNote});

  Future<void> Function(BuildContext context, {quill.Document? document})
  addEditNote;

  @override
  String get key => 'notes';

  @override
  String toPlainText(quill.Embed node) {
    return node.toPlainText();
  }

  @override
  Widget build(
      BuildContext context,
      quill.QuillController controller,
      quill.Embed node,
      bool readOnly,
      bool inline,
      TextStyle textStyle,
      ) {
    final notes = NotesBlockEmbed(node.value.data).document;
    final isDark = Theme.of(context).brightness == Brightness.dark;

    return Material(
      color: Colors.transparent,
      child: Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(6),
          // border: Border.all(color: Colors.grey),
          color: isDark ? Colors.grey[900] : Colors.grey[200],
        ),
        child: Stack(
          children: [
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(
                notes.toPlainText().trimRight(),
                overflow: TextOverflow.ellipsis,
                style: TextStyle(
                    color: Theme.of(context)
                        .textTheme
                        .bodyMedium
                        ?.color
                        ?.withOpacity(0.6)),
              ),
            ),
            Positioned(
              top: 6,
              right: 6,
              child: Row(
                children: [
                  Material(
                    color: Colors.transparent,
                    child: InkWell(
                      onTap: () => addEditNote(context, document: notes),
                      borderRadius: BorderRadius.circular(20),
                      child: const Padding(
                        padding: EdgeInsets.all(8.0),
                        child: Icon(Icons.edit, size: 16.0, color: Colors.grey),
                      ),
                    ),
                  ),
                  Material(
                    color: Colors.transparent,
                    child: InkWell(
                      borderRadius: BorderRadius.circular(20),
                      onTap: () {
                        final notesText = notes.toPlainText().trimRight();
                        Clipboard.setData(ClipboardData(text: notesText));
                        ScaffoldMessenger.of(context)
                          ..removeCurrentSnackBar()
                          ..showSnackBar(
                            const SnackBar(
                              content: Text('Copy it'),
                              duration: Duration(seconds: 2),
                            ),
                          );
                      },
                      child: const Padding(
                        padding: EdgeInsets.all(8.0),
                        child: Icon(Icons.copy, size: 16.0, color: Colors.grey),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

yzxh24 avatar Dec 10 '24 09:12 yzxh24

@EchoEllet should we expose to TextBlock and TextLine to the public API? This is possible to do, but, requires this. I'm thinking in change the way on how Flutter Quill select a node to avoid expose these widgets and give to the editor more flexibility. By now, i'm watching: block_selection_area, block_selection_container, selectable. default_selectable and paragraph_component.

CatHood0 avatar Jan 20 '25 11:01 CatHood0

should we expose to TextBlock and TextLine to the public API?

No, at this point, this is just not a priority. It would only lead to more tasks. We're introducing issues and then fixing them, which affects productions apps, the spell checker, and table are still not supported as they should, why we're thinking about adding more features?

EchoEllet avatar Jan 20 '25 11:01 EchoEllet

Maybe it was because i didn't explain myself, but i was just mentioning that point to get your opinion. I know very well that it's not the time to add any changes of this kind. For now, it's not a priority for me to add those kinds of changes either.

CatHood0 avatar Jan 22 '25 00:01 CatHood0

tables are still not supported as they should

To achieve this, we must also expose exactly those parts, if we want QuillJs-like functionality. But for now i'll leave this for later.

CatHood0 avatar Jan 22 '25 00:01 CatHood0

tables are still not

This is not a priority either, but my opinion is not to do too much at the same time. My preference is to develop less well-executed features over too many half-baked solutions.

EchoEllet avatar Jan 22 '25 06:01 EchoEllet

same question here, I want to build the code block with a addon label like this

Image

how can I?

annd22 avatar Oct 20 '25 09:10 annd22