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.
736 lines
22 KiB
736 lines
22 KiB
import 'dart:async'; |
|
import 'dart:convert'; |
|
|
|
import 'package:flutter/cupertino.dart'; |
|
import 'package:flutter/foundation.dart'; |
|
import 'package:flutter/gestures.dart'; |
|
import 'package:flutter/material.dart'; |
|
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:tuple/tuple.dart'; |
|
|
|
import '../models/documents/attribute.dart'; |
|
import '../models/documents/document.dart'; |
|
import '../models/documents/nodes/block.dart'; |
|
import '../models/documents/nodes/line.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 'raw_editor/raw_editor_state_keyboard_mixin.dart'; |
|
import 'raw_editor/raw_editor_state_selection_delegate_mixin.dart'; |
|
import 'raw_editor/raw_editor_state_text_input_client_mixin.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.scrollBottomInset, |
|
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; |
|
final bool scrollable; |
|
final double scrollBottomInset; |
|
final EdgeInsetsGeometry padding; |
|
final bool readOnly; |
|
final String? placeholder; |
|
final ValueChanged<String>? onLaunchUrl; |
|
final ToolbarOptions toolbarOptions; |
|
final bool showSelectionHandles; |
|
final bool showCursor; |
|
final CursorStyle cursorStyle; |
|
final TextCapitalization textCapitalization; |
|
final double? maxHeight; |
|
final double? minHeight; |
|
final DefaultStyles? customStyles; |
|
final bool expands; |
|
final bool autoFocus; |
|
final Color selectionColor; |
|
final TextSelectionControls selectionCtrls; |
|
final Brightness keyboardAppearance; |
|
final bool enableInteractiveSelection; |
|
final ScrollPhysics? scrollPhysics; |
|
final EmbedBuilder embedBuilder; |
|
|
|
@override |
|
State<StatefulWidget> createState() => RawEditorState(); |
|
} |
|
|
|
class RawEditorState extends EditorState |
|
with |
|
AutomaticKeepAliveClientMixin<RawEditor>, |
|
WidgetsBindingObserver, |
|
TickerProviderStateMixin<RawEditor>, |
|
RawEditorStateKeyboardMixin, |
|
RawEditorStateTextInputClientMixin, |
|
RawEditorStateSelectionDelegateMixin { |
|
final GlobalKey _editorKey = GlobalKey(); |
|
|
|
// Keyboard |
|
late KeyboardListener _keyboardListener; |
|
KeyboardVisibilityController? _keyboardVisibilityController; |
|
StreamSubscription<bool>? _keyboardVisibilitySubscription; |
|
bool _keyboardVisible = false; |
|
|
|
// Selection overlay |
|
@override |
|
EditorTextSelectionOverlay? getSelectionOverlay() => _selectionOverlay; |
|
EditorTextSelectionOverlay? _selectionOverlay; |
|
|
|
ScrollController? _scrollController; |
|
|
|
late CursorCont _cursorCont; |
|
|
|
// Focus |
|
bool _didAutoFocus = false; |
|
FocusAttachment? _focusAttachment; |
|
bool get _hasFocus => widget.focusNode.hasFocus; |
|
|
|
DefaultStyles? _styles; |
|
|
|
final ClipboardStatusNotifier? _clipboardStatus = |
|
kIsWeb ? null : ClipboardStatusNotifier(); |
|
final LayerLink _toolbarLayerLink = LayerLink(); |
|
final LayerLink _startHandleLayerLink = LayerLink(); |
|
final LayerLink _endHandleLayerLink = LayerLink(); |
|
|
|
TextDirection get _textDirection => Directionality.of(context); |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
assert(debugCheckHasMediaQuery(context)); |
|
_focusAttachment!.reparent(); |
|
super.build(context); |
|
|
|
var _doc = widget.controller.document; |
|
if (_doc.isEmpty() && |
|
!widget.focusNode.hasFocus && |
|
widget.placeholder != null) { |
|
_doc = Document.fromJson(jsonDecode( |
|
'[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); |
|
} |
|
|
|
Widget child = CompositedTransformTarget( |
|
link: _toolbarLayerLink, |
|
child: Semantics( |
|
child: _Editor( |
|
key: _editorKey, |
|
document: _doc, |
|
selection: widget.controller.selection, |
|
hasFocus: _hasFocus, |
|
textDirection: _textDirection, |
|
startHandleLayerLink: _startHandleLayerLink, |
|
endHandleLayerLink: _endHandleLayerLink, |
|
onSelectionChanged: _handleSelectionChanged, |
|
scrollBottomInset: widget.scrollBottomInset, |
|
padding: widget.padding, |
|
children: _buildChildren(_doc, context), |
|
), |
|
), |
|
); |
|
|
|
if (widget.scrollable) { |
|
final baselinePadding = |
|
EdgeInsets.only(top: _styles!.paragraph!.verticalSpacing.item1); |
|
child = BaselineProxy( |
|
textStyle: _styles!.paragraph!.style, |
|
padding: baselinePadding, |
|
child: SingleChildScrollView( |
|
controller: _scrollController, |
|
physics: widget.scrollPhysics, |
|
child: child, |
|
), |
|
); |
|
} |
|
|
|
final constraints = widget.expands |
|
? const BoxConstraints.expand() |
|
: BoxConstraints( |
|
minHeight: widget.minHeight ?? 0.0, |
|
maxHeight: widget.maxHeight ?? double.infinity); |
|
|
|
return QuillStyles( |
|
data: _styles!, |
|
child: MouseRegion( |
|
cursor: SystemMouseCursors.text, |
|
child: Container( |
|
constraints: constraints, |
|
child: child, |
|
), |
|
), |
|
); |
|
} |
|
|
|
void _handleSelectionChanged( |
|
TextSelection selection, SelectionChangedCause cause) { |
|
widget.controller.updateSelection(selection, ChangeSource.LOCAL); |
|
|
|
_selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); |
|
|
|
if (!_keyboardVisible) { |
|
requestKeyboard(); |
|
} |
|
} |
|
|
|
/// Updates the checkbox positioned at [offset] in document |
|
/// by changing its attribute according to [value]. |
|
void _handleCheckboxTap(int offset, bool value) { |
|
if (!widget.readOnly) { |
|
if (value) { |
|
widget.controller.formatText(offset, 0, Attribute.checked); |
|
} else { |
|
widget.controller.formatText(offset, 0, Attribute.unchecked); |
|
} |
|
} |
|
} |
|
|
|
List<Widget> _buildChildren(Document doc, BuildContext context) { |
|
final result = <Widget>[]; |
|
final indentLevelCounts = <int, int>{}; |
|
for (final node in doc.root.children) { |
|
if (node is Line) { |
|
final editableTextLine = _getEditableTextLineFromNode(node, context); |
|
result.add(editableTextLine); |
|
} else if (node is Block) { |
|
final attrs = node.style.attributes; |
|
final editableTextBlock = EditableTextBlock( |
|
node, |
|
_textDirection, |
|
widget.scrollBottomInset, |
|
_getVerticalSpacingForBlock(node, _styles), |
|
widget.controller.selection, |
|
widget.selectionColor, |
|
_styles, |
|
widget.enableInteractiveSelection, |
|
_hasFocus, |
|
attrs.containsKey(Attribute.codeBlock.key) |
|
? const EdgeInsets.all(16) |
|
: null, |
|
widget.embedBuilder, |
|
_cursorCont, |
|
indentLevelCounts, |
|
_handleCheckboxTap, |
|
); |
|
result.add(editableTextBlock); |
|
} else { |
|
throw StateError('Unreachable.'); |
|
} |
|
} |
|
return result; |
|
} |
|
|
|
EditableTextLine _getEditableTextLineFromNode( |
|
Line node, BuildContext context) { |
|
final textLine = TextLine( |
|
line: node, |
|
textDirection: _textDirection, |
|
embedBuilder: widget.embedBuilder, |
|
styles: _styles!, |
|
); |
|
final editableTextLine = EditableTextLine( |
|
node, |
|
null, |
|
textLine, |
|
0, |
|
_getVerticalSpacingForLine(node, _styles), |
|
_textDirection, |
|
widget.controller.selection, |
|
widget.selectionColor, |
|
widget.enableInteractiveSelection, |
|
_hasFocus, |
|
MediaQuery.of(context).devicePixelRatio, |
|
_cursorCont); |
|
return editableTextLine; |
|
} |
|
|
|
Tuple2<double, double> _getVerticalSpacingForLine( |
|
Line line, DefaultStyles? defaultStyles) { |
|
final attrs = line.style.attributes; |
|
if (attrs.containsKey(Attribute.header.key)) { |
|
final int? level = attrs[Attribute.header.key]!.value; |
|
switch (level) { |
|
case 1: |
|
return defaultStyles!.h1!.verticalSpacing; |
|
case 2: |
|
return defaultStyles!.h2!.verticalSpacing; |
|
case 3: |
|
return defaultStyles!.h3!.verticalSpacing; |
|
default: |
|
throw 'Invalid level $level'; |
|
} |
|
} |
|
|
|
return defaultStyles!.paragraph!.verticalSpacing; |
|
} |
|
|
|
Tuple2<double, double> _getVerticalSpacingForBlock( |
|
Block node, DefaultStyles? defaultStyles) { |
|
final attrs = node.style.attributes; |
|
if (attrs.containsKey(Attribute.blockQuote.key)) { |
|
return defaultStyles!.quote!.verticalSpacing; |
|
} else if (attrs.containsKey(Attribute.codeBlock.key)) { |
|
return defaultStyles!.code!.verticalSpacing; |
|
} else if (attrs.containsKey(Attribute.indent.key)) { |
|
return defaultStyles!.indent!.verticalSpacing; |
|
} |
|
return defaultStyles!.lists!.verticalSpacing; |
|
} |
|
|
|
@override |
|
void initState() { |
|
super.initState(); |
|
|
|
_clipboardStatus?.addListener(_onChangedClipboardStatus); |
|
|
|
widget.controller.addListener(() { |
|
_didChangeTextEditingValue(widget.controller.ignoreFocusOnTextChange); |
|
}); |
|
|
|
_scrollController = widget.scrollController; |
|
_scrollController!.addListener(_updateSelectionOverlayForScroll); |
|
|
|
_cursorCont = CursorCont( |
|
show: ValueNotifier<bool>(widget.showCursor), |
|
style: widget.cursorStyle, |
|
tickerProvider: this, |
|
); |
|
|
|
_keyboardListener = KeyboardListener( |
|
handleCursorMovement, |
|
handleShortcut, |
|
handleDelete, |
|
); |
|
|
|
if (defaultTargetPlatform == TargetPlatform.windows || |
|
defaultTargetPlatform == TargetPlatform.macOS || |
|
defaultTargetPlatform == TargetPlatform.linux || |
|
defaultTargetPlatform == TargetPlatform.fuchsia) { |
|
_keyboardVisible = true; |
|
} else { |
|
_keyboardVisibilityController = KeyboardVisibilityController(); |
|
_keyboardVisible = _keyboardVisibilityController!.isVisible; |
|
_keyboardVisibilitySubscription = |
|
_keyboardVisibilityController?.onChange.listen((visible) { |
|
_keyboardVisible = visible; |
|
if (visible) { |
|
_onChangeTextEditingValue(); |
|
} |
|
}); |
|
} |
|
|
|
_focusAttachment = widget.focusNode.attach(context, |
|
onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); |
|
widget.focusNode.addListener(_handleFocusChanged); |
|
} |
|
|
|
@override |
|
void didChangeDependencies() { |
|
super.didChangeDependencies(); |
|
final parentStyles = QuillStyles.getStyles(context, true); |
|
final defaultStyles = DefaultStyles.getInstance(context); |
|
_styles = (parentStyles != null) |
|
? defaultStyles.merge(parentStyles) |
|
: defaultStyles; |
|
|
|
if (widget.customStyles != null) { |
|
_styles = _styles!.merge(widget.customStyles!); |
|
} |
|
|
|
if (!_didAutoFocus && widget.autoFocus) { |
|
FocusScope.of(context).autofocus(widget.focusNode); |
|
_didAutoFocus = true; |
|
} |
|
} |
|
|
|
@override |
|
void didUpdateWidget(RawEditor oldWidget) { |
|
super.didUpdateWidget(oldWidget); |
|
|
|
_cursorCont.show.value = widget.showCursor; |
|
_cursorCont.style = widget.cursorStyle; |
|
|
|
if (widget.controller != oldWidget.controller) { |
|
oldWidget.controller.removeListener(_didChangeTextEditingValue); |
|
widget.controller.addListener(_didChangeTextEditingValue); |
|
updateRemoteValueIfNeeded(); |
|
} |
|
|
|
if (widget.scrollController != _scrollController) { |
|
_scrollController!.removeListener(_updateSelectionOverlayForScroll); |
|
_scrollController = widget.scrollController; |
|
_scrollController!.addListener(_updateSelectionOverlayForScroll); |
|
} |
|
|
|
if (widget.focusNode != oldWidget.focusNode) { |
|
oldWidget.focusNode.removeListener(_handleFocusChanged); |
|
_focusAttachment?.detach(); |
|
_focusAttachment = widget.focusNode.attach(context, |
|
onKey: (node, event) => _keyboardListener.handleRawKeyEvent(event)); |
|
widget.focusNode.addListener(_handleFocusChanged); |
|
updateKeepAlive(); |
|
} |
|
|
|
if (widget.controller.selection != oldWidget.controller.selection) { |
|
_selectionOverlay?.update(textEditingValue); |
|
} |
|
|
|
_selectionOverlay?.handlesVisible = _shouldShowSelectionHandles(); |
|
if (!shouldCreateInputConnection) { |
|
closeConnectionIfNeeded(); |
|
} else { |
|
if (oldWidget.readOnly && _hasFocus) { |
|
openConnectionIfNeeded(); |
|
} |
|
} |
|
} |
|
|
|
bool _shouldShowSelectionHandles() { |
|
return widget.showSelectionHandles && |
|
!widget.controller.selection.isCollapsed; |
|
} |
|
|
|
@override |
|
void dispose() { |
|
closeConnectionIfNeeded(); |
|
_keyboardVisibilitySubscription?.cancel(); |
|
assert(!hasConnection); |
|
_selectionOverlay?.dispose(); |
|
_selectionOverlay = null; |
|
widget.controller.removeListener(_didChangeTextEditingValue); |
|
widget.focusNode.removeListener(_handleFocusChanged); |
|
_focusAttachment!.detach(); |
|
_cursorCont.dispose(); |
|
_clipboardStatus?.removeListener(_onChangedClipboardStatus); |
|
_clipboardStatus?.dispose(); |
|
super.dispose(); |
|
} |
|
|
|
void _updateSelectionOverlayForScroll() { |
|
_selectionOverlay?.markNeedsBuild(); |
|
} |
|
|
|
void _didChangeTextEditingValue([bool ignoreFocus = false]) { |
|
if (kIsWeb) { |
|
_onChangeTextEditingValue(ignoreFocus); |
|
if (!ignoreFocus) { |
|
requestKeyboard(); |
|
} |
|
return; |
|
} |
|
|
|
if (ignoreFocus || _keyboardVisible) { |
|
_onChangeTextEditingValue(ignoreFocus); |
|
} else { |
|
requestKeyboard(); |
|
if (mounted) { |
|
setState(() { |
|
// Use widget.controller.value in build() |
|
// Trigger build and updateChildren |
|
}); |
|
} |
|
} |
|
} |
|
|
|
void _onChangeTextEditingValue([bool ignoreCaret = false]) { |
|
updateRemoteValueIfNeeded(); |
|
if (ignoreCaret) { |
|
return; |
|
} |
|
_showCaretOnScreen(); |
|
_cursorCont.startOrStopCursorTimerIfNeeded( |
|
_hasFocus, widget.controller.selection); |
|
if (hasConnection) { |
|
_cursorCont |
|
..stopCursorTimer(resetCharTicks: false) |
|
..startCursorTimer(); |
|
} |
|
|
|
SchedulerBinding.instance!.addPostFrameCallback( |
|
(_) => _updateOrDisposeSelectionOverlayIfNeeded()); |
|
if (mounted) { |
|
setState(() { |
|
// Use widget.controller.value in build() |
|
// Trigger build and updateChildren |
|
}); |
|
} |
|
} |
|
|
|
void _updateOrDisposeSelectionOverlayIfNeeded() { |
|
if (_selectionOverlay != null) { |
|
if (_hasFocus) { |
|
_selectionOverlay!.update(textEditingValue); |
|
} else { |
|
_selectionOverlay!.dispose(); |
|
_selectionOverlay = null; |
|
} |
|
} else if (_hasFocus) { |
|
_selectionOverlay?.hide(); |
|
_selectionOverlay = null; |
|
|
|
_selectionOverlay = EditorTextSelectionOverlay( |
|
textEditingValue, |
|
false, |
|
context, |
|
widget, |
|
_toolbarLayerLink, |
|
_startHandleLayerLink, |
|
_endHandleLayerLink, |
|
getRenderEditor(), |
|
widget.selectionCtrls, |
|
this, |
|
DragStartBehavior.start, |
|
null, |
|
_clipboardStatus!, |
|
); |
|
_selectionOverlay!.handlesVisible = _shouldShowSelectionHandles(); |
|
_selectionOverlay!.showHandles(); |
|
} |
|
} |
|
|
|
void _handleFocusChanged() { |
|
openOrCloseConnection(); |
|
_cursorCont.startOrStopCursorTimerIfNeeded( |
|
_hasFocus, widget.controller.selection); |
|
_updateOrDisposeSelectionOverlayIfNeeded(); |
|
if (_hasFocus) { |
|
WidgetsBinding.instance!.addObserver(this); |
|
_showCaretOnScreen(); |
|
} else { |
|
WidgetsBinding.instance!.removeObserver(this); |
|
} |
|
updateKeepAlive(); |
|
} |
|
|
|
void _onChangedClipboardStatus() { |
|
if (!mounted) return; |
|
setState(() { |
|
// Inform the widget that the value of clipboardStatus has changed. |
|
// Trigger build and updateChildren |
|
}); |
|
} |
|
|
|
bool _showCaretOnScreenScheduled = false; |
|
|
|
void _showCaretOnScreen() { |
|
if (!widget.showCursor || _showCaretOnScreenScheduled) { |
|
return; |
|
} |
|
|
|
_showCaretOnScreenScheduled = true; |
|
SchedulerBinding.instance!.addPostFrameCallback((_) { |
|
if (widget.scrollable) { |
|
_showCaretOnScreenScheduled = false; |
|
|
|
final viewport = RenderAbstractViewport.of(getRenderEditor()); |
|
|
|
final editorOffset = getRenderEditor()! |
|
.localToGlobal(const Offset(0, 0), ancestor: viewport); |
|
final offsetInViewport = _scrollController!.offset + editorOffset.dy; |
|
|
|
final offset = getRenderEditor()!.getOffsetToRevealCursor( |
|
_scrollController!.position.viewportDimension, |
|
_scrollController!.offset, |
|
offsetInViewport, |
|
); |
|
|
|
if (offset != null) { |
|
_scrollController!.animateTo( |
|
offset, |
|
duration: const Duration(milliseconds: 100), |
|
curve: Curves.fastOutSlowIn, |
|
); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
@override |
|
RenderEditor? getRenderEditor() { |
|
return _editorKey.currentContext!.findRenderObject() as RenderEditor?; |
|
} |
|
|
|
@override |
|
TextEditingValue getTextEditingValue() { |
|
return widget.controller.plainTextEditingValue; |
|
} |
|
|
|
@override |
|
void requestKeyboard() { |
|
if (_hasFocus) { |
|
openConnectionIfNeeded(); |
|
} else { |
|
widget.focusNode.requestFocus(); |
|
} |
|
} |
|
|
|
@override |
|
void setTextEditingValue(TextEditingValue value) { |
|
if (value.text == textEditingValue.text) { |
|
widget.controller.updateSelection(value.selection, ChangeSource.LOCAL); |
|
} else { |
|
__setEditingValue(value); |
|
} |
|
} |
|
|
|
Future<void> __setEditingValue(TextEditingValue value) async { |
|
if (await __isItCut(value)) { |
|
widget.controller.replaceText( |
|
textEditingValue.selection.start, |
|
textEditingValue.text.length - value.text.length, |
|
'', |
|
value.selection, |
|
); |
|
} else { |
|
final value = textEditingValue; |
|
final data = await Clipboard.getData(Clipboard.kTextPlain); |
|
if (data != null) { |
|
final length = |
|
textEditingValue.selection.end - textEditingValue.selection.start; |
|
widget.controller.replaceText( |
|
value.selection.start, |
|
length, |
|
data.text, |
|
value.selection, |
|
); |
|
// move cursor to the end of pasted text selection |
|
widget.controller.updateSelection( |
|
TextSelection.collapsed( |
|
offset: value.selection.start + data.text!.length), |
|
ChangeSource.LOCAL); |
|
} |
|
} |
|
} |
|
|
|
Future<bool> __isItCut(TextEditingValue value) async { |
|
final data = await Clipboard.getData(Clipboard.kTextPlain); |
|
if (data == null) { |
|
return false; |
|
} |
|
return textEditingValue.text.length - value.text.length == |
|
data.text!.length; |
|
} |
|
|
|
@override |
|
bool showToolbar() { |
|
// Web is using native dom elements to enable clipboard functionality of the |
|
// toolbar: copy, paste, select, cut. It might also provide additional |
|
// functionality depending on the browser (such as translate). Due to this |
|
// we should not show a Flutter toolbar for the editable text elements. |
|
if (kIsWeb) { |
|
return false; |
|
} |
|
if (_selectionOverlay == null || _selectionOverlay!.toolbar != null) { |
|
return false; |
|
} |
|
|
|
_selectionOverlay!.update(textEditingValue); |
|
_selectionOverlay!.showToolbar(); |
|
return true; |
|
} |
|
|
|
@override |
|
bool get wantKeepAlive => widget.focusNode.hasFocus; |
|
|
|
@override |
|
void userUpdateTextEditingValue( |
|
TextEditingValue value, SelectionChangedCause cause) { |
|
// TODO: implement userUpdateTextEditingValue |
|
} |
|
} |
|
|
|
class _Editor extends MultiChildRenderObjectWidget { |
|
_Editor({ |
|
required Key key, |
|
required List<Widget> children, |
|
required this.document, |
|
required this.textDirection, |
|
required this.hasFocus, |
|
required this.selection, |
|
required this.startHandleLayerLink, |
|
required this.endHandleLayerLink, |
|
required this.onSelectionChanged, |
|
required this.scrollBottomInset, |
|
this.padding = EdgeInsets.zero, |
|
}) : super(key: key, children: children); |
|
|
|
final Document document; |
|
final TextDirection textDirection; |
|
final bool hasFocus; |
|
final TextSelection selection; |
|
final LayerLink startHandleLayerLink; |
|
final LayerLink endHandleLayerLink; |
|
final TextSelectionChangedHandler onSelectionChanged; |
|
final double scrollBottomInset; |
|
final EdgeInsetsGeometry padding; |
|
|
|
@override |
|
RenderEditor createRenderObject(BuildContext context) { |
|
return RenderEditor( |
|
null, |
|
textDirection, |
|
scrollBottomInset, |
|
padding, |
|
document, |
|
selection, |
|
hasFocus, |
|
onSelectionChanged, |
|
startHandleLayerLink, |
|
endHandleLayerLink, |
|
const EdgeInsets.fromLTRB(4, 4, 4, 5), |
|
); |
|
} |
|
|
|
@override |
|
void updateRenderObject( |
|
BuildContext context, covariant RenderEditor renderObject) { |
|
renderObject |
|
..document = document |
|
..setContainer(document.root) |
|
..textDirection = textDirection |
|
..setHasFocus(hasFocus) |
|
..setSelection(selection) |
|
..setStartHandleLayerLink(startHandleLayerLink) |
|
..setEndHandleLayerLink(endHandleLayerLink) |
|
..onSelectionChanged = onSelectionChanged |
|
..setScrollBottomInset(scrollBottomInset) |
|
..setPadding(padding); |
|
} |
|
}
|
|
|