1/*! 2* imagemapster - v1.5.4 - 2021-02-20 3* https://github.com/jamietre/ImageMapster/ 4* Copyright (c) 2011 - 2021 James Treworgy 5* License: MIT 6*/ 7(function (factory) { 8 if (typeof define === 'function' && define.amd) { 9 // AMD. Register as an anonymous module. 10 define(['jquery'], factory); 11 } else if (typeof module === 'object' && module.exports) { 12 // Node/CommonJS 13 module.exports = function( root, jQuery ) { 14 if ( jQuery === undefined ) { 15 // require('jQuery') returns a factory that requires window to 16 // build a jQuery instance, we normalize how we use modules 17 // that require this pattern but the window provided is a noop 18 // if it's defined (how jquery works) 19 if ( typeof window !== 'undefined' ) { 20 jQuery = require('jquery'); 21 } 22 else { 23 jQuery = require('jquery')(root); 24 } 25 } 26 factory(jQuery); 27 return jQuery; 28 }; 29 } else { 30 // Browser globals 31 factory(jQuery); 32 } 33}(function (jQuery) { 34 /* 35 jqueryextensions.js 36 Extend/intercept jquery behavior 37*/ 38 39(function ($) { 40 'use strict'; 41 42 function setupPassiveListeners() { 43 // Test via a getter in the options object to see if the passive property is accessed 44 var supportsPassive = false; 45 try { 46 var opts = Object.defineProperty({}, 'passive', { 47 get: function () { 48 supportsPassive = true; 49 return true; 50 } 51 }); 52 window.addEventListener('testPassive.mapster', function () {}, opts); 53 window.removeEventListener('testPassive.mapster', function () {}, opts); 54 } catch (e) { 55 // intentionally ignored 56 } 57 58 if (supportsPassive) { 59 // In order to not interrupt scrolling on touch devices 60 // we commit to not calling preventDefault from within listeners 61 // There is a plan to handle this natively in jQuery 4.0 but for 62 // now we are on our own. 63 // TODO: Migrate to jQuery 4.0 approach if/when released 64 // https://www.chromestatus.com/feature/5745543795965952 65 // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md 66 // https://github.com/jquery/jquery/issues/2871#issuecomment-175175180 67 // https://jsbin.com/bupesajoza/edit?html,js,output 68 var setupListener = function (ns, type, listener) { 69 if (ns.includes('noPreventDefault')) { 70 window.addEventListener(type, listener, { passive: true }); 71 } else { 72 console.warn('non-passive events - listener not added'); 73 return false; 74 } 75 }; 76 77 // special events for noPreventDefault 78 $.event.special.touchstart = { 79 setup: function (_, ns, listener) { 80 return setupListener(ns, 'touchstart', listener); 81 } 82 }; 83 $.event.special.touchend = { 84 setup: function (_, ns, listener) { 85 return setupListener(ns, 'touchend', listener); 86 } 87 }; 88 } 89 } 90 91 function supportsSpecialEvents() { 92 return $.event && $.event.special; 93 } 94 95 // Zepto does not support special events 96 // TODO: Remove when Zepto support is removed 97 if (supportsSpecialEvents()) { 98 setupPassiveListeners(); 99 } 100})(jQuery); 101 102/* 103 core.js 104 ImageMapster core 105*/ 106 107(function ($) { 108 'use strict'; 109 110 var mapster_version = '1.5.4'; 111 112 // all public functions in $.mapster.impl are methods 113 $.fn.mapster = function (method) { 114 var m = $.mapster.impl; 115 if ($.mapster.utils.isFunction(m[method])) { 116 return m[method].apply(this, Array.prototype.slice.call(arguments, 1)); 117 } else if (typeof method === 'object' || !method) { 118 return m.bind.apply(this, arguments); 119 } else { 120 $.error('Method ' + method + ' does not exist on jQuery.mapster'); 121 } 122 }; 123 124 $.mapster = { 125 version: mapster_version, 126 render_defaults: { 127 isSelectable: true, 128 isDeselectable: true, 129 fade: false, 130 fadeDuration: 150, 131 fill: true, 132 fillColor: '000000', 133 fillColorMask: 'FFFFFF', 134 fillOpacity: 0.7, 135 highlight: true, 136 stroke: false, 137 strokeColor: 'ff0000', 138 strokeOpacity: 1, 139 strokeWidth: 1, 140 includeKeys: '', 141 altImage: null, 142 altImageId: null, // used internally 143 altImages: {} 144 }, 145 defaults: { 146 clickNavigate: false, 147 navigateMode: 'location', // location|open 148 wrapClass: null, 149 wrapCss: null, 150 onGetList: null, 151 sortList: false, 152 listenToList: false, 153 mapKey: '', 154 mapValue: '', 155 singleSelect: false, 156 listKey: 'value', 157 listSelectedAttribute: 'selected', 158 listSelectedClass: null, 159 onClick: null, 160 onMouseover: null, 161 onMouseout: null, 162 mouseoutDelay: 0, 163 onStateChange: null, 164 boundList: null, 165 onConfigured: null, 166 configTimeout: 30000, 167 noHrefIsMask: true, 168 scaleMap: true, 169 enableAutoResizeSupport: false, // TODO: Remove in next major release 170 autoResize: false, 171 autoResizeDelay: 0, 172 autoResizeDuration: 0, 173 onAutoResize: null, 174 safeLoad: false, 175 areas: [] 176 }, 177 shared_defaults: { 178 render_highlight: { fade: true }, 179 render_select: { fade: false }, 180 staticState: null, 181 selected: null 182 }, 183 area_defaults: { 184 includeKeys: '', 185 isMask: false 186 }, 187 canvas_style: { 188 position: 'absolute', 189 left: 0, 190 top: 0, 191 padding: 0, 192 border: 0 193 }, 194 hasCanvas: null, 195 map_cache: [], 196 hooks: {}, 197 addHook: function (name, callback) { 198 this.hooks[name] = (this.hooks[name] || []).push(callback); 199 }, 200 callHooks: function (name, context) { 201 $.each(this.hooks[name] || [], function (_, e) { 202 e.apply(context); 203 }); 204 }, 205 utils: { 206 when: { 207 all: function (deferredArray) { 208 // TODO: Promise breaks ES5 support 209 // eslint-disable-next-line no-undef 210 return Promise.all(deferredArray); 211 }, 212 defer: function () { 213 // Deferred is frequently referred to as an anti-pattern largely 214 // due to error handling, however to avoid reworking existing 215 // APIs and support backwards compat, creating a "deferred" 216 // polyfill via native promise 217 var Deferred = function () { 218 // TODO: Promise breaks ES5 support 219 // eslint-disable-next-line no-undef 220 this.promise = new Promise( 221 function (resolve, reject) { 222 this.resolve = resolve; 223 this.reject = reject; 224 }.bind(this) 225 ); 226 227 this.then = this.promise.then.bind(this.promise); 228 this.catch = this.promise.catch.bind(this.promise); 229 }; 230 return new Deferred(); 231 } 232 }, 233 defer: function () { 234 return this.when.defer(); 235 }, 236 // extends the constructor, returns a new object prototype. Does not refer to the 237 // original constructor so is protected if the original object is altered. This way you 238 // can "extend" an object by replacing it with its subclass. 239 subclass: function (BaseClass, constr) { 240 var Subclass = function () { 241 var me = this, 242 args = Array.prototype.slice.call(arguments, 0); 243 me.base = BaseClass.prototype; 244 me.base.init = function () { 245 BaseClass.prototype.constructor.apply(me, args); 246 }; 247 constr.apply(me, args); 248 }; 249 Subclass.prototype = new BaseClass(); 250 Subclass.prototype.constructor = Subclass; 251 return Subclass; 252 }, 253 asArray: function (obj) { 254 return obj.constructor === Array ? obj : this.split(obj); 255 }, 256 // clean split: no padding or empty elements 257 split: function (text, cb) { 258 var i, 259 el, 260 arr = text.split(','); 261 for (i = 0; i < arr.length; i++) { 262 // backwards compat for $.trim which would return empty string on null 263 // which theoertically should not happen here 264 el = arr[i] ? arr[i].trim() : ''; 265 if (el === '') { 266 arr.splice(i, 1); 267 } else { 268 arr[i] = cb ? cb(el) : el; 269 } 270 } 271 return arr; 272 }, 273 // similar to $.extend but does not add properties (only updates), unless the 274 // first argument is an empty object, then all properties will be copied 275 updateProps: function (_target, _template) { 276 var onlyProps, 277 target = _target || {}, 278 template = $.isEmptyObject(target) ? _template : _target; 279 280 //if (template) { 281 onlyProps = []; 282 $.each(template, function (prop) { 283 onlyProps.push(prop); 284 }); 285 //} 286 287 $.each(Array.prototype.slice.call(arguments, 1), function (_, src) { 288 $.each(src || {}, function (prop) { 289 if (!onlyProps || $.inArray(prop, onlyProps) >= 0) { 290 var p = src[prop]; 291 292 if ($.isPlainObject(p)) { 293 // not recursive - only copies 1 level of subobjects, and always merges 294 target[prop] = $.extend(target[prop] || {}, p); 295 } else if (p && p.constructor === Array) { 296 target[prop] = p.slice(0); 297 } else if (typeof p !== 'undefined') { 298 target[prop] = src[prop]; 299 } 300 } 301 }); 302 }); 303 return target; 304 }, 305 isElement: function (o) { 306 return typeof HTMLElement === 'object' 307 ? o instanceof HTMLElement 308 : o && 309 typeof o === 'object' && 310 o.nodeType === 1 && 311 typeof o.nodeName === 'string'; 312 }, 313 /** 314 * Basic indexOf implementation for IE7-8. Though we use $.inArray, some jQuery versions will try to 315 * use a prototpye on the calling object, defeating the purpose of using $.inArray in the first place. 316 * 317 * This will be replaced with the array prototype if it's available. 318 * 319 * @param {Array} arr The array to search 320 * @param {Object} target The item to search for 321 * @return {Number} The index of the item, or -1 if not found 322 */ 323 indexOf: function (arr, target) { 324 if (Array.prototype.indexOf) { 325 return Array.prototype.indexOf.call(arr, target); 326 } else { 327 for (var i = 0; i < arr.length; i++) { 328 if (arr[i] === target) { 329 return i; 330 } 331 } 332 return -1; 333 } 334 }, 335 336 // finds element of array or object with a property "prop" having value "val" 337 // if prop is not defined, then just looks for property with value "val" 338 indexOfProp: function (obj, prop, val) { 339 var result = obj.constructor === Array ? -1 : null; 340 $.each(obj, function (i, e) { 341 if (e && (prop ? e[prop] : e) === val) { 342 result = i; 343 return false; 344 } 345 }); 346 return result; 347 }, 348 // returns "obj" if true or false, or "def" if not true/false 349 boolOrDefault: function (obj, def) { 350 return this.isBool(obj) ? obj : def || false; 351 }, 352 isBool: function (obj) { 353 return typeof obj === 'boolean'; 354 }, 355 isUndef: function (obj) { 356 return typeof obj === 'undefined'; 357 }, 358 isFunction: function (obj) { 359 return typeof obj === 'function'; 360 }, 361 // evaluates "obj", if function, calls it with args 362 // (todo - update this to handle variable lenght/more than one arg) 363 ifFunction: function (obj, that, args) { 364 if (this.isFunction(obj)) { 365 obj.call(that, args); 366 } 367 }, 368 size: function (image, raw) { 369 var u = $.mapster.utils; 370 return { 371 width: raw 372 ? image.width || image.naturalWidth 373 : u.imgWidth(image, true), 374 height: raw 375 ? image.height || image.naturalHeight 376 : u.imgHeight(image, true), 377 complete: function () { 378 return !!this.height && !!this.width; 379 } 380 }; 381 }, 382 383 /** 384 * Set the opacity of the element. This is an IE<8 specific function for handling VML. 385 * When using VML we must override the "setOpacity" utility function (monkey patch ourselves). 386 * jQuery does not deal with opacity correctly for VML elements. This deals with that. 387 * 388 * @param {Element} el The DOM element 389 * @param {double} opacity A value between 0 and 1 inclusive. 390 */ 391 392 setOpacity: function (el, opacity) { 393 if ($.mapster.hasCanvas()) { 394 el.style.opacity = opacity; 395 } else { 396 $(el).each(function (_, e) { 397 if (typeof e.opacity !== 'undefined') { 398 e.opacity = opacity; 399 } else { 400 $(e).css('opacity', opacity); 401 } 402 }); 403 } 404 }, 405 406 // fade "el" from opacity "op" to "endOp" over a period of time "duration" 407 408 fader: (function () { 409 var elements = {}, 410 lastKey = 0, 411 fade_func = function (el, op, endOp, duration) { 412 var index, 413 cbIntervals = duration / 15, 414 obj, 415 u = $.mapster.utils; 416 417 if (typeof el === 'number') { 418 obj = elements[el]; 419 if (!obj) { 420 return; 421 } 422 } else { 423 index = u.indexOfProp(elements, null, el); 424 if (index) { 425 delete elements[index]; 426 } 427 elements[++lastKey] = obj = el; 428 el = lastKey; 429 } 430 431 endOp = endOp || 1; 432 433 op = 434 op + endOp / cbIntervals > endOp - 0.01 435 ? endOp 436 : op + endOp / cbIntervals; 437 438 u.setOpacity(obj, op); 439 if (op < endOp) { 440 setTimeout(function () { 441 fade_func(el, op, endOp, duration); 442 }, 15); 443 } 444 }; 445 return fade_func; 446 })(), 447 getShape: function (areaEl) { 448 // per HTML spec, invalid value and missing value default is 'rect' 449 // Handling as follows: 450 // - Missing/Empty value will be treated as 'rect' per spec 451 // - Avoid handling invalid values do to perf impact 452 // Note - IM currently does not support shape of 'default' so while its technically 453 // a valid attribute value it should not be used. 454 // https://html.spec.whatwg.org/multipage/image-maps.html#the-area-element 455 return (areaEl.shape || 'rect').toLowerCase(); 456 }, 457 hasAttribute: function (el, attrName) { 458 var attr = $(el).attr(attrName); 459 // For some browsers, `attr` is undefined; for others, `attr` is false. 460 return typeof attr !== 'undefined' && attr !== false; 461 } 462 }, 463 getBoundList: function (opts, key_list) { 464 if (!opts.boundList) { 465 return null; 466 } 467 var index, 468 key, 469 result = $(), 470 list = $.mapster.utils.split(key_list); 471 opts.boundList.each(function (_, e) { 472 for (index = 0; index < list.length; index++) { 473 key = list[index]; 474 if ($(e).is('[' + opts.listKey + '="' + key + '"]')) { 475 result = result.add(e); 476 } 477 } 478 }); 479 return result; 480 }, 481 getMapDataIndex: function (obj) { 482 var img, id; 483 switch (obj.tagName && obj.tagName.toLowerCase()) { 484 case 'area': 485 id = $(obj).parent().attr('name'); 486 img = $("img[usemap='#" + id + "']")[0]; 487 break; 488 case 'img': 489 img = obj; 490 break; 491 } 492 return img ? this.utils.indexOfProp(this.map_cache, 'image', img) : -1; 493 }, 494 getMapData: function (obj) { 495 var index = this.getMapDataIndex(obj.length ? obj[0] : obj); 496 if (index >= 0) { 497 return index >= 0 ? this.map_cache[index] : null; 498 } 499 }, 500 /** 501 * Queue a command to be run after the active async operation has finished 502 * @param {MapData} map_data The target MapData object 503 * @param {jQuery} that jQuery object on which the command was invoked 504 * @param {string} command the ImageMapster method name 505 * @param {object[]} args arguments passed to the method 506 * @return {bool} true if the command was queued, false if not (e.g. there was no need to) 507 */ 508 queueCommand: function (map_data, that, command, args) { 509 if (!map_data) { 510 return false; 511 } 512 if (!map_data.complete || map_data.currentAction) { 513 map_data.commands.push({ 514 that: that, 515 command: command, 516 args: args 517 }); 518 return true; 519 } 520 return false; 521 }, 522 unload: function () { 523 this.impl.unload(); 524 this.utils = null; 525 this.impl = null; 526 $.fn.mapster = null; 527 $.mapster = null; 528 return $('*').off('.mapster'); 529 } 530 }; 531 532 // Config for object prototypes 533 // first: use only first object (for things that should not apply to lists) 534 /// calls back one of two fuinctions, depending on whether an area was obtained. 535 // opts: { 536 // name: 'method name', 537 // key: 'key, 538 // args: 'args' 539 // 540 //} 541 // name: name of method (required) 542 // args: arguments to re-call with 543 // Iterates through all the objects passed, and determines whether it's an area or an image, and calls the appropriate 544 // callback for each. If anything is returned from that callback, the process is stopped and that data return. Otherwise, 545 // the object itself is returned. 546 547 var m = $.mapster, 548 u = m.utils, 549 ap = Array.prototype; 550 551 // jQuery's width() and height() are broken on IE9 in some situations. This tries everything. 552 $.each(['width', 'height'], function (_, e) { 553 var capProp = e.substr(0, 1).toUpperCase() + e.substr(1); 554 // when jqwidth parm is passed, it also checks the jQuery width()/height() property 555 // the issue is that jQUery width() can report a valid size before the image is loaded in some browsers 556 // without it, we can read zero even when image is loaded in other browsers if its not visible 557 // we must still check because stuff like adblock can temporarily block it 558 // what a goddamn headache 559 u['img' + capProp] = function (img, jqwidth) { 560 return ( 561 (jqwidth ? $(img)[e]() : 0) || 562 img[e] || 563 img['natural' + capProp] || 564 img['client' + capProp] || 565 img['offset' + capProp] 566 ); 567 }; 568 }); 569 570 /** 571 * The Method object encapsulates the process of testing an ImageMapster method to see if it's being 572 * invoked on an image, or an area; then queues the command if the MapData is in an active state. 573 * 574 * @param {[jQuery]} that The target of the invocation 575 * @param {[function]} func_map The callback if the target is an imagemap 576 * @param {[function]} func_area The callback if the target is an area 577 * @param {[object]} opt Options: { key: a map key if passed explicitly 578 * name: the command name, if it can be queued, 579 * args: arguments to the method 580 * } 581 */ 582 583 m.Method = function (that, func_map, func_area, opts) { 584 var me = this; 585 me.name = opts.name; 586 me.output = that; 587 me.input = that; 588 me.first = opts.first || false; 589 me.args = opts.args ? ap.slice.call(opts.args, 0) : []; 590 me.key = opts.key; 591 me.func_map = func_map; 592 me.func_area = func_area; 593 //$.extend(me, opts); 594 me.name = opts.name; 595 me.allowAsync = opts.allowAsync || false; 596 }; 597 m.Method.prototype = { 598 constructor: m.Method, 599 go: function () { 600 var i, 601 data, 602 ar, 603 len, 604 result, 605 src = this.input, 606 area_list = [], 607 me = this; 608 609 len = src.length; 610 for (i = 0; i < len; i++) { 611 data = $.mapster.getMapData(src[i]); 612 if (data) { 613 if ( 614 !me.allowAsync && 615 m.queueCommand(data, me.input, me.name, me.args) 616 ) { 617 if (this.first) { 618 result = ''; 619 } 620 continue; 621 } 622 623 ar = data.getData(src[i].nodeName === 'AREA' ? src[i] : this.key); 624 if (ar) { 625 if ($.inArray(ar, area_list) < 0) { 626 area_list.push(ar); 627 } 628 } else { 629 result = this.func_map.apply(data, me.args); 630 } 631 if (this.first || typeof result !== 'undefined') { 632 break; 633 } 634 } 635 } 636 // if there were areas, call the area function for each unique group 637 $(area_list).each(function (_, e) { 638 result = me.func_area.apply(e, me.args); 639 }); 640 641 if (typeof result !== 'undefined') { 642 return result; 643 } else { 644 return this.output; 645 } 646 } 647 }; 648 649 $.mapster.impl = (function () { 650 var me = {}, 651 addMap = function (map_data) { 652 return m.map_cache.push(map_data) - 1; 653 }, 654 removeMap = function (map_data) { 655 m.map_cache.splice(map_data.index, 1); 656 for (var i = m.map_cache.length - 1; i >= map_data.index; i--) { 657 m.map_cache[i].index--; 658 } 659 }; 660 661 /** 662 * Test whether the browser supports VML. Credit: google. 663 * http://stackoverflow.com/questions/654112/how-do-you-detect-support-for-vml-or-svg-in-a-browser 664 * 665 * @return {bool} true if vml is supported, false if not 666 */ 667 668 function hasVml() { 669 var a = $('<div />').appendTo('body'); 670 a.html('<v:shape id="vml_flag1" adj="1" />'); 671 672 var b = a[0].firstChild; 673 b.style.behavior = 'url(#default#VML)'; 674 var has = b ? typeof b.adj === 'object' : true; 675 a.remove(); 676 return has; 677 } 678 679 /** 680 * Return a reference to the IE namespaces object, if available, or an empty object otherwise 681 * @return {obkect} The document.namespaces object. 682 */ 683 function namespaces() { 684 return typeof document.namespaces === 'object' 685 ? document.namespaces 686 : null; 687 } 688 689 /** 690 * Test for the presence of HTML5 Canvas support. This also checks to see if excanvas.js has been 691 * loaded and is faking it; if so, we assume that canvas is not supported. 692 * 693 * @return {bool} true if HTML5 canvas support, false if not 694 */ 695 696 function hasCanvas() { 697 var d = namespaces(); 698 // when g_vml_ is present, then we can be sure excanvas is active, meaning there's not a real canvas. 699 700 return d && d.g_vml_ 701 ? false 702 : $('<canvas />')[0].getContext 703 ? true 704 : false; 705 } 706 707 /** 708 * Merge new area data into existing area options on a MapData object. Used for rebinding. 709 * 710 * @param {[MapData]} map_data The MapData object 711 * @param {[object[]]} areas areas array to merge 712 */ 713 714 function merge_areas(map_data, areas) { 715 var ar, 716 index, 717 map_areas = map_data.options.areas; 718 719 if (areas) { 720 $.each(areas, function (_, e) { 721 // Issue #68 - ignore invalid data in areas array 722 723 if (!e || !e.key) { 724 return; 725 } 726 727 index = u.indexOfProp(map_areas, 'key', e.key); 728 729 if (index >= 0) { 730 $.extend(map_areas[index], e); 731 } else { 732 map_areas.push(e); 733 } 734 ar = map_data.getDataForKey(e.key); 735 if (ar) { 736 $.extend(ar.options, e); 737 } 738 }); 739 } 740 } 741 function merge_options(map_data, options) { 742 var temp_opts = u.updateProps({}, options); 743 delete temp_opts.areas; 744 745 u.updateProps(map_data.options, temp_opts); 746 747 merge_areas(map_data, options.areas); 748 // refresh the area_option template 749 u.updateProps(map_data.area_options, map_data.options); 750 } 751 752 // Most methods use the "Method" object which handles figuring out whether it's an image or area called and 753 // parsing key parameters. The constructor wants: 754 // this, the jQuery object 755 // a function that is called when an image was passed (with a this context of the MapData) 756 // a function that is called when an area was passed (with a this context of the AreaData) 757 // options: first = true means only the first member of a jQuery object is handled 758 // key = the key parameters passed 759 // defaultReturn: a value to return other than the jQuery object (if its not chainable) 760 // args: the arguments 761 // Returns a comma-separated list of user-selected areas. "staticState" areas are not considered selected for the purposes of this method. 762 763 me.get = function (key) { 764 var md = m.getMapData(this); 765 if (!(md && md.complete)) { 766 throw "Can't access data until binding complete."; 767 } 768 769 return new m.Method( 770 this, 771 function () { 772 // map_data return 773 return this.getSelected(); 774 }, 775 function () { 776 return this.isSelected(); 777 }, 778 { 779 name: 'get', 780 args: arguments, 781 key: key, 782 first: true, 783 allowAsync: true, 784 defaultReturn: '' 785 } 786 ).go(); 787 }; 788 me.data = function (key) { 789 return new m.Method( 790 this, 791 null, 792 function () { 793 return this; 794 }, 795 { name: 'data', args: arguments, key: key } 796 ).go(); 797 }; 798 799 // Set or return highlight state. 800 // $(img).mapster('highlight') -- return highlighted area key, or null if none 801 // $(area).mapster('highlight') -- highlight an area 802 // $(img).mapster('highlight','area_key') -- highlight an area 803 // $(img).mapster('highlight',false) -- remove highlight 804 me.highlight = function (key) { 805 return new m.Method( 806 this, 807 function () { 808 if (key === false) { 809 this.ensureNoHighlight(); 810 } else { 811 var id = this.highlightId; 812 return id >= 0 ? this.data[id].key : null; 813 } 814 }, 815 function () { 816 this.highlight(); 817 }, 818 { name: 'highlight', args: arguments, key: key, first: true } 819 ).go(); 820 }; 821 // Return the primary keys for an area or group key. 822 // $(area).mapster('key') 823 // includes all keys (not just primary keys) 824 // $(area).mapster('key',true) 825 // $(img).mapster('key','group-key') 826 827 // $(img).mapster('key','group-key', true) 828 me.keys = function (key, all) { 829 var keyList = [], 830 md = m.getMapData(this); 831 832 if (!(md && md.complete)) { 833 throw "Can't access data until binding complete."; 834 } 835 836 function addUniqueKeys(ad) { 837 var areas, 838 keys = []; 839 if (!all) { 840 keys.push(ad.key); 841 } else { 842 areas = ad.areas(); 843 $.each(areas, function (_, e) { 844 keys = keys.concat(e.keys); 845 }); 846 } 847 $.each(keys, function (_, e) { 848 if ($.inArray(e, keyList) < 0) { 849 keyList.push(e); 850 } 851 }); 852 } 853 854 if (!(md && md.complete)) { 855 return ''; 856 } 857 if (typeof key === 'string') { 858 if (all) { 859 addUniqueKeys(md.getDataForKey(key)); 860 } else { 861 keyList = [md.getKeysForGroup(key)]; 862 } 863 } else { 864 all = key; 865 this.each(function (_, e) { 866 if (e.nodeName === 'AREA') { 867 addUniqueKeys(md.getDataForArea(e)); 868 } 869 }); 870 } 871 return keyList.join(','); 872 }; 873 me.select = function () { 874 me.set.call(this, true); 875 }; 876 me.deselect = function () { 877 me.set.call(this, false); 878 }; 879 880 /** 881 * Select or unselect areas. Areas can be identified by a single string key, a comma-separated list of keys, 882 * or an array of strings. 883 * 884 * 885 * @param {boolean} selected Determines whether areas are selected or deselected 886 * @param {string|string[]} key A string, comma-separated string, or array of strings indicating 887 * the areas to select or deselect 888 * @param {object} options Rendering options to apply when selecting an area 889 */ 890 891 me.set = function (selected, key, options) { 892 var lastMap, 893 map_data, 894 opts = options, 895 key_list, 896 area_list; // array of unique areas passed 897 898 function setSelection(ar) { 899 var newState = selected; 900 if (ar) { 901 switch (selected) { 902 case true: 903 ar.select(opts); 904 break; 905 case false: 906 ar.deselect(true); 907 break; 908 default: 909 newState = ar.toggle(opts); 910 break; 911 } 912 return newState; 913 } 914 } 915 function addArea(ar) { 916 if (ar && $.inArray(ar, area_list) < 0) { 917 area_list.push(ar); 918 key_list += (key_list === '' ? '' : ',') + ar.key; 919 } 920 } 921 // Clean up after a group that applied to the same map 922 function finishSetForMap(map_data) { 923 $.each(area_list, function (_, el) { 924 setSelection(el); 925 }); 926 if (!selected) { 927 map_data.removeSelectionFinish(); 928 } 929 } 930 931 this.filter('img,area').each(function (_, e) { 932 var keys; 933 map_data = m.getMapData(e); 934 935 if (map_data !== lastMap) { 936 if (lastMap) { 937 finishSetForMap(lastMap); 938 } 939 940 area_list = []; 941 key_list = ''; 942 } 943 944 if (map_data) { 945 keys = ''; 946 if (e.nodeName.toUpperCase() === 'IMG') { 947 if (!m.queueCommand(map_data, $(e), 'set', [selected, key, opts])) { 948 if (key instanceof Array) { 949 if (key.length) { 950 keys = key.join(','); 951 } 952 } else { 953 keys = key; 954 } 955 956 if (keys) { 957 $.each(u.split(keys), function (_, key) { 958 addArea(map_data.getDataForKey(key.toString())); 959 lastMap = map_data; 960 }); 961 } 962 } 963 } else { 964 opts = key; 965 if (!m.queueCommand(map_data, $(e), 'set', [selected, opts])) { 966 addArea(map_data.getDataForArea(e)); 967 lastMap = map_data; 968 } 969 } 970 } 971 }); 972 973 if (map_data) { 974 finishSetForMap(map_data); 975 } 976 977 return this; 978 }; 979 me.unbind = function (preserveState) { 980 return new m.Method( 981 this, 982 function () { 983 this.clearEvents(); 984 this.clearMapData(preserveState); 985 removeMap(this); 986 }, 987 null, 988 { name: 'unbind', args: arguments } 989 ).go(); 990 }; 991 992 // refresh options and update selection information. 993 me.rebind = function (options) { 994 return new m.Method( 995 this, 996 function () { 997 var me = this; 998 999 me.complete = false; 1000 me.configureOptions(options); 1001 me.bindImages().then(function () { 1002 me.buildDataset(true); 1003 me.complete = true; 1004 me.onConfigured(); 1005 }); 1006 //this.redrawSelections(); 1007 }, 1008 null, 1009 { 1010 name: 'rebind', 1011 args: arguments 1012 } 1013 ).go(); 1014 }; 1015 // get options. nothing or false to get, or "true" to get effective options (versus passed options) 1016 me.get_options = function (key, effective) { 1017 var eff = u.isBool(key) ? key : effective; // allow 2nd parm as "effective" when no key 1018 return new m.Method( 1019 this, 1020 function () { 1021 var opts = $.extend({}, this.options); 1022 if (eff) { 1023 opts.render_select = u.updateProps( 1024 {}, 1025 m.render_defaults, 1026 opts, 1027 opts.render_select 1028 ); 1029 1030 opts.render_highlight = u.updateProps( 1031 {}, 1032 m.render_defaults, 1033 opts, 1034 opts.render_highlight 1035 ); 1036 } 1037 return opts; 1038 }, 1039 function () { 1040 return eff ? this.effectiveOptions() : this.options; 1041 }, 1042 { 1043 name: 'get_options', 1044 args: arguments, 1045 first: true, 1046 allowAsync: true, 1047 key: key 1048 } 1049 ).go(); 1050 }; 1051 1052 // set options - pass an object with options to set, 1053 me.set_options = function (options) { 1054 return new m.Method( 1055 this, 1056 function () { 1057 merge_options(this, options); 1058 }, 1059 null, 1060 { 1061 name: 'set_options', 1062 args: arguments 1063 } 1064 ).go(); 1065 }; 1066 me.unload = function () { 1067 var i; 1068 for (i = m.map_cache.length - 1; i >= 0; i--) { 1069 if (m.map_cache[i]) { 1070 me.unbind.call($(m.map_cache[i].image)); 1071 } 1072 } 1073 me.graphics = null; 1074 }; 1075 1076 me.snapshot = function () { 1077 return new m.Method( 1078 this, 1079 function () { 1080 $.each(this.data, function (_, e) { 1081 e.selected = false; 1082 }); 1083 1084 this.base_canvas = this.graphics.createVisibleCanvas(this); 1085 $(this.image).before(this.base_canvas); 1086 }, 1087 null, 1088 { name: 'snapshot' } 1089 ).go(); 1090 }; 1091 1092 // do not queue this function 1093 1094 me.state = function () { 1095 var md, 1096 result = null; 1097 $(this).each(function (_, e) { 1098 if (e.nodeName === 'IMG') { 1099 md = m.getMapData(e); 1100 if (md) { 1101 result = md.state(); 1102 } 1103 return false; 1104 } 1105 }); 1106 return result; 1107 }; 1108 1109 me.bind = function (options) { 1110 return this.each(function (_, e) { 1111 var img, map, usemap, md; 1112 1113 // save ref to this image even if we can't access it yet. commands will be queued 1114 img = $(e); 1115 1116 md = m.getMapData(e); 1117 1118 // if already bound completely, do a total rebind 1119 1120 if (md) { 1121 me.unbind.apply(img); 1122 if (!md.complete) { 1123 // will be queued 1124 return true; 1125 } 1126 md = null; 1127 } 1128 1129 // ensure it's a valid image 1130 // jQuery bug with Opera, results in full-url#usemap being returned from jQuery's attr. 1131 // So use raw getAttribute instead. 1132 1133 usemap = this.getAttribute('usemap'); 1134 map = usemap && $('map[name="' + usemap.substr(1) + '"]'); 1135 if (!(img.is('img') && usemap && map.length > 0)) { 1136 return true; 1137 } 1138 1139 // sorry - your image must have border:0, things are too unpredictable otherwise. 1140 img.css('border', 0); 1141 1142 if (!md) { 1143 md = new m.MapData(this, options); 1144 1145 md.index = addMap(md); 1146 md.map = map; 1147 md.bindImages().then(function () { 1148 md.initialize(); 1149 }); 1150 } 1151 }); 1152 }; 1153 1154 me.init = function (useCanvas) { 1155 var style, shapes; 1156 1157 // for testing/debugging, use of canvas can be forced by initializing 1158 // manually with "true" or "false". But generally we test for it. 1159 1160 m.hasCanvas = function () { 1161 if (!u.isBool(m.hasCanvas.value)) { 1162 m.hasCanvas.value = u.isBool(useCanvas) ? useCanvas : hasCanvas(); 1163 } 1164 return m.hasCanvas.value; 1165 }; 1166 1167 m.hasVml = function () { 1168 if (!u.isBool(m.hasVml.value)) { 1169 // initialize VML the first time we detect its presence. 1170 var d = namespaces(); 1171 1172 if (d && !d.v) { 1173 d.add('v', 'urn:schemas-microsoft-com:vml'); 1174 style = document.createStyleSheet(); 1175 shapes = [ 1176 'shape', 1177 'rect', 1178 'oval', 1179 'circ', 1180 'fill', 1181 'stroke', 1182 'imagedata', 1183 'group', 1184 'textbox' 1185 ]; 1186 $.each(shapes, function (_, el) { 1187 style.addRule( 1188 'v\\:' + el, 1189 'behavior: url(#default#VML); antialias:true' 1190 ); 1191 }); 1192 } 1193 m.hasVml.value = hasVml(); 1194 } 1195 1196 return m.hasVml.value; 1197 }; 1198 1199 $.extend(m.defaults, m.render_defaults, m.shared_defaults); 1200 $.extend(m.area_defaults, m.render_defaults, m.shared_defaults); 1201 }; 1202 me.test = function (obj) { 1203 return eval(obj); 1204 }; 1205 return me; 1206 })(); 1207 1208 $.mapster.impl.init(); 1209})(jQuery); 1210 1211/* 1212 graphics.js 1213 Graphics object handles all rendering. 1214*/ 1215 1216(function ($) { 1217 'use strict'; 1218 1219 var p, 1220 m = $.mapster, 1221 u = m.utils, 1222 canvasMethods, 1223 vmlMethods; 1224 1225 /** 1226 * Implemenation to add each area in an AreaData object to the canvas 1227 * @param {Graphics} graphics The target graphics object 1228 * @param {AreaData} areaData The AreaData object (a collection of area elements and metadata) 1229 * @param {object} options Rendering options to apply when rendering this group of areas 1230 */ 1231 function addShapeGroupImpl(graphics, areaData, options) { 1232 var me = graphics, 1233 md = me.map_data, 1234 isMask = options.isMask; 1235 1236 // first get area options. Then override fade for selecting, and finally merge in the 1237 // "select" effect options. 1238 1239 $.each(areaData.areas(), function (_, e) { 1240 options.isMask = isMask || (e.nohref && md.options.noHrefIsMask); 1241 me.addShape(e, options); 1242 }); 1243 1244 // it's faster just to manipulate the passed options isMask property and restore it, than to 1245 // copy the object each time 1246 1247 options.isMask = isMask; 1248 } 1249 1250 /** 1251 * Convert a hex value to decimal 1252 * @param {string} hex A hexadecimal toString 1253 * @return {int} Integer represenation of the hex string 1254 */ 1255 1256 function hex_to_decimal(hex) { 1257 return Math.max(0, Math.min(parseInt(hex, 16), 255)); 1258 } 1259 function css3color(color, opacity) { 1260 return ( 1261 'rgba(' + 1262 hex_to_decimal(color.substr(0, 2)) + 1263 ',' + 1264 hex_to_decimal(color.substr(2, 2)) + 1265 ',' + 1266 hex_to_decimal(color.substr(4, 2)) + 1267 ',' + 1268 opacity + 1269 ')' 1270 ); 1271 } 1272 /** 1273 * An object associated with a particular map_data instance to manage renderin. 1274 * @param {MapData} map_data The MapData object bound to this instance 1275 */ 1276 1277 m.Graphics = function (map_data) { 1278 //$(window).unload($.mapster.unload); 1279 // create graphics functions for canvas and vml browsers. usage: 1280 // 1) init with map_data, 2) call begin with canvas to be used (these are separate b/c may not require canvas to be specified 1281 // 3) call add_shape_to for each shape or mask, 4) call render() to finish 1282 1283 var me = this; 1284 me.active = false; 1285 me.canvas = null; 1286 me.width = 0; 1287 me.height = 0; 1288 me.shapes = []; 1289 me.masks = []; 1290 me.map_data = map_data; 1291 }; 1292 1293 p = m.Graphics.prototype = { 1294 constructor: m.Graphics, 1295 1296 /** 1297 * Initiate a graphics request for a canvas 1298 * @param {Element} canvas The canvas element that is the target of this operation 1299 * @param {string} [elementName] The name to assign to the element (VML only) 1300 */ 1301 1302 begin: function (canvas, elementName) { 1303 var c = $(canvas); 1304 1305 this.elementName = elementName; 1306 this.canvas = canvas; 1307 1308 this.width = c.width(); 1309 this.height = c.height(); 1310 this.shapes = []; 1311 this.masks = []; 1312 this.active = true; 1313 }, 1314 1315 /** 1316 * Add an area to be rendered to this canvas. 1317 * @param {MapArea} mapArea The MapArea object to render 1318 * @param {object} options An object containing any rendering options that should override the 1319 * defaults for the area 1320 */ 1321 1322 addShape: function (mapArea, options) { 1323 var addto = options.isMask ? this.masks : this.shapes; 1324 addto.push({ mapArea: mapArea, options: options }); 1325 }, 1326 1327 /** 1328 * Create a canvas that is sized and styled for the MapData object 1329 * @param {MapData} mapData The MapData object that will receive this new canvas 1330 * @return {Element} A canvas element 1331 */ 1332 1333 createVisibleCanvas: function (mapData) { 1334 return $(this.createCanvasFor(mapData)) 1335 .addClass('mapster_el') 1336 .css(m.canvas_style)[0]; 1337 }, 1338 1339 /** 1340 * Add a group of shapes from an AreaData object to the canvas 1341 * 1342 * @param {AreaData} areaData An AreaData object (a set of area elements) 1343 * @param {string} mode The rendering mode, "select" or "highlight". This determines the target 1344 * canvas and which default options to use. 1345 * @param {striong} options Rendering options 1346 */ 1347 1348 addShapeGroup: function (areaData, mode, options) { 1349 // render includeKeys first - because they could be masks 1350 var me = this, 1351 list, 1352 name, 1353 canvas, 1354 map_data = this.map_data, 1355 opts = areaData.effectiveRenderOptions(mode); 1356 1357 if (options) { 1358 $.extend(opts, options); 1359 } 1360 1361 if (mode === 'select') { 1362 name = 'static_' + areaData.areaId.toString(); 1363 canvas = map_data.base_canvas; 1364 } else { 1365 canvas = map_data.overlay_canvas; 1366 } 1367 1368 me.begin(canvas, name); 1369 1370 if (opts.includeKeys) { 1371 list = u.split(opts.includeKeys); 1372 $.each(list, function (_, e) { 1373 var areaData = map_data.getDataForKey(e.toString()); 1374 addShapeGroupImpl( 1375 me, 1376 areaData, 1377 areaData.effectiveRenderOptions(mode) 1378 ); 1379 }); 1380 } 1381 1382 addShapeGroupImpl(me, areaData, opts); 1383 me.render(); 1384 if (opts.fade) { 1385 // fading requires special handling for IE. We must access the fill elements directly. The fader also has to deal with 1386 // the "opacity" attribute (not css) 1387 1388 u.fader( 1389 m.hasCanvas() 1390 ? canvas 1391 : $(canvas).find('._fill').not('.mapster_mask'), 1392 0, 1393 m.hasCanvas() ? 1 : opts.fillOpacity, 1394 opts.fadeDuration 1395 ); 1396 } 1397 } 1398 1399 // These prototype methods are implementation dependent 1400 }; 1401 1402 function noop() {} 1403 1404 // configure remaining prototype methods for ie or canvas-supporting browser 1405 1406 canvasMethods = { 1407 renderShape: function (context, mapArea, offset) { 1408 var i, 1409 c = mapArea.coords(null, offset); 1410 1411 switch (mapArea.shape) { 1412 case 'rect': 1413 case 'rectangle': 1414 context.rect(c[0], c[1], c[2] - c[0], c[3] - c[1]); 1415 break; 1416 case 'poly': 1417 case 'polygon': 1418 context.moveTo(c[0], c[1]); 1419 1420 for (i = 2; i < mapArea.length; i += 2) { 1421 context.lineTo(c[i], c[i + 1]); 1422 } 1423 context.lineTo(c[0], c[1]); 1424 break; 1425 case 'circ': 1426 case 'circle': 1427 context.arc(c[0], c[1], c[2], 0, Math.PI * 2, false); 1428 break; 1429 } 1430 }, 1431 addAltImage: function (context, image, mapArea, options) { 1432 context.beginPath(); 1433 1434 this.renderShape(context, mapArea); 1435 context.closePath(); 1436 context.clip(); 1437 1438 context.globalAlpha = options.altImageOpacity || options.fillOpacity; 1439 1440 context.drawImage( 1441 image, 1442 0, 1443 0, 1444 mapArea.owner.scaleInfo.width, 1445 mapArea.owner.scaleInfo.height 1446 ); 1447 }, 1448 render: function () { 1449 // firefox 6.0 context.save() seems to be broken. to work around, we have to draw the contents on one temp canvas, 1450 // the mask on another, and merge everything. ugh. fixed in 1.2.2. unfortunately this is a lot more code for masks, 1451 // but no other way around it that i can see. 1452 1453 var maskCanvas, 1454 maskContext, 1455 me = this, 1456 md = me.map_data, 1457 hasMasks = me.masks.length, 1458 shapeCanvas = me.createCanvasFor(md), 1459 shapeContext = shapeCanvas.getContext('2d'), 1460 context = me.canvas.getContext('2d'); 1461 1462 if (hasMasks) { 1463 maskCanvas = me.createCanvasFor(md); 1464 maskContext = maskCanvas.getContext('2d'); 1465 maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height); 1466 1467 $.each(me.masks, function (_, e) { 1468 maskContext.save(); 1469 maskContext.beginPath(); 1470 me.renderShape(maskContext, e.mapArea); 1471 maskContext.closePath(); 1472 maskContext.clip(); 1473 maskContext.lineWidth = 0; 1474 maskContext.fillStyle = '#000'; 1475 maskContext.fill(); 1476 maskContext.restore(); 1477 }); 1478 } 1479 1480 $.each(me.shapes, function (_, s) { 1481 shapeContext.save(); 1482 if (s.options.fill) { 1483 if (s.options.altImageId) { 1484 me.addAltImage( 1485 shapeContext, 1486 md.images[s.options.altImageId], 1487 s.mapArea, 1488 s.options 1489 ); 1490 } else { 1491 shapeContext.beginPath(); 1492 me.renderShape(shapeContext, s.mapArea); 1493 shapeContext.closePath(); 1494 //shapeContext.clip(); 1495 shapeContext.fillStyle = css3color( 1496 s.options.fillColor, 1497 s.options.fillOpacity 1498 ); 1499 shapeContext.fill(); 1500 } 1501 } 1502 shapeContext.restore(); 1503 }); 1504 1505 // render strokes at end since masks get stroked too 1506 1507 $.each(me.shapes.concat(me.masks), function (_, s) { 1508 var offset = s.options.strokeWidth === 1 ? 0.5 : 0; 1509 // offset applies only when stroke width is 1 and stroke would render between pixels. 1510 1511 if (s.options.stroke) { 1512 shapeContext.save(); 1513 shapeContext.strokeStyle = css3color( 1514 s.options.strokeColor, 1515 s.options.strokeOpacity 1516 ); 1517 shapeContext.lineWidth = s.options.strokeWidth; 1518 1519 shapeContext.beginPath(); 1520 1521 me.renderShape(shapeContext, s.mapArea, offset); 1522 shapeContext.closePath(); 1523 shapeContext.stroke(); 1524 shapeContext.restore(); 1525 } 1526 }); 1527 1528 if (hasMasks) { 1529 // render the new shapes against the mask 1530 1531 maskContext.globalCompositeOperation = 'source-out'; 1532 maskContext.drawImage(shapeCanvas, 0, 0); 1533 1534 // flatten into the main canvas 1535 context.drawImage(maskCanvas, 0, 0); 1536 } else { 1537 context.drawImage(shapeCanvas, 0, 0); 1538 } 1539 1540 me.active = false; 1541 return me.canvas; 1542 }, 1543 1544 // create a canvas mimicing dimensions of an existing element 1545 createCanvasFor: function (md) { 1546 return $( 1547 '<canvas width="' + 1548 md.scaleInfo.width + 1549 '" height="' + 1550 md.scaleInfo.height + 1551 '"></canvas>' 1552 )[0]; 1553 }, 1554 clearHighlight: function () { 1555 var c = this.map_data.overlay_canvas; 1556 c.getContext('2d').clearRect(0, 0, c.width, c.height); 1557 }, 1558 // Draw all items from selected_list to a new canvas, then swap with the old one. This is used to delete items when using canvases. 1559 refreshSelections: function () { 1560 var canvas_temp, 1561 map_data = this.map_data; 1562 // draw new base canvas, then swap with the old one to avoid flickering 1563 canvas_temp = map_data.base_canvas; 1564 1565 map_data.base_canvas = this.createVisibleCanvas(map_data); 1566 $(map_data.base_canvas).hide(); 1567 $(canvas_temp).before(map_data.base_canvas); 1568 1569 map_data.redrawSelections(); 1570 1571 $(map_data.base_canvas).show(); 1572 $(canvas_temp).remove(); 1573 } 1574 }; 1575 1576 vmlMethods = { 1577 renderShape: function (mapArea, options, cssclass) { 1578 var me = this, 1579 fill, 1580 stroke, 1581 e, 1582 t_fill, 1583 el_name, 1584 el_class, 1585 template, 1586 c = mapArea.coords(); 1587 el_name = me.elementName ? 'name="' + me.elementName + '" ' : ''; 1588 el_class = cssclass ? 'class="' + cssclass + '" ' : ''; 1589 1590 t_fill = 1591 '<v:fill color="#' + 1592 options.fillColor + 1593 '" class="_fill" opacity="' + 1594 (options.fill ? options.fillOpacity : 0) + 1595 '" /><v:stroke class="_fill" opacity="' + 1596 options.strokeOpacity + 1597 '"/>'; 1598 1599 stroke = options.stroke 1600 ? ' strokeweight=' + 1601 options.strokeWidth + 1602 ' stroked="t" strokecolor="#' + 1603 options.strokeColor + 1604 '"' 1605 : ' stroked="f"'; 1606 1607 fill = options.fill ? ' filled="t"' : ' filled="f"'; 1608 1609 switch (mapArea.shape) { 1610 case 'rect': 1611 case 'rectangle': 1612 template = 1613 '<v:rect ' + 1614 el_class + 1615 el_name + 1616 fill + 1617 stroke + 1618 ' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:' + 1619 c[0] + 1620 'px;top:' + 1621 c[1] + 1622 'px;width:' + 1623 (c[2] - c[0]) + 1624 'px;height:' + 1625 (c[3] - c[1]) + 1626 'px;">' + 1627 t_fill + 1628 '</v:rect>'; 1629 break; 1630 case 'poly': 1631 case 'polygon': 1632 template = 1633 '<v:shape ' + 1634 el_class + 1635 el_name + 1636 fill + 1637 stroke + 1638 ' coordorigin="0,0" coordsize="' + 1639 me.width + 1640 ',' + 1641 me.height + 1642 '" path="m ' + 1643 c[0] + 1644 ',' + 1645 c[1] + 1646 ' l ' + 1647 c.slice(2).join(',') + 1648 ' x e" style="zoom:1;margin:0;padding:0;display:block;position:absolute;top:0px;left:0px;width:' + 1649 me.width + 1650 'px;height:' + 1651 me.height + 1652 'px;">' + 1653 t_fill + 1654 '</v:shape>'; 1655 break; 1656 case 'circ': 1657 case 'circle': 1658 template = 1659 '<v:oval ' + 1660 el_class + 1661 el_name + 1662 fill + 1663 stroke + 1664 ' style="zoom:1;margin:0;padding:0;display:block;position:absolute;left:' + 1665 (c[0] - c[2]) + 1666 'px;top:' + 1667 (c[1] - c[2]) + 1668 'px;width:' + 1669 c[2] * 2 + 1670 'px;height:' + 1671 c[2] * 2 + 1672 'px;">' + 1673 t_fill + 1674 '</v:oval>'; 1675 break; 1676 } 1677 e = $(template); 1678 $(me.canvas).append(e); 1679 1680 return e; 1681 }, 1682 render: function () { 1683 var opts, 1684 me = this; 1685 1686 $.each(this.shapes, function (_, e) { 1687 me.renderShape(e.mapArea, e.options); 1688 }); 1689 1690 if (this.masks.length) { 1691 $.each(this.masks, function (_, e) { 1692 opts = u.updateProps({}, e.options, { 1693 fillOpacity: 1, 1694 fillColor: e.options.fillColorMask 1695 }); 1696 me.renderShape(e.mapArea, opts, 'mapster_mask'); 1697 }); 1698 } 1699 1700 this.active = false; 1701 return this.canvas; 1702 }, 1703 1704 createCanvasFor: function (md) { 1705 var w = md.scaleInfo.width, 1706 h = md.scaleInfo.height; 1707 return $( 1708 '<var width="' + 1709 w + 1710 '" height="' + 1711 h + 1712 '" style="zoom:1;overflow:hidden;display:block;width:' + 1713 w + 1714 'px;height:' + 1715 h + 1716 'px;"></var>' 1717 )[0]; 1718 }, 1719 1720 clearHighlight: function () { 1721 $(this.map_data.overlay_canvas).children().remove(); 1722 }, 1723 // remove single or all selections 1724 removeSelections: function (area_id) { 1725 if (area_id >= 0) { 1726 $(this.map_data.base_canvas) 1727 .find('[name="static_' + area_id.toString() + '"]') 1728 .remove(); 1729 } else { 1730 $(this.map_data.base_canvas).children().remove(); 1731 } 1732 } 1733 }; 1734 1735 // for all methods with two implemenatations, add a function that will automatically replace itself with the correct 1736 // method on first invocation 1737 1738 $.each( 1739 [ 1740 'renderShape', 1741 'addAltImage', 1742 'render', 1743 'createCanvasFor', 1744 'clearHighlight', 1745 'removeSelections', 1746 'refreshSelections' 1747 ], 1748 function (_, e) { 1749 p[e] = (function (method) { 1750 return function () { 1751 p[method] = 1752 (m.hasCanvas() ? canvasMethods[method] : vmlMethods[method]) || 1753 noop; 1754 1755 return p[method].apply(this, arguments); 1756 }; 1757 })(e); 1758 } 1759 ); 1760})(jQuery); 1761 1762/* 1763 mapimage.js 1764 The MapImage object, repesents an instance of a single bound imagemap 1765*/ 1766 1767(function ($) { 1768 'use strict'; 1769 1770 var m = $.mapster, 1771 u = m.utils, 1772 ap = []; 1773 /** 1774 * An object encapsulating all the images used by a MapData. 1775 */ 1776 1777 m.MapImages = function (owner) { 1778 this.owner = owner; 1779 this.clear(); 1780 }; 1781 1782 m.MapImages.prototype = { 1783 constructor: m.MapImages, 1784 1785 /* interface to make this array-like */ 1786 1787 slice: function () { 1788 return ap.slice.apply(this, arguments); 1789 }, 1790 splice: function () { 1791 ap.slice.apply(this.status, arguments); 1792 var result = ap.slice.apply(this, arguments); 1793 return result; 1794 }, 1795 1796 /** 1797 * a boolean value indicates whether all images are done loading 1798 * @return {bool} true when all are done 1799 */ 1800 complete: function () { 1801 return $.inArray(false, this.status) < 0; 1802 }, 1803 1804 /** 1805 * Save an image in the images array and return its index 1806 * @param {Image} image An Image object 1807 * @return {int} the index of the image 1808 */ 1809 1810 _add: function (image) { 1811 var index = ap.push.call(this, image) - 1; 1812 this.status[index] = false; 1813 return index; 1814 }, 1815 1816 /** 1817 * Return the index of an Image within the images array 1818 * @param {Image} img An Image 1819 * @return {int} the index within the array, or -1 if it was not found 1820 */ 1821 1822 indexOf: function (image) { 1823 return u.indexOf(this, image); 1824 }, 1825 1826 /** 1827 * Clear this object and reset it to its initial state after binding. 1828 */ 1829 1830 clear: function () { 1831 var me = this; 1832 1833 if (me.ids && me.ids.length > 0) { 1834 $.each(me.ids, function (_, e) { 1835 delete me[e]; 1836 }); 1837 } 1838 1839 /** 1840 * A list of the cross-reference IDs bound to this object 1841 * @type {string[]} 1842 */ 1843 1844 me.ids = []; 1845 1846 /** 1847 * Length property for array-like behavior, set to zero when initializing. Array prototype 1848 * methods will update it after that. 1849 * 1850 * @type {int} 1851 */ 1852 1853 me.length = 0; 1854 1855 /** 1856 * the loaded status of the corresponding image 1857 * @type {boolean[]} 1858 */ 1859 1860 me.status = []; 1861 1862 // actually erase the images 1863 1864 me.splice(0); 1865 }, 1866 1867 /** 1868 * Bind an image to the map and add it to the queue to be loaded; return an ID that 1869 * can be used to reference the 1870 * 1871 * @param {Image|string} image An Image object or a URL to an image 1872 * @param {string} [id] An id to refer to this image 1873 * @returns {int} an ID referencing the index of the image object in 1874 * map_data.images 1875 */ 1876 1877 add: function (image, id) { 1878 var index, 1879 src, 1880 me = this; 1881 1882 if (!image) { 1883 return; 1884 } 1885 1886 if (typeof image === 'string') { 1887 src = image; 1888 image = me[src]; 1889 if (typeof image === 'object') { 1890 return me.indexOf(image); 1891 } 1892 1893 image = $('<img />').addClass('mapster_el').hide(); 1894 1895 index = me._add(image[0]); 1896 1897 image 1898 .on('load.mapster', function (e) { 1899 me.imageLoaded.call(me, e); 1900 }) 1901 .on('error.mapster', function (e) { 1902 me.imageLoadError.call(me, e); 1903 }); 1904 1905 image.attr('src', src); 1906 } else { 1907 // use attr because we want the actual source, not the resolved path the browser will return directly calling image.src 1908 1909 index = me._add($(image)[0]); 1910 } 1911 if (id) { 1912 if (this[id]) { 1913 throw ( 1914 id + ' is already used or is not available as an altImage alias.' 1915 ); 1916 } 1917 me.ids.push(id); 1918 me[id] = me[index]; 1919 } 1920 return index; 1921 }, 1922 1923 /** 1924 * Bind the images in this object, 1925 * @return {Promise} a promise that resolves when the images have finished loading 1926 */ 1927 1928 bind: function () { 1929 var me = this, 1930 promise, 1931 triesLeft = me.owner.options.configTimeout / 200, 1932 /* A recursive function to continue checking that the images have been 1933 loaded until a timeout has elapsed */ 1934 1935 check = function () { 1936 var i; 1937 1938 // refresh status of images 1939 1940 i = me.length; 1941 1942 while (i-- > 0) { 1943 if (!me.isLoaded(i)) { 1944 break; 1945 } 1946 } 1947 1948 // check to see if every image has already been loaded 1949 1950 if (me.complete()) { 1951 me.resolve(); 1952 } else { 1953 // to account for failure of onLoad to fire in rare situations 1954 if (triesLeft-- > 0) { 1955 me.imgTimeout = window.setTimeout(function () { 1956 check.call(me, true); 1957 }, 50); 1958 } else { 1959 me.imageLoadError.call(me); 1960 } 1961 } 1962 }; 1963 1964 promise = me.deferred = u.defer(); 1965 1966 check(); 1967 return promise; 1968 }, 1969 1970 resolve: function () { 1971 var me = this, 1972 resolver = me.deferred; 1973 1974 if (resolver) { 1975 // Make a copy of the resolver before calling & removing it to ensure 1976 // it is not called twice 1977 me.deferred = null; 1978 resolver.resolve(); 1979 } 1980 }, 1981 1982 /** 1983 * Event handler for image onload 1984 * @param {object} e jQuery event data 1985 */ 1986 1987 imageLoaded: function (e) { 1988 var me = this, 1989 index = me.indexOf(e.target); 1990 1991 if (index >= 0) { 1992 me.status[index] = true; 1993 if ($.inArray(false, me.status) < 0) { 1994 me.resolve(); 1995 } 1996 } 1997 }, 1998 1999 /** 2000 * Event handler for onload error 2001 * @param {object} e jQuery event data 2002 */ 2003 2004 imageLoadError: function (e) { 2005 clearTimeout(this.imgTimeout); 2006 this.triesLeft = 0; 2007 var err = e 2008 ? 'The image ' + e.target.src + ' failed to load.' 2009 : 'The images never seemed to finish loading. You may just need to increase the configTimeout if images could take a long time to load.'; 2010 throw err; 2011 }, 2012 /** 2013 * Test if the image at specificed index has finished loading 2014 * @param {int} index The image index 2015 * @return {boolean} true if loaded, false if not 2016 */ 2017 2018 isLoaded: function (index) { 2019 var img, 2020 me = this, 2021 status = me.status; 2022 2023 if (status[index]) { 2024 return true; 2025 } 2026 img = me[index]; 2027 2028 if (typeof img.complete !== 'undefined') { 2029 status[index] = img.complete; 2030 } else { 2031 status[index] = !!u.imgWidth(img); 2032 } 2033 // if complete passes, the image is loaded, but may STILL not be available because of stuff like adblock. 2034 // make sure it is. 2035 2036 return status[index]; 2037 } 2038 }; 2039})(jQuery); 2040 2041/* 2042 mapdata.js 2043 The MapData object, repesents an instance of a single bound imagemap 2044*/ 2045 2046(function ($) { 2047 'use strict'; 2048 2049 var m = $.mapster, 2050 u = m.utils; 2051 2052 /** 2053 * Set default values for MapData object properties 2054 * @param {MapData} me The MapData object 2055 */ 2056 2057 function initializeDefaults(me) { 2058 $.extend(me, { 2059 complete: false, // (bool) when configuration is complete 2060 map: null, // ($) the image map 2061 base_canvas: null, // (canvas|var) where selections are rendered 2062 overlay_canvas: null, // (canvas|var) where highlights are rendered 2063 commands: [], // {} commands that were run before configuration was completed (b/c images weren't loaded) 2064 data: [], // MapData[] area groups 2065 mapAreas: [], // MapArea[] list. AreaData entities contain refs to this array, so options are stored with each. 2066 _xref: {}, // (int) xref of mapKeys to data[] 2067 highlightId: -1, // (int) the currently highlighted element. 2068 currentAreaId: -1, 2069 _tooltip_events: [], // {} info on events we bound to a tooltip container, so we can properly unbind them 2070 scaleInfo: null, // {} info about the image size, scaling, defaults 2071 index: -1, // index of this in map_cache - so we have an ID to use for wraper div 2072 activeAreaEvent: null, 2073 autoResizeTimer: null // tracks autoresize timer based on options.autoResizeDelay 2074 }); 2075 } 2076 2077 /** 2078 * Return an array of all image-containing options from an options object; 2079 * that is, containers that may have an "altImage" property 2080 * 2081 * @param {object} obj An options object 2082 * @return {object[]} An array of objects 2083 */ 2084 function getOptionImages(obj) { 2085 return [obj, obj.render_highlight, obj.render_select]; 2086 } 2087 2088 /** 2089 * Parse all the altImage references, adding them to the library so they can be preloaded 2090 * and aliased. 2091 * 2092 * @param {MapData} me The MapData object on which to operate 2093 */ 2094 function configureAltImages(me) { 2095 var opts = me.options, 2096 mi = me.images; 2097 2098 // add alt images 2099 2100 if (m.hasCanvas()) { 2101 // map altImage library first 2102 2103 $.each(opts.altImages || {}, function (i, e) { 2104 mi.add(e, i); 2105 }); 2106 2107 // now find everything else 2108 2109 $.each([opts].concat(opts.areas), function (_, e) { 2110 $.each(getOptionImages(e), function (_, e2) { 2111 if (e2 && e2.altImage) { 2112 e2.altImageId = mi.add(e2.altImage); 2113 } 2114 }); 2115 }); 2116 } 2117 2118 // set area_options 2119 me.area_options = u.updateProps( 2120 {}, // default options for any MapArea 2121 m.area_defaults, 2122 opts 2123 ); 2124 } 2125 2126 /** 2127 * Queue a mouse move action based on current delay settings 2128 * (helper for mouseover/mouseout handlers) 2129 * 2130 * @param {MapData} me The MapData context 2131 * @param {number} delay The number of milliseconds to delay the action 2132 * @param {AreaData} area AreaData affected 2133 * @param {Deferred} deferred A deferred object to return (instead of a new one) 2134 * @return {Promise} A promise that resolves when the action is completed 2135 */ 2136 function queueMouseEvent(me, delay, area, deferred) { 2137 deferred = deferred || u.when.defer(); 2138 2139 function cbFinal(areaId) { 2140 if (me.currentAreaId !== areaId && me.highlightId >= 0) { 2141 deferred.resolve({ completeAction: true }); 2142 } 2143 } 2144 if (me.activeAreaEvent) { 2145 window.clearTimeout(me.activeAreaEvent); 2146 me.activeAreaEvent = 0; 2147 } 2148 if (delay < 0) { 2149 deferred.resolve({ completeAction: false }); 2150 } else { 2151 if (area.owner.currentAction || delay) { 2152 me.activeAreaEvent = window.setTimeout( 2153 (function () { 2154 return function () { 2155 queueMouseEvent(me, 0, area, deferred); 2156 }; 2157 })(area), 2158 delay || 100 2159 ); 2160 } else { 2161 cbFinal(area.areaId); 2162 } 2163 } 2164 return deferred; 2165 } 2166 2167 function shouldNavigateTo(href) { 2168 return !!href && href !== '#'; 2169 } 2170 2171 /** 2172 * Mousedown event. This is captured only to prevent browser from drawing an outline around an 2173 * area when it's clicked. 2174 * 2175 * @param {EventData} e jQuery event data 2176 */ 2177 2178 function mousedown(e) { 2179 if (!m.hasCanvas()) { 2180 this.blur(); 2181 } 2182 e.preventDefault(); 2183 } 2184 2185 /** 2186 * Mouseover event. Handle highlight rendering and client callback on mouseover 2187 * 2188 * @param {MapData} me The MapData context 2189 * @param {EventData} e jQuery event data 2190 * @return {[type]} [description] 2191 */ 2192 2193 function mouseover(me, e) { 2194 var arData = me.getAllDataForArea(this), 2195 ar = arData.length ? arData[0] : null; 2196 2197 // mouseover events are ignored entirely while resizing, though we do care about mouseout events 2198 // and must queue the action to keep things clean. 2199 2200 if (!ar || ar.isNotRendered() || ar.owner.currentAction) { 2201 return; 2202 } 2203 2204 if (me.currentAreaId === ar.areaId) { 2205 return; 2206 } 2207 if (me.highlightId !== ar.areaId) { 2208 me.clearEffects(); 2209 2210 ar.highlight(); 2211 2212 if (me.options.showToolTip) { 2213 $.each(arData, function (_, e) { 2214 if (e.effectiveOptions().toolTip) { 2215 e.showToolTip(); 2216 } 2217 }); 2218 } 2219 } 2220 2221 me.currentAreaId = ar.areaId; 2222 2223 if (u.isFunction(me.options.onMouseover)) { 2224 me.options.onMouseover.call(this, { 2225 e: e, 2226 options: ar.effectiveOptions(), 2227 key: ar.key, 2228 selected: ar.isSelected() 2229 }); 2230 } 2231 } 2232 2233 /** 2234 * Mouseout event. 2235 * 2236 * @param {MapData} me The MapData context 2237 * @param {EventData} e jQuery event data 2238 * @return {[type]} [description] 2239 */ 2240 2241 function mouseout(me, e) { 2242 var newArea, 2243 ar = me.getDataForArea(this), 2244 opts = me.options; 2245 2246 if (me.currentAreaId < 0 || !ar) { 2247 return; 2248 } 2249 2250 newArea = me.getDataForArea(e.relatedTarget); 2251 2252 if (newArea === ar) { 2253 return; 2254 } 2255 2256 me.currentAreaId = -1; 2257 ar.area = null; 2258 2259 queueMouseEvent(me, opts.mouseoutDelay, ar).then(function (result) { 2260 if (!result.completeAction) { 2261 return; 2262 } 2263 me.clearEffects(); 2264 }); 2265 2266 if (u.isFunction(opts.onMouseout)) { 2267 opts.onMouseout.call(this, { 2268 e: e, 2269 options: opts, 2270 key: ar.key, 2271 selected: ar.isSelected() 2272 }); 2273 } 2274 } 2275 2276 /** 2277 * Clear any active tooltip or highlight 2278 * 2279 * @param {MapData} me The MapData context 2280 * @param {EventData} e jQuery event data 2281 * @return {[type]} [description] 2282 */ 2283 2284 function clearEffects(me) { 2285 var opts = me.options; 2286 2287 me.ensureNoHighlight(); 2288 2289 if ( 2290 opts.toolTipClose && 2291 $.inArray('area-mouseout', opts.toolTipClose) >= 0 && 2292 me.activeToolTip 2293 ) { 2294 me.clearToolTip(); 2295 } 2296 } 2297 2298 /** 2299 * Mouse click event handler 2300 * 2301 * @param {MapData} me The MapData context 2302 * @param {EventData} e jQuery event data 2303 * @return {[type]} [description] 2304 */ 2305 2306 function click(me, e) { 2307 var list, 2308 list_target, 2309 newSelectionState, 2310 canChangeState, 2311 cbResult, 2312 that = this, 2313 ar = me.getDataForArea(this), 2314 opts = me.options, 2315 navDetails, 2316 areaOpts; 2317 2318 function navigateTo(mode, href, target) { 2319 switch (mode) { 2320 // if no target is specified, use legacy 2321 // behavior and change current window 2322 case 'open': 2323 window.open(href, target || '_self'); 2324 return; 2325 2326 // default legacy behavior of ImageMapster 2327 default: 2328 window.location.href = href; 2329 return; 2330 } 2331 } 2332 2333 function getNavDetails(ar, mode, defaultHref) { 2334 if (mode === 'open') { 2335 var elHref = $(ar.area).attr('href'), 2336 useEl = shouldNavigateTo(elHref); 2337 2338 return { 2339 href: useEl ? elHref : ar.href, 2340 target: useEl ? $(ar.area).attr('target') : ar.hrefTarget 2341 }; 2342 } 2343 2344 return { 2345 href: defaultHref 2346 }; 2347 } 2348 2349 function clickArea(ar) { 2350 var target; 2351 canChangeState = 2352 ar.isSelectable() && (ar.isDeselectable() || !ar.isSelected()); 2353 2354 if (canChangeState) { 2355 newSelectionState = !ar.isSelected(); 2356 } else { 2357 newSelectionState = ar.isSelected(); 2358 } 2359 2360 list_target = m.getBoundList(opts, ar.key); 2361 2362 if (u.isFunction(opts.onClick)) { 2363 cbResult = opts.onClick.call(that, { 2364 e: e, 2365 listTarget: list_target, 2366 key: ar.key, 2367 selected: newSelectionState 2368 }); 2369 2370 if (u.isBool(cbResult)) { 2371 if (!cbResult) { 2372 return false; 2373 } 2374 target = getNavDetails( 2375 ar, 2376 opts.navigateMode, 2377 $(ar.area).attr('href') 2378 ); 2379 if (shouldNavigateTo(target.href)) { 2380 navigateTo(opts.navigateMode, target.href, target.target); 2381 return false; 2382 } 2383 } 2384 } 2385 2386 if (canChangeState) { 2387 ar.toggle(); 2388 } 2389 } 2390 2391 mousedown.call(this, e); 2392 2393 navDetails = getNavDetails(ar, opts.navigateMode, ar.href); 2394 if (opts.clickNavigate && shouldNavigateTo(navDetails.href)) { 2395 navigateTo(opts.navigateMode, navDetails.href, navDetails.target); 2396 return; 2397 } 2398 2399 if (ar && !ar.owner.currentAction) { 2400 opts = me.options; 2401 clickArea(ar); 2402 areaOpts = ar.effectiveOptions(); 2403 if (areaOpts.includeKeys) { 2404 list = u.split(areaOpts.includeKeys); 2405 $.each(list, function (_, e) { 2406 var ar = me.getDataForKey(e.toString()); 2407 if (!ar.options.isMask) { 2408 clickArea(ar); 2409 } 2410 }); 2411 } 2412 } 2413 } 2414 2415 /** 2416 * Prototype for a MapData object, representing an ImageMapster bound object 2417 * @param {Element} image an IMG element 2418 * @param {object} options ImageMapster binding options 2419 */ 2420 m.MapData = function (image, options) { 2421 var me = this; 2422 2423 // (Image) main map image 2424 2425 me.image = image; 2426 2427 me.images = new m.MapImages(me); 2428 me.graphics = new m.Graphics(me); 2429 2430 // save the initial style of the image for unbinding. This is problematic, chrome 2431 // duplicates styles when assigning, and cssText is apparently not universally supported. 2432 // Need to do something more robust to make unbinding work universally. 2433 2434 me.imgCssText = image.style.cssText || null; 2435 2436 initializeDefaults(me); 2437 2438 me.configureOptions(options); 2439 2440 // create context-bound event handlers from our private functions 2441 2442 me.mouseover = function (e) { 2443 mouseover.call(this, me, e); 2444 }; 2445 me.mouseout = function (e) { 2446 mouseout.call(this, me, e); 2447 }; 2448 me.click = function (e) { 2449 click.call(this, me, e); 2450 }; 2451 me.clearEffects = function (e) { 2452 clearEffects.call(this, me, e); 2453 }; 2454 me.mousedown = function (e) { 2455 mousedown.call(this, e); 2456 }; 2457 }; 2458 2459 m.MapData.prototype = { 2460 constructor: m.MapData, 2461 2462 /** 2463 * Set target.options from defaults + options 2464 * @param {[type]} target The target 2465 * @param {[type]} options The options to merge 2466 */ 2467 2468 configureOptions: function (options) { 2469 this.options = u.updateProps({}, m.defaults, options); 2470 }, 2471 2472 /** 2473 * Ensure all images are loaded 2474 * @return {Promise} A promise that resolves when the images have finished loading (or fail) 2475 */ 2476 2477 bindImages: function () { 2478 var me = this, 2479 mi = me.images; 2480 2481 // reset the images if this is a rebind 2482 2483 if (mi.length > 2) { 2484 mi.splice(2); 2485 } else if (mi.length === 0) { 2486 // add the actual main image 2487 mi.add(me.image); 2488 // will create a duplicate of the main image, we need this to get raw size info 2489 mi.add(me.image.src); 2490 } 2491 2492 configureAltImages(me); 2493 2494 return me.images.bind(); 2495 }, 2496 2497 /** 2498 * Test whether an async action is currently in progress 2499 * @return {Boolean} true or false indicating state 2500 */ 2501 2502 isActive: function () { 2503 return !this.complete || this.currentAction; 2504 }, 2505 2506 /** 2507 * Return an object indicating the various states. This isn't really used by 2508 * production code. 2509 * 2510 * @return {object} An object with properties for various states 2511 */ 2512 2513 state: function () { 2514 return { 2515 complete: this.complete, 2516 resizing: this.currentAction === 'resizing', 2517 zoomed: this.zoomed, 2518 zoomedArea: this.zoomedArea, 2519 scaleInfo: this.scaleInfo 2520 }; 2521 }, 2522 2523 /** 2524 * Get a unique ID for the wrapper of this imagemapster 2525 * @return {string} A string that is unique to this image 2526 */ 2527 2528 wrapId: function () { 2529 return 'mapster_wrap_' + this.index; 2530 }, 2531 instanceEventNamespace: function () { 2532 return '.mapster.' + this.wrapId(); 2533 }, 2534 _idFromKey: function (key) { 2535 return typeof key === 'string' && 2536 Object.prototype.hasOwnProperty.call(this._xref, key) 2537 ? this._xref[key] 2538 : -1; 2539 }, 2540 2541 /** 2542 * Return a comma-separated string of all selected keys 2543 * @return {string} CSV of all keys that are currently selected 2544 */ 2545 2546 getSelected: function () { 2547 var result = ''; 2548 $.each(this.data, function (_, e) { 2549 if (e.isSelected()) { 2550 result += (result ? ',' : '') + this.key; 2551 } 2552 }); 2553 return result; 2554 }, 2555 2556 /** 2557 * Get an array of MapAreas associated with a specific AREA based on the keys for that area 2558 * @param {Element} area An HTML AREA 2559 * @param {number} atMost A number limiting the number of areas to be returned (typically 1 or 0 for no limit) 2560 * @return {MapArea[]} Array of MapArea objects 2561 */ 2562 2563 getAllDataForArea: function (area, atMost) { 2564 var i, 2565 ar, 2566 result, 2567 me = this, 2568 key = $(area).filter('area').attr(me.options.mapKey); 2569 2570 if (key) { 2571 result = []; 2572 key = u.split(key); 2573 2574 for (i = 0; i < (atMost || key.length); i++) { 2575 ar = me.data[me._idFromKey(key[i])]; 2576 if (ar) { 2577 ar.area = area.length ? area[0] : area; 2578 // set the actual area moused over/selected 2579 // TODO: this is a brittle model for capturing which specific area - if this method was not used, 2580 // ar.area could have old data. fix this. 2581 result.push(ar); 2582 } 2583 } 2584 } 2585 2586 return result; 2587 }, 2588 getDataForArea: function (area) { 2589 var ar = this.getAllDataForArea(area, 1); 2590 return ar ? ar[0] || null : null; 2591 }, 2592 getDataForKey: function (key) { 2593 return this.data[this._idFromKey(key)]; 2594 }, 2595 2596 /** 2597 * Get the primary keys associated with an area group. 2598 * If this is a primary key, it will be returned. 2599 * 2600 * @param {string key An area key 2601 * @return {string} A CSV of area keys 2602 */ 2603 2604 getKeysForGroup: function (key) { 2605 var ar = this.getDataForKey(key); 2606 2607 return !ar 2608 ? '' 2609 : ar.isPrimary 2610 ? ar.key 2611 : this.getPrimaryKeysForMapAreas(ar.areas()).join(','); 2612 }, 2613 2614 /** 2615 * given an array of MapArea object, return an array of its unique primary keys 2616 * @param {MapArea[]} areas The areas to analyze 2617 * @return {string[]} An array of unique primary keys 2618 */ 2619 2620 getPrimaryKeysForMapAreas: function (areas) { 2621 var keys = []; 2622 $.each(areas, function (_, e) { 2623 if ($.inArray(e.keys[0], keys) < 0) { 2624 keys.push(e.keys[0]); 2625 } 2626 }); 2627 return keys; 2628 }, 2629 getData: function (obj) { 2630 if (typeof obj === 'string') { 2631 return this.getDataForKey(obj); 2632 } else if ((obj && obj.mapster) || u.isElement(obj)) { 2633 return this.getDataForArea(obj); 2634 } else { 2635 return null; 2636 } 2637 }, 2638 // remove highlight if present, raise event 2639 ensureNoHighlight: function () { 2640 var ar; 2641 if (this.highlightId >= 0) { 2642 this.graphics.clearHighlight(); 2643 ar = this.data[this.highlightId]; 2644 ar.changeState('highlight', false); 2645 this.setHighlightId(-1); 2646 } 2647 }, 2648 setHighlightId: function (id) { 2649 this.highlightId = id; 2650 }, 2651 2652 /** 2653 * Clear all active selections on this map 2654 */ 2655 2656 clearSelections: function () { 2657 $.each(this.data, function (_, e) { 2658 if (e.selected) { 2659 e.deselect(true); 2660 } 2661 }); 2662 this.removeSelectionFinish(); 2663 }, 2664 2665 /** 2666 * Set area options from an array of option data. 2667 * 2668 * @param {object[]} areas An array of objects containing area-specific options 2669 */ 2670 2671 setAreaOptions: function (areas) { 2672 var i, area_options, ar; 2673 areas = areas || []; 2674 2675 // refer by: map_data.options[map_data.data[x].area_option_id] 2676 2677 for (i = areas.length - 1; i >= 0; i--) { 2678 area_options = areas[i]; 2679 if (area_options) { 2680 ar = this.getDataForKey(area_options.key); 2681 if (ar) { 2682 u.updateProps(ar.options, area_options); 2683 2684 // TODO: will not deselect areas that were previously selected, so this only works 2685 // for an initial bind. 2686 2687 if (u.isBool(area_options.selected)) { 2688 ar.selected = area_options.selected; 2689 } 2690 } 2691 } 2692 } 2693 }, 2694 // keys: a comma-separated list 2695 drawSelections: function (keys) { 2696 var i, 2697 key_arr = u.asArray(keys); 2698 2699 for (i = key_arr.length - 1; i >= 0; i--) { 2700 this.data[key_arr[i]].drawSelection(); 2701 } 2702 }, 2703 redrawSelections: function () { 2704 $.each(this.data, function (_, e) { 2705 if (e.isSelectedOrStatic()) { 2706 e.drawSelection(); 2707 } 2708 }); 2709 }, 2710 // Causes changes to the bound list based on the user action (select or deselect) 2711 // area: the jQuery area object 2712 // returns the matching elements from the bound list for the first area passed 2713 // (normally only one should be passed, but a list can be passed) 2714 setBoundListProperties: function (opts, target, selected) { 2715 target.each(function (_, e) { 2716 if (opts.listSelectedClass) { 2717 if (selected) { 2718 $(e).addClass(opts.listSelectedClass); 2719 } else { 2720 $(e).removeClass(opts.listSelectedClass); 2721 } 2722 } 2723 if (opts.listSelectedAttribute) { 2724 $(e).prop(opts.listSelectedAttribute, selected); 2725 } 2726 }); 2727 }, 2728 clearBoundListProperties: function (opts) { 2729 var me = this; 2730 if (!opts.boundList) { 2731 return; 2732 } 2733 me.setBoundListProperties(opts, opts.boundList, false); 2734 }, 2735 refreshBoundList: function (opts) { 2736 var me = this; 2737 me.clearBoundListProperties(opts); 2738 me.setBoundListProperties( 2739 opts, 2740 m.getBoundList(opts, me.getSelected()), 2741 true 2742 ); 2743 }, 2744 setBoundList: function (opts) { 2745 var me = this, 2746 sorted_list = me.data.slice(0), 2747 sort_func; 2748 if (opts.sortList) { 2749 if (opts.sortList === 'desc') { 2750 sort_func = function (a, b) { 2751 return a === b ? 0 : a > b ? -1 : 1; 2752 }; 2753 } else { 2754 sort_func = function (a, b) { 2755 return a === b ? 0 : a < b ? -1 : 1; 2756 }; 2757 } 2758 2759 sorted_list.sort(function (a, b) { 2760 a = a.value; 2761 b = b.value; 2762 return sort_func(a, b); 2763 }); 2764 } 2765 me.options.boundList = opts.onGetList.call(me.image, sorted_list); 2766 }, 2767 ///called when images are done loading 2768 initialize: function () { 2769 var imgCopy, 2770 base_canvas, 2771 overlay_canvas, 2772 wrap, 2773 parentId, 2774 css, 2775 i, 2776 size, 2777 img, 2778 scale, 2779 me = this, 2780 opts = me.options; 2781 2782 if (me.complete) { 2783 return; 2784 } 2785 2786 img = $(me.image); 2787 2788 parentId = img.parent().attr('id'); 2789 2790 // create a div wrapper only if there's not already a wrapper, otherwise, own it 2791 2792 if ( 2793 parentId && 2794 parentId.length >= 12 && 2795 parentId.substring(0, 12) === 'mapster_wrap' 2796 ) { 2797 wrap = img.parent(); 2798 wrap.attr('id', me.wrapId()); 2799 } else { 2800 wrap = $('<div id="' + me.wrapId() + '"></div>'); 2801 2802 if (opts.wrapClass) { 2803 if (opts.wrapClass === true) { 2804 wrap.addClass(img[0].className); 2805 } else { 2806 wrap.addClass(opts.wrapClass); 2807 } 2808 } 2809 } 2810 me.wrapper = wrap; 2811 2812 // me.images[1] is the copy of the original image. It should be loaded & at its native size now so we can obtain the true 2813 // width & height. This is needed to scale the imagemap if not being shown at its native size. It is also needed purely 2814 // to finish binding in case the original image was not visible. It can be impossible in some browsers to obtain the 2815 // native size of a hidden image. 2816 2817 me.scaleInfo = scale = u.scaleMap( 2818 me.images[0], 2819 me.images[1], 2820 opts.scaleMap 2821 ); 2822 2823 me.base_canvas = base_canvas = me.graphics.createVisibleCanvas(me); 2824 me.overlay_canvas = overlay_canvas = me.graphics.createVisibleCanvas(me); 2825 2826 // Now we got what we needed from the copy -clone from the original image again to make sure any other attributes are copied 2827 imgCopy = $(me.images[1]) 2828 .addClass('mapster_el ' + me.images[0].className) 2829 .attr({ id: null, usemap: null }); 2830 2831 size = u.size(me.images[0]); 2832 2833 if (size.complete) { 2834 imgCopy.css({ 2835 width: size.width, 2836 height: size.height 2837 }); 2838 } 2839 2840 me.buildDataset(); 2841 2842 // now that we have processed all the areas, set css for wrapper, scale map if needed 2843 2844 css = $.extend( 2845 { 2846 display: 'block', 2847 position: 'relative', 2848 padding: 0 2849 }, 2850 opts.enableAutoResizeSupport === true 2851 ? {} 2852 : { 2853 width: scale.width, 2854 height: scale.height 2855 } 2856 ); 2857 2858 if (opts.wrapCss) { 2859 $.extend(css, opts.wrapCss); 2860 } 2861 // if we were rebinding with an existing wrapper, the image will aready be in it 2862 if (img.parent()[0] !== me.wrapper[0]) { 2863 img.before(me.wrapper); 2864 } 2865 2866 wrap.css(css); 2867 2868 // move all generated images into the wrapper for easy removal later 2869 2870 $(me.images.slice(2)).hide(); 2871 for (i = 1; i < me.images.length; i++) { 2872 wrap.append(me.images[i]); 2873 } 2874 2875 //me.images[1].style.cssText = me.image.style.cssText; 2876 2877 wrap 2878 .append(base_canvas) 2879 .append(overlay_canvas) 2880 .append(img.css(m.canvas_style)); 2881 2882 // images[0] is the original image with map, images[1] is the copy/background that is visible 2883 2884 u.setOpacity(me.images[0], 0); 2885 $(me.images[1]).show(); 2886 2887 u.setOpacity(me.images[1], 1); 2888 2889 me.complete = true; 2890 me.processCommandQueue(); 2891 2892 if (opts.enableAutoResizeSupport === true) { 2893 me.configureAutoResize(); 2894 } 2895 2896 me.onConfigured(); 2897 }, 2898 2899 onConfigured: function () { 2900 var me = this, 2901 $img = $(me.image), 2902 opts = me.options; 2903 2904 if (opts.onConfigured && typeof opts.onConfigured === 'function') { 2905 opts.onConfigured.call($img, true); 2906 } 2907 }, 2908 2909 // when rebind is true, the MapArea data will not be rebuilt. 2910 buildDataset: function (rebind) { 2911 var sel, 2912 areas, 2913 j, 2914 area_id, 2915 $area, 2916 area, 2917 curKey, 2918 mapArea, 2919 key, 2920 keys, 2921 mapAreaId, 2922 group_value, 2923 dataItem, 2924 href, 2925 me = this, 2926 opts = me.options, 2927 default_group; 2928 2929 function addAreaData(key, value) { 2930 var dataItem = new m.AreaData(me, key, value); 2931 dataItem.areaId = me._xref[key] = me.data.push(dataItem) - 1; 2932 return dataItem.areaId; 2933 } 2934 2935 me._xref = {}; 2936 me.data = []; 2937 if (!rebind) { 2938 me.mapAreas = []; 2939 } 2940 2941 default_group = !opts.mapKey; 2942 if (default_group) { 2943 opts.mapKey = 'data-mapster-key'; 2944 } 2945 2946 // the [attribute] selector is broken on old IE with jQuery. hasVml() is a quick and dirty 2947 // way to test for that 2948 2949 sel = m.hasVml() 2950 ? 'area' 2951 : default_group 2952 ? 'area[coords]' 2953 : 'area[' + opts.mapKey + ']'; 2954 2955 areas = $(me.map).find(sel).off('.mapster'); 2956 2957 for (mapAreaId = 0; mapAreaId < areas.length; mapAreaId++) { 2958 area_id = 0; 2959 area = areas[mapAreaId]; 2960 $area = $(area); 2961 2962 // skip areas with no coords - selector broken for older ie 2963 if (!area.coords) { 2964 continue; 2965 } 2966 // Create a key if none was assigned by the user 2967 2968 if (default_group) { 2969 curKey = String(mapAreaId); 2970 $area.attr('data-mapster-key', curKey); 2971 } else { 2972 curKey = area.getAttribute(opts.mapKey); 2973 } 2974 2975 // conditions for which the area will be bound to mouse events 2976 // only bind to areas that don't have nohref. ie 6&7 cannot detect the presence of nohref, so we have to also not bind if href is missing. 2977 2978 if (rebind) { 2979 mapArea = me.mapAreas[$area.data('mapster') - 1]; 2980 mapArea.configure(curKey); 2981 mapArea.areaDataXref = []; 2982 } else { 2983 mapArea = new m.MapArea(me, area, curKey); 2984 me.mapAreas.push(mapArea); 2985 } 2986 2987 keys = mapArea.keys; // converted to an array by mapArea 2988 2989 // Iterate through each mapKey assigned to this area 2990 for (j = keys.length - 1; j >= 0; j--) { 2991 key = keys[j]; 2992 2993 if (opts.mapValue) { 2994 group_value = $area.attr(opts.mapValue); 2995 } 2996 if (default_group) { 2997 // set an attribute so we can refer to the area by index from the DOM object if no key 2998 area_id = addAreaData(me.data.length, group_value); 2999 dataItem = me.data[area_id]; 3000 dataItem.key = key = area_id.toString(); 3001 } else { 3002 area_id = me._xref[key]; 3003 if (area_id >= 0) { 3004 dataItem = me.data[area_id]; 3005 if (group_value && !me.data[area_id].value) { 3006 dataItem.value = group_value; 3007 } 3008 } else { 3009 area_id = addAreaData(key, group_value); 3010 dataItem = me.data[area_id]; 3011 dataItem.isPrimary = j === 0; 3012 } 3013 } 3014 mapArea.areaDataXref.push(area_id); 3015 dataItem.areasXref.push(mapAreaId); 3016 } 3017 3018 href = $area.attr('href'); 3019 if (shouldNavigateTo(href) && !dataItem.href) { 3020 dataItem.href = href; 3021 dataItem.hrefTarget = $area.attr('target'); 3022 } 3023 3024 if (!mapArea.nohref) { 3025 $area 3026 .on('click.mapster', me.click) 3027 .on( 3028 'mouseover.mapster touchstart.mapster.noPreventDefault', 3029 me.mouseover 3030 ) 3031 .on( 3032 'mouseout.mapster touchend.mapster.noPreventDefault', 3033 me.mouseout 3034 ) 3035 .on('mousedown.mapster', me.mousedown); 3036 } 3037 3038 // store an ID with each area. 3039 $area.data('mapster', mapAreaId + 1); 3040 } 3041 3042 // TODO listenToList 3043 // if (opts.listenToList && opts.nitG) { 3044 // opts.nitG.bind('click.mapster', event_hooks[map_data.hooks_index].listclick_hook); 3045 // } 3046 3047 // populate areas from config options 3048 me.setAreaOptions(opts.areas); 3049 if (opts.onGetList) { 3050 me.setBoundList(opts); 3051 } 3052 3053 if (opts.boundList && opts.boundList.length > 0) { 3054 me.refreshBoundList(opts); 3055 } 3056 3057 if (rebind) { 3058 me.graphics.removeSelections(); 3059 me.graphics.refreshSelections(); 3060 } else { 3061 me.redrawSelections(); 3062 } 3063 }, 3064 processCommandQueue: function () { 3065 var cur, 3066 me = this; 3067 while (!me.currentAction && me.commands.length) { 3068 cur = me.commands[0]; 3069 me.commands.splice(0, 1); 3070 m.impl[cur.command].apply(cur.that, cur.args); 3071 } 3072 }, 3073 clearEvents: function () { 3074 $(this.map).find('area').off('.mapster'); 3075 $(this.images).off('.mapster'); 3076 $(window).off(this.instanceEventNamespace()); 3077 $(window.document).off(this.instanceEventNamespace()); 3078 }, 3079 _clearCanvases: function (preserveState) { 3080 // remove the canvas elements created 3081 if (!preserveState) { 3082 $(this.base_canvas).remove(); 3083 } 3084 $(this.overlay_canvas).remove(); 3085 }, 3086 clearMapData: function (preserveState) { 3087 var me = this; 3088 this._clearCanvases(preserveState); 3089 3090 // release refs to DOM elements 3091 $.each(this.data, function (_, e) { 3092 e.reset(); 3093 }); 3094 this.data = null; 3095 if (!preserveState) { 3096 // get rid of everything except the original image 3097 this.image.style.cssText = this.imgCssText; 3098 $(this.wrapper).before(this.image).remove(); 3099 } 3100 3101 me.images.clear(); 3102 3103 if (me.autoResizeTimer) { 3104 clearTimeout(me.autoResizeTimer); 3105 } 3106 me.autoResizeTimer = null; 3107 this.image = null; 3108 u.ifFunction(this.clearToolTip, this); 3109 }, 3110 3111 // Compelete cleanup process for deslecting items. Called after a batch operation, or by AreaData for single 3112 // operations not flagged as "partial" 3113 3114 removeSelectionFinish: function () { 3115 var g = this.graphics; 3116 3117 g.refreshSelections(); 3118 // do not call ensure_no_highlight- we don't really want to unhilight it, just remove the effect 3119 g.clearHighlight(); 3120 } 3121 }; 3122})(jQuery); 3123 3124/* areadata.js 3125 AreaData and MapArea protoypes 3126*/ 3127 3128(function ($) { 3129 'use strict'; 3130 3131 var m = $.mapster, 3132 u = m.utils; 3133 3134 function optsAreEqual(opts1, opts2) { 3135 // deep compare is not trivial and current testing framework 3136 // doesn't provide a way to detect this accurately so only 3137 // implementing basic compare at this time. 3138 // TODO: Implement deep obj compare or for perf reasons shallow 3139 // with a short-circuit if deep is required for full compare 3140 // since config options should only require shallow 3141 return opts1 === opts2; 3142 } 3143 3144 /** 3145 * Update selected state of this area 3146 * 3147 * @param {boolean} selected Determines whether areas are selected or deselected 3148 */ 3149 function updateSelected(selected) { 3150 var me = this, 3151 prevSelected = me.selected; 3152 3153 me.selected = selected; 3154 me.staticStateOverridden = u.isBool(me.effectiveOptions().staticState) 3155 ? true 3156 : false; 3157 3158 return prevSelected !== selected; 3159 } 3160 3161 /** 3162 * Select this area 3163 * 3164 * @param {AreaData} me AreaData context 3165 * @param {object} options Options for rendering the selection 3166 */ 3167 function select(options) { 3168 function buildOptions() { 3169 // map the altImageId if an altimage was passed 3170 return $.extend(me.effectiveRenderOptions('select'), options, { 3171 altImageId: o.images.add(options.altImage) 3172 }); 3173 } 3174 3175 var me = this, 3176 o = me.owner, 3177 hasOptions = !$.isEmptyObject(options), 3178 newOptsCache = hasOptions ? buildOptions() : null, 3179 // Per docs, options changed via set_options for an area that is 3180 // already selected will not be reflected until the next time 3181 // the area becomes selected. 3182 changeOptions = hasOptions 3183 ? !optsAreEqual(me.optsCache, newOptsCache) 3184 : false, 3185 selectedHasChanged = false, 3186 isDrawn = me.isSelectedOrStatic(); 3187 3188 // This won't clear staticState === true areas that have not been overridden via API set/select/deselect. 3189 // This could be optimized to only clear if we are the only one selected. However, there are scenarios 3190 // that do not respect singleSelect (e.g. initialization) so we force clear if there should only be one. 3191 // TODO: Only clear if we aren't the only one selected (depends on #370) 3192 if (o.options.singleSelect) { 3193 o.clearSelections(); 3194 // we may (staticState === true) or may not still be visible 3195 isDrawn = me.isSelectedOrStatic(); 3196 } 3197 3198 if (changeOptions) { 3199 me.optsCache = newOptsCache; 3200 } 3201 3202 // Update before we start drawing for methods 3203 // that rely on internal selected value. 3204 // Force update because area can be selected 3205 // at multiple levels (selected / area_options.selected / staticState / etc.) 3206 // and could have been cleared. 3207 selectedHasChanged = me.updateSelected(true); 3208 3209 if (isDrawn && changeOptions) { 3210 // no way to remove just this area from canvas so must refresh everything 3211 3212 // explicitly remove vml element since it uses removeSelections instead of refreshSelections 3213 // TODO: Not sure why removeSelections isn't incorporated in to refreshSelections 3214 // need to investigate and possibly consolidate 3215 o.graphics.removeSelections(me.areaId); 3216 o.graphics.refreshSelections(); 3217 } else if (!isDrawn) { 3218 me.drawSelection(); 3219 } 3220 3221 // don't fire until everything is done 3222 if (selectedHasChanged) { 3223 me.changeState('select', true); 3224 } 3225 } 3226 3227 /** 3228 * Deselect this area, optionally deferring finalization so additional areas can be deselected 3229 * in a single operation 3230 * 3231 * @param {boolean} partial when true, the caller must invoke "finishRemoveSelection" to render 3232 */ 3233 3234 function deselect(partial) { 3235 var me = this, 3236 selectedHasChanged = false; 3237 3238 // update before we start drawing for methods 3239 // that rely on internal selected value 3240 // force update because area can be selected 3241 // at multiple levels (selected / area_options.selected / staticState / etc.) 3242 selectedHasChanged = me.updateSelected(false); 3243 3244 // release information about last area options when deselecting. 3245 me.optsCache = null; 3246 me.owner.graphics.removeSelections(me.areaId); 3247 3248 // Complete selection removal process. This is separated because it's very inefficient to perform the whole 3249 // process for multiple removals, as the canvas must be totally redrawn at the end of the process.ar.remove 3250 if (!partial) { 3251 me.owner.removeSelectionFinish(); 3252 } 3253 3254 // don't fire until everything is done 3255 if (selectedHasChanged) { 3256 me.changeState('select', false); 3257 } 3258 } 3259 3260 /** 3261 * Toggle the selection state of this area 3262 * @param {object} options Rendering options, if toggling on 3263 * @return {bool} The new selection state 3264 */ 3265 function toggle(options) { 3266 var me = this; 3267 if (!me.isSelected()) { 3268 me.select(options); 3269 } else { 3270 me.deselect(); 3271 } 3272 return me.isSelected(); 3273 } 3274 3275 function isNoHref(areaEl) { 3276 var $area = $(areaEl); 3277 return u.hasAttribute($area, 'nohref') || !u.hasAttribute($area, 'href'); 3278 } 3279 3280 /** 3281 * An AreaData object; represents a conceptual area that can be composed of 3282 * one or more MapArea objects 3283 * 3284 * @param {MapData} owner The MapData object to which this belongs 3285 * @param {string} key The key for this area 3286 * @param {string} value The mapValue string for this area 3287 */ 3288 3289 m.AreaData = function (owner, key, value) { 3290 $.extend(this, { 3291 owner: owner, 3292 key: key || '', 3293 // means this represents the first key in a list of keys (it's the area group that gets highlighted on mouseover) 3294 isPrimary: true, 3295 areaId: -1, 3296 href: '', 3297 hrefTarget: null, 3298 value: value || '', 3299 options: {}, 3300 // "null" means unchanged. Use "isSelected" method to just test true/false 3301 selected: null, 3302 // "true" means selected has been set via API AND staticState is true/false 3303 staticStateOverridden: false, 3304 // xref to MapArea objects 3305 areasXref: [], 3306 // (temporary storage) - the actual area moused over 3307 area: null, 3308 // the last options used to render this. Cache so when re-drawing after a remove, changes in options won't 3309 // break already selected things. 3310 optsCache: null 3311 }); 3312 }; 3313 3314 /** 3315 * The public API for AreaData object 3316 */ 3317 3318 m.AreaData.prototype = { 3319 constuctor: m.AreaData, 3320 select: select, 3321 deselect: deselect, 3322 toggle: toggle, 3323 updateSelected: updateSelected, 3324 areas: function () { 3325 var i, 3326 result = []; 3327 for (i = 0; i < this.areasXref.length; i++) { 3328 result.push(this.owner.mapAreas[this.areasXref[i]]); 3329 } 3330 return result; 3331 }, 3332 // return all coordinates for all areas 3333 coords: function (offset) { 3334 var coords = []; 3335 $.each(this.areas(), function (_, el) { 3336 coords = coords.concat(el.coords(offset)); 3337 }); 3338 return coords; 3339 }, 3340 reset: function () { 3341 $.each(this.areas(), function (_, e) { 3342 e.reset(); 3343 }); 3344 this.areasXref = []; 3345 this.options = null; 3346 }, 3347 // Return the effective selected state of an area, incorporating staticState 3348 isSelectedOrStatic: function () { 3349 var o = this.effectiveOptions(); 3350 return !u.isBool(o.staticState) || this.staticStateOverridden 3351 ? this.isSelected() 3352 : o.staticState; 3353 }, 3354 isSelected: function () { 3355 return u.isBool(this.selected) 3356 ? this.selected 3357 : u.isBool(this.owner.area_options.selected) 3358 ? this.owner.area_options.selected 3359 : false; 3360 }, 3361 isSelectable: function () { 3362 return u.isBool(this.effectiveOptions().staticState) 3363 ? false 3364 : u.isBool(this.owner.options.staticState) 3365 ? false 3366 : u.boolOrDefault(this.effectiveOptions().isSelectable, true); 3367 }, 3368 isDeselectable: function () { 3369 return u.isBool(this.effectiveOptions().staticState) 3370 ? false 3371 : u.isBool(this.owner.options.staticState) 3372 ? false 3373 : u.boolOrDefault(this.effectiveOptions().isDeselectable, true); 3374 }, 3375 isNotRendered: function () { 3376 return isNoHref(this.area) || this.effectiveOptions().isMask; 3377 }, 3378 /** 3379 * Return the overall options effective for this area. 3380 * This should get the default options, and merge in area-specific options, finally 3381 * overlaying options passed by parameter 3382 * 3383 * @param {[type]} options options which will supercede all other options for this area 3384 * @return {[type]} the combined options 3385 */ 3386 3387 effectiveOptions: function (options) { 3388 var opts = u.updateProps( 3389 {}, 3390 this.owner.area_options, 3391 this.options, 3392 options || {}, 3393 { 3394 id: this.areaId 3395 } 3396 ); 3397 3398 opts.selected = this.isSelected(); 3399 3400 return opts; 3401 }, 3402 3403 /** 3404 * Return the options effective for this area for a "render" or "highlight" mode. 3405 * This should get the default options, merge in the areas-specific options, 3406 * and then the mode-specific options. 3407 * @param {string} mode 'render' or 'highlight' 3408 * @param {[type]} options options which will supercede all other options for this area 3409 * @return {[type]} the combined options 3410 */ 3411 3412 effectiveRenderOptions: function (mode, options) { 3413 var allOpts, 3414 opts = this.optsCache; 3415 3416 if (!opts || mode === 'highlight') { 3417 allOpts = this.effectiveOptions(options); 3418 opts = u.updateProps({}, allOpts, allOpts['render_' + mode]); 3419 3420 if (mode !== 'highlight') { 3421 this.optsCache = opts; 3422 } 3423 } 3424 return $.extend({}, opts); 3425 }, 3426 3427 // Fire callback on area state change 3428 changeState: function (state_type, state) { 3429 if (u.isFunction(this.owner.options.onStateChange)) { 3430 this.owner.options.onStateChange.call(this.owner.image, { 3431 key: this.key, 3432 state: state_type, 3433 selected: state 3434 }); 3435 } 3436 if (state_type === 'select' && this.owner.options.boundList) { 3437 this.owner.setBoundListProperties( 3438 this.owner.options, 3439 m.getBoundList(this.owner.options, this.key), 3440 state 3441 ); 3442 } 3443 }, 3444 3445 // highlight this area 3446 3447 highlight: function (options) { 3448 var o = this.owner; 3449 o.ensureNoHighlight(); 3450 if (this.effectiveOptions().highlight) { 3451 o.graphics.addShapeGroup(this, 'highlight', options); 3452 } 3453 o.setHighlightId(this.areaId); 3454 this.changeState('highlight', true); 3455 }, 3456 3457 // select this area. if "callEvent" is true then the state change event will be called. (This method can be used 3458 // during config operations, in which case no event is indicated) 3459 3460 drawSelection: function () { 3461 this.owner.graphics.addShapeGroup(this, 'select'); 3462 } 3463 }; 3464 // represents an HTML area 3465 m.MapArea = function (owner, areaEl, keys) { 3466 if (!owner) { 3467 return; 3468 } 3469 var me = this; 3470 me.owner = owner; // a MapData object 3471 me.area = areaEl; 3472 me.areaDataXref = []; // a list of map_data.data[] id's for each areaData object containing this 3473 me.originalCoords = []; 3474 $.each(u.split(areaEl.coords), function (_, el) { 3475 me.originalCoords.push(parseFloat(el)); 3476 }); 3477 me.length = me.originalCoords.length; 3478 me.shape = u.getShape(areaEl); 3479 me.nohref = isNoHref(areaEl); 3480 me.configure(keys); 3481 }; 3482 m.MapArea.prototype = { 3483 constructor: m.MapArea, 3484 configure: function (keys) { 3485 this.keys = u.split(keys); 3486 }, 3487 reset: function () { 3488 this.area = null; 3489 }, 3490 coords: function (offset) { 3491 return $.map(this.originalCoords, function (e) { 3492 return offset ? e : e + offset; 3493 }); 3494 } 3495 }; 3496})(jQuery); 3497 3498/* areacorners.js 3499 determine the best place to put a box of dimensions (width,height) given a circle, rect or poly 3500*/ 3501 3502(function ($) { 3503 'use strict'; 3504 3505 var u = $.mapster.utils; 3506 3507 /** 3508 * Compute positions that will place a target with dimensions [width,height] outside 3509 * but near the boundaries of the elements "elements". When an imagemap is passed, the 3510 * 3511 * @param {Element|Element[]} elements An element or an array of elements (such as a jQuery object) 3512 * @param {Element} image The image to which area elements are bound, if this is an image map. 3513 * @param {Element} container The contianer in which the target must be constrained (or document, if missing) 3514 * @param {int} width The width of the target object 3515 * @return {object} a structure with the x and y positions 3516 */ 3517 u.areaCorners = function (elements, image, container, width, height) { 3518 var pos, 3519 found, 3520 minX, 3521 minY, 3522 maxX, 3523 maxY, 3524 bestMinX, 3525 bestMaxX, 3526 bestMinY, 3527 bestMaxY, 3528 curX, 3529 curY, 3530 nest, 3531 j, 3532 offsetx = 0, 3533 offsety = 0, 3534 rootx, 3535 rooty, 3536 iCoords, 3537 radius, 3538 angle, 3539 el, 3540 coords = []; 3541 3542 // if a single element was passed, map it to an array 3543 3544 elements = elements.length ? elements : [elements]; 3545 3546 container = container ? $(container) : $(document.body); 3547 3548 // get the relative root of calculation 3549 3550 pos = container.offset(); 3551 rootx = pos.left; 3552 rooty = pos.top; 3553 3554 // with areas, all we know about is relative to the top-left corner of the image. We need to add an offset compared to 3555 // the actual container. After this calculation, offsetx/offsety can be added to either the area coords, or the target's 3556 // absolute position to get the correct top/left boundaries of the container. 3557 3558 if (image) { 3559 pos = $(image).offset(); 3560 offsetx = pos.left; 3561 offsety = pos.top; 3562 } 3563 3564 // map the coordinates of any type of shape to a poly and use the logic. simpler than using three different 3565 // calculation methods. Circles use a 20 degree increment for this estimation. 3566 3567 for (j = 0; j < elements.length; j++) { 3568 el = elements[j]; 3569 if (el.nodeName === 'AREA') { 3570 iCoords = u.split(el.coords, parseInt); 3571 3572 switch (u.getShape(el)) { 3573 case 'circle': 3574 case 'circ': 3575 curX = iCoords[0]; 3576 curY = iCoords[1]; 3577 radius = iCoords[2]; 3578 coords = []; 3579 for (j = 0; j < 360; j += 20) { 3580 angle = (j * Math.PI) / 180; 3581 coords.push( 3582 curX + radius * Math.cos(angle), 3583 curY + radius * Math.sin(angle) 3584 ); 3585 } 3586 break; 3587 case 'rectangle': 3588 case 'rect': 3589 coords.push( 3590 iCoords[0], 3591 iCoords[1], 3592 iCoords[2], 3593 iCoords[1], 3594 iCoords[2], 3595 iCoords[3], 3596 iCoords[0], 3597 iCoords[3] 3598 ); 3599 break; 3600 default: 3601 coords = coords.concat(iCoords); 3602 break; 3603 } 3604 3605 // map area positions to it's real position in the container 3606 3607 for (j = 0; j < coords.length; j += 2) { 3608 coords[j] = parseInt(coords[j], 10) + offsetx; 3609 coords[j + 1] = parseInt(coords[j + 1], 10) + offsety; 3610 } 3611 } else { 3612 el = $(el); 3613 pos = el.position(); 3614 coords.push( 3615 pos.left, 3616 pos.top, 3617 pos.left + el.width(), 3618 pos.top, 3619 pos.left + el.width(), 3620 pos.top + el.height(), 3621 pos.left, 3622 pos.top + el.height() 3623 ); 3624 } 3625 } 3626 3627 minX = minY = bestMinX = bestMinY = 999999; 3628 maxX = maxY = bestMaxX = bestMaxY = -1; 3629 3630 for (j = coords.length - 2; j >= 0; j -= 2) { 3631 curX = coords[j]; 3632 curY = coords[j + 1]; 3633 3634 if (curX < minX) { 3635 minX = curX; 3636 bestMaxY = curY; 3637 } 3638 if (curX > maxX) { 3639 maxX = curX; 3640 bestMinY = curY; 3641 } 3642 if (curY < minY) { 3643 minY = curY; 3644 bestMaxX = curX; 3645 } 3646 if (curY > maxY) { 3647 maxY = curY; 3648 bestMinX = curX; 3649 } 3650 } 3651 3652 // try to figure out the best place for the tooltip 3653 3654 if (width && height) { 3655 found = false; 3656 $.each( 3657 [ 3658 [bestMaxX - width, minY - height], 3659 [bestMinX, minY - height], 3660 [minX - width, bestMaxY - height], 3661 [minX - width, bestMinY], 3662 [maxX, bestMaxY - height], 3663 [maxX, bestMinY], 3664 [bestMaxX - width, maxY], 3665 [bestMinX, maxY] 3666 ], 3667 function (_, e) { 3668 if (!found && e[0] > rootx && e[1] > rooty) { 3669 nest = e; 3670 found = true; 3671 return false; 3672 } 3673 } 3674 ); 3675 3676 // default to lower-right corner if nothing fit inside the boundaries of the image 3677 3678 if (!found) { 3679 nest = [maxX, maxY]; 3680 } 3681 } 3682 return nest; 3683 }; 3684})(jQuery); 3685 3686/* 3687 scale.js 3688 Resize and zoom functionality 3689 Requires areacorners.js 3690*/ 3691 3692(function ($) { 3693 'use strict'; 3694 3695 var m = $.mapster, 3696 u = m.utils, 3697 p = m.MapArea.prototype; 3698 3699 m.utils.getScaleInfo = function (eff, actual) { 3700 var pct; 3701 if (!actual) { 3702 pct = 1; 3703 actual = eff; 3704 } else { 3705 pct = eff.width / actual.width || eff.height / actual.height; 3706 // make sure a float error doesn't muck us up 3707 if (pct > 0.98 && pct < 1.02) { 3708 pct = 1; 3709 } 3710 } 3711 return { 3712 scale: pct !== 1, 3713 scalePct: pct, 3714 realWidth: actual.width, 3715 realHeight: actual.height, 3716 width: eff.width, 3717 height: eff.height, 3718 ratio: eff.width / eff.height 3719 }; 3720 }; 3721 // Scale a set of AREAs, return old data as an array of objects 3722 m.utils.scaleMap = function (image, imageRaw, scale) { 3723 // stunningly, jQuery width can return zero even as width does not, seems to happen only 3724 // with adBlock or maybe other plugins. These must interfere with onload events somehow. 3725 3726 var vis = u.size(image), 3727 raw = u.size(imageRaw, true); 3728 3729 if (!raw.complete()) { 3730 throw 'Another script, such as an extension, appears to be interfering with image loading. Please let us know about this.'; 3731 } 3732 if (!vis.complete()) { 3733 vis = raw; 3734 } 3735 return this.getScaleInfo(vis, scale ? raw : null); 3736 }; 3737 3738 /** 3739 * Resize the image map. Only one of newWidth and newHeight should be passed to preserve scale 3740 * 3741 * @param {int} width The new width OR an object containing named parameters matching this function sig 3742 * @param {int} height The new height 3743 * @param {int} effectDuration Time in ms for the resize animation, or zero for no animation 3744 * @param {function} callback A function to invoke when the operation finishes 3745 * @return {promise} NOT YET IMPLEMENTED 3746 */ 3747 3748 m.MapData.prototype.resize = function (width, height, duration, callback) { 3749 var p, 3750 promises, 3751 newsize, 3752 els, 3753 highlightId, 3754 ratio, 3755 me = this; 3756 3757 // allow omitting duration 3758 callback = callback || duration; 3759 3760 function sizeCanvas(canvas, w, h) { 3761 if (m.hasCanvas()) { 3762 canvas.width = w; 3763 canvas.height = h; 3764 } else { 3765 $(canvas).width(w); 3766 $(canvas).height(h); 3767 } 3768 } 3769 3770 // Finalize resize action, do callback, pass control to command queue 3771 3772 function cleanupAndNotify() { 3773 me.currentAction = ''; 3774 3775 if (u.isFunction(callback)) { 3776 callback(); 3777 } 3778 3779 me.processCommandQueue(); 3780 } 3781 3782 // handle cleanup after the inner elements are resized 3783 3784 function finishResize() { 3785 sizeCanvas(me.overlay_canvas, width, height); 3786 3787 // restore highlight state if it was highlighted before 3788 if (highlightId >= 0) { 3789 var areaData = me.data[highlightId]; 3790 areaData.tempOptions = { fade: false }; 3791 me.getDataForKey(areaData.key).highlight(); 3792 areaData.tempOptions = null; 3793 } 3794 sizeCanvas(me.base_canvas, width, height); 3795 me.redrawSelections(); 3796 cleanupAndNotify(); 3797 } 3798 3799 function resizeMapData() { 3800 $(me.image).css(newsize); 3801 // start calculation at the same time as effect 3802 me.scaleInfo = u.getScaleInfo( 3803 { 3804 width: width, 3805 height: height 3806 }, 3807 { 3808 width: me.scaleInfo.realWidth, 3809 height: me.scaleInfo.realHeight 3810 } 3811 ); 3812 $.each(me.data, function (_, e) { 3813 $.each(e.areas(), function (_, e) { 3814 e.resize(); 3815 }); 3816 }); 3817 } 3818 3819 if (me.scaleInfo.width === width && me.scaleInfo.height === height) { 3820 return; 3821 } 3822 3823 highlightId = me.highlightId; 3824 3825 if (!width) { 3826 ratio = height / me.scaleInfo.realHeight; 3827 width = Math.round(me.scaleInfo.realWidth * ratio); 3828 } 3829 if (!height) { 3830 ratio = width / me.scaleInfo.realWidth; 3831 height = Math.round(me.scaleInfo.realHeight * ratio); 3832 } 3833 3834 newsize = { width: String(width) + 'px', height: String(height) + 'px' }; 3835 if (!m.hasCanvas()) { 3836 $(me.base_canvas).children().remove(); 3837 } 3838 3839 // resize all the elements that are part of the map except the image itself (which is not visible) 3840 // but including the div wrapper 3841 els = $(me.wrapper).find('.mapster_el'); 3842 if (me.options.enableAutoResizeSupport !== true) { 3843 els = els.add(me.wrapper); 3844 } 3845 3846 if (duration) { 3847 promises = []; 3848 me.currentAction = 'resizing'; 3849 els.filter(':visible').each(function (_, e) { 3850 p = u.defer(); 3851 promises.push(p); 3852 3853 $(e).animate(newsize, { 3854 duration: duration, 3855 complete: p.resolve, 3856 easing: 'linear' 3857 }); 3858 }); 3859 els.filter(':hidden').css(newsize); 3860 3861 p = u.defer(); 3862 promises.push(p); 3863 3864 // though resizeMapData is not async, it needs to be finished just the same as the animations, 3865 // so add it to the "to do" list. 3866 3867 u.when.all(promises).then(finishResize); 3868 resizeMapData(); 3869 p.resolve(); 3870 } else { 3871 els.css(newsize); 3872 resizeMapData(); 3873 finishResize(); 3874 } 3875 }; 3876 3877 m.MapData.prototype.autoResize = function (duration, callback) { 3878 var me = this; 3879 me.resize($(me.wrapper).width(), null, duration, callback); 3880 }; 3881 3882 m.MapData.prototype.configureAutoResize = function () { 3883 var me = this, 3884 ns = me.instanceEventNamespace(); 3885 3886 function resizeMap() { 3887 // Evaluate this at runtime to allow for set_options 3888 // to change behavior as set_options intentionally 3889 // does not change any rendering behavior when invoked. 3890 // To improve perf, in next major release this should 3891 // be refactored to add/remove listeners when autoResize 3892 // changes rather than always having listeners attached 3893 // and conditionally resizing 3894 if (me.options.autoResize !== true) { 3895 return; 3896 } 3897 3898 me.autoResize(me.options.autoResizeDuration, me.options.onAutoResize); 3899 } 3900 3901 function debounce() { 3902 if (me.autoResizeTimer) { 3903 clearTimeout(me.autoResizeTimer); 3904 } 3905 me.autoResizeTimer = setTimeout(resizeMap, me.options.autoResizeDelay); 3906 } 3907 3908 $(me.image).on('load' + ns, resizeMap); //Detect late image loads in IE11 3909 $(window).on('focus' + ns, resizeMap); 3910 $(window).on('resize' + ns, debounce); 3911 $(window).on('readystatechange' + ns, resizeMap); 3912 $(window.document).on('fullscreenchange' + ns, resizeMap); 3913 resizeMap(); 3914 }; 3915 3916 m.MapArea = u.subclass(m.MapArea, function () { 3917 //change the area tag data if needed 3918 this.base.init(); 3919 if (this.owner.scaleInfo.scale) { 3920 this.resize(); 3921 } 3922 }); 3923 3924 p.coords = function (percent, coordOffset) { 3925 var j, 3926 newCoords = [], 3927 pct = percent || this.owner.scaleInfo.scalePct, 3928 offset = coordOffset || 0; 3929 3930 if (pct === 1 && coordOffset === 0) { 3931 return this.originalCoords; 3932 } 3933 3934 for (j = 0; j < this.length; j++) { 3935 //amount = j % 2 === 0 ? xPct : yPct; 3936 newCoords.push(Math.round(this.originalCoords[j] * pct) + offset); 3937 } 3938 return newCoords; 3939 }; 3940 p.resize = function () { 3941 this.area.coords = this.coords().join(','); 3942 }; 3943 3944 p.reset = function () { 3945 this.area.coords = this.coords(1).join(','); 3946 }; 3947 3948 m.impl.resize = function (width, height, duration, callback) { 3949 var x = new m.Method( 3950 this, 3951 function () { 3952 var me = this, 3953 noDimensions = !width && !height, 3954 isAutoResize = 3955 me.options.enableAutoResizeSupport && 3956 me.options.autoResize && 3957 noDimensions; 3958 3959 if (isAutoResize) { 3960 me.autoResize(duration, callback); 3961 return; 3962 } 3963 3964 if (noDimensions) { 3965 return false; 3966 } 3967 3968 me.resize(width, height, duration, callback); 3969 }, 3970 null, 3971 { 3972 name: 'resize', 3973 args: arguments 3974 } 3975 ).go(); 3976 return x; 3977 }; 3978 3979 /* 3980 m.impl.zoom = function (key, opts) { 3981 var options = opts || {}; 3982 3983 function zoom(areaData) { 3984 // this will be MapData object returned by Method 3985 3986 var scroll, corners, height, width, ratio, 3987 diffX, diffY, ratioX, ratioY, offsetX, offsetY, newWidth, newHeight, scrollLeft, scrollTop, 3988 padding = options.padding || 0, 3989 scrollBarSize = areaData ? 20 : 0, 3990 me = this, 3991 zoomOut = false; 3992 3993 if (areaData) { 3994 // save original state on first zoom operation 3995 if (!me.zoomed) { 3996 me.zoomed = true; 3997 me.preZoomWidth = me.scaleInfo.width; 3998 me.preZoomHeight = me.scaleInfo.height; 3999 me.zoomedArea = areaData; 4000 if (options.scroll) { 4001 me.wrapper.css({ overflow: 'auto' }); 4002 } 4003 } 4004 corners = $.mapster.utils.areaCorners(areaData.coords(1, 0)); 4005 width = me.wrapper.innerWidth() - scrollBarSize - padding * 2; 4006 height = me.wrapper.innerHeight() - scrollBarSize - padding * 2; 4007 diffX = corners.maxX - corners.minX; 4008 diffY = corners.maxY - corners.minY; 4009 ratioX = width / diffX; 4010 ratioY = height / diffY; 4011 ratio = Math.min(ratioX, ratioY); 4012 offsetX = (width - diffX * ratio) / 2; 4013 offsetY = (height - diffY * ratio) / 2; 4014 4015 newWidth = me.scaleInfo.realWidth * ratio; 4016 newHeight = me.scaleInfo.realHeight * ratio; 4017 scrollLeft = (corners.minX) * ratio - padding - offsetX; 4018 scrollTop = (corners.minY) * ratio - padding - offsetY; 4019 } else { 4020 if (!me.zoomed) { 4021 return; 4022 } 4023 zoomOut = true; 4024 newWidth = me.preZoomWidth; 4025 newHeight = me.preZoomHeight; 4026 scrollLeft = null; 4027 scrollTop = null; 4028 } 4029 4030 this.resize({ 4031 width: newWidth, 4032 height: newHeight, 4033 duration: options.duration, 4034 scroll: scroll, 4035 scrollLeft: scrollLeft, 4036 scrollTop: scrollTop, 4037 // closure so we can be sure values are correct 4038 callback: (function () { 4039 var isZoomOut = zoomOut, 4040 scroll = options.scroll, 4041 areaD = areaData; 4042 return function () { 4043 if (isZoomOut) { 4044 me.preZoomWidth = null; 4045 me.preZoomHeight = null; 4046 me.zoomed = false; 4047 me.zoomedArea = false; 4048 if (scroll) { 4049 me.wrapper.css({ overflow: 'inherit' }); 4050 } 4051 } else { 4052 // just to be sure it wasn't canceled & restarted 4053 me.zoomedArea = areaD; 4054 } 4055 }; 4056 } ()) 4057 }); 4058 } 4059 return (new m.Method(this, 4060 function (opts) { 4061 zoom.call(this); 4062 }, 4063 function () { 4064 zoom.call(this.owner, this); 4065 }, 4066 { 4067 name: 'zoom', 4068 args: arguments, 4069 first: true, 4070 key: key 4071 } 4072 )).go(); 4073 }; 4074 */ 4075})(jQuery); 4076 4077/* 4078 tooltip.js 4079 Tooltip functionality 4080 Requires areacorners.js 4081*/ 4082 4083(function ($) { 4084 'use strict'; 4085 4086 var m = $.mapster, 4087 u = m.utils; 4088 4089 $.extend(m.defaults, { 4090 toolTipContainer: 4091 '<div style="border: 2px solid black; background: #EEEEEE; width:160px; padding:4px; margin: 4px; -moz-box-shadow: 3px 3px 5px #535353; ' + 4092 '-webkit-box-shadow: 3px 3px 5px #535353; box-shadow: 3px 3px 5px #535353; -moz-border-radius: 6px 6px 6px 6px; -webkit-border-radius: 6px; ' + 4093 'border-radius: 6px 6px 6px 6px; opacity: 0.9;"></div>', 4094 showToolTip: false, 4095 toolTip: null, 4096 toolTipFade: true, 4097 toolTipClose: ['area-mouseout', 'image-mouseout', 'generic-mouseout'], 4098 onShowToolTip: null, 4099 onHideToolTip: null 4100 }); 4101 4102 $.extend(m.area_defaults, { 4103 toolTip: null, 4104 toolTipClose: null 4105 }); 4106 4107 /** 4108 * Show a tooltip positioned near this area. 4109 * 4110 * @param {string|jquery} html A string of html or a jQuery object containing the tooltip content. 4111 * @param {string|jquery} [template] The html template in which to wrap the content 4112 * @param {string|object} [css] CSS to apply to the outermost element of the tooltip 4113 * @return {jquery} The tooltip that was created 4114 */ 4115 4116 function createToolTip(html, template, css) { 4117 var tooltip; 4118 4119 // wrap the template in a jQuery object, or clone the template if it's already one. 4120 // This assumes that anything other than a string is a jQuery object; if it's not jQuery will 4121 // probably throw an error. 4122 4123 if (template) { 4124 tooltip = 4125 typeof template === 'string' ? $(template) : $(template).clone(); 4126 4127 tooltip.append(html); 4128 } else { 4129 tooltip = $(html); 4130 } 4131 4132 // always set display to block, or the positioning css won't work if the end user happened to 4133 // use a non-block type element. 4134 4135 tooltip 4136 .css( 4137 $.extend(css || {}, { 4138 display: 'block', 4139 position: 'absolute' 4140 }) 4141 ) 4142 .hide(); 4143 4144 $('body').append(tooltip); 4145 4146 // we must actually add the tooltip to the DOM and "show" it in order to figure out how much space it 4147 // consumes, and then reposition it with that knowledge. 4148 // We also cache the actual opacity setting to restore finally. 4149 4150 tooltip.attr('data-opacity', tooltip.css('opacity')).css('opacity', 0); 4151 4152 // doesn't really show it because opacity=0 4153 4154 return tooltip.show(); 4155 } 4156 4157 /** 4158 * Show a tooltip positioned near this area. 4159 * 4160 * @param {jquery} tooltip The tooltip 4161 * @param {object} [options] options for displaying the tooltip. 4162 * @config {int} [left] The 0-based absolute x position for the tooltip 4163 * @config {int} [top] The 0-based absolute y position for the tooltip 4164 * @config {string|object} [css] CSS to apply to the outermost element of the tooltip 4165 * @config {bool} [fadeDuration] When non-zero, the duration in milliseconds of a fade-in effect for the tooltip. 4166 */ 4167 4168 function showToolTipImpl(tooltip, options) { 4169 var tooltipCss = { 4170 left: options.left + 'px', 4171 top: options.top + 'px' 4172 }, 4173 actalOpacity = tooltip.attr('data-opacity') || 0, 4174 zindex = tooltip.css('z-index'); 4175 4176 if (parseInt(zindex, 10) === 0 || zindex === 'auto') { 4177 tooltipCss['z-index'] = 9999; 4178 } 4179 4180 tooltip.css(tooltipCss).addClass('mapster_tooltip'); 4181 4182 if (options.fadeDuration && options.fadeDuration > 0) { 4183 u.fader(tooltip[0], 0, actalOpacity, options.fadeDuration); 4184 } else { 4185 u.setOpacity(tooltip[0], actalOpacity); 4186 } 4187 } 4188 4189 /** 4190 * Hide and remove active tooltips 4191 * 4192 * @param {MapData} this The mapdata object to which the tooltips belong 4193 */ 4194 4195 m.MapData.prototype.clearToolTip = function () { 4196 if (this.activeToolTip) { 4197 this.activeToolTip.stop().remove(); 4198 this.activeToolTip = null; 4199 this.activeToolTipID = null; 4200 u.ifFunction(this.options.onHideToolTip, this); 4201 } 4202 }; 4203 4204 /** 4205 * Configure the binding between a named tooltip closing option, and a mouse event. 4206 * 4207 * If a callback is passed, it will be called when the activating event occurs, and the tooltip will 4208 * only closed if it returns true. 4209 * 4210 * @param {MapData} [this] The MapData object to which this tooltip belongs. 4211 * @param {String} option The name of the tooltip closing option 4212 * @param {String} event UI event to bind to this option 4213 * @param {Element} target The DOM element that is the target of the event 4214 * @param {Function} [beforeClose] Callback when the tooltip is closed 4215 * @param {Function} [onClose] Callback when the tooltip is closed 4216 */ 4217 function bindToolTipClose( 4218 options, 4219 bindOption, 4220 event, 4221 target, 4222 beforeClose, 4223 onClose 4224 ) { 4225 var tooltip_ns = '.mapster.tooltip', 4226 event_name = event + tooltip_ns; 4227 4228 if ($.inArray(bindOption, options) >= 0) { 4229 target.off(event_name).on(event_name, function (e) { 4230 if (!beforeClose || beforeClose.call(this, e)) { 4231 target.off(tooltip_ns); 4232 if (onClose) { 4233 onClose.call(this); 4234 } 4235 } 4236 }); 4237 4238 return { 4239 object: target, 4240 event: event_name 4241 }; 4242 } 4243 } 4244 4245 /** 4246 * Show a tooltip. 4247 * 4248 * @param {string|jquery} [tooltip] A string of html or a jQuery object containing the tooltip content. 4249 * 4250 * @param {string|jquery} [target] The target of the tooltip, to be used to determine positioning. If null, 4251 * absolute position values must be passed with left and top. 4252 * 4253 * @param {string|jquery} [image] If target is an [area] the image that owns it 4254 * 4255 * @param {string|jquery} [container] An element within which the tooltip must be bounded 4256 * 4257 * @param {object|string|jQuery} [options] options to apply when creating this tooltip 4258 * @config {int} [offsetx] the horizontal amount to offset the tooltip 4259 * @config {int} [offsety] the vertical amount to offset the tooltip 4260 * @config {string|object} [css] CSS to apply to the outermost element of the tooltip 4261 * @config {bool} [fadeDuration] When non-zero, the duration in milliseconds of a fade-in effect for the tooltip. 4262 * @config {int} [left] The 0-based absolute x position for the tooltip (only used if target is not specified) 4263 * @config {int} [top] The 0-based absolute y position for the tooltip (only used if target it not specified) 4264 */ 4265 4266 function showToolTip(tooltip, target, image, container, options) { 4267 var corners, 4268 ttopts = {}; 4269 4270 options = options || {}; 4271 4272 if (target) { 4273 corners = u.areaCorners( 4274 target, 4275 image, 4276 container, 4277 tooltip.outerWidth(true), 4278 tooltip.outerHeight(true) 4279 ); 4280 4281 // Try to upper-left align it first, if that doesn't work, change the parameters 4282 4283 ttopts.left = corners[0]; 4284 ttopts.top = corners[1]; 4285 } else { 4286 ttopts.left = options.left; 4287 ttopts.top = options.top; 4288 } 4289 4290 ttopts.left += options.offsetx || 0; 4291 ttopts.top += options.offsety || 0; 4292 4293 ttopts.css = options.css; 4294 ttopts.fadeDuration = options.fadeDuration; 4295 4296 showToolTipImpl(tooltip, ttopts); 4297 4298 return tooltip; 4299 } 4300 4301 /** 4302 * Show a tooltip positioned near this area. 4303 * 4304 * @param {string|jquery|function} [content] A string of html, jQuery object or function that returns same containing the tooltip content. 4305 4306 * @param {object} [options] options to apply when creating this tooltip 4307 * @config {string|jquery} [container] An element within which the tooltip must be bounded 4308 * @config {bool} [template] a template to use instead of the default. If this property exists and is null, 4309 * then no template will be used. 4310 * @config {string} [closeEvents] A string with one or more comma-separated values that determine when the tooltip 4311 * closes: 'area-click','tooltip-click','image-mouseout','image-click' are valid values 4312 * then no template will be used. 4313 * @config {int} [offsetx] the horizontal amount to offset the tooltip 4314 * @config {int} [offsety] the vertical amount to offset the tooltip 4315 * @config {string|object} [css] CSS to apply to the outermost element of the tooltip 4316 * @config {bool} [fadeDuration] When non-zero, the duration in milliseconds of a fade-in effect for the tooltip. 4317 */ 4318 m.AreaData.prototype.showToolTip = function (content, options) { 4319 var tooltip, 4320 closeOpts, 4321 target, 4322 tipClosed, 4323 template, 4324 ttopts = {}, 4325 ad = this, 4326 md = ad.owner, 4327 areaOpts = ad.effectiveOptions(); 4328 4329 // copy the options object so we can update it 4330 options = options ? $.extend({}, options) : {}; 4331 4332 content = content || areaOpts.toolTip; 4333 closeOpts = 4334 options.closeEvents || 4335 areaOpts.toolTipClose || 4336 md.options.toolTipClose || 4337 'tooltip-click'; 4338 4339 template = 4340 typeof options.template !== 'undefined' 4341 ? options.template 4342 : md.options.toolTipContainer; 4343 4344 options.closeEvents = 4345 typeof closeOpts === 'string' 4346 ? (closeOpts = u.split(closeOpts)) 4347 : closeOpts; 4348 4349 options.fadeDuration = 4350 options.fadeDuration || 4351 (md.options.toolTipFade 4352 ? md.options.fadeDuration || areaOpts.fadeDuration 4353 : 0); 4354 4355 target = ad.area 4356 ? ad.area 4357 : $.map(ad.areas(), function (e) { 4358 return e.area; 4359 }); 4360 4361 if (md.activeToolTipID === ad.areaId) { 4362 return; 4363 } 4364 4365 md.clearToolTip(); 4366 4367 var effectiveContent = u.isFunction(content) 4368 ? content({ key: this.key, target: target }) 4369 : content; 4370 4371 if (!effectiveContent) { 4372 return; 4373 } 4374 4375 md.activeToolTip = tooltip = createToolTip( 4376 effectiveContent, 4377 template, 4378 options.css 4379 ); 4380 4381 md.activeToolTipID = ad.areaId; 4382 4383 tipClosed = function () { 4384 md.clearToolTip(); 4385 }; 4386 4387 bindToolTipClose( 4388 closeOpts, 4389 'area-click', 4390 'click', 4391 $(md.map), 4392 null, 4393 tipClosed 4394 ); 4395 bindToolTipClose( 4396 closeOpts, 4397 'tooltip-click', 4398 'click', 4399 tooltip, 4400 null, 4401 tipClosed 4402 ); 4403 bindToolTipClose( 4404 closeOpts, 4405 'image-mouseout', 4406 'mouseout', 4407 $(md.image), 4408 function (e) { 4409 return ( 4410 e.relatedTarget && 4411 e.relatedTarget.nodeName !== 'AREA' && 4412 e.relatedTarget !== ad.area 4413 ); 4414 }, 4415 tipClosed 4416 ); 4417 bindToolTipClose( 4418 closeOpts, 4419 'image-click', 4420 'click', 4421 $(md.image), 4422 null, 4423 tipClosed 4424 ); 4425 4426 showToolTip(tooltip, target, md.image, options.container, options); 4427 4428 u.ifFunction(md.options.onShowToolTip, ad.area, { 4429 toolTip: tooltip, 4430 options: ttopts, 4431 areaOptions: areaOpts, 4432 key: ad.key, 4433 selected: ad.isSelected() 4434 }); 4435 4436 return tooltip; 4437 }; 4438 4439 /** 4440 * Parse an object that could be a string, a jquery object, or an object with a "contents" property 4441 * containing html or a jQuery object. 4442 * 4443 * @param {object|string|jQuery} options The parameter to parse 4444 * @return {string|jquery} A string or jquery object 4445 */ 4446 function getHtmlFromOptions(options) { 4447 // see if any html was passed as either the options object itself, or the content property 4448 4449 return options 4450 ? typeof options === 'string' || options.jquery || u.isFunction(options) 4451 ? options 4452 : options.content 4453 : null; 4454 } 4455 4456 function getOptionsFromOptions(options) { 4457 return options 4458 ? typeof options == 'string' || options.jquery || u.isFunction(options) 4459 ? { content: options } 4460 : options 4461 : {}; 4462 } 4463 4464 /** 4465 * Activate or remove a tooltip for an area. When this method is called on an area, the 4466 * key parameter doesn't apply and "options" is the first parameter. 4467 * 4468 * When called with no parameters, or "key" is a falsy value, any active tooltip is cleared. 4469 * 4470 * When only a key is provided, the default tooltip for the area is used. 4471 * 4472 * When html is provided, this is used instead of the default tooltip. 4473 * 4474 * When "noTemplate" is true, the default tooltip template will not be used either, meaning only 4475 * the actual html passed will be used. 4476 * 4477 * @param {string|AreaElement|HTMLElement} key The area key or element for which to activate a tooltip, or a DOM element/selector. 4478 * 4479 * @param {object|string|jquery} [options] options to apply when creating this tooltip - OR - 4480 * The markup, or a jquery object, containing the data for the tooltip 4481 * @config {string|jQuery|function} [content] the inner content of the tooltip; the tooltip text, HTML or function that returns same 4482 * @config {Element|jQuery} [container] the inner content of the tooltip; the tooltip text or HTML 4483 * @config {bool} [template] a template to use instead of the default. If this property exists and is null, 4484 * then no template will be used. 4485 * @config {string} [closeEvents] A string with one or more comma-separated values that determine when the tooltip 4486 * closes: 'area-click','tooltip-click','image-mouseout','image-click','generic-click','generic-mouseout' are valid values 4487 * @config {int} [offsetx] the horizontal amount to offset the tooltip. 4488 * @config {int} [offsety] the vertical amount to offset the tooltip. 4489 * @config {string|object} [css] CSS to apply to the outermost element of the tooltip 4490 * @config {bool} [fadeDuration] When non-zero, the duration in milliseconds of a fade-in effect for the tooltip. 4491 * @return {jQuery} The jQuery object 4492 */ 4493 4494 m.impl.tooltip = function (key, options) { 4495 return new m.Method( 4496 this, 4497 function mapData() { 4498 var tooltip, 4499 target, 4500 defaultTarget, 4501 closeOpts, 4502 tipClosed, 4503 md = this; 4504 if (!key) { 4505 md.clearToolTip(); 4506 } else { 4507 target = $(key); 4508 defaultTarget = target && target.length > 0 ? target[0] : null; 4509 if (md.activeToolTipID === defaultTarget) { 4510 return; 4511 } 4512 4513 md.clearToolTip(); 4514 if (!defaultTarget) { 4515 return; 4516 } 4517 4518 var content = getHtmlFromOptions(options), 4519 effectiveContent = u.isFunction(content) 4520 ? content({ key: this.key, target: target }) 4521 : content; 4522 4523 if (!effectiveContent) { 4524 return; 4525 } 4526 4527 options = getOptionsFromOptions(options); 4528 4529 closeOpts = 4530 options.closeEvents || md.options.toolTipClose || 'tooltip-click'; 4531 4532 options.closeEvents = 4533 typeof closeOpts === 'string' 4534 ? (closeOpts = u.split(closeOpts)) 4535 : closeOpts; 4536 4537 options.fadeDuration = 4538 options.fadeDuration || 4539 (md.options.toolTipFade ? md.options.fadeDuration : 0); 4540 4541 tipClosed = function () { 4542 md.clearToolTip(); 4543 }; 4544 4545 md.activeToolTip = tooltip = createToolTip( 4546 effectiveContent, 4547 options.template || md.options.toolTipContainer, 4548 options.css 4549 ); 4550 md.activeToolTipID = defaultTarget; 4551 4552 bindToolTipClose( 4553 closeOpts, 4554 'tooltip-click', 4555 'click', 4556 tooltip, 4557 null, 4558 tipClosed 4559 ); 4560 4561 bindToolTipClose( 4562 closeOpts, 4563 'generic-mouseout', 4564 'mouseout', 4565 target, 4566 null, 4567 tipClosed 4568 ); 4569 4570 bindToolTipClose( 4571 closeOpts, 4572 'generic-click', 4573 'click', 4574 target, 4575 null, 4576 tipClosed 4577 ); 4578 4579 md.activeToolTip = tooltip = showToolTip( 4580 tooltip, 4581 target, 4582 md.image, 4583 options.container, 4584 options 4585 ); 4586 } 4587 }, 4588 function areaData() { 4589 if ($.isPlainObject(key) && !options) { 4590 options = key; 4591 } 4592 4593 this.showToolTip( 4594 getHtmlFromOptions(options), 4595 getOptionsFromOptions(options) 4596 ); 4597 }, 4598 { 4599 name: 'tooltip', 4600 args: arguments, 4601 key: key 4602 } 4603 ).go(); 4604 }; 4605})(jQuery); 4606 4607}));