Merge branch 'master' into fix2

pull/142/head
Xun Gong 4 years ago
commit fe6ab738c0
  1. 2
      .github/ISSUE_TEMPLATE/issue-template.md
  2. 3
      CHANGELOG.md
  3. 22
      analysis_options.yaml
  4. 34
      example/lib/pages/home_page.dart
  5. 4
      example/lib/pages/read_only_page.dart
  6. 6
      example/lib/universal_ui/universal_ui.dart
  7. 14
      example/lib/widgets/demo_scaffold.dart
  8. 2
      example/test/widget_test.dart
  9. 10
      lib/models/documents/attribute.dart
  10. 82
      lib/models/documents/document.dart
  11. 35
      lib/models/documents/history.dart
  12. 37
      lib/models/documents/nodes/block.dart
  13. 70
      lib/models/documents/nodes/container.dart
  14. 24
      lib/models/documents/nodes/embed.dart
  15. 156
      lib/models/documents/nodes/leaf.dart
  16. 207
      lib/models/documents/nodes/line.dart
  17. 86
      lib/models/documents/nodes/node.dart
  18. 29
      lib/models/documents/style.dart
  19. 86
      lib/models/quill_delta.dart
  20. 39
      lib/models/rules/delete.dart
  21. 44
      lib/models/rules/format.dart
  22. 109
      lib/models/rules/insert.dart
  23. 13
      lib/models/rules/rule.dart
  24. 6
      lib/utils/color.dart
  25. 36
      lib/utils/diff_delta.dart
  26. 3
      lib/widgets/box.dart
  27. 39
      lib/widgets/controller.dart
  28. 68
      lib/widgets/cursor.dart
  29. 117
      lib/widgets/default_styles.dart
  30. 10
      lib/widgets/delegate.dart
  31. 349
      lib/widgets/editor.dart
  32. 11
      lib/widgets/keyboard_listener.dart
  33. 57
      lib/widgets/proxy.dart
  34. 268
      lib/widgets/raw_editor.dart
  35. 8
      lib/widgets/responsive_widget.dart
  36. 246
      lib/widgets/text_block.dart
  37. 234
      lib/widgets/text_line.dart
  38. 113
      lib/widgets/text_selection.dart
  39. 184
      lib/widgets/toolbar.dart
  40. 2
      pubspec.yaml

@ -11,6 +11,6 @@ My issue is about [Web]
My issue is about [Mobile]
My issue is about [Desktop]
I have tried running `example` directory successfully before creating an issue here AND it is NOT a stupid question.
I have tried running `example` directory successfully before creating an issue here.
Please note that we are using stable channel. If you are using beta or master channel, those are not supported.

@ -1,3 +1,6 @@
## [1.1.8]
* Fix height of empty line bug.
## [1.1.7]
* Fix text selection in read-only mode.

@ -3,13 +3,33 @@ include: package:pedantic/analysis_options.yaml
analyzer:
errors:
undefined_prefixed_name: ignore
omit_local_variable_types: ignore
unsafe_html: ignore
linter:
rules:
- always_declare_return_types
- always_put_required_named_parameters_first
- annotate_overrides
- avoid_empty_else
- avoid_escaping_inner_quotes
- avoid_print
- avoid_redundant_argument_values
- avoid_types_on_closure_parameters
- avoid_void_async
- cascade_invocations
- directives_ordering
- omit_local_variable_types
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_initializing_formals
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_relative_imports
- prefer_single_quotes
- sort_constructors_first
- sort_unnamed_constructors_first
- unnecessary_parenthesis
- unnecessary_string_interpolations

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:app/universal_ui/universal_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -16,6 +15,7 @@ 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';
class HomePage extends StatefulWidget {
@ -74,7 +74,7 @@ class _HomePageState extends State<HomePage> {
),
body: RawKeyboardListener(
focusNode: FocusNode(),
onKey: (RawKeyEvent event) {
onKey: (event) {
if (event.data.isControlPressed && event.character == 'b') {
if (_controller!
.getSelectionStyle()
@ -107,15 +107,15 @@ class _HomePageState extends State<HomePage> {
customStyles: DefaultStyles(
h1: DefaultTextBlockStyle(
const TextStyle(
fontSize: 32.0,
fontSize: 32,
color: Colors.black,
height: 1.15,
fontWeight: FontWeight.w300,
),
const Tuple2(16.0, 0.0),
const Tuple2(0.0, 0.0),
const Tuple2(16, 0),
const Tuple2(0, 0),
null),
sizeSmall: const TextStyle(fontSize: 9.0),
sizeSmall: const TextStyle(fontSize: 9),
));
if (kIsWeb) {
quillEditor = QuillEditor(
@ -131,15 +131,15 @@ class _HomePageState extends State<HomePage> {
customStyles: DefaultStyles(
h1: DefaultTextBlockStyle(
const TextStyle(
fontSize: 32.0,
fontSize: 32,
color: Colors.black,
height: 1.15,
fontWeight: FontWeight.w300,
),
const Tuple2(16.0, 0.0),
const Tuple2(0.0, 0.0),
const Tuple2(16, 0),
const Tuple2(0, 0),
null),
sizeSmall: const TextStyle(fontSize: 9.0),
sizeSmall: const TextStyle(fontSize: 9),
),
embedBuilder: defaultEmbedBuilderWeb);
}
@ -151,7 +151,7 @@ class _HomePageState extends State<HomePage> {
flex: 15,
child: Container(
color: Colors.white,
padding: const EdgeInsets.only(left: 16.0, right: 16.0),
padding: const EdgeInsets.only(left: 16, right: 16),
child: quillEditor,
),
),
@ -178,15 +178,15 @@ class _HomePageState extends State<HomePage> {
// You can also upload the picked image to any server (eg : AWS s3 or Firebase) and then return the uploaded image URL
Future<String> _onImagePickCallback(File file) async {
// Copies the picked file from temporary cache to applications directory
Directory appDocDir = await getApplicationDocumentsDirectory();
File copiedFile =
final appDocDir = await getApplicationDocumentsDirectory();
final copiedFile =
await file.copy('${appDocDir.path}/${basename(file.path)}');
return copiedFile.path.toString();
}
Widget _buildMenuBar(BuildContext context) {
Size size = MediaQuery.of(context).size;
final itemStyle = const TextStyle(
final size = MediaQuery.of(context).size;
const itemStyle = TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
@ -201,7 +201,7 @@ class _HomePageState extends State<HomePage> {
endIndent: size.width * 0.1,
),
ListTile(
title: Center(child: Text('Read only demo', style: itemStyle)),
title: const Center(child: Text('Read only demo', style: itemStyle)),
dense: true,
visualDensity: VisualDensity.compact,
onTap: _readOnly,
@ -220,7 +220,7 @@ class _HomePageState extends State<HomePage> {
Navigator.push(
super.context,
MaterialPageRoute(
builder: (BuildContext context) => ReadOnlyPage(),
builder: (context) => ReadOnlyPage(),
),
);
}

@ -1,9 +1,9 @@
import 'package:app/universal_ui/universal_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/widgets/controller.dart';
import 'package:flutter_quill/widgets/editor.dart';
import '../universal_ui/universal_ui.dart';
import '../widgets/demo_scaffold.dart';
class ReadOnlyPage extends StatefulWidget {
@ -53,7 +53,7 @@ class _ReadOnlyPageState extends State<ReadOnlyPage> {
embedBuilder: defaultEmbedBuilderWeb);
}
return Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8),
child: Container(
decoration: BoxDecoration(
color: Colors.white,

@ -28,10 +28,10 @@ var ui = UniversalUI();
Widget defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) {
switch (node.value.type) {
case 'image':
String imageUrl = node.value.data;
Size size = MediaQuery.of(context).size;
final String imageUrl = node.value.data;
final size = MediaQuery.of(context).size;
UniversalUI().platformViewRegistry.registerViewFactory(
imageUrl, (int viewId) => html.ImageElement()..src = imageUrl);
imageUrl, (viewId) => html.ImageElement()..src = imageUrl);
return Padding(
padding: EdgeInsets.only(
right: ResponsiveWidget.isMediumScreen(context)

@ -11,13 +11,6 @@ typedef DemoContentBuilder = Widget Function(
// Common scaffold for all examples.
class DemoScaffold extends StatefulWidget {
/// Filename of the document to load into the editor.
final String documentFilename;
final DemoContentBuilder builder;
final List<Widget>? actions;
final Widget? floatingActionButton;
final bool showToolbar;
const DemoScaffold({
required this.documentFilename,
required this.builder,
@ -27,6 +20,13 @@ class DemoScaffold extends StatefulWidget {
Key? key,
}) : super(key: key);
/// Filename of the document to load into the editor.
final String documentFilename;
final DemoContentBuilder builder;
final List<Widget>? actions;
final Widget? floatingActionButton;
final bool showToolbar;
@override
_DemoScaffoldState createState() => _DemoScaffoldState();
}

@ -10,7 +10,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
testWidgets('Counter increments smoke test', (tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());

@ -8,12 +8,12 @@ enum AttributeScope {
}
class Attribute<T> {
Attribute(this.key, this.scope, this.value);
final String key;
final AttributeScope scope;
final T value;
Attribute(this.key, this.scope, this.value);
static final Map<String, Attribute> _registry = {
Attribute.bold.key: Attribute.bold,
Attribute.italic.key: Attribute.italic,
@ -164,8 +164,8 @@ class Attribute<T> {
if (!_registry.containsKey(key)) {
throw ArgumentError.value(key, 'key "$key" not found.');
}
Attribute origin = _registry[key]!;
Attribute attribute = clone(origin, value);
final origin = _registry[key]!;
final attribute = clone(origin, value);
return attribute;
}
@ -177,7 +177,7 @@ class Attribute<T> {
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! Attribute<T>) return false;
Attribute<T> typedOther = other;
final typedOther = other;
return key == typedOther.key &&
scope == typedOther.scope &&
value == typedOther.value;

@ -1,20 +1,32 @@
import 'dart:async';
import 'package:flutter_quill/models/documents/nodes/block.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart';
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:tuple/tuple.dart';
import '../quill_delta.dart';
import '../rules/rule.dart';
import 'attribute.dart';
import 'history.dart';
import 'nodes/block.dart';
import 'nodes/container.dart';
import 'nodes/embed.dart';
import 'nodes/line.dart';
import 'nodes/node.dart';
import 'style.dart';
/// The rich text document
class Document {
Document() : _delta = Delta()..insert('\n') {
_loadDocument(_delta);
}
Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) {
_loadDocument(_delta);
}
Document.fromDelta(Delta delta) : _delta = delta {
_loadDocument(delta);
}
/// The root node of the document tree
final Root _root = Root();
@ -35,18 +47,6 @@ class Document {
Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => _observer.stream;
Document() : _delta = Delta()..insert('\n') {
_loadDocument(_delta);
}
Document.fromJson(List data) : _delta = _transform(Delta.fromJson(data)) {
_loadDocument(_delta);
}
Document.fromDelta(Delta delta) : _delta = delta {
_loadDocument(delta);
}
Delta insert(int index, Object? data) {
assert(index >= 0);
assert(data is String || data is Embeddable);
@ -56,14 +56,14 @@ class Document {
return Delta();
}
Delta delta = _rules.apply(RuleType.INSERT, this, index, data: data);
final delta = _rules.apply(RuleType.INSERT, this, index, data: data);
compose(delta, ChangeSource.LOCAL);
return delta;
}
Delta delete(int index, int len) {
assert(index >= 0 && len > 0);
Delta delta = _rules.apply(RuleType.DELETE, this, index, len: len);
final delta = _rules.apply(RuleType.DELETE, this, index, len: len);
if (delta.isNotEmpty) {
compose(delta, ChangeSource.LOCAL);
}
@ -74,18 +74,18 @@ class Document {
assert(index >= 0);
assert(data is String || data is Embeddable);
bool dataIsNotEmpty = (data is String) ? data.isNotEmpty : true;
final dataIsNotEmpty = (data is String) ? data.isNotEmpty : true;
assert(dataIsNotEmpty || len > 0);
Delta delta = Delta();
var delta = Delta();
if (dataIsNotEmpty) {
delta = insert(index + len, data);
}
if (len > 0) {
Delta deleteDelta = delete(index, len);
final deleteDelta = delete(index, len);
delta = delta.compose(deleteDelta);
}
@ -95,9 +95,9 @@ class Document {
Delta format(int index, int len, Attribute? attribute) {
assert(index >= 0 && len >= 0 && attribute != null);
Delta delta = Delta();
var delta = Delta();
Delta formatDelta = _rules.apply(RuleType.FORMAT, this, index,
final formatDelta = _rules.apply(RuleType.FORMAT, this, index,
len: len, attribute: attribute);
if (formatDelta.isNotEmpty) {
compose(formatDelta, ChangeSource.LOCAL);
@ -108,16 +108,16 @@ class Document {
}
Style collectStyle(int index, int len) {
ChildQuery res = queryChild(index);
final res = queryChild(index);
return (res.node as Line).collectStyle(res.offset, len);
}
ChildQuery queryChild(int offset) {
ChildQuery res = _root.queryChild(offset, true);
final res = _root.queryChild(offset, true);
if (res.node is Line) {
return res;
}
Block block = res.node as Block;
final block = res.node as Block;
return block.queryChild(res.offset, true);
}
@ -126,11 +126,11 @@ class Document {
delta.trim();
assert(delta.isNotEmpty);
int offset = 0;
var offset = 0;
delta = _transform(delta);
Delta originalDelta = toDelta();
for (Operation op in delta.toList()) {
Style? style =
final originalDelta = toDelta();
for (final op in delta.toList()) {
final style =
op.attributes != null ? Style.fromJson(op.attributes) : null;
if (op.isInsert) {
@ -172,10 +172,10 @@ class Document {
bool get hasRedo => _history.hasRedo;
static Delta _transform(Delta delta) {
Delta res = Delta();
List<Operation> ops = delta.toList();
for (int i = 0; i < ops.length; i++) {
Operation op = ops[i];
final res = Delta();
final ops = delta.toList();
for (var i = 0; i < ops.length; i++) {
final op = ops[i];
res.push(op);
_handleImageInsert(i, ops, op, res);
}
@ -184,14 +184,14 @@ class Document {
static void _handleImageInsert(
int i, List<Operation> ops, Operation op, Delta res) {
bool nextOpIsImage =
final nextOpIsImage =
i + 1 < ops.length && ops[i + 1].isInsert && ops[i + 1].data is! String;
if (nextOpIsImage && !(op.data as String).endsWith('\n')) {
res.push(Operation.insert('\n'));
}
// Currently embed is equivalent to image and hence `is! String`
bool opInsertImage = op.isInsert && op.data is! String;
bool nextOpIsLineBreak = i + 1 < ops.length &&
final opInsertImage = op.isInsert && op.data is! String;
final nextOpIsLineBreak = i + 1 < ops.length &&
ops[i + 1].isInsert &&
ops[i + 1].data is String &&
(ops[i + 1].data as String).startsWith('\n');
@ -221,7 +221,7 @@ class Document {
void _loadDocument(Delta doc) {
assert((doc.last.data as String).endsWith('\n'));
int offset = 0;
var offset = 0;
for (final op in doc.toList()) {
if (!op.isInsert) {
throw ArgumentError.value(doc,
@ -247,12 +247,12 @@ class Document {
return false;
}
final Node node = root.children.first;
final node = root.children.first;
if (!node.isLast) {
return false;
}
Delta delta = node.toDelta();
final delta = node.toDelta();
return delta.length == 1 &&
delta.first.data == '\n' &&
delta.first.key == 'insert';

@ -1,9 +1,17 @@
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:tuple/tuple.dart';
import '../quill_delta.dart';
import 'document.dart';
class History {
History({
this.ignoreChange = false,
this.interval = 400,
this.maxStack = 100,
this.userOnly = false,
this.lastRecorded = 0,
});
final HistoryStack stack = HistoryStack.empty();
bool get hasUndo => stack.undo.isNotEmpty;
@ -24,13 +32,6 @@ class History {
///record delay
final int interval;
History(
{this.ignoreChange = false,
this.interval = 400,
this.maxStack = 100,
this.userOnly = false,
this.lastRecorded = 0});
void handleDocChange(Tuple3<Delta, Delta, ChangeSource> change) {
if (ignoreChange) return;
if (!userOnly || change.item3 == ChangeSource.LOCAL) {
@ -47,7 +48,7 @@ class History {
void record(Delta change, Delta before) {
if (change.isEmpty) return;
stack.redo.clear();
Delta undoDelta = change.invert(before);
var undoDelta = change.invert(before);
final timeStamp = DateTime.now().millisecondsSinceEpoch;
if (lastRecorded + interval > timeStamp && stack.undo.isNotEmpty) {
@ -74,7 +75,7 @@ class History {
}
void transformStack(List<Delta> stack, Delta delta) {
for (int i = stack.length - 1; i >= 0; i -= 1) {
for (var i = stack.length - 1; i >= 0; i -= 1) {
final oldDelta = stack[i];
stack[i] = delta.transform(oldDelta, true);
delta = oldDelta.transform(delta, false);
@ -88,10 +89,10 @@ class History {
if (source.isEmpty) {
return const Tuple2(false, 0);
}
Delta delta = source.removeLast();
final delta = source.removeLast();
// look for insert or delete
int? len = 0;
List<Operation> ops = delta.toList();
final ops = delta.toList();
for (var i = 0; i < ops.length; i++) {
if (ops[i].key == Operation.insertKey) {
len = ops[i].length;
@ -99,8 +100,8 @@ class History {
len = ops[i].length! * -1;
}
}
Delta base = Delta.from(doc.toDelta());
Delta inverseDelta = delta.invert(base);
final base = Delta.from(doc.toDelta());
final inverseDelta = delta.invert(base);
dest.add(inverseDelta);
lastRecorded = 0;
ignoreChange = true;
@ -119,13 +120,13 @@ class History {
}
class HistoryStack {
final List<Delta> undo;
final List<Delta> redo;
HistoryStack.empty()
: undo = [],
redo = [];
final List<Delta> undo;
final List<Delta> redo;
void clear() {
undo.clear();
redo.clear();

@ -1,10 +1,23 @@
import 'package:flutter_quill/models/quill_delta.dart';
import '../../quill_delta.dart';
import 'container.dart';
import 'line.dart';
import 'node.dart';
/// Represents a group of adjacent [Line]s with the same block style.
///
/// Block elements are:
/// - Blockquote
/// - Header
/// - Indent
/// - List
/// - Text Alignment
/// - Text Direction
/// - Code Block
class Block extends Container<Line?> {
/// Creates new unmounted [Block].
@override
Node newInstance() => Block();
@override
Line get defaultChild => Line();
@ -18,7 +31,7 @@ class Block extends Container<Line?> {
@override
void adjust() {
if (isEmpty) {
Node? sibling = previous;
final sibling = previous;
unlink();
if (sibling != null) {
sibling.adjust();
@ -26,17 +39,18 @@ class Block extends Container<Line?> {
return;
}
Block block = this;
Node? prev = block.previous;
var block = this;
final prev = block.previous;
// merging it with previous block if style is the same
if (!block.isFirst &&
block.previous is Block &&
prev!.style == block.style) {
block.moveChildToNewParent(prev as Container<Node?>?);
block.unlink();
block
..moveChildToNewParent(prev as Container<Node?>?)
..unlink();
block = prev as Block;
}
Node? next = block.next;
final next = block.next;
// merging it with next block if style is the same
if (!block.isLast && block.next is Block && next!.style == block.style) {
(next as Block).moveChildToNewParent(block);
@ -48,16 +62,11 @@ class Block extends Container<Line?> {
String toString() {
final block = style.attributes.toString();
final buffer = StringBuffer('§ {$block}\n');
for (var child in children) {
for (final child in children) {
final tree = child.isLast ? '' : '';
buffer.write(' $tree $child');
if (!child.isLast) buffer.writeln();
}
return buffer.toString();
}
@override
Node newInstance() {
return Block();
}
}

@ -1,70 +1,99 @@
import 'dart:collection';
import '../style.dart';
import 'leaf.dart';
import 'line.dart';
import 'node.dart';
/* Container of multiple nodes */
/// Container can accommodate other nodes.
///
/// Delegates insert, retain and delete operations to children nodes. For each
/// operation container looks for a child at specified index position and
/// forwards operation to that child.
///
/// Most of the operation handling logic is implemented by [Line] and [Text].
abstract class Container<T extends Node?> extends Node {
final LinkedList<Node> _children = LinkedList<Node>();
/// List of children.
LinkedList<Node> get children => _children;
/// Returns total number of child nodes in this container.
///
/// To get text length of this container see [length].
int get childCount => _children.length;
/// Returns the first child [Node].
Node get first => _children.first;
/// Returns the last child [Node].
Node get last => _children.last;
/// Returns `true` if this container has no child nodes.
bool get isEmpty => _children.isEmpty;
/// Returns `true` if this container has at least 1 child.
bool get isNotEmpty => _children.isNotEmpty;
/// abstract methods begin
/// Returns an instance of default child for this container node.
///
/// Always returns fresh instance.
T get defaultChild;
/// abstract methods end
/// Adds [node] to the end of this container children list.
void add(T node) {
assert(node?.parent == null);
node?.parent = this;
_children.add(node as Node);
}
/// Adds [node] to the beginning of this container children list.
void addFirst(T node) {
assert(node?.parent == null);
node?.parent = this;
_children.addFirst(node as Node);
}
/// Removes [node] from this container.
void remove(T node) {
assert(node?.parent == this);
node?.parent = null;
_children.remove(node as Node);
}
/// Moves children of this node to [newParent].
void moveChildToNewParent(Container? newParent) {
if (isEmpty) {
return;
}
T? last = newParent!.isEmpty ? null : newParent.last as T?;
final last = newParent!.isEmpty ? null : newParent.last as T?;
while (isNotEmpty) {
T child = first as T;
final child = first as T;
child?.unlink();
newParent.add(child);
}
/// In case [newParent] already had children we need to make sure
/// combined list is optimized.
if (last != null) last.adjust();
}
/// Queries the child [Node] at specified character [offset] in this container.
///
/// The result may contain the found node or `null` if no node is found
/// at specified offset.
///
/// [ChildQuery.offset] is set to relative offset within returned child node
/// which points at the same character position in the document as the
/// original [offset].
ChildQuery queryChild(int offset, bool inclusive) {
if (offset < 0 || offset > length) {
return ChildQuery(null, 0);
}
for (Node node in children) {
int len = node.length;
for (final node in children) {
final len = node.length;
if (offset < len || (inclusive && offset == len && (node.isLast))) {
return ChildQuery(node, offset);
}
@ -76,6 +105,9 @@ abstract class Container<T extends Node?> extends Node {
@override
String toPlainText() => children.map((child) => child.toPlainText()).join();
/// Content length of this node's children.
///
/// To get number of children in this node use [childCount].
@override
int get length => _children.fold(0, (cur, node) => cur + node.length);
@ -84,14 +116,14 @@ abstract class Container<T extends Node?> extends Node {
assert(index == 0 || (index > 0 && index < length));
if (isNotEmpty) {
ChildQuery child = queryChild(index, false);
final child = queryChild(index, false);
child.node!.insert(child.offset, data, style);
return;
}
// empty
assert(index == 0);
T node = defaultChild;
final node = defaultChild;
add(node);
node?.insert(index, data, style);
}
@ -99,14 +131,14 @@ abstract class Container<T extends Node?> extends Node {
@override
void retain(int index, int? length, Style? attributes) {
assert(isNotEmpty);
ChildQuery child = queryChild(index, false);
final child = queryChild(index, false);
child.node!.retain(child.offset, length, attributes);
}
@override
void delete(int index, int? length) {
assert(isNotEmpty);
ChildQuery child = queryChild(index, false);
final child = queryChild(index, false);
child.node!.delete(child.offset, length);
}
@ -114,11 +146,15 @@ abstract class Container<T extends Node?> extends Node {
String toString() => _children.join('\n');
}
/// Query of a child in a Container
/// Result of a child query in a [Container].
class ChildQuery {
final Node? node; // null if not found
ChildQuery(this.node, this.offset);
final int offset;
/// The child node if found, otherwise `null`.
final Node? node;
ChildQuery(this.node, this.offset);
/// Starting offset within the child [node] which points at the same
/// character in the document as the original offset passed to
/// [Container.queryChild] method.
final int offset;
}

@ -1,26 +1,40 @@
/// An object which can be embedded into a Quill document.
///
/// See also:
///
/// * [BlockEmbed] which represents a block embed.
class Embeddable {
Embeddable(this.type, this.data);
/// The type of this object.
final String type;
final dynamic data;
Embeddable(this.type, this.data);
/// The data payload of this object.
final dynamic data;
Map<String, dynamic> toJson() {
Map<String, String> m = {type: data};
final m = <String, String>{type: data};
return m;
}
static Embeddable fromJson(Map<String, dynamic> json) {
Map<String, dynamic> m = Map<String, dynamic>.from(json);
final m = Map<String, dynamic>.from(json);
assert(m.length == 1, 'Embeddable map has one key');
return BlockEmbed(m.keys.first, m.values.first);
}
}
/// An object which occupies an entire line in a document and cannot co-exist
/// inline with regular text.
///
/// There are two built-in embed types supported by Quill documents, however
/// the document model itself does not make any assumptions about the types
/// of embedded objects and allows users to define their own types.
class BlockEmbed extends Embeddable {
BlockEmbed(String type, String data) : super(type, data);
static final BlockEmbed horizontalRule = BlockEmbed('divider', 'hr');
static BlockEmbed horizontalRule = BlockEmbed('divider', 'hr');
static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl);
}

@ -1,29 +1,30 @@
import 'dart:math' as math;
import 'package:flutter_quill/models/quill_delta.dart';
import '../../quill_delta.dart';
import '../style.dart';
import 'embed.dart';
import 'line.dart';
import 'node.dart';
/* A leaf node in document tree */
/// A leaf in Quill document tree.
abstract class Leaf extends Node {
Object _value;
Object get value => _value;
Leaf.val(Object val) : _value = val;
/// Creates a new [Leaf] with specified [data].
factory Leaf(Object data) {
if (data is Embeddable) {
return Embed(data);
}
String text = data as String;
final text = data as String;
assert(text.isNotEmpty);
return Text(text);
}
Leaf.val(Object val) : _value = val;
/// Contents of this node, either a String if this is a [Text] or an
/// [Embed] if this is an [BlockEmbed].
Object get value => _value;
Object _value;
@override
void applyStyle(Style value) {
assert(value.isInline || value.isIgnored || value.isEmpty,
@ -45,14 +46,15 @@ abstract class Leaf extends Node {
@override
Delta toDelta() {
var data = _value is Embeddable ? (_value as Embeddable).toJson() : _value;
final data =
_value is Embeddable ? (_value as Embeddable).toJson() : _value;
return Delta()..insert(data, style.toJson());
}
@override
void insert(int index, Object data, Style? style) {
assert(index >= 0 && index <= length);
Leaf node = Leaf(data);
final node = Leaf(data);
if (index < length) {
splitAt(index)!.insertBefore(node);
} else {
@ -67,9 +69,9 @@ abstract class Leaf extends Node {
return;
}
int local = math.min(length - index, len!);
int remain = len - local;
Leaf node = _isolate(index, local);
final local = math.min(length - index, len!);
final remain = len - local;
final node = _isolate(index, local);
if (remain > 0) {
assert(node.next != null);
@ -82,13 +84,13 @@ abstract class Leaf extends Node {
void delete(int index, int? len) {
assert(index < length);
int local = math.min(length - index, len!);
Leaf target = _isolate(index, local);
Leaf? prev = target.previous as Leaf?;
Leaf? next = target.next as Leaf?;
final local = math.min(length - index, len!);
final target = _isolate(index, local);
final prev = target.previous as Leaf?;
final next = target.next as Leaf?;
target.unlink();
int remain = len - local;
final remain = len - local;
if (remain > 0) {
assert(next != null);
next!.delete(0, remain);
@ -99,36 +101,47 @@ abstract class Leaf extends Node {
}
}
/// Adjust this text node by merging it with adjacent nodes if they share
/// the same style.
@override
void adjust() {
if (this is Embed) {
// Embed nodes cannot be merged with text nor other embeds (in fact,
// there could be no two adjacent embeds on the same line since an
// embed occupies an entire line).
return;
}
Text node = this as Text;
// merging it with previous node if style is the same
Node? prev = node.previous;
// This is a text node and it can only be merged with other text nodes.
var node = this as Text;
// Merging it with previous node if style is the same.
final prev = node.previous;
if (!node.isFirst && prev is Text && prev.style == node.style) {
prev._value = prev.value + node.value;
node.unlink();
node = prev;
}
// merging it with next node if style is the same
Node? next = node.next;
// Merging it with next node if style is the same.
final next = node.next;
if (!node.isLast && next is Text && next.style == node.style) {
node._value = node.value + next.value;
next.unlink();
}
}
Leaf? cutAt(int index) {
assert(index >= 0 && index <= length);
Leaf? cut = splitAt(index);
cut?.unlink();
return cut;
}
/// Splits this leaf node at [index] and returns new node.
///
/// If this is the last node in its list and [index] equals this node's
/// length then this method returns `null` as there is nothing left to split.
/// If there is another leaf node after this one and [index] equals this
/// node's length then the next leaf node is returned.
///
/// If [index] equals to `0` then this node itself is returned unchanged.
///
/// In case a new node is actually split from this one, it inherits this
/// node's style.
Leaf? splitAt(int index) {
assert(index >= 0 && index <= length);
if (index == 0) {
@ -139,64 +152,101 @@ abstract class Leaf extends Node {
}
assert(this is Text);
String text = _value as String;
final text = _value as String;
_value = text.substring(0, index);
Leaf split = Leaf(text.substring(index));
split.applyStyle(style);
final split = Leaf(text.substring(index))..applyStyle(style);
insertAfter(split);
return split;
}
/// Cuts a leaf from [index] to the end of this node and returns new node
/// in detached state (e.g. [mounted] returns `false`).
///
/// Splitting logic is identical to one described in [splitAt], meaning this
/// method may return `null`.
Leaf? cutAt(int index) {
assert(index >= 0 && index <= length);
final cut = splitAt(index);
cut?.unlink();
return cut;
}
/// Formats this node and optimizes it with adjacent leaf nodes if needed.
void format(Style? style) {
if (style != null && style.isNotEmpty) {
applyStyle(style);
}
adjust();
}
/// Isolates a new leaf starting at [index] with specified [length].
///
/// Splitting logic is identical to one described in [splitAt], with one
/// exception that it is required for [index] to always be less than this
/// node's length. As a result this method always returns a [LeafNode]
/// instance. Returned node may still be the same as this node
/// if provided [index] is `0`.
Leaf _isolate(int index, int length) {
assert(
index >= 0 && index < this.length && (index + length <= this.length));
Leaf target = splitAt(index)!;
target.splitAt(length);
final target = splitAt(index)!..splitAt(length);
return target;
}
}
/// A span of formatted text within a line in a Quill document.
///
/// Text is a leaf node of a document tree.
///
/// Parent of a text node is always a [Line], and as a consequence text
/// node's [value] cannot contain any line-break characters.
///
/// See also:
///
/// * [Embed], a leaf node representing an embeddable object.
/// * [Line], a node representing a line of text.
class Text extends Leaf {
Text([String text = ''])
: assert(!text.contains('\n')),
super.val(text);
@override
String get value => _value as String;
Node newInstance() => Text();
@override
String toPlainText() {
return value;
}
String get value => _value as String;
@override
Node newInstance() {
return Text();
}
String toPlainText() => value;
}
/// An embedded node such as image or video
/// An embed node inside of a line in a Quill document.
///
/// Embed node is a leaf node similar to [Text]. It represents an arbitrary
/// piece of non-textual content embedded into a document, such as, image,
/// horizontal rule, video, or any other object with defined structure,
/// like a tweet, for instance.
///
/// Embed node's length is always `1` character and it is represented with
/// unicode object replacement character in the document text.
///
/// Any inline style can be applied to an embed, however this does not
/// necessarily mean the embed will look according to that style. For instance,
/// applying "bold" style to an image gives no effect, while adding a "link" to
/// an image actually makes the image react to user's action.
class Embed extends Leaf {
Embed(Embeddable data) : super.val(data);
static const kObjectReplacementCharacter = '\uFFFC';
@override
Embeddable get value => super.value as Embeddable;
Node newInstance() => throw UnimplementedError();
@override
String toPlainText() {
return '\uFFFC';
}
Embeddable get value => super.value as Embeddable;
/// // Embed nodes are represented as unicode object replacement character in
// plain text.
@override
Node newInstance() {
throw UnimplementedError();
}
String toPlainText() => kObjectReplacementCharacter;
}

@ -1,15 +1,20 @@
import 'dart:math' as math;
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import '../../quill_delta.dart';
import '../attribute.dart';
import '../style.dart';
import 'block.dart';
import 'container.dart';
import 'embed.dart';
import 'leaf.dart';
import 'node.dart';
/// A line of rich text in a Quill document.
///
/// Line serves as a container for [Leaf]s, like [Text] and [Embed].
///
/// When a line contains an embed, it fully occupies the line, no other embeds
/// or text nodes are allowed.
class Line extends Container<Leaf?> {
@override
Leaf get defaultChild => Text();
@ -17,6 +22,7 @@ class Line extends Container<Leaf?> {
@override
int get length => super.length + 1;
/// Returns `true` if this line contains an embedded object.
bool get hasEmbed {
if (childCount != 1) {
return false;
@ -25,6 +31,7 @@ class Line extends Container<Leaf?> {
return children.single is Embed;
}
/// Returns next [Line] or `null` if this is the last line in the document.
Line? get nextLine {
if (!isLast) {
return next is Block ? (next as Block).first as Line? : next as Line?;
@ -41,6 +48,9 @@ class Line extends Container<Leaf?> {
: parent!.next as Line?;
}
@override
Node newInstance() => Line();
@override
Delta toDelta() {
final delta = children
@ -48,7 +58,7 @@ class Line extends Container<Leaf?> {
.fold(Delta(), (dynamic a, b) => a.concat(b));
var attributes = style;
if (parent is Block) {
Block block = parent as Block;
final block = parent as Block;
attributes = attributes.mergeAll(block.style);
}
delta.insert('\n', attributes.toJson());
@ -56,7 +66,7 @@ class Line extends Container<Leaf?> {
}
@override
String toPlainText() => super.toPlainText() + '\n';
String toPlainText() => '${super.toPlainText()}\n';
@override
String toString() {
@ -68,35 +78,43 @@ class Line extends Container<Leaf?> {
@override
void insert(int index, Object data, Style? style) {
if (data is Embeddable) {
_insert(index, data, style);
// We do not check whether this line already has any children here as
// inserting an embed into a line with other text is acceptable from the
// Delta format perspective.
// We rely on heuristic rules to ensure that embeds occupy an entire line.
_insertSafe(index, data, style);
return;
}
String text = data as String;
int lineBreak = text.indexOf('\n');
final text = data as String;
final lineBreak = text.indexOf('\n');
if (lineBreak < 0) {
_insert(index, text, style);
_insertSafe(index, text, style);
// No need to update line or block format since those attributes can only
// be attached to `\n` character and we already know it's not present.
return;
}
String prefix = text.substring(0, lineBreak);
_insert(index, prefix, style);
final prefix = text.substring(0, lineBreak);
_insertSafe(index, prefix, style);
if (prefix.isNotEmpty) {
index += prefix.length;
}
Line nextLine = _getNextLine(index);
// Next line inherits our format.
final nextLine = _getNextLine(index);
// Reset our format and unwrap from a block if needed.
clearStyle();
if (parent is Block) {
_unwrap();
}
// Now we can apply new format and re-layout.
_format(style);
// Continue with the remaining
String remain = text.substring(lineBreak + 1);
// Continue with remaining part.
final remain = text.substring(lineBreak + 1);
nextLine.insert(0, remain, style);
}
@ -105,20 +123,24 @@ class Line extends Container<Leaf?> {
if (style == null) {
return;
}
int thisLen = length;
final thisLength = length;
int local = math.min(thisLen - index, len!);
final local = math.min(thisLength - index, len!);
// If index is at newline character then this is a line/block style update.
final isLineFormat = (index + local == thisLength) && local == 1;
if (index + local == thisLen && local == 1) {
assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK));
if (isLineFormat) {
assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK),
'It is not allowed to apply inline attributes to line itself.');
_format(style);
} else {
// Otherwise forward to children as it's an inline format update.
assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE));
assert(index + local != thisLen);
assert(index + local != thisLength);
super.retain(index, local, style);
}
int remain = len - local;
final remain = len - local;
if (remain > 0) {
assert(nextLine != null);
nextLine!.retain(0, remain, style);
@ -127,66 +149,80 @@ class Line extends Container<Leaf?> {
@override
void delete(int index, int? len) {
int local = math.min(length - index, len!);
bool deleted = index + local == length;
if (deleted) {
final local = math.min(length - index, len!);
final isLFDeleted = index + local == length; // Line feed
if (isLFDeleted) {
// Our newline character deleted with all style information.
clearStyle();
if (local > 1) {
// Exclude newline character from delete range for children.
super.delete(index, local - 1);
}
} else {
super.delete(index, local);
}
int remain = len - local;
if (remain > 0) {
final remaining = len - local;
if (remaining > 0) {
assert(nextLine != null);
nextLine!.delete(0, remain);
nextLine!.delete(0, remaining);
}
if (deleted && isNotEmpty) {
if (isLFDeleted && isNotEmpty) {
// Since we lost our line-break and still have child text nodes those must
// migrate to the next line.
// nextLine might have been unmounted since last assert so we need to
// check again we still have a line after us.
assert(nextLine != null);
// Move remaining children in this line to the next line so that all
// attributes of nextLine are preserved.
nextLine!.moveChildToNewParent(this);
moveChildToNewParent(nextLine);
}
if (deleted) {
Node p = parent!;
if (isLFDeleted) {
// Now we can remove this line.
final block = parent!; // remember reference before un-linking.
unlink();
p.adjust();
block.adjust();
}
}
/// Formats this line.
void _format(Style? newStyle) {
if (newStyle == null || newStyle.isEmpty) {
return;
}
applyStyle(newStyle);
Attribute? blockStyle = newStyle.getBlockExceptHeader();
final blockStyle = newStyle.getBlockExceptHeader();
if (blockStyle == null) {
return;
}
} // No block-level changes
if (parent is Block) {
Attribute? parentStyle = (parent as Block).style.getBlockExceptHeader();
final parentStyle = (parent as Block).style.getBlockExceptHeader();
if (blockStyle.value == null) {
_unwrap();
} else if (blockStyle != parentStyle) {
_unwrap();
Block block = Block();
block.applyAttribute(blockStyle);
final block = Block()..applyAttribute(blockStyle);
_wrap(block);
block.adjust();
}
} // else the same style, no-op.
} else if (blockStyle.value != null) {
Block block = Block();
block.applyAttribute(blockStyle);
// Only wrap with a new block if this is not an unset
final block = Block()..applyAttribute(blockStyle);
_wrap(block);
block.adjust();
}
}
/// Wraps this line with new parent [block].
///
/// This line can not be in a [Block] when this method is called.
void _wrap(Block block) {
assert(parent != null && parent is! Block);
insertAfter(block);
@ -194,11 +230,14 @@ class Line extends Container<Leaf?> {
block.add(this);
}
/// Unwraps this line from it's parent [Block].
///
/// This method asserts if current [parent] of this line is not a [Block].
void _unwrap() {
if (parent is! Block) {
throw ArgumentError('Invalid parent');
}
Block block = parent as Block;
final block = parent as Block;
assert(block.children.contains(this));
@ -209,10 +248,10 @@ class Line extends Container<Leaf?> {
unlink();
block.insertAfter(this);
} else {
Block before = block.clone() as Block;
final before = block.clone() as Block;
block.insertBefore(before);
Line child = block.first as Line;
var child = block.first as Line;
while (child != this) {
child.unlink();
before.add(child);
@ -227,26 +266,25 @@ class Line extends Container<Leaf?> {
Line _getNextLine(int index) {
assert(index == 0 || (index > 0 && index < length));
Line line = clone() as Line;
final line = clone() as Line;
insertAfter(line);
if (index == length - 1) {
return line;
}
ChildQuery query = queryChild(index, false);
final query = queryChild(index, false);
while (!query.node!.isLast) {
Leaf next = last as Leaf;
next.unlink();
final next = (last as Leaf)..unlink();
line.addFirst(next);
}
Leaf child = query.node as Leaf;
Leaf? cut = child.splitAt(query.offset);
final child = query.node as Leaf;
final cut = child.splitAt(query.offset);
cut?.unlink();
line.addFirst(cut);
return line;
}
void _insert(int index, Object data, Style? style) {
void _insertSafe(int index, Object data, Style? style) {
assert(index == 0 || (index > 0 && index < length));
if (data is String) {
@ -256,47 +294,51 @@ class Line extends Container<Leaf?> {
}
}
if (isNotEmpty) {
ChildQuery result = queryChild(index, true);
if (isEmpty) {
final child = Leaf(data);
add(child);
child.format(style);
} else {
final result = queryChild(index, true);
result.node!.insert(result.offset, data, style);
return;
}
Leaf child = Leaf(data);
add(child);
child.format(style);
}
@override
Node newInstance() {
return Line();
}
/// Returns style for specified text range.
///
/// Only attributes applied to all characters within this range are
/// included in the result. Inline and line level attributes are
/// handled separately, e.g.:
///
/// - line attribute X is included in the result only if it exists for
/// every line within this range (partially included lines are counted).
/// - inline attribute X is included in the result only if it exists
/// for every character within this range (line-break characters excluded).
Style collectStyle(int offset, int len) {
int local = math.min(length - offset, len);
Style res = Style();
var excluded = <Attribute>{};
final local = math.min(length - offset, len);
var result = Style();
final excluded = <Attribute>{};
void _handle(Style style) {
if (res.isEmpty) {
if (result.isEmpty) {
excluded.addAll(style.values);
} else {
for (Attribute attr in res.values) {
for (final attr in result.values) {
if (!style.containsKey(attr.key)) {
excluded.add(attr);
}
}
}
Style remain = style.removeAll(excluded);
res = res.removeAll(excluded);
res = res.mergeAll(remain);
final remaining = style.removeAll(excluded);
result = result.removeAll(excluded);
result = result.mergeAll(remaining);
}
ChildQuery data = queryChild(offset, true);
Leaf? node = data.node as Leaf?;
final data = queryChild(offset, true);
var node = data.node as Leaf?;
if (node != null) {
res = res.mergeAll(node.style);
int pos = node.length - data.offset;
result = result.mergeAll(node.style);
var pos = node.length - data.offset;
while (!node!.isLast && pos < local) {
node = node.next as Leaf?;
_handle(node!.style);
@ -304,17 +346,18 @@ class Line extends Container<Leaf?> {
}
}
res = res.mergeAll(style);
result = result.mergeAll(style);
if (parent is Block) {
Block block = parent as Block;
res = res.mergeAll(block.style);
final block = parent as Block;
result = result.mergeAll(block.style);
}
int remain = len - local;
if (remain > 0) {
_handle(nextLine!.collectStyle(0, remain));
final remaining = len - local;
if (remaining > 0) {
final rest = nextLine!.collectStyle(0, remaining);
_handle(rest);
}
return res;
return result;
}
}

@ -1,51 +1,49 @@
import 'dart:collection';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import '../../quill_delta.dart';
import '../attribute.dart';
import '../style.dart';
import 'container.dart';
import 'line.dart';
/* node in a document tree */
/// An abstract node in a document tree.
///
/// Represents a segment of a Quill document with specified [offset]
/// and [length].
///
/// The [offset] property is relative to [parent]. See also [documentOffset]
/// which provides absolute offset of this node within the document.
///
/// The current parent node is exposed by the [parent] property.
abstract class Node extends LinkedListEntry<Node> {
/// Current parent of this node. May be null if this node is not mounted.
Container? parent;
Style _style = Style();
Style get style => _style;
Style _style = Style();
void applyAttribute(Attribute attribute) {
_style = _style.merge(attribute);
}
void applyStyle(Style value) {
_style = _style.mergeAll(value);
}
void clearStyle() {
_style = Style();
}
/// Returns `true` if this node is the first node in the [parent] list.
bool get isFirst => list!.first == this;
/// Returns `true` if this node is the last node in the [parent] list.
bool get isLast => list!.last == this;
/// Length of this node in characters.
int get length;
Node clone() {
Node node = newInstance();
node.applyStyle(style);
return node;
}
Node clone() => newInstance()..applyStyle(style);
int getOffset() {
int offset = 0;
/// Offset in characters of this node relative to [parent] node.
///
/// To get offset of this node in the document see [documentOffset].
int get offset {
var offset = 0;
if (list == null || isFirst) {
return offset;
}
Node cur = this;
var cur = this;
do {
cur = cur.previous!;
offset += cur.length;
@ -53,16 +51,31 @@ abstract class Node extends LinkedListEntry<Node> {
return offset;
}
int getDocumentOffset() {
final parentOffset = (parent is! Root) ? parent!.getDocumentOffset() : 0;
return parentOffset + getOffset();
/// Offset in characters of this node in the document.
int get documentOffset {
final parentOffset = (parent is! Root) ? parent!.documentOffset : 0;
return parentOffset + offset;
}
/// Returns `true` if this node contains character at specified [offset] in
/// the document.
bool containsOffset(int offset) {
final o = getDocumentOffset();
final o = documentOffset;
return o <= offset && offset < o + length;
}
void applyAttribute(Attribute attribute) {
_style = _style.merge(attribute);
}
void applyStyle(Style value) {
_style = _style.mergeAll(value);
}
void clearStyle() {
_style = Style();
}
@override
void insertBefore(Node entry) {
assert(entry.parent == null && parent != null);
@ -84,9 +97,7 @@ abstract class Node extends LinkedListEntry<Node> {
super.unlink();
}
void adjust() {
// do nothing
}
void adjust() {/* no-op */}
/// abstract methods begin
@ -103,11 +114,13 @@ abstract class Node extends LinkedListEntry<Node> {
void delete(int index, int? len);
/// abstract methods end
}
/* Root node of document tree */
/// Root node of document tree.
class Root extends Container<Container<Node?>> {
@override
Node newInstance() => Root();
@override
Container<Node?> get defaultChild => Line();
@ -115,9 +128,4 @@ class Root extends Container<Container<Node?>> {
Delta toDelta() => children
.map((child) => child.toDelta())
.fold(Delta(), (a, b) => a.concat(b));
@override
Node newInstance() {
return Root();
}
}

@ -1,22 +1,23 @@
import 'package:collection/collection.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:quiver/core.dart';
import 'attribute.dart';
/* Collection of style attributes */
class Style {
final Map<String, Attribute> _attributes;
Style() : _attributes = <String, Attribute>{};
Style.attr(this._attributes);
Style() : _attributes = <String, Attribute>{};
final Map<String, Attribute> _attributes;
static Style fromJson(Map<String, dynamic>? attributes) {
if (attributes == null) {
return Style();
}
Map<String, Attribute> result = attributes.map((String key, dynamic value) {
Attribute attr = Attribute.fromKeyValue(key, value);
final result = attributes.map((key, dynamic value) {
final attr = Attribute.fromKeyValue(key, value);
return MapEntry<String, Attribute>(key, attr);
});
return Style.attr(result);
@ -24,7 +25,7 @@ class Style {
Map<String, dynamic>? toJson() => _attributes.isEmpty
? null
: _attributes.map<String, dynamic>((String _, Attribute attribute) =>
: _attributes.map<String, dynamic>((_, attribute) =>
MapEntry<String, dynamic>(attribute.key, attribute.value));
Iterable<String> get keys => _attributes.keys;
@ -47,7 +48,7 @@ class Style {
bool containsKey(String key) => _attributes.containsKey(key);
Attribute? getBlockExceptHeader() {
for (Attribute val in values) {
for (final val in values) {
if (val.isBlockExceptHeader) {
return val;
}
@ -56,7 +57,7 @@ class Style {
}
Style merge(Attribute attribute) {
Map<String, Attribute> merged = Map<String, Attribute>.from(_attributes);
final merged = Map<String, Attribute>.from(_attributes);
if (attribute.value == null) {
merged.remove(attribute.key);
} else {
@ -66,21 +67,21 @@ class Style {
}
Style mergeAll(Style other) {
Style result = Style.attr(_attributes);
for (Attribute attribute in other.values) {
var result = Style.attr(_attributes);
for (final attribute in other.values) {
result = result.merge(attribute);
}
return result;
}
Style removeAll(Set<Attribute> attributes) {
Map<String, Attribute> merged = Map<String, Attribute>.from(_attributes);
final merged = Map<String, Attribute>.from(_attributes);
attributes.map((item) => item.key).forEach(merged.remove);
return Style.attr(merged);
}
Style put(Attribute attribute) {
Map<String, Attribute> m = Map<String, Attribute>.from(attributes);
final m = Map<String, Attribute>.from(attributes);
m[attribute.key] = attribute;
return Style.attr(m);
}
@ -93,8 +94,8 @@ class Style {
if (other is! Style) {
return false;
}
Style typedOther = other;
final eq = const MapEquality<String, Attribute>();
final typedOther = other;
const eq = MapEquality<String, Attribute>();
return eq.equals(_attributes, typedOther._attributes);
}

@ -22,6 +22,29 @@ Object? _passThroughDataDecoder(Object? data) => data;
/// Operation performed on a rich-text document.
class Operation {
Operation._(this.key, this.length, this.data, Map? attributes)
: assert(_validKeys.contains(key), 'Invalid operation key "$key".'),
assert(() {
if (key != Operation.insertKey) return true;
return data is String ? data.length == length : length == 1;
}(), 'Length of insert operation must be equal to the data length.'),
_attributes =
attributes != null ? Map<String, dynamic>.from(attributes) : null;
/// Creates operation which deletes [length] of characters.
factory Operation.delete(int length) =>
Operation._(Operation.deleteKey, length, '', null);
/// Creates operation which inserts [text] with optional [attributes].
factory Operation.insert(dynamic data, [Map<String, dynamic>? attributes]) =>
Operation._(Operation.insertKey, data is String ? data.length : 1, data,
attributes);
/// Creates operation which retains [length] of characters and optionally
/// applies attributes.
factory Operation.retain(int? length, [Map<String, dynamic>? attributes]) =>
Operation._(Operation.retainKey, length, '', attributes);
/// Key of insert operations.
static const String insertKey = 'insert';
@ -50,15 +73,6 @@ class Operation {
_attributes == null ? null : Map<String, dynamic>.from(_attributes!);
final Map<String, dynamic>? _attributes;
Operation._(this.key, this.length, this.data, Map? attributes)
: assert(_validKeys.contains(key), 'Invalid operation key "$key".'),
assert(() {
if (key != Operation.insertKey) return true;
return data is String ? data.length == length : length == 1;
}(), 'Length of insert operation must be equal to the data length.'),
_attributes =
attributes != null ? Map<String, dynamic>.from(attributes) : null;
/// Creates new [Operation] from JSON payload.
///
/// If `dataDecoder` parameter is not null then it is used to additionally
@ -89,20 +103,6 @@ class Operation {
return json;
}
/// Creates operation which deletes [length] of characters.
factory Operation.delete(int length) =>
Operation._(Operation.deleteKey, length, '', null);
/// Creates operation which inserts [text] with optional [attributes].
factory Operation.insert(dynamic data, [Map<String, dynamic>? attributes]) =>
Operation._(Operation.insertKey, data is String ? data.length : 1, data,
attributes);
/// Creates operation which retains [length] of characters and optionally
/// applies attributes.
factory Operation.retain(int? length, [Map<String, dynamic>? attributes]) =>
Operation._(Operation.retainKey, length, '', attributes);
/// Returns value of this operation.
///
/// For insert operations this returns text, for delete and retain - length.
@ -135,7 +135,7 @@ class Operation {
bool operator ==(other) {
if (identical(this, other)) return true;
if (other is! Operation) return false;
Operation typedOther = other;
final typedOther = other;
return key == typedOther.key &&
length == typedOther.length &&
_valueEquality.equals(data, typedOther.data) &&
@ -180,6 +180,15 @@ class Operation {
/// "document delta". When delta includes also "retain" or "delete" operations
/// it is a "change delta".
class Delta {
/// Creates new empty [Delta].
factory Delta() => Delta._(<Operation>[]);
Delta._(List<Operation> operations) : _operations = operations;
/// Creates new [Delta] from [other].
factory Delta.from(Delta other) =>
Delta._(List<Operation>.from(other._operations));
/// Transforms two attribute sets.
static Map<String, dynamic>? transformAttributes(
Map<String, dynamic>? a, Map<String, dynamic>? b, bool priority) {
@ -221,14 +230,14 @@ class Delta {
attr ??= const {};
base ??= const {};
var baseInverted = base.keys.fold({}, (dynamic memo, key) {
final baseInverted = base.keys.fold({}, (dynamic memo, key) {
if (base![key] != attr![key] && attr.containsKey(key)) {
memo[key] = base[key];
}
return memo;
});
var inverted =
final inverted =
Map<String, dynamic>.from(attr.keys.fold(baseInverted, (memo, key) {
if (base![key] != attr![key] && !base.containsKey(key)) {
memo[key] = null;
@ -242,15 +251,6 @@ class Delta {
int _modificationCount = 0;
Delta._(List<Operation> operations) : _operations = operations;
/// Creates new empty [Delta].
factory Delta() => Delta._(<Operation>[]);
/// Creates new [Delta] from [other].
factory Delta.from(Delta other) =>
Delta._(List<Operation>.from(other._operations));
/// Creates [Delta] from de-serialized JSON representation.
///
/// If `dataDecoder` parameter is not null then it is used to additionally
@ -292,9 +292,8 @@ class Delta {
bool operator ==(dynamic other) {
if (identical(this, other)) return true;
if (other is! Delta) return false;
Delta typedOther = other;
final comparator =
const ListEquality<Operation>(DefaultEquality<Operation>());
final typedOther = other;
const comparator = ListEquality<Operation>(DefaultEquality<Operation>());
return comparator.equals(_operations, typedOther._operations);
}
@ -529,7 +528,8 @@ class Delta {
if (op.isDelete) {
inverted.push(baseOp);
} else if (op.isRetain && op.isNotPlain) {
var invertAttr = invertAttributes(op.attributes, baseOp.attributes);
final invertAttr =
invertAttributes(op.attributes, baseOp.attributes);
inverted.retain(
baseOp.length!, invertAttr.isEmpty ? null : invertAttr);
}
@ -548,7 +548,7 @@ class Delta {
Delta slice(int start, [int? end]) {
final delta = Delta();
var index = 0;
var opIterator = DeltaIterator(this);
final opIterator = DeltaIterator(this);
final actualEnd = end ?? double.infinity;
@ -599,13 +599,13 @@ class Delta {
/// Specialized iterator for [Delta]s.
class DeltaIterator {
DeltaIterator(this.delta) : _modificationCount = delta._modificationCount;
final Delta delta;
final int _modificationCount;
int _index = 0;
num _offset = 0;
DeltaIterator(this.delta) : _modificationCount = delta._modificationCount;
bool get isNextInsert => nextOperationKey == Operation.insertKey;
bool get isNextDelete => nextOperationKey == Operation.deleteKey;
@ -661,7 +661,7 @@ class DeltaIterator {
final opIsNotEmpty =
opData is String ? opData.isNotEmpty : true; // embeds are never empty
final opLength = opData is String ? opData.length : 1;
final int opActualLength = opIsNotEmpty ? opLength : actualLength as int;
final opActualLength = opIsNotEmpty ? opLength : actualLength as int;
return Operation._(opKey, opActualLength, opData, opAttributes);
}
return Operation.retain(length);

@ -1,6 +1,6 @@
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:flutter_quill/models/rules/rule.dart';
import '../documents/attribute.dart';
import '../quill_delta.dart';
import 'rule.dart';
abstract class DeleteRule extends Rule {
const DeleteRule();
@ -34,34 +34,33 @@ class PreserveLineStyleOnMergeRule extends DeleteRule {
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
Operation op = itr.next(1);
final itr = DeltaIterator(document)..skip(index);
var op = itr.next(1);
if (op.data != '\n') {
return null;
}
bool isNotPlain = op.isNotPlain;
Map<String, dynamic>? attrs = op.attributes;
final isNotPlain = op.isNotPlain;
final attrs = op.attributes;
itr.skip(len! - 1);
Delta delta = Delta()
final delta = Delta()
..retain(index)
..delete(len);
while (itr.hasNext) {
op = itr.next();
String text = op.data is String ? (op.data as String?)! : '';
int lineBreak = text.indexOf('\n');
final text = op.data is String ? (op.data as String?)! : '';
final lineBreak = text.indexOf('\n');
if (lineBreak == -1) {
delta.retain(op.length!);
continue;
}
Map<String, dynamic>? attributes = op.attributes == null
var attributes = op.attributes == null
? null
: op.attributes!.map<String, dynamic>((String key, dynamic value) =>
MapEntry<String, dynamic>(key, null));
: op.attributes!.map<String, dynamic>(
(key, dynamic value) => MapEntry<String, dynamic>(key, null));
if (isNotPlain) {
attributes ??= <String, dynamic>{};
@ -80,15 +79,15 @@ class EnsureEmbedLineRule extends DeleteRule {
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
DeltaIterator itr = DeltaIterator(document);
final itr = DeltaIterator(document);
Operation? op = itr.skip(index);
var op = itr.skip(index);
int? indexDelta = 0, lengthDelta = 0, remain = len;
bool embedFound = op != null && op.data is! String;
bool hasLineBreakBefore =
var embedFound = op != null && op.data is! String;
final hasLineBreakBefore =
!embedFound && (op == null || (op.data as String).endsWith('\n'));
if (embedFound) {
Operation candidate = itr.next(1);
var candidate = itr.next(1);
if (remain != null) {
remain--;
if (candidate.data == '\n') {
@ -107,7 +106,7 @@ class EnsureEmbedLineRule extends DeleteRule {
op = itr.skip(remain!);
if (op != null &&
(op.data is String ? op.data as String? : '')!.endsWith('\n')) {
Operation candidate = itr.next(1);
final candidate = itr.next(1);
if (candidate.data is! String && !hasLineBreakBefore) {
embedFound = true;
lengthDelta--;

@ -1,6 +1,6 @@
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:flutter_quill/models/rules/rule.dart';
import '../documents/attribute.dart';
import '../quill_delta.dart';
import 'rule.dart';
abstract class FormatRule extends Rule {
const FormatRule();
@ -26,21 +26,20 @@ class ResolveLineFormatRule extends FormatRule {
return null;
}
Delta delta = Delta()..retain(index);
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
var delta = Delta()..retain(index);
final itr = DeltaIterator(document)..skip(index);
Operation op;
for (int cur = 0; cur < len! && itr.hasNext; cur += op.length!) {
for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) {
op = itr.next(len - cur);
if (op.data is! String || !(op.data as String).contains('\n')) {
delta.retain(op.length!);
continue;
}
String text = op.data as String;
Delta tmp = Delta();
int offset = 0;
final text = op.data as String;
final tmp = Delta();
var offset = 0;
for (int lineBreak = text.indexOf('\n');
for (var lineBreak = text.indexOf('\n');
lineBreak >= 0;
lineBreak = text.indexOf('\n', offset)) {
tmp..retain(lineBreak - offset)..retain(1, attribute.toJson());
@ -52,8 +51,8 @@ class ResolveLineFormatRule extends FormatRule {
while (itr.hasNext) {
op = itr.next();
String text = op.data is String ? (op.data as String?)! : '';
int lineBreak = text.indexOf('\n');
final text = op.data is String ? (op.data as String?)! : '';
final lineBreak = text.indexOf('\n');
if (lineBreak < 0) {
delta.retain(op.length!);
continue;
@ -75,9 +74,9 @@ class FormatLinkAtCaretPositionRule extends FormatRule {
return null;
}
Delta delta = Delta();
DeltaIterator itr = DeltaIterator(document);
Operation? before = itr.skip(index), after = itr.next();
final delta = Delta();
final itr = DeltaIterator(document);
final before = itr.skip(index), after = itr.next();
int? beg = index, retain = 0;
if (before != null && before.hasAttribute(attribute.key)) {
beg -= before.length!;
@ -105,20 +104,19 @@ class ResolveInlineFormatRule extends FormatRule {
return null;
}
Delta delta = Delta()..retain(index);
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
final delta = Delta()..retain(index);
final itr = DeltaIterator(document)..skip(index);
Operation op;
for (int cur = 0; cur < len! && itr.hasNext; cur += op.length!) {
for (var cur = 0; cur < len! && itr.hasNext; cur += op.length!) {
op = itr.next(len - cur);
String text = op.data is String ? (op.data as String?)! : '';
int lineBreak = text.indexOf('\n');
final text = op.data is String ? (op.data as String?)! : '';
var lineBreak = text.indexOf('\n');
if (lineBreak < 0) {
delta.retain(op.length!, attribute.toJson());
continue;
}
int pos = 0;
var pos = 0;
while (lineBreak >= 0) {
delta..retain(lineBreak - pos, attribute.toJson())..retain(1);
pos = lineBreak + 1;

@ -1,9 +1,10 @@
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:flutter_quill/models/rules/rule.dart';
import 'package:tuple/tuple.dart';
import '../documents/attribute.dart';
import '../documents/style.dart';
import '../quill_delta.dart';
import 'rule.dart';
abstract class InsertRule extends Rule {
const InsertRule();
@ -28,28 +29,28 @@ class PreserveLineStyleOnSplitRule extends InsertRule {
return null;
}
DeltaIterator itr = DeltaIterator(document);
Operation? before = itr.skip(index);
final itr = DeltaIterator(document);
final before = itr.skip(index);
if (before == null ||
before.data is! String ||
(before.data as String).endsWith('\n')) {
return null;
}
Operation after = itr.next();
final after = itr.next();
if (after.data is! String || (after.data as String).startsWith('\n')) {
return null;
}
final text = after.data as String;
Delta delta = Delta()..retain(index);
final delta = Delta()..retain(index);
if (text.contains('\n')) {
assert(after.isPlain);
delta.insert('\n');
return delta;
}
Tuple2<Operation?, int?> nextNewLine = _getNextNewLine(itr);
Map<String, dynamic>? attributes = nextNewLine.item1?.attributes;
final nextNewLine = _getNextNewLine(itr);
final attributes = nextNewLine.item1?.attributes;
return delta..insert('\n', attributes);
}
@ -65,19 +66,18 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
return null;
}
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
final itr = DeltaIterator(document)..skip(index);
Tuple2<Operation?, int?> nextNewLine = _getNextNewLine(itr);
Style lineStyle =
final nextNewLine = _getNextNewLine(itr);
final lineStyle =
Style.fromJson(nextNewLine.item1?.attributes ?? <String, dynamic>{});
Attribute? attribute = lineStyle.getBlockExceptHeader();
final attribute = lineStyle.getBlockExceptHeader();
if (attribute == null) {
return null;
}
var blockStyle = <String, dynamic>{attribute.key: attribute.value};
final blockStyle = <String, dynamic>{attribute.key: attribute.value};
Map<String, dynamic>? resetStyle;
@ -85,10 +85,10 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
resetStyle = Attribute.header.toJson();
}
List<String> lines = data.split('\n');
Delta delta = Delta()..retain(index);
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
final lines = data.split('\n');
final delta = Delta()..retain(index);
for (var i = 0; i < lines.length; i++) {
final line = lines[i];
if (line.isNotEmpty) {
delta.insert(line);
}
@ -100,8 +100,8 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
}
if (resetStyle != null) {
delta.retain(nextNewLine.item2!);
delta
..retain(nextNewLine.item2!)
..retain((nextNewLine.item1!.data as String).indexOf('\n'))
..retain(1, resetStyle);
}
@ -130,10 +130,9 @@ class AutoExitBlockRule extends InsertRule {
return null;
}
DeltaIterator itr = DeltaIterator(document);
Operation? prev = itr.skip(index), cur = itr.next();
Attribute? blockStyle =
Style.fromJson(cur.attributes).getBlockExceptHeader();
final itr = DeltaIterator(document);
final prev = itr.skip(index), cur = itr.next();
final blockStyle = Style.fromJson(cur.attributes).getBlockExceptHeader();
if (cur.isPlain || blockStyle == null) {
return null;
}
@ -145,7 +144,7 @@ class AutoExitBlockRule extends InsertRule {
return null;
}
Tuple2<Operation?, int?> nextNewLine = _getNextNewLine(itr);
final nextNewLine = _getNextNewLine(itr);
if (nextNewLine.item1 != null &&
nextNewLine.item1!.attributes != null &&
Style.fromJson(nextNewLine.item1!.attributes).getBlockExceptHeader() ==
@ -154,7 +153,7 @@ class AutoExitBlockRule extends InsertRule {
}
final attributes = cur.attributes ?? <String, dynamic>{};
String k = attributes.keys
final k = attributes.keys
.firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k));
attributes[k] = null;
// retain(1) should be '\n', set it with no attribute
@ -172,9 +171,8 @@ class ResetLineFormatOnNewLineRule extends InsertRule {
return null;
}
DeltaIterator itr = DeltaIterator(document);
itr.skip(index);
Operation cur = itr.next();
final itr = DeltaIterator(document)..skip(index);
final cur = itr.next();
if (cur.data is! String || !(cur.data as String).startsWith('\n')) {
return null;
}
@ -202,12 +200,12 @@ class InsertEmbedsRule extends InsertRule {
return null;
}
Delta delta = Delta()..retain(index);
DeltaIterator itr = DeltaIterator(document);
Operation? prev = itr.skip(index), cur = itr.next();
final delta = Delta()..retain(index);
final itr = DeltaIterator(document);
final prev = itr.skip(index), cur = itr.next();
String? textBefore = prev?.data is String ? prev!.data as String? : '';
String textAfter = cur.data is String ? (cur.data as String?)! : '';
final textBefore = prev?.data is String ? prev!.data as String? : '';
final textAfter = cur.data is String ? (cur.data as String?)! : '';
final isNewlineBefore = prev == null || textBefore!.endsWith('\n');
final isNewlineAfter = textAfter.startsWith('\n');
@ -221,7 +219,7 @@ class InsertEmbedsRule extends InsertRule {
lineStyle = cur.attributes;
} else {
while (itr.hasNext) {
Operation op = itr.next();
final op = itr.next();
if ((op.data is String ? op.data as String? : '')!.contains('\n')) {
lineStyle = op.attributes;
break;
@ -250,17 +248,17 @@ class ForceNewlineForInsertsAroundEmbedRule extends InsertRule {
return null;
}
String text = data;
DeltaIterator itr = DeltaIterator(document);
final text = data;
final itr = DeltaIterator(document);
final prev = itr.skip(index);
final cur = itr.next();
bool cursorBeforeEmbed = cur.data is! String;
bool cursorAfterEmbed = prev != null && prev.data is! String;
final cursorBeforeEmbed = cur.data is! String;
final cursorAfterEmbed = prev != null && prev.data is! String;
if (!cursorBeforeEmbed && !cursorAfterEmbed) {
return null;
}
Delta delta = Delta()..retain(index);
final delta = Delta()..retain(index);
if (cursorBeforeEmbed && !text.endsWith('\n')) {
return delta..insert(text)..insert('\n');
}
@ -281,19 +279,19 @@ class AutoFormatLinksRule extends InsertRule {
return null;
}
DeltaIterator itr = DeltaIterator(document);
Operation? prev = itr.skip(index);
final itr = DeltaIterator(document);
final prev = itr.skip(index);
if (prev == null || prev.data is! String) {
return null;
}
try {
String cand = (prev.data as String).split('\n').last.split(' ').last;
Uri link = Uri.parse(cand);
final cand = (prev.data as String).split('\n').last.split(' ').last;
final link = Uri.parse(cand);
if (!['https', 'http'].contains(link.scheme)) {
return null;
}
Map<String, dynamic> attributes = prev.attributes ?? <String, dynamic>{};
final attributes = prev.attributes ?? <String, dynamic>{};
if (attributes.containsKey(Attribute.link.key)) {
return null;
@ -320,16 +318,16 @@ class PreserveInlineStylesRule extends InsertRule {
return null;
}
DeltaIterator itr = DeltaIterator(document);
Operation? prev = itr.skip(index);
final itr = DeltaIterator(document);
final prev = itr.skip(index);
if (prev == null ||
prev.data is! String ||
(prev.data as String).contains('\n')) {
return null;
}
Map<String, dynamic>? attributes = prev.attributes;
String text = data;
final attributes = prev.attributes;
final text = data;
if (attributes == null || !attributes.containsKey(Attribute.link.key)) {
return Delta()
..retain(index)
@ -337,13 +335,12 @@ class PreserveInlineStylesRule extends InsertRule {
}
attributes.remove(Attribute.link.key);
Delta delta = Delta()
final delta = Delta()
..retain(index)
..insert(text, attributes.isEmpty ? null : attributes);
Operation next = itr.next();
final next = itr.next();
Map<String, dynamic> nextAttributes =
next.attributes ?? const <String, dynamic>{};
final nextAttributes = next.attributes ?? const <String, dynamic>{};
if (!nextAttributes.containsKey(Attribute.link.key)) {
return delta;
}
@ -370,9 +367,9 @@ class CatchAllInsertRule extends InsertRule {
Tuple2<Operation?, int?> _getNextNewLine(DeltaIterator iterator) {
Operation op;
for (int skipped = 0; iterator.hasNext; skipped += op.length!) {
for (var skipped = 0; iterator.hasNext; skipped += op.length!) {
op = iterator.next();
int lineBreak =
final lineBreak =
(op.data is String ? op.data as String? : '')!.indexOf('\n');
if (lineBreak >= 0) {
return Tuple2(op, skipped);

@ -1,7 +1,6 @@
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import '../documents/attribute.dart';
import '../documents/document.dart';
import '../quill_delta.dart';
import 'delete.dart';
import 'format.dart';
import 'insert.dart';
@ -27,6 +26,8 @@ abstract class Rule {
}
class Rules {
Rules(this._rules);
final List<Rule> _rules;
static final Rules _instance = Rules([
const FormatLinkAtCaretPositionRule(),
@ -46,14 +47,12 @@ class Rules {
const CatchAllDeleteRule(),
]);
Rules(this._rules);
static Rules getInstance() => _instance;
Delta apply(RuleType ruleType, Document document, int index,
{int? len, Object? data, Attribute? attribute}) {
final delta = document.toDelta();
for (var rule in _rules) {
for (final rule in _rules) {
if (rule.type != ruleType) {
continue;
}

@ -118,8 +118,8 @@ Color stringToColor(String? s) {
throw 'Color code not supported';
}
String hex = s.replaceFirst('#', '');
hex = hex.length == 6 ? 'ff' + hex : hex;
int val = int.parse(hex, radix: 16);
var hex = s.replaceFirst('#', '');
hex = hex.length == 6 ? 'ff$hex' : hex;
final val = int.parse(hex, radix: 16);
return Color(val);
}

@ -1,6 +1,6 @@
import 'dart:math' as math;
import 'package:flutter_quill/models/quill_delta.dart';
import '../models/quill_delta.dart';
const Set<int> WHITE_SPACE = {
0x9,
@ -33,6 +33,8 @@ const Set<int> WHITE_SPACE = {
// Diff between two texts - old text and new text
class Diff {
Diff(this.start, this.deleted, this.inserted);
// Start index in old text at which changes begin.
final int start;
@ -42,8 +44,6 @@ class Diff {
// The inserted text
final String inserted;
Diff(this.start, this.deleted, this.inserted);
@override
String toString() {
return 'Diff[$start, "$deleted", "$inserted"]';
@ -52,17 +52,17 @@ class Diff {
/* Get diff operation between old text and new text */
Diff getDiff(String oldText, String newText, int cursorPosition) {
int end = oldText.length;
int delta = newText.length - end;
for (int limit = math.max(0, cursorPosition - delta);
var end = oldText.length;
final delta = newText.length - end;
for (final limit = math.max(0, cursorPosition - delta);
end > limit && oldText[end - 1] == newText[end + delta - 1];
end--) {}
int start = 0;
for (int startLimit = cursorPosition - math.max(0, delta);
var start = 0;
for (final startLimit = cursorPosition - math.max(0, delta);
start < startLimit && oldText[start] == newText[start];
start++) {}
String deleted = (start >= end) ? '' : oldText.substring(start, end);
String inserted = newText.substring(start, end + delta);
final deleted = (start >= end) ? '' : oldText.substring(start, end);
final inserted = newText.substring(start, end + delta);
return Diff(start, deleted, inserted);
}
@ -71,19 +71,15 @@ int getPositionDelta(Delta user, Delta actual) {
return 0;
}
DeltaIterator userItr = DeltaIterator(user);
DeltaIterator actualItr = DeltaIterator(actual);
int diff = 0;
final userItr = DeltaIterator(user);
final actualItr = DeltaIterator(actual);
var diff = 0;
while (userItr.hasNext || actualItr.hasNext) {
final length = math.min(userItr.peekLength(), actualItr.peekLength());
Operation userOperation = userItr.next(length as int);
Operation actualOperation = actualItr.next(length);
final userOperation = userItr.next(length as int);
final actualOperation = actualItr.next(length);
if (userOperation.length != actualOperation.length) {
throw 'userOp ' +
userOperation.length.toString() +
' does not match ' +
' actualOp ' +
actualOperation.length.toString();
throw 'userOp ${userOperation.length} does not match actualOp ${actualOperation.length}';
}
if (userOperation.key == actualOperation.key) {
continue;

@ -1,5 +1,6 @@
import 'package:flutter/rendering.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart';
import '../models/documents/nodes/container.dart';
abstract class RenderContentProxyBox implements RenderBox {
double getPreferredLineHeight();

@ -1,19 +1,16 @@
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart';
import 'package:flutter_quill/models/documents/nodes/embed.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/models/quill_delta.dart';
import 'package:flutter_quill/utils/diff_delta.dart';
import 'package:tuple/tuple.dart';
class QuillController extends ChangeNotifier {
final Document document;
TextSelection selection;
Style toggledStyle = Style();
import '../models/documents/attribute.dart';
import '../models/documents/document.dart';
import '../models/documents/nodes/embed.dart';
import '../models/documents/style.dart';
import '../models/quill_delta.dart';
import '../utils/diff_delta.dart';
class QuillController extends ChangeNotifier {
QuillController({required this.document, required this.selection});
factory QuillController.basic() {
@ -23,6 +20,10 @@ class QuillController extends ChangeNotifier {
);
}
final Document document;
TextSelection selection;
Style toggledStyle = Style();
// item1: Document state before [change].
//
// item2: Change delta applied to the document.
@ -42,7 +43,7 @@ class QuillController extends ChangeNotifier {
}
void undo() {
Tuple2 tup = document.undo();
final tup = document.undo();
if (tup.item1) {
_handleHistoryChange(tup.item2);
}
@ -64,7 +65,7 @@ class QuillController extends ChangeNotifier {
}
void redo() {
Tuple2 tup = document.redo();
final tup = document.redo();
if (tup.item1) {
_handleHistoryChange(tup.item2);
}
@ -81,7 +82,7 @@ class QuillController extends ChangeNotifier {
Delta? delta;
if (len > 0 || data is! String || data.isNotEmpty) {
delta = document.replace(index, len, data);
bool shouldRetainDelta = toggledStyle.isNotEmpty &&
var shouldRetainDelta = toggledStyle.isNotEmpty &&
delta.isNotEmpty &&
delta.length <= 2 &&
delta.last.isInsert;
@ -97,7 +98,7 @@ class QuillController extends ChangeNotifier {
}
}
if (shouldRetainDelta) {
Delta retainDelta = Delta()
final retainDelta = Delta()
..retain(index)
..retain(data is String ? data.length : 1, toggledStyle.toJson());
document.compose(retainDelta, ChangeSource.LOCAL);
@ -109,11 +110,11 @@ class QuillController extends ChangeNotifier {
if (delta == null || delta.isEmpty) {
_updateSelection(textSelection, ChangeSource.LOCAL);
} else {
Delta user = Delta()
final user = Delta()
..retain(index)
..insert(data)
..delete(len);
int positionDelta = getPositionDelta(user, delta);
final positionDelta = getPositionDelta(user, delta);
_updateSelection(
textSelection.copyWith(
baseOffset: textSelection.baseOffset + positionDelta,
@ -133,8 +134,8 @@ class QuillController extends ChangeNotifier {
toggledStyle = toggledStyle.put(attribute);
}
Delta change = document.format(index, len, attribute);
TextSelection adjustedSelection = selection.copyWith(
final change = document.format(index, len, attribute);
final adjustedSelection = selection.copyWith(
baseOffset: change.transformPosition(selection.baseOffset),
extentOffset: change.transformPosition(selection.extentOffset));
if (selection != adjustedSelection) {
@ -176,7 +177,7 @@ class QuillController extends ChangeNotifier {
void _updateSelection(TextSelection textSelection, ChangeSource source) {
selection = textSelection;
int end = document.length - 1;
final end = document.length - 1;
selection = selection.copyWith(
baseOffset: math.min(selection.baseOffset, end),
extentOffset: math.min(selection.extentOffset, end));

@ -8,15 +8,6 @@ import 'box.dart';
const Duration _FADE_DURATION = Duration(milliseconds: 250);
class CursorStyle {
final Color color;
final Color backgroundColor;
final double width;
final double? height;
final Radius? radius;
final Offset? offset;
final bool opacityAnimates;
final bool paintAboveText;
const CursorStyle({
required this.color,
required this.backgroundColor,
@ -28,6 +19,15 @@ class CursorStyle {
this.paintAboveText = false,
});
final Color color;
final Color backgroundColor;
final double width;
final double? height;
final Radius? radius;
final Offset? offset;
final bool opacityAnimates;
final bool paintAboveText;
@override
bool operator ==(Object other) =>
identical(this, other) ||
@ -55,20 +55,11 @@ class CursorStyle {
}
class CursorCont extends ChangeNotifier {
final ValueNotifier<bool> show;
final ValueNotifier<bool> _blink;
final ValueNotifier<Color> color;
late AnimationController _blinkOpacityCont;
Timer? _cursorTimer;
bool _targetCursorVisibility = false;
CursorStyle _style;
CursorCont({
required ValueNotifier<bool> show,
required this.show,
required CursorStyle style,
required TickerProvider tickerProvider,
}) : show = show,
_style = style,
}) : _style = style,
_blink = ValueNotifier(false),
color = ValueNotifier(style.color) {
_blinkOpacityCont =
@ -76,6 +67,14 @@ class CursorCont extends ChangeNotifier {
_blinkOpacityCont.addListener(_onColorTick);
}
final ValueNotifier<bool> show;
final ValueNotifier<bool> _blink;
final ValueNotifier<Color> color;
late AnimationController _blinkOpacityCont;
Timer? _cursorTimer;
bool _targetCursorVisibility = false;
CursorStyle _style;
ValueNotifier<bool> get cursorBlink => _blink;
ValueNotifier<Color> get cursorColor => color;
@ -99,7 +98,7 @@ class CursorCont extends ChangeNotifier {
void _cursorTick(Timer timer) {
_targetCursorVisibility = !_targetCursorVisibility;
double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
final targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
if (style.opacityAnimates) {
_blinkOpacityCont.animateTo(targetOpacity, curve: Curves.easeOut);
} else {
@ -133,8 +132,9 @@ class CursorCont extends ChangeNotifier {
_blinkOpacityCont.value = 0.0;
if (style.opacityAnimates) {
_blinkOpacityCont.stop();
_blinkOpacityCont.value = 0.0;
_blinkOpacityCont
..stop()
..value = 0.0;
}
}
@ -156,30 +156,30 @@ class CursorCont extends ChangeNotifier {
}
class CursorPainter {
CursorPainter(this.editable, this.style, this.prototype, this.color,
this.devicePixelRatio);
final RenderContentProxyBox? editable;
final CursorStyle style;
final Rect? prototype;
final Color color;
final double devicePixelRatio;
CursorPainter(this.editable, this.style, this.prototype, this.color,
this.devicePixelRatio);
void paint(Canvas canvas, Offset offset, TextPosition position) {
assert(prototype != null);
Offset caretOffset =
final caretOffset =
editable!.getOffsetForCaret(position, prototype) + offset;
Rect caretRect = prototype!.shift(caretOffset);
var caretRect = prototype!.shift(caretOffset);
if (style.offset != null) {
caretRect = caretRect.shift(style.offset!);
}
if (caretRect.left < 0.0) {
caretRect = caretRect.shift(Offset(-caretRect.left, 0.0));
caretRect = caretRect.shift(Offset(-caretRect.left, 0));
}
double? caretHeight = editable!.getFullHeightForCaret(position);
final caretHeight = editable!.getFullHeightForCaret(position);
if (caretHeight != null) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
@ -207,8 +207,8 @@ class CursorPainter {
}
}
Offset caretPosition = editable!.localToGlobal(caretRect.topLeft);
double pixelMultiple = 1.0 / devicePixelRatio;
final caretPosition = editable!.localToGlobal(caretRect.topLeft);
final pixelMultiple = 1.0 / devicePixelRatio;
caretRect = caretRect.shift(Offset(
caretPosition.dx.isFinite
? (caretPosition.dx / pixelMultiple).round() * pixelMultiple -
@ -219,13 +219,13 @@ class CursorPainter {
caretPosition.dy
: caretPosition.dy));
Paint paint = Paint()..color = color;
final paint = Paint()..color = color;
if (style.radius == null) {
canvas.drawRect(caretRect, paint);
return;
}
RRect caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!);
final caretRRect = RRect.fromRectAndRadius(caretRect, style.radius!);
canvas.drawRRect(caretRRect, paint);
}
}

@ -3,21 +3,21 @@ import 'package:flutter/widgets.dart';
import 'package:tuple/tuple.dart';
class QuillStyles extends InheritedWidget {
final DefaultStyles data;
const QuillStyles({
required this.data,
required Widget child,
Key? key,
}) : super(key: key, child: child);
final DefaultStyles data;
@override
bool updateShouldNotify(QuillStyles oldWidget) {
return data != oldWidget.data;
}
static DefaultStyles? getStyles(BuildContext context, bool nullOk) {
var widget = context.dependOnInheritedWidgetOfExactType<QuillStyles>();
final widget = context.dependOnInheritedWidgetOfExactType<QuillStyles>();
if (widget == null && nullOk) {
return null;
}
@ -27,6 +27,13 @@ class QuillStyles extends InheritedWidget {
}
class DefaultTextBlockStyle {
DefaultTextBlockStyle(
this.style,
this.verticalSpacing,
this.lineSpacing,
this.decoration,
);
final TextStyle style;
final Tuple2<double, double> verticalSpacing;
@ -34,12 +41,32 @@ class DefaultTextBlockStyle {
final Tuple2<double, double> lineSpacing;
final BoxDecoration? decoration;
DefaultTextBlockStyle(
this.style, this.verticalSpacing, this.lineSpacing, this.decoration);
}
class DefaultStyles {
DefaultStyles({
this.h1,
this.h2,
this.h3,
this.paragraph,
this.bold,
this.italic,
this.underline,
this.strikeThrough,
this.link,
this.color,
this.placeHolder,
this.lists,
this.quote,
this.code,
this.indent,
this.align,
this.leading,
this.sizeSmall,
this.sizeLarge,
this.sizeHuge,
});
final DefaultTextBlockStyle? h1;
final DefaultTextBlockStyle? h2;
final DefaultTextBlockStyle? h3;
@ -61,36 +88,14 @@ class DefaultStyles {
final DefaultTextBlockStyle? align;
final DefaultTextBlockStyle? leading;
DefaultStyles(
{this.h1,
this.h2,
this.h3,
this.paragraph,
this.bold,
this.italic,
this.underline,
this.strikeThrough,
this.link,
this.color,
this.placeHolder,
this.lists,
this.quote,
this.code,
this.indent,
this.align,
this.leading,
this.sizeSmall,
this.sizeLarge,
this.sizeHuge});
static DefaultStyles getInstance(BuildContext context) {
ThemeData themeData = Theme.of(context);
DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
TextStyle baseStyle = defaultTextStyle.style.copyWith(
fontSize: 16.0,
final themeData = Theme.of(context);
final defaultTextStyle = DefaultTextStyle.of(context);
final baseStyle = defaultTextStyle.style.copyWith(
fontSize: 16,
height: 1.3,
);
Tuple2<double, double> baseSpacing = const Tuple2(6.0, 0);
const baseSpacing = Tuple2<double, double>(6, 0);
String fontFamily;
switch (themeData.platform) {
case TargetPlatform.iOS:
@ -110,36 +115,36 @@ class DefaultStyles {
return DefaultStyles(
h1: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith(
fontSize: 34.0,
fontSize: 34,
color: defaultTextStyle.style.color!.withOpacity(0.70),
height: 1.15,
fontWeight: FontWeight.w300,
),
const Tuple2(16.0, 0.0),
const Tuple2(0.0, 0.0),
const Tuple2(16, 0),
const Tuple2(0, 0),
null),
h2: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith(
fontSize: 24.0,
fontSize: 24,
color: defaultTextStyle.style.color!.withOpacity(0.70),
height: 1.15,
fontWeight: FontWeight.normal,
),
const Tuple2(8.0, 0.0),
const Tuple2(0.0, 0.0),
const Tuple2(8, 0),
const Tuple2(0, 0),
null),
h3: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith(
fontSize: 20.0,
fontSize: 20,
color: defaultTextStyle.style.color!.withOpacity(0.70),
height: 1.25,
fontWeight: FontWeight.w500,
),
const Tuple2(8.0, 0.0),
const Tuple2(0.0, 0.0),
const Tuple2(8, 0),
const Tuple2(0, 0),
null),
paragraph: DefaultTextBlockStyle(
baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null),
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
bold: const TextStyle(fontWeight: FontWeight.bold),
italic: const TextStyle(fontStyle: FontStyle.italic),
underline: const TextStyle(decoration: TextDecoration.underline),
@ -150,19 +155,19 @@ class DefaultStyles {
),
placeHolder: DefaultTextBlockStyle(
defaultTextStyle.style.copyWith(
fontSize: 20.0,
fontSize: 20,
height: 1.5,
color: Colors.grey.withOpacity(0.6),
),
const Tuple2(0.0, 0.0),
const Tuple2(0.0, 0.0),
const Tuple2(0, 0),
const Tuple2(0, 0),
null),
lists: DefaultTextBlockStyle(
baseStyle, baseSpacing, const Tuple2(0.0, 6.0), null),
baseStyle, baseSpacing, const Tuple2(0, 6), null),
quote: DefaultTextBlockStyle(
TextStyle(color: baseStyle.color!.withOpacity(0.6)),
baseSpacing,
const Tuple2(6.0, 2.0),
const Tuple2(6, 2),
BoxDecoration(
border: Border(
left: BorderSide(width: 4, color: Colors.grey.shade300),
@ -172,24 +177,24 @@ class DefaultStyles {
TextStyle(
color: Colors.blue.shade900.withOpacity(0.9),
fontFamily: fontFamily,
fontSize: 13.0,
fontSize: 13,
height: 1.15,
),
baseSpacing,
const Tuple2(0.0, 0.0),
const Tuple2(0, 0),
BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(2),
)),
indent: DefaultTextBlockStyle(
baseStyle, baseSpacing, const Tuple2(0.0, 6.0), null),
baseStyle, baseSpacing, const Tuple2(0, 6), null),
align: DefaultTextBlockStyle(
baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null),
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
leading: DefaultTextBlockStyle(
baseStyle, const Tuple2(0.0, 0.0), const Tuple2(0.0, 0.0), null),
sizeSmall: const TextStyle(fontSize: 10.0),
sizeLarge: const TextStyle(fontSize: 18.0),
sizeHuge: const TextStyle(fontSize: 22.0));
baseStyle, const Tuple2(0, 0), const Tuple2(0, 0), null),
sizeSmall: const TextStyle(fontSize: 10),
sizeLarge: const TextStyle(fontSize: 18),
sizeHuge: const TextStyle(fontSize: 22));
}
DefaultStyles merge(DefaultStyles other) {

@ -2,10 +2,10 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_quill/models/documents/nodes/leaf.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import '../models/documents/nodes/leaf.dart';
import 'editor.dart';
import 'text_selection.dart';
typedef EmbedBuilder = Widget Function(BuildContext context, Embed node);
@ -18,11 +18,11 @@ abstract class EditorTextSelectionGestureDetectorBuilderDelegate {
}
class EditorTextSelectionGestureDetectorBuilder {
EditorTextSelectionGestureDetectorBuilder(this.delegate);
final EditorTextSelectionGestureDetectorBuilderDelegate delegate;
bool shouldShowSelectionToolbar = true;
EditorTextSelectionGestureDetectorBuilder(this.delegate);
EditorState? getEditor() {
return delegate.getEditableTextKey().currentState;
}
@ -34,7 +34,7 @@ class EditorTextSelectionGestureDetectorBuilder {
void onTapDown(TapDownDetails details) {
getRenderEditor()!.handleTapDown(details);
PointerDeviceKind? kind = details.kind;
final kind = details.kind;
shouldShowSelectionToolbar = kind == null ||
kind == PointerDeviceKind.touch ||
kind == PointerDeviceKind.stylus;

@ -8,25 +8,23 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart'
as container_node;
import 'package:flutter_quill/models/documents/nodes/embed.dart';
import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf;
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/widgets/image.dart';
import 'package:flutter_quill/widgets/raw_editor.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import 'package:string_validator/string_validator.dart';
import 'package:url_launcher/url_launcher.dart';
import '../models/documents/attribute.dart';
import '../models/documents/document.dart';
import '../models/documents/nodes/container.dart' as container_node;
import '../models/documents/nodes/embed.dart';
import '../models/documents/nodes/leaf.dart' as leaf;
import '../models/documents/nodes/line.dart';
import 'box.dart';
import 'controller.dart';
import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart';
import 'image.dart';
import 'raw_editor.dart';
import 'text_selection.dart';
const linkPrefixes = [
'mailto:', // email
@ -101,7 +99,7 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) {
assert(!kIsWeb, 'Please provide EmbedBuilder for Web');
switch (node.value.type) {
case 'image':
String imageUrl = _standardizeImageUrl(node.value.data);
final imageUrl = _standardizeImageUrl(node.value.data);
return imageUrl.startsWith('http')
? Image.network(imageUrl)
: isBase64(imageUrl)
@ -116,6 +114,43 @@ Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) {
}
class QuillEditor extends StatefulWidget {
const QuillEditor({
required this.controller,
required this.focusNode,
required this.scrollController,
required this.scrollable,
required this.padding,
required this.autoFocus,
required this.readOnly,
required this.expands,
this.showCursor,
this.placeholder,
this.enableInteractiveSelection = true,
this.minHeight,
this.maxHeight,
this.customStyles,
this.textCapitalization = TextCapitalization.sentences,
this.keyboardAppearance = Brightness.light,
this.scrollPhysics,
this.onLaunchUrl,
this.embedBuilder = _defaultEmbedBuilder,
});
factory QuillEditor.basic({
required QuillController controller,
required bool readOnly,
}) {
return QuillEditor(
controller: controller,
scrollController: ScrollController(),
scrollable: true,
focusNode: FocusNode(),
autoFocus: true,
readOnly: readOnly,
expands: false,
padding: EdgeInsets.zero);
}
final QuillController controller;
final FocusNode focusNode;
final ScrollController scrollController;
@ -146,45 +181,6 @@ class QuillEditor extends StatefulWidget {
final bool Function(LongPressEndDetails details, TextPosition textPosition)? onSingleLongTapEnd;
final EmbedBuilder embedBuilder;
const QuillEditor(
{required this.controller,
required this.focusNode,
required this.scrollController,
required this.scrollable,
required this.padding,
required this.autoFocus,
required this.readOnly,
required this.expands,
this.showCursor,
this.placeholder,
this.enableInteractiveSelection = true,
this.minHeight,
this.maxHeight,
this.customStyles,
this.textCapitalization = TextCapitalization.sentences,
this.keyboardAppearance = Brightness.light,
this.scrollPhysics,
this.onLaunchUrl,
this.onTapDown,
this.onTapUp,
this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
this.embedBuilder = _defaultEmbedBuilder});
factory QuillEditor.basic(
{required QuillController controller, required bool readOnly}) {
return QuillEditor(
controller: controller,
scrollController: ScrollController(),
scrollable: true,
focusNode: FocusNode(),
autoFocus: true,
readOnly: readOnly,
expands: false,
padding: EdgeInsets.zero);
}
@override
_QuillEditorState createState() => _QuillEditorState();
}
@ -204,8 +200,8 @@ class _QuillEditorState extends State<QuillEditor>
@override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context);
final theme = Theme.of(context);
final selectionTheme = TextSelectionTheme.of(context);
TextSelectionControls textSelectionControls;
bool paintCursorAboveText;
@ -229,7 +225,7 @@ class _QuillEditorState extends State<QuillEditor>
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
final cupertinoTheme = CupertinoTheme.of(context);
textSelectionControls = cupertinoTextSelectionControls;
paintCursorAboveText = true;
cursorOpacityAnimates = true;
@ -237,7 +233,7 @@ class _QuillEditorState extends State<QuillEditor>
selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionTheme.selectionColor ??
cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
cursorRadius ??= const Radius.circular(2);
cursorOffset = Offset(
iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
break;
@ -269,7 +265,7 @@ class _QuillEditorState extends State<QuillEditor>
CursorStyle(
color: cursorColor,
backgroundColor: Colors.grey,
width: 2.0,
width: 2,
radius: cursorRadius,
offset: cursorOffset,
paintAboveText: paintCursorAboveText,
@ -286,6 +282,11 @@ class _QuillEditorState extends State<QuillEditor>
widget.keyboardAppearance,
widget.enableInteractiveSelection,
widget.scrollPhysics,
widget.onTapDown,
widget.onTapUp,
widget.onSingleLongTapStart,
widget.onSingleLongTapMoveUpdate,
widget.onSingleLongTapEnd,
widget.embedBuilder),
);
}
@ -312,10 +313,10 @@ class _QuillEditorState extends State<QuillEditor>
class _QuillEditorSelectionGestureDetectorBuilder
extends EditorTextSelectionGestureDetectorBuilder {
final _QuillEditorState _state;
_QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state);
final _QuillEditorState _state;
@override
void onForcePressStart(ForcePressDetails details) {
super.onForcePressStart(details);
@ -369,16 +370,14 @@ class _QuillEditorSelectionGestureDetectorBuilder
if (_state.widget.controller.document.isEmpty()) {
return false;
}
TextPosition pos =
getRenderEditor()!.getPositionForOffset(details.globalPosition);
container_node.ChildQuery result =
final pos = getRenderEditor()!.getPositionForOffset(details.globalPosition);
final result =
getEditor()!.widget.controller.document.queryChild(pos.offset);
if (result.node == null) {
return false;
}
Line line = result.node as Line;
container_node.ChildQuery segmentResult =
line.queryChild(result.offset, false);
final line = result.node as Line;
final segmentResult = line.queryChild(result.offset, false);
if (segmentResult.node == null) {
if (line.length == 1) {
// tapping when no text yet on this line
@ -389,7 +388,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
}
return false;
}
leaf.Leaf segment = segmentResult.node as leaf.Leaf;
final segment = segmentResult.node as leaf.Leaf;
if (segment.style.containsKey(Attribute.link.key)) {
var launchUrl = getEditor()!.widget.onLaunchUrl;
launchUrl ??= _launchUrl;
@ -405,9 +404,9 @@ class _QuillEditorSelectionGestureDetectorBuilder
return false;
}
if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) {
BlockEmbed blockEmbed = segment.value as BlockEmbed;
final blockEmbed = segment.value as BlockEmbed;
if (blockEmbed.type == 'image') {
final String imageUrl = _standardizeImageUrl(blockEmbed.data);
final imageUrl = _standardizeImageUrl(blockEmbed.data);
Navigator.push(
getEditor()!.context,
MaterialPageRoute(
@ -438,7 +437,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
return false;
}
// segmentResult.offset == 0 means tap at the beginning of the TextLine
String? listVal = line.style.attributes[Attribute.list.key]!.value;
final String? listVal = line.style.attributes[Attribute.list.key]!.value;
if (listVal == Attribute.unchecked.value) {
getEditor()!
.widget
@ -455,7 +454,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
return true;
}
void _launchUrl(String url) async {
Future<void> _launchUrl(String url) async {
await launch(url);
}
@ -485,7 +484,7 @@ class _QuillEditorSelectionGestureDetectorBuilder
getEditor()!.hideToolbar();
bool positionSelected = _onTapping(details);
final positionSelected = _onTapping(details);
if (delegate.getSelectionEnabled() && !positionSelected) {
switch (Theme.of(_state.context).platform) {
@ -567,6 +566,24 @@ typedef TextSelectionChangedHandler = void Function(
class RenderEditor extends RenderEditableContainerBox
implements RenderAbstractEditor {
RenderEditor(
List<RenderEditableBox>? children,
TextDirection textDirection,
EdgeInsetsGeometry padding,
this.document,
this.selection,
this._hasFocus,
this.onSelectionChanged,
this._startHandleLayerLink,
this._endHandleLayerLink,
EdgeInsets floatingCursorAddedMargin,
) : super(
children,
document.root,
textDirection,
padding,
);
Document document;
TextSelection selection;
bool _hasFocus = false;
@ -582,24 +599,6 @@ class RenderEditor extends RenderEditableContainerBox
ValueListenable<bool> get selectionEndInViewport => _selectionEndInViewport;
final ValueNotifier<bool> _selectionEndInViewport = ValueNotifier<bool>(true);
RenderEditor(
List<RenderEditableBox>? children,
TextDirection textDirection,
EdgeInsetsGeometry padding,
this.document,
this.selection,
this._hasFocus,
this.onSelectionChanged,
this._startHandleLayerLink,
this._endHandleLayerLink,
EdgeInsets floatingCursorAddedMargin)
: super(
children,
document.root,
textDirection,
padding,
);
void setDocument(Document doc) {
if (document == doc) {
return;
@ -644,22 +643,21 @@ class RenderEditor extends RenderEditableContainerBox
List<TextSelectionPoint> getEndpointsForSelection(
TextSelection textSelection) {
if (textSelection.isCollapsed) {
RenderEditableBox child = childAtPosition(textSelection.extent);
TextPosition localPosition = TextPosition(
offset:
textSelection.extentOffset - child.getContainer().getOffset());
Offset localOffset = child.getOffsetForCaret(localPosition);
BoxParentData parentData = child.parentData as BoxParentData;
final child = childAtPosition(textSelection.extent);
final localPosition = TextPosition(
offset: textSelection.extentOffset - child.getContainer().offset);
final localOffset = child.getOffsetForCaret(localPosition);
final parentData = child.parentData as BoxParentData;
return <TextSelectionPoint>[
TextSelectionPoint(
Offset(0.0, child.preferredLineHeight(localPosition)) +
Offset(0, child.preferredLineHeight(localPosition)) +
localOffset +
parentData.offset,
null)
];
}
Node? baseNode = _container.queryChild(textSelection.start, false).node;
final baseNode = _container.queryChild(textSelection.start, false).node;
var baseChild = firstChild;
while (baseChild != null) {
@ -670,15 +668,14 @@ class RenderEditor extends RenderEditableContainerBox
}
assert(baseChild != null);
BoxParentData baseParentData = baseChild!.parentData as BoxParentData;
TextSelection baseSelection =
final baseParentData = baseChild!.parentData as BoxParentData;
final baseSelection =
localSelection(baseChild.getContainer(), textSelection, true);
TextSelectionPoint basePoint =
baseChild.getBaseEndpointForSelection(baseSelection);
var basePoint = baseChild.getBaseEndpointForSelection(baseSelection);
basePoint = TextSelectionPoint(
basePoint.point + baseParentData.offset, basePoint.direction);
Node? extentNode = _container.queryChild(textSelection.end, false).node;
final extentNode = _container.queryChild(textSelection.end, false).node;
RenderEditableBox? extentChild = baseChild;
while (extentChild != null) {
if (extentChild.getContainer() == extentNode) {
@ -688,10 +685,10 @@ class RenderEditor extends RenderEditableContainerBox
}
assert(extentChild != null);
BoxParentData extentParentData = extentChild!.parentData as BoxParentData;
TextSelection extentSelection =
final extentParentData = extentChild!.parentData as BoxParentData;
final extentSelection =
localSelection(extentChild.getContainer(), textSelection, true);
TextSelectionPoint extentPoint =
var extentPoint =
extentChild.getExtentEndpointForSelection(extentSelection);
extentPoint = TextSelectionPoint(
extentPoint.point + extentParentData.offset, extentPoint.direction);
@ -712,9 +709,9 @@ class RenderEditor extends RenderEditableContainerBox
Offset? to,
SelectionChangedCause cause,
) {
TextPosition firstPosition = getPositionForOffset(from);
TextSelection firstWord = selectWordAtPosition(firstPosition);
TextSelection lastWord =
final firstPosition = getPositionForOffset(from);
final firstWord = selectWordAtPosition(firstPosition);
final lastWord =
to == null ? firstWord : selectWordAtPosition(getPositionForOffset(to));
_handleSelectionChange(
@ -731,7 +728,7 @@ class RenderEditor extends RenderEditableContainerBox
TextSelection nextSelection,
SelectionChangedCause cause,
) {
bool focusingEmpty = nextSelection.baseOffset == 0 &&
final focusingEmpty = nextSelection.baseOffset == 0 &&
nextSelection.extentOffset == 0 &&
!_hasFocus;
if (nextSelection == selection &&
@ -745,15 +742,15 @@ class RenderEditor extends RenderEditableContainerBox
@override
void selectWordEdge(SelectionChangedCause cause) {
assert(_lastTapDownPosition != null);
TextPosition position = getPositionForOffset(_lastTapDownPosition!);
RenderEditableBox child = childAtPosition(position);
int nodeOffset = child.getContainer().getOffset();
TextPosition localPosition = TextPosition(
final position = getPositionForOffset(_lastTapDownPosition!);
final child = childAtPosition(position);
final nodeOffset = child.getContainer().offset;
final localPosition = TextPosition(
offset: position.offset - nodeOffset,
affinity: position.affinity,
);
TextRange localWord = child.getWordBoundary(localPosition);
TextRange word = TextRange(
final localWord = child.getWordBoundary(localPosition);
final word = TextRange(
start: localWord.start + nodeOffset,
end: localWord.end + nodeOffset,
);
@ -777,17 +774,17 @@ class RenderEditor extends RenderEditableContainerBox
Offset? to,
SelectionChangedCause cause,
) {
TextPosition fromPosition = getPositionForOffset(from);
TextPosition? toPosition = to == null ? null : getPositionForOffset(to);
final fromPosition = getPositionForOffset(from);
final toPosition = to == null ? null : getPositionForOffset(to);
int baseOffset = fromPosition.offset;
int extentOffset = fromPosition.offset;
var baseOffset = fromPosition.offset;
var extentOffset = fromPosition.offset;
if (toPosition != null) {
baseOffset = math.min(fromPosition.offset, toPosition.offset);
extentOffset = math.max(fromPosition.offset, toPosition.offset);
}
TextSelection newSelection = TextSelection(
final newSelection = TextSelection(
baseOffset: baseOffset,
extentOffset: extentOffset,
affinity: fromPosition.affinity,
@ -807,12 +804,12 @@ class RenderEditor extends RenderEditableContainerBox
@override
TextSelection selectWordAtPosition(TextPosition position) {
RenderEditableBox child = childAtPosition(position);
int nodeOffset = child.getContainer().getOffset();
TextPosition localPosition = TextPosition(
final child = childAtPosition(position);
final nodeOffset = child.getContainer().offset;
final localPosition = TextPosition(
offset: position.offset - nodeOffset, affinity: position.affinity);
TextRange localWord = child.getWordBoundary(localPosition);
TextRange word = TextRange(
final localWord = child.getWordBoundary(localPosition);
final word = TextRange(
start: localWord.start + nodeOffset,
end: localWord.end + nodeOffset,
);
@ -824,12 +821,12 @@ class RenderEditor extends RenderEditableContainerBox
@override
TextSelection selectLineAtPosition(TextPosition position) {
RenderEditableBox child = childAtPosition(position);
int nodeOffset = child.getContainer().getOffset();
TextPosition localPosition = TextPosition(
final child = childAtPosition(position);
final nodeOffset = child.getContainer().offset;
final localPosition = TextPosition(
offset: position.offset - nodeOffset, affinity: position.affinity);
TextRange localLineRange = child.getLineBoundary(localPosition);
TextRange line = TextRange(
final localLineRange = child.getLineBoundary(localPosition);
final line = TextRange(
start: localLineRange.start + nodeOffset,
end: localLineRange.end + nodeOffset,
);
@ -879,21 +876,21 @@ class RenderEditor extends RenderEditableContainerBox
@override
double preferredLineHeight(TextPosition position) {
RenderEditableBox child = childAtPosition(position);
return child.preferredLineHeight(TextPosition(
offset: position.offset - child.getContainer().getOffset()));
final child = childAtPosition(position);
return child.preferredLineHeight(
TextPosition(offset: position.offset - child.getContainer().offset));
}
@override
TextPosition getPositionForOffset(Offset offset) {
Offset local = globalToLocal(offset);
RenderEditableBox child = childAtOffset(local)!;
final local = globalToLocal(offset);
final child = childAtOffset(local)!;
BoxParentData parentData = child.parentData as BoxParentData;
Offset localOffset = local - parentData.offset;
TextPosition localPosition = child.getPositionForOffset(localOffset);
final parentData = child.parentData as BoxParentData;
final localOffset = local - parentData.offset;
final localPosition = child.getPositionForOffset(localOffset);
return TextPosition(
offset: localPosition.offset + child.getContainer().getOffset(),
offset: localPosition.offset + child.getContainer().offset,
affinity: localPosition.affinity,
);
}
@ -905,15 +902,14 @@ class RenderEditor extends RenderEditableContainerBox
/// Returns null if [selection] is already visible.
double? getOffsetToRevealCursor(
double viewportHeight, double scrollOffset, double offsetInViewport) {
List<TextSelectionPoint> endpoints = getEndpointsForSelection(selection);
TextSelectionPoint endpoint = endpoints.first;
RenderEditableBox child = childAtPosition(selection.extent);
final endpoints = getEndpointsForSelection(selection);
final endpoint = endpoints.first;
final child = childAtPosition(selection.extent);
const kMargin = 8.0;
double caretTop = endpoint.point.dy -
final caretTop = endpoint.point.dy -
child.preferredLineHeight(TextPosition(
offset:
selection.extentOffset - child.getContainer().getOffset())) -
offset: selection.extentOffset - child.getContainer().offset)) -
kMargin +
offsetInViewport;
final caretBottom = endpoint.point.dy + kMargin + offsetInViewport;
@ -926,7 +922,7 @@ class RenderEditor extends RenderEditableContainerBox
if (dy == null) {
return null;
}
return math.max(dy, 0.0);
return math.max(dy, 0);
}
}
@ -939,17 +935,20 @@ class RenderEditableContainerBox extends RenderBox
EditableContainerParentData>,
RenderBoxContainerDefaultsMixin<RenderEditableBox,
EditableContainerParentData> {
RenderEditableContainerBox(
List<RenderEditableBox>? children,
this._container,
this.textDirection,
this._padding,
) : assert(_padding.isNonNegative) {
addAll(children);
}
container_node.Container _container;
TextDirection textDirection;
EdgeInsetsGeometry _padding;
EdgeInsets? _resolvedPadding;
RenderEditableContainerBox(List<RenderEditableBox>? children, this._container,
this.textDirection, this._padding)
: assert(_padding.isNonNegative) {
addAll(children);
}
container_node.Container getContainer() {
return _container;
}
@ -988,7 +987,7 @@ class RenderEditableContainerBox extends RenderBox
RenderEditableBox childAtPosition(TextPosition position) {
assert(firstChild != null);
Node? targetNode = _container.queryChild(position.offset, false).node;
final targetNode = _container.queryChild(position.offset, false).node;
var targetChild = firstChild;
while (targetChild != null) {
@ -1020,7 +1019,8 @@ class RenderEditableContainerBox extends RenderBox
}
var child = firstChild;
double dx = -offset.dx, dy = _resolvedPadding!.top;
final dx = -offset.dx;
var dy = _resolvedPadding!.top;
while (child != null) {
if (child.size.contains(offset.translate(dx, -dy))) {
return child;
@ -1047,16 +1047,15 @@ class RenderEditableContainerBox extends RenderBox
_resolvePadding();
assert(_resolvedPadding != null);
double mainAxisExtent = _resolvedPadding!.top;
var mainAxisExtent = _resolvedPadding!.top;
var child = firstChild;
BoxConstraints innerConstraints =
final innerConstraints =
BoxConstraints.tightFor(width: constraints.maxWidth)
.deflate(_resolvedPadding!);
while (child != null) {
child.layout(innerConstraints, parentUsesSize: true);
final EditableContainerParentData childParentData =
child.parentData as EditableContainerParentData;
childParentData.offset = Offset(_resolvedPadding!.left, mainAxisExtent);
final childParentData = (child.parentData as EditableContainerParentData)
..offset = Offset(_resolvedPadding!.left, mainAxisExtent);
mainAxisExtent += child.size.height;
assert(child.parentData == childParentData);
child = childParentData.nextSibling;
@ -1068,24 +1067,22 @@ class RenderEditableContainerBox extends RenderBox
}
double _getIntrinsicCrossAxis(double Function(RenderBox child) childSize) {
double extent = 0.0;
var extent = 0.0;
var child = firstChild;
while (child != null) {
extent = math.max(extent, childSize(child));
EditableContainerParentData childParentData =
child.parentData as EditableContainerParentData;
final childParentData = child.parentData as EditableContainerParentData;
child = childParentData.nextSibling;
}
return extent;
}
double _getIntrinsicMainAxis(double Function(RenderBox child) childSize) {
double extent = 0.0;
var extent = 0.0;
var child = firstChild;
while (child != null) {
extent += childSize(child);
EditableContainerParentData childParentData =
child.parentData as EditableContainerParentData;
final childParentData = child.parentData as EditableContainerParentData;
child = childParentData.nextSibling;
}
return extent;
@ -1094,9 +1091,9 @@ class RenderEditableContainerBox extends RenderBox
@override
double computeMinIntrinsicWidth(double height) {
_resolvePadding();
return _getIntrinsicCrossAxis((RenderBox child) {
double childHeight = math.max(
0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
return _getIntrinsicCrossAxis((child) {
final childHeight = math.max<double>(
0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
return child.getMinIntrinsicWidth(childHeight) +
_resolvedPadding!.left +
_resolvedPadding!.right;
@ -1106,9 +1103,9 @@ class RenderEditableContainerBox extends RenderBox
@override
double computeMaxIntrinsicWidth(double height) {
_resolvePadding();
return _getIntrinsicCrossAxis((RenderBox child) {
double childHeight = math.max(
0.0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
return _getIntrinsicCrossAxis((child) {
final childHeight = math.max<double>(
0, height - _resolvedPadding!.top + _resolvedPadding!.bottom);
return child.getMaxIntrinsicWidth(childHeight) +
_resolvedPadding!.left +
_resolvedPadding!.right;
@ -1118,9 +1115,9 @@ class RenderEditableContainerBox extends RenderBox
@override
double computeMinIntrinsicHeight(double width) {
_resolvePadding();
return _getIntrinsicMainAxis((RenderBox child) {
double childWidth = math.max(
0.0, width - _resolvedPadding!.left + _resolvedPadding!.right);
return _getIntrinsicMainAxis((child) {
final childWidth = math.max<double>(
0, width - _resolvedPadding!.left + _resolvedPadding!.right);
return child.getMinIntrinsicHeight(childWidth) +
_resolvedPadding!.top +
_resolvedPadding!.bottom;
@ -1130,9 +1127,9 @@ class RenderEditableContainerBox extends RenderBox
@override
double computeMaxIntrinsicHeight(double width) {
_resolvePadding();
return _getIntrinsicMainAxis((RenderBox child) {
final childWidth = math.max(
0.0, width - _resolvedPadding!.left + _resolvedPadding!.right);
return _getIntrinsicMainAxis((child) {
final childWidth = math.max<double>(
0, width - _resolvedPadding!.left + _resolvedPadding!.right);
return child.getMaxIntrinsicHeight(childWidth) +
_resolvedPadding!.top +
_resolvedPadding!.bottom;

@ -9,9 +9,12 @@ typedef InputShortcutCallback = void Function(InputShortcut? shortcut);
typedef OnDeleteCallback = void Function(bool forward);
class KeyboardListener {
KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete);
final CursorMoveCallback onCursorMove;
final InputShortcutCallback onShortcut;
final OnDeleteCallback onDelete;
static final Set<LogicalKeyboardKey> _moveKeys = <LogicalKeyboardKey>{
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowLeft,
@ -59,8 +62,6 @@ class KeyboardListener {
LogicalKeyboardKey.keyA: InputShortcut.SELECT_ALL,
};
KeyboardListener(this.onCursorMove, this.onShortcut, this.onDelete);
bool handleRawKeyEvent(RawKeyEvent event) {
if (kIsWeb) {
// On web platform, we should ignore the key because it's processed already.
@ -71,10 +72,10 @@ class KeyboardListener {
return false;
}
Set<LogicalKeyboardKey> keysPressed =
final keysPressed =
LogicalKeyboardKey.collapseSynonyms(RawKeyboard.instance.keysPressed);
LogicalKeyboardKey key = event.logicalKey;
bool isMacOS = event.data is RawKeyEventDataMacOs;
final key = event.logicalKey;
final isMacOS = event.data is RawKeyEventDataMacOs;
if (!_nonModifierKeys.contains(key) ||
keysPressed
.difference(isMacOS ? _macOsModifierKeys : _modifierKeys)

@ -4,12 +4,12 @@ import 'package:flutter/widgets.dart';
import 'box.dart';
class BaselineProxy extends SingleChildRenderObjectWidget {
final TextStyle? textStyle;
final EdgeInsets? padding;
const BaselineProxy({Key? key, Widget? child, this.textStyle, this.padding})
: super(key: key, child: child);
final TextStyle? textStyle;
final EdgeInsets? padding;
@override
RenderBaselineProxy createRenderObject(BuildContext context) {
return RenderBaselineProxy(
@ -87,14 +87,14 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox {
List<TextBox> getBoxesForSelection(TextSelection selection) {
if (!selection.isCollapsed) {
return <TextBox>[
TextBox.fromLTRBD(0.0, 0.0, size.width, size.height, TextDirection.ltr)
TextBox.fromLTRBD(0, 0, size.width, size.height, TextDirection.ltr)
];
}
double left = selection.extentOffset == 0 ? 0.0 : size.width;
double right = selection.extentOffset == 0 ? 0.0 : size.width;
final left = selection.extentOffset == 0 ? 0.0 : size.width;
final right = selection.extentOffset == 0 ? 0.0 : size.width;
return <TextBox>[
TextBox.fromLTRBD(left, 0.0, right, size.height, TextDirection.ltr)
TextBox.fromLTRBD(left, 0, right, size.height, TextDirection.ltr)
];
}
@ -104,7 +104,7 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox {
@override
Offset getOffsetForCaret(TextPosition position, Rect? caretPrototype) {
assert(position.offset <= 1 && position.offset >= 0);
return position.offset == 0 ? Offset.zero : Offset(size.width, 0.0);
return position.offset == 0 ? Offset.zero : Offset(size.width, 0);
}
@override
@ -122,6 +122,18 @@ class RenderEmbedProxy extends RenderProxyBox implements RenderContentProxyBox {
}
class RichTextProxy extends SingleChildRenderObjectWidget {
const RichTextProxy(
RichText child,
this.textStyle,
this.textAlign,
this.textDirection,
this.textScaleFactor,
this.locale,
this.strutStyle,
this.textWidthBasis,
this.textHeightBehavior,
) : super(child: child);
final TextStyle textStyle;
final TextAlign textAlign;
final TextDirection textDirection;
@ -145,29 +157,18 @@ class RichTextProxy extends SingleChildRenderObjectWidget {
textHeightBehavior);
}
const RichTextProxy(
RichText child,
this.textStyle,
this.textAlign,
this.textDirection,
this.textScaleFactor,
this.locale,
this.strutStyle,
this.textWidthBasis,
this.textHeightBehavior)
: super(child: child);
@override
void updateRenderObject(
BuildContext context, covariant RenderParagraphProxy renderObject) {
renderObject.textStyle = textStyle;
renderObject.textAlign = textAlign;
renderObject.textDirection = textDirection;
renderObject.textScaleFactor = textScaleFactor;
renderObject.locale = locale;
renderObject.strutStyle = strutStyle;
renderObject.textWidthBasis = textWidthBasis;
renderObject.textHeightBehavior = textHeightBehavior;
renderObject
..textStyle = textStyle
..textAlign = textAlign
..textDirection = textDirection
..textScaleFactor = textScaleFactor
..locale = locale
..strutStyle = strutStyle
..textWidthBasis = textWidthBasis
..textHeightBehavior = textHeightBehavior;
}
}

@ -9,27 +9,58 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart';
import 'package:flutter_quill/models/documents/nodes/block.dart';
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/utils/diff_delta.dart';
import 'package:flutter_quill/widgets/default_styles.dart';
import 'package:flutter_quill/widgets/proxy.dart';
import 'package:flutter_quill/widgets/text_block.dart';
import 'package:flutter_quill/widgets/text_line.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import 'package:tuple/tuple.dart';
import 'box.dart';
import '../models/documents/attribute.dart';
import '../models/documents/document.dart';
import '../models/documents/nodes/block.dart';
import '../models/documents/nodes/line.dart';
import '../utils/diff_delta.dart';
import 'controller.dart';
import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart';
import 'editor.dart';
import 'keyboard_listener.dart';
import 'proxy.dart';
import 'text_block.dart';
import 'text_line.dart';
import 'text_selection.dart';
class RawEditor extends StatefulWidget {
const RawEditor(
Key key,
this.controller,
this.focusNode,
this.scrollController,
this.scrollable,
this.padding,
this.readOnly,
this.placeholder,
this.onLaunchUrl,
this.toolbarOptions,
this.showSelectionHandles,
bool? showCursor,
this.cursorStyle,
this.textCapitalization,
this.maxHeight,
this.minHeight,
this.customStyles,
this.expands,
this.autoFocus,
this.selectionColor,
this.selectionCtrls,
this.keyboardAppearance,
this.enableInteractiveSelection,
this.scrollPhysics,
this.embedBuilder,
) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'),
assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'),
assert(maxHeight == null || minHeight == null || maxHeight >= minHeight,
'maxHeight cannot be null'),
showCursor = showCursor ?? true,
super(key: key);
final QuillController controller;
final FocusNode focusNode;
final ScrollController scrollController;
@ -55,39 +86,6 @@ class RawEditor extends StatefulWidget {
final ScrollPhysics? scrollPhysics;
final EmbedBuilder embedBuilder;
const RawEditor(
Key key,
this.controller,
this.focusNode,
this.scrollController,
this.scrollable,
this.padding,
this.readOnly,
this.placeholder,
this.onLaunchUrl,
this.toolbarOptions,
this.showSelectionHandles,
bool? showCursor,
this.cursorStyle,
this.textCapitalization,
this.maxHeight,
this.minHeight,
this.customStyles,
this.expands,
this.autoFocus,
this.selectionColor,
this.selectionCtrls,
this.keyboardAppearance,
this.enableInteractiveSelection,
this.scrollPhysics,
this.embedBuilder)
: assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'),
assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'),
assert(maxHeight == null || minHeight == null || maxHeight >= minHeight,
'maxHeight cannot be null'),
showCursor = showCursor ?? true,
super(key: key);
@override
State<StatefulWidget> createState() {
return RawEditorState();
@ -140,7 +138,7 @@ class RawEditorState extends EditorState
bool get _hasFocus => widget.focusNode.hasFocus;
TextDirection get _textDirection {
TextDirection result = Directionality.of(context);
final result = Directionality.of(context);
return result;
}
@ -153,13 +151,13 @@ class RawEditorState extends EditorState
if (wordModifier && lineModifier) {
return;
}
TextSelection selection = widget.controller.selection;
final selection = widget.controller.selection;
TextSelection newSelection = widget.controller.selection;
var newSelection = widget.controller.selection;
String plainText = textEditingValue.text;
final plainText = textEditingValue.text;
bool rightKey = key == LogicalKeyboardKey.arrowRight,
final rightKey = key == LogicalKeyboardKey.arrowRight,
leftKey = key == LogicalKeyboardKey.arrowLeft,
upKey = key == LogicalKeyboardKey.arrowUp,
downKey = key == LogicalKeyboardKey.arrowDown;
@ -184,7 +182,7 @@ class RawEditorState extends EditorState
TextSelection _placeCollapsedSelection(TextSelection selection,
TextSelection newSelection, bool leftKey, bool rightKey) {
int newOffset = newSelection.extentOffset;
var newOffset = newSelection.extentOffset;
if (!selection.isCollapsed) {
if (leftKey) {
newOffset = newSelection.baseOffset < newSelection.extentOffset
@ -206,41 +204,38 @@ class RawEditorState extends EditorState
TextSelection selection,
TextSelection newSelection,
String plainText) {
TextPosition originPosition = TextPosition(
final originPosition = TextPosition(
offset: upKey ? selection.baseOffset : selection.extentOffset);
RenderEditableBox child =
getRenderEditor()!.childAtPosition(originPosition);
TextPosition localPosition = TextPosition(
offset:
originPosition.offset - child.getContainer().getDocumentOffset());
final child = getRenderEditor()!.childAtPosition(originPosition);
final localPosition = TextPosition(
offset: originPosition.offset - child.getContainer().documentOffset);
TextPosition? position = upKey
var position = upKey
? child.getPositionAbove(localPosition)
: child.getPositionBelow(localPosition);
if (position == null) {
var sibling = upKey
final sibling = upKey
? getRenderEditor()!.childBefore(child)
: getRenderEditor()!.childAfter(child);
if (sibling == null) {
position = TextPosition(offset: upKey ? 0 : plainText.length - 1);
} else {
Offset finalOffset = Offset(
final finalOffset = Offset(
child.getOffsetForCaret(localPosition).dx,
sibling
.getOffsetForCaret(TextPosition(
offset: upKey ? sibling.getContainer().length - 1 : 0))
.dy);
TextPosition siblingPosition =
sibling.getPositionForOffset(finalOffset);
final siblingPosition = sibling.getPositionForOffset(finalOffset);
position = TextPosition(
offset: sibling.getContainer().getDocumentOffset() +
siblingPosition.offset);
offset:
sibling.getContainer().documentOffset + siblingPosition.offset);
}
} else {
position = TextPosition(
offset: child.getContainer().getDocumentOffset() + position.offset);
offset: child.getContainer().documentOffset + position.offset);
}
if (position.offset == newSelection.extentOffset) {
@ -273,28 +268,28 @@ class RawEditorState extends EditorState
bool shift) {
if (wordModifier) {
if (leftKey) {
TextSelection textSelection = getRenderEditor()!.selectWordAtPosition(
final textSelection = getRenderEditor()!.selectWordAtPosition(
TextPosition(
offset: _previousCharacter(
newSelection.extentOffset, plainText, false)));
return newSelection.copyWith(extentOffset: textSelection.baseOffset);
}
TextSelection textSelection = getRenderEditor()!.selectWordAtPosition(
final textSelection = getRenderEditor()!.selectWordAtPosition(
TextPosition(
offset:
_nextCharacter(newSelection.extentOffset, plainText, false)));
return newSelection.copyWith(extentOffset: textSelection.extentOffset);
} else if (lineModifier) {
if (leftKey) {
TextSelection textSelection = getRenderEditor()!.selectLineAtPosition(
final textSelection = getRenderEditor()!.selectLineAtPosition(
TextPosition(
offset: _previousCharacter(
newSelection.extentOffset, plainText, false)));
return newSelection.copyWith(extentOffset: textSelection.baseOffset);
}
int startPoint = newSelection.extentOffset;
final startPoint = newSelection.extentOffset;
if (startPoint < plainText.length) {
TextSelection textSelection = getRenderEditor()!
final textSelection = getRenderEditor()!
.selectLineAtPosition(TextPosition(offset: startPoint));
return newSelection.copyWith(extentOffset: textSelection.extentOffset);
}
@ -302,9 +297,9 @@ class RawEditorState extends EditorState
}
if (rightKey && newSelection.extentOffset < plainText.length) {
int nextExtent =
final nextExtent =
_nextCharacter(newSelection.extentOffset, plainText, true);
int distance = nextExtent - newSelection.extentOffset;
final distance = nextExtent - newSelection.extentOffset;
newSelection = newSelection.copyWith(extentOffset: nextExtent);
if (shift) {
_cursorResetLocation += distance;
@ -313,9 +308,9 @@ class RawEditorState extends EditorState
}
if (leftKey && newSelection.extentOffset > 0) {
int previousExtent =
final previousExtent =
_previousCharacter(newSelection.extentOffset, plainText, true);
int distance = newSelection.extentOffset - previousExtent;
final distance = newSelection.extentOffset - previousExtent;
newSelection = newSelection.copyWith(extentOffset: previousExtent);
if (shift) {
_cursorResetLocation -= distance;
@ -331,8 +326,8 @@ class RawEditorState extends EditorState
return string.length;
}
int count = 0;
Characters remain = string.characters.skipWhile((String currentString) {
var count = 0;
final remain = string.characters.skipWhile((currentString) {
if (count <= index) {
count += currentString.length;
return true;
@ -351,9 +346,9 @@ class RawEditorState extends EditorState
return 0;
}
int count = 0;
var count = 0;
int? lastNonWhitespace;
for (String currentString in string.characters) {
for (final currentString in string.characters) {
if (!includeWhitespace &&
!WHITE_SPACE.contains(
currentString.characters.first.toString().codeUnitAt(0))) {
@ -411,7 +406,7 @@ class RawEditorState extends EditorState
return;
}
TextEditingValue actualValue = textEditingValue.copyWith(
final actualValue = textEditingValue.copyWith(
composing: _lastKnownRemoteTextEditingValue!.composing,
);
@ -419,7 +414,7 @@ class RawEditorState extends EditorState
return;
}
bool shouldRemember =
final shouldRemember =
textEditingValue.text != _lastKnownRemoteTextEditingValue!.text;
_lastKnownRemoteTextEditingValue = actualValue;
_textInputConnection!.setEditingState(actualValue);
@ -456,13 +451,12 @@ class RawEditorState extends EditorState
return;
}
TextEditingValue effectiveLastKnownValue =
_lastKnownRemoteTextEditingValue!;
final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue!;
_lastKnownRemoteTextEditingValue = value;
String oldText = effectiveLastKnownValue.text;
String text = value.text;
int cursorPosition = value.selection.extentOffset;
Diff diff = getDiff(oldText, text, cursorPosition);
final oldText = effectiveLastKnownValue.text;
final text = value.text;
final cursorPosition = value.selection.extentOffset;
final diff = getDiff(oldText, text, cursorPosition);
widget.controller.replaceText(
diff.start, diff.deleted.length, diff.inserted, value.selection);
}
@ -513,7 +507,7 @@ class RawEditorState extends EditorState
_focusAttachment!.reparent();
super.build(context);
Document _doc = widget.controller.document;
var _doc = widget.controller.document;
if (_doc.isEmpty() &&
!widget.focusNode.hasFocus &&
widget.placeholder != null) {
@ -540,7 +534,7 @@ class RawEditorState extends EditorState
);
if (widget.scrollable) {
EdgeInsets baselinePadding =
final baselinePadding =
EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1);
child = BaselineProxy(
textStyle: _styles!.paragraph!.style,
@ -553,7 +547,7 @@ class RawEditorState extends EditorState
);
}
BoxConstraints constraints = widget.expands
final constraints = widget.expands
? const BoxConstraints.expand()
: BoxConstraints(
minHeight: widget.minHeight ?? 0.0,
@ -584,15 +578,14 @@ class RawEditorState extends EditorState
List<Widget> _buildChildren(Document doc, BuildContext context) {
final result = <Widget>[];
Map<int, int> indentLevelCounts = {};
for (Node node in doc.root.children) {
final indentLevelCounts = <int, int>{};
for (final node in doc.root.children) {
if (node is Line) {
EditableTextLine editableTextLine =
_getEditableTextLineFromNode(node, context);
final editableTextLine = _getEditableTextLineFromNode(node, context);
result.add(editableTextLine);
} else if (node is Block) {
Map<String, Attribute> attrs = node.style.attributes;
EditableTextBlock editableTextBlock = EditableTextBlock(
final attrs = node.style.attributes;
final editableTextBlock = EditableTextBlock(
node,
_textDirection,
_getVerticalSpacingForBlock(node, _styles),
@ -602,7 +595,7 @@ class RawEditorState extends EditorState
widget.enableInteractiveSelection,
_hasFocus,
attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16.0)
? const EdgeInsets.all(16)
: null,
widget.embedBuilder,
_cursorCont,
@ -617,13 +610,13 @@ class RawEditorState extends EditorState
EditableTextLine _getEditableTextLineFromNode(
Line node, BuildContext context) {
TextLine textLine = TextLine(
final textLine = TextLine(
line: node,
textDirection: _textDirection,
embedBuilder: widget.embedBuilder,
styles: _styles!,
);
EditableTextLine editableTextLine = EditableTextLine(
final editableTextLine = EditableTextLine(
node,
null,
textLine,
@ -641,9 +634,9 @@ class RawEditorState extends EditorState
Tuple2<double, double> _getVerticalSpacingForLine(
Line line, DefaultStyles? defaultStyles) {
Map<String, Attribute> attrs = line.style.attributes;
final attrs = line.style.attributes;
if (attrs.containsKey(Attribute.header.key)) {
int? level = attrs[Attribute.header.key]!.value;
final int? level = attrs[Attribute.header.key]!.value;
switch (level) {
case 1:
return defaultStyles!.h1!.verticalSpacing;
@ -661,7 +654,7 @@ class RawEditorState extends EditorState
Tuple2<double, double> _getVerticalSpacingForBlock(
Block node, DefaultStyles? defaultStyles) {
Map<String, Attribute> attrs = node.style.attributes;
final attrs = node.style.attributes;
if (attrs.containsKey(Attribute.blockQuote.key)) {
return defaultStyles!.quote!.verticalSpacing;
} else if (attrs.containsKey(Attribute.codeBlock.key)) {
@ -704,7 +697,7 @@ class RawEditorState extends EditorState
_keyboardVisibilityController = KeyboardVisibilityController();
_keyboardVisible = _keyboardVisibilityController!.isVisible;
_keyboardVisibilitySubscription =
_keyboardVisibilityController?.onChange.listen((bool visible) {
_keyboardVisibilityController?.onChange.listen((visible) {
_keyboardVisible = visible;
if (visible) {
_onChangeTextEditingValue();
@ -720,8 +713,8 @@ class RawEditorState extends EditorState
@override
void didChangeDependencies() {
super.didChangeDependencies();
DefaultStyles? parentStyles = QuillStyles.getStyles(context, true);
DefaultStyles defaultStyles = DefaultStyles.getInstance(context);
final parentStyles = QuillStyles.getStyles(context, true);
final defaultStyles = DefaultStyles.getInstance(context);
_styles = (parentStyles != null)
? defaultStyles.merge(parentStyles)
: defaultStyles;
@ -784,27 +777,26 @@ class RawEditorState extends EditorState
}
void handleDelete(bool forward) {
TextSelection selection = widget.controller.selection;
String plainText = textEditingValue.text;
int cursorPosition = selection.start;
String textBefore = selection.textBefore(plainText);
String textAfter = selection.textAfter(plainText);
final selection = widget.controller.selection;
final plainText = textEditingValue.text;
var cursorPosition = selection.start;
var textBefore = selection.textBefore(plainText);
var textAfter = selection.textAfter(plainText);
if (selection.isCollapsed) {
if (!forward && textBefore.isNotEmpty) {
final int characterBoundary =
final characterBoundary =
_previousCharacter(textBefore.length, textBefore, true);
textBefore = textBefore.substring(0, characterBoundary);
cursorPosition = characterBoundary;
}
if (forward && textAfter.isNotEmpty && textAfter != '\n') {
final int deleteCount = _nextCharacter(0, textAfter, true);
final deleteCount = _nextCharacter(0, textAfter, true);
textAfter = textAfter.substring(deleteCount);
}
}
TextSelection newSelection =
TextSelection.collapsed(offset: cursorPosition);
String newText = textBefore + textAfter;
int size = plainText.length - newText.length;
final newSelection = TextSelection.collapsed(offset: cursorPosition);
final newText = textBefore + textAfter;
final size = plainText.length - newText.length;
widget.controller.replaceText(
cursorPosition,
size,
@ -813,9 +805,9 @@ class RawEditorState extends EditorState
);
}
void handleShortcut(InputShortcut? shortcut) async {
TextSelection selection = widget.controller.selection;
String plainText = textEditingValue.text;
Future<void> handleShortcut(InputShortcut? shortcut) async {
final selection = widget.controller.selection;
final plainText = textEditingValue.text;
if (shortcut == InputShortcut.COPY) {
if (!selection.isCollapsed) {
await Clipboard.setData(
@ -844,7 +836,7 @@ class RawEditorState extends EditorState
return;
}
if (shortcut == InputShortcut.PASTE && !widget.readOnly) {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) {
widget.controller.replaceText(
selection.start,
@ -913,12 +905,13 @@ class RawEditorState extends EditorState
_cursorCont.startOrStopCursorTimerIfNeeded(
_hasFocus, widget.controller.selection);
if (hasConnection) {
_cursorCont.stopCursorTimer(resetCharTicks: false);
_cursorCont.startCursorTimer();
_cursorCont
..stopCursorTimer(resetCharTicks: false)
..startCursorTimer();
}
SchedulerBinding.instance!.addPostFrameCallback(
(Duration _) => _updateOrDisposeSelectionOverlayIfNeeded());
(_) => _updateOrDisposeSelectionOverlayIfNeeded());
if (mounted) {
setState(() {
// Use widget.controller.value in build()
@ -989,12 +982,12 @@ class RawEditorState extends EditorState
}
_showCaretOnScreenScheduled = true;
SchedulerBinding.instance!.addPostFrameCallback((Duration _) {
SchedulerBinding.instance!.addPostFrameCallback((_) {
_showCaretOnScreenScheduled = false;
final viewport = RenderAbstractViewport.of(getRenderEditor())!;
final editorOffset = getRenderEditor()!
.localToGlobal(const Offset(0.0, 0.0), ancestor: viewport);
.localToGlobal(const Offset(0, 0), ancestor: viewport);
final offsetInViewport = _scrollController!.offset + editorOffset.dy;
final offset = getRenderEditor()!.getOffsetToRevealCursor(
@ -1065,7 +1058,7 @@ class RawEditorState extends EditorState
}
}
void __setEditingValue(TextEditingValue value) async {
Future<void> __setEditingValue(TextEditingValue value) async {
if (await __isItCut(value)) {
widget.controller.replaceText(
textEditingValue.selection.start,
@ -1074,8 +1067,8 @@ class RawEditorState extends EditorState
value.selection,
);
} else {
final TextEditingValue value = textEditingValue;
final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
final value = textEditingValue;
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null) {
final length =
textEditingValue.selection.end - textEditingValue.selection.start;
@ -1095,7 +1088,7 @@ class RawEditorState extends EditorState
}
Future<bool> __isItCut(TextEditingValue value) async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
final data = await Clipboard.getData(Clipboard.kTextPlain);
if (data == null) {
return false;
}
@ -1175,14 +1168,15 @@ class _Editor extends MultiChildRenderObjectWidget {
@override
void updateRenderObject(
BuildContext context, covariant RenderEditor renderObject) {
renderObject.document = document;
renderObject.setContainer(document.root);
renderObject.textDirection = textDirection;
renderObject.setHasFocus(hasFocus);
renderObject.setSelection(selection);
renderObject.setStartHandleLayerLink(startHandleLayerLink);
renderObject.setEndHandleLayerLink(endHandleLayerLink);
renderObject.onSelectionChanged = onSelectionChanged;
renderObject.setPadding(padding);
renderObject
..document = document
..setContainer(document.root)
..textDirection = textDirection
..setHasFocus(hasFocus)
..setSelection(selection)
..setStartHandleLayerLink(startHandleLayerLink)
..setEndHandleLayerLink(endHandleLayerLink)
..onSelectionChanged = onSelectionChanged
..setPadding(padding);
}
}

@ -1,10 +1,6 @@
import 'package:flutter/material.dart';
class ResponsiveWidget extends StatelessWidget {
final Widget largeScreen;
final Widget? mediumScreen;
final Widget? smallScreen;
const ResponsiveWidget({
required this.largeScreen,
this.mediumScreen,
@ -12,6 +8,10 @@ class ResponsiveWidget extends StatelessWidget {
Key? key,
}) : super(key: key);
final Widget largeScreen;
final Widget? mediumScreen;
final Widget? smallScreen;
static bool isSmallScreen(BuildContext context) {
return MediaQuery.of(context).size.width < 800;
}

@ -1,19 +1,18 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/nodes/block.dart';
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/widgets/cursor.dart';
import 'package:flutter_quill/widgets/default_styles.dart';
import 'package:flutter_quill/widgets/text_line.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart';
import '../models/documents/nodes/block.dart';
import '../models/documents/nodes/line.dart';
import 'box.dart';
import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart';
import 'editor.dart';
import 'text_line.dart';
import 'text_selection.dart';
const List<int> arabianRomanNumbers = [
1000,
@ -48,6 +47,21 @@ const List<String> romanNumbers = [
];
class EditableTextBlock extends StatelessWidget {
const EditableTextBlock(
this.block,
this.textDirection,
this.verticalSpacing,
this.textSelection,
this.color,
this.styles,
this.enableInteractiveSelection,
this.hasFocus,
this.contentPadding,
this.embedBuilder,
this.cursorCont,
this.indentLevelCounts,
);
final Block block;
final TextDirection textDirection;
final Tuple2 verticalSpacing;
@ -61,25 +75,11 @@ class EditableTextBlock extends StatelessWidget {
final CursorCont cursorCont;
final Map<int, int> indentLevelCounts;
const EditableTextBlock(
this.block,
this.textDirection,
this.verticalSpacing,
this.textSelection,
this.color,
this.styles,
this.enableInteractiveSelection,
this.hasFocus,
this.contentPadding,
this.embedBuilder,
this.cursorCont,
this.indentLevelCounts);
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false);
final defaultStyles = QuillStyles.getStyles(context, false);
return _EditableBlock(
block,
textDirection,
@ -91,7 +91,7 @@ class EditableTextBlock extends StatelessWidget {
BoxDecoration? _getDecorationForBlock(
Block node, DefaultStyles? defaultStyles) {
Map<String, Attribute> attrs = block.style.attributes;
final attrs = block.style.attributes;
if (attrs.containsKey(Attribute.blockQuote.key)) {
return defaultStyles!.quote!.decoration;
}
@ -103,13 +103,13 @@ class EditableTextBlock extends StatelessWidget {
List<Widget> _buildChildren(
BuildContext context, Map<int, int> indentLevelCounts) {
DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false);
int count = block.children.length;
var children = <Widget>[];
int index = 0;
for (Line line in Iterable.castFrom<dynamic, Line>(block.children)) {
final defaultStyles = QuillStyles.getStyles(context, false);
final count = block.children.length;
final children = <Widget>[];
var index = 0;
for (final line in Iterable.castFrom<dynamic, Line>(block.children)) {
index++;
EditableTextLine editableTextLine = EditableTextLine(
final editableTextLine = EditableTextLine(
line,
_buildLeading(context, line, index, indentLevelCounts, count),
TextLine(
@ -134,8 +134,8 @@ class EditableTextBlock extends StatelessWidget {
Widget? _buildLeading(BuildContext context, Line line, int index,
Map<int, int> indentLevelCounts, int count) {
DefaultStyles? defaultStyles = QuillStyles.getStyles(context, false);
Map<String, Attribute> attrs = line.style.attributes;
final defaultStyles = QuillStyles.getStyles(context, false);
final attrs = line.style.attributes;
if (attrs[Attribute.list.key] == Attribute.ol) {
return _NumberPoint(
index: index,
@ -143,8 +143,8 @@ class EditableTextBlock extends StatelessWidget {
count: count,
style: defaultStyles!.leading!.style,
attrs: attrs,
width: 32.0,
padding: 8.0,
width: 32,
padding: 8,
);
}
@ -173,9 +173,9 @@ class EditableTextBlock extends StatelessWidget {
count: count,
style: defaultStyles!.code!.style
.copyWith(color: defaultStyles.code!.style.color!.withOpacity(0.4)),
width: 32.0,
width: 32,
attrs: attrs,
padding: 16.0,
padding: 16,
withDot: false,
);
}
@ -183,10 +183,10 @@ class EditableTextBlock extends StatelessWidget {
}
double _getIndentWidth() {
Map<String, Attribute> attrs = block.style.attributes;
final attrs = block.style.attributes;
Attribute? indent = attrs[Attribute.indent.key];
double extraIndent = 0.0;
final indent = attrs[Attribute.indent.key];
var extraIndent = 0.0;
if (indent != null && indent.value != null) {
extraIndent = 16.0 * indent.value;
}
@ -200,11 +200,11 @@ class EditableTextBlock extends StatelessWidget {
Tuple2 _getSpacingForLine(
Line node, int index, int count, DefaultStyles? defaultStyles) {
double top = 0.0, bottom = 0.0;
var top = 0.0, bottom = 0.0;
Map<String, Attribute> attrs = block.style.attributes;
final attrs = block.style.attributes;
if (attrs.containsKey(Attribute.header.key)) {
int? level = attrs[Attribute.header.key]!.value;
final level = attrs[Attribute.header.key]!.value;
switch (level) {
case 1:
top = defaultStyles!.h1!.verticalSpacing.item1;
@ -310,22 +310,22 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
@override
TextRange getLineBoundary(TextPosition position) {
RenderEditableBox child = childAtPosition(position);
TextRange rangeInChild = child.getLineBoundary(TextPosition(
offset: position.offset - child.getContainer().getOffset(),
final child = childAtPosition(position);
final rangeInChild = child.getLineBoundary(TextPosition(
offset: position.offset - child.getContainer().offset,
affinity: position.affinity,
));
return TextRange(
start: rangeInChild.start + child.getContainer().getOffset(),
end: rangeInChild.end + child.getContainer().getOffset(),
start: rangeInChild.start + child.getContainer().offset,
end: rangeInChild.end + child.getContainer().offset,
);
}
@override
Offset getOffsetForCaret(TextPosition position) {
RenderEditableBox child = childAtPosition(position);
final child = childAtPosition(position);
return child.getOffsetForCaret(TextPosition(
offset: position.offset - child.getContainer().getOffset(),
offset: position.offset - child.getContainer().offset,
affinity: position.affinity,
)) +
(child.parentData as BoxParentData).offset;
@ -333,21 +333,21 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
@override
TextPosition getPositionForOffset(Offset offset) {
RenderEditableBox child = childAtOffset(offset)!;
BoxParentData parentData = child.parentData as BoxParentData;
TextPosition localPosition =
final child = childAtOffset(offset)!;
final parentData = child.parentData as BoxParentData;
final localPosition =
child.getPositionForOffset(offset - parentData.offset);
return TextPosition(
offset: localPosition.offset + child.getContainer().getOffset(),
offset: localPosition.offset + child.getContainer().offset,
affinity: localPosition.affinity,
);
}
@override
TextRange getWordBoundary(TextPosition position) {
RenderEditableBox child = childAtPosition(position);
int nodeOffset = child.getContainer().getOffset();
TextRange childWord = child
final child = childAtPosition(position);
final nodeOffset = child.getContainer().offset;
final childWord = child
.getWordBoundary(TextPosition(offset: position.offset - nodeOffset));
return TextRange(
start: childWord.start + nodeOffset,
@ -359,27 +359,26 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
TextPosition? getPositionAbove(TextPosition position) {
assert(position.offset < getContainer().length);
RenderEditableBox child = childAtPosition(position);
TextPosition childLocalPosition = TextPosition(
offset: position.offset - child.getContainer().getOffset());
TextPosition? result = child.getPositionAbove(childLocalPosition);
final child = childAtPosition(position);
final childLocalPosition =
TextPosition(offset: position.offset - child.getContainer().offset);
final result = child.getPositionAbove(childLocalPosition);
if (result != null) {
return TextPosition(
offset: result.offset + child.getContainer().getOffset());
return TextPosition(offset: result.offset + child.getContainer().offset);
}
RenderEditableBox? sibling = childBefore(child);
final sibling = childBefore(child);
if (sibling == null) {
return null;
}
Offset caretOffset = child.getOffsetForCaret(childLocalPosition);
TextPosition testPosition =
final caretOffset = child.getOffsetForCaret(childLocalPosition);
final testPosition =
TextPosition(offset: sibling.getContainer().length - 1);
Offset testOffset = sibling.getOffsetForCaret(testPosition);
Offset finalOffset = Offset(caretOffset.dx, testOffset.dy);
final testOffset = sibling.getOffsetForCaret(testPosition);
final finalOffset = Offset(caretOffset.dx, testOffset.dy);
return TextPosition(
offset: sibling.getContainer().getOffset() +
offset: sibling.getContainer().offset +
sibling.getPositionForOffset(finalOffset).offset);
}
@ -387,46 +386,44 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
TextPosition? getPositionBelow(TextPosition position) {
assert(position.offset < getContainer().length);
RenderEditableBox child = childAtPosition(position);
TextPosition childLocalPosition = TextPosition(
offset: position.offset - child.getContainer().getOffset());
TextPosition? result = child.getPositionBelow(childLocalPosition);
final child = childAtPosition(position);
final childLocalPosition =
TextPosition(offset: position.offset - child.getContainer().offset);
final result = child.getPositionBelow(childLocalPosition);
if (result != null) {
return TextPosition(
offset: result.offset + child.getContainer().getOffset());
return TextPosition(offset: result.offset + child.getContainer().offset);
}
RenderEditableBox? sibling = childAfter(child);
final sibling = childAfter(child);
if (sibling == null) {
return null;
}
Offset caretOffset = child.getOffsetForCaret(childLocalPosition);
Offset testOffset =
sibling.getOffsetForCaret(const TextPosition(offset: 0));
Offset finalOffset = Offset(caretOffset.dx, testOffset.dy);
final caretOffset = child.getOffsetForCaret(childLocalPosition);
final testOffset = sibling.getOffsetForCaret(const TextPosition(offset: 0));
final finalOffset = Offset(caretOffset.dx, testOffset.dy);
return TextPosition(
offset: sibling.getContainer().getOffset() +
offset: sibling.getContainer().offset +
sibling.getPositionForOffset(finalOffset).offset);
}
@override
double preferredLineHeight(TextPosition position) {
RenderEditableBox child = childAtPosition(position);
return child.preferredLineHeight(TextPosition(
offset: position.offset - child.getContainer().getOffset()));
final child = childAtPosition(position);
return child.preferredLineHeight(
TextPosition(offset: position.offset - child.getContainer().offset));
}
@override
TextSelectionPoint getBaseEndpointForSelection(TextSelection selection) {
if (selection.isCollapsed) {
return TextSelectionPoint(
Offset(0.0, preferredLineHeight(selection.extent)) +
Offset(0, preferredLineHeight(selection.extent)) +
getOffsetForCaret(selection.extent),
null);
}
Node? baseNode = getContainer().queryChild(selection.start, false).node;
final baseNode = getContainer().queryChild(selection.start, false).node;
var baseChild = firstChild;
while (baseChild != null) {
if (baseChild.getContainer() == baseNode) {
@ -436,7 +433,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
}
assert(baseChild != null);
TextSelectionPoint basePoint = baseChild!.getBaseEndpointForSelection(
final basePoint = baseChild!.getBaseEndpointForSelection(
localSelection(baseChild.getContainer(), selection, true));
return TextSelectionPoint(
basePoint.point + (baseChild.parentData as BoxParentData).offset,
@ -447,12 +444,12 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
TextSelectionPoint getExtentEndpointForSelection(TextSelection selection) {
if (selection.isCollapsed) {
return TextSelectionPoint(
Offset(0.0, preferredLineHeight(selection.extent)) +
Offset(0, preferredLineHeight(selection.extent)) +
getOffsetForCaret(selection.extent),
null);
}
Node? extentNode = getContainer().queryChild(selection.end, false).node;
final extentNode = getContainer().queryChild(selection.end, false).node;
var extentChild = firstChild;
while (extentChild != null) {
@ -463,7 +460,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
}
assert(extentChild != null);
TextSelectionPoint extentPoint = extentChild!.getExtentEndpointForSelection(
final extentPoint = extentChild!.getExtentEndpointForSelection(
localSelection(extentChild.getContainer(), selection, true));
return TextSelectionPoint(
extentPoint.point + (extentChild.parentData as BoxParentData).offset,
@ -487,11 +484,11 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
void _paintDecoration(PaintingContext context, Offset offset) {
_painter ??= _decoration.createBoxPainter(markNeedsPaint);
EdgeInsets decorationPadding = resolvedPadding! - _contentPadding;
final decorationPadding = resolvedPadding! - _contentPadding;
ImageConfiguration filledConfiguration =
final filledConfiguration =
configuration.copyWith(size: decorationPadding.deflateSize(size));
int debugSaveCount = context.canvas.getSaveCount();
final debugSaveCount = context.canvas.getSaveCount();
final decorationOffset =
offset.translate(decorationPadding.left, decorationPadding.top);
@ -511,16 +508,21 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
}
class _EditableBlock extends MultiChildRenderObjectWidget {
_EditableBlock(
this.block,
this.textDirection,
this.padding,
this.decoration,
this.contentPadding,
List<Widget> children,
) : super(children: children);
final Block block;
final TextDirection textDirection;
final Tuple2<double, double> padding;
final Decoration decoration;
final EdgeInsets? contentPadding;
_EditableBlock(this.block, this.textDirection, this.padding, this.decoration,
this.contentPadding, List<Widget> children)
: super(children: children);
EdgeInsets get _padding =>
EdgeInsets.only(top: padding.item1, bottom: padding.item2);
@ -540,24 +542,16 @@ class _EditableBlock extends MultiChildRenderObjectWidget {
@override
void updateRenderObject(
BuildContext context, covariant RenderEditableTextBlock renderObject) {
renderObject.setContainer(block);
renderObject.textDirection = textDirection;
renderObject.setPadding(_padding);
renderObject.decoration = decoration;
renderObject.contentPadding = _contentPadding;
renderObject
..setContainer(block)
..textDirection = textDirection
..setPadding(_padding)
..decoration = decoration
..contentPadding = _contentPadding;
}
}
class _NumberPoint extends StatelessWidget {
final int index;
final Map<int?, int> indentLevelCounts;
final int count;
final TextStyle style;
final double width;
final Map<String, Attribute> attrs;
final bool withDot;
final double padding;
const _NumberPoint({
required this.index,
required this.indentLevelCounts,
@ -570,9 +564,18 @@ class _NumberPoint extends StatelessWidget {
Key? key,
}) : super(key: key);
final int index;
final Map<int?, int> indentLevelCounts;
final int count;
final TextStyle style;
final double width;
final Map<String, Attribute> attrs;
final bool withDot;
final double padding;
@override
Widget build(BuildContext context) {
String s = index.toString();
var s = index.toString();
int? level = 0;
if (!attrs.containsKey(Attribute.indent.key) &&
!indentLevelCounts.containsKey(1)) {
@ -581,7 +584,7 @@ class _NumberPoint extends StatelessWidget {
alignment: AlignmentDirectional.topEnd,
width: width,
padding: EdgeInsetsDirectional.only(end: padding),
child: Text(withDot ? '$s.' : '$s', style: style),
child: Text(withDot ? '$s.' : s, style: style),
);
}
if (attrs.containsKey(Attribute.indent.key)) {
@ -595,7 +598,7 @@ class _NumberPoint extends StatelessWidget {
// last visited level is done, going up
indentLevelCounts.remove(level + 1);
}
int count = (indentLevelCounts[level] ?? 0) + 1;
final count = (indentLevelCounts[level] ?? 0) + 1;
indentLevelCounts[level] = count;
s = count.toString();
@ -612,7 +615,7 @@ class _NumberPoint extends StatelessWidget {
alignment: AlignmentDirectional.topEnd,
width: width,
padding: EdgeInsetsDirectional.only(end: padding),
child: Text(withDot ? '$s.' : '$s', style: style),
child: Text(withDot ? '$s.' : s, style: style),
);
}
@ -653,33 +656,34 @@ class _NumberPoint extends StatelessWidget {
}
class _BulletPoint extends StatelessWidget {
final TextStyle style;
final double width;
const _BulletPoint({
required this.style,
required this.width,
Key? key,
}) : super(key: key);
final TextStyle style;
final double width;
@override
Widget build(BuildContext context) {
return Container(
alignment: AlignmentDirectional.topEnd,
width: width,
padding: const EdgeInsetsDirectional.only(end: 13.0),
padding: const EdgeInsetsDirectional.only(end: 13),
child: Text('', style: style),
);
}
}
class _Checkbox extends StatefulWidget {
const _Checkbox({Key? key, this.style, this.width, this.isChecked})
: super(key: key);
final TextStyle? style;
final double? width;
final bool? isChecked;
const _Checkbox({Key? key, this.style, this.width, this.isChecked})
: super(key: key);
@override
__CheckboxState createState() => __CheckboxState();
}
@ -708,7 +712,7 @@ class __CheckboxState extends State<_Checkbox> {
return Container(
alignment: AlignmentDirectional.topEnd,
width: widget.width,
padding: const EdgeInsetsDirectional.only(end: 13.0),
padding: const EdgeInsetsDirectional.only(end: 13),
child: Checkbox(
value: widget.isChecked,
onChanged: _onCheckboxClicked,

@ -3,30 +3,23 @@ import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart'
as container;
import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf;
import 'package:flutter_quill/models/documents/nodes/leaf.dart';
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/utils/color.dart';
import 'package:flutter_quill/widgets/cursor.dart';
import 'package:flutter_quill/widgets/proxy.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import 'package:tuple/tuple.dart';
import '../models/documents/attribute.dart';
import '../models/documents/nodes/container.dart' as container;
import '../models/documents/nodes/leaf.dart' as leaf;
import '../models/documents/nodes/leaf.dart';
import '../models/documents/nodes/line.dart';
import '../models/documents/nodes/node.dart';
import '../utils/color.dart';
import 'box.dart';
import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart';
import 'proxy.dart';
import 'text_selection.dart';
class TextLine extends StatelessWidget {
final Line line;
final TextDirection? textDirection;
final EmbedBuilder embedBuilder;
final DefaultStyles styles;
const TextLine({
required this.line,
required this.embedBuilder,
@ -35,21 +28,25 @@ class TextLine extends StatelessWidget {
Key? key,
}) : super(key: key);
final Line line;
final TextDirection? textDirection;
final EmbedBuilder embedBuilder;
final DefaultStyles styles;
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
if (line.hasEmbed) {
Embed embed = line.children.single as Embed;
final embed = line.children.single as Embed;
return EmbedProxy(embedBuilder(context, embed));
}
TextSpan textSpan = _buildTextSpan(context);
StrutStyle strutStyle =
StrutStyle.fromTextStyle(textSpan.style!, forceStrutHeight: true);
final textSpan = _buildTextSpan(context);
final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
final textAlign = _getTextAlign();
RichText child = RichText(
text: TextSpan(children: [textSpan]),
final child = RichText(
text: textSpan,
textAlign: textAlign,
textDirection: textDirection,
strutStyle: strutStyle,
@ -60,7 +57,7 @@ class TextLine extends StatelessWidget {
textSpan.style!,
textAlign,
textDirection!,
1.0,
1,
Localizations.localeOf(context),
strutStyle,
TextWidthBasis.parent,
@ -82,20 +79,20 @@ class TextLine extends StatelessWidget {
}
TextSpan _buildTextSpan(BuildContext context) {
DefaultStyles defaultStyles = styles;
List<TextSpan> children = line.children
final defaultStyles = styles;
final children = line.children
.map((node) => _getTextSpanFromNode(defaultStyles, node))
.toList(growable: false);
TextStyle textStyle = const TextStyle();
var textStyle = const TextStyle();
if (line.style.containsKey(Attribute.placeholder.key)) {
textStyle = defaultStyles.placeHolder!.style;
return TextSpan(children: children, style: textStyle);
}
Attribute? header = line.style.attributes[Attribute.header.key];
Map<Attribute, TextStyle> m = {
final header = line.style.attributes[Attribute.header.key];
final m = <Attribute, TextStyle>{
Attribute.h1: defaultStyles.h1!.style,
Attribute.h2: defaultStyles.h2!.style,
Attribute.h3: defaultStyles.h3!.style,
@ -103,7 +100,7 @@ class TextLine extends StatelessWidget {
textStyle = textStyle.merge(m[header] ?? defaultStyles.paragraph!.style);
Attribute? block = line.style.getBlockExceptHeader();
final block = line.style.getBlockExceptHeader();
TextStyle? toMerge;
if (block == Attribute.blockQuote) {
toMerge = defaultStyles.quote!.style;
@ -119,29 +116,28 @@ class TextLine extends StatelessWidget {
}
TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) {
leaf.Text textNode = node as leaf.Text;
Style style = textNode.style;
TextStyle res = const TextStyle();
final textNode = node as leaf.Text;
final style = textNode.style;
var res = const TextStyle();
Map<String, TextStyle?> m = {
<String, TextStyle?>{
Attribute.bold.key: defaultStyles.bold,
Attribute.italic.key: defaultStyles.italic,
Attribute.link.key: defaultStyles.link,
Attribute.underline.key: defaultStyles.underline,
Attribute.strikeThrough.key: defaultStyles.strikeThrough,
};
m.forEach((k, s) {
}.forEach((k, s) {
if (style.values.any((v) => v.key == k)) {
res = _merge(res, s!);
}
});
Attribute? font = textNode.style.attributes[Attribute.font.key];
final font = textNode.style.attributes[Attribute.font.key];
if (font != null && font.value != null) {
res = res.merge(TextStyle(fontFamily: font.value));
}
Attribute? size = textNode.style.attributes[Attribute.size.key];
final size = textNode.style.attributes[Attribute.size.key];
if (size != null && size.value != null) {
switch (size.value) {
case 'small':
@ -154,7 +150,7 @@ class TextLine extends StatelessWidget {
res = res.merge(defaultStyles.sizeHuge);
break;
default:
double? fontSize = double.tryParse(size.value);
final fontSize = double.tryParse(size.value);
if (fontSize != null) {
res = res.merge(TextStyle(fontSize: fontSize));
} else {
@ -163,7 +159,7 @@ class TextLine extends StatelessWidget {
}
}
Attribute? color = textNode.style.attributes[Attribute.color.key];
final color = textNode.style.attributes[Attribute.color.key];
if (color != null && color.value != null) {
var textColor = defaultStyles.color;
if (color.value is String) {
@ -174,7 +170,7 @@ class TextLine extends StatelessWidget {
}
}
Attribute? background = textNode.style.attributes[Attribute.background.key];
final background = textNode.style.attributes[Attribute.background.key];
if (background != null && background.value != null) {
final backgroundColor = stringToColor(background.value);
res = res.merge(TextStyle(backgroundColor: backgroundColor));
@ -198,6 +194,21 @@ class TextLine extends StatelessWidget {
}
class EditableTextLine extends RenderObjectWidget {
const EditableTextLine(
this.line,
this.leading,
this.body,
this.indentWidth,
this.verticalSpacing,
this.textDirection,
this.textSelection,
this.color,
this.enableInteractiveSelection,
this.hasFocus,
this.devicePixelRatio,
this.cursorCont,
);
final Line line;
final Widget? leading;
final Widget body;
@ -211,20 +222,6 @@ class EditableTextLine extends RenderObjectWidget {
final double devicePixelRatio;
final CursorCont cursorCont;
const EditableTextLine(
this.line,
this.leading,
this.body,
this.indentWidth,
this.verticalSpacing,
this.textDirection,
this.textSelection,
this.color,
this.enableInteractiveSelection,
this.hasFocus,
this.devicePixelRatio,
this.cursorCont);
@override
RenderObjectElement createElement() {
return _TextLineElement(this);
@ -247,15 +244,16 @@ class EditableTextLine extends RenderObjectWidget {
@override
void updateRenderObject(
BuildContext context, covariant RenderEditableTextLine renderObject) {
renderObject.setLine(line);
renderObject.setPadding(_getPadding());
renderObject.setTextDirection(textDirection);
renderObject.setTextSelection(textSelection);
renderObject.setColor(color);
renderObject.setEnableInteractiveSelection(enableInteractiveSelection);
renderObject.hasFocus = hasFocus;
renderObject.setDevicePixelRatio(devicePixelRatio);
renderObject.setCursorCont(cursorCont);
renderObject
..setLine(line)
..setPadding(_getPadding())
..setTextDirection(textDirection)
..setTextSelection(textSelection)
..setColor(color)
..setEnableInteractiveSelection(enableInteractiveSelection)
..hasFocus = hasFocus
..setDevicePixelRatio(devicePixelRatio)
..setCursorCont(cursorCont);
}
EdgeInsetsGeometry _getPadding() {
@ -269,6 +267,18 @@ class EditableTextLine extends RenderObjectWidget {
enum TextLineSlot { LEADING, BODY }
class RenderEditableTextLine extends RenderEditableBox {
RenderEditableTextLine(
this.line,
this.textDirection,
this.textSelection,
this.enableInteractiveSelection,
this.hasFocus,
this.devicePixelRatio,
this.padding,
this.color,
this.cursorCont,
);
RenderBox? _leading;
RenderContentProxyBox? _body;
Line line;
@ -286,17 +296,6 @@ class RenderEditableTextLine extends RenderEditableBox {
Rect? _caretPrototype;
final Map<TextLineSlot, RenderBox> children = <TextLineSlot, RenderBox>{};
RenderEditableTextLine(
this.line,
this.textDirection,
this.textSelection,
this.enableInteractiveSelection,
this.hasFocus,
this.devicePixelRatio,
this.padding,
this.color,
this.cursorCont);
Iterable<RenderBox> get _children sync* {
if (_leading != null) {
yield _leading!;
@ -347,7 +346,7 @@ class RenderEditableTextLine extends RenderEditableBox {
return;
}
bool containsSelection = containsTextSelection();
final containsSelection = containsTextSelection();
if (attached && containsCursor()) {
cursorCont.removeListener(markNeedsLayout);
cursorCont.color.removeListener(markNeedsPaint);
@ -403,8 +402,8 @@ class RenderEditableTextLine extends RenderEditableBox {
}
bool containsTextSelection() {
return line.getDocumentOffset() <= textSelection.end &&
textSelection.start <= line.getDocumentOffset() + line.length - 1;
return line.documentOffset <= textSelection.end &&
textSelection.start <= line.documentOffset + line.length - 1;
}
bool containsCursor() {
@ -426,7 +425,7 @@ class RenderEditableTextLine extends RenderEditableBox {
}
List<TextBox> _getBoxes(TextSelection textSelection) {
BoxParentData? parentData = _body!.parentData as BoxParentData?;
final parentData = _body!.parentData as BoxParentData?;
return _body!.getBoxesForSelection(textSelection).map((box) {
return TextBox.fromLTRBD(
box.left + parentData!.offset.dx,
@ -461,13 +460,13 @@ class RenderEditableTextLine extends RenderEditableBox {
TextSelection textSelection, bool first) {
if (textSelection.isCollapsed) {
return TextSelectionPoint(
Offset(0.0, preferredLineHeight(textSelection.extent)) +
Offset(0, preferredLineHeight(textSelection.extent)) +
getOffsetForCaret(textSelection.extent),
null);
}
List<TextBox> boxes = _getBoxes(textSelection);
final boxes = _getBoxes(textSelection);
assert(boxes.isNotEmpty);
TextBox targetBox = first ? boxes.first : boxes.last;
final targetBox = first ? boxes.first : boxes.last;
return TextSelectionPoint(
Offset(first ? targetBox.start : targetBox.end, targetBox.bottom),
targetBox.direction);
@ -475,10 +474,10 @@ class RenderEditableTextLine extends RenderEditableBox {
@override
TextRange getLineBoundary(TextPosition position) {
double lineDy = getOffsetForCaret(position)
.translate(0.0, 0.5 * preferredLineHeight(position))
final lineDy = getOffsetForCaret(position)
.translate(0, 0.5 * preferredLineHeight(position))
.dy;
List<TextBox> lineBoxes =
final lineBoxes =
_getBoxes(TextSelection(baseOffset: 0, extentOffset: line.length - 1))
.where((element) => element.top < lineDy && element.bottom > lineDy)
.toList(growable: false);
@ -506,7 +505,7 @@ class RenderEditableTextLine extends RenderEditableBox {
TextPosition? _getPosition(TextPosition textPosition, double dyScale) {
assert(textPosition.offset < line.length);
Offset offset = getOffsetForCaret(textPosition)
final offset = getOffsetForCaret(textPosition)
.translate(0, dyScale * preferredLineHeight(textPosition));
if (_body!.size
.contains(offset - (_body!.parentData as BoxParentData).offset)) {
@ -546,15 +545,13 @@ class RenderEditableTextLine extends RenderEditableBox {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
_caretPrototype =
Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight + 2);
_caretPrototype = Rect.fromLTWH(0, 0, cursorWidth, cursorHeight + 2);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
_caretPrototype =
Rect.fromLTWH(0.0, 2.0, cursorWidth, cursorHeight - 4.0);
_caretPrototype = Rect.fromLTWH(0, 2, cursorWidth, cursorHeight - 4.0);
break;
default:
throw 'Invalid platform';
@ -576,7 +573,7 @@ class RenderEditableTextLine extends RenderEditableBox {
@override
void detach() {
super.detach();
for (RenderBox child in _children) {
for (final child in _children) {
child.detach();
}
if (containsCursor()) {
@ -597,7 +594,7 @@ class RenderEditableTextLine extends RenderEditableBox {
@override
List<DiagnosticsNode> debugDescribeChildren() {
var value = <DiagnosticsNode>[];
final value = <DiagnosticsNode>[];
void add(RenderBox? child, String name) {
if (child != null) {
value.add(child.toDiagnosticsNode(name: name));
@ -615,14 +612,14 @@ class RenderEditableTextLine extends RenderEditableBox {
@override
double computeMinIntrinsicWidth(double height) {
_resolvePadding();
double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
int leadingWidth = _leading == null
final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
final leadingWidth = _leading == null
? 0
: _leading!.getMinIntrinsicWidth(height - verticalPadding) as int;
int bodyWidth = _body == null
final bodyWidth = _body == null
? 0
: _body!.getMinIntrinsicWidth(math.max(0.0, height - verticalPadding))
: _body!.getMinIntrinsicWidth(math.max(0, height - verticalPadding))
as int;
return horizontalPadding + leadingWidth + bodyWidth;
}
@ -630,14 +627,14 @@ class RenderEditableTextLine extends RenderEditableBox {
@override
double computeMaxIntrinsicWidth(double height) {
_resolvePadding();
double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
int leadingWidth = _leading == null
final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
final leadingWidth = _leading == null
? 0
: _leading!.getMaxIntrinsicWidth(height - verticalPadding) as int;
int bodyWidth = _body == null
final bodyWidth = _body == null
? 0
: _body!.getMaxIntrinsicWidth(math.max(0.0, height - verticalPadding))
: _body!.getMaxIntrinsicWidth(math.max(0, height - verticalPadding))
as int;
return horizontalPadding + leadingWidth + bodyWidth;
}
@ -645,11 +642,11 @@ class RenderEditableTextLine extends RenderEditableBox {
@override
double computeMinIntrinsicHeight(double width) {
_resolvePadding();
double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
if (_body != null) {
return _body!
.getMinIntrinsicHeight(math.max(0.0, width - horizontalPadding)) +
.getMinIntrinsicHeight(math.max(0, width - horizontalPadding)) +
verticalPadding;
}
return verticalPadding;
@ -658,11 +655,11 @@ class RenderEditableTextLine extends RenderEditableBox {
@override
double computeMaxIntrinsicHeight(double width) {
_resolvePadding();
double horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
double verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
final horizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right;
final verticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom;
if (_body != null) {
return _body!
.getMaxIntrinsicHeight(math.max(0.0, width - horizontalPadding)) +
.getMaxIntrinsicHeight(math.max(0, width - horizontalPadding)) +
verticalPadding;
}
return verticalPadding;
@ -697,8 +694,7 @@ class RenderEditableTextLine extends RenderEditableBox {
: _resolvedPadding!.right;
_body!.layout(innerConstraints, parentUsesSize: true);
final bodyParentData = _body!.parentData as BoxParentData;
bodyParentData.offset =
(_body!.parentData as BoxParentData).offset =
Offset(_resolvedPadding!.left, _resolvedPadding!.top);
if (_leading != null) {
@ -707,8 +703,8 @@ class RenderEditableTextLine extends RenderEditableBox {
maxWidth: indentWidth,
maxHeight: _body!.size.height);
_leading!.layout(leadingConstraints, parentUsesSize: true);
final parentData = _leading!.parentData as BoxParentData;
parentData.offset = Offset(0.0, _resolvedPadding!.top);
(_leading!.parentData as BoxParentData).offset =
Offset(0, _resolvedPadding!.top);
}
size = constraints.constrain(Size(
@ -739,8 +735,8 @@ class RenderEditableTextLine extends RenderEditableBox {
final parentData = _body!.parentData as BoxParentData;
final effectiveOffset = offset + parentData.offset;
if (enableInteractiveSelection &&
line.getDocumentOffset() <= textSelection.end &&
textSelection.start <= line.getDocumentOffset() + line.length - 1) {
line.documentOffset <= textSelection.end &&
textSelection.start <= line.documentOffset + line.length - 1) {
final local = localSelection(line, textSelection, false);
_selectedRects ??= _body!.getBoxesForSelection(
local,
@ -776,7 +772,7 @@ class RenderEditableTextLine extends RenderEditableBox {
void _paintCursor(PaintingContext context, Offset effectiveOffset) {
final position = TextPosition(
offset: textSelection.extentOffset - line.getDocumentOffset(),
offset: textSelection.extentOffset - line.documentOffset,
affinity: textSelection.base.affinity,
);
_cursorPainter.paint(context.canvas, effectiveOffset, position);
@ -851,8 +847,8 @@ class _TextLineElement extends RenderObjectElement {
}
void _mountChild(Widget? widget, TextLineSlot slot) {
Element? oldChild = _slotToChildren[slot];
Element? newChild = updateChild(oldChild, widget, slot);
final oldChild = _slotToChildren[slot];
final newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) {
_slotToChildren.remove(slot);
}
@ -875,8 +871,8 @@ class _TextLineElement extends RenderObjectElement {
}
void _updateChild(Widget? widget, TextLineSlot slot) {
Element? oldChild = _slotToChildren[slot];
Element? newChild = updateChild(oldChild, widget, slot);
final oldChild = _slotToChildren[slot];
final newChild = updateChild(oldChild, widget, slot);
if (oldChild != null) {
_slotToChildren.remove(slot);
}

@ -7,15 +7,15 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import '../models/documents/nodes/node.dart';
import 'editor.dart';
TextSelection localSelection(Node node, TextSelection selection, fromParent) {
int base = fromParent ? node.getOffset() : node.getDocumentOffset();
final base = fromParent ? node.offset : node.documentOffset;
assert(base <= selection.end && selection.start <= base + node.length - 1);
int offset = fromParent ? node.getOffset() : node.getDocumentOffset();
final offset = fromParent ? node.offset : node.documentOffset;
return selection.copyWith(
baseOffset: math.max(selection.start - offset, 0),
extentOffset: math.min(selection.end - offset, node.length - 1));
@ -24,6 +24,27 @@ TextSelection localSelection(Node node, TextSelection selection, fromParent) {
enum _TextSelectionHandlePosition { START, END }
class EditorTextSelectionOverlay {
EditorTextSelectionOverlay(
this.value,
this.handlesVisible,
this.context,
this.debugRequiredFor,
this.toolbarLayerLink,
this.startHandleLayerLink,
this.endHandleLayerLink,
this.renderObject,
this.selectionCtrls,
this.selectionDelegate,
this.dragStartBehavior,
this.onSelectionHandleTapped,
this.clipboardStatus,
) {
final overlay = Overlay.of(context, rootOverlay: true)!;
_toolbarController = AnimationController(
duration: const Duration(milliseconds: 150), vsync: overlay);
}
TextEditingValue value;
bool handlesVisible = false;
final BuildContext context;
@ -41,26 +62,6 @@ class EditorTextSelectionOverlay {
List<OverlayEntry>? _handles;
OverlayEntry? toolbar;
EditorTextSelectionOverlay(
this.value,
this.handlesVisible,
this.context,
this.debugRequiredFor,
this.toolbarLayerLink,
this.startHandleLayerLink,
this.endHandleLayerLink,
this.renderObject,
this.selectionCtrls,
this.selectionDelegate,
this.dragStartBehavior,
this.onSelectionHandleTapped,
this.clipboardStatus) {
OverlayState overlay = Overlay.of(context, rootOverlay: true)!;
_toolbarController = AnimationController(
duration: const Duration(milliseconds: 150), vsync: overlay);
}
TextSelection get _selection => value.selection;
Animation<double> get _toolbarOpacity => _toolbarController.view;
@ -99,7 +100,7 @@ class EditorTextSelectionOverlay {
toolbar = OverlayEntry(builder: _buildToolbar);
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
.insert(toolbar!);
_toolbarController.forward(from: 0.0);
_toolbarController.forward(from: 0);
}
Widget _buildHandle(
@ -111,7 +112,7 @@ class EditorTextSelectionOverlay {
return Visibility(
visible: handlesVisible,
child: _TextSelectionHandleOverlay(
onSelectionHandleChanged: (TextSelection? newSelection) {
onSelectionHandleChanged: (newSelection) {
_handleSelectionHandleChanged(newSelection, position);
},
onSelectionHandleTapped: onSelectionHandleTapped,
@ -155,32 +156,32 @@ class EditorTextSelectionOverlay {
default:
throw 'Invalid position';
}
selectionDelegate.textEditingValue =
value.copyWith(selection: newSelection, composing: TextRange.empty);
selectionDelegate.bringIntoView(textPosition);
selectionDelegate
..textEditingValue =
value.copyWith(selection: newSelection, composing: TextRange.empty)
..bringIntoView(textPosition);
}
Widget _buildToolbar(BuildContext context) {
List<TextSelectionPoint> endpoints =
renderObject!.getEndpointsForSelection(_selection);
final endpoints = renderObject!.getEndpointsForSelection(_selection);
Rect editingRegion = Rect.fromPoints(
final editingRegion = Rect.fromPoints(
renderObject!.localToGlobal(Offset.zero),
renderObject!.localToGlobal(renderObject!.size.bottomRight(Offset.zero)),
);
double baseLineHeight = renderObject!.preferredLineHeight(_selection.base);
double extentLineHeight =
final baseLineHeight = renderObject!.preferredLineHeight(_selection.base);
final extentLineHeight =
renderObject!.preferredLineHeight(_selection.extent);
double smallestLineHeight = math.min(baseLineHeight, extentLineHeight);
bool isMultiline = endpoints.last.point.dy - endpoints.first.point.dy >
final smallestLineHeight = math.min(baseLineHeight, extentLineHeight);
final isMultiline = endpoints.last.point.dy - endpoints.first.point.dy >
smallestLineHeight / 2;
double midX = isMultiline
final midX = isMultiline
? editingRegion.width / 2
: (endpoints.first.point.dx + endpoints.last.point.dx) / 2;
Offset midpoint = Offset(
final midpoint = Offset(
midX,
endpoints[0].point.dy - baseLineHeight,
);
@ -232,10 +233,10 @@ class EditorTextSelectionOverlay {
assert(_handles == null);
_handles = <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) =>
builder: (context) =>
_buildHandle(context, _TextSelectionHandlePosition.START)),
OverlayEntry(
builder: (BuildContext context) =>
builder: (context) =>
_buildHandle(context, _TextSelectionHandlePosition.END)),
];
@ -326,14 +327,14 @@ class _TextSelectionHandleOverlayState
void _handleDragStart(DragStartDetails details) {}
void _handleDragUpdate(DragUpdateDetails details) {
TextPosition position =
final position =
widget.renderObject!.getPositionForOffset(details.globalPosition);
if (widget.selection.isCollapsed) {
widget.onSelectionHandleChanged(TextSelection.fromPosition(position));
return;
}
bool isNormalized =
final isNormalized =
widget.selection.extentOffset >= widget.selection.baseOffset;
TextSelection? newSelection;
switch (widget.position) {
@ -389,27 +390,26 @@ class _TextSelectionHandleOverlayState
break;
}
TextPosition textPosition =
widget.position == _TextSelectionHandlePosition.START
? widget.selection.base
: widget.selection.extent;
double lineHeight = widget.renderObject!.preferredLineHeight(textPosition);
Offset handleAnchor =
final textPosition = widget.position == _TextSelectionHandlePosition.START
? widget.selection.base
: widget.selection.extent;
final lineHeight = widget.renderObject!.preferredLineHeight(textPosition);
final handleAnchor =
widget.selectionControls.getHandleAnchor(type!, lineHeight);
Size handleSize = widget.selectionControls.getHandleSize(lineHeight);
final handleSize = widget.selectionControls.getHandleSize(lineHeight);
Rect handleRect = Rect.fromLTWH(
final handleRect = Rect.fromLTWH(
-handleAnchor.dx,
-handleAnchor.dy,
handleSize.width,
handleSize.height,
);
Rect interactiveRect = handleRect.expandToInclude(
final interactiveRect = handleRect.expandToInclude(
Rect.fromCircle(
center: handleRect.center, radius: kMinInteractiveDimension / 2),
);
RelativeRect padding = RelativeRect.fromLTRB(
final padding = RelativeRect.fromLTRB(
math.max((interactiveRect.width - handleRect.width) / 2, 0),
math.max((interactiveRect.height - handleRect.height) / 2, 0),
math.max((interactiveRect.width - handleRect.width) / 2, 0),
@ -657,13 +657,12 @@ class _EditorTextSelectionGestureDetectorState
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures =
<Type, GestureRecognizerFactory>{};
final gestures = <Type, GestureRecognizerFactory>{};
gestures[TapGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
(instance) {
instance
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp
@ -678,7 +677,7 @@ class _EditorTextSelectionGestureDetectorState
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(
debugOwner: this, kind: PointerDeviceKind.touch),
(LongPressGestureRecognizer instance) {
(instance) {
instance
..onLongPressStart = _handleLongPressStart
..onLongPressMoveUpdate = _handleLongPressMoveUpdate
@ -694,7 +693,7 @@ class _EditorTextSelectionGestureDetectorState
GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(
debugOwner: this, kind: PointerDeviceKind.mouse),
(HorizontalDragGestureRecognizer instance) {
(instance) {
instance
..dragStartBehavior = DragStartBehavior.down
..onStart = _handleDragStart
@ -708,7 +707,7 @@ class _EditorTextSelectionGestureDetectorState
gestures[ForcePressGestureRecognizer] =
GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
() => ForcePressGestureRecognizer(debugOwner: this),
(ForcePressGestureRecognizer instance) {
(instance) {
instance
..onStart =
widget.onForcePressStart != null ? _forcePressStarted : null

@ -5,31 +5,31 @@ import 'package:filesystem_picker/filesystem_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/nodes/embed.dart';
import 'package:flutter_quill/models/documents/style.dart';
import 'package:flutter_quill/utils/color.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import '../models/documents/attribute.dart';
import '../models/documents/nodes/embed.dart';
import '../models/documents/style.dart';
import '../utils/color.dart';
import 'controller.dart';
double iconSize = 18.0;
double iconSize = 18;
double kToolbarHeight = iconSize * 2;
typedef OnImagePickCallback = Future<String> Function(File file);
typedef ImagePickImpl = Future<String> Function(ImageSource source);
class InsertEmbedButton extends StatelessWidget {
final QuillController controller;
final IconData icon;
const InsertEmbedButton({
required this.controller,
required this.icon,
Key? key,
}) : super(key: key);
final QuillController controller;
final IconData icon;
@override
Widget build(BuildContext context) {
return QuillIconButton(
@ -52,15 +52,15 @@ class InsertEmbedButton extends StatelessWidget {
}
class LinkStyleButton extends StatefulWidget {
final QuillController controller;
final IconData? icon;
const LinkStyleButton({
required this.controller,
this.icon,
Key? key,
}) : super(key: key);
final QuillController controller;
final IconData? icon;
@override
_LinkStyleButtonState createState() => _LinkStyleButtonState();
}
@ -174,14 +174,6 @@ typedef ToggleStyleButtonBuilder = Widget Function(
);
class ToggleStyleButton extends StatefulWidget {
final Attribute attribute;
final IconData icon;
final QuillController controller;
final ToggleStyleButtonBuilder childBuilder;
const ToggleStyleButton({
required this.attribute,
required this.icon,
@ -190,6 +182,14 @@ class ToggleStyleButton extends StatefulWidget {
Key? key,
}) : super(key: key);
final Attribute attribute;
final IconData icon;
final QuillController controller;
final ToggleStyleButtonBuilder childBuilder;
@override
_ToggleStyleButtonState createState() => _ToggleStyleButtonState();
}
@ -215,7 +215,7 @@ class _ToggleStyleButtonState extends State<ToggleStyleButton> {
bool _getIsToggled(Map<String, Attribute> attrs) {
if (widget.attribute.key == Attribute.list.key) {
Attribute? attribute = attrs[widget.attribute.key];
final attribute = attrs[widget.attribute.key];
if (attribute == null) {
return false;
}
@ -258,14 +258,6 @@ class _ToggleStyleButtonState extends State<ToggleStyleButton> {
}
class ToggleCheckListButton extends StatefulWidget {
final IconData icon;
final QuillController controller;
final ToggleStyleButtonBuilder childBuilder;
final Attribute attribute;
const ToggleCheckListButton({
required this.icon,
required this.controller,
@ -274,6 +266,14 @@ class ToggleCheckListButton extends StatefulWidget {
Key? key,
}) : super(key: key);
final IconData icon;
final QuillController controller;
final ToggleStyleButtonBuilder childBuilder;
final Attribute attribute;
@override
_ToggleCheckListButtonState createState() => _ToggleCheckListButtonState();
}
@ -299,7 +299,7 @@ class _ToggleCheckListButtonState extends State<ToggleCheckListButton> {
bool _getIsToggled(Map<String, Attribute> attrs) {
if (widget.attribute.key == Attribute.list.key) {
Attribute? attribute = attrs[widget.attribute.key];
final attribute = attrs[widget.attribute.key];
if (attribute == null) {
return false;
}
@ -369,11 +369,11 @@ Widget defaultToggleStyleButtonBuilder(
}
class SelectHeaderStyleButton extends StatefulWidget {
final QuillController controller;
const SelectHeaderStyleButton({required this.controller, Key? key})
: super(key: key);
final QuillController controller;
@override
_SelectHeaderStyleButtonState createState() =>
_SelectHeaderStyleButtonState();
@ -430,20 +430,20 @@ class _SelectHeaderStyleButtonState extends State<SelectHeaderStyleButton> {
Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value,
ValueChanged<Attribute?> onSelected) {
final Map<Attribute, String> _valueToText = {
final _valueToText = <Attribute, String>{
Attribute.header: 'N',
Attribute.h1: 'H1',
Attribute.h2: 'H2',
Attribute.h3: 'H3',
};
List<Attribute> _valueAttribute = [
final _valueAttribute = <Attribute>[
Attribute.header,
Attribute.h1,
Attribute.h2,
Attribute.h3
];
List<String> _valueString = ['N', 'H1', 'H2', 'H3'];
final _valueString = <String>['N', 'H1', 'H2', 'H3'];
final theme = Theme.of(context);
final style = TextStyle(
@ -464,7 +464,7 @@ Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value,
child: RawMaterialButton(
hoverElevation: 0,
highlightElevation: 0,
elevation: 0.0,
elevation: 0,
visualDensity: VisualDensity.compact,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)),
@ -490,6 +490,15 @@ Widget _selectHeadingStyleButtonBuilder(BuildContext context, Attribute? value,
}
class ImageButton extends StatefulWidget {
const ImageButton({
required this.icon,
required this.controller,
required this.imageSource,
this.onImagePickCallback,
this.imagePickImpl,
Key? key,
}) : super(key: key);
final IconData icon;
final QuillController controller;
@ -500,15 +509,6 @@ class ImageButton extends StatefulWidget {
final ImageSource imageSource;
const ImageButton({
required this.icon,
required this.controller,
required this.imageSource,
this.onImagePickCallback,
this.imagePickImpl,
Key? key,
}) : super(key: key);
@override
_ImageButtonState createState() => _ImageButtonState();
}
@ -520,10 +520,10 @@ class _ImageButtonState extends State<ImageButton> {
final FileType _pickingType = FileType.any;
Future<String?> _pickImage(ImageSource source) async {
final PickedFile? pickedFile = await _picker.getImage(source: source);
final pickedFile = await _picker.getImage(source: source);
if (pickedFile == null) return null;
final File file = File(pickedFile.path);
final file = File(pickedFile.path);
return widget.onImagePickCallback!(file);
}
@ -536,11 +536,11 @@ class _ImageButtonState extends State<ImageButton> {
: null,
))
?.files;
var _fileName =
final _fileName =
_paths != null ? _paths!.map((e) => e.name).toString() : '...';
if (_paths != null) {
File file = File(_fileName);
final file = File(_fileName);
// We simply return the absolute path to selected file.
return widget.onImagePickCallback!(file);
} else {
@ -550,7 +550,7 @@ class _ImageButtonState extends State<ImageButton> {
}
Future<String> _pickImageDesktop() async {
var filePath = await FilesystemPicker.open(
final filePath = await FilesystemPicker.open(
context: context,
rootDirectory: await getApplicationDocumentsDirectory(),
fsType: FilesystemType.file,
@ -558,7 +558,7 @@ class _ImageButtonState extends State<ImageButton> {
);
if (filePath != null && filePath.isEmpty) return '';
final File file = File(filePath!);
final file = File(filePath!);
return widget.onImagePickCallback!(file);
}
@ -602,10 +602,6 @@ class _ImageButtonState extends State<ImageButton> {
/// When pressed, this button displays overlay toolbar with
/// buttons for each color.
class ColorButton extends StatefulWidget {
final IconData icon;
final bool background;
final QuillController controller;
const ColorButton({
required this.icon,
required this.controller,
@ -613,6 +609,10 @@ class ColorButton extends StatefulWidget {
Key? key,
}) : super(key: key);
final IconData icon;
final bool background;
final QuillController controller;
@override
_ColorButtonState createState() => _ColorButtonState();
}
@ -683,19 +683,19 @@ class _ColorButtonState extends State<ColorButton> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
Color? iconColor = _isToggledColor && !widget.background && !_isWhite
final iconColor = _isToggledColor && !widget.background && !_isWhite
? stringToColor(_selectionStyle.attributes['color']!.value)
: theme.iconTheme.color;
var iconColorBackground =
final iconColorBackground =
_isToggledBackground && widget.background && !_isWhitebackground
? stringToColor(_selectionStyle.attributes['background']!.value)
: theme.iconTheme.color;
Color fillColor = _isToggledColor && !widget.background && _isWhite
final fillColor = _isToggledColor && !widget.background && _isWhite
? stringToColor('#ffffff')
: theme.canvasColor;
Color fillColorBackground =
final fillColorBackground =
_isToggledBackground && widget.background && _isWhitebackground
? stringToColor('#ffffff')
: theme.canvasColor;
@ -713,7 +713,7 @@ class _ColorButtonState extends State<ColorButton> {
}
void _changeColor(Color color) {
String hex = color.value.toRadixString(16);
var hex = color.value.toRadixString(16);
if (hex.startsWith('ff')) {
hex = hex.substring(2);
}
@ -740,10 +740,6 @@ class _ColorButtonState extends State<ColorButton> {
}
class HistoryButton extends StatefulWidget {
final IconData icon;
final bool undo;
final QuillController controller;
const HistoryButton({
required this.icon,
required this.controller,
@ -751,6 +747,10 @@ class HistoryButton extends StatefulWidget {
Key? key,
}) : super(key: key);
final IconData icon;
final bool undo;
final QuillController controller;
@override
_HistoryButtonState createState() => _HistoryButtonState();
}
@ -812,10 +812,6 @@ class _HistoryButtonState extends State<HistoryButton> {
}
class IndentButton extends StatefulWidget {
final IconData icon;
final QuillController controller;
final bool isIncrease;
const IndentButton({
required this.icon,
required this.controller,
@ -823,6 +819,10 @@ class IndentButton extends StatefulWidget {
Key? key,
}) : super(key: key);
final IconData icon;
final QuillController controller;
final bool isIncrease;
@override
_IndentButtonState createState() => _IndentButtonState();
}
@ -867,16 +867,16 @@ class _IndentButtonState extends State<IndentButton> {
}
class ClearFormatButton extends StatefulWidget {
final IconData icon;
final QuillController controller;
const ClearFormatButton({
required this.icon,
required this.controller,
Key? key,
}) : super(key: key);
final IconData icon;
final QuillController controller;
@override
_ClearFormatButtonState createState() => _ClearFormatButtonState();
}
@ -894,7 +894,7 @@ class _ClearFormatButtonState extends State<ClearFormatButton> {
icon: Icon(widget.icon, size: iconSize, color: iconColor),
fillColor: fillColor,
onPressed: () {
for (Attribute k
for (final k
in widget.controller.getSelectionStyle().attributes.values) {
widget.controller.formatSelection(Attribute.clone(k, null));
}
@ -903,8 +903,6 @@ class _ClearFormatButtonState extends State<ClearFormatButton> {
}
class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
final List<Widget> children;
const QuillToolbar({required this.children, Key? key}) : super(key: key);
factory QuillToolbar.basic({
@ -1117,6 +1115,8 @@ class QuillToolbar extends StatefulWidget implements PreferredSizeWidget {
]);
}
final List<Widget> children;
@override
_QuillToolbarState createState() => _QuillToolbarState();
@ -1148,13 +1148,6 @@ class _QuillToolbarState extends State<QuillToolbar> {
}
class QuillIconButton extends StatelessWidget {
final VoidCallback? onPressed;
final Widget? icon;
final double size;
final Color? fillColor;
final double hoverElevation;
final double highlightElevation;
const QuillIconButton({
required this.onPressed,
this.icon,
@ -1165,6 +1158,13 @@ class QuillIconButton extends StatelessWidget {
Key? key,
}) : super(key: key);
final VoidCallback? onPressed;
final Widget? icon;
final double size;
final Color? fillColor;
final double hoverElevation;
final double highlightElevation;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
@ -1184,15 +1184,6 @@ class QuillIconButton extends StatelessWidget {
}
class QuillDropdownButton<T> extends StatefulWidget {
final double height;
final Color? fillColor;
final double hoverElevation;
final double highlightElevation;
final Widget child;
final T initialValue;
final List<PopupMenuEntry<T>> items;
final ValueChanged<T> onSelected;
const QuillDropdownButton({
required this.child,
required this.initialValue,
@ -1205,6 +1196,15 @@ class QuillDropdownButton<T> extends StatefulWidget {
Key? key,
}) : super(key: key);
final double height;
final Color? fillColor;
final double hoverElevation;
final double highlightElevation;
final Widget child;
final T initialValue;
final List<PopupMenuEntry<T>> items;
final ValueChanged<T> onSelected;
@override
_QuillDropdownButtonState<T> createState() => _QuillDropdownButtonState<T>();
}
@ -1251,7 +1251,7 @@ class _QuillDropdownButtonState<T> extends State<QuillDropdownButton<T>> {
// widget.shape ?? popupMenuTheme.shape,
color: popupMenuTheme.color, // widget.color ?? popupMenuTheme.color,
// captureInheritedThemes: widget.captureInheritedThemes,
).then((T? newValue) {
).then((newValue) {
if (!mounted) return null;
if (newValue == null) {
// if (widget.onCanceled != null) widget.onCanceled();
@ -1265,7 +1265,7 @@ class _QuillDropdownButtonState<T> extends State<QuillDropdownButton<T>> {
return ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: 110),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
widget.child,

@ -1,6 +1,6 @@
name: flutter_quill
description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us)
version: 1.1.7
version: 1.1.8
#author: bulletjournal
homepage: https://bulletjournal.us/home/index.html
repository: https://github.com/singerdmx/flutter-quill

Loading…
Cancel
Save