pull/1566/head
Ellet 2 years ago
parent 64ea5efc39
commit 5fcda94b59
No known key found for this signature in database
GPG Key ID: C488CC70BBCEF0D1
  1. 2
      CHANGELOG.md
  2. 8
      README.md
  3. 11
      doc/todo.md
  4. 31
      example/lib/presentation/quill/quill_screen.dart
  5. 1
      example/pubspec.yaml
  6. 2
      flutter_quill_extensions/CHANGELOG.md
  7. 2
      flutter_quill_extensions/lib/embeds/image/editor/image_embed_types.dart
  8. 2
      flutter_quill_extensions/lib/embeds/video/video.dart
  9. 0
      flutter_quill_extensions/lib/extensions/controller_ext.dart
  10. 2
      flutter_quill_extensions/lib/flutter_quill_extensions.dart
  11. 2
      flutter_quill_test/CHANGELOG.md
  12. 4
      lib/flutter_quill.dart
  13. 5
      lib/markdown_quill.dart
  14. 4
      lib/src/models/config/editor/editor_configurations.dart
  15. 2
      lib/src/models/config/raw_editor/raw_editor_configurations.dart
  16. 2
      lib/src/models/config/toolbar/buttons/color_configurations.dart
  17. 2
      lib/src/models/config/toolbar/buttons/font_size_configurations.dart
  18. 4
      lib/src/models/config/toolbar/simple_toolbar_configurations.dart
  19. 3
      lib/src/models/documents/document.dart
  20. 2
      lib/src/models/documents/nodes/container.dart
  21. 2
      lib/src/models/documents/nodes/leaf.dart
  22. 2
      lib/src/models/documents/nodes/line.dart
  23. 2
      lib/src/models/documents/nodes/node.dart
  24. 11
      lib/src/packages/quill_markdown/custom_quill_attributes.dart
  25. 359
      lib/src/packages/quill_markdown/delta_to_markdown.dart
  26. 116
      lib/src/packages/quill_markdown/embeddable_table_syntax.dart
  27. 424
      lib/src/packages/quill_markdown/markdown_to_delta.dart
  28. 59
      lib/src/packages/quill_markdown/utils.dart
  29. 2
      lib/src/utils/embeds.dart
  30. 2
      lib/src/widgets/editor/editor.dart
  31. 2
      lib/src/widgets/others/delegate.dart
  32. 2
      lib/src/widgets/quill/embeds.dart
  33. 46
      lib/src/widgets/quill/quill_controller.dart
  34. 14
      lib/src/widgets/quill/text_block.dart
  35. 22
      lib/src/widgets/quill/text_line.dart
  36. 66
      lib/src/widgets/raw_editor/raw_editor_state.dart
  37. 2
      lib/src/widgets/style_widgets/number_point.dart
  38. 2
      lib/src/widgets/toolbar/buttons/clear_format_button.dart
  39. 2
      lib/src/widgets/toolbar/buttons/color/color_button.dart
  40. 2
      lib/src/widgets/toolbar/buttons/custom_button_button.dart
  41. 2
      lib/src/widgets/toolbar/buttons/font_family_button.dart
  42. 2
      lib/src/widgets/toolbar/buttons/font_size_button.dart
  43. 2
      lib/src/widgets/toolbar/buttons/history_button.dart
  44. 2
      lib/src/widgets/toolbar/buttons/indent_button.dart
  45. 2
      lib/src/widgets/toolbar/buttons/link_style2_button.dart
  46. 2
      lib/src/widgets/toolbar/buttons/link_style_button.dart
  47. 2
      lib/src/widgets/toolbar/buttons/search/search_button.dart
  48. 2
      lib/src/widgets/toolbar/buttons/search/search_dialog.dart
  49. 2
      lib/src/widgets/toolbar/buttons/select_alignment_buttons.dart
  50. 2
      lib/src/widgets/toolbar/buttons/select_header_style_button.dart
  51. 2
      lib/src/widgets/toolbar/buttons/select_header_style_buttons.dart
  52. 2
      lib/src/widgets/toolbar/buttons/toggle_check_list_button.dart
  53. 2
      lib/src/widgets/toolbar/buttons/toggle_style_button.dart
  54. 2
      packages/quill_html_converter/CHANGELOG.md
  55. 32
      packages/quill_html_converter/lib/quill_html_converter.dart
  56. 7
      packages/quill_html_converter/pubspec.yaml
  57. 5
      pubspec.yaml

@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
## 9.0.0-dev-7
* Fix a bug in chaning the background/font color of ol/ul list
* Better support for pasting HTML contents from external websites to the editor
* The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`)
* Flutter Quill Extensions:
* Fix link bug in the video url
* Fix patterns

@ -238,11 +238,13 @@ To see how to use the extension package, please take a look at the [README](./fl
Having your document stored in Quill Delta format is sometimes not enough. Often you'll need to convert
it to other formats such as HTML to publish it, or send an email.
**Note**: This package support converting from HTML back to Quill delta but it's experimental and used internally when pasting Html content from the cliboard to the Quill Editor
You have two options:
1. Using [quill_html_converter](./packages/quill_html_converter/) to convert to/from HTML, the package can convert the Quill delta to HTML well
(it uses [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html)) but the converting from HTML back to Quill delta is experimental
2. Another option is to use
1. Using [quill_html_converter](./packages/quill_html_converter/) to convert to HTML, the package can convert the Quill delta to HTML well
(it uses [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html)), it just a handy extension to do it more quickly
1. Another option is to use
[vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html) to convert your document
to HTML.
This package has full support for all Quill operations—including images, videos, formulas,

@ -29,6 +29,10 @@ This is a todo list page that added recently and will be updated soon.
- Extract the shared properties between `QuillRawEditorConfigurations` and `QuillEditorConfigurations`
- The todo in the this [commit](https://github.com/singerdmx/flutter-quill/commit/79597ea6425357795c0663588ac079665241f23a) needs to be checked
- use `maybeOf` and of instead `ofNotNull` in the providers to follow flutter offical convenstion, completly rework the providers and update the build context extensions
- Add line through to the text when the check point checked is true
- Change the color of the numbers and dots in ol/ul to match the ones in the item list
- Fix the bugs of the font family and font size
- Try to update Quill Html Converter
### Bugs
@ -39,12 +43,7 @@ Please go to the [issues](https://github.com/singerdmx/flutter-quill/issues)
## Flutter Quill Extensions
### Features
- Add support for copying images to the Clipboard
### Improvemenets
Please check the todos, this list will be updated soon.
### Bugs
Please check the todos, this list will be updated soon.
### Bugs

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'
show FlutterQuillEmbeds, QuillSharedExtensionsConfigurations;
import 'package:quill_html_converter/quill_html_converter.dart';
import 'package:share_plus/share_plus.dart' show Share;
@ -46,6 +45,18 @@ class _QuillScreenState extends State<QuillScreen> {
_controller.document = widget.args.document;
}
// Future<void> _init() async {
// final reader = await ClipboardReader.readClipboard();
// if (reader.canProvide(Formats.htmlText)) {
// final html = await reader.readValue(Formats.htmlText);
// if (html == null) {
// return;
// }
// final delta = DeltaHtmlExt.fromHtml(html);
// _controller.document = Document.fromDelta(delta);
// }
// }
@override
void dispose() {
_controller.dispose();
@ -65,7 +76,7 @@ class _QuillScreenState extends State<QuillScreen> {
onPressed: () {
final html = _controller.document.toDelta().toHtml();
_controller.document =
Document.fromDelta(DeltaHtmlExt.fromHtml(html));
Document.fromDelta(QuillController.fromHtml(html));
},
icon: const Icon(Icons.html),
),
@ -119,14 +130,14 @@ class _QuillScreenState extends State<QuillScreen> {
codeBlock: QuillEditorCodeBlockElementOptions(
enableLineNumbers: true,
),
orderedList: QuillEditorOrderedListElementOptions(
backgroundColor: Colors.amber,
fontColor: Colors.black,
),
unorderedList: QuillEditorUnOrderedListElementOptions(
backgroundColor: Colors.green,
fontColor: Colors.red,
),
// orderedList: QuillEditorOrderedListElementOptions(
// backgroundColor: Colors.amber,
// fontColor: Colors.black,
// ),
// unorderedList: QuillEditorUnOrderedListElementOptions(
// backgroundColor: Colors.green,
// fontColor: Colors.red,
// ),
),
),
scrollController: _editorScrollController,

@ -44,6 +44,7 @@ dependencies:
file_picker: ^6.1.1
# For sharing text
share_plus: ^7.2.1
super_clipboard: ^0.7.3
dependency_overrides:
flutter_quill:

@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
## 9.0.0-dev-7
* Fix a bug in chaning the background/font color of ol/ul list
* Better support for pasting HTML contents from external websites to the editor
* The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`)
* Flutter Quill Extensions:
* Fix link bug in the video url
* Fix patterns

@ -4,7 +4,7 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import '../../../extensions/controller.dart';
import '../../../extensions/controller_ext.dart';
import '../../../services/image_picker/s_image_picker.dart';
/// When request picking an image, for example when the image button toolbar

@ -2,7 +2,7 @@ import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_quill/flutter_quill.dart';
import 'package:meta/meta.dart' show immutable;
import '../../extensions/controller.dart';
import '../../extensions/controller_ext.dart';
import '../../services/image_picker/s_image_picker.dart';
/// When request picking an video, for example when the video button toolbar

@ -36,7 +36,7 @@ export 'embeds/video/editor/video_embed.dart';
export 'embeds/video/editor/video_web_embed.dart';
export 'embeds/video/toolbar/video_button.dart';
export 'embeds/video/video.dart';
export 'extensions/controller.dart';
export 'extensions/controller_ext.dart';
export 'models/config/editor/image/image.dart';
export 'models/config/editor/image/image_web.dart';
export 'models/config/editor/video/video.dart';

@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
## 9.0.0-dev-7
* Fix a bug in chaning the background/font color of ol/ul list
* Better support for pasting HTML contents from external websites to the editor
* The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`)
* Flutter Quill Extensions:
* Fix link bug in the video url
* Fix patterns

@ -23,10 +23,10 @@ export 'src/models/themes/quill_dialog_theme.dart';
export 'src/models/themes/quill_icon_theme.dart';
export 'src/utils/embeds.dart';
export 'src/widgets/editor/editor.dart';
export 'src/widgets/others/controller.dart';
export 'src/widgets/quill/quill_controller.dart';
export 'src/widgets/others/cursor.dart';
export 'src/widgets/others/default_styles.dart';
export 'src/widgets/others/embeds.dart';
export 'src/widgets/quill/embeds.dart';
export 'src/widgets/others/link.dart'
show LinkActionPickerDelegate, LinkMenuAction;
export 'src/widgets/raw_editor/raw_editor.dart';

@ -0,0 +1,5 @@
library quill_markdown;
export 'src/packages/quill_markdown/delta_to_markdown.dart';
export 'src/packages/quill_markdown/embeddable_table_syntax.dart';
export 'src/packages/quill_markdown/markdown_to_delta.dart';

@ -7,10 +7,10 @@ import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart' show experimental;
import '../../../widgets/editor/editor_builder.dart';
import '../../../widgets/others/controller.dart';
import '../../../widgets/quill/quill_controller.dart';
import '../../../widgets/others/default_styles.dart';
import '../../../widgets/others/delegate.dart';
import '../../../widgets/others/embeds.dart';
import '../../../widgets/quill/embeds.dart';
import '../../../widgets/others/link.dart';
import '../../../widgets/raw_editor/raw_editor.dart';
import '../../themes/quill_dialog_theme.dart';

@ -25,7 +25,7 @@ import 'package:flutter/widgets.dart'
Widget;
import 'package:meta/meta.dart' show immutable;
import '../../../widgets/others/controller.dart';
import '../../../widgets/quill/quill_controller.dart';
import '../../../widgets/others/cursor.dart';
import '../../../widgets/others/default_styles.dart';
import '../../../widgets/others/delegate.dart';

@ -1,6 +1,6 @@
import 'package:flutter/widgets.dart' show Color;
import '../../../../widgets/others/controller.dart';
import '../../../../widgets/quill/quill_controller.dart';
import '../../quill_shared_configurations.dart' show QuillSharedConfigurations;
import 'base_configurations.dart';

@ -6,7 +6,7 @@ import 'package:flutter/material.dart'
import 'package:flutter/widgets.dart'
show Color, EdgeInsets, EdgeInsetsGeometry, TextOverflow, TextStyle;
import '../../../../widgets/others/controller.dart';
import '../../../../widgets/quill/quill_controller.dart';
import '../../../documents/attribute.dart';
import '../../../themes/quill_icon_theme.dart';
import '../../quill_configurations.dart';

@ -4,8 +4,8 @@ import 'package:flutter/foundation.dart' show immutable;
import 'package:flutter/widgets.dart'
show Axis, Widget, WrapAlignment, WrapCrossAlignment;
import '../../../widgets/others/controller.dart';
import '../../../widgets/others/embeds.dart';
import '../../../widgets/quill/quill_controller.dart';
import '../../../widgets/quill/embeds.dart';
import '../../themes/quill_dialog_theme.dart';
import '../../themes/quill_icon_theme.dart';
import 'buttons/base_configurations.dart';

@ -1,6 +1,6 @@
import 'dart:async';
import '../../widgets/others/embeds.dart';
import '../../widgets/quill/embeds.dart';
import '../quill_delta.dart';
import '../rules/rule.dart';
import '../structs/doc_change.dart';
@ -402,6 +402,7 @@ class Document {
throw ArgumentError.value(doc, 'Document Delta cannot be empty.');
}
// print(doc.last.data.runtimeType);
assert((doc.last.data as String).endsWith('\n'));
var offset = 0;

@ -1,6 +1,6 @@
import 'dart:collection';
import '../../../widgets/others/embeds.dart';
import '../../../widgets/quill/embeds.dart';
import '../style.dart';
import 'leaf.dart';
import 'line.dart';

@ -1,6 +1,6 @@
import 'dart:math' as math;
import '../../../widgets/others/embeds.dart';
import '../../../widgets/quill/embeds.dart';
import '../../quill_delta.dart';
import '../style.dart';
import 'embeddable.dart';

@ -2,7 +2,7 @@ import 'dart:math' as math;
import 'package:collection/collection.dart';
import '../../../widgets/others/embeds.dart';
import '../../../widgets/quill/embeds.dart';
import '../../quill_delta.dart';
import '../../structs/offset_value.dart';
import '../attribute.dart';

@ -1,6 +1,6 @@
import 'dart:collection';
import '../../../widgets/others/embeds.dart';
import '../../../widgets/quill/embeds.dart';
import '../../quill_delta.dart';
import '../attribute.dart';
import '../style.dart';

@ -0,0 +1,11 @@
import 'package:flutter_quill/flutter_quill.dart';
/// Custom attribute to save the language of codeblock
class CodeBlockLanguageAttribute extends Attribute<String?> {
/// @nodoc
const CodeBlockLanguageAttribute(String? value)
: super(attrKey, AttributeScope.ignore, value);
/// attribute key
static const attrKey = 'x-md-codeblock-lang';
}

@ -0,0 +1,359 @@
import 'dart:convert';
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:flutter_quill/flutter_quill.dart';
import './custom_quill_attributes.dart';
import './utils.dart';
class _AttributeHandler {
_AttributeHandler({
this.beforeContent,
this.afterContent,
});
final void Function(
Attribute<Object?> attribute,
Node node,
StringSink output,
)? beforeContent;
final void Function(
Attribute<Object?> attribute,
Node node,
StringSink output,
)? afterContent;
}
/// Outputs [Embed] element as markdown.
typedef EmbedToMarkdown = void Function(Embed embed, StringSink out);
extension on Object? {
T? asNullable<T>() {
final self = this;
return self == null ? null : self as T;
}
}
/// Convertor from [Delta] to quill Markdown string.
class DeltaToMarkdown extends Converter<Delta, String>
implements _NodeVisitor<StringSink> {
///
DeltaToMarkdown({
Map<String, EmbedToMarkdown>? customEmbedHandlers,
}) {
if (customEmbedHandlers != null) {
_embedHandlers.addAll(customEmbedHandlers);
}
}
@override
String convert(Delta input) {
final newDelta = transform(input);
final quillDocument = Document.fromDelta(newDelta);
final outBuffer = quillDocument.root.accept(this);
return outBuffer.toString();
}
final Map<String, _AttributeHandler> _blockAttrsHandlers = {
Attribute.codeBlock.key: _AttributeHandler(
beforeContent: (attribute, node, output) {
var infoString = '';
if (node.containsAttr(CodeBlockLanguageAttribute.attrKey)) {
infoString = node.getAttrValueOr(
CodeBlockLanguageAttribute.attrKey,
'',
);
}
if (infoString.isEmpty) {
final linesWithLang = (node as Block).children.where((child) =>
child.containsAttr(CodeBlockLanguageAttribute.attrKey));
if (linesWithLang.isNotEmpty) {
infoString = linesWithLang.first.getAttrValueOr(
CodeBlockLanguageAttribute.attrKey,
'or',
);
}
}
output.writeln('```$infoString');
},
afterContent: (attribute, node, output) => output.writeln('```'),
),
};
final Map<String, _AttributeHandler> _lineAttrsHandlers = {
Attribute.header.key: _AttributeHandler(
beforeContent: (attribute, node, output) {
output
..write('#' * (attribute.value.asNullable<int>() ?? 1))
..write(' ');
},
),
Attribute.blockQuote.key: _AttributeHandler(
beforeContent: (attribute, node, output) => output.write('> '),
),
Attribute.list.key: _AttributeHandler(
beforeContent: (attribute, node, output) {
final indentLevel = node.getAttrValueOr(Attribute.indent.key, 0);
final isNumbered = attribute.value == 'ordered';
output
..write((isNumbered ? ' ' : ' ') * indentLevel)
..write('${isNumbered ? '1.' : '-'} ');
},
),
};
final Map<String, _AttributeHandler> _textAttrsHandlers = {
Attribute.italic.key: _AttributeHandler(
beforeContent: (attribute, node, output) {
if (node.previous?.containsAttr(attribute.key) != true) {
output.write('_');
}
},
afterContent: (attribute, node, output) {
if (node.next?.containsAttr(attribute.key) != true) {
output.write('_');
}
},
),
Attribute.bold.key: _AttributeHandler(
beforeContent: (attribute, node, output) {
if (node.previous?.containsAttr(attribute.key) != true) {
output.write('**');
}
},
afterContent: (attribute, node, output) {
if (node.next?.containsAttr(attribute.key) != true) {
output.write('**');
}
},
),
Attribute.strikeThrough.key: _AttributeHandler(
beforeContent: (attribute, node, output) {
if (node.previous?.containsAttr(attribute.key) != true) {
output.write('~~');
}
},
afterContent: (attribute, node, output) {
if (node.next?.containsAttr(attribute.key) != true) {
output.write('~~');
}
},
),
Attribute.inlineCode.key: _AttributeHandler(
beforeContent: (attribute, node, output) {
if (node.previous?.containsAttr(attribute.key) != true) {
output.write('`');
}
},
afterContent: (attribute, node, output) {
if (node.next?.containsAttr(attribute.key) != true) {
output.write('`');
}
},
),
Attribute.link.key: _AttributeHandler(
beforeContent: (attribute, node, output) {
if (node.previous?.containsAttr(attribute.key, attribute.value) !=
true) {
output.write('[');
}
},
afterContent: (attribute, node, output) {
if (node.next?.containsAttr(attribute.key, attribute.value) != true) {
output.write('](${attribute.value.asNullable<String>() ?? ''})');
}
},
),
};
final Map<String, EmbedToMarkdown> _embedHandlers = {
BlockEmbed.imageType: (embed, out) => out.write('![](${embed.value.data})'),
horizontalRuleType: (embed, out) {
// adds new line after it
// make --- separated so it doesn't get rendered as header
out.writeln('- - -');
},
};
@override
StringSink visitRoot(Root root, [StringSink? output]) {
final out = output ??= StringBuffer();
for (final container in root.children) {
container.accept(this, out);
}
return out;
}
@override
StringSink visitBlock(Block block, [StringSink? output]) {
final out = output ??= StringBuffer();
_handleAttribute(_blockAttrsHandlers, block, output, () {
for (final line in block.children) {
line.accept(this, out);
}
});
return out;
}
@override
StringSink visitLine(Line line, [StringSink? output]) {
final out = output ??= StringBuffer();
final style = line.style;
_handleAttribute(_lineAttrsHandlers, line, output, () {
for (final leaf in line.children) {
leaf.accept(this, out);
}
});
if (style.isEmpty ||
style.values.every((item) => item.scope != AttributeScope.block)) {
out.writeln();
}
if (style.containsKey(Attribute.list.key) &&
line.nextLine?.style.containsKey(Attribute.list.key) != true) {
out.writeln();
}
out.writeln();
return out;
}
@override
StringSink visitText(QuillText text, [StringSink? output]) {
final out = output ??= StringBuffer();
final style = text.style;
_handleAttribute(
_textAttrsHandlers,
text,
output,
() {
var content = text.value;
if (!(style.containsKey(Attribute.codeBlock.key) ||
style.containsKey(Attribute.inlineCode.key) ||
(text.parent?.style.containsKey(Attribute.codeBlock.key) ??
false))) {
content = content.replaceAllMapped(
RegExp(r'[\\\`\*\_\{\}\[\]\(\)\#\+\-\.\!\>\<]'), (match) {
return '\\${match[0]}';
});
}
out.write(content);
},
sortedAttrsBySpan: true,
);
return out;
}
@override
StringSink visitEmbed(Embed embed, [StringSink? output]) {
final out = output ??= StringBuffer();
final type = embed.value.type;
_embedHandlers[type]!.call(embed, out);
return out;
}
void _handleAttribute(
Map<String, _AttributeHandler> handlers,
Node node,
StringSink output,
VoidCallback contentHandler, {
bool sortedAttrsBySpan = false,
}) {
final attrs = sortedAttrsBySpan
? node.attrsSortedByLongestSpan()
: node.style.attributes.values.toList();
final handlersToUse = attrs
.where((attr) => handlers.containsKey(attr.key))
.map((attr) => MapEntry(attr.key, handlers[attr.key]!))
.toList();
for (final handlerEntry in handlersToUse) {
handlerEntry.value.beforeContent?.call(
node.style.attributes[handlerEntry.key]!,
node,
output,
);
}
contentHandler();
for (final handlerEntry in handlersToUse.reversed) {
handlerEntry.value.afterContent?.call(
node.style.attributes[handlerEntry.key]!,
node,
output,
);
}
}
}
//// AST with visitor
abstract class _NodeVisitor<T> {
const _NodeVisitor._();
T visitRoot(Root root, [T? context]);
T visitBlock(Block block, [T? context]);
T visitLine(Line line, [T? context]);
T visitText(QuillText text, [T? context]);
T visitEmbed(Embed embed, [T? context]);
}
extension _NodeX on Node {
T accept<T>(_NodeVisitor<T> visitor, [T? context]) {
switch (runtimeType) {
case Root _:
return visitor.visitRoot(this as Root, context);
case Block _:
return visitor.visitBlock(this as Block, context);
case Line _:
return visitor.visitLine(this as Line, context);
case QuillText _:
return visitor.visitText(this as QuillText, context);
case Embed _:
return visitor.visitEmbed(this as Embed, context);
}
throw Exception('Container of type $runtimeType cannot be visited');
}
bool containsAttr(String attributeKey, [Object? value]) {
if (!style.containsKey(attributeKey)) {
return false;
}
if (value == null) {
return true;
}
return style.attributes[attributeKey]!.value == value;
}
T getAttrValueOr<T>(String attributeKey, T or) {
final attrs = style.attributes;
final attrValue = attrs[attributeKey]?.value as T?;
return attrValue ?? or;
}
List<Attribute<Object?>> attrsSortedByLongestSpan() {
final attrCount = <Attribute<dynamic>, int>{};
var node = this;
// get the first node
while (node.previous != null) {
node = node.previous!;
node.style.attributes.forEach((key, value) {
attrCount[value] = (attrCount[value] ?? 0) + 1;
});
node = node.next!;
}
final attrs = style.attributes.values.sorted(
(attr1, attr2) => attrCount[attr2]!.compareTo(attrCount[attr1]!));
return attrs;
}
}

@ -0,0 +1,116 @@
import 'package:charcode/charcode.dart';
import 'package:flutter_quill/flutter_quill.dart' hide Node;
import 'package:markdown/markdown.dart';
/// Parses markdown table and saves the table markdown content into the element attributes.
class EmbeddableTableSyntax extends BlockSyntax {
/// @nodoc
const EmbeddableTableSyntax();
static const _base = TableSyntax();
@override
bool canEndBlock(BlockParser parser) => false;
@override
RegExp get pattern => _base.pattern;
@override
bool canParse(BlockParser parser) => _base.canParse(parser);
/// Parses a table into its three parts:
///
/// * a head row of head cells (`<th>` cells)
/// * a divider of hyphens and pipes (not rendered)
/// * many body rows of body cells (`<td>` cells)
@override
Node? parse(BlockParser parser) {
final columnCount = _columnCount(parser.next!.content);
final headCells = _columnCount(parser.current.content);
final valBuf =
StringBuffer('${parser.current.content}\n${parser.next!.content}');
parser.advance();
if (columnCount != headCells) {
return null;
}
// advance header and divider of hyphens.
parser.advance();
while (!parser.isDone && !BlockSyntax.isAtBlockEnd(parser)) {
valBuf.write('\n${parser.current.content}');
parser.advance();
}
return Element.empty(EmbeddableTable.tableType)
..attributes['data'] = valBuf.toString();
}
int _columnCount(String line) {
final startIndex = _walkPastOpeningPipe(line);
var endIndex = line.length - 1;
while (endIndex > 0) {
final ch = line.codeUnitAt(endIndex);
if (ch == $pipe) {
endIndex--;
break;
}
if (ch != $space && ch != $tab) {
break;
}
endIndex--;
}
return line.substring(startIndex, endIndex + 1).split('|').length;
}
int _walkPastWhitespace(String line, int index) {
while (index < line.length) {
final ch = line.codeUnitAt(index);
if (ch != $space && ch != $tab) {
break;
}
//ignore: parameter_assignments
index++;
}
return index;
}
int _walkPastOpeningPipe(String line) {
var index = 0;
while (index < line.length) {
final ch = line.codeUnitAt(index);
if (ch == $pipe) {
index++;
index = _walkPastWhitespace(line, index);
}
if (ch != $space && ch != $tab) {
// No leading pipe.
break;
}
index++;
}
return index;
}
}
/// An [Embeddable] table that can used to render a table in quill_editor
class EmbeddableTable extends BlockEmbed {
/// @nodoc
EmbeddableTable(String data) : super(tableType, data);
/// [Embeddable] type
static const tableType = 'x-embed-table';
/// Create from markdown.
//ignore: prefer_constructors_over_static_methods
static EmbeddableTable fromMdSyntax(Map<String, String> attributes) =>
EmbeddableTable(attributes['data']!);
/// Outputs table markdown to output.
static void toMdSyntax(Embed embed, StringSink out) {
out
..writeln(embed.value.data)
..writeln();
}
}

@ -0,0 +1,424 @@
import 'dart:collection';
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:markdown/markdown.dart' as md;
import './custom_quill_attributes.dart';
import './embeddable_table_syntax.dart';
import './utils.dart';
/// Converts markdown [md.Element] to list of [Attribute].
typedef ElementToAttributeConvertor = List<Attribute<dynamic>> Function(
md.Element element,
);
/// Converts markdown [md.Element] to [Embeddable].
typedef ElementToEmbeddableConvertor = Embeddable Function(
Map<String, String> elAttrs,
);
/// Convertor from Markdown string to quill [Delta].
class MarkdownToDelta extends Converter<String, Delta>
implements md.NodeVisitor {
///
MarkdownToDelta({
required this.markdownDocument,
this.customElementToInlineAttribute = const {},
this.customElementToBlockAttribute = const {},
this.customElementToEmbeddable = const {},
this.softLineBreak = false,
});
final md.Document markdownDocument;
final Map<String, ElementToAttributeConvertor> customElementToInlineAttribute;
final Map<String, ElementToAttributeConvertor> customElementToBlockAttribute;
final Map<String, ElementToEmbeddableConvertor> customElementToEmbeddable;
final bool softLineBreak;
// final _blockTags = <String>[
// 'p',
// 'h1',
// 'h2',
// 'h3',
// 'h4',
// 'h5',
// 'h6',
// 'li',
// 'blockquote',
// 'pre',
// 'ol',
// 'ul',
// 'hr',
// 'table',
// 'thead',
// 'tbody',
// 'tr'
// ];
final _elementToBlockAttr = <String, ElementToAttributeConvertor>{
'ul': (_) => [Attribute.ul],
'ol': (_) => [Attribute.ol],
'pre': (element) {
final codeChild = element.children!.first as md.Element;
final language = (codeChild.attributes['class'] ?? '')
.split(' ')
.where((class_) => class_.startsWith('language-'))
.firstOrNull
?.split('-')
.lastOrNull;
return [
Attribute.codeBlock,
if (language != null) CodeBlockLanguageAttribute(language),
];
},
'blockquote': (_) => [Attribute.blockQuote],
'h1': (_) => [Attribute.h1],
'h2': (_) => [Attribute.h2],
'h3': (_) => [Attribute.h3],
};
final _elementToInlineAttr = <String, ElementToAttributeConvertor>{
'em': (_) => [Attribute.italic],
'strong': (_) => [Attribute.bold],
'del': (_) => [Attribute.strikeThrough],
'a': (element) => [LinkAttribute(element.attributes['href'])],
'code': (_) => [Attribute.inlineCode],
};
final _elementToEmbed = <String, ElementToEmbeddableConvertor>{
'hr': (_) => horizontalRule,
'img': (elAttrs) => BlockEmbed.image(elAttrs['src'] ?? ''),
};
var _delta = Delta();
final _activeInlineAttributes = Queue<List<Attribute<dynamic>>>();
final _activeBlockAttributes = Queue<List<Attribute<dynamic>>>();
final _topLevelNodes = <md.Node>[];
bool _isInBlockQuote = false;
bool _isInCodeblock = false;
bool _justPreviousBlockExit = false;
String? _lastTag;
String? _currentBlockTag;
int _listItemIndent = -1;
@override
Delta convert(String input) {
_delta = Delta();
_activeInlineAttributes.clear();
_activeBlockAttributes.clear();
_topLevelNodes.clear();
_lastTag = null;
_currentBlockTag = null;
_isInBlockQuote = false;
_isInCodeblock = false;
_justPreviousBlockExit = false;
_listItemIndent = -1;
final lines = const LineSplitter().convert(input);
final mdNodes = markdownDocument.parseLines(lines);
_topLevelNodes.addAll(mdNodes);
for (final node in mdNodes) {
node.accept(this);
}
// Ensure the delta ends with a newline.
_appendLastNewLineIfNeeded();
return _delta;
}
void _appendLastNewLineIfNeeded() {
if (_delta.isEmpty) return;
final dynamic lastValue = _delta.last.value;
if (!(lastValue is String && lastValue.endsWith('\n'))) {
_delta.insert('\n', _effectiveBlockAttrs());
}
}
@override
void visitText(md.Text text) {
String renderedText;
if (_isInBlockQuote) {
renderedText = text.text;
} else if (_isInCodeblock) {
renderedText = text.text.endsWith('\n')
? text.text.substring(0, text.text.length - 1)
: text.text;
} else {
renderedText = _trimTextToMdSpec(text.text);
}
if (renderedText.contains('\n')) {
var lines = renderedText.split('\n');
if (renderedText.endsWith('\n')) {
lines = lines.sublist(0, lines.length - 1);
}
for (var i = 0; i < lines.length; i++) {
final isLastItem = i == lines.length - 1;
final line = lines[i];
_delta.insert(line, _effectiveInlineAttrs());
if (!isLastItem) {
_delta.insert('\n', _effectiveBlockAttrs());
}
}
} else {
_delta.insert(renderedText, _effectiveInlineAttrs());
}
_lastTag = null;
_justPreviousBlockExit = false;
}
@override
bool visitElementBefore(md.Element element) {
_insertNewLineBeforeElementIfNeeded(element);
final tag = element.tag;
_currentBlockTag ??= tag;
_lastTag = tag;
if (_haveBlockAttrs(element)) {
_activeBlockAttributes.addLast(_toBlockAttributes(element));
}
if (_haveInlineAttrs(element)) {
_activeInlineAttributes.addLast(_toInlineAttributes(element));
}
if (tag == 'blockquote') {
_isInBlockQuote = true;
}
if (tag == 'pre') {
_isInCodeblock = true;
}
if (tag == 'li') {
_listItemIndent++;
}
return true;
}
@override
void visitElementAfter(md.Element element) {
final tag = element.tag;
if (_isEmbedElement(element)) {
_delta.insert(_toEmbeddable(element).toJson());
}
if (tag == 'br') {
_delta.insert('\n');
}
// exit block with new line
// hr need to be followed by new line
_insertNewLineAfterElementIfNeeded(element);
if (tag == 'blockquote') {
_isInBlockQuote = false;
}
if (tag == 'pre') {
_isInCodeblock = false;
}
if (tag == 'li') {
_listItemIndent--;
}
if (_haveBlockAttrs(element)) {
_activeBlockAttributes.removeLast();
}
if (_haveInlineAttrs(element)) {
_activeInlineAttributes.removeLast();
}
if (_currentBlockTag == tag) {
_currentBlockTag = null;
}
_lastTag = tag;
}
void _insertNewLine() {
_delta.insert('\n', _effectiveBlockAttrs());
}
void _insertNewLineBeforeElementIfNeeded(md.Element element) {
if (!_isInBlockQuote &&
_lastTag == 'blockquote' &&
element.tag == 'blockquote') {
_insertNewLine();
return;
}
if (!_isInCodeblock && _lastTag == 'pre' && element.tag == 'pre') {
_insertNewLine();
return;
}
if (_listItemIndent >= 0 && (element.tag == 'ul' || element.tag == 'ol')) {
_insertNewLine();
return;
}
}
void _insertNewLineAfterElementIfNeeded(md.Element element) {
// TODO: refactor this to allow embeds to specify if they require
// new line after them
if (element.tag == 'hr' || element.tag == EmbeddableTable.tableType) {
// Always add new line after divider
_justPreviousBlockExit = true;
_insertNewLine();
return;
}
// if all the p children are embeddable add a new line
// example: images in a single line
if (element.tag == 'p' &&
(element.children?.every(
(child) => child is md.Element && _isEmbedElement(child),
) ??
false)) {
_justPreviousBlockExit = true;
_insertNewLine();
return;
}
if (!_justPreviousBlockExit &&
(_isTopLevelNode(element) ||
_haveBlockAttrs(element) ||
element.tag == 'li')) {
_justPreviousBlockExit = true;
_insertNewLine();
return;
}
}
bool _isTopLevelNode(md.Node node) => _topLevelNodes.contains(node);
Map<String, dynamic>? _effectiveBlockAttrs() {
if (_activeBlockAttributes.isEmpty) return null;
final attrsRespectingExclusivity = <Attribute<dynamic>>[
if (_listItemIndent > 0) IndentAttribute(level: _listItemIndent),
];
for (final attr in _activeBlockAttributes.expand((e) => e)) {
final isExclusiveAttr = Attribute.exclusiveBlockKeys.contains(
attr.key,
);
final isThereAlreadyExclusiveAttr = attrsRespectingExclusivity.any(
(element) => Attribute.exclusiveBlockKeys.contains(element.key),
);
if (!(isExclusiveAttr && isThereAlreadyExclusiveAttr)) {
attrsRespectingExclusivity.add(attr);
}
}
return <String, dynamic>{
for (final a in attrsRespectingExclusivity) ...a.toJson(),
};
}
Map<String, dynamic>? _effectiveInlineAttrs() {
if (_activeInlineAttributes.isEmpty) return null;
return <String, dynamic>{
for (final attrs in _activeInlineAttributes)
for (final a in attrs) ...a.toJson(),
};
}
// Define trim text function to remove spaces from text elements in
// accordance with Markdown specifications.
String _trimTextToMdSpec(String text) {
var result = text;
// The leading spaces pattern is used to identify spaces
// at the beginning of a line of text.
final leadingSpacesPattern = RegExp('^ *');
// The soft line break is used to identify the spaces at the end of a line
// of text and the leading spaces in the immediately following the line
// of text. These spaces are removed in accordance with the Markdown
// specification on soft line breaks when lines of text are joined.
final softLineBreak = RegExp(r' ?\n *');
// Leading spaces following a hard line break are ignored.
// https://github.github.com/gfm/#example-657
if (const ['p', 'ol', 'li', 'br'].contains(_lastTag)) {
result = result.replaceAll(leadingSpacesPattern, '');
}
if (softLineBreak.hasMatch(result)) {
return result;
}
return result.replaceAll(softLineBreak, ' ');
}
Map<String, ElementToAttributeConvertor> _effectiveElementToInlineAttr() {
return {
...customElementToInlineAttribute,
..._elementToInlineAttr,
};
}
bool _haveInlineAttrs(md.Element element) {
if (_isInCodeblock && element.tag == 'code') return false;
return _effectiveElementToInlineAttr().containsKey(element.tag);
}
List<Attribute<dynamic>> _toInlineAttributes(md.Element element) {
List<Attribute<dynamic>>? result;
if (!(_isInCodeblock && element.tag == 'code')) {
result = _effectiveElementToInlineAttr()[element.tag]?.call(element);
}
if (result == null) {
throw Exception(
'Element $element cannot be converted to inline attribute');
}
return result;
}
Map<String, ElementToAttributeConvertor> _effectiveElementToBlockAttr() {
return {
...customElementToBlockAttribute,
..._elementToBlockAttr,
};
}
bool _haveBlockAttrs(md.Element element) {
return _effectiveElementToBlockAttr().containsKey(element.tag);
}
List<Attribute<dynamic>> _toBlockAttributes(md.Element element) {
final result = _effectiveElementToBlockAttr()[element.tag]?.call(element);
if (result == null) {
throw Exception(
'Element $element cannot be converted to block attribute');
}
return result;
}
Map<String, ElementToEmbeddableConvertor> _effectiveElementToEmbed() {
return {
...customElementToEmbeddable,
..._elementToEmbed,
};
}
bool _isEmbedElement(md.Element element) =>
_effectiveElementToEmbed().containsKey(element.tag);
Embeddable _toEmbeddable(md.Element element) {
final result =
_effectiveElementToEmbed()[element.tag]?.call(element.attributes);
if (result == null) {
throw Exception('Element $element cannot be converted to Embeddable');
}
return result;
}
}

@ -0,0 +1,59 @@
//ignore_for_file: cast_nullable_to_non_nullable
import 'package:flutter_quill/flutter_quill.dart';
import './embeddable_table_syntax.dart';
/// To allow embedding images/videos in horizontal mode.
const BlockEmbed horizontalRule = BlockEmbed(horizontalRuleType, 'hr');
/// Necessary for [horizontalRule] BlockEmbed.
const String horizontalRuleType = 'divider';
/// Format the passed delta to ensure that there is new line
/// after embeds
Delta transform(Delta delta) {
final res = Delta();
final ops = delta.toList();
for (var i = 0; i < ops.length; i++) {
final op = ops[i];
res.push(op);
autoAppendNewlineAfterEmbeddable(i, ops, op, res, [
'hr',
EmbeddableTable.tableType,
]);
}
return res;
}
/// Appends new line after embeds if needed
void autoAppendNewlineAfterEmbeddable(
int i,
List<Operation> ops,
Operation op,
Delta res,
List<String> types,
) {
final nextOpIsEmbed = i + 1 < ops.length &&
ops[i + 1].isInsert &&
ops[i + 1].data is Map &&
types.any((type) => (ops[i + 1].data as Map).containsKey(type));
if (nextOpIsEmbed &&
op.data is String &&
(op.data as String).isNotEmpty &&
!(op.data as String).endsWith('\n')) {
res.push(Operation.insert('\n'));
}
// embed could be image or video
final opInsertEmbed = op.isInsert &&
op.data is Map &&
types.any((type) => (op.data as Map).containsKey(type));
final nextOpIsLineBreak = i + 1 < ops.length &&
ops[i + 1].isInsert &&
ops[i + 1].data is String &&
(ops[i + 1].data as String).startsWith('\n');
if (opInsertEmbed && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) {
// automatically append '\n' for embeddable
res.push(Operation.insert('\n'));
}
}

@ -2,7 +2,7 @@ import 'dart:math';
import '../models/documents/nodes/leaf.dart';
import '../models/structs/offset_value.dart';
import '../widgets/others/controller.dart';
import '../widgets/quill/quill_controller.dart';
OffsetValue<Embed> getEmbedNode(QuillController controller, int offset) {
var offset = controller.selection.start;

@ -18,7 +18,7 @@ import '../../utils/platform.dart';
import '../others/box.dart';
import '../others/cursor.dart';
import '../others/delegate.dart';
import '../others/embeds.dart';
import '../quill/embeds.dart';
import '../others/float_cursor.dart';
import '../others/text_selection.dart';
import '../raw_editor/raw_editor.dart';

@ -7,8 +7,8 @@ import '../../models/documents/attribute.dart';
import '../../models/documents/nodes/leaf.dart';
import '../../utils/platform.dart';
import '../editor/editor.dart';
import '../quill/embeds.dart';
import '../raw_editor/raw_editor.dart';
import 'embeds.dart';
import 'text_selection.dart';
typedef EmbedsBuilder = EmbedBuilder Function(Embed node);

@ -4,7 +4,7 @@ import '../../../extensions.dart';
import '../../models/documents/nodes/leaf.dart' as leaf;
import '../../models/themes/quill_dialog_theme.dart';
import '../../models/themes/quill_icon_theme.dart';
import 'controller.dart';
import 'quill_controller.dart';
abstract class EmbedBuilder {
const EmbedBuilder();

@ -1,8 +1,11 @@
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:html2md/html2md.dart' as html2md;
import 'package:markdown/markdown.dart' as md;
import '../../../markdown_quill.dart';
import '../../models/documents/attribute.dart';
import '../../models/documents/document.dart';
import '../../models/documents/nodes/embeddable.dart';
@ -42,7 +45,7 @@ class QuillController extends ChangeNotifier {
Document get document => _document;
set document(doc) {
set document(Document doc) {
_document = doc;
// Prevent the selection from
@ -51,6 +54,11 @@ class QuillController extends ChangeNotifier {
notifyListeners();
}
void updateDocument(Document newDocument) {
_document = newDocument;
notifyListeners();
}
/// Tells whether to keep or reset the [toggledStyle]
/// when user adds a new line.
final bool _keepStyleOnNewLine;
@ -439,4 +447,38 @@ class QuillController extends ChangeNotifier {
// Notify toolbar buttons directly with attributes
Map<String, Attribute> toolbarButtonToggler = const {};
/// Convert the HTML Raw string to [Delta]
///
/// It will run using the following steps:
///
/// 1. Convert the html to markdown string using `html2md` package
/// 2. Convert the markdown string to quill delta json string
/// 3. Decode the delta json string to [Delta]
///
/// for more [info](https://github.com/singerdmx/flutter-quill/issues/1100)
static Delta fromHtml(String html) {
final markdown = html2md
.convert(
html,
)
.replaceAll('unsafe:', '');
final mdDocument = md.Document(encodeHtml: false);
final mdToDelta = MarkdownToDelta(markdownDocument: mdDocument);
return mdToDelta.convert(markdown);
// final deltaJsonString = markdownToDelta(markdown);
// final deltaJson = jsonDecode(deltaJsonString);
// if (deltaJson is! List) {
// throw ArgumentError(
// 'The delta json string should be of type list when jsonDecode() it',
// );
// }
// return Delta.fromJson(
// deltaJson,
// );
}
}

@ -11,14 +11,14 @@ import '../editor/editor.dart';
import '../style_widgets/bullet_point.dart';
import '../style_widgets/checkbox_point.dart';
import '../style_widgets/number_point.dart';
import 'box.dart';
import 'controller.dart';
import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart';
import 'link.dart';
import '../others/box.dart';
import 'quill_controller.dart';
import '../others/cursor.dart';
import '../others/default_styles.dart';
import '../others/delegate.dart';
import '../others/link.dart';
import 'text_line.dart';
import 'text_selection.dart';
import '../others/text_selection.dart';
const List<int> arabianRomanNumbers = [
1000,

@ -20,15 +20,15 @@ import '../../models/structs/vertical_spacing.dart';
import '../../utils/color.dart';
import '../../utils/font.dart';
import '../../utils/platform.dart';
import 'box.dart';
import 'controller.dart';
import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart';
import 'keyboard_listener.dart';
import 'link.dart';
import 'proxy.dart';
import 'text_selection.dart';
import '../others/box.dart';
import 'quill_controller.dart';
import '../others/cursor.dart';
import '../others/default_styles.dart';
import '../others/delegate.dart';
import '../others/keyboard_listener.dart';
import '../others/link.dart';
import '../others/proxy.dart';
import '../others/text_selection.dart';
class TextLine extends StatefulWidget {
const TextLine({
@ -163,7 +163,7 @@ class _TextLineState extends State<TextLine> {
);
}
}
final textSpan = _getTextSpanForWholeLine(context);
final textSpan = _getTextSpanForWholeLine();
final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
final textAlign = _getTextAlign();
final child = RichText(
@ -185,7 +185,7 @@ class _TextLineState extends State<TextLine> {
);
}
InlineSpan _getTextSpanForWholeLine(BuildContext context) {
InlineSpan _getTextSpanForWholeLine() {
final lineStyle = _getLineStyle(widget.styles);
if (!widget.line.hasEmbed) {
return _buildTextSpan(widget.styles, widget.line.children, lineStyle);

@ -28,6 +28,7 @@ import '../../models/documents/nodes/embeddable.dart';
import '../../models/documents/nodes/leaf.dart' as leaf;
import '../../models/documents/nodes/line.dart';
import '../../models/documents/nodes/node.dart';
import '../../models/quill_delta.dart';
import '../../models/structs/offset_value.dart';
import '../../models/structs/vertical_spacing.dart';
import '../../utils/cast.dart';
@ -35,14 +36,14 @@ import '../../utils/delta.dart';
import '../../utils/embeds.dart';
import '../../utils/platform.dart';
import '../editor/editor.dart';
import '../others/controller.dart';
import '../quill/quill_controller.dart';
import '../others/cursor.dart';
import '../others/default_styles.dart';
import '../others/keyboard_listener.dart';
import '../others/link.dart';
import '../others/proxy.dart';
import '../others/text_block.dart';
import '../others/text_line.dart';
import '../quill/text_block.dart';
import '../quill/text_line.dart';
import '../others/text_selection.dart';
import 'quill_single_child_scroll_view.dart';
import 'raw_editor.dart';
@ -224,14 +225,65 @@ class QuillRawEditorState extends EditorState
if (!selection.isValid) {
return;
}
// TODO: Could be improved
Delta? deltaFromCliboard;
final reader = await ClipboardReader.readClipboard();
if (reader.canProvide(Formats.htmlText)) {
final html = await reader.readValue(Formats.htmlText);
if (html == null) {
return;
}
deltaFromCliboard = QuillController.fromHtml(html);
}
if (deltaFromCliboard != null) {
// final index = selection.baseOffset;
// final length = selection.extentOffset - index;
final list = controller.document.toDelta().toList()
..insertAll(controller.document.toDelta().toList().length - 1,
deltaFromCliboard.toList());
final delta = controller.document.toDelta();
for (final operation in list) {
delta.push(operation);
}
controller
..updateDocument(
Document.fromDelta(delta),
)
..updateSelection(
TextSelection.collapsed(
offset: controller.document.length,
),
ChangeSource.local,
);
bringIntoView(textEditingValue.selection.extent);
// Collapse the selection and hide the toolbar and handles.
userUpdateTextEditingValue(
TextEditingValue(
text: textEditingValue.text,
selection: TextSelection.collapsed(
offset: textEditingValue.selection.end,
),
),
cause,
);
return;
}
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final text = await Clipboard.getData(Clipboard.kTextPlain);
if (text != null) {
final plainText = await Clipboard.getData(Clipboard.kTextPlain);
if (plainText != null) {
_replaceText(
ReplaceTextIntent(
textEditingValue,
text.text!,
plainText.text!,
selection,
cause,
),
@ -1693,6 +1745,8 @@ class QuillRawEditorState extends EditorState
}
}
// TODO: Review those
@override
bool get liveTextInputEnabled => false;

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../extensions/quill_configurations_ext.dart';
import '../../models/documents/attribute.dart';
import '../others/text_block.dart';
import '../quill/text_block.dart';
class QuillEditorNumberPoint extends StatelessWidget {
const QuillEditorNumberPoint({

@ -4,7 +4,7 @@ import '../../../extensions/quill_configurations_ext.dart';
import '../../../l10n/extensions/localizations.dart';
import '../../../models/documents/attribute.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import '../base_toolbar.dart';
class QuillToolbarClearFormatButton extends StatelessWidget {

@ -7,7 +7,7 @@ import '../../../../models/documents/attribute.dart';
import '../../../../models/documents/style.dart';
import '../../../../models/themes/quill_icon_theme.dart';
import '../../../../utils/color.dart';
import '../../../others/controller.dart';
import '../../../quill/quill_controller.dart';
import '../../base_toolbar.dart';
import 'color_dialog.dart';

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../../extensions/quill_configurations_ext.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import '../base_toolbar.dart';
class QuillToolbarCustomButton extends StatelessWidget {

@ -7,7 +7,7 @@ import '../../../models/config/toolbar/buttons/font_family_configurations.dart';
import '../../../models/documents/attribute.dart';
import '../../../models/documents/style.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
class QuillToolbarFontFamilyButton extends StatefulWidget {
QuillToolbarFontFamilyButton({

@ -8,7 +8,7 @@ import '../../../models/documents/attribute.dart';
import '../../../models/documents/style.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../../utils/font.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
class QuillToolbarFontSizeButton extends StatefulWidget {
QuillToolbarFontSizeButton({

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../../extensions/quill_configurations_ext.dart';
import '../../../l10n/extensions/localizations.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import '../base_toolbar.dart';
class QuillToolbarHistoryButton extends StatefulWidget {

@ -4,7 +4,7 @@ import '../../../extensions/quill_configurations_ext.dart';
import '../../../l10n/extensions/localizations.dart';
import '../../../models/config/toolbar/buttons/indent_configurations.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import '../base_toolbar.dart'
show QuillToolbarBaseButtonOptions, QuillToolbarIconButton;

@ -10,7 +10,7 @@ import '../../../l10n/widgets/localizations.dart';
import '../../../models/documents/attribute.dart';
import '../../../models/themes/quill_dialog_theme.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import '../../others/link.dart';
import '../base_toolbar.dart';

@ -8,7 +8,7 @@ import '../../../models/rules/insert.dart';
import '../../../models/structs/link_dialog_action.dart';
import '../../../models/themes/quill_dialog_theme.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import '../../others/link.dart';
import '../base_toolbar.dart';

@ -5,7 +5,7 @@ import '../../../../l10n/extensions/localizations.dart';
import '../../../../l10n/widgets/localizations.dart';
import '../../../../models/themes/quill_dialog_theme.dart';
import '../../../../models/themes/quill_icon_theme.dart';
import '../../../others/controller.dart';
import '../../../quill/quill_controller.dart';
import '../../base_toolbar.dart';
class QuillToolbarSearchButton extends StatelessWidget {

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import '../../../../l10n/extensions/localizations.dart';
import '../../../../models/documents/document.dart';
import '../../../../models/themes/quill_dialog_theme.dart';
import '../../../others/controller.dart';
import '../../../quill/quill_controller.dart';
@immutable
class QuillToolbarSearchDialogChildBuilderExtraOptions {

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import '../../../models/config/toolbar/buttons/select_alignment_configurations.dart';
import '../../../models/documents/attribute.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import 'toggle_style_button.dart';
enum _AlignmentOptions {

@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import '../../../../translations.dart';
import '../../../models/config/toolbar/buttons/select_header_style_configurations.dart';
import '../../../models/documents/attribute.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
enum _HeaderStyleOptions {
normal,

@ -7,7 +7,7 @@ import '../../../l10n/extensions/localizations.dart';
import '../../../models/documents/attribute.dart';
import '../../../models/documents/style.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import '../base_toolbar.dart';
class QuillToolbarSelectHeaderStyleButtons extends StatefulWidget {

@ -8,7 +8,7 @@ import '../../../models/documents/attribute.dart';
import '../../../models/documents/style.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../../utils/widgets.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import 'toggle_style_button.dart';
class QuillToolbarToggleCheckListButton extends StatefulWidget {

@ -6,7 +6,7 @@ import '../../../models/documents/attribute.dart';
import '../../../models/documents/style.dart';
import '../../../models/themes/quill_icon_theme.dart';
import '../../../utils/widgets.dart';
import '../../others/controller.dart';
import '../../quill/quill_controller.dart';
import '../base_toolbar.dart';
typedef ToggleStyleButtonBuilder = Widget Function(

@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
## 9.0.0-dev-7
* Fix a bug in chaning the background/font color of ol/ul list
* Better support for pasting HTML contents from external websites to the editor
* The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`)
* Flutter Quill Extensions:
* Fix link bug in the video url
* Fix patterns

@ -1,11 +1,6 @@
library quill_html_converter;
import 'dart:convert' show jsonDecode;
import 'package:delta_markdown_converter/delta_markdown_converter.dart'
as delta_markdown show markdownToDelta;
import 'package:flutter_quill/flutter_quill.dart' show Delta;
import 'package:html2md/html2md.dart' as html2md;
import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart'
as conventer show ConverterOptions, QuillDeltaToHtmlConverter;
@ -30,31 +25,4 @@ extension DeltaHtmlExt on Delta {
).convert();
return html;
}
/// Convert the HTML Raw string to [Delta]
///
/// It will run using the following steps:
///
/// 1. Convert the html to markdown string using `html2md` package
/// 2. Convert the markdown string to quill delta json string
/// 3. Decode the delta json string to [Delta]
///
/// for more [info](https://github.com/singerdmx/flutter-quill/issues/1100)
static Delta fromHtml(String html) {
final markdown = html2md
.convert(
html,
)
.replaceAll('unsafe:', '');
final deltaJsonString = delta_markdown.markdownToDelta(markdown);
final deltaJson = jsonDecode(deltaJsonString);
if (deltaJson is! List) {
throw ArgumentError(
'The delta json string should be of type list when jsonDecode() it',
);
}
return Delta.fromJson(
deltaJson,
);
}
}

@ -20,11 +20,14 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_quill: ^7.10.2
flutter_quill: ^9.0.0-dev-6
vsc_quill_delta_to_html: ^1.0.3
html2md: ^1.3.1
# markdown: ^7.1.1
delta_markdown_converter: ^0.0.2
# delta_markdown_converter: ^0.0.3-dev
markdown: ^7.1.1
charcode: ^1.3.1
collection: ^1.18.0
dev_dependencies:
flutter_test:

@ -52,11 +52,16 @@ dependencies:
equatable: ^2.0.5
meta: ^1.9.1
# For Quill HTML
markdown: ^7.1.1
html2md: ^1.3.1
# Plugins
url_launcher: ^6.1.14
flutter_keyboard_visibility: ^5.4.1
device_info_plus: ^9.1.0
super_clipboard: ^0.7.3
charcode: ^1.3.1
dev_dependencies:
flutter_lints: ^3.0.1

Loading…
Cancel
Save