@qooxdoo/framework
Version:
The JS Framework for Coders
638 lines (543 loc) • 19.9 kB
JavaScript
/* ************************************************************************
qooxdoo - the new era of web development
http://qooxdoo.org
Copyright:
2004-2008 1&1 Internet AG, Germany, http://www.1und1.de
License:
MIT: https://opensource.org/licenses/MIT
See the LICENSE file in the project's top-level directory for details.
Authors:
* Sebastian Werner (wpbasti)
* Alexander Steitz (aback)
************************************************************************ */
/**
* Powerful creation and update features for images used for decoration
* purposes like for rounded borders, icons, etc.
*
* Includes support for image clipping, PNG alpha channel support, additional
* repeat options like <code>scale-x</code> or <code>scale-y</code>.
*/
qx.Class.define("qx.bom.element.Decoration",
{
/*
*****************************************************************************
STATICS
*****************************************************************************
*/
statics :
{
/** @type {Boolean} Whether clipping hints should be logged */
DEBUG : false,
/** @type {Map} Collect warnings for potential clipped images */
__warnings : {},
/** @type {Map} List of repeat modes which supports the IE AlphaImageLoader */
__alphaFixRepeats : qx.core.Environment.select("engine.name",
{
"mshtml" :
{
"scale-x" : true,
"scale-y" : true,
"scale" : true,
"no-repeat" : true
},
"default" : null
}),
/** @type {Map} Mapping between background repeat and the tag to create */
__repeatToTagname :
{
"scale-x" : "img",
"scale-y" : "img",
"scale" : "img",
"repeat" : "div",
"no-repeat" : "div",
"repeat-x" : "div",
"repeat-y" : "div"
},
/**
* Updates the element to display the given source
* with the repeat option.
*
* @param element {Element} DOM element to update
* @param source {String} Any valid URI
* @param repeat {String} One of <code>scale-x</code>, <code>scale-y</code>,
* <code>scale</code>, <code>repeat</code>, <code>repeat-x</code>,
* <code>repeat-y</code>, <code>repeat</code>
* @param style {Map} Additional styles to apply
*/
update : function(element, source, repeat, style)
{
var tag = this.getTagName(repeat, source);
if (tag != element.tagName.toLowerCase()) {
throw new Error("Image modification not possible because elements could not be replaced at runtime anymore!");
}
var ret = this.getAttributes(source, repeat, style);
if (tag === "img") {
element.src = ret.src || qx.util.ResourceManager.getInstance().toUri("qx/static/blank.gif");
}
// Fix for old background position
if (element.style.backgroundPosition != "" && ret.style.backgroundPosition === undefined) {
ret.style.backgroundPosition = null;
}
// Fix for old clip
if (element.style.clip != "" && ret.style.clip === undefined) {
ret.style.clip = null;
}
// Apply new styles
qx.bom.element.Style.setStyles(element, ret.style);
// we need to apply the filter to prevent black rendering artifacts
// http://blog.hackedbrain.com/archive/2007/05/21/6110.aspx
if (qx.core.Environment.get("css.alphaimageloaderneeded"))
{
try {
element.filters["DXImageTransform.Microsoft.AlphaImageLoader"].apply();
} catch(e) {}
}
},
/**
* Creates the HTML for a decorator image element with the given options.
*
* @param source {String} Any valid URI
* @param repeat {String} One of <code>scale-x</code>, <code>scale-y</code>,
* <code>scale</code>, <code>repeat</code>, <code>repeat-x</code>,
* <code>repeat-y</code>, <code>repeat</code>
* @param style {Map} Additional styles to apply
* @return {String} Decorator image HTML
*/
create : function(source, repeat, style)
{
var tag = this.getTagName(repeat, source);
var ret = this.getAttributes(source, repeat, style);
var css = qx.bom.element.Style.compile(ret.style);
var ResourceManager = qx.util.ResourceManager.getInstance();
if (ResourceManager.isFontUri(source)) {
var font = qx.theme.manager.Font.getInstance().resolve(source.match(/@([^/]+)/)[1]);
var styles = qx.lang.Object.clone(font.getStyles());
styles['width'] = style.width;
styles['height'] = style.height;
styles['fontSize'] = (parseInt(style.width) > parseInt(style.height) ? style.height : style.width);
styles['display'] = style.display;
styles['verticalAlign'] = style.verticalAlign;
styles['position'] = style.position;
var css = "";
for (var _style in styles) {
if (styles.hasOwnProperty(_style)) {
css += qx.bom.Style.getCssName(_style) + ": " + styles[_style] + ";";
}
}
var resource = ResourceManager.getData(source);
var charCode;
if (resource) {
charCode = resource[2];
}
else {
charCode = parseInt(qx.theme.manager.Font.getInstance().resolve(source.match(/@([^/]+)\/(.*)$/)[2]), 16);
qx.core.Assert.assertNumber(charCode, "Font source needs either a glyph name or the unicode number in hex");
}
return '<div style="' + css + '">' + String.fromCharCode(charCode) + '</div>';
}
else {
if (tag === "img") {
return '<img src="' + ret.src + '" style="' + css + '"/>';
} else {
return '<div style="' + css + '"></div>';
}
}
},
/**
* Translates the given repeat option to a tag name. Useful
* for systems which depends on early information of the tag
* name to prepare element like {@link qx.html.Image}.
*
* @param repeat {String} One of <code>scale-x</code>, <code>scale-y</code>,
* <code>scale</code>, <code>repeat</code>, <code>repeat-x</code>,
* <code>repeat-y</code>, <code>repeat</code>
* @param source {String?null} Source used to identify the image format
* @return {String} The tag name: <code>div</code> or <code>img</code>
*/
getTagName : function(repeat, source)
{
if (source && qx.core.Environment.get("css.alphaimageloaderneeded") &&
this.__alphaFixRepeats[repeat] && source.endsWith(".png"))
{
return "div";
}
return this.__repeatToTagname[repeat];
},
/**
* This method is used to collect all needed attributes for
* the tag name detected by {@link #getTagName}.
*
* @param source {String} Image source
* @param repeat {String} Repeat mode of the image
* @param style {Map} Additional styles to apply
* @return {String} Markup for image
*/
getAttributes : function(source, repeat, style)
{
if (!style) {
style = {};
}
if (!style.position) {
style.position = "absolute";
}
if ((qx.core.Environment.get("engine.name") == "mshtml"))
{
// Add a fix for small blocks where IE has a minHeight
// of the fontSize in quirks mode
style.fontSize = 0;
style.lineHeight = 0;
}
else if ((qx.core.Environment.get("engine.name") == "webkit"))
{
// This stops images from being draggable in webkit
style.WebkitUserDrag = "none";
}
var format = qx.util.ResourceManager.getInstance().getImageFormat(source) ||
qx.io.ImageLoader.getFormat(source);
if (qx.core.Environment.get("qx.debug"))
{
if (source != null && format == null) {
qx.log.Logger.warn("ImageLoader: Not recognized format of external image '" + source + "'!");
}
}
var result;
// Enable AlphaImageLoader in IE6/IE7/IE8
if (qx.core.Environment.get("css.alphaimageloaderneeded") &&
this.__alphaFixRepeats[repeat] && format === "png")
{
var dimension = this.__getDimension(source);
this.__normalizeWidthHeight(style, dimension.width, dimension.height);
result = this.processAlphaFix(style, repeat, source);
}
else
{
delete style.clip;
if (repeat === "scale") {
result = this.__processScale(style, repeat, source);
} else if (repeat === "scale-x" || repeat === "scale-y") {
result = this.__processScaleXScaleY(style, repeat, source);
} else {
// Native repeats or "no-repeat"
result = this.__processRepeats(style, repeat, source);
}
}
return result;
},
/**
* Normalize the given width and height values
*
* @param style {Map} style information
* @param width {Integer?null} width as number or null
* @param height {Integer?null} height as number or null
*/
__normalizeWidthHeight : function(style, width, height)
{
if (style.width == null && width != null) {
style.width = width + "px";
}
if (style.height == null && height != null) {
style.height = height + "px";
}
},
/**
* Returns the dimension of the image by calling
* {@link qx.util.ResourceManager} or {@link qx.io.ImageLoader}
* depending on if the image is a managed one.
*
* @param source {String} image source
* @return {Map} dimension of image
*/
__getDimension : function(source)
{
var width = qx.util.ResourceManager.getInstance().getImageWidth(source) ||
qx.io.ImageLoader.getWidth(source);
var height = qx.util.ResourceManager.getInstance().getImageHeight(source) ||
qx.io.ImageLoader.getHeight(source);
return {
width: width,
height: height
};
},
/**
* Get all styles for IE browser which need to load the image
* with the help of the AlphaImageLoader
*
* @param style {Map} style information
* @param repeat {String} repeat mode
* @param source {String} image source
*
* @return {Map} style infos
*/
processAlphaFix : function(style, repeat, source)
{
if (repeat == "repeat" || repeat == "repeat-x" || repeat == "repeat-y") {
return style;
}
var sizingMethod = repeat == "no-repeat" ? "crop" : "scale";
var filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" +
qx.util.ResourceManager.getInstance().toUri(source) +
"', sizingMethod='" + sizingMethod + "')";
style.filter = filter;
style.backgroundImage = style.backgroundRepeat = "";
delete style["background-image"];
delete style["background-repeat"];
return {
style : style
};
},
/**
* Process scaled images.
*
* @param style {Map} style information
* @param repeat {String} repeat mode
* @param source {String} image source
*
* @return {Map} image URI and style infos
*/
__processScale : function(style, repeat, source)
{
var uri = qx.util.ResourceManager.getInstance().toUri(source);
var dimension = this.__getDimension(source);
this.__normalizeWidthHeight(style, dimension.width, dimension.height);
return {
src : uri,
style : style
};
},
/**
* Process images which are either scaled horizontally or
* vertically.
*
* @param style {Map} style information
* @param repeat {String} repeat mode
* @param sourceid {String} image resource id
*
* @return {Map} image URI and style infos
*/
__processScaleXScaleY : function(style, repeat, sourceid)
{
var ResourceManager = qx.util.ResourceManager.getInstance();
var clipped = ResourceManager.getCombinedFormat(sourceid);
var dimension = this.__getDimension(sourceid);
var uri;
if (clipped)
{
var data = ResourceManager.getData(sourceid);
var combinedid = data[4];
if (clipped == "b64") {
uri = ResourceManager.toDataUri(sourceid);
}
else {
uri = ResourceManager.toUri(combinedid);
}
if (repeat === "scale-x") {
style = this.__getStylesForClippedScaleX(style, data, dimension.height);
} else {
style = this.__getStylesForClippedScaleY(style, data, dimension.width);
}
return {
src : uri,
style : style
};
}
// No clipped image available
else
{
if (qx.core.Environment.get("qx.debug")) {
this.__checkForPotentialClippedImage(sourceid);
}
if (repeat == "scale-x")
{
style.height = dimension.height == null ? null : dimension.height + "px";
// note: width is given by the user
}
else if (repeat == "scale-y")
{
style.width = dimension.width == null ? null : dimension.width + "px";
// note: height is given by the user
}
uri = ResourceManager.toUri(sourceid);
return {
src : uri,
style : style
};
}
},
/**
* Generates the style infos for horizontally scaled clipped images.
*
* @param style {Map} style infos
* @param data {Array} image data retrieved from the {@link qx.util.ResourceManager}
* @param height {Integer} image height
*
* @return {Map} style infos and image URI
*/
__getStylesForClippedScaleX : function(style, data, height)
{
// Use clipped image (multi-images on x-axis)
var imageHeight = qx.util.ResourceManager.getInstance().getImageHeight(data[4]);
// Add size and clipping
style.clip = {top: -data[6], height: height};
style.height = imageHeight + "px";
// note: width is given by the user
// Fix user given y-coordinate to include the combined image offset
if (style.top != null) {
style.top = (parseInt(style.top, 10) + data[6]) + "px";
} else if (style.bottom != null) {
style.bottom = (parseInt(style.bottom, 10) + height - imageHeight - data[6]) + "px";
}
return style;
},
/**
* Generates the style infos for vertically scaled clipped images.
*
* @param style {Map} style infos
* @param data {Array} image data retrieved from the {@link qx.util.ResourceManager}
* @param width {Integer} image width
*
* @return {Map} style infos and image URI
*/
__getStylesForClippedScaleY : function(style, data, width)
{
// Use clipped image (multi-images on x-axis)
var imageWidth = qx.util.ResourceManager.getInstance().getImageWidth(data[4]);
// Add size and clipping
style.clip = {left: -data[5], width: width};
style.width = imageWidth + "px";
// note: height is given by the user
// Fix user given x-coordinate to include the combined image offset
if (style.left != null) {
style.left = (parseInt(style.left, 10) + data[5]) + "px";
} else if (style.right != null) {
style.right = (parseInt(style.right, 10) + width - imageWidth - data[5]) + "px";
}
return style;
},
/**
* Process repeated images.
*
* @param style {Map} style information
* @param repeat {String} repeat mode
* @param sourceid {String} image resource id
*
* @return {Map} image URI and style infos
*/
__processRepeats : function(style, repeat, sourceid)
{
var ResourceManager = qx.util.ResourceManager.getInstance();
var clipped = ResourceManager.getCombinedFormat(sourceid);
var dimension = this.__getDimension(sourceid);
// Double axis repeats cannot be clipped
if (clipped && repeat !== "repeat")
{
// data = [ 8, 5, "png", "qx", "qx/decoration/Modern/arrows-combined.png", -36, 0]
var data = ResourceManager.getData(sourceid);
var combinedid = data[4];
if (clipped == "b64")
{
var uri = ResourceManager.toDataUri(sourceid);
var offx = 0;
var offy = 0;
}
else
{
var uri = ResourceManager.toUri(combinedid);
var offx = data[5];
var offy = data[6];
// honor padding for combined images
if (style.paddingTop || style.paddingLeft || style.paddingRight || style.paddingBottom) {
var top = style.paddingTop || 0;
var left = style.paddingLeft || 0;
offx += style.paddingLeft || 0;
offy += style.paddingTop || 0;
style.clip = {left: left, top: top, width: dimension.width, height: dimension.height};
}
}
var bg = qx.bom.element.Background.getStyles(uri, repeat, offx, offy);
for (var key in bg) {
style[key] = bg[key];
}
if (dimension.width != null && style.width == null && (repeat == "repeat-y" || repeat === "no-repeat")) {
style.width = dimension.width + "px";
}
if (dimension.height != null && style.height == null && (repeat == "repeat-x" || repeat === "no-repeat")) {
style.height = dimension.height + "px";
}
return {
style : style
};
}
else
{
// honor padding
var top = style.paddingTop || 0;
var left = style.paddingLeft || 0;
style.backgroundPosition = left + "px " + top + "px";
if (qx.core.Environment.get("qx.debug"))
{
if (repeat !== "repeat") {
this.__checkForPotentialClippedImage(sourceid);
}
}
this.__normalizeWidthHeight(style, dimension.width, dimension.height);
this.__getStylesForSingleRepeat(style, sourceid, repeat);
return {
style : style
};
}
},
/**
* Generate all style infos for single repeated images
*
* @param style {Map} style information
* @param repeat {String} repeat mode
* @param source {String} image source
*/
__getStylesForSingleRepeat : function(style, source, repeat)
{
// retrieve the "backgroundPosition" style if available to prevent
// overwriting with default values
var top = null;
var left = null;
if (style.backgroundPosition)
{
var backgroundPosition = style.backgroundPosition.split(" ");
left = parseInt(backgroundPosition[0], 10);
if (isNaN(left)) {
left = backgroundPosition[0];
}
top = parseInt(backgroundPosition[1], 10);
if (isNaN(top)) {
top = backgroundPosition[1];
}
}
var bg = qx.bom.element.Background.getStyles(source, repeat, left, top);
for (var key in bg) {
style[key] = bg[key];
}
// Reset the AlphaImageLoader filter if applied
// This prevents IE from setting BOTH CSS filter AND backgroundImage
// This is only a fallback if the image is not recognized as PNG
// If it's a Alpha-PNG file it *may* result in display problems
if (style.filter) {
style.filter = "";
}
},
/**
* Output a warning if the image can be clipped.
*
* @param source {String} image source
*/
__checkForPotentialClippedImage : function(source)
{
if (this.DEBUG && qx.util.ResourceManager.getInstance().has(source) && source.indexOf("qx/icon") == -1)
{
if (!this.__warnings[source])
{
qx.log.Logger.debug("Potential clipped image candidate: " + source);
this.__warnings[source] = true;
}
}
}
}
});