(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
};
});