@softwarity/geojson-editor 1.0.5 → 1.0.6
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/README.md +67 -37
- package/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.js +678 -433
package/src/geojson-editor.js
CHANGED
|
@@ -17,68 +17,40 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
17
17
|
this._cachedLineHeight = null;
|
|
18
18
|
this._cachedPaddingTop = null;
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
this.themes = {
|
|
22
|
-
dark: { ...GeoJsonEditor.DEFAULT_THEMES.dark },
|
|
23
|
-
light: { ...GeoJsonEditor.DEFAULT_THEMES.light }
|
|
24
|
-
};
|
|
20
|
+
// Custom theme overrides (empty by default, CSS has defaults)
|
|
21
|
+
this.themes = { dark: {}, light: {} };
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
static get observedAttributes() {
|
|
28
|
-
return ['readonly', 'value', 'placeholder', 'dark-selector'
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Default theme values
|
|
33
|
-
static DEFAULT_THEMES = {
|
|
34
|
-
dark: {
|
|
35
|
-
background: '#1e1e1e',
|
|
36
|
-
textColor: '#d4d4d4',
|
|
37
|
-
caretColor: '#fff',
|
|
38
|
-
gutterBackground: '#252526',
|
|
39
|
-
gutterBorder: '#3e3e42',
|
|
40
|
-
jsonKey: '#9cdcfe',
|
|
41
|
-
jsonString: '#ce9178',
|
|
42
|
-
jsonNumber: '#b5cea8',
|
|
43
|
-
jsonBoolean: '#569cd6',
|
|
44
|
-
jsonNull: '#569cd6',
|
|
45
|
-
jsonPunctuation: '#d4d4d4',
|
|
46
|
-
controlColor: '#c586c0',
|
|
47
|
-
controlBg: '#3e3e42',
|
|
48
|
-
controlBorder: '#555',
|
|
49
|
-
geojsonKey: '#c586c0',
|
|
50
|
-
geojsonType: '#4ec9b0',
|
|
51
|
-
geojsonTypeInvalid: '#f44747',
|
|
52
|
-
jsonKeyInvalid: '#f44747'
|
|
53
|
-
},
|
|
54
|
-
light: {
|
|
55
|
-
background: '#ffffff',
|
|
56
|
-
textColor: '#333333',
|
|
57
|
-
caretColor: '#000',
|
|
58
|
-
gutterBackground: '#f5f5f5',
|
|
59
|
-
gutterBorder: '#ddd',
|
|
60
|
-
jsonKey: '#0000ff',
|
|
61
|
-
jsonString: '#a31515',
|
|
62
|
-
jsonNumber: '#098658',
|
|
63
|
-
jsonBoolean: '#0000ff',
|
|
64
|
-
jsonNull: '#0000ff',
|
|
65
|
-
jsonPunctuation: '#333333',
|
|
66
|
-
controlColor: '#a31515',
|
|
67
|
-
controlBg: '#e0e0e0',
|
|
68
|
-
controlBorder: '#999',
|
|
69
|
-
geojsonKey: '#af00db',
|
|
70
|
-
geojsonType: '#267f99',
|
|
71
|
-
geojsonTypeInvalid: '#d32f2f',
|
|
72
|
-
jsonKeyInvalid: '#d32f2f'
|
|
73
|
-
}
|
|
74
|
-
};
|
|
25
|
+
return ['readonly', 'value', 'placeholder', 'dark-selector'];
|
|
26
|
+
}
|
|
75
27
|
|
|
76
|
-
//
|
|
77
|
-
static
|
|
78
|
-
|
|
28
|
+
// Helper: Convert camelCase to kebab-case
|
|
29
|
+
static _toKebabCase(str) {
|
|
30
|
+
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
31
|
+
}
|
|
79
32
|
|
|
80
|
-
//
|
|
81
|
-
static
|
|
33
|
+
// Dark theme defaults - IntelliJ Darcula (light defaults are CSS fallbacks)
|
|
34
|
+
static DARK_THEME_DEFAULTS = {
|
|
35
|
+
bgColor: '#2b2b2b',
|
|
36
|
+
textColor: '#a9b7c6',
|
|
37
|
+
caretColor: '#bbbbbb',
|
|
38
|
+
gutterBg: '#313335',
|
|
39
|
+
gutterBorder: '#3c3f41',
|
|
40
|
+
jsonKey: '#9876aa',
|
|
41
|
+
jsonString: '#6a8759',
|
|
42
|
+
jsonNumber: '#6897bb',
|
|
43
|
+
jsonBoolean: '#cc7832',
|
|
44
|
+
jsonNull: '#cc7832',
|
|
45
|
+
jsonPunct: '#a9b7c6',
|
|
46
|
+
controlColor: '#cc7832',
|
|
47
|
+
controlBg: '#3c3f41',
|
|
48
|
+
controlBorder: '#5a5a5a',
|
|
49
|
+
geojsonKey: '#9876aa',
|
|
50
|
+
geojsonType: '#6a8759',
|
|
51
|
+
geojsonTypeInvalid: '#ff6b68',
|
|
52
|
+
jsonKeyInvalid: '#ff6b68'
|
|
53
|
+
};
|
|
82
54
|
|
|
83
55
|
// Pre-compiled regex patterns (avoid recompilation on each call)
|
|
84
56
|
static REGEX = {
|
|
@@ -118,6 +90,21 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
118
90
|
this.updatePlaceholderContent();
|
|
119
91
|
}
|
|
120
92
|
|
|
93
|
+
disconnectedCallback() {
|
|
94
|
+
// Clean up any open color picker and its global listener
|
|
95
|
+
const colorPicker = document.querySelector('.geojson-color-picker-input');
|
|
96
|
+
if (colorPicker && colorPicker._closeListener) {
|
|
97
|
+
document.removeEventListener('click', colorPicker._closeListener, true);
|
|
98
|
+
colorPicker.remove();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Clear any pending highlight timer
|
|
102
|
+
if (this.highlightTimer) {
|
|
103
|
+
clearTimeout(this.highlightTimer);
|
|
104
|
+
this.highlightTimer = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
121
108
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
122
109
|
if (oldValue === newValue) return;
|
|
123
110
|
|
|
@@ -129,8 +116,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
129
116
|
this.updatePlaceholderContent();
|
|
130
117
|
} else if (name === 'dark-selector') {
|
|
131
118
|
this.updateThemeCSS();
|
|
132
|
-
} else if (name === 'feature-collection') {
|
|
133
|
-
this.updatePrefixSuffix();
|
|
134
119
|
}
|
|
135
120
|
}
|
|
136
121
|
|
|
@@ -148,17 +133,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
148
133
|
return this.getAttribute('placeholder') || '';
|
|
149
134
|
}
|
|
150
135
|
|
|
151
|
-
|
|
152
|
-
return this.hasAttribute('feature-collection');
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Internal getters for prefix/suffix based on feature-collection mode
|
|
136
|
+
// Always in FeatureCollection mode - prefix/suffix are constant
|
|
156
137
|
get prefix() {
|
|
157
|
-
return
|
|
138
|
+
return '{"type": "FeatureCollection", "features": [';
|
|
158
139
|
}
|
|
159
|
-
|
|
140
|
+
|
|
160
141
|
get suffix() {
|
|
161
|
-
return
|
|
142
|
+
return ']}';
|
|
162
143
|
}
|
|
163
144
|
|
|
164
145
|
render() {
|
|
@@ -188,9 +169,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
188
169
|
position: relative;
|
|
189
170
|
width: 100%;
|
|
190
171
|
height: 400px;
|
|
191
|
-
font-family: 'Courier New', Courier, monospace;
|
|
192
|
-
font-size: 13px;
|
|
193
|
-
line-height: 1.5;
|
|
194
172
|
border-radius: 4px;
|
|
195
173
|
overflow: hidden;
|
|
196
174
|
}
|
|
@@ -221,18 +199,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
221
199
|
position: relative;
|
|
222
200
|
width: 100%;
|
|
223
201
|
flex: 1;
|
|
224
|
-
background: var(--bg-color);
|
|
202
|
+
background: var(--bg-color, #ffffff);
|
|
225
203
|
display: flex;
|
|
226
|
-
font-family: 'Courier New', Courier, monospace;
|
|
227
|
-
font-size: 13px;
|
|
228
|
-
line-height: 1.5;
|
|
229
204
|
}
|
|
230
205
|
|
|
231
206
|
.gutter {
|
|
232
207
|
width: 24px;
|
|
233
208
|
height: 100%;
|
|
234
|
-
background: var(--gutter-bg);
|
|
235
|
-
border-right: 1px solid var(--gutter-border);
|
|
209
|
+
background: var(--gutter-bg, #f0f0f0);
|
|
210
|
+
border-right: 1px solid var(--gutter-border, #e0e0e0);
|
|
236
211
|
overflow: hidden;
|
|
237
212
|
flex-shrink: 0;
|
|
238
213
|
position: relative;
|
|
@@ -274,10 +249,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
274
249
|
.collapse-button {
|
|
275
250
|
width: 12px;
|
|
276
251
|
height: 12px;
|
|
277
|
-
background: var(--control-bg);
|
|
278
|
-
border: 1px solid var(--control-border);
|
|
252
|
+
background: var(--control-bg, #e8e8e8);
|
|
253
|
+
border: 1px solid var(--control-border, #c0c0c0);
|
|
279
254
|
border-radius: 2px;
|
|
280
|
-
color: var(--control-color);
|
|
255
|
+
color: var(--control-color, #000080);
|
|
281
256
|
font-size: 8px;
|
|
282
257
|
font-weight: bold;
|
|
283
258
|
cursor: pointer;
|
|
@@ -290,8 +265,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
290
265
|
}
|
|
291
266
|
|
|
292
267
|
.collapse-button:hover {
|
|
293
|
-
background: var(--control-bg);
|
|
294
|
-
border-color: var(--control-color);
|
|
268
|
+
background: var(--control-bg, #e8e8e8);
|
|
269
|
+
border-color: var(--control-color, #000080);
|
|
295
270
|
transform: scale(1.1);
|
|
296
271
|
}
|
|
297
272
|
|
|
@@ -300,7 +275,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
300
275
|
height: 14px;
|
|
301
276
|
background: transparent;
|
|
302
277
|
border: none;
|
|
303
|
-
color: var(--control-color);
|
|
304
278
|
cursor: pointer;
|
|
305
279
|
display: flex;
|
|
306
280
|
align-items: center;
|
|
@@ -309,21 +283,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
309
283
|
flex-shrink: 0;
|
|
310
284
|
opacity: 0.7;
|
|
311
285
|
padding: 0;
|
|
286
|
+
font-size: 11px;
|
|
312
287
|
}
|
|
313
288
|
|
|
314
289
|
.visibility-button:hover {
|
|
315
290
|
opacity: 1;
|
|
316
|
-
transform: scale(1.
|
|
291
|
+
transform: scale(1.15);
|
|
317
292
|
}
|
|
318
293
|
|
|
319
294
|
.visibility-button.hidden {
|
|
320
|
-
opacity: 0.
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
.visibility-button svg {
|
|
324
|
-
width: 12px;
|
|
325
|
-
height: 12px;
|
|
326
|
-
fill: currentColor;
|
|
295
|
+
opacity: 0.35;
|
|
327
296
|
}
|
|
328
297
|
|
|
329
298
|
/* Hidden feature lines - grayed out */
|
|
@@ -362,17 +331,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
362
331
|
width: 100%;
|
|
363
332
|
height: 100%;
|
|
364
333
|
padding: 8px 12px;
|
|
365
|
-
font-family: 'Courier New', Courier, monospace;
|
|
366
|
-
font-size: 13px;
|
|
367
|
-
font-weight: normal;
|
|
368
|
-
font-style: normal;
|
|
369
|
-
line-height: 1.5;
|
|
370
334
|
white-space: pre-wrap;
|
|
371
335
|
word-wrap: break-word;
|
|
372
336
|
overflow: auto;
|
|
373
337
|
pointer-events: none;
|
|
374
338
|
z-index: 1;
|
|
375
|
-
color: var(--text-color);
|
|
339
|
+
color: var(--text-color, #000000);
|
|
376
340
|
}
|
|
377
341
|
|
|
378
342
|
.highlight-layer::-webkit-scrollbar {
|
|
@@ -391,18 +355,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
391
355
|
outline: none;
|
|
392
356
|
background: transparent;
|
|
393
357
|
color: transparent;
|
|
394
|
-
caret-color: var(--caret-color);
|
|
395
|
-
font-family: 'Courier New', Courier, monospace;
|
|
396
|
-
font-size: 13px;
|
|
397
|
-
font-weight: normal;
|
|
398
|
-
font-style: normal;
|
|
399
|
-
line-height: 1.5;
|
|
358
|
+
caret-color: var(--caret-color, #000);
|
|
400
359
|
white-space: pre-wrap;
|
|
401
360
|
word-wrap: break-word;
|
|
402
361
|
resize: none;
|
|
403
362
|
overflow: auto;
|
|
404
363
|
z-index: 2;
|
|
405
|
-
box-sizing: border-box;
|
|
406
364
|
}
|
|
407
365
|
|
|
408
366
|
textarea::selection {
|
|
@@ -420,11 +378,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
420
378
|
width: 100%;
|
|
421
379
|
height: 100%;
|
|
422
380
|
padding: 8px 12px;
|
|
423
|
-
font-family: 'Courier New', Courier, monospace;
|
|
424
|
-
font-size: 13px;
|
|
425
|
-
font-weight: normal;
|
|
426
|
-
font-style: normal;
|
|
427
|
-
line-height: 1.5;
|
|
428
381
|
white-space: pre-wrap;
|
|
429
382
|
word-wrap: break-word;
|
|
430
383
|
color: #6a6a6a;
|
|
@@ -438,74 +391,116 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
438
391
|
opacity: 0.6;
|
|
439
392
|
}
|
|
440
393
|
|
|
441
|
-
/* Syntax highlighting colors */
|
|
394
|
+
/* Syntax highlighting colors - IntelliJ Light defaults */
|
|
442
395
|
.json-key {
|
|
443
|
-
color: var(--json-key);
|
|
396
|
+
color: var(--json-key, #660e7a);
|
|
444
397
|
}
|
|
445
398
|
|
|
446
399
|
.json-string {
|
|
447
|
-
color: var(--json-string);
|
|
400
|
+
color: var(--json-string, #008000);
|
|
448
401
|
}
|
|
449
402
|
|
|
450
403
|
.json-number {
|
|
451
|
-
color: var(--json-number);
|
|
404
|
+
color: var(--json-number, #0000ff);
|
|
452
405
|
}
|
|
453
406
|
|
|
454
407
|
.json-boolean {
|
|
455
|
-
color: var(--json-boolean);
|
|
408
|
+
color: var(--json-boolean, #000080);
|
|
456
409
|
}
|
|
457
410
|
|
|
458
411
|
.json-null {
|
|
459
|
-
color: var(--json-null);
|
|
412
|
+
color: var(--json-null, #000080);
|
|
460
413
|
}
|
|
461
414
|
|
|
462
415
|
.json-punctuation {
|
|
463
|
-
color: var(--json-punct);
|
|
416
|
+
color: var(--json-punct, #000000);
|
|
464
417
|
}
|
|
465
418
|
|
|
466
419
|
/* GeoJSON-specific highlighting */
|
|
467
420
|
.geojson-key {
|
|
468
|
-
color: var(--geojson-key);
|
|
421
|
+
color: var(--geojson-key, #660e7a);
|
|
469
422
|
font-weight: 600;
|
|
470
423
|
}
|
|
471
424
|
|
|
472
425
|
.geojson-type {
|
|
473
|
-
color: var(--geojson-type);
|
|
426
|
+
color: var(--geojson-type, #008000);
|
|
474
427
|
font-weight: 600;
|
|
475
428
|
}
|
|
476
429
|
|
|
477
430
|
.geojson-type-invalid {
|
|
478
|
-
color: var(--geojson-type-invalid);
|
|
431
|
+
color: var(--geojson-type-invalid, #ff0000);
|
|
479
432
|
font-weight: 600;
|
|
480
433
|
}
|
|
481
434
|
|
|
482
435
|
.json-key-invalid {
|
|
483
|
-
color: var(--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 {
|
|
442
|
+
display: flex;
|
|
443
|
+
flex-shrink: 0;
|
|
444
|
+
background: var(--bg-color, #ffffff);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.prefix-gutter,
|
|
448
|
+
.suffix-gutter {
|
|
449
|
+
width: 24px;
|
|
450
|
+
background: var(--gutter-bg, #f0f0f0);
|
|
451
|
+
border-right: 1px solid var(--gutter-border, #e0e0e0);
|
|
452
|
+
flex-shrink: 0;
|
|
484
453
|
}
|
|
485
454
|
|
|
486
|
-
/* Prefix and suffix styling */
|
|
487
455
|
.editor-prefix,
|
|
488
456
|
.editor-suffix {
|
|
457
|
+
flex: 1;
|
|
489
458
|
padding: 4px 12px;
|
|
490
|
-
color: var(--text-color);
|
|
491
|
-
background: var(--bg-color);
|
|
459
|
+
color: var(--text-color, #000000);
|
|
460
|
+
background: var(--bg-color, #ffffff);
|
|
492
461
|
user-select: none;
|
|
493
462
|
white-space: pre-wrap;
|
|
494
463
|
word-wrap: break-word;
|
|
495
|
-
flex-shrink: 0;
|
|
496
|
-
font-family: 'Courier New', Courier, monospace;
|
|
497
|
-
font-size: 13px;
|
|
498
|
-
line-height: 1.5;
|
|
499
464
|
opacity: 0.6;
|
|
500
|
-
border-left: 3px solid rgba(102, 126, 234, 0.5);
|
|
501
465
|
}
|
|
502
466
|
|
|
503
|
-
.
|
|
467
|
+
.prefix-wrapper {
|
|
504
468
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
505
469
|
}
|
|
506
470
|
|
|
507
|
-
.
|
|
471
|
+
.suffix-wrapper {
|
|
508
472
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
473
|
+
position: relative;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* Clear button in suffix area */
|
|
477
|
+
.clear-btn {
|
|
478
|
+
position: absolute;
|
|
479
|
+
right: 0.5rem;
|
|
480
|
+
top: 50%;
|
|
481
|
+
transform: translateY(-50%);
|
|
482
|
+
background: transparent;
|
|
483
|
+
border: none;
|
|
484
|
+
color: var(--text-color, #000000);
|
|
485
|
+
opacity: 0.3;
|
|
486
|
+
cursor: pointer;
|
|
487
|
+
font-size: 0.65rem;
|
|
488
|
+
width: 1rem;
|
|
489
|
+
height: 1rem;
|
|
490
|
+
padding: 0.15rem 0 0 0;
|
|
491
|
+
border-radius: 3px;
|
|
492
|
+
display: flex;
|
|
493
|
+
align-items: center;
|
|
494
|
+
justify-content: center;
|
|
495
|
+
box-sizing: border-box;
|
|
496
|
+
transition: opacity 0.2s, background 0.2s;
|
|
497
|
+
}
|
|
498
|
+
.clear-btn:hover {
|
|
499
|
+
opacity: 0.7;
|
|
500
|
+
background: rgba(255, 255, 255, 0.1);
|
|
501
|
+
}
|
|
502
|
+
.clear-btn[hidden] {
|
|
503
|
+
display: none;
|
|
509
504
|
}
|
|
510
505
|
|
|
511
506
|
/* Scrollbar styling - WebKit (Chrome, Safari, Edge) */
|
|
@@ -515,28 +510,31 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
515
510
|
}
|
|
516
511
|
|
|
517
512
|
textarea::-webkit-scrollbar-track {
|
|
518
|
-
background: var(--control-bg);
|
|
513
|
+
background: var(--control-bg, #e8e8e8);
|
|
519
514
|
}
|
|
520
515
|
|
|
521
516
|
textarea::-webkit-scrollbar-thumb {
|
|
522
|
-
background: var(--control-border);
|
|
517
|
+
background: var(--control-border, #c0c0c0);
|
|
523
518
|
border-radius: 5px;
|
|
524
519
|
}
|
|
525
520
|
|
|
526
521
|
textarea::-webkit-scrollbar-thumb:hover {
|
|
527
|
-
background: var(--control-color);
|
|
522
|
+
background: var(--control-color, #000080);
|
|
528
523
|
}
|
|
529
524
|
|
|
530
525
|
/* Scrollbar styling - Firefox */
|
|
531
526
|
textarea {
|
|
532
527
|
scrollbar-width: thin;
|
|
533
|
-
scrollbar-color: var(--control-border) var(--control-bg);
|
|
528
|
+
scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8);
|
|
534
529
|
}
|
|
535
530
|
</style>
|
|
536
531
|
`;
|
|
537
532
|
|
|
538
533
|
const template = `
|
|
539
|
-
<div class="
|
|
534
|
+
<div class="prefix-wrapper">
|
|
535
|
+
<div class="prefix-gutter"></div>
|
|
536
|
+
<div class="editor-prefix" id="editorPrefix"></div>
|
|
537
|
+
</div>
|
|
540
538
|
<div class="editor-wrapper">
|
|
541
539
|
<div class="gutter">
|
|
542
540
|
<div class="gutter-content" id="gutterContent"></div>
|
|
@@ -553,7 +551,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
553
551
|
></textarea>
|
|
554
552
|
</div>
|
|
555
553
|
</div>
|
|
556
|
-
<div class="
|
|
554
|
+
<div class="suffix-wrapper">
|
|
555
|
+
<div class="suffix-gutter"></div>
|
|
556
|
+
<div class="editor-suffix" id="editorSuffix"></div>
|
|
557
|
+
<button class="clear-btn" id="clearBtn" title="Clear editor">✕</button>
|
|
558
|
+
</div>
|
|
557
559
|
`;
|
|
558
560
|
|
|
559
561
|
this.shadowRoot.innerHTML = styles + template;
|
|
@@ -646,6 +648,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
646
648
|
this.handleCutWithCollapsedContent(e);
|
|
647
649
|
});
|
|
648
650
|
|
|
651
|
+
// Clear button
|
|
652
|
+
const clearBtn = this.shadowRoot.getElementById('clearBtn');
|
|
653
|
+
clearBtn.addEventListener('click', () => {
|
|
654
|
+
this.removeAll();
|
|
655
|
+
});
|
|
656
|
+
|
|
649
657
|
// Update readonly state
|
|
650
658
|
this.updateReadonly();
|
|
651
659
|
}
|
|
@@ -660,14 +668,20 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
660
668
|
if (textarea) {
|
|
661
669
|
textarea.disabled = this.readonly;
|
|
662
670
|
}
|
|
671
|
+
// Hide clear button in readonly mode
|
|
672
|
+
const clearBtn = this.shadowRoot.getElementById('clearBtn');
|
|
673
|
+
if (clearBtn) {
|
|
674
|
+
clearBtn.hidden = this.readonly;
|
|
675
|
+
}
|
|
663
676
|
}
|
|
664
677
|
|
|
665
678
|
escapeHtml(text) {
|
|
666
679
|
if (!text) return '';
|
|
680
|
+
const R = GeoJsonEditor.REGEX;
|
|
667
681
|
return text
|
|
668
|
-
.replace(
|
|
669
|
-
.replace(
|
|
670
|
-
.replace(
|
|
682
|
+
.replace(R.ampersand, '&')
|
|
683
|
+
.replace(R.lessThan, '<')
|
|
684
|
+
.replace(R.greaterThan, '>');
|
|
671
685
|
}
|
|
672
686
|
|
|
673
687
|
updatePlaceholderVisibility() {
|
|
@@ -744,24 +758,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
744
758
|
const prefixEl = this.shadowRoot.getElementById('editorPrefix');
|
|
745
759
|
const suffixEl = this.shadowRoot.getElementById('editorSuffix');
|
|
746
760
|
|
|
761
|
+
// Always show prefix/suffix (always in FeatureCollection mode)
|
|
747
762
|
if (prefixEl) {
|
|
748
|
-
|
|
749
|
-
prefixEl.textContent = this.prefix;
|
|
750
|
-
prefixEl.style.display = 'block';
|
|
751
|
-
} else {
|
|
752
|
-
prefixEl.textContent = '';
|
|
753
|
-
prefixEl.style.display = 'none';
|
|
754
|
-
}
|
|
763
|
+
prefixEl.textContent = this.prefix;
|
|
755
764
|
}
|
|
756
765
|
|
|
757
766
|
if (suffixEl) {
|
|
758
|
-
|
|
759
|
-
suffixEl.textContent = this.suffix;
|
|
760
|
-
suffixEl.style.display = 'block';
|
|
761
|
-
} else {
|
|
762
|
-
suffixEl.textContent = '';
|
|
763
|
-
suffixEl.style.display = 'none';
|
|
764
|
-
}
|
|
767
|
+
suffixEl.textContent = this.suffix;
|
|
765
768
|
}
|
|
766
769
|
}
|
|
767
770
|
|
|
@@ -902,8 +905,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
902
905
|
const contextStack = []; // Stack of {context, isArray}
|
|
903
906
|
let pendingContext = null; // Context for next object/array
|
|
904
907
|
|
|
905
|
-
//
|
|
906
|
-
const rootContext =
|
|
908
|
+
// Root context is always 'Feature' (always in FeatureCollection mode)
|
|
909
|
+
const rootContext = 'Feature';
|
|
907
910
|
|
|
908
911
|
for (let i = 0; i < lines.length; i++) {
|
|
909
912
|
const line = lines[i];
|
|
@@ -915,37 +918,61 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
915
918
|
contextMap.set(i, lineContext);
|
|
916
919
|
|
|
917
920
|
// Process each character to track brackets for subsequent lines
|
|
921
|
+
// Track string state to ignore brackets inside strings
|
|
922
|
+
let inString = false;
|
|
923
|
+
let escape = false;
|
|
924
|
+
|
|
918
925
|
for (let j = 0; j < line.length; j++) {
|
|
919
926
|
const char = line[j];
|
|
920
927
|
|
|
921
|
-
//
|
|
928
|
+
// Handle escape sequences
|
|
929
|
+
if (escape) {
|
|
930
|
+
escape = false;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (char === '\\' && inString) {
|
|
934
|
+
escape = true;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Track string boundaries
|
|
922
939
|
if (char === '"') {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
if (
|
|
927
|
-
|
|
940
|
+
if (!inString) {
|
|
941
|
+
// Entering string - check for special patterns before toggling
|
|
942
|
+
const keyMatch = line.substring(j).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/);
|
|
943
|
+
if (keyMatch) {
|
|
944
|
+
const keyName = keyMatch[1];
|
|
945
|
+
if (GeoJsonEditor.CONTEXT_CHANGING_KEYS[keyName]) {
|
|
946
|
+
pendingContext = GeoJsonEditor.CONTEXT_CHANGING_KEYS[keyName];
|
|
947
|
+
}
|
|
948
|
+
j += keyMatch[0].length - 1; // Skip past the key
|
|
949
|
+
continue;
|
|
928
950
|
}
|
|
929
|
-
j += keyMatch[0].length - 1; // Skip past the key
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
951
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
952
|
+
// Check for type value to refine context: "type": "Point"
|
|
953
|
+
if (contextStack.length > 0) {
|
|
954
|
+
const typeMatch = line.substring(0, j).match(/"type"\s*:\s*$/);
|
|
955
|
+
if (typeMatch) {
|
|
956
|
+
const valueMatch = line.substring(j).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);
|
|
957
|
+
if (valueMatch && GeoJsonEditor.GEOJSON_TYPES_ALL.includes(valueMatch[1])) {
|
|
958
|
+
const currentCtx = contextStack[contextStack.length - 1];
|
|
959
|
+
if (currentCtx) {
|
|
960
|
+
currentCtx.context = valueMatch[1];
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// Skip past this string value
|
|
964
|
+
j += valueMatch ? valueMatch[0].length - 1 : 0;
|
|
965
|
+
continue;
|
|
944
966
|
}
|
|
945
967
|
}
|
|
946
968
|
}
|
|
969
|
+
inString = !inString;
|
|
970
|
+
continue;
|
|
947
971
|
}
|
|
948
972
|
|
|
973
|
+
// Skip everything inside strings (brackets, etc.)
|
|
974
|
+
if (inString) continue;
|
|
975
|
+
|
|
949
976
|
// Opening bracket - push context
|
|
950
977
|
if (char === '{' || char === '[') {
|
|
951
978
|
let newContext;
|
|
@@ -953,10 +980,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
953
980
|
newContext = pendingContext;
|
|
954
981
|
pendingContext = null;
|
|
955
982
|
} else if (contextStack.length === 0) {
|
|
956
|
-
// Root level
|
|
957
983
|
newContext = rootContext;
|
|
958
984
|
} else {
|
|
959
|
-
// Inherit from parent if in array
|
|
960
985
|
const parent = contextStack[contextStack.length - 1];
|
|
961
986
|
if (parent && parent.isArray) {
|
|
962
987
|
newContext = parent.context;
|
|
@@ -1020,7 +1045,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1020
1045
|
.replace(R.lessThan, '<')
|
|
1021
1046
|
.replace(R.greaterThan, '>')
|
|
1022
1047
|
// All JSON keys - validate against context
|
|
1023
|
-
.replace(R.jsonKey, (
|
|
1048
|
+
.replace(R.jsonKey, (_, key) => {
|
|
1024
1049
|
// Inside properties - all keys are regular user keys
|
|
1025
1050
|
if (context === 'properties') {
|
|
1026
1051
|
return `<span class="json-key">"${key}"</span>:`;
|
|
@@ -1037,7 +1062,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1037
1062
|
}
|
|
1038
1063
|
})
|
|
1039
1064
|
// GeoJSON "type" values - validate based on context
|
|
1040
|
-
.replace(R.typeValue, (
|
|
1065
|
+
.replace(R.typeValue, (_, typeValue) => {
|
|
1041
1066
|
if (isTypeValid(typeValue)) {
|
|
1042
1067
|
return `<span class="geojson-key">"type"</span>: <span class="geojson-type">"${typeValue}"</span>`;
|
|
1043
1068
|
} else {
|
|
@@ -1109,48 +1134,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1109
1134
|
|
|
1110
1135
|
const indent = match[1];
|
|
1111
1136
|
const openBracket = match[3];
|
|
1112
|
-
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
1113
|
-
|
|
1114
|
-
// Check if bracket closes on same line - can't collapse
|
|
1115
|
-
if (this.bracketClosesOnSameLine(currentLine, openBracket)) return;
|
|
1116
|
-
|
|
1117
|
-
// Find closing bracket in following lines
|
|
1118
|
-
let depth = 1;
|
|
1119
|
-
let endLine = line;
|
|
1120
|
-
const content = [];
|
|
1121
|
-
|
|
1122
|
-
for (let i = line + 1; i < lines.length; i++) {
|
|
1123
|
-
const scanLine = lines[i];
|
|
1124
|
-
|
|
1125
|
-
for (const char of scanLine) {
|
|
1126
|
-
if (char === openBracket) depth++;
|
|
1127
|
-
if (char === closeBracket) depth--;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
content.push(scanLine);
|
|
1131
|
-
|
|
1132
|
-
if (depth === 0) {
|
|
1133
|
-
endLine = i;
|
|
1134
|
-
break;
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
1137
|
|
|
1138
|
-
//
|
|
1139
|
-
|
|
1140
|
-
this.collapsedData.set(uniqueKey, {
|
|
1141
|
-
originalLine: currentLine,
|
|
1142
|
-
content: content,
|
|
1143
|
-
indent: indent.length,
|
|
1144
|
-
nodeKey: nodeKey // Store nodeKey for later use
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
// Replace with marker
|
|
1148
|
-
const beforeBracket = currentLine.substring(0, currentLine.indexOf(openBracket));
|
|
1149
|
-
const hasTrailingComma = lines[endLine] && lines[endLine].trim().endsWith(',');
|
|
1150
|
-
lines[line] = `${beforeBracket}${openBracket}...${closeBracket}${hasTrailingComma ? ',' : ''}`;
|
|
1151
|
-
|
|
1152
|
-
// Remove content lines
|
|
1153
|
-
lines.splice(line + 1, endLine - line);
|
|
1138
|
+
// Use common collapse helper
|
|
1139
|
+
if (this._performCollapse(lines, line, nodeKey, indent, openBracket) === 0) return;
|
|
1154
1140
|
}
|
|
1155
1141
|
|
|
1156
1142
|
// Update textarea
|
|
@@ -1176,48 +1162,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1176
1162
|
if (nodeKey === 'coordinates') {
|
|
1177
1163
|
const indent = match[1];
|
|
1178
1164
|
const openBracket = match[3];
|
|
1179
|
-
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
1180
|
-
|
|
1181
|
-
// Skip if bracket closes on same line
|
|
1182
|
-
if (this.bracketClosesOnSameLine(line, openBracket)) continue;
|
|
1183
|
-
|
|
1184
|
-
// Find closing bracket in following lines
|
|
1185
|
-
let depth = 1;
|
|
1186
|
-
let endLine = i;
|
|
1187
|
-
const content = [];
|
|
1188
|
-
|
|
1189
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
1190
|
-
const scanLine = lines[j];
|
|
1191
|
-
|
|
1192
|
-
for (const char of scanLine) {
|
|
1193
|
-
if (char === openBracket) depth++;
|
|
1194
|
-
if (char === closeBracket) depth--;
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
content.push(scanLine);
|
|
1198
|
-
|
|
1199
|
-
if (depth === 0) {
|
|
1200
|
-
endLine = j;
|
|
1201
|
-
break;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// Store the original data with unique key
|
|
1206
|
-
const uniqueKey = `${i}-${nodeKey}`;
|
|
1207
|
-
this.collapsedData.set(uniqueKey, {
|
|
1208
|
-
originalLine: line,
|
|
1209
|
-
content: content,
|
|
1210
|
-
indent: indent.length,
|
|
1211
|
-
nodeKey: nodeKey
|
|
1212
|
-
});
|
|
1213
1165
|
|
|
1214
|
-
//
|
|
1215
|
-
|
|
1216
|
-
const hasTrailingComma = lines[endLine] && lines[endLine].trim().endsWith(',');
|
|
1217
|
-
lines[i] = `${beforeBracket}${openBracket}...${closeBracket}${hasTrailingComma ? ',' : ''}`;
|
|
1218
|
-
|
|
1219
|
-
// Remove content lines
|
|
1220
|
-
lines.splice(i + 1, endLine - i);
|
|
1166
|
+
// Use common collapse helper
|
|
1167
|
+
this._performCollapse(lines, i, nodeKey, indent, openBracket);
|
|
1221
1168
|
}
|
|
1222
1169
|
}
|
|
1223
1170
|
}
|
|
@@ -1285,7 +1232,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1285
1232
|
elements.visibilityButtons.forEach(({ featureKey, isHidden }) => {
|
|
1286
1233
|
const button = document.createElement('button');
|
|
1287
1234
|
button.className = 'visibility-button' + (isHidden ? ' hidden' : '');
|
|
1288
|
-
button.
|
|
1235
|
+
button.textContent = '👁';
|
|
1289
1236
|
button.dataset.featureKey = featureKey;
|
|
1290
1237
|
button.title = isHidden ? 'Show feature in events' : 'Hide feature from events';
|
|
1291
1238
|
gutterLine.appendChild(button);
|
|
@@ -1322,9 +1269,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1322
1269
|
}
|
|
1323
1270
|
|
|
1324
1271
|
showColorPicker(indicator, line, currentColor, attributeName) {
|
|
1325
|
-
// Remove existing picker
|
|
1272
|
+
// Remove existing picker and clean up its listener
|
|
1326
1273
|
const existing = document.querySelector('.geojson-color-picker-input');
|
|
1327
|
-
if (existing)
|
|
1274
|
+
if (existing) {
|
|
1275
|
+
// Clean up the stored listener before removing
|
|
1276
|
+
if (existing._closeListener) {
|
|
1277
|
+
document.removeEventListener('click', existing._closeListener, true);
|
|
1278
|
+
}
|
|
1279
|
+
existing.remove();
|
|
1280
|
+
}
|
|
1328
1281
|
|
|
1329
1282
|
// Create small color input positioned at the indicator
|
|
1330
1283
|
const colorInput = document.createElement('input');
|
|
@@ -1358,11 +1311,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1358
1311
|
// Close picker when clicking anywhere else
|
|
1359
1312
|
const closeOnClickOutside = (e) => {
|
|
1360
1313
|
if (e.target !== colorInput && !colorInput.contains(e.target)) {
|
|
1361
|
-
colorInput.remove();
|
|
1362
1314
|
document.removeEventListener('click', closeOnClickOutside, true);
|
|
1315
|
+
colorInput.remove();
|
|
1363
1316
|
}
|
|
1364
1317
|
};
|
|
1365
1318
|
|
|
1319
|
+
// Store the listener reference on the element for cleanup
|
|
1320
|
+
colorInput._closeListener = closeOnClickOutside;
|
|
1321
|
+
|
|
1366
1322
|
// Add to document body with fixed positioning
|
|
1367
1323
|
document.body.appendChild(colorInput);
|
|
1368
1324
|
|
|
@@ -1424,8 +1380,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1424
1380
|
return; // No collapsed content, use default copy behavior
|
|
1425
1381
|
}
|
|
1426
1382
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1383
|
+
let expandedText;
|
|
1384
|
+
|
|
1385
|
+
// If selecting all content, use expandAllCollapsed directly (more reliable)
|
|
1386
|
+
if (start === 0 && end === textarea.value.length) {
|
|
1387
|
+
expandedText = this.expandAllCollapsed(selectedText);
|
|
1388
|
+
} else {
|
|
1389
|
+
// For partial selection, expand using line-by-line matching
|
|
1390
|
+
expandedText = this.expandCollapsedMarkersInText(selectedText, start);
|
|
1391
|
+
}
|
|
1429
1392
|
|
|
1430
1393
|
// Put expanded text in clipboard
|
|
1431
1394
|
e.preventDefault();
|
|
@@ -1436,6 +1399,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1436
1399
|
const textarea = this.shadowRoot.getElementById('textarea');
|
|
1437
1400
|
const beforeSelection = textarea.value.substring(0, startPos);
|
|
1438
1401
|
const startLineNum = beforeSelection.split('\n').length - 1;
|
|
1402
|
+
const R = GeoJsonEditor.REGEX;
|
|
1439
1403
|
|
|
1440
1404
|
const lines = text.split('\n');
|
|
1441
1405
|
const expandedLines = [];
|
|
@@ -1445,17 +1409,43 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1445
1409
|
|
|
1446
1410
|
// Check if this line has a collapsed marker
|
|
1447
1411
|
if (line.includes('{...}') || line.includes('[...]')) {
|
|
1448
|
-
|
|
1412
|
+
const match = line.match(R.collapsedMarker);
|
|
1413
|
+
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);
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
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}`)) {
|
|
1429
|
+
expandedLines.push(collapsed.originalLine);
|
|
1430
|
+
expandedLines.push(...collapsed.content);
|
|
1431
|
+
found = true;
|
|
1432
|
+
break;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (found) return;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Fallback: search by line number only
|
|
1449
1439
|
let found = false;
|
|
1450
|
-
this.collapsedData.
|
|
1440
|
+
for (const [key, collapsed] of this.collapsedData.entries()) {
|
|
1451
1441
|
const collapsedLineNum = parseInt(key.split('-')[0]);
|
|
1452
1442
|
if (collapsedLineNum === absoluteLineNum) {
|
|
1453
|
-
// Replace with original line and all collapsed content
|
|
1454
1443
|
expandedLines.push(collapsed.originalLine);
|
|
1455
1444
|
expandedLines.push(...collapsed.content);
|
|
1456
1445
|
found = true;
|
|
1446
|
+
break;
|
|
1457
1447
|
}
|
|
1458
|
-
}
|
|
1448
|
+
}
|
|
1459
1449
|
if (!found) {
|
|
1460
1450
|
expandedLines.push(line);
|
|
1461
1451
|
}
|
|
@@ -1605,11 +1595,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1605
1595
|
this.emitChange();
|
|
1606
1596
|
}
|
|
1607
1597
|
|
|
1608
|
-
// Check if a feature is hidden
|
|
1609
|
-
isFeatureHidden(featureKey) {
|
|
1610
|
-
return this.hiddenFeatures.has(featureKey);
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
1598
|
// Parse JSON and extract feature ranges (line numbers for each Feature)
|
|
1614
1599
|
updateFeatureRanges() {
|
|
1615
1600
|
const textarea = this.shadowRoot.getElementById('textarea');
|
|
@@ -1663,22 +1648,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1663
1648
|
inFeature = true;
|
|
1664
1649
|
|
|
1665
1650
|
// Start braceDepth at 1 since we're inside the Feature's opening brace
|
|
1666
|
-
// Then count any additional braces from startLine to current line
|
|
1651
|
+
// Then count any additional braces from startLine to current line (ignoring strings)
|
|
1667
1652
|
braceDepth = 1;
|
|
1668
1653
|
for (let k = startLine; k <= i; k++) {
|
|
1669
1654
|
const scanLine = lines[k];
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
} else {
|
|
1677
|
-
braceDepth++;
|
|
1678
|
-
}
|
|
1679
|
-
} else if (char === '}') {
|
|
1680
|
-
braceDepth--;
|
|
1681
|
-
}
|
|
1655
|
+
const counts = this._countBracketsOutsideStrings(scanLine, '{');
|
|
1656
|
+
if (k === startLine) {
|
|
1657
|
+
// Skip the first { we already counted
|
|
1658
|
+
braceDepth += (counts.open - 1) - counts.close;
|
|
1659
|
+
} else {
|
|
1660
|
+
braceDepth += counts.open - counts.close;
|
|
1682
1661
|
}
|
|
1683
1662
|
}
|
|
1684
1663
|
|
|
@@ -1687,11 +1666,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1687
1666
|
currentFeatureKey = this.getFeatureKey(features[featureIndex]);
|
|
1688
1667
|
}
|
|
1689
1668
|
} else if (inFeature) {
|
|
1690
|
-
// Count braces
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
else if (char === '}') braceDepth--;
|
|
1694
|
-
}
|
|
1669
|
+
// Count braces (ignoring those in strings)
|
|
1670
|
+
const counts = this._countBracketsOutsideStrings(line, '{');
|
|
1671
|
+
braceDepth += counts.open - counts.close;
|
|
1695
1672
|
|
|
1696
1673
|
// Feature ends when braceDepth returns to 0
|
|
1697
1674
|
if (braceDepth <= 0) {
|
|
@@ -1779,19 +1756,131 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1779
1756
|
return errors;
|
|
1780
1757
|
}
|
|
1781
1758
|
|
|
1782
|
-
// Helper:
|
|
1783
|
-
|
|
1759
|
+
// Helper: Count bracket depth change in a line, ignoring brackets inside strings
|
|
1760
|
+
// Returns {open: count, close: count} for the specified bracket type
|
|
1761
|
+
_countBracketsOutsideStrings(line, openBracket) {
|
|
1784
1762
|
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
1763
|
+
let openCount = 0;
|
|
1764
|
+
let closeCount = 0;
|
|
1765
|
+
let inString = false;
|
|
1766
|
+
let escape = false;
|
|
1767
|
+
|
|
1768
|
+
for (let i = 0; i < line.length; i++) {
|
|
1769
|
+
const char = line[i];
|
|
1770
|
+
|
|
1771
|
+
if (escape) {
|
|
1772
|
+
escape = false;
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
if (char === '\\' && inString) {
|
|
1777
|
+
escape = true;
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if (char === '"') {
|
|
1782
|
+
inString = !inString;
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
if (!inString) {
|
|
1787
|
+
if (char === openBracket) openCount++;
|
|
1788
|
+
if (char === closeBracket) closeCount++;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
return { open: openCount, close: closeCount };
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// Helper: Check if bracket closes on same line (ignores brackets in strings)
|
|
1796
|
+
bracketClosesOnSameLine(line, openBracket) {
|
|
1785
1797
|
const bracketPos = line.indexOf(openBracket);
|
|
1786
1798
|
if (bracketPos === -1) return false;
|
|
1799
|
+
|
|
1787
1800
|
const restOfLine = line.substring(bracketPos + 1);
|
|
1801
|
+
const counts = this._countBracketsOutsideStrings(restOfLine, openBracket);
|
|
1802
|
+
|
|
1803
|
+
// Depth starts at 1 (we're after the opening bracket)
|
|
1804
|
+
// If closes equal or exceed opens + 1, the bracket closes on this line
|
|
1805
|
+
return counts.close > counts.open;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// Helper: Find closing bracket line starting from startLine
|
|
1809
|
+
// Returns { endLine, content: string[] } or null if not found
|
|
1810
|
+
_findClosingBracket(lines, startLine, openBracket) {
|
|
1788
1811
|
let depth = 1;
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1812
|
+
const content = [];
|
|
1813
|
+
|
|
1814
|
+
// Count remaining brackets on the start line (after the opening bracket)
|
|
1815
|
+
const startLineContent = lines[startLine];
|
|
1816
|
+
const bracketPos = startLineContent.indexOf(openBracket);
|
|
1817
|
+
if (bracketPos !== -1) {
|
|
1818
|
+
const restOfStartLine = startLineContent.substring(bracketPos + 1);
|
|
1819
|
+
const startCounts = this._countBracketsOutsideStrings(restOfStartLine, openBracket);
|
|
1820
|
+
depth += startCounts.open - startCounts.close;
|
|
1821
|
+
if (depth === 0) {
|
|
1822
|
+
return { endLine: startLine, content: [] };
|
|
1823
|
+
}
|
|
1793
1824
|
}
|
|
1794
|
-
|
|
1825
|
+
|
|
1826
|
+
for (let i = startLine + 1; i < lines.length; i++) {
|
|
1827
|
+
const scanLine = lines[i];
|
|
1828
|
+
const counts = this._countBracketsOutsideStrings(scanLine, openBracket);
|
|
1829
|
+
depth += counts.open - counts.close;
|
|
1830
|
+
|
|
1831
|
+
content.push(scanLine);
|
|
1832
|
+
|
|
1833
|
+
if (depth === 0) {
|
|
1834
|
+
return { endLine: i, content };
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
return null; // Not found (malformed JSON)
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Helper: Perform collapse operation on a node at given line
|
|
1843
|
+
* Stores data in collapsedData, replaces line with marker, removes content lines
|
|
1844
|
+
* @param {string[]} lines - Array of lines (modified in place)
|
|
1845
|
+
* @param {number} lineIndex - Index of line to collapse
|
|
1846
|
+
* @param {string} nodeKey - Key of the node (e.g., 'coordinates')
|
|
1847
|
+
* @param {string} indent - Indentation string
|
|
1848
|
+
* @param {string} openBracket - Opening bracket character ('{' or '[')
|
|
1849
|
+
* @returns {number} Number of lines removed, or 0 if collapse failed
|
|
1850
|
+
* @private
|
|
1851
|
+
*/
|
|
1852
|
+
_performCollapse(lines, lineIndex, nodeKey, indent, openBracket) {
|
|
1853
|
+
const line = lines[lineIndex];
|
|
1854
|
+
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
1855
|
+
|
|
1856
|
+
// Skip if bracket closes on same line
|
|
1857
|
+
if (this.bracketClosesOnSameLine(line, openBracket)) return 0;
|
|
1858
|
+
|
|
1859
|
+
// Find closing bracket
|
|
1860
|
+
const result = this._findClosingBracket(lines, lineIndex, openBracket);
|
|
1861
|
+
if (!result) return 0;
|
|
1862
|
+
|
|
1863
|
+
const { endLine, content } = result;
|
|
1864
|
+
|
|
1865
|
+
// Store the original data with unique key
|
|
1866
|
+
const uniqueKey = `${lineIndex}-${nodeKey}`;
|
|
1867
|
+
this.collapsedData.set(uniqueKey, {
|
|
1868
|
+
originalLine: line,
|
|
1869
|
+
content: content,
|
|
1870
|
+
indent: indent.length,
|
|
1871
|
+
nodeKey: nodeKey
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
// Replace with marker
|
|
1875
|
+
const beforeBracket = line.substring(0, line.indexOf(openBracket));
|
|
1876
|
+
const hasTrailingComma = lines[endLine] && lines[endLine].trim().endsWith(',');
|
|
1877
|
+
lines[lineIndex] = `${beforeBracket}${openBracket}...${closeBracket}${hasTrailingComma ? ',' : ''}`;
|
|
1878
|
+
|
|
1879
|
+
// Remove content lines
|
|
1880
|
+
const linesRemoved = endLine - lineIndex;
|
|
1881
|
+
lines.splice(lineIndex + 1, linesRemoved);
|
|
1882
|
+
|
|
1883
|
+
return linesRemoved;
|
|
1795
1884
|
}
|
|
1796
1885
|
|
|
1797
1886
|
// Helper: Expand all collapsed markers and return expanded content
|
|
@@ -1908,34 +1997,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1908
1997
|
}
|
|
1909
1998
|
}
|
|
1910
1999
|
|
|
1911
|
-
autoFormatContent() {
|
|
1912
|
-
const textarea = this.shadowRoot.getElementById('textarea');
|
|
1913
|
-
|
|
1914
|
-
// Save collapsed node details
|
|
1915
|
-
const collapsedNodes = Array.from(this.collapsedData.values()).map(data => ({
|
|
1916
|
-
nodeKey: data.nodeKey,
|
|
1917
|
-
indent: data.indent
|
|
1918
|
-
}));
|
|
1919
|
-
|
|
1920
|
-
// Expand and format
|
|
1921
|
-
const content = this.expandAllCollapsed(textarea.value);
|
|
1922
|
-
|
|
1923
|
-
try {
|
|
1924
|
-
const formattedContent = this.formatJSONContent(content);
|
|
1925
|
-
|
|
1926
|
-
if (formattedContent !== content) {
|
|
1927
|
-
this.collapsedData.clear();
|
|
1928
|
-
textarea.value = formattedContent;
|
|
1929
|
-
|
|
1930
|
-
if (collapsedNodes.length > 0) {
|
|
1931
|
-
this.reapplyCollapsed(collapsedNodes);
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
} catch (e) {
|
|
1935
|
-
// Invalid JSON, don't format
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
2000
|
reapplyCollapsed(collapsedNodes) {
|
|
1940
2001
|
const textarea = this.shadowRoot.getElementById('textarea');
|
|
1941
2002
|
const lines = textarea.value.split('\n');
|
|
@@ -1967,50 +2028,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1967
2028
|
|
|
1968
2029
|
// Only collapse if this occurrence should be collapsed
|
|
1969
2030
|
if (currentOccurrence <= collapseMap.get(key)) {
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
1973
|
-
|
|
1974
|
-
// Skip if closes on same line
|
|
1975
|
-
if (this.bracketClosesOnSameLine(line, openBracket)) continue;
|
|
1976
|
-
|
|
1977
|
-
// Find closing bracket
|
|
1978
|
-
let depth = 1;
|
|
1979
|
-
let endLine = i;
|
|
1980
|
-
const content = [];
|
|
2031
|
+
const indent = match[1];
|
|
2032
|
+
const openBracket = match[3];
|
|
1981
2033
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
for (const char of scanLine) {
|
|
1986
|
-
if (char === openBracket) depth++;
|
|
1987
|
-
if (char === closeBracket) depth--;
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
content.push(scanLine);
|
|
1991
|
-
|
|
1992
|
-
if (depth === 0) {
|
|
1993
|
-
endLine = j;
|
|
1994
|
-
break;
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
// Store with unique key
|
|
1999
|
-
const uniqueKey = `${i}-${nodeKey}`;
|
|
2000
|
-
this.collapsedData.set(uniqueKey, {
|
|
2001
|
-
originalLine: line,
|
|
2002
|
-
content: content,
|
|
2003
|
-
indent: indent.length,
|
|
2004
|
-
nodeKey: nodeKey
|
|
2005
|
-
});
|
|
2006
|
-
|
|
2007
|
-
// Replace with marker
|
|
2008
|
-
const beforeBracket = line.substring(0, line.indexOf(openBracket));
|
|
2009
|
-
const hasTrailingComma = lines[endLine] && lines[endLine].trim().endsWith(',');
|
|
2010
|
-
lines[i] = `${beforeBracket}${openBracket}...${closeBracket}${hasTrailingComma ? ',' : ''}`;
|
|
2011
|
-
|
|
2012
|
-
// Remove content lines
|
|
2013
|
-
lines.splice(i + 1, endLine - i);
|
|
2034
|
+
// Use common collapse helper
|
|
2035
|
+
this._performCollapse(lines, i, nodeKey, indent, openBracket);
|
|
2014
2036
|
}
|
|
2015
2037
|
}
|
|
2016
2038
|
}
|
|
@@ -2039,77 +2061,41 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2039
2061
|
// Generate and inject theme CSS based on dark selector
|
|
2040
2062
|
updateThemeCSS() {
|
|
2041
2063
|
const darkSelector = this.getAttribute('dark-selector') || '.dark';
|
|
2042
|
-
|
|
2043
|
-
// Parse selector to create CSS rule for dark theme
|
|
2044
2064
|
const darkRule = this.parseSelectorToHostRule(darkSelector);
|
|
2045
|
-
// Light theme is the default (no selector = light)
|
|
2046
|
-
const lightRule = ':host';
|
|
2047
2065
|
|
|
2048
2066
|
// Find or create theme style element
|
|
2049
2067
|
let themeStyle = this.shadowRoot.getElementById('theme-styles');
|
|
2050
2068
|
if (!themeStyle) {
|
|
2051
2069
|
themeStyle = document.createElement('style');
|
|
2052
2070
|
themeStyle.id = 'theme-styles';
|
|
2053
|
-
// Insert at the beginning of shadow root to ensure it's before static styles
|
|
2054
2071
|
this.shadowRoot.insertBefore(themeStyle, this.shadowRoot.firstChild);
|
|
2055
2072
|
}
|
|
2056
2073
|
|
|
2057
|
-
//
|
|
2058
|
-
const
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
--gutter-bg: ${this.themes.light.gutterBackground};
|
|
2064
|
-
--gutter-border: ${this.themes.light.gutterBorder};
|
|
2065
|
-
--json-key: ${this.themes.light.jsonKey};
|
|
2066
|
-
--json-string: ${this.themes.light.jsonString};
|
|
2067
|
-
--json-number: ${this.themes.light.jsonNumber};
|
|
2068
|
-
--json-boolean: ${this.themes.light.jsonBoolean};
|
|
2069
|
-
--json-null: ${this.themes.light.jsonNull};
|
|
2070
|
-
--json-punct: ${this.themes.light.jsonPunctuation};
|
|
2071
|
-
--control-color: ${this.themes.light.controlColor};
|
|
2072
|
-
--control-bg: ${this.themes.light.controlBg};
|
|
2073
|
-
--control-border: ${this.themes.light.controlBorder};
|
|
2074
|
-
--geojson-key: ${this.themes.light.geojsonKey};
|
|
2075
|
-
--geojson-type: ${this.themes.light.geojsonType};
|
|
2076
|
-
--geojson-type-invalid: ${this.themes.light.geojsonTypeInvalid};
|
|
2077
|
-
--json-key-invalid: ${this.themes.light.jsonKeyInvalid};
|
|
2078
|
-
}
|
|
2074
|
+
// Helper to generate CSS variables from theme object
|
|
2075
|
+
const generateVars = (themeObj) => {
|
|
2076
|
+
return Object.entries(themeObj || {})
|
|
2077
|
+
.map(([key, value]) => `--${GeoJsonEditor._toKebabCase(key)}: ${value};`)
|
|
2078
|
+
.join('\n ');
|
|
2079
|
+
};
|
|
2079
2080
|
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
--control-bg: ${this.themes.dark.controlBg};
|
|
2094
|
-
--control-border: ${this.themes.dark.controlBorder};
|
|
2095
|
-
--geojson-key: ${this.themes.dark.geojsonKey};
|
|
2096
|
-
--geojson-type: ${this.themes.dark.geojsonType};
|
|
2097
|
-
--geojson-type-invalid: ${this.themes.dark.geojsonTypeInvalid};
|
|
2098
|
-
--json-key-invalid: ${this.themes.dark.jsonKeyInvalid};
|
|
2099
|
-
}
|
|
2100
|
-
`;
|
|
2081
|
+
// Light theme: only overrides (defaults are in static CSS)
|
|
2082
|
+
const lightVars = generateVars(this.themes.light);
|
|
2083
|
+
|
|
2084
|
+
// Dark theme: ALWAYS generate with defaults + overrides (selector is dynamic)
|
|
2085
|
+
const darkTheme = { ...GeoJsonEditor.DARK_THEME_DEFAULTS, ...this.themes.dark };
|
|
2086
|
+
const darkVars = generateVars(darkTheme);
|
|
2087
|
+
|
|
2088
|
+
let css = '';
|
|
2089
|
+
if (lightVars) {
|
|
2090
|
+
css += `:host {\n ${lightVars}\n }\n`;
|
|
2091
|
+
}
|
|
2092
|
+
// Dark theme is always generated (selector is configurable)
|
|
2093
|
+
css += `${darkRule} {\n ${darkVars}\n }`;
|
|
2101
2094
|
|
|
2102
2095
|
themeStyle.textContent = css;
|
|
2103
2096
|
}
|
|
2104
2097
|
|
|
2105
2098
|
// Public API: Theme management
|
|
2106
|
-
getTheme() {
|
|
2107
|
-
return {
|
|
2108
|
-
dark: { ...this.themes.dark },
|
|
2109
|
-
light: { ...this.themes.light }
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
2099
|
setTheme(theme) {
|
|
2114
2100
|
if (theme.dark) {
|
|
2115
2101
|
this.themes.dark = { ...this.themes.dark, ...theme.dark };
|
|
@@ -2117,19 +2103,278 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2117
2103
|
if (theme.light) {
|
|
2118
2104
|
this.themes.light = { ...this.themes.light, ...theme.light };
|
|
2119
2105
|
}
|
|
2120
|
-
|
|
2121
|
-
// Regenerate CSS with new theme values
|
|
2122
2106
|
this.updateThemeCSS();
|
|
2123
2107
|
}
|
|
2124
2108
|
|
|
2125
2109
|
resetTheme() {
|
|
2126
|
-
|
|
2127
|
-
this.themes = {
|
|
2128
|
-
dark: { ...GeoJsonEditor.DEFAULT_THEMES.dark },
|
|
2129
|
-
light: { ...GeoJsonEditor.DEFAULT_THEMES.light }
|
|
2130
|
-
};
|
|
2110
|
+
this.themes = { dark: {}, light: {} };
|
|
2131
2111
|
this.updateThemeCSS();
|
|
2132
2112
|
}
|
|
2113
|
+
|
|
2114
|
+
// ========================================
|
|
2115
|
+
// Features API - Programmatic manipulation
|
|
2116
|
+
// ========================================
|
|
2117
|
+
|
|
2118
|
+
/**
|
|
2119
|
+
* Normalize a Python-style index (supports negative values)
|
|
2120
|
+
* @param {number} index - Index to normalize (negative = from end)
|
|
2121
|
+
* @param {number} length - Length of the array
|
|
2122
|
+
* @param {boolean} clamp - If true, clamp to valid range; if false, return -1 for out of bounds
|
|
2123
|
+
* @returns {number} Normalized index, or -1 if out of bounds (when clamp=false)
|
|
2124
|
+
* @private
|
|
2125
|
+
*/
|
|
2126
|
+
_normalizeIndex(index, length, clamp = false) {
|
|
2127
|
+
let idx = index;
|
|
2128
|
+
if (idx < 0) {
|
|
2129
|
+
idx = length + idx;
|
|
2130
|
+
}
|
|
2131
|
+
if (clamp) {
|
|
2132
|
+
return Math.max(0, Math.min(idx, length));
|
|
2133
|
+
}
|
|
2134
|
+
return (idx < 0 || idx >= length) ? -1 : idx;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
/**
|
|
2138
|
+
* Parse current textarea content into an array of features
|
|
2139
|
+
* @returns {Array} Array of feature objects
|
|
2140
|
+
* @private
|
|
2141
|
+
*/
|
|
2142
|
+
_parseFeatures() {
|
|
2143
|
+
const textarea = this.shadowRoot.getElementById('textarea');
|
|
2144
|
+
if (!textarea || !textarea.value.trim()) {
|
|
2145
|
+
return [];
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
try {
|
|
2149
|
+
// Expand collapsed nodes to get full content
|
|
2150
|
+
const content = this.expandAllCollapsed(textarea.value);
|
|
2151
|
+
// Wrap in array brackets and parse
|
|
2152
|
+
const wrapped = '[' + content + ']';
|
|
2153
|
+
return JSON.parse(wrapped);
|
|
2154
|
+
} catch (e) {
|
|
2155
|
+
return [];
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
/**
|
|
2160
|
+
* Update textarea with features array and trigger all updates
|
|
2161
|
+
* @param {Array} features - Array of feature objects
|
|
2162
|
+
* @private
|
|
2163
|
+
*/
|
|
2164
|
+
_setFeatures(features) {
|
|
2165
|
+
const textarea = this.shadowRoot.getElementById('textarea');
|
|
2166
|
+
if (!textarea) return;
|
|
2167
|
+
|
|
2168
|
+
// Clear internal state when replacing features (prevent memory leaks)
|
|
2169
|
+
this.collapsedData.clear();
|
|
2170
|
+
this.hiddenFeatures.clear();
|
|
2171
|
+
|
|
2172
|
+
if (!features || features.length === 0) {
|
|
2173
|
+
textarea.value = '';
|
|
2174
|
+
} else {
|
|
2175
|
+
// Format each feature and join with comma
|
|
2176
|
+
const formatted = features
|
|
2177
|
+
.map(f => JSON.stringify(f, null, 2))
|
|
2178
|
+
.join(',\n');
|
|
2179
|
+
|
|
2180
|
+
textarea.value = formatted;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// Trigger all updates
|
|
2184
|
+
this.updateHighlight();
|
|
2185
|
+
this.updatePlaceholderVisibility();
|
|
2186
|
+
|
|
2187
|
+
// Auto-collapse coordinates
|
|
2188
|
+
if (textarea.value) {
|
|
2189
|
+
requestAnimationFrame(() => {
|
|
2190
|
+
this.applyAutoCollapsed();
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// Emit change event
|
|
2195
|
+
this.emitChange();
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Validate a single feature object
|
|
2200
|
+
* @param {Object} feature - Feature object to validate
|
|
2201
|
+
* @returns {string[]} Array of validation error messages (empty if valid)
|
|
2202
|
+
* @private
|
|
2203
|
+
*/
|
|
2204
|
+
_validateFeature(feature) {
|
|
2205
|
+
const errors = [];
|
|
2206
|
+
|
|
2207
|
+
if (!feature || typeof feature !== 'object') {
|
|
2208
|
+
errors.push('Feature must be an object');
|
|
2209
|
+
return errors;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
if (Array.isArray(feature)) {
|
|
2213
|
+
errors.push('Feature cannot be an array');
|
|
2214
|
+
return errors;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// Check required type field
|
|
2218
|
+
if (!('type' in feature)) {
|
|
2219
|
+
errors.push('Feature must have a "type" property');
|
|
2220
|
+
} else if (feature.type !== 'Feature') {
|
|
2221
|
+
errors.push(`Feature type must be "Feature", got "${feature.type}"`);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// Check geometry field exists (can be null for features without location)
|
|
2225
|
+
if (!('geometry' in feature)) {
|
|
2226
|
+
errors.push('Feature must have a "geometry" property (can be null)');
|
|
2227
|
+
} else if (feature.geometry !== null) {
|
|
2228
|
+
// Validate geometry if not null
|
|
2229
|
+
if (typeof feature.geometry !== 'object' || Array.isArray(feature.geometry)) {
|
|
2230
|
+
errors.push('Feature geometry must be an object or null');
|
|
2231
|
+
} else {
|
|
2232
|
+
// Check geometry has valid type
|
|
2233
|
+
if (!('type' in feature.geometry)) {
|
|
2234
|
+
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(', ')})`);
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// Check geometry has coordinates (except GeometryCollection)
|
|
2240
|
+
if (feature.geometry.type !== 'GeometryCollection' && !('coordinates' in feature.geometry)) {
|
|
2241
|
+
errors.push('Geometry must have a "coordinates" property');
|
|
2242
|
+
}
|
|
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
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// Check properties field exists (can be null)
|
|
2252
|
+
if (!('properties' in feature)) {
|
|
2253
|
+
errors.push('Feature must have a "properties" property (can be null)');
|
|
2254
|
+
} else if (feature.properties !== null && (typeof feature.properties !== 'object' || Array.isArray(feature.properties))) {
|
|
2255
|
+
errors.push('Feature properties must be an object or null');
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
return errors;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
/**
|
|
2262
|
+
* Replace all features with the given array
|
|
2263
|
+
* @param {Array} features - Array of feature objects to set
|
|
2264
|
+
* @throws {Error} If features is not an array or contains invalid features
|
|
2265
|
+
*/
|
|
2266
|
+
set(features) {
|
|
2267
|
+
if (!Array.isArray(features)) {
|
|
2268
|
+
throw new Error('set() expects an array of features');
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// Validate each feature
|
|
2272
|
+
const allErrors = [];
|
|
2273
|
+
features.forEach((feature, index) => {
|
|
2274
|
+
const errors = this._validateFeature(feature);
|
|
2275
|
+
if (errors.length > 0) {
|
|
2276
|
+
allErrors.push(`Feature[${index}]: ${errors.join(', ')}`);
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
if (allErrors.length > 0) {
|
|
2281
|
+
throw new Error(`Invalid features: ${allErrors.join('; ')}`);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
this._setFeatures(features);
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/**
|
|
2288
|
+
* Add a feature at the end of the list
|
|
2289
|
+
* @param {Object} feature - Feature object to add
|
|
2290
|
+
* @throws {Error} If feature is invalid
|
|
2291
|
+
*/
|
|
2292
|
+
add(feature) {
|
|
2293
|
+
const errors = this._validateFeature(feature);
|
|
2294
|
+
if (errors.length > 0) {
|
|
2295
|
+
throw new Error(`Invalid feature: ${errors.join(', ')}`);
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
const features = this._parseFeatures();
|
|
2299
|
+
features.push(feature);
|
|
2300
|
+
this._setFeatures(features);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
/**
|
|
2304
|
+
* Insert a feature at the specified index
|
|
2305
|
+
* @param {Object} feature - Feature object to insert
|
|
2306
|
+
* @param {number} index - Index to insert at (negative = from end)
|
|
2307
|
+
* @throws {Error} If feature is invalid
|
|
2308
|
+
*/
|
|
2309
|
+
insertAt(feature, index) {
|
|
2310
|
+
const errors = this._validateFeature(feature);
|
|
2311
|
+
if (errors.length > 0) {
|
|
2312
|
+
throw new Error(`Invalid feature: ${errors.join(', ')}`);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
const features = this._parseFeatures();
|
|
2316
|
+
const idx = this._normalizeIndex(index, features.length, true);
|
|
2317
|
+
|
|
2318
|
+
features.splice(idx, 0, feature);
|
|
2319
|
+
this._setFeatures(features);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
/**
|
|
2323
|
+
* Remove the feature at the specified index
|
|
2324
|
+
* @param {number} index - Index to remove (negative = from end)
|
|
2325
|
+
* @returns {Object|undefined} The removed feature, or undefined if index out of bounds
|
|
2326
|
+
*/
|
|
2327
|
+
removeAt(index) {
|
|
2328
|
+
const features = this._parseFeatures();
|
|
2329
|
+
if (features.length === 0) return undefined;
|
|
2330
|
+
|
|
2331
|
+
const idx = this._normalizeIndex(index, features.length);
|
|
2332
|
+
if (idx === -1) return undefined;
|
|
2333
|
+
|
|
2334
|
+
const removed = features.splice(idx, 1)[0];
|
|
2335
|
+
this._setFeatures(features);
|
|
2336
|
+
return removed;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
/**
|
|
2340
|
+
* Remove all features
|
|
2341
|
+
* @returns {Array} Array of removed features
|
|
2342
|
+
*/
|
|
2343
|
+
removeAll() {
|
|
2344
|
+
const removed = this._parseFeatures();
|
|
2345
|
+
this._setFeatures([]);
|
|
2346
|
+
return removed;
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
/**
|
|
2350
|
+
* Get the feature at the specified index
|
|
2351
|
+
* @param {number} index - Index to get (negative = from end)
|
|
2352
|
+
* @returns {Object|undefined} The feature, or undefined if index out of bounds
|
|
2353
|
+
*/
|
|
2354
|
+
get(index) {
|
|
2355
|
+
const features = this._parseFeatures();
|
|
2356
|
+
if (features.length === 0) return undefined;
|
|
2357
|
+
|
|
2358
|
+
const idx = this._normalizeIndex(index, features.length);
|
|
2359
|
+
if (idx === -1) return undefined;
|
|
2360
|
+
|
|
2361
|
+
return features[idx];
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
/**
|
|
2365
|
+
* Get all features as an array
|
|
2366
|
+
* @returns {Array} Array of all feature objects
|
|
2367
|
+
*/
|
|
2368
|
+
getAll() {
|
|
2369
|
+
return this._parseFeatures();
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* Emit the current document on the change event
|
|
2374
|
+
*/
|
|
2375
|
+
emit() {
|
|
2376
|
+
this.emitChange();
|
|
2377
|
+
}
|
|
2133
2378
|
}
|
|
2134
2379
|
|
|
2135
2380
|
// Register the custom element
|