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