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