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/svgMap.css CHANGED
@@ -204,6 +204,15 @@
204
204
  cursor: pointer;
205
205
  }
206
206
 
207
+ .svgMap-pin-group {
208
+ pointer-events: none;
209
+ }
210
+
211
+ .svgMap-pin {
212
+ vector-effect: non-scaling-stroke;
213
+ transition: r 250ms;
214
+ }
215
+
207
216
  .svgMap-tooltip,
208
217
  .svgMap-persistent-tooltip {
209
218
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
package/dist/svgMap.js CHANGED
@@ -2379,7 +2379,34 @@
2379
2379
  showContinentSelector: false,
2380
2380
 
2381
2381
  // Reset zoom on resize
2382
- resetZoomOnResize: false
2382
+ resetZoomOnResize: false,
2383
+
2384
+ // Static pins: false | string[] | function(countryID, countryValues) => boolean
2385
+ staticPins: false,
2386
+
2387
+ // Default pin fill color
2388
+ pinColor: '#000000',
2389
+
2390
+ // Default pin stroke color and width (circle pins; width is in screen pixels with non-scaling stroke)
2391
+ pinStrokeColor: '#ffffff',
2392
+ pinStrokeWidth: 1.5,
2393
+
2394
+ // Default pin radius in SVG units (viewBox is 2000 × 1001)
2395
+ pinSize: 8,
2396
+
2397
+ // Custom pin element: function(countryID, countryValues) => SVGElement | null
2398
+ onGetPin: null,
2399
+
2400
+ // Image URL to use as a pin instead of the default circle (can also be set per-country via values[id].pinImage)
2401
+ pinImage: null,
2402
+
2403
+ // Width and height of the pin image in SVG units (viewBox is 2000 × 1001)
2404
+ pinImageWidth: 20,
2405
+ pinImageHeight: 20,
2406
+
2407
+ // Offset from computed pin position, in SVG units (added after auto center or pinX/pinY)
2408
+ pinOffsetX: 0,
2409
+ pinOffsetY: 0
2383
2410
  };
2384
2411
 
2385
2412
  this.options = Object.assign({}, defaultOptions, options);
@@ -3231,6 +3258,102 @@
3231
3258
 
3232
3259
  // Add map elements
3233
3260
  var countryElements = [];
3261
+
3262
+ const clearActive = function clearActive() {
3263
+ this.mapImage
3264
+ .querySelectorAll('.svgMap-active')
3265
+ .forEach((el) => el.classList.remove('svgMap-active'));
3266
+ }.bind(this);
3267
+
3268
+ const isClickTooltip =
3269
+ this.options.showTooltips && this.options.tooltipTrigger === 'click';
3270
+
3271
+ const getCountryFromEvent = function (e) {
3272
+ return e.target && e.target.closest
3273
+ ? e.target.closest('.svgMap-country')
3274
+ : null;
3275
+ };
3276
+
3277
+ const raiseCountry = function (countryElement, setActive) {
3278
+ if (setActive) {
3279
+ clearActive();
3280
+ }
3281
+ countryElement.parentNode.insertBefore(
3282
+ countryElement,
3283
+ this.persistentTooltipGroup || this.pinGroup || null
3284
+ );
3285
+ if (setActive) {
3286
+ countryElement.classList.add('svgMap-active');
3287
+ }
3288
+ }.bind(this);
3289
+
3290
+ const showCountryTooltip = function (countryElement, e, setActive) {
3291
+ raiseCountry(countryElement, setActive);
3292
+ this.setTooltipContent(this.getTooltipContent(countryElement.dataset.id));
3293
+ this.showTooltip(e);
3294
+ }.bind(this);
3295
+
3296
+ // Touch only: preview tooltip on finger down without marking country active
3297
+ // (active is set on pointerup so link countries keep two-tap navigation)
3298
+ this.mapImage.addEventListener(
3299
+ 'pointerdown',
3300
+ (e) => {
3301
+ if (!this.options.showTooltips || e.pointerType !== 'touch') {
3302
+ return;
3303
+ }
3304
+
3305
+ const countryElement = getCountryFromEvent(e);
3306
+ if (!countryElement) {
3307
+ this.hideTooltip();
3308
+ return;
3309
+ }
3310
+
3311
+ showCountryTooltip(countryElement, e, false);
3312
+ this.moveTooltip(e);
3313
+ },
3314
+ { passive: true }
3315
+ );
3316
+
3317
+ this.mapImage.addEventListener(
3318
+ 'pointercancel',
3319
+ (e) => {
3320
+ if (e.pointerType === 'touch') {
3321
+ this.hideTooltip();
3322
+ }
3323
+ },
3324
+ { passive: true }
3325
+ );
3326
+
3327
+ // Hover (mouse/pen) and touch drag: raise country + optional floating tooltip
3328
+ this.mapImage.addEventListener(
3329
+ 'pointermove',
3330
+ (e) => {
3331
+ const countryElement = getCountryFromEvent(e);
3332
+ if (!countryElement) {
3333
+ clearActive();
3334
+ if (this.options.showTooltips) {
3335
+ this.hideTooltip();
3336
+ }
3337
+ return;
3338
+ }
3339
+
3340
+ const mouseClickMode = e.pointerType === 'mouse' && isClickTooltip;
3341
+
3342
+ // Always raise hovered country (SVG paint order + .svgMap-active stroke)
3343
+ if (!this.options.showTooltips || mouseClickMode) {
3344
+ raiseCountry(countryElement, true);
3345
+ return;
3346
+ }
3347
+
3348
+ showCountryTooltip(countryElement, e, true);
3349
+
3350
+ if (e.pointerType === 'touch') {
3351
+ this.moveTooltip(e);
3352
+ }
3353
+ },
3354
+ { passive: true }
3355
+ );
3356
+
3234
3357
  Object.keys(mapPaths).forEach(
3235
3358
  function (countryID) {
3236
3359
  var countryData = this.mapPaths[countryID];
@@ -3248,166 +3371,22 @@
3248
3371
  'id',
3249
3372
  this.id + '-map-country-' + countryID
3250
3373
  );
3251
- countryElement.setAttribute('data-id', countryID);
3374
+ countryElement.dataset.id = countryID;
3252
3375
  countryElement.classList.add('svgMap-country');
3253
3376
 
3254
3377
  this.mapImage.appendChild(countryElement);
3255
3378
  countryElements.push(countryElement);
3256
3379
 
3257
- // Add tooltip when touch is used
3258
- function handlePointerMove(e) {
3259
- if (e.pointerType === 'touch') return;
3260
-
3261
- const target = document.elementFromPoint(e.clientX, e.clientY);
3262
-
3263
- if (
3264
- !target ||
3265
- (!target.closest('.svgMap-country') &&
3266
- !target.closest('.svgMap-tooltip'))
3267
- ) {
3268
- this.hideTooltip();
3269
- document
3270
- .querySelectorAll('.svgMap-active')
3271
- .forEach((el) => el.classList.remove('svgMap-active'));
3272
- }
3273
- }
3274
-
3275
- const handlePointerMoveBound = handlePointerMove.bind(this);
3276
-
3277
- countryElement.addEventListener(
3278
- 'pointerenter',
3279
- function (e) {
3280
- if (
3281
- e.pointerType === 'mouse' &&
3282
- this.options.showTooltips &&
3283
- this.options.tooltipTrigger === 'click'
3284
- ) {
3285
- return;
3286
- }
3287
-
3288
- // Only add pointermove listener for non-touch pointers
3289
- if (e.pointerType !== 'touch') {
3290
- document.addEventListener('pointermove', handlePointerMoveBound, {
3291
- passive: true
3292
- });
3293
- }
3294
-
3295
- document
3296
- .querySelectorAll('.svgMap-active')
3297
- .forEach((el) => el.classList.remove('svgMap-active'));
3298
-
3299
- countryElement.parentNode.insertBefore(
3300
- countryElement,
3301
- this.persistentTooltipGroup || null
3302
- );
3303
- countryElement.classList.add('svgMap-active');
3304
-
3305
- const countryID = countryElement.getAttribute('data-id');
3306
- if (this.options.showTooltips) {
3307
- this.setTooltipContent(this.getTooltipContent(countryID));
3308
- this.showTooltip(e);
3309
-
3310
- // For touch, move tooltip to the touch position and keep it there
3311
- if (e.pointerType === 'touch') {
3312
- this.moveTooltip(e);
3313
- }
3314
- }
3315
- }.bind(this)
3316
- );
3317
-
3318
- // Handle touch move - update tooltip position while panning
3319
- countryElement.addEventListener(
3320
- 'touchmove',
3321
- function (e) {
3322
- this.moveTooltip(e);
3323
- }.bind(this),
3324
- { passive: true }
3325
- );
3326
-
3327
- // Handle touch end - remove active state and hide tooltip
3328
- countryElement.addEventListener(
3329
- 'touchend',
3330
- function (e) {
3331
- const touch = e.changedTouches[0];
3332
- const elementAtEnd = document.elementFromPoint(
3333
- touch.clientX,
3334
- touch.clientY
3335
- );
3336
-
3337
- // Only hide if touch ended outside the country or tooltip
3338
- if (
3339
- !elementAtEnd ||
3340
- (!elementAtEnd.closest('.svgMap-country') &&
3341
- !elementAtEnd.closest('.svgMap-tooltip'))
3342
- ) {
3343
- this.hideTooltip();
3344
- document
3345
- .querySelectorAll('.svgMap-active')
3346
- .forEach((el) => el.classList.remove('svgMap-active'));
3347
- }
3348
- }.bind(this),
3349
- { passive: true }
3350
- );
3351
-
3352
- // Remove pointermove listener when leaving non-touch pointer
3353
- countryElement.addEventListener(
3354
- 'pointerleave',
3355
- function (e) {
3356
- if (e.pointerType !== 'touch') {
3357
- document.removeEventListener(
3358
- 'pointermove',
3359
- handlePointerMoveBound
3360
- );
3361
- if (
3362
- !(
3363
- e.pointerType === 'mouse' &&
3364
- this.options.showTooltips &&
3365
- this.options.tooltipTrigger === 'click'
3366
- )
3367
- ) {
3368
- this.hideTooltip();
3369
- document
3370
- .querySelectorAll('.svgMap-active')
3371
- .forEach((el) => el.classList.remove('svgMap-active'));
3372
- }
3373
- }
3374
- }.bind(this)
3375
- );
3376
-
3377
- document.addEventListener(
3378
- 'pointerover',
3379
- function (e) {
3380
- if (e.pointerType !== 'touch') return;
3381
-
3382
- if (
3383
- e.target.closest('.svgMap-country') ||
3384
- e.target.closest('.svgMap-tooltip')
3385
- ) {
3386
- return;
3387
- }
3388
-
3389
- this.hideTooltip();
3390
- document
3391
- .querySelectorAll('.svgMap-active')
3392
- .forEach((el) => el.classList.remove('svgMap-active'));
3393
- }.bind(this),
3394
- { passive: true }
3395
- );
3396
-
3397
3380
  if (
3398
3381
  this.options.data.values &&
3399
3382
  this.options.data.values[countryID] &&
3400
3383
  this.options.data.values[countryID]['link']
3401
3384
  ) {
3402
- countryElement.setAttribute(
3403
- 'data-link',
3404
- this.options.data.values[countryID]['link']
3405
- );
3385
+ countryElement.dataset.link =
3386
+ this.options.data.values[countryID]['link'];
3406
3387
  if (this.options.data.values[countryID]['linkTarget']) {
3407
- countryElement.setAttribute(
3408
- 'data-link-target',
3409
- this.options.data.values[countryID]['linkTarget']
3410
- );
3388
+ countryElement.dataset.linkTarget =
3389
+ this.options.data.values[countryID]['linkTarget'];
3411
3390
  }
3412
3391
  }
3413
3392
  }.bind(this)
@@ -3421,6 +3400,10 @@
3421
3400
  this.createPersistentTooltips(countryElements);
3422
3401
  }
3423
3402
 
3403
+ if (this.options.staticPins) {
3404
+ this.createStaticPins(countryElements);
3405
+ }
3406
+
3424
3407
  let pointerStart = null;
3425
3408
  let activeCountry = null;
3426
3409
 
@@ -3453,21 +3436,18 @@
3453
3436
  const countryElement = e.target.closest('.svgMap-country');
3454
3437
  if (!countryElement) return;
3455
3438
 
3456
- const countryID = countryElement.getAttribute('data-id');
3457
- const link = countryElement.getAttribute('data-link');
3458
- const linkTarget = countryElement.getAttribute('data-link-target');
3439
+ const countryID = countryElement.dataset.id;
3440
+ const link = countryElement.dataset.link;
3441
+ const linkTarget = countryElement.dataset.linkTarget;
3459
3442
  const hasCallback = typeof this.options.onCountryClick === 'function';
3460
3443
  const hasLink = !!link;
3461
3444
  const isTouch = e.pointerType === 'touch' || e.pointerType === 'pen';
3462
3445
 
3463
- const isClickTooltipMouse =
3464
- e.pointerType === 'mouse' &&
3465
- this.options.showTooltips &&
3466
- this.options.tooltipTrigger === 'click';
3446
+ const isClickTooltipMouse = e.pointerType === 'mouse' && isClickTooltip;
3467
3447
 
3468
3448
  if (!hasLink && !hasCallback && !isClickTooltipMouse) return;
3469
3449
 
3470
- if (isClickTooltipMouse && this.options.showTooltips) {
3450
+ if (isClickTooltipMouse) {
3471
3451
  const willNavigate =
3472
3452
  hasLink && countryElement.classList.contains('svgMap-active');
3473
3453
  const shouldFireCallback = hasCallback && (!hasLink || willNavigate);
@@ -3483,12 +3463,10 @@
3483
3463
  if (linkTarget) window.open(link, linkTarget);
3484
3464
  else window.location.href = link;
3485
3465
  } else {
3486
- this.mapImage
3487
- .querySelectorAll('.svgMap-country.svgMap-active')
3488
- .forEach((el) => el.classList.remove('svgMap-active'));
3466
+ clearActive();
3489
3467
  countryElement.parentNode.insertBefore(
3490
3468
  countryElement,
3491
- this.persistentTooltipGroup || null
3469
+ this.persistentTooltipGroup || this.pinGroup || null
3492
3470
  );
3493
3471
  countryElement.classList.add('svgMap-active');
3494
3472
  this.setTooltipContent(this.getTooltipContent(countryID));
@@ -3499,12 +3477,10 @@
3499
3477
 
3500
3478
  if (callbackResultClick === false) return;
3501
3479
 
3502
- this.mapImage
3503
- .querySelectorAll('.svgMap-country.svgMap-active')
3504
- .forEach((el) => el.classList.remove('svgMap-active'));
3480
+ clearActive();
3505
3481
  countryElement.parentNode.insertBefore(
3506
3482
  countryElement,
3507
- this.persistentTooltipGroup || null
3483
+ this.persistentTooltipGroup || this.pinGroup || null
3508
3484
  );
3509
3485
  countryElement.classList.add('svgMap-active');
3510
3486
  this.setTooltipContent(this.getTooltipContent(countryID));
@@ -3554,15 +3530,9 @@
3554
3530
  });
3555
3531
 
3556
3532
  this._clickTooltipOutsideHandler = function (ev) {
3557
- if (ev.pointerType !== 'mouse') return;
3558
- if (
3559
- !this.options.showTooltips ||
3560
- this.options.tooltipTrigger !== 'click' ||
3561
- !this.tooltip
3562
- ) {
3533
+ if (!this.tooltip || !this.tooltip.classList.contains('svgMap-active')) {
3563
3534
  return;
3564
3535
  }
3565
- if (!this.tooltip.classList.contains('svgMap-active')) return;
3566
3536
  var node = ev.target;
3567
3537
  if (
3568
3538
  node &&
@@ -3580,11 +3550,6 @@
3580
3550
  });
3581
3551
  }
3582
3552
  }.bind(this);
3583
- document.addEventListener(
3584
- 'pointerdown',
3585
- this._clickTooltipOutsideHandler,
3586
- true
3587
- );
3588
3553
 
3589
3554
  // Expose instance
3590
3555
  var me = this;
@@ -3667,7 +3632,7 @@
3667
3632
 
3668
3633
  countryElements.forEach(
3669
3634
  function (countryElement) {
3670
- var countryID = countryElement.getAttribute('data-id');
3635
+ var countryID = countryElement.dataset.id;
3671
3636
  if (!this.shouldShowTooltipOnLoad(countryID)) {
3672
3637
  return;
3673
3638
  }
@@ -3703,6 +3668,157 @@
3703
3668
  );
3704
3669
  }
3705
3670
 
3671
+ // Create static pins on the map
3672
+
3673
+ createStaticPins(countryElements) {
3674
+ if (this.pinGroup) {
3675
+ this.pinGroup.remove();
3676
+ }
3677
+
3678
+ this.pinGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3679
+ this.pinGroup.classList.add('svgMap-pin-group');
3680
+ this.mapImage.appendChild(this.pinGroup);
3681
+
3682
+ countryElements.forEach(
3683
+ function (countryElement) {
3684
+ var countryID = countryElement.getAttribute('data-id');
3685
+ if (!this.shouldShowPin(countryID)) {
3686
+ return;
3687
+ }
3688
+
3689
+ var countryValues = this.options.data.values[countryID];
3690
+ var cx, cy;
3691
+
3692
+ if (
3693
+ countryValues &&
3694
+ countryValues.pinX != null &&
3695
+ countryValues.pinY != null
3696
+ ) {
3697
+ cx = countryValues.pinX;
3698
+ cy = countryValues.pinY;
3699
+ } else {
3700
+ // Split the path at absolute M commands and use the largest sub-path
3701
+ // to avoid overseas territories (islands, colonies) skewing the center.
3702
+ var d = countryElement.getAttribute('d');
3703
+ var subPaths = d.split(/(?=M)/).filter((s) => s.trim().length > 0);
3704
+ var largestBB = null;
3705
+ var largestArea = -1;
3706
+
3707
+ subPaths.forEach(
3708
+ function (subPath) {
3709
+ var tmp = document.createElementNS(
3710
+ 'http://www.w3.org/2000/svg',
3711
+ 'path'
3712
+ );
3713
+ tmp.setAttribute('d', subPath);
3714
+ this.mapImage.appendChild(tmp);
3715
+ var bb = tmp.getBBox();
3716
+ var area = bb.width * bb.height;
3717
+ if (area > largestArea) {
3718
+ largestArea = area;
3719
+ largestBB = bb;
3720
+ }
3721
+ this.mapImage.removeChild(tmp);
3722
+ }.bind(this)
3723
+ );
3724
+
3725
+ cx = largestBB.x + largestBB.width / 2;
3726
+ cy = largestBB.y + largestBB.height / 2;
3727
+ }
3728
+
3729
+ var offsetX =
3730
+ countryValues && countryValues.pinOffsetX != null
3731
+ ? countryValues.pinOffsetX
3732
+ : this.options.pinOffsetX;
3733
+ var offsetY =
3734
+ countryValues && countryValues.pinOffsetY != null
3735
+ ? countryValues.pinOffsetY
3736
+ : this.options.pinOffsetY;
3737
+ cx += offsetX;
3738
+ cy += offsetY;
3739
+
3740
+ var color =
3741
+ (countryValues && countryValues.pinColor) || this.options.pinColor;
3742
+ var size =
3743
+ (countryValues && countryValues.pinSize) || this.options.pinSize;
3744
+ var strokeColor =
3745
+ (countryValues && countryValues.pinStrokeColor) ||
3746
+ this.options.pinStrokeColor;
3747
+ var strokeWidth =
3748
+ (countryValues && countryValues.pinStrokeWidth) ||
3749
+ this.options.pinStrokeWidth;
3750
+
3751
+ if (typeof this.options.onGetPin === 'function') {
3752
+ var custom = this.options.onGetPin(countryID, countryValues);
3753
+ if (custom) {
3754
+ custom.setAttribute(
3755
+ 'transform',
3756
+ 'translate(' + cx + ',' + cy + ')'
3757
+ );
3758
+ this.pinGroup.appendChild(custom);
3759
+ return;
3760
+ }
3761
+ }
3762
+
3763
+ var pinImage =
3764
+ (countryValues && countryValues.pinImage) || this.options.pinImage;
3765
+
3766
+ if (pinImage) {
3767
+ var pinW =
3768
+ (countryValues && countryValues.pinImageWidth) ||
3769
+ this.options.pinImageWidth;
3770
+ var pinH =
3771
+ (countryValues && countryValues.pinImageHeight) ||
3772
+ this.options.pinImageHeight;
3773
+ var img = document.createElementNS(
3774
+ 'http://www.w3.org/2000/svg',
3775
+ 'image'
3776
+ );
3777
+ img.setAttribute('href', pinImage);
3778
+ img.setAttribute('x', cx - pinW / 2);
3779
+ img.setAttribute('y', cy - pinH / 2);
3780
+ img.setAttribute('width', pinW);
3781
+ img.setAttribute('height', pinH);
3782
+ img.setAttribute('data-id', countryID);
3783
+ img.classList.add('svgMap-pin');
3784
+ this.pinGroup.appendChild(img);
3785
+ return;
3786
+ }
3787
+
3788
+ var circle = document.createElementNS(
3789
+ 'http://www.w3.org/2000/svg',
3790
+ 'circle'
3791
+ );
3792
+ circle.setAttribute('cx', cx);
3793
+ circle.setAttribute('cy', cy);
3794
+ circle.setAttribute('r', size);
3795
+ circle.setAttribute('fill', color);
3796
+ if (strokeWidth > 0) {
3797
+ circle.setAttribute('stroke', strokeColor);
3798
+ circle.setAttribute('stroke-width', strokeWidth);
3799
+ circle.setAttribute('vector-effect', 'non-scaling-stroke');
3800
+ }
3801
+ circle.setAttribute('data-id', countryID);
3802
+ circle.classList.add('svgMap-pin');
3803
+ this.pinGroup.appendChild(circle);
3804
+ }.bind(this)
3805
+ );
3806
+ }
3807
+
3808
+ // Check if a static pin should be shown for a country
3809
+
3810
+ shouldShowPin(countryID) {
3811
+ var pins = this.options.staticPins;
3812
+ var countryValues = this.options.data.values[countryID];
3813
+ if (Array.isArray(pins)) {
3814
+ return pins.indexOf(countryID) !== -1;
3815
+ }
3816
+ if (typeof pins === 'function') {
3817
+ return pins(countryID, countryValues);
3818
+ }
3819
+ return false;
3820
+ }
3821
+
3706
3822
  // Check if a persistent tooltip should be shown on load
3707
3823
 
3708
3824
  shouldShowTooltipOnLoad(countryID) {
@@ -4712,6 +4828,23 @@
4712
4828
  return;
4713
4829
  }
4714
4830
  this.tooltip.classList.add('svgMap-active');
4831
+
4832
+ if (
4833
+ this.options.showTooltips &&
4834
+ this.options.tooltipTrigger === 'click' &&
4835
+ e.pointerType === 'mouse'
4836
+ ) {
4837
+ // don't register event listener in the same frame
4838
+ // to prevent it from being triggered immediately
4839
+ requestAnimationFrame(() => {
4840
+ document.addEventListener(
4841
+ 'pointerdown',
4842
+ this._clickTooltipOutsideHandler,
4843
+ { once: true, passive: true }
4844
+ );
4845
+ });
4846
+ }
4847
+
4715
4848
  this.moveTooltip(e);
4716
4849
  }
4717
4850
 
@@ -4721,7 +4854,12 @@
4721
4854
  if (!this.tooltip) {
4722
4855
  return;
4723
4856
  }
4857
+
4724
4858
  this.tooltip.classList.remove('svgMap-active');
4859
+ document.removeEventListener(
4860
+ 'pointerdown',
4861
+ this._clickTooltipOutsideHandler
4862
+ );
4725
4863
  }
4726
4864
 
4727
4865
  // Move the tooltip