@softwarity/geojson-editor 1.0.4 → 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 +72 -40
- package/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.js +937 -433
package/src/geojson-editor.js
CHANGED
|
@@ -7,6 +7,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
7
7
|
this.collapsedData = new Map(); // nodeKey -> {originalLines: string[], indent: number}
|
|
8
8
|
this.colorPositions = []; // {line, color}
|
|
9
9
|
this.nodeTogglePositions = []; // {line, nodeKey, isCollapsed, indent}
|
|
10
|
+
this.hiddenFeatures = new Set(); // Set of feature keys (hidden from events)
|
|
11
|
+
this.featureRanges = new Map(); // featureKey -> {startLine, endLine, featureIndex}
|
|
10
12
|
|
|
11
13
|
// Debounce timer for syntax highlighting
|
|
12
14
|
this.highlightTimer = null;
|
|
@@ -15,65 +17,40 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
15
17
|
this._cachedLineHeight = null;
|
|
16
18
|
this._cachedPaddingTop = null;
|
|
17
19
|
|
|
18
|
-
//
|
|
19
|
-
this.themes = {
|
|
20
|
-
dark: { ...GeoJsonEditor.DEFAULT_THEMES.dark },
|
|
21
|
-
light: { ...GeoJsonEditor.DEFAULT_THEMES.light }
|
|
22
|
-
};
|
|
20
|
+
// Custom theme overrides (empty by default, CSS has defaults)
|
|
21
|
+
this.themes = { dark: {}, light: {} };
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
static get observedAttributes() {
|
|
26
|
-
return ['readonly', 'value', 'placeholder', '
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Default theme values
|
|
31
|
-
static DEFAULT_THEMES = {
|
|
32
|
-
dark: {
|
|
33
|
-
background: '#1e1e1e',
|
|
34
|
-
textColor: '#d4d4d4',
|
|
35
|
-
caretColor: '#fff',
|
|
36
|
-
gutterBackground: '#252526',
|
|
37
|
-
gutterBorder: '#3e3e42',
|
|
38
|
-
jsonKey: '#9cdcfe',
|
|
39
|
-
jsonString: '#ce9178',
|
|
40
|
-
jsonNumber: '#b5cea8',
|
|
41
|
-
jsonBoolean: '#569cd6',
|
|
42
|
-
jsonNull: '#569cd6',
|
|
43
|
-
jsonPunctuation: '#d4d4d4',
|
|
44
|
-
controlColor: '#c586c0',
|
|
45
|
-
controlBg: '#3e3e42',
|
|
46
|
-
controlBorder: '#555',
|
|
47
|
-
geojsonKey: '#c586c0',
|
|
48
|
-
geojsonType: '#4ec9b0',
|
|
49
|
-
geojsonTypeInvalid: '#f44747',
|
|
50
|
-
jsonKeyInvalid: '#f44747'
|
|
51
|
-
},
|
|
52
|
-
light: {
|
|
53
|
-
background: '#ffffff',
|
|
54
|
-
textColor: '#333333',
|
|
55
|
-
caretColor: '#000',
|
|
56
|
-
gutterBackground: '#f5f5f5',
|
|
57
|
-
gutterBorder: '#ddd',
|
|
58
|
-
jsonKey: '#0000ff',
|
|
59
|
-
jsonString: '#a31515',
|
|
60
|
-
jsonNumber: '#098658',
|
|
61
|
-
jsonBoolean: '#0000ff',
|
|
62
|
-
jsonNull: '#0000ff',
|
|
63
|
-
jsonPunctuation: '#333333',
|
|
64
|
-
controlColor: '#a31515',
|
|
65
|
-
controlBg: '#e0e0e0',
|
|
66
|
-
controlBorder: '#999',
|
|
67
|
-
geojsonKey: '#af00db',
|
|
68
|
-
geojsonType: '#267f99',
|
|
69
|
-
geojsonTypeInvalid: '#d32f2f',
|
|
70
|
-
jsonKeyInvalid: '#d32f2f'
|
|
71
|
-
}
|
|
72
|
-
};
|
|
25
|
+
return ['readonly', 'value', 'placeholder', 'dark-selector'];
|
|
26
|
+
}
|
|
73
27
|
|
|
74
|
-
//
|
|
75
|
-
static
|
|
76
|
-
|
|
28
|
+
// Helper: Convert camelCase to kebab-case
|
|
29
|
+
static _toKebabCase(str) {
|
|
30
|
+
return str.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
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
|
+
};
|
|
77
54
|
|
|
78
55
|
// Pre-compiled regex patterns (avoid recompilation on each call)
|
|
79
56
|
static REGEX = {
|
|
@@ -91,7 +68,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
91
68
|
allNumbers: /\b(-?\d+\.?\d*)\b/g,
|
|
92
69
|
punctuation: /([{}[\],])/g,
|
|
93
70
|
// Highlighting detection
|
|
94
|
-
colorInLine: /"(\w+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,
|
|
71
|
+
colorInLine: /"([\w-]+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,
|
|
95
72
|
collapsibleNode: /^(\s*)"(\w+)"\s*:\s*([{\[])/,
|
|
96
73
|
collapsedMarker: /^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/
|
|
97
74
|
};
|
|
@@ -113,6 +90,21 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
113
90
|
this.updatePlaceholderContent();
|
|
114
91
|
}
|
|
115
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
|
+
|
|
116
108
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
117
109
|
if (oldValue === newValue) return;
|
|
118
110
|
|
|
@@ -124,15 +116,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
124
116
|
this.updatePlaceholderContent();
|
|
125
117
|
} else if (name === 'dark-selector') {
|
|
126
118
|
this.updateThemeCSS();
|
|
127
|
-
} else if (name === 'feature-collection') {
|
|
128
|
-
this.updatePrefixSuffix();
|
|
129
|
-
} else if (name === 'auto-format') {
|
|
130
|
-
// When auto-format is enabled, format the current content
|
|
131
|
-
const textarea = this.shadowRoot?.getElementById('textarea');
|
|
132
|
-
if (textarea && textarea.value && this.autoFormat) {
|
|
133
|
-
this.autoFormatContent();
|
|
134
|
-
this.updateHighlight();
|
|
135
|
-
}
|
|
136
119
|
}
|
|
137
120
|
}
|
|
138
121
|
|
|
@@ -150,21 +133,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
150
133
|
return this.getAttribute('placeholder') || '';
|
|
151
134
|
}
|
|
152
135
|
|
|
153
|
-
|
|
154
|
-
return this.hasAttribute('auto-format');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
get featureCollection() {
|
|
158
|
-
return this.hasAttribute('feature-collection');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Internal getters for prefix/suffix based on feature-collection mode
|
|
136
|
+
// Always in FeatureCollection mode - prefix/suffix are constant
|
|
162
137
|
get prefix() {
|
|
163
|
-
return
|
|
138
|
+
return '{"type": "FeatureCollection", "features": [';
|
|
164
139
|
}
|
|
165
|
-
|
|
140
|
+
|
|
166
141
|
get suffix() {
|
|
167
|
-
return
|
|
142
|
+
return ']}';
|
|
168
143
|
}
|
|
169
144
|
|
|
170
145
|
render() {
|
|
@@ -194,9 +169,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
194
169
|
position: relative;
|
|
195
170
|
width: 100%;
|
|
196
171
|
height: 400px;
|
|
197
|
-
font-family: 'Courier New', Courier, monospace;
|
|
198
|
-
font-size: 13px;
|
|
199
|
-
line-height: 1.5;
|
|
200
172
|
border-radius: 4px;
|
|
201
173
|
overflow: hidden;
|
|
202
174
|
}
|
|
@@ -227,18 +199,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
227
199
|
position: relative;
|
|
228
200
|
width: 100%;
|
|
229
201
|
flex: 1;
|
|
230
|
-
background: var(--bg-color);
|
|
202
|
+
background: var(--bg-color, #ffffff);
|
|
231
203
|
display: flex;
|
|
232
|
-
font-family: 'Courier New', Courier, monospace;
|
|
233
|
-
font-size: 13px;
|
|
234
|
-
line-height: 1.5;
|
|
235
204
|
}
|
|
236
205
|
|
|
237
206
|
.gutter {
|
|
238
207
|
width: 24px;
|
|
239
208
|
height: 100%;
|
|
240
|
-
background: var(--gutter-bg);
|
|
241
|
-
border-right: 1px solid var(--gutter-border);
|
|
209
|
+
background: var(--gutter-bg, #f0f0f0);
|
|
210
|
+
border-right: 1px solid var(--gutter-border, #e0e0e0);
|
|
242
211
|
overflow: hidden;
|
|
243
212
|
flex-shrink: 0;
|
|
244
213
|
position: relative;
|
|
@@ -280,10 +249,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
280
249
|
.collapse-button {
|
|
281
250
|
width: 12px;
|
|
282
251
|
height: 12px;
|
|
283
|
-
background: var(--control-bg);
|
|
284
|
-
border: 1px solid var(--control-border);
|
|
252
|
+
background: var(--control-bg, #e8e8e8);
|
|
253
|
+
border: 1px solid var(--control-border, #c0c0c0);
|
|
285
254
|
border-radius: 2px;
|
|
286
|
-
color: var(--control-color);
|
|
255
|
+
color: var(--control-color, #000080);
|
|
287
256
|
font-size: 8px;
|
|
288
257
|
font-weight: bold;
|
|
289
258
|
cursor: pointer;
|
|
@@ -296,11 +265,42 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
296
265
|
}
|
|
297
266
|
|
|
298
267
|
.collapse-button:hover {
|
|
299
|
-
background: var(--control-bg);
|
|
300
|
-
border-color: var(--control-color);
|
|
268
|
+
background: var(--control-bg, #e8e8e8);
|
|
269
|
+
border-color: var(--control-color, #000080);
|
|
301
270
|
transform: scale(1.1);
|
|
302
271
|
}
|
|
303
272
|
|
|
273
|
+
.visibility-button {
|
|
274
|
+
width: 14px;
|
|
275
|
+
height: 14px;
|
|
276
|
+
background: transparent;
|
|
277
|
+
border: none;
|
|
278
|
+
cursor: pointer;
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
justify-content: center;
|
|
282
|
+
transition: all 0.1s;
|
|
283
|
+
flex-shrink: 0;
|
|
284
|
+
opacity: 0.7;
|
|
285
|
+
padding: 0;
|
|
286
|
+
font-size: 11px;
|
|
287
|
+
}
|
|
288
|
+
|
|
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
304
|
.color-picker-popup {
|
|
305
305
|
position: absolute;
|
|
306
306
|
background: #2d2d30;
|
|
@@ -331,17 +331,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
331
331
|
width: 100%;
|
|
332
332
|
height: 100%;
|
|
333
333
|
padding: 8px 12px;
|
|
334
|
-
font-family: 'Courier New', Courier, monospace;
|
|
335
|
-
font-size: 13px;
|
|
336
|
-
font-weight: normal;
|
|
337
|
-
font-style: normal;
|
|
338
|
-
line-height: 1.5;
|
|
339
334
|
white-space: pre-wrap;
|
|
340
335
|
word-wrap: break-word;
|
|
341
336
|
overflow: auto;
|
|
342
337
|
pointer-events: none;
|
|
343
338
|
z-index: 1;
|
|
344
|
-
color: var(--text-color);
|
|
339
|
+
color: var(--text-color, #000000);
|
|
345
340
|
}
|
|
346
341
|
|
|
347
342
|
.highlight-layer::-webkit-scrollbar {
|
|
@@ -360,18 +355,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
360
355
|
outline: none;
|
|
361
356
|
background: transparent;
|
|
362
357
|
color: transparent;
|
|
363
|
-
caret-color: var(--caret-color);
|
|
364
|
-
font-family: 'Courier New', Courier, monospace;
|
|
365
|
-
font-size: 13px;
|
|
366
|
-
font-weight: normal;
|
|
367
|
-
font-style: normal;
|
|
368
|
-
line-height: 1.5;
|
|
358
|
+
caret-color: var(--caret-color, #000);
|
|
369
359
|
white-space: pre-wrap;
|
|
370
360
|
word-wrap: break-word;
|
|
371
361
|
resize: none;
|
|
372
362
|
overflow: auto;
|
|
373
363
|
z-index: 2;
|
|
374
|
-
box-sizing: border-box;
|
|
375
364
|
}
|
|
376
365
|
|
|
377
366
|
textarea::selection {
|
|
@@ -389,11 +378,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
389
378
|
width: 100%;
|
|
390
379
|
height: 100%;
|
|
391
380
|
padding: 8px 12px;
|
|
392
|
-
font-family: 'Courier New', Courier, monospace;
|
|
393
|
-
font-size: 13px;
|
|
394
|
-
font-weight: normal;
|
|
395
|
-
font-style: normal;
|
|
396
|
-
line-height: 1.5;
|
|
397
381
|
white-space: pre-wrap;
|
|
398
382
|
word-wrap: break-word;
|
|
399
383
|
color: #6a6a6a;
|
|
@@ -407,74 +391,116 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
407
391
|
opacity: 0.6;
|
|
408
392
|
}
|
|
409
393
|
|
|
410
|
-
/* Syntax highlighting colors */
|
|
394
|
+
/* Syntax highlighting colors - IntelliJ Light defaults */
|
|
411
395
|
.json-key {
|
|
412
|
-
color: var(--json-key);
|
|
396
|
+
color: var(--json-key, #660e7a);
|
|
413
397
|
}
|
|
414
398
|
|
|
415
399
|
.json-string {
|
|
416
|
-
color: var(--json-string);
|
|
400
|
+
color: var(--json-string, #008000);
|
|
417
401
|
}
|
|
418
402
|
|
|
419
403
|
.json-number {
|
|
420
|
-
color: var(--json-number);
|
|
404
|
+
color: var(--json-number, #0000ff);
|
|
421
405
|
}
|
|
422
406
|
|
|
423
407
|
.json-boolean {
|
|
424
|
-
color: var(--json-boolean);
|
|
408
|
+
color: var(--json-boolean, #000080);
|
|
425
409
|
}
|
|
426
410
|
|
|
427
411
|
.json-null {
|
|
428
|
-
color: var(--json-null);
|
|
412
|
+
color: var(--json-null, #000080);
|
|
429
413
|
}
|
|
430
414
|
|
|
431
415
|
.json-punctuation {
|
|
432
|
-
color: var(--json-punct);
|
|
416
|
+
color: var(--json-punct, #000000);
|
|
433
417
|
}
|
|
434
418
|
|
|
435
419
|
/* GeoJSON-specific highlighting */
|
|
436
420
|
.geojson-key {
|
|
437
|
-
color: var(--geojson-key);
|
|
421
|
+
color: var(--geojson-key, #660e7a);
|
|
438
422
|
font-weight: 600;
|
|
439
423
|
}
|
|
440
424
|
|
|
441
425
|
.geojson-type {
|
|
442
|
-
color: var(--geojson-type);
|
|
426
|
+
color: var(--geojson-type, #008000);
|
|
443
427
|
font-weight: 600;
|
|
444
428
|
}
|
|
445
429
|
|
|
446
430
|
.geojson-type-invalid {
|
|
447
|
-
color: var(--geojson-type-invalid);
|
|
431
|
+
color: var(--geojson-type-invalid, #ff0000);
|
|
448
432
|
font-weight: 600;
|
|
449
433
|
}
|
|
450
434
|
|
|
451
435
|
.json-key-invalid {
|
|
452
|
-
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;
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
/* Prefix and suffix styling */
|
|
456
455
|
.editor-prefix,
|
|
457
456
|
.editor-suffix {
|
|
457
|
+
flex: 1;
|
|
458
458
|
padding: 4px 12px;
|
|
459
|
-
color: var(--text-color);
|
|
460
|
-
background: var(--bg-color);
|
|
459
|
+
color: var(--text-color, #000000);
|
|
460
|
+
background: var(--bg-color, #ffffff);
|
|
461
461
|
user-select: none;
|
|
462
462
|
white-space: pre-wrap;
|
|
463
463
|
word-wrap: break-word;
|
|
464
|
-
flex-shrink: 0;
|
|
465
|
-
font-family: 'Courier New', Courier, monospace;
|
|
466
|
-
font-size: 13px;
|
|
467
|
-
line-height: 1.5;
|
|
468
464
|
opacity: 0.6;
|
|
469
|
-
border-left: 3px solid rgba(102, 126, 234, 0.5);
|
|
470
465
|
}
|
|
471
466
|
|
|
472
|
-
.
|
|
467
|
+
.prefix-wrapper {
|
|
473
468
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
474
469
|
}
|
|
475
470
|
|
|
476
|
-
.
|
|
471
|
+
.suffix-wrapper {
|
|
477
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;
|
|
478
504
|
}
|
|
479
505
|
|
|
480
506
|
/* Scrollbar styling - WebKit (Chrome, Safari, Edge) */
|
|
@@ -484,28 +510,31 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
484
510
|
}
|
|
485
511
|
|
|
486
512
|
textarea::-webkit-scrollbar-track {
|
|
487
|
-
background: var(--control-bg);
|
|
513
|
+
background: var(--control-bg, #e8e8e8);
|
|
488
514
|
}
|
|
489
515
|
|
|
490
516
|
textarea::-webkit-scrollbar-thumb {
|
|
491
|
-
background: var(--control-border);
|
|
517
|
+
background: var(--control-border, #c0c0c0);
|
|
492
518
|
border-radius: 5px;
|
|
493
519
|
}
|
|
494
520
|
|
|
495
521
|
textarea::-webkit-scrollbar-thumb:hover {
|
|
496
|
-
background: var(--control-color);
|
|
522
|
+
background: var(--control-color, #000080);
|
|
497
523
|
}
|
|
498
524
|
|
|
499
525
|
/* Scrollbar styling - Firefox */
|
|
500
526
|
textarea {
|
|
501
527
|
scrollbar-width: thin;
|
|
502
|
-
scrollbar-color: var(--control-border) var(--control-bg);
|
|
528
|
+
scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8);
|
|
503
529
|
}
|
|
504
530
|
</style>
|
|
505
531
|
`;
|
|
506
532
|
|
|
507
533
|
const template = `
|
|
508
|
-
<div class="
|
|
534
|
+
<div class="prefix-wrapper">
|
|
535
|
+
<div class="prefix-gutter"></div>
|
|
536
|
+
<div class="editor-prefix" id="editorPrefix"></div>
|
|
537
|
+
</div>
|
|
509
538
|
<div class="editor-wrapper">
|
|
510
539
|
<div class="gutter">
|
|
511
540
|
<div class="gutter-content" id="gutterContent"></div>
|
|
@@ -522,7 +551,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
522
551
|
></textarea>
|
|
523
552
|
</div>
|
|
524
553
|
</div>
|
|
525
|
-
<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>
|
|
526
559
|
`;
|
|
527
560
|
|
|
528
561
|
this.shadowRoot.innerHTML = styles + template;
|
|
@@ -546,10 +579,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
546
579
|
|
|
547
580
|
clearTimeout(this.highlightTimer);
|
|
548
581
|
this.highlightTimer = setTimeout(() => {
|
|
549
|
-
// Auto-format
|
|
550
|
-
|
|
551
|
-
this.autoFormatContentWithCursor();
|
|
552
|
-
}
|
|
582
|
+
// Auto-format JSON content
|
|
583
|
+
this.autoFormatContentWithCursor();
|
|
553
584
|
this.updateHighlight();
|
|
554
585
|
this.emitChange();
|
|
555
586
|
}, 150);
|
|
@@ -563,10 +594,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
563
594
|
// Use a short delay to let the paste complete
|
|
564
595
|
setTimeout(() => {
|
|
565
596
|
this.updatePlaceholderVisibility();
|
|
566
|
-
// Auto-format
|
|
567
|
-
|
|
568
|
-
this.autoFormatContentWithCursor();
|
|
569
|
-
}
|
|
597
|
+
// Auto-format JSON content
|
|
598
|
+
this.autoFormatContentWithCursor();
|
|
570
599
|
this.updateHighlight();
|
|
571
600
|
this.emitChange();
|
|
572
601
|
// Auto-collapse coordinates after paste
|
|
@@ -577,6 +606,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
577
606
|
// Gutter clicks (color indicators and collapse buttons)
|
|
578
607
|
const gutterContent = this.shadowRoot.getElementById('gutterContent');
|
|
579
608
|
gutterContent.addEventListener('click', (e) => {
|
|
609
|
+
// Check for visibility button (may click on SVG inside button)
|
|
610
|
+
const visibilityButton = e.target.closest('.visibility-button');
|
|
611
|
+
if (visibilityButton) {
|
|
612
|
+
const featureKey = visibilityButton.dataset.featureKey;
|
|
613
|
+
this.toggleFeatureVisibility(featureKey);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
580
617
|
if (e.target.classList.contains('color-indicator')) {
|
|
581
618
|
const line = parseInt(e.target.dataset.line);
|
|
582
619
|
const color = e.target.dataset.color;
|
|
@@ -611,6 +648,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
611
648
|
this.handleCutWithCollapsedContent(e);
|
|
612
649
|
});
|
|
613
650
|
|
|
651
|
+
// Clear button
|
|
652
|
+
const clearBtn = this.shadowRoot.getElementById('clearBtn');
|
|
653
|
+
clearBtn.addEventListener('click', () => {
|
|
654
|
+
this.removeAll();
|
|
655
|
+
});
|
|
656
|
+
|
|
614
657
|
// Update readonly state
|
|
615
658
|
this.updateReadonly();
|
|
616
659
|
}
|
|
@@ -625,14 +668,20 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
625
668
|
if (textarea) {
|
|
626
669
|
textarea.disabled = this.readonly;
|
|
627
670
|
}
|
|
671
|
+
// Hide clear button in readonly mode
|
|
672
|
+
const clearBtn = this.shadowRoot.getElementById('clearBtn');
|
|
673
|
+
if (clearBtn) {
|
|
674
|
+
clearBtn.hidden = this.readonly;
|
|
675
|
+
}
|
|
628
676
|
}
|
|
629
677
|
|
|
630
678
|
escapeHtml(text) {
|
|
631
679
|
if (!text) return '';
|
|
680
|
+
const R = GeoJsonEditor.REGEX;
|
|
632
681
|
return text
|
|
633
|
-
.replace(
|
|
634
|
-
.replace(
|
|
635
|
-
.replace(
|
|
682
|
+
.replace(R.ampersand, '&')
|
|
683
|
+
.replace(R.lessThan, '<')
|
|
684
|
+
.replace(R.greaterThan, '>');
|
|
636
685
|
}
|
|
637
686
|
|
|
638
687
|
updatePlaceholderVisibility() {
|
|
@@ -656,8 +705,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
656
705
|
if (textarea && textarea.value !== newValue) {
|
|
657
706
|
textarea.value = newValue || '';
|
|
658
707
|
|
|
659
|
-
//
|
|
660
|
-
if (
|
|
708
|
+
// Auto-format JSON content
|
|
709
|
+
if (newValue) {
|
|
661
710
|
try {
|
|
662
711
|
const prefix = this.prefix;
|
|
663
712
|
const suffix = this.suffix;
|
|
@@ -709,24 +758,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
709
758
|
const prefixEl = this.shadowRoot.getElementById('editorPrefix');
|
|
710
759
|
const suffixEl = this.shadowRoot.getElementById('editorSuffix');
|
|
711
760
|
|
|
761
|
+
// Always show prefix/suffix (always in FeatureCollection mode)
|
|
712
762
|
if (prefixEl) {
|
|
713
|
-
|
|
714
|
-
prefixEl.textContent = this.prefix;
|
|
715
|
-
prefixEl.style.display = 'block';
|
|
716
|
-
} else {
|
|
717
|
-
prefixEl.textContent = '';
|
|
718
|
-
prefixEl.style.display = 'none';
|
|
719
|
-
}
|
|
763
|
+
prefixEl.textContent = this.prefix;
|
|
720
764
|
}
|
|
721
765
|
|
|
722
766
|
if (suffixEl) {
|
|
723
|
-
|
|
724
|
-
suffixEl.textContent = this.suffix;
|
|
725
|
-
suffixEl.style.display = 'block';
|
|
726
|
-
} else {
|
|
727
|
-
suffixEl.textContent = '';
|
|
728
|
-
suffixEl.style.display = 'none';
|
|
729
|
-
}
|
|
767
|
+
suffixEl.textContent = this.suffix;
|
|
730
768
|
}
|
|
731
769
|
}
|
|
732
770
|
|
|
@@ -738,8 +776,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
738
776
|
|
|
739
777
|
const text = textarea.value;
|
|
740
778
|
|
|
779
|
+
// Update feature ranges for visibility tracking
|
|
780
|
+
this.updateFeatureRanges();
|
|
781
|
+
|
|
782
|
+
// Get hidden line ranges
|
|
783
|
+
const hiddenRanges = this.getHiddenLineRanges();
|
|
784
|
+
|
|
741
785
|
// Parse and highlight
|
|
742
|
-
const { highlighted, colors, toggles } = this.highlightJSON(text);
|
|
786
|
+
const { highlighted, colors, toggles } = this.highlightJSON(text, hiddenRanges);
|
|
743
787
|
|
|
744
788
|
highlightLayer.innerHTML = highlighted;
|
|
745
789
|
this.colorPositions = colors;
|
|
@@ -749,7 +793,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
749
793
|
this.updateGutter();
|
|
750
794
|
}
|
|
751
795
|
|
|
752
|
-
highlightJSON(text) {
|
|
796
|
+
highlightJSON(text, hiddenRanges = []) {
|
|
753
797
|
if (!text.trim()) {
|
|
754
798
|
return { highlighted: '', colors: [], toggles: [] };
|
|
755
799
|
}
|
|
@@ -762,6 +806,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
762
806
|
// Build context map for validation
|
|
763
807
|
const contextMap = this.buildContextMap(text);
|
|
764
808
|
|
|
809
|
+
// Helper to check if a line is in a hidden range
|
|
810
|
+
const isLineHidden = (lineIndex) => {
|
|
811
|
+
return hiddenRanges.some(range => lineIndex >= range.startLine && lineIndex <= range.endLine);
|
|
812
|
+
};
|
|
813
|
+
|
|
765
814
|
lines.forEach((line, lineIndex) => {
|
|
766
815
|
// Detect any hex color (6 digits) in string values
|
|
767
816
|
const R = GeoJsonEditor.REGEX;
|
|
@@ -804,7 +853,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
804
853
|
|
|
805
854
|
// Highlight the line with context
|
|
806
855
|
const context = contextMap.get(lineIndex);
|
|
807
|
-
|
|
856
|
+
let highlightedLine = this.highlightSyntax(line, context);
|
|
857
|
+
|
|
858
|
+
// Wrap hidden lines with .line-hidden class
|
|
859
|
+
if (isLineHidden(lineIndex)) {
|
|
860
|
+
highlightedLine = `<span class="line-hidden">${highlightedLine}</span>`;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
highlightedLines.push(highlightedLine);
|
|
808
864
|
});
|
|
809
865
|
|
|
810
866
|
return {
|
|
@@ -849,8 +905,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
849
905
|
const contextStack = []; // Stack of {context, isArray}
|
|
850
906
|
let pendingContext = null; // Context for next object/array
|
|
851
907
|
|
|
852
|
-
//
|
|
853
|
-
const rootContext =
|
|
908
|
+
// Root context is always 'Feature' (always in FeatureCollection mode)
|
|
909
|
+
const rootContext = 'Feature';
|
|
854
910
|
|
|
855
911
|
for (let i = 0; i < lines.length; i++) {
|
|
856
912
|
const line = lines[i];
|
|
@@ -862,37 +918,61 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
862
918
|
contextMap.set(i, lineContext);
|
|
863
919
|
|
|
864
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
|
+
|
|
865
925
|
for (let j = 0; j < line.length; j++) {
|
|
866
926
|
const char = line[j];
|
|
867
927
|
|
|
868
|
-
//
|
|
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
|
|
869
939
|
if (char === '"') {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const
|
|
873
|
-
if (
|
|
874
|
-
|
|
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;
|
|
875
950
|
}
|
|
876
|
-
j += keyMatch[0].length - 1; // Skip past the key
|
|
877
|
-
continue;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
951
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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;
|
|
891
966
|
}
|
|
892
967
|
}
|
|
893
968
|
}
|
|
969
|
+
inString = !inString;
|
|
970
|
+
continue;
|
|
894
971
|
}
|
|
895
972
|
|
|
973
|
+
// Skip everything inside strings (brackets, etc.)
|
|
974
|
+
if (inString) continue;
|
|
975
|
+
|
|
896
976
|
// Opening bracket - push context
|
|
897
977
|
if (char === '{' || char === '[') {
|
|
898
978
|
let newContext;
|
|
@@ -900,10 +980,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
900
980
|
newContext = pendingContext;
|
|
901
981
|
pendingContext = null;
|
|
902
982
|
} else if (contextStack.length === 0) {
|
|
903
|
-
// Root level
|
|
904
983
|
newContext = rootContext;
|
|
905
984
|
} else {
|
|
906
|
-
// Inherit from parent if in array
|
|
907
985
|
const parent = contextStack[contextStack.length - 1];
|
|
908
986
|
if (parent && parent.isArray) {
|
|
909
987
|
newContext = parent.context;
|
|
@@ -967,7 +1045,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
967
1045
|
.replace(R.lessThan, '<')
|
|
968
1046
|
.replace(R.greaterThan, '>')
|
|
969
1047
|
// All JSON keys - validate against context
|
|
970
|
-
.replace(R.jsonKey, (
|
|
1048
|
+
.replace(R.jsonKey, (_, key) => {
|
|
971
1049
|
// Inside properties - all keys are regular user keys
|
|
972
1050
|
if (context === 'properties') {
|
|
973
1051
|
return `<span class="json-key">"${key}"</span>:`;
|
|
@@ -984,7 +1062,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
984
1062
|
}
|
|
985
1063
|
})
|
|
986
1064
|
// GeoJSON "type" values - validate based on context
|
|
987
|
-
.replace(R.typeValue, (
|
|
1065
|
+
.replace(R.typeValue, (_, typeValue) => {
|
|
988
1066
|
if (isTypeValid(typeValue)) {
|
|
989
1067
|
return `<span class="geojson-key">"type"</span>: <span class="geojson-type">"${typeValue}"</span>`;
|
|
990
1068
|
} else {
|
|
@@ -1056,48 +1134,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1056
1134
|
|
|
1057
1135
|
const indent = match[1];
|
|
1058
1136
|
const openBracket = match[3];
|
|
1059
|
-
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
1060
|
-
|
|
1061
|
-
// Check if bracket closes on same line - can't collapse
|
|
1062
|
-
if (this.bracketClosesOnSameLine(currentLine, openBracket)) return;
|
|
1063
1137
|
|
|
1064
|
-
//
|
|
1065
|
-
|
|
1066
|
-
let endLine = line;
|
|
1067
|
-
const content = [];
|
|
1068
|
-
|
|
1069
|
-
for (let i = line + 1; i < lines.length; i++) {
|
|
1070
|
-
const scanLine = lines[i];
|
|
1071
|
-
|
|
1072
|
-
for (const char of scanLine) {
|
|
1073
|
-
if (char === openBracket) depth++;
|
|
1074
|
-
if (char === closeBracket) depth--;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
content.push(scanLine);
|
|
1078
|
-
|
|
1079
|
-
if (depth === 0) {
|
|
1080
|
-
endLine = i;
|
|
1081
|
-
break;
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// Store the original data with unique key
|
|
1086
|
-
const uniqueKey = `${line}-${nodeKey}`;
|
|
1087
|
-
this.collapsedData.set(uniqueKey, {
|
|
1088
|
-
originalLine: currentLine,
|
|
1089
|
-
content: content,
|
|
1090
|
-
indent: indent.length,
|
|
1091
|
-
nodeKey: nodeKey // Store nodeKey for later use
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
// Replace with marker
|
|
1095
|
-
const beforeBracket = currentLine.substring(0, currentLine.indexOf(openBracket));
|
|
1096
|
-
const hasTrailingComma = lines[endLine] && lines[endLine].trim().endsWith(',');
|
|
1097
|
-
lines[line] = `${beforeBracket}${openBracket}...${closeBracket}${hasTrailingComma ? ',' : ''}`;
|
|
1098
|
-
|
|
1099
|
-
// Remove content lines
|
|
1100
|
-
lines.splice(line + 1, endLine - line);
|
|
1138
|
+
// Use common collapse helper
|
|
1139
|
+
if (this._performCollapse(lines, line, nodeKey, indent, openBracket) === 0) return;
|
|
1101
1140
|
}
|
|
1102
1141
|
|
|
1103
1142
|
// Update textarea
|
|
@@ -1123,48 +1162,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1123
1162
|
if (nodeKey === 'coordinates') {
|
|
1124
1163
|
const indent = match[1];
|
|
1125
1164
|
const openBracket = match[3];
|
|
1126
|
-
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
1127
|
-
|
|
1128
|
-
// Skip if bracket closes on same line
|
|
1129
|
-
if (this.bracketClosesOnSameLine(line, openBracket)) continue;
|
|
1130
1165
|
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
let endLine = i;
|
|
1134
|
-
const content = [];
|
|
1135
|
-
|
|
1136
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
1137
|
-
const scanLine = lines[j];
|
|
1138
|
-
|
|
1139
|
-
for (const char of scanLine) {
|
|
1140
|
-
if (char === openBracket) depth++;
|
|
1141
|
-
if (char === closeBracket) depth--;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
content.push(scanLine);
|
|
1145
|
-
|
|
1146
|
-
if (depth === 0) {
|
|
1147
|
-
endLine = j;
|
|
1148
|
-
break;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// Store the original data with unique key
|
|
1153
|
-
const uniqueKey = `${i}-${nodeKey}`;
|
|
1154
|
-
this.collapsedData.set(uniqueKey, {
|
|
1155
|
-
originalLine: line,
|
|
1156
|
-
content: content,
|
|
1157
|
-
indent: indent.length,
|
|
1158
|
-
nodeKey: nodeKey
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
// Replace with marker
|
|
1162
|
-
const beforeBracket = line.substring(0, line.indexOf(openBracket));
|
|
1163
|
-
const hasTrailingComma = lines[endLine] && lines[endLine].trim().endsWith(',');
|
|
1164
|
-
lines[i] = `${beforeBracket}${openBracket}...${closeBracket}${hasTrailingComma ? ',' : ''}`;
|
|
1165
|
-
|
|
1166
|
-
// Remove content lines
|
|
1167
|
-
lines.splice(i + 1, endLine - i);
|
|
1166
|
+
// Use common collapse helper
|
|
1167
|
+
this._performCollapse(lines, i, nodeKey, indent, openBracket);
|
|
1168
1168
|
}
|
|
1169
1169
|
}
|
|
1170
1170
|
}
|
|
@@ -1193,25 +1193,33 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1193
1193
|
// Clear gutter
|
|
1194
1194
|
gutterContent.textContent = '';
|
|
1195
1195
|
|
|
1196
|
-
// Create a map of line -> elements (color, collapse button,
|
|
1196
|
+
// Create a map of line -> elements (color, collapse button, visibility button)
|
|
1197
1197
|
const lineElements = new Map();
|
|
1198
1198
|
|
|
1199
|
-
//
|
|
1200
|
-
|
|
1199
|
+
// Helper to ensure line entry exists
|
|
1200
|
+
const ensureLine = (line) => {
|
|
1201
1201
|
if (!lineElements.has(line)) {
|
|
1202
|
-
lineElements.set(line, { colors: [], buttons: [] });
|
|
1202
|
+
lineElements.set(line, { colors: [], buttons: [], visibilityButtons: [] });
|
|
1203
1203
|
}
|
|
1204
|
-
lineElements.get(line)
|
|
1204
|
+
return lineElements.get(line);
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// Add color indicators
|
|
1208
|
+
this.colorPositions.forEach(({ line, color, attributeName }) => {
|
|
1209
|
+
ensureLine(line).colors.push({ color, attributeName });
|
|
1205
1210
|
});
|
|
1206
1211
|
|
|
1207
1212
|
// Add collapse buttons
|
|
1208
1213
|
this.nodeTogglePositions.forEach(({ line, nodeKey, isCollapsed }) => {
|
|
1209
|
-
|
|
1210
|
-
lineElements.set(line, { colors: [], buttons: [] });
|
|
1211
|
-
}
|
|
1212
|
-
lineElements.get(line).buttons.push({ nodeKey, isCollapsed });
|
|
1214
|
+
ensureLine(line).buttons.push({ nodeKey, isCollapsed });
|
|
1213
1215
|
});
|
|
1214
1216
|
|
|
1217
|
+
// Add visibility buttons for Features (on the opening brace line)
|
|
1218
|
+
for (const [featureKey, range] of this.featureRanges) {
|
|
1219
|
+
const isHidden = this.hiddenFeatures.has(featureKey);
|
|
1220
|
+
ensureLine(range.startLine).visibilityButtons.push({ featureKey, isHidden });
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1215
1223
|
// Create gutter lines with DocumentFragment (single DOM update)
|
|
1216
1224
|
const fragment = document.createDocumentFragment();
|
|
1217
1225
|
|
|
@@ -1220,6 +1228,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1220
1228
|
gutterLine.className = 'gutter-line';
|
|
1221
1229
|
gutterLine.style.top = `${paddingTop + line * lineHeight}px`;
|
|
1222
1230
|
|
|
1231
|
+
// Add visibility buttons first (leftmost)
|
|
1232
|
+
elements.visibilityButtons.forEach(({ featureKey, isHidden }) => {
|
|
1233
|
+
const button = document.createElement('button');
|
|
1234
|
+
button.className = 'visibility-button' + (isHidden ? ' hidden' : '');
|
|
1235
|
+
button.textContent = '👁';
|
|
1236
|
+
button.dataset.featureKey = featureKey;
|
|
1237
|
+
button.title = isHidden ? 'Show feature in events' : 'Hide feature from events';
|
|
1238
|
+
gutterLine.appendChild(button);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1223
1241
|
// Add color indicators
|
|
1224
1242
|
elements.colors.forEach(({ color, attributeName }) => {
|
|
1225
1243
|
const indicator = document.createElement('div');
|
|
@@ -1251,9 +1269,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1251
1269
|
}
|
|
1252
1270
|
|
|
1253
1271
|
showColorPicker(indicator, line, currentColor, attributeName) {
|
|
1254
|
-
// Remove existing picker
|
|
1272
|
+
// Remove existing picker and clean up its listener
|
|
1255
1273
|
const existing = document.querySelector('.geojson-color-picker-input');
|
|
1256
|
-
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
|
+
}
|
|
1257
1281
|
|
|
1258
1282
|
// Create small color input positioned at the indicator
|
|
1259
1283
|
const colorInput = document.createElement('input');
|
|
@@ -1287,11 +1311,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1287
1311
|
// Close picker when clicking anywhere else
|
|
1288
1312
|
const closeOnClickOutside = (e) => {
|
|
1289
1313
|
if (e.target !== colorInput && !colorInput.contains(e.target)) {
|
|
1290
|
-
colorInput.remove();
|
|
1291
1314
|
document.removeEventListener('click', closeOnClickOutside, true);
|
|
1315
|
+
colorInput.remove();
|
|
1292
1316
|
}
|
|
1293
1317
|
};
|
|
1294
1318
|
|
|
1319
|
+
// Store the listener reference on the element for cleanup
|
|
1320
|
+
colorInput._closeListener = closeOnClickOutside;
|
|
1321
|
+
|
|
1295
1322
|
// Add to document body with fixed positioning
|
|
1296
1323
|
document.body.appendChild(colorInput);
|
|
1297
1324
|
|
|
@@ -1353,8 +1380,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1353
1380
|
return; // No collapsed content, use default copy behavior
|
|
1354
1381
|
}
|
|
1355
1382
|
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
+
}
|
|
1358
1392
|
|
|
1359
1393
|
// Put expanded text in clipboard
|
|
1360
1394
|
e.preventDefault();
|
|
@@ -1365,6 +1399,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1365
1399
|
const textarea = this.shadowRoot.getElementById('textarea');
|
|
1366
1400
|
const beforeSelection = textarea.value.substring(0, startPos);
|
|
1367
1401
|
const startLineNum = beforeSelection.split('\n').length - 1;
|
|
1402
|
+
const R = GeoJsonEditor.REGEX;
|
|
1368
1403
|
|
|
1369
1404
|
const lines = text.split('\n');
|
|
1370
1405
|
const expandedLines = [];
|
|
@@ -1374,17 +1409,43 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1374
1409
|
|
|
1375
1410
|
// Check if this line has a collapsed marker
|
|
1376
1411
|
if (line.includes('{...}') || line.includes('[...]')) {
|
|
1377
|
-
|
|
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
|
|
1378
1439
|
let found = false;
|
|
1379
|
-
this.collapsedData.
|
|
1440
|
+
for (const [key, collapsed] of this.collapsedData.entries()) {
|
|
1380
1441
|
const collapsedLineNum = parseInt(key.split('-')[0]);
|
|
1381
1442
|
if (collapsedLineNum === absoluteLineNum) {
|
|
1382
|
-
// Replace with original line and all collapsed content
|
|
1383
1443
|
expandedLines.push(collapsed.originalLine);
|
|
1384
1444
|
expandedLines.push(...collapsed.content);
|
|
1385
1445
|
found = true;
|
|
1446
|
+
break;
|
|
1386
1447
|
}
|
|
1387
|
-
}
|
|
1448
|
+
}
|
|
1388
1449
|
if (!found) {
|
|
1389
1450
|
expandedLines.push(line);
|
|
1390
1451
|
}
|
|
@@ -1428,7 +1489,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1428
1489
|
|
|
1429
1490
|
// Try to parse
|
|
1430
1491
|
try {
|
|
1431
|
-
|
|
1492
|
+
let parsed = JSON.parse(fullValue);
|
|
1493
|
+
|
|
1494
|
+
// Filter out hidden features before emitting
|
|
1495
|
+
parsed = this.filterHiddenFeatures(parsed);
|
|
1432
1496
|
|
|
1433
1497
|
// Validate GeoJSON types
|
|
1434
1498
|
const validationErrors = this.validateGeoJSON(parsed);
|
|
@@ -1467,6 +1531,178 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1467
1531
|
}
|
|
1468
1532
|
}
|
|
1469
1533
|
|
|
1534
|
+
// Filter hidden features from parsed GeoJSON before emitting events
|
|
1535
|
+
filterHiddenFeatures(parsed) {
|
|
1536
|
+
if (!parsed || this.hiddenFeatures.size === 0) return parsed;
|
|
1537
|
+
|
|
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;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
// ========== Feature Visibility Management ==========
|
|
1558
|
+
|
|
1559
|
+
// Generate a unique key for a Feature to track visibility state
|
|
1560
|
+
getFeatureKey(feature) {
|
|
1561
|
+
if (!feature || typeof feature !== 'object') return null;
|
|
1562
|
+
|
|
1563
|
+
// 1. Use GeoJSON id if present (most stable)
|
|
1564
|
+
if (feature.id !== undefined) return `id:${feature.id}`;
|
|
1565
|
+
|
|
1566
|
+
// 2. Use properties.id if present
|
|
1567
|
+
if (feature.properties?.id !== undefined) return `prop:${feature.properties.id}`;
|
|
1568
|
+
|
|
1569
|
+
// 3. Fallback: hash based on geometry type + first coordinates
|
|
1570
|
+
const geomType = feature.geometry?.type || 'null';
|
|
1571
|
+
const coords = JSON.stringify(feature.geometry?.coordinates || []).slice(0, 100);
|
|
1572
|
+
return `hash:${geomType}:${this.simpleHash(coords)}`;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Simple hash function for string
|
|
1576
|
+
simpleHash(str) {
|
|
1577
|
+
let hash = 0;
|
|
1578
|
+
for (let i = 0; i < str.length; i++) {
|
|
1579
|
+
const char = str.charCodeAt(i);
|
|
1580
|
+
hash = ((hash << 5) - hash) + char;
|
|
1581
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
1582
|
+
}
|
|
1583
|
+
return hash.toString(36);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Toggle feature visibility
|
|
1587
|
+
toggleFeatureVisibility(featureKey) {
|
|
1588
|
+
if (this.hiddenFeatures.has(featureKey)) {
|
|
1589
|
+
this.hiddenFeatures.delete(featureKey);
|
|
1590
|
+
} else {
|
|
1591
|
+
this.hiddenFeatures.add(featureKey);
|
|
1592
|
+
}
|
|
1593
|
+
this.updateHighlight();
|
|
1594
|
+
this.updateGutter();
|
|
1595
|
+
this.emitChange();
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Parse JSON and extract feature ranges (line numbers for each Feature)
|
|
1599
|
+
updateFeatureRanges() {
|
|
1600
|
+
const textarea = this.shadowRoot.getElementById('textarea');
|
|
1601
|
+
if (!textarea) return;
|
|
1602
|
+
|
|
1603
|
+
const text = textarea.value;
|
|
1604
|
+
this.featureRanges.clear();
|
|
1605
|
+
|
|
1606
|
+
try {
|
|
1607
|
+
// Expand collapsed content for parsing (collapsed markers like [...] are not valid JSON)
|
|
1608
|
+
const expandedText = this.expandAllCollapsed(text);
|
|
1609
|
+
|
|
1610
|
+
// Try to parse and find Features
|
|
1611
|
+
const prefix = this.prefix;
|
|
1612
|
+
const suffix = this.suffix;
|
|
1613
|
+
const fullValue = prefix + expandedText + suffix;
|
|
1614
|
+
const parsed = JSON.parse(fullValue);
|
|
1615
|
+
|
|
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
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// Now find each feature's line range in the text
|
|
1624
|
+
const lines = text.split('\n');
|
|
1625
|
+
let featureIndex = 0;
|
|
1626
|
+
let braceDepth = 0;
|
|
1627
|
+
let inFeature = false;
|
|
1628
|
+
let featureStartLine = -1;
|
|
1629
|
+
let currentFeatureKey = null;
|
|
1630
|
+
|
|
1631
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1632
|
+
const line = lines[i];
|
|
1633
|
+
|
|
1634
|
+
// Detect start of a Feature object (not FeatureCollection)
|
|
1635
|
+
// Use regex to match exact "Feature" value, not "FeatureCollection"
|
|
1636
|
+
const isFeatureTypeLine = /"type"\s*:\s*"Feature"/.test(line);
|
|
1637
|
+
if (!inFeature && isFeatureTypeLine) {
|
|
1638
|
+
// Find the opening brace for this Feature
|
|
1639
|
+
// Look backwards for the opening brace
|
|
1640
|
+
let startLine = i;
|
|
1641
|
+
for (let j = i; j >= 0; j--) {
|
|
1642
|
+
if (lines[j].includes('{')) {
|
|
1643
|
+
startLine = j;
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
featureStartLine = startLine;
|
|
1648
|
+
inFeature = true;
|
|
1649
|
+
|
|
1650
|
+
// Start braceDepth at 1 since we're inside the Feature's opening brace
|
|
1651
|
+
// Then count any additional braces from startLine to current line (ignoring strings)
|
|
1652
|
+
braceDepth = 1;
|
|
1653
|
+
for (let k = startLine; k <= i; k++) {
|
|
1654
|
+
const scanLine = lines[k];
|
|
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;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// Get the feature key
|
|
1665
|
+
if (featureIndex < features.length) {
|
|
1666
|
+
currentFeatureKey = this.getFeatureKey(features[featureIndex]);
|
|
1667
|
+
}
|
|
1668
|
+
} else if (inFeature) {
|
|
1669
|
+
// Count braces (ignoring those in strings)
|
|
1670
|
+
const counts = this._countBracketsOutsideStrings(line, '{');
|
|
1671
|
+
braceDepth += counts.open - counts.close;
|
|
1672
|
+
|
|
1673
|
+
// Feature ends when braceDepth returns to 0
|
|
1674
|
+
if (braceDepth <= 0) {
|
|
1675
|
+
if (currentFeatureKey) {
|
|
1676
|
+
this.featureRanges.set(currentFeatureKey, {
|
|
1677
|
+
startLine: featureStartLine,
|
|
1678
|
+
endLine: i,
|
|
1679
|
+
featureIndex: featureIndex
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
featureIndex++;
|
|
1683
|
+
inFeature = false;
|
|
1684
|
+
currentFeatureKey = null;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
} catch (e) {
|
|
1689
|
+
// Invalid JSON, can't extract feature ranges
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// Get hidden line ranges for highlighting
|
|
1694
|
+
getHiddenLineRanges() {
|
|
1695
|
+
const ranges = [];
|
|
1696
|
+
for (const [featureKey, range] of this.featureRanges) {
|
|
1697
|
+
if (this.hiddenFeatures.has(featureKey)) {
|
|
1698
|
+
ranges.push(range);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return ranges;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// ========== GeoJSON Validation ==========
|
|
1705
|
+
|
|
1470
1706
|
// Validate GeoJSON structure and types
|
|
1471
1707
|
// context: 'root' | 'geometry' | 'properties'
|
|
1472
1708
|
validateGeoJSON(obj, path = '', context = 'root') {
|
|
@@ -1520,19 +1756,131 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1520
1756
|
return errors;
|
|
1521
1757
|
}
|
|
1522
1758
|
|
|
1523
|
-
// Helper:
|
|
1524
|
-
|
|
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) {
|
|
1525
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) {
|
|
1526
1797
|
const bracketPos = line.indexOf(openBracket);
|
|
1527
1798
|
if (bracketPos === -1) return false;
|
|
1799
|
+
|
|
1528
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) {
|
|
1529
1811
|
let depth = 1;
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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
|
+
}
|
|
1534
1824
|
}
|
|
1535
|
-
|
|
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;
|
|
1536
1884
|
}
|
|
1537
1885
|
|
|
1538
1886
|
// Helper: Expand all collapsed markers and return expanded content
|
|
@@ -1649,34 +1997,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1649
1997
|
}
|
|
1650
1998
|
}
|
|
1651
1999
|
|
|
1652
|
-
autoFormatContent() {
|
|
1653
|
-
const textarea = this.shadowRoot.getElementById('textarea');
|
|
1654
|
-
|
|
1655
|
-
// Save collapsed node details
|
|
1656
|
-
const collapsedNodes = Array.from(this.collapsedData.values()).map(data => ({
|
|
1657
|
-
nodeKey: data.nodeKey,
|
|
1658
|
-
indent: data.indent
|
|
1659
|
-
}));
|
|
1660
|
-
|
|
1661
|
-
// Expand and format
|
|
1662
|
-
const content = this.expandAllCollapsed(textarea.value);
|
|
1663
|
-
|
|
1664
|
-
try {
|
|
1665
|
-
const formattedContent = this.formatJSONContent(content);
|
|
1666
|
-
|
|
1667
|
-
if (formattedContent !== content) {
|
|
1668
|
-
this.collapsedData.clear();
|
|
1669
|
-
textarea.value = formattedContent;
|
|
1670
|
-
|
|
1671
|
-
if (collapsedNodes.length > 0) {
|
|
1672
|
-
this.reapplyCollapsed(collapsedNodes);
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
} catch (e) {
|
|
1676
|
-
// Invalid JSON, don't format
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
2000
|
reapplyCollapsed(collapsedNodes) {
|
|
1681
2001
|
const textarea = this.shadowRoot.getElementById('textarea');
|
|
1682
2002
|
const lines = textarea.value.split('\n');
|
|
@@ -1708,50 +2028,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1708
2028
|
|
|
1709
2029
|
// Only collapse if this occurrence should be collapsed
|
|
1710
2030
|
if (currentOccurrence <= collapseMap.get(key)) {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
1714
|
-
|
|
1715
|
-
// Skip if closes on same line
|
|
1716
|
-
if (this.bracketClosesOnSameLine(line, openBracket)) continue;
|
|
1717
|
-
|
|
1718
|
-
// Find closing bracket
|
|
1719
|
-
let depth = 1;
|
|
1720
|
-
let endLine = i;
|
|
1721
|
-
const content = [];
|
|
1722
|
-
|
|
1723
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
1724
|
-
const scanLine = lines[j];
|
|
2031
|
+
const indent = match[1];
|
|
2032
|
+
const openBracket = match[3];
|
|
1725
2033
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
if (char === closeBracket) depth--;
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
content.push(scanLine);
|
|
1732
|
-
|
|
1733
|
-
if (depth === 0) {
|
|
1734
|
-
endLine = j;
|
|
1735
|
-
break;
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
// Store with unique key
|
|
1740
|
-
const uniqueKey = `${i}-${nodeKey}`;
|
|
1741
|
-
this.collapsedData.set(uniqueKey, {
|
|
1742
|
-
originalLine: line,
|
|
1743
|
-
content: content,
|
|
1744
|
-
indent: indent.length,
|
|
1745
|
-
nodeKey: nodeKey
|
|
1746
|
-
});
|
|
1747
|
-
|
|
1748
|
-
// Replace with marker
|
|
1749
|
-
const beforeBracket = line.substring(0, line.indexOf(openBracket));
|
|
1750
|
-
const hasTrailingComma = lines[endLine] && lines[endLine].trim().endsWith(',');
|
|
1751
|
-
lines[i] = `${beforeBracket}${openBracket}...${closeBracket}${hasTrailingComma ? ',' : ''}`;
|
|
1752
|
-
|
|
1753
|
-
// Remove content lines
|
|
1754
|
-
lines.splice(i + 1, endLine - i);
|
|
2034
|
+
// Use common collapse helper
|
|
2035
|
+
this._performCollapse(lines, i, nodeKey, indent, openBracket);
|
|
1755
2036
|
}
|
|
1756
2037
|
}
|
|
1757
2038
|
}
|
|
@@ -1780,77 +2061,41 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1780
2061
|
// Generate and inject theme CSS based on dark selector
|
|
1781
2062
|
updateThemeCSS() {
|
|
1782
2063
|
const darkSelector = this.getAttribute('dark-selector') || '.dark';
|
|
1783
|
-
|
|
1784
|
-
// Parse selector to create CSS rule for dark theme
|
|
1785
2064
|
const darkRule = this.parseSelectorToHostRule(darkSelector);
|
|
1786
|
-
// Light theme is the default (no selector = light)
|
|
1787
|
-
const lightRule = ':host';
|
|
1788
2065
|
|
|
1789
2066
|
// Find or create theme style element
|
|
1790
2067
|
let themeStyle = this.shadowRoot.getElementById('theme-styles');
|
|
1791
2068
|
if (!themeStyle) {
|
|
1792
2069
|
themeStyle = document.createElement('style');
|
|
1793
2070
|
themeStyle.id = 'theme-styles';
|
|
1794
|
-
// Insert at the beginning of shadow root to ensure it's before static styles
|
|
1795
2071
|
this.shadowRoot.insertBefore(themeStyle, this.shadowRoot.firstChild);
|
|
1796
2072
|
}
|
|
1797
2073
|
|
|
1798
|
-
//
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
--gutter-bg: ${this.themes.light.gutterBackground};
|
|
1805
|
-
--gutter-border: ${this.themes.light.gutterBorder};
|
|
1806
|
-
--json-key: ${this.themes.light.jsonKey};
|
|
1807
|
-
--json-string: ${this.themes.light.jsonString};
|
|
1808
|
-
--json-number: ${this.themes.light.jsonNumber};
|
|
1809
|
-
--json-boolean: ${this.themes.light.jsonBoolean};
|
|
1810
|
-
--json-null: ${this.themes.light.jsonNull};
|
|
1811
|
-
--json-punct: ${this.themes.light.jsonPunctuation};
|
|
1812
|
-
--control-color: ${this.themes.light.controlColor};
|
|
1813
|
-
--control-bg: ${this.themes.light.controlBg};
|
|
1814
|
-
--control-border: ${this.themes.light.controlBorder};
|
|
1815
|
-
--geojson-key: ${this.themes.light.geojsonKey};
|
|
1816
|
-
--geojson-type: ${this.themes.light.geojsonType};
|
|
1817
|
-
--geojson-type-invalid: ${this.themes.light.geojsonTypeInvalid};
|
|
1818
|
-
--json-key-invalid: ${this.themes.light.jsonKeyInvalid};
|
|
1819
|
-
}
|
|
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
|
+
};
|
|
1820
2080
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
--control-bg: ${this.themes.dark.controlBg};
|
|
1835
|
-
--control-border: ${this.themes.dark.controlBorder};
|
|
1836
|
-
--geojson-key: ${this.themes.dark.geojsonKey};
|
|
1837
|
-
--geojson-type: ${this.themes.dark.geojsonType};
|
|
1838
|
-
--geojson-type-invalid: ${this.themes.dark.geojsonTypeInvalid};
|
|
1839
|
-
--json-key-invalid: ${this.themes.dark.jsonKeyInvalid};
|
|
1840
|
-
}
|
|
1841
|
-
`;
|
|
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 }`;
|
|
1842
2094
|
|
|
1843
2095
|
themeStyle.textContent = css;
|
|
1844
2096
|
}
|
|
1845
2097
|
|
|
1846
2098
|
// Public API: Theme management
|
|
1847
|
-
getTheme() {
|
|
1848
|
-
return {
|
|
1849
|
-
dark: { ...this.themes.dark },
|
|
1850
|
-
light: { ...this.themes.light }
|
|
1851
|
-
};
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
2099
|
setTheme(theme) {
|
|
1855
2100
|
if (theme.dark) {
|
|
1856
2101
|
this.themes.dark = { ...this.themes.dark, ...theme.dark };
|
|
@@ -1858,19 +2103,278 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1858
2103
|
if (theme.light) {
|
|
1859
2104
|
this.themes.light = { ...this.themes.light, ...theme.light };
|
|
1860
2105
|
}
|
|
1861
|
-
|
|
1862
|
-
// Regenerate CSS with new theme values
|
|
1863
2106
|
this.updateThemeCSS();
|
|
1864
2107
|
}
|
|
1865
2108
|
|
|
1866
2109
|
resetTheme() {
|
|
1867
|
-
|
|
1868
|
-
this.themes = {
|
|
1869
|
-
dark: { ...GeoJsonEditor.DEFAULT_THEMES.dark },
|
|
1870
|
-
light: { ...GeoJsonEditor.DEFAULT_THEMES.light }
|
|
1871
|
-
};
|
|
2110
|
+
this.themes = { dark: {}, light: {} };
|
|
1872
2111
|
this.updateThemeCSS();
|
|
1873
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
|
+
}
|
|
1874
2378
|
}
|
|
1875
2379
|
|
|
1876
2380
|
// Register the custom element
|