svgmap 2.19.3 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,7 +99,11 @@ You can pass the following options into svgMap:
99
99
  | `hideFlag` | `boolean` | `false` | Hide the flag in tooltips |
100
100
  | `noDataText` | `string` | `'No data available'` | The text to be shown when no data is present |
101
101
  | `touchLink` | `boolean` | `false` | Set to `true` to open the link (see `data.values.link`) on mobile devices, by default the tooltip will be shown |
102
+ | `showTooltips` | `boolean` | `true` | When `false`, disables hover and touch-following tooltips only. Persistent on-map labels from `persistentTooltips` are unaffected. On touch devices, countries with a `link` open it on the first tap instead of using the two-tap pattern (first tap preview, second tap navigate). |
103
+ | `tooltipTrigger` | `'hover'`, `'click'` | `'hover'` | How the floating tooltip opens with the **mouse**: `'hover'` opens on mouseenter/mouseleave.`'click'` opens on primary click and closes when clicking outside the map countries or tooltip. Only applies when `showTooltips` is `true`. |
104
+ | `persistentTooltips` | `false`, `array`, `function` | `false` | Persistent tooltips fixed on the map when it loads: an array of country IDs, or a function (`function (countryID, countryValues) { … }`) to decide per country. Independent of `showTooltips`. Best used with `showTooltips: false` or `tooltipTrigger: 'click'`. |
102
105
  | `onGetTooltip` | `function` | | Called when a tooltip is created to custimize the tooltip content (`function (tooltipDiv, countryID, countryValues) { return 'Custom HTML'; }`) |
106
+ | `onCountryClick` | `function` | | Called when the user clicks a country (primary button, pointer released without dragging). Signature: `function (countryID, event) { … }`. Use this for custom actions instead of or in addition to `data.values.link`. Return `false` to skip opening the URL when the country has a `link`. On touch devices with a link, the callback runs when the tap would navigate (not on the first tap that only shows the tooltip). Countries show a pointer cursor while this option is set. |
103
107
  | `countries` | `object` | | Additional options specific to countries: |
104
108
  |    `↳ EH` | `boolean` | `true` | When set to `false`, Western Sahara (EH) will be combined with Morocco (MA) |
105
109
  | `data` | `object` | | The chart data to use for coloring and to show in the tooltip. Use a unique data-id as key and provide following options as value: |
package/dist/index.cjs CHANGED
@@ -2344,6 +2344,15 @@ class svgMap {
2344
2344
  // Set to true to open the link on mobile devices, set to false (default) to show the tooltip
2345
2345
  touchLink: false,
2346
2346
 
2347
+ // When false, disables hover/touch-following tooltips (not the on-map persistent labels; see persistentTooltips)
2348
+ showTooltips: true,
2349
+
2350
+ // 'hover' (default): mouse shows tooltip on enter. 'click': mouse opens tooltip on click; touch/pen unchanged.
2351
+ tooltipTrigger: 'hover',
2352
+
2353
+ // Persistent on-map tooltips: an array of country IDs, or a function (countryID, countryValues) => boolean
2354
+ persistentTooltips: false,
2355
+
2347
2356
  // Set to true to show the to show a zoom reset button
2348
2357
  showZoomReset: false,
2349
2358
 
@@ -2352,6 +2361,10 @@ class svgMap {
2352
2361
  return null;
2353
2362
  },
2354
2363
 
2364
+ // Called on country click (pointer released without dragging). Receives
2365
+ // (countryID, event). Return false to skip opening data.values[*].link.
2366
+ onCountryClick: null,
2367
+
2355
2368
  // Country specific options
2356
2369
  countries: {
2357
2370
  // Western Sahara: Set to false to combine Morocco (MA) and Western Sahara (EH)
@@ -2391,6 +2404,9 @@ class svgMap {
2391
2404
  // Wrapper element
2392
2405
  this.wrapper = document.getElementById(this.options.targetElementID);
2393
2406
  this.wrapper.classList.add('svgMap-wrapper');
2407
+ if (typeof this.options.onCountryClick === 'function') {
2408
+ this.wrapper.classList.add('svgMap-country-click-callback');
2409
+ }
2394
2410
 
2395
2411
  // Container element
2396
2412
  this.container = document.createElement('div');
@@ -3082,7 +3098,9 @@ class svgMap {
3082
3098
 
3083
3099
  createMap() {
3084
3100
  // Create the tooltip
3085
- this.createTooltip();
3101
+ if (this.options.showTooltips) {
3102
+ this.createTooltip();
3103
+ }
3086
3104
 
3087
3105
  // Create map wrappers
3088
3106
  this.mapWrapper = this.createElement(
@@ -3208,6 +3226,7 @@ class svgMap {
3208
3226
  }.bind(this);
3209
3227
 
3210
3228
  // Add map elements
3229
+ var countryElements = [];
3211
3230
  Object.keys(mapPaths).forEach(
3212
3231
  function (countryID) {
3213
3232
  var countryData = this.mapPaths[countryID];
@@ -3229,6 +3248,7 @@ class svgMap {
3229
3248
  countryElement.classList.add('svgMap-country');
3230
3249
 
3231
3250
  this.mapImage.appendChild(countryElement);
3251
+ countryElements.push(countryElement);
3232
3252
 
3233
3253
  // Add tooltip when touch is used
3234
3254
  function handlePointerMove(e) {
@@ -3244,7 +3264,7 @@ class svgMap {
3244
3264
  this.hideTooltip();
3245
3265
  document
3246
3266
  .querySelectorAll('.svgMap-active')
3247
- .forEach(el => el.classList.remove('svgMap-active'));
3267
+ .forEach((el) => el.classList.remove('svgMap-active'));
3248
3268
  }
3249
3269
  }
3250
3270
 
@@ -3253,29 +3273,40 @@ class svgMap {
3253
3273
  countryElement.addEventListener(
3254
3274
  'pointerenter',
3255
3275
  function (e) {
3276
+ if (
3277
+ e.pointerType === 'mouse' &&
3278
+ this.options.showTooltips &&
3279
+ this.options.tooltipTrigger === 'click'
3280
+ ) {
3281
+ return;
3282
+ }
3283
+
3256
3284
  // Only add pointermove listener for non-touch pointers
3257
3285
  if (e.pointerType !== 'touch') {
3258
- document.addEventListener(
3259
- 'pointermove',
3260
- handlePointerMoveBound,
3261
- { passive: true }
3262
- );
3286
+ document.addEventListener('pointermove', handlePointerMoveBound, {
3287
+ passive: true
3288
+ });
3263
3289
  }
3264
3290
 
3265
3291
  document
3266
3292
  .querySelectorAll('.svgMap-active')
3267
- .forEach(el => el.classList.remove('svgMap-active'));
3293
+ .forEach((el) => el.classList.remove('svgMap-active'));
3268
3294
 
3269
- countryElement.parentNode.appendChild(countryElement);
3295
+ countryElement.parentNode.insertBefore(
3296
+ countryElement,
3297
+ this.persistentTooltipGroup || null
3298
+ );
3270
3299
  countryElement.classList.add('svgMap-active');
3271
3300
 
3272
3301
  const countryID = countryElement.getAttribute('data-id');
3273
- this.setTooltipContent(this.getTooltipContent(countryID));
3274
- this.showTooltip(e);
3302
+ if (this.options.showTooltips) {
3303
+ this.setTooltipContent(this.getTooltipContent(countryID));
3304
+ this.showTooltip(e);
3275
3305
 
3276
- // For touch, move tooltip to the touch position and keep it there
3277
- if (e.pointerType === 'touch') {
3278
- this.moveTooltip(e);
3306
+ // For touch, move tooltip to the touch position and keep it there
3307
+ if (e.pointerType === 'touch') {
3308
+ this.moveTooltip(e);
3309
+ }
3279
3310
  }
3280
3311
  }.bind(this)
3281
3312
  );
@@ -3294,7 +3325,10 @@ class svgMap {
3294
3325
  'touchend',
3295
3326
  function (e) {
3296
3327
  const touch = e.changedTouches[0];
3297
- const elementAtEnd = document.elementFromPoint(touch.clientX, touch.clientY);
3328
+ const elementAtEnd = document.elementFromPoint(
3329
+ touch.clientX,
3330
+ touch.clientY
3331
+ );
3298
3332
 
3299
3333
  // Only hide if touch ended outside the country or tooltip
3300
3334
  if (
@@ -3305,7 +3339,7 @@ class svgMap {
3305
3339
  this.hideTooltip();
3306
3340
  document
3307
3341
  .querySelectorAll('.svgMap-active')
3308
- .forEach(el => el.classList.remove('svgMap-active'));
3342
+ .forEach((el) => el.classList.remove('svgMap-active'));
3309
3343
  }
3310
3344
  }.bind(this),
3311
3345
  { passive: true }
@@ -3316,11 +3350,22 @@ class svgMap {
3316
3350
  'pointerleave',
3317
3351
  function (e) {
3318
3352
  if (e.pointerType !== 'touch') {
3319
- document.removeEventListener('pointermove', handlePointerMoveBound);
3320
- this.hideTooltip();
3321
- document
3322
- .querySelectorAll('.svgMap-active')
3323
- .forEach(el => el.classList.remove('svgMap-active'));
3353
+ document.removeEventListener(
3354
+ 'pointermove',
3355
+ handlePointerMoveBound
3356
+ );
3357
+ if (
3358
+ !(
3359
+ e.pointerType === 'mouse' &&
3360
+ this.options.showTooltips &&
3361
+ this.options.tooltipTrigger === 'click'
3362
+ )
3363
+ ) {
3364
+ this.hideTooltip();
3365
+ document
3366
+ .querySelectorAll('.svgMap-active')
3367
+ .forEach((el) => el.classList.remove('svgMap-active'));
3368
+ }
3324
3369
  }
3325
3370
  }.bind(this)
3326
3371
  );
@@ -3340,7 +3385,7 @@ class svgMap {
3340
3385
  this.hideTooltip();
3341
3386
  document
3342
3387
  .querySelectorAll('.svgMap-active')
3343
- .forEach(el => el.classList.remove('svgMap-active'));
3388
+ .forEach((el) => el.classList.remove('svgMap-active'));
3344
3389
  }.bind(this),
3345
3390
  { passive: true }
3346
3391
  );
@@ -3364,17 +3409,29 @@ class svgMap {
3364
3409
  }.bind(this)
3365
3410
  );
3366
3411
 
3412
+ var persistent = this.options.persistentTooltips;
3413
+ if (
3414
+ persistent &&
3415
+ (Array.isArray(persistent) || typeof persistent === 'function')
3416
+ ) {
3417
+ this.createPersistentTooltips(countryElements);
3418
+ }
3419
+
3367
3420
  let pointerStart = null;
3368
3421
  let activeCountry = null;
3369
3422
 
3370
- this.mapImage.addEventListener('pointerdown', e => {
3371
- // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
3372
- if (e.button !== 0) return;
3423
+ this.mapImage.addEventListener(
3424
+ 'pointerdown',
3425
+ (e) => {
3426
+ // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
3427
+ if (e.button !== 0) return;
3373
3428
 
3374
- pointerStart = { x: e.clientX, y: e.clientY };
3375
- }, { passive: true });
3429
+ pointerStart = { x: e.clientX, y: e.clientY };
3430
+ },
3431
+ { passive: true }
3432
+ );
3376
3433
 
3377
- this.mapImage.addEventListener('pointerup', e => {
3434
+ this.mapImage.addEventListener('pointerup', (e) => {
3378
3435
  // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
3379
3436
  if (e.button !== 0) return;
3380
3437
 
@@ -3394,31 +3451,137 @@ class svgMap {
3394
3451
 
3395
3452
  const countryID = countryElement.getAttribute('data-id');
3396
3453
  const link = countryElement.getAttribute('data-link');
3397
- const target = countryElement.getAttribute('data-link-target');
3398
- if (!link) return;
3399
-
3454
+ const linkTarget = countryElement.getAttribute('data-link-target');
3455
+ const hasCallback = typeof this.options.onCountryClick === 'function';
3456
+ const hasLink = !!link;
3400
3457
  const isTouch = e.pointerType === 'touch' || e.pointerType === 'pen';
3401
3458
 
3459
+ const isClickTooltipMouse =
3460
+ e.pointerType === 'mouse' &&
3461
+ this.options.showTooltips &&
3462
+ this.options.tooltipTrigger === 'click';
3463
+
3464
+ if (!hasLink && !hasCallback && !isClickTooltipMouse) return;
3465
+
3466
+ if (isClickTooltipMouse && this.options.showTooltips) {
3467
+ const willNavigate =
3468
+ hasLink && countryElement.classList.contains('svgMap-active');
3469
+ const shouldFireCallback = hasCallback && (!hasLink || willNavigate);
3470
+
3471
+ var callbackResultClick;
3472
+ if (shouldFireCallback) {
3473
+ callbackResultClick = this.options.onCountryClick(countryID, e);
3474
+ }
3475
+
3476
+ if (hasLink) {
3477
+ if (callbackResultClick === false) return;
3478
+ if (countryElement.classList.contains('svgMap-active')) {
3479
+ if (linkTarget) window.open(link, linkTarget);
3480
+ else window.location.href = link;
3481
+ } else {
3482
+ this.mapImage
3483
+ .querySelectorAll('.svgMap-country.svgMap-active')
3484
+ .forEach((el) => el.classList.remove('svgMap-active'));
3485
+ countryElement.parentNode.insertBefore(
3486
+ countryElement,
3487
+ this.persistentTooltipGroup || null
3488
+ );
3489
+ countryElement.classList.add('svgMap-active');
3490
+ this.setTooltipContent(this.getTooltipContent(countryID));
3491
+ this.showTooltip(e);
3492
+ }
3493
+ return;
3494
+ }
3495
+
3496
+ if (callbackResultClick === false) return;
3497
+
3498
+ this.mapImage
3499
+ .querySelectorAll('.svgMap-country.svgMap-active')
3500
+ .forEach((el) => el.classList.remove('svgMap-active'));
3501
+ countryElement.parentNode.insertBefore(
3502
+ countryElement,
3503
+ this.persistentTooltipGroup || null
3504
+ );
3505
+ countryElement.classList.add('svgMap-active');
3506
+ this.setTooltipContent(this.getTooltipContent(countryID));
3507
+ this.showTooltip(e);
3508
+ return;
3509
+ }
3510
+
3511
+ const willNavigate =
3512
+ hasLink &&
3513
+ (!isTouch || countryElement.classList.contains('svgMap-active'));
3514
+
3515
+ const shouldFireCallback =
3516
+ hasCallback && (!hasLink || !isTouch || willNavigate);
3517
+
3518
+ var callbackResult;
3519
+ if (shouldFireCallback) {
3520
+ callbackResult = this.options.onCountryClick(countryID, e);
3521
+ }
3522
+
3523
+ if (!hasLink) return;
3524
+
3525
+ if (callbackResult === false) return;
3526
+
3402
3527
  if (isTouch) {
3403
3528
  // Touch: only open if already active
3404
3529
  if (countryElement.classList.contains('svgMap-active')) {
3405
- if (target) window.open(link, target);
3530
+ if (linkTarget) window.open(link, linkTarget);
3406
3531
  else window.location.href = link;
3407
3532
  } else {
3408
- // first tap shows tooltip
3533
+ // first tap shows tooltip (or opens link immediately if tooltips are off)
3409
3534
  if (activeCountry) activeCountry.classList.remove('svgMap-active');
3410
3535
  activeCountry = countryElement;
3411
3536
  countryElement.classList.add('svgMap-active');
3412
- this.setTooltipContent(this.getTooltipContent(countryID));
3413
- this.showTooltip(e);
3537
+ if (this.options.showTooltips) {
3538
+ this.setTooltipContent(this.getTooltipContent(countryID));
3539
+ this.showTooltip(e);
3540
+ } else {
3541
+ if (linkTarget) window.open(link, linkTarget);
3542
+ else window.location.href = link;
3543
+ }
3414
3544
  }
3415
3545
  } else {
3416
3546
  // Desktop: open immediately
3417
- if (target) window.open(link, target);
3547
+ if (linkTarget) window.open(link, linkTarget);
3418
3548
  else window.location.href = link;
3419
3549
  }
3420
3550
  });
3421
3551
 
3552
+ this._clickTooltipOutsideHandler = function (ev) {
3553
+ if (ev.pointerType !== 'mouse') return;
3554
+ if (
3555
+ !this.options.showTooltips ||
3556
+ this.options.tooltipTrigger !== 'click' ||
3557
+ !this.tooltip
3558
+ ) {
3559
+ return;
3560
+ }
3561
+ if (!this.tooltip.classList.contains('svgMap-active')) return;
3562
+ var node = ev.target;
3563
+ if (
3564
+ node &&
3565
+ node.closest &&
3566
+ (node.closest('.svgMap-country') || node.closest('.svgMap-tooltip'))
3567
+ ) {
3568
+ return;
3569
+ }
3570
+ this.hideTooltip();
3571
+ if (this.mapImage) {
3572
+ this.mapImage
3573
+ .querySelectorAll('.svgMap-country.svgMap-active')
3574
+ .forEach(function (el) {
3575
+ el.classList.remove('svgMap-active');
3576
+ });
3577
+ }
3578
+ }.bind(this);
3579
+ document.addEventListener(
3580
+ 'pointerdown',
3581
+ this._clickTooltipOutsideHandler,
3582
+ true
3583
+ );
3584
+
3422
3585
  // Expose instance
3423
3586
  var me = this;
3424
3587
 
@@ -3484,13 +3647,82 @@ class svgMap {
3484
3647
  }
3485
3648
  }
3486
3649
 
3650
+ // Create the persistent tooltips
3651
+
3652
+ createPersistentTooltips(countryElements) {
3653
+ if (this.persistentTooltipGroup) {
3654
+ this.persistentTooltipGroup.remove();
3655
+ }
3656
+
3657
+ this.persistentTooltipGroup = document.createElementNS(
3658
+ 'http://www.w3.org/2000/svg',
3659
+ 'g'
3660
+ );
3661
+ this.persistentTooltipGroup.classList.add('svgMap-persistent-tooltips');
3662
+ this.mapImage.appendChild(this.persistentTooltipGroup);
3663
+
3664
+ countryElements.forEach(
3665
+ function (countryElement) {
3666
+ var countryID = countryElement.getAttribute('data-id');
3667
+ if (!this.shouldShowTooltipOnLoad(countryID)) {
3668
+ return;
3669
+ }
3670
+
3671
+ var boundingBox = countryElement.getBBox();
3672
+ var tooltipPosition = {
3673
+ x: boundingBox.x + boundingBox.width / 2,
3674
+ y: boundingBox.y + boundingBox.height / 2
3675
+ };
3676
+
3677
+ var tooltipObject = document.createElementNS(
3678
+ 'http://www.w3.org/2000/svg',
3679
+ 'foreignObject'
3680
+ );
3681
+ tooltipObject.setAttribute('x', tooltipPosition.x);
3682
+ tooltipObject.setAttribute('y', tooltipPosition.y);
3683
+ tooltipObject.setAttribute('width', 1);
3684
+ tooltipObject.setAttribute('height', 1);
3685
+ tooltipObject.classList.add('svgMap-persistent-tooltip-wrapper');
3686
+
3687
+ var tooltipElement = this.createElement(
3688
+ 'div',
3689
+ 'svgMap-persistent-tooltip',
3690
+ tooltipObject
3691
+ );
3692
+ tooltipElement.append(
3693
+ this.getTooltipContent(countryID, tooltipElement)
3694
+ );
3695
+ this.createElement('div', 'svgMap-tooltip-pointer', tooltipElement);
3696
+
3697
+ this.persistentTooltipGroup.appendChild(tooltipObject);
3698
+ }.bind(this)
3699
+ );
3700
+ }
3701
+
3702
+ // Check if a persistent tooltip should be shown on load
3703
+
3704
+ shouldShowTooltipOnLoad(countryID) {
3705
+ var persistent = this.options.persistentTooltips;
3706
+ var countryValues = this.options.data.values[countryID];
3707
+
3708
+ if (Array.isArray(persistent)) {
3709
+ return persistent.indexOf(countryID) !== -1;
3710
+ }
3711
+
3712
+ if (typeof persistent === 'function') {
3713
+ return persistent(countryID, countryValues);
3714
+ }
3715
+
3716
+ return false;
3717
+ }
3718
+
3487
3719
  // Create the tooltip content
3488
3720
 
3489
- getTooltipContent(countryID) {
3721
+ getTooltipContent(countryID, tooltipDiv = this.tooltip) {
3490
3722
  // Custom tooltip
3491
3723
  if (this.options.onGetTooltip) {
3492
3724
  var customDiv = this.options.onGetTooltip(
3493
- this.tooltip,
3725
+ tooltipDiv,
3494
3726
  countryID,
3495
3727
  this.options.data.values[countryID]
3496
3728
  );
@@ -4472,6 +4704,9 @@ class svgMap {
4472
4704
  // Show the tooltip
4473
4705
 
4474
4706
  showTooltip(e) {
4707
+ if (!this.tooltip) {
4708
+ return;
4709
+ }
4475
4710
  this.tooltip.classList.add('svgMap-active');
4476
4711
  this.moveTooltip(e);
4477
4712
  }
@@ -4479,12 +4714,18 @@ class svgMap {
4479
4714
  // Hide the tooltip
4480
4715
 
4481
4716
  hideTooltip() {
4717
+ if (!this.tooltip) {
4718
+ return;
4719
+ }
4482
4720
  this.tooltip.classList.remove('svgMap-active');
4483
4721
  }
4484
4722
 
4485
4723
  // Move the tooltip
4486
4724
 
4487
4725
  moveTooltip(e) {
4726
+ if (!this.tooltip) {
4727
+ return;
4728
+ }
4488
4729
  var x = e.pageX || (e.touches && e.touches[0] ? e.touches[0].pageX : null);
4489
4730
  var y = e.pageY || (e.touches && e.touches[0] ? e.touches[0].pageY : null);
4490
4731