@softwarity/geojson-editor 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,6 +55,7 @@ A feature-rich, framework-agnostic **Web Component** for editing GeoJSON feature
55
55
  - **Collapsible Nodes** - Collapse/expand JSON objects and arrays with visual indicators (`{...}` / `[...]`); `coordinates` auto-collapsed on load
56
56
  - **Feature Visibility Toggle** - Hide/show individual Features via eye icon in gutter; hidden features are grayed out and excluded from `change` events (useful for temporary filtering without deleting data)
57
57
  - **Color Picker** - Built-in color picker for color properties in left gutter
58
+ - **Default Properties** - Auto-inject default visualization properties (fill-color, stroke-color, etc.) into features based on configurable rules
58
59
  - **Dark/Light Themes** - Automatic theme detection from parent page (Bootstrap, Tailwind, custom)
59
60
  - **Auto-format** - Automatic JSON formatting in real-time (always enabled)
60
61
  - **Readonly Mode** - Visual indicator with diagonal stripes when editing is disabled
@@ -151,6 +152,7 @@ editor.addEventListener('error', (e) => {
151
152
  | `placeholder` | `string` | `""` | Placeholder text |
152
153
  | `readonly` | `boolean` | `false` | Make editor read-only |
153
154
  | `dark-selector` | `string` | `".dark"` | CSS selector for dark theme (if matches → dark, else → light) |
155
+ | `default-properties` | `string` | `""` | Default properties to add to features (see [Default Properties](#default-properties)) |
154
156
 
155
157
  **Note:** `coordinates` nodes are automatically collapsed when content is loaded to improve readability. All nodes can be manually expanded/collapsed by clicking the toggle button.
156
158
 
@@ -165,6 +167,66 @@ The `dark-selector` attribute determines when the dark theme is active. If the s
165
167
  - `html[data-bs-theme=dark]` - HTML has Bootstrap theme attribute: `<html data-bs-theme="dark">`
166
168
  - Empty string `""` - Uses component's `data-color-scheme` attribute as fallback
167
169
 
170
+ ### Default Properties
171
+
172
+ The `default-properties` attribute allows you to define default properties that will be automatically added to features. This is useful for setting visualization attributes (fill color, stroke color, opacity, etc.) that your mapping framework can use for styling.
173
+
174
+ **Behavior:**
175
+ - Default properties are **injected directly into the editor content** when features are added (via API or paste/typing)
176
+ - Properties are only added if not already defined on the feature (existing properties are never overwritten)
177
+ - Users can see and modify the default values in the editor
178
+
179
+ #### Simple Format (all features)
180
+
181
+ Apply the same default properties to all features:
182
+
183
+ ```html
184
+ <geojson-editor default-properties='{"fill-color": "#1a465b", "stroke-color": "#000", "stroke-width": 2}'></geojson-editor>
185
+ ```
186
+
187
+ #### Conditional Format (based on geometry type or properties)
188
+
189
+ Apply different default properties based on conditions using an array of rules. Conditions support dot notation for nested properties:
190
+
191
+ ```html
192
+ <geojson-editor default-properties='[
193
+ {"match": {"geometry.type": "Polygon"}, "values": {"fill-color": "#1a465b", "fill-opacity": 0.5}},
194
+ {"match": {"geometry.type": "LineString"}, "values": {"stroke-color": "#ff0000", "stroke-width": 3}},
195
+ {"match": {"geometry.type": "Point"}, "values": {"marker-color": "#00ff00"}}
196
+ ]'></geojson-editor>
197
+ ```
198
+
199
+ #### Conditional on Feature Properties
200
+
201
+ You can also match on existing feature properties:
202
+
203
+ ```html
204
+ <geojson-editor default-properties='[
205
+ {"match": {"properties.type": "airport"}, "values": {"marker-symbol": "airport", "marker-color": "#0000ff"}},
206
+ {"match": {"properties.category": "water"}, "values": {"fill-color": "#0066cc"}},
207
+ {"values": {"stroke-width": 1}}
208
+ ]'></geojson-editor>
209
+ ```
210
+
211
+ #### Combined: Conditionals with Fallback
212
+
213
+ Rules without a `match` condition apply to all features (use as fallback). All matching rules are applied, with later rules taking precedence for the same property:
214
+
215
+ ```html
216
+ <geojson-editor default-properties='[
217
+ {"values": {"stroke-width": 1, "stroke-color": "#333"}},
218
+ {"match": {"geometry.type": "Polygon"}, "values": {"fill-color": "#1a465b", "fill-opacity": 0.3}},
219
+ {"match": {"properties.highlighted": true}, "values": {"stroke-color": "#ff0000", "stroke-width": 3}}
220
+ ]'></geojson-editor>
221
+ ```
222
+
223
+ In this example:
224
+ - All features get `stroke-width: 1` and `stroke-color: "#333"` by default
225
+ - Polygons additionally get fill styling
226
+ - Features with `properties.highlighted: true` override the stroke styling
227
+
228
+ **Use Case:** This feature is designed to work seamlessly with mapping libraries like Mapbox GL, Leaflet, OpenLayers, etc. You can define default visualization properties that your layer styling can reference, without manually editing each feature.
229
+
168
230
  ## API Methods
169
231
 
170
232
  ```javascript
@@ -1,10 +1,9 @@
1
- var e=Object.defineProperty,t=(t,n,o)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[n]=o,n=(e,n,o)=>t(e,"symbol"!=typeof n?n+"":n,o);
2
1
  /**
3
2
  * @license MIT
4
3
  * @name @softwarity/geojson-editor
5
- * @version 1.0.6
4
+ * @version 1.0.7
6
5
  * @author Softwarity (https://www.softwarity.io/)
7
- * @copyright 2024 Softwarity
6
+ * @copyright 2025 Softwarity
8
7
  * @see https://github.com/softwarity/geojson-editor
9
8
  */
10
- const o=class e extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.collapsedData=new Map,this.colorPositions=[],this.nodeTogglePositions=[],this.hiddenFeatures=new Set,this.featureRanges=new Map,this.highlightTimer=null,this._cachedLineHeight=null,this._cachedPaddingTop=null,this.themes={dark:{},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);
9
+ class e extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.collapsedData=/* @__PURE__ */new Map,this.colorPositions=[],this.nodeTogglePositions=[],this.hiddenFeatures=/* @__PURE__ */new Set,this.featureRanges=/* @__PURE__ */new Map,this.highlightTimer=null,this._cachedLineHeight=null,this._cachedPaddingTop=null,this.themes={dark:{},light:{}}}static get observedAttributes(){return["readonly","value","placeholder","dark-selector","default-properties"]}_defaultPropertiesRules=null;static _toKebabCase(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase()}static DARK_THEME_DEFAULTS={bgColor:"#2b2b2b",textColor:"#a9b7c6",caretColor:"#bbbbbb",gutterBg:"#313335",gutterBorder:"#3c3f41",jsonKey:"#9876aa",jsonString:"#6a8759",jsonNumber:"#6897bb",jsonBoolean:"#cc7832",jsonNull:"#cc7832",jsonPunct:"#a9b7c6",controlColor:"#cc7832",controlBg:"#3c3f41",controlBorder:"#5a5a5a",geojsonKey:"#9876aa",geojsonType:"#6a8759",geojsonTypeInvalid:"#ff6b68",jsonKeyInvalid:"#ff6b68"};static REGEX={ampersand:/&/g,lessThan:/</g,greaterThan:/>/g,jsonKey:/"([^"]+)"\s*:/g,typeValue:/<span class="geojson-key">"type"<\/span>:\s*"([^"]*)"/g,stringValue:/:\s*"([^"]*)"/g,numberAfterColon:/:\s*(-?\d+\.?\d*)/g,boolean:/:\s*(true|false)/g,nullValue:/:\s*(null)/g,allNumbers:/\b(-?\d+\.?\d*)\b/g,punctuation:/([{}[\],])/g,colorInLine:/"([\w-]+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,collapsibleNode:/^(\s*)"(\w+)"\s*:\s*([{\[])/,collapsedMarker:/^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/};_findCollapsedData(e,t,n){const o=`${e}-${t}`;if(this.collapsedData.has(o))return{key:o,data:this.collapsedData.get(o)};for(const[s,r]of this.collapsedData.entries())if(r.nodeKey===t&&r.indent===n)return{key:s,data:r};return null}connectedCallback(){this.render(),this.setupEventListeners(),this.updatePrefixSuffix(),this.updateThemeCSS(),this._parseDefaultProperties(),this.value&&this.updateValue(this.value),this.updatePlaceholderContent()}disconnectedCallback(){const e=document.querySelector(".geojson-color-picker-input");e&&e._closeListener&&(document.removeEventListener("click",e._closeListener,!0),e.remove()),this.highlightTimer&&(clearTimeout(this.highlightTimer),this.highlightTimer=null)}attributeChangedCallback(e,t,n){t!==n&&("value"===e?this.updateValue(n):"readonly"===e?this.updateReadonly():"placeholder"===e?this.updatePlaceholderContent():"dark-selector"===e?this.updateThemeCSS():"default-properties"===e&&this._parseDefaultProperties())}get readonly(){return this.hasAttribute("readonly")}get value(){return this.getAttribute("value")||""}get placeholder(){return this.getAttribute("placeholder")||""}get prefix(){return'{"type": "FeatureCollection", "features": ['}get suffix(){return"]}"}get defaultProperties(){return this.getAttribute("default-properties")||""}_parseDefaultProperties(){const e=this.defaultProperties;if(!e)return this._defaultPropertiesRules=[],this._defaultPropertiesRules;try{const t=JSON.parse(e);Array.isArray(t)?this._defaultPropertiesRules=t.map(e=>({match:e.match||null,values:e.values||{}})):this._defaultPropertiesRules="object"==typeof t&&null!==t?[{match:null,values:t}]:[]}catch(t){console.warn("geojson-editor: Invalid default-properties JSON:",t.message),this._defaultPropertiesRules=[]}return this._defaultPropertiesRules}_matchesCondition(e,t){if(!t||"object"!=typeof t)return!0;for(const[n,o]of Object.entries(t))if(this._getNestedValue(e,n)!==o)return!1;return!0}_getNestedValue(e,t){const n=t.split(".");let o=e;for(const s of n){if(null==o)return;o=o[s]}return o}_applyDefaultPropertiesToFeature(e){if(!e||"object"!=typeof e)return e;if(!this._defaultPropertiesRules||0===this._defaultPropertiesRules.length)return e;const t={};for(const r of this._defaultPropertiesRules)this._matchesCondition(e,r.match)&&Object.assign(t,r.values);if(0===Object.keys(t).length)return e;const n=e.properties||{},o={...n};let s=!1;for(const[r,i]of Object.entries(t))r in n||(o[r]=i,s=!0);return s?{...e,properties:o}:e}render(){const e=`\n <div class="prefix-wrapper">\n <div class="prefix-gutter"></div>\n <div class="editor-prefix" id="editorPrefix"></div>\n </div>\n <div class="editor-wrapper">\n <div class="gutter">\n <div class="gutter-content" id="gutterContent"></div>\n </div>\n <div class="editor-content">\n <div class="placeholder-layer" id="placeholderLayer">${this.escapeHtml(this.placeholder)}</div>\n <div class="highlight-layer" id="highlightLayer"></div>\n <textarea\n id="textarea"\n spellcheck="false"\n autocomplete="off"\n autocorrect="off"\n autocapitalize="off"\n ></textarea>\n </div>\n </div>\n <div class="suffix-wrapper">\n <div class="suffix-gutter"></div>\n <div class="editor-suffix" id="editorSuffix"></div>\n <button class="clear-btn" id="clearBtn" title="Clear editor">✕</button>\n </div>\n `;this.shadowRoot.innerHTML="\n <style>\n /* Base reset - protect against inherited styles */\n :host *, :host *::before, :host *::after {\n box-sizing: border-box;\n font: normal normal 13px/1.5 'Courier New', Courier, monospace;\n font-variant: normal;\n letter-spacing: 0;\n word-spacing: 0;\n text-transform: none;\n text-decoration: none;\n text-indent: 0;\n }\n\n :host {\n display: flex;\n flex-direction: column;\n position: relative;\n width: 100%;\n height: 400px;\n border-radius: 4px;\n overflow: hidden;\n }\n\n :host([readonly]) .editor-wrapper::after {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n background: repeating-linear-gradient(-45deg, rgba(128,128,128,0.08), rgba(128,128,128,0.08) 3px, transparent 3px, transparent 12px);\n z-index: 1;\n }\n\n :host([readonly]) textarea { cursor: text; }\n\n .editor-wrapper {\n position: relative;\n width: 100%;\n flex: 1;\n background: var(--bg-color, #fff);\n display: flex;\n }\n\n .gutter {\n width: 24px;\n height: 100%;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n overflow: hidden;\n flex-shrink: 0;\n position: relative;\n }\n\n .gutter-content {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n padding: 8px 4px;\n }\n\n .gutter-line {\n position: absolute;\n left: 0;\n width: 100%;\n height: 1.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .color-indicator, .collapse-button {\n width: 12px;\n height: 12px;\n border-radius: 2px;\n cursor: pointer;\n transition: transform 0.1s;\n flex-shrink: 0;\n }\n\n .color-indicator {\n border: 1px solid #555;\n }\n .color-indicator:hover {\n transform: scale(1.2);\n border-color: #fff;\n }\n\n .collapse-button {\n padding-top: 1px;\n background: var(--control-bg, #e8e8e8);\n border: 1px solid var(--control-border, #c0c0c0);\n color: var(--control-color, #000080);\n font-size: 8px;\n font-weight: bold;\n display: flex;\n align-items: center;\n justify-content: center;\n user-select: none;\n }\n .collapse-button:hover {\n border-color: var(--control-color, #000080);\n transform: scale(1.1);\n }\n\n .visibility-button {\n width: 14px;\n height: 14px;\n background: transparent;\n color: var(--control-color, #000080);\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.1s;\n flex-shrink: 0;\n opacity: 0.7;\n padding: 0;\n font-size: 11px;\n }\n .visibility-button:hover { opacity: 1; transform: scale(1.15); }\n .visibility-button.hidden { opacity: 0.35; }\n\n .line-hidden { opacity: 0.35; filter: grayscale(50%); }\n\n .editor-content {\n position: relative;\n flex: 1;\n overflow: hidden;\n }\n\n .highlight-layer, textarea, .placeholder-layer {\n position: absolute;\n inset: 0;\n padding: 8px 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n .highlight-layer {\n overflow: auto;\n pointer-events: none;\n z-index: 1;\n color: var(--text-color, #000);\n }\n .highlight-layer::-webkit-scrollbar { display: none; }\n\n textarea {\n margin: 0;\n border: none;\n outline: none;\n background: transparent;\n color: transparent;\n caret-color: var(--caret-color, #000);\n resize: none;\n overflow: auto;\n z-index: 2;\n }\n textarea::selection { background: rgba(51,153,255,0.3); }\n textarea::placeholder { color: transparent; }\n textarea:disabled { cursor: not-allowed; opacity: 0.6; }\n\n .placeholder-layer {\n color: #6a6a6a;\n pointer-events: none;\n z-index: 0;\n overflow: hidden;\n }\n\n .json-key { color: var(--json-key, #660e7a); }\n .json-string { color: var(--json-string, #008000); }\n .json-number { color: var(--json-number, #00f); }\n .json-boolean, .json-null { color: var(--json-boolean, #000080); }\n .json-punctuation { color: var(--json-punct, #000); }\n .json-key-invalid { color: var(--json-key-invalid, #f00); }\n\n .geojson-key { color: var(--geojson-key, #660e7a); font-weight: 600; }\n .geojson-type { color: var(--geojson-type, #008000); font-weight: 600; }\n .geojson-type-invalid { color: var(--geojson-type-invalid, #f00); font-weight: 600; }\n\n .prefix-wrapper, .suffix-wrapper {\n display: flex;\n flex-shrink: 0;\n background: var(--bg-color, #fff);\n }\n\n .prefix-gutter, .suffix-gutter {\n width: 24px;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n flex-shrink: 0;\n }\n\n .editor-prefix, .editor-suffix {\n flex: 1;\n padding: 4px 12px;\n color: var(--text-color, #000);\n background: var(--bg-color, #fff);\n user-select: none;\n white-space: pre-wrap;\n word-wrap: break-word;\n opacity: 0.6;\n }\n\n .prefix-wrapper { border-bottom: 1px solid rgba(255,255,255,0.1); }\n .suffix-wrapper { border-top: 1px solid rgba(255,255,255,0.1); position: relative; }\n\n .clear-btn {\n position: absolute;\n right: 0.5rem;\n top: 50%;\n transform: translateY(-50%);\n background: transparent;\n border: none;\n color: var(--text-color, #000);\n opacity: 0.3;\n cursor: pointer;\n font-size: 0.65rem;\n width: 1rem;\n height: 1rem;\n padding: 0.15rem 0 0 0;\n border-radius: 3px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: opacity 0.2s, background 0.2s;\n }\n .clear-btn:hover { opacity: 0.7; background: rgba(255,255,255,0.1); }\n .clear-btn[hidden] { display: none; }\n\n textarea::-webkit-scrollbar { width: 10px; height: 10px; }\n textarea::-webkit-scrollbar-track { background: var(--control-bg, #e8e8e8); }\n textarea::-webkit-scrollbar-thumb { background: var(--control-border, #c0c0c0); border-radius: 5px; }\n textarea::-webkit-scrollbar-thumb:hover { background: var(--control-color, #000080); }\n textarea { scrollbar-width: thin; scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8); }\n </style>\n "+e}setupEventListeners(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");e.addEventListener("scroll",()=>{t.scrollTop=e.scrollTop,t.scrollLeft=e.scrollLeft,this.syncGutterScroll(e.scrollTop)}),e.addEventListener("input",()=>{this.updatePlaceholderVisibility(),clearTimeout(this.highlightTimer),this.highlightTimer=setTimeout(()=>{this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange()},150)}),e.addEventListener("paste",()=>{clearTimeout(this.highlightTimer),setTimeout(()=>{this.updatePlaceholderVisibility(),this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange(),this.applyAutoCollapsed()},10)}),this.shadowRoot.getElementById("gutterContent").addEventListener("click",e=>{const t=e.target.closest(".visibility-button");if(t){const e=t.dataset.featureKey;return void this.toggleFeatureVisibility(e)}if(e.target.classList.contains("color-indicator")){const t=parseInt(e.target.dataset.line),n=e.target.dataset.color,o=e.target.dataset.attributeName;this.showColorPicker(e.target,t,n,o)}else if(e.target.classList.contains("collapse-button")){const t=e.target.dataset.nodeKey,n=parseInt(e.target.dataset.line);this.toggleCollapse(t,n)}}),this.shadowRoot.querySelector(".gutter").addEventListener("wheel",t=>{t.preventDefault(),e.scrollTop+=t.deltaY}),e.addEventListener("keydown",e=>{this.handleKeydownInCollapsedArea(e)}),e.addEventListener("copy",e=>{this.handleCopyWithCollapsedContent(e)}),e.addEventListener("cut",e=>{this.handleCutWithCollapsedContent(e)}),this.shadowRoot.getElementById("clearBtn").addEventListener("click",()=>{this.removeAll()}),this.updateReadonly()}syncGutterScroll(e){this.shadowRoot.getElementById("gutterContent").style.transform=`translateY(-${e}px)`}updateReadonly(){const e=this.shadowRoot.getElementById("textarea");e&&(e.disabled=this.readonly);const t=this.shadowRoot.getElementById("clearBtn");t&&(t.hidden=this.readonly)}escapeHtml(t){if(!t)return"";const n=e.REGEX;return t.replace(n.ampersand,"&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="["+e+"]",o=JSON.parse(n),s=JSON.stringify(o,null,2).split("\n");s.length>2?t.value=s.slice(1,-1).join("\n"):t.value=""}catch(n){}this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateHighlight(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");if(!e||!t)return;const n=e.value;this.updateFeatureRanges();const o=this.getHiddenLineRanges(),{highlighted:s,colors:r,toggles:i}=this.highlightJSON(n,o);t.innerHTML=s,this.colorPositions=r,this.nodeTogglePositions=i,this.updateGutter()}highlightJSON(t,n=[]){if(!t.trim())return{highlighted:"",colors:[],toggles:[]};const o=t.split("\n"),s=[],r=[];let i=[];const a=this.buildContextMap(t);return o.forEach((t,o)=>{const l=e.REGEX;let c;for(l.colorInLine.lastIndex=0;null!==(c=l.colorInLine.exec(t));)s.push({line:o,color:c[2],attributeName:c[1]});const d=t.match(l.collapsibleNode);if(d){const e=d[2];t.includes("{...}")||t.includes("[...]")?r.push({line:o,nodeKey:e,isCollapsed:!0}):this.bracketClosesOnSameLine(t,d[3])||r.push({line:o,nodeKey:e,isCollapsed:!1})}const h=a.get(o);let u=this.highlightSyntax(t,h);(e=>n.some(t=>e>=t.startLine&&e<=t.endLine))(o)&&(u=`<span class="line-hidden">${u}</span>`),i.push(u)}),{highlighted:i.join("\n"),colors:s,toggles:r}}static GEOJSON={FEATURE_TYPES:["Feature","FeatureCollection"],GEOMETRY_TYPES:["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"],ALL_TYPES:["Feature","FeatureCollection","Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"]};static VALID_KEYS_BY_CONTEXT={Feature:["type","geometry","properties","id","bbox"],FeatureCollection:["type","features","bbox","properties"],Point:["type","coordinates","bbox"],MultiPoint:["type","coordinates","bbox"],LineString:["type","coordinates","bbox"],MultiLineString:["type","coordinates","bbox"],Polygon:["type","coordinates","bbox"],MultiPolygon:["type","coordinates","bbox"],GeometryCollection:["type","geometries","bbox"],properties:null,geometry:["type","coordinates","geometries","bbox"]};static CONTEXT_CHANGING_KEYS={geometry:"geometry",properties:"properties",features:"Feature",geometries:"geometry"};buildContextMap(t){const n=t.split("\n"),o=/* @__PURE__ */new Map,s=[];let r=null;const i="Feature";for(let a=0;a<n.length;a++){const t=n[a],l=s.length>0?s[s.length-1]?.context:i;o.set(a,l);let c=!1,d=!1;for(let n=0;n<t.length;n++){const o=t[n];if(d)d=!1;else if("\\"===o&&c)d=!0;else if('"'!==o){if(!c){if("{"===o||"["===o){let e;if(r)e=r,r=null;else if(0===s.length)e=i;else{const t=s[s.length-1];e=t&&t.isArray?t.context:null}s.push({context:e,isArray:"["===o})}"}"!==o&&"]"!==o||s.length>0&&s.pop()}}else{if(!c){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/);if(o){const t=o[1];e.CONTEXT_CHANGING_KEYS[t]&&(r=e.CONTEXT_CHANGING_KEYS[t]),n+=o[0].length-1;continue}if(s.length>0&&t.substring(0,n).match(/"type"\s*:\s*$/)){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);if(o&&e.GEOJSON.ALL_TYPES.includes(o[1])){const e=s[s.length-1];e&&(e.context=o[1])}n+=o?o[0].length-1:0;continue}}c=!c}}}return o}static GEOJSON_STRUCTURAL_KEYS=["type","geometry","properties","features","geometries","coordinates","bbox","id","crs"];highlightSyntax(t,n){if(!t.trim())return"";const o=n?e.VALID_KEYS_BY_CONTEXT[n]:null,s=e.REGEX;return t.replace(s.ampersand,"&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.GEOMETRY_TYPES.includes(n)?e.GEOJSON.GEOMETRY_TYPES.includes(t):"Feature"!==n&&"FeatureCollection"!==n||e.GEOJSON.ALL_TYPES.includes(t)))(o)?`<span class="geojson-key">"type"</span>: <span class="geojson-type">"${o}"</span>`:`<span class="geojson-key">"type"</span>: <span class="geojson-type-invalid">"${o}"</span>`).replace(s.stringValue,(e,t)=>e.includes("<span")?e:`: <span class="json-string">"${t}"</span>`).replace(s.numberAfterColon,': <span class="json-number">$1</span>').replace(s.boolean,': <span class="json-boolean">$1</span>').replace(s.nullValue,': <span class="json-null">$1</span>').replace(s.allNumbers,'<span class="json-number">$1</span>').replace(s.punctuation,'<span class="json-punctuation">$1</span>')}toggleCollapse(e,t){const n=this.shadowRoot.getElementById("textarea"),o=n.value.split("\n"),s=o[t];if(s.includes("{...}")||s.includes("[...]")){const n=s.match(/^(\s*)/)[1].length,r=this._findCollapsedData(t,e,n);if(!r)return;const{key:i,data:a}=r,{originalLine:l,content:c}=a;o[t]=l,o.splice(t+1,0,...c),this.collapsedData.delete(i)}else{const n=s.match(/^(\s*)"([^"]+)"\s*:\s*([{\[])/);if(!n)return;const r=n[1],i=n[3];if(0===this._performCollapse(o,t,e,r,i))return}n.value=o.join("\n"),this.updateHighlight()}applyAutoCollapsed(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value)return;const t=e.value.split("\n");for(let n=t.length-1;n>=0;n--){const e=t[n].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const o=e[2];if("coordinates"===o){const s=e[1],r=e[3];this._performCollapse(t,n,o,s,r)}}}e.value=t.join("\n"),this.updateHighlight()}updateGutter(){const e=this.shadowRoot.getElementById("gutterContent"),t=this.shadowRoot.getElementById("textarea");if(!t)return;if(null===this._cachedLineHeight){const e=getComputedStyle(t);this._cachedLineHeight=parseFloat(e.lineHeight),this._cachedPaddingTop=parseFloat(e.paddingTop)}const n=this._cachedLineHeight,o=this._cachedPaddingTop;e.textContent="";const s=/* @__PURE__ */new Map,r=e=>(s.has(e)||s.set(e,{colors:[],buttons:[],visibilityButtons:[]}),s.get(e));this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{r(e).colors.push({color:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{r(e).buttons.push({nodeKey:t,isCollapsed:n})});for(const[a,l]of this.featureRanges){const e=this.hiddenFeatures.has(a);r(l.startLine).visibilityButtons.push({featureKey:a,isHidden:e})}const i=document.createDocumentFragment();s.forEach((e,t)=>{const s=document.createElement("div");s.className="gutter-line",s.style.top=`${o+t*n}px`,e.visibilityButtons.forEach(({featureKey:e,isHidden:t})=>{const n=document.createElement("button");n.className="visibility-button"+(t?" hidden":""),n.textContent="👁",n.dataset.featureKey=e,n.title=t?"Show feature in events":"Hide feature from events",s.appendChild(n)}),e.colors.forEach(({color:e,attributeName:n})=>{const o=document.createElement("div");o.className="color-indicator",o.style.backgroundColor=e,o.dataset.line=t,o.dataset.color=e,o.dataset.attributeName=n,o.title=`${n}: ${e}`,s.appendChild(o)}),e.buttons.forEach(({nodeKey:e,isCollapsed:n})=>{const o=document.createElement("div");o.className="collapse-button",o.textContent=n?"+":"-",o.dataset.line=t,o.dataset.nodeKey=e,o.title=n?"Expand":"Collapse",s.appendChild(o)}),i.appendChild(s)}),e.appendChild(i)}showColorPicker(e,t,n,o){const s=document.querySelector(".geojson-color-picker-input");s&&(s._closeListener&&document.removeEventListener("click",s._closeListener,!0),s.remove());const r=document.createElement("input");r.type="color",r.value=n,r.className="geojson-color-picker-input";const i=e.getBoundingClientRect();r.style.position="fixed",r.style.left=`${i.left}px`,r.style.top=`${i.top}px`,r.style.width="12px",r.style.height="12px",r.style.opacity="0.01",r.style.border="none",r.style.padding="0",r.style.zIndex="9999",r.addEventListener("input",e=>{this.updateColorValue(t,e.target.value,o)}),r.addEventListener("change",e=>{this.updateColorValue(t,e.target.value,o)});const a=e=>{e.target===r||r.contains(e.target)||(document.removeEventListener("click",a,!0),r.remove())};r._closeListener=a,document.body.appendChild(r),setTimeout(()=>{document.addEventListener("click",a,!0)},100),r.focus(),r.click()}updateColorValue(e,t,n){const o=this.shadowRoot.getElementById("textarea"),s=o.value.split("\n"),r=new RegExp(`"${n}"\\s*:\\s*"#[0-9a-fA-F]{6}"`);s[e]=s[e].replace(r,`"${n}": "${t}"`),o.value=s.join("\n"),this.updateHighlight(),this.emitChange()}handleKeydownInCollapsedArea(e){if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Tab"].includes(e.key))return;if(e.ctrlKey||e.metaKey)return;const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.value.substring(0,n).split("\n").length-1,s=t.value.split("\n")[o];s&&(s.includes("{...}")||s.includes("[...]"))&&e.preventDefault()}handleCopyWithCollapsedContent(e){const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n===o)return;const s=t.value.substring(n,o);if(!s.includes("{...}")&&!s.includes("[...]"))return;let r;r=0===n&&o===t.value.length?this.expandAllCollapsed(s):this.expandCollapsedMarkersInText(s,n),e.preventDefault(),e.clipboardData.setData("text/plain",r)}expandCollapsedMarkersInText(t,n){const o=this.shadowRoot.getElementById("textarea").value.substring(0,n).split("\n").length-1,s=e.REGEX,r=t.split("\n"),i=[];return r.forEach((e,t)=>{const n=o+t;if(e.includes("{...}")||e.includes("[...]")){const t=e.match(s.collapsedMarker);if(t){const e=t[2],o=t[1].length,s=this._findCollapsedData(n,e,o);if(s)return i.push(s.data.originalLine),void i.push(...s.data.content);for(const[,t]of this.collapsedData.entries())if(t.nodeKey===e)return i.push(t.originalLine),void i.push(...t.content)}i.push(e)}else i.push(e)}),i.join("\n")}handleCutWithCollapsedContent(e){this.handleCopyWithCollapsedContent(e);const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n!==o){const e=t.value;t.value=e.substring(0,n)+e.substring(o),t.selectionStart=t.selectionEnd=n,this.updateHighlight(),this.updatePlaceholderVisibility(),this.emitChange()}}emitChange(){const e=this.shadowRoot.getElementById("textarea"),t=this.expandAllCollapsed(e.value),n=this.prefix+t+this.suffix;try{let e=JSON.parse(n);e=this.filterHiddenFeatures(e);const o=this.validateGeoJSON(e);o.length>0?this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:/* @__PURE__ */(new Date).toISOString(),error:`GeoJSON validation: ${o.join("; ")}`,errors:o,content:t},bubbles:!0,composed:!0})):this.dispatchEvent(new CustomEvent("change",{detail:e,bubbles:!0,composed:!0}))}catch(o){this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:/* @__PURE__ */(new Date).toISOString(),error:o.message,content:t},bubbles:!0,composed:!0}))}}filterHiddenFeatures(e){if(!e||0===this.hiddenFeatures.size)return e;if("FeatureCollection"===e.type&&Array.isArray(e.features)){const t=e.features.filter(e=>{const t=this.getFeatureKey(e);return!this.hiddenFeatures.has(t)});return{...e,features:t}}if("Feature"===e.type){const t=this.getFeatureKey(e);if(this.hiddenFeatures.has(t))return{type:"FeatureCollection",features:[]}}return e}getFeatureKey(e){if(!e||"object"!=typeof e)return null;if(void 0!==e.id)return`id:${e.id}`;if(void 0!==e.properties?.id)return`prop:${e.properties.id}`;const t=e.geometry?.type||"null",n=JSON.stringify(e.geometry?.coordinates||[]).slice(0,100);return`hash:${t}:${this.simpleHash(n)}`}simpleHash(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t&=t;return t.toString(36)}toggleFeatureVisibility(e){this.hiddenFeatures.has(e)?this.hiddenFeatures.delete(e):this.hiddenFeatures.add(e),this.updateHighlight(),this.updateGutter(),this.emitChange()}updateFeatureRanges(){const e=this.shadowRoot.getElementById("textarea");if(!e)return;const t=e.value;this.featureRanges.clear();try{const e=this.expandAllCollapsed(t),n=this.prefix+e+this.suffix,o=JSON.parse(n);let s=[];"FeatureCollection"===o.type&&Array.isArray(o.features)?s=o.features:"Feature"===o.type&&(s=[o]);const r=t.split("\n");let i=0,a=0,l=!1,c=-1,d=null;for(let t=0;t<r.length;t++){const e=r[t],n=/"type"\s*:\s*"Feature"/.test(e);if(!l&&n){let e=t;for(let n=t;n>=0;n--)if(r[n].includes("{")){e=n;break}c=e,l=!0,a=1;for(let n=e;n<=t;n++){const t=r[n],o=this._countBracketsOutsideStrings(t,"{");a+=n===e?o.open-1-o.close:o.open-o.close}i<s.length&&(d=this.getFeatureKey(s[i]))}else if(l){const n=this._countBracketsOutsideStrings(e,"{");a+=n.open-n.close,a<=0&&(d&&this.featureRanges.set(d,{startLine:c,endLine:t,featureIndex:i}),i++,l=!1,d=null)}}}catch(n){}}getHiddenLineRanges(){const e=[];for(const[t,n]of this.featureRanges)this.hiddenFeatures.has(t)&&e.push(n);return e}validateGeoJSON(t,n="",o="root"){const s=[];if(!t||"object"!=typeof t)return s;if("properties"!==o&&void 0!==t.type){const r=t.type;"string"==typeof r&&("geometry"===o?e.GEOJSON.GEOMETRY_TYPES.includes(r)||s.push(`Invalid geometry type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.GEOMETRY_TYPES.join(", ")})`):e.GEOJSON.FEATURE_TYPES.includes(r)||s.push(`Invalid type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.FEATURE_TYPES.join(", ")})`))}if(Array.isArray(t))t.forEach((e,t)=>{s.push(...this.validateGeoJSON(e,`${n}[${t}]`,o))});else for(const[e,r]of Object.entries(t))if("object"==typeof r&&null!==r){const t=n?`${n}.${e}`:e;let i=o;"properties"===e?i="properties":"geometry"===e||"geometries"===e?i="geometry":"features"===e&&(i="root"),s.push(...this.validateGeoJSON(r,t,i))}return s}_countBracketsOutsideStrings(e,t){const n="{"===t?"}":"]";let o=0,s=0,r=!1,i=!1;for(let a=0;a<e.length;a++){const l=e[a];i?i=!1:"\\"===l&&r?i=!0:'"'!==l?r||(l===t&&o++,l===n&&s++):r=!r}return{open:o,close:s}}bracketClosesOnSameLine(e,t){const n=e.indexOf(t);if(-1===n)return!1;const o=e.substring(n+1),s=this._countBracketsOutsideStrings(o,t);return s.close>s.open}_findClosingBracket(e,t,n){let o=1;const s=[],r=e[t],i=r.indexOf(n);if(-1!==i){const e=r.substring(i+1),s=this._countBracketsOutsideStrings(e,n);if(o+=s.open-s.close,0===o)return{endLine:t,content:[]}}for(let a=t+1;a<e.length;a++){const t=e[a],r=this._countBracketsOutsideStrings(t,n);if(o+=r.open-r.close,s.push(t),0===o)return{endLine:a,content:s}}return null}_performCollapse(e,t,n,o,s){const r=e[t],i="{"===s?"}":"]";if(this.bracketClosesOnSameLine(r,s))return 0;const a=this._findClosingBracket(e,t,s);if(!a)return 0;const{endLine:l,content:c}=a,d=`${t}-${n}`;this.collapsedData.set(d,{originalLine:r,content:c,indent:o.length,nodeKey:n});const h=r.substring(0,r.indexOf(s)),u=e[l]&&e[l].trim().endsWith(",");e[t]=`${h}${s}...${i}${u?",":""}`;const p=l-t;return e.splice(t+1,p),p}expandAllCollapsed(t){const n=e.REGEX;for(;t.includes("{...}")||t.includes("[...]");){const e=t.split("\n");let o=!1;for(let t=0;t<e.length;t++){const s=e[t];if(!s.includes("{...}")&&!s.includes("[...]"))continue;const r=s.match(n.collapsedMarker);if(!r)continue;const i=r[2],a=r[1].length,l=this._findCollapsedData(t,i,a);if(l){const{data:{originalLine:n,content:s}}=l;e[t]=n,e.splice(t+1,0,...s),o=!0;break}}if(!o)break;t=e.join("\n")}return t}formatJSONContent(e){const t="["+e+"]";let n=JSON.parse(t);Array.isArray(n)&&(n=n.map(e=>this._applyDefaultPropertiesToFeature(e)));const o=JSON.stringify(n,null,2).split("\n");return o.length>2?o.slice(1,-1).join("\n"):""}autoFormatContentWithCursor(){const e=this.shadowRoot.getElementById("textarea"),t=e.selectionStart,n=e.value.substring(0,t).split("\n"),o=n.length-1,s=n[n.length-1].length,r=Array.from(this.collapsedData.values()).map(e=>({nodeKey:e.nodeKey,indent:e.indent})),i=this.expandAllCollapsed(e.value);try{const t=this.formatJSONContent(i);if(t!==i){this.collapsedData.clear(),e.value=t,r.length>0&&this.reapplyCollapsed(r);const n=e.value.split("\n");if(o<n.length){const t=Math.min(s,n[o].length);let r=0;for(let e=0;e<o;e++)r+=n[e].length+1;r+=t,e.setSelectionRange(r,r)}}}catch(a){}}reapplyCollapsed(e){const t=this.shadowRoot.getElementById("textarea"),n=t.value.split("\n"),o=/* @__PURE__ */new Map;e.forEach(({nodeKey:e,indent:t})=>{const n=`${e}-${t}`;o.set(n,(o.get(n)||0)+1)});const s=/* @__PURE__ */new Map;for(let r=n.length-1;r>=0;r--){const e=n[r].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const t=e[2],i=`${t}-${e[1].length}`;if(o.has(i)&&(s.set(i,(s.get(i)||0)+1),s.get(i)<=o.get(i))){const o=e[1],s=e[3];this._performCollapse(n,r,t,o,s)}}}t.value=n.join("\n")}parseSelectorToHostRule(e){return e&&""!==e?e.startsWith(".")&&!e.includes(" ")?`:host(${e})`:`:host-context(${e})`:':host([data-color-scheme="dark"])'}updateThemeCSS(){const t=this.getAttribute("dark-selector")||".dark",n=this.parseSelectorToHostRule(t);let o=this.shadowRoot.getElementById("theme-styles");o||(o=document.createElement("style"),o.id="theme-styles",this.shadowRoot.insertBefore(o,this.shadowRoot.firstChild));const s=t=>Object.entries(t||{}).map(([t,n])=>`--${e._toKebabCase(t)}: ${n};`).join("\n "),r=s(this.themes.light);let i="";r&&(i+=`:host {\n ${r}\n }\n`),i+=`${n} {\n ${s({...e.DARK_THEME_DEFAULTS,...this.themes.dark})}\n }`,o.textContent=i}setTheme(e){e.dark&&(this.themes.dark={...this.themes.dark,...e.dark}),e.light&&(this.themes.light={...this.themes.light,...e.light}),this.updateThemeCSS()}resetTheme(){this.themes={dark:{},light:{}},this.updateThemeCSS()}_normalizeIndex(e,t,n=!1){let o=e;return o<0&&(o=t+o),n?Math.max(0,Math.min(o,t)):o<0||o>=t?-1:o}_parseFeatures(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value.trim())return[];try{const t="["+this.expandAllCollapsed(e.value)+"]";return JSON.parse(t)}catch(t){return[]}}_setFeatures(e){const t=this.shadowRoot.getElementById("textarea");if(t){if(this.collapsedData.clear(),this.hiddenFeatures.clear(),e&&0!==e.length){const n=e.map(e=>JSON.stringify(e,null,2)).join(",\n");t.value=n}else t.value="";this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}_validateFeature(t){const n=[];return t&&"object"==typeof t?Array.isArray(t)?(n.push("Feature cannot be an array"),n):("type"in t?"Feature"!==t.type&&n.push(`Feature type must be "Feature", got "${t.type}"`):n.push('Feature must have a "type" property'),"geometry"in t?null!==t.geometry&&("object"!=typeof t.geometry||Array.isArray(t.geometry)?n.push("Feature geometry must be an object or null"):("type"in t.geometry?e.GEOJSON.GEOMETRY_TYPES.includes(t.geometry.type)||n.push(`Invalid geometry type "${t.geometry.type}" (expected: ${e.GEOJSON.GEOMETRY_TYPES.join(", ")})`):n.push('Geometry must have a "type" property'),"GeometryCollection"===t.geometry.type||"coordinates"in t.geometry||n.push('Geometry must have a "coordinates" property'),"GeometryCollection"!==t.geometry.type||Array.isArray(t.geometry.geometries)||n.push('GeometryCollection must have a "geometries" array'))):n.push('Feature must have a "geometry" property (can be null)'),"properties"in t?null===t.properties||"object"==typeof t.properties&&!Array.isArray(t.properties)||n.push("Feature properties must be an object or null"):n.push('Feature must have a "properties" property (can be null)'),n):(n.push("Feature must be an object"),n)}set(e){if(!Array.isArray(e))throw new Error("set() expects an array of features");const t=[];if(e.forEach((e,n)=>{const o=this._validateFeature(e);o.length>0&&t.push(`Feature[${n}]: ${o.join(", ")}`)}),t.length>0)throw new Error(`Invalid features: ${t.join("; ")}`);const n=e.map(e=>this._applyDefaultPropertiesToFeature(e));this._setFeatures(n)}add(e){const t=this._validateFeature(e);if(t.length>0)throw new Error(`Invalid feature: ${t.join(", ")}`);const n=this._parseFeatures();n.push(this._applyDefaultPropertiesToFeature(e)),this._setFeatures(n)}insertAt(e,t){const n=this._validateFeature(e);if(n.length>0)throw new Error(`Invalid feature: ${n.join(", ")}`);const o=this._parseFeatures(),s=this._normalizeIndex(t,o.length,!0);o.splice(s,0,this._applyDefaultPropertiesToFeature(e)),this._setFeatures(o)}removeAt(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);if(-1===n)return;const o=t.splice(n,1)[0];return this._setFeatures(t),o}removeAll(){const e=this._parseFeatures();return this._setFeatures([]),e}get(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);return-1!==n?t[n]:void 0}getAll(){return this._parseFeatures()}emit(){this.emitChange()}}customElements.define("geojson-editor",e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softwarity/geojson-editor",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "A feature-rich GeoJSON editor Web Component with syntax highlighting, collapsible nodes, and color picker",
5
5
  "type": "module",
6
6
  "main": "./dist/geojson-editor.js",
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "dev": "vite",
19
- "build": "vite build && terser dist/geojson-editor.js -o dist/geojson-editor.js --compress passes=3 --mangle toplevel=true --comments /license/i",
19
+ "build": "vite build",
20
20
  "preview": "vite preview",
21
21
  "test": "web-test-runner",
22
22
  "test:watch": "web-test-runner --watch"
@@ -44,7 +44,7 @@
44
44
  "@open-wc/testing": "^4.0.0",
45
45
  "@web/test-runner": "^0.20.2",
46
46
  "@web/test-runner-playwright": "^0.11.1",
47
- "terser": "^5.44.1",
48
- "vite": "^5.0.0"
47
+ "minify-literals": "^1.0.10",
48
+ "vite": "^7.2.4"
49
49
  }
50
50
  }
@@ -22,9 +22,12 @@ class GeoJsonEditor extends HTMLElement {
22
22
  }
23
23
 
24
24
  static get observedAttributes() {
25
- return ['readonly', 'value', 'placeholder', 'dark-selector'];
25
+ return ['readonly', 'value', 'placeholder', 'dark-selector', 'default-properties'];
26
26
  }
27
27
 
28
+ // Parsed default properties rules (cache)
29
+ _defaultPropertiesRules = null;
30
+
28
31
  // Helper: Convert camelCase to kebab-case
29
32
  static _toKebabCase(str) {
30
33
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
@@ -73,6 +76,31 @@ class GeoJsonEditor extends HTMLElement {
73
76
  collapsedMarker: /^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/
74
77
  };
75
78
 
79
+ /**
80
+ * Find collapsed data by line index, nodeKey, and indent
81
+ * @param {number} lineIndex - Current line index
82
+ * @param {string} nodeKey - Node key to find
83
+ * @param {number} indent - Indentation level to match
84
+ * @returns {{key: string, data: Object}|null} Found key and data, or null
85
+ * @private
86
+ */
87
+ _findCollapsedData(lineIndex, nodeKey, indent) {
88
+ // Try exact match first
89
+ const exactKey = `${lineIndex}-${nodeKey}`;
90
+ if (this.collapsedData.has(exactKey)) {
91
+ return { key: exactKey, data: this.collapsedData.get(exactKey) };
92
+ }
93
+
94
+ // Search for any key with this nodeKey and matching indent
95
+ for (const [key, data] of this.collapsedData.entries()) {
96
+ if (data.nodeKey === nodeKey && data.indent === indent) {
97
+ return { key, data };
98
+ }
99
+ }
100
+
101
+ return null;
102
+ }
103
+
76
104
  connectedCallback() {
77
105
  this.render();
78
106
  this.setupEventListeners();
@@ -83,6 +111,9 @@ class GeoJsonEditor extends HTMLElement {
83
111
  // Setup theme CSS
84
112
  this.updateThemeCSS();
85
113
 
114
+ // Parse default properties rules
115
+ this._parseDefaultProperties();
116
+
86
117
  // Initialize textarea with value attribute (attributeChangedCallback fires before render)
87
118
  if (this.value) {
88
119
  this.updateValue(this.value);
@@ -116,6 +147,9 @@ class GeoJsonEditor extends HTMLElement {
116
147
  this.updatePlaceholderContent();
117
148
  } else if (name === 'dark-selector') {
118
149
  this.updateThemeCSS();
150
+ } else if (name === 'default-properties') {
151
+ // Re-parse the default properties rules
152
+ this._parseDefaultProperties();
119
153
  }
120
154
  }
121
155
 
@@ -142,25 +176,130 @@ class GeoJsonEditor extends HTMLElement {
142
176
  return ']}';
143
177
  }
144
178
 
179
+ get defaultProperties() {
180
+ return this.getAttribute('default-properties') || '';
181
+ }
182
+
183
+ /**
184
+ * Parse and cache the default-properties attribute.
185
+ * Supports two formats:
186
+ * 1. Simple object: {"fill-color": "#1a465b", "stroke-width": 2}
187
+ * 2. Conditional array: [{"match": {"geometry.type": "Polygon"}, "values": {...}}, ...]
188
+ *
189
+ * Returns an array of rules: [{match: null|object, values: object}]
190
+ */
191
+ _parseDefaultProperties() {
192
+ const attr = this.defaultProperties;
193
+ if (!attr) {
194
+ this._defaultPropertiesRules = [];
195
+ return this._defaultPropertiesRules;
196
+ }
197
+
198
+ try {
199
+ const parsed = JSON.parse(attr);
200
+
201
+ if (Array.isArray(parsed)) {
202
+ // Conditional format: array of rules
203
+ this._defaultPropertiesRules = parsed.map(rule => ({
204
+ match: rule.match || null,
205
+ values: rule.values || {}
206
+ }));
207
+ } else if (typeof parsed === 'object' && parsed !== null) {
208
+ // Simple format: single object of properties for all features
209
+ this._defaultPropertiesRules = [{ match: null, values: parsed }];
210
+ } else {
211
+ this._defaultPropertiesRules = [];
212
+ }
213
+ } catch (e) {
214
+ console.warn('geojson-editor: Invalid default-properties JSON:', e.message);
215
+ this._defaultPropertiesRules = [];
216
+ }
217
+
218
+ return this._defaultPropertiesRules;
219
+ }
220
+
221
+ /**
222
+ * Check if a feature matches a condition.
223
+ * Supports dot notation for nested properties:
224
+ * - "geometry.type": "Polygon"
225
+ * - "properties.category": "airport"
226
+ */
227
+ _matchesCondition(feature, match) {
228
+ if (!match || typeof match !== 'object') return true;
229
+
230
+ for (const [path, expectedValue] of Object.entries(match)) {
231
+ const actualValue = this._getNestedValue(feature, path);
232
+ if (actualValue !== expectedValue) {
233
+ return false;
234
+ }
235
+ }
236
+ return true;
237
+ }
238
+
239
+ /**
240
+ * Get a nested value from an object using dot notation.
241
+ * E.g., _getNestedValue(feature, "geometry.type") => "Polygon"
242
+ */
243
+ _getNestedValue(obj, path) {
244
+ const parts = path.split('.');
245
+ let current = obj;
246
+ for (const part of parts) {
247
+ if (current === null || current === undefined) return undefined;
248
+ current = current[part];
249
+ }
250
+ return current;
251
+ }
252
+
253
+ /**
254
+ * Apply default properties to a single feature.
255
+ * Only adds properties that don't already exist.
256
+ * Returns a new feature object (doesn't mutate original).
257
+ */
258
+ _applyDefaultPropertiesToFeature(feature) {
259
+ if (!feature || typeof feature !== 'object') return feature;
260
+ if (!this._defaultPropertiesRules || this._defaultPropertiesRules.length === 0) return feature;
261
+
262
+ // Collect all properties to apply (later rules override earlier for same key)
263
+ const propsToApply = {};
264
+
265
+ for (const rule of this._defaultPropertiesRules) {
266
+ if (this._matchesCondition(feature, rule.match)) {
267
+ Object.assign(propsToApply, rule.values);
268
+ }
269
+ }
270
+
271
+ if (Object.keys(propsToApply).length === 0) return feature;
272
+
273
+ // Apply only properties that don't already exist
274
+ const existingProps = feature.properties || {};
275
+ const newProps = { ...existingProps };
276
+ let hasChanges = false;
277
+
278
+ for (const [key, value] of Object.entries(propsToApply)) {
279
+ if (!(key in existingProps)) {
280
+ newProps[key] = value;
281
+ hasChanges = true;
282
+ }
283
+ }
284
+
285
+ if (!hasChanges) return feature;
286
+
287
+ return { ...feature, properties: newProps };
288
+ }
289
+
145
290
  render() {
146
291
  const styles = `
147
292
  <style>
148
- /* Global reset with exact values to prevent external CSS interference */
149
- :host *,
150
- :host *::before,
151
- :host *::after {
293
+ /* Base reset - protect against inherited styles */
294
+ :host *, :host *::before, :host *::after {
152
295
  box-sizing: border-box;
153
- font-family: 'Courier New', Courier, monospace;
154
- font-size: 13px;
155
- font-weight: normal;
156
- font-style: normal;
296
+ font: normal normal 13px/1.5 'Courier New', Courier, monospace;
157
297
  font-variant: normal;
158
- line-height: 1.5;
159
298
  letter-spacing: 0;
299
+ word-spacing: 0;
160
300
  text-transform: none;
161
301
  text-decoration: none;
162
302
  text-indent: 0;
163
- word-spacing: 0;
164
303
  }
165
304
 
166
305
  :host {
@@ -176,30 +315,19 @@ class GeoJsonEditor extends HTMLElement {
176
315
  :host([readonly]) .editor-wrapper::after {
177
316
  content: '';
178
317
  position: absolute;
179
- top: 0;
180
- left: 0;
181
- right: 0;
182
- bottom: 0;
318
+ inset: 0;
183
319
  pointer-events: none;
184
- background: repeating-linear-gradient(
185
- -45deg,
186
- rgba(128, 128, 128, 0.08),
187
- rgba(128, 128, 128, 0.08) 3px,
188
- transparent 3px,
189
- transparent 12px
190
- );
320
+ background: repeating-linear-gradient(-45deg, rgba(128,128,128,0.08), rgba(128,128,128,0.08) 3px, transparent 3px, transparent 12px);
191
321
  z-index: 1;
192
322
  }
193
323
 
194
- :host([readonly]) textarea {
195
- cursor: text;
196
- }
324
+ :host([readonly]) textarea { cursor: text; }
197
325
 
198
326
  .editor-wrapper {
199
327
  position: relative;
200
328
  width: 100%;
201
329
  flex: 1;
202
- background: var(--bg-color, #ffffff);
330
+ background: var(--bg-color, #fff);
203
331
  display: flex;
204
332
  }
205
333
 
@@ -231,41 +359,36 @@ class GeoJsonEditor extends HTMLElement {
231
359
  justify-content: center;
232
360
  }
233
361
 
234
- .color-indicator {
362
+ .color-indicator, .collapse-button {
235
363
  width: 12px;
236
364
  height: 12px;
237
365
  border-radius: 2px;
238
- border: 1px solid #555;
239
366
  cursor: pointer;
240
367
  transition: transform 0.1s;
241
368
  flex-shrink: 0;
242
369
  }
243
370
 
371
+ .color-indicator {
372
+ border: 1px solid #555;
373
+ }
244
374
  .color-indicator:hover {
245
375
  transform: scale(1.2);
246
376
  border-color: #fff;
247
377
  }
248
378
 
249
379
  .collapse-button {
250
- width: 12px;
251
- height: 12px;
380
+ padding-top: 1px;
252
381
  background: var(--control-bg, #e8e8e8);
253
382
  border: 1px solid var(--control-border, #c0c0c0);
254
- border-radius: 2px;
255
383
  color: var(--control-color, #000080);
256
384
  font-size: 8px;
257
385
  font-weight: bold;
258
- cursor: pointer;
259
386
  display: flex;
260
387
  align-items: center;
261
388
  justify-content: center;
262
- transition: all 0.1s;
263
- flex-shrink: 0;
264
389
  user-select: none;
265
390
  }
266
-
267
391
  .collapse-button:hover {
268
- background: var(--control-bg, #e8e8e8);
269
392
  border-color: var(--control-color, #000080);
270
393
  transform: scale(1.1);
271
394
  }
@@ -274,6 +397,7 @@ class GeoJsonEditor extends HTMLElement {
274
397
  width: 14px;
275
398
  height: 14px;
276
399
  background: transparent;
400
+ color: var(--control-color, #000080);
277
401
  border: none;
278
402
  cursor: pointer;
279
403
  display: flex;
@@ -285,38 +409,10 @@ class GeoJsonEditor extends HTMLElement {
285
409
  padding: 0;
286
410
  font-size: 11px;
287
411
  }
412
+ .visibility-button:hover { opacity: 1; transform: scale(1.15); }
413
+ .visibility-button.hidden { opacity: 0.35; }
288
414
 
289
- .visibility-button:hover {
290
- opacity: 1;
291
- transform: scale(1.15);
292
- }
293
-
294
- .visibility-button.hidden {
295
- opacity: 0.35;
296
- }
297
-
298
- /* Hidden feature lines - grayed out */
299
- .line-hidden {
300
- opacity: 0.35;
301
- filter: grayscale(50%);
302
- }
303
-
304
- .color-picker-popup {
305
- position: absolute;
306
- background: #2d2d30;
307
- border: 1px solid #555;
308
- border-radius: 4px;
309
- padding: 8px;
310
- z-index: 1000;
311
- box-shadow: 0 4px 12px rgba(0,0,0,0.5);
312
- }
313
-
314
- .color-picker-popup input[type="color"] {
315
- width: 150px;
316
- height: 30px;
317
- border: none;
318
- cursor: pointer;
319
- }
415
+ .line-hidden { opacity: 0.35; filter: grayscale(50%); }
320
416
 
321
417
  .editor-content {
322
418
  position: relative;
@@ -324,156 +420,82 @@ class GeoJsonEditor extends HTMLElement {
324
420
  overflow: hidden;
325
421
  }
326
422
 
327
- .highlight-layer {
423
+ .highlight-layer, textarea, .placeholder-layer {
328
424
  position: absolute;
329
- top: 0;
330
- left: 0;
331
- width: 100%;
332
- height: 100%;
425
+ inset: 0;
333
426
  padding: 8px 12px;
334
427
  white-space: pre-wrap;
335
428
  word-wrap: break-word;
429
+ }
430
+
431
+ .highlight-layer {
336
432
  overflow: auto;
337
433
  pointer-events: none;
338
434
  z-index: 1;
339
- color: var(--text-color, #000000);
340
- }
341
-
342
- .highlight-layer::-webkit-scrollbar {
343
- display: none;
435
+ color: var(--text-color, #000);
344
436
  }
437
+ .highlight-layer::-webkit-scrollbar { display: none; }
345
438
 
346
439
  textarea {
347
- position: absolute;
348
- top: 0;
349
- left: 0;
350
- width: 100%;
351
- height: 100%;
352
- padding: 8px 12px;
353
440
  margin: 0;
354
441
  border: none;
355
442
  outline: none;
356
443
  background: transparent;
357
444
  color: transparent;
358
445
  caret-color: var(--caret-color, #000);
359
- white-space: pre-wrap;
360
- word-wrap: break-word;
361
446
  resize: none;
362
447
  overflow: auto;
363
448
  z-index: 2;
364
449
  }
365
-
366
- textarea::selection {
367
- background: rgba(51, 153, 255, 0.3);
368
- }
369
-
370
- textarea::placeholder {
371
- color: transparent;
372
- }
450
+ textarea::selection { background: rgba(51,153,255,0.3); }
451
+ textarea::placeholder { color: transparent; }
452
+ textarea:disabled { cursor: not-allowed; opacity: 0.6; }
373
453
 
374
454
  .placeholder-layer {
375
- position: absolute;
376
- top: 0;
377
- left: 0;
378
- width: 100%;
379
- height: 100%;
380
- padding: 8px 12px;
381
- white-space: pre-wrap;
382
- word-wrap: break-word;
383
455
  color: #6a6a6a;
384
456
  pointer-events: none;
385
457
  z-index: 0;
386
458
  overflow: hidden;
387
459
  }
388
460
 
389
- textarea:disabled {
390
- cursor: not-allowed;
391
- opacity: 0.6;
392
- }
393
-
394
- /* Syntax highlighting colors - IntelliJ Light defaults */
395
- .json-key {
396
- color: var(--json-key, #660e7a);
397
- }
398
-
399
- .json-string {
400
- color: var(--json-string, #008000);
401
- }
402
-
403
- .json-number {
404
- color: var(--json-number, #0000ff);
405
- }
461
+ .json-key { color: var(--json-key, #660e7a); }
462
+ .json-string { color: var(--json-string, #008000); }
463
+ .json-number { color: var(--json-number, #00f); }
464
+ .json-boolean, .json-null { color: var(--json-boolean, #000080); }
465
+ .json-punctuation { color: var(--json-punct, #000); }
466
+ .json-key-invalid { color: var(--json-key-invalid, #f00); }
406
467
 
407
- .json-boolean {
408
- color: var(--json-boolean, #000080);
409
- }
410
-
411
- .json-null {
412
- color: var(--json-null, #000080);
413
- }
414
-
415
- .json-punctuation {
416
- color: var(--json-punct, #000000);
417
- }
418
-
419
- /* GeoJSON-specific highlighting */
420
- .geojson-key {
421
- color: var(--geojson-key, #660e7a);
422
- font-weight: 600;
423
- }
424
-
425
- .geojson-type {
426
- color: var(--geojson-type, #008000);
427
- font-weight: 600;
428
- }
429
-
430
- .geojson-type-invalid {
431
- color: var(--geojson-type-invalid, #ff0000);
432
- font-weight: 600;
433
- }
468
+ .geojson-key { color: var(--geojson-key, #660e7a); font-weight: 600; }
469
+ .geojson-type { color: var(--geojson-type, #008000); font-weight: 600; }
470
+ .geojson-type-invalid { color: var(--geojson-type-invalid, #f00); font-weight: 600; }
434
471
 
435
- .json-key-invalid {
436
- color: var(--json-key-invalid, #ff0000);
437
- }
438
-
439
- /* Prefix and suffix wrapper with gutter */
440
- .prefix-wrapper,
441
- .suffix-wrapper {
472
+ .prefix-wrapper, .suffix-wrapper {
442
473
  display: flex;
443
474
  flex-shrink: 0;
444
- background: var(--bg-color, #ffffff);
475
+ background: var(--bg-color, #fff);
445
476
  }
446
477
 
447
- .prefix-gutter,
448
- .suffix-gutter {
478
+ .prefix-gutter, .suffix-gutter {
449
479
  width: 24px;
450
480
  background: var(--gutter-bg, #f0f0f0);
451
481
  border-right: 1px solid var(--gutter-border, #e0e0e0);
452
482
  flex-shrink: 0;
453
483
  }
454
484
 
455
- .editor-prefix,
456
- .editor-suffix {
485
+ .editor-prefix, .editor-suffix {
457
486
  flex: 1;
458
487
  padding: 4px 12px;
459
- color: var(--text-color, #000000);
460
- background: var(--bg-color, #ffffff);
488
+ color: var(--text-color, #000);
489
+ background: var(--bg-color, #fff);
461
490
  user-select: none;
462
491
  white-space: pre-wrap;
463
492
  word-wrap: break-word;
464
493
  opacity: 0.6;
465
494
  }
466
495
 
467
- .prefix-wrapper {
468
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
469
- }
470
-
471
- .suffix-wrapper {
472
- border-top: 1px solid rgba(255, 255, 255, 0.1);
473
- position: relative;
474
- }
496
+ .prefix-wrapper { border-bottom: 1px solid rgba(255,255,255,0.1); }
497
+ .suffix-wrapper { border-top: 1px solid rgba(255,255,255,0.1); position: relative; }
475
498
 
476
- /* Clear button in suffix area */
477
499
  .clear-btn {
478
500
  position: absolute;
479
501
  right: 0.5rem;
@@ -481,7 +503,7 @@ class GeoJsonEditor extends HTMLElement {
481
503
  transform: translateY(-50%);
482
504
  background: transparent;
483
505
  border: none;
484
- color: var(--text-color, #000000);
506
+ color: var(--text-color, #000);
485
507
  opacity: 0.3;
486
508
  cursor: pointer;
487
509
  font-size: 0.65rem;
@@ -492,41 +514,16 @@ class GeoJsonEditor extends HTMLElement {
492
514
  display: flex;
493
515
  align-items: center;
494
516
  justify-content: center;
495
- box-sizing: border-box;
496
517
  transition: opacity 0.2s, background 0.2s;
497
518
  }
498
- .clear-btn:hover {
499
- opacity: 0.7;
500
- background: rgba(255, 255, 255, 0.1);
501
- }
502
- .clear-btn[hidden] {
503
- display: none;
504
- }
505
-
506
- /* Scrollbar styling - WebKit (Chrome, Safari, Edge) */
507
- textarea::-webkit-scrollbar {
508
- width: 10px;
509
- height: 10px;
510
- }
519
+ .clear-btn:hover { opacity: 0.7; background: rgba(255,255,255,0.1); }
520
+ .clear-btn[hidden] { display: none; }
511
521
 
512
- textarea::-webkit-scrollbar-track {
513
- background: var(--control-bg, #e8e8e8);
514
- }
515
-
516
- textarea::-webkit-scrollbar-thumb {
517
- background: var(--control-border, #c0c0c0);
518
- border-radius: 5px;
519
- }
520
-
521
- textarea::-webkit-scrollbar-thumb:hover {
522
- background: var(--control-color, #000080);
523
- }
524
-
525
- /* Scrollbar styling - Firefox */
526
- textarea {
527
- scrollbar-width: thin;
528
- scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8);
529
- }
522
+ textarea::-webkit-scrollbar { width: 10px; height: 10px; }
523
+ textarea::-webkit-scrollbar-track { background: var(--control-bg, #e8e8e8); }
524
+ textarea::-webkit-scrollbar-thumb { background: var(--control-border, #c0c0c0); border-radius: 5px; }
525
+ textarea::-webkit-scrollbar-thumb:hover { background: var(--control-color, #000080); }
526
+ textarea { scrollbar-width: thin; scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8); }
530
527
  </style>
531
528
  `;
532
529
 
@@ -708,32 +705,18 @@ class GeoJsonEditor extends HTMLElement {
708
705
  // Auto-format JSON content
709
706
  if (newValue) {
710
707
  try {
711
- const prefix = this.prefix;
712
- const suffix = this.suffix;
713
-
714
- // Check if prefix ends with [ and suffix starts with ]
715
- const prefixEndsWithBracket = prefix.trimEnd().endsWith('[');
716
- const suffixStartsWithBracket = suffix.trimStart().startsWith(']');
717
-
718
- if (prefixEndsWithBracket && suffixStartsWithBracket) {
719
- // Wrap content in array brackets for validation and formatting
720
- const wrapped = '[' + newValue + ']';
721
- const parsed = JSON.parse(wrapped);
722
- const formatted = JSON.stringify(parsed, null, 2);
723
-
724
- // Remove first [ and last ] from formatted
725
- const lines = formatted.split('\n');
726
- if (lines.length > 2) {
727
- textarea.value = lines.slice(1, -1).join('\n');
728
- } else {
729
- textarea.value = '';
730
- }
731
- } else if (!prefix && !suffix) {
732
- // No prefix/suffix - format directly
733
- const parsed = JSON.parse(newValue);
734
- textarea.value = JSON.stringify(parsed, null, 2);
708
+ // Wrap content in array brackets for validation and formatting
709
+ const wrapped = '[' + newValue + ']';
710
+ const parsed = JSON.parse(wrapped);
711
+ const formatted = JSON.stringify(parsed, null, 2);
712
+
713
+ // Remove first [ and last ] from formatted
714
+ const lines = formatted.split('\n');
715
+ if (lines.length > 2) {
716
+ textarea.value = lines.slice(1, -1).join('\n');
717
+ } else {
718
+ textarea.value = '';
735
719
  }
736
- // else: keep as-is for complex cases
737
720
  } catch (e) {
738
721
  // Invalid JSON, keep as-is
739
722
  }
@@ -870,10 +853,12 @@ class GeoJsonEditor extends HTMLElement {
870
853
  };
871
854
  }
872
855
 
873
- // GeoJSON type constants
874
- static GEOJSON_TYPES_FEATURE = ['Feature', 'FeatureCollection'];
875
- static GEOJSON_TYPES_GEOMETRY = ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection'];
876
- static GEOJSON_TYPES_ALL = [...GeoJsonEditor.GEOJSON_TYPES_FEATURE, ...GeoJsonEditor.GEOJSON_TYPES_GEOMETRY];
856
+ // GeoJSON type constants (consolidated)
857
+ static GEOJSON = {
858
+ FEATURE_TYPES: ['Feature', 'FeatureCollection'],
859
+ GEOMETRY_TYPES: ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection'],
860
+ ALL_TYPES: ['Feature', 'FeatureCollection', 'Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection']
861
+ };
877
862
 
878
863
  // Valid keys per context (null = any key is valid)
879
864
  static VALID_KEYS_BY_CONTEXT = {
@@ -954,7 +939,7 @@ class GeoJsonEditor extends HTMLElement {
954
939
  const typeMatch = line.substring(0, j).match(/"type"\s*:\s*$/);
955
940
  if (typeMatch) {
956
941
  const valueMatch = line.substring(j).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);
957
- if (valueMatch && GeoJsonEditor.GEOJSON_TYPES_ALL.includes(valueMatch[1])) {
942
+ if (valueMatch && GeoJsonEditor.GEOJSON.ALL_TYPES.includes(valueMatch[1])) {
958
943
  const currentCtx = contextStack[contextStack.length - 1];
959
944
  if (currentCtx) {
960
945
  currentCtx.context = valueMatch[1];
@@ -1027,12 +1012,12 @@ class GeoJsonEditor extends HTMLElement {
1027
1012
  // Unknown context - don't validate (could be inside misspelled properties, etc.)
1028
1013
  if (!context) return true;
1029
1014
  if (context === 'properties') return true; // Any type in properties
1030
- if (context === 'geometry' || GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.includes(context)) {
1031
- return GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.includes(typeValue);
1015
+ if (context === 'geometry' || GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(context)) {
1016
+ return GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(typeValue);
1032
1017
  }
1033
1018
  // Only validate as GeoJSON type in known Feature/FeatureCollection context
1034
1019
  if (context === 'Feature' || context === 'FeatureCollection') {
1035
- return GeoJsonEditor.GEOJSON_TYPES_ALL.includes(typeValue);
1020
+ return GeoJsonEditor.GEOJSON.ALL_TYPES.includes(typeValue);
1036
1021
  }
1037
1022
  return true; // Unknown context - accept any type
1038
1023
  };
@@ -1091,35 +1076,16 @@ class GeoJsonEditor extends HTMLElement {
1091
1076
  const hasMarker = currentLine.includes('{...}') || currentLine.includes('[...]');
1092
1077
 
1093
1078
  if (hasMarker) {
1094
- // Expand: find the correct collapsed data by searching for this nodeKey
1095
- let foundKey = null;
1096
- let foundData = null;
1097
-
1098
- // Try exact match first
1099
- const exactKey = `${line}-${nodeKey}`;
1100
- if (this.collapsedData.has(exactKey)) {
1101
- foundKey = exactKey;
1102
- foundData = this.collapsedData.get(exactKey);
1103
- } else {
1104
- // Search for any key with this nodeKey (line numbers may have shifted)
1105
- for (const [key, data] of this.collapsedData.entries()) {
1106
- if (data.nodeKey === nodeKey) {
1107
- // Check indent to distinguish between multiple nodes with same name
1108
- const currentIndent = currentLine.match(/^(\s*)/)[1].length;
1109
- if (data.indent === currentIndent) {
1110
- foundKey = key;
1111
- foundData = data;
1112
- break;
1113
- }
1114
- }
1115
- }
1116
- }
1079
+ // Expand: find the correct collapsed data
1080
+ const currentIndent = currentLine.match(/^(\s*)/)[1].length;
1081
+ const found = this._findCollapsedData(line, nodeKey, currentIndent);
1117
1082
 
1118
- if (!foundKey || !foundData) {
1083
+ if (!found) {
1119
1084
  return;
1120
1085
  }
1121
1086
 
1122
- const {originalLine, content} = foundData;
1087
+ const { key: foundKey, data: foundData } = found;
1088
+ const { originalLine, content } = foundData;
1123
1089
 
1124
1090
  // Restore original line and content
1125
1091
  lines[line] = originalLine;
@@ -1411,44 +1377,29 @@ class GeoJsonEditor extends HTMLElement {
1411
1377
  if (line.includes('{...}') || line.includes('[...]')) {
1412
1378
  const match = line.match(R.collapsedMarker);
1413
1379
  if (match) {
1414
- const nodeKey = match[2]; // Extract nodeKey from the marker
1415
- const exactKey = `${absoluteLineNum}-${nodeKey}`;
1416
-
1417
- // Try exact key match first
1418
- if (this.collapsedData.has(exactKey)) {
1419
- const collapsed = this.collapsedData.get(exactKey);
1420
- expandedLines.push(collapsed.originalLine);
1421
- expandedLines.push(...collapsed.content);
1380
+ const nodeKey = match[2];
1381
+ const currentIndent = match[1].length;
1382
+
1383
+ // Try to find collapsed data using helper
1384
+ const found = this._findCollapsedData(absoluteLineNum, nodeKey, currentIndent);
1385
+ if (found) {
1386
+ expandedLines.push(found.data.originalLine);
1387
+ expandedLines.push(...found.data.content);
1422
1388
  return;
1423
1389
  }
1424
1390
 
1425
- // Fallback: search by line number and nodeKey
1426
- let found = false;
1427
- for (const [key, collapsed] of this.collapsedData.entries()) {
1428
- if (key.endsWith(`-${nodeKey}`)) {
1391
+ // Fallback: search by nodeKey only (line numbers may have shifted)
1392
+ for (const [, collapsed] of this.collapsedData.entries()) {
1393
+ if (collapsed.nodeKey === nodeKey) {
1429
1394
  expandedLines.push(collapsed.originalLine);
1430
1395
  expandedLines.push(...collapsed.content);
1431
- found = true;
1432
- break;
1396
+ return;
1433
1397
  }
1434
1398
  }
1435
- if (found) return;
1436
1399
  }
1437
1400
 
1438
- // Fallback: search by line number only
1439
- let found = false;
1440
- for (const [key, collapsed] of this.collapsedData.entries()) {
1441
- const collapsedLineNum = parseInt(key.split('-')[0]);
1442
- if (collapsedLineNum === absoluteLineNum) {
1443
- expandedLines.push(collapsed.originalLine);
1444
- expandedLines.push(...collapsed.content);
1445
- found = true;
1446
- break;
1447
- }
1448
- }
1449
- if (!found) {
1450
- expandedLines.push(line);
1451
- }
1401
+ // Line not found in collapsed data, keep as-is
1402
+ expandedLines.push(line);
1452
1403
  } else {
1453
1404
  expandedLines.push(line);
1454
1405
  }
@@ -1482,10 +1433,8 @@ class GeoJsonEditor extends HTMLElement {
1482
1433
  // Expand ALL collapsed nodes to get full content
1483
1434
  const editorContent = this.expandAllCollapsed(textarea.value);
1484
1435
 
1485
- // Build complete value with prefix/suffix
1486
- const prefix = this.prefix;
1487
- const suffix = this.suffix;
1488
- const fullValue = prefix + editorContent + suffix;
1436
+ // Build complete value with prefix/suffix (fixed FeatureCollection wrapper)
1437
+ const fullValue = this.prefix + editorContent + this.suffix;
1489
1438
 
1490
1439
  // Try to parse
1491
1440
  try {
@@ -1718,13 +1667,13 @@ class GeoJsonEditor extends HTMLElement {
1718
1667
  if (typeof typeValue === 'string') {
1719
1668
  if (context === 'geometry') {
1720
1669
  // In geometry: must be a geometry type
1721
- if (!GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.includes(typeValue)) {
1722
- errors.push(`Invalid geometry type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.join(', ')})`);
1670
+ if (!GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(typeValue)) {
1671
+ errors.push(`Invalid geometry type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.join(', ')})`);
1723
1672
  }
1724
1673
  } else {
1725
1674
  // At root or in features: must be Feature or FeatureCollection
1726
- if (!GeoJsonEditor.GEOJSON_TYPES_FEATURE.includes(typeValue)) {
1727
- errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON_TYPES_FEATURE.join(', ')})`);
1675
+ if (!GeoJsonEditor.GEOJSON.FEATURE_TYPES.includes(typeValue)) {
1676
+ errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON.FEATURE_TYPES.join(', ')})`);
1728
1677
  }
1729
1678
  }
1730
1679
  }
@@ -1900,20 +1849,10 @@ class GeoJsonEditor extends HTMLElement {
1900
1849
 
1901
1850
  const nodeKey = match[2];
1902
1851
  const currentIndent = match[1].length;
1903
- const exactKey = `${i}-${nodeKey}`;
1852
+ const found = this._findCollapsedData(i, nodeKey, currentIndent);
1904
1853
 
1905
- let foundKey = this.collapsedData.has(exactKey) ? exactKey : null;
1906
- if (!foundKey) {
1907
- for (const [key, data] of this.collapsedData.entries()) {
1908
- if (data.nodeKey === nodeKey && data.indent === currentIndent) {
1909
- foundKey = key;
1910
- break;
1911
- }
1912
- }
1913
- }
1914
-
1915
- if (foundKey) {
1916
- const {originalLine, content: nodeContent} = this.collapsedData.get(foundKey);
1854
+ if (found) {
1855
+ const { data: { originalLine, content: nodeContent } } = found;
1917
1856
  lines[i] = originalLine;
1918
1857
  lines.splice(i + 1, 0, ...nodeContent);
1919
1858
  expanded = true;
@@ -1927,27 +1866,20 @@ class GeoJsonEditor extends HTMLElement {
1927
1866
  return content;
1928
1867
  }
1929
1868
 
1930
- // Helper: Format JSON content respecting prefix/suffix
1869
+ // Helper: Format JSON content (always in FeatureCollection mode)
1870
+ // Also applies default properties to features if configured
1931
1871
  formatJSONContent(content) {
1932
- const prefix = this.prefix;
1933
- const suffix = this.suffix;
1934
- const prefixEndsWithBracket = prefix.trimEnd().endsWith('[');
1935
- const suffixStartsWithBracket = suffix.trimStart().startsWith(']');
1936
-
1937
- if (prefixEndsWithBracket && suffixStartsWithBracket) {
1938
- const wrapped = '[' + content + ']';
1939
- const parsed = JSON.parse(wrapped);
1940
- const formatted = JSON.stringify(parsed, null, 2);
1941
- const lines = formatted.split('\n');
1942
- return lines.length > 2 ? lines.slice(1, -1).join('\n') : '';
1943
- } else if (!prefix && !suffix) {
1944
- const parsed = JSON.parse(content);
1945
- return JSON.stringify(parsed, null, 2);
1946
- } else {
1947
- const fullValue = prefix + content + suffix;
1948
- JSON.parse(fullValue); // Validate only
1949
- return content;
1872
+ const wrapped = '[' + content + ']';
1873
+ let parsed = JSON.parse(wrapped);
1874
+
1875
+ // Apply default properties to each feature in the array
1876
+ if (Array.isArray(parsed)) {
1877
+ parsed = parsed.map(f => this._applyDefaultPropertiesToFeature(f));
1950
1878
  }
1879
+
1880
+ const formatted = JSON.stringify(parsed, null, 2);
1881
+ const lines = formatted.split('\n');
1882
+ return lines.length > 2 ? lines.slice(1, -1).join('\n') : '';
1951
1883
  }
1952
1884
 
1953
1885
  autoFormatContentWithCursor() {
@@ -2232,8 +2164,8 @@ class GeoJsonEditor extends HTMLElement {
2232
2164
  // Check geometry has valid type
2233
2165
  if (!('type' in feature.geometry)) {
2234
2166
  errors.push('Geometry must have a "type" property');
2235
- } else if (!GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.includes(feature.geometry.type)) {
2236
- errors.push(`Invalid geometry type "${feature.geometry.type}" (expected: ${GeoJsonEditor.GEOJSON_TYPES_GEOMETRY.join(', ')})`);
2167
+ } else if (!GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(feature.geometry.type)) {
2168
+ errors.push(`Invalid geometry type "${feature.geometry.type}" (expected: ${GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.join(', ')})`);
2237
2169
  }
2238
2170
 
2239
2171
  // Check geometry has coordinates (except GeometryCollection)
@@ -2281,7 +2213,9 @@ class GeoJsonEditor extends HTMLElement {
2281
2213
  throw new Error(`Invalid features: ${allErrors.join('; ')}`);
2282
2214
  }
2283
2215
 
2284
- this._setFeatures(features);
2216
+ // Apply default properties to each feature
2217
+ const featuresWithDefaults = features.map(f => this._applyDefaultPropertiesToFeature(f));
2218
+ this._setFeatures(featuresWithDefaults);
2285
2219
  }
2286
2220
 
2287
2221
  /**
@@ -2296,7 +2230,8 @@ class GeoJsonEditor extends HTMLElement {
2296
2230
  }
2297
2231
 
2298
2232
  const features = this._parseFeatures();
2299
- features.push(feature);
2233
+ // Apply default properties before adding
2234
+ features.push(this._applyDefaultPropertiesToFeature(feature));
2300
2235
  this._setFeatures(features);
2301
2236
  }
2302
2237
 
@@ -2315,7 +2250,8 @@ class GeoJsonEditor extends HTMLElement {
2315
2250
  const features = this._parseFeatures();
2316
2251
  const idx = this._normalizeIndex(index, features.length, true);
2317
2252
 
2318
- features.splice(idx, 0, feature);
2253
+ // Apply default properties before inserting
2254
+ features.splice(idx, 0, this._applyDefaultPropertiesToFeature(feature));
2319
2255
  this._setFeatures(features);
2320
2256
  }
2321
2257