/* ImageMapster
Version: 1.2.10 (2/25/2013)
Copyright 2011-2012 James Treworgy
http://www.outsharked.com/imagemapster
https://github.com/jamietre/ImageMapster
A jQuery plugin to enhance image maps.
*/
;
/// LICENSE (MIT License)
///
/// Permission is hereby granted, free of charge, to any person obtaining
/// a copy of this software and associated documentation files (the
/// "Software"), to deal in the Software without restriction, including
/// without limitation the rights to use, copy, modify, merge, publish,
/// distribute, sublicense, and/or sell copies of the Software, and to
/// permit persons to whom the Software is furnished to do so, subject to
/// the following conditions:
///
/// The above copyright notice and this permission notice shall be
/// included in all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
/// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
/// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
/// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
/// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
/// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
/// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
///
/// January 19, 2011
/** @license MIT License (c) copyright B Cavalier & J Hann */
/**
* when
* A lightweight CommonJS Promises/A and when() implementation
*
* when is part of the cujo.js family of libraries (http://cujojs.com/)
*
* Licensed under the MIT License at:
* http://www.opensource.org/licenses/mit-license.php
*
* @version 1.2.0
*/
/*lint-ignore-start*/
(function (define) {
define(function () {
var freeze, reduceArray, slice, undef;
//
// Public API
//
when.defer = defer;
when.reject = reject;
when.isPromise = isPromise;
when.all = all;
when.some = some;
when.any = any;
when.map = map;
when.reduce = reduce;
when.chain = chain;
/** Object.freeze */
freeze = Object.freeze || function (o) { return o; };
/**
* Trusted Promise constructor. A Promise created from this constructor is
* a trusted when.js promise. Any other duck-typed promise is considered
* untrusted.
*
* @constructor
*/
function Promise() { }
Promise.prototype = freeze({
always: function (alwaysback, progback) {
return this.then(alwaysback, alwaysback, progback);
},
otherwise: function (errback) {
return this.then(undef, errback);
}
});
/**
* Create an already-resolved promise for the supplied value
* @private
*
* @param value anything
* @return {Promise}
*/
function resolved(value) {
var p = new Promise();
p.then = function (callback) {
var nextValue;
try {
if (callback) nextValue = callback(value);
return promise(nextValue === undef ? value : nextValue);
} catch (e) {
return rejected(e);
}
};
return freeze(p);
}
/**
* Create an already-rejected {@link Promise} with the supplied
* rejection reason.
* @private
*
* @param reason rejection reason
* @return {Promise}
*/
function rejected(reason) {
var p = new Promise();
p.then = function (callback, errback) {
var nextValue;
try {
if (errback) {
nextValue = errback(reason);
return promise(nextValue === undef ? reason : nextValue)
}
return rejected(reason);
} catch (e) {
return rejected(e);
}
};
return freeze(p);
}
/**
* Returns a rejected promise for the supplied promiseOrValue. If
* promiseOrValue is a value, it will be the rejection value of the
* returned promise. If promiseOrValue is a promise, its
* completion value will be the rejected value of the returned promise
*
* @param promiseOrValue {*} the rejected value of the returned {@link Promise}
*
* @return {Promise} rejected {@link Promise}
*/
function reject(promiseOrValue) {
return when(promiseOrValue, function (value) {
return rejected(value);
});
}
/**
* Creates a new, CommonJS compliant, Deferred with fully isolated
* resolver and promise parts, either or both of which may be given out
* safely to consumers.
* The Deferred itself has the full API: resolve, reject, progress, and
* then. The resolver has resolve, reject, and progress. The promise
* only has then.
*
* @memberOf when
* @function
*
* @returns {Deferred}
*/
function defer() {
var deferred, promise, listeners, progressHandlers, _then, _progress, complete;
listeners = [];
progressHandlers = [];
/**
* Pre-resolution then() that adds the supplied callback, errback, and progback
* functions to the registered listeners
*
* @private
*
* @param [callback] {Function} resolution handler
* @param [errback] {Function} rejection handler
* @param [progback] {Function} progress handler
*
* @throws {Error} if any argument is not null, undefined, or a Function
*/
_then = function unresolvedThen(callback, errback, progback) {
var deferred = defer();
listeners.push(function (promise) {
promise.then(callback, errback)
.then(deferred.resolve, deferred.reject, deferred.progress);
});
progback && progressHandlers.push(progback);
return deferred.promise;
};
/**
* Registers a handler for this {@link Deferred}'s {@link Promise}. Even though all arguments
* are optional, each argument that *is* supplied must be null, undefined, or a Function.
* Any other value will cause an Error to be thrown.
*
* @memberOf Promise
*
* @param [callback] {Function} resolution handler
* @param [errback] {Function} rejection handler
* @param [progback] {Function} progress handler
*
* @throws {Error} if any argument is not null, undefined, or a Function
*/
function then(callback, errback, progback) {
return _then(callback, errback, progback);
}
/**
* Resolves this {@link Deferred}'s {@link Promise} with val as the
* resolution value.
*
* @memberOf Resolver
*
* @param val anything
*/
function resolve(val) {
complete(resolved(val));
}
/**
* Rejects this {@link Deferred}'s {@link Promise} with err as the
* reason.
*
* @memberOf Resolver
*
* @param err anything
*/
function reject(err) {
complete(rejected(err));
}
/**
* @private
* @param update
*/
_progress = function (update) {
var progress, i = 0;
while (progress = progressHandlers[i++]) progress(update);
};
/**
* Emits a progress update to all progress observers registered with
* this {@link Deferred}'s {@link Promise}
*
* @memberOf Resolver
*
* @param update anything
*/
function progress(update) {
_progress(update);
}
/**
* Transition from pre-resolution state to post-resolution state, notifying
* all listeners of the resolution or rejection
*
* @private
*
* @param completed {Promise} the completed value of this deferred
*/
complete = function (completed) {
var listener, i = 0;
// Replace _then with one that directly notifies with the result.
_then = completed.then;
// Replace complete so that this Deferred can only be completed
// once. Also Replace _progress, so that subsequent attempts to issue
// progress throw.
complete = _progress = function alreadyCompleted() {
// TODO: Consider silently returning here so that parties who
// have a reference to the resolver cannot tell that the promise
// has been resolved using try/catch
throw new Error("already completed");
};
// Free progressHandlers array since we'll never issue progress events
// for this promise again now that it's completed
progressHandlers = undef;
// Notify listeners
// Traverse all listeners registered directly with this Deferred
while (listener = listeners[i++]) {
listener(completed);
}
listeners = [];
};
/**
* The full Deferred object, with both {@link Promise} and {@link Resolver}
* parts
* @class Deferred
* @name Deferred
*/
deferred = {};
// Promise and Resolver parts
// Freeze Promise and Resolver APIs
promise = new Promise();
promise.then = deferred.then = then;
/**
* The {@link Promise} for this {@link Deferred}
* @memberOf Deferred
* @name promise
* @type {Promise}
*/
deferred.promise = freeze(promise);
/**
* The {@link Resolver} for this {@link Deferred}
* @memberOf Deferred
* @name resolver
* @class Resolver
*/
deferred.resolver = freeze({
resolve: (deferred.resolve = resolve),
reject: (deferred.reject = reject),
progress: (deferred.progress = progress)
});
return deferred;
}
/**
* Determines if promiseOrValue is a promise or not. Uses the feature
* test from http://wiki.commonjs.org/wiki/Promises/A to determine if
* promiseOrValue is a promise.
*
* @param promiseOrValue anything
*
* @returns {Boolean} true if promiseOrValue is a {@link Promise}
*/
function isPromise(promiseOrValue) {
return promiseOrValue && typeof promiseOrValue.then === 'function';
}
/**
* Register an observer for a promise or immediate value.
*
* @function
* @name when
* @namespace
*
* @param promiseOrValue anything
* @param {Function} [callback] callback to be called when promiseOrValue is
* successfully resolved. If promiseOrValue is an immediate value, callback
* will be invoked immediately.
* @param {Function} [errback] callback to be called when promiseOrValue is
* rejected.
* @param {Function} [progressHandler] callback to be called when progress updates
* are issued for promiseOrValue.
*
* @returns {Promise} a new {@link Promise} that will complete with the return
* value of callback or errback or the completion value of promiseOrValue if
* callback and/or errback is not supplied.
*/
function when(promiseOrValue, callback, errback, progressHandler) {
// Get a promise for the input promiseOrValue
// See promise()
var trustedPromise = promise(promiseOrValue);
// Register promise handlers
return trustedPromise.then(callback, errback, progressHandler);
}
/**
* Returns promiseOrValue if promiseOrValue is a {@link Promise}, a new Promise if
* promiseOrValue is a foreign promise, or a new, already-resolved {@link Promise}
* whose resolution value is promiseOrValue if promiseOrValue is an immediate value.
*
* Note that this function is not safe to export since it will return its
* input when promiseOrValue is a {@link Promise}
*
* @private
*
* @param promiseOrValue anything
*
* @returns Guaranteed to return a trusted Promise. If promiseOrValue is a when.js {@link Promise}
* returns promiseOrValue, otherwise, returns a new, already-resolved, when.js {@link Promise}
* whose resolution value is:
* * the resolution value of promiseOrValue if it's a foreign promise, or
* * promiseOrValue if it's a value
*/
function promise(promiseOrValue) {
var promise, deferred;
if (promiseOrValue instanceof Promise) {
// It's a when.js promise, so we trust it
promise = promiseOrValue;
} else {
// It's not a when.js promise. Check to see if it's a foreign promise
// or a value.
deferred = defer();
if (isPromise(promiseOrValue)) {
// It's a compliant promise, but we don't know where it came from,
// so we don't trust its implementation entirely. Introduce a trusted
// middleman when.js promise
// IMPORTANT: This is the only place when.js should ever call .then() on
// an untrusted promise.
promiseOrValue.then(deferred.resolve, deferred.reject, deferred.progress);
promise = deferred.promise;
} else {
// It's a value, not a promise. Create an already-resolved promise
// for it.
deferred.resolve(promiseOrValue);
promise = deferred.promise;
}
}
return promise;
}
/**
* Return a promise that will resolve when howMany of the supplied promisesOrValues
* have resolved. The resolution value of the returned promise will be an array of
* length howMany containing the resolutions values of the triggering promisesOrValues.
*
* @memberOf when
*
* @param promisesOrValues {Array} array of anything, may contain a mix
* of {@link Promise}s and values
* @param howMany
* @param [callback]
* @param [errback]
* @param [progressHandler]
*
* @returns {Promise}
*/
function some(promisesOrValues, howMany, callback, errback, progressHandler) {
checkCallbacks(2, arguments);
return when(promisesOrValues, function (promisesOrValues) {
var toResolve, results, ret, deferred, resolver, rejecter, handleProgress, len, i;
len = promisesOrValues.length >>> 0;
toResolve = Math.max(0, Math.min(howMany, len));
results = [];
deferred = defer();
ret = when(deferred, callback, errback, progressHandler);
// Wrapper so that resolver can be replaced
function resolve(val) {
resolver(val);
}
// Wrapper so that rejecter can be replaced
function reject(err) {
rejecter(err);
}
// Wrapper so that progress can be replaced
function progress(update) {
handleProgress(update);
}
function complete() {
resolver = rejecter = handleProgress = noop;
}
// No items in the input, resolve immediately
if (!toResolve) {
deferred.resolve(results);
} else {
// Resolver for promises. Captures the value and resolves
// the returned promise when toResolve reaches zero.
// Overwrites resolver var with a noop once promise has
// be resolved to cover case where n < promises.length
resolver = function (val) {
// This orders the values based on promise resolution order
// Another strategy would be to use the original position of
// the corresponding promise.
results.push(val);
if (! --toResolve) {
complete();
deferred.resolve(results);
}
};
// Rejecter for promises. Rejects returned promise
// immediately, and overwrites rejecter var with a noop
// once promise to cover case where n < promises.length.
// TODO: Consider rejecting only when N (or promises.length - N?)
// promises have been rejected instead of only one?
rejecter = function (err) {
complete();
deferred.reject(err);
};
handleProgress = deferred.progress;
// TODO: Replace while with forEach
for (i = 0; i < len; ++i) {
if (i in promisesOrValues) {
when(promisesOrValues[i], resolve, reject, progress);
}
}
}
return ret;
});
}
/**
* Return a promise that will resolve only once all the supplied promisesOrValues
* have resolved. The resolution value of the returned promise will be an array
* containing the resolution values of each of the promisesOrValues.
*
* @memberOf when
*
* @param promisesOrValues {Array|Promise} array of anything, may contain a mix
* of {@link Promise}s and values
* @param [callback] {Function}
* @param [errback] {Function}
* @param [progressHandler] {Function}
*
* @returns {Promise}
*/
function all(promisesOrValues, callback, errback, progressHandler) {
checkCallbacks(1, arguments);
return when(promisesOrValues, function (promisesOrValues) {
return _reduce(promisesOrValues, reduceIntoArray, []);
}).then(callback, errback, progressHandler);
}
function reduceIntoArray(current, val, i) {
current[i] = val;
return current;
}
/**
* Return a promise that will resolve when any one of the supplied promisesOrValues
* has resolved. The resolution value of the returned promise will be the resolution
* value of the triggering promiseOrValue.
*
* @memberOf when
*
* @param promisesOrValues {Array|Promise} array of anything, may contain a mix
* of {@link Promise}s and values
* @param [callback] {Function}
* @param [errback] {Function}
* @param [progressHandler] {Function}
*
* @returns {Promise}
*/
function any(promisesOrValues, callback, errback, progressHandler) {
function unwrapSingleResult(val) {
return callback ? callback(val[0]) : val[0];
}
return some(promisesOrValues, 1, unwrapSingleResult, errback, progressHandler);
}
/**
* Traditional map function, similar to `Array.prototype.map()`, but allows
* input to contain {@link Promise}s and/or values, and mapFunc may return
* either a value or a {@link Promise}
*
* @memberOf when
*
* @param promise {Array|Promise} array of anything, may contain a mix
* of {@link Promise}s and values
* @param mapFunc {Function} mapping function mapFunc(value) which may return
* either a {@link Promise} or value
*
* @returns {Promise} a {@link Promise} that will resolve to an array containing
* the mapped output values.
*/
function map(promise, mapFunc) {
return when(promise, function (array) {
return _map(array, mapFunc);
});
}
/**
* Private map helper to map an array of promises
* @private
*
* @param promisesOrValues {Array}
* @param mapFunc {Function}
* @return {Promise}
*/
function _map(promisesOrValues, mapFunc) {
var results, len, i;
// Since we know the resulting length, we can preallocate the results
// array to avoid array expansions.
len = promisesOrValues.length >>> 0;
results = new Array(len);
// Since mapFunc may be async, get all invocations of it into flight
// asap, and then use reduce() to collect all the results
for (i = 0; i < len; i++) {
if (i in promisesOrValues)
results[i] = when(promisesOrValues[i], mapFunc);
}
// Could use all() here, but that would result in another array
// being allocated, i.e. map() would end up allocating 2 arrays
// of size len instead of just 1. Since all() uses reduce()
// anyway, avoid the additional allocation by calling reduce
// directly.
return _reduce(results, reduceIntoArray, results);
}
/**
* Traditional reduce function, similar to `Array.prototype.reduce()`, but
* input may contain {@link Promise}s and/or values, and reduceFunc
* may return either a value or a {@link Promise}, *and* initialValue may
* be a {@link Promise} for the starting value.
*
* @memberOf when
*
* @param promise {Array|Promise} array of anything, may contain a mix
* of {@link Promise}s and values. May also be a {@link Promise} for
* an array.
* @param reduceFunc {Function} reduce function reduce(currentValue, nextValue, index, total),
* where total is the total number of items being reduced, and will be the same
* in each call to reduceFunc.
* @param initialValue starting value, or a {@link Promise} for the starting value
*
* @returns {Promise} that will resolve to the final reduced value
*/
function reduce(promise, reduceFunc, initialValue) {
var args = slice.call(arguments, 1);
return when(promise, function (array) {
return _reduce.apply(undef, [array].concat(args));
});
}
/**
* Private reduce to reduce an array of promises
* @private
*
* @param promisesOrValues {Array}
* @param reduceFunc {Function}
* @param initialValue {*}
* @return {Promise}
*/
function _reduce(promisesOrValues, reduceFunc, initialValue) {
var total, args;
total = promisesOrValues.length;
// Skip promisesOrValues, since it will be used as 'this' in the call
// to the actual reduce engine below.
// Wrap the supplied reduceFunc with one that handles promises and then
// delegates to the supplied.
args = [
function (current, val, i) {
return when(current, function (c) {
return when(val, function (value) {
return reduceFunc(c, value, i, total);
});
});
}
];
if (arguments.length > 2) args.push(initialValue);
return reduceArray.apply(promisesOrValues, args);
}
/**
* Ensure that resolution of promiseOrValue will complete resolver with the completion
* value of promiseOrValue, or instead with resolveValue if it is provided.
*
* @memberOf when
*
* @param promiseOrValue
* @param resolver {Resolver}
* @param [resolveValue] anything
*
* @returns {Promise}
*/
function chain(promiseOrValue, resolver, resolveValue) {
var useResolveValue = arguments.length > 2;
return when(promiseOrValue,
function (val) {
if (useResolveValue) val = resolveValue;
resolver.resolve(val);
return val;
},
function (e) {
resolver.reject(e);
return rejected(e);
},
resolver.progress
);
}
//
// Utility functions
//
/**
* Helper that checks arrayOfCallbacks to ensure that each element is either
* a function, or null or undefined.
*
* @private
*
* @param arrayOfCallbacks {Array} array to check
* @throws {Error} if any element of arrayOfCallbacks is something other than
* a Functions, null, or undefined.
*/
function checkCallbacks(start, arrayOfCallbacks) {
var arg, i = arrayOfCallbacks.length;
while (i > start) {
arg = arrayOfCallbacks[--i];
if (arg != null && typeof arg != 'function') throw new Error('callback is not a function');
}
}
/**
* No-Op function used in method replacement
* @private
*/
function noop() { }
slice = [].slice;
// ES5 reduce implementation if native not available
// See: http://es5.github.com/#x15.4.4.21 as there are many
// specifics and edge cases.
reduceArray = [].reduce ||
function (reduceFunc /*, initialValue */) {
// ES5 dictates that reduce.length === 1
// This implementation deviates from ES5 spec in the following ways:
// 1. It does not check if reduceFunc is a Callable
var arr, args, reduced, len, i;
i = 0;
arr = Object(this);
len = arr.length >>> 0;
args = arguments;
// If no initialValue, use first item of array (we know length !== 0 here)
// and adjust i to start at second item
if (args.length <= 1) {
// Skip to the first real element in the array
for (; ; ) {
if (i in arr) {
reduced = arr[i++];
break;
}
// If we reached the end of the array without finding any real
// elements, it's a TypeError
if (++i >= len) {
throw new TypeError();
}
}
} else {
// If initialValue provided, use it
reduced = args[1];
}
// Do the actual reduce
for (; i < len; ++i) {
// Skip holes
if (i in arr)
reduced = reduceFunc(reduced, arr[i], i, arr);
}
return reduced;
};
return when;
});
})(typeof define == 'function'
? define
: function (factory) {
typeof module != 'undefined'
? (module.exports = factory())
: (jQuery.mapster_when = factory());
}
// Boilerplate for AMD, Node, and browser global
);
/*lint-ignore-end*/
/* ImageMapster core */
/*jslint laxbreak: true, evil: true, unparam: true */
/*global jQuery: true, Zepto: true */
(function ($) {
// all public functions in $.mapster.impl are methods
$.fn.mapster = function (method) {
var m = $.mapster.impl;
if ($.isFunction(m[method])) {
return m[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return m.bind.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.mapster');
}
};
$.mapster = {
version: "1.2.10",
render_defaults: {
isSelectable: true,
isDeselectable: true,
fade: false,
fadeDuration: 150,
fill: true,
fillColor: '000000',
fillColorMask: 'FFFFFF',
fillOpacity: 0.7,
highlight: true,
stroke: false,
strokeColor: 'ff0000',
strokeOpacity: 1,
strokeWidth: 1,
includeKeys: '',
altImage: null,
altImageId: null, // used internally
altImages: {}
},
defaults: {
clickNavigate: false,
wrapClass: null,
wrapCss: null,
onGetList: null,
sortList: false,
listenToList: false,
mapKey: '',
mapValue: '',
singleSelect: false,
listKey: 'value',
listSelectedAttribute: 'selected',
listSelectedClass: null,
onClick: null,
onMouseover: null,
onMouseout: null,
mouseoutDelay: 0,
onStateChange: null,
boundList: null,
onConfigured: null,
configTimeout: 30000,
noHrefIsMask: true,
scaleMap: true,
safeLoad: false,
areas: []
},
shared_defaults: {
render_highlight: { fade: true },
render_select: { fade: false },
staticState: null,
selected: null
},
area_defaults:
{
includeKeys: '',
isMask: false
},
canvas_style: {
position: 'absolute',
left: 0,
top: 0,
padding: 0,
border: 0
},
hasCanvas: null,
isTouch: null,
map_cache: [],
hooks: {},
addHook: function(name,callback) {
this.hooks[name]=(this.hooks[name]||[]).push(callback);
},
callHooks: function(name,context) {
$.each(this.hooks[name]||[],function(i,e) {
e.apply(context);
});
},
utils: {
when: $.mapster_when,
defer: $.mapster_when.defer,
// extends the constructor, returns a new object prototype. Does not refer to the
// original constructor so is protected if the original object is altered. This way you
// can "extend" an object by replacing it with its subclass.
subclass: function(BaseClass, constr) {
var Subclass=function() {
var me=this,
args=Array.prototype.slice.call(arguments,0);
me.base = BaseClass.prototype;
me.base.init = function() {
BaseClass.prototype.constructor.apply(me,args);
};
constr.apply(me,args);
};
Subclass.prototype = new BaseClass();
Subclass.prototype.constructor=Subclass;
return Subclass;
},
asArray: function (obj) {
return obj.constructor === Array ?
obj : this.split(obj);
},
// clean split: no padding or empty elements
split: function (text,cb) {
var i,el, arr = text.split(',');
for (i = 0; i < arr.length; i++) {
el = $.trim(arr[i]);
if (el==='') {
arr.splice(i,1);
} else {
arr[i] = cb ? cb(el):el;
}
}
return arr;
},
// similar to $.extend but does not add properties (only updates), unless the
// first argument is an empty object, then all properties will be copied
updateProps: function (_target, _template) {
var onlyProps,
target = _target || {},
template = $.isEmptyObject(target) ? _template : _target;
//if (template) {
onlyProps = [];
$.each(template, function (prop) {
onlyProps.push(prop);
});
//}
$.each(Array.prototype.slice.call(arguments, 1), function (i, src) {
$.each(src || {}, function (prop) {
if (!onlyProps || $.inArray(prop, onlyProps) >= 0) {
var p = src[prop];
if ($.isPlainObject(p)) {
// not recursive - only copies 1 level of subobjects, and always merges
target[prop] = $.extend(target[prop] || {}, p);
} else if (p && p.constructor === Array) {
target[prop] = p.slice(0);
} else if (typeof p !== 'undefined') {
target[prop] = src[prop];
}
}
});
});
return target;
},
isElement: function (o) {
return (typeof HTMLElement === "object" ? o instanceof HTMLElement :
o && typeof o === "object" && o.nodeType === 1 && typeof o.nodeName === "string");
},
// finds element of array or object with a property "prop" having value "val"
// if prop is not defined, then just looks for property with value "val"
indexOfProp: function (obj, prop, val) {
var result = obj.constructor === Array ? -1 : null;
$.each(obj, function (i, e) {
if (e && (prop ? e[prop] : e) === val) {
result = i;
return false;
}
});
return result;
},
// returns "obj" if true or false, or "def" if not true/false
boolOrDefault: function (obj, def) {
return this.isBool(obj) ?
obj : def || false;
},
isBool: function (obj) {
return typeof obj === "boolean";
},
isUndef: function(obj) {
return typeof obj === "undefined";
},
// evaluates "obj", if function, calls it with args
// (todo - update this to handle variable lenght/more than one arg)
ifFunction: function (obj, that, args) {
if ($.isFunction(obj)) {
obj.call(that, args);
}
},
size: function(image, raw) {
var u=$.mapster.utils;
return {
width: raw ? (image.width || image.naturalWidth) : u.imgWidth(image,true) ,
height: raw ? (image.height || image.naturalHeight) : u.imgHeight(image,true),
complete: function() { return !!this.height && !!this.width;}
};
},
/**
* Set the opacity of the element. This is an IE<8 specific function for handling VML.
* When using VML we must override the "setOpacity" utility function (monkey patch ourselves).
* jQuery does not deal with opacity correctly for VML elements. This deals with that.
*
* @param {Element} el The DOM element
* @param {double} opacity A value between 0 and 1 inclusive.
*/
setOpacity: function (el, opacity) {
if ($.mapster.hasCanvas()) {
el.style.opacity = opacity;
} else {
$(el).each(function(i,e) {
if (typeof e.opacity !=='undefined') {
e.opacity=opacity;
} else {
$(e).css("opacity",opacity);
}
});
}
},
// fade "el" from opacity "op" to "endOp" over a period of time "duration"
fader: (function () {
var elements = {},
lastKey = 0,
fade_func = function (el, op, endOp, duration) {
var index,
cbIntervals = duration/15,
obj, u = $.mapster.utils;
if (typeof el === 'number') {
obj = elements[el];
if (!obj) {
return;
}
} else {
index = u.indexOfProp(elements, null, el);
if (index) {
delete elements[index];
}
elements[++lastKey] = obj = el;
el = lastKey;
}
endOp = endOp || 1;
op = (op + (endOp / cbIntervals) > endOp - 0.01) ? endOp : op + (endOp / cbIntervals);
u.setOpacity(obj, op);
if (op < endOp) {
setTimeout(function () {
fade_func(el, op, endOp, duration);
}, 15);
}
};
return fade_func;
} ())
},
getBoundList: function (opts, key_list) {
if (!opts.boundList) {
return null;
}
var index, key, result = $(), list = $.mapster.utils.split(key_list);
opts.boundList.each(function (i,e) {
for (index = 0; index < list.length; index++) {
key = list[index];
if ($(e).is('[' + opts.listKey + '="' + key + '"]')) {
result = result.add(e);
}
}
});
return result;
},
// Causes changes to the bound list based on the user action (select or deselect)
// area: the jQuery area object
// returns the matching elements from the bound list for the first area passed (normally only one should be passed, but
// a list can be passed
setBoundListProperties: function (opts, target, selected) {
target.each(function (i,e) {
if (opts.listSelectedClass) {
if (selected) {
$(e).addClass(opts.listSelectedClass);
} else {
$(e).removeClass(opts.listSelectedClass);
}
}
if (opts.listSelectedAttribute) {
$(e).attr(opts.listSelectedAttribute, selected);
}
});
},
getMapDataIndex: function (obj) {
var img, id;
switch (obj.tagName && obj.tagName.toLowerCase()) {
case 'area':
id = $(obj).parent().attr('name');
img = $("img[usemap='#" + id + "']")[0];
break;
case 'img':
img = obj;
break;
}
return img ?
this.utils.indexOfProp(this.map_cache, 'image', img) : -1;
},
getMapData: function (obj) {
var index = this.getMapDataIndex(obj.length ? obj[0]:obj);
if (index >= 0) {
return index >= 0 ? this.map_cache[index] : null;
}
},
/**
* Queue a command to be run after the active async operation has finished
* @param {MapData} map_data The target MapData object
* @param {jQuery} that jQuery object on which the command was invoked
* @param {string} command the ImageMapster method name
* @param {object[]} args arguments passed to the method
* @return {bool} true if the command was queued, false if not (e.g. there was no need to)
*/
queueCommand: function (map_data, that, command, args) {
if (!map_data) {
return false;
}
if (!map_data.complete || map_data.currentAction) {
map_data.commands.push(
{
that: that,
command: command,
args: args
});
return true;
}
return false;
},
unload: function () {
this.impl.unload();
this.utils = null;
this.impl = null;
$.fn.mapster = null;
$.mapster = null;
$('*').unbind();
}
};
// Config for object prototypes
// first: use only first object (for things that should not apply to lists)
/// calls back one of two fuinctions, depending on whether an area was obtained.
// opts: {
// name: 'method name',
// key: 'key,
// args: 'args'
//
//}
// name: name of method (required)
// args: arguments to re-call with
// Iterates through all the objects passed, and determines whether it's an area or an image, and calls the appropriate
// callback for each. If anything is returned from that callback, the process is stopped and that data return. Otherwise,
// the object itself is returned.
var m = $.mapster,
u = m.utils,
ap = Array.prototype;
// jQuery's width() and height() are broken on IE9 in some situations. This tries everything.
$.each(["width","height"],function(i,e) {
var capProp = e.substr(0,1).toUpperCase() + e.substr(1);
// when jqwidth parm is passed, it also checks the jQuery width()/height() property
// the issue is that jQUery width() can report a valid size before the image is loaded in some browsers
// without it, we can read zero even when image is loaded in other browsers if its not visible
// we must still check because stuff like adblock can temporarily block it
// what a goddamn headache
u["img"+capProp]=function(img,jqwidth) {
return (jqwidth ? $(img)[e]() : 0) ||
img[e] || img["natural"+capProp] || img["client"+capProp] || img["offset"+capProp];
};
});
/**
* The Method object encapsulates the process of testing an ImageMapster method to see if it's being
* invoked on an image, or an area; then queues the command if the MapData is in an active state.
*
* @param {[jQuery]} that The target of the invocation
* @param {[function]} func_map The callback if the target is an imagemap
* @param {[function]} func_area The callback if the target is an area
* @param {[object]} opt Options: { key: a map key if passed explicitly
* name: the command name, if it can be queued,
* args: arguments to the method
* }
*/
m.Method = function (that, func_map, func_area, opts) {
var me = this;
me.name = opts.name;
me.output = that;
me.input = that;
me.first = opts.first || false;
me.args = opts.args ? ap.slice.call(opts.args, 0) : [];
me.key = opts.key;
me.func_map = func_map;
me.func_area = func_area;
//$.extend(me, opts);
me.name = opts.name;
me.allowAsync = opts.allowAsync || false;
};
m.Method.prototype = {
constructor: m.Method,
go: function () {
var i, data, ar, len, result, src = this.input,
area_list = [],
me = this;
len = src.length;
for (i = 0; i < len; i++) {
data = $.mapster.getMapData(src[i]);
if (data) {
if (!me.allowAsync && m.queueCommand(data, me.input, me.name, me.args)) {
if (this.first) {
result = '';
}
continue;
}
ar = data.getData(src[i].nodeName === 'AREA' ? src[i] : this.key);
if (ar) {
if ($.inArray(ar, area_list) < 0) {
area_list.push(ar);
}
} else {
result = this.func_map.apply(data, me.args);
}
if (this.first || typeof result !== 'undefined') {
break;
}
}
}
// if there were areas, call the area function for each unique group
$(area_list).each(function (i,e) {
result = me.func_area.apply(e, me.args);
});
if (typeof result !== 'undefined') {
return result;
} else {
return this.output;
}
}
};
$.mapster.impl = (function () {
var me = {},
addMap= function (map_data) {
return m.map_cache.push(map_data) - 1;
},
removeMap = function (map_data) {
m.map_cache.splice(map_data.index, 1);
for (var i = m.map_cache.length - 1; i >= this.index; i--) {
m.map_cache[i].index--;
}
};
/**
* Test whether the browser supports VML. Credit: google.
* http://stackoverflow.com/questions/654112/how-do-you-detect-support-for-vml-or-svg-in-a-browser
*
* @return {bool} true if vml is supported, false if not
*/
function hasVml() {
var a = $('
').appendTo('body');
a.html('');
var b = a[0].firstChild;
b.style.behavior = "url(#default#VML)";
var has = b ? typeof b.adj === "object" : true;
a.remove();
return has;
}
/**
* Return a reference to the IE namespaces object, if available, or an empty object otherwise
* @return {obkect} The document.namespaces object.
*/
function namespaces() {
return typeof(document.namespaces)==='object' ?
document.namespaces :
null;
}
/**
* Test for the presence of HTML5 Canvas support. This also checks to see if excanvas.js has been
* loaded and is faking it; if so, we assume that canvas is not supported.
*
* @return {bool} true if HTML5 canvas support, false if not
*/
function hasCanvas() {
var d = namespaces();
// when g_vml_ is present, then we can be sure excanvas is active, meaning there's not a real canvas.
return d && d.g_vml_ ?
false :
$('')[0].getContext ?
true :
false;
}
/**
* Merge new area data into existing area options on a MapData object. Used for rebinding.
*
* @param {[MapData]} map_data The MapData object
* @param {[object[]]} areas areas array to merge
*/
function merge_areas(map_data, areas) {
var ar, index,
map_areas = map_data.options.areas;
if (areas) {
$.each(areas, function (i, e) {
// Issue #68 - ignore invalid data in areas array
if (!e || !e.key) {
return;
}
index = u.indexOfProp(map_areas, "key", e.key);
if (index >= 0) {
$.extend(map_areas[index], e);
}
else {
map_areas.push(e);
}
ar = map_data.getDataForKey(e.key);
if (ar) {
$.extend(ar.options, e);
}
});
}
}
function merge_options(map_data, options) {
var temp_opts = u.updateProps({}, options);
delete temp_opts.areas;
u.updateProps(map_data.options, temp_opts);
merge_areas(map_data, options.areas);
// refresh the area_option template
u.updateProps(map_data.area_options, map_data.options);
}
// Most methods use the "Method" object which handles figuring out whether it's an image or area called and
// parsing key parameters. The constructor wants:
// this, the jQuery object
// a function that is called when an image was passed (with a this context of the MapData)
// a function that is called when an area was passed (with a this context of the AreaData)
// options: first = true means only the first member of a jQuery object is handled
// key = the key parameters passed
// defaultReturn: a value to return other than the jQuery object (if its not chainable)
// args: the arguments
// Returns a comma-separated list of user-selected areas. "staticState" areas are not considered selected for the purposes of this method.
me.get = function (key) {
var md = m.getMapData(this);
if (!(md && md.complete)) {
throw("Can't access data until binding complete.");
}
return (new m.Method(this,
function () {
// map_data return
return this.getSelected();
},
function () {
return this.isSelected();
},
{ name: 'get',
args: arguments,
key: key,
first: true,
allowAsync: true,
defaultReturn: ''
}
)).go();
};
me.data = function (key) {
return (new m.Method(this,
null,
function () {
return this;
},
{ name: 'data',
args: arguments,
key: key
}
)).go();
};
// Set or return highlight state.
// $(img).mapster('highlight') -- return highlighted area key, or null if none
// $(area).mapster('highlight') -- highlight an area
// $(img).mapster('highlight','area_key') -- highlight an area
// $(img).mapster('highlight',false) -- remove highlight
me.highlight = function (key) {
return (new m.Method(this,
function () {
if (key === false) {
this.ensureNoHighlight();
} else {
var id = this.highlightId;
return id >= 0 ? this.data[id].key : null;
}
},
function () {
this.highlight();
},
{ name: 'highlight',
args: arguments,
key: key,
first: true
}
)).go();
};
// Return the primary keys for an area or group key.
// $(area).mapster('key')
// includes all keys (not just primary keys)
// $(area).mapster('key',true)
// $(img).mapster('key','group-key')
// $(img).mapster('key','group-key', true)
me.keys = function(key,all) {
var keyList=[],
md = m.getMapData(this);
if (!(md && md.complete)) {
throw("Can't access data until binding complete.");
}
function addUniqueKeys(ad) {
var areas,keys=[];
if (!all) {
keys.push(ad.key);
} else {
areas=ad.areas();
$.each(areas,function(i,e) {
keys=keys.concat(e.keys);
});
}
$.each(keys,function(i,e) {
if ($.inArray(e,keyList)<0) {
keyList.push(e);
}
});
}
if (!(md && md.complete)) {
return '';
}
if (typeof key === 'string') {
if (all) {
addUniqueKeys(md.getDataForKey(key));
} else {
keyList=[md.getKeysForGroup(key)];
}
} else {
all = key;
this.each(function(i,e) {
if (e.nodeName==='AREA') {
addUniqueKeys(md.getDataForArea(e));
}
});
}
return keyList.join(',');
};
me.select = function () {
me.set.call(this, true);
};
me.deselect = function () {
me.set.call(this, false);
};
/**
* Select or unselect areas. Areas can be identified by a single string key, a comma-separated list of keys,
* or an array of strings.
*
*
* @param {boolean} selected Determines whether areas are selected or deselected
* @param {string|string[]} key A string, comma-separated string, or array of strings indicating
* the areas to select or deselect
* @param {object} options Rendering options to apply when selecting an area
*/
me.set = function (selected, key, options) {
var lastMap, map_data, opts=options,
key_list, area_list; // array of unique areas passed
function setSelection(ar) {
if (ar) {
switch (selected) {
case true:
ar.select(opts); break;
case false:
ar.deselect(true); break;
default:
ar.toggle(opts); break;
}
}
}
function addArea(ar) {
if (ar && $.inArray(ar, area_list) < 0) {
area_list.push(ar);
key_list+=(key_list===''?'':',')+ar.key;
}
}
// Clean up after a group that applied to the same map
function finishSetForMap(map_data) {
$.each(area_list, function (i, el) {
setSelection(el);
});
if (!selected) {
map_data.removeSelectionFinish();
}
if (map_data.options.boundList) {
m.setBoundListProperties(map_data.options, m.getBoundList(map_data.options, key_list), selected);
}
}
this.filter('img,area').each(function (i,e) {
var keys;
map_data = m.getMapData(e);
if (map_data !== lastMap) {
if (lastMap) {
finishSetForMap(lastMap);
}
area_list = [];
key_list='';
}
if (map_data) {
keys = '';
if (e.nodeName.toUpperCase()==='IMG') {
if (!m.queueCommand(map_data, $(e), 'set', [selected, key, opts])) {
if (key instanceof Array) {
if (key.length) {
keys = key.join(",");
}
}
else {
keys = key;
}
if (keys) {
$.each(u.split(keys), function (i,key) {
addArea(map_data.getDataForKey(key.toString()));
lastMap = map_data;
});
}
}
} else {
opts=key;
if (!m.queueCommand(map_data, $(e), 'set', [selected, opts])) {
addArea(map_data.getDataForArea(e));
lastMap = map_data;
}
}
}
});
if (map_data) {
finishSetForMap(map_data);
}
return this;
};
me.unbind = function (preserveState) {
return (new m.Method(this,
function () {
this.clearEvents();
this.clearMapData(preserveState);
removeMap(this);
},
null,
{ name: 'unbind',
args: arguments
}
)).go();
};
// refresh options and update selection information.
me.rebind = function (options) {
return (new m.Method(this,
function () {
var me=this;
me.complete=false;
me.configureOptions(options);
me.bindImages().then(function() {
me.buildDataset(true);
me.complete=true;
});
//this.redrawSelections();
},
null,
{
name: 'rebind',
args: arguments
}
)).go();
};
// get options. nothing or false to get, or "true" to get effective options (versus passed options)
me.get_options = function (key, effective) {
var eff = u.isBool(key) ? key : effective; // allow 2nd parm as "effective" when no key
return (new m.Method(this,
function () {
var opts = $.extend({}, this.options);
if (eff) {
opts.render_select = u.updateProps(
{},
m.render_defaults,
opts,
opts.render_select);
opts.render_highlight = u.updateProps(
{},
m.render_defaults,
opts,
opts.render_highlight);
}
return opts;
},
function () {
return eff ? this.effectiveOptions() : this.options;
},
{
name: 'get_options',
args: arguments,
first: true,
allowAsync: true,
key: key
}
)).go();
};
// set options - pass an object with options to set,
me.set_options = function (options) {
return (new m.Method(this,
function () {
merge_options(this, options);
},
null,
{
name: 'set_options',
args: arguments
}
)).go();
};
me.unload = function () {
var i;
for (i = m.map_cache.length - 1; i >= 0; i--) {
if (m.map_cache[i]) {
me.unbind.call($(m.map_cache[i].image));
}
}
me.graphics = null;
};
me.snapshot = function () {
return (new m.Method(this,
function () {
$.each(this.data, function (i, e) {
e.selected = false;
});
this.base_canvas = this.graphics.createVisibleCanvas(this);
$(this.image).before(this.base_canvas);
},
null,
{ name: 'snapshot' }
)).go();
};
// do not queue this function
me.state = function () {
var md, result = null;
$(this).each(function (i,e) {
if (e.nodeName === 'IMG') {
md = m.getMapData(e);
if (md) {
result = md.state();
}
return false;
}
});
return result;
};
me.bind = function (options) {
return this.each(function (i,e) {
var img, map, usemap, md;
// save ref to this image even if we can't access it yet. commands will be queued
img = $(e);
md = m.getMapData(e);
// if already bound completely, do a total rebind
if (md) {
me.unbind.apply(img);
if (!md.complete) {
// will be queued
img.bind();
return true;
}
md = null;
}
// ensure it's a valid image
// jQuery bug with Opera, results in full-url#usemap being returned from jQuery's attr.
// So use raw getAttribute instead.
usemap = this.getAttribute('usemap');
map = usemap && $('map[name="' + usemap.substr(1) + '"]');
if (!(img.is('img') && usemap && map.size() > 0)) {
return true;
}
// sorry - your image must have border:0, things are too unpredictable otherwise.
img.css('border', 0);
if (!md) {
md = new m.MapData(this, options);
md.index = addMap(md);
md.map = map;
md.bindImages().then(function() {
md.initialize();
});
}
});
};
me.init = function (useCanvas) {
var style, shapes;
// for testing/debugging, use of canvas can be forced by initializing
// manually with "true" or "false". But generally we test for it.
m.hasCanvas = function() {
if (!u.isBool(m.hasCanvas.value)) {
m.hasCanvas.value = u.isBool(useCanvas) ?
useCanvas :
hasCanvas();
}
return m.hasCanvas.value;
};
m.hasVml = function() {
if (!u.isBool(m.hasVml.value)) {
// initialize VML the first time we detect its presence.
var d = namespaces();
if (d && !d.v) {
d.add("v", "urn:schemas-microsoft-com:vml");
style = document.createStyleSheet();
shapes = ['shape', 'rect', 'oval', 'circ', 'fill', 'stroke', 'imagedata', 'group', 'textbox'];
$.each(shapes,
function (i, el) {
style.addRule('v\\:' + el, "behavior: url(#default#VML); antialias:true");
});
}
m.hasVml.value = hasVml();
}
return m.hasVml.value;
};
m.isTouch = !!document.documentElement.ontouchstart;
$.extend(m.defaults, m.render_defaults,m.shared_defaults);
$.extend(m.area_defaults, m.render_defaults,m.shared_defaults);
};
me.test = function (obj) {
return eval(obj);
};
return me;
} ());
$.mapster.impl.init();
} (jQuery));
/* graphics.js
Graphics object handles all rendering.
*/
(function ($) {
var p, m=$.mapster,
u=m.utils,
canvasMethods,
vmlMethods;
/**
* Implemenation to add each area in an AreaData object to the canvas
* @param {Graphics} graphics The target graphics object
* @param {AreaData} areaData The AreaData object (a collection of area elements and metadata)
* @param {object} options Rendering options to apply when rendering this group of areas
*/
function addShapeGroupImpl(graphics, areaData, options) {
var me = graphics,
md = me.map_data,
isMask = options.isMask;
// first get area options. Then override fade for selecting, and finally merge in the
// "select" effect options.
$.each(areaData.areas(), function (i,e) {
options.isMask = isMask || (e.nohref && md.options.noHrefIsMask);
me.addShape(e, options);
});
// it's faster just to manipulate the passed options isMask property and restore it, than to
// copy the object each time
options.isMask=isMask;
}
/**
* Convert a hex value to decimal
* @param {string} hex A hexadecimal toString
* @return {int} Integer represenation of the hex string
*/
function hex_to_decimal(hex) {
return Math.max(0, Math.min(parseInt(hex, 16), 255));
}
function css3color(color, opacity) {
return 'rgba(' + hex_to_decimal(color.substr(0, 2)) + ','
+ hex_to_decimal(color.substr(2, 2)) + ','
+ hex_to_decimal(color.substr(4, 2)) + ',' + opacity + ')';
}
/**
* An object associated with a particular map_data instance to manage renderin.
* @param {MapData} map_data The MapData object bound to this instance
*/
m.Graphics = function (map_data) {
//$(window).unload($.mapster.unload);
// create graphics functions for canvas and vml browsers. usage:
// 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
// 3) call add_shape_to for each shape or mask, 4) call render() to finish
var me = this;
me.active = false;
me.canvas = null;
me.width = 0;
me.height = 0;
me.shapes = [];
me.masks = [];
me.map_data = map_data;
};
p = m.Graphics.prototype= {
constructor: m.Graphics,
/**
* Initiate a graphics request for a canvas
* @param {Element} canvas The canvas element that is the target of this operation
* @param {string} [elementName] The name to assign to the element (VML only)
*/
begin: function(canvas, elementName) {
var c = $(canvas);
this.elementName = elementName;
this.canvas = canvas;
this.width = c.width();
this.height = c.height();
this.shapes = [];
this.masks = [];
this.active = true;
},
/**
* Add an area to be rendered to this canvas.
* @param {MapArea} mapArea The MapArea object to render
* @param {object} options An object containing any rendering options that should override the
* defaults for the area
*/
addShape: function(mapArea, options) {
var addto = options.isMask ? this.masks : this.shapes;
addto.push({ mapArea: mapArea, options: options });
},
/**
* Create a canvas that is sized and styled for the MapData object
* @param {MapData} mapData The MapData object that will receive this new canvas
* @return {Element} A canvas element
*/
createVisibleCanvas: function (mapData) {
return $(this.createCanvasFor(mapData))
.addClass('mapster_el')
.css(m.canvas_style)[0];
},
/**
* Add a group of shapes from an AreaData object to the canvas
*
* @param {AreaData} areaData An AreaData object (a set of area elements)
* @param {string} mode The rendering mode, "select" or "highlight". This determines the target
* canvas and which default options to use.
* @param {striong} options Rendering options
*/
addShapeGroup: function (areaData, mode,options) {
// render includeKeys first - because they could be masks
var me = this,
list, name, canvas,
map_data = this.map_data,
opts = areaData.effectiveRenderOptions(mode);
if (options) {
$.extend(opts,options);
}
if (mode === 'select') {
name = "static_" + areaData.areaId.toString();
canvas = map_data.base_canvas;
} else {
canvas = map_data.overlay_canvas;
}
me.begin(canvas, name);
if (opts.includeKeys) {
list = u.split(opts.includeKeys);
$.each(list, function (i,e) {
var areaData = map_data.getDataForKey(e.toString());
addShapeGroupImpl(me,areaData, areaData.effectiveRenderOptions(mode));
});
}
addShapeGroupImpl(me,areaData, opts);
me.render();
if (opts.fade) {
// fading requires special handling for IE. We must access the fill elements directly. The fader also has to deal with
// the "opacity" attribute (not css)
u.fader(m.hasCanvas() ?
canvas :
$(canvas).find('._fill').not('.mapster_mask'),
0,
m.hasCanvas() ?
1 :
opts.fillOpacity,
opts.fadeDuration);
}
}
// These prototype methods are implementation dependent
};
function noop() {}
// configure remaining prototype methods for ie or canvas-supporting browser
canvasMethods = {
renderShape: function (context, mapArea, offset) {
var i,
c = mapArea.coords(null,offset);
switch (mapArea.shape) {
case 'rect':
context.rect(c[0], c[1], c[2] - c[0], c[3] - c[1]);
break;
case 'poly':
context.moveTo(c[0], c[1]);
for (i = 2; i < mapArea.length; i += 2) {
context.lineTo(c[i], c[i + 1]);
}
context.lineTo(c[0], c[1]);
break;
case 'circ':
case 'circle':
context.arc(c[0], c[1], c[2], 0, Math.PI * 2, false);
break;
}
},
addAltImage: function (context, image, mapArea, options) {
context.beginPath();
this.renderShape(context, mapArea);
context.closePath();
context.clip();
context.globalAlpha = options.altImageOpacity || options.fillOpacity;
context.drawImage(image, 0, 0, mapArea.owner.scaleInfo.width, mapArea.owner.scaleInfo.height);
},
render: function () {
// firefox 6.0 context.save() seems to be broken. to work around, we have to draw the contents on one temp canvas,
// the mask on another, and merge everything. ugh. fixed in 1.2.2. unfortunately this is a lot more code for masks,
// but no other way around it that i can see.
var maskCanvas, maskContext,
me = this,
md = me.map_data,
hasMasks = me.masks.length,
shapeCanvas = me.createCanvasFor(md),
shapeContext = shapeCanvas.getContext('2d'),
context = me.canvas.getContext('2d');
if (hasMasks) {
maskCanvas = me.createCanvasFor(md);
maskContext = maskCanvas.getContext('2d');
maskContext.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
$.each(me.masks, function (i,e) {
maskContext.save();
maskContext.beginPath();
me.renderShape(maskContext, e.mapArea);
maskContext.closePath();
maskContext.clip();
maskContext.lineWidth = 0;
maskContext.fillStyle = '#000';
maskContext.fill();
maskContext.restore();
});
}
$.each(me.shapes, function (i,s) {
shapeContext.save();
if (s.options.fill) {
if (s.options.altImageId) {
me.addAltImage(shapeContext, md.images[s.options.altImageId], s.mapArea, s.options);
} else {
shapeContext.beginPath();
me.renderShape(shapeContext, s.mapArea);
shapeContext.closePath();
//shapeContext.clip();
shapeContext.fillStyle = css3color(s.options.fillColor, s.options.fillOpacity);
shapeContext.fill();
}
}
shapeContext.restore();
});
// render strokes at end since masks get stroked too
$.each(me.shapes.concat(me.masks), function (i,s) {
var offset = s.options.strokeWidth === 1 ? 0.5 : 0;
// offset applies only when stroke width is 1 and stroke would render between pixels.
if (s.options.stroke) {
shapeContext.save();
shapeContext.strokeStyle = css3color(s.options.strokeColor, s.options.strokeOpacity);
shapeContext.lineWidth = s.options.strokeWidth;
shapeContext.beginPath();
me.renderShape(shapeContext, s.mapArea, offset);
shapeContext.closePath();
shapeContext.stroke();
shapeContext.restore();
}
});
if (hasMasks) {
// render the new shapes against the mask
maskContext.globalCompositeOperation = "source-out";
maskContext.drawImage(shapeCanvas, 0, 0);
// flatten into the main canvas
context.drawImage(maskCanvas, 0, 0);
} else {
context.drawImage(shapeCanvas, 0, 0);
}
me.active = false;
return me.canvas;
},
// create a canvas mimicing dimensions of an existing element
createCanvasFor: function (md) {
return $('')[0];
},
clearHighlight: function () {
var c = this.map_data.overlay_canvas;
c.getContext('2d').clearRect(0, 0, c.width, c.height);
},
// 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.
refreshSelections: function () {
var canvas_temp, map_data = this.map_data;
// draw new base canvas, then swap with the old one to avoid flickering
canvas_temp = map_data.base_canvas;
map_data.base_canvas = this.createVisibleCanvas(map_data);
$(map_data.base_canvas).hide();
$(canvas_temp).before(map_data.base_canvas);
map_data.redrawSelections();
$(map_data.base_canvas).show();
$(canvas_temp).remove();
}
};
vmlMethods = {
renderShape: function (mapArea, options, cssclass) {
var me = this, fill,stroke, e, t_fill, el_name, el_class, template, c = mapArea.coords();
el_name = me.elementName ? 'name="' + me.elementName + '" ' : '';
el_class = cssclass ? 'class="' + cssclass + '" ' : '';
t_fill = '';
stroke = options.stroke ?
' strokeweight=' + options.strokeWidth + ' stroked="t" strokecolor="#' +
options.strokeColor + '"' :
' stroked="f"';
fill = options.fill ?
' filled="t"' :
' filled="f"';
switch (mapArea.shape) {
case 'rect':
template = '' + t_fill + '';
break;
case 'poly':
template = '' + t_fill + '';
break;
case 'circ':
case 'circle':
template = '' + t_fill + '';
break;
}
e = $(template);
$(me.canvas).append(e);
return e;
},
render: function () {
var opts, me = this;
$.each(this.shapes, function (i,e) {
me.renderShape(e.mapArea, e.options);
});
if (this.masks.length) {
$.each(this.masks, function (i,e) {
opts = u.updateProps({},
e.options, {
fillOpacity: 1,
fillColor: e.options.fillColorMask
});
me.renderShape(e.mapArea, opts, 'mapster_mask');
});
}
this.active = false;
return this.canvas;
},
createCanvasFor: function (md) {
var w = md.scaleInfo.width,
h = md.scaleInfo.height;
return $('')[0];
},
clearHighlight: function () {
$(this.map_data.overlay_canvas).children().remove();
},
// remove single or all selections
removeSelections: function (area_id) {
if (area_id >= 0) {
$(this.map_data.base_canvas).find('[name="static_' + area_id.toString() + '"]').remove();
}
else {
$(this.map_data.base_canvas).children().remove();
}
}
};
// for all methods with two implemenatations, add a function that will automatically replace itself with the correct
// method on first invocation
$.each(['renderShape',
'addAltImage',
'render',
'createCanvasFor',
'clearHighlight',
'removeSelections',
'refreshSelections'],
function(i,e) {
p[e]=(function(method) {
return function() {
p[method] = (m.hasCanvas() ?
canvasMethods[method] :
vmlMethods[method]) || noop;
return p[method].apply(this,arguments);
};
}(e));
});
} (jQuery));
/* mapimage.js
the MapImage object, repesents an instance of a single bound imagemap
*/
(function ($) {
var m = $.mapster,
u = m.utils,
ap=[];
/**
* An object encapsulating all the images used by a MapData.
*/
m.MapImages = function(owner) {
this.owner = owner;
this.clear();
};
m.MapImages.prototype = {
constructor: m.MapImages,
/* interface to make this array-like */
slice: function() {
return ap.slice.apply(this,arguments);
},
splice: function() {
ap.slice.apply(this.status,arguments);
var result= ap.slice.apply(this,arguments);
return result;
},
/**
* a boolean value indicates whether all images are done loading
* @return {bool} true when all are done
*/
complete: function() {
return $.inArray(false, this.status) < 0;
},
/**
* Save an image in the images array and return its index
* @param {Image} image An Image object
* @return {int} the index of the image
*/
_add: function(image) {
var index = ap.push.call(this,image)-1;
this.status[index] = false;
return index;
},
/**
* Return the index of an Image within the images array
* @param {Image} img An Image
* @return {int} the index within the array, or -1 if it was not found
*/
indexOf: function(image) {
return $.inArray(image, this);
},
/**
* Clear this object and reset it to its initial state after binding.
*/
clear: function() {
var me=this;
if (me.ids && me.ids.length>0) {
$.each(me.ids,function(i,e) {
delete me[e];
});
}
/**
* A list of the cross-reference IDs bound to this object
* @type {string[]}
*/
me.ids=[];
/**
* Length property for array-like behavior, set to zero when initializing. Array prototype
* methods will update it after that.
*
* @type {int}
*/
me.length=0;
/**
* the loaded status of the corresponding image
* @type {boolean[]}
*/
me.status=[];
// actually erase the images
me.splice(0);
},
/**
* Bind an image to the map and add it to the queue to be loaded; return an ID that
* can be used to reference the
*
* @param {Image|string} image An Image object or a URL to an image
* @param {string} [id] An id to refer to this image
* @returns {int} an ID referencing the index of the image object in
* map_data.images
*/
add: function(image,id) {
var index,src,me = this;
if (!image) { return; }
if (typeof image === 'string') {
src = image;
image = me[src];
if (typeof image==='object') {
return me.indexOf(image);
}
image = $('
')
.addClass('mapster_el')
.hide();
index=me._add(image[0]);
image
.bind('load',function(e) {
me.imageLoaded.call(me,e);
})
.bind('error',function(e) {
me.imageLoadError.call(me,e);
});
image.attr('src', src);
} else {
// use attr because we want the actual source, not the resolved path the browser will return directly calling image.src
index=me._add($(image)[0]);
}
if (id) {
if (this[id]) {
throw(id+" is already used or is not available as an altImage alias.");
}
me.ids.push(id);
me[id]=me[index];
}
return index;
},
/**
* Bind the images in this object,
* @param {boolean} retry when true, indicates that the function is calling itself after failure
* @return {Promise} a promise that resolves when the images have finished loading
*/
bind: function(retry) {
var me = this,
promise,
triesLeft = me.owner.options.configTimeout / 200,
/* A recursive function to continue checking that the images have been
loaded until a timeout has elapsed */
check=function() {
var i;
// refresh status of images
i=me.length;
while (i-->0) {
if (!me.isLoaded(i)) {
break;
}
}
// check to see if every image has already been loaded
if (me.complete()) {
me.resolve();
} else {
// to account for failure of onLoad to fire in rare situations
if (triesLeft-- > 0) {
me.imgTimeout=window.setTimeout(function() {
check.call(me,true);
}, 50);
} else {
me.imageLoadError.call(me);
}
}
};
promise = me.deferred=u.defer();
check();
return promise;
},
resolve: function() {
var me=this,
resolver=me.deferred;
if (resolver) {
// Make a copy of the resolver before calling & removing it to ensure
// it is not called twice
me.deferred=null;
resolver.resolve();
}
},
/**
* Event handler for image onload
* @param {object} e jQuery event data
*/
imageLoaded: function(e) {
var me=this,
index = me.indexOf(e.target);
if (index>=0) {
me.status[index] = true;
if ($.inArray(false, me.status) < 0) {
me.resolve();
}
}
},
/**
* Event handler for onload error
* @param {object} e jQuery event data
*/
imageLoadError: function(e) {
clearTimeout(this.imgTimeout);
this.triesLeft=0;
var err = e ? 'The image ' + e.target.src + ' failed to load.' :
'The images never seemed to finish loading. You may just need to increase the configTimeout if images could take a long time to load.';
throw err;
},
/**
* Test if the image at specificed index has finished loading
* @param {int} index The image index
* @return {boolean} true if loaded, false if not
*/
isLoaded: function(index) {
var img,
me=this,
status=me.status;
if (status[index]) { return true; }
img = me[index];
if (typeof img.complete !== 'undefined') {
status[index]=img.complete;
} else {
status[index]=!!u.imgWidth(img);
}
// if complete passes, the image is loaded, but may STILL not be available because of stuff like adblock.
// make sure it is.
return status[index];
}
};
} (jQuery));
/* mapdata.js
the MapData object, repesents an instance of a single bound imagemap
*/
(function ($) {
var m = $.mapster,
u = m.utils;
/**
* Set default values for MapData object properties
* @param {MapData} me The MapData object
*/
function initializeDefaults(me) {
$.extend(me,{
complete: false, // (bool) when configuration is complete
map: null, // ($) the image map
base_canvas: null, // (canvas|var) where selections are rendered
overlay_canvas: null, // (canvas|var) where highlights are rendered
commands: [], // {} commands that were run before configuration was completed (b/c images weren't loaded)
data: [], // MapData[] area groups
mapAreas: [], // MapArea[] list. AreaData entities contain refs to this array, so options are stored with each.
_xref: {}, // (int) xref of mapKeys to data[]
highlightId: -1, // (int) the currently highlighted element.
currentAreaId: -1,
_tooltip_events: [], // {} info on events we bound to a tooltip container, so we can properly unbind them
scaleInfo: null, // {} info about the image size, scaling, defaults
index: -1, // index of this in map_cache - so we have an ID to use for wraper div
activeAreaEvent: null
});
}
/**
* Return an array of all image-containing options from an options object;
* that is, containers that may have an "altImage" property
*
* @param {object} obj An options object
* @return {object[]} An array of objects
*/
function getOptionImages(obj) {
return [obj, obj.render_highlight, obj.render_select];
}
/**
* Parse all the altImage references, adding them to the library so they can be preloaded
* and aliased.
*
* @param {MapData} me The MapData object on which to operate
*/
function configureAltImages(me)
{
var opts = me.options,
mi = me.images;
// add alt images
if (m.hasCanvas()) {
// map altImage library first
$.each(opts.altImages || {}, function(i,e) {
mi.add(e,i);
});
// now find everything else
$.each([opts].concat(opts.areas),function(i,e) {
$.each(getOptionImages(e),function(i2,e2) {
if (e2 && e2.altImage) {
e2.altImageId=mi.add(e2.altImage);
}
});
});
}
// set area_options
me.area_options = u.updateProps({}, // default options for any MapArea
m.area_defaults,
opts);
}
/**
* Queue a mouse move action based on current delay settings
* (helper for mouseover/mouseout handlers)
*
* @param {MapData} me The MapData context
* @param {number} delay The number of milliseconds to delay the action
* @param {AreaData} area AreaData affected
* @param {Deferred} deferred A deferred object to return (instead of a new one)
* @return {Promise} A promise that resolves when the action is completed
*/
function queueMouseEvent(me,delay,area, deferred) {
deferred = deferred || u.when.defer();
function cbFinal(areaId) {
if (me.currentAreaId!==areaId && me.highlightId>=0) {
deferred.resolve();
}
}
if (me.activeAreaEvent) {
window.clearTimeout(me.activeAreaEvent);
me.activeAreaEvent=0;
}
if (delay<0) {
return;
}
if (area.owner.currentAction || delay) {
me.activeAreaEvent = window.setTimeout((function() {
return function() {
queueMouseEvent(me,0,area,deferred);
};
}(area)),
delay || 100);
} else {
cbFinal(area.areaId);
}
return deferred;
}
/**
* Mousedown event. This is captured only to prevent browser from drawing an outline around an
* area when it's clicked.
*
* @param {EventData} e jQuery event data
*/
function mousedown(e) {
if (!m.hasCanvas()) {
this.blur();
}
e.preventDefault();
}
/**
* Mouseover event. Handle highlight rendering and client callback on mouseover
*
* @param {MapData} me The MapData context
* @param {EventData} e jQuery event data
* @return {[type]} [description]
*/
function mouseover(me,e) {
var arData = me.getAllDataForArea(this),
ar=arData.length ? arData[0] : null;
// mouseover events are ignored entirely while resizing, though we do care about mouseout events
// and must queue the action to keep things clean.
if (!ar || ar.isNotRendered() || ar.owner.currentAction) {
return;
}
if (me.currentAreaId === ar.areaId) {
return;
}
if (me.highlightId !== ar.areaId) {
me.clearEffects();
ar.highlight();
if (me.options.showToolTip) {
$.each(arData,function(i,e) {
if (e.effectiveOptions().toolTip) {
e.showToolTip();
}
});
}
}
me.currentAreaId = ar.areaId;
if ($.isFunction(me.options.onMouseover)) {
me.options.onMouseover.call(this,
{
e: e,
options:ar.effectiveOptions(),
key: ar.key,
selected: ar.isSelected()
});
}
}
/**
* Mouseout event.
*
* @param {MapData} me The MapData context
* @param {EventData} e jQuery event data
* @return {[type]} [description]
*/
function mouseout(me,e) {
var newArea,
ar = me.getDataForArea(this),
opts = me.options;
if (me.currentAreaId<0 || !ar) {
return;
}
newArea=me.getDataForArea(e.relatedTarget);
if (newArea === ar) {
return;
}
me.currentAreaId = -1;
ar.area=null;
queueMouseEvent(me,opts.mouseoutDelay,ar)
.then(me.clearEffects);
if ($.isFunction(opts.onMouseout)) {
opts.onMouseout.call(this,
{
e: e,
options: opts,
key: ar.key,
selected: ar.isSelected()
});
}
}
/**
* Clear any active tooltip or highlight
*
* @param {MapData} me The MapData context
* @param {EventData} e jQuery event data
* @return {[type]} [description]
*/
function clearEffects(me) {
var opts = me.options;
me.ensureNoHighlight();
if (opts.toolTipClose
&& $.inArray('area-mouseout', opts.toolTipClose) >= 0
&& me.activeToolTip)
{
me.clearToolTip();
}
}
/**
* Mouse click event handler
*
* @param {MapData} me The MapData context
* @param {EventData} e jQuery event data
* @return {[type]} [description]
*/
function click(me,e) {
var selected, list, list_target, newSelectionState, canChangeState, cbResult,
that = this,
ar = me.getDataForArea(this),
opts = me.options;
function clickArea(ar) {
var areaOpts,target;
canChangeState = (ar.isSelectable() &&
(ar.isDeselectable() || !ar.isSelected()));
if (canChangeState) {
newSelectionState = !ar.isSelected();
} else {
newSelectionState = ar.isSelected();
}
list_target = m.getBoundList(opts, ar.key);
if ($.isFunction(opts.onClick))
{
cbResult= opts.onClick.call(that,
{
e: e,
listTarget: list_target,
key: ar.key,
selected: newSelectionState
});
if (u.isBool(cbResult)) {
if (!cbResult) {
return false;
}
target = $(ar.area).attr('href');
if (target!=='#') {
window.location.href=target;
return false;
}
}
}
if (canChangeState) {
selected = ar.toggle();
}
if (opts.boundList && opts.boundList.length > 0) {
m.setBoundListProperties(opts, list_target, ar.isSelected());
}
areaOpts = ar.effectiveOptions();
if (areaOpts.includeKeys) {
list = u.split(areaOpts.includeKeys);
$.each(list, function (i, e) {
var ar = me.getDataForKey(e.toString());
if (!ar.options.isMask) {
clickArea(ar);
}
});
}
}
mousedown.call(this,e);
if (opts.clickNavigate && ar.href) {
window.location.href=ar.href;
return;
}
if (ar && !ar.owner.currentAction) {
opts = me.options;
clickArea(ar);
}
}
/**
* Prototype for a MapData object, representing an ImageMapster bound object
* @param {Element} image an IMG element
* @param {object} options ImageMapster binding options
*/
m.MapData = function (image, options)
{
var me = this;
// (Image) main map image
me.image = image;
me.images = new m.MapImages(me);
me.graphics = new m.Graphics(me);
// save the initial style of the image for unbinding. This is problematic, chrome
// duplicates styles when assigning, and cssText is apparently not universally supported.
// Need to do something more robust to make unbinding work universally.
me.imgCssText = image.style.cssText || null;
initializeDefaults(me);
me.configureOptions(options);
// create context-bound event handlers from our private functions
me.mouseover = function(e) { mouseover.call(this,me,e); };
me.mouseout = function(e) { mouseout.call(this,me,e); };
me.click = function(e) { click.call(this,me,e); };
me.clearEffects = function(e) { clearEffects.call(this,me,e); };
};
m.MapData.prototype = {
constructor: m.MapData,
/**
* Set target.options from defaults + options
* @param {[type]} target The target
* @param {[type]} options The options to merge
*/
configureOptions: function(options) {
this.options= u.updateProps({}, m.defaults, options);
},
/**
* Ensure all images are loaded
* @return {Promise} A promise that resolves when the images have finished loading (or fail)
*/
bindImages: function() {
var me=this,
mi = me.images;
// reset the images if this is a rebind
if (mi.length>2) {
mi.splice(2);
} else if (mi.length===0) {
// add the actual main image
mi.add(me.image);
// will create a duplicate of the main image, we need this to get raw size info
mi.add(me.image.src);
}
configureAltImages(me);
return me.images.bind();
},
/**
* Test whether an async action is currently in progress
* @return {Boolean} true or false indicating state
*/
isActive: function() {
return !this.complete || this.currentAction;
},
/**
* Return an object indicating the various states. This isn't really used by
* production code.
*
* @return {object} An object with properties for various states
*/
state: function () {
return {
complete: this.complete,
resizing: this.currentAction==='resizing',
zoomed: this.zoomed,
zoomedArea: this.zoomedArea,
scaleInfo: this.scaleInfo
};
},
/**
* Get a unique ID for the wrapper of this imagemapster
* @return {string} A string that is unique to this image
*/
wrapId: function () {
return 'mapster_wrap_' + this.index;
},
_idFromKey: function (key) {
return typeof key === "string" && this._xref.hasOwnProperty(key) ?
this._xref[key] : -1;
},
/**
* Return a comma-separated string of all selected keys
* @return {string} CSV of all keys that are currently selected
*/
getSelected: function () {
var result = '';
$.each(this.data, function (i,e) {
if (e.isSelected()) {
result += (result ? ',' : '') + this.key;
}
});
return result;
},
/**
* Get an array of MapAreas associated with a specific AREA based on the keys for that area
* @param {Element} area An HTML AREA
* @param {number} atMost A number limiting the number of areas to be returned (typically 1 or 0 for no limit)
* @return {MapArea[]} Array of MapArea objects
*/
getAllDataForArea:function (area,atMost) {
var i,ar, result,
me=this,
key = $(area).filter('area').attr(me.options.mapKey);
if (key) {
result=[];
key = u.split(key);
for (i=0;i<(atMost || key.length);i++) {
ar = me.data[me._idFromKey(key[i])];
ar.area=area.length ? area[0]:area;
// set the actual area moused over/selected
// TODO: this is a brittle model for capturing which specific area - if this method was not used,
// ar.area could have old data. fix this.
result.push(ar);
}
}
return result;
},
getDataForArea: function(area) {
var ar=this.getAllDataForArea(area,1);
return ar ? ar[0] || null : null;
},
getDataForKey: function (key) {
return this.data[this._idFromKey(key)];
},
/**
* Get the primary keys associated with an area group.
* If this is a primary key, it will be returned.
*
* @param {string key An area key
* @return {string} A CSV of area keys
*/
getKeysForGroup: function(key) {
var ar=this.getDataForKey(key);
return !ar ? '':
ar.isPrimary ?
ar.key :
this.getPrimaryKeysForMapAreas(ar.areas()).join(',');
},
/**
* given an array of MapArea object, return an array of its unique primary keys
* @param {MapArea[]} areas The areas to analyze
* @return {string[]} An array of unique primary keys
*/
getPrimaryKeysForMapAreas: function(areas)
{
var keys=[];
$.each(areas,function(i,e) {
if ($.inArray(e.keys[0],keys)<0) {
keys.push(e.keys[0]);
}
});
return keys;
},
getData: function (obj) {
if (typeof obj === 'string') {
return this.getDataForKey(obj);
} else if (obj && obj.mapster || u.isElement(obj)) {
return this.getDataForArea(obj);
} else {
return null;
}
},
// remove highlight if present, raise event
ensureNoHighlight: function () {
var ar;
if (this.highlightId >= 0) {
this.graphics.clearHighlight();
ar = this.data[this.highlightId];
ar.changeState('highlight', false);
this.setHighlightId(-1);
}
},
setHighlightId: function(id) {
this.highlightId = id;
},
/**
* Clear all active selections on this map
*/
clearSelections: function () {
$.each(this.data, function (i,e) {
if (e.selected) {
e.deselect(true);
}
});
this.removeSelectionFinish();
},
/**
* Set area options from an array of option data.
*
* @param {object[]} areas An array of objects containing area-specific options
*/
setAreaOptions: function (areas) {
var i, area_options, ar;
areas = areas || [];
// refer by: map_data.options[map_data.data[x].area_option_id]
for (i = areas.length - 1; i >= 0; i--) {
area_options = areas[i];
if (area_options) {
ar = this.getDataForKey(area_options.key);
if (ar) {
u.updateProps(ar.options, area_options);
// TODO: will not deselect areas that were previously selected, so this only works
// for an initial bind.
if (u.isBool(area_options.selected)) {
ar.selected = area_options.selected;
}
}
}
}
},
// keys: a comma-separated list
drawSelections: function (keys) {
var i, key_arr = u.asArray(keys);
for (i = key_arr.length - 1; i >= 0; i--) {
this.data[key_arr[i]].drawSelection();
}
},
redrawSelections: function () {
$.each(this.data, function (i, e) {
if (e.isSelectedOrStatic()) {
e.drawSelection();
}
});
},
///called when images are done loading
initialize: function () {
var imgCopy, base_canvas, overlay_canvas, wrap, parentId, css, i,size,
img,sort_func, sorted_list, scale,
me = this,
opts = me.options;
if (me.complete) {
return;
}
img = $(me.image);
parentId = img.parent().attr('id');
// create a div wrapper only if there's not already a wrapper, otherwise, own it
if (parentId && parentId.length >= 12 && parentId.substring(0, 12) === "mapster_wrap") {
wrap = img.parent();
wrap.attr('id', me.wrapId());
} else {
wrap = $('');
if (opts.wrapClass) {
if (opts.wrapClass === true) {
wrap.addClass(img[0].className);
}
else {
wrap.addClass(opts.wrapClass);
}
}
}
me.wrapper = wrap;
// 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
// width & height. This is needed to scale the imagemap if not being shown at its native size. It is also needed purely
// to finish binding in case the original image was not visible. It can be impossible in some browsers to obtain the
// native size of a hidden image.
me.scaleInfo = scale = u.scaleMap(me.images[0],me.images[1], opts.scaleMap);
me.base_canvas = base_canvas = me.graphics.createVisibleCanvas(me);
me.overlay_canvas = overlay_canvas = me.graphics.createVisibleCanvas(me);
// Now we got what we needed from the copy -clone from the original image again to make sure any other attributes are copied
imgCopy = $(me.images[1])
.addClass('mapster_el '+ me.images[0].className)
.attr({id:null, usemap: null});
size=u.size(me.images[0]);
if (size.complete) {
imgCopy.css({
width: size.width,
height: size.height
});
}
me.buildDataset();
// now that we have processed all the areas, set css for wrapper, scale map if needed
css = {
display: 'block',
position: 'relative',
padding: 0,
width: scale.width,
height: scale.height
};
if (opts.wrapCss) {
$.extend(css, opts.wrapCss);
}
// if we were rebinding with an existing wrapper, the image will aready be in it
if (img.parent()[0] !== me.wrapper[0]) {
img.before(me.wrapper);
}
wrap.css(css);
// move all generated images into the wrapper for easy removal later
$(me.images.slice(2)).hide();
for (i = 1; i < me.images.length; i++) {
wrap.append(me.images[i]);
}
//me.images[1].style.cssText = me.image.style.cssText;
wrap.append(base_canvas)
.append(overlay_canvas)
.append(img.css(m.canvas_style));
// images[0] is the original image with map, images[1] is the copy/background that is visible
u.setOpacity(me.images[0], 0);
$(me.images[1]).show();
u.setOpacity(me.images[1],1);
if (opts.isSelectable && opts.onGetList) {
sorted_list = me.data.slice(0);
if (opts.sortList) {
if (opts.sortList === "desc") {
sort_func = function (a, b) {
return a === b ? 0 : (a > b ? -1 : 1);
};
}
else {
sort_func = function (a, b) {
return a === b ? 0 : (a < b ? -1 : 1);
};
}
sorted_list.sort(function (a, b) {
a = a.value;
b = b.value;
return sort_func(a, b);
});
}
me.options.boundList = opts.onGetList.call(me.image, sorted_list);
}
me.complete=true;
me.processCommandQueue();
if (opts.onConfigured && typeof opts.onConfigured === 'function') {
opts.onConfigured.call(img, true);
}
},
// when rebind is true, the MapArea data will not be rebuilt.
buildDataset: function(rebind) {
var sel,areas,j,area_id,$area,area,curKey,mapArea,key,keys,mapAreaId,group_value,dataItem,href,
me=this,
opts=me.options,
default_group;
function addAreaData(key, value) {
var dataItem = new m.AreaData(me, key, value);
dataItem.areaId = me._xref[key] = me.data.push(dataItem) - 1;
return dataItem.areaId;
}
me._xref = {};
me.data = [];
if (!rebind) {
me.mapAreas=[];
}
default_group = !opts.mapKey;
if (default_group) {
opts.mapKey = 'data-mapster-key';
}
// the [attribute] selector is broken on old IE with jQuery. hasVml() is a quick and dirty
// way to test for that
sel = m.hasVml() ? 'area' :
(default_group ?
'area[coords]' :
'area[' + opts.mapKey + ']');
areas = $(me.map).find(sel).unbind('.mapster');
for (mapAreaId = 0;mapAreaId= 0; j--) {
key = keys[j];
if (opts.mapValue) {
group_value = $area.attr(opts.mapValue);
}
if (default_group) {
// set an attribute so we can refer to the area by index from the DOM object if no key
area_id = addAreaData(me.data.length, group_value);
dataItem = me.data[area_id];
dataItem.key = key = area_id.toString();
}
else {
area_id = me._xref[key];
if (area_id >= 0) {
dataItem = me.data[area_id];
if (group_value && !me.data[area_id].value) {
dataItem.value = group_value;
}
}
else {
area_id = addAreaData(key, group_value);
dataItem = me.data[area_id];
dataItem.isPrimary=j===0;
}
}
mapArea.areaDataXref.push(area_id);
dataItem.areasXref.push(mapAreaId);
}
href=$area.attr('href');
if (href && href!=='#' && !dataItem.href)
{
dataItem.href=href;
}
if (!mapArea.nohref) {
$area.bind('click.mapster', me.click);
if (!m.isTouch) {
$area.bind('mouseover.mapster', me.mouseover)
.bind('mouseout.mapster', me.mouseout)
.bind('mousedown.mapster', me.mousedown);
}
}
// store an ID with each area.
$area.data("mapster", mapAreaId+1);
}
// TODO listenToList
// if (opts.listenToList && opts.nitG) {
// opts.nitG.bind('click.mapster', event_hooks[map_data.hooks_index].listclick_hook);
// }
// populate areas from config options
me.setAreaOptions(opts.areas);
me.redrawSelections();
},
processCommandQueue: function() {
var cur,me=this;
while (!me.currentAction && me.commands.length) {
cur = me.commands[0];
me.commands.splice(0,1);
m.impl[cur.command].apply(cur.that, cur.args);
}
},
clearEvents: function () {
$(this.map).find('area')
.unbind('.mapster');
$(this.images)
.unbind('.mapster');
},
_clearCanvases: function (preserveState) {
// remove the canvas elements created
if (!preserveState) {
$(this.base_canvas).remove();
}
$(this.overlay_canvas).remove();
},
clearMapData: function (preserveState) {
var me = this;
this._clearCanvases(preserveState);
// release refs to DOM elements
$.each(this.data, function (i, e) {
e.reset();
});
this.data = null;
if (!preserveState) {
// get rid of everything except the original image
this.image.style.cssText = this.imgCssText;
$(this.wrapper).before(this.image).remove();
}
me.images.clear();
this.image = null;
u.ifFunction(this.clearTooltip, this);
},
// Compelete cleanup process for deslecting items. Called after a batch operation, or by AreaData for single
// operations not flagged as "partial"
removeSelectionFinish: function () {
var g = this.graphics;
g.refreshSelections();
// do not call ensure_no_highlight- we don't really want to unhilight it, just remove the effect
g.clearHighlight();
}
};
} (jQuery));
/* areadata.js
AreaData and MapArea protoypes
*/
(function ($) {
var m = $.mapster, u = m.utils;
/**
* Select this area
*
* @param {AreaData} me AreaData context
* @param {object} options Options for rendering the selection
*/
function select(options) {
// need to add the new one first so that the double-opacity effect leaves the current one highlighted for singleSelect
var me=this, o = me.owner;
if (o.options.singleSelect) {
o.clearSelections();
}
// because areas can overlap - we can't depend on the selection state to tell us anything about the inner areas.
// don't check if it's already selected
if (!me.isSelected()) {
if (options) {
// cache the current options, and map the altImageId if an altimage
// was passed
me.optsCache = $.extend(me.effectiveRenderOptions('select'),
options,
{
altImageId: o.images.add(options.altImage)
});
}
me.drawSelection();
me.selected = true;
me.changeState('select', true);
}
if (o.options.singleSelect) {
o.graphics.refreshSelections();
}
}
/**
* Deselect this area, optionally deferring finalization so additional areas can be deselected
* in a single operation
*
* @param {boolean} partial when true, the caller must invoke "finishRemoveSelection" to render
*/
function deselect(partial) {
var me=this;
me.selected = false;
me.changeState('select', false);
// release information about last area options when deselecting.
me.optsCache=null;
me.owner.graphics.removeSelections(me.areaId);
// Complete selection removal process. This is separated because it's very inefficient to perform the whole
// process for multiple removals, as the canvas must be totally redrawn at the end of the process.ar.remove
if (!partial) {
me.owner.removeSelectionFinish();
}
}
/**
* Toggle the selection state of this area
* @param {object} options Rendering options, if toggling on
* @return {bool} The new selection state
*/
function toggle(options) {
var me=this;
if (!me.isSelected()) {
me.select(options);
}
else {
me.deselect();
}
return me.isSelected();
}
/**
* An AreaData object; represents a conceptual area that can be composed of
* one or more MapArea objects
*
* @param {MapData} owner The MapData object to which this belongs
* @param {string} key The key for this area
* @param {string} value The mapValue string for this area
*/
m.AreaData = function (owner, key, value) {
$.extend(this,{
owner: owner,
key: key || '',
// means this represents the first key in a list of keys (it's the area group that gets highlighted on mouseover)
isPrimary: true,
areaId: -1,
href: '',
value: value || '',
options:{},
// "null" means unchanged. Use "isSelected" method to just test true/false
selected: null,
// xref to MapArea objects
areasXref: [],
// (temporary storage) - the actual area moused over
area: null,
// the last options used to render this. Cache so when re-drawing after a remove, changes in options won't
// break already selected things.
optsCache: null
});
};
/**
* The public API for AreaData object
*/
m.AreaData.prototype = {
constuctor: m.AreaData,
select: select,
deselect: deselect,
toggle: toggle,
areas: function() {
var i,result=[];
for (i=0;i= 0; j -= 2) {
curX = coords[j];
curY = coords[j + 1];
if (curX < minX) {
minX = curX;
bestMaxY = curY;
}
if (curX > maxX) {
maxX = curX;
bestMinY = curY;
}
if (curY < minY) {
minY = curY;
bestMaxX = curX;
}
if (curY > maxY) {
maxY = curY;
bestMinX = curX;
}
}
// try to figure out the best place for the tooltip
if (width && height) {
found=false;
$.each([[bestMaxX - width, minY - height], [bestMinX, minY - height],
[minX - width, bestMaxY - height], [minX - width, bestMinY],
[maxX,bestMaxY - height], [ maxX,bestMinY],
[bestMaxX - width, maxY], [bestMinX, maxY]
],function (i, e) {
if (!found && (e[0] > rootx && e[1] > rooty)) {
nest = e;
found=true;
return false;
}
});
// default to lower-right corner if nothing fit inside the boundaries of the image
if (!found) {
nest=[maxX,maxY];
}
}
return nest;
};
} (jQuery));
/* scale.js: resize and zoom functionality
requires areacorners.js, when.js
*/
(function ($) {
var m = $.mapster, u = m.utils, p = m.MapArea.prototype;
m.utils.getScaleInfo = function (eff, actual) {
var pct;
if (!actual) {
pct = 1;
actual=eff;
} else {
pct = eff.width / actual.width || eff.height / actual.height;
// make sure a float error doesn't muck us up
if (pct > 0.98 && pct < 1.02) { pct = 1; }
}
return {
scale: (pct !== 1),
scalePct: pct,
realWidth: actual.width,
realHeight: actual.height,
width: eff.width,
height: eff.height,
ratio: eff.width / eff.height
};
};
// Scale a set of AREAs, return old data as an array of objects
m.utils.scaleMap = function (image, imageRaw, scale) {
// stunningly, jQuery width can return zero even as width does not, seems to happen only
// with adBlock or maybe other plugins. These must interfere with onload events somehow.
var vis=u.size(image),
raw=u.size(imageRaw,true);
if (!raw.complete()) {
throw("Another script, such as an extension, appears to be interfering with image loading. Please let us know about this.");
}
if (!vis.complete()) {
vis=raw;
}
return this.getScaleInfo(vis, scale ? raw : null);
};
/**
* Resize the image map. Only one of newWidth and newHeight should be passed to preserve scale
*
* @param {int} width The new width OR an object containing named parameters matching this function sig
* @param {int} height The new height
* @param {int} effectDuration Time in ms for the resize animation, or zero for no animation
* @param {function} callback A function to invoke when the operation finishes
* @return {promise} NOT YET IMPLEMENTED
*/
m.MapData.prototype.resize = function (width, height, duration, callback) {
var p,promises,newsize,els, highlightId, ratio,
me = this;
// allow omitting duration
callback = callback || duration;
function sizeCanvas(canvas, w, h) {
if (m.hasCanvas()) {
canvas.width = w;
canvas.height = h;
} else {
$(canvas).width(w);
$(canvas).height(h);
}
}
// Finalize resize action, do callback, pass control to command queue
function cleanupAndNotify() {
me.currentAction = '';
if ($.isFunction(callback)) {
callback();
}
me.processCommandQueue();
}
// handle cleanup after the inner elements are resized
function finishResize() {
sizeCanvas(me.overlay_canvas, width, height);
// restore highlight state if it was highlighted before
if (highlightId >= 0) {
var areaData = me.data[highlightId];
areaData.tempOptions = { fade: false };
me.getDataForKey(areaData.key).highlight();
areaData.tempOptions = null;
}
sizeCanvas(me.base_canvas, width, height);
me.redrawSelections();
cleanupAndNotify();
}
function resizeMapData() {
$(me.image).css(newsize);
// start calculation at the same time as effect
me.scaleInfo = u.getScaleInfo({
width: width,
height: height
},
{
width: me.scaleInfo.realWidth,
height: me.scaleInfo.realHeight
});
$.each(me.data, function (i, e) {
$.each(e.areas(), function (i, e) {
e.resize();
});
});
}
if (me.scaleInfo.width === width && me.scaleInfo.height === height) {
return;
}
highlightId = me.highlightId;
if (!width) {
ratio = height / me.scaleInfo.realHeight;
width = Math.round(me.scaleInfo.realWidth * ratio);
}
if (!height) {
ratio = width / me.scaleInfo.realWidth;
height = Math.round(me.scaleInfo.realHeight * ratio);
}
newsize = { 'width': String(width) + 'px', 'height': String(height) + 'px' };
if (!m.hasCanvas()) {
$(me.base_canvas).children().remove();
}
// resize all the elements that are part of the map except the image itself (which is not visible)
// but including the div wrapper
els = $(me.wrapper).find('.mapster_el').add(me.wrapper);
if (duration) {
promises = [];
me.currentAction = 'resizing';
els.each(function (i, e) {
p = u.defer();
promises.push(p);
$(e).animate(newsize, {
duration: duration,
complete: p.resolve,
easing: "linear"
});
});
p = u.defer();
promises.push(p);
// though resizeMapData is not async, it needs to be finished just the same as the animations,
// so add it to the "to do" list.
u.when.all(promises).then(finishResize);
resizeMapData();
p.resolve();
} else {
els.css(newsize);
resizeMapData();
finishResize();
}
};
m.MapArea = u.subclass(m.MapArea, function () {
//change the area tag data if needed
this.base.init();
if (this.owner.scaleInfo.scale) {
this.resize();
}
});
p.coords = function (percent, coordOffset) {
var j, newCoords = [],
pct = percent || this.owner.scaleInfo.scalePct,
offset = coordOffset || 0;
if (pct === 1 && coordOffset === 0) {
return this.originalCoords;
}
for (j = 0; j < this.length; j++) {
//amount = j % 2 === 0 ? xPct : yPct;
newCoords.push(Math.round(this.originalCoords[j] * pct) + offset);
}
return newCoords;
};
p.resize = function () {
this.area.coords = this.coords().join(',');
};
p.reset = function () {
this.area.coords = this.coords(1).join(',');
};
m.impl.resize = function (width, height, duration, callback) {
if (!width && !height) {
return false;
}
var x= (new m.Method(this,
function () {
this.resize(width, height, duration, callback);
},
null,
{
name: 'resize',
args: arguments
}
)).go();
return x;
};
/*
m.impl.zoom = function (key, opts) {
var options = opts || {};
function zoom(areaData) {
// this will be MapData object returned by Method
var scroll, corners, height, width, ratio,
diffX, diffY, ratioX, ratioY, offsetX, offsetY, newWidth, newHeight, scrollLeft, scrollTop,
padding = options.padding || 0,
scrollBarSize = areaData ? 20 : 0,
me = this,
zoomOut = false;
if (areaData) {
// save original state on first zoom operation
if (!me.zoomed) {
me.zoomed = true;
me.preZoomWidth = me.scaleInfo.width;
me.preZoomHeight = me.scaleInfo.height;
me.zoomedArea = areaData;
if (options.scroll) {
me.wrapper.css({ overflow: 'auto' });
}
}
corners = $.mapster.utils.areaCorners(areaData.coords(1, 0));
width = me.wrapper.innerWidth() - scrollBarSize - padding * 2;
height = me.wrapper.innerHeight() - scrollBarSize - padding * 2;
diffX = corners.maxX - corners.minX;
diffY = corners.maxY - corners.minY;
ratioX = width / diffX;
ratioY = height / diffY;
ratio = Math.min(ratioX, ratioY);
offsetX = (width - diffX * ratio) / 2;
offsetY = (height - diffY * ratio) / 2;
newWidth = me.scaleInfo.realWidth * ratio;
newHeight = me.scaleInfo.realHeight * ratio;
scrollLeft = (corners.minX) * ratio - padding - offsetX;
scrollTop = (corners.minY) * ratio - padding - offsetY;
} else {
if (!me.zoomed) {
return;
}
zoomOut = true;
newWidth = me.preZoomWidth;
newHeight = me.preZoomHeight;
scrollLeft = null;
scrollTop = null;
}
this.resize({
width: newWidth,
height: newHeight,
duration: options.duration,
scroll: scroll,
scrollLeft: scrollLeft,
scrollTop: scrollTop,
// closure so we can be sure values are correct
callback: (function () {
var isZoomOut = zoomOut,
scroll = options.scroll,
areaD = areaData;
return function () {
if (isZoomOut) {
me.preZoomWidth = null;
me.preZoomHeight = null;
me.zoomed = false;
me.zoomedArea = false;
if (scroll) {
me.wrapper.css({ overflow: 'inherit' });
}
} else {
// just to be sure it wasn't canceled & restarted
me.zoomedArea = areaD;
}
};
} ())
});
}
return (new m.Method(this,
function (opts) {
zoom.call(this);
},
function () {
zoom.call(this.owner, this);
},
{
name: 'zoom',
args: arguments,
first: true,
key: key
}
)).go();
};
*/
} (jQuery));
/* tooltip.js - tooltip functionality
requires areacorners.js
*/
(function ($) {
var m = $.mapster, u = m.utils;
$.extend(m.defaults, {
toolTipContainer: '',
showToolTip: false,
toolTipFade: true,
toolTipClose: ['area-mouseout','image-mouseout'],
onShowToolTip: null,
onHideToolTip: null
});
$.extend(m.area_defaults, {
toolTip: null,
toolTipClose: null
});
/**
* Show a tooltip positioned near this area.
*
* @param {string|jquery} html A string of html or a jQuery object containing the tooltip content.
* @param {string|jquery} [template] The html template in which to wrap the content
* @param {string|object} [css] CSS to apply to the outermost element of the tooltip
* @return {jquery} The tooltip that was created
*/
function createToolTip(html, template, css) {
var tooltip;
// wrap the template in a jQuery object, or clone the template if it's already one.
// This assumes that anything other than a string is a jQuery object; if it's not jQuery will
// probably throw an error.
if (template) {
tooltip = typeof template === 'string' ?
$(template) :
$(template).clone();
tooltip.append(html);
} else {
tooltip=$(html);
}
// always set display to block, or the positioning css won't work if the end user happened to
// use a non-block type element.
tooltip.css($.extend((css || {}),{
display:"block",
position:"absolute"
})).hide();
$('body').append(tooltip);
// we must actually add the tooltip to the DOM and "show" it in order to figure out how much space it
// consumes, and then reposition it with that knowledge.
// We also cache the actual opacity setting to restore finally.
tooltip.attr("data-opacity",tooltip.css("opacity"))
.css("opacity",0);
// doesn't really show it because opacity=0
return tooltip.show();
}
/**
* Show a tooltip positioned near this area.
*
* @param {jquery} tooltip The tooltip
* @param {object} [options] options for displaying the tooltip.
* @config {int} [left] The 0-based absolute x position for the tooltip
* @config {int} [top] The 0-based absolute y position for the tooltip
* @config {string|object} [css] CSS to apply to the outermost element of the tooltip
* @config {bool} [fadeDuration] When non-zero, the duration in milliseconds of a fade-in effect for the tooltip.
*/
function showToolTipImpl(tooltip,options)
{
var tooltipCss = {
"left": options.left + "px",
"top": options.top + "px"
},
actalOpacity=tooltip.attr("data-opacity") || 0,
zindex = tooltip.css("z-index");
if (parseInt(zindex,10)===0
|| zindex === "auto") {
tooltipCss["z-index"] = 9999;
}
tooltip.css(tooltipCss)
.addClass('mapster_tooltip');
if (options.fadeDuration && options.fadeDuration>0) {
u.fader(tooltip[0], 0, actalOpacity, options.fadeDuration);
} else {
u.setOpacity(tooltip[0], actalOpacity);
}
}
/**
* Hide and remove active tooltips
*
* @param {MapData} this The mapdata object to which the tooltips belong
*/
m.MapData.prototype.clearToolTip = function() {
if (this.activeToolTip) {
this.activeToolTip.stop().remove();
this.activeToolTip = null;
this.activeToolTipID = null;
u.ifFunction(this.options.onHideToolTip, this);
}
};
/**
* Configure the binding between a named tooltip closing option, and a mouse event.
*
* If a callback is passed, it will be called when the activating event occurs, and the tooltip will
* only closed if it returns true.
*
* @param {MapData} [this] The MapData object to which this tooltip belongs.
* @param {String} option The name of the tooltip closing option
* @param {String} event UI event to bind to this option
* @param {Element} target The DOM element that is the target of the event
* @param {Function} [beforeClose] Callback when the tooltip is closed
* @param {Function} [onClose] Callback when the tooltip is closed
*/
function bindToolTipClose(options, bindOption, event, target, beforeClose, onClose) {
var event_name = event + '.mapster-tooltip';
if ($.inArray(bindOption, options) >= 0) {
target.unbind(event_name)
.bind(event_name, function (e) {
if (!beforeClose || beforeClose.call(this,e)) {
target.unbind('.mapster-tooltip');
if (onClose) {
onClose.call(this);
}
}
});
return {
object: target,
event: event_name
};
}
}
/**
* Show a tooltip.
*
* @param {string|jquery} [tooltip] A string of html or a jQuery object containing the tooltip content.
*
* @param {string|jquery} [target] The target of the tooltip, to be used to determine positioning. If null,
* absolute position values must be passed with left and top.
*
* @param {string|jquery} [image] If target is an [area] the image that owns it
*
* @param {string|jquery} [container] An element within which the tooltip must be bounded
*
*
*
* @param {object|string|jQuery} [options] options to apply when creating this tooltip - OR -
* The markup, or a jquery object, containing the data for the tooltip
*
* @config {string} [closeEvents] A string with one or more comma-separated values that determine when the tooltip
* closes: 'area-click','tooltip-click','image-mouseout' are valid values
* then no template will be used.
* @config {int} [offsetx] the horizontal amount to offset the tooltip
* @config {int} [offsety] the vertical amount to offset the tooltip
* @config {string|object} [css] CSS to apply to the outermost element of the tooltip
*/
function showToolTip(tooltip,target,image,container,options) {
var corners,
ttopts = {};
options = options || {};
if (target) {
corners = u.areaCorners(target,image,container,
tooltip.outerWidth(true),
tooltip.outerHeight(true));
// Try to upper-left align it first, if that doesn't work, change the parameters
ttopts.left = corners[0];
ttopts.top = corners[1];
} else {
ttopts.left = options.left;
ttopts.top = options.top;
}
ttopts.left += (options.offsetx || 0);
ttopts.top +=(options.offsety || 0);
ttopts.css= options.css;
ttopts.fadeDuration = options.fadeDuration;
showToolTipImpl(tooltip,ttopts);
return tooltip;
}
/**
* Show a tooltip positioned near this area.
*
* @param {string|jquery} [content] A string of html or a jQuery object containing the tooltip content.
* @param {object|string|jQuery} [options] options to apply when creating this tooltip - OR -
* The markup, or a jquery object, containing the data for the tooltip
* @config {string|jquery} [container] An element within which the tooltip must be bounded
* @config {bool} [template] a template to use instead of the default. If this property exists and is null,
* then no template will be used.
* @config {string} [closeEvents] A string with one or more comma-separated values that determine when the tooltip
* closes: 'area-click','tooltip-click','image-mouseout' are valid values
* then no template will be used.
* @config {int} [offsetx] the horizontal amount to offset the tooltip
* @config {int} [offsety] the vertical amount to offset the tooltip
* @config {string|object} [css] CSS to apply to the outermost element of the tooltip
*/
m.AreaData.prototype.showToolTip= function(content,options) {
var tooltip, closeOpts, target, tipClosed, template,
ttopts = {},
ad=this,
md=ad.owner,
areaOpts = ad.effectiveOptions();
// copy the options object so we can update it
options = options ? $.extend({},options) : {};
content = content || areaOpts.toolTip;
closeOpts = options.closeEvents || areaOpts.toolTipClose || md.options.toolTipClose || 'tooltip-click';
template = typeof options.template !== 'undefined' ?
options.template :
md.options.toolTipContainer;
options.closeEvents = typeof closeOpts === 'string' ?
closeOpts = u.split(closeOpts) :
closeOpts;
options.fadeDuration = options.fadeDuration ||
(md.options.toolTipFade ?
(md.options.fadeDuration || areaOpts.fadeDuration) : 0);
target = ad.area ?
ad.area :
$.map(ad.areas(),
function(e) {
return e.area;
});
if (md.activeToolTipID===ad.areaId) {
return;
}
md.clearToolTip();
md.activeToolTip = tooltip = createToolTip(content,
template,
options.css);
md.activeToolTipID = ad.areaId;
tipClosed = function() {
md.clearToolTip();
};
bindToolTipClose(closeOpts,'area-click', 'click', $(md.map), null, tipClosed);
bindToolTipClose(closeOpts,'tooltip-click', 'click', tooltip,null, tipClosed);
bindToolTipClose(closeOpts,'image-mouseout', 'mouseout', $(md.image), function(e) {
return (e.relatedTarget && e.relatedTarget.nodeName!=='AREA' && e.relatedTarget!==ad.area);
}, tipClosed);
showToolTip(tooltip,
target,
md.image,
options.container,
template,
options);
u.ifFunction(md.options.onShowToolTip, ad.area,
{
toolTip: tooltip,
options: ttopts,
areaOptions: areaOpts,
key: ad.key,
selected: ad.isSelected()
});
return tooltip;
};
/**
* Parse an object that could be a string, a jquery object, or an object with a "contents" property
* containing html or a jQuery object.
*
* @param {object|string|jQuery} options The parameter to parse
* @return {string|jquery} A string or jquery object
*/
function getHtmlFromOptions(options) {
// see if any html was passed as either the options object itself, or the content property
return (options ?
((typeof options === 'string' || options.jquery) ?
options :
options.content) :
null);
}
/**
* Activate or remove a tooltip for an area. When this method is called on an area, the
* key parameter doesn't apply and "options" is the first parameter.
*
* When called with no parameters, or "key" is a falsy value, any active tooltip is cleared.
*
* When only a key is provided, the default tooltip for the area is used.
*
* When html is provided, this is used instead of the default tooltip.
*
* When "noTemplate" is true, the default tooltip template will not be used either, meaning only
* the actual html passed will be used.
*
* @param {string|AreaElement} key The area for which to activate a tooltip, or a DOM element.
*
* @param {object|string|jquery} [options] options to apply when creating this tooltip - OR -
* The markup, or a jquery object, containing the data for the tooltip
* @config {string|jQuery} [content] the inner content of the tooltip; the tooltip text or HTML
* @config {Element|jQuery} [container] the inner content of the tooltip; the tooltip text or HTML
* @config {bool} [template] a template to use instead of the default. If this property exists and is null,
* then no template will be used.
* @config {int} [offsetx] the horizontal amount to offset the tooltip.
* @config {int} [offsety] the vertical amount to offset the tooltip.
* @config {string|object} [css] CSS to apply to the outermost element of the tooltip
* @config {string|object} [css] CSS to apply to the outermost element of the tooltip
* @config {bool} [fadeDuration] When non-zero, the duration in milliseconds of a fade-in effect for the tooltip.
* @return {jQuery} The jQuery object
*/
m.impl.tooltip = function (key,options) {
return (new m.Method(this,
function mapData() {
var tooltip, target, md=this;
if (!key) {
md.clearToolTip();
} else {
target=$(key);
if (md.activeToolTipID ===target[0]) {
return;
}
md.clearToolTip();
md.activeToolTip = tooltip = createToolTip(getHtmlFromOptions(options),
options.template || md.options.toolTipContainer,
options.css);
md.activeToolTipID = target[0];
bindToolTipClose(['tooltip-click'],'tooltip-click', 'click', tooltip, null, function() {
md.clearToolTip();
});
md.activeToolTip = tooltip = showToolTip(tooltip,
target,
md.image,
options.container,
options);
}
},
function areaData() {
if ($.isPlainObject(key) && !options) {
options = key;
}
this.showToolTip(getHtmlFromOptions(options),options);
},
{
name: 'tooltip',
args: arguments,
key: key
}
)).go();
};
} (jQuery));