(function(root, factory) { if (typeof define === "function" && define.amd) { define(["jquery"], factory); } else { factory(root.jQuery); } })(this, function($) { var has_VML, has_canvas, create_canvas_for, add_shape_to, clear_canvas, shape_from_area, canvas_style, hex_to_decimal, css3color, is_image_loaded, options_from_area; has_canvas = !!document.createElement("canvas").getContext; // VML: more complex has_VML = (function() { var a = document.createElement("div"); a.innerHTML = ''; var b = a.firstChild; b.style.behavior = "url(#default#VML)"; return b ? typeof b.adj == "object" : true; })(); if (!(has_canvas || has_VML)) { $.fn.maphilight = function() { return this; }; return; } if (has_canvas) { hex_to_decimal = function(hex) { return Math.max(0, Math.min(parseInt(hex, 16), 255)); }; css3color = function(color, opacity) { return ( "rgba(" + hex_to_decimal(color.substr(0, 2)) + "," + hex_to_decimal(color.substr(2, 2)) + "," + hex_to_decimal(color.substr(4, 2)) + "," + opacity + ")" ); }; create_canvas_for = function(img) { var c = $( '' ).get(0); c.getContext("2d").clearRect(0, 0, $(img).width(), $(img).height()); return c; }; var draw_shape = function(context, shape, coords, x_shift, y_shift) { x_shift = x_shift || 0; y_shift = y_shift || 0; context.beginPath(); if (shape == "rect") { // x, y, width, height context.rect( coords[0] + x_shift, coords[1] + y_shift, coords[2] - coords[0], coords[3] - coords[1] ); } else if (shape == "poly") { context.moveTo(coords[0] + x_shift, coords[1] + y_shift); for (i = 2; i < coords.length; i += 2) { context.lineTo(coords[i] + x_shift, coords[i + 1] + y_shift); } } else if (shape == "circ") { // x, y, radius, startAngle, endAngle, anticlockwise context.arc( coords[0] + x_shift, coords[1] + y_shift, coords[2], 0, Math.PI * 2, false ); } context.closePath(); }; add_shape_to = function(canvas, shape, coords, options, name) { var i, context = canvas.getContext("2d"); // Because I don't want to worry about setting things back to a base state // Shadow has to happen first, since it's on the bottom, and it does some clip / // fill operations which would interfere with what comes next. if (options.shadow) { context.save(); if (options.shadowPosition == "inside") { // Cause the following stroke to only apply to the inside of the path draw_shape(context, shape, coords); context.clip(); } // Redraw the shape shifted off the canvas massively so we can cast a shadow // onto the canvas without having to worry about the stroke or fill (which // cannot have 0 opacity or width, since they're what cast the shadow). var x_shift = canvas.width * 100; var y_shift = canvas.height * 100; draw_shape(context, shape, coords, x_shift, y_shift); context.shadowOffsetX = options.shadowX - x_shift; context.shadowOffsetY = options.shadowY - y_shift; context.shadowBlur = options.shadowRadius; context.shadowColor = css3color( options.shadowColor, options.shadowOpacity ); // Now, work out where to cast the shadow from! It looks better if it's cast // from a fill when it's an outside shadow or a stroke when it's an interior // shadow. Allow the user to override this if they need to. var shadowFrom = options.shadowFrom; if (!shadowFrom) { if (options.shadowPosition == "outside") { shadowFrom = "fill"; } else { shadowFrom = "stroke"; } } if (shadowFrom == "stroke") { context.strokeStyle = "rgba(0,0,0,1)"; context.stroke(); } else if (shadowFrom == "fill") { context.fillStyle = "rgba(0,0,0,1)"; context.fill(); } context.restore(); // and now we clean up if (options.shadowPosition == "outside") { context.save(); // Clear out the center draw_shape(context, shape, coords); context.globalCompositeOperation = "destination-out"; context.fillStyle = "rgba(0,0,0,1);"; context.fill(); context.restore(); } } context.save(); draw_shape(context, shape, coords); // fill has to come after shadow, otherwise the shadow will be drawn over the fill, // which mostly looks weird when the shadow has a high opacity if (options.fill) { context.fillStyle = css3color(options.fillColor, options.fillOpacity); context.fill(); } // Likewise, stroke has to come at the very end, or it'll wind up under bits of the // shadow or the shadow-background if it's present. if (options.stroke) { context.strokeStyle = css3color( options.strokeColor, options.strokeOpacity ); context.lineWidth = options.strokeWidth; context.stroke(); } context.restore(); if (options.fade) { $(canvas) .css("opacity", 0) .animate({ opacity: 1 }, 100); } }; clear_canvas = function(canvas) { canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); }; } else { // ie executes this code create_canvas_for = function(img) { return $( '' ).get(0); }; add_shape_to = function(canvas, shape, coords, options, name) { var fill, stroke, opacity, e; for (var i in coords) { coords[i] = parseInt(coords[i], 10); } fill = ''; stroke = options.stroke ? 'strokeweight="' + options.strokeWidth + '" stroked="t" strokecolor="#' + options.strokeColor + '"' : 'stroked="f"'; opacity = ''; if (shape == "rect") { e = $( '' ); } else if (shape == "poly") { e = $( '' ); } else if (shape == "circ") { e = $( '' ); } e.get(0).innerHTML = fill + opacity; $(canvas).append(e); }; clear_canvas = function(canvas) { // jquery1.8 + ie7 var $html = $("
" + canvas.innerHTML + "
"); $html.children("[name=highlighted]").remove(); canvas.innerHTML = $html.html(); }; } shape_from_area = function(area) { var i, coords = area.getAttribute("coords").split(","); for (i = 0; i < coords.length; i++) { coords[i] = parseFloat(coords[i]); } return [ area .getAttribute("shape") .toLowerCase() .substr(0, 4), coords ]; }; options_from_area = function(area, options) { var $area = $(area); return $.extend( {}, options, $.metadata ? $area.metadata() : false, $area.data("maphilight") ); }; is_image_loaded = function(img) { if (!img.complete) { return false; } // IE if (typeof img.naturalWidth != "undefined" && img.naturalWidth === 0) { return false; } // Others return true; }; canvas_style = { position: "absolute", left: 0, top: 0, padding: 0, border: 0 }; var ie_hax_done = false; $.fn.maphilight = function(opts) { opts = $.extend({}, $.fn.maphilight.defaults, opts); if (!has_canvas && !ie_hax_done) { $(window).ready(function() { document.namespaces.add("v", "urn:schemas-microsoft-com:vml"); var style = document.createStyleSheet(); var shapes = [ "shape", "rect", "oval", "circ", "fill", "stroke", "imagedata", "group", "textbox" ]; $.each(shapes, function() { style.addRule( "v\\:" + this, "behavior: url(#default#VML); antialias:true" ); }); }); ie_hax_done = true; } return this.each(function() { var img, wrap, options, map, canvas, canvas_always, highlighted_shape, usemap; img = $(this); if (!is_image_loaded(this)) { // If the image isn't fully loaded, this won't work right. Try again later. return window.setTimeout(function() { img.maphilight(opts); }, 200); } options = $.extend( {}, opts, $.metadata ? img.metadata() : false, img.data("maphilight") ); // jQuery bug with Opera, results in full-url#usemap being returned from jQuery's attr. // So use raw getAttribute instead. usemap = img.get(0).getAttribute("usemap"); if (!usemap) { return; } map = $('map[name="' + usemap.substr(1) + '"]'); if (!(img.is('img,input[type="image"]') && usemap && map.length > 0)) { return; } if (img.hasClass("maphilighted")) { // We're redrawing an old map, probably to pick up changes to the options. // Just clear out all the old stuff. var wrapper = img.parent(); img.insertBefore(wrapper); wrapper.remove(); $(map).unbind(".maphilight"); } wrap = $("
").css({ display: "block", backgroundImage: 'url("' + this.src + '")', backgroundSize: "contain", position: "relative", padding: 0, width: this.width, height: this.height }); if (options.wrapClass) { if (options.wrapClass === true) { wrap.addClass($(this).attr("class")); } else { wrap.addClass(options.wrapClass); } } // Firefox has a bug that prevents tabbing into the image map if // we set opacity of the image to 0, but very nearly 0 works! img .before(wrap) .css("opacity", 0.0000000001) .css(canvas_style) .remove(); if (has_VML) { img.css("filter", "Alpha(opacity=0)"); } wrap.append(img); canvas = create_canvas_for(this); $(canvas).css(canvas_style); canvas.height = this.height; canvas.width = this.width; $(map) .bind("alwaysOn.maphilight", function() { // Check for areas with alwaysOn set. These are added to a *second* canvas, // which will get around flickering during fading. if (canvas_always) { clear_canvas(canvas_always); } if (!has_canvas) { $(canvas).empty(); } $(map) .find("area[coords]") .each(function() { var shape, area_options; area_options = options_from_area(this, options); if (area_options.alwaysOn) { if (!canvas_always && has_canvas) { canvas_always = create_canvas_for(img[0]); $(canvas_always).css(canvas_style); canvas_always.width = img[0].width; canvas_always.height = img[0].height; img.before(canvas_always); } area_options.fade = area_options.alwaysOnFade; // alwaysOn shouldn't fade in initially shape = shape_from_area(this); if (has_canvas) { add_shape_to( canvas_always, shape[0], shape[1], area_options, "" ); } else { add_shape_to(canvas, shape[0], shape[1], area_options, ""); } } }); }) .trigger("alwaysOn.maphilight") .bind("mouseover.maphilight focusin.maphilight", function(e) { var shape, area_options, area = e.target; area_options = options_from_area(area, options); if (!area_options.neverOn && !area_options.alwaysOn) { shape = shape_from_area(area); add_shape_to( canvas, shape[0], shape[1], area_options, "highlighted" ); if (area_options.groupBy) { var areas; // two ways groupBy might work; attribute and selector if (/^[a-zA-Z][\-a-zA-Z]+$/.test(area_options.groupBy)) { areas = map.find( "area[" + area_options.groupBy + '="' + $(area).attr(area_options.groupBy) + '"]' ); } else { areas = map.find(area_options.groupBy); } var first = area; areas.each(function() { if (this != first) { var subarea_options = options_from_area(this, options); if (!subarea_options.neverOn && !subarea_options.alwaysOn) { var shape = shape_from_area(this); add_shape_to( canvas, shape[0], shape[1], subarea_options, "highlighted" ); } } }); } // workaround for IE7, IE8 not rendering the final rectangle in a group if (!has_canvas) { $(canvas).append(""); } } }) .bind("mouseout.maphilight focusout.maphilight", function(e) { clear_canvas(canvas); }); img.before(canvas); // if we put this after, the mouseover events wouldn't fire. img.addClass("maphilighted"); }); }; $.fn.maphilight.defaults = { fill: true, fillColor: "000000", fillOpacity: 0.2, stroke: true, strokeColor: "ff0000", strokeOpacity: 1, strokeWidth: 1, fade: true, alwaysOn: false, neverOn: false, groupBy: false, wrapClass: true, // plenty of shadow: shadow: false, shadowX: 0, shadowY: 0, shadowRadius: 6, shadowColor: "000000", shadowOpacity: 0.8, shadowPosition: "outside", shadowFrom: false }; });