1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc.
  5  *
  6  * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at: https://www.zimbra.com/license
  9  * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15
 10  * have been added to cover use of software over a computer network and provide for limited attribution
 11  * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B.
 12  *
 13  * Software distributed under the License is distributed on an "AS IS" basis,
 14  * WITHOUT WARRANTY OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing rights and limitations under the License.
 16  * The Original Code is Zimbra Open Source Web Client.
 17  * The Initial Developer of the Original Code is Zimbra, Inc.  All rights to the Original Code were
 18  * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015.
 19  *
 20  * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 
 25 /**
 26  * @class
 27  * This static class provides basic image support by using CSS and background 
 28  * images rather than <img> tags.
 29  *  
 30  * @author Conrad Damon
 31  * @author Ross Dargahi
 32  * 
 33  * @private
 34  */
 35 AjxImg = function() {};
 36 
 37 AjxImg.prototype = new Object;
 38 AjxImg.prototype.constructor = null;
 39 
 40 AjxImg._VIEWPORT_ID = "AjxImg_VP";
 41 
 42 AjxImg.DISABLED = true;
 43 
 44 AjxImg.RE_COLOR = /^(.*?),color=(.*)$/;
 45 
 46 /**
 47  * This method will set the image for <i>parentEl</i>. <i>parentEl</i> should 
 48  * only contain this image and no other children
 49  *
 50  * @param parentEl 		the parent element for the image
 51  * @param imageName 		the name of the image.  The CSS class for the image will be "Img<imageName>".
 52  * @param useParenEl 	if <code>true</code> will use the parent element as the root for the image and will not create an intermediate DIV
 53  * @param _disabled		if <code>true</code>, will append " ZDisabledImage" to the CSS class for the image,
 54  * @param {array}       classes             array of class names to be applied to this image
 55  *							which will make the image partly transparent
 56  * @param {string}		altText			alternative text for non-visual users
 57  */
 58 AjxImg.setImage =
 59 function(parentEl, imageName, useParentEl, _disabled, classes, altText) {
 60 	
 61 	if (!parentEl) { return; }
 62 	
 63 	classes = classes || [];
 64 	var origImageName = imageName;
 65     var color, m = imageName && imageName.match(AjxImg.RE_COLOR);
 66 	if (m) {
 67 		imageName = m && m[1];
 68 		color = m && m[2];
 69 	}
 70 
 71 	var className = AjxImg.getClassForImage(imageName, _disabled);
 72 	if (useParentEl) {
 73 		classes.push(className);
 74 		parentEl.className = classes.join(" ");
 75 		return;
 76 	}
 77 	var id = parentEl.firstChild && parentEl.firstChild.id;
 78         
 79 	var overlayName = className+"Overlay";
 80 	var maskName = className+"Mask";
 81 	if (color && window.AjxImgData && AjxImgData[overlayName] && AjxImgData[maskName]) {
 82 		color = (color.match(/^\d$/) ? ZmOrganizer.COLOR_VALUES[color] : color) ||
 83 				ZmOrganizer.COLOR_VALUES[ZmOrganizer.ORG_DEFAULT_COLOR];
 84 		parentEl.innerHTML = AjxImg.getImageHtml({
 85 			imageName: origImageName,
 86 			attrStr: id ? "id='"+id+"'" : null,
 87 			altText: altText,
 88 			disabled: _disabled
 89 		});
 90 		return;
 91 	}
 92 
 93 	if (parentEl.firstChild == null || parentEl.firstChild.nodeName.toLowerCase() != "div") {
 94 		var html = [], i = 0;
 95 		html[i++] = "<div ";
 96 		if (id) {
 97 			html[i++] = " id='";
 98 			html[i++] = id;
 99 			html[i++] = "' ";
100 		}
101 		if (className) {
102 			classes.push(className);
103 		}
104 		html[i++] = AjxUtil.getClassAttr(classes);
105 		html[i++] = ">";
106 		if (altText) {
107 			html[i++] = "<div class='ScreenReaderOnly'>";
108 			html[i++] = AjxStringUtil.htmlEncode(altText);
109 			html[i++] = "</div>";
110 		}
111 		html[i++] = "</div>";
112 		parentEl.innerHTML = html.join("");
113 		return;
114 	} else if (AjxEnv.isIE && !AjxEnv.isIE9up) {
115 		parentEl.firstChild.innerHTML = "";
116 	}
117 	if (className) {
118 		classes.push(className);
119 	}
120 	parentEl.firstChild.className = classes.join(" ");
121 };
122 
123 AjxImg.setDisabledImage = function(parentEl, imageName, useParentEl, classes) {
124 	return AjxImg.setImage(parentEl, imageName, useParentEl, true, classes);
125 };
126 
127 AjxImg.getClassForImage =
128 function(imageName, disabled) {
129 	var className = imageName ? "Img" + imageName : "";
130 	if (disabled) className += " ZDisabledImage";
131 	return className;
132 };
133 
134 AjxImg.getImageClass =
135 function(parentEl) {
136 	return parentEl.firstChild ? parentEl.firstChild.className : parentEl.className;
137 };
138 
139 AjxImg.getImageElement =
140 function(parentEl) {
141 	return parentEl.firstChild ? parentEl.firstChild : parentEl;
142 };
143 
144 AjxImg.getParentElement =
145 function(imageEl) {
146 	return imageEl.parentNode;
147 };
148 
149 AjxImg.GET_IMAGE_HTML_PARAMS = [
150 	"imageName",
151 	"styles",
152 	"attrStr",
153 	"wrapInTable",
154 	"disabled",
155 	"classes",
156 	"altText"
157 ];
158 
159 /**
160  * Returns the HTML needed to display the given image.
161  *
162  * @param {object}		params		hash of params:
163  * @param {string}		imageName		the image you want to render
164  * @param {string}		styles			optional style info (for example, "display:inline")
165  * @param {string}		attrStr			optional attributes (for example, "id=X748")
166  * @param {boolean}		wrapInTable		if true, wrap the HTML in a TABLE
167  * @param {boolean}		disabled		if true, show image as disabled
168  * @param {array}		classes			array of class names to be applied to this image
169  * @param {string}		altText			alternative text for non-visual users
170  * 
171  * @return	{string}	the image string
172  */
173 AjxImg.getImageHtml = 
174 function() {
175 	var params = Dwt.getParams(arguments, AjxImg.GET_IMAGE_HTML_PARAMS);
176 
177 	var imageName = params.imageName;
178 	var styles = params.styles || "";
179 	var styleStr = styles ? " style='" + styles + "'" : "";
180 	var attrStr = params.attrStr ? " " + params.attrStr : "";
181 	var disabled = params.disabled;
182 	var classes = params.classes || [];
183 	var altText = params.altText;
184 
185 	var pre = params.wrapInTable ? "<table style='display:inline' cellpadding=0 cellspacing=0 border=0><tr><td align=center valign=bottom>" : "";
186     var html = "";
187 	var post = params.wrapInTable ? "</td></tr></table>" : "";
188 
189 	if (imageName) {
190         var color, m = imageName.match(AjxImg.RE_COLOR);
191         if (m) {
192             imageName = m && m[1];
193             color = m && m[2];
194         }
195 
196         var className = AjxImg.getClassForImage(imageName, disabled);
197         var overlayName = className + "Overlay";
198         var maskName = className + "Mask";
199         if (color && window.AjxImgData && AjxImgData[overlayName] && AjxImgData[maskName]) {
200             color = (color.match(/^\d$/) ? ZmOrganizer.COLOR_VALUES[color] : color) ||
201                     ZmOrganizer.COLOR_VALUES[ZmOrganizer.ORG_DEFAULT_COLOR];
202 
203             var overlay = AjxImgData[overlayName], mask = AjxImgData[maskName];
204 
205             // we're creating IMG elements here, so we can use the alt attribute
206             if (altText) {
207                 attrStr += " alt='" + AjxStringUtil.encodeQuotes(altText) + "'";
208             }
209 
210             if (AjxEnv.isIE && !AjxEnv.isIE9up) {
211                 var size = [
212                     "width:", overlay.w, "px;",
213                     "height:", overlay.h, "px;"
214                 ].join("");
215                 var location = [
216                     "top:", mask.t, ";",
217                     "left:", mask.l, ";"
218                 ].join("");
219                 var clip = [
220                     'clip:rect(',
221                     (-1 * mask.t) - 1, 'px,',
222                     overlay.w - 1, 'px,',
223                     (mask.t * -1) + overlay.h - 1, 'px,',
224                     overlay.l, 'px);'
225                 ].join('');
226                 var filter = 'filter:mask(color=' + color + ');';
227                 html = [
228                     // NOTE: Keep in sync with output of ImageMerger.java.
229                     "<div class='IEImage' style='display:inline-block;zoom:1;position:relative;overflow:hidden;", size, styles, "' ", attrStr,">",
230                         "<div class='IEImageMask' style='position:absolute;top:0px;left:0px;", size, "'>",
231                             "<img src='", mask.ief, "?v=", window.cacheKillerVersion, "' border=0 style='position:absolute;", location, filter, "'>",
232                         "</div>",
233                         "<div class='IEImageOverlay ", overlayName, "' style='", size, "position:absolute;top:0;left:0;'></div>",
234                     "</div>"
235                 ].join("");
236             }
237             else {
238                 if (!overlay[color]) {
239                     var width = overlay.w, height = overlay.h;
240 
241                     var canvas = document.createElement("CANVAS");
242                     canvas.width = width;
243                     canvas.height = height;
244 
245                     var ctx = canvas.getContext("2d");
246 
247                     ctx.save();
248                     ctx.clearRect(0,0,width,height);
249 
250                     ctx.save();
251 	                var imgId = attrStr;
252 	                if (!imgId) {
253 		                imgId = Dwt.getNextId("CANVAS_IMG_");  //create an imgId in case we need to update the img.src for an element without an id
254 		                attrStr = " id='" + imgId + "'";
255 	                }
256 	                else {
257 		                var match = attrStr.match(/id=[\"\']([^\"\']+)[\"\']+/);
258 		                if (match && match.length > 1) {
259 			                imgId = match[1]; //extract the ID value
260 		                }
261 		                AjxDebug.println(AjxDebug.TAG_ICON, "imgId = " + imgId);
262 	                }
263 	                var maskElement = document.getElementById(maskName);
264 	                var overlayElement = document.getElementById(overlayName);
265 	                if (!maskElement.complete || !overlayElement.complete) {
266 		                AjxDebug.println(AjxDebug.TAG_ICON, "mask status = " + maskElement.complete + " for " + imgId);
267 		                AjxDebug.println(AjxDebug.TAG_ICON, "overlay status = " + overlayElement.complete + " for " + imgId);
268 						var maskImg = new Image();
269 						maskImg.onload = function() {
270 							AjxDebug.println(AjxDebug.TAG_ICON, "mask image loaded");
271 							var overlayImg = new Image();
272 							overlayImg.onload = function() {
273 								AjxImg._drawCanvasImage(ctx, maskImg, overlayImg, mask, overlay, color, width, height)
274 								AjxDebug.println(AjxDebug.TAG_ICON, "overlay image loaded");
275 								var el = document.getElementById(imgId);
276 								if (el) {
277 									AjxDebug.println(AjxDebug.TAG_ICON, "element found for id = " + imgId);
278 									el.src = canvas.toDataURL();
279 									overlay[color] = canvas.toDataURL(); //only save if successful
280 								}
281 								else {
282 									AjxDebug.println(AjxDebug.TAG_ICON, "no element found for id = " + imgId);
283 								}
284 							}
285 							overlayImg.src = document.getElementById(overlayName).src;
286 	                    }
287 	                    maskImg.src = document.getElementById(maskName).src;
288 	                }
289 	                else {
290 		                //image already downloaded
291 		                AjxImg._drawCanvasImage(ctx, maskElement, overlayElement, mask, overlay, color, width, height);
292 		                overlay[color] = canvas.toDataURL();
293 	                }
294                 }
295 
296                 html = [
297                     "<img src='", overlay[color], "'"," border=0 ", AjxUtil.getClassAttr(classes), styleStr, attrStr, ">"
298                 ].join("");
299             }
300         }
301         else {
302 	        classes.push("Img" + imageName);
303             html = [
304                 "<div ", AjxUtil.getClassAttr(classes), styleStr, attrStr, ">"
305             ];
306             if (altText) {
307                 // alt is invalid on DIVs, so use a hidden element
308                 html.push(
309                     "<span class='ScreenReaderOnly'>",
310                     AjxStringUtil.htmlEncode(altText),
311                     "</span>"
312                 );
313             };
314             html.push("</div>");
315 
316             html = html.join("");
317         }
318 	}
319     else {
320         html = [
321             "<div", styleStr, attrStr, ">"
322         ];
323         if (altText) {
324             // alt is invalid on DIVs, so use a hidden element
325             html.push(
326                 "<span class='ScreenReaderOnly'>",
327                 AjxStringUtil.htmlEncode(altText),
328                 "</span>"
329             );
330         };
331         html.push("</div>");
332         html = html.join("");
333     }
334 	return pre || post ? [pre,html,post].join("") : html;
335 };
336 
337 /**
338  * Gets the "image" as an HTML string.
339  *
340  * @param imageName		     the image you want to render
341  * @param imageStyleStr      optional style info (for example, "display:inline")
342  * @param attrStr		     optional attributes (for example, "id=X748")
343  * @param label			     the text that follows this image
344  * @param containerClassName class to use instead of the default inlineIcon class
345  * @return	{string}	     the image string
346  */
347 AjxImg.getImageSpanHtml =
348 function(imageName, imageStyleStr, attrStr, label, containerClassName) {
349     containerClassName = containerClassName || "inlineIcon";
350 	var html = [
351         "<span style='white-space:nowrap'>",
352         "<span class='",
353         containerClassName,
354         "'>",
355         AjxImg.getImageHtml(imageName, imageStyleStr, attrStr),
356         (label || ""),
357         "</span>",
358         "</span>"
359     ];
360 
361 	return html.join("");
362 };
363 
364 /**
365  * Helper method to draw the image using both the mask image and the overlay image
366  * 
367  * @param ctx  {Object} canvas context
368  * @param maskImg   {HtmlElement} mask image object
369  * @param overlayImg {HtmlElement} overlay image object
370  * @param mask  {Object} mask object
371  * @param overlay {Object} overlay object
372  * @param color {String} color for fill
373  * @param width {int} width
374  * @param height {int} height
375  * 
376  * @private
377  */
378 AjxImg._drawCanvasImage = 
379 function(ctx, maskImg, overlayImg, mask, overlay, color, width, height) {
380 	ctx.drawImage(maskImg, mask.l, mask.t);
381 	ctx.globalCompositeOperation = "source-out";
382 	ctx.fillStyle = color;
383 	ctx.fillRect(0, 0, width, height);
384 	ctx.restore();
385 	ctx.drawImage(overlayImg, overlay.l, overlay.t);
386 	ctx.restore();	
387 };
388