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 
  9 /**
 10   A constant indicating an unsupported method, property or other.
 11 
 12   @static
 13   @constant
 14 */
 15 SC.UNSUPPORTED = '_sc_unsupported';
 16 
 17 
 18 /** @class
 19 
 20   This platform object allows you to conditionally support certain HTML5
 21   features.
 22 
 23   Rather than relying on the user agent, it detects whether the given elements
 24   and events are supported by the browser, allowing you to create much more
 25   robust apps.
 26 */
 27 SC.platform = SC.Object.create({
 28   /**
 29     The size of scrollbars in this browser.
 30 
 31     @type Number
 32   */
 33   scrollbarSize: function () {
 34     var tester = document.createElement("DIV"),
 35         child;
 36     tester.innerHTML = "<div style='height:1px;'></div>";
 37     tester.style.cssText = "position:absolute;width:100px;height:100px;overflow-y:visible;";
 38 
 39     child = tester.childNodes[0];
 40     document.body.appendChild(tester);
 41     var noScroller = child.innerWidth || child.clientWidth;
 42     tester.style.overflowY = 'scroll';
 43     var withScroller = child.innerWidth || child.clientWidth;
 44     document.body.removeChild(tester);
 45 
 46     return noScroller - withScroller;
 47 
 48   }.property().cacheable(),
 49 
 50 
 51   /*
 52     NOTES
 53      - Chrome would incorrectly indicate support for touch events.  This has been fixed:
 54        http://code.google.com/p/chromium/issues/detail?id=36415
 55      - Android is assumed to support touch, but incorrectly reports that it does not.
 56      - See: https://github.com/Modernizr/Modernizr/issues/84 for a discussion on detecting
 57        touch capability.
 58      - See: https://github.com/highslide-software/highcharts.com/issues/1331 for a discussion
 59        about why we need to check if ontouchstart is null in addition to check if it's defined
 60      - The test for window._phantom provides support for phantomjs, the headless WebKit browser
 61        used in Travis-CI, and which incorredtly (see above) identifies itself as a touch browser.
 62        For more information on CI see https://github.com/sproutcore/sproutcore/pull/1025
 63        For discussion of the phantomjs touch issue see https://github.com/ariya/phantomjs/issues/10375
 64   */
 65   /**
 66     YES if the current device supports touch events, NO otherwise.
 67 
 68     You can simulate touch events in environments that don't support them by
 69     calling SC.platform.simulateTouchEvents() from your browser's console.
 70 
 71     Note! The support for "touch" is a browser property and can't be relied on
 72     to determine if the device is actually a "touch" device or if the device
 73     actually uses touch events.  There are instances where "touch" devices will
 74     not send touch events or will send touch and mouse events together and
 75     there are instances where "non-touch" devices will support touch events.
 76 
 77     It is recommended that you do not use this property at this time.
 78 
 79     @type Boolean
 80   */
 81   touch: (!SC.none(window.ontouchstart) || SC.browser.name === SC.BROWSER.android || 'ontouchstart' in document.documentElement) && SC.none(window._phantom),
 82 
 83   /**
 84     True if bouncing on scroll is expected in the current platform.
 85 
 86     @type Boolean
 87   */
 88   bounceOnScroll: SC.browser.os === SC.OS.ios,
 89 
 90   /**
 91     True if pinch-to-zoom is expected in the current platform.
 92 
 93     @type Boolean
 94   */
 95   pinchToZoom: SC.browser.os === SC.OS.ios,
 96 
 97   /**
 98     A hash that contains properties that indicate support for new HTML5
 99     a attributes.
100 
101     For example, to test to see if the `download` attribute is supported,
102     you would verify that `SC.platform.a.download` is true.
103 
104     @type Array
105   */
106   a: function () {
107     var elem = document.createElement('a');
108 
109     return {
110       download: !!('download' in elem),
111       media: !!('media' in elem),
112       ping: !!('ping' in elem),
113     };
114   }(),
115 
116   /**
117     A hash that contains properties that indicate support for new HTML5
118     input attributes.
119 
120     For example, to test to see if the `placeholder` attribute is supported,
121     you would verify that `SC.platform.input.placeholder` is true.
122 
123     @type Array
124   */
125   input: function (attributes) {
126     var ret = {},
127         len = attributes.length,
128         elem = document.createElement('input'),
129         attr, idx;
130 
131     for (idx = 0; idx < len; idx++) {
132       attr = attributes[idx];
133 
134       ret[attr] = !!(attr in elem);
135     }
136 
137     return ret;
138   }(['autocomplete', 'readonly', 'list', 'size', 'required', 'multiple', 'maxlength',
139         'pattern', 'min', 'max', 'step', 'placeholder',
140         'selectionStart', 'selectionEnd', 'selectionDirection']),
141 
142   /**
143     YES if the application is currently running as a standalone application.
144 
145     For example, if the user has saved your web application to their home
146     screen on an iPhone OS-based device, this property will be true.
147 
148     @type Boolean
149   */
150   standalone: !!navigator.standalone,
151 
152 
153   /** @deprecated Since version 1.10. Use SC.browser.cssPrefix.
154     Prefix for browser specific CSS attributes.
155   */
156   cssPrefix: SC.browser.cssPrefix,
157 
158   /** @deprecated Since version 1.10. Use SC.browser.domPrefix.
159     Prefix for browser specific CSS attributes when used in the DOM.
160   */
161   domCSSPrefix: SC.browser.domPrefix,
162 
163   /**
164     Call this method to swap out the default mouse handlers with proxy methods
165     that will translate mouse events to touch events.
166 
167     This is useful if you are debugging touch functionality on the desktop.
168   */
169   simulateTouchEvents: function () {
170     // Touch events are supported natively, no need for this.
171     if (this.touch) {
172       // @if (debug)
173       SC.Logger.info("Can't simulate touch events in an environment that supports them.");
174       // @endif
175       return;
176     }
177 
178     SC.Logger.log("Simulating touch events");
179 
180     // Tell the app that we now "speak" touch
181     SC.platform.touch = YES;
182     SC.platform.bounceOnScroll = YES;
183 
184     // CSS selectors may depend on the touch class name being present
185     document.body.className = document.body.className + ' touch';
186 
187     // Initialize a counter, which we will use to generate unique ids for each
188     // fake touch.
189     this._simtouch_counter = 1;
190 
191     // Remove events that don't exist in touch environments
192     this.removeEvents(['click', 'dblclick', 'mouseout', 'mouseover', 'mousewheel']);
193 
194     // Replace mouse events with our translation methods
195     this.replaceEvent('mousemove', this._simtouch_mousemove);
196     this.replaceEvent('mousedown', this._simtouch_mousedown);
197     this.replaceEvent('mouseup', this._simtouch_mouseup);
198 
199     // fix orientation handling
200     SC.platform.windowSizeDeterminesOrientation = YES;
201     SC.device.orientationHandlingShouldChange();
202   },
203 
204   /** @private
205     Removes event listeners from the document.
206 
207     @param {Array} events Array of strings representing the events to remove
208   */
209   removeEvents: function (events) {
210     var idx, len = events.length, key;
211     for (idx = 0; idx < len; idx++) {
212       key = events[idx];
213       SC.Event.remove(document, key, SC.RootResponder.responder, SC.RootResponder.responder[key]);
214     }
215   },
216 
217   /** @private
218     Replaces an event listener with another.
219 
220     @param {String} evt The event to replace
221     @param {Function} replacement The method that should be called instead
222   */
223   replaceEvent: function (evt, replacement) {
224     SC.Event.remove(document, evt, SC.RootResponder.responder, SC.RootResponder.responder[evt]);
225     SC.Event.add(document, evt, this, replacement);
226   },
227 
228   /** @private
229     When simulating touch events, this method is called when mousemove events
230     are received.
231 
232     If the altKey is depressed and pinch center not yet established, we will capture the mouse position.
233   */
234   _simtouch_mousemove: function (evt) {
235     if (!this._mousedown) {
236       /*
237         we need to capture when was the first spot that the altKey was pressed and use it as
238         the center point of a pinch
239        */
240       if (evt.altKey && this._pinchCenter === null) {
241         this._pinchCenter = {
242           pageX: evt.pageX,
243           pageY: evt.pageY,
244           screenX: evt.screenX,
245           screenY: evt.screenY,
246           clientX: evt.clientX,
247           clientY: evt.clientY
248         };
249       } else if (!evt.altKey && this._pinchCenter !== null) {
250         this._pinchCenter = null;
251       }
252       return NO;
253     }
254 
255     var manufacturedEvt = this.manufactureTouchEvent(evt, 'touchmove');
256     return SC.RootResponder.responder.touchmove(manufacturedEvt);
257   },
258 
259   /** @private
260     When simulating touch events, this method is called when mousedown events
261     are received.
262   */
263   _simtouch_mousedown: function (evt) {
264     this._mousedown = YES;
265 
266     var manufacturedEvt = this.manufactureTouchEvent(evt, 'touchstart');
267     return SC.RootResponder.responder.touchstart(manufacturedEvt);
268   },
269 
270   /** @private
271     When simulating touch events, this method is called when mouseup events
272     are received.
273   */
274   _simtouch_mouseup: function (evt) {
275     var manufacturedEvt = this.manufactureTouchEvent(evt, 'touchend'),
276         ret = SC.RootResponder.responder.touchend(manufacturedEvt);
277 
278     this._mousedown = NO;
279     this._simtouch_counter++;
280     return ret;
281   },
282 
283   /** @private
284     Converts a mouse-style event to a touch-style event.
285 
286     Note that this method edits the passed event in place, and returns
287     that same instance instead of a new, modified version.
288 
289     If altKey is depressed and we have previously captured a position for the center of
290     the pivot point for the virtual second touch, we will manufacture an additional touch.
291     The position of the virtual touch will be the reflection of the mouse position,
292     relative to the pinch center.
293 
294     @param {Event} evt the mouse event to modify
295     @param {String} type the type of event (e.g., touchstart)
296     @returns {Event} the mouse event with an added changedTouches array
297   */
298   manufactureTouchEvent: function (evt, type) {
299     var realTouch, virtualTouch, realTouchIdentifier = this._simtouch_counter;
300 
301     realTouch = {
302       type: type,
303       target: evt.target,
304       identifier: realTouchIdentifier,
305       pageX: evt.pageX,
306       pageY: evt.pageY,
307       screenX: evt.screenX,
308       screenY: evt.screenY,
309       clientX: evt.clientX,
310       clientY: evt.clientY
311     };
312     evt.touches = [ realTouch ];
313 
314     /*
315       simulate pinch gesture
316      */
317     if (evt.altKey && this._pinchCenter !== null) {
318       //calculate the mirror position of the virtual touch
319       var pageX = this._pinchCenter.pageX + this._pinchCenter.pageX - evt.pageX,
320           pageY = this._pinchCenter.pageY + this._pinchCenter.pageY - evt.pageY,
321           screenX = this._pinchCenter.screenX + this._pinchCenter.screenX - evt.screenX,
322           screenY = this._pinchCenter.screenY + this._pinchCenter.screenY - evt.screenY,
323           clientX = this._pinchCenter.clientX + this._pinchCenter.clientX - evt.clientX,
324           clientY = this._pinchCenter.clientY + this._pinchCenter.clientY - evt.clientY,
325           virtualTouchIdentifier = this._simtouch_counter + 1;
326 
327       virtualTouch = {
328         type: type,
329         target: evt.target,
330         identifier: virtualTouchIdentifier,
331         pageX: pageX,
332         pageY: pageY,
333         screenX: screenX,
334         screenY: screenY,
335         clientX: clientX,
336         clientY: clientY
337       };
338 
339       evt.touches = [realTouch, virtualTouch];
340     }
341     evt.changedTouches = evt.touches;
342 
343     return evt;
344   },
345 
346   /**
347     Whether the browser supports CSS animations.
348 
349     @type Boolean
350   */
351   supportsCSSAnimations: SC.browser.experimentalStyleNameFor('animation') !== SC.UNSUPPORTED,
352 
353   /**
354     Whether the browser supports CSS transitions.
355 
356     @type Boolean
357   */
358   supportsCSSTransitions: SC.browser.experimentalStyleNameFor('transition') !== SC.UNSUPPORTED,
359 
360   /**
361     Whether the browser supports 2D CSS transforms.
362 
363     @type Boolean
364   */
365   supportsCSSTransforms: SC.browser.experimentalStyleNameFor('transform') !== SC.UNSUPPORTED,
366 
367   /**
368     Whether the browser can properly handle 3D CSS transforms.
369 
370     @type Boolean
371   */
372   supportsCSS3DTransforms: SC.browser.experimentalStyleNameFor('perspective') !== SC.UNSUPPORTED,
373 
374   /**
375     Whether the browser supports the application cache.
376 
377     @type Boolean
378   */
379   supportsApplicationCache: ('applicationCache' in window),
380 
381   /**
382     Whether the browser supports the hashchange event.
383 
384     @type Boolean
385   */
386   supportsHashChange: function () {
387     // Code copied from Modernizr which copied code from YUI (MIT licenses)
388     // documentMode logic from YUI to filter out IE8 Compat Mode which false positives
389     return ('onhashchange' in window) && (document.documentMode === undefined || document.documentMode > 7);
390   }(),
391 
392   /**
393     Whether the browser supports HTML5 history.
394 
395     @type Boolean
396   */
397   supportsHistory: function () {
398     return !!(window.history && window.history.pushState);
399   }(),
400 
401   /**
402     Whether the browser supports IndexedDB.
403 
404     @type Boolean
405   */
406   supportsIndexedDB: function () {
407     return !!(window.indexedDB || window[SC.browser.domPrefix + 'IndexedDB']);
408   }(),
409 
410   /**
411     Whether the browser supports the canvas element.
412 
413     @type Boolean
414   */
415   supportsCanvas: function () {
416     return !!document.createElement('canvas').getContext;
417   }(),
418 
419   /**
420     Whether the browser supports the XHR2 ProgressEvent specification. This
421     reliably implies support for XMLHttpRequest 'loadstart' and 'progress'
422     events, as well as the terminal 'load', 'error' and 'abort' events. Support
423     for 'loadend', which fires no matter how the request terminats, is a bit
424     spottier and should be verified separately using `supportsXHR2LoadEndEvent`.
425 
426     @type Boolean
427   */
428   supportsXHR2ProgressEvent: ('ProgressEvent' in window),
429 
430   /**
431     Whether the browser supports the XHR2 FormData specification.
432 
433     @type Boolean
434   */
435   supportsXHR2FormData: ('FormData' in window),
436 
437   /**
438     Whether the browser supports the XHR2 ProgressEvent's loadend event. If not
439     supported, you should handle 'load', 'error' and 'abort' events instead.
440 
441     @type Boolean
442    */
443   supportsXHR2LoadEndEvent: function () {
444     return (new XMLHttpRequest).onloadend === null;
445   } (),
446 
447   /**
448     Whether the browser supports the orientationchange event.
449 
450     @type Boolean
451   */
452   supportsOrientationChange: ('onorientationchange' in window),
453 
454   /**
455     Whether the browser supports WebSocket or not.
456 
457     @type Boolean
458   */
459   supportsWebSocket: ("WebSocket" in window),
460 
461   /**
462     Whether the browser supports WebSQL.
463 
464     @type Boolean
465   */
466   supportsWebSQL: ('openDatabase' in window),
467 
468   /**
469     Because iOS is slow to dispatch the window.onorientationchange event,
470     we use the window size to determine the orientation on iOS devices
471     and desktop environments when SC.platform.touch is YES (ie. when
472     SC.platform.simulateTouchEvents has been called)
473 
474     @type Boolean
475   */
476   windowSizeDeterminesOrientation: SC.browser.os === SC.OS.ios || !('onorientationchange' in window),
477 
478   /**
479     Does this browser support the Apache Cordova (formerly phonegap) runtime?
480 
481     This requires that you (the engineer) manually include the cordova
482     javascript library for the appropriate platform (Android, iOS, etc)
483     in your code. There are various methods of doing this; creating your own
484     platform-specific index.rhtml is probably the easiest option.
485 
486     WARNING: Using the javascript_libs Buildfile option for the cordova include
487     will NOT work. The library will be included after your application code,
488     by which time this property will already have been evaluated.
489 
490     @type Boolean
491     @see http://incubator.apache.org/cordova/
492   */
493   // Check for the global cordova property.
494   cordova: (typeof window.cordova !== "undefined")
495 
496 });
497 
498 /** @private
499   Test the transition and animation event names of this platform.  We could hard
500   code event names into the framework, but at some point things would change and
501   we would get it wrong.  Instead we perform actual tests to find out the proper
502   names and only add the proper listeners.
503 
504   Once the tests are completed the RootResponder is notified in order to clean up
505   unnecessary transition and animation event listeners.
506 */
507 SC.ready(function () {
508   // This will add 4 different variations of the named event listener and clean
509   // them up again.
510   // Note: we pass in capitalizedEventName, because we can't just capitalize
511   // the standard event name.  For example, in WebKit the standard transitionend
512   // event is named webkitTransitionEnd, not webkitTransitionend.
513   var executeTest = function (el, standardEventName, capitalizedEventName, cleanUpFunc) {
514     var domPrefix = SC.browser.domPrefix,
515       lowerDomPrefix = domPrefix.toLowerCase(),
516       eventNameKey = standardEventName + 'EventName',
517       callback = function (evt) {
518         var domPrefix = SC.browser.domPrefix,
519           lowerDomPrefix = domPrefix.toLowerCase(),
520           eventNameKey = standardEventName + 'EventName';
521 
522         // Remove all the event listeners.
523         el.removeEventListener(standardEventName, callback, NO);
524         el.removeEventListener(lowerDomPrefix + standardEventName, callback, NO);
525         el.removeEventListener(lowerDomPrefix + capitalizedEventName, callback, NO);
526         el.removeEventListener(domPrefix + capitalizedEventName, callback, NO);
527 
528         // The cleanup timer re-uses this function and doesn't pass evt.
529         if (evt) {
530           SC.platform[eventNameKey] = evt.type;
531 
532           // Don't allow the event to bubble, because SC.RootResponder will be
533           // adding event listeners as soon as the testing is complete.  It is
534           // important that SC.RootResponder's listeners don't catch the last
535           // test event.
536           evt.stopPropagation();
537         }
538 
539         // Call the clean up function, pass in success state.
540         if (cleanUpFunc) { cleanUpFunc(!!evt); }
541       };
542 
543     // Set the initial value as unsupported.
544     SC.platform[eventNameKey] = SC.UNSUPPORTED;
545 
546     // Try the various implementations.
547     // ex. transitionend, webkittransitionend, webkitTransitionEnd, WebkitTransitionEnd
548     el.addEventListener(standardEventName, callback, NO);
549     el.addEventListener(lowerDomPrefix + standardEventName, callback, NO);
550     el.addEventListener(lowerDomPrefix + capitalizedEventName, callback, NO);
551     el.addEventListener(domPrefix + capitalizedEventName, callback, NO);
552   };
553 
554   // Set up and execute the transition event test.
555   if (SC.platform.supportsCSSTransitions) {
556     var transitionEl = document.createElement('div'),
557       transitionStyleName = SC.browser.experimentalStyleNameFor('transition', 'all 1ms linear');
558 
559     transitionEl.style[transitionStyleName] = 'all 1ms linear';
560 
561     // Test transition events.
562     executeTest(transitionEl, 'transitionend', 'TransitionEnd', function (success) {
563       // If an end event never fired, we can't really support CSS transitions in SproutCore.
564       if (success) {
565         // Set up the SC transition event listener.
566         SC.RootResponder.responder.cleanUpTransitionListeners();
567       } else {
568         SC.platform.supportsCSSTransitions = NO;
569       }
570 
571       transitionEl.parentNode.removeChild(transitionEl);
572       transitionEl = null;
573     });
574 
575     // Append the test element.
576     document.documentElement.appendChild(transitionEl);
577 
578     // Break execution to allow the browser to update the DOM before altering the style.
579     setTimeout(function () {
580       transitionEl.style.opacity = '0';
581     });
582 
583     // Set up and execute the animation event test.
584     if (SC.platform.supportsCSSAnimations) {
585       var animationEl = document.createElement('div'),
586         keyframes,
587         prefixedKeyframes;
588 
589       // Generate both the regular and prefixed version of the style.
590       keyframes = '@keyframes _sc_animation_test { from { opacity: 1; } to { opacity: 0; } }';
591       prefixedKeyframes = '@' + SC.browser.cssPrefix + 'keyframes _sc_prefixed_animation_test { from { opacity: 1; } to { opacity: 0; } }';
592 
593       // Add test animation styles.
594       animationEl.innerHTML = '<style>' + keyframes + '\n' + prefixedKeyframes + '</style>';
595 
596       // Set up and execute the animation event test.
597       animationEl.style.animation = '_sc_animation_test 1ms linear';
598       animationEl.style[SC.browser.domPrefix + 'Animation'] = '_sc_prefixed_animation_test 5ms linear';
599 
600       // NOTE: We could test start, but it's extra work and easier just to test the end
601       // and infer the start event name from it.  Keeping this code for example.
602       // executeTest(animationEl, 'animationstart', 'AnimationStart', function (success) {
603       //   // If an iteration start never fired, we can't really support CSS transitions in SproutCore.
604       //   if (!success) {
605       //     SC.platform.supportsCSSAnimations = NO;
606       //   }
607       // });
608 
609       // NOTE: Testing iteration event support proves very problematic.  Many
610       // browsers can't iterate less than several milliseconds which means we
611       // have to wait too long to find out this event name.  Instead we test
612       // the end only and infer the iteration event name from it. Keeping this
613       // code for example, but it wont' work reliably unless the animation style
614       // is something like '_sc_animation_test 30ms linear' (i.e. ~60ms wait time)
615       // executeTest(animationEl, 'animationiteration', 'AnimationIteration', function (success) {
616       //   // If an iteration event never fired, we can't really support CSS transitions in SproutCore.
617       //   if (!success) {
618       //     SC.platform.supportsCSSAnimations = NO;
619       //   }
620       // });
621 
622       // Test animation events.
623       executeTest(animationEl, 'animationend', 'AnimationEnd', function (success) {
624         // If an end event never fired, we can't really support CSS animations in SproutCore.
625         if (success) {
626           // Infer the start and iteration event names based on the success of the end event.
627           var domPrefix = SC.browser.domPrefix,
628             lowerDomPrefix = domPrefix.toLowerCase(),
629             endEventName = SC.platform.animationendEventName;
630 
631           switch (endEventName) {
632           case lowerDomPrefix + 'animationend':
633             SC.platform.animationstartEventName = lowerDomPrefix + 'animationstart';
634             SC.platform.animationiterationEventName = lowerDomPrefix + 'animationiteration';
635             break;
636           case lowerDomPrefix + 'AnimationEnd':
637             SC.platform.animationstartEventName = lowerDomPrefix + 'AnimationStart';
638             SC.platform.animationiterationEventName = lowerDomPrefix + 'AnimationIteration';
639             break;
640           case domPrefix + 'AnimationEnd':
641             SC.platform.animationstartEventName = domPrefix + 'AnimationStart';
642             SC.platform.animationiterationEventName = domPrefix + 'AnimationIteration';
643             break;
644           default:
645             SC.platform.animationstartEventName = 'animationstart';
646             SC.platform.animationiterationEventName = 'animationiteration';
647           }
648 
649           // Set up the SC animation event listeners.
650           SC.RootResponder.responder.cleanUpAnimationListeners();
651         } else {
652           SC.platform.supportsCSSAnimations = NO;
653         }
654 
655         // Clean up.
656         animationEl.parentNode.removeChild(animationEl);
657         animationEl = null;
658       });
659 
660       // Break execution to allow the browser to update the DOM before altering the style.
661       document.documentElement.appendChild(animationEl);
662     }
663   }
664 });
665