svgmap 2.20.0 → 2.21.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/dist/index.js CHANGED
@@ -2373,7 +2373,34 @@ class svgMap {
2373
2373
  showContinentSelector: false,
2374
2374
 
2375
2375
  // Reset zoom on resize
2376
- resetZoomOnResize: false
2376
+ resetZoomOnResize: false,
2377
+
2378
+ // Static pins: false | string[] | function(countryID, countryValues) => boolean
2379
+ staticPins: false,
2380
+
2381
+ // Default pin fill color
2382
+ pinColor: '#000000',
2383
+
2384
+ // Default pin stroke color and width (circle pins; width is in screen pixels with non-scaling stroke)
2385
+ pinStrokeColor: '#ffffff',
2386
+ pinStrokeWidth: 1.5,
2387
+
2388
+ // Default pin radius in SVG units (viewBox is 2000 × 1001)
2389
+ pinSize: 8,
2390
+
2391
+ // Custom pin element: function(countryID, countryValues) => SVGElement | null
2392
+ onGetPin: null,
2393
+
2394
+ // Image URL to use as a pin instead of the default circle (can also be set per-country via values[id].pinImage)
2395
+ pinImage: null,
2396
+
2397
+ // Width and height of the pin image in SVG units (viewBox is 2000 × 1001)
2398
+ pinImageWidth: 20,
2399
+ pinImageHeight: 20,
2400
+
2401
+ // Offset from computed pin position, in SVG units (added after auto center or pinX/pinY)
2402
+ pinOffsetX: 0,
2403
+ pinOffsetY: 0
2377
2404
  };
2378
2405
 
2379
2406
  this.options = Object.assign({}, defaultOptions, options);
@@ -3225,6 +3252,102 @@ class svgMap {
3225
3252
 
3226
3253
  // Add map elements
3227
3254
  var countryElements = [];
3255
+
3256
+ const clearActive = function clearActive() {
3257
+ this.mapImage
3258
+ .querySelectorAll('.svgMap-active')
3259
+ .forEach((el) => el.classList.remove('svgMap-active'));
3260
+ }.bind(this);
3261
+
3262
+ const isClickTooltip =
3263
+ this.options.showTooltips && this.options.tooltipTrigger === 'click';
3264
+
3265
+ const getCountryFromEvent = function (e) {
3266
+ return e.target && e.target.closest
3267
+ ? e.target.closest('.svgMap-country')
3268
+ : null;
3269
+ };
3270
+
3271
+ const raiseCountry = function (countryElement, setActive) {
3272
+ if (setActive) {
3273
+ clearActive();
3274
+ }
3275
+ countryElement.parentNode.insertBefore(
3276
+ countryElement,
3277
+ this.persistentTooltipGroup || this.pinGroup || null
3278
+ );
3279
+ if (setActive) {
3280
+ countryElement.classList.add('svgMap-active');
3281
+ }
3282
+ }.bind(this);
3283
+
3284
+ const showCountryTooltip = function (countryElement, e, setActive) {
3285
+ raiseCountry(countryElement, setActive);
3286
+ this.setTooltipContent(this.getTooltipContent(countryElement.dataset.id));
3287
+ this.showTooltip(e);
3288
+ }.bind(this);
3289
+
3290
+ // Touch only: preview tooltip on finger down without marking country active
3291
+ // (active is set on pointerup so link countries keep two-tap navigation)
3292
+ this.mapImage.addEventListener(
3293
+ 'pointerdown',
3294
+ (e) => {
3295
+ if (!this.options.showTooltips || e.pointerType !== 'touch') {
3296
+ return;
3297
+ }
3298
+
3299
+ const countryElement = getCountryFromEvent(e);
3300
+ if (!countryElement) {
3301
+ this.hideTooltip();
3302
+ return;
3303
+ }
3304
+
3305
+ showCountryTooltip(countryElement, e, false);
3306
+ this.moveTooltip(e);
3307
+ },
3308
+ { passive: true }
3309
+ );
3310
+
3311
+ this.mapImage.addEventListener(
3312
+ 'pointercancel',
3313
+ (e) => {
3314
+ if (e.pointerType === 'touch') {
3315
+ this.hideTooltip();
3316
+ }
3317
+ },
3318
+ { passive: true }
3319
+ );
3320
+
3321
+ // Hover (mouse/pen) and touch drag: raise country + optional floating tooltip
3322
+ this.mapImage.addEventListener(
3323
+ 'pointermove',
3324
+ (e) => {
3325
+ const countryElement = getCountryFromEvent(e);
3326
+ if (!countryElement) {
3327
+ clearActive();
3328
+ if (this.options.showTooltips) {
3329
+ this.hideTooltip();
3330
+ }
3331
+ return;
3332
+ }
3333
+
3334
+ const mouseClickMode = e.pointerType === 'mouse' && isClickTooltip;
3335
+
3336
+ // Always raise hovered country (SVG paint order + .svgMap-active stroke)
3337
+ if (!this.options.showTooltips || mouseClickMode) {
3338
+ raiseCountry(countryElement, true);
3339
+ return;
3340
+ }
3341
+
3342
+ showCountryTooltip(countryElement, e, true);
3343
+
3344
+ if (e.pointerType === 'touch') {
3345
+ this.moveTooltip(e);
3346
+ }
3347
+ },
3348
+ { passive: true }
3349
+ );
3350
+
3228
3351
  Object.keys(mapPaths).forEach(
3229
3352
  function (countryID) {
3230
3353
  var countryData = this.mapPaths[countryID];
@@ -3242,166 +3365,22 @@ class svgMap {
3242
3365
  'id',
3243
3366
  this.id + '-map-country-' + countryID
3244
3367
  );
3245
- countryElement.setAttribute('data-id', countryID);
3368
+ countryElement.dataset.id = countryID;
3246
3369
  countryElement.classList.add('svgMap-country');
3247
3370
 
3248
3371
  this.mapImage.appendChild(countryElement);
3249
3372
  countryElements.push(countryElement);
3250
3373
 
3251
- // Add tooltip when touch is used
3252
- function handlePointerMove(e) {
3253
- if (e.pointerType === 'touch') return;
3254
-
3255
- const target = document.elementFromPoint(e.clientX, e.clientY);
3256
-
3257
- if (
3258
- !target ||
3259
- (!target.closest('.svgMap-country') &&
3260
- !target.closest('.svgMap-tooltip'))
3261
- ) {
3262
- this.hideTooltip();
3263
- document
3264
- .querySelectorAll('.svgMap-active')
3265
- .forEach((el) => el.classList.remove('svgMap-active'));
3266
- }
3267
- }
3268
-
3269
- const handlePointerMoveBound = handlePointerMove.bind(this);
3270
-
3271
- countryElement.addEventListener(
3272
- 'pointerenter',
3273
- function (e) {
3274
- if (
3275
- e.pointerType === 'mouse' &&
3276
- this.options.showTooltips &&
3277
- this.options.tooltipTrigger === 'click'
3278
- ) {
3279
- return;
3280
- }
3281
-
3282
- // Only add pointermove listener for non-touch pointers
3283
- if (e.pointerType !== 'touch') {
3284
- document.addEventListener('pointermove', handlePointerMoveBound, {
3285
- passive: true
3286
- });
3287
- }
3288
-
3289
- document
3290
- .querySelectorAll('.svgMap-active')
3291
- .forEach((el) => el.classList.remove('svgMap-active'));
3292
-
3293
- countryElement.parentNode.insertBefore(
3294
- countryElement,
3295
- this.persistentTooltipGroup || null
3296
- );
3297
- countryElement.classList.add('svgMap-active');
3298
-
3299
- const countryID = countryElement.getAttribute('data-id');
3300
- if (this.options.showTooltips) {
3301
- this.setTooltipContent(this.getTooltipContent(countryID));
3302
- this.showTooltip(e);
3303
-
3304
- // For touch, move tooltip to the touch position and keep it there
3305
- if (e.pointerType === 'touch') {
3306
- this.moveTooltip(e);
3307
- }
3308
- }
3309
- }.bind(this)
3310
- );
3311
-
3312
- // Handle touch move - update tooltip position while panning
3313
- countryElement.addEventListener(
3314
- 'touchmove',
3315
- function (e) {
3316
- this.moveTooltip(e);
3317
- }.bind(this),
3318
- { passive: true }
3319
- );
3320
-
3321
- // Handle touch end - remove active state and hide tooltip
3322
- countryElement.addEventListener(
3323
- 'touchend',
3324
- function (e) {
3325
- const touch = e.changedTouches[0];
3326
- const elementAtEnd = document.elementFromPoint(
3327
- touch.clientX,
3328
- touch.clientY
3329
- );
3330
-
3331
- // Only hide if touch ended outside the country or tooltip
3332
- if (
3333
- !elementAtEnd ||
3334
- (!elementAtEnd.closest('.svgMap-country') &&
3335
- !elementAtEnd.closest('.svgMap-tooltip'))
3336
- ) {
3337
- this.hideTooltip();
3338
- document
3339
- .querySelectorAll('.svgMap-active')
3340
- .forEach((el) => el.classList.remove('svgMap-active'));
3341
- }
3342
- }.bind(this),
3343
- { passive: true }
3344
- );
3345
-
3346
- // Remove pointermove listener when leaving non-touch pointer
3347
- countryElement.addEventListener(
3348
- 'pointerleave',
3349
- function (e) {
3350
- if (e.pointerType !== 'touch') {
3351
- document.removeEventListener(
3352
- 'pointermove',
3353
- handlePointerMoveBound
3354
- );
3355
- if (
3356
- !(
3357
- e.pointerType === 'mouse' &&
3358
- this.options.showTooltips &&
3359
- this.options.tooltipTrigger === 'click'
3360
- )
3361
- ) {
3362
- this.hideTooltip();
3363
- document
3364
- .querySelectorAll('.svgMap-active')
3365
- .forEach((el) => el.classList.remove('svgMap-active'));
3366
- }
3367
- }
3368
- }.bind(this)
3369
- );
3370
-
3371
- document.addEventListener(
3372
- 'pointerover',
3373
- function (e) {
3374
- if (e.pointerType !== 'touch') return;
3375
-
3376
- if (
3377
- e.target.closest('.svgMap-country') ||
3378
- e.target.closest('.svgMap-tooltip')
3379
- ) {
3380
- return;
3381
- }
3382
-
3383
- this.hideTooltip();
3384
- document
3385
- .querySelectorAll('.svgMap-active')
3386
- .forEach((el) => el.classList.remove('svgMap-active'));
3387
- }.bind(this),
3388
- { passive: true }
3389
- );
3390
-
3391
3374
  if (
3392
3375
  this.options.data.values &&
3393
3376
  this.options.data.values[countryID] &&
3394
3377
  this.options.data.values[countryID]['link']
3395
3378
  ) {
3396
- countryElement.setAttribute(
3397
- 'data-link',
3398
- this.options.data.values[countryID]['link']
3399
- );
3379
+ countryElement.dataset.link =
3380
+ this.options.data.values[countryID]['link'];
3400
3381
  if (this.options.data.values[countryID]['linkTarget']) {
3401
- countryElement.setAttribute(
3402
- 'data-link-target',
3403
- this.options.data.values[countryID]['linkTarget']
3404
- );
3382
+ countryElement.dataset.linkTarget =
3383
+ this.options.data.values[countryID]['linkTarget'];
3405
3384
  }
3406
3385
  }
3407
3386
  }.bind(this)
@@ -3415,6 +3394,10 @@ class svgMap {
3415
3394
  this.createPersistentTooltips(countryElements);
3416
3395
  }
3417
3396
 
3397
+ if (this.options.staticPins) {
3398
+ this.createStaticPins(countryElements);
3399
+ }
3400
+
3418
3401
  let pointerStart = null;
3419
3402
  let activeCountry = null;
3420
3403
 
@@ -3447,21 +3430,18 @@ class svgMap {
3447
3430
  const countryElement = e.target.closest('.svgMap-country');
3448
3431
  if (!countryElement) return;
3449
3432
 
3450
- const countryID = countryElement.getAttribute('data-id');
3451
- const link = countryElement.getAttribute('data-link');
3452
- const linkTarget = countryElement.getAttribute('data-link-target');
3433
+ const countryID = countryElement.dataset.id;
3434
+ const link = countryElement.dataset.link;
3435
+ const linkTarget = countryElement.dataset.linkTarget;
3453
3436
  const hasCallback = typeof this.options.onCountryClick === 'function';
3454
3437
  const hasLink = !!link;
3455
3438
  const isTouch = e.pointerType === 'touch' || e.pointerType === 'pen';
3456
3439
 
3457
- const isClickTooltipMouse =
3458
- e.pointerType === 'mouse' &&
3459
- this.options.showTooltips &&
3460
- this.options.tooltipTrigger === 'click';
3440
+ const isClickTooltipMouse = e.pointerType === 'mouse' && isClickTooltip;
3461
3441
 
3462
3442
  if (!hasLink && !hasCallback && !isClickTooltipMouse) return;
3463
3443
 
3464
- if (isClickTooltipMouse && this.options.showTooltips) {
3444
+ if (isClickTooltipMouse) {
3465
3445
  const willNavigate =
3466
3446
  hasLink && countryElement.classList.contains('svgMap-active');
3467
3447
  const shouldFireCallback = hasCallback && (!hasLink || willNavigate);
@@ -3477,12 +3457,10 @@ class svgMap {
3477
3457
  if (linkTarget) window.open(link, linkTarget);
3478
3458
  else window.location.href = link;
3479
3459
  } else {
3480
- this.mapImage
3481
- .querySelectorAll('.svgMap-country.svgMap-active')
3482
- .forEach((el) => el.classList.remove('svgMap-active'));
3460
+ clearActive();
3483
3461
  countryElement.parentNode.insertBefore(
3484
3462
  countryElement,
3485
- this.persistentTooltipGroup || null
3463
+ this.persistentTooltipGroup || this.pinGroup || null
3486
3464
  );
3487
3465
  countryElement.classList.add('svgMap-active');
3488
3466
  this.setTooltipContent(this.getTooltipContent(countryID));
@@ -3493,12 +3471,10 @@ class svgMap {
3493
3471
 
3494
3472
  if (callbackResultClick === false) return;
3495
3473
 
3496
- this.mapImage
3497
- .querySelectorAll('.svgMap-country.svgMap-active')
3498
- .forEach((el) => el.classList.remove('svgMap-active'));
3474
+ clearActive();
3499
3475
  countryElement.parentNode.insertBefore(
3500
3476
  countryElement,
3501
- this.persistentTooltipGroup || null
3477
+ this.persistentTooltipGroup || this.pinGroup || null
3502
3478
  );
3503
3479
  countryElement.classList.add('svgMap-active');
3504
3480
  this.setTooltipContent(this.getTooltipContent(countryID));
@@ -3548,15 +3524,9 @@ class svgMap {
3548
3524
  });
3549
3525
 
3550
3526
  this._clickTooltipOutsideHandler = function (ev) {
3551
- if (ev.pointerType !== 'mouse') return;
3552
- if (
3553
- !this.options.showTooltips ||
3554
- this.options.tooltipTrigger !== 'click' ||
3555
- !this.tooltip
3556
- ) {
3527
+ if (!this.tooltip || !this.tooltip.classList.contains('svgMap-active')) {
3557
3528
  return;
3558
3529
  }
3559
- if (!this.tooltip.classList.contains('svgMap-active')) return;
3560
3530
  var node = ev.target;
3561
3531
  if (
3562
3532
  node &&
@@ -3574,11 +3544,6 @@ class svgMap {
3574
3544
  });
3575
3545
  }
3576
3546
  }.bind(this);
3577
- document.addEventListener(
3578
- 'pointerdown',
3579
- this._clickTooltipOutsideHandler,
3580
- true
3581
- );
3582
3547
 
3583
3548
  // Expose instance
3584
3549
  var me = this;
@@ -3661,7 +3626,7 @@ class svgMap {
3661
3626
 
3662
3627
  countryElements.forEach(
3663
3628
  function (countryElement) {
3664
- var countryID = countryElement.getAttribute('data-id');
3629
+ var countryID = countryElement.dataset.id;
3665
3630
  if (!this.shouldShowTooltipOnLoad(countryID)) {
3666
3631
  return;
3667
3632
  }
@@ -3697,6 +3662,157 @@ class svgMap {
3697
3662
  );
3698
3663
  }
3699
3664
 
3665
+ // Create static pins on the map
3666
+
3667
+ createStaticPins(countryElements) {
3668
+ if (this.pinGroup) {
3669
+ this.pinGroup.remove();
3670
+ }
3671
+
3672
+ this.pinGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3673
+ this.pinGroup.classList.add('svgMap-pin-group');
3674
+ this.mapImage.appendChild(this.pinGroup);
3675
+
3676
+ countryElements.forEach(
3677
+ function (countryElement) {
3678
+ var countryID = countryElement.getAttribute('data-id');
3679
+ if (!this.shouldShowPin(countryID)) {
3680
+ return;
3681
+ }
3682
+
3683
+ var countryValues = this.options.data.values[countryID];
3684
+ var cx, cy;
3685
+
3686
+ if (
3687
+ countryValues &&
3688
+ countryValues.pinX != null &&
3689
+ countryValues.pinY != null
3690
+ ) {
3691
+ cx = countryValues.pinX;
3692
+ cy = countryValues.pinY;
3693
+ } else {
3694
+ // Split the path at absolute M commands and use the largest sub-path
3695
+ // to avoid overseas territories (islands, colonies) skewing the center.
3696
+ var d = countryElement.getAttribute('d');
3697
+ var subPaths = d.split(/(?=M)/).filter((s) => s.trim().length > 0);
3698
+ var largestBB = null;
3699
+ var largestArea = -1;
3700
+
3701
+ subPaths.forEach(
3702
+ function (subPath) {
3703
+ var tmp = document.createElementNS(
3704
+ 'http://www.w3.org/2000/svg',
3705
+ 'path'
3706
+ );
3707
+ tmp.setAttribute('d', subPath);
3708
+ this.mapImage.appendChild(tmp);
3709
+ var bb = tmp.getBBox();
3710
+ var area = bb.width * bb.height;
3711
+ if (area > largestArea) {
3712
+ largestArea = area;
3713
+ largestBB = bb;
3714
+ }
3715
+ this.mapImage.removeChild(tmp);
3716
+ }.bind(this)
3717
+ );
3718
+
3719
+ cx = largestBB.x + largestBB.width / 2;
3720
+ cy = largestBB.y + largestBB.height / 2;
3721
+ }
3722
+
3723
+ var offsetX =
3724
+ countryValues && countryValues.pinOffsetX != null
3725
+ ? countryValues.pinOffsetX
3726
+ : this.options.pinOffsetX;
3727
+ var offsetY =
3728
+ countryValues && countryValues.pinOffsetY != null
3729
+ ? countryValues.pinOffsetY
3730
+ : this.options.pinOffsetY;
3731
+ cx += offsetX;
3732
+ cy += offsetY;
3733
+
3734
+ var color =
3735
+ (countryValues && countryValues.pinColor) || this.options.pinColor;
3736
+ var size =
3737
+ (countryValues && countryValues.pinSize) || this.options.pinSize;
3738
+ var strokeColor =
3739
+ (countryValues && countryValues.pinStrokeColor) ||
3740
+ this.options.pinStrokeColor;
3741
+ var strokeWidth =
3742
+ (countryValues && countryValues.pinStrokeWidth) ||
3743
+ this.options.pinStrokeWidth;
3744
+
3745
+ if (typeof this.options.onGetPin === 'function') {
3746
+ var custom = this.options.onGetPin(countryID, countryValues);
3747
+ if (custom) {
3748
+ custom.setAttribute(
3749
+ 'transform',
3750
+ 'translate(' + cx + ',' + cy + ')'
3751
+ );
3752
+ this.pinGroup.appendChild(custom);
3753
+ return;
3754
+ }
3755
+ }
3756
+
3757
+ var pinImage =
3758
+ (countryValues && countryValues.pinImage) || this.options.pinImage;
3759
+
3760
+ if (pinImage) {
3761
+ var pinW =
3762
+ (countryValues && countryValues.pinImageWidth) ||
3763
+ this.options.pinImageWidth;
3764
+ var pinH =
3765
+ (countryValues && countryValues.pinImageHeight) ||
3766
+ this.options.pinImageHeight;
3767
+ var img = document.createElementNS(
3768
+ 'http://www.w3.org/2000/svg',
3769
+ 'image'
3770
+ );
3771
+ img.setAttribute('href', pinImage);
3772
+ img.setAttribute('x', cx - pinW / 2);
3773
+ img.setAttribute('y', cy - pinH / 2);
3774
+ img.setAttribute('width', pinW);
3775
+ img.setAttribute('height', pinH);
3776
+ img.setAttribute('data-id', countryID);
3777
+ img.classList.add('svgMap-pin');
3778
+ this.pinGroup.appendChild(img);
3779
+ return;
3780
+ }
3781
+
3782
+ var circle = document.createElementNS(
3783
+ 'http://www.w3.org/2000/svg',
3784
+ 'circle'
3785
+ );
3786
+ circle.setAttribute('cx', cx);
3787
+ circle.setAttribute('cy', cy);
3788
+ circle.setAttribute('r', size);
3789
+ circle.setAttribute('fill', color);
3790
+ if (strokeWidth > 0) {
3791
+ circle.setAttribute('stroke', strokeColor);
3792
+ circle.setAttribute('stroke-width', strokeWidth);
3793
+ circle.setAttribute('vector-effect', 'non-scaling-stroke');
3794
+ }
3795
+ circle.setAttribute('data-id', countryID);
3796
+ circle.classList.add('svgMap-pin');
3797
+ this.pinGroup.appendChild(circle);
3798
+ }.bind(this)
3799
+ );
3800
+ }
3801
+
3802
+ // Check if a static pin should be shown for a country
3803
+
3804
+ shouldShowPin(countryID) {
3805
+ var pins = this.options.staticPins;
3806
+ var countryValues = this.options.data.values[countryID];
3807
+ if (Array.isArray(pins)) {
3808
+ return pins.indexOf(countryID) !== -1;
3809
+ }
3810
+ if (typeof pins === 'function') {
3811
+ return pins(countryID, countryValues);
3812
+ }
3813
+ return false;
3814
+ }
3815
+
3700
3816
  // Check if a persistent tooltip should be shown on load
3701
3817
 
3702
3818
  shouldShowTooltipOnLoad(countryID) {
@@ -4706,6 +4822,23 @@ class svgMap {
4706
4822
  return;
4707
4823
  }
4708
4824
  this.tooltip.classList.add('svgMap-active');
4825
+
4826
+ if (
4827
+ this.options.showTooltips &&
4828
+ this.options.tooltipTrigger === 'click' &&
4829
+ e.pointerType === 'mouse'
4830
+ ) {
4831
+ // don't register event listener in the same frame
4832
+ // to prevent it from being triggered immediately
4833
+ requestAnimationFrame(() => {
4834
+ document.addEventListener(
4835
+ 'pointerdown',
4836
+ this._clickTooltipOutsideHandler,
4837
+ { once: true, passive: true }
4838
+ );
4839
+ });
4840
+ }
4841
+
4709
4842
  this.moveTooltip(e);
4710
4843
  }
4711
4844
 
@@ -4715,7 +4848,12 @@ class svgMap {
4715
4848
  if (!this.tooltip) {
4716
4849
  return;
4717
4850
  }
4851
+
4718
4852
  this.tooltip.classList.remove('svgMap-active');
4853
+ document.removeEventListener(
4854
+ 'pointerdown',
4855
+ this._clickTooltipOutsideHandler
4856
+ );
4719
4857
  }
4720
4858
 
4721
4859
  // Move the tooltip