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