PageSourceSearch

https://www.ephome.org/js/layout.js?v=af9

js ephome.org collected 2026-10-09 03:21:33 UTC 13,951 bytes, 278 lines download raw bytes

1/* Shared header and footer, injected on every page.
2   Edit the nav or footer HERE once and every page updates.
3   Text labels are translated via js/translations.js (data-i18n keys). */
4
5/* HOME logo — official star-of-houses mark (img/home-logo-mark.png,
6   cropped from ephome.org's logo.png; full lockup in img/home-logo.png). */
7const LOGO_IMG = `<img src="img/home-logo-mark.png" alt="HOME — five colorful houses forming a star">`;
8
9const SITE_HEADER = `
10  <div class="utility">
11    <div class="wrap">
12      <a class="utility-contact" href="contact.html"><svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M6.62 10.79a15.05 15.05 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.25c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.61 21 3 13.39 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.24.2 2.45.57 3.57a1 1 0 0 1-.25 1.02l-2.2 2.2z"/></svg> <span data-i18n="nav.contact">Contact</span></a>
13      <div class="links">
14        <div class="a11y-tools" role="group" aria-label="Accessibility tools / Herramientas de accesibilidad">
15          <button type="button" class="font-btn" id="font-dec" aria-label="Smaller text / Texto más pequeño">A−</button>
16          <button type="button" class="font-btn" id="font-inc" aria-label="Larger text / Texto más grande">A+</button>
17          <button type="button" id="tts-btn" aria-label="Listen to this page / Escuchar esta página">🔊 <span data-i18n="a11y.listen">Listen</span></button>
18          <button type="button" id="hc-btn" aria-pressed="false" aria-label="High contrast / Alto contraste">◐ <span data-i18n="a11y.contrast">High Contrast</span></button>
19        </div>
20        <a href="#" id="lang-toggle" lang="es">Español</a>
21      </div>
22    </div>
23  </div>
24  <header class="site">
25    <div class="wrap">
26      <a class="brand" href="index.html">
27        <span class="mark">${LOGO_IMG}</span>
28        <span class="name">
29          <strong>HOME</strong>
30          <span>Housing Opportunity Management Enterprises <em>· El Paso, TX</em></span>
31        </span>
32      </a>
33      <nav class="primary" aria-label="Main navigation">
34        <a href="about.html" data-i18n="nav.about">About</a>
35        <a href="index.html#programs" data-i18n="nav.programs">Programs</a>
36        <a href="notices.html" data-i18n="nav.notices">Public Notices</a>
37        <a href="landlords.html" data-i18n="nav.landlords">Landlords</a>
38        <a href="contact.html" data-i18n="nav.contact">Contact</a>
39      </nav>
40    </div>
41  </header>`;
42
43const SITE_FOOTER = `
44  <footer class="site">
45    <div class="wrap">
46      <div class="cols">
47        <div>
48          <h4>HOME — Housing Opportunity Management Enterprises</h4>
49          <p data-i18n="footer.tagline">Proudly providing safe, decent, and affordable housing for El Pasoans since 1938.</p>
50          <p style="margin-top:12px;"><span data-i18n="footer.follow">Follow us:</span> <a href="https://www.facebook.com/homeeptx" target="_blank" rel="noopener" style="display:inline;">Facebook</a> · <a href="https://www.instagram.com/homeeptx" target="_blank" rel="noopener" style="display:inline;">Instagram</a> · <a href="https://www.linkedin.com/company/homeeptx" target="_blank" rel="noopener" style="display:inline;">LinkedIn</a> · <a href="https://www.youtube.com/@homeeptx" target="_blank" rel="noopener" style="display:inline;">YouTube</a></p>
51        </div>
52        <div>
53          <h4 data-i18n="footer.residents">Residents</h4>
54          <a href="apply.html" data-i18n="footer.apply">Apply for Housing</a>
55          <a href="participant.html" data-i18n="footer.residentPortal">Resident Portal</a>
56          <a href="voucher.html" data-i18n="footer.voucher">Voucher Program</a>
57          <a href="affordable-housing.html" data-i18n="footer.affordable">Affordable Housing</a>
58          <a href="fss.html" data-i18n="footer.fss">Family Self-Sufficiency</a>
59        </div>
60        <div>
61          <h4 data-i18n="footer.partners">Partners</h4>
62          <a href="https://landlord-home.ephome.org/sign-in-otp" target="_blank" rel="noopener" data-i18n="footer.landlordPortal">Landlord Portal</a>
63          <a href="business.html" data-i18n="footer.procurement">Procurement</a>
64          <a href="employment.html" data-i18n="footer.employment">Employment</a>
65        </div>
66        <div>
67          <h4 data-i18n="footer.agency">Agency</h4>
68          <a href="about.html" data-i18n="nav.about">About</a>
69          <a href="notices.html" data-i18n="footer.notices">Public Notices</a>
70          <a href="contact.html#fraud" data-i18n="footer.fraud">Report Fraud</a>
71        </div>
72      </div>
73      <div class="legal">
74        <span data-i18n="footer.rights">© 2026 Housing Opportunity Management Enterprises. All rights reserved.</span>
75        <span data-i18n="footer.legal">Equal Housing Opportunity ⌂ &nbsp;·&nbsp; Accessibility &nbsp;·&nbsp; Privacy</span>
76      </div>
77    </div>
78  </footer>`;
79
80/* "Join the Virtual Board Meeting" panel — injected wherever a page has
81   <div data-panel="board-meeting"></div>. Update the link/number here once. */
82const BOARD_MEETING_PANEL = `
83  <div class="board-panel">
84    <div class="bp-info">
85      <span class="bp-kicker" data-i18n="boardMeet.kicker">Open to the Public</span>
86      <h3 data-i18n="boardMeet.title">Join the Virtual Board Meeting</h3>
87      <p class="bp-when">🗓️ <span data-i18n="boardMeet.when">Third Wednesday of every month · 5:00–6:00 PM Mountain Time</span></p>
88      <p class="bp-desc" data-i18n="boardMeet.desc">Every board meeting is open to the public — attend in person at the Blue Flame Building, 304 Texas Ave., 17th Floor, or join online from anywhere. Agendas are posted under Board Meeting notices before each meeting.</p>
89    </div>
90    <div class="bp-join">
91      <a class="btn bp-btn" href="https://housingopportunitymanagemententerprises.webex.com/meet/boardmeetings" target="_blank" rel="noopener">🎥 <span data-i18n="boardMeet.join">Join via WebEx</span></a>
92      <div class="bp-dial">
93        <div>📞 <span data-i18n="boardMeet.phone">Or call:</span> <a href="tel:+16504793208">+1 (650) 479-3208</a></div>
94        <div><span data-i18n="boardMeet.code">Access code:</span> <strong>2663 394 1998</strong></div>
95      </div>
96    </div>
97  </div>`;
98
99(function injectLayout() {
100  const headerSlot = document.getElementById("site-header");
101  const footerSlot = document.getElementById("site-footer");
102  if (headerSlot) headerSlot.outerHTML = SITE_HEADER;
103  if (footerSlot) footerSlot.outerHTML = SITE_FOOTER;
104  document.querySelectorAll('[data-panel="board-meeting"]').forEach((el) => {
105    // Forward extra classes from the placeholder (e.g. "demure") onto the panel
106    const extra = el.className ? " " + el.className : "";
107    el.outerHTML = BOARD_MEETING_PANEL.replace('class="board-panel"', `class="board-panel${extra}"`);
108  });
109
110  // Use the logo as the browser-tab favicon on every page.
111  const fav = document.createElement("link");
112  fav.rel = "icon";
113  fav.type = "image/png";
114  fav.href = "img/home-logo-mark.png";
115  document.head.appendChild(fav);
116
117  // Highlight the nav link for the page you're on.
118  const page = location.pathname.split("/").pop() || "index.html";
119  document.querySelectorAll("nav.primary a").forEach((a) => {
120    const href = a.getAttribute("href");
121    if (href.includes("#")) return; // section links aren't a "current page"
122    if (href === page && !a.classList.contains("cta")) a.style.background = "#eaf1f9";
123  });
124})();
125
126/* ---------- Mia chat widget (RespondIT) ----------
127   The same embed the applicant portal uses: one script that mounts its own
128   launcher and loads its stylesheet. The widget reads the language once at
129   load, so a language toggle reaches it on the next page view. */
130const CHAT_WIDGET_URL = "https://dms-chat-widget-production.kruko.co/entry-ui.iife.js";
131(function loadChatWidget() {
132  if (document.getElementById("chat-widget-script")) return;
133  let lang = "en";
134  try { lang = localStorage.getItem("home-lang") || "en"; } catch (e) { /* storage blocked: English */ }
135  // Locale codes are the CRM's (LANGUAGE_LABELS): es-mx for the Spanish site.
136  window.__CHAT_WIDGET_CONFIG = { locale: lang === "es" ? "es-mx" : "en-us" };
137  const s = document.createElement("script");
138  s.id = "chat-widget-script";
139  s.src = CHAT_WIDGET_URL;
140  document.head.appendChild(s);
141})();
142
143/* ---------- Accessibility tools: text size, high contrast, read-aloud ---------- */
144(function a11yTools() {
145  const SIZES = [16, 18, 20, 22, 24]; // px for the root font; everything scales
146  const DEFAULT_STEP = 1;             // 18px
147
148  function step() {
149    const s = parseInt(localStorage.getItem("home-fontstep") ?? DEFAULT_STEP, 10);
150    return Math.min(Math.max(s, 0), SIZES.length - 1);
151  }
152  function applyPrefs() {
153    document.documentElement.style.fontSize = SIZES[step()] + "px";
154    const hc = localStorage.getItem("home-contrast") === "1";
155    document.documentElement.classList.toggle("hc", hc);
156    const hcBtn = document.getElementById("hc-btn");
157    if (hcBtn) hcBtn.setAttribute("aria-pressed", hc ? "true" : "false");
158  }
159  applyPrefs();
160
161  function bump(delta) {
162    localStorage.setItem("home-fontstep", String(step() + delta));
163    applyPrefs();
164  }
165  document.getElementById("font-dec")?.addEventListener("click", () => bump(-1));
166  document.getElementById("font-inc")?.addEventListener("click", () => bump(1));
167  document.getElementById("hc-btn")?.addEventListener("click", () => {
168    localStorage.setItem("home-contrast", localStorage.getItem("home-contrast") === "1" ? "0" : "1");
169    applyPrefs();
170  });
171
172  /* Read-aloud: speaks the page's main content with the browser's built-in
173     voice, in whichever language the site is currently showing. */
174  const ttsBtn = document.getElementById("tts-btn");
175  if (ttsBtn && "speechSynthesis" in window) {
176    let speaking = false;
177
178    function setLabel(key) {
179      const span = ttsBtn.querySelector("span");
180      if (span) span.textContent = (typeof t === "function") ? t(key) : (key === "a11y.stop" ? "Stop" : "Listen");
181    }
182    function stop() {
183      speechSynthesis.cancel();
184      speaking = false;
185      ttsBtn.firstChild.textContent = "🔊 ";
186      setLabel("a11y.listen");
187    }
188    function readablePageText() {
189      const parts = [];
190      document.querySelectorAll("section h1, section h2, section h3, section p, section li").forEach((el) => {
191        if (el.closest("nav, .a11y-tools, script")) return;
192        if (!el.offsetParent && el.offsetHeight === 0) return; // hidden
193        const text = el.innerText.trim();
194        if (text) parts.push(text);
195      });
196      return parts;
197    }
198
199    /* Pick the most natural voice the visitor's device offers for the
200       language. Devices differ (Siri on Apple, Microsoft on Windows,
201       Google in Chrome) — this ranks whatever is actually installed. */
202    speechSynthesis.getVoices(); // warm the async voice list
203    speechSynthesis.addEventListener?.("voiceschanged", () => speechSynthesis.getVoices());
204    function bestVoice(lang) {
205      const base = lang.split("-")[0].toLowerCase();
206      const candidates = speechSynthesis.getVoices().filter(
207        (v) => v.lang && v.lang.toLowerCase().startsWith(base)
208      );
209      if (!candidates.length) return null;
210      const score = (v) => {
211        const n = v.name.toLowerCase();
212        let s = 0;
213        if (/natural|neural|premium|enhanced/.test(n)) s += 8;
214        if (/siri/.test(n)) s += 6;
215        if (/google/.test(n)) s += 5;
216        if (/samantha|ava |allison|paulina|mónica|monica/.test(n)) s += 4;
217        if (v.lang.toLowerCase() === lang.toLowerCase()) s += 2;
218        if (v.localService) s += 1;
219        return s;
220      };
221      return candidates.sort((a, b) => score(b) - score(a))[0];
222    }
223
224    ttsBtn.addEventListener("click", () => {
225      if (speaking) { stop(); return; }
226      const lang = (typeof currentLang === "function" && currentLang() === "es") ? "es-MX" : "en-US";
227      const parts = readablePageText();
228      if (!parts.length) return;
229      speechSynthesis.cancel();
230      const voice = bestVoice(lang);
231      parts.forEach((text, i) => {
232        const u = new SpeechSynthesisUtterance(text);
233        u.lang = lang;
234        if (voice) u.voice = voice;
235        u.rate = 0.95;
236        if (i === parts.length - 1) u.onend = stop;
237        speechSynthesis.speak(u);
238      });
239      speaking = true;
240      ttsBtn.firstChild.textContent = "⏹ ";
241      setLabel("a11y.stop");
242    });
243    window.addEventListener("beforeunload", () => speechSynthesis.cancel());
244  } else if (ttsBtn) {
245    ttsBtn.style.display = "none"; // browser has no speech support
246  }
247})();
248
249/* Count stat numbers up from zero when they scroll into view.
250   Handles prefixes/suffixes ("1st", "52,000+") and comma grouping. */
251(function animateStats() {
252  const nums = document.querySelectorAll(".stat .num");
253  if (!nums.length || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
254
255  const io = new IntersectionObserver((entries) => {
256    entries.forEach((entry) => {
257      if (!entry.isIntersecting) return;
258      io.unobserve(entry.target);
259      const el = entry.target;
260      const m = el.textContent.trim().match(/^([^\d]*)([\d,]+)(.*)$/);
261      if (!m) return;
262      const target = parseInt(m[2].replace(/,/g, ""), 10);
263      const grouped = m[2].includes(",");
264      const duration = 1300;
265      const start = performance.now();
266      const tick = (now) => {
267        const p = Math.min((now - start) / duration, 1);
268        const eased = 1 - Math.pow(1 - p, 3); // fast start, gentle landing
269        const val = Math.round(target * eased);
270        el.textContent = m[1] + (grouped ? val.toLocaleString("en-US") : String(val)) + m[3];
271        if (p < 1) requestAnimationFrame(tick);
272      };
273      requestAnimationFrame(tick);
274    });
275  }, { threshold: 0.5 });
276
277  nums.forEach((n) => { if (!n.hasAttribute("data-i18n")) io.observe(n); });
278})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.