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