PageSourceSearch

https://clearleft.com/v6-assets/js/hs-newsletter.js

js clearleft.com collected 2026-10-02 02:40:08 UTC 5,120 bytes, 145 lines download raw bytes

1/*!
2 * hs-newsletter.js — Clearleft newsletter signup → HubSpot Forms API
3 * Portal 148482298 (EU hublet). One file, drops into every site unchanged.
4 *
5 * Markup contract — put these on the <form>:
6 *   data-hs-form                      marks the form for this script
7 *   data-hs-portal=""148482298""
8 *   data-hs-id=""<form GUID>""
9 *   data-hs-newsletter=""<value>""      writes the newsletter_subscriptions property
10 *
11 * And on each input you want submitted:
12 *   data-hs=""0-1/email""               ""<objectTypeId>/<internal name>""
13 *     0-1 = Contact,  0-2 = Company
14 *
15 * Adding a field later is a markup change only — no edit to this file.
16 * If a CSP is ever added to a site, connect-src must include api-eu1.hsforms.com
17 */
18(function () {
19  "use strict";
20
21  var ENDPOINT_BASE =
22    "https://api-eu1.hsforms.com/submissions/v3/integration/submit/";
23
24  function hutk() {
25    var m = document.cookie.match(/(^|;)\s*hubspotutk=([^;]*)/);
26    return m ? m[2] : undefined; // undefined keys are dropped by JSON.stringify
27  }
28
29  function textFrom(html) {
30    var d = document.createElement("div");
31    d.innerHTML = html || "";
32    return d.textContent.trim();
33  }
34
35  function init(form) {
36    var portal = form.dataset.hsPortal;
37    var formId = form.dataset.hsId;
38    var newsletter = form.dataset.hsNewsletter;
39    if (!portal || !formId) return;
40
41    var status = form.querySelector("[data-hs-status]");
42    var button = form.querySelector("button[type=submit], input[type=submit]");
43    var honey = form.querySelector("[data-hs-honeypot]");
44
45    function say(msg) {
46      if (status) status.textContent = msg;
47    }
48
49    form.addEventListener("submit", function (e) {
50      e.preventDefault();
51      if (honey && honey.value) return; // bot
52
53      var fields = [];
54      var missing = false;
55
56      form.querySelectorAll("[data-hs]").forEach(function (input) {
57        var parts = input.getAttribute("data-hs").split("/");
58        var value = (input.value || "").trim();
59        if (!value) {
60          if (input.required) missing = true;
61          return;
62        }
63        fields.push({ objectTypeId: parts[0], name: parts[1], value: value });
64      });
65
66      if (missing || !fields.length) {
67        say("Please fill in the required fields.");
68        return;
69      }
70
71      // Tag which newsletter this signup came from. Without this every brand's
72      // signups land in HubSpot indistinguishable from each other.
73      if (newsletter) {
74        fields.push({
75          objectTypeId: "0-1",
76          name: "newsletter_subscriptions",
77          value: newsletter,
78        });
79      }
80
81      if (button) button.disabled = true;
82      say("Subscribing…");
83
84      fetch(ENDPOINT_BASE + portal + "/" + formId, {
85        method: "POST",
86        headers: { "Content-Type": "application/json" },
87        body: JSON.stringify({
88          fields: fields,
89          context: {
90            hutk: hutk(),
91            pageUri: window.location.href,
92            pageName: document.title,
93          },
94
95          // ── CONSENT ─────────────────────────────────────────────────────
96          // GDPR is currently OFF in portal 148482298, so no consent record
97          // is stored. See README before deploying. When it is switched on,
98          // add here and surface matching copy/checkbox in the markup:
99          //
100          // ,legalConsentOptions: {
101          //   consent: {
102          //     consentToProcess: true,
103          //     text: 'I agree to allow Clearleft to store and process my personal data.',
104          //     communications: [{
105          //       value: true,
106          //       subscriptionTypeId: 0,   // real ID per brand
107          //       text: 'I agree to receive this newsletter.'
108          //     }]
109          //   }
110          // }
111          // ────────────────────────────────────────────────────────────────
112        }),
113      })
114        .then(function (res) {
115          return res.json().then(function (body) {
116            if (!res.ok) throw new Error(JSON.stringify(body));
117            return body;
118          });
119        })
120        .then(function (data) {
121          if (data.redirectUri) {
122            window.location.assign(data.redirectUri);
123            return;
124          }
125          say(textFrom(data.inlineMessage) || "Thanks — you’re on the list.");
126          var group = form.querySelector("[data-hs-fields]");
127          if (group) group.hidden = true;
128        })
129        .catch(function (err) {
130          if (window.console) console.error("[hs-newsletter]", err);
131          say("Sorry, something went wrong. Please try again.");
132          if (button) button.disabled = false;
133        });
134    });
135  }
136
137  function boot() {
138    document.querySelectorAll("[data-hs-form]").forEach(init);
139  }
140  if (document.readyState === "loading") {
141    document.addEventListener("DOMContentLoaded", boot);
142  } else {
143    boot();
144  }
145})();

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.