@softwarity/geojson-editor 1.0.17 → 1.0.18
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 +8 -5
- package/dist/geojson-editor.js +2 -2
- package/package.json +2 -2
- package/src/constants.ts +74 -0
- package/src/geojson-editor.css +15 -3
- package/src/geojson-editor.d.ts +155 -0
- package/src/geojson-editor.ts +665 -765
- package/src/internal-types.ts +111 -0
- package/src/syntax-highlighter.ts +197 -0
- package/src/types.ts +48 -0
- package/src/utils.ts +59 -0
- package/src/validation.ts +90 -0
- package/types/geojson-editor.d.ts +146 -488
- package/types/types.d.ts +44 -0
- package/types/geojson-editor.template.d.ts +0 -4
package/src/geojson-editor.ts
CHANGED
|
@@ -1,177 +1,56 @@
|
|
|
1
1
|
import styles from './geojson-editor.css?inline';
|
|
2
2
|
import { getTemplate } from './geojson-editor.template.js';
|
|
3
|
-
import type { Feature
|
|
4
|
-
|
|
5
|
-
// ==========
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
export interface ThemeSettings {
|
|
55
|
-
dark?: ThemeConfig;
|
|
56
|
-
light?: ThemeConfig;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Color metadata for a line */
|
|
60
|
-
interface ColorMeta {
|
|
61
|
-
attributeName: string;
|
|
62
|
-
color: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Boolean metadata for a line */
|
|
66
|
-
interface BooleanMeta {
|
|
67
|
-
attributeName: string;
|
|
68
|
-
value: boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Collapse button metadata */
|
|
72
|
-
interface CollapseButtonMeta {
|
|
73
|
-
nodeKey: string;
|
|
74
|
-
nodeId: string;
|
|
75
|
-
isCollapsed: boolean;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Visibility button metadata */
|
|
79
|
-
interface VisibilityButtonMeta {
|
|
80
|
-
featureKey: string;
|
|
81
|
-
isHidden: boolean;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Line metadata */
|
|
85
|
-
interface LineMeta {
|
|
86
|
-
colors: ColorMeta[];
|
|
87
|
-
booleans: BooleanMeta[];
|
|
88
|
-
collapseButton: CollapseButtonMeta | null;
|
|
89
|
-
visibilityButton: VisibilityButtonMeta | null;
|
|
90
|
-
isHidden: boolean;
|
|
91
|
-
isCollapsed: boolean;
|
|
92
|
-
featureKey: string | null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Visible line data */
|
|
96
|
-
interface VisibleLine {
|
|
97
|
-
index: number;
|
|
98
|
-
content: string;
|
|
99
|
-
meta: LineMeta | undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Feature range in the editor */
|
|
103
|
-
interface FeatureRange {
|
|
104
|
-
startLine: number;
|
|
105
|
-
endLine: number;
|
|
106
|
-
featureIndex: number;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Node range info */
|
|
110
|
-
interface NodeRangeInfo {
|
|
111
|
-
startLine: number;
|
|
112
|
-
endLine: number;
|
|
113
|
-
nodeKey?: string;
|
|
114
|
-
isRootFeature?: boolean;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Collapsible range info */
|
|
118
|
-
interface CollapsibleRange extends NodeRangeInfo {
|
|
119
|
-
nodeId: string;
|
|
120
|
-
openBracket: string;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Editor state snapshot for undo/redo */
|
|
124
|
-
interface EditorSnapshot {
|
|
125
|
-
lines: string[];
|
|
126
|
-
cursorLine: number;
|
|
127
|
-
cursorColumn: number;
|
|
128
|
-
timestamp: number;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** Bracket count result */
|
|
132
|
-
interface BracketCount {
|
|
133
|
-
open: number;
|
|
134
|
-
close: number;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Context stack item */
|
|
138
|
-
interface ContextStackItem {
|
|
139
|
-
context: string;
|
|
140
|
-
isArray: boolean;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/** Input types accepted by API methods */
|
|
144
|
-
export type FeatureInput = Feature | Feature[] | FeatureCollection;
|
|
145
|
-
|
|
146
|
-
// Version injected by Vite build from package.json
|
|
147
|
-
const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'dev';
|
|
148
|
-
|
|
149
|
-
// GeoJSON constants
|
|
150
|
-
const GEOJSON_KEYS: string[] = ['type', 'geometry', 'properties', 'coordinates', 'id', 'features'];
|
|
151
|
-
const GEOMETRY_TYPES: GeometryType[] = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon'];
|
|
152
|
-
|
|
153
|
-
// Pre-compiled regex patterns for performance (avoid re-creation on each call)
|
|
154
|
-
const RE_CONTEXT_GEOMETRY = /"geometry"\s*:/;
|
|
155
|
-
const RE_CONTEXT_PROPERTIES = /"properties"\s*:/;
|
|
156
|
-
const RE_CONTEXT_FEATURES = /"features"\s*:/;
|
|
157
|
-
const RE_COLLAPSED_BRACKET = /^(\s*"[^"]+"\s*:\s*)([{\[])/;
|
|
158
|
-
const RE_COLLAPSED_ROOT = /^(\s*)([{\[]),?\s*$/;
|
|
159
|
-
const RE_ESCAPE_AMP = /&/g;
|
|
160
|
-
const RE_ESCAPE_LT = /</g;
|
|
161
|
-
const RE_ESCAPE_GT = />/g;
|
|
162
|
-
const RE_PUNCTUATION = /([{}[\],:])/g;
|
|
163
|
-
const RE_JSON_KEYS = /"([^"]+)"(<span class="json-punctuation">:<\/span>)/g;
|
|
164
|
-
const RE_TYPE_VALUES = /<span class="geojson-key">"type"<\/span><span class="json-punctuation">:<\/span>(\s*)"([^"]*)"/g;
|
|
165
|
-
const RE_STRING_VALUES = /(<span class="json-punctuation">:<\/span>)(\s*)"([^"]*)"/g;
|
|
166
|
-
const RE_COLOR_HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
167
|
-
const RE_NUMBERS_COLON = /(<span class="json-punctuation">:<\/span>)(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi;
|
|
168
|
-
const RE_NUMBERS_ARRAY = /(<span class="json-punctuation">[\[,]<\/span>)(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi;
|
|
169
|
-
const RE_NUMBERS_START = /^(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gim;
|
|
170
|
-
const RE_BOOLEANS = /(<span class="json-punctuation">:<\/span>)(\s*)(true|false)/g;
|
|
171
|
-
const RE_NULL = /(<span class="json-punctuation">:<\/span>)(\s*)(null)/g;
|
|
172
|
-
const RE_UNRECOGNIZED = /(<\/span>|^)([^<]+)(<span|$)/g;
|
|
173
|
-
const RE_WHITESPACE_ONLY = /^\s*$/;
|
|
174
|
-
const RE_WHITESPACE_SPLIT = /(\s+)/;
|
|
3
|
+
import type { Feature } from 'geojson';
|
|
4
|
+
|
|
5
|
+
// ========== Imports from extracted modules ==========
|
|
6
|
+
import type {
|
|
7
|
+
SetOptions,
|
|
8
|
+
ThemeSettings
|
|
9
|
+
} from './types.js';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
CursorPosition,
|
|
13
|
+
FeatureInput,
|
|
14
|
+
LineMeta,
|
|
15
|
+
VisibleLine,
|
|
16
|
+
FeatureRange,
|
|
17
|
+
NodeRangeInfo,
|
|
18
|
+
EditorSnapshot,
|
|
19
|
+
CollapsedZoneContext,
|
|
20
|
+
CollapsedNodeInfo
|
|
21
|
+
} from './internal-types.js';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
VERSION,
|
|
25
|
+
RE_CONTEXT_GEOMETRY,
|
|
26
|
+
RE_CONTEXT_PROPERTIES,
|
|
27
|
+
RE_CONTEXT_FEATURES,
|
|
28
|
+
RE_ATTR_VALUE,
|
|
29
|
+
RE_ATTR_VALUE_SINGLE,
|
|
30
|
+
RE_NORMALIZE_COLOR,
|
|
31
|
+
RE_COLOR_HEX,
|
|
32
|
+
RE_IS_FEATURE,
|
|
33
|
+
RE_KV_MATCH,
|
|
34
|
+
RE_ROOT_MATCH,
|
|
35
|
+
RE_BRACKET_POS,
|
|
36
|
+
RE_IS_WORD_CHAR,
|
|
37
|
+
RE_ATTR_AND_BOOL_VALUE,
|
|
38
|
+
RE_TO_KEBAB,
|
|
39
|
+
RE_OPEN_BRACES,
|
|
40
|
+
RE_CLOSE_BRACES,
|
|
41
|
+
RE_OPEN_BRACKETS,
|
|
42
|
+
RE_CLOSE_BRACKET
|
|
43
|
+
} from './constants.js';
|
|
44
|
+
|
|
45
|
+
import { createElement, getFeatureKey, countBrackets, parseSelectorToHostRule } from './utils.js';
|
|
46
|
+
import { validateGeoJSON, normalizeToFeatures } from './validation.js';
|
|
47
|
+
import { highlightSyntax, namedColorToHex, isNamedColor } from './syntax-highlighter.js';
|
|
48
|
+
|
|
49
|
+
// Re-export public types
|
|
50
|
+
export type { SetOptions, ThemeConfig, ThemeSettings } from './types.js';
|
|
51
|
+
|
|
52
|
+
// Alias for minification
|
|
53
|
+
const _ce = createElement;
|
|
175
54
|
|
|
176
55
|
/**
|
|
177
56
|
* GeoJSON Editor Web Component
|
|
@@ -211,8 +90,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
211
90
|
selectionEnd: CursorPosition | null = null;
|
|
212
91
|
|
|
213
92
|
// ========== Debounce ==========
|
|
214
|
-
private renderTimer:
|
|
215
|
-
private inputTimer:
|
|
93
|
+
private renderTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
94
|
+
private inputTimer: ReturnType<typeof setTimeout> | undefined = undefined;
|
|
216
95
|
|
|
217
96
|
// ========== Theme ==========
|
|
218
97
|
themes: ThemeSettings = { dark: {}, light: {} };
|
|
@@ -229,19 +108,38 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
229
108
|
private _isSelecting: boolean = false;
|
|
230
109
|
private _isComposing: boolean = false;
|
|
231
110
|
private _blockRender: boolean = false;
|
|
111
|
+
private _insertMode: boolean = true; // true = insert, false = overwrite
|
|
232
112
|
private _charWidth: number | null = null;
|
|
233
113
|
private _contextMapCache: Map<number, string> | null = null;
|
|
234
114
|
private _contextMapLinesLength: number = 0;
|
|
235
115
|
private _contextMapFirstLine: string | undefined = undefined;
|
|
236
116
|
private _contextMapLastLine: string | undefined = undefined;
|
|
237
117
|
|
|
118
|
+
// ========== Cached DOM Elements ==========
|
|
119
|
+
private _viewport: HTMLElement | null = null;
|
|
120
|
+
private _linesContainer: HTMLElement | null = null;
|
|
121
|
+
private _scrollContent: HTMLElement | null = null;
|
|
122
|
+
private _hiddenTextarea: HTMLTextAreaElement | null = null;
|
|
123
|
+
private _gutterContent: HTMLElement | null = null;
|
|
124
|
+
private _gutterScrollContent: HTMLElement | null = null;
|
|
125
|
+
private _gutterScroll: HTMLElement | null = null;
|
|
126
|
+
private _gutter: HTMLElement | null = null;
|
|
127
|
+
private _clearBtn: HTMLButtonElement | null = null;
|
|
128
|
+
private _editorWrapper: HTMLElement | null = null;
|
|
129
|
+
private _placeholderLayer: HTMLElement | null = null;
|
|
130
|
+
private _editorPrefix: HTMLElement | null = null;
|
|
131
|
+
private _editorSuffix: HTMLElement | null = null;
|
|
132
|
+
|
|
238
133
|
constructor() {
|
|
239
134
|
super();
|
|
240
135
|
this.attachShadow({ mode: 'open' });
|
|
241
136
|
}
|
|
242
137
|
|
|
138
|
+
// Alias for shadowRoot.getElementById (minification)
|
|
139
|
+
private _id(id: string) { return this.shadowRoot!.getElementById(id); }
|
|
140
|
+
|
|
243
141
|
// ========== Render Cache ==========
|
|
244
|
-
_invalidateRenderCache() {
|
|
142
|
+
private _invalidateRenderCache() {
|
|
245
143
|
this._lastStartIndex = -1;
|
|
246
144
|
this._lastEndIndex = -1;
|
|
247
145
|
this._lastTotalLines = -1;
|
|
@@ -253,7 +151,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
253
151
|
* Create a snapshot of current editor state
|
|
254
152
|
* @returns {Object} State snapshot
|
|
255
153
|
*/
|
|
256
|
-
_createSnapshot() {
|
|
154
|
+
private _createSnapshot() {
|
|
257
155
|
return {
|
|
258
156
|
lines: [...this.lines],
|
|
259
157
|
cursorLine: this.cursorLine,
|
|
@@ -264,9 +162,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
264
162
|
|
|
265
163
|
/**
|
|
266
164
|
* Restore editor state from snapshot
|
|
267
|
-
* @param {Object} snapshot - State to restore
|
|
268
165
|
*/
|
|
269
|
-
_restoreSnapshot(snapshot) {
|
|
166
|
+
private _restoreSnapshot(snapshot: EditorSnapshot): void {
|
|
270
167
|
this.lines = [...snapshot.lines];
|
|
271
168
|
this.cursorLine = snapshot.cursorLine;
|
|
272
169
|
this.cursorColumn = snapshot.cursorColumn;
|
|
@@ -281,7 +178,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
281
178
|
* Save current state to undo stack before making changes
|
|
282
179
|
* @param {string} actionType - Type of action (insert, delete, paste, etc.)
|
|
283
180
|
*/
|
|
284
|
-
_saveToHistory(actionType = 'edit') {
|
|
181
|
+
private _saveToHistory(actionType = 'edit') {
|
|
285
182
|
const now = Date.now();
|
|
286
183
|
const shouldGroup = (
|
|
287
184
|
actionType === this._lastActionType &&
|
|
@@ -310,7 +207,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
310
207
|
* Undo last action
|
|
311
208
|
* @returns {boolean} True if undo was performed
|
|
312
209
|
*/
|
|
313
|
-
undo() {
|
|
210
|
+
undo(): boolean {
|
|
314
211
|
if (this._undoStack.length === 0) return false;
|
|
315
212
|
|
|
316
213
|
// Save current state to redo stack
|
|
@@ -318,7 +215,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
318
215
|
|
|
319
216
|
// Restore previous state
|
|
320
217
|
const previousState = this._undoStack.pop();
|
|
321
|
-
this._restoreSnapshot(previousState);
|
|
218
|
+
if (previousState) this._restoreSnapshot(previousState);
|
|
322
219
|
|
|
323
220
|
// Reset action tracking
|
|
324
221
|
this._lastActionType = null;
|
|
@@ -331,7 +228,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
331
228
|
* Redo previously undone action
|
|
332
229
|
* @returns {boolean} True if redo was performed
|
|
333
230
|
*/
|
|
334
|
-
redo() {
|
|
231
|
+
redo(): boolean {
|
|
335
232
|
if (this._redoStack.length === 0) return false;
|
|
336
233
|
|
|
337
234
|
// Save current state to undo stack
|
|
@@ -339,7 +236,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
339
236
|
|
|
340
237
|
// Restore next state
|
|
341
238
|
const nextState = this._redoStack.pop();
|
|
342
|
-
this._restoreSnapshot(nextState);
|
|
239
|
+
if (nextState) this._restoreSnapshot(nextState);
|
|
343
240
|
|
|
344
241
|
// Reset action tracking
|
|
345
242
|
this._lastActionType = null;
|
|
@@ -351,7 +248,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
351
248
|
/**
|
|
352
249
|
* Clear undo/redo history
|
|
353
250
|
*/
|
|
354
|
-
clearHistory() {
|
|
251
|
+
clearHistory(): void {
|
|
355
252
|
this._undoStack = [];
|
|
356
253
|
this._redoStack = [];
|
|
357
254
|
this._lastActionType = null;
|
|
@@ -362,7 +259,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
362
259
|
* Check if undo is available
|
|
363
260
|
* @returns {boolean}
|
|
364
261
|
*/
|
|
365
|
-
canUndo() {
|
|
262
|
+
canUndo(): boolean {
|
|
366
263
|
return this._undoStack.length > 0;
|
|
367
264
|
}
|
|
368
265
|
|
|
@@ -370,21 +267,19 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
370
267
|
* Check if redo is available
|
|
371
268
|
* @returns {boolean}
|
|
372
269
|
*/
|
|
373
|
-
canRedo() {
|
|
270
|
+
canRedo(): boolean {
|
|
374
271
|
return this._redoStack.length > 0;
|
|
375
272
|
}
|
|
376
273
|
|
|
377
274
|
// ========== Unique ID Generation ==========
|
|
378
|
-
_generateNodeId() {
|
|
275
|
+
private _generateNodeId() {
|
|
379
276
|
return `node_${++this._nodeIdCounter}`;
|
|
380
277
|
}
|
|
381
278
|
|
|
382
279
|
/**
|
|
383
280
|
* Check if a line is inside a collapsed node (hidden lines between opening and closing)
|
|
384
|
-
* @param {number} lineIndex - The line index to check
|
|
385
|
-
* @returns {Object|null} - The collapsed range info or null
|
|
386
281
|
*/
|
|
387
|
-
_getCollapsedRangeForLine(lineIndex) {
|
|
282
|
+
private _getCollapsedRangeForLine(lineIndex: number): CollapsedNodeInfo | null {
|
|
388
283
|
for (const [nodeId, info] of this._nodeIdToLines) {
|
|
389
284
|
// Lines strictly between opening and closing are hidden
|
|
390
285
|
if (this.collapsedNodes.has(nodeId) && lineIndex > info.startLine && lineIndex < info.endLine) {
|
|
@@ -396,10 +291,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
396
291
|
|
|
397
292
|
/**
|
|
398
293
|
* Check if cursor is on the closing line of a collapsed node
|
|
399
|
-
* @param {number} lineIndex - The line index to check
|
|
400
|
-
* @returns {Object|null} - The collapsed range info or null
|
|
401
294
|
*/
|
|
402
|
-
_getCollapsedClosingLine(lineIndex) {
|
|
295
|
+
private _getCollapsedClosingLine(lineIndex: number): CollapsedNodeInfo | null {
|
|
403
296
|
for (const [nodeId, info] of this._nodeIdToLines) {
|
|
404
297
|
if (this.collapsedNodes.has(nodeId) && lineIndex === info.endLine) {
|
|
405
298
|
return { nodeId, ...info };
|
|
@@ -410,10 +303,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
410
303
|
|
|
411
304
|
/**
|
|
412
305
|
* Get the position of the closing bracket on a line
|
|
413
|
-
* @param {string} line - The line content
|
|
414
|
-
* @returns {number} - Position of bracket or -1
|
|
415
306
|
*/
|
|
416
|
-
_getClosingBracketPos(line) {
|
|
307
|
+
private _getClosingBracketPos(line: string): number {
|
|
417
308
|
// Find the last ] or } on the line
|
|
418
309
|
const lastBracket = Math.max(line.lastIndexOf(']'), line.lastIndexOf('}'));
|
|
419
310
|
return lastBracket;
|
|
@@ -421,29 +312,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
421
312
|
|
|
422
313
|
/**
|
|
423
314
|
* Check if cursor is on the opening line of a collapsed node
|
|
424
|
-
* @param {number} lineIndex - The line index to check
|
|
425
|
-
* @returns {Object|null} - The collapsed range info or null
|
|
426
315
|
*/
|
|
427
|
-
_getCollapsedNodeAtLine(lineIndex) {
|
|
316
|
+
private _getCollapsedNodeAtLine(lineIndex: number): CollapsedNodeInfo | null {
|
|
428
317
|
const nodeId = this._lineToNodeId.get(lineIndex);
|
|
429
318
|
if (nodeId && this.collapsedNodes.has(nodeId)) {
|
|
430
319
|
const info = this._nodeIdToLines.get(nodeId);
|
|
431
|
-
return { nodeId, ...info };
|
|
432
|
-
}
|
|
433
|
-
return null;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Check if cursor is on a line that has a collapsible node (expanded or collapsed)
|
|
438
|
-
* @param {number} lineIndex - The line index to check
|
|
439
|
-
* @returns {Object|null} - The node info with isCollapsed flag or null
|
|
440
|
-
*/
|
|
441
|
-
_getCollapsibleNodeAtLine(lineIndex) {
|
|
442
|
-
const nodeId = this._lineToNodeId.get(lineIndex);
|
|
443
|
-
if (nodeId) {
|
|
444
|
-
const info = this._nodeIdToLines.get(nodeId);
|
|
445
|
-
const isCollapsed = this.collapsedNodes.has(nodeId);
|
|
446
|
-
return { nodeId, isCollapsed, ...info };
|
|
320
|
+
if (info) return { nodeId, ...info };
|
|
447
321
|
}
|
|
448
322
|
return null;
|
|
449
323
|
}
|
|
@@ -451,16 +325,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
451
325
|
/**
|
|
452
326
|
* Find the innermost expanded node that contains the given line
|
|
453
327
|
* Used for Shift+Tab to collapse the parent node from anywhere inside it
|
|
454
|
-
* @param {number} lineIndex - The line index to check
|
|
455
|
-
* @returns {Object|null} - The containing node info or null
|
|
456
328
|
*/
|
|
457
|
-
_getContainingExpandedNode(lineIndex) {
|
|
458
|
-
let bestMatch = null;
|
|
459
|
-
|
|
329
|
+
private _getContainingExpandedNode(lineIndex: number): CollapsedNodeInfo | null {
|
|
330
|
+
let bestMatch: CollapsedNodeInfo | null = null;
|
|
331
|
+
|
|
460
332
|
for (const [nodeId, info] of this._nodeIdToLines) {
|
|
461
333
|
// Skip collapsed nodes
|
|
462
334
|
if (this.collapsedNodes.has(nodeId)) continue;
|
|
463
|
-
|
|
335
|
+
|
|
464
336
|
// Check if line is within this node's range
|
|
465
337
|
if (lineIndex >= info.startLine && lineIndex <= info.endLine) {
|
|
466
338
|
// Prefer the innermost (smallest) containing node
|
|
@@ -469,15 +341,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
469
341
|
}
|
|
470
342
|
}
|
|
471
343
|
}
|
|
472
|
-
|
|
344
|
+
|
|
473
345
|
return bestMatch;
|
|
474
346
|
}
|
|
475
347
|
|
|
476
348
|
/**
|
|
477
349
|
* Delete an entire collapsed node (opening line to closing line)
|
|
478
|
-
* @param {Object} range - The range info {startLine, endLine}
|
|
479
350
|
*/
|
|
480
|
-
_deleteCollapsedNode(range) {
|
|
351
|
+
private _deleteCollapsedNode(range: CollapsedNodeInfo): void {
|
|
481
352
|
this._saveToHistory('delete');
|
|
482
353
|
|
|
483
354
|
// Remove all lines from startLine to endLine
|
|
@@ -495,7 +366,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
495
366
|
* Rebuild nodeId mappings after content changes
|
|
496
367
|
* Preserves collapsed state by matching nodeKey + sequential occurrence
|
|
497
368
|
*/
|
|
498
|
-
_rebuildNodeIdMappings() {
|
|
369
|
+
private _rebuildNodeIdMappings() {
|
|
499
370
|
// Save old state to try to preserve collapsed nodes
|
|
500
371
|
const oldCollapsed = new Set(this.collapsedNodes);
|
|
501
372
|
const oldNodeKeyMap = new Map(); // nodeId -> nodeKey
|
|
@@ -524,26 +395,29 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
524
395
|
const line = this.lines[i];
|
|
525
396
|
|
|
526
397
|
// Match "key": { or "key": [
|
|
527
|
-
const kvMatch = line.match(
|
|
398
|
+
const kvMatch = line.match(RE_KV_MATCH);
|
|
528
399
|
// Also match standalone { or {, (root Feature objects)
|
|
529
|
-
const rootMatch = !kvMatch && line.match(
|
|
400
|
+
const rootMatch = !kvMatch && line.match(RE_ROOT_MATCH);
|
|
530
401
|
|
|
531
402
|
if (!kvMatch && !rootMatch) continue;
|
|
532
|
-
|
|
533
|
-
let nodeKey
|
|
534
|
-
|
|
403
|
+
|
|
404
|
+
let nodeKey: string;
|
|
405
|
+
let openBracket: string;
|
|
406
|
+
|
|
535
407
|
if (kvMatch) {
|
|
536
408
|
nodeKey = kvMatch[1];
|
|
537
409
|
openBracket = kvMatch[2];
|
|
538
|
-
} else {
|
|
410
|
+
} else if (rootMatch) {
|
|
539
411
|
// Root object - use special key based on line number and bracket type
|
|
540
412
|
openBracket = rootMatch[1];
|
|
541
413
|
nodeKey = `__root_${openBracket}_${i}`;
|
|
414
|
+
} else {
|
|
415
|
+
continue;
|
|
542
416
|
}
|
|
543
417
|
|
|
544
418
|
// Check if closes on same line
|
|
545
419
|
const rest = line.substring(line.indexOf(openBracket) + 1);
|
|
546
|
-
const counts =
|
|
420
|
+
const counts = countBrackets(rest, openBracket);
|
|
547
421
|
if (counts.close > counts.open) continue;
|
|
548
422
|
|
|
549
423
|
const endLine = this._findClosingLine(i, openBracket);
|
|
@@ -577,10 +451,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
577
451
|
// ========== Lifecycle ==========
|
|
578
452
|
connectedCallback() {
|
|
579
453
|
this.render();
|
|
454
|
+
this._cacheElements();
|
|
580
455
|
this.setupEventListeners();
|
|
581
456
|
this.updatePrefixSuffix();
|
|
582
457
|
this.updateThemeCSS();
|
|
583
|
-
|
|
458
|
+
|
|
584
459
|
if (this.value) {
|
|
585
460
|
this.setValue(this.value);
|
|
586
461
|
}
|
|
@@ -601,7 +476,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
601
476
|
}
|
|
602
477
|
}
|
|
603
478
|
|
|
604
|
-
attributeChangedCallback(name, oldValue, newValue) {
|
|
479
|
+
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
|
|
605
480
|
if (oldValue === newValue) return;
|
|
606
481
|
|
|
607
482
|
switch (name) {
|
|
@@ -629,27 +504,48 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
629
504
|
|
|
630
505
|
// ========== Initial Render ==========
|
|
631
506
|
render() {
|
|
632
|
-
const
|
|
507
|
+
const shadowRoot = this.shadowRoot!;
|
|
508
|
+
const styleEl = _ce('style');
|
|
633
509
|
styleEl.textContent = styles;
|
|
634
|
-
|
|
635
|
-
const template =
|
|
510
|
+
|
|
511
|
+
const template = _ce('div');
|
|
636
512
|
template.innerHTML = getTemplate(this.placeholder, VERSION);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
513
|
+
|
|
514
|
+
shadowRoot.innerHTML = '';
|
|
515
|
+
shadowRoot.appendChild(styleEl);
|
|
640
516
|
while (template.firstChild) {
|
|
641
|
-
|
|
517
|
+
shadowRoot.appendChild(template.firstChild);
|
|
642
518
|
}
|
|
643
519
|
}
|
|
644
520
|
|
|
521
|
+
// ========== DOM Element Cache ==========
|
|
522
|
+
private _cacheElements() {
|
|
523
|
+
this._viewport = this._id('viewport');
|
|
524
|
+
this._linesContainer = this._id('linesContainer');
|
|
525
|
+
this._scrollContent = this._id('scrollContent');
|
|
526
|
+
this._hiddenTextarea = this._id('hiddenTextarea') as HTMLTextAreaElement;
|
|
527
|
+
this._gutterContent = this._id('gutterContent');
|
|
528
|
+
this._gutterScrollContent = this._id('gutterScrollContent');
|
|
529
|
+
this._gutterScroll = this._id('gutterScroll');
|
|
530
|
+
this._gutter = this.shadowRoot!.querySelector('.gutter');
|
|
531
|
+
this._clearBtn = this._id('clearBtn') as HTMLButtonElement;
|
|
532
|
+
this._editorWrapper = this.shadowRoot!.querySelector('.editor-wrapper');
|
|
533
|
+
this._placeholderLayer = this._id('placeholderLayer');
|
|
534
|
+
this._editorPrefix = this._id('editorPrefix');
|
|
535
|
+
this._editorSuffix = this._id('editorSuffix');
|
|
536
|
+
}
|
|
537
|
+
|
|
645
538
|
// ========== Event Listeners ==========
|
|
646
539
|
setupEventListeners() {
|
|
647
|
-
const hiddenTextarea = this.
|
|
648
|
-
const viewport = this.
|
|
649
|
-
const gutterContent = this.
|
|
650
|
-
const gutter = this.
|
|
651
|
-
const clearBtn = this.
|
|
652
|
-
const editorWrapper = this.
|
|
540
|
+
const hiddenTextarea = this._hiddenTextarea;
|
|
541
|
+
const viewport = this._viewport;
|
|
542
|
+
const gutterContent = this._gutterContent;
|
|
543
|
+
const gutter = this._gutter;
|
|
544
|
+
const clearBtn = this._clearBtn;
|
|
545
|
+
const editorWrapper = this._editorWrapper;
|
|
546
|
+
|
|
547
|
+
// Guard: all elements must exist
|
|
548
|
+
if (!hiddenTextarea || !viewport || !gutterContent || !gutter || !clearBtn || !editorWrapper) return;
|
|
653
549
|
|
|
654
550
|
// Mouse selection state
|
|
655
551
|
this._isSelecting = false;
|
|
@@ -658,7 +554,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
658
554
|
// Editor inline control clicks (color swatches, checkboxes, visibility icons)
|
|
659
555
|
// Use capture phase to intercept before mousedown
|
|
660
556
|
viewport.addEventListener('click', (e) => {
|
|
661
|
-
this.handleEditorClick(e);
|
|
557
|
+
this.handleEditorClick(e as MouseEvent);
|
|
662
558
|
}, true);
|
|
663
559
|
|
|
664
560
|
viewport.addEventListener('mousedown', (e: MouseEvent) => {
|
|
@@ -687,13 +583,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
687
583
|
return;
|
|
688
584
|
}
|
|
689
585
|
}
|
|
690
|
-
|
|
586
|
+
|
|
691
587
|
// Prevent default to avoid losing focus after click
|
|
692
588
|
e.preventDefault();
|
|
693
|
-
|
|
589
|
+
|
|
694
590
|
// Calculate click position
|
|
695
591
|
const pos = this._getPositionFromClick(e);
|
|
696
|
-
|
|
592
|
+
|
|
697
593
|
if (e.shiftKey && this.selectionStart) {
|
|
698
594
|
// Shift+click: extend selection
|
|
699
595
|
this.selectionEnd = pos;
|
|
@@ -707,7 +603,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
707
603
|
this.selectionEnd = null;
|
|
708
604
|
this._isSelecting = true;
|
|
709
605
|
}
|
|
710
|
-
|
|
606
|
+
|
|
711
607
|
// Focus textarea
|
|
712
608
|
hiddenTextarea.focus();
|
|
713
609
|
this._invalidateRenderCache();
|
|
@@ -715,19 +611,18 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
715
611
|
});
|
|
716
612
|
|
|
717
613
|
// Mouse move for drag selection
|
|
718
|
-
viewport.addEventListener('mousemove', (e) => {
|
|
614
|
+
viewport.addEventListener('mousemove', (e: MouseEvent) => {
|
|
719
615
|
if (!this._isSelecting) return;
|
|
720
|
-
|
|
721
616
|
const pos = this._getPositionFromClick(e);
|
|
722
617
|
this.selectionEnd = pos;
|
|
723
618
|
this.cursorLine = pos.line;
|
|
724
619
|
this.cursorColumn = pos.column;
|
|
725
|
-
|
|
620
|
+
|
|
726
621
|
// Auto-scroll when near edges
|
|
727
622
|
const rect = viewport.getBoundingClientRect();
|
|
728
623
|
const scrollMargin = 30; // pixels from edge to start scrolling
|
|
729
624
|
const scrollSpeed = 20; // pixels to scroll per frame
|
|
730
|
-
|
|
625
|
+
|
|
731
626
|
if (e.clientY < rect.top + scrollMargin) {
|
|
732
627
|
// Near top edge, scroll up
|
|
733
628
|
viewport.scrollTop -= scrollSpeed;
|
|
@@ -735,7 +630,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
735
630
|
// Near bottom edge, scroll down
|
|
736
631
|
viewport.scrollTop += scrollSpeed;
|
|
737
632
|
}
|
|
738
|
-
|
|
633
|
+
|
|
739
634
|
this._invalidateRenderCache();
|
|
740
635
|
this.scheduleRender();
|
|
741
636
|
});
|
|
@@ -763,7 +658,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
763
658
|
viewport.addEventListener('scroll', () => {
|
|
764
659
|
if (isRendering) return;
|
|
765
660
|
this.syncGutterScroll();
|
|
766
|
-
|
|
661
|
+
|
|
767
662
|
// Use requestAnimationFrame to batch scroll updates
|
|
768
663
|
if (!this._scrollRaf) {
|
|
769
664
|
this._scrollRaf = requestAnimationFrame(() => {
|
|
@@ -794,38 +689,38 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
794
689
|
});
|
|
795
690
|
|
|
796
691
|
hiddenTextarea.addEventListener('keydown', (e) => {
|
|
797
|
-
this.handleKeydown(e);
|
|
692
|
+
this.handleKeydown(e as KeyboardEvent);
|
|
798
693
|
});
|
|
799
694
|
|
|
800
695
|
// Paste handling
|
|
801
696
|
hiddenTextarea.addEventListener('paste', (e) => {
|
|
802
|
-
this.handlePaste(e);
|
|
697
|
+
this.handlePaste(e as ClipboardEvent);
|
|
803
698
|
});
|
|
804
699
|
|
|
805
700
|
// Copy handling
|
|
806
701
|
hiddenTextarea.addEventListener('copy', (e) => {
|
|
807
|
-
this.handleCopy(e);
|
|
702
|
+
this.handleCopy(e as ClipboardEvent);
|
|
808
703
|
});
|
|
809
704
|
|
|
810
705
|
// Cut handling
|
|
811
706
|
hiddenTextarea.addEventListener('cut', (e) => {
|
|
812
|
-
this.handleCut(e);
|
|
707
|
+
this.handleCut(e as ClipboardEvent);
|
|
813
708
|
});
|
|
814
709
|
|
|
815
710
|
// Gutter interactions
|
|
816
711
|
gutterContent.addEventListener('click', (e) => {
|
|
817
|
-
this.handleGutterClick(e);
|
|
712
|
+
this.handleGutterClick(e as MouseEvent);
|
|
818
713
|
});
|
|
819
|
-
|
|
714
|
+
|
|
820
715
|
// Prevent gutter from stealing focus
|
|
821
716
|
gutter.addEventListener('mousedown', (e) => {
|
|
822
717
|
e.preventDefault();
|
|
823
718
|
});
|
|
824
719
|
|
|
825
720
|
// Wheel on gutter -> scroll viewport
|
|
826
|
-
gutter.addEventListener('wheel', (e
|
|
721
|
+
gutter.addEventListener('wheel', (e) => {
|
|
827
722
|
e.preventDefault();
|
|
828
|
-
viewport.scrollTop += e.deltaY;
|
|
723
|
+
viewport.scrollTop += (e as WheelEvent).deltaY;
|
|
829
724
|
});
|
|
830
725
|
|
|
831
726
|
// Clear button
|
|
@@ -842,7 +737,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
842
737
|
/**
|
|
843
738
|
* Set the editor content from a string value
|
|
844
739
|
*/
|
|
845
|
-
setValue(value, autoCollapse = true) {
|
|
740
|
+
setValue(value: string | null, autoCollapse = true): void {
|
|
846
741
|
// Save to history only if there's existing content
|
|
847
742
|
if (this.lines.length > 0) {
|
|
848
743
|
this._saveToHistory('setValue');
|
|
@@ -941,7 +836,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
941
836
|
for (let i = 0; i < this.lines.length; i++) {
|
|
942
837
|
const line = this.lines[i];
|
|
943
838
|
|
|
944
|
-
if (!inFeature &&
|
|
839
|
+
if (!inFeature && RE_IS_FEATURE.test(line)) {
|
|
945
840
|
// Find opening brace
|
|
946
841
|
let startLine = i;
|
|
947
842
|
for (let j = i; j >= 0; j--) {
|
|
@@ -957,7 +852,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
957
852
|
|
|
958
853
|
// Count braces from start to current line
|
|
959
854
|
for (let k = startLine; k <= i; k++) {
|
|
960
|
-
const counts =
|
|
855
|
+
const counts = countBrackets(this.lines[k], '{');
|
|
961
856
|
if (k === startLine) {
|
|
962
857
|
braceDepth += (counts.open - 1) - counts.close;
|
|
963
858
|
} else {
|
|
@@ -966,10 +861,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
966
861
|
}
|
|
967
862
|
|
|
968
863
|
if (featureIndex < parsed.features.length) {
|
|
969
|
-
currentFeatureKey =
|
|
864
|
+
currentFeatureKey = getFeatureKey(parsed.features[featureIndex]);
|
|
970
865
|
}
|
|
971
866
|
} else if (inFeature) {
|
|
972
|
-
const counts =
|
|
867
|
+
const counts = countBrackets(line, '{');
|
|
973
868
|
braceDepth += counts.open - counts.close;
|
|
974
869
|
|
|
975
870
|
if (braceDepth <= 0) {
|
|
@@ -1001,7 +896,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1001
896
|
|
|
1002
897
|
for (let i = 0; i < this.lines.length; i++) {
|
|
1003
898
|
const line = this.lines[i];
|
|
1004
|
-
const meta = {
|
|
899
|
+
const meta: LineMeta = {
|
|
1005
900
|
colors: [],
|
|
1006
901
|
booleans: [],
|
|
1007
902
|
collapseButton: null,
|
|
@@ -1011,18 +906,24 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1011
906
|
featureKey: null
|
|
1012
907
|
};
|
|
1013
908
|
|
|
1014
|
-
// Detect colors
|
|
1015
|
-
|
|
1016
|
-
let
|
|
1017
|
-
while ((
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
909
|
+
// Detect colors and booleans in a single pass
|
|
910
|
+
RE_ATTR_VALUE.lastIndex = 0;
|
|
911
|
+
let match: RegExpExecArray | null;
|
|
912
|
+
while ((match = RE_ATTR_VALUE.exec(line)) !== null) {
|
|
913
|
+
const [, attributeName, strValue, boolValue] = match;
|
|
914
|
+
if (boolValue) {
|
|
915
|
+
// Boolean value
|
|
916
|
+
meta.booleans.push({ attributeName, value: boolValue === 'true' });
|
|
917
|
+
} else if (strValue) {
|
|
918
|
+
// String value - check if it's a color
|
|
919
|
+
if (RE_COLOR_HEX.test(strValue)) {
|
|
920
|
+
// Hex color (#fff or #ffffff)
|
|
921
|
+
meta.colors.push({ attributeName, color: strValue });
|
|
922
|
+
} else if (isNamedColor(strValue)) {
|
|
923
|
+
// Named CSS color (red, blue, etc.) - validated via browser
|
|
924
|
+
meta.colors.push({ attributeName, color: strValue });
|
|
925
|
+
}
|
|
926
|
+
}
|
|
1026
927
|
}
|
|
1027
928
|
|
|
1028
929
|
// Check if line starts a collapsible node
|
|
@@ -1084,8 +985,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1084
985
|
|
|
1085
986
|
// Reset render cache to force re-render
|
|
1086
987
|
this._invalidateRenderCache();
|
|
1087
|
-
this._lastEndIndex = -1;
|
|
1088
|
-
this._lastTotalLines = -1;
|
|
1089
988
|
}
|
|
1090
989
|
|
|
1091
990
|
// ========== Rendering ==========
|
|
@@ -1093,7 +992,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1093
992
|
scheduleRender() {
|
|
1094
993
|
if (this.renderTimer) return;
|
|
1095
994
|
this.renderTimer = requestAnimationFrame(() => {
|
|
1096
|
-
this.renderTimer =
|
|
995
|
+
this.renderTimer = undefined;
|
|
1097
996
|
this.renderViewport();
|
|
1098
997
|
});
|
|
1099
998
|
}
|
|
@@ -1103,10 +1002,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1103
1002
|
if (this._blockRender) {
|
|
1104
1003
|
return;
|
|
1105
1004
|
}
|
|
1106
|
-
const viewport = this.
|
|
1107
|
-
const linesContainer = this.
|
|
1108
|
-
const scrollContent = this.
|
|
1109
|
-
const gutterContent = this.shadowRoot.getElementById('gutterContent');
|
|
1005
|
+
const viewport = this._viewport;
|
|
1006
|
+
const linesContainer = this._linesContainer;
|
|
1007
|
+
const scrollContent = this._scrollContent;
|
|
1110
1008
|
|
|
1111
1009
|
if (!viewport || !linesContainer) return;
|
|
1112
1010
|
|
|
@@ -1142,17 +1040,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1142
1040
|
|
|
1143
1041
|
// Build context map for syntax highlighting
|
|
1144
1042
|
const contextMap = this._buildContextMap();
|
|
1145
|
-
|
|
1043
|
+
|
|
1146
1044
|
// Check if editor is focused (for cursor display)
|
|
1147
|
-
const
|
|
1148
|
-
const isFocused = editorWrapper?.classList.contains('focused');
|
|
1045
|
+
const isFocused = this._editorWrapper?.classList.contains('focused');
|
|
1149
1046
|
|
|
1150
1047
|
// Render visible lines
|
|
1151
1048
|
const fragment = document.createDocumentFragment();
|
|
1152
1049
|
|
|
1153
1050
|
// Handle empty editor: render an empty line with cursor
|
|
1154
1051
|
if (totalLines === 0) {
|
|
1155
|
-
const lineEl =
|
|
1052
|
+
const lineEl = _ce('div');
|
|
1156
1053
|
lineEl.className = 'line empty-line';
|
|
1157
1054
|
lineEl.dataset.lineIndex = '0';
|
|
1158
1055
|
if (isFocused) {
|
|
@@ -1169,7 +1066,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1169
1066
|
const lineData = this.visibleLines[i];
|
|
1170
1067
|
if (!lineData) continue;
|
|
1171
1068
|
|
|
1172
|
-
const lineEl =
|
|
1069
|
+
const lineEl = _ce('div');
|
|
1173
1070
|
lineEl.className = 'line';
|
|
1174
1071
|
lineEl.dataset.lineIndex = String(lineData.index);
|
|
1175
1072
|
|
|
@@ -1188,8 +1085,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1188
1085
|
}
|
|
1189
1086
|
|
|
1190
1087
|
// Highlight syntax and add cursor if this is the cursor line and editor is focused
|
|
1191
|
-
const context = contextMap.get(lineData.index);
|
|
1192
|
-
let html =
|
|
1088
|
+
const context = contextMap.get(lineData.index) || 'Feature';
|
|
1089
|
+
let html = highlightSyntax(lineData.content, context, lineData.meta);
|
|
1193
1090
|
|
|
1194
1091
|
// Add selection highlight if line is in selection
|
|
1195
1092
|
if (isFocused && this._hasSelection()) {
|
|
@@ -1216,20 +1113,27 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1216
1113
|
/**
|
|
1217
1114
|
* Insert cursor element at the specified column position
|
|
1218
1115
|
* Uses absolute positioning to avoid affecting text layout
|
|
1116
|
+
* In overwrite mode, cursor is a block covering the next character
|
|
1219
1117
|
*/
|
|
1220
|
-
_insertCursor(column) {
|
|
1221
|
-
// Calculate cursor position in pixels using character width
|
|
1118
|
+
private _insertCursor(column: number): string {
|
|
1222
1119
|
const charWidth = this._getCharWidth();
|
|
1223
1120
|
const left = column * charWidth;
|
|
1224
|
-
|
|
1121
|
+
if (this._insertMode) {
|
|
1122
|
+
// Insert mode: thin line cursor
|
|
1123
|
+
return `<span class="cursor" style="left: ${left}px"></span>`;
|
|
1124
|
+
} else {
|
|
1125
|
+
// Overwrite mode: block cursor covering the character
|
|
1126
|
+
return `<span class="cursor cursor-block" style="left: ${left}px; width: ${charWidth}px"></span>`;
|
|
1127
|
+
}
|
|
1225
1128
|
}
|
|
1226
1129
|
|
|
1227
1130
|
/**
|
|
1228
1131
|
* Add selection highlight to a line
|
|
1229
1132
|
*/
|
|
1230
|
-
_addSelectionHighlight(html, lineIndex, content) {
|
|
1231
|
-
const
|
|
1232
|
-
if (!
|
|
1133
|
+
private _addSelectionHighlight(html: string, lineIndex: number, content: string): string {
|
|
1134
|
+
const sel = this._normalizeSelection();
|
|
1135
|
+
if (!sel) return html;
|
|
1136
|
+
const { start, end } = sel;
|
|
1233
1137
|
|
|
1234
1138
|
// Check if this line is in the selection
|
|
1235
1139
|
if (lineIndex < start.line || lineIndex > end.line) return html;
|
|
@@ -1266,20 +1170,25 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1266
1170
|
/**
|
|
1267
1171
|
* Get character width for monospace font
|
|
1268
1172
|
*/
|
|
1269
|
-
_getCharWidth() {
|
|
1173
|
+
private _getCharWidth(): number {
|
|
1270
1174
|
if (!this._charWidth) {
|
|
1271
|
-
const canvas =
|
|
1175
|
+
const canvas = _ce('canvas') as HTMLCanvasElement;
|
|
1272
1176
|
const ctx = canvas.getContext('2d');
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1177
|
+
if (ctx) {
|
|
1178
|
+
// Use exact same font as CSS: 'Courier New', Courier, monospace at 13px
|
|
1179
|
+
ctx.font = "13px 'Courier New', Courier, monospace";
|
|
1180
|
+
this._charWidth = ctx.measureText('M').width;
|
|
1181
|
+
} else {
|
|
1182
|
+
// Fallback to approximate monospace character width
|
|
1183
|
+
this._charWidth = 7.8;
|
|
1184
|
+
}
|
|
1276
1185
|
}
|
|
1277
1186
|
return this._charWidth;
|
|
1278
1187
|
}
|
|
1279
1188
|
|
|
1280
|
-
renderGutter(startIndex, endIndex) {
|
|
1281
|
-
const gutterContent = this.
|
|
1282
|
-
const gutterScrollContent = this.
|
|
1189
|
+
renderGutter(startIndex: number, endIndex: number): void {
|
|
1190
|
+
const gutterContent = this._gutterContent;
|
|
1191
|
+
const gutterScrollContent = this._gutterScrollContent;
|
|
1283
1192
|
if (!gutterContent) return;
|
|
1284
1193
|
|
|
1285
1194
|
// Set total height for gutter scroll
|
|
@@ -1298,22 +1207,22 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1298
1207
|
const lineData = this.visibleLines[i];
|
|
1299
1208
|
if (!lineData) continue;
|
|
1300
1209
|
|
|
1301
|
-
const gutterLine =
|
|
1210
|
+
const gutterLine = _ce('div');
|
|
1302
1211
|
gutterLine.className = 'gutter-line';
|
|
1303
1212
|
|
|
1304
1213
|
const meta = lineData.meta;
|
|
1305
1214
|
|
|
1306
1215
|
// Line number first
|
|
1307
|
-
const lineNum =
|
|
1216
|
+
const lineNum = _ce('span');
|
|
1308
1217
|
lineNum.className = 'line-number';
|
|
1309
1218
|
lineNum.textContent = String(lineData.index + 1);
|
|
1310
1219
|
gutterLine.appendChild(lineNum);
|
|
1311
1220
|
|
|
1312
1221
|
// Collapse column (always present for alignment)
|
|
1313
|
-
const collapseCol =
|
|
1222
|
+
const collapseCol = _ce('div');
|
|
1314
1223
|
collapseCol.className = 'collapse-column';
|
|
1315
1224
|
if (meta?.collapseButton) {
|
|
1316
|
-
const btn =
|
|
1225
|
+
const btn = _ce('div');
|
|
1317
1226
|
btn.className = 'collapse-button' + (meta.collapseButton.isCollapsed ? ' collapsed' : '');
|
|
1318
1227
|
btn.textContent = meta.collapseButton.isCollapsed ? '›' : '⌄';
|
|
1319
1228
|
btn.dataset.line = String(lineData.index);
|
|
@@ -1331,28 +1240,31 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1331
1240
|
}
|
|
1332
1241
|
|
|
1333
1242
|
syncGutterScroll() {
|
|
1334
|
-
|
|
1335
|
-
const viewport = this.shadowRoot.getElementById('viewport');
|
|
1336
|
-
if (gutterScroll && viewport) {
|
|
1243
|
+
if (this._gutterScroll && this._viewport) {
|
|
1337
1244
|
// Sync gutter scroll position with viewport
|
|
1338
|
-
|
|
1245
|
+
this._gutterScroll.scrollTop = this._viewport.scrollTop;
|
|
1339
1246
|
}
|
|
1340
1247
|
}
|
|
1341
1248
|
|
|
1342
1249
|
// ========== Input Handling ==========
|
|
1343
1250
|
|
|
1344
1251
|
handleInput(): void {
|
|
1345
|
-
const textarea = this.
|
|
1346
|
-
const inputValue = textarea
|
|
1347
|
-
|
|
1252
|
+
const textarea = this._hiddenTextarea;
|
|
1253
|
+
const inputValue = textarea?.value;
|
|
1254
|
+
|
|
1348
1255
|
if (!inputValue) return;
|
|
1349
|
-
|
|
1256
|
+
|
|
1257
|
+
// Delete selection first if any (replace selection with input)
|
|
1258
|
+
if (this._hasSelection()) {
|
|
1259
|
+
this._deleteSelection();
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1350
1262
|
// Block input in hidden collapsed zones
|
|
1351
1263
|
if (this._getCollapsedRangeForLine(this.cursorLine)) {
|
|
1352
1264
|
textarea.value = '';
|
|
1353
1265
|
return;
|
|
1354
1266
|
}
|
|
1355
|
-
|
|
1267
|
+
|
|
1356
1268
|
// On closing line, only allow after bracket
|
|
1357
1269
|
const onClosingLine = this._getCollapsedClosingLine(this.cursorLine);
|
|
1358
1270
|
if (onClosingLine) {
|
|
@@ -1363,31 +1275,40 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1363
1275
|
return;
|
|
1364
1276
|
}
|
|
1365
1277
|
}
|
|
1366
|
-
|
|
1278
|
+
|
|
1367
1279
|
// On collapsed opening line, only allow before bracket
|
|
1368
1280
|
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
1369
1281
|
if (onCollapsed) {
|
|
1370
1282
|
const line = this.lines[this.cursorLine];
|
|
1371
|
-
const bracketPos = line.search(
|
|
1283
|
+
const bracketPos = line.search(RE_BRACKET_POS);
|
|
1372
1284
|
if (this.cursorColumn > bracketPos) {
|
|
1373
1285
|
textarea.value = '';
|
|
1374
1286
|
return;
|
|
1375
1287
|
}
|
|
1376
1288
|
}
|
|
1377
|
-
|
|
1378
|
-
// Insert the input at cursor position
|
|
1289
|
+
|
|
1290
|
+
// Insert or overwrite the input at cursor position
|
|
1379
1291
|
if (this.cursorLine < this.lines.length) {
|
|
1380
1292
|
const line = this.lines[this.cursorLine];
|
|
1381
1293
|
const before = line.substring(0, this.cursorColumn);
|
|
1382
|
-
|
|
1383
|
-
|
|
1294
|
+
|
|
1384
1295
|
// Handle newlines in input
|
|
1385
1296
|
const inputLines = inputValue.split('\n');
|
|
1386
1297
|
if (inputLines.length === 1) {
|
|
1387
|
-
|
|
1298
|
+
// Single line input: insert or overwrite mode
|
|
1299
|
+
if (this._insertMode) {
|
|
1300
|
+
// Insert mode: keep text after cursor
|
|
1301
|
+
const after = line.substring(this.cursorColumn);
|
|
1302
|
+
this.lines[this.cursorLine] = before + inputValue + after;
|
|
1303
|
+
} else {
|
|
1304
|
+
// Overwrite mode: replace characters after cursor
|
|
1305
|
+
const after = line.substring(this.cursorColumn + inputValue.length);
|
|
1306
|
+
this.lines[this.cursorLine] = before + inputValue + after;
|
|
1307
|
+
}
|
|
1388
1308
|
this.cursorColumn += inputValue.length;
|
|
1389
1309
|
} else {
|
|
1390
|
-
// Multi-line input
|
|
1310
|
+
// Multi-line input: always insert mode
|
|
1311
|
+
const after = line.substring(this.cursorColumn);
|
|
1391
1312
|
this.lines[this.cursorLine] = before + inputLines[0];
|
|
1392
1313
|
for (let i = 1; i < inputLines.length - 1; i++) {
|
|
1393
1314
|
this.lines.splice(this.cursorLine + i, 0, inputLines[i]);
|
|
@@ -1415,17 +1336,17 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1415
1336
|
}, 150);
|
|
1416
1337
|
}
|
|
1417
1338
|
|
|
1418
|
-
handleKeydown(e) {
|
|
1339
|
+
handleKeydown(e: KeyboardEvent): void {
|
|
1419
1340
|
// Build context for collapsed zone detection
|
|
1420
|
-
const ctx = {
|
|
1341
|
+
const ctx: CollapsedZoneContext = {
|
|
1421
1342
|
inCollapsedZone: this._getCollapsedRangeForLine(this.cursorLine),
|
|
1422
1343
|
onCollapsedNode: this._getCollapsedNodeAtLine(this.cursorLine),
|
|
1423
1344
|
onClosingLine: this._getCollapsedClosingLine(this.cursorLine)
|
|
1424
1345
|
};
|
|
1425
1346
|
|
|
1426
1347
|
// Lookup table for key handlers
|
|
1427
|
-
const keyHandlers = {
|
|
1428
|
-
'Enter': () => this._handleEnter(ctx),
|
|
1348
|
+
const keyHandlers: Record<string, () => void> = {
|
|
1349
|
+
'Enter': () => this._handleEnter(e.shiftKey, ctx),
|
|
1429
1350
|
'Backspace': () => this._handleBackspace(ctx),
|
|
1430
1351
|
'Delete': () => this._handleDelete(ctx),
|
|
1431
1352
|
'ArrowUp': () => this._handleArrowKey(-1, 0, e.shiftKey, e.ctrlKey || e.metaKey),
|
|
@@ -1434,11 +1355,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1434
1355
|
'ArrowRight': () => this._handleArrowKey(0, 1, e.shiftKey, e.ctrlKey || e.metaKey),
|
|
1435
1356
|
'Home': () => this._handleHomeEnd('home', e.shiftKey, ctx.onClosingLine),
|
|
1436
1357
|
'End': () => this._handleHomeEnd('end', e.shiftKey, ctx.onClosingLine),
|
|
1437
|
-
'Tab': () => this._handleTab(e.shiftKey, ctx)
|
|
1358
|
+
'Tab': () => this._handleTab(e.shiftKey, ctx),
|
|
1359
|
+
'Insert': () => { this._insertMode = !this._insertMode; this.scheduleRender(); }
|
|
1438
1360
|
};
|
|
1439
1361
|
|
|
1440
1362
|
// Modifier key handlers (Ctrl/Cmd)
|
|
1441
|
-
const modifierHandlers = {
|
|
1363
|
+
const modifierHandlers: Record<string, () => void | boolean | Promise<boolean>> = {
|
|
1442
1364
|
'a': () => this._selectAll(),
|
|
1443
1365
|
'z': () => e.shiftKey ? this.redo() : this.undo(),
|
|
1444
1366
|
'y': () => this.redo(),
|
|
@@ -1460,21 +1382,47 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1460
1382
|
}
|
|
1461
1383
|
}
|
|
1462
1384
|
|
|
1463
|
-
_handleEnter(ctx) {
|
|
1464
|
-
//
|
|
1465
|
-
if (
|
|
1466
|
-
|
|
1385
|
+
private _handleEnter(isShiftKey: boolean, ctx: CollapsedZoneContext): void {
|
|
1386
|
+
// Shift+Enter: collapse the containing expanded node
|
|
1387
|
+
if (isShiftKey) {
|
|
1388
|
+
const containingNode = this._getContainingExpandedNode(this.cursorLine);
|
|
1389
|
+
if (containingNode) {
|
|
1390
|
+
const startLine = this.lines[containingNode.startLine];
|
|
1391
|
+
const bracketPos = startLine.search(RE_BRACKET_POS);
|
|
1392
|
+
this.toggleCollapse(containingNode.nodeId);
|
|
1393
|
+
this.cursorLine = containingNode.startLine;
|
|
1394
|
+
this.cursorColumn = bracketPos >= 0 ? bracketPos + 1 : startLine.length;
|
|
1395
|
+
this._clearSelection();
|
|
1396
|
+
this._scrollToCursor();
|
|
1397
|
+
}
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// Enter on collapsed node: expand it
|
|
1402
|
+
if (ctx.onCollapsedNode) {
|
|
1403
|
+
this.toggleCollapse(ctx.onCollapsedNode.nodeId);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Enter on closing line of collapsed node: expand it
|
|
1467
1408
|
if (ctx.onClosingLine) {
|
|
1468
1409
|
const line = this.lines[this.cursorLine];
|
|
1469
1410
|
const bracketPos = this._getClosingBracketPos(line);
|
|
1411
|
+
// If cursor is before or on bracket, expand
|
|
1470
1412
|
if (bracketPos >= 0 && this.cursorColumn <= bracketPos) {
|
|
1413
|
+
this.toggleCollapse(ctx.onClosingLine.nodeId);
|
|
1471
1414
|
return;
|
|
1472
1415
|
}
|
|
1473
1416
|
}
|
|
1417
|
+
|
|
1418
|
+
// Block in collapsed zones
|
|
1419
|
+
if (ctx.inCollapsedZone) return;
|
|
1420
|
+
|
|
1421
|
+
// Normal Enter: insert newline
|
|
1474
1422
|
this.insertNewline();
|
|
1475
1423
|
}
|
|
1476
1424
|
|
|
1477
|
-
_handleBackspace(ctx) {
|
|
1425
|
+
private _handleBackspace(ctx: CollapsedZoneContext): void {
|
|
1478
1426
|
// Delete selection if any
|
|
1479
1427
|
if (this._hasSelection()) {
|
|
1480
1428
|
this._deleteSelection();
|
|
@@ -1502,7 +1450,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1502
1450
|
// On opening line, allow editing before bracket
|
|
1503
1451
|
if (ctx.onCollapsedNode) {
|
|
1504
1452
|
const line = this.lines[this.cursorLine];
|
|
1505
|
-
const bracketPos = line.search(
|
|
1453
|
+
const bracketPos = line.search(RE_BRACKET_POS);
|
|
1506
1454
|
if (this.cursorColumn > bracketPos + 1) {
|
|
1507
1455
|
this._deleteCollapsedNode(ctx.onCollapsedNode);
|
|
1508
1456
|
return;
|
|
@@ -1511,7 +1459,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1511
1459
|
this.deleteBackward();
|
|
1512
1460
|
}
|
|
1513
1461
|
|
|
1514
|
-
_handleDelete(ctx) {
|
|
1462
|
+
private _handleDelete(ctx: CollapsedZoneContext): void {
|
|
1515
1463
|
// Delete selection if any
|
|
1516
1464
|
if (this._hasSelection()) {
|
|
1517
1465
|
this._deleteSelection();
|
|
@@ -1532,7 +1480,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1532
1480
|
// If on collapsed node opening line
|
|
1533
1481
|
if (ctx.onCollapsedNode) {
|
|
1534
1482
|
const line = this.lines[this.cursorLine];
|
|
1535
|
-
const bracketPos = line.search(
|
|
1483
|
+
const bracketPos = line.search(RE_BRACKET_POS);
|
|
1536
1484
|
if (this.cursorColumn > bracketPos) {
|
|
1537
1485
|
this._deleteCollapsedNode(ctx.onCollapsedNode);
|
|
1538
1486
|
return;
|
|
@@ -1543,29 +1491,208 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1543
1491
|
this.deleteForward();
|
|
1544
1492
|
}
|
|
1545
1493
|
|
|
1546
|
-
_handleTab(isShiftKey,
|
|
1547
|
-
// Shift+Tab:
|
|
1494
|
+
private _handleTab(isShiftKey: boolean, _ctx: CollapsedZoneContext): void {
|
|
1495
|
+
// Tab/Shift+Tab: navigate between attributes (key and value)
|
|
1548
1496
|
if (isShiftKey) {
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1497
|
+
this._navigateToPrevAttribute();
|
|
1498
|
+
} else {
|
|
1499
|
+
this._navigateToNextAttribute();
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/**
|
|
1504
|
+
* Navigate to the next attribute (key or value) in the JSON
|
|
1505
|
+
*/
|
|
1506
|
+
private _navigateToNextAttribute(): void {
|
|
1507
|
+
const totalLines = this.visibleLines.length;
|
|
1508
|
+
let currentVisibleIdx = this.visibleLines.findIndex(vl => vl.index === this.cursorLine);
|
|
1509
|
+
if (currentVisibleIdx < 0) currentVisibleIdx = 0;
|
|
1510
|
+
|
|
1511
|
+
// Search from current position forward
|
|
1512
|
+
for (let i = currentVisibleIdx; i < totalLines; i++) {
|
|
1513
|
+
const vl = this.visibleLines[i];
|
|
1514
|
+
const line = this.lines[vl.index];
|
|
1515
|
+
const startCol = (i === currentVisibleIdx) ? this.cursorColumn : 0;
|
|
1516
|
+
|
|
1517
|
+
const pos = this._findNextAttributeInLine(line, startCol);
|
|
1518
|
+
if (pos !== null) {
|
|
1519
|
+
this.cursorLine = vl.index;
|
|
1520
|
+
this.cursorColumn = pos.start;
|
|
1521
|
+
// Select the attribute key or value
|
|
1522
|
+
this.selectionStart = { line: vl.index, column: pos.start };
|
|
1523
|
+
this.selectionEnd = { line: vl.index, column: pos.end };
|
|
1557
1524
|
this._scrollToCursor();
|
|
1525
|
+
this._invalidateRenderCache();
|
|
1526
|
+
this.scheduleRender();
|
|
1527
|
+
return;
|
|
1558
1528
|
}
|
|
1559
|
-
return;
|
|
1560
1529
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1530
|
+
|
|
1531
|
+
// Wrap to beginning
|
|
1532
|
+
for (let i = 0; i < currentVisibleIdx; i++) {
|
|
1533
|
+
const vl = this.visibleLines[i];
|
|
1534
|
+
const line = this.lines[vl.index];
|
|
1535
|
+
const pos = this._findNextAttributeInLine(line, 0);
|
|
1536
|
+
if (pos !== null) {
|
|
1537
|
+
this.cursorLine = vl.index;
|
|
1538
|
+
this.cursorColumn = pos.start;
|
|
1539
|
+
this.selectionStart = { line: vl.index, column: pos.start };
|
|
1540
|
+
this.selectionEnd = { line: vl.index, column: pos.end };
|
|
1541
|
+
this._scrollToCursor();
|
|
1542
|
+
this._invalidateRenderCache();
|
|
1543
|
+
this.scheduleRender();
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1565
1546
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Navigate to the previous attribute (key or value) in the JSON
|
|
1551
|
+
*/
|
|
1552
|
+
private _navigateToPrevAttribute(): void {
|
|
1553
|
+
const totalLines = this.visibleLines.length;
|
|
1554
|
+
let currentVisibleIdx = this.visibleLines.findIndex(vl => vl.index === this.cursorLine);
|
|
1555
|
+
if (currentVisibleIdx < 0) currentVisibleIdx = totalLines - 1;
|
|
1556
|
+
|
|
1557
|
+
// Search from current position backward
|
|
1558
|
+
for (let i = currentVisibleIdx; i >= 0; i--) {
|
|
1559
|
+
const vl = this.visibleLines[i];
|
|
1560
|
+
const line = this.lines[vl.index];
|
|
1561
|
+
const endCol = (i === currentVisibleIdx) ? this.cursorColumn : line.length;
|
|
1562
|
+
|
|
1563
|
+
const pos = this._findPrevAttributeInLine(line, endCol);
|
|
1564
|
+
if (pos !== null) {
|
|
1565
|
+
this.cursorLine = vl.index;
|
|
1566
|
+
this.cursorColumn = pos.start;
|
|
1567
|
+
this.selectionStart = { line: vl.index, column: pos.start };
|
|
1568
|
+
this.selectionEnd = { line: vl.index, column: pos.end };
|
|
1569
|
+
this._scrollToCursor();
|
|
1570
|
+
this._invalidateRenderCache();
|
|
1571
|
+
this.scheduleRender();
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// Wrap to end
|
|
1577
|
+
for (let i = totalLines - 1; i > currentVisibleIdx; i--) {
|
|
1578
|
+
const vl = this.visibleLines[i];
|
|
1579
|
+
const line = this.lines[vl.index];
|
|
1580
|
+
const pos = this._findPrevAttributeInLine(line, line.length);
|
|
1581
|
+
if (pos !== null) {
|
|
1582
|
+
this.cursorLine = vl.index;
|
|
1583
|
+
this.cursorColumn = pos.start;
|
|
1584
|
+
this.selectionStart = { line: vl.index, column: pos.start };
|
|
1585
|
+
this.selectionEnd = { line: vl.index, column: pos.end };
|
|
1586
|
+
this._scrollToCursor();
|
|
1587
|
+
this._invalidateRenderCache();
|
|
1588
|
+
this.scheduleRender();
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* Find next attribute position in a line after startCol
|
|
1596
|
+
* Returns {start, end} for the key or value, or null if none found
|
|
1597
|
+
*/
|
|
1598
|
+
private _findNextAttributeInLine(line: string, startCol: number): { start: number; end: number } | null {
|
|
1599
|
+
// Pattern: "key": value where value can be "string", number, true, false, null
|
|
1600
|
+
const re = /"([^"]+)"(?:\s*:\s*(?:"([^"]*)"|(-?\d+\.?\d*(?:e[+-]?\d+)?)|true|false|null))?/gi;
|
|
1601
|
+
let match;
|
|
1602
|
+
|
|
1603
|
+
while ((match = re.exec(line)) !== null) {
|
|
1604
|
+
const keyStart = match.index + 1; // Skip opening quote
|
|
1605
|
+
const keyEnd = keyStart + match[1].length;
|
|
1606
|
+
|
|
1607
|
+
// If key is after startCol, return key position
|
|
1608
|
+
if (keyStart > startCol) {
|
|
1609
|
+
return { start: keyStart, end: keyEnd };
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// Check if there's a value (string, number, boolean, null)
|
|
1613
|
+
if (match[2] !== undefined) {
|
|
1614
|
+
// String value - find its position
|
|
1615
|
+
const valueMatch = line.substring(match.index).match(/:\s*"([^"]*)"/);
|
|
1616
|
+
if (valueMatch) {
|
|
1617
|
+
const valueStart = match.index + (valueMatch.index || 0) + valueMatch[0].indexOf('"') + 1;
|
|
1618
|
+
const valueEnd = valueStart + match[2].length;
|
|
1619
|
+
if (valueStart > startCol) {
|
|
1620
|
+
return { start: valueStart, end: valueEnd };
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
} else if (match[3] !== undefined) {
|
|
1624
|
+
// Number value
|
|
1625
|
+
const numMatch = line.substring(match.index).match(/:\s*(-?\d+\.?\d*(?:e[+-]?\d+)?)/i);
|
|
1626
|
+
if (numMatch) {
|
|
1627
|
+
const valueStart = match.index + (numMatch.index || 0) + numMatch[0].indexOf(numMatch[1]);
|
|
1628
|
+
const valueEnd = valueStart + numMatch[1].length;
|
|
1629
|
+
if (valueStart > startCol) {
|
|
1630
|
+
return { start: valueStart, end: valueEnd };
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
} else {
|
|
1634
|
+
// Boolean or null - check after the colon
|
|
1635
|
+
const boolMatch = line.substring(match.index).match(/:\s*(true|false|null)/);
|
|
1636
|
+
if (boolMatch) {
|
|
1637
|
+
const valueStart = match.index + (boolMatch.index || 0) + boolMatch[0].indexOf(boolMatch[1]);
|
|
1638
|
+
const valueEnd = valueStart + boolMatch[1].length;
|
|
1639
|
+
if (valueStart > startCol) {
|
|
1640
|
+
return { start: valueStart, end: valueEnd };
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1568
1644
|
}
|
|
1645
|
+
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
/**
|
|
1650
|
+
* Find previous attribute position in a line before endCol
|
|
1651
|
+
*/
|
|
1652
|
+
private _findPrevAttributeInLine(line: string, endCol: number): { start: number; end: number } | null {
|
|
1653
|
+
// Collect all attributes in the line
|
|
1654
|
+
const attrs: { start: number; end: number }[] = [];
|
|
1655
|
+
const re = /"([^"]+)"(?:\s*:\s*(?:"([^"]*)"|(-?\d+\.?\d*(?:e[+-]?\d+)?)|true|false|null))?/gi;
|
|
1656
|
+
let match;
|
|
1657
|
+
|
|
1658
|
+
while ((match = re.exec(line)) !== null) {
|
|
1659
|
+
const keyStart = match.index + 1;
|
|
1660
|
+
const keyEnd = keyStart + match[1].length;
|
|
1661
|
+
attrs.push({ start: keyStart, end: keyEnd });
|
|
1662
|
+
|
|
1663
|
+
// Check for value
|
|
1664
|
+
if (match[2] !== undefined) {
|
|
1665
|
+
const valueMatch = line.substring(match.index).match(/:\s*"([^"]*)"/);
|
|
1666
|
+
if (valueMatch) {
|
|
1667
|
+
const valueStart = match.index + (valueMatch.index || 0) + valueMatch[0].indexOf('"') + 1;
|
|
1668
|
+
const valueEnd = valueStart + match[2].length;
|
|
1669
|
+
attrs.push({ start: valueStart, end: valueEnd });
|
|
1670
|
+
}
|
|
1671
|
+
} else if (match[3] !== undefined) {
|
|
1672
|
+
const numMatch = line.substring(match.index).match(/:\s*(-?\d+\.?\d*(?:e[+-]?\d+)?)/i);
|
|
1673
|
+
if (numMatch) {
|
|
1674
|
+
const valueStart = match.index + (numMatch.index || 0) + numMatch[0].indexOf(numMatch[1]);
|
|
1675
|
+
const valueEnd = valueStart + numMatch[1].length;
|
|
1676
|
+
attrs.push({ start: valueStart, end: valueEnd });
|
|
1677
|
+
}
|
|
1678
|
+
} else {
|
|
1679
|
+
const boolMatch = line.substring(match.index).match(/:\s*(true|false|null)/);
|
|
1680
|
+
if (boolMatch) {
|
|
1681
|
+
const valueStart = match.index + (boolMatch.index || 0) + boolMatch[0].indexOf(boolMatch[1]);
|
|
1682
|
+
const valueEnd = valueStart + boolMatch[1].length;
|
|
1683
|
+
attrs.push({ start: valueStart, end: valueEnd });
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// Find the last attribute that ends before endCol
|
|
1689
|
+
for (let i = attrs.length - 1; i >= 0; i--) {
|
|
1690
|
+
if (attrs[i].end < endCol) {
|
|
1691
|
+
return attrs[i];
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
return null;
|
|
1569
1696
|
}
|
|
1570
1697
|
|
|
1571
1698
|
insertNewline() {
|
|
@@ -1629,7 +1756,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1629
1756
|
/**
|
|
1630
1757
|
* Move cursor vertically, skipping hidden collapsed lines only
|
|
1631
1758
|
*/
|
|
1632
|
-
moveCursorSkipCollapsed(deltaLine) {
|
|
1759
|
+
moveCursorSkipCollapsed(deltaLine: number): void {
|
|
1633
1760
|
let targetLine = this.cursorLine + deltaLine;
|
|
1634
1761
|
|
|
1635
1762
|
// Skip over hidden collapsed zones only (not opening/closing lines)
|
|
@@ -1642,8 +1769,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1642
1769
|
} else {
|
|
1643
1770
|
targetLine = collapsed.startLine; // Jump to opening line
|
|
1644
1771
|
}
|
|
1772
|
+
} else {
|
|
1773
|
+
break; // Not in a collapsed zone, stop
|
|
1645
1774
|
}
|
|
1646
|
-
break;
|
|
1647
1775
|
}
|
|
1648
1776
|
|
|
1649
1777
|
this.cursorLine = Math.max(0, Math.min(this.lines.length - 1, targetLine));
|
|
@@ -1660,7 +1788,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1660
1788
|
/**
|
|
1661
1789
|
* Move cursor horizontally with smart navigation around collapsed nodes
|
|
1662
1790
|
*/
|
|
1663
|
-
moveCursorHorizontal(delta) {
|
|
1791
|
+
moveCursorHorizontal(delta: number): void {
|
|
1664
1792
|
if (delta > 0) {
|
|
1665
1793
|
this._moveCursorRight();
|
|
1666
1794
|
} else {
|
|
@@ -1671,7 +1799,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1671
1799
|
this.scheduleRender();
|
|
1672
1800
|
}
|
|
1673
1801
|
|
|
1674
|
-
_moveCursorRight() {
|
|
1802
|
+
private _moveCursorRight() {
|
|
1675
1803
|
const line = this.lines[this.cursorLine];
|
|
1676
1804
|
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
1677
1805
|
const onClosingLine = this._getCollapsedClosingLine(this.cursorLine);
|
|
@@ -1689,7 +1817,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1689
1817
|
this.cursorColumn++;
|
|
1690
1818
|
}
|
|
1691
1819
|
} else if (onCollapsed) {
|
|
1692
|
-
const bracketPos = line.search(
|
|
1820
|
+
const bracketPos = line.search(RE_BRACKET_POS);
|
|
1693
1821
|
if (this.cursorColumn < bracketPos) {
|
|
1694
1822
|
this.cursorColumn++;
|
|
1695
1823
|
} else if (this.cursorColumn === bracketPos) {
|
|
@@ -1713,7 +1841,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1713
1841
|
}
|
|
1714
1842
|
}
|
|
1715
1843
|
|
|
1716
|
-
_moveCursorLeft() {
|
|
1844
|
+
private _moveCursorLeft() {
|
|
1717
1845
|
const line = this.lines[this.cursorLine];
|
|
1718
1846
|
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
1719
1847
|
const onClosingLine = this._getCollapsedClosingLine(this.cursorLine);
|
|
@@ -1726,10 +1854,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1726
1854
|
// Jump to opening line after bracket
|
|
1727
1855
|
this.cursorLine = onClosingLine.startLine;
|
|
1728
1856
|
const openLine = this.lines[this.cursorLine];
|
|
1729
|
-
this.cursorColumn = openLine.search(
|
|
1857
|
+
this.cursorColumn = openLine.search(RE_BRACKET_POS) + 1;
|
|
1730
1858
|
}
|
|
1731
1859
|
} else if (onCollapsed) {
|
|
1732
|
-
const bracketPos = line.search(
|
|
1860
|
+
const bracketPos = line.search(RE_BRACKET_POS);
|
|
1733
1861
|
if (this.cursorColumn > bracketPos + 1) {
|
|
1734
1862
|
this.cursorColumn = bracketPos + 1;
|
|
1735
1863
|
} else if (this.cursorColumn === bracketPos + 1) {
|
|
@@ -1752,7 +1880,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1752
1880
|
if (collapsed) {
|
|
1753
1881
|
this.cursorLine = collapsed.startLine;
|
|
1754
1882
|
const openLine = this.lines[this.cursorLine];
|
|
1755
|
-
this.cursorColumn = openLine.search(
|
|
1883
|
+
this.cursorColumn = openLine.search(RE_BRACKET_POS) + 1;
|
|
1756
1884
|
} else {
|
|
1757
1885
|
this.cursorColumn = this.lines[this.cursorLine]?.length || 0;
|
|
1758
1886
|
}
|
|
@@ -1763,8 +1891,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1763
1891
|
/**
|
|
1764
1892
|
* Scroll viewport to ensure cursor is visible
|
|
1765
1893
|
*/
|
|
1766
|
-
_scrollToCursor() {
|
|
1767
|
-
const viewport = this.
|
|
1894
|
+
private _scrollToCursor() {
|
|
1895
|
+
const viewport = this._viewport;
|
|
1768
1896
|
if (!viewport) return;
|
|
1769
1897
|
|
|
1770
1898
|
// Find the visible line index for the cursor
|
|
@@ -1788,7 +1916,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1788
1916
|
/**
|
|
1789
1917
|
* Handle arrow key with optional selection and word jump
|
|
1790
1918
|
*/
|
|
1791
|
-
_handleArrowKey(deltaLine, deltaCol, isShift, isCtrl = false) {
|
|
1919
|
+
private _handleArrowKey(deltaLine: number, deltaCol: number, isShift: boolean, isCtrl = false): void {
|
|
1792
1920
|
// Start selection if shift is pressed and no selection exists
|
|
1793
1921
|
if (isShift && !this.selectionStart) {
|
|
1794
1922
|
this.selectionStart = { line: this.cursorLine, column: this.cursorColumn };
|
|
@@ -1822,10 +1950,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1822
1950
|
* - Ctrl+Right: move to end of current word, or start of next word
|
|
1823
1951
|
* - Ctrl+Left: move to start of current word, or start of previous word
|
|
1824
1952
|
*/
|
|
1825
|
-
_moveCursorByWord(direction) {
|
|
1953
|
+
private _moveCursorByWord(direction: number): void {
|
|
1826
1954
|
const line = this.lines[this.cursorLine] || '';
|
|
1827
1955
|
// Word character: alphanumeric, underscore, or hyphen (for kebab-case identifiers)
|
|
1828
|
-
const isWordChar = (ch) =>
|
|
1956
|
+
const isWordChar = (ch: string) => RE_IS_WORD_CHAR.test(ch);
|
|
1829
1957
|
|
|
1830
1958
|
// Check if we're on a collapsed node's opening line
|
|
1831
1959
|
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
@@ -1836,7 +1964,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1836
1964
|
|
|
1837
1965
|
// If on collapsed node opening line and cursor is at/after the bracket, jump to closing line
|
|
1838
1966
|
if (onCollapsed) {
|
|
1839
|
-
const bracketPos = line.search(
|
|
1967
|
+
const bracketPos = line.search(RE_BRACKET_POS);
|
|
1840
1968
|
if (bracketPos >= 0 && pos >= bracketPos) {
|
|
1841
1969
|
this.cursorLine = onCollapsed.endLine;
|
|
1842
1970
|
this.cursorColumn = (this.lines[this.cursorLine] || '').length;
|
|
@@ -1884,7 +2012,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1884
2012
|
// Jump to opening line, after the bracket
|
|
1885
2013
|
this.cursorLine = onClosingLine.startLine;
|
|
1886
2014
|
const openLine = this.lines[this.cursorLine] || '';
|
|
1887
|
-
const openBracketPos = openLine.search(
|
|
2015
|
+
const openBracketPos = openLine.search(RE_BRACKET_POS);
|
|
1888
2016
|
this.cursorColumn = openBracketPos >= 0 ? openBracketPos : 0;
|
|
1889
2017
|
this._invalidateRenderCache();
|
|
1890
2018
|
this._scrollToCursor();
|
|
@@ -1931,7 +2059,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1931
2059
|
/**
|
|
1932
2060
|
* Handle Home/End with optional selection
|
|
1933
2061
|
*/
|
|
1934
|
-
_handleHomeEnd(key, isShift, onClosingLine) {
|
|
2062
|
+
private _handleHomeEnd(key: string, isShift: boolean, onClosingLine: CollapsedNodeInfo | null): void {
|
|
1935
2063
|
// Start selection if shift is pressed and no selection exists
|
|
1936
2064
|
if (isShift && !this.selectionStart) {
|
|
1937
2065
|
this.selectionStart = { line: this.cursorLine, column: this.cursorColumn };
|
|
@@ -1964,7 +2092,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1964
2092
|
/**
|
|
1965
2093
|
* Select all content
|
|
1966
2094
|
*/
|
|
1967
|
-
_selectAll() {
|
|
2095
|
+
private _selectAll() {
|
|
1968
2096
|
this.selectionStart = { line: 0, column: 0 };
|
|
1969
2097
|
const lastLine = this.lines.length - 1;
|
|
1970
2098
|
this.selectionEnd = { line: lastLine, column: this.lines[lastLine]?.length || 0 };
|
|
@@ -1979,11 +2107,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1979
2107
|
/**
|
|
1980
2108
|
* Get selected text
|
|
1981
2109
|
*/
|
|
1982
|
-
_getSelectedText() {
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
const { start, end } =
|
|
1986
|
-
if (!start || !end) return '';
|
|
2110
|
+
private _getSelectedText(): string {
|
|
2111
|
+
const sel = this._normalizeSelection();
|
|
2112
|
+
if (!sel) return '';
|
|
2113
|
+
const { start, end } = sel;
|
|
1987
2114
|
|
|
1988
2115
|
if (start.line === end.line) {
|
|
1989
2116
|
return this.lines[start.line].substring(start.column, end.column);
|
|
@@ -2001,14 +2128,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2001
2128
|
/**
|
|
2002
2129
|
* Normalize selection so start is before end
|
|
2003
2130
|
*/
|
|
2004
|
-
_normalizeSelection() {
|
|
2131
|
+
private _normalizeSelection(): { start: CursorPosition; end: CursorPosition } | null {
|
|
2005
2132
|
if (!this.selectionStart || !this.selectionEnd) {
|
|
2006
|
-
return
|
|
2133
|
+
return null;
|
|
2007
2134
|
}
|
|
2008
|
-
|
|
2135
|
+
|
|
2009
2136
|
const s = this.selectionStart;
|
|
2010
2137
|
const e = this.selectionEnd;
|
|
2011
|
-
|
|
2138
|
+
|
|
2012
2139
|
if (s.line < e.line || (s.line === e.line && s.column <= e.column)) {
|
|
2013
2140
|
return { start: s, end: e };
|
|
2014
2141
|
} else {
|
|
@@ -2019,7 +2146,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2019
2146
|
/**
|
|
2020
2147
|
* Check if there is an active selection
|
|
2021
2148
|
*/
|
|
2022
|
-
_hasSelection() {
|
|
2149
|
+
private _hasSelection() {
|
|
2023
2150
|
if (!this.selectionStart || !this.selectionEnd) return false;
|
|
2024
2151
|
return this.selectionStart.line !== this.selectionEnd.line ||
|
|
2025
2152
|
this.selectionStart.column !== this.selectionEnd.column;
|
|
@@ -2028,7 +2155,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2028
2155
|
/**
|
|
2029
2156
|
* Clear the current selection
|
|
2030
2157
|
*/
|
|
2031
|
-
_clearSelection() {
|
|
2158
|
+
private _clearSelection() {
|
|
2032
2159
|
this.selectionStart = null;
|
|
2033
2160
|
this.selectionEnd = null;
|
|
2034
2161
|
}
|
|
@@ -2036,13 +2163,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2036
2163
|
/**
|
|
2037
2164
|
* Delete selected text
|
|
2038
2165
|
*/
|
|
2039
|
-
_deleteSelection() {
|
|
2040
|
-
|
|
2166
|
+
private _deleteSelection(): boolean {
|
|
2167
|
+
const sel = this._normalizeSelection();
|
|
2168
|
+
if (!sel) return false;
|
|
2169
|
+
const { start, end } = sel;
|
|
2041
2170
|
|
|
2042
2171
|
this._saveToHistory('delete');
|
|
2043
2172
|
|
|
2044
|
-
const { start, end } = this._normalizeSelection();
|
|
2045
|
-
|
|
2046
2173
|
if (start.line === end.line) {
|
|
2047
2174
|
// Single line selection
|
|
2048
2175
|
const line = this.lines[start.line];
|
|
@@ -2063,7 +2190,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2063
2190
|
return true;
|
|
2064
2191
|
}
|
|
2065
2192
|
|
|
2066
|
-
insertText(text) {
|
|
2193
|
+
insertText(text: string): void {
|
|
2067
2194
|
// Delete selection first if any
|
|
2068
2195
|
if (this._hasSelection()) {
|
|
2069
2196
|
this._deleteSelection();
|
|
@@ -2084,7 +2211,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2084
2211
|
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
2085
2212
|
if (onCollapsed) {
|
|
2086
2213
|
const line = this.lines[this.cursorLine];
|
|
2087
|
-
const bracketPos = line.search(
|
|
2214
|
+
const bracketPos = line.search(RE_BRACKET_POS);
|
|
2088
2215
|
if (this.cursorColumn > bracketPos) return;
|
|
2089
2216
|
}
|
|
2090
2217
|
|
|
@@ -2106,9 +2233,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2106
2233
|
this.formatAndUpdate();
|
|
2107
2234
|
}
|
|
2108
2235
|
|
|
2109
|
-
handlePaste(e) {
|
|
2236
|
+
handlePaste(e: ClipboardEvent): void {
|
|
2110
2237
|
e.preventDefault();
|
|
2111
|
-
const text = e.clipboardData
|
|
2238
|
+
const text = e.clipboardData?.getData('text/plain');
|
|
2112
2239
|
if (!text) return;
|
|
2113
2240
|
|
|
2114
2241
|
const wasEmpty = this.lines.length === 0;
|
|
@@ -2116,7 +2243,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2116
2243
|
// Try to parse as GeoJSON and normalize
|
|
2117
2244
|
try {
|
|
2118
2245
|
const parsed = JSON.parse(text);
|
|
2119
|
-
const features =
|
|
2246
|
+
const features = normalizeToFeatures(parsed);
|
|
2120
2247
|
// Valid GeoJSON - insert formatted features
|
|
2121
2248
|
const formatted = features.map(f => JSON.stringify(f, null, 2)).join(',\n');
|
|
2122
2249
|
this.insertText(formatted);
|
|
@@ -2130,14 +2257,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2130
2257
|
// Cancel pending render, collapse first, then render once
|
|
2131
2258
|
if (this.renderTimer) {
|
|
2132
2259
|
cancelAnimationFrame(this.renderTimer);
|
|
2133
|
-
this.renderTimer =
|
|
2260
|
+
this.renderTimer = undefined;
|
|
2134
2261
|
}
|
|
2135
2262
|
this.autoCollapseCoordinates();
|
|
2136
2263
|
}
|
|
2137
2264
|
}
|
|
2138
2265
|
|
|
2139
|
-
handleCopy(e) {
|
|
2266
|
+
handleCopy(e: ClipboardEvent): void {
|
|
2140
2267
|
e.preventDefault();
|
|
2268
|
+
if (!e.clipboardData) return;
|
|
2141
2269
|
// Copy selected text if there's a selection, otherwise copy all
|
|
2142
2270
|
if (this._hasSelection()) {
|
|
2143
2271
|
e.clipboardData.setData('text/plain', this._getSelectedText());
|
|
@@ -2146,8 +2274,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2146
2274
|
}
|
|
2147
2275
|
}
|
|
2148
2276
|
|
|
2149
|
-
handleCut(e) {
|
|
2277
|
+
handleCut(e: ClipboardEvent): void {
|
|
2150
2278
|
e.preventDefault();
|
|
2279
|
+
if (!e.clipboardData) return;
|
|
2151
2280
|
if (this._hasSelection()) {
|
|
2152
2281
|
e.clipboardData.setData('text/plain', this._getSelectedText());
|
|
2153
2282
|
this._saveToHistory('cut');
|
|
@@ -2167,9 +2296,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2167
2296
|
/**
|
|
2168
2297
|
* Get line/column position from mouse event
|
|
2169
2298
|
*/
|
|
2170
|
-
_getPositionFromClick(e) {
|
|
2171
|
-
const viewport = this.
|
|
2172
|
-
const linesContainer = this.
|
|
2299
|
+
private _getPositionFromClick(e: MouseEvent): { line: number; column: number } {
|
|
2300
|
+
const viewport = this._viewport;
|
|
2301
|
+
const linesContainer = this._linesContainer;
|
|
2302
|
+
if (!viewport) return { line: 0, column: 0 };
|
|
2173
2303
|
const rect = viewport.getBoundingClientRect();
|
|
2174
2304
|
|
|
2175
2305
|
const paddingTop = 8;
|
|
@@ -2210,28 +2340,34 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2210
2340
|
|
|
2211
2341
|
// ========== Gutter Interactions ==========
|
|
2212
2342
|
|
|
2213
|
-
handleGutterClick(e) {
|
|
2343
|
+
handleGutterClick(e: MouseEvent): void {
|
|
2344
|
+
const target = e.target as HTMLElement;
|
|
2345
|
+
if (!target) return;
|
|
2346
|
+
|
|
2214
2347
|
// Visibility button in gutter
|
|
2215
|
-
const visBtn =
|
|
2348
|
+
const visBtn = target.closest('.visibility-button') as HTMLElement | null;
|
|
2216
2349
|
if (visBtn) {
|
|
2217
2350
|
this.toggleFeatureVisibility(visBtn.dataset.featureKey);
|
|
2218
2351
|
return;
|
|
2219
2352
|
}
|
|
2220
|
-
|
|
2353
|
+
|
|
2221
2354
|
// Collapse button in gutter
|
|
2222
|
-
if (
|
|
2223
|
-
const nodeId =
|
|
2224
|
-
this.toggleCollapse(nodeId);
|
|
2355
|
+
if (target.classList.contains('collapse-button')) {
|
|
2356
|
+
const nodeId = target.dataset.nodeId;
|
|
2357
|
+
if (nodeId) this.toggleCollapse(nodeId);
|
|
2225
2358
|
return;
|
|
2226
2359
|
}
|
|
2227
2360
|
}
|
|
2228
2361
|
|
|
2229
|
-
handleEditorClick(e) {
|
|
2362
|
+
handleEditorClick(e: MouseEvent): void {
|
|
2363
|
+
const target = e.target as HTMLElement;
|
|
2364
|
+
if (!target) return;
|
|
2365
|
+
|
|
2230
2366
|
// Unblock render now that click is being processed
|
|
2231
2367
|
this._blockRender = false;
|
|
2232
2368
|
|
|
2233
2369
|
// Line-level visibility button (pseudo-element ::before on .line.has-visibility)
|
|
2234
|
-
const lineEl =
|
|
2370
|
+
const lineEl = target.closest('.line.has-visibility') as HTMLElement | null;
|
|
2235
2371
|
if (lineEl) {
|
|
2236
2372
|
const rect = lineEl.getBoundingClientRect();
|
|
2237
2373
|
const clickX = e.clientX - rect.left;
|
|
@@ -2245,42 +2381,44 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2245
2381
|
return;
|
|
2246
2382
|
}
|
|
2247
2383
|
}
|
|
2248
|
-
|
|
2384
|
+
|
|
2249
2385
|
// Inline color swatch (pseudo-element positioned with left: -8px)
|
|
2250
|
-
if (
|
|
2251
|
-
const rect =
|
|
2386
|
+
if (target.classList.contains('json-color')) {
|
|
2387
|
+
const rect = target.getBoundingClientRect();
|
|
2252
2388
|
const clickX = e.clientX - rect.left;
|
|
2253
2389
|
// Pseudo-element is at left: -8px, so clickX will be negative when clicking on it
|
|
2254
2390
|
if (clickX < 0 && clickX >= -8) {
|
|
2255
2391
|
e.preventDefault();
|
|
2256
2392
|
e.stopPropagation();
|
|
2257
|
-
const color =
|
|
2258
|
-
const targetLineEl =
|
|
2393
|
+
const color = target.dataset.color;
|
|
2394
|
+
const targetLineEl = target.closest('.line') as HTMLElement | null;
|
|
2259
2395
|
if (targetLineEl) {
|
|
2260
|
-
const lineIndex = parseInt(targetLineEl.dataset.lineIndex);
|
|
2396
|
+
const lineIndex = parseInt(targetLineEl.dataset.lineIndex || '0');
|
|
2261
2397
|
const line = this.lines[lineIndex];
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2398
|
+
// Match any string attribute (hex or named color)
|
|
2399
|
+
// RE_ATTR_VALUE_SINGLE captures: [1] attributeName, [2] stringValue
|
|
2400
|
+
const match = line.match(RE_ATTR_VALUE_SINGLE);
|
|
2401
|
+
if (match && match[1] && color) {
|
|
2402
|
+
this.showColorPicker(target, lineIndex, color, match[1]);
|
|
2265
2403
|
}
|
|
2266
2404
|
}
|
|
2267
2405
|
return;
|
|
2268
2406
|
}
|
|
2269
2407
|
}
|
|
2270
|
-
|
|
2408
|
+
|
|
2271
2409
|
// Inline boolean checkbox (pseudo-element positioned with left: -8px)
|
|
2272
|
-
if (
|
|
2273
|
-
const rect =
|
|
2410
|
+
if (target.classList.contains('json-boolean')) {
|
|
2411
|
+
const rect = target.getBoundingClientRect();
|
|
2274
2412
|
const clickX = e.clientX - rect.left;
|
|
2275
2413
|
// Pseudo-element is at left: -8px, so clickX will be negative when clicking on it
|
|
2276
2414
|
if (clickX < 0 && clickX >= -8) {
|
|
2277
2415
|
e.preventDefault();
|
|
2278
2416
|
e.stopPropagation();
|
|
2279
|
-
const targetLineEl =
|
|
2417
|
+
const targetLineEl = target.closest('.line') as HTMLElement | null;
|
|
2280
2418
|
if (targetLineEl) {
|
|
2281
|
-
const lineIndex = parseInt(targetLineEl.dataset.lineIndex);
|
|
2419
|
+
const lineIndex = parseInt(targetLineEl.dataset.lineIndex || '0');
|
|
2282
2420
|
const line = this.lines[lineIndex];
|
|
2283
|
-
const match = line.match(
|
|
2421
|
+
const match = line.match(RE_ATTR_AND_BOOL_VALUE);
|
|
2284
2422
|
if (match) {
|
|
2285
2423
|
const currentValue = match[2] === 'true';
|
|
2286
2424
|
this.updateBooleanValue(lineIndex, !currentValue, match[1]);
|
|
@@ -2293,7 +2431,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2293
2431
|
|
|
2294
2432
|
// ========== Collapse/Expand ==========
|
|
2295
2433
|
|
|
2296
|
-
toggleCollapse(nodeId) {
|
|
2434
|
+
toggleCollapse(nodeId: string): void {
|
|
2297
2435
|
if (this.collapsedNodes.has(nodeId)) {
|
|
2298
2436
|
this.collapsedNodes.delete(nodeId);
|
|
2299
2437
|
} else {
|
|
@@ -2312,10 +2450,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2312
2450
|
|
|
2313
2451
|
/**
|
|
2314
2452
|
* Helper to apply collapsed option from API methods
|
|
2315
|
-
* @param {object} options - Options object with optional collapsed property
|
|
2316
|
-
* @param {array} features - Features array for function mode
|
|
2317
2453
|
*/
|
|
2318
|
-
_applyCollapsedFromOptions(options, features) {
|
|
2454
|
+
private _applyCollapsedFromOptions(options: SetOptions, features: Feature[]): void {
|
|
2319
2455
|
const collapsed = options.collapsed !== undefined ? options.collapsed : ['coordinates'];
|
|
2320
2456
|
if (collapsed && (Array.isArray(collapsed) ? collapsed.length > 0 : true)) {
|
|
2321
2457
|
this._applyCollapsedOption(collapsed, features);
|
|
@@ -2324,10 +2460,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2324
2460
|
|
|
2325
2461
|
/**
|
|
2326
2462
|
* Apply collapsed option to nodes
|
|
2327
|
-
* @param {string[]|function} collapsed - Attributes to collapse or function returning them
|
|
2328
|
-
* @param {array} features - Features array for function mode (optional)
|
|
2329
2463
|
*/
|
|
2330
|
-
_applyCollapsedOption(collapsed, features = null) {
|
|
2464
|
+
private _applyCollapsedOption(collapsed: string[] | ((feature: Feature | null, index: number) => string[]), features: Feature[] | null = null): void {
|
|
2331
2465
|
const ranges = this._findCollapsibleRanges();
|
|
2332
2466
|
|
|
2333
2467
|
// Group ranges by feature (root nodes)
|
|
@@ -2372,7 +2506,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2372
2506
|
|
|
2373
2507
|
// ========== Feature Visibility ==========
|
|
2374
2508
|
|
|
2375
|
-
toggleFeatureVisibility(featureKey) {
|
|
2509
|
+
toggleFeatureVisibility(featureKey: string | undefined): void {
|
|
2510
|
+
if (!featureKey) return;
|
|
2376
2511
|
if (this.hiddenFeatures.has(featureKey)) {
|
|
2377
2512
|
this.hiddenFeatures.delete(featureKey);
|
|
2378
2513
|
} else {
|
|
@@ -2386,17 +2521,17 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2386
2521
|
}
|
|
2387
2522
|
|
|
2388
2523
|
// ========== Color Picker ==========
|
|
2389
|
-
|
|
2390
|
-
showColorPicker(indicator, line, currentColor, attributeName) {
|
|
2524
|
+
|
|
2525
|
+
showColorPicker(indicator: HTMLElement, line: number, currentColor: string, attributeName: string) {
|
|
2391
2526
|
// Remove existing picker and anchor
|
|
2392
2527
|
const existing = document.querySelector('.geojson-color-picker-anchor');
|
|
2393
2528
|
if (existing) {
|
|
2394
2529
|
existing.remove();
|
|
2395
2530
|
}
|
|
2396
|
-
|
|
2531
|
+
|
|
2397
2532
|
// Create an anchor element at the pseudo-element position
|
|
2398
2533
|
// The browser will position the color picker popup relative to this
|
|
2399
|
-
const anchor =
|
|
2534
|
+
const anchor = _ce('div');
|
|
2400
2535
|
anchor.className = 'geojson-color-picker-anchor';
|
|
2401
2536
|
const rect = indicator.getBoundingClientRect();
|
|
2402
2537
|
anchor.style.cssText = `
|
|
@@ -2408,10 +2543,19 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2408
2543
|
z-index: 9998;
|
|
2409
2544
|
`;
|
|
2410
2545
|
document.body.appendChild(anchor);
|
|
2411
|
-
|
|
2412
|
-
const colorInput =
|
|
2546
|
+
|
|
2547
|
+
const colorInput = _ce('input') as HTMLInputElement & { _closeListener?: EventListener };
|
|
2413
2548
|
colorInput.type = 'color';
|
|
2414
|
-
|
|
2549
|
+
// Convert color to hex format for the color picker
|
|
2550
|
+
let hexColor = currentColor;
|
|
2551
|
+
if (!currentColor.startsWith('#')) {
|
|
2552
|
+
// Named color - convert to hex
|
|
2553
|
+
hexColor = namedColorToHex(currentColor) || '#000000';
|
|
2554
|
+
} else {
|
|
2555
|
+
// Expand 3-char hex to 6-char (#abc -> #aabbcc)
|
|
2556
|
+
hexColor = currentColor.replace(RE_NORMALIZE_COLOR, '#$1$1$2$2$3$3');
|
|
2557
|
+
}
|
|
2558
|
+
colorInput.value = hexColor;
|
|
2415
2559
|
colorInput.className = 'geojson-color-picker-input';
|
|
2416
2560
|
|
|
2417
2561
|
// Position the color input inside the anchor
|
|
@@ -2428,7 +2572,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2428
2572
|
`;
|
|
2429
2573
|
anchor.appendChild(colorInput);
|
|
2430
2574
|
|
|
2431
|
-
colorInput.addEventListener('input', (e
|
|
2575
|
+
colorInput.addEventListener('input', (e) => {
|
|
2432
2576
|
this.updateColorValue(line, (e.target as HTMLInputElement).value, attributeName);
|
|
2433
2577
|
});
|
|
2434
2578
|
|
|
@@ -2440,7 +2584,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2440
2584
|
};
|
|
2441
2585
|
|
|
2442
2586
|
colorInput._closeListener = closeOnClickOutside;
|
|
2443
|
-
|
|
2587
|
+
|
|
2444
2588
|
setTimeout(() => {
|
|
2445
2589
|
document.addEventListener('click', closeOnClickOutside, true);
|
|
2446
2590
|
}, 100);
|
|
@@ -2449,17 +2593,18 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2449
2593
|
colorInput.click();
|
|
2450
2594
|
}
|
|
2451
2595
|
|
|
2452
|
-
updateColorValue(line, newColor, attributeName) {
|
|
2453
|
-
|
|
2596
|
+
updateColorValue(line: number, newColor: string, attributeName: string) {
|
|
2597
|
+
// Match both hex colors (#xxx, #xxxxxx) and named colors (red, blue, etc.)
|
|
2598
|
+
const regex = new RegExp(`"${attributeName}"\\s*:\\s*"(?:#[0-9a-fA-F]{3,6}|[a-zA-Z]+)"`);
|
|
2454
2599
|
this.lines[line] = this.lines[line].replace(regex, `"${attributeName}": "${newColor}"`);
|
|
2455
|
-
|
|
2600
|
+
|
|
2456
2601
|
// Use updateView to preserve collapsed state (line count didn't change)
|
|
2457
2602
|
this.updateView();
|
|
2458
2603
|
this.scheduleRender();
|
|
2459
2604
|
this.emitChange();
|
|
2460
2605
|
}
|
|
2461
2606
|
|
|
2462
|
-
updateBooleanValue(line, newValue, attributeName) {
|
|
2607
|
+
updateBooleanValue(line: number, newValue: boolean, attributeName: string): void {
|
|
2463
2608
|
const regex = new RegExp(`"${attributeName}"\\s*:\\s*(true|false)`);
|
|
2464
2609
|
this.lines[line] = this.lines[line].replace(regex, `"${attributeName}": ${newValue}`);
|
|
2465
2610
|
|
|
@@ -2501,14 +2646,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2501
2646
|
|
|
2502
2647
|
// Filter hidden features
|
|
2503
2648
|
if (this.hiddenFeatures.size > 0) {
|
|
2504
|
-
parsed.features = parsed.features.filter((feature) => {
|
|
2505
|
-
const key =
|
|
2506
|
-
return !this.hiddenFeatures.has(key);
|
|
2649
|
+
parsed.features = parsed.features.filter((feature: Feature) => {
|
|
2650
|
+
const key = getFeatureKey(feature);
|
|
2651
|
+
return key ? !this.hiddenFeatures.has(key) : true;
|
|
2507
2652
|
});
|
|
2508
2653
|
}
|
|
2509
2654
|
|
|
2510
2655
|
// Validate
|
|
2511
|
-
const errors =
|
|
2656
|
+
const errors = validateGeoJSON(parsed);
|
|
2512
2657
|
|
|
2513
2658
|
if (errors.length > 0) {
|
|
2514
2659
|
this.dispatchEvent(new CustomEvent('error', {
|
|
@@ -2525,7 +2670,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2525
2670
|
}
|
|
2526
2671
|
} catch (e) {
|
|
2527
2672
|
this.dispatchEvent(new CustomEvent('error', {
|
|
2528
|
-
detail: { error: e.message, content },
|
|
2673
|
+
detail: { error: e instanceof Error ? e.message : 'Unknown error', content },
|
|
2529
2674
|
bubbles: true,
|
|
2530
2675
|
composed: true
|
|
2531
2676
|
}));
|
|
@@ -2533,56 +2678,48 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2533
2678
|
}
|
|
2534
2679
|
|
|
2535
2680
|
// ========== UI Updates ==========
|
|
2536
|
-
|
|
2537
|
-
updateReadonly() {
|
|
2538
|
-
const textarea = this.shadowRoot.getElementById('hiddenTextarea');
|
|
2539
|
-
const clearBtn = this.shadowRoot!.getElementById('clearBtn') as HTMLButtonElement;
|
|
2540
2681
|
|
|
2682
|
+
updateReadonly() {
|
|
2541
2683
|
// Use readOnly instead of disabled to allow text selection for copying
|
|
2542
|
-
if (
|
|
2543
|
-
if (
|
|
2684
|
+
if (this._hiddenTextarea) this._hiddenTextarea.readOnly = this.readonly;
|
|
2685
|
+
if (this._clearBtn) this._clearBtn.hidden = this.readonly;
|
|
2544
2686
|
}
|
|
2545
2687
|
|
|
2546
2688
|
updatePlaceholderVisibility() {
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
placeholder.style.display = this.lines.length > 0 ? 'none' : 'block';
|
|
2689
|
+
if (this._placeholderLayer) {
|
|
2690
|
+
this._placeholderLayer.style.display = this.lines.length > 0 ? 'none' : 'block';
|
|
2550
2691
|
}
|
|
2551
2692
|
}
|
|
2552
2693
|
|
|
2553
2694
|
updatePlaceholderContent() {
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
placeholder.textContent = this.placeholder;
|
|
2695
|
+
if (this._placeholderLayer) {
|
|
2696
|
+
this._placeholderLayer.textContent = this.placeholder;
|
|
2557
2697
|
}
|
|
2558
2698
|
this.updatePlaceholderVisibility();
|
|
2559
2699
|
}
|
|
2560
2700
|
|
|
2561
2701
|
updatePrefixSuffix() {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
if (prefix) prefix.textContent = this.prefix;
|
|
2566
|
-
if (suffix) suffix.textContent = this.suffix;
|
|
2702
|
+
if (this._editorPrefix) this._editorPrefix.textContent = this.prefix;
|
|
2703
|
+
if (this._editorSuffix) this._editorSuffix.textContent = this.suffix;
|
|
2567
2704
|
}
|
|
2568
2705
|
|
|
2569
2706
|
// ========== Theme ==========
|
|
2570
2707
|
|
|
2571
2708
|
updateThemeCSS() {
|
|
2572
2709
|
const darkSelector = this.getAttribute('dark-selector') || '.dark';
|
|
2573
|
-
const darkRule =
|
|
2574
|
-
|
|
2575
|
-
let themeStyle = this.
|
|
2710
|
+
const darkRule = parseSelectorToHostRule(darkSelector);
|
|
2711
|
+
|
|
2712
|
+
let themeStyle = this._id('theme-styles') as HTMLStyleElement;
|
|
2576
2713
|
if (!themeStyle) {
|
|
2577
|
-
themeStyle =
|
|
2714
|
+
themeStyle = _ce('style') as HTMLStyleElement;
|
|
2578
2715
|
themeStyle.id = 'theme-styles';
|
|
2579
|
-
this.shadowRoot
|
|
2716
|
+
this.shadowRoot!.insertBefore(themeStyle, this.shadowRoot!.firstChild);
|
|
2580
2717
|
}
|
|
2581
2718
|
|
|
2582
2719
|
const darkDefaults = {
|
|
2583
2720
|
bgColor: '#2b2b2b',
|
|
2584
2721
|
textColor: '#a9b7c6',
|
|
2585
|
-
caretColor: '#
|
|
2722
|
+
caretColor: '#bbb',
|
|
2586
2723
|
gutterBg: '#313335',
|
|
2587
2724
|
gutterBorder: '#3c3f41',
|
|
2588
2725
|
gutterText: '#606366',
|
|
@@ -2602,14 +2739,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2602
2739
|
jsonKeyInvalid: '#ff6b68'
|
|
2603
2740
|
};
|
|
2604
2741
|
|
|
2605
|
-
|
|
2606
|
-
const
|
|
2742
|
+
RE_TO_KEBAB.lastIndex = 0;
|
|
2743
|
+
const toKebab = (str: string) => str.replace(RE_TO_KEBAB, '-$1').toLowerCase();
|
|
2744
|
+
const generateVars = (obj: Record<string, string | undefined>) => Object.entries(obj)
|
|
2745
|
+
.filter((entry): entry is [string, string] => entry[1] !== undefined)
|
|
2607
2746
|
.map(([k, v]) => `--${toKebab(k)}: ${v};`)
|
|
2608
2747
|
.join('\n ');
|
|
2609
2748
|
|
|
2610
|
-
const lightVars = generateVars(this.themes.light || {});
|
|
2749
|
+
const lightVars = generateVars(this.themes.light as Record<string, string | undefined> || {});
|
|
2611
2750
|
const darkTheme = { ...darkDefaults, ...this.themes.dark };
|
|
2612
|
-
const darkVars = generateVars(darkTheme);
|
|
2751
|
+
const darkVars = generateVars(darkTheme as Record<string, string | undefined>);
|
|
2613
2752
|
|
|
2614
2753
|
let css = lightVars ? `:host {\n ${lightVars}\n }\n` : '';
|
|
2615
2754
|
css += `${darkRule} {\n ${darkVars}\n }`;
|
|
@@ -2617,14 +2756,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2617
2756
|
themeStyle.textContent = css;
|
|
2618
2757
|
}
|
|
2619
2758
|
|
|
2620
|
-
_parseSelectorToHostRule(selector) {
|
|
2621
|
-
if (!selector) return ':host([data-color-scheme="dark"])';
|
|
2622
|
-
if (selector.startsWith('.') && !selector.includes(' ')) {
|
|
2623
|
-
return `:host(${selector})`;
|
|
2624
|
-
}
|
|
2625
|
-
return `:host-context(${selector})`;
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
2759
|
setTheme(theme: ThemeSettings): void {
|
|
2629
2760
|
if (theme.dark) this.themes.dark = { ...this.themes.dark, ...theme.dark };
|
|
2630
2761
|
if (theme.light) this.themes.light = { ...this.themes.light, ...theme.light };
|
|
@@ -2636,45 +2767,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2636
2767
|
this.updateThemeCSS();
|
|
2637
2768
|
}
|
|
2638
2769
|
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
_getFeatureKey(feature) {
|
|
2642
|
-
if (!feature) return null;
|
|
2643
|
-
if (feature.id !== undefined) return `id:${feature.id}`;
|
|
2644
|
-
if (feature.properties?.id !== undefined) return `prop:${feature.properties.id}`;
|
|
2645
|
-
|
|
2646
|
-
const geomType = feature.geometry?.type || 'null';
|
|
2647
|
-
const coords = JSON.stringify(feature.geometry?.coordinates || []);
|
|
2648
|
-
let hash = 0;
|
|
2649
|
-
for (let i = 0; i < coords.length; i++) {
|
|
2650
|
-
hash = ((hash << 5) - hash) + coords.charCodeAt(i);
|
|
2651
|
-
hash = hash & hash;
|
|
2652
|
-
}
|
|
2653
|
-
return `hash:${geomType}:${hash.toString(36)}`;
|
|
2654
|
-
}
|
|
2655
|
-
|
|
2656
|
-
_countBrackets(line, openBracket) {
|
|
2657
|
-
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
2658
|
-
let open = 0, close = 0, inString = false, escape = false;
|
|
2659
|
-
|
|
2660
|
-
for (const char of line) {
|
|
2661
|
-
if (escape) { escape = false; continue; }
|
|
2662
|
-
if (char === '\\' && inString) { escape = true; continue; }
|
|
2663
|
-
if (char === '"') { inString = !inString; continue; }
|
|
2664
|
-
if (!inString) {
|
|
2665
|
-
if (char === openBracket) open++;
|
|
2666
|
-
if (char === closeBracket) close++;
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
|
-
return { open, close };
|
|
2770
|
+
getTheme(): ThemeSettings {
|
|
2771
|
+
return { ...this.themes };
|
|
2671
2772
|
}
|
|
2672
2773
|
|
|
2673
2774
|
/**
|
|
2674
2775
|
* Find all collapsible ranges using the mappings built by _rebuildNodeIdMappings
|
|
2675
2776
|
* This method only READS the existing mappings, it doesn't create new IDs
|
|
2676
2777
|
*/
|
|
2677
|
-
_findCollapsibleRanges() {
|
|
2778
|
+
private _findCollapsibleRanges() {
|
|
2678
2779
|
const ranges = [];
|
|
2679
2780
|
|
|
2680
2781
|
// Simply iterate through the existing mappings
|
|
@@ -2686,13 +2787,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2686
2787
|
if (!line) continue;
|
|
2687
2788
|
|
|
2688
2789
|
// Match "key": { or "key": [
|
|
2689
|
-
const kvMatch = line.match(
|
|
2790
|
+
const kvMatch = line.match(RE_KV_MATCH);
|
|
2690
2791
|
// Also match standalone { or [ (root Feature objects)
|
|
2691
|
-
const rootMatch = !kvMatch && line.match(
|
|
2792
|
+
const rootMatch = !kvMatch && line.match(RE_ROOT_MATCH);
|
|
2692
2793
|
|
|
2693
2794
|
if (!kvMatch && !rootMatch) continue;
|
|
2694
|
-
|
|
2695
|
-
const openBracket = kvMatch ? kvMatch[2] : rootMatch[1];
|
|
2795
|
+
|
|
2796
|
+
const openBracket = kvMatch ? kvMatch[2] : (rootMatch ? rootMatch[1] : '{');
|
|
2696
2797
|
|
|
2697
2798
|
ranges.push({
|
|
2698
2799
|
startLine: rangeInfo.startLine,
|
|
@@ -2710,20 +2811,20 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2710
2811
|
return ranges;
|
|
2711
2812
|
}
|
|
2712
2813
|
|
|
2713
|
-
_findClosingLine(startLine, openBracket) {
|
|
2814
|
+
private _findClosingLine(startLine: number, openBracket: string): number {
|
|
2714
2815
|
let depth = 1;
|
|
2715
2816
|
const line = this.lines[startLine];
|
|
2716
2817
|
const bracketPos = line.indexOf(openBracket);
|
|
2717
2818
|
|
|
2718
2819
|
if (bracketPos !== -1) {
|
|
2719
2820
|
const rest = line.substring(bracketPos + 1);
|
|
2720
|
-
const counts =
|
|
2821
|
+
const counts = countBrackets(rest, openBracket);
|
|
2721
2822
|
depth += counts.open - counts.close;
|
|
2722
2823
|
if (depth === 0) return startLine;
|
|
2723
2824
|
}
|
|
2724
2825
|
|
|
2725
2826
|
for (let i = startLine + 1; i < this.lines.length; i++) {
|
|
2726
|
-
const counts =
|
|
2827
|
+
const counts = countBrackets(this.lines[i], openBracket);
|
|
2727
2828
|
depth += counts.open - counts.close;
|
|
2728
2829
|
if (depth === 0) return i;
|
|
2729
2830
|
}
|
|
@@ -2731,7 +2832,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2731
2832
|
return -1;
|
|
2732
2833
|
}
|
|
2733
2834
|
|
|
2734
|
-
_buildContextMap() {
|
|
2835
|
+
private _buildContextMap() {
|
|
2735
2836
|
// Memoization: return cached result if content hasn't changed
|
|
2736
2837
|
const linesLength = this.lines.length;
|
|
2737
2838
|
if (this._contextMapCache &&
|
|
@@ -2741,9 +2842,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2741
2842
|
return this._contextMapCache;
|
|
2742
2843
|
}
|
|
2743
2844
|
|
|
2744
|
-
const contextMap = new Map();
|
|
2745
|
-
const contextStack = [];
|
|
2746
|
-
let pendingContext = null;
|
|
2845
|
+
const contextMap = new Map<number, string>();
|
|
2846
|
+
const contextStack: { context: string; isArray: boolean }[] = [];
|
|
2847
|
+
let pendingContext: string | null = null;
|
|
2747
2848
|
|
|
2748
2849
|
for (let i = 0; i < linesLength; i++) {
|
|
2749
2850
|
const line = this.lines[i];
|
|
@@ -2756,10 +2857,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2756
2857
|
else if (RE_CONTEXT_FEATURES.test(line)) pendingContext = 'Feature';
|
|
2757
2858
|
|
|
2758
2859
|
// Track brackets
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2860
|
+
RE_OPEN_BRACES.lastIndex = 0;
|
|
2861
|
+
RE_CLOSE_BRACES.lastIndex = 0;
|
|
2862
|
+
RE_OPEN_BRACKETS.lastIndex = 0;
|
|
2863
|
+
RE_CLOSE_BRACKET.lastIndex = 0;
|
|
2864
|
+
const openBraces = (line.match(RE_OPEN_BRACES) || []).length;
|
|
2865
|
+
const closeBraces = (line.match(RE_CLOSE_BRACES) || []).length;
|
|
2866
|
+
const openBrackets = (line.match(RE_OPEN_BRACKETS) || []).length;
|
|
2867
|
+
const closeBrackets = (line.match(RE_CLOSE_BRACKET) || []).length;
|
|
2763
2868
|
|
|
2764
2869
|
for (let j = 0; j < openBraces + openBrackets; j++) {
|
|
2765
2870
|
contextStack.push({ context: pendingContext || currentContext, isArray: j >= openBraces });
|
|
@@ -2780,214 +2885,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2780
2885
|
return contextMap;
|
|
2781
2886
|
}
|
|
2782
2887
|
|
|
2783
|
-
|
|
2784
|
-
if (!text) return '';
|
|
2785
|
-
|
|
2786
|
-
// For collapsed nodes, truncate the text at the opening bracket
|
|
2787
|
-
let displayText = text;
|
|
2788
|
-
let collapsedBracket = null;
|
|
2789
|
-
|
|
2790
|
-
if (meta?.collapseButton?.isCollapsed) {
|
|
2791
|
-
// Match "key": { or "key": [
|
|
2792
|
-
const bracketMatch = text.match(RE_COLLAPSED_BRACKET);
|
|
2793
|
-
// Also match standalone { or [ (root Feature objects)
|
|
2794
|
-
const rootMatch = !bracketMatch && text.match(RE_COLLAPSED_ROOT);
|
|
2795
|
-
|
|
2796
|
-
if (bracketMatch) {
|
|
2797
|
-
displayText = bracketMatch[1] + bracketMatch[2];
|
|
2798
|
-
collapsedBracket = bracketMatch[2];
|
|
2799
|
-
} else if (rootMatch) {
|
|
2800
|
-
displayText = rootMatch[1] + rootMatch[2];
|
|
2801
|
-
collapsedBracket = rootMatch[2];
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
|
|
2805
|
-
// Escape HTML first
|
|
2806
|
-
let result = displayText
|
|
2807
|
-
.replace(RE_ESCAPE_AMP, '&')
|
|
2808
|
-
.replace(RE_ESCAPE_LT, '<')
|
|
2809
|
-
.replace(RE_ESCAPE_GT, '>');
|
|
2810
|
-
|
|
2811
|
-
// Punctuation FIRST (before other replacements can interfere)
|
|
2812
|
-
result = result.replace(RE_PUNCTUATION, '<span class="json-punctuation">$1</span>');
|
|
2813
|
-
|
|
2814
|
-
// JSON keys - match "key" followed by :
|
|
2815
|
-
// In properties context, all keys are treated as regular JSON keys
|
|
2816
|
-
RE_JSON_KEYS.lastIndex = 0;
|
|
2817
|
-
result = result.replace(RE_JSON_KEYS, (match, key, colon) => {
|
|
2818
|
-
if (context !== 'properties' && GEOJSON_KEYS.includes(key)) {
|
|
2819
|
-
return `<span class="geojson-key">"${key}"</span>${colon}`;
|
|
2820
|
-
}
|
|
2821
|
-
return `<span class="json-key">"${key}"</span>${colon}`;
|
|
2822
|
-
});
|
|
2823
|
-
|
|
2824
|
-
// Type values - "type": "Value" - but NOT inside properties context
|
|
2825
|
-
if (context !== 'properties') {
|
|
2826
|
-
RE_TYPE_VALUES.lastIndex = 0;
|
|
2827
|
-
result = result.replace(RE_TYPE_VALUES, (match, space, type) => {
|
|
2828
|
-
const isValid = type === 'Feature' || type === 'FeatureCollection' || GEOMETRY_TYPES.includes(type);
|
|
2829
|
-
const cls = isValid ? 'geojson-type' : 'geojson-type-invalid';
|
|
2830
|
-
return `<span class="geojson-key">"type"</span><span class="json-punctuation">:</span>${space}<span class="${cls}">"${type}"</span>`;
|
|
2831
|
-
});
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
// String values (not already wrapped in spans)
|
|
2835
|
-
RE_STRING_VALUES.lastIndex = 0;
|
|
2836
|
-
result = result.replace(RE_STRING_VALUES, (match, colon, space, val) => {
|
|
2837
|
-
if (match.includes('geojson-type') || match.includes('json-string')) return match;
|
|
2838
|
-
if (RE_COLOR_HEX.test(val)) {
|
|
2839
|
-
return `${colon}${space}<span class="json-string json-color" data-color="${val}" style="--swatch-color: ${val}">"${val}"</span>`;
|
|
2840
|
-
}
|
|
2841
|
-
return `${colon}${space}<span class="json-string">"${val}"</span>`;
|
|
2842
|
-
});
|
|
2843
|
-
|
|
2844
|
-
// Numbers after colon
|
|
2845
|
-
RE_NUMBERS_COLON.lastIndex = 0;
|
|
2846
|
-
result = result.replace(RE_NUMBERS_COLON, '$1$2<span class="json-number">$3</span>');
|
|
2847
|
-
|
|
2848
|
-
// Numbers in arrays (after [ or ,)
|
|
2849
|
-
RE_NUMBERS_ARRAY.lastIndex = 0;
|
|
2850
|
-
result = result.replace(RE_NUMBERS_ARRAY, '$1$2<span class="json-number">$3</span>');
|
|
2851
|
-
|
|
2852
|
-
// Standalone numbers at start of line (coordinates arrays)
|
|
2853
|
-
RE_NUMBERS_START.lastIndex = 0;
|
|
2854
|
-
result = result.replace(RE_NUMBERS_START, '$1<span class="json-number">$2</span>');
|
|
2855
|
-
|
|
2856
|
-
// Booleans - use ::before for checkbox via CSS class
|
|
2857
|
-
RE_BOOLEANS.lastIndex = 0;
|
|
2858
|
-
result = result.replace(RE_BOOLEANS, (match, colon, space, val) => {
|
|
2859
|
-
const checkedClass = val === 'true' ? ' json-bool-true' : ' json-bool-false';
|
|
2860
|
-
return `${colon}${space}<span class="json-boolean${checkedClass}">${val}</span>`;
|
|
2861
|
-
});
|
|
2862
|
-
|
|
2863
|
-
// Null
|
|
2864
|
-
RE_NULL.lastIndex = 0;
|
|
2865
|
-
result = result.replace(RE_NULL, '$1$2<span class="json-null">$3</span>');
|
|
2866
|
-
|
|
2867
|
-
// Collapsed bracket indicator
|
|
2868
|
-
if (collapsedBracket) {
|
|
2869
|
-
const bracketClass = collapsedBracket === '[' ? 'collapsed-bracket-array' : 'collapsed-bracket-object';
|
|
2870
|
-
result = result.replace(
|
|
2871
|
-
new RegExp(`<span class="json-punctuation">\\${collapsedBracket}<\\/span>$`),
|
|
2872
|
-
`<span class="${bracketClass}">${collapsedBracket}</span>`
|
|
2873
|
-
);
|
|
2874
|
-
}
|
|
2875
|
-
|
|
2876
|
-
// Mark unrecognized text as error
|
|
2877
|
-
RE_UNRECOGNIZED.lastIndex = 0;
|
|
2878
|
-
result = result.replace(RE_UNRECOGNIZED, (match, before, text, after) => {
|
|
2879
|
-
if (!text || RE_WHITESPACE_ONLY.test(text)) return match;
|
|
2880
|
-
// Check for unrecognized words/tokens (not whitespace, not just spaces/commas)
|
|
2881
|
-
// Keep whitespace as-is, wrap any non-whitespace unrecognized token
|
|
2882
|
-
const parts = text.split(RE_WHITESPACE_SPLIT);
|
|
2883
|
-
let hasError = false;
|
|
2884
|
-
const processed = parts.map(part => {
|
|
2885
|
-
// If it's whitespace, keep it
|
|
2886
|
-
if (RE_WHITESPACE_ONLY.test(part)) return part;
|
|
2887
|
-
// Mark as error
|
|
2888
|
-
hasError = true;
|
|
2889
|
-
return `<span class="json-error">${part}</span>`;
|
|
2890
|
-
}).join('');
|
|
2891
|
-
return hasError ? before + processed + after : match;
|
|
2892
|
-
});
|
|
2893
|
-
|
|
2894
|
-
// Note: visibility is now handled at line level (has-visibility class on .line element)
|
|
2895
|
-
|
|
2896
|
-
return result;
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
_validateGeoJSON(parsed) {
|
|
2900
|
-
const errors = [];
|
|
2901
|
-
|
|
2902
|
-
if (!parsed.features) return errors;
|
|
2903
|
-
|
|
2904
|
-
parsed.features.forEach((feature, i) => {
|
|
2905
|
-
if (feature.type !== 'Feature') {
|
|
2906
|
-
errors.push(`features[${i}]: type must be "Feature"`);
|
|
2907
|
-
}
|
|
2908
|
-
if (feature.geometry && feature.geometry.type) {
|
|
2909
|
-
if (!GEOMETRY_TYPES.includes(feature.geometry.type)) {
|
|
2910
|
-
errors.push(`features[${i}].geometry: invalid type "${feature.geometry.type}"`);
|
|
2911
|
-
}
|
|
2912
|
-
}
|
|
2913
|
-
});
|
|
2914
|
-
|
|
2915
|
-
return errors;
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
/**
|
|
2919
|
-
* Validate a single feature object
|
|
2920
|
-
* @param {object} feature - The feature to validate
|
|
2921
|
-
* @throws {Error} If the feature is invalid
|
|
2922
|
-
*/
|
|
2923
|
-
_validateFeature(feature) {
|
|
2924
|
-
if (!feature || typeof feature !== 'object') {
|
|
2925
|
-
throw new Error('Feature must be an object');
|
|
2926
|
-
}
|
|
2927
|
-
if (feature.type !== 'Feature') {
|
|
2928
|
-
throw new Error('Feature type must be "Feature"');
|
|
2929
|
-
}
|
|
2930
|
-
if (!('geometry' in feature)) {
|
|
2931
|
-
throw new Error('Feature must have a geometry property');
|
|
2932
|
-
}
|
|
2933
|
-
if (!('properties' in feature)) {
|
|
2934
|
-
throw new Error('Feature must have a properties property');
|
|
2935
|
-
}
|
|
2936
|
-
if (feature.geometry !== null) {
|
|
2937
|
-
if (!feature.geometry || typeof feature.geometry !== 'object') {
|
|
2938
|
-
throw new Error('Feature geometry must be an object or null');
|
|
2939
|
-
}
|
|
2940
|
-
if (!feature.geometry.type) {
|
|
2941
|
-
throw new Error('Feature geometry must have a type');
|
|
2942
|
-
}
|
|
2943
|
-
if (!GEOMETRY_TYPES.includes(feature.geometry.type)) {
|
|
2944
|
-
throw new Error(`Invalid geometry type: "${feature.geometry.type}"`);
|
|
2945
|
-
}
|
|
2946
|
-
if (!('coordinates' in feature.geometry)) {
|
|
2947
|
-
throw new Error('Feature geometry must have coordinates');
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
if (feature.properties !== null && typeof feature.properties !== 'object') {
|
|
2951
|
-
throw new Error('Feature properties must be an object or null');
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
|
|
2955
|
-
/**
|
|
2956
|
-
* Normalize input to an array of features
|
|
2957
|
-
* Accepts: FeatureCollection, Feature[], or single Feature
|
|
2958
|
-
* @param {object|array} input - Input to normalize
|
|
2959
|
-
* @returns {array} Array of features
|
|
2960
|
-
* @throws {Error} If input is invalid
|
|
2961
|
-
*/
|
|
2962
|
-
_normalizeToFeatures(input) {
|
|
2963
|
-
let features = [];
|
|
2964
|
-
|
|
2965
|
-
if (Array.isArray(input)) {
|
|
2966
|
-
// Array of features
|
|
2967
|
-
features = input;
|
|
2968
|
-
} else if (input && typeof input === 'object') {
|
|
2969
|
-
if (input.type === 'FeatureCollection' && Array.isArray(input.features)) {
|
|
2970
|
-
// FeatureCollection
|
|
2971
|
-
features = input.features;
|
|
2972
|
-
} else if (input.type === 'Feature') {
|
|
2973
|
-
// Single Feature
|
|
2974
|
-
features = [input];
|
|
2975
|
-
} else {
|
|
2976
|
-
throw new Error('Input must be a Feature, array of Features, or FeatureCollection');
|
|
2977
|
-
}
|
|
2978
|
-
} else {
|
|
2979
|
-
throw new Error('Input must be a Feature, array of Features, or FeatureCollection');
|
|
2980
|
-
}
|
|
2981
|
-
|
|
2982
|
-
// Validate each feature
|
|
2983
|
-
for (const feature of features) {
|
|
2984
|
-
this._validateFeature(feature);
|
|
2985
|
-
}
|
|
2986
|
-
|
|
2987
|
-
return features;
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
// ========== Public API ==========
|
|
2888
|
+
// ========== Public API ==========
|
|
2991
2889
|
|
|
2992
2890
|
/**
|
|
2993
2891
|
* Replace all features in the editor
|
|
@@ -3001,10 +2899,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3001
2899
|
* @throws {Error} If input is invalid
|
|
3002
2900
|
*/
|
|
3003
2901
|
set(input: FeatureInput, options: SetOptions = {}): void {
|
|
3004
|
-
const features =
|
|
3005
|
-
|
|
3006
|
-
this.setValue(formatted, false); // Don't auto-collapse coordinates
|
|
3007
|
-
this._applyCollapsedFromOptions(options, features);
|
|
2902
|
+
const features = normalizeToFeatures(input);
|
|
2903
|
+
this._setFeaturesInternal(features, options);
|
|
3008
2904
|
}
|
|
3009
2905
|
|
|
3010
2906
|
/**
|
|
@@ -3016,12 +2912,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3016
2912
|
* @throws {Error} If input is invalid
|
|
3017
2913
|
*/
|
|
3018
2914
|
add(input: FeatureInput, options: SetOptions = {}): void {
|
|
3019
|
-
const newFeatures =
|
|
3020
|
-
const
|
|
3021
|
-
|
|
3022
|
-
const formatted = allFeatures.map(f => JSON.stringify(f, null, 2)).join(',\n');
|
|
3023
|
-
this.setValue(formatted, false); // Don't auto-collapse coordinates
|
|
3024
|
-
this._applyCollapsedFromOptions(options, allFeatures);
|
|
2915
|
+
const newFeatures = normalizeToFeatures(input);
|
|
2916
|
+
const allFeatures = [...this._parseFeatures(), ...newFeatures];
|
|
2917
|
+
this._setFeaturesInternal(allFeatures, options);
|
|
3025
2918
|
}
|
|
3026
2919
|
|
|
3027
2920
|
/**
|
|
@@ -3034,12 +2927,19 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3034
2927
|
* @throws {Error} If input is invalid
|
|
3035
2928
|
*/
|
|
3036
2929
|
insertAt(input: FeatureInput, index: number, options: SetOptions = {}): void {
|
|
3037
|
-
const newFeatures =
|
|
2930
|
+
const newFeatures = normalizeToFeatures(input);
|
|
3038
2931
|
const features = this._parseFeatures();
|
|
3039
2932
|
const idx = index < 0 ? features.length + index : index;
|
|
3040
2933
|
features.splice(Math.max(0, Math.min(idx, features.length)), 0, ...newFeatures);
|
|
2934
|
+
this._setFeaturesInternal(features, options);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
/**
|
|
2938
|
+
* Internal method to set features with formatting and collapse options
|
|
2939
|
+
*/
|
|
2940
|
+
private _setFeaturesInternal(features: Feature[], options: SetOptions): void {
|
|
3041
2941
|
const formatted = features.map(f => JSON.stringify(f, null, 2)).join(',\n');
|
|
3042
|
-
this.setValue(formatted, false);
|
|
2942
|
+
this.setValue(formatted, false);
|
|
3043
2943
|
this._applyCollapsedFromOptions(options, features);
|
|
3044
2944
|
}
|
|
3045
2945
|
|
|
@@ -3097,7 +2997,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3097
2997
|
const blob = new Blob([json], { type: 'application/geo+json' });
|
|
3098
2998
|
const url = URL.createObjectURL(blob);
|
|
3099
2999
|
|
|
3100
|
-
const a =
|
|
3000
|
+
const a = _ce('a') as HTMLAnchorElement;
|
|
3101
3001
|
a.href = url;
|
|
3102
3002
|
a.download = filename;
|
|
3103
3003
|
document.body.appendChild(a);
|
|
@@ -3120,12 +3020,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3120
3020
|
*/
|
|
3121
3021
|
open(options: SetOptions = {}): Promise<boolean> {
|
|
3122
3022
|
return new Promise((resolve) => {
|
|
3123
|
-
const input =
|
|
3023
|
+
const input = _ce('input') as HTMLInputElement;
|
|
3124
3024
|
input.type = 'file';
|
|
3125
3025
|
input.accept = '.geojson,.json,application/geo+json,application/json';
|
|
3126
3026
|
input.style.display = 'none';
|
|
3127
3027
|
|
|
3128
|
-
input.addEventListener('change', (e
|
|
3028
|
+
input.addEventListener('change', (e) => {
|
|
3129
3029
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
3130
3030
|
if (!file) {
|
|
3131
3031
|
document.body.removeChild(input);
|
|
@@ -3140,7 +3040,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3140
3040
|
const parsed = JSON.parse(content);
|
|
3141
3041
|
|
|
3142
3042
|
// Normalize and validate features
|
|
3143
|
-
const features =
|
|
3043
|
+
const features = normalizeToFeatures(parsed);
|
|
3144
3044
|
|
|
3145
3045
|
// Load features into editor
|
|
3146
3046
|
this._saveToHistory('open');
|
|
@@ -3173,7 +3073,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3173
3073
|
});
|
|
3174
3074
|
}
|
|
3175
3075
|
|
|
3176
|
-
_parseFeatures() {
|
|
3076
|
+
private _parseFeatures() {
|
|
3177
3077
|
try {
|
|
3178
3078
|
const content = this.lines.join('\n');
|
|
3179
3079
|
if (!content.trim()) return [];
|