dartlangeditorflutterflutter-appsflutter-examplesflutter-packageflutter-widgetquillquill-deltaquilljsreactquillrich-textrich-text-editorwysiwygwysiwyg-editor
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
177 lines
4.6 KiB
177 lines
4.6 KiB
import '../documents/attribute.dart'; |
|
import '../documents/nodes/embeddable.dart'; |
|
import '../quill_delta.dart'; |
|
import 'rule.dart'; |
|
|
|
/// A heuristic rule for delete operations. |
|
abstract class DeleteRule extends Rule { |
|
const DeleteRule(); |
|
|
|
@override |
|
RuleType get type => RuleType.DELETE; |
|
|
|
@override |
|
void validateArgs(int? len, Object? data, Attribute? attribute) { |
|
assert(len != null); |
|
assert(data == null); |
|
assert(attribute == null); |
|
} |
|
} |
|
|
|
class EnsureLastLineBreakDeleteRule extends DeleteRule { |
|
const EnsureLastLineBreakDeleteRule(); |
|
|
|
@override |
|
Delta? applyRule(Delta document, int index, |
|
{int? len, Object? data, Attribute? attribute}) { |
|
final itr = DeltaIterator(document)..skip(index + len!); |
|
|
|
return Delta() |
|
..retain(index) |
|
..delete(itr.hasNext ? len : len - 1); |
|
} |
|
} |
|
|
|
/// Fallback rule for delete operations which simply deletes specified text |
|
/// range without any special handling. |
|
class CatchAllDeleteRule extends DeleteRule { |
|
const CatchAllDeleteRule(); |
|
|
|
@override |
|
Delta applyRule(Delta document, int index, |
|
{int? len, Object? data, Attribute? attribute}) { |
|
final itr = DeltaIterator(document)..skip(index + len!); |
|
|
|
return Delta() |
|
..retain(index) |
|
..delete(itr.hasNext ? len : len - 1); |
|
} |
|
} |
|
|
|
/// Preserves line format when user deletes the line's newline character |
|
/// effectively merging it with the next line. |
|
/// |
|
/// This rule makes sure to apply all style attributes of deleted newline |
|
/// to the next available newline, which may reset any style attributes |
|
/// already present there. |
|
class PreserveLineStyleOnMergeRule extends DeleteRule { |
|
const PreserveLineStyleOnMergeRule(); |
|
|
|
@override |
|
Delta? applyRule(Delta document, int index, |
|
{int? len, Object? data, Attribute? attribute}) { |
|
final itr = DeltaIterator(document)..skip(index); |
|
var op = itr.next(1); |
|
if (op.data != '\n') { |
|
return null; |
|
} |
|
|
|
final isNotPlain = op.isNotPlain; |
|
final attrs = op.attributes; |
|
|
|
itr.skip(len! - 1); |
|
|
|
if (!itr.hasNext) { |
|
// User attempts to delete the last newline character, prevent it. |
|
return Delta() |
|
..retain(index) |
|
..delete(len - 1); |
|
} |
|
|
|
final delta = Delta() |
|
..retain(index) |
|
..delete(len); |
|
|
|
while (itr.hasNext) { |
|
op = itr.next(); |
|
final text = op.data is String ? (op.data as String?)! : ''; |
|
final lineBreak = text.indexOf('\n'); |
|
if (lineBreak == -1) { |
|
delta.retain(op.length!); |
|
continue; |
|
} |
|
|
|
var attributes = op.attributes == null |
|
? null |
|
: op.attributes!.map<String, dynamic>( |
|
(key, dynamic value) => MapEntry<String, dynamic>(key, null)); |
|
|
|
if (isNotPlain) { |
|
attributes ??= <String, dynamic>{}; |
|
attributes.addAll(attrs!); |
|
} |
|
delta |
|
..retain(lineBreak) |
|
..retain(1, attributes); |
|
break; |
|
} |
|
return delta; |
|
} |
|
} |
|
|
|
/// This rule applies to video, not image |
|
/// |
|
/// Prevents user from merging a line containing an video embed with other |
|
/// lines, Corresponds [InsertEmbedsRule]. |
|
class EnsureEmbedLineRule extends DeleteRule { |
|
const EnsureEmbedLineRule(); |
|
|
|
@override |
|
Delta? applyRule(Delta document, int index, |
|
{int? len, Object? data, Attribute? attribute}) { |
|
final itr = DeltaIterator(document); |
|
|
|
var op = itr.skip(index); |
|
final opAfter = itr.skip(index + 1); |
|
|
|
//Only video embed need can't merging a line. |
|
if (!_isVideo(op) || !_isVideo(opAfter)) { |
|
return null; |
|
} |
|
|
|
int? indexDelta = 0, lengthDelta = 0, remain = len; |
|
var embedFound = op != null && op.data is! String; |
|
final hasLineBreakBefore = |
|
!embedFound && (op == null || (op.data as String).endsWith('\n')); |
|
if (embedFound) { |
|
var candidate = itr.next(1); |
|
if (remain != null) { |
|
remain--; |
|
if (candidate.data == '\n') { |
|
indexDelta++; |
|
lengthDelta--; |
|
|
|
candidate = itr.next(1); |
|
remain--; |
|
if (candidate.data == '\n') { |
|
lengthDelta++; |
|
} |
|
} |
|
} |
|
} |
|
|
|
op = itr.skip(remain!); |
|
if (op != null && |
|
(op.data is String ? op.data as String? : '')!.endsWith('\n')) { |
|
final candidate = itr.next(1); |
|
if (candidate.data is! String && !hasLineBreakBefore) { |
|
embedFound = true; |
|
lengthDelta--; |
|
} |
|
} |
|
|
|
if (!embedFound) { |
|
return null; |
|
} |
|
|
|
return Delta() |
|
..retain(index + indexDelta) |
|
..delete(len! + lengthDelta); |
|
} |
|
|
|
bool _isVideo(op) { |
|
return op != null && |
|
op.data is! String && |
|
!(op.data as Map).containsKey(BlockEmbed.videoType); |
|
} |
|
}
|
|
|