@softwarity/geojson-editor 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,6 +35,7 @@ A feature-rich, framework-agnostic **Web Component** for editing GeoJSON feature
35
35
  | **Invalid type detection** | ✅ Visual feedback | ❌ | ❌ | ❌ |
36
36
  | **Collapsible nodes** | ✅ Native | ✅ | ✅ Plugin | ❌ |
37
37
  | **Color picker** | ✅ Integrated | ❌ | ❌ | ❌ |
38
+ | **Boolean checkbox** | ✅ Integrated | ❌ | ❌ | ❌ |
38
39
  | **Feature visibility toggle** | ✅ | ❌ | ❌ | ❌ |
39
40
  | **Auto-collapse coordinates** | ✅ | ❌ | ❌ | ❌ |
40
41
  | **FeatureCollection output** | ✅ Always | ❌ | ❌ | ❌ |
@@ -54,7 +55,8 @@ A feature-rich, framework-agnostic **Web Component** for editing GeoJSON feature
54
55
  - **Syntax Highlighting** - JSON syntax highlighting with customizable color schemes
55
56
  - **Collapsible Nodes** - Collapse/expand JSON objects and arrays with visual indicators (`{...}` / `[...]`); `coordinates` auto-collapsed on load
56
57
  - **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
- - **Color Picker** - Built-in color picker for color properties in left gutter
58
+ - **Color Picker** - Built-in color picker for hex color properties (`#rrggbb`) in left gutter; click to open native color picker
59
+ - **Boolean Checkbox** - Inline checkbox for boolean properties in left gutter; toggle to switch between `true`/`false` and emit changes (e.g., `marker: true` to show vertices)
58
60
  - **Default Properties** - Auto-inject default visualization properties (fill-color, stroke-color, etc.) into features based on configurable rules
59
61
  - **Dark/Light Themes** - Automatic theme detection from parent page (Bootstrap, Tailwind, custom)
60
62
  - **Auto-format** - Automatic JSON formatting in real-time (always enabled)
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @license MIT
3
3
  * @name @softwarity/geojson-editor
4
- * @version 1.0.7
4
+ * @version 1.0.9
5
5
  * @author Softwarity (https://www.softwarity.io/)
6
6
  * @copyright 2025 Softwarity
7
7
  * @see https://github.com/softwarity/geojson-editor
8
8
  */
9
- class e extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.collapsedData=/* @__PURE__ */new Map,this.colorPositions=[],this.nodeTogglePositions=[],this.hiddenFeatures=/* @__PURE__ */new Set,this.featureRanges=/* @__PURE__ */new Map,this.highlightTimer=null,this._cachedLineHeight=null,this._cachedPaddingTop=null,this.themes={dark:{},light:{}}}static get observedAttributes(){return["readonly","value","placeholder","dark-selector","default-properties"]}_defaultPropertiesRules=null;static _toKebabCase(e){return e.replace(/([A-Z])/g,"-$1").toLowerCase()}static DARK_THEME_DEFAULTS={bgColor:"#2b2b2b",textColor:"#a9b7c6",caretColor:"#bbbbbb",gutterBg:"#313335",gutterBorder:"#3c3f41",jsonKey:"#9876aa",jsonString:"#6a8759",jsonNumber:"#6897bb",jsonBoolean:"#cc7832",jsonNull:"#cc7832",jsonPunct:"#a9b7c6",controlColor:"#cc7832",controlBg:"#3c3f41",controlBorder:"#5a5a5a",geojsonKey:"#9876aa",geojsonType:"#6a8759",geojsonTypeInvalid:"#ff6b68",jsonKeyInvalid:"#ff6b68"};static REGEX={ampersand:/&/g,lessThan:/</g,greaterThan:/>/g,jsonKey:/"([^"]+)"\s*:/g,typeValue:/<span class="geojson-key">"type"<\/span>:\s*"([^"]*)"/g,stringValue:/:\s*"([^"]*)"/g,numberAfterColon:/:\s*(-?\d+\.?\d*)/g,boolean:/:\s*(true|false)/g,nullValue:/:\s*(null)/g,allNumbers:/\b(-?\d+\.?\d*)\b/g,punctuation:/([{}[\],])/g,colorInLine:/"([\w-]+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,collapsibleNode:/^(\s*)"(\w+)"\s*:\s*([{\[])/,collapsedMarker:/^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/};_findCollapsedData(e,t,n){const o=`${e}-${t}`;if(this.collapsedData.has(o))return{key:o,data:this.collapsedData.get(o)};for(const[s,r]of this.collapsedData.entries())if(r.nodeKey===t&&r.indent===n)return{key:s,data:r};return null}connectedCallback(){this.render(),this.setupEventListeners(),this.updatePrefixSuffix(),this.updateThemeCSS(),this._parseDefaultProperties(),this.value&&this.updateValue(this.value),this.updatePlaceholderContent()}disconnectedCallback(){const e=document.querySelector(".geojson-color-picker-input");e&&e._closeListener&&(document.removeEventListener("click",e._closeListener,!0),e.remove()),this.highlightTimer&&(clearTimeout(this.highlightTimer),this.highlightTimer=null)}attributeChangedCallback(e,t,n){t!==n&&("value"===e?this.updateValue(n):"readonly"===e?this.updateReadonly():"placeholder"===e?this.updatePlaceholderContent():"dark-selector"===e?this.updateThemeCSS():"default-properties"===e&&this._parseDefaultProperties())}get readonly(){return this.hasAttribute("readonly")}get value(){return this.getAttribute("value")||""}get placeholder(){return this.getAttribute("placeholder")||""}get prefix(){return'{"type": "FeatureCollection", "features": ['}get suffix(){return"]}"}get defaultProperties(){return this.getAttribute("default-properties")||""}_parseDefaultProperties(){const e=this.defaultProperties;if(!e)return this._defaultPropertiesRules=[],this._defaultPropertiesRules;try{const t=JSON.parse(e);Array.isArray(t)?this._defaultPropertiesRules=t.map(e=>({match:e.match||null,values:e.values||{}})):this._defaultPropertiesRules="object"==typeof t&&null!==t?[{match:null,values:t}]:[]}catch(t){console.warn("geojson-editor: Invalid default-properties JSON:",t.message),this._defaultPropertiesRules=[]}return this._defaultPropertiesRules}_matchesCondition(e,t){if(!t||"object"!=typeof t)return!0;for(const[n,o]of Object.entries(t))if(this._getNestedValue(e,n)!==o)return!1;return!0}_getNestedValue(e,t){const n=t.split(".");let o=e;for(const s of n){if(null==o)return;o=o[s]}return o}_applyDefaultPropertiesToFeature(e){if(!e||"object"!=typeof e)return e;if(!this._defaultPropertiesRules||0===this._defaultPropertiesRules.length)return e;const t={};for(const r of this._defaultPropertiesRules)this._matchesCondition(e,r.match)&&Object.assign(t,r.values);if(0===Object.keys(t).length)return e;const n=e.properties||{},o={...n};let s=!1;for(const[r,i]of Object.entries(t))r in n||(o[r]=i,s=!0);return s?{...e,properties:o}:e}render(){const e=`\n <div class="prefix-wrapper">\n <div class="prefix-gutter"></div>\n <div class="editor-prefix" id="editorPrefix"></div>\n </div>\n <div class="editor-wrapper">\n <div class="gutter">\n <div class="gutter-content" id="gutterContent"></div>\n </div>\n <div class="editor-content">\n <div class="placeholder-layer" id="placeholderLayer">${this.escapeHtml(this.placeholder)}</div>\n <div class="highlight-layer" id="highlightLayer"></div>\n <textarea\n id="textarea"\n spellcheck="false"\n autocomplete="off"\n autocorrect="off"\n autocapitalize="off"\n ></textarea>\n </div>\n </div>\n <div class="suffix-wrapper">\n <div class="suffix-gutter"></div>\n <div class="editor-suffix" id="editorSuffix"></div>\n <button class="clear-btn" id="clearBtn" title="Clear editor">✕</button>\n </div>\n `;this.shadowRoot.innerHTML="\n <style>\n /* Base reset - protect against inherited styles */\n :host *, :host *::before, :host *::after {\n box-sizing: border-box;\n font: normal normal 13px/1.5 'Courier New', Courier, monospace;\n font-variant: normal;\n letter-spacing: 0;\n word-spacing: 0;\n text-transform: none;\n text-decoration: none;\n text-indent: 0;\n }\n\n :host {\n display: flex;\n flex-direction: column;\n position: relative;\n width: 100%;\n height: 400px;\n border-radius: 4px;\n overflow: hidden;\n }\n\n :host([readonly]) .editor-wrapper::after {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n background: repeating-linear-gradient(-45deg, rgba(128,128,128,0.08), rgba(128,128,128,0.08) 3px, transparent 3px, transparent 12px);\n z-index: 1;\n }\n\n :host([readonly]) textarea { cursor: text; }\n\n .editor-wrapper {\n position: relative;\n width: 100%;\n flex: 1;\n background: var(--bg-color, #fff);\n display: flex;\n }\n\n .gutter {\n width: 24px;\n height: 100%;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n overflow: hidden;\n flex-shrink: 0;\n position: relative;\n }\n\n .gutter-content {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n padding: 8px 4px;\n }\n\n .gutter-line {\n position: absolute;\n left: 0;\n width: 100%;\n height: 1.5em;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .color-indicator, .collapse-button {\n width: 12px;\n height: 12px;\n border-radius: 2px;\n cursor: pointer;\n transition: transform 0.1s;\n flex-shrink: 0;\n }\n\n .color-indicator {\n border: 1px solid #555;\n }\n .color-indicator:hover {\n transform: scale(1.2);\n border-color: #fff;\n }\n\n .collapse-button {\n padding-top: 1px;\n background: var(--control-bg, #e8e8e8);\n border: 1px solid var(--control-border, #c0c0c0);\n color: var(--control-color, #000080);\n font-size: 8px;\n font-weight: bold;\n display: flex;\n align-items: center;\n justify-content: center;\n user-select: none;\n }\n .collapse-button:hover {\n border-color: var(--control-color, #000080);\n transform: scale(1.1);\n }\n\n .visibility-button {\n width: 14px;\n height: 14px;\n background: transparent;\n color: var(--control-color, #000080);\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: all 0.1s;\n flex-shrink: 0;\n opacity: 0.7;\n padding: 0;\n font-size: 11px;\n }\n .visibility-button:hover { opacity: 1; transform: scale(1.15); }\n .visibility-button.hidden { opacity: 0.35; }\n\n .line-hidden { opacity: 0.35; filter: grayscale(50%); }\n\n .editor-content {\n position: relative;\n flex: 1;\n overflow: hidden;\n }\n\n .highlight-layer, textarea, .placeholder-layer {\n position: absolute;\n inset: 0;\n padding: 8px 12px;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n .highlight-layer {\n overflow: auto;\n pointer-events: none;\n z-index: 1;\n color: var(--text-color, #000);\n }\n .highlight-layer::-webkit-scrollbar { display: none; }\n\n textarea {\n margin: 0;\n border: none;\n outline: none;\n background: transparent;\n color: transparent;\n caret-color: var(--caret-color, #000);\n resize: none;\n overflow: auto;\n z-index: 2;\n }\n textarea::selection { background: rgba(51,153,255,0.3); }\n textarea::placeholder { color: transparent; }\n textarea:disabled { cursor: not-allowed; opacity: 0.6; }\n\n .placeholder-layer {\n color: #6a6a6a;\n pointer-events: none;\n z-index: 0;\n overflow: hidden;\n }\n\n .json-key { color: var(--json-key, #660e7a); }\n .json-string { color: var(--json-string, #008000); }\n .json-number { color: var(--json-number, #00f); }\n .json-boolean, .json-null { color: var(--json-boolean, #000080); }\n .json-punctuation { color: var(--json-punct, #000); }\n .json-key-invalid { color: var(--json-key-invalid, #f00); }\n\n .geojson-key { color: var(--geojson-key, #660e7a); font-weight: 600; }\n .geojson-type { color: var(--geojson-type, #008000); font-weight: 600; }\n .geojson-type-invalid { color: var(--geojson-type-invalid, #f00); font-weight: 600; }\n\n .prefix-wrapper, .suffix-wrapper {\n display: flex;\n flex-shrink: 0;\n background: var(--bg-color, #fff);\n }\n\n .prefix-gutter, .suffix-gutter {\n width: 24px;\n background: var(--gutter-bg, #f0f0f0);\n border-right: 1px solid var(--gutter-border, #e0e0e0);\n flex-shrink: 0;\n }\n\n .editor-prefix, .editor-suffix {\n flex: 1;\n padding: 4px 12px;\n color: var(--text-color, #000);\n background: var(--bg-color, #fff);\n user-select: none;\n white-space: pre-wrap;\n word-wrap: break-word;\n opacity: 0.6;\n }\n\n .prefix-wrapper { border-bottom: 1px solid rgba(255,255,255,0.1); }\n .suffix-wrapper { border-top: 1px solid rgba(255,255,255,0.1); position: relative; }\n\n .clear-btn {\n position: absolute;\n right: 0.5rem;\n top: 50%;\n transform: translateY(-50%);\n background: transparent;\n border: none;\n color: var(--text-color, #000);\n opacity: 0.3;\n cursor: pointer;\n font-size: 0.65rem;\n width: 1rem;\n height: 1rem;\n padding: 0.15rem 0 0 0;\n border-radius: 3px;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: opacity 0.2s, background 0.2s;\n }\n .clear-btn:hover { opacity: 0.7; background: rgba(255,255,255,0.1); }\n .clear-btn[hidden] { display: none; }\n\n textarea::-webkit-scrollbar { width: 10px; height: 10px; }\n textarea::-webkit-scrollbar-track { background: var(--control-bg, #e8e8e8); }\n textarea::-webkit-scrollbar-thumb { background: var(--control-border, #c0c0c0); border-radius: 5px; }\n textarea::-webkit-scrollbar-thumb:hover { background: var(--control-color, #000080); }\n textarea { scrollbar-width: thin; scrollbar-color: var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8); }\n </style>\n "+e}setupEventListeners(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");e.addEventListener("scroll",()=>{t.scrollTop=e.scrollTop,t.scrollLeft=e.scrollLeft,this.syncGutterScroll(e.scrollTop)}),e.addEventListener("input",()=>{this.updatePlaceholderVisibility(),clearTimeout(this.highlightTimer),this.highlightTimer=setTimeout(()=>{this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange()},150)}),e.addEventListener("paste",()=>{clearTimeout(this.highlightTimer),setTimeout(()=>{this.updatePlaceholderVisibility(),this.autoFormatContentWithCursor(),this.updateHighlight(),this.emitChange(),this.applyAutoCollapsed()},10)}),this.shadowRoot.getElementById("gutterContent").addEventListener("click",e=>{const t=e.target.closest(".visibility-button");if(t){const e=t.dataset.featureKey;return void this.toggleFeatureVisibility(e)}if(e.target.classList.contains("color-indicator")){const t=parseInt(e.target.dataset.line),n=e.target.dataset.color,o=e.target.dataset.attributeName;this.showColorPicker(e.target,t,n,o)}else if(e.target.classList.contains("collapse-button")){const t=e.target.dataset.nodeKey,n=parseInt(e.target.dataset.line);this.toggleCollapse(t,n)}}),this.shadowRoot.querySelector(".gutter").addEventListener("wheel",t=>{t.preventDefault(),e.scrollTop+=t.deltaY}),e.addEventListener("keydown",e=>{this.handleKeydownInCollapsedArea(e)}),e.addEventListener("copy",e=>{this.handleCopyWithCollapsedContent(e)}),e.addEventListener("cut",e=>{this.handleCutWithCollapsedContent(e)}),this.shadowRoot.getElementById("clearBtn").addEventListener("click",()=>{this.removeAll()}),this.updateReadonly()}syncGutterScroll(e){this.shadowRoot.getElementById("gutterContent").style.transform=`translateY(-${e}px)`}updateReadonly(){const e=this.shadowRoot.getElementById("textarea");e&&(e.disabled=this.readonly);const t=this.shadowRoot.getElementById("clearBtn");t&&(t.hidden=this.readonly)}escapeHtml(t){if(!t)return"";const n=e.REGEX;return t.replace(n.ampersand,"&amp;").replace(n.lessThan,"&lt;").replace(n.greaterThan,"&gt;")}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("placeholderLayer");e&&t&&(t.style.display=e.value?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updateValue(e){const t=this.shadowRoot.getElementById("textarea");if(t&&t.value!==e){if(t.value=e||"",e)try{const n="["+e+"]",o=JSON.parse(n),s=JSON.stringify(o,null,2).split("\n");s.length>2?t.value=s.slice(1,-1).join("\n"):t.value=""}catch(n){}this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateHighlight(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");if(!e||!t)return;const n=e.value;this.updateFeatureRanges();const o=this.getHiddenLineRanges(),{highlighted:s,colors:r,toggles:i}=this.highlightJSON(n,o);t.innerHTML=s,this.colorPositions=r,this.nodeTogglePositions=i,this.updateGutter()}highlightJSON(t,n=[]){if(!t.trim())return{highlighted:"",colors:[],toggles:[]};const o=t.split("\n"),s=[],r=[];let i=[];const a=this.buildContextMap(t);return o.forEach((t,o)=>{const l=e.REGEX;let c;for(l.colorInLine.lastIndex=0;null!==(c=l.colorInLine.exec(t));)s.push({line:o,color:c[2],attributeName:c[1]});const d=t.match(l.collapsibleNode);if(d){const e=d[2];t.includes("{...}")||t.includes("[...]")?r.push({line:o,nodeKey:e,isCollapsed:!0}):this.bracketClosesOnSameLine(t,d[3])||r.push({line:o,nodeKey:e,isCollapsed:!1})}const h=a.get(o);let u=this.highlightSyntax(t,h);(e=>n.some(t=>e>=t.startLine&&e<=t.endLine))(o)&&(u=`<span class="line-hidden">${u}</span>`),i.push(u)}),{highlighted:i.join("\n"),colors:s,toggles:r}}static GEOJSON={FEATURE_TYPES:["Feature","FeatureCollection"],GEOMETRY_TYPES:["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"],ALL_TYPES:["Feature","FeatureCollection","Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"]};static VALID_KEYS_BY_CONTEXT={Feature:["type","geometry","properties","id","bbox"],FeatureCollection:["type","features","bbox","properties"],Point:["type","coordinates","bbox"],MultiPoint:["type","coordinates","bbox"],LineString:["type","coordinates","bbox"],MultiLineString:["type","coordinates","bbox"],Polygon:["type","coordinates","bbox"],MultiPolygon:["type","coordinates","bbox"],GeometryCollection:["type","geometries","bbox"],properties:null,geometry:["type","coordinates","geometries","bbox"]};static CONTEXT_CHANGING_KEYS={geometry:"geometry",properties:"properties",features:"Feature",geometries:"geometry"};buildContextMap(t){const n=t.split("\n"),o=/* @__PURE__ */new Map,s=[];let r=null;const i="Feature";for(let a=0;a<n.length;a++){const t=n[a],l=s.length>0?s[s.length-1]?.context:i;o.set(a,l);let c=!1,d=!1;for(let n=0;n<t.length;n++){const o=t[n];if(d)d=!1;else if("\\"===o&&c)d=!0;else if('"'!==o){if(!c){if("{"===o||"["===o){let e;if(r)e=r,r=null;else if(0===s.length)e=i;else{const t=s[s.length-1];e=t&&t.isArray?t.context:null}s.push({context:e,isArray:"["===o})}"}"!==o&&"]"!==o||s.length>0&&s.pop()}}else{if(!c){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/);if(o){const t=o[1];e.CONTEXT_CHANGING_KEYS[t]&&(r=e.CONTEXT_CHANGING_KEYS[t]),n+=o[0].length-1;continue}if(s.length>0&&t.substring(0,n).match(/"type"\s*:\s*$/)){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);if(o&&e.GEOJSON.ALL_TYPES.includes(o[1])){const e=s[s.length-1];e&&(e.context=o[1])}n+=o?o[0].length-1:0;continue}}c=!c}}}return o}static GEOJSON_STRUCTURAL_KEYS=["type","geometry","properties","features","geometries","coordinates","bbox","id","crs"];highlightSyntax(t,n){if(!t.trim())return"";const o=n?e.VALID_KEYS_BY_CONTEXT[n]:null,s=e.REGEX;return t.replace(s.ampersand,"&amp;").replace(s.lessThan,"&lt;").replace(s.greaterThan,"&gt;").replace(s.jsonKey,(t,s)=>"properties"===n?`<span class="json-key">"${s}"</span>:`:e.GEOJSON_STRUCTURAL_KEYS.includes(s)?`<span class="geojson-key">"${s}"</span>:`:(t=>!!e.GEOJSON_STRUCTURAL_KEYS.includes(t)||!n||null==o||o.includes(t))(s)?`<span class="json-key">"${s}"</span>:`:`<span class="json-key-invalid">"${s}"</span>:`).replace(s.typeValue,(t,o)=>(t=>!n||"properties"===n||("geometry"===n||e.GEOJSON.GEOMETRY_TYPES.includes(n)?e.GEOJSON.GEOMETRY_TYPES.includes(t):"Feature"!==n&&"FeatureCollection"!==n||e.GEOJSON.ALL_TYPES.includes(t)))(o)?`<span class="geojson-key">"type"</span>: <span class="geojson-type">"${o}"</span>`:`<span class="geojson-key">"type"</span>: <span class="geojson-type-invalid">"${o}"</span>`).replace(s.stringValue,(e,t)=>e.includes("<span")?e:`: <span class="json-string">"${t}"</span>`).replace(s.numberAfterColon,': <span class="json-number">$1</span>').replace(s.boolean,': <span class="json-boolean">$1</span>').replace(s.nullValue,': <span class="json-null">$1</span>').replace(s.allNumbers,'<span class="json-number">$1</span>').replace(s.punctuation,'<span class="json-punctuation">$1</span>')}toggleCollapse(e,t){const n=this.shadowRoot.getElementById("textarea"),o=n.value.split("\n"),s=o[t];if(s.includes("{...}")||s.includes("[...]")){const n=s.match(/^(\s*)/)[1].length,r=this._findCollapsedData(t,e,n);if(!r)return;const{key:i,data:a}=r,{originalLine:l,content:c}=a;o[t]=l,o.splice(t+1,0,...c),this.collapsedData.delete(i)}else{const n=s.match(/^(\s*)"([^"]+)"\s*:\s*([{\[])/);if(!n)return;const r=n[1],i=n[3];if(0===this._performCollapse(o,t,e,r,i))return}n.value=o.join("\n"),this.updateHighlight()}applyAutoCollapsed(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value)return;const t=e.value.split("\n");for(let n=t.length-1;n>=0;n--){const e=t[n].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const o=e[2];if("coordinates"===o){const s=e[1],r=e[3];this._performCollapse(t,n,o,s,r)}}}e.value=t.join("\n"),this.updateHighlight()}updateGutter(){const e=this.shadowRoot.getElementById("gutterContent"),t=this.shadowRoot.getElementById("textarea");if(!t)return;if(null===this._cachedLineHeight){const e=getComputedStyle(t);this._cachedLineHeight=parseFloat(e.lineHeight),this._cachedPaddingTop=parseFloat(e.paddingTop)}const n=this._cachedLineHeight,o=this._cachedPaddingTop;e.textContent="";const s=/* @__PURE__ */new Map,r=e=>(s.has(e)||s.set(e,{colors:[],buttons:[],visibilityButtons:[]}),s.get(e));this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{r(e).colors.push({color:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{r(e).buttons.push({nodeKey:t,isCollapsed:n})});for(const[a,l]of this.featureRanges){const e=this.hiddenFeatures.has(a);r(l.startLine).visibilityButtons.push({featureKey:a,isHidden:e})}const i=document.createDocumentFragment();s.forEach((e,t)=>{const s=document.createElement("div");s.className="gutter-line",s.style.top=`${o+t*n}px`,e.visibilityButtons.forEach(({featureKey:e,isHidden:t})=>{const n=document.createElement("button");n.className="visibility-button"+(t?" hidden":""),n.textContent="👁",n.dataset.featureKey=e,n.title=t?"Show feature in events":"Hide feature from events",s.appendChild(n)}),e.colors.forEach(({color:e,attributeName:n})=>{const o=document.createElement("div");o.className="color-indicator",o.style.backgroundColor=e,o.dataset.line=t,o.dataset.color=e,o.dataset.attributeName=n,o.title=`${n}: ${e}`,s.appendChild(o)}),e.buttons.forEach(({nodeKey:e,isCollapsed:n})=>{const o=document.createElement("div");o.className="collapse-button",o.textContent=n?"+":"-",o.dataset.line=t,o.dataset.nodeKey=e,o.title=n?"Expand":"Collapse",s.appendChild(o)}),i.appendChild(s)}),e.appendChild(i)}showColorPicker(e,t,n,o){const s=document.querySelector(".geojson-color-picker-input");s&&(s._closeListener&&document.removeEventListener("click",s._closeListener,!0),s.remove());const r=document.createElement("input");r.type="color",r.value=n,r.className="geojson-color-picker-input";const i=e.getBoundingClientRect();r.style.position="fixed",r.style.left=`${i.left}px`,r.style.top=`${i.top}px`,r.style.width="12px",r.style.height="12px",r.style.opacity="0.01",r.style.border="none",r.style.padding="0",r.style.zIndex="9999",r.addEventListener("input",e=>{this.updateColorValue(t,e.target.value,o)}),r.addEventListener("change",e=>{this.updateColorValue(t,e.target.value,o)});const a=e=>{e.target===r||r.contains(e.target)||(document.removeEventListener("click",a,!0),r.remove())};r._closeListener=a,document.body.appendChild(r),setTimeout(()=>{document.addEventListener("click",a,!0)},100),r.focus(),r.click()}updateColorValue(e,t,n){const o=this.shadowRoot.getElementById("textarea"),s=o.value.split("\n"),r=new RegExp(`"${n}"\\s*:\\s*"#[0-9a-fA-F]{6}"`);s[e]=s[e].replace(r,`"${n}": "${t}"`),o.value=s.join("\n"),this.updateHighlight(),this.emitChange()}handleKeydownInCollapsedArea(e){if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Tab"].includes(e.key))return;if(e.ctrlKey||e.metaKey)return;const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.value.substring(0,n).split("\n").length-1,s=t.value.split("\n")[o];s&&(s.includes("{...}")||s.includes("[...]"))&&e.preventDefault()}handleCopyWithCollapsedContent(e){const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n===o)return;const s=t.value.substring(n,o);if(!s.includes("{...}")&&!s.includes("[...]"))return;let r;r=0===n&&o===t.value.length?this.expandAllCollapsed(s):this.expandCollapsedMarkersInText(s,n),e.preventDefault(),e.clipboardData.setData("text/plain",r)}expandCollapsedMarkersInText(t,n){const o=this.shadowRoot.getElementById("textarea").value.substring(0,n).split("\n").length-1,s=e.REGEX,r=t.split("\n"),i=[];return r.forEach((e,t)=>{const n=o+t;if(e.includes("{...}")||e.includes("[...]")){const t=e.match(s.collapsedMarker);if(t){const e=t[2],o=t[1].length,s=this._findCollapsedData(n,e,o);if(s)return i.push(s.data.originalLine),void i.push(...s.data.content);for(const[,t]of this.collapsedData.entries())if(t.nodeKey===e)return i.push(t.originalLine),void i.push(...t.content)}i.push(e)}else i.push(e)}),i.join("\n")}handleCutWithCollapsedContent(e){this.handleCopyWithCollapsedContent(e);const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n!==o){const e=t.value;t.value=e.substring(0,n)+e.substring(o),t.selectionStart=t.selectionEnd=n,this.updateHighlight(),this.updatePlaceholderVisibility(),this.emitChange()}}emitChange(){const e=this.shadowRoot.getElementById("textarea"),t=this.expandAllCollapsed(e.value),n=this.prefix+t+this.suffix;try{let e=JSON.parse(n);e=this.filterHiddenFeatures(e);const o=this.validateGeoJSON(e);o.length>0?this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:/* @__PURE__ */(new Date).toISOString(),error:`GeoJSON validation: ${o.join("; ")}`,errors:o,content:t},bubbles:!0,composed:!0})):this.dispatchEvent(new CustomEvent("change",{detail:e,bubbles:!0,composed:!0}))}catch(o){this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:/* @__PURE__ */(new Date).toISOString(),error:o.message,content:t},bubbles:!0,composed:!0}))}}filterHiddenFeatures(e){if(!e||0===this.hiddenFeatures.size)return e;if("FeatureCollection"===e.type&&Array.isArray(e.features)){const t=e.features.filter(e=>{const t=this.getFeatureKey(e);return!this.hiddenFeatures.has(t)});return{...e,features:t}}if("Feature"===e.type){const t=this.getFeatureKey(e);if(this.hiddenFeatures.has(t))return{type:"FeatureCollection",features:[]}}return e}getFeatureKey(e){if(!e||"object"!=typeof e)return null;if(void 0!==e.id)return`id:${e.id}`;if(void 0!==e.properties?.id)return`prop:${e.properties.id}`;const t=e.geometry?.type||"null",n=JSON.stringify(e.geometry?.coordinates||[]).slice(0,100);return`hash:${t}:${this.simpleHash(n)}`}simpleHash(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t&=t;return t.toString(36)}toggleFeatureVisibility(e){this.hiddenFeatures.has(e)?this.hiddenFeatures.delete(e):this.hiddenFeatures.add(e),this.updateHighlight(),this.updateGutter(),this.emitChange()}updateFeatureRanges(){const e=this.shadowRoot.getElementById("textarea");if(!e)return;const t=e.value;this.featureRanges.clear();try{const e=this.expandAllCollapsed(t),n=this.prefix+e+this.suffix,o=JSON.parse(n);let s=[];"FeatureCollection"===o.type&&Array.isArray(o.features)?s=o.features:"Feature"===o.type&&(s=[o]);const r=t.split("\n");let i=0,a=0,l=!1,c=-1,d=null;for(let t=0;t<r.length;t++){const e=r[t],n=/"type"\s*:\s*"Feature"/.test(e);if(!l&&n){let e=t;for(let n=t;n>=0;n--)if(r[n].includes("{")){e=n;break}c=e,l=!0,a=1;for(let n=e;n<=t;n++){const t=r[n],o=this._countBracketsOutsideStrings(t,"{");a+=n===e?o.open-1-o.close:o.open-o.close}i<s.length&&(d=this.getFeatureKey(s[i]))}else if(l){const n=this._countBracketsOutsideStrings(e,"{");a+=n.open-n.close,a<=0&&(d&&this.featureRanges.set(d,{startLine:c,endLine:t,featureIndex:i}),i++,l=!1,d=null)}}}catch(n){}}getHiddenLineRanges(){const e=[];for(const[t,n]of this.featureRanges)this.hiddenFeatures.has(t)&&e.push(n);return e}validateGeoJSON(t,n="",o="root"){const s=[];if(!t||"object"!=typeof t)return s;if("properties"!==o&&void 0!==t.type){const r=t.type;"string"==typeof r&&("geometry"===o?e.GEOJSON.GEOMETRY_TYPES.includes(r)||s.push(`Invalid geometry type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.GEOMETRY_TYPES.join(", ")})`):e.GEOJSON.FEATURE_TYPES.includes(r)||s.push(`Invalid type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.FEATURE_TYPES.join(", ")})`))}if(Array.isArray(t))t.forEach((e,t)=>{s.push(...this.validateGeoJSON(e,`${n}[${t}]`,o))});else for(const[e,r]of Object.entries(t))if("object"==typeof r&&null!==r){const t=n?`${n}.${e}`:e;let i=o;"properties"===e?i="properties":"geometry"===e||"geometries"===e?i="geometry":"features"===e&&(i="root"),s.push(...this.validateGeoJSON(r,t,i))}return s}_countBracketsOutsideStrings(e,t){const n="{"===t?"}":"]";let o=0,s=0,r=!1,i=!1;for(let a=0;a<e.length;a++){const l=e[a];i?i=!1:"\\"===l&&r?i=!0:'"'!==l?r||(l===t&&o++,l===n&&s++):r=!r}return{open:o,close:s}}bracketClosesOnSameLine(e,t){const n=e.indexOf(t);if(-1===n)return!1;const o=e.substring(n+1),s=this._countBracketsOutsideStrings(o,t);return s.close>s.open}_findClosingBracket(e,t,n){let o=1;const s=[],r=e[t],i=r.indexOf(n);if(-1!==i){const e=r.substring(i+1),s=this._countBracketsOutsideStrings(e,n);if(o+=s.open-s.close,0===o)return{endLine:t,content:[]}}for(let a=t+1;a<e.length;a++){const t=e[a],r=this._countBracketsOutsideStrings(t,n);if(o+=r.open-r.close,s.push(t),0===o)return{endLine:a,content:s}}return null}_performCollapse(e,t,n,o,s){const r=e[t],i="{"===s?"}":"]";if(this.bracketClosesOnSameLine(r,s))return 0;const a=this._findClosingBracket(e,t,s);if(!a)return 0;const{endLine:l,content:c}=a,d=`${t}-${n}`;this.collapsedData.set(d,{originalLine:r,content:c,indent:o.length,nodeKey:n});const h=r.substring(0,r.indexOf(s)),u=e[l]&&e[l].trim().endsWith(",");e[t]=`${h}${s}...${i}${u?",":""}`;const p=l-t;return e.splice(t+1,p),p}expandAllCollapsed(t){const n=e.REGEX;for(;t.includes("{...}")||t.includes("[...]");){const e=t.split("\n");let o=!1;for(let t=0;t<e.length;t++){const s=e[t];if(!s.includes("{...}")&&!s.includes("[...]"))continue;const r=s.match(n.collapsedMarker);if(!r)continue;const i=r[2],a=r[1].length,l=this._findCollapsedData(t,i,a);if(l){const{data:{originalLine:n,content:s}}=l;e[t]=n,e.splice(t+1,0,...s),o=!0;break}}if(!o)break;t=e.join("\n")}return t}formatJSONContent(e){const t="["+e+"]";let n=JSON.parse(t);Array.isArray(n)&&(n=n.map(e=>this._applyDefaultPropertiesToFeature(e)));const o=JSON.stringify(n,null,2).split("\n");return o.length>2?o.slice(1,-1).join("\n"):""}autoFormatContentWithCursor(){const e=this.shadowRoot.getElementById("textarea"),t=e.selectionStart,n=e.value.substring(0,t).split("\n"),o=n.length-1,s=n[n.length-1].length,r=Array.from(this.collapsedData.values()).map(e=>({nodeKey:e.nodeKey,indent:e.indent})),i=this.expandAllCollapsed(e.value);try{const t=this.formatJSONContent(i);if(t!==i){this.collapsedData.clear(),e.value=t,r.length>0&&this.reapplyCollapsed(r);const n=e.value.split("\n");if(o<n.length){const t=Math.min(s,n[o].length);let r=0;for(let e=0;e<o;e++)r+=n[e].length+1;r+=t,e.setSelectionRange(r,r)}}}catch(a){}}reapplyCollapsed(e){const t=this.shadowRoot.getElementById("textarea"),n=t.value.split("\n"),o=/* @__PURE__ */new Map;e.forEach(({nodeKey:e,indent:t})=>{const n=`${e}-${t}`;o.set(n,(o.get(n)||0)+1)});const s=/* @__PURE__ */new Map;for(let r=n.length-1;r>=0;r--){const e=n[r].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const t=e[2],i=`${t}-${e[1].length}`;if(o.has(i)&&(s.set(i,(s.get(i)||0)+1),s.get(i)<=o.get(i))){const o=e[1],s=e[3];this._performCollapse(n,r,t,o,s)}}}t.value=n.join("\n")}parseSelectorToHostRule(e){return e&&""!==e?e.startsWith(".")&&!e.includes(" ")?`:host(${e})`:`:host-context(${e})`:':host([data-color-scheme="dark"])'}updateThemeCSS(){const t=this.getAttribute("dark-selector")||".dark",n=this.parseSelectorToHostRule(t);let o=this.shadowRoot.getElementById("theme-styles");o||(o=document.createElement("style"),o.id="theme-styles",this.shadowRoot.insertBefore(o,this.shadowRoot.firstChild));const s=t=>Object.entries(t||{}).map(([t,n])=>`--${e._toKebabCase(t)}: ${n};`).join("\n "),r=s(this.themes.light);let i="";r&&(i+=`:host {\n ${r}\n }\n`),i+=`${n} {\n ${s({...e.DARK_THEME_DEFAULTS,...this.themes.dark})}\n }`,o.textContent=i}setTheme(e){e.dark&&(this.themes.dark={...this.themes.dark,...e.dark}),e.light&&(this.themes.light={...this.themes.light,...e.light}),this.updateThemeCSS()}resetTheme(){this.themes={dark:{},light:{}},this.updateThemeCSS()}_normalizeIndex(e,t,n=!1){let o=e;return o<0&&(o=t+o),n?Math.max(0,Math.min(o,t)):o<0||o>=t?-1:o}_parseFeatures(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value.trim())return[];try{const t="["+this.expandAllCollapsed(e.value)+"]";return JSON.parse(t)}catch(t){return[]}}_setFeatures(e){const t=this.shadowRoot.getElementById("textarea");if(t){if(this.collapsedData.clear(),this.hiddenFeatures.clear(),e&&0!==e.length){const n=e.map(e=>JSON.stringify(e,null,2)).join(",\n");t.value=n}else t.value="";this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}_validateFeature(t){const n=[];return t&&"object"==typeof t?Array.isArray(t)?(n.push("Feature cannot be an array"),n):("type"in t?"Feature"!==t.type&&n.push(`Feature type must be "Feature", got "${t.type}"`):n.push('Feature must have a "type" property'),"geometry"in t?null!==t.geometry&&("object"!=typeof t.geometry||Array.isArray(t.geometry)?n.push("Feature geometry must be an object or null"):("type"in t.geometry?e.GEOJSON.GEOMETRY_TYPES.includes(t.geometry.type)||n.push(`Invalid geometry type "${t.geometry.type}" (expected: ${e.GEOJSON.GEOMETRY_TYPES.join(", ")})`):n.push('Geometry must have a "type" property'),"GeometryCollection"===t.geometry.type||"coordinates"in t.geometry||n.push('Geometry must have a "coordinates" property'),"GeometryCollection"!==t.geometry.type||Array.isArray(t.geometry.geometries)||n.push('GeometryCollection must have a "geometries" array'))):n.push('Feature must have a "geometry" property (can be null)'),"properties"in t?null===t.properties||"object"==typeof t.properties&&!Array.isArray(t.properties)||n.push("Feature properties must be an object or null"):n.push('Feature must have a "properties" property (can be null)'),n):(n.push("Feature must be an object"),n)}set(e){if(!Array.isArray(e))throw new Error("set() expects an array of features");const t=[];if(e.forEach((e,n)=>{const o=this._validateFeature(e);o.length>0&&t.push(`Feature[${n}]: ${o.join(", ")}`)}),t.length>0)throw new Error(`Invalid features: ${t.join("; ")}`);const n=e.map(e=>this._applyDefaultPropertiesToFeature(e));this._setFeatures(n)}add(e){const t=this._validateFeature(e);if(t.length>0)throw new Error(`Invalid feature: ${t.join(", ")}`);const n=this._parseFeatures();n.push(this._applyDefaultPropertiesToFeature(e)),this._setFeatures(n)}insertAt(e,t){const n=this._validateFeature(e);if(n.length>0)throw new Error(`Invalid feature: ${n.join(", ")}`);const o=this._parseFeatures(),s=this._normalizeIndex(t,o.length,!0);o.splice(s,0,this._applyDefaultPropertiesToFeature(e)),this._setFeatures(o)}removeAt(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);if(-1===n)return;const o=t.splice(n,1)[0];return this._setFeatures(t),o}removeAll(){const e=this._parseFeatures();return this._setFeatures([]),e}get(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);return-1!==n?t[n]:void 0}getAll(){return this._parseFeatures()}emit(){this.emitChange()}}customElements.define("geojson-editor",e);
9
+ class e extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.collapsedData=/* @__PURE__ */new Map,this.colorPositions=[],this.booleanPositions=[],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]{3}|[0-9a-fA-F]{6}))"/g,booleanInLine:/"([\w-]+)"\s*:\s*(true|false)/g,collapsibleNode:/^(\s*)"(\w+)"\s*:\s*([{\[])/,collapsedMarker:/^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/};static ICONS={expanded:"⌄",collapsed:"›",visibility:"👁"};_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,a]of Object.entries(t))r in n||(o[r]=a,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, .boolean-checkbox {\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 .boolean-checkbox {\n appearance: none;\n -webkit-appearance: none;\n background: transparent;\n border: 1.5px solid var(--control-border, #c0c0c0);\n border-radius: 2px;\n margin: 0;\n position: relative;\n }\n .boolean-checkbox:checked {\n border-color: var(--control-color, #000080);\n }\n .boolean-checkbox:checked::after {\n content: '✔';\n color: var(--control-color, #000080);\n font-size: 11px;\n font-weight: bold;\n position: absolute;\n top: -3px;\n right: -1px;\n }\n .boolean-checkbox:hover {\n transform: scale(1.2);\n border-color: var(--control-color, #000080);\n }\n\n .collapse-button {\n background: transparent;\n border: none;\n color: var(--json-punct, #a9b7c6);\n font-size: 10px;\n display: flex;\n align-items: center;\n justify-content: center;\n user-select: none;\n opacity: 0;\n transition: opacity 0.15s;\n }\n .collapse-button.collapsed {\n opacity: 1;\n }\n .gutter:hover .collapse-button {\n opacity: 1;\n }\n .collapse-button:hover {\n transform: scale(1.2);\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("boolean-checkbox")){const t=parseInt(e.target.dataset.line),n=e.target.dataset.attributeName,o=e.target.checked;this.updateBooleanValue(t,o,n)}else if(e.target.classList.contains("collapse-button")){const t=e.target.dataset.nodeKey,n=parseInt(e.target.dataset.line);this.toggleCollapse(t,n)}}),this.shadowRoot.querySelector(".gutter").addEventListener("wheel",t=>{t.preventDefault(),e.scrollTop+=t.deltaY}),e.addEventListener("keydown",e=>{this.handleKeydownInCollapsedArea(e)}),e.addEventListener("copy",e=>{this.handleCopyWithCollapsedContent(e)}),e.addEventListener("cut",e=>{this.handleCutWithCollapsedContent(e)}),this.shadowRoot.getElementById("clearBtn").addEventListener("click",()=>{this.removeAll()}),this.updateReadonly()}syncGutterScroll(e){this.shadowRoot.getElementById("gutterContent").style.transform=`translateY(-${e}px)`}updateReadonly(){const e=this.shadowRoot.getElementById("textarea");e&&(e.disabled=this.readonly);const t=this.shadowRoot.getElementById("clearBtn");t&&(t.hidden=this.readonly)}escapeHtml(t){if(!t)return"";const n=e.REGEX;return t.replace(n.ampersand,"&amp;").replace(n.lessThan,"&lt;").replace(n.greaterThan,"&gt;")}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("placeholderLayer");e&&t&&(t.style.display=e.value?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updateValue(e){const t=this.shadowRoot.getElementById("textarea");if(t&&t.value!==e){if(t.value=e||"",e)try{const n="["+e+"]",o=JSON.parse(n),s=JSON.stringify(o,null,2).split("\n");s.length>2?t.value=s.slice(1,-1).join("\n"):t.value=""}catch(n){}this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateHighlight(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");if(!e||!t)return;const n=e.value;this.updateFeatureRanges();const o=this.getHiddenLineRanges(),{highlighted:s,colors:r,booleans:a,toggles:i}=this.highlightJSON(n,o);t.innerHTML=s,this.colorPositions=r,this.booleanPositions=a,this.nodeTogglePositions=i,this.updateGutter()}highlightJSON(t,n=[]){if(!t.trim())return{highlighted:"",colors:[],booleans:[],toggles:[]};const o=t.split("\n"),s=[],r=[],a=[];let i=[];const l=this.buildContextMap(t);return o.forEach((t,o)=>{const c=e.REGEX;let d,h;for(c.colorInLine.lastIndex=0;null!==(d=c.colorInLine.exec(t));)s.push({line:o,color:d[2],attributeName:d[1]});for(c.booleanInLine.lastIndex=0;null!==(h=c.booleanInLine.exec(t));)r.push({line:o,value:"true"===h[2],attributeName:h[1]});const u=t.match(c.collapsibleNode);if(u){const e=u[2];t.includes("{...}")||t.includes("[...]")?a.push({line:o,nodeKey:e,isCollapsed:!0}):this.bracketClosesOnSameLine(t,u[3])||a.push({line:o,nodeKey:e,isCollapsed:!1})}const p=l.get(o);let g=this.highlightSyntax(t,p);(e=>n.some(t=>e>=t.startLine&&e<=t.endLine))(o)&&(g=`<span class="line-hidden">${g}</span>`),i.push(g)}),{highlighted:i.join("\n"),colors:s,booleans:r,toggles:a}}static GEOJSON={GEOMETRY_TYPES:["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon"]};static VALID_KEYS_BY_CONTEXT={Feature:["type","geometry","properties","id"],properties:null,geometry:["type","coordinates"]};static CONTEXT_CHANGING_KEYS={geometry:"geometry",properties:"properties",features:"Feature"};buildContextMap(t){const n=t.split("\n"),o=/* @__PURE__ */new Map,s=[];let r=null;const a="Feature";for(let i=0;i<n.length;i++){const t=n[i],l=s.length>0?s[s.length-1]?.context:a;o.set(i,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=a;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(/^"([^"\\]*(?:\\.[^"\\]*)*)"/),r=["Feature",...e.GEOJSON.GEOMETRY_TYPES];if(o&&r.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","coordinates","id"];highlightSyntax(t,n){if(!t.trim())return"";const o=n?e.VALID_KEYS_BY_CONTEXT[n]:null,s=e.REGEX;return t.replace(s.ampersand,"&amp;").replace(s.lessThan,"&lt;").replace(s.greaterThan,"&gt;").replace(s.jsonKey,(t,s)=>"properties"===n?`<span class="json-key">"${s}"</span>:`:e.GEOJSON_STRUCTURAL_KEYS.includes(s)?`<span class="geojson-key">"${s}"</span>:`:(t=>!!e.GEOJSON_STRUCTURAL_KEYS.includes(t)||!n||null==o||o.includes(t))(s)?`<span class="json-key">"${s}"</span>:`:`<span class="json-key-invalid">"${s}"</span>:`).replace(s.typeValue,(t,o)=>(t=>!n||"properties"===n||("geometry"===n||e.GEOJSON.GEOMETRY_TYPES.includes(n)?e.GEOJSON.GEOMETRY_TYPES.includes(t):"Feature"!==n||"Feature"===t||e.GEOJSON.GEOMETRY_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:a,data:i}=r,{originalLine:l,content:c}=i;o[t]=l,o.splice(t+1,0,...c),this.collapsedData.delete(a)}else{const n=s.match(/^(\s*)"([^"]+)"\s*:\s*([{\[])/);if(!n)return;const r=n[1],a=n[3];if(0===this._performCollapse(o,t,e,r,a))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 t=this.shadowRoot.getElementById("gutterContent"),n=this.shadowRoot.getElementById("textarea");if(!n)return;if(null===this._cachedLineHeight){const e=getComputedStyle(n);this._cachedLineHeight=parseFloat(e.lineHeight),this._cachedPaddingTop=parseFloat(e.paddingTop)}const o=this._cachedLineHeight,s=this._cachedPaddingTop;t.textContent="";const r=/* @__PURE__ */new Map,a=e=>(r.has(e)||r.set(e,{colors:[],booleans:[],buttons:[],visibilityButtons:[]}),r.get(e));this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{a(e).colors.push({color:t,attributeName:n})}),this.booleanPositions.forEach(({line:e,value:t,attributeName:n})=>{a(e).booleans.push({value:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{a(e).buttons.push({nodeKey:t,isCollapsed:n})});for(const[e,l]of this.featureRanges){const t=this.hiddenFeatures.has(e);a(l.startLine).visibilityButtons.push({featureKey:e,isHidden:t})}const i=document.createDocumentFragment();r.forEach((t,n)=>{const r=document.createElement("div");r.className="gutter-line",r.style.top=`${s+n*o}px`,t.visibilityButtons.forEach(({featureKey:t,isHidden:n})=>{const o=document.createElement("button");o.className="visibility-button"+(n?" hidden":""),o.textContent=e.ICONS.visibility,o.dataset.featureKey=t,o.title=n?"Show feature in events":"Hide feature from events",r.appendChild(o)}),t.colors.forEach(({color:e,attributeName:t})=>{const o=document.createElement("div");o.className="color-indicator",o.style.backgroundColor=e,o.dataset.line=n,o.dataset.color=e,o.dataset.attributeName=t,o.title=`${t}: ${e}`,r.appendChild(o)}),t.booleans.forEach(({value:e,attributeName:t})=>{const o=document.createElement("input");o.type="checkbox",o.className="boolean-checkbox",o.checked=e,o.dataset.line=n,o.dataset.attributeName=t,o.title=`${t}: ${e}`,r.appendChild(o)}),t.buttons.forEach(({nodeKey:t,isCollapsed:o})=>{const s=document.createElement("div");s.className=o?"collapse-button collapsed":"collapse-button",s.textContent=o?e.ICONS.collapsed:e.ICONS.expanded,s.dataset.line=n,s.dataset.nodeKey=t,s.title=o?"Expand":"Collapse",r.appendChild(s)}),i.appendChild(r)}),t.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 a=e.getBoundingClientRect();r.style.position="fixed",r.style.left=`${a.left}px`,r.style.top=`${a.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 i=e=>{e.target===r||r.contains(e.target)||(document.removeEventListener("click",i,!0),r.remove())};r._closeListener=i,document.body.appendChild(r),setTimeout(()=>{document.addEventListener("click",i,!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]{3}|[0-9a-fA-F]{6})"`);s[e]=s[e].replace(r,`"${n}": "${t}"`),o.value=s.join("\n"),this.updateHighlight(),this.emitChange()}updateBooleanValue(e,t,n){const o=this.shadowRoot.getElementById("textarea"),s=o.value.split("\n"),r=new RegExp(`"${n}"\\s*:\\s*(true|false)`);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"),a=[];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 a.push(s.data.originalLine),void a.push(...s.data.content);for(const[,t]of this.collapsedData.entries())if(t.nodeKey===e)return a.push(t.originalLine),void a.push(...t.content)}a.push(e)}else a.push(e)}),a.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);let o=[];e.features.forEach((e,t)=>{o.push(...this.validateGeoJSON(e,`features[${t}]`,"root"))}),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;const t=e.features.filter(e=>{const t=this.getFeatureKey(e);return!this.hiddenFeatures.has(t)});return{...e,features:t}}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||[]);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).features,s=t.split("\n");let r=0,a=0,i=!1,l=-1,c=null;for(let t=0;t<s.length;t++){const e=s[t],n=/"type"\s*:\s*"Feature"/.test(e);if(!i&&n){let e=t;for(let n=t;n>=0;n--){const t=s[n].trim();if("{"===t||"{,"===t){e=n;break}if(t.startsWith("{")&&!t.includes(":")){e=n;break}}l=e,i=!0,a=1;for(let n=e;n<=t;n++){const t=s[n],o=this._countBracketsOutsideStrings(t,"{");a+=n===e?o.open-1-o.close:o.open-o.close}r<o.length&&(c=this.getFeatureKey(o[r]))}else if(i){const n=this._countBracketsOutsideStrings(e,"{");a+=n.open-n.close,a<=0&&(c&&this.featureRanges.set(c,{startLine:l,endLine:t,featureIndex:r}),r++,i=!1,c=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(", ")})`):"Feature"!==r&&s.push(`Invalid type "${r}" at ${n||"root"} (expected: Feature)`))}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 a=o;"properties"===e?a="properties":"geometry"===e||"geometries"===e?a="geometry":"features"===e&&(a="root"),s.push(...this.validateGeoJSON(r,t,a))}return s}_countBracketsOutsideStrings(e,t){const n="{"===t?"}":"]";let o=0,s=0,r=!1,a=!1;for(let i=0;i<e.length;i++){const l=e[i];a?a=!1:"\\"===l&&r?a=!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],a=r.indexOf(n);if(-1!==a){const e=r.substring(a+1),s=this._countBracketsOutsideStrings(e,n);if(o+=s.open-s.close,0===o)return{endLine:t,content:[]}}for(let i=t+1;i<e.length;i++){const t=e[i],r=this._countBracketsOutsideStrings(t,n);if(o+=r.open-r.close,s.push(t),0===o)return{endLine:i,content:s}}return null}_performCollapse(e,t,n,o,s){const r=e[t],a="{"===s?"}":"]";if(this.bracketClosesOnSameLine(r,s))return 0;const i=this._findClosingBracket(e,t,s);if(!i)return 0;const{endLine:l,content:c}=i,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}...${a}${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 a=r[2],i=r[1].length,l=this._findCollapsedData(t,a,i);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})),a=this.expandAllCollapsed(e.value);try{const t=this.formatJSONContent(a);if(t!==a){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(i){}}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],a=`${t}-${e[1].length}`;if(o.has(a)&&(s.set(a,(s.get(a)||0)+1),s.get(a)<=o.get(a))){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 a="";r&&(a+=`:host {\n ${r}\n }\n`),a+=`${n} {\n ${s({...e.DARK_THEME_DEFAULTS,...this.themes.dark})}\n }`,o.textContent=a}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'),"coordinates"in t.geometry||n.push('Geometry must have a "coordinates" property'))):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);export{e as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softwarity/geojson-editor",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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",
@@ -6,6 +6,7 @@ class GeoJsonEditor extends HTMLElement {
6
6
  // Internal state
7
7
  this.collapsedData = new Map(); // nodeKey -> {originalLines: string[], indent: number}
8
8
  this.colorPositions = []; // {line, color}
9
+ this.booleanPositions = []; // {line, value, attributeName}
9
10
  this.nodeTogglePositions = []; // {line, nodeKey, isCollapsed, indent}
10
11
  this.hiddenFeatures = new Set(); // Set of feature keys (hidden from events)
11
12
  this.featureRanges = new Map(); // featureKey -> {startLine, endLine, featureIndex}
@@ -71,11 +72,19 @@ class GeoJsonEditor extends HTMLElement {
71
72
  allNumbers: /\b(-?\d+\.?\d*)\b/g,
72
73
  punctuation: /([{}[\],])/g,
73
74
  // Highlighting detection
74
- colorInLine: /"([\w-]+)"\s*:\s*"(#[0-9a-fA-F]{6})"/g,
75
+ colorInLine: /"([\w-]+)"\s*:\s*"(#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}))"/g,
76
+ booleanInLine: /"([\w-]+)"\s*:\s*(true|false)/g,
75
77
  collapsibleNode: /^(\s*)"(\w+)"\s*:\s*([{\[])/,
76
78
  collapsedMarker: /^(\s*)"(\w+)"\s*:\s*([{\[])\.\.\.([\]\}])/
77
79
  };
78
80
 
81
+ // Icons used in the gutter
82
+ static ICONS = {
83
+ expanded: '⌄', // Chevron down (collapse button when expanded)
84
+ collapsed: '›', // Chevron right (expand button when collapsed)
85
+ visibility: '👁' // Eye icon for visibility toggle
86
+ };
87
+
79
88
  /**
80
89
  * Find collapsed data by line index, nodeKey, and indent
81
90
  * @param {number} lineIndex - Current line index
@@ -359,7 +368,7 @@ class GeoJsonEditor extends HTMLElement {
359
368
  justify-content: center;
360
369
  }
361
370
 
362
- .color-indicator, .collapse-button {
371
+ .color-indicator, .collapse-button, .boolean-checkbox {
363
372
  width: 12px;
364
373
  height: 12px;
365
374
  border-radius: 2px;
@@ -376,21 +385,52 @@ class GeoJsonEditor extends HTMLElement {
376
385
  border-color: #fff;
377
386
  }
378
387
 
379
- .collapse-button {
380
- padding-top: 1px;
381
- background: var(--control-bg, #e8e8e8);
382
- border: 1px solid var(--control-border, #c0c0c0);
388
+ .boolean-checkbox {
389
+ appearance: none;
390
+ -webkit-appearance: none;
391
+ background: transparent;
392
+ border: 1.5px solid var(--control-border, #c0c0c0);
393
+ border-radius: 2px;
394
+ margin: 0;
395
+ position: relative;
396
+ }
397
+ .boolean-checkbox:checked {
398
+ border-color: var(--control-color, #000080);
399
+ }
400
+ .boolean-checkbox:checked::after {
401
+ content: '✔';
383
402
  color: var(--control-color, #000080);
384
- font-size: 8px;
403
+ font-size: 11px;
385
404
  font-weight: bold;
405
+ position: absolute;
406
+ top: -3px;
407
+ right: -1px;
408
+ }
409
+ .boolean-checkbox:hover {
410
+ transform: scale(1.2);
411
+ border-color: var(--control-color, #000080);
412
+ }
413
+
414
+ .collapse-button {
415
+ background: transparent;
416
+ border: none;
417
+ color: var(--json-punct, #a9b7c6);
418
+ font-size: 10px;
386
419
  display: flex;
387
420
  align-items: center;
388
421
  justify-content: center;
389
422
  user-select: none;
423
+ opacity: 0;
424
+ transition: opacity 0.15s;
425
+ }
426
+ .collapse-button.collapsed {
427
+ opacity: 1;
428
+ }
429
+ .gutter:hover .collapse-button {
430
+ opacity: 1;
390
431
  }
391
432
  .collapse-button:hover {
392
- border-color: var(--control-color, #000080);
393
- transform: scale(1.1);
433
+ transform: scale(1.2);
394
434
  }
395
435
 
396
436
  .visibility-button {
@@ -600,7 +640,7 @@ class GeoJsonEditor extends HTMLElement {
600
640
  }, 10);
601
641
  });
602
642
 
603
- // Gutter clicks (color indicators and collapse buttons)
643
+ // Gutter clicks (color indicators, boolean checkboxes, and collapse buttons)
604
644
  const gutterContent = this.shadowRoot.getElementById('gutterContent');
605
645
  gutterContent.addEventListener('click', (e) => {
606
646
  // Check for visibility button (may click on SVG inside button)
@@ -616,6 +656,11 @@ class GeoJsonEditor extends HTMLElement {
616
656
  const color = e.target.dataset.color;
617
657
  const attributeName = e.target.dataset.attributeName;
618
658
  this.showColorPicker(e.target, line, color, attributeName);
659
+ } else if (e.target.classList.contains('boolean-checkbox')) {
660
+ const line = parseInt(e.target.dataset.line);
661
+ const attributeName = e.target.dataset.attributeName;
662
+ const newValue = e.target.checked;
663
+ this.updateBooleanValue(line, newValue, attributeName);
619
664
  } else if (e.target.classList.contains('collapse-button')) {
620
665
  const nodeKey = e.target.dataset.nodeKey;
621
666
  const line = parseInt(e.target.dataset.line);
@@ -766,10 +811,11 @@ class GeoJsonEditor extends HTMLElement {
766
811
  const hiddenRanges = this.getHiddenLineRanges();
767
812
 
768
813
  // Parse and highlight
769
- const { highlighted, colors, toggles } = this.highlightJSON(text, hiddenRanges);
814
+ const { highlighted, colors, booleans, toggles } = this.highlightJSON(text, hiddenRanges);
770
815
 
771
816
  highlightLayer.innerHTML = highlighted;
772
817
  this.colorPositions = colors;
818
+ this.booleanPositions = booleans;
773
819
  this.nodeTogglePositions = toggles;
774
820
 
775
821
  // Update gutter with color indicators
@@ -778,11 +824,12 @@ class GeoJsonEditor extends HTMLElement {
778
824
 
779
825
  highlightJSON(text, hiddenRanges = []) {
780
826
  if (!text.trim()) {
781
- return { highlighted: '', colors: [], toggles: [] };
827
+ return { highlighted: '', colors: [], booleans: [], toggles: [] };
782
828
  }
783
829
 
784
830
  const lines = text.split('\n');
785
831
  const colors = [];
832
+ const booleans = [];
786
833
  const toggles = [];
787
834
  let highlightedLines = [];
788
835
 
@@ -807,6 +854,17 @@ class GeoJsonEditor extends HTMLElement {
807
854
  });
808
855
  }
809
856
 
857
+ // Detect boolean values in properties
858
+ R.booleanInLine.lastIndex = 0; // Reset for global regex
859
+ let booleanMatch;
860
+ while ((booleanMatch = R.booleanInLine.exec(line)) !== null) {
861
+ booleans.push({
862
+ line: lineIndex,
863
+ value: booleanMatch[2] === 'true', // The boolean value
864
+ attributeName: booleanMatch[1] // The attribute name
865
+ });
866
+ }
867
+
810
868
  // Detect collapsible nodes (all nodes are collapsible)
811
869
  const nodeMatch = line.match(R.collapsibleNode);
812
870
  if (nodeMatch) {
@@ -849,30 +907,21 @@ class GeoJsonEditor extends HTMLElement {
849
907
  return {
850
908
  highlighted: highlightedLines.join('\n'),
851
909
  colors,
910
+ booleans,
852
911
  toggles
853
912
  };
854
913
  }
855
914
 
856
915
  // GeoJSON type constants (consolidated)
857
916
  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']
917
+ GEOMETRY_TYPES: ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon'],
861
918
  };
862
919
 
863
920
  // Valid keys per context (null = any key is valid)
864
921
  static VALID_KEYS_BY_CONTEXT = {
865
- Feature: ['type', 'geometry', 'properties', 'id', 'bbox'],
866
- FeatureCollection: ['type', 'features', 'bbox', 'properties'],
867
- Point: ['type', 'coordinates', 'bbox'],
868
- MultiPoint: ['type', 'coordinates', 'bbox'],
869
- LineString: ['type', 'coordinates', 'bbox'],
870
- MultiLineString: ['type', 'coordinates', 'bbox'],
871
- Polygon: ['type', 'coordinates', 'bbox'],
872
- MultiPolygon: ['type', 'coordinates', 'bbox'],
873
- GeometryCollection: ['type', 'geometries', 'bbox'],
922
+ Feature: ['type', 'geometry', 'properties', 'id'],
874
923
  properties: null, // Any key valid in properties
875
- geometry: ['type', 'coordinates', 'geometries', 'bbox'], // Generic geometry context
924
+ geometry: ['type', 'coordinates'], // Generic geometry context
876
925
  };
877
926
 
878
927
  // Keys that change context for their value
@@ -880,7 +929,6 @@ class GeoJsonEditor extends HTMLElement {
880
929
  geometry: 'geometry',
881
930
  properties: 'properties',
882
931
  features: 'Feature', // Array of Features
883
- geometries: 'geometry', // Array of geometries
884
932
  };
885
933
 
886
934
  // Build context map for each line by analyzing JSON structure
@@ -939,7 +987,8 @@ class GeoJsonEditor extends HTMLElement {
939
987
  const typeMatch = line.substring(0, j).match(/"type"\s*:\s*$/);
940
988
  if (typeMatch) {
941
989
  const valueMatch = line.substring(j).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);
942
- if (valueMatch && GeoJsonEditor.GEOJSON.ALL_TYPES.includes(valueMatch[1])) {
990
+ const validTypes = ['Feature', ...GeoJsonEditor.GEOJSON.GEOMETRY_TYPES];
991
+ if (valueMatch && validTypes.includes(valueMatch[1])) {
943
992
  const currentCtx = contextStack[contextStack.length - 1];
944
993
  if (currentCtx) {
945
994
  currentCtx.context = valueMatch[1];
@@ -990,7 +1039,8 @@ class GeoJsonEditor extends HTMLElement {
990
1039
  }
991
1040
 
992
1041
  // All known GeoJSON structural keys (always valid in GeoJSON)
993
- static GEOJSON_STRUCTURAL_KEYS = ['type', 'geometry', 'properties', 'features', 'geometries', 'coordinates', 'bbox', 'id', 'crs'];
1042
+ // GeoJSON structural keys that are always valid (not user properties)
1043
+ static GEOJSON_STRUCTURAL_KEYS = ['type', 'geometry', 'properties', 'coordinates', 'id'];
994
1044
 
995
1045
  highlightSyntax(text, context) {
996
1046
  if (!text.trim()) return '';
@@ -1009,15 +1059,15 @@ class GeoJsonEditor extends HTMLElement {
1009
1059
 
1010
1060
  // Helper to check if a type value is valid in current context
1011
1061
  const isTypeValid = (typeValue) => {
1012
- // Unknown context - don't validate (could be inside misspelled properties, etc.)
1062
+ // Unknown context - don't validate
1013
1063
  if (!context) return true;
1014
1064
  if (context === 'properties') return true; // Any type in properties
1015
1065
  if (context === 'geometry' || GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(context)) {
1016
1066
  return GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(typeValue);
1017
1067
  }
1018
- // Only validate as GeoJSON type in known Feature/FeatureCollection context
1019
- if (context === 'Feature' || context === 'FeatureCollection') {
1020
- return GeoJsonEditor.GEOJSON.ALL_TYPES.includes(typeValue);
1068
+ // In Feature context: accept Feature or geometry types
1069
+ if (context === 'Feature') {
1070
+ return typeValue === 'Feature' || GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(typeValue);
1021
1071
  }
1022
1072
  return true; // Unknown context - accept any type
1023
1073
  };
@@ -1159,13 +1209,13 @@ class GeoJsonEditor extends HTMLElement {
1159
1209
  // Clear gutter
1160
1210
  gutterContent.textContent = '';
1161
1211
 
1162
- // Create a map of line -> elements (color, collapse button, visibility button)
1212
+ // Create a map of line -> elements (color, boolean, collapse button, visibility button)
1163
1213
  const lineElements = new Map();
1164
1214
 
1165
1215
  // Helper to ensure line entry exists
1166
1216
  const ensureLine = (line) => {
1167
1217
  if (!lineElements.has(line)) {
1168
- lineElements.set(line, { colors: [], buttons: [], visibilityButtons: [] });
1218
+ lineElements.set(line, { colors: [], booleans: [], buttons: [], visibilityButtons: [] });
1169
1219
  }
1170
1220
  return lineElements.get(line);
1171
1221
  };
@@ -1175,6 +1225,11 @@ class GeoJsonEditor extends HTMLElement {
1175
1225
  ensureLine(line).colors.push({ color, attributeName });
1176
1226
  });
1177
1227
 
1228
+ // Add boolean checkboxes
1229
+ this.booleanPositions.forEach(({ line, value, attributeName }) => {
1230
+ ensureLine(line).booleans.push({ value, attributeName });
1231
+ });
1232
+
1178
1233
  // Add collapse buttons
1179
1234
  this.nodeTogglePositions.forEach(({ line, nodeKey, isCollapsed }) => {
1180
1235
  ensureLine(line).buttons.push({ nodeKey, isCollapsed });
@@ -1198,7 +1253,7 @@ class GeoJsonEditor extends HTMLElement {
1198
1253
  elements.visibilityButtons.forEach(({ featureKey, isHidden }) => {
1199
1254
  const button = document.createElement('button');
1200
1255
  button.className = 'visibility-button' + (isHidden ? ' hidden' : '');
1201
- button.textContent = '👁';
1256
+ button.textContent = GeoJsonEditor.ICONS.visibility;
1202
1257
  button.dataset.featureKey = featureKey;
1203
1258
  button.title = isHidden ? 'Show feature in events' : 'Hide feature from events';
1204
1259
  gutterLine.appendChild(button);
@@ -1216,11 +1271,23 @@ class GeoJsonEditor extends HTMLElement {
1216
1271
  gutterLine.appendChild(indicator);
1217
1272
  });
1218
1273
 
1274
+ // Add boolean checkboxes
1275
+ elements.booleans.forEach(({ value, attributeName }) => {
1276
+ const checkbox = document.createElement('input');
1277
+ checkbox.type = 'checkbox';
1278
+ checkbox.className = 'boolean-checkbox';
1279
+ checkbox.checked = value;
1280
+ checkbox.dataset.line = line;
1281
+ checkbox.dataset.attributeName = attributeName;
1282
+ checkbox.title = `${attributeName}: ${value}`;
1283
+ gutterLine.appendChild(checkbox);
1284
+ });
1285
+
1219
1286
  // Add collapse buttons
1220
1287
  elements.buttons.forEach(({ nodeKey, isCollapsed }) => {
1221
1288
  const button = document.createElement('div');
1222
- button.className = 'collapse-button';
1223
- button.textContent = isCollapsed ? '+' : '-';
1289
+ button.className = isCollapsed ? 'collapse-button collapsed' : 'collapse-button';
1290
+ button.textContent = isCollapsed ? GeoJsonEditor.ICONS.collapsed : GeoJsonEditor.ICONS.expanded;
1224
1291
  button.dataset.line = line;
1225
1292
  button.dataset.nodeKey = nodeKey;
1226
1293
  button.title = isCollapsed ? 'Expand' : 'Collapse';
@@ -1302,8 +1369,8 @@ class GeoJsonEditor extends HTMLElement {
1302
1369
  const textarea = this.shadowRoot.getElementById('textarea');
1303
1370
  const lines = textarea.value.split('\n');
1304
1371
 
1305
- // Replace color value on the specified line for the specific attribute
1306
- const regex = new RegExp(`"${attributeName}"\\s*:\\s*"#[0-9a-fA-F]{6}"`);
1372
+ // Replace color value on the specified line for the specific attribute (supports #rgb and #rrggbb)
1373
+ const regex = new RegExp(`"${attributeName}"\\s*:\\s*"#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})"`);
1307
1374
  lines[line] = lines[line].replace(regex, `"${attributeName}": "${newColor}"`);
1308
1375
 
1309
1376
  textarea.value = lines.join('\n');
@@ -1311,6 +1378,19 @@ class GeoJsonEditor extends HTMLElement {
1311
1378
  this.emitChange();
1312
1379
  }
1313
1380
 
1381
+ updateBooleanValue(line, newValue, attributeName) {
1382
+ const textarea = this.shadowRoot.getElementById('textarea');
1383
+ const lines = textarea.value.split('\n');
1384
+
1385
+ // Replace boolean value on the specified line for the specific attribute
1386
+ const regex = new RegExp(`"${attributeName}"\\s*:\\s*(true|false)`);
1387
+ lines[line] = lines[line].replace(regex, `"${attributeName}": ${newValue}`);
1388
+
1389
+ textarea.value = lines.join('\n');
1390
+ this.updateHighlight();
1391
+ this.emitChange();
1392
+ }
1393
+
1314
1394
  handleKeydownInCollapsedArea(e) {
1315
1395
  // Allow navigation keys
1316
1396
  const navigationKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Tab'];
@@ -1443,8 +1523,11 @@ class GeoJsonEditor extends HTMLElement {
1443
1523
  // Filter out hidden features before emitting
1444
1524
  parsed = this.filterHiddenFeatures(parsed);
1445
1525
 
1446
- // Validate GeoJSON types
1447
- const validationErrors = this.validateGeoJSON(parsed);
1526
+ // Validate GeoJSON types (validate only features, not the wrapper)
1527
+ let validationErrors = [];
1528
+ parsed.features.forEach((feature, index) => {
1529
+ validationErrors.push(...this.validateGeoJSON(feature, `features[${index}]`, 'root'));
1530
+ });
1448
1531
 
1449
1532
  if (validationErrors.length > 0) {
1450
1533
  // Emit error event for GeoJSON validation errors
@@ -1484,23 +1567,12 @@ class GeoJsonEditor extends HTMLElement {
1484
1567
  filterHiddenFeatures(parsed) {
1485
1568
  if (!parsed || this.hiddenFeatures.size === 0) return parsed;
1486
1569
 
1487
- if (parsed.type === 'FeatureCollection' && Array.isArray(parsed.features)) {
1488
- // Filter features array
1489
- const visibleFeatures = parsed.features.filter(feature => {
1490
- const key = this.getFeatureKey(feature);
1491
- return !this.hiddenFeatures.has(key);
1492
- });
1493
- return { ...parsed, features: visibleFeatures };
1494
- } else if (parsed.type === 'Feature') {
1495
- // Single feature - check if hidden
1496
- const key = this.getFeatureKey(parsed);
1497
- if (this.hiddenFeatures.has(key)) {
1498
- // Return empty FeatureCollection when single feature is hidden
1499
- return { type: 'FeatureCollection', features: [] };
1500
- }
1501
- }
1502
-
1503
- return parsed;
1570
+ // parsed is always a FeatureCollection (from wrapper)
1571
+ const visibleFeatures = parsed.features.filter((feature) => {
1572
+ const key = this.getFeatureKey(feature);
1573
+ return !this.hiddenFeatures.has(key);
1574
+ });
1575
+ return { ...parsed, features: visibleFeatures };
1504
1576
  }
1505
1577
 
1506
1578
  // ========== Feature Visibility Management ==========
@@ -1515,9 +1587,9 @@ class GeoJsonEditor extends HTMLElement {
1515
1587
  // 2. Use properties.id if present
1516
1588
  if (feature.properties?.id !== undefined) return `prop:${feature.properties.id}`;
1517
1589
 
1518
- // 3. Fallback: hash based on geometry type + first coordinates
1590
+ // 3. Fallback: hash based on geometry type + ALL coordinates
1519
1591
  const geomType = feature.geometry?.type || 'null';
1520
- const coords = JSON.stringify(feature.geometry?.coordinates || []).slice(0, 100);
1592
+ const coords = JSON.stringify(feature.geometry?.coordinates || []);
1521
1593
  return `hash:${geomType}:${this.simpleHash(coords)}`;
1522
1594
  }
1523
1595
 
@@ -1562,12 +1634,8 @@ class GeoJsonEditor extends HTMLElement {
1562
1634
  const fullValue = prefix + expandedText + suffix;
1563
1635
  const parsed = JSON.parse(fullValue);
1564
1636
 
1565
- let features = [];
1566
- if (parsed.type === 'FeatureCollection' && Array.isArray(parsed.features)) {
1567
- features = parsed.features;
1568
- } else if (parsed.type === 'Feature') {
1569
- features = [parsed];
1570
- }
1637
+ // parsed is always a FeatureCollection (from wrapper)
1638
+ const features = parsed.features;
1571
1639
 
1572
1640
  // Now find each feature's line range in the text
1573
1641
  const lines = text.split('\n');
@@ -1585,10 +1653,18 @@ class GeoJsonEditor extends HTMLElement {
1585
1653
  const isFeatureTypeLine = /"type"\s*:\s*"Feature"/.test(line);
1586
1654
  if (!inFeature && isFeatureTypeLine) {
1587
1655
  // Find the opening brace for this Feature
1588
- // Look backwards for the opening brace
1656
+ // Look backwards for a line that starts with just '{' (the Feature's opening brace)
1657
+ // Not a line like '"geometry": {' which contains other content before the brace
1589
1658
  let startLine = i;
1590
1659
  for (let j = i; j >= 0; j--) {
1591
- if (lines[j].includes('{')) {
1660
+ const trimmed = lines[j].trim();
1661
+ // Line is just '{' or '{' followed by nothing significant (opening brace only)
1662
+ if (trimmed === '{' || trimmed === '{,') {
1663
+ startLine = j;
1664
+ break;
1665
+ }
1666
+ // Also handle case where Feature starts on same line: { "type": "Feature"
1667
+ if (trimmed.startsWith('{') && !trimmed.includes(':')) {
1592
1668
  startLine = j;
1593
1669
  break;
1594
1670
  }
@@ -1671,9 +1747,9 @@ class GeoJsonEditor extends HTMLElement {
1671
1747
  errors.push(`Invalid geometry type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.join(', ')})`);
1672
1748
  }
1673
1749
  } else {
1674
- // At root or in features: must be Feature or FeatureCollection
1675
- if (!GeoJsonEditor.GEOJSON.FEATURE_TYPES.includes(typeValue)) {
1676
- errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON.FEATURE_TYPES.join(', ')})`);
1750
+ // At root or in features: must be Feature
1751
+ if (typeValue !== 'Feature') {
1752
+ errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected: Feature)`);
1677
1753
  }
1678
1754
  }
1679
1755
  }
@@ -2168,15 +2244,10 @@ class GeoJsonEditor extends HTMLElement {
2168
2244
  errors.push(`Invalid geometry type "${feature.geometry.type}" (expected: ${GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.join(', ')})`);
2169
2245
  }
2170
2246
 
2171
- // Check geometry has coordinates (except GeometryCollection)
2172
- if (feature.geometry.type !== 'GeometryCollection' && !('coordinates' in feature.geometry)) {
2247
+ // Check geometry has coordinates
2248
+ if (!('coordinates' in feature.geometry)) {
2173
2249
  errors.push('Geometry must have a "coordinates" property');
2174
2250
  }
2175
-
2176
- // GeometryCollection must have geometries array
2177
- if (feature.geometry.type === 'GeometryCollection' && !Array.isArray(feature.geometry.geometries)) {
2178
- errors.push('GeometryCollection must have a "geometries" array');
2179
- }
2180
2251
  }
2181
2252
  }
2182
2253
 
@@ -2315,3 +2386,5 @@ class GeoJsonEditor extends HTMLElement {
2315
2386
 
2316
2387
  // Register the custom element
2317
2388
  customElements.define('geojson-editor', GeoJsonEditor);
2389
+
2390
+ export default GeoJsonEditor;