1/** 2 * beehome-forms.js â zero-dependency HubSpot form replacement. 3 * 4 * Renders native HTML forms from /assets/forms/catalog.json and posts them 5 * directly to HubSpot's public Forms API v3 (no auth, CORS-enabled). 6 * 7 * Usage: add <div data-hs-form="FORM_ID" data-page-name="..." 8 * data-redirect="..."></div> and include this script. 9 * Uses HubSpot's standard class names (.hs-form, .hs-input, .hs-button) 10 * so existing site CSS still styles the form correctly. 11 */ 12(function () { 13 "use strict"; 14 15 var CATALOG_URL = "/assets/forms/catalog.json"; 16 // API Gateway â Lambda â api.beehomecro.net (HMAC sign server-side). 17 // Lambda aceita o formato HubSpot v3 (fields/context/legalConsentOptions) e mapeia 18 // para o schema CRM Beehome (top-level + attribution + extras). 19 var API_BASE = "https://7hhwl55d9d.execute-api.us-west-2.amazonaws.com/"; 20 var catalogPromise = null; 21 22 // âââ Utilities ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 23 function el(tag, attrs, children) { 24 var e = document.createElement(tag); 25 if (attrs) { 26 for (var k in attrs) { 27 if (k === "class") e.className = attrs[k]; 28 else if (k === "html") e.innerHTML = attrs[k]; 29 else if (k === "text") e.textContent = attrs[k]; 30 else if (attrs[k] !== null && attrs[k] !== false && attrs[k] !== undefined) { 31 e.setAttribute(k, attrs[k]); 32 } 33 } 34 } 35 (children || []).forEach(function (c) { 36 if (c == null) return; 37 if (typeof c === "string") e.appendChild(document.createTextNode(c)); 38 else e.appendChild(c); 39 }); 40 return e; 41 } 42 43 function decodeEntities(s) { 44 if (!s) return ""; 45 var tx = document.createElement("textarea"); 46 tx.innerHTML = s; 47 return tx.value; 48 } 49 50 function loadCatalog() { 51 if (!catalogPromise) { 52 catalogPromise = fetch(CATALOG_URL, { credentials: "omit", cache: "no-cache" }) 53 .then(function (r) { 54 if (!r.ok) throw new Error("Failed to load catalog: " + r.status); 55 return r.json(); 56 }); 57 } 58 return catalogPromise; 59 } 60 61 function getCookie(name) { 62 var m = document.cookie.match("(^|;)\\s*" + name + "=([^;]+)"); 63 return m ? decodeURIComponent(m[2]) : null; 64 } 65 66 // âââ Field rendering ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 67 function renderField(field, formId) { 68 var fieldTypeClass = "hs-fieldtype-" + String(field.type || "text").replace(/_/g, "-"); 69 var wrapper = el("div", { class: "hs-form-field " + fieldTypeClass }); 70 var inputId = "bh_" + formId.slice(0, 8) + "_" + field.name; 71 var type = field.type; 72 73 // single_checkbox renders inline (checkbox + label text to the right) â 74 // no separate hs-form-label. Mirrors HubSpot's booleancheckbox markup. 75 if (type === "single_checkbox" && !field.hidden) { 76 wrapper.className = "hs-form-field hs-fieldtype-booleancheckbox"; 77 var inputWrap = el("div", { class: "input" }); 78 var ul = el("ul", { class: "inputs-list" }); 79 var li = el("li", { class: "hs-form-booleancheckbox" }); 80 var cbLabel = el("label", { for: inputId, class: "hs-form-booleancheckbox-display" }); 81 var cb = el("input", { 82 id: inputId, type: "checkbox", value: "true", 83 name: field.name, class: "hs-input", 84 required: field.required ? "required" : null, 85 }); 86 var labelTxt = decodeEntities(field.label) || field.name; 87 cbLabel.appendChild(cb); 88 var spanTxt = el("span", {}, [labelTxt]); 89 if (field.required) { 90 spanTxt.appendChild(el("span", { class: "hs-form-required", "aria-label": "required" }, ["*"])); 91 } 92 cbLabel.appendChild(spanTxt); 93 li.appendChild(cbLabel); 94 ul.appendChild(li); 95 inputWrap.appendChild(ul); 96 wrapper.appendChild(inputWrap); 97 if (field.description) { 98 wrapper.appendChild(el("p", { class: "hs-field-desc" }, [decodeEntities(field.description)])); 99 } 100 wrapper.appendChild(el("div", { class: "hs-error-msgs", role: "alert", "aria-live": "polite" })); 101 return wrapper; 102 } 103 104 if (!field.hidden) { 105 var label = el("label", { for: inputId, class: "hs-form-label" }, [ 106 decodeEntities(field.label) || field.name, 107 ]); 108 if (field.required) { 109 label.appendChild(el("span", { class: "hs-form-required", "aria-label": "required" }, ["*"])); 110 } 111 wrapper.appendChild(label); 112 } 113 114 var input; 115 var common = { 116 id: inputId, 117 name: field.name, 118 class: "hs-input", 119 placeholder: field.placeholder || "", 120 }; 121 if (field.required) common.required = "required"; 122 if (field.default) common.value = field.default; 123 124 if (type === "single_line_text") { 125 input = el("input", Object.assign({ type: "text" }
125, common)); 126 } else if (type === "email") { 127 input = el("input", Object.assign({ type: "email", autocomplete: "email" }, common)); 128 } else if (type === "phone") { 129 input = el("input", Object.assign({ type: "tel", autocomplete: "tel", inputmode: "tel" }, common)); 130 if (field.phoneDigits) { 131 if (field.phoneDigits.min) input.setAttribute("data-min-digits", field.phoneDigits.min); 132 if (field.phoneDigits.max) input.setAttribute("data-max-digits", field.phoneDigits.max); 133 } 134 } else if (type === "number") { 135 input = el("input", Object.assign({ type: "number", inputmode: "numeric" }, common)); 136 } else if (type === "multi_line_text") { 137 input = el("textarea", Object.assign({ rows: "4" }, common)); 138 } else if (type === "dropdown") { 139 input = el("select", common); 140 input.appendChild(el("option", { value: "" }, ["Selecione..."])); 141 (field.options || []).forEach(function (opt) { 142 input.appendChild(el("option", { value: opt.value }, [decodeEntities(opt.label)])); 143 }); 144 } else if (type === "single_checkbox") { 145 // Hidden single_checkbox falls back here (no user-facing UI, just an input) 146 input = el("input", Object.assign({ type: "checkbox", value: "true" }, common)); 147 } else if (type === "multiple_checkboxes") { 148 input = el("div", { class: "hs-multi-checkboxes" }); 149 (field.options || []).forEach(function (opt, i) { 150 var optId = inputId + "_" + i; 151 input.appendChild( 152 el("label", { class: "hs-checkbox-row" }, [ 153 el("input", { type: "checkbox", name: field.name, value: opt.value, id: optId }), 154 " " + decodeEntities(opt.label), 155 ]) 156 ); 157 }); 158 } else if (type === "datepicker") { 159 input = el("input", Object.assign({ type: "date" }, common)); 160 } else if (type === "file") { 161 input = el("input", Object.assign({ type: "file" }, common)); 162 } else { 163 input = el("input", Object.assign({ type: "text" }, common)); 164 } 165 166 if (field.hidden) input.setAttribute("type", "hidden"); 167 168 wrapper.appendChild(input); 169 170 if (field.description) { 171 wrapper.appendChild(el("p", { class: "hs-field-desc" }, [decodeEntities(field.description)])); 172 } 173 wrapper.appendChild(el("div", { class: "hs-error-msgs", role: "alert", "aria-live": "polite" })); 174 175 return wrapper; 176 } 177 178 // âââ Client-side validation âââââââââââââââââââââââââââââââââââââââââââââââ 179 function validateField(field, value, element) { 180 if (field.required) { 181 if (value == null || value === "" || (Array.isArray(value) && value.length === 0)) { 182 return "Campo obrigatório"; 183 } 184 } 185 if (value && field.type === "email") { 186 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return "Email inválido"; 187 if (field.blockedEmailDomains && field.blockedEmailDomains.length) { 188 var lower = value.toLowerCase(); 189 for (var i = 0; i < field.blockedEmailDomains.length; i++) { 190 var dom = field.blockedEmailDomains[i].toLowerCase(); 191 if (lower.endsWith("@" + dom)) return "Por favor, use um email corporativo"; 192 } 193 } 194 } 195 if (value && field.type === "phone" && field.phoneDigits) { 196 var digits = String(value).replace(/\D/g, ""); 197 var min = field.phoneDigits.min, max = field.phoneDigits.max; 198 if (min && digits.length < min) return "Telefone muito curto (mÃn " + min + " dÃgitos)"; 199 if (max && digits.length > max) return "Telefone muito longo (máx " + max + " dÃgitos)"; 200 } 201 return null; 202 } 203 204 function setFieldError(wrapper, msg) { 205 var box = wrapper.querySelector(".hs-error-msgs"); 206 if (!box) return; 207 box.innerHTML = ""; 208 if (msg) box.appendChild(el("label", { class: "hs-error-msg" }, [msg])); 209 } 210 211 // Default texts used when HubSpot's form definition omits them (runtime fills 212 // these in prod from templates; the exports API does not include them). 213 var PROCESSING_CONSENT_DEFAULT_TEXT = 214 "Para fornecer o conteúdo solicitado, precisamos armazenar e processar seus dados pessoais. " +
215 "Se você consentir com o armazenamento dos seus dados pessoais para essa finalidade, marque a caixa de seleção abaixo."; 216 var PROCESSING_CONSENT_DEFAULT_LABEL = 217 "Eu concordo em permitir que a Beehome armazene e processe meus dados pessoais."; 218 219 // âââ Form rendering + submit ââââââââââââââââââââââââââââââââââââââââââââââ 220 function applyFormColors(form, formDef) { 221 // Label / legal-consent text colors come from the HubSpot form config â 222 // critical for the WhatsApp modal (white-on-dark). Submit-button colors 223 // are driven by the site's theme CSS, not form config (prod enforces 224 // yellow globally, overriding whatever HubSpot config says). 225 if (formDef.labelTextColor) form.style.setProperty("--bh-label-color", formDef.labelTextColor); 226 if (formDef.helpTextColor) form.style.setProperty("--bh-help-color", formDef.helpTextColor); 227 if (formDef.legalConsentTextColor) 228 form.style.setProperty("--bh-legal-color", formDef.legalConsentTextColor); 229 } 230 231 function renderLegalConsent(formDef) { 232 var lco = formDef.legalConsentOptions; 233 if (!lco) return null; 234 var fieldset = el("fieldset", { class: "form-columns-1" }); 235 var wrap = el("div", { class: "hs-legal-consent-container legal-consent-container" }); 236 237 if (lco.communicationConsentText) { 238 wrap.appendChild(el("div", { 239 class: "hs-richtext hs-main-font-element", 240 html: decodeEntities(lco.communicationConsentText), 241 })); 242 } 243 244 (lco.communicationsCheckboxes || []).forEach(function (cb) { 245 var id = "bh_" + formDef.id.slice(0, 8) + "_comm_" + (cb.subscriptionTypeId || "x"); 246 var labelHtml = decodeEntities(cb.label || ""); 247 var row = el("div", { class: "hs-form-booleancheckbox-display" }); 248 var lbl = el("label", { for: id, class: "hs-form-booleancheckbox-display-label" }); 249 lbl.appendChild(el("input", { 250 id: id, 251 type: "checkbox", 252 name: "LEGAL_CONSENT.subscription_type_" + cb.subscriptionTypeId, 253 value: "true", 254 required: cb.required ? "required" : null, 255 class: "hs-input", 256 "data-consent-subscription": cb.subscriptionTypeId, 257 })); 258 lbl.appendChild(el("span", { html: labelHtml })); 259 row.appendChild(lbl); 260 wrap.appendChild(row); 261 }); 262 263 if (lco.type === "explicit_consent_to_process") { 264 var processingText = (lco.processingConsentText || "").replace(/<[^>]+>/g, "").trim() 265 || PROCESSING_CONSENT_DEFAULT_TEXT; 266 wrap.appendChild(el("div", { class: "hs-richtext" }, [processingText])); 267 268 var processingLabel = (lco.processingConsentCheckboxLabel || "").replace(/<[^>]+>/g, "").trim() 269 || PROCESSING_CONSENT_DEFAULT_LABEL; 270 // Checkbox de "processamento de dados" NÃO impeditivo: o lead é capturado 271 // mesmo desmarcado. O valor real (true/false) vai pro CRM para auditoria 272 // â time de marketing decide o que fazer com leads sem consent. 273 var processId = "bh_" + formDef.id.slice(0, 8) + "_processing"; 274 var row = el("div", { class: "hs-form-booleancheckbox-display" }); 275 var lbl = el("label", { for: processId, class: "hs-form-booleancheckbox-display-label" }); 276 lbl.appendChild(el("input", { 277 id: processId, 278 type: "checkbox", 279 name: "LEGAL_CONSENT.processing", 280 value: "true", 281 class: "hs-input", 282 })); 283 var span = el("span", {}, [processingLabel]); 284 lbl.appendChild(span); 285 row.appendChild(lbl); 286 wrap.appendChild(row); 287 } else if (lco.processingConsentText) { 288 wrap.appendChild(el("div", { 289 class: "hs-richtext hs-main-font-element", 290 html: decodeEntities(lco.processingConsentText), 291 })); 292 } 293 294 fieldset.appendChild(wrap); 295 return fieldset; 296 } 297 298 function buildForm(formDef, target, meta) { 299 target.classList.add("hs-form", "beehome-form"); 300 target.setAttribute("data-bh-rendered", "1"); 301 target.innerHTML = ""; 302 303 var form = el("form", { 304 class: "hs-form-private", 305 novalidate: "novalidate", 306 action: "#", 307 }); 308 applyFormColors(form, formDef); 309 310 var groups = (formDef.groups && formDef.groups.length) ? formDef.groups : null; 311 if (groups) { 312 groups.forEach(function (g) { 313 var gFields = g.fields || [];
314 var fieldset = el("fieldset", { class: "form-columns-" + gFields.length }); 315 if (g.richText) { 316 fieldset.appendChild(el("div", { 317 class: "hs-richtext hs-main-font-element", 318 html: g.richText, 319 })); 320 } 321 gFields.forEach(function (field) { 322 fieldset.appendChild(renderField(field, formDef.id)); 323 }); 324 form.appendChild(fieldset); 325 }); 326 } else { 327 // Fallback: flat fields (legacy catalogs without groups) 328 (formDef.fields || []).forEach(function (field) { 329 var fieldset = el("fieldset", { class: "form-columns-1" }); 330 fieldset.appendChild(renderField(field, formDef.id)); 331 form.appendChild(fieldset); 332 }); 333 } 334 335 var legal = renderLegalConsent(formDef); 336 if (legal) form.appendChild(legal); 337 338 var submit = el("div", { class: "hs-submit" }, [ 339 el("input", { 340 type: "submit", 341 class: "hs-button primary large", 342 value: formDef.submitButtonText || "Enviar", 343 }), 344 ]); 345 form.appendChild(submit); 346 347 var feedback = el("div", { class: "beehome-form-feedback", role: "status", "aria-live": "polite" }); 348 form.appendChild(feedback); 349 350 form.addEventListener("submit", function (ev) { 351 ev.preventDefault(); 352 handleSubmit(form, formDef, target, meta, feedback); 353 }); 354 355 target.appendChild(form); 356 } 357 358 function collectValues(form, formDef) { 359 var errors = []; 360 var values = []; 361 362 formDef.fields.forEach(function (field) { 363 // bh_* fields are populated post-collect by the tracker (BeehomeTrack 364 // snapshot). Skip them here to avoid duplicate empty entries. 365 if (field.name && field.name.indexOf("bh_") === 0) return; 366 var wrapper = form.querySelector('[name="' + CSS.escape(field.name) + '"]'); 367 if (!wrapper) return; 368 var fieldWrapper = wrapper.closest(".hs-form-field"); 369 var v; 370 371 if (field.type === "single_checkbox") { 372 v = wrapper.checked ? "true" : ""; 373 } else if (field.type === "multiple_checkboxes") { 374 var checked = form.querySelectorAll('input[name="' + CSS.escape(field.name) + '"]:checked'); 375 v = Array.prototype.map.call(checked, function (c) { return c.value; }).join(";"); 376 } else if (field.type === "file") { 377 v = wrapper.files && wrapper.files[0] ? wrapper.files[0].name : ""; 378 } else { 379 v = wrapper.value; 380 } 381 382 var err = validateField(field, v, wrapper); 383 if (fieldWrapper) setFieldError(fieldWrapper, err); 384 if (err) { 385 errors.push({ field: field.name, message: err }); 386 } 387 388 values.push({ 389 objectTypeId: field.objectTypeId || "0-1", 390 name: field.name, 391 value: v == null ? "" : String(v), 392 }); 393 }); 394 395 return { errors: errors, values: values }; 396 } 397 398 // âââ Google Ads conversion tracking ââââââââââââââââââââââââââââââââââââââ 399 // TODO: substituir CONVERSION_LABEL quando Beatriz criar 400 // a action "[BEEHOME] Lead Form" no Google Ads UI 401 var GADS_CONVERSION_ID = "AW-708191396"; 402 var GADS_CONVERSION_LABEL = "CuSbCNaK6LEcEKTJ2NEC"; 403 404 function fireGadsConversion(form, formDef) { 405 if (typeof gtag !== "function") return; 406 if (GADS_CONVERSION_LABEL === "__REPLACE_ME__") return; // placeholder â skip until configured 407 408 // Collect email + phone for Enhanced Conversions for Leads 409 var emailInput = form.querySelector('input[name="email"]'); 410 var phoneInput = form.querySelector('input[name="phone"]'); 411 var userData = {}; 412 if (emailInput && emailInput.value) userData.email = emailInput.value.trim(); 413 if (phoneInput && phoneInput.value) userData.phone_number = phoneInput.value.trim(); 414 415 if (Object.keys(userData).length) { 416 gtag("set", "user_data", userData); 417 } 418 419 // Transaction ID = visitor ID (dedup) or random fallback 420 var txId = ""; 421 try { 422 if (window.BeehomeTrack && typeof window.BeehomeTrack.snapshot === "function") { 423 txId = window.BeehomeTrack.snapshot().bh_visitor_id || ""; 424 } 425 } catch (e) {} 426 if (!txId) txId = "f_" + Date.now().toString(36); 427 428 gtag("event", "conversion", { 429 send_to: GADS_CONVERSION_ID + "/" + GADS_CONVERSION_LABEL, 430 transaction_id: txId, 431 }); 432 } 433 434 function handleSubmit(form, formDef, target, meta, feedback) { 435 feedback.className = "beehome-form-feedback"; 436 feedback.textContent = ""; 437 438 var collected = collectValues(form, formDef); 439 if (collected.errors.length) { 440 feedback.className = "beehome-form-feedback error"; 441 feedback.textContent = "Corrija os campos destacados e tente novamente."; 442 // Focus first errored input 443 var first = form.querySelector(".hs-error-msg"); 444 if (first && first.closest(".hs-form-field")) { 445 var input = first.closest(".hs-form-field").querySelector(".hs-input"); 446 if (input) input.focus(); 447 } 448 return; 449 } 450 451 var submitBtn = form.querySelector('input[type="submit"]'); 452 if (submitBtn) { 453 submitBtn.disabled = true; 454 submitBtn.setAttribute("data-original-text", submitBtn.value); 455 submitBtn.value = "Enviandoâ¦"; 456 } 457 458 // Separar LEGAL_CONSENT.* dos fields + montar legalConsentOptions 459 var realFields = []; 460 var consentComms = []; 461 var consentProcess = null; 462 var lco = formDef.legalConsentOptions || {};
463 collected.values.forEach(function (v) { 464 if (v.name && v.name.indexOf("LEGAL_CONSENT.subscription_type_") === 0) { 465 var subId = parseInt(v.name.split("_").pop(), 10); 466 var cbDef = ((lco.communicationsCheckboxes || []).find(function (c) { 467 return c.subscriptionTypeId === subId; 468 }) || {}); 469 consentComms.push({ 470 value: v.value === "true", 471 subscriptionTypeId: subId, 472 text: (cbDef.label || "").replace(/<[^>]+>/g, "").trim(), 473 }); 474 } else if (v.name === "LEGAL_CONSENT.processing") { 475 consentProcess = v.value === "true"; 476 } else { 477 realFields.push(v); 478 } 479 }); 480 481 // Pull first-party attribution snapshot from BeehomeTrack (if loaded). 482 // HubSpot's Forms API v3 rejects the entire submission when any field 483 // is not declared on the form (FIELD_NOT_IN_FORM_DEFINITION / 400), so 484 // we can only attach attribution fields that exist on this form's 485 // schema. Enable per-form by adding a hidden field named bh_* to the 486 // form in HubSpot; the property must also exist on the contact object. 487 var track = {}; 488 try { 489 if (window.BeehomeTrack && typeof window.BeehomeTrack.snapshot === "function") { 490 track = window.BeehomeTrack.snapshot() || {}; 491 } 492 } catch (e) { track = {}; } 493 // Anteriormente filtrávamos bh_* pelos fields declarados no formDef 494 // (HubSpot Forms v3 API rejeita FIELD_NOT_IN_FORM_DEFINITION). Como 495 // agora postamos pro Lambda proxy (que mapeia bh_* â attribution.*), 496 // o constraint mudou: sempre incluÃmos os bh_* â o Lambda decide o 497 // que fazer com eles. Isso garante attribution mesmo em forms que 498 // não receberam os hidden fields no HubSpot (ex. Forms CS bloqueado 499 // por v4 'too many fields in group'). 500 Object.keys(track).forEach(function (name) { 501 realFields.push({ objectTypeId: "0-1", name: name, value: String(track[name]) }); 502 }); 503 504 var payload = { 505 fields: realFields, 506 context: { 507 pageUri: window.location.href, 508 pageName: meta.pageName || document.title, 509 formId: formDef.id, 510 formName: formDef.name, 511 }, 512 skipValidation: false, 513 }; 514 var hutk = getCookie("hubspotutk"); 515 if (hutk) payload.context.hutk = hutk; 516 517 // Só enviar legalConsentOptions se houver algum consent config 518 if (lco.type && (consentComms.length || consentProcess !== null)) { 519 var consentWrap = {}; 520 if (lco.type === "explicit_consent_to_process") { 521 consentWrap.consent = { 522 consentToProcess: consentProcess === true, 523 text: (lco.processingConsentText || "").replace(/<[^>]+>/g, "").trim(), 524 communications: consentComms, 525 }; 526 } else { 527 // legitimate_interest or implicit 528 consentWrap.legitimateInterest = { 529 value: true, 530 subscriptionTypeId: consentComms[0] ? consentComms[0].subscriptionTypeId : 999, 531 legalBasis: "LEAD", 532 text: (lco.communicationConsentText || "").replace(/<[^>]+>/g, "").trim(), 533 }; 534 } 535 payload.legalConsentOptions = consentWrap; 536 } 537 538 // Proxy sem path-args â formId vai no payload.context.formId (Lambda lê de lá). 539 var url = API_BASE; 540 541 fetch(url, { 542 method: "POST", 543 headers: { "Content-Type": "application/json" }, 544 body: JSON.stringify(payload), 545 }) 546 .then(function (resp) { 547 return resp.json().then(function (body) { 548 return { ok: resp.ok, status: resp.status, body: body }; 549 }); 550 }) 551 .then(function (result) { 552 if (!result.ok) { 553 var msg = (result.body && (result.body.message || (result.body.errors && result.body.errors[0] && result.body.errors[0].message))) || 554 "Erro ao enviar o formulário (" + result.status + ")"; 555 feedback.className = "beehome-form-feedback error"; 556 feedback.textContent = msg; 557 if (submitBtn) { 558 submitBtn.disabled = false; 559 submitBtn.value = submitBtn.getAttribute("data-original-text") || "Enviar"; 560 } 561 return; 562 } 563 // Success â fire Google Ads conversion event 564 fireGadsConversion(form, formDef); 565 566 var redirect = meta.redirect || (formDef.postSubmit && formDef.postSubmit.type === "redirect_url" && formDef.postSubmit.value); 567 if (redirect) { 568 window.location.href = redirect; 569 return; 570 } 571 var message = 572 (result.body && result.body.inlineMessage) || 573 (formDef.postSubmit && formDef.postSubmit.type === "thank_you" && formDef.postSubmit.value) ||
574 "Formulário enviado com sucesso. Obrigado!"; 575 target.innerHTML = '<div class="beehome-form-success hs-richtext">' + message + "</div>"; 576 }) 577 .catch(function (err) { 578 feedback.className = "beehome-form-feedback error"; 579 feedback.textContent = "Erro de conexão. Verifique sua internet e tente novamente."; 580 if (submitBtn) { 581 submitBtn.disabled = false; 582 submitBtn.value = submitBtn.getAttribute("data-original-text") || "Enviar"; 583 } 584 }); 585 } 586 587 // âââ Discovery + boot âââââââââââââââââââââââââââââââââââââââââââââââââââââ 588 function mountAll() { 589 var targets = document.querySelectorAll("[data-hs-form]:not([data-bh-rendered])"); 590 if (!targets.length) return; 591 loadCatalog().then(function (catalog) { 592 targets.forEach(function (target) { 593 var formId = target.getAttribute("data-hs-form"); 594 var def = catalog.forms[formId]; 595 if (!def) { 596 target.innerHTML = '<div class="beehome-form-error">Formulário não encontrado: ' + formId + "</div>"; 597 return; 598 } 599 var meta = { 600 portalId: catalog.portalId,
601 region: catalog.region, 602 pageName: target.getAttribute("data-page-name") || document.title, 603 redirect: target.getAttribute("data-redirect") || null, 604 }; 605 buildForm(def, target, meta); 606 }); 607 }).catch(function (err) { 608 targets.forEach(function (target) { 609 target.innerHTML = '<div class="beehome-form-error">Falha ao carregar formulário: ' + (err && err.message || err) + "</div>"; 610 }); 611 }); 612 } 613 614 // Expose small API for programmatic use 615 window.BeehomeForms = { 616 mountAll: mountAll, 617 render: function (opts) { 618 // opts = { formId, target, pageName, redirect } 619 var node = typeof opts.target === "string" ? document.querySelector(opts.target) : opts.target; 620 if (!node) throw new Error("target not found: " + opts.target); 621 node.setAttribute("data-hs-form", opts.formId); 622 if (opts.pageName) node.setAttribute("data-page-name", opts.pageName); 623 if (opts.redirect) node.setAttribute("data-redirect", opts.redirect); 624 mountAll(); 625 }, 626 }; 627 628 if (document.readyState === "loading") { 629 document.addEventListener("DOMContentLoaded", mountAll); 630 } else { 631 mountAll(); 632 } 633})();
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.