1// Copyright 2013 The Flutter Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5part of engine; 6 7/// Manages semantics objects that represent editable text fields. 8/// 9/// This role is implemented via a content-editable HTML element. This role does 10/// not proactively switch modes depending on the current 11/// [EngineSemanticsOwner.gestureMode]. However, in Chrome on Android it ignores 12/// browser gestures when in pointer mode. In Safari on iOS touch events are 13/// used to detect text box invocation. This is because Safari issues touch 14/// events even when Voiceover is enabled. 15class TextField extends RoleManager { 16 TextField(SemanticsObject semanticsObject) 17 : super(Role.textField, semanticsObject) { 18 final html.HtmlElement editableDomElement = 19 semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline) 20 ? html.TextAreaElement() 21 : html.InputElement(); 22 persistentTextEditingElement = PersistentTextEditingElement( 23 editableDomElement, 24 onDomElementSwap: _setupDomElement, 25 ); 26 _setupDomElement(); 27 } 28 29 PersistentTextEditingElement persistentTextEditingElement; 30 html.Element get _textFieldElement => persistentTextEditingElement.domElement; 31 32 void _setupDomElement() { 33 // On iOS, even though the semantic text field is transparent, the cursor 34 // and text highlighting are still visible. The cursor and text selection 35 // are made invisible by CSS in [DomRenderer.reset]. 36 // But there's one more case where iOS highlights text. That's when there's 37 // and autocorrect suggestion. To disable that, we have to do the following: 38 _textFieldElement 39 ..spellcheck = false 40 ..setAttribute('spellcheck', 'false') 41 ..setAttribute('autocorrect', 'off') 42 ..setAttribute('autocomplete', 'off') 43 ..setAttribute('data-semantics-role', 'text-field'); 44 45 _textFieldElement.style 46 ..position = 'absolute' 47 // `top` and `left` are intentionally set to zero here. 48 // 49 // The text field would live inside a `<flt-semantics>` which should 50 // already be positioned using semantics.rect. 51 // 52 // See also: 53 // 54 // * [SemanticsObject.recomputePositionAndSize], which sets the position 55 // and size of the parent `<flt-semantics>` element. 56 ..top = '0' 57 ..left = '0' 58 ..width = '${semanticsObject.rect.width}px' 59 ..height = '${semanticsObject.rect.height}px'; 60 semanticsObject.element.append(_textFieldElement); 61 62 switch (browserEngine) { 63 case BrowserEngine.blink: 64 case BrowserEngine.unknown: 65 _initializeForBlink(); 66 break; 67 case BrowserEngine.webkit: 68 _initializeForWebkit(); 69 break; 70 } 71 } 72 73 /// Chrome on Android reports text field activation as a "click" event. 74 /// 75 /// When in browser gesture mode, the focus is forwarded to the framework as 76 /// a tap to initialize editing. 77 void _initializeForBlink() { 78 _textFieldElement.addEventListener('focus', (html.Event event) { 79 if (semanticsObject.owner.gestureMode != GestureMode.browserGestures) { 80 return; 81 } 82 83 textEditing.useCustomEditableElement(persistentTextEditingElement); 84 ui.window 85 .onSemanticsAction(semanticsObject.id, ui.SemanticsAction.tap, null); 86 }); 87 } 88 89 /// Safari on iOS reports text field activation via touch events. 90 /// 91 /// This emulates a tap recognizer to detect the activation. Because touch 92 /// events are present regardless of whether accessibility is enabled or not, 93 /// this mode is always enabled. 94 void _initializeForWebkit() { 95 num lastTouchStartOffsetX; 96 num lastTouchStartOffsetY; 97 98 _textFieldElement.addEventListener('touchstart', (html.Event event) { 99 textEditing.useCustomEditableElement(persistentTextEditingElement); 100 final html.TouchEvent touchEvent = event; 101 lastTouchStartOffsetX = touchEvent.changedTouches.last.client.x; 102 lastTouchStartOffsetY = touchEvent.changedTouches.last.client.y; 103 }, true); 104 105 _textFieldElement.addEventListener('touchend', (html.Event event) { 106 final html.TouchEvent touchEvent = event; 107 108 if (lastTouchStartOffsetX != null) { 109 assert(lastTouchStartOffsetY != null); 110 final num offsetX = touchEvent.changedTouches.last.client.x; 111 final num offsetY = touchEvent.changedTouches.last.client.y; 112 113 // This should match the similar constant define in: 114 // 115 // lib/src/gestures/constants.dart 116 // 117 // The value is pre-squared so we have to do less math at runtime. 118 const double kTouchSlop = 18.0 * 18.0; // Logical pixels squared 119 120 if (offsetX * offsetX + offsetY * offsetY < kTouchSlop) { 121 // Recognize it as a tap that requires a keyboard. 122 ui.window.onSemanticsAction( 123 semanticsObject.id, ui.SemanticsAction.tap, null); 124 } 125 } else { 126 assert(lastTouchStartOffsetY == null); 127 } 128 129 lastTouchStartOffsetX = null; 130 lastTouchStartOffsetY = null; 131 }, true); 132 } 133 134 @override 135 void update() { 136 // The user is editing the semantic text field directly, so there's no need 137 // to do any update here. 138 } 139 140 @override 141 void dispose() { 142 _textFieldElement.remove(); 143 textEditing.stopUsingCustomEditableElement(); 144 } 145} 146