svgmap 2.19.2 → 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
@@ -22,9 +22,8 @@ import 'svgmap/dist/svg-map.css';
22
22
  ### CDN
23
23
 
24
24
  ```html
25
- <script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
26
- <script src="https://cdn.jsdelivr.net/npm/svgmap@v2.18.0/dist/svg-map.umd.min.js"></script>
27
- <link href="https://cdn.jsdelivr.net/npm/svgmap@v2.18.0/dist/svg-map.min.css" rel="stylesheet">
25
+ <script src="https://cdn.jsdelivr.net/npm/svgmap@v2.19.2/dist/svg-map.umd.min.js"></script>
26
+ <link href="https://cdn.jsdelivr.net/npm/svgmap@v2.19.2/dist/svg-map.min.css" rel="stylesheet">
28
27
  ```
29
28
 
30
29
  ---
@@ -100,7 +99,11 @@ You can pass the following options into svgMap:
100
99
  | `hideFlag` | `boolean` | `false` | Hide the flag in tooltips |
101
100
  | `noDataText` | `string` | `'No data available'` | The text to be shown when no data is present |
102
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'`. |
103
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. |
104
107
  | `countries` | `object` | | Additional options specific to countries: |
105
108
  | &nbsp;&nbsp;&nbsp;`↳ EH` | `boolean` | `true` | When set to `false`, Western Sahara (EH) will be combined with Morocco (MA) |
106
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(
@@ -3090,6 +3108,10 @@ class svgMap {
3090
3108
  'svgMap-map-wrapper',
3091
3109
  this.mapContainer
3092
3110
  );
3111
+ this.mapWrapper.style.setProperty(
3112
+ '--svg-map-country-fill',
3113
+ this.toHex(this.options.colorNoData)
3114
+ );
3093
3115
  this.mapImage = document.createElementNS(
3094
3116
  'http://www.w3.org/2000/svg',
3095
3117
  'svg'
@@ -3204,6 +3226,7 @@ class svgMap {
3204
3226
  }.bind(this);
3205
3227
 
3206
3228
  // Add map elements
3229
+ var countryElements = [];
3207
3230
  Object.keys(mapPaths).forEach(
3208
3231
  function (countryID) {
3209
3232
  var countryData = this.mapPaths[countryID];
@@ -3225,6 +3248,7 @@ class svgMap {
3225
3248
  countryElement.classList.add('svgMap-country');
3226
3249
 
3227
3250
  this.mapImage.appendChild(countryElement);
3251
+ countryElements.push(countryElement);
3228
3252
 
3229
3253
  // Add tooltip when touch is used
3230
3254
  function handlePointerMove(e) {
@@ -3240,7 +3264,7 @@ class svgMap {
3240
3264
  this.hideTooltip();
3241
3265
  document
3242
3266
  .querySelectorAll('.svgMap-active')
3243
- .forEach(el => el.classList.remove('svgMap-active'));
3267
+ .forEach((el) => el.classList.remove('svgMap-active'));
3244
3268
  }
3245
3269
  }
3246
3270
 
@@ -3249,29 +3273,40 @@ class svgMap {
3249
3273
  countryElement.addEventListener(
3250
3274
  'pointerenter',
3251
3275
  function (e) {
3276
+ if (
3277
+ e.pointerType === 'mouse' &&
3278
+ this.options.showTooltips &&
3279
+ this.options.tooltipTrigger === 'click'
3280
+ ) {
3281
+ return;
3282
+ }
3283
+
3252
3284
  // Only add pointermove listener for non-touch pointers
3253
3285
  if (e.pointerType !== 'touch') {
3254
- document.addEventListener(
3255
- 'pointermove',
3256
- handlePointerMoveBound,
3257
- { passive: true }
3258
- );
3286
+ document.addEventListener('pointermove', handlePointerMoveBound, {
3287
+ passive: true
3288
+ });
3259
3289
  }
3260
3290
 
3261
3291
  document
3262
3292
  .querySelectorAll('.svgMap-active')
3263
- .forEach(el => el.classList.remove('svgMap-active'));
3293
+ .forEach((el) => el.classList.remove('svgMap-active'));
3264
3294
 
3265
- countryElement.parentNode.appendChild(countryElement);
3295
+ countryElement.parentNode.insertBefore(
3296
+ countryElement,
3297
+ this.persistentTooltipGroup || null
3298
+ );
3266
3299
  countryElement.classList.add('svgMap-active');
3267
3300
 
3268
3301
  const countryID = countryElement.getAttribute('data-id');
3269
- this.setTooltipContent(this.getTooltipContent(countryID));
3270
- this.showTooltip(e);
3302
+ if (this.options.showTooltips) {
3303
+ this.setTooltipContent(this.getTooltipContent(countryID));
3304
+ this.showTooltip(e);
3271
3305
 
3272
- // For touch, move tooltip to the touch position and keep it there
3273
- if (e.pointerType === 'touch') {
3274
- 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
+ }
3275
3310
  }
3276
3311
  }.bind(this)
3277
3312
  );
@@ -3290,7 +3325,10 @@ class svgMap {
3290
3325
  'touchend',
3291
3326
  function (e) {
3292
3327
  const touch = e.changedTouches[0];
3293
- const elementAtEnd = document.elementFromPoint(touch.clientX, touch.clientY);
3328
+ const elementAtEnd = document.elementFromPoint(
3329
+ touch.clientX,
3330
+ touch.clientY
3331
+ );
3294
3332
 
3295
3333
  // Only hide if touch ended outside the country or tooltip
3296
3334
  if (
@@ -3301,7 +3339,7 @@ class svgMap {
3301
3339
  this.hideTooltip();
3302
3340
  document
3303
3341
  .querySelectorAll('.svgMap-active')
3304
- .forEach(el => el.classList.remove('svgMap-active'));
3342
+ .forEach((el) => el.classList.remove('svgMap-active'));
3305
3343
  }
3306
3344
  }.bind(this),
3307
3345
  { passive: true }
@@ -3312,11 +3350,22 @@ class svgMap {
3312
3350
  'pointerleave',
3313
3351
  function (e) {
3314
3352
  if (e.pointerType !== 'touch') {
3315
- document.removeEventListener('pointermove', handlePointerMoveBound);
3316
- this.hideTooltip();
3317
- document
3318
- .querySelectorAll('.svgMap-active')
3319
- .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
+ }
3320
3369
  }
3321
3370
  }.bind(this)
3322
3371
  );
@@ -3336,7 +3385,7 @@ class svgMap {
3336
3385
  this.hideTooltip();
3337
3386
  document
3338
3387
  .querySelectorAll('.svgMap-active')
3339
- .forEach(el => el.classList.remove('svgMap-active'));
3388
+ .forEach((el) => el.classList.remove('svgMap-active'));
3340
3389
  }.bind(this),
3341
3390
  { passive: true }
3342
3391
  );
@@ -3360,17 +3409,29 @@ class svgMap {
3360
3409
  }.bind(this)
3361
3410
  );
3362
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
+
3363
3420
  let pointerStart = null;
3364
3421
  let activeCountry = null;
3365
3422
 
3366
- this.mapImage.addEventListener('pointerdown', e => {
3367
- // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
3368
- 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;
3369
3428
 
3370
- pointerStart = { x: e.clientX, y: e.clientY };
3371
- }, { passive: true });
3429
+ pointerStart = { x: e.clientX, y: e.clientY };
3430
+ },
3431
+ { passive: true }
3432
+ );
3372
3433
 
3373
- this.mapImage.addEventListener('pointerup', e => {
3434
+ this.mapImage.addEventListener('pointerup', (e) => {
3374
3435
  // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
3375
3436
  if (e.button !== 0) return;
3376
3437
 
@@ -3390,31 +3451,137 @@ class svgMap {
3390
3451
 
3391
3452
  const countryID = countryElement.getAttribute('data-id');
3392
3453
  const link = countryElement.getAttribute('data-link');
3393
- const target = countryElement.getAttribute('data-link-target');
3394
- if (!link) return;
3395
-
3454
+ const linkTarget = countryElement.getAttribute('data-link-target');
3455
+ const hasCallback = typeof this.options.onCountryClick === 'function';
3456
+ const hasLink = !!link;
3396
3457
  const isTouch = e.pointerType === 'touch' || e.pointerType === 'pen';
3397
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
+
3398
3527
  if (isTouch) {
3399
3528
  // Touch: only open if already active
3400
3529
  if (countryElement.classList.contains('svgMap-active')) {
3401
- if (target) window.open(link, target);
3530
+ if (linkTarget) window.open(link, linkTarget);
3402
3531
  else window.location.href = link;
3403
3532
  } else {
3404
- // first tap shows tooltip
3533
+ // first tap shows tooltip (or opens link immediately if tooltips are off)
3405
3534
  if (activeCountry) activeCountry.classList.remove('svgMap-active');
3406
3535
  activeCountry = countryElement;
3407
3536
  countryElement.classList.add('svgMap-active');
3408
- this.setTooltipContent(this.getTooltipContent(countryID));
3409
- 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
+ }
3410
3544
  }
3411
3545
  } else {
3412
3546
  // Desktop: open immediately
3413
- if (target) window.open(link, target);
3547
+ if (linkTarget) window.open(link, linkTarget);
3414
3548
  else window.location.href = link;
3415
3549
  }
3416
3550
  });
3417
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
+
3418
3585
  // Expose instance
3419
3586
  var me = this;
3420
3587
 
@@ -3480,13 +3647,82 @@ class svgMap {
3480
3647
  }
3481
3648
  }
3482
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
+
3483
3719
  // Create the tooltip content
3484
3720
 
3485
- getTooltipContent(countryID) {
3721
+ getTooltipContent(countryID, tooltipDiv = this.tooltip) {
3486
3722
  // Custom tooltip
3487
3723
  if (this.options.onGetTooltip) {
3488
3724
  var customDiv = this.options.onGetTooltip(
3489
- this.tooltip,
3725
+ tooltipDiv,
3490
3726
  countryID,
3491
3727
  this.options.data.values[countryID]
3492
3728
  );
@@ -4468,6 +4704,9 @@ class svgMap {
4468
4704
  // Show the tooltip
4469
4705
 
4470
4706
  showTooltip(e) {
4707
+ if (!this.tooltip) {
4708
+ return;
4709
+ }
4471
4710
  this.tooltip.classList.add('svgMap-active');
4472
4711
  this.moveTooltip(e);
4473
4712
  }
@@ -4475,12 +4714,18 @@ class svgMap {
4475
4714
  // Hide the tooltip
4476
4715
 
4477
4716
  hideTooltip() {
4717
+ if (!this.tooltip) {
4718
+ return;
4719
+ }
4478
4720
  this.tooltip.classList.remove('svgMap-active');
4479
4721
  }
4480
4722
 
4481
4723
  // Move the tooltip
4482
4724
 
4483
4725
  moveTooltip(e) {
4726
+ if (!this.tooltip) {
4727
+ return;
4728
+ }
4484
4729
  var x = e.pageX || (e.touches && e.touches[0] ? e.touches[0].pageX : null);
4485
4730
  var y = e.pageY || (e.touches && e.touches[0] ? e.touches[0].pageY : null);
4486
4731