How to add a copy button to a code block?
Is there an existing issue for this?
- [X] I have searched the existing issues
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:
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.
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:
- 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
- when I use the
toPlainText()method, the content customized withCustomBlockEmbeddoesn't export the text and displays an empty content,toPlainText()allows to pass anIterable<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 usingtoPlainText()to generate a new json content.
Yes, the screenshot in my question was generated from
CustomBlockEmbedin the documentation. However, there are two issues I can't deal with when usingCustomBlockEmbed:
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 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.
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),
),
),
),
],
),
),
],
),
),
);
}
}
@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.
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?
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.
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.
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.
same question here, I want to build the code block with a addon label like this
how can I?