svgmap 2.19.3 → 2.20.1

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/dist/index.js CHANGED
@@ -2342,6 +2342,15 @@ class svgMap {
2342
2342
  // Set to true to open the link on mobile devices, set to false (default) to show the tooltip
2343
2343
  touchLink: false,
2344
2344
 
2345
+ // When false, disables hover/touch-following tooltips (not the on-map persistent labels; see persistentTooltips)
2346
+ showTooltips: true,
2347
+
2348
+ // 'hover' (default): mouse shows tooltip on enter. 'click': mouse opens tooltip on click; touch/pen unchanged.
2349
+ tooltipTrigger: 'hover',
2350
+
2351
+ // Persistent on-map tooltips: an array of country IDs, or a function (countryID, countryValues) => boolean
2352
+ persistentTooltips: false,
2353
+
2345
2354
  // Set to true to show the to show a zoom reset button
2346
2355
  showZoomReset: false,
2347
2356
 
@@ -2350,6 +2359,10 @@ class svgMap {
2350
2359
  return null;
2351
2360
  },
2352
2361
 
2362
+ // Called on country click (pointer released without dragging). Receives
2363
+ // (countryID, event). Return false to skip opening data.values[*].link.
2364
+ onCountryClick: null,
2365
+
2353
2366
  // Country specific options
2354
2367
  countries: {
2355
2368
  // Western Sahara: Set to false to combine Morocco (MA) and Western Sahara (EH)
@@ -2389,6 +2402,9 @@ class svgMap {
2389
2402
  // Wrapper element
2390
2403
  this.wrapper = document.getElementById(this.options.targetElementID);
2391
2404
  this.wrapper.classList.add('svgMap-wrapper');
2405
+ if (typeof this.options.onCountryClick === 'function') {
2406
+ this.wrapper.classList.add('svgMap-country-click-callback');
2407
+ }
2392
2408
 
2393
2409
  // Container element
2394
2410
  this.container = document.createElement('div');
@@ -3080,7 +3096,9 @@ class svgMap {
3080
3096
 
3081
3097
  createMap() {
3082
3098
  // Create the tooltip
3083
- this.createTooltip();
3099
+ if (this.options.showTooltips) {
3100
+ this.createTooltip();
3101
+ }
3084
3102
 
3085
3103
  // Create map wrappers
3086
3104
  this.mapWrapper = this.createElement(
@@ -3206,6 +3224,103 @@ class svgMap {
3206
3224
  }.bind(this);
3207
3225
 
3208
3226
  // Add map elements
3227
+ var countryElements = [];
3228
+
3229
+ const clearActive = function clearActive() {
3230
+ this.mapImage
3231
+ .querySelectorAll('.svgMap-active')
3232
+ .forEach((el) => el.classList.remove('svgMap-active'));
3233
+ }.bind(this);
3234
+
3235
+ const isClickTooltip =
3236
+ this.options.showTooltips && this.options.tooltipTrigger === 'click';
3237
+
3238
+ const getCountryFromEvent = function (e) {
3239
+ return e.target && e.target.closest
3240
+ ? e.target.closest('.svgMap-country')
3241
+ : null;
3242
+ };
3243
+
3244
+ const raiseCountry = function (countryElement, setActive) {
3245
+ if (setActive) {
3246
+ clearActive();
3247
+ }
3248
+ countryElement.parentNode.insertBefore(
3249
+ countryElement,
3250
+ this.persistentTooltipGroup || null
3251
+ );
3252
+ if (setActive) {
3253
+ countryElement.classList.add('svgMap-active');
3254
+ }
3255
+ }.bind(this);
3256
+
3257
+ const showCountryTooltip = function (countryElement, e, setActive) {
3258
+ raiseCountry(countryElement, setActive);
3259
+ this.setTooltipContent(this.getTooltipContent(countryElement.dataset.id));
3260
+ this.showTooltip(e);
3261
+ }.bind(this);
3262
+
3263
+ // Touch only: preview tooltip on finger down without marking country active
3264
+ // (active is set on pointerup so link countries keep two-tap navigation)
3265
+ this.mapImage.addEventListener(
3266
+ 'pointerdown',
3267
+ (e) => {
3268
+ if (!this.options.showTooltips || e.pointerType !== 'touch') {
3269
+ return;
3270
+ }
3271
+
3272
+ const countryElement = getCountryFromEvent(e);
3273
+ if (!countryElement) {
3274
+ this.hideTooltip();
3275
+ return;
3276
+ }
3277
+
3278
+ showCountryTooltip(countryElement, e, false);
3279
+ this.moveTooltip(e);
3280
+ },
3281
+ { passive: true }
3282
+ );
3283
+
3284
+ this.mapImage.addEventListener(
3285
+ 'pointercancel',
3286
+ (e) => {
3287
+ if (e.pointerType === 'touch') {
3288
+ this.hideTooltip();
3289
+ }
3290
+ },
3291
+ { passive: true }
3292
+ );
3293
+
3294
+ // Hover (mouse/pen) and touch drag: raise country + optional floating tooltip
3295
+ this.mapImage.addEventListener(
3296
+ 'pointermove',
3297
+ (e) => {
3298
+ const countryElement = getCountryFromEvent(e);
3299
+ if (!countryElement) {
3300
+ clearActive();
3301
+ if (this.options.showTooltips) {
3302
+ this.hideTooltip();
3303
+ }
3304
+ return;
3305
+ }
3306
+
3307
+ const mouseClickMode = e.pointerType === 'mouse' && isClickTooltip;
3308
+
3309
+ // Always raise hovered country (SVG paint order + .svgMap-active stroke)
3310
+ if (!this.options.showTooltips || mouseClickMode) {
3311
+ raiseCountry(countryElement, true);
3312
+ return;
3313
+ }
3314
+
3315
+ showCountryTooltip(countryElement, e, true);
3316
+
3317
+ if (e.pointerType === 'touch') {
3318
+ this.moveTooltip(e);
3319
+ }
3320
+ },
3321
+ { passive: true }
3322
+ );
3323
+
3209
3324
  Object.keys(mapPaths).forEach(
3210
3325
  function (countryID) {
3211
3326
  var countryData = this.mapPaths[countryID];
@@ -3223,156 +3338,50 @@ class svgMap {
3223
3338
  'id',
3224
3339
  this.id + '-map-country-' + countryID
3225
3340
  );
3226
- countryElement.setAttribute('data-id', countryID);
3341
+ countryElement.dataset.id = countryID;
3227
3342
  countryElement.classList.add('svgMap-country');
3228
3343
 
3229
3344
  this.mapImage.appendChild(countryElement);
3230
-
3231
- // Add tooltip when touch is used
3232
- function handlePointerMove(e) {
3233
- if (e.pointerType === 'touch') return;
3234
-
3235
- const target = document.elementFromPoint(e.clientX, e.clientY);
3236
-
3237
- if (
3238
- !target ||
3239
- (!target.closest('.svgMap-country') &&
3240
- !target.closest('.svgMap-tooltip'))
3241
- ) {
3242
- this.hideTooltip();
3243
- document
3244
- .querySelectorAll('.svgMap-active')
3245
- .forEach(el => el.classList.remove('svgMap-active'));
3246
- }
3247
- }
3248
-
3249
- const handlePointerMoveBound = handlePointerMove.bind(this);
3250
-
3251
- countryElement.addEventListener(
3252
- 'pointerenter',
3253
- function (e) {
3254
- // Only add pointermove listener for non-touch pointers
3255
- if (e.pointerType !== 'touch') {
3256
- document.addEventListener(
3257
- 'pointermove',
3258
- handlePointerMoveBound,
3259
- { passive: true }
3260
- );
3261
- }
3262
-
3263
- document
3264
- .querySelectorAll('.svgMap-active')
3265
- .forEach(el => el.classList.remove('svgMap-active'));
3266
-
3267
- countryElement.parentNode.appendChild(countryElement);
3268
- countryElement.classList.add('svgMap-active');
3269
-
3270
- const countryID = countryElement.getAttribute('data-id');
3271
- this.setTooltipContent(this.getTooltipContent(countryID));
3272
- this.showTooltip(e);
3273
-
3274
- // For touch, move tooltip to the touch position and keep it there
3275
- if (e.pointerType === 'touch') {
3276
- this.moveTooltip(e);
3277
- }
3278
- }.bind(this)
3279
- );
3280
-
3281
- // Handle touch move - update tooltip position while panning
3282
- countryElement.addEventListener(
3283
- 'touchmove',
3284
- function (e) {
3285
- this.moveTooltip(e);
3286
- }.bind(this),
3287
- { passive: true }
3288
- );
3289
-
3290
- // Handle touch end - remove active state and hide tooltip
3291
- countryElement.addEventListener(
3292
- 'touchend',
3293
- function (e) {
3294
- const touch = e.changedTouches[0];
3295
- const elementAtEnd = document.elementFromPoint(touch.clientX, touch.clientY);
3296
-
3297
- // Only hide if touch ended outside the country or tooltip
3298
- if (
3299
- !elementAtEnd ||
3300
- (!elementAtEnd.closest('.svgMap-country') &&
3301
- !elementAtEnd.closest('.svgMap-tooltip'))
3302
- ) {
3303
- this.hideTooltip();
3304
- document
3305
- .querySelectorAll('.svgMap-active')
3306
- .forEach(el => el.classList.remove('svgMap-active'));
3307
- }
3308
- }.bind(this),
3309
- { passive: true }
3310
- );
3311
-
3312
- // Remove pointermove listener when leaving non-touch pointer
3313
- countryElement.addEventListener(
3314
- 'pointerleave',
3315
- function (e) {
3316
- if (e.pointerType !== 'touch') {
3317
- document.removeEventListener('pointermove', handlePointerMoveBound);
3318
- this.hideTooltip();
3319
- document
3320
- .querySelectorAll('.svgMap-active')
3321
- .forEach(el => el.classList.remove('svgMap-active'));
3322
- }
3323
- }.bind(this)
3324
- );
3325
-
3326
- document.addEventListener(
3327
- 'pointerover',
3328
- function (e) {
3329
- if (e.pointerType !== 'touch') return;
3330
-
3331
- if (
3332
- e.target.closest('.svgMap-country') ||
3333
- e.target.closest('.svgMap-tooltip')
3334
- ) {
3335
- return;
3336
- }
3337
-
3338
- this.hideTooltip();
3339
- document
3340
- .querySelectorAll('.svgMap-active')
3341
- .forEach(el => el.classList.remove('svgMap-active'));
3342
- }.bind(this),
3343
- { passive: true }
3344
- );
3345
+ countryElements.push(countryElement);
3345
3346
 
3346
3347
  if (
3347
3348
  this.options.data.values &&
3348
3349
  this.options.data.values[countryID] &&
3349
3350
  this.options.data.values[countryID]['link']
3350
3351
  ) {
3351
- countryElement.setAttribute(
3352
- 'data-link',
3353
- this.options.data.values[countryID]['link']
3354
- );
3352
+ countryElement.dataset.link =
3353
+ this.options.data.values[countryID]['link'];
3355
3354
  if (this.options.data.values[countryID]['linkTarget']) {
3356
- countryElement.setAttribute(
3357
- 'data-link-target',
3358
- this.options.data.values[countryID]['linkTarget']
3359
- );
3355
+ countryElement.dataset.linkTarget =
3356
+ this.options.data.values[countryID]['linkTarget'];
3360
3357
  }
3361
3358
  }
3362
3359
  }.bind(this)
3363
3360
  );
3364
3361
 
3362
+ var persistent = this.options.persistentTooltips;
3363
+ if (
3364
+ persistent &&
3365
+ (Array.isArray(persistent) || typeof persistent === 'function')
3366
+ ) {
3367
+ this.createPersistentTooltips(countryElements);
3368
+ }
3369
+
3365
3370
  let pointerStart = null;
3366
3371
  let activeCountry = null;
3367
3372
 
3368
- this.mapImage.addEventListener('pointerdown', e => {
3369
- // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
3370
- if (e.button !== 0) return;
3373
+ this.mapImage.addEventListener(
3374
+ 'pointerdown',
3375
+ (e) => {
3376
+ // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
3377
+ if (e.button !== 0) return;
3371
3378
 
3372
- pointerStart = { x: e.clientX, y: e.clientY };
3373
- }, { passive: true });
3379
+ pointerStart = { x: e.clientX, y: e.clientY };
3380
+ },
3381
+ { passive: true }
3382
+ );
3374
3383
 
3375
- this.mapImage.addEventListener('pointerup', e => {
3384
+ this.mapImage.addEventListener('pointerup', (e) => {
3376
3385
  // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
3377
3386
  if (e.button !== 0) return;
3378
3387
 
@@ -3390,33 +3399,121 @@ class svgMap {
3390
3399
  const countryElement = e.target.closest('.svgMap-country');
3391
3400
  if (!countryElement) return;
3392
3401
 
3393
- const countryID = countryElement.getAttribute('data-id');
3394
- const link = countryElement.getAttribute('data-link');
3395
- const target = countryElement.getAttribute('data-link-target');
3396
- if (!link) return;
3397
-
3402
+ const countryID = countryElement.dataset.id;
3403
+ const link = countryElement.dataset.link;
3404
+ const linkTarget = countryElement.dataset.linkTarget;
3405
+ const hasCallback = typeof this.options.onCountryClick === 'function';
3406
+ const hasLink = !!link;
3398
3407
  const isTouch = e.pointerType === 'touch' || e.pointerType === 'pen';
3399
3408
 
3409
+ const isClickTooltipMouse = e.pointerType === 'mouse' && isClickTooltip;
3410
+
3411
+ if (!hasLink && !hasCallback && !isClickTooltipMouse) return;
3412
+
3413
+ if (isClickTooltipMouse) {
3414
+ const willNavigate =
3415
+ hasLink && countryElement.classList.contains('svgMap-active');
3416
+ const shouldFireCallback = hasCallback && (!hasLink || willNavigate);
3417
+
3418
+ var callbackResultClick;
3419
+ if (shouldFireCallback) {
3420
+ callbackResultClick = this.options.onCountryClick(countryID, e);
3421
+ }
3422
+
3423
+ if (hasLink) {
3424
+ if (callbackResultClick === false) return;
3425
+ if (countryElement.classList.contains('svgMap-active')) {
3426
+ if (linkTarget) window.open(link, linkTarget);
3427
+ else window.location.href = link;
3428
+ } else {
3429
+ clearActive();
3430
+ countryElement.parentNode.insertBefore(
3431
+ countryElement,
3432
+ this.persistentTooltipGroup || null
3433
+ );
3434
+ countryElement.classList.add('svgMap-active');
3435
+ this.setTooltipContent(this.getTooltipContent(countryID));
3436
+ this.showTooltip(e);
3437
+ }
3438
+ return;
3439
+ }
3440
+
3441
+ if (callbackResultClick === false) return;
3442
+
3443
+ clearActive();
3444
+ countryElement.parentNode.insertBefore(
3445
+ countryElement,
3446
+ this.persistentTooltipGroup || null
3447
+ );
3448
+ countryElement.classList.add('svgMap-active');
3449
+ this.setTooltipContent(this.getTooltipContent(countryID));
3450
+ this.showTooltip(e);
3451
+ return;
3452
+ }
3453
+
3454
+ const willNavigate =
3455
+ hasLink &&
3456
+ (!isTouch || countryElement.classList.contains('svgMap-active'));
3457
+
3458
+ const shouldFireCallback =
3459
+ hasCallback && (!hasLink || !isTouch || willNavigate);
3460
+
3461
+ var callbackResult;
3462
+ if (shouldFireCallback) {
3463
+ callbackResult = this.options.onCountryClick(countryID, e);
3464
+ }
3465
+
3466
+ if (!hasLink) return;
3467
+
3468
+ if (callbackResult === false) return;
3469
+
3400
3470
  if (isTouch) {
3401
3471
  // Touch: only open if already active
3402
3472
  if (countryElement.classList.contains('svgMap-active')) {
3403
- if (target) window.open(link, target);
3473
+ if (linkTarget) window.open(link, linkTarget);
3404
3474
  else window.location.href = link;
3405
3475
  } else {
3406
- // first tap shows tooltip
3476
+ // first tap shows tooltip (or opens link immediately if tooltips are off)
3407
3477
  if (activeCountry) activeCountry.classList.remove('svgMap-active');
3408
3478
  activeCountry = countryElement;
3409
3479
  countryElement.classList.add('svgMap-active');
3410
- this.setTooltipContent(this.getTooltipContent(countryID));
3411
- this.showTooltip(e);
3480
+ if (this.options.showTooltips) {
3481
+ this.setTooltipContent(this.getTooltipContent(countryID));
3482
+ this.showTooltip(e);
3483
+ } else {
3484
+ if (linkTarget) window.open(link, linkTarget);
3485
+ else window.location.href = link;
3486
+ }
3412
3487
  }
3413
3488
  } else {
3414
3489
  // Desktop: open immediately
3415
- if (target) window.open(link, target);
3490
+ if (linkTarget) window.open(link, linkTarget);
3416
3491
  else window.location.href = link;
3417
3492
  }
3418
3493
  });
3419
3494
 
3495
+ this._clickTooltipOutsideHandler = function (ev) {
3496
+ if (!this.tooltip || !this.tooltip.classList.contains('svgMap-active')) {
3497
+ return;
3498
+ }
3499
+ var node = ev.target;
3500
+ if (
3501
+ node &&
3502
+ node.closest &&
3503
+ (node.closest('.svgMap-country') || node.closest('.svgMap-tooltip'))
3504
+ ) {
3505
+ return;
3506
+ }
3507
+ this.hideTooltip();
3508
+ if (this.mapImage) {
3509
+ this.mapImage
3510
+ .querySelectorAll('.svgMap-country.svgMap-active')
3511
+ .forEach(function (el) {
3512
+ el.classList.remove('svgMap-active');
3513
+ });
3514
+ }
3515
+ }.bind(this);
3516
+
3420
3517
  // Expose instance
3421
3518
  var me = this;
3422
3519
 
@@ -3482,13 +3579,82 @@ class svgMap {
3482
3579
  }
3483
3580
  }
3484
3581
 
3582
+ // Create the persistent tooltips
3583
+
3584
+ createPersistentTooltips(countryElements) {
3585
+ if (this.persistentTooltipGroup) {
3586
+ this.persistentTooltipGroup.remove();
3587
+ }
3588
+
3589
+ this.persistentTooltipGroup = document.createElementNS(
3590
+ 'http://www.w3.org/2000/svg',
3591
+ 'g'
3592
+ );
3593
+ this.persistentTooltipGroup.classList.add('svgMap-persistent-tooltips');
3594
+ this.mapImage.appendChild(this.persistentTooltipGroup);
3595
+
3596
+ countryElements.forEach(
3597
+ function (countryElement) {
3598
+ var countryID = countryElement.dataset.id;
3599
+ if (!this.shouldShowTooltipOnLoad(countryID)) {
3600
+ return;
3601
+ }
3602
+
3603
+ var boundingBox = countryElement.getBBox();
3604
+ var tooltipPosition = {
3605
+ x: boundingBox.x + boundingBox.width / 2,
3606
+ y: boundingBox.y + boundingBox.height / 2
3607
+ };
3608
+
3609
+ var tooltipObject = document.createElementNS(
3610
+ 'http://www.w3.org/2000/svg',
3611
+ 'foreignObject'
3612
+ );
3613
+ tooltipObject.setAttribute('x', tooltipPosition.x);
3614
+ tooltipObject.setAttribute('y', tooltipPosition.y);
3615
+ tooltipObject.setAttribute('width', 1);
3616
+ tooltipObject.setAttribute('height', 1);
3617
+ tooltipObject.classList.add('svgMap-persistent-tooltip-wrapper');
3618
+
3619
+ var tooltipElement = this.createElement(
3620
+ 'div',
3621
+ 'svgMap-persistent-tooltip',
3622
+ tooltipObject
3623
+ );
3624
+ tooltipElement.append(
3625
+ this.getTooltipContent(countryID, tooltipElement)
3626
+ );
3627
+ this.createElement('div', 'svgMap-tooltip-pointer', tooltipElement);
3628
+
3629
+ this.persistentTooltipGroup.appendChild(tooltipObject);
3630
+ }.bind(this)
3631
+ );
3632
+ }
3633
+
3634
+ // Check if a persistent tooltip should be shown on load
3635
+
3636
+ shouldShowTooltipOnLoad(countryID) {
3637
+ var persistent = this.options.persistentTooltips;
3638
+ var countryValues = this.options.data.values[countryID];
3639
+
3640
+ if (Array.isArray(persistent)) {
3641
+ return persistent.indexOf(countryID) !== -1;
3642
+ }
3643
+
3644
+ if (typeof persistent === 'function') {
3645
+ return persistent(countryID, countryValues);
3646
+ }
3647
+
3648
+ return false;
3649
+ }
3650
+
3485
3651
  // Create the tooltip content
3486
3652
 
3487
- getTooltipContent(countryID) {
3653
+ getTooltipContent(countryID, tooltipDiv = this.tooltip) {
3488
3654
  // Custom tooltip
3489
3655
  if (this.options.onGetTooltip) {
3490
3656
  var customDiv = this.options.onGetTooltip(
3491
- this.tooltip,
3657
+ tooltipDiv,
3492
3658
  countryID,
3493
3659
  this.options.data.values[countryID]
3494
3660
  );
@@ -4470,19 +4636,50 @@ class svgMap {
4470
4636
  // Show the tooltip
4471
4637
 
4472
4638
  showTooltip(e) {
4639
+ if (!this.tooltip) {
4640
+ return;
4641
+ }
4473
4642
  this.tooltip.classList.add('svgMap-active');
4643
+
4644
+ if (
4645
+ this.options.showTooltips &&
4646
+ this.options.tooltipTrigger === 'click' &&
4647
+ e.pointerType === 'mouse'
4648
+ ) {
4649
+ // don't register event listener in the same frame
4650
+ // to prevent it from being triggered immediately
4651
+ requestAnimationFrame(() => {
4652
+ document.addEventListener(
4653
+ 'pointerdown',
4654
+ this._clickTooltipOutsideHandler,
4655
+ { once: true, passive: true }
4656
+ );
4657
+ });
4658
+ }
4659
+
4474
4660
  this.moveTooltip(e);
4475
4661
  }
4476
4662
 
4477
4663
  // Hide the tooltip
4478
4664
 
4479
4665
  hideTooltip() {
4666
+ if (!this.tooltip) {
4667
+ return;
4668
+ }
4669
+
4480
4670
  this.tooltip.classList.remove('svgMap-active');
4671
+ document.removeEventListener(
4672
+ 'pointerdown',
4673
+ this._clickTooltipOutsideHandler
4674
+ );
4481
4675
  }
4482
4676
 
4483
4677
  // Move the tooltip
4484
4678
 
4485
4679
  moveTooltip(e) {
4680
+ if (!this.tooltip) {
4681
+ return;
4682
+ }
4486
4683
  var x = e.pageX || (e.touches && e.touches[0] ? e.touches[0].pageX : null);
4487
4684
  var y = e.pageY || (e.touches && e.touches[0] ? e.touches[0].pageY : null);
4488
4685