mapshaper 0.7.2 → 0.7.4

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.
Files changed (86) hide show
  1. package/LICENSE +1 -1
  2. package/bin/mapshaper-gui +31 -7
  3. package/mapshaper.js +45 -42
  4. package/package.json +8 -7
  5. package/www/geopackage.js +10 -10
  6. package/www/index.html +32 -4
  7. package/www/mapshaper-gui.js +529 -83
  8. package/www/mapshaper.js +45 -42
  9. package/www/modules.js +5 -5
  10. package/www/page.css +157 -0
  11. package/www/docs/_assets/cmd-search.js +0 -213
  12. package/www/docs/_assets/docs.css +0 -722
  13. package/www/docs/_assets/docs.js +0 -75
  14. package/www/docs/_assets/highlight.css +0 -10
  15. package/www/docs/essentials/command-line.html +0 -127
  16. package/www/docs/essentials/command-line.html.md +0 -112
  17. package/www/docs/essentials/web-app.html +0 -138
  18. package/www/docs/essentials/web-app.html.md +0 -106
  19. package/www/docs/examples/basics.html +0 -275
  20. package/www/docs/examples/basics.html.md +0 -370
  21. package/www/docs/examples/data/Makefile +0 -31
  22. package/www/docs/examples/data/globe.msx +0 -0
  23. package/www/docs/examples/data/globe.svg +0 -616
  24. package/www/docs/examples/data/globe.txt +0 -21
  25. package/www/docs/examples/data/globe.zip +0 -0
  26. package/www/docs/examples/data/ne_50m_admin_0_countries.geojson +0 -1
  27. package/www/docs/examples/data/ne_50m_admin_1_states_provinces_lakes.geojson +0 -1
  28. package/www/docs/examples/data/us-states.msx +0 -0
  29. package/www/docs/examples/data/us-states.svg +0 -56
  30. package/www/docs/examples/data/us-states.txt +0 -6
  31. package/www/docs/examples/data/us-states.zip +0 -0
  32. package/www/docs/examples/globe.html +0 -108
  33. package/www/docs/examples/globe.html.md +0 -64
  34. package/www/docs/examples/us-states.html +0 -88
  35. package/www/docs/examples/us-states.html.md +0 -44
  36. package/www/docs/formats/csv.html +0 -127
  37. package/www/docs/formats/csv.html.md +0 -97
  38. package/www/docs/formats/dbf.html +0 -87
  39. package/www/docs/formats/dbf.html.md +0 -39
  40. package/www/docs/formats/flatgeobuf.html +0 -85
  41. package/www/docs/formats/flatgeobuf.html.md +0 -41
  42. package/www/docs/formats/geojson.html +0 -107
  43. package/www/docs/formats/geojson.html.md +0 -65
  44. package/www/docs/formats/geopackage.html +0 -87
  45. package/www/docs/formats/geopackage.html.md +0 -42
  46. package/www/docs/formats/json.html +0 -83
  47. package/www/docs/formats/json.html.md +0 -35
  48. package/www/docs/formats/kml.html +0 -82
  49. package/www/docs/formats/kml.html.md +0 -39
  50. package/www/docs/formats/overview.html +0 -191
  51. package/www/docs/formats/overview.html.md +0 -34
  52. package/www/docs/formats/shapefile.html +0 -123
  53. package/www/docs/formats/shapefile.html.md +0 -84
  54. package/www/docs/formats/snapshot.html +0 -87
  55. package/www/docs/formats/snapshot.html.md +0 -39
  56. package/www/docs/formats/svg.html +0 -99
  57. package/www/docs/formats/svg.html.md +0 -51
  58. package/www/docs/formats/topojson.html +0 -102
  59. package/www/docs/formats/topojson.html.md +0 -54
  60. package/www/docs/gallery/index.html +0 -80
  61. package/www/docs/gallery/index.html.md +0 -29
  62. package/www/docs/guides/combining-layers.html +0 -109
  63. package/www/docs/guides/combining-layers.html.md +0 -89
  64. package/www/docs/guides/expressions.html +0 -600
  65. package/www/docs/guides/expressions.html.md +0 -376
  66. package/www/docs/guides/programmatic.html +0 -117
  67. package/www/docs/guides/programmatic.html.md +0 -91
  68. package/www/docs/guides/projections.html +0 -158
  69. package/www/docs/guides/projections.html.md +0 -118
  70. package/www/docs/guides/simplification.html +0 -110
  71. package/www/docs/guides/simplification.html.md +0 -96
  72. package/www/docs/guides/topology.html +0 -90
  73. package/www/docs/guides/topology.html.md +0 -63
  74. package/www/docs/images/cal-counties.png +0 -0
  75. package/www/docs/images/simplification-detail.png +0 -0
  76. package/www/docs/images/simplification-dp.png +0 -0
  77. package/www/docs/images/simplification-mod2.png +0 -0
  78. package/www/docs/images/tiger-counties.png +0 -0
  79. package/www/docs/index.html +0 -101
  80. package/www/docs/index.html.md +0 -59
  81. package/www/docs/reference.html +0 -1302
  82. package/www/docs/reference.html.md +0 -1817
  83. package/www/docs/whats-new.html +0 -77
  84. package/www/docs/whats-new.html.md +0 -59
  85. package/www/llms-full.txt +0 -4053
  86. package/www/llms.txt +0 -55
package/www/mapshaper.js CHANGED
@@ -12416,8 +12416,8 @@
12416
12416
  return obj;
12417
12417
  };
12418
12418
 
12419
- async function writeFiles(exports, opts) {
12420
- return _writeFiles(exports, opts);
12419
+ async function writeFiles(exports$1, opts) {
12420
+ return _writeFiles(exports$1, opts);
12421
12421
  }
12422
12422
 
12423
12423
  // Used by GUI to replace the CLI version of writeFiles()
@@ -12426,24 +12426,24 @@
12426
12426
  _writeFiles = func;
12427
12427
  }
12428
12428
 
12429
- var _writeFiles = function(exports, opts) {
12430
- if (exports.length > 0 === false) {
12429
+ var _writeFiles = function(exports$1, opts) {
12430
+ if (exports$1.length > 0 === false) {
12431
12431
  message("No files to save");
12432
12432
  } else if (opts.dry_run) ; else if (opts.stdout) {
12433
12433
  // Using async writeFile() function -- synchronous output to stdout can
12434
12434
  // trigger EAGAIN error, e.g. when piped to less.
12435
- require$1('rw').writeFile('/dev/stdout', exports[0].content, function() {});
12435
+ require$1('rw').writeFile('/dev/stdout', exports$1[0].content, function() {});
12436
12436
  } else {
12437
12437
  if (opts.zip) {
12438
- exports = [{
12438
+ exports$1 = [{
12439
12439
  // TODO: add output subdirectory, if relevant
12440
12440
  filename: opts.zipfile || 'output.zip',
12441
- content: zipSync(exports)
12441
+ content: zipSync(exports$1)
12442
12442
  }];
12443
12443
  }
12444
- var paths = getOutputPaths(utils.pluck(exports, 'filename'), opts);
12444
+ var paths = getOutputPaths(utils.pluck(exports$1, 'filename'), opts);
12445
12445
  var inputFiles = getStashedVar('input_files');
12446
- exports.forEach(function(obj, i) {
12446
+ exports$1.forEach(function(obj, i) {
12447
12447
  var path = paths[i];
12448
12448
  if (obj.content instanceof ArrayBuffer) {
12449
12449
  // replacing content so ArrayBuffers can be gc'd
@@ -14993,10 +14993,10 @@
14993
14993
  return d;
14994
14994
  }
14995
14995
 
14996
- var cache$1 = {};
14996
+ var cache = {};
14997
14997
 
14998
14998
  function getParseRxp(fmt) {
14999
- if (fmt in cache$1) return cache$1[fmt];
14999
+ if (fmt in cache) return cache[fmt];
15000
15000
  var rxp = fmt;
15001
15001
  rxp = rxp.replace('[-]', '(?<prefix>-)?'); // optional -
15002
15002
  rxp = rxp.replace(/\[[NSEW, +-]{2,}\]/, '(?<prefix>$&)');
@@ -15020,11 +15020,11 @@
15020
15020
  rxp = '^' + rxp + '$';
15021
15021
  try {
15022
15022
  // TODO: make sure all DMS codes have been matched
15023
- cache$1[fmt] = new RegExp(rxp);
15023
+ cache[fmt] = new RegExp(rxp);
15024
15024
  } catch(e) {
15025
15025
  stop$1('Invalid DMS format string:', fmt);
15026
15026
  }
15027
- return cache$1[fmt];
15027
+ return cache[fmt];
15028
15028
  }
15029
15029
 
15030
15030
  function formatNumber(val, integers, decimals) {
@@ -21150,15 +21150,6 @@
21150
21150
  getSphereEffectParams: getSphereEffectParams
21151
21151
  });
21152
21152
 
21153
- var cache = {};
21154
- function fetchFileSync(url) {
21155
- if (url in cache) return cache[url];
21156
- var res = require$1('sync-request')('GET', url, {timeout: 2000});
21157
- var content = res.getBody().toString();
21158
- cache[url] = content;
21159
- return content;
21160
- }
21161
-
21162
21153
  // convert object properties to definitions for images and hatch fills
21163
21154
  function convertPropertiesToDefinitions(obj, defs) {
21164
21155
  procNode(obj);
@@ -21183,6 +21174,13 @@
21183
21174
  function convertSvgImage(obj, defs) {
21184
21175
  // Same-origin policy prevents embedding images in the web UI
21185
21176
  var href = obj.properties.href;
21177
+ // Remote URLs were previously fetched at export time so the SVG could be
21178
+ // embedded inline. That capability has been removed (it required a
21179
+ // synchronous-HTTP dependency built on a child-process hack); download
21180
+ // the asset first and reference it by local path instead.
21181
+ if (href.indexOf('http') === 0) {
21182
+ stop$1('Remote SVG asset references are not supported. Download the file first and reference it by a local path:', href);
21183
+ }
21186
21184
  // look for a previously added definition to use
21187
21185
  // (assumes that images that share the same href can also use the same defn)
21188
21186
  var item = utils.find(defs, function(item) {return item.href == href;});
@@ -21200,12 +21198,12 @@
21200
21198
  }
21201
21199
  }
21202
21200
 
21203
- // Returns the content of an SVG file from a local path or URL
21204
- // Returns '' if unable to get the content (e.g. due to cross-domain security rules)
21201
+ // Returns the content of an SVG file from a local path.
21202
+ // Returns '' if unable to get the content; the caller leaves the original
21203
+ // <image> tag in the output so the SVG renderer can attempt to load it.
21205
21204
  function serializeSvgImage(href, id) {
21206
21205
  var svg = '';
21207
21206
  try {
21208
- // try to download the SVG content and use that
21209
21207
  svg = convertSvgToDefn(getSvgContent(href), id) + '\n';
21210
21208
  svg = '<!-- ' + href + '-->\n' + svg; // add href as a comment, to aid in debugging
21211
21209
  } catch(e) {
@@ -21216,20 +21214,12 @@
21216
21214
  return svg;
21217
21215
  }
21218
21216
 
21219
- // href: A URL or a local path
21220
- // TODO: download SVG files asynchronously
21221
- // (currently, files are downloaded synchronously, which is obviously undesirable)
21222
- //
21217
+ // href: A local path
21223
21218
  function getSvgContent(href) {
21224
- var content;
21225
- if (href.indexOf('http') === 0) {
21226
- content = fetchFileSync(href);
21227
- } else if (require$1('fs').existsSync(href)) {
21228
- content = require$1('fs').readFileSync(href, 'utf8');
21229
- } else {
21230
- stop$1("Invalid SVG location:", href);
21219
+ if (require$1('fs').existsSync(href)) {
21220
+ return require$1('fs').readFileSync(href, 'utf8');
21231
21221
  }
21232
- return content;
21222
+ stop$1("Invalid SVG location:", href);
21233
21223
  }
21234
21224
 
21235
21225
  function convertSvgToDefn(svg, id) {
@@ -21372,12 +21362,27 @@ ${svg}
21372
21362
  var geojson = exportDatasetAsGeoJSON(d, opts);
21373
21363
  var features = geojson.features || geojson.geometries || (geojson.type ? [geojson] : []);
21374
21364
  var children = importGeoJSONFeatures(features, opts);
21365
+ // Drop empty placeholder <g/> elements (features whose geometry was null in the
21366
+ // source data, collapsed during simplification, or otherwise produced no
21367
+ // visible output). Keep the layer's records in lockstep so that data-*
21368
+ // attributes added via -o svg-data= remain correctly aligned.
21369
+ var records = lyr.data ? lyr.data.getRecords() : null;
21370
+ var keptRecords = records ? [] : null;
21371
+ children = children.filter(function(child, i) {
21372
+ if (isEmptyPlaceholder(child)) return false;
21373
+ if (keptRecords) keptRecords.push(records[i]);
21374
+ return true;
21375
+ });
21375
21376
  if (opts.svg_data && lyr.data) {
21376
- addDataAttributesToSVG(children, lyr.data, opts.svg_data);
21377
+ addDataAttributesToSVG(children, keptRecords, lyr.data.getFields(), opts.svg_data);
21377
21378
  }
21378
21379
  return children;
21379
21380
  }
21380
21381
 
21382
+ function isEmptyPlaceholder(o) {
21383
+ return o.tag == 'g' && (!o.children || o.children.length === 0);
21384
+ }
21385
+
21381
21386
  function validateSvgDataFields(layers, fieldsArg) {
21382
21387
  var missingFields = fieldsArg.reduce(function(memo, field) {
21383
21388
  if (!fieldExists(layers, field)) {
@@ -21397,15 +21402,13 @@ ${svg}
21397
21402
  }
21398
21403
  }
21399
21404
 
21400
- function addDataAttributesToSVG(children, table, fieldsArg) {
21401
- var allFields = table.getFields();
21405
+ function addDataAttributesToSVG(children, records, allFields, fieldsArg) {
21402
21406
  var dataFields = fieldsArg.indexOf('*') > -1 ? allFields.concat() : fieldsArg;
21403
21407
  var missingFields = utils.difference(dataFields, allFields);
21404
21408
  if (missingFields.length > 0) {
21405
21409
  dataFields = utils.difference(dataFields, missingFields);
21406
21410
  // stop("Missing data field(s):", missingFields.join(', '));
21407
21411
  }
21408
- var records = table.getRecords();
21409
21412
  var data = exportDataAttributesForSVG(records, dataFields);
21410
21413
  if (children.length != data.length) {
21411
21414
  error("Mismatch between number of SVG symbols and data attributes");
@@ -52127,7 +52130,7 @@ ${svg}
52127
52130
  });
52128
52131
  }
52129
52132
 
52130
- var version = "0.7.2";
52133
+ var version = "0.7.4";
52131
52134
 
52132
52135
  // Parse command line args into commands and run them
52133
52136
  // Function takes an optional Node-style callback. A Promise is returned if no callback is given.
package/www/modules.js CHANGED
@@ -13503,7 +13503,7 @@
13503
13503
  function requireEncodings () {
13504
13504
  if (hasRequiredEncodings) return encodings;
13505
13505
  hasRequiredEncodings = 1;
13506
- (function (exports) {
13506
+ (function (exports$1) {
13507
13507
  "use strict";
13508
13508
 
13509
13509
  // Update this array if you add/rename/remove files in this directory.
@@ -13525,7 +13525,7 @@
13525
13525
  var module = modules[i];
13526
13526
  for (var enc in module)
13527
13527
  if (Object.prototype.hasOwnProperty.call(module, enc))
13528
- exports[enc] = module[enc];
13528
+ exports$1[enc] = module[enc];
13529
13529
  }
13530
13530
  } (encodings));
13531
13531
  return encodings;
@@ -18975,7 +18975,7 @@
18975
18975
  tcxGen: tcxGen
18976
18976
  });
18977
18977
 
18978
- var __filename = '/Users/matthewbloch/mb4/mapshaper/node_modules/mproj/dist';
18978
+ var __filename$1 = '/Users/matthewbloch/mb4/mapshaper/node_modules/mproj/dist';
18979
18979
 
18980
18980
  var mproj$2 = {exports: {}};
18981
18981
 
@@ -19247,7 +19247,7 @@
19247
19247
  function requireMproj () {
19248
19248
  if (hasRequiredMproj) return mproj$2.exports;
19249
19249
  hasRequiredMproj = 1;
19250
- (function (module, exports) {
19250
+ (function (module, exports$1) {
19251
19251
  (function(){
19252
19252
 
19253
19253
  // add math.h functions to library scope
@@ -19967,7 +19967,7 @@
19967
19967
  var fs = require$$0,
19968
19968
  path = require$$1,
19969
19969
  // path to library assumes mproj script is in the dist/ directory
19970
- dir = path.join(path.dirname(__filename), '../nad'),
19970
+ dir = path.join(path.dirname(__filename$1), '../nad'),
19971
19971
  pathUC = path.join(dir, libFile.toUpperCase()),
19972
19972
  pathLC = path.join(dir, libFile.toLowerCase()),
19973
19973
  contents;
package/www/page.css CHANGED
@@ -165,6 +165,13 @@ body.map-view {
165
165
 
166
166
  }
167
167
 
168
+ /* When the page is preloaded with data (CLI files, ?files= or ?catalog= URL
169
+ params), suppress the splash header links so they don't flash into view
170
+ for ~a second before the editor mounts and hides them. */
171
+ .mapshaper-preload #splash-buttons {
172
+ display: none;
173
+ }
174
+
168
175
  .page-header-buttons {
169
176
  z-index: 20;
170
177
  position: absolute;
@@ -1197,6 +1204,156 @@ img.close-btn:hover,
1197
1204
  pointer-events: initial;
1198
1205
  }
1199
1206
 
1207
+ /* --- Messages inbox --------------- */
1208
+
1209
+ .messages-btn.btn {
1210
+ /* SVG-only button; align like the hamburger so its baseline doesn't drag
1211
+ the row's text-bearing buttons down. */
1212
+ padding: 8px 8px 4px 8px;
1213
+ user-select: none;
1214
+ vertical-align: top;
1215
+ cursor: pointer;
1216
+ position: relative;
1217
+ }
1218
+
1219
+ .messages-btn.btn.hidden {
1220
+ display: none;
1221
+ }
1222
+
1223
+ .messages-btn svg {
1224
+ display: block;
1225
+ fill: currentColor;
1226
+ }
1227
+
1228
+ .messages-badge {
1229
+ position: absolute;
1230
+ top: 2px;
1231
+ right: -1px;
1232
+ min-width: 14px;
1233
+ height: 14px;
1234
+ padding: 0 3px;
1235
+ border-radius: 7px;
1236
+ background-color: #d63333;
1237
+ color: #fff;
1238
+ font-size: 10px;
1239
+ line-height: 14px;
1240
+ font-weight: 600;
1241
+ text-align: center;
1242
+ box-sizing: border-box;
1243
+ pointer-events: none;
1244
+ }
1245
+
1246
+ .messages-badge.warn {
1247
+ background-color: #d68f33;
1248
+ }
1249
+
1250
+ /* Quick pulse to draw attention when a new message arrives. The animation
1251
+ is added by JS for ~2s and then removed, so it doesn't cycle forever. */
1252
+ .messages-btn.pulse .messages-badge {
1253
+ animation: messages-pulse 0.9s ease-out 2;
1254
+ }
1255
+
1256
+ @keyframes messages-pulse {
1257
+ 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(214, 51, 51, 0.55); }
1258
+ 60% { transform: scale(1.15); box-shadow: 0 0 0 6px rgba(214, 51, 51, 0); }
1259
+ 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(214, 51, 51, 0); }
1260
+ }
1261
+
1262
+ .messages-panel {
1263
+ pointer-events: none;
1264
+ }
1265
+
1266
+ .messages-panel .info-box {
1267
+ pointer-events: initial;
1268
+ width: 360px;
1269
+ max-width: 90vw;
1270
+ }
1271
+
1272
+ .messages-empty {
1273
+ color: #888;
1274
+ font-size: 13px;
1275
+ font-style: italic;
1276
+ padding: 4px 0 8px 0;
1277
+ }
1278
+
1279
+ .messages-list {
1280
+ max-height: 50vh;
1281
+ overflow: hidden;
1282
+ overflow-y: auto;
1283
+ margin: 0 -4px 6px -4px;
1284
+ }
1285
+
1286
+ .message-item {
1287
+ position: relative;
1288
+ border-left: 3px solid #888;
1289
+ background-color: #f4f7fa;
1290
+ padding: 6px 22px 6px 9px;
1291
+ margin: 4px 4px;
1292
+ border-radius: 3px;
1293
+ font-size: 13px;
1294
+ line-height: 1.35;
1295
+ }
1296
+
1297
+ .message-item.severity-warn {
1298
+ border-left-color: #d68f33;
1299
+ background-color: #fbf3e6;
1300
+ }
1301
+
1302
+ .message-item.severity-error {
1303
+ border-left-color: #d63333;
1304
+ background-color: #fbecec;
1305
+ }
1306
+
1307
+ .message-item .message-title {
1308
+ font-weight: 600;
1309
+ display: block;
1310
+ margin-bottom: 1px;
1311
+ }
1312
+
1313
+ .message-item .message-body {
1314
+ white-space: pre-wrap;
1315
+ word-wrap: break-word;
1316
+ }
1317
+
1318
+ .message-item .message-time {
1319
+ color: #888;
1320
+ font-size: 11px;
1321
+ margin-top: 2px;
1322
+ display: block;
1323
+ }
1324
+
1325
+ .message-item .message-dismiss {
1326
+ position: absolute;
1327
+ top: 3px;
1328
+ right: 5px;
1329
+ width: 16px;
1330
+ height: 16px;
1331
+ line-height: 14px;
1332
+ text-align: center;
1333
+ color: #888;
1334
+ cursor: pointer;
1335
+ font-size: 14px;
1336
+ user-select: none;
1337
+ border-radius: 8px;
1338
+ }
1339
+
1340
+ .message-item .message-dismiss:hover {
1341
+ color: #333;
1342
+ background-color: rgba(0, 0, 0, 0.05);
1343
+ }
1344
+
1345
+ .messages-footer {
1346
+ text-align: right;
1347
+ padding-top: 4px;
1348
+ border-top: 1px solid #eee;
1349
+ }
1350
+
1351
+ .messages-footer .messages-clear-btn {
1352
+ font-size: 12px;
1353
+ min-width: 0;
1354
+ padding: 3px 9px;
1355
+ }
1356
+
1200
1357
  .basemap-container {
1201
1358
  z-index: -1;
1202
1359
  position: absolute;
@@ -1,213 +0,0 @@
1
- // Jump-to-command dropdown for the command reference page.
2
- //
3
- // The build script wraps each command in <section class="cmd-section"
4
- // data-id="-clip" data-name="-clip" data-options="..."> and tags the H2
5
- // above each group as <h2 class="cmd-category">. We use that to populate a
6
- // dropdown of matching commands beneath the search input. Selecting an item
7
- // jumps to the corresponding section -- the page itself is never filtered.
8
-
9
- (function () {
10
- 'use strict';
11
-
12
- var input = document.querySelector('.cmd-search-input');
13
- var panel = document.querySelector('.cmd-search-results');
14
- if (!input || !panel) return;
15
-
16
- var statusEl = panel.querySelector('.cmd-search-status');
17
- var listEl = panel.querySelector('.cmd-search-list');
18
- var sections = Array.prototype.slice.call(
19
- document.querySelectorAll('.cmd-section')
20
- );
21
- if (!sections.length) return;
22
-
23
- // Build a flat command index. We walk the article in document order so we
24
- // can attach the most recent h2.cmd-category to each section.
25
- var article = sections[0].closest('.docs-article') || document;
26
- var commands = (function () {
27
- var out = [];
28
- var currentCat = '';
29
- var nodes = article.querySelectorAll('h2.cmd-category, section.cmd-section');
30
- for (var i = 0; i < nodes.length; i++) {
31
- var n = nodes[i];
32
- if (n.tagName === 'H2') {
33
- currentCat = (n.textContent || '').trim();
34
- } else {
35
- var name = n.dataset.name || '';
36
- var opts = n.dataset.options || '';
37
- var id = n.dataset.id || '';
38
- if (!id) continue;
39
- out.push({
40
- id: id,
41
- name: name,
42
- options: opts,
43
- category: currentCat,
44
- haystack: (name + ' ' + opts).toLowerCase(),
45
- el: n
46
- });
47
- }
48
- }
49
- return out;
50
- })();
51
-
52
- var MAX_RESULTS = 60;
53
- var activeIdx = -1;
54
- var results = [];
55
-
56
- function render() {
57
- var raw = input.value.trim();
58
- var tokens = raw.toLowerCase().split(/\s+/).filter(Boolean);
59
-
60
- if (!tokens.length) {
61
- close();
62
- return;
63
- }
64
-
65
- // Score-free matching: every token must appear in the name + options
66
- // string. We deliberately don't search the full body text -- the dropdown
67
- // is for jumping to a known command, not for free-text search.
68
- results = commands.filter(function (c) {
69
- return tokens.every(function (t) { return c.haystack.indexOf(t) !== -1; });
70
- });
71
-
72
- var truncated = results.length > MAX_RESULTS;
73
- var shown = truncated ? results.slice(0, MAX_RESULTS) : results;
74
-
75
- listEl.innerHTML = shown.map(function (c, i) {
76
- return '<li role="option" id="cmd-result-' + i + '" class="cmd-result">' +
77
- '<a href="#' + escapeAttr(c.id) + '" class="cmd-result-link" tabindex="-1">' +
78
- '<span class="cmd-result-name">' + escapeHtml(c.name) + '</span>' +
79
- (c.category
80
- ? '<span class="cmd-result-cat">' + escapeHtml(c.category) + '</span>'
81
- : '') +
82
- '</a>' +
83
- '</li>';
84
- }).join('');
85
-
86
- if (!results.length) {
87
- statusEl.textContent = 'No commands match \u201c' + raw + '\u201d.';
88
- } else if (truncated) {
89
- statusEl.textContent = 'Showing first ' + MAX_RESULTS + ' of ' +
90
- results.length + ' matches.';
91
- } else if (results.length === 1) {
92
- statusEl.textContent = '1 match (press Enter to jump).';
93
- } else {
94
- statusEl.textContent = results.length + ' matches.';
95
- }
96
-
97
- open();
98
- setActive(results.length ? 0 : -1);
99
- }
100
-
101
- function open() {
102
- panel.hidden = false;
103
- input.setAttribute('aria-expanded', 'true');
104
- }
105
-
106
- function close() {
107
- panel.hidden = true;
108
- input.setAttribute('aria-expanded', 'false');
109
- input.removeAttribute('aria-activedescendant');
110
- activeIdx = -1;
111
- results = [];
112
- listEl.innerHTML = '';
113
- statusEl.textContent = '';
114
- }
115
-
116
- function setActive(idx) {
117
- var items = listEl.querySelectorAll('.cmd-result');
118
- if (activeIdx >= 0 && items[activeIdx]) {
119
- items[activeIdx].classList.remove('is-active');
120
- }
121
- activeIdx = idx;
122
- if (idx < 0 || !items[idx]) {
123
- input.removeAttribute('aria-activedescendant');
124
- return;
125
- }
126
- items[idx].classList.add('is-active');
127
- input.setAttribute('aria-activedescendant', 'cmd-result-' + idx);
128
- // Keep the highlighted item visible inside the (scrollable) dropdown.
129
- var el = items[idx];
130
- var top = el.offsetTop;
131
- var bottom = top + el.offsetHeight;
132
- if (top < listEl.scrollTop) {
133
- listEl.scrollTop = top;
134
- } else if (bottom > listEl.scrollTop + listEl.clientHeight) {
135
- listEl.scrollTop = bottom - listEl.clientHeight;
136
- }
137
- }
138
-
139
- function jumpTo(idx) {
140
- if (idx < 0 || idx >= results.length) return;
141
- var c = results[idx];
142
- // Use the anchor's href so the browser updates location.hash and triggers
143
- // its native scroll-to-anchor behaviour (which respects scroll-margin-top).
144
- var link = listEl.querySelectorAll('.cmd-result-link')[idx];
145
- if (link) link.click();
146
- close();
147
- input.blur();
148
- }
149
-
150
- function escapeHtml(s) {
151
- return String(s).replace(/[&<>"']/g, function (c) {
152
- return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;',
153
- '"': '&quot;', "'": '&#39;' })[c];
154
- });
155
- }
156
- function escapeAttr(s) { return escapeHtml(s); }
157
-
158
- input.addEventListener('input', render);
159
- input.addEventListener('focus', function () {
160
- if (input.value.trim()) render();
161
- });
162
-
163
- input.addEventListener('keydown', function (e) {
164
- if (e.key === 'ArrowDown') {
165
- if (panel.hidden) { render(); return; }
166
- e.preventDefault();
167
- if (results.length) setActive((activeIdx + 1) % results.length);
168
- } else if (e.key === 'ArrowUp') {
169
- if (panel.hidden) return;
170
- e.preventDefault();
171
- if (results.length) {
172
- setActive(activeIdx <= 0 ? results.length - 1 : activeIdx - 1);
173
- }
174
- } else if (e.key === 'Enter') {
175
- if (panel.hidden || activeIdx < 0) return;
176
- e.preventDefault();
177
- jumpTo(activeIdx);
178
- } else if (e.key === 'Escape') {
179
- if (!panel.hidden) {
180
- e.preventDefault();
181
- close();
182
- } else if (input.value) {
183
- e.preventDefault();
184
- input.value = '';
185
- }
186
- }
187
- });
188
-
189
- // Mouse interaction with the dropdown.
190
- listEl.addEventListener('mousemove', function (e) {
191
- var li = e.target.closest('.cmd-result');
192
- if (!li) return;
193
- var idx = Array.prototype.indexOf.call(listEl.children, li);
194
- if (idx !== activeIdx) setActive(idx);
195
- });
196
-
197
- listEl.addEventListener('click', function (e) {
198
- var link = e.target.closest('.cmd-result-link');
199
- if (!link) return;
200
- // Let cmd/ctrl/middle-click open in new tab as normal; otherwise
201
- // intercept so we can close the dropdown after navigation.
202
- if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) return;
203
- // Default <a> click handles the scroll; just tidy up the UI.
204
- setTimeout(close, 0);
205
- });
206
-
207
- // Click outside the wrap closes the dropdown.
208
- document.addEventListener('click', function (e) {
209
- if (panel.hidden) return;
210
- if (e.target.closest('.cmd-search-wrap')) return;
211
- close();
212
- });
213
- })();