@softwarity/geojson-editor 1.0.13 → 1.0.14
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/dist/geojson-editor.js +2 -2
- package/package.json +1 -1
- package/src/geojson-editor.js +108 -53
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.14
|
|
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
|
-
const e=["type","geometry","properties","coordinates","id","features"],t=["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon"];class s extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.lines=[],this.collapsedNodes=/* @__PURE__ */new Set,this.hiddenFeatures=/* @__PURE__ */new Set,this._nodeIdCounter=0,this._lineToNodeId=/* @__PURE__ */new Map,this._nodeIdToLines=/* @__PURE__ */new Map,this.visibleLines=[],this.lineMetadata=/* @__PURE__ */new Map,this.featureRanges=/* @__PURE__ */new Map,this.scrollTop=0,this.viewportHeight=0,this.lineHeight=19.5,this.bufferLines=5,this._lastStartIndex=-1,this._lastEndIndex=-1,this._lastTotalLines=-1,this._scrollRaf=null,this.cursorLine=0,this.cursorColumn=0,this.selectionStart=null,this.selectionEnd=null,this.renderTimer=null,this.inputTimer=null,this.themes={dark:{},light:{}}}_generateNodeId(){return"node_"+ ++this._nodeIdCounter}_getCollapsedRangeForLine(e){for(const[t,s]of this._nodeIdToLines)if(this.collapsedNodes.has(t)&&e>s.startLine&&e<s.endLine)return{nodeId:t,...s};return null}_getCollapsedClosingLine(e){for(const[t,s]of this._nodeIdToLines)if(this.collapsedNodes.has(t)&&e===s.endLine)return{nodeId:t,...s};return null}_getClosingBracketPos(e){return Math.max(e.lastIndexOf("]"),e.lastIndexOf("}"))}_getCollapsedNodeAtLine(e){const t=this._lineToNodeId.get(e);return t&&this.collapsedNodes.has(t)?{nodeId:t,...this._nodeIdToLines.get(t)}:null}_getCollapsibleNodeAtLine(e){const t=this._lineToNodeId.get(e);if(t){const e=this._nodeIdToLines.get(t);return{nodeId:t,isCollapsed:this.collapsedNodes.has(t),...e}}return null}_getContainingExpandedNode(e){let t=null;for(const[s,i]of this._nodeIdToLines)this.collapsedNodes.has(s)||e>=i.startLine&&e<=i.endLine&&(!t||i.endLine-i.startLine<t.endLine-t.startLine)&&(t={nodeId:s,...i});return t}_deleteCollapsedNode(e){const t=e.endLine-e.startLine+1;this.lines.splice(e.startLine,t),this.cursorLine=Math.min(e.startLine,this.lines.length-1),this.cursorColumn=0,this.formatAndUpdate()}_rebuildNodeIdMappings(){const e=new Set(this.collapsedNodes),t=/* @__PURE__ */new Map;for(const[n,o]of this._nodeIdToLines)o.nodeKey&&t.set(n,o.nodeKey);const s=[];for(const n of e){const e=t.get(n);e&&s.push(e)}this._nodeIdCounter=0,this._lineToNodeId.clear(),this._nodeIdToLines.clear(),this.collapsedNodes.clear();const i=/* @__PURE__ */new Map;for(let n=0;n<this.lines.length;n++){const e=this.lines[n],t=e.match(/^\s*"([^"]+)"\s*:\s*([{\[])/),o=!t&&e.match(/^\s*([{\[]),?\s*$/);if(!t&&!o)continue;let r,l;t?(r=t[1],l=t[2]):(l=o[1],r=`__root_${l}_${n}`);const a=e.substring(e.indexOf(l)+1),c=this._countBrackets(a,l);if(c.close>c.open)continue;const h=this._findClosingLine(n,l);if(-1===h||h===n)continue;const d=this._generateNodeId();this._lineToNodeId.set(n,d),this._nodeIdToLines.set(d,{startLine:n,endLine:h,nodeKey:r,isRootFeature:!!o});const u=i.get(r)||0;i.set(r,u+1);const p=s.indexOf(r);-1!==p&&(s.splice(p,1),this.collapsedNodes.add(d))}}static get observedAttributes(){return["readonly","value","placeholder","dark-selector"]}connectedCallback(){this.render(),this.setupEventListeners(),this.updatePrefixSuffix(),this.updateThemeCSS(),this.value&&this.setValue(this.value),this.updatePlaceholderVisibility()}disconnectedCallback(){this.renderTimer&&clearTimeout(this.renderTimer),this.inputTimer&&clearTimeout(this.inputTimer);const e=document.querySelector(".geojson-color-picker-input");e&&(e._closeListener&&document.removeEventListener("click",e._closeListener,!0),e.remove())}attributeChangedCallback(e,t,s){if(t!==s)switch(e){case"value":this.setValue(s);break;case"readonly":this.updateReadonly();break;case"placeholder":this.updatePlaceholderContent();break;case"dark-selector":this.updateThemeCSS()}}get readonly(){return this.hasAttribute("readonly")}get value(){return this.getAttribute("value")||""}get placeholder(){return this.getAttribute("placeholder")||""}get prefix(){return'{"type": "FeatureCollection", "features": ['}get suffix(){return"]}"}render(){const e=document.createElement("style");e.textContent=':host *,:host *:before,:host *:after{box-sizing:border-box;font: 13px/1.5 Courier New,Courier,monospace;font-variant:normal;letter-spacing:0;word-spacing:0;text-transform:none;text-decoration:none;text-indent:0}:host{display:flex;flex-direction:column;position:relative;width:100%;height:400px;border-radius:4px;overflow:hidden}:host([readonly]) .editor-wrapper:after{content:"";position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(-45deg,rgba(128,128,128,.08),rgba(128,128,128,.08) 3px,transparent 3px,transparent 12px);z-index:1}:host([readonly]) .hidden-textarea{cursor:text}.editor-wrapper{position:relative;width:100%;flex:1;background:var(--bg-color, #fff);display:flex;overflow:hidden}.gutter{width:50px;background:var(--gutter-bg, #f0f0f0);border-right:1px solid var(--gutter-border, #e0e0e0);overflow:hidden;flex-shrink:0;position:relative}.gutter-scroll{position:absolute;inset:0;overflow:hidden;padding:8px 0}.gutter-scroll-content{position:relative;width:100%}.gutter-content{position:absolute;top:0;left:0;right:0;will-change:transform}.gutter-line{height:19.5px;display:flex;align-items:center;justify-content:flex-end;padding-right:4px}.line-number{font-size:11px;color:var(--gutter-text, #999);-webkit-user-select:none;user-select:none;min-width:20px;text-align:right}.collapse-column{width:16px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.color-indicator,.collapse-button,.boolean-checkbox{width:12px;height:12px;border-radius:2px;cursor:pointer;transition:transform .1s;flex-shrink:0}.color-indicator{border:1px solid #555}.color-indicator:hover{transform:scale(1.2);border-color:#fff}.boolean-checkbox{appearance:none;-webkit-appearance:none;background:transparent;border:1.5px solid var(--control-border, #c0c0c0);border-radius:2px;margin:0;position:relative}.boolean-checkbox:checked{border-color:var(--control-color, #000080)}.boolean-checkbox:checked:after{content:"✔";color:var(--control-color, #000080);font-size:11px;font-weight:700;position:absolute;top:-3px;right:-1px}.boolean-checkbox:hover{transform:scale(1.2);border-color:var(--control-color, #000080)}.collapse-button{background:transparent;border:none;color:var(--json-punct, #a9b7c6);font-size:10px;display:flex;align-items:center;justify-content:center;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}.collapse-button.collapsed,.gutter:hover .collapse-button{opacity:1}.collapse-button:hover{transform:scale(1.2)}@media(hover:none),(pointer:coarse){.collapse-button{opacity:1}}.visibility-button{width:14px;height:14px;background:transparent;color:var(--control-color, #000080);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .1s;flex-shrink:0;opacity:.7;padding:0;font-size:11px}.visibility-button:hover{opacity:1;transform:scale(1.15)}.visibility-button.hidden{opacity:.35}.editor-content{position:relative;flex:1;overflow:hidden}.hidden-textarea{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;padding:0;margin:0;border:none;outline:none;resize:none;overflow:hidden;z-index:-1;pointer-events:none;caret-color:transparent}.viewport{position:absolute;inset:0;overflow:auto;padding:8px 12px;overscroll-behavior:contain}.scroll-content{position:relative;width:100%}.lines-container{position:absolute;top:0;left:0;right:0;will-change:transform}.line{height:19.5px;white-space:pre;display:block;position:relative}.cursor{position:absolute;width:2px;height:1em;background:var(--caret-color, #000);top:.15em;pointer-events:none;animation:cursor-blink 1s step-end infinite}@keyframes cursor-blink{0%,to{opacity:1}50%{opacity:0}}.selection{position:absolute;height:100%;top:0;background:var(--selection-color, rgba(51, 153, 255, .3));pointer-events:none;z-index:0}.line-hidden>span{opacity:.35;filter:grayscale(50%)}.line-collapsed{display:none}.placeholder-layer{position:absolute;top:8px;left:12px;color:#6a6a6a;pointer-events:none;z-index:0;white-space:pre}.json-key{color:var(--json-key, #660e7a)}.json-string{color:var(--json-string, #008000)}.json-number{color:var(--json-number, #00f)}.json-boolean,.json-null{color:var(--json-boolean, #000080)}.json-punctuation{color:var(--json-punct, #000)}.json-key-invalid{color:var(--json-key-invalid, #f00)}.json-error{color:var(--json-error, #f00)}.geojson-key{color:var(--geojson-key, #660e7a);font-weight:600}.geojson-type{color:var(--geojson-type, #008000);font-weight:600}.geojson-type-invalid{color:var(--geojson-type-invalid, #f00);font-weight:600}.d-none{display:none}.collapsed-bracket-array,.collapsed-bracket-object{color:var(--json-punct, #000)}.collapsed-bracket-array:after,.collapsed-bracket-object:after{content:"…";color:var(--json-punct, #888)}.collapsed-content{display:none}.prefix-wrapper,.suffix-wrapper{display:flex;flex-shrink:0;background:var(--bg-color, #fff)}.prefix-gutter,.suffix-gutter{width:50px;background:var(--gutter-bg, #f0f0f0);border-right:1px solid var(--gutter-border, #e0e0e0);flex-shrink:0}.editor-prefix,.editor-suffix{flex:1;padding:4px 12px;color:var(--text-color, #000);background:var(--bg-color, #fff);-webkit-user-select:none;user-select:none;white-space:pre-wrap;word-wrap:break-word;opacity:.6}.prefix-wrapper{border-bottom:1px solid rgba(255,255,255,.1);position:relative}.suffix-wrapper{border-top:1px solid rgba(255,255,255,.1);position:relative}.info-btn{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:transparent;border:none;color:var(--text-color, #000);opacity:.15;cursor:pointer;font-size:.7rem;width:1rem;height:1rem;padding:0;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:opacity .2s;font-family:sans-serif}.info-btn:hover{opacity:.5}.clear-btn{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:transparent;border:none;color:var(--text-color, #000);opacity:.3;cursor:pointer;font-size:.65rem;width:1rem;height:1rem;padding:.15rem 0 0;border-radius:3px;display:flex;align-items:center;justify-content:center;transition:opacity .2s,background .2s}.clear-btn:hover{opacity:.7;background:#ffffff1a}.clear-btn[hidden]{display:none}.viewport::-webkit-scrollbar{width:10px;height:10px}.viewport::-webkit-scrollbar-track{background:var(--control-bg, #e8e8e8)}.viewport::-webkit-scrollbar-thumb{background:var(--control-border, #c0c0c0);border-radius:5px}.viewport::-webkit-scrollbar-thumb:hover{background:var(--control-color, #000080)}.viewport{scrollbar-width:thin;scrollbar-color:var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8)}.line .selected{background:#3399ff4d}.json-color,.json-boolean{position:relative}.json-color:before,.json-boolean:before{content:"";position:absolute;left:-8px;top:50%;transform:translateY(-50%);margin-top:-1px;width:8px;height:8px;border-radius:2px;cursor:pointer}.json-color:hover:before,.json-boolean:hover:before{transform:translateY(-50%) scale(1.2);border-color:var(--control-color, #000080)}.json-color:before{background-color:var(--swatch-color);border:1px solid var(--json-punct, #a9b7c6)}.json-boolean:before{border:1.5px solid var(--control-border, #c0c0c0);background:transparent}.json-bool-true:before{content:"✔";border-color:var(--control-color, #000080);color:var(--control-color, #000080);font-size:8px;display:flex;align-items:center;justify-content:center;line-height:8px}.line.has-visibility{position:relative}.line.has-visibility:before{content:"👁";position:absolute;left:0;top:3px;font-size:10px;color:var(--control-color, #000080);opacity:.6;cursor:pointer;z-index:1}.line.has-visibility:hover:before{opacity:1;transform:scale(1.15)}.line.has-visibility.feature-hidden:before{opacity:.5}';const t=document.createElement("div");for(t.innerHTML=function(e="",t=""){return`\n <div class="prefix-wrapper">\n <div class="prefix-gutter"></div>\n <div class="editor-prefix" id="editorPrefix"></div>\n <button class="info-btn" id="infoBtn" title="@softwarity/geojson-editor v${t}" aria-label="About">ⓘ</button>\n </div>\n <div class="editor-wrapper">\n <div class="gutter">\n <div class="gutter-scroll" id="gutterScroll">\n <div class="gutter-scroll-content" id="gutterScrollContent">\n <div class="gutter-content" id="gutterContent"></div>\n </div>\n </div>\n </div>\n <div class="editor-content">\n <div class="placeholder-layer" id="placeholderLayer">${s=e,s?s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"):""}</div>\n <textarea\n class="hidden-textarea"\n id="hiddenTextarea"\n spellcheck="false"\n autocomplete="off"\n autocorrect="off"\n autocapitalize="off"\n tabindex="0"\n ></textarea>\n <div class="viewport" id="viewport">\n <div class="scroll-content" id="scrollContent">\n <div class="lines-container" id="linesContainer"></div>\n </div>\n </div>\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 `;var s}(this.placeholder,"1.0.13"),this.shadowRoot.innerHTML="",this.shadowRoot.appendChild(e);t.firstChild;)this.shadowRoot.appendChild(t.firstChild)}setupEventListeners(){const e=this.shadowRoot.getElementById("hiddenTextarea"),t=this.shadowRoot.getElementById("viewport"),s=this.shadowRoot.getElementById("gutterContent"),i=this.shadowRoot.querySelector(".gutter"),n=this.shadowRoot.getElementById("clearBtn"),o=this.shadowRoot.querySelector(".editor-wrapper");this._isSelecting=!1,t.addEventListener("click",e=>{this.handleEditorClick(e)},!0),t.addEventListener("mousedown",t=>{const s=t.target.closest(".line.has-visibility");if(s){const e=s.getBoundingClientRect();if(t.clientX-e.left<14)return}if(t.target.classList.contains("json-color")||t.target.classList.contains("json-boolean")){const e=t.target.getBoundingClientRect(),s=t.clientX-e.left;if(s<0&&s>=-8)return}t.preventDefault();const i=this._getPositionFromClick(t);t.shiftKey&&this.selectionStart?(this.selectionEnd=i,this.cursorLine=i.line,this.cursorColumn=i.column):(this.cursorLine=i.line,this.cursorColumn=i.column,this.selectionStart={line:i.line,column:i.column},this.selectionEnd=null,this._isSelecting=!0),e.focus(),this._lastStartIndex=-1,this.scheduleRender()}),t.addEventListener("mousemove",e=>{if(!this._isSelecting)return;const s=this._getPositionFromClick(e);this.selectionEnd=s,this.cursorLine=s.line,this.cursorColumn=s.column;const i=t.getBoundingClientRect();e.clientY<i.top+30?t.scrollTop-=20:e.clientY>i.bottom-30&&(t.scrollTop+=20),this._lastStartIndex=-1,this.scheduleRender()}),document.addEventListener("mouseup",()=>{this._isSelecting=!1}),e.addEventListener("focus",()=>{o.classList.add("focused"),this._lastStartIndex=-1,this.scheduleRender()}),e.addEventListener("blur",()=>{o.classList.remove("focused"),this._lastStartIndex=-1,this.scheduleRender()});let r=!1;t.addEventListener("scroll",()=>{r||(this.scrollTop=t.scrollTop,this.syncGutterScroll(),this._scrollRaf||(this._scrollRaf=requestAnimationFrame(()=>{this._scrollRaf=null,r=!0,this.renderViewport(),r=!1})))}),e.addEventListener("input",()=>{this.handleInput()}),e.addEventListener("keydown",e=>{this.handleKeydown(e)}),e.addEventListener("paste",e=>{this.handlePaste(e)}),e.addEventListener("copy",e=>{this.handleCopy(e)}),e.addEventListener("cut",e=>{this.handleCut(e)}),s.addEventListener("click",e=>{this.handleGutterClick(e)}),i.addEventListener("mousedown",e=>{e.preventDefault()}),i.addEventListener("wheel",e=>{e.preventDefault(),t.scrollTop+=e.deltaY}),n.addEventListener("click",()=>{this.removeAll()}),this.updateReadonly()}setValue(e){if(e&&e.trim())try{const t="["+e+"]",s=JSON.parse(t),i=JSON.stringify(s,null,2).split("\n");this.lines=i.slice(1,-1)}catch(t){this.lines=e.split("\n")}else this.lines=[];this.collapsedNodes.clear(),this.hiddenFeatures.clear(),this._lineToNodeId.clear(),this._nodeIdToLines.clear(),this.cursorLine=0,this.cursorColumn=0,this.updateModel(),this.scheduleRender(),this.updatePlaceholderVisibility(),this.lines.length>0&&requestAnimationFrame(()=>{this.autoCollapseCoordinates()}),this.emitChange()}getContent(){return this.lines.join("\n")}updateModel(){this._rebuildNodeIdMappings(),this.computeFeatureRanges(),this.computeLineMetadata(),this.computeVisibleLines()}updateView(){this.computeLineMetadata(),this.computeVisibleLines()}computeFeatureRanges(){this.featureRanges.clear();try{const e=this.lines.join("\n"),t=this.prefix+e+this.suffix,s=JSON.parse(t);if(!s.features)return;let i=0,n=0,o=!1,r=-1,l=null;for(let a=0;a<this.lines.length;a++){const e=this.lines[a];if(!o&&/"type"\s*:\s*"Feature"/.test(e)){let e=a;for(let t=a;t>=0;t--){const s=this.lines[t].trim();if("{"===s||"{,"===s){e=t;break}}r=e,o=!0,n=1;for(let t=e;t<=a;t++){const s=this._countBrackets(this.lines[t],"{");n+=t===e?s.open-1-s.close:s.open-s.close}i<s.features.length&&(l=this._getFeatureKey(s.features[i]))}else if(o){const t=this._countBrackets(e,"{");n+=t.open-t.close,n<=0&&(l&&this.featureRanges.set(l,{startLine:r,endLine:a,featureIndex:i}),i++,o=!1,l=null)}}}catch(e){}}computeLineMetadata(){this.lineMetadata.clear();const e=this._findCollapsibleRanges();for(let t=0;t<this.lines.length;t++){const s=this.lines[t],i={colors:[],booleans:[],collapseButton:null,visibilityButton:null,isHidden:!1,isCollapsed:!1,featureKey:null},n=/"([\w-]+)"\s*:\s*"(#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}))"/g;let o;for(;null!==(o=n.exec(s));)i.colors.push({attributeName:o[1],color:o[2]});const r=/"([\w-]+)"\s*:\s*(true|false)/g;let l;for(;null!==(l=r.exec(s));)i.booleans.push({attributeName:l[1],value:"true"===l[2]});const a=e.find(e=>e.startLine===t);a&&(i.collapseButton={nodeKey:a.nodeKey,nodeId:a.nodeId,isCollapsed:this.collapsedNodes.has(a.nodeId)}),e.find(e=>this.collapsedNodes.has(e.nodeId)&&t>e.startLine&&t<e.endLine)&&(i.isCollapsed=!0);for(const[e,c]of this.featureRanges)if(t>=c.startLine&&t<=c.endLine){i.featureKey=e,this.hiddenFeatures.has(e)&&(i.isHidden=!0),t===c.startLine&&(i.visibilityButton={featureKey:e,isHidden:this.hiddenFeatures.has(e)});break}this.lineMetadata.set(t,i)}}computeVisibleLines(){this.visibleLines=[];for(let e=0;e<this.lines.length;e++){const t=this.lineMetadata.get(e);t&&t.isCollapsed||this.visibleLines.push({index:e,content:this.lines[e],meta:t})}this._lastStartIndex=-1,this._lastEndIndex=-1,this._lastTotalLines=-1}scheduleRender(){this.renderTimer||(this.renderTimer=requestAnimationFrame(()=>{this.renderTimer=null,this.renderViewport()}))}renderViewport(){const e=this.shadowRoot.getElementById("viewport"),t=this.shadowRoot.getElementById("linesContainer"),s=this.shadowRoot.getElementById("scrollContent");if(this.shadowRoot.getElementById("gutterContent"),!e||!t)return;this.viewportHeight=e.clientHeight;const i=this.visibleLines.length,n=i*this.lineHeight;s&&(s.style.height=`${n}px`);const o=e.scrollTop,r=Math.floor(o/this.lineHeight),l=Math.ceil(this.viewportHeight/this.lineHeight),a=Math.max(0,r-this.bufferLines),c=Math.min(i,r+l+this.bufferLines);if(i>0&&this._lastStartIndex===a&&this._lastEndIndex===c&&this._lastTotalLines===i)return;this._lastStartIndex=a,this._lastEndIndex=c,this._lastTotalLines=i;const h=a*this.lineHeight;t.style.transform=`translateY(${h}px)`;const d=this._buildContextMap(),u=this.shadowRoot.querySelector(".editor-wrapper"),p=u?.classList.contains("focused"),g=document.createDocumentFragment();if(0===i){const e=document.createElement("div");return e.className="line empty-line",e.dataset.lineIndex="0",p&&(e.innerHTML=this._insertCursor(0)),g.appendChild(e),t.innerHTML="",t.appendChild(g),void this.renderGutter(0,0)}for(let f=a;f<c;f++){const e=this.visibleLines[f];if(!e)continue;const t=document.createElement("div");t.className="line",t.dataset.lineIndex=e.index,e.meta?.visibilityButton&&(t.classList.add("has-visibility"),t.dataset.featureKey=e.meta.visibilityButton.featureKey,e.meta.visibilityButton.isHidden&&t.classList.add("feature-hidden")),e.meta?.isHidden&&t.classList.add("line-hidden");const s=d.get(e.index);let i=this._highlightSyntax(e.content,s,e.meta);p&&this._hasSelection()&&(i=this._addSelectionHighlight(i,e.index,e.content)),p&&e.index===this.cursorLine&&(i+=this._insertCursor(this.cursorColumn)),t.innerHTML=i,g.appendChild(t)}t.innerHTML="",t.appendChild(g),this.renderGutter(a,c)}_insertCursor(e){return`<span class="cursor" style="left: ${e*this._getCharWidth()}px"></span>`}_addSelectionHighlight(e,t,s){const{start:i,end:n}=this._normalizeSelection();if(!i||!n)return e;if(t<i.line||t>n.line)return e;const o=this._getCharWidth();let r,l;return t===i.line&&t===n.line?(r=i.column,l=n.column):t===i.line?(r=i.column,l=s.length):t===n.line?(r=0,l=n.column):(r=0,l=s.length),`<span class="selection" style="left: ${r*o}px; width: ${(l-r)*o}px"></span>`+e}_getCharWidth(){if(!this._charWidth){const e=document.createElement("canvas").getContext("2d");e.font="13px monospace",this._charWidth=e.measureText("M").width}return this._charWidth}renderGutter(e,t){const s=this.shadowRoot.getElementById("gutterContent"),i=this.shadowRoot.getElementById("gutterScrollContent");if(!s)return;const n=this.visibleLines.length*this.lineHeight;i&&(i.style.height=`${n}px`);const o=e*this.lineHeight;s.style.transform=`translateY(${o}px)`;const r=document.createDocumentFragment();for(let l=e;l<t;l++){const e=this.visibleLines[l];if(!e)continue;const t=document.createElement("div");t.className="gutter-line";const s=e.meta,i=document.createElement("span");i.className="line-number",i.textContent=e.index+1,t.appendChild(i);const n=document.createElement("div");if(n.className="collapse-column",s?.collapseButton){const t=document.createElement("div");t.className="collapse-button"+(s.collapseButton.isCollapsed?" collapsed":""),t.textContent=s.collapseButton.isCollapsed?"›":"⌄",t.dataset.line=e.index,t.dataset.nodeId=s.collapseButton.nodeId,t.title=s.collapseButton.isCollapsed?"Expand":"Collapse",n.appendChild(t)}t.appendChild(n),r.appendChild(t)}s.innerHTML="",s.appendChild(r)}syncGutterScroll(){const e=this.shadowRoot.getElementById("gutterScroll"),t=this.shadowRoot.getElementById("viewport");e&&t&&(e.scrollTop=t.scrollTop)}handleInput(){const e=this.shadowRoot.getElementById("hiddenTextarea"),t=e.value;if(t)if(this._getCollapsedRangeForLine(this.cursorLine))e.value="";else{if(this._getCollapsedClosingLine(this.cursorLine)){const t=this.lines[this.cursorLine],s=this._getClosingBracketPos(t);if(this.cursorColumn<=s)return void(e.value="")}if(this._getCollapsedNodeAtLine(this.cursorLine)){const t=this.lines[this.cursorLine].search(/[{\[]/);if(this.cursorColumn>t)return void(e.value="")}if(this.cursorLine<this.lines.length){const e=this.lines[this.cursorLine],s=e.substring(0,this.cursorColumn),i=e.substring(this.cursorColumn),n=t.split("\n");if(1===n.length)this.lines[this.cursorLine]=s+t+i,this.cursorColumn+=t.length;else{this.lines[this.cursorLine]=s+n[0];for(let t=1;t<n.length-1;t++)this.lines.splice(this.cursorLine+t,0,n[t]);const e=n[n.length-1]+i;this.lines.splice(this.cursorLine+n.length-1,0,e),this.cursorLine+=n.length-1,this.cursorColumn=n[n.length-1].length}}else{const e=t.split("\n");this.lines.push(...e),this.cursorLine=this.lines.length-1,this.cursorColumn=this.lines[this.cursorLine].length}e.value="",clearTimeout(this.inputTimer),this.inputTimer=setTimeout(()=>{this.formatAndUpdate()},150)}}handleKeydown(e){const t=this._getCollapsedRangeForLine(this.cursorLine),s=this._getCollapsedNodeAtLine(this.cursorLine),i=this._getCollapsedClosingLine(this.cursorLine);switch(e.key){case"Enter":if(e.preventDefault(),s||t)return;if(i){const e=this.lines[this.cursorLine],t=this._getClosingBracketPos(e);if(t>=0&&this.cursorColumn<=t)return}this.insertNewline();break;case"Backspace":if(e.preventDefault(),this._hasSelection())return this._deleteSelection(),void this.formatAndUpdate();if(i){const e=this.lines[this.cursorLine],t=this._getClosingBracketPos(e);return t>=0&&this.cursorColumn>t+1?void this.deleteBackward():(this.cursorColumn,void this._deleteCollapsedNode(i))}if(s&&0===this.cursorColumn)return void this._deleteCollapsedNode(s);if(t)return;if(s){const e=this.lines[this.cursorLine].search(/[{\[]/);if(this.cursorColumn>e+1)return void this._deleteCollapsedNode(s)}this.deleteBackward();break;case"Delete":if(e.preventDefault(),this._hasSelection())return this._deleteSelection(),void this.formatAndUpdate();if(i){const e=this.lines[this.cursorLine],t=this._getClosingBracketPos(e);return t>=0&&this.cursorColumn>t?void this.deleteForward():void this._deleteCollapsedNode(i)}if(s){const e=this.lines[this.cursorLine].search(/[{\[]/);if(this.cursorColumn>e)return void this._deleteCollapsedNode(s)}if(t)return;this.deleteForward();break;case"ArrowUp":e.preventDefault(),this._handleArrowKey(-1,0,e.shiftKey);break;case"ArrowDown":e.preventDefault(),this._handleArrowKey(1,0,e.shiftKey);break;case"ArrowLeft":e.preventDefault(),this._handleArrowKey(0,-1,e.shiftKey);break;case"ArrowRight":e.preventDefault(),this._handleArrowKey(0,1,e.shiftKey);break;case"Home":e.preventDefault(),this._handleHomeEnd("home",e.shiftKey,i);break;case"End":e.preventDefault(),this._handleHomeEnd("end",e.shiftKey,i);break;case"a":if(e.ctrlKey||e.metaKey)return e.preventDefault(),void this._selectAll();break;case"Tab":if(e.preventDefault(),e.shiftKey){const e=this._getContainingExpandedNode(this.cursorLine);if(e){const t=this.lines[e.startLine],s=t.search(/[{\[]/);this.toggleCollapse(e.nodeId),this.cursorLine=e.startLine,this.cursorColumn=s>=0?s+1:t.length,this._clearSelection(),this._scrollToCursor()}return}if(s)return void this.toggleCollapse(s.nodeId);if(i)return void this.toggleCollapse(i.nodeId);if(t)return}}insertNewline(){if(this.cursorLine<this.lines.length){const e=this.lines[this.cursorLine],t=e.substring(0,this.cursorColumn),s=e.substring(this.cursorColumn);this.lines[this.cursorLine]=t,this.lines.splice(this.cursorLine+1,0,s),this.cursorLine++,this.cursorColumn=0}else this.lines.push(""),this.cursorLine=this.lines.length-1,this.cursorColumn=0;this.formatAndUpdate()}deleteBackward(){if(this.cursorColumn>0){const e=this.lines[this.cursorLine];this.lines[this.cursorLine]=e.substring(0,this.cursorColumn-1)+e.substring(this.cursorColumn),this.cursorColumn--}else if(this.cursorLine>0){const e=this.lines[this.cursorLine],t=this.lines[this.cursorLine-1];this.cursorColumn=t.length,this.lines[this.cursorLine-1]=t+e,this.lines.splice(this.cursorLine,1),this.cursorLine--}this.formatAndUpdate()}deleteForward(){if(this.cursorLine<this.lines.length){const e=this.lines[this.cursorLine];this.cursorColumn<e.length?this.lines[this.cursorLine]=e.substring(0,this.cursorColumn)+e.substring(this.cursorColumn+1):this.cursorLine<this.lines.length-1&&(this.lines[this.cursorLine]=e+this.lines[this.cursorLine+1],this.lines.splice(this.cursorLine+1,1))}this.formatAndUpdate()}moveCursorSkipCollapsed(e){let t=this.cursorLine+e;for(;t>=0&&t<this.lines.length;){const s=this._getCollapsedRangeForLine(t);s&&(t=e>0?s.endLine:s.startLine);break}this.cursorLine=Math.max(0,Math.min(this.lines.length-1,t));const s=this.lines[this.cursorLine]?.length||0;this.cursorColumn=Math.min(this.cursorColumn,s),this._lastStartIndex=-1,this._scrollToCursor(),this.scheduleRender()}moveCursorHorizontal(e){const t=this.lines[this.cursorLine],s=this._getCollapsedNodeAtLine(this.cursorLine),i=this._getCollapsedClosingLine(this.cursorLine);if(e>0)if(i){const e=this._getClosingBracketPos(t);this.cursorColumn<e?this.cursorColumn=e:this.cursorColumn>=t.length?this.cursorLine<this.lines.length-1&&(this.cursorLine++,this.cursorColumn=0):this.cursorColumn++}else if(s){const e=t.search(/[{\[]/);if(this.cursorColumn<e)this.cursorColumn++;else if(this.cursorColumn===e)this.cursorColumn=e+1;else{this.cursorLine=s.endLine;const e=this.lines[this.cursorLine];this.cursorColumn=this._getClosingBracketPos(e)}}else if(this.cursorColumn>=t.length){if(this.cursorLine<this.lines.length-1){this.cursorLine++,this.cursorColumn=0;const e=this._getCollapsedRangeForLine(this.cursorLine);e&&(this.cursorLine=e.endLine,this.cursorColumn=0)}}else this.cursorColumn++;else if(i){const e=this._getClosingBracketPos(t);if(this.cursorColumn>e+1)this.cursorColumn--;else if(this.cursorColumn===e+1){this.cursorLine=i.startLine;const e=this.lines[this.cursorLine].search(/[{\[]/);this.cursorColumn=e+1}else{this.cursorLine=i.startLine;const e=this.lines[this.cursorLine].search(/[{\[]/);this.cursorColumn=e+1}}else if(s){const e=t.search(/[{\[]/);this.cursorColumn>e+1?this.cursorColumn=e+1:this.cursorColumn===e+1?this.cursorColumn=e:this.cursorColumn>0?this.cursorColumn--:this.cursorLine>0&&(this.cursorLine--,this.cursorColumn=this.lines[this.cursorLine]?.length||0)}else if(this.cursorColumn>0)this.cursorColumn--;else if(this.cursorLine>0)if(this.cursorLine--,this._getCollapsedClosingLine(this.cursorLine))this.cursorColumn=this.lines[this.cursorLine]?.length||0;else{const e=this._getCollapsedRangeForLine(this.cursorLine);if(e){this.cursorLine=e.startLine;const t=this.lines[this.cursorLine].search(/[{\[]/);this.cursorColumn=t+1}else this.cursorColumn=this.lines[this.cursorLine]?.length||0}this._lastStartIndex=-1,this._scrollToCursor(),this.scheduleRender()}_scrollToCursor(){const e=this.shadowRoot.getElementById("viewport");if(!e)return;const t=this.visibleLines.findIndex(e=>e.index===this.cursorLine);if(-1===t)return;const s=t*this.lineHeight,i=e.scrollTop,n=i+e.clientHeight;s<i?e.scrollTop=s:s+this.lineHeight>n&&(e.scrollTop=s+this.lineHeight-e.clientHeight)}moveCursor(e,t){0!==e?this.moveCursorSkipCollapsed(e):0!==t&&this.moveCursorHorizontal(t)}_handleArrowKey(e,t,s){s&&!this.selectionStart&&(this.selectionStart={line:this.cursorLine,column:this.cursorColumn}),0!==e?this.moveCursorSkipCollapsed(e):0!==t&&this.moveCursorHorizontal(t),s?this.selectionEnd={line:this.cursorLine,column:this.cursorColumn}:(this.selectionStart=null,this.selectionEnd=null)}_handleHomeEnd(e,t,s){t&&!this.selectionStart&&(this.selectionStart={line:this.cursorLine,column:this.cursorColumn}),"home"===e?(s&&(this.cursorLine=s.startLine),this.cursorColumn=0):this.cursorLine<this.lines.length&&(this.cursorColumn=this.lines[this.cursorLine].length),t?this.selectionEnd={line:this.cursorLine,column:this.cursorColumn}:(this.selectionStart=null,this.selectionEnd=null),this._lastStartIndex=-1,this._scrollToCursor(),this.scheduleRender()}_selectAll(){this.selectionStart={line:0,column:0};const e=this.lines.length-1;this.selectionEnd={line:e,column:this.lines[e]?.length||0},this.cursorLine=e,this.cursorColumn=this.lines[e]?.length||0,this._lastStartIndex=-1,this._scrollToCursor(),this.scheduleRender()}_getSelectedText(){if(!this.selectionStart||!this.selectionEnd)return"";const{start:e,end:t}=this._normalizeSelection();if(!e||!t)return"";if(e.line===t.line)return this.lines[e.line].substring(e.column,t.column);let s=this.lines[e.line].substring(e.column)+"\n";for(let i=e.line+1;i<t.line;i++)s+=this.lines[i]+"\n";return s+=this.lines[t.line].substring(0,t.column),s}_normalizeSelection(){if(!this.selectionStart||!this.selectionEnd)return{start:null,end:null};const e=this.selectionStart,t=this.selectionEnd;return e.line<t.line||e.line===t.line&&e.column<=t.column?{start:e,end:t}:{start:t,end:e}}_hasSelection(){return!(!this.selectionStart||!this.selectionEnd||this.selectionStart.line===this.selectionEnd.line&&this.selectionStart.column===this.selectionEnd.column)}_clearSelection(){this.selectionStart=null,this.selectionEnd=null}_deleteSelection(){if(!this._hasSelection())return!1;const{start:e,end:t}=this._normalizeSelection();if(e.line===t.line){const s=this.lines[e.line];this.lines[e.line]=s.substring(0,e.column)+s.substring(t.column)}else{const s=this.lines[e.line].substring(0,e.column),i=this.lines[t.line].substring(t.column);this.lines[e.line]=s+i,this.lines.splice(e.line+1,t.line-e.line)}return this.cursorLine=e.line,this.cursorColumn=e.column,this.selectionStart=null,this.selectionEnd=null,!0}insertText(e){if(this._hasSelection()&&this._deleteSelection(),!this._getCollapsedRangeForLine(this.cursorLine)){if(this._getCollapsedClosingLine(this.cursorLine)){const e=this.lines[this.cursorLine],t=this._getClosingBracketPos(e);if(this.cursorColumn<=t)return}if(this._getCollapsedNodeAtLine(this.cursorLine)){const e=this.lines[this.cursorLine].search(/[{\[]/);if(this.cursorColumn>e)return}if(0===this.lines.length)this.lines=[e],this.cursorColumn=e.length;else if(this.cursorLine<this.lines.length){const t=this.lines[this.cursorLine];this.lines[this.cursorLine]=t.substring(0,this.cursorColumn)+e+t.substring(this.cursorColumn),this.cursorColumn+=e.length}this.formatAndUpdate()}}handlePaste(e){e.preventDefault();const t=e.clipboardData.getData("text/plain");t&&(this.insertText(t),requestAnimationFrame(()=>{this.autoCollapseCoordinates()}))}handleCopy(e){e.preventDefault(),this._hasSelection()?e.clipboardData.setData("text/plain",this._getSelectedText()):e.clipboardData.setData("text/plain",this.getContent())}handleCut(e){e.preventDefault(),this._hasSelection()?(e.clipboardData.setData("text/plain",this._getSelectedText()),this._deleteSelection(),this.formatAndUpdate()):(e.clipboardData.setData("text/plain",this.getContent()),this.lines=[],this.cursorLine=0,this.cursorColumn=0,this.formatAndUpdate())}_getPositionFromClick(e){const t=this.shadowRoot.getElementById("viewport"),s=t.getBoundingClientRect(),i=e.clientY-s.top+t.scrollTop-8,n=e.clientX-s.left-12,o=Math.floor(i/this.lineHeight);let r=0,l=0;if(o>=0&&o<this.visibleLines.length){const e=this.visibleLines[o];r=e.index;const t=this._getCharWidth(),s=Math.round(n/t),i=e.content?.length||0;l=Math.max(0,Math.min(s,i))}return{line:r,column:l}}handleGutterClick(e){const t=e.target.closest(".visibility-button");if(t)this.toggleFeatureVisibility(t.dataset.featureKey);else if(e.target.classList.contains("collapse-button")){const t=e.target.dataset.nodeId;return void this.toggleCollapse(t)}}handleEditorClick(e){const t=e.target.closest(".line.has-visibility");if(t){const s=t.getBoundingClientRect();if(e.clientX-s.left<14)return e.preventDefault(),e.stopPropagation(),void this.toggleFeatureVisibility(t.dataset.featureKey)}if(e.target.classList.contains("json-color")){const t=e.target.getBoundingClientRect(),s=e.clientX-t.left;if(s<0&&s>=-8){e.preventDefault(),e.stopPropagation();const t=e.target.dataset.color,s=e.target.closest(".line");if(s){const i=parseInt(s.dataset.lineIndex),n=this.lines[i].match(/"([\w-]+)"\s*:\s*"#/);n&&this.showColorPicker(e.target,i,t,n[1])}return}}if(e.target.classList.contains("json-boolean")){const t=e.target.getBoundingClientRect(),s=e.clientX-t.left;if(s<0&&s>=-8){e.preventDefault(),e.stopPropagation();const t=e.target.closest(".line");if(t){const e=parseInt(t.dataset.lineIndex),s=this.lines[e].match(/"([\w-]+)"\s*:\s*(true|false)/);if(s){const t="true"===s[2];this.updateBooleanValue(e,!t,s[1])}}return}}}toggleCollapse(e){this.collapsedNodes.has(e)?this.collapsedNodes.delete(e):this.collapsedNodes.add(e),this.updateView(),this._lastStartIndex=-1,this.scheduleRender()}autoCollapseCoordinates(){const e=this._findCollapsibleRanges();for(const t of e)"coordinates"===t.nodeKey&&this.collapsedNodes.add(t.nodeId);this.updateView(),this.scheduleRender()}toggleFeatureVisibility(e){this.hiddenFeatures.has(e)?this.hiddenFeatures.delete(e):this.hiddenFeatures.add(e),this.updateView(),this.scheduleRender(),this.emitChange()}showColorPicker(e,t,s,i){const n=document.querySelector(".geojson-color-picker-anchor");n&&n.remove();const o=document.createElement("div");o.className="geojson-color-picker-anchor";const r=e.getBoundingClientRect();o.style.cssText=`\n position: fixed;\n left: ${r.left-8}px;\n top: ${r.top+r.height}px;\n width: 10px;\n height: 10px;\n z-index: 9998;\n `,document.body.appendChild(o);const l=document.createElement("input");l.type="color",l.value=s,l.className="geojson-color-picker-input",l.style.cssText="\n position: absolute;\n left: 0;\n top: 0;\n width: 10px;\n height: 10px;\n opacity: 0;\n border: none;\n padding: 0;\n cursor: pointer;\n ",o.appendChild(l),l.addEventListener("input",e=>{this.updateColorValue(t,e.target.value,i)});const a=e=>{e.target!==l&&(document.removeEventListener("click",a,!0),o.remove())};l._closeListener=a,setTimeout(()=>{document.addEventListener("click",a,!0)},100),l.focus(),l.click()}updateColorValue(e,t,s){const i=new RegExp(`"${s}"\\s*:\\s*"#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})"`);this.lines[e]=this.lines[e].replace(i,`"${s}": "${t}"`),this.updateView(),this.scheduleRender(),this.emitChange()}updateBooleanValue(e,t,s){const i=new RegExp(`"${s}"\\s*:\\s*(true|false)`);this.lines[e]=this.lines[e].replace(i,`"${s}": ${t}`),this.updateView(),this.scheduleRender(),this.emitChange()}formatAndUpdate(){try{const e="["+this.lines.join("\n")+"]",t=JSON.parse(e),s=JSON.stringify(t,null,2).split("\n");this.lines=s.slice(1,-1)}catch(e){}this.updateModel(),this.scheduleRender(),this.updatePlaceholderVisibility(),this.emitChange()}emitChange(){const e=this.getContent(),t=this.prefix+e+this.suffix;try{let s=JSON.parse(t);this.hiddenFeatures.size>0&&(s.features=s.features.filter(e=>{const t=this._getFeatureKey(e);return!this.hiddenFeatures.has(t)}));const i=this._validateGeoJSON(s);i.length>0?this.dispatchEvent(new CustomEvent("error",{detail:{error:i.join("; "),errors:i,content:e},bubbles:!0,composed:!0})):this.dispatchEvent(new CustomEvent("change",{detail:s,bubbles:!0,composed:!0}))}catch(s){this.dispatchEvent(new CustomEvent("error",{detail:{error:s.message,content:e},bubbles:!0,composed:!0}))}}updateReadonly(){const e=this.shadowRoot.getElementById("hiddenTextarea"),t=this.shadowRoot.getElementById("clearBtn");e&&(e.readOnly=this.readonly),t&&(t.hidden=this.readonly)}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.style.display=this.lines.length>0?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateThemeCSS(){const e=this.getAttribute("dark-selector")||".dark",t=this._parseSelectorToHostRule(e);let s=this.shadowRoot.getElementById("theme-styles");s||(s=document.createElement("style"),s.id="theme-styles",this.shadowRoot.insertBefore(s,this.shadowRoot.firstChild));const i=e=>Object.entries(e).map(([e,t])=>{return`--${s=e,s.replace(/([A-Z])/g,"-$1").toLowerCase()}: ${t};`;var s}).join("\n "),n=i(this.themes.light||{});let o=n?`:host {\n ${n}\n }\n`:"";o+=`${t} {\n ${i({bgColor:"#2b2b2b",textColor:"#a9b7c6",caretColor:"#bbbbbb",gutterBg:"#313335",gutterBorder:"#3c3f41",gutterText:"#606366",jsonKey:"#9876aa",jsonString:"#6a8759",jsonNumber:"#6897bb",jsonBoolean:"#cc7832",jsonNull:"#cc7832",jsonPunct:"#a9b7c6",jsonError:"#ff6b68",controlColor:"#cc7832",controlBg:"#3c3f41",controlBorder:"#5a5a5a",geojsonKey:"#9876aa",geojsonType:"#6a8759",geojsonTypeInvalid:"#ff6b68",jsonKeyInvalid:"#ff6b68",...this.themes.dark})}\n }`,s.textContent=o}_parseSelectorToHostRule(e){return e?e.startsWith(".")&&!e.includes(" ")?`:host(${e})`:`:host-context(${e})`:':host([data-color-scheme="dark"])'}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()}_getFeatureKey(e){if(!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",s=JSON.stringify(e.geometry?.coordinates||[]);let i=0;for(let n=0;n<s.length;n++)i=(i<<5)-i+s.charCodeAt(n),i&=i;return`hash:${t}:${i.toString(36)}`}_countBrackets(e,t){const s="{"===t?"}":"]";let i=0,n=0,o=!1,r=!1;for(const l of e)r?r=!1:"\\"===l&&o?r=!0:'"'!==l?o||(l===t&&i++,l===s&&n++):o=!o;return{open:i,close:n}}_findCollapsibleRanges(){const e=[];for(const[t,s]of this._lineToNodeId){const i=this._nodeIdToLines.get(s);if(!i)continue;const n=this.lines[t];if(!n)continue;const o=n.match(/^\s*"([^"]+)"\s*:\s*([{\[])/),r=!o&&n.match(/^\s*([{\[]),?\s*$/);if(!o&&!r)continue;const l=o?o[2]:r[1];e.push({startLine:i.startLine,endLine:i.endLine,nodeKey:i.nodeKey||(o?o[1]:`__root_${t}`),nodeId:s,openBracket:l,isRootFeature:!!r})}return e.sort((e,t)=>e.startLine-t.startLine),e}_findClosingLine(e,t){let s=1;const i=this.lines[e],n=i.indexOf(t);if(-1!==n){const o=i.substring(n+1),r=this._countBrackets(o,t);if(s+=r.open-r.close,0===s)return e}for(let o=e+1;o<this.lines.length;o++){const e=this._countBrackets(this.lines[o],t);if(s+=e.open-e.close,0===s)return o}return-1}_buildContextMap(){const e=/* @__PURE__ */new Map,t=[];let s=null;for(let i=0;i<this.lines.length;i++){const n=this.lines[i],o=t[t.length-1]?.context||"Feature";e.set(i,o),/"geometry"\s*:/.test(n)?s="geometry":/"properties"\s*:/.test(n)?s="properties":/"features"\s*:/.test(n)&&(s="Feature");const r=(n.match(/\{/g)||[]).length,l=(n.match(/\}/g)||[]).length,a=(n.match(/\[/g)||[]).length,c=(n.match(/\]/g)||[]).length;for(let e=0;e<r+a;e++)t.push({context:s||o,isArray:e>=r}),s=null;for(let e=0;e<l+c&&t.length>0;e++)t.pop()}return e}_highlightSyntax(s,i,n){if(!s)return"";let o=s,r=null;if(n?.collapseButton?.isCollapsed){const e=s.match(/^(\s*"[^"]+"\s*:\s*)([{\[])/),t=!e&&s.match(/^(\s*)([{\[]),?\s*$/);e?(o=e[1]+e[2],r=e[2]):t&&(o=t[1]+t[2],r=t[2])}let l=o.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");if(l=l.replace(/([{}[\],:])/g,'<span class="json-punctuation">$1</span>'),l=l.replace(/"([^"]+)"(<span class="json-punctuation">:<\/span>)/g,(t,s,n)=>"properties"!==i&&e.includes(s)?`<span class="geojson-key">"${s}"</span>${n}`:`<span class="json-key">"${s}"</span>${n}`),"properties"!==i&&(l=l.replace(/<span class="geojson-key">"type"<\/span><span class="json-punctuation">:<\/span>\s*"([^"]*)"/g,(e,s)=>`<span class="geojson-key">"type"</span><span class="json-punctuation">:</span> <span class="${"Feature"===s||"FeatureCollection"===s||t.includes(s)?"geojson-type":"geojson-type-invalid"}">"${s}"</span>`)),l=l.replace(/(<span class="json-punctuation">:<\/span>)\s*"([^"]*)"/g,(e,t,s)=>e.includes("geojson-type")||e.includes("json-string")?e:/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(s)?`${t} <span class="json-string json-color" data-color="${s}" style="--swatch-color: ${s}">"${s}"</span>`:`${t} <span class="json-string">"${s}"</span>`),l=l.replace(/(<span class="json-punctuation">:<\/span>)\s*(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi,'$1 <span class="json-number">$2</span>'),l=l.replace(/(<span class="json-punctuation">[\[,]<\/span>)\s*(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi,'$1<span class="json-number">$2</span>'),l=l.replace(/^(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gim,'$1<span class="json-number">$2</span>'),l=l.replace(/(<span class="json-punctuation">:<\/span>)\s*(true|false)/g,(e,t,s)=>`${t} <span class="json-boolean${"true"===s?" json-bool-true":" json-bool-false"}">${s}</span>`),l=l.replace(/(<span class="json-punctuation">:<\/span>)\s*(null)/g,'$1 <span class="json-null">$2</span>'),r){const e="["===r?"collapsed-bracket-array":"collapsed-bracket-object";l=l.replace(new RegExp(`<span class="json-punctuation">\\${r}<\\/span>$`),`<span class="${e}">${r}</span>`)}return l=l.replace(/(<\/span>|^)([^<]+)(<span|$)/g,(e,t,s,i)=>{if(!s||/^\s*$/.test(s))return e;const n=s.split(/(\s+)/);let o=!1;const r=n.map(e=>/^\s*$/.test(e)?e:(o=!0,`<span class="json-error">${e}</span>`)).join("");return o?t+r+i:e}),l}_validateGeoJSON(e){const s=[];return e.features?(e.features.forEach((e,i)=>{"Feature"!==e.type&&s.push(`features[${i}]: type must be "Feature"`),e.geometry&&e.geometry.type&&(t.includes(e.geometry.type)||s.push(`features[${i}].geometry: invalid type "${e.geometry.type}"`))}),s):s}set(e){if(!Array.isArray(e))throw new Error("set() expects an array");const t=e.map(e=>JSON.stringify(e,null,2)).join(",\n");this.setValue(t)}add(e){const t=this._parseFeatures();t.push(e),this.set(t)}insertAt(e,t){const s=this._parseFeatures(),i=t<0?s.length+t:t;s.splice(Math.max(0,Math.min(i,s.length)),0,e),this.set(s)}removeAt(e){const t=this._parseFeatures(),s=e<0?t.length+e:e;if(s>=0&&s<t.length){const e=t.splice(s,1)[0];return this.set(t),e}}removeAll(){const e=this._parseFeatures();return this.lines=[],this.collapsedNodes.clear(),this.hiddenFeatures.clear(),this.updateModel(),this.scheduleRender(),this.updatePlaceholderVisibility(),this.emitChange(),e}get(e){const t=this._parseFeatures();return t[e<0?t.length+e:e]}getAll(){return this._parseFeatures()}emit(){this.emitChange()}_parseFeatures(){try{const e=this.lines.join("\n");return e.trim()?JSON.parse("["+e+"]"):[]}catch(e){return[]}}}customElements.get("geojson-editor")||customElements.define("geojson-editor",s);export{s as default};
|
|
9
|
+
const e=["type","geometry","properties","coordinates","id","features"],t=["Point","MultiPoint","LineString","MultiLineString","Polygon","MultiPolygon"];class s extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.lines=[],this.collapsedNodes=/* @__PURE__ */new Set,this.hiddenFeatures=/* @__PURE__ */new Set,this._nodeIdCounter=0,this._lineToNodeId=/* @__PURE__ */new Map,this._nodeIdToLines=/* @__PURE__ */new Map,this.visibleLines=[],this.lineMetadata=/* @__PURE__ */new Map,this.featureRanges=/* @__PURE__ */new Map,this.scrollTop=0,this.viewportHeight=0,this.lineHeight=19.5,this.bufferLines=5,this._lastStartIndex=-1,this._lastEndIndex=-1,this._lastTotalLines=-1,this._scrollRaf=null,this.cursorLine=0,this.cursorColumn=0,this.selectionStart=null,this.selectionEnd=null,this.renderTimer=null,this.inputTimer=null,this.themes={dark:{},light:{}}}_generateNodeId(){return"node_"+ ++this._nodeIdCounter}_getCollapsedRangeForLine(e){for(const[t,s]of this._nodeIdToLines)if(this.collapsedNodes.has(t)&&e>s.startLine&&e<s.endLine)return{nodeId:t,...s};return null}_getCollapsedClosingLine(e){for(const[t,s]of this._nodeIdToLines)if(this.collapsedNodes.has(t)&&e===s.endLine)return{nodeId:t,...s};return null}_getClosingBracketPos(e){return Math.max(e.lastIndexOf("]"),e.lastIndexOf("}"))}_getCollapsedNodeAtLine(e){const t=this._lineToNodeId.get(e);return t&&this.collapsedNodes.has(t)?{nodeId:t,...this._nodeIdToLines.get(t)}:null}_getCollapsibleNodeAtLine(e){const t=this._lineToNodeId.get(e);if(t){const e=this._nodeIdToLines.get(t);return{nodeId:t,isCollapsed:this.collapsedNodes.has(t),...e}}return null}_getContainingExpandedNode(e){let t=null;for(const[s,i]of this._nodeIdToLines)this.collapsedNodes.has(s)||e>=i.startLine&&e<=i.endLine&&(!t||i.endLine-i.startLine<t.endLine-t.startLine)&&(t={nodeId:s,...i});return t}_deleteCollapsedNode(e){const t=e.endLine-e.startLine+1;this.lines.splice(e.startLine,t),this.cursorLine=Math.min(e.startLine,this.lines.length-1),this.cursorColumn=0,this.formatAndUpdate()}_rebuildNodeIdMappings(){const e=new Set(this.collapsedNodes),t=/* @__PURE__ */new Map;for(const[n,o]of this._nodeIdToLines)o.nodeKey&&t.set(n,o.nodeKey);const s=[];for(const n of e){const e=t.get(n);e&&s.push(e)}this._nodeIdCounter=0,this._lineToNodeId.clear(),this._nodeIdToLines.clear(),this.collapsedNodes.clear();const i=/* @__PURE__ */new Map;for(let n=0;n<this.lines.length;n++){const e=this.lines[n],t=e.match(/^\s*"([^"]+)"\s*:\s*([{\[])/),o=!t&&e.match(/^\s*([{\[]),?\s*$/);if(!t&&!o)continue;let r,l;t?(r=t[1],l=t[2]):(l=o[1],r=`__root_${l}_${n}`);const a=e.substring(e.indexOf(l)+1),c=this._countBrackets(a,l);if(c.close>c.open)continue;const h=this._findClosingLine(n,l);if(-1===h||h===n)continue;const d=this._generateNodeId();this._lineToNodeId.set(n,d),this._nodeIdToLines.set(d,{startLine:n,endLine:h,nodeKey:r,isRootFeature:!!o});const u=i.get(r)||0;i.set(r,u+1);const p=s.indexOf(r);-1!==p&&(s.splice(p,1),this.collapsedNodes.add(d))}}static get observedAttributes(){return["readonly","value","placeholder","dark-selector"]}connectedCallback(){this.render(),this.setupEventListeners(),this.updatePrefixSuffix(),this.updateThemeCSS(),this.value&&this.setValue(this.value),this.updatePlaceholderVisibility()}disconnectedCallback(){this.renderTimer&&clearTimeout(this.renderTimer),this.inputTimer&&clearTimeout(this.inputTimer);const e=document.querySelector(".geojson-color-picker-input");e&&(e._closeListener&&document.removeEventListener("click",e._closeListener,!0),e.remove())}attributeChangedCallback(e,t,s){if(t!==s)switch(e){case"value":this.setValue(s);break;case"readonly":this.updateReadonly();break;case"placeholder":this.updatePlaceholderContent();break;case"dark-selector":this.updateThemeCSS()}}get readonly(){return this.hasAttribute("readonly")}get value(){return this.getAttribute("value")||""}get placeholder(){return this.getAttribute("placeholder")||""}get prefix(){return'{"type": "FeatureCollection", "features": ['}get suffix(){return"]}"}render(){const e=document.createElement("style");e.textContent=':host *,:host *:before,:host *:after{box-sizing:border-box;font: 13px/1.5 Courier New,Courier,monospace;font-variant:normal;letter-spacing:0;word-spacing:0;text-transform:none;text-decoration:none;text-indent:0}:host{display:flex;flex-direction:column;position:relative;width:100%;height:400px;border-radius:4px;overflow:hidden}:host([readonly]) .editor-wrapper:after{content:"";position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(-45deg,rgba(128,128,128,.08),rgba(128,128,128,.08) 3px,transparent 3px,transparent 12px);z-index:1}:host([readonly]) .hidden-textarea{cursor:text}.editor-wrapper{position:relative;width:100%;flex:1;background:var(--bg-color, #fff);display:flex;overflow:hidden}.gutter{width:50px;background:var(--gutter-bg, #f0f0f0);border-right:1px solid var(--gutter-border, #e0e0e0);overflow:hidden;flex-shrink:0;position:relative}.gutter-scroll{position:absolute;inset:0;overflow:hidden;padding:8px 0}.gutter-scroll-content{position:relative;width:100%}.gutter-content{position:absolute;top:0;left:0;right:0;will-change:transform}.gutter-line{height:19.5px;display:flex;align-items:center;justify-content:flex-end;padding-right:4px}.line-number{font-size:11px;color:var(--gutter-text, #999);-webkit-user-select:none;user-select:none;min-width:20px;text-align:right}.collapse-column{width:16px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.color-indicator,.collapse-button,.boolean-checkbox{width:12px;height:12px;border-radius:2px;cursor:pointer;transition:transform .1s;flex-shrink:0}.color-indicator{border:1px solid #555}.color-indicator:hover{transform:scale(1.2);border-color:#fff}.boolean-checkbox{appearance:none;-webkit-appearance:none;background:transparent;border:1.5px solid var(--control-border, #c0c0c0);border-radius:2px;margin:0;position:relative}.boolean-checkbox:checked{border-color:var(--control-color, #000080)}.boolean-checkbox:checked:after{content:"✔";color:var(--control-color, #000080);font-size:11px;font-weight:700;position:absolute;top:-3px;right:-1px}.boolean-checkbox:hover{transform:scale(1.2);border-color:var(--control-color, #000080)}.collapse-button{background:transparent;border:none;color:var(--json-punct, #a9b7c6);font-size:10px;display:flex;align-items:center;justify-content:center;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .15s}.collapse-button.collapsed,.gutter:hover .collapse-button{opacity:1}.collapse-button:hover{transform:scale(1.2)}@media(hover:none),(pointer:coarse){.collapse-button{opacity:1}}.visibility-button{width:14px;height:14px;background:transparent;color:var(--control-color, #000080);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .1s;flex-shrink:0;opacity:.7;padding:0;font-size:11px}.visibility-button:hover{opacity:1;transform:scale(1.15)}.visibility-button.hidden{opacity:.35}.editor-content{position:relative;flex:1;overflow:hidden}.hidden-textarea{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;padding:0;margin:0;border:none;outline:none;resize:none;overflow:hidden;z-index:-1;pointer-events:none;caret-color:transparent}.viewport{position:absolute;inset:0;overflow:auto;padding:8px 12px;overscroll-behavior:contain}.scroll-content{position:relative;width:100%}.lines-container{position:absolute;top:0;left:0;right:0;will-change:transform}.line{height:19.5px;white-space:pre;display:block;position:relative}.cursor{position:absolute;width:2px;height:1em;background:var(--caret-color, #000);top:.15em;pointer-events:none;animation:cursor-blink 1s step-end infinite}@keyframes cursor-blink{0%,to{opacity:1}50%{opacity:0}}.selection{position:absolute;height:100%;top:0;background:var(--selection-color, rgba(51, 153, 255, .3));pointer-events:none;z-index:0}.line-hidden>span{opacity:.35;filter:grayscale(50%)}.line-collapsed{display:none}.placeholder-layer{position:absolute;top:8px;left:12px;color:#6a6a6a;pointer-events:none;z-index:0;white-space:pre}.json-key{color:var(--json-key, #660e7a)}.json-string{color:var(--json-string, #008000)}.json-number{color:var(--json-number, #00f)}.json-boolean,.json-null{color:var(--json-boolean, #000080)}.json-punctuation{color:var(--json-punct, #000)}.json-key-invalid{color:var(--json-key-invalid, #f00)}.json-error{color:var(--json-error, #f00)}.geojson-key{color:var(--geojson-key, #660e7a);font-weight:600}.geojson-type{color:var(--geojson-type, #008000);font-weight:600}.geojson-type-invalid{color:var(--geojson-type-invalid, #f00);font-weight:600}.d-none{display:none}.collapsed-bracket-array,.collapsed-bracket-object{color:var(--json-punct, #000)}.collapsed-bracket-array:after,.collapsed-bracket-object:after{content:"…";color:var(--json-punct, #888)}.collapsed-content{display:none}.prefix-wrapper,.suffix-wrapper{display:flex;flex-shrink:0;background:var(--bg-color, #fff)}.prefix-gutter,.suffix-gutter{width:50px;background:var(--gutter-bg, #f0f0f0);border-right:1px solid var(--gutter-border, #e0e0e0);flex-shrink:0}.editor-prefix,.editor-suffix{flex:1;padding:4px 12px;color:var(--text-color, #000);background:var(--bg-color, #fff);-webkit-user-select:none;user-select:none;white-space:pre-wrap;word-wrap:break-word;opacity:.6}.prefix-wrapper{border-bottom:1px solid rgba(255,255,255,.1);position:relative}.suffix-wrapper{border-top:1px solid rgba(255,255,255,.1);position:relative}.info-btn{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:transparent;border:none;color:var(--text-color, #000);opacity:.15;cursor:pointer;font-size:.7rem;width:1rem;height:1rem;padding:0;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:opacity .2s;font-family:sans-serif}.info-btn:hover{opacity:.5}.clear-btn{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:transparent;border:none;color:var(--text-color, #000);opacity:.3;cursor:pointer;font-size:.65rem;width:1rem;height:1rem;padding:.15rem 0 0;border-radius:3px;display:flex;align-items:center;justify-content:center;transition:opacity .2s,background .2s}.clear-btn:hover{opacity:.7;background:#ffffff1a}.clear-btn[hidden]{display:none}.viewport::-webkit-scrollbar{width:10px;height:10px}.viewport::-webkit-scrollbar-track{background:var(--control-bg, #e8e8e8)}.viewport::-webkit-scrollbar-thumb{background:var(--control-border, #c0c0c0);border-radius:5px}.viewport::-webkit-scrollbar-thumb:hover{background:var(--control-color, #000080)}.viewport{scrollbar-width:thin;scrollbar-color:var(--control-border, #c0c0c0) var(--control-bg, #e8e8e8)}.line .selected{background:#3399ff4d}.json-color,.json-boolean{position:relative}.json-color:before,.json-boolean:before{content:"";position:absolute;left:-8px;top:50%;transform:translateY(-50%);margin-top:-1px;width:8px;height:8px;border-radius:2px;cursor:pointer}.json-color:hover:before,.json-boolean:hover:before{transform:translateY(-50%) scale(1.2);border-color:var(--control-color, #000080)}.json-color:before{background-color:var(--swatch-color);border:1px solid var(--json-punct, #a9b7c6)}.json-boolean:before{border:1.5px solid var(--control-border, #c0c0c0);background:transparent}.json-bool-true:before{content:"✔";border-color:var(--control-color, #000080);color:var(--control-color, #000080);font-size:8px;display:flex;align-items:center;justify-content:center;line-height:8px}.line.has-visibility{position:relative}.line.has-visibility:before{content:"👁";position:absolute;left:0;top:3px;font-size:10px;color:var(--control-color, #000080);opacity:.6;cursor:pointer;z-index:1}.line.has-visibility:hover:before{opacity:1;transform:scale(1.15)}.line.has-visibility.feature-hidden:before{opacity:.5}';const t=document.createElement("div");for(t.innerHTML=function(e="",t=""){return`\n <div class="prefix-wrapper">\n <div class="prefix-gutter"></div>\n <div class="editor-prefix" id="editorPrefix"></div>\n <button class="info-btn" id="infoBtn" title="@softwarity/geojson-editor v${t}" aria-label="About">ⓘ</button>\n </div>\n <div class="editor-wrapper">\n <div class="gutter">\n <div class="gutter-scroll" id="gutterScroll">\n <div class="gutter-scroll-content" id="gutterScrollContent">\n <div class="gutter-content" id="gutterContent"></div>\n </div>\n </div>\n </div>\n <div class="editor-content">\n <div class="placeholder-layer" id="placeholderLayer">${s=e,s?s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"):""}</div>\n <textarea\n class="hidden-textarea"\n id="hiddenTextarea"\n spellcheck="false"\n autocomplete="off"\n autocorrect="off"\n autocapitalize="off"\n tabindex="0"\n ></textarea>\n <div class="viewport" id="viewport">\n <div class="scroll-content" id="scrollContent">\n <div class="lines-container" id="linesContainer"></div>\n </div>\n </div>\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 `;var s}(this.placeholder,"1.0.14"),this.shadowRoot.innerHTML="",this.shadowRoot.appendChild(e);t.firstChild;)this.shadowRoot.appendChild(t.firstChild)}setupEventListeners(){const e=this.shadowRoot.getElementById("hiddenTextarea"),t=this.shadowRoot.getElementById("viewport"),s=this.shadowRoot.getElementById("gutterContent"),i=this.shadowRoot.querySelector(".gutter"),n=this.shadowRoot.getElementById("clearBtn"),o=this.shadowRoot.querySelector(".editor-wrapper");this._isSelecting=!1,t.addEventListener("click",e=>{this.handleEditorClick(e)},!0),t.addEventListener("mousedown",t=>{const s=t.target.closest(".line.has-visibility");if(s){const e=s.getBoundingClientRect();if(t.clientX-e.left<14)return void(this._blockRender=!0)}if(t.target.classList.contains("json-color")||t.target.classList.contains("json-boolean")){const e=t.target.getBoundingClientRect(),s=t.clientX-e.left;if(s<0&&s>=-8)return void(this._blockRender=!0)}t.preventDefault();const i=this._getPositionFromClick(t);t.shiftKey&&this.selectionStart?(this.selectionEnd=i,this.cursorLine=i.line,this.cursorColumn=i.column):(this.cursorLine=i.line,this.cursorColumn=i.column,this.selectionStart={line:i.line,column:i.column},this.selectionEnd=null,this._isSelecting=!0),e.focus(),this._lastStartIndex=-1,this.scheduleRender()}),t.addEventListener("mousemove",e=>{if(!this._isSelecting)return;const s=this._getPositionFromClick(e);this.selectionEnd=s,this.cursorLine=s.line,this.cursorColumn=s.column;const i=t.getBoundingClientRect();e.clientY<i.top+30?t.scrollTop-=20:e.clientY>i.bottom-30&&(t.scrollTop+=20),this._lastStartIndex=-1,this.scheduleRender()}),document.addEventListener("mouseup",()=>{this._isSelecting=!1}),e.addEventListener("focus",()=>{o.classList.add("focused"),this._lastStartIndex=-1,this.scheduleRender()}),e.addEventListener("blur",()=>{o.classList.remove("focused"),this._lastStartIndex=-1,this.scheduleRender()});let r=!1;t.addEventListener("scroll",()=>{r||(this.scrollTop=t.scrollTop,this.syncGutterScroll(),this._scrollRaf||(this._scrollRaf=requestAnimationFrame(()=>{this._scrollRaf=null,r=!0,this.renderViewport(),r=!1})))}),e.addEventListener("compositionstart",()=>{this._isComposing=!0}),e.addEventListener("compositionend",()=>{this._isComposing=!1,this.handleInput()}),e.addEventListener("input",()=>{this._isComposing||this.handleInput()}),e.addEventListener("keydown",e=>{this.handleKeydown(e)}),e.addEventListener("paste",e=>{this.handlePaste(e)}),e.addEventListener("copy",e=>{this.handleCopy(e)}),e.addEventListener("cut",e=>{this.handleCut(e)}),s.addEventListener("click",e=>{this.handleGutterClick(e)}),i.addEventListener("mousedown",e=>{e.preventDefault()}),i.addEventListener("wheel",e=>{e.preventDefault(),t.scrollTop+=e.deltaY}),n.addEventListener("click",()=>{this.removeAll()}),this.updateReadonly()}setValue(e){if(e&&e.trim())try{const t="["+e+"]",s=JSON.parse(t),i=JSON.stringify(s,null,2).split("\n");this.lines=i.slice(1,-1)}catch(t){this.lines=e.split("\n")}else this.lines=[];this.collapsedNodes.clear(),this.hiddenFeatures.clear(),this._lineToNodeId.clear(),this._nodeIdToLines.clear(),this.cursorLine=0,this.cursorColumn=0,this.updateModel(),this.scheduleRender(),this.updatePlaceholderVisibility(),this.lines.length>0&&requestAnimationFrame(()=>{this.autoCollapseCoordinates()}),this.emitChange()}getContent(){return this.lines.join("\n")}updateModel(){this._rebuildNodeIdMappings(),this.computeFeatureRanges(),this.computeLineMetadata(),this.computeVisibleLines()}updateView(){this.computeLineMetadata(),this.computeVisibleLines()}computeFeatureRanges(){this.featureRanges.clear();try{const e=this.lines.join("\n"),t=this.prefix+e+this.suffix,s=JSON.parse(t);if(!s.features)return;let i=0,n=0,o=!1,r=-1,l=null;for(let a=0;a<this.lines.length;a++){const e=this.lines[a];if(!o&&/"type"\s*:\s*"Feature"/.test(e)){let e=a;for(let t=a;t>=0;t--){const s=this.lines[t].trim();if("{"===s||"{,"===s){e=t;break}}r=e,o=!0,n=1;for(let t=e;t<=a;t++){const s=this._countBrackets(this.lines[t],"{");n+=t===e?s.open-1-s.close:s.open-s.close}i<s.features.length&&(l=this._getFeatureKey(s.features[i]))}else if(o){const t=this._countBrackets(e,"{");n+=t.open-t.close,n<=0&&(l&&this.featureRanges.set(l,{startLine:r,endLine:a,featureIndex:i}),i++,o=!1,l=null)}}}catch(e){}}computeLineMetadata(){this.lineMetadata.clear();const e=this._findCollapsibleRanges();for(let t=0;t<this.lines.length;t++){const s=this.lines[t],i={colors:[],booleans:[],collapseButton:null,visibilityButton:null,isHidden:!1,isCollapsed:!1,featureKey:null},n=/"([\w-]+)"\s*:\s*"(#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}))"/g;let o;for(;null!==(o=n.exec(s));)i.colors.push({attributeName:o[1],color:o[2]});const r=/"([\w-]+)"\s*:\s*(true|false)/g;let l;for(;null!==(l=r.exec(s));)i.booleans.push({attributeName:l[1],value:"true"===l[2]});const a=e.find(e=>e.startLine===t);a&&(i.collapseButton={nodeKey:a.nodeKey,nodeId:a.nodeId,isCollapsed:this.collapsedNodes.has(a.nodeId)}),e.find(e=>this.collapsedNodes.has(e.nodeId)&&t>e.startLine&&t<e.endLine)&&(i.isCollapsed=!0);for(const[e,c]of this.featureRanges)if(t>=c.startLine&&t<=c.endLine){i.featureKey=e,this.hiddenFeatures.has(e)&&(i.isHidden=!0),t===c.startLine&&(i.visibilityButton={featureKey:e,isHidden:this.hiddenFeatures.has(e)});break}this.lineMetadata.set(t,i)}}computeVisibleLines(){this.visibleLines=[];for(let e=0;e<this.lines.length;e++){const t=this.lineMetadata.get(e);t&&t.isCollapsed||this.visibleLines.push({index:e,content:this.lines[e],meta:t})}this._lastStartIndex=-1,this._lastEndIndex=-1,this._lastTotalLines=-1}scheduleRender(){this.renderTimer||(this.renderTimer=requestAnimationFrame(()=>{this.renderTimer=null,this.renderViewport()}))}renderViewport(){if(this._blockRender)return;const e=this.shadowRoot.getElementById("viewport"),t=this.shadowRoot.getElementById("linesContainer"),s=this.shadowRoot.getElementById("scrollContent");if(this.shadowRoot.getElementById("gutterContent"),!e||!t)return;this.viewportHeight=e.clientHeight;const i=this.visibleLines.length,n=i*this.lineHeight;s&&(s.style.height=`${n}px`);const o=e.scrollTop,r=Math.floor(o/this.lineHeight),l=Math.ceil(this.viewportHeight/this.lineHeight),a=Math.max(0,r-this.bufferLines),c=Math.min(i,r+l+this.bufferLines);if(i>0&&this._lastStartIndex===a&&this._lastEndIndex===c&&this._lastTotalLines===i)return;this._lastStartIndex=a,this._lastEndIndex=c,this._lastTotalLines=i;const h=a*this.lineHeight;t.style.transform=`translateY(${h}px)`;const d=this._buildContextMap(),u=this.shadowRoot.querySelector(".editor-wrapper"),p=u?.classList.contains("focused"),g=document.createDocumentFragment();if(0===i){const e=document.createElement("div");return e.className="line empty-line",e.dataset.lineIndex="0",p&&(e.innerHTML=this._insertCursor(0)),g.appendChild(e),t.innerHTML="",t.appendChild(g),void this.renderGutter(0,0)}for(let f=a;f<c;f++){const e=this.visibleLines[f];if(!e)continue;const t=document.createElement("div");t.className="line",t.dataset.lineIndex=e.index,e.meta?.visibilityButton&&(t.classList.add("has-visibility"),t.dataset.featureKey=e.meta.visibilityButton.featureKey,e.meta.visibilityButton.isHidden&&t.classList.add("feature-hidden")),e.meta?.isHidden&&t.classList.add("line-hidden");const s=d.get(e.index);let i=this._highlightSyntax(e.content,s,e.meta);p&&this._hasSelection()&&(i=this._addSelectionHighlight(i,e.index,e.content)),p&&e.index===this.cursorLine&&(i+=this._insertCursor(this.cursorColumn)),t.innerHTML=i,g.appendChild(t)}t.innerHTML="",t.appendChild(g),this.renderGutter(a,c)}_insertCursor(e){return`<span class="cursor" style="left: ${e*this._getCharWidth()}px"></span>`}_addSelectionHighlight(e,t,s){const{start:i,end:n}=this._normalizeSelection();if(!i||!n)return e;if(t<i.line||t>n.line)return e;const o=this._getCharWidth();let r,l;return t===i.line&&t===n.line?(r=i.column,l=n.column):t===i.line?(r=i.column,l=s.length):t===n.line?(r=0,l=n.column):(r=0,l=s.length),`<span class="selection" style="left: ${r*o}px; width: ${(l-r)*o}px"></span>`+e}_getCharWidth(){if(!this._charWidth){const e=document.createElement("canvas").getContext("2d");e.font="13px 'Courier New', Courier, monospace",this._charWidth=e.measureText("M").width}return this._charWidth}renderGutter(e,t){const s=this.shadowRoot.getElementById("gutterContent"),i=this.shadowRoot.getElementById("gutterScrollContent");if(!s)return;const n=this.visibleLines.length*this.lineHeight;i&&(i.style.height=`${n}px`);const o=e*this.lineHeight;s.style.transform=`translateY(${o}px)`;const r=document.createDocumentFragment();for(let l=e;l<t;l++){const e=this.visibleLines[l];if(!e)continue;const t=document.createElement("div");t.className="gutter-line";const s=e.meta,i=document.createElement("span");i.className="line-number",i.textContent=e.index+1,t.appendChild(i);const n=document.createElement("div");if(n.className="collapse-column",s?.collapseButton){const t=document.createElement("div");t.className="collapse-button"+(s.collapseButton.isCollapsed?" collapsed":""),t.textContent=s.collapseButton.isCollapsed?"›":"⌄",t.dataset.line=e.index,t.dataset.nodeId=s.collapseButton.nodeId,t.title=s.collapseButton.isCollapsed?"Expand":"Collapse",n.appendChild(t)}t.appendChild(n),r.appendChild(t)}s.innerHTML="",s.appendChild(r)}syncGutterScroll(){const e=this.shadowRoot.getElementById("gutterScroll"),t=this.shadowRoot.getElementById("viewport");e&&t&&(e.scrollTop=t.scrollTop)}handleInput(){const e=this.shadowRoot.getElementById("hiddenTextarea"),t=e.value;if(t)if(this._getCollapsedRangeForLine(this.cursorLine))e.value="";else{if(this._getCollapsedClosingLine(this.cursorLine)){const t=this.lines[this.cursorLine],s=this._getClosingBracketPos(t);if(this.cursorColumn<=s)return void(e.value="")}if(this._getCollapsedNodeAtLine(this.cursorLine)){const t=this.lines[this.cursorLine].search(/[{\[]/);if(this.cursorColumn>t)return void(e.value="")}if(this.cursorLine<this.lines.length){const e=this.lines[this.cursorLine],s=e.substring(0,this.cursorColumn),i=e.substring(this.cursorColumn),n=t.split("\n");if(1===n.length)this.lines[this.cursorLine]=s+t+i,this.cursorColumn+=t.length;else{this.lines[this.cursorLine]=s+n[0];for(let t=1;t<n.length-1;t++)this.lines.splice(this.cursorLine+t,0,n[t]);const e=n[n.length-1]+i;this.lines.splice(this.cursorLine+n.length-1,0,e),this.cursorLine+=n.length-1,this.cursorColumn=n[n.length-1].length}}else{const e=t.split("\n");this.lines.push(...e),this.cursorLine=this.lines.length-1,this.cursorColumn=this.lines[this.cursorLine].length}e.value="",clearTimeout(this.inputTimer),this.inputTimer=setTimeout(()=>{this.formatAndUpdate()},150)}}handleKeydown(e){const t=this._getCollapsedRangeForLine(this.cursorLine),s=this._getCollapsedNodeAtLine(this.cursorLine),i=this._getCollapsedClosingLine(this.cursorLine);switch(e.key){case"Enter":if(e.preventDefault(),s||t)return;if(i){const e=this.lines[this.cursorLine],t=this._getClosingBracketPos(e);if(t>=0&&this.cursorColumn<=t)return}this.insertNewline();break;case"Backspace":if(e.preventDefault(),this._hasSelection())return this._deleteSelection(),void this.formatAndUpdate();if(i){const e=this.lines[this.cursorLine],t=this._getClosingBracketPos(e);return t>=0&&this.cursorColumn>t+1?void this.deleteBackward():(this.cursorColumn,void this._deleteCollapsedNode(i))}if(s&&0===this.cursorColumn)return void this._deleteCollapsedNode(s);if(t)return;if(s){const e=this.lines[this.cursorLine].search(/[{\[]/);if(this.cursorColumn>e+1)return void this._deleteCollapsedNode(s)}this.deleteBackward();break;case"Delete":if(e.preventDefault(),this._hasSelection())return this._deleteSelection(),void this.formatAndUpdate();if(i){const e=this.lines[this.cursorLine],t=this._getClosingBracketPos(e);return t>=0&&this.cursorColumn>t?void this.deleteForward():void this._deleteCollapsedNode(i)}if(s){const e=this.lines[this.cursorLine].search(/[{\[]/);if(this.cursorColumn>e)return void this._deleteCollapsedNode(s)}if(t)return;this.deleteForward();break;case"ArrowUp":e.preventDefault(),this._handleArrowKey(-1,0,e.shiftKey);break;case"ArrowDown":e.preventDefault(),this._handleArrowKey(1,0,e.shiftKey);break;case"ArrowLeft":e.preventDefault(),this._handleArrowKey(0,-1,e.shiftKey);break;case"ArrowRight":e.preventDefault(),this._handleArrowKey(0,1,e.shiftKey);break;case"Home":e.preventDefault(),this._handleHomeEnd("home",e.shiftKey,i);break;case"End":e.preventDefault(),this._handleHomeEnd("end",e.shiftKey,i);break;case"a":if(e.ctrlKey||e.metaKey)return e.preventDefault(),void this._selectAll();break;case"Tab":if(e.preventDefault(),e.shiftKey){const e=this._getContainingExpandedNode(this.cursorLine);if(e){const t=this.lines[e.startLine],s=t.search(/[{\[]/);this.toggleCollapse(e.nodeId),this.cursorLine=e.startLine,this.cursorColumn=s>=0?s+1:t.length,this._clearSelection(),this._scrollToCursor()}return}if(s)return void this.toggleCollapse(s.nodeId);if(i)return void this.toggleCollapse(i.nodeId);if(t)return}}insertNewline(){if(this.cursorLine<this.lines.length){const e=this.lines[this.cursorLine],t=e.substring(0,this.cursorColumn),s=e.substring(this.cursorColumn);this.lines[this.cursorLine]=t,this.lines.splice(this.cursorLine+1,0,s),this.cursorLine++,this.cursorColumn=0}else this.lines.push(""),this.cursorLine=this.lines.length-1,this.cursorColumn=0;this.formatAndUpdate()}deleteBackward(){if(this.cursorColumn>0){const e=this.lines[this.cursorLine];this.lines[this.cursorLine]=e.substring(0,this.cursorColumn-1)+e.substring(this.cursorColumn),this.cursorColumn--}else if(this.cursorLine>0){const e=this.lines[this.cursorLine],t=this.lines[this.cursorLine-1];this.cursorColumn=t.length,this.lines[this.cursorLine-1]=t+e,this.lines.splice(this.cursorLine,1),this.cursorLine--}this.formatAndUpdate()}deleteForward(){if(this.cursorLine<this.lines.length){const e=this.lines[this.cursorLine];this.cursorColumn<e.length?this.lines[this.cursorLine]=e.substring(0,this.cursorColumn)+e.substring(this.cursorColumn+1):this.cursorLine<this.lines.length-1&&(this.lines[this.cursorLine]=e+this.lines[this.cursorLine+1],this.lines.splice(this.cursorLine+1,1))}this.formatAndUpdate()}moveCursorSkipCollapsed(e){let t=this.cursorLine+e;for(;t>=0&&t<this.lines.length;){const s=this._getCollapsedRangeForLine(t);s&&(t=e>0?s.endLine:s.startLine);break}this.cursorLine=Math.max(0,Math.min(this.lines.length-1,t));const s=this.lines[this.cursorLine]?.length||0;this.cursorColumn=Math.min(this.cursorColumn,s),this._lastStartIndex=-1,this._scrollToCursor(),this.scheduleRender()}moveCursorHorizontal(e){const t=this.lines[this.cursorLine],s=this._getCollapsedNodeAtLine(this.cursorLine),i=this._getCollapsedClosingLine(this.cursorLine);if(e>0)if(i){const e=this._getClosingBracketPos(t);this.cursorColumn<e?this.cursorColumn=e:this.cursorColumn>=t.length?this.cursorLine<this.lines.length-1&&(this.cursorLine++,this.cursorColumn=0):this.cursorColumn++}else if(s){const e=t.search(/[{\[]/);if(this.cursorColumn<e)this.cursorColumn++;else if(this.cursorColumn===e)this.cursorColumn=e+1;else{this.cursorLine=s.endLine;const e=this.lines[this.cursorLine];this.cursorColumn=this._getClosingBracketPos(e)}}else if(this.cursorColumn>=t.length){if(this.cursorLine<this.lines.length-1){this.cursorLine++,this.cursorColumn=0;const e=this._getCollapsedRangeForLine(this.cursorLine);e&&(this.cursorLine=e.endLine,this.cursorColumn=0)}}else this.cursorColumn++;else if(i){const e=this._getClosingBracketPos(t);if(this.cursorColumn>e+1)this.cursorColumn--;else if(this.cursorColumn===e+1){this.cursorLine=i.startLine;const e=this.lines[this.cursorLine].search(/[{\[]/);this.cursorColumn=e+1}else{this.cursorLine=i.startLine;const e=this.lines[this.cursorLine].search(/[{\[]/);this.cursorColumn=e+1}}else if(s){const e=t.search(/[{\[]/);this.cursorColumn>e+1?this.cursorColumn=e+1:this.cursorColumn===e+1?this.cursorColumn=e:this.cursorColumn>0?this.cursorColumn--:this.cursorLine>0&&(this.cursorLine--,this.cursorColumn=this.lines[this.cursorLine]?.length||0)}else if(this.cursorColumn>0)this.cursorColumn--;else if(this.cursorLine>0)if(this.cursorLine--,this._getCollapsedClosingLine(this.cursorLine))this.cursorColumn=this.lines[this.cursorLine]?.length||0;else{const e=this._getCollapsedRangeForLine(this.cursorLine);if(e){this.cursorLine=e.startLine;const t=this.lines[this.cursorLine].search(/[{\[]/);this.cursorColumn=t+1}else this.cursorColumn=this.lines[this.cursorLine]?.length||0}this._lastStartIndex=-1,this._scrollToCursor(),this.scheduleRender()}_scrollToCursor(){const e=this.shadowRoot.getElementById("viewport");if(!e)return;const t=this.visibleLines.findIndex(e=>e.index===this.cursorLine);if(-1===t)return;const s=t*this.lineHeight,i=e.scrollTop,n=i+e.clientHeight;s<i?e.scrollTop=s:s+this.lineHeight>n&&(e.scrollTop=s+this.lineHeight-e.clientHeight)}moveCursor(e,t){0!==e?this.moveCursorSkipCollapsed(e):0!==t&&this.moveCursorHorizontal(t)}_handleArrowKey(e,t,s){s&&!this.selectionStart&&(this.selectionStart={line:this.cursorLine,column:this.cursorColumn}),0!==e?this.moveCursorSkipCollapsed(e):0!==t&&this.moveCursorHorizontal(t),s?this.selectionEnd={line:this.cursorLine,column:this.cursorColumn}:(this.selectionStart=null,this.selectionEnd=null)}_handleHomeEnd(e,t,s){t&&!this.selectionStart&&(this.selectionStart={line:this.cursorLine,column:this.cursorColumn}),"home"===e?(s&&(this.cursorLine=s.startLine),this.cursorColumn=0):this.cursorLine<this.lines.length&&(this.cursorColumn=this.lines[this.cursorLine].length),t?this.selectionEnd={line:this.cursorLine,column:this.cursorColumn}:(this.selectionStart=null,this.selectionEnd=null),this._lastStartIndex=-1,this._scrollToCursor(),this.scheduleRender()}_selectAll(){this.selectionStart={line:0,column:0};const e=this.lines.length-1;this.selectionEnd={line:e,column:this.lines[e]?.length||0},this.cursorLine=e,this.cursorColumn=this.lines[e]?.length||0,this._lastStartIndex=-1,this._scrollToCursor(),this.scheduleRender()}_getSelectedText(){if(!this.selectionStart||!this.selectionEnd)return"";const{start:e,end:t}=this._normalizeSelection();if(!e||!t)return"";if(e.line===t.line)return this.lines[e.line].substring(e.column,t.column);let s=this.lines[e.line].substring(e.column)+"\n";for(let i=e.line+1;i<t.line;i++)s+=this.lines[i]+"\n";return s+=this.lines[t.line].substring(0,t.column),s}_normalizeSelection(){if(!this.selectionStart||!this.selectionEnd)return{start:null,end:null};const e=this.selectionStart,t=this.selectionEnd;return e.line<t.line||e.line===t.line&&e.column<=t.column?{start:e,end:t}:{start:t,end:e}}_hasSelection(){return!(!this.selectionStart||!this.selectionEnd||this.selectionStart.line===this.selectionEnd.line&&this.selectionStart.column===this.selectionEnd.column)}_clearSelection(){this.selectionStart=null,this.selectionEnd=null}_deleteSelection(){if(!this._hasSelection())return!1;const{start:e,end:t}=this._normalizeSelection();if(e.line===t.line){const s=this.lines[e.line];this.lines[e.line]=s.substring(0,e.column)+s.substring(t.column)}else{const s=this.lines[e.line].substring(0,e.column),i=this.lines[t.line].substring(t.column);this.lines[e.line]=s+i,this.lines.splice(e.line+1,t.line-e.line)}return this.cursorLine=e.line,this.cursorColumn=e.column,this.selectionStart=null,this.selectionEnd=null,!0}insertText(e){if(this._hasSelection()&&this._deleteSelection(),!this._getCollapsedRangeForLine(this.cursorLine)){if(this._getCollapsedClosingLine(this.cursorLine)){const e=this.lines[this.cursorLine],t=this._getClosingBracketPos(e);if(this.cursorColumn<=t)return}if(this._getCollapsedNodeAtLine(this.cursorLine)){const e=this.lines[this.cursorLine].search(/[{\[]/);if(this.cursorColumn>e)return}if(0===this.lines.length){const t=e.split("\n");this.lines=t,this.cursorLine=t.length-1,this.cursorColumn=t[t.length-1].length}else if(this.cursorLine<this.lines.length){const t=this.lines[this.cursorLine];this.lines[this.cursorLine]=t.substring(0,this.cursorColumn)+e+t.substring(this.cursorColumn),this.cursorColumn+=e.length}this.formatAndUpdate()}}handlePaste(e){e.preventDefault();const t=e.clipboardData.getData("text/plain");if(t){const e=0===this.lines.length;this.insertText(t),e&&this.lines.length>0&&(this.renderTimer&&(cancelAnimationFrame(this.renderTimer),this.renderTimer=null),this.autoCollapseCoordinates())}}handleCopy(e){e.preventDefault(),this._hasSelection()?e.clipboardData.setData("text/plain",this._getSelectedText()):e.clipboardData.setData("text/plain",this.getContent())}handleCut(e){e.preventDefault(),this._hasSelection()?(e.clipboardData.setData("text/plain",this._getSelectedText()),this._deleteSelection(),this.formatAndUpdate()):(e.clipboardData.setData("text/plain",this.getContent()),this.lines=[],this.cursorLine=0,this.cursorColumn=0,this.formatAndUpdate())}_getPositionFromClick(e){const t=this.shadowRoot.getElementById("viewport"),s=this.shadowRoot.getElementById("linesContainer"),i=t.getBoundingClientRect(),n=e.clientY-i.top+t.scrollTop-8,o=Math.floor(n/this.lineHeight);let r=0,l=0;if(o>=0&&o<this.visibleLines.length){const n=this.visibleLines[o];r=n.index;const a=s?.querySelector(`.line[data-line-index="${n.index}"]`),c=this._getCharWidth();if(a){const t=a.getBoundingClientRect(),s=e.clientX-t.left,i=Math.round(s/c),o=n.content?.length||0;l=Math.max(0,Math.min(i,o))}else{const s=12,o=e.clientX-i.left+t.scrollLeft-s,r=Math.round(o/c),a=n.content?.length||0;l=Math.max(0,Math.min(r,a))}}return{line:r,column:l}}handleGutterClick(e){const t=e.target.closest(".visibility-button");if(t)this.toggleFeatureVisibility(t.dataset.featureKey);else if(e.target.classList.contains("collapse-button")){const t=e.target.dataset.nodeId;return void this.toggleCollapse(t)}}handleEditorClick(e){this._blockRender=!1;const t=e.target.closest(".line.has-visibility");if(t){const s=t.getBoundingClientRect();if(e.clientX-s.left<14){e.preventDefault(),e.stopPropagation();const s=t.dataset.featureKey;return void(s&&this.toggleFeatureVisibility(s))}}if(e.target.classList.contains("json-color")){const t=e.target.getBoundingClientRect(),s=e.clientX-t.left;if(s<0&&s>=-8){e.preventDefault(),e.stopPropagation();const t=e.target.dataset.color,s=e.target.closest(".line");if(s){const i=parseInt(s.dataset.lineIndex),n=this.lines[i].match(/"([\w-]+)"\s*:\s*"#/);n&&this.showColorPicker(e.target,i,t,n[1])}return}}if(e.target.classList.contains("json-boolean")){const t=e.target.getBoundingClientRect(),s=e.clientX-t.left;if(s<0&&s>=-8){e.preventDefault(),e.stopPropagation();const t=e.target.closest(".line");if(t){const e=parseInt(t.dataset.lineIndex),s=this.lines[e].match(/"([\w-]+)"\s*:\s*(true|false)/);if(s){const t="true"===s[2];this.updateBooleanValue(e,!t,s[1])}}return}}}toggleCollapse(e){this.collapsedNodes.has(e)?this.collapsedNodes.delete(e):this.collapsedNodes.add(e),this.updateView(),this._lastStartIndex=-1,this.scheduleRender()}autoCollapseCoordinates(){const e=this._findCollapsibleRanges();for(const t of e)"coordinates"===t.nodeKey&&this.collapsedNodes.add(t.nodeId);this.updateModel(),this.scheduleRender()}toggleFeatureVisibility(e){this.hiddenFeatures.has(e)?this.hiddenFeatures.delete(e):this.hiddenFeatures.add(e),this.updateView(),this.scheduleRender(),this.emitChange()}showColorPicker(e,t,s,i){const n=document.querySelector(".geojson-color-picker-anchor");n&&n.remove();const o=document.createElement("div");o.className="geojson-color-picker-anchor";const r=e.getBoundingClientRect();o.style.cssText=`\n position: fixed;\n left: ${r.left-8}px;\n top: ${r.top+r.height}px;\n width: 10px;\n height: 10px;\n z-index: 9998;\n `,document.body.appendChild(o);const l=document.createElement("input");l.type="color",l.value=s,l.className="geojson-color-picker-input",l.style.cssText="\n position: absolute;\n left: 0;\n top: 0;\n width: 10px;\n height: 10px;\n opacity: 0;\n border: none;\n padding: 0;\n cursor: pointer;\n ",o.appendChild(l),l.addEventListener("input",e=>{this.updateColorValue(t,e.target.value,i)});const a=e=>{e.target!==l&&(document.removeEventListener("click",a,!0),o.remove())};l._closeListener=a,setTimeout(()=>{document.addEventListener("click",a,!0)},100),l.focus(),l.click()}updateColorValue(e,t,s){const i=new RegExp(`"${s}"\\s*:\\s*"#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})"`);this.lines[e]=this.lines[e].replace(i,`"${s}": "${t}"`),this.updateView(),this.scheduleRender(),this.emitChange()}updateBooleanValue(e,t,s){const i=new RegExp(`"${s}"\\s*:\\s*(true|false)`);this.lines[e]=this.lines[e].replace(i,`"${s}": ${t}`),this.updateView(),this.scheduleRender(),this.emitChange()}formatAndUpdate(){try{const e="["+this.lines.join("\n")+"]",t=JSON.parse(e),s=JSON.stringify(t,null,2).split("\n");this.lines=s.slice(1,-1)}catch(e){}this.updateModel(),this.scheduleRender(),this.updatePlaceholderVisibility(),this.emitChange()}emitChange(){const e=this.getContent(),t=this.prefix+e+this.suffix;try{let s=JSON.parse(t);this.hiddenFeatures.size>0&&(s.features=s.features.filter(e=>{const t=this._getFeatureKey(e);return!this.hiddenFeatures.has(t)}));const i=this._validateGeoJSON(s);i.length>0?this.dispatchEvent(new CustomEvent("error",{detail:{error:i.join("; "),errors:i,content:e},bubbles:!0,composed:!0})):this.dispatchEvent(new CustomEvent("change",{detail:s,bubbles:!0,composed:!0}))}catch(s){this.dispatchEvent(new CustomEvent("error",{detail:{error:s.message,content:e},bubbles:!0,composed:!0}))}}updateReadonly(){const e=this.shadowRoot.getElementById("hiddenTextarea"),t=this.shadowRoot.getElementById("clearBtn");e&&(e.readOnly=this.readonly),t&&(t.hidden=this.readonly)}updatePlaceholderVisibility(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.style.display=this.lines.length>0?"none":"block")}updatePlaceholderContent(){const e=this.shadowRoot.getElementById("placeholderLayer");e&&(e.textContent=this.placeholder),this.updatePlaceholderVisibility()}updatePrefixSuffix(){const e=this.shadowRoot.getElementById("editorPrefix"),t=this.shadowRoot.getElementById("editorSuffix");e&&(e.textContent=this.prefix),t&&(t.textContent=this.suffix)}updateThemeCSS(){const e=this.getAttribute("dark-selector")||".dark",t=this._parseSelectorToHostRule(e);let s=this.shadowRoot.getElementById("theme-styles");s||(s=document.createElement("style"),s.id="theme-styles",this.shadowRoot.insertBefore(s,this.shadowRoot.firstChild));const i=e=>Object.entries(e).map(([e,t])=>{return`--${s=e,s.replace(/([A-Z])/g,"-$1").toLowerCase()}: ${t};`;var s}).join("\n "),n=i(this.themes.light||{});let o=n?`:host {\n ${n}\n }\n`:"";o+=`${t} {\n ${i({bgColor:"#2b2b2b",textColor:"#a9b7c6",caretColor:"#bbbbbb",gutterBg:"#313335",gutterBorder:"#3c3f41",gutterText:"#606366",jsonKey:"#9876aa",jsonString:"#6a8759",jsonNumber:"#6897bb",jsonBoolean:"#cc7832",jsonNull:"#cc7832",jsonPunct:"#a9b7c6",jsonError:"#ff6b68",controlColor:"#cc7832",controlBg:"#3c3f41",controlBorder:"#5a5a5a",geojsonKey:"#9876aa",geojsonType:"#6a8759",geojsonTypeInvalid:"#ff6b68",jsonKeyInvalid:"#ff6b68",...this.themes.dark})}\n }`,s.textContent=o}_parseSelectorToHostRule(e){return e?e.startsWith(".")&&!e.includes(" ")?`:host(${e})`:`:host-context(${e})`:':host([data-color-scheme="dark"])'}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()}_getFeatureKey(e){if(!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",s=JSON.stringify(e.geometry?.coordinates||[]);let i=0;for(let n=0;n<s.length;n++)i=(i<<5)-i+s.charCodeAt(n),i&=i;return`hash:${t}:${i.toString(36)}`}_countBrackets(e,t){const s="{"===t?"}":"]";let i=0,n=0,o=!1,r=!1;for(const l of e)r?r=!1:"\\"===l&&o?r=!0:'"'!==l?o||(l===t&&i++,l===s&&n++):o=!o;return{open:i,close:n}}_findCollapsibleRanges(){const e=[];for(const[t,s]of this._lineToNodeId){const i=this._nodeIdToLines.get(s);if(!i)continue;const n=this.lines[t];if(!n)continue;const o=n.match(/^\s*"([^"]+)"\s*:\s*([{\[])/),r=!o&&n.match(/^\s*([{\[]),?\s*$/);if(!o&&!r)continue;const l=o?o[2]:r[1];e.push({startLine:i.startLine,endLine:i.endLine,nodeKey:i.nodeKey||(o?o[1]:`__root_${t}`),nodeId:s,openBracket:l,isRootFeature:!!r})}return e.sort((e,t)=>e.startLine-t.startLine),e}_findClosingLine(e,t){let s=1;const i=this.lines[e],n=i.indexOf(t);if(-1!==n){const o=i.substring(n+1),r=this._countBrackets(o,t);if(s+=r.open-r.close,0===s)return e}for(let o=e+1;o<this.lines.length;o++){const e=this._countBrackets(this.lines[o],t);if(s+=e.open-e.close,0===s)return o}return-1}_buildContextMap(){const e=/* @__PURE__ */new Map,t=[];let s=null;for(let i=0;i<this.lines.length;i++){const n=this.lines[i],o=t[t.length-1]?.context||"Feature";e.set(i,o),/"geometry"\s*:/.test(n)?s="geometry":/"properties"\s*:/.test(n)?s="properties":/"features"\s*:/.test(n)&&(s="Feature");const r=(n.match(/\{/g)||[]).length,l=(n.match(/\}/g)||[]).length,a=(n.match(/\[/g)||[]).length,c=(n.match(/\]/g)||[]).length;for(let e=0;e<r+a;e++)t.push({context:s||o,isArray:e>=r}),s=null;for(let e=0;e<l+c&&t.length>0;e++)t.pop()}return e}_highlightSyntax(s,i,n){if(!s)return"";let o=s,r=null;if(n?.collapseButton?.isCollapsed){const e=s.match(/^(\s*"[^"]+"\s*:\s*)([{\[])/),t=!e&&s.match(/^(\s*)([{\[]),?\s*$/);e?(o=e[1]+e[2],r=e[2]):t&&(o=t[1]+t[2],r=t[2])}let l=o.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");if(l=l.replace(/([{}[\],:])/g,'<span class="json-punctuation">$1</span>'),l=l.replace(/"([^"]+)"(<span class="json-punctuation">:<\/span>)/g,(t,s,n)=>"properties"!==i&&e.includes(s)?`<span class="geojson-key">"${s}"</span>${n}`:`<span class="json-key">"${s}"</span>${n}`),"properties"!==i&&(l=l.replace(/<span class="geojson-key">"type"<\/span><span class="json-punctuation">:<\/span>(\s*)"([^"]*)"/g,(e,s,i)=>`<span class="geojson-key">"type"</span><span class="json-punctuation">:</span>${s}<span class="${"Feature"===i||"FeatureCollection"===i||t.includes(i)?"geojson-type":"geojson-type-invalid"}">"${i}"</span>`)),l=l.replace(/(<span class="json-punctuation">:<\/span>)(\s*)"([^"]*)"/g,(e,t,s,i)=>e.includes("geojson-type")||e.includes("json-string")?e:/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(i)?`${t}${s}<span class="json-string json-color" data-color="${i}" style="--swatch-color: ${i}">"${i}"</span>`:`${t}${s}<span class="json-string">"${i}"</span>`),l=l.replace(/(<span class="json-punctuation">:<\/span>)(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi,'$1$2<span class="json-number">$3</span>'),l=l.replace(/(<span class="json-punctuation">[\[,]<\/span>)(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi,'$1$2<span class="json-number">$3</span>'),l=l.replace(/^(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gim,'$1<span class="json-number">$2</span>'),l=l.replace(/(<span class="json-punctuation">:<\/span>)(\s*)(true|false)/g,(e,t,s,i)=>`${t}${s}<span class="json-boolean${"true"===i?" json-bool-true":" json-bool-false"}">${i}</span>`),l=l.replace(/(<span class="json-punctuation">:<\/span>)(\s*)(null)/g,'$1$2<span class="json-null">$3</span>'),r){const e="["===r?"collapsed-bracket-array":"collapsed-bracket-object";l=l.replace(new RegExp(`<span class="json-punctuation">\\${r}<\\/span>$`),`<span class="${e}">${r}</span>`)}return l=l.replace(/(<\/span>|^)([^<]+)(<span|$)/g,(e,t,s,i)=>{if(!s||/^\s*$/.test(s))return e;const n=s.split(/(\s+)/);let o=!1;const r=n.map(e=>/^\s*$/.test(e)?e:(o=!0,`<span class="json-error">${e}</span>`)).join("");return o?t+r+i:e}),l}_validateGeoJSON(e){const s=[];return e.features?(e.features.forEach((e,i)=>{"Feature"!==e.type&&s.push(`features[${i}]: type must be "Feature"`),e.geometry&&e.geometry.type&&(t.includes(e.geometry.type)||s.push(`features[${i}].geometry: invalid type "${e.geometry.type}"`))}),s):s}set(e){if(!Array.isArray(e))throw new Error("set() expects an array");const t=e.map(e=>JSON.stringify(e,null,2)).join(",\n");this.setValue(t)}add(e){const t=this._parseFeatures();t.push(e),this.set(t)}insertAt(e,t){const s=this._parseFeatures(),i=t<0?s.length+t:t;s.splice(Math.max(0,Math.min(i,s.length)),0,e),this.set(s)}removeAt(e){const t=this._parseFeatures(),s=e<0?t.length+e:e;if(s>=0&&s<t.length){const e=t.splice(s,1)[0];return this.set(t),e}}removeAll(){const e=this._parseFeatures();return this.lines=[],this.collapsedNodes.clear(),this.hiddenFeatures.clear(),this.updateModel(),this.scheduleRender(),this.updatePlaceholderVisibility(),this.emitChange(),e}get(e){const t=this._parseFeatures();return t[e<0?t.length+e:e]}getAll(){return this._parseFeatures()}emit(){this.emitChange()}_parseFeatures(){try{const e=this.lines.join("\n");return e.trim()?JSON.parse("["+e+"]"):[]}catch(e){return[]}}}customElements.get("geojson-editor")||customElements.define("geojson-editor",s);export{s 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.14",
|
|
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
|
@@ -350,17 +350,21 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
350
350
|
const rect = lineEl.getBoundingClientRect();
|
|
351
351
|
const clickX = e.clientX - rect.left;
|
|
352
352
|
if (clickX < 14) {
|
|
353
|
+
// Block render until click is processed to prevent DOM destruction
|
|
354
|
+
this._blockRender = true;
|
|
353
355
|
return;
|
|
354
356
|
}
|
|
355
357
|
}
|
|
356
|
-
|
|
358
|
+
|
|
357
359
|
// Skip if clicking on an inline control pseudo-element (positioned with negative left)
|
|
358
|
-
if (e.target.classList.contains('json-color') ||
|
|
360
|
+
if (e.target.classList.contains('json-color') ||
|
|
359
361
|
e.target.classList.contains('json-boolean')) {
|
|
360
362
|
const rect = e.target.getBoundingClientRect();
|
|
361
363
|
const clickX = e.clientX - rect.left;
|
|
362
364
|
// Pseudo-element is at left: -8px, so clickX will be negative when clicking on it
|
|
363
365
|
if (clickX < 0 && clickX >= -8) {
|
|
366
|
+
// Block render until click is processed to prevent DOM destruction
|
|
367
|
+
this._blockRender = true;
|
|
364
368
|
return;
|
|
365
369
|
}
|
|
366
370
|
}
|
|
@@ -453,8 +457,21 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
453
457
|
}
|
|
454
458
|
});
|
|
455
459
|
|
|
460
|
+
// Composition handling for international keyboards (dead keys)
|
|
461
|
+
hiddenTextarea.addEventListener('compositionstart', () => {
|
|
462
|
+
this._isComposing = true;
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
hiddenTextarea.addEventListener('compositionend', () => {
|
|
466
|
+
this._isComposing = false;
|
|
467
|
+
// Process the final composed text
|
|
468
|
+
this.handleInput();
|
|
469
|
+
});
|
|
470
|
+
|
|
456
471
|
// Input handling (hidden textarea)
|
|
457
472
|
hiddenTextarea.addEventListener('input', () => {
|
|
473
|
+
// Skip input during composition (dead keys on international keyboards)
|
|
474
|
+
if (this._isComposing) return;
|
|
458
475
|
this.handleInput();
|
|
459
476
|
});
|
|
460
477
|
|
|
@@ -756,11 +773,15 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
756
773
|
}
|
|
757
774
|
|
|
758
775
|
renderViewport() {
|
|
776
|
+
// Skip render if blocked (during inline control click to prevent DOM destruction)
|
|
777
|
+
if (this._blockRender) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
759
780
|
const viewport = this.shadowRoot.getElementById('viewport');
|
|
760
781
|
const linesContainer = this.shadowRoot.getElementById('linesContainer');
|
|
761
782
|
const scrollContent = this.shadowRoot.getElementById('scrollContent');
|
|
762
783
|
const gutterContent = this.shadowRoot.getElementById('gutterContent');
|
|
763
|
-
|
|
784
|
+
|
|
764
785
|
if (!viewport || !linesContainer) return;
|
|
765
786
|
|
|
766
787
|
this.viewportHeight = viewport.clientHeight;
|
|
@@ -923,7 +944,8 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
923
944
|
if (!this._charWidth) {
|
|
924
945
|
const canvas = document.createElement('canvas');
|
|
925
946
|
const ctx = canvas.getContext('2d');
|
|
926
|
-
|
|
947
|
+
// Use exact same font as CSS: 'Courier New', Courier, monospace at 13px
|
|
948
|
+
ctx.font = "13px 'Courier New', Courier, monospace";
|
|
927
949
|
this._charWidth = ctx.measureText('M').width;
|
|
928
950
|
}
|
|
929
951
|
return this._charWidth;
|
|
@@ -1668,8 +1690,11 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1668
1690
|
|
|
1669
1691
|
// Handle empty editor case
|
|
1670
1692
|
if (this.lines.length === 0) {
|
|
1671
|
-
|
|
1672
|
-
|
|
1693
|
+
// Split text by newlines to properly handle multi-line paste
|
|
1694
|
+
const textLines = text.split('\n');
|
|
1695
|
+
this.lines = textLines;
|
|
1696
|
+
this.cursorLine = textLines.length - 1;
|
|
1697
|
+
this.cursorColumn = textLines[textLines.length - 1].length;
|
|
1673
1698
|
} else if (this.cursorLine < this.lines.length) {
|
|
1674
1699
|
const line = this.lines[this.cursorLine];
|
|
1675
1700
|
this.lines[this.cursorLine] = line.substring(0, this.cursorColumn) + text + line.substring(this.cursorColumn);
|
|
@@ -1682,11 +1707,17 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1682
1707
|
e.preventDefault();
|
|
1683
1708
|
const text = e.clipboardData.getData('text/plain');
|
|
1684
1709
|
if (text) {
|
|
1710
|
+
const wasEmpty = this.lines.length === 0;
|
|
1685
1711
|
this.insertText(text);
|
|
1686
|
-
// Auto-collapse coordinates after pasting
|
|
1687
|
-
|
|
1712
|
+
// Auto-collapse coordinates after pasting into empty editor
|
|
1713
|
+
if (wasEmpty && this.lines.length > 0) {
|
|
1714
|
+
// Cancel pending render, collapse first, then render once
|
|
1715
|
+
if (this.renderTimer) {
|
|
1716
|
+
cancelAnimationFrame(this.renderTimer);
|
|
1717
|
+
this.renderTimer = null;
|
|
1718
|
+
}
|
|
1688
1719
|
this.autoCollapseCoordinates();
|
|
1689
|
-
}
|
|
1720
|
+
}
|
|
1690
1721
|
}
|
|
1691
1722
|
}
|
|
1692
1723
|
|
|
@@ -1721,29 +1752,42 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1721
1752
|
*/
|
|
1722
1753
|
_getPositionFromClick(e) {
|
|
1723
1754
|
const viewport = this.shadowRoot.getElementById('viewport');
|
|
1755
|
+
const linesContainer = this.shadowRoot.getElementById('linesContainer');
|
|
1724
1756
|
const rect = viewport.getBoundingClientRect();
|
|
1725
|
-
|
|
1757
|
+
|
|
1726
1758
|
const paddingTop = 8;
|
|
1727
|
-
|
|
1728
|
-
|
|
1759
|
+
|
|
1729
1760
|
const y = e.clientY - rect.top + viewport.scrollTop - paddingTop;
|
|
1730
|
-
const x = e.clientX - rect.left - paddingLeft;
|
|
1731
|
-
|
|
1732
1761
|
const visibleLineIndex = Math.floor(y / this.lineHeight);
|
|
1733
|
-
|
|
1762
|
+
|
|
1734
1763
|
let line = 0;
|
|
1735
1764
|
let column = 0;
|
|
1736
|
-
|
|
1765
|
+
|
|
1737
1766
|
if (visibleLineIndex >= 0 && visibleLineIndex < this.visibleLines.length) {
|
|
1738
1767
|
const lineData = this.visibleLines[visibleLineIndex];
|
|
1739
1768
|
line = lineData.index;
|
|
1740
|
-
|
|
1769
|
+
|
|
1770
|
+
// Get actual line element to calculate column position accurately
|
|
1771
|
+
const lineEl = linesContainer?.querySelector(`.line[data-line-index="${lineData.index}"]`);
|
|
1741
1772
|
const charWidth = this._getCharWidth();
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1773
|
+
|
|
1774
|
+
if (lineEl) {
|
|
1775
|
+
// Use line element's actual position for accurate column calculation
|
|
1776
|
+
const lineRect = lineEl.getBoundingClientRect();
|
|
1777
|
+
const clickRelativeToLine = e.clientX - lineRect.left;
|
|
1778
|
+
const rawColumn = Math.round(clickRelativeToLine / charWidth);
|
|
1779
|
+
const lineLength = lineData.content?.length || 0;
|
|
1780
|
+
column = Math.max(0, Math.min(rawColumn, lineLength));
|
|
1781
|
+
} else {
|
|
1782
|
+
// Fallback to padding-based calculation if line element not found
|
|
1783
|
+
const paddingLeft = 12;
|
|
1784
|
+
const x = e.clientX - rect.left + viewport.scrollLeft - paddingLeft;
|
|
1785
|
+
const rawColumn = Math.round(x / charWidth);
|
|
1786
|
+
const lineLength = lineData.content?.length || 0;
|
|
1787
|
+
column = Math.max(0, Math.min(rawColumn, lineLength));
|
|
1788
|
+
}
|
|
1745
1789
|
}
|
|
1746
|
-
|
|
1790
|
+
|
|
1747
1791
|
return { line, column };
|
|
1748
1792
|
}
|
|
1749
1793
|
|
|
@@ -1766,16 +1810,21 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1766
1810
|
}
|
|
1767
1811
|
|
|
1768
1812
|
handleEditorClick(e) {
|
|
1813
|
+
// Unblock render now that click is being processed
|
|
1814
|
+
this._blockRender = false;
|
|
1815
|
+
|
|
1769
1816
|
// Line-level visibility button (pseudo-element ::before on .line.has-visibility)
|
|
1770
1817
|
const lineEl = e.target.closest('.line.has-visibility');
|
|
1771
1818
|
if (lineEl) {
|
|
1772
1819
|
const rect = lineEl.getBoundingClientRect();
|
|
1773
1820
|
const clickX = e.clientX - rect.left;
|
|
1774
|
-
// Pseudo-element is at the start of the line, check first ~14px
|
|
1775
1821
|
if (clickX < 14) {
|
|
1776
1822
|
e.preventDefault();
|
|
1777
1823
|
e.stopPropagation();
|
|
1778
|
-
|
|
1824
|
+
const featureKey = lineEl.dataset.featureKey;
|
|
1825
|
+
if (featureKey) {
|
|
1826
|
+
this.toggleFeatureVisibility(featureKey);
|
|
1827
|
+
}
|
|
1779
1828
|
return;
|
|
1780
1829
|
}
|
|
1781
1830
|
}
|
|
@@ -1842,27 +1891,28 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
1842
1891
|
|
|
1843
1892
|
autoCollapseCoordinates() {
|
|
1844
1893
|
const ranges = this._findCollapsibleRanges();
|
|
1845
|
-
|
|
1894
|
+
|
|
1846
1895
|
for (const range of ranges) {
|
|
1847
1896
|
if (range.nodeKey === 'coordinates') {
|
|
1848
1897
|
this.collapsedNodes.add(range.nodeId);
|
|
1849
1898
|
}
|
|
1850
1899
|
}
|
|
1851
|
-
|
|
1852
|
-
//
|
|
1853
|
-
|
|
1900
|
+
|
|
1901
|
+
// Rebuild everything to ensure consistent state after collapse changes
|
|
1902
|
+
// This is especially important after paste into empty editor
|
|
1903
|
+
this.updateModel();
|
|
1854
1904
|
this.scheduleRender();
|
|
1855
1905
|
}
|
|
1856
1906
|
|
|
1857
1907
|
// ========== Feature Visibility ==========
|
|
1858
|
-
|
|
1908
|
+
|
|
1859
1909
|
toggleFeatureVisibility(featureKey) {
|
|
1860
1910
|
if (this.hiddenFeatures.has(featureKey)) {
|
|
1861
1911
|
this.hiddenFeatures.delete(featureKey);
|
|
1862
1912
|
} else {
|
|
1863
1913
|
this.hiddenFeatures.add(featureKey);
|
|
1864
1914
|
}
|
|
1865
|
-
|
|
1915
|
+
|
|
1866
1916
|
// Use updateView - content didn't change, just visibility
|
|
1867
1917
|
this.updateView();
|
|
1868
1918
|
this.scheduleRender();
|
|
@@ -2292,64 +2342,69 @@ class GeoJsonEditor extends HTMLElement {
|
|
|
2292
2342
|
});
|
|
2293
2343
|
|
|
2294
2344
|
// Type values - "type": "Value" - but NOT inside properties context
|
|
2345
|
+
// IMPORTANT: Preserve original spacing by capturing and re-emitting whitespace
|
|
2295
2346
|
if (context !== 'properties') {
|
|
2296
2347
|
result = result.replace(
|
|
2297
|
-
/<span class="geojson-key">"type"<\/span><span class="json-punctuation">:<\/span
|
|
2298
|
-
(match, type) => {
|
|
2348
|
+
/<span class="geojson-key">"type"<\/span><span class="json-punctuation">:<\/span>(\s*)"([^"]*)"/g,
|
|
2349
|
+
(match, space, type) => {
|
|
2299
2350
|
const isValid = type === 'Feature' || type === 'FeatureCollection' || GEOMETRY_TYPES.includes(type);
|
|
2300
2351
|
const cls = isValid ? 'geojson-type' : 'geojson-type-invalid';
|
|
2301
|
-
return `<span class="geojson-key">"type"</span><span class="json-punctuation">:</span
|
|
2352
|
+
return `<span class="geojson-key">"type"</span><span class="json-punctuation">:</span>${space}<span class="${cls}">"${type}"</span>`;
|
|
2302
2353
|
}
|
|
2303
2354
|
);
|
|
2304
2355
|
}
|
|
2305
|
-
|
|
2356
|
+
|
|
2306
2357
|
// String values (not already wrapped in spans)
|
|
2358
|
+
// IMPORTANT: Preserve original spacing by capturing and re-emitting whitespace
|
|
2307
2359
|
result = result.replace(
|
|
2308
|
-
/(<span class="json-punctuation">:<\/span>)\s*"([^"]*)"/g,
|
|
2309
|
-
(match, colon, val) => {
|
|
2360
|
+
/(<span class="json-punctuation">:<\/span>)(\s*)"([^"]*)"/g,
|
|
2361
|
+
(match, colon, space, val) => {
|
|
2310
2362
|
// Don't double-wrap if already has a span after colon
|
|
2311
2363
|
if (match.includes('geojson-type') || match.includes('json-string')) return match;
|
|
2312
|
-
|
|
2364
|
+
|
|
2313
2365
|
// Check if it's a color value (hex) - use ::before for swatch via CSS class
|
|
2314
2366
|
if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(val)) {
|
|
2315
|
-
return `${colon}
|
|
2367
|
+
return `${colon}${space}<span class="json-string json-color" data-color="${val}" style="--swatch-color: ${val}">"${val}"</span>`;
|
|
2316
2368
|
}
|
|
2317
|
-
|
|
2318
|
-
return `${colon}
|
|
2369
|
+
|
|
2370
|
+
return `${colon}${space}<span class="json-string">"${val}"</span>`;
|
|
2319
2371
|
}
|
|
2320
2372
|
);
|
|
2321
|
-
|
|
2373
|
+
|
|
2322
2374
|
// Numbers after colon
|
|
2375
|
+
// IMPORTANT: Preserve original spacing by capturing and re-emitting whitespace
|
|
2323
2376
|
result = result.replace(
|
|
2324
|
-
/(<span class="json-punctuation">:<\/span>)\s*(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi,
|
|
2325
|
-
'$1
|
|
2377
|
+
/(<span class="json-punctuation">:<\/span>)(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi,
|
|
2378
|
+
'$1$2<span class="json-number">$3</span>'
|
|
2326
2379
|
);
|
|
2327
|
-
|
|
2380
|
+
|
|
2328
2381
|
// Numbers in arrays (after [ or ,)
|
|
2329
2382
|
result = result.replace(
|
|
2330
|
-
/(<span class="json-punctuation">[\[,]<\/span>)\s*(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi,
|
|
2331
|
-
'$1<span class="json-number">$
|
|
2383
|
+
/(<span class="json-punctuation">[\[,]<\/span>)(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gi,
|
|
2384
|
+
'$1$2<span class="json-number">$3</span>'
|
|
2332
2385
|
);
|
|
2333
|
-
|
|
2386
|
+
|
|
2334
2387
|
// Standalone numbers at start of line (coordinates arrays)
|
|
2335
2388
|
result = result.replace(
|
|
2336
2389
|
/^(\s*)(-?\d+\.?\d*(?:e[+-]?\d+)?)/gim,
|
|
2337
2390
|
'$1<span class="json-number">$2</span>'
|
|
2338
2391
|
);
|
|
2339
|
-
|
|
2392
|
+
|
|
2340
2393
|
// Booleans - use ::before for checkbox via CSS class
|
|
2394
|
+
// IMPORTANT: Preserve original spacing by capturing and re-emitting whitespace
|
|
2341
2395
|
result = result.replace(
|
|
2342
|
-
/(<span class="json-punctuation">:<\/span>)\s*(true|false)/g,
|
|
2343
|
-
(match, colon, val) => {
|
|
2396
|
+
/(<span class="json-punctuation">:<\/span>)(\s*)(true|false)/g,
|
|
2397
|
+
(match, colon, space, val) => {
|
|
2344
2398
|
const checkedClass = val === 'true' ? ' json-bool-true' : ' json-bool-false';
|
|
2345
|
-
return `${colon}
|
|
2399
|
+
return `${colon}${space}<span class="json-boolean${checkedClass}">${val}</span>`;
|
|
2346
2400
|
}
|
|
2347
2401
|
);
|
|
2348
|
-
|
|
2402
|
+
|
|
2349
2403
|
// Null
|
|
2404
|
+
// IMPORTANT: Preserve original spacing by capturing and re-emitting whitespace
|
|
2350
2405
|
result = result.replace(
|
|
2351
|
-
/(<span class="json-punctuation">:<\/span>)\s*(null)/g,
|
|
2352
|
-
'$1
|
|
2406
|
+
/(<span class="json-punctuation">:<\/span>)(\s*)(null)/g,
|
|
2407
|
+
'$1$2<span class="json-null">$3</span>'
|
|
2353
2408
|
);
|
|
2354
2409
|
|
|
2355
2410
|
// Collapsed bracket indicator - just add the class, CSS ::after adds the "...]" or "...}"
|