@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 +62 -0
- package/dist/geojson-editor.js +3 -4
- package/package.json +4 -4
- package/src/geojson-editor.js +269 -333
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
|
package/dist/geojson-editor.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
var e=Object.defineProperty,t=(t,n,o)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:o}):t[n]=o,n=(e,n,o)=>t(e,"symbol"!=typeof n?n+"":n,o);
|
|
2
1
|
/**
|
|
3
2
|
* @license MIT
|
|
4
3
|
* @name @softwarity/geojson-editor
|
|
5
|
-
* @version 1.0.
|
|
4
|
+
* @version 1.0.7
|
|
6
5
|
* @author Softwarity (https://www.softwarity.io/)
|
|
7
|
-
* @copyright
|
|
6
|
+
* @copyright 2025 Softwarity
|
|
8
7
|
* @see https://github.com/softwarity/geojson-editor
|
|
9
8
|
*/
|
|
10
|
-
const o=class e extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.collapsedData=new Map,this.colorPositions=[],this.nodeTogglePositions=[],this.hiddenFeatures=new Set,this.featureRanges=new Map,this.highlightTimer=null,this._cachedLineHeight=null,this._cachedPaddingTop=null,this.themes={dark:{},light:{}}}static get observedAttributes(){return["readonly","value","placeholder","dark-selector"]}static _toKebabCase(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase()}connectedCallback(){this.render(),this.setupEventListeners(),this.updatePrefixSuffix(),this.updateThemeCSS(),this.value&&this.updateValue(this.value),this.updatePlaceholderContent()}disconnectedCallback(){const e=document.querySelector(".geojson-color-picker-input");e&&e._closeListener&&(document.removeEventListener("click",e._closeListener,!0),e.remove()),this.highlightTimer&&(clearTimeout(this.highlightTimer),this.highlightTimer=null)}attributeChangedCallback(e,t,n){t!==n&&("value"===e?this.updateValue(n):"readonly"===e?this.updateReadonly():"placeholder"===e?this.updatePlaceholderContent():"dark-selector"===e&&this.updateThemeCSS())}get readonly(){return this.hasAttribute("readonly")}get value(){return this.getAttribute("value")||""}get placeholder(){return this.getAttribute("placeholder")||""}get prefix(){return'{"type": "FeatureCollection", "features": ['}get suffix(){return"]}"}render(){const e=`\n <div class="prefix-wrapper">\n <div class="prefix-gutter"></div>\n <div class="editor-prefix" id="editorPrefix"></div>\n </div>\n <div class="editor-wrapper">\n <div class="gutter">\n <div class="gutter-content" id="gutterContent"></div>\n </div>\n <div class="editor-content">\n <div class="placeholder-layer" id="placeholderLayer">${this.escapeHtml(this.placeholder)}</div>\n <div class="highlight-layer" id="highlightLayer"></div>\n <textarea\n id="textarea"\n spellcheck="false"\n autocomplete="off"\n autocorrect="off"\n autocapitalize="off"\n ></textarea>\n </div>\n </div>\n <div class="suffix-wrapper">\n <div class="suffix-gutter"></div>\n <div class="editor-suffix" id="editorSuffix"></div>\n <button class="clear-btn" id="clearBtn" title="Clear editor">✕</button>\n </div>\n `;this.shadowRoot.innerHTML="\n <style>\n /* Global reset with exact values to prevent external CSS interference */\n :host *,\n :host *::before,\n :host *::after {\n box-sizing: border-box;\n font-family: 'Courier New', Courier, monospace;\n font-size: 13px;\n font-weight: normal;\n font-style: normal;\n font-variant: normal;\n line-height: 1.5;\n letter-spacing: 0;\n text-transform: none;\n text-decoration: none;\n text-indent: 0;\n word-spacing: 0;\n }\n\n :host {\n display: flex;\n flex-direction: column;\n position: relative;\n width: 100%;\n height: 400px;\n border-radius: 4px;\n overflow: hidden;\n }\n\n :host([readonly]) .editor-wrapper::after {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n background: repeating-linear-gradient(\n -45deg,\n rgba(128, 128, 128, 0.08),\n rgba(128, 128, 128, 0.08) 3px,\n transparent 3px,\n transparent 12px\n );\n z-index: 1;\n }\n\n :host([readonly]) textarea {\n cursor: text;\n }\n\n .editor-wrapper {\n position: relative;\n width: 100%;\n flex: 1;\n background: var(--bg-color, #ffffff);\n display: flex;\n }\n\n .gutter {\n width: 24px;\n height: 100%;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n overflow: hidden;\n flex-shrink: 0;\n position: relative;\n }\n\n .gutter-content {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n padding: 8px 4px;\n }\n\n .gutter-line {\n position: absolute;\n left: 0;\n width: 100%;\n height: 1.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .color-indicator {\n width: 12px;\n height: 12px;\n border-radius: 2px;\n border: 1px solid #555;\n cursor: pointer;\n transition: transform 0.1s;\n flex-shrink: 0;\n }\n\n .color-indicator:hover {\n transform: scale(1.2);\n border-color: #fff;\n }\n\n .collapse-button {\n width: 12px;\n height: 12px;\n background: var(--control-bg, #e8e8e8);\n border: 1px solid var(--control-border, #c0c0c0);\n border-radius: 2px;\n color: var(--control-color, #000080);\n font-size: 8px;\n font-weight: bold;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.1s;\n flex-shrink: 0;\n user-select: none;\n }\n\n .collapse-button:hover {\n background: var(--control-bg, #e8e8e8);\n border-color: var(--control-color, #000080);\n transform: scale(1.1);\n }\n\n .visibility-button {\n width: 14px;\n height: 14px;\n background: transparent;\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.1s;\n flex-shrink: 0;\n opacity: 0.7;\n padding: 0;\n font-size: 11px;\n }\n\n .visibility-button:hover {\n opacity: 1;\n transform: scale(1.15);\n }\n\n .visibility-button.hidden {\n opacity: 0.35;\n }\n\n /* Hidden feature lines - grayed out */\n .line-hidden {\n opacity: 0.35;\n filter: grayscale(50%);\n }\n\n .color-picker-popup {\n position: absolute;\n background: #2d2d30;\n border: 1px solid #555;\n border-radius: 4px;\n padding: 8px;\n z-index: 1000;\n box-shadow: 0 4px 12px rgba(0,0,0,0.5);\n }\n\n .color-picker-popup input[type=\"color\"] {\n width: 150px;\n height: 30px;\n border: none;\n cursor: pointer;\n }\n\n .editor-content {\n position: relative;\n flex: 1;\n overflow: hidden;\n }\n\n .highlight-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 8px 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow: auto;\n pointer-events: none;\n z-index: 1;\n color: var(--text-color, #000000);\n }\n\n .highlight-layer::-webkit-scrollbar {\n display: none;\n }\n\n textarea {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 8px 12px;\n margin: 0;\n border: none;\n outline: none;\n background: transparent;\n color: transparent;\n caret-color: var(--caret-color, #000);\n white-space: pre-wrap;\n word-wrap: break-word;\n resize: none;\n overflow: auto;\n z-index: 2;\n }\n\n textarea::selection {\n background: rgba(51, 153, 255, 0.3);\n }\n\n textarea::placeholder {\n color: transparent;\n }\n\n .placeholder-layer {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n padding: 8px 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n color: #6a6a6a;\n pointer-events: none;\n z-index: 0;\n overflow: hidden;\n }\n\n textarea:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n\n /* Syntax highlighting colors - IntelliJ Light defaults */\n .json-key {\n color: var(--json-key, #660e7a);\n }\n\n .json-string {\n color: var(--json-string, #008000);\n }\n\n .json-number {\n color: var(--json-number, #0000ff);\n }\n\n .json-boolean {\n color: var(--json-boolean, #000080);\n }\n\n .json-null {\n color: var(--json-null, #000080);\n }\n\n .json-punctuation {\n color: var(--json-punct, #000000);\n }\n\n /* GeoJSON-specific highlighting */\n .geojson-key {\n color: var(--geojson-key, #660e7a);\n font-weight: 600;\n }\n\n .geojson-type {\n color: var(--geojson-type, #008000);\n font-weight: 600;\n }\n\n .geojson-type-invalid {\n color: var(--geojson-type-invalid, #ff0000);\n font-weight: 600;\n }\n\n .json-key-invalid {\n color: var(--json-key-invalid, #ff0000);\n }\n\n /* Prefix and suffix wrapper with gutter */\n .prefix-wrapper,\n .suffix-wrapper {\n display: flex;\n flex-shrink: 0;\n background: var(--bg-color, #ffffff);\n }\n\n .prefix-gutter,\n .suffix-gutter {\n width: 24px;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n flex-shrink: 0;\n }\n\n .editor-prefix,\n .editor-suffix {\n flex: 1;\n padding: 4px 12px;\n color: var(--text-color, #000000);\n background: var(--bg-color, #ffffff);\n user-select: none;\n white-space: pre-wrap;\n word-wrap: break-word;\n opacity: 0.6;\n }\n\n .prefix-wrapper {\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n }\n\n .suffix-wrapper {\n border-top: 1px solid rgba(255, 255, 255, 0.1);\n position: relative;\n }\n\n /* Clear button in suffix area */\n .clear-btn {\n position: absolute;\n right: 0.5rem;\n top: 50%;\n transform: translateY(-50%);\n background: transparent;\n border: none;\n color: var(--text-color, #000000);\n opacity: 0.3;\n cursor: pointer;\n font-size: 0.65rem;\n width: 1rem;\n height: 1rem;\n padding: 0.15rem 0 0 0;\n border-radius: 3px;\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n transition: opacity 0.2s, background 0.2s;\n }\n .clear-btn:hover {\n opacity: 0.7;\n background: rgba(255, 255, 255, 0.1);\n }\n .clear-btn[hidden] {\n display: none;\n }\n\n /* Scrollbar styling - WebKit (Chrome, Safari, Edge) */\n textarea::-webkit-scrollbar {\n width: 10px;\n height: 10px;\n }\n\n textarea::-webkit-scrollbar-track {\n background: var(--control-bg, #e8e8e8);\n }\n\n textarea::-webkit-scrollbar-thumb {\n background: var(--control-border, #c0c0c0);\n border-radius: 5px;\n }\n\n textarea::-webkit-scrollbar-thumb:hover {\n background: var(--control-color, #000080);\n }\n\n /* Scrollbar styling - Firefox */\n textarea {\n scrollbar-width: thin;\n scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8);\n }\n </style>\n "+e}setupEventListeners(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");e.addEventListener("scroll",()=>{t.scrollTop=e.scrollTop,t.scrollLeft=e.scrollLeft,this.syncGutterScroll(e.scrollTop)}),e.addEventListener("input",()=>{this.updatePlaceholderVisibility(),clearTimeout(this.highlightTimer),this.highlightTimer=setTimeout(()=>{this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange()},150)}),e.addEventListener("paste",()=>{clearTimeout(this.highlightTimer),setTimeout(()=>{this.updatePlaceholderVisibility(),this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange(),this.applyAutoCollapsed()},10)}),this.shadowRoot.getElementById("gutterContent").addEventListener("click",e=>{const t=e.target.closest(".visibility-button");if(t){const e=t.dataset.featureKey;return void this.toggleFeatureVisibility(e)}if(e.target.classList.contains("color-indicator")){const t=parseInt(e.target.dataset.line),n=e.target.dataset.color,o=e.target.dataset.attributeName;this.showColorPicker(e.target,t,n,o)}else if(e.target.classList.contains("collapse-button")){const t=e.target.dataset.nodeKey,n=parseInt(e.target.dataset.line);this.toggleCollapse(t,n)}}),this.shadowRoot.querySelector(".gutter").addEventListener("wheel",t=>{t.preventDefault(),e.scrollTop+=t.deltaY}),e.addEventListener("keydown",e=>{this.handleKeydownInCollapsedArea(e)}),e.addEventListener("copy",e=>{this.handleCopyWithCollapsedContent(e)}),e.addEventListener("cut",e=>{this.handleCutWithCollapsedContent(e)}),this.shadowRoot.getElementById("clearBtn").addEventListener("click",()=>{this.removeAll()}),this.updateReadonly()}syncGutterScroll(e){this.shadowRoot.getElementById("gutterContent").style.transform=`translateY(-${e}px)`}updateReadonly(){const e=this.shadowRoot.getElementById("textarea");e&&(e.disabled=this.readonly);const t=this.shadowRoot.getElementById("clearBtn");t&&(t.hidden=this.readonly)}escapeHtml(t){if(!t)return"";const n=e.REGEX;return t.replace(n.ampersand,"&").replace(n.lessThan,"<").replace(n.greaterThan,">")}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("placeholderLayer");e&&t&&(t.style.display=e.value?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updateValue(e){const t=this.shadowRoot.getElementById("textarea");if(t&&t.value!==e){if(t.value=e||"",e)try{const n=this.prefix,o=this.suffix,s=n.trimEnd().endsWith("["),r=o.trimStart().startsWith("]");if(s&&r){const n="["+e+"]",o=JSON.parse(n),s=JSON.stringify(o,null,2).split("\n");s.length>2?t.value=s.slice(1,-1).join("\n"):t.value=""}else if(!n&&!o){const n=JSON.parse(e);t.value=JSON.stringify(n,null,2)}}catch{}this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateHighlight(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");if(!e||!t)return;const n=e.value;this.updateFeatureRanges();const o=this.getHiddenLineRanges(),{highlighted:s,colors:r,toggles:i}=this.highlightJSON(n,o);t.innerHTML=s,this.colorPositions=r,this.nodeTogglePositions=i,this.updateGutter()}highlightJSON(t,n=[]){if(!t.trim())return{highlighted:"",colors:[],toggles:[]};const o=t.split("\n"),s=[],r=[];let i=[];const a=this.buildContextMap(t);return o.forEach((t,o)=>{const l=e.REGEX;let c;for(l.colorInLine.lastIndex=0;null!==(c=l.colorInLine.exec(t));)s.push({line:o,color:c[2],attributeName:c[1]});const d=t.match(l.collapsibleNode);if(d){const e=d[2];t.includes("{...}")||t.includes("[...]")?r.push({line:o,nodeKey:e,isCollapsed:!0}):this.bracketClosesOnSameLine(t,d[3])||r.push({line:o,nodeKey:e,isCollapsed:!1})}const h=a.get(o);let p=this.highlightSyntax(t,h);(e=>n.some(t=>e>=t.startLine&&e<=t.endLine))(o)&&(p=`<span class="line-hidden">${p}</span>`),i.push(p)}),{highlighted:i.join("\n"),colors:s,toggles:r}}buildContextMap(t){var n;const o=t.split("\n"),s=new Map,r=[];let i=null;const a="Feature";for(let t=0;t<o.length;t++){const l=o[t],c=r.length>0?null==(n=r[r.length-1])?void 0:n.context:a;s.set(t,c);let d=!1,h=!1;for(let t=0;t<l.length;t++){const n=l[t];if(h)h=!1;else if("\\"===n&&d)h=!0;else if('"'!==n){if(!d){if("{"===n||"["===n){let e;if(i)e=i,i=null;else if(0===r.length)e=a;else{const t=r[r.length-1];e=t&&t.isArray?t.context:null}r.push({context:e,isArray:"["===n})}("}"===n||"]"===n)&&r.length>0&&r.pop()}}else{if(!d){const n=l.substring(t).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/);if(n){const o=n[1];e.CONTEXT_CHANGING_KEYS[o]&&(i=e.CONTEXT_CHANGING_KEYS[o]),t+=n[0].length-1;continue}if(r.length>0&&l.substring(0,t).match(/"type"\s*:\s*$/)){const n=l.substring(t).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);if(n&&e.GEOJSON_TYPES_ALL.includes(n[1])){const e=r[r.length-1];e&&(e.context=n[1])}t+=n?n[0].length-1:0;continue}}d=!d}}}return s}highlightSyntax(t,n){if(!t.trim())return"";const o=n?e.VALID_KEYS_BY_CONTEXT[n]:null,s=e.REGEX;return t.replace(s.ampersand,"&").replace(s.lessThan,"<").replace(s.greaterThan,">").replace(s.jsonKey,(t,s)=>"properties"===n?`<span class="json-key">"${s}"</span>:`:e.GEOJSON_STRUCTURAL_KEYS.includes(s)?`<span class="geojson-key">"${s}"</span>:`:(t=>!(!e.GEOJSON_STRUCTURAL_KEYS.includes(t)&&n&&null!=o)||o.includes(t))(s)?`<span class="json-key">"${s}"</span>:`:`<span class="json-key-invalid">"${s}"</span>:`).replace(s.typeValue,(t,o)=>(t=>!n||"properties"===n||("geometry"===n||e.GEOJSON_TYPES_GEOMETRY.includes(n)?e.GEOJSON_TYPES_GEOMETRY.includes(t):"Feature"!==n&&"FeatureCollection"!==n||e.GEOJSON_TYPES_ALL.includes(t)))(o)?`<span class="geojson-key">"type"</span>: <span class="geojson-type">"${o}"</span>`:`<span class="geojson-key">"type"</span>: <span class="geojson-type-invalid">"${o}"</span>`).replace(s.stringValue,(e,t)=>e.includes("<span")?e:`: <span class="json-string">"${t}"</span>`).replace(s.numberAfterColon,': <span class="json-number">$1</span>').replace(s.boolean,': <span class="json-boolean">$1</span>').replace(s.nullValue,': <span class="json-null">$1</span>').replace(s.allNumbers,'<span class="json-number">$1</span>').replace(s.punctuation,'<span class="json-punctuation">$1</span>')}toggleCollapse(e,t){const n=this.shadowRoot.getElementById("textarea"),o=n.value.split("\n"),s=o[t];if(s.includes("{...}")||s.includes("[...]")){let n=null,r=null;const i=`${t}-${e}`;if(this.collapsedData.has(i))n=i,r=this.collapsedData.get(i);else for(const[t,o]of this.collapsedData.entries())if(o.nodeKey===e){const e=s.match(/^(\s*)/)[1].length;if(o.indent===e){n=t,r=o;break}}if(!n||!r)return;const{originalLine:a,content:l}=r;o[t]=a,o.splice(t+1,0,...l),this.collapsedData.delete(n)}else{const n=s.match(/^(\s*)"([^"]+)"\s*:\s*([{\[])/);if(!n)return;const r=n[1],i=n[3];if(0===this._performCollapse(o,t,e,r,i))return}n.value=o.join("\n"),this.updateHighlight()}applyAutoCollapsed(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value)return;const t=e.value.split("\n");for(let e=t.length-1;e>=0;e--){const n=t[e].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(n){const o=n[2];if("coordinates"===o){const s=n[1],r=n[3];this._performCollapse(t,e,o,s,r)}}}e.value=t.join("\n"),this.updateHighlight()}updateGutter(){const e=this.shadowRoot.getElementById("gutterContent"),t=this.shadowRoot.getElementById("textarea");if(!t)return;if(null===this._cachedLineHeight){const e=getComputedStyle(t);this._cachedLineHeight=parseFloat(e.lineHeight),this._cachedPaddingTop=parseFloat(e.paddingTop)}const n=this._cachedLineHeight,o=this._cachedPaddingTop;e.textContent="";const s=new Map,r=e=>(s.has(e)||s.set(e,{colors:[],buttons:[],visibilityButtons:[]}),s.get(e));this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{r(e).colors.push({color:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{r(e).buttons.push({nodeKey:t,isCollapsed:n})});for(const[e,t]of this.featureRanges){const n=this.hiddenFeatures.has(e);r(t.startLine).visibilityButtons.push({featureKey:e,isHidden:n})}const i=document.createDocumentFragment();s.forEach((e,t)=>{const s=document.createElement("div");s.className="gutter-line",s.style.top=`${o+t*n}px`,e.visibilityButtons.forEach(({featureKey:e,isHidden:t})=>{const n=document.createElement("button");n.className="visibility-button"+(t?" hidden":""),n.textContent="👁",n.dataset.featureKey=e,n.title=t?"Show feature in events":"Hide feature from events",s.appendChild(n)}),e.colors.forEach(({color:e,attributeName:n})=>{const o=document.createElement("div");o.className="color-indicator",o.style.backgroundColor=e,o.dataset.line=t,o.dataset.color=e,o.dataset.attributeName=n,o.title=`${n}: ${e}`,s.appendChild(o)}),e.buttons.forEach(({nodeKey:e,isCollapsed:n})=>{const o=document.createElement("div");o.className="collapse-button",o.textContent=n?"+":"-",o.dataset.line=t,o.dataset.nodeKey=e,o.title=n?"Expand":"Collapse",s.appendChild(o)}),i.appendChild(s)}),e.appendChild(i)}showColorPicker(e,t,n,o){const s=document.querySelector(".geojson-color-picker-input");s&&(s._closeListener&&document.removeEventListener("click",s._closeListener,!0),s.remove());const r=document.createElement("input");r.type="color",r.value=n,r.className="geojson-color-picker-input";const i=e.getBoundingClientRect();r.style.position="fixed",r.style.left=`${i.left}px`,r.style.top=`${i.top}px`,r.style.width="12px",r.style.height="12px",r.style.opacity="0.01",r.style.border="none",r.style.padding="0",r.style.zIndex="9999",r.addEventListener("input",e=>{this.updateColorValue(t,e.target.value,o)}),r.addEventListener("change",e=>{this.updateColorValue(t,e.target.value,o)});const a=e=>{e.target!==r&&!r.contains(e.target)&&(document.removeEventListener("click",a,!0),r.remove())};r._closeListener=a,document.body.appendChild(r),setTimeout(()=>{document.addEventListener("click",a,!0)},100),r.focus(),r.click()}updateColorValue(e,t,n){const o=this.shadowRoot.getElementById("textarea"),s=o.value.split("\n"),r=new RegExp(`"${n}"\\s*:\\s*"#[0-9a-fA-F]{6}"`);s[e]=s[e].replace(r,`"${n}": "${t}"`),o.value=s.join("\n"),this.updateHighlight(),this.emitChange()}handleKeydownInCollapsedArea(e){if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Tab"].includes(e.key)||e.ctrlKey||e.metaKey)return;const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.value.substring(0,n).split("\n").length-1,s=t.value.split("\n")[o];s&&(s.includes("{...}")||s.includes("[...]"))&&e.preventDefault()}handleCopyWithCollapsedContent(e){const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n===o)return;const s=t.value.substring(n,o);if(!s.includes("{...}")&&!s.includes("[...]"))return;let r;r=0===n&&o===t.value.length?this.expandAllCollapsed(s):this.expandCollapsedMarkersInText(s,n),e.preventDefault(),e.clipboardData.setData("text/plain",r)}expandCollapsedMarkersInText(t,n){const o=this.shadowRoot.getElementById("textarea").value.substring(0,n).split("\n").length-1,s=e.REGEX,r=t.split("\n"),i=[];return r.forEach((e,t)=>{const n=o+t;if(e.includes("{...}")||e.includes("[...]")){const t=e.match(s.collapsedMarker);if(t){const e=t[2],o=`${n}-${e}`;if(this.collapsedData.has(o)){const e=this.collapsedData.get(o);return i.push(e.originalLine),void i.push(...e.content)}let s=!1;for(const[t,n]of this.collapsedData.entries())if(t.endsWith(`-${e}`)){i.push(n.originalLine),i.push(...n.content),s=!0;break}if(s)return}let o=!1;for(const[e,t]of this.collapsedData.entries())if(parseInt(e.split("-")[0])===n){i.push(t.originalLine),i.push(...t.content),o=!0;break}o||i.push(e)}else i.push(e)}),i.join("\n")}handleCutWithCollapsedContent(e){this.handleCopyWithCollapsedContent(e);const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n!==o){const e=t.value;t.value=e.substring(0,n)+e.substring(o),t.selectionStart=t.selectionEnd=n,this.updateHighlight(),this.updatePlaceholderVisibility(),this.emitChange()}}emitChange(){const e=this.shadowRoot.getElementById("textarea"),t=this.expandAllCollapsed(e.value),n=this.prefix+t+this.suffix;try{let e=JSON.parse(n);e=this.filterHiddenFeatures(e);const o=this.validateGeoJSON(e);o.length>0?this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:(new Date).toISOString(),error:`GeoJSON validation: ${o.join("; ")}`,errors:o,content:t},bubbles:!0,composed:!0})):this.dispatchEvent(new CustomEvent("change",{detail:e,bubbles:!0,composed:!0}))}catch(e){this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:(new Date).toISOString(),error:e.message,content:t},bubbles:!0,composed:!0}))}}filterHiddenFeatures(e){if(!e||0===this.hiddenFeatures.size)return e;if("FeatureCollection"===e.type&&Array.isArray(e.features)){const t=e.features.filter(e=>{const t=this.getFeatureKey(e);return!this.hiddenFeatures.has(t)});return{...e,features:t}}if("Feature"===e.type){const t=this.getFeatureKey(e);if(this.hiddenFeatures.has(t))return{type:"FeatureCollection",features:[]}}return e}getFeatureKey(e){var t,n,o;if(!e||"object"!=typeof e)return null;if(void 0!==e.id)return`id:${e.id}`;if(void 0!==(null==(t=e.properties)?void 0:t.id))return`prop:${e.properties.id}`;const s=(null==(n=e.geometry)?void 0:n.type)||"null",r=JSON.stringify((null==(o=e.geometry)?void 0:o.coordinates)||[]).slice(0,100);return`hash:${s}:${this.simpleHash(r)}`}simpleHash(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t&=t;return t.toString(36)}toggleFeatureVisibility(e){this.hiddenFeatures.has(e)?this.hiddenFeatures.delete(e):this.hiddenFeatures.add(e),this.updateHighlight(),this.updateGutter(),this.emitChange()}updateFeatureRanges(){const e=this.shadowRoot.getElementById("textarea");if(!e)return;const t=e.value;this.featureRanges.clear();try{const e=this.expandAllCollapsed(t),n=this.prefix+e+this.suffix,o=JSON.parse(n);let s=[];"FeatureCollection"===o.type&&Array.isArray(o.features)?s=o.features:"Feature"===o.type&&(s=[o]);const r=t.split("\n");let i=0,a=0,l=!1,c=-1,d=null;for(let e=0;e<r.length;e++){const t=r[e],n=/"type"\s*:\s*"Feature"/.test(t);if(!l&&n){let t=e;for(let n=e;n>=0;n--)if(r[n].includes("{")){t=n;break}c=t,l=!0,a=1;for(let n=t;n<=e;n++){const e=r[n],o=this._countBracketsOutsideStrings(e,"{");a+=n===t?o.open-1-o.close:o.open-o.close}i<s.length&&(d=this.getFeatureKey(s[i]))}else if(l){const n=this._countBracketsOutsideStrings(t,"{");a+=n.open-n.close,a<=0&&(d&&this.featureRanges.set(d,{startLine:c,endLine:e,featureIndex:i}),i++,l=!1,d=null)}}}catch{}}getHiddenLineRanges(){const e=[];for(const[t,n]of this.featureRanges)this.hiddenFeatures.has(t)&&e.push(n);return e}validateGeoJSON(t,n="",o="root"){const s=[];if(!t||"object"!=typeof t)return s;if("properties"!==o&&void 0!==t.type){const r=t.type;"string"==typeof r&&("geometry"===o?e.GEOJSON_TYPES_GEOMETRY.includes(r)||s.push(`Invalid geometry type "${r}" at ${n||"root"} (expected: ${e.GEOJSON_TYPES_GEOMETRY.join(", ")})`):e.GEOJSON_TYPES_FEATURE.includes(r)||s.push(`Invalid type "${r}" at ${n||"root"} (expected: ${e.GEOJSON_TYPES_FEATURE.join(", ")})`))}if(Array.isArray(t))t.forEach((e,t)=>{s.push(...this.validateGeoJSON(e,`${n}[${t}]`,o))});else for(const[e,r]of Object.entries(t))if("object"==typeof r&&null!==r){const t=n?`${n}.${e}`:e;let i=o;"properties"===e?i="properties":"geometry"===e||"geometries"===e?i="geometry":"features"===e&&(i="root"),s.push(...this.validateGeoJSON(r,t,i))}return s}_countBracketsOutsideStrings(e,t){const n="{"===t?"}":"]";let o=0,s=0,r=!1,i=!1;for(let a=0;a<e.length;a++){const l=e[a];i?i=!1:"\\"===l&&r?i=!0:'"'!==l?r||(l===t&&o++,l===n&&s++):r=!r}return{open:o,close:s}}bracketClosesOnSameLine(e,t){const n=e.indexOf(t);if(-1===n)return!1;const o=e.substring(n+1),s=this._countBracketsOutsideStrings(o,t);return s.close>s.open}_findClosingBracket(e,t,n){let o=1;const s=[],r=e[t],i=r.indexOf(n);if(-1!==i){const e=r.substring(i+1),s=this._countBracketsOutsideStrings(e,n);if(o+=s.open-s.close,0===o)return{endLine:t,content:[]}}for(let r=t+1;r<e.length;r++){const t=e[r],i=this._countBracketsOutsideStrings(t,n);if(o+=i.open-i.close,s.push(t),0===o)return{endLine:r,content:s}}return null}_performCollapse(e,t,n,o,s){const r=e[t],i="{"===s?"}":"]";if(this.bracketClosesOnSameLine(r,s))return 0;const a=this._findClosingBracket(e,t,s);if(!a)return 0;const{endLine:l,content:c}=a,d=`${t}-${n}`;this.collapsedData.set(d,{originalLine:r,content:c,indent:o.length,nodeKey:n});const h=r.substring(0,r.indexOf(s)),p=e[l]&&e[l].trim().endsWith(",");e[t]=`${h}${s}...${i}${p?",":""}`;const u=l-t;return e.splice(t+1,u),u}expandAllCollapsed(t){const n=e.REGEX;for(;t.includes("{...}")||t.includes("[...]");){const e=t.split("\n");let o=!1;for(let t=0;t<e.length;t++){const s=e[t];if(!s.includes("{...}")&&!s.includes("[...]"))continue;const r=s.match(n.collapsedMarker);if(!r)continue;const i=r[2],a=r[1].length,l=`${t}-${i}`;let c=this.collapsedData.has(l)?l:null;if(!c)for(const[e,t]of this.collapsedData.entries())if(t.nodeKey===i&&t.indent===a){c=e;break}if(c){const{originalLine:n,content:s}=this.collapsedData.get(c);e[t]=n,e.splice(t+1,0,...s),o=!0;break}}if(!o)break;t=e.join("\n")}return t}formatJSONContent(e){const t=this.prefix,n=this.suffix,o=t.trimEnd().endsWith("["),s=n.trimStart().startsWith("]");if(o&&s){const t="["+e+"]",n=JSON.parse(t),o=JSON.stringify(n,null,2).split("\n");return o.length>2?o.slice(1,-1).join("\n"):""}if(t||n){const o=t+e+n;return JSON.parse(o),e}{const t=JSON.parse(e);return JSON.stringify(t,null,2)}}autoFormatContentWithCursor(){const e=this.shadowRoot.getElementById("textarea"),t=e.selectionStart,n=e.value.substring(0,t).split("\n"),o=n.length-1,s=n[n.length-1].length,r=Array.from(this.collapsedData.values()).map(e=>({nodeKey:e.nodeKey,indent:e.indent})),i=this.expandAllCollapsed(e.value);try{const t=this.formatJSONContent(i);if(t!==i){this.collapsedData.clear(),e.value=t,r.length>0&&this.reapplyCollapsed(r);const n=e.value.split("\n");if(o<n.length){const t=Math.min(s,n[o].length);let r=0;for(let e=0;e<o;e++)r+=n[e].length+1;r+=t,e.setSelectionRange(r,r)}}}catch{}}reapplyCollapsed(e){const t=this.shadowRoot.getElementById("textarea"),n=t.value.split("\n"),o=new Map;e.forEach(({nodeKey:e,indent:t})=>{const n=`${e}-${t}`;o.set(n,(o.get(n)||0)+1)});const s=new Map;for(let e=n.length-1;e>=0;e--){const t=n[e].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(t){const r=t[2],i=`${r}-${t[1].length}`;if(o.has(i)&&(s.set(i,(s.get(i)||0)+1),s.get(i)<=o.get(i))){const o=t[1],s=t[3];this._performCollapse(n,e,r,o,s)}}}t.value=n.join("\n")}parseSelectorToHostRule(e){return e&&""!==e?e.startsWith(".")&&!e.includes(" ")?`:host(${e})`:`:host-context(${e})`:':host([data-color-scheme="dark"])'}updateThemeCSS(){const t=this.getAttribute("dark-selector")||".dark",n=this.parseSelectorToHostRule(t);let o=this.shadowRoot.getElementById("theme-styles");o||(o=document.createElement("style"),o.id="theme-styles",this.shadowRoot.insertBefore(o,this.shadowRoot.firstChild));const s=t=>Object.entries(t||{}).map(([t,n])=>`--${e._toKebabCase(t)}: ${n};`).join("\n "),r=s(this.themes.light);let i="";r&&(i+=`:host {\n ${r}\n }\n`),i+=`${n} {\n ${s({...e.DARK_THEME_DEFAULTS,...this.themes.dark})}\n }`,o.textContent=i}setTheme(e){e.dark&&(this.themes.dark={...this.themes.dark,...e.dark}),e.light&&(this.themes.light={...this.themes.light,...e.light}),this.updateThemeCSS()}resetTheme(){this.themes={dark:{},light:{}},this.updateThemeCSS()}_normalizeIndex(e,t,n=!1){let o=e;return o<0&&(o=t+o),n?Math.max(0,Math.min(o,t)):o<0||o>=t?-1:o}_parseFeatures(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value.trim())return[];try{const t="["+this.expandAllCollapsed(e.value)+"]";return JSON.parse(t)}catch{return[]}}_setFeatures(e){const t=this.shadowRoot.getElementById("textarea");if(t){if(this.collapsedData.clear(),this.hiddenFeatures.clear(),e&&0!==e.length){const n=e.map(e=>JSON.stringify(e,null,2)).join(",\n");t.value=n}else t.value="";this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}_validateFeature(t){const n=[];return t&&"object"==typeof t?Array.isArray(t)?(n.push("Feature cannot be an array"),n):("type"in t?"Feature"!==t.type&&n.push(`Feature type must be "Feature", got "${t.type}"`):n.push('Feature must have a "type" property'),"geometry"in t?null!==t.geometry&&("object"!=typeof t.geometry||Array.isArray(t.geometry)?n.push("Feature geometry must be an object or null"):("type"in t.geometry?e.GEOJSON_TYPES_GEOMETRY.includes(t.geometry.type)||n.push(`Invalid geometry type "${t.geometry.type}" (expected: ${e.GEOJSON_TYPES_GEOMETRY.join(", ")})`):n.push('Geometry must have a "type" property'),"GeometryCollection"!==t.geometry.type&&!("coordinates"in t.geometry)&&n.push('Geometry must have a "coordinates" property'),"GeometryCollection"===t.geometry.type&&!Array.isArray(t.geometry.geometries)&&n.push('GeometryCollection must have a "geometries" array'))):n.push('Feature must have a "geometry" property (can be null)'),"properties"in t?null!==t.properties&&("object"!=typeof t.properties||Array.isArray(t.properties))&&n.push("Feature properties must be an object or null"):n.push('Feature must have a "properties" property (can be null)'),n):(n.push("Feature must be an object"),n)}set(e){if(!Array.isArray(e))throw new Error("set() expects an array of features");const t=[];if(e.forEach((e,n)=>{const o=this._validateFeature(e);o.length>0&&t.push(`Feature[${n}]: ${o.join(", ")}`)}),t.length>0)throw new Error(`Invalid features: ${t.join("; ")}`);this._setFeatures(e)}add(e){const t=this._validateFeature(e);if(t.length>0)throw new Error(`Invalid feature: ${t.join(", ")}`);const n=this._parseFeatures();n.push(e),this._setFeatures(n)}insertAt(e,t){const n=this._validateFeature(e);if(n.length>0)throw new Error(`Invalid feature: ${n.join(", ")}`);const o=this._parseFeatures(),s=this._normalizeIndex(t,o.length,!0);o.splice(s,0,e),this._setFeatures(o)}removeAt(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);if(-1===n)return;const o=t.splice(n,1)[0];return this._setFeatures(t),o}removeAll(){const e=this._parseFeatures();return this._setFeatures([]),e}get(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);return-1!==n?t[n]:void 0}getAll(){return this._parseFeatures()}emit(){this.emitChange()}};n(o,"DARK_THEME_DEFAULTS",{bgColor:"#2b2b2b",textColor:"#a9b7c6",caretColor:"#bbbbbb",gutterBg:"#313335",gutterBorder:"#3c3f41",jsonKey:"#9876aa",jsonString:"#6a8759",jsonNumber:"#6897bb",jsonBoolean:"#cc7832",jsonNull:"#cc7832",jsonPunct:"#a9b7c6",controlColor:"#cc7832",controlBg:"#3c3f41",controlBorder:"#5a5a5a",geojsonKey:"#9876aa",geojsonType:"#6a8759",geojsonTypeInvalid:"#ff6b68",jsonKeyInvalid:"#ff6b68"}),n(o,"REGEX",{ampersand:/&/g,lessThan:/</g,greaterThan:/>/g,jsonKey:/"([^"]+)"\s*:/g,typeValue:/<span class="geojson-key">"type"<\/span>:\s*"([^"]*)"/g,stringValue:/:\s*"([^"]*)"/g,numberAfterColon:/:\s*(-?\d+\.?\d*)/g,boolean:/:\s*(true|false)/g,nullValue:/:\s*(null)/g,allNumbers:/\b(-?\d+\.?\d*)\b/g,punctuation:/([{}[\],])/g,colorInLine:/"([\w-]+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,collapsibleNode:/^(\s*)"(\w+)"\s*:\s*([{\[])/,collapsedMarker:/^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/}),n(o,"GEOJSON_TYPES_FEATURE",["Feature","FeatureCollection"]),n(o,"GEOJSON_TYPES_GEOMETRY",["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"]),n(o,"GEOJSON_TYPES_ALL",[...o.GEOJSON_TYPES_FEATURE,...o.GEOJSON_TYPES_GEOMETRY]),n(o,"VALID_KEYS_BY_CONTEXT",{Feature:["type","geometry","properties","id","bbox"],FeatureCollection:["type","features","bbox","properties"],Point:["type","coordinates","bbox"],MultiPoint:["type","coordinates","bbox"],LineString:["type","coordinates","bbox"],MultiLineString:["type","coordinates","bbox"],Polygon:["type","coordinates","bbox"],MultiPolygon:["type","coordinates","bbox"],GeometryCollection:["type","geometries","bbox"],properties:null,geometry:["type","coordinates","geometries","bbox"]}),n(o,"CONTEXT_CHANGING_KEYS",{geometry:"geometry",properties:"properties",features:"Feature",geometries:"geometry"}),n(o,"GEOJSON_STRUCTURAL_KEYS",["type","geometry","properties","features","geometries","coordinates","bbox","id","crs"]);let s=o;customElements.define("geojson-editor",s);
|
|
9
|
+
class e extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.collapsedData=/* @__PURE__ */new Map,this.colorPositions=[],this.nodeTogglePositions=[],this.hiddenFeatures=/* @__PURE__ */new Set,this.featureRanges=/* @__PURE__ */new Map,this.highlightTimer=null,this._cachedLineHeight=null,this._cachedPaddingTop=null,this.themes={dark:{},light:{}}}static get observedAttributes(){return["readonly","value","placeholder","dark-selector","default-properties"]}_defaultPropertiesRules=null;static _toKebabCase(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase()}static DARK_THEME_DEFAULTS={bgColor:"#2b2b2b",textColor:"#a9b7c6",caretColor:"#bbbbbb",gutterBg:"#313335",gutterBorder:"#3c3f41",jsonKey:"#9876aa",jsonString:"#6a8759",jsonNumber:"#6897bb",jsonBoolean:"#cc7832",jsonNull:"#cc7832",jsonPunct:"#a9b7c6",controlColor:"#cc7832",controlBg:"#3c3f41",controlBorder:"#5a5a5a",geojsonKey:"#9876aa",geojsonType:"#6a8759",geojsonTypeInvalid:"#ff6b68",jsonKeyInvalid:"#ff6b68"};static REGEX={ampersand:/&/g,lessThan:/</g,greaterThan:/>/g,jsonKey:/"([^"]+)"\s*:/g,typeValue:/<span class="geojson-key">"type"<\/span>:\s*"([^"]*)"/g,stringValue:/:\s*"([^"]*)"/g,numberAfterColon:/:\s*(-?\d+\.?\d*)/g,boolean:/:\s*(true|false)/g,nullValue:/:\s*(null)/g,allNumbers:/\b(-?\d+\.?\d*)\b/g,punctuation:/([{}[\],])/g,colorInLine:/"([\w-]+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,collapsibleNode:/^(\s*)"(\w+)"\s*:\s*([{\[])/,collapsedMarker:/^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/};_findCollapsedData(e,t,n){const o=`${e}-${t}`;if(this.collapsedData.has(o))return{key:o,data:this.collapsedData.get(o)};for(const[s,r]of this.collapsedData.entries())if(r.nodeKey===t&&r.indent===n)return{key:s,data:r};return null}connectedCallback(){this.render(),this.setupEventListeners(),this.updatePrefixSuffix(),this.updateThemeCSS(),this._parseDefaultProperties(),this.value&&this.updateValue(this.value),this.updatePlaceholderContent()}disconnectedCallback(){const e=document.querySelector(".geojson-color-picker-input");e&&e._closeListener&&(document.removeEventListener("click",e._closeListener,!0),e.remove()),this.highlightTimer&&(clearTimeout(this.highlightTimer),this.highlightTimer=null)}attributeChangedCallback(e,t,n){t!==n&&("value"===e?this.updateValue(n):"readonly"===e?this.updateReadonly():"placeholder"===e?this.updatePlaceholderContent():"dark-selector"===e?this.updateThemeCSS():"default-properties"===e&&this._parseDefaultProperties())}get readonly(){return this.hasAttribute("readonly")}get value(){return this.getAttribute("value")||""}get placeholder(){return this.getAttribute("placeholder")||""}get prefix(){return'{"type": "FeatureCollection", "features": ['}get suffix(){return"]}"}get defaultProperties(){return this.getAttribute("default-properties")||""}_parseDefaultProperties(){const e=this.defaultProperties;if(!e)return this._defaultPropertiesRules=[],this._defaultPropertiesRules;try{const t=JSON.parse(e);Array.isArray(t)?this._defaultPropertiesRules=t.map(e=>({match:e.match||null,values:e.values||{}})):this._defaultPropertiesRules="object"==typeof t&&null!==t?[{match:null,values:t}]:[]}catch(t){console.warn("geojson-editor: Invalid default-properties JSON:",t.message),this._defaultPropertiesRules=[]}return this._defaultPropertiesRules}_matchesCondition(e,t){if(!t||"object"!=typeof t)return!0;for(const[n,o]of Object.entries(t))if(this._getNestedValue(e,n)!==o)return!1;return!0}_getNestedValue(e,t){const n=t.split(".");let o=e;for(const s of n){if(null==o)return;o=o[s]}return o}_applyDefaultPropertiesToFeature(e){if(!e||"object"!=typeof e)return e;if(!this._defaultPropertiesRules||0===this._defaultPropertiesRules.length)return e;const t={};for(const r of this._defaultPropertiesRules)this._matchesCondition(e,r.match)&&Object.assign(t,r.values);if(0===Object.keys(t).length)return e;const n=e.properties||{},o={...n};let s=!1;for(const[r,i]of Object.entries(t))r in n||(o[r]=i,s=!0);return s?{...e,properties:o}:e}render(){const e=`\n <div class="prefix-wrapper">\n <div class="prefix-gutter"></div>\n <div class="editor-prefix" id="editorPrefix"></div>\n </div>\n <div class="editor-wrapper">\n <div class="gutter">\n <div class="gutter-content" id="gutterContent"></div>\n </div>\n <div class="editor-content">\n <div class="placeholder-layer" id="placeholderLayer">${this.escapeHtml(this.placeholder)}</div>\n <div class="highlight-layer" id="highlightLayer"></div>\n <textarea\n id="textarea"\n spellcheck="false"\n autocomplete="off"\n autocorrect="off"\n autocapitalize="off"\n ></textarea>\n </div>\n </div>\n <div class="suffix-wrapper">\n <div class="suffix-gutter"></div>\n <div class="editor-suffix" id="editorSuffix"></div>\n <button class="clear-btn" id="clearBtn" title="Clear editor">✕</button>\n </div>\n `;this.shadowRoot.innerHTML="\n <style>\n /* Base reset - protect against inherited styles */\n :host *, :host *::before, :host *::after {\n box-sizing: border-box;\n font: normal normal 13px/1.5 'Courier New', Courier, monospace;\n font-variant: normal;\n letter-spacing: 0;\n word-spacing: 0;\n text-transform: none;\n text-decoration: none;\n text-indent: 0;\n }\n\n :host {\n display: flex;\n flex-direction: column;\n position: relative;\n width: 100%;\n height: 400px;\n border-radius: 4px;\n overflow: hidden;\n }\n\n :host([readonly]) .editor-wrapper::after {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n background: repeating-linear-gradient(-45deg, rgba(128,128,128,0.08), rgba(128,128,128,0.08) 3px, transparent 3px, transparent 12px);\n z-index: 1;\n }\n\n :host([readonly]) textarea { cursor: text; }\n\n .editor-wrapper {\n position: relative;\n width: 100%;\n flex: 1;\n background: var(--bg-color, #fff);\n display: flex;\n }\n\n .gutter {\n width: 24px;\n height: 100%;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n overflow: hidden;\n flex-shrink: 0;\n position: relative;\n }\n\n .gutter-content {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n padding: 8px 4px;\n }\n\n .gutter-line {\n position: absolute;\n left: 0;\n width: 100%;\n height: 1.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .color-indicator, .collapse-button {\n width: 12px;\n height: 12px;\n border-radius: 2px;\n cursor: pointer;\n transition: transform 0.1s;\n flex-shrink: 0;\n }\n\n .color-indicator {\n border: 1px solid #555;\n }\n .color-indicator:hover {\n transform: scale(1.2);\n border-color: #fff;\n }\n\n .collapse-button {\n padding-top: 1px;\n background: var(--control-bg, #e8e8e8);\n border: 1px solid var(--control-border, #c0c0c0);\n color: var(--control-color, #000080);\n font-size: 8px;\n font-weight: bold;\n display: flex;\n align-items: center;\n justify-content: center;\n user-select: none;\n }\n .collapse-button:hover {\n border-color: var(--control-color, #000080);\n transform: scale(1.1);\n }\n\n .visibility-button {\n width: 14px;\n height: 14px;\n background: transparent;\n color: var(--control-color, #000080);\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.1s;\n flex-shrink: 0;\n opacity: 0.7;\n padding: 0;\n font-size: 11px;\n }\n .visibility-button:hover { opacity: 1; transform: scale(1.15); }\n .visibility-button.hidden { opacity: 0.35; }\n\n .line-hidden { opacity: 0.35; filter: grayscale(50%); }\n\n .editor-content {\n position: relative;\n flex: 1;\n overflow: hidden;\n }\n\n .highlight-layer, textarea, .placeholder-layer {\n position: absolute;\n inset: 0;\n padding: 8px 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n .highlight-layer {\n overflow: auto;\n pointer-events: none;\n z-index: 1;\n color: var(--text-color, #000);\n }\n .highlight-layer::-webkit-scrollbar { display: none; }\n\n textarea {\n margin: 0;\n border: none;\n outline: none;\n background: transparent;\n color: transparent;\n caret-color: var(--caret-color, #000);\n resize: none;\n overflow: auto;\n z-index: 2;\n }\n textarea::selection { background: rgba(51,153,255,0.3); }\n textarea::placeholder { color: transparent; }\n textarea:disabled { cursor: not-allowed; opacity: 0.6; }\n\n .placeholder-layer {\n color: #6a6a6a;\n pointer-events: none;\n z-index: 0;\n overflow: hidden;\n }\n\n .json-key { color: var(--json-key, #660e7a); }\n .json-string { color: var(--json-string, #008000); }\n .json-number { color: var(--json-number, #00f); }\n .json-boolean, .json-null { color: var(--json-boolean, #000080); }\n .json-punctuation { color: var(--json-punct, #000); }\n .json-key-invalid { color: var(--json-key-invalid, #f00); }\n\n .geojson-key { color: var(--geojson-key, #660e7a); font-weight: 600; }\n .geojson-type { color: var(--geojson-type, #008000); font-weight: 600; }\n .geojson-type-invalid { color: var(--geojson-type-invalid, #f00); font-weight: 600; }\n\n .prefix-wrapper, .suffix-wrapper {\n display: flex;\n flex-shrink: 0;\n background: var(--bg-color, #fff);\n }\n\n .prefix-gutter, .suffix-gutter {\n width: 24px;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n flex-shrink: 0;\n }\n\n .editor-prefix, .editor-suffix {\n flex: 1;\n padding: 4px 12px;\n color: var(--text-color, #000);\n background: var(--bg-color, #fff);\n user-select: none;\n white-space: pre-wrap;\n word-wrap: break-word;\n opacity: 0.6;\n }\n\n .prefix-wrapper { border-bottom: 1px solid rgba(255,255,255,0.1); }\n .suffix-wrapper { border-top: 1px solid rgba(255,255,255,0.1); position: relative; }\n\n .clear-btn {\n position: absolute;\n right: 0.5rem;\n top: 50%;\n transform: translateY(-50%);\n background: transparent;\n border: none;\n color: var(--text-color, #000);\n opacity: 0.3;\n cursor: pointer;\n font-size: 0.65rem;\n width: 1rem;\n height: 1rem;\n padding: 0.15rem 0 0 0;\n border-radius: 3px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: opacity 0.2s, background 0.2s;\n }\n .clear-btn:hover { opacity: 0.7; background: rgba(255,255,255,0.1); }\n .clear-btn[hidden] { display: none; }\n\n textarea::-webkit-scrollbar { width: 10px; height: 10px; }\n textarea::-webkit-scrollbar-track { background: var(--control-bg, #e8e8e8); }\n textarea::-webkit-scrollbar-thumb { background: var(--control-border, #c0c0c0); border-radius: 5px; }\n textarea::-webkit-scrollbar-thumb:hover { background: var(--control-color, #000080); }\n textarea { scrollbar-width: thin; scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8); }\n </style>\n "+e}setupEventListeners(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");e.addEventListener("scroll",()=>{t.scrollTop=e.scrollTop,t.scrollLeft=e.scrollLeft,this.syncGutterScroll(e.scrollTop)}),e.addEventListener("input",()=>{this.updatePlaceholderVisibility(),clearTimeout(this.highlightTimer),this.highlightTimer=setTimeout(()=>{this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange()},150)}),e.addEventListener("paste",()=>{clearTimeout(this.highlightTimer),setTimeout(()=>{this.updatePlaceholderVisibility(),this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange(),this.applyAutoCollapsed()},10)}),this.shadowRoot.getElementById("gutterContent").addEventListener("click",e=>{const t=e.target.closest(".visibility-button");if(t){const e=t.dataset.featureKey;return void this.toggleFeatureVisibility(e)}if(e.target.classList.contains("color-indicator")){const t=parseInt(e.target.dataset.line),n=e.target.dataset.color,o=e.target.dataset.attributeName;this.showColorPicker(e.target,t,n,o)}else if(e.target.classList.contains("collapse-button")){const t=e.target.dataset.nodeKey,n=parseInt(e.target.dataset.line);this.toggleCollapse(t,n)}}),this.shadowRoot.querySelector(".gutter").addEventListener("wheel",t=>{t.preventDefault(),e.scrollTop+=t.deltaY}),e.addEventListener("keydown",e=>{this.handleKeydownInCollapsedArea(e)}),e.addEventListener("copy",e=>{this.handleCopyWithCollapsedContent(e)}),e.addEventListener("cut",e=>{this.handleCutWithCollapsedContent(e)}),this.shadowRoot.getElementById("clearBtn").addEventListener("click",()=>{this.removeAll()}),this.updateReadonly()}syncGutterScroll(e){this.shadowRoot.getElementById("gutterContent").style.transform=`translateY(-${e}px)`}updateReadonly(){const e=this.shadowRoot.getElementById("textarea");e&&(e.disabled=this.readonly);const t=this.shadowRoot.getElementById("clearBtn");t&&(t.hidden=this.readonly)}escapeHtml(t){if(!t)return"";const n=e.REGEX;return t.replace(n.ampersand,"&").replace(n.lessThan,"<").replace(n.greaterThan,">")}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("placeholderLayer");e&&t&&(t.style.display=e.value?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updateValue(e){const t=this.shadowRoot.getElementById("textarea");if(t&&t.value!==e){if(t.value=e||"",e)try{const n="["+e+"]",o=JSON.parse(n),s=JSON.stringify(o,null,2).split("\n");s.length>2?t.value=s.slice(1,-1).join("\n"):t.value=""}catch(n){}this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateHighlight(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");if(!e||!t)return;const n=e.value;this.updateFeatureRanges();const o=this.getHiddenLineRanges(),{highlighted:s,colors:r,toggles:i}=this.highlightJSON(n,o);t.innerHTML=s,this.colorPositions=r,this.nodeTogglePositions=i,this.updateGutter()}highlightJSON(t,n=[]){if(!t.trim())return{highlighted:"",colors:[],toggles:[]};const o=t.split("\n"),s=[],r=[];let i=[];const a=this.buildContextMap(t);return o.forEach((t,o)=>{const l=e.REGEX;let c;for(l.colorInLine.lastIndex=0;null!==(c=l.colorInLine.exec(t));)s.push({line:o,color:c[2],attributeName:c[1]});const d=t.match(l.collapsibleNode);if(d){const e=d[2];t.includes("{...}")||t.includes("[...]")?r.push({line:o,nodeKey:e,isCollapsed:!0}):this.bracketClosesOnSameLine(t,d[3])||r.push({line:o,nodeKey:e,isCollapsed:!1})}const h=a.get(o);let u=this.highlightSyntax(t,h);(e=>n.some(t=>e>=t.startLine&&e<=t.endLine))(o)&&(u=`<span class="line-hidden">${u}</span>`),i.push(u)}),{highlighted:i.join("\n"),colors:s,toggles:r}}static GEOJSON={FEATURE_TYPES:["Feature","FeatureCollection"],GEOMETRY_TYPES:["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"],ALL_TYPES:["Feature","FeatureCollection","Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"]};static VALID_KEYS_BY_CONTEXT={Feature:["type","geometry","properties","id","bbox"],FeatureCollection:["type","features","bbox","properties"],Point:["type","coordinates","bbox"],MultiPoint:["type","coordinates","bbox"],LineString:["type","coordinates","bbox"],MultiLineString:["type","coordinates","bbox"],Polygon:["type","coordinates","bbox"],MultiPolygon:["type","coordinates","bbox"],GeometryCollection:["type","geometries","bbox"],properties:null,geometry:["type","coordinates","geometries","bbox"]};static CONTEXT_CHANGING_KEYS={geometry:"geometry",properties:"properties",features:"Feature",geometries:"geometry"};buildContextMap(t){const n=t.split("\n"),o=/* @__PURE__ */new Map,s=[];let r=null;const i="Feature";for(let a=0;a<n.length;a++){const t=n[a],l=s.length>0?s[s.length-1]?.context:i;o.set(a,l);let c=!1,d=!1;for(let n=0;n<t.length;n++){const o=t[n];if(d)d=!1;else if("\\"===o&&c)d=!0;else if('"'!==o){if(!c){if("{"===o||"["===o){let e;if(r)e=r,r=null;else if(0===s.length)e=i;else{const t=s[s.length-1];e=t&&t.isArray?t.context:null}s.push({context:e,isArray:"["===o})}"}"!==o&&"]"!==o||s.length>0&&s.pop()}}else{if(!c){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/);if(o){const t=o[1];e.CONTEXT_CHANGING_KEYS[t]&&(r=e.CONTEXT_CHANGING_KEYS[t]),n+=o[0].length-1;continue}if(s.length>0&&t.substring(0,n).match(/"type"\s*:\s*$/)){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);if(o&&e.GEOJSON.ALL_TYPES.includes(o[1])){const e=s[s.length-1];e&&(e.context=o[1])}n+=o?o[0].length-1:0;continue}}c=!c}}}return o}static GEOJSON_STRUCTURAL_KEYS=["type","geometry","properties","features","geometries","coordinates","bbox","id","crs"];highlightSyntax(t,n){if(!t.trim())return"";const o=n?e.VALID_KEYS_BY_CONTEXT[n]:null,s=e.REGEX;return t.replace(s.ampersand,"&").replace(s.lessThan,"<").replace(s.greaterThan,">").replace(s.jsonKey,(t,s)=>"properties"===n?`<span class="json-key">"${s}"</span>:`:e.GEOJSON_STRUCTURAL_KEYS.includes(s)?`<span class="geojson-key">"${s}"</span>:`:(t=>!!e.GEOJSON_STRUCTURAL_KEYS.includes(t)||!n||null==o||o.includes(t))(s)?`<span class="json-key">"${s}"</span>:`:`<span class="json-key-invalid">"${s}"</span>:`).replace(s.typeValue,(t,o)=>(t=>!n||"properties"===n||("geometry"===n||e.GEOJSON.GEOMETRY_TYPES.includes(n)?e.GEOJSON.GEOMETRY_TYPES.includes(t):"Feature"!==n&&"FeatureCollection"!==n||e.GEOJSON.ALL_TYPES.includes(t)))(o)?`<span class="geojson-key">"type"</span>: <span class="geojson-type">"${o}"</span>`:`<span class="geojson-key">"type"</span>: <span class="geojson-type-invalid">"${o}"</span>`).replace(s.stringValue,(e,t)=>e.includes("<span")?e:`: <span class="json-string">"${t}"</span>`).replace(s.numberAfterColon,': <span class="json-number">$1</span>').replace(s.boolean,': <span class="json-boolean">$1</span>').replace(s.nullValue,': <span class="json-null">$1</span>').replace(s.allNumbers,'<span class="json-number">$1</span>').replace(s.punctuation,'<span class="json-punctuation">$1</span>')}toggleCollapse(e,t){const n=this.shadowRoot.getElementById("textarea"),o=n.value.split("\n"),s=o[t];if(s.includes("{...}")||s.includes("[...]")){const n=s.match(/^(\s*)/)[1].length,r=this._findCollapsedData(t,e,n);if(!r)return;const{key:i,data:a}=r,{originalLine:l,content:c}=a;o[t]=l,o.splice(t+1,0,...c),this.collapsedData.delete(i)}else{const n=s.match(/^(\s*)"([^"]+)"\s*:\s*([{\[])/);if(!n)return;const r=n[1],i=n[3];if(0===this._performCollapse(o,t,e,r,i))return}n.value=o.join("\n"),this.updateHighlight()}applyAutoCollapsed(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value)return;const t=e.value.split("\n");for(let n=t.length-1;n>=0;n--){const e=t[n].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const o=e[2];if("coordinates"===o){const s=e[1],r=e[3];this._performCollapse(t,n,o,s,r)}}}e.value=t.join("\n"),this.updateHighlight()}updateGutter(){const e=this.shadowRoot.getElementById("gutterContent"),t=this.shadowRoot.getElementById("textarea");if(!t)return;if(null===this._cachedLineHeight){const e=getComputedStyle(t);this._cachedLineHeight=parseFloat(e.lineHeight),this._cachedPaddingTop=parseFloat(e.paddingTop)}const n=this._cachedLineHeight,o=this._cachedPaddingTop;e.textContent="";const s=/* @__PURE__ */new Map,r=e=>(s.has(e)||s.set(e,{colors:[],buttons:[],visibilityButtons:[]}),s.get(e));this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{r(e).colors.push({color:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{r(e).buttons.push({nodeKey:t,isCollapsed:n})});for(const[a,l]of this.featureRanges){const e=this.hiddenFeatures.has(a);r(l.startLine).visibilityButtons.push({featureKey:a,isHidden:e})}const i=document.createDocumentFragment();s.forEach((e,t)=>{const s=document.createElement("div");s.className="gutter-line",s.style.top=`${o+t*n}px`,e.visibilityButtons.forEach(({featureKey:e,isHidden:t})=>{const n=document.createElement("button");n.className="visibility-button"+(t?" hidden":""),n.textContent="👁",n.dataset.featureKey=e,n.title=t?"Show feature in events":"Hide feature from events",s.appendChild(n)}),e.colors.forEach(({color:e,attributeName:n})=>{const o=document.createElement("div");o.className="color-indicator",o.style.backgroundColor=e,o.dataset.line=t,o.dataset.color=e,o.dataset.attributeName=n,o.title=`${n}: ${e}`,s.appendChild(o)}),e.buttons.forEach(({nodeKey:e,isCollapsed:n})=>{const o=document.createElement("div");o.className="collapse-button",o.textContent=n?"+":"-",o.dataset.line=t,o.dataset.nodeKey=e,o.title=n?"Expand":"Collapse",s.appendChild(o)}),i.appendChild(s)}),e.appendChild(i)}showColorPicker(e,t,n,o){const s=document.querySelector(".geojson-color-picker-input");s&&(s._closeListener&&document.removeEventListener("click",s._closeListener,!0),s.remove());const r=document.createElement("input");r.type="color",r.value=n,r.className="geojson-color-picker-input";const i=e.getBoundingClientRect();r.style.position="fixed",r.style.left=`${i.left}px`,r.style.top=`${i.top}px`,r.style.width="12px",r.style.height="12px",r.style.opacity="0.01",r.style.border="none",r.style.padding="0",r.style.zIndex="9999",r.addEventListener("input",e=>{this.updateColorValue(t,e.target.value,o)}),r.addEventListener("change",e=>{this.updateColorValue(t,e.target.value,o)});const a=e=>{e.target===r||r.contains(e.target)||(document.removeEventListener("click",a,!0),r.remove())};r._closeListener=a,document.body.appendChild(r),setTimeout(()=>{document.addEventListener("click",a,!0)},100),r.focus(),r.click()}updateColorValue(e,t,n){const o=this.shadowRoot.getElementById("textarea"),s=o.value.split("\n"),r=new RegExp(`"${n}"\\s*:\\s*"#[0-9a-fA-F]{6}"`);s[e]=s[e].replace(r,`"${n}": "${t}"`),o.value=s.join("\n"),this.updateHighlight(),this.emitChange()}handleKeydownInCollapsedArea(e){if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Tab"].includes(e.key))return;if(e.ctrlKey||e.metaKey)return;const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.value.substring(0,n).split("\n").length-1,s=t.value.split("\n")[o];s&&(s.includes("{...}")||s.includes("[...]"))&&e.preventDefault()}handleCopyWithCollapsedContent(e){const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n===o)return;const s=t.value.substring(n,o);if(!s.includes("{...}")&&!s.includes("[...]"))return;let r;r=0===n&&o===t.value.length?this.expandAllCollapsed(s):this.expandCollapsedMarkersInText(s,n),e.preventDefault(),e.clipboardData.setData("text/plain",r)}expandCollapsedMarkersInText(t,n){const o=this.shadowRoot.getElementById("textarea").value.substring(0,n).split("\n").length-1,s=e.REGEX,r=t.split("\n"),i=[];return r.forEach((e,t)=>{const n=o+t;if(e.includes("{...}")||e.includes("[...]")){const t=e.match(s.collapsedMarker);if(t){const e=t[2],o=t[1].length,s=this._findCollapsedData(n,e,o);if(s)return i.push(s.data.originalLine),void i.push(...s.data.content);for(const[,t]of this.collapsedData.entries())if(t.nodeKey===e)return i.push(t.originalLine),void i.push(...t.content)}i.push(e)}else i.push(e)}),i.join("\n")}handleCutWithCollapsedContent(e){this.handleCopyWithCollapsedContent(e);const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n!==o){const e=t.value;t.value=e.substring(0,n)+e.substring(o),t.selectionStart=t.selectionEnd=n,this.updateHighlight(),this.updatePlaceholderVisibility(),this.emitChange()}}emitChange(){const e=this.shadowRoot.getElementById("textarea"),t=this.expandAllCollapsed(e.value),n=this.prefix+t+this.suffix;try{let e=JSON.parse(n);e=this.filterHiddenFeatures(e);const o=this.validateGeoJSON(e);o.length>0?this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:/* @__PURE__ */(new Date).toISOString(),error:`GeoJSON validation: ${o.join("; ")}`,errors:o,content:t},bubbles:!0,composed:!0})):this.dispatchEvent(new CustomEvent("change",{detail:e,bubbles:!0,composed:!0}))}catch(o){this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:/* @__PURE__ */(new Date).toISOString(),error:o.message,content:t},bubbles:!0,composed:!0}))}}filterHiddenFeatures(e){if(!e||0===this.hiddenFeatures.size)return e;if("FeatureCollection"===e.type&&Array.isArray(e.features)){const t=e.features.filter(e=>{const t=this.getFeatureKey(e);return!this.hiddenFeatures.has(t)});return{...e,features:t}}if("Feature"===e.type){const t=this.getFeatureKey(e);if(this.hiddenFeatures.has(t))return{type:"FeatureCollection",features:[]}}return e}getFeatureKey(e){if(!e||"object"!=typeof e)return null;if(void 0!==e.id)return`id:${e.id}`;if(void 0!==e.properties?.id)return`prop:${e.properties.id}`;const t=e.geometry?.type||"null",n=JSON.stringify(e.geometry?.coordinates||[]).slice(0,100);return`hash:${t}:${this.simpleHash(n)}`}simpleHash(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t&=t;return t.toString(36)}toggleFeatureVisibility(e){this.hiddenFeatures.has(e)?this.hiddenFeatures.delete(e):this.hiddenFeatures.add(e),this.updateHighlight(),this.updateGutter(),this.emitChange()}updateFeatureRanges(){const e=this.shadowRoot.getElementById("textarea");if(!e)return;const t=e.value;this.featureRanges.clear();try{const e=this.expandAllCollapsed(t),n=this.prefix+e+this.suffix,o=JSON.parse(n);let s=[];"FeatureCollection"===o.type&&Array.isArray(o.features)?s=o.features:"Feature"===o.type&&(s=[o]);const r=t.split("\n");let i=0,a=0,l=!1,c=-1,d=null;for(let t=0;t<r.length;t++){const e=r[t],n=/"type"\s*:\s*"Feature"/.test(e);if(!l&&n){let e=t;for(let n=t;n>=0;n--)if(r[n].includes("{")){e=n;break}c=e,l=!0,a=1;for(let n=e;n<=t;n++){const t=r[n],o=this._countBracketsOutsideStrings(t,"{");a+=n===e?o.open-1-o.close:o.open-o.close}i<s.length&&(d=this.getFeatureKey(s[i]))}else if(l){const n=this._countBracketsOutsideStrings(e,"{");a+=n.open-n.close,a<=0&&(d&&this.featureRanges.set(d,{startLine:c,endLine:t,featureIndex:i}),i++,l=!1,d=null)}}}catch(n){}}getHiddenLineRanges(){const e=[];for(const[t,n]of this.featureRanges)this.hiddenFeatures.has(t)&&e.push(n);return e}validateGeoJSON(t,n="",o="root"){const s=[];if(!t||"object"!=typeof t)return s;if("properties"!==o&&void 0!==t.type){const r=t.type;"string"==typeof r&&("geometry"===o?e.GEOJSON.GEOMETRY_TYPES.includes(r)||s.push(`Invalid geometry type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.GEOMETRY_TYPES.join(", ")})`):e.GEOJSON.FEATURE_TYPES.includes(r)||s.push(`Invalid type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.FEATURE_TYPES.join(", ")})`))}if(Array.isArray(t))t.forEach((e,t)=>{s.push(...this.validateGeoJSON(e,`${n}[${t}]`,o))});else for(const[e,r]of Object.entries(t))if("object"==typeof r&&null!==r){const t=n?`${n}.${e}`:e;let i=o;"properties"===e?i="properties":"geometry"===e||"geometries"===e?i="geometry":"features"===e&&(i="root"),s.push(...this.validateGeoJSON(r,t,i))}return s}_countBracketsOutsideStrings(e,t){const n="{"===t?"}":"]";let o=0,s=0,r=!1,i=!1;for(let a=0;a<e.length;a++){const l=e[a];i?i=!1:"\\"===l&&r?i=!0:'"'!==l?r||(l===t&&o++,l===n&&s++):r=!r}return{open:o,close:s}}bracketClosesOnSameLine(e,t){const n=e.indexOf(t);if(-1===n)return!1;const o=e.substring(n+1),s=this._countBracketsOutsideStrings(o,t);return s.close>s.open}_findClosingBracket(e,t,n){let o=1;const s=[],r=e[t],i=r.indexOf(n);if(-1!==i){const e=r.substring(i+1),s=this._countBracketsOutsideStrings(e,n);if(o+=s.open-s.close,0===o)return{endLine:t,content:[]}}for(let a=t+1;a<e.length;a++){const t=e[a],r=this._countBracketsOutsideStrings(t,n);if(o+=r.open-r.close,s.push(t),0===o)return{endLine:a,content:s}}return null}_performCollapse(e,t,n,o,s){const r=e[t],i="{"===s?"}":"]";if(this.bracketClosesOnSameLine(r,s))return 0;const a=this._findClosingBracket(e,t,s);if(!a)return 0;const{endLine:l,content:c}=a,d=`${t}-${n}`;this.collapsedData.set(d,{originalLine:r,content:c,indent:o.length,nodeKey:n});const h=r.substring(0,r.indexOf(s)),u=e[l]&&e[l].trim().endsWith(",");e[t]=`${h}${s}...${i}${u?",":""}`;const p=l-t;return e.splice(t+1,p),p}expandAllCollapsed(t){const n=e.REGEX;for(;t.includes("{...}")||t.includes("[...]");){const e=t.split("\n");let o=!1;for(let t=0;t<e.length;t++){const s=e[t];if(!s.includes("{...}")&&!s.includes("[...]"))continue;const r=s.match(n.collapsedMarker);if(!r)continue;const i=r[2],a=r[1].length,l=this._findCollapsedData(t,i,a);if(l){const{data:{originalLine:n,content:s}}=l;e[t]=n,e.splice(t+1,0,...s),o=!0;break}}if(!o)break;t=e.join("\n")}return t}formatJSONContent(e){const t="["+e+"]";let n=JSON.parse(t);Array.isArray(n)&&(n=n.map(e=>this._applyDefaultPropertiesToFeature(e)));const o=JSON.stringify(n,null,2).split("\n");return o.length>2?o.slice(1,-1).join("\n"):""}autoFormatContentWithCursor(){const e=this.shadowRoot.getElementById("textarea"),t=e.selectionStart,n=e.value.substring(0,t).split("\n"),o=n.length-1,s=n[n.length-1].length,r=Array.from(this.collapsedData.values()).map(e=>({nodeKey:e.nodeKey,indent:e.indent})),i=this.expandAllCollapsed(e.value);try{const t=this.formatJSONContent(i);if(t!==i){this.collapsedData.clear(),e.value=t,r.length>0&&this.reapplyCollapsed(r);const n=e.value.split("\n");if(o<n.length){const t=Math.min(s,n[o].length);let r=0;for(let e=0;e<o;e++)r+=n[e].length+1;r+=t,e.setSelectionRange(r,r)}}}catch(a){}}reapplyCollapsed(e){const t=this.shadowRoot.getElementById("textarea"),n=t.value.split("\n"),o=/* @__PURE__ */new Map;e.forEach(({nodeKey:e,indent:t})=>{const n=`${e}-${t}`;o.set(n,(o.get(n)||0)+1)});const s=/* @__PURE__ */new Map;for(let r=n.length-1;r>=0;r--){const e=n[r].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const t=e[2],i=`${t}-${e[1].length}`;if(o.has(i)&&(s.set(i,(s.get(i)||0)+1),s.get(i)<=o.get(i))){const o=e[1],s=e[3];this._performCollapse(n,r,t,o,s)}}}t.value=n.join("\n")}parseSelectorToHostRule(e){return e&&""!==e?e.startsWith(".")&&!e.includes(" ")?`:host(${e})`:`:host-context(${e})`:':host([data-color-scheme="dark"])'}updateThemeCSS(){const t=this.getAttribute("dark-selector")||".dark",n=this.parseSelectorToHostRule(t);let o=this.shadowRoot.getElementById("theme-styles");o||(o=document.createElement("style"),o.id="theme-styles",this.shadowRoot.insertBefore(o,this.shadowRoot.firstChild));const s=t=>Object.entries(t||{}).map(([t,n])=>`--${e._toKebabCase(t)}: ${n};`).join("\n "),r=s(this.themes.light);let i="";r&&(i+=`:host {\n ${r}\n }\n`),i+=`${n} {\n ${s({...e.DARK_THEME_DEFAULTS,...this.themes.dark})}\n }`,o.textContent=i}setTheme(e){e.dark&&(this.themes.dark={...this.themes.dark,...e.dark}),e.light&&(this.themes.light={...this.themes.light,...e.light}),this.updateThemeCSS()}resetTheme(){this.themes={dark:{},light:{}},this.updateThemeCSS()}_normalizeIndex(e,t,n=!1){let o=e;return o<0&&(o=t+o),n?Math.max(0,Math.min(o,t)):o<0||o>=t?-1:o}_parseFeatures(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value.trim())return[];try{const t="["+this.expandAllCollapsed(e.value)+"]";return JSON.parse(t)}catch(t){return[]}}_setFeatures(e){const t=this.shadowRoot.getElementById("textarea");if(t){if(this.collapsedData.clear(),this.hiddenFeatures.clear(),e&&0!==e.length){const n=e.map(e=>JSON.stringify(e,null,2)).join(",\n");t.value=n}else t.value="";this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}_validateFeature(t){const n=[];return t&&"object"==typeof t?Array.isArray(t)?(n.push("Feature cannot be an array"),n):("type"in t?"Feature"!==t.type&&n.push(`Feature type must be "Feature", got "${t.type}"`):n.push('Feature must have a "type" property'),"geometry"in t?null!==t.geometry&&("object"!=typeof t.geometry||Array.isArray(t.geometry)?n.push("Feature geometry must be an object or null"):("type"in t.geometry?e.GEOJSON.GEOMETRY_TYPES.includes(t.geometry.type)||n.push(`Invalid geometry type "${t.geometry.type}" (expected: ${e.GEOJSON.GEOMETRY_TYPES.join(", ")})`):n.push('Geometry must have a "type" property'),"GeometryCollection"===t.geometry.type||"coordinates"in t.geometry||n.push('Geometry must have a "coordinates" property'),"GeometryCollection"!==t.geometry.type||Array.isArray(t.geometry.geometries)||n.push('GeometryCollection must have a "geometries" array'))):n.push('Feature must have a "geometry" property (can be null)'),"properties"in t?null===t.properties||"object"==typeof t.properties&&!Array.isArray(t.properties)||n.push("Feature properties must be an object or null"):n.push('Feature must have a "properties" property (can be null)'),n):(n.push("Feature must be an object"),n)}set(e){if(!Array.isArray(e))throw new Error("set() expects an array of features");const t=[];if(e.forEach((e,n)=>{const o=this._validateFeature(e);o.length>0&&t.push(`Feature[${n}]: ${o.join(", ")}`)}),t.length>0)throw new Error(`Invalid features: ${t.join("; ")}`);const n=e.map(e=>this._applyDefaultPropertiesToFeature(e));this._setFeatures(n)}add(e){const t=this._validateFeature(e);if(t.length>0)throw new Error(`Invalid feature: ${t.join(", ")}`);const n=this._parseFeatures();n.push(this._applyDefaultPropertiesToFeature(e)),this._setFeatures(n)}insertAt(e,t){const n=this._validateFeature(e);if(n.length>0)throw new Error(`Invalid feature: ${n.join(", ")}`);const o=this._parseFeatures(),s=this._normalizeIndex(t,o.length,!0);o.splice(s,0,this._applyDefaultPropertiesToFeature(e)),this._setFeatures(o)}removeAt(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);if(-1===n)return;const o=t.splice(n,1)[0];return this._setFeatures(t),o}removeAll(){const e=this._parseFeatures();return this._setFeatures([]),e}get(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);return-1!==n?t[n]:void 0}getAll(){return this._parseFeatures()}emit(){this.emitChange()}}customElements.define("geojson-editor",e);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softwarity/geojson-editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "A feature-rich GeoJSON editor Web Component with syntax highlighting, collapsible nodes, and color picker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/geojson-editor.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"dev": "vite",
|
|
19
|
-
"build": "vite build
|
|
19
|
+
"build": "vite build",
|
|
20
20
|
"preview": "vite preview",
|
|
21
21
|
"test": "web-test-runner",
|
|
22
22
|
"test:watch": "web-test-runner --watch"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@open-wc/testing": "^4.0.0",
|
|
45
45
|
"@web/test-runner": "^0.20.2",
|
|
46
46
|
"@web/test-runner-playwright": "^0.11.1",
|
|
47
|
-
"
|
|
48
|
-
"vite": "^
|
|
47
|
+
"minify-literals": "^1.0.10",
|
|
48
|
+
"vite": "^7.2.4"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/geojson-editor.js
CHANGED
|
@@ -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
|
-
/*
|
|
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
|
|
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
|
-
|
|
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, #
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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, #
|
|
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::
|
|
367
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
.
|
|
408
|
-
|
|
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
|
-
.
|
|
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, #
|
|
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, #
|
|
460
|
-
background: var(--bg-color, #
|
|
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
|
-
|
|
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, #
|
|
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
|
-
|
|
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
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
textarea
|
|
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
|
-
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
|
875
|
-
|
|
876
|
-
|
|
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.
|
|
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.
|
|
1031
|
-
return GeoJsonEditor.
|
|
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.
|
|
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
|
|
1095
|
-
|
|
1096
|
-
|
|
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 (!
|
|
1083
|
+
if (!found) {
|
|
1119
1084
|
return;
|
|
1120
1085
|
}
|
|
1121
1086
|
|
|
1122
|
-
const {
|
|
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];
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
// Try
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
expandedLines.push(
|
|
1421
|
-
expandedLines.push(...
|
|
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
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
-
|
|
1432
|
-
break;
|
|
1396
|
+
return;
|
|
1433
1397
|
}
|
|
1434
1398
|
}
|
|
1435
|
-
if (found) return;
|
|
1436
1399
|
}
|
|
1437
1400
|
|
|
1438
|
-
//
|
|
1439
|
-
|
|
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
|
|
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.
|
|
1722
|
-
errors.push(`Invalid geometry type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.
|
|
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.
|
|
1727
|
-
errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.
|
|
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
|
|
1852
|
+
const found = this._findCollapsedData(i, nodeKey, currentIndent);
|
|
1904
1853
|
|
|
1905
|
-
|
|
1906
|
-
|
|
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
|
|
1869
|
+
// Helper: Format JSON content (always in FeatureCollection mode)
|
|
1870
|
+
// Also applies default properties to features if configured
|
|
1931
1871
|
formatJSONContent(content) {
|
|
1932
|
-
const
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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.
|
|
2236
|
-
errors.push(`Invalid geometry type "${feature.geometry.type}" (expected: ${GeoJsonEditor.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2253
|
+
// Apply default properties before inserting
|
|
2254
|
+
features.splice(idx, 0, this._applyDefaultPropertiesToFeature(feature));
|
|
2319
2255
|
this._setFeatures(features);
|
|
2320
2256
|
}
|
|
2321
2257
|
|