@softwarity/geojson-editor 1.0.6 → 1.0.8

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.
@@ -6,6 +6,7 @@ class GeoJsonEditor extends HTMLElement {
6
6
  // Internal state
7
7
  this.collapsedData = new Map(); // nodeKey -> {originalLines: string[], indent: number}
8
8
  this.colorPositions = []; // {line, color}
9
+ this.booleanPositions = []; // {line, value, attributeName}
9
10
  this.nodeTogglePositions = []; // {line, nodeKey, isCollapsed, indent}
10
11
  this.hiddenFeatures = new Set(); // Set of feature keys (hidden from events)
11
12
  this.featureRanges = new Map(); // featureKey -> {startLine, endLine, featureIndex}
@@ -22,9 +23,12 @@ class GeoJsonEditor extends HTMLElement {
22
23
  }
23
24
 
24
25
  static get observedAttributes() {
25
- return ['readonly', 'value', 'placeholder', 'dark-selector'];
26
+ return ['readonly', 'value', 'placeholder', 'dark-selector', 'default-properties'];
26
27
  }
27
28
 
29
+ // Parsed default properties rules (cache)
30
+ _defaultPropertiesRules = null;
31
+
28
32
  // Helper: Convert camelCase to kebab-case
29
33
  static _toKebabCase(str) {
30
34
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
@@ -69,10 +73,36 @@ class GeoJsonEditor extends HTMLElement {
69
73
  punctuation: /([{}[\],])/g,
70
74
  // Highlighting detection
71
75
  colorInLine: /"([\w-]+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,
76
+ booleanInLine: /"([\w-]+)"\s*:\s*(true|false)/g,
72
77
  collapsibleNode: /^(\s*)"(\w+)"\s*:\s*([{\[])/,
73
78
  collapsedMarker: /^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/
74
79
  };
75
80
 
81
+ /**
82
+ * Find collapsed data by line index, nodeKey, and indent
83
+ * @param {number} lineIndex - Current line index
84
+ * @param {string} nodeKey - Node key to find
85
+ * @param {number} indent - Indentation level to match
86
+ * @returns {{key: string, data: Object}|null} Found key and data, or null
87
+ * @private
88
+ */
89
+ _findCollapsedData(lineIndex, nodeKey, indent) {
90
+ // Try exact match first
91
+ const exactKey = `${lineIndex}-${nodeKey}`;
92
+ if (this.collapsedData.has(exactKey)) {
93
+ return { key: exactKey, data: this.collapsedData.get(exactKey) };
94
+ }
95
+
96
+ // Search for any key with this nodeKey and matching indent
97
+ for (const [key, data] of this.collapsedData.entries()) {
98
+ if (data.nodeKey === nodeKey && data.indent === indent) {
99
+ return { key, data };
100
+ }
101
+ }
102
+
103
+ return null;
104
+ }
105
+
76
106
  connectedCallback() {
77
107
  this.render();
78
108
  this.setupEventListeners();
@@ -83,6 +113,9 @@ class GeoJsonEditor extends HTMLElement {
83
113
  // Setup theme CSS
84
114
  this.updateThemeCSS();
85
115
 
116
+ // Parse default properties rules
117
+ this._parseDefaultProperties();
118
+
86
119
  // Initialize textarea with value attribute (attributeChangedCallback fires before render)
87
120
  if (this.value) {
88
121
  this.updateValue(this.value);
@@ -116,6 +149,9 @@ class GeoJsonEditor extends HTMLElement {
116
149
  this.updatePlaceholderContent();
117
150
  } else if (name === 'dark-selector') {
118
151
  this.updateThemeCSS();
152
+ } else if (name === 'default-properties') {
153
+ // Re-parse the default properties rules
154
+ this._parseDefaultProperties();
119
155
  }
120
156
  }
121
157
 
@@ -142,25 +178,130 @@ class GeoJsonEditor extends HTMLElement {
142
178
  return ']}';
143
179
  }
144
180
 
181
+ get defaultProperties() {
182
+ return this.getAttribute('default-properties') || '';
183
+ }
184
+
185
+ /**
186
+ * Parse and cache the default-properties attribute.
187
+ * Supports two formats:
188
+ * 1. Simple object: {"fill-color": "#1a465b", "stroke-width": 2}
189
+ * 2. Conditional array: [{"match": {"geometry.type": "Polygon"}, "values": {...}}, ...]
190
+ *
191
+ * Returns an array of rules: [{match: null|object, values: object}]
192
+ */
193
+ _parseDefaultProperties() {
194
+ const attr = this.defaultProperties;
195
+ if (!attr) {
196
+ this._defaultPropertiesRules = [];
197
+ return this._defaultPropertiesRules;
198
+ }
199
+
200
+ try {
201
+ const parsed = JSON.parse(attr);
202
+
203
+ if (Array.isArray(parsed)) {
204
+ // Conditional format: array of rules
205
+ this._defaultPropertiesRules = parsed.map(rule => ({
206
+ match: rule.match || null,
207
+ values: rule.values || {}
208
+ }));
209
+ } else if (typeof parsed === 'object' && parsed !== null) {
210
+ // Simple format: single object of properties for all features
211
+ this._defaultPropertiesRules = [{ match: null, values: parsed }];
212
+ } else {
213
+ this._defaultPropertiesRules = [];
214
+ }
215
+ } catch (e) {
216
+ console.warn('geojson-editor: Invalid default-properties JSON:', e.message);
217
+ this._defaultPropertiesRules = [];
218
+ }
219
+
220
+ return this._defaultPropertiesRules;
221
+ }
222
+
223
+ /**
224
+ * Check if a feature matches a condition.
225
+ * Supports dot notation for nested properties:
226
+ * - "geometry.type": "Polygon"
227
+ * - "properties.category": "airport"
228
+ */
229
+ _matchesCondition(feature, match) {
230
+ if (!match || typeof match !== 'object') return true;
231
+
232
+ for (const [path, expectedValue] of Object.entries(match)) {
233
+ const actualValue = this._getNestedValue(feature, path);
234
+ if (actualValue !== expectedValue) {
235
+ return false;
236
+ }
237
+ }
238
+ return true;
239
+ }
240
+
241
+ /**
242
+ * Get a nested value from an object using dot notation.
243
+ * E.g., _getNestedValue(feature, "geometry.type") => "Polygon"
244
+ */
245
+ _getNestedValue(obj, path) {
246
+ const parts = path.split('.');
247
+ let current = obj;
248
+ for (const part of parts) {
249
+ if (current === null || current === undefined) return undefined;
250
+ current = current[part];
251
+ }
252
+ return current;
253
+ }
254
+
255
+ /**
256
+ * Apply default properties to a single feature.
257
+ * Only adds properties that don't already exist.
258
+ * Returns a new feature object (doesn't mutate original).
259
+ */
260
+ _applyDefaultPropertiesToFeature(feature) {
261
+ if (!feature || typeof feature !== 'object') return feature;
262
+ if (!this._defaultPropertiesRules || this._defaultPropertiesRules.length === 0) return feature;
263
+
264
+ // Collect all properties to apply (later rules override earlier for same key)
265
+ const propsToApply = {};
266
+
267
+ for (const rule of this._defaultPropertiesRules) {
268
+ if (this._matchesCondition(feature, rule.match)) {
269
+ Object.assign(propsToApply, rule.values);
270
+ }
271
+ }
272
+
273
+ if (Object.keys(propsToApply).length === 0) return feature;
274
+
275
+ // Apply only properties that don't already exist
276
+ const existingProps = feature.properties || {};
277
+ const newProps = { ...existingProps };
278
+ let hasChanges = false;
279
+
280
+ for (const [key, value] of Object.entries(propsToApply)) {
281
+ if (!(key in existingProps)) {
282
+ newProps[key] = value;
283
+ hasChanges = true;
284
+ }
285
+ }
286
+
287
+ if (!hasChanges) return feature;
288
+
289
+ return { ...feature, properties: newProps };
290
+ }
291
+
145
292
  render() {
146
293
  const styles = `
147
294
  <style>
148
- /* Global reset with exact values to prevent external CSS interference */
149
- :host *,
150
- :host *::before,
151
- :host *::after {
295
+ /* Base reset - protect against inherited styles */
296
+ :host *, :host *::before, :host *::after {
152
297
  box-sizing: border-box;
153
- font-family: 'Courier New', Courier, monospace;
154
- font-size: 13px;
155
- font-weight: normal;
156
- font-style: normal;
298
+ font: normal normal 13px/1.5 'Courier New', Courier, monospace;
157
299
  font-variant: normal;
158
- line-height: 1.5;
159
300
  letter-spacing: 0;
301
+ word-spacing: 0;
160
302
  text-transform: none;
161
303
  text-decoration: none;
162
304
  text-indent: 0;
163
- word-spacing: 0;
164
305
  }
165
306
 
166
307
  :host {
@@ -176,30 +317,19 @@ class GeoJsonEditor extends HTMLElement {
176
317
  :host([readonly]) .editor-wrapper::after {
177
318
  content: '';
178
319
  position: absolute;
179
- top: 0;
180
- left: 0;
181
- right: 0;
182
- bottom: 0;
320
+ inset: 0;
183
321
  pointer-events: none;
184
- background: repeating-linear-gradient(
185
- -45deg,
186
- rgba(128, 128, 128, 0.08),
187
- rgba(128, 128, 128, 0.08) 3px,
188
- transparent 3px,
189
- transparent 12px
190
- );
322
+ background: repeating-linear-gradient(-45deg, rgba(128,128,128,0.08), rgba(128,128,128,0.08) 3px, transparent 3px, transparent 12px);
191
323
  z-index: 1;
192
324
  }
193
325
 
194
- :host([readonly]) textarea {
195
- cursor: text;
196
- }
326
+ :host([readonly]) textarea { cursor: text; }
197
327
 
198
328
  .editor-wrapper {
199
329
  position: relative;
200
330
  width: 100%;
201
331
  flex: 1;
202
- background: var(--bg-color, #ffffff);
332
+ background: var(--bg-color, #fff);
203
333
  display: flex;
204
334
  }
205
335
 
@@ -231,41 +361,62 @@ class GeoJsonEditor extends HTMLElement {
231
361
  justify-content: center;
232
362
  }
233
363
 
234
- .color-indicator {
364
+ .color-indicator, .collapse-button, .boolean-checkbox {
235
365
  width: 12px;
236
366
  height: 12px;
237
367
  border-radius: 2px;
238
- border: 1px solid #555;
239
368
  cursor: pointer;
240
369
  transition: transform 0.1s;
241
370
  flex-shrink: 0;
242
371
  }
243
372
 
373
+ .color-indicator {
374
+ border: 1px solid #555;
375
+ }
244
376
  .color-indicator:hover {
245
377
  transform: scale(1.2);
246
378
  border-color: #fff;
247
379
  }
248
380
 
381
+ .boolean-checkbox {
382
+ appearance: none;
383
+ -webkit-appearance: none;
384
+ background: transparent;
385
+ border: 1.5px solid var(--control-border, #c0c0c0);
386
+ border-radius: 2px;
387
+ margin: 0;
388
+ position: relative;
389
+ }
390
+ .boolean-checkbox:checked {
391
+ border-color: var(--control-color, #000080);
392
+ }
393
+ .boolean-checkbox:checked::after {
394
+ content: '✔';
395
+ color: var(--control-color, #000080);
396
+ font-size: 11px;
397
+ font-weight: bold;
398
+ position: absolute;
399
+ top: -3px;
400
+ right: -1px;
401
+ }
402
+ .boolean-checkbox:hover {
403
+ transform: scale(1.2);
404
+ border-color: var(--control-color, #000080);
405
+ }
406
+
249
407
  .collapse-button {
250
- width: 12px;
251
- height: 12px;
408
+ padding-top: 1px;
252
409
  background: var(--control-bg, #e8e8e8);
253
410
  border: 1px solid var(--control-border, #c0c0c0);
254
- border-radius: 2px;
255
411
  color: var(--control-color, #000080);
256
412
  font-size: 8px;
257
413
  font-weight: bold;
258
- cursor: pointer;
259
414
  display: flex;
260
415
  align-items: center;
261
416
  justify-content: center;
262
- transition: all 0.1s;
263
- flex-shrink: 0;
264
417
  user-select: none;
265
418
  }
266
-
267
419
  .collapse-button:hover {
268
- background: var(--control-bg, #e8e8e8);
269
420
  border-color: var(--control-color, #000080);
270
421
  transform: scale(1.1);
271
422
  }
@@ -274,6 +425,7 @@ class GeoJsonEditor extends HTMLElement {
274
425
  width: 14px;
275
426
  height: 14px;
276
427
  background: transparent;
428
+ color: var(--control-color, #000080);
277
429
  border: none;
278
430
  cursor: pointer;
279
431
  display: flex;
@@ -285,38 +437,10 @@ class GeoJsonEditor extends HTMLElement {
285
437
  padding: 0;
286
438
  font-size: 11px;
287
439
  }
440
+ .visibility-button:hover { opacity: 1; transform: scale(1.15); }
441
+ .visibility-button.hidden { opacity: 0.35; }
288
442
 
289
- .visibility-button:hover {
290
- opacity: 1;
291
- transform: scale(1.15);
292
- }
293
-
294
- .visibility-button.hidden {
295
- opacity: 0.35;
296
- }
297
-
298
- /* Hidden feature lines - grayed out */
299
- .line-hidden {
300
- opacity: 0.35;
301
- filter: grayscale(50%);
302
- }
303
-
304
- .color-picker-popup {
305
- position: absolute;
306
- background: #2d2d30;
307
- border: 1px solid #555;
308
- border-radius: 4px;
309
- padding: 8px;
310
- z-index: 1000;
311
- box-shadow: 0 4px 12px rgba(0,0,0,0.5);
312
- }
313
-
314
- .color-picker-popup input[type="color"] {
315
- width: 150px;
316
- height: 30px;
317
- border: none;
318
- cursor: pointer;
319
- }
443
+ .line-hidden { opacity: 0.35; filter: grayscale(50%); }
320
444
 
321
445
  .editor-content {
322
446
  position: relative;
@@ -324,156 +448,82 @@ class GeoJsonEditor extends HTMLElement {
324
448
  overflow: hidden;
325
449
  }
326
450
 
327
- .highlight-layer {
451
+ .highlight-layer, textarea, .placeholder-layer {
328
452
  position: absolute;
329
- top: 0;
330
- left: 0;
331
- width: 100%;
332
- height: 100%;
453
+ inset: 0;
333
454
  padding: 8px 12px;
334
455
  white-space: pre-wrap;
335
456
  word-wrap: break-word;
457
+ }
458
+
459
+ .highlight-layer {
336
460
  overflow: auto;
337
461
  pointer-events: none;
338
462
  z-index: 1;
339
- color: var(--text-color, #000000);
340
- }
341
-
342
- .highlight-layer::-webkit-scrollbar {
343
- display: none;
463
+ color: var(--text-color, #000);
344
464
  }
465
+ .highlight-layer::-webkit-scrollbar { display: none; }
345
466
 
346
467
  textarea {
347
- position: absolute;
348
- top: 0;
349
- left: 0;
350
- width: 100%;
351
- height: 100%;
352
- padding: 8px 12px;
353
468
  margin: 0;
354
469
  border: none;
355
470
  outline: none;
356
471
  background: transparent;
357
472
  color: transparent;
358
473
  caret-color: var(--caret-color, #000);
359
- white-space: pre-wrap;
360
- word-wrap: break-word;
361
474
  resize: none;
362
475
  overflow: auto;
363
476
  z-index: 2;
364
477
  }
365
-
366
- textarea::selection {
367
- background: rgba(51, 153, 255, 0.3);
368
- }
369
-
370
- textarea::placeholder {
371
- color: transparent;
372
- }
478
+ textarea::selection { background: rgba(51,153,255,0.3); }
479
+ textarea::placeholder { color: transparent; }
480
+ textarea:disabled { cursor: not-allowed; opacity: 0.6; }
373
481
 
374
482
  .placeholder-layer {
375
- position: absolute;
376
- top: 0;
377
- left: 0;
378
- width: 100%;
379
- height: 100%;
380
- padding: 8px 12px;
381
- white-space: pre-wrap;
382
- word-wrap: break-word;
383
483
  color: #6a6a6a;
384
484
  pointer-events: none;
385
485
  z-index: 0;
386
486
  overflow: hidden;
387
487
  }
388
488
 
389
- textarea:disabled {
390
- cursor: not-allowed;
391
- opacity: 0.6;
392
- }
393
-
394
- /* Syntax highlighting colors - IntelliJ Light defaults */
395
- .json-key {
396
- color: var(--json-key, #660e7a);
397
- }
398
-
399
- .json-string {
400
- color: var(--json-string, #008000);
401
- }
489
+ .json-key { color: var(--json-key, #660e7a); }
490
+ .json-string { color: var(--json-string, #008000); }
491
+ .json-number { color: var(--json-number, #00f); }
492
+ .json-boolean, .json-null { color: var(--json-boolean, #000080); }
493
+ .json-punctuation { color: var(--json-punct, #000); }
494
+ .json-key-invalid { color: var(--json-key-invalid, #f00); }
402
495
 
403
- .json-number {
404
- color: var(--json-number, #0000ff);
405
- }
496
+ .geojson-key { color: var(--geojson-key, #660e7a); font-weight: 600; }
497
+ .geojson-type { color: var(--geojson-type, #008000); font-weight: 600; }
498
+ .geojson-type-invalid { color: var(--geojson-type-invalid, #f00); font-weight: 600; }
406
499
 
407
- .json-boolean {
408
- color: var(--json-boolean, #000080);
409
- }
410
-
411
- .json-null {
412
- color: var(--json-null, #000080);
413
- }
414
-
415
- .json-punctuation {
416
- color: var(--json-punct, #000000);
417
- }
418
-
419
- /* GeoJSON-specific highlighting */
420
- .geojson-key {
421
- color: var(--geojson-key, #660e7a);
422
- font-weight: 600;
423
- }
424
-
425
- .geojson-type {
426
- color: var(--geojson-type, #008000);
427
- font-weight: 600;
428
- }
429
-
430
- .geojson-type-invalid {
431
- color: var(--geojson-type-invalid, #ff0000);
432
- font-weight: 600;
433
- }
434
-
435
- .json-key-invalid {
436
- color: var(--json-key-invalid, #ff0000);
437
- }
438
-
439
- /* Prefix and suffix wrapper with gutter */
440
- .prefix-wrapper,
441
- .suffix-wrapper {
500
+ .prefix-wrapper, .suffix-wrapper {
442
501
  display: flex;
443
502
  flex-shrink: 0;
444
- background: var(--bg-color, #ffffff);
503
+ background: var(--bg-color, #fff);
445
504
  }
446
505
 
447
- .prefix-gutter,
448
- .suffix-gutter {
506
+ .prefix-gutter, .suffix-gutter {
449
507
  width: 24px;
450
508
  background: var(--gutter-bg, #f0f0f0);
451
509
  border-right: 1px solid var(--gutter-border, #e0e0e0);
452
510
  flex-shrink: 0;
453
511
  }
454
512
 
455
- .editor-prefix,
456
- .editor-suffix {
513
+ .editor-prefix, .editor-suffix {
457
514
  flex: 1;
458
515
  padding: 4px 12px;
459
- color: var(--text-color, #000000);
460
- background: var(--bg-color, #ffffff);
516
+ color: var(--text-color, #000);
517
+ background: var(--bg-color, #fff);
461
518
  user-select: none;
462
519
  white-space: pre-wrap;
463
520
  word-wrap: break-word;
464
521
  opacity: 0.6;
465
522
  }
466
523
 
467
- .prefix-wrapper {
468
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
469
- }
470
-
471
- .suffix-wrapper {
472
- border-top: 1px solid rgba(255, 255, 255, 0.1);
473
- position: relative;
474
- }
524
+ .prefix-wrapper { border-bottom: 1px solid rgba(255,255,255,0.1); }
525
+ .suffix-wrapper { border-top: 1px solid rgba(255,255,255,0.1); position: relative; }
475
526
 
476
- /* Clear button in suffix area */
477
527
  .clear-btn {
478
528
  position: absolute;
479
529
  right: 0.5rem;
@@ -481,7 +531,7 @@ class GeoJsonEditor extends HTMLElement {
481
531
  transform: translateY(-50%);
482
532
  background: transparent;
483
533
  border: none;
484
- color: var(--text-color, #000000);
534
+ color: var(--text-color, #000);
485
535
  opacity: 0.3;
486
536
  cursor: pointer;
487
537
  font-size: 0.65rem;
@@ -492,41 +542,16 @@ class GeoJsonEditor extends HTMLElement {
492
542
  display: flex;
493
543
  align-items: center;
494
544
  justify-content: center;
495
- box-sizing: border-box;
496
545
  transition: opacity 0.2s, background 0.2s;
497
546
  }
498
- .clear-btn:hover {
499
- opacity: 0.7;
500
- background: rgba(255, 255, 255, 0.1);
501
- }
502
- .clear-btn[hidden] {
503
- display: none;
504
- }
505
-
506
- /* Scrollbar styling - WebKit (Chrome, Safari, Edge) */
507
- textarea::-webkit-scrollbar {
508
- width: 10px;
509
- height: 10px;
510
- }
511
-
512
- textarea::-webkit-scrollbar-track {
513
- background: var(--control-bg, #e8e8e8);
514
- }
515
-
516
- textarea::-webkit-scrollbar-thumb {
517
- background: var(--control-border, #c0c0c0);
518
- border-radius: 5px;
519
- }
520
-
521
- textarea::-webkit-scrollbar-thumb:hover {
522
- background: var(--control-color, #000080);
523
- }
547
+ .clear-btn:hover { opacity: 0.7; background: rgba(255,255,255,0.1); }
548
+ .clear-btn[hidden] { display: none; }
524
549
 
525
- /* Scrollbar styling - Firefox */
526
- textarea {
527
- scrollbar-width: thin;
528
- scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8);
529
- }
550
+ textarea::-webkit-scrollbar { width: 10px; height: 10px; }
551
+ textarea::-webkit-scrollbar-track { background: var(--control-bg, #e8e8e8); }
552
+ textarea::-webkit-scrollbar-thumb { background: var(--control-border, #c0c0c0); border-radius: 5px; }
553
+ textarea::-webkit-scrollbar-thumb:hover { background: var(--control-color, #000080); }
554
+ textarea { scrollbar-width: thin; scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8); }
530
555
  </style>
531
556
  `;
532
557
 
@@ -603,7 +628,7 @@ class GeoJsonEditor extends HTMLElement {
603
628
  }, 10);
604
629
  });
605
630
 
606
- // Gutter clicks (color indicators and collapse buttons)
631
+ // Gutter clicks (color indicators, boolean checkboxes, and collapse buttons)
607
632
  const gutterContent = this.shadowRoot.getElementById('gutterContent');
608
633
  gutterContent.addEventListener('click', (e) => {
609
634
  // Check for visibility button (may click on SVG inside button)
@@ -619,6 +644,11 @@ class GeoJsonEditor extends HTMLElement {
619
644
  const color = e.target.dataset.color;
620
645
  const attributeName = e.target.dataset.attributeName;
621
646
  this.showColorPicker(e.target, line, color, attributeName);
647
+ } else if (e.target.classList.contains('boolean-checkbox')) {
648
+ const line = parseInt(e.target.dataset.line);
649
+ const attributeName = e.target.dataset.attributeName;
650
+ const newValue = e.target.checked;
651
+ this.updateBooleanValue(line, newValue, attributeName);
622
652
  } else if (e.target.classList.contains('collapse-button')) {
623
653
  const nodeKey = e.target.dataset.nodeKey;
624
654
  const line = parseInt(e.target.dataset.line);
@@ -708,32 +738,18 @@ class GeoJsonEditor extends HTMLElement {
708
738
  // Auto-format JSON content
709
739
  if (newValue) {
710
740
  try {
711
- const prefix = this.prefix;
712
- const suffix = this.suffix;
713
-
714
- // Check if prefix ends with [ and suffix starts with ]
715
- const prefixEndsWithBracket = prefix.trimEnd().endsWith('[');
716
- const suffixStartsWithBracket = suffix.trimStart().startsWith(']');
717
-
718
- if (prefixEndsWithBracket && suffixStartsWithBracket) {
719
- // Wrap content in array brackets for validation and formatting
720
- const wrapped = '[' + newValue + ']';
721
- const parsed = JSON.parse(wrapped);
722
- const formatted = JSON.stringify(parsed, null, 2);
723
-
724
- // Remove first [ and last ] from formatted
725
- const lines = formatted.split('\n');
726
- if (lines.length > 2) {
727
- textarea.value = lines.slice(1, -1).join('\n');
728
- } else {
729
- textarea.value = '';
730
- }
731
- } else if (!prefix && !suffix) {
732
- // No prefix/suffix - format directly
733
- const parsed = JSON.parse(newValue);
734
- textarea.value = JSON.stringify(parsed, null, 2);
741
+ // Wrap content in array brackets for validation and formatting
742
+ const wrapped = '[' + newValue + ']';
743
+ const parsed = JSON.parse(wrapped);
744
+ const formatted = JSON.stringify(parsed, null, 2);
745
+
746
+ // Remove first [ and last ] from formatted
747
+ const lines = formatted.split('\n');
748
+ if (lines.length > 2) {
749
+ textarea.value = lines.slice(1, -1).join('\n');
750
+ } else {
751
+ textarea.value = '';
735
752
  }
736
- // else: keep as-is for complex cases
737
753
  } catch (e) {
738
754
  // Invalid JSON, keep as-is
739
755
  }
@@ -783,10 +799,11 @@ class GeoJsonEditor extends HTMLElement {
783
799
  const hiddenRanges = this.getHiddenLineRanges();
784
800
 
785
801
  // Parse and highlight
786
- const { highlighted, colors, toggles } = this.highlightJSON(text, hiddenRanges);
802
+ const { highlighted, colors, booleans, toggles } = this.highlightJSON(text, hiddenRanges);
787
803
 
788
804
  highlightLayer.innerHTML = highlighted;
789
805
  this.colorPositions = colors;
806
+ this.booleanPositions = booleans;
790
807
  this.nodeTogglePositions = toggles;
791
808
 
792
809
  // Update gutter with color indicators
@@ -795,11 +812,12 @@ class GeoJsonEditor extends HTMLElement {
795
812
 
796
813
  highlightJSON(text, hiddenRanges = []) {
797
814
  if (!text.trim()) {
798
- return { highlighted: '', colors: [], toggles: [] };
815
+ return { highlighted: '', colors: [], booleans: [], toggles: [] };
799
816
  }
800
817
 
801
818
  const lines = text.split('\n');
802
819
  const colors = [];
820
+ const booleans = [];
803
821
  const toggles = [];
804
822
  let highlightedLines = [];
805
823
 
@@ -824,6 +842,17 @@ class GeoJsonEditor extends HTMLElement {
824
842
  });
825
843
  }
826
844
 
845
+ // Detect boolean values in properties
846
+ R.booleanInLine.lastIndex = 0; // Reset for global regex
847
+ let booleanMatch;
848
+ while ((booleanMatch = R.booleanInLine.exec(line)) !== null) {
849
+ booleans.push({
850
+ line: lineIndex,
851
+ value: booleanMatch[2] === 'true', // The boolean value
852
+ attributeName: booleanMatch[1] // The attribute name
853
+ });
854
+ }
855
+
827
856
  // Detect collapsible nodes (all nodes are collapsible)
828
857
  const nodeMatch = line.match(R.collapsibleNode);
829
858
  if (nodeMatch) {
@@ -866,28 +895,21 @@ class GeoJsonEditor extends HTMLElement {
866
895
  return {
867
896
  highlighted: highlightedLines.join('\n'),
868
897
  colors,
898
+ booleans,
869
899
  toggles
870
900
  };
871
901
  }
872
902
 
873
- // GeoJSON type constants
874
- static GEOJSON_TYPES_FEATURE = ['Feature', 'FeatureCollection'];
875
- static GEOJSON_TYPES_GEOMETRY = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection'];
876
- static GEOJSON_TYPES_ALL = [...GeoJsonEditor.GEOJSON_TYPES_FEATURE, ...GeoJsonEditor.GEOJSON_TYPES_GEOMETRY];
903
+ // GeoJSON type constants (consolidated)
904
+ static GEOJSON = {
905
+ GEOMETRY_TYPES: ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon'],
906
+ };
877
907
 
878
908
  // Valid keys per context (null = any key is valid)
879
909
  static VALID_KEYS_BY_CONTEXT = {
880
- Feature: ['type', 'geometry', 'properties', 'id', 'bbox'],
881
- FeatureCollection: ['type', 'features', 'bbox', 'properties'],
882
- Point: ['type', 'coordinates', 'bbox'],
883
- MultiPoint: ['type', 'coordinates', 'bbox'],
884
- LineString: ['type', 'coordinates', 'bbox'],
885
- MultiLineString: ['type', 'coordinates', 'bbox'],
886
- Polygon: ['type', 'coordinates', 'bbox'],
887
- MultiPolygon: ['type', 'coordinates', 'bbox'],
888
- GeometryCollection: ['type', 'geometries', 'bbox'],
910
+ Feature: ['type', 'geometry', 'properties', 'id'],
889
911
  properties: null, // Any key valid in properties
890
- geometry: ['type', 'coordinates', 'geometries', 'bbox'], // Generic geometry context
912
+ geometry: ['type', 'coordinates'], // Generic geometry context
891
913
  };
892
914
 
893
915
  // Keys that change context for their value
@@ -895,7 +917,6 @@ class GeoJsonEditor extends HTMLElement {
895
917
  geometry: 'geometry',
896
918
  properties: 'properties',
897
919
  features: 'Feature', // Array of Features
898
- geometries: 'geometry', // Array of geometries
899
920
  };
900
921
 
901
922
  // Build context map for each line by analyzing JSON structure
@@ -954,7 +975,8 @@ class GeoJsonEditor extends HTMLElement {
954
975
  const typeMatch = line.substring(0, j).match(/"type"\s*:\s*$/);
955
976
  if (typeMatch) {
956
977
  const valueMatch = line.substring(j).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);
957
- if (valueMatch && GeoJsonEditor.GEOJSON_TYPES_ALL.includes(valueMatch[1])) {
978
+ const validTypes = ['Feature', ...GeoJsonEditor.GEOJSON.GEOMETRY_TYPES];
979
+ if (valueMatch && validTypes.includes(valueMatch[1])) {
958
980
  const currentCtx = contextStack[contextStack.length - 1];
959
981
  if (currentCtx) {
960
982
  currentCtx.context = valueMatch[1];
@@ -1005,7 +1027,8 @@ class GeoJsonEditor extends HTMLElement {
1005
1027
  }
1006
1028
 
1007
1029
  // All known GeoJSON structural keys (always valid in GeoJSON)
1008
- static GEOJSON_STRUCTURAL_KEYS = ['type', 'geometry', 'properties', 'features', 'geometries', 'coordinates', 'bbox', 'id', 'crs'];
1030
+ // GeoJSON structural keys that are always valid (not user properties)
1031
+ static GEOJSON_STRUCTURAL_KEYS = ['type', 'geometry', 'properties', 'coordinates', 'id'];
1009
1032
 
1010
1033
  highlightSyntax(text, context) {
1011
1034
  if (!text.trim()) return '';
@@ -1024,15 +1047,15 @@ class GeoJsonEditor extends HTMLElement {
1024
1047
 
1025
1048
  // Helper to check if a type value is valid in current context
1026
1049
  const isTypeValid = (typeValue) => {
1027
- // Unknown context - don't validate (could be inside misspelled properties, etc.)
1050
+ // Unknown context - don't validate
1028
1051
  if (!context) return true;
1029
1052
  if (context === 'properties') return true; // Any type in properties
1030
- if (context === 'geometry' || GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.includes(context)) {
1031
- return GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.includes(typeValue);
1053
+ if (context === 'geometry' || GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(context)) {
1054
+ return GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(typeValue);
1032
1055
  }
1033
- // Only validate as GeoJSON type in known Feature/FeatureCollection context
1034
- if (context === 'Feature' || context === 'FeatureCollection') {
1035
- return GeoJsonEditor.GEOJSON_TYPES_ALL.includes(typeValue);
1056
+ // In Feature context: accept Feature or geometry types
1057
+ if (context === 'Feature') {
1058
+ return typeValue === 'Feature' || GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(typeValue);
1036
1059
  }
1037
1060
  return true; // Unknown context - accept any type
1038
1061
  };
@@ -1091,35 +1114,16 @@ class GeoJsonEditor extends HTMLElement {
1091
1114
  const hasMarker = currentLine.includes('{...}') || currentLine.includes('[...]');
1092
1115
 
1093
1116
  if (hasMarker) {
1094
- // Expand: find the correct collapsed data by searching for this nodeKey
1095
- let foundKey = null;
1096
- let foundData = null;
1097
-
1098
- // Try exact match first
1099
- const exactKey = `${line}-${nodeKey}`;
1100
- if (this.collapsedData.has(exactKey)) {
1101
- foundKey = exactKey;
1102
- foundData = this.collapsedData.get(exactKey);
1103
- } else {
1104
- // Search for any key with this nodeKey (line numbers may have shifted)
1105
- for (const [key, data] of this.collapsedData.entries()) {
1106
- if (data.nodeKey === nodeKey) {
1107
- // Check indent to distinguish between multiple nodes with same name
1108
- const currentIndent = currentLine.match(/^(\s*)/)[1].length;
1109
- if (data.indent === currentIndent) {
1110
- foundKey = key;
1111
- foundData = data;
1112
- break;
1113
- }
1114
- }
1115
- }
1116
- }
1117
+ // Expand: find the correct collapsed data
1118
+ const currentIndent = currentLine.match(/^(\s*)/)[1].length;
1119
+ const found = this._findCollapsedData(line, nodeKey, currentIndent);
1117
1120
 
1118
- if (!foundKey || !foundData) {
1121
+ if (!found) {
1119
1122
  return;
1120
1123
  }
1121
1124
 
1122
- const {originalLine, content} = foundData;
1125
+ const { key: foundKey, data: foundData } = found;
1126
+ const { originalLine, content } = foundData;
1123
1127
 
1124
1128
  // Restore original line and content
1125
1129
  lines[line] = originalLine;
@@ -1193,13 +1197,13 @@ class GeoJsonEditor extends HTMLElement {
1193
1197
  // Clear gutter
1194
1198
  gutterContent.textContent = '';
1195
1199
 
1196
- // Create a map of line -> elements (color, collapse button, visibility button)
1200
+ // Create a map of line -> elements (color, boolean, collapse button, visibility button)
1197
1201
  const lineElements = new Map();
1198
1202
 
1199
1203
  // Helper to ensure line entry exists
1200
1204
  const ensureLine = (line) => {
1201
1205
  if (!lineElements.has(line)) {
1202
- lineElements.set(line, { colors: [], buttons: [], visibilityButtons: [] });
1206
+ lineElements.set(line, { colors: [], booleans: [], buttons: [], visibilityButtons: [] });
1203
1207
  }
1204
1208
  return lineElements.get(line);
1205
1209
  };
@@ -1209,6 +1213,11 @@ class GeoJsonEditor extends HTMLElement {
1209
1213
  ensureLine(line).colors.push({ color, attributeName });
1210
1214
  });
1211
1215
 
1216
+ // Add boolean checkboxes
1217
+ this.booleanPositions.forEach(({ line, value, attributeName }) => {
1218
+ ensureLine(line).booleans.push({ value, attributeName });
1219
+ });
1220
+
1212
1221
  // Add collapse buttons
1213
1222
  this.nodeTogglePositions.forEach(({ line, nodeKey, isCollapsed }) => {
1214
1223
  ensureLine(line).buttons.push({ nodeKey, isCollapsed });
@@ -1250,6 +1259,18 @@ class GeoJsonEditor extends HTMLElement {
1250
1259
  gutterLine.appendChild(indicator);
1251
1260
  });
1252
1261
 
1262
+ // Add boolean checkboxes
1263
+ elements.booleans.forEach(({ value, attributeName }) => {
1264
+ const checkbox = document.createElement('input');
1265
+ checkbox.type = 'checkbox';
1266
+ checkbox.className = 'boolean-checkbox';
1267
+ checkbox.checked = value;
1268
+ checkbox.dataset.line = line;
1269
+ checkbox.dataset.attributeName = attributeName;
1270
+ checkbox.title = `${attributeName}: ${value}`;
1271
+ gutterLine.appendChild(checkbox);
1272
+ });
1273
+
1253
1274
  // Add collapse buttons
1254
1275
  elements.buttons.forEach(({ nodeKey, isCollapsed }) => {
1255
1276
  const button = document.createElement('div');
@@ -1345,6 +1366,19 @@ class GeoJsonEditor extends HTMLElement {
1345
1366
  this.emitChange();
1346
1367
  }
1347
1368
 
1369
+ updateBooleanValue(line, newValue, attributeName) {
1370
+ const textarea = this.shadowRoot.getElementById('textarea');
1371
+ const lines = textarea.value.split('\n');
1372
+
1373
+ // Replace boolean value on the specified line for the specific attribute
1374
+ const regex = new RegExp(`"${attributeName}"\\s*:\\s*(true|false)`);
1375
+ lines[line] = lines[line].replace(regex, `"${attributeName}": ${newValue}`);
1376
+
1377
+ textarea.value = lines.join('\n');
1378
+ this.updateHighlight();
1379
+ this.emitChange();
1380
+ }
1381
+
1348
1382
  handleKeydownInCollapsedArea(e) {
1349
1383
  // Allow navigation keys
1350
1384
  const navigationKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Tab'];
@@ -1411,44 +1445,29 @@ class GeoJsonEditor extends HTMLElement {
1411
1445
  if (line.includes('{...}') || line.includes('[...]')) {
1412
1446
  const match = line.match(R.collapsedMarker);
1413
1447
  if (match) {
1414
- const nodeKey = match[2]; // Extract nodeKey from the marker
1415
- const exactKey = `${absoluteLineNum}-${nodeKey}`;
1416
-
1417
- // Try exact key match first
1418
- if (this.collapsedData.has(exactKey)) {
1419
- const collapsed = this.collapsedData.get(exactKey);
1420
- expandedLines.push(collapsed.originalLine);
1421
- expandedLines.push(...collapsed.content);
1448
+ const nodeKey = match[2];
1449
+ const currentIndent = match[1].length;
1450
+
1451
+ // Try to find collapsed data using helper
1452
+ const found = this._findCollapsedData(absoluteLineNum, nodeKey, currentIndent);
1453
+ if (found) {
1454
+ expandedLines.push(found.data.originalLine);
1455
+ expandedLines.push(...found.data.content);
1422
1456
  return;
1423
1457
  }
1424
1458
 
1425
- // Fallback: search by line number and nodeKey
1426
- let found = false;
1427
- for (const [key, collapsed] of this.collapsedData.entries()) {
1428
- if (key.endsWith(`-${nodeKey}`)) {
1459
+ // Fallback: search by nodeKey only (line numbers may have shifted)
1460
+ for (const [, collapsed] of this.collapsedData.entries()) {
1461
+ if (collapsed.nodeKey === nodeKey) {
1429
1462
  expandedLines.push(collapsed.originalLine);
1430
1463
  expandedLines.push(...collapsed.content);
1431
- found = true;
1432
- break;
1464
+ return;
1433
1465
  }
1434
1466
  }
1435
- if (found) return;
1436
1467
  }
1437
1468
 
1438
- // Fallback: search by line number only
1439
- let found = false;
1440
- for (const [key, collapsed] of this.collapsedData.entries()) {
1441
- const collapsedLineNum = parseInt(key.split('-')[0]);
1442
- if (collapsedLineNum === absoluteLineNum) {
1443
- expandedLines.push(collapsed.originalLine);
1444
- expandedLines.push(...collapsed.content);
1445
- found = true;
1446
- break;
1447
- }
1448
- }
1449
- if (!found) {
1450
- expandedLines.push(line);
1451
- }
1469
+ // Line not found in collapsed data, keep as-is
1470
+ expandedLines.push(line);
1452
1471
  } else {
1453
1472
  expandedLines.push(line);
1454
1473
  }
@@ -1482,10 +1501,8 @@ class GeoJsonEditor extends HTMLElement {
1482
1501
  // Expand ALL collapsed nodes to get full content
1483
1502
  const editorContent = this.expandAllCollapsed(textarea.value);
1484
1503
 
1485
- // Build complete value with prefix/suffix
1486
- const prefix = this.prefix;
1487
- const suffix = this.suffix;
1488
- const fullValue = prefix + editorContent + suffix;
1504
+ // Build complete value with prefix/suffix (fixed FeatureCollection wrapper)
1505
+ const fullValue = this.prefix + editorContent + this.suffix;
1489
1506
 
1490
1507
  // Try to parse
1491
1508
  try {
@@ -1494,8 +1511,11 @@ class GeoJsonEditor extends HTMLElement {
1494
1511
  // Filter out hidden features before emitting
1495
1512
  parsed = this.filterHiddenFeatures(parsed);
1496
1513
 
1497
- // Validate GeoJSON types
1498
- const validationErrors = this.validateGeoJSON(parsed);
1514
+ // Validate GeoJSON types (validate only features, not the wrapper)
1515
+ let validationErrors = [];
1516
+ parsed.features.forEach((feature, index) => {
1517
+ validationErrors.push(...this.validateGeoJSON(feature, `features[${index}]`, 'root'));
1518
+ });
1499
1519
 
1500
1520
  if (validationErrors.length > 0) {
1501
1521
  // Emit error event for GeoJSON validation errors
@@ -1535,23 +1555,12 @@ class GeoJsonEditor extends HTMLElement {
1535
1555
  filterHiddenFeatures(parsed) {
1536
1556
  if (!parsed || this.hiddenFeatures.size === 0) return parsed;
1537
1557
 
1538
- if (parsed.type === 'FeatureCollection' && Array.isArray(parsed.features)) {
1539
- // Filter features array
1540
- const visibleFeatures = parsed.features.filter(feature => {
1541
- const key = this.getFeatureKey(feature);
1542
- return !this.hiddenFeatures.has(key);
1543
- });
1544
- return { ...parsed, features: visibleFeatures };
1545
- } else if (parsed.type === 'Feature') {
1546
- // Single feature - check if hidden
1547
- const key = this.getFeatureKey(parsed);
1548
- if (this.hiddenFeatures.has(key)) {
1549
- // Return empty FeatureCollection when single feature is hidden
1550
- return { type: 'FeatureCollection', features: [] };
1551
- }
1552
- }
1553
-
1554
- return parsed;
1558
+ // parsed is always a FeatureCollection (from wrapper)
1559
+ const visibleFeatures = parsed.features.filter((feature) => {
1560
+ const key = this.getFeatureKey(feature);
1561
+ return !this.hiddenFeatures.has(key);
1562
+ });
1563
+ return { ...parsed, features: visibleFeatures };
1555
1564
  }
1556
1565
 
1557
1566
  // ========== Feature Visibility Management ==========
@@ -1566,9 +1575,9 @@ class GeoJsonEditor extends HTMLElement {
1566
1575
  // 2. Use properties.id if present
1567
1576
  if (feature.properties?.id !== undefined) return `prop:${feature.properties.id}`;
1568
1577
 
1569
- // 3. Fallback: hash based on geometry type + first coordinates
1578
+ // 3. Fallback: hash based on geometry type + ALL coordinates
1570
1579
  const geomType = feature.geometry?.type || 'null';
1571
- const coords = JSON.stringify(feature.geometry?.coordinates || []).slice(0, 100);
1580
+ const coords = JSON.stringify(feature.geometry?.coordinates || []);
1572
1581
  return `hash:${geomType}:${this.simpleHash(coords)}`;
1573
1582
  }
1574
1583
 
@@ -1613,12 +1622,8 @@ class GeoJsonEditor extends HTMLElement {
1613
1622
  const fullValue = prefix + expandedText + suffix;
1614
1623
  const parsed = JSON.parse(fullValue);
1615
1624
 
1616
- let features = [];
1617
- if (parsed.type === 'FeatureCollection' && Array.isArray(parsed.features)) {
1618
- features = parsed.features;
1619
- } else if (parsed.type === 'Feature') {
1620
- features = [parsed];
1621
- }
1625
+ // parsed is always a FeatureCollection (from wrapper)
1626
+ const features = parsed.features;
1622
1627
 
1623
1628
  // Now find each feature's line range in the text
1624
1629
  const lines = text.split('\n');
@@ -1636,10 +1641,18 @@ class GeoJsonEditor extends HTMLElement {
1636
1641
  const isFeatureTypeLine = /"type"\s*:\s*"Feature"/.test(line);
1637
1642
  if (!inFeature && isFeatureTypeLine) {
1638
1643
  // Find the opening brace for this Feature
1639
- // Look backwards for the opening brace
1644
+ // Look backwards for a line that starts with just '{' (the Feature's opening brace)
1645
+ // Not a line like '"geometry": {' which contains other content before the brace
1640
1646
  let startLine = i;
1641
1647
  for (let j = i; j >= 0; j--) {
1642
- if (lines[j].includes('{')) {
1648
+ const trimmed = lines[j].trim();
1649
+ // Line is just '{' or '{' followed by nothing significant (opening brace only)
1650
+ if (trimmed === '{' || trimmed === '{,') {
1651
+ startLine = j;
1652
+ break;
1653
+ }
1654
+ // Also handle case where Feature starts on same line: { "type": "Feature"
1655
+ if (trimmed.startsWith('{') && !trimmed.includes(':')) {
1643
1656
  startLine = j;
1644
1657
  break;
1645
1658
  }
@@ -1718,13 +1731,13 @@ class GeoJsonEditor extends HTMLElement {
1718
1731
  if (typeof typeValue === 'string') {
1719
1732
  if (context === 'geometry') {
1720
1733
  // In geometry: must be a geometry type
1721
- if (!GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.includes(typeValue)) {
1722
- errors.push(`Invalid geometry type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.join(', ')})`);
1734
+ if (!GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(typeValue)) {
1735
+ errors.push(`Invalid geometry type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.join(', ')})`);
1723
1736
  }
1724
1737
  } else {
1725
- // At root or in features: must be Feature or FeatureCollection
1726
- if (!GeoJsonEditor.GEOJSON_TYPES_FEATURE.includes(typeValue)) {
1727
- errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON_TYPES_FEATURE.join(', ')})`);
1738
+ // At root or in features: must be Feature
1739
+ if (typeValue !== 'Feature') {
1740
+ errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected: Feature)`);
1728
1741
  }
1729
1742
  }
1730
1743
  }
@@ -1900,20 +1913,10 @@ class GeoJsonEditor extends HTMLElement {
1900
1913
 
1901
1914
  const nodeKey = match[2];
1902
1915
  const currentIndent = match[1].length;
1903
- const exactKey = `${i}-${nodeKey}`;
1904
-
1905
- let foundKey = this.collapsedData.has(exactKey) ? exactKey : null;
1906
- if (!foundKey) {
1907
- for (const [key, data] of this.collapsedData.entries()) {
1908
- if (data.nodeKey === nodeKey && data.indent === currentIndent) {
1909
- foundKey = key;
1910
- break;
1911
- }
1912
- }
1913
- }
1916
+ const found = this._findCollapsedData(i, nodeKey, currentIndent);
1914
1917
 
1915
- if (foundKey) {
1916
- const {originalLine, content: nodeContent} = this.collapsedData.get(foundKey);
1918
+ if (found) {
1919
+ const { data: { originalLine, content: nodeContent } } = found;
1917
1920
  lines[i] = originalLine;
1918
1921
  lines.splice(i + 1, 0, ...nodeContent);
1919
1922
  expanded = true;
@@ -1927,27 +1930,20 @@ class GeoJsonEditor extends HTMLElement {
1927
1930
  return content;
1928
1931
  }
1929
1932
 
1930
- // Helper: Format JSON content respecting prefix/suffix
1933
+ // Helper: Format JSON content (always in FeatureCollection mode)
1934
+ // Also applies default properties to features if configured
1931
1935
  formatJSONContent(content) {
1932
- const prefix = this.prefix;
1933
- const suffix = this.suffix;
1934
- const prefixEndsWithBracket = prefix.trimEnd().endsWith('[');
1935
- const suffixStartsWithBracket = suffix.trimStart().startsWith(']');
1936
-
1937
- if (prefixEndsWithBracket && suffixStartsWithBracket) {
1938
- const wrapped = '[' + content + ']';
1939
- const parsed = JSON.parse(wrapped);
1940
- const formatted = JSON.stringify(parsed, null, 2);
1941
- const lines = formatted.split('\n');
1942
- return lines.length > 2 ? lines.slice(1, -1).join('\n') : '';
1943
- } else if (!prefix && !suffix) {
1944
- const parsed = JSON.parse(content);
1945
- return JSON.stringify(parsed, null, 2);
1946
- } else {
1947
- const fullValue = prefix + content + suffix;
1948
- JSON.parse(fullValue); // Validate only
1949
- return content;
1936
+ const wrapped = '[' + content + ']';
1937
+ let parsed = JSON.parse(wrapped);
1938
+
1939
+ // Apply default properties to each feature in the array
1940
+ if (Array.isArray(parsed)) {
1941
+ parsed = parsed.map(f => this._applyDefaultPropertiesToFeature(f));
1950
1942
  }
1943
+
1944
+ const formatted = JSON.stringify(parsed, null, 2);
1945
+ const lines = formatted.split('\n');
1946
+ return lines.length > 2 ? lines.slice(1, -1).join('\n') : '';
1951
1947
  }
1952
1948
 
1953
1949
  autoFormatContentWithCursor() {
@@ -2232,19 +2228,14 @@ class GeoJsonEditor extends HTMLElement {
2232
2228
  // Check geometry has valid type
2233
2229
  if (!('type' in feature.geometry)) {
2234
2230
  errors.push('Geometry must have a "type" property');
2235
- } else if (!GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.includes(feature.geometry.type)) {
2236
- errors.push(`Invalid geometry type "${feature.geometry.type}" (expected: ${GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.join(', ')})`);
2231
+ } else if (!GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(feature.geometry.type)) {
2232
+ errors.push(`Invalid geometry type "${feature.geometry.type}" (expected: ${GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.join(', ')})`);
2237
2233
  }
2238
2234
 
2239
- // Check geometry has coordinates (except GeometryCollection)
2240
- if (feature.geometry.type !== 'GeometryCollection' && !('coordinates' in feature.geometry)) {
2235
+ // Check geometry has coordinates
2236
+ if (!('coordinates' in feature.geometry)) {
2241
2237
  errors.push('Geometry must have a "coordinates" property');
2242
2238
  }
2243
-
2244
- // GeometryCollection must have geometries array
2245
- if (feature.geometry.type === 'GeometryCollection' && !Array.isArray(feature.geometry.geometries)) {
2246
- errors.push('GeometryCollection must have a "geometries" array');
2247
- }
2248
2239
  }
2249
2240
  }
2250
2241
 
@@ -2281,7 +2272,9 @@ class GeoJsonEditor extends HTMLElement {
2281
2272
  throw new Error(`Invalid features: ${allErrors.join('; ')}`);
2282
2273
  }
2283
2274
 
2284
- this._setFeatures(features);
2275
+ // Apply default properties to each feature
2276
+ const featuresWithDefaults = features.map(f => this._applyDefaultPropertiesToFeature(f));
2277
+ this._setFeatures(featuresWithDefaults);
2285
2278
  }
2286
2279
 
2287
2280
  /**
@@ -2296,7 +2289,8 @@ class GeoJsonEditor extends HTMLElement {
2296
2289
  }
2297
2290
 
2298
2291
  const features = this._parseFeatures();
2299
- features.push(feature);
2292
+ // Apply default properties before adding
2293
+ features.push(this._applyDefaultPropertiesToFeature(feature));
2300
2294
  this._setFeatures(features);
2301
2295
  }
2302
2296
 
@@ -2315,7 +2309,8 @@ class GeoJsonEditor extends HTMLElement {
2315
2309
  const features = this._parseFeatures();
2316
2310
  const idx = this._normalizeIndex(index, features.length, true);
2317
2311
 
2318
- features.splice(idx, 0, feature);
2312
+ // Apply default properties before inserting
2313
+ features.splice(idx, 0, this._applyDefaultPropertiesToFeature(feature));
2319
2314
  this._setFeatures(features);
2320
2315
  }
2321
2316