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