sproutcore 1.11.0.rc3 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. checksums.yaml +6 -14
  2. data/CHANGELOG +5 -0
  3. data/VERSION.yml +1 -1
  4. data/lib/frameworks/sproutcore/Buildfile +3 -2
  5. data/lib/frameworks/sproutcore/CHANGELOG.md +59 -10
  6. data/lib/frameworks/sproutcore/apps/showcase/resources/main_page.js +1 -0
  7. data/lib/frameworks/sproutcore/apps/showcase/resources/stylesheet.css +9 -4
  8. data/lib/frameworks/sproutcore/frameworks/core_foundation/panes/manipulation.js +1 -1
  9. data/lib/frameworks/sproutcore/frameworks/core_foundation/system/event.js +0 -10
  10. data/lib/frameworks/sproutcore/frameworks/core_foundation/system/locale.js +1 -1
  11. data/lib/frameworks/sproutcore/frameworks/core_foundation/system/root_responder.js +10 -45
  12. data/lib/frameworks/sproutcore/frameworks/core_foundation/system/selection_set.js +3 -3
  13. data/lib/frameworks/sproutcore/frameworks/core_foundation/system/touch.js +76 -0
  14. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/system/{touch.js → touch_test.js} +64 -0
  15. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/views/pane/append_remove.js +1 -1
  16. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/views/pane/design_mode_test.js +61 -24
  17. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/views/view/createChildViews.js +1 -2
  18. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/views/view/destroy.js +0 -3
  19. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/views/view/{layoutStyle.js → layout_style_test.js} +4 -4
  20. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/views/view/layout_test.js +602 -0
  21. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/views/view/viewDidResize.js +0 -23
  22. data/lib/frameworks/sproutcore/frameworks/core_foundation/views/view.js +18 -17
  23. data/lib/frameworks/sproutcore/frameworks/core_foundation/views/view/animation.js +5 -5
  24. data/lib/frameworks/sproutcore/frameworks/core_foundation/views/view/design_mode.js +64 -24
  25. data/lib/frameworks/sproutcore/frameworks/core_foundation/views/view/layout.js +904 -871
  26. data/lib/frameworks/sproutcore/frameworks/core_foundation/views/view/layout_style.js +1 -3
  27. data/lib/frameworks/sproutcore/frameworks/core_foundation/views/view/statechart.js +40 -24
  28. data/lib/frameworks/sproutcore/frameworks/datastore/data_sources/fixtures.js +2 -2
  29. data/lib/frameworks/sproutcore/frameworks/datastore/models/record.js +5 -5
  30. data/lib/frameworks/sproutcore/frameworks/datastore/system/nested_store.js +14 -14
  31. data/lib/frameworks/sproutcore/frameworks/datastore/system/query.js +1 -1
  32. data/lib/frameworks/sproutcore/frameworks/datastore/system/record_array.js +2 -2
  33. data/lib/frameworks/sproutcore/frameworks/datastore/system/store.js +36 -33
  34. data/lib/frameworks/sproutcore/frameworks/datastore/tests/models/record/writeAttribute.js +1 -1
  35. data/lib/frameworks/sproutcore/frameworks/datastore/tests/system/nested_store/autonomous_dataSourceCallbacks.js +6 -6
  36. data/lib/frameworks/sproutcore/frameworks/datastore/tests/system/store/commitChangesFromNestedStore.js +1 -1
  37. data/lib/frameworks/sproutcore/frameworks/datastore/tests/system/store/commitRecord.js +1 -1
  38. data/lib/frameworks/sproutcore/frameworks/datastore/tests/system/store/dataSourceCallbacks.js +7 -7
  39. data/lib/frameworks/sproutcore/frameworks/datastore/tests/system/store/destroyRecord.js +2 -2
  40. data/lib/frameworks/sproutcore/frameworks/datastore/tests/system/store/recordDidChange.js +2 -2
  41. data/lib/frameworks/sproutcore/frameworks/datastore/tests/system/store/retrieveRecord.js +4 -4
  42. data/lib/frameworks/sproutcore/frameworks/datetime/frameworks/core/system/datetime.js +11 -11
  43. data/lib/frameworks/sproutcore/frameworks/designer/coders/object.js +1 -1
  44. data/lib/frameworks/sproutcore/frameworks/desktop/panes/menu.js +30 -1
  45. data/lib/frameworks/sproutcore/frameworks/desktop/panes/picker.js +12 -1
  46. data/lib/frameworks/sproutcore/frameworks/desktop/views/menu_scroll.js +5 -3
  47. data/lib/frameworks/sproutcore/frameworks/desktop/views/menu_scroller_view.js +0 -36
  48. data/lib/frameworks/sproutcore/frameworks/desktop/views/scroll_view.js +3 -3
  49. data/lib/frameworks/sproutcore/frameworks/desktop/views/segmented.js +1 -1
  50. data/lib/frameworks/sproutcore/frameworks/desktop/views/static_content.js +1 -1
  51. data/lib/frameworks/sproutcore/frameworks/foundation/gestures/pinch_gesture.js +286 -0
  52. data/lib/frameworks/sproutcore/frameworks/foundation/gestures/swipe_gesture.js +449 -0
  53. data/lib/frameworks/sproutcore/frameworks/foundation/gestures/tap_gesture.js +259 -0
  54. data/lib/frameworks/sproutcore/frameworks/foundation/mixins/gesturable.js +218 -30
  55. data/lib/frameworks/sproutcore/frameworks/foundation/system/gesture.js +259 -158
  56. data/lib/frameworks/sproutcore/frameworks/foundation/system/string.js +58 -50
  57. data/lib/frameworks/sproutcore/frameworks/foundation/system/utils/string_measurement.js +1 -1
  58. data/lib/frameworks/sproutcore/frameworks/foundation/tests/gestures/pinch_gesture_test.js +321 -0
  59. data/lib/frameworks/sproutcore/frameworks/foundation/tests/gestures/swipe_gesture_test.js +154 -0
  60. data/lib/frameworks/sproutcore/frameworks/foundation/tests/gestures/tap_gesture_test.js +55 -0
  61. data/lib/frameworks/sproutcore/frameworks/foundation/tests/mixins/gesturable_test.js +233 -0
  62. data/lib/frameworks/sproutcore/frameworks/foundation/tests/mixins/staticLayout.js +2 -2
  63. data/lib/frameworks/sproutcore/frameworks/foundation/tests/system/gesture_test.js +254 -0
  64. data/lib/frameworks/sproutcore/frameworks/foundation/views/container.js +1 -0
  65. data/lib/frameworks/sproutcore/frameworks/foundation/views/text_field.js +1 -1
  66. data/lib/frameworks/sproutcore/frameworks/legacy/object_keys_polyfill.js +51 -0
  67. data/lib/frameworks/sproutcore/frameworks/{core_foundation/system/req_anim_frame.js → legacy/request_animation_frame_polyfill.js} +10 -3
  68. data/lib/frameworks/sproutcore/frameworks/media/views/audio.js +19 -25
  69. data/lib/frameworks/sproutcore/frameworks/media/views/controls.js +7 -7
  70. data/lib/frameworks/sproutcore/frameworks/media/views/mini_controls.js +3 -3
  71. data/lib/frameworks/sproutcore/frameworks/media/views/simple_controls.js +9 -9
  72. data/lib/frameworks/sproutcore/frameworks/runtime/core.js +2 -2
  73. data/lib/frameworks/sproutcore/frameworks/runtime/debug/test_suites/array/insertAt.js +2 -2
  74. data/lib/frameworks/sproutcore/frameworks/runtime/debug/test_suites/array/removeAt.js +1 -1
  75. data/lib/frameworks/sproutcore/frameworks/runtime/ext/array.js +1 -1
  76. data/lib/frameworks/sproutcore/frameworks/runtime/mixins/array.js +2 -2
  77. data/lib/frameworks/sproutcore/frameworks/runtime/mixins/freezable.js +2 -2
  78. data/lib/frameworks/sproutcore/frameworks/runtime/mixins/observable.js +66 -5
  79. data/lib/frameworks/sproutcore/frameworks/runtime/mixins/tree.js +44 -0
  80. data/lib/frameworks/sproutcore/frameworks/runtime/private/observer_queue.js +4 -1
  81. data/lib/frameworks/sproutcore/frameworks/runtime/system/binding.js +0 -25
  82. data/lib/frameworks/sproutcore/frameworks/runtime/system/enumerator.js +1 -1
  83. data/lib/frameworks/sproutcore/frameworks/runtime/system/error.js +67 -15
  84. data/lib/frameworks/sproutcore/frameworks/runtime/system/index_set.js +6 -11
  85. data/lib/frameworks/sproutcore/frameworks/runtime/system/set.js +6 -6
  86. data/lib/frameworks/sproutcore/frameworks/runtime/system/string.js +1 -1
  87. data/lib/frameworks/sproutcore/frameworks/runtime/tests/mixins/observable/{observable.js → observable_test.js} +110 -16
  88. data/lib/frameworks/sproutcore/frameworks/runtime/tests/system/error.js +21 -0
  89. data/lib/frameworks/sproutcore/frameworks/statechart/system/statechart.js +2 -2
  90. data/lib/frameworks/sproutcore/frameworks/template_view/ext/handlebars.js +1 -1
  91. data/lib/frameworks/sproutcore/frameworks/testing/system/plan.js +1 -1
  92. data/lib/sproutcore/render_engines/haml.rb +1 -1
  93. metadata +610 -604
  94. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/views/view/layout.js +0 -210
  95. data/lib/frameworks/sproutcore/frameworks/core_foundation/tests/views/view/layoutDidChange.js +0 -275
  96. data/lib/frameworks/sproutcore/frameworks/foundation/gestures/pinch.js +0 -119
  97. data/lib/frameworks/sproutcore/frameworks/foundation/gestures/swipe.js +0 -234
  98. data/lib/frameworks/sproutcore/frameworks/foundation/gestures/tap.js +0 -157
@@ -43,7 +43,7 @@ SC.SCROLL = {
43
43
 
44
44
  @static
45
45
  @type Number
46
- @default 5
46
+ @default 3
47
47
  */
48
48
  SCALE_GESTURE_THRESHOLD: 3
49
49
 
@@ -1705,7 +1705,7 @@ SC.ScrollView = SC.View.extend({
1705
1705
  isVisible: !this.get('autohidesHorizontalScroller'),
1706
1706
  layoutDirection: SC.LAYOUT_HORIZONTAL,
1707
1707
  value: this.get('horizontalScrollOffset'),
1708
- valueBinding: '.owner.horizontalScrollOffset', // Bind the value of the scroller to our horizontal offset.
1708
+ valueBinding: '.parentView.horizontalScrollOffset', // Bind the value of the scroller to our horizontal offset.
1709
1709
  minimum: this.get('minimumHorizontalScrollOffset'),
1710
1710
  maximum: this.get('maximumHorizontalScrollOffset')
1711
1711
  });
@@ -1732,7 +1732,7 @@ SC.ScrollView = SC.View.extend({
1732
1732
  isVisible: !this.get('autohidesVerticalScroller'),
1733
1733
  layoutDirection: SC.LAYOUT_VERTICAL,
1734
1734
  value: this.get('verticalScrollOffset'),
1735
- valueBinding: '.owner.verticalScrollOffset', // Bind the value of the scroller to our vertical offset.
1735
+ valueBinding: '.parentView.verticalScrollOffset', // Bind the value of the scroller to our vertical offset.
1736
1736
  minimum: this.get('minimumVerticalScrollOffset'),
1737
1737
  maximum: this.get('maximumVerticalScrollOffset')
1738
1738
  });
@@ -540,7 +540,7 @@ SC.SegmentedView = SC.View.extend(SC.Control,
540
540
  Whenever the view resizes, we need to check to see if we're overflowing.
541
541
  */
542
542
  viewDidResize: function () {
543
- this._viewFrameDidChange();
543
+ this._sc_viewFrameDidChange();
544
544
 
545
545
  var isHorizontal = this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL,
546
546
  visibleDim = isHorizontal ? this.$().width() : this.$().height();
@@ -70,7 +70,7 @@ SC.StaticContentView = SC.View.extend(
70
70
  automatically.
71
71
  */
72
72
  contentLayoutDidChange: function() {
73
- this._viewFrameDidChange();
73
+ this._sc_viewFrameDidChange();
74
74
  },
75
75
 
76
76
  // ..........................................................
@@ -0,0 +1,286 @@
1
+ // ==========================================================================
2
+ // Project: SproutCore - JavaScript Application Framework
3
+ // Copyright: ©2006-2011 Strobe Inc. and contributors.
4
+ // Portions ©2008-2011 Apple Inc. All rights reserved.
5
+ // License: Licensed under MIT license (see license.js)
6
+ // ==========================================================================
7
+
8
+ sc_require("system/gesture");
9
+
10
+ /*
11
+ TODO Document this class
12
+ */
13
+
14
+ /**
15
+ @class
16
+ @extends SC.Gesture
17
+ */
18
+ SC.PinchGesture = SC.Gesture.extend(
19
+ /** @scope SC.PinchGesture.prototype */{
20
+
21
+ /** @private Whether we have started pinching or not.
22
+
23
+ @type Boolean
24
+ @default false
25
+ */
26
+ _sc_isPinching: false,
27
+
28
+ /** @private The previous distance between touches.
29
+
30
+ @type Number
31
+ @default null
32
+ */
33
+ _sc_pinchAnchorD: null,
34
+
35
+ /** @private The initial scale of the view before pinching.
36
+
37
+ @type Number
38
+ @default null
39
+ */
40
+ _sc_pinchAnchorScale: null,
41
+
42
+ /**
43
+ @type String
44
+ @default "pinch"
45
+ */
46
+ name: "pinch",
47
+
48
+ /**
49
+ The amount of time in milliseconds that touches should stop moving before a `pinchEnd` event
50
+ will fire. When a pinch gesture begins, the `pinchStart` event is fired and as long as the
51
+ touches continue to change distance, multiple `pinch` events will fire. If the touches remain
52
+ active but don't change distance any longer, then after `pinchDelay` milliseconds the `pinchEnd`
53
+ event will fire.
54
+
55
+ @type Number
56
+ @default 500
57
+ */
58
+ pinchDelay: 500,
59
+
60
+ /**
61
+ The number of pixels that multiple touches need to expand or contract in order to trigger the
62
+ beginning of a pinch.
63
+
64
+ @type Number
65
+ @default 3
66
+ */
67
+ // pinchStartThreshold: 3,
68
+
69
+ /** @private Cleans up the touch session. */
70
+ _sc_cleanUpTouchSession: function () {
71
+ // If we were pinching before, end the pinch immediately.
72
+ if (this._sc_isPinching) {
73
+ this._sc_pinchingTimer.invalidate();
74
+ this._sc_pinchingTimer = null;
75
+ this._sc_lastPinchTime = null;
76
+ this._sc_isPinching = false;
77
+
78
+ // Trigger the gesture, 'pinchEnd'.
79
+ this.end();
80
+ }
81
+
82
+ // Clean up.
83
+ this._sc_pinchAnchorD = null;
84
+ },
85
+
86
+ /** @private Shared function for when a touch ends or cancels. */
87
+ _sc_touchFinishedInSession: function (touch, touchesInSession) {
88
+ // If there are more than two touches, keep monitoring for pinches by updating _sc_pinchAnchorD.
89
+ if (touchesInSession.length > 1) {
90
+ // Get the averaged touches for the the view. Because pinch is always interested in every touch
91
+ // the touchesInSession will equal the touches for the view.
92
+ var avgTouch = touch.averagedTouchesForView(this.view);
93
+
94
+ this._sc_pinchAnchorD = avgTouch.d;
95
+
96
+ // Disregard incoming touches by clearing out _sc_pinchAnchorD and end an active pinch immediately.
97
+ } else {
98
+ this._sc_cleanUpTouchSession();
99
+ }
100
+ },
101
+
102
+ /** @private Triggers pinchEnd and resets _sc_isPinching if enough time has passed. */
103
+ _sc_triggerPinchEnd: function () {
104
+ // If a pinch came in since the time the timer was registered, set up a new timer for the
105
+ // remaining time.
106
+ if (this._sc_lastPinchTime) {
107
+ var timePassed = Date.now() - this._sc_lastPinchTime,
108
+ pinchDelay = this.get('pinchDelay');
109
+
110
+ // Prepare to send 'pinchEnd' again.
111
+ this._sc_pinchingTimer = SC.Timer.schedule({
112
+ target: this,
113
+ action: this._sc_triggerPinchEnd,
114
+ interval: pinchDelay - timePassed // Trigger the timer the amount of time left since the last pinch
115
+ });
116
+
117
+ // Clear out the last pinch time.
118
+ this._sc_lastPinchTime = null;
119
+
120
+ // No additional pinches appeared in the amount of time.
121
+ } else {
122
+ // Trigger the gesture, 'pinchEnd'.
123
+ this.end();
124
+
125
+ // Clear out the pinching session.
126
+ this._sc_isPinching = false;
127
+ this._sc_pinchingTimer = null;
128
+ }
129
+ },
130
+
131
+ /**
132
+ The pinch gesture is always interested in the touch session. When a new touch is added, the
133
+ distance between all of the touches is registered in order to check for distance changes
134
+ equating to a pinch gesture.
135
+
136
+ @param {SC.Touch} touch The touch to be added to the session.
137
+ @param {Array} touchesInSession The touches already in the session.
138
+ @returns {Boolean} True.
139
+ @see SC.Gesture#touchAddedToSession
140
+ */
141
+ touchAddedToSession: function (touch, touchesInSession) {
142
+ // Get the averaged touches for the the view. Because pinch is always interested in every touch
143
+ // the touchesInSession will equal the touches for the view.
144
+ var avgTouch = touch.averagedTouchesForView(this.view, true);
145
+
146
+ this._sc_pinchAnchorD = avgTouch.d;
147
+
148
+ return true;
149
+ },
150
+
151
+ /**
152
+ If a touch cancels, the pinch remains interested (even if there's only one touch left, because a
153
+ second touch may appear again), but updates its internal variable for tracking for pinch
154
+ movements.
155
+
156
+ @param {SC.Touch} touch The touch to be removed from the session.
157
+ @param {Array} touchesInSession The touches in the session.
158
+ @returns {Boolean} True
159
+ @see SC.Gesture#touchCancelledInSession
160
+ */
161
+ touchCancelledInSession: function (touch, touchesInSession) {
162
+ this._sc_touchFinishedInSession(touch, touchesInSession);
163
+
164
+ return true;
165
+ },
166
+
167
+ /**
168
+ If a touch ends, the pinch remains interested (even if there's only one touch left, because a
169
+ second touch may appear again), but updates its internal variable for tracking for pinch
170
+ movements.
171
+
172
+ @param {SC.Touch} touch The touch to be removed from the session.
173
+ @param {Array} touchesInSession The touches in the session.
174
+ @returns {Boolean} True
175
+ @see SC.Gesture#touchEndedInSession
176
+ */
177
+ touchEndedInSession: function (touch, touchesInSession) {
178
+ this._sc_touchFinishedInSession(touch, touchesInSession);
179
+
180
+ return true;
181
+ },
182
+
183
+ /**
184
+ The pinch is only interested in more than one touch moving. If there are multiple touches
185
+ moving and the distance between the touches has changed then a `pinchStart` event will fire.
186
+ If the touches keep expanding or contracting, the `pinch` event will repeatedly fire. Finally,
187
+ if the touch distance stops changing and enough time passes (value of `pinchDelay`), the
188
+ `pinchEnd` event will fire.
189
+
190
+ Therefore, it's possible for a pinch gesture to start and end more than once in a single touch
191
+ session. For example, a person may touch two fingers down, expand them to zoom in (`pinchStart`
192
+ and multiple `pinch` events fire) and then if they stop or move their fingers in one direction
193
+ in tandem to scroll content (`pinchEnd` event fires after `pinchDelay` exceeded). If the person
194
+ then starts expanding their fingers again without lifting them, a new set of pinch events will
195
+ fire.
196
+
197
+ @param {Array} touchesInSession All touches in the session.
198
+ @returns {Boolean} True.
199
+ @see SC.Gesture#touchesMovedInSession
200
+ */
201
+ touchesMovedInSession: function (touchesInSession) {
202
+ // console.log('touchesMovedInSession: %@'.fmt(touchesInSession.length));
203
+ // We should pay attention to the movement.
204
+ if (touchesInSession.length > 1) {
205
+ // Get the averaged touches for the the view. Because pinch is always interested in every touch
206
+ // the touchesInSession will equal the touches for the view.
207
+ var avgTouch = SC.Touch.averagedTouch(touchesInSession); // touchesInSession[0].averagedTouchesForView(this.view);
208
+
209
+ var touchDeltaD = this._sc_pinchAnchorD - avgTouch.d,
210
+ absDeltaD = Math.abs(touchDeltaD);
211
+
212
+ // console.log(' this._sc_pinchAnchorD, %@ - avgTouch.d, %@ = touchDeltaD, %@'.fmt(this._sc_pinchAnchorD, avgTouch.d, touchDeltaD));
213
+ if (absDeltaD > 0) {
214
+ // Trigger the gesture, 'pinchStart', once.
215
+ if (!this._sc_isPinching) {
216
+ this.start();
217
+
218
+ // Prepare to send 'pinchEnd'.
219
+ this._sc_pinchingTimer = SC.Timer.schedule({
220
+ target: this,
221
+ action: this._sc_triggerPinchEnd,
222
+ interval: this.get('pinchDelay')
223
+ });
224
+
225
+ // Track that we are pinching.
226
+ this._sc_isPinching = true;
227
+
228
+ // Update the last pinch time so that when the timer expires, it doesn't fire pinchEnd.
229
+ // This is faster than invalidating and creating a new timer each time this method is called.
230
+ } else {
231
+ this._sc_lastPinchTime = Date.now();
232
+ }
233
+
234
+ // The percentage difference in touch distance.
235
+ var scalePercentChange = avgTouch.d / this._sc_pinchAnchorD,
236
+ scale = this._sc_pinchAnchorScale * scalePercentChange;
237
+
238
+ // Trigger the gesture, 'pinch'.
239
+ this.trigger(scale, touchesInSession.length);
240
+
241
+ // Reset the anchor.
242
+ this._sc_pinchAnchorD = avgTouch.d;
243
+ this._sc_pinchAnchorScale = scale;
244
+ }
245
+ }
246
+
247
+ return true;
248
+ },
249
+
250
+ /**
251
+ Cleans up all touch session variables.
252
+
253
+ @returns {void}
254
+ @see SC.Gesture#touchSessionCancelled
255
+ */
256
+ touchSessionCancelled: function () {
257
+ // Clean up.
258
+ this._sc_cleanUpTouchSession();
259
+ },
260
+
261
+ /**
262
+ Cleans up all touch session variables and triggers the gesture.
263
+
264
+ @returns {void}
265
+ @see SC.Gesture#touchSessionEnded
266
+ */
267
+ touchSessionEnded: function () {
268
+ // Clean up.
269
+ this._sc_cleanUpTouchSession();
270
+ },
271
+
272
+ /**
273
+ Registers the scale of the view when it starts.
274
+
275
+ @param {SC.Touch} touch The touch that started the session.
276
+ @returns {void}
277
+ @see SC.Gesture#touchSessionStarted
278
+ */
279
+ touchSessionStarted: function (touch) {
280
+ var viewLayout = this.view.get('layout');
281
+
282
+ /*jshint eqnull:true*/
283
+ this._sc_pinchAnchorScale = viewLayout.scale == null ? 1 : viewLayout.scale;
284
+ }
285
+
286
+ });
@@ -0,0 +1,449 @@
1
+ // ==========================================================================
2
+ // Project: SproutCore - JavaScript Application Framework
3
+ // Copyright: ©2006-2011 Strobe Inc. and contributors.
4
+ // Portions ©2008-2011 Apple Inc. All rights reserved.
5
+ // License: Licensed under MIT license (see license.js)
6
+ // ==========================================================================
7
+
8
+ sc_require("system/gesture");
9
+
10
+
11
+ /**
12
+ @static
13
+ @type String
14
+ @constant
15
+ */
16
+ SC.SWIPE_HORIZONTAL = [0, 180];
17
+
18
+ /**
19
+ @static
20
+ @type String
21
+ @constant
22
+ */
23
+ SC.SWIPE_VERTICAL = [90, -90];
24
+
25
+ /**
26
+ @static
27
+ @type String
28
+ @constant
29
+ */
30
+ SC.SWIPE_ANY = null;
31
+
32
+ /**
33
+ @static
34
+ @type String
35
+ @constant
36
+ */
37
+ SC.SWIPE_LEFT = [180];
38
+
39
+ /**
40
+ @static
41
+ @type String
42
+ @constant
43
+ */
44
+ SC.SWIPE_RIGHT = [0];
45
+
46
+ /**
47
+ @static
48
+ @type String
49
+ @constant
50
+ */
51
+ SC.SWIPE_UP = [-90];
52
+
53
+ /**
54
+ @static
55
+ @type String
56
+ @constant
57
+ */
58
+ SC.SWIPE_DOWN = [90];
59
+
60
+ /**
61
+ ## What is a "swipe"?
62
+
63
+ A swipe is one or more quick unidirectionally moving touches that end abruptly. By this, it is
64
+ meant that at some point the touches begin to move quickly, `swipeVelocity` in a single direction,
65
+ `angles`, and cover a fair amount of distance, `swipeDistance`, before ending.
66
+
67
+ The single direction that the touches move in, must follow one of the angles specified in the
68
+ `angles` array. However, the touches do not need to precisely match an angle and may vary by
69
+ an amount above or below the angle as defined by the `tolerance` property.
70
+
71
+ Because the swipe is the last moment of the touch session, the swipe gesture is always interested
72
+ in a touch session. As long as the last distance traveled is great enough and at an
73
+ approved angle, then a swipe will trigger.
74
+
75
+ @class
76
+ @extends SC.Gesture
77
+ */
78
+ SC.SwipeGesture = SC.Gesture.extend(
79
+ /** @scope SC.SwipeGesture.prototype */ {
80
+
81
+ //
82
+ // - Properties --------------------------------------------------------------------
83
+ //
84
+
85
+ /** @private The last approved angle. Is set as long as a swipe appears valid. */
86
+ _sc_lastAngle: null,
87
+
88
+ /** @private The last computed distance. Is set as long as a swipe appears valid. */
89
+ _sc_lastDistance: null,
90
+
91
+ /** @private The number of touches in the current swipe. */
92
+ _sc_numberOfTouches: 0,
93
+
94
+ /** @private The initial point where a swipe appears to begin. */
95
+ _sc_swipeAnchorX: null,
96
+
97
+ /** @private The initial point where a swipe appears to begin. */
98
+ _sc_swipeAnchorY: null,
99
+
100
+ /** @private The last time a movement in a swipe was recorded. */
101
+ _sc_swipeLastMovedAt: null,
102
+
103
+ /**
104
+ The angles that the swipe will accept, between 0° and ±180°. The angles start from the right
105
+ side (0°) and end at the left side (±180°). With the positive angles passing through *down*
106
+ (+90°) and the negative angles passing through *up* (-90°). The following ASCII art shows the
107
+ directions of the angles,
108
+
109
+
110
+ -90° (up)
111
+ |
112
+ |
113
+ (left) ± 180° --------- 0° (right)
114
+ |
115
+ |
116
+ (down) +90°
117
+
118
+ To make this easier, there are several predefined angles arrays that you can use,
119
+
120
+ * SC.SWIPE_HORIZONTAL ([180, 0]), i.e. left or right
121
+ * SC.SWIPE_VERTICAL ([-90, 90]), i.e. up or down
122
+ * SC.SWIPE_ANY (null), i.e. 0° to up, down, left or right
123
+ * SC.SWIPE_LEFT ([180]), i.e. left only
124
+ * SC.SWIPE_RIGHT ([0]), i.e. right only
125
+ * SC.SWIPE_UP ([-90]), i.e. up only
126
+ * SC.SWIPE_DOWN ([90]), down only
127
+
128
+ However, you can provide any combination of angles that you want. For example, to support
129
+ 45° angled swipes to the right and straight swipes to the left, we would use,
130
+
131
+ angles: [180, -45, 45] // 180° straight left, -45° up & right, 45° down & right
132
+
133
+ ## How to use the angles.
134
+
135
+ When the `swipe` event fires, the angle of the swipe is passed to your view allowing you to
136
+ recognize which of the supported angles matched the swipe.
137
+
138
+ Note, there is one special case, as defined by `SC.SWIPE_ANY`, which is to set angles to `null`
139
+ in order to support swipes in *any* direction. The code will look for a swipe (unidirectional
140
+ fast motion) in any direction and pass the observed angle to the `swipe` handler.
141
+
142
+ @type Array
143
+ @default 24
144
+ */
145
+ // This is a computed property in order to provide backwards compatibility for direction.
146
+ // When direction is removed completely, this can become a simple `SC.SWIPE_HORIZONTAL` value.
147
+ angles: function (key, value) {
148
+ var direction = this.get('direction'),
149
+ ret = SC.SWIPE_HORIZONTAL;
150
+
151
+ // Backwards compatibility support
152
+ if (!SC.none(direction)) {
153
+ //@if(debug)
154
+ SC.warn('Developer Warning: The direction property of SC.SwipeGesture has been renamed to angles.');
155
+ //@endif
156
+
157
+ return direction;
158
+ }
159
+
160
+ if (!SC.none(value)) { ret = value; }
161
+
162
+ return ret;
163
+ }.property('direction').cacheable(),
164
+
165
+
166
+ /** @deprecated Version 1.11. Please use the `angles` property instead.
167
+ @type Array
168
+ @default SC.SWIPE_HORIZONTAL
169
+ */
170
+ direction: SC.SWIPE_HORIZONTAL,
171
+
172
+ /**
173
+ @type String
174
+ @default "swipe"
175
+ @readOnly
176
+ */
177
+ name: "swipe",
178
+
179
+ /**
180
+ The distance in pixels that touches must move in a single direction to be far enough in order to
181
+ be considered a swipe. If the touches don't move `swipeDistance` amount of pixels, then the
182
+ gesture will not trigger.
183
+
184
+ @type Number
185
+ @default 40
186
+ */
187
+ swipeDistance: 40,
188
+
189
+ /**
190
+ The velocity in pixels per millisecond that touches must be traveling to begin a swipe motion.
191
+ If the touches are moving slower than the velocity, the swipe start point won't be set.
192
+
193
+ @type Number
194
+ @default 0.5
195
+ */
196
+ swipeVelocity: 0.5,
197
+
198
+ /**
199
+ Amount of degrees that a touch is allowed to vary off of the target angle(s).
200
+
201
+ @type Number
202
+ @default 15
203
+ */
204
+ tolerance: 15,
205
+
206
+ //
207
+ // - Methods --------------------------------------------------------------------
208
+ //
209
+
210
+ /** @private Cleans up the touch session. */
211
+ _sc_cleanUpTouchSession: function (wasCancelled) {
212
+ // Clean up.
213
+ this._sc_numberOfTouches = 0;
214
+ this._sc_lastDistance = null;
215
+ this._sc_swipeStartedAt = null;
216
+ this._sc_lastAngle = null;
217
+ this._sc_swipeAnchorX = null;
218
+ this._sc_swipeAnchorY = null;
219
+ },
220
+
221
+ /** @private Timer used to tell if swipe was too slow. */
222
+ _sc_swipeTooSlow: function () {
223
+ // The session took to long to finish from when a swipe appeared to start. Reset.
224
+ this._sc_cleanUpTouchSession();
225
+ },
226
+
227
+ /**
228
+ The swipe gesture is always interested in a touch session, because it is only concerned in how
229
+ the session ends. If it ends with a fast unidirectional sliding movement, then it is a swipe.
230
+
231
+ Note, that for multiple touches, touches are expected to start while other touches are already
232
+ moving. When touches are added we update the swipe start position. This means that inadvertent
233
+ taps that occur while swiping could break a swipe recognition by making the swipe too short to
234
+ register.
235
+
236
+ @param {SC.Touch} touch The touch to be added to the session.
237
+ @param {Array} touchesInSession The touches already in the session.
238
+ @returns {Boolean} True as long as the new touch doesn't start too late after the first touch.
239
+ @see SC.Gesture#touchAddedToSession
240
+ */
241
+ // TODO: What about first touch starts moving, second touch taps, first touch finishes?
242
+ // TODO: What about first touch starts tap, second touch starts moving, first touch finishes tap, second touch finishes?
243
+ touchAddedToSession: function (touch, touchesInSession) {
244
+ // Get the averaged touches for the the view. Because pinch is always interested in every touch
245
+ // the touchesInSession will equal the touches for the view.
246
+ var avgTouch = touch.averagedTouchesForView(this.view, true);
247
+
248
+ this._sc_swipeAnchorX = avgTouch.x;
249
+ this._sc_swipeAnchorY = avgTouch.y;
250
+
251
+ return true;
252
+ },
253
+
254
+ /**
255
+ The swipe gesture is always interested in a touch session, because it is only concerned in how
256
+ the session ends. If it ends with a fast unidirectional sliding movement, then it is a swipe.
257
+
258
+ Note, that a touch may cancel while swiping (went off screen inadvertently). Because of this we
259
+ don't immediately reduce the number of touches in the swipe, because if the rest of the touches
260
+ end right away in a swipe, it's best to consider the cancelled touch as part of the group.
261
+
262
+ @param {SC.Touch} touch The touch to be removed from the session.
263
+ @param {Array} touchesInSession The touches in the session.
264
+ @returns {Boolean} True
265
+ @see SC.Gesture#touchCancelledInSession
266
+ */
267
+ touchCancelledInSession: function (touch, touchesInSession) {
268
+ return true;
269
+ },
270
+
271
+ /**
272
+ The swipe gesture is always interested in a touch session, because it is only concerned in how
273
+ the session ends. If it ends with a fast unidirectional sliding movement, then it is a swipe.
274
+
275
+ Note, that touches are expected to end while swiping. Because of this we don't immediately
276
+ reduce the number of touches in the swipe, because if the rest of the touches also end right
277
+ away in a swiping motion, it's best to consider this ended touch as part of the group.
278
+
279
+ @param {SC.Touch} touch The touch to be removed from the session.
280
+ @param {Array} touchesInSession The touches in the session.
281
+ @returns {Boolean} True if it is the first touch to end or a subsequent touch that ends not too long after the first touch ended.
282
+ @see SC.Gesture#touchEndedInSession
283
+ */
284
+ touchEndedInSession: function (touch, touchesInSession) {
285
+ return true;
286
+ },
287
+
288
+ /** @private Test the given angle against an approved angle. */
289
+ _sc_testAngle: function (absoluteCurrentAngle, currentIsPositive, approvedAngle, tolerance) {
290
+ var angleIsPositive = approvedAngle >= 0,
291
+ absoluteAngle = !angleIsPositive ? Math.abs(approvedAngle) : approvedAngle,
292
+ upperBound = absoluteAngle + tolerance,
293
+ lowerBound = absoluteAngle - tolerance,
294
+ ret = false;
295
+
296
+ if (lowerBound <= absoluteCurrentAngle && absoluteCurrentAngle <= upperBound) {
297
+ // Special case: ex. Don't confuse -45° with 45° or vice versa.
298
+ var upperBoundIsPositive = upperBound >= 0 && upperBound <= 180,
299
+ lowerBoundIsPositive = lowerBound >= 0;
300
+
301
+ ret = upperBoundIsPositive === lowerBoundIsPositive ? currentIsPositive === angleIsPositive : true;
302
+ }
303
+
304
+ return ret;
305
+ },
306
+
307
+ /**
308
+ The swipe gesture is always interested in a touch session, because it is only concerned in how
309
+ the session ends. If it ends with a fast unidirectional sliding movement, then it is a swipe.
310
+
311
+ @param {Array} touchesInSession All touches in the session.
312
+ @returns {Boolean} True as long as none of the touches have moved too far off-axis to be a clean swipe.
313
+ @see SC.Gesture#touchesMovedInSession
314
+ */
315
+ touchesMovedInSession: function (touchesInSession) {
316
+ // Get the averaged touches for the the view. Because swipe is always interested in every touch
317
+ // (or none) the touchesInSession will equal the touches for the view.
318
+ var angles = this.get('direction'),
319
+ avgTouch = touchesInSession[0].averagedTouchesForView(this.view),
320
+ xDiff = avgTouch.x - this._sc_swipeAnchorX,
321
+ yDiff = avgTouch.y - this._sc_swipeAnchorY,
322
+ currentAngle = Math.atan2(yDiff, xDiff) * (180 / Math.PI),
323
+ absoluteCurrentAngle = Math.abs(currentAngle),
324
+ currentIsPositive = currentAngle >= 0,
325
+ tolerance = this.get('tolerance'),
326
+ approvedAngle = null,
327
+ angle;
328
+
329
+ // There is one special case, when angles is null, allow all angles.
330
+ if (angles === null) {
331
+ // Use the last angle against itself.
332
+ angle = this._sc_lastAngle;
333
+
334
+ if (angle !== null) {
335
+ var withinLastAngle = this._sc_testAngle(absoluteCurrentAngle, currentIsPositive, angle, tolerance);
336
+
337
+ // If still within the start angle, leave it going.
338
+ if (withinLastAngle) {
339
+ approvedAngle = angle;
340
+ } else {
341
+ approvedAngle = currentAngle;
342
+ }
343
+ } else {
344
+ approvedAngle = currentAngle;
345
+ }
346
+
347
+ // Check against approved angles.
348
+ } else {
349
+ for (var i = 0, len = angles.length; i < len; i++) {
350
+ angle = angles[i];
351
+
352
+ // If the current angle is within the tolerance of the given angle, it's a match.
353
+ if (this._sc_testAngle(absoluteCurrentAngle, currentIsPositive, angle, tolerance)) {
354
+ approvedAngle = angle;
355
+
356
+ break; // No need to continue.
357
+ }
358
+ }
359
+ }
360
+
361
+ // Got angle.
362
+ if (approvedAngle !== null) {
363
+ // Same angle. Ensure we're traveling fast enough to keep the angle.
364
+ if (this._sc_lastAngle === approvedAngle) {
365
+ // Get distance between the anchor and current average point.
366
+ var dx = Math.abs(xDiff),
367
+ dy = Math.abs(yDiff),
368
+ now = Date.now(),
369
+ distance,
370
+ velocity;
371
+
372
+ distance = Math.pow(dx * dx + dy * dy, 0.5);
373
+ velocity = distance / (now - this._sc_swipeStartedAt);
374
+
375
+ // If velocity is too slow, lost swipe.
376
+ var minimumVelocity = this.get('swipeVelocity');
377
+ if (velocity < minimumVelocity) {
378
+ this._sc_lastAngle = null;
379
+ this._sc_swipeAnchorX = avgTouch.x;
380
+ this._sc_swipeAnchorY = avgTouch.y;
381
+ this._sc_lastDistance = 0;
382
+ this._sc_swipeStartedAt = null;
383
+ } else {
384
+ // Track how far we've gone in this approved direction.
385
+ this._sc_lastDistance = distance;
386
+ this._sc_swipeLastMovedAt = Date.now();
387
+ }
388
+
389
+ // This is the first matched angle or a new direction. Track its values for future comparison.
390
+ } else {
391
+ // Track the current approved angle and when we started going on it.
392
+ this._sc_lastAngle = approvedAngle;
393
+ this._sc_swipeStartedAt = Date.now();
394
+
395
+ // Use the current number of touches as the number in the session. Some may get cancelled.
396
+ this._sc_numberOfTouches = touchesInSession.length;
397
+ }
398
+
399
+ // No angle or lost the angle.
400
+ } else {
401
+ this._sc_lastAngle = null;
402
+ this._sc_swipeAnchorX = avgTouch.x;
403
+ this._sc_swipeAnchorY = avgTouch.y;
404
+ this._sc_lastDistance = 0;
405
+ this._sc_swipeStartedAt = null;
406
+ }
407
+
408
+ return true;
409
+ },
410
+
411
+ /**
412
+ Cleans up all touch session variables.
413
+
414
+ @returns {void}
415
+ @see SC.Gesture#touchSessionCancelled
416
+ */
417
+ touchSessionCancelled: function () {
418
+ // Clean up.
419
+ this._sc_cleanUpTouchSession(true);
420
+ },
421
+
422
+ /**
423
+ Cleans up all touch session variables and triggers the gesture.
424
+
425
+ @returns {void}
426
+ @see SC.Gesture#touchSessionEnded
427
+ */
428
+ touchSessionEnded: function () {
429
+ // Watch out for touches that move far and fast, but then hesitate too long before ending.
430
+ var notTooLongSinceLastMove = (Date.now() - this._sc_swipeLastMovedAt) < 200;
431
+
432
+ // If an approved angle remained set, the distance was far enough and it wasn't too long since
433
+ // the last movement, trigger the gesture, 'swipe'.
434
+ if (this._sc_lastAngle !== null &&
435
+ this._sc_lastDistance > this.get('swipeDistance') &&
436
+ notTooLongSinceLastMove) {
437
+ this.trigger(this._sc_lastAngle, this._sc_numberOfTouches);
438
+ }
439
+
440
+ // Clean up (will fire tapEnd if _sc_isTapping is true).
441
+ this._sc_cleanUpTouchSession(false);
442
+ },
443
+
444
+ touchSessionStarted: function (touch) {
445
+ this._sc_swipeAnchorX = touch.pageX;
446
+ this._sc_swipeAnchorY = touch.pageY;
447
+ }
448
+
449
+ });