bracelet

GDPR Cookie Consent with Real Cookie Banner /* ============================================================= KJ — Birth Chart Generator (Universal, Clean + Stable) - OSM place search (portal + mini fallback) - Timezone-by-place-and-date → UT (fixes DST / wrong ASC) - Swiss Ephemeris bootstrap (WASM) with safe wrappers - Bright wheel, colored planet glyphs with anti-overlap stacking - Aspect lines on wheel + responsive aspects table - Export composite PNG (wheel + table) and email sender - Elementor-safe wiring; single consent mover (no duplicates) ============================================================= */ /* ---------- Paths (update if you moved files) ---------- */ const JS_DIRECT = "/wp-content/kjahli/swisseph-kjahli.js"; const WASM_DIRECT = "/wp-content/kjahli/swisseph-kjahli.wasm"; const DATA_DIRECT = "/wp-content/kjahli/swisseph-kjahli.data"; const OSM_PROXY = "/wp-content/kjahli/osm-proxy.php"; /* ---------- UI helpers ---------- */ const $ = sel => document.querySelector(sel); const logBox = $("#kj-log"); const log = (m)=>{ if(logBox){ logBox.textContent += m+"\n"; logBox.scrollTop = logBox.scrollHeight; } console.log(m); }; /* ---------- Colors / glyphs ---------- */ const PLANET_COLORS = { Sun:"#f6c200", Moon:"#cccccc", Mercury:"#00a2ff", Venus:"#e75480", Mars:"#d62828", Jupiter:"#ff7f0e", Saturn:"#8d6e63", Uranus:"#29b6f6", Neptune:"#4b70dd", Pluto:"#9c27b0", "True Node":"#2e7d32", Chiron:"#607d8b" }; window.PLANET_GLYPH = { Sun:"☉", Moon:"☾", Mercury:"☿", Venus:"♀", Mars:"♂", Jupiter:"♃", Saturn:"♄", Uranus:"♅", Neptune:"♆", Pluto:"♇", "True Node":"☊", Chiron:"⚷" }; const SIGNS = ["\u2648\uFE0E","\u2649\uFE0E","\u264A\uFE0E","\u264B\uFE0E","\u264C\uFE0E","\u264D\uFE0E","\u264E\uFE0E","\u264F\uFE0E","\u2650\uFE0E","\u2651\uFE0E","\u2652\uFE0E","\u2653\uFE0E"]; /* ---------- Aspect matcher (shared by wheel + exporter) ---------- */ (function exposeAspectMatcher(){ const _norm = d => ((d%360)+360)%360; const _sep = (a,b)=>{ let d=Math.abs(_norm(a)-_norm(b)); return d>180?360-d:d; }; const ASPECTS = [ {key:"conj", angle: 0, orb:8, color:"#999999", width:1.8, sym:"☌"}, {key:"opp", angle:180, orb:8, color:"#e53935", width:2.4, sym:"☍"}, {key:"tri", angle:120, orb:6, color:"#1e88e5", width:2.0, sym:"△"}, {key:"sqr", angle: 90, orb:6, color:"#e53935", width:2.0, sym:"□"}, {key:"sex", angle: 60, orb:4, color:"#1e88e5", width:1.8, sym:"✶"}, ]; window.matchAspect = (a1,a2)=>{ const s=_sep(a1,a2); for(const a of ASPECTS){ if(Math.abs(s-a.angle)<=a.orb) return a; } return null; }; })(); /* ===================================================== TIMEZONE: Convert local birth time (place+date) to UTC ===================================================== */ async function getOffsetMinutesByLatLonAtLocalTime(lat, lon, localISO){ // TimeAPI.io (free, no key) try{ const u = `https://timeapi.io/api/TimeZone/coordinate?latitude=${lat}&longitude=${lon}`; const r = await fetch(u); if(r.ok){ const j = await r.json(); if(j?.timeZone){ const tz = j.timeZone; const off = offsetMinutesForZoneAtLocal(tz, localISO); if(Number.isFinite(off)) return off; } } }catch(_){/* ignore */} // worldtimeapi fallback — pick a plausible zone for AU/region try{ const zones = await (await fetch("https://worldtimeapi.org/api/timezone")).json(); const guess = zones.find(z=>/Australia\//.test(z)) || zones.find(z=>/Etc\/GMT/.test(z)); if(guess){ const off = offsetMinutesForZoneAtLocal(guess, localISO); if(Number.isFinite(off)) return off; } }catch(_){/* ignore */} // Browser zone as last meaningful fallback try{ const z = Intl.DateTimeFormat().resolvedOptions().timeZone; const off = offsetMinutesForZoneAtLocal(z, localISO); if(Number.isFinite(off)) return off; }catch(_){/* ignore */} log("⚠️ Timezone lookup failed — using UTC offset 0"); return 0; } function offsetMinutesForZoneAtLocal(timeZone, localISO){ try{ const dtf = new Intl.DateTimeFormat('en-US', { timeZone, hour12:false, year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit' }); const [dPart,tPart] = localISO.split("T"); const [y,m,d] = dPart.split("-").map(Number); const [H,Mi] = (tPart||"00:00").split(":").map(Number); const utcGuess = Date.UTC(y, m-1, d, H, Mi, 0); const parts = dtf.formatToParts(new Date(utcGuess)); const get = (type)=>+(parts.find(p=>p.type===type)?.value||"0"); const y2=get('year'), M2=get('month'), d2=get('day'), H2=get('hour'), m2=get('minute'), s2=get('second'); const backUTC = Date.UTC(y2, M2-1, d2, H2, m2, s2); return (utcGuess - backUTC)/60000; // minutes }catch(e){ log("TZ calc error: "+(e.message||e)); return NaN; } } function localToUTC_JD(y,m,d,H,M, lat, lon){ const iso = `${String(y).padStart(4,"0")}-${String(m).padStart(2,"0")}-${String(d).padStart(2,"0")}T${String(H).padStart(2,"0")}:${String(M).padStart(2,"0")}`; return getOffsetMinutesByLatLonAtLocalTime(lat, lon, iso).then(offMin=> (H + M/60) - (offMin/60)); } /* ===================================================== Swiss Ephemeris bootstrap ===================================================== */ let SwissephFactory=null, __se=null; (async()=>{ try{ const mod=await import(JS_DIRECT); SwissephFactory=mod?.default||mod; log("🟣 Module attached"); }catch(e){ log("❌ ESM import failed: "+(e.message||e)); } })(); async function ensureSwiss(){ if(__se) return __se; if(typeof SwissephFactory!=="function") throw new Error("Swiss factory not found"); const wasmURL=WASM_DIRECT+"?v="+Date.now(); const dataURL=DATA_DIRECT+"?v="+Date.now(); async function fetchInstantiateWasm(imports){ const r=await fetch(wasmURL,{cache:"no-store"}); if(!r.ok) throw new Error("WASM HTTP "+r.status); const buf=await r.arrayBuffer(); const {instance}=await WebAssembly.instantiate(buf,imports); return instance; } const modOpts={ locateFile:(p)=> p.endsWith(".wasm")?wasmURL : p.endsWith(".data")?dataURL : p, instantiateWasm:(imports,cb)=>{fetchInstantiateWasm(imports).then(i=>cb(i));return{};}, print:(m)=>log(String(m)), printErr:(m)=>log("ERR:"+String(m)), monitorRunDependencies:(n)=>log("⏳ runDeps: "+n) }; let mod = SwissephFactory(modOpts); if(mod && typeof mod.then==="function") mod = await mod; // Wrap low-level exports if needed (Emscripten variations) if(typeof mod.swe_julday!=="function" && typeof mod._swe_julday==="function"){ const readCString=(ptr,max=512)=>{const heap=mod.HEAPU8;let end=ptr,stop=ptr+max;while(endmod._swe_julday(y|0,m|0,d|0,+h,g|0); mod.swe_calc_ut=(jd,ipl,iflag)=>{const px=mod._malloc(24),serr=mod._malloc(256);const rc=mod._swe_calc_ut(+jd,ipl|0,iflag|0,px,serr);const out={rc,x:[NaN,NaN,NaN],serr:""};if(rc>=0){out.x=[mod.HEAPF64[px/8],mod.HEAPF64[px/8+1],mod.HEAPF64[px/8+2]];}else{out.serr=readCString(serr,256);}mod._free(px);mod._free(serr);return out;}; const _housesFn = mod._swe_houses_ex || mod._swe_houses_ex2; if(typeof _housesFn==="function"){ mod.swe_houses_ex=(jd,lat,lon,hsys)=>{const cusp=mod._malloc(13*8),ascm=mod._malloc(10*8);_housesFn(+jd,0,+lat,+lon,hsys.charCodeAt(0),cusp,ascm);const out={cusp:[],ascmc:[]};for(let i=0;i<13;i++) out.cusp[i]=mod.HEAPF64[cusp/8+i];for(let i=0;i<10;i++) out.ascmc[i]=mod.HEAPF64[ascm/8+i];mod._free(cusp);mod._free(ascm);return out;}; } if(typeof mod.SE_GREG_CAL==="undefined") mod.SE_GREG_CAL=1; log("ℹ️ Using wrapped C exports"); } if(typeof mod.swe_julday!=="function") throw new Error("Swiss Ephemeris did not initialise."); __se=mod; log("✅ Swiss Ephemeris ready"); return mod; } /* ===================================================== Wheel drawing (bright) + aspects on top ===================================================== */ function drawWheel(container,{asc,houses,planets}){ container.innerHTML=""; const size=640,cx=size/2,cy=size/2; const rOuter=270, rBandO=260, rBandI=228, rInner=180; const rChord=rInner-14, rGlyph=(rBandI+rInner)/2, rSignGlyph=(rBandO+rBandI)/2; const SVGNS="http://www.w3.org/2000/svg"; const mk=(n)=>document.createElementNS(SVGNS,n); const polar=(r,deg)=>{const a=(deg-90)*Math.PI/180; return {x:cx+r*Math.cos(a), y:cy+r*Math.sin(a)};}; const text=(x,y,val,fs,fill)=>{const t=mk("text");t.setAttribute("x",x);t.setAttribute("y",y);t.setAttribute("fill",fill); t.setAttribute("font-size",fs);t.setAttribute("font-family","Noto Sans Symbols 2, DejaVu Sans, Arial Unicode MS, sans-serif"); t.setAttribute("font-weight","600");t.setAttribute("text-anchor","middle");t.setAttribute("dominant-baseline","middle");t.textContent=val;return t;}; const line=(x1,y1,x2,y2,stroke,sw,op=1)=>{const l=mk("line");l.setAttribute("x1",x1);l.setAttribute("y1",y1);l.setAttribute("x2",x2);l.setAttribute("y2",y2); l.setAttribute("stroke",stroke);l.setAttribute("stroke-width",sw);l.setAttribute("stroke-linecap","round");l.setAttribute("opacity",op);return l;}; const svg=mk("svg"); svg.classList.add("kj-wheel"); svg.setAttribute("viewBox",`0 0 ${size} ${size}`); // Outer rim + sign band slices const rimOuter=mk("circle"); rimOuter.setAttribute("cx",cx);rimOuter.setAttribute("cy",cy);rimOuter.setAttribute("r",rOuter); rimOuter.setAttribute("fill","#fff"); rimOuter.setAttribute("stroke","#fff"); rimOuter.setAttribute("stroke-width","26"); svg.appendChild(rimOuter); const bandRing=mk("circle"); bandRing.setAttribute("cx",cx);bandRing.setAttribute("cy",cy);bandRing.setAttribute("r",rBandO+1); bandRing.setAttribute("fill","none"); bandRing.setAttribute("stroke","#222"); bandRing.setAttribute("stroke-width","2"); svg.appendChild(bandRing); const signBandFill="#4a4a4f", signSep="#2c2c30"; const toXY=(r,deg)=>{const a=(deg-90)*Math.PI/180;return[cx+r*Math.cos(a),cy+r*Math.sin(a)];}; for(let i=0;i<12;i++){ const start=i*30, end=start+30; const [x1,y1]=toXY(rBandO,start), [x2,y2]=toXY(rBandO,end), [x3,y3]=toXY(rBandI,end), [x4,y4]=toXY(rBandI,start); const p=mk("path"); p.setAttribute("d",`M ${x1} ${y1} A ${rBandO} ${rBandO} 0 0 1 ${x2} ${y2} L ${x3} ${y3} A ${rBandI} ${rBandI} 0 0 0 ${x4} ${y4} Z`); p.setAttribute("fill",signBandFill); svg.appendChild(p); const s1=polar(rBandO,start), s2=polar(rBandI,start); svg.appendChild(line(s1.x,s1.y,s2.x,s2.y,signSep,2,0.9)); } const bandInner=mk("circle"); bandInner.setAttribute("cx",cx);bandInner.setAttribute("cy",cy);bandInner.setAttribute("r",rBandI); bandInner.setAttribute("fill","none"); bandInner.setAttribute("stroke","#1f1f22"); bandInner.setAttribute("stroke-width","2"); svg.appendChild(bandInner); // Inner white wheel const inner=mk("circle"); inner.setAttribute("cx",cx);inner.setAttribute("cy",cy);inner.setAttribute("r",rInner); inner.setAttribute("fill","#ffffff"); inner.setAttribute("stroke","#d9dbe2"); inner.setAttribute("stroke-width","1.5"); svg.appendChild(inner); // House cusps (Asc highlighted) const cusps=(houses?.cusp?.length>=13)?houses.cusp:Array.from({length:13},(_,i)=>i*30); for(let i=1;i<=12;i++){ const a=cusps[i] ?? (i*30); const p1=polar(rInner,a), p2=polar(70,a); const isAsc=(i===1); svg.appendChild(line(p1.x,p1.y,p2.x,p2.y, isAsc? "#7c3aed":"#c8cbd5", isAsc?2.2:1.2, isAsc?1:.95)); } // House numbers for(let i=0;i<12;i++){ const mid=i*30+15; const pos=polar(100,mid); svg.appendChild(text(pos.x,pos.y,String(i+1),12,"#8a8f99")); } // Sign glyphs (not purple) for(let i=0;i<12;i++){ const pos=polar(rSignGlyph,i*30+15); svg.appendChild(text(pos.x,pos.y,SIGNS[i],19,"#f2f3f6")); } // Aspect lines if(Array.isArray(planets) && planets.length>1 && typeof window.matchAspect==="function"){ for(let i=0;ia.lon-b.lon); let cur = [sorted[0]]; for(let i=1;i{ const k = group.length; const start = -(k-1)/2; for(let i=0;i{ const th=document.createElement("th"); th.innerHTML=(window.PLANET_GLYPH[p.name]||"•")+" "+p.name; th.style.padding="8px"; th.style.borderBottom="1px solid #e6e6e6"; trh.appendChild(th); }); thead.appendChild(trh); const tbody=document.createElement("tbody"); for(let r=0;r=r){ td.textContent="—"; td.style.color="#bbb"; } else{ const asp=window.matchAspect(planets[r].lon, planets[c].lon); if(asp){ td.textContent=asp.sym; td.style.color = (asp.key==="conj") ? "#666" : ((asp.key==="opp"||asp.key==="sqr") ? "#e53935" : "#1e88e5"); td.style.fontWeight="700"; }else{ td.textContent="·"; td.style.color="#bbb"; } } tr.appendChild(td); } tbody.appendChild(tr); } tbl.appendChild(thead); tbl.appendChild(tbody); } /* ===================================================== Generate & Clear ===================================================== */ const _norm = d => ((d%360)+360)%360; async function generate(){ log("=== Generating chart ==="); try{ const se = await ensureSwiss(); const date = $("#kj-date")?.value; const time = $("#kj-time")?.value; const c = $("#kj-coords")?.value; const hsys = $("#kj-house")?.value || "P"; if(!date||!time||!c){ log("❌ Fill date, time, and place"); alert("Please fill date, time, and pick a place."); return; } const [y,m,d] = date.split("-").map(n=>parseInt(n,10)); const [H,Mi] = (time||"0:0").split(":").map(n=>parseInt(n,10)); const [lat,lon]= c.split(",").map(v=>parseFloat(v.trim())); // Convert local time (at place) to UT hour const utHour = await localToUTC_JD(y,m,d,H||0,Mi||0, lat, lon); const jd = se.swe_julday(y,m,d, utHour, se.SE_GREG_CAL); log("JD (UT): "+jd.toFixed(6)); // Planets const PID={Sun:0,Moon:1,Mercury:2,Venus:3,Mars:4,Jupiter:5,Saturn:6,Uranus:7,Neptune:8,Pluto:9,"True Node":11,Chiron:15}; const planets=[]; for(const [name,id] of Object.entries(PID)){ const r=se.swe_calc_ut(jd,id,0); if(r?.x){ planets.push({name,lon:_norm(r.x[0])}); } } // Houses & ASC (Swiss expects geo lon East+) const hs = se.swe_houses_ex(jd, lat, lon, hsys); const asc = hs.ascmc?.[0]; // Publish window.LAST_PLANETS = planets; window.LAST_ASC = asc; window.LAST_HOUSES = hs; drawWheel($("#kj-chart"), {asc, houses:hs, planets}); renderAspectsTable(planets); }catch(e){ log("❌ Chart error: "+(e.message||e)); alert("Chart error: "+(e.message||e)); } } function clearAll(){ ["#kj-date","#kj-time","#kj-place","#kj-coords"].forEach(sel=>{ const el=$(sel); if(el) el.value=""; }); if(logBox) logBox.textContent=""; const chart=$("#kj-chart"); if(chart) chart.innerHTML="The wheel will render here."; const tbl=$("#kj-aspects-table"); if(tbl) tbl.innerHTML=""; } /* Wire buttons (survive Elementor re-render) */ function wireButtons(){ $("#kj-go")?.addEventListener("click", generate); $("#kj-clear")?.addEventListener("click", clearAll); } document.addEventListener("DOMContentLoaded", wireButtons); new MutationObserver(wireButtons).observe(document.documentElement,{childList:true,subtree:true}); /* ===================================================== Exporter (wheel + full aspects table) & Email sender ===================================================== */ window.ensureCanvg = window.ensureCanvg || (async function ensureCanvgSafe(){ if (window.Canvg && typeof window.Canvg.from === "function") return window.Canvg; const sources = [ "/wp-content/kjahli/canvg.min.js", "https://cdn.jsdelivr.net/npm/canvg@3/dist/browser/canvg.min.js", "https://unpkg.com/canvg@3/dist/browser/canvg.min.js", "https://cdnjs.cloudflare.com/ajax/libs/canvg/3.0.10/umd/canvg.min.js" ]; for(const src of sources){ try{ await new Promise((res,rej)=>{ const s=document.createElement("script"); s.src=src; s.async=true; s.onload=res; s.onerror=()=>rej(); document.head.appendChild(s); }); if(window.Canvg && typeof window.Canvg.from==="function") return window.Canvg; }catch(_){/* try next */} } return null; // no throw }); window.exportCompositePngDataUrl = async function exportCompositePngDataUrl(){ const wheelSVG = document.querySelector('#kj-chart svg.kj-wheel'); if(!wheelSVG) throw new Error("No chart found. Generate a chart first."); const svgString = new XMLSerializer().serializeToString(wheelSVG); const wheelSize = 1100; // Rasterize wheel const wheelCanvas = document.createElement('canvas'); wheelCanvas.width = wheelSize; wheelCanvas.height = wheelSize; const wCtx = wheelCanvas.getContext('2d'); wCtx.fillStyle = '#ffffff'; wCtx.fillRect(0,0,wheelSize,wheelSize); try{ const CanvgRef = await window.ensureCanvg(); if(CanvgRef){ const v = await CanvgRef.from(wCtx, svgString); await v.render(); } else{ const svg64 = btoa(unescape(encodeURIComponent(svgString))); const img = new Image(); await new Promise((res,rej)=>{ img.onload=res; img.onerror=()=>rej(new Error("SVG rasterize failed")); img.src='data:image/svg+xml;base64,'+svg64; }); wCtx.drawImage(img, 0, 0, wheelSize, wheelSize); } }catch(_){ const svg64 = btoa(unescape(encodeURIComponent(svgString))); const img = new Image(); await new Promise((res,rej)=>{ img.onload=res; img.onerror=()=>rej(new Error("SVG rasterize failed")); img.src='data:image/svg+xml;base64,'+svg64; }); wCtx.drawImage(img, 0, 0, wheelSize, wheelSize); } // Table data const P = Array.isArray(window.LAST_PLANETS) ? window.LAST_PLANETS : []; const W = 1200, topPad=40, leftPad=40, rightPad=40, gap=28, bottomPad=36; const headerH=36, rowH=28; const cols = P.length ? P.length+1 : 0; const colW = cols ? Math.floor((W-leftPad-rightPad)/cols) : 0; const tableH = P.length ? (headerH + rowH*P.length + 16) : 0; const H = topPad + wheelSize + (P.length ? gap + tableH : 0) + bottomPad; const canvas = document.createElement('canvas'); canvas.width = W; canvas.height = H; const ctx = canvas.getContext('2d'); // BG ctx.fillStyle="#ffffff"; ctx.fillRect(0,0,W,H); // Wheel centered const wx = Math.round((W-wheelSize)/2), wy = topPad; ctx.drawImage(wheelCanvas, wx, wy, wheelSize, wheelSize); if(P.length){ const x0=leftPad, y0=wy+wheelSize+gap; // header ctx.fillStyle="#f6f7fb"; ctx.fillRect(x0,y0, colW*cols, headerH); ctx.strokeStyle="#e6e6e6"; ctx.lineWidth=1; ctx.fillStyle="#222"; ctx.font="600 14px system-ui, -apple-system, Segoe UI, Arial"; // corner ctx.strokeRect(x0,y0,colW,headerH); for(let c=0;c=r){ ctx.fillStyle="#bbb"; ctx.fillText('—', x+colW/2-4, y+19); } else{ const asp = window.matchAspect(P[r].lon, P[c].lon); if(asp){ ctx.fillStyle=(asp.key==="conj")?"#666":((asp.key==="opp"||asp.key==="sqr")?"#e53935":"#1e88e5"); ctx.fillText(asp.sym, x+colW/2-6, y+19); } else { ctx.fillStyle="#bbb"; ctx.fillText('·', x+colW/2-2, y+19); } } } } } return canvas.toDataURL('image/png'); }; (function wireEmailButton(){ document.addEventListener("click", async (e)=>{ const btn = e.target.closest && e.target.closest("#kj-send"); if(!btn) return; e.preventDefault(); const email = ($("#kj-email")?.value||"").trim(); if(!email){ alert("Please enter your email."); return; } if(!window.kjAjax?.url || !window.kjAjax?.nonce){ alert("Email service not booted. Add the [kj_ajax_boot] shortcode on this page."); return; } if(!document.querySelector('#kj-chart svg.kj-wheel')){ alert("Please generate a chart first."); return; } try{ const pngDataUrl = await window.exportCompositePngDataUrl(); const date = $("#kj-date")?.value || ''; const time = $("#kj-time")?.value || ''; const place= $("#kj-place")?.value || ''; const consent = !!$("#kj-consent")?.checked; const meta = `DOB: ${date} ${time}
Place: ${place}`; const fd = new FormData(); fd.append('action','kj_send_chart'); fd.append('nonce', window.kjAjax.nonce); fd.append('email', email); fd.append('consent', consent ? '1' : '0'); fd.append('meta', meta); fd.append('png', pngDataUrl); const res = await fetch(window.kjAjax.url, { method:'POST', body: fd, credentials:'same-origin' }); const j = await res.json(); if(!j?.success) throw new Error((j?.data?.message||j?.message)||('HTTP '+res.status)); alert("Chart sent! Check your inbox."); }catch(err){ console.error(err); alert("Sorry — could not send image: "+(err.message||err)); } }); })(); /* ===================================================== OSM place search (portal, bright, self-healing) ===================================================== */ (function OSMv3(){ const PROXY = OSM_PROXY || "/wp-content/kjahli/osm-proxy.php"; const LIMIT = 8; // Badge (status) let badge = document.getElementById("kj-badge"); if(!badge){ badge = document.createElement("div"); badge.id = "kj-badge"; Object.assign(badge.style,{ position:"fixed",right:"10px",bottom:"10px",zIndex:2147483647, background:"#111",color:"#fff",font:"12px/1.3 system-ui", padding:"8px 10px",borderRadius:"8px",boxShadow:"0 6px 16px rgba(0,0,0,.35)", pointerEvents:'none' // never blocks other UI }); badge.textContent = "OSM: booting…"; (document.readyState==="loading" ? document.addEventListener("DOMContentLoaded",()=>document.body.appendChild(badge)) : document.body.appendChild(badge)); } const setBadge = t => { try{ badge.style.display="block"; badge.textContent=t; }catch(_){} }; // Styles for portal if(!document.getElementById("kj-osm-portal-style")){ const st=document.createElement("style"); st.id="kj-osm-portal-style"; st.textContent = ` #kj-osm-portal{position:fixed;display:none;background:#fff;color:#111;border:1px solid #ccc;border-radius:10px; max-height:260px;overflow:auto;padding:4px;box-shadow:0 10px 30px rgba(0,0,0,.2);z-index:2147483600} #kj-osm-portal .itm{padding:8px 10px;border-radius:6px;cursor:pointer;color:#111;font-size:14px} #kj-osm-portal .itm:hover,#kj-osm-portal .itm.active{background:#e8f0fe;color:#000} `; document.head.appendChild(st); } // Portal let portal = document.getElementById("kj-osm-portal"); if(!portal){ portal=document.createElement("div"); portal.id="kj-osm-portal"; document.body.appendChild(portal); } function findInput(){ return document.getElementById("kj-place") || document.querySelector('input#kj-place') || document.querySelector('input[placeholder*="Type 3"][type="text"]') || document.querySelector('input[placeholder*="letters"][type="text"]') || document.querySelector('input[placeholder*="Place"][type="text"]') || null; } function findCoords(){ return document.getElementById("kj-coords") || document.querySelector('input#kj-coords') || document.querySelector('input[readonly]') || null; } let state={input:null,coords:null,items:[],sel:-1,lastId:0,debounce:null}; function ensurePos(){ if(!state.input) return; const r=state.input.getBoundingClientRect(); portal.style.left=r.left+"px"; portal.style.top=(r.bottom+6)+"px"; portal.style.width=r.width+"px"; } function show(){ ensurePos(); portal.style.display="block"; } function hide(){ portal.style.display="none"; portal.innerHTML=""; state.sel=-1; state.items=[]; } function unbind(prev){ if(!prev) return; prev.onfocus=prev.oninput=prev.onkeydown=null; } function bind(input, coords){ if(!input) return; if(state.input===input) return; unbind(state.input); state={...state,input,coords,items:[],sel:-1,lastId:0}; document.addEventListener("pointerdown",(e)=>{ if(e.target===state.input || portal.contains(e.target)) return; hide(); }); window.addEventListener("scroll",()=>{ if(portal.style.display==="block") ensurePos(); }, true); window.addEventListener("resize",()=>{ if(portal.style.display==="block") ensurePos(); }); state.input.onfocus=()=>{ if(state.input.value.trim().length>=3 && state.items.length) show(); }; state.input.oninput=()=>{ const q=state.input.value.trim(); if(q.length<3){ hide(); return; } clearTimeout(state.debounce); const id=++state.lastId; state.debounce=setTimeout(()=>search(q,id),240); }; state.input.onkeydown=(e)=>{ if(portal.style.display!=="block"||!state.items.length) return; if(e.key==="ArrowDown"){ e.preventDefault(); state.sel=(state.sel+1)%state.items.length; updateSel(); } else if(e.key==="ArrowUp"){ e.preventDefault(); state.sel=(state.sel-1+state.items.length)%state.items.length; updateSel(); } else if(e.key==="Enter"){ e.preventDefault(); pick(state.items[state.sel>=0?state.sel:0]); } else if(e.key==="Escape"){ hide(); } }; setBadge("OSM: ready — type a place"); } function updateSel(){ [...portal.children].forEach((el,i)=>el.classList.toggle("active", i===state.sel)); } function pick(p){ const name=p.display_name||p.name||"[no name]"; state.input.value=name; if(state.coords) state.coords.value=`${p.lat},${p.lon}`; setBadge("OSM: picked "+(state.coords?state.coords.value:"(no coords field)")); hide(); state.input.blur(); } async function search(q,id){ try{ setBadge('OSM: searching… "'+q+'"'); let data=null; try{ const r=await fetch(`${PROXY}?q=${encodeURIComponent(q)}&limit=${LIMIT}&v=${Date.now()}`,{cache:"no-store"}); if(r.ok) data=await r.json(); else setBadge("OSM: proxy "+r.status+" (fallback)"); }catch(_){ setBadge("OSM: proxy error (fallback)"); } if(!Array.isArray(data)){ const rr=await fetch(`https://nominatim.openstreetmap.org/search?format=jsonv2&q=${encodeURIComponent(q)}&limit=${LIMIT}`); if(rr.ok) data=await rr.json(); else setBadge("OSM: public "+rr.status); } if(id!==state.lastId) return; render(Array.isArray(data)?data:[]); }catch(e){ setBadge("OSM: error "+(e.message||e)); hide(); } } function render(results){ portal.innerHTML=""; state.items=results; if(!state.items.length){ hide(); setBadge("OSM: 0 results"); return; } for(const p of state.items){ const row=document.createElement("div"); row.className="itm"; row.textContent=p.display_name||p.name||"[no name]"; row.addEventListener("pointerdown",(e)=>{ e.preventDefault(); pick(p); }); portal.appendChild(row); } state.sel=-1; updateSel(); ensurePos(); show(); setBadge(`OSM: rendered ${state.items.length} items`); } // Poll + observe + watchdog function poll(ms=250,max=160){ let tries=0; const t=setInterval(()=>{ const inp=findInput(), crd=findCoords(); tries++; if(inp){ clearInterval(t); bind(inp, crd); } else if(tries%10===0){ setBadge("OSM: waiting… ("+tries+")"); } if(tries>max){ clearInterval(t); setBadge("OSM: gave up waiting"); } },ms); } const mo=new MutationObserver(()=>{ const inp=findInput(), crd=findCoords(); if(inp && state.input!==inp) bind(inp, crd); }); mo.observe(document.documentElement,{childList:true,subtree:true}); setInterval(()=>{ const inp=findInput(), crd=findCoords(); if(inp && state.input!==inp) bind(inp, crd); }, 2000); poll(); })(); /* ===================================================== Consent checkbox: move directly under email (single source of truth) ===================================================== */ (function placeConsentUnderEmail(){ const MSG_TEXT = 'Send me this chart and add me to your newsletter (optional).'; function moveOnce(){ const email = document.getElementById('kj-email'); const cb = document.getElementById('kj-consent'); if(!email || !cb) return false; // Prefer a wrapping label; else build one so tap target stays big let label = cb.closest('label'); if(!label){ label = document.createElement('label'); label.style.display = 'flex'; label.style.alignItems = 'center'; label.style.gap = '8px'; label.appendChild(cb); const span = document.createElement('span'); span.id = 'kj-consent-msg'; span.textContent = MSG_TEXT; label.appendChild(span); }else{ if(!label.querySelector('#kj-consent-msg')){ const span = document.createElement('span'); span.id = 'kj-consent-msg'; span.textContent = MSG_TEXT; label.appendChild(span); } } const emailBlock = email.closest('.elementor-field-group') || email.parentElement; if(!emailBlock) return false; let wrap = document.getElementById('kj-consent-wrap'); if(!wrap){ wrap = document.createElement('div'); wrap.id = 'kj-consent-wrap'; } Object.assign(wrap.style,{ marginTop:'8px', display:'flex', alignItems:'center', gap:'10px', font:'14px/1.4 system-ui,-apple-system,Segoe UI,Arial', color:'#e9e9ee' }); cb.style.width = cb.style.height = '18px'; cb.style.flex = '0 0 auto'; wrap.innerHTML = ''; wrap.appendChild(label); emailBlock.insertAdjacentElement('afterend', wrap); return true; } const tryMove = ()=>{ try{ moveOnce(); }catch(_){ /* noop */ } }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tryMove); else tryMove(); new MutationObserver(tryMove).observe(document.body,{childList:true,subtree:true}); setTimeout(tryMove, 400); setTimeout(tryMove, 900); setTimeout(tryMove, 1600); })(); /* ===================================================== Mini OSM fallback — binds only if portal is not active ===================================================== */ (function miniOSMKeepAlive(){ const PROXY = "/wp-content/kjahli/osm-proxy.php"; let bound = false, tId = null; function ensureStyles(){ if(document.getElementById('kj-mini-osm-style')) return; const s=document.createElement('style'); s.id='kj-mini-osm-style'; s.textContent = ` #kj-mini-osm{position:absolute;left:0;right:0;top:100%;margin-top:6px;background:#fff;color:#111; border:1px solid #ccc;border-radius:10px;max-height:260px;overflow:auto;padding:4px;box-shadow:0 10px 30px rgba(0,0,0,.2);z-index:2147483600;display:none} #kj-mini-osm .itm{padding:8px 10px;border-radius:6px;cursor:pointer} #kj-mini-osm .itm:hover{background:#e8f0fe} .kj-mini-wrap{position:relative} `; document.head.appendChild(s); } function bind(){ if(bound) return; const input = document.getElementById('kj-place'); const coords = document.getElementById('kj-coords'); if(!input) return; // If the big portal is present and visible, do nothing const portal = document.getElementById('kj-osm-portal'); if(portal && portal.style.display !== 'none') return; ensureStyles(); if(!input.parentElement.classList.contains('kj-mini-wrap')){ input.parentElement.classList.add('kj-mini-wrap'); const dd = document.createElement('div'); dd.id='kj-mini-osm'; input.parentElement.appendChild(dd); } const list = document.getElementById('kj-mini-osm'); function show(){ list.style.display='block'; } function hide(){ list.style.display='none'; list.innerHTML=''; } function render(items){ list.innerHTML=''; if(!items.length){ hide(); return; } items.forEach(p=>{ const row=document.createElement('div'); row.className='itm'; row.textContent = p.display_name || p.name || '[no name]'; row.onclick = ()=>{ input.value = row.textContent; if(coords) coords.value = `${p.lat},${p.lon}`; hide(); input.blur(); }; list.appendChild(row); }); show(); } let lastId=0, deb=null; input.addEventListener('input', ()=>{ const q=input.value.trim(); if(q.length<3){ hide(); return; } const id=++lastId; clearTimeout(deb); deb=setTimeout(async ()=>{ try{ const u = `${PROXY}?q=${encodeURIComponent(q)}&limit=8&v=${Date.now()}`; let data=null; try{ const r=await fetch(u,{cache:'no-store'}); if(r.ok) data=await r.json(); }catch(_){/* ignore */} if(!Array.isArray(data)){ const rr=await fetch(`https://nominatim.openstreetmap.org/search?format=jsonv2&q=${encodeURIComponent(q)}&limit=8`); if(rr.ok) data=await rr.json(); } if(id!==lastId) return; render(Array.isArray(data)?data:[]); }catch(_){ hide(); } }, 250); }); input.addEventListener('keydown', (e)=>{ if(e.key==='Escape') hide(); }); document.addEventListener('pointerdown', (e)=>{ if(e.target!==input && !list.contains(e.target)) hide(); }, {capture:true}); bound = true; } function tick(){ try{ bind(); }catch(_){/* noop */} tId = setTimeout(tick, 1000); } tick(); })(); /* ===================================================== Mobile/UI tweaks ===================================================== */ (function injectMobileCSS(){ if(document.getElementById('kj-mobile-fixes')) return; const st = document.createElement('style'); st.id='kj-mobile-fixes'; st.textContent = ` .kj-bc{ max-width:680px; margin:0 auto; } #kj-chart{ min-height: 40px; } #kj-aspects-table{ overflow:auto; max-width:100%; } /* Ensure overlays never block menu/toggles unless visible */ #kj-osm-portal,[id='kj-mini-osm']{ pointer-events:auto; } #kj-badge{ pointer-events:none; } /* Form label contrast on dark */ .kj-bc label, .kj-bc small, #kj-consent-wrap{ color:#e9e9ee; } `; document.head.appendChild(st); })();
0
YOUR CART
  • No products in the cart.