First step of improving the quill editor (#1510)

* First step of improving the raw quill editor
pull/1511/head
Ellet 1 year ago committed by GitHub
parent 39154183b7
commit cf93061446
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 3
      CHANGELOG.md
  3. 3
      CONTRIBUTING.md
  4. 2
      README.md
  5. 60
      doc/CONTRIBUTING.md
  6. 2
      flutter_quill_extensions/README.md
  7. 7
      flutter_quill_extensions/pubspec.yaml
  8. 3
      flutter_quill_extensions/pubspec_overrides.yaml.g
  9. 2
      flutter_quill_test/README.md
  10. 10
      flutter_quill_test/pubspec.yaml
  11. 3
      flutter_quill_test/pubspec_overrides.yaml.g
  12. 1
      lib/flutter_quill.dart
  13. 7
      lib/src/widgets/editor/editor.dart
  14. 2534
      lib/src/widgets/raw_editor/raw_editor.dart
  15. 578
      lib/src/widgets/raw_editor/raw_editor_actions.dart
  16. 1617
      lib/src/widgets/raw_editor/raw_editor_state.dart
  17. 292
      lib/src/widgets/raw_editor/raw_editor_text_boundaries.dart
  18. 86
      lib/src/widgets/raw_editor/raw_editor_widget.dart
  19. 7
      pubspec.yaml
  20. 3
      pubspec_overrides.yaml.g
  21. 0
      scripts/before-push.sh
  22. 20
      scripts/disable_local_dev.sh
  23. 20
      scripts/enable_local_dev.sh

3
.gitignore vendored

@ -75,3 +75,6 @@ example/ios/Podfile.lock
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
pubspec.lock
# For local development
pubspec_overrides.yaml

@ -1,3 +1,6 @@
## [8.2.6]
- Organize `QuillRawEditor` code
## [8.2.5]
- Add `builder` property in the `QuillEditorConfigurations`

@ -1,3 +0,0 @@
# Contributing
We welcome contributions!

@ -297,7 +297,7 @@ Made with [contrib.rocks](https://contrib.rocks).
We welcome contributions!
Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](./doc/CONTRIBUTING.md) for more details.
[Quill]: https://quilljs.com/docs/formats
[Flutter]: https://github.com/flutter/flutter

@ -0,0 +1,60 @@
# Contributing
The contributions are more than welcome! <br>
This project will be better with the open-source community help
There are no guidelines for now.
This page will be updated in the future.
## Steps to contributing
You will need GitHub account as well as git installed and configured with your GitHub account on your machine
1. Fork the repository in GitHub
2. clone the forked repository using `git`
3. Add the `upstream` repository using:
```
git remote add upstream git@github.com:singerdmx/flutter-quill.git
```
4. Open the project with your favorite IDE, we suggest using [IntelliJ IDEA Community Edition](https://www.jetbrains.com/idea/download/)
5. Sync the project with Gradle
6. Create a new git branch and switch to it using:
```
git checkout -b your-branch-name
```
The `your-branch-name` is your choice
7. Make your changes
8. If you are working on changes that depend on different library in the same repo, then in that directory copy `pubspec_overrides.yaml.g` which exists in all the libraries (`flutter_quill_test` and `flutter_quill_extensions` etc..)
to `pubspec_overrides.yaml` which will be ignored by `.gitignore` and it will be used by dart pub to override the libraries
```
cp pubspec_overrides.yaml.g pubspec_overrides.yaml
```
or save some time and the following script:
```
./scripts/enable_local_dev.sh
```
10. Test them in the [example](../example) and add changes in there if necessary
11. Mention the new changes in the [CHANGELOG.md](../CHANGELOG.md) in the next block
12. Run the following script if possible
```
./scripts/before-push.sh
```
13. When you are done to send your pull request, run:
```
git add .
git commit -m "Your commit message"
git push origin your-branch-name
```
this will push the new branch to your forked repository
14. Now you can send your pull request either by following the link that you will get in the command line or open your
forked repository, and you will find an option to send the pull request, you can also
open the [Pull Requests](https://github.com/singerdmx/flutter-quill) tab and send new pull request
1. Please wait for the review, and we might ask you to make more changes, then run:
```
git add .
git commit -m "Your new commit message"
git push origin your-branch-name
```
Thank you for your time and efforts to this open-source community project!!

@ -241,7 +241,7 @@ OnDragDoneCallback get _onDragDone {
We welcome contributions!
Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](./../CONTRIBUTING.md) for more details.
Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](./../doc/CONTRIBUTING.md) for more details.
## License

@ -42,13 +42,6 @@ dependencies:
meta: ^1.9.1
cross_file: ^0.3.3+6
# In case you are working on changes for both libraries
# Comment the dependency_overrides section when publishing the package,
# then uncomment it back, this will be automated later
dependency_overrides:
flutter_quill:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter

@ -0,0 +1,3 @@
dependency_overrides:
flutter_quill:
path: ../

@ -38,7 +38,7 @@ await tester.quillEnterText(find.byType(QuillEditor), 'test\n');
We welcome contributions!
Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](./../CONTRIBUTING.md) for more details.
Please follow these guidelines when contributing to our project. See [CONTRIBUTING.md](./../doc/CONTRIBUTING.md) for more details.
## License

@ -26,20 +26,12 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_quill: ^8.2.4
flutter_quill: ^8.2.5
flutter_test:
sdk: flutter
dev_dependencies:
flutter_lints: ^3.0.1
# In case you are working on changes for both libraries
# Comment the dependency_overrides section when publishing the package,
# then uncomment it back, this will be automated later
# dependency_overrides:
# flutter_quill:
# path: ../
flutter:
uses-material-design: true

@ -0,0 +1,3 @@
dependency_overrides:
flutter_quill:
path: ../

@ -28,6 +28,7 @@ export 'src/widgets/editor/editor.dart';
export 'src/widgets/embeds.dart';
export 'src/widgets/link.dart' show LinkActionPickerDelegate, LinkMenuAction;
export 'src/widgets/raw_editor/raw_editor.dart';
export 'src/widgets/raw_editor/raw_editor_state.dart';
export 'src/widgets/style_widgets/style_widgets.dart';
export 'src/widgets/toolbar/base_toolbar.dart';
export 'src/widgets/toolbar/toolbar.dart';

@ -1,8 +1,9 @@
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/cupertino.dart'
show CupertinoTheme, cupertinoTextSelectionControls;
import 'package:flutter/foundation.dart' show ValueListenable;
import 'package:flutter/gestures.dart' show PointerDeviceKind;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

File diff suppressed because it is too large Load Diff

@ -0,0 +1,578 @@
import 'package:flutter/material.dart';
import '../../models/documents/attribute.dart';
import '../editor/editor.dart';
import '../toolbar/buttons/link_style2.dart';
import '../toolbar/buttons/search/search_dialog.dart';
import 'raw_editor_state.dart';
import 'raw_editor_text_boundaries.dart';
// ------------------------------- Text Actions -------------------------------
class QuillEditorDeleteTextAction<T extends DirectionalTextEditingIntent>
extends ContextAction<T> {
QuillEditorDeleteTextAction(this.state, this.getTextBoundariesForIntent);
final QuillRawEditorState state;
final QuillEditorTextBoundary Function(T intent) getTextBoundariesForIntent;
TextRange _expandNonCollapsedRange(TextEditingValue value) {
final TextRange selection = value.selection;
assert(selection.isValid);
assert(!selection.isCollapsed);
final atomicBoundary = QuillEditorCharacterBoundary(value);
return TextRange(
start: atomicBoundary
.getLeadingTextBoundaryAt(TextPosition(offset: selection.start))
.offset,
end: atomicBoundary
.getTrailingTextBoundaryAt(TextPosition(offset: selection.end - 1))
.offset,
);
}
@override
Object? invoke(T intent, [BuildContext? context]) {
final selection = state.textEditingValue.selection;
assert(selection.isValid);
if (!selection.isCollapsed) {
return Actions.invoke(
context!,
ReplaceTextIntent(
state.textEditingValue,
'',
_expandNonCollapsedRange(state.textEditingValue),
SelectionChangedCause.keyboard),
);
}
final textBoundary = getTextBoundariesForIntent(intent);
if (!textBoundary.textEditingValue.selection.isValid) {
return null;
}
if (!textBoundary.textEditingValue.selection.isCollapsed) {
return Actions.invoke(
context!,
ReplaceTextIntent(
state.textEditingValue,
'',
_expandNonCollapsedRange(textBoundary.textEditingValue),
SelectionChangedCause.keyboard),
);
}
return Actions.invoke(
context!,
ReplaceTextIntent(
textBoundary.textEditingValue,
'',
textBoundary
.getTextBoundaryAt(textBoundary.textEditingValue.selection.base),
SelectionChangedCause.keyboard,
),
);
}
@override
bool get isActionEnabled =>
!state.widget.readOnly && state.textEditingValue.selection.isValid;
}
class QuillEditorUpdateTextSelectionAction<
T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
QuillEditorUpdateTextSelectionAction(this.state,
this.ignoreNonCollapsedSelection, this.getTextBoundariesForIntent);
final QuillRawEditorState state;
final bool ignoreNonCollapsedSelection;
final QuillEditorTextBoundary Function(T intent) getTextBoundariesForIntent;
@override
Object? invoke(T intent, [BuildContext? context]) {
final selection = state.textEditingValue.selection;
assert(selection.isValid);
final collapseSelection =
intent.collapseSelection || !state.widget.selectionEnabled;
// Collapse to the logical start/end.
TextSelection collapse(TextSelection selection) {
assert(selection.isValid);
assert(!selection.isCollapsed);
return selection.copyWith(
baseOffset: intent.forward ? selection.end : selection.start,
extentOffset: intent.forward ? selection.end : selection.start,
);
}
if (!selection.isCollapsed &&
!ignoreNonCollapsedSelection &&
collapseSelection) {
return Actions.invoke(
context!,
UpdateSelectionIntent(
state.textEditingValue,
collapse(selection),
SelectionChangedCause.keyboard,
),
);
}
final textBoundary = getTextBoundariesForIntent(intent);
final textBoundarySelection = textBoundary.textEditingValue.selection;
if (!textBoundarySelection.isValid) {
return null;
}
if (!textBoundarySelection.isCollapsed &&
!ignoreNonCollapsedSelection &&
collapseSelection) {
return Actions.invoke(
context!,
UpdateSelectionIntent(state.textEditingValue,
collapse(textBoundarySelection), SelectionChangedCause.keyboard),
);
}
final extent = textBoundarySelection.extent;
final newExtent = intent.forward
? textBoundary.getTrailingTextBoundaryAt(extent)
: textBoundary.getLeadingTextBoundaryAt(extent);
final newSelection = collapseSelection
? TextSelection.fromPosition(newExtent)
: textBoundarySelection.extendTo(newExtent);
// If collapseAtReversal is true and would have an effect, collapse it.
if (!selection.isCollapsed &&
intent.collapseAtReversal &&
(selection.baseOffset < selection.extentOffset !=
newSelection.baseOffset < newSelection.extentOffset)) {
return Actions.invoke(
context!,
UpdateSelectionIntent(
state.textEditingValue,
TextSelection.fromPosition(selection.base),
SelectionChangedCause.keyboard,
),
);
}
return Actions.invoke(
context!,
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection,
SelectionChangedCause.keyboard),
);
}
@override
bool get isActionEnabled => state.textEditingValue.selection.isValid;
}
class QuillEditorExtendSelectionOrCaretPositionAction extends ContextAction<
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent> {
QuillEditorExtendSelectionOrCaretPositionAction(
this.state, this.getTextBoundariesForIntent);
final QuillRawEditorState state;
final QuillEditorTextBoundary Function(
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent)
getTextBoundariesForIntent;
@override
Object? invoke(ExtendSelectionToNextWordBoundaryOrCaretLocationIntent intent,
[BuildContext? context]) {
final selection = state.textEditingValue.selection;
assert(selection.isValid);
final textBoundary = getTextBoundariesForIntent(intent);
final textBoundarySelection = textBoundary.textEditingValue.selection;
if (!textBoundarySelection.isValid) {
return null;
}
final extent = textBoundarySelection.extent;
final newExtent = intent.forward
? textBoundary.getTrailingTextBoundaryAt(extent)
: textBoundary.getLeadingTextBoundaryAt(extent);
final newSelection = (newExtent.offset - textBoundarySelection.baseOffset) *
(textBoundarySelection.extentOffset -
textBoundarySelection.baseOffset) <
0
? textBoundarySelection.copyWith(
extentOffset: textBoundarySelection.baseOffset,
affinity: textBoundarySelection.extentOffset >
textBoundarySelection.baseOffset
? TextAffinity.downstream
: TextAffinity.upstream,
)
: textBoundarySelection.extendTo(newExtent);
return Actions.invoke(
context!,
UpdateSelectionIntent(textBoundary.textEditingValue, newSelection,
SelectionChangedCause.keyboard),
);
}
@override
bool get isActionEnabled =>
state.widget.selectionEnabled && state.textEditingValue.selection.isValid;
}
class QuillEditorUpdateTextSelectionToAdjacentLineAction<
T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
QuillEditorUpdateTextSelectionToAdjacentLineAction(this.state);
final QuillRawEditorState state;
QuillVerticalCaretMovementRun? _verticalMovementRun;
TextSelection? _runSelection;
void stopCurrentVerticalRunIfSelectionChanges() {
final runSelection = _runSelection;
if (runSelection == null) {
assert(_verticalMovementRun == null);
return;
}
_runSelection = state.textEditingValue.selection;
final currentSelection = state.controller.selection;
final continueCurrentRun = currentSelection.isValid &&
currentSelection.isCollapsed &&
currentSelection.baseOffset == runSelection.baseOffset &&
currentSelection.extentOffset == runSelection.extentOffset;
if (!continueCurrentRun) {
_verticalMovementRun = null;
_runSelection = null;
}
}
@override
void invoke(T intent, [BuildContext? context]) {
assert(state.textEditingValue.selection.isValid);
final collapseSelection =
intent.collapseSelection || !state.widget.selectionEnabled;
final value = state.textEditingValue;
if (!value.selection.isValid) {
return;
}
final currentRun = _verticalMovementRun ??
state.renderEditor
.startVerticalCaretMovement(state.renderEditor.selection.extent);
final shouldMove =
intent.forward ? currentRun.moveNext() : currentRun.movePrevious();
final newExtent = shouldMove
? currentRun.current
: (intent.forward
? TextPosition(offset: state.textEditingValue.text.length)
: const TextPosition(offset: 0));
final newSelection = collapseSelection
? TextSelection.fromPosition(newExtent)
: value.selection.extendTo(newExtent);
Actions.invoke(
context!,
UpdateSelectionIntent(
value, newSelection, SelectionChangedCause.keyboard),
);
if (state.textEditingValue.selection == newSelection) {
_verticalMovementRun = currentRun;
_runSelection = newSelection;
}
}
@override
bool get isActionEnabled => state.textEditingValue.selection.isValid;
}
class QuillEditorSelectAllAction extends ContextAction<SelectAllTextIntent> {
QuillEditorSelectAllAction(this.state);
final QuillRawEditorState state;
@override
Object? invoke(SelectAllTextIntent intent, [BuildContext? context]) {
return Actions.invoke(
context!,
UpdateSelectionIntent(
state.textEditingValue,
TextSelection(
baseOffset: 0, extentOffset: state.textEditingValue.text.length),
intent.cause,
),
);
}
@override
bool get isActionEnabled => state.widget.selectionEnabled;
}
class QuillEditorCopySelectionAction
extends ContextAction<CopySelectionTextIntent> {
QuillEditorCopySelectionAction(this.state);
final QuillRawEditorState state;
@override
void invoke(CopySelectionTextIntent intent, [BuildContext? context]) {
if (intent.collapseSelection) {
state.cutSelection(intent.cause);
} else {
state.copySelection(intent.cause);
}
}
@override
bool get isActionEnabled =>
state.textEditingValue.selection.isValid &&
!state.textEditingValue.selection.isCollapsed;
}
//Intent class for "escape" key to dismiss selection toolbar in Windows platform
class HideSelectionToolbarIntent extends Intent {
const HideSelectionToolbarIntent();
}
class QuillEditorHideSelectionToolbarAction
extends ContextAction<HideSelectionToolbarIntent> {
QuillEditorHideSelectionToolbarAction(this.state);
final QuillRawEditorState state;
@override
void invoke(HideSelectionToolbarIntent intent, [BuildContext? context]) {
state.hideToolbar();
}
@override
bool get isActionEnabled => state.textEditingValue.selection.isValid;
}
class QuillEditorUndoKeyboardAction extends ContextAction<UndoTextIntent> {
QuillEditorUndoKeyboardAction(this.state);
final QuillRawEditorState state;
@override
void invoke(UndoTextIntent intent, [BuildContext? context]) {
if (state.controller.hasUndo) {
state.controller.undo();
}
}
@override
bool get isActionEnabled => true;
}
class QuillEditorRedoKeyboardAction extends ContextAction<RedoTextIntent> {
QuillEditorRedoKeyboardAction(this.state);
final QuillRawEditorState state;
@override
void invoke(RedoTextIntent intent, [BuildContext? context]) {
if (state.controller.hasRedo) {
state.controller.redo();
}
}
@override
bool get isActionEnabled => true;
}
class ToggleTextStyleIntent extends Intent {
const ToggleTextStyleIntent(this.attribute);
final Attribute attribute;
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorToggleTextStyleAction extends Action<ToggleTextStyleIntent> {
QuillEditorToggleTextStyleAction(this.state);
final QuillRawEditorState state;
bool _isStyleActive(Attribute styleAttr, Map<String, Attribute> attrs) {
if (styleAttr.key == Attribute.list.key) {
final attribute = attrs[styleAttr.key];
if (attribute == null) {
return false;
}
return attribute.value == styleAttr.value;
}
return attrs.containsKey(styleAttr.key);
}
@override
void invoke(ToggleTextStyleIntent intent, [BuildContext? context]) {
final isActive = _isStyleActive(
intent.attribute, state.controller.getSelectionStyle().attributes);
state.controller.formatSelection(
isActive ? Attribute.clone(intent.attribute, null) : intent.attribute);
}
@override
bool get isActionEnabled => true;
}
class IndentSelectionIntent extends Intent {
const IndentSelectionIntent(this.isIncrease);
final bool isIncrease;
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorIndentSelectionAction extends Action<IndentSelectionIntent> {
QuillEditorIndentSelectionAction(this.state);
final QuillRawEditorState state;
@override
void invoke(IndentSelectionIntent intent, [BuildContext? context]) {
state.controller.indentSelection(intent.isIncrease);
}
@override
bool get isActionEnabled => true;
}
class OpenSearchIntent extends Intent {
const OpenSearchIntent();
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorOpenSearchAction extends ContextAction<OpenSearchIntent> {
QuillEditorOpenSearchAction(this.state);
final QuillRawEditorState state;
@override
Future invoke(OpenSearchIntent intent, [BuildContext? context]) async {
if (context == null) {
throw ArgumentError(
'The context should not be null to use invoke() method',
);
}
await showDialog<String>(
context: context,
builder: (_) => QuillToolbarSearchDialog(
controller: state.controller,
text: '',
),
);
}
@override
bool get isActionEnabled => true;
}
class QuillEditorApplyHeaderIntent extends Intent {
const QuillEditorApplyHeaderIntent(this.header);
final Attribute header;
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorApplyHeaderAction
extends Action<QuillEditorApplyHeaderIntent> {
QuillEditorApplyHeaderAction(this.state);
final QuillRawEditorState state;
Attribute<dynamic> _getHeaderValue() {
return state.controller
.getSelectionStyle()
.attributes[Attribute.header.key] ??
Attribute.header;
}
@override
void invoke(QuillEditorApplyHeaderIntent intent, [BuildContext? context]) {
final attribute =
_getHeaderValue() == intent.header ? Attribute.header : intent.header;
state.controller.formatSelection(attribute);
}
@override
bool get isActionEnabled => true;
}
class QuillEditorApplyCheckListIntent extends Intent {
const QuillEditorApplyCheckListIntent();
}
// Toggles a text style (underline, bold, italic, strikethrough) on, or off.
class QuillEditorApplyCheckListAction
extends Action<QuillEditorApplyCheckListIntent> {
QuillEditorApplyCheckListAction(this.state);
final QuillRawEditorState state;
bool _getIsToggled() {
final attrs = state.controller.getSelectionStyle().attributes;
var attribute = state.controller.toolbarButtonToggler[Attribute.list.key];
if (attribute == null) {
attribute = attrs[Attribute.list.key];
} else {
// checkbox tapping causes controller.selection to go to offset 0
state.controller.toolbarButtonToggler.remove(Attribute.list.key);
}
if (attribute == null) {
return false;
}
return attribute.value == Attribute.unchecked.value ||
attribute.value == Attribute.checked.value;
}
@override
void invoke(QuillEditorApplyCheckListIntent intent, [BuildContext? context]) {
state.controller.formatSelection(_getIsToggled()
? Attribute.clone(Attribute.unchecked, null)
: Attribute.unchecked);
}
@override
bool get isActionEnabled => true;
}
class QuillEditorApplyLinkIntent extends Intent {
const QuillEditorApplyLinkIntent();
}
class QuillEditorApplyLinkAction extends Action<QuillEditorApplyLinkIntent> {
QuillEditorApplyLinkAction(this.state);
final QuillRawEditorState state;
@override
Object? invoke(QuillEditorApplyLinkIntent intent) async {
final initialTextLink = QuillTextLink.prepare(state.controller);
final textLink = await showDialog<QuillTextLink>(
context: state.context,
builder: (context) {
return LinkStyleDialog(
text: initialTextLink.text,
link: initialTextLink.link,
dialogTheme: state.widget.dialogTheme,
);
},
);
if (textLink != null) {
textLink.submit(state.controller);
}
return null;
}
}
class QuillEditorInsertEmbedIntent extends Intent {
const QuillEditorInsertEmbedIntent(this.type);
final Attribute type;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,292 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show TextLayoutMetrics;
/// An interface for retrieving the logical text boundary
/// (left-closed-right-open)
/// at a given location in a document.
///
/// Depending on the implementation of the [QuillEditorTextBoundary], the input
/// [TextPosition] can either point to a code unit, or a position between 2 code
/// units (which can be visually represented by the caret if the selection were
/// to collapse to that position).
///
/// For example, [QuillEditorLineBreak] interprets the input [TextPosition] as a caret
/// location, since in Flutter the caret is generally painted between the
/// character the [TextPosition] points to and its previous character, and
/// [QuillEditorLineBreak] cares about the affinity of the input [TextPosition]. Most
/// other text boundaries however, interpret the input [TextPosition] as the
/// location of a code unit in the document, since it's easier to reason about
/// the text boundary given a code unit in the text.
///
/// To convert a "code-unit-based" [QuillEditorTextBoundary] to "caret-location-based",
/// use the [QuillEditorCollapsedSelectionBoundary] combinator.
abstract class QuillEditorTextBoundary {
const QuillEditorTextBoundary();
TextEditingValue get textEditingValue;
/// Returns the leading text boundary at the given location, inclusive.
TextPosition getLeadingTextBoundaryAt(TextPosition position);
/// Returns the trailing text boundary at the given location, exclusive.
TextPosition getTrailingTextBoundaryAt(TextPosition position);
TextRange getTextBoundaryAt(TextPosition position) {
return TextRange(
start: getLeadingTextBoundaryAt(position).offset,
end: getTrailingTextBoundaryAt(position).offset,
);
}
}
// ----------------------------- Text Boundaries -----------------------------
// The word modifier generally removes the word boundaries around white spaces
// (and newlines), IOW white spaces and some other punctuations are considered
// a part of the next word in the search direction.
class QuillEditorWhitespaceBoundary extends QuillEditorTextBoundary {
const QuillEditorWhitespaceBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
for (var index = position.offset; index >= 0; index -= 1) {
if (!TextLayoutMetrics.isWhitespace(
textEditingValue.text.codeUnitAt(index))) {
return TextPosition(offset: index);
}
}
return const TextPosition(offset: 0);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
for (var index = position.offset;
index < textEditingValue.text.length;
index += 1) {
if (!TextLayoutMetrics.isWhitespace(
textEditingValue.text.codeUnitAt(index))) {
return TextPosition(offset: index + 1);
}
}
return TextPosition(offset: textEditingValue.text.length);
}
}
// Most apps delete the entire grapheme when the backspace key is pressed.
// Also always put the new caret location to character boundaries to avoid
// sending malformed UTF-16 code units to the paragraph builder.
class QuillEditorCharacterBoundary extends QuillEditorTextBoundary {
const QuillEditorCharacterBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
final int endOffset =
math.min(position.offset + 1, textEditingValue.text.length);
return TextPosition(
offset:
CharacterRange.at(textEditingValue.text, position.offset, endOffset)
.stringBeforeLength,
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
final int endOffset =
math.min(position.offset + 1, textEditingValue.text.length);
final range =
CharacterRange.at(textEditingValue.text, position.offset, endOffset);
return TextPosition(
offset: textEditingValue.text.length - range.stringAfterLength,
);
}
@override
TextRange getTextBoundaryAt(TextPosition position) {
final int endOffset =
math.min(position.offset + 1, textEditingValue.text.length);
final range =
CharacterRange.at(textEditingValue.text, position.offset, endOffset);
return TextRange(
start: range.stringBeforeLength,
end: textEditingValue.text.length - range.stringAfterLength,
);
}
}
// [UAX #29](https://unicode.org/reports/tr29/) defined word boundaries.
class QuillEditorWordBoundary extends QuillEditorTextBoundary {
const QuillEditorWordBoundary(this.textLayout, this.textEditingValue);
final TextLayoutMetrics textLayout;
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getWordBoundary(position).start,
// Word boundary seems to always report downstream on many platforms.
affinity:
TextAffinity.downstream, // ignore: avoid_redundant_argument_values
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getWordBoundary(position).end,
// Word boundary seems to always report downstream on many platforms.
affinity:
TextAffinity.downstream, // ignore: avoid_redundant_argument_values
);
}
}
// The linebreaks of the current text layout. The input [TextPosition]s are
// interpreted as caret locations because [TextPainter.getLineAtOffset] is
// text-affinity-aware.
class QuillEditorLineBreak extends QuillEditorTextBoundary {
const QuillEditorLineBreak(this.textLayout, this.textEditingValue);
final TextLayoutMetrics textLayout;
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getLineAtOffset(position).start,
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textLayout.getLineAtOffset(position).end,
affinity: TextAffinity.upstream,
);
}
}
// The document boundary is unique and is a constant function of the input
// position.
class QuillEditorDocumentBoundary extends QuillEditorTextBoundary {
const QuillEditorDocumentBoundary(this.textEditingValue);
@override
final TextEditingValue textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) =>
const TextPosition(offset: 0);
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
offset: textEditingValue.text.length,
affinity: TextAffinity.upstream,
);
}
}
// ------------------------ Text Boundary Combinators ------------------------
// Expands the innerTextBoundary with outerTextBoundary.
class QuillEditorExpandedTextBoundary extends QuillEditorTextBoundary {
QuillEditorExpandedTextBoundary(
this.innerTextBoundary, this.outerTextBoundary);
final QuillEditorTextBoundary innerTextBoundary;
final QuillEditorTextBoundary outerTextBoundary;
@override
TextEditingValue get textEditingValue {
assert(innerTextBoundary.textEditingValue ==
outerTextBoundary.textEditingValue);
return innerTextBoundary.textEditingValue;
}
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return outerTextBoundary.getLeadingTextBoundaryAt(
innerTextBoundary.getLeadingTextBoundaryAt(position),
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return outerTextBoundary.getTrailingTextBoundaryAt(
innerTextBoundary.getTrailingTextBoundaryAt(position),
);
}
}
// Force the innerTextBoundary to interpret the input [TextPosition]s as caret
// locations instead of code unit positions.
//
// The innerTextBoundary must be a [_TextBoundary] that interprets the input
// [TextPosition]s as code unit positions.
class QuillEditorCollapsedSelectionBoundary extends QuillEditorTextBoundary {
QuillEditorCollapsedSelectionBoundary(this.innerTextBoundary, this.isForward);
final QuillEditorTextBoundary innerTextBoundary;
final bool isForward;
@override
TextEditingValue get textEditingValue => innerTextBoundary.textEditingValue;
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
return isForward
? innerTextBoundary.getLeadingTextBoundaryAt(position)
: position.offset <= 0
? const TextPosition(offset: 0)
: innerTextBoundary.getLeadingTextBoundaryAt(
TextPosition(offset: position.offset - 1));
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return isForward
? innerTextBoundary.getTrailingTextBoundaryAt(position)
: position.offset <= 0
? const TextPosition(offset: 0)
: innerTextBoundary.getTrailingTextBoundaryAt(
TextPosition(offset: position.offset - 1));
}
}
// A _TextBoundary that creates a [TextRange] where its start is from the
// specified leading text boundary and its end is from the specified trailing
// text boundary.
class QuillEditorMixedBoundary extends QuillEditorTextBoundary {
QuillEditorMixedBoundary(this.leadingTextBoundary, this.trailingTextBoundary);
final QuillEditorTextBoundary leadingTextBoundary;
final QuillEditorTextBoundary trailingTextBoundary;
@override
TextEditingValue get textEditingValue {
assert(leadingTextBoundary.textEditingValue ==
trailingTextBoundary.textEditingValue);
return leadingTextBoundary.textEditingValue;
}
@override
TextPosition getLeadingTextBoundaryAt(TextPosition position) =>
leadingTextBoundary.getLeadingTextBoundaryAt(position);
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) =>
trailingTextBoundary.getTrailingTextBoundaryAt(position);
}

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ViewportOffset;
import '../../models/documents/document.dart';
import '../cursor.dart';
import '../editor/editor.dart';
class QuilRawEditorMultiChildRenderObjectWidget
extends MultiChildRenderObjectWidget {
const QuilRawEditorMultiChildRenderObjectWidget({
required super.children,
required this.document,
required this.textDirection,
required this.hasFocus,
required this.scrollable,
required this.selection,
required this.startHandleLayerLink,
required this.endHandleLayerLink,
required this.onSelectionChanged,
required this.onSelectionCompleted,
required this.scrollBottomInset,
required this.cursorController,
required this.floatingCursorDisabled,
super.key,
this.padding = EdgeInsets.zero,
this.maxContentWidth,
this.offset,
});
final ViewportOffset? offset;
final Document document;
final TextDirection textDirection;
final bool hasFocus;
final bool scrollable;
final TextSelection selection;
final LayerLink startHandleLayerLink;
final LayerLink endHandleLayerLink;
final TextSelectionChangedHandler onSelectionChanged;
final TextSelectionCompletedHandler onSelectionCompleted;
final double scrollBottomInset;
final EdgeInsetsGeometry padding;
final double? maxContentWidth;
final CursorCont cursorController;
final bool floatingCursorDisabled;
@override
RenderEditor createRenderObject(BuildContext context) {
return RenderEditor(
offset: offset,
document: document,
textDirection: textDirection,
hasFocus: hasFocus,
scrollable: scrollable,
selection: selection,
startHandleLayerLink: startHandleLayerLink,
endHandleLayerLink: endHandleLayerLink,
onSelectionChanged: onSelectionChanged,
onSelectionCompleted: onSelectionCompleted,
cursorController: cursorController,
padding: padding,
maxContentWidth: maxContentWidth,
scrollBottomInset: scrollBottomInset,
floatingCursorDisabled: floatingCursorDisabled,
);
}
@override
void updateRenderObject(
BuildContext context,
covariant RenderEditor renderObject,
) {
renderObject
..offset = offset
..document = document
..setContainer(document.root)
..textDirection = textDirection
..setHasFocus(hasFocus)
..setSelection(selection)
..setStartHandleLayerLink(startHandleLayerLink)
..setEndHandleLayerLink(endHandleLayerLink)
..onSelectionChanged = onSelectionChanged
..setScrollBottomInset(scrollBottomInset)
..setPadding(padding)
..maxContentWidth = maxContentWidth;
}
}

@ -1,6 +1,6 @@
name: flutter_quill
description: A rich text editor built for the modern Android, iOS, web and desktop platforms. It is the WYSIWYG editor and a Quill component for Flutter.
version: 8.2.5
version: 8.2.6
homepage: https://1o24bbs.com/c/bulletjournal/108
repository: https://github.com/singerdmx/flutter-quill
@ -31,7 +31,6 @@ platforms:
environment:
sdk: '>=3.1.5 <4.0.0'
# sdk: ">=2.17.0 <4.0.0"
flutter: ">=3.10.0"
dependencies:
@ -51,10 +50,6 @@ dependencies:
flutter_animate: ^4.2.0+1
meta: ^1.9.1
dependency_overrides:
flutter_quill_test:
path: ./flutter_quill_test
dev_dependencies:
flutter_lints: ^3.0.1
flutter_test:

@ -0,0 +1,3 @@
dependency_overrides:
flutter_quill_test:
path: ./flutter_quill_test

@ -0,0 +1,20 @@
#!/bin/bash
echo ""
echo "Disable local development for flutter_quill:"
rm pubspec_overrides.yaml
echo ""
echo "Enable local development for flutter_quill_extensions:"
rm flutter_quill_extensions/pubspec_overrides.yaml
echo ""
echo "Enable local development for flutter_quill_test:"
rm flutter_quill_test/pubspec_overrides.yaml
echo ""
echo "Local development for all libraries has been disabled, please 'flutter pub get' for each one of them"

@ -0,0 +1,20 @@
#!/bin/bash
echo ""
echo "Enable local development for flutter_quill:"
cp pubspec_overrides.yaml.g pubspec_overrides.yaml
echo ""
echo "Enable local development for flutter_quill_extensions:"
cp flutter_quill_extensions/pubspec_overrides.yaml.g flutter_quill_extensions/pubspec_overrides.yaml
echo ""
echo "Enable local development for flutter_quill_test:"
cp flutter_quill_test/pubspec_overrides.yaml.g flutter_quill_test/pubspec_overrides.yaml
echo ""
echo "Local development for all libraries has been enabled, please 'flutter pub get' for each one of them"
Loading…
Cancel
Save