@softwarity/geojson-editor 1.0.4 → 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 +72 -40
- package/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.js +937 -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,20 +27,24 @@ 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 | ❌ | ❌ | ❌ |
|
|
39
36
|
| **Collapsible nodes** | ✅ Native | ✅ | ✅ Plugin | ❌ |
|
|
40
37
|
| **Color picker** | ✅ Integrated | ❌ | ❌ | ❌ |
|
|
38
|
+
| **Feature visibility toggle** | ✅ | ❌ | ❌ | ❌ |
|
|
41
39
|
| **Auto-collapse coordinates** | ✅ | ❌ | ❌ | ❌ |
|
|
42
|
-
| **FeatureCollection
|
|
40
|
+
| **FeatureCollection output** | ✅ Always | ❌ | ❌ | ❌ |
|
|
41
|
+
| **Clear button** | ✅ | ❌ | ❌ | ❌ |
|
|
43
42
|
| **Dark mode detection** | ✅ Auto | ⚠️ Manual | ⚠️ Manual | ⚠️ Manual |
|
|
44
43
|
| **Dependencies** | 0 | Many | Few | 0 |
|
|
45
44
|
| **Setup complexity** | 1 line | Complex | Moderate | Simple |
|
|
46
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
|
+
|
|
47
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.
|
|
48
49
|
|
|
49
50
|
## Features
|
|
@@ -52,13 +53,15 @@ A feature-rich, framework-agnostic **Web Component** for editing GeoJSON feature
|
|
|
52
53
|
- **GeoJSON Type Validation** - Valid types (`Point`, `LineString`, `Polygon`, etc.) highlighted distinctly; invalid types (`LinearRing`, unknown types) shown with error styling (colors configurable via theme)
|
|
53
54
|
- **Syntax Highlighting** - JSON syntax highlighting with customizable color schemes
|
|
54
55
|
- **Collapsible Nodes** - Collapse/expand JSON objects and arrays with visual indicators (`{...}` / `[...]`); `coordinates` auto-collapsed on load
|
|
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)
|
|
55
57
|
- **Color Picker** - Built-in color picker for color properties in left gutter
|
|
56
58
|
- **Dark/Light Themes** - Automatic theme detection from parent page (Bootstrap, Tailwind, custom)
|
|
57
|
-
- **Auto-format** -
|
|
59
|
+
- **Auto-format** - Automatic JSON formatting in real-time (always enabled)
|
|
58
60
|
- **Readonly Mode** - Visual indicator with diagonal stripes when editing is disabled
|
|
59
61
|
- **Block Editing in Collapsed Areas** - Prevents accidental edits in collapsed sections
|
|
60
62
|
- **Smart Copy/Paste** - Copy includes expanded content even from collapsed nodes
|
|
61
|
-
- **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)
|
|
62
65
|
|
|
63
66
|
## Installation
|
|
64
67
|
|
|
@@ -100,7 +103,7 @@ import '@softwarity/geojson-editor';
|
|
|
100
103
|
|
|
101
104
|
## Usage
|
|
102
105
|
|
|
103
|
-
### Basic Usage
|
|
106
|
+
### Basic Usage
|
|
104
107
|
|
|
105
108
|
```html
|
|
106
109
|
<!DOCTYPE html>
|
|
@@ -110,31 +113,17 @@ import '@softwarity/geojson-editor';
|
|
|
110
113
|
</head>
|
|
111
114
|
<body>
|
|
112
115
|
<!-- User edits features, component wraps in FeatureCollection -->
|
|
113
|
-
<geojson-editor
|
|
114
|
-
feature-collection
|
|
115
|
-
placeholder="Enter GeoJSON features here..."
|
|
116
|
-
></geojson-editor>
|
|
116
|
+
<geojson-editor placeholder="Enter GeoJSON features here..."></geojson-editor>
|
|
117
117
|
</body>
|
|
118
118
|
</html>
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
Users edit features directly (comma-separated), and the component automatically wraps them in a `{"type": "FeatureCollection", "features": [...]}` structure for validation and events.
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
<!-- User edits a complete GeoJSON object (Feature or FeatureCollection) -->
|
|
125
|
-
<geojson-editor
|
|
126
|
-
placeholder="Enter GeoJSON here..."
|
|
127
|
-
></geojson-editor>
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### With Auto-format and Theme Detection
|
|
123
|
+
### With Theme Detection
|
|
131
124
|
|
|
132
125
|
```html
|
|
133
|
-
<geojson-editor
|
|
134
|
-
feature-collection
|
|
135
|
-
dark-selector="html.dark"
|
|
136
|
-
auto-format
|
|
137
|
-
></geojson-editor>
|
|
126
|
+
<geojson-editor dark-selector="html.dark"></geojson-editor>
|
|
138
127
|
```
|
|
139
128
|
|
|
140
129
|
### Listen to Changes
|
|
@@ -158,12 +147,10 @@ editor.addEventListener('error', (e) => {
|
|
|
158
147
|
|
|
159
148
|
| Attribute | Type | Default | Description |
|
|
160
149
|
|-----------|------|---------|-------------|
|
|
161
|
-
| `value` | `string` | `""` | Initial editor content |
|
|
150
|
+
| `value` | `string` | `""` | Initial editor content (features array content) |
|
|
162
151
|
| `placeholder` | `string` | `""` | Placeholder text |
|
|
163
152
|
| `readonly` | `boolean` | `false` | Make editor read-only |
|
|
164
|
-
| `auto-format` | `boolean` | `false` | Auto-format JSON on input |
|
|
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
|
+
]);
|
|
185
200
|
|
|
186
|
-
//
|
|
201
|
+
// Add a feature
|
|
202
|
+
editor.add({ type: 'Feature', geometry: { type: 'Point', coordinates: [2, 2] }, properties: {} });
|
|
203
|
+
|
|
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,13 +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
|
-
|
|
216
|
-
<geojson-editor feature-collection></geojson-editor>
|
|
217
|
-
```
|
|
249
|
+
**Example:**
|
|
218
250
|
|
|
219
251
|
```javascript
|
|
220
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.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","auto-format","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){var o;if(t!==n)if("value"===e)this.updateValue(n);else if("readonly"===e)this.updateReadonly();else if("placeholder"===e)this.updatePlaceholderContent();else if("dark-selector"===e)this.updateThemeCSS();else if("feature-collection"===e)this.updatePrefixSuffix();else if("auto-format"===e){const e=null==(o=this.shadowRoot)?void 0:o.getElementById("textarea");e&&e.value&&this.autoFormat&&(this.autoFormatContent(),this.updateHighlight())}}get readonly(){return this.hasAttribute("readonly")}get value(){return this.getAttribute("value")||""}get placeholder(){return this.getAttribute("placeholder")||""}get autoFormat(){return this.hasAttribute("auto-format")}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 .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.autoFormat&&this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange()},150)}),e.addEventListener("paste",()=>{clearTimeout(this.highlightTimer),setTimeout(()=>{this.updatePlaceholderVisibility(),this.autoFormat&&this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange(),this.applyAutoCollapsed()},10)}),this.shadowRoot.getElementById("gutterContent").addEventListener("click",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||"",this.autoFormat&&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,{highlighted:o,colors:s,toggles:i}=this.highlightJSON(n);t.innerHTML=o,this.colorPositions=s,this.nodeTogglePositions=i,this.updateGutter()}highlightJSON(t){if(!t.trim())return{highlighted:"",colors:[],toggles:[]};const n=t.split("\n"),o=[],s=[];let i=[];const r=this.buildContextMap(t);return n.forEach((t,n)=>{const l=e.REGEX;let a;for(l.colorInLine.lastIndex=0;null!==(a=l.colorInLine.exec(t));)o.push({line:n,color:a[2],attributeName:a[1]});const h=t.match(l.collapsibleNode);if(h){const e=h[2];t.includes("{...}")||t.includes("[...]")?s.push({line:n,nodeKey:e,isCollapsed:!0}):this.bracketClosesOnSameLine(t,h[3])||s.push({line:n,nodeKey:e,isCollapsed:!1})}const d=r.get(n);i.push(this.highlightSyntax(t,d))}),{highlighted:i.join("\n"),colors:o,toggles:s}}buildContextMap(t){var n;const o=t.split("\n"),s=new Map,i=[];let r=null;const l=this.featureCollection?"Feature":null;for(let t=0;t<o.length;t++){const a=o[t],h=i.length>0?null==(n=i[i.length-1])?void 0:n.context:l;s.set(t,h);for(let t=0;t<a.length;t++){const n=a[t];if('"'===n){const n=a.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&&a.substring(0,t).match(/"type"\s*:\s*$/)){const n=a.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=l;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:l,content:a}=i;o[t]=l,o.splice(t+1,0,...a),this.collapsedData.delete(n)}else{const n=s.match(/^(\s*)"([^"]+)"\s*:\s*([{\[])/);if(!n)return;const i=n[1],r=n[3],l="{"===r?"}":"]";if(this.bracketClosesOnSameLine(s,r))return;let a=1,h=t;const d=[];for(let e=t+1;e<o.length;e++){const t=o[e];for(const e of t)e===r&&a++,e===l&&a--;if(d.push(t),0===a){h=e;break}}const c=`${t}-${e}`;this.collapsedData.set(c,{originalLine:s,content:d,indent:i.length,nodeKey:e});const p=s.substring(0,s.indexOf(r)),u=o[h]&&o[h].trim().endsWith(",");o[t]=`${p}${r}...${l}${u?",":""}`,o.splice(t+1,h-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],l="{"===r?"}":"]";if(this.bracketClosesOnSameLine(n,r))continue;let a=1,h=e;const d=[];for(let n=e+1;n<t.length;n++){const e=t[n];for(const t of e)t===r&&a++,t===l&&a--;if(d.push(e),0===a){h=n;break}}const c=`${e}-${s}`;this.collapsedData.set(c,{originalLine:n,content:d,indent:i.length,nodeKey:s});const p=n.substring(0,n.indexOf(r)),u=t[h]&&t[h].trim().endsWith(",");t[e]=`${p}${r}...${l}${u?",":""}`,t.splice(e+1,h-e)}}}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;this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{s.has(e)||s.set(e,{colors:[],buttons:[]}),s.get(e).colors.push({color:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{s.has(e)||s.set(e,{colors:[],buttons:[]}),s.get(e).buttons.push({nodeKey:t,isCollapsed: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.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.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 l=e=>{e.target!==i&&!i.contains(e.target)&&(i.remove(),document.removeEventListener("click",l,!0))};document.body.appendChild(i),setTimeout(()=>{document.addEventListener("click",l,!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{const e=JSON.parse(n),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}))}}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],l=i[1].length,a=`${t}-${r}`;let h=this.collapsedData.has(a)?a:null;if(!h)for(const[e,t]of this.collapsedData.entries())if(t.nodeKey===r&&t.indent===l){h=e;break}if(h){const{originalLine:n,content:s}=this.collapsedData.get(h);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],l=`${r}-${i[1].length}`;if(o.has(l)&&(s.set(l,(s.get(l)||0)+1),s.get(l)<=o.get(l))){const o=i[1],s=i[3],l="{"===s?"}":"]";if(this.bracketClosesOnSameLine(t,s))continue;let a=1,h=e;const d=[];for(let t=e+1;t<n.length;t++){const e=n[t];for(const t of e)t===s&&a++,t===l&&a--;if(d.push(e),0===a){h=t;break}}const c=`${e}-${r}`;this.collapsedData.set(c,{originalLine:t,content:d,indent:o.length,nodeKey:r});const p=t.substring(0,t.indexOf(s)),u=n[h]&&n[h].trim().endsWith(",");n[e]=`${p}${s}...${l}${u?",":""}`,n.splice(e+1,h-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,"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",
|