SC.mixin( /** @scope SC */ { _copy_computed_props: [ "maxWidth", "maxHeight", "paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "fontFamily", "fontSize", "fontStyle", "fontWeight", "fontVariant", "lineHeight", "whiteSpace" ], /** Returns a string representation of the layout hash. Layouts can contain the following keys: - left: the left edge - top: the top edge - right: the right edge - bottom: the bottom edge - height: the height - width: the width - centerX: an offset from center X - centerY: an offset from center Y - minWidth: a minimum width - minHeight: a minimum height - maxWidth: a maximum width - maxHeight: a maximum height @param layout {Hash} The layout hash to stringify. @returns {String} A string representation of the layout hash. */ stringFromLayout: function(layout) { // Put them in the reverse order that we want to display them, because // iterating in reverse is faster for CPUs that can compare against zero // quickly. var keys = ['maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'centerY', 'centerX', 'width', 'height', 'bottom', 'right', 'top', 'left'], keyValues = [], key, i = keys.length; while (--i >= 0) { key = keys[i]; if (layout.hasOwnProperty(key)) { keyValues.push(key + ':' + layout[key]); } } return '{' + keyValues.join(', ') + '}'; }, /** Given a string and a fixed width, calculates the height of that block of text using a style string, a set of class names, or both. @param str {String} The text to calculate @param width {Number} The fixed width to assume the text will fill @param style {String} A CSS style declaration. E.g., 'font-weight: bold' @param classNames {Array} An array of class names that may affect the style @param ignoreEscape {Boolean} To NOT html escape the string. @returns {Number} The height of the text given the passed parameters */ heightForString: function(str, width, style, classNames, ignoreEscape) { var elem = this._heightCalcElement, classes, height; if(!ignoreEscape) str = SC.RenderContext.escapeHTML(str); // Coalesce the array of class names to one string, if the array exists classes = (classNames && SC.typeOf(classNames) === SC.T_ARRAY) ? classNames.join(' ') : ''; if (!width) width = 100; // default to 100 pixels // Only create the offscreen element once, then cache it if (!elem) { elem = this._heightCalcElement = document.createElement('div'); document.body.insertBefore(elem, null); } style = style+'; width: '+width+'px; left: '+(-1*width)+'px; position: absolute'; var cqElem = SC.$(elem); cqElem.attr('style', style); if (classes !== '') { cqElem.attr('class', classes); } elem.innerHTML = str; height = elem.clientHeight; elem = null; // don't leak memory return height; }, /** Sets up a string measuring environment. You may want to use this, in conjunction with teardownStringMeasurement and measureString, instead of metricsForString, if you will be measuring many strings with the same settings. It would be a lot more efficient, as it would only prepare and teardown once instead of several times. @param exampleElement The example element to grab styles from, or the style string to use. @param classNames {String} (Optional) Class names to add to the test element. */ prepareStringMeasurement: function(exampleElement, classNames) { var element = this._metricsCalculationElement, classes, styles, style, cqElem; // collect the class names classes = SC.A(classNames).join(' '); // get the calculation element if (!element) { var parentElement = document.createElement("div"); // to make sure the measurement element is never visible, put it inside a 0x0 element with overflow: hidden SC.mixin(parentElement.style, { position: 'absolute', left: '0px', top: '0px', height: '0px', right: '0px', overflow: 'hidden' }); element = this._metricsCalculationElement = document.createElement("div"); parentElement.appendChild(element); document.body.insertBefore(parentElement, null); } cqElem = SC.$(element); // two possibilities: example element or type string if (SC.typeOf(exampleElement) != SC.T_STRING) { var computed = null; if (document.defaultView && document.defaultView.getComputedStyle) { computed = document.defaultView.getComputedStyle(exampleElement, null); } else { computed = exampleElement.currentStyle; } var props = this._copy_computed_props; // firefox ONLY allows this method for (var i = 0; i < props.length; i++) { var prop = props[i], val = computed[prop]; element.style[prop] = val; } // and why does firefox specifically need "font" set? var cs = element.style; // cached style if (cs.font === "") { var font = ""; if (cs.fontStyle) font += cs.fontStyle + " "; if (cs.fontVariant) font += cs.fontVariant + " "; if (cs.fontWeight) font += cs.fontWeight + " "; if (cs.fontSize) font += cs.fontSize; else font += "10px"; //force a default if (cs.lineHeight) font += "/" + cs.lineHeight; font += " "; if (cs.fontFamily) font += cs.fontFamily; else cs += "sans-serif"; element.style.font = font; } SC.mixin(element.style, { left: "0px", top: "0px", position: "absolute", bottom: "auto", right: "auto", width: "auto", height: "auto" }); // clean up computed = null; } else { // it is a style string already style = exampleElement; // set style cqElem.attr("style", style + "; position:absolute; left: 0px; top: 0px; bottom: auto; right: auto; width: auto; height: auto;"); } element.className = classes; element = null; }, /** Tears down the string measurement environment. Usually, this doesn't _have_ to be called, but there are too many what ifs: for example, what if the measurement environment has a bright green background and is over 10,000px wide? Guess what: it will become visible on the screen. So, generally, we tear the measurement environment down so that it doesn't cause issue. However, we keep the DOM element for efficiency. */ teardownStringMeasurement: function() { var element = this._metricsCalculationElement; // clear element element.innerHTML = ""; element.className = ""; element.setAttribute("style", ""); // get rid of any junk from computed style. element = null; }, /** Measures a string in the prepared environment. An easier and simpler alternative (but less efficient for bulk measuring) is metricsForString. @param string {String} The string to measure. @param ignoreEscape {Boolean} To NOT html escape the string. */ measureString: function(string, ignoreEscape) { if(!ignoreEscape) string = SC.RenderContext.escapeHTML(string); var element = this._metricsCalculationElement; if (!element) { throw "measureString requires a string measurement environment to be set up. Did you mean metricsForString?"; } // the conclusion of which to use (innerText or textContent) should be cached if (typeof element.innerText != "undefined") element.innerText = string; else element.textContent = string; // generate result var result = { width: element.clientWidth, height: element.clientHeight }; element = null; return result; }, /** Given a string and an example element or style string, and an optional set of class names, calculates the width and height of that block of text. To constrain the width, set max-width on the exampleElement or in the style string. @param string {String} The string to measure. @param exampleElement The example element to grab styles from, or the style string to use. @param classNames {String} (Optional) Class names to add to the test element. @param ignoreEscape {Boolean} To NOT html escape the string. */ metricsForString: function(string, exampleElement, classNames, ignoreEscape) { SC.prepareStringMeasurement(exampleElement, classNames); var result = SC.measureString(string, ignoreEscape); SC.teardownStringMeasurement(); return result; } });