@softwarity/geojson-editor 1.0.18 → 1.0.19
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 +26 -0
- package/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.css +46 -1
- package/src/geojson-editor.template.ts +5 -0
- package/src/geojson-editor.ts +748 -133
- package/src/internal-types.ts +2 -12
package/src/geojson-editor.ts
CHANGED
|
@@ -66,6 +66,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
66
66
|
private _nodeIdCounter: number = 0;
|
|
67
67
|
private _lineToNodeId: Map<number, string> = new Map();
|
|
68
68
|
private _nodeIdToLines: Map<string, NodeRangeInfo> = new Map();
|
|
69
|
+
private _openedNodeKeys: Set<string> = new Set(); // UniqueKeys (nodeKey:occurrence) that user opened
|
|
69
70
|
|
|
70
71
|
// ========== Derived State (computed from model) ==========
|
|
71
72
|
visibleLines: VisibleLine[] = [];
|
|
@@ -114,6 +115,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
114
115
|
private _contextMapLinesLength: number = 0;
|
|
115
116
|
private _contextMapFirstLine: string | undefined = undefined;
|
|
116
117
|
private _contextMapLastLine: string | undefined = undefined;
|
|
118
|
+
private _errorLinesCache: Set<number> | null = null;
|
|
117
119
|
|
|
118
120
|
// ========== Cached DOM Elements ==========
|
|
119
121
|
private _viewport: HTMLElement | null = null;
|
|
@@ -129,6 +131,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
129
131
|
private _placeholderLayer: HTMLElement | null = null;
|
|
130
132
|
private _editorPrefix: HTMLElement | null = null;
|
|
131
133
|
private _editorSuffix: HTMLElement | null = null;
|
|
134
|
+
private _errorNav: HTMLElement | null = null;
|
|
135
|
+
private _errorCount: HTMLElement | null = null;
|
|
136
|
+
private _prevErrorBtn: HTMLButtonElement | null = null;
|
|
137
|
+
private _nextErrorBtn: HTMLButtonElement | null = null;
|
|
132
138
|
|
|
133
139
|
constructor() {
|
|
134
140
|
super();
|
|
@@ -367,38 +373,31 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
367
373
|
* Preserves collapsed state by matching nodeKey + sequential occurrence
|
|
368
374
|
*/
|
|
369
375
|
private _rebuildNodeIdMappings() {
|
|
370
|
-
// Save
|
|
371
|
-
const
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
if (info
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Build list of collapsed nodeKeys for matching
|
|
378
|
-
const collapsedNodeKeys = [];
|
|
379
|
-
for (const nodeId of oldCollapsed) {
|
|
380
|
-
const nodeKey = oldNodeKeyMap.get(nodeId);
|
|
381
|
-
if (nodeKey) collapsedNodeKeys.push(nodeKey);
|
|
376
|
+
// Save collapsed uniqueKeys from old state
|
|
377
|
+
const collapsedUniqueKeys = new Set<string>();
|
|
378
|
+
for (const nodeId of this.collapsedNodes) {
|
|
379
|
+
const info = this._nodeIdToLines.get(nodeId);
|
|
380
|
+
if (info?.uniqueKey) collapsedUniqueKeys.add(info.uniqueKey);
|
|
382
381
|
}
|
|
383
|
-
|
|
382
|
+
|
|
384
383
|
// Reset mappings
|
|
385
384
|
this._nodeIdCounter = 0;
|
|
386
385
|
this._lineToNodeId.clear();
|
|
387
386
|
this._nodeIdToLines.clear();
|
|
388
387
|
this.collapsedNodes.clear();
|
|
389
|
-
|
|
390
|
-
// Track occurrences of each nodeKey
|
|
391
|
-
const nodeKeyOccurrences = new Map();
|
|
392
|
-
|
|
388
|
+
|
|
389
|
+
// Track occurrences of each nodeKey
|
|
390
|
+
const nodeKeyOccurrences = new Map<string, number>();
|
|
391
|
+
|
|
393
392
|
// Assign fresh IDs to all collapsible nodes
|
|
394
393
|
for (let i = 0; i < this.lines.length; i++) {
|
|
395
394
|
const line = this.lines[i];
|
|
396
|
-
|
|
395
|
+
|
|
397
396
|
// Match "key": { or "key": [
|
|
398
397
|
const kvMatch = line.match(RE_KV_MATCH);
|
|
399
398
|
// Also match standalone { or {, (root Feature objects)
|
|
400
399
|
const rootMatch = !kvMatch && line.match(RE_ROOT_MATCH);
|
|
401
|
-
|
|
400
|
+
|
|
402
401
|
if (!kvMatch && !rootMatch) continue;
|
|
403
402
|
|
|
404
403
|
let nodeKey: string;
|
|
@@ -414,30 +413,26 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
414
413
|
} else {
|
|
415
414
|
continue;
|
|
416
415
|
}
|
|
417
|
-
|
|
416
|
+
|
|
418
417
|
// Check if closes on same line
|
|
419
418
|
const rest = line.substring(line.indexOf(openBracket) + 1);
|
|
420
419
|
const counts = countBrackets(rest, openBracket);
|
|
421
420
|
if (counts.close > counts.open) continue;
|
|
422
|
-
|
|
421
|
+
|
|
423
422
|
const endLine = this._findClosingLine(i, openBracket);
|
|
424
423
|
if (endLine === -1 || endLine === i) continue;
|
|
425
|
-
|
|
426
|
-
// Generate unique ID for this node
|
|
424
|
+
|
|
425
|
+
// Generate unique ID and unique key for this node
|
|
427
426
|
const nodeId = this._generateNodeId();
|
|
428
|
-
|
|
429
|
-
this._lineToNodeId.set(i, nodeId);
|
|
430
|
-
this._nodeIdToLines.set(nodeId, { startLine: i, endLine, nodeKey, isRootFeature: !!rootMatch });
|
|
431
|
-
|
|
432
|
-
// Track occurrence of this nodeKey
|
|
433
427
|
const occurrence = nodeKeyOccurrences.get(nodeKey) || 0;
|
|
434
428
|
nodeKeyOccurrences.set(nodeKey, occurrence + 1);
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
429
|
+
const uniqueKey = `${nodeKey}:${occurrence}`;
|
|
430
|
+
|
|
431
|
+
this._lineToNodeId.set(i, nodeId);
|
|
432
|
+
this._nodeIdToLines.set(nodeId, { startLine: i, endLine, nodeKey, uniqueKey, isRootFeature: !!rootMatch });
|
|
433
|
+
|
|
434
|
+
// Restore collapsed state if was collapsed and not explicitly opened
|
|
435
|
+
if (collapsedUniqueKeys.has(uniqueKey) && !this._openedNodeKeys.has(uniqueKey)) {
|
|
441
436
|
this.collapsedNodes.add(nodeId);
|
|
442
437
|
}
|
|
443
438
|
}
|
|
@@ -533,6 +528,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
533
528
|
this._placeholderLayer = this._id('placeholderLayer');
|
|
534
529
|
this._editorPrefix = this._id('editorPrefix');
|
|
535
530
|
this._editorSuffix = this._id('editorSuffix');
|
|
531
|
+
this._errorNav = this._id('errorNav');
|
|
532
|
+
this._errorCount = this._id('errorCount');
|
|
533
|
+
this._prevErrorBtn = this._id('prevErrorBtn') as HTMLButtonElement;
|
|
534
|
+
this._nextErrorBtn = this._id('nextErrorBtn') as HTMLButtonElement;
|
|
536
535
|
}
|
|
537
536
|
|
|
538
537
|
// ========== Event Listeners ==========
|
|
@@ -728,6 +727,14 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
728
727
|
this.removeAll();
|
|
729
728
|
});
|
|
730
729
|
|
|
730
|
+
// Error navigation buttons
|
|
731
|
+
this._prevErrorBtn?.addEventListener('click', () => {
|
|
732
|
+
this.goToPrevError();
|
|
733
|
+
});
|
|
734
|
+
this._nextErrorBtn?.addEventListener('click', () => {
|
|
735
|
+
this.goToNextError();
|
|
736
|
+
});
|
|
737
|
+
|
|
731
738
|
// Initial readonly state
|
|
732
739
|
this.updateReadonly();
|
|
733
740
|
}
|
|
@@ -763,6 +770,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
763
770
|
// Clear state for new content
|
|
764
771
|
this.collapsedNodes.clear();
|
|
765
772
|
this.hiddenFeatures.clear();
|
|
773
|
+
this._openedNodeKeys.clear();
|
|
766
774
|
this._lineToNodeId.clear();
|
|
767
775
|
this._nodeIdToLines.clear();
|
|
768
776
|
this.cursorLine = 0;
|
|
@@ -794,8 +802,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
794
802
|
* Rebuilds line-to-nodeId mapping while preserving collapsed state
|
|
795
803
|
*/
|
|
796
804
|
updateModel() {
|
|
797
|
-
// Invalidate
|
|
805
|
+
// Invalidate caches since content changed
|
|
798
806
|
this._contextMapCache = null;
|
|
807
|
+
this._errorLinesCache = null;
|
|
799
808
|
|
|
800
809
|
// Rebuild lineToNodeId mapping (may shift due to edits)
|
|
801
810
|
this._rebuildNodeIdMappings();
|
|
@@ -891,9 +900,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
891
900
|
*/
|
|
892
901
|
computeLineMetadata() {
|
|
893
902
|
this.lineMetadata.clear();
|
|
894
|
-
|
|
903
|
+
|
|
895
904
|
const collapsibleRanges = this._findCollapsibleRanges();
|
|
896
|
-
|
|
905
|
+
|
|
906
|
+
// Compute error lines once (cached)
|
|
907
|
+
const errorLines = this._computeErrorLines();
|
|
908
|
+
|
|
897
909
|
for (let i = 0; i < this.lines.length; i++) {
|
|
898
910
|
const line = this.lines[i];
|
|
899
911
|
const meta: LineMeta = {
|
|
@@ -903,9 +915,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
903
915
|
visibilityButton: null,
|
|
904
916
|
isHidden: false,
|
|
905
917
|
isCollapsed: false,
|
|
906
|
-
featureKey: null
|
|
918
|
+
featureKey: null,
|
|
919
|
+
hasError: errorLines.has(i)
|
|
907
920
|
};
|
|
908
|
-
|
|
921
|
+
|
|
909
922
|
// Detect colors and booleans in a single pass
|
|
910
923
|
RE_ATTR_VALUE.lastIndex = 0;
|
|
911
924
|
let match: RegExpExecArray | null;
|
|
@@ -925,7 +938,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
925
938
|
}
|
|
926
939
|
}
|
|
927
940
|
}
|
|
928
|
-
|
|
941
|
+
|
|
929
942
|
// Check if line starts a collapsible node
|
|
930
943
|
const collapsible = collapsibleRanges.find(r => r.startLine === i);
|
|
931
944
|
if (collapsible) {
|
|
@@ -935,15 +948,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
935
948
|
isCollapsed: this.collapsedNodes.has(collapsible.nodeId)
|
|
936
949
|
};
|
|
937
950
|
}
|
|
938
|
-
|
|
951
|
+
|
|
939
952
|
// Check if line is inside a collapsed node (exclude closing bracket line)
|
|
940
|
-
const insideCollapsed = collapsibleRanges.find(r =>
|
|
953
|
+
const insideCollapsed = collapsibleRanges.find(r =>
|
|
941
954
|
this.collapsedNodes.has(r.nodeId) && i > r.startLine && i < r.endLine
|
|
942
955
|
);
|
|
943
956
|
if (insideCollapsed) {
|
|
944
957
|
meta.isCollapsed = true;
|
|
945
958
|
}
|
|
946
|
-
|
|
959
|
+
|
|
947
960
|
// Check if line belongs to a hidden feature
|
|
948
961
|
for (const [featureKey, range] of this.featureRanges) {
|
|
949
962
|
if (i >= range.startLine && i <= range.endLine) {
|
|
@@ -961,11 +974,152 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
961
974
|
break;
|
|
962
975
|
}
|
|
963
976
|
}
|
|
964
|
-
|
|
977
|
+
|
|
965
978
|
this.lineMetadata.set(i, meta);
|
|
966
979
|
}
|
|
967
980
|
}
|
|
968
981
|
|
|
982
|
+
/**
|
|
983
|
+
* Compute error lines (syntax highlighting + structural errors)
|
|
984
|
+
* Called once per model update, result is used by computeLineMetadata
|
|
985
|
+
*/
|
|
986
|
+
private _computeErrorLines(): Set<number> {
|
|
987
|
+
if (this._errorLinesCache !== null) {
|
|
988
|
+
return this._errorLinesCache;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const errorLines = new Set<number>();
|
|
992
|
+
|
|
993
|
+
// Check syntax highlighting errors for each line
|
|
994
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
995
|
+
const highlighted = highlightSyntax(this.lines[i], '', undefined);
|
|
996
|
+
if (highlighted.includes('json-error')) {
|
|
997
|
+
errorLines.add(i);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Check structural error from JSON.parse
|
|
1002
|
+
try {
|
|
1003
|
+
const content = this.lines.join('\n');
|
|
1004
|
+
const wrapped = '[' + content + ']';
|
|
1005
|
+
JSON.parse(wrapped);
|
|
1006
|
+
} catch (e) {
|
|
1007
|
+
if (e instanceof Error) {
|
|
1008
|
+
// Try to extract line number from error message
|
|
1009
|
+
// Chrome/Node: "... at line X column Y"
|
|
1010
|
+
const lineMatch = e.message.match(/line (\d+)/);
|
|
1011
|
+
if (lineMatch) {
|
|
1012
|
+
// Subtract 1 because we wrapped with '[' on first line
|
|
1013
|
+
const errorLine = Math.max(0, parseInt(lineMatch[1], 10) - 1);
|
|
1014
|
+
errorLines.add(errorLine);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
this._errorLinesCache = errorLines;
|
|
1020
|
+
return errorLines;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Get all lines that have errors (for navigation and counting)
|
|
1025
|
+
* Returns array of line indices sorted by line number
|
|
1026
|
+
*/
|
|
1027
|
+
private _getErrorLines(): number[] {
|
|
1028
|
+
const errorLines: number[] = [];
|
|
1029
|
+
for (const [lineIndex, meta] of this.lineMetadata) {
|
|
1030
|
+
if (meta.hasError) {
|
|
1031
|
+
errorLines.push(lineIndex);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return errorLines.sort((a, b) => a - b);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Navigate to the next error line
|
|
1039
|
+
*/
|
|
1040
|
+
goToNextError(): boolean {
|
|
1041
|
+
const errorLines = this._getErrorLines();
|
|
1042
|
+
if (errorLines.length === 0) return false;
|
|
1043
|
+
|
|
1044
|
+
// Find next error after current cursor position
|
|
1045
|
+
const nextError = errorLines.find(line => line > this.cursorLine);
|
|
1046
|
+
const targetLine = nextError !== undefined ? nextError : errorLines[0]; // Wrap to first
|
|
1047
|
+
|
|
1048
|
+
return this._goToErrorLine(targetLine);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Navigate to the previous error line
|
|
1053
|
+
*/
|
|
1054
|
+
goToPrevError(): boolean {
|
|
1055
|
+
const errorLines = this._getErrorLines();
|
|
1056
|
+
if (errorLines.length === 0) return false;
|
|
1057
|
+
|
|
1058
|
+
// Find previous error before current cursor position
|
|
1059
|
+
const prevErrors = errorLines.filter(line => line < this.cursorLine);
|
|
1060
|
+
const targetLine = prevErrors.length > 0 ? prevErrors[prevErrors.length - 1] : errorLines[errorLines.length - 1]; // Wrap to last
|
|
1061
|
+
|
|
1062
|
+
return this._goToErrorLine(targetLine);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Expand all collapsed nodes containing a specific line
|
|
1067
|
+
* Returns true if any nodes were expanded
|
|
1068
|
+
*/
|
|
1069
|
+
private _expandNodesContainingLine(lineIndex: number): boolean {
|
|
1070
|
+
let expanded = false;
|
|
1071
|
+
for (const [nodeId, nodeInfo] of this._nodeIdToLines) {
|
|
1072
|
+
if (this.collapsedNodes.has(nodeId) && lineIndex > nodeInfo.startLine && lineIndex <= nodeInfo.endLine) {
|
|
1073
|
+
this.collapsedNodes.delete(nodeId);
|
|
1074
|
+
// Track that this node was opened - don't re-collapse during edits
|
|
1075
|
+
if (nodeInfo.uniqueKey) {
|
|
1076
|
+
this._openedNodeKeys.add(nodeInfo.uniqueKey);
|
|
1077
|
+
}
|
|
1078
|
+
expanded = true;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return expanded;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/**
|
|
1085
|
+
* Navigate to a specific error line
|
|
1086
|
+
*/
|
|
1087
|
+
private _goToErrorLine(lineIndex: number): boolean {
|
|
1088
|
+
if (this._expandNodesContainingLine(lineIndex)) {
|
|
1089
|
+
this.updateView();
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
this.cursorLine = lineIndex;
|
|
1093
|
+
this.cursorColumn = 0;
|
|
1094
|
+
this._invalidateRenderCache();
|
|
1095
|
+
this._scrollToCursor(true); // Center the error line
|
|
1096
|
+
this.renderViewport();
|
|
1097
|
+
this._updateErrorDisplay();
|
|
1098
|
+
|
|
1099
|
+
// Focus the editor
|
|
1100
|
+
this._hiddenTextarea?.focus();
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Expand all collapsed nodes that contain error lines
|
|
1106
|
+
*/
|
|
1107
|
+
private _expandErrorNodes(): void {
|
|
1108
|
+
const errorLines = this._getErrorLines();
|
|
1109
|
+
if (errorLines.length === 0) return;
|
|
1110
|
+
|
|
1111
|
+
let expanded = false;
|
|
1112
|
+
for (const errorLine of errorLines) {
|
|
1113
|
+
if (this._expandNodesContainingLine(errorLine)) {
|
|
1114
|
+
expanded = true;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (expanded) {
|
|
1119
|
+
this.updateView();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
969
1123
|
/**
|
|
970
1124
|
* Compute which lines are visible (not inside collapsed nodes)
|
|
971
1125
|
*/
|
|
@@ -1012,10 +1166,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1012
1166
|
|
|
1013
1167
|
const totalLines = this.visibleLines.length;
|
|
1014
1168
|
const totalHeight = totalLines * this.lineHeight;
|
|
1015
|
-
|
|
1016
|
-
// Set total scrollable
|
|
1169
|
+
|
|
1170
|
+
// Set total scrollable dimensions (height and width based on content)
|
|
1017
1171
|
if (scrollContent) {
|
|
1018
1172
|
scrollContent.style.height = `${totalHeight}px`;
|
|
1173
|
+
// Calculate max line width to update horizontal scroll
|
|
1174
|
+
const charWidth = this._getCharWidth();
|
|
1175
|
+
const maxLineLength = this.lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
1176
|
+
const minWidth = maxLineLength * charWidth + 20; // 20px padding
|
|
1177
|
+
scrollContent.style.minWidth = `${minWidth}px`;
|
|
1019
1178
|
}
|
|
1020
1179
|
|
|
1021
1180
|
// Calculate visible range based on scroll position
|
|
@@ -1206,18 +1365,23 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1206
1365
|
for (let i = startIndex; i < endIndex; i++) {
|
|
1207
1366
|
const lineData = this.visibleLines[i];
|
|
1208
1367
|
if (!lineData) continue;
|
|
1209
|
-
|
|
1368
|
+
|
|
1210
1369
|
const gutterLine = _ce('div');
|
|
1211
1370
|
gutterLine.className = 'gutter-line';
|
|
1212
|
-
|
|
1371
|
+
|
|
1213
1372
|
const meta = lineData.meta;
|
|
1214
|
-
|
|
1373
|
+
|
|
1374
|
+
// Add error indicator class
|
|
1375
|
+
if (meta?.hasError) {
|
|
1376
|
+
gutterLine.classList.add('has-error');
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1215
1379
|
// Line number first
|
|
1216
1380
|
const lineNum = _ce('span');
|
|
1217
1381
|
lineNum.className = 'line-number';
|
|
1218
1382
|
lineNum.textContent = String(lineData.index + 1);
|
|
1219
1383
|
gutterLine.appendChild(lineNum);
|
|
1220
|
-
|
|
1384
|
+
|
|
1221
1385
|
// Collapse column (always present for alignment)
|
|
1222
1386
|
const collapseCol = _ce('div');
|
|
1223
1387
|
collapseCol.className = 'collapse-column';
|
|
@@ -1231,7 +1395,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1231
1395
|
collapseCol.appendChild(btn);
|
|
1232
1396
|
}
|
|
1233
1397
|
gutterLine.appendChild(collapseCol);
|
|
1234
|
-
|
|
1398
|
+
|
|
1235
1399
|
fragment.appendChild(gutterLine);
|
|
1236
1400
|
}
|
|
1237
1401
|
|
|
@@ -1355,6 +1519,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1355
1519
|
'ArrowRight': () => this._handleArrowKey(0, 1, e.shiftKey, e.ctrlKey || e.metaKey),
|
|
1356
1520
|
'Home': () => this._handleHomeEnd('home', e.shiftKey, ctx.onClosingLine),
|
|
1357
1521
|
'End': () => this._handleHomeEnd('end', e.shiftKey, ctx.onClosingLine),
|
|
1522
|
+
'PageUp': () => this._handlePageUpDown('up', e.shiftKey),
|
|
1523
|
+
'PageDown': () => this._handlePageUpDown('down', e.shiftKey),
|
|
1358
1524
|
'Tab': () => this._handleTab(e.shiftKey, ctx),
|
|
1359
1525
|
'Insert': () => { this._insertMode = !this._insertMode; this.scheduleRender(); }
|
|
1360
1526
|
};
|
|
@@ -1418,8 +1584,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1418
1584
|
// Block in collapsed zones
|
|
1419
1585
|
if (ctx.inCollapsedZone) return;
|
|
1420
1586
|
|
|
1421
|
-
//
|
|
1422
|
-
this.insertNewline();
|
|
1587
|
+
// Enter anywhere else: do nothing (JSON structure is managed automatically)
|
|
1423
1588
|
}
|
|
1424
1589
|
|
|
1425
1590
|
private _handleBackspace(ctx: CollapsedZoneContext): void {
|
|
@@ -1502,6 +1667,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1502
1667
|
|
|
1503
1668
|
/**
|
|
1504
1669
|
* Navigate to the next attribute (key or value) in the JSON
|
|
1670
|
+
* Also stops on collapsed node brackets to allow expansion with Enter
|
|
1505
1671
|
*/
|
|
1506
1672
|
private _navigateToNextAttribute(): void {
|
|
1507
1673
|
const totalLines = this.visibleLines.length;
|
|
@@ -1514,13 +1680,17 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1514
1680
|
const line = this.lines[vl.index];
|
|
1515
1681
|
const startCol = (i === currentVisibleIdx) ? this.cursorColumn : 0;
|
|
1516
1682
|
|
|
1517
|
-
const pos = this.
|
|
1683
|
+
const pos = this._findNextAttributeOrBracket(line, startCol, vl.index);
|
|
1518
1684
|
if (pos !== null) {
|
|
1519
1685
|
this.cursorLine = vl.index;
|
|
1520
1686
|
this.cursorColumn = pos.start;
|
|
1521
|
-
// Select the attribute key or value
|
|
1522
|
-
|
|
1523
|
-
|
|
1687
|
+
// Select the attribute key or value (not brackets)
|
|
1688
|
+
if (!pos.isBracket) {
|
|
1689
|
+
this.selectionStart = { line: vl.index, column: pos.start };
|
|
1690
|
+
this.selectionEnd = { line: vl.index, column: pos.end };
|
|
1691
|
+
} else {
|
|
1692
|
+
this._clearSelection();
|
|
1693
|
+
}
|
|
1524
1694
|
this._scrollToCursor();
|
|
1525
1695
|
this._invalidateRenderCache();
|
|
1526
1696
|
this.scheduleRender();
|
|
@@ -1532,12 +1702,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1532
1702
|
for (let i = 0; i < currentVisibleIdx; i++) {
|
|
1533
1703
|
const vl = this.visibleLines[i];
|
|
1534
1704
|
const line = this.lines[vl.index];
|
|
1535
|
-
const pos = this.
|
|
1705
|
+
const pos = this._findNextAttributeOrBracket(line, 0, vl.index);
|
|
1536
1706
|
if (pos !== null) {
|
|
1537
1707
|
this.cursorLine = vl.index;
|
|
1538
1708
|
this.cursorColumn = pos.start;
|
|
1539
|
-
|
|
1540
|
-
|
|
1709
|
+
if (!pos.isBracket) {
|
|
1710
|
+
this.selectionStart = { line: vl.index, column: pos.start };
|
|
1711
|
+
this.selectionEnd = { line: vl.index, column: pos.end };
|
|
1712
|
+
} else {
|
|
1713
|
+
this._clearSelection();
|
|
1714
|
+
}
|
|
1541
1715
|
this._scrollToCursor();
|
|
1542
1716
|
this._invalidateRenderCache();
|
|
1543
1717
|
this.scheduleRender();
|
|
@@ -1548,6 +1722,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1548
1722
|
|
|
1549
1723
|
/**
|
|
1550
1724
|
* Navigate to the previous attribute (key or value) in the JSON
|
|
1725
|
+
* Also stops on collapsed node brackets to allow expansion with Enter
|
|
1551
1726
|
*/
|
|
1552
1727
|
private _navigateToPrevAttribute(): void {
|
|
1553
1728
|
const totalLines = this.visibleLines.length;
|
|
@@ -1560,12 +1735,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1560
1735
|
const line = this.lines[vl.index];
|
|
1561
1736
|
const endCol = (i === currentVisibleIdx) ? this.cursorColumn : line.length;
|
|
1562
1737
|
|
|
1563
|
-
const pos = this.
|
|
1738
|
+
const pos = this._findPrevAttributeOrBracket(line, endCol, vl.index);
|
|
1564
1739
|
if (pos !== null) {
|
|
1565
1740
|
this.cursorLine = vl.index;
|
|
1566
1741
|
this.cursorColumn = pos.start;
|
|
1567
|
-
|
|
1568
|
-
|
|
1742
|
+
if (!pos.isBracket) {
|
|
1743
|
+
this.selectionStart = { line: vl.index, column: pos.start };
|
|
1744
|
+
this.selectionEnd = { line: vl.index, column: pos.end };
|
|
1745
|
+
} else {
|
|
1746
|
+
this._clearSelection();
|
|
1747
|
+
}
|
|
1569
1748
|
this._scrollToCursor();
|
|
1570
1749
|
this._invalidateRenderCache();
|
|
1571
1750
|
this.scheduleRender();
|
|
@@ -1577,12 +1756,16 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1577
1756
|
for (let i = totalLines - 1; i > currentVisibleIdx; i--) {
|
|
1578
1757
|
const vl = this.visibleLines[i];
|
|
1579
1758
|
const line = this.lines[vl.index];
|
|
1580
|
-
const pos = this.
|
|
1759
|
+
const pos = this._findPrevAttributeOrBracket(line, line.length, vl.index);
|
|
1581
1760
|
if (pos !== null) {
|
|
1582
1761
|
this.cursorLine = vl.index;
|
|
1583
1762
|
this.cursorColumn = pos.start;
|
|
1584
|
-
|
|
1585
|
-
|
|
1763
|
+
if (!pos.isBracket) {
|
|
1764
|
+
this.selectionStart = { line: vl.index, column: pos.start };
|
|
1765
|
+
this.selectionEnd = { line: vl.index, column: pos.end };
|
|
1766
|
+
} else {
|
|
1767
|
+
this._clearSelection();
|
|
1768
|
+
}
|
|
1586
1769
|
this._scrollToCursor();
|
|
1587
1770
|
this._invalidateRenderCache();
|
|
1588
1771
|
this.scheduleRender();
|
|
@@ -1594,71 +1777,88 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1594
1777
|
/**
|
|
1595
1778
|
* Find next attribute position in a line after startCol
|
|
1596
1779
|
* Returns {start, end} for the key or value, or null if none found
|
|
1780
|
+
* Also finds standalone values (numbers in arrays, etc.)
|
|
1597
1781
|
*/
|
|
1598
1782
|
private _findNextAttributeInLine(line: string, startCol: number): { start: number; end: number } | null {
|
|
1599
|
-
//
|
|
1600
|
-
const
|
|
1783
|
+
// Collect all navigable positions
|
|
1784
|
+
const positions: { start: number; end: number }[] = [];
|
|
1785
|
+
|
|
1786
|
+
// Pattern for "key": value pairs
|
|
1787
|
+
const keyValueRe = /"([^"]+)"(?:\s*:\s*(?:"([^"]*)"|(-?\d+\.?\d*(?:e[+-]?\d+)?)|true|false|null))?/gi;
|
|
1601
1788
|
let match;
|
|
1602
1789
|
|
|
1603
|
-
while ((match =
|
|
1790
|
+
while ((match = keyValueRe.exec(line)) !== null) {
|
|
1604
1791
|
const keyStart = match.index + 1; // Skip opening quote
|
|
1605
1792
|
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
|
-
}
|
|
1793
|
+
positions.push({ start: keyStart, end: keyEnd });
|
|
1611
1794
|
|
|
1612
1795
|
// Check if there's a value (string, number, boolean, null)
|
|
1613
1796
|
if (match[2] !== undefined) {
|
|
1614
|
-
// String value
|
|
1797
|
+
// String value
|
|
1615
1798
|
const valueMatch = line.substring(match.index).match(/:\s*"([^"]*)"/);
|
|
1616
1799
|
if (valueMatch) {
|
|
1617
1800
|
const valueStart = match.index + (valueMatch.index || 0) + valueMatch[0].indexOf('"') + 1;
|
|
1618
1801
|
const valueEnd = valueStart + match[2].length;
|
|
1619
|
-
|
|
1620
|
-
return { start: valueStart, end: valueEnd };
|
|
1621
|
-
}
|
|
1802
|
+
positions.push({ start: valueStart, end: valueEnd });
|
|
1622
1803
|
}
|
|
1623
1804
|
} else if (match[3] !== undefined) {
|
|
1624
|
-
// Number value
|
|
1805
|
+
// Number value after colon
|
|
1625
1806
|
const numMatch = line.substring(match.index).match(/:\s*(-?\d+\.?\d*(?:e[+-]?\d+)?)/i);
|
|
1626
1807
|
if (numMatch) {
|
|
1627
1808
|
const valueStart = match.index + (numMatch.index || 0) + numMatch[0].indexOf(numMatch[1]);
|
|
1628
1809
|
const valueEnd = valueStart + numMatch[1].length;
|
|
1629
|
-
|
|
1630
|
-
return { start: valueStart, end: valueEnd };
|
|
1631
|
-
}
|
|
1810
|
+
positions.push({ start: valueStart, end: valueEnd });
|
|
1632
1811
|
}
|
|
1633
1812
|
} else {
|
|
1634
|
-
// Boolean or null
|
|
1813
|
+
// Boolean or null
|
|
1635
1814
|
const boolMatch = line.substring(match.index).match(/:\s*(true|false|null)/);
|
|
1636
1815
|
if (boolMatch) {
|
|
1637
1816
|
const valueStart = match.index + (boolMatch.index || 0) + boolMatch[0].indexOf(boolMatch[1]);
|
|
1638
1817
|
const valueEnd = valueStart + boolMatch[1].length;
|
|
1639
|
-
|
|
1640
|
-
return { start: valueStart, end: valueEnd };
|
|
1641
|
-
}
|
|
1818
|
+
positions.push({ start: valueStart, end: valueEnd });
|
|
1642
1819
|
}
|
|
1643
1820
|
}
|
|
1644
1821
|
}
|
|
1645
1822
|
|
|
1823
|
+
// Also find standalone numbers (not after a colon) - for array elements
|
|
1824
|
+
const standaloneNumRe = /(?:^|[\[,\s])(-?\d+\.?\d*(?:e[+-]?\d+)?)\s*(?:[,\]]|$)/gi;
|
|
1825
|
+
while ((match = standaloneNumRe.exec(line)) !== null) {
|
|
1826
|
+
const numStr = match[1];
|
|
1827
|
+
const numStart = match.index + match[0].indexOf(numStr);
|
|
1828
|
+
const numEnd = numStart + numStr.length;
|
|
1829
|
+
// Avoid duplicates (numbers already captured by key-value pattern)
|
|
1830
|
+
if (!positions.some(p => p.start === numStart && p.end === numEnd)) {
|
|
1831
|
+
positions.push({ start: numStart, end: numEnd });
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Sort by start position and find first after startCol
|
|
1836
|
+
positions.sort((a, b) => a.start - b.start);
|
|
1837
|
+
for (const pos of positions) {
|
|
1838
|
+
if (pos.start > startCol) {
|
|
1839
|
+
return pos;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1646
1843
|
return null;
|
|
1647
1844
|
}
|
|
1648
1845
|
|
|
1649
1846
|
/**
|
|
1650
1847
|
* Find previous attribute position in a line before endCol
|
|
1848
|
+
* Also finds standalone values (numbers in arrays, etc.)
|
|
1651
1849
|
*/
|
|
1652
1850
|
private _findPrevAttributeInLine(line: string, endCol: number): { start: number; end: number } | null {
|
|
1653
|
-
// Collect all
|
|
1654
|
-
const
|
|
1655
|
-
|
|
1851
|
+
// Collect all navigable positions
|
|
1852
|
+
const positions: { start: number; end: number }[] = [];
|
|
1853
|
+
|
|
1854
|
+
// Pattern for "key": value pairs
|
|
1855
|
+
const keyValueRe = /"([^"]+)"(?:\s*:\s*(?:"([^"]*)"|(-?\d+\.?\d*(?:e[+-]?\d+)?)|true|false|null))?/gi;
|
|
1656
1856
|
let match;
|
|
1657
1857
|
|
|
1658
|
-
while ((match =
|
|
1858
|
+
while ((match = keyValueRe.exec(line)) !== null) {
|
|
1659
1859
|
const keyStart = match.index + 1;
|
|
1660
1860
|
const keyEnd = keyStart + match[1].length;
|
|
1661
|
-
|
|
1861
|
+
positions.push({ start: keyStart, end: keyEnd });
|
|
1662
1862
|
|
|
1663
1863
|
// Check for value
|
|
1664
1864
|
if (match[2] !== undefined) {
|
|
@@ -1666,35 +1866,120 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1666
1866
|
if (valueMatch) {
|
|
1667
1867
|
const valueStart = match.index + (valueMatch.index || 0) + valueMatch[0].indexOf('"') + 1;
|
|
1668
1868
|
const valueEnd = valueStart + match[2].length;
|
|
1669
|
-
|
|
1869
|
+
positions.push({ start: valueStart, end: valueEnd });
|
|
1670
1870
|
}
|
|
1671
1871
|
} else if (match[3] !== undefined) {
|
|
1672
1872
|
const numMatch = line.substring(match.index).match(/:\s*(-?\d+\.?\d*(?:e[+-]?\d+)?)/i);
|
|
1673
1873
|
if (numMatch) {
|
|
1674
1874
|
const valueStart = match.index + (numMatch.index || 0) + numMatch[0].indexOf(numMatch[1]);
|
|
1675
1875
|
const valueEnd = valueStart + numMatch[1].length;
|
|
1676
|
-
|
|
1876
|
+
positions.push({ start: valueStart, end: valueEnd });
|
|
1677
1877
|
}
|
|
1678
1878
|
} else {
|
|
1679
1879
|
const boolMatch = line.substring(match.index).match(/:\s*(true|false|null)/);
|
|
1680
1880
|
if (boolMatch) {
|
|
1681
1881
|
const valueStart = match.index + (boolMatch.index || 0) + boolMatch[0].indexOf(boolMatch[1]);
|
|
1682
1882
|
const valueEnd = valueStart + boolMatch[1].length;
|
|
1683
|
-
|
|
1883
|
+
positions.push({ start: valueStart, end: valueEnd });
|
|
1684
1884
|
}
|
|
1685
1885
|
}
|
|
1686
1886
|
}
|
|
1687
1887
|
|
|
1688
|
-
//
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1888
|
+
// Also find standalone numbers (not after a colon) - for array elements
|
|
1889
|
+
const standaloneNumRe = /(?:^|[\[,\s])(-?\d+\.?\d*(?:e[+-]?\d+)?)\s*(?:[,\]]|$)/gi;
|
|
1890
|
+
while ((match = standaloneNumRe.exec(line)) !== null) {
|
|
1891
|
+
const numStr = match[1];
|
|
1892
|
+
const numStart = match.index + match[0].indexOf(numStr);
|
|
1893
|
+
const numEnd = numStart + numStr.length;
|
|
1894
|
+
// Avoid duplicates
|
|
1895
|
+
if (!positions.some(p => p.start === numStart && p.end === numEnd)) {
|
|
1896
|
+
positions.push({ start: numStart, end: numEnd });
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// Sort by start position and find last that ends before endCol
|
|
1901
|
+
positions.sort((a, b) => a.start - b.start);
|
|
1902
|
+
for (let i = positions.length - 1; i >= 0; i--) {
|
|
1903
|
+
if (positions[i].end < endCol) {
|
|
1904
|
+
return positions[i];
|
|
1692
1905
|
}
|
|
1693
1906
|
}
|
|
1694
1907
|
|
|
1695
1908
|
return null;
|
|
1696
1909
|
}
|
|
1697
1910
|
|
|
1911
|
+
/**
|
|
1912
|
+
* Find bracket position in a line (opening bracket for collapsible nodes)
|
|
1913
|
+
* Looks for { or [ at end of line (for both expanded and collapsed nodes)
|
|
1914
|
+
* Returns position AFTER the bracket, or null if not found
|
|
1915
|
+
*/
|
|
1916
|
+
private _findBracketInLine(line: string): number | null {
|
|
1917
|
+
// Look for { or [ at end of line (indicates a collapsible node)
|
|
1918
|
+
// Works for both expanded and collapsed nodes - collapsed nodes still have
|
|
1919
|
+
// the bracket in raw text, the "..." is only added visually via CSS
|
|
1920
|
+
const bracketMatch = line.match(/[\[{]\s*$/);
|
|
1921
|
+
if (bracketMatch && bracketMatch.index !== undefined) {
|
|
1922
|
+
return bracketMatch.index + 1; // Position after bracket
|
|
1923
|
+
}
|
|
1924
|
+
return null;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/**
|
|
1928
|
+
* Find next attribute or bracket position in a line
|
|
1929
|
+
* Returns position with isBracket flag to indicate if it's a bracket
|
|
1930
|
+
* For brackets, cursor is placed AFTER the bracket (where Enter/Shift+Enter works)
|
|
1931
|
+
* Stops on ALL opening brackets to allow collapse/expand navigation
|
|
1932
|
+
*/
|
|
1933
|
+
private _findNextAttributeOrBracket(line: string, startCol: number, _lineIndex: number): { start: number; end: number; isBracket: boolean } | null {
|
|
1934
|
+
// First check for regular attributes
|
|
1935
|
+
const attrPos = this._findNextAttributeInLine(line, startCol);
|
|
1936
|
+
|
|
1937
|
+
// Find opening bracket position (collapsed or expanded)
|
|
1938
|
+
const bracketPos = this._findBracketInLine(line);
|
|
1939
|
+
|
|
1940
|
+
// Return whichever comes first after startCol
|
|
1941
|
+
if (attrPos !== null && bracketPos !== null) {
|
|
1942
|
+
if (bracketPos > startCol && (bracketPos < attrPos.start)) {
|
|
1943
|
+
return { start: bracketPos, end: bracketPos, isBracket: true };
|
|
1944
|
+
}
|
|
1945
|
+
return { ...attrPos, isBracket: false };
|
|
1946
|
+
} else if (attrPos !== null) {
|
|
1947
|
+
return { ...attrPos, isBracket: false };
|
|
1948
|
+
} else if (bracketPos !== null && bracketPos > startCol) {
|
|
1949
|
+
return { start: bracketPos, end: bracketPos, isBracket: true };
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
return null;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
/**
|
|
1956
|
+
* Find previous attribute or bracket position in a line
|
|
1957
|
+
* Returns position with isBracket flag to indicate if it's a bracket
|
|
1958
|
+
* For brackets, cursor is placed AFTER the bracket (where Enter/Shift+Enter works)
|
|
1959
|
+
* Stops on ALL opening brackets to allow collapse/expand navigation
|
|
1960
|
+
*/
|
|
1961
|
+
private _findPrevAttributeOrBracket(line: string, endCol: number, _lineIndex: number): { start: number; end: number; isBracket: boolean } | null {
|
|
1962
|
+
// First check for regular attributes
|
|
1963
|
+
const attrPos = this._findPrevAttributeInLine(line, endCol);
|
|
1964
|
+
|
|
1965
|
+
// Find opening bracket position (collapsed or expanded)
|
|
1966
|
+
const bracketPos = this._findBracketInLine(line);
|
|
1967
|
+
|
|
1968
|
+
// Return whichever comes last STRICTLY BEFORE endCol (to avoid staying in place)
|
|
1969
|
+
if (attrPos !== null && bracketPos !== null) {
|
|
1970
|
+
if (bracketPos < endCol && bracketPos > attrPos.end) {
|
|
1971
|
+
return { start: bracketPos, end: bracketPos, isBracket: true };
|
|
1972
|
+
}
|
|
1973
|
+
return { ...attrPos, isBracket: false };
|
|
1974
|
+
} else if (attrPos !== null) {
|
|
1975
|
+
return { ...attrPos, isBracket: false };
|
|
1976
|
+
} else if (bracketPos !== null && bracketPos < endCol) {
|
|
1977
|
+
return { start: bracketPos, end: bracketPos, isBracket: true };
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1698
1983
|
insertNewline() {
|
|
1699
1984
|
this._saveToHistory('newline');
|
|
1700
1985
|
|
|
@@ -1890,26 +2175,34 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1890
2175
|
|
|
1891
2176
|
/**
|
|
1892
2177
|
* Scroll viewport to ensure cursor is visible
|
|
2178
|
+
* @param center - if true, center the cursor line in the viewport
|
|
1893
2179
|
*/
|
|
1894
|
-
private _scrollToCursor() {
|
|
2180
|
+
private _scrollToCursor(center = false) {
|
|
1895
2181
|
const viewport = this._viewport;
|
|
1896
2182
|
if (!viewport) return;
|
|
1897
|
-
|
|
2183
|
+
|
|
1898
2184
|
// Find the visible line index for the cursor
|
|
1899
2185
|
const visibleIndex = this.visibleLines.findIndex(vl => vl.index === this.cursorLine);
|
|
1900
2186
|
if (visibleIndex === -1) return;
|
|
1901
|
-
|
|
2187
|
+
|
|
1902
2188
|
const cursorY = visibleIndex * this.lineHeight;
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2189
|
+
const viewportHeight = viewport.clientHeight;
|
|
2190
|
+
|
|
2191
|
+
if (center) {
|
|
2192
|
+
// Center the cursor line in the viewport
|
|
2193
|
+
viewport.scrollTop = Math.max(0, cursorY - viewportHeight / 2 + this.lineHeight / 2);
|
|
2194
|
+
} else {
|
|
2195
|
+
const viewportTop = viewport.scrollTop;
|
|
2196
|
+
const viewportBottom = viewportTop + viewportHeight;
|
|
2197
|
+
|
|
2198
|
+
// Scroll up if cursor is above viewport
|
|
2199
|
+
if (cursorY < viewportTop) {
|
|
2200
|
+
viewport.scrollTop = cursorY;
|
|
2201
|
+
}
|
|
2202
|
+
// Scroll down if cursor is below viewport
|
|
2203
|
+
else if (cursorY + this.lineHeight > viewportBottom) {
|
|
2204
|
+
viewport.scrollTop = cursorY + this.lineHeight - viewportHeight;
|
|
2205
|
+
}
|
|
1913
2206
|
}
|
|
1914
2207
|
}
|
|
1915
2208
|
|
|
@@ -2064,18 +2357,32 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2064
2357
|
if (isShift && !this.selectionStart) {
|
|
2065
2358
|
this.selectionStart = { line: this.cursorLine, column: this.cursorColumn };
|
|
2066
2359
|
}
|
|
2067
|
-
|
|
2360
|
+
|
|
2068
2361
|
if (key === 'home') {
|
|
2069
2362
|
if (onClosingLine) {
|
|
2363
|
+
// On closing line of collapsed node: go to start line
|
|
2070
2364
|
this.cursorLine = onClosingLine.startLine;
|
|
2365
|
+
this.cursorColumn = 0;
|
|
2366
|
+
} else if (this.cursorColumn === 0) {
|
|
2367
|
+
// Already at start of line: go to start of document
|
|
2368
|
+
this.cursorLine = 0;
|
|
2369
|
+
this.cursorColumn = 0;
|
|
2370
|
+
} else {
|
|
2371
|
+
// Go to start of line
|
|
2372
|
+
this.cursorColumn = 0;
|
|
2071
2373
|
}
|
|
2072
|
-
this.cursorColumn = 0;
|
|
2073
2374
|
} else {
|
|
2074
|
-
|
|
2075
|
-
|
|
2375
|
+
const lineLength = this.lines[this.cursorLine]?.length || 0;
|
|
2376
|
+
if (this.cursorColumn === lineLength) {
|
|
2377
|
+
// Already at end of line: go to end of document
|
|
2378
|
+
this.cursorLine = this.lines.length - 1;
|
|
2379
|
+
this.cursorColumn = this.lines[this.cursorLine]?.length || 0;
|
|
2380
|
+
} else {
|
|
2381
|
+
// Go to end of line
|
|
2382
|
+
this.cursorColumn = lineLength;
|
|
2076
2383
|
}
|
|
2077
2384
|
}
|
|
2078
|
-
|
|
2385
|
+
|
|
2079
2386
|
// Update selection end if shift is pressed
|
|
2080
2387
|
if (isShift) {
|
|
2081
2388
|
this.selectionEnd = { line: this.cursorLine, column: this.cursorColumn };
|
|
@@ -2083,7 +2390,50 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2083
2390
|
this.selectionStart = null;
|
|
2084
2391
|
this.selectionEnd = null;
|
|
2085
2392
|
}
|
|
2086
|
-
|
|
2393
|
+
|
|
2394
|
+
this._invalidateRenderCache();
|
|
2395
|
+
this._scrollToCursor();
|
|
2396
|
+
this.scheduleRender();
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
/**
|
|
2400
|
+
* Handle PageUp/PageDown
|
|
2401
|
+
*/
|
|
2402
|
+
private _handlePageUpDown(direction: 'up' | 'down', isShift: boolean): void {
|
|
2403
|
+
// Start selection if shift is pressed and no selection exists
|
|
2404
|
+
if (isShift && !this.selectionStart) {
|
|
2405
|
+
this.selectionStart = { line: this.cursorLine, column: this.cursorColumn };
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
const viewport = this._viewport;
|
|
2409
|
+
if (!viewport) return;
|
|
2410
|
+
|
|
2411
|
+
// Calculate how many lines fit in the viewport
|
|
2412
|
+
const linesPerPage = Math.floor(viewport.clientHeight / this.lineHeight);
|
|
2413
|
+
|
|
2414
|
+
if (direction === 'up') {
|
|
2415
|
+
// Find current visible index and move up by page
|
|
2416
|
+
const currentVisibleIdx = this.visibleLines.findIndex(vl => vl.index === this.cursorLine);
|
|
2417
|
+
const newVisibleIdx = Math.max(0, currentVisibleIdx - linesPerPage);
|
|
2418
|
+
this.cursorLine = this.visibleLines[newVisibleIdx]?.index || 0;
|
|
2419
|
+
} else {
|
|
2420
|
+
// Find current visible index and move down by page
|
|
2421
|
+
const currentVisibleIdx = this.visibleLines.findIndex(vl => vl.index === this.cursorLine);
|
|
2422
|
+
const newVisibleIdx = Math.min(this.visibleLines.length - 1, currentVisibleIdx + linesPerPage);
|
|
2423
|
+
this.cursorLine = this.visibleLines[newVisibleIdx]?.index || this.lines.length - 1;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
// Clamp cursor column to line length
|
|
2427
|
+
this.cursorColumn = Math.min(this.cursorColumn, this.lines[this.cursorLine]?.length || 0);
|
|
2428
|
+
|
|
2429
|
+
// Update selection end if shift is pressed
|
|
2430
|
+
if (isShift) {
|
|
2431
|
+
this.selectionEnd = { line: this.cursorLine, column: this.cursorColumn };
|
|
2432
|
+
} else {
|
|
2433
|
+
this.selectionStart = null;
|
|
2434
|
+
this.selectionEnd = null;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2087
2437
|
this._invalidateRenderCache();
|
|
2088
2438
|
this._scrollToCursor();
|
|
2089
2439
|
this.scheduleRender();
|
|
@@ -2098,9 +2448,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2098
2448
|
this.selectionEnd = { line: lastLine, column: this.lines[lastLine]?.length || 0 };
|
|
2099
2449
|
this.cursorLine = lastLine;
|
|
2100
2450
|
this.cursorColumn = this.lines[lastLine]?.length || 0;
|
|
2101
|
-
|
|
2451
|
+
|
|
2102
2452
|
this._invalidateRenderCache();
|
|
2103
|
-
|
|
2453
|
+
// Don't scroll - viewport should stay in place when selecting all
|
|
2104
2454
|
this.scheduleRender();
|
|
2105
2455
|
}
|
|
2106
2456
|
|
|
@@ -2252,15 +2602,22 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2252
2602
|
this.insertText(text);
|
|
2253
2603
|
}
|
|
2254
2604
|
|
|
2605
|
+
// Cancel any pending render from insertText/formatAndUpdate
|
|
2606
|
+
if (this.renderTimer) {
|
|
2607
|
+
cancelAnimationFrame(this.renderTimer);
|
|
2608
|
+
this.renderTimer = undefined;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2255
2611
|
// Auto-collapse coordinates after pasting into empty editor
|
|
2256
2612
|
if (wasEmpty && this.lines.length > 0) {
|
|
2257
|
-
// Cancel pending render, collapse first, then render once
|
|
2258
|
-
if (this.renderTimer) {
|
|
2259
|
-
cancelAnimationFrame(this.renderTimer);
|
|
2260
|
-
this.renderTimer = undefined;
|
|
2261
|
-
}
|
|
2262
2613
|
this.autoCollapseCoordinates();
|
|
2263
2614
|
}
|
|
2615
|
+
|
|
2616
|
+
// Expand any collapsed nodes that contain errors
|
|
2617
|
+
this._expandErrorNodes();
|
|
2618
|
+
|
|
2619
|
+
// Force immediate render (not via RAF) to ensure content displays instantly
|
|
2620
|
+
this.renderViewport();
|
|
2264
2621
|
}
|
|
2265
2622
|
|
|
2266
2623
|
handleCopy(e: ClipboardEvent): void {
|
|
@@ -2430,14 +2787,23 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2430
2787
|
}
|
|
2431
2788
|
|
|
2432
2789
|
// ========== Collapse/Expand ==========
|
|
2433
|
-
|
|
2790
|
+
|
|
2434
2791
|
toggleCollapse(nodeId: string): void {
|
|
2792
|
+
const nodeInfo = this._nodeIdToLines.get(nodeId);
|
|
2435
2793
|
if (this.collapsedNodes.has(nodeId)) {
|
|
2436
2794
|
this.collapsedNodes.delete(nodeId);
|
|
2795
|
+
// Track that user opened this node - don't re-collapse during edits
|
|
2796
|
+
if (nodeInfo?.uniqueKey) {
|
|
2797
|
+
this._openedNodeKeys.add(nodeInfo.uniqueKey);
|
|
2798
|
+
}
|
|
2437
2799
|
} else {
|
|
2438
2800
|
this.collapsedNodes.add(nodeId);
|
|
2801
|
+
// User closed it - allow re-collapse
|
|
2802
|
+
if (nodeInfo?.uniqueKey) {
|
|
2803
|
+
this._openedNodeKeys.delete(nodeInfo.uniqueKey);
|
|
2804
|
+
}
|
|
2439
2805
|
}
|
|
2440
|
-
|
|
2806
|
+
|
|
2441
2807
|
// Use updateView - don't rebuild nodeId mappings since content didn't change
|
|
2442
2808
|
this.updateView();
|
|
2443
2809
|
this._invalidateRenderCache(); // Force re-render
|
|
@@ -2445,13 +2811,47 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2445
2811
|
}
|
|
2446
2812
|
|
|
2447
2813
|
autoCollapseCoordinates() {
|
|
2814
|
+
// Don't collapse if there are errors - they should remain visible
|
|
2815
|
+
if (this._hasErrors()) {
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2448
2818
|
this._applyCollapsedOption(['coordinates']);
|
|
2449
2819
|
}
|
|
2450
2820
|
|
|
2821
|
+
/**
|
|
2822
|
+
* Check if current content has any errors (JSON parse errors or syntax highlighting errors)
|
|
2823
|
+
*/
|
|
2824
|
+
private _hasErrors(): boolean {
|
|
2825
|
+
// Check JSON parse errors
|
|
2826
|
+
try {
|
|
2827
|
+
const content = this.lines.join('\n');
|
|
2828
|
+
const wrapped = '[' + content + ']';
|
|
2829
|
+
JSON.parse(wrapped);
|
|
2830
|
+
} catch {
|
|
2831
|
+
return true;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// Check for syntax highlighting errors (json-error class)
|
|
2835
|
+
for (const line of this.lines) {
|
|
2836
|
+
const highlighted = highlightSyntax(line, '', undefined);
|
|
2837
|
+
if (highlighted.includes('json-error')) {
|
|
2838
|
+
return true;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
return false;
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2451
2845
|
/**
|
|
2452
2846
|
* Helper to apply collapsed option from API methods
|
|
2847
|
+
* Does not collapse if there are errors (so they remain visible)
|
|
2453
2848
|
*/
|
|
2454
2849
|
private _applyCollapsedFromOptions(options: SetOptions, features: Feature[]): void {
|
|
2850
|
+
// Don't collapse if there are errors - they should remain visible
|
|
2851
|
+
if (this._hasErrors()) {
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2455
2855
|
const collapsed = options.collapsed !== undefined ? options.collapsed : ['coordinates'];
|
|
2456
2856
|
if (collapsed && (Array.isArray(collapsed) ? collapsed.length > 0 : true)) {
|
|
2457
2857
|
this._applyCollapsedOption(collapsed, features);
|
|
@@ -2615,23 +3015,223 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2615
3015
|
}
|
|
2616
3016
|
|
|
2617
3017
|
// ========== Format and Update ==========
|
|
2618
|
-
|
|
3018
|
+
|
|
3019
|
+
/**
|
|
3020
|
+
* Best-effort formatting for invalid JSON
|
|
3021
|
+
* Splits on structural characters and indents as much as possible
|
|
3022
|
+
* @param content The content to format
|
|
3023
|
+
* @param skipLineIndex Optional line index to skip (keep as-is)
|
|
3024
|
+
*/
|
|
3025
|
+
private _bestEffortFormat(content: string, skipLineIndex?: number): string[] {
|
|
3026
|
+
const sourceLines = content.split('\n');
|
|
3027
|
+
|
|
3028
|
+
// If we have a line to skip, handle it specially
|
|
3029
|
+
if (skipLineIndex !== undefined && skipLineIndex >= 0 && skipLineIndex < sourceLines.length) {
|
|
3030
|
+
const skippedLine = sourceLines[skipLineIndex];
|
|
3031
|
+
|
|
3032
|
+
// Format content before the skipped line
|
|
3033
|
+
const beforeContent = sourceLines.slice(0, skipLineIndex).join('\n');
|
|
3034
|
+
const beforeLines = beforeContent.trim() ? this._formatChunk(beforeContent) : [];
|
|
3035
|
+
|
|
3036
|
+
// Keep skipped line exactly as-is (don't re-indent, user is typing on it)
|
|
3037
|
+
const depthBefore = this._computeDepthAtEnd(beforeLines);
|
|
3038
|
+
|
|
3039
|
+
// Compute depth after the skipped line (including its brackets)
|
|
3040
|
+
const depthAfterSkipped = depthBefore + this._computeBracketDelta(skippedLine);
|
|
3041
|
+
|
|
3042
|
+
// Format content after the skipped line, starting at correct depth
|
|
3043
|
+
const afterContent = sourceLines.slice(skipLineIndex + 1).join('\n');
|
|
3044
|
+
const afterLines = afterContent.trim() ? this._formatChunk(afterContent, depthAfterSkipped) : [];
|
|
3045
|
+
|
|
3046
|
+
return [...beforeLines, skippedLine, ...afterLines];
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
// No line to skip - format everything
|
|
3050
|
+
return this._formatChunk(content);
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
/**
|
|
3054
|
+
* Compute the net bracket delta for a line (opens - closes)
|
|
3055
|
+
*/
|
|
3056
|
+
private _computeBracketDelta(line: string): number {
|
|
3057
|
+
let delta = 0;
|
|
3058
|
+
let inString = false;
|
|
3059
|
+
let escaped = false;
|
|
3060
|
+
for (const char of line) {
|
|
3061
|
+
if (escaped) { escaped = false; continue; }
|
|
3062
|
+
if (char === '\\' && inString) { escaped = true; continue; }
|
|
3063
|
+
if (char === '"') { inString = !inString; continue; }
|
|
3064
|
+
if (inString) continue;
|
|
3065
|
+
if (char === '{' || char === '[') delta++;
|
|
3066
|
+
else if (char === '}' || char === ']') delta--;
|
|
3067
|
+
}
|
|
3068
|
+
return delta;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
/**
|
|
3072
|
+
* Compute the bracket depth at the end of formatted lines
|
|
3073
|
+
* Starts at 1 to account for FeatureCollection wrapper
|
|
3074
|
+
*/
|
|
3075
|
+
private _computeDepthAtEnd(lines: string[]): number {
|
|
3076
|
+
let depth = 1; // Start at 1 for FeatureCollection wrapper
|
|
3077
|
+
for (const line of lines) {
|
|
3078
|
+
for (const char of line) {
|
|
3079
|
+
if (char === '{' || char === '[') depth++;
|
|
3080
|
+
else if (char === '}' || char === ']') depth = Math.max(0, depth - 1);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
return depth;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
/**
|
|
3087
|
+
* Format a chunk of JSON content
|
|
3088
|
+
* @param content The content to format
|
|
3089
|
+
* @param initialDepth Starting indentation depth (default 1 for FeatureCollection wrapper)
|
|
3090
|
+
*/
|
|
3091
|
+
private _formatChunk(content: string, initialDepth: number = 1): string[] {
|
|
3092
|
+
const result: string[] = [];
|
|
3093
|
+
let currentLine = '';
|
|
3094
|
+
let depth = initialDepth;
|
|
3095
|
+
let inString = false;
|
|
3096
|
+
let escaped = false;
|
|
3097
|
+
|
|
3098
|
+
for (let i = 0; i < content.length; i++) {
|
|
3099
|
+
const char = content[i];
|
|
3100
|
+
|
|
3101
|
+
// Track escape sequences inside strings
|
|
3102
|
+
if (escaped) {
|
|
3103
|
+
currentLine += char;
|
|
3104
|
+
escaped = false;
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
if (char === '\\' && inString) {
|
|
3109
|
+
currentLine += char;
|
|
3110
|
+
escaped = true;
|
|
3111
|
+
continue;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
// Track if we're inside a string
|
|
3115
|
+
if (char === '"') {
|
|
3116
|
+
inString = !inString;
|
|
3117
|
+
currentLine += char;
|
|
3118
|
+
continue;
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
// Inside string - just append
|
|
3122
|
+
if (inString) {
|
|
3123
|
+
currentLine += char;
|
|
3124
|
+
continue;
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// Outside string - handle structural characters
|
|
3128
|
+
if (char === '{' || char === '[') {
|
|
3129
|
+
currentLine += char;
|
|
3130
|
+
result.push(' '.repeat(depth) + currentLine.trim());
|
|
3131
|
+
depth++;
|
|
3132
|
+
currentLine = '';
|
|
3133
|
+
} else if (char === '}' || char === ']') {
|
|
3134
|
+
if (currentLine.trim()) {
|
|
3135
|
+
result.push(' '.repeat(depth) + currentLine.trim());
|
|
3136
|
+
}
|
|
3137
|
+
depth = Math.max(0, depth - 1);
|
|
3138
|
+
currentLine = char;
|
|
3139
|
+
} else if (char === ',') {
|
|
3140
|
+
currentLine += char;
|
|
3141
|
+
result.push(' '.repeat(depth) + currentLine.trim());
|
|
3142
|
+
currentLine = '';
|
|
3143
|
+
} else if (char === ':') {
|
|
3144
|
+
currentLine += ': '; // Add space after colon for readability
|
|
3145
|
+
i++; // Skip if next char is space
|
|
3146
|
+
if (content[i] === ' ') continue;
|
|
3147
|
+
i--; // Not a space, go back
|
|
3148
|
+
} else if (char === '\n' || char === '\r') {
|
|
3149
|
+
// Ignore existing newlines
|
|
3150
|
+
continue;
|
|
3151
|
+
} else {
|
|
3152
|
+
currentLine += char;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// Don't forget last line
|
|
3157
|
+
if (currentLine.trim()) {
|
|
3158
|
+
result.push(' '.repeat(depth) + currentLine.trim());
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
return result;
|
|
3162
|
+
}
|
|
3163
|
+
|
|
2619
3164
|
formatAndUpdate() {
|
|
3165
|
+
// Save cursor position
|
|
3166
|
+
const oldCursorLine = this.cursorLine;
|
|
3167
|
+
const oldCursorColumn = this.cursorColumn;
|
|
3168
|
+
const oldContent = this.lines.join('\n');
|
|
3169
|
+
|
|
2620
3170
|
try {
|
|
2621
|
-
const
|
|
2622
|
-
const wrapped = '[' + content + ']';
|
|
3171
|
+
const wrapped = '[' + oldContent + ']';
|
|
2623
3172
|
const parsed = JSON.parse(wrapped);
|
|
2624
|
-
|
|
3173
|
+
|
|
2625
3174
|
const formatted = JSON.stringify(parsed, null, 2);
|
|
2626
3175
|
const lines = formatted.split('\n');
|
|
2627
3176
|
this.lines = lines.slice(1, -1); // Remove wrapper brackets
|
|
2628
|
-
} catch
|
|
2629
|
-
// Invalid JSON
|
|
3177
|
+
} catch {
|
|
3178
|
+
// Invalid JSON - apply best-effort formatting
|
|
3179
|
+
if (oldContent.trim()) {
|
|
3180
|
+
// Skip the cursor line only for small content (typing, not paste)
|
|
3181
|
+
// This avoids text jumping while user is typing
|
|
3182
|
+
// For paste/large insertions, format everything for proper structure
|
|
3183
|
+
const cursorLineContent = this.lines[oldCursorLine] || '';
|
|
3184
|
+
// If cursor line is short, likely typing. Long lines = paste
|
|
3185
|
+
const isSmallEdit = cursorLineContent.length < 80;
|
|
3186
|
+
const skipLine = isSmallEdit ? oldCursorLine : undefined;
|
|
3187
|
+
this.lines = this._bestEffortFormat(oldContent, skipLine);
|
|
3188
|
+
}
|
|
2630
3189
|
}
|
|
2631
|
-
|
|
3190
|
+
|
|
3191
|
+
const newContent = this.lines.join('\n');
|
|
3192
|
+
|
|
3193
|
+
// If content didn't change, keep cursor exactly where it was
|
|
3194
|
+
if (newContent === oldContent) {
|
|
3195
|
+
this.cursorLine = oldCursorLine;
|
|
3196
|
+
this.cursorColumn = oldCursorColumn;
|
|
3197
|
+
} else {
|
|
3198
|
+
// Content changed due to reformatting
|
|
3199
|
+
// The cursor position (this.cursorLine, this.cursorColumn) was set by the calling
|
|
3200
|
+
// operation (insertText, insertNewline, etc.) BEFORE formatAndUpdate was called.
|
|
3201
|
+
// We need to adjust for indentation changes while keeping the logical position.
|
|
3202
|
+
|
|
3203
|
+
// If cursor is at column 0 (e.g., after newline), keep it there
|
|
3204
|
+
// This preserves expected behavior for newline insertion
|
|
3205
|
+
if (this.cursorColumn === 0) {
|
|
3206
|
+
// Just keep line, column 0 - indentation will be handled by auto-indent
|
|
3207
|
+
} else {
|
|
3208
|
+
// For other cases, try to maintain position relative to content (not indentation)
|
|
3209
|
+
const oldLines = oldContent.split('\n');
|
|
3210
|
+
const oldLineContent = oldLines[oldCursorLine] || '';
|
|
3211
|
+
const oldLeadingSpaces = oldLineContent.length - oldLineContent.trimStart().length;
|
|
3212
|
+
const oldColumnInContent = Math.max(0, oldCursorColumn - oldLeadingSpaces);
|
|
3213
|
+
|
|
3214
|
+
// Apply same offset to new line's indentation
|
|
3215
|
+
if (this.cursorLine < this.lines.length) {
|
|
3216
|
+
const newLineContent = this.lines[this.cursorLine];
|
|
3217
|
+
const newLeadingSpaces = newLineContent.length - newLineContent.trimStart().length;
|
|
3218
|
+
this.cursorColumn = newLeadingSpaces + oldColumnInContent;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// Clamp cursor to valid range
|
|
3224
|
+
this.cursorLine = Math.min(this.cursorLine, Math.max(0, this.lines.length - 1));
|
|
3225
|
+
this.cursorColumn = Math.min(this.cursorColumn, this.lines[this.cursorLine]?.length || 0);
|
|
3226
|
+
|
|
2632
3227
|
this.updateModel();
|
|
3228
|
+
|
|
3229
|
+
// Expand any nodes that contain errors (prevents closing edited nodes with typos)
|
|
3230
|
+
this._expandErrorNodes();
|
|
3231
|
+
|
|
2633
3232
|
this.scheduleRender();
|
|
2634
3233
|
this.updatePlaceholderVisibility();
|
|
3234
|
+
this._updateErrorDisplay();
|
|
2635
3235
|
this.emitChange();
|
|
2636
3236
|
}
|
|
2637
3237
|
|
|
@@ -2691,6 +3291,21 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2691
3291
|
}
|
|
2692
3292
|
}
|
|
2693
3293
|
|
|
3294
|
+
/**
|
|
3295
|
+
* Update error display (counter and navigation visibility)
|
|
3296
|
+
*/
|
|
3297
|
+
private _updateErrorDisplay() {
|
|
3298
|
+
const errorLines = this._getErrorLines();
|
|
3299
|
+
const count = errorLines.length;
|
|
3300
|
+
|
|
3301
|
+
if (this._errorNav) {
|
|
3302
|
+
this._errorNav.classList.toggle('visible', count > 0);
|
|
3303
|
+
}
|
|
3304
|
+
if (this._errorCount) {
|
|
3305
|
+
this._errorCount.textContent = count > 0 ? String(count) : '';
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
|
|
2694
3309
|
updatePlaceholderContent() {
|
|
2695
3310
|
if (this._placeholderLayer) {
|
|
2696
3311
|
this._placeholderLayer.textContent = this.placeholder;
|