@qooxdoo/framework
Version:
The JS Framework for Coders
1,084 lines (902 loc) • 30.5 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:
* Fabian Jakobs (fjakobs)
* Sebastian Werner (wpbasti)
************************************************************************ */
/**
* The image class displays an image file
*
* This class supports image clipping, which means that multiple images can be combined
* into one large image and only the relevant part is shown.
*
* *Example*
*
* Here is a little example of how to use the widget.
*
* <pre class='javascript'>
* var image = new qx.ui.basic.Image("icon/32/actions/format-justify-left.png");
*
* this.getRoot().add(image);
* </pre>
*
* This example create a widget to display the image
* <code>icon/32/actions/format-justify-left.png</code>.
*
* *External Documentation*
*
* <a href='http://manual.qooxdoo.org/${qxversion}/pages/widget/image.html' target='_blank'>
* Documentation of this widget in the qooxdoo manual.</a>
*
* NOTE: Instances of this class must be disposed of after use
*
*/
qx.Class.define("qx.ui.basic.Image",
{
extend : qx.ui.core.Widget,
/*
*****************************************************************************
CONSTRUCTOR
*****************************************************************************
*/
/**
* @param source {String?null} The URL of the image to display.
*/
construct : function(source)
{
this.__contentElements = {};
this.base(arguments);
if (source) {
this.setSource(source);
}
},
/*
*****************************************************************************
PROPERTIES
*****************************************************************************
*/
properties :
{
/** The URL of the image. Setting it will possibly abort loading of current image. */
source :
{
check : "String",
init : null,
nullable : true,
event : "changeSource",
apply : "_applySource",
themeable : true
},
/**
* Whether the image should be scaled to the given dimensions
*
* This is disabled by default because it prevents the usage
* of image clipping when enabled.
*/
scale :
{
check : "Boolean",
init : false,
event : "changeScale",
themeable : true,
apply : "_applyScale"
},
// overridden
appearance :
{
refine : true,
init : "image"
},
// overridden
allowShrinkX :
{
refine : true,
init : false
},
// overridden
allowShrinkY :
{
refine : true,
init : false
},
// overridden
allowGrowX :
{
refine : true,
init : false
},
// overridden
allowGrowY :
{
refine : true,
init : false
}
},
/*
*****************************************************************************
EVENTS
*****************************************************************************
*/
events :
{
/**
* Fired if the image source can not be loaded. This event can only be
* fired for the first loading of an unmanaged resource (external image).
*/
loadingFailed : "qx.event.type.Event",
/**
* Fired if the image has been loaded. This is even true for managed
* resources (images known by generator).
*/
loaded : "qx.event.type.Event",
/** Fired when the pending request has been aborted. */
aborted : "qx.event.type.Event"
},
statics:
{
PLACEHOLDER_IMAGE: "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
},
/*
*****************************************************************************
MEMBERS
*****************************************************************************
*/
members :
{
__width : null,
__height : null,
__mode : null,
__contentElements : null,
__currentContentElement : null,
__wrapper : null,
__requestId : 0,
// overridden
_onChangeTheme : function() {
this.base(arguments);
// restyle source (theme change might have changed the resolved url)
this._styleSource();
},
/*
---------------------------------------------------------------------------
WIDGET API
---------------------------------------------------------------------------
*/
// overridden
getContentElement : function() {
return this.__getSuitableContentElement();
},
// overridden
_createContentElement : function() {
return this.__getSuitableContentElement();
},
// overridden
_getContentHint : function()
{
return {
width : this.__width || 0,
height : this.__height || 0
};
},
// overridden
_applyDecorator : function(value, old) {
this.base(arguments, value, old);
var source = this.getSource();
source = qx.util.AliasManager.getInstance().resolve(source);
var el = this.getContentElement();
if (this.__wrapper) {
el = el.getChild(0);
}
this.__setSource(el, source);
},
// overridden
_applyTextColor : function(value)
{
if (this.__getMode() === "font") {
var el = this.getContentElement();
if (this.__wrapper) {
el = el.getChild(0);
}
if (value) {
el.setStyle("color", qx.theme.manager.Color.getInstance().resolve(value));
} else {
el.removeStyle("color");
}
}
},
// overridden
_applyPadding : function(value, old, name)
{
this.base(arguments, value, old, name);
var element = this.getContentElement();
if (this.__wrapper) {
element.getChild(0).setStyles({
top: this.getPaddingTop() || 0,
left: this.getPaddingLeft() || 0
});
} else if (this.__getMode() === 'font') {
element.setStyles({
top: this.getPaddingTop() || 0,
left: this.getPaddingLeft() || 0
});
} else {
element.setPadding(
this.getPaddingLeft() || 0, this.getPaddingTop() || 0
);
}
},
renderLayout : function(left, top, width, height) {
this.base(arguments, left, top, width, height);
var element = this.getContentElement();
if (this.__wrapper) {
element.getChild(0).setStyles({
width: width - (this.getPaddingLeft() || 0) - (this.getPaddingRight() || 0),
height: height - (this.getPaddingTop() || 0) - (this.getPaddingBottom() || 0),
top: this.getPaddingTop() || 0,
left: this.getPaddingLeft() || 0
});
}
},
/*
---------------------------------------------------------------------------
IMAGE API
---------------------------------------------------------------------------
*/
// property apply, overridden
_applyEnabled : function(value, old)
{
this.base(arguments, value, old);
if (this.getSource()) {
this._styleSource();
}
},
// property apply
_applySource : function(value, old)
{
// abort loading current image
if (old) {
if (qx.io.ImageLoader.isLoading(old)) {
qx.io.ImageLoader.abort(old);
}
}
this._styleSource();
},
// property apply
_applyScale : function(value) {
this._styleSource();
},
/**
* Remembers the mode to keep track which contentElement is currently in use.
* @param mode {String} internal mode (alphaScaled|scaled|nonScaled)
*/
__setMode : function(mode) {
this.__mode = mode;
},
/**
* Returns the current mode if set. Otherwise checks the current source and
* the current scaling to determine the current mode.
*
* @return {String} current internal mode
*/
__getMode : function()
{
if (this.__mode == null)
{
var source = this.getSource();
if (source && qx.lang.String.startsWith(source, "@")) {
this.__mode = "font";
}
var isPng = false;
if (source != null) {
isPng = source.endsWith(".png");
}
if (this.getScale() && isPng && qx.core.Environment.get("css.alphaimageloaderneeded")) {
this.__mode = "alphaScaled";
} else if (this.getScale()) {
this.__mode = "scaled";
} else {
this.__mode = "nonScaled";
}
}
return this.__mode;
},
/**
* Creates a contentElement suitable for the current mode
*
* @param mode {String} internal mode
* @return {qx.html.Image} suitable image content element
*/
__createSuitableContentElement : function(mode)
{
var scale;
var tagName;
var clazz = qx.html.Image;
switch (mode) {
case "font":
clazz = qx.html.Label;
scale = true;
tagName = "div";
break;
case "alphaScaled":
scale = true;
tagName = "div";
break;
case "nonScaled":
scale = false;
tagName = "div";
break;
default:
scale = true;
tagName = "img";
break;
}
var element = new (clazz)(tagName);
element.connectWidget(this);
element.setStyles({
"overflowX": "hidden",
"overflowY": "hidden",
"boxSizing": "border-box"
});
if (mode == "font") {
element.setRich(true);
}
else {
element.setScale(scale);
if (qx.core.Environment.get("css.alphaimageloaderneeded")) {
var wrapper = this.__wrapper = new qx.html.Element("div");
element.connectWidget(this);
wrapper.setStyle("position", "absolute");
wrapper.add(element);
return wrapper;
}
}
return element;
},
/**
* Returns a contentElement suitable for the current mode
*
* @return {qx.html.Image} suitable image contentElement
*/
__getSuitableContentElement : function()
{
if (this.$$disposed) {
return null;
}
var mode = this.__getMode();
if (this.__contentElements[mode] == null) {
this.__contentElements[mode] = this.__createSuitableContentElement(mode);
}
var element = this.__contentElements[mode];
if (!this.__currentContentElement) {
this.__currentContentElement = element;
}
return element;
},
/**
* Applies the source to the clipped image instance or preload
* an image to detect sizes and apply it afterwards.
*
*/
_styleSource : function()
{
var AliasManager = qx.util.AliasManager.getInstance();
var ResourceManager = qx.util.ResourceManager.getInstance();
var source = AliasManager.resolve(this.getSource());
var element = this.getContentElement();
if (this.__wrapper) {
element = element.getChild(0);
}
if (!source)
{
this.__resetSource(element);
return;
}
this.__checkForContentElementSwitch(source);
if ((qx.core.Environment.get("engine.name") == "mshtml") &&
(parseInt(qx.core.Environment.get("engine.version"), 10) < 9 ||
qx.core.Environment.get("browser.documentmode") < 9))
{
var repeat = this.getScale() ? "scale" : "no-repeat";
element.tagNameHint = qx.bom.element.Decoration.getTagName(repeat, source);
}
var contentEl = this.__getContentElement();
// Detect if the image registry knows this image
if (ResourceManager.isFontUri(source)) {
this.__setManagedImage(contentEl, source);
var color = this.getTextColor();
if (qx.lang.Type.isString(color)) {
this._applyTextColor(color, null);
}
}
else if (ResourceManager.has(source)) {
var highResolutionSource = ResourceManager.findHighResolutionSource(source);
if (highResolutionSource) {
var imageWidth = ResourceManager.getImageWidth(source);
var imageHeight = ResourceManager.getImageHeight(source);
this.setWidth(imageWidth);
this.setHeight(imageHeight);
// set background size on current element (div or img)
var backgroundSize = imageWidth + "px, " + imageHeight + "px";
this.__currentContentElement.setStyle("background-size", backgroundSize);
this.setSource(highResolutionSource);
source = highResolutionSource;
}
this.__setManagedImage(contentEl, source);
this.__fireLoadEvent();
} else if (qx.io.ImageLoader.isLoaded(source)) {
this.__setUnmanagedImage(contentEl, source);
this.__fireLoadEvent();
} else {
this.__loadUnmanagedImage(contentEl, source);
}
},
/**
* Helper function, which fires <code>loaded</code> event asynchronously.
* It emulates native <code>loaded</code> event of an image object. This
* helper will be called, if you try to load a managed image or an
* previously loaded unmanaged image.
*/
__fireLoadEvent : function()
{
this.__requestId++;
qx.bom.AnimationFrame.request(function(rId){
// prevent firing of the event if source changed in the meantime
if (rId === this.__requestId) {
this.fireEvent("loaded");
} else {
this.fireEvent("aborted");
}
}.bind(this, this.__requestId));
},
/**
* Returns the content element.
* @return {qx.html.Image} content element
*/
__getContentElement : function()
{
var contentEl = this.__currentContentElement;
if (this.__wrapper) {
contentEl = contentEl.getChild(0);
}
return contentEl;
},
/**
* Checks if the current content element is capable to display the image
* with the current settings (scaling, alpha PNG)
*
* @param source {String} source of the image
*/
__checkForContentElementSwitch : qx.core.Environment.select("engine.name",
{
"mshtml" : function(source)
{
var alphaImageLoader = qx.core.Environment.get("css.alphaimageloaderneeded");
var isPng = source.endsWith(".png");
var isFont = source.startsWith("@");
if (isFont) {
this.__setMode("font");
} else if (alphaImageLoader && isPng)
{
if (this.getScale() && this.__getMode() != "alphaScaled") {
this.__setMode("alphaScaled");
} else if (!this.getScale() && this.__getMode() != "nonScaled") {
this.__setMode("nonScaled");
}
}
else
{
if (this.getScale() && this.__getMode() != "scaled") {
this.__setMode("scaled");
} else if (!this.getScale() && this.__getMode() != "nonScaled") {
this.__setMode("nonScaled");
}
}
this.__checkForContentElementReplacement(this.__getSuitableContentElement());
},
"default" : function(source)
{
var isFont = source && qx.lang.String.startsWith(source, "@");
if (isFont) {
this.__setMode("font");
} else if (this.getScale() && this.__getMode() != "scaled") {
this.__setMode("scaled");
} else if (!this.getScale() && this.__getMode() != "nonScaled") {
this.__setMode("nonScaled");
}
this.__checkForContentElementReplacement(this.__getSuitableContentElement());
}
}),
/**
* Checks the current child and replaces it if necessary
*
* @param elementToAdd {qx.html.Image} content element to add
*/
__checkForContentElementReplacement : function(elementToAdd)
{
var currentContentElement = this.__currentContentElement;
if (currentContentElement != elementToAdd)
{
if (currentContentElement != null)
{
var pixel = "px";
var styles = {};
//inherit styles from current element
var currentStyles = currentContentElement.getAllStyles();
if(currentStyles) {
for(var prop in currentStyles) {
styles[prop] = currentStyles[prop];
}
}
// Don't transfer background image when switching from image to icon font
if (this.__getMode() === "font") {
delete styles.backgroundImage;
}
// Copy dimension and location of the current content element
var bounds = this.getBounds();
if (bounds != null)
{
styles.width = bounds.width + pixel;
styles.height = bounds.height + pixel;
}
var insets = this.getInsets();
styles.left = parseInt(currentContentElement.getStyle("left") || insets.left) + pixel;
styles.top = parseInt(currentContentElement.getStyle("top") || insets.top) + pixel;
styles.zIndex = 10;
var newEl = this.__wrapper ? elementToAdd.getChild(0) : elementToAdd;
newEl.setStyles(styles, true);
newEl.setSelectable(this.getSelectable());
if (!currentContentElement.isVisible()) {
elementToAdd.hide();
} else if (!elementToAdd.isVisible()) {
elementToAdd.show();
}
if (!currentContentElement.isIncluded()) {
elementToAdd.exclude();
} else if (!elementToAdd.isIncluded()) {
elementToAdd.include();
}
var container = currentContentElement.getParent();
if (container) {
var index = container.getChildren().indexOf(currentContentElement);
container.removeAt(index);
container.addAt(elementToAdd, index);
}
// force re-application of source so __setSource is called again
var hint = newEl.getNodeName();
if (newEl.setSource) {
newEl.setSource(null);
}
else {
newEl.setValue("");
}
var currentEl = this.__getContentElement();
newEl.tagNameHint = hint;
newEl.setAttribute("class", currentEl.getAttribute("class"));
// Flush elements to make sure the DOM elements are created.
qx.html.Element.flush();
var currentDomEl = currentEl.getDomElement();
var newDomEl = elementToAdd.getDomElement();
// copy event listeners
var listeners = currentContentElement.getListeners() || [];
listeners.forEach(function(listenerData) {
elementToAdd.addListener(listenerData.type, listenerData.handler, listenerData.self, listenerData.capture);
});
if (currentDomEl && newDomEl) {
// Switch the DOM elements' hash codes. This is required for the event
// layer to work [BUG #7447]
var currentHash = currentDomEl.$$hash;
currentDomEl.$$hash = newDomEl.$$hash;
newDomEl.$$hash = currentHash;
}
this.__currentContentElement = elementToAdd;
}
}
},
/**
* Use the ResourceManager to set a managed image
*
* @param el {Element} image DOM element
* @param source {String} source path
*/
__setManagedImage : function(el, source)
{
var ResourceManager = qx.util.ResourceManager.getInstance();
var isFont = ResourceManager.isFontUri(source);
// Try to find a disabled image in registry
if (!this.getEnabled())
{
var disabled = source.replace(/\.([a-z]+)$/, "-disabled.$1");
if (!isFont && ResourceManager.has(disabled))
{
source = disabled;
this.addState("replacement");
}
else
{
this.removeState("replacement");
}
}
// Optimize case for enabled changes when no disabled image was found
if (!isFont && el.getSource() === source) {
return;
}
// Special case for non resource manager handled font icons
if (isFont) {
// Don't use scale if size is set via postfix
if (this.getScale() && parseInt(source.split("/")[2], 10)) {
this.setScale(false);
}
// Adjust size if scaling is applied
var width;
var height;
if (this.getScale()) {
var hint = this.getSizeHint();
width = this.getWidth() || hint.width;
height = this.getHeight() || hint.height;
}
else {
var font = qx.theme.manager.Font.getInstance().resolve(source.match(/@([^/]+)/)[1]);
if (qx.core.Environment.get("qx.debug")) {
this.assertObject(font, "Virtual image source contains unkown font descriptor");
}
var size = parseInt(source.split("/")[2] || font.getSize(), 10);
width = ResourceManager.getImageWidth(source) || size;
height = ResourceManager.getImageHeight(source) || size;
}
this.__updateContentHint(width, height);
this.__setSource(el, source);
// Apply source
}
else {
// Apply source
this.__setSource(el, source);
// Compare with old sizes and relayout if necessary
this.__updateContentHint(
ResourceManager.getImageWidth(source),
ResourceManager.getImageHeight(source)
);
}
},
_applyDimension : function()
{
this.base(arguments);
var isFont = this.getSource() && qx.lang.String.startsWith(this.getSource(), "@");
if (isFont) {
var el = this.getContentElement();
if (el) {
if (this.getScale()) {
var hint = this.getSizeHint();
var width = this.getWidth() || hint.width || 40;
var height = this.getHeight() || hint.height || 40;
el.setStyle("fontSize", (width > height ? height : width) + "px");
}
else {
var font = qx.theme.manager.Font.getInstance().resolve(this.getSource().match(/@([^/]+)/)[1]);
el.setStyle("fontSize", font.getSize() + "px");
}
}
}
},
/**
* Use the infos of the ImageLoader to set an unmanaged image
*
* @param el {Element} image DOM element
* @param source {String} source path
*/
__setUnmanagedImage : function(el, source)
{
var ImageLoader = qx.io.ImageLoader;
// Apply source
this.__setSource(el, source);
// Compare with old sizes and relayout if necessary
var width = ImageLoader.getWidth(source);
var height = ImageLoader.getHeight(source);
this.__updateContentHint(width, height);
},
/**
* Use the ImageLoader to load an unmanaged image
*
* @param el {Element} image DOM element
* @param source {String} source path
*/
__loadUnmanagedImage : function(el, source)
{
var ImageLoader = qx.io.ImageLoader;
if (qx.core.Environment.get("qx.debug"))
{
// loading external images via HTTP/HTTPS is a common usecase, as is
// using data URLs.
var sourceLC = source.toLowerCase();
if (!sourceLC.startsWith("http") &&
!sourceLC.startsWith("data:image/"))
{
var self = this.self(arguments);
if (!self.__warned) {
self.__warned = {};
}
if (!self.__warned[source])
{
this.debug("try to load an unmanaged relative image: " + source);
self.__warned[source] = true;
}
}
}
// only try to load the image if it not already failed
if(!ImageLoader.isFailed(source)) {
ImageLoader.load(source, this.__loaderCallback, this);
} else {
this.__resetSource(el);
}
},
/**
* Reset source displayed by the DOM element.
*
* @param el {Element} image DOM element
*/
__resetSource : function(el)
{
if (el != null) {
if (el instanceof qx.html.Image) {
el.resetSource();
}
else {
el.resetValue();
}
}
},
/**
* Combines the decorator's image styles with our own image to make sure
* gradient and backgroundImage decorators work on Images.
*
* @param el {Element} image DOM element
* @param source {String} source path
*/
__setSource: function (el, source) {
var isFont = source && qx.lang.String.startsWith(source, "@");
if (isFont) {
var sparts = source.split("/");
var fontSource = source;
if (sparts.length > 2) {
fontSource = sparts[0] + "/" + sparts[1];
}
var ResourceManager = qx.util.ResourceManager.getInstance();
var font = qx.theme.manager.Font.getInstance().resolve(source.match(/@([^/]+)/)[1]);
var fontStyles = qx.lang.Object.clone(font.getStyles());
delete fontStyles.color;
el.setStyles(fontStyles);
el.setStyle("font");
el.setStyle("display", "table-cell");
el.setStyle("verticalAlign", "middle");
el.setStyle("textAlign", "center");
if (this.getScale()) {
el.setStyle("fontSize", (this.__width > this.__height ? this.__height : this.__width) + "px");
}
else {
var size = parseInt(sparts[2] || qx.theme.manager.Font.getInstance().resolve(source.match(/@([^/]+)/)[1]).getSize());
el.setStyle("fontSize", size + "px");
}
var resource = ResourceManager.getData(fontSource);
if (resource) {
el.setValue(String.fromCharCode(resource[2]));
}
else {
var charCode = parseInt(qx.theme.manager.Font.getInstance().resolve(source.match(/@([^/]+)\/(.*)$/)[2]), 16);
if (qx.core.Environment.get("qx.debug")) {
this.assertNumber(charCode, "Font source needs either a glyph name or the unicode number in hex");
}
el.setValue(String.fromCharCode(charCode));
}
return;
}
else if (el.getNodeName() == "div") {
// checks if a decorator already set.
// In this case we have to merge background styles
var decorator = qx.theme.manager.Decoration.getInstance().resolve(this.getDecorator());
if (decorator) {
var hasGradient = (decorator.getStartColor() && decorator.getEndColor());
var hasBackground = decorator.getBackgroundImage();
if (hasGradient || hasBackground) {
var repeat = this.getScale() ? "scale" : "no-repeat";
// get the style attributes for the given source
var attr = qx.bom.element.Decoration.getAttributes(source, repeat);
// get the background image(s) defined by the decorator
var decoratorStyle = decorator.getStyles(true);
var combinedStyles = {
"backgroundImage": attr.style.backgroundImage,
"backgroundPosition": (attr.style.backgroundPosition || "0 0"),
"backgroundRepeat": (attr.style.backgroundRepeat || "no-repeat")
};
if (hasBackground) {
combinedStyles["backgroundPosition"] += "," + decoratorStyle["background-position"] || "0 0";
combinedStyles["backgroundRepeat"] += ", " + decorator.getBackgroundRepeat();
}
if (hasGradient) {
combinedStyles["backgroundPosition"] += ", 0 0";
combinedStyles["backgroundRepeat"] += ", no-repeat";
}
combinedStyles["backgroundImage"] += "," + (decoratorStyle["background-image"] || decoratorStyle["background"]);
// apply combined background images
el.setStyles(combinedStyles);
return;
}
} else {
// force re-apply to remove old decorator styles
if (el.setSource){
el.setSource(null);
}
}
}
if (el.setSource){
el.setSource(source);
}
},
/**
* Event handler fired after the preloader has finished loading the icon
*
* @param source {String} Image source which was loaded
* @param imageInfo {Map} Dimensions of the loaded image
*/
__loaderCallback : function(source, imageInfo)
{
// Ignore the callback on already disposed images
if (this.$$disposed === true) {
return;
}
// Ignore when the source has already been modified
if (source !== qx.util.AliasManager.getInstance().resolve(this.getSource())) {
this.fireEvent("aborted");
return;
}
/// Output a warning if the image could not loaded and quit
if (imageInfo.failed) {
this.warn("Image could not be loaded: " + source);
this.fireEvent("loadingFailed");
} else if (imageInfo.aborted) {
this.fireEvent("aborted");
return;
} else {
this.fireEvent("loaded");
}
// Update image
this.__setUnmanagedImage(this.__getContentElement(), source);
},
/**
* Updates the content hint when the image size has been changed
*
* @param width {Integer} width of the image
* @param height {Integer} height of the image
*/
__updateContentHint : function(width, height)
{
// Compare with old sizes and relayout if necessary
if (width !== this.__width || height !== this.__height)
{
this.__width = width;
this.__height = height;
qx.ui.core.queue.Layout.add(this);
}
}
},
/*
*****************************************************************************
DESTRUCTOR
*****************************************************************************
*/
destruct : function() {
for (var mode in this.__contentElements) {
if (this.__contentElements.hasOwnProperty(mode)) {
this.__contentElements[mode].disconnectWidget(this);
}
}
delete this.__currentContentElement;
if (this.__wrapper) {
delete this.__wrapper;
}
this._disposeMap("__contentElements");
}
});