@softwarity/geojson-editor 1.0.24 → 1.0.26
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 +172 -32
- package/dist/geojson-editor.js +2 -2
- package/package.json +9 -3
- package/src/constants.ts +0 -3
- package/src/geojson-editor.css +48 -43
- package/src/geojson-editor.ts +143 -90
- package/src/types.ts +0 -30
- package/src/utils.ts +0 -10
- package/themes/github.css +43 -0
- package/themes/monokai.css +43 -0
- package/themes/solarized.css +44 -0
- package/themes/vscode.css +43 -0
- package/types/types.d.ts +0 -28
package/src/geojson-editor.css
CHANGED
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
:host {
|
|
14
|
+
/* Color scheme - inherits from parent, falls back to system preference if not set */
|
|
15
|
+
color-scheme: inherit;
|
|
16
|
+
|
|
17
|
+
/* Layout variables (internal only) */
|
|
14
18
|
--line-height: 19.5px;
|
|
15
19
|
--gutter-width: 50px;
|
|
16
20
|
--editor-padding-y: 8px;
|
|
17
21
|
--editor-padding-x: 12px;
|
|
22
|
+
|
|
18
23
|
display: flex;
|
|
19
24
|
flex-direction: column;
|
|
20
25
|
position: relative;
|
|
@@ -39,7 +44,7 @@
|
|
|
39
44
|
position: relative;
|
|
40
45
|
width: 100%;
|
|
41
46
|
flex: 1;
|
|
42
|
-
background: var(--bg-color, #fff);
|
|
47
|
+
background: var(--geojson-editor-bg-color, light-dark(#fff, #2b2b2b));
|
|
43
48
|
display: flex;
|
|
44
49
|
overflow: hidden;
|
|
45
50
|
}
|
|
@@ -47,8 +52,8 @@
|
|
|
47
52
|
/* ========== Gutter ========== */
|
|
48
53
|
.gutter {
|
|
49
54
|
width: var(--gutter-width);
|
|
50
|
-
background: var(--gutter-bg, #f0f0f0);
|
|
51
|
-
border-right: 1px solid var(--gutter-border, #e0e0e0);
|
|
55
|
+
background: var(--geojson-editor-gutter-bg, light-dark(#f0f0f0, #313335));
|
|
56
|
+
border-right: 1px solid var(--geojson-editor-gutter-border, light-dark(#e0e0e0, #3c3f41));
|
|
52
57
|
overflow: hidden;
|
|
53
58
|
flex-shrink: 0;
|
|
54
59
|
position: relative;
|
|
@@ -90,12 +95,12 @@
|
|
|
90
95
|
top: 0;
|
|
91
96
|
bottom: 0;
|
|
92
97
|
width: 3px;
|
|
93
|
-
background: var(--error-color, #dc3545);
|
|
98
|
+
background: var(--geojson-editor-error-color, light-dark(#dc3545, #ff6b68));
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
.line-number {
|
|
97
102
|
font-size: 11px;
|
|
98
|
-
color: var(--gutter-text, #999);
|
|
103
|
+
color: var(--geojson-editor-gutter-text, light-dark(#999, #606366));
|
|
99
104
|
user-select: none;
|
|
100
105
|
min-width: 20px;
|
|
101
106
|
text-align: right;
|
|
@@ -118,7 +123,7 @@
|
|
|
118
123
|
flex-shrink: 0;
|
|
119
124
|
background: transparent;
|
|
120
125
|
border: none;
|
|
121
|
-
color: var(--json-punct, #a9b7c6);
|
|
126
|
+
color: var(--geojson-editor-json-punct, light-dark(#000, #a9b7c6));
|
|
122
127
|
font-size: 10px;
|
|
123
128
|
display: flex;
|
|
124
129
|
align-items: center;
|
|
@@ -178,7 +183,7 @@
|
|
|
178
183
|
padding: var(--editor-padding-y) var(--editor-padding-x);
|
|
179
184
|
overscroll-behavior: contain; /* Prevent scroll chaining to parent */
|
|
180
185
|
scrollbar-width: thin;
|
|
181
|
-
scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8);
|
|
186
|
+
scrollbar-color: var(--geojson-editor-control-border, light-dark(#c0c0c0, #5a5a5a)) var(--geojson-editor-control-bg, light-dark(#e8e8e8, #3c3f41));
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
/* Scroll content - defines total scrollable height */
|
|
@@ -209,7 +214,7 @@
|
|
|
209
214
|
position: absolute;
|
|
210
215
|
width: 2px;
|
|
211
216
|
height: 1em;
|
|
212
|
-
background: var(--caret-color, #000);
|
|
217
|
+
background: var(--geojson-editor-caret-color, light-dark(#000, #bbb));
|
|
213
218
|
top: 0.15em;
|
|
214
219
|
pointer-events: none;
|
|
215
220
|
animation: cursor-blink 1s step-end infinite;
|
|
@@ -237,7 +242,7 @@
|
|
|
237
242
|
position: absolute;
|
|
238
243
|
height: 100%;
|
|
239
244
|
top: 0;
|
|
240
|
-
background: var(--selection-color, rgba(51, 153, 255, 0.3));
|
|
245
|
+
background: var(--geojson-editor-selection-color, light-dark(rgba(51, 153, 255, 0.3), rgba(51, 153, 255, 0.4)));
|
|
241
246
|
pointer-events: none;
|
|
242
247
|
z-index: 0;
|
|
243
248
|
}
|
|
@@ -259,47 +264,47 @@
|
|
|
259
264
|
}
|
|
260
265
|
|
|
261
266
|
/* ========== Syntax Highlighting ========== */
|
|
262
|
-
.json-key { color: var(--json-key, #660e7a); }
|
|
263
|
-
.json-string { color: var(--json-string, #008000); }
|
|
264
|
-
.json-number { color: var(--json-number, #00f); }
|
|
265
|
-
.json-boolean, .json-null { color: var(--json-boolean, #000080); }
|
|
266
|
-
.json-punctuation { color: var(--json-punct, #000); }
|
|
267
|
-
.json-error { color: var(--json-error, #f00); }
|
|
267
|
+
.json-key { color: var(--geojson-editor-json-key, light-dark(#660e7a, #9876aa)); }
|
|
268
|
+
.json-string { color: var(--geojson-editor-json-string, light-dark(#008000, #6a8759)); }
|
|
269
|
+
.json-number { color: var(--geojson-editor-json-number, light-dark(#00f, #6897bb)); }
|
|
270
|
+
.json-boolean, .json-null { color: var(--geojson-editor-json-boolean, light-dark(#000080, #cc7832)); }
|
|
271
|
+
.json-punctuation { color: var(--geojson-editor-json-punct, light-dark(#000, #a9b7c6)); }
|
|
272
|
+
.json-error { color: var(--geojson-editor-json-error, light-dark(#f00, #ff6b68)); }
|
|
268
273
|
|
|
269
|
-
.geojson-key { color: var(--geojson-key, #660e7a); font-weight: 600; }
|
|
270
|
-
.geojson-type { color: var(--geojson-type, #008000); font-weight: 600; }
|
|
271
|
-
.geojson-type-invalid { color: var(--geojson-type-invalid, #f00); font-weight: 600; }
|
|
274
|
+
.geojson-key { color: var(--geojson-editor-geojson-key, light-dark(#660e7a, #9876aa)); font-weight: 600; }
|
|
275
|
+
.geojson-type { color: var(--geojson-editor-geojson-type, light-dark(#008000, #6a8759)); font-weight: 600; }
|
|
276
|
+
.geojson-type-invalid { color: var(--geojson-editor-geojson-type-invalid, light-dark(#f00, #ff6b68)); font-weight: 600; }
|
|
272
277
|
|
|
273
278
|
/* Collapsed node styling */
|
|
274
279
|
.collapsed-bracket-array,
|
|
275
280
|
.collapsed-bracket-object {
|
|
276
|
-
color: var(--json-punct, #000);
|
|
281
|
+
color: var(--geojson-editor-json-punct, light-dark(#000, #a9b7c6));
|
|
277
282
|
}
|
|
278
283
|
.collapsed-bracket-array::after,
|
|
279
284
|
.collapsed-bracket-object::after {
|
|
280
285
|
content: '…';
|
|
281
|
-
color: var(--json-punct, #
|
|
286
|
+
color: var(--geojson-editor-json-punct, light-dark(#000, #a9b7c6));
|
|
282
287
|
}
|
|
283
288
|
|
|
284
289
|
/* ========== Prefix/Suffix ========== */
|
|
285
290
|
.prefix-wrapper, .suffix-wrapper {
|
|
286
291
|
display: flex;
|
|
287
292
|
flex-shrink: 0;
|
|
288
|
-
background: var(--bg-color, #fff);
|
|
293
|
+
background: var(--geojson-editor-bg-color, light-dark(#fff, #2b2b2b));
|
|
289
294
|
}
|
|
290
295
|
|
|
291
296
|
.prefix-gutter, .suffix-gutter {
|
|
292
297
|
width: var(--gutter-width);
|
|
293
|
-
background: var(--gutter-bg, #f0f0f0);
|
|
294
|
-
border-right: 1px solid var(--gutter-border, #e0e0e0);
|
|
298
|
+
background: var(--geojson-editor-gutter-bg, light-dark(#f0f0f0, #313335));
|
|
299
|
+
border-right: 1px solid var(--geojson-editor-gutter-border, light-dark(#e0e0e0, #3c3f41));
|
|
295
300
|
flex-shrink: 0;
|
|
296
301
|
}
|
|
297
302
|
|
|
298
303
|
.editor-prefix, .editor-suffix {
|
|
299
304
|
flex: 1;
|
|
300
305
|
padding: 4px 12px;
|
|
301
|
-
color: var(--text-color, #000);
|
|
302
|
-
background: var(--bg-color, #fff);
|
|
306
|
+
color: var(--geojson-editor-text-color, light-dark(#000, #a9b7c6));
|
|
307
|
+
background: var(--geojson-editor-bg-color, light-dark(#fff, #2b2b2b));
|
|
303
308
|
user-select: none;
|
|
304
309
|
white-space: pre-wrap;
|
|
305
310
|
word-wrap: break-word;
|
|
@@ -316,7 +321,7 @@
|
|
|
316
321
|
transform: translateY(-50%);
|
|
317
322
|
background: transparent;
|
|
318
323
|
border: none;
|
|
319
|
-
color: var(--text-color, #000);
|
|
324
|
+
color: var(--geojson-editor-text-color, light-dark(#000, #a9b7c6));
|
|
320
325
|
opacity: 0.15;
|
|
321
326
|
cursor: pointer;
|
|
322
327
|
font-size: 0.7rem;
|
|
@@ -339,8 +344,8 @@
|
|
|
339
344
|
top: 2rem;
|
|
340
345
|
right: 0.5rem;
|
|
341
346
|
z-index: 1000;
|
|
342
|
-
background: var(--bg-color, #
|
|
343
|
-
border: 1px solid var(--gutter-border, #e0e0e0);
|
|
347
|
+
background: var(--geojson-editor-bg-color, light-dark(#fff, #2b2b2b));
|
|
348
|
+
border: 1px solid var(--geojson-editor-gutter-border, light-dark(#e0e0e0, #3c3f41));
|
|
344
349
|
border-radius: 6px;
|
|
345
350
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
346
351
|
padding: 12px 16px;
|
|
@@ -356,13 +361,13 @@
|
|
|
356
361
|
.info-popup-title {
|
|
357
362
|
font-weight: bold;
|
|
358
363
|
font-size: 13px;
|
|
359
|
-
color: var(--text-color, #
|
|
364
|
+
color: var(--geojson-editor-text-color, light-dark(#000, #a9b7c6));
|
|
360
365
|
margin-bottom: 4px;
|
|
361
366
|
}
|
|
362
367
|
.info-popup-version {
|
|
363
368
|
display: block;
|
|
364
369
|
font-size: 11px;
|
|
365
|
-
color: var(--text-color, #
|
|
370
|
+
color: var(--geojson-editor-text-color, light-dark(#000, #a9b7c6));
|
|
366
371
|
opacity: 0.6;
|
|
367
372
|
margin-bottom: 8px;
|
|
368
373
|
text-decoration: none;
|
|
@@ -373,7 +378,7 @@
|
|
|
373
378
|
}
|
|
374
379
|
.info-popup-copyright {
|
|
375
380
|
font-size: 10px;
|
|
376
|
-
color: var(--text-color, #
|
|
381
|
+
color: var(--geojson-editor-text-color, light-dark(#000, #a9b7c6));
|
|
377
382
|
opacity: 0.5;
|
|
378
383
|
}
|
|
379
384
|
|
|
@@ -384,7 +389,7 @@
|
|
|
384
389
|
transform: translateY(-50%);
|
|
385
390
|
background: transparent;
|
|
386
391
|
border: none;
|
|
387
|
-
color: var(--text-color, #000);
|
|
392
|
+
color: var(--geojson-editor-text-color, light-dark(#000, #a9b7c6));
|
|
388
393
|
opacity: 0.3;
|
|
389
394
|
cursor: pointer;
|
|
390
395
|
font-size: 0.65rem;
|
|
@@ -413,7 +418,7 @@
|
|
|
413
418
|
.error-nav-btn {
|
|
414
419
|
background: transparent;
|
|
415
420
|
border: none;
|
|
416
|
-
color: var(--error-color, #dc3545);
|
|
421
|
+
color: var(--geojson-editor-error-color, light-dark(#dc3545, #ff6b68));
|
|
417
422
|
cursor: pointer;
|
|
418
423
|
font-size: 8px;
|
|
419
424
|
width: 16px;
|
|
@@ -429,7 +434,7 @@
|
|
|
429
434
|
opacity: 1;
|
|
430
435
|
}
|
|
431
436
|
.error-count {
|
|
432
|
-
color: var(--error-color, #dc3545);
|
|
437
|
+
color: var(--geojson-editor-error-color, light-dark(#dc3545, #ff6b68));
|
|
433
438
|
font-size: 11px;
|
|
434
439
|
min-width: 20px;
|
|
435
440
|
text-align: center;
|
|
@@ -437,9 +442,9 @@
|
|
|
437
442
|
|
|
438
443
|
/* ========== Scrollbar ========== */
|
|
439
444
|
.viewport::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
440
|
-
.viewport::-webkit-scrollbar-track { background: var(--control-bg, #e8e8e8); }
|
|
441
|
-
.viewport::-webkit-scrollbar-thumb { background: var(--control-border, #c0c0c0); border-radius: 5px; }
|
|
442
|
-
.viewport::-webkit-scrollbar-thumb:hover { background: var(--control-color, #000080); }
|
|
445
|
+
.viewport::-webkit-scrollbar-track { background: var(--geojson-editor-control-bg, light-dark(#e8e8e8, #3c3f41)); }
|
|
446
|
+
.viewport::-webkit-scrollbar-thumb { background: var(--geojson-editor-control-border, light-dark(#c0c0c0, #5a5a5a)); border-radius: 5px; }
|
|
447
|
+
.viewport::-webkit-scrollbar-thumb:hover { background: var(--geojson-editor-control-color, light-dark(#000080, #cc7832)); }
|
|
443
448
|
|
|
444
449
|
/* ========== Inline Controls (using ::before pseudo-elements) ========== */
|
|
445
450
|
|
|
@@ -464,24 +469,24 @@
|
|
|
464
469
|
.json-color:hover::before,
|
|
465
470
|
.json-boolean:hover::before {
|
|
466
471
|
transform: translateY(-50%) scale(1.2);
|
|
467
|
-
border-color: var(--control-color, #000080);
|
|
472
|
+
border-color: var(--geojson-editor-control-color, light-dark(#000080, #cc7832));
|
|
468
473
|
}
|
|
469
474
|
|
|
470
475
|
/* Color swatch specific styles */
|
|
471
476
|
.json-color::before {
|
|
472
477
|
background-color: var(--swatch-color);
|
|
473
|
-
border: 1px solid var(--json-punct, #a9b7c6);
|
|
478
|
+
border: 1px solid var(--geojson-editor-json-punct, light-dark(#000, #a9b7c6));
|
|
474
479
|
}
|
|
475
480
|
|
|
476
481
|
/* Boolean checkbox specific styles */
|
|
477
482
|
.json-boolean::before {
|
|
478
|
-
border: 1.5px solid var(--control-border, #c0c0c0);
|
|
483
|
+
border: 1.5px solid var(--geojson-editor-control-border, light-dark(#c0c0c0, #5a5a5a));
|
|
479
484
|
background: transparent;
|
|
480
485
|
}
|
|
481
486
|
.json-bool-true::before {
|
|
482
487
|
content: '✔';
|
|
483
|
-
border-color: var(--control-color, #000080);
|
|
484
|
-
color: var(--control-color, #000080);
|
|
488
|
+
border-color: var(--geojson-editor-control-color, light-dark(#000080, #cc7832));
|
|
489
|
+
color: var(--geojson-editor-control-color, light-dark(#000080, #cc7832));
|
|
485
490
|
font-size: 8px;
|
|
486
491
|
display: flex;
|
|
487
492
|
align-items: center;
|
|
@@ -496,7 +501,7 @@
|
|
|
496
501
|
left: 0;
|
|
497
502
|
top: 3px;
|
|
498
503
|
font-size: 10px;
|
|
499
|
-
color: var(--control-color, #000080);
|
|
504
|
+
color: var(--geojson-editor-control-color, light-dark(#000080, #cc7832));
|
|
500
505
|
opacity: 0.6;
|
|
501
506
|
cursor: pointer;
|
|
502
507
|
z-index: 1;
|
package/src/geojson-editor.ts
CHANGED
|
@@ -4,8 +4,7 @@ import type { Feature } from 'geojson';
|
|
|
4
4
|
|
|
5
5
|
// ========== Imports from extracted modules ==========
|
|
6
6
|
import type {
|
|
7
|
-
SetOptions
|
|
8
|
-
ThemeSettings
|
|
7
|
+
SetOptions
|
|
9
8
|
} from './types.js';
|
|
10
9
|
|
|
11
10
|
import type {
|
|
@@ -35,19 +34,18 @@ import {
|
|
|
35
34
|
RE_BRACKET_POS,
|
|
36
35
|
RE_IS_WORD_CHAR,
|
|
37
36
|
RE_ATTR_AND_BOOL_VALUE,
|
|
38
|
-
RE_TO_KEBAB,
|
|
39
37
|
RE_OPEN_BRACES,
|
|
40
38
|
RE_CLOSE_BRACES,
|
|
41
39
|
RE_OPEN_BRACKETS,
|
|
42
40
|
RE_CLOSE_BRACKET
|
|
43
41
|
} from './constants.js';
|
|
44
42
|
|
|
45
|
-
import { createElement, countBrackets
|
|
43
|
+
import { createElement, countBrackets } from './utils.js';
|
|
46
44
|
import { validateGeoJSON, normalizeToFeatures } from './validation.js';
|
|
47
45
|
import { highlightSyntax, namedColorToHex, isNamedColor } from './syntax-highlighter.js';
|
|
48
46
|
|
|
49
47
|
// Re-export public types
|
|
50
|
-
export type { SetOptions
|
|
48
|
+
export type { SetOptions } from './types.js';
|
|
51
49
|
|
|
52
50
|
// Alias for minification
|
|
53
51
|
const _ce = createElement;
|
|
@@ -94,8 +92,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
94
92
|
private renderTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
95
93
|
private inputTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
96
94
|
|
|
97
|
-
// ========== Theme ==========
|
|
98
|
-
themes: ThemeSettings = { dark: {}, light: {} };
|
|
99
95
|
|
|
100
96
|
// ========== Undo/Redo History ==========
|
|
101
97
|
private _undoStack: EditorSnapshot[] = [];
|
|
@@ -116,6 +112,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
116
112
|
private _contextMapFirstLine: string | undefined = undefined;
|
|
117
113
|
private _contextMapLastLine: string | undefined = undefined;
|
|
118
114
|
private _errorLinesCache: Set<number> | null = null;
|
|
115
|
+
private _lastCurrentFeatureIndices: string | null = null; // For current-features event deduplication (JSON stringified indices)
|
|
119
116
|
|
|
120
117
|
// ========== Cached DOM Elements ==========
|
|
121
118
|
private _viewport: HTMLElement | null = null;
|
|
@@ -440,7 +437,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
440
437
|
|
|
441
438
|
// ========== Observed Attributes ==========
|
|
442
439
|
static get observedAttributes() {
|
|
443
|
-
return ['readonly', 'value', 'placeholder', '
|
|
440
|
+
return ['readonly', 'value', 'placeholder', 'internal-add-shortcut'];
|
|
444
441
|
}
|
|
445
442
|
|
|
446
443
|
// ========== Lifecycle ==========
|
|
@@ -449,7 +446,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
449
446
|
this._cacheElements();
|
|
450
447
|
this.setupEventListeners();
|
|
451
448
|
this.updatePrefixSuffix();
|
|
452
|
-
this.updateThemeCSS();
|
|
453
449
|
|
|
454
450
|
if (this.value) {
|
|
455
451
|
this.setValue(this.value);
|
|
@@ -484,9 +480,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
484
480
|
case 'placeholder':
|
|
485
481
|
this.updatePlaceholderContent();
|
|
486
482
|
break;
|
|
487
|
-
case 'dark-selector':
|
|
488
|
-
this.updateThemeCSS();
|
|
489
|
-
break;
|
|
490
483
|
}
|
|
491
484
|
}
|
|
492
485
|
|
|
@@ -697,12 +690,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
697
690
|
editorWrapper.classList.add('focused');
|
|
698
691
|
this._invalidateRenderCache(); // Force re-render to show cursor
|
|
699
692
|
this.scheduleRender();
|
|
693
|
+
// Emit current feature on focus (force to always emit on focus gain)
|
|
694
|
+
this._emitCurrentFeature(true);
|
|
700
695
|
});
|
|
701
696
|
|
|
702
697
|
hiddenTextarea.addEventListener('blur', () => {
|
|
703
698
|
editorWrapper.classList.remove('focused');
|
|
704
699
|
this._invalidateRenderCache(); // Force re-render to hide cursor
|
|
705
700
|
this.scheduleRender();
|
|
701
|
+
// Emit null on blur
|
|
702
|
+
this._emitCurrentFeatureNull();
|
|
706
703
|
});
|
|
707
704
|
|
|
708
705
|
// Scroll handling
|
|
@@ -1324,11 +1321,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1324
1321
|
|
|
1325
1322
|
linesContainer.innerHTML = '';
|
|
1326
1323
|
linesContainer.appendChild(fragment);
|
|
1327
|
-
|
|
1324
|
+
|
|
1328
1325
|
// Render gutter with same range
|
|
1329
1326
|
this.renderGutter(startIndex, endIndex);
|
|
1327
|
+
|
|
1328
|
+
// Emit current-features event if feature changed (only when editor is focused)
|
|
1329
|
+
if (this._editorWrapper?.classList.contains('focused')) {
|
|
1330
|
+
this._emitCurrentFeature();
|
|
1331
|
+
}
|
|
1330
1332
|
}
|
|
1331
|
-
|
|
1333
|
+
|
|
1332
1334
|
/**
|
|
1333
1335
|
* Insert cursor element at the specified column position
|
|
1334
1336
|
* Uses absolute positioning to avoid affecting text layout
|
|
@@ -2237,7 +2239,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2237
2239
|
}
|
|
2238
2240
|
|
|
2239
2241
|
/**
|
|
2240
|
-
* Scroll viewport to ensure cursor is visible
|
|
2242
|
+
* Scroll viewport to ensure cursor is visible with comfortable margin
|
|
2241
2243
|
* @param center - if true, center the cursor line in the viewport
|
|
2242
2244
|
*/
|
|
2243
2245
|
private _scrollToCursor(center = false) {
|
|
@@ -2250,6 +2252,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2250
2252
|
|
|
2251
2253
|
const cursorY = visibleIndex * this.lineHeight;
|
|
2252
2254
|
const viewportHeight = viewport.clientHeight;
|
|
2255
|
+
// Add margin of 2 lines for comfortable visibility
|
|
2256
|
+
const scrollMargin = this.lineHeight * 2;
|
|
2253
2257
|
|
|
2254
2258
|
if (center) {
|
|
2255
2259
|
// Center the cursor line in the viewport
|
|
@@ -2258,13 +2262,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2258
2262
|
const viewportTop = viewport.scrollTop;
|
|
2259
2263
|
const viewportBottom = viewportTop + viewportHeight;
|
|
2260
2264
|
|
|
2261
|
-
// Scroll up if cursor is above viewport
|
|
2262
|
-
if (cursorY < viewportTop) {
|
|
2263
|
-
viewport.scrollTop = cursorY;
|
|
2265
|
+
// Scroll up if cursor is above viewport (with margin)
|
|
2266
|
+
if (cursorY < viewportTop + scrollMargin) {
|
|
2267
|
+
viewport.scrollTop = Math.max(0, cursorY - scrollMargin);
|
|
2264
2268
|
}
|
|
2265
|
-
// Scroll down if cursor is below viewport
|
|
2266
|
-
else if (cursorY + this.lineHeight > viewportBottom) {
|
|
2267
|
-
viewport.scrollTop = cursorY + this.lineHeight - viewportHeight;
|
|
2269
|
+
// Scroll down if cursor is below viewport (with margin)
|
|
2270
|
+
else if (cursorY + this.lineHeight > viewportBottom - scrollMargin) {
|
|
2271
|
+
viewport.scrollTop = cursorY + this.lineHeight + scrollMargin - viewportHeight;
|
|
2268
2272
|
}
|
|
2269
2273
|
}
|
|
2270
2274
|
}
|
|
@@ -2757,7 +2761,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2757
2761
|
// Try to parse as GeoJSON and normalize
|
|
2758
2762
|
let pastedFeatureCount = 0;
|
|
2759
2763
|
try {
|
|
2760
|
-
|
|
2764
|
+
// First try direct parse (single Feature, Feature[], or FeatureCollection)
|
|
2765
|
+
let parsed;
|
|
2766
|
+
try {
|
|
2767
|
+
parsed = JSON.parse(text);
|
|
2768
|
+
} catch {
|
|
2769
|
+
// If direct parse fails, try wrapping with [] (for "feature, feature" format from editor copy)
|
|
2770
|
+
parsed = JSON.parse('[' + text + ']');
|
|
2771
|
+
}
|
|
2761
2772
|
const features = normalizeToFeatures(parsed);
|
|
2762
2773
|
pastedFeatureCount = features.length;
|
|
2763
2774
|
// Valid GeoJSON - insert formatted features
|
|
@@ -2808,6 +2819,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2808
2819
|
|
|
2809
2820
|
// Force immediate render (not via RAF) to ensure content displays instantly
|
|
2810
2821
|
this.renderViewport();
|
|
2822
|
+
|
|
2823
|
+
// Ensure cursor is visible in viewport after paste
|
|
2824
|
+
// Use RAF to ensure layout is updated before scrolling
|
|
2825
|
+
requestAnimationFrame(() => {
|
|
2826
|
+
this._scrollToCursor();
|
|
2827
|
+
});
|
|
2811
2828
|
}
|
|
2812
2829
|
|
|
2813
2830
|
handleCopy(e: ClipboardEvent): void {
|
|
@@ -3532,6 +3549,96 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3532
3549
|
}
|
|
3533
3550
|
}
|
|
3534
3551
|
|
|
3552
|
+
/**
|
|
3553
|
+
* Emit current-features event when cursor/selection changes
|
|
3554
|
+
* Includes all features that overlap with the selection (or just cursor position if no selection)
|
|
3555
|
+
* Only emits when the set of features changes (not on every cursor move)
|
|
3556
|
+
* @param force - If true, emit even if features haven't changed (used on focus)
|
|
3557
|
+
*/
|
|
3558
|
+
private _emitCurrentFeature(force: boolean = false): void {
|
|
3559
|
+
// Collect feature indices based on cursor or selection
|
|
3560
|
+
const featureIndices = this._getFeatureIndicesForCurrentSelection();
|
|
3561
|
+
|
|
3562
|
+
// Stringify for comparison (deduplication)
|
|
3563
|
+
const indicesKey = JSON.stringify(featureIndices);
|
|
3564
|
+
|
|
3565
|
+
// Only emit if features changed (unless forced)
|
|
3566
|
+
if (!force && indicesKey === this._lastCurrentFeatureIndices) return;
|
|
3567
|
+
this._lastCurrentFeatureIndices = indicesKey;
|
|
3568
|
+
|
|
3569
|
+
if (featureIndices.length === 0) {
|
|
3570
|
+
// Cursor/selection is not in any feature - emit empty FeatureCollection
|
|
3571
|
+
this.dispatchEvent(new CustomEvent('current-features', {
|
|
3572
|
+
detail: { type: 'FeatureCollection', features: [] },
|
|
3573
|
+
bubbles: true,
|
|
3574
|
+
composed: true
|
|
3575
|
+
}));
|
|
3576
|
+
} else {
|
|
3577
|
+
// Get all features that overlap with cursor/selection
|
|
3578
|
+
const allFeatures = this._parseFeatures();
|
|
3579
|
+
const selectedFeatures = featureIndices
|
|
3580
|
+
.map(idx => allFeatures[idx])
|
|
3581
|
+
.filter(f => f != null);
|
|
3582
|
+
|
|
3583
|
+
const featureCollection = {
|
|
3584
|
+
type: 'FeatureCollection',
|
|
3585
|
+
features: selectedFeatures
|
|
3586
|
+
};
|
|
3587
|
+
|
|
3588
|
+
this.dispatchEvent(new CustomEvent('current-features', {
|
|
3589
|
+
detail: featureCollection,
|
|
3590
|
+
bubbles: true,
|
|
3591
|
+
composed: true
|
|
3592
|
+
}));
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
/**
|
|
3597
|
+
* Get all feature indices that overlap with the current cursor position or selection
|
|
3598
|
+
* Returns sorted unique indices
|
|
3599
|
+
*/
|
|
3600
|
+
private _getFeatureIndicesForCurrentSelection(): number[] {
|
|
3601
|
+
const indices = new Set<number>();
|
|
3602
|
+
|
|
3603
|
+
// Determine the line range to check
|
|
3604
|
+
let startLine: number;
|
|
3605
|
+
let endLine: number;
|
|
3606
|
+
|
|
3607
|
+
if (this.selectionStart && this.selectionEnd) {
|
|
3608
|
+
// Selection exists - get all features overlapping the selection
|
|
3609
|
+
startLine = Math.min(this.selectionStart.line, this.selectionEnd.line);
|
|
3610
|
+
endLine = Math.max(this.selectionStart.line, this.selectionEnd.line);
|
|
3611
|
+
} else {
|
|
3612
|
+
// No selection - just use cursor position
|
|
3613
|
+
startLine = this.cursorLine;
|
|
3614
|
+
endLine = this.cursorLine;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
// Find all features that overlap with the line range
|
|
3618
|
+
for (const [, range] of this.featureRanges) {
|
|
3619
|
+
// Check if feature range overlaps with selection range
|
|
3620
|
+
if (range.startLine <= endLine && range.endLine >= startLine) {
|
|
3621
|
+
indices.add(range.featureIndex);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
// Return sorted array for consistent comparison
|
|
3626
|
+
return Array.from(indices).sort((a, b) => a - b);
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
/**
|
|
3630
|
+
* Emit current-features with empty FeatureCollection (used on blur)
|
|
3631
|
+
* Always emits to ensure map is cleared when editor loses focus
|
|
3632
|
+
*/
|
|
3633
|
+
private _emitCurrentFeatureNull(): void {
|
|
3634
|
+
this._lastCurrentFeatureIndices = null;
|
|
3635
|
+
this.dispatchEvent(new CustomEvent('current-features', {
|
|
3636
|
+
detail: { type: 'FeatureCollection', features: [] },
|
|
3637
|
+
bubbles: true,
|
|
3638
|
+
composed: true
|
|
3639
|
+
}));
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3535
3642
|
// ========== UI Updates ==========
|
|
3536
3643
|
|
|
3537
3644
|
updateReadonly() {
|
|
@@ -3573,74 +3680,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3573
3680
|
if (this._editorSuffix) this._editorSuffix.textContent = this.suffix;
|
|
3574
3681
|
}
|
|
3575
3682
|
|
|
3576
|
-
// ========== Theme ==========
|
|
3577
|
-
|
|
3578
|
-
updateThemeCSS() {
|
|
3579
|
-
const darkSelector = this.getAttribute('dark-selector') || '.dark';
|
|
3580
|
-
const darkRule = parseSelectorToHostRule(darkSelector);
|
|
3581
|
-
|
|
3582
|
-
let themeStyle = this._id('theme-styles') as HTMLStyleElement;
|
|
3583
|
-
if (!themeStyle) {
|
|
3584
|
-
themeStyle = _ce('style') as HTMLStyleElement;
|
|
3585
|
-
themeStyle.id = 'theme-styles';
|
|
3586
|
-
this.shadowRoot!.insertBefore(themeStyle, this.shadowRoot!.firstChild);
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
const darkDefaults = {
|
|
3590
|
-
bgColor: '#2b2b2b',
|
|
3591
|
-
textColor: '#a9b7c6',
|
|
3592
|
-
caretColor: '#bbb',
|
|
3593
|
-
gutterBg: '#313335',
|
|
3594
|
-
gutterBorder: '#3c3f41',
|
|
3595
|
-
gutterText: '#606366',
|
|
3596
|
-
jsonKey: '#9876aa',
|
|
3597
|
-
jsonString: '#6a8759',
|
|
3598
|
-
jsonNumber: '#6897bb',
|
|
3599
|
-
jsonBoolean: '#cc7832',
|
|
3600
|
-
jsonNull: '#cc7832',
|
|
3601
|
-
jsonPunct: '#a9b7c6',
|
|
3602
|
-
jsonError: '#ff6b68',
|
|
3603
|
-
controlColor: '#cc7832',
|
|
3604
|
-
controlBg: '#3c3f41',
|
|
3605
|
-
controlBorder: '#5a5a5a',
|
|
3606
|
-
geojsonKey: '#9876aa',
|
|
3607
|
-
geojsonType: '#6a8759',
|
|
3608
|
-
geojsonTypeInvalid: '#ff6b68',
|
|
3609
|
-
jsonKeyInvalid: '#ff6b68'
|
|
3610
|
-
};
|
|
3611
|
-
|
|
3612
|
-
RE_TO_KEBAB.lastIndex = 0;
|
|
3613
|
-
const toKebab = (str: string) => str.replace(RE_TO_KEBAB, '-$1').toLowerCase();
|
|
3614
|
-
const generateVars = (obj: Record<string, string | undefined>) => Object.entries(obj)
|
|
3615
|
-
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
3616
|
-
.map(([k, v]) => `--${toKebab(k)}: ${v};`)
|
|
3617
|
-
.join('\n ');
|
|
3618
|
-
|
|
3619
|
-
const lightVars = generateVars(this.themes.light as Record<string, string | undefined> || {});
|
|
3620
|
-
const darkTheme = { ...darkDefaults, ...this.themes.dark };
|
|
3621
|
-
const darkVars = generateVars(darkTheme as Record<string, string | undefined>);
|
|
3622
|
-
|
|
3623
|
-
let css = lightVars ? `:host {\n ${lightVars}\n }\n` : '';
|
|
3624
|
-
css += `${darkRule} {\n ${darkVars}\n }`;
|
|
3625
|
-
|
|
3626
|
-
themeStyle.textContent = css;
|
|
3627
|
-
}
|
|
3628
|
-
|
|
3629
|
-
setTheme(theme: ThemeSettings): void {
|
|
3630
|
-
if (theme.dark) this.themes.dark = { ...this.themes.dark, ...theme.dark };
|
|
3631
|
-
if (theme.light) this.themes.light = { ...this.themes.light, ...theme.light };
|
|
3632
|
-
this.updateThemeCSS();
|
|
3633
|
-
}
|
|
3634
|
-
|
|
3635
|
-
resetTheme(): void {
|
|
3636
|
-
this.themes = { dark: {}, light: {} };
|
|
3637
|
-
this.updateThemeCSS();
|
|
3638
|
-
}
|
|
3639
|
-
|
|
3640
|
-
getTheme(): ThemeSettings {
|
|
3641
|
-
return { ...this.themes };
|
|
3642
|
-
}
|
|
3643
|
-
|
|
3644
3683
|
/**
|
|
3645
3684
|
* Find all collapsible ranges using the mappings built by _rebuildNodeIdMappings
|
|
3646
3685
|
* This method only READS the existing mappings, it doesn't create new IDs
|
|
@@ -4053,6 +4092,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
4053
4092
|
this.updateView();
|
|
4054
4093
|
this.scheduleRender();
|
|
4055
4094
|
this.emitChange();
|
|
4095
|
+
// Invalidate current-features cache and emit if focused (feature removal changes context)
|
|
4096
|
+
this._lastCurrentFeatureIndices = null;
|
|
4097
|
+
if (this._editorWrapper?.classList.contains('focused')) {
|
|
4098
|
+
this._emitCurrentFeature(true);
|
|
4099
|
+
}
|
|
4056
4100
|
return removed;
|
|
4057
4101
|
}
|
|
4058
4102
|
return undefined;
|
|
@@ -4066,10 +4110,19 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
4066
4110
|
this.lines = [];
|
|
4067
4111
|
this.collapsedNodes.clear();
|
|
4068
4112
|
this.hiddenFeatures.clear();
|
|
4113
|
+
this.cursorLine = 0;
|
|
4114
|
+
this.cursorColumn = 0;
|
|
4115
|
+
this.selectionStart = null;
|
|
4116
|
+
this.selectionEnd = null;
|
|
4069
4117
|
this.updateModel();
|
|
4070
4118
|
this.scheduleRender();
|
|
4071
4119
|
this.updatePlaceholderVisibility();
|
|
4072
4120
|
this.emitChange();
|
|
4121
|
+
// Invalidate current-features cache and emit empty if focused (all features removed)
|
|
4122
|
+
this._lastCurrentFeatureIndices = null;
|
|
4123
|
+
if (this._editorWrapper?.classList.contains('focused')) {
|
|
4124
|
+
this._emitCurrentFeature(true);
|
|
4125
|
+
}
|
|
4073
4126
|
return removed;
|
|
4074
4127
|
}
|
|
4075
4128
|
|
package/src/types.ts
CHANGED
|
@@ -16,33 +16,3 @@ export interface SetOptions {
|
|
|
16
16
|
*/
|
|
17
17
|
collapsed?: string[] | ((feature: Feature | null, index: number) => string[]);
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
/** Theme configuration */
|
|
21
|
-
export interface ThemeConfig {
|
|
22
|
-
bgColor?: string;
|
|
23
|
-
textColor?: string;
|
|
24
|
-
caretColor?: string;
|
|
25
|
-
gutterBg?: string;
|
|
26
|
-
gutterBorder?: string;
|
|
27
|
-
gutterText?: string;
|
|
28
|
-
jsonKey?: string;
|
|
29
|
-
jsonString?: string;
|
|
30
|
-
jsonNumber?: string;
|
|
31
|
-
jsonBoolean?: string;
|
|
32
|
-
jsonNull?: string;
|
|
33
|
-
jsonPunct?: string;
|
|
34
|
-
jsonError?: string;
|
|
35
|
-
controlColor?: string;
|
|
36
|
-
controlBg?: string;
|
|
37
|
-
controlBorder?: string;
|
|
38
|
-
geojsonKey?: string;
|
|
39
|
-
geojsonType?: string;
|
|
40
|
-
geojsonTypeInvalid?: string;
|
|
41
|
-
jsonKeyInvalid?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Theme settings for dark and light modes */
|
|
45
|
-
export interface ThemeSettings {
|
|
46
|
-
dark?: ThemeConfig;
|
|
47
|
-
light?: ThemeConfig;
|
|
48
|
-
}
|
package/src/utils.ts
CHANGED
|
@@ -26,13 +26,3 @@ export function countBrackets(line: string, openBracket: string): BracketCount {
|
|
|
26
26
|
return { open, close };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
* Parse a CSS selector to a :host rule for shadow DOM
|
|
31
|
-
*/
|
|
32
|
-
export function parseSelectorToHostRule(selector: string | null): string {
|
|
33
|
-
if (!selector) return ':host([data-color-scheme="dark"])';
|
|
34
|
-
if (selector.startsWith('.') && !selector.includes(' ')) {
|
|
35
|
-
return `:host(${selector})`;
|
|
36
|
-
}
|
|
37
|
-
return `:host-context(${selector})`;
|
|
38
|
-
}
|