(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); } } img.before(wrap).css('opacity', 0).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, focus.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, blur.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 }; });