@softwarity/geojson-editor 1.0.16 → 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 +712 -767
- 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
|
+
}
|
|
1568
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
|
+
}
|
|
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,19 +1950,41 @@ 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);
|
|
1957
|
+
|
|
1958
|
+
// Check if we're on a collapsed node's opening line
|
|
1959
|
+
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
1829
1960
|
|
|
1830
1961
|
if (direction > 0) {
|
|
1831
1962
|
// Move right
|
|
1832
1963
|
let pos = this.cursorColumn;
|
|
1833
1964
|
|
|
1965
|
+
// If on collapsed node opening line and cursor is at/after the bracket, jump to closing line
|
|
1966
|
+
if (onCollapsed) {
|
|
1967
|
+
const bracketPos = line.search(RE_BRACKET_POS);
|
|
1968
|
+
if (bracketPos >= 0 && pos >= bracketPos) {
|
|
1969
|
+
this.cursorLine = onCollapsed.endLine;
|
|
1970
|
+
this.cursorColumn = (this.lines[this.cursorLine] || '').length;
|
|
1971
|
+
this._invalidateRenderCache();
|
|
1972
|
+
this._scrollToCursor();
|
|
1973
|
+
this.scheduleRender();
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1834
1978
|
if (pos >= line.length) {
|
|
1835
|
-
// At end of line, move to start of next line
|
|
1979
|
+
// At end of line, move to start of next visible line
|
|
1836
1980
|
if (this.cursorLine < this.lines.length - 1) {
|
|
1837
|
-
this.cursorLine
|
|
1981
|
+
let nextLine = this.cursorLine + 1;
|
|
1982
|
+
// Skip collapsed zones
|
|
1983
|
+
const collapsed = this._getCollapsedRangeForLine(nextLine);
|
|
1984
|
+
if (collapsed) {
|
|
1985
|
+
nextLine = collapsed.endLine;
|
|
1986
|
+
}
|
|
1987
|
+
this.cursorLine = Math.min(nextLine, this.lines.length - 1);
|
|
1838
1988
|
this.cursorColumn = 0;
|
|
1839
1989
|
}
|
|
1840
1990
|
} else if (isWordChar(line[pos])) {
|
|
@@ -1854,10 +2004,33 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1854
2004
|
// Move left
|
|
1855
2005
|
let pos = this.cursorColumn;
|
|
1856
2006
|
|
|
2007
|
+
// Check if we're on closing line of a collapsed node
|
|
2008
|
+
const onClosingLine = this._getCollapsedClosingLine(this.cursorLine);
|
|
2009
|
+
if (onClosingLine) {
|
|
2010
|
+
const bracketPos = this._getClosingBracketPos(line);
|
|
2011
|
+
if (bracketPos >= 0 && pos <= bracketPos + 1) {
|
|
2012
|
+
// Jump to opening line, after the bracket
|
|
2013
|
+
this.cursorLine = onClosingLine.startLine;
|
|
2014
|
+
const openLine = this.lines[this.cursorLine] || '';
|
|
2015
|
+
const openBracketPos = openLine.search(RE_BRACKET_POS);
|
|
2016
|
+
this.cursorColumn = openBracketPos >= 0 ? openBracketPos : 0;
|
|
2017
|
+
this._invalidateRenderCache();
|
|
2018
|
+
this._scrollToCursor();
|
|
2019
|
+
this.scheduleRender();
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1857
2024
|
if (pos === 0) {
|
|
1858
|
-
// At start of line, move to end of previous line
|
|
2025
|
+
// At start of line, move to end of previous visible line
|
|
1859
2026
|
if (this.cursorLine > 0) {
|
|
1860
|
-
this.cursorLine
|
|
2027
|
+
let prevLine = this.cursorLine - 1;
|
|
2028
|
+
// Skip collapsed zones
|
|
2029
|
+
const collapsed = this._getCollapsedRangeForLine(prevLine);
|
|
2030
|
+
if (collapsed) {
|
|
2031
|
+
prevLine = collapsed.startLine;
|
|
2032
|
+
}
|
|
2033
|
+
this.cursorLine = Math.max(prevLine, 0);
|
|
1861
2034
|
this.cursorColumn = this.lines[this.cursorLine].length;
|
|
1862
2035
|
}
|
|
1863
2036
|
} else if (pos > 0 && isWordChar(line[pos - 1])) {
|
|
@@ -1886,7 +2059,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1886
2059
|
/**
|
|
1887
2060
|
* Handle Home/End with optional selection
|
|
1888
2061
|
*/
|
|
1889
|
-
_handleHomeEnd(key, isShift, onClosingLine) {
|
|
2062
|
+
private _handleHomeEnd(key: string, isShift: boolean, onClosingLine: CollapsedNodeInfo | null): void {
|
|
1890
2063
|
// Start selection if shift is pressed and no selection exists
|
|
1891
2064
|
if (isShift && !this.selectionStart) {
|
|
1892
2065
|
this.selectionStart = { line: this.cursorLine, column: this.cursorColumn };
|
|
@@ -1919,7 +2092,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1919
2092
|
/**
|
|
1920
2093
|
* Select all content
|
|
1921
2094
|
*/
|
|
1922
|
-
_selectAll() {
|
|
2095
|
+
private _selectAll() {
|
|
1923
2096
|
this.selectionStart = { line: 0, column: 0 };
|
|
1924
2097
|
const lastLine = this.lines.length - 1;
|
|
1925
2098
|
this.selectionEnd = { line: lastLine, column: this.lines[lastLine]?.length || 0 };
|
|
@@ -1934,11 +2107,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1934
2107
|
/**
|
|
1935
2108
|
* Get selected text
|
|
1936
2109
|
*/
|
|
1937
|
-
_getSelectedText() {
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
const { start, end } =
|
|
1941
|
-
if (!start || !end) return '';
|
|
2110
|
+
private _getSelectedText(): string {
|
|
2111
|
+
const sel = this._normalizeSelection();
|
|
2112
|
+
if (!sel) return '';
|
|
2113
|
+
const { start, end } = sel;
|
|
1942
2114
|
|
|
1943
2115
|
if (start.line === end.line) {
|
|
1944
2116
|
return this.lines[start.line].substring(start.column, end.column);
|
|
@@ -1956,14 +2128,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1956
2128
|
/**
|
|
1957
2129
|
* Normalize selection so start is before end
|
|
1958
2130
|
*/
|
|
1959
|
-
_normalizeSelection() {
|
|
2131
|
+
private _normalizeSelection(): { start: CursorPosition; end: CursorPosition } | null {
|
|
1960
2132
|
if (!this.selectionStart || !this.selectionEnd) {
|
|
1961
|
-
return
|
|
2133
|
+
return null;
|
|
1962
2134
|
}
|
|
1963
|
-
|
|
2135
|
+
|
|
1964
2136
|
const s = this.selectionStart;
|
|
1965
2137
|
const e = this.selectionEnd;
|
|
1966
|
-
|
|
2138
|
+
|
|
1967
2139
|
if (s.line < e.line || (s.line === e.line && s.column <= e.column)) {
|
|
1968
2140
|
return { start: s, end: e };
|
|
1969
2141
|
} else {
|
|
@@ -1974,7 +2146,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1974
2146
|
/**
|
|
1975
2147
|
* Check if there is an active selection
|
|
1976
2148
|
*/
|
|
1977
|
-
_hasSelection() {
|
|
2149
|
+
private _hasSelection() {
|
|
1978
2150
|
if (!this.selectionStart || !this.selectionEnd) return false;
|
|
1979
2151
|
return this.selectionStart.line !== this.selectionEnd.line ||
|
|
1980
2152
|
this.selectionStart.column !== this.selectionEnd.column;
|
|
@@ -1983,7 +2155,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1983
2155
|
/**
|
|
1984
2156
|
* Clear the current selection
|
|
1985
2157
|
*/
|
|
1986
|
-
_clearSelection() {
|
|
2158
|
+
private _clearSelection() {
|
|
1987
2159
|
this.selectionStart = null;
|
|
1988
2160
|
this.selectionEnd = null;
|
|
1989
2161
|
}
|
|
@@ -1991,13 +2163,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1991
2163
|
/**
|
|
1992
2164
|
* Delete selected text
|
|
1993
2165
|
*/
|
|
1994
|
-
_deleteSelection() {
|
|
1995
|
-
|
|
2166
|
+
private _deleteSelection(): boolean {
|
|
2167
|
+
const sel = this._normalizeSelection();
|
|
2168
|
+
if (!sel) return false;
|
|
2169
|
+
const { start, end } = sel;
|
|
1996
2170
|
|
|
1997
2171
|
this._saveToHistory('delete');
|
|
1998
2172
|
|
|
1999
|
-
const { start, end } = this._normalizeSelection();
|
|
2000
|
-
|
|
2001
2173
|
if (start.line === end.line) {
|
|
2002
2174
|
// Single line selection
|
|
2003
2175
|
const line = this.lines[start.line];
|
|
@@ -2018,7 +2190,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2018
2190
|
return true;
|
|
2019
2191
|
}
|
|
2020
2192
|
|
|
2021
|
-
insertText(text) {
|
|
2193
|
+
insertText(text: string): void {
|
|
2022
2194
|
// Delete selection first if any
|
|
2023
2195
|
if (this._hasSelection()) {
|
|
2024
2196
|
this._deleteSelection();
|
|
@@ -2039,7 +2211,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2039
2211
|
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
2040
2212
|
if (onCollapsed) {
|
|
2041
2213
|
const line = this.lines[this.cursorLine];
|
|
2042
|
-
const bracketPos = line.search(
|
|
2214
|
+
const bracketPos = line.search(RE_BRACKET_POS);
|
|
2043
2215
|
if (this.cursorColumn > bracketPos) return;
|
|
2044
2216
|
}
|
|
2045
2217
|
|
|
@@ -2061,9 +2233,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2061
2233
|
this.formatAndUpdate();
|
|
2062
2234
|
}
|
|
2063
2235
|
|
|
2064
|
-
handlePaste(e) {
|
|
2236
|
+
handlePaste(e: ClipboardEvent): void {
|
|
2065
2237
|
e.preventDefault();
|
|
2066
|
-
const text = e.clipboardData
|
|
2238
|
+
const text = e.clipboardData?.getData('text/plain');
|
|
2067
2239
|
if (!text) return;
|
|
2068
2240
|
|
|
2069
2241
|
const wasEmpty = this.lines.length === 0;
|
|
@@ -2071,7 +2243,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2071
2243
|
// Try to parse as GeoJSON and normalize
|
|
2072
2244
|
try {
|
|
2073
2245
|
const parsed = JSON.parse(text);
|
|
2074
|
-
const features =
|
|
2246
|
+
const features = normalizeToFeatures(parsed);
|
|
2075
2247
|
// Valid GeoJSON - insert formatted features
|
|
2076
2248
|
const formatted = features.map(f => JSON.stringify(f, null, 2)).join(',\n');
|
|
2077
2249
|
this.insertText(formatted);
|
|
@@ -2085,14 +2257,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2085
2257
|
// Cancel pending render, collapse first, then render once
|
|
2086
2258
|
if (this.renderTimer) {
|
|
2087
2259
|
cancelAnimationFrame(this.renderTimer);
|
|
2088
|
-
this.renderTimer =
|
|
2260
|
+
this.renderTimer = undefined;
|
|
2089
2261
|
}
|
|
2090
2262
|
this.autoCollapseCoordinates();
|
|
2091
2263
|
}
|
|
2092
2264
|
}
|
|
2093
2265
|
|
|
2094
|
-
handleCopy(e) {
|
|
2266
|
+
handleCopy(e: ClipboardEvent): void {
|
|
2095
2267
|
e.preventDefault();
|
|
2268
|
+
if (!e.clipboardData) return;
|
|
2096
2269
|
// Copy selected text if there's a selection, otherwise copy all
|
|
2097
2270
|
if (this._hasSelection()) {
|
|
2098
2271
|
e.clipboardData.setData('text/plain', this._getSelectedText());
|
|
@@ -2101,8 +2274,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2101
2274
|
}
|
|
2102
2275
|
}
|
|
2103
2276
|
|
|
2104
|
-
handleCut(e) {
|
|
2277
|
+
handleCut(e: ClipboardEvent): void {
|
|
2105
2278
|
e.preventDefault();
|
|
2279
|
+
if (!e.clipboardData) return;
|
|
2106
2280
|
if (this._hasSelection()) {
|
|
2107
2281
|
e.clipboardData.setData('text/plain', this._getSelectedText());
|
|
2108
2282
|
this._saveToHistory('cut');
|
|
@@ -2122,9 +2296,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2122
2296
|
/**
|
|
2123
2297
|
* Get line/column position from mouse event
|
|
2124
2298
|
*/
|
|
2125
|
-
_getPositionFromClick(e) {
|
|
2126
|
-
const viewport = this.
|
|
2127
|
-
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 };
|
|
2128
2303
|
const rect = viewport.getBoundingClientRect();
|
|
2129
2304
|
|
|
2130
2305
|
const paddingTop = 8;
|
|
@@ -2165,28 +2340,34 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2165
2340
|
|
|
2166
2341
|
// ========== Gutter Interactions ==========
|
|
2167
2342
|
|
|
2168
|
-
handleGutterClick(e) {
|
|
2343
|
+
handleGutterClick(e: MouseEvent): void {
|
|
2344
|
+
const target = e.target as HTMLElement;
|
|
2345
|
+
if (!target) return;
|
|
2346
|
+
|
|
2169
2347
|
// Visibility button in gutter
|
|
2170
|
-
const visBtn =
|
|
2348
|
+
const visBtn = target.closest('.visibility-button') as HTMLElement | null;
|
|
2171
2349
|
if (visBtn) {
|
|
2172
2350
|
this.toggleFeatureVisibility(visBtn.dataset.featureKey);
|
|
2173
2351
|
return;
|
|
2174
2352
|
}
|
|
2175
|
-
|
|
2353
|
+
|
|
2176
2354
|
// Collapse button in gutter
|
|
2177
|
-
if (
|
|
2178
|
-
const nodeId =
|
|
2179
|
-
this.toggleCollapse(nodeId);
|
|
2355
|
+
if (target.classList.contains('collapse-button')) {
|
|
2356
|
+
const nodeId = target.dataset.nodeId;
|
|
2357
|
+
if (nodeId) this.toggleCollapse(nodeId);
|
|
2180
2358
|
return;
|
|
2181
2359
|
}
|
|
2182
2360
|
}
|
|
2183
2361
|
|
|
2184
|
-
handleEditorClick(e) {
|
|
2362
|
+
handleEditorClick(e: MouseEvent): void {
|
|
2363
|
+
const target = e.target as HTMLElement;
|
|
2364
|
+
if (!target) return;
|
|
2365
|
+
|
|
2185
2366
|
// Unblock render now that click is being processed
|
|
2186
2367
|
this._blockRender = false;
|
|
2187
2368
|
|
|
2188
2369
|
// Line-level visibility button (pseudo-element ::before on .line.has-visibility)
|
|
2189
|
-
const lineEl =
|
|
2370
|
+
const lineEl = target.closest('.line.has-visibility') as HTMLElement | null;
|
|
2190
2371
|
if (lineEl) {
|
|
2191
2372
|
const rect = lineEl.getBoundingClientRect();
|
|
2192
2373
|
const clickX = e.clientX - rect.left;
|
|
@@ -2200,42 +2381,44 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2200
2381
|
return;
|
|
2201
2382
|
}
|
|
2202
2383
|
}
|
|
2203
|
-
|
|
2384
|
+
|
|
2204
2385
|
// Inline color swatch (pseudo-element positioned with left: -8px)
|
|
2205
|
-
if (
|
|
2206
|
-
const rect =
|
|
2386
|
+
if (target.classList.contains('json-color')) {
|
|
2387
|
+
const rect = target.getBoundingClientRect();
|
|
2207
2388
|
const clickX = e.clientX - rect.left;
|
|
2208
2389
|
// Pseudo-element is at left: -8px, so clickX will be negative when clicking on it
|
|
2209
2390
|
if (clickX < 0 && clickX >= -8) {
|
|
2210
2391
|
e.preventDefault();
|
|
2211
2392
|
e.stopPropagation();
|
|
2212
|
-
const color =
|
|
2213
|
-
const targetLineEl =
|
|
2393
|
+
const color = target.dataset.color;
|
|
2394
|
+
const targetLineEl = target.closest('.line') as HTMLElement | null;
|
|
2214
2395
|
if (targetLineEl) {
|
|
2215
|
-
const lineIndex = parseInt(targetLineEl.dataset.lineIndex);
|
|
2396
|
+
const lineIndex = parseInt(targetLineEl.dataset.lineIndex || '0');
|
|
2216
2397
|
const line = this.lines[lineIndex];
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
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]);
|
|
2220
2403
|
}
|
|
2221
2404
|
}
|
|
2222
2405
|
return;
|
|
2223
2406
|
}
|
|
2224
2407
|
}
|
|
2225
|
-
|
|
2408
|
+
|
|
2226
2409
|
// Inline boolean checkbox (pseudo-element positioned with left: -8px)
|
|
2227
|
-
if (
|
|
2228
|
-
const rect =
|
|
2410
|
+
if (target.classList.contains('json-boolean')) {
|
|
2411
|
+
const rect = target.getBoundingClientRect();
|
|
2229
2412
|
const clickX = e.clientX - rect.left;
|
|
2230
2413
|
// Pseudo-element is at left: -8px, so clickX will be negative when clicking on it
|
|
2231
2414
|
if (clickX < 0 && clickX >= -8) {
|
|
2232
2415
|
e.preventDefault();
|
|
2233
2416
|
e.stopPropagation();
|
|
2234
|
-
const targetLineEl =
|
|
2417
|
+
const targetLineEl = target.closest('.line') as HTMLElement | null;
|
|
2235
2418
|
if (targetLineEl) {
|
|
2236
|
-
const lineIndex = parseInt(targetLineEl.dataset.lineIndex);
|
|
2419
|
+
const lineIndex = parseInt(targetLineEl.dataset.lineIndex || '0');
|
|
2237
2420
|
const line = this.lines[lineIndex];
|
|
2238
|
-
const match = line.match(
|
|
2421
|
+
const match = line.match(RE_ATTR_AND_BOOL_VALUE);
|
|
2239
2422
|
if (match) {
|
|
2240
2423
|
const currentValue = match[2] === 'true';
|
|
2241
2424
|
this.updateBooleanValue(lineIndex, !currentValue, match[1]);
|
|
@@ -2248,7 +2431,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2248
2431
|
|
|
2249
2432
|
// ========== Collapse/Expand ==========
|
|
2250
2433
|
|
|
2251
|
-
toggleCollapse(nodeId) {
|
|
2434
|
+
toggleCollapse(nodeId: string): void {
|
|
2252
2435
|
if (this.collapsedNodes.has(nodeId)) {
|
|
2253
2436
|
this.collapsedNodes.delete(nodeId);
|
|
2254
2437
|
} else {
|
|
@@ -2267,10 +2450,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2267
2450
|
|
|
2268
2451
|
/**
|
|
2269
2452
|
* Helper to apply collapsed option from API methods
|
|
2270
|
-
* @param {object} options - Options object with optional collapsed property
|
|
2271
|
-
* @param {array} features - Features array for function mode
|
|
2272
2453
|
*/
|
|
2273
|
-
_applyCollapsedFromOptions(options, features) {
|
|
2454
|
+
private _applyCollapsedFromOptions(options: SetOptions, features: Feature[]): void {
|
|
2274
2455
|
const collapsed = options.collapsed !== undefined ? options.collapsed : ['coordinates'];
|
|
2275
2456
|
if (collapsed && (Array.isArray(collapsed) ? collapsed.length > 0 : true)) {
|
|
2276
2457
|
this._applyCollapsedOption(collapsed, features);
|
|
@@ -2279,10 +2460,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2279
2460
|
|
|
2280
2461
|
/**
|
|
2281
2462
|
* Apply collapsed option to nodes
|
|
2282
|
-
* @param {string[]|function} collapsed - Attributes to collapse or function returning them
|
|
2283
|
-
* @param {array} features - Features array for function mode (optional)
|
|
2284
2463
|
*/
|
|
2285
|
-
_applyCollapsedOption(collapsed, features = null) {
|
|
2464
|
+
private _applyCollapsedOption(collapsed: string[] | ((feature: Feature | null, index: number) => string[]), features: Feature[] | null = null): void {
|
|
2286
2465
|
const ranges = this._findCollapsibleRanges();
|
|
2287
2466
|
|
|
2288
2467
|
// Group ranges by feature (root nodes)
|
|
@@ -2327,7 +2506,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2327
2506
|
|
|
2328
2507
|
// ========== Feature Visibility ==========
|
|
2329
2508
|
|
|
2330
|
-
toggleFeatureVisibility(featureKey) {
|
|
2509
|
+
toggleFeatureVisibility(featureKey: string | undefined): void {
|
|
2510
|
+
if (!featureKey) return;
|
|
2331
2511
|
if (this.hiddenFeatures.has(featureKey)) {
|
|
2332
2512
|
this.hiddenFeatures.delete(featureKey);
|
|
2333
2513
|
} else {
|
|
@@ -2341,17 +2521,17 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2341
2521
|
}
|
|
2342
2522
|
|
|
2343
2523
|
// ========== Color Picker ==========
|
|
2344
|
-
|
|
2345
|
-
showColorPicker(indicator, line, currentColor, attributeName) {
|
|
2524
|
+
|
|
2525
|
+
showColorPicker(indicator: HTMLElement, line: number, currentColor: string, attributeName: string) {
|
|
2346
2526
|
// Remove existing picker and anchor
|
|
2347
2527
|
const existing = document.querySelector('.geojson-color-picker-anchor');
|
|
2348
2528
|
if (existing) {
|
|
2349
2529
|
existing.remove();
|
|
2350
2530
|
}
|
|
2351
|
-
|
|
2531
|
+
|
|
2352
2532
|
// Create an anchor element at the pseudo-element position
|
|
2353
2533
|
// The browser will position the color picker popup relative to this
|
|
2354
|
-
const anchor =
|
|
2534
|
+
const anchor = _ce('div');
|
|
2355
2535
|
anchor.className = 'geojson-color-picker-anchor';
|
|
2356
2536
|
const rect = indicator.getBoundingClientRect();
|
|
2357
2537
|
anchor.style.cssText = `
|
|
@@ -2363,10 +2543,19 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2363
2543
|
z-index: 9998;
|
|
2364
2544
|
`;
|
|
2365
2545
|
document.body.appendChild(anchor);
|
|
2366
|
-
|
|
2367
|
-
const colorInput =
|
|
2546
|
+
|
|
2547
|
+
const colorInput = _ce('input') as HTMLInputElement & { _closeListener?: EventListener };
|
|
2368
2548
|
colorInput.type = 'color';
|
|
2369
|
-
|
|
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;
|
|
2370
2559
|
colorInput.className = 'geojson-color-picker-input';
|
|
2371
2560
|
|
|
2372
2561
|
// Position the color input inside the anchor
|
|
@@ -2383,7 +2572,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2383
2572
|
`;
|
|
2384
2573
|
anchor.appendChild(colorInput);
|
|
2385
2574
|
|
|
2386
|
-
colorInput.addEventListener('input', (e
|
|
2575
|
+
colorInput.addEventListener('input', (e) => {
|
|
2387
2576
|
this.updateColorValue(line, (e.target as HTMLInputElement).value, attributeName);
|
|
2388
2577
|
});
|
|
2389
2578
|
|
|
@@ -2395,7 +2584,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2395
2584
|
};
|
|
2396
2585
|
|
|
2397
2586
|
colorInput._closeListener = closeOnClickOutside;
|
|
2398
|
-
|
|
2587
|
+
|
|
2399
2588
|
setTimeout(() => {
|
|
2400
2589
|
document.addEventListener('click', closeOnClickOutside, true);
|
|
2401
2590
|
}, 100);
|
|
@@ -2404,17 +2593,18 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2404
2593
|
colorInput.click();
|
|
2405
2594
|
}
|
|
2406
2595
|
|
|
2407
|
-
updateColorValue(line, newColor, attributeName) {
|
|
2408
|
-
|
|
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]+)"`);
|
|
2409
2599
|
this.lines[line] = this.lines[line].replace(regex, `"${attributeName}": "${newColor}"`);
|
|
2410
|
-
|
|
2600
|
+
|
|
2411
2601
|
// Use updateView to preserve collapsed state (line count didn't change)
|
|
2412
2602
|
this.updateView();
|
|
2413
2603
|
this.scheduleRender();
|
|
2414
2604
|
this.emitChange();
|
|
2415
2605
|
}
|
|
2416
2606
|
|
|
2417
|
-
updateBooleanValue(line, newValue, attributeName) {
|
|
2607
|
+
updateBooleanValue(line: number, newValue: boolean, attributeName: string): void {
|
|
2418
2608
|
const regex = new RegExp(`"${attributeName}"\\s*:\\s*(true|false)`);
|
|
2419
2609
|
this.lines[line] = this.lines[line].replace(regex, `"${attributeName}": ${newValue}`);
|
|
2420
2610
|
|
|
@@ -2456,14 +2646,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2456
2646
|
|
|
2457
2647
|
// Filter hidden features
|
|
2458
2648
|
if (this.hiddenFeatures.size > 0) {
|
|
2459
|
-
parsed.features = parsed.features.filter((feature) => {
|
|
2460
|
-
const key =
|
|
2461
|
-
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;
|
|
2462
2652
|
});
|
|
2463
2653
|
}
|
|
2464
2654
|
|
|
2465
2655
|
// Validate
|
|
2466
|
-
const errors =
|
|
2656
|
+
const errors = validateGeoJSON(parsed);
|
|
2467
2657
|
|
|
2468
2658
|
if (errors.length > 0) {
|
|
2469
2659
|
this.dispatchEvent(new CustomEvent('error', {
|
|
@@ -2480,7 +2670,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2480
2670
|
}
|
|
2481
2671
|
} catch (e) {
|
|
2482
2672
|
this.dispatchEvent(new CustomEvent('error', {
|
|
2483
|
-
detail: { error: e.message, content },
|
|
2673
|
+
detail: { error: e instanceof Error ? e.message : 'Unknown error', content },
|
|
2484
2674
|
bubbles: true,
|
|
2485
2675
|
composed: true
|
|
2486
2676
|
}));
|
|
@@ -2488,56 +2678,48 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2488
2678
|
}
|
|
2489
2679
|
|
|
2490
2680
|
// ========== UI Updates ==========
|
|
2491
|
-
|
|
2492
|
-
updateReadonly() {
|
|
2493
|
-
const textarea = this.shadowRoot.getElementById('hiddenTextarea');
|
|
2494
|
-
const clearBtn = this.shadowRoot!.getElementById('clearBtn') as HTMLButtonElement;
|
|
2495
2681
|
|
|
2682
|
+
updateReadonly() {
|
|
2496
2683
|
// Use readOnly instead of disabled to allow text selection for copying
|
|
2497
|
-
if (
|
|
2498
|
-
if (
|
|
2684
|
+
if (this._hiddenTextarea) this._hiddenTextarea.readOnly = this.readonly;
|
|
2685
|
+
if (this._clearBtn) this._clearBtn.hidden = this.readonly;
|
|
2499
2686
|
}
|
|
2500
2687
|
|
|
2501
2688
|
updatePlaceholderVisibility() {
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
placeholder.style.display = this.lines.length > 0 ? 'none' : 'block';
|
|
2689
|
+
if (this._placeholderLayer) {
|
|
2690
|
+
this._placeholderLayer.style.display = this.lines.length > 0 ? 'none' : 'block';
|
|
2505
2691
|
}
|
|
2506
2692
|
}
|
|
2507
2693
|
|
|
2508
2694
|
updatePlaceholderContent() {
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
placeholder.textContent = this.placeholder;
|
|
2695
|
+
if (this._placeholderLayer) {
|
|
2696
|
+
this._placeholderLayer.textContent = this.placeholder;
|
|
2512
2697
|
}
|
|
2513
2698
|
this.updatePlaceholderVisibility();
|
|
2514
2699
|
}
|
|
2515
2700
|
|
|
2516
2701
|
updatePrefixSuffix() {
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
if (prefix) prefix.textContent = this.prefix;
|
|
2521
|
-
if (suffix) suffix.textContent = this.suffix;
|
|
2702
|
+
if (this._editorPrefix) this._editorPrefix.textContent = this.prefix;
|
|
2703
|
+
if (this._editorSuffix) this._editorSuffix.textContent = this.suffix;
|
|
2522
2704
|
}
|
|
2523
2705
|
|
|
2524
2706
|
// ========== Theme ==========
|
|
2525
2707
|
|
|
2526
2708
|
updateThemeCSS() {
|
|
2527
2709
|
const darkSelector = this.getAttribute('dark-selector') || '.dark';
|
|
2528
|
-
const darkRule =
|
|
2529
|
-
|
|
2530
|
-
let themeStyle = this.
|
|
2710
|
+
const darkRule = parseSelectorToHostRule(darkSelector);
|
|
2711
|
+
|
|
2712
|
+
let themeStyle = this._id('theme-styles') as HTMLStyleElement;
|
|
2531
2713
|
if (!themeStyle) {
|
|
2532
|
-
themeStyle =
|
|
2714
|
+
themeStyle = _ce('style') as HTMLStyleElement;
|
|
2533
2715
|
themeStyle.id = 'theme-styles';
|
|
2534
|
-
this.shadowRoot
|
|
2716
|
+
this.shadowRoot!.insertBefore(themeStyle, this.shadowRoot!.firstChild);
|
|
2535
2717
|
}
|
|
2536
2718
|
|
|
2537
2719
|
const darkDefaults = {
|
|
2538
2720
|
bgColor: '#2b2b2b',
|
|
2539
2721
|
textColor: '#a9b7c6',
|
|
2540
|
-
caretColor: '#
|
|
2722
|
+
caretColor: '#bbb',
|
|
2541
2723
|
gutterBg: '#313335',
|
|
2542
2724
|
gutterBorder: '#3c3f41',
|
|
2543
2725
|
gutterText: '#606366',
|
|
@@ -2557,14 +2739,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2557
2739
|
jsonKeyInvalid: '#ff6b68'
|
|
2558
2740
|
};
|
|
2559
2741
|
|
|
2560
|
-
|
|
2561
|
-
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)
|
|
2562
2746
|
.map(([k, v]) => `--${toKebab(k)}: ${v};`)
|
|
2563
2747
|
.join('\n ');
|
|
2564
2748
|
|
|
2565
|
-
const lightVars = generateVars(this.themes.light || {});
|
|
2749
|
+
const lightVars = generateVars(this.themes.light as Record<string, string | undefined> || {});
|
|
2566
2750
|
const darkTheme = { ...darkDefaults, ...this.themes.dark };
|
|
2567
|
-
const darkVars = generateVars(darkTheme);
|
|
2751
|
+
const darkVars = generateVars(darkTheme as Record<string, string | undefined>);
|
|
2568
2752
|
|
|
2569
2753
|
let css = lightVars ? `:host {\n ${lightVars}\n }\n` : '';
|
|
2570
2754
|
css += `${darkRule} {\n ${darkVars}\n }`;
|
|
@@ -2572,14 +2756,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2572
2756
|
themeStyle.textContent = css;
|
|
2573
2757
|
}
|
|
2574
2758
|
|
|
2575
|
-
_parseSelectorToHostRule(selector) {
|
|
2576
|
-
if (!selector) return ':host([data-color-scheme="dark"])';
|
|
2577
|
-
if (selector.startsWith('.') && !selector.includes(' ')) {
|
|
2578
|
-
return `:host(${selector})`;
|
|
2579
|
-
}
|
|
2580
|
-
return `:host-context(${selector})`;
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
2759
|
setTheme(theme: ThemeSettings): void {
|
|
2584
2760
|
if (theme.dark) this.themes.dark = { ...this.themes.dark, ...theme.dark };
|
|
2585
2761
|
if (theme.light) this.themes.light = { ...this.themes.light, ...theme.light };
|
|
@@ -2591,45 +2767,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2591
2767
|
this.updateThemeCSS();
|
|
2592
2768
|
}
|
|
2593
2769
|
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
_getFeatureKey(feature) {
|
|
2597
|
-
if (!feature) return null;
|
|
2598
|
-
if (feature.id !== undefined) return `id:${feature.id}`;
|
|
2599
|
-
if (feature.properties?.id !== undefined) return `prop:${feature.properties.id}`;
|
|
2600
|
-
|
|
2601
|
-
const geomType = feature.geometry?.type || 'null';
|
|
2602
|
-
const coords = JSON.stringify(feature.geometry?.coordinates || []);
|
|
2603
|
-
let hash = 0;
|
|
2604
|
-
for (let i = 0; i < coords.length; i++) {
|
|
2605
|
-
hash = ((hash << 5) - hash) + coords.charCodeAt(i);
|
|
2606
|
-
hash = hash & hash;
|
|
2607
|
-
}
|
|
2608
|
-
return `hash:${geomType}:${hash.toString(36)}`;
|
|
2609
|
-
}
|
|
2610
|
-
|
|
2611
|
-
_countBrackets(line, openBracket) {
|
|
2612
|
-
const closeBracket = openBracket === '{' ? '}' : ']';
|
|
2613
|
-
let open = 0, close = 0, inString = false, escape = false;
|
|
2614
|
-
|
|
2615
|
-
for (const char of line) {
|
|
2616
|
-
if (escape) { escape = false; continue; }
|
|
2617
|
-
if (char === '\\' && inString) { escape = true; continue; }
|
|
2618
|
-
if (char === '"') { inString = !inString; continue; }
|
|
2619
|
-
if (!inString) {
|
|
2620
|
-
if (char === openBracket) open++;
|
|
2621
|
-
if (char === closeBracket) close++;
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
return { open, close };
|
|
2770
|
+
getTheme(): ThemeSettings {
|
|
2771
|
+
return { ...this.themes };
|
|
2626
2772
|
}
|
|
2627
2773
|
|
|
2628
2774
|
/**
|
|
2629
2775
|
* Find all collapsible ranges using the mappings built by _rebuildNodeIdMappings
|
|
2630
2776
|
* This method only READS the existing mappings, it doesn't create new IDs
|
|
2631
2777
|
*/
|
|
2632
|
-
_findCollapsibleRanges() {
|
|
2778
|
+
private _findCollapsibleRanges() {
|
|
2633
2779
|
const ranges = [];
|
|
2634
2780
|
|
|
2635
2781
|
// Simply iterate through the existing mappings
|
|
@@ -2641,13 +2787,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2641
2787
|
if (!line) continue;
|
|
2642
2788
|
|
|
2643
2789
|
// Match "key": { or "key": [
|
|
2644
|
-
const kvMatch = line.match(
|
|
2790
|
+
const kvMatch = line.match(RE_KV_MATCH);
|
|
2645
2791
|
// Also match standalone { or [ (root Feature objects)
|
|
2646
|
-
const rootMatch = !kvMatch && line.match(
|
|
2792
|
+
const rootMatch = !kvMatch && line.match(RE_ROOT_MATCH);
|
|
2647
2793
|
|
|
2648
2794
|
if (!kvMatch && !rootMatch) continue;
|
|
2649
|
-
|
|
2650
|
-
const openBracket = kvMatch ? kvMatch[2] : rootMatch[1];
|
|
2795
|
+
|
|
2796
|
+
const openBracket = kvMatch ? kvMatch[2] : (rootMatch ? rootMatch[1] : '{');
|
|
2651
2797
|
|
|
2652
2798
|
ranges.push({
|
|
2653
2799
|
startLine: rangeInfo.startLine,
|
|
@@ -2665,20 +2811,20 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2665
2811
|
return ranges;
|
|
2666
2812
|
}
|
|
2667
2813
|
|
|
2668
|
-
_findClosingLine(startLine, openBracket) {
|
|
2814
|
+
private _findClosingLine(startLine: number, openBracket: string): number {
|
|
2669
2815
|
let depth = 1;
|
|
2670
2816
|
const line = this.lines[startLine];
|
|
2671
2817
|
const bracketPos = line.indexOf(openBracket);
|
|
2672
2818
|
|
|
2673
2819
|
if (bracketPos !== -1) {
|
|
2674
2820
|
const rest = line.substring(bracketPos + 1);
|
|
2675
|
-
const counts =
|
|
2821
|
+
const counts = countBrackets(rest, openBracket);
|
|
2676
2822
|
depth += counts.open - counts.close;
|
|
2677
2823
|
if (depth === 0) return startLine;
|
|
2678
2824
|
}
|
|
2679
2825
|
|
|
2680
2826
|
for (let i = startLine + 1; i < this.lines.length; i++) {
|
|
2681
|
-
const counts =
|
|
2827
|
+
const counts = countBrackets(this.lines[i], openBracket);
|
|
2682
2828
|
depth += counts.open - counts.close;
|
|
2683
2829
|
if (depth === 0) return i;
|
|
2684
2830
|
}
|
|
@@ -2686,7 +2832,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2686
2832
|
return -1;
|
|
2687
2833
|
}
|
|
2688
2834
|
|
|
2689
|
-
_buildContextMap() {
|
|
2835
|
+
private _buildContextMap() {
|
|
2690
2836
|
// Memoization: return cached result if content hasn't changed
|
|
2691
2837
|
const linesLength = this.lines.length;
|
|
2692
2838
|
if (this._contextMapCache &&
|
|
@@ -2696,9 +2842,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2696
2842
|
return this._contextMapCache;
|
|
2697
2843
|
}
|
|
2698
2844
|
|
|
2699
|
-
const contextMap = new Map();
|
|
2700
|
-
const contextStack = [];
|
|
2701
|
-
let pendingContext = null;
|
|
2845
|
+
const contextMap = new Map<number, string>();
|
|
2846
|
+
const contextStack: { context: string; isArray: boolean }[] = [];
|
|
2847
|
+
let pendingContext: string | null = null;
|
|
2702
2848
|
|
|
2703
2849
|
for (let i = 0; i < linesLength; i++) {
|
|
2704
2850
|
const line = this.lines[i];
|
|
@@ -2711,10 +2857,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2711
2857
|
else if (RE_CONTEXT_FEATURES.test(line)) pendingContext = 'Feature';
|
|
2712
2858
|
|
|
2713
2859
|
// Track brackets
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
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;
|
|
2718
2868
|
|
|
2719
2869
|
for (let j = 0; j < openBraces + openBrackets; j++) {
|
|
2720
2870
|
contextStack.push({ context: pendingContext || currentContext, isArray: j >= openBraces });
|
|
@@ -2735,214 +2885,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2735
2885
|
return contextMap;
|
|
2736
2886
|
}
|
|
2737
2887
|
|
|
2738
|
-
|
|
2739
|
-
if (!text) return '';
|
|
2740
|
-
|
|
2741
|
-
// For collapsed nodes, truncate the text at the opening bracket
|
|
2742
|
-
let displayText = text;
|
|
2743
|
-
let collapsedBracket = null;
|
|
2744
|
-
|
|
2745
|
-
if (meta?.collapseButton?.isCollapsed) {
|
|
2746
|
-
// Match "key": { or "key": [
|
|
2747
|
-
const bracketMatch = text.match(RE_COLLAPSED_BRACKET);
|
|
2748
|
-
// Also match standalone { or [ (root Feature objects)
|
|
2749
|
-
const rootMatch = !bracketMatch && text.match(RE_COLLAPSED_ROOT);
|
|
2750
|
-
|
|
2751
|
-
if (bracketMatch) {
|
|
2752
|
-
displayText = bracketMatch[1] + bracketMatch[2];
|
|
2753
|
-
collapsedBracket = bracketMatch[2];
|
|
2754
|
-
} else if (rootMatch) {
|
|
2755
|
-
displayText = rootMatch[1] + rootMatch[2];
|
|
2756
|
-
collapsedBracket = rootMatch[2];
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
// Escape HTML first
|
|
2761
|
-
let result = displayText
|
|
2762
|
-
.replace(RE_ESCAPE_AMP, '&')
|
|
2763
|
-
.replace(RE_ESCAPE_LT, '<')
|
|
2764
|
-
.replace(RE_ESCAPE_GT, '>');
|
|
2765
|
-
|
|
2766
|
-
// Punctuation FIRST (before other replacements can interfere)
|
|
2767
|
-
result = result.replace(RE_PUNCTUATION, '<span class="json-punctuation">$1</span>');
|
|
2768
|
-
|
|
2769
|
-
// JSON keys - match "key" followed by :
|
|
2770
|
-
// In properties context, all keys are treated as regular JSON keys
|
|
2771
|
-
RE_JSON_KEYS.lastIndex = 0;
|
|
2772
|
-
result = result.replace(RE_JSON_KEYS, (match, key, colon) => {
|
|
2773
|
-
if (context !== 'properties' && GEOJSON_KEYS.includes(key)) {
|
|
2774
|
-
return `<span class="geojson-key">"${key}"</span>${colon}`;
|
|
2775
|
-
}
|
|
2776
|
-
return `<span class="json-key">"${key}"</span>${colon}`;
|
|
2777
|
-
});
|
|
2778
|
-
|
|
2779
|
-
// Type values - "type": "Value" - but NOT inside properties context
|
|
2780
|
-
if (context !== 'properties') {
|
|
2781
|
-
RE_TYPE_VALUES.lastIndex = 0;
|
|
2782
|
-
result = result.replace(RE_TYPE_VALUES, (match, space, type) => {
|
|
2783
|
-
const isValid = type === 'Feature' || type === 'FeatureCollection' || GEOMETRY_TYPES.includes(type);
|
|
2784
|
-
const cls = isValid ? 'geojson-type' : 'geojson-type-invalid';
|
|
2785
|
-
return `<span class="geojson-key">"type"</span><span class="json-punctuation">:</span>${space}<span class="${cls}">"${type}"</span>`;
|
|
2786
|
-
});
|
|
2787
|
-
}
|
|
2788
|
-
|
|
2789
|
-
// String values (not already wrapped in spans)
|
|
2790
|
-
RE_STRING_VALUES.lastIndex = 0;
|
|
2791
|
-
result = result.replace(RE_STRING_VALUES, (match, colon, space, val) => {
|
|
2792
|
-
if (match.includes('geojson-type') || match.includes('json-string')) return match;
|
|
2793
|
-
if (RE_COLOR_HEX.test(val)) {
|
|
2794
|
-
return `${colon}${space}<span class="json-string json-color" data-color="${val}" style="--swatch-color: ${val}">"${val}"</span>`;
|
|
2795
|
-
}
|
|
2796
|
-
return `${colon}${space}<span class="json-string">"${val}"</span>`;
|
|
2797
|
-
});
|
|
2798
|
-
|
|
2799
|
-
// Numbers after colon
|
|
2800
|
-
RE_NUMBERS_COLON.lastIndex = 0;
|
|
2801
|
-
result = result.replace(RE_NUMBERS_COLON, '$1$2<span class="json-number">$3</span>');
|
|
2802
|
-
|
|
2803
|
-
// Numbers in arrays (after [ or ,)
|
|
2804
|
-
RE_NUMBERS_ARRAY.lastIndex = 0;
|
|
2805
|
-
result = result.replace(RE_NUMBERS_ARRAY, '$1$2<span class="json-number">$3</span>');
|
|
2806
|
-
|
|
2807
|
-
// Standalone numbers at start of line (coordinates arrays)
|
|
2808
|
-
RE_NUMBERS_START.lastIndex = 0;
|
|
2809
|
-
result = result.replace(RE_NUMBERS_START, '$1<span class="json-number">$2</span>');
|
|
2810
|
-
|
|
2811
|
-
// Booleans - use ::before for checkbox via CSS class
|
|
2812
|
-
RE_BOOLEANS.lastIndex = 0;
|
|
2813
|
-
result = result.replace(RE_BOOLEANS, (match, colon, space, val) => {
|
|
2814
|
-
const checkedClass = val === 'true' ? ' json-bool-true' : ' json-bool-false';
|
|
2815
|
-
return `${colon}${space}<span class="json-boolean${checkedClass}">${val}</span>`;
|
|
2816
|
-
});
|
|
2817
|
-
|
|
2818
|
-
// Null
|
|
2819
|
-
RE_NULL.lastIndex = 0;
|
|
2820
|
-
result = result.replace(RE_NULL, '$1$2<span class="json-null">$3</span>');
|
|
2821
|
-
|
|
2822
|
-
// Collapsed bracket indicator
|
|
2823
|
-
if (collapsedBracket) {
|
|
2824
|
-
const bracketClass = collapsedBracket === '[' ? 'collapsed-bracket-array' : 'collapsed-bracket-object';
|
|
2825
|
-
result = result.replace(
|
|
2826
|
-
new RegExp(`<span class="json-punctuation">\\${collapsedBracket}<\\/span>$`),
|
|
2827
|
-
`<span class="${bracketClass}">${collapsedBracket}</span>`
|
|
2828
|
-
);
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
// Mark unrecognized text as error
|
|
2832
|
-
RE_UNRECOGNIZED.lastIndex = 0;
|
|
2833
|
-
result = result.replace(RE_UNRECOGNIZED, (match, before, text, after) => {
|
|
2834
|
-
if (!text || RE_WHITESPACE_ONLY.test(text)) return match;
|
|
2835
|
-
// Check for unrecognized words/tokens (not whitespace, not just spaces/commas)
|
|
2836
|
-
// Keep whitespace as-is, wrap any non-whitespace unrecognized token
|
|
2837
|
-
const parts = text.split(RE_WHITESPACE_SPLIT);
|
|
2838
|
-
let hasError = false;
|
|
2839
|
-
const processed = parts.map(part => {
|
|
2840
|
-
// If it's whitespace, keep it
|
|
2841
|
-
if (RE_WHITESPACE_ONLY.test(part)) return part;
|
|
2842
|
-
// Mark as error
|
|
2843
|
-
hasError = true;
|
|
2844
|
-
return `<span class="json-error">${part}</span>`;
|
|
2845
|
-
}).join('');
|
|
2846
|
-
return hasError ? before + processed + after : match;
|
|
2847
|
-
});
|
|
2848
|
-
|
|
2849
|
-
// Note: visibility is now handled at line level (has-visibility class on .line element)
|
|
2850
|
-
|
|
2851
|
-
return result;
|
|
2852
|
-
}
|
|
2853
|
-
|
|
2854
|
-
_validateGeoJSON(parsed) {
|
|
2855
|
-
const errors = [];
|
|
2856
|
-
|
|
2857
|
-
if (!parsed.features) return errors;
|
|
2858
|
-
|
|
2859
|
-
parsed.features.forEach((feature, i) => {
|
|
2860
|
-
if (feature.type !== 'Feature') {
|
|
2861
|
-
errors.push(`features[${i}]: type must be "Feature"`);
|
|
2862
|
-
}
|
|
2863
|
-
if (feature.geometry && feature.geometry.type) {
|
|
2864
|
-
if (!GEOMETRY_TYPES.includes(feature.geometry.type)) {
|
|
2865
|
-
errors.push(`features[${i}].geometry: invalid type "${feature.geometry.type}"`);
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
});
|
|
2869
|
-
|
|
2870
|
-
return errors;
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
/**
|
|
2874
|
-
* Validate a single feature object
|
|
2875
|
-
* @param {object} feature - The feature to validate
|
|
2876
|
-
* @throws {Error} If the feature is invalid
|
|
2877
|
-
*/
|
|
2878
|
-
_validateFeature(feature) {
|
|
2879
|
-
if (!feature || typeof feature !== 'object') {
|
|
2880
|
-
throw new Error('Feature must be an object');
|
|
2881
|
-
}
|
|
2882
|
-
if (feature.type !== 'Feature') {
|
|
2883
|
-
throw new Error('Feature type must be "Feature"');
|
|
2884
|
-
}
|
|
2885
|
-
if (!('geometry' in feature)) {
|
|
2886
|
-
throw new Error('Feature must have a geometry property');
|
|
2887
|
-
}
|
|
2888
|
-
if (!('properties' in feature)) {
|
|
2889
|
-
throw new Error('Feature must have a properties property');
|
|
2890
|
-
}
|
|
2891
|
-
if (feature.geometry !== null) {
|
|
2892
|
-
if (!feature.geometry || typeof feature.geometry !== 'object') {
|
|
2893
|
-
throw new Error('Feature geometry must be an object or null');
|
|
2894
|
-
}
|
|
2895
|
-
if (!feature.geometry.type) {
|
|
2896
|
-
throw new Error('Feature geometry must have a type');
|
|
2897
|
-
}
|
|
2898
|
-
if (!GEOMETRY_TYPES.includes(feature.geometry.type)) {
|
|
2899
|
-
throw new Error(`Invalid geometry type: "${feature.geometry.type}"`);
|
|
2900
|
-
}
|
|
2901
|
-
if (!('coordinates' in feature.geometry)) {
|
|
2902
|
-
throw new Error('Feature geometry must have coordinates');
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
if (feature.properties !== null && typeof feature.properties !== 'object') {
|
|
2906
|
-
throw new Error('Feature properties must be an object or null');
|
|
2907
|
-
}
|
|
2908
|
-
}
|
|
2909
|
-
|
|
2910
|
-
/**
|
|
2911
|
-
* Normalize input to an array of features
|
|
2912
|
-
* Accepts: FeatureCollection, Feature[], or single Feature
|
|
2913
|
-
* @param {object|array} input - Input to normalize
|
|
2914
|
-
* @returns {array} Array of features
|
|
2915
|
-
* @throws {Error} If input is invalid
|
|
2916
|
-
*/
|
|
2917
|
-
_normalizeToFeatures(input) {
|
|
2918
|
-
let features = [];
|
|
2919
|
-
|
|
2920
|
-
if (Array.isArray(input)) {
|
|
2921
|
-
// Array of features
|
|
2922
|
-
features = input;
|
|
2923
|
-
} else if (input && typeof input === 'object') {
|
|
2924
|
-
if (input.type === 'FeatureCollection' && Array.isArray(input.features)) {
|
|
2925
|
-
// FeatureCollection
|
|
2926
|
-
features = input.features;
|
|
2927
|
-
} else if (input.type === 'Feature') {
|
|
2928
|
-
// Single Feature
|
|
2929
|
-
features = [input];
|
|
2930
|
-
} else {
|
|
2931
|
-
throw new Error('Input must be a Feature, array of Features, or FeatureCollection');
|
|
2932
|
-
}
|
|
2933
|
-
} else {
|
|
2934
|
-
throw new Error('Input must be a Feature, array of Features, or FeatureCollection');
|
|
2935
|
-
}
|
|
2936
|
-
|
|
2937
|
-
// Validate each feature
|
|
2938
|
-
for (const feature of features) {
|
|
2939
|
-
this._validateFeature(feature);
|
|
2940
|
-
}
|
|
2941
|
-
|
|
2942
|
-
return features;
|
|
2943
|
-
}
|
|
2944
|
-
|
|
2945
|
-
// ========== Public API ==========
|
|
2888
|
+
// ========== Public API ==========
|
|
2946
2889
|
|
|
2947
2890
|
/**
|
|
2948
2891
|
* Replace all features in the editor
|
|
@@ -2956,10 +2899,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2956
2899
|
* @throws {Error} If input is invalid
|
|
2957
2900
|
*/
|
|
2958
2901
|
set(input: FeatureInput, options: SetOptions = {}): void {
|
|
2959
|
-
const features =
|
|
2960
|
-
|
|
2961
|
-
this.setValue(formatted, false); // Don't auto-collapse coordinates
|
|
2962
|
-
this._applyCollapsedFromOptions(options, features);
|
|
2902
|
+
const features = normalizeToFeatures(input);
|
|
2903
|
+
this._setFeaturesInternal(features, options);
|
|
2963
2904
|
}
|
|
2964
2905
|
|
|
2965
2906
|
/**
|
|
@@ -2971,12 +2912,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2971
2912
|
* @throws {Error} If input is invalid
|
|
2972
2913
|
*/
|
|
2973
2914
|
add(input: FeatureInput, options: SetOptions = {}): void {
|
|
2974
|
-
const newFeatures =
|
|
2975
|
-
const
|
|
2976
|
-
|
|
2977
|
-
const formatted = allFeatures.map(f => JSON.stringify(f, null, 2)).join(',\n');
|
|
2978
|
-
this.setValue(formatted, false); // Don't auto-collapse coordinates
|
|
2979
|
-
this._applyCollapsedFromOptions(options, allFeatures);
|
|
2915
|
+
const newFeatures = normalizeToFeatures(input);
|
|
2916
|
+
const allFeatures = [...this._parseFeatures(), ...newFeatures];
|
|
2917
|
+
this._setFeaturesInternal(allFeatures, options);
|
|
2980
2918
|
}
|
|
2981
2919
|
|
|
2982
2920
|
/**
|
|
@@ -2989,12 +2927,19 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2989
2927
|
* @throws {Error} If input is invalid
|
|
2990
2928
|
*/
|
|
2991
2929
|
insertAt(input: FeatureInput, index: number, options: SetOptions = {}): void {
|
|
2992
|
-
const newFeatures =
|
|
2930
|
+
const newFeatures = normalizeToFeatures(input);
|
|
2993
2931
|
const features = this._parseFeatures();
|
|
2994
2932
|
const idx = index < 0 ? features.length + index : index;
|
|
2995
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 {
|
|
2996
2941
|
const formatted = features.map(f => JSON.stringify(f, null, 2)).join(',\n');
|
|
2997
|
-
this.setValue(formatted, false);
|
|
2942
|
+
this.setValue(formatted, false);
|
|
2998
2943
|
this._applyCollapsedFromOptions(options, features);
|
|
2999
2944
|
}
|
|
3000
2945
|
|
|
@@ -3052,7 +2997,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3052
2997
|
const blob = new Blob([json], { type: 'application/geo+json' });
|
|
3053
2998
|
const url = URL.createObjectURL(blob);
|
|
3054
2999
|
|
|
3055
|
-
const a =
|
|
3000
|
+
const a = _ce('a') as HTMLAnchorElement;
|
|
3056
3001
|
a.href = url;
|
|
3057
3002
|
a.download = filename;
|
|
3058
3003
|
document.body.appendChild(a);
|
|
@@ -3075,12 +3020,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3075
3020
|
*/
|
|
3076
3021
|
open(options: SetOptions = {}): Promise<boolean> {
|
|
3077
3022
|
return new Promise((resolve) => {
|
|
3078
|
-
const input =
|
|
3023
|
+
const input = _ce('input') as HTMLInputElement;
|
|
3079
3024
|
input.type = 'file';
|
|
3080
3025
|
input.accept = '.geojson,.json,application/geo+json,application/json';
|
|
3081
3026
|
input.style.display = 'none';
|
|
3082
3027
|
|
|
3083
|
-
input.addEventListener('change', (e
|
|
3028
|
+
input.addEventListener('change', (e) => {
|
|
3084
3029
|
const file = (e.target as HTMLInputElement).files?.[0];
|
|
3085
3030
|
if (!file) {
|
|
3086
3031
|
document.body.removeChild(input);
|
|
@@ -3095,7 +3040,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3095
3040
|
const parsed = JSON.parse(content);
|
|
3096
3041
|
|
|
3097
3042
|
// Normalize and validate features
|
|
3098
|
-
const features =
|
|
3043
|
+
const features = normalizeToFeatures(parsed);
|
|
3099
3044
|
|
|
3100
3045
|
// Load features into editor
|
|
3101
3046
|
this._saveToHistory('open');
|
|
@@ -3128,7 +3073,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3128
3073
|
});
|
|
3129
3074
|
}
|
|
3130
3075
|
|
|
3131
|
-
_parseFeatures() {
|
|
3076
|
+
private _parseFeatures() {
|
|
3132
3077
|
try {
|
|
3133
3078
|
const content = this.lines.join('\n');
|
|
3134
3079
|
if (!content.trim()) return [];
|