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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svgmap",
3
3
  "description": "svgMap is a JavaScript library that lets you easily create an interactable world map comparing customizable data for each country.",
4
- "version": "2.19.2",
4
+ "version": "2.20.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -43,12 +43,12 @@
43
43
  "prepublishOnly": "npm run build && node test/assets.js"
44
44
  },
45
45
  "devDependencies": {
46
- "@rollup/plugin-commonjs": "^29.0.0",
46
+ "@rollup/plugin-commonjs": "^29.0.2",
47
47
  "@rollup/plugin-node-resolve": "^16.0.3",
48
- "@rollup/plugin-terser": "^0.4.4",
48
+ "@rollup/plugin-terser": "^1.0.0",
49
49
  "csso": "^5.0.5",
50
- "rollup": "^4.57.1",
50
+ "rollup": "^4.60.2",
51
51
  "rollup-plugin-postcss": "^4.0.2",
52
- "sass": "^1.97.3"
52
+ "sass": "^1.99.0"
53
53
  }
54
54
  }
@@ -70,6 +70,15 @@ export default class svgMap {
70
70
  // Set to true to open the link on mobile devices, set to false (default) to show the tooltip
71
71
  touchLink: false,
72
72
 
73
+ // When false, disables hover/touch-following tooltips (not the on-map persistent labels; see persistentTooltips)
74
+ showTooltips: true,
75
+
76
+ // 'hover' (default): mouse shows tooltip on enter. 'click': mouse opens tooltip on click; touch/pen unchanged.
77
+ tooltipTrigger: 'hover',
78
+
79
+ // Persistent on-map tooltips: an array of country IDs, or a function (countryID, countryValues) => boolean
80
+ persistentTooltips: false,
81
+
73
82
  // Set to true to show the to show a zoom reset button
74
83
  showZoomReset: false,
75
84
 
@@ -78,6 +87,10 @@ export default class svgMap {
78
87
  return null;
79
88
  },
80
89
 
90
+ // Called on country click (pointer released without dragging). Receives
91
+ // (countryID, event). Return false to skip opening data.values[*].link.
92
+ onCountryClick: null,
93
+
81
94
  // Country specific options
82
95
  countries: {
83
96
  // Western Sahara: Set to false to combine Morocco (MA) and Western Sahara (EH)
@@ -117,6 +130,9 @@ export default class svgMap {
117
130
  // Wrapper element
118
131
  this.wrapper = document.getElementById(this.options.targetElementID);
119
132
  this.wrapper.classList.add('svgMap-wrapper');
133
+ if (typeof this.options.onCountryClick === 'function') {
134
+ this.wrapper.classList.add('svgMap-country-click-callback');
135
+ }
120
136
 
121
137
  // Container element
122
138
  this.container = document.createElement('div');
@@ -808,7 +824,9 @@ export default class svgMap {
808
824
 
809
825
  createMap() {
810
826
  // Create the tooltip
811
- this.createTooltip();
827
+ if (this.options.showTooltips) {
828
+ this.createTooltip();
829
+ }
812
830
 
813
831
  // Create map wrappers
814
832
  this.mapWrapper = this.createElement(
@@ -816,6 +834,10 @@ export default class svgMap {
816
834
  'svgMap-map-wrapper',
817
835
  this.mapContainer
818
836
  );
837
+ this.mapWrapper.style.setProperty(
838
+ '--svg-map-country-fill',
839
+ this.toHex(this.options.colorNoData)
840
+ );
819
841
  this.mapImage = document.createElementNS(
820
842
  'http://www.w3.org/2000/svg',
821
843
  'svg'
@@ -930,6 +952,7 @@ export default class svgMap {
930
952
  }.bind(this);
931
953
 
932
954
  // Add map elements
955
+ var countryElements = [];
933
956
  Object.keys(mapPaths).forEach(
934
957
  function (countryID) {
935
958
  var countryData = this.mapPaths[countryID];
@@ -951,6 +974,7 @@ export default class svgMap {
951
974
  countryElement.classList.add('svgMap-country');
952
975
 
953
976
  this.mapImage.appendChild(countryElement);
977
+ countryElements.push(countryElement);
954
978
 
955
979
  // Add tooltip when touch is used
956
980
  function handlePointerMove(e) {
@@ -966,7 +990,7 @@ export default class svgMap {
966
990
  this.hideTooltip();
967
991
  document
968
992
  .querySelectorAll('.svgMap-active')
969
- .forEach(el => el.classList.remove('svgMap-active'));
993
+ .forEach((el) => el.classList.remove('svgMap-active'));
970
994
  }
971
995
  }
972
996
 
@@ -975,29 +999,40 @@ export default class svgMap {
975
999
  countryElement.addEventListener(
976
1000
  'pointerenter',
977
1001
  function (e) {
1002
+ if (
1003
+ e.pointerType === 'mouse' &&
1004
+ this.options.showTooltips &&
1005
+ this.options.tooltipTrigger === 'click'
1006
+ ) {
1007
+ return;
1008
+ }
1009
+
978
1010
  // Only add pointermove listener for non-touch pointers
979
1011
  if (e.pointerType !== 'touch') {
980
- document.addEventListener(
981
- 'pointermove',
982
- handlePointerMoveBound,
983
- { passive: true }
984
- );
1012
+ document.addEventListener('pointermove', handlePointerMoveBound, {
1013
+ passive: true
1014
+ });
985
1015
  }
986
1016
 
987
1017
  document
988
1018
  .querySelectorAll('.svgMap-active')
989
- .forEach(el => el.classList.remove('svgMap-active'));
1019
+ .forEach((el) => el.classList.remove('svgMap-active'));
990
1020
 
991
- countryElement.parentNode.appendChild(countryElement);
1021
+ countryElement.parentNode.insertBefore(
1022
+ countryElement,
1023
+ this.persistentTooltipGroup || null
1024
+ );
992
1025
  countryElement.classList.add('svgMap-active');
993
1026
 
994
1027
  const countryID = countryElement.getAttribute('data-id');
995
- this.setTooltipContent(this.getTooltipContent(countryID));
996
- this.showTooltip(e);
1028
+ if (this.options.showTooltips) {
1029
+ this.setTooltipContent(this.getTooltipContent(countryID));
1030
+ this.showTooltip(e);
997
1031
 
998
- // For touch, move tooltip to the touch position and keep it there
999
- if (e.pointerType === 'touch') {
1000
- this.moveTooltip(e);
1032
+ // For touch, move tooltip to the touch position and keep it there
1033
+ if (e.pointerType === 'touch') {
1034
+ this.moveTooltip(e);
1035
+ }
1001
1036
  }
1002
1037
  }.bind(this)
1003
1038
  );
@@ -1016,7 +1051,10 @@ export default class svgMap {
1016
1051
  'touchend',
1017
1052
  function (e) {
1018
1053
  const touch = e.changedTouches[0];
1019
- const elementAtEnd = document.elementFromPoint(touch.clientX, touch.clientY);
1054
+ const elementAtEnd = document.elementFromPoint(
1055
+ touch.clientX,
1056
+ touch.clientY
1057
+ );
1020
1058
 
1021
1059
  // Only hide if touch ended outside the country or tooltip
1022
1060
  if (
@@ -1027,7 +1065,7 @@ export default class svgMap {
1027
1065
  this.hideTooltip();
1028
1066
  document
1029
1067
  .querySelectorAll('.svgMap-active')
1030
- .forEach(el => el.classList.remove('svgMap-active'));
1068
+ .forEach((el) => el.classList.remove('svgMap-active'));
1031
1069
  }
1032
1070
  }.bind(this),
1033
1071
  { passive: true }
@@ -1038,11 +1076,22 @@ export default class svgMap {
1038
1076
  'pointerleave',
1039
1077
  function (e) {
1040
1078
  if (e.pointerType !== 'touch') {
1041
- document.removeEventListener('pointermove', handlePointerMoveBound);
1042
- this.hideTooltip();
1043
- document
1044
- .querySelectorAll('.svgMap-active')
1045
- .forEach(el => el.classList.remove('svgMap-active'));
1079
+ document.removeEventListener(
1080
+ 'pointermove',
1081
+ handlePointerMoveBound
1082
+ );
1083
+ if (
1084
+ !(
1085
+ e.pointerType === 'mouse' &&
1086
+ this.options.showTooltips &&
1087
+ this.options.tooltipTrigger === 'click'
1088
+ )
1089
+ ) {
1090
+ this.hideTooltip();
1091
+ document
1092
+ .querySelectorAll('.svgMap-active')
1093
+ .forEach((el) => el.classList.remove('svgMap-active'));
1094
+ }
1046
1095
  }
1047
1096
  }.bind(this)
1048
1097
  );
@@ -1062,7 +1111,7 @@ export default class svgMap {
1062
1111
  this.hideTooltip();
1063
1112
  document
1064
1113
  .querySelectorAll('.svgMap-active')
1065
- .forEach(el => el.classList.remove('svgMap-active'));
1114
+ .forEach((el) => el.classList.remove('svgMap-active'));
1066
1115
  }.bind(this),
1067
1116
  { passive: true }
1068
1117
  );
@@ -1086,17 +1135,29 @@ export default class svgMap {
1086
1135
  }.bind(this)
1087
1136
  );
1088
1137
 
1138
+ var persistent = this.options.persistentTooltips;
1139
+ if (
1140
+ persistent &&
1141
+ (Array.isArray(persistent) || typeof persistent === 'function')
1142
+ ) {
1143
+ this.createPersistentTooltips(countryElements);
1144
+ }
1145
+
1089
1146
  let pointerStart = null;
1090
1147
  let activeCountry = null;
1091
1148
 
1092
- this.mapImage.addEventListener('pointerdown', e => {
1093
- // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
1094
- if (e.button !== 0) return;
1149
+ this.mapImage.addEventListener(
1150
+ 'pointerdown',
1151
+ (e) => {
1152
+ // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
1153
+ if (e.button !== 0) return;
1095
1154
 
1096
- pointerStart = { x: e.clientX, y: e.clientY };
1097
- }, { passive: true });
1155
+ pointerStart = { x: e.clientX, y: e.clientY };
1156
+ },
1157
+ { passive: true }
1158
+ );
1098
1159
 
1099
- this.mapImage.addEventListener('pointerup', e => {
1160
+ this.mapImage.addEventListener('pointerup', (e) => {
1100
1161
  // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
1101
1162
  if (e.button !== 0) return;
1102
1163
 
@@ -1116,31 +1177,137 @@ export default class svgMap {
1116
1177
 
1117
1178
  const countryID = countryElement.getAttribute('data-id');
1118
1179
  const link = countryElement.getAttribute('data-link');
1119
- const target = countryElement.getAttribute('data-link-target');
1120
- if (!link) return;
1121
-
1180
+ const linkTarget = countryElement.getAttribute('data-link-target');
1181
+ const hasCallback = typeof this.options.onCountryClick === 'function';
1182
+ const hasLink = !!link;
1122
1183
  const isTouch = e.pointerType === 'touch' || e.pointerType === 'pen';
1123
1184
 
1185
+ const isClickTooltipMouse =
1186
+ e.pointerType === 'mouse' &&
1187
+ this.options.showTooltips &&
1188
+ this.options.tooltipTrigger === 'click';
1189
+
1190
+ if (!hasLink && !hasCallback && !isClickTooltipMouse) return;
1191
+
1192
+ if (isClickTooltipMouse && this.options.showTooltips) {
1193
+ const willNavigate =
1194
+ hasLink && countryElement.classList.contains('svgMap-active');
1195
+ const shouldFireCallback = hasCallback && (!hasLink || willNavigate);
1196
+
1197
+ var callbackResultClick;
1198
+ if (shouldFireCallback) {
1199
+ callbackResultClick = this.options.onCountryClick(countryID, e);
1200
+ }
1201
+
1202
+ if (hasLink) {
1203
+ if (callbackResultClick === false) return;
1204
+ if (countryElement.classList.contains('svgMap-active')) {
1205
+ if (linkTarget) window.open(link, linkTarget);
1206
+ else window.location.href = link;
1207
+ } else {
1208
+ this.mapImage
1209
+ .querySelectorAll('.svgMap-country.svgMap-active')
1210
+ .forEach((el) => el.classList.remove('svgMap-active'));
1211
+ countryElement.parentNode.insertBefore(
1212
+ countryElement,
1213
+ this.persistentTooltipGroup || null
1214
+ );
1215
+ countryElement.classList.add('svgMap-active');
1216
+ this.setTooltipContent(this.getTooltipContent(countryID));
1217
+ this.showTooltip(e);
1218
+ }
1219
+ return;
1220
+ }
1221
+
1222
+ if (callbackResultClick === false) return;
1223
+
1224
+ this.mapImage
1225
+ .querySelectorAll('.svgMap-country.svgMap-active')
1226
+ .forEach((el) => el.classList.remove('svgMap-active'));
1227
+ countryElement.parentNode.insertBefore(
1228
+ countryElement,
1229
+ this.persistentTooltipGroup || null
1230
+ );
1231
+ countryElement.classList.add('svgMap-active');
1232
+ this.setTooltipContent(this.getTooltipContent(countryID));
1233
+ this.showTooltip(e);
1234
+ return;
1235
+ }
1236
+
1237
+ const willNavigate =
1238
+ hasLink &&
1239
+ (!isTouch || countryElement.classList.contains('svgMap-active'));
1240
+
1241
+ const shouldFireCallback =
1242
+ hasCallback && (!hasLink || !isTouch || willNavigate);
1243
+
1244
+ var callbackResult;
1245
+ if (shouldFireCallback) {
1246
+ callbackResult = this.options.onCountryClick(countryID, e);
1247
+ }
1248
+
1249
+ if (!hasLink) return;
1250
+
1251
+ if (callbackResult === false) return;
1252
+
1124
1253
  if (isTouch) {
1125
1254
  // Touch: only open if already active
1126
1255
  if (countryElement.classList.contains('svgMap-active')) {
1127
- if (target) window.open(link, target);
1256
+ if (linkTarget) window.open(link, linkTarget);
1128
1257
  else window.location.href = link;
1129
1258
  } else {
1130
- // first tap shows tooltip
1259
+ // first tap shows tooltip (or opens link immediately if tooltips are off)
1131
1260
  if (activeCountry) activeCountry.classList.remove('svgMap-active');
1132
1261
  activeCountry = countryElement;
1133
1262
  countryElement.classList.add('svgMap-active');
1134
- this.setTooltipContent(this.getTooltipContent(countryID));
1135
- this.showTooltip(e);
1263
+ if (this.options.showTooltips) {
1264
+ this.setTooltipContent(this.getTooltipContent(countryID));
1265
+ this.showTooltip(e);
1266
+ } else {
1267
+ if (linkTarget) window.open(link, linkTarget);
1268
+ else window.location.href = link;
1269
+ }
1136
1270
  }
1137
1271
  } else {
1138
1272
  // Desktop: open immediately
1139
- if (target) window.open(link, target);
1273
+ if (linkTarget) window.open(link, linkTarget);
1140
1274
  else window.location.href = link;
1141
1275
  }
1142
1276
  });
1143
1277
 
1278
+ this._clickTooltipOutsideHandler = function (ev) {
1279
+ if (ev.pointerType !== 'mouse') return;
1280
+ if (
1281
+ !this.options.showTooltips ||
1282
+ this.options.tooltipTrigger !== 'click' ||
1283
+ !this.tooltip
1284
+ ) {
1285
+ return;
1286
+ }
1287
+ if (!this.tooltip.classList.contains('svgMap-active')) return;
1288
+ var node = ev.target;
1289
+ if (
1290
+ node &&
1291
+ node.closest &&
1292
+ (node.closest('.svgMap-country') || node.closest('.svgMap-tooltip'))
1293
+ ) {
1294
+ return;
1295
+ }
1296
+ this.hideTooltip();
1297
+ if (this.mapImage) {
1298
+ this.mapImage
1299
+ .querySelectorAll('.svgMap-country.svgMap-active')
1300
+ .forEach(function (el) {
1301
+ el.classList.remove('svgMap-active');
1302
+ });
1303
+ }
1304
+ }.bind(this);
1305
+ document.addEventListener(
1306
+ 'pointerdown',
1307
+ this._clickTooltipOutsideHandler,
1308
+ true
1309
+ );
1310
+
1144
1311
  // Expose instance
1145
1312
  var me = this;
1146
1313
 
@@ -1206,13 +1373,82 @@ export default class svgMap {
1206
1373
  }
1207
1374
  }
1208
1375
 
1376
+ // Create the persistent tooltips
1377
+
1378
+ createPersistentTooltips(countryElements) {
1379
+ if (this.persistentTooltipGroup) {
1380
+ this.persistentTooltipGroup.remove();
1381
+ }
1382
+
1383
+ this.persistentTooltipGroup = document.createElementNS(
1384
+ 'http://www.w3.org/2000/svg',
1385
+ 'g'
1386
+ );
1387
+ this.persistentTooltipGroup.classList.add('svgMap-persistent-tooltips');
1388
+ this.mapImage.appendChild(this.persistentTooltipGroup);
1389
+
1390
+ countryElements.forEach(
1391
+ function (countryElement) {
1392
+ var countryID = countryElement.getAttribute('data-id');
1393
+ if (!this.shouldShowTooltipOnLoad(countryID)) {
1394
+ return;
1395
+ }
1396
+
1397
+ var boundingBox = countryElement.getBBox();
1398
+ var tooltipPosition = {
1399
+ x: boundingBox.x + boundingBox.width / 2,
1400
+ y: boundingBox.y + boundingBox.height / 2
1401
+ };
1402
+
1403
+ var tooltipObject = document.createElementNS(
1404
+ 'http://www.w3.org/2000/svg',
1405
+ 'foreignObject'
1406
+ );
1407
+ tooltipObject.setAttribute('x', tooltipPosition.x);
1408
+ tooltipObject.setAttribute('y', tooltipPosition.y);
1409
+ tooltipObject.setAttribute('width', 1);
1410
+ tooltipObject.setAttribute('height', 1);
1411
+ tooltipObject.classList.add('svgMap-persistent-tooltip-wrapper');
1412
+
1413
+ var tooltipElement = this.createElement(
1414
+ 'div',
1415
+ 'svgMap-persistent-tooltip',
1416
+ tooltipObject
1417
+ );
1418
+ tooltipElement.append(
1419
+ this.getTooltipContent(countryID, tooltipElement)
1420
+ );
1421
+ this.createElement('div', 'svgMap-tooltip-pointer', tooltipElement);
1422
+
1423
+ this.persistentTooltipGroup.appendChild(tooltipObject);
1424
+ }.bind(this)
1425
+ );
1426
+ }
1427
+
1428
+ // Check if a persistent tooltip should be shown on load
1429
+
1430
+ shouldShowTooltipOnLoad(countryID) {
1431
+ var persistent = this.options.persistentTooltips;
1432
+ var countryValues = this.options.data.values[countryID];
1433
+
1434
+ if (Array.isArray(persistent)) {
1435
+ return persistent.indexOf(countryID) !== -1;
1436
+ }
1437
+
1438
+ if (typeof persistent === 'function') {
1439
+ return persistent(countryID, countryValues);
1440
+ }
1441
+
1442
+ return false;
1443
+ }
1444
+
1209
1445
  // Create the tooltip content
1210
1446
 
1211
- getTooltipContent(countryID) {
1447
+ getTooltipContent(countryID, tooltipDiv = this.tooltip) {
1212
1448
  // Custom tooltip
1213
1449
  if (this.options.onGetTooltip) {
1214
1450
  var customDiv = this.options.onGetTooltip(
1215
- this.tooltip,
1451
+ tooltipDiv,
1216
1452
  countryID,
1217
1453
  this.options.data.values[countryID]
1218
1454
  );
@@ -2194,6 +2430,9 @@ export default class svgMap {
2194
2430
  // Show the tooltip
2195
2431
 
2196
2432
  showTooltip(e) {
2433
+ if (!this.tooltip) {
2434
+ return;
2435
+ }
2197
2436
  this.tooltip.classList.add('svgMap-active');
2198
2437
  this.moveTooltip(e);
2199
2438
  }
@@ -2201,12 +2440,18 @@ export default class svgMap {
2201
2440
  // Hide the tooltip
2202
2441
 
2203
2442
  hideTooltip() {
2443
+ if (!this.tooltip) {
2444
+ return;
2445
+ }
2204
2446
  this.tooltip.classList.remove('svgMap-active');
2205
2447
  }
2206
2448
 
2207
2449
  // Move the tooltip
2208
2450
 
2209
2451
  moveTooltip(e) {
2452
+ if (!this.tooltip) {
2453
+ return;
2454
+ }
2210
2455
  var x = e.pageX || (e.touches && e.touches[0] ? e.touches[0].pageX : null);
2211
2456
  var y = e.pageY || (e.touches && e.touches[0] ? e.touches[0].pageY : null);
2212
2457
 
package/src/scss/map.scss CHANGED
@@ -255,3 +255,7 @@
255
255
  }
256
256
  }
257
257
  }
258
+
259
+ .svgMap-wrapper.svgMap-country-click-callback .svgMap-map-wrapper .svgMap-country {
260
+ cursor: pointer;
261
+ }
@@ -1,6 +1,7 @@
1
1
  @use 'variables' as *;
2
2
 
3
- .svgMap-tooltip {
3
+ .svgMap-tooltip,
4
+ .svgMap-persistent-tooltip {
4
5
  box-shadow: 0 0 3px $mapTooltipBoxShadowColor;
5
6
  position: absolute;
6
7
  z-index: 2;
@@ -8,7 +9,6 @@
8
9
  background: $mapTooltipBackgroundColor;
9
10
  transform: translate(-50%, -100%);
10
11
  border-bottom: 1px solid $mapTooltipColor;
11
- display: none;
12
12
  pointer-events: none;
13
13
  min-width: 60px;
14
14
 
@@ -18,10 +18,6 @@
18
18
  border-top: 1px solid $mapTooltipColor;
19
19
  }
20
20
 
21
- &.svgMap-active {
22
- display: block;
23
- }
24
-
25
21
  .svgMap-tooltip-content-container {
26
22
  position: relative;
27
23
  padding: 10px 20px;
@@ -126,3 +122,27 @@
126
122
  }
127
123
  }
128
124
  }
125
+
126
+ .svgMap-tooltip {
127
+ display: none;
128
+
129
+ &.svgMap-active {
130
+ display: block;
131
+ }
132
+ }
133
+
134
+ .svgMap-persistent-tooltips,
135
+ .svgMap-persistent-tooltip-wrapper {
136
+ pointer-events: none;
137
+ }
138
+
139
+ .svgMap-persistent-tooltip-wrapper {
140
+ overflow: visible;
141
+ }
142
+
143
+ .svgMap-persistent-tooltip {
144
+ display: block;
145
+ left: 0;
146
+ top: 0;
147
+ width: max-content;
148
+ }