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