Merge branch 'singerdmx:master' into master

pull/1145/head
Cierra_Runis 2 years ago committed by GitHub
commit 44674977fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      CHANGELOG.md
  2. 8
      README.md
  3. 5
      doc_cn.md
  4. 67
      example/lib/pages/home_page.dart
  5. 22
      example/lib/universal_ui/universal_ui.dart
  6. 30
      flutter_quill_extensions/lib/embeds/builders.dart
  7. 12
      flutter_quill_extensions/pubspec.yaml
  8. 5
      lib/flutter_quill.dart
  9. 31
      lib/src/models/documents/document.dart
  10. 22
      lib/src/models/documents/history.dart
  11. 10
      lib/src/models/documents/nodes/line.dart
  12. 32
      lib/src/models/rules/insert.dart
  13. 19
      lib/src/models/structs/doc_change.dart
  14. 9
      lib/src/models/structs/history_changed.dart
  15. 9
      lib/src/models/structs/image_url.dart
  16. 5
      lib/src/models/structs/offset_value.dart
  17. 14
      lib/src/models/structs/optional_size.dart
  18. 9
      lib/src/models/structs/segment_leaf_node.dart
  19. 9
      lib/src/models/structs/vertical_spacing.dart
  20. 35
      lib/src/translations/toolbar.i18n.dart
  21. 7
      lib/src/utils/embeds.dart
  22. 34
      lib/src/widgets/controller.dart
  23. 48
      lib/src/widgets/default_styles.dart
  24. 12
      lib/src/widgets/delegate.dart
  25. 47
      lib/src/widgets/editor.dart
  26. 6
      lib/src/widgets/embeds.dart
  27. 44
      lib/src/widgets/raw_editor.dart
  28. 6
      lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart
  29. 5
      lib/src/widgets/style_widgets/number_point.dart
  30. 32
      lib/src/widgets/text_block.dart
  31. 64
      lib/src/widgets/text_line.dart
  32. 5
      lib/src/widgets/toolbar/clear_format_button.dart
  33. 4
      lib/src/widgets/toolbar/history_button.dart
  34. 4
      lib/src/widgets/toolbar/indent_button.dart
  35. 30
      lib/src/widgets/toolbar/link_style_button.dart
  36. 5
      pubspec.yaml

@ -1,3 +1,15 @@
# [7.0.3]
* Fix ordered list numeration for lists with more than one level of list.
# [7.0.2]
* Allow widgets to override widget span properties.
# [7.0.1]
* Update i18n_extension dependency to version 8.0.0.
# [7.0.0]
* Breaking change: Tuples are no longer used. They have been replaced with a number of data classes.
# [6.4.4]
* Increased compatibility with Flutter widget tests.

@ -241,7 +241,7 @@ After that, we need to map this "notes" type into a widget. In that case, I used
Don't forget to add this method to the `QuillEditor` after that!
```dart
class NotesEmbedBuilder implements EmbedBuilder {
class NotesEmbedBuilder extends EmbedBuilder {
NotesEmbedBuilder({required this.addEditNote});
Future<void> Function(BuildContext context, {Document? document}) addEditNote;
@ -255,6 +255,7 @@ class NotesEmbedBuilder implements EmbedBuilder {
QuillController controller,
Embed node,
bool readOnly,
bool inline,
) {
final notes = NotesBlockEmbed(node.value.data).document;
@ -319,7 +320,7 @@ Future<void> _addEditNote(BuildContext context, {Document? document}) async {
final length = controller.selection.extentOffset - index;
if (isEditing) {
final offset = getEmbedNode(controller, controller.selection.start).item1;
final offset = getEmbedNode(controller, controller.selection.start).offset;
controller.replaceText(
offset, 1, block, TextSelection.collapsed(offset: offset));
} else {
@ -346,7 +347,7 @@ QuillToolbar(locale: Locale('fr'), ...)
QuillEditor(locale: Locale('fr'), ...)
```
Currently, translations are available for these 26 locales:
Currently, translations are available for these 27 locales:
* `Locale('en')`
* `Locale('ar')`
@ -367,6 +368,7 @@ Currently, translations are available for these 26 locales:
* `Locale('pl')`
* `Locale('vi')`
* `Locale('id')`
* `Locale('it')`
* `Locale('ms')`
* `Locale('nl')`
* `Locale('no')`

@ -246,7 +246,7 @@ class NotesBlockEmbed extends CustomBlockEmbed {
在这里我们使用 `ListTile` 来渲染它,并使用 `onTap` 方法来编辑内容,最后不要忘记将此方法添加到 `QuillEditor`
```dart
class NotesEmbedBuilder implements EmbedBuilder {
class NotesEmbedBuilder extends EmbedBuilder {
NotesEmbedBuilder({required this.addEditNote});
Future<void> Function(BuildContext context, {Document? document}) addEditNote;
@ -260,6 +260,7 @@ class NotesEmbedBuilder implements EmbedBuilder {
QuillController controller,
Embed node,
bool readOnly,
bool inline,
) {
final notes = NotesBlockEmbed(node.value.data).document;
@ -328,7 +329,7 @@ Future<void> _addEditNote(BuildContext context, {Document? document}) async {
final length = controller.selection.extentOffset - index;
if (isEditing) {
final offset = getEmbedNode(controller, controller.selection.start).item1;
final offset = getEmbedNode(controller, controller.selection.start).offset;
controller.replaceText(
offset, 1, block, TextSelection.collapsed(offset: offset));
} else {

@ -12,7 +12,6 @@ import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:flutter_quill_extensions/flutter_quill_extensions.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tuple/tuple.dart';
import '../universal_ui/universal_ui.dart';
import 'read_only_page.dart';
@ -184,8 +183,8 @@ class _HomePageState extends State<HomePage> {
height: 1.15,
fontWeight: FontWeight.w300,
),
const Tuple2(16, 0),
const Tuple2(0, 0),
const VerticalSpacing(16, 0),
const VerticalSpacing(0, 0),
null),
sizeSmall: const TextStyle(fontSize: 9),
),
@ -199,33 +198,35 @@ class _HomePageState extends State<HomePage> {
quillEditor = MouseRegion(
cursor: SystemMouseCursors.text,
child: QuillEditor(
controller: _controller!,
scrollController: ScrollController(),
scrollable: true,
focusNode: _focusNode,
autoFocus: false,
readOnly: false,
placeholder: 'Add content',
expands: false,
padding: EdgeInsets.zero,
onTapUp: (details, p1) {
return _onTripleClickSelection();
},
customStyles: DefaultStyles(
h1: DefaultTextBlockStyle(
const TextStyle(
fontSize: 32,
color: Colors.black,
height: 1.15,
fontWeight: FontWeight.w300,
),
const Tuple2(16, 0),
const Tuple2(0, 0),
null),
sizeSmall: const TextStyle(fontSize: 9),
),
embedBuilders: defaultEmbedBuildersWeb,
),
controller: _controller!,
scrollController: ScrollController(),
scrollable: true,
focusNode: _focusNode,
autoFocus: false,
readOnly: false,
placeholder: 'Add content',
expands: false,
padding: EdgeInsets.zero,
onTapUp: (details, p1) {
return _onTripleClickSelection();
},
customStyles: DefaultStyles(
h1: DefaultTextBlockStyle(
const TextStyle(
fontSize: 32,
color: Colors.black,
height: 1.15,
fontWeight: FontWeight.w300,
),
const VerticalSpacing(16, 0),
const VerticalSpacing(0, 0),
null),
sizeSmall: const TextStyle(fontSize: 9),
),
embedBuilders: [
...defaultEmbedBuildersWeb,
NotesEmbedBuilder(addEditNote: _addEditNote),
]),
);
}
var toolbar = QuillToolbar.basic(
@ -476,7 +477,8 @@ class _HomePageState extends State<HomePage> {
final length = controller.selection.extentOffset - index;
if (isEditing) {
final offset = getEmbedNode(controller, controller.selection.start).item1;
final offset =
getEmbedNode(controller, controller.selection.start).offset;
controller.replaceText(
offset, 1, block, TextSelection.collapsed(offset: offset));
} else {
@ -485,7 +487,7 @@ class _HomePageState extends State<HomePage> {
}
}
class NotesEmbedBuilder implements EmbedBuilder {
class NotesEmbedBuilder extends EmbedBuilder {
NotesEmbedBuilder({required this.addEditNote});
Future<void> Function(BuildContext context, {Document? document}) addEditNote;
@ -499,6 +501,7 @@ class NotesEmbedBuilder implements EmbedBuilder {
QuillController controller,
Embed node,
bool readOnly,
bool inline,
) {
final notes = NotesBlockEmbed(node.value.data).document;

@ -27,7 +27,7 @@ class UniversalUI {
var ui = UniversalUI();
class ImageEmbedBuilderWeb implements EmbedBuilder {
class ImageEmbedBuilderWeb extends EmbedBuilder {
@override
String get key => BlockEmbed.imageType;
@ -37,6 +37,7 @@ class ImageEmbedBuilderWeb implements EmbedBuilder {
QuillController controller,
Embed node,
bool readOnly,
bool inline,
) {
final imageUrl = node.value.data;
if (isImageBase64(imageUrl)) {
@ -44,8 +45,12 @@ class ImageEmbedBuilderWeb implements EmbedBuilder {
return const SizedBox();
}
final size = MediaQuery.of(context).size;
UniversalUI().platformViewRegistry.registerViewFactory(
imageUrl, (viewId) => html.ImageElement()..src = imageUrl);
UniversalUI().platformViewRegistry.registerViewFactory(imageUrl, (viewId) {
return html.ImageElement()
..src = imageUrl
..style.height = 'auto'
..style.width = 'auto';
});
return Padding(
padding: EdgeInsets.only(
right: ResponsiveWidget.isMediumScreen(context)
@ -64,13 +69,18 @@ class ImageEmbedBuilderWeb implements EmbedBuilder {
}
}
class VideoEmbedBuilderWeb implements EmbedBuilder {
class VideoEmbedBuilderWeb extends EmbedBuilder {
@override
String get key => BlockEmbed.videoType;
@override
Widget build(BuildContext context, QuillController controller, Embed node,
bool readOnly) {
Widget build(
BuildContext context,
QuillController controller,
Embed node,
bool readOnly,
bool inline,
) {
var videoUrl = node.value.data;
if (videoUrl.contains('youtube.com') || videoUrl.contains('youtu.be')) {
final youtubeID = YoutubePlayer.convertUrlToId(videoUrl);

@ -7,7 +7,6 @@ import 'package:flutter_quill/flutter_quill.dart' hide Text;
import 'package:flutter_quill/translations.dart';
import 'package:gallery_saver/gallery_saver.dart';
import 'package:math_keyboard/math_keyboard.dart';
import 'package:tuple/tuple.dart';
import 'utils.dart';
import 'widgets/image.dart';
@ -15,7 +14,7 @@ import 'widgets/image_resizer.dart';
import 'widgets/video_app.dart';
import 'widgets/youtube_video_app.dart';
class ImageEmbedBuilder implements EmbedBuilder {
class ImageEmbedBuilder extends EmbedBuilder {
@override
String get key => BlockEmbed.imageType;
@ -25,12 +24,13 @@ class ImageEmbedBuilder implements EmbedBuilder {
QuillController controller,
base.Embed node,
bool readOnly,
bool inline,
) {
assert(!kIsWeb, 'Please provide image EmbedBuilder for Web');
var image;
final imageUrl = standardizeImageUrl(node.value.data);
Tuple2<double?, double?>? _widthHeight;
OptionalSize? _imageSize;
final style = node.style.attributes['style'];
if (base.isMobile() && style != null) {
final _attrs = base.parseKeyValuePairs(style.value.toString(), {
@ -46,7 +46,7 @@ class ImageEmbedBuilder implements EmbedBuilder {
'mobileWidth and mobileHeight must be specified');
final w = double.parse(_attrs[Attribute.mobileWidth]!);
final h = double.parse(_attrs[Attribute.mobileHeight]!);
_widthHeight = Tuple2(w, h);
_imageSize = OptionalSize(w, h);
final m = _attrs[Attribute.mobileMargin] == null
? 0.0
: double.parse(_attrs[Attribute.mobileMargin]!);
@ -57,9 +57,9 @@ class ImageEmbedBuilder implements EmbedBuilder {
}
}
if (_widthHeight == null) {
if (_imageSize == null) {
image = imageByUrl(imageUrl);
_widthHeight = Tuple2((image as Image).width, image.height);
_imageSize = OptionalSize((image as Image).width, image.height);
}
if (!readOnly && base.isMobile()) {
@ -87,10 +87,10 @@ class ImageEmbedBuilder implements EmbedBuilder {
controller
..skipRequestKeyboard = true
..formatText(
res.item1, 1, StyleAttribute(attr));
res.offset, 1, StyleAttribute(attr));
},
imageWidth: _widthHeight?.item1,
imageHeight: _widthHeight?.item2,
imageWidth: _imageSize?.width,
imageHeight: _imageSize?.height,
maxWidth: _screenSize.width,
maxHeight: _screenSize.height);
});
@ -103,10 +103,10 @@ class ImageEmbedBuilder implements EmbedBuilder {
onPressed: () {
final imageNode =
getEmbedNode(controller, controller.selection.start)
.item2;
.value;
final imageUrl = imageNode.value.data;
controller.copiedImageUrl =
Tuple2(imageUrl, getImageStyleString(controller));
ImageUrl(imageUrl, getImageStyleString(controller));
Navigator.pop(context);
},
);
@ -117,7 +117,7 @@ class ImageEmbedBuilder implements EmbedBuilder {
onPressed: () {
final offset =
getEmbedNode(controller, controller.selection.start)
.item1;
.offset;
controller.replaceText(offset, 1, '',
TextSelection.collapsed(offset: offset));
Navigator.pop(context);
@ -145,7 +145,7 @@ class ImageEmbedBuilder implements EmbedBuilder {
}
}
class VideoEmbedBuilder implements EmbedBuilder {
class VideoEmbedBuilder extends EmbedBuilder {
VideoEmbedBuilder({this.onVideoInit});
final void Function(GlobalKey videoContainerKey)? onVideoInit;
@ -159,6 +159,7 @@ class VideoEmbedBuilder implements EmbedBuilder {
QuillController controller,
base.Embed node,
bool readOnly,
bool inline,
) {
assert(!kIsWeb, 'Please provide video EmbedBuilder for Web');
@ -176,7 +177,7 @@ class VideoEmbedBuilder implements EmbedBuilder {
}
}
class FormulaEmbedBuilder implements EmbedBuilder {
class FormulaEmbedBuilder extends EmbedBuilder {
@override
String get key => BlockEmbed.formulaType;
@ -186,6 +187,7 @@ class FormulaEmbedBuilder implements EmbedBuilder {
QuillController controller,
base.Embed node,
bool readOnly,
bool inline,
) {
assert(!kIsWeb, 'Please provide formula EmbedBuilder for Web');

@ -12,19 +12,19 @@ dependencies:
flutter:
sdk: flutter
flutter_quill: ^6.0.0
flutter_quill: ^7.0.2
image_picker: ^0.8.5+3
photo_view: ^0.14.0
video_player: ^2.4.2
youtube_player_flutter: ^8.1.1
gallery_saver: ^2.3.2
math_keyboard: ^0.1.6
string_validator: ^0.3.0
math_keyboard: ^0.1.8
string_validator: ^1.0.0
#dependency_overrides:
# flutter_quill:
# path: ../
# dependency_overrides:
# flutter_quill:
# path: ../
dev_dependencies:
flutter_test:

@ -9,6 +9,11 @@ export 'src/models/documents/nodes/line.dart';
export 'src/models/documents/nodes/node.dart';
export 'src/models/documents/style.dart';
export 'src/models/quill_delta.dart';
export 'src/models/structs/doc_change.dart';
export 'src/models/structs/image_url.dart';
export 'src/models/structs/offset_value.dart';
export 'src/models/structs/optional_size.dart';
export 'src/models/structs/vertical_spacing.dart';
export 'src/models/themes/quill_custom_button.dart';
export 'src/models/themes/quill_dialog_theme.dart';
export 'src/models/themes/quill_icon_theme.dart';

@ -1,9 +1,11 @@
import 'dart:async';
import 'package:tuple/tuple.dart';
import '../quill_delta.dart';
import '../rules/rule.dart';
import '../structs/doc_change.dart';
import '../structs/history_changed.dart';
import '../structs/offset_value.dart';
import '../structs/segment_leaf_node.dart';
import 'attribute.dart';
import 'history.dart';
import 'nodes/block.dart';
@ -50,13 +52,12 @@ class Document {
_rules.setCustomRules(customRules);
}
final StreamController<Tuple3<Delta, Delta, ChangeSource>> _observer =
StreamController.broadcast();
final StreamController<DocChange> _observer = StreamController.broadcast();
final History _history = History();
/// Stream of [Change]s applied to this document.
Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => _observer.stream;
/// Stream of [DocChange]s applied to this document.
Stream<DocChange> get changes => _observer.stream;
/// Inserts [data] in this document at specified [index].
///
@ -158,7 +159,7 @@ class Document {
}
/// Returns all styles for each node within selection
List<Tuple2<int, Style>> collectAllIndividualStyles(int index, int len) {
List<OffsetValue<Style>> collectAllIndividualStyles(int index, int len) {
final res = queryChild(index);
return (res.node as Line).collectAllIndividualStyles(res.offset, len);
}
@ -216,19 +217,15 @@ class Document {
}
/// Given offset, find its leaf node in document
Tuple2<Line?, Leaf?> querySegmentLeafNode(int offset) {
SegmentLeafNode querySegmentLeafNode(int offset) {
final result = queryChild(offset);
if (result.node == null) {
return const Tuple2(null, null);
return const SegmentLeafNode(null, null);
}
final line = result.node as Line;
final segmentResult = line.queryChild(result.offset, false);
if (segmentResult.node == null) {
return Tuple2(line, null);
}
final segment = segmentResult.node as Leaf;
return Tuple2(line, segment);
return SegmentLeafNode(line, segmentResult.node as Leaf?);
}
/// Composes [change] Delta into this document.
@ -276,16 +273,16 @@ class Document {
if (_delta != _root.toDelta()) {
throw 'Compose failed';
}
final change = Tuple3(originalDelta, delta, changeSource);
final change = DocChange(originalDelta, delta, changeSource);
_observer.add(change);
_history.handleDocChange(change);
}
Tuple2 undo() {
HistoryChanged undo() {
return _history.undo(this);
}
Tuple2 redo() {
HistoryChanged redo() {
return _history.redo(this);
}

@ -1,6 +1,6 @@
import 'package:tuple/tuple.dart';
import '../quill_delta.dart';
import '../structs/doc_change.dart';
import '../structs/history_changed.dart';
import 'document.dart';
class History {
@ -32,12 +32,12 @@ class History {
///record delay
final int interval;
void handleDocChange(Tuple3<Delta, Delta, ChangeSource> change) {
void handleDocChange(DocChange docChange) {
if (ignoreChange) return;
if (!userOnly || change.item3 == ChangeSource.LOCAL) {
record(change.item2, change.item1);
if (!userOnly || docChange.source == ChangeSource.LOCAL) {
record(docChange.change, docChange.before);
} else {
transform(change.item2);
transform(docChange.change);
}
}
@ -85,9 +85,9 @@ class History {
}
}
Tuple2 _change(Document doc, List<Delta> source, List<Delta> dest) {
HistoryChanged _change(Document doc, List<Delta> source, List<Delta> dest) {
if (source.isEmpty) {
return const Tuple2(false, 0);
return const HistoryChanged(false, 0);
}
final delta = source.removeLast();
// look for insert or delete
@ -107,14 +107,14 @@ class History {
ignoreChange = true;
doc.compose(delta, ChangeSource.LOCAL);
ignoreChange = false;
return Tuple2(true, len);
return HistoryChanged(true, len);
}
Tuple2 undo(Document doc) {
HistoryChanged undo(Document doc) {
return _change(doc, stack.undo, stack.redo);
}
Tuple2 redo(Document doc) {
HistoryChanged redo(Document doc) {
return _change(doc, stack.redo, stack.undo);
}
}

@ -1,9 +1,9 @@
import 'dart:math' as math;
import 'package:collection/collection.dart';
import 'package:tuple/tuple.dart';
import '../../quill_delta.dart';
import '../../structs/offset_value.dart';
import '../attribute.dart';
import '../style.dart';
import 'block.dart';
@ -391,10 +391,10 @@ class Line extends Container<Leaf?> {
/// Returns each node segment's offset in selection
/// with its corresponding style as a list
List<Tuple2<int, Style>> collectAllIndividualStyles(int offset, int len,
List<OffsetValue<Style>> collectAllIndividualStyles(int offset, int len,
{int beg = 0}) {
final local = math.min(length - offset, len);
final result = <Tuple2<int, Style>>[];
final result = <OffsetValue<Style>>[];
final data = queryChild(offset, true);
var node = data.node as Leaf?;
@ -402,12 +402,12 @@ class Line extends Container<Leaf?> {
var pos = 0;
if (node is Text) {
pos = node.length - data.offset;
result.add(Tuple2(beg, node.style));
result.add(OffsetValue(beg, node.style));
}
while (!node!.isLast && pos < local) {
node = node.next as Leaf;
if (node is Text) {
result.add(Tuple2(pos + beg, node.style));
result.add(OffsetValue(pos + beg, node.style));
pos += node.length;
}
}

@ -1,5 +1,3 @@
import 'package:tuple/tuple.dart';
import '../../models/documents/document.dart';
import '../documents/attribute.dart';
import '../documents/nodes/embeddable.dart';
@ -56,7 +54,7 @@ class PreserveLineStyleOnSplitRule extends InsertRule {
return delta;
}
final nextNewLine = _getNextNewLine(itr);
final attributes = nextNewLine.item1?.attributes;
final attributes = nextNewLine.operation?.attributes;
return delta..insert('\n', attributes);
}
@ -85,8 +83,8 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
// Look for the next newline.
final nextNewLine = _getNextNewLine(itr);
final lineStyle =
Style.fromJson(nextNewLine.item1?.attributes ?? <String, dynamic>{});
final lineStyle = Style.fromJson(
nextNewLine.operation?.attributes ?? <String, dynamic>{});
final blockStyle = lineStyle.getBlocksExceptHeader();
// Are we currently in a block? If not then ignore.
@ -126,8 +124,8 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
// Reset style of the original newline character if needed.
if (resetStyle.isNotEmpty) {
delta
..retain(nextNewLine.item2!)
..retain((nextNewLine.item1!.data as String).indexOf('\n'))
..retain(nextNewLine.skipped!)
..retain((nextNewLine.operation!.data as String).indexOf('\n'))
..retain(1, resetStyle);
}
@ -188,9 +186,10 @@ class AutoExitBlockRule extends InsertRule {
// Keep looking for the next newline character to see if it shares the same
// block style as `cur`.
final nextNewLine = _getNextNewLine(itr);
if (nextNewLine.item1 != null &&
nextNewLine.item1!.attributes != null &&
Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() ==
if (nextNewLine.operation != null &&
nextNewLine.operation!.attributes != null &&
Style.fromJson(nextNewLine.operation!.attributes)
.getBlockExceptHeader() ==
blockStyle) {
// We are not at the end of this block, ignore.
return null;
@ -524,15 +523,22 @@ class CatchAllInsertRule extends InsertRule {
}
}
Tuple2<Operation?, int?> _getNextNewLine(DeltaIterator iterator) {
_NextNewLine _getNextNewLine(DeltaIterator iterator) {
Operation op;
for (var skipped = 0; iterator.hasNext; skipped += op.length!) {
op = iterator.next();
final lineBreak =
(op.data is String ? op.data as String? : '')!.indexOf('\n');
if (lineBreak >= 0) {
return Tuple2(op, skipped);
return _NextNewLine(op, skipped);
}
}
return const Tuple2(null, null);
return const _NextNewLine(null, null);
}
class _NextNewLine {
const _NextNewLine(this.operation, this.skipped);
final Operation? operation;
final int? skipped;
}

@ -0,0 +1,19 @@
import '../documents/document.dart';
import '../quill_delta.dart';
class DocChange {
DocChange(
this.before,
this.change,
this.source,
);
/// Document state before [change].
final Delta before;
/// Change delta applied to the document.
final Delta change;
/// The source of this change.
final ChangeSource source;
}

@ -0,0 +1,9 @@
class HistoryChanged {
const HistoryChanged(
this.changed,
this.len,
);
final bool changed;
final int? len;
}

@ -0,0 +1,9 @@
class ImageUrl {
const ImageUrl(
this.url,
this.styleString,
);
final String url;
final String styleString;
}

@ -0,0 +1,5 @@
class OffsetValue<T> {
OffsetValue(this.offset, this.value);
final int offset;
final T value;
}

@ -0,0 +1,14 @@
class OptionalSize {
OptionalSize(
this.width,
this.height,
);
/// If non-null, requires the child to have exactly this width.
/// If null, the child is free to choose its own width.
final double? width;
/// If non-null, requires the child to have exactly this height.
/// If null, the child is free to choose its own height.
final double? height;
}

@ -0,0 +1,9 @@
import '../documents/nodes/leaf.dart';
import '../documents/nodes/line.dart';
class SegmentLeafNode {
const SegmentLeafNode(this.line, this.leaf);
final Line? line;
final Leaf? leaf;
}

@ -0,0 +1,9 @@
class VerticalSpacing {
const VerticalSpacing(
this.top,
this.bottom,
);
final double top;
final double bottom;
}

@ -287,6 +287,7 @@ extension Localization on String {
'Resize': '크기조정',
'Width': '넓이',
'Height': '높이',
'Size': '크기',
'Small': '작게',
'Large': '크게',
'Huge': '매우크게',
@ -933,6 +934,40 @@ extension Localization on String {
'Camera': 'Kamera',
'Video': 'Video',
},
'it': {
'Paste a link': 'Incolla un collegamento',
'Ok': 'Ok',
'Select Color': 'Seleziona Colore',
'Gallery': 'Galleria',
'Link': 'Collegamento',
'Please first select some text to transform into a link.':
'Per prima cosa seleziona del testo da trasformare in un link.',
'Open': 'Apri',
'Copy': 'Copia',
'Remove': 'Rimuovi',
'Save': 'Salva',
'Zoom': 'Ingrandisci',
'Saved': 'Salvato',
'Text': 'Testo',
'What is entered is not a link':
'Ciò che viene inserito non è un collegamento',
'Resize': 'Ridimensiona',
'Width': 'Larghezza',
'Height': 'Altezza',
'Size': 'Dimensione',
'Small': 'Piccolo',
'Large': 'Largo',
'Huge': 'Enorme',
'Clear': 'Cancella',
'Font': 'Font',
'Search': 'Ricerca',
'matches': 'corrispondenze',
'showing match': 'visualizza corrispondenza',
'Prev': 'Prec',
'Next': 'Succ',
'Camera': 'Camera',
'Video': 'Video',
},
};
String get i18n => localize(this, _t);

@ -1,11 +1,10 @@
import 'dart:math';
import 'package:tuple/tuple.dart';
import '../models/documents/nodes/leaf.dart';
import '../models/structs/offset_value.dart';
import '../widgets/controller.dart';
Tuple2<int, Embed> getEmbedNode(QuillController controller, int offset) {
OffsetValue<Embed> getEmbedNode(QuillController controller, int offset) {
var offset = controller.selection.start;
var embedNode = controller.queryNode(offset);
if (embedNode == null || !(embedNode is Embed)) {
@ -13,7 +12,7 @@ Tuple2<int, Embed> getEmbedNode(QuillController controller, int offset) {
embedNode = controller.queryNode(offset);
}
if (embedNode != null && embedNode is Embed) {
return Tuple2(offset, embedNode);
return OffsetValue(offset, embedNode);
}
return throw 'Embed node not found by offset $offset';

@ -2,7 +2,6 @@ import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart';
import '../models/documents/document.dart';
@ -10,6 +9,9 @@ import '../models/documents/nodes/embeddable.dart';
import '../models/documents/nodes/leaf.dart';
import '../models/documents/style.dart';
import '../models/quill_delta.dart';
import '../models/structs/doc_change.dart';
import '../models/structs/image_url.dart';
import '../models/structs/offset_value.dart';
import '../utils/delta.dart';
typedef ReplaceTextCallback = bool Function(int index, int len, Object? data);
@ -82,12 +84,7 @@ class QuillController extends ChangeNotifier {
/// removing or listeners to this instance.
bool _isDisposed = false;
// item1: Document state before [change].
//
// item2: Change delta applied to the document.
//
// item3: The source of this change.
Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => document.changes;
Stream<DocChange> get changes => document.changes;
TextEditingValue get plainTextEditingValue => TextEditingValue(
text: document.toPlainText(),
@ -123,7 +120,7 @@ class QuillController extends ChangeNotifier {
}
/// Returns all styles for each node within selection
List<Tuple2<int, Style>> getAllIndividualSelectionStyles() {
List<OffsetValue<Style>> getAllIndividualSelectionStyles() {
final styles = document.collectAllIndividualStyles(
selection.start, selection.end - selection.start);
return styles;
@ -145,9 +142,9 @@ class QuillController extends ChangeNotifier {
}
void undo() {
final tup = document.undo();
if (tup.item1) {
_handleHistoryChange(tup.item2);
final result = document.undo();
if (result.changed) {
_handleHistoryChange(result.len);
}
}
@ -167,9 +164,9 @@ class QuillController extends ChangeNotifier {
}
void redo() {
final tup = document.redo();
if (tup.item1) {
_handleHistoryChange(tup.item2);
final result = document.redo();
if (result.changed) {
_handleHistoryChange(result.len);
}
}
@ -369,16 +366,15 @@ class QuillController extends ChangeNotifier {
/// Given offset, find its leaf node in document
Leaf? queryNode(int offset) {
return document.querySegmentLeafNode(offset).item2;
return document.querySegmentLeafNode(offset).leaf;
}
/// Clipboard for image url and its corresponding style
/// item1 is url and item2 is style string
Tuple2<String, String>? _copiedImageUrl;
ImageUrl? _copiedImageUrl;
Tuple2<String, String>? get copiedImageUrl => _copiedImageUrl;
ImageUrl? get copiedImageUrl => _copiedImageUrl;
set copiedImageUrl(Tuple2<String, String>? value) {
set copiedImageUrl(ImageUrl? value) {
_copiedImageUrl = value;
Clipboard.setData(const ClipboardData(text: ''));
}

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart';
import '../models/documents/style.dart';
import '../models/structs/vertical_spacing.dart';
import '../utils/platform.dart';
import 'style_widgets/checkbox_point.dart';
@ -44,11 +44,11 @@ class DefaultTextBlockStyle {
final TextStyle style;
/// Vertical spacing around a text block.
final Tuple2<double, double> verticalSpacing;
final VerticalSpacing verticalSpacing;
/// Vertical spacing for individual lines within a text block.
///
final Tuple2<double, double> lineSpacing;
final VerticalSpacing lineSpacing;
/// Decoration of a text block.
///
@ -125,8 +125,8 @@ class InlineCodeStyle {
class DefaultListBlockStyle extends DefaultTextBlockStyle {
DefaultListBlockStyle(
TextStyle style,
Tuple2<double, double> verticalSpacing,
Tuple2<double, double> lineSpacing,
VerticalSpacing verticalSpacing,
VerticalSpacing lineSpacing,
BoxDecoration? decoration,
this.checkboxUIBuilder,
) : super(style, verticalSpacing, lineSpacing, decoration);
@ -193,7 +193,7 @@ class DefaultStyles {
height: 1.3,
decoration: TextDecoration.none,
);
const baseSpacing = Tuple2<double, double>(6, 0);
const baseSpacing = VerticalSpacing(6, 0);
String fontFamily;
if (isAppleOS(themeData.platform)) {
fontFamily = 'Menlo';
@ -216,8 +216,8 @@ class DefaultStyles {
fontWeight: FontWeight.w300,
decoration: TextDecoration.none,
),
const Tuple2(16, 0),
const Tuple2(0, 0),
const VerticalSpacing(16, 0),
const VerticalSpacing(0, 0),
null),
h2: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith(
@ -227,8 +227,8 @@ class DefaultStyles {
fontWeight: FontWeight.normal,
decoration: TextDecoration.none,
),
const Tuple2(8, 0),
const Tuple2(0, 0),
const VerticalSpacing(8, 0),
const VerticalSpacing(0, 0),
null),
h3: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith(
@ -238,11 +238,11 @@ class DefaultStyles {
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
),
const Tuple2(8, 0),
const Tuple2(0, 0),
const VerticalSpacing(8, 0),
const VerticalSpacing(0, 0),
null),
paragraph: DefaultTextBlockStyle(
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
paragraph: DefaultTextBlockStyle(baseStyle, const VerticalSpacing(0, 0),
const VerticalSpacing(0, 0), null),
bold: const TextStyle(fontWeight: FontWeight.bold),
italic: const TextStyle(fontStyle: FontStyle.italic),
small: const TextStyle(fontSize: 12),
@ -272,15 +272,15 @@ class DefaultStyles {
height: 1.5,
color: Colors.grey.withOpacity(0.6),
),
const Tuple2(0, 0),
const Tuple2(0, 0),
const VerticalSpacing(0, 0),
const VerticalSpacing(0, 0),
null),
lists: DefaultListBlockStyle(
baseStyle, baseSpacing, const Tuple2(0, 6), null, null),
baseStyle, baseSpacing, const VerticalSpacing(0, 6), null, null),
quote: DefaultTextBlockStyle(
TextStyle(color: baseStyle.color!.withOpacity(0.6)),
baseSpacing,
const Tuple2(6, 2),
const VerticalSpacing(6, 2),
BoxDecoration(
border: Border(
left: BorderSide(width: 4, color: Colors.grey.shade300),
@ -294,17 +294,17 @@ class DefaultStyles {
height: 1.15,
),
baseSpacing,
const Tuple2(0, 0),
const VerticalSpacing(0, 0),
BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(2),
)),
indent: DefaultTextBlockStyle(
baseStyle, baseSpacing, const Tuple2(0, 6), null),
align: DefaultTextBlockStyle(
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
leading: DefaultTextBlockStyle(
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
baseStyle, baseSpacing, const VerticalSpacing(0, 6), null),
align: DefaultTextBlockStyle(baseStyle, const VerticalSpacing(0, 0),
const VerticalSpacing(0, 0), null),
leading: DefaultTextBlockStyle(baseStyle, const VerticalSpacing(0, 0),
const VerticalSpacing(0, 0), null),
sizeSmall: const TextStyle(fontSize: 10),
sizeLarge: const TextStyle(fontSize: 18),
sizeHuge: const TextStyle(fontSize: 22));

@ -3,16 +3,14 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import '../../flutter_quill.dart';
import '../models/documents/attribute.dart';
import '../models/documents/nodes/leaf.dart';
import '../utils/platform.dart';
import 'editor.dart';
import 'embeds.dart';
import 'text_selection.dart';
typedef EmbedsBuilder = Widget Function(
BuildContext context,
QuillController controller,
Embed node,
bool readOnly,
);
typedef EmbedsBuilder = EmbedBuilder Function(Embed node);
typedef CustomStyleBuilder = TextStyle Function(Attribute attribute);

@ -9,13 +9,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:i18n_extension/i18n_widget.dart';
import 'package:tuple/tuple.dart';
import '../models/documents/document.dart';
import '../models/documents/nodes/container.dart' as container_node;
import '../models/documents/nodes/embeddable.dart';
import '../models/documents/nodes/leaf.dart';
import '../models/documents/style.dart';
import '../models/structs/offset_value.dart';
import '../utils/platform.dart';
import 'box.dart';
import 'controller.dart';
@ -38,7 +37,7 @@ abstract class EditorState extends State<RawEditor>
EditorTextSelectionOverlay? get selectionOverlay;
List<Tuple2<int, Style>> get pasteStyle;
List<OffsetValue<Style>> get pasteStyle;
String get pastePlainText;
@ -363,7 +362,7 @@ class QuillEditor extends StatefulWidget {
onSingleLongTapEnd;
final Iterable<EmbedBuilder>? embedBuilders;
final EmbedsBuilder? unknownEmbedBuilder;
final EmbedBuilder? unknownEmbedBuilder;
final CustomStyleBuilder? customStyleBuilder;
/// The locale to use for the editor toolbar, defaults to system locale
@ -492,19 +491,7 @@ class QuillEditorState extends State<QuillEditor>
keyboardAppearance: widget.keyboardAppearance,
enableInteractiveSelection: widget.enableInteractiveSelection,
scrollPhysics: widget.scrollPhysics,
embedBuilder: (
context,
controller,
node,
readOnly,
) =>
_buildCustomBlockEmbed(
node,
context,
controller,
readOnly,
widget.unknownEmbedBuilder,
),
embedBuilder: _getEmbedBuilder,
linkActionPickerDelegate: widget.linkActionPickerDelegate,
customStyleBuilder: widget.customStyleBuilder,
floatingCursorDisabled: widget.floatingCursorDisabled,
@ -541,31 +528,19 @@ class QuillEditorState extends State<QuillEditor>
return editor;
}
Widget _buildCustomBlockEmbed(
Embed node,
BuildContext context,
QuillController controller,
bool readOnly,
EmbedsBuilder? unknownEmbedBuilder,
) {
EmbedBuilder _getEmbedBuilder(Embed node) {
final builders = widget.embedBuilders;
var _node = node;
// Creates correct node for custom embed
if (node.value.type == BlockEmbed.customType) {
_node = Embed(CustomBlockEmbed.fromJsonString(node.value.data));
}
if (builders != null) {
for (final builder in builders) {
if (builder.key == _node.value.type) {
return builder.build(context, controller, _node, readOnly);
if (builder.key == node.value.type) {
return builder;
}
}
}
if (unknownEmbedBuilder != null) {
return unknownEmbedBuilder(context, controller, _node, readOnly);
if (widget.unknownEmbedBuilder != null) {
return widget.unknownEmbedBuilder!;
}
throw UnimplementedError(
@ -645,11 +620,11 @@ class _QuillEditorSelectionGestureDetectorBuilder
final pos = renderEditor!.getPositionForOffset(details.globalPosition);
final result =
editor!.widget.controller.document.querySegmentLeafNode(pos.offset);
final line = result.item1;
final line = result.line;
if (line == null) {
return false;
}
final segmentLeaf = result.item2;
final segmentLeaf = result.leaf;
if (segmentLeaf == null && line.length == 1) {
editor!.widget.controller.updateSelection(
TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL);

@ -7,12 +7,18 @@ import 'controller.dart';
abstract class EmbedBuilder {
String get key;
bool get expanded => true;
WidgetSpan buildWidgetSpan(Widget widget) {
return WidgetSpan(child: widget);
}
Widget build(
BuildContext context,
QuillController controller,
leaf.Embed node,
bool readOnly,
bool inline,
);
}

@ -12,7 +12,6 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:pasteboard/pasteboard.dart';
import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart';
import '../models/documents/document.dart';
@ -22,6 +21,8 @@ import '../models/documents/nodes/leaf.dart' as leaf;
import '../models/documents/nodes/line.dart';
import '../models/documents/nodes/node.dart';
import '../models/documents/style.dart';
import '../models/structs/offset_value.dart';
import '../models/structs/vertical_spacing.dart';
import '../utils/cast.dart';
import '../utils/delta.dart';
import '../utils/embeds.dart';
@ -288,8 +289,8 @@ class RawEditorState extends EditorState
// for pasting style
@override
List<Tuple2<int, Style>> get pasteStyle => _pasteStyle;
List<Tuple2<int, Style>> _pasteStyle = <Tuple2<int, Style>>[];
List<OffsetValue<Style>> get pasteStyle => _pasteStyle;
List<OffsetValue<Style>> _pasteStyle = <OffsetValue<Style>>[];
@override
String get pastePlainText => _pastePlainText;
@ -331,8 +332,8 @@ class RawEditorState extends EditorState
final points = renderEditor.getEndpointsForSelection(selection);
return TextSelectionToolbarAnchors.fromSelection(
renderBox: renderEditor,
startGlyphHeight: glyphHeights.item1,
endGlyphHeight: glyphHeights.item2,
startGlyphHeight: glyphHeights.startGlyphHeight,
endGlyphHeight: glyphHeights.endGlyphHeight,
selectionEndpoints: points,
);
}
@ -341,7 +342,7 @@ class RawEditorState extends EditorState
/// [RawEditorState].
///
/// Copied from [EditableTextState].
Tuple2<double, double> _getGlyphHeights() {
_GlyphHeights _getGlyphHeights() {
final selection = textEditingValue.selection;
// Only calculate handle rects if the text in the previous frame
@ -354,7 +355,7 @@ class RawEditorState extends EditorState
final prevText = renderEditor.document.toPlainText();
final currText = textEditingValue.text;
if (prevText != currText || !selection.isValid || selection.isCollapsed) {
return Tuple2(
return _GlyphHeights(
renderEditor.preferredLineHeight(selection.base),
renderEditor.preferredLineHeight(selection.base),
);
@ -364,7 +365,7 @@ class RawEditorState extends EditorState
renderEditor.getLocalRectForCaret(selection.base);
final endCharacterRect =
renderEditor.getLocalRectForCaret(selection.extent);
return Tuple2(
return _GlyphHeights(
startCharacterRect.height,
endCharacterRect.height,
);
@ -414,7 +415,7 @@ class RawEditorState extends EditorState
/// baseline.
// This implies that the first line has no styles applied to it.
final baselinePadding =
EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1);
EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.top);
child = BaselineProxy(
textStyle: _styles!.paragraph!.style,
padding: baselinePadding,
@ -778,7 +779,7 @@ class RawEditorState extends EditorState
return editableTextLine;
}
Tuple2<double, double> _getVerticalSpacingForLine(
VerticalSpacing _getVerticalSpacingForLine(
Line line, DefaultStyles? defaultStyles) {
final attrs = line.style.attributes;
if (attrs.containsKey(Attribute.header.key)) {
@ -803,7 +804,7 @@ class RawEditorState extends EditorState
return defaultStyles!.paragraph!.verticalSpacing;
}
Tuple2<double, double> _getVerticalSpacingForBlock(
VerticalSpacing _getVerticalSpacingForBlock(
Block node, DefaultStyles? defaultStyles) {
final attrs = node.style.attributes;
if (attrs.containsKey(Attribute.blockQuote.key)) {
@ -817,7 +818,7 @@ class RawEditorState extends EditorState
} else if (attrs.containsKey(Attribute.align.key)) {
return defaultStyles!.align!.verticalSpacing;
}
return const Tuple2(0, 0);
return const VerticalSpacing(0, 0);
}
@override
@ -1280,11 +1281,10 @@ class RawEditorState extends EditorState
final index = textEditingValue.selection.baseOffset;
final length = textEditingValue.selection.extentOffset - index;
final copied = controller.copiedImageUrl!;
controller.replaceText(
index, length, BlockEmbed.image(copied.item1), null);
if (copied.item2.isNotEmpty) {
controller.formatText(getEmbedNode(controller, index + 1).item1, 1,
StyleAttribute(copied.item2));
controller.replaceText(index, length, BlockEmbed.image(copied.url), null);
if (copied.styleString.isNotEmpty) {
controller.formatText(getEmbedNode(controller, index + 1).offset, 1,
StyleAttribute(copied.styleString));
}
controller.copiedImageUrl = null;
await Clipboard.setData(const ClipboardData(text: ''));
@ -2427,3 +2427,13 @@ typedef QuillEditorContextMenuBuilder = Widget Function(
BuildContext context,
RawEditorState rawEditorState,
);
class _GlyphHeights {
_GlyphHeights(
this.startGlyphHeight,
this.endGlyphHeight,
);
final double startGlyphHeight;
final double endGlyphHeight;
}

@ -38,13 +38,13 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState
if (insertedText == pastePlainText && pastePlainText != '') {
final pos = start;
for (var i = 0; i < pasteStyle.length; i++) {
final offset = pasteStyle[i].item1;
final style = pasteStyle[i].item2;
final offset = pasteStyle[i].offset;
final style = pasteStyle[i].value;
widget.controller.formatTextStyle(
pos + offset,
i == pasteStyle.length - 1
? pastePlainText.length - offset
: pasteStyle[i + 1].item1,
: pasteStyle[i + 1].offset,
style);
}
}

@ -29,8 +29,7 @@ class QuillNumberPoint extends StatelessWidget {
Widget build(BuildContext context) {
var s = index.toString();
int? level = 0;
if (!attrs.containsKey(Attribute.indent.key) &&
!indentLevelCounts.containsKey(1)) {
if (!attrs.containsKey(Attribute.indent.key) && indentLevelCounts.isEmpty) {
indentLevelCounts.clear();
return Container(
alignment: AlignmentDirectional.topEnd,
@ -41,7 +40,7 @@ class QuillNumberPoint extends StatelessWidget {
}
if (attrs.containsKey(Attribute.indent.key)) {
level = attrs[Attribute.indent.key]!.value;
} else {
} else if (!indentLevelCounts.containsKey(0)) {
// first level but is back from previous indent level
// supposed to be "2."
indentLevelCounts[0] = 1;

@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart';
import '../models/documents/nodes/block.dart';
import '../models/documents/nodes/line.dart';
import '../models/structs/vertical_spacing.dart';
import '../utils/delta.dart';
import 'box.dart';
import 'controller.dart';
@ -78,7 +78,7 @@ class EditableTextBlock extends StatelessWidget {
final QuillController controller;
final TextDirection textDirection;
final double scrollBottomInset;
final Tuple2 verticalSpacing;
final VerticalSpacing verticalSpacing;
final TextSelection textSelection;
final Color color;
final DefaultStyles? styles;
@ -102,7 +102,7 @@ class EditableTextBlock extends StatelessWidget {
return _EditableBlock(
block: block,
textDirection: textDirection,
padding: verticalSpacing as Tuple2<double, double>,
padding: verticalSpacing,
scrollBottomInset: scrollBottomInset,
decoration: _getDecorationForBlock(block, defaultStyles) ??
const BoxDecoration(),
@ -243,7 +243,7 @@ class EditableTextBlock extends StatelessWidget {
return baseIndent + extraIndent;
}
Tuple2 _getSpacingForLine(
VerticalSpacing _getSpacingForLine(
Line node, int index, int count, DefaultStyles? defaultStyles) {
var top = 0.0, bottom = 0.0;
@ -252,22 +252,22 @@ class EditableTextBlock extends StatelessWidget {
final level = attrs[Attribute.header.key]!.value;
switch (level) {
case 1:
top = defaultStyles!.h1!.verticalSpacing.item1;
bottom = defaultStyles.h1!.verticalSpacing.item2;
top = defaultStyles!.h1!.verticalSpacing.top;
bottom = defaultStyles.h1!.verticalSpacing.bottom;
break;
case 2:
top = defaultStyles!.h2!.verticalSpacing.item1;
bottom = defaultStyles.h2!.verticalSpacing.item2;
top = defaultStyles!.h2!.verticalSpacing.top;
bottom = defaultStyles.h2!.verticalSpacing.bottom;
break;
case 3:
top = defaultStyles!.h3!.verticalSpacing.item1;
bottom = defaultStyles.h3!.verticalSpacing.item2;
top = defaultStyles!.h3!.verticalSpacing.top;
bottom = defaultStyles.h3!.verticalSpacing.bottom;
break;
default:
throw 'Invalid level $level';
}
} else {
late Tuple2 lineSpacing;
late VerticalSpacing lineSpacing;
if (attrs.containsKey(Attribute.blockQuote.key)) {
lineSpacing = defaultStyles!.quote!.lineSpacing;
} else if (attrs.containsKey(Attribute.indent.key)) {
@ -282,8 +282,8 @@ class EditableTextBlock extends StatelessWidget {
// use paragraph linespacing as a default
lineSpacing = defaultStyles!.paragraph!.lineSpacing;
}
top = lineSpacing.item1;
bottom = lineSpacing.item2;
top = lineSpacing.top;
bottom = lineSpacing.bottom;
}
if (index == 1) {
@ -294,7 +294,7 @@ class EditableTextBlock extends StatelessWidget {
bottom = 0.0;
}
return Tuple2(top, bottom);
return VerticalSpacing(top, bottom);
}
}
@ -601,13 +601,13 @@ class _EditableBlock extends MultiChildRenderObjectWidget {
final Block block;
final TextDirection textDirection;
final Tuple2<double, double> padding;
final VerticalSpacing padding;
final double scrollBottomInset;
final Decoration decoration;
final EdgeInsets? contentPadding;
EdgeInsets get _padding =>
EdgeInsets.only(top: padding.item1, bottom: padding.item2);
EdgeInsets.only(top: padding.top, bottom: padding.bottom);
EdgeInsets get _contentPadding => contentPadding ?? EdgeInsets.zero;

@ -6,16 +6,17 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:tuple/tuple.dart';
import 'package:url_launcher/url_launcher.dart';
import '../models/documents/attribute.dart';
import '../models/documents/nodes/container.dart' as container_node;
import '../models/documents/nodes/embeddable.dart';
import '../models/documents/nodes/leaf.dart';
import '../models/documents/nodes/leaf.dart' as leaf;
import '../models/documents/nodes/line.dart';
import '../models/documents/nodes/node.dart';
import '../models/documents/style.dart';
import '../models/structs/vertical_spacing.dart';
import '../utils/color.dart';
import '../utils/font.dart';
import '../utils/platform.dart';
@ -132,17 +133,28 @@ class _TextLineState extends State<TextLine> {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
if (widget.line.hasEmbed && widget.line.childCount == 1) {
// For video, it is always single child
final embed = widget.line.children.single as Embed;
return EmbedProxy(
widget.embedBuilder(
context,
widget.controller,
embed,
widget.readOnly,
),
);
// Single child embeds can be expanded
var embed = widget.line.children.single as Embed;
// Creates correct node for custom embed
if (embed.value.type == BlockEmbed.customType) {
embed = Embed(CustomBlockEmbed.fromJsonString(embed.value.data));
}
final embedBuilder = widget.embedBuilder(embed);
if (embedBuilder.expanded) {
// Creates correct node for custom embed
return EmbedProxy(
embedBuilder.build(
context,
widget.controller,
embed,
widget.readOnly,
false,
),
);
}
}
final textSpan = _getTextSpanForWholeLine(context);
final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
@ -173,24 +185,28 @@ class _TextLineState extends State<TextLine> {
// The line could contain more than one Embed & more than one Text
final textSpanChildren = <InlineSpan>[];
var textNodes = LinkedList<Node>();
for (final child in widget.line.children) {
for (var child in widget.line.children) {
if (child is Embed) {
if (textNodes.isNotEmpty) {
textSpanChildren
.add(_buildTextSpan(widget.styles, textNodes, lineStyle));
textNodes = LinkedList<Node>();
}
// Here it should be image
final embed = WidgetSpan(
child: EmbedProxy(
widget.embedBuilder(
context,
widget.controller,
child,
widget.readOnly,
),
// Creates correct node for custom embed
if (child.value.type == BlockEmbed.customType) {
child = Embed(CustomBlockEmbed.fromJsonString(child.value.data));
}
final embedBuilder = widget.embedBuilder(child);
final embedWidget = EmbedProxy(
embedBuilder.build(
context,
widget.controller,
child,
widget.readOnly,
true,
),
);
final embed = embedBuilder.buildWidgetSpan(embedWidget);
textSpanChildren.add(embed);
continue;
}
@ -476,7 +492,7 @@ class EditableTextLine extends RenderObjectWidget {
final Widget? leading;
final Widget body;
final double indentWidth;
final Tuple2 verticalSpacing;
final VerticalSpacing verticalSpacing;
final TextDirection textDirection;
final TextSelection textSelection;
final Color color;
@ -526,8 +542,8 @@ class EditableTextLine extends RenderObjectWidget {
EdgeInsetsGeometry _getPadding() {
return EdgeInsetsDirectional.only(
start: indentWidth,
top: verticalSpacing.item1,
bottom: verticalSpacing.item2);
top: verticalSpacing.top,
bottom: verticalSpacing.bottom);
}
}

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import '../../../flutter_quill.dart';
import '../../models/documents/attribute.dart';
import '../../models/themes/quill_icon_theme.dart';
import '../controller.dart';
import '../toolbar.dart';
class ClearFormatButton extends StatefulWidget {
const ClearFormatButton({

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import '../../../flutter_quill.dart';
import '../../models/themes/quill_icon_theme.dart';
import '../controller.dart';
import '../toolbar.dart';
class HistoryButton extends StatefulWidget {
const HistoryButton({

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import '../../../flutter_quill.dart';
import '../../models/themes/quill_icon_theme.dart';
import '../controller.dart';
import '../toolbar.dart';
class IndentButton extends StatefulWidget {
const IndentButton({

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:tuple/tuple.dart';
import '../../models/documents/attribute.dart';
import '../../models/rules/insert.dart';
@ -86,7 +85,7 @@ class _LinkStyleButtonState extends State<LinkStyleButton> {
}
void _openLinkDialog(BuildContext context) {
showDialog<dynamic>(
showDialog<_TextLink>(
context: context,
builder: (ctx) {
final link = _getLinkAttributeValue();
@ -96,7 +95,7 @@ class _LinkStyleButtonState extends State<LinkStyleButton> {
if (link != null) {
// text should be the link's corresponding text, not selection
final leaf =
widget.controller.document.querySegmentLeafNode(index).item2;
widget.controller.document.querySegmentLeafNode(index).leaf;
if (leaf != null) {
text = leaf.toPlainText();
}
@ -122,24 +121,21 @@ class _LinkStyleButtonState extends State<LinkStyleButton> {
?.value;
}
void _linkSubmitted(dynamic value) {
// text.isNotEmpty && link.isNotEmpty
final String text = (value as Tuple2).item1;
final String link = value.item2.trim();
void _linkSubmitted(_TextLink value) {
var index = widget.controller.selection.start;
var length = widget.controller.selection.end - index;
if (_getLinkAttributeValue() != null) {
// text should be the link's corresponding text, not selection
final leaf = widget.controller.document.querySegmentLeafNode(index).item2;
final leaf = widget.controller.document.querySegmentLeafNode(index).leaf;
if (leaf != null) {
final range = getLinkRange(leaf);
index = range.start;
length = range.end - range.start;
}
}
widget.controller.replaceText(index, length, text, null);
widget.controller.formatText(index, text.length, LinkAttribute(link));
widget.controller.replaceText(index, length, value.text, null);
widget.controller
.formatText(index, value.text.length, LinkAttribute(value.link));
}
}
@ -240,6 +236,16 @@ class _LinkDialogState extends State<_LinkDialog> {
}
void _applyLink() {
Navigator.pop(context, Tuple2(_text.trim(), _link.trim()));
Navigator.pop(context, _TextLink(_text.trim(), _link.trim()));
}
}
class _TextLink {
_TextLink(
this.text,
this.link,
);
final String text;
final String link;
}

@ -1,6 +1,6 @@
name: flutter_quill
description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us)
version: 6.4.4
version: 7.0.3
#author: bulletjournal
homepage: https://bulletjournal.us/home/index.html
repository: https://github.com/singerdmx/flutter-quill
@ -16,12 +16,11 @@ dependencies:
flutter_colorpicker: ^1.0.3
flutter_keyboard_visibility: ^5.4.0
quiver: ^3.2.1
tuple: ^2.0.1
url_launcher: ^6.1.9
pedantic: ^1.11.1
characters: ^1.2.1
diff_match_patch: ^0.4.1
i18n_extension: ^7.0.0
i18n_extension: ^8.0.0
device_info_plus: ^8.1.0
platform: ^3.1.0
pasteboard: ^0.2.0

Loading…
Cancel
Save