Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
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
85 changes: 63 additions & 22 deletions lib/web_ui/lib/src/engine/text_editing/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ part of engine;
/// Make the content editable span visible to facilitate debugging.
const bool _debugVisibleTextEditing = false;

/// The `keyCode` of the "Enter" key.
const int _kReturnKeyCode = 13;

void _emptyCallback(dynamic _) {}

/// These style attributes are constant throughout the life time of an input
Expand Down Expand Up @@ -171,23 +174,29 @@ class EditingState {
/// This corresponds to Flutter's [TextInputConfiguration].
class InputConfiguration {
InputConfiguration({
this.inputType,
this.obscureText = false,
@required this.inputType,
@required this.inputAction,
@required this.obscureText,
});

InputConfiguration.fromFlutter(Map<String, dynamic> flutterInputConfiguration)
: inputType = EngineInputType.fromName(
flutterInputConfiguration['inputType']['name']),
inputAction = flutterInputConfiguration['inputAction'],
obscureText = flutterInputConfiguration['obscureText'];

/// The type of information being edited in the input control.
final EngineInputType inputType;

/// The default action for the input field.
final String inputAction;

/// Whether to hide the text being edited.
final bool obscureText;
}

typedef _OnChangeCallback = void Function(EditingState editingState);
typedef _OnActionCallback = void Function(String inputAction);

/// Wraps the DOM element used to provide text editing capabilities.
///
Expand Down Expand Up @@ -219,11 +228,16 @@ class TextEditingElement {
const Duration(milliseconds: 100);

final HybridTextEditing owner;
bool _enabled = false;

@visibleForTesting
bool isEnabled = false;

html.HtmlElement domElement;
InputConfiguration _inputConfiguration;
EditingState _lastEditingState;

_OnChangeCallback _onChange;
_OnActionCallback _onAction;

final List<StreamSubscription<html.Event>> _subscriptions =
<StreamSubscription<html.Event>>[];
Expand Down Expand Up @@ -261,12 +275,15 @@ class TextEditingElement {
void enable(
InputConfiguration inputConfig, {
@required _OnChangeCallback onChange,
@required _OnActionCallback onAction,
}) {
assert(!_enabled);
assert(!isEnabled);

_initDomElement(inputConfig);
_enabled = true;
isEnabled = true;
_inputConfiguration = inputConfig;
_onChange = onChange;
_onAction = onAction;

// Chrome on Android will hide the onscreen keyboard when you tap outside
// the text box. Instead, we want the framework to tell us to hide the
Expand All @@ -279,7 +296,7 @@ class TextEditingElement {
if (browserEngine == BrowserEngine.blink ||
browserEngine == BrowserEngine.unknown) {
_subscriptions.add(domElement.onBlur.listen((_) {
if (_enabled) {
if (isEnabled) {
_refocus();
}
}));
Expand All @@ -297,6 +314,8 @@ class TextEditingElement {
// Subscribe to text and selection changes.
_subscriptions.add(domElement.onInput.listen(_handleChange));

_subscriptions.add(domElement.onKeyDown.listen(_maybeSendAction));

/// Detects changes in text selection.
///
/// Currently only used in Firefox.
Expand Down Expand Up @@ -330,9 +349,9 @@ class TextEditingElement {
///
/// Calling [disable] also removes any registered event listeners.
void disable() {
assert(_enabled);
assert(isEnabled);

_enabled = false;
isEnabled = false;
_lastEditingState = null;

for (int i = 0; i < _subscriptions.length; i++) {
Expand Down Expand Up @@ -390,7 +409,7 @@ class TextEditingElement {

void setEditingState(EditingState editingState) {
_lastEditingState = editingState;
if (!_enabled || !editingState.isValid) {
if (!isEnabled || !editingState.isValid) {
return;
}

Expand All @@ -416,6 +435,13 @@ class TextEditingElement {
_onChange(_lastEditingState);
}
}

void _maybeSendAction(html.KeyboardEvent event) {
if (event.keyCode == _kReturnKeyCode) {
event.preventDefault();
_onAction(_inputConfiguration.inputAction);
}
}
}

/// The implementation of a persistent mode for [TextEditingElement].
Expand Down Expand Up @@ -512,7 +538,7 @@ class HybridTextEditing {
///
/// Use [stopUsingCustomEditableElement] to switch back to default element.
void useCustomEditableElement(TextEditingElement customEditingElement) {
if (_isEditing && customEditingElement != _customEditingElement) {
if (isEditing && customEditingElement != _customEditingElement) {
stopEditing();
}
_customEditingElement = customEditingElement;
Expand All @@ -529,20 +555,21 @@ class HybridTextEditing {
/// Flag which shows if there is an ongoing editing.
///
/// Also used to define if a keyboard is needed.
bool _isEditing = false;
@visibleForTesting
bool isEditing = false;

/// Indicates whether the input element needs to be positioned.
///
/// See [TextEditingElement._delayBeforePositioning].
bool get inputElementNeedsToBePositioned =>
!inputPositioned && _isEditing && doesKeyboardShiftInput;
!inputPositioned && isEditing && doesKeyboardShiftInput;

/// Flag indicating whether the input element's position is set.
///
/// See [inputElementNeedsToBePositioned].
bool inputPositioned = false;

Map<String, dynamic> _configuration;
InputConfiguration _configuration;

/// All "flutter/textinput" platform messages should be sent to this method.
void handleTextInput(ByteData data) {
Expand All @@ -551,11 +578,11 @@ class HybridTextEditing {
case 'TextInput.setClient':
final bool clientIdChanged =
_clientId != null && _clientId != call.arguments[0];
if (clientIdChanged && _isEditing) {
if (clientIdChanged && isEditing) {
stopEditing();
}
_clientId = call.arguments[0];
_configuration = call.arguments[1];
_configuration = InputConfiguration.fromFlutter(call.arguments[1]);
break;

case 'TextInput.setEditingState':
Expand All @@ -564,7 +591,7 @@ class HybridTextEditing {
break;

case 'TextInput.show':
if (!_isEditing) {
if (!isEditing) {
_startEditing();
}
break;
Expand All @@ -579,25 +606,26 @@ class HybridTextEditing {

case 'TextInput.clearClient':
case 'TextInput.hide':
if (_isEditing) {
if (isEditing) {
stopEditing();
}
break;
}
}

void _startEditing() {
assert(!_isEditing);
_isEditing = true;
assert(!isEditing);
isEditing = true;
editingElement.enable(
InputConfiguration.fromFlutter(_configuration),
_configuration,
onChange: _syncEditingStateToFlutter,
onAction: _sendInputActionToFlutter,
);
}

void stopEditing() {
assert(_isEditing);
_isEditing = false;
assert(isEditing);
isEditing = false;
editingElement.disable();
}

Expand Down Expand Up @@ -666,6 +694,19 @@ class HybridTextEditing {
);
}

void _sendInputActionToFlutter(String inputAction) {
ui.window.onPlatformMessage(
'flutter/textinput',
const JSONMethodCodec().encodeMethodCall(
MethodCall(
'TextInputClient.performAction',
<dynamic>[_clientId, inputAction],
),
),
_emptyCallback,
);
}

/// Positioning of input element is only done if we are not expecting input
/// to be shifted by a virtual keyboard or if the input is already positioned.
///
Expand Down
Loading