svgmap 2.19.3 → 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.3",
4
+ "version": "2.20.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -47,8 +47,8 @@
47
47
  "@rollup/plugin-node-resolve": "^16.0.3",
48
48
  "@rollup/plugin-terser": "^1.0.0",
49
49
  "csso": "^5.0.5",
50
- "rollup": "^4.60.1",
50
+ "rollup": "^4.60.2",
51
51
  "rollup-plugin-postcss": "^4.0.2",
52
- "sass": "^1.98.0"
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(
@@ -934,6 +952,7 @@ export default class svgMap {
934
952
  }.bind(this);
935
953
 
936
954
  // Add map elements
955
+ var countryElements = [];
937
956
  Object.keys(mapPaths).forEach(
938
957
  function (countryID) {
939
958
  var countryData = this.mapPaths[countryID];
@@ -955,6 +974,7 @@ export default class svgMap {
955
974
  countryElement.classList.add('svgMap-country');
956
975
 
957
976
  this.mapImage.appendChild(countryElement);
977
+ countryElements.push(countryElement);
958
978
 
959
979
  // Add tooltip when touch is used
960
980
  function handlePointerMove(e) {
@@ -970,7 +990,7 @@ export default class svgMap {
970
990
  this.hideTooltip();
971
991
  document
972
992
  .querySelectorAll('.svgMap-active')
973
- .forEach(el => el.classList.remove('svgMap-active'));
993
+ .forEach((el) => el.classList.remove('svgMap-active'));
974
994
  }
975
995
  }
976
996
 
@@ -979,29 +999,40 @@ export default class svgMap {
979
999
  countryElement.addEventListener(
980
1000
  'pointerenter',
981
1001
  function (e) {
1002
+ if (
1003
+ e.pointerType === 'mouse' &&
1004
+ this.options.showTooltips &&
1005
+ this.options.tooltipTrigger === 'click'
1006
+ ) {
1007
+ return;
1008
+ }
1009
+
982
1010
  // Only add pointermove listener for non-touch pointers
983
1011
  if (e.pointerType !== 'touch') {
984
- document.addEventListener(
985
- 'pointermove',
986
- handlePointerMoveBound,
987
- { passive: true }
988
- );
1012
+ document.addEventListener('pointermove', handlePointerMoveBound, {
1013
+ passive: true
1014
+ });
989
1015
  }
990
1016
 
991
1017
  document
992
1018
  .querySelectorAll('.svgMap-active')
993
- .forEach(el => el.classList.remove('svgMap-active'));
1019
+ .forEach((el) => el.classList.remove('svgMap-active'));
994
1020
 
995
- countryElement.parentNode.appendChild(countryElement);
1021
+ countryElement.parentNode.insertBefore(
1022
+ countryElement,
1023
+ this.persistentTooltipGroup || null
1024
+ );
996
1025
  countryElement.classList.add('svgMap-active');
997
1026
 
998
1027
  const countryID = countryElement.getAttribute('data-id');
999
- this.setTooltipContent(this.getTooltipContent(countryID));
1000
- this.showTooltip(e);
1028
+ if (this.options.showTooltips) {
1029
+ this.setTooltipContent(this.getTooltipContent(countryID));
1030
+ this.showTooltip(e);
1001
1031
 
1002
- // For touch, move tooltip to the touch position and keep it there
1003
- if (e.pointerType === 'touch') {
1004
- 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
+ }
1005
1036
  }
1006
1037
  }.bind(this)
1007
1038
  );
@@ -1020,7 +1051,10 @@ export default class svgMap {
1020
1051
  'touchend',
1021
1052
  function (e) {
1022
1053
  const touch = e.changedTouches[0];
1023
- const elementAtEnd = document.elementFromPoint(touch.clientX, touch.clientY);
1054
+ const elementAtEnd = document.elementFromPoint(
1055
+ touch.clientX,
1056
+ touch.clientY
1057
+ );
1024
1058
 
1025
1059
  // Only hide if touch ended outside the country or tooltip
1026
1060
  if (
@@ -1031,7 +1065,7 @@ export default class svgMap {
1031
1065
  this.hideTooltip();
1032
1066
  document
1033
1067
  .querySelectorAll('.svgMap-active')
1034
- .forEach(el => el.classList.remove('svgMap-active'));
1068
+ .forEach((el) => el.classList.remove('svgMap-active'));
1035
1069
  }
1036
1070
  }.bind(this),
1037
1071
  { passive: true }
@@ -1042,11 +1076,22 @@ export default class svgMap {
1042
1076
  'pointerleave',
1043
1077
  function (e) {
1044
1078
  if (e.pointerType !== 'touch') {
1045
- document.removeEventListener('pointermove', handlePointerMoveBound);
1046
- this.hideTooltip();
1047
- document
1048
- .querySelectorAll('.svgMap-active')
1049
- .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
+ }
1050
1095
  }
1051
1096
  }.bind(this)
1052
1097
  );
@@ -1066,7 +1111,7 @@ export default class svgMap {
1066
1111
  this.hideTooltip();
1067
1112
  document
1068
1113
  .querySelectorAll('.svgMap-active')
1069
- .forEach(el => el.classList.remove('svgMap-active'));
1114
+ .forEach((el) => el.classList.remove('svgMap-active'));
1070
1115
  }.bind(this),
1071
1116
  { passive: true }
1072
1117
  );
@@ -1090,17 +1135,29 @@ export default class svgMap {
1090
1135
  }.bind(this)
1091
1136
  );
1092
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
+
1093
1146
  let pointerStart = null;
1094
1147
  let activeCountry = null;
1095
1148
 
1096
- this.mapImage.addEventListener('pointerdown', e => {
1097
- // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
1098
- 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;
1099
1154
 
1100
- pointerStart = { x: e.clientX, y: e.clientY };
1101
- }, { passive: true });
1155
+ pointerStart = { x: e.clientX, y: e.clientY };
1156
+ },
1157
+ { passive: true }
1158
+ );
1102
1159
 
1103
- this.mapImage.addEventListener('pointerup', e => {
1160
+ this.mapImage.addEventListener('pointerup', (e) => {
1104
1161
  // Ignore right click (on desktop it allows inspecting the chart elements without opening the URL)
1105
1162
  if (e.button !== 0) return;
1106
1163
 
@@ -1120,31 +1177,137 @@ export default class svgMap {
1120
1177
 
1121
1178
  const countryID = countryElement.getAttribute('data-id');
1122
1179
  const link = countryElement.getAttribute('data-link');
1123
- const target = countryElement.getAttribute('data-link-target');
1124
- if (!link) return;
1125
-
1180
+ const linkTarget = countryElement.getAttribute('data-link-target');
1181
+ const hasCallback = typeof this.options.onCountryClick === 'function';
1182
+ const hasLink = !!link;
1126
1183
  const isTouch = e.pointerType === 'touch' || e.pointerType === 'pen';
1127
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
+
1128
1253
  if (isTouch) {
1129
1254
  // Touch: only open if already active
1130
1255
  if (countryElement.classList.contains('svgMap-active')) {
1131
- if (target) window.open(link, target);
1256
+ if (linkTarget) window.open(link, linkTarget);
1132
1257
  else window.location.href = link;
1133
1258
  } else {
1134
- // first tap shows tooltip
1259
+ // first tap shows tooltip (or opens link immediately if tooltips are off)
1135
1260
  if (activeCountry) activeCountry.classList.remove('svgMap-active');
1136
1261
  activeCountry = countryElement;
1137
1262
  countryElement.classList.add('svgMap-active');
1138
- this.setTooltipContent(this.getTooltipContent(countryID));
1139
- 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
+ }
1140
1270
  }
1141
1271
  } else {
1142
1272
  // Desktop: open immediately
1143
- if (target) window.open(link, target);
1273
+ if (linkTarget) window.open(link, linkTarget);
1144
1274
  else window.location.href = link;
1145
1275
  }
1146
1276
  });
1147
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
+
1148
1311
  // Expose instance
1149
1312
  var me = this;
1150
1313
 
@@ -1210,13 +1373,82 @@ export default class svgMap {
1210
1373
  }
1211
1374
  }
1212
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
+
1213
1445
  // Create the tooltip content
1214
1446
 
1215
- getTooltipContent(countryID) {
1447
+ getTooltipContent(countryID, tooltipDiv = this.tooltip) {
1216
1448
  // Custom tooltip
1217
1449
  if (this.options.onGetTooltip) {
1218
1450
  var customDiv = this.options.onGetTooltip(
1219
- this.tooltip,
1451
+ tooltipDiv,
1220
1452
  countryID,
1221
1453
  this.options.data.values[countryID]
1222
1454
  );
@@ -2198,6 +2430,9 @@ export default class svgMap {
2198
2430
  // Show the tooltip
2199
2431
 
2200
2432
  showTooltip(e) {
2433
+ if (!this.tooltip) {
2434
+ return;
2435
+ }
2201
2436
  this.tooltip.classList.add('svgMap-active');
2202
2437
  this.moveTooltip(e);
2203
2438
  }
@@ -2205,12 +2440,18 @@ export default class svgMap {
2205
2440
  // Hide the tooltip
2206
2441
 
2207
2442
  hideTooltip() {
2443
+ if (!this.tooltip) {
2444
+ return;
2445
+ }
2208
2446
  this.tooltip.classList.remove('svgMap-active');
2209
2447
  }
2210
2448
 
2211
2449
  // Move the tooltip
2212
2450
 
2213
2451
  moveTooltip(e) {
2452
+ if (!this.tooltip) {
2453
+ return;
2454
+ }
2214
2455
  var x = e.pageX || (e.touches && e.touches[0] ? e.touches[0].pageX : null);
2215
2456
  var y = e.pageY || (e.touches && e.touches[0] ? e.touches[0].pageY : null);
2216
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
+ }