@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 +3 -1
- package/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.js +151 -78
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.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,"&").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]{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,"&").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 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.
|
|
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",
|
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}
|
|
@@ -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
|
-
.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
1019
|
-
if (context === 'Feature'
|
|
1020
|
-
return GeoJsonEditor.GEOJSON.
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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 +
|
|
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 || [])
|
|
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
|
-
|
|
1566
|
-
|
|
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
|
-
|
|
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
|
|
1675
|
-
if (
|
|
1676
|
-
errors.push(`Invalid type "${typeValue}" at ${path || 'root'} (expected:
|
|
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
|
|
2172
|
-
if (
|
|
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;
|