Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to add a copy button to a code block? #2411

Open
1 task done
yzxh24 opened this issue Dec 10, 2024 · 4 comments
Open
1 task done

How to add a copy button to a code block? #2411

yzxh24 opened this issue Dec 10, 2024 · 4 comments
Labels
question Further information is requested

Comments

@yzxh24
Copy link

yzxh24 commented Dec 10, 2024

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 yzxh24 added the question Further information is requested label Dec 10, 2024
@EchoEllet
Copy link
Collaborator

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.

@yzxh24
Copy link
Author

yzxh24 commented Dec 10, 2024

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.

@EchoEllet
Copy link
Collaborator

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

@yzxh24
Copy link
Author

yzxh24 commented Dec 10, 2024

@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),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants