PageSourceSearch

https://beehome.company/assets/forms/beehome-forms.js

js beehome.company collected 2026-10-02 14:40:28 UTC 26,138 bytes, 633 lines download raw bytes

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.