@softwarity/geojson-editor 1.0.5 → 1.0.6

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