@softwarity/geojson-editor 1.0.21 → 1.0.23
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 +4 -2
- package/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.css +45 -0
- package/src/geojson-editor.template.ts +7 -0
- package/src/geojson-editor.ts +495 -96
- package/src/internal-types.ts +2 -2
- package/src/utils.ts +0 -21
package/src/geojson-editor.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
RE_CLOSE_BRACKET
|
|
43
43
|
} from './constants.js';
|
|
44
44
|
|
|
45
|
-
import { createElement,
|
|
45
|
+
import { createElement, countBrackets, parseSelectorToHostRule } from './utils.js';
|
|
46
46
|
import { validateGeoJSON, normalizeToFeatures } from './validation.js';
|
|
47
47
|
import { highlightSyntax, namedColorToHex, isNamedColor } from './syntax-highlighter.js';
|
|
48
48
|
|
|
@@ -60,7 +60,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
60
60
|
// ========== Model (Source of Truth) ==========
|
|
61
61
|
lines: string[] = [];
|
|
62
62
|
collapsedNodes: Set<string> = new Set();
|
|
63
|
-
hiddenFeatures: Set<
|
|
63
|
+
hiddenFeatures: Set<number> = new Set(); // Feature indices that are hidden
|
|
64
64
|
|
|
65
65
|
// ========== Node ID Management ==========
|
|
66
66
|
private _nodeIdCounter: number = 0;
|
|
@@ -71,7 +71,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
71
71
|
// ========== Derived State (computed from model) ==========
|
|
72
72
|
visibleLines: VisibleLine[] = [];
|
|
73
73
|
lineMetadata: Map<number, LineMeta> = new Map();
|
|
74
|
-
featureRanges: Map<
|
|
74
|
+
featureRanges: Map<number, FeatureRange> = new Map(); // featureIndex -> range
|
|
75
75
|
|
|
76
76
|
// ========== View State ==========
|
|
77
77
|
viewportHeight: number = 0;
|
|
@@ -621,34 +621,75 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
621
621
|
this.scheduleRender();
|
|
622
622
|
});
|
|
623
623
|
|
|
624
|
-
//
|
|
625
|
-
|
|
624
|
+
// Auto-scroll interval for drag selection outside editor
|
|
625
|
+
let autoScrollInterval: ReturnType<typeof setInterval> | null = null;
|
|
626
|
+
|
|
627
|
+
const stopAutoScroll = () => {
|
|
628
|
+
if (autoScrollInterval) {
|
|
629
|
+
clearInterval(autoScrollInterval);
|
|
630
|
+
autoScrollInterval = null;
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
const startAutoScroll = (direction: 'up' | 'down') => {
|
|
635
|
+
stopAutoScroll();
|
|
636
|
+
const scrollSpeed = 20;
|
|
637
|
+
autoScrollInterval = setInterval(() => {
|
|
638
|
+
if (!this._isSelecting) {
|
|
639
|
+
stopAutoScroll();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (direction === 'up') {
|
|
643
|
+
viewport.scrollTop -= scrollSpeed;
|
|
644
|
+
} else {
|
|
645
|
+
viewport.scrollTop += scrollSpeed;
|
|
646
|
+
}
|
|
647
|
+
// Update selection based on scroll position
|
|
648
|
+
this._updateSelectionFromScroll(direction);
|
|
649
|
+
this._invalidateRenderCache();
|
|
650
|
+
this.scheduleRender();
|
|
651
|
+
}, 50);
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
// Mouse move for drag selection - listen on document to handle drag outside editor
|
|
655
|
+
document.addEventListener('mousemove', (e: MouseEvent) => {
|
|
626
656
|
if (!this._isSelecting) return;
|
|
627
|
-
const pos = this._getPositionFromClick(e);
|
|
628
|
-
this.selectionEnd = pos;
|
|
629
|
-
this.cursorLine = pos.line;
|
|
630
|
-
this.cursorColumn = pos.column;
|
|
631
657
|
|
|
632
|
-
// Auto-scroll when near edges
|
|
633
658
|
const rect = viewport.getBoundingClientRect();
|
|
634
|
-
const scrollMargin = 30;
|
|
635
|
-
const scrollSpeed = 20;
|
|
659
|
+
const scrollMargin = 30;
|
|
660
|
+
const scrollSpeed = 20;
|
|
661
|
+
|
|
662
|
+
// Check if mouse is outside the viewport
|
|
663
|
+
if (e.clientY < rect.top) {
|
|
664
|
+
// Mouse above viewport - start continuous scroll up
|
|
665
|
+
startAutoScroll('up');
|
|
666
|
+
} else if (e.clientY > rect.bottom) {
|
|
667
|
+
// Mouse below viewport - start continuous scroll down
|
|
668
|
+
startAutoScroll('down');
|
|
669
|
+
} else {
|
|
670
|
+
// Mouse inside viewport - stop auto-scroll and update normally
|
|
671
|
+
stopAutoScroll();
|
|
672
|
+
const pos = this._getPositionFromClick(e);
|
|
673
|
+
this.selectionEnd = pos;
|
|
674
|
+
this.cursorLine = pos.line;
|
|
675
|
+
this.cursorColumn = pos.column;
|
|
636
676
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
}
|
|
677
|
+
// Auto-scroll when near edges (inside viewport)
|
|
678
|
+
if (e.clientY < rect.top + scrollMargin) {
|
|
679
|
+
viewport.scrollTop -= scrollSpeed;
|
|
680
|
+
} else if (e.clientY > rect.bottom - scrollMargin) {
|
|
681
|
+
viewport.scrollTop += scrollSpeed;
|
|
682
|
+
}
|
|
644
683
|
|
|
645
|
-
|
|
646
|
-
|
|
684
|
+
this._invalidateRenderCache();
|
|
685
|
+
this.scheduleRender();
|
|
686
|
+
}
|
|
647
687
|
});
|
|
648
|
-
|
|
688
|
+
|
|
649
689
|
// Mouse up to end selection
|
|
650
690
|
document.addEventListener('mouseup', () => {
|
|
651
691
|
this._isSelecting = false;
|
|
692
|
+
stopAutoScroll();
|
|
652
693
|
});
|
|
653
694
|
|
|
654
695
|
// Focus/blur handling to show/hide cursor
|
|
@@ -739,6 +780,20 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
739
780
|
this.removeAll();
|
|
740
781
|
});
|
|
741
782
|
|
|
783
|
+
// Info button - toggle popup
|
|
784
|
+
const infoBtn = this._id('infoBtn');
|
|
785
|
+
const infoPopup = this._id('infoPopup');
|
|
786
|
+
if (infoBtn && infoPopup) {
|
|
787
|
+
infoBtn.addEventListener('click', (e: MouseEvent) => {
|
|
788
|
+
e.stopPropagation();
|
|
789
|
+
infoPopup.classList.toggle('visible');
|
|
790
|
+
});
|
|
791
|
+
// Close popup when clicking outside
|
|
792
|
+
document.addEventListener('click', () => {
|
|
793
|
+
infoPopup.classList.remove('visible');
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
742
797
|
// Error navigation buttons
|
|
743
798
|
this._prevErrorBtn?.addEventListener('click', () => {
|
|
744
799
|
this.goToPrevError();
|
|
@@ -840,23 +895,22 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
840
895
|
*/
|
|
841
896
|
computeFeatureRanges() {
|
|
842
897
|
this.featureRanges.clear();
|
|
843
|
-
|
|
898
|
+
|
|
844
899
|
try {
|
|
845
900
|
const content = this.lines.join('\n');
|
|
846
901
|
const fullValue = this.prefix + content + this.suffix;
|
|
847
902
|
const parsed = JSON.parse(fullValue);
|
|
848
|
-
|
|
903
|
+
|
|
849
904
|
if (!parsed.features) return;
|
|
850
|
-
|
|
905
|
+
|
|
851
906
|
let featureIndex = 0;
|
|
852
907
|
let braceDepth = 0;
|
|
853
908
|
let inFeature = false;
|
|
854
909
|
let featureStartLine = -1;
|
|
855
|
-
|
|
856
|
-
|
|
910
|
+
|
|
857
911
|
for (let i = 0; i < this.lines.length; i++) {
|
|
858
912
|
const line = this.lines[i];
|
|
859
|
-
|
|
913
|
+
|
|
860
914
|
if (!inFeature && RE_IS_FEATURE.test(line)) {
|
|
861
915
|
// Find opening brace
|
|
862
916
|
let startLine = i;
|
|
@@ -870,7 +924,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
870
924
|
featureStartLine = startLine;
|
|
871
925
|
inFeature = true;
|
|
872
926
|
braceDepth = 1;
|
|
873
|
-
|
|
927
|
+
|
|
874
928
|
// Count braces from start to current line
|
|
875
929
|
for (let k = startLine; k <= i; k++) {
|
|
876
930
|
const counts = countBrackets(this.lines[k], '{');
|
|
@@ -880,25 +934,19 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
880
934
|
braceDepth += counts.open - counts.close;
|
|
881
935
|
}
|
|
882
936
|
}
|
|
883
|
-
|
|
884
|
-
if (featureIndex < parsed.features.length) {
|
|
885
|
-
currentFeatureKey = getFeatureKey(parsed.features[featureIndex]);
|
|
886
|
-
}
|
|
887
937
|
} else if (inFeature) {
|
|
888
938
|
const counts = countBrackets(line, '{');
|
|
889
939
|
braceDepth += counts.open - counts.close;
|
|
890
|
-
|
|
940
|
+
|
|
891
941
|
if (braceDepth <= 0) {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
942
|
+
// Store by featureIndex instead of hash key
|
|
943
|
+
this.featureRanges.set(featureIndex, {
|
|
944
|
+
startLine: featureStartLine,
|
|
945
|
+
endLine: i,
|
|
946
|
+
featureIndex
|
|
947
|
+
});
|
|
899
948
|
featureIndex++;
|
|
900
949
|
inFeature = false;
|
|
901
|
-
currentFeatureKey = null;
|
|
902
950
|
}
|
|
903
951
|
}
|
|
904
952
|
}
|
|
@@ -927,7 +975,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
927
975
|
visibilityButton: null,
|
|
928
976
|
isHidden: false,
|
|
929
977
|
isCollapsed: false,
|
|
930
|
-
|
|
978
|
+
featureIndex: null,
|
|
931
979
|
hasError: errorLines.has(i)
|
|
932
980
|
};
|
|
933
981
|
|
|
@@ -970,17 +1018,17 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
970
1018
|
}
|
|
971
1019
|
|
|
972
1020
|
// Check if line belongs to a hidden feature
|
|
973
|
-
for (const [
|
|
1021
|
+
for (const [featureIndex, range] of this.featureRanges) {
|
|
974
1022
|
if (i >= range.startLine && i <= range.endLine) {
|
|
975
|
-
meta.
|
|
976
|
-
if (this.hiddenFeatures.has(
|
|
1023
|
+
meta.featureIndex = featureIndex;
|
|
1024
|
+
if (this.hiddenFeatures.has(featureIndex)) {
|
|
977
1025
|
meta.isHidden = true;
|
|
978
1026
|
}
|
|
979
1027
|
// Add visibility button only on feature start line
|
|
980
1028
|
if (i === range.startLine) {
|
|
981
1029
|
meta.visibilityButton = {
|
|
982
|
-
|
|
983
|
-
isHidden: this.hiddenFeatures.has(
|
|
1030
|
+
featureIndex,
|
|
1031
|
+
isHidden: this.hiddenFeatures.has(featureIndex)
|
|
984
1032
|
};
|
|
985
1033
|
}
|
|
986
1034
|
break;
|
|
@@ -1244,7 +1292,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1244
1292
|
// Add visibility button on line (uses ::before pseudo-element)
|
|
1245
1293
|
if (lineData.meta?.visibilityButton) {
|
|
1246
1294
|
lineEl.classList.add('has-visibility');
|
|
1247
|
-
lineEl.dataset.
|
|
1295
|
+
lineEl.dataset.featureIndex = String(lineData.meta.visibilityButton.featureIndex);
|
|
1248
1296
|
if (lineData.meta.visibilityButton.isHidden) {
|
|
1249
1297
|
lineEl.classList.add('feature-hidden');
|
|
1250
1298
|
}
|
|
@@ -1529,8 +1577,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1529
1577
|
'ArrowDown': () => this._handleArrowKey(1, 0, e.shiftKey, e.ctrlKey || e.metaKey),
|
|
1530
1578
|
'ArrowLeft': () => this._handleArrowKey(0, -1, e.shiftKey, e.ctrlKey || e.metaKey),
|
|
1531
1579
|
'ArrowRight': () => this._handleArrowKey(0, 1, e.shiftKey, e.ctrlKey || e.metaKey),
|
|
1532
|
-
'Home': () => this._handleHomeEnd('home', e.shiftKey, ctx.onClosingLine),
|
|
1533
|
-
'End': () => this._handleHomeEnd('end', e.shiftKey, ctx.onClosingLine),
|
|
1580
|
+
'Home': () => this._handleHomeEnd('home', e.shiftKey, e.ctrlKey || e.metaKey, ctx.onClosingLine),
|
|
1581
|
+
'End': () => this._handleHomeEnd('end', e.shiftKey, e.ctrlKey || e.metaKey, ctx.onClosingLine),
|
|
1534
1582
|
'PageUp': () => this._handlePageUpDown('up', e.shiftKey),
|
|
1535
1583
|
'PageDown': () => this._handlePageUpDown('down', e.shiftKey),
|
|
1536
1584
|
'Tab': () => this._handleTab(e.shiftKey, ctx),
|
|
@@ -2366,34 +2414,38 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2366
2414
|
|
|
2367
2415
|
/**
|
|
2368
2416
|
* Handle Home/End with optional selection
|
|
2417
|
+
* @param key - 'home' or 'end'
|
|
2418
|
+
* @param isShift - Shift key pressed (for selection)
|
|
2419
|
+
* @param isCtrl - Ctrl/Cmd key pressed (for document start/end)
|
|
2420
|
+
* @param onClosingLine - Collapsed node info if on closing line
|
|
2369
2421
|
*/
|
|
2370
|
-
private _handleHomeEnd(key: string, isShift: boolean, onClosingLine: CollapsedNodeInfo | null): void {
|
|
2422
|
+
private _handleHomeEnd(key: string, isShift: boolean, isCtrl: boolean, onClosingLine: CollapsedNodeInfo | null): void {
|
|
2371
2423
|
// Start selection if shift is pressed and no selection exists
|
|
2372
2424
|
if (isShift && !this.selectionStart) {
|
|
2373
2425
|
this.selectionStart = { line: this.cursorLine, column: this.cursorColumn };
|
|
2374
2426
|
}
|
|
2375
2427
|
|
|
2376
2428
|
if (key === 'home') {
|
|
2377
|
-
if (
|
|
2429
|
+
if (isCtrl) {
|
|
2430
|
+
// Ctrl+Home: go to start of document
|
|
2431
|
+
this.cursorLine = 0;
|
|
2432
|
+
this.cursorColumn = 0;
|
|
2433
|
+
} else if (onClosingLine) {
|
|
2378
2434
|
// On closing line of collapsed node: go to start line
|
|
2379
2435
|
this.cursorLine = onClosingLine.startLine;
|
|
2380
2436
|
this.cursorColumn = 0;
|
|
2381
|
-
} else if (this.cursorColumn === 0) {
|
|
2382
|
-
// Already at start of line: go to start of document
|
|
2383
|
-
this.cursorLine = 0;
|
|
2384
|
-
this.cursorColumn = 0;
|
|
2385
2437
|
} else {
|
|
2386
2438
|
// Go to start of line
|
|
2387
2439
|
this.cursorColumn = 0;
|
|
2388
2440
|
}
|
|
2389
2441
|
} else {
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
// Already at end of line: go to end of document
|
|
2442
|
+
if (isCtrl) {
|
|
2443
|
+
// Ctrl+End: go to end of document
|
|
2393
2444
|
this.cursorLine = this.lines.length - 1;
|
|
2394
2445
|
this.cursorColumn = this.lines[this.cursorLine]?.length || 0;
|
|
2395
2446
|
} else {
|
|
2396
2447
|
// Go to end of line
|
|
2448
|
+
const lineLength = this.lines[this.cursorLine]?.length || 0;
|
|
2397
2449
|
this.cursorColumn = lineLength;
|
|
2398
2450
|
}
|
|
2399
2451
|
}
|
|
@@ -2663,9 +2715,25 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2663
2715
|
this.cursorLine = textLines.length - 1;
|
|
2664
2716
|
this.cursorColumn = textLines[textLines.length - 1].length;
|
|
2665
2717
|
} else if (this.cursorLine < this.lines.length) {
|
|
2718
|
+
const textLines = text.split('\n');
|
|
2666
2719
|
const line = this.lines[this.cursorLine];
|
|
2667
|
-
|
|
2668
|
-
this.cursorColumn
|
|
2720
|
+
const before = line.substring(0, this.cursorColumn);
|
|
2721
|
+
const after = line.substring(this.cursorColumn);
|
|
2722
|
+
|
|
2723
|
+
if (textLines.length === 1) {
|
|
2724
|
+
// Single line insertion
|
|
2725
|
+
this.lines[this.cursorLine] = before + text + after;
|
|
2726
|
+
this.cursorColumn += text.length;
|
|
2727
|
+
} else {
|
|
2728
|
+
// Multi-line insertion
|
|
2729
|
+
const firstLine = before + textLines[0];
|
|
2730
|
+
const lastLine = textLines[textLines.length - 1] + after;
|
|
2731
|
+
const middleLines = textLines.slice(1, -1);
|
|
2732
|
+
|
|
2733
|
+
this.lines.splice(this.cursorLine, 1, firstLine, ...middleLines, lastLine);
|
|
2734
|
+
this.cursorLine += textLines.length - 1;
|
|
2735
|
+
this.cursorColumn = textLines[textLines.length - 1].length;
|
|
2736
|
+
}
|
|
2669
2737
|
}
|
|
2670
2738
|
this.formatAndUpdate();
|
|
2671
2739
|
}
|
|
@@ -2675,12 +2743,23 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2675
2743
|
const text = e.clipboardData?.getData('text/plain');
|
|
2676
2744
|
if (!text) return;
|
|
2677
2745
|
|
|
2678
|
-
|
|
2746
|
+
// Save collapsed state of existing features before paste
|
|
2747
|
+
const existingCollapsedKeys = new Set<string>();
|
|
2748
|
+
for (const nodeId of this.collapsedNodes) {
|
|
2749
|
+
const nodeInfo = this._nodeIdToLines.get(nodeId);
|
|
2750
|
+
if (nodeInfo?.nodeKey) {
|
|
2751
|
+
const featureIndex = this._getFeatureIndexForLine(nodeInfo.startLine);
|
|
2752
|
+
existingCollapsedKeys.add(`${featureIndex}:${nodeInfo.nodeKey}`);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
const existingFeatureCount = this._parseFeatures().length;
|
|
2679
2756
|
|
|
2680
2757
|
// Try to parse as GeoJSON and normalize
|
|
2758
|
+
let pastedFeatureCount = 0;
|
|
2681
2759
|
try {
|
|
2682
2760
|
const parsed = JSON.parse(text);
|
|
2683
2761
|
const features = normalizeToFeatures(parsed);
|
|
2762
|
+
pastedFeatureCount = features.length;
|
|
2684
2763
|
// Valid GeoJSON - insert formatted features
|
|
2685
2764
|
const formatted = features.map(f => JSON.stringify(f, null, 2)).join(',\n');
|
|
2686
2765
|
this.insertText(formatted);
|
|
@@ -2695,9 +2774,35 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2695
2774
|
this.renderTimer = undefined;
|
|
2696
2775
|
}
|
|
2697
2776
|
|
|
2698
|
-
// Auto-collapse coordinates
|
|
2699
|
-
if
|
|
2700
|
-
|
|
2777
|
+
// Auto-collapse coordinates for pasted features (if valid GeoJSON was pasted)
|
|
2778
|
+
// Note: We collapse even if there are errors (e.g., missing comma) because
|
|
2779
|
+
// the user will fix them and we want the coordinates already collapsed
|
|
2780
|
+
if (pastedFeatureCount > 0) {
|
|
2781
|
+
// Restore collapsed state for existing features and collapse new features' coordinates
|
|
2782
|
+
const ranges = this._findCollapsibleRanges();
|
|
2783
|
+
const featureRanges = ranges.filter(r => r.isRootFeature);
|
|
2784
|
+
|
|
2785
|
+
for (const range of ranges) {
|
|
2786
|
+
// Find which feature this range belongs to
|
|
2787
|
+
const featureIndex = featureRanges.findIndex(fr =>
|
|
2788
|
+
range.startLine >= fr.startLine && range.endLine <= fr.endLine
|
|
2789
|
+
);
|
|
2790
|
+
|
|
2791
|
+
if (featureIndex < existingFeatureCount) {
|
|
2792
|
+
// Existing feature - restore collapsed state
|
|
2793
|
+
const key = `${featureIndex}:${range.nodeKey}`;
|
|
2794
|
+
if (existingCollapsedKeys.has(key)) {
|
|
2795
|
+
this.collapsedNodes.add(range.nodeId);
|
|
2796
|
+
}
|
|
2797
|
+
} else {
|
|
2798
|
+
// New feature - collapse coordinates
|
|
2799
|
+
if (range.nodeKey === 'coordinates') {
|
|
2800
|
+
this.collapsedNodes.add(range.nodeId);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
this.updateView();
|
|
2701
2806
|
}
|
|
2702
2807
|
|
|
2703
2808
|
// Expand any collapsed nodes that contain errors
|
|
@@ -2782,6 +2887,28 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2782
2887
|
return { line, column };
|
|
2783
2888
|
}
|
|
2784
2889
|
|
|
2890
|
+
/**
|
|
2891
|
+
* Update selection during auto-scroll when dragging outside editor
|
|
2892
|
+
*/
|
|
2893
|
+
private _updateSelectionFromScroll(direction: 'up' | 'down'): void {
|
|
2894
|
+
if (!this.visibleLines.length) return;
|
|
2895
|
+
|
|
2896
|
+
if (direction === 'up') {
|
|
2897
|
+
// Scrolling up - select to first visible line
|
|
2898
|
+
const firstVisible = this.visibleLines[0];
|
|
2899
|
+
this.selectionEnd = { line: firstVisible.index, column: 0 };
|
|
2900
|
+
this.cursorLine = firstVisible.index;
|
|
2901
|
+
this.cursorColumn = 0;
|
|
2902
|
+
} else {
|
|
2903
|
+
// Scrolling down - select to last visible line
|
|
2904
|
+
const lastVisible = this.visibleLines[this.visibleLines.length - 1];
|
|
2905
|
+
const lineLength = lastVisible.content?.length || 0;
|
|
2906
|
+
this.selectionEnd = { line: lastVisible.index, column: lineLength };
|
|
2907
|
+
this.cursorLine = lastVisible.index;
|
|
2908
|
+
this.cursorColumn = lineLength;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2785
2912
|
// ========== Gutter Interactions ==========
|
|
2786
2913
|
|
|
2787
2914
|
handleGutterClick(e: MouseEvent): void {
|
|
@@ -2790,8 +2917,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2790
2917
|
|
|
2791
2918
|
// Visibility button in gutter
|
|
2792
2919
|
const visBtn = target.closest('.visibility-button') as HTMLElement | null;
|
|
2793
|
-
if (visBtn) {
|
|
2794
|
-
this.toggleFeatureVisibility(visBtn.dataset.
|
|
2920
|
+
if (visBtn && visBtn.dataset.featureIndex !== undefined) {
|
|
2921
|
+
this.toggleFeatureVisibility(parseInt(visBtn.dataset.featureIndex, 10));
|
|
2795
2922
|
return;
|
|
2796
2923
|
}
|
|
2797
2924
|
|
|
@@ -2818,9 +2945,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2818
2945
|
if (clickX < 14) {
|
|
2819
2946
|
e.preventDefault();
|
|
2820
2947
|
e.stopPropagation();
|
|
2821
|
-
const
|
|
2822
|
-
if (
|
|
2823
|
-
this.toggleFeatureVisibility(
|
|
2948
|
+
const featureIndexStr = lineEl.dataset.featureIndex;
|
|
2949
|
+
if (featureIndexStr !== undefined) {
|
|
2950
|
+
this.toggleFeatureVisibility(parseInt(featureIndexStr, 10));
|
|
2824
2951
|
}
|
|
2825
2952
|
return;
|
|
2826
2953
|
}
|
|
@@ -2993,12 +3120,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2993
3120
|
|
|
2994
3121
|
// ========== Feature Visibility ==========
|
|
2995
3122
|
|
|
2996
|
-
toggleFeatureVisibility(
|
|
2997
|
-
if (
|
|
2998
|
-
if (this.hiddenFeatures.has(
|
|
2999
|
-
this.hiddenFeatures.delete(
|
|
3123
|
+
toggleFeatureVisibility(featureIndex: number | undefined): void {
|
|
3124
|
+
if (featureIndex === undefined) return;
|
|
3125
|
+
if (this.hiddenFeatures.has(featureIndex)) {
|
|
3126
|
+
this.hiddenFeatures.delete(featureIndex);
|
|
3000
3127
|
} else {
|
|
3001
|
-
this.hiddenFeatures.add(
|
|
3128
|
+
this.hiddenFeatures.add(featureIndex);
|
|
3002
3129
|
}
|
|
3003
3130
|
|
|
3004
3131
|
// Use updateView - content didn't change, just visibility
|
|
@@ -3254,6 +3381,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3254
3381
|
const oldCursorColumn = this.cursorColumn;
|
|
3255
3382
|
const oldContent = this.lines.join('\n');
|
|
3256
3383
|
|
|
3384
|
+
// Save feature count before modification (for index adjustment)
|
|
3385
|
+
const oldFeatureCount = this._countFeatures(oldContent);
|
|
3386
|
+
const cursorFeatureIndex = this._getFeatureIndexForLine(oldCursorLine);
|
|
3387
|
+
|
|
3257
3388
|
try {
|
|
3258
3389
|
const wrapped = '[' + oldContent + ']';
|
|
3259
3390
|
const parsed = JSON.parse(wrapped);
|
|
@@ -3299,23 +3430,43 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3299
3430
|
// operation (insertText, insertNewline, etc.) BEFORE formatAndUpdate was called.
|
|
3300
3431
|
// We need to adjust for indentation changes while keeping the logical position.
|
|
3301
3432
|
|
|
3302
|
-
//
|
|
3303
|
-
//
|
|
3304
|
-
if (
|
|
3305
|
-
//
|
|
3433
|
+
// Special case: cursor at column 0 means we're at start of a line (after newline)
|
|
3434
|
+
// Keep both line and column as set by the calling operation
|
|
3435
|
+
if (oldCursorColumn === 0) {
|
|
3436
|
+
// cursorLine was already set by the calling operation, keep column at 0
|
|
3437
|
+
this.cursorColumn = 0;
|
|
3306
3438
|
} else {
|
|
3307
|
-
//
|
|
3439
|
+
// Calculate character offset in old content (ignoring whitespace for comparison)
|
|
3308
3440
|
const oldLines = oldContent.split('\n');
|
|
3441
|
+
let oldCharOffset = 0;
|
|
3442
|
+
for (let i = 0; i < oldCursorLine && i < oldLines.length; i++) {
|
|
3443
|
+
oldCharOffset += oldLines[i].replace(/\s/g, '').length;
|
|
3444
|
+
}
|
|
3309
3445
|
const oldLineContent = oldLines[oldCursorLine] || '';
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
//
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3446
|
+
const oldLineUpToCursor = oldLineContent.substring(0, oldCursorColumn);
|
|
3447
|
+
oldCharOffset += oldLineUpToCursor.replace(/\s/g, '').length;
|
|
3448
|
+
|
|
3449
|
+
// Find corresponding position in new content
|
|
3450
|
+
let charCount = 0;
|
|
3451
|
+
let newLine = 0;
|
|
3452
|
+
let newCol = 0;
|
|
3453
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
3454
|
+
const lineContent = this.lines[i];
|
|
3455
|
+
for (let j = 0; j <= lineContent.length; j++) {
|
|
3456
|
+
if (charCount >= oldCharOffset) {
|
|
3457
|
+
newLine = i;
|
|
3458
|
+
newCol = j;
|
|
3459
|
+
break;
|
|
3460
|
+
}
|
|
3461
|
+
if (j < lineContent.length && !/\s/.test(lineContent[j])) {
|
|
3462
|
+
charCount++;
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
if (charCount >= oldCharOffset) break;
|
|
3318
3466
|
}
|
|
3467
|
+
|
|
3468
|
+
this.cursorLine = newLine;
|
|
3469
|
+
this.cursorColumn = newCol;
|
|
3319
3470
|
}
|
|
3320
3471
|
}
|
|
3321
3472
|
|
|
@@ -3323,6 +3474,17 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3323
3474
|
this.cursorLine = Math.min(this.cursorLine, Math.max(0, this.lines.length - 1));
|
|
3324
3475
|
this.cursorColumn = Math.min(this.cursorColumn, this.lines[this.cursorLine]?.length || 0);
|
|
3325
3476
|
|
|
3477
|
+
// Adjust hidden feature indices if feature count changed
|
|
3478
|
+
const finalContent = this.lines.join('\n');
|
|
3479
|
+
const newFeatureCount = this._countFeatures(finalContent);
|
|
3480
|
+
if (oldFeatureCount >= 0 && newFeatureCount >= 0 && oldFeatureCount !== newFeatureCount) {
|
|
3481
|
+
const delta = newFeatureCount - oldFeatureCount;
|
|
3482
|
+
// Use cursor position to determine insertion point
|
|
3483
|
+
// If cursor was inside a feature, changes happened at/after that feature
|
|
3484
|
+
const insertionIndex = cursorFeatureIndex >= 0 ? cursorFeatureIndex : 0;
|
|
3485
|
+
this._adjustHiddenIndices(insertionIndex, delta);
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3326
3488
|
this.updateModel();
|
|
3327
3489
|
|
|
3328
3490
|
// Expand any nodes that contain errors (prevents closing edited nodes with typos)
|
|
@@ -3345,9 +3507,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3345
3507
|
|
|
3346
3508
|
// Filter hidden features
|
|
3347
3509
|
if (this.hiddenFeatures.size > 0) {
|
|
3348
|
-
parsed.features = parsed.features.filter((
|
|
3349
|
-
|
|
3350
|
-
return key ? !this.hiddenFeatures.has(key) : true;
|
|
3510
|
+
parsed.features = parsed.features.filter((_feature: Feature, index: number) => {
|
|
3511
|
+
return !this.hiddenFeatures.has(index);
|
|
3351
3512
|
});
|
|
3352
3513
|
}
|
|
3353
3514
|
|
|
@@ -3627,8 +3788,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3627
3788
|
*/
|
|
3628
3789
|
add(input: FeatureInput, options: SetOptions = {}): void {
|
|
3629
3790
|
const newFeatures = normalizeToFeatures(input);
|
|
3791
|
+
const existingCount = this._parseFeatures().length;
|
|
3630
3792
|
const allFeatures = [...this._parseFeatures(), ...newFeatures];
|
|
3631
|
-
|
|
3793
|
+
// Preserve collapsed state for existing features, apply options only to new ones
|
|
3794
|
+
this._setFeaturesInternalPreserving(allFeatures, options, existingCount);
|
|
3632
3795
|
}
|
|
3633
3796
|
|
|
3634
3797
|
/**
|
|
@@ -3644,8 +3807,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3644
3807
|
const newFeatures = normalizeToFeatures(input);
|
|
3645
3808
|
const features = this._parseFeatures();
|
|
3646
3809
|
const idx = index < 0 ? features.length + index : index;
|
|
3647
|
-
|
|
3648
|
-
|
|
3810
|
+
const insertIdx = Math.max(0, Math.min(idx, features.length));
|
|
3811
|
+
|
|
3812
|
+
// Adjust hidden feature indices before insertion
|
|
3813
|
+
// Features at or after insertIdx need to shift by newFeatures.length
|
|
3814
|
+
this._adjustHiddenIndices(insertIdx, newFeatures.length);
|
|
3815
|
+
|
|
3816
|
+
features.splice(insertIdx, 0, ...newFeatures);
|
|
3817
|
+
// Preserve collapsed state, apply options only to inserted features
|
|
3818
|
+
this._setFeaturesInternalPreserving(features, options, insertIdx, newFeatures.length);
|
|
3649
3819
|
}
|
|
3650
3820
|
|
|
3651
3821
|
/**
|
|
@@ -3657,12 +3827,241 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
3657
3827
|
this._applyCollapsedFromOptions(options, features);
|
|
3658
3828
|
}
|
|
3659
3829
|
|
|
3830
|
+
/**
|
|
3831
|
+
* Internal method to set features while preserving collapsed state of existing features
|
|
3832
|
+
* @param features All features (existing + new)
|
|
3833
|
+
* @param options Collapse options for new features
|
|
3834
|
+
* @param newStartIndex Index where new features start (for add) or were inserted (for insertAt)
|
|
3835
|
+
* @param newCount Number of new features (optional, defaults to features from newStartIndex to end)
|
|
3836
|
+
*/
|
|
3837
|
+
private _setFeaturesInternalPreserving(
|
|
3838
|
+
features: Feature[],
|
|
3839
|
+
options: SetOptions,
|
|
3840
|
+
newStartIndex: number,
|
|
3841
|
+
newCount?: number
|
|
3842
|
+
): void {
|
|
3843
|
+
// Save collapsed state by nodeKey before modification
|
|
3844
|
+
const collapsedKeys = new Set<string>();
|
|
3845
|
+
for (const nodeId of this.collapsedNodes) {
|
|
3846
|
+
const nodeInfo = this._nodeIdToLines.get(nodeId);
|
|
3847
|
+
if (nodeInfo?.nodeKey) {
|
|
3848
|
+
// Include feature index in key to handle multiple features with same structure
|
|
3849
|
+
const featureIndex = this._getFeatureIndexForLine(nodeInfo.startLine);
|
|
3850
|
+
collapsedKeys.add(`${featureIndex}:${nodeInfo.nodeKey}`);
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
// Save hidden features (already adjusted by caller for insert/remove)
|
|
3855
|
+
const savedHiddenFeatures = new Set(this.hiddenFeatures);
|
|
3856
|
+
|
|
3857
|
+
// Format and set content
|
|
3858
|
+
const formatted = features.map(f => JSON.stringify(f, null, 2)).join(',\n');
|
|
3859
|
+
this.setValue(formatted, false);
|
|
3860
|
+
|
|
3861
|
+
// Restore hidden features
|
|
3862
|
+
this.hiddenFeatures = savedHiddenFeatures;
|
|
3863
|
+
|
|
3864
|
+
// Restore collapsed state for existing features
|
|
3865
|
+
const ranges = this._findCollapsibleRanges();
|
|
3866
|
+
const featureRanges = ranges.filter(r => r.isRootFeature);
|
|
3867
|
+
const actualNewCount = newCount !== undefined ? newCount : features.length - newStartIndex;
|
|
3868
|
+
|
|
3869
|
+
for (const range of ranges) {
|
|
3870
|
+
// Find which feature this range belongs to
|
|
3871
|
+
const featureIndex = featureRanges.findIndex(fr =>
|
|
3872
|
+
range.startLine >= fr.startLine && range.endLine <= fr.endLine
|
|
3873
|
+
);
|
|
3874
|
+
|
|
3875
|
+
// Determine if this is an existing feature (adjust index for insertAt case)
|
|
3876
|
+
let originalFeatureIndex = featureIndex;
|
|
3877
|
+
if (featureIndex >= newStartIndex && featureIndex < newStartIndex + actualNewCount) {
|
|
3878
|
+
// This is a new feature - apply options
|
|
3879
|
+
continue;
|
|
3880
|
+
} else if (featureIndex >= newStartIndex + actualNewCount) {
|
|
3881
|
+
// Feature was shifted by insertion - adjust index
|
|
3882
|
+
originalFeatureIndex = featureIndex - actualNewCount;
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
// Check if this node was collapsed before
|
|
3886
|
+
const key = `${originalFeatureIndex}:${range.nodeKey}`;
|
|
3887
|
+
if (collapsedKeys.has(key)) {
|
|
3888
|
+
this.collapsedNodes.add(range.nodeId);
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
// Apply collapse options to new features only
|
|
3893
|
+
this._applyCollapsedToNewFeatures(options, features, newStartIndex, actualNewCount);
|
|
3894
|
+
|
|
3895
|
+
// Use updateView instead of updateModel since setValue already rebuilt mappings
|
|
3896
|
+
// and we just need to recompute visible lines with new collapsed state
|
|
3897
|
+
this.updateView();
|
|
3898
|
+
this.scheduleRender();
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
/**
|
|
3902
|
+
* Apply collapsed options only to specific new features
|
|
3903
|
+
*/
|
|
3904
|
+
private _applyCollapsedToNewFeatures(
|
|
3905
|
+
options: SetOptions,
|
|
3906
|
+
features: Feature[],
|
|
3907
|
+
startIndex: number,
|
|
3908
|
+
count: number
|
|
3909
|
+
): void {
|
|
3910
|
+
const collapsed = options.collapsed !== undefined ? options.collapsed : ['coordinates'];
|
|
3911
|
+
if (!collapsed || (Array.isArray(collapsed) && collapsed.length === 0)) return;
|
|
3912
|
+
|
|
3913
|
+
const ranges = this._findCollapsibleRanges();
|
|
3914
|
+
const featureRanges = ranges.filter(r => r.isRootFeature);
|
|
3915
|
+
|
|
3916
|
+
for (const range of ranges) {
|
|
3917
|
+
// Find which feature this range belongs to
|
|
3918
|
+
const featureIndex = featureRanges.findIndex(fr =>
|
|
3919
|
+
range.startLine >= fr.startLine && range.endLine <= fr.endLine
|
|
3920
|
+
);
|
|
3921
|
+
|
|
3922
|
+
// Only process new features
|
|
3923
|
+
if (featureIndex < startIndex || featureIndex >= startIndex + count) continue;
|
|
3924
|
+
|
|
3925
|
+
let shouldCollapse = false;
|
|
3926
|
+
if (typeof collapsed === 'function') {
|
|
3927
|
+
const feature = features[featureIndex];
|
|
3928
|
+
const collapsedAttrs = collapsed(feature, featureIndex);
|
|
3929
|
+
shouldCollapse = range.isRootFeature
|
|
3930
|
+
? collapsedAttrs.includes('$root')
|
|
3931
|
+
: collapsedAttrs.includes(range.nodeKey);
|
|
3932
|
+
} else if (Array.isArray(collapsed)) {
|
|
3933
|
+
shouldCollapse = range.isRootFeature
|
|
3934
|
+
? collapsed.includes('$root')
|
|
3935
|
+
: collapsed.includes(range.nodeKey);
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
if (shouldCollapse) {
|
|
3939
|
+
this.collapsedNodes.add(range.nodeId);
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
/**
|
|
3945
|
+
* Get feature index for a given line
|
|
3946
|
+
*/
|
|
3947
|
+
/**
|
|
3948
|
+
* Count features in content (returns -1 if JSON invalid)
|
|
3949
|
+
*/
|
|
3950
|
+
private _countFeatures(content: string): number {
|
|
3951
|
+
try {
|
|
3952
|
+
const wrapped = '[' + content + ']';
|
|
3953
|
+
const parsed = JSON.parse(wrapped);
|
|
3954
|
+
return Array.isArray(parsed) ? parsed.length : -1;
|
|
3955
|
+
} catch {
|
|
3956
|
+
return -1;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
/**
|
|
3961
|
+
* Adjust hiddenFeatures indices when features are inserted or removed
|
|
3962
|
+
* @param insertionIndex - Index where features were inserted (or removed from)
|
|
3963
|
+
* @param delta - Number of features added (positive) or removed (negative)
|
|
3964
|
+
*/
|
|
3965
|
+
private _adjustHiddenIndices(insertionIndex: number, delta: number): void {
|
|
3966
|
+
if (delta === 0 || this.hiddenFeatures.size === 0) return;
|
|
3967
|
+
|
|
3968
|
+
const newHiddenFeatures = new Set<number>();
|
|
3969
|
+
for (const idx of this.hiddenFeatures) {
|
|
3970
|
+
if (idx < insertionIndex) {
|
|
3971
|
+
// Before insertion point - keep same index
|
|
3972
|
+
newHiddenFeatures.add(idx);
|
|
3973
|
+
} else {
|
|
3974
|
+
// At or after insertion point - shift by delta
|
|
3975
|
+
const newIdx = idx + delta;
|
|
3976
|
+
if (newIdx >= 0) {
|
|
3977
|
+
newHiddenFeatures.add(newIdx);
|
|
3978
|
+
}
|
|
3979
|
+
// If newIdx < 0, the feature was removed, so we don't add it
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
this.hiddenFeatures = newHiddenFeatures;
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
/**
|
|
3986
|
+
* Remove a hidden index and shift all indices after it by -1
|
|
3987
|
+
* Used when removing a feature via API
|
|
3988
|
+
*/
|
|
3989
|
+
private _removeAndShiftHiddenIndex(removedIndex: number): void {
|
|
3990
|
+
if (this.hiddenFeatures.size === 0) return;
|
|
3991
|
+
|
|
3992
|
+
const newHiddenFeatures = new Set<number>();
|
|
3993
|
+
for (const idx of this.hiddenFeatures) {
|
|
3994
|
+
if (idx < removedIndex) {
|
|
3995
|
+
// Before removed index - keep same
|
|
3996
|
+
newHiddenFeatures.add(idx);
|
|
3997
|
+
} else if (idx > removedIndex) {
|
|
3998
|
+
// After removed index - shift by -1
|
|
3999
|
+
newHiddenFeatures.add(idx - 1);
|
|
4000
|
+
}
|
|
4001
|
+
// idx === removedIndex is dropped (feature was removed)
|
|
4002
|
+
}
|
|
4003
|
+
this.hiddenFeatures = newHiddenFeatures;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
private _getFeatureIndexForLine(line: number): number {
|
|
4007
|
+
for (const [, range] of this.featureRanges) {
|
|
4008
|
+
if (line >= range.startLine && line <= range.endLine) {
|
|
4009
|
+
return range.featureIndex;
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
return -1;
|
|
4013
|
+
}
|
|
4014
|
+
|
|
3660
4015
|
removeAt(index: number): Feature | undefined {
|
|
3661
4016
|
const features = this._parseFeatures();
|
|
3662
4017
|
const idx = index < 0 ? features.length + index : index;
|
|
3663
4018
|
if (idx >= 0 && idx < features.length) {
|
|
4019
|
+
// Save collapsed state by nodeKey before modification
|
|
4020
|
+
const collapsedKeys = new Set<string>();
|
|
4021
|
+
for (const nodeId of this.collapsedNodes) {
|
|
4022
|
+
const nodeInfo = this._nodeIdToLines.get(nodeId);
|
|
4023
|
+
if (nodeInfo?.nodeKey) {
|
|
4024
|
+
const featureIndex = this._getFeatureIndexForLine(nodeInfo.startLine);
|
|
4025
|
+
// Skip the feature being removed, adjust indices for features after it
|
|
4026
|
+
if (featureIndex === idx) continue;
|
|
4027
|
+
const adjustedIndex = featureIndex > idx ? featureIndex - 1 : featureIndex;
|
|
4028
|
+
collapsedKeys.add(`${adjustedIndex}:${nodeInfo.nodeKey}`);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
// Adjust hidden feature indices: remove idx, shift indices after idx by -1
|
|
4033
|
+
this._removeAndShiftHiddenIndex(idx);
|
|
4034
|
+
|
|
4035
|
+
// Save hidden features before setValue (which clears them)
|
|
4036
|
+
const savedHiddenFeatures = new Set(this.hiddenFeatures);
|
|
4037
|
+
|
|
3664
4038
|
const removed = features.splice(idx, 1)[0];
|
|
3665
|
-
|
|
4039
|
+
|
|
4040
|
+
// Format and set content
|
|
4041
|
+
const formatted = features.map((f: Feature) => JSON.stringify(f, null, 2)).join(',\n');
|
|
4042
|
+
this.setValue(formatted, false);
|
|
4043
|
+
|
|
4044
|
+
// Restore hidden features
|
|
4045
|
+
this.hiddenFeatures = savedHiddenFeatures;
|
|
4046
|
+
|
|
4047
|
+
// Restore collapsed state for remaining features
|
|
4048
|
+
const ranges = this._findCollapsibleRanges();
|
|
4049
|
+
const featureRanges = ranges.filter(r => r.isRootFeature);
|
|
4050
|
+
|
|
4051
|
+
for (const range of ranges) {
|
|
4052
|
+
const featureIndex = featureRanges.findIndex(fr =>
|
|
4053
|
+
range.startLine >= fr.startLine && range.endLine <= fr.endLine
|
|
4054
|
+
);
|
|
4055
|
+
const key = `${featureIndex}:${range.nodeKey}`;
|
|
4056
|
+
if (collapsedKeys.has(key)) {
|
|
4057
|
+
this.collapsedNodes.add(range.nodeId);
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
// Use updateView instead of updateModel since setValue already rebuilt mappings
|
|
4062
|
+
this.updateView();
|
|
4063
|
+
this.scheduleRender();
|
|
4064
|
+
this.emitChange();
|
|
3666
4065
|
return removed;
|
|
3667
4066
|
}
|
|
3668
4067
|
return undefined;
|