sproutcore 1.11.0.rc3 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ });