From 6b5204dca954011bb45b2766b2bcc4e7080a5a7d Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 9 Mar 2013 13:53:15 -0800 Subject: [PATCH] Add support for device swipe events Summary: Ref T2700. Allow JS to listen for swipes on devices. There are a bunch of tricky cases here and I probably didn't get them all totally right, but this interaction broadly looks like this: - We implement gesture recognition for the mouse in device modes (narrow browser), and for touch events from an actual device. - The sigil `touchable` indicates that a node wants to react to touch events. - When the user touches a `touchable` node, we start listening for moves. They might be tapping/clicking (in which case we don't care), but they might also be gesturing. - Once the user moves their finger/pointer far enough away from the tap origin, we recognize it as a gesture. I hardcoded this at 20px; I wasn't able to find any "official" Apple value, but 20px seems like a common default. - At this point, we look at where their finger has moved. - If they moved it mostly up/down, we interpret the gesture as "scroll" and just stop listening. The device does its own thing. - However, if they moved it mostly left/right, we interpret it as a "swipe". We start killing the moves so the device doesn't scroll. - Once we've recognized that a gesture is underway, we send a "gesture.swipe.start" event and then "gesture.swipe.move" events for every move. - When the user ends the gesture, we send "gesture.swipe.end". - If the user cancels the gesture (currently, only by tapping with a second finger), we send "gesture.swipe.cancel". - Gesture events have raw position data and some convenience fields. Test Plan: Wrote UI example and used it from the Desktop, iPhone simulator, and a real iphone. - The code always seems to get "scroll" vs "swipe" correct (i.e., consistent with my intentions). - The threshold feels pretty good to me. - Tapping with a second finger cancels the action. Reviewers: chad, btrahan Reviewed By: chad CC: aran Maniphest Tasks: T2700 Differential Revision: https://secure.phabricator.com/D5308 --- scripts/celerity_mapper.php | 1 + src/__celerity_resource_map__.php | 216 ++++++++++-------- src/__phutil_library_map__.php | 2 + .../examples/PhabricatorGestureExample.php | 35 +++ src/view/page/PhabricatorStandardPageView.php | 1 + .../js/application/core/behavior-gesture.js | 125 ++++++++++ .../application/uiexample/gesture-example.js | 85 +++++++ 7 files changed, 373 insertions(+), 92 deletions(-) create mode 100644 src/applications/uiexample/examples/PhabricatorGestureExample.php create mode 100644 webroot/rsrc/js/application/core/behavior-gesture.js create mode 100644 webroot/rsrc/js/application/uiexample/gesture-example.js diff --git a/scripts/celerity_mapper.php b/scripts/celerity_mapper.php index 74e7ff3fd0..c80aa69e46 100755 --- a/scripts/celerity_mapper.php +++ b/scripts/celerity_mapper.php @@ -51,6 +51,7 @@ $package_spec = array( 'javelin-behavior-konami', 'javelin-behavior-aphlict-dropdown', 'javelin-behavior-history-install', + 'javelin-behavior-phabricator-gesture', 'javelin-behavior-phabricator-active-nav', 'javelin-behavior-phabricator-nav', diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index b34e247c9c..7c4a5557f7 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -805,7 +805,7 @@ celerity_register_resource_map(array( ), 'conpherence-widget-pane-css' => array( - 'uri' => '/res/6e5755bb/rsrc/css/application/conpherence/widget-pane.css', + 'uri' => '/res/e67ad581/rsrc/css/application/conpherence/widget-pane.css', 'type' => 'css', 'requires' => array( @@ -1167,28 +1167,30 @@ celerity_register_resource_map(array( ), 'javelin-behavior-conpherence-menu' => array( - 'uri' => '/res/0ad6ab54/rsrc/js/application/conpherence/behavior-menu.js', + 'uri' => '/res/cb1a5cf0/rsrc/js/application/conpherence/behavior-menu.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', - 2 => 'javelin-workflow', - 3 => 'javelin-util', - 4 => 'javelin-stratcom', - 5 => 'javelin-uri', + 2 => 'javelin-request', + 3 => 'javelin-stratcom', + 4 => 'javelin-uri', + 5 => 'javelin-util', + 6 => 'javelin-workflow', ), 'disk' => '/rsrc/js/application/conpherence/behavior-menu.js', ), 'javelin-behavior-conpherence-pontificate' => array( - 'uri' => '/res/06214a06/rsrc/js/application/conpherence/behavior-pontificate.js', + 'uri' => '/res/15263692/rsrc/js/application/conpherence/behavior-pontificate.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-behavior', 1 => 'javelin-dom', - 2 => 'javelin-request', + 2 => 'javelin-util', + 3 => 'javelin-workflow', ), 'disk' => '/rsrc/js/application/conpherence/behavior-pontificate.js', ), @@ -1746,6 +1748,34 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-file-tree.js', ), + 'javelin-behavior-phabricator-gesture' => + array( + 'uri' => '/res/1222d486/rsrc/js/application/core/behavior-gesture.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-behavior-device', + 2 => 'javelin-stratcom', + 3 => 'javelin-vector', + 4 => 'javelin-dom', + 5 => 'javelin-magical-init', + ), + 'disk' => '/rsrc/js/application/core/behavior-gesture.js', + ), + 'javelin-behavior-phabricator-gesture-example' => + array( + 'uri' => '/res/da636e19/rsrc/js/application/uiexample/gesture-example.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-stratcom', + 1 => 'javelin-behavior', + 2 => 'javelin-vector', + 3 => 'javelin-dom', + ), + 'disk' => '/rsrc/js/application/uiexample/gesture-example.js', + ), 'javelin-behavior-phabricator-keyboard-pager' => array( 'uri' => '/res/56d64eff/rsrc/js/application/core/behavior-keyboard-pager.js', @@ -2664,7 +2694,7 @@ celerity_register_resource_map(array( ), 'phabricator-core-css' => array( - 'uri' => '/res/b34e5c75/rsrc/css/core/core.css', + 'uri' => '/res/1e7afaa9/rsrc/css/core/core.css', 'type' => 'css', 'requires' => array( @@ -3055,7 +3085,7 @@ celerity_register_resource_map(array( ), 'phabricator-standard-page-view' => array( - 'uri' => '/res/252faaf4/rsrc/css/application/base/standard-page-view.css', + 'uri' => '/res/70fa2da4/rsrc/css/application/base/standard-page-view.css', 'type' => 'css', 'requires' => array( @@ -3500,7 +3530,7 @@ celerity_register_resource_map(array( ), array( 'packages' => array( - '8e2735e2' => + 'fdd3bb5f' => array( 'name' => 'core.pkg.css', 'symbols' => @@ -3543,10 +3573,10 @@ celerity_register_resource_map(array( 35 => 'phabricator-object-item-list-view-css', 36 => 'global-drag-and-drop-css', ), - 'uri' => '/res/pkg/8e2735e2/core.pkg.css', + 'uri' => '/res/pkg/fdd3bb5f/core.pkg.css', 'type' => 'css', ), - '0ed3458f' => + '9abfe389' => array( 'name' => 'core.pkg.js', 'symbols' => @@ -3578,15 +3608,16 @@ celerity_register_resource_map(array( 24 => 'javelin-behavior-konami', 25 => 'javelin-behavior-aphlict-dropdown', 26 => 'javelin-behavior-history-install', - 27 => 'javelin-behavior-phabricator-active-nav', - 28 => 'javelin-behavior-phabricator-nav', - 29 => 'javelin-behavior-phabricator-remarkup-assist', - 30 => 'phabricator-textareautils', - 31 => 'phabricator-file-upload', - 32 => 'javelin-behavior-global-drag-and-drop', - 33 => 'javelin-behavior-phabricator-reveal-content', + 27 => 'javelin-behavior-phabricator-gesture', + 28 => 'javelin-behavior-phabricator-active-nav', + 29 => 'javelin-behavior-phabricator-nav', + 30 => 'javelin-behavior-phabricator-remarkup-assist', + 31 => 'phabricator-textareautils', + 32 => 'phabricator-file-upload', + 33 => 'javelin-behavior-global-drag-and-drop', + 34 => 'javelin-behavior-phabricator-reveal-content', ), - 'uri' => '/res/pkg/0ed3458f/core.pkg.js', + 'uri' => '/res/pkg/9abfe389/core.pkg.js', 'type' => 'js', ), 'dca4a03d' => @@ -3733,17 +3764,17 @@ celerity_register_resource_map(array( 'reverse' => array( 'aphront-attached-file-view-css' => 'eb35a026', - 'aphront-crumbs-view-css' => '8e2735e2', - 'aphront-dialog-view-css' => '8e2735e2', - 'aphront-error-view-css' => '8e2735e2', - 'aphront-form-view-css' => '8e2735e2', - 'aphront-list-filter-view-css' => '8e2735e2', - 'aphront-pager-view-css' => '8e2735e2', - 'aphront-panel-view-css' => '8e2735e2', - 'aphront-table-view-css' => '8e2735e2', - 'aphront-tokenizer-control-css' => '8e2735e2', - 'aphront-tooltip-css' => '8e2735e2', - 'aphront-typeahead-control-css' => '8e2735e2', + 'aphront-crumbs-view-css' => 'fdd3bb5f', + 'aphront-dialog-view-css' => 'fdd3bb5f', + 'aphront-error-view-css' => 'fdd3bb5f', + 'aphront-form-view-css' => 'fdd3bb5f', + 'aphront-list-filter-view-css' => 'fdd3bb5f', + 'aphront-pager-view-css' => 'fdd3bb5f', + 'aphront-panel-view-css' => 'fdd3bb5f', + 'aphront-table-view-css' => 'fdd3bb5f', + 'aphront-tokenizer-control-css' => 'fdd3bb5f', + 'aphront-tooltip-css' => 'fdd3bb5f', + 'aphront-typeahead-control-css' => 'fdd3bb5f', 'differential-changeset-view-css' => '8aaacd1b', 'differential-core-view-css' => '8aaacd1b', 'differential-inline-comment-editor' => '322728f3', @@ -3757,19 +3788,19 @@ celerity_register_resource_map(array( 'differential-table-of-contents-css' => '8aaacd1b', 'diffusion-commit-view-css' => 'c8ce2d88', 'diffusion-icons-css' => 'c8ce2d88', - 'global-drag-and-drop-css' => '8e2735e2', + 'global-drag-and-drop-css' => 'fdd3bb5f', 'inline-comment-summary-css' => '8aaacd1b', - 'javelin-aphlict' => '0ed3458f', + 'javelin-aphlict' => '9abfe389', 'javelin-behavior' => 'cd1d650a', - 'javelin-behavior-aphlict-dropdown' => '0ed3458f', - 'javelin-behavior-aphlict-listen' => '0ed3458f', - 'javelin-behavior-aphront-basic-tokenizer' => '0ed3458f', + 'javelin-behavior-aphlict-dropdown' => '9abfe389', + 'javelin-behavior-aphlict-listen' => '9abfe389', + 'javelin-behavior-aphront-basic-tokenizer' => '9abfe389', 'javelin-behavior-aphront-drag-and-drop' => '322728f3', 'javelin-behavior-aphront-drag-and-drop-textarea' => '322728f3', - 'javelin-behavior-aphront-form-disable-on-submit' => '0ed3458f', + 'javelin-behavior-aphront-form-disable-on-submit' => '9abfe389', 'javelin-behavior-audit-preview' => 'f96657b8', 'javelin-behavior-dark-console' => 'dca4a03d', - 'javelin-behavior-device' => '0ed3458f', + 'javelin-behavior-device' => '9abfe389', 'javelin-behavior-differential-accept-with-errors' => '322728f3', 'javelin-behavior-differential-add-reviewers-and-ccs' => '322728f3', 'javelin-behavior-differential-comment-jump' => '322728f3', @@ -3785,31 +3816,32 @@ celerity_register_resource_map(array( 'javelin-behavior-diffusion-commit-graph' => 'f96657b8', 'javelin-behavior-diffusion-pull-lastmodified' => 'f96657b8', 'javelin-behavior-error-log' => 'dca4a03d', - 'javelin-behavior-global-drag-and-drop' => '0ed3458f', - 'javelin-behavior-history-install' => '0ed3458f', - 'javelin-behavior-konami' => '0ed3458f', - 'javelin-behavior-lightbox-attachments' => '0ed3458f', + 'javelin-behavior-global-drag-and-drop' => '9abfe389', + 'javelin-behavior-history-install' => '9abfe389', + 'javelin-behavior-konami' => '9abfe389', + 'javelin-behavior-lightbox-attachments' => '9abfe389', 'javelin-behavior-load-blame' => '322728f3', 'javelin-behavior-maniphest-batch-selector' => '7707de41', 'javelin-behavior-maniphest-subpriority-editor' => '7707de41', 'javelin-behavior-maniphest-transaction-controls' => '7707de41', 'javelin-behavior-maniphest-transaction-expand' => '7707de41', 'javelin-behavior-maniphest-transaction-preview' => '7707de41', - 'javelin-behavior-phabricator-active-nav' => '0ed3458f', - 'javelin-behavior-phabricator-autofocus' => '0ed3458f', - 'javelin-behavior-phabricator-keyboard-shortcuts' => '0ed3458f', - 'javelin-behavior-phabricator-nav' => '0ed3458f', + 'javelin-behavior-phabricator-active-nav' => '9abfe389', + 'javelin-behavior-phabricator-autofocus' => '9abfe389', + 'javelin-behavior-phabricator-gesture' => '9abfe389', + 'javelin-behavior-phabricator-keyboard-shortcuts' => '9abfe389', + 'javelin-behavior-phabricator-nav' => '9abfe389', 'javelin-behavior-phabricator-object-selector' => '322728f3', - 'javelin-behavior-phabricator-oncopy' => '0ed3458f', - 'javelin-behavior-phabricator-remarkup-assist' => '0ed3458f', - 'javelin-behavior-phabricator-reveal-content' => '0ed3458f', - 'javelin-behavior-phabricator-search-typeahead' => '0ed3458f', - 'javelin-behavior-phabricator-tooltips' => '0ed3458f', - 'javelin-behavior-phabricator-watch-anchor' => '0ed3458f', - 'javelin-behavior-refresh-csrf' => '0ed3458f', + 'javelin-behavior-phabricator-oncopy' => '9abfe389', + 'javelin-behavior-phabricator-remarkup-assist' => '9abfe389', + 'javelin-behavior-phabricator-reveal-content' => '9abfe389', + 'javelin-behavior-phabricator-search-typeahead' => '9abfe389', + 'javelin-behavior-phabricator-tooltips' => '9abfe389', + 'javelin-behavior-phabricator-watch-anchor' => '9abfe389', + 'javelin-behavior-refresh-csrf' => '9abfe389', 'javelin-behavior-repository-crossreference' => '322728f3', - 'javelin-behavior-toggle-class' => '0ed3458f', - 'javelin-behavior-workflow' => '0ed3458f', + 'javelin-behavior-toggle-class' => '9abfe389', + 'javelin-behavior-workflow' => '9abfe389', 'javelin-dom' => 'cd1d650a', 'javelin-event' => 'cd1d650a', 'javelin-install' => 'cd1d650a', @@ -3828,48 +3860,48 @@ celerity_register_resource_map(array( 'javelin-util' => 'cd1d650a', 'javelin-vector' => 'cd1d650a', 'javelin-workflow' => 'cd1d650a', - 'lightbox-attachment-css' => '8e2735e2', + 'lightbox-attachment-css' => 'fdd3bb5f', 'maniphest-task-summary-css' => 'eb35a026', 'maniphest-transaction-detail-css' => 'eb35a026', - 'phabricator-busy' => '0ed3458f', + 'phabricator-busy' => '9abfe389', 'phabricator-content-source-view-css' => '8aaacd1b', - 'phabricator-core-buttons-css' => '8e2735e2', - 'phabricator-core-css' => '8e2735e2', - 'phabricator-crumbs-view-css' => '8e2735e2', - 'phabricator-directory-css' => '8e2735e2', + 'phabricator-core-buttons-css' => 'fdd3bb5f', + 'phabricator-core-css' => 'fdd3bb5f', + 'phabricator-crumbs-view-css' => 'fdd3bb5f', + 'phabricator-directory-css' => 'fdd3bb5f', 'phabricator-drag-and-drop-file-upload' => '322728f3', - 'phabricator-dropdown-menu' => '0ed3458f', - 'phabricator-file-upload' => '0ed3458f', - 'phabricator-filetree-view-css' => '8e2735e2', - 'phabricator-flag-css' => '8e2735e2', - 'phabricator-form-view-css' => '8e2735e2', - 'phabricator-header-view-css' => '8e2735e2', - 'phabricator-jump-nav' => '8e2735e2', - 'phabricator-keyboard-shortcut' => '0ed3458f', - 'phabricator-keyboard-shortcut-manager' => '0ed3458f', - 'phabricator-main-menu-view' => '8e2735e2', - 'phabricator-menu-item' => '0ed3458f', - 'phabricator-nav-view-css' => '8e2735e2', - 'phabricator-notification' => '0ed3458f', - 'phabricator-notification-css' => '8e2735e2', - 'phabricator-notification-menu-css' => '8e2735e2', - 'phabricator-object-item-list-view-css' => '8e2735e2', + 'phabricator-dropdown-menu' => '9abfe389', + 'phabricator-file-upload' => '9abfe389', + 'phabricator-filetree-view-css' => 'fdd3bb5f', + 'phabricator-flag-css' => 'fdd3bb5f', + 'phabricator-form-view-css' => 'fdd3bb5f', + 'phabricator-header-view-css' => 'fdd3bb5f', + 'phabricator-jump-nav' => 'fdd3bb5f', + 'phabricator-keyboard-shortcut' => '9abfe389', + 'phabricator-keyboard-shortcut-manager' => '9abfe389', + 'phabricator-main-menu-view' => 'fdd3bb5f', + 'phabricator-menu-item' => '9abfe389', + 'phabricator-nav-view-css' => 'fdd3bb5f', + 'phabricator-notification' => '9abfe389', + 'phabricator-notification-css' => 'fdd3bb5f', + 'phabricator-notification-menu-css' => 'fdd3bb5f', + 'phabricator-object-item-list-view-css' => 'fdd3bb5f', 'phabricator-object-selector-css' => '8aaacd1b', - 'phabricator-paste-file-upload' => '0ed3458f', - 'phabricator-prefab' => '0ed3458f', + 'phabricator-paste-file-upload' => '9abfe389', + 'phabricator-prefab' => '9abfe389', 'phabricator-project-tag-css' => 'eb35a026', - 'phabricator-remarkup-css' => '8e2735e2', + 'phabricator-remarkup-css' => 'fdd3bb5f', 'phabricator-shaped-request' => '322728f3', - 'phabricator-side-menu-view-css' => '8e2735e2', - 'phabricator-standard-page-view' => '8e2735e2', - 'phabricator-textareautils' => '0ed3458f', - 'phabricator-tooltip' => '0ed3458f', - 'phabricator-transaction-view-css' => '8e2735e2', - 'phabricator-zindex-css' => '8e2735e2', - 'sprite-apps-large-css' => '8e2735e2', - 'sprite-gradient-css' => '8e2735e2', - 'sprite-icon-css' => '8e2735e2', - 'sprite-menu-css' => '8e2735e2', - 'syntax-highlighting-css' => '8e2735e2', + 'phabricator-side-menu-view-css' => 'fdd3bb5f', + 'phabricator-standard-page-view' => 'fdd3bb5f', + 'phabricator-textareautils' => '9abfe389', + 'phabricator-tooltip' => '9abfe389', + 'phabricator-transaction-view-css' => 'fdd3bb5f', + 'phabricator-zindex-css' => 'fdd3bb5f', + 'sprite-apps-large-css' => 'fdd3bb5f', + 'sprite-gradient-css' => 'fdd3bb5f', + 'sprite-icon-css' => 'fdd3bb5f', + 'sprite-menu-css' => 'fdd3bb5f', + 'syntax-highlighting-css' => 'fdd3bb5f', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d7eb68cd88..38bc10cc9f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -960,6 +960,7 @@ phutil_register_library_map(array( 'PhabricatorFormExample' => 'applications/uiexample/examples/PhabricatorFormExample.php', 'PhabricatorGarbageCollectorConfigOptions' => 'applications/config/option/PhabricatorGarbageCollectorConfigOptions.php', 'PhabricatorGarbageCollectorDaemon' => 'infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php', + 'PhabricatorGestureExample' => 'applications/uiexample/examples/PhabricatorGestureExample.php', 'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php', 'PhabricatorGitHubConfigOptions' => 'applications/config/option/PhabricatorGitHubConfigOptions.php', 'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php', @@ -2481,6 +2482,7 @@ phutil_register_library_map(array( 'PhabricatorFormExample' => 'PhabricatorUIExample', 'PhabricatorGarbageCollectorConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon', + 'PhabricatorGestureExample' => 'PhabricatorUIExample', 'PhabricatorGitHubConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorGlobalLock' => 'PhutilLock', 'PhabricatorGlobalUploadTargetView' => 'AphrontView', diff --git a/src/applications/uiexample/examples/PhabricatorGestureExample.php b/src/applications/uiexample/examples/PhabricatorGestureExample.php new file mode 100644 index 0000000000..c8d4b5e68c --- /dev/null +++ b/src/applications/uiexample/examples/PhabricatorGestureExample.php @@ -0,0 +1,35 @@ +touchable to listen for gesture events. Note that you '. + 'must be in device mode for this to work (you can narrow your browser '. + 'window if you are on a desktop).'); + } + + public function renderExample() { + + $id = celerity_generate_unique_node_id(); + + Javelin::initBehavior( + 'phabricator-gesture-example', + array( + 'rootID' => $id, + )); + + return javelin_tag( + 'div', + array( + 'sigil' => 'touchable', + 'id' => $id, + 'style' => 'width: 320px; height: 240px; margin: auto;', + ), + ''); + } +} diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 0963391c96..db32095e85 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -154,6 +154,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView { Javelin::initBehavior('toggle-class', array()); Javelin::initBehavior('konami', array()); Javelin::initBehavior('history-install'); + Javelin::initBehavior('phabricator-gesture'); $current_token = null; if ($user) { diff --git a/webroot/rsrc/js/application/core/behavior-gesture.js b/webroot/rsrc/js/application/core/behavior-gesture.js new file mode 100644 index 0000000000..9a352afe85 --- /dev/null +++ b/webroot/rsrc/js/application/core/behavior-gesture.js @@ -0,0 +1,125 @@ +/** + * @provides javelin-behavior-phabricator-gesture + * @requires javelin-behavior + * javelin-behavior-device + * javelin-stratcom + * javelin-vector + * javelin-dom + * javelin-magical-init + * @javelin + */ + +/** + * Basic gesture recognition. Unstable. Only supports swipes. + */ +JX.behavior('phabricator-gesture', function(config) { + + var target = null; + var swiping = false; + var p0; + var p1; + + JX.Stratcom.listen( + ['touchstart', 'mousedown'], + 'touchable', + function(e) { + if (JX.Device.getDevice() == 'desktop') { + return; + } + if (JX.Stratcom.pass()) { + return; + } + + if (target && e.getType() == 'touchstart') { + // This corresponds to a second finger touching while the first finger + // is held: stop the swipe. + var event_data = get_swipe_data(); + var event_target = target; + stop_swipe(); + JX.DOM.invoke(event_target, 'gesture.swipe.cancel', event_data); + return; + } + + target = e.getNode('touchable'); + p0 = JX.Vector.getPos(e); + p1 = JX.Vector.getPos(e); + }); + + JX.enableDispatch(document.body, 'mousemove'); + JX.Stratcom.listen( + ['touchmove', 'mousemove'], + null, + function(e) { + if (!target) { + return; + } + p1 = JX.Vector.getPos(e); + if (!swiping) { + + // Here, we haven't started the swipe yet. We're waiting for you to + // move your finger far enough to make it clear that you intend to + // swipe or drag, not just tap. + + var dx = (p1.x - p0.x); + var dy = (p1.y - p0.y); + var swipe_radius = 20; + if ((dx * dx) + (dy * dy) >= (swipe_radius * swipe_radius)) { + + // You've moved your finger far enough away from the origin that + // we're sure you mean to swipe, scroll, or drag, not just tap. + // Decide if you're trying to scroll or swipe. If your finger's + // motion has been primarily vertical, assume this is a scroll. If + // your finger's motion has been primarily horizontal, assume this + // is a swipe. + if (dy * dy >= dx * dx) { + stop_swipe(); + return; + } + swiping = true; + JX.DOM.invoke(target, 'gesture.swipe.start', get_swipe_data()); + } + } + if (swiping) { + if (!e.getNode('touchable')) { + p1 = JX.$V(p0); + } + JX.DOM.invoke(target, 'gesture.swipe.move', get_swipe_data()); + e.prevent(); + } + }); + + JX.Stratcom.listen( + ['touchend', 'touchcancel', 'mouseup'], + null, + function(e) { + if (!target) { + return; + } + + // NOTE: Clear the event state first so we don't keep swiping if a + // handler throws. + var event_target = target; + var event_data = get_swipe_data(); + stop_swipe(); + + JX.DOM.invoke(event_target, 'gesture.swipe.end', event_data); + }); + + function get_swipe_data() { + var dir = (p1.x > p0.x) ? 'left' : 'right'; + var length = Math.abs(p1.x - p0.x); + + return { + p0: p0, + p1: p1, + dir: dir, + length: length + }; + } + + function stop_swipe() { + target = null; + swiping = false; + } + +}); diff --git a/webroot/rsrc/js/application/uiexample/gesture-example.js b/webroot/rsrc/js/application/uiexample/gesture-example.js new file mode 100644 index 0000000000..8fdf8351ed --- /dev/null +++ b/webroot/rsrc/js/application/uiexample/gesture-example.js @@ -0,0 +1,85 @@ +/** + * @requires javelin-stratcom + * javelin-behavior + * javelin-vector + * javelin-dom + * @provides javelin-behavior-phabricator-gesture-example + */ + +JX.behavior('phabricator-gesture-example', function(config) { + + var strokes = []; + var current = []; + + var root = JX.$(config.rootID); + + var canvas = JX.$N('canvas'); + var d = JX.Vector.getDim(root); + canvas.width = d.x; + canvas.height = d.y; + root.appendChild(canvas); + + var cxt = canvas.getContext('2d'); + JX.Stratcom.listen( + 'gesture.swipe.end', + null, + function(e) { + var stroke = get_stroke(e); + strokes.push(stroke); + current = []; + redraw(); + }); + + JX.Stratcom.listen( + 'gesture.swipe.move', + null, + function(e) { + var stroke = get_stroke(e); + current = [stroke]; + redraw(); + }); + + JX.Stratcom.listen( + 'gesture.swipe.cancel', + null, + function(e) { + current = []; + redraw(); + }); + + function get_stroke(e) { + var data = e.getData(); + var p = JX.$V(root); + return [ + data.p0.x - p.x, + data.p0.y - p.y, + data.p1.x - p.x, + data.p1.y - p.y + ]; + } + + function redraw() { + cxt.fillStyle = '#dfdfdf'; + cxt.fillRect(0, 0, d.x, d.y); + + for (var ii = 0; ii < strokes.length; ii++) { + var s = strokes[ii]; + cxt.strokeStyle = 'rgba(0, 0, 0, 0.50)'; + cxt.beginPath(); + cxt.moveTo(s[0], s[1]); + cxt.lineTo(s[2], s[3]); + cxt.stroke(); + } + + for (var ii = 0; ii < current.length; ii++) { + var s = current[ii]; + cxt.strokeStyle = 'rgba(255, 0, 0, 1)'; + cxt.beginPath(); + cxt.moveTo(s[0], s[1]); + cxt.lineTo(s[2], s[3]); + cxt.stroke(); + } + } + + redraw(); +}); -- 2.51.2