Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add onDetectionFinished #63

Merged
merged 1 commit into from
Dec 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ class _MyAppState extends State<MyApp> {
onDetectionTyped: (text) {
print(text);
},
onDetectionFinished: () {
print("finished");
},
),
// TextField(),
],
Expand Down
36 changes: 20 additions & 16 deletions lib/composer/composer.dart
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hashtagable/decorator/decorator.dart' as decorator;
import 'package:hashtagable/detector/detector.dart';

/// Add composing to hashtag decorated text.
///
/// Expected to be used when Japanese letters are typed.
class Composer {
Composer({
@required this.decorations,
@required this.composing,
@required this.sourceText,
@required this.onDetectionTyped,
@required this.detections,
@required this.composing,
@required this.selection,
@required this.decoratedStyle,
@required this.onDetectionTyped,
});

final List<decorator.Decoration> decorations;
final TextRange composing;
final String sourceText;
final ValueChanged<String> onDetectionTyped;
final List<Detection> detections;
final TextRange composing;
final int selection;
final TextStyle decoratedStyle;
final ValueChanged<String> onDetectionTyped;

// TODO(Takahashi): Add test code for composing
TextSpan getComposedTextSpan() {
final span = decorations.map(
final span = detections.map(
(item) {
final spanRange = item.range;
final spanStyle = item.style;
Expand Down Expand Up @@ -85,18 +85,22 @@ class Composer {
return TextSpan(children: span);
}

void callOnDetectionTyped() {
final typingDecoration = decorations.firstWhere(
(decoration) =>
decoration.style == decoratedStyle &&
decoration.range.start <= selection &&
decoration.range.end >= selection,
Detection typingDetection() {
return detections.firstWhere(
(detection) =>
detection.style == decoratedStyle &&
detection.range.start <= selection &&
detection.range.end >= selection,
orElse: () {
return null;
},
);
if (typingDecoration != null) {
onDetectionTyped(typingDecoration.range.textInside(sourceText));
}

void callOnDetectionTyped() {
final typingRange = typingDetection()?.range;
if (typingRange != null) {
onDetectionTyped(typingRange.textInside(sourceText));
}
}
}
48 changes: 24 additions & 24 deletions lib/decorator/decorator.dart → lib/detector/detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,57 @@ import 'package:flutter/cupertino.dart';

import 'hashtag_regular_expression.dart';

/// DataModel to explain the unit of word in decoration system
class Decoration extends Comparable<Decoration> {
Decoration({@required this.range, this.style, this.emojiStartPoint});
/// DataModel to explain the unit of word in detection system
class Detection extends Comparable<Detection> {
Detection({@required this.range, this.style, this.emojiStartPoint});

final TextRange range;
final TextStyle style;
final int emojiStartPoint;

@override
int compareTo(Decoration other) {
int compareTo(Detection other) {
return range.start.compareTo(other.range.start);
}
}

/// Hold functions to decorate tagged text
///
/// Return the list of [Decoration] in [getDecorations]
class Decorator {
/// Return the list of [Detection] in [getDetections]
class Detector {
final TextStyle textStyle;
final TextStyle decoratedStyle;
final bool decorateAtSign;

Decorator({this.textStyle, this.decoratedStyle, this.decorateAtSign = false});
Detector({this.textStyle, this.decoratedStyle, this.decorateAtSign = false});

List<Decoration> _getSourceDecorations(
List<Detection> _getSourceDetections(
List<RegExpMatch> tags, String copiedText) {
TextRange previousItem;
final result = List<Decoration>();
final result = List<Detection>();
for (var tag in tags) {
///Add untagged content
if (previousItem == null) {
if (tag.start > 0) {
result.add(Decoration(
result.add(Detection(
range: TextRange(start: 0, end: tag.start), style: textStyle));
}
} else {
result.add(Decoration(
result.add(Detection(
range: TextRange(start: previousItem.end, end: tag.start),
style: textStyle));
}

///Add tagged content
result.add(Decoration(
result.add(Detection(
range: TextRange(start: tag.start, end: tag.end),
style: decoratedStyle));
previousItem = TextRange(start: tag.start, end: tag.end);
}

///Add remaining untagged content
if (result.last.range.end < copiedText.length) {
result.add(Decoration(
result.add(Detection(
range:
TextRange(start: result.last.range.end, end: copiedText.length),
style: textStyle));
Expand All @@ -61,17 +61,17 @@ class Decorator {
}

///Decorate tagged content, filter out the ones includes emoji.
List<Decoration> _getEmojiFilteredDecorations(
{List<Decoration> source,
List<Detection> _getEmojiFilteredDetections(
{List<Detection> source,
String copiedText,
List<RegExpMatch> emojiMatches}) {
final result = List<Decoration>();
final result = List<Detection>();
for (var item in source) {
int emojiStartPoint;
for (var emojiMatch in emojiMatches) {
final decorationContainsEmoji = (item.range.start < emojiMatch.start &&
final detectionContainsEmoji = (item.range.start < emojiMatch.start &&
emojiMatch.end <= item.range.end);
if (decorationContainsEmoji) {
if (detectionContainsEmoji) {
/// If the current Emoji's range.start is the smallest in the tag, update emojiStartPoint
emojiStartPoint = (emojiStartPoint != null)
? ((emojiMatch.start < emojiStartPoint)
Expand All @@ -81,11 +81,11 @@ class Decorator {
}
}
if (item.style == decoratedStyle && emojiStartPoint != null) {
result.add(Decoration(
result.add(Detection(
range: TextRange(start: item.range.start, end: emojiStartPoint),
style: decoratedStyle,
));
result.add(Decoration(
result.add(Detection(
range: TextRange(start: emojiStartPoint, end: item.range.end),
style: textStyle));
} else {
Expand All @@ -96,7 +96,7 @@ class Decorator {
}

/// Return the list of decorations with tagged and untagged text
List<Decoration> getDecorations(String copiedText) {
List<Detection> getDetections(String copiedText) {
/// Text to change emoji into replacement text
final fullWidthRegExp = RegExp(
r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])');
Expand Down Expand Up @@ -125,12 +125,12 @@ class Decorator {
return [];
}

final sourceDecorations = _getSourceDecorations(tags, copiedText);
final sourceDetections = _getSourceDetections(tags, copiedText);

final emojiFilteredResult = _getEmojiFilteredDecorations(
final emojiFilteredResult = _getEmojiFilteredDetections(
copiedText: copiedText,
emojiMatches: emojiMatches,
source: sourceDecorations);
source: sourceDetections);

return emojiFilteredResult;
}
Expand Down
26 changes: 13 additions & 13 deletions lib/functions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,32 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:hashtagable/widgets/hashtag_text.dart';

import 'decorator/decorator.dart';
import 'detector/detector.dart';

/// Check if the text has hashTags
bool hasHashTags(String value) {
final decoratedTextColor = Colors.blue;
final decorator = Decorator(
final detector = Detector(
textStyle: TextStyle(),
decoratedStyle: TextStyle(color: decoratedTextColor));
final result = decorator.getDecorations(value);
final taggedDecorations = result
.where((decoration) => decoration.style.color == decoratedTextColor)
final result = detector.getDetections(value);
final detections = result
.where((detection) => detection.style.color == decoratedTextColor)
.toList();
return taggedDecorations.isNotEmpty;
return detections.isNotEmpty;
}

/// Extract hashTags from the text
List<String> extractHashTags(String value) {
final decoratedTextColor = Colors.blue;
final decorator = Decorator(
final detector = Detector(
textStyle: TextStyle(),
decoratedStyle: TextStyle(color: decoratedTextColor));
final decorations = decorator.getDecorations(value);
final taggedDecorations = decorations
.where((decoration) => decoration.style.color == decoratedTextColor)
final detections = detector.getDetections(value);
final taggedDetections = detections
.where((detection) => detection.style.color == decoratedTextColor)
.toList();
final result = taggedDecorations.map((decoration) {
final result = taggedDetections.map((decoration) {
final text = decoration.range.textInside(value);
return text.trim();
}).toList();
Expand All @@ -44,11 +44,11 @@ TextSpan getHashTagTextSpan({
Function(String) onTap,
bool decorateAtSign = false,
}) {
final decorations = Decorator(
final decorations = Detector(
decoratedStyle: decoratedStyle,
textStyle: basicStyle,
decorateAtSign: decorateAtSign)
.getDecorations(source);
.getDetections(source);
if (decorations.isEmpty) {
return TextSpan(text: source, style: basicStyle);
} else {
Expand Down
4 changes: 2 additions & 2 deletions lib/hashtagable.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
library hashtagable;

export 'decorator/decorator.dart';
export 'decorator/hashtag_regular_expression.dart';
export 'detector/detector.dart';
export 'detector/hashtag_regular_expression.dart';
export 'functions.dart';
export 'widgets/hashtag_text.dart';
export 'widgets/hashtag_text_field.dart';
46 changes: 28 additions & 18 deletions lib/widgets/hashtag_editable_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hashtagable/composer/composer.dart';
import 'package:hashtagable/decorator/decorator.dart';
import 'package:hashtagable/detector/detector.dart';

/// Show decorated tagged text while user is inputting text.
///
Expand All @@ -22,6 +22,7 @@ class HashTagEditableText extends EditableText {
@required this.decoratedStyle,
@required Color cursorColor,
this.onDetectionTyped,
this.onDetectionFinished,
this.decorateAtSign,
ValueChanged<String> onChanged,
ValueChanged<String> onSubmitted,
Expand Down Expand Up @@ -128,6 +129,8 @@ class HashTagEditableText extends EditableText {

final ValueChanged<String> onDetectionTyped;

final VoidCallback onDetectionFinished;

final TextStyle decoratedStyle;

final decorateAtSign;
Expand All @@ -138,44 +141,51 @@ class HashTagEditableText extends EditableText {

/// State of [HashTagEditableText]
///
/// Return decorated tagged text by using functions in [Decorator]
/// Return decorated tagged text by using functions in [Detector]
class HashTagEditableTextState extends EditableTextState {
@override
HashTagEditableText get widget => super.widget;

Decorator decorator;
Detector detector;

Detection prevTypingDetection;

@override
void initState() {
decorator = Decorator(
super.initState();
detector = Detector(
textStyle: widget.style,
decoratedStyle: widget.decoratedStyle,
decorateAtSign: widget.decorateAtSign);
super.initState();
}

@override
TextSpan buildTextSpan() {
final String sourceText = textEditingValue.text;
final decorations = decorator.getDecorations(sourceText);
if (decorations.isEmpty) {
final detections = detector.getDetections(textEditingValue.text);
final composer = Composer(
selection: textEditingValue?.selection?.start ?? -1,
onDetectionTyped: widget.onDetectionTyped,
sourceText: textEditingValue.text,
decoratedStyle: widget.decoratedStyle,
detections: detections,
composing: textEditingValue.composing,
);

final typingDetection = composer.typingDetection();
if (prevTypingDetection != null && typingDetection == null) {
widget.onDetectionFinished?.call();
}

prevTypingDetection = typingDetection;
if (detections.isEmpty) {
/// use same method as default textField to show composing underline
return widget.controller.buildTextSpan(
style: widget.style,
withComposing: !widget.readOnly,
);
} else {
/// use [Composer] to show composing underline
decorations.sort();
final composing = textEditingValue.composing;
final composer = Composer(
selection: textEditingValue?.selection?.start ?? -1,
onDetectionTyped: widget.onDetectionTyped,
sourceText: sourceText,
decorations: decorations,
composing: composing,
decoratedStyle: widget.decoratedStyle,
);
detections.sort();
if (widget.onDetectionTyped != null) {
composer.callOnDetectionTyped();
}
Expand Down
Loading