1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 SC.mixin( /** @scope SC */ {
  9 
 10   _copy_computed_props: [
 11     "maxWidth", "maxHeight", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom",
 12     "fontFamily", "fontSize", "fontStyle", "fontWeight", "fontVariant", "lineHeight",
 13     "whiteSpace", "letterSpacing", "wordWrap"
 14   ],
 15 
 16   /**
 17     Returns a string representation of the layout hash.
 18 
 19     Layouts can contain the following keys:
 20       - left: the left edge
 21       - top: the top edge
 22       - right: the right edge
 23       - bottom: the bottom edge
 24       - height: the height
 25       - width: the width
 26       - centerX: an offset from center X
 27       - centerY: an offset from center Y
 28       - minWidth: a minimum width
 29       - minHeight: a minimum height
 30       - maxWidth: a maximum width
 31       - maxHeight: a maximum height
 32       - rotateX
 33       - rotateY
 34       - rotateZ
 35       - scale
 36 
 37     @param layout {Hash} The layout hash to stringify.
 38     @returns {String} A string representation of the layout hash.
 39   */
 40   stringFromLayout: function(layout) {
 41     // Put them in the reverse order that we want to display them, because
 42     // iterating in reverse is faster for CPUs that can compare against zero
 43     // quickly.
 44     var keys = ['maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'centerY',
 45                 'centerX', 'width', 'height', 'bottom', 'right', 'top',
 46                 'left', 'zIndex', 'opacity', 'border', 'borderLeft',
 47                 'borderRight', 'borderTop', 'borderBottom', 'rotateX',
 48                 'rotateY', 'rotateZ', 'scale'],
 49         keyValues = [], key,
 50         i = keys.length;
 51     while (--i >= 0) {
 52       key = keys[i];
 53       if (layout.hasOwnProperty(key)) {
 54         keyValues.push(key + ':' + layout[key]);
 55       }
 56     }
 57 
 58     return '{ ' + keyValues.join(', ') + ' }';
 59   },
 60 
 61   /**
 62     Given a string and a fixed width, calculates the height of that
 63     block of text using a style string, a set of class names,
 64     or both.
 65 
 66     @param str {String} The text to calculate
 67     @param width {Number} The fixed width to assume the text will fill
 68     @param style {String} A CSS style declaration.  E.g., 'font-weight: bold'
 69     @param classNames {Array} An array of class names that may affect the style
 70     @param ignoreEscape {Boolean} To NOT html escape the string.
 71     @returns {Number} The height of the text given the passed parameters
 72   */
 73   heightForString: function(str, width, style, classNames, ignoreEscape) {
 74     var elem = this._heightCalcElement, classes, height;
 75 
 76     if(!ignoreEscape) str = SC.RenderContext.escapeHTML(str);
 77 
 78     // Coalesce the array of class names to one string, if the array exists
 79     classes = (classNames && SC.typeOf(classNames) === SC.T_ARRAY) ? classNames.join(' ') : '';
 80 
 81     if (!width) width = 100; // default to 100 pixels
 82 
 83     // Only create the offscreen element once, then cache it
 84     if (!elem) {
 85       elem = this._heightCalcElement = document.createElement('div');
 86       document.body.insertBefore(elem, null);
 87     }
 88 
 89     style = style+'; width: '+width+'px; left: '+(-1*width)+'px; position: absolute';
 90     var cqElem = SC.$(elem);
 91     cqElem.attr('style', style);
 92 
 93     if (classes !== '') {
 94       cqElem.attr('class', classes);
 95     }
 96 
 97     elem.innerHTML = str;
 98     height = elem.clientHeight;
 99 
100     elem = null; // don't leak memory
101     return height;
102   },
103 
104   /**
105     Sets up a string measuring environment.
106 
107     You may want to use this, in conjunction with teardownStringMeasurement and
108     measureString, instead of metricsForString, if you will be measuring many
109     strings with the same settings. It would be a lot more efficient, as it
110     would only prepare and teardown once instead of several times.
111 
112     @param exampleElement The example element to grab styles from, or the style
113                           string to use.
114     @param classNames {String} (Optional) Class names to add to the test element.
115   */
116   prepareStringMeasurement: function(exampleElement, classNames) {
117     var element = this._metricsCalculationElement, classes, style,
118         cqElem;
119 
120     // collect the class names
121     classes = SC.A(classNames).join(' ');
122 
123     // get the calculation element
124     if (!element) {
125       var parentElement = document.createElement("div");
126 
127       // To have effectively unbounded widths when no max-width is set,
128       // give the metricsCalculationElement a very wide sandbox.
129       // To make sure it's never visible, position it way, way offscreen.
130       parentElement.style.cssText = "position:absolute; left:-10010px; top:-10px;"+
131                                     "width:10000px; height:0px; overflow:hidden;"+
132                                     "visibility:hidden;";
133 
134       element = this._metricsCalculationElement = document.createElement("div");
135 
136       parentElement.appendChild(element);
137       document.body.insertBefore(parentElement, null);
138     }
139 
140     cqElem = SC.$(element);
141     // two possibilities: example element or type string
142     if (SC.typeOf(exampleElement) != SC.T_STRING) {
143       var computed = null;
144       if (document.defaultView && document.defaultView.getComputedStyle) {
145         computed = document.defaultView.getComputedStyle(exampleElement, null);
146       } else {
147       computed = exampleElement.currentStyle;
148       }
149 
150       var props = this._copy_computed_props;
151 
152       // firefox ONLY allows this method
153       for (var i = 0; i < props.length; i++) {
154         var prop = props[i], val = computed[prop];
155         element.style[prop] = val;
156       }
157 
158       // and why does firefox specifically need "font" set?
159       var cs = element.style; // cached style
160       if (cs.font === "") {
161         var font = "";
162         if (cs.fontStyle) font += cs.fontStyle + " ";
163         if (cs.fontVariant) font += cs.fontVariant + " ";
164         if (cs.fontWeight) font += cs.fontWeight + " ";
165         if (cs.fontSize) font += cs.fontSize; else font += "10px"; //force a default
166         if (cs.lineHeight) font += "/" + cs.lineHeight;
167         font += " ";
168         if (cs.fontFamily) font += cs.fontFamily; else cs += "sans-serif";
169 
170         element.style.font = font;
171       }
172 
173       SC.mixin(element.style, {
174         left: "0px", top: "0px", position: "absolute", bottom: "auto", right: "auto", width: "auto", height: "auto"
175       });
176      // clean up
177       computed = null;
178     } else {
179       // it is a style string already
180       style = exampleElement;
181 
182       // set style
183       cqElem.attr("style", style + "; position:absolute; left: 0px; top: 0px; bottom: auto; right: auto; width: auto; height: auto;");
184     }
185 
186     element.className = classes;
187     element = null;
188   },
189 
190   /**
191     Tears down the string measurement environment. Usually, this doesn't _have_
192     to be called, but there are too many what ifs: for example, what if the measurement
193     environment has a bright green background and is over 10,000px wide? Guess what: it will
194     become visible on the screen.
195 
196     So, generally, we tear the measurement environment down so that it doesn't cause issue.
197     However, we keep the DOM element for efficiency.
198   */
199   teardownStringMeasurement: function() {
200     var element = this._metricsCalculationElement;
201 
202     // clear element
203     element.innerHTML = "";
204     element.className = "";
205     element.setAttribute("style", ""); // get rid of any junk from computed style.
206     element = null;
207   },
208 
209   /**
210     Measures a string in the prepared environment.
211 
212     An easier and simpler alternative (but less efficient for bulk measuring) is metricsForString.
213 
214     @param string {String} The string to measure.
215     @param ignoreEscape {Boolean} To NOT html escape the string.
216   */
217   measureString: function(string, ignoreEscape) {
218     var element = this._metricsCalculationElement,
219     padding = 0;
220 
221     if (!element) {
222       throw new Error("measureString requires a string measurement environment to be set up. Did you mean metricsForString?");
223     }
224 
225     // since the string has already been escaped (if the user wants it to be),
226     // we should set the innerHTML instead of innertext
227     if(ignoreEscape) element.innerHTML = string;
228     // the conclusion of which to use (innerText or textContent) should be cached
229     else if (typeof element.innerText != "undefined") element.innerText = string;
230     else element.textContent = string;
231 
232     // for some reason IE measures 1 pixel too small
233     if(SC.browser.isIE) padding = 1;
234 
235     // generate result
236     var result = {
237       width: element.clientWidth + padding,
238       height: element.clientHeight
239     };
240 
241     // Firefox seems to be 1 px short at times, especially with non english characters.
242     if (SC.browser.isMozilla) {
243       result.width += 1;
244     }
245 
246     element = null;
247     return result;
248   },
249 
250   /**
251     Given a string and an example element or style string, and an optional
252     set of class names, calculates the width and height of that block of text.
253 
254     To constrain the width, set max-width on the exampleElement or in the style string.
255 
256     @param string {String} The string to measure.
257     @param exampleElement The example element to grab styles from, or the style string to use.
258     @param classNames {String} (Optional) Class names to add to the test element.
259     @param ignoreEscape {Boolean} To NOT html escape the string.
260   */
261   metricsForString: function(string, exampleElement, classNames, ignoreEscape) {
262     SC.prepareStringMeasurement(exampleElement, classNames);
263     var result = SC.measureString(string, ignoreEscape);
264     SC.teardownStringMeasurement();
265     return result;
266   }
267 
268 });
269