diff --git a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java index 281ffd9b8eb5a410b6b4eef2854fbfbf99c767b1..f7d07209d2e37fd0fdef12591e2aa5a11c6e5fd8 100644 --- a/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java +++ b/src/main/java/org/olat/ims/qti21/ui/components/AssessmentTestComponent.java @@ -24,6 +24,7 @@ import java.net.URI; import org.olat.core.gui.UserRequest; import org.olat.core.gui.components.AbstractComponent; import org.olat.core.gui.components.ComponentRenderer; +import org.olat.core.gui.control.JSAndCSSAdder; import org.olat.core.gui.render.ValidationResult; import org.olat.ims.qti21.ui.CandidateSessionContext; @@ -96,7 +97,10 @@ public class AssessmentTestComponent extends AbstractComponent { @Override public void validate(UserRequest ureq, ValidationResult vr) { super.validate(ureq, vr); - vr.getJsAndCSSAdder().addRequiredStaticJsFile("assessment/rendering/javascript/QtiWorksRendering.js"); + + JSAndCSSAdder jsa = vr.getJsAndCSSAdder(); + jsa.addRequiredStaticJsFile("assessment/rendering/javascript/QtiWorksRendering.js"); + jsa.addRequiredStaticJsFile("js/jquery/maphilight/jquery.maphilight.js"); } @Override diff --git a/src/main/java/org/olat/ims/qti21/ui/rendering/AssessmentRenderer.java b/src/main/java/org/olat/ims/qti21/ui/rendering/AssessmentRenderer.java index cba4348c9d113436f8d8249d3c5ecdf09573c680..971dad59d3bbd237e6c1c593a6d0670e5ec373df 100644 --- a/src/main/java/org/olat/ims/qti21/ui/rendering/AssessmentRenderer.java +++ b/src/main/java/org/olat/ims/qti21/ui/rendering/AssessmentRenderer.java @@ -54,6 +54,8 @@ import javax.xml.transform.stream.StreamResult; import org.olat.core.dispatcher.impl.StaticMediaDispatcher; import org.olat.core.gui.render.StringOutput; import org.olat.core.helpers.Settings; +import org.olat.core.util.StringHelper; +import org.olat.core.util.WebappHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.validation.BeanPropertyBindingResult; @@ -73,6 +75,7 @@ import uk.ac.ed.ph.jqtiplus.state.TestSessionState; import uk.ac.ed.ph.jqtiplus.state.marshalling.ItemSessionStateXmlMarshaller; import uk.ac.ed.ph.jqtiplus.state.marshalling.TestSessionStateXmlMarshaller; import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ClassPathResourceLocator; +import uk.ac.ed.ph.jqtiplus.xmlutils.locators.FileResourceLocator; import uk.ac.ed.ph.jqtiplus.xmlutils.locators.ResourceLocator; import uk.ac.ed.ph.jqtiplus.xmlutils.xslt.SimpleXsltStylesheetCache; import uk.ac.ed.ph.jqtiplus.xmlutils.xslt.XsltStylesheetManager; @@ -98,30 +101,47 @@ public class AssessmentRenderer { private static final Logger logger = LoggerFactory.getLogger(AssessmentRenderer.class); - private static final URI serializeXsltUri = URI.create("classpath:/rendering-xslt/serialize.xsl"); - private static final URI ctopXsltUri = URI.create("classpath:/rendering-xslt/ctop.xsl"); - private static final URI itemStandaloneXsltUri = URI.create("classpath:/rendering-xslt/item-standalone.xsl"); - private static final URI testItemXsltUri = URI.create("classpath:/rendering-xslt/test-item.xsl"); - private static final URI testEntryXsltUri = URI.create("classpath:/rendering-xslt/test-entry.xsl"); - private static final URI testPartNavigationXsltUri = URI.create("classpath:/rendering-xslt/test-testpart-navigation.xsl"); - private static final URI testPartFeedbackXsltUri = URI.create("classpath:/rendering-xslt/test-testpart-feedback.xsl"); - private static final URI testFeedbackXsltUri = URI.create("classpath:/rendering-xslt/test-feedback.xsl"); - private static final URI terminatedXsltUri = URI.create("classpath:/rendering-xslt/terminated.xsl"); - private static final URI explodedXsltUri = URI.create("classpath:/rendering-xslt/exploded.xsl"); - - + private static final URI serializeXsltUri; + private static final URI ctopXsltUri; + private static final URI itemStandaloneXsltUri; + private static final URI testItemXsltUri; + private static final URI testEntryXsltUri; + private static final URI testPartNavigationXsltUri; + private static final URI testPartFeedbackXsltUri; + private static final URI testFeedbackXsltUri; + private static final URI terminatedXsltUri; + private static final URI explodedXsltUri; + + static { + String path; + if(Settings.isDebuging() && StringHelper.containsNonWhitespace(WebappHelper.getSourcePath())) { + path = "file://" + WebappHelper.getSourcePath().replace("/src/main/java", "/src/main/resources"); + } else { + path = "classpath:"; + } + + serializeXsltUri = URI.create(path + "/rendering-xslt/serialize.xsl"); + ctopXsltUri = URI.create(path + "/rendering-xslt/ctop.xsl"); + itemStandaloneXsltUri = URI.create(path + "/rendering-xslt/item-standalone.xsl"); + testItemXsltUri = URI.create(path + "/rendering-xslt/test-item.xsl"); + testEntryXsltUri = URI.create(path + "/rendering-xslt/test-entry.xsl"); + testPartNavigationXsltUri = URI.create(path + "/rendering-xslt/test-testpart-navigation.xsl"); + testPartFeedbackXsltUri = URI.create(path + "/rendering-xslt/test-testpart-feedback.xsl"); + testFeedbackXsltUri = URI.create(path + "/rendering-xslt/test-feedback.xsl"); + terminatedXsltUri = URI.create(path + "/rendering-xslt/terminated.xsl"); + explodedXsltUri = URI.create(path + "/rendering-xslt/exploded.xsl"); + } /** Manager for the XSLT stylesheets, created during init. */ private XsltStylesheetManager stylesheetManager; - //---------------------------------------------------- - - - - //---------------------------------------------------- public AssessmentRenderer() { - this.stylesheetManager = new XsltStylesheetManager(new ClassPathResourceLocator(), new SimpleXsltStylesheetCache()); + if(Settings.isDebuging() && StringHelper.containsNonWhitespace(WebappHelper.getSourcePath())) { + stylesheetManager = new XsltStylesheetManager(new FileResourceLocator(), null); + } else { + stylesheetManager = new XsltStylesheetManager(new ClassPathResourceLocator(), new SimpleXsltStylesheetCache()); + } } //---------------------------------------------------- diff --git a/src/main/resources/rendering-xslt/interactions/hotspotInteraction.xsl b/src/main/resources/rendering-xslt/interactions/hotspotInteraction.xsl index 4eba570ed5f8b7cbe2f0fdf45a4bb9a52681b6a2..736461ba9dfd03d95bd05d5aad91fa526b9793ec 100644 --- a/src/main/resources/rendering-xslt/interactions/hotspotInteraction.xsl +++ b/src/main/resources/rendering-xslt/interactions/hotspotInteraction.xsl @@ -22,37 +22,82 @@ <xsl:variable name="object" select="qti:object" as="element(qti:object)"/> <xsl:variable name="appletContainerId" select="concat('qtiworks_id_appletContainer_', @responseIdentifier)" as="xs:string"/> + <xsl:variable name="responseValue" select="qw:get-response-value(/, @responseIdentifier)" as="element(qw:responseVariable)?"/> + <div id="{$appletContainerId}" class="appletContainer v2"> - <object type="application/x-java-applet" height="{$object/@height + 40}" width="{$object/@width}"> - <param name="code" value="BoundedGraphicalApplet"/> - <param name="codebase" value="{$appletCodebase}"/> - <param name="identifier" value="{@responseIdentifier}"/> - <param name="operation_mode" value="hotspot_interaction"/> - <!-- (BoundedGraphicalApplet uses -1 to represent 'unlimited') --> - <param name="number_of_responses" value="{if (@maxChoices > 0) then @maxChoices else -1}"/> - <param name="background_image" value="{qw:convert-link-full($object/@data)}"/> - <xsl:variable name="hotspotChoices" select="qw:filter-visible(qti:hotspotChoice)" as="element(qti:hotspotChoice)*"/> - <param name="hotspot_count" value="{count($hotspotChoices)}"/> - <xsl:for-each select="qti:hotspotChoice"> - <param name="hotspot{position()-1}" - value="{@identifier}::::{@shape}::{@coords}{if (@label) then concat('::hotSpotLabel',@label) else ''}{if (@matchGroup) then concat('::', translate(normalize-space(@matchGroup), ' ', '::')) else ''}"/> - </xsl:for-each> + <img id="{$appletContainerId}_img" width="206" height="280" src="{qw:convert-link-full($object/@data)}" usemap="#{$appletContainerId}_map"></img> + <map name="{$appletContainerId}_map"> + <xsl:for-each select="qti:hotspotChoice"> + <!-- Match group, label --> + <area id="{@identifier}" shape="{@shape}" coords="{@coords}" href="javascript:clickArea('{@identifier}')" data-maphilight=''></area> + </xsl:for-each> + </map> - <xsl:variable name="responseValue" select="qw:get-response-value(/, @responseIdentifier)" as="element(qw:responseVariable)?"/> - <xsl:if test="qw:is-not-null-value($responseValue)"> - <param name="feedback"> - <xsl:attribute name="value"> - <xsl:value-of select="$responseValue/qw:value" separator=","/> - </xsl:attribute> - </param> - </xsl:if> - </object> - <script type="text/javascript"> - jQuery(document).ready(function() { - QtiWorksRendering.registerAppletBasedInteractionContainer('<xsl:value-of - select="$appletContainerId"/>', ['<xsl:value-of select="@responseIdentifier"/>']); - }); - </script> + <script type="text/javascript"> + jQuery(function() { + jQuery('#<xsl:value-of select="$appletContainerId"/>_img').maphilight({ + fillColor: '888888', + strokeColor: '0000ff', + strokeWidth: 3 + }); + }); + + <xsl:choose> + <xsl:when test="qw:is-not-null-value($responseValue)"> + + jQuery(function() { + var areaIds = '<xsl:value-of select="$responseValue/qw:value" separator=","/>'.split(','); + for(i=areaIds.length; i-->0; ) { + var areaEl = jQuery('#' + areaIds[i]) + var data = areaEl.data('maphilight') || {}; + data.alwaysOn = true; + areaEl.data('maphilight', data).trigger('alwaysOn.maphilight'); + } + }); + + function clickArea(spot) { }; + + </xsl:when><xsl:otherwise> + + function clickArea(spot) { + var areaEl = jQuery('#' + spot) + var data = areaEl.data('maphilight') || {}; + if(!data.alwaysOn) { + var numOfChoices = 1; + if(numOfChoices > 0) { + var countChoices = 0; + jQuery("area", "map[name='<xsl:value-of select="$appletContainerId"/>_map']").each(function(index, el) { + var cData = jQuery(el).data('maphilight') || {}; + if(cData.alwaysOn) { + countChoices++; + + } + }); + if(countChoices >= numOfChoices) { + return false; + } + } + } + data.alwaysOn = !data.alwaysOn; + areaEl.data('maphilight', data).trigger('alwaysOn.maphilight'); + + var divContainer = jQuery('#<xsl:value-of select="$appletContainerId"/>'); + divContainer.find("input[type='hidden']").remove(); + jQuery("area", "map[name='<xsl:value-of select="$appletContainerId"/>_map']").each(function(index, el) { + var cAreaEl = jQuery(el); + var cData = cAreaEl.data('maphilight') || {}; + if(cData.alwaysOn) { + var inputElement = jQuery('<input type="hidden"/>') + .attr('name', 'qtiworks_response_<xsl:value-of select="@responseIdentifier"/>') + .attr('value', areaEl.attr('id')); + divContainer.append(inputElement); + } + }); + }; + + </xsl:otherwise> + </xsl:choose> + </script> </div> </div> </xsl:template> diff --git a/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.js b/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.js new file mode 100644 index 0000000000000000000000000000000000000000..c87dcbc42a3f3e11ef49063b2057e50a23b8dcff --- /dev/null +++ b/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.js @@ -0,0 +1,368 @@ +(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 = '<v:shape id="vml_flag1" adj="1" />'; + 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 = $('<canvas style="width:'+$(img).width()+'px;height:'+$(img).height()+'px;"></canvas>').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 $('<var style="zoom:1;overflow:hidden;display:block;width:'+img.width+'px;height:'+img.height+'px;"></var>').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 = '<v:fill color="#'+options.fillColor+'" opacity="'+(options.fill ? options.fillOpacity : 0)+'" />'; + stroke = (options.stroke ? 'strokeweight="'+options.strokeWidth+'" stroked="t" strokecolor="#'+options.strokeColor+'"' : 'stroked="f"'); + opacity = '<v:stroke opacity="'+options.strokeOpacity+'"/>'; + if(shape == 'rect') { + e = $('<v:rect name="'+name+'" filled="t" '+stroke+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+coords[0]+'px;top:'+coords[1]+'px;width:'+(coords[2] - coords[0])+'px;height:'+(coords[3] - coords[1])+'px;"></v:rect>'); + } else if(shape == 'poly') { + e = $('<v:shape name="'+name+'" filled="t" '+stroke+' coordorigin="0,0" coordsize="'+canvas.width+','+canvas.height+'" path="m '+coords[0]+','+coords[1]+' l '+coords.join(',')+' x e" style="zoom:1;margin:0;padding:0;display:block;position:absolute;top:0px;left:0px;width:'+canvas.width+'px;height:'+canvas.height+'px;"></v:shape>'); + } else if(shape == 'circ') { + e = $('<v:oval name="'+name+'" filled="t" '+stroke+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+(coords[0] - coords[2])+'px;top:'+(coords[1] - coords[2])+'px;width:'+(coords[2]*2)+'px;height:'+(coords[2]*2)+'px;"></v:oval>'); + } + e.get(0).innerHTML = fill+opacity; + $(canvas).append(e); + }; + clear_canvas = function(canvas) { + // jquery1.8 + ie7 + var $html = $("<div>" + canvas.innerHTML + "</div>"); + $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, mouseover, 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.size() > 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').find('area[coords]').unbind('.maphilight'); + } + + wrap = $('<div></div>').css({ + display:'block', + background:'url("'+this.src+'")', + 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; + + mouseover = function(e) { + var shape, area_options; + area_options = options_from_area(this, options); + if( + !area_options.neverOn + && + !area_options.alwaysOn + ) { + shape = shape_from_area(this); + 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+'="'+$(this).attr(area_options.groupBy)+'"]'); + } else { + areas = map.find(area_options.groupBy); + } + var first = this; + 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('<v:rect></v:rect>'); + } + } + } + + $(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, ""); + } + } + }); + }); + + $(map).trigger('alwaysOn.maphilight').find('area[coords]') + .bind('mouseover.maphilight', mouseover) + .bind('mouseout.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 + }; +})(jQuery); diff --git a/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.min.js b/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.min.js new file mode 100644 index 0000000000000000000000000000000000000000..ec9d0214fd494c951f9480ac159c07c5eb23da3e --- /dev/null +++ b/src/main/webapp/static/js/jquery/maphilight/jquery.maphilight.min.js @@ -0,0 +1 @@ +(function(G){var B,J,C,K,N,M,I,E,H,A,L;J=!!document.createElement("canvas").getContext;B=(function(){var P=document.createElement("div");P.innerHTML='<v:shape id="vml_flag1" adj="1" />';var O=P.firstChild;O.style.behavior="url(#default#VML)";return O?typeof O.adj=="object":true})();if(!(J||B)){G.fn.maphilight=function(){return this};return }if(J){E=function(O){return Math.max(0,Math.min(parseInt(O,16),255))};H=function(O,P){return"rgba("+E(O.substr(0,2))+","+E(O.substr(2,2))+","+E(O.substr(4,2))+","+P+")"};C=function(O){var P=G('<canvas style="width:'+O.width+"px;height:"+O.height+'px;"></canvas>').get(0);P.getContext("2d").clearRect(0,0,P.width,P.height);return P};var F=function(Q,O,R,P,S){P=P||0;S=S||0;Q.beginPath();if(O=="rect"){Q.rect(R[0]+P,R[1]+S,R[2]-R[0],R[3]-R[1])}else{if(O=="poly"){Q.moveTo(R[0]+P,R[1]+S);for(i=2;i<R.length;i+=2){Q.lineTo(R[i]+P,R[i+1]+S)}}else{if(O=="circ"){Q.arc(R[0]+P,R[1]+S,R[2],0,Math.PI*2,false)}}}Q.closePath()};K=function(Q,T,U,X,O){var S,P=Q.getContext("2d");if(X.shadow){P.save();if(X.shadowPosition=="inside"){F(P,T,U);P.clip()}var R=Q.width*100;var W=Q.height*100;F(P,T,U,R,W);P.shadowOffsetX=X.shadowX-R;P.shadowOffsetY=X.shadowY-W;P.shadowBlur=X.shadowRadius;P.shadowColor=H(X.shadowColor,X.shadowOpacity);var V=X.shadowFrom;if(!V){if(X.shadowPosition=="outside"){V="fill"}else{V="stroke"}}if(V=="stroke"){P.strokeStyle="rgba(0,0,0,1)";P.stroke()}else{if(V=="fill"){P.fillStyle="rgba(0,0,0,1)";P.fill()}}P.restore();if(X.shadowPosition=="outside"){P.save();F(P,T,U);P.globalCompositeOperation="destination-out";P.fillStyle="rgba(0,0,0,1);";P.fill();P.restore()}}P.save();F(P,T,U);if(X.fill){P.fillStyle=H(X.fillColor,X.fillOpacity);P.fill()}if(X.stroke){P.strokeStyle=H(X.strokeColor,X.strokeOpacity);P.lineWidth=X.strokeWidth;P.stroke()}P.restore();if(X.fade){G(Q).css("opacity",0).animate({opacity:1},100)}};N=function(O){O.getContext("2d").clearRect(0,0,O.width,O.height)}}else{C=function(O){return G('<var style="zoom:1;overflow:hidden;display:block;width:'+O.width+"px;height:"+O.height+'px;"></var>').get(0)};K=function(P,T,U,X,O){var V,W,R,S;for(var Q in U){U[Q]=parseInt(U[Q],10)}V='<v:fill color="#'+X.fillColor+'" opacity="'+(X.fill?X.fillOpacity:0)+'" />';W=(X.stroke?'strokeweight="'+X.strokeWidth+'" stroked="t" strokecolor="#'+X.strokeColor+'"':'stroked="f"');R='<v:stroke opacity="'+X.strokeOpacity+'"/>';if(T=="rect"){S=G('<v:rect name="'+O+'" filled="t" '+W+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+U[0]+"px;top:"+U[1]+"px;width:"+(U[2]-U[0])+"px;height:"+(U[3]-U[1])+'px;"></v:rect>')}else{if(T=="poly"){S=G('<v:shape name="'+O+'" filled="t" '+W+' coordorigin="0,0" coordsize="'+P.width+","+P.height+'" path="m '+U[0]+","+U[1]+" l "+U.join(",")+' x e" style="zoom:1;margin:0;padding:0;display:block;position:absolute;top:0px;left:0px;width:'+P.width+"px;height:"+P.height+'px;"></v:shape>')}else{if(T=="circ"){S=G('<v:oval name="'+O+'" filled="t" '+W+' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:'+(U[0]-U[2])+"px;top:"+(U[1]-U[2])+"px;width:"+(U[2]*2)+"px;height:"+(U[2]*2)+'px;"></v:oval>')}}}S.get(0).innerHTML=V+R;G(P).append(S)};N=function(P){var O=G("<div>"+P.innerHTML+"</div>");O.children("[name=highlighted]").remove();P.innerHTML=O.html()}}M=function(P){var O,Q=P.getAttribute("coords").split(",");for(O=0;O<Q.length;O++){Q[O]=parseFloat(Q[O])}return[P.getAttribute("shape").toLowerCase().substr(0,4),Q]};L=function(Q,P){var O=G(Q);return G.extend({},P,G.metadata?O.metadata():false,O.data("maphilight"))};A=function(O){if(!O.complete){return false}if(typeof O.naturalWidth!="undefined"&&O.naturalWidth===0){return false}return true};I={position:"absolute",left:0,top:0,padding:0,border:0};var D=false;G.fn.maphilight=function(O){O=G.extend({},G.fn.maphilight.defaults,O);if(!J&&!D){G(window).ready(function(){document.namespaces.add("v","urn:schemas-microsoft-com:vml");var Q=document.createStyleSheet();var P=["shape","rect","oval","circ","fill","stroke","imagedata","group","textbox"];G.each(P,function(){Q.addRule("v\\:"+this,"behavior: url(#default#VML); antialias:true")})});D=true}return this.each(function(){var U,R,Y,Q,T,V,X,S,W;U=G(this);if(!A(this)){return window.setTimeout(function(){U.maphilight(O)},200)}Y=G.extend({},O,G.metadata?U.metadata():false,U.data("maphilight"));W=U.get(0).getAttribute("usemap");if(!W){return }Q=G('map[name="'+W.substr(1)+'"]');if(!(U.is('img,input[type="image"]')&&W&&Q.size()>0)){return }if(U.hasClass("maphilighted")){var P=U.parent();U.insertBefore(P);P.remove();G(Q).unbind(".maphilight").find("area[coords]").unbind(".maphilight")}R=G("<div></div>").css({display:"block",background:'url("'+this.src+'")',position:"relative",padding:0,width:this.width,height:this.height});if(Y.wrapClass){if(Y.wrapClass===true){R.addClass(G(this).attr("class"))}else{R.addClass(Y.wrapClass)}}U.before(R).css("opacity",0).css(I).remove();if(B){U.css("filter","Alpha(opacity=0)")}R.append(U);T=C(this);G(T).css(I);T.height=this.height;T.width=this.width;X=function(c){var a,b;b=L(this,Y);if(!b.neverOn&&!b.alwaysOn){a=M(this);K(T,a[0],a[1],b,"highlighted");if(b.groupBy){var Z;if(/^[a-zA-Z][\-a-zA-Z]+$/.test(b.groupBy)){Z=Q.find("area["+b.groupBy+'="'+G(this).attr(b.groupBy)+'"]')}else{Z=Q.find(b.groupBy)}var d=this;Z.each(function(){if(this!=d){var f=L(this,Y);if(!f.neverOn&&!f.alwaysOn){var e=M(this);K(T,e[0],e[1],f,"highlighted")}}})}if(!J){G(T).append("<v:rect></v:rect>")}}};G(Q).bind("alwaysOn.maphilight",function(){if(V){N(V)}if(!J){G(T).empty()}G(Q).find("area[coords]").each(function(){var Z,a;a=L(this,Y);if(a.alwaysOn){if(!V&&J){V=C(U[0]);G(V).css(I);V.width=U[0].width;V.height=U[0].height;U.before(V)}a.fade=a.alwaysOnFade;Z=M(this);if(J){K(V,Z[0],Z[1],a,"")}else{K(T,Z[0],Z[1],a,"")}}})});G(Q).trigger("alwaysOn.maphilight").find("area[coords]").bind("mouseover.maphilight",X).bind("mouseout.maphilight",function(Z){N(T)});U.before(T);U.addClass("maphilighted")})};G.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,shadow:false,shadowX:0,shadowY:0,shadowRadius:6,shadowColor:"000000",shadowOpacity:0.8,shadowPosition:"outside",shadowFrom:false}})(jQuery); \ No newline at end of file