@qooxdoo/framework
Version:
The JS Framework for Coders
646 lines (567 loc) • 20.3 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(element, source, repeat, style) {
var tag = this.getTagName(repeat, source);
if (tag != element.tagName.toLowerCase()) {
// The "no-repeat" means that `getTagName` will suggest a `div` as opposed to an `img` tag, preferring to use
// `img` only for things that need scaling. The Desktop `qx.ui.*` will always follow this rule, but it
// is valid for virtual DOM (`qx.html.*`) to be used to create a no-repeat `img` tag. Ignore the validation
// for `no-repeat` `img`.
if (repeat != "no-repeat" || element.tagName.toLowerCase() != "img") {
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(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["lineHeight"] = 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)) {
let value = styles[_style];
if (value !== null) {
css += qx.bom.Style.getCssName(_style) + ": " + value + ";";
}
}
}
var charCode = ResourceManager.fromFontUriToCharCode(source);
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(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(source, repeat, style) {
if (!style) {
style = {};
}
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(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(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(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(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(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(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(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(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(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(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;
}
}
}
}
});