Forest Ultrasonic Diffuser

$69.95

3 in stock

Description

Forest Ultrasonic Diffuser for essential oils.

About the product

Essential Oil Diffuser: essential oil diffuser is an unique upgraded version, made from Iron Art and premium plastic. It is more durable than other plastic diffuser.

Quiet Ultrasonic Operation: Adopted ultrasonic technology, aromatherapy oil diffuser produce a great smooth mist to purified and humidify air. Whisper-quiet operation won’t interfere your sleep quality.

Waterless Shut-off: Aroma diffusers for essential oils with 100ML capacity, the oil diffuser humidifier can last up to 3-6hrs, the oil diffuser will shut-off auto when runs out of water.

7 Colors Night Light: This essential oil diffuser has 7 changing color light, each colors can be changed or fixed, creating a calm and soft atmosphere when you are working, reading, studying or sleeping. Long press light button to turn off the light.

Features: Essential oil diffuser is good ideal for home, office, bedroom, living room, study, yoga, office, spa, etc.Turn water or essential oil into an ultra-fine mist to make the air fresh and clean.Fresh fragrance and soft light to create a relaxing environment.

 

Specifications:

– Water tank capacity:100 mL

– Noise level: 35dB

-Pattern:Tree/Butterfly/Deer&Squirrel&Owl&Bird

-Size: 6.1 inchx3.66 inch

– Material: PP/Carved iron/Shading paper

– Input power: AC 100-240V 50/ 60Hz

– Advanced ultrasound technology:2.4MHz

 

Note:

-Oil is not included in the package.

-Clean the atomizer regularly.Please clean the diffuser at least once a week and keep it dry when not in use.

-Please don’t touch the internal ultrasonic wave vibration board inside of the water tank directly with your hands.

– In case of a short circuit please remove the diffuser from the mains.

 

Package:

– 1 x Aromatherapy Diffuser

– 1 x Power cord (60inch)

– 1 x Warranty card

– 1 x User manual

 

KjahliKjahliKjahliKjahliKjahliKjahliKjahliKjahli

 

 

 

 

 

 

 

 

10% off,
especially for you
🎁

Sign up to receive your exclusive discount, and keep up to date on our latest products & offers!

We don’t spam! Read our privacy policy for more info.

A promotional image for the Kjahlil Cosmic Circle shows a starry clock, celestial art prints, a candle, an astrological cube, and a star-patterned card on a purple gradient background with text inviting subscription for mystical gifts.

10% Off Your 1st Order

SIGN UP TO RECEIVE YOUR WELCOME DISCOUNT - PLUS GET NOTIFIED OF NEW PRODUCTS - SALES & EXCLUSIVE OFFERS

We don’t spam! Read our privacy policy for more info.

Additional information

Color

Light wood base

Plug Type

AU PLUG

Reviews

There are no reviews yet.

Only logged in customers who have purchased this product may leave a review.

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); })();
1
YOUR CART