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 SC.mixin(SC.browser, 10 /** @scope SC.browser */ { 11 12 /* @private Internal property for the cache of pre-determined experimental names. */ 13 _cachedNames: null, 14 15 /* @private Internal property for the test element used for style testing. */ 16 _testEl: null, 17 18 /** @private */ 19 _testSupportFor: function (target, propertyName, testValue) { 20 /*jshint eqnull:true*/ 21 var ret = target[propertyName] != null, 22 originalValue; 23 24 if (testValue != null) { 25 originalValue = target[propertyName]; 26 target[propertyName] = testValue; 27 ret = target[propertyName] === testValue; 28 target[propertyName] = originalValue; 29 } 30 31 return ret; 32 }, 33 34 /** 35 Version Strings should not be compared against Numbers. For example, 36 the version "1.20" is greater than "1.2" and less than "1.200", but as 37 Numbers, they are all 1.2. 38 39 Pass in one of the browser versions: SC.browser.version, 40 SC.browser.engineVersion or SC.browser.osVersion and a String to compare 41 against. The function will split each version on the decimals and compare 42 the parts numerically. 43 44 Examples: 45 46 SC.browser.compare('1.20', '1.2') == 18 47 SC.browser.compare('1.08', '1.8') == 0 48 SC.browser.compare('1.1.1', '1.1.004') == -3 49 50 @param {String} version One of SC.browser.version, SC.browser.engineVersion or SC.browser.osVersion 51 @param {String} other The version to compare against. 52 @returns {Number} The difference between the versions at the first difference. 53 */ 54 compare: function (version, other) { 55 var coerce, 56 parts, 57 tests; 58 59 // Ensure that the versions are Strings. 60 if (typeof version === 'number' || typeof other === 'number') { 61 //@if(debug) 62 SC.warn('Developer Warning: SC.browser.compare(): Versions compared against Numbers may not provide accurate results. Use a String of decimal separated Numbers instead.'); 63 //@endif 64 version = String(version); 65 other = String(other); 66 } 67 68 // This function transforms the String to a Number or NaN 69 coerce = function (part) { 70 return Number(part.match(/^[0-9]+/)); 71 }; 72 73 parts = SC.A(version.split('.')).map(coerce); 74 tests = SC.A(other.split('.')).map(coerce); 75 76 // Test each part stopping when there is a difference. 77 for (var i = 0; i < tests.length; i++) { 78 var check = parts[i] - tests[i]; 79 if (isNaN(check)) return 0; 80 if (check !== 0) return check; 81 } 82 83 return 0; 84 }, 85 86 /** 87 This simple method allows you to more safely use experimental properties and 88 methods in current and future browsers. 89 90 Using browser specific methods and properties is a risky coding practice. 91 With sufficient testing, you may be able to match prefixes to today's 92 browsers, but this is prone to error and not future proof. For instance, 93 if a property becomes standard and the browser drops the prefix, your code 94 could suddenly stop working. 95 96 Instead, use SC.browser.experimentalNameFor(target, standardName), which 97 will check the existence of the standard name on the target and if not found 98 will try different camel-cased versions of the name with the current 99 browser's prefix appended. 100 101 If it is still not found, SC.UNSUPPORTED will be returned, allowing 102 you a chance to recover from the lack of browser support. 103 104 Note that `experimentalNameFor` is not really meant for determining browser 105 support, only to ensure that using browser prefixed properties and methods 106 is safe. Instead, SC.platform provides several properties that can be used 107 to determine support for a certain platform feature, which should be 108 used before calling `experimentalNameFor` to safely use the feature. 109 110 For example, 111 112 // Checks for IndexedDB support first on the current platform. 113 if (SC.platform.supportsIndexedDB) { 114 var db = window.indexedDB, 115 // Example return values: 'getDatabaseNames', 'webkitGetDatabaseNames', 'MozGetDatabaseNames', SC.UNSUPPORTED. 116 getNamesMethod = SC.browser.experimentalNameFor(db, 'getDatabaseNames'), 117 names; 118 119 if (getNamesMethod === SC.UNSUPPORTED) { 120 // Work without it. 121 } else { 122 names = db[getNamesMethod](...); 123 } 124 } else { 125 // Work without it. 126 } 127 128 ## Improving deduction 129 Occasionally a target will appear to support a property, but will fail to 130 actually accept a value. In order to ensure that the property doesn't just 131 exist but is also usable, you can provide an optional `testValue` that will 132 be temporarily assigned to the target to verify that the detected property 133 is usable. 134 135 @param {Object} target The target for the method. 136 @param {String} standardName The standard name of the property or method we wish to check on the target. 137 @param {String} [testValue] A value to temporarily assign to the property. 138 @returns {string} The name of the property or method on the target or SC.UNSUPPORTED if no method found. 139 */ 140 experimentalNameFor: function (target, standardName, testValue) { 141 // Test the property name. 142 var ret = standardName; 143 144 // ex. window.indexedDB.getDatabaseNames 145 if (!this._testSupportFor(target, ret, testValue)) { 146 // ex. window.WebKitCSSMatrix 147 ret = SC.browser.classPrefix + standardName.capitalize(); 148 if (!this._testSupportFor(target, ret, testValue)) { 149 // No need to check if the prefix is the same for properties and classes 150 if (SC.browser.domPrefix === SC.browser.classPrefix) { 151 // Always show a warning so that production usage information has a 152 // better chance of filtering back to the developer(s). 153 SC.warn("SC.browser.experimentalNameFor(): target, %@, does not have property `%@` or `%@`.".fmt(target, standardName, ret)); 154 ret = SC.UNSUPPORTED; 155 } else { 156 // ex. window.indexedDB.webkitGetDatabaseNames 157 ret = SC.browser.domPrefix + standardName.capitalize(); 158 if (!this._testSupportFor(target, ret, testValue)) { 159 // Always show a warning so that production usage information has a 160 // better chance of filtering back to the developer(s). 161 SC.warn("SC.browser.experimentalNameFor(): target, %@, does not have property `%@`, '%@' or `%@`.".fmt(target, standardName, SC.browser.classPrefix + standardName.capitalize(), ret)); 162 ret = SC.UNSUPPORTED; 163 } 164 } 165 } 166 } 167 168 return ret; 169 }, 170 171 /** 172 This method returns safe style names for current and future browsers. 173 174 Using browser specific style prefixes is a risky coding practice. With 175 sufficient testing, you may be able to match styles across today's most 176 popular browsers, but this is a lot of work and not future proof. For 177 instance, if a browser drops the prefix and supports the standard style 178 name, your code will suddenly stop working. This happens ALL the time! 179 180 Instead, use SC.browser.experimentalStyleNameFor(standardStyleName), which 181 will test support for the standard style name and if not found will try the 182 prefixed version with the current browser's prefix appended. 183 184 Note: the proper style name is only determined once per standard style 185 name tested and then cached. Therefore, calling experimentalStyleNameFor 186 repeatedly has no performance detriment. 187 188 For example, 189 190 var boxShadowName = SC.browser.experimentalStyleNameFor('boxShadow'), 191 el = document.createElement('div'); 192 193 // `boxShadowName` may be "boxShadow", "WebkitBoxShadow", "msBoxShadow", etc. depending on the browser support. 194 el.style[boxShadowName] = "rgb(0,0,0) 0px 3px 5px"; 195 196 ## Improving deduction 197 Occasionally a browser will appear to support a style, but will fail to 198 actually accept a value. In order to ensure that the style doesn't just 199 exist but is also usable, you can provide an optional `testValue` that will 200 be used to verify that the detected style is usable. 201 202 @param {string} standardStyleName The standard name of the experimental style as it should be un-prefixed. This is the DOM property name, which is camel-cased (ex. boxShadow) 203 @param {String} [testValue] A value to temporarily assign to the style to ensure support. 204 @returns {string} Future-proof style name for use in the current browser or SC.UNSUPPORTED if no style support found. 205 */ 206 experimentalStyleNameFor: function (standardStyleName, testValue) { 207 var cachedNames = this._sc_experimentalStyleNames, 208 ret; 209 210 // Fast path & cache initialization. 211 if (!cachedNames) { 212 cachedNames = this._sc_experimentalStyleNames = {}; 213 } 214 215 if (cachedNames[standardStyleName]) { 216 ret = cachedNames[standardStyleName]; 217 } else { 218 // Test the style name. 219 var el = this._testEl; 220 221 // Create a test element and cache it for repeated use. 222 if (!el) { el = this._testEl = document.createElement("div"); } 223 224 // Cache the experimental style name (even SC.UNSUPPORTED) for quick repeat access. 225 ret = cachedNames[standardStyleName] = this.experimentalNameFor(el.style, standardStyleName, testValue); 226 } 227 228 return ret; 229 }, 230 231 /** 232 This method returns safe CSS attribute names for current and future browsers. 233 234 Using browser specific CSS prefixes is a risky coding practice. With 235 sufficient testing, you may be able to match attributes across today's most 236 popular browsers, but this is a lot of work and not future proof. For 237 instance, if a browser drops the prefix and supports the standard CSS 238 name, your code will suddenly stop working. This happens ALL the time! 239 240 Instead, use SC.browser.experimentalCSSNameFor(standardCSSName), which 241 will test support for the standard CSS name and if not found will try the 242 prefixed version with the current browser's prefix appended. 243 244 Note: the proper CSS name is only determined once per standard CSS 245 name tested and then cached. Therefore, calling experimentalCSSNameFor 246 repeatedly has no performance detriment. 247 248 For example, 249 250 var boxShadowCSS = SC.browser.experimentalCSSNameFor('box-shadow'), 251 el = document.createElement('div'); 252 253 // `boxShadowCSS` may be "box-shadow", "-webkit-box-shadow", "-ms-box-shadow", etc. depending on the current browser. 254 el.style.cssText = boxShadowCSS + " rgb(0,0,0) 0px 3px 5px"; 255 256 ## Improving deduction 257 Occasionally a browser will appear to support a style, but will fail to 258 actually accept a value. In order to ensure that the style doesn't just 259 exist but is also usable, you can provide an optional `testValue` that will 260 be used to verify that the detected style is usable. 261 262 @param {string} standardCSSName The standard name of the experimental CSS attribute as it should be un-prefixed (ex. box-shadow). 263 @param {String} [testValue] A value to temporarily assign to the style to ensure support. 264 @returns {string} Future-proof CSS name for use in the current browser or SC.UNSUPPORTED if no style support found. 265 */ 266 experimentalCSSNameFor: function (standardCSSName, testValue) { 267 var ret = standardCSSName, 268 standardStyleName = standardCSSName.camelize(), 269 styleName = this.experimentalStyleNameFor(standardStyleName, testValue); 270 271 if (styleName === SC.UNSUPPORTED) { 272 ret = SC.UNSUPPORTED; 273 } else if (styleName !== standardStyleName) { 274 // If the DOM property is prefixed, then the CSS name should be prefixed. 275 ret = SC.browser.cssPrefix + standardCSSName; 276 } 277 278 return ret; 279 } 280 281 }); 282