Skip to content
This repository was archived by the owner on Aug 1, 2024. It is now read-only.

Commit 605aa47

Browse files
Closure Teamcopybara-github
Closure Team
authored andcommitted
RELNOTES[NEW]: Fix goog.editor.Field so handleChanges is called when content is changed on mobile devices.
PiperOrigin-RevId: 436459407 Change-Id: I62b265e64b1c9441406b8f02498a47d82e25e3a3
1 parent e9d43fc commit 605aa47

File tree

3 files changed

+115
-11
lines changed

3 files changed

+115
-11
lines changed

closure/goog/editor/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ closure_js_library(
9696
"//closure/goog/html:safehtml",
9797
"//closure/goog/html:safestylesheet",
9898
"//closure/goog/html:trustedresourceurl",
99+
"//closure/goog/labs/useragent:platform",
99100
"//closure/goog/log",
100101
"//closure/goog/object",
101102
"//closure/goog/reflect",

closure/goog/editor/field.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ goog.require('goog.functions');
4343
goog.require('goog.html.SafeHtml');
4444
goog.require('goog.html.SafeStyleSheet');
4545
goog.require('goog.html.legacyconversions');
46+
goog.require('goog.labs.userAgent.platform');
4647
goog.require('goog.log');
4748
goog.require('goog.log.Level');
4849
goog.require('goog.string');
@@ -686,12 +687,18 @@ goog.editor.Field.CTRL_KEYS_CAUSING_CHANGES_ = {
686687
88: true // X
687688
};
688689

689-
if (goog.userAgent.WINDOWS && !goog.userAgent.GECKO) {
690+
if ((goog.userAgent.WINDOWS || goog.labs.userAgent.platform.isAndroid()) &&
691+
!goog.userAgent.GECKO) {
690692
// In IE and Webkit, input from IME (Input Method Editor) does not generate a
691693
// keypress event so we have to rely on the keydown event. This way we have
692694
// false positives while the user is using keyboard to select the
693695
// character to input, but it is still better than the false negatives
694696
// that ignores user's final input at all.
697+
// The same phenomina happen on android devices - no KeyPress events are
698+
// emitted, and all KeyDown events have no useful charCode or other
699+
// identifying information (see
700+
// https://bugs.chromium.org/p/chromium/issues/detail?id=118639 for
701+
// background, but it's considered WAI by various Input Method experts).
695702
goog.editor.Field.KEYS_CAUSING_CHANGES_[229] = true; // from IME;
696703
}
697704

@@ -894,6 +901,10 @@ goog.editor.Field.prototype.setupChangeListeners_ = function() {
894901
this.addListener(goog.events.EventType.KEYPRESS, this.handleKeyPress_);
895902
this.addListener(goog.events.EventType.KEYUP, this.handleKeyUp_);
896903

904+
// Handles changes from non-keyboard forms of input. Such as choosing a
905+
// spellcheck suggestion.
906+
this.addListener(goog.events.EventType.INPUT, this.handleChange);
907+
897908
this.selectionChangeTimer_ = new goog.async.Delay(
898909
this.handleSelectionChangeTimer_,
899910
goog.editor.Field.SELECTION_CHANGE_FREQUENCY_, this);
@@ -1092,7 +1103,7 @@ goog.editor.Field.prototype.handleBeforeChangeKeyEvent_ = function(e) {
10921103

10931104
// TODO(arv): Del at end of field or backspace at beginning should be
10941105
// ignored.
1095-
this.gotGeneratingKey_ = e.charCode ||
1106+
this.gotGeneratingKey_ = !!e.charCode ||
10961107
goog.editor.Field.isGeneratingKey_(e, goog.userAgent.GECKO);
10971108
if (this.gotGeneratingKey_) {
10981109
this.dispatchBeforeChange();
@@ -2125,7 +2136,7 @@ goog.editor.Field.prototype.handleMouseUp_ = function(e) {
21252136
*
21262137
* Do NOT just get the innerHTML of a field directly--there's a lot of
21272138
* processing that needs to happen.
2128-
* @return {string} The scrubbed contents of the field.
2139+
* @return {string} The scrubbed contents of the field.
21292140
*/
21302141
goog.editor.Field.prototype.getCleanContents = function() {
21312142
'use strict';
@@ -2139,7 +2150,7 @@ goog.editor.Field.prototype.getCleanContents = function() {
21392150
if (!elem) {
21402151
goog.log.log(
21412152
this.logger, goog.log.Level.SHOUT,
2142-
"Couldn't get the field element to read the contents");
2153+
'Couldn\'t get the field element to read the contents');
21432154
}
21442155
return elem.innerHTML;
21452156
}
@@ -2192,7 +2203,7 @@ goog.editor.Field.prototype.setSafeHtml = function(
21922203
addParas, html, opt_dontFireDelayedChange, opt_applyLorem) {
21932204
'use strict';
21942205
if (this.isLoading()) {
2195-
goog.log.error(this.logger, "Can't set html while loading Trogedit");
2206+
goog.log.error(this.logger, 'Can\'t set html while loading Trogedit');
21962207
return;
21972208
}
21982209

closure/goog/editor/field_test.js

+98-6
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ const Plugin = goog.require('goog.editor.Plugin');
2626
const Range = goog.require('goog.dom.Range');
2727
const SafeHtml = goog.require('goog.html.SafeHtml');
2828
const TagName = goog.require('goog.dom.TagName');
29+
const TestEvent = goog.require('goog.testing.events.Event');
2930
const classlist = goog.require('goog.dom.classlist');
3031
const editorRange = goog.require('goog.editor.range');
3132
const events = goog.require('goog.events');
3233
const functions = goog.require('goog.functions');
3334
const googDom = goog.require('goog.dom');
35+
const platform = goog.require('goog.labs.userAgent.platform');
3436
const recordFunction = goog.require('goog.testing.recordFunction');
3537
const testSuite = goog.require('goog.testing.testSuite');
3638
const testingDom = goog.require('goog.testing.dom');
@@ -181,7 +183,8 @@ function doTestPlaceCursorAtStart(html = undefined, parentId = undefined) {
181183

182184
let startNode = parentId ?
183185
editableField.getEditableDomHelper().getElement(parentId).firstChild :
184-
textNode ? textNode : editableField.getElement();
186+
textNode ? textNode :
187+
editableField.getElement();
185188
assertEquals(
186189
'The range should start at the specified expected node', startNode,
187190
range.getStartNode());
@@ -239,13 +242,14 @@ function doTestPlaceCursorAtEnd(
239242

240243
const endNode = parentId ?
241244
editableField.getEditableDomHelper().getElement(parentId).lastChild :
242-
textNode ? textNode : editableField.getElement();
245+
textNode ? textNode :
246+
editableField.getElement();
243247
assertEquals(
244248
'The range should end at the specified expected node', endNode,
245249
range.getEndNode());
246-
const offset = (opt_offset != null) ?
247-
opt_offset :
248-
textNode ? endNode.nodeValue.length : endNode.childNodes.length - 1;
250+
const offset = (opt_offset != null) ? opt_offset :
251+
textNode ? endNode.nodeValue.length :
252+
endNode.childNodes.length - 1;
249253
if (hasBogusNode) {
250254
assertEquals(
251255
'The range should end at the ending of the bogus node ' +
@@ -1425,13 +1429,101 @@ testSuite({
14251429

14261430
assertTrue(Field.isGeneratingKey_(regularKeyEvent, true));
14271431
assertFalse(Field.isGeneratingKey_(ctrlKeyEvent, true));
1428-
if (userAgent.WINDOWS && !userAgent.GECKO) {
1432+
if ((userAgent.WINDOWS || platform.isAndroid()) && !userAgent.GECKO) {
14291433
assertTrue(Field.isGeneratingKey_(imeKeyEvent, false));
14301434
} else {
14311435
assertFalse(Field.isGeneratingKey_(imeKeyEvent, false));
14321436
}
14331437
},
14341438

1439+
testRegularKeyDispatchesDelayedChange() {
1440+
if (userAgent.GECKO) {
1441+
// Gecko based browsers handle changes via mutation events
1442+
return;
1443+
}
1444+
if (userAgent.WINDOWS || platform.isAndroid()) {
1445+
// Windows and Android platforms do not emit events with 'regular' key
1446+
// codes.
1447+
return;
1448+
}
1449+
const editableField = new FieldConstructor('testField');
1450+
const clock = new MockClock(true);
1451+
const delayedChanges = recordFunction();
1452+
1453+
editableField.makeEditable();
1454+
events.listen(editableField, Field.EventType.DELAYEDCHANGE, delayedChanges);
1455+
1456+
testingEvents.fireKeySequence(editableField.getElement(), KeyCodes.A);
1457+
clock.tick(1000);
1458+
1459+
if (!(userAgent.WINDOWS || platform.isAndroid()) || userAgent.GECKO) {
1460+
assertEquals(
1461+
'Delayed change event should\'ve been dispatched', 1,
1462+
delayedChanges.getCallCount());
1463+
}
1464+
1465+
clock.dispose();
1466+
editableField.dispose();
1467+
},
1468+
1469+
testImeKeyDispatchesDelayedChange() {
1470+
if (BrowserFeature.USE_MUTATION_EVENTS) {
1471+
// Gecko based browsers handle changes via mutation events
1472+
return;
1473+
}
1474+
if (!(userAgent.WINDOWS || platform.isAndroid())) {
1475+
// Only Windows and Android platforms emit these IME-specific events.
1476+
return;
1477+
}
1478+
const editableField = new FieldConstructor('testField');
1479+
const clock = new MockClock(true);
1480+
const delayedChanges = recordFunction();
1481+
1482+
editableField.makeEditable();
1483+
events.listen(editableField, Field.EventType.DELAYEDCHANGE, delayedChanges);
1484+
1485+
testingEvents.fireKeySequence(editableField.getElement(), KeyCodes.WIN_IME);
1486+
clock.tick(1000);
1487+
1488+
assertEquals(
1489+
'Delayed change event should\'ve been dispatched', 1,
1490+
delayedChanges.getCallCount());
1491+
1492+
1493+
clock.dispose();
1494+
editableField.dispose();
1495+
},
1496+
1497+
testInputEventDispatchesDelayedChange() {
1498+
if (BrowserFeature.USE_MUTATION_EVENTS) {
1499+
// Gecko based browsers handle changes via mutation events
1500+
return;
1501+
}
1502+
const editableField = new FieldConstructor('testField');
1503+
const clock = new MockClock(true);
1504+
const delayedChanges = recordFunction();
1505+
1506+
editableField.makeEditable();
1507+
events.listen(editableField, Field.EventType.DELAYEDCHANGE, delayedChanges);
1508+
1509+
// Non-typing changes on some devices rely on emitting an INPUT event, such
1510+
// as:
1511+
// - swipe-typing a word (on iOS)
1512+
// - accepting a word prediction (on iOS)
1513+
// - using speech to text (on iOS)
1514+
// - accepting a spellcheck suggestion (on iOS, Android or Desktop)
1515+
testingEvents.fireBrowserEvent(
1516+
new TestEvent(EventType.INPUT, editableField.getElement()));
1517+
clock.tick(1000);
1518+
1519+
assertEquals(
1520+
'Delayed change event should\'ve been dispatched', 1,
1521+
delayedChanges.getCallCount());
1522+
1523+
clock.dispose();
1524+
editableField.dispose();
1525+
},
1526+
14351527
testSetEditableClassName() {
14361528
const element = googDom.getElement('testField');
14371529
const editableField = new FieldConstructor('testField');

0 commit comments

Comments
 (0)