@softwarity/geojson-editor 1.0.7 → 1.0.8
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 +3 -1
- package/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.js +124 -65
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)
|
package/dist/geojson-editor.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @license MIT
|
|
3
3
|
* @name @softwarity/geojson-editor
|
|
4
|
-
* @version 1.0.
|
|
4
|
+
* @version 1.0.8
|
|
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,"&").replace(n.lessThan,"<").replace(n.greaterThan,">")}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("placeholderLayer");e&&t&&(t.style.display=e.value?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updateValue(e){const t=this.shadowRoot.getElementById("textarea");if(t&&t.value!==e){if(t.value=e||"",e)try{const n="["+e+"]",o=JSON.parse(n),s=JSON.stringify(o,null,2).split("\n");s.length>2?t.value=s.slice(1,-1).join("\n"):t.value=""}catch(n){}this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateHighlight(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");if(!e||!t)return;const n=e.value;this.updateFeatureRanges();const o=this.getHiddenLineRanges(),{highlighted:s,colors:r,toggles:i}=this.highlightJSON(n,o);t.innerHTML=s,this.colorPositions=r,this.nodeTogglePositions=i,this.updateGutter()}highlightJSON(t,n=[]){if(!t.trim())return{highlighted:"",colors:[],toggles:[]};const o=t.split("\n"),s=[],r=[];let i=[];const a=this.buildContextMap(t);return o.forEach((t,o)=>{const l=e.REGEX;let c;for(l.colorInLine.lastIndex=0;null!==(c=l.colorInLine.exec(t));)s.push({line:o,color:c[2],attributeName:c[1]});const d=t.match(l.collapsibleNode);if(d){const e=d[2];t.includes("{...}")||t.includes("[...]")?r.push({line:o,nodeKey:e,isCollapsed:!0}):this.bracketClosesOnSameLine(t,d[3])||r.push({line:o,nodeKey:e,isCollapsed:!1})}const h=a.get(o);let u=this.highlightSyntax(t,h);(e=>n.some(t=>e>=t.startLine&&e<=t.endLine))(o)&&(u=`<span class="line-hidden">${u}</span>`),i.push(u)}),{highlighted:i.join("\n"),colors:s,toggles:r}}static GEOJSON={FEATURE_TYPES:["Feature","FeatureCollection"],GEOMETRY_TYPES:["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"],ALL_TYPES:["Feature","FeatureCollection","Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon","GeometryCollection"]};static VALID_KEYS_BY_CONTEXT={Feature:["type","geometry","properties","id","bbox"],FeatureCollection:["type","features","bbox","properties"],Point:["type","coordinates","bbox"],MultiPoint:["type","coordinates","bbox"],LineString:["type","coordinates","bbox"],MultiLineString:["type","coordinates","bbox"],Polygon:["type","coordinates","bbox"],MultiPolygon:["type","coordinates","bbox"],GeometryCollection:["type","geometries","bbox"],properties:null,geometry:["type","coordinates","geometries","bbox"]};static CONTEXT_CHANGING_KEYS={geometry:"geometry",properties:"properties",features:"Feature",geometries:"geometry"};buildContextMap(t){const n=t.split("\n"),o=/* @__PURE__ */new Map,s=[];let r=null;const i="Feature";for(let a=0;a<n.length;a++){const t=n[a],l=s.length>0?s[s.length-1]?.context:i;o.set(a,l);let c=!1,d=!1;for(let n=0;n<t.length;n++){const o=t[n];if(d)d=!1;else if("\\"===o&&c)d=!0;else if('"'!==o){if(!c){if("{"===o||"["===o){let e;if(r)e=r,r=null;else if(0===s.length)e=i;else{const t=s[s.length-1];e=t&&t.isArray?t.context:null}s.push({context:e,isArray:"["===o})}"}"!==o&&"]"!==o||s.length>0&&s.pop()}}else{if(!c){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"\s*:/);if(o){const t=o[1];e.CONTEXT_CHANGING_KEYS[t]&&(r=e.CONTEXT_CHANGING_KEYS[t]),n+=o[0].length-1;continue}if(s.length>0&&t.substring(0,n).match(/"type"\s*:\s*$/)){const o=t.substring(n).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);if(o&&e.GEOJSON.ALL_TYPES.includes(o[1])){const e=s[s.length-1];e&&(e.context=o[1])}n+=o?o[0].length-1:0;continue}}c=!c}}}return o}static GEOJSON_STRUCTURAL_KEYS=["type","geometry","properties","features","geometries","coordinates","bbox","id","crs"];highlightSyntax(t,n){if(!t.trim())return"";const o=n?e.VALID_KEYS_BY_CONTEXT[n]:null,s=e.REGEX;return t.replace(s.ampersand,"&").replace(s.lessThan,"<").replace(s.greaterThan,">").replace(s.jsonKey,(t,s)=>"properties"===n?`<span class="json-key">"${s}"</span>:`:e.GEOJSON_STRUCTURAL_KEYS.includes(s)?`<span class="geojson-key">"${s}"</span>:`:(t=>!!e.GEOJSON_STRUCTURAL_KEYS.includes(t)||!n||null==o||o.includes(t))(s)?`<span class="json-key">"${s}"</span>:`:`<span class="json-key-invalid">"${s}"</span>:`).replace(s.typeValue,(t,o)=>(t=>!n||"properties"===n||("geometry"===n||e.GEOJSON.GEOMETRY_TYPES.includes(n)?e.GEOJSON.GEOMETRY_TYPES.includes(t):"Feature"!==n&&"FeatureCollection"!==n||e.GEOJSON.ALL_TYPES.includes(t)))(o)?`<span class="geojson-key">"type"</span>: <span class="geojson-type">"${o}"</span>`:`<span class="geojson-key">"type"</span>: <span class="geojson-type-invalid">"${o}"</span>`).replace(s.stringValue,(e,t)=>e.includes("<span")?e:`: <span class="json-string">"${t}"</span>`).replace(s.numberAfterColon,': <span class="json-number">$1</span>').replace(s.boolean,': <span class="json-boolean">$1</span>').replace(s.nullValue,': <span class="json-null">$1</span>').replace(s.allNumbers,'<span class="json-number">$1</span>').replace(s.punctuation,'<span class="json-punctuation">$1</span>')}toggleCollapse(e,t){const n=this.shadowRoot.getElementById("textarea"),o=n.value.split("\n"),s=o[t];if(s.includes("{...}")||s.includes("[...]")){const n=s.match(/^(\s*)/)[1].length,r=this._findCollapsedData(t,e,n);if(!r)return;const{key:i,data:a}=r,{originalLine:l,content:c}=a;o[t]=l,o.splice(t+1,0,...c),this.collapsedData.delete(i)}else{const n=s.match(/^(\s*)"([^"]+)"\s*:\s*([{\[])/);if(!n)return;const r=n[1],i=n[3];if(0===this._performCollapse(o,t,e,r,i))return}n.value=o.join("\n"),this.updateHighlight()}applyAutoCollapsed(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value)return;const t=e.value.split("\n");for(let n=t.length-1;n>=0;n--){const e=t[n].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const o=e[2];if("coordinates"===o){const s=e[1],r=e[3];this._performCollapse(t,n,o,s,r)}}}e.value=t.join("\n"),this.updateHighlight()}updateGutter(){const e=this.shadowRoot.getElementById("gutterContent"),t=this.shadowRoot.getElementById("textarea");if(!t)return;if(null===this._cachedLineHeight){const e=getComputedStyle(t);this._cachedLineHeight=parseFloat(e.lineHeight),this._cachedPaddingTop=parseFloat(e.paddingTop)}const n=this._cachedLineHeight,o=this._cachedPaddingTop;e.textContent="";const s=/* @__PURE__ */new Map,r=e=>(s.has(e)||s.set(e,{colors:[],buttons:[],visibilityButtons:[]}),s.get(e));this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{r(e).colors.push({color:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{r(e).buttons.push({nodeKey:t,isCollapsed:n})});for(const[a,l]of this.featureRanges){const e=this.hiddenFeatures.has(a);r(l.startLine).visibilityButtons.push({featureKey:a,isHidden:e})}const i=document.createDocumentFragment();s.forEach((e,t)=>{const s=document.createElement("div");s.className="gutter-line",s.style.top=`${o+t*n}px`,e.visibilityButtons.forEach(({featureKey:e,isHidden:t})=>{const n=document.createElement("button");n.className="visibility-button"+(t?" hidden":""),n.textContent="👁",n.dataset.featureKey=e,n.title=t?"Show feature in events":"Hide feature from events",s.appendChild(n)}),e.colors.forEach(({color:e,attributeName:n})=>{const o=document.createElement("div");o.className="color-indicator",o.style.backgroundColor=e,o.dataset.line=t,o.dataset.color=e,o.dataset.attributeName=n,o.title=`${n}: ${e}`,s.appendChild(o)}),e.buttons.forEach(({nodeKey:e,isCollapsed:n})=>{const o=document.createElement("div");o.className="collapse-button",o.textContent=n?"+":"-",o.dataset.line=t,o.dataset.nodeKey=e,o.title=n?"Expand":"Collapse",s.appendChild(o)}),i.appendChild(s)}),e.appendChild(i)}showColorPicker(e,t,n,o){const s=document.querySelector(".geojson-color-picker-input");s&&(s._closeListener&&document.removeEventListener("click",s._closeListener,!0),s.remove());const r=document.createElement("input");r.type="color",r.value=n,r.className="geojson-color-picker-input";const i=e.getBoundingClientRect();r.style.position="fixed",r.style.left=`${i.left}px`,r.style.top=`${i.top}px`,r.style.width="12px",r.style.height="12px",r.style.opacity="0.01",r.style.border="none",r.style.padding="0",r.style.zIndex="9999",r.addEventListener("input",e=>{this.updateColorValue(t,e.target.value,o)}),r.addEventListener("change",e=>{this.updateColorValue(t,e.target.value,o)});const a=e=>{e.target===r||r.contains(e.target)||(document.removeEventListener("click",a,!0),r.remove())};r._closeListener=a,document.body.appendChild(r),setTimeout(()=>{document.addEventListener("click",a,!0)},100),r.focus(),r.click()}updateColorValue(e,t,n){const o=this.shadowRoot.getElementById("textarea"),s=o.value.split("\n"),r=new RegExp(`"${n}"\\s*:\\s*"#[0-9a-fA-F]{6}"`);s[e]=s[e].replace(r,`"${n}": "${t}"`),o.value=s.join("\n"),this.updateHighlight(),this.emitChange()}handleKeydownInCollapsedArea(e){if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight","Home","End","PageUp","PageDown","Tab"].includes(e.key))return;if(e.ctrlKey||e.metaKey)return;const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.value.substring(0,n).split("\n").length-1,s=t.value.split("\n")[o];s&&(s.includes("{...}")||s.includes("[...]"))&&e.preventDefault()}handleCopyWithCollapsedContent(e){const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n===o)return;const s=t.value.substring(n,o);if(!s.includes("{...}")&&!s.includes("[...]"))return;let r;r=0===n&&o===t.value.length?this.expandAllCollapsed(s):this.expandCollapsedMarkersInText(s,n),e.preventDefault(),e.clipboardData.setData("text/plain",r)}expandCollapsedMarkersInText(t,n){const o=this.shadowRoot.getElementById("textarea").value.substring(0,n).split("\n").length-1,s=e.REGEX,r=t.split("\n"),i=[];return r.forEach((e,t)=>{const n=o+t;if(e.includes("{...}")||e.includes("[...]")){const t=e.match(s.collapsedMarker);if(t){const e=t[2],o=t[1].length,s=this._findCollapsedData(n,e,o);if(s)return i.push(s.data.originalLine),void i.push(...s.data.content);for(const[,t]of this.collapsedData.entries())if(t.nodeKey===e)return i.push(t.originalLine),void i.push(...t.content)}i.push(e)}else i.push(e)}),i.join("\n")}handleCutWithCollapsedContent(e){this.handleCopyWithCollapsedContent(e);const t=this.shadowRoot.getElementById("textarea"),n=t.selectionStart,o=t.selectionEnd;if(n!==o){const e=t.value;t.value=e.substring(0,n)+e.substring(o),t.selectionStart=t.selectionEnd=n,this.updateHighlight(),this.updatePlaceholderVisibility(),this.emitChange()}}emitChange(){const e=this.shadowRoot.getElementById("textarea"),t=this.expandAllCollapsed(e.value),n=this.prefix+t+this.suffix;try{let e=JSON.parse(n);e=this.filterHiddenFeatures(e);const o=this.validateGeoJSON(e);o.length>0?this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:/* @__PURE__ */(new Date).toISOString(),error:`GeoJSON validation: ${o.join("; ")}`,errors:o,content:t},bubbles:!0,composed:!0})):this.dispatchEvent(new CustomEvent("change",{detail:e,bubbles:!0,composed:!0}))}catch(o){this.dispatchEvent(new CustomEvent("error",{detail:{timestamp:/* @__PURE__ */(new Date).toISOString(),error:o.message,content:t},bubbles:!0,composed:!0}))}}filterHiddenFeatures(e){if(!e||0===this.hiddenFeatures.size)return e;if("FeatureCollection"===e.type&&Array.isArray(e.features)){const t=e.features.filter(e=>{const t=this.getFeatureKey(e);return!this.hiddenFeatures.has(t)});return{...e,features:t}}if("Feature"===e.type){const t=this.getFeatureKey(e);if(this.hiddenFeatures.has(t))return{type:"FeatureCollection",features:[]}}return e}getFeatureKey(e){if(!e||"object"!=typeof e)return null;if(void 0!==e.id)return`id:${e.id}`;if(void 0!==e.properties?.id)return`prop:${e.properties.id}`;const t=e.geometry?.type||"null",n=JSON.stringify(e.geometry?.coordinates||[]).slice(0,100);return`hash:${t}:${this.simpleHash(n)}`}simpleHash(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t&=t;return t.toString(36)}toggleFeatureVisibility(e){this.hiddenFeatures.has(e)?this.hiddenFeatures.delete(e):this.hiddenFeatures.add(e),this.updateHighlight(),this.updateGutter(),this.emitChange()}updateFeatureRanges(){const e=this.shadowRoot.getElementById("textarea");if(!e)return;const t=e.value;this.featureRanges.clear();try{const e=this.expandAllCollapsed(t),n=this.prefix+e+this.suffix,o=JSON.parse(n);let s=[];"FeatureCollection"===o.type&&Array.isArray(o.features)?s=o.features:"Feature"===o.type&&(s=[o]);const r=t.split("\n");let i=0,a=0,l=!1,c=-1,d=null;for(let t=0;t<r.length;t++){const e=r[t],n=/"type"\s*:\s*"Feature"/.test(e);if(!l&&n){let e=t;for(let n=t;n>=0;n--)if(r[n].includes("{")){e=n;break}c=e,l=!0,a=1;for(let n=e;n<=t;n++){const t=r[n],o=this._countBracketsOutsideStrings(t,"{");a+=n===e?o.open-1-o.close:o.open-o.close}i<s.length&&(d=this.getFeatureKey(s[i]))}else if(l){const n=this._countBracketsOutsideStrings(e,"{");a+=n.open-n.close,a<=0&&(d&&this.featureRanges.set(d,{startLine:c,endLine:t,featureIndex:i}),i++,l=!1,d=null)}}}catch(n){}}getHiddenLineRanges(){const e=[];for(const[t,n]of this.featureRanges)this.hiddenFeatures.has(t)&&e.push(n);return e}validateGeoJSON(t,n="",o="root"){const s=[];if(!t||"object"!=typeof t)return s;if("properties"!==o&&void 0!==t.type){const r=t.type;"string"==typeof r&&("geometry"===o?e.GEOJSON.GEOMETRY_TYPES.includes(r)||s.push(`Invalid geometry type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.GEOMETRY_TYPES.join(", ")})`):e.GEOJSON.FEATURE_TYPES.includes(r)||s.push(`Invalid type "${r}" at ${n||"root"} (expected: ${e.GEOJSON.FEATURE_TYPES.join(", ")})`))}if(Array.isArray(t))t.forEach((e,t)=>{s.push(...this.validateGeoJSON(e,`${n}[${t}]`,o))});else for(const[e,r]of Object.entries(t))if("object"==typeof r&&null!==r){const t=n?`${n}.${e}`:e;let i=o;"properties"===e?i="properties":"geometry"===e||"geometries"===e?i="geometry":"features"===e&&(i="root"),s.push(...this.validateGeoJSON(r,t,i))}return s}_countBracketsOutsideStrings(e,t){const n="{"===t?"}":"]";let o=0,s=0,r=!1,i=!1;for(let a=0;a<e.length;a++){const l=e[a];i?i=!1:"\\"===l&&r?i=!0:'"'!==l?r||(l===t&&o++,l===n&&s++):r=!r}return{open:o,close:s}}bracketClosesOnSameLine(e,t){const n=e.indexOf(t);if(-1===n)return!1;const o=e.substring(n+1),s=this._countBracketsOutsideStrings(o,t);return s.close>s.open}_findClosingBracket(e,t,n){let o=1;const s=[],r=e[t],i=r.indexOf(n);if(-1!==i){const e=r.substring(i+1),s=this._countBracketsOutsideStrings(e,n);if(o+=s.open-s.close,0===o)return{endLine:t,content:[]}}for(let a=t+1;a<e.length;a++){const t=e[a],r=this._countBracketsOutsideStrings(t,n);if(o+=r.open-r.close,s.push(t),0===o)return{endLine:a,content:s}}return null}_performCollapse(e,t,n,o,s){const r=e[t],i="{"===s?"}":"]";if(this.bracketClosesOnSameLine(r,s))return 0;const a=this._findClosingBracket(e,t,s);if(!a)return 0;const{endLine:l,content:c}=a,d=`${t}-${n}`;this.collapsedData.set(d,{originalLine:r,content:c,indent:o.length,nodeKey:n});const h=r.substring(0,r.indexOf(s)),u=e[l]&&e[l].trim().endsWith(",");e[t]=`${h}${s}...${i}${u?",":""}`;const p=l-t;return e.splice(t+1,p),p}expandAllCollapsed(t){const n=e.REGEX;for(;t.includes("{...}")||t.includes("[...]");){const e=t.split("\n");let o=!1;for(let t=0;t<e.length;t++){const s=e[t];if(!s.includes("{...}")&&!s.includes("[...]"))continue;const r=s.match(n.collapsedMarker);if(!r)continue;const i=r[2],a=r[1].length,l=this._findCollapsedData(t,i,a);if(l){const{data:{originalLine:n,content:s}}=l;e[t]=n,e.splice(t+1,0,...s),o=!0;break}}if(!o)break;t=e.join("\n")}return t}formatJSONContent(e){const t="["+e+"]";let n=JSON.parse(t);Array.isArray(n)&&(n=n.map(e=>this._applyDefaultPropertiesToFeature(e)));const o=JSON.stringify(n,null,2).split("\n");return o.length>2?o.slice(1,-1).join("\n"):""}autoFormatContentWithCursor(){const e=this.shadowRoot.getElementById("textarea"),t=e.selectionStart,n=e.value.substring(0,t).split("\n"),o=n.length-1,s=n[n.length-1].length,r=Array.from(this.collapsedData.values()).map(e=>({nodeKey:e.nodeKey,indent:e.indent})),i=this.expandAllCollapsed(e.value);try{const t=this.formatJSONContent(i);if(t!==i){this.collapsedData.clear(),e.value=t,r.length>0&&this.reapplyCollapsed(r);const n=e.value.split("\n");if(o<n.length){const t=Math.min(s,n[o].length);let r=0;for(let e=0;e<o;e++)r+=n[e].length+1;r+=t,e.setSelectionRange(r,r)}}}catch(a){}}reapplyCollapsed(e){const t=this.shadowRoot.getElementById("textarea"),n=t.value.split("\n"),o=/* @__PURE__ */new Map;e.forEach(({nodeKey:e,indent:t})=>{const n=`${e}-${t}`;o.set(n,(o.get(n)||0)+1)});const s=/* @__PURE__ */new Map;for(let r=n.length-1;r>=0;r--){const e=n[r].match(/^(\s*)"(\w+)"\s*:\s*([{\[])/);if(e){const t=e[2],i=`${t}-${e[1].length}`;if(o.has(i)&&(s.set(i,(s.get(i)||0)+1),s.get(i)<=o.get(i))){const o=e[1],s=e[3];this._performCollapse(n,r,t,o,s)}}}t.value=n.join("\n")}parseSelectorToHostRule(e){return e&&""!==e?e.startsWith(".")&&!e.includes(" ")?`:host(${e})`:`:host-context(${e})`:':host([data-color-scheme="dark"])'}updateThemeCSS(){const t=this.getAttribute("dark-selector")||".dark",n=this.parseSelectorToHostRule(t);let o=this.shadowRoot.getElementById("theme-styles");o||(o=document.createElement("style"),o.id="theme-styles",this.shadowRoot.insertBefore(o,this.shadowRoot.firstChild));const s=t=>Object.entries(t||{}).map(([t,n])=>`--${e._toKebabCase(t)}: ${n};`).join("\n "),r=s(this.themes.light);let i="";r&&(i+=`:host {\n ${r}\n }\n`),i+=`${n} {\n ${s({...e.DARK_THEME_DEFAULTS,...this.themes.dark})}\n }`,o.textContent=i}setTheme(e){e.dark&&(this.themes.dark={...this.themes.dark,...e.dark}),e.light&&(this.themes.light={...this.themes.light,...e.light}),this.updateThemeCSS()}resetTheme(){this.themes={dark:{},light:{}},this.updateThemeCSS()}_normalizeIndex(e,t,n=!1){let o=e;return o<0&&(o=t+o),n?Math.max(0,Math.min(o,t)):o<0||o>=t?-1:o}_parseFeatures(){const e=this.shadowRoot.getElementById("textarea");if(!e||!e.value.trim())return[];try{const t="["+this.expandAllCollapsed(e.value)+"]";return JSON.parse(t)}catch(t){return[]}}_setFeatures(e){const t=this.shadowRoot.getElementById("textarea");if(t){if(this.collapsedData.clear(),this.hiddenFeatures.clear(),e&&0!==e.length){const n=e.map(e=>JSON.stringify(e,null,2)).join(",\n");t.value=n}else t.value="";this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}_validateFeature(t){const n=[];return t&&"object"==typeof t?Array.isArray(t)?(n.push("Feature cannot be an array"),n):("type"in t?"Feature"!==t.type&&n.push(`Feature type must be "Feature", got "${t.type}"`):n.push('Feature must have a "type" property'),"geometry"in t?null!==t.geometry&&("object"!=typeof t.geometry||Array.isArray(t.geometry)?n.push("Feature geometry must be an object or null"):("type"in t.geometry?e.GEOJSON.GEOMETRY_TYPES.includes(t.geometry.type)||n.push(`Invalid geometry type "${t.geometry.type}" (expected: ${e.GEOJSON.GEOMETRY_TYPES.join(", ")})`):n.push('Geometry must have a "type" property'),"GeometryCollection"===t.geometry.type||"coordinates"in t.geometry||n.push('Geometry must have a "coordinates" property'),"GeometryCollection"!==t.geometry.type||Array.isArray(t.geometry.geometries)||n.push('GeometryCollection must have a "geometries" array'))):n.push('Feature must have a "geometry" property (can be null)'),"properties"in t?null===t.properties||"object"==typeof t.properties&&!Array.isArray(t.properties)||n.push("Feature properties must be an object or null"):n.push('Feature must have a "properties" property (can be null)'),n):(n.push("Feature must be an object"),n)}set(e){if(!Array.isArray(e))throw new Error("set() expects an array of features");const t=[];if(e.forEach((e,n)=>{const o=this._validateFeature(e);o.length>0&&t.push(`Feature[${n}]: ${o.join(", ")}`)}),t.length>0)throw new Error(`Invalid features: ${t.join("; ")}`);const n=e.map(e=>this._applyDefaultPropertiesToFeature(e));this._setFeatures(n)}add(e){const t=this._validateFeature(e);if(t.length>0)throw new Error(`Invalid feature: ${t.join(", ")}`);const n=this._parseFeatures();n.push(this._applyDefaultPropertiesToFeature(e)),this._setFeatures(n)}insertAt(e,t){const n=this._validateFeature(e);if(n.length>0)throw new Error(`Invalid feature: ${n.join(", ")}`);const o=this._parseFeatures(),s=this._normalizeIndex(t,o.length,!0);o.splice(s,0,this._applyDefaultPropertiesToFeature(e)),this._setFeatures(o)}removeAt(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);if(-1===n)return;const o=t.splice(n,1)[0];return this._setFeatures(t),o}removeAll(){const e=this._parseFeatures();return this._setFeatures([]),e}get(e){const t=this._parseFeatures();if(0===t.length)return;const n=this._normalizeIndex(e,t.length);return-1!==n?t[n]:void 0}getAll(){return this._parseFeatures()}emit(){this.emitChange()}}customElements.define("geojson-editor",e);
|
|
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]{6})"/g,booleanInLine:/"([\w-]+)"\s*:\s*(true|false)/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,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 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("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,"&").replace(n.lessThan,"<").replace(n.greaterThan,">")}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("placeholderLayer");e&&t&&(t.style.display=e.value?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updateValue(e){const t=this.shadowRoot.getElementById("textarea");if(t&&t.value!==e){if(t.value=e||"",e)try{const n="["+e+"]",o=JSON.parse(n),s=JSON.stringify(o,null,2).split("\n");s.length>2?t.value=s.slice(1,-1).join("\n"):t.value=""}catch(n){}this.updateHighlight(),this.updatePlaceholderVisibility(),t.value&&requestAnimationFrame(()=>{this.applyAutoCollapsed()}),this.emitChange()}}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateHighlight(){const e=this.shadowRoot.getElementById("textarea"),t=this.shadowRoot.getElementById("highlightLayer");if(!e||!t)return;const n=e.value;this.updateFeatureRanges();const o=this.getHiddenLineRanges(),{highlighted:s,colors:r,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,"&").replace(s.lessThan,"<").replace(s.greaterThan,">").replace(s.jsonKey,(t,s)=>"properties"===n?`<span class="json-key">"${s}"</span>:`:e.GEOJSON_STRUCTURAL_KEYS.includes(s)?`<span class="geojson-key">"${s}"</span>:`:(t=>!!e.GEOJSON_STRUCTURAL_KEYS.includes(t)||!n||null==o||o.includes(t))(s)?`<span class="json-key">"${s}"</span>:`:`<span class="json-key-invalid">"${s}"</span>:`).replace(s.typeValue,(t,o)=>(t=>!n||"properties"===n||("geometry"===n||e.GEOJSON.GEOMETRY_TYPES.includes(n)?e.GEOJSON.GEOMETRY_TYPES.includes(t):"Feature"!==n||"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 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:[],booleans:[],buttons:[],visibilityButtons:[]}),s.get(e));this.colorPositions.forEach(({line:e,color:t,attributeName:n})=>{r(e).colors.push({color:t,attributeName:n})}),this.booleanPositions.forEach(({line:e,value:t,attributeName:n})=>{r(e).booleans.push({value:t,attributeName:n})}),this.nodeTogglePositions.forEach(({line:e,nodeKey:t,isCollapsed:n})=>{r(e).buttons.push({nodeKey:t,isCollapsed:n})});for(const[i,l]of this.featureRanges){const e=this.hiddenFeatures.has(i);r(l.startLine).visibilityButtons.push({featureKey:i,isHidden:e})}const a=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.booleans.forEach(({value:e,attributeName:n})=>{const o=document.createElement("input");o.type="checkbox",o.className="boolean-checkbox",o.checked=e,o.dataset.line=t,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)}),a.appendChild(s)}),e.appendChild(a)}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]{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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softwarity/geojson-editor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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",
|
package/src/geojson-editor.js
CHANGED
|
@@ -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}
|
|
@@ -72,6 +73,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
72
73
|
punctuation: /([{}[\],])/g,
|
|
73
74
|
// Highlighting detection
|
|
74
75
|
colorInLine: /"([\w-]+)"\s*:\s*"(#[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
|
};
|
|
@@ -359,7 +361,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
359
361
|
justify-content: center;
|
|
360
362
|
}
|
|
361
363
|
|
|
362
|
-
.color-indicator, .collapse-button {
|
|
364
|
+
.color-indicator, .collapse-button, .boolean-checkbox {
|
|
363
365
|
width: 12px;
|
|
364
366
|
height: 12px;
|
|
365
367
|
border-radius: 2px;
|
|
@@ -376,6 +378,32 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
376
378
|
border-color: #fff;
|
|
377
379
|
}
|
|
378
380
|
|
|
381
|
+
.boolean-checkbox {
|
|
382
|
+
appearance: none;
|
|
383
|
+
-webkit-appearance: none;
|
|
384
|
+
background: transparent;
|
|
385
|
+
border: 1.5px solid var(--control-border, #c0c0c0);
|
|
386
|
+
border-radius: 2px;
|
|
387
|
+
margin: 0;
|
|
388
|
+
position: relative;
|
|
389
|
+
}
|
|
390
|
+
.boolean-checkbox:checked {
|
|
391
|
+
border-color: var(--control-color, #000080);
|
|
392
|
+
}
|
|
393
|
+
.boolean-checkbox:checked::after {
|
|
394
|
+
content: '✔';
|
|
395
|
+
color: var(--control-color, #000080);
|
|
396
|
+
font-size: 11px;
|
|
397
|
+
font-weight: bold;
|
|
398
|
+
position: absolute;
|
|
399
|
+
top: -3px;
|
|
400
|
+
right: -1px;
|
|
401
|
+
}
|
|
402
|
+
.boolean-checkbox:hover {
|
|
403
|
+
transform: scale(1.2);
|
|
404
|
+
border-color: var(--control-color, #000080);
|
|
405
|
+
}
|
|
406
|
+
|
|
379
407
|
.collapse-button {
|
|
380
408
|
padding-top: 1px;
|
|
381
409
|
background: var(--control-bg, #e8e8e8);
|
|
@@ -600,7 +628,7 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
600
628
|
}, 10);
|
|
601
629
|
});
|
|
602
630
|
|
|
603
|
-
// Gutter clicks (color indicators and collapse buttons)
|
|
631
|
+
// Gutter clicks (color indicators, boolean checkboxes, and collapse buttons)
|
|
604
632
|
const gutterContent = this.shadowRoot.getElementById('gutterContent');
|
|
605
633
|
gutterContent.addEventListener('click', (e) => {
|
|
606
634
|
// Check for visibility button (may click on SVG inside button)
|
|
@@ -616,6 +644,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
616
644
|
const color = e.target.dataset.color;
|
|
617
645
|
const attributeName = e.target.dataset.attributeName;
|
|
618
646
|
this.showColorPicker(e.target, line, color, attributeName);
|
|
647
|
+
} else if (e.target.classList.contains('boolean-checkbox')) {
|
|
648
|
+
const line = parseInt(e.target.dataset.line);
|
|
649
|
+
const attributeName = e.target.dataset.attributeName;
|
|
650
|
+
const newValue = e.target.checked;
|
|
651
|
+
this.updateBooleanValue(line, newValue, attributeName);
|
|
619
652
|
} else if (e.target.classList.contains('collapse-button')) {
|
|
620
653
|
const nodeKey = e.target.dataset.nodeKey;
|
|
621
654
|
const line = parseInt(e.target.dataset.line);
|
|
@@ -766,10 +799,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
766
799
|
const hiddenRanges = this.getHiddenLineRanges();
|
|
767
800
|
|
|
768
801
|
// Parse and highlight
|
|
769
|
-
const { highlighted, colors, toggles } = this.highlightJSON(text, hiddenRanges);
|
|
802
|
+
const { highlighted, colors, booleans, toggles } = this.highlightJSON(text, hiddenRanges);
|
|
770
803
|
|
|
771
804
|
highlightLayer.innerHTML = highlighted;
|
|
772
805
|
this.colorPositions = colors;
|
|
806
|
+
this.booleanPositions = booleans;
|
|
773
807
|
this.nodeTogglePositions = toggles;
|
|
774
808
|
|
|
775
809
|
// Update gutter with color indicators
|
|
@@ -778,11 +812,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
778
812
|
|
|
779
813
|
highlightJSON(text, hiddenRanges = []) {
|
|
780
814
|
if (!text.trim()) {
|
|
781
|
-
return { highlighted: '', colors: [], toggles: [] };
|
|
815
|
+
return { highlighted: '', colors: [], booleans: [], toggles: [] };
|
|
782
816
|
}
|
|
783
817
|
|
|
784
818
|
const lines = text.split('\n');
|
|
785
819
|
const colors = [];
|
|
820
|
+
const booleans = [];
|
|
786
821
|
const toggles = [];
|
|
787
822
|
let highlightedLines = [];
|
|
788
823
|
|
|
@@ -807,6 +842,17 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
807
842
|
});
|
|
808
843
|
}
|
|
809
844
|
|
|
845
|
+
// Detect boolean values in properties
|
|
846
|
+
R.booleanInLine.lastIndex = 0; // Reset for global regex
|
|
847
|
+
let booleanMatch;
|
|
848
|
+
while ((booleanMatch = R.booleanInLine.exec(line)) !== null) {
|
|
849
|
+
booleans.push({
|
|
850
|
+
line: lineIndex,
|
|
851
|
+
value: booleanMatch[2] === 'true', // The boolean value
|
|
852
|
+
attributeName: booleanMatch[1] // The attribute name
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
810
856
|
// Detect collapsible nodes (all nodes are collapsible)
|
|
811
857
|
const nodeMatch = line.match(R.collapsibleNode);
|
|
812
858
|
if (nodeMatch) {
|
|
@@ -849,30 +895,21 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
849
895
|
return {
|
|
850
896
|
highlighted: highlightedLines.join('\n'),
|
|
851
897
|
colors,
|
|
898
|
+
booleans,
|
|
852
899
|
toggles
|
|
853
900
|
};
|
|
854
901
|
}
|
|
855
902
|
|
|
856
903
|
// GeoJSON type constants (consolidated)
|
|
857
904
|
static GEOJSON = {
|
|
858
|
-
|
|
859
|
-
GEOMETRY_TYPES: ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection'],
|
|
860
|
-
ALL_TYPES: ['Feature', 'FeatureCollection', 'Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection']
|
|
905
|
+
GEOMETRY_TYPES: ['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon'],
|
|
861
906
|
};
|
|
862
907
|
|
|
863
908
|
// Valid keys per context (null = any key is valid)
|
|
864
909
|
static VALID_KEYS_BY_CONTEXT = {
|
|
865
|
-
Feature: ['type', 'geometry', 'properties', 'id'
|
|
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'],
|
|
910
|
+
Feature: ['type', 'geometry', 'properties', 'id'],
|
|
874
911
|
properties: null, // Any key valid in properties
|
|
875
|
-
geometry: ['type', 'coordinates'
|
|
912
|
+
geometry: ['type', 'coordinates'], // Generic geometry context
|
|
876
913
|
};
|
|
877
914
|
|
|
878
915
|
// Keys that change context for their value
|
|
@@ -880,7 +917,6 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
880
917
|
geometry: 'geometry',
|
|
881
918
|
properties: 'properties',
|
|
882
919
|
features: 'Feature', // Array of Features
|
|
883
|
-
geometries: 'geometry', // Array of geometries
|
|
884
920
|
};
|
|
885
921
|
|
|
886
922
|
// Build context map for each line by analyzing JSON structure
|
|
@@ -939,7 +975,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
939
975
|
const typeMatch = line.substring(0, j).match(/"type"\s*:\s*$/);
|
|
940
976
|
if (typeMatch) {
|
|
941
977
|
const valueMatch = line.substring(j).match(/^"([^"\\]*(?:\\.[^"\\]*)*)"/);
|
|
942
|
-
|
|
978
|
+
const validTypes = ['Feature', ...GeoJsonEditor.GEOJSON.GEOMETRY_TYPES];
|
|
979
|
+
if (valueMatch && validTypes.includes(valueMatch[1])) {
|
|
943
980
|
const currentCtx = contextStack[contextStack.length - 1];
|
|
944
981
|
if (currentCtx) {
|
|
945
982
|
currentCtx.context = valueMatch[1];
|
|
@@ -990,7 +1027,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
990
1027
|
}
|
|
991
1028
|
|
|
992
1029
|
// All known GeoJSON structural keys (always valid in GeoJSON)
|
|
993
|
-
|
|
1030
|
+
// GeoJSON structural keys that are always valid (not user properties)
|
|
1031
|
+
static GEOJSON_STRUCTURAL_KEYS = ['type', 'geometry', 'properties', 'coordinates', 'id'];
|
|
994
1032
|
|
|
995
1033
|
highlightSyntax(text, context) {
|
|
996
1034
|
if (!text.trim()) return '';
|
|
@@ -1009,15 +1047,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1009
1047
|
|
|
1010
1048
|
// Helper to check if a type value is valid in current context
|
|
1011
1049
|
const isTypeValid = (typeValue) => {
|
|
1012
|
-
// Unknown context - don't validate
|
|
1050
|
+
// Unknown context - don't validate
|
|
1013
1051
|
if (!context) return true;
|
|
1014
1052
|
if (context === 'properties') return true; // Any type in properties
|
|
1015
1053
|
if (context === 'geometry' || GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(context)) {
|
|
1016
1054
|
return GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(typeValue);
|
|
1017
1055
|
}
|
|
1018
|
-
//
|
|
1019
|
-
if (context === 'Feature'
|
|
1020
|
-
return GeoJsonEditor.GEOJSON.
|
|
1056
|
+
// In Feature context: accept Feature or geometry types
|
|
1057
|
+
if (context === 'Feature') {
|
|
1058
|
+
return typeValue === 'Feature' || GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.includes(typeValue);
|
|
1021
1059
|
}
|
|
1022
1060
|
return true; // Unknown context - accept any type
|
|
1023
1061
|
};
|
|
@@ -1159,13 +1197,13 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1159
1197
|
// Clear gutter
|
|
1160
1198
|
gutterContent.textContent = '';
|
|
1161
1199
|
|
|
1162
|
-
// Create a map of line -> elements (color, collapse button, visibility button)
|
|
1200
|
+
// Create a map of line -> elements (color, boolean, collapse button, visibility button)
|
|
1163
1201
|
const lineElements = new Map();
|
|
1164
1202
|
|
|
1165
1203
|
// Helper to ensure line entry exists
|
|
1166
1204
|
const ensureLine = (line) => {
|
|
1167
1205
|
if (!lineElements.has(line)) {
|
|
1168
|
-
lineElements.set(line, { colors: [], buttons: [], visibilityButtons: [] });
|
|
1206
|
+
lineElements.set(line, { colors: [], booleans: [], buttons: [], visibilityButtons: [] });
|
|
1169
1207
|
}
|
|
1170
1208
|
return lineElements.get(line);
|
|
1171
1209
|
};
|
|
@@ -1175,6 +1213,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1175
1213
|
ensureLine(line).colors.push({ color, attributeName });
|
|
1176
1214
|
});
|
|
1177
1215
|
|
|
1216
|
+
// Add boolean checkboxes
|
|
1217
|
+
this.booleanPositions.forEach(({ line, value, attributeName }) => {
|
|
1218
|
+
ensureLine(line).booleans.push({ value, attributeName });
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1178
1221
|
// Add collapse buttons
|
|
1179
1222
|
this.nodeTogglePositions.forEach(({ line, nodeKey, isCollapsed }) => {
|
|
1180
1223
|
ensureLine(line).buttons.push({ nodeKey, isCollapsed });
|
|
@@ -1216,6 +1259,18 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1216
1259
|
gutterLine.appendChild(indicator);
|
|
1217
1260
|
});
|
|
1218
1261
|
|
|
1262
|
+
// Add boolean checkboxes
|
|
1263
|
+
elements.booleans.forEach(({ value, attributeName }) => {
|
|
1264
|
+
const checkbox = document.createElement('input');
|
|
1265
|
+
checkbox.type = 'checkbox';
|
|
1266
|
+
checkbox.className = 'boolean-checkbox';
|
|
1267
|
+
checkbox.checked = value;
|
|
1268
|
+
checkbox.dataset.line = line;
|
|
1269
|
+
checkbox.dataset.attributeName = attributeName;
|
|
1270
|
+
checkbox.title = `${attributeName}: ${value}`;
|
|
1271
|
+
gutterLine.appendChild(checkbox);
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1219
1274
|
// Add collapse buttons
|
|
1220
1275
|
elements.buttons.forEach(({ nodeKey, isCollapsed }) => {
|
|
1221
1276
|
const button = document.createElement('div');
|
|
@@ -1311,6 +1366,19 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1311
1366
|
this.emitChange();
|
|
1312
1367
|
}
|
|
1313
1368
|
|
|
1369
|
+
updateBooleanValue(line, newValue, attributeName) {
|
|
1370
|
+
const textarea = this.shadowRoot.getElementById('textarea');
|
|
1371
|
+
const lines = textarea.value.split('\n');
|
|
1372
|
+
|
|
1373
|
+
// Replace boolean value on the specified line for the specific attribute
|
|
1374
|
+
const regex = new RegExp(`"${attributeName}"\\s*:\\s*(true|false)`);
|
|
1375
|
+
lines[line] = lines[line].replace(regex, `"${attributeName}": ${newValue}`);
|
|
1376
|
+
|
|
1377
|
+
textarea.value = lines.join('\n');
|
|
1378
|
+
this.updateHighlight();
|
|
1379
|
+
this.emitChange();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1314
1382
|
handleKeydownInCollapsedArea(e) {
|
|
1315
1383
|
// Allow navigation keys
|
|
1316
1384
|
const navigationKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Tab'];
|
|
@@ -1443,8 +1511,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1443
1511
|
// Filter out hidden features before emitting
|
|
1444
1512
|
parsed = this.filterHiddenFeatures(parsed);
|
|
1445
1513
|
|
|
1446
|
-
// Validate GeoJSON types
|
|
1447
|
-
|
|
1514
|
+
// Validate GeoJSON types (validate only features, not the wrapper)
|
|
1515
|
+
let validationErrors = [];
|
|
1516
|
+
parsed.features.forEach((feature, index) => {
|
|
1517
|
+
validationErrors.push(...this.validateGeoJSON(feature, `features[${index}]`, 'root'));
|
|
1518
|
+
});
|
|
1448
1519
|
|
|
1449
1520
|
if (validationErrors.length > 0) {
|
|
1450
1521
|
// Emit error event for GeoJSON validation errors
|
|
@@ -1484,23 +1555,12 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1484
1555
|
filterHiddenFeatures(parsed) {
|
|
1485
1556
|
if (!parsed || this.hiddenFeatures.size === 0) return parsed;
|
|
1486
1557
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1491
|
-
|
|
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;
|
|
1558
|
+
// parsed is always a FeatureCollection (from wrapper)
|
|
1559
|
+
const visibleFeatures = parsed.features.filter((feature) => {
|
|
1560
|
+
const key = this.getFeatureKey(feature);
|
|
1561
|
+
return !this.hiddenFeatures.has(key);
|
|
1562
|
+
});
|
|
1563
|
+
return { ...parsed, features: visibleFeatures };
|
|
1504
1564
|
}
|
|
1505
1565
|
|
|
1506
1566
|
// ========== Feature Visibility Management ==========
|
|
@@ -1515,9 +1575,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1515
1575
|
// 2. Use properties.id if present
|
|
1516
1576
|
if (feature.properties?.id !== undefined) return `prop:${feature.properties.id}`;
|
|
1517
1577
|
|
|
1518
|
-
// 3. Fallback: hash based on geometry type +
|
|
1578
|
+
// 3. Fallback: hash based on geometry type + ALL coordinates
|
|
1519
1579
|
const geomType = feature.geometry?.type || 'null';
|
|
1520
|
-
const coords = JSON.stringify(feature.geometry?.coordinates || [])
|
|
1580
|
+
const coords = JSON.stringify(feature.geometry?.coordinates || []);
|
|
1521
1581
|
return `hash:${geomType}:${this.simpleHash(coords)}`;
|
|
1522
1582
|
}
|
|
1523
1583
|
|
|
@@ -1562,12 +1622,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1562
1622
|
const fullValue = prefix + expandedText + suffix;
|
|
1563
1623
|
const parsed = JSON.parse(fullValue);
|
|
1564
1624
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
features = parsed.features;
|
|
1568
|
-
} else if (parsed.type === 'Feature') {
|
|
1569
|
-
features = [parsed];
|
|
1570
|
-
}
|
|
1625
|
+
// parsed is always a FeatureCollection (from wrapper)
|
|
1626
|
+
const features = parsed.features;
|
|
1571
1627
|
|
|
1572
1628
|
// Now find each feature's line range in the text
|
|
1573
1629
|
const lines = text.split('\n');
|
|
@@ -1585,10 +1641,18 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1585
1641
|
const isFeatureTypeLine = /"type"\s*:\s*"Feature"/.test(line);
|
|
1586
1642
|
if (!inFeature && isFeatureTypeLine) {
|
|
1587
1643
|
// Find the opening brace for this Feature
|
|
1588
|
-
// Look backwards for the opening brace
|
|
1644
|
+
// Look backwards for a line that starts with just '{' (the Feature's opening brace)
|
|
1645
|
+
// Not a line like '"geometry": {' which contains other content before the brace
|
|
1589
1646
|
let startLine = i;
|
|
1590
1647
|
for (let j = i; j >= 0; j--) {
|
|
1591
|
-
|
|
1648
|
+
const trimmed = lines[j].trim();
|
|
1649
|
+
// Line is just '{' or '{' followed by nothing significant (opening brace only)
|
|
1650
|
+
if (trimmed === '{' || trimmed === '{,') {
|
|
1651
|
+
startLine = j;
|
|
1652
|
+
break;
|
|
1653
|
+
}
|
|
1654
|
+
// Also handle case where Feature starts on same line: { "type": "Feature"
|
|
1655
|
+
if (trimmed.startsWith('{') && !trimmed.includes(':')) {
|
|
1592
1656
|
startLine = j;
|
|
1593
1657
|
break;
|
|
1594
1658
|
}
|
|
@@ -1671,9 +1735,9 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1671
1735
|
errors.push(`Invalid geometry type "${typeValue}" at ${path || 'root'} (expected: ${GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.join(', ')})`);
|
|
1672
1736
|
}
|
|
1673
1737
|
} else {
|
|
1674
|
-
// At root or in features: must be Feature
|
|
1675
|
-
if (
|
|
1676
|
-
errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected:
|
|
1738
|
+
// At root or in features: must be Feature
|
|
1739
|
+
if (typeValue !== 'Feature') {
|
|
1740
|
+
errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected: Feature)`);
|
|
1677
1741
|
}
|
|
1678
1742
|
}
|
|
1679
1743
|
}
|
|
@@ -2168,15 +2232,10 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2168
2232
|
errors.push(`Invalid geometry type "${feature.geometry.type}" (expected: ${GeoJsonEditor.GEOJSON.GEOMETRY_TYPES.join(', ')})`);
|
|
2169
2233
|
}
|
|
2170
2234
|
|
|
2171
|
-
// Check geometry has coordinates
|
|
2172
|
-
if (
|
|
2235
|
+
// Check geometry has coordinates
|
|
2236
|
+
if (!('coordinates' in feature.geometry)) {
|
|
2173
2237
|
errors.push('Geometry must have a "coordinates" property');
|
|
2174
2238
|
}
|
|
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
2239
|
}
|
|
2181
2240
|
}
|
|
2182
2241
|
|