@softwarity/geojson-editor 1.0.5 → 1.0.7
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 +129 -37
- package/dist/geojson-editor.js +3 -4
- package/package.json +4 -4
- package/src/geojson-editor.js +871 -690
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
|
|
@@ -55,12 +55,14 @@ A feature-rich, framework-agnostic **Web Component** for editing GeoJSON feature
|
|
|
55
55
|
- **Collapsible Nodes** - Collapse/expand JSON objects and arrays with visual indicators (`{...}` / `[...]`); `coordinates` auto-collapsed on load
|
|
56
56
|
- **Feature Visibility Toggle** - Hide/show individual Features via eye icon in gutter; hidden features are grayed out and excluded from `change` events (useful for temporary filtering without deleting data)
|
|
57
57
|
- **Color Picker** - Built-in color picker for color properties in left gutter
|
|
58
|
+
- **Default Properties** - Auto-inject default visualization properties (fill-color, stroke-color, etc.) into features based on configurable rules
|
|
58
59
|
- **Dark/Light Themes** - Automatic theme detection from parent page (Bootstrap, Tailwind, custom)
|
|
59
60
|
- **Auto-format** - Automatic JSON formatting in real-time (always enabled)
|
|
60
61
|
- **Readonly Mode** - Visual indicator with diagonal stripes when editing is disabled
|
|
61
62
|
- **Block Editing in Collapsed Areas** - Prevents accidental edits in collapsed sections
|
|
62
63
|
- **Smart Copy/Paste** - Copy includes expanded content even from collapsed nodes
|
|
63
|
-
- **FeatureCollection
|
|
64
|
+
- **FeatureCollection Output** - Emits valid FeatureCollection with all edited features
|
|
65
|
+
- **Clear Button** - Discreet ✕ button in suffix area to clear all editor content (hidden in readonly mode)
|
|
64
66
|
|
|
65
67
|
## Installation
|
|
66
68
|
|
|
@@ -102,7 +104,7 @@ import '@softwarity/geojson-editor';
|
|
|
102
104
|
|
|
103
105
|
## Usage
|
|
104
106
|
|
|
105
|
-
### Basic Usage
|
|
107
|
+
### Basic Usage
|
|
106
108
|
|
|
107
109
|
```html
|
|
108
110
|
<!DOCTYPE html>
|
|
@@ -112,30 +114,17 @@ import '@softwarity/geojson-editor';
|
|
|
112
114
|
</head>
|
|
113
115
|
<body>
|
|
114
116
|
<!-- User edits features, component wraps in FeatureCollection -->
|
|
115
|
-
<geojson-editor
|
|
116
|
-
feature-collection
|
|
117
|
-
placeholder="Enter GeoJSON features here..."
|
|
118
|
-
></geojson-editor>
|
|
117
|
+
<geojson-editor placeholder="Enter GeoJSON features here..."></geojson-editor>
|
|
119
118
|
</body>
|
|
120
119
|
</html>
|
|
121
120
|
```
|
|
122
121
|
|
|
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
|
-
```
|
|
122
|
+
Users edit features directly (comma-separated), and the component automatically wraps them in a `{"type": "FeatureCollection", "features": [...]}` structure for validation and events.
|
|
131
123
|
|
|
132
124
|
### With Theme Detection
|
|
133
125
|
|
|
134
126
|
```html
|
|
135
|
-
<geojson-editor
|
|
136
|
-
feature-collection
|
|
137
|
-
dark-selector="html.dark"
|
|
138
|
-
></geojson-editor>
|
|
127
|
+
<geojson-editor dark-selector="html.dark"></geojson-editor>
|
|
139
128
|
```
|
|
140
129
|
|
|
141
130
|
### Listen to Changes
|
|
@@ -159,11 +148,11 @@ editor.addEventListener('error', (e) => {
|
|
|
159
148
|
|
|
160
149
|
| Attribute | Type | Default | Description |
|
|
161
150
|
|-----------|------|---------|-------------|
|
|
162
|
-
| `value` | `string` | `""` | Initial editor content |
|
|
151
|
+
| `value` | `string` | `""` | Initial editor content (features array content) |
|
|
163
152
|
| `placeholder` | `string` | `""` | Placeholder text |
|
|
164
153
|
| `readonly` | `boolean` | `false` | Make editor read-only |
|
|
165
154
|
| `dark-selector` | `string` | `".dark"` | CSS selector for dark theme (if matches → dark, else → light) |
|
|
166
|
-
| `
|
|
155
|
+
| `default-properties` | `string` | `""` | Default properties to add to features (see [Default Properties](#default-properties)) |
|
|
167
156
|
|
|
168
157
|
**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
158
|
|
|
@@ -178,22 +167,129 @@ The `dark-selector` attribute determines when the dark theme is active. If the s
|
|
|
178
167
|
- `html[data-bs-theme=dark]` - HTML has Bootstrap theme attribute: `<html data-bs-theme="dark">`
|
|
179
168
|
- Empty string `""` - Uses component's `data-color-scheme` attribute as fallback
|
|
180
169
|
|
|
170
|
+
### Default Properties
|
|
171
|
+
|
|
172
|
+
The `default-properties` attribute allows you to define default properties that will be automatically added to features. This is useful for setting visualization attributes (fill color, stroke color, opacity, etc.) that your mapping framework can use for styling.
|
|
173
|
+
|
|
174
|
+
**Behavior:**
|
|
175
|
+
- Default properties are **injected directly into the editor content** when features are added (via API or paste/typing)
|
|
176
|
+
- Properties are only added if not already defined on the feature (existing properties are never overwritten)
|
|
177
|
+
- Users can see and modify the default values in the editor
|
|
178
|
+
|
|
179
|
+
#### Simple Format (all features)
|
|
180
|
+
|
|
181
|
+
Apply the same default properties to all features:
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<geojson-editor default-properties='{"fill-color": "#1a465b", "stroke-color": "#000", "stroke-width": 2}'></geojson-editor>
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Conditional Format (based on geometry type or properties)
|
|
188
|
+
|
|
189
|
+
Apply different default properties based on conditions using an array of rules. Conditions support dot notation for nested properties:
|
|
190
|
+
|
|
191
|
+
```html
|
|
192
|
+
<geojson-editor default-properties='[
|
|
193
|
+
{"match": {"geometry.type": "Polygon"}, "values": {"fill-color": "#1a465b", "fill-opacity": 0.5}},
|
|
194
|
+
{"match": {"geometry.type": "LineString"}, "values": {"stroke-color": "#ff0000", "stroke-width": 3}},
|
|
195
|
+
{"match": {"geometry.type": "Point"}, "values": {"marker-color": "#00ff00"}}
|
|
196
|
+
]'></geojson-editor>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Conditional on Feature Properties
|
|
200
|
+
|
|
201
|
+
You can also match on existing feature properties:
|
|
202
|
+
|
|
203
|
+
```html
|
|
204
|
+
<geojson-editor default-properties='[
|
|
205
|
+
{"match": {"properties.type": "airport"}, "values": {"marker-symbol": "airport", "marker-color": "#0000ff"}},
|
|
206
|
+
{"match": {"properties.category": "water"}, "values": {"fill-color": "#0066cc"}},
|
|
207
|
+
{"values": {"stroke-width": 1}}
|
|
208
|
+
]'></geojson-editor>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Combined: Conditionals with Fallback
|
|
212
|
+
|
|
213
|
+
Rules without a `match` condition apply to all features (use as fallback). All matching rules are applied, with later rules taking precedence for the same property:
|
|
214
|
+
|
|
215
|
+
```html
|
|
216
|
+
<geojson-editor default-properties='[
|
|
217
|
+
{"values": {"stroke-width": 1, "stroke-color": "#333"}},
|
|
218
|
+
{"match": {"geometry.type": "Polygon"}, "values": {"fill-color": "#1a465b", "fill-opacity": 0.3}},
|
|
219
|
+
{"match": {"properties.highlighted": true}, "values": {"stroke-color": "#ff0000", "stroke-width": 3}}
|
|
220
|
+
]'></geojson-editor>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
In this example:
|
|
224
|
+
- All features get `stroke-width: 1` and `stroke-color: "#333"` by default
|
|
225
|
+
- Polygons additionally get fill styling
|
|
226
|
+
- Features with `properties.highlighted: true` override the stroke styling
|
|
227
|
+
|
|
228
|
+
**Use Case:** This feature is designed to work seamlessly with mapping libraries like Mapbox GL, Leaflet, OpenLayers, etc. You can define default visualization properties that your layer styling can reference, without manually editing each feature.
|
|
229
|
+
|
|
181
230
|
## API Methods
|
|
182
231
|
|
|
183
232
|
```javascript
|
|
184
233
|
const editor = document.querySelector('geojson-editor');
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Features API
|
|
237
|
+
|
|
238
|
+
Programmatic manipulation of features:
|
|
239
|
+
|
|
240
|
+
| Method | Description |
|
|
241
|
+
|--------|-------------|
|
|
242
|
+
| `set(features[])` | Replace all features with the given array (throws if invalid) |
|
|
243
|
+
| `add(feature)` | Add a feature at the end (throws if invalid) |
|
|
244
|
+
| `insertAt(feature, index)` | Insert at index (negative = from end: -1 = before last) (throws if invalid) |
|
|
245
|
+
| `removeAt(index)` | Remove feature at index (negative = from end), returns removed feature |
|
|
246
|
+
| `removeAll()` | Remove all features, returns array of removed features |
|
|
247
|
+
| `get(index)` | Get feature at index (negative = from end) |
|
|
248
|
+
| `getAll()` | Get all features as an array |
|
|
249
|
+
| `emit()` | Emit the current document on the change event |
|
|
250
|
+
|
|
251
|
+
**Validation:** `set()`, `add()`, and `insertAt()` validate features before adding. Invalid features throw an `Error` with a descriptive message. A valid Feature must have:
|
|
252
|
+
- `type: "Feature"`
|
|
253
|
+
- `geometry`: object with valid type (`Point`, `LineString`, `Polygon`, etc.) and `coordinates`, or `null`
|
|
254
|
+
- `properties`: object or `null`
|
|
255
|
+
|
|
256
|
+
```javascript
|
|
257
|
+
// Set features
|
|
258
|
+
editor.set([
|
|
259
|
+
{ type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] }, properties: {} },
|
|
260
|
+
{ type: 'Feature', geometry: { type: 'Point', coordinates: [1, 1] }, properties: {} }
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
// Add a feature
|
|
264
|
+
editor.add({ type: 'Feature', geometry: { type: 'Point', coordinates: [2, 2] }, properties: {} });
|
|
185
265
|
|
|
186
|
-
//
|
|
266
|
+
// Insert at position 1
|
|
267
|
+
editor.insertAt({ type: 'Feature', ... }, 1);
|
|
268
|
+
|
|
269
|
+
// Remove last feature
|
|
270
|
+
const removed = editor.removeAt(-1);
|
|
271
|
+
|
|
272
|
+
// Get all features
|
|
273
|
+
const features = editor.getAll();
|
|
274
|
+
|
|
275
|
+
// Manually emit change event
|
|
276
|
+
editor.emit();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Theme API
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
// Get current theme
|
|
187
283
|
const themes = editor.getTheme();
|
|
284
|
+
|
|
285
|
+
// Set custom theme (partial update)
|
|
188
286
|
editor.setTheme({
|
|
189
287
|
dark: { background: '#000', textColor: '#fff' },
|
|
190
288
|
light: { background: '#fff', textColor: '#000' }
|
|
191
289
|
});
|
|
192
|
-
editor.resetTheme();
|
|
193
290
|
|
|
194
|
-
//
|
|
195
|
-
editor.
|
|
196
|
-
const value = editor.querySelector('#textarea').value; // via Shadow DOM
|
|
291
|
+
// Reset to defaults
|
|
292
|
+
editor.resetTheme();
|
|
197
293
|
```
|
|
198
294
|
|
|
199
295
|
## Events
|
|
@@ -208,15 +304,11 @@ editor.addEventListener('change', (e) => {
|
|
|
208
304
|
});
|
|
209
305
|
```
|
|
210
306
|
|
|
211
|
-
**Event detail:** The parsed GeoJSON object
|
|
307
|
+
**Event detail:** The parsed GeoJSON object (always a FeatureCollection).
|
|
212
308
|
|
|
213
309
|
**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
310
|
|
|
215
|
-
**Example
|
|
216
|
-
|
|
217
|
-
```html
|
|
218
|
-
<geojson-editor feature-collection></geojson-editor>
|
|
219
|
-
```
|
|
311
|
+
**Example:**
|
|
220
312
|
|
|
221
313
|
```javascript
|
|
222
314
|
// User edits features only, but change event includes the FeatureCollection wrapper
|
package/dist/geojson-editor.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
var e=Object.defineProperty,t=(t,n,o)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[n]=o,n=(e,n,o)=>t(e,"symbol"!=typeof n?n+"":n,o);
|
|
2
1
|
/**
|
|
3
2
|
* @license MIT
|
|
4
3
|
* @name @softwarity/geojson-editor
|
|
5
|
-
* @version 1.0.
|
|
4
|
+
* @version 1.0.7
|
|
6
5
|
* @author Softwarity (https://www.softwarity.io/)
|
|
7
|
-
* @copyright
|
|
6
|
+
* @copyright 2025 Softwarity
|
|
8
7
|
* @see https://github.com/softwarity/geojson-editor
|
|
9
8
|
*/
|
|
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);
|
|
9
|
+
class e extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.collapsedData=/* @__PURE__ */new Map,this.colorPositions=[],this.nodeTogglePositions=[],this.hiddenFeatures=/* @__PURE__ */new Set,this.featureRanges=/* @__PURE__ */new Map,this.highlightTimer=null,this._cachedLineHeight=null,this._cachedPaddingTop=null,this.themes={dark:{},light:{}}}static get observedAttributes(){return["readonly","value","placeholder","dark-selector","default-properties"]}_defaultPropertiesRules=null;static _toKebabCase(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase()}static 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"};static 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*([{\[])\.\.\.([\]\}])/};_findCollapsedData(e,t,n){const o=`${e}-${t}`;if(this.collapsedData.has(o))return{key:o,data:this.collapsedData.get(o)};for(const[s,r]of this.collapsedData.entries())if(r.nodeKey===t&&r.indent===n)return{key:s,data:r};return null}connectedCallback(){this.render(),this.setupEventListeners(),this.updatePrefixSuffix(),this.updateThemeCSS(),this._parseDefaultProperties(),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():"default-properties"===e&&this._parseDefaultProperties())}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"]}"}get defaultProperties(){return this.getAttribute("default-properties")||""}_parseDefaultProperties(){const e=this.defaultProperties;if(!e)return this._defaultPropertiesRules=[],this._defaultPropertiesRules;try{const t=JSON.parse(e);Array.isArray(t)?this._defaultPropertiesRules=t.map(e=>({match:e.match||null,values:e.values||{}})):this._defaultPropertiesRules="object"==typeof t&&null!==t?[{match:null,values:t}]:[]}catch(t){console.warn("geojson-editor: Invalid default-properties JSON:",t.message),this._defaultPropertiesRules=[]}return this._defaultPropertiesRules}_matchesCondition(e,t){if(!t||"object"!=typeof t)return!0;for(const[n,o]of Object.entries(t))if(this._getNestedValue(e,n)!==o)return!1;return!0}_getNestedValue(e,t){const n=t.split(".");let o=e;for(const s of n){if(null==o)return;o=o[s]}return o}_applyDefaultPropertiesToFeature(e){if(!e||"object"!=typeof e)return e;if(!this._defaultPropertiesRules||0===this._defaultPropertiesRules.length)return e;const t={};for(const r of this._defaultPropertiesRules)this._matchesCondition(e,r.match)&&Object.assign(t,r.values);if(0===Object.keys(t).length)return e;const n=e.properties||{},o={...n};let s=!1;for(const[r,i]of Object.entries(t))r in n||(o[r]=i,s=!0);return s?{...e,properties:o}:e}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 /* Base reset - protect against inherited styles */\n :host *, :host *::before, :host *::after {\n box-sizing: border-box;\n font: normal normal 13px/1.5 'Courier New', Courier, monospace;\n font-variant: normal;\n letter-spacing: 0;\n word-spacing: 0;\n text-transform: none;\n text-decoration: none;\n text-indent: 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 inset: 0;\n pointer-events: none;\n background: repeating-linear-gradient(-45deg, rgba(128,128,128,0.08), rgba(128,128,128,0.08) 3px, transparent 3px, transparent 12px);\n z-index: 1;\n }\n\n :host([readonly]) textarea { cursor: text; }\n\n .editor-wrapper {\n position: relative;\n width: 100%;\n flex: 1;\n background: var(--bg-color, #fff);\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, .collapse-button {\n width: 12px;\n height: 12px;\n border-radius: 2px;\n cursor: pointer;\n transition: transform 0.1s;\n flex-shrink: 0;\n }\n\n .color-indicator {\n border: 1px solid #555;\n }\n .color-indicator:hover {\n transform: scale(1.2);\n border-color: #fff;\n }\n\n .collapse-button {\n padding-top: 1px;\n background: var(--control-bg, #e8e8e8);\n border: 1px solid var(--control-border, #c0c0c0);\n color: var(--control-color, #000080);\n font-size: 8px;\n font-weight: bold;\n display: flex;\n align-items: center;\n justify-content: center;\n user-select: none;\n }\n .collapse-button:hover {\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 color: var(--control-color, #000080);\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 .visibility-button:hover { opacity: 1; transform: scale(1.15); }\n .visibility-button.hidden { opacity: 0.35; }\n\n .line-hidden { opacity: 0.35; filter: grayscale(50%); }\n\n .editor-content {\n position: relative;\n flex: 1;\n overflow: hidden;\n }\n\n .highlight-layer, textarea, .placeholder-layer {\n position: absolute;\n inset: 0;\n padding: 8px 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n .highlight-layer {\n overflow: auto;\n pointer-events: none;\n z-index: 1;\n color: var(--text-color, #000);\n }\n .highlight-layer::-webkit-scrollbar { display: none; }\n\n textarea {\n margin: 0;\n border: none;\n outline: none;\n background: transparent;\n color: transparent;\n caret-color: var(--caret-color, #000);\n resize: none;\n overflow: auto;\n z-index: 2;\n }\n textarea::selection { background: rgba(51,153,255,0.3); }\n textarea::placeholder { color: transparent; }\n textarea:disabled { cursor: not-allowed; opacity: 0.6; }\n\n .placeholder-layer {\n color: #6a6a6a;\n pointer-events: none;\n z-index: 0;\n overflow: hidden;\n }\n\n .json-key { color: var(--json-key, #660e7a); }\n .json-string { color: var(--json-string, #008000); }\n .json-number { color: var(--json-number, #00f); }\n .json-boolean, .json-null { color: var(--json-boolean, #000080); }\n .json-punctuation { color: var(--json-punct, #000); }\n .json-key-invalid { color: var(--json-key-invalid, #f00); }\n\n .geojson-key { color: var(--geojson-key, #660e7a); font-weight: 600; }\n .geojson-type { color: var(--geojson-type, #008000); font-weight: 600; }\n .geojson-type-invalid { color: var(--geojson-type-invalid, #f00); font-weight: 600; }\n\n .prefix-wrapper, .suffix-wrapper {\n display: flex;\n flex-shrink: 0;\n background: var(--bg-color, #fff);\n }\n\n .prefix-gutter, .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, .editor-suffix {\n flex: 1;\n padding: 4px 12px;\n color: var(--text-color, #000);\n background: var(--bg-color, #fff);\n user-select: none;\n white-space: pre-wrap;\n word-wrap: break-word;\n opacity: 0.6;\n }\n\n .prefix-wrapper { border-bottom: 1px solid rgba(255,255,255,0.1); }\n .suffix-wrapper { border-top: 1px solid rgba(255,255,255,0.1); position: relative; }\n\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, #000);\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 transition: opacity 0.2s, background 0.2s;\n }\n .clear-btn:hover { opacity: 0.7; background: rgba(255,255,255,0.1); }\n .clear-btn[hidden] { display: none; }\n\n textarea::-webkit-scrollbar { width: 10px; height: 10px; }\n textarea::-webkit-scrollbar-track { background: var(--control-bg, #e8e8e8); }\n textarea::-webkit-scrollbar-thumb { background: var(--control-border, #c0c0c0); border-radius: 5px; }\n textarea::-webkit-scrollbar-thumb:hover { background: var(--control-color, #000080); }\n textarea { scrollbar-width: thin; scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8); }\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="["+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=""}catch(n){}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 u=this.highlightSyntax(t,h);(e=>n.some(t=>e>=t.startLine&&e<=t.endLine))(o)&&(u=`<span class="line-hidden">${u}</span>`),i.push(u)}),{highlighted:i.join("\n"),colors:s,toggles:r}}static GEOJSON={FEATURE_TYPES:["Feature","FeatureCollection"],GEOMETRY_TYPES:["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"],ALL_TYPES:["Feature","FeatureCollection","Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"]};static 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"]};static CONTEXT_CHANGING_KEYS={geometry:"geometry",properties:"properties",features:"Feature",geometries:"geometry"};buildContextMap(t){const n=t.split("\n"),o=/* @__PURE__ */new Map,s=[];let r=null;const i="Feature";for(let a=0;a<n.length;a++){const t=n[a],l=s.length>0?s[s.length-1]?.context:i;o.set(a,l);let c=!1,d=!1;for(let n=0;n<t.length;n++){const o=t[n];if(d)d=!1;else if("\\"===o&&c)d=!0;else if('"'!==o){if(!c){if("{"===o||"["===o){let e;if(r)e=r,r=null;else if(0===s.length)e=i;else{const t=s[s.length-1];e=t&&t.isArray?t.context:null}s.push({context:e,isArray:"["===o})}"}"!==o&&"]"!==o||s.length>0&&s.pop()}}else{if(!c){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/);if(o){const t=o[1];e.CONTEXT_CHANGING_KEYS[t]&&(r=e.CONTEXT_CHANGING_KEYS[t]),n+=o[0].length-1;continue}if(s.length>0&&t.substring(0,n).match(/"type"\s*:\s*$/)){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);if(o&&e.GEOJSON.ALL_TYPES.includes(o[1])){const e=s[s.length-1];e&&(e.context=o[1])}n+=o?o[0].length-1:0;continue}}c=!c}}}return o}static GEOJSON_STRUCTURAL_KEYS=["type","geometry","properties","features","geometries","coordinates","bbox","id","crs"];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.GEOMETRY_TYPES.includes(n)?e.GEOJSON.GEOMETRY_TYPES.includes(t):"Feature"!==n&&"FeatureCollection"!==n||e.GEOJSON.ALL_TYPES.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("[...]")){const n=s.match(/^(\s*)/)[1].length,r=this._findCollapsedData(t,e,n);if(!r)return;const{key:i,data:a}=r,{originalLine:l,content:c}=a;o[t]=l,o.splice(t+1,0,...c),this.collapsedData.delete(i)}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 n=t.length-1;n>=0;n--){const e=t[n].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const o=e[2];if("coordinates"===o){const s=e[1],r=e[3];this._performCollapse(t,n,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=/* @__PURE__ */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[a,l]of this.featureRanges){const e=this.hiddenFeatures.has(a);r(l.startLine).visibilityButtons.push({featureKey:a,isHidden:e})}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))return;if(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=t[1].length,s=this._findCollapsedData(n,e,o);if(s)return i.push(s.data.originalLine),void i.push(...s.data.content);for(const[,t]of this.collapsedData.entries())if(t.nodeKey===e)return i.push(t.originalLine),void i.push(...t.content)}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:/* @__PURE__ */(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(o){this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:/* @__PURE__ */(new Date).toISOString(),error:o.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){if(!e||"object"!=typeof e)return null;if(void 0!==e.id)return`id:${e.id}`;if(void 0!==e.properties?.id)return`prop:${e.properties.id}`;const t=e.geometry?.type||"null",n=JSON.stringify(e.geometry?.coordinates||[]).slice(0,100);return`hash:${t}:${this.simpleHash(n)}`}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 t=0;t<r.length;t++){const e=r[t],n=/"type"\s*:\s*"Feature"/.test(e);if(!l&&n){let e=t;for(let n=t;n>=0;n--)if(r[n].includes("{")){e=n;break}c=e,l=!0,a=1;for(let n=e;n<=t;n++){const t=r[n],o=this._countBracketsOutsideStrings(t,"{");a+=n===e?o.open-1-o.close:o.open-o.close}i<s.length&&(d=this.getFeatureKey(s[i]))}else if(l){const n=this._countBracketsOutsideStrings(e,"{");a+=n.open-n.close,a<=0&&(d&&this.featureRanges.set(d,{startLine:c,endLine:t,featureIndex:i}),i++,l=!1,d=null)}}}catch(n){}}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.GEOMETRY_TYPES.includes(r)||s.push(`Invalid geometry type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.GEOMETRY_TYPES.join(", ")})`):e.GEOJSON.FEATURE_TYPES.includes(r)||s.push(`Invalid type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.FEATURE_TYPES.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 a=t+1;a<e.length;a++){const t=e[a],r=this._countBracketsOutsideStrings(t,n);if(o+=r.open-r.close,s.push(t),0===o)return{endLine:a,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)),u=e[l]&&e[l].trim().endsWith(",");e[t]=`${h}${s}...${i}${u?",":""}`;const p=l-t;return e.splice(t+1,p),p}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=this._findCollapsedData(t,i,a);if(l){const{data:{originalLine:n,content:s}}=l;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="["+e+"]";let n=JSON.parse(t);Array.isArray(n)&&(n=n.map(e=>this._applyDefaultPropertiesToFeature(e)));const o=JSON.stringify(n,null,2).split("\n");return o.length>2?o.slice(1,-1).join("\n"):""}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(a){}}reapplyCollapsed(e){const t=this.shadowRoot.getElementById("textarea"),n=t.value.split("\n"),o=/* @__PURE__ */new Map;e.forEach(({nodeKey:e,indent:t})=>{const n=`${e}-${t}`;o.set(n,(o.get(n)||0)+1)});const s=/* @__PURE__ */new Map;for(let r=n.length-1;r>=0;r--){const e=n[r].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const t=e[2],i=`${t}-${e[1].length}`;if(o.has(i)&&(s.set(i,(s.get(i)||0)+1),s.get(i)<=o.get(i))){const o=e[1],s=e[3];this._performCollapse(n,r,t,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(t){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.GEOMETRY_TYPES.includes(t.geometry.type)||n.push(`Invalid geometry type "${t.geometry.type}" (expected: ${e.GEOJSON.GEOMETRY_TYPES.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("; ")}`);const n=e.map(e=>this._applyDefaultPropertiesToFeature(e));this._setFeatures(n)}add(e){const t=this._validateFeature(e);if(t.length>0)throw new Error(`Invalid feature: ${t.join(", ")}`);const n=this._parseFeatures();n.push(this._applyDefaultPropertiesToFeature(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,this._applyDefaultPropertiesToFeature(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()}}customElements.define("geojson-editor",e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softwarity/geojson-editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"dev": "vite",
|
|
19
|
-
"build": "vite build
|
|
19
|
+
"build": "vite build",
|
|
20
20
|
"preview": "vite preview",
|
|
21
21
|
"test": "web-test-runner",
|
|
22
22
|
"test:watch": "web-test-runner --watch"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@open-wc/testing": "^4.0.0",
|
|
45
45
|
"@web/test-runner": "^0.20.2",
|
|
46
46
|
"@web/test-runner-playwright": "^0.11.1",
|
|
47
|
-
"
|
|
48
|
-
"vite": "^
|
|
47
|
+
"minify-literals": "^1.0.10",
|
|
48
|
+
"vite": "^7.2.4"
|
|
49
49
|
}
|
|
50
50
|
}
|