@softwarity/geojson-editor 1.0.14 → 1.0.15
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 +98 -2
- package/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.css +1 -69
- package/src/geojson-editor.js +633 -361
package/src/geojson-editor.js
CHANGED
|
@@ -8,6 +8,29 @@ const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'dev';
|
|
|
8
8
|
const GEOJSON_KEYS = ['type', 'geometry', 'properties', 'coordinates', 'id', 'features'];
|
|
9
9
|
const GEOMETRY_TYPES = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon'];
|
|
10
10
|
|
|
11
|
+
// Pre-compiled regex patterns for performance (avoid re-creation on each call)
|
|
12
|
+
const RE_CONTEXT_GEOMETRY = /"geometry"\s*:/;
|
|
13
|
+
const RE_CONTEXT_PROPERTIES = /"properties"\s*:/;
|
|
14
|
+
const RE_CONTEXT_FEATURES = /"features"\s*:/;
|
|
15
|
+
const RE_COLLAPSED_BRACKET = /^(\s*"[^"]+"\s*:\s*)([{\[])/;
|
|
16
|
+
const RE_COLLAPSED_ROOT = /^(\s*)([{\[]),?\s*$/;
|
|
17
|
+
const RE_ESCAPE_AMP = /&/g;
|
|
18
|
+
const RE_ESCAPE_LT = /</g;
|
|
19
|
+
const RE_ESCAPE_GT = />/g;
|
|
20
|
+
const RE_PUNCTUATION = /([{}[\],:])/g;
|
|
21
|
+
const RE_JSON_KEYS = /"([^"]+)"(<span class="json-punctuation">:<\/span>)/g;
|
|
22
|
+
const RE_TYPE_VALUES = /<span class="geojson-key">"type"<\/span><span class="json-punctuation">:<\/span>(\s*)"([^"]*)"/g;
|
|
23
|
+
const RE_STRING_VALUES = /(<span class="json-punctuation">:<\/span>)(\s*)"([^"]*)"/g;
|
|
24
|
+
const RE_COLOR_HEX = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
25
|
+
const RE_NUMBERS_COLON = /(<span class="json-punctuation">:<\/span>)(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi;
|
|
26
|
+
const RE_NUMBERS_ARRAY = /(<span class="json-punctuation">[\[,]<\/span>)(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi;
|
|
27
|
+
const RE_NUMBERS_START = /^(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gim;
|
|
28
|
+
const RE_BOOLEANS = /(<span class="json-punctuation">:<\/span>)(\s*)(true|false)/g;
|
|
29
|
+
const RE_NULL = /(<span class="json-punctuation">:<\/span>)(\s*)(null)/g;
|
|
30
|
+
const RE_UNRECOGNIZED = /(<\/span>|^)([^<]+)(<span|$)/g;
|
|
31
|
+
const RE_WHITESPACE_ONLY = /^\s*$/;
|
|
32
|
+
const RE_WHITESPACE_SPLIT = /(\s+)/;
|
|
33
|
+
|
|
11
34
|
/**
|
|
12
35
|
* GeoJSON Editor Web Component
|
|
13
36
|
* Monaco-like architecture with virtualized line rendering
|
|
@@ -33,7 +56,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
33
56
|
this.featureRanges = new Map(); // featureKey -> {startLine, endLine, featureIndex}
|
|
34
57
|
|
|
35
58
|
// ========== View State ==========
|
|
36
|
-
this.scrollTop = 0;
|
|
37
59
|
this.viewportHeight = 0;
|
|
38
60
|
this.lineHeight = 19.5; // CSS: line-height * font-size = 1.5 * 13px
|
|
39
61
|
this.bufferLines = 5; // Extra lines to render above/below viewport
|
|
@@ -56,6 +78,148 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
56
78
|
|
|
57
79
|
// ========== Theme ==========
|
|
58
80
|
this.themes = { dark: {}, light: {} };
|
|
81
|
+
|
|
82
|
+
// ========== Undo/Redo History ==========
|
|
83
|
+
this._undoStack = []; // Stack of previous states
|
|
84
|
+
this._redoStack = []; // Stack of undone states
|
|
85
|
+
this._maxHistorySize = 100; // Maximum history entries
|
|
86
|
+
this._lastActionTime = 0; // Timestamp of last action (for grouping)
|
|
87
|
+
this._lastActionType = null; // Type of last action (for grouping)
|
|
88
|
+
this._groupingDelay = 500; // ms - actions within this delay are grouped
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ========== Render Cache ==========
|
|
92
|
+
_invalidateRenderCache() {
|
|
93
|
+
this._lastStartIndex = -1;
|
|
94
|
+
this._lastEndIndex = -1;
|
|
95
|
+
this._lastTotalLines = -1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ========== Undo/Redo System ==========
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create a snapshot of current editor state
|
|
102
|
+
* @returns {Object} State snapshot
|
|
103
|
+
*/
|
|
104
|
+
_createSnapshot() {
|
|
105
|
+
return {
|
|
106
|
+
lines: [...this.lines],
|
|
107
|
+
cursorLine: this.cursorLine,
|
|
108
|
+
cursorColumn: this.cursorColumn,
|
|
109
|
+
timestamp: Date.now()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Restore editor state from snapshot
|
|
115
|
+
* @param {Object} snapshot - State to restore
|
|
116
|
+
*/
|
|
117
|
+
_restoreSnapshot(snapshot) {
|
|
118
|
+
this.lines = [...snapshot.lines];
|
|
119
|
+
this.cursorLine = snapshot.cursorLine;
|
|
120
|
+
this.cursorColumn = snapshot.cursorColumn;
|
|
121
|
+
this.updateModel();
|
|
122
|
+
this._invalidateRenderCache();
|
|
123
|
+
this.scheduleRender();
|
|
124
|
+
this.updatePlaceholderVisibility();
|
|
125
|
+
this.emitChange();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Save current state to undo stack before making changes
|
|
130
|
+
* @param {string} actionType - Type of action (insert, delete, paste, etc.)
|
|
131
|
+
*/
|
|
132
|
+
_saveToHistory(actionType = 'edit') {
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const shouldGroup = (
|
|
135
|
+
actionType === this._lastActionType &&
|
|
136
|
+
(now - this._lastActionTime) < this._groupingDelay
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// If same action type within grouping delay, don't create new entry
|
|
140
|
+
if (!shouldGroup) {
|
|
141
|
+
const snapshot = this._createSnapshot();
|
|
142
|
+
this._undoStack.push(snapshot);
|
|
143
|
+
|
|
144
|
+
// Limit stack size
|
|
145
|
+
if (this._undoStack.length > this._maxHistorySize) {
|
|
146
|
+
this._undoStack.shift();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Clear redo stack on new action
|
|
150
|
+
this._redoStack = [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this._lastActionTime = now;
|
|
154
|
+
this._lastActionType = actionType;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Undo last action
|
|
159
|
+
* @returns {boolean} True if undo was performed
|
|
160
|
+
*/
|
|
161
|
+
undo() {
|
|
162
|
+
if (this._undoStack.length === 0) return false;
|
|
163
|
+
|
|
164
|
+
// Save current state to redo stack
|
|
165
|
+
this._redoStack.push(this._createSnapshot());
|
|
166
|
+
|
|
167
|
+
// Restore previous state
|
|
168
|
+
const previousState = this._undoStack.pop();
|
|
169
|
+
this._restoreSnapshot(previousState);
|
|
170
|
+
|
|
171
|
+
// Reset action tracking
|
|
172
|
+
this._lastActionType = null;
|
|
173
|
+
this._lastActionTime = 0;
|
|
174
|
+
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Redo previously undone action
|
|
180
|
+
* @returns {boolean} True if redo was performed
|
|
181
|
+
*/
|
|
182
|
+
redo() {
|
|
183
|
+
if (this._redoStack.length === 0) return false;
|
|
184
|
+
|
|
185
|
+
// Save current state to undo stack
|
|
186
|
+
this._undoStack.push(this._createSnapshot());
|
|
187
|
+
|
|
188
|
+
// Restore next state
|
|
189
|
+
const nextState = this._redoStack.pop();
|
|
190
|
+
this._restoreSnapshot(nextState);
|
|
191
|
+
|
|
192
|
+
// Reset action tracking
|
|
193
|
+
this._lastActionType = null;
|
|
194
|
+
this._lastActionTime = 0;
|
|
195
|
+
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Clear undo/redo history
|
|
201
|
+
*/
|
|
202
|
+
clearHistory() {
|
|
203
|
+
this._undoStack = [];
|
|
204
|
+
this._redoStack = [];
|
|
205
|
+
this._lastActionType = null;
|
|
206
|
+
this._lastActionTime = 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if undo is available
|
|
211
|
+
* @returns {boolean}
|
|
212
|
+
*/
|
|
213
|
+
canUndo() {
|
|
214
|
+
return this._undoStack.length > 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if redo is available
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
canRedo() {
|
|
222
|
+
return this._redoStack.length > 0;
|
|
59
223
|
}
|
|
60
224
|
|
|
61
225
|
// ========== Unique ID Generation ==========
|
|
@@ -162,14 +326,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
162
326
|
* @param {Object} range - The range info {startLine, endLine}
|
|
163
327
|
*/
|
|
164
328
|
_deleteCollapsedNode(range) {
|
|
329
|
+
this._saveToHistory('delete');
|
|
330
|
+
|
|
165
331
|
// Remove all lines from startLine to endLine
|
|
166
332
|
const count = range.endLine - range.startLine + 1;
|
|
167
333
|
this.lines.splice(range.startLine, count);
|
|
168
|
-
|
|
334
|
+
|
|
169
335
|
// Position cursor at the line where the node was
|
|
170
336
|
this.cursorLine = Math.min(range.startLine, this.lines.length - 1);
|
|
171
337
|
this.cursorColumn = 0;
|
|
172
|
-
|
|
338
|
+
|
|
173
339
|
this.formatAndUpdate();
|
|
174
340
|
}
|
|
175
341
|
|
|
@@ -391,7 +557,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
391
557
|
|
|
392
558
|
// Focus textarea
|
|
393
559
|
hiddenTextarea.focus();
|
|
394
|
-
this.
|
|
560
|
+
this._invalidateRenderCache();
|
|
395
561
|
this.scheduleRender();
|
|
396
562
|
});
|
|
397
563
|
|
|
@@ -417,7 +583,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
417
583
|
viewport.scrollTop += scrollSpeed;
|
|
418
584
|
}
|
|
419
585
|
|
|
420
|
-
this.
|
|
586
|
+
this._invalidateRenderCache();
|
|
421
587
|
this.scheduleRender();
|
|
422
588
|
});
|
|
423
589
|
|
|
@@ -429,13 +595,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
429
595
|
// Focus/blur handling to show/hide cursor
|
|
430
596
|
hiddenTextarea.addEventListener('focus', () => {
|
|
431
597
|
editorWrapper.classList.add('focused');
|
|
432
|
-
this.
|
|
598
|
+
this._invalidateRenderCache(); // Force re-render to show cursor
|
|
433
599
|
this.scheduleRender();
|
|
434
600
|
});
|
|
435
601
|
|
|
436
602
|
hiddenTextarea.addEventListener('blur', () => {
|
|
437
603
|
editorWrapper.classList.remove('focused');
|
|
438
|
-
this.
|
|
604
|
+
this._invalidateRenderCache(); // Force re-render to hide cursor
|
|
439
605
|
this.scheduleRender();
|
|
440
606
|
});
|
|
441
607
|
|
|
@@ -443,7 +609,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
443
609
|
let isRendering = false;
|
|
444
610
|
viewport.addEventListener('scroll', () => {
|
|
445
611
|
if (isRendering) return;
|
|
446
|
-
this.scrollTop = viewport.scrollTop;
|
|
447
612
|
this.syncGutterScroll();
|
|
448
613
|
|
|
449
614
|
// Use requestAnimationFrame to batch scroll updates
|
|
@@ -525,6 +690,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
525
690
|
* Set the editor content from a string value
|
|
526
691
|
*/
|
|
527
692
|
setValue(value) {
|
|
693
|
+
// Save to history only if there's existing content
|
|
694
|
+
if (this.lines.length > 0) {
|
|
695
|
+
this._saveToHistory('setValue');
|
|
696
|
+
}
|
|
697
|
+
|
|
528
698
|
if (!value || !value.trim()) {
|
|
529
699
|
this.lines = [];
|
|
530
700
|
} else {
|
|
@@ -541,7 +711,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
541
711
|
this.lines = value.split('\n');
|
|
542
712
|
}
|
|
543
713
|
}
|
|
544
|
-
|
|
714
|
+
|
|
545
715
|
// Clear state for new content
|
|
546
716
|
this.collapsedNodes.clear();
|
|
547
717
|
this.hiddenFeatures.clear();
|
|
@@ -576,9 +746,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
576
746
|
* Rebuilds line-to-nodeId mapping while preserving collapsed state
|
|
577
747
|
*/
|
|
578
748
|
updateModel() {
|
|
749
|
+
// Invalidate context map cache since content changed
|
|
750
|
+
this._contextMapCache = null;
|
|
751
|
+
|
|
579
752
|
// Rebuild lineToNodeId mapping (may shift due to edits)
|
|
580
753
|
this._rebuildNodeIdMappings();
|
|
581
|
-
|
|
754
|
+
|
|
582
755
|
this.computeFeatureRanges();
|
|
583
756
|
this.computeLineMetadata();
|
|
584
757
|
this.computeVisibleLines();
|
|
@@ -757,7 +930,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
757
930
|
}
|
|
758
931
|
|
|
759
932
|
// Reset render cache to force re-render
|
|
760
|
-
this.
|
|
933
|
+
this._invalidateRenderCache();
|
|
761
934
|
this._lastEndIndex = -1;
|
|
762
935
|
this._lastTotalLines = -1;
|
|
763
936
|
}
|
|
@@ -1090,106 +1263,25 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1090
1263
|
}
|
|
1091
1264
|
|
|
1092
1265
|
handleKeydown(e) {
|
|
1093
|
-
//
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1266
|
+
// Build context for collapsed zone detection
|
|
1267
|
+
const ctx = {
|
|
1268
|
+
inCollapsedZone: this._getCollapsedRangeForLine(this.cursorLine),
|
|
1269
|
+
onCollapsedNode: this._getCollapsedNodeAtLine(this.cursorLine),
|
|
1270
|
+
onClosingLine: this._getCollapsedClosingLine(this.cursorLine)
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1098
1273
|
switch (e.key) {
|
|
1099
1274
|
case 'Enter':
|
|
1100
1275
|
e.preventDefault();
|
|
1101
|
-
|
|
1102
|
-
if (onCollapsedNode || inCollapsedZone) return;
|
|
1103
|
-
// On closing line, before bracket -> block
|
|
1104
|
-
if (onClosingLine) {
|
|
1105
|
-
const line = this.lines[this.cursorLine];
|
|
1106
|
-
const bracketPos = this._getClosingBracketPos(line);
|
|
1107
|
-
if (bracketPos >= 0 && this.cursorColumn <= bracketPos) {
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
// After bracket, allow normal enter (add new line)
|
|
1111
|
-
}
|
|
1112
|
-
this.insertNewline();
|
|
1276
|
+
this._handleEnter(ctx);
|
|
1113
1277
|
break;
|
|
1114
1278
|
case 'Backspace':
|
|
1115
1279
|
e.preventDefault();
|
|
1116
|
-
|
|
1117
|
-
if (this._hasSelection()) {
|
|
1118
|
-
this._deleteSelection();
|
|
1119
|
-
this.formatAndUpdate();
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
// On closing line
|
|
1123
|
-
if (onClosingLine) {
|
|
1124
|
-
const line = this.lines[this.cursorLine];
|
|
1125
|
-
const bracketPos = this._getClosingBracketPos(line);
|
|
1126
|
-
if (bracketPos >= 0 && this.cursorColumn > bracketPos + 1) {
|
|
1127
|
-
// After bracket, allow delete
|
|
1128
|
-
this.deleteBackward();
|
|
1129
|
-
return;
|
|
1130
|
-
} else if (this.cursorColumn === bracketPos + 1) {
|
|
1131
|
-
// Just after bracket, delete whole node
|
|
1132
|
-
this._deleteCollapsedNode(onClosingLine);
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
// On or before bracket, delete whole node
|
|
1136
|
-
this._deleteCollapsedNode(onClosingLine);
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
// If on collapsed node opening line at position 0, delete whole node
|
|
1140
|
-
if (onCollapsedNode && this.cursorColumn === 0) {
|
|
1141
|
-
this._deleteCollapsedNode(onCollapsedNode);
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
// Block inside collapsed zones
|
|
1145
|
-
if (inCollapsedZone) return;
|
|
1146
|
-
// On opening line, allow editing before and at bracket
|
|
1147
|
-
if (onCollapsedNode) {
|
|
1148
|
-
const line = this.lines[this.cursorLine];
|
|
1149
|
-
const bracketPos = line.search(/[{\[]/);
|
|
1150
|
-
if (this.cursorColumn > bracketPos + 1) {
|
|
1151
|
-
// After bracket, delete whole node
|
|
1152
|
-
this._deleteCollapsedNode(onCollapsedNode);
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
this.deleteBackward();
|
|
1280
|
+
this._handleBackspace(ctx);
|
|
1157
1281
|
break;
|
|
1158
1282
|
case 'Delete':
|
|
1159
1283
|
e.preventDefault();
|
|
1160
|
-
|
|
1161
|
-
if (this._hasSelection()) {
|
|
1162
|
-
this._deleteSelection();
|
|
1163
|
-
this.formatAndUpdate();
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
// On closing line
|
|
1167
|
-
if (onClosingLine) {
|
|
1168
|
-
const line = this.lines[this.cursorLine];
|
|
1169
|
-
const bracketPos = this._getClosingBracketPos(line);
|
|
1170
|
-
if (bracketPos >= 0 && this.cursorColumn > bracketPos) {
|
|
1171
|
-
// After bracket, allow delete
|
|
1172
|
-
this.deleteForward();
|
|
1173
|
-
return;
|
|
1174
|
-
}
|
|
1175
|
-
// On or before bracket, delete whole node
|
|
1176
|
-
this._deleteCollapsedNode(onClosingLine);
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
// If on collapsed node opening line
|
|
1180
|
-
if (onCollapsedNode) {
|
|
1181
|
-
const line = this.lines[this.cursorLine];
|
|
1182
|
-
const bracketPos = line.search(/[{\[]/);
|
|
1183
|
-
if (this.cursorColumn > bracketPos) {
|
|
1184
|
-
// After bracket, delete whole node
|
|
1185
|
-
this._deleteCollapsedNode(onCollapsedNode);
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
// Before bracket, allow editing key name
|
|
1189
|
-
}
|
|
1190
|
-
// Block inside collapsed zones
|
|
1191
|
-
if (inCollapsedZone) return;
|
|
1192
|
-
this.deleteForward();
|
|
1284
|
+
this._handleDelete(ctx);
|
|
1193
1285
|
break;
|
|
1194
1286
|
case 'ArrowUp':
|
|
1195
1287
|
e.preventDefault();
|
|
@@ -1209,64 +1301,169 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1209
1301
|
break;
|
|
1210
1302
|
case 'Home':
|
|
1211
1303
|
e.preventDefault();
|
|
1212
|
-
this._handleHomeEnd('home', e.shiftKey, onClosingLine);
|
|
1304
|
+
this._handleHomeEnd('home', e.shiftKey, ctx.onClosingLine);
|
|
1213
1305
|
break;
|
|
1214
1306
|
case 'End':
|
|
1215
1307
|
e.preventDefault();
|
|
1216
|
-
this._handleHomeEnd('end', e.shiftKey, onClosingLine);
|
|
1308
|
+
this._handleHomeEnd('end', e.shiftKey, ctx.onClosingLine);
|
|
1217
1309
|
break;
|
|
1218
1310
|
case 'a':
|
|
1219
|
-
// Ctrl+A or Cmd+A: select all
|
|
1220
1311
|
if (e.ctrlKey || e.metaKey) {
|
|
1221
1312
|
e.preventDefault();
|
|
1222
1313
|
this._selectAll();
|
|
1223
|
-
return;
|
|
1224
1314
|
}
|
|
1225
1315
|
break;
|
|
1226
|
-
case '
|
|
1227
|
-
e.
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
// Find the position just after the opening bracket
|
|
1234
|
-
const startLine = this.lines[containingNode.startLine];
|
|
1235
|
-
const bracketPos = startLine.search(/[{\[]/);
|
|
1236
|
-
|
|
1237
|
-
this.toggleCollapse(containingNode.nodeId);
|
|
1238
|
-
|
|
1239
|
-
// Move cursor to just after the opening bracket
|
|
1240
|
-
this.cursorLine = containingNode.startLine;
|
|
1241
|
-
this.cursorColumn = bracketPos >= 0 ? bracketPos + 1 : startLine.length;
|
|
1242
|
-
this._clearSelection();
|
|
1243
|
-
this._scrollToCursor();
|
|
1316
|
+
case 'z':
|
|
1317
|
+
if (e.ctrlKey || e.metaKey) {
|
|
1318
|
+
e.preventDefault();
|
|
1319
|
+
if (e.shiftKey) {
|
|
1320
|
+
this.redo();
|
|
1321
|
+
} else {
|
|
1322
|
+
this.undo();
|
|
1244
1323
|
}
|
|
1245
|
-
return;
|
|
1246
1324
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
if (
|
|
1250
|
-
|
|
1251
|
-
|
|
1325
|
+
break;
|
|
1326
|
+
case 'y':
|
|
1327
|
+
if (e.ctrlKey || e.metaKey) {
|
|
1328
|
+
e.preventDefault();
|
|
1329
|
+
this.redo();
|
|
1252
1330
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1331
|
+
break;
|
|
1332
|
+
case 's':
|
|
1333
|
+
if (e.ctrlKey || e.metaKey) {
|
|
1334
|
+
e.preventDefault();
|
|
1335
|
+
this.save();
|
|
1336
|
+
}
|
|
1337
|
+
break;
|
|
1338
|
+
case 'o':
|
|
1339
|
+
if ((e.ctrlKey || e.metaKey) && !this.hasAttribute('readonly')) {
|
|
1340
|
+
e.preventDefault();
|
|
1341
|
+
this.open();
|
|
1256
1342
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1343
|
+
break;
|
|
1344
|
+
case 'Tab':
|
|
1345
|
+
e.preventDefault();
|
|
1346
|
+
this._handleTab(e.shiftKey, ctx);
|
|
1260
1347
|
break;
|
|
1261
1348
|
}
|
|
1262
1349
|
}
|
|
1263
1350
|
|
|
1351
|
+
_handleEnter(ctx) {
|
|
1352
|
+
// Block in collapsed zones
|
|
1353
|
+
if (ctx.onCollapsedNode || ctx.inCollapsedZone) return;
|
|
1354
|
+
// On closing line, before bracket -> block
|
|
1355
|
+
if (ctx.onClosingLine) {
|
|
1356
|
+
const line = this.lines[this.cursorLine];
|
|
1357
|
+
const bracketPos = this._getClosingBracketPos(line);
|
|
1358
|
+
if (bracketPos >= 0 && this.cursorColumn <= bracketPos) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
this.insertNewline();
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
_handleBackspace(ctx) {
|
|
1366
|
+
// Delete selection if any
|
|
1367
|
+
if (this._hasSelection()) {
|
|
1368
|
+
this._deleteSelection();
|
|
1369
|
+
this.formatAndUpdate();
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
// On closing line
|
|
1373
|
+
if (ctx.onClosingLine) {
|
|
1374
|
+
const line = this.lines[this.cursorLine];
|
|
1375
|
+
const bracketPos = this._getClosingBracketPos(line);
|
|
1376
|
+
if (bracketPos >= 0 && this.cursorColumn > bracketPos + 1) {
|
|
1377
|
+
this.deleteBackward();
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
this._deleteCollapsedNode(ctx.onClosingLine);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
// If on collapsed node opening line at position 0, delete whole node
|
|
1384
|
+
if (ctx.onCollapsedNode && this.cursorColumn === 0) {
|
|
1385
|
+
this._deleteCollapsedNode(ctx.onCollapsedNode);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
// Block inside collapsed zones
|
|
1389
|
+
if (ctx.inCollapsedZone) return;
|
|
1390
|
+
// On opening line, allow editing before bracket
|
|
1391
|
+
if (ctx.onCollapsedNode) {
|
|
1392
|
+
const line = this.lines[this.cursorLine];
|
|
1393
|
+
const bracketPos = line.search(/[{\[]/);
|
|
1394
|
+
if (this.cursorColumn > bracketPos + 1) {
|
|
1395
|
+
this._deleteCollapsedNode(ctx.onCollapsedNode);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
this.deleteBackward();
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
_handleDelete(ctx) {
|
|
1403
|
+
// Delete selection if any
|
|
1404
|
+
if (this._hasSelection()) {
|
|
1405
|
+
this._deleteSelection();
|
|
1406
|
+
this.formatAndUpdate();
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
// On closing line
|
|
1410
|
+
if (ctx.onClosingLine) {
|
|
1411
|
+
const line = this.lines[this.cursorLine];
|
|
1412
|
+
const bracketPos = this._getClosingBracketPos(line);
|
|
1413
|
+
if (bracketPos >= 0 && this.cursorColumn > bracketPos) {
|
|
1414
|
+
this.deleteForward();
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
this._deleteCollapsedNode(ctx.onClosingLine);
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
// If on collapsed node opening line
|
|
1421
|
+
if (ctx.onCollapsedNode) {
|
|
1422
|
+
const line = this.lines[this.cursorLine];
|
|
1423
|
+
const bracketPos = line.search(/[{\[]/);
|
|
1424
|
+
if (this.cursorColumn > bracketPos) {
|
|
1425
|
+
this._deleteCollapsedNode(ctx.onCollapsedNode);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
// Block inside collapsed zones
|
|
1430
|
+
if (ctx.inCollapsedZone) return;
|
|
1431
|
+
this.deleteForward();
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
_handleTab(isShiftKey, ctx) {
|
|
1435
|
+
// Shift+Tab: collapse the containing expanded node
|
|
1436
|
+
if (isShiftKey) {
|
|
1437
|
+
const containingNode = this._getContainingExpandedNode(this.cursorLine);
|
|
1438
|
+
if (containingNode) {
|
|
1439
|
+
const startLine = this.lines[containingNode.startLine];
|
|
1440
|
+
const bracketPos = startLine.search(/[{\[]/);
|
|
1441
|
+
this.toggleCollapse(containingNode.nodeId);
|
|
1442
|
+
this.cursorLine = containingNode.startLine;
|
|
1443
|
+
this.cursorColumn = bracketPos >= 0 ? bracketPos + 1 : startLine.length;
|
|
1444
|
+
this._clearSelection();
|
|
1445
|
+
this._scrollToCursor();
|
|
1446
|
+
}
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
// Tab: expand collapsed node if on one
|
|
1450
|
+
if (ctx.onCollapsedNode) {
|
|
1451
|
+
this.toggleCollapse(ctx.onCollapsedNode.nodeId);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (ctx.onClosingLine) {
|
|
1455
|
+
this.toggleCollapse(ctx.onClosingLine.nodeId);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1264
1459
|
insertNewline() {
|
|
1460
|
+
this._saveToHistory('newline');
|
|
1461
|
+
|
|
1265
1462
|
if (this.cursorLine < this.lines.length) {
|
|
1266
1463
|
const line = this.lines[this.cursorLine];
|
|
1267
1464
|
const before = line.substring(0, this.cursorColumn);
|
|
1268
1465
|
const after = line.substring(this.cursorColumn);
|
|
1269
|
-
|
|
1466
|
+
|
|
1270
1467
|
this.lines[this.cursorLine] = before;
|
|
1271
1468
|
this.lines.splice(this.cursorLine + 1, 0, after);
|
|
1272
1469
|
this.cursorLine++;
|
|
@@ -1276,11 +1473,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1276
1473
|
this.cursorLine = this.lines.length - 1;
|
|
1277
1474
|
this.cursorColumn = 0;
|
|
1278
1475
|
}
|
|
1279
|
-
|
|
1476
|
+
|
|
1280
1477
|
this.formatAndUpdate();
|
|
1281
1478
|
}
|
|
1282
1479
|
|
|
1283
1480
|
deleteBackward() {
|
|
1481
|
+
this._saveToHistory('delete');
|
|
1482
|
+
|
|
1284
1483
|
if (this.cursorColumn > 0) {
|
|
1285
1484
|
const line = this.lines[this.cursorLine];
|
|
1286
1485
|
this.lines[this.cursorLine] = line.substring(0, this.cursorColumn - 1) + line.substring(this.cursorColumn);
|
|
@@ -1294,11 +1493,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1294
1493
|
this.lines.splice(this.cursorLine, 1);
|
|
1295
1494
|
this.cursorLine--;
|
|
1296
1495
|
}
|
|
1297
|
-
|
|
1496
|
+
|
|
1298
1497
|
this.formatAndUpdate();
|
|
1299
1498
|
}
|
|
1300
1499
|
|
|
1301
1500
|
deleteForward() {
|
|
1501
|
+
this._saveToHistory('delete');
|
|
1502
|
+
|
|
1302
1503
|
if (this.cursorLine < this.lines.length) {
|
|
1303
1504
|
const line = this.lines[this.cursorLine];
|
|
1304
1505
|
if (this.cursorColumn < line.length) {
|
|
@@ -1309,7 +1510,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1309
1510
|
this.lines.splice(this.cursorLine + 1, 1);
|
|
1310
1511
|
}
|
|
1311
1512
|
}
|
|
1312
|
-
|
|
1513
|
+
|
|
1313
1514
|
this.formatAndUpdate();
|
|
1314
1515
|
}
|
|
1315
1516
|
|
|
@@ -1339,7 +1540,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1339
1540
|
const maxCol = this.lines[this.cursorLine]?.length || 0;
|
|
1340
1541
|
this.cursorColumn = Math.min(this.cursorColumn, maxCol);
|
|
1341
1542
|
|
|
1342
|
-
this.
|
|
1543
|
+
this._invalidateRenderCache();
|
|
1343
1544
|
this._scrollToCursor();
|
|
1344
1545
|
this.scheduleRender();
|
|
1345
1546
|
}
|
|
@@ -1348,124 +1549,103 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1348
1549
|
* Move cursor horizontally with smart navigation around collapsed nodes
|
|
1349
1550
|
*/
|
|
1350
1551
|
moveCursorHorizontal(delta) {
|
|
1552
|
+
if (delta > 0) {
|
|
1553
|
+
this._moveCursorRight();
|
|
1554
|
+
} else {
|
|
1555
|
+
this._moveCursorLeft();
|
|
1556
|
+
}
|
|
1557
|
+
this._invalidateRenderCache();
|
|
1558
|
+
this._scrollToCursor();
|
|
1559
|
+
this.scheduleRender();
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
_moveCursorRight() {
|
|
1351
1563
|
const line = this.lines[this.cursorLine];
|
|
1352
1564
|
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
1353
1565
|
const onClosingLine = this._getCollapsedClosingLine(this.cursorLine);
|
|
1354
|
-
|
|
1355
|
-
if (
|
|
1356
|
-
|
|
1357
|
-
if (
|
|
1358
|
-
|
|
1359
|
-
if (this.cursorColumn < bracketPos) {
|
|
1360
|
-
// Before bracket, jump to bracket
|
|
1361
|
-
this.cursorColumn = bracketPos;
|
|
1362
|
-
} else if (this.cursorColumn >= line.length) {
|
|
1363
|
-
// At end, go to next line
|
|
1364
|
-
if (this.cursorLine < this.lines.length - 1) {
|
|
1365
|
-
this.cursorLine++;
|
|
1366
|
-
this.cursorColumn = 0;
|
|
1367
|
-
}
|
|
1368
|
-
} else {
|
|
1369
|
-
// On or after bracket, move normally
|
|
1370
|
-
this.cursorColumn++;
|
|
1371
|
-
}
|
|
1372
|
-
} else if (onCollapsed) {
|
|
1373
|
-
const bracketPos = line.search(/[{\[]/);
|
|
1374
|
-
if (this.cursorColumn < bracketPos) {
|
|
1375
|
-
// Before bracket, move normally
|
|
1376
|
-
this.cursorColumn++;
|
|
1377
|
-
} else if (this.cursorColumn === bracketPos) {
|
|
1378
|
-
// On bracket, go to after bracket
|
|
1379
|
-
this.cursorColumn = bracketPos + 1;
|
|
1380
|
-
} else {
|
|
1381
|
-
// After bracket, jump to closing line at bracket
|
|
1382
|
-
this.cursorLine = onCollapsed.endLine;
|
|
1383
|
-
const closingLine = this.lines[this.cursorLine];
|
|
1384
|
-
this.cursorColumn = this._getClosingBracketPos(closingLine);
|
|
1385
|
-
}
|
|
1566
|
+
|
|
1567
|
+
if (onClosingLine) {
|
|
1568
|
+
const bracketPos = this._getClosingBracketPos(line);
|
|
1569
|
+
if (this.cursorColumn < bracketPos) {
|
|
1570
|
+
this.cursorColumn = bracketPos;
|
|
1386
1571
|
} else if (this.cursorColumn >= line.length) {
|
|
1387
|
-
// Move to next line
|
|
1388
1572
|
if (this.cursorLine < this.lines.length - 1) {
|
|
1389
1573
|
this.cursorLine++;
|
|
1390
1574
|
this.cursorColumn = 0;
|
|
1391
|
-
// Skip hidden collapsed zones
|
|
1392
|
-
const collapsed = this._getCollapsedRangeForLine(this.cursorLine);
|
|
1393
|
-
if (collapsed) {
|
|
1394
|
-
this.cursorLine = collapsed.endLine;
|
|
1395
|
-
this.cursorColumn = 0;
|
|
1396
|
-
}
|
|
1397
1575
|
}
|
|
1398
1576
|
} else {
|
|
1399
1577
|
this.cursorColumn++;
|
|
1400
1578
|
}
|
|
1401
|
-
} else {
|
|
1402
|
-
|
|
1403
|
-
if (
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
this.cursorColumn = openBracketPos + 1;
|
|
1420
|
-
}
|
|
1421
|
-
} else if (onCollapsed) {
|
|
1422
|
-
const bracketPos = line.search(/[{\[]/);
|
|
1423
|
-
if (this.cursorColumn > bracketPos + 1) {
|
|
1424
|
-
// After bracket, go to just after bracket
|
|
1425
|
-
this.cursorColumn = bracketPos + 1;
|
|
1426
|
-
} else if (this.cursorColumn === bracketPos + 1) {
|
|
1427
|
-
// Just after bracket, go to bracket
|
|
1428
|
-
this.cursorColumn = bracketPos;
|
|
1429
|
-
} else if (this.cursorColumn > 0) {
|
|
1430
|
-
// Before bracket, move normally
|
|
1431
|
-
this.cursorColumn--;
|
|
1432
|
-
} else {
|
|
1433
|
-
// At start, go to previous line
|
|
1434
|
-
if (this.cursorLine > 0) {
|
|
1435
|
-
this.cursorLine--;
|
|
1436
|
-
this.cursorColumn = this.lines[this.cursorLine]?.length || 0;
|
|
1437
|
-
}
|
|
1579
|
+
} else if (onCollapsed) {
|
|
1580
|
+
const bracketPos = line.search(/[{\[]/);
|
|
1581
|
+
if (this.cursorColumn < bracketPos) {
|
|
1582
|
+
this.cursorColumn++;
|
|
1583
|
+
} else if (this.cursorColumn === bracketPos) {
|
|
1584
|
+
this.cursorColumn = bracketPos + 1;
|
|
1585
|
+
} else {
|
|
1586
|
+
this.cursorLine = onCollapsed.endLine;
|
|
1587
|
+
this.cursorColumn = this._getClosingBracketPos(this.lines[this.cursorLine]);
|
|
1588
|
+
}
|
|
1589
|
+
} else if (this.cursorColumn >= line.length) {
|
|
1590
|
+
if (this.cursorLine < this.lines.length - 1) {
|
|
1591
|
+
this.cursorLine++;
|
|
1592
|
+
this.cursorColumn = 0;
|
|
1593
|
+
const collapsed = this._getCollapsedRangeForLine(this.cursorLine);
|
|
1594
|
+
if (collapsed) {
|
|
1595
|
+
this.cursorLine = collapsed.endLine;
|
|
1596
|
+
this.cursorColumn = 0;
|
|
1438
1597
|
}
|
|
1598
|
+
}
|
|
1599
|
+
} else {
|
|
1600
|
+
this.cursorColumn++;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
_moveCursorLeft() {
|
|
1605
|
+
const line = this.lines[this.cursorLine];
|
|
1606
|
+
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
1607
|
+
const onClosingLine = this._getCollapsedClosingLine(this.cursorLine);
|
|
1608
|
+
|
|
1609
|
+
if (onClosingLine) {
|
|
1610
|
+
const bracketPos = this._getClosingBracketPos(line);
|
|
1611
|
+
if (this.cursorColumn > bracketPos + 1) {
|
|
1612
|
+
this.cursorColumn--;
|
|
1613
|
+
} else {
|
|
1614
|
+
// Jump to opening line after bracket
|
|
1615
|
+
this.cursorLine = onClosingLine.startLine;
|
|
1616
|
+
const openLine = this.lines[this.cursorLine];
|
|
1617
|
+
this.cursorColumn = openLine.search(/[{\[]/) + 1;
|
|
1618
|
+
}
|
|
1619
|
+
} else if (onCollapsed) {
|
|
1620
|
+
const bracketPos = line.search(/[{\[]/);
|
|
1621
|
+
if (this.cursorColumn > bracketPos + 1) {
|
|
1622
|
+
this.cursorColumn = bracketPos + 1;
|
|
1623
|
+
} else if (this.cursorColumn === bracketPos + 1) {
|
|
1624
|
+
this.cursorColumn = bracketPos;
|
|
1439
1625
|
} else if (this.cursorColumn > 0) {
|
|
1440
1626
|
this.cursorColumn--;
|
|
1441
1627
|
} else if (this.cursorLine > 0) {
|
|
1442
|
-
// Move to previous line
|
|
1443
1628
|
this.cursorLine--;
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1629
|
+
this.cursorColumn = this.lines[this.cursorLine]?.length || 0;
|
|
1630
|
+
}
|
|
1631
|
+
} else if (this.cursorColumn > 0) {
|
|
1632
|
+
this.cursorColumn--;
|
|
1633
|
+
} else if (this.cursorLine > 0) {
|
|
1634
|
+
this.cursorLine--;
|
|
1635
|
+
const closing = this._getCollapsedClosingLine(this.cursorLine);
|
|
1636
|
+
if (closing) {
|
|
1637
|
+
this.cursorColumn = this.lines[this.cursorLine]?.length || 0;
|
|
1638
|
+
} else {
|
|
1639
|
+
const collapsed = this._getCollapsedRangeForLine(this.cursorLine);
|
|
1640
|
+
if (collapsed) {
|
|
1641
|
+
this.cursorLine = collapsed.startLine;
|
|
1642
|
+
const openLine = this.lines[this.cursorLine];
|
|
1643
|
+
this.cursorColumn = openLine.search(/[{\[]/) + 1;
|
|
1450
1644
|
} else {
|
|
1451
|
-
|
|
1452
|
-
const collapsed = this._getCollapsedRangeForLine(this.cursorLine);
|
|
1453
|
-
if (collapsed) {
|
|
1454
|
-
// Jump to opening line after bracket
|
|
1455
|
-
this.cursorLine = collapsed.startLine;
|
|
1456
|
-
const openLine = this.lines[this.cursorLine];
|
|
1457
|
-
const bracketPos = openLine.search(/[{\[]/);
|
|
1458
|
-
this.cursorColumn = bracketPos + 1;
|
|
1459
|
-
} else {
|
|
1460
|
-
this.cursorColumn = this.lines[this.cursorLine]?.length || 0;
|
|
1461
|
-
}
|
|
1645
|
+
this.cursorColumn = this.lines[this.cursorLine]?.length || 0;
|
|
1462
1646
|
}
|
|
1463
1647
|
}
|
|
1464
1648
|
}
|
|
1465
|
-
|
|
1466
|
-
this._lastStartIndex = -1;
|
|
1467
|
-
this._scrollToCursor();
|
|
1468
|
-
this.scheduleRender();
|
|
1469
1649
|
}
|
|
1470
1650
|
|
|
1471
1651
|
/**
|
|
@@ -1493,17 +1673,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1493
1673
|
}
|
|
1494
1674
|
}
|
|
1495
1675
|
|
|
1496
|
-
/**
|
|
1497
|
-
* Legacy moveCursor for compatibility
|
|
1498
|
-
*/
|
|
1499
|
-
moveCursor(deltaLine, deltaCol) {
|
|
1500
|
-
if (deltaLine !== 0) {
|
|
1501
|
-
this.moveCursorSkipCollapsed(deltaLine);
|
|
1502
|
-
} else if (deltaCol !== 0) {
|
|
1503
|
-
this.moveCursorHorizontal(deltaCol);
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
1676
|
/**
|
|
1508
1677
|
* Handle arrow key with optional selection
|
|
1509
1678
|
*/
|
|
@@ -1558,7 +1727,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1558
1727
|
this.selectionEnd = null;
|
|
1559
1728
|
}
|
|
1560
1729
|
|
|
1561
|
-
this.
|
|
1730
|
+
this._invalidateRenderCache();
|
|
1562
1731
|
this._scrollToCursor();
|
|
1563
1732
|
this.scheduleRender();
|
|
1564
1733
|
}
|
|
@@ -1573,7 +1742,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1573
1742
|
this.cursorLine = lastLine;
|
|
1574
1743
|
this.cursorColumn = this.lines[lastLine]?.length || 0;
|
|
1575
1744
|
|
|
1576
|
-
this.
|
|
1745
|
+
this._invalidateRenderCache();
|
|
1577
1746
|
this._scrollToCursor();
|
|
1578
1747
|
this.scheduleRender();
|
|
1579
1748
|
}
|
|
@@ -1640,9 +1809,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1640
1809
|
*/
|
|
1641
1810
|
_deleteSelection() {
|
|
1642
1811
|
if (!this._hasSelection()) return false;
|
|
1643
|
-
|
|
1812
|
+
|
|
1813
|
+
this._saveToHistory('delete');
|
|
1814
|
+
|
|
1644
1815
|
const { start, end } = this._normalizeSelection();
|
|
1645
|
-
|
|
1816
|
+
|
|
1646
1817
|
if (start.line === end.line) {
|
|
1647
1818
|
// Single line selection
|
|
1648
1819
|
const line = this.lines[start.line];
|
|
@@ -1654,12 +1825,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1654
1825
|
this.lines[start.line] = startLine + endLine;
|
|
1655
1826
|
this.lines.splice(start.line + 1, end.line - start.line);
|
|
1656
1827
|
}
|
|
1657
|
-
|
|
1828
|
+
|
|
1658
1829
|
this.cursorLine = start.line;
|
|
1659
1830
|
this.cursorColumn = start.column;
|
|
1660
1831
|
this.selectionStart = null;
|
|
1661
1832
|
this.selectionEnd = null;
|
|
1662
|
-
|
|
1833
|
+
|
|
1663
1834
|
return true;
|
|
1664
1835
|
}
|
|
1665
1836
|
|
|
@@ -1668,10 +1839,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1668
1839
|
if (this._hasSelection()) {
|
|
1669
1840
|
this._deleteSelection();
|
|
1670
1841
|
}
|
|
1671
|
-
|
|
1842
|
+
|
|
1672
1843
|
// Block insertion in hidden collapsed zones
|
|
1673
1844
|
if (this._getCollapsedRangeForLine(this.cursorLine)) return;
|
|
1674
|
-
|
|
1845
|
+
|
|
1675
1846
|
// On closing line, only allow after bracket
|
|
1676
1847
|
const onClosingLine = this._getCollapsedClosingLine(this.cursorLine);
|
|
1677
1848
|
if (onClosingLine) {
|
|
@@ -1679,7 +1850,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1679
1850
|
const bracketPos = this._getClosingBracketPos(line);
|
|
1680
1851
|
if (this.cursorColumn <= bracketPos) return;
|
|
1681
1852
|
}
|
|
1682
|
-
|
|
1853
|
+
|
|
1683
1854
|
// On collapsed opening line, only allow before bracket
|
|
1684
1855
|
const onCollapsed = this._getCollapsedNodeAtLine(this.cursorLine);
|
|
1685
1856
|
if (onCollapsed) {
|
|
@@ -1687,7 +1858,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1687
1858
|
const bracketPos = line.search(/[{\[]/);
|
|
1688
1859
|
if (this.cursorColumn > bracketPos) return;
|
|
1689
1860
|
}
|
|
1690
|
-
|
|
1861
|
+
|
|
1862
|
+
// Save to history before making changes
|
|
1863
|
+
this._saveToHistory('insert');
|
|
1864
|
+
|
|
1691
1865
|
// Handle empty editor case
|
|
1692
1866
|
if (this.lines.length === 0) {
|
|
1693
1867
|
// Split text by newlines to properly handle multi-line paste
|
|
@@ -1735,11 +1909,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1735
1909
|
e.preventDefault();
|
|
1736
1910
|
if (this._hasSelection()) {
|
|
1737
1911
|
e.clipboardData.setData('text/plain', this._getSelectedText());
|
|
1912
|
+
this._saveToHistory('cut');
|
|
1738
1913
|
this._deleteSelection();
|
|
1739
1914
|
this.formatAndUpdate();
|
|
1740
1915
|
} else {
|
|
1741
1916
|
// Cut all content
|
|
1742
1917
|
e.clipboardData.setData('text/plain', this.getContent());
|
|
1918
|
+
this._saveToHistory('cut');
|
|
1743
1919
|
this.lines = [];
|
|
1744
1920
|
this.cursorLine = 0;
|
|
1745
1921
|
this.cursorColumn = 0;
|
|
@@ -1885,7 +2061,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1885
2061
|
|
|
1886
2062
|
// Use updateView - don't rebuild nodeId mappings since content didn't change
|
|
1887
2063
|
this.updateView();
|
|
1888
|
-
this.
|
|
2064
|
+
this._invalidateRenderCache(); // Force re-render
|
|
1889
2065
|
this.scheduleRender();
|
|
1890
2066
|
}
|
|
1891
2067
|
|
|
@@ -2266,178 +2442,164 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2266
2442
|
}
|
|
2267
2443
|
|
|
2268
2444
|
_buildContextMap() {
|
|
2445
|
+
// Memoization: return cached result if content hasn't changed
|
|
2446
|
+
const linesLength = this.lines.length;
|
|
2447
|
+
if (this._contextMapCache &&
|
|
2448
|
+
this._contextMapLinesLength === linesLength &&
|
|
2449
|
+
this._contextMapFirstLine === this.lines[0] &&
|
|
2450
|
+
this._contextMapLastLine === this.lines[linesLength - 1]) {
|
|
2451
|
+
return this._contextMapCache;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2269
2454
|
const contextMap = new Map();
|
|
2270
2455
|
const contextStack = [];
|
|
2271
2456
|
let pendingContext = null;
|
|
2272
|
-
|
|
2273
|
-
for (let i = 0; i <
|
|
2457
|
+
|
|
2458
|
+
for (let i = 0; i < linesLength; i++) {
|
|
2274
2459
|
const line = this.lines[i];
|
|
2275
2460
|
const currentContext = contextStack[contextStack.length - 1]?.context || 'Feature';
|
|
2276
2461
|
contextMap.set(i, currentContext);
|
|
2277
|
-
|
|
2462
|
+
|
|
2278
2463
|
// Check for context-changing keys
|
|
2279
|
-
if (
|
|
2280
|
-
else if (
|
|
2281
|
-
else if (
|
|
2282
|
-
|
|
2464
|
+
if (RE_CONTEXT_GEOMETRY.test(line)) pendingContext = 'geometry';
|
|
2465
|
+
else if (RE_CONTEXT_PROPERTIES.test(line)) pendingContext = 'properties';
|
|
2466
|
+
else if (RE_CONTEXT_FEATURES.test(line)) pendingContext = 'Feature';
|
|
2467
|
+
|
|
2283
2468
|
// Track brackets
|
|
2284
2469
|
const openBraces = (line.match(/\{/g) || []).length;
|
|
2285
2470
|
const closeBraces = (line.match(/\}/g) || []).length;
|
|
2286
2471
|
const openBrackets = (line.match(/\[/g) || []).length;
|
|
2287
2472
|
const closeBrackets = (line.match(/\]/g) || []).length;
|
|
2288
|
-
|
|
2473
|
+
|
|
2289
2474
|
for (let j = 0; j < openBraces + openBrackets; j++) {
|
|
2290
2475
|
contextStack.push({ context: pendingContext || currentContext, isArray: j >= openBraces });
|
|
2291
2476
|
pendingContext = null;
|
|
2292
2477
|
}
|
|
2293
|
-
|
|
2478
|
+
|
|
2294
2479
|
for (let j = 0; j < closeBraces + closeBrackets && contextStack.length > 0; j++) {
|
|
2295
2480
|
contextStack.pop();
|
|
2296
2481
|
}
|
|
2297
2482
|
}
|
|
2298
|
-
|
|
2483
|
+
|
|
2484
|
+
// Cache the result
|
|
2485
|
+
this._contextMapCache = contextMap;
|
|
2486
|
+
this._contextMapLinesLength = linesLength;
|
|
2487
|
+
this._contextMapFirstLine = this.lines[0];
|
|
2488
|
+
this._contextMapLastLine = this.lines[linesLength - 1];
|
|
2489
|
+
|
|
2299
2490
|
return contextMap;
|
|
2300
2491
|
}
|
|
2301
2492
|
|
|
2302
2493
|
_highlightSyntax(text, context, meta) {
|
|
2303
2494
|
if (!text) return '';
|
|
2304
|
-
|
|
2495
|
+
|
|
2305
2496
|
// For collapsed nodes, truncate the text at the opening bracket
|
|
2306
2497
|
let displayText = text;
|
|
2307
2498
|
let collapsedBracket = null;
|
|
2308
|
-
|
|
2499
|
+
|
|
2309
2500
|
if (meta?.collapseButton?.isCollapsed) {
|
|
2310
2501
|
// Match "key": { or "key": [
|
|
2311
|
-
const bracketMatch = text.match(
|
|
2502
|
+
const bracketMatch = text.match(RE_COLLAPSED_BRACKET);
|
|
2312
2503
|
// Also match standalone { or [ (root Feature objects)
|
|
2313
|
-
const rootMatch = !bracketMatch && text.match(
|
|
2314
|
-
|
|
2504
|
+
const rootMatch = !bracketMatch && text.match(RE_COLLAPSED_ROOT);
|
|
2505
|
+
|
|
2315
2506
|
if (bracketMatch) {
|
|
2316
|
-
// Keep only the part up to and including the opening bracket
|
|
2317
2507
|
displayText = bracketMatch[1] + bracketMatch[2];
|
|
2318
2508
|
collapsedBracket = bracketMatch[2];
|
|
2319
2509
|
} else if (rootMatch) {
|
|
2320
|
-
// Root object - just keep the bracket
|
|
2321
2510
|
displayText = rootMatch[1] + rootMatch[2];
|
|
2322
2511
|
collapsedBracket = rootMatch[2];
|
|
2323
2512
|
}
|
|
2324
2513
|
}
|
|
2325
|
-
|
|
2514
|
+
|
|
2326
2515
|
// Escape HTML first
|
|
2327
2516
|
let result = displayText
|
|
2328
|
-
.replace(
|
|
2329
|
-
.replace(
|
|
2330
|
-
.replace(
|
|
2331
|
-
|
|
2517
|
+
.replace(RE_ESCAPE_AMP, '&')
|
|
2518
|
+
.replace(RE_ESCAPE_LT, '<')
|
|
2519
|
+
.replace(RE_ESCAPE_GT, '>');
|
|
2520
|
+
|
|
2332
2521
|
// Punctuation FIRST (before other replacements can interfere)
|
|
2333
|
-
result = result.replace(
|
|
2334
|
-
|
|
2522
|
+
result = result.replace(RE_PUNCTUATION, '<span class="json-punctuation">$1</span>');
|
|
2523
|
+
|
|
2335
2524
|
// JSON keys - match "key" followed by :
|
|
2336
2525
|
// In properties context, all keys are treated as regular JSON keys
|
|
2337
|
-
|
|
2526
|
+
RE_JSON_KEYS.lastIndex = 0;
|
|
2527
|
+
result = result.replace(RE_JSON_KEYS, (match, key, colon) => {
|
|
2338
2528
|
if (context !== 'properties' && GEOJSON_KEYS.includes(key)) {
|
|
2339
2529
|
return `<span class="geojson-key">"${key}"</span>${colon}`;
|
|
2340
2530
|
}
|
|
2341
2531
|
return `<span class="json-key">"${key}"</span>${colon}`;
|
|
2342
2532
|
});
|
|
2343
|
-
|
|
2533
|
+
|
|
2344
2534
|
// Type values - "type": "Value" - but NOT inside properties context
|
|
2345
|
-
// IMPORTANT: Preserve original spacing by capturing and re-emitting whitespace
|
|
2346
2535
|
if (context !== 'properties') {
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
}
|
|
2354
|
-
);
|
|
2536
|
+
RE_TYPE_VALUES.lastIndex = 0;
|
|
2537
|
+
result = result.replace(RE_TYPE_VALUES, (match, space, type) => {
|
|
2538
|
+
const isValid = type === 'Feature' || type === 'FeatureCollection' || GEOMETRY_TYPES.includes(type);
|
|
2539
|
+
const cls = isValid ? 'geojson-type' : 'geojson-type-invalid';
|
|
2540
|
+
return `<span class="geojson-key">"type"</span><span class="json-punctuation">:</span>${space}<span class="${cls}">"${type}"</span>`;
|
|
2541
|
+
});
|
|
2355
2542
|
}
|
|
2356
2543
|
|
|
2357
2544
|
// String values (not already wrapped in spans)
|
|
2358
|
-
|
|
2359
|
-
result = result.replace(
|
|
2360
|
-
|
|
2361
|
-
(
|
|
2362
|
-
|
|
2363
|
-
if (match.includes('geojson-type') || match.includes('json-string')) return match;
|
|
2364
|
-
|
|
2365
|
-
// Check if it's a color value (hex) - use ::before for swatch via CSS class
|
|
2366
|
-
if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(val)) {
|
|
2367
|
-
return `${colon}${space}<span class="json-string json-color" data-color="${val}" style="--swatch-color: ${val}">"${val}"</span>`;
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
return `${colon}${space}<span class="json-string">"${val}"</span>`;
|
|
2545
|
+
RE_STRING_VALUES.lastIndex = 0;
|
|
2546
|
+
result = result.replace(RE_STRING_VALUES, (match, colon, space, val) => {
|
|
2547
|
+
if (match.includes('geojson-type') || match.includes('json-string')) return match;
|
|
2548
|
+
if (RE_COLOR_HEX.test(val)) {
|
|
2549
|
+
return `${colon}${space}<span class="json-string json-color" data-color="${val}" style="--swatch-color: ${val}">"${val}"</span>`;
|
|
2371
2550
|
}
|
|
2372
|
-
|
|
2551
|
+
return `${colon}${space}<span class="json-string">"${val}"</span>`;
|
|
2552
|
+
});
|
|
2373
2553
|
|
|
2374
2554
|
// Numbers after colon
|
|
2375
|
-
|
|
2376
|
-
result = result.replace(
|
|
2377
|
-
/(<span class="json-punctuation">:<\/span>)(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi,
|
|
2378
|
-
'$1$2<span class="json-number">$3</span>'
|
|
2379
|
-
);
|
|
2555
|
+
RE_NUMBERS_COLON.lastIndex = 0;
|
|
2556
|
+
result = result.replace(RE_NUMBERS_COLON, '$1$2<span class="json-number">$3</span>');
|
|
2380
2557
|
|
|
2381
2558
|
// Numbers in arrays (after [ or ,)
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
'$1$2<span class="json-number">$3</span>'
|
|
2385
|
-
);
|
|
2559
|
+
RE_NUMBERS_ARRAY.lastIndex = 0;
|
|
2560
|
+
result = result.replace(RE_NUMBERS_ARRAY, '$1$2<span class="json-number">$3</span>');
|
|
2386
2561
|
|
|
2387
2562
|
// Standalone numbers at start of line (coordinates arrays)
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
'$1<span class="json-number">$2</span>'
|
|
2391
|
-
);
|
|
2563
|
+
RE_NUMBERS_START.lastIndex = 0;
|
|
2564
|
+
result = result.replace(RE_NUMBERS_START, '$1<span class="json-number">$2</span>');
|
|
2392
2565
|
|
|
2393
2566
|
// Booleans - use ::before for checkbox via CSS class
|
|
2394
|
-
|
|
2395
|
-
result = result.replace(
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
return `${colon}${space}<span class="json-boolean${checkedClass}">${val}</span>`;
|
|
2400
|
-
}
|
|
2401
|
-
);
|
|
2567
|
+
RE_BOOLEANS.lastIndex = 0;
|
|
2568
|
+
result = result.replace(RE_BOOLEANS, (match, colon, space, val) => {
|
|
2569
|
+
const checkedClass = val === 'true' ? ' json-bool-true' : ' json-bool-false';
|
|
2570
|
+
return `${colon}${space}<span class="json-boolean${checkedClass}">${val}</span>`;
|
|
2571
|
+
});
|
|
2402
2572
|
|
|
2403
2573
|
// Null
|
|
2404
|
-
|
|
2405
|
-
result = result.replace(
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
);
|
|
2409
|
-
|
|
2410
|
-
// Collapsed bracket indicator - just add the class, CSS ::after adds the "...]" or "...}"
|
|
2574
|
+
RE_NULL.lastIndex = 0;
|
|
2575
|
+
result = result.replace(RE_NULL, '$1$2<span class="json-null">$3</span>');
|
|
2576
|
+
|
|
2577
|
+
// Collapsed bracket indicator
|
|
2411
2578
|
if (collapsedBracket) {
|
|
2412
2579
|
const bracketClass = collapsedBracket === '[' ? 'collapsed-bracket-array' : 'collapsed-bracket-object';
|
|
2413
|
-
// Replace the last punctuation span (the opening bracket) with collapsed style class
|
|
2414
2580
|
result = result.replace(
|
|
2415
2581
|
new RegExp(`<span class="json-punctuation">\\${collapsedBracket}<\\/span>$`),
|
|
2416
2582
|
`<span class="${bracketClass}">${collapsedBracket}</span>`
|
|
2417
2583
|
);
|
|
2418
2584
|
}
|
|
2419
|
-
|
|
2420
|
-
// Mark unrecognized text as error
|
|
2421
|
-
|
|
2422
|
-
result = result.replace(
|
|
2423
|
-
|
|
2424
|
-
(
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
}).join('');
|
|
2438
|
-
return hasError ? before + processed + after : match;
|
|
2439
|
-
}
|
|
2440
|
-
);
|
|
2585
|
+
|
|
2586
|
+
// Mark unrecognized text as error
|
|
2587
|
+
RE_UNRECOGNIZED.lastIndex = 0;
|
|
2588
|
+
result = result.replace(RE_UNRECOGNIZED, (match, before, text, after) => {
|
|
2589
|
+
if (!text || RE_WHITESPACE_ONLY.test(text)) return match;
|
|
2590
|
+
// Check for unrecognized words/tokens (not whitespace, not just spaces/commas)
|
|
2591
|
+
// Keep whitespace as-is, wrap any non-whitespace unrecognized token
|
|
2592
|
+
const parts = text.split(RE_WHITESPACE_SPLIT);
|
|
2593
|
+
let hasError = false;
|
|
2594
|
+
const processed = parts.map(part => {
|
|
2595
|
+
// If it's whitespace, keep it
|
|
2596
|
+
if (RE_WHITESPACE_ONLY.test(part)) return part;
|
|
2597
|
+
// Mark as error
|
|
2598
|
+
hasError = true;
|
|
2599
|
+
return `<span class="json-error">${part}</span>`;
|
|
2600
|
+
}).join('');
|
|
2601
|
+
return hasError ? before + processed + after : match;
|
|
2602
|
+
});
|
|
2441
2603
|
|
|
2442
2604
|
// Note: visibility is now handled at line level (has-visibility class on .line element)
|
|
2443
2605
|
|
|
@@ -2496,6 +2658,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2496
2658
|
}
|
|
2497
2659
|
|
|
2498
2660
|
removeAll() {
|
|
2661
|
+
if (this.lines.length > 0) {
|
|
2662
|
+
this._saveToHistory('removeAll');
|
|
2663
|
+
}
|
|
2499
2664
|
const removed = this._parseFeatures();
|
|
2500
2665
|
this.lines = [];
|
|
2501
2666
|
this.collapsedNodes.clear();
|
|
@@ -2521,6 +2686,113 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2521
2686
|
this.emitChange();
|
|
2522
2687
|
}
|
|
2523
2688
|
|
|
2689
|
+
/**
|
|
2690
|
+
* Save GeoJSON to a file (triggers download)
|
|
2691
|
+
* @param {string} filename - Optional filename (default: 'features.geojson')
|
|
2692
|
+
* @returns {boolean} True if save was successful
|
|
2693
|
+
*/
|
|
2694
|
+
save(filename = 'features.geojson') {
|
|
2695
|
+
try {
|
|
2696
|
+
const features = this._parseFeatures();
|
|
2697
|
+
const geojson = {
|
|
2698
|
+
type: 'FeatureCollection',
|
|
2699
|
+
features: features
|
|
2700
|
+
};
|
|
2701
|
+
const json = JSON.stringify(geojson, null, 2);
|
|
2702
|
+
const blob = new Blob([json], { type: 'application/geo+json' });
|
|
2703
|
+
const url = URL.createObjectURL(blob);
|
|
2704
|
+
|
|
2705
|
+
const a = document.createElement('a');
|
|
2706
|
+
a.href = url;
|
|
2707
|
+
a.download = filename;
|
|
2708
|
+
document.body.appendChild(a);
|
|
2709
|
+
a.click();
|
|
2710
|
+
document.body.removeChild(a);
|
|
2711
|
+
URL.revokeObjectURL(url);
|
|
2712
|
+
|
|
2713
|
+
return true;
|
|
2714
|
+
} catch (e) {
|
|
2715
|
+
return false;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
/**
|
|
2720
|
+
* Open a GeoJSON file from the client filesystem
|
|
2721
|
+
* Note: Available even in readonly mode via API (only Ctrl+O shortcut is blocked)
|
|
2722
|
+
* @returns {Promise<boolean>} Promise that resolves to true if file was loaded successfully
|
|
2723
|
+
*/
|
|
2724
|
+
open() {
|
|
2725
|
+
return new Promise((resolve) => {
|
|
2726
|
+
const input = document.createElement('input');
|
|
2727
|
+
input.type = 'file';
|
|
2728
|
+
input.accept = '.geojson,.json,application/geo+json,application/json';
|
|
2729
|
+
input.style.display = 'none';
|
|
2730
|
+
|
|
2731
|
+
input.addEventListener('change', (e) => {
|
|
2732
|
+
const file = e.target.files?.[0];
|
|
2733
|
+
if (!file) {
|
|
2734
|
+
document.body.removeChild(input);
|
|
2735
|
+
resolve(false);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
const reader = new FileReader();
|
|
2740
|
+
reader.onload = (event) => {
|
|
2741
|
+
try {
|
|
2742
|
+
const content = event.target.result;
|
|
2743
|
+
const parsed = JSON.parse(content);
|
|
2744
|
+
|
|
2745
|
+
// Extract features from various GeoJSON formats
|
|
2746
|
+
let features = [];
|
|
2747
|
+
if (parsed.type === 'FeatureCollection' && Array.isArray(parsed.features)) {
|
|
2748
|
+
features = parsed.features;
|
|
2749
|
+
} else if (parsed.type === 'Feature') {
|
|
2750
|
+
features = [parsed];
|
|
2751
|
+
} else if (Array.isArray(parsed)) {
|
|
2752
|
+
features = parsed;
|
|
2753
|
+
} else {
|
|
2754
|
+
// Invalid GeoJSON structure
|
|
2755
|
+
document.body.removeChild(input);
|
|
2756
|
+
resolve(false);
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
// Validate features
|
|
2761
|
+
for (const feature of features) {
|
|
2762
|
+
this._validateFeature(feature);
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// Load features into editor
|
|
2766
|
+
this._saveToHistory('open');
|
|
2767
|
+
this.set(features);
|
|
2768
|
+
this.clearHistory(); // Clear history after opening new file
|
|
2769
|
+
document.body.removeChild(input);
|
|
2770
|
+
resolve(true);
|
|
2771
|
+
} catch (err) {
|
|
2772
|
+
document.body.removeChild(input);
|
|
2773
|
+
resolve(false);
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
|
|
2777
|
+
reader.onerror = () => {
|
|
2778
|
+
document.body.removeChild(input);
|
|
2779
|
+
resolve(false);
|
|
2780
|
+
};
|
|
2781
|
+
|
|
2782
|
+
reader.readAsText(file);
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
// Handle cancel (no file selected)
|
|
2786
|
+
input.addEventListener('cancel', () => {
|
|
2787
|
+
document.body.removeChild(input);
|
|
2788
|
+
resolve(false);
|
|
2789
|
+
});
|
|
2790
|
+
|
|
2791
|
+
document.body.appendChild(input);
|
|
2792
|
+
input.click();
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2524
2796
|
_parseFeatures() {
|
|
2525
2797
|
try {
|
|
2526
2798
|
const content = this.lines.join('\n');
|