@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Don't mutate DOM on touch-originated cursor events in Differential

Summary:
Fixes T10229. Broadly:

- When the user hovers over a line number or inline comment, we update the yellow reticle to highlight the relevant lines. Specifically, this is in response to a `mouseover` event.
- On touch devices, touches fire `mouseover` and if you mutate the DOM inside the event, the device aborts the touch.

To remedy this:

- Distingiush between mouse-originated and touch-originated cursor events.
- We do this, roughly, by setting a flag when we see "touchstart", and clearing it when we see the second copy of any unique cursor event.
- This method is complex, but should be robust to any implementation differences between devices (for example, it will work no matter which order the events are fired in).
- This method should also produce the correct results on weird devices that have both mouse-devices and touch-devices available for cursor input.
- When we see a touch-originated `mouseover` or `mouseout`, don't mutate the DOM.
- Put an extra DOM mutation into the `click` event to improve highlighting behavior on touch devices.

Test Plan:
- In iOS Simulator (4s, iOS 9.2), clicked various inline actions ("Reply", "Hide", "Done", "Cancel", line numbers, etc). Got responses after a single touch.
- Verified hover + click behavior on a desktop.
- Logged and examined a bunch of events as a general sanity check.

Reviewers: chad

Reviewed By: chad

Subscribers: aljungberg

Maniphest Tasks: T10229

Differential Revision: https://secure.phabricator.com/D15136

+134 -52
+34 -34
resources/celerity/map.php
··· 8 8 return array( 9 9 'names' => array( 10 10 'core.pkg.css' => '8abb1666', 11 - 'core.pkg.js' => '573e6664', 11 + 'core.pkg.js' => 'a79eed25', 12 12 'darkconsole.pkg.js' => 'e7393ebb', 13 13 'differential.pkg.css' => '2de124c9', 14 - 'differential.pkg.js' => 'f83532f8', 14 + 'differential.pkg.js' => '5c2ba922', 15 15 'diffusion.pkg.css' => 'f45955ed', 16 16 'diffusion.pkg.js' => '3a9a8bfa', 17 17 'maniphest.pkg.css' => '4845691a', ··· 192 192 'rsrc/externals/font/lato/lato-regular.ttf' => 'e270165b', 193 193 'rsrc/externals/font/lato/lato-regular.woff' => '13d39fe2', 194 194 'rsrc/externals/font/lato/lato-regular.woff2' => '57a9f742', 195 - 'rsrc/externals/javelin/core/Event.js' => '85ea0626', 196 - 'rsrc/externals/javelin/core/Stratcom.js' => '6c53634d', 195 + 'rsrc/externals/javelin/core/Event.js' => '2ee659ce', 196 + 'rsrc/externals/javelin/core/Stratcom.js' => '6ad39b6f', 197 197 'rsrc/externals/javelin/core/__tests__/event-stop-and-kill.js' => '717554e4', 198 198 'rsrc/externals/javelin/core/__tests__/install.js' => 'c432ee85', 199 199 'rsrc/externals/javelin/core/__tests__/stratcom.js' => '88bf7313', ··· 379 379 'rsrc/js/application/differential/behavior-comment-preview.js' => 'b064af76', 380 380 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', 381 381 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '9a6b9324', 382 - 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '65ef6074', 382 + 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '4fbbc3e9', 383 383 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '2c426492', 384 384 'rsrc/js/application/differential/behavior-populate.js' => '8694b1df', 385 385 'rsrc/js/application/differential/behavior-toggle-files.js' => 'ca3f91eb', ··· 465 465 'rsrc/js/core/behavior-choose-control.js' => '327a00d1', 466 466 'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2', 467 467 'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae', 468 - 'rsrc/js/core/behavior-device.js' => 'a205cf28', 468 + 'rsrc/js/core/behavior-device.js' => 'b5b36110', 469 469 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '4f6a4b4e', 470 470 'rsrc/js/core/behavior-error-log.js' => '6882e80a', 471 471 'rsrc/js/core/behavior-fancy-datepicker.js' => '8ae55229', ··· 584 584 'javelin-behavior-dashboard-tab-panel' => 'd4eecc63', 585 585 'javelin-behavior-day-view' => '5c46cff2', 586 586 'javelin-behavior-desktop-notifications-control' => 'edd1ba66', 587 - 'javelin-behavior-device' => 'a205cf28', 587 + 'javelin-behavior-device' => 'b5b36110', 588 588 'javelin-behavior-differential-add-reviewers-and-ccs' => 'e10f8e18', 589 589 'javelin-behavior-differential-comment-jump' => '4fdb476d', 590 590 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', 591 591 'javelin-behavior-differential-dropdown-menus' => '9a6b9324', 592 - 'javelin-behavior-differential-edit-inline-comments' => '65ef6074', 592 + 'javelin-behavior-differential-edit-inline-comments' => '4fbbc3e9', 593 593 'javelin-behavior-differential-feedback-preview' => 'b064af76', 594 594 'javelin-behavior-differential-keyboard-navigation' => '2c426492', 595 595 'javelin-behavior-differential-populate' => '8694b1df', ··· 683 683 'javelin-diffusion-locate-file-source' => 'b42eddc7', 684 684 'javelin-dom' => '805b806a', 685 685 'javelin-dynval' => 'f6555212', 686 - 'javelin-event' => '85ea0626', 686 + 'javelin-event' => '2ee659ce', 687 687 'javelin-fx' => '54b612ba', 688 688 'javelin-history' => 'd4505101', 689 689 'javelin-install' => '05270951', ··· 702 702 'javelin-router' => '29274e2b', 703 703 'javelin-scrollbar' => '087e919c', 704 704 'javelin-sound' => '949c0fe5', 705 - 'javelin-stratcom' => '6c53634d', 705 + 'javelin-stratcom' => '6ad39b6f', 706 706 'javelin-tokenizer' => '8d3bc1b2', 707 707 'javelin-typeahead' => '70baed2f', 708 708 'javelin-typeahead-composite-source' => '503e17fd', ··· 1050 1050 'javelin-install', 1051 1051 'javelin-event', 1052 1052 ), 1053 + '2ee659ce' => array( 1054 + 'javelin-install', 1055 + ), 1053 1056 '327a00d1' => array( 1054 1057 'javelin-behavior', 1055 1058 'javelin-stratcom', ··· 1163 1166 'javelin-dom', 1164 1167 'phabricator-drag-and-drop-file-upload', 1165 1168 'phabricator-textareautils', 1169 + ), 1170 + '4fbbc3e9' => array( 1171 + 'javelin-behavior', 1172 + 'javelin-stratcom', 1173 + 'javelin-dom', 1174 + 'javelin-util', 1175 + 'javelin-vector', 1176 + 'differential-inline-comment-editor', 1166 1177 ), 1167 1178 '4fdb476d' => array( 1168 1179 'javelin-behavior', ··· 1295 1306 'javelin-request', 1296 1307 'javelin-workflow', 1297 1308 ), 1298 - '65ef6074' => array( 1299 - 'javelin-behavior', 1300 - 'javelin-stratcom', 1301 - 'javelin-dom', 1302 - 'javelin-util', 1303 - 'javelin-vector', 1304 - 'differential-inline-comment-editor', 1305 - ), 1306 1309 '66dd6e9e' => array( 1307 1310 'javelin-behavior', 1308 1311 'javelin-behavior-device', ··· 1316 1319 '69adf288' => array( 1317 1320 'javelin-install', 1318 1321 ), 1322 + '6ad39b6f' => array( 1323 + 'javelin-install', 1324 + 'javelin-event', 1325 + 'javelin-util', 1326 + 'javelin-magical-init', 1327 + ), 1319 1328 '6b8ef10b' => array( 1320 1329 'javelin-install', 1321 1330 ), ··· 1326 1335 '6c2b09a2' => array( 1327 1336 'javelin-install', 1328 1337 'javelin-util', 1329 - ), 1330 - '6c53634d' => array( 1331 - 'javelin-install', 1332 - 'javelin-event', 1333 - 'javelin-util', 1334 - 'javelin-magical-init', 1335 1338 ), 1336 1339 '6d3e1947' => array( 1337 1340 'javelin-behavior', ··· 1446 1449 'javelin-dom', 1447 1450 'javelin-stratcom', 1448 1451 ), 1449 - '85ea0626' => array( 1450 - 'javelin-install', 1451 - ), 1452 1452 '85ee8ce6' => array( 1453 1453 'aphront-dialog-view-css', 1454 1454 ), ··· 1602 1602 'javelin-vector', 1603 1603 'javelin-magical-init', 1604 1604 ), 1605 - 'a205cf28' => array( 1606 - 'javelin-behavior', 1607 - 'javelin-stratcom', 1608 - 'javelin-dom', 1609 - 'javelin-vector', 1610 - 'javelin-install', 1611 - ), 1612 1605 'a2828756' => array( 1613 1606 'javelin-dom', 1614 1607 'javelin-util', ··· 1738 1731 'javelin-workflow', 1739 1732 'javelin-dom', 1740 1733 'phabricator-draggable-list', 1734 + ), 1735 + 'b5b36110' => array( 1736 + 'javelin-behavior', 1737 + 'javelin-stratcom', 1738 + 'javelin-dom', 1739 + 'javelin-vector', 1740 + 'javelin-install', 1741 1741 ), 1742 1742 'b5c256b8' => array( 1743 1743 'javelin-install',
+17 -2
webroot/rsrc/externals/javelin/core/Event.js
··· 326 326 /** 327 327 * @task info 328 328 */ 329 - nodeDistances : {} 329 + nodeDistances : {}, 330 + 331 + /** 332 + * True if this is a cursor event that was caused by a touch interaction 333 + * rather than a mouse device interaction. 334 + * 335 + * @type bool 336 + * @taks info 337 + */ 338 + isTouchEvent: false 330 339 }, 331 340 332 341 /** ··· 339 348 if (__DEV__) { 340 349 JX.Event.prototype.toString = function() { 341 350 var path = '['+this.getPath().join(', ')+']'; 342 - return 'Event<'+this.getType()+', '+path+', '+this.getTarget()+'>'; 351 + 352 + var type = this.getType(); 353 + if (this.getIsTouchEvent()) { 354 + type = type + '/touch'; 355 + } 356 + 357 + return 'Event<'+type+', '+path+', '+this.getTarget()+'>'; 343 358 }; 344 359 } 345 360 }
+43 -1
webroot/rsrc/externals/javelin/core/Stratcom.js
··· 41 41 _auto : '*', 42 42 _data : {}, 43 43 _execContext : [], 44 + _touchState: null, 44 45 45 46 /** 46 47 * Node metadata is stored in a series of blocks to prevent collisions ··· 330 331 etype = 'blur'; 331 332 } 332 333 334 + // Map of touch events and whether they are unique per touch. 335 + var touch_map = { 336 + touchstart: true, 337 + touchend: true, 338 + mousedown: true, 339 + mouseup: true, 340 + click: true, 341 + 342 + // These can conceivably fire multiple times per touch, so if we see 343 + // them more than once that doesn't tell us that we're handling a new 344 + // event. 345 + mouseover: false, 346 + mouseout: false, 347 + mousemove: false, 348 + touchmove: false 349 + }; 350 + 351 + // If this is a 'touchstart', we're handling touch events. 352 + if (etype == 'touchstart') { 353 + this._touchState = {}; 354 + } 355 + 356 + // If this is a unique touch event that we haven't seen before, remember 357 + // it as part of the current touch state. On the other hand, if we've 358 + // already seen it, we can deduce that we must be handling a new cursor 359 + // event that is unrelated to the last touch we saw, and conclude that 360 + // we are no longer processing a touch event. 361 + if (touch_map[etype] && this._touchState) { 362 + if (!this._touchState[etype]) { 363 + this._touchState[etype] = true; 364 + } else { 365 + this._touchState = null; 366 + } 367 + } 368 + 369 + // This event is a touch event if we're carrying some touch state. 370 + var is_touch = (etype in touch_map) && 371 + (this._touchState !== null); 372 + 333 373 var proxy = new JX.Event() 334 374 .setRawEvent(event) 335 375 .setData(event.customData) ··· 337 377 .setTarget(target) 338 378 .setNodes(nodes) 339 379 .setNodeDistances(distances) 380 + .setIsTouchEvent(is_touch) 340 381 .setPath(path.reverse()); 341 382 342 - // Don't touch this for debugging purposes 383 + // You can uncomment this to print out all events flowing through 384 + // Stratcom, which tends to make debugging easier. 343 385 //JX.log('~> '+proxy.toString()); 344 386 345 387 return this._dispatchProxy(proxy);
+35 -15
webroot/rsrc/js/application/differential/behavior-edit-inline-comments.js
··· 22 22 23 23 var editor = null; 24 24 25 + function updateReticleForComment(e) { 26 + root = e.getNode('differential-changeset'); 27 + if (!root) { 28 + return; 29 + } 30 + 31 + var data = e.getNodeData('differential-inline-comment'); 32 + var change = e.getNodeData('differential-changeset'); 33 + 34 + var id_part = data.on_right ? change.right : change.left; 35 + var new_part = data.isNewFile ? 'N' : 'O'; 36 + var prefix = 'C' + id_part + new_part + 'L'; 37 + 38 + origin = JX.$(prefix + data.number); 39 + target = JX.$(prefix + (parseInt(data.number, 10) + 40 + parseInt(data.length, 10))); 41 + 42 + updateReticle(); 43 + } 44 + 25 45 function updateReticle() { 26 46 JX.DOM.getContentFrame().appendChild(reticle); 27 47 ··· 176 196 ['mouseover', 'mouseout'], 177 197 ['differential-changeset', 'tag:th'], 178 198 function(e) { 199 + if (e.getIsTouchEvent()) { 200 + return; 201 + } 202 + 179 203 if (editor) { 180 204 // Don't update the reticle if we're editing a comment, since this 181 205 // would be distracting and we want to keep the lines corresponding ··· 274 298 ['mouseover', 'mouseout'], 275 299 'differential-inline-comment', 276 300 function(e) { 301 + if (e.getIsTouchEvent()) { 302 + return; 303 + } 304 + 277 305 if (e.getType() == 'mouseout') { 278 306 hideReticle(); 279 307 } else { 280 - root = e.getNode('differential-changeset'); 281 - if (root) { 282 - var data = e.getNodeData('differential-inline-comment'); 283 - var change = e.getNodeData('differential-changeset'); 284 - 285 - var id_part = data.on_right ? change.right : change.left; 286 - var new_part = data.isNewFile ? 'N' : 'O'; 287 - var prefix = 'C' + id_part + new_part + 'L'; 288 - 289 - origin = JX.$(prefix + data.number); 290 - target = JX.$(prefix + (parseInt(data.number, 10) + 291 - parseInt(data.length, 10))); 292 - 293 - updateReticle(); 294 - } 308 + updateReticleForComment(e); 295 309 } 296 310 }); 297 311 ··· 303 317 } 304 318 305 319 var node = e.getNode('differential-inline-comment'); 320 + 321 + // If we're on a touch device, we didn't highlight the affected lines 322 + // earlier because we can't use hover events to mutate the document. 323 + // Highlight them now. 324 + updateReticleForComment(e); 325 + 306 326 handle_inline_action(node, op); 307 327 }; 308 328
+5
webroot/rsrc/js/core/behavior-device.js
··· 62 62 JX.Stratcom.invoke('phabricator-device-change', null, device); 63 63 }, 64 64 65 + isDesktop: function() { 66 + var self = JX.Device; 67 + return (self.getDevice() == 'desktop'); 68 + }, 69 + 65 70 getDevice : function() { 66 71 var self = JX.Device; 67 72 if (self._device === null) {