@softwarity/geojson-editor 1.0.5 → 1.0.6
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 +67 -37
- package/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.js +678 -433
package/README.md
CHANGED
|
@@ -10,9 +10,6 @@
|
|
|
10
10
|
<a href="https://www.npmjs.com/package/@softwarity/geojson-editor">
|
|
11
11
|
<img src="https://img.shields.io/npm/v/@softwarity/geojson-editor?color=blue&label=npm" alt="npm version">
|
|
12
12
|
</a>
|
|
13
|
-
<a href="https://www.npmjs.com/package/@softwarity/geojson-editor">
|
|
14
|
-
<img src="https://img.shields.io/npm/dm/@softwarity/geojson-editor?color=green" alt="npm downloads">
|
|
15
|
-
</a>
|
|
16
13
|
<a href="https://bundlephobia.com/package/@softwarity/geojson-editor">
|
|
17
14
|
<img src="https://img.shields.io/bundlephobia/minzip/@softwarity/geojson-editor?label=size" alt="bundle size">
|
|
18
15
|
</a>
|
|
@@ -30,9 +27,9 @@ A feature-rich, framework-agnostic **Web Component** for editing GeoJSON feature
|
|
|
30
27
|
|
|
31
28
|
## Why not Monaco, CodeMirror, or Prism?
|
|
32
29
|
|
|
33
|
-
| | @softwarity/geojson-editor | Monaco Editor | CodeMirror | Prism.js |
|
|
30
|
+
| | @softwarity/geojson-editor | Monaco Editor | CodeMirror 6 | Prism.js |
|
|
34
31
|
|---|:---:|:---:|:---:|:---:|
|
|
35
|
-
| **Size (gzip)** |
|
|
32
|
+
| **Size (gzip)** | <img src="https://img.shields.io/bundlephobia/minzip/@softwarity/geojson-editor?label="> | ~2.5 MB* | ~150 KB* | ~20 KB* |
|
|
36
33
|
| **GeoJSON validation** | ✅ Built-in | ❌ Manual | ❌ Manual | ❌ None |
|
|
37
34
|
| **Type highlighting** | ✅ Contextual | ⚠️ Generic JSON | ⚠️ Generic JSON | ⚠️ Generic JSON |
|
|
38
35
|
| **Invalid type detection** | ✅ Visual feedback | ❌ | ❌ | ❌ |
|
|
@@ -40,11 +37,14 @@ A feature-rich, framework-agnostic **Web Component** for editing GeoJSON feature
|
|
|
40
37
|
| **Color picker** | ✅ Integrated | ❌ | ❌ | ❌ |
|
|
41
38
|
| **Feature visibility toggle** | ✅ | ❌ | ❌ | ❌ |
|
|
42
39
|
| **Auto-collapse coordinates** | ✅ | ❌ | ❌ | ❌ |
|
|
43
|
-
| **FeatureCollection
|
|
40
|
+
| **FeatureCollection output** | ✅ Always | ❌ | ❌ | ❌ |
|
|
41
|
+
| **Clear button** | ✅ | ❌ | ❌ | ❌ |
|
|
44
42
|
| **Dark mode detection** | ✅ Auto | ⚠️ Manual | ⚠️ Manual | ⚠️ Manual |
|
|
45
43
|
| **Dependencies** | 0 | Many | Few | 0 |
|
|
46
44
|
| **Setup complexity** | 1 line | Complex | Moderate | Simple |
|
|
47
45
|
|
|
46
|
+
<sub>* Estimated total size: Monaco includes web workers loaded dynamically; CodeMirror/Prism require plugins for equivalent functionality (line numbers, folding, language support).</sub>
|
|
47
|
+
|
|
48
48
|
**TL;DR**: If you're building a GeoJSON-focused application and need a lightweight, specialized editor with built-in validation and GeoJSON-aware features, this component does exactly that — without the overhead of a general-purpose code editor.
|
|
49
49
|
|
|
50
50
|
## Features
|
|
@@ -60,7 +60,8 @@ A feature-rich, framework-agnostic **Web Component** for editing GeoJSON feature
|
|
|
60
60
|
- **Readonly Mode** - Visual indicator with diagonal stripes when editing is disabled
|
|
61
61
|
- **Block Editing in Collapsed Areas** - Prevents accidental edits in collapsed sections
|
|
62
62
|
- **Smart Copy/Paste** - Copy includes expanded content even from collapsed nodes
|
|
63
|
-
- **FeatureCollection
|
|
63
|
+
- **FeatureCollection Output** - Emits valid FeatureCollection with all edited features
|
|
64
|
+
- **Clear Button** - Discreet ✕ button in suffix area to clear all editor content (hidden in readonly mode)
|
|
64
65
|
|
|
65
66
|
## Installation
|
|
66
67
|
|
|
@@ -102,7 +103,7 @@ import '@softwarity/geojson-editor';
|
|
|
102
103
|
|
|
103
104
|
## Usage
|
|
104
105
|
|
|
105
|
-
### Basic Usage
|
|
106
|
+
### Basic Usage
|
|
106
107
|
|
|
107
108
|
```html
|
|
108
109
|
<!DOCTYPE html>
|
|
@@ -112,30 +113,17 @@ import '@softwarity/geojson-editor';
|
|
|
112
113
|
</head>
|
|
113
114
|
<body>
|
|
114
115
|
<!-- User edits features, component wraps in FeatureCollection -->
|
|
115
|
-
<geojson-editor
|
|
116
|
-
feature-collection
|
|
117
|
-
placeholder="Enter GeoJSON features here..."
|
|
118
|
-
></geojson-editor>
|
|
116
|
+
<geojson-editor placeholder="Enter GeoJSON features here..."></geojson-editor>
|
|
119
117
|
</body>
|
|
120
118
|
</html>
|
|
121
119
|
```
|
|
122
120
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
```html
|
|
126
|
-
<!-- User edits a complete GeoJSON object (Feature or FeatureCollection) -->
|
|
127
|
-
<geojson-editor
|
|
128
|
-
placeholder="Enter GeoJSON here..."
|
|
129
|
-
></geojson-editor>
|
|
130
|
-
```
|
|
121
|
+
Users edit features directly (comma-separated), and the component automatically wraps them in a `{"type": "FeatureCollection", "features": [...]}` structure for validation and events.
|
|
131
122
|
|
|
132
123
|
### With Theme Detection
|
|
133
124
|
|
|
134
125
|
```html
|
|
135
|
-
<geojson-editor
|
|
136
|
-
feature-collection
|
|
137
|
-
dark-selector="html.dark"
|
|
138
|
-
></geojson-editor>
|
|
126
|
+
<geojson-editor dark-selector="html.dark"></geojson-editor>
|
|
139
127
|
```
|
|
140
128
|
|
|
141
129
|
### Listen to Changes
|
|
@@ -159,11 +147,10 @@ editor.addEventListener('error', (e) => {
|
|
|
159
147
|
|
|
160
148
|
| Attribute | Type | Default | Description |
|
|
161
149
|
|-----------|------|---------|-------------|
|
|
162
|
-
| `value` | `string` | `""` | Initial editor content |
|
|
150
|
+
| `value` | `string` | `""` | Initial editor content (features array content) |
|
|
163
151
|
| `placeholder` | `string` | `""` | Placeholder text |
|
|
164
152
|
| `readonly` | `boolean` | `false` | Make editor read-only |
|
|
165
153
|
| `dark-selector` | `string` | `".dark"` | CSS selector for dark theme (if matches → dark, else → light) |
|
|
166
|
-
| `feature-collection` | `boolean` | `false` | When set, wraps editor content in a FeatureCollection for validation/events |
|
|
167
154
|
|
|
168
155
|
**Note:** `coordinates` nodes are automatically collapsed when content is loaded to improve readability. All nodes can be manually expanded/collapsed by clicking the toggle button.
|
|
169
156
|
|
|
@@ -182,18 +169,65 @@ The `dark-selector` attribute determines when the dark theme is active. If the s
|
|
|
182
169
|
|
|
183
170
|
```javascript
|
|
184
171
|
const editor = document.querySelector('geojson-editor');
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Features API
|
|
175
|
+
|
|
176
|
+
Programmatic manipulation of features:
|
|
177
|
+
|
|
178
|
+
| Method | Description |
|
|
179
|
+
|--------|-------------|
|
|
180
|
+
| `set(features[])` | Replace all features with the given array (throws if invalid) |
|
|
181
|
+
| `add(feature)` | Add a feature at the end (throws if invalid) |
|
|
182
|
+
| `insertAt(feature, index)` | Insert at index (negative = from end: -1 = before last) (throws if invalid) |
|
|
183
|
+
| `removeAt(index)` | Remove feature at index (negative = from end), returns removed feature |
|
|
184
|
+
| `removeAll()` | Remove all features, returns array of removed features |
|
|
185
|
+
| `get(index)` | Get feature at index (negative = from end) |
|
|
186
|
+
| `getAll()` | Get all features as an array |
|
|
187
|
+
| `emit()` | Emit the current document on the change event |
|
|
188
|
+
|
|
189
|
+
**Validation:** `set()`, `add()`, and `insertAt()` validate features before adding. Invalid features throw an `Error` with a descriptive message. A valid Feature must have:
|
|
190
|
+
- `type: "Feature"`
|
|
191
|
+
- `geometry`: object with valid type (`Point`, `LineString`, `Polygon`, etc.) and `coordinates`, or `null`
|
|
192
|
+
- `properties`: object or `null`
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// Set features
|
|
196
|
+
editor.set([
|
|
197
|
+
{ type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] }, properties: {} },
|
|
198
|
+
{ type: 'Feature', geometry: { type: 'Point', coordinates: [1, 1] }, properties: {} }
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
// Add a feature
|
|
202
|
+
editor.add({ type: 'Feature', geometry: { type: 'Point', coordinates: [2, 2] }, properties: {} });
|
|
185
203
|
|
|
186
|
-
//
|
|
204
|
+
// Insert at position 1
|
|
205
|
+
editor.insertAt({ type: 'Feature', ... }, 1);
|
|
206
|
+
|
|
207
|
+
// Remove last feature
|
|
208
|
+
const removed = editor.removeAt(-1);
|
|
209
|
+
|
|
210
|
+
// Get all features
|
|
211
|
+
const features = editor.getAll();
|
|
212
|
+
|
|
213
|
+
// Manually emit change event
|
|
214
|
+
editor.emit();
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Theme API
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// Get current theme
|
|
187
221
|
const themes = editor.getTheme();
|
|
222
|
+
|
|
223
|
+
// Set custom theme (partial update)
|
|
188
224
|
editor.setTheme({
|
|
189
225
|
dark: { background: '#000', textColor: '#fff' },
|
|
190
226
|
light: { background: '#fff', textColor: '#000' }
|
|
191
227
|
});
|
|
192
|
-
editor.resetTheme();
|
|
193
228
|
|
|
194
|
-
//
|
|
195
|
-
editor.
|
|
196
|
-
const value = editor.querySelector('#textarea').value; // via Shadow DOM
|
|
229
|
+
// Reset to defaults
|
|
230
|
+
editor.resetTheme();
|
|
197
231
|
```
|
|
198
232
|
|
|
199
233
|
## Events
|
|
@@ -208,15 +242,11 @@ editor.addEventListener('change', (e) => {
|
|
|
208
242
|
});
|
|
209
243
|
```
|
|
210
244
|
|
|
211
|
-
**Event detail:** The parsed GeoJSON object
|
|
245
|
+
**Event detail:** The parsed GeoJSON object (always a FeatureCollection).
|
|
212
246
|
|
|
213
247
|
**Note:** Hidden features (toggled via the eye icon) are automatically excluded from the emitted GeoJSON. This allows temporary filtering without modifying the actual JSON content.
|
|
214
248
|
|
|
215
|
-
**Example
|
|
216
|
-
|
|
217
|
-
```html
|
|
218
|
-
<geojson-editor feature-collection></geojson-editor>
|
|
219
|
-
```
|
|
249
|
+
**Example:**
|
|
220
250
|
|
|
221
251
|
```javascript
|
|
222
252
|
// User edits features only, but change event includes the FeatureCollection wrapper
|
package/dist/geojson-editor.js
CHANGED
|
@@ -2,9 +2,9 @@ var e=Object.defineProperty,t=(t,n,o)=>n in t?e(t,n,{enumerable:!0,configurable:
|
|
|
2
2
|
/**
|
|
3
3
|
* @license MIT
|
|
4
4
|
* @name @softwarity/geojson-editor
|
|
5
|
-
* @version 1.0.
|
|
5
|
+
* @version 1.0.6
|
|
6
6
|
* @author Softwarity (https://www.softwarity.io/)
|
|
7
7
|
* @copyright 2024 Softwarity
|
|
8
8
|
* @see https://github.com/softwarity/geojson-editor
|
|
9
9
|
*/
|
|
10
|
-
const o=class e extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.collapsedData=new Map,this.colorPositions=[],this.nodeTogglePositions=[],this.hiddenFeatures=new Set,this.featureRanges=new Map,this.highlightTimer=null,this._cachedLineHeight=null,this._cachedPaddingTop=null,this.themes={dark:{...e.DEFAULT_THEMES.dark},light:{...e.DEFAULT_THEMES.light}}}static get observedAttributes(){return["readonly","value","placeholder","dark-selector","feature-collection"]}connectedCallback(){this.render(),this.setupEventListeners(),this.updatePrefixSuffix(),this.updateThemeCSS(),this.value&&this.updateValue(this.value),this.updatePlaceholderContent()}attributeChangedCallback(e,t,n){t!==n&&("value"===e?this.updateValue(n):"readonly"===e?this.updateReadonly():"placeholder"===e?this.updatePlaceholderContent():"dark-selector"===e?this.updateThemeCSS():"feature-collection"===e&&this.updatePrefixSuffix())}get readonly(){return this.hasAttribute("readonly")}get value(){return this.getAttribute("value")||""}get placeholder(){return this.getAttribute("placeholder")||""}get featureCollection(){return this.hasAttribute("feature-collection")}get prefix(){return this.featureCollection?e.FEATURE_COLLECTION_PREFIX:""}get suffix(){return this.featureCollection?e.FEATURE_COLLECTION_SUFFIX:""}render(){const e=`\n <div class="editor-prefix" id="editorPrefix"></div>\n <div class="editor-wrapper">\n <div class="gutter">\n <div class="gutter-content" id="gutterContent"></div>\n </div>\n <div class="editor-content">\n <div class="placeholder-layer" id="placeholderLayer">${this.escapeHtml(this.placeholder)}</div>\n <div class="highlight-layer" id="highlightLayer"></div>\n <textarea\n id="textarea"\n spellcheck="false"\n autocomplete="off"\n autocorrect="off"\n autocapitalize="off"\n ></textarea>\n </div>\n </div>\n <div class="editor-suffix" id="editorSuffix"></div>\n `;this.shadowRoot.innerHTML="\n <style>\n /* Global reset with exact values to prevent external CSS interference */\n :host *,\n :host *::before,\n :host *::after {\n box-sizing: border-box;\n font-family: 'Courier New', Courier, monospace;\n font-size: 13px;\n font-weight: normal;\n font-style: normal;\n font-variant: normal;\n line-height: 1.5;\n letter-spacing: 0;\n text-transform: none;\n text-decoration: none;\n text-indent: 0;\n word-spacing: 0;\n }\n\n :host {\n display: flex;\n flex-direction: column;\n position: relative;\n width: 100%;\n height: 400px;\n font-family: 'Courier New', Courier, monospace;\n font-size: 13px;\n line-height: 1.5;\n border-radius: 4px;\n overflow: hidden;\n }\n\n :host([readonly]) .editor-wrapper::after {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n background: repeating-linear-gradient(\n -45deg,\n rgba(128, 128, 128, 0.08),\n rgba(128, 128, 128, 0.08) 3px,\n transparent 3px,\n transparent 12px\n );\n z-index: 1;\n }\n\n :host([readonly]) textarea {\n cursor: text;\n }\n\n .editor-wrapper {\n position: relative;\n width: 100%;\n flex: 1;\n background: var(--bg-color);\n display: flex;\n font-family: 'Courier New', Courier, monospace;\n font-size: 13px;\n line-height: 1.5;\n }\n\n .gutter {\n width: 24px;\n height: 100%;\n background: var(--gutter-bg);\n border-right: 1px solid var(--gutter-border);\n overflow: hidden;\n flex-shrink: 0;\n position: relative;\n }\n\n .gutter-content {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n padding: 8px 4px;\n }\n\n .gutter-line {\n position: absolute;\n left: 0;\n width: 100%;\n height: 1.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .color-indicator {\n width: 12px;\n height: 12px;\n border-radius: 2px;\n border: 1px solid #555;\n cursor: pointer;\n transition: transform 0.1s;\n flex-shrink: 0;\n }\n\n .color-indicator:hover {\n transform: scale(1.2);\n border-color: #fff;\n }\n\n .collapse-button {\n width: 12px;\n height: 12px;\n background: var(--control-bg);\n border: 1px solid var(--control-border);\n border-radius: 2px;\n color: var(--control-color);\n font-size: 8px;\n font-weight: bold;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.1s;\n flex-shrink: 0;\n user-select: none;\n }\n\n .collapse-button:hover {\n background: var(--control-bg);\n border-color: var(--control-color);\n transform: scale(1.1);\n }\n\n .visibility-button {\n width: 14px;\n height: 14px;\n background: transparent;\n border: none;\n color: var(--control-color);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.1s;\n flex-shrink: 0;\n opacity: 0.7;\n padding: 0;\n }\n\n .visibility-button:hover {\n opacity: 1;\n transform: scale(1.1);\n }\n\n .visibility-button.hidden {\n opacity: 0.4;\n }\n\n .visibility-button svg {\n width: 12px;\n height: 12px;\n fill: currentColor;\n }\n\n /* Hidden feature lines - grayed out */\n .line-hidden {\n opacity: 0.35;\n filter: grayscale(50%);\n }\n\n .color-picker-popup {\n position: absolute;\n background: #2d2d30;\n border: 1px solid #555;\n border-radius: 4px;\n padding: 8px;\n z-index: 1000;\n box-shadow: 0 4px 12px rgba(0,0,0,0.5);\n }\n\n .color-picker-popup input[type=\"color\"] {\n width: 150px;\n height: 30px;\n border: none;\n cursor: pointer;\n }\n\n .editor-content {\n position: relative;\n flex: 1;\n overflow: hidden;\n }\n\n .highlight-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 8px 12px;\n font-family: 'Courier New', Courier, monospace;\n font-size: 13px;\n font-weight: normal;\n font-style: normal;\n line-height: 1.5;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow: auto;\n pointer-events: none;\n z-index: 1;\n color: var(--text-color);\n }\n\n .highlight-layer::-webkit-scrollbar {\n display: none;\n }\n\n textarea {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 8px 12px;\n margin: 0;\n border: none;\n outline: none;\n background: transparent;\n color: transparent;\n caret-color: var(--caret-color);\n font-family: 'Courier New', Courier, monospace;\n font-size: 13px;\n font-weight: normal;\n font-style: normal;\n line-height: 1.5;\n white-space: pre-wrap;\n word-wrap: break-word;\n resize: none;\n overflow: auto;\n z-index: 2;\n box-sizing: border-box;\n }\n\n textarea::selection {\n background: rgba(51, 153, 255, 0.3);\n }\n\n textarea::placeholder {\n color: transparent;\n }\n\n .placeholder-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 8px 12px;\n font-family: 'Courier New', Courier, monospace;\n font-size: 13px;\n font-weight: normal;\n font-style: normal;\n line-height: 1.5;\n white-space: pre-wrap;\n word-wrap: break-word;\n color: #6a6a6a;\n pointer-events: none;\n z-index: 0;\n overflow: hidden;\n }\n\n textarea:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n\n /* Syntax highlighting colors */\n .json-key {\n color: var(--json-key);\n }\n\n .json-string {\n color: var(--json-string);\n }\n\n .json-number {\n color: var(--json-number);\n }\n\n .json-boolean {\n color: var(--json-boolean);\n }\n\n .json-null {\n color: var(--json-null);\n }\n\n .json-punctuation {\n color: var(--json-punct);\n }\n\n /* GeoJSON-specific highlighting */\n .geojson-key {\n color: var(--geojson-key);\n font-weight: 600;\n }\n\n .geojson-type {\n color: var(--geojson-type);\n font-weight: 600;\n }\n\n .geojson-type-invalid {\n color: var(--geojson-type-invalid);\n font-weight: 600;\n }\n\n .json-key-invalid {\n color: var(--json-key-invalid);\n }\n\n /* Prefix and suffix styling */\n .editor-prefix,\n .editor-suffix {\n padding: 4px 12px;\n color: var(--text-color);\n background: var(--bg-color);\n user-select: none;\n white-space: pre-wrap;\n word-wrap: break-word;\n flex-shrink: 0;\n font-family: 'Courier New', Courier, monospace;\n font-size: 13px;\n line-height: 1.5;\n opacity: 0.6;\n border-left: 3px solid rgba(102, 126, 234, 0.5);\n }\n\n .editor-prefix {\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n }\n\n .editor-suffix {\n border-top: 1px solid rgba(255, 255, 255, 0.1);\n }\n\n /* Scrollbar styling - WebKit (Chrome, Safari, Edge) */\n textarea::-webkit-scrollbar {\n width: 10px;\n height: 10px;\n }\n\n textarea::-webkit-scrollbar-track {\n background: var(--control-bg);\n }\n\n textarea::-webkit-scrollbar-thumb {\n background: var(--control-border);\n border-radius: 5px;\n }\n\n textarea::-webkit-scrollbar-thumb:hover {\n background: var(--control-color);\n }\n\n /* Scrollbar styling - Firefox */\n textarea {\n scrollbar-width: thin;\n scrollbar-color: var(--control-border) var(--control-bg);\n }\n </style>\n "+e}setupEventListeners(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");e.addEventListener("scroll",()=>{t.scrollTop=e.scrollTop,t.scrollLeft=e.scrollLeft,this.syncGutterScroll(e.scrollTop)}),e.addEventListener("input",()=>{this.updatePlaceholderVisibility(),clearTimeout(this.highlightTimer),this.highlightTimer=setTimeout(()=>{this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange()},150)}),e.addEventListener("paste",()=>{clearTimeout(this.highlightTimer),setTimeout(()=>{this.updatePlaceholderVisibility(),this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange(),this.applyAutoCollapsed()},10)}),this.shadowRoot.getElementById("gutterContent").addEventListener("click",e=>{const t=e.target.closest(".visibility-button");if(t){const e=t.dataset.featureKey;return void this.toggleFeatureVisibility(e)}if(e.target.classList.contains("color-indicator")){const t=parseInt(e.target.dataset.line),n=e.target.dataset.color,o=e.target.dataset.attributeName;this.showColorPicker(e.target,t,n,o)}else if(e.target.classList.contains("collapse-button")){const t=e.target.dataset.nodeKey,n=parseInt(e.target.dataset.line);this.toggleCollapse(t,n)}}),this.shadowRoot.querySelector(".gutter").addEventListener("wheel",t=>{t.preventDefault(),e.scrollTop+=t.deltaY}),e.addEventListener("keydown",e=>{this.handleKeydownInCollapsedArea(e)}),e.addEventListener("copy",e=>{this.handleCopyWithCollapsedContent(e)}),e.addEventListener("cut",e=>{this.handleCutWithCollapsedContent(e)}),this.updateReadonly()}syncGutterScroll(e){this.shadowRoot.getElementById("gutterContent").style.transform=`translateY(-${e}px)`}updateReadonly(){const e=this.shadowRoot.getElementById("textarea");e&&(e.disabled=this.readonly)}escapeHtml(e){return e?e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"):""}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("placeholderLayer");e&&t&&(t.style.display=e.value?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updateValue(e){const t=this.shadowRoot.getElementById("textarea");if(t&&t.value!==e){if(t.value=e||"",e)try{const n=this.prefix,o=this.suffix,s=n.trimEnd().endsWith("["),i=o.trimStart().startsWith("]");if(s&&i){const n="["+e+"]",o=JSON.parse(n),s=JSON.stringify(o,null,2).split("\n");s.length>2?t.value=s.slice(1,-1).join("\n"):t.value=""}else if(!n&&!o){const n=JSON.parse(e);t.value=JSON.stringify(n,null,2)}}catch{}this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(this.prefix?(e.textContent=this.prefix,e.style.display="block"):(e.textContent="",e.style.display="none")),t&&(this.suffix?(t.textContent=this.suffix,t.style.display="block"):(t.textContent="",t.style.display="none"))}updateHighlight(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");if(!e||!t)return;const n=e.value;this.updateFeatureRanges();const o=this.getHiddenLineRanges(),{highlighted:s,colors:i,toggles:r}=this.highlightJSON(n,o);t.innerHTML=s,this.colorPositions=i,this.nodeTogglePositions=r,this.updateGutter()}highlightJSON(t,n=[]){if(!t.trim())return{highlighted:"",colors:[],toggles:[]};const o=t.split("\n"),s=[],i=[];let r=[];const a=this.buildContextMap(t);return o.forEach((t,o)=>{const l=e.REGEX;let d;for(l.colorInLine.lastIndex=0;null!==(d=l.colorInLine.exec(t));)s.push({line:o,color:d[2],attributeName:d[1]});const h=t.match(l.collapsibleNode);if(h){const e=h[2];t.includes("{...}")||t.includes("[...]")?i.push({line:o,nodeKey:e,isCollapsed:!0}):this.bracketClosesOnSameLine(t,h[3])||i.push({line:o,nodeKey:e,isCollapsed:!1})}const c=a.get(o);let p=this.highlightSyntax(t,c);(e=>n.some(t=>e>=t.startLine&&e<=t.endLine))(o)&&(p=`<span class="line-hidden">${p}</span>`),r.push(p)}),{highlighted:r.join("\n"),colors:s,toggles:i}}buildContextMap(t){var n;const o=t.split("\n"),s=new Map,i=[];let r=null;const a=this.featureCollection?"Feature":null;for(let t=0;t<o.length;t++){const l=o[t],d=i.length>0?null==(n=i[i.length-1])?void 0:n.context:a;s.set(t,d);for(let t=0;t<l.length;t++){const n=l[t];if('"'===n){const n=l.substring(t).match(/^"([^"]+)"\s*:/);if(n){const o=n[1];e.CONTEXT_CHANGING_KEYS[o]&&(r=e.CONTEXT_CHANGING_KEYS[o]),t+=n[0].length-1;continue}}if('"'===n&&i.length>0&&l.substring(0,t).match(/"type"\s*:\s*$/)){const n=l.substring(t).match(/^"([^"]+)"/);if(n&&e.GEOJSON_TYPES_ALL.includes(n[1])){const e=i[i.length-1];e&&(e.context=n[1])}}if("{"===n||"["===n){let e;if(r)e=r,r=null;else if(0===i.length)e=a;else{const t=i[i.length-1];e=t&&t.isArray?t.context:null}i.push({context:e,isArray:"["===n})}("}"===n||"]"===n)&&i.length>0&&i.pop()}}return s}highlightSyntax(t,n){if(!t.trim())return"";const o=n?e.VALID_KEYS_BY_CONTEXT[n]:null,s=e.REGEX;return t.replace(s.ampersand,"&").replace(s.lessThan,"<").replace(s.greaterThan,">").replace(s.jsonKey,(t,s)=>"properties"===n?`<span class="json-key">"${s}"</span>:`:e.GEOJSON_STRUCTURAL_KEYS.includes(s)?`<span class="geojson-key">"${s}"</span>:`:(t=>!(!e.GEOJSON_STRUCTURAL_KEYS.includes(t)&&n&&null!=o)||o.includes(t))(s)?`<span class="json-key">"${s}"</span>:`:`<span class="json-key-invalid">"${s}"</span>:`).replace(s.typeValue,(t,o)=>(t=>!n||"properties"===n||("geometry"===n||e.GEOJSON_TYPES_GEOMETRY.includes(n)?e.GEOJSON_TYPES_GEOMETRY.includes(t):"Feature"!==n&&"FeatureCollection"!==n||e.GEOJSON_TYPES_ALL.includes(t)))(o)?`<span class="geojson-key">"type"</span>: <span class="geojson-type">"${o}"</span>`:`<span class="geojson-key">"type"</span>: <span class="geojson-type-invalid">"${o}"</span>`).replace(s.stringValue,(e,t)=>e.includes("<span")?e:`: <span class="json-string">"${t}"</span>`).replace(s.numberAfterColon,': <span class="json-number">$1</span>').replace(s.boolean,': <span class="json-boolean">$1</span>').replace(s.nullValue,': <span class="json-null">$1</span>').replace(s.allNumbers,'<span class="json-number">$1</span>').replace(s.punctuation,'<span class="json-punctuation">$1</span>')}toggleCollapse(e,t){const n=this.shadowRoot.getElementById("textarea"),o=n.value.split("\n"),s=o[t];if(s.includes("{...}")||s.includes("[...]")){let n=null,i=null;const r=`${t}-${e}`;if(this.collapsedData.has(r))n=r,i=this.collapsedData.get(r);else for(const[t,o]of this.collapsedData.entries())if(o.nodeKey===e){const e=s.match(/^(\s*)/)[1].length;if(o.indent===e){n=t,i=o;break}}if(!n||!i)return;const{originalLine:a,content:l}=i;o[t]=a,o.splice(t+1,0,...l),this.collapsedData.delete(n)}else{const n=s.match(/^(\s*)"([^"]+)"\s*:\s*([{\[])/);if(!n)return;const i=n[1],r=n[3],a="{"===r?"}":"]";if(this.bracketClosesOnSameLine(s,r))return;let l=1,d=t;const h=[];for(let e=t+1;e<o.length;e++){const t=o[e];for(const e of t)e===r&&l++,e===a&&l--;if(h.push(t),0===l){d=e;break}}const c=`${t}-${e}`;this.collapsedData.set(c,{originalLine:s,content:h,indent:i.length,nodeKey:e});const p=s.substring(0,s.indexOf(r)),u=o[d]&&o[d].trim().endsWith(",");o[t]=`${p}${r}...${a}${u?",":""}`,o.splice(t+1,d-t)}n.value=o.join("\n"),this.updateHighlight()}applyAutoCollapsed(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value)return;const t=e.value.split("\n");for(let e=t.length-1;e>=0;e--){const n=t[e],o=n.match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(o){const s=o[2];if("coordinates"===s){const i=o[1],r=o[3],a="{"===r?"}":"]";if(this.bracketClosesOnSameLine(n,r))continue;let l=1,d=e;const h=[];for(let n=e+1;n<t.length;n++){const e=t[n];for(const t of e)t===r&&l++,t===a&&l--;if(h.push(e),0===l){d=n;break}}const c=`${e}-${s}`;this.collapsedData.set(c,{originalLine:n,content:h,indent:i.length,nodeKey:s});const p=n.substring(0,n.indexOf(r)),u=t[d]&&t[d].trim().endsWith(",");t[e]=`${p}${r}...${a}${u?",":""}`,t.splice(e+1,d-e)}}}e.value=t.join("\n"),this.updateHighlight()}updateGutter(){const t=this.shadowRoot.getElementById("gutterContent"),n=this.shadowRoot.getElementById("textarea");if(!n)return;if(null===this._cachedLineHeight){const e=getComputedStyle(n);this._cachedLineHeight=parseFloat(e.lineHeight),this._cachedPaddingTop=parseFloat(e.paddingTop)}const o=this._cachedLineHeight,s=this._cachedPaddingTop;t.textContent="";const i=new Map,r=e=>(i.has(e)||i.set(e,{colors:[],buttons:[],visibilityButtons:[]}),i.get(e));this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{r(e).colors.push({color:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{r(e).buttons.push({nodeKey:t,isCollapsed:n})});for(const[e,t]of this.featureRanges){const n=this.hiddenFeatures.has(e);r(t.startLine).visibilityButtons.push({featureKey:e,isHidden:n})}const a=document.createDocumentFragment();i.forEach((t,n)=>{const i=document.createElement("div");i.className="gutter-line",i.style.top=`${s+n*o}px`,t.visibilityButtons.forEach(({featureKey:t,isHidden:n})=>{const o=document.createElement("button");o.className="visibility-button"+(n?" hidden":""),o.innerHTML=e.ICON_EYE,o.dataset.featureKey=t,o.title=n?"Show feature in events":"Hide feature from events",i.appendChild(o)}),t.colors.forEach(({color:e,attributeName:t})=>{const o=document.createElement("div");o.className="color-indicator",o.style.backgroundColor=e,o.dataset.line=n,o.dataset.color=e,o.dataset.attributeName=t,o.title=`${t}: ${e}`,i.appendChild(o)}),t.buttons.forEach(({nodeKey:e,isCollapsed:t})=>{const o=document.createElement("div");o.className="collapse-button",o.textContent=t?"+":"-",o.dataset.line=n,o.dataset.nodeKey=e,o.title=t?"Expand":"Collapse",i.appendChild(o)}),a.appendChild(i)}),t.appendChild(a)}showColorPicker(e,t,n,o){const s=document.querySelector(".geojson-color-picker-input");s&&s.remove();const i=document.createElement("input");i.type="color",i.value=n,i.className="geojson-color-picker-input";const r=e.getBoundingClientRect();i.style.position="fixed",i.style.left=`${r.left}px`,i.style.top=`${r.top}px`,i.style.width="12px",i.style.height="12px",i.style.opacity="0.01",i.style.border="none",i.style.padding="0",i.style.zIndex="9999",i.addEventListener("input",e=>{this.updateColorValue(t,e.target.value,o)}),i.addEventListener("change",e=>{this.updateColorValue(t,e.target.value,o)});const a=e=>{e.target!==i&&!i.contains(e.target)&&(i.remove(),document.removeEventListener("click",a,!0))};document.body.appendChild(i),setTimeout(()=>{document.addEventListener("click",a,!0)},100),i.focus(),i.click()}updateColorValue(e,t,n){const o=this.shadowRoot.getElementById("textarea"),s=o.value.split("\n"),i=new RegExp(`"${n}"\\s*:\\s*"#[0-9a-fA-F]{6}"`);s[e]=s[e].replace(i,`"${n}": "${t}"`),o.value=s.join("\n"),this.updateHighlight(),this.emitChange()}handleKeydownInCollapsedArea(e){if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Tab"].includes(e.key)||e.ctrlKey||e.metaKey)return;const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.value.substring(0,n).split("\n").length-1,s=t.value.split("\n")[o];s&&(s.includes("{...}")||s.includes("[...]"))&&e.preventDefault()}handleCopyWithCollapsedContent(e){const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n===o)return;const s=t.value.substring(n,o);if(!s.includes("{...}")&&!s.includes("[...]"))return;const i=this.expandCollapsedMarkersInText(s,n);e.preventDefault(),e.clipboardData.setData("text/plain",i)}expandCollapsedMarkersInText(e,t){const n=this.shadowRoot.getElementById("textarea").value.substring(0,t).split("\n").length-1,o=e.split("\n"),s=[];return o.forEach((e,t)=>{const o=n+t;if(e.includes("{...}")||e.includes("[...]")){let t=!1;this.collapsedData.forEach((e,n)=>{parseInt(n.split("-")[0])===o&&(s.push(e.originalLine),s.push(...e.content),t=!0)}),t||s.push(e)}else s.push(e)}),s.join("\n")}handleCutWithCollapsedContent(e){this.handleCopyWithCollapsedContent(e);const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n!==o){const e=t.value;t.value=e.substring(0,n)+e.substring(o),t.selectionStart=t.selectionEnd=n,this.updateHighlight(),this.updatePlaceholderVisibility(),this.emitChange()}}emitChange(){const e=this.shadowRoot.getElementById("textarea"),t=this.expandAllCollapsed(e.value),n=this.prefix+t+this.suffix;try{let e=JSON.parse(n);e=this.filterHiddenFeatures(e);const o=this.validateGeoJSON(e);o.length>0?this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:(new Date).toISOString(),error:`GeoJSON validation: ${o.join("; ")}`,errors:o,content:t},bubbles:!0,composed:!0})):this.dispatchEvent(new CustomEvent("change",{detail:e,bubbles:!0,composed:!0}))}catch(e){this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:(new Date).toISOString(),error:e.message,content:t},bubbles:!0,composed:!0}))}}filterHiddenFeatures(e){if(!e||0===this.hiddenFeatures.size)return e;if("FeatureCollection"===e.type&&Array.isArray(e.features)){const t=e.features.filter(e=>{const t=this.getFeatureKey(e);return!this.hiddenFeatures.has(t)});return{...e,features:t}}if("Feature"===e.type){const t=this.getFeatureKey(e);if(this.hiddenFeatures.has(t))return{type:"FeatureCollection",features:[]}}return e}getFeatureKey(e){var t,n,o;if(!e||"object"!=typeof e)return null;if(void 0!==e.id)return`id:${e.id}`;if(void 0!==(null==(t=e.properties)?void 0:t.id))return`prop:${e.properties.id}`;const s=(null==(n=e.geometry)?void 0:n.type)||"null",i=JSON.stringify((null==(o=e.geometry)?void 0:o.coordinates)||[]).slice(0,100);return`hash:${s}:${this.simpleHash(i)}`}simpleHash(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t&=t;return t.toString(36)}toggleFeatureVisibility(e){this.hiddenFeatures.has(e)?this.hiddenFeatures.delete(e):this.hiddenFeatures.add(e),this.updateHighlight(),this.updateGutter(),this.emitChange()}isFeatureHidden(e){return this.hiddenFeatures.has(e)}updateFeatureRanges(){const e=this.shadowRoot.getElementById("textarea");if(!e)return;const t=e.value;this.featureRanges.clear();try{const e=this.expandAllCollapsed(t),n=this.prefix+e+this.suffix,o=JSON.parse(n);let s=[];"FeatureCollection"===o.type&&Array.isArray(o.features)?s=o.features:"Feature"===o.type&&(s=[o]);const i=t.split("\n");let r=0,a=0,l=!1,d=-1,h=null;for(let e=0;e<i.length;e++){const t=i[e],n=/"type"\s*:\s*"Feature"/.test(t);if(!l&&n){let t=e;for(let n=e;n>=0;n--)if(i[n].includes("{")){t=n;break}d=t,l=!0,a=1;for(let n=t;n<=e;n++){const e=i[n];let o=n===t;for(const t of e)"{"===t?o?o=!1:a++:"}"===t&&a--}r<s.length&&(h=this.getFeatureKey(s[r]))}else if(l){for(const e of t)"{"===e?a++:"}"===e&&a--;a<=0&&(h&&this.featureRanges.set(h,{startLine:d,endLine:e,featureIndex:r}),r++,l=!1,h=null)}}}catch{}}getHiddenLineRanges(){const e=[];for(const[t,n]of this.featureRanges)this.hiddenFeatures.has(t)&&e.push(n);return e}validateGeoJSON(t,n="",o="root"){const s=[];if(!t||"object"!=typeof t)return s;if("properties"!==o&&void 0!==t.type){const i=t.type;"string"==typeof i&&("geometry"===o?e.GEOJSON_TYPES_GEOMETRY.includes(i)||s.push(`Invalid geometry type "${i}" at ${n||"root"} (expected: ${e.GEOJSON_TYPES_GEOMETRY.join(", ")})`):e.GEOJSON_TYPES_FEATURE.includes(i)||s.push(`Invalid type "${i}" at ${n||"root"} (expected: ${e.GEOJSON_TYPES_FEATURE.join(", ")})`))}if(Array.isArray(t))t.forEach((e,t)=>{s.push(...this.validateGeoJSON(e,`${n}[${t}]`,o))});else for(const[e,i]of Object.entries(t))if("object"==typeof i&&null!==i){const t=n?`${n}.${e}`:e;let r=o;"properties"===e?r="properties":"geometry"===e||"geometries"===e?r="geometry":"features"===e&&(r="root"),s.push(...this.validateGeoJSON(i,t,r))}return s}bracketClosesOnSameLine(e,t){const n="{"===t?"}":"]",o=e.indexOf(t);if(-1===o)return!1;const s=e.substring(o+1);let i=1;for(const e of s)if(e===t&&i++,e===n&&i--,0===i)return!0;return!1}expandAllCollapsed(t){const n=e.REGEX;for(;t.includes("{...}")||t.includes("[...]");){const e=t.split("\n");let o=!1;for(let t=0;t<e.length;t++){const s=e[t];if(!s.includes("{...}")&&!s.includes("[...]"))continue;const i=s.match(n.collapsedMarker);if(!i)continue;const r=i[2],a=i[1].length,l=`${t}-${r}`;let d=this.collapsedData.has(l)?l:null;if(!d)for(const[e,t]of this.collapsedData.entries())if(t.nodeKey===r&&t.indent===a){d=e;break}if(d){const{originalLine:n,content:s}=this.collapsedData.get(d);e[t]=n,e.splice(t+1,0,...s),o=!0;break}}if(!o)break;t=e.join("\n")}return t}formatJSONContent(e){const t=this.prefix,n=this.suffix,o=t.trimEnd().endsWith("["),s=n.trimStart().startsWith("]");if(o&&s){const t="["+e+"]",n=JSON.parse(t),o=JSON.stringify(n,null,2).split("\n");return o.length>2?o.slice(1,-1).join("\n"):""}if(t||n){const o=t+e+n;return JSON.parse(o),e}{const t=JSON.parse(e);return JSON.stringify(t,null,2)}}autoFormatContentWithCursor(){const e=this.shadowRoot.getElementById("textarea"),t=e.selectionStart,n=e.value.substring(0,t).split("\n"),o=n.length-1,s=n[n.length-1].length,i=Array.from(this.collapsedData.values()).map(e=>({nodeKey:e.nodeKey,indent:e.indent})),r=this.expandAllCollapsed(e.value);try{const t=this.formatJSONContent(r);if(t!==r){this.collapsedData.clear(),e.value=t,i.length>0&&this.reapplyCollapsed(i);const n=e.value.split("\n");if(o<n.length){const t=Math.min(s,n[o].length);let i=0;for(let e=0;e<o;e++)i+=n[e].length+1;i+=t,e.setSelectionRange(i,i)}}}catch{}}autoFormatContent(){const e=this.shadowRoot.getElementById("textarea"),t=Array.from(this.collapsedData.values()).map(e=>({nodeKey:e.nodeKey,indent:e.indent})),n=this.expandAllCollapsed(e.value);try{const o=this.formatJSONContent(n);o!==n&&(this.collapsedData.clear(),e.value=o,t.length>0&&this.reapplyCollapsed(t))}catch{}}reapplyCollapsed(e){const t=this.shadowRoot.getElementById("textarea"),n=t.value.split("\n"),o=new Map;e.forEach(({nodeKey:e,indent:t})=>{const n=`${e}-${t}`;o.set(n,(o.get(n)||0)+1)});const s=new Map;for(let e=n.length-1;e>=0;e--){const t=n[e],i=t.match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(i){const r=i[2],a=`${r}-${i[1].length}`;if(o.has(a)&&(s.set(a,(s.get(a)||0)+1),s.get(a)<=o.get(a))){const o=i[1],s=i[3],a="{"===s?"}":"]";if(this.bracketClosesOnSameLine(t,s))continue;let l=1,d=e;const h=[];for(let t=e+1;t<n.length;t++){const e=n[t];for(const t of e)t===s&&l++,t===a&&l--;if(h.push(e),0===l){d=t;break}}const c=`${e}-${r}`;this.collapsedData.set(c,{originalLine:t,content:h,indent:o.length,nodeKey:r});const p=t.substring(0,t.indexOf(s)),u=n[d]&&n[d].trim().endsWith(",");n[e]=`${p}${s}...${a}${u?",":""}`,n.splice(e+1,d-e)}}}t.value=n.join("\n")}parseSelectorToHostRule(e){return e&&""!==e?e.startsWith(".")&&!e.includes(" ")?`:host(${e})`:`:host-context(${e})`:':host([data-color-scheme="dark"])'}updateThemeCSS(){const e=this.getAttribute("dark-selector")||".dark",t=this.parseSelectorToHostRule(e);let n=this.shadowRoot.getElementById("theme-styles");n||(n=document.createElement("style"),n.id="theme-styles",this.shadowRoot.insertBefore(n,this.shadowRoot.firstChild));const o=`\n :host {\n --bg-color: ${this.themes.light.background};\n --text-color: ${this.themes.light.textColor};\n --caret-color: ${this.themes.light.caretColor};\n --gutter-bg: ${this.themes.light.gutterBackground};\n --gutter-border: ${this.themes.light.gutterBorder};\n --json-key: ${this.themes.light.jsonKey};\n --json-string: ${this.themes.light.jsonString};\n --json-number: ${this.themes.light.jsonNumber};\n --json-boolean: ${this.themes.light.jsonBoolean};\n --json-null: ${this.themes.light.jsonNull};\n --json-punct: ${this.themes.light.jsonPunctuation};\n --control-color: ${this.themes.light.controlColor};\n --control-bg: ${this.themes.light.controlBg};\n --control-border: ${this.themes.light.controlBorder};\n --geojson-key: ${this.themes.light.geojsonKey};\n --geojson-type: ${this.themes.light.geojsonType};\n --geojson-type-invalid: ${this.themes.light.geojsonTypeInvalid};\n --json-key-invalid: ${this.themes.light.jsonKeyInvalid};\n }\n\n ${t} {\n --bg-color: ${this.themes.dark.background};\n --text-color: ${this.themes.dark.textColor};\n --caret-color: ${this.themes.dark.caretColor};\n --gutter-bg: ${this.themes.dark.gutterBackground};\n --gutter-border: ${this.themes.dark.gutterBorder};\n --json-key: ${this.themes.dark.jsonKey};\n --json-string: ${this.themes.dark.jsonString};\n --json-number: ${this.themes.dark.jsonNumber};\n --json-boolean: ${this.themes.dark.jsonBoolean};\n --json-null: ${this.themes.dark.jsonNull};\n --json-punct: ${this.themes.dark.jsonPunctuation};\n --control-color: ${this.themes.dark.controlColor};\n --control-bg: ${this.themes.dark.controlBg};\n --control-border: ${this.themes.dark.controlBorder};\n --geojson-key: ${this.themes.dark.geojsonKey};\n --geojson-type: ${this.themes.dark.geojsonType};\n --geojson-type-invalid: ${this.themes.dark.geojsonTypeInvalid};\n --json-key-invalid: ${this.themes.dark.jsonKeyInvalid};\n }\n `;n.textContent=o}getTheme(){return{dark:{...this.themes.dark},light:{...this.themes.light}}}setTheme(e){e.dark&&(this.themes.dark={...this.themes.dark,...e.dark}),e.light&&(this.themes.light={...this.themes.light,...e.light}),this.updateThemeCSS()}resetTheme(){this.themes={dark:{...e.DEFAULT_THEMES.dark},light:{...e.DEFAULT_THEMES.light}},this.updateThemeCSS()}};n(o,"DEFAULT_THEMES",{dark:{background:"#1e1e1e",textColor:"#d4d4d4",caretColor:"#fff",gutterBackground:"#252526",gutterBorder:"#3e3e42",jsonKey:"#9cdcfe",jsonString:"#ce9178",jsonNumber:"#b5cea8",jsonBoolean:"#569cd6",jsonNull:"#569cd6",jsonPunctuation:"#d4d4d4",controlColor:"#c586c0",controlBg:"#3e3e42",controlBorder:"#555",geojsonKey:"#c586c0",geojsonType:"#4ec9b0",geojsonTypeInvalid:"#f44747",jsonKeyInvalid:"#f44747"},light:{background:"#ffffff",textColor:"#333333",caretColor:"#000",gutterBackground:"#f5f5f5",gutterBorder:"#ddd",jsonKey:"#0000ff",jsonString:"#a31515",jsonNumber:"#098658",jsonBoolean:"#0000ff",jsonNull:"#0000ff",jsonPunctuation:"#333333",controlColor:"#a31515",controlBg:"#e0e0e0",controlBorder:"#999",geojsonKey:"#af00db",geojsonType:"#267f99",geojsonTypeInvalid:"#d32f2f",jsonKeyInvalid:"#d32f2f"}}),n(o,"FEATURE_COLLECTION_PREFIX",'{"type": "FeatureCollection", "features": ['),n(o,"FEATURE_COLLECTION_SUFFIX","]}"),n(o,"ICON_EYE",'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M480-320q75 0 127.5-52.5T660-500q0-75-52.5-127.5T480-680q-75 0-127.5 52.5T300-500q0 75 52.5 127.5T480-320Zm0-72q-45 0-76.5-31.5T372-500q0-45 31.5-76.5T480-608q45 0 76.5 31.5T588-500q0 45-31.5 76.5T480-392Zm0 192q-146 0-266-81.5T40-500q54-137 174-218.5T480-800q146 0 266 81.5T920-500q-54 137-174 218.5T480-200Zm0-300Zm0 220q113 0 207.5-59.5T832-500q-50-101-144.5-160.5T480-720q-113 0-207.5 59.5T128-500q50 101 144.5 160.5T480-280Z"/></svg>'),n(o,"REGEX",{ampersand:/&/g,lessThan:/</g,greaterThan:/>/g,jsonKey:/"([^"]+)"\s*:/g,typeValue:/<span class="geojson-key">"type"<\/span>:\s*"([^"]*)"/g,stringValue:/:\s*"([^"]*)"/g,numberAfterColon:/:\s*(-?\d+\.?\d*)/g,boolean:/:\s*(true|false)/g,nullValue:/:\s*(null)/g,allNumbers:/\b(-?\d+\.?\d*)\b/g,punctuation:/([{}[\],])/g,colorInLine:/"([\w-]+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,collapsibleNode:/^(\s*)"(\w+)"\s*:\s*([{\[])/,collapsedMarker:/^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/}),n(o,"GEOJSON_TYPES_FEATURE",["Feature","FeatureCollection"]),n(o,"GEOJSON_TYPES_GEOMETRY",["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"]),n(o,"GEOJSON_TYPES_ALL",[...o.GEOJSON_TYPES_FEATURE,...o.GEOJSON_TYPES_GEOMETRY]),n(o,"VALID_KEYS_BY_CONTEXT",{Feature:["type","geometry","properties","id","bbox"],FeatureCollection:["type","features","bbox","properties"],Point:["type","coordinates","bbox"],MultiPoint:["type","coordinates","bbox"],LineString:["type","coordinates","bbox"],MultiLineString:["type","coordinates","bbox"],Polygon:["type","coordinates","bbox"],MultiPolygon:["type","coordinates","bbox"],GeometryCollection:["type","geometries","bbox"],properties:null,geometry:["type","coordinates","geometries","bbox"]}),n(o,"CONTEXT_CHANGING_KEYS",{geometry:"geometry",properties:"properties",features:"Feature",geometries:"geometry"}),n(o,"GEOJSON_STRUCTURAL_KEYS",["type","geometry","properties","features","geometries","coordinates","bbox","id","crs"]);let s=o;customElements.define("geojson-editor",s);
|
|
10
|
+
const o=class e extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.collapsedData=new Map,this.colorPositions=[],this.nodeTogglePositions=[],this.hiddenFeatures=new Set,this.featureRanges=new Map,this.highlightTimer=null,this._cachedLineHeight=null,this._cachedPaddingTop=null,this.themes={dark:{},light:{}}}static get observedAttributes(){return["readonly","value","placeholder","dark-selector"]}static _toKebabCase(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase()}connectedCallback(){this.render(),this.setupEventListeners(),this.updatePrefixSuffix(),this.updateThemeCSS(),this.value&&this.updateValue(this.value),this.updatePlaceholderContent()}disconnectedCallback(){const e=document.querySelector(".geojson-color-picker-input");e&&e._closeListener&&(document.removeEventListener("click",e._closeListener,!0),e.remove()),this.highlightTimer&&(clearTimeout(this.highlightTimer),this.highlightTimer=null)}attributeChangedCallback(e,t,n){t!==n&&("value"===e?this.updateValue(n):"readonly"===e?this.updateReadonly():"placeholder"===e?this.updatePlaceholderContent():"dark-selector"===e&&this.updateThemeCSS())}get readonly(){return this.hasAttribute("readonly")}get value(){return this.getAttribute("value")||""}get placeholder(){return this.getAttribute("placeholder")||""}get prefix(){return'{"type": "FeatureCollection", "features": ['}get suffix(){return"]}"}render(){const e=`\n <div class="prefix-wrapper">\n <div class="prefix-gutter"></div>\n <div class="editor-prefix" id="editorPrefix"></div>\n </div>\n <div class="editor-wrapper">\n <div class="gutter">\n <div class="gutter-content" id="gutterContent"></div>\n </div>\n <div class="editor-content">\n <div class="placeholder-layer" id="placeholderLayer">${this.escapeHtml(this.placeholder)}</div>\n <div class="highlight-layer" id="highlightLayer"></div>\n <textarea\n id="textarea"\n spellcheck="false"\n autocomplete="off"\n autocorrect="off"\n autocapitalize="off"\n ></textarea>\n </div>\n </div>\n <div class="suffix-wrapper">\n <div class="suffix-gutter"></div>\n <div class="editor-suffix" id="editorSuffix"></div>\n <button class="clear-btn" id="clearBtn" title="Clear editor">✕</button>\n </div>\n `;this.shadowRoot.innerHTML="\n <style>\n /* Global reset with exact values to prevent external CSS interference */\n :host *,\n :host *::before,\n :host *::after {\n box-sizing: border-box;\n font-family: 'Courier New', Courier, monospace;\n font-size: 13px;\n font-weight: normal;\n font-style: normal;\n font-variant: normal;\n line-height: 1.5;\n letter-spacing: 0;\n text-transform: none;\n text-decoration: none;\n text-indent: 0;\n word-spacing: 0;\n }\n\n :host {\n display: flex;\n flex-direction: column;\n position: relative;\n width: 100%;\n height: 400px;\n border-radius: 4px;\n overflow: hidden;\n }\n\n :host([readonly]) .editor-wrapper::after {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n background: repeating-linear-gradient(\n -45deg,\n rgba(128, 128, 128, 0.08),\n rgba(128, 128, 128, 0.08) 3px,\n transparent 3px,\n transparent 12px\n );\n z-index: 1;\n }\n\n :host([readonly]) textarea {\n cursor: text;\n }\n\n .editor-wrapper {\n position: relative;\n width: 100%;\n flex: 1;\n background: var(--bg-color, #ffffff);\n display: flex;\n }\n\n .gutter {\n width: 24px;\n height: 100%;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n overflow: hidden;\n flex-shrink: 0;\n position: relative;\n }\n\n .gutter-content {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n padding: 8px 4px;\n }\n\n .gutter-line {\n position: absolute;\n left: 0;\n width: 100%;\n height: 1.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .color-indicator {\n width: 12px;\n height: 12px;\n border-radius: 2px;\n border: 1px solid #555;\n cursor: pointer;\n transition: transform 0.1s;\n flex-shrink: 0;\n }\n\n .color-indicator:hover {\n transform: scale(1.2);\n border-color: #fff;\n }\n\n .collapse-button {\n width: 12px;\n height: 12px;\n background: var(--control-bg, #e8e8e8);\n border: 1px solid var(--control-border, #c0c0c0);\n border-radius: 2px;\n color: var(--control-color, #000080);\n font-size: 8px;\n font-weight: bold;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.1s;\n flex-shrink: 0;\n user-select: none;\n }\n\n .collapse-button:hover {\n background: var(--control-bg, #e8e8e8);\n border-color: var(--control-color, #000080);\n transform: scale(1.1);\n }\n\n .visibility-button {\n width: 14px;\n height: 14px;\n background: transparent;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.1s;\n flex-shrink: 0;\n opacity: 0.7;\n padding: 0;\n font-size: 11px;\n }\n\n .visibility-button:hover {\n opacity: 1;\n transform: scale(1.15);\n }\n\n .visibility-button.hidden {\n opacity: 0.35;\n }\n\n /* Hidden feature lines - grayed out */\n .line-hidden {\n opacity: 0.35;\n filter: grayscale(50%);\n }\n\n .color-picker-popup {\n position: absolute;\n background: #2d2d30;\n border: 1px solid #555;\n border-radius: 4px;\n padding: 8px;\n z-index: 1000;\n box-shadow: 0 4px 12px rgba(0,0,0,0.5);\n }\n\n .color-picker-popup input[type=\"color\"] {\n width: 150px;\n height: 30px;\n border: none;\n cursor: pointer;\n }\n\n .editor-content {\n position: relative;\n flex: 1;\n overflow: hidden;\n }\n\n .highlight-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 8px 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow: auto;\n pointer-events: none;\n z-index: 1;\n color: var(--text-color, #000000);\n }\n\n .highlight-layer::-webkit-scrollbar {\n display: none;\n }\n\n textarea {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 8px 12px;\n margin: 0;\n border: none;\n outline: none;\n background: transparent;\n color: transparent;\n caret-color: var(--caret-color, #000);\n white-space: pre-wrap;\n word-wrap: break-word;\n resize: none;\n overflow: auto;\n z-index: 2;\n }\n\n textarea::selection {\n background: rgba(51, 153, 255, 0.3);\n }\n\n textarea::placeholder {\n color: transparent;\n }\n\n .placeholder-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 8px 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n color: #6a6a6a;\n pointer-events: none;\n z-index: 0;\n overflow: hidden;\n }\n\n textarea:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n\n /* Syntax highlighting colors - IntelliJ Light defaults */\n .json-key {\n color: var(--json-key, #660e7a);\n }\n\n .json-string {\n color: var(--json-string, #008000);\n }\n\n .json-number {\n color: var(--json-number, #0000ff);\n }\n\n .json-boolean {\n color: var(--json-boolean, #000080);\n }\n\n .json-null {\n color: var(--json-null, #000080);\n }\n\n .json-punctuation {\n color: var(--json-punct, #000000);\n }\n\n /* GeoJSON-specific highlighting */\n .geojson-key {\n color: var(--geojson-key, #660e7a);\n font-weight: 600;\n }\n\n .geojson-type {\n color: var(--geojson-type, #008000);\n font-weight: 600;\n }\n\n .geojson-type-invalid {\n color: var(--geojson-type-invalid, #ff0000);\n font-weight: 600;\n }\n\n .json-key-invalid {\n color: var(--json-key-invalid, #ff0000);\n }\n\n /* Prefix and suffix wrapper with gutter */\n .prefix-wrapper,\n .suffix-wrapper {\n display: flex;\n flex-shrink: 0;\n background: var(--bg-color, #ffffff);\n }\n\n .prefix-gutter,\n .suffix-gutter {\n width: 24px;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n flex-shrink: 0;\n }\n\n .editor-prefix,\n .editor-suffix {\n flex: 1;\n padding: 4px 12px;\n color: var(--text-color, #000000);\n background: var(--bg-color, #ffffff);\n user-select: none;\n white-space: pre-wrap;\n word-wrap: break-word;\n opacity: 0.6;\n }\n\n .prefix-wrapper {\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n }\n\n .suffix-wrapper {\n border-top: 1px solid rgba(255, 255, 255, 0.1);\n position: relative;\n }\n\n /* Clear button in suffix area */\n .clear-btn {\n position: absolute;\n right: 0.5rem;\n top: 50%;\n transform: translateY(-50%);\n background: transparent;\n border: none;\n color: var(--text-color, #000000);\n opacity: 0.3;\n cursor: pointer;\n font-size: 0.65rem;\n width: 1rem;\n height: 1rem;\n padding: 0.15rem 0 0 0;\n border-radius: 3px;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n transition: opacity 0.2s, background 0.2s;\n }\n .clear-btn:hover {\n opacity: 0.7;\n background: rgba(255, 255, 255, 0.1);\n }\n .clear-btn[hidden] {\n display: none;\n }\n\n /* Scrollbar styling - WebKit (Chrome, Safari, Edge) */\n textarea::-webkit-scrollbar {\n width: 10px;\n height: 10px;\n }\n\n textarea::-webkit-scrollbar-track {\n background: var(--control-bg, #e8e8e8);\n }\n\n textarea::-webkit-scrollbar-thumb {\n background: var(--control-border, #c0c0c0);\n border-radius: 5px;\n }\n\n textarea::-webkit-scrollbar-thumb:hover {\n background: var(--control-color, #000080);\n }\n\n /* Scrollbar styling - Firefox */\n textarea {\n scrollbar-width: thin;\n scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8);\n }\n </style>\n "+e}setupEventListeners(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");e.addEventListener("scroll",()=>{t.scrollTop=e.scrollTop,t.scrollLeft=e.scrollLeft,this.syncGutterScroll(e.scrollTop)}),e.addEventListener("input",()=>{this.updatePlaceholderVisibility(),clearTimeout(this.highlightTimer),this.highlightTimer=setTimeout(()=>{this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange()},150)}),e.addEventListener("paste",()=>{clearTimeout(this.highlightTimer),setTimeout(()=>{this.updatePlaceholderVisibility(),this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange(),this.applyAutoCollapsed()},10)}),this.shadowRoot.getElementById("gutterContent").addEventListener("click",e=>{const t=e.target.closest(".visibility-button");if(t){const e=t.dataset.featureKey;return void this.toggleFeatureVisibility(e)}if(e.target.classList.contains("color-indicator")){const t=parseInt(e.target.dataset.line),n=e.target.dataset.color,o=e.target.dataset.attributeName;this.showColorPicker(e.target,t,n,o)}else if(e.target.classList.contains("collapse-button")){const t=e.target.dataset.nodeKey,n=parseInt(e.target.dataset.line);this.toggleCollapse(t,n)}}),this.shadowRoot.querySelector(".gutter").addEventListener("wheel",t=>{t.preventDefault(),e.scrollTop+=t.deltaY}),e.addEventListener("keydown",e=>{this.handleKeydownInCollapsedArea(e)}),e.addEventListener("copy",e=>{this.handleCopyWithCollapsedContent(e)}),e.addEventListener("cut",e=>{this.handleCutWithCollapsedContent(e)}),this.shadowRoot.getElementById("clearBtn").addEventListener("click",()=>{this.removeAll()}),this.updateReadonly()}syncGutterScroll(e){this.shadowRoot.getElementById("gutterContent").style.transform=`translateY(-${e}px)`}updateReadonly(){const e=this.shadowRoot.getElementById("textarea");e&&(e.disabled=this.readonly);const t=this.shadowRoot.getElementById("clearBtn");t&&(t.hidden=this.readonly)}escapeHtml(t){if(!t)return"";const n=e.REGEX;return t.replace(n.ampersand,"&").replace(n.lessThan,"<").replace(n.greaterThan,">")}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("placeholderLayer");e&&t&&(t.style.display=e.value?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updateValue(e){const t=this.shadowRoot.getElementById("textarea");if(t&&t.value!==e){if(t.value=e||"",e)try{const n=this.prefix,o=this.suffix,s=n.trimEnd().endsWith("["),r=o.trimStart().startsWith("]");if(s&&r){const n="["+e+"]",o=JSON.parse(n),s=JSON.stringify(o,null,2).split("\n");s.length>2?t.value=s.slice(1,-1).join("\n"):t.value=""}else if(!n&&!o){const n=JSON.parse(e);t.value=JSON.stringify(n,null,2)}}catch{}this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateHighlight(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");if(!e||!t)return;const n=e.value;this.updateFeatureRanges();const o=this.getHiddenLineRanges(),{highlighted:s,colors:r,toggles:i}=this.highlightJSON(n,o);t.innerHTML=s,this.colorPositions=r,this.nodeTogglePositions=i,this.updateGutter()}highlightJSON(t,n=[]){if(!t.trim())return{highlighted:"",colors:[],toggles:[]};const o=t.split("\n"),s=[],r=[];let i=[];const a=this.buildContextMap(t);return o.forEach((t,o)=>{const l=e.REGEX;let c;for(l.colorInLine.lastIndex=0;null!==(c=l.colorInLine.exec(t));)s.push({line:o,color:c[2],attributeName:c[1]});const d=t.match(l.collapsibleNode);if(d){const e=d[2];t.includes("{...}")||t.includes("[...]")?r.push({line:o,nodeKey:e,isCollapsed:!0}):this.bracketClosesOnSameLine(t,d[3])||r.push({line:o,nodeKey:e,isCollapsed:!1})}const h=a.get(o);let p=this.highlightSyntax(t,h);(e=>n.some(t=>e>=t.startLine&&e<=t.endLine))(o)&&(p=`<span class="line-hidden">${p}</span>`),i.push(p)}),{highlighted:i.join("\n"),colors:s,toggles:r}}buildContextMap(t){var n;const o=t.split("\n"),s=new Map,r=[];let i=null;const a="Feature";for(let t=0;t<o.length;t++){const l=o[t],c=r.length>0?null==(n=r[r.length-1])?void 0:n.context:a;s.set(t,c);let d=!1,h=!1;for(let t=0;t<l.length;t++){const n=l[t];if(h)h=!1;else if("\\"===n&&d)h=!0;else if('"'!==n){if(!d){if("{"===n||"["===n){let e;if(i)e=i,i=null;else if(0===r.length)e=a;else{const t=r[r.length-1];e=t&&t.isArray?t.context:null}r.push({context:e,isArray:"["===n})}("}"===n||"]"===n)&&r.length>0&&r.pop()}}else{if(!d){const n=l.substring(t).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/);if(n){const o=n[1];e.CONTEXT_CHANGING_KEYS[o]&&(i=e.CONTEXT_CHANGING_KEYS[o]),t+=n[0].length-1;continue}if(r.length>0&&l.substring(0,t).match(/"type"\s*:\s*$/)){const n=l.substring(t).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);if(n&&e.GEOJSON_TYPES_ALL.includes(n[1])){const e=r[r.length-1];e&&(e.context=n[1])}t+=n?n[0].length-1:0;continue}}d=!d}}}return s}highlightSyntax(t,n){if(!t.trim())return"";const o=n?e.VALID_KEYS_BY_CONTEXT[n]:null,s=e.REGEX;return t.replace(s.ampersand,"&").replace(s.lessThan,"<").replace(s.greaterThan,">").replace(s.jsonKey,(t,s)=>"properties"===n?`<span class="json-key">"${s}"</span>:`:e.GEOJSON_STRUCTURAL_KEYS.includes(s)?`<span class="geojson-key">"${s}"</span>:`:(t=>!(!e.GEOJSON_STRUCTURAL_KEYS.includes(t)&&n&&null!=o)||o.includes(t))(s)?`<span class="json-key">"${s}"</span>:`:`<span class="json-key-invalid">"${s}"</span>:`).replace(s.typeValue,(t,o)=>(t=>!n||"properties"===n||("geometry"===n||e.GEOJSON_TYPES_GEOMETRY.includes(n)?e.GEOJSON_TYPES_GEOMETRY.includes(t):"Feature"!==n&&"FeatureCollection"!==n||e.GEOJSON_TYPES_ALL.includes(t)))(o)?`<span class="geojson-key">"type"</span>: <span class="geojson-type">"${o}"</span>`:`<span class="geojson-key">"type"</span>: <span class="geojson-type-invalid">"${o}"</span>`).replace(s.stringValue,(e,t)=>e.includes("<span")?e:`: <span class="json-string">"${t}"</span>`).replace(s.numberAfterColon,': <span class="json-number">$1</span>').replace(s.boolean,': <span class="json-boolean">$1</span>').replace(s.nullValue,': <span class="json-null">$1</span>').replace(s.allNumbers,'<span class="json-number">$1</span>').replace(s.punctuation,'<span class="json-punctuation">$1</span>')}toggleCollapse(e,t){const n=this.shadowRoot.getElementById("textarea"),o=n.value.split("\n"),s=o[t];if(s.includes("{...}")||s.includes("[...]")){let n=null,r=null;const i=`${t}-${e}`;if(this.collapsedData.has(i))n=i,r=this.collapsedData.get(i);else for(const[t,o]of this.collapsedData.entries())if(o.nodeKey===e){const e=s.match(/^(\s*)/)[1].length;if(o.indent===e){n=t,r=o;break}}if(!n||!r)return;const{originalLine:a,content:l}=r;o[t]=a,o.splice(t+1,0,...l),this.collapsedData.delete(n)}else{const n=s.match(/^(\s*)"([^"]+)"\s*:\s*([{\[])/);if(!n)return;const r=n[1],i=n[3];if(0===this._performCollapse(o,t,e,r,i))return}n.value=o.join("\n"),this.updateHighlight()}applyAutoCollapsed(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value)return;const t=e.value.split("\n");for(let e=t.length-1;e>=0;e--){const n=t[e].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(n){const o=n[2];if("coordinates"===o){const s=n[1],r=n[3];this._performCollapse(t,e,o,s,r)}}}e.value=t.join("\n"),this.updateHighlight()}updateGutter(){const e=this.shadowRoot.getElementById("gutterContent"),t=this.shadowRoot.getElementById("textarea");if(!t)return;if(null===this._cachedLineHeight){const e=getComputedStyle(t);this._cachedLineHeight=parseFloat(e.lineHeight),this._cachedPaddingTop=parseFloat(e.paddingTop)}const n=this._cachedLineHeight,o=this._cachedPaddingTop;e.textContent="";const s=new Map,r=e=>(s.has(e)||s.set(e,{colors:[],buttons:[],visibilityButtons:[]}),s.get(e));this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{r(e).colors.push({color:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{r(e).buttons.push({nodeKey:t,isCollapsed:n})});for(const[e,t]of this.featureRanges){const n=this.hiddenFeatures.has(e);r(t.startLine).visibilityButtons.push({featureKey:e,isHidden:n})}const i=document.createDocumentFragment();s.forEach((e,t)=>{const s=document.createElement("div");s.className="gutter-line",s.style.top=`${o+t*n}px`,e.visibilityButtons.forEach(({featureKey:e,isHidden:t})=>{const n=document.createElement("button");n.className="visibility-button"+(t?" hidden":""),n.textContent="👁",n.dataset.featureKey=e,n.title=t?"Show feature in events":"Hide feature from events",s.appendChild(n)}),e.colors.forEach(({color:e,attributeName:n})=>{const o=document.createElement("div");o.className="color-indicator",o.style.backgroundColor=e,o.dataset.line=t,o.dataset.color=e,o.dataset.attributeName=n,o.title=`${n}: ${e}`,s.appendChild(o)}),e.buttons.forEach(({nodeKey:e,isCollapsed:n})=>{const o=document.createElement("div");o.className="collapse-button",o.textContent=n?"+":"-",o.dataset.line=t,o.dataset.nodeKey=e,o.title=n?"Expand":"Collapse",s.appendChild(o)}),i.appendChild(s)}),e.appendChild(i)}showColorPicker(e,t,n,o){const s=document.querySelector(".geojson-color-picker-input");s&&(s._closeListener&&document.removeEventListener("click",s._closeListener,!0),s.remove());const r=document.createElement("input");r.type="color",r.value=n,r.className="geojson-color-picker-input";const i=e.getBoundingClientRect();r.style.position="fixed",r.style.left=`${i.left}px`,r.style.top=`${i.top}px`,r.style.width="12px",r.style.height="12px",r.style.opacity="0.01",r.style.border="none",r.style.padding="0",r.style.zIndex="9999",r.addEventListener("input",e=>{this.updateColorValue(t,e.target.value,o)}),r.addEventListener("change",e=>{this.updateColorValue(t,e.target.value,o)});const a=e=>{e.target!==r&&!r.contains(e.target)&&(document.removeEventListener("click",a,!0),r.remove())};r._closeListener=a,document.body.appendChild(r),setTimeout(()=>{document.addEventListener("click",a,!0)},100),r.focus(),r.click()}updateColorValue(e,t,n){const o=this.shadowRoot.getElementById("textarea"),s=o.value.split("\n"),r=new RegExp(`"${n}"\\s*:\\s*"#[0-9a-fA-F]{6}"`);s[e]=s[e].replace(r,`"${n}": "${t}"`),o.value=s.join("\n"),this.updateHighlight(),this.emitChange()}handleKeydownInCollapsedArea(e){if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Tab"].includes(e.key)||e.ctrlKey||e.metaKey)return;const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.value.substring(0,n).split("\n").length-1,s=t.value.split("\n")[o];s&&(s.includes("{...}")||s.includes("[...]"))&&e.preventDefault()}handleCopyWithCollapsedContent(e){const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n===o)return;const s=t.value.substring(n,o);if(!s.includes("{...}")&&!s.includes("[...]"))return;let r;r=0===n&&o===t.value.length?this.expandAllCollapsed(s):this.expandCollapsedMarkersInText(s,n),e.preventDefault(),e.clipboardData.setData("text/plain",r)}expandCollapsedMarkersInText(t,n){const o=this.shadowRoot.getElementById("textarea").value.substring(0,n).split("\n").length-1,s=e.REGEX,r=t.split("\n"),i=[];return r.forEach((e,t)=>{const n=o+t;if(e.includes("{...}")||e.includes("[...]")){const t=e.match(s.collapsedMarker);if(t){const e=t[2],o=`${n}-${e}`;if(this.collapsedData.has(o)){const e=this.collapsedData.get(o);return i.push(e.originalLine),void i.push(...e.content)}let s=!1;for(const[t,n]of this.collapsedData.entries())if(t.endsWith(`-${e}`)){i.push(n.originalLine),i.push(...n.content),s=!0;break}if(s)return}let o=!1;for(const[e,t]of this.collapsedData.entries())if(parseInt(e.split("-")[0])===n){i.push(t.originalLine),i.push(...t.content),o=!0;break}o||i.push(e)}else i.push(e)}),i.join("\n")}handleCutWithCollapsedContent(e){this.handleCopyWithCollapsedContent(e);const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n!==o){const e=t.value;t.value=e.substring(0,n)+e.substring(o),t.selectionStart=t.selectionEnd=n,this.updateHighlight(),this.updatePlaceholderVisibility(),this.emitChange()}}emitChange(){const e=this.shadowRoot.getElementById("textarea"),t=this.expandAllCollapsed(e.value),n=this.prefix+t+this.suffix;try{let e=JSON.parse(n);e=this.filterHiddenFeatures(e);const o=this.validateGeoJSON(e);o.length>0?this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:(new Date).toISOString(),error:`GeoJSON validation: ${o.join("; ")}`,errors:o,content:t},bubbles:!0,composed:!0})):this.dispatchEvent(new CustomEvent("change",{detail:e,bubbles:!0,composed:!0}))}catch(e){this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:(new Date).toISOString(),error:e.message,content:t},bubbles:!0,composed:!0}))}}filterHiddenFeatures(e){if(!e||0===this.hiddenFeatures.size)return e;if("FeatureCollection"===e.type&&Array.isArray(e.features)){const t=e.features.filter(e=>{const t=this.getFeatureKey(e);return!this.hiddenFeatures.has(t)});return{...e,features:t}}if("Feature"===e.type){const t=this.getFeatureKey(e);if(this.hiddenFeatures.has(t))return{type:"FeatureCollection",features:[]}}return e}getFeatureKey(e){var t,n,o;if(!e||"object"!=typeof e)return null;if(void 0!==e.id)return`id:${e.id}`;if(void 0!==(null==(t=e.properties)?void 0:t.id))return`prop:${e.properties.id}`;const s=(null==(n=e.geometry)?void 0:n.type)||"null",r=JSON.stringify((null==(o=e.geometry)?void 0:o.coordinates)||[]).slice(0,100);return`hash:${s}:${this.simpleHash(r)}`}simpleHash(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t&=t;return t.toString(36)}toggleFeatureVisibility(e){this.hiddenFeatures.has(e)?this.hiddenFeatures.delete(e):this.hiddenFeatures.add(e),this.updateHighlight(),this.updateGutter(),this.emitChange()}updateFeatureRanges(){const e=this.shadowRoot.getElementById("textarea");if(!e)return;const t=e.value;this.featureRanges.clear();try{const e=this.expandAllCollapsed(t),n=this.prefix+e+this.suffix,o=JSON.parse(n);let s=[];"FeatureCollection"===o.type&&Array.isArray(o.features)?s=o.features:"Feature"===o.type&&(s=[o]);const r=t.split("\n");let i=0,a=0,l=!1,c=-1,d=null;for(let e=0;e<r.length;e++){const t=r[e],n=/"type"\s*:\s*"Feature"/.test(t);if(!l&&n){let t=e;for(let n=e;n>=0;n--)if(r[n].includes("{")){t=n;break}c=t,l=!0,a=1;for(let n=t;n<=e;n++){const e=r[n],o=this._countBracketsOutsideStrings(e,"{");a+=n===t?o.open-1-o.close:o.open-o.close}i<s.length&&(d=this.getFeatureKey(s[i]))}else if(l){const n=this._countBracketsOutsideStrings(t,"{");a+=n.open-n.close,a<=0&&(d&&this.featureRanges.set(d,{startLine:c,endLine:e,featureIndex:i}),i++,l=!1,d=null)}}}catch{}}getHiddenLineRanges(){const e=[];for(const[t,n]of this.featureRanges)this.hiddenFeatures.has(t)&&e.push(n);return e}validateGeoJSON(t,n="",o="root"){const s=[];if(!t||"object"!=typeof t)return s;if("properties"!==o&&void 0!==t.type){const r=t.type;"string"==typeof r&&("geometry"===o?e.GEOJSON_TYPES_GEOMETRY.includes(r)||s.push(`Invalid geometry type "${r}" at ${n||"root"} (expected: ${e.GEOJSON_TYPES_GEOMETRY.join(", ")})`):e.GEOJSON_TYPES_FEATURE.includes(r)||s.push(`Invalid type "${r}" at ${n||"root"} (expected: ${e.GEOJSON_TYPES_FEATURE.join(", ")})`))}if(Array.isArray(t))t.forEach((e,t)=>{s.push(...this.validateGeoJSON(e,`${n}[${t}]`,o))});else for(const[e,r]of Object.entries(t))if("object"==typeof r&&null!==r){const t=n?`${n}.${e}`:e;let i=o;"properties"===e?i="properties":"geometry"===e||"geometries"===e?i="geometry":"features"===e&&(i="root"),s.push(...this.validateGeoJSON(r,t,i))}return s}_countBracketsOutsideStrings(e,t){const n="{"===t?"}":"]";let o=0,s=0,r=!1,i=!1;for(let a=0;a<e.length;a++){const l=e[a];i?i=!1:"\\"===l&&r?i=!0:'"'!==l?r||(l===t&&o++,l===n&&s++):r=!r}return{open:o,close:s}}bracketClosesOnSameLine(e,t){const n=e.indexOf(t);if(-1===n)return!1;const o=e.substring(n+1),s=this._countBracketsOutsideStrings(o,t);return s.close>s.open}_findClosingBracket(e,t,n){let o=1;const s=[],r=e[t],i=r.indexOf(n);if(-1!==i){const e=r.substring(i+1),s=this._countBracketsOutsideStrings(e,n);if(o+=s.open-s.close,0===o)return{endLine:t,content:[]}}for(let r=t+1;r<e.length;r++){const t=e[r],i=this._countBracketsOutsideStrings(t,n);if(o+=i.open-i.close,s.push(t),0===o)return{endLine:r,content:s}}return null}_performCollapse(e,t,n,o,s){const r=e[t],i="{"===s?"}":"]";if(this.bracketClosesOnSameLine(r,s))return 0;const a=this._findClosingBracket(e,t,s);if(!a)return 0;const{endLine:l,content:c}=a,d=`${t}-${n}`;this.collapsedData.set(d,{originalLine:r,content:c,indent:o.length,nodeKey:n});const h=r.substring(0,r.indexOf(s)),p=e[l]&&e[l].trim().endsWith(",");e[t]=`${h}${s}...${i}${p?",":""}`;const u=l-t;return e.splice(t+1,u),u}expandAllCollapsed(t){const n=e.REGEX;for(;t.includes("{...}")||t.includes("[...]");){const e=t.split("\n");let o=!1;for(let t=0;t<e.length;t++){const s=e[t];if(!s.includes("{...}")&&!s.includes("[...]"))continue;const r=s.match(n.collapsedMarker);if(!r)continue;const i=r[2],a=r[1].length,l=`${t}-${i}`;let c=this.collapsedData.has(l)?l:null;if(!c)for(const[e,t]of this.collapsedData.entries())if(t.nodeKey===i&&t.indent===a){c=e;break}if(c){const{originalLine:n,content:s}=this.collapsedData.get(c);e[t]=n,e.splice(t+1,0,...s),o=!0;break}}if(!o)break;t=e.join("\n")}return t}formatJSONContent(e){const t=this.prefix,n=this.suffix,o=t.trimEnd().endsWith("["),s=n.trimStart().startsWith("]");if(o&&s){const t="["+e+"]",n=JSON.parse(t),o=JSON.stringify(n,null,2).split("\n");return o.length>2?o.slice(1,-1).join("\n"):""}if(t||n){const o=t+e+n;return JSON.parse(o),e}{const t=JSON.parse(e);return JSON.stringify(t,null,2)}}autoFormatContentWithCursor(){const e=this.shadowRoot.getElementById("textarea"),t=e.selectionStart,n=e.value.substring(0,t).split("\n"),o=n.length-1,s=n[n.length-1].length,r=Array.from(this.collapsedData.values()).map(e=>({nodeKey:e.nodeKey,indent:e.indent})),i=this.expandAllCollapsed(e.value);try{const t=this.formatJSONContent(i);if(t!==i){this.collapsedData.clear(),e.value=t,r.length>0&&this.reapplyCollapsed(r);const n=e.value.split("\n");if(o<n.length){const t=Math.min(s,n[o].length);let r=0;for(let e=0;e<o;e++)r+=n[e].length+1;r+=t,e.setSelectionRange(r,r)}}}catch{}}reapplyCollapsed(e){const t=this.shadowRoot.getElementById("textarea"),n=t.value.split("\n"),o=new Map;e.forEach(({nodeKey:e,indent:t})=>{const n=`${e}-${t}`;o.set(n,(o.get(n)||0)+1)});const s=new Map;for(let e=n.length-1;e>=0;e--){const t=n[e].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(t){const r=t[2],i=`${r}-${t[1].length}`;if(o.has(i)&&(s.set(i,(s.get(i)||0)+1),s.get(i)<=o.get(i))){const o=t[1],s=t[3];this._performCollapse(n,e,r,o,s)}}}t.value=n.join("\n")}parseSelectorToHostRule(e){return e&&""!==e?e.startsWith(".")&&!e.includes(" ")?`:host(${e})`:`:host-context(${e})`:':host([data-color-scheme="dark"])'}updateThemeCSS(){const t=this.getAttribute("dark-selector")||".dark",n=this.parseSelectorToHostRule(t);let o=this.shadowRoot.getElementById("theme-styles");o||(o=document.createElement("style"),o.id="theme-styles",this.shadowRoot.insertBefore(o,this.shadowRoot.firstChild));const s=t=>Object.entries(t||{}).map(([t,n])=>`--${e._toKebabCase(t)}: ${n};`).join("\n "),r=s(this.themes.light);let i="";r&&(i+=`:host {\n ${r}\n }\n`),i+=`${n} {\n ${s({...e.DARK_THEME_DEFAULTS,...this.themes.dark})}\n }`,o.textContent=i}setTheme(e){e.dark&&(this.themes.dark={...this.themes.dark,...e.dark}),e.light&&(this.themes.light={...this.themes.light,...e.light}),this.updateThemeCSS()}resetTheme(){this.themes={dark:{},light:{}},this.updateThemeCSS()}_normalizeIndex(e,t,n=!1){let o=e;return o<0&&(o=t+o),n?Math.max(0,Math.min(o,t)):o<0||o>=t?-1:o}_parseFeatures(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value.trim())return[];try{const t="["+this.expandAllCollapsed(e.value)+"]";return JSON.parse(t)}catch{return[]}}_setFeatures(e){const t=this.shadowRoot.getElementById("textarea");if(t){if(this.collapsedData.clear(),this.hiddenFeatures.clear(),e&&0!==e.length){const n=e.map(e=>JSON.stringify(e,null,2)).join(",\n");t.value=n}else t.value="";this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}_validateFeature(t){const n=[];return t&&"object"==typeof t?Array.isArray(t)?(n.push("Feature cannot be an array"),n):("type"in t?"Feature"!==t.type&&n.push(`Feature type must be "Feature", got "${t.type}"`):n.push('Feature must have a "type" property'),"geometry"in t?null!==t.geometry&&("object"!=typeof t.geometry||Array.isArray(t.geometry)?n.push("Feature geometry must be an object or null"):("type"in t.geometry?e.GEOJSON_TYPES_GEOMETRY.includes(t.geometry.type)||n.push(`Invalid geometry type "${t.geometry.type}" (expected: ${e.GEOJSON_TYPES_GEOMETRY.join(", ")})`):n.push('Geometry must have a "type" property'),"GeometryCollection"!==t.geometry.type&&!("coordinates"in t.geometry)&&n.push('Geometry must have a "coordinates" property'),"GeometryCollection"===t.geometry.type&&!Array.isArray(t.geometry.geometries)&&n.push('GeometryCollection must have a "geometries" array'))):n.push('Feature must have a "geometry" property (can be null)'),"properties"in t?null!==t.properties&&("object"!=typeof t.properties||Array.isArray(t.properties))&&n.push("Feature properties must be an object or null"):n.push('Feature must have a "properties" property (can be null)'),n):(n.push("Feature must be an object"),n)}set(e){if(!Array.isArray(e))throw new Error("set() expects an array of features");const t=[];if(e.forEach((e,n)=>{const o=this._validateFeature(e);o.length>0&&t.push(`Feature[${n}]: ${o.join(", ")}`)}),t.length>0)throw new Error(`Invalid features: ${t.join("; ")}`);this._setFeatures(e)}add(e){const t=this._validateFeature(e);if(t.length>0)throw new Error(`Invalid feature: ${t.join(", ")}`);const n=this._parseFeatures();n.push(e),this._setFeatures(n)}insertAt(e,t){const n=this._validateFeature(e);if(n.length>0)throw new Error(`Invalid feature: ${n.join(", ")}`);const o=this._parseFeatures(),s=this._normalizeIndex(t,o.length,!0);o.splice(s,0,e),this._setFeatures(o)}removeAt(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);if(-1===n)return;const o=t.splice(n,1)[0];return this._setFeatures(t),o}removeAll(){const e=this._parseFeatures();return this._setFeatures([]),e}get(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);return-1!==n?t[n]:void 0}getAll(){return this._parseFeatures()}emit(){this.emitChange()}};n(o,"DARK_THEME_DEFAULTS",{bgColor:"#2b2b2b",textColor:"#a9b7c6",caretColor:"#bbbbbb",gutterBg:"#313335",gutterBorder:"#3c3f41",jsonKey:"#9876aa",jsonString:"#6a8759",jsonNumber:"#6897bb",jsonBoolean:"#cc7832",jsonNull:"#cc7832",jsonPunct:"#a9b7c6",controlColor:"#cc7832",controlBg:"#3c3f41",controlBorder:"#5a5a5a",geojsonKey:"#9876aa",geojsonType:"#6a8759",geojsonTypeInvalid:"#ff6b68",jsonKeyInvalid:"#ff6b68"}),n(o,"REGEX",{ampersand:/&/g,lessThan:/</g,greaterThan:/>/g,jsonKey:/"([^"]+)"\s*:/g,typeValue:/<span class="geojson-key">"type"<\/span>:\s*"([^"]*)"/g,stringValue:/:\s*"([^"]*)"/g,numberAfterColon:/:\s*(-?\d+\.?\d*)/g,boolean:/:\s*(true|false)/g,nullValue:/:\s*(null)/g,allNumbers:/\b(-?\d+\.?\d*)\b/g,punctuation:/([{}[\],])/g,colorInLine:/"([\w-]+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,collapsibleNode:/^(\s*)"(\w+)"\s*:\s*([{\[])/,collapsedMarker:/^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/}),n(o,"GEOJSON_TYPES_FEATURE",["Feature","FeatureCollection"]),n(o,"GEOJSON_TYPES_GEOMETRY",["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"]),n(o,"GEOJSON_TYPES_ALL",[...o.GEOJSON_TYPES_FEATURE,...o.GEOJSON_TYPES_GEOMETRY]),n(o,"VALID_KEYS_BY_CONTEXT",{Feature:["type","geometry","properties","id","bbox"],FeatureCollection:["type","features","bbox","properties"],Point:["type","coordinates","bbox"],MultiPoint:["type","coordinates","bbox"],LineString:["type","coordinates","bbox"],MultiLineString:["type","coordinates","bbox"],Polygon:["type","coordinates","bbox"],MultiPolygon:["type","coordinates","bbox"],GeometryCollection:["type","geometries","bbox"],properties:null,geometry:["type","coordinates","geometries","bbox"]}),n(o,"CONTEXT_CHANGING_KEYS",{geometry:"geometry",properties:"properties",features:"Feature",geometries:"geometry"}),n(o,"GEOJSON_STRUCTURAL_KEYS",["type","geometry","properties","features","geometries","coordinates","bbox","id","crs"]);let s=o;customElements.define("geojson-editor",s);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softwarity/geojson-editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "A feature-rich GeoJSON editor Web Component with syntax highlighting, collapsible nodes, and color picker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/geojson-editor.js",
|